morikomorou’s blog

自分が学んだことなどの備忘録的なやつ

【python】言語処理100本ノック2020を解く(第9章後半)


はじめに

前回の続きで言語処理100本ノック解いていきたいと思います。
すこし時間が空いてしまいましたね。すいません。

今回は第9章後半です。
9章は非常に盛りだくさんなので3部に分けて、そのうちの真ん中です。
前回はRNN(Recurrent Neural Network)と呼ばれる再帰的構造をもったモデルを使用していきましたが、今回はその多層化や、CNNの実装を行っていきます。




第9章: RNNとCNN

深層学習フレームワークを用い,再帰型ニューラルネットワーク(RNN)や畳み込みニューラルネットワーク(CNN)を実装します.

85. 双方向RNN・多層化

順方向と逆方向のRNNの両方を用いて入力テキストをエンコードし,モデルを学習せよ.
さらに,双方向RNNを多層化して実験せよ

前回の最後に作ったモデルを少し変えるだけで実装できます。
双方向RNN化はnn.LSTMの引数にbidirectional=Trueを加えるだけです。
多層化はNUM_LAYERSを増やしておきます。

class RNN(nn.Module):
    def __init__(self, vocab_size, emb_size, padding_idx, hidden_size, output_size, num_layers=3, emb_weights=None):
        super().__init__()
        if emb_weights != None:
            self.emb = nn.Embedding.from_pretrained(emb_weights, padding_idx=padding_idx)
        else:
            self.emb = nn.Embedding(vocab_size, emb_size, padding_idx=padding_idx)
        self.rnn = nn.LSTM(emb_size, hidden_size, num_layers, batch_first=True, bidirectional=True)
        self.fc = nn.Linear(hidden_size * 2, output_size)
    
    def forward(self, x, h0=None):
        x = self.emb(x)
        x, h = self.rnn(x, h0)
        x = x[:, -1, :]
        logits = self.fc(x)
        return logits

# パラメータの設定
VOCAB_SIZE = len(set(word2id.values())) + 2  # 辞書のID数 + unknown + パディングID
EMB_SIZE = 300
PADDING_IDX = len(set(word2id.values())) + 1
OUTPUT_SIZE = 4
HIDDEN_SIZE = 50
NUM_LAYERS = 3

# モデルの定義
net = RNN(VOCAB_SIZE, EMB_SIZE, PADDING_IDX, HIDDEN_SIZE, OUTPUT_SIZE, NUM_LAYERS, weights)
net.train()

# 損失関数の定義
criterion = nn.CrossEntropyLoss()

# 最適化手法の定義
optimizer = torch.optim.SGD(net.parameters(), lr=0.1, momentum=0.9)

num_epochs = 30
train_loss, train_acc, valid_loss, valid_acc = train_model(net,
            dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)

fig, ax = plt.subplots(1,2, figsize=(10, 5))
epochs = np.arange(num_epochs)
ax[0].plot(epochs, train_loss, label='train')
ax[0].plot(epochs, valid_loss, label='valid')
ax[0].set_title('loss')
ax[0].set_xlabel('epoch')
ax[0].set_ylabel('loss')
ax[1].plot(epochs, train_acc, label='train')
ax[1].plot(epochs, valid_acc, label='valid')
ax[1].set_title('acc')
ax[1].set_xlabel('epoch')
ax[1].set_ylabel('acc')
ax[0].legend(loc='best')
ax[1].legend(loc='best')
plt.tight_layout()
plt.savefig('fig85.png')
plt.show()

acc_train = calc_acc(net, train_dataloader)
acc_valid = calc_acc(net, valid_dataloader)
acc_test = calc_acc(net, test_dataloader)
print('学習データの正解率: {:.4f}'.format(acc_train))
print('検証データの正解率: {:.4f}'.format(acc_valid))
print('テストデータの正解率: {:.4f}'.format(acc_test))

出力は以下。

