はじめに
前回の続きで言語処理100本ノック解いていきたいと思います。
今回は第8章です。
ついにニューラルネットの実装にチャレンジしてみます。
第8章: ニューラルネット
深層学習フレームワークの使い方を学び,ニューラルネットワークに基づくカテゴリ分類を実装します.
第6章で取り組んだニュース記事のカテゴリ分類を題材として,ニューラルネットワークでカテゴリ分類モデルを実装する.なお,この章ではPyTorch, TensorFlow, Chainerなどの機械学習プラットフォームを活用せよ.
70. 単語ベクトルの和による特徴量
問題50で構築した学習データ,検証データ,評価データを行列・ベクトルに変換したい.例えば,学習データについて,すべての事例の特徴ベクトルを並べた行列Xと,正解ラベルを並べた行列(ベクトル)を作成したい.
ここで,は学習データの事例数であり,とはそれぞれ,番目の事例の特徴量ベクトルと正解ラベルを表す. なお,今回は「ビジネス」「科学技術」「エンターテイメント」「健康」の4カテゴリ分類である.で4未満の自然数(0を含む)を表すことにすれば,任意の事例の正解ラベルはで表現できる. 以降では,ラベルの種類数をで表す(今回の分類タスクではである).
番目の事例の特徴ベクトルは,次式で求める.
ここで,番目の事例は個の(記事見出しの)単語列から構成され,は単語に対応する単語ベクトル(次元数は)である.すなわち,番目の事例の記事見出しを,その見出しに含まれる単語のベクトルの平均で表現したものがである.今回は単語ベクトルとして,問題60でダウンロードしたものを用いればよい.300次元の単語ベクトルを用いたので,である.番目の事例のラベルは,次のように定義する.
- : 記事が「ビジネス」カテゴリの場合
- : 記事が「科学技術」カテゴリの場合
- : 記事が「エンターテイメント」カテゴリの場合
- : 記事が「健康」カテゴリの場合
なお,カテゴリ名とラベルの番号が一対一で対応付いていれば,上式の通りの対応付けでなくてもよい.
以上の仕様に基づき,以下の行列・ベクトルを作成し,ファイルに保存せよ.
- 学習データの特徴量行列:
- 学習データのラベルベクトル:
- 検証データの特徴量行列:
- 検証データのラベルベクトル:
- 評価データの特徴量行列:
- 評価データのラベルベクトル:
なお,はそれぞれ,学習データの事例数,検証データの事例数,評価データの事例数である.
まずは第6章でもやったようにデータを読み込んで、学習、検証、評価用に分けます
# 単語ベクトルのロード from gensim.models import KeyedVectors file = './input/section7/GoogleNews-vectors-negative300.bin.gz' model = KeyedVectors.load_word2vec_format(file, binary=True) # データのロード import pandas as pd import re import numpy as np # ファイル読み込み file = './input/section6/newsCorpora.csv' data = pd.read_csv(file, encoding='utf-8', header=None, sep='\t', names=['ID', 'TITLE', 'URL', 'PUBLISHER', 'CATEGORY', 'STORY', 'HOSTNAME', 'TIMESTAMP']) data = data.replace('"', "'") # 特定のpublisherのみ抽出 publishers = ['Reuters', 'Huffington Post', 'Businessweek', 'Contactmusic.com', 'Daily Mail'] data = data.loc[data['PUBLISHER'].isin(publishers), ['TITLE', 'CATEGORY']].reset_index(drop=True) # 学習用、検証用、評価用に分割する from sklearn.model_selection import train_test_split train, valid_test = train_test_split(data, test_size=0.2, shuffle=True, random_state=64, stratify=data['CATEGORY']) valid, test = train_test_split(valid_test, test_size=0.5, shuffle=True, random_state=64, stratify=valid_test['CATEGORY']) train = train.reset_index(drop=True) valid = valid.reset_index(drop=True) test = test.reset_index(drop=True) # データ数の確認 print('学習データ') print(train['CATEGORY'].value_counts()) print('検証データ') print(valid['CATEGORY'].value_counts()) print('評価データ') print(test['CATEGORY'].value_counts()) import re from nltk import stem # データの結合 df = pd.concat([train, valid, test], axis=0).reset_index(drop=True) # 前処理 def preprocessing(text): text_clean = re.sub(r'[\"\'.,:;\(\)#\|\*\+\!\?#$%&/\]\[\{\}]', '', text) text_clean = re.sub('[0-9]+', '0', text_clean) text_clean = re.sub('\s-\s', ' ', text_clean) return text_clean df['TITLE'] = df['TITLE'].apply(preprocessing)
以下出力です。
学習データ b 4502 e 4223 t 1219 m 728 Name: CATEGORY, dtype: int64 検証データ b 562 e 528 t 153 m 91 Name: CATEGORY, dtype: int64 評価データ b 563 e 528 t 152 m 91 Name: CATEGORY, dtype: int64
続いて、平均単語ベクトルを計算し、学習にはpytorchを使用するので特徴ベクトルをテンソル化しておきます
import numpy as np # 平均単語ベクトルの取得 def w2v(text): words = text.rstrip().split() vec = [model[word] for word in words if word in model] return np.array(sum(vec) / len(vec)) vecs = np.array([]) for text in df['TITLE']: if len(vecs) == 0: vecs = w2v(text) else: vecs = np.vstack([vecs, w2v(text)]) # 特徴ベクトルのテンソル化 import torch # 乱数のシードを設定 torch.manual_seed(1234) np.random.seed(1234) X_train = torch.from_numpy(vecs[:len(train), :]) X_valid = torch.from_numpy(vecs[len(train):len(train)+ len(valid), :]) X_test = torch.from_numpy(vecs[len(train)+ len(valid):, :]) print(X_train.size()) print(X_train)
以下出力です。
torch.Size([10672, 300]) tensor([[ 0.0368, 0.0300, -0.0738, ..., -0.1523, 0.0419, -0.0774], [ 0.0002, -0.0056, -0.0824, ..., -0.0544, 0.0776, -0.0214], [ 0.0266, -0.0166, -0.0877, ..., -0.0522, 0.0517, 0.0093], ..., [-0.0291, 0.0529, -0.1453, ..., 0.0494, 0.1548, -0.0910], [-0.0269, 0.1204, -0.0289, ..., -0.0062, 0.0739, -0.0327], [ 0.0361, 0.1236, 0.0260, ..., -0.0099, -0.0193, 0.0262]])
最後に目的変数であるカテゴリもテンソル化して保存します
# ターゲットのテンソル化 category_dict = {'b': 0, 't': 1, 'e':2, 'm':3} Y_train = torch.from_numpy(train['CATEGORY'].map(category_dict).values) Y_valid = torch.from_numpy(valid['CATEGORY'].map(category_dict).values) Y_test = torch.from_numpy(test['CATEGORY'].map(category_dict).values) # 保存 torch.save(X_train, './input/section8/X_train.pt') torch.save(X_valid, './input/section8/X_valid.pt') torch.save(X_test, './input/section8/X_test.pt') torch.save(Y_train, './input/section8/y_train.pt') torch.save(Y_valid, './input/section8/y_valid.pt') torch.save(Y_test, './input/section8/y_test.pt')
71. 単層ニューラルネットワークによる予測
問題70で保存した行列を読み込み,学習データについて以下の計算を実行せよ.
ただし,はソフトマックス関数,は特徴ベクトルを縦に並べた行列である.
行列は単層ニューラルネットワークの重み行列で,ここではランダムな値で初期化すればよい(問題73以降で学習して求める).なお,は未学習の行列で事例を分類したときに,各カテゴリに属する確率を表すベクトルである. 同様に,は,学習データの事例について,各カテゴリに属する確率を行列として表現している.
まずは単層のニューラルネットワークをSLNetという名前で作成しておきます。
# モデルの構築 from torch import nn class SLNet(nn.Module): def __init__(self, input_size, output_size): super().__init__() self.fc = nn.Linear(input_size, output_size) def forward(self, x): logits = self.fc(x) return logits model = SLNet(300, 4) print(model)
出力は以下。
SLNet( (fc): Linear(in_features=300, out_features=4, bias=True) )
事例1についてy_hatを計算します。softmaxはpytorchにモジュールがあるので使用します。
サンプル1個での予測の場合は、logitsが1次元のテンソルになってしまうので、dimには-1を指定します
logits = model(X_train[0]) y_hat_1 = nn.Softmax(dim=-1)(logits) print(logits) print(y_hat_1)
結果は以下
tensor([-0.0176, 0.0697, 0.0920, 0.0596], grad_fn=<AddBackward0>) tensor([0.2332, 0.2545, 0.2603, 0.2520], grad_fn=<SoftmaxBackward0>)
事例1~4についてY_hatを計算します。
logits = model(X_train[:4]) Y_hat = nn.Softmax(dim=1)(logits) print(logits) print(Y_hat)
結果は以下
tensor([[-0.0176, 0.0697, 0.0920, 0.0596], [-0.0210, 0.0704, 0.0814, 0.0636], [ 0.0360, 0.1010, 0.0704, 0.0456], [ 0.0362, 0.1266, 0.0222, 0.1343]], grad_fn=<AddmmBackward0>) tensor([[0.2332, 0.2545, 0.2603, 0.2520], [0.2330, 0.2553, 0.2581, 0.2536], [0.2432, 0.2595, 0.2517, 0.2455], [0.2390, 0.2616, 0.2357, 0.2637]], grad_fn=<SoftmaxBackward0>)
72. 損失と勾配の計算
学習データの事と事例集合に対して,クロスエントロピー損失と,行列に対する勾配を計算せよ.なお,ある事例に対して損失は次式で計算される.
[事例がに分類される確率]
ただし,事例集合に対するクロスエントロピー損失は,その集合に含まれる各事例の損失の平均とする.
クロスエントロピーロスもモジュールがあるので使用します
ソフトマックスをモジュール内で行うので、引数には予測確率と目的変数を入れます。
# x_1のロスを求める criterion = nn.CrossEntropyLoss() logits = model(X_train[0]) loss = criterion(logits, Y_train[0]) print("損失: ", loss.item()) model.zero_grad() loss.backward() print("勾配: ") print(model.fc.weight.grad)
出力は以下。
損失: 1.3460214138031006 勾配: tensor([[ 0.0086, 0.0070, -0.0172, ..., -0.0355, 0.0098, -0.0181], [ 0.0094, 0.0076, -0.0188, ..., -0.0388, 0.0107, -0.0197], [-0.0273, -0.0222, 0.0546, ..., 0.1127, -0.0310, 0.0573], [ 0.0093, 0.0076, -0.0186, ..., -0.0384, 0.0106, -0.0195]])
# x_1~x_4のロスを求める logits = model(X_train[:4]) loss = criterion(logits, Y_train[:4]) print("損失: ", loss.item()) model.zero_grad() loss.backward() print("勾配: ") print(model.fc.weight.grad)
出力は以下。
損失: 1.3807432651519775 勾配: tensor([[ 5.0444e-03, 3.9170e-03, 7.1611e-03, ..., -2.0513e-03, -4.2177e-03, 5.3509e-04], [ 3.9538e-05, -6.0439e-03, -1.8717e-02, ..., -1.5257e-02, -4.6948e-03, -6.8699e-03], [-1.0491e-02, -7.2170e-04, 2.5712e-02, ..., 3.4016e-02, -7.4130e-03, 1.1578e-02], [ 5.4069e-03, 2.8486e-03, -1.4156e-02, ..., -1.6707e-02, 1.6325e-02, -5.2435e-03]])
73. 確率的勾配降下法による学習
確率的勾配降下法(SGD: Stochastic Gradient Descent)を用いて,行列を学習せよ.なお,学習は適当な基準で終了させればよい(例えば「100エポックで終了」など).
まずは学習、検証等に使うデータセットを作成します。
# データセットを作成する import torch.utils.data as data class NewsDataset(data.Dataset): """ newsのDatasetクラス Attributes ---------------------------- X : テンソル 単語ベクトルの平均をまとめたテンソル y : テンソル カテゴリをラベル化したテンソル phase : 'train' or 'val' 学習か訓練かを設定する """ def __init__(self, X, y, phase='train'): self.X = X self.y = y self.phase = phase def __len__(self): """全データサイズを返す""" return len(self.y) def __getitem__(self, idx): """idxに対応するテンソル形式のデータとラベルを取得""" return self.X[idx], self.y[idx] train_dataset = NewsDataset(X_train, Y_train, phase='train') valid_dataset = NewsDataset(X_valid, Y_valid, phase='val') test_dataset = NewsDataset(X_test, Y_test, phase='val') # 動作確認 idx = 0 print(train_dataset.__getitem__(idx)[0].size()) print(train_dataset.__getitem__(idx)[1]) print(valid_dataset.__getitem__(idx)[0].size()) print(valid_dataset.__getitem__(idx)[1]) print(test_dataset.__getitem__(idx)[0].size()) print(test_dataset.__getitem__(idx)[1])
出力は以下です。
torch.Size([300]) tensor(2) torch.Size([300]) tensor(3) torch.Size([300]) tensor(2)
次に学習、推論時にデータセットからデータを読むためのデータローダを作成します
後々ミニバッチ化しますが、今回はしないので、バッチサイズは1としておきます。
# DataLoaderを作成 batch_size = 1 train_dataloader = data.DataLoader( train_dataset, batch_size=batch_size, shuffle=True) valid_dataloader = data.DataLoader( valid_dataset, batch_size=len(valid_dataset), shuffle=False) test_dataloader = data.DataLoader( test_dataset, batch_size=len(test_dataset), shuffle=False) dataloaders_dict = {'train': train_dataloader, 'val': valid_dataloader, 'test': test_dataloader, } # 動作確認 batch_iter = iter(dataloaders_dict['train']) inputs, labels = next(batch_iter) print(inputs.size()) print(labels)
出力は以下です。
torch.Size([1, 300]) tensor([2])
いよいよ学習です。
10epochで学習することにします。
from tqdm import tqdm # 学習 # モデルの定義 net = SLNet(300, 4) net.train() # 損失関数の定義 criterion = nn.CrossEntropyLoss() # 最適化手法の定義 optimizer = torch.optim.SGD(net.parameters(), lr=0.01, momentum=0.9) # 学習用の関数を定義 def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs): # epochのループ for epoch in range(num_epochs): print('Epoch {} / {}'.format(epoch + 1, num_epochs)) print('--------------------------------------------') # epochごとの学習と検証のループ for phase in ['train', 'val']: if phase == 'train': net.train() # 訓練モード else: net.eval() # 検証モード epoch_loss = 0.0 # epochの損失和 epoch_corrects = 0 # epochの正解数 # データローダーからミニバッチを取り出すループ for inputs, labels in tqdm(dataloaders_dict[phase]): optimizer.zero_grad() # optimizerを初期化 # 順伝播計算(forward) with torch.set_grad_enabled(phase == 'train'): outputs = net(inputs) loss = criterion(outputs, labels) # 損失を計算 _, preds = torch.max(outputs, 1) # ラベルを予想 # 訓練時は逆伝播 if phase == 'train': loss.backward() optimizer.step() # イテレーション結果の計算 # lossの合計を更新 epoch_loss += loss.item() * inputs.size(0) # 正解数の合計を更新 epoch_corrects += torch.sum(preds == labels.data) # epochごとのlossと正解率の表示 epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset) epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset) print('{} Loss: {:.4f}, Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc)) # 学習を実行する num_epochs = 10 train_model(net, dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)
結果は以下です。
Epoch 1 / 10 -------------------------------------------- 100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3014.67it/s] train Loss: 0.4169, Acc: 0.8569 100%|████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 71.43it/s] val Loss: 0.3204, Acc: 0.8936 Epoch 2 / 10 -------------------------------------------- 100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3071.06it/s] train Loss: 0.3108, Acc: 0.8925 100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.81it/s] val Loss: 0.2800, Acc: 0.9055 Epoch 3 / 10 -------------------------------------------- 100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3059.61it/s] train Loss: 0.2897, Acc: 0.8996 100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 166.12it/s] val Loss: 0.2809, Acc: 0.9063 Epoch 4 / 10 -------------------------------------------- 100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3058.73it/s] train Loss: 0.2794, Acc: 0.9027 100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.99it/s] val Loss: 0.2728, Acc: 0.9123 Epoch 5 / 10 -------------------------------------------- 100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3053.36it/s] train Loss: 0.2718, Acc: 0.9055 100%|████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 83.34it/s] val Loss: 0.2681, Acc: 0.9100 Epoch 6 / 10 -------------------------------------------- 100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3017.23it/s] train Loss: 0.2642, Acc: 0.9096 100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.39it/s] val Loss: 0.2768, Acc: 0.9078 Epoch 7 / 10 -------------------------------------------- 100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3044.77it/s] train Loss: 0.2600, Acc: 0.9117 100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.50it/s] val Loss: 0.2788, Acc: 0.9055 Epoch 8 / 10 -------------------------------------------- 100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3027.49it/s] train Loss: 0.2576, Acc: 0.9116 100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.93it/s] val Loss: 0.2700, Acc: 0.9085 Epoch 9 / 10 -------------------------------------------- 100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3023.21it/s] train Loss: 0.2551, Acc: 0.9125 100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.87it/s] val Loss: 0.2748, Acc: 0.9160 Epoch 10 / 10 -------------------------------------------- 100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3016.37it/s] train Loss: 0.2530, Acc: 0.9136 100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.24it/s] val Loss: 0.2691, Acc: 0.9115
74. 正解率の計測
問題73で求めた行列を用いて学習データおよび評価データの事例を分類したとき,その正解率をそれぞれ求めよ.
推論、および正解率を求める関数を作成しておきます。
def calc_acc(net, dataloader): net.eval() corrects = 0 with torch.no_grad(): for inputs, labels in dataloader: outputs = net(inputs) _, preds = torch.max(outputs, 1) # ラベルを予想 corrects += torch.sum(preds == labels.data) return corrects / len(dataloader.dataset) 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))
結果は以下。
90%近く当てれるとは、すごいですね。
学習データの正解率: 0.9198 検証データの正解率: 0.9115 テストデータの正解率: 0.8958
75. 損失と正解率のプロット
問題73のコードを改変し,各エポックのパラメータ更新が完了するたびに,訓練データでの損失,正解率,検証データでの損失,正解率をグラフにプロットし,学習の進捗状況を確認できるようにせよ.
以下の問題76も一緒にやってしまいます
76. チェックポイント
問題75のコードを改変し,各エポックのパラメータ更新が完了するたびに,チェックポイント(学習途中のパラメータ(重み行列など)の値や最適化アルゴリズムの内部状態)をファイルに書き出せ.
学習用の関数を変更して、エポックごとのロス、正解率をリストに格納していきます。
ついでに各エポックの学習が終わったあとに、重み等をチェックポイントファイルとして保存します。
# 学習用の関数を定義 def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs): train_loss = [] train_acc = [] valid_loss = [] valid_acc = [] # epochのループ for epoch in range(num_epochs): print('Epoch {} / {}'.format(epoch + 1, num_epochs)) print('--------------------------------------------') # epochごとの学習と検証のループ for phase in ['train', 'val']: if phase == 'train': net.train() # 訓練モード else: net.eval() # 検証モード epoch_loss = 0.0 # epochの損失和 epoch_corrects = 0 # epochの正解数 # データローダーからミニバッチを取り出すループ for inputs, labels in tqdm(dataloaders_dict[phase]): optimizer.zero_grad() # optimizerを初期化 # 順伝播計算(forward) with torch.set_grad_enabled(phase == 'train'): outputs = net(inputs) loss = criterion(outputs, labels) # 損失を計算 _, preds = torch.max(outputs, 1) # ラベルを予想 # 訓練時は逆伝播 if phase == 'train': loss.backward() optimizer.step() # イテレーション結果の計算 # lossの合計を更新 epoch_loss += loss.item() * inputs.size(0) # 正解数の合計を更新 epoch_corrects += torch.sum(preds == labels.data) # epochごとのlossと正解率の表示 epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset) epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset) if phase == 'train': train_loss.append(epoch_loss) train_acc.append(epoch_acc) else: valid_loss.append(epoch_loss) valid_acc.append(epoch_acc) print('{} Loss: {:.4f}, Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc)) # チェックポイントの保存 torch.save({'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict() }, './output/section8/checkpoint{}.pt'.format(epoch + 1)) return train_loss, train_acc, valid_loss, valid_acc # 学習を実行する # モデルの定義 net = SLNet(300, 4) net.train() # 損失関数の定義 criterion = nn.CrossEntropyLoss() # 最適化手法の定義 optimizer = torch.optim.SGD(net.parameters(), lr=0.01, momentum=0.9) num_epochs = 10 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('fig74.png')
結果は以下の通り。
4epochほどでvalid lossが下がらなくなってきてますね。
おわりに
8章前半終わりです。
後半はGPUを使ってみたり、多層にしてみたりします。
pytorch初めて使いましたが、以下の本が入門としても大変参考になりました。