あかんわ

覚えたことをブログに書くようにすれば多少はやる気が出るかと思ったんです

恋するプログラムをSinatraでWebアプリにするPart.11[CHAPTER8 文章を作り出す]

記事の概要

『恋するプログラム』の[CHAPTER8 文章を作り出す]を参考に、マルコフ連鎖形態素解析を利用して応答時の文を生成するチャットボットを作成します。

目次

開発環境

参考記事
Rubyの開発環境構築は、こちらの記事を参考にさせていただきました。

ノビィに確率を応用する

マルコフ連鎖の考え方を利用して応答時の文を生成するために、入力した会話の内容を形態素解析を利用して分割し、単語のつながりの情報を保存するマルコフ辞書を作成します。

アプリケーションディレクトリの構成
~/programinlove
    |- proto.rb                 // CUIチャットボットのメインファイル
    |- unmo.rb                  // チャットボットオブジェクトのモデル
    |- responder.rb             // 応答オブジェクトのモデル
    |- dictionary.rb            // 辞書を読み込み管理するクラス
    |- mecab_natto.rb           // 形態素解析器MeCabを使うためのモジュール
    |- morph.rb                 // MeCabNattoモジュールをunmo.rbで使うためのモジュール
    |- morkov.rb                // マルコフ連鎖を利用して辞書を作成するクラス
    |- utils.rb                 // select_randomメソッドを定義しただけのファイル
    |- log.txt                  // ユーザの入力履歴を保存するログファイル
    |- /dics                    // 辞書ファイルを格納するディレクトリ
    |     |- random.txt         // ランダムな応答を返すための辞書
    |     |- pattern.txt        // パターンに合った応答を返すための辞書
    |     |- template.txt       // テンプレートを使った応答を返すための辞書
    |     |- markov.dat         // マルコフ連鎖を使った応答を返すための辞書
    |- noby.rb                  // Webアプリのメインファイル
    |- /views                   // ビューのテンプレートを配置するディレクトリ
    |     |- index.erb          // チャットのインターフェースを表示するビュー
    |- /public                  // 静的ファイルを配置するディレクトリ
          |- nobycanvas.js      // キャラクターの画像をアニメーションさせるJavascript
          |- styles.css         // チャットのインターフェースをレイアウトするcss
          |- /img               // 画像ファイルを格納するディレクトリ
               |- /normal       // 平常時の画像を格納するディレクトリ
               |- /blink        // 瞬きの画像を格納するディレクトリ
               |- /lookaround   // 周囲を見回す画像を格納するディレクトリ
               |- /talk         // 喋る画像を格納するディレクトリ
               ~ 以下に表情ごとの画像を格納するディレクトリを配置
ソースコード

markov.rbのテストコードを動かすためにテキストファイルをコマンドライン引数に与えて起動したところ、undefined method 'chomp' for nil:NilClass (NoMethodError)が出て会話の入力が出来なかったため、loop内のgets.chomp$stdin.gets.chompに変更しました。

その他のRubyファイルは、サンプルプログラムと同様の記述で、特に変更はありません。

markov.rb
require_relative 'morph'
require_relative 'utils'

class Markov
  ENDMARK = '%END%'
  CHAIN_MAX = 30

  def initialize
    @dic = {}
    @starts = {}
  end

  def add_sentence(parts)
    return if parts.size < 3

    parts = parts.dup
    prefix1, prefix2 = parts.shift[0], parts.shift[0]
    add_start(prefix1)

    parts.each do |suffix, part|
      add_suffix(prefix1, prefix2, suffix)
      prefix1, prefix2 = prefix2, suffix
    end
    add_suffix(prefix1, prefix2, ENDMARK)
  end

  def generate(keyword)
    return nil if @dic.empty?

    words = []
    prefix1 = (@dic[keyword])? keyword : select_start
    prefix2 = select_random(@dic[prefix1].keys)
    words.push(prefix1, prefix2)
    CHAIN_MAX.times do
      suffix = select_random(@dic[prefix1][prefix2])
      break if suffix == ENDMARK
      words.push(suffix)
      prefix1, prefix2 = prefix2, suffix
    end
    return words.join
  end

  def load(f)
    @dic = Marshal::load(f)
    @starts = Marshal::load(f)
  end

  def save(f)
    Marshal::dump(@dic, f)
    Marshal::dump(@starts, f)
  end

  private
  def add_suffix(prefix1, prefix2, suffix)
    @dic[prefix1] = {} unless @dic[prefix1]
    @dic[prefix1][prefix2] = [] unless @dic[prefix1][prefix2]
    @dic[prefix1][prefix2].push(suffix)
  end

  def add_start(prefix1)
    @starts[prefix1] = 0 unless @starts[prefix1]
    @starts[prefix1] += 1
  end

  def select_start
    return select_random(@starts.keys)
  end