NVIDIA GeForce GTX 1660
使用デバイス: cuda:0
Epoch 1 / 30 (train) Loss: 1.1720, Acc: 0.4120, (val) Loss: 1.1644, Acc: 0.4213
Epoch 2 / 30 (train) Loss: 1.1644, Acc: 0.4150, (val) Loss: 1.1647, Acc: 0.4213
Epoch 3 / 30 (train) Loss: 1.1616, Acc: 0.4311, (val) Loss: 1.1409, Acc: 0.4783
Epoch 4 / 30 (train) Loss: 1.0805, Acc: 0.5624, (val) Loss: 1.0210, Acc: 0.5967
Epoch 5 / 30 (train) Loss: 1.0180, Acc: 0.6192, (val) Loss: 0.9276, Acc: 0.6732
...
Epoch 26 / 30 (train) Loss: 0.3511, Acc: 0.8568, (val) Loss: 0.7825, Acc: 0.7264
Epoch 27 / 30 (train) Loss: 0.3434, Acc: 0.8648, (val) Loss: 0.7137, Acc: 0.7361
Epoch 28 / 30 (train) Loss: 0.3260, Acc: 0.8679, (val) Loss: 0.7650, Acc: 0.7676
Epoch 29 / 30 (train) Loss: 0.2948, Acc: 0.8824, (val) Loss: 0.7262, Acc: 0.7646
Epoch 30 / 30 (train) Loss: 0.2766, Acc: 0.8909, (val) Loss: 0.7340, Acc: 0.7586
学習データの正解率: 0.9007
検証データの正解率: 0.7586
テストデータの正解率: 0.7759

なんか悪くなっちゃいましたね、ハイパーパラメータがあんまりよくないのでしょうか…

86. 畳み込みニューラルネットワーク (CNN)

問題文長いので割愛しますが、畳み込みニューラルネットワーク(CNN: Convolutional Neural Network)を用い,単語列 xからカテゴリ yを予測するモデルを実装せよ。
というものです。

問題文に沿って畳み込み層とmaxプーリング層を実装し、モデルを作成していきます。

from torch.nn import functional as F

class CNN(nn.Module):
    def __init__(self, vocab_size, emb_size, padding_idx, output_size, out_channels, kernel_heights, stride, padding, emb_weights=None):
        super().__init__()
        if emb_weights != None:  # 指定があれば埋め込み層の重みをemb_weightsで初期化
            self.emb = nn.Embedding.from_pretrained(emb_weights, padding_idx=padding_idx)
        else:
            self.emb = nn.Embedding(vocab_size, emb_size, padding_idx=padding_idx)
        self.conv = nn.Conv2d(1, out_channels, (kernel_heights, emb_size), stride, (padding, 0))
        self.drop = nn.Dropout(0.4)
        self.fc = nn.Linear(out_channels, output_size)

    def forward(self, x):
        emb = self.emb(x).unsqueeze(1)
        conv = self.conv(emb)
        act = F.relu(conv.squeeze(3))
        max_pool = F.max_pool1d(act, act.size()[2])
        logits = self.fc(self.drop(max_pool.squeeze(2)))
        return logits

# パラメータの設定
VOCAB_SIZE = len(set(word2id.values())) + 2
EMB_SIZE = 300
PADDING_IDX = len(set(word2id.values())) + 1
OUTPUT_SIZE = 4
OUT_CHANNELS = 100
KERNEL_HEIGHTS = 3
STRIDE = 1
PADDING = 1

# モデルの定義
model = CNN(VOCAB_SIZE, EMB_SIZE, PADDING_IDX, OUTPUT_SIZE, OUT_CHANNELS, KERNEL_HEIGHTS, STRIDE, PADDING, emb_weights=weights)
x = torch.tensor([tokenizer(sample)], dtype=torch.int64)
print(x)
print(x.size())
print(nn.Softmax(dim=-1)(model(x)))

出力は以下。

tensor([[  68,   76,  782, 1974,   21, 5054, 5055,   34, 1602,    0]])
torch.Size([1, 10])
tensor([[0.2539, 0.2323, 0.2989, 0.2149]], grad_fn=<SoftmaxBackward0>)

とりあえずうまく出力まではできてそうです。




87. 確率的勾配降下法によるCNNの学習

確率的勾配降下法(SGD: Stochastic Gradient Descent)を用いて,問題86で構築したモデルを学習せよ.訓練データ上の損失と正解率,評価データ上の損失と正解率を表示しながらモデルを学習し,適当な基準(例えば10エポックなど)で終了させよ.

学習させていきましょう

