Rock Book

history | grep `future`

マルコフ連鎖でラップボットつくった

追記 (2020/01/14)

BERT 版はこちら

masaki925.hatenablog.com

はじめに

こんにちは。ヴァイブス満タンですか?

この投稿は LINEBot&Clova Advent Calendar 2018 の14日目の投稿です。

一昨年昨年 に続いて、ラップボットの第3弾となります。

昨年はDopeLearning を使って、ノスタルジックなラッパー、ピーさんをリリースしました。

昨年を振り返って反省したこととしては、「バトル感が無い」ということでした。

そこで今年はもっとバトル感を醸成すべく、世のラッパーたちが過去に残したパンチラインを学習し、前人未踏のパンチの効いたヴァースをかますラッパーを生み出したいと考えました。

そこで目をつけたのが、「マルコフ連鎖」という文章生成モデルです。

マルコフ連鎖での文章生成については、下記の記事がわかりやすかったので紹介させていただきます。

f:id:masaki925_8107:20181222121617p:plain 引用元: マルコフ連鎖による文章生成 - 知識のサラダボウル

簡単に言うと、上記の図の通り、「ある単語が来たら次に来そうな単語はこれだ」というパターンをあらかじめ学習しておくことで、パターンに沿って自由に文章を生み出すことができるようになります。

そうして生まれたのがこちら、「お麩」界のカリスマこと、DJ マルコ です。

f:id:masaki925_8107:20181222114330p:plain:w300

マルコにバトルを仕掛けると、こんなかんじでケンカを買ってくれます。

f:id:masaki925_8107:20181223182813p:plain:w300

いかがでしょうか。

「お前のライム ナチュラル無農薬野菜」は、攻撃力が無くてむしろ健康になっちゃうぜ、的なdis りなのか、あるいは手間ひまかけていいもの作ってるね、的な賛辞なのか、一瞬戸惑ってしまう奥深いパンチラインですね。

そんなDJ マルコとバトルをしてみたいという奇特な方がいらっしゃったら、ぜひ下記のリンクからバトルを仕掛けてみてください。

f:id:masaki925_8107:20181223183919p:plain:w200

友だち追加

DJ マルコの育て方

サーバー構成について

  • チャットボットのフロントエンドとして、LINE Messanger API + Rails を利用しました。Rails なのは前回のコードを流用したからです。

github.com

  • ラップ生成エンジンを別API として、Python(Flask) で動くWeb API をDocker で固めてHeroku にデプロイしました

github.com

ラップ生成エンジンについて

大変だったこと

どうすればラップっぽい文章が生成できるか

マルコフ連鎖で今回ポイントになると考えた点と、TextGenerator での実装は下記の通りでした。

  • スタートをどう始めるか
    • 開始フラグ( __BEGIN__ ) がついたノードをランダムに選択
  • 途中のノードをどう選択するか
    • 出現比率に応じてランダム
  • 文章の最後はどう終わるか
    • 終了フラグ( __END__ ) がついたノードに当たったら

ここで、ラップバトルには「アンサー」と呼ばれる、相手のヴァースに対してメンションし、うまく次の攻撃に繋げるという特徴があることから、スタート部分を下記のとおり改変しました。

  • チャットで受け取った文をMeCab形態素解析し、単語を含むレコードを起点として文章を生成

しかし、今回は、パンチラインの数が少なかったこともあり、該当する単語がDB に無いことが頻発しました。

そこでword2vec で類似する単語を推定し、元の単語が見つからないときには類似単語でレコードから参照するようにしています。

word2vec の語彙コーパスとしては、 Livedoor コーパス を利用しました。

その他つまづきポイント

heroku container:push が重い

MeCab の辞書を含んでいたため、1GB を超えるファイル転送が走り、待ち時間が長くなってしまうことがあったため、なるべくローカルでdocker run して問題がないことを確認してからpush するようにしました

Mac とalpine のmecab で、surface の返り値が異なる

これがなかなか曲者で、「俺がウドの大木」という文章をパースした際、下記のような違いがありました

Mac

mt = MeCab.Tagger()
node = mt.parseToNode('俺がウドの大木')
node = node.next # EOS をスキップ
node.feature
#=> '名詞,代名詞,一般,*,*,*,俺,オレ,オレ'
node.surface
#=> '俺'

Alpine

(途中省略)

node.feature
#=> '名詞,代名詞,一般,*,*,*,俺,オレ,オレ'
node.surface
#=> '俺がウドの大木'

わかりづらい。。

今後の展望

  • 韻を踏めてないので、踏めるようにしたい
  • パンチラインのデータベースを増やしたい
  • マルコフ連鎖のノード選択を、ランダムではなく何かしらのロジックを入れたい

以上となります。