ようじょのおえかきちょう

ふぇぇ お医者さんにペン持ったらダメっていわれた〜〜

言語処理100本ノック2015 をRubyでやる【第1章】

自然言語処理を扱う研究室に配属になったので、この秋から課題として「言語処理100本ノック 2015」をやっています。先輩も同期も Python で書いているのですが、みんな一緒はつまんないので Ruby で書いてみることにしました。コードは GitHub に随時上げていきます。

github.com

最初の方こそググれば「Rubyでやってみた」記事が引っかかるのですが、途中から全くヒットしなくなるので悲しいです。これも続くかわかりませんが可能な限りやっていきます。

今回は第 1 章「準備運動」です。

00. 文字列の逆順

文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.

解答

puts "stressed".reverse

Ruby では、引数のための小括弧は省略可能です。 puts("stressed".reverse) でもいいけど、書かなくてもわかるものは書かないほうがそれっぽいですね。

出力

desserts

01. 「パタトクカシーー」

「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

解答

str = "パタトクカシーー"
puts str.chars.select.with_index { |char, i| i.even? }.join

順番に見ていきます。

String#chars -> Array は、各文字を要素とした配列を返します。

str.chars
# => ["パ", "タ", "ト", "ク", "カ", "シ", "ー", "ー"]

Array#select { |item| block } -> Array Array#select -> Enumerator は、ブロック内の条件に合うものだけ選んで、それを要素とした配列を返します。

Enumerator#with_index(offset) { |(*args), i| ... } -> object は、インデックス付きでブロック {} を繰り返します。開始番号 offset のデフォルトは 0 です。

str.chars.select.with_index { |char, i| i.even? }
# => ["パ", "ト", "カ", "ー"]

Array#join(sep) -> String は、各要素を先頭から順に文字列 sep を挟んだ文字列にして返します。

str.chars.select.with_index { |char, i| i.even? }.join
# => "パトカー"

出力

パトカー

02. 「パトカー」+「タクシー」=「パタトクカシーー」

「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.

解答

str1 = "パトカー"
str2 = "タクシー"

puts [str1, str2].map(&:chars).transpose.join

Array#map { |item| block } -> Array は各要素を順にブロック内で評価し、結果を要素として写像のように返します。 ブロックに書く処理がひとつのメソッドなら &:method のように略して書けます(この説明は語弊があります、[Ruby Symbol & method]とかでググると良さそう)。

[str1, str2].map(&:chars)
# => [["パ", "ト", "カ", "ー"], ["タ", "ク", "シ", "ー"]]

Array#transpose -> Array で配列を行列とみなして転置します。

[str1, str2].map(&:chars).transpose
# => [["パ", "タ"], ["ト", "ク"], ["カ", "シ"], ["ー", "ー"]]

出力

パタトクカシーー

03. 円周率

"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.

解答

str = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
puts str.split(/\W+/).map(&:count)

String#split(sep) -> [String] は sep で分割して文字列の配列を返します。sep には正規表現も書けます。

str.split(/\W+/) => ["Now", "I", "need", "a", "drink", "alcoholic", ...

ここで使った正規表現Ruby特有のものではない)

  • \W : 非単語構成文字、つまり a-z A-Z 0-9 _ でないもの
  • + : 直前の1文字の1回以上の繰り返し

String#count -> Integer 文字列がいくつあるか数えます。length size も使い方はだいたい一緒です。

出力

[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]

04. 元素記号

"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文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

解答

str = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
map = {}

str.split(/\W+/).each.with_index(1) do |word, i|
  abbr = [1, 5, 6, 7, 8, 9, 15, 16, 19].include?(i) ? word[0] : word[0..1]
  map[abbr] = i
end

puts map

Array#each { |item| ... } -> self Array#each -> Enumerator は、各要素に対してブロックを評価します。雰囲気は for みたいなものです。

Array#include?(val) -> bool は、配列の中に val と等しい(==)要素があれば true を返します。

[1, 5, 6, 7, 8, 9, 15, 16, 19].include?(3)
# => false

Rubyの範囲表現

  • 0..5 => 0から5まで(5を含む)
  • 0...5 => 0から5まで(5を含まない)

出力