# モデルの定義
net = CNN(VOCAB_SIZE, EMB_SIZE, PADDING_IDX, OUTPUT_SIZE, OUT_CHANNELS, KERNEL_HEIGHTS, STRIDE, PADDING, emb_weights=weights)
net.train()

# 損失関数の定義
criterion = nn.CrossEntropyLoss()

# 最適化手法の定義
optimizer = torch.optim.SGD(net.parameters(), lr=0.1, momentum=0.9)

num_epochs = 30
train_loss, train_acc, valid_loss, valid_acc = train_model(net,
            dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)

fig, ax = plt.subplots(1,2, figsize=(10, 5))
epochs = np.arange(num_epochs)
ax[0].plot(epochs, train_loss, label='train')
ax[0].plot(epochs, valid_loss, label='valid')
ax[0].set_title('loss')
ax[0].set_xlabel('epoch')
ax[0].set_ylabel('loss')
ax[1].plot(epochs, train_acc, label='train')
ax[1].plot(epochs, valid_acc, label='valid')
ax[1].set_title('acc')
ax[1].set_xlabel('epoch')
ax[1].set_ylabel('acc')
ax[0].legend(loc='best')
ax[1].legend(loc='best')
plt.tight_layout()
plt.savefig('fig87.png')
plt.show()

acc_train = calc_acc(net, train_dataloader)
acc_valid = calc_acc(net, valid_dataloader)
acc_test = calc_acc(net, test_dataloader)
print('学習データの正解率: {:.4f}'.format(acc_train))
print('検証データの正解率: {:.4f}'.format(acc_valid))
print('テストデータの正解率: {:.4f}'.format(acc_test))

出力は以下。

NVIDIA GeForce GTX 1660
使用デバイス: cuda:0
Epoch 1 / 30 (train) Loss: 0.9632, Acc: 0.6180, (val) Loss: 0.7480, Acc: 0.7369
Epoch 2 / 30 (train) Loss: 0.6651, Acc: 0.7585, (val) Loss: 0.5898, Acc: 0.7864
Epoch 3 / 30 (train) Loss: 0.5002, Acc: 0.8221, (val) Loss: 0.5116, Acc: 0.8178
Epoch 4 / 30 (train) Loss: 0.3920, Acc: 0.8614, (val) Loss: 0.5248, Acc: 0.8186
Epoch 5 / 30 (train) Loss: 0.3057, Acc: 0.8890, (val) Loss: 0.5322, Acc: 0.8223
...
Epoch 26 / 30 (train) Loss: 0.0686, Acc: 0.9784, (val) Loss: 0.6891, Acc: 0.8366
Epoch 27 / 30 (train) Loss: 0.0654, Acc: 0.9783, (val) Loss: 0.6809, Acc: 0.8471
Epoch 28 / 30 (train) Loss: 0.0614, Acc: 0.9777, (val) Loss: 0.6953, Acc: 0.8448
Epoch 29 / 30 (train) Loss: 0.0616, Acc: 0.9790, (val) Loss: 0.7041, Acc: 0.8351
Epoch 30 / 30 (train) Loss: 0.0558, Acc: 0.9820, (val) Loss: 0.6912, Acc: 0.8388

学習データの正解率: 0.9989
検証データの正解率: 0.8388
テストデータの正解率: 0.8643

RNNよりCNNのほうが精度がよさそうです。
ただ、8章でやった畳み込みなしのNNには精度勝てそうにないですね。。。謎です。

88. パラメータチューニング

問題85や問題87のコードを改変し,ニューラルネットワークの形状やハイパーパラメータを調整しながら,高性能なカテゴリ分類器を構築せよ.

後半の最後にいままでのモデルのハイパーパラメータを調整して精度上がるかやってみます。
CNNのほうが精度よさそうだったのでそっちを変えてみることにしましょう。
optimizerをAdamにかえて、畳み込みフィルターのサイズも調整しました。

