トップ 最新 追記

本 日 の h o g e

hogeとはワイルドカードのようなものです。日々起こった、さまざまなこと −すなわちワイルドカード− を取り上げて日記を書く、という意味で名付けたのかというとそうでもありません。適当に決めたらこんな理由が浮かんできました。

更新情報の取得には rdflirs を使ってもらえると嬉しいです.


09/13/2014 ふむ [長年日記]

tDiary 4342日目

[Py][小ネタ] 人工無脳 amazonas

最近暇を見てこつこつ作っている python 製の人工無脳エンジン. 実は人工無脳は 10 年近く前にも一度作っているのだけれど,もうコードが古くてちょっとアレなので,全面的にフルスクラッチで書き直している.

一応 IRC に住まわせるつもりで作ったものなので IRC Bot のコードも付属しているけれど,エンジン部と IRC Bot 部は完全に分離されている. エンジンは REST 風な API を持つ HTTP サーバで,Bot は API 経由で文章を引っ張ってきて発言する. つまり別に IRC Bot を動かさなくても,文章の学習や生成は可能なので,エンジンを直接叩くための簡易コンソールも付属している. 外側を作れば例えば twitter とかでも動かせるはず.

今はドキュメントもテストもない状態だけれど,大きく手を入れることも少なくなってきたので,そろそろ書こうかなぁ... などと思っているところ.

マルコフ連鎖

この人工無脳エンジンは,いわゆるマルコフ連鎖型の文章生成を行う. 学習時に文章を形態素解析してマルコフ連鎖用のテーブルを作って,適当に単語を繋げていく. しかしただ単に単語 1 語 1 語で繋がりを認識するのでは微妙な点もあるので,N-gram 風のキー生成を行っている. どういうことかと言うと,例えば

わたしはたわしです

という文章をテーブル化する際に,通常なら

わたし → は
は     → たわし
たわし → です
です   → NULL

とするところを,例えば n=2 なら

(わたし,は)   → たわし
(は,たわし)   → です
(たわし,です) → NULL

といった具合にする. n を大きくすると,一文から学習できるパターン数が減るのと,キーへのヒット率が低くなるので,あまり高くしすぎると良くないけれど,2, 3 程度なら大抵の場合問題ない.

マルコフ性というのは「次の状態は今の状態にのみ依存する」という性質のことだけれど,「今の状態」を組み合わせと定義すれば,こういうのもまあアリなんじゃないかと.

追記:
この手のマルコフ連鎖は N 階マルコフ連鎖とか N 次マルコフ連鎖とか言う.上記の例は 2 階マルコフ連鎖. Pさんご指摘ありがとうございます.

連鎖の開始

マルコフ連鎖用にテーブルを作っても,膨大なキーの中で,どこから文章を紡いでいけば良いかが分からない. 完全なランダムで開始語を選ぶのは良くなくて,例えば助詞から始まる文章なんてのはどう見てもおかしい.

このエンジンは直近 m 個の学習文の中で開始語として良い単語を覚えていることができるので,その中から適当に選んで文章を生成しようとする.

なぜ直近 m 個にしたかというと,元々 IRC Bot 用だったから. IRC というのは主題の移り変わりが速いメディアなので,最近話されていたことの中から単語を選べば良いのかな,ということで. ちなみに m=0 の時は全体から開始語として良い単語を探してくるので,この辺は用途で切り替えれば良い.

なお過去の「文脈」をサーチするということはしない. ここで文脈サーチと言っているのは,例えば,ある問いかけに対して,過去似たようなやりとりがなかったかを探して,その返答を真似るといったもの. これをやっていないのは,以下の理由による.

  • マルコフ連鎖を使っている関係上ちょっと実装が難しいかも
  • 実装によっては発言アクションのトリガーが不自由になるかも
  • 学習内容によって返答内容がいつも似たり寄ったりになってしまったりするかも

バリデーションとスコアリング

単語を適当に繋ぎ合わせていくだけだと,文章として成り立っていないものが出来上がってしまうことが多い. 例えば終助詞以外の助詞で文章が終端されてしまったりすると,何だかなぁ感が高い. そのため,文章を生成した後でバリデーションとスコアリングを行って,変な文章が生成されにくいようにしている.

