「恋するプログラム」のMac向け読み替え その4

その3に続き、Macユーザーが恋するプログラムを勉強する上で、読み替えや説明が必要だったことをメモして行く。

  • CHAPTER 8-1
    マルコフモデルの解説であり、コードは出てこない。

  • CHAPTER 8-2
    この章ではサンプル(KOISURU_PROGRAM/sample/markov)のうち、markov.rbについて解説している。テストコードの一箇所を除き、サンプルからの変更はない(*注)が、コード理解のためのポイントをメモしておく。

    1. 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
    2. markov.rb Markovクラス add_startメソッド
      上記学習と、文章生成で使われるサブメソッドである。

      add_startメソッド
      def add_start(prefix1)
      
          # prefix1をキーとして、整数を値とするハッシュを生成
          # prefix1をキーとする値が存在しない時0を値とする
          # 以降はインクリメントした値を取る
          @starts[prefix1] = 0 unless @starts[prefix1]
          @starts[prefix1] += 1
      end
    3. 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
    4. 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
    5. markov.rb Markovクラス select_startメソッド
      上記文章生成メソッドで使われるサブメソッドである。

      select_startメソッド
      def select_start
          
          # @startsハッシュのkeysでキー一覧配列取得
          # それをランダムに選択
          return select_random(@starts.keys)
      end
    6. 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を除いた残りの部分について解説している。サンプルからの変更はない(*注)が、コード理解のためのポイントをメモしておく。

    1. 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についても、変更は、辞書を開く処理に例外対応が追加されたのみである。

    2. 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
    3. dictionary.rb Dictionaryクラス saveメソッド(抜粋)
      saveメソッドのうち、新しく追加されたマルコフ辞書の書き込み処理を抜粋したものである。

      saveメソッド(抜粋)
      def save
      
          (一部省略)
      
          # 'rw'でバイナリーモードで書き込む
          open('dics/markov.dat', 'wb') do |f|
          
              # 次項参照
              @markov.save(f)
          end
      end
    4. dictionary.rb Dictionaryクラス study_markovメソッド
      新しく追加されたマルコフモデルによる学習処理である。

      study_markovメソッド
      def study_markov(parts)
          @markov.add_sentence(parts)
      end
    5. 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
    6. 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
    7. 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
    8. proto.rbは7章から変更はない。

    9. マルコフ辞書ファイルについて
      サンプルの動作にあたっては、初回は、マルコフ辞書ファイル(markov.dat)がない状態で起動すること。サンプルに含まれる辞書はmacOSでは文字化けする。

*注意*
文字コード(SJIS → UTF8)変換、改行コード(CR/LF → LF)変換、およびrequireのrequire_relativeへの書き換えは除く。

区切りが良いので、今回はここまでにする。その5に続く。

この投稿へのコメント

コメントはありません。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

この投稿へのトラックバック

トラックバックはありません。

トラックバック URL