{"H"=>1, "He"=>2, "Li"=>3, "Be"=>4, "B"=>5, "C"=>6, "N"=>7, "O"=>8, "F"=>9, "Ne"=>10, "Na"=>11, "Mi"=>12, "Al"=>13, "Si"=>14, "P"=>15, "S"=>16, "Cl"=>17, "Ar"=>18, "K"=>19, "Ca"=>20}

05. n-gram

与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.

解答

class Array
  def ngram(n)
    self.each_cons(n).map(&:join)
  end
end

str = "I am an NLPer"

p str.split(/\W+/).ngram(2)
p str.chars.ngram(2)

Enumerable#each_cons(n) -> Enumerator Enumerable#each_cons(n) { |list| ... } -> nil は、要素をn個ずつに区切って(重複あり)ブロックに渡して繰り返します。

また Ruby は既存のクラスを再オープンし、メソッドの修正・追加をすることができます。

出力

["Iam", "aman", "anNLPer"]
["I ", " a", "am", "m ", " a", "an", "n ", " N", "NL", "LP", "Pe", "er"]

06. 集合

"paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.

解答

class Array
  def ngram(n)
    self.each_cons(n).map(&:join)
  end
end

str1 = "paraparaparadise"
str2 = "paragraph"

X = str1.chars.ngram(2)
Y = str2.chars.ngram(2)

p X | Y
p X & Y
p X - Y
puts X.include?("se")
puts Y.include?("se")

Ruby| & -Python と同じっぽいです。

出力

["pa", "ar", "ra", "ap", "ad", "di", "is", "se", "ag", "gr", "ph"]
["pa", "ar", "ra", "ap"]
["ad", "di", "is", "se"]
true
false

07. テンプレートによる文生成

引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y="気温", z=22.4として,実行結果を確認せよ.

解答

def template(x, y, z)
  "#{x}時の#{y}#{z}"
end

puts template(12, "気温", 22.4)

#{var} と書くことで変数 var の中身を展開できます。

出力

12時の気温は22.4

08. 暗号文

与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.

  • 英小文字ならば(219 - 文字コード)の文字に置換
  • その他の文字はそのまま出力

この関数を用い,英語のメッセージを暗号化・復号化せよ.

解答

def cipher(str)
  map = str.chars.map do |c|
    /[[:lower:]]/.match(c) ? (219 - c.ord).chr : c
  end
  map.join
end

str = "Lorem Ipsum is simply dummy text of the printing and typesetting industry."
ciphered_str = cipher(str)

puts ciphered_str
puts cipher(ciphered_str)

Regexp#match(str) -> MatchData | nil

ここではPOSIX文字クラスを使っています。

  • [:lower:] : 小文字
  • [:upper:] : 大文字
  • [:alnum:] : 英数字

String#ord -> Integer は、最初の文字の文字コードを整数で返します。

"A".ord
# => 65

Integer#chr は、与えられたIntegerを文字コードと見た時、それに対応する1文字を返します。

65.chr
# => "A"

出力

Llivn Ikhfn rh hrnkob wfnnb gvcg lu gsv kirmgrmt zmw gbkvhvggrmt rmwfhgib.
Lorem Ipsum is simply dummy text of the printing and typesetting industry.

09. Typoglycemia

スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ.ただし,長さが4以下の単語は並び替えないこととする.適当な英語の文(例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")を与え,その実行結果を確認せよ.

解答

str = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."

map = str.split(/\s/).map do |word|
  word.size <= 4 ? word : word[0] + word[1...-1].chars.shuffle.join + word[-1]
end

puts map.join(" ")

Array#shuffle -> Array は、配列の要素をランダムシャッフルし、その結果を配列として返します。

出力例

I cdn'luot beivlee that I cloud aulaltcy utsandnred what I was reiadng : the pneeohamnl pewor of the human mind .

Ruby 書いたことない人向けの解説を用意していたので、それも載せてみました。自分は新しい言語を触るとき、まず慣れるために第 1 章をやるようにしてます。2 章くらいまでは雰囲気でやれるので、いろんな言語で挑戦すると楽しそうです。

自分もまだまだ勉強中の身なので、もっと良い書き方あるよ〜などあれば issue とかで教えてもらえるとうれしいです!

github.com