ドロップアウトとは「ニューラルネットワークを構成するノードを学習時にランダムに無効化」する手法のことです(以前の回でも少し触れました)。この目的にPyTorchではDropoutクラスが使えます。ここではドロップアウト層を埋め込み層とRNN層の間に入れてみましょう。
class Net2(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_size,
batch_size=25, num_layers=1):
super().__init__()
self.hidden_size = hidden_size
self.batch_size = batch_size
self.num_layers = num_layers
self.device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=0)
self.dropout = nn.Dropout(0.5)
self.rnn = nn.RNN(embedding_dim, hidden_size,
batch_first=True, num_layers=self.num_layers)
self.fc = nn.Linear(hidden_size, vocab_size)
self = self.to(self.device)
def init_hidden(self, batch_size=None):
if not batch_size:
batch_size = self.batch_size
self.hidden_state = torch.zeros(self.num_layers, batch_size,
self.hidden_size).to(self.device)
def forward(self, x):
x = self.embedding(x)
x = self.dropout(x)
x, self.hidden_state = self.rnn(x, self.hidden_state)
x = self.fc(x)
return x
変更点は強調書体で示した2行とクラス名(Net→Net2)だけです。今度はこのクラスからモデルを作成して、学習を行ってみます。ただし、似たようなコードを何度も入力するのも面倒なので、以下のような関数をここでは定義しました。
def do_train(model, dataloader, epochs, vocab_size):
criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = torch.optim.SGD(model.parameters(), lr=0.03)
losses, ppls = train(model, dataloader, criterion, optimizer, epochs, vocab_size)
return losses, ppls
損失関数や最適化アルゴリズムの用意と、train関数の呼び出しをまとめたdo_train関数をここでは定義しました。この関数を次のようにして呼び出しましょう。
model2 = Net2(VOCAB_SIZE, EMBEDDING_DIM, HIDDEN_SIZE, BATCH_SIZE, NUM_LAYERS)
losses, ppls = do_train(model2, dataloader, EPOCHS, VOCAB_SIZE)
実行結果を以下に示します。
損失とパープレキシティーの減少ペースが落ちていることが、出力からは分かります(学習をもっと行った方がよかったかもしれません)。また、最終的な値もドロップアウト層がないNetクラスのインスタンスよりも悪くなっています。次に先ほどと同様なコードで文を生成させてみましょう。
morphemes = ['私', '京都', '昨日', '猫', '檸檬', '檸檬', '檸檬', '檸檬']
make_sentence_from_one_word(morphemes, model2, w2i, i2w)
実行結果は以下の通りです。
「檸檬など〜」というフレーズは生成されませんでしたが、一番上の「私は母の鏡台の前まで走りました」というのは梶井基次郎の『橡の花』に含まれる原文そのままです。ただし、このような文が生成される頻度は少なくなっていると感じられました。少なくとも訓練データに対する過剰な適合はある程度は緩和できているといえます。
「猫は耳を待っている」なんて文はごく普通のことをいっているだけですが、ニューラルネットワークモデルが生成したとなると少しうれしくなります。ではありますが、その他の文は(原文そのままのものを除くと)あまりよくなさそうです。この結果を見ただけでは、ドロップアウト層なしのモデルと比べて、どちらがよい感じの文を生成しているかは難しいといえるでしょう。
パープレキシティーが1に近いほど、選択肢が少ないという考え方からは、この値が1に近いドロップアウト層なしのモデルの方が、日本語としては(あるいは梶井基次郎の文としては)精度の高い予測ができているといえるかもしれません(原文そのままに生成された文を目にしたときには、一瞬、「これはいい感じに生成できたな」と感じた後にすぐさまそれが原文そのままかどうか調べるためにWeb検索をすることになります)。
ただ、元データにはない(けれど、それらしい)文を生成することがここでの目的ですから、ドロップアウト層なしのモデルの方がよいともいえません(よくないでしょう)。ドロップアウト層があるモデルでは「檸檬など〜」などの原文そのままのフレーズが生成されにくくなったことは、バリエーションのある文を生成するという面では一歩前進といえるでしょう。
このニューラルネットワークをもっとよいものにするとしたら、どんな方法があるでしょう。
一つは元データをもっとキレイなものにすることが考えられます。旧仮名遣いで書かれた作品は訓練データには含めないようにするとか、表記の揺れがあれば統一するとかといったことが行えるでしょう。
あるいは、ここではPyTorchのRNNクラスを使っていますが、これをLSTMクラスに置き換える方法もあるかもしれません(興味のある方はぜひ試してみてください)。さらにハイパーパラメーターをチューンすることを考えてもよいでしょう。そうしたことを行った上でもやはりあまりよい結果が出ないのであれば、より根源的な部分から考える必要があるでしょう。
前回よりも精度のよい文を生成できるかどうかという観点からは、今回の結果はよいものとはいえませんでした。そこで上記のような点を考慮しながら、少し間を置いてから、文章生成に再度トライをしてみるつもりです(予定は未定)。
もう一つうまくいかなかった例を簡単に紹介しましょう。
ここまでコーパスにインデックスを振って、ニューラルネットワークモデルに入力する元データを作成する際には形態素数の上限(変数max_length)に20を設定していました。これを40に増やしたらどうでしょう。これまでよりも、ある語の後にくる語の選択肢が増える、つまり表現力が高まるのではないでしょうか。
そう考えて以下のコードで、新たなデータセット/データローダーを作ってみました(ちなみにデータセットに含まれるデータの数は3500個程度から5500個程度にまで増えています)。
max_length = 40 # 形態素数の上限を40にした辞書/データセット/データローダーの作成
id_data2 = word2idx(corpus, w2i, max_length)
dataset2 = KajiiDataset(id_data2)
dataloader2 = DataLoader(dataset2, batch_size=BATCH_SIZE, shuffle=True, drop_last=True)
そして、次のコードでNetクラスとNet2クラスのインスタンスを生成し、学習をしてみました。
model3 = Net(VOCAB_SIZE, EMBEDDING_DIM, HIDDEN_SIZE, BATCH_SIZE, NUM_LAYERS)
losses, ppls = do_train(model3, dataloader2, EPOCHS, VOCAB_SIZE)
model4 = Net2(VOCAB_SIZE, EMBEDDING_DIM, HIDDEN_SIZE, BATCH_SIZE, NUM_LAYERS)
losses, ppls = do_train(model4, dataloader2, EPOCHS, VOCAB_SIZE)
詳細な結果は割愛しますが、学習にかかる時間、損失、パープレキシティーの全てが増加しました(当たり前といえば当たり前です)。
後は以下のコードで、今作成したモデルとこれまでに作成したモデルの合計4つのモデルに文を生成させてみました。
morphemes = ['私', '京都', '夜', '猫', '檸檬']
s1 = make_sentence_from_one_word(morphemes, model, w2i, i2w)
s2 = make_sentence_from_one_word(morphemes, model3, w2i, i2w)
s3 = make_sentence_from_one_word(morphemes, model2, w2i, i2w)
s4 = make_sentence_from_one_word(morphemes, model4, w2i, i2w)
for item in zip(s1, s2, s3, s4):
print('net1, max 20:', item[0])
print('net1, max 40:', item[1])
print('net2, max 20:', item[2])
print('net2, max 40:', item[3])
print()
実行結果を以下に示します。
変数morphemesに指定した最初の語ごとに出力をまとめました。最初の出力がドロップアウトなし/上限20のもの、次がドロップアウトなし/上限40のもの、3番目がドロップアウトあり/上限20のもので、最後がドロップアウトあり/上限40の出力です。
懸案であった「檸檬など〜」のフレーズがドロップアウトなしのニューラルネットワークモデルの両方から生成されていますね。こういうのを見ると、ドロップアウトなしよりはドロップアウトありのモデルの方がよさそうだなと思います(ドロップアウトありのモデルが生成したものもどうかとは思うものが多いのですが)。
結論としては、「もともとがあまりよい文を生成できていないところで、表現力を求めてみてもあまり意味がなかった」ということになるでしょう(何か「Garbage In, Garbage Out」というフレーズを体感しただけのような気もしますね)。
Copyright© Digital Advantage Corp. All Rights Reserved.