BERT でBATTLE
はじめに
こんにちは。
今日も元気に言葉のウェイトトレーニングしてますか?
本記事は、LINE Bot Advent Calendar 2019 の23日目の記事となります。
今回も懲りずに、ラップボットシリーズの第4作目です。
前回はマルコフ連鎖を用いて、血の気盛んなバトルラッパー DJマルコを生み出しました。
今回はもっとAI や機械学習的なことを ということで、 最近話題の自然言語処理モデル、BERT を利用してみました。
BERT に関しては、こちらの記事がわかりやすかったので、もしご存知ない方は参照してみてください。
Googleが公開した自然言語処理の最新技術、BERTとは何者なのか | AI-SCHOLAR
余談ですが、ラップボットが取り組む問題は文章生成なので、 個人的にはBERT と同じく話題の言語モデル GPT-2 (参考記事) を利用してみたかったのですが、 日本語での事前学習モデルが公開されていなかったため、 今回は京都大学の研究室が公開している、BERT の日本語事前学習モデルを利用させていただくこととしました。
BERT日本語Pretrainedモデル - KUROHASHI-KAWAHARA LAB
と、御託はこのくらいにして、今回のラッパーの紹介です。
今回のラッパーは、こいつだ!
本人は全くその気はないのに、大御所の存在により演歌歌手に間違えられがちな咆哮系ラッパー、鳥羽さんです。
ぜひお友達になって、バトルしてみてください!
… と言いたいところだったのですが、
なんとこの鳥羽さん、パフォーマンスに難ありでデプロイできず、
お披露目することができませんでした。。
くやしい。
代わりにいくつか生成したパンチラインを紹介するに留めておきたいと思います。
詳細は後述しますが、以下は収集したラップバトルのパンチラインを学習(fine tuning) させてモデルを構築し、文字数を指定(下記は40) して文章を自動生成したものになります。
おいACE[UNK][UNK][UNK][UNK][UNK][UNK]に教えてやる Beatで踏んでるお客さんそれがお豆
突然のお豆。 ([UNK] はUnkown の略で、うまく言葉を生成できなかった部分です。)
俺にとってはビビい所から来たメンタルはやっぱりこのスキルをアップデートしないとこだよ
くー。今回のスキル不足を指摘されてるみたいで刺さる。精進します。
アンタに勝てるぜレペゼンパッパラパパパパパッパラ そっからはお客さん
レペゼンどこそれ?からの無茶振りw
お前を見てヤバいブーメラン本当そうだな このステージのお母ちゃんテメーじゃん
「ブーメラン」「お母ちゃん」「テメーじゃん」で韻踏んでるような。。?
とまぁこんなかんじになりました。
正直、クオリティ的には過去のラッパーたちに比べても大きく改善したとはいいにくいですが、
「ビビい所」「パッパラパパパパパッパラ」といった造語っぽいものや、
上記にもあった韻を踏んでるっぽいところは、今後の可能性の片鱗を見せてくれたような気がします。
今後、言葉のウェイトとは裏腹に身軽になった鳥羽さんが降臨してくれることに期待しましょう…!!
以下、実装編となります。
鳥羽さんの作り方
概要
GitHub - masaki925/bert_rap: BATTLE on BERT
(あんまり整備できてないので気になる部分があればお気軽にお声がけください)
- 構成
方針
下記の記事を参考にさせていただきました。
こちらで紹介されている BertMouth は、 自分でテキストを用意してfine tuning することで独自モデルが構築できるようになっています。
生成時は指定された長さの文字列を穴埋め問題化して、1文字ずつ予測して埋めていくという手法です。
ので、今回のポイントは
- どういったデータを
- どのくらい
- どういった形式で用意するか
といったものでした。
まずはデータ集めです。
データセット
- フリースタイルダンジョン の書き起こしを利用させていただきました
放送回と、書き起こしサイトの対応表も一応載せておきます。
単純にクロールして済む問題でもないので、地道に丹精込めて集めました。
フリースタイルダンジョン書き起こし - Google スプレッドシート
※ なおYouTube の字幕も見てみましたが、やはり日本語の精度はまだ使い物にならなかったです。
単純な1行の文章でいくと、2722文を集めることができました。
ポケモンの方が4000文以上集めていたのでそこを目指したのですが、やや及ばずでした。
実物はこんなかんじです。
高鳴って来たぜ心拍数 コイツは俺に負けて自身無くす この会場で俺は進化する 俺がバトルを始めればみんな夢中 ゴングは鳴ったぜ文句はあっか これは荒手のモンスターハンター 俺からすればボンクララッパー 返してこいちゃんとしたドープなアンサー
フリースタイルダンジョン:シーズン1 REC1:Dragon One VS T-PABLOW | フリースタイル文字起こし
これは記念すべき1回目の放送(っぽい) で、Dragon One のバースですね。
教科書のようなライミングです。
他にも、
Remember ファーストチャレンジャー I’m a danger あ?二代目だ?あんまナメんな これは事件だ 後ろにはアベンジャーズ やっぱり ヤベェメンバーも集まってんだよ 余裕で獲る天下 これは口喧嘩 度が過ぎてんだ 若干してるぜ 劣化 与えるぜ プレッシャー 速攻でステップアップ 崇勲? 俺1人で充分
https://hiphopzanmai.com/freestyledungeon/81/
またもやDragon One のバースですが、 こんなかんじで、短めの一行もあれば、これらがまとめて一行になっているものも混ざっています。
こちらを使って学習していきます。
学習
手元のMacbook で学習しようとしたところ時間がかかりすぎたため、colab を利用しました。
GPU がサクッと使えるのは感動的ですね。
デフォルトのランタイムがCPU にセットされていて、GPU に変更するとそれまで利用したファイルが消えてしまうので要注意です。
また時間(90分?) が経つとセッションが切られて、これまたダウンロード・インストールしたファイル (特にBERT モデルなど、大きいファイル) が削除されてしまいきっつー となるので、Auto Refresh を入れて定期的に接続するようにしました。
が、学習途中でリフレッシュされて学習が止まってしまったことがあった(たぶん。1回学習を回すのに1時間以上かかるため、待ちながら他のことやってたりするので記憶が曖昧になる。。) ので、要注意でした。。
学習の様子
12/21/2019 11:34:51 - INFO - __main__ - train starts Epoch: 0% 0/100 [00:00<?, ?it/s]12/21/2019 11:34:57 - INFO - __main__ - [1 epochs] train loss: 1.02 12/21/2019 11:34:58 - INFO - __main__ - sampled sequence: ののののののののののののののののののののののののは 12/21/2019 11:34:58 - INFO - __main__ - [1 epochs] valid loss: 1.02 Epoch: 1% 1/100 [00:06<11:14, 6.82s/it]12/21/2019 11:35:04 - INFO - __main__ - [2 epochs] train loss: 0.965 12/21/2019 11:35:05 - INFO - __main__ - sampled sequence: ははははははははははははははははははははははははは 12/21/2019 11:35:05 - INFO - __main__ - [2 epochs] valid loss: 0.96 Epoch: 2% 2/100 [00:13<11:16, 6.91s/it]12/21/2019 11:35:11 - INFO - __main__ - [3 epochs] train loss: 0.908 12/21/2019 11:35:12 - INFO - __main__ - sampled sequence: ののののののののののののののののののののののののは 12/21/2019 11:35:12 - INFO - __main__ - [3 epochs] valid loss: 0.894 Epoch: 3% 3/100 [00:20<11:06, 6.87s/it]12/21/2019 11:35:18 - INFO - __main__ - [4 epochs] train loss: 0.829 12/21/2019 11:35:21 - INFO - __main__ - sampled sequence: 俺はは俺は俺は俺は俺俺は俺俺は俺は俺は俺は俺は俺は 12/21/2019 11:35:21 - INFO - __main__ - [4 epochs] valid loss: 0.847 Epoch: 4% 4/100 [00:30<12:09, 7.60s/it]12/21/2019 11:35:27 - INFO - __main__ - [5 epochs] train loss: 0.799 12/21/2019 11:35:31 - INFO - __main__ - sampled sequence: 俺のの俺ののの俺ののがねぇねぇ俺のののののののねぇ 12/21/2019 11:35:31 - INFO - __main__ - [5 epochs] valid loss: 0.869 Epoch: 5% 5/100 [00:39<12:51, 8.12s/it]12/21/2019 11:35:36 - INFO - __main__ - [6 epochs] train loss: 0.771 12/21/2019 11:35:40 - INFO - __main__ - sampled sequence: ねねぇねねぇ俺はお前はお前はねぇ俺お前ははお前はお前はねねぇ 12/21/2019 11:35:40 - INFO - __main__ - [6 epochs] valid loss: 0.766 Epoch: 6% 6/100 [00:48<13:15, 8.47s/it]12/21/2019 11:35:46 - INFO - __main__ - [7 epochs] train loss: 0.762 12/21/2019 11:35:48 - INFO - __main__ - sampled sequence: がねねぇ俺は俺は俺は俺が俺がねぇぜ俺は俺は俺は俺は 12/21/2019 11:35:48 - INFO - __main__ - [7 epochs] valid loss: 0.82 Epoch: 7% 7/100 [00:56<13:04, 8.43s/it]12/21/2019 11:35:54 - INFO - __main__ - [8 epochs] train loss: 0.693 12/21/2019 11:35:55 - INFO - __main__ - sampled sequence: お前の俺の俺のの俺の俺の俺の俺の俺の俺ののだぜお前のの 12/21/2019 11:35:55 - INFO - __main__ - [8 epochs] valid loss: 0.827 Epoch: 8% 8/100 [01:04<12:18, 8.03s/it]12/21/2019 11:36:01 - INFO - __main__ - [9 epochs] train loss: 0.729 12/21/2019 11:36:04 - INFO - __main__ - sampled sequence: 俺はお前の俺だぜぜお前の俺のは俺の俺だぜ俺がだねぇねぇ 12/21/2019 11:36:04 - INFO - __main__ - [9 epochs] valid loss: 0.763 Epoch: 9% 9/100 [01:13<12:37, 8.32s/it]12/21/2019 11:36:10 - INFO - __main__ - [10 epochs] train loss: 0.738
最初は「俺の」しか言わない
12/21/2019 11:41:45 - INFO - __main__ - sampled sequence: 俺はオメェ俺はHIPHOPフロウだよ?まぁまぁラップしてるぜこの勝利 12/21/2019 11:41:45 - INFO - __main__ - [50 epochs] valid loss: 0.617 Epoch: 50% 50/100 [06:53<06:32, 7.84s/it]12/21/2019 11:41:50 - INFO - __main__ - [51 epochs] train loss: 0.528 12/21/2019 11:41:53 - INFO - __main__ - sampled sequence: 俺らはここで俺の方が俺はこの上でじゃないぜ俺の方が俺の方だ 12/21/2019 11:41:53 - INFO - __main__ - [51 epochs] valid loss: 0.607 Epoch: 51% 51/100 [07:01<06:27, 7.90s/it]12/21/2019 11:41:58 - INFO - __main__ - [52 epochs] train loss: 0.517 12/21/2019 11:42:01 - INFO - __main__ - sampled sequence: お前のフロウの上には踏めないオメェがヤバいしてるぜこのアンタの 12/21/2019 11:42:01 - INFO - __main__ - [52 epochs] valid loss: 0.561 Epoch: 52% 52/100 [07:10<06:29, 8.12s/it]12/21/2019 11:42:07 - INFO - __main__ - [53 epochs] train loss: 0.529 12/21/2019 11:42:11 - INFO - __main__ - sampled sequence: 俺の事俺は俺はこの事じゃないのは何だよ俺の事じゃないののか? 12/21/2019 11:42:11 - INFO - __main__ - [53 epochs] valid loss: 0.614 Epoch: 53% 53/100 [07:19<06:37, 8.46s/it]12/21/2019 11:42:16 - INFO - __main__ - [54 epochs] train loss: 0.478 12/21/2019 11:42:18 - INFO - __main__ - sampled sequence: 俺らはこの上のアンタに1人俺らはアンタはこの事じゃないぜ俺の事 12/21/2019 11:42:18 - INFO - __main__ - [54 epochs] valid loss: 0.611 Epoch: 54% 54/100 [07:27<06:18, 8.24s/it]12/21/2019 11:42:24 - INFO - __main__ - [55 epochs] train loss: 0.511 12/21/2019 11:42:28 - INFO - __main__ - sampled sequence: このアンタ俺がフリースタイル俺が俺の方がお前らだけじゃないオメェお前らだよ 12/21/2019 11:42:28 - INFO - __main__ - [55 epochs] valid loss: 0.627 Epoch: 55% 55/100 [07:36<06:24, 8.54s/it]12/21/2019 11:42:33 - INFO - __main__ - [56 epochs] train loss: 0.538 12/21/2019 11:42:36 - INFO - __main__ - sampled sequence: このクソだよアンタの上だけじゃねぇどっちがHIPHOPアンタだよここで 12/21/2019 11:42:36 - INFO - __main__ - [56 epochs] valid loss: 0.696 Epoch: 56% 56/100 [07:45<06:18, 8.59s/it]12/21/2019 11:42:42 - INFO - __main__ - [57 epochs] train loss: 0.531 12/21/2019 11:42:45 - INFO - __main__ - sampled sequence: フロウ俺はフロウで何年もここでここでここで踏めないか?ここでこの勝利 12/21/2019 11:42:45 - INFO - __main__ - [57 epochs] valid loss: 0.606 Epoch: 57% 57/100 [07:53<06:09, 8.60s/it]12/21/2019 11:42:51 - INFO - __main__ - [58 epochs] train loss: 0.518 12/21/2019 11:42:53 - INFO - __main__ - sampled sequence: ヤバい言葉の上俺がヤバいぜ俺らがこのフロウの上じゃない俺らが 12/21/2019 11:42:53 - INFO - __main__ - [58 epochs] valid loss: 0.568 Epoch: 58% 58/100 [08:02<05:58, 8.54s/it]12/21/2019 11:42:59 - INFO - __main__ - [59 epochs] train loss: 0.536 12/21/2019 11:43:03 - INFO - __main__ - sampled sequence: 何がないぜこの事フリースタイルマジでフロウだけじゃないぜオメェはヤバいだけだ 12/21/2019 11:43:03 - INFO - __main__ - [59 epochs] valid loss: 0.63 Epoch: 59% 59/100 [08:11<05:58, 8.75s/it]12/21/2019 11:43:08 - INFO - __main__ - [60 epochs] train loss: 0.503
だいぶ語彙が増えてくるが、同じ言葉を繰り返してしまったりしている。
12/21/2019 11:47:37 - INFO - __main__ - sampled sequence: FORK俺が俺の方俺の方がこのラップには俺の方じゃないぜどっちの方 12/21/2019 11:47:37 - INFO - __main__ - [91 epochs] valid loss: 0.578 Epoch: 91% 91/100 [12:45<01:17, 8.63s/it]12/21/2019 11:47:42 - INFO - __main__ - [92 epochs] train loss: 0.455 12/21/2019 11:47:45 - INFO - __main__ - sampled sequence: このラップは踏めないぜお前らは俺のオメェのフロウはHIPHOP俺の方 12/21/2019 11:47:45 - INFO - __main__ - [92 epochs] valid loss: 0.585 Epoch: 92% 92/100 [12:54<01:08, 8.62s/it]12/21/2019 11:47:51 - INFO - __main__ - [93 epochs] train loss: 0.428 12/21/2019 11:47:54 - INFO - __main__ - sampled sequence: このフロウフロウ大丈夫だろ大丈夫俺はアンタにはここにはない大丈夫俺は 12/21/2019 11:47:54 - INFO - __main__ - [93 epochs] valid loss: 0.605 Epoch: 93% 93/100 [13:02<00:59, 8.55s/it]12/21/2019 11:47:59 - INFO - __main__ - [94 epochs] train loss: 0.418 12/21/2019 11:48:01 - INFO - __main__ - sampled sequence: 俺の方がこの勝利まぁ[UNK][UNK][UNK][UNK][UNK][UNK][UNK][UNK][UNK][UNK][UNK][UNK][UNK][UNK][UNK][UNK][UNK] 12/21/2019 11:48:01 - INFO - __main__ - [94 epochs] valid loss: 0.624 Epoch: 94% 94/100 [13:09<00:49, 8.19s/it]12/21/2019 11:48:07 - INFO - __main__ - [95 epochs] train loss: 0.423 12/21/2019 11:48:09 - INFO - __main__ - sampled sequence: 俺の方ぶちがコイツがどっちとかだよ?お前らのフロウで俺らは 12/21/2019 11:48:09 - INFO - __main__ - [95 epochs] valid loss: 0.553 Epoch: 95% 95/100 [13:17<00:40, 8.13s/it]12/21/2019 11:48:15 - INFO - __main__ - [96 epochs] train loss: 0.435 12/21/2019 11:48:18 - INFO - __main__ - sampled sequence: 来い大丈夫だ俺は俺らだったらNo.1俺ら1人の方が俺の方だ 12/21/2019 11:48:18 - INFO - __main__ - [96 epochs] valid loss: 0.565 Epoch: 96% 96/100 [13:26<00:32, 8.20s/it]12/21/2019 11:48:23 - INFO - __main__ - [97 epochs] train loss: 0.419 12/21/2019 11:48:26 - INFO - __main__ - sampled sequence: 何?俺が俺の方が俺がこのラップだけだよ?お前らは大丈夫?ここで俺 12/21/2019 11:48:26 - INFO - __main__ - [97 epochs] valid loss: 0.582 Epoch: 97% 97/100 [13:34<00:24, 8.26s/it]12/21/2019 11:48:32 - INFO - __main__ - [98 epochs] train loss: 0.464 12/21/2019 11:48:35 - INFO - __main__ - sampled sequence: この勝利俺はNo.大丈夫だろこの場でラップO....ラップしてるぜ一回 12/21/2019 11:48:35 - INFO - __main__ - [98 epochs] valid loss: 0.634 Epoch: 98% 98/100 [13:43<00:17, 8.55s/it]12/21/2019 11:48:41 - INFO - __main__ - [99 epochs] train loss: 0.431 12/21/2019 11:48:43 - INFO - __main__ - sampled sequence: もうここで1回もコイツ俺はこの上に行くぜこのラップよりもいいよそんな事だぜ 12/21/2019 11:48:43 - INFO - __main__ - [99 epochs] valid loss: 0.647 Epoch: 99% 99/100 [13:51<00:08, 8.30s/it]12/21/2019 11:48:48 - INFO - __main__ - [100 epochs] train loss: 0.418 12/21/2019 11:48:51 - INFO - __main__ - sampled sequence: くだらうぜフロウの事俺らの方じゃないのかだろここで俺らの方が 12/21/2019 11:48:51 - INFO - __main__ - [100 epochs] valid loss: 0.523
あんまりいい例を選び損ねてしまったのですが、段々と性能が上がっていることが見て取れます。
試したこと
学習で試したパターンは以下の通りです
- 1行ずつ
- 1-1. 約1200文 (train : valid = 600 : 100)
- 1-2. 約2700文 (2500 : 200)
- 1ターンをまるごと1行にする
- 2-1. 約380文 (300 : 80)
- 2-2. 約2400文 水増し + (2200 : 200)
- 2-3. 約2400文 水増し + 半角スペースを全角スペースに置換 したもの (2200 : 200)
まず、最初の1200文ではいい文章にならなかったため、諦めて 気合を入れて残りを集めました。データ量は正義ですね。
まるごと1行に関しては、「アンサー」を再現したかったので実施しました。
XXXXX >>> OOOOO
のような形式に加工し、右側(O の部分) を予測するよう学習させれば、ラップバトルのように相手の言葉を加味して返答を考える、といったことを期待しました。
また、1つ前だけではなく複数前のターンの内容も加味させられると面白くなるかなぁと。
その前段として、1行にまとめるのを試して、、、今回は燃え尽きました。
また「水増し」に関して、
1行にまとめると文章数が減ってしまうため、やはり精度が悪く使い物になりませんでした。
そこで、汚職政治家と機械学習業界ではおなじみの水増しを実施しました。
文章数を6倍くらいにしたかったため、 文章の中から名詞を抽出し、random で6つ選び、韻を踏んでいる単語に置き換えました。単語の検索には前回もお世話になった、「WEB 便利ツール」を利用しました。
こんなかんじ
変換前: ドープ 変換後: 覆う 高鳴って来たぜ心拍数 コイツは俺に負けて自身無くす この会場で俺は進化する 俺がバトルを始めればみんな夢中 ゴングは鳴ったぜ文句はあっか これは荒手のモンスターハンター 俺からすれば ボンクララッパー 返してこいちゃんとした覆うなアンサー 変換前: 会場 変換後: 靉嘔 高鳴って来たぜ心拍数 コイツは俺に負けて自身無くす この靉嘔で俺は進化する 俺がバトルを始めればみんな夢中 ゴングは鳴ったぜ文句はあっか これは荒手のモンスターハンター 俺からすれば ボンクララッパー 返してこいちゃんとしたドープなアンサー 変換前: 俺 変換後: カラオケ 高鳴って来たぜ心拍数 コイツはカラオケに負けて自身無くす この会場でカラオケは進化する カラオケがバトルを始めればみんな夢中 ゴングは鳴ったぜ文句はあっか これは荒手のモンスターハンター カラオケからすれば ボンクララッパー 返してこいちゃんとしたドープなアンサー 変換前: 心拍 変換後: 金髪 高鳴って来たぜ金髪数 コイツは俺に負けて自身無くす この会場で俺は進化する 俺がバトルを始めればみんな夢中 ゴングは鳴ったぜ文句はあっか これは荒手のモンスターハンター 俺からすれば ボンクララッパー 返してこいちゃんとしたドープなアンサー 変換前: 自身 変換後: 政審 高鳴って来たぜ心拍数 コイツは俺に負けて政審無くす この会場で俺は進化する 俺がバトルを始めればみんな夢中 ゴングは鳴ったぜ文句はあっか これは荒手のモンスターハンター 俺からすれば ボンクララッパー 返してこいちゃんとしたドープなアンサー 変換前: ゴング 変換後: レスポンス 高鳴って来たぜ心拍数 コイツは俺に負けて自身無くす この会場で俺は進化する 俺がバトルを始めればみんな夢中 レスポンスは鳴ったぜ文句はあっか これは荒手のモンスターハンター 俺からす れば ボンクララッパー 返してこいちゃんとしたドープなアンサー
さらに、文章の途中に切れ目を入れてくれないことが気に食わなかったため、半角スペースを全角スペースに直したパターンも試しました。
ただ、思い通りにいかなかったため、冒頭の生成文では時間が無いので手元にあった それなりに精度の出た2-2. のモデルを採用しました。
トラブルシューティング
メモ程度。。
Mac にJuman++ 入れるの大変
- => Docker 使え
ValueError: invalid literal for int() with base 10: '\'
- => Juman は半角使えない
http://nlp.ist.i.kyoto-u.ac.jp/index.php?KNP%2FFAQ
半角スペースを含む文が解析できないのですが。 † JUMAN/KNPでは、入力文が全角文字で記述されていることを前提としています。半角スペースなどの半角文字は全角文字に変換してから入力してください。
ImportError: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `CXXABI_1.3.11' not found (required by /opt/conda/lib/python3.6/site-packages/torch/lib/libtorch_python.so)
- よくわからないが、リンカの設定が食い違っている? Tokenizer の前にimport torch しないといけない。謎。
Cloud Run
- Memory limit of 256M exceeded with 276M used. Consider increasing the memory limit, see https://cloud.google.com/run/docs/configuring/memory-limits
files named ['vocab.txt'] but couldn't find such vocabulary files at this path or url.
- gcloud build submit は、.gitignore を踏襲するので、model ディレクトリがpush されてなかった
OSError: Model name './rap_bert_model' was not found in tokenizers model name list (bert-base-uncased, ..., bert-base-german-dbmdz-uncased). We assumed './rap_bert_model' was a path or url to a directory containing vocabulary files named ['vocab.txt'] but couldn't find such vocabulary files at this path or url.
- すぐDisk Full になる
- Docker で試行錯誤してるとすぐ一杯になる
- いま250GB しか積んでないから、次買い換えるときは拡張しよう。。
今後の展望
今回はBERT の導入と学習に精一杯で、ユーザー目線での改善がほとんどできませんでした。
これではユーザー会のみなさんに怒られてしまうので、下記を意識して、次回作では技術に走りすぎないように留意したいと思います。
ユーザー観点でのラップボットとしての改善点
- 成長
- 音楽
- DOPE 度測りたい
- 1つ前までしか(今までの内容を) 覚えてくれない
文章生成モデルとしての改善点
以上です、長文お読みいただきありがとうございました。