先日,言語処理100本ノック2020が公開されました.私自身,自然言語処理を初めてから1年しか経っておらず,細かいことはよくわかっていませんが,技術力向上のために全ての問題を解いて公開していこうと思います.
すべてjupyter notebook上で実行するものとし,問題文の制約は都合よく破っていいものとします.
ソースコードはgithubにもあります.あります.
7章はこちら.
環境はPython3.8.2とUbuntu18.04です.
第8章: ニューラルネット
第6章で取り組んだニュース記事のカテゴリ分類を題材として,ニューラルネットワークでカテゴリ分類モデルを実装する.なお,この章ではPyTorch, TensorFlow, Chainerなどの機械学習プラットフォームを活用せよ.
PyTorchを使います.
70. 単語ベクトルの和による特徴量
問題50で構築した学習データ,検証データ,評価データを行列・ベクトルに変換したい.例えば,学習データについて,すべての事例の特徴ベクトルを並べた行列$X$と正解ラベルを並べた行列(ベクトル)$Y$を作成したい.
X = \begin{pmatrix}
\boldsymbol{x}_1 \
\boldsymbol{x}_2 \
\dots \
\boldsymbol{x}_n \
\end{pmatrix} \in \mathbb{R}^{n \times d},
Y = \begin{pmatrix}
y_1 \
y_2 \
\dots \
y_n \
\end{pmatrix} \in \mathbb{N}^{n}
>
> ここで,$n$は学習データの事例数であり,$\boldsymbol x_i \in \mathbb{R}^d$と$y_i \in \mathbb N$はそれぞれ,$i \in \{1, \dots, n\}$番目の事例の特徴量ベクトルと正解ラベルを表す.
> なお,今回は「ビジネス」「科学技術」「エンターテイメント」「健康」の4カテゴリ分類である.$\mathbb N_4$で$4$未満の自然数($0$を含む)を表すことにすれば,任意の事例の正解ラベル$y_i$は$y_i \in \mathbb N_4$で表現できる.
> 以降では,ラベルの種類数を$L$で表す(今回の分類タスクでは$L=4$である).
>
> $i$番目の事例の特徴ベクトル$\boldsymbol x_i$は,次式で求める.
>
> $$\boldsymbol x_i = \frac{1}{T_i} \sum_{t=1}^{T_i} \mathrm{emb}(w_{i,t})$$
>
> ここで,$i$番目の事例は$T_i$個の(記事見出しの)単語列$(w_{i,1}, w_{i,2}, \dots, w_{i,T_i})$から構成され,$\mathrm{emb}(w) \in \mathbb{R}^d$は単語$w$に対応する単語ベクトル(次元数は$d$)である.すなわち,$i$番目の事例の記事見出しを,その見出しに含まれる単語のベクトルの平均で表現したものが$\boldsymbol x_i$である.今回は単語ベクトルとして,問題60でダウンロードしたものを用いればよい.$300$次元の単語ベクトルを用いたので,$d=300$である.
> $i$番目の事例のラベル$y_i$は,次のように定義する.
>
>```math
y_i = \begin{cases}
0 & (\mbox{記事}\boldsymbol x_i\mbox{が「ビジネス」カテゴリの場合}) \\
1 & (\mbox{記事}\boldsymbol x_i\mbox{が「科学技術」カテゴリの場合}) \\
2 & (\mbox{記事}\boldsymbol x_i\mbox{が「エンターテイメント」カテゴリの場合}) \\
3 & (\mbox{記事}\boldsymbol x_i\mbox{が「健康」カテゴリの場合}) \\
\end{cases}
なお,カテゴリ名とラベルの番号が一対一で対応付いていれば,上式の通りの対応付けでなくてもよい.
以上の仕様に基づき,以下の行列・ベクトルを作成し,ファイルに保存せよ.
- 学習データの特徴量行列: $X_{\rm train} \in \mathbb{R}^{N_t \times d}$
- 学習データのラベルベクトル: $Y_{\rm train} \in \mathbb{N}^{N_t}$
- 検証データの特徴量行列: $X_{\rm valid} \in \mathbb{R}^{N_v \times d}$
- 検証データのラベルベクトル: $Y_{\rm valid} \in \mathbb{N}^{N_v}$
- 評価データの特徴量行列: $X_{\rm test} \in \mathbb{R}^{N_e \times d}$
- 評価データのラベルベクトル: $Y_{\rm test} \in \mathbb{N}^{N_e}$
なお,$N_t, N_v, N_e$はそれぞれ,学習データの事例数,検証データの事例数,評価データの事例数である.
問題文が長くてTeXの修正がつらい.
import re
import spacy
単語に分割します.
nlp = spacy.load('en')
categories = ['b', 't', 'e', 'm']
category_names = ['business', 'science and technology', 'entertainment', 'health']
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, dataset_t
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')
特徴ベクトルに変換します.
import torch
from gensim.models import KeyedVectors
model = KeyedVectors.load_word2vec_format('../GoogleNews-vectors-negative300.bin.gz', binary=True)
def sent_to_vector(sent):
lst = [torch.tensor(model[token]) for token in sent if token in model]
return sum(lst) / len(lst)
def dataset_to_vector(dataset):
return torch.stack([sent_to_vector(x) for x in dataset])
train_v = dataset_to_vector(train_x)
valid_v = dataset_to_vector(valid_x)
test_v = dataset_to_vector(test_x)
train_v[0]
tensor([ 9.0576e-02, 5.4932e-02, -7.7393e-02, 1.1810e-01, -3.8849e-02,
-2.6074e-01, -6.4484e-02, 3.2715e-02, 1.1792e-01, -3.4363e-02,
-1.5137e-02, -1.7090e-02, 7.2632e-02, 1.0742e-02, 1.1194e-01,
5.8945e-02, 1.6275e-01, 1.5393e-01, 7.0496e-02, -1.5210e-01,
2.8320e-02, 1.1719e-02, 1.9702e-01, -1.5610e-02, -2.3438e-02,
1.8921e-02, 2.8687e-02, -2.3438e-02, 2.3315e-02, -5.7480e-02,
2.1973e-03, -1.0449e-01, -9.7534e-02, -1.3694e-01, 1.6144e-01,
-2.6062e-02, 3.1250e-02, 1.9482e-01, -1.0788e-01, 7.2571e-02,
-1.3916e-02, 1.1121e-01, 7.0801e-03, -4.1016e-02, -1.9580e-01,
1.7334e-02, 1.0986e-02, -6.9485e-03, 9.2773e-02, 7.2205e-02,
6.8298e-02, -5.3589e-02, -1.7447e-01, 1.0245e-01, -8.6426e-02,
-9.0942e-03, -1.7212e-01, -1.3789e-01, -1.0355e-01, 1.9226e-02,
1.0620e-02, 9.7626e-02, -5.1147e-02, 1.1371e-01, 3.5156e-02,
-4.8523e-03, -7.1960e-02, 1.1841e-01, -1.0974e-01, 1.2878e-01,
-7.3273e-02, 5.3711e-02, 9.6313e-02, -9.0950e-02, 4.3335e-02,
-4.7424e-02, -3.0518e-02, 5.2856e-02, 3.7842e-02, 2.2559e-01,
4.0161e-02, -2.3822e-01, -1.3531e-01, -3.8513e-02, -1.1475e-02,
-7.3242e-02, -1.9324e-01, 1.9553e-01, 1.0870e-01, 1.5405e-01,
2.8793e-02, -1.9226e-01, 3.1952e-02, -1.0471e-01, 4.9561e-02,
6.5918e-03, -5.6793e-02, 1.8628e-01, -5.5908e-02, -9.8999e-02,
-2.1448e-01, -1.6602e-02, 6.7627e-02, 2.1149e-02, -6.8970e-02,
2.3804e-03, -2.1729e-02, -9.1599e-02, -8.7585e-02, -1.1963e-01,
-8.7555e-02, 6.1768e-02, -1.6205e-02, 2.9572e-02, 1.2207e-04,
1.3300e-01, 1.6541e-02, -1.3672e-01, 1.4978e-01, -4.8828e-03,
-2.6172e-01, 3.9093e-02, 1.4761e-01, 1.3745e-01, 8.6670e-03,
-1.0797e-01, 8.3801e-02, 3.2690e-01, -6.9336e-02, 6.8115e-02,
1.0571e-01, -1.2269e-01, -1.4209e-01, 7.7923e-02, -1.6113e-02,
-6.8039e-02, 1.2909e-02, -4.9911e-02, 2.0142e-01, 9.5764e-02,
8.1078e-02, -2.6733e-02, -1.4606e-01, -1.0449e-01, 7.1014e-02,
9.4604e-03, 9.6436e-02, -3.3386e-02, -6.5552e-02, -4.0009e-02,
2.0976e-01, -9.5825e-02, 1.2494e-01, -1.1230e-02, 1.3062e-02,
1.8829e-02, -1.7525e-01, -1.6845e-01, -3.0334e-02, -5.6152e-02,
-2.3193e-02, -8.4961e-02, 4.6021e-02, 1.5533e-01, -2.4780e-02,
-1.7255e-01, -2.9472e-02, -3.2959e-03, -3.2166e-02, 1.1292e-01,
-5.0537e-02, 6.0730e-02, 1.8042e-01, -2.6678e-01, 6.5601e-02,
-2.4567e-01, -4.1382e-02, -2.4902e-02, -7.3853e-02, 3.8330e-02,
-3.5229e-01, -4.8477e-02, 7.8522e-02, 2.4719e-03, -1.1414e-02,
-8.9661e-02, -2.4341e-01, 4.9133e-02, -2.7954e-02, 9.2651e-02,
-4.8340e-02, -5.2063e-02, 5.5817e-02, -3.7842e-03, -1.6852e-01,
9.8267e-03, 2.1698e-02, -6.5107e-02, 9.8053e-02, -3.6621e-03,
-2.2009e-01, 1.1389e-01, 5.0537e-02, -1.4322e-01, -8.2336e-02,
-5.0507e-02, -2.2461e-02, -9.4971e-02, -1.0464e-01, -2.0959e-01,
-1.2964e-01, -1.0208e-02, -4.0894e-03, -1.4893e-02, -4.9637e-02,
6.3507e-02, -8.5968e-02, 2.3340e-01, 1.2207e-01, -1.6663e-01,
-1.6541e-01, 6.9924e-02, 2.4414e-02, -3.3630e-02, -2.2583e-02,
-2.1289e-01, 8.4106e-02, 1.1916e-01, -1.9623e-02, -3.2654e-02,
-3.2394e-02, 1.5515e-01, -7.9224e-02, -9.1919e-02, -6.3782e-03,
-3.6926e-02, 8.0456e-02, -4.5288e-02, 1.9531e-02, 7.4951e-02,
-8.0195e-02, -2.5232e-01, 1.0986e-01, -1.2573e-01, -1.0083e-01,
2.0972e-01, 1.3380e-03, 2.2363e-01, -6.7322e-02, -6.3477e-02,
-2.1167e-01, 5.0659e-03, -3.2227e-02, -2.0752e-02, 2.2107e-01,
-2.4243e-01, 1.4246e-01, 1.4465e-01, -2.0691e-01, -1.0516e-01,
-1.0327e-01, 1.6028e-01, -1.4748e-02, -1.9310e-02, 2.3193e-02,
1.5234e-01, 2.2034e-02, -8.0872e-04, -8.7729e-02, 5.9967e-02,
-2.6306e-02, 1.3672e-01, 1.5301e-02, 6.3965e-02, 1.9131e-02,
-5.8695e-02, 1.4355e-01, -9.6710e-02, 7.2235e-02, -1.0620e-02,
6.1523e-02, -1.2626e-01, 3.3813e-02, -2.1973e-03, -1.3843e-01,
-1.3458e-01, 5.4447e-02, -2.0325e-01, 1.2244e-01, 4.3335e-02,
-3.1372e-02, -1.9659e-01, -1.7270e-01, 2.9846e-02, -5.8533e-02,
6.7017e-02, 1.6748e-01, 1.1859e-01, 1.2134e-01, -1.7578e-02])
pickleにして保存.
import pickle
train_t = torch.tensor(train_t).long()
valid_t = torch.tensor(valid_t).long()
test_t = torch.tensor(test_t).long()
with open('data/train.feature.pickle', 'wb') as f:
pickle.dump(train_v, f)
with open('data/train.label.pickle', 'wb') as f:
pickle.dump(train_t, f)
with open('data/valid.feature.pickle', 'wb') as f:
pickle.dump(valid_v, f)
with open('data/valid.label.pickle', 'wb') as f:
pickle.dump(valid_t, f)
with open('data/test.feature.pickle', 'wb') as f:
pickle.dump(test_v, f)
with open('data/test.label.pickle', 'wb') as f:
pickle.dump(test_t, f)
71. 単層ニューラルネットワークによる予測
問題70で保存した行列を読み込み,学習データについて以下の計算を実行せよ.
$$
\hat{\boldsymbol y_1} = {\rm softmax}(\boldsymbol x_1 W), \
\hat{Y} = {\rm softmax}(X_{[1:4]} W)
$$
ただし,${\rm softmax}$はソフトマックス関数,$X_{[1:4]} \in \mathbb{R}^{4 \times d}$は特徴ベクトル$\boldsymbol x_1, \boldsymbol x_2, \boldsymbol x_3, \boldsymbol x_4$を縦に並べた行列である.
$$
X_{[1:4]} = \begin{pmatrix}
\boldsymbol x_1 \
\boldsymbol x_2 \
\boldsymbol x_3 \
\boldsymbol x_4 \
\end{pmatrix}
$$
行列$W \in \mathbb{R}^{d \times L}$は単層ニューラルネットワークの重み行列で,ここではランダムな値で初期化すればよい(問題73以降で学習して求める).なお,$\hat{\boldsymbol y_1} \in \mathbb{N}^L$は未学習の行列$W$で事例$x_1$を分類したときに,各カテゴリに属する確率を表すベクトルである.
同様に,$\hat{Y} \in \mathbb{N}^{n \times L}$は,学習データの事例$x_1, x_2, x_3, x_4$について,各カテゴリに属する確率を行列として表現している.
出題者の意図はおそらく,torch.empty().normal_()
をして,F.linear()
するみたいな操作をすることなのかと思いますが,わかってる人はもうnn.Module
継承しちゃっていいんじゃないかなという感じがします.次の問題と合わせて自動微分が何かしらない人になんとなくわからせたいという出題意図を感じます.
import torch.nn as nn
class Perceptron(nn.Module):
def __init__(self, v_size, c_size):
super().__init__()
self.fc = nn.Linear(v_size, c_size, bias = False)
nn.init.xavier_normal_(self.fc.weight)
def forward(self, x):
x = self.fc(x)
return x
model = Perceptron(300, 4)
x = model(train_v[0])
x = torch.softmax(x, dim=-1)
x
tensor([0.2450, 0.2351, 0.2716, 0.2483], grad_fn=<SoftmaxBackward>)
x = model(train_v[:4])
x = torch.softmax(x, dim=-1)
x
tensor([[0.2450, 0.2351, 0.2716, 0.2483],
[0.2323, 0.2387, 0.2765, 0.2525],
[0.2093, 0.2120, 0.2750, 0.3037],
[0.2200, 0.2427, 0.2623, 0.2751]], grad_fn=<SoftmaxBackward>)
ちゃんと確率分布になっていますね.
72. 損失と勾配の計算
学習データの事例$x_1$と事例集合$x_1, x_2, x_3, x_4$に対して,クロスエントロピー損失と,行列$W$に対する勾配を計算せよ.なお,ある事例$x_i$に対して損失は次式で計算される.
$$
l_i = - \log [\mbox{事例}x_i\mbox{が}y_i\mbox{に分類される確率}]
$$
ただし,事例集合に対するクロスエントロピー損失は,その集合に含まれる各事例の損失の平均とする.
criterion = nn.CrossEntropyLoss()
y = model(train_v[:1])
t = train_t[:1]
loss = criterion(y, t)
model.zero_grad()
loss.backward()
print('損失 :', loss.item())
print('勾配')
print(model.fc.weight.grad)
損失 : 1.2622126340866089
勾配
tensor([[ 0.0229, 0.0139, -0.0196, ..., 0.0300, 0.0307, -0.0044],
[ 0.0219, 0.0133, -0.0187, ..., 0.0287, 0.0294, -0.0043],
[ 0.0201, 0.0122, -0.0172, ..., 0.0263, 0.0270, -0.0039],
[-0.0649, -0.0394, 0.0555, ..., -0.0850, -0.0870, 0.0126]])
zero_grad()
を忘れると延々と勾配が蓄積されてっておかしなことになってしまいます.
model.zero_grad()
model.fc.weight.grad
tensor([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]])
y = model(train_v[:4])
t = train_t[:4]
loss = criterion(y, t)
model.zero_grad()
loss.backward()
print('損失 :', loss.item())
print('勾配')
print(model.fc.weight.grad)
損失 : 1.3049677610397339
勾配
tensor([[ 0.0044, 0.0014, -0.0114, ..., 0.0090, 0.0150, -0.0018],
[-0.0008, 0.0036, -0.0066, ..., 0.0076, 0.0111, -0.0007],
[ 0.0038, 0.0008, -0.0114, ..., 0.0082, 0.0145, -0.0017],
[-0.0074, -0.0059, 0.0295, ..., -0.0248, -0.0406, 0.0042]])
73. 確率的勾配降下法による学習
確率的勾配降下法(SGD: Stochastic Gradient Descent)を用いて,行列$W$を学習せよ.なお,学習は適当な基準で終了させればよい(例えば「100エポックで終了」など).
ロジスティック回帰のときはデータ全部まとめて準ニュートン法していましたが,今回は確率的勾配降下なので,データセットをシャッフルしてすこしずつ取り出すようにします.
Datasetクラスはデータを持っている子です.__getitem__
でデータのインデックスの整数を受け取ったら,その番地のデータを返すようにします.
class Dataset(torch.utils.data.Dataset):
def __init__(self, x, t):
self.x = x
self.t = t
self.size = len(x)
def __len__(self):
return self.size
def __getitem__(self, index):
return {
'x':self.x[index],
't':self.t[index],
}
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
DatasetとSamplerをtorch.utils.data.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),
num_workers = num_workers,
)
訓練データと検証データでDataLoaderを用意します.
train_dataset = Dataset(train_v, train_t)
valid_dataset = Dataset(valid_v, valid_t)
test_dataset = Dataset(test_v, test_t)
loaders = (
gen_loader(train_dataset, 1, shuffle = True),
gen_loader(valid_dataset, 1),
)
損失を計算するTaskと最適化を回すTrainerを用意します.
import torch.optim as optim
class Task:
def __init__(self):
self.criterion = nn.CrossEntropyLoss()
def train_step(self, model, batch):
model.zero_grad()
loss = self.criterion(model(batch['x']), batch['t'])
loss.backward()
return loss.item()
def valid_step(self, model, batch):
with torch.no_grad():
loss = self.criterion(model(batch['x']), batch['t'])
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.max_iter = max_iter
self.optimizer = optimizer
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))
学習を実行します.
model = Perceptron(300, 4)
task = Task()
optimizer = optim.SGD(model.parameters(), 0.1)
trainer = Trainer(model, loaders, task, optimizer, 10)
trainer.train()
epoch 0, train_loss:0.40178, valid_loss:0.32123
epoch 1, train_loss:0.29685, valid_loss:0.30087
epoch 2, train_loss:0.27381, valid_loss:0.31221
epoch 3, train_loss:0.26309, valid_loss:0.29472
epoch 4, train_loss:0.25435, valid_loss:0.29926
epoch 5, train_loss:0.24851, valid_loss:0.30723
epoch 6, train_loss:0.24424, valid_loss:0.30154
epoch 7, train_loss:0.23987, valid_loss:0.30601
epoch 8, train_loss:0.23762, valid_loss:0.30835
epoch 9, train_loss:0.23378, valid_loss:0.31116
学習データのロスが下がっているのがわかります.
74. 正解率の計測
問題73で求めた行列を用いて学習データおよび評価データの事例を分類したとき,その正解率をそれぞれ求めよ.
import numpy as np
class Predictor:
def __init__(self, model, loader):
self.model = model
self.loader = loader
def infer(self, batch):
self.model.eval()
return self.model(batch['x']).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, Loader(train_dataset, 1))
pred = predictor.predict()
print('学習データでの正解率 :', accuracy(train_t, pred))
学習データでの正解率 : 0.9239049045301385
predictor = Predictor(model, gen_loader(train_dataset, 1))
pred = predictor.predict()
print('学習データでの正解率 :', accuracy(train_t, pred))
評価データでの正解率 : 0.8952095808383234
75. 損失と正解率のプロット
問 73のコードを改変し,各エポックのパラメータ更新が完了するたびに,訓練データでの損失,正解率,開発データでの損失,正解率をグラフにプロットし,学習の進捗状況を確認できるようにせよ.
import matplotlib.pyplot as plt
import japanize_matplotlib
from IPython.display import clear_output
jupyter notebook上でリアルタイムに図を更新していくコードです.
class RealTimePlot:
def __init__(self, legends):
self.legends = legends
self.fig, self.axs = plt.subplots(1, len(legends), figsize = (10, 5))
self.lst = [[[] for _ in xs] for xs in legends]
def __enter__(self):
return self
def update(self, *args):
for i, ys in enumerate(args):
for j, y in enumerate(ys):
self.lst[i][j].append(y)
clear_output(wait = True)
for i, ax in enumerate(self.axs):
ax.cla()
for ys in self.lst[i]:
ax.plot(ys)
ax.legend(self.legends[i])
display(self.fig)
def __exit__(self, *exc_info):
plt.close(self.fig)
class VerboseTrainer(Trainer):
def accuracy(self, true, pred):
return np.mean([t == p for t, p in zip(true, pred)])
def train(self, train_v, train_t, valid_v, valid_t):
train_loader = gen_loader(Dataset(train_v, train_t), 1)
valid_loader = gen_loader(Dataset(valid_v, valid_t), 1)
with RealTimePlot([['学習', '検証']] * 2) as rtp:
for epoch in range(self.max_iter):
self.model.to(self.device)
train_loss = self.train_epoch()
valid_loss = self.valid_epoch()
train_acc = self.accuracy(train_t, Predictor(self.model.cpu(), train_loader).predict())
valid_acc = self.accuracy(valid_t, Predictor(self.model.cpu(), valid_loader).predict())
rtp.update([train_loss, valid_loss], [train_acc, valid_acc])
Trainerの中で正解率を計算し,jupyter notebook上で消す・表示するを繰り返します.
model = Perceptron(300, 4)
task = Task()
optimizer = optim.SGD(model.parameters(), 0.1)
trainer = VerboseTrainer(model, loaders, task, optimizer, 10)
train_predictor = Predictor(model, gen_loader(test_dataset, 1))
valid_predictor = Predictor(model, gen_loader(test_dataset, 1))
trainer.train(train_v, train_t, valid_v, valid_t)
76. チェックポイント
問題75のコードを改変し,各エポックのパラメータ更新が完了するたびに,チェックポイント(学習途中のパラメータ(重み行列など)の値や最適化アルゴリズムの内部状態)をファイルに書き出せ.
import os
class LoggingTrainer(Trainer):
def save(self, epoch):
torch.save({'epoch' : epoch, 'optimizer': self.optimizer}, f'trainer_states{epoch}.pt')
torch.save(self.model.state_dict(), f'checkpoint{epoch}.pt')
def train(self):
for epoch in range(self.max_iter):
train_loss = self.train_epoch()
valid_loss = self.valid_epoch()
self.save(epoch)
print('epoch {}, train_loss:{:.5f}, valid_loss:{:.5f}'.format(epoch, train_loss, valid_loss))
モデルの保存はstate_dict()
で呼び出した値の辞書もtorch.save
します.
model = Perceptron(300, 4)
task = Task()
optimizer = optim.SGD(model.parameters(), 0.1)
trainer = LoggingTrainer(model, loaders, task, optimizer, 10)
trainer.train()
epoch 0, train_loss:0.40303, valid_loss:0.31214
epoch 1, train_loss:0.29639, valid_loss:0.29592
epoch 2, train_loss:0.27451, valid_loss:0.29903
epoch 3, train_loss:0.26194, valid_loss:0.29984
epoch 4, train_loss:0.25443, valid_loss:0.29787
epoch 5, train_loss:0.24855, valid_loss:0.30021
epoch 6, train_loss:0.24384, valid_loss:0.30676
epoch 7, train_loss:0.24003, valid_loss:0.30658
epoch 8, train_loss:0.23756, valid_loss:0.30995
epoch 9, train_loss:0.23390, valid_loss:0.30879
ls result/checkpoint*
result/checkpoint0.pt result/checkpoint4.pt result/checkpoint8.pt
result/checkpoint1.pt result/checkpoint5.pt result/checkpoint9.pt
result/checkpoint2.pt result/checkpoint6.pt
result/checkpoint3.pt result/checkpoint7.pt
ls result/trainer_states*
result/trainer_states0.pt result/trainer_states4.pt result/trainer_states8.pt
result/trainer_states1.pt result/trainer_states5.pt result/trainer_states9.pt
result/trainer_states2.pt result/trainer_states6.pt
result/trainer_states3.pt result/trainer_states7.pt
77. ミニバッチ化
問題76のコードを改変し,$B$事例ごとに損失・勾配を計算し,行列$W$の値を更新せよ(ミニバッチ化).$B$の値を$1, 2, 4, 8, \dots$と変化させながら,1エポックの学習に要する時間を比較せよ.
ミニバッチ化はDataset
ですでに実装してあるので,Sampler
の幅を変えるだけです.
from time import time
from contextlib import contextmanager
@contextmanager
def timer(description):
start = time()
yield
print(description, ': {:.3f} 秒'.format(time()-start))
B = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
task = Task()
for b in B:
model = Perceptron(300, 4)
loaders = (
gen_loader(train_dataset, b, shuffle = True),
gen_loader(valid_dataset, 1)
)
optimizer = optim.SGD(model.parameters(), 0.1 * b)
trainer = Trainer(model, loaders, task, optimizer, 3)
with timer(f'バッチサイズ {b}'):
trainer.train()
epoch 0, train_loss:0.40374, valid_loss:0.31423
epoch 1, train_loss:0.29578, valid_loss:0.29623
epoch 2, train_loss:0.27499, valid_loss:0.29798
バッチサイズ 1 : 9.657 秒
epoch 0, train_loss:0.39955, valid_loss:0.31440
epoch 1, train_loss:0.29591, valid_loss:0.29844
epoch 2, train_loss:0.27373, valid_loss:0.29537
バッチサイズ 2 : 5.325 秒
epoch 0, train_loss:0.40296, valid_loss:0.31603
epoch 1, train_loss:0.29613, valid_loss:0.31031
epoch 2, train_loss:0.27469, valid_loss:0.29736
バッチサイズ 4 : 3.083 秒
epoch 0, train_loss:0.40289, valid_loss:0.31443
epoch 1, train_loss:0.29676, valid_loss:0.30920
epoch 2, train_loss:0.27498, valid_loss:0.30645
バッチサイズ 8 : 1.982 秒
epoch 0, train_loss:0.40211, valid_loss:0.31350
epoch 1, train_loss:0.29613, valid_loss:0.30777
epoch 2, train_loss:0.27449, valid_loss:0.29903
バッチサイズ 16 : 1.420 秒
epoch 0, train_loss:0.40343, valid_loss:0.32170
epoch 1, train_loss:0.29695, valid_loss:0.30777
epoch 2, train_loss:0.27486, valid_loss:0.29472
バッチサイズ 32 : 1.202 秒
epoch 0, train_loss:0.40753, valid_loss:0.32378
epoch 1, train_loss:0.29829, valid_loss:0.29770
epoch 2, train_loss:0.27663, valid_loss:0.30175
バッチサイズ 64 : 1.060 秒
epoch 0, train_loss:0.41799, valid_loss:0.33559
epoch 1, train_loss:0.30109, valid_loss:0.30401
epoch 2, train_loss:0.27763, valid_loss:0.30351
バッチサイズ 128 : 0.906 秒
epoch 0, train_loss:0.56407, valid_loss:0.30955
epoch 1, train_loss:0.31099, valid_loss:0.32111
epoch 2, train_loss:0.28797, valid_loss:0.29928
バッチサイズ 256 : 2.234 秒
epoch 0, train_loss:1.19123, valid_loss:0.32315
epoch 1, train_loss:0.52350, valid_loss:0.39943
epoch 2, train_loss:0.42246, valid_loss:0.36194
バッチサイズ 512 : 1.323 秒
epoch 0, train_loss:3.77615, valid_loss:0.60957
epoch 1, train_loss:1.05934, valid_loss:0.89198
epoch 2, train_loss:0.80346, valid_loss:0.61814
バッチサイズ 1024 : 1.057 秒
78. GPU上での学習
問題77のコードを改変し,GPU上で学習を実行せよ.
torch.device('cuda')
上に乗せれば乗ります.
device = torch.device('cuda')
model = Perceptron(300, 4)
task = Task()
loaders = (
gen_loader(train_dataset, 128, shuffle = True),
gen_loader(valid_dataset, 1),
)
optimizer = optim.SGD(model.parameters(), 0.1 * 128)
trainer = Trainer(model, loaders, task, optimizer, 3, device=device)
with timer('時間'):
trainer.train()
epoch 0, train_loss:0.41928, valid_loss:0.31376
epoch 1, train_loss:0.30053, valid_loss:0.29341
epoch 2, train_loss:0.27865, valid_loss:0.29917
時間 : 1.433 秒
79. 多層ニューラルネットワーク
問題78のコードを改変し,バイアス項の導入や多層化など,ニューラルネットワークの形状を変更しながら,高性能なカテゴリ分類器を構築せよ.
多層にしてみましたが,それだけでは性能が大きく上がることはないみたいです.
class ModelNLP79(nn.Module):
def __init__(self, v_size, h_size, c_size):
super().__init__()
self.fc1 = nn.Linear(v_size, h_size)
self.act = nn.ReLU()
self.fc2 = nn.Linear(h_size, c_size)
self.dropout = nn.Dropout(0.2)
nn.init.kaiming_normal_(self.fc1.weight)
nn.init.kaiming_normal_(self.fc2.weight)
def forward(self, x):
x = self.fc1(x)
x = self.act(x)
x = self.dropout(x)
x = self.fc2(x)
return x
model = ModelNLP79(300, 128, 4)
task = Task()
loaders = (
gen_loader(train_dataset, 128, shuffle = True),
gen_loader(valid_dataset, 1)
)
optimizer = optim.SGD(model.parameters(), 0.1)
trainer = VerboseTrainer(model, loaders, task, optimizer, 30, device)
trainer.train(train_v, train_t, valid_v, valid_t)