恋するプログラムをSinatraでWebアプリにするPart.11[CHAPTER8 文章を作り出す]
記事の概要
『恋するプログラム』の[CHAPTER8 文章を作り出す]を参考に、マルコフ連鎖と形態素解析を利用して応答時の文を生成するチャットボットを作成します。
目次
開発環境
- OSX 10.11.2 El Capitan
- テキストエディタ: MacVim
- ターミナルエミュレータ: Macターミナル
- シェル: zsh
- パッケージマネージャ: Homebrew
- ブラウザ: Firefox - Ruby 2.0.0p645
- バージョンマネージャ: rbenv
- Webフレームワーク: Sinatra - MeCab 0.996
- 日本語辞書: IPA辞書
参考記事
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
会話の内容から2単語プレフィクスとサフィックスのつながりの情報が、マルコフ辞書に保存されます。
$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については、こちらの記事を参考にさせていただきました。
- Rubyの入門や書き捨てアプリを作る場合は sinatraがオススメ! - むかぁ~ どっと こむ
- SinatraとjQueryでおよそ100行で作るAjax掲示板アプリケーション - gaaamiiのブログ
- Sinatra: README (Japanese)
関連記事
- 恋するプログラムを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[おわりに]