バリデーションは単純で,単に,生成した文章を再度形態素解析してみて,特定の品詞で始まって特定の品詞で終わるものしか許さない,といったもの. その他,括弧が入っていたらダメ,などともしているけれど,これは括弧がペアになっていないと変だなあということで,とりあえず乱暴に括弧を全部 NG にした.

あとエンジンは学習した文章と生成した文章を一定量覚えていることができるので,生成した文章が,過去 p 回の学習/生成文に含まれている場合は NG となる. これはオウム返しや繰り返しが起こらないようにするためのもの.

そしてスコアリング. エンジンは学習した文章の品詞の並びをこれまた一定量覚えている. 学習する文章というのは人間の書いた「まともな」文章であると期待/仮定して,学習した文章の品詞の並びを正として,生成した文章の品詞の並びと比較する.比較した結果,閾値よりスコアが悪かったら NG となる.

品詞の並びの比較にはレーベンシュタイン距離を使う. これは編集距離とも呼ばれるもので,2 つの文字列がどれだけ違うかを数値化するのに使われたりするのだけれど,別に文字列でなくとも,「何らかの並び」であれば適用は可能.

スコアリングはなかなか使い方が難しくて,閾値を高く設定しすぎると生成した文章が全て NG になってしまったりするし,低く設定しすぎると意味をなさなくなったりする. 学習内容によってもどういう文章を生成できるかは変わってくるはずなので,学習時に,学習した文章のスコアを計算し,閾値との平均を取って自動調整している. しかしそうすると今度は閾値が高くなってしまって全然文章を生成できなくなったりするので,実際は生成時にも閾値の調整を行っている. 結果的にスコアは,生成文の中でも特別おかしなものを弾く,という役割になるので,ちょっとくらい変でも通してしまう. まあこの辺りは false positive が云々とかいう世界だろう.

まあそもそも IRC とかだと typo が多くあったりするので,学習する文章を正とするのも微妙だったりするんだけどね :-P

形態素解析器

chasen, mecab, juman 辺りが良く知られていると思うけど,形態素解析といえば mecab,というくらいに mecab がよく利用されていると思う. 実際 10 年近く前に作った人工無脳は mecab を使っていたのだけれど,いくつか不満な点もあった.

  • 空白が無視されてしまうので,単語を連結する際に困ったことになることがある.特に文章中に英単語が連続で出てくると,連結した時に繋がってしまう
  • 分解しなくていいところを分解してしまうことがある.たとえば「n-gram」を解析すると「n」「-」「gram」という具合.この手のものは 1 単語として扱って欲しいことがある

ということで,mecab は引き続き使えるようにしておいた上で,juman も使えるようにした. juman には空白を無視しない,分解して欲しくないところを分解せず未定義語として扱ってくれる等,人工無脳には嬉しい特徴がある.

mecab は python バインディングを通して使うようになっているけれど,juman は別途プロセスを起動して使っている. juman にも python バインディングはあるようだけれど,環境が悪いのか,どうにも SEGV を起こしまくるので,バインディング利用は諦めた.

もし他にも良い形態素解析器があったりすれば対応したい.

データストア

プロセスを落とした時に学習結果が吹き飛んでしまうのは悲しいので,現状 2 種類のデータストアを使えるようにしてある.

  • dict/json
    • データは基本的に全てメモリ上に持ち,終了時に json としてダンプする.ファイルが存在すれば起動時に読み込む
    • 終了時に書き出さない,吹き飛んでよし.というのも可能
    • 気軽に使うことができるが,データが多くなってくるとたぶん苦しい
  • redis
    • 永続化すべきデータは全て redis に入れるので,人工無脳プロセス自身がメモリを圧迫することはない

現状 redis を使うと,マルコフ連鎖を行う際に,分岐が必ず同確率になってしまうという制限がある. これはまあ単に実装が悪いのだけれど,ある集合からランダムに 1 つメンバを得る,という操作を redis の set 型に対する srandmember 命令を使って行っている. しかし set 型というのはメンバの重複が許されない. 重複が許されれば,頻出メンバが選ばれる確率が上がってくれるはずだけれど,そうは問屋が卸してくれなかったというわけ. まあこれはそのうちどうにかするかも.

