前置き
勉強のためにrubyで言語処理100本ノックをなるべく答えを見ずにやってみる・・・!
言語処理100本ノック 2015
http://www.cl.ecei.tohoku.ac.jp/nlp100/
言語処理100本ノックは,実践的な課題に取り組みながら,プログラミング,データ分析,研究のスキルを楽しく習得することを目指した問題集です
Gist
rubyで言語処理100本ノック · GitHub
※作業用のGistです、アドバイス等頂けるとありがたいです(´;ω;`)
第1章: 準備運動
00. 文字列の逆順
Q.文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.
A.これは単純にstring.reverseを使用しました。
p "stressed".reverse
01. 「パタトクカシーー」
Q.パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ. A.これは、文字列の奇数番目を取り出すようにしました。
str = "パタトクカシーー" str.length.times{ |n| p str[n] if n.even? }
02. 「パトカー」+「タクシー」=「パタトクカシーー」
Q.「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.
A.配列にそれぞれを入れて、各要素のn文字目の値を結合した値を設定する処理を文字数分行いました。
ans = "" words = %W(パトカー タクシー) 4.times{ |n| ans << (words[0][n] + words[1][n]) } p ans
03. 円周率
Q.“Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.
A.gsub
で”,“、”.“を空文字に置換して、split
を使って” “で配列に分割したあと、map
でそれぞれの要素の文字数を持つ配列を返すようにしました。
str = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics." p str.gsub(/,|\./,'').split(" ").map{ |s| s.length }
04. 元素記号
Q.“Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.
A.単語に分割後、指定したインデックスの要素の1文字目だけを格納した配列とそれ以外のインデックスの要素の1~2文字目を格納した配列を用意して、それらを結合後に昇順に並び替えて、ハッシュに変換した。なんだかイマイチな気がする。。。
str = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can." words = str.gsub(/,|\./,"").split( ) target = [0, 4, 5, 6, 7, 8, 14, 15, 18] words1 = words.map{ |n| [words.index(n),n[0]] if target.include?(words.index(n)) }.compact words2 = words.map{ |n| [words.index(n),n[0..1]] unless target.include?(words.index(n)) }.compact p words1.concat(words2).sort.to_h
05. n-gram
Q.与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.
A. 与えられた引数seq
が配列だった場合は、join
し文字列に変更する。chars
で一文字ずつに分割後["a","b","c"]
、それに対してeach_con(2)
で、重複ありで2文字に区切る。[["a","b"],["b","c"]
2文字に区切った要素をjoin
で結合し、返却した。["ab","bc"]
def to_ngram(seq) seq = seq.join if seq.instance_of?(Array) seq.chars.each_cons(2).map{|chars| chars.join } end p to_ngram("I am an NLPer") p to_ngram(%W(I am an NLPer))
06. 集合
Q.“paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.
A. bi-gramを出力するロジックは前回のものを流用。それぞれの結果をX・Yに格納し、&
で積集合、|
で和集合、include?
で'se'
が含まれるか判定した。
def to_ngram(seq) seq = seq.join if seq.instance_of?(Array) seq.chars.each_cons(2).map{|chars| chars.join } end X = to_ngram "paragraph" Y = to_ngram "paraparaparadise" # 積集合 p X & Y # 和集合 p X | Y # seが含まれるbi-gramの存在チェック p "X:#{X.include?('se')}、Y:#{Y.include?('se')}"
07. テンプレートによる文生成
Q.引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y=“気温”, z=22.4として,実行結果を確認せよ.
A.引数x,y,z
を式展開#{}
を使用して文字列に埋め込む関数template
を実装し、指定されたテンプレートに引数を埋め込んだ文字列を返すようにした。
def template(x,y,z) return "#{x}時の#{y}は#{z}" end p template(12,"気温",22.4)
08. 暗号文
Q.与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.
- 英小文字ならば(219 - 文字コード)の文字に置換
- その他の文字はそのまま出力
この関数を用い,英語のメッセージを暗号化・復号化せよ.
A.暗号は、文字列をeach_char
で1文字ずつ抽出し、英字(a-z)の場合だけ、codepoints
で文字コードに変換し219から引いた値を格納。復号の方は、ゴリ押し感がひどすぎて辛すぎる。勉強して書き直せるようになりたい。。。
def cipher(str,option) ans = [] case option when "encode" str.each_char{ |c| c = 219 - c.codepoints.join.to_i if /[a-z]/.match(c); ans << c } when "decode" dec_targets = [] i = 1 str.each_char do |c| if /[\d]/.match(c) dec_targets << c c = i % 3 == 0 ? "@" : "" ; i += 1 end ans << c if !c.empty? end dec_targets = dec_targets.join.scan(/.{1,3}/).map{ |n| (219 - n.to_i) }.pack("U*").split("") i,j = 0,0 ans.each do |c| if c == "@" ans[i] = dec_targets[j]; j+= 1 end i += 1 end end ans.join end p cipher("ABCabcABCabc","encode") p cipher("ABC122121120ABC122121120","decode")
ちょっとリファクタリングしました…φ(..)
def cipher(str,option) ans = [] case option when "encode" str.each_char{ |c| c = 219 - c.codepoints.join.to_i if /[a-z]/.match(c); ans << c } when "decode" dec_targets = [] str.each_char { |c| dec_targets << c if /[\d]/.match(c) } dec_targets = dec_targets.join.scan(/.{1,3}/).map{ |n| (219 - n.to_i) }.pack("U*").split("") ans = str.gsub(/\d{3}/,"@").split("") ans.length.times { |i| ans[i] = dec_targets.shift if ans[i] == "@" } end ans.join end p cipher("ABCabcABCabc","encode") p cipher("ABC122121120ABC122121120","decode")
09.Typoglycemia
Q.スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ.ただし,長さが4以下の単語は並び替えないこととする.適当な英語の文(例えば"I couldn’t believe that I could actually understand what I was reading : the phenomenal power of the human mind .“)を与え,その実行結果を確認せよ.
A.まず文字列をsplit(" ")
でスペース区切りの配列に変換し、each
で各文字列を抽出。抽出した文字列の最初と最後の文字以外をc[1..(c.length - 2)].shuffle.join
でシャッフルし、最初の文字c[0]
とc[c.length - 1]
とjoin
で結合して格納し、最後にjoin(" ")
でスペースを入れて結合して返却した。
str = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ." ans = [] str.split(" ").map{ |c| c.split("") }.each do |c| c = c[0],c[1..(c.length - 2)].shuffle.join,c[c.length - 1] if c.length > 4 ans << c.join end p ans.join(" ")