class CNN(nn.Module):
    def __init__(self, vocab_size, emb_size, padding_idx, output_size, out_channels, kernel_heights, stride, padding, emb_weights=None):
        super().__init__()
        if emb_weights != None:  # 指定があれば埋め込み層の重みをemb_weightsで初期化
            self.emb = nn.Embedding.from_pretrained(emb_weights, padding_idx=padding_idx)
        else:
            self.emb = nn.Embedding(vocab_size, emb_size, padding_idx=padding_idx)
        self.conv = nn.Conv2d(1, out_channels, (kernel_heights, emb_size), stride, (padding, 0))
        self.drop = nn.Dropout(0.4)
        self.fc = nn.Linear(out_channels, output_size)

    def forward(self, x):
        emb = self.emb(x).unsqueeze(1)
        conv = self.conv(emb)
        act = F.relu(conv.squeeze(3))
        max_pool = F.max_pool1d(act, act.size()[2])
        logits = self.fc(self.drop(max_pool.squeeze(2)))
        return logits

# パラメータの設定
VOCAB_SIZE = len(set(word2id.values())) + 2
EMB_SIZE = 300
PADDING_IDX = len(set(word2id.values())) + 1
OUTPUT_SIZE = 4
OUT_CHANNELS = 500
KERNEL_HEIGHTS = 2
STRIDE = 1
PADDING = 1

# モデルの定義
net = CNN(VOCAB_SIZE, EMB_SIZE, PADDING_IDX, OUTPUT_SIZE, OUT_CHANNELS, KERNEL_HEIGHTS, STRIDE, PADDING, emb_weights=weights)
net.train()

# 損失関数の定義
criterion = nn.CrossEntropyLoss()

# 最適化手法の定義
optimizer = torch.optim.Adam(net.parameters(), lr=0.0005)

num_epochs = 30
train_loss, train_acc, valid_loss, valid_acc = train_model(net,
            dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)

fig, ax = plt.subplots(1,2, figsize=(10, 5))
epochs = np.arange(num_epochs)
ax[0].plot(epochs, train_loss, label='train')
ax[0].plot(epochs, valid_loss, label='valid')
ax[0].set_title('loss')
ax[0].set_xlabel('epoch')
ax[0].set_ylabel('loss')
ax[1].plot(epochs, train_acc, label='train')
ax[1].plot(epochs, valid_acc, label='valid')
ax[1].set_title('acc')
ax[1].set_xlabel('epoch')
ax[1].set_ylabel('acc')
ax[0].legend(loc='best')
ax[1].legend(loc='best')
plt.tight_layout()
plt.savefig('fig88.png')
plt.show()

acc_train = calc_acc(net, train_dataloader)
acc_valid = calc_acc(net, valid_dataloader)
acc_test = calc_acc(net, test_dataloader)
print('学習データの正解率: {:.4f}'.format(acc_train))
print('検証データの正解率: {:.4f}'.format(acc_valid))
print('テストデータの正解率: {:.4f}'.format(acc_test))

結果は以下の通り。

NVIDIA GeForce GTX 1660
使用デバイス: cuda:0
Epoch 1 / 30 (train) Loss: 1.0482, Acc: 0.5971, (val) Loss: 0.8920, Acc: 0.6897
Epoch 2 / 30 (train) Loss: 0.7826, Acc: 0.7262, (val) Loss: 0.7146, Acc: 0.7496
Epoch 3 / 30 (train) Loss: 0.6073, Acc: 0.7900, (val) Loss: 0.6166, Acc: 0.7856
Epoch 4 / 30 (train) Loss: 0.4830, Acc: 0.8420, (val) Loss: 0.5560, Acc: 0.8111
...
Epoch 27 / 30 (train) Loss: 0.0211, Acc: 0.9978, (val) Loss: 0.4855, Acc: 0.8651
Epoch 28 / 30 (train) Loss: 0.0207, Acc: 0.9969, (val) Loss: 0.4754, Acc: 0.8621
Epoch 29 / 30 (train) Loss: 0.0172, Acc: 0.9983, (val) Loss: 0.4814, Acc: 0.8643
Epoch 30 / 30 (train) Loss: 0.0163, Acc: 0.9980, (val) Loss: 0.4926, Acc: 0.8651

学習データの正解率: 0.9993
検証データの正解率: 0.8651
テストデータの正解率: 0.8793

ちょびっとだけ改善しましたかね??むずかしいです。

おわりに

時系列データの予測に強いといわれるRNNやCNNの実装をおこないました。
一通りやってみましたが、8章でやった結果を上回ることはできませんでしたね。