できること
入力欄に入力した言葉に対して、無脳ちゃんが受け応えしてくれます。ただし、大抵は短い罵り言葉ですが、ときどき長文も返してくれます。ただし、たいてい意味不明ですが、ときどき気の利いたことを言うこともあります(単なる偶然です。)。
起動方法
(あらかじめ、必要なライブラリ等をインストールしておく必要があります。(READMEをごらんください。))
ファイルの置いてあるディレクトリに移動して、
$ ruby gui.rb
と実行します。
音声を出したい場合は、
$ ruby gui.rb -v
終了したいときは、入力欄に「バイバイ」または「ばいばい」と入力します。
会話の続け方と機械学習(?)のコツ
- 完全にゼロから入力した文章を蓄積して学習(?)していくので、はじめのうちはかなりおバカです。辛抱強く付き合ってください。
- 入力した文章から名詞を見つけて返答するようになっているので、名詞のない文章には反応しません(名詞があっても反応しないこともあります。)。
- 名詞だけだと、文章(「てにをは」など)を学習できません。
- 以上から、特にはじめのうちは完結した文章をたくさん入力することをお勧めします。
- よい例
「あなたが今言ったことはとてもいい内容ですね。」 「人生で大事なのは友情と努力と勝利の3つだと思うわけです。」
- 悪い例
「いいね」、「あ、そう」、「だよね」、「友情、努力、勝利」
- よい例
- 変な言葉を覚え込ませてしまい、忘れさせたい場合は、MRKOV_DIC.yaml内のその言葉を別の言葉に置換しましょう。単純にその単語だけ削除するとMRKOV_DIC.yamlが壊れる恐れがあります。
内部でやっていること
基本的には、こちらの入力した言葉から、ランダムに名詞をひとつ抜き出して、マルコフ辞書からその名詞につながる言葉をランダムにつなげているだけ。
例
- こちらの入力
おやつにアイス食べたい。
- 名詞抽出
おやつ、アイス
- 名詞をひとつ取り出す。
アイス
- 「アイス」で始まる言葉の塊をマルコフ辞書から探す。
|アイス|は|やっぱり| |アイス|の|味| ......
- 言葉の塊をひとつ取り出す。
|アイス|の|味|
- 今度は、「味」で始まる言葉の塊をマルコフ辞書から探す。
|味|なら|食べ| |味|の|焼きそば|
- 言葉の塊をひとつ取り出す。
|味|の|焼きそば|
- 今度は「焼きそば」で始まる言葉をマルコフ辞書から探す。という作業を繰り返していく。
- 最後に、取り出した言葉の塊をつなげて、文章らしきものにして、出力する。
アイスの味の焼きそば....
プログラム全文
ここでは、GUI部分と会話部分がひとつのファイルになっているが、本来やらせたいこと別にファイルで作成すべき。将来のこととか考えないで作っていったため、こんなことになってしまった。
これを「老舗温泉旅館プログラミング」あるいは「田舎の観光ホテルプログラミング」という。
require 'gtk3' require 'yaml' require 'natto' require 'pp' require 'open_jtalk'
#### Ui クラスを定義 class Ui def initialize @markov_dic = YAML.load_file 'MARKOV_DIC.yaml' rescue @markov_dic = [] end
def make_window win = Gtk::Window.new
#### 次のコードで「閉じる」を押すと、プログラムは終了。 win.signal_connect('destroy') { Gtk.main_quit } win.set_border_width(10)
#### 縦の箱を用意 vbox = Gtk::Box.new(:vertical, 10)
#### 上から画像、無脳ちゃん(仮)の言葉、こちらの入力欄を置く ibox = Gtk::Image.new(file: 'yome.png') tp = Gdk::RGBA.new(1.0, 0.5, 1.0, 0.5) ibox.override_background_color(:normal, tp)
yome_area = Gtk::TextView.new yome_area.set_size_request(240, -1) yome_area.set_cursor_visible(false) yome_area.set_editable(false) @output = '何か用事なの?私だって忙しんだからね。そうそう『バイバイ」でさよならよ。' yome_area.buffer.text = "ヨメ> #{@output}" yome_talk
ore_area = Gtk::Entry.new ore_area.set_size_request(240, -1) ore_area.signal_connect('activate') do |e| @entry = e.text if @entry == 'バイバイ' || @entry == 'ばいばい' puts @entry YAML.dump(@markov_dic, File.open('MARKOV_DIC.yaml', 'w')) @output = 'じゃ、またね。バイバイ' yome_area.buffer.text = "ヨメ> #{@output}" yome_talk exit end ore_strings yome_strings yome_area.buffer.text = "ヨメ> #{@output}" e.text = '' yome_talk end
vbox.pack_start(ibox, expand: true, fill: true, padding: 0) vbox.pack_start(yome_area, expand: true, fill: true, padding: 0) vbox.pack_start(ore_area, expand: true, fill: true, padding: 0) ore_area.grab_focus
win.add(vbox) win.show_all Gtk.main end
private
def ore_strings ## 単語分割し、配列に代入 parse_ore = Natto::MeCab.new parsed_str = [] parse_ore.parse(@entry) do |line| parsed_str.push line.surface end
## マルコフ連鎖用辞書に収録 cycle = parsed_str.size - 3 cycle.times do |key1, key2, val| key1 = parsed_str[0] key2 = parsed_str[1] val = parsed_str[2] keys = [key1, key2] cell_arr = [keys, val] @markov_dic.push cell_arr parsed_str.shift end @markov_dic.uniq! @markov_dic.shuffle!
## 嫁の返事のキーワードとなる名詞をランダムに抜き出す。 ore_words = [] parse_ore.parse(@entry) do |line| if line.feature =~ /固有名詞/ || line.feature =~ /名詞,サ変接続/ || line.feature =~ /名詞,一般/ ore_words.push line.surface end end @keyword = ore_words[rand(ore_words.size)] end
#### ときどきあるはずのない引数を受け取って突然死する。 def yome_strings (*) @yome_strings = ['え!? 嘘でしょ? もー、信じらんない! '] @markov_dic.each do |line| next unless line[0][0] == @keyword if @keyword == '。' || @keyword == '?' @yome_strings.push @keyword break end @yome_strings.push line[0][0] @yome_strings.push line[0][1] @keyword = line[1] next if @keyword != line[0][0] yome_strings @keyword end @yome_strings.push ' バカ、死ね、カス!' @output = @yome_strings.join @yome_strings = ['ヨメ(仮)> '] end
def yome_talk if ARGV[0] == '-v' OpenJtalk.load(OpenJtalk::Config::Mei::FAST) do |openjtalk| header, data = openjtalk.synthesis(openjtalk.normalize_text(@output)) OpenJtalk::WaveFileWriter.save('yome.wav', header, data) `aplay yome.wav` end # File.delete 'yome.wav' end end end
Ui.new.make_window
gui.rb詳細
追加のライブラリー呼び出し
require 'gtk3' require 'yaml' require 'natto' require 'pp' require 'open_jtalk'
Uiクラスの初期化。マルコフ辞書を配列@markov_dicに読み込ませる。マルコフ辞書がないときには、空の@markov_dicを作る。
def initialize @markov_dic = YAML.load_file 'MARKOV_DIC.yaml' rescue @markov_dic = [] end
GUIインターフェースの作成とプログラムの実行をするメソッド(本当は、それぞれの機能を分けた方がいい。)
def make_window win = Gtk::Window.new #### 次のコードで「×ボタン」を押すと、プログラムは終了。 win.signal_connect('destroy') { Gtk.main_quit } win.set_border_width(10) #### 縦の箱を用意 vbox = Gtk::Box.new(:vertical, 10 #### 無脳ちゃん画像を表示 ibox = Gtk::Image.new(file: 'yome.png') tp = Gdk::RGBA.new(1.0, 0.5, 1.0, 0.5) ibox.override_background_color(:normal, tp) #### 無脳ちゃんの言葉を表示 yome_area = Gtk::TextView.new yome_area.set_size_request(240, -1) yome_area.set_cursor_visible(false) yome_area.set_editable(false) @output = '何か用事なの?私だって忙しんだからね。そうそう『バイバイ」でさよならよ。' yome_area.buffer.text = "ヨメ> #{@output}" yome_talk #### こちらの入力欄 ore_area = Gtk::Entry.new ore_area.set_size_request(240, -1) ore_area.signal_connect('activate') do |e| @entry = e.text #### ばいばい、またはバイバイでプログラム終了 if @entry == 'バイバイ' || @entry == 'ばいばい' puts @entry YAML.dump(@markov_dic, File.open('MARKOV_DIC.yaml', 'w')) @output = 'じゃ、またね。バイバイ' yome_area.buffer.text = "ヨメ> #{@output}" yome_talk exit end
以下こちらの入力に応じて、無脳ちゃんの言葉を表示するとともに、音声を出す(あれ?ここにあるのは不自然では?)。
ore_strings yome_strings yome_area.buffer.text = "ヨメ> #{@output}" e.text = '' yome_talk end
以下、GUI描画
vbox.pack_start(ibox, expand: true, fill: true, padding: 0) vbox.pack_start(yome_area, expand: true, fill: true, padding: 0) vbox.pack_start(ore_area, expand: true, fill: true, padding: 0) ore_area.grab_focus win.add(vbox) win.show_all Gtk.main end
以下、プライベートメソッド
ore_stringsでは、1こちらの入力内容をマルコフ辞書に追加する。2ランダムに名詞をひとつ抽出して、yome_stringsにわたす。作業を行う。
def ore_strings ## 単語分割し、配列に代入 parse_ore = Natto::MeCab.new parsed_str = [] parse_ore.parse(@entry) do |line| parsed_str.push line.surface end ## マルコフ連鎖用辞書に収録 cycle = parsed_str.size - 3 cycle.times do |key1, key2, val| key1 = parsed_str[0] key2 = parsed_str[1] val = parsed_str[2] keys = [key1, key2] cell_arr = [keys, val] @markov_dic.push cell_arr parsed_str.shift end @markov_dic.uniq! @markov_dic.shuffle! ## 嫁の返事のキーワードとなる名詞をランダムに抜き出す。 ore_words = [] parse_ore.parse(@entry) do |line| if line.feature =~ /固有名詞/ || line.feature =~ /名詞,サ変接続/ || line.feature =~ /名詞,一般/ ore_words.push line.surface end end @keyword = ore_words[rand(ore_words.size)] end
yome_stringsでは、ore_stringsから受けた言葉を元に文章らしきものを作成する。
(*)がついているのは、ときどきあるはずのない引数を受け取って突然死するのを防止するため、実際には意味のない引数を受け取れるようにしてるため。
def yome_strings (*) @yome_strings = ['え!? 嘘でしょ? もー、信じらんない! '] @markov_dic.each do |line| next unless line[0][0] == @keyword if @keyword == '。' || @keyword == '?' @yome_strings.push @keyword break end @yome_strings.push line[0][0] @yome_strings.push line[0][1] @keyword = line[1] next if @keyword != line[0][0] ## yome_stringsメソッドを再帰的に実行 yome_strings @keyword end @yome_strings.push ' バカ、死ね、カス!' @output = @yome_strings.join @yome_strings = ['ヨメ(仮)> '] end
open_Jtaikを使って、音声を出すためのメソッド
def yome_talk if ARGV[0] == '-v' OpenJtalk.load(OpenJtalk::Config::Mei::FAST) do |openjtalk| header, data = openjtalk.synthesis(openjtalk.normalize_text(@output)) OpenJtalk::WaveFileWriter.save('yome.wav', header, data) `aplay yome.wav` end # File.delete 'yome.wav' end end
実行
Ui.new.make_window