はじめに
日本語形態素解析エンジンのjanome
とマルコフ連鎖ジェネレータmarkovfy
を使って、櫻坂の歌詞を学習してそれっぽい歌詞を生成します。
文章生成と言うと、RNNモデルを構築して学習する方法もありますが、色々な記事をググった感じ、精度が微妙そうだったので今回はマルコフ連鎖を使います。
目的
自分は機械学習を学び始めておよそ2ヶ月でして、画像分類・顔認識といった方向のアウトプットとしてコチラの記事で櫻坂46メンバーの顔分類アプリケーションを作成しました。
自然言語処理方面でも何かアウトプットしたいということで、これまた好きな櫻坂46を取り上げ、歌詞生成をやってみることにしました。
歌詞生成や文章生成について先例が多くあるのですが、意外とmarkovify
とjanome
の組み合わせは多くない(形態素解析でMecab
を使っていることが多いです)ので、その辺りに実装の参考例になれば幸いです。
markovfyって?
markovfy
については公式Githubに
Markovify is a simple, extensible Markov chain generator. Right now, its primary use is for building Markov models of large corpora of text and generating random sentences from that. However, in theory, it could be used for other applications.
との記載があり、要は「めちゃ簡単に、マルコフ連鎖モデルを構築して、文章生成できて、また理論上は、他のことにも応用できるライブラリ」ということです。
マルコフ連鎖って?
ちなみにマルコフ連鎖に関してはコチラの記事が分かりやすかったです。
マルコフ過程(未来の状態が現在の状態のみで決定し、過去の状態とは無関係という性質を持つ確立過程)のうちとりうる状態が離散的なもののことをマルコフ連鎖と言います。
今日勉強すれば、明日も勉強する確率が70%、ゲームをしてしまう確率が30%とわかっている状態であれば、未来の状態が、現在の値で確定するのでマルコフ過程ですし、明日の行動が「勉強するか・ゲームするか」の2択しかない(その間や別の選択肢がない)とするならば、とりうる状態は離散的ですからマルコフ連鎖でもあります。
これを文章生成に置き換えると
今日は勉強をたくさんします。
という文章を考えるとして、文字ベースの状態の変化は
「今」→「日」→「は」→「勉」→「強」→「を」→「た」→「く」→「さ」→「ん」→「し」→「ま」→「す」→「。」
となります。
単語ベースでの状態の変化は
「今日」→「は」→「勉強」→「を」→「たくさん」→「し」→「ます」→「。」
となります
文字ベースだと区分けが細かすぎて意味の通る文章を生成しづらいので、今回は単語ベースで生成していきます。
要は「今日」と来たら「は」を選び、「は」が来たら「勉強」を選んでいくイメージです。
ただこれだと同じ文章しか生成されないので、たくさんの文章を用意してモデルを作ることで、「今日」の後に選ぶ単語として、「は」が20%、「も」が30%、「だけ」が5%...のように枝分かれするはずです。
この確率に従って次に来る単語を選択して文章を作っていきます。
ちなみに強化学習の意思決定プロセスでもマルコフ過程は登場するので、知っておくとそっち方面の知識の理解もしやすいなと思いました。
環境
M1 mac M1 Monterey
markovify (0.9.3)
Janome (0.4.1)
Python (3.9.5)
Anacondaのjupyterlabで実行しています。
データ集め
櫻坂46の楽曲の歌詞を収集します。現在2ndシングルまでなので櫻坂46名義では14曲しかないので、スクレイピングせず、手作業で集めました。コチラからスプレッドシートに貼り付けてcsv出力しました。
データ加工
import pandas as pd
df = pd.read_csv('sakurazaka_lyric.csv')
df
このlylic欄の歌詞をテキストデータにします
# 歌詞全体が表示されるように
pd.set_option("display.max_colwidth", 1000)
# string型に変形(index=Falseでインデックスを消す)
# lyric.txtという新しいテキストファイルを作成して書き込む
string = df['lyric'].to_string(index=False)
output_file = open('lyric.txt', 'a')
output_file.write(string)
output_file.close()
1行目のpd.set_option("display.max_colwidth", 1000)
がないと歌詞が全部表示されず途中までしか表示されないので忘れずに設定。
あとは一旦でSeries型からstring型にして、テキストファイルに書き込みます。
著作権の関係もあるので全部は映せませんがこんな感じでテキストファイルに歌詞データが書き込めました。
このテキストファイルを整形していきます。
text_file = open('text.txt', 'r')
text = text_file.read()
text = text.replace('\\n', ' ') # 文字列\n として認識されていた改行文字をスペース置き換え
text = text.replace('\n', ' ') # 改行文字をスペースに置き換え
text = text.replace('\u3000', '') # 全角スペースを消す
# 以降不要な文字を消去
text = text.replace('・・・', '')
text = text.replace('...', '')
text = text.replace('...', '')
text = text.replace('(', '')
text = text.replace(')', '')
text = text.replace('「', '')
text = text.replace('」', '')
text = text.replace('…', '')
text = text.replace('、', '')
text = text.replace(',', '')
# いい感じのところで改行
text = text.replace(' ', '\n')
# 2連続改行は消す
text = text.replace('\n\n', '')
かなりめんどくさい書き方をしてますが、テキストファイルをいい感じに整形してます。
最終的にmarkovify
に突っ込むときに改行文字がないと読み込んでくれません。どこで改行文字を入れるかはお好みですが、今回は歌で言う息継ぎのタイミングで改行してます(伝わるかな?)
janomeで単語に分ける
from janome.tokenizer import Tokenizer
# Tokenizerをインスタンス化
t = Tokenizer()
# さっきのテキストファイルを分かち書く(単語に分ける)
words = t.tokenize(text, wakati = True)
# 単語を順番にリストに格納
word_list = []
for i in words:
word_list.append(i)
これで完成したword_listの一部がこんな感じ
['私',
'の',
'こと',
'なんて',
'誰',
'も',
'きっと',
'\n',
'興味',
'ない',
'と',
'思っ',
'て',
'た',
'\n',
'だから',
'ちょっ
あとはこれを単語をスペースで区切って、いいところで改行したstring型にします。
word_list_string = ' '.join(word_list)
# 次のようになる → 私 の こと なんて 誰 も きっと \n 興味 ない
' '.join(word_list)
でword_listの単語たちを空白スペースで全部連結します。
example_list = ['a','b','c','d','e']
example_string = ' '.join(example_list)
# example_string = 'a b c d e'
markovifyに突っ込む
import markovify
# 改行で区切って1文として読み込ませてモデルを作成、state_sizeはN階マルコフ連鎖のNの値
text_model = markovify.NewlineText(text, state_size=2)
# 文章生成(とりあえず10回)
for i in range(11):
print(f'------------{i}回目-----------------')
sentence = text_model.make_sentence()
if sentence == None:
print('文章が生成できませんでした。')
else:
print(sentence.replace(' ', ''))
------------0回目-----------------
ドキドキしてるうちにみんな死ぬんだ
------------1回目-----------------
生きよう全てをもう水に流すでしょう何言われても
------------2回目-----------------
今さらもうどうでもいいけど
------------3回目-----------------
大事なことはないよね
------------4回目-----------------
一度だって忘れたことに遅刻してないよ
------------5回目-----------------
涙なんかじゃないんですか?
------------6回目-----------------
何が嫌ってわけじゃないんだどうせならば僕たちはあの頃の僕って病んでいたんだから無理ね
------------7回目-----------------
サラリーマンが愚痴を言って急に思いついたかなんて分からないけど
------------8回目-----------------
君のことに遅刻してしまう
------------9回目-----------------
原因究明ある日ばったり
------------10回目-----------------
涙なんかじゃないか人生の電源切られるようにやって前に進もう微笑んで
まともな歌詞というか文章は生成されましたね。
ただやはりデータ数が少ないので、一文が短いし、多様性に欠けます。
ちなみにstate_size
の値を変えることで「過去N個の単語の情報を影響を受けて未来の単語を決めるか」が決定します。今回は2なので過去2単語分ですね。ここを大きくすればするほど、元の文章の内容を反映しやすくなります。
まとめ
簡単ですが、歌詞の1文を作成することができました。
ちなみに1曲の歌詞を丸ごと1文として学習させたら結構な長文(1曲に相当するくらいの)が生成されたので、色々と試行錯誤してみると面白そうです。
学習している感じがないので、やっぱりRNNモデルを作って学習もしてみたいですね。
文字列の扱いや、正規表現の練習にはなったので良かったかな。
自然言語系ではアウトプットもう少しやりたい。
参考
https://qiita.com/shge/items/fbfce6b54d2e0cc1b382
https://qiita.com/Cherno/items/ac9ab5a53baa2cacec31