パープレキシティーと呼ばれる指標を用いて、どのくらいの精度で文を生成できるかを確認した後、ドロップアウトと呼ばれる手法で過学習状態を回避してみましょう。
前回はPyTorchのRNNクラスを使って、梶井基次郎の小説データを基に文章生成を行いました。できあがったニューラルネットワークモデルは、「檸檬」を与えると「檸檬などごくありふれている」という文を頻繁に生成するようなものとなっていました。恐らくは訓練データに過剰に適合した(過学習をした)ことにより、このようなモデルになってしまったのでしょう。
そこで、今回はパープレキシティーという損失とは別の指標を導入して、前回のニューラルネットワークモデルがどんなものだったかを観察した後、過学習を避ける仕組みを導入します。果たして、これにより前回よりもよい精度で文を生成できるようになるのでしょうか。
なお、分かち書きされたデータから辞書やデータセット/データローダーを作成するコードなどは前回までに見てきたものと同様なので、本稿ではそれらについては紹介しません。ここでは、前回までのコードに少し手を加えたものを部分的に紹介するだけとします。全体のコードについては今回のノートブックを参照してください。
前回に定義したニューラルネットワークモジュールは以下のようなものでした。
class Net(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.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.hidden_state = self.rnn(x, self.hidden_state)
x = self.fc(x)
return x
ベクトルの埋め込みを行い、それをRNNクラスに入力して、最後に全結合層を通して、辞書の要素数の出力を行います。その出力が、辞書のインデックス(整数値)に対応していて、「ある語の次にくるのはこの語である」という推測を行っていたのでした。
これにより、「私」の次に「は」がくる確率は0.xxで、「の」がくる確率は0.yyで、「が」がくる確率は0.zzのような推測を行っていたというわけです。そして、先ほど述べた新たな指標とはこの確率の逆数のようなものだと考えられます(実際の算出式は異なります)。これを「パープレキシティー」と呼びます。パープレキシティーは「ある語」が入力されたときに、次にくると考えられる候補が何個あるかを示す値とも考えられます。「は」がくる確率が0.5だったとすると、そのパープレキシティーは1/0.5=2.0ですが、これは次にくる語の選択肢としては2つくらいに絞れていると考えられるということです(よって、この値は1に近いほど、候補の絞り込みができているともいえますね)。
以下は前回に定義した学習を行うためのtrain関数に、パープレキシティーを算出するコードを加えたものです(変更点が他にも数か所ありますが、それらについての説明は省略します)。
from math import exp
def train(model, dataloader, criterion, optimizer, epochs, vocab_size):
device = model.device
model.train()
losses = []
ppls = []
for epoch in range(epochs):
running_loss = 0
for cnt, (X_train, y_train) in enumerate(dataloader):
optimizer.zero_grad()
X_train, y_train = X_train.to(device), y_train.to(device)
model.init_hidden()
outputs = model(X_train)
outputs = outputs.reshape(-1, vocab_size)
y_train = y_train.reshape(-1)
loss = criterion(outputs, y_train)
running_loss += loss.item()
loss.backward()
optimizer.step()
avg_loss = running_loss / cnt
losses.append(avg_loss)
ppl = exp(avg_loss)
ppls.append(ppl)
print('+', end='')
if epoch % 50 == 0:
print(f'\nepoch: {epoch:3}, loss: {avg_loss:.3f}, ppl: {ppl:.3f}')
print(f'\nepoch: {epoch:3}, loss: {loss:.3f}, ppl: {ppl:.3f}')
return losses, ppls
強調書体とした部分がパープレキシティー関連の修正点です。ここでは「ppl = exp(avg_loss)」のように、損失関数で得た損失の値(の1エポックごとの平均値)を基にパープレキシティーを計算しています(この理論的な背景については書籍『ゼロから作るDeep Learning 自然言語処理編』などを参照してください)。
まずは、修正後のtrain関数を使って、上に掲載した前回と同様なNetクラスのインスタンスを対象に学習を行ってみます。
# 定数定義などは省略
model = Net(VOCAB_SIZE, EMBEDDING_DIM, HIDDEN_SIZE, BATCH_SIZE, NUM_LAYERS)
criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = torch.optim.SGD(model.parameters(), lr=0.03)
EPOCHS = 1000
losses, ppls = train(model, dataloader, criterion, optimizer, EPOCHS, VOCAB_SIZE)
実行結果は以下の通りです。
出力の右端にある「ppl: xxx」というのがパープレキシティーの値です。これを見ると、3600オーバーの値から最後は1.3程度にまで数値が減少しています。パープレキシティーが1.3というのはよい値なのですが、よすぎるために、前回は「檸檬」という語から特定のフレーズが頻繁に生成されたとも考えられます(本稿では損失やパープレキシティーをグラフにプロットした結果は省略します。興味のある方は今回のノートブックを参照してください)。
前回同様、モデルに最初の1語を与えて、文を自動生成させてみましょう。
morphemes = ['私', '京都', '昨日', '猫', '檸檬', '檸檬', '檸檬', '檸檬']
make_sentence_from_one_word(morphemes, model, w2i, i2w)
檸檬を多めに指定していますが、これは「檸檬などごくありふれている」というフレーズがどれだけ出てくるかを見るためです。実行結果は次のようになりました(このセルを繰り返し実行すれば、もちろん、その結果は異なるものになります)。
見事なまでに「檸檬などごくありふれている」ばかりが生成されましたね。とはいっても、ここまでは前回と同じクラスからモデル(インスタンス)を生成し、前回と同様なデータを与えていたので、これは当たり前です。そこで、次に過学習(訓練データへの過剰な適合)を避けるためにドロップアウトを呼ばれる処理をNetクラスに追加します。
Copyright© Digital Advantage Corp. All Rights Reserved.