end

if $0 == __FILE__
  Morph::init_analyzer

  markov = Markov.new
  while line = gets do
    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('> ')
    line = $stdin.gets.chomp
    break if line.empty?
    parts = Morph::analyze(line)
    keyword, p = parts.find{|w, part| Morph::keyword?(part)}
    puts(markov.generate(keyword))
  end
end

このコードのコミットには、[chapter8-3]のタグが付いてます。

実行結果

ターミナルでnoby.rbを動かし、ブラウザにhttp://localhost:4567を入力します。

~/programinlove
$ruby noby.rb

f:id:b0npu:20160107142020p:plain

会話の内容から2単語プレフィクスとサフィックスのつながりの情報が、マルコフ辞書に保存されます。

f:id:b0npu:20160309214435p:plain

$pry 
[1] pry(main)> open('dics/markov.dat', 'rb') do |f|
[1] pry(main)*   Marshal::load(f)
[1] pry(main)* end  
=> {"坊っちゃん"=>{"を"=>["読ん"], "の"=>["キーボード"]},
 "を"=>{"読ん"=>["だ"]},
 "読ん"=>{"だ"=>["こと"]},
 "だ"=>{"こと"=>["は"]},
 "こと"=>{"は"=>["あり"]},
 "は"=>{"あり"=>["ます"], "面白い"=>["です"]},
 "あり"=>{"ます"=>["か"]},
 "ます"=>{"か"=>["%END%"]},
 "の"=>{"キーボード"=>["の"], "話"=>["です", "は"]},
 "キーボード"=>{"の"=>["話"]},
 "話"=>{"です"=>["か"], "は"=>["面白い"]},
 "です"=>{"か"=>["?"], "ね"=>["%END%"]},
 "か"=>{"?"=>["%END%"]},
 "それでは"=>{"ターナー"=>["も"]},
 "ターナー"=>{"も"=>["ご存知"]},
 "も"=>{"ご存知"=>["で"]},
 "ご存知"=>{"で"=>["%END%"]},
 "練兵"=>{"場"=>["の"]},
 "場"=>{"の"=>["話"]},
 "面白い"=>{"です"=>["ね"]}}
[2] pry(main)> 

参考書籍

参考記事

Sinatraについては、こちらの記事を参考にさせていただきました。

関連記事

- 恋するプログラムをSinatraでWebアプリにするPart.0[はじめに]
- 恋するプログラムをSinatraでWebアプリにするPart.1[CHAPTER3 ほんとに無能]
- 恋するプログラムをSinatraでWebアプリにするPart.2[CHAPTER4 あこがれのGUI]①
- 恋するプログラムをSinatraでWebアプリにするPart.3[CHAPTER4 あこがれのGUI]②
- 恋するプログラムをSinatraでWebアプリにするPart.4[CHAPTER4 あこがれのGUI]③
- 恋するプログラムをSinatraでWebアプリにするPart.5[CHAPTER5 辞書を片手に]
- 恋するプログラムをSinatraでWebアプリにするPart.6[CHAPTER6 感情コントロールの魔術師]①
- 恋するプログラムをSinatraでWebアプリにするPart.7[CHAPTER6 感情コントロールの魔術師]②
- 恋するプログラムをSinatraでWebアプリにするPart.8[CHAPTER7 学習のススメ]①
- 恋するプログラムをSinatraでWebアプリにするPart.9[CHAPTER7 学習のススメ]②
- 恋するプログラムをSinatraでWebアプリにするPart.10[CHAPTER7 学習のススメ]③
- 恋するプログラムをSinatraでWebアプリにするPart.12[CHAPTER9 ノビィ、ネットワークにつながる]
- 恋するプログラムをSinatraでWebアプリにするPart.13[おわりに]