先日,言語処理100本ノック2020が公開されました.私自身,自然言語処理を初めてから1年しか経っておらず,細かいことはよくわかっていませんが,技術力向上のために全ての問題を解いて公開していこうと思います.
すべてjupyter notebook上で実行するものとし,問題文の制約は都合よく破っていいものとします.
ソースコードはgithubにもあります.あります.
8章はこちら.
環境はPython3.8.2とUbuntu18.04です.
第9章: RNN, CNN
第8章と同じでPyTorchを使います.
80. ID番号への変換
問題51で構築した学習データ中の単語にユニークなID番号を付与したい.学習データ中で最も頻出する単語に
1
,2番目に頻出する単語に2
,……といった方法で,学習データ中で2回以上出現する単語にID番号を付与せよ.そして,与えられた単語列に対して,ID番号の列を返す関数を実装せよ.ただし,出現頻度が2回未満の単語のID番号はすべて0
とせよ.
import re
import spacy
import torch
spacyモデルとラベルを用意.
nlp = spacy.load('en')
categories = ['b', 't', 'e', 'm']
category_names = ['business', 'science and technology', 'entertainment', 'health']
ファイルを読み込んでspacyでトークナイズします.
def tokenize(x):
x = re.sub(r'\s+', ' ', x)
x = nlp.make_doc(x)
x = [d.text for d in x]
return x
def read_feature_dataset(filename):
with open(filename) as f:
dataset = f.read().splitlines()
dataset = [line.split('\t') for line in dataset]
dataset_t = [categories.index(line[0]) for line in dataset]
dataset_x = [tokenize(line[1]) for line in dataset]
return dataset_x, torch.tensor(dataset_t, dtype=torch.long)
train_x, train_t = read_feature_dataset('data/train.txt')
valid_x, valid_t = read_feature_dataset('data/valid.txt')
test_x, test_t = read_feature_dataset('data/test.txt')
語彙を抽出します.2回以上現れた語のみを対象とします.
from collections import Counter
counter = Counter([
x
for sent in train_x
for x in sent
])
vocab_in_train = [
token
for token, freq in counter.most_common()
if freq > 1
]
len(vocab_in_train)
9700
単語列をIDの番号の列に変換します.
vocab_list = ['[UNK]'] + vocab_in_train
vocab_dict = {x:n for n, x in enumerate(vocab_list)}
def sent_to_ids(sent):
return torch.tensor([vocab_dict[x if x in vocab_dict else '[UNK]'] for x in sent], dtype=torch.long)
訓練データの最初の文に対してトークナイズを行ってみます.
print(train_x[0])
print(sent_to_ids(train_x[0]))
['Kathleen', 'Sebelius', "'", 'LGBT', 'legacy']
tensor([ 0, 0, 2, 2648, 0])
ID番号の列に変換しておく.
def dataset_to_ids(dataset):
return [sent_to_ids(x) for x in dataset]
train_s = dataset_to_ids(train_x)
valid_s = dataset_to_ids(valid_x)
test_s = dataset_to_ids(test_x)
train_s[:3]
[tensor([ 0, 0, 2, 2648, 0]),
tensor([ 9, 6740, 1445, 2076, 583, 10, 547, 32, 51, 873, 6741]),
tensor([ 0, 205, 4198, 315, 1899, 1232, 0])]
81. RNNによる予測
ID番号で表現された単語列$\boldsymbol{x} = (x_1, x_2, \dots, x_T)$がある.ただし,$T$は単語列の長さ,$x_t \in \mathbb{R}^{V}$は単語のID番号のone-hot表記である($V$は単語の総数である).再帰型ニューラルネットワーク(RNN: Recurrent Neural Network)を用い,単語列$\boldsymbol{x}$からカテゴリ$y$を予測するモデルとして,次式を実装せよ.
$
\overrightarrow h_0 = 0, \
\overrightarrow h_t = {\rm \overrightarrow{RNN}}(\mathrm{emb}(x_t), \overrightarrow h_{t-1}), \
y = {\rm softmax}(W^{(yh)} \overrightarrow h_T + b^{(y)})
$
ただし,$\mathrm{emb}(x) \in \mathbb{R}^{d_w}$は単語埋め込み(単語のone-hot表記から単語ベクトルに変換する関数),$\overrightarrow h_t \in \mathbb{R}^{d_h}$は時刻$t$の隠れ状態ベクトル,${\rm \overrightarrow{RNN}}(x,h)$は入力$x$と前時刻の隠れ状態$h$から次状態を計算するRNNユニット,$W^{(yh)} \in \mathbb{R}^{L \times d_h}$は隠れ状態ベクトルからカテゴリを予測するための行列,$b^{(y)} \in \mathbb{R}^{L}$はバイアス項である($d_w, d_h, L$はそれぞれ,単語埋め込みの次元数,隠れ状態ベクトルの次元数,ラベル数である).RNNユニット${\rm \overrightarrow{RNN}}(x,h)$には様々な構成が考えられるが,典型例として次式が挙げられる.
$
{\rm \overrightarrow{RNN}}(x,h) = g(W^{(hx)} x + W^{(hh)}h + b^{(h)})
$
ただし,$W^{(hx)} \in \mathbb{R}^{d_h \times d_w},W^{(hh)} \in \mathbb{R}^{d_h \times d_h}, b^{(h)} \in \mathbb{R}^{d_h}$はRNNユニットのパラメータ,$g$は活性化関数(例えば$\tanh$やReLUなど)である.
なお,この問題ではパラメータの学習を行わず,ランダムに初期化されたパラメータで$y$を計算するだけでよい.次元数などのハイパーパラメータは,$d_w = 300, d_h=50$など,適当な値に設定せよ(以降の問題でも同様である).
第8章と違って入力データの長さが文によってちがいます.torch.nn.utils.rnn
にあるいろいろを使って可変長の系列の最後に詰めもの(パディング)をして扱えるようにします.
import random as rd
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.nn.utils.rnn import pad_sequence as pad
from torch.nn.utils.rnn import pack_padded_sequence as pack
from torch.nn.utils.rnn import pad_packed_sequence as unpack
データセットを保持するDataset
クラスを作ります.
入力文のsource
と目的ラベルのtarget
に加えて,入力文の長さlengths
をメンバとして持ちます.
class Dataset(torch.utils.data.Dataset):
def __init__(self, source, target):
self.source = source
self.target = target
self.lengths = torch.tensor([len(x) for x in source])
self.size = len(source)
def __len__(self):
return self.size
def __getitem__(self, index):
return {
'src':self.source[index],
'trg':self.target[index],
'lengths':self.lengths[index],
}
def collate(self, xs):
return {
'src':pad([x['src'] for x in xs]),
'trg':torch.stack([x['trg'] for x in xs], dim=-1),
'lengths':torch.stack([x['lengths'] for x in xs], dim=-1)
}
データセットを用意します.
train_dataset = Dataset(train_s, train_t)
valid_dataset = Dataset(valid_s, valid_t)
test_dataset = Dataset(test_s, test_t)
8章と同じSampler
を定義します.
class Sampler(torch.utils.data.Sampler):
def __init__(self, dataset, width, shuffle = False):
self.dataset = dataset
self.width = width
self.shuffle = shuffle
if not shuffle:
self.indices = torch.arange(len(dataset))
def __iter__(self):
if self.shuffle:
self.indices = torch.randperm(len(self.dataset))
index = 0
while index < len(self.dataset):
yield self.indices[index : index + self.width]
index += self.width
バッチ内の系列が降順になっているとパディングを詰めるときに都合がいいので,そのような成約を満たすサンプラを定義します.
インデックスをあらかじめ長さの降順に並び替えて前からバッチに積んでいくだけです.
class DescendingSampler(Sampler):
def __init__(self, dataset, width, shuffle = False):
assert not shuffle
super().__init__(dataset, width, shuffle)
self.indices = self.indices[self.dataset.lengths[self.indices].argsort(descending=True)]
また,訓練時はなるべくバッチ内のパディングが少ない方が無駄な計算が少なくなって学習が速くなるので,そういう詰み方も実装します.上記2例はバッチ数でインデックスを区切っていますが,次の子はバッチ内の最大トークン数で区切っているので,バッチ内の事例数は必ずしも一定ではありません.
class MaxTokensSampler(Sampler):
def __iter__(self):
self.indices = torch.randperm(len(self.dataset))
self.indices = self.indices[self.dataset.lengths[self.indices].argsort(descending=True)]
for batch in self.generate_batches():
yield batch
def generate_batches(self):
batches = []
batch = []
acc = 0
max_len = 0
for index in self.indices:
acc += 1
this_len = self.dataset.lengths[index]
max_len = max(max_len, this_len)
if acc * max_len > self.width:
batches.append(batch)
batch = [index]
acc = 1
max_len = this_len
else:
batch.append(index)
if batch != []:
batches.append(batch)
rd.shuffle(batches)
return batches
DataLoader
をつくる関数を用意しておきます.
def gen_loader(dataset, width, sampler=Sampler, shuffle=False, num_workers=8):
return torch.utils.data.DataLoader(
dataset,
batch_sampler = sampler(dataset, width, shuffle),
collate_fn = dataset.collate,
num_workers = num_workers,
)
def gen_descending_loader(dataset, width, num_workers=0):
return gen_loader(dataset, width, sampler = DescendingSampler, shuffle = False, num_workers = num_workers)
def gen_maxtokens_loader(dataset, width, num_workers=0):
return gen_loader(dataset, width, sampler = MaxTokensSampler, shuffle = True, num_workers = num_workers)
1層の単方向LSTM分類器を定義します.データセットのcollate
でpad
されたテンソルをpack
するときに各文の長さが必要となります.
class LSTMClassifier(nn.Module):
def __init__(self, v_size, e_size, h_size, c_size, dropout=0.2):
super().__init__()
self.embed = nn.Embedding(v_size, e_size)
self.rnn = nn.LSTM(e_size, h_size, num_layers = 1)
self.out = nn.Linear(h_size, c_size)
self.dropout = nn.Dropout(dropout)
self.embed.weight.data.uniform_(-0.1, 0.1)
for name, param in self.rnn.named_parameters():
if 'weight' in name or 'bias' in name:
param.data.uniform_(-0.1, 0.1)
self.out.weight.data.uniform_(-0.1, 0.1)
def forward(self, batch, h=None):
x = self.embed(batch['src'])
x = pack(x, batch['lengths'])
x, (h, c) = self.rnn(x, h)
h = self.out(h)
return h.squeeze(0)
予測します.
model = LSTMClassifier(len(vocab_dict), 300, 50, 4)
loader = gen_loader(test_dataset, 10, DescendingSampler, False)
model(iter(loader).next()).argmax(dim=-1)
tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
82. 確率的勾配降下法による学習
確率的勾配降下法(SGD: Stochastic Gradient Descent)を用いて,問題81で構築したモデルを学習せよ.訓練データ上の損失と正解率,評価データ上の損失と正解率を表示しながらモデルを学習し,適当な基準(例えば10エポックなど)で終了させよ.
TaskとTrainerを定義して訓練を回せばいいです.
class Task:
def __init__(self):
self.criterion = nn.CrossEntropyLoss()
def train_step(self, model, batch):
model.zero_grad()
loss = self.criterion(model(batch), batch['trg'])
loss.backward()
return loss.item()
def valid_step(self, model, batch):
with torch.no_grad():
loss = self.criterion(model(batch), batch['trg'])
return loss.item()
class Trainer:
def __init__(self, model, loaders, task, optimizer, max_iter, device = None):
self.model = model
self.model.to(device)
self.train_loader, self.valid_loader = loaders
self.task = task
self.optimizer = optimizer
self.max_iter = max_iter
self.device = device
def send(self, batch):
for key in batch:
batch[key] = batch[key].to(self.device)
return batch
def train_epoch(self):
self.model.train()
acc = 0
for n, batch in enumerate(self.train_loader):
batch = self.send(batch)
acc += self.task.train_step(self.model, batch)
self.optimizer.step()
return acc / n
def valid_epoch(self):
self.model.eval()
acc = 0
for n, batch in enumerate(self.valid_loader):
batch = self.send(batch)
acc += self.task.valid_step(self.model, batch)
return acc / n
def train(self):
for epoch in range(self.max_iter):
train_loss = self.train_epoch()
valid_loss = self.valid_epoch()
print('epoch {}, train_loss:{:.5f}, valid_loss:{:.5f}'.format(epoch, train_loss, valid_loss))
学習します.
device = torch.device('cuda')
model = LSTMClassifier(len(vocab_dict), 300, 128, 4)
loaders = (
gen_loader(train_dataset, 1),
gen_loader(valid_dataset, 1),
)
task = Task()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=True)
trainer = Trainer(model, loaders, task, optimizer, 3, device)
trainer.train()
予測してみます.
import numpy as np
class Predictor:
def __init__(self, model, loader, device=None):
self.model = model
self.loader = loader
self.device = device
def send(self, batch):
for key in batch:
batch[key] = batch[key].to(self.device)
return batch
def infer(self, batch):
self.model.eval()
batch = self.send(batch)
return self.model(batch).argmax(dim=-1).item()
def predict(self):
lst = []
for batch in self.loader:
lst.append(self.infer(batch))
return lst
def accuracy(true, pred):
return np.mean([t == p for t, p in zip(true, pred)])
predictor = Predictor(model, gen_loader(train_dataset, 1), device)
pred = predictor.predict()
print('学習データでの正解率 :', accuracy(train_t, pred))
predictor = Predictor(model, gen_loader(test_dataset, 1), device)
pred = predictor.predict()
print('評価データでの正解率 :', accuracy(test_t, pred))
学習データでの正解率 : 0.7592661924372894
評価データでの正解率 : 0.6384730538922155
もうすこしエポック回せばもうすこし精度あがりますが,気にしないことにします.
83. ミニバッチ化・GPU上での学習
問題82のコードを改変し,$B$事例ごとに損失・勾配を計算して学習を行えるようにせよ($B$の値は適当に選べ).また,GPU上で学習を実行せよ.
model = LSTMClassifier(len(vocab_dict), 300, 128, 4)
loaders = (
gen_maxtokens_loader(train_dataset, 4000),
gen_descending_loader(valid_dataset, 128),
)
task = Task()
optimizer = optim.SGD(model.parameters(), lr=0.2, momentum=0.9, nesterov=True)
trainer = Trainer(model, loaders, task, optimizer, 10, device)
trainer.train()
epoch 0, train_loss:1.22489, valid_loss:1.26302
epoch 1, train_loss:1.11631, valid_loss:1.19404
epoch 2, train_loss:1.07750, valid_loss:1.18451
epoch 3, train_loss:0.96149, valid_loss:1.06748
epoch 4, train_loss:0.81597, valid_loss:0.86547
epoch 5, train_loss:0.74748, valid_loss:0.81049
epoch 6, train_loss:0.80179, valid_loss:0.89621
epoch 7, train_loss:0.60231, valid_loss:0.78494
epoch 8, train_loss:0.52551, valid_loss:0.73272
epoch 9, train_loss:0.97286, valid_loss:1.05034
predictor = Predictor(model, gen_loader(train_dataset, 1), device)
pred = predictor.predict()
print('学習データでの正解率 :', accuracy(train_t, pred))
predictor = Predictor(model, gen_loader(test_dataset, 1), device)
pred = predictor.predict()
print('評価データでの正解率 :', accuracy(test_t, pred))
学習データでの正解率 : 0.7202358667165856
評価データでの正解率 : 0.6773952095808383
なぜかロスが上がって精度が落ちていますが,気にしないで強く生きましょう.
84. 単語ベクトルの導入
事前学習済みの単語ベクトル(例えば,Google Newsデータセット(約1,000億単語)での学習済み単語ベクトル)で単語埋め込み$\mathrm{emb}(x)$を初期化し,学習せよ.
from gensim.models import KeyedVectors
vectors = KeyedVectors.load_word2vec_format('data/GoogleNews-vectors-negative300.bin.gz', binary=True)
単語エンベディングを初期化してから学習させます.
def init_embed(embed):
for i, token in enumerate(vocab_list):
if token in vectors:
embed.weight.data[i] = torch.from_numpy(vectors[token])
return embed
model = LSTMClassifier(len(vocab_dict), 300, 128, 4)
init_embed(model.embed)
task = Task()
optimizer = optim.SGD(model.parameters(), lr=0.05, momentum=0.9, nesterov=True)
trainer = Trainer(model, loaders, task, optimizer, 10, device)
trainer.train()
epoch 0, train_loss:1.21390, valid_loss:1.19333
epoch 1, train_loss:0.88751, valid_loss:0.74930
epoch 2, train_loss:0.57240, valid_loss:0.65822
epoch 3, train_loss:0.50240, valid_loss:0.62686
epoch 4, train_loss:0.45800, valid_loss:0.59535
epoch 5, train_loss:0.44051, valid_loss:0.55849
epoch 6, train_loss:0.38251, valid_loss:0.51837
epoch 7, train_loss:0.35731, valid_loss:0.47709
epoch 8, train_loss:0.30278, valid_loss:0.43797
epoch 9, train_loss:0.25518, valid_loss:0.41287
predictor = Predictor(model, gen_loader(train_dataset, 1), device)
pred = predictor.predict()
print('学習データでの正解率 :', accuracy(train_t, pred))
predictor = Predictor(model, gen_loader(test_dataset, 1), device)
pred = predictor.predict()
print('評価データでの正解率 :', accuracy(test_t, pred))
学習データでの正解率 : 0.925028079371022
評価データでの正解率 : 0.8839820359281437
85. 双方向RNN・多層化
順方向と逆方向のRNNの両方を用いて入力テキストをエンコードし,モデルを学習せよ.
$
\overleftarrow h_{T+1} = 0, \
\overleftarrow h_t = {\rm \overleftarrow{RNN}}(\mathrm{emb}(x_t), \overleftarrow h_{t+1}), \
y = {\rm softmax}(W^{(yh)} [\overrightarrow h_T; \overleftarrow h_1] + b^{(y)})
$
ただし,$\overrightarrow h_t \in \mathbb{R}^{d_h}, \overleftarrow h_t \in \mathbb{R}^{d_h}$はそれぞれ,順方向および逆方向のRNNで求めた時刻$t$の隠れ状態ベクトル,${\rm \overleftarrow{RNN}}(x,h)$は入力$x$と次時刻の隠れ状態$h$から前状態を計算するRNNユニット,$W^{(yh)} \in \mathbb{R}^{L \times 2d_h}$は隠れ状態ベクトルからカテゴリを予測するための行列,$b^{(y)} \in \mathbb{R}^{L}$はバイアス項である.また,$[a; b]$はベクトル$a$と$b$の連結を表す。
さらに,双方向RNNを多層化して実験せよ.
nn.LSTM
のパラメータをちょっとかえれば多層化も双方向化もできます.
隠れ状態が増えるので,最後の2つ(最後の層の順方向と逆方向の隠れ状態)を取得します.
class BiLSTMClassifier(nn.Module):
def __init__(self, v_size, e_size, h_size, c_size, dropout=0.2):
super().__init__()
self.embed = nn.Embedding(v_size, e_size)
self.rnn = nn.LSTM(e_size, h_size, num_layers = 2, bidirectional = True)
self.out = nn.Linear(h_size * 2, c_size)
self.dropout = nn.Dropout(dropout)
nn.init.uniform_(self.embed.weight, -0.1, 0.1)
for name, param in self.rnn.named_parameters():
if 'weight' in name or 'bias' in name:
nn.init.uniform_(param, -0.1, 0.1)
nn.init.uniform_(self.out.weight, -0.1, 0.1)
def forward(self, batch, h=None):
x = self.embed(batch['src'])
x = pack(x, batch['lengths'])
x, (h, c) = self.rnn(x, h)
h = h[-2:]
h = h.transpose(0,1)
h = h.contiguous().view(-1, h.size(1) * h.size(2))
h = self.out(h)
return h
model = BiLSTMClassifier(len(vocab_dict), 300, 128, 4)
init_embed(model.embed)
task = Task()
optimizer = optim.SGD(model.parameters(), lr=0.05, momentum=0.9, nesterov=True)
trainer = Trainer(model, loaders, task, optimizer, 10, device)
trainer.train()
predictor = Predictor(model, gen_loader(train_dataset, 1), device)
pred = predictor.predict()
print('学習データでの正解率 :', accuracy(train_t, pred))
predictor = Predictor(model, gen_loader(test_dataset, 1), device)
pred = predictor.predict()
print('評価データでの正解率 :', accuracy(test_t, pred))
86. 畳み込みニューラルネットワーク (CNN)
ID番号で表現された単語列$\boldsymbol x = (x_1, x_2, \dots, x_T)$がある.ただし,$T$は単語列の長さ,$x_t \in \mathbb{R}^{V}$は単語のID番号のone-hot表記である($V$は単語の総数である).畳み込みニューラルネットワーク(CNN: Convolutional Neural Network)を用い,単語列$\boldsymbol x$からカテゴリ$y$を予測するモデルを実装せよ.
ただし,畳み込みニューラルネットワークの構成は以下の通りとする.
- 単語埋め込みの次元数: $d_w$
- 畳み込みのフィルターのサイズ: 3 トークン
- 畳み込みのストライド: 1 トークン
- 畳み込みのパディング: あり
- 畳み込み演算後の各時刻のベクトルの次元数: $d_h$
- 畳み込み演算後に最大値プーリング(max pooling)を適用し,入力文を$d_h$次元の隠れベクトルで表現
すなわち,時刻$t$の特徴ベクトル$p_t \in \mathbb{R}^{d_h}$は次式で表される.
$
p_t = g(W^{(px)} [\mathrm{emb}(x_{t-1}); \mathrm{emb}(x_t); \mathrm{emb}(x_{t+1})] + b^{(p)})
$
ただし,$W^{(px)} \in \mathbb{R}^{d_h \times 3d_w}, b^{(p)} \in \mathbb{R}^{d_h}$はCNNのパラメータ,$g$は活性化関数(例えば$\tanh$やReLUなど),$[a; b; c]$はベクトル$a, b, c$の連結である.なお,行列$W^{(px)}$の列数が$3d_w$になるのは,3個のトークンの単語埋め込みを連結したものに対して,線形変換を行うためである.
最大値プーリングでは,特徴ベクトルの次元毎に全時刻における最大値を取り,入力文書の特徴ベクトル$c \in \mathbb{R}^{d_h}$を求める.$c[i]$でベクトル$c$の$i$番目の次元の値を表すことにすると,最大値プーリングは次式で表される.
$
c[i] = \max_{1 \leq t \leq T} p_t[i]
$
最後に,入力文書の特徴ベクトル$c$に行列$W^{(yc)} \in \mathbb{R}^{L \times d_h}$とバイアス項$b^{(y)} \in \mathbb{R}^{L}$による線形変換とソフトマックス関数を適用し,カテゴリ$y$を予測する.
$
y = {\rm softmax}(W^{(yc)} c + b^{(y)})
$
なお,この問題ではモデルの学習を行わず,ランダムに初期化された重み行列で$y$を計算するだけでよい.
入力データの両端にPAD
トークンを付けたいので,そういうデータセットにします.
cnn_vocab_list = ['[PAD]', '[UNK]'] + vocab_in_train
cnn_vocab_dict = {x:n for n, x in enumerate(cnn_vocab_list)}
def cnn_sent_to_ids(sent):
return torch.tensor([cnn_vocab_dict[x if x in cnn_vocab_dict else '[UNK]'] for x in sent], dtype=torch.long)
print(train_x[0])
print(cnn_sent_to_ids(train_x[0]))
['Kathleen', 'Sebelius', "'", 'LGBT', 'legacy']
tensor([ 1, 1, 3, 2649, 1])
窓幅が3なのでEOS
を2つつけました.
def cnn_dataset_to_ids(dataset):
return [cnn_sent_to_ids(x) for x in dataset]
cnn_train_s = cnn_dataset_to_ids(train_x)
cnn_valid_s = cnn_dataset_to_ids(valid_x)
cnn_test_s = cnn_dataset_to_ids(test_x)
cnn_train_s[:3]
[tensor([ 1, 1, 3, 2649, 1]),
tensor([ 10, 6741, 1446, 2077, 584, 11, 548, 33, 52, 874, 6742]),
tensor([ 1, 206, 4199, 316, 1900, 1233, 1])]
class CNNDataset(Dataset):
def collate(self, xs):
max_seq_len = max([x['lengths'] for x in xs])
src = [torch.cat([x['src'], torch.zeros(max_seq_len - x['lengths'], dtype=torch.long)], dim=-1) for x in xs]
src = torch.stack(src)
mask = [[1] * x['lengths'] + [0] * (max_seq_len - x['lengths']) for x in xs]
mask = torch.tensor(mask, dtype=torch.long)
return {
'src':src,
'trg':torch.tensor([x['trg'] for x in xs]),
'mask':mask,
}
cnn_train_dataset = CNNDataset(cnn_train_s, train_t)
cnn_valid_dataset = CNNDataset(cnn_valid_s, valid_t)
cnn_test_dataset = CNNDataset(cnn_test_s, test_t)
CNNモデルを作ります.
class CNNClassifier(nn.Module):
def __init__(self, v_size, e_size, h_size, c_size, dropout=0.2):
super().__init__()
self.embed = nn.Embedding(v_size, e_size)
self.conv = nn.Conv1d(e_size, h_size, 3, padding=1)
self.act = nn.ReLU()
self.out = nn.Linear(h_size, c_size)
self.dropout = nn.Dropout(dropout)
nn.init.normal_(self.embed.weight, 0, 0.1)
nn.init.kaiming_normal_(self.conv.weight)
nn.init.constant_(self.conv.bias, 0)
nn.init.kaiming_normal_(self.out.weight)
nn.init.constant_(self.out.bias, 0)
def forward(self, batch):
x = self.embed(batch['src'])
x = self.dropout(x)
x = self.conv(x.transpose(-1, -2))
x = self.act(x)
x = self.dropout(x)
x.masked_fill_(batch['mask'].unsqueeze(-2) == 0, -1e4)
x = F.max_pool1d(x, x.size(-1)).squeeze(-1)
x = self.out(x)
return x
nn.Conv1d
にpadding=1
を指定したので,両端にパディングトークンが1つずつ挿入されます.このトークンはデフォルトで0ですが,系列長の長さを合わせるパディングのidも0なので一応問題はないです.
時間方向に畳み込む際に,最後の次元を時間軸にするためにtranspose
しています.
パディング部分の値は-1
にして最大値プーリングで値をとってこないようにしています.
max_pool1d
で最大値プーリングする際には次元を指定する必要があります.
87. 確率的勾配降下法によるCNNの学習
確率的勾配降下法(SGD: Stochastic Gradient Descent)を用いて,問題86で構築したモデルを学習せよ.訓練データ上の損失と正解率,評価データ上の損失と正解率を表示しながらモデルを学習し,適当な基準(例えば10エポックなど)で終了させよ.
学習させます.
def init_cnn_embed(embed):
for i, token in enumerate(cnn_vocab_list):
if token in vectors:
embed.weight.data[i] = torch.from_numpy(vectors[token])
return embed
model = CNNClassifier(len(cnn_vocab_dict), 300, 128, 4)
init_cnn_embed(model.embed)
loaders = (
gen_maxtokens_loader(cnn_train_dataset, 4000),
gen_descending_loader(cnn_valid_dataset, 32),
)
task = Task()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=True)
trainer = Trainer(model, loaders, task, optimizer, 10, device)
trainer.train()
epoch 0, train_loss:1.03501, valid_loss:0.85454
epoch 1, train_loss:0.68068, valid_loss:0.70825
epoch 2, train_loss:0.56784, valid_loss:0.60257
epoch 3, train_loss:0.50570, valid_loss:0.55611
epoch 4, train_loss:0.45707, valid_loss:0.52386
epoch 5, train_loss:0.42078, valid_loss:0.48479
epoch 6, train_loss:0.38858, valid_loss:0.45933
epoch 7, train_loss:0.36667, valid_loss:0.43547
epoch 8, train_loss:0.34746, valid_loss:0.41509
epoch 9, train_loss:0.32849, valid_loss:0.40350
predictor = Predictor(model, gen_loader(cnn_train_dataset, 1), device)
pred = predictor.predict()
print('学習データでの正解率 :', accuracy(train_t, pred))
predictor = Predictor(model, gen_loader(cnn_test_dataset, 1), device)
pred = predictor.predict()
print('評価データでの正解率 :', accuracy(test_t, pred))
学習データでの正解率 : 0.9106140022463497
評価データでの正解率 : 0.8847305389221557
88. パラメータチューニング
問題85や問題87のコードを改変し,ニューラルネットワークの形状やハイパーパラメータを調整しながら,高性能なカテゴリ分類器を構築せよ.
せっかくなのでLSTMの出力をCNNで最大値プーリングするモデルを書いてみました.
class BiLSTMCNNDataset(Dataset):
def collate(self, xs):
max_seq_len = max([x['lengths'] for x in xs])
mask = [[1] * (x['lengths'] - 2) + [0] * (max_seq_len - x['lengths']) for x in xs]
mask = torch.tensor(mask, dtype=torch.long)
return {
'src':pad([x['src'] for x in xs]),
'trg':torch.stack([x['trg'] for x in xs], dim=-1),
'mask':mask,
'lengths':torch.stack([x['lengths'] for x in xs], dim=-1)
}
rnncnn_train_dataset = BiLSTMCNNDataset(cnn_train_s, train_t)
rnncnn_valid_dataset = BiLSTMCNNDataset(cnn_valid_s, valid_t)
rnncnn_test_dataset = BiLSTMCNNDataset(cnn_test_s, test_t)
class BiLSTMCNNClassifier(nn.Module):
def __init__(self, v_size, e_size, h_size, c_size, dropout=0.2):
super().__init__()
self.embed = nn.Embedding(v_size, e_size)
self.rnn = nn.LSTM(e_size, h_size, bidirectional = True)
self.conv = nn.Conv1d(h_size* 2, h_size, 3, padding=1)
self.act = nn.ReLU()
self.out = nn.Linear(h_size, c_size)
self.dropout = nn.Dropout(dropout)
nn.init.uniform_(self.embed.weight, -0.1, 0.1)
for name, param in self.rnn.named_parameters():
if 'weight' in name or 'bias' in name:
nn.init.uniform_(param, -0.1, 0.1)
nn.init.kaiming_normal_(self.conv.weight)
nn.init.constant_(self.conv.bias, 0)
nn.init.kaiming_normal_(self.out.weight)
nn.init.constant_(self.out.bias, 0)
def forward(self, batch, h=None):
x = self.embed(batch['src'])
x = self.dropout(x)
x = pack(x, batch['lengths'])
x, (h, c) = self.rnn(x, h)
x, _ = unpack(x)
x = self.dropout(x)
x = self.conv(x.permute(1, 2, 0))
x = self.act(x)
x = self.dropout(x)
x.masked_fill_(batch['mask'].unsqueeze(-2) == 0, -1)
x = F.max_pool1d(x, x.size(-1)).squeeze(-1)
x = self.out(x)
return x
loaders = (
gen_maxtokens_loader(rnncnn_train_dataset, 4000),
gen_descending_loader(rnncnn_valid_dataset, 32),
)
task = Task()
for h in [32, 64, 128, 256, 512]:
model = BiLSTMCNNClassifier(len(cnn_vocab_dict), 300, h, 4)
init_cnn_embed(model.embed)
optimizer = optim.SGD(model.parameters(), lr=0.02, momentum=0.9, nesterov=True)
trainer = Trainer(model, loaders, task, optimizer, 10, device)
trainer.train()
predictor = Predictor(model, gen_loader(rnncnn_test_dataset, 1), device)
pred = predictor.predict()
print('評価データでの正解率 :', accuracy(test_t, pred))
epoch 0, train_loss:1.21905, valid_loss:1.12725
epoch 1, train_loss:0.95913, valid_loss:0.84094
epoch 2, train_loss:0.66851, valid_loss:0.66997
epoch 3, train_loss:0.57141, valid_loss:0.61373
epoch 4, train_loss:0.52795, valid_loss:0.59354
epoch 5, train_loss:0.49844, valid_loss:0.57013
epoch 6, train_loss:0.47408, valid_loss:0.55163
epoch 7, train_loss:0.44922, valid_loss:0.52349
epoch 8, train_loss:0.41864, valid_loss:0.49231
epoch 9, train_loss:0.38975, valid_loss:0.46807
評価データでの正解率 : 0.8690119760479041
epoch 0, train_loss:1.16516, valid_loss:1.06582
epoch 1, train_loss:0.81246, valid_loss:0.71224
epoch 2, train_loss:0.58068, valid_loss:0.61988
epoch 3, train_loss:0.52451, valid_loss:0.58465
epoch 4, train_loss:0.48807, valid_loss:0.55663
epoch 5, train_loss:0.45712, valid_loss:0.52742
epoch 6, train_loss:0.41639, valid_loss:0.50089
epoch 7, train_loss:0.38595, valid_loss:0.46442
epoch 8, train_loss:0.35262, valid_loss:0.43459
epoch 9, train_loss:0.32527, valid_loss:0.40692
評価データでの正解率 : 0.8772455089820359
epoch 0, train_loss:1.12191, valid_loss:0.97533
epoch 1, train_loss:0.71378, valid_loss:0.66554
epoch 2, train_loss:0.55280, valid_loss:0.59733
epoch 3, train_loss:0.50526, valid_loss:0.57163
epoch 4, train_loss:0.46889, valid_loss:0.53955
epoch 5, train_loss:0.43500, valid_loss:0.50500
epoch 6, train_loss:0.40006, valid_loss:0.47222
epoch 7, train_loss:0.36444, valid_loss:0.43941
epoch 8, train_loss:0.33329, valid_loss:0.41224
epoch 9, train_loss:0.30588, valid_loss:0.39965
評価データでの正解率 : 0.8839820359281437
epoch 0, train_loss:1.04536, valid_loss:0.84626
epoch 1, train_loss:0.61410, valid_loss:0.62255
epoch 2, train_loss:0.49830, valid_loss:0.55984
epoch 3, train_loss:0.44190, valid_loss:0.51720
epoch 4, train_loss:0.39713, valid_loss:0.46718
epoch 5, train_loss:0.35052, valid_loss:0.43181
epoch 6, train_loss:0.32145, valid_loss:0.39898
epoch 7, train_loss:0.30279, valid_loss:0.37586
epoch 8, train_loss:0.28171, valid_loss:0.37333
epoch 9, train_loss:0.26904, valid_loss:0.37849
評価データでの正解率 : 0.8884730538922155
epoch 0, train_loss:0.93974, valid_loss:0.71999
epoch 1, train_loss:0.53687, valid_loss:0.58747
epoch 2, train_loss:0.44848, valid_loss:0.52432
epoch 3, train_loss:0.38761, valid_loss:0.46509
epoch 4, train_loss:0.34431, valid_loss:0.43651
epoch 5, train_loss:0.31699, valid_loss:0.39881
epoch 6, train_loss:0.28963, valid_loss:0.38732
epoch 7, train_loss:0.27550, valid_loss:0.37152
epoch 8, train_loss:0.26003, valid_loss:0.36476
epoch 9, train_loss:0.24991, valid_loss:0.36012
評価データでの正解率 : 0.8944610778443114
隠れ状態のサイズを変えながら学習してみました.隠れ状態のサイズが大きいほうが良さそうという結果になってます.
89. 事前学習済み言語モデルからの転移学習
事前学習済み言語モデル(例えばBERTなど)を出発点として,ニュース記事見出しをカテゴリに分類するモデルを構築せよ.
huggingface/transformersを使います.
from transformers import *
BERTの入力はwordpieceなので,トークナイザを通してあげないといけません.
トークナイザを用意します.
tokenizer = BertTokenizer.from_pretrained('bert-base-cased')
トークナイズします.
def read_for_bert(filename):
with open(filename) as f:
dataset = f.read().splitlines()
dataset = [line.split('\t') for line in dataset]
dataset_t = [categories.index(line[0]) for line in dataset]
dataset_x = [torch.tensor(tokenizer.encode(line[1]), dtype=torch.long) for line in dataset]
return dataset_x, torch.tensor(dataset_t, dtype=torch.long)
bert_train_x, bert_train_t = read_for_bert('data/train.txt')
bert_valid_x, bert_valid_t = read_for_bert('data/valid.txt')
bert_test_x, bert_test_t = read_for_bert('data/test.txt')
BERT用のデータセットクラスを用意します.パディングとかを雑にやっています.
mask
はアテンションマスクです.パディングトークンにアテンションがかからないようにしています.
class BertDataset(Dataset):
def collate(self, xs):
max_seq_len = max([x['lengths'] for x in xs])
src = [torch.cat([x['src'], torch.zeros(max_seq_len - x['lengths'], dtype=torch.long)], dim=-1) for x in xs]
src = torch.stack(src)
mask = [[1] * x['lengths'] + [0] * (max_seq_len - x['lengths']) for x in xs]
mask = torch.tensor(mask, dtype=torch.long)
return {
'src':src,
'trg':torch.tensor([x['trg'] for x in xs]),
'mask':mask,
}
bert_train_dataset = BertDataset(bert_train_x, bert_train_t)
bert_valid_dataset = BertDataset(bert_valid_x, bert_valid_t)
bert_test_dataset = BertDataset(bert_test_x, bert_test_t)
事前学習モデルを読み込みます.
class BertClassifier(nn.Module):
def __init__(self):
super().__init__()
config = BertConfig.from_pretrained('bert-base-cased', num_labels=4)
self.bert = BertForSequenceClassification.from_pretrained('bert-base-cased', config=config)
def forward(self, batch):
x = self.bert(batch['src'], attention_mask=batch['mask'])
return x[0]
model = BertClassifier()
loaders = (
gen_maxtokens_loader(bert_train_dataset, 1000),
gen_descending_loader(bert_valid_dataset, 32),
)
task = Task()
optimizer = optim.AdamW(model.parameters(), lr=1e-5)
trainer = Trainer(model, loaders, task, optimizer, 5, device)
trainer.train()
predictor = Predictor(model, gen_loader(bert_train_dataset, 1), device)
pred = predictor.predict()
print('学習データでの正解率 :', accuracy(train_t, pred))
predictor = Predictor(model, gen_loader(bert_test_dataset, 1), device)
pred = predictor.predict()
print('学習データでの正解率 :', accuracy(test_t, pred))
学習データでの正解率 : 0.9927929614376638
学習データでの正解率 : 0.9229041916167665
いいですね.