Theories of Pleiades

技術の話とかイベントに行った話とか思ったこととか

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単語ずつのブロックを生成します。 f:id:mwc922_hsm:20180825101226j:plain


最後にマルコフ連鎖をする関数を作ります。

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は考えられるブロックを一時的に格納しておく配列です。


マルコフ連鎖ループの中で何度も呼び出される
- 考えられるブロックを抽出する - ブロックを連結する

の操作は個別に関数にしてあります。 f:id:mwc922_hsm:20180825101257j:plain



完成

ということで完成したものがこちらです。

twitter.com

github.com

なんか一気に飛ばした気もしますがコード全部載せても仕方ないのであとの細かいところはGitHubを参照してください。


マルコフ連鎖botは簡単なのでみんな作りましょう。自分の文体でbotが支離滅裂な文章を生成してツイートしてるの見るのは結構面白いです。

以上、Rubyマルコフ連鎖botを作った話でした!

Rubyでマルコフ連鎖botを作る話[3/3:理論編]

前回のラブライブ!

RubyからMeCabが使えるようになったにゃん

mwc922-hsm.hatenablog.com



マルコフ連鎖ってなーに

マルコフ連鎖(マルコフれんさ、英: 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の概要です。 あとはやるだけ!!!次の記事が最後です。

mwc922-hsm.hatenablog.com

Rubyでマルコフ連鎖botを作る話[2/3:納豆編]

前回のラブライブ!

mwc922-hsm.hatenablog.com

  1. しゅまいになりたい
  2. MeCabをインストールした
  3. MeCabの辞書をパワーアップした


MeCabを使ってみる

早速MeCabを使ってみます。

MeCab形態素解析を行うにはmecabコマンドを実行し、好きな文章を入力します。
実行例:

mecab
ぼくはすみくんだよ〜
ぼく    名詞,代名詞,一般,*,*,*,ぼく,ボク,ボク
はすみ  名詞,固有名詞,人名,一般,*,*,はすみ,ハスミ,ハスミ
くん    名詞,接尾,人名,*,*,*,くん,クン,クン
だ      助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ
よ      助詞,終助詞,*,*,*,*,よ,ヨ,ヨ
〜      記号,一般,*,*,*,*,〜,〜,〜
EOS
ぷよよよよんよをしている
ぷよよよよんよ  名詞,一般,*,*,*,*,ぷよよよよんよ,プヨヨヨヨンヨ,プヨヨヨヨンヨ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
し      動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
て      助詞,接続助詞,*,*,*,*,て,テ,テ
いる    動詞,非自立,*,*,一段,基本形,いる,イル,イル
EOS

ちゃんとユーザー辞書も認識してくれていることがわかります。やったー


RubyMeCabを使う

RubyMeCabを使うには
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さえ取得できればオッケーなので深くは掘り下げませんでした。



ということで、次のエントリでマルコフ連鎖をやっていきます。

mwc922-hsm.hatenablog.com