Rubyでマルコフ連鎖botを作る話[4/3:実装編]
前回のラブライブ!
マルコフ連鎖を理解しbotの実装を考えた mwc922-hsm.hatenablog.com
Rubyでマルコフ連鎖botを実装する
ここまで来たらあとはやるだけ。やりましょう。
まずはTwitterAPIを使うためのbotクラスを定義していきます。
class Bot attr_accessor :client def initialize @client = Twitter::REST::Client.new do |config| config.consumer_key = CONSUMER_KEY config.consumer_secret = CONSUMER_SECRET config.access_token= OAUTH_TOKEN config.access_token_secret= OAUTH_SECRET end end def post(text = "") @client.update(text) end # user_nameのツイートを過去tweet_count個取得する def get_tweet(user_name, tweet_count) tweets = [] @client.user_timeline(user_name, {count: tweet_count, exclude: retweets}).each do |timeline| tweet = @client.status(timeline.id) if not tweet.text.include?("RT") tweets.push(tweet2textdata(tweet.text)) end end return tweets end end
とりあえずツイートをするためのpostメソッドとツイートを取得するためのget_tweetメソッドを実装しておきました。
次に、形態素解析をするためのNattoParserクラスを作ります。こんな名前でいいのかはわかりません。
class NattoParser attr_accessor :nm def initialize() @nm = Natto::MeCab.new end def parseTextArray(texts) words = [] index = 0 for text in texts do words.push(Array[]) @nm.parse(text) do |n| if n.surface != "" words[index].push(n.surface) end end index += 1 end return words end end def genMarcovBlock(words) array = [] # 最初と最後はnilにする words.unshift(nil) words.push(nil) # 3単語ずつ配列に格納 for i in 0..words.length-3 array.push([words[i], words[i+1], words[i+2]]) end return array end
命名ガバガバです。parseTextArrayは与えられた文字列配列を分割された単語の配列の集合に変換します。
genMarcovBlockは単語の配列から3単語ずつのブロックを生成します。
最後にマルコフ連鎖をする関数を作ります。
def findBlocks(array, target) blocks = [] for block in array if block[0] == target blocks.push(block) end end return blocks end def connectBlocks(array, dist) i = 0 for word in array[rand(array.length)] if i != 0 dist.push(word) end i += 1 end return dist end def marcov(array) result = [] block = [] # nilから始まるブロックを選ぶ block = findBlocks(array, nil) result = connectBlocks(block, result) # resultの最後の単語がnilになるまで繰り返す while result[result.length-1] != nil do block = findBlocks(array, result[result.length-1]) result = connectBlocks(block, result) end return result end
marcovがマルコフ連鎖ループ本体です。
resultに最終的に生成される単語の集合、blockは考えられるブロックを一時的に格納しておく配列です。
マルコフ連鎖ループの中で何度も呼び出される
- 考えられるブロックを抽出する
- ブロックを連結する
の操作は個別に関数にしてあります。
完成
ということで完成したものがこちらです。
なんか一気に飛ばした気もしますがコード全部載せても仕方ないのであとの細かいところはGitHubを参照してください。
マルコフ連鎖botは簡単なのでみんな作りましょう。自分の文体でbotが支離滅裂な文章を生成してツイートしてるの見るのは結構面白いです。
Rubyでマルコフ連鎖botを作る話[3/3:理論編]
前回のラブライブ!
マルコフ連鎖ってなーに
マルコフ連鎖(マルコフれんさ、英: Markov chain)とは、確率過程の一種であるマルコフ過程のうち、とりうる状態が離散的(有限または可算)なもの(離散状態マルコフ過程)をいう。
(出典: マルコフ連鎖 - Wikipedia)
よくわからないですね…(わかってない)
マルコフ連鎖については以下のブログがとてもわかりやすかったのでおすすめです。
マルコフ連鎖でTwitter Botをつくりました | takuti.me
一応以下に自分の理解をメモとして残しておきます。
マルコフ連鎖は、単語からそれっぽい文章を生成するのによく使われるアルゴリズムみたいなものです。
どういうものか例を出して説明すると、
「ぼくはすみくんだよ~」 「はすみくんは神」
上の2つの文章があったとします。これをそれぞれ単語に分割すると
ぼく はすみ くん だ よ ~ はすみ くん は 神
となります。
さらに、これを3単語ずつに分けたブロックを作ります。
[ (START) ぼく はすみ ] [ ぼく はすみ くん ] [ はすみ くん だ ] [ くん だ よ ] [ だ よ ~ ] [ よ ~ (END) ]
[ (START) はすみ くん ] [ はすみ くん は ] [ くん は 神 ] [ は 神 (END) ]
こんな感じになります。
さて、この2つの文から得られたブロックデータを用いて新しい文をマルコフ連鎖で作っていきます。
まず、ブロックの集合から(START)で始まるブロックを抽出します。
今回の場合、[ (START) ぼく はすみ ]、[ (START) はすみ くん ]の2つです。
ブロックが複数個ある場合、ランダムにそのうち一つを選択します。
今回は[ (START) ぼく はすみ ]が選ばれたと仮定して、次に進みます。
次に、選ばれたブロックの最後の単語である「はすみ」から始まるブロックを抽出します。
[ はすみ くん だ ]、[ はすみ くん は ]の2つです。
ランダムに一つを選択します。[ はすみ くん は ]が選ばれたと仮定します。
これで、[ (START) ぼく はすみ くん は ]というフレーズになります。
次は「は」から始まるブロックですが、これは[ は 神 (END) ]の一つなのでここで終了となります。
このようにして、2つの文から「ぼく はすみ くん は 神」という文をマルコフ連鎖で生成することができました。
botの構成
さて、今回作るbotの構成について書き出していきます。
まず、直近n件の自分のツイートを取得してそれぞれを単語ごとに分割します。
このとき、urlや他のユーザーのIDは除外されるようにします。
次に、ツイートごとに3単語のブロックを作ります。このとき、(START)と(END)はnilで表現することとします。
選んだブロックを格納しておくArrayを作っておき、作ったブロックの中から
初回:nilで始まるブロック それ以外:現在までに選んだブロックの最後の単語で始まるブロック
を選んで次々と文にするブロックをpushしていきます。
あとは選んだブロックを文字列に整形し、TwitterAPIを使ってツイートして終わりです。
以上が作成するbotの概要です。 あとはやるだけ!!!次の記事が最後です。
Rubyでマルコフ連鎖botを作る話[2/3:納豆編]
前回のラブライブ!
MeCabを使ってみる
早速MeCabを使ってみます。
MeCabで形態素解析を行うにはmecab
コマンドを実行し、好きな文章を入力します。
実行例:
mecab ぼくはすみくんだよ〜 ぼく 名詞,代名詞,一般,*,*,*,ぼく,ボク,ボク はすみ 名詞,固有名詞,人名,一般,*,*,はすみ,ハスミ,ハスミ くん 名詞,接尾,人名,*,*,*,くん,クン,クン だ 助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ よ 助詞,終助詞,*,*,*,*,よ,ヨ,ヨ 〜 記号,一般,*,*,*,*,〜,〜,〜 EOS ぷよよよよんよをしている ぷよよよよんよ 名詞,一般,*,*,*,*,ぷよよよよんよ,プヨヨヨヨンヨ,プヨヨヨヨンヨ を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ し 動詞,自立,*,*,サ変・スル,連用形,する,シ,シ て 助詞,接続助詞,*,*,*,*,て,テ,テ いる 動詞,非自立,*,*,一段,基本形,いる,イル,イル EOS
ちゃんとユーザー辞書も認識してくれていることがわかります。やったー
RubyでMeCabを使う
RubyでMeCabを使うには
1. 公式に配布されているRubyバインディングを使う
2. gem 'natto'を利用する
の2つの方法があります。今回はnattoを利用することにします。
gemはGemfileに書いてbundle install
してもいいですし、直接gem install natto
してもいいと思います。
以下はRubyからMeCabを利用するサンプルスクリプトです。
require 'natto' text = "ぼくはすみくんだよ〜" nm = Natto::MeCab.new nm.parse(text) do |n| puts "#{n.surface}\t#{n.feature}" end
ruby natto.rb ぼく 名詞,代名詞,一般,*,*,*,ぼく,ボク,ボク はすみ 名詞,固有名詞,人名,一般,*,*,はすみ,ハスミ,ハスミ くん 名詞,接尾,人名,*,*,*,くん,クン,クン だ 助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ よ 助詞,終助詞,*,*,*,*,よ,ヨ,ヨ 〜 記号,一般,*,*,*,*,〜,〜,〜 BOS/EOS,*,*,*,*,*,*,*,*
Natto::MeCabクラスのメソッドparseに形態素解析したい文字列を渡しています。
その返り値をnとして受け取り、surfaceが単語、featureがその詳細…という感じになっている。
nattoについてはもっとできることあるっぽいですがマルコフ連鎖をするにはsurfaceさえ取得できればオッケーなので深くは掘り下げませんでした。
ということで、次のエントリでマルコフ連鎖をやっていきます。