なお現状では前述の「直近」のようなデータや,自動調整後の閾値などは永続化対象としていないので,プロセスを落とすと揮発する.

応用

IRC のように 1 データが 1 行の場合はここまで.

ちょっと試してみたいことがあって複数行にも対応させたのだけれど,それはまた別の日にでも書こうと思う. というか実はこれを書くための前振りのはずだったのだけれど,疲れ果ててしまった.

本日のツッコミ(全2件) [ツッコミを入れる]

P [この辺あんま詳しくないけど、2 階マルコフ連鎖というのではないかな? < N-gram 風。 レーベンシュタイン距離..]

atzm [おお,そうでした.N階マルコフ連鎖ですね. 何か呼び方があったような気がしつつ適当に流してました :-P ]


09/14/2014 うむ [長年日記]

tDiary 4343日目

[Py][小ネタ] 人工無脳 amazonas 実践編

さて昨日 この人工無脳の構造やアルゴリズム的なところを書いた わけだけれど,実際に,どんな文章を学習するとどんな文章を吐き出すのか,それを簡単に書いてみようと思う.

昨日,最後に「試したいことがあった」と書いたけれど,それは「歌詞の自動生成」. これは「歌詞とか詩的なものって文章の繋がりとか多少変でも気にならないというかむしろ多少変な方がそれっぽいんじゃね?」という変な思いつきから生まれたもの. よく Web 上では「J-POP の歌詞はワンパターンだ」と揶揄されているようだけれど,ワンパターンなら,文章の自動生成にはうってつけの題材でもあるはずだ.

ただし何か特定の用途向けにコードをチューニングするのはつまらないので,パラメタはチューニングするとしても,アルゴリズム自体を歌詞向けに特化させるようなことはしない. あくまでただのマルコフ連鎖型文章生成器で,どのような歌詞を吐き出せるのかを見てみたい.

なおテーマ自体は「歌詞 自動生成」とかでぐぐれば大量に出てくることなので,さして新しいことというわけではない.

動作環境を整える

インストールとかその辺は pip とか ebuild 書くとか setup.py 叩くとかで適当に. インストールすると amzweb (人工無脳 API サーバ),amzcons (簡易コンソール),amzirc (IRC Bot) という実行ファイルが作られるので,設定ファイルと共に起動してやる. サーバの設定は下記としてみた.

[web]
instances = second
host      = 127.0.0.1
port      = 8349
daemon    = false
debug     = true

[module]
parsers   = juman
databases = dictdb

[textgen:second]
score_threshold = 0.0
nr_retry        = 50
nr_history      = 0
nr_wordclass    = 100
nr_entrypoint   = 0

[markov:second]
level    = 2
maxchain = 350

[parser:morph:second]
type   = Juman
path   = /usr/bin/juman
encode = utf-8

[db:markov:second]
type = Dict
path = markov.json

[db:entrypoint:second]
type = Dict
path = entrypoint.json

形態素解析器に juman を,データストアに dict/json を使い,2階マルコフ連鎖で最大 350 回まで連鎖させる. ただし連鎖数は初期値.連鎖数は学習する文章の単語数から勝手に調整されていく. スコアの初期値も 0 としておいて,閾値は学習から適当に調整させる. 歌詞生成において「最近の話題を」とか「繰り返しは禁止」というようなことは不要なので,ヒストリ等は 0 にしてある.

$ amzweb amzweb.ini
 * Running on http://127.0.0.1:8349/

設定した "second" という名前の人工無脳インスタンスを起動したので, コンソールから "second" インスタンスを操作する.

$ amzcons second
amzcons>

コンソールから learn というコマンドを使って,ファイルから文章を学習することができる.

一行文生成

手始めに西野カナの歌詞 109 個を一行ずつ学習させた. その結果,下記のような状態となった.

amzcons> stat
score threshold: 0.164676
markov maxchain: 6
markov keys:     8942
entrypoints:     1340

実際に文章を生成してみる.

amzcons> print
近すぎると怖くなって [0.167785]

ふむ何やらそれっぽい.最後の数値はこの文章のスコアを示す.

一行文の連結による歌詞の生成

