「恋するプログラム」のMac向け読み替え その4
その3に続き、Macユーザーが恋するプログラム
を勉強する上で、読み替えや説明が必要だったことをメモして行く。
-
CHAPTER 8-1
マルコフモデルの解説であり、コードは出てこない。 -
CHAPTER 8-2
この章ではサンプル(KOISURU_PROGRAM/sample/markov)のうち、markov.rb
について解説している。テストコードの一箇所を除き、サンプルからの変更はない(*注)が、コード理解のためのポイントをメモしておく。-
markov.rb Markovクラス add_sentenceメソッド
マルコフ辞書に登録するフォーマットでの文章の追加(学習)を行うメソッドである。- add_sentenceメソッド
-
def add_sentence(parts) # 引数として受け取った形態素解析結果の配列の要素数が3未満ならばリターン return if parts.size < 3 # 引数の形態素解析結果配列をコピーしたものをpartsに代入 # コピーする理由は次の行でshiftメソッドで配列自身を変更するから parts = parts.dup # parts.shiftによって得られるのは、parts配列の中の最初の要素 # この要素は形態素と品詞の配列なので、[0]で形態素を取り出している # 2回目のshiftでは、1回目のshiftで要素がひとつ減ったparts配列 # が対象となる。 # 上記結果をprefix1, prefix2に多重代入 # これは形態素解析結果配列の先頭の2要素に対して行われる処理 prefix1, prefix2 = parts.shift[0], parts.shift[0] # 次項項参照 add_start(prefix1) # ここのpartsはshiftを2回経た(要素が2個減った)ものでスタート # つまり、1回目のsuffix登録から始まる。 # ブロックパラメータを2個用意することで、品詞のみを取り出している parts.each do |suffix, part| # 次項項参照 add_suffix(prefix1, prefix2, suffix) # マルコフ連鎖の仕組み通り次のプレフィックスを生成 prefix1, prefix2 = prefix2, suffix end # ループを抜けた最後の処理(ENDMARKをサフィックスにする) add_suffix(prefix1, prefix2, ENDMARK) end
-
markov.rb Markovクラス add_startメソッド
上記学習と、文章生成で使われるサブメソッドである。- add_startメソッド
-
def add_start(prefix1) # prefix1をキーとして、整数を値とするハッシュを生成 # prefix1をキーとする値が存在しない時0を値とする # 以降はインクリメントした値を取る @starts[prefix1] = 0 unless @starts[prefix1] @starts[prefix1] += 1 end
-
markov.rb Markovクラス add_suffixメソッド
学習で使われるサブメソッドである。- add_suffixメソッド
-
def add_suffix(prefix1, prefix2, suffix) # prefix1をキーとする値が存在しない時、空のハッシュを値とする @dic[prefix1] = {} unless @dic[prefix1] # @dic[prefix1]の中のハッシュで、prefix2をキーとする値が存在 # しない時、空の配列を値とする @dic[prefix1][prefix2] = [] unless @dic[prefix1][prefix2] # @dicの構成は次の形になる。 # @dic = {prefix1 => {prefix2 => [suffix]}} # # 値が配列である理由は、候補が複数ある場合があるため @dic[prefix1][prefix2].push(suffix) end
-
markov.rb Markovクラス generateメソッド
マルコフ辞書から文章を生成するメソッドである。- generateメソッド
-
def generate(keyword) return nil if @dic.empty? words = [] # 文頭になる2つのプレフィックスを生成 # 3項演算子 xx ? yy : zzは、if xx then yy else zz endと同じ # @dicでkeywordがキーになっていればkeywordを、なっていなければ # select_start(次項参照)を実行し、prefix1とする prefix1 = (@dic[keyword])? keyword : select_start # @dic[prefix1]で、ハッシュが指定される # keysで、そのハッシュのキーの一覧が配列で取得される # そこからランダムに選択する prefix2 = select_random(@dic[prefix1].keys) # 文頭の設定 words.push(prefix1, prefix2) # ループの上限はCHAIN_MAX CHAIN_MAX.times do # @dicの構成は以下の形である # @dic = {prefix1 => {prefix2 => [suffix]}} # よって@dic[prefix1][prefix2]は[suffix] # [suffix]からランダムに選択 suffix = select_random(@dic[prefix1][prefix2]) break if suffix == ENDMARK words.push(suffix) # 次のprefix1, prefix2をprefix2, suffixから生成 prefix1, prefix2 = prefix2, suffix end return words.join end
-
markov.rb Markovクラス select_startメソッド
上記文章生成メソッドで使われるサブメソッドである。- select_startメソッド
-
def select_start # @startsハッシュのkeysでキー一覧配列取得 # それをランダムに選択 return select_random(@starts.keys) end
-
markov.rb テストコード
マルコフモデルによる学習と文章生成のテストコードである。- markov.rb テストコード
-
if $0 == __FILE__ # 形態素解析モジュールの初期化 Morph::init_analyzer # Markovのインスタンス生成 markov = Markov.new # getsは標準入力もしくは第一引数の内容を,文字列として一行ずつ得る # while line = gets do 〜 endでgetsの値(一行分)をlineに代入 # lineがnilになるまでwhileで回す。 while line = gets do # 改行を除去して、[。??!! ]の1回以上の繰り返しを区切りに # 配列を作る texts = line.chomp.split(/[。??!! ]+/) texts.each do |text| next if text.empty? # マルコフモデルによる学習 markov.add_sentence(Morph::analyze(text)) print '.' end end puts loop do print('> ') # サンプルからの変更点はこちら # getsだけでは、上のループを抜けたnilが入ってエラーになる # $stdin.getsに修正することで標準入力の受け取り待ちになる line = $stdin.gets.chomp break if line.empty? # 形態素解析 parts = Morph::analyze(line) # キーワード抽出 # parts配列に対してpartがキーワードであるwを見つける keyword, p = parts.find{|w, part| Morph::keyword?(part)} # マルコフモデルによる文章生成 puts(markov.generate(keyword)) end end
-
-
CHAPTER 8-3
この章ではサンプル(KOISURU_PROGRAM/sample/markov)のうち、markov.rb
を除いた残りの部分について解説している。サンプルからの変更はない(*注)が、コード理解のためのポイントをメモしておく。-
dictionary.rb Dictionaryクラス initializeメソッドとload_randomメソッド
7章までは、initializeメソッドに辞書の読み込み処理が直接書かれていたが、以下のように辞書ごとのメソッドに分割された。- initializeメソッド
-
def initialize load_random load_pattern load_template load_markov end
- load_randomメソッド
-
def load_random @random = [] # bigin 〜 rescueの間に例外が発生しうる処理が入る # この例では辞書読み込み処理を挟んでいるので、辞書が開けないときに備えている。 begin open('dics/random.txt') do |f| f.each do |line| line.chomp! next if line.empty? @random.push(line) end end rescue => e # 例外内容の取得 # 例外が発生したときの処理 puts(e.message) @random.push('こんにちは') end end
load_pattern、load_templateについても、変更は、辞書を開く処理に例外対応が追加されたのみである。
-
dictionary.rb Dictionaryクラス load_markovメソッド
新しく追加されたマルコフ辞書の読み込み処理である。- load_markovメソッド
-
def load_markov # Markovクラスのインスタンス生成 @markov = Markov.new begin # 'rb'でバイナリーモードで読み込む open('dics/markov.dat', 'rb') do |f| # 次項参照 @markov.load(f) end rescue => e puts(e.message) end end
-
dictionary.rb Dictionaryクラス saveメソッド(抜粋)
saveメソッドのうち、新しく追加されたマルコフ辞書の書き込み処理を抜粋したものである。- saveメソッド(抜粋)
-
def save (一部省略) # 'rw'でバイナリーモードで書き込む open('dics/markov.dat', 'wb') do |f| # 次項参照 @markov.save(f) end end
-
dictionary.rb Dictionaryクラス study_markovメソッド
新しく追加されたマルコフモデルによる学習処理である。- study_markovメソッド
-
def study_markov(parts) @markov.add_sentence(parts) end
-
markov.rb Markovクラス loadメソッドおよびsaveメソッド
マルコフ辞書ファイルからの読み込み、書き込みを行う。どちらもMarshal形式(オブジェクトのまま読み書き=バイナリーデータ)を用いている。- loadメソッド
-
def load(f) @dic = Marshal::load(f) @starts = Marshal::load(f) end
- saveメソッド
-
def save(f) Marshal::dump(@dic, f) Marshal::dump(@starts, f) end
-
responder.rb MarkovResponderクラス
マルコフモデルによる応答生成部である。- MarkovResponderクラス
-
class MarkovResponder < Responder def response(input, parts, mood) # 形態素解析結果のうちキーワードのものを抽出 keyword, p = parts.find{|w, part| Morph::keyword?(part)} # マルコフモデルによる文章生成 resp = @dictionary.markov.generate(keyword) # 結果がnilでなければ、こちらが戻り値になる return resp unless resp.nil? # nilの時、ランダム辞書が呼ばれる return select_random(@dictionary.random) end end
-
unmo.rb
マルコフモデルによる応答追加に伴うresponder出現確率の変更とselect_randomの分離がなされている。- unmo.rbの変更点
-
--- a/unmo.rb 2017-01-22 22:52:38.000000000 +0900 +++ b/unmo.rb 2017-01-23 16:50:46.000000000 +0900 @@ -12,6 +12,7 @@ @resp_random = RandomResponder.new('Random', @dictionary) @resp_pattern = PatternResponder.new('Pattern', @dictionary) @resp_template = TemplateResponder.new('Template', @dictionary) + @resp_markov = MarkovResponder.new('Markov', @dictionary) @responder = @resp_pattern end @@ -20,12 +21,14 @@ parts = Morph::analyze(input) case rand(100) - when 0..39 + when 0..29 @responder = @resp_pattern - when 40..69 + when 30..49 @responder = @resp_template - when 70..89 + when 50..69 @responder = @resp_random + when 70..89 + @responder = @resp_markov else @responder = @resp_what end @@ -86,7 +89,3 @@ attr_reader :mood end - -def select_random(ary) - return ary[rand(ary.size)] -end
-
proto.rbは7章から変更はない。
-
マルコフ辞書ファイルについて
サンプルの動作にあたっては、初回は、マルコフ辞書ファイル(markov.dat)がない状態で起動すること。サンプルに含まれる辞書はmacOSでは文字化けする。
-
- *注意*
- 文字コード(SJIS → UTF8)変換、改行コード(CR/LF → LF)変換、およびrequireのrequire_relativeへの書き換えは除く。
区切りが良いので、今回はここまでにする。その5に続く。
この投稿へのトラックバック
トラックバックはありません。
- トラックバック URL
この投稿へのコメント