はじめに
作曲する度に売れる米津玄師さん。
紡ぎ出される歌詞は人を魅了する力があるように思えます。
今回は、その魅力をディープラーニングに学習させてみようと思いました。
本記事は「実装編」となっています。「前処理」のコードは前回の記事をご覧ください。
実装編の大まかな流れは以下のようになります。
- ハイパーパラメータ / モデル / 損失関数 / 最適化方法 の設定
- 学習コード
- テストコード
使ったモデル
フレームワーク: Pytorch
モデル: Attentionを加えたseq2seq
形態素解析モジュール: jonome
環境: Google Colaboratory
seq2seqやAttentionの仕組みについては前回の記事をご覧ください。
今回のモデルの模式図は以下になります。 参考論文
ここで、SOSは"_"としています。
実装
Google colabに必要な自作モジュールをアップロードした後、
後ほど記載するmain.pyをコピペして実行。
必要な自作モジュール
なお、これら自作モジュールのコードはgithubに載せておりますのでご参照ください。
問題設定(前回の再掲)
以下のように、米津玄師さんがこれまでに出された曲の「一節」から「次の一節」を予測します。
|入力テキスト| 出力テキスト |
|-------+-------|
|あたしあなたに会えて本当に嬉しいのに | _当たり前のようにそれらすべてが悲しいんだ |
|当たり前のようにそれらすべてが悲しいんだ| _今 痛いくらい幸せな思い出が |
|今 痛いくらい幸せな思い出が| _いつか来るお別れを育てて歩く|
|いつか来るお別れを育てて歩く| _誰かの居場所を奪い生きるくらいならばもう|
これは歌詞ネットからスクレイピングさせていただき作成いたしました。
ハイパーパラメータ / モデル / 損失関数 / 最適化方法 の設定
ここで前回の内容に関するところで補足があります。
前回は、「入力テキスト」と「出力テキスト」を作ることが目標であり、それらは日本語でしたが、実はDLが読み込めるようにyonedu_dataset.prepare()
でID化(数値化)されています。
ハイパーパラメータには上から次のように指定しています。
- encoderの埋め込みレイヤにおけるノード数
- encoderのLSTMレイヤにおける中間層のノード数
- バッチサイズ
- 米津さんがこれまで作詞された歌詞の語彙数(形態素解析にはjonomeを使用)
- 「空白」を表す単語ID
モデルはseq2seqということで、encoderとdecoderの二つに役割を分けます。
encoder: embedding layer + hidden layer with LSTM
decoder with attention: embedding layer + hidden layer with LSTM + attention system + softmax layer
損失関数にはクロスエントロピー誤差関数を使用し、最適化方法はencoderとdecoderともにAdamを使用しています。
また、モデルパラメータがあればそちらをロードする形になっています。
from datasets import LyricDataset
import torch
import torch.optim as optim
from modules import *
from device import device
from utils import *
from dataloaders import SeqDataLoader
import math
import os
from utils
# ==========================================
# データ用意
# ==========================================
# 米津玄師_lyrics.txtのパス
file_path = "lyric/米津玄師_lyrics.txt"
edited_file_path = "lyric/米津玄師_lyrics_edit.txt"
yonedu_dataset = LyricDataset(file_path, edited_file_path)
yonedu_dataset.prepare()
# check
print(yonedu_dataset[0])
# 8:2でtrainとtestに分ける
train_rate = 0.8
data_num = len(yonedu_dataset)
train_set = yonedu_dataset[:math.floor(data_num * train_rate)]
test_set = yonedu_dataset[math.floor(data_num * train_rate):]
# 前回はここまで
# ================================================
# ハイパーパラメータ設定 / モデル / 損失関数 / 最適化方法
# ================================================
# ハイパーパラメータ
embedding_dim = 200
hidden_dim = 128
BATCH_NUM = 100
EPOCH_NUM = 100
vocab_size = len(yonedu_dataset.word2id) # 語彙数
padding_idx = yonedu_dataset.word2id[" "] # 空白のID
# モデル
encoder = Encoder(vocab_size, embedding_dim, hidden_dim, padding_idx).to(device)
attn_decoder = AttentionDecoder(vocab_size, embedding_dim, hidden_dim, BATCH_NUM, padding_idx).to(device)
# 損失関数
criterion = nn.CrossEntropyLoss()
# 最適化方法
encoder_optimizer = optim.Adam(encoder.parameters(), lr=0.001)
attn_decoder_optimizer = optim.Adam(attn_decoder.parameters(), lr=0.001)
# 学習済みモデルがあれば,パラメータをロード
encoder_weights_path = "yonedsu_lyric_encoder.pth"
decoder_weights_path = "yonedsu_lyric_decoder.pth"
if os.path.exists(encoder_weights_path):
encoder.load_state_dict(torch.load(encoder_weights_path))
if os.path.exists(decoder_weights_path):
attn_decoder.load_state_dict(torch.load(decoder_weights_path))
学習コード
続いて学習コードになります。Attention付きのseq2seqは大体このようになるかと思いますが、一点だけ補足します。自作のデータローダーを使って、epochごとにバッチサイズ100個分のミニバッチを取得して、そのデータにおける合計の損失を逆伝播させ、勾配を取得し、パラメータを更新しています。
自作のデータローダーは、斎藤 康毅さんのゼロから作るディープラーニング3のソースコードを参考にしています。
# ================================================
# 学習
# ================================================
all_losses = []
print("training ...")
for epoch in range(1, EPOCH_NUM+1):
epoch_loss = 0
# データをミニバッチに分ける
dataloader = SeqDataLoader(train_set, batch_size=BATCH_NUM, shuffle=False)
for train_x, train_y in dataloader:
# 勾配の初期化
encoder_optimizer.zero_grad()
attn_decoder_optimizer.zero_grad()
# Encoderの順伝搬
hs, h = encoder(train_x)
# Attention Decoderのインプット
source = train_y[:, :-1]
# Attention Decoderの正解データ
target = train_y[:, 1:]
loss = 0
decoder_output, _, attention_weight = attn_decoder(source, hs, h)
for j in range(decoder_output.size()[1]):
loss += criterion(decoder_output[:, j, :], target[:, j])
epoch_loss += loss.item()
# 誤差逆伝播
loss.backward()
# パラメータ更新
encoder_optimizer.step()
attn_decoder_optimizer.step()
# 損失を表示
print("Epoch %d: %.2f" % (epoch, epoch_loss))
all_losses.append(epoch_loss)
if epoch_loss < 0.1: break
print("Done")
import matplotlib.pyplot as plt
plt.plot(all_losses)
plt.savefig("attn_loss.png")
# モデル保存
torch.save(encoder.state_dict(), encoder_weights_path)
torch.save(attn_decoder.state_dict(), decoder_weights_path)
テストコード
こちらがテストコードとなります。何をしているのかと言いますと、**[結果]**に表示しているテーブルを作っています。注意点は、以下の二つあります。
- テスト段階で予測するためなので、勾配は取得しない
- Decoderにはまず文字列生成開始を表す"_"をインプットにする(学習の際と同じ条件)
# =======================================
# テスト
# =======================================
# 単語 -> ID 変換の辞書
word2id = yonedu_dataset.word2id
# ID -> 単語 変換の辞書
id2word = get_id2word(word2id)
# 一つの正解データの要素数
output_len = len(yonedu_dataset[0][1])
# 評価用データ
test_dataloader = SeqDataLoader(test_set, batch_size=BATCH_NUM, shuffle=False)
# 結果を表示するデータフレーム
df = pd.DataFrame(None, columns=["input", "answer", "predict", "judge"])
# データローダーを回して、結果を表示するデータフレームに値を入れる
for test_x, test_y in test_dataloader:
with torch.no_grad():
hs, encoder_state = encoder(test_x)
# Decoderにはまず文字列生成開始を表す"_"をインプットにするので、
# "_"のtensorをバッチサイズ分作成
start_char_batch = [[word2id["_"]] for _ in range(BATCH_NUM)]
decoder_input_tensor = torch.tensor(start_char_batch, device=device)
decoder_hidden = encoder_state
batch_tmp = torch.zeros(100,1, dtype=torch.long, device=device)
for _ in range(output_len - 1):
decoder_output, decoder_hidden, _ = attn_decoder(decoder_input_tensor, hs, decoder_hidden)
# 予測文字を取得しつつ、そのまま次のdecoderのインプットとなる
decoder_input_tensor = get_max_index(decoder_output.squeeze(), BATCH_NUM)
batch_tmp = torch.cat([batch_tmp, decoder_input_tensor], dim=1)
predicts = batch_tmp[:,1:] # 予測されたものをバッチごと受け取る
if test_dataloader.reverse:
test_x = [list(line)[::-1] for line in test_x] # 反転されたものをもどす
df = predict2df(test_x, test_y, predicts, df)
df.to_csv("predict_yonedsu_lyric.csv", index=False)
結果
全問不正解。ですが、今回の目標は「米津玄師さんの歌詞の特徴を捉えること」でした。
表の一部を抜粋します。
input: 入力テキスト
output: 正解となる出力テキスト
predict: DLが予測したテキスト
judge: outputとpredictが合っているか
input | output | predict | judge
---------+----------------+----------------+------------
間違いか正解かだなんてどうでもよかった |瞬く間に落っこちた 淡い靄の中で|愛されたいのは 悲しくなるから あなただけかもしらんが|X
その日から何もかも 変わり果てた気がした|風に飛ばされそでうな 深い春の隅|暖かい場所はまだ 今も 美しくあれるように|X
一つずつ 探し当てていこう|起きがけの 子供みたいに|枯れている 青い その色さえも |X
今日がどんな日でも 何をしていようとも|僕はあなたを探してしまうだろう|変わらない街 探していたんだ|X
わかったこと
- predictの文も意味不明ではない(「まだ 今も」のように文法は正確)
- inputからの文脈もそれほど外れてはいない
- ただ、米津さんの言葉選びの特徴を捉えられているかは正直微妙。
今回、過学習は見られなかったので、学習不足の原因は主にデータ数の少なさだと考えられる。
いや、「学習不足」と決めつけているのは私たちだけで、AIなりに考えていることがあるのかもしれません...