一行ずつ学習させた結果だから一行ずつしか出てこないので,print コマンドを何度か叩いて結果だけを集めた結果がこちら. なお空行は適当に手で入れた.

近すぎると怖くなって
リピートできないくらい泣いて
嫌いだった鏡に問いかける

久々のオフ 天気も最高
ふたりで過ごしたこと
顔くっつけ合って

恋が凍えてる
先週もまた怒られて
ここに戻ってる
側にいたいよ

最後の恋に恋して
顔くっつけ合って

アイライナー濃いめで
苦いコーヒー流しこみ
この世を去った

何だかよく分からないうちに何かが死んでしまった...

「混ぜる」

意味は分からないが何かちょっと面白いので,悪ノリして Luna Sea の歌詞を 106 個ほど追加学習させてみる. 交わりそうにない西野カナ分と Luna Sea 分を混ぜてみるのだ. 結果は下記の通り.

amzcons> stat
score threshold: 0.150078
markov maxchain: 7
markov keys:     14704
entrypoints:     1875

では文章の生成を行ってみる.

amzcons> print
冷たく透き通る瞳の 恋の色☆ [0.155039]

テンションが低いのか高いのかよく分からない. まあ面白いのでこのままいくつか文章を生成させていく.

冷たく透き通る瞳の 恋の色☆
ナビに怒ってる君を愛してる

伝説の夜に 僕が消えて行く

リミットに気持ちが揺れてる
夢から覚めてすべての事があった

先週もまた怒られて
ローマ風の中でずっと叫んでる
せつなくて ずっと昔から知ってるみたいだね
次の日の未来が違っていたんだ

楽園に刻まれた こんな自分を好きに
伝説の夜 抱きしめた…

淋しげな歌を聴いて
雨音を夜まで数え
夢の中 鍵を探している
窓に映った 自分見つめて

思い通りにはいかないかもしれないけど叫ぶ
限り無く 今はほどいて 頼むからどいて

コノ胸ノ アリフレタ 夢に見る

最初と最後,このテンションの差である.

複数行文生成

一行一行生成して,それっぽいところで手動で空行を挿入するというのもまあ良いのだけれど,そうすると人間の脳と手が介在することになってめんどくさい. この人工無脳エンジンは複数行から成る文章の学習と生成にも対応させてあるので,一発で一気に歌詞を生成できるように,一行一行ではなく歌詞全体を一発で学習させてみる.

AKB48 の歌詞 164 個を行毎ではなく作品毎に学習させてみた結果がこちら.

amzcons> stat
score threshold: 0.267828
markov maxchain: 306
markov keys:     18086
entrypoints:     129

複数行から成る文章の場合,文章中に含まれる単語の品詞の羅列をそのままスコアリングに使ってしまうと,恐ろしく低い値となってしまう (長文同士の比較となり全く似ない). そのため複数行の場合でもスコアリングは行毎に行って,その平均値を使う形にしてある.

さてこの状態で print コマンドを打ってみた結果が以下.

amzcons> print
目の前にそびえる
悩みながら
あの日の栞
輝いた青春の熱は
ヘビーローテーション

時は静かに
広がってく波紋
その瞬間
自分のMINDで動けよ
熱く 燃え尽きるまで
別の力
生まれた場所

やさしさを心の道で
見つけた
どこへ行けばいいのか?

喧嘩して 泣いたこと あきらめかけても
木っ端なんか
ここで死ぬのだろう [0.273942]

...また死んだ.なぜだ.

複数行文も「混ぜる」

では AKB48 分と THE BLUE HEARTS 分を混ぜてみよう. THE BLUE HEARTS の歌詞 93 個を追加学習させた結果がこちら.

amzcons> stat
score threshold: 0.169714
markov maxchain: 160
markov keys:     23498
entrypoints:     199

どうやら歌詞の構成の傾向が違うようで,スコア閾値や最大連鎖数がだいぶ下がっているのが見て取れる.

では歌詞を生成してみよう.

amzcons> print
レストラン レストラン
レストランに行きたい
遠くに見えても
何か別の答えを探す
愛し合おうぜ
裸になっていたんだ

M・O・N・K・E・Y 燃えている

