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

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

  • CHAPTER 7-1
    サンプル(KOISURU_PROGRAM/sample/study)を動かす上での変更は、その1のCHAPTER 3-2での読み替えで作成したproto.rbを除き、これまでやってきたものと同じである。

    proto.rbでは、学習結果の保存とログ機能追加のため、以下の変更を行う。

    proto.rbの変更点
    --- a/proto.rb	2017-01-20 00:49:46.000000000 +0900
    +++ b/proto.rb	2017-01-20 21:23:05.000000000 +0900
    @@ -5,14 +5,33 @@
       return unmo.name + ':' + unmo.responder_name + '> '
     end
    
    +def putlog(log)
    +    @log.push(log)
    +end
    +
     puts('Unmo System prototype : proto')
     proto = Unmo.new('proto')
    +
    +# ログ
    +@log = ["Unmo System : #{proto.name} Log -- #{Time.now}"]
    +
     while true
       print('> ')
       input = gets
       input.chomp!
    +  putlog('> ' + input)
       break if input == ''
    
       response = proto.dialogue(input)
    -  puts(prompt(proto) + response)
    +  
    +  withPrompt = prompt(proto) + response
    +  puts(withPrompt)
    +  putlog(withPrompt)
     end
    +
    +proto.save
    +
    +open('log.txt', 'a') do |f|
    +    f.puts(@log)
    +    f.puts
    +end
  • CHAPTER 7-2
    この章以降、形態素解析というものを使うようになる。本を読めば詳しく説明してあるが、いきなりここに来て何の事か分からない方は、こちらを見て欲しい。

    恋するプログラムでは、茶筅(ちゃせん)を形態素解析エンジンにしているが、筆者は和布蕪(めかぶ)を用いることにした。何故そうしたのかはよく覚えていない。多分、こちらの記事がきっかけだったような気がする。
    後から知ったのだが、Macに茶筅(ちゃせん)をインストールすることもできるようだ。

    1. 和布蕪(めかぶ)のインストール
      インストール手順はこちらの手順に従えば良い。ただしファイルの入手先がリンク切れしている。ファイルは以下からダウンロードする。

    2. mecab.rbの作成
      こちらの記事では、nattoというgemを導入しているが、筆者はMeCab本家謹製のMeCab-Rubyを用いた。

      mecab.rbの内容
      require 'MeCab'
      
      module MeCab
      
          def setarg(opt)
              @mecab = MeCab::Tagger.new(opt)
          end
      
          def analyze(text)
              @mecab.parse(text)
          end
      
          module_function :setarg, :analyze
      end
      
      # $0は、実行しているスクリプトファイル名
      # __FILE__は、現在のファイル名
      # require時は両者は異なるが、単体実行時は同じになる。
      # すなわち、モジュール単体テスト用のスクリプトをこのブロック内に書く
      if $0 == __FILE__
      
          # 出力フォーマットを指定して和布蕪を作る
          MeCab::setarg('-F%m\s%F-[0,1,2]\n')
      
          # 標準入力を取得
          while line = gets() do
              line.chomp!
              break if line.empty?
      
              # 解析する
              puts MeCab.analyze(line)
          end
      end
    3. mecab.rbのテスト
      本にあるあたしはプログラムの女の子ですの解析をテストしてみる。

      mecab.rbのテスト
      $ ruby mecab.rb
      あたしはプログラムの女の子です
      あたし 名詞-代名詞-一般
      は 助詞-係助詞
      プログラム 名詞-サ変接続
      の 助詞-連体化
      女の子 名詞-一般
      です 助動詞
      EOS

      オッケーだ。

  • CHAPTER 7-3
    サンプルKOISURU_PROGRAM/sample/study2)を動かす上での変更のうち、文字コード変更等の既に説明した内容は省いて説明する。

    1. morph.rbの作成
      morph.rbも本の通りchasen.rbではなくmecab.rbを利用するものに書き換える。

      morph.rbの内容
      require_relative 'mecab'
       
      module Morph
          def init_analyzer
              # %m\s%F-[0,1,2]\tは、
              # 単語と品詞をスペース(\s)で区切ってグループ化し、タブ文字(\t)でグループ
              # を区切るというフォーマットを設定している。品詞は0,1,2番目まで表示する
              MeCab::setarg('-F%m\s%F-[0,1,2]\t');
          end
      
          def analyze(text)
              # MeCab::analyze(text)の結果は形態素と品詞をスペースで区切ったもの。形態素ごとにタブで区切られている
              # gsub!(/EOS$/,'')で解析結果の最後にある「EOS」を除去
              # chompで改行除去
              # split(/\t/)でタブを区切りに分割した配列を作る
              # mapで、その配列の全要素に対してpart.split(/\s/)を実行することで、
              # 要素からスペースを区切りに分割した配列を作る。
              # つまり、形態素と品詞の配列を要素とする配列が戻り値となる
              return MeCab::analyze(text).gsub!(/EOS$/,'').chomp.split(/\t/).map do |part|
                  part.split(/\s/)
              end
          end
      
          def keyword?(part)
              return /名詞-(一般|固有名詞|サ変接続|形容動詞語幹)/ =~ part
          end
      
          module_function :init_analyzer, :analyze, :keyword?
      end
    2. dictionary.rb dictionaryクラス study_patternメソッドとPatternItemクラス add_phraseメソッド
      サンプルからの変更はないが、コード理解のためのポイントをメモしておく。

      study_patternメソッド
      def study_pattern(input, parts)
                                  
          # unmo.rbでpartsに代入された形態素解析結果は配列が要素になっている。
          # 「word, part」の2つのブロックパラメータで、その配列の要素を、それぞれに代入する。
          parts.each do |word, part|
          
              # Morphモジュールのkeyword?メソッドに合致しなければ、
              # つまり、キーワードでなければ飛ばす。
              next unless Morph::keyword?(part)
              
              # Regexp::quote(word)は、wordに含まれる正規表現の特殊文字をエスケープする。
              # それが元のwordと一致しない場合は次へ飛ばす。
              # つまり、正規表現の時は次へ飛ばしている?(本には記述がない)
              next if Regexp::quote(word) != word
              
              # @patternはパターン辞書を読み込んだ配列
              # findはブロック(ptn_item.pattern == word)の条件を満たす
              # ものを探す。
              duped = @pattern.find{|ptn_item| ptn_item.pattern == word}
              
              if duped
              
                  # add_phraseメソッドは次項参照
                  duped.add_phrase(input)
              else
              
                  # PatternItem.newによって、PatternItemクラスのinitialize
                  # メソッドが自動的に呼ばれる。
                  # PatternItemクラスのinitializeメソッドは(pattern, phrases)の
                  # 引数を持っている。
                  # よって、パターン辞書1行分の@phrasesが作られる。
                  @pattern.push(PatternItem.new(word, input))
              end
              return
          end
      end
      add_phraseメソッド
      def add_phrase(phrase)
                                      
          # pは@phrasesの一要素、つまりneedとphraseからなるハッシュ
          # よってp['phrase']は、ハッシュのphraseを取り出したもの。
          # それが、引数と一致するかチェックしている。
          return if @phrases.find{|p| p['phrase'] == phrase}
          
          # 一致しなければ追加
          @phrases.push({'need'=>0, 'phrase'=>phrase})
      end
    3. dictionary.rb dictionaryクラス saveメソッドとPatternItemクラス make_lineメソッド
      同様にサンプルからの変更はないが、コード理解のためのポイントをメモしておく。

      saveメソッド
      def save
          open('dics/random.txt', 'w') do |f|
              f.puts(@random)
          end
      
          open('dics/pattern.txt', 'w') do |f|
          
              # make_lineメソッドは下を参照
              @pattern.each{|ptn_item| f.puts(ptn_item.make_line)}
          end
      end
      make_lineメソッド
      def make_line
                                  
          # @modifyは機嫌変動値。@patternはパターン
          pattern = @modify.to_s + "##" + @pattern
          
          # 応答例のハッシュの配列(@phrases)を必要機嫌値needと応答例phrase
          # を「##」で区切ったものの配列に分解
          phrases = @phrases.map{|p| p['need'].to_s + "##" + p['phrase']}
          
          # phrases.join('|')で、配列phrasesを'|'で区切った文字列にする。
          # patternと上記をタブで区切ってパターン辞書1行分のデータとなる。
          return pattern + "\t" + phrases.join('|')
      end
    4. proto.rbの変更
      Morphモジュールの初期化(つまり和布蕪の初期化)を追加する。

      proto.rbの変更点
      --- a/proto.rb	2017-01-21 16:28:58.000000000 +0900
      +++ b/proto.rb	2017-01-21 16:30:52.000000000 +0900
      @@ -12,6 +12,9 @@
       puts('Unmo System prototype : proto')
       proto = Unmo.new('proto')
      
      +# 形態素解析モジュールの初期化
      +Morph::init_analyzer
      +
       # ログ
       @log = ["Unmo System : #{proto.name} Log -- #{Time.now}"]
  • CHAPTER 7-4
    サンプルKOISURU_PROGRAM/sample/study3)を動かす上での変更を説明する。

    1. 文字コードの変換
      文字コードの変換はコマンドラインでもできる。
      ただし「¥」や「~」など、置き換えが必要な文字もあるので、テキストエディタで修正する。

      iconv -f SJIS -t UTF8 SJIS.txt > UTF8.txt
    2. 改行コードの変換
      改行コードの変換はコマンドラインでもできる。

      perl -pe 's/\r//' dos.txt > dos2unix.txt
    3. 文字、改行コード両方の変換
      どちらも変換する場合は、以下のようにする。

      iconv -f SJIS -t UTF8 SJIS.txt | perl -pe 's/\r//' > dos2unix.txt
    4. dictionary.rbの誤記訂正
      テンプレート辞書読み込み部に誤記と思われる部分があったので訂正する。

      dictionary.rbの訂正内容
      --- a/dictionary.rb	2017-01-21 19:41:14.000000000 +0900
      +++ b/dictionary.rb	2017-01-21 19:42:03.000000000 +0900
      @@ -24,7 +24,7 @@
           open('dics/template.txt') do |f|
             f.each do |line|
               count, template = line.chomp.split(/\t/)
      -        next if count.nil? or pattern.nil?
      +        next if count.nil? or template.nil?
               count = count.to_i              # (2)
               @template[count] = [] unless @template[count]
               @template[count].push(template)
    5. dictionary.rb dictionaryクラス initializeメソッド(抜粋)
      テンプレート辞書読み込み部のコード理解のためのポイントをメモしておく。

      initializeメソッド(抜粋)
      def initialize
      
          (一部省略)
      
          @template = []
          open('dics/template.txt') do |f|
              f.each do |line|
              
                  # 読み込んだ行をタブで分割した配列を作り、その要素を多重代入
                  count, template = line.chomp.split(/\t/)
                  
                  # count、templateどちらかがnilなら飛ばす
                  next if count.nil? or template.nil?
                  
                  count = count.to_i
      
                  # @template[count]がfalse、つまり配列要素(の配列)が存在しない時
                  # に空配列で初期化
                  @template[count] = [] unless @template[count]
      
                  @template[count].push(template)
              end
          end
      end
    6. dictionary.rb dictionaryクラス study_templateメソッド
      テンプレート辞書学習部のコード理解のためのポイントをメモしておく。

      study_templateメソッド
      def study_template(parts)
          template = ''
          count = 0
          
          # unmo.rbでpartsに代入された形態素解析結果は配列が要素になっている。
          # 「word, part」の2つのブロックパラメータで、その配列の要素を、それぞれに代入する。
          parts.each do |word, part|
          
              # partがキーワードの時、wordを置き換えてカウントアップ
              if Morph::keyword?(part)
                  word = '%noun%'
                  count += 1
              end
              
              # キーワードだけ置き換えた文を生成
              template += word
          end
          
          # count > 0が偽の場合、つまり0の時はリターン
          return unless count > 0
      
          # @template[count]がfalse、つまり配列要素(の配列)が存在しない時
          # に空配列で初期化
          @template[count] = [] unless @template[count]
          
          # include?メソッドが偽の場合、つまり重複していない場合に登録
          unless @template[count].include?(template)
              @template[count].push(template)
          end
      end
    7. dictionary.rb dictionaryクラス saveメソッド(抜粋)
      テンプレート辞書学習部のコード理解のためのポイントをメモしておく。

      saveメソッド(抜粋)
      def save
                                  
         (一部省略)
         
         open('dics/template.txt', 'w') do |f|
         
              # each_with_indexで配列要素とインデックスをブロックパラメータにする
              @template.each_with_index do |templates, i|
              
                  next if templates.nil?
                  
                  # templates配列に対して
                  templates.each do |template|
                  
                      # インデックスとテンプレートのタブ区切りテキストを生成
                      f.puts(i.to_s + "\t" + template)
                  end
              end
          end
      end
    8. responder.rb TemplateResponderクラス
      テンプレート応答生成部のコード理解のためのポイントをメモしておく。

      TemplateResponderクラス
      class TemplateResponder < Responder
          def response(input, parts, mood)
              keywords = []
              
              # unmo.rbでpartsに代入された形態素解析結果は配列が要素になっている。
              # 「word, part」の2つのブロックパラメータで、その配列の要素を、それぞれに代入する。
              parts.each do |word, part|
                  
                  # キーワードの場合keywords配列に追加
                  keywords.push(word) if Morph.keyword?(part)
              end
              
              # キーワードの数
              count = keywords.size
              
              # templatesに代入しつつ空かどうかをチェック
              if count > 0 and templates =  @dictionary.template[count]
                  
                  # templatesからランダムに選択
                  template = select_random(templates)
                  
                  # keywords.shiftは、keywords配列の先頭の要素を外す
                  # gsubのブロック呼び出しgsub(/a/){}では、
                  # 「%noun%」が登場するたびに{}が実行される
                  return template.gsub(/%noun%/){keywords.shift}
              end
      
              return select_random(@dictionary.random)
          end
      end

今回は、盛り沢山で長くなった。その4に続く。

この投稿へのコメント

コメントはありません。

コメントを残す

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

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

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

トラックバック URL