校庭は一面
鈍い銀の世界を
忘れられない

楽しい事をたくさんしたい 喚きたい
ミサイルほどのペンを片手に
僕たちの隠した牙

恋は
ずっと手を取り 連れ出したい
妄想だけじゃ
はかれない
目の前のマネキンたち 何かキレる音がした
アイスのときめき
ハートに火を吹くぜ

明日は明日のために
おもしろい事をたくさん見たよ
何よりも しょうがないから
オレの心が騒ぐよ
しまっておけない [0.174378]

何言ってんだこいつ.

3階マルコフ連鎖

ここまで 2階マルコフ連鎖で文章を生成してきたけれど,3階マルコフ連鎖としてみよう. 設定ファイルを下記のように書き換えて学習し直せば,3階マルコフ連鎖で文章生成を行える.

[markov:second]
level    = 3

この状態で Luna Sea の歌詞 106 個を学習させて歌詞を生成.

amzcons> print
この詩 今夜おくろう君に かけがえない君に
変わることない風に
失いかけていたんだ この想いさえ
記憶の扉 鍵を壊して uh 二人の隙間で 育ってゆく物は
消えた 記憶のトビラ 開くカギか

Break your mind,
going back the dream I can't live without you
このまま 君だけは ぬれないで ずっと
さよなら 君だけは 微笑んで ずっと
さよなら 揺れていた せつなくて ずっと
さよなら 揺れていた せつなくて ずっと
このまま 君だけは 大切な事 抱きしめていて

嘘の世界であなたと二人
愛し合ってみたい
キミが欲しい キミの匂いと
あどけない 微笑みが
塞がれた こんな夜には
ROSIER 近づけない
ROSIER 抱き締められない
貴方が与えて呉れた一生をばらばらに壊したい

瞳を閉じて フラッシュの中 散り咲こう 無情な夜
今誓う空虚の中 散り咲こう [0.132440]

やはり学習したフレーズがそのまま出てきてしまう率が上がるように見える. 過去の履歴を加味して次の状態を決める場合,次の状態への分岐が少なくなってしまうため,数値を大きくすると「そのまま感」が出てきてしまう.

ただ学習数を増やせば分岐も増えるはずではあるので,このまま AKB48,THE BLUE HEARTS,西野カナを全て混ぜてみて,文章を生成してみよう.

amzcons> print
夢まで寝静まる街
ベッドから抜け出して
大人を起こさないように

パンパンパン パンパンパン
パーンと弾けて 飛んで行け

前を邪魔する奴は
喧嘩上等 明け暮れて
無意味なことだとわかった
生意気な奴をボコボコにしても
僕はぼんやりと
眺めていた
苦しめたくない でも忘れられない 何故?
涙 拭いて
歩き出そうよ
そこに 岸はあるんだ

やわらかい君の声を聞かせて 涙を止めて
Dancing on me
life is cool.

ありふれた毎日が
Baby I like candy candy oh
そうテレビをつけたって
話題のパンケーキ特集だって
妄想限界 Help me
life is "LOVE IS OVER
I tell you
just wait for beauty…
貴方のため 生まれ変わる [0.167322]

やっぱ混ざらんわ.無理.

まとめ

おそらく,似たテイストを持つアーティストに絞ったりして学習を進めていけば,それっぽいものを吐き出せるようにはなるんじゃないかと思う. ただ,歌詞というものには主題となるものがあって,ある種の一貫性を文章全体として求めてくる. マルコフ連鎖で文章を生成する以上,どこまでも確率によって内容が展開されていくので,主題の違う文章を学習させてしまうと,生成された文章の中で矛盾を生んだりすることになる. 「このインスタンスはこういう文章の生成用」等といったように主題別に学習を進めて行けば,もしかしたらそれっぽいものを吐き出せるようになるかも知れない.

あと複数行から成る文章の学習と生成は昨日ざくっと適当に作ったものなので,まだまだコードとしてのチューニングの余地があるのではと思っている. 特に,スコアリングを一行一行に対して行うようにしたのは悪手だったかも知れないと思っている.全体の構造が無視されてしまうからだ. まあ,この辺はまたおいおいと詰めて行こうと思う.

この世を去った