【言語処理100本ノック 2020】第8章: ニューラルネット【Python】
自然言語処理の問題集として有名な言語処理100本ノックの2020年版の解答例です。 この記事では、以下の第1章から第10章のうち、「第8章: ニューラルネット」を解いてみた結果をまとめています。
- 第1章: 準備運動
- 第2章: UNIXコマンド
- 第3章: 正規表現
- 第4章: 形態素解析
- 第5章: 係り受け解析
- 第6章: 機械学習
- 第7章: 単語ベクトル
- 第8章: ニューラルネット
- 第9章: RNNとCNN
- 第10章: 機械翻訳
環境設定
コードの実行はGoogle Colaboratoryで行います。 以降の解答の実行結果をすべて含むノートブックは、以下のリンクから直接参照することができます。
第8章: ニューラルネット
第6章で取り組んだニュース記事のカテゴリ分類を題材として,ニューラルネットワークでカテゴリ分類モデルを実装する.なお,この章ではPyTorch, TensorFlow, Chainerなどの機械学習プラットフォームを活用せよ.
70. 単語ベクトルの和による特徴量
問題50で構築した学習データ,検証データ,評価データを行列・ベクトルに変換したい.例えば,学習データについて,すべての事例の特徴ベクトルを並べた行列と正解ラベルを並べた行列(ベクトル)を作成したい.
ここで,は学習データの事例数であり,とはそれぞれ,番目の事例の特徴量ベクトルと正解ラベルを表す. なお,今回は「ビジネス」「科学技術」「エンターテイメント」「健康」の4カテゴリ分類である.で未満の自然数(を含む)を表すことにすれば,任意の事例の正解ラベルはで表現できる. 以降では,ラベルの種類数をで表す(今回の分類タスクではである).
番目の事例の特徴ベクトルは,次式で求める.
ここで,番目の事例は個の(記事見出しの)単語列から構成され,は単語に対応する単語ベクトル(次元数は)である.すなわち, 番目の事例の記事見出しを,その見出しに含まれる単語のベクトルの平均で表現したものがである.今回は単語ベクトルとして,問題60でダウンロードしたものを用いればよい.次元の単語ベクトルを用いたので,である. 番目の事例のラベルは,次のように定義する.
なお,カテゴリ名とラベルの番号が一対一で対応付いていれば,上式の通りの対応付けでなくてもよい.
以上の仕様に基づき,以下の行列・ベクトルを作成し,ファイルに保存せよ.
- 学習データの特徴量行列:
- 学習データのラベルベクトル:
- 検証データの特徴量行列:
- 検証データのラベルベクトル:
- 評価データの特徴量行列:
- 評価データのラベルベクトル:
なお,はそれぞれ,学習データの事例数,検証データの事例数,評価データの事例数である.
まずは、指定のデータをダウンロード後、データフレームとして読込みます。そして、学習データ、検証データ、評価データに分割し、保存します。
ここまでは、第6章の問題50とまったく同じ処理のため、そちらで作成したデータを読み込んでも問題ありません。
# データのダウンロード !wget https://archive.ics.uci.edu/ml/machine-learning-databases/00359/NewsAggregatorDataset.zip !unzip NewsAggregatorDataset.zip
# 読込時のエラー回避のためダブルクォーテーションをシングルクォーテーションに置換 !sed -e 's/"/'\''/g' ./newsCorpora.csv > ./newsCorpora_re.csv
import pandas as pd from sklearn.model_selection import train_test_split # データの読込 df = pd.read_csv('./newsCorpora_re.csv', header=None, sep='\t', names=['ID', 'TITLE', 'URL', 'PUBLISHER', 'CATEGORY', 'STORY', 'HOSTNAME', 'TIMESTAMP']) # データの抽出 df = df.loc[df['PUBLISHER'].isin(['Reuters', 'Huffington Post', 'Businessweek', 'Contactmusic.com', 'Daily Mail']), ['TITLE', 'CATEGORY']] # データの分割 train, valid_test = train_test_split(df, test_size=0.2, shuffle=True, random_state=123, stratify=df['CATEGORY']) valid, test = train_test_split(valid_test, test_size=0.5, shuffle=True, random_state=123, stratify=valid_test['CATEGORY']) # 事例数の確認 print('【学習データ】') print(train['CATEGORY'].value_counts()) print('【検証データ】') print(valid['CATEGORY'].value_counts()) print('【評価データ】') print(test['CATEGORY'].value_counts())
--- 出力 --- 【学習データ】 b 4501 e 4235 t 1220 m 728 Name: CATEGORY, dtype: int64 【検証データ】 b 563 e 529 t 153 m 91 Name: CATEGORY, dtype: int64 【評価データ】 b 563 e 530 t 152 m 91 Name: CATEGORY, dtype: int64
続いて、第7章の問題60でも利用した学習済み単語ベクトルをダウンロードし、ロードします。
import gdown from gensim.models import KeyedVectors # 学習済み単語ベクトルのダウンロード url = "https://drive.google.com/uc?id=0B7XkCwpI5KDYNlNUTTlSS21pQmM" output = 'GoogleNews-vectors-negative300.bin.gz' gdown.download(url, output, quiet=True) # ダウンロードファイルのロード model = KeyedVectors.load_word2vec_format('GoogleNews-vectors-negative300.bin.gz', binary=True)
最後に、特徴ベクトルとラベルベクトルを作成し、保存します。 なお、このあとPyTorchによるニューラルネットのインプットとして利用するため、Tensor型に変換しています。
import string import torch def transform_w2v(text): table = str.maketrans(string.punctuation, ' '*len(string.punctuation)) words = text.translate(table).split() # 記号をスペースに置換後、スペースで分割してリスト化 vec = [model[word] for word in words if word in model] # 1語ずつベクトル化 return torch.tensor(sum(vec) / len(vec)) # 平均ベクトルをTensor型に変換して出力
# 特徴ベクトルの作成 X_train = torch.stack([transform_w2v(text) for text in train['TITLE']]) X_valid = torch.stack([transform_w2v(text) for text in valid['TITLE']]) X_test = torch.stack([transform_w2v(text) for text in test['TITLE']]) print(X_train.size()) print(X_train)
--- 出力 --- torch.Size([10684, 300]) tensor([[ 0.0837, 0.0056, 0.0068, ..., 0.0751, 0.0433, -0.0868], [ 0.0272, 0.0266, -0.0947, ..., -0.1046, -0.0489, -0.0092], [ 0.0577, -0.0159, -0.0780, ..., -0.0421, 0.1229, 0.0876], ..., [ 0.0392, -0.0052, 0.0686, ..., -0.0175, 0.0061, -0.0224], [ 0.0798, 0.1017, 0.1066, ..., -0.0752, 0.0623, 0.1138], [ 0.1664, 0.0451, 0.0508, ..., -0.0531, -0.0183, -0.0039]])
# ラベルベクトルの作成 category_dict = {'b': 0, 't': 1, 'e':2, 'm':3} y_train = torch.tensor(train['CATEGORY'].map(lambda x: category_dict[x]).values) y_valid = torch.tensor(valid['CATEGORY'].map(lambda x: category_dict[x]).values) y_test = torch.tensor(test['CATEGORY'].map(lambda x: category_dict[x]).values) print(y_train.size()) print(y_train)
--- 出力 --- torch.Size([10684]) tensor([0, 1, 3, ..., 0, 3, 2])
# 保存 torch.save(X_train, 'X_train.pt') torch.save(X_valid, 'X_valid.pt') torch.save(X_test, 'X_test.pt') torch.save(y_train, 'y_train.pt') torch.save(y_valid, 'y_valid.pt') torch.save(y_test, 'y_test.pt')
71. 単層ニューラルネットワークによる予測
問題70で保存した行列を読み込み,学習データについて以下の計算を実行せよ.
ただし,はソフトマックス関数,は特徴ベクトルを縦に並べた行列である.
行列は単層ニューラルネットワークの重み行列で,ここではランダムな値で初期化すればよい(問題73以降で学習して求める).なお,は未学習の行列で事例を分類したときに,各カテゴリに属する確率を表すベクトルである. 同様に,は,学習データの事例について,各カテゴリに属する確率を行列として表現している.
はじめに、SLPNet
という単層ニューラルネットワークを定義します。__init__
でネットワークを構成するレイヤーを定義し、forward
メソッドでインプットデータが順伝播時に通るレイヤーを順に配置していきます。
from torch import nn class SLPNet(nn.Module): def __init__(self, input_size, output_size): super().__init__() self.fc = nn.Linear(input_size, output_size, bias=False) nn.init.normal_(self.fc.weight, 0.0, 1.0) # 正規乱数で重みを初期化 def forward(self, x): x = self.fc(x) return x
続いて、定義したモデルを初期化し、指示された計算を実行します。
model = SLPNet(300, 4) # 単層ニューラルネットワークの初期化 y_hat_1 = torch.softmax(model(X_train[:1]), dim=-1) print(y_hat_1)
--- 出力 --- tensor([[0.4273, 0.0958, 0.2492, 0.2277]], grad_fn=<SoftmaxBackward>)
Y_hat = torch.softmax(model.forward(X_train[:4]), dim=-1) print(Y_hat)
--- 出力 --- tensor([[0.4273, 0.0958, 0.2492, 0.2277], [0.2445, 0.2431, 0.0197, 0.4927], [0.7853, 0.1132, 0.0291, 0.0724], [0.5279, 0.2319, 0.0873, 0.1529]], grad_fn=<SoftmaxBackward>)
72. 損失と勾配の計算
学習データの事例と事例集合に対して,クロスエントロピー損失と,行列に対する勾配を計算せよ.なお,ある事例に対して損失は次式で計算される.
ただし,事例集合に対するクロスエントロピー損失は,その集合に含まれる各事例の損失の平均とする.
ここでは、nn
パッケージのCrossEntropyLoss
を利用します。
モデルの出力ベクトルとラベルベクトルを入力することで、上式の平均損失を計算することができます。
criterion = nn.CrossEntropyLoss()
l_1 = criterion(model(X_train[:1]), y_train[:1]) # 入力ベクトルはsoftmax前の値 model.zero_grad() # 勾配をゼロで初期化 l_1.backward() # 勾配を計算 print(f'損失: {l_1:.4f}') print(f'勾配:\n{model.fc.weight.grad}')
--- 出力 --- 損失: 2.9706 勾配: tensor([[-0.0794, -0.0053, -0.0065, ..., -0.0713, -0.0411, 0.0823], [ 0.0022, 0.0001, 0.0002, ..., 0.0020, 0.0011, -0.0023], [ 0.0611, 0.0041, 0.0050, ..., 0.0549, 0.0316, -0.0634], [ 0.0161, 0.0011, 0.0013, ..., 0.0144, 0.0083, -0.0167]])
l = criterion(model(X_train[:4]), y_train[:4]) model.zero_grad() l.backward() print(f'損失: {l:.4f}') print(f'勾配:\n{model.fc.weight.grad}')
損失: 3.0799 勾配: tensor([[-0.0207, 0.0079, -0.0090, ..., -0.0350, -0.0003, 0.0232], [-0.0055, -0.0063, 0.0225, ..., 0.0252, 0.0166, 0.0039], [ 0.0325, -0.0089, -0.0215, ..., 0.0084, 0.0122, -0.0030], [-0.0063, 0.0072, 0.0081, ..., 0.0014, -0.0285, -0.0241]])
73. 確率的勾配降下法による学習
確率的勾配降下法(SGD: Stochastic Gradient Descent)を用いて,行列を学習せよ.なお,学習は適当な基準で終了させればよい(例えば「100エポックで終了」など).
学習に当たり、Dataset
とDataloader
を準備します。
Dataset
は特徴ベクトルとラベルベクトルを合わせて保持することができる型で、以下のクラスを用いてもとのTensorを変換します。
from torch.utils.data import Dataset class NewsDataset(Dataset): def __init__(self, X, y): # datasetの構成要素を指定 self.X = X self.y = y def __len__(self): # len(dataset)で返す値を指定 return len(self.y) def __getitem__(self, idx): # dataset[idx]で返す値を指定 return [self.X[idx], self.y[idx]]
変換後、DataLoader
を作成します。Dataloader
はDataset
を入力とし、指定したサイズ(batch_size
)にまとめたデータを順に取り出すことができます。ここではbatch_size=1
としているので、1つずつデータを取り出すDataloader
を作成することを意味します。
なお、Dataloader
はfor
文で順に取り出すか、またはnext(iter(Dataloader))
で次のかたまりを呼び出すことが可能です。
from torch.utils.data import DataLoader # Datasetの作成 Dataset_train = NewsDataset(X_train, y_train) dataset_valid = NewsDataset(X_valid, y_valid) dataset_test = NewsDataset(X_test, y_test) # Dataloaderの作成 dataloader_train = DataLoader(dataset_train, batch_size=1, shuffle=True) dataloader_valid = DataLoader(dataset_valid, batch_size=len(dataset_valid), shuffle=False) dataloader_test = DataLoader(dataset_test, batch_size=len(dataset_test), shuffle=False)
データの準備ができたので、行列を学習します。 モデルの定義、損失関数の定義は前問と同様です。今回は計算した勾配から重みも更新するため、オプティマイザも定義します。ここでは指示に従いSGDをセットしています。
部品が揃ったところで、エポック数を10として学習を実行します。
# モデルの定義 model = SLPNet(300, 4) # 損失関数の定義 criterion = nn.CrossEntropyLoss() # オプティマイザの定義 optimizer = torch.optim.SGD(model.parameters(), lr=1e-1) # 学習 num_epochs = 10 for epoch in range(num_epochs): # 訓練モードに設定 model.train() loss_train = 0.0 for i, (inputs, labels) in enumerate(dataloader_train): # 勾配をゼロで初期化 optimizer.zero_grad() # 順伝播 + 誤差逆伝播 + 重み更新 outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # 損失を記録 loss_train += loss.item() # バッチ単位の平均損失計算 loss_train = loss_train / i # 検証データの損失計算 model.eval() with torch.no_grad(): inputs, labels = next(iter(dataloader_valid)) outputs = model(inputs) loss_valid = criterion(outputs, labels) # ログを出力 print(f'epoch: {epoch + 1}, loss_train: {loss_train:.4f}, loss_valid: {loss_valid:.4f}')
--- 出力 --- epoch: 1, loss_train: 0.4745, loss_valid: 0.3637 epoch: 2, loss_train: 0.3173, loss_valid: 0.3306 epoch: 3, loss_train: 0.2884, loss_valid: 0.3208 epoch: 4, loss_train: 0.2716, loss_valid: 0.3150 epoch: 5, loss_train: 0.2615, loss_valid: 0.3141 epoch: 6, loss_train: 0.2519, loss_valid: 0.3092 epoch: 7, loss_train: 0.2474, loss_valid: 0.3114 epoch: 8, loss_train: 0.2431, loss_valid: 0.3072 epoch: 9, loss_train: 0.2393, loss_valid: 0.3096 epoch: 10, loss_train: 0.2359, loss_valid: 0.3219
エポックが進むについて、徐々に学習データの損失が下がっていることが分かります。
74. 正解率の計測
問題73で求めた行列を用いて学習データおよび評価データの事例を分類したとき,その正解率をそれぞれ求めよ.
学習したモデルとDataloader
を入力として、正解率を算出する関数を定義します。
def calculate_accuracy(model, loader): model.eval() total = 0 correct = 0 with torch.no_grad(): for inputs, labels in loader: outputs = model(inputs) pred = torch.argmax(outputs, dim=-1) total += len(inputs) correct += (pred == labels).sum().item() return correct / total
acc_train = calculate_accuracy(model, dataloader_train) acc_test = calculate_accuracy(model, dataloader_test) print(f'正解率(学習データ):{acc_train:.3f}') print(f'正解率(評価データ):{acc_test:.3f}')
--- 出力 --- 正解率(学習データ):0.920 正解率(評価データ):0.891
75. 損失と正解率のプロット
問題73のコードを改変し,各エポックのパラメータ更新が完了するたびに,訓練データでの損失,正解率,検証データでの損失,正解率をグラフにプロットし,学習の進捗状況を確認できるようにせよ.
前問の関数を損失も計算できるように改変し、エポック毎に適用することで損失と正解率を記録します。
def calculate_loss_and_accuracy(model, criterion, loader): model.eval() loss = 0.0 total = 0 correct = 0 with torch.no_grad(): for inputs, labels in loader: outputs = model(inputs) loss += criterion(outputs, labels).item() pred = torch.argmax(outputs, dim=-1) total += len(inputs) correct += (pred == labels).sum().item() return loss / len(loader), correct / total
# モデルの定義 model = SLPNet(300, 4) # 損失関数の定義 criterion = nn.CrossEntropyLoss() # オプティマイザの定義 optimizer = torch.optim.SGD(model.parameters(), lr=1e-1) # 学習 num_epochs = 30 log_train = [] log_valid = [] for epoch in range(num_epochs): # 訓練モードに設定 model.train() for inputs, labels in dataloader_train: # 勾配をゼロで初期化 optimizer.zero_grad() # 順伝播 + 誤差逆伝播 + 重み更新 outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # 損失と正解率の算出 loss_train, acc_train = calculate_loss_and_accuracy(model, criterion, dataloader_train) loss_valid, acc_valid = calculate_loss_and_accuracy(model, criterion, dataloader_valid) log_train.append([loss_train, acc_train]) log_valid.append([loss_valid, acc_valid]) # ログを出力 print(f'epoch: {epoch + 1}, loss_train: {loss_train:.4f}, accuracy_train: {acc_train:.4f}, loss_valid: {loss_valid:.4f}, accuracy_valid: {acc_valid:.4f}')
--- 出力 --- epoch: 1, loss_train: 0.3476, accuracy_train: 0.8796, loss_valid: 0.3656, accuracy_valid: 0.8840 epoch: 2, loss_train: 0.2912, accuracy_train: 0.8988, loss_valid: 0.3219, accuracy_valid: 0.8967 ・・・ epoch: 29, loss_train: 0.2102, accuracy_train: 0.9287, loss_valid: 0.3259, accuracy_valid: 0.8930 epoch: 30, loss_train: 0.2119, accuracy_train: 0.9289, loss_valid: 0.3262, accuracy_valid: 0.8945
from matplotlib import pyplot as plt # 視覚化 fig, ax = plt.subplots(1, 2, figsize=(15, 5)) ax[0].plot(np.array(log_train).T[0], label='train') ax[0].plot(np.array(log_valid).T[0], label='valid') ax[0].set_xlabel('epoch') ax[0].set_ylabel('loss') ax[0].legend() ax[1].plot(np.array(log_train).T[1], label='train') ax[1].plot(np.array(log_valid).T[1], label='valid') ax[1].set_xlabel('epoch') ax[1].set_ylabel('accuracy') ax[1].legend() plt.show()
76. チェックポイント
問題75のコードを改変し,各エポックのパラメータ更新が完了するたびに,チェックポイント(学習途中のパラメータ(重み行列など)の値や最適化アルゴリズムの内部状態)をファイルに書き出せ.
学習途中のパラメータはmodel.state_dict()
、最適化アルゴリズムの内部状態はoptimizer.state_dict()
でアクセス可能なので、各エポックでエポック数と合わせて保存する処理を追加します。
なお、出力は前問と同様のため省略します。
# モデルの定義 model = SLPNet(300, 4) # 損失関数の定義 criterion = nn.CrossEntropyLoss() # オプティマイザの定義 optimizer = torch.optim.SGD(model.parameters(), lr=1e-1) # 学習 num_epochs = 10 log_train = [] log_valid = [] for epoch in range(num_epochs): # 訓練モードに設定 model.train() for inputs, labels in dataloader_train: # 勾配をゼロで初期化 optimizer.zero_grad() # 順伝播 + 誤差逆伝播 + 重み更新 outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # 損失と正解率の算出 loss_train, acc_train = calculate_loss_and_accuracy(model, criterion, dataloader_train) loss_valid, acc_valid = calculate_loss_and_accuracy(model, criterion, dataloader_valid) log_train.append([loss_train, acc_train]) log_valid.append([loss_valid, acc_valid]) # チェックポイントの保存 torch.save({'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict()}, f'checkpoint{epoch + 1}.pt') # ログを出力 print(f'epoch: {epoch + 1}, loss_train: {loss_train:.4f}, accuracy_train: {acc_train:.4f}, loss_valid: {loss_valid:.4f}, accuracy_valid: {acc_valid:.4f}')
77. ミニバッチ化
問題76のコードを改変し,事例ごとに損失・勾配を計算し,行列の値を更新せよ(ミニバッチ化).の値をと変化させながら,1エポックの学習に要する時間を比較せよ.
バッチサイズを変えるごとにすべての処理を書くのは大変なので、Dataloader
の作成以降の処理をtrain_model
として関数化し、バッチサイズを含むいくつかのパラメータを引数として設定します。
import time def train_model(dataset_train, dataset_valid, batch_size, model, criterion, optimizer, num_epochs): # dataloaderの作成 dataloader_train = DataLoader(dataset_train, batch_size=batch_size, shuffle=True) dataloader_valid = DataLoader(dataset_valid, batch_size=len(dataset_valid), shuffle=False) # 学習 log_train = [] log_valid = [] for epoch in range(num_epochs): # 開始時刻の記録 s_time = time.time() # 訓練モードに設定 model.train() for inputs, labels in dataloader_train: # 勾配をゼロで初期化 optimizer.zero_grad() # 順伝播 + 誤差逆伝播 + 重み更新 outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # 損失と正解率の算出 loss_train, acc_train = calculate_loss_and_accuracy(model, criterion, dataloader_train) loss_valid, acc_valid = calculate_loss_and_accuracy(model, criterion, dataloader_valid) log_train.append([loss_train, acc_train]) log_valid.append([loss_valid, acc_valid]) # チェックポイントの保存 torch.save({'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict()}, f'checkpoint{epoch + 1}.pt') # 終了時刻の記録 e_time = time.time() # ログを出力 print(f'epoch: {epoch + 1}, loss_train: {loss_train:.4f}, accuracy_train: {acc_train:.4f}, loss_valid: {loss_valid:.4f}, accuracy_valid: {acc_valid:.4f}, {(e_time - s_time):.4f}sec') return {'train': log_train, 'valid': log_valid}
バッチサイズを変えながら、処理時間を計測します。
# datasetの作成 dataset_train = CreateDataset(X_train, y_train) dataset_valid = CreateDataset(X_valid, y_valid) # モデルの定義 model = SLPNet(300, 4) # 損失関数の定義 criterion = nn.CrossEntropyLoss() # オプティマイザの定義 optimizer = torch.optim.SGD(model.parameters(), lr=1e-1) # モデルの学習 for batch_size in [2 ** i for i in range(11)]: print(f'バッチサイズ: {batch_size}') log = train_model(dataset_train, dataset_valid, batch_size, model, criterion, optimizer, 1)
--- 出力 --- バッチサイズ: 1 epoch: 1, loss_train: 0.3237, accuracy_train: 0.8888, loss_valid: 0.3476, accuracy_valid: 0.8817, 5.4416sec バッチサイズ: 2 epoch: 1, loss_train: 0.2966, accuracy_train: 0.8999, loss_valid: 0.3258, accuracy_valid: 0.8847, 3.0029sec バッチサイズ: 4 epoch: 1, loss_train: 0.2883, accuracy_train: 0.8999, loss_valid: 0.3222, accuracy_valid: 0.8862, 1.5988sec バッチサイズ: 8 epoch: 1, loss_train: 0.2835, accuracy_train: 0.9023, loss_valid: 0.3179, accuracy_valid: 0.8907, 0.8732sec バッチサイズ: 16 epoch: 1, loss_train: 0.2817, accuracy_train: 0.9038, loss_valid: 0.3164, accuracy_valid: 0.8907, 0.5445sec バッチサイズ: 32 epoch: 1, loss_train: 0.2810, accuracy_train: 0.9038, loss_valid: 0.3159, accuracy_valid: 0.8900, 0.3482sec バッチサイズ: 64 epoch: 1, loss_train: 0.2806, accuracy_train: 0.9040, loss_valid: 0.3157, accuracy_valid: 0.8900, 0.2580sec バッチサイズ: 128 epoch: 1, loss_train: 0.2806, accuracy_train: 0.9041, loss_valid: 0.3156, accuracy_valid: 0.8900, 0.1984sec バッチサイズ: 256 epoch: 1, loss_train: 0.2801, accuracy_train: 0.9039, loss_valid: 0.3155, accuracy_valid: 0.8900, 0.1715sec バッチサイズ: 512 epoch: 1, loss_train: 0.2802, accuracy_train: 0.9038, loss_valid: 0.3155, accuracy_valid: 0.8900, 0.2177sec バッチサイズ: 1024 epoch: 1, loss_train: 0.2792, accuracy_train: 0.9038, loss_valid: 0.3155, accuracy_valid: 0.8900, 0.1603sec
概ね、バッチサイズが大きいほど計算時間が短くなってることが分かります。
78. GPU上での学習
問題77のコードを改変し,GPU上で学習を実行せよ.
GPUを指定する引数device
をcalculate_loss_and_accuracy
、train_model
に追加します。
それぞれの関数内で、モデルおよび入力TensorをGPUに送る処理を追加し、device
にcuda
を指定すればGPUを使用することができます。
なお、Google Colaboratoryでは、あらかじめ画面上部のメニューから、「ランタイム」、「ランタイムのタイプを変更」と進み、「ハードウェアアクセラレータ」を「GPU」に変更しておく必要があります(※ 利用しているプランによっては料金がかかります。事前にご確認ください)。
def calculate_loss_and_accuracy(model, criterion, loader, device): model.eval() loss = 0.0 total = 0 correct = 0 with torch.no_grad(): for inputs, labels in loader: inputs = inputs.to(device) labels = labels.to(device) outputs = model(inputs) loss += criterion(outputs, labels).item() pred = torch.argmax(outputs, dim=-1) total += len(inputs) correct += (pred == labels).sum().item() return loss / len(loader), correct / total def train_model(dataset_train, dataset_valid, batch_size, model, criterion, optimizer, num_epochs, device=None): # GPUに送る model.to(device) # dataloaderの作成 dataloader_train = DataLoader(dataset_train, batch_size=batch_size, shuffle=True) dataloader_valid = DataLoader(dataset_valid, batch_size=len(dataset_valid), shuffle=False) # 学習 log_train = [] log_valid = [] for epoch in range(num_epochs): # 開始時刻の記録 s_time = time.time() # 訓練モードに設定 model.train() for inputs, labels in dataloader_train: # 勾配をゼロで初期化 optimizer.zero_grad() # 順伝播 + 誤差逆伝播 + 重み更新 inputs = inputs.to(device) labels = labels.to(device) outputs = model.forward(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # 損失と正解率の算出 loss_train, acc_train = calculate_loss_and_accuracy(model, criterion, dataloader_train, device) loss_valid, acc_valid = calculate_loss_and_accuracy(model, criterion, dataloader_valid, device) log_train.append([loss_train, acc_train]) log_valid.append([loss_valid, acc_valid]) # チェックポイントの保存 torch.save({'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict()}, f'checkpoint{epoch + 1}.pt') # 終了時刻の記録 e_time = time.time() # ログを出力 print(f'epoch: {epoch + 1}, loss_train: {loss_train:.4f}, accuracy_train: {acc_train:.4f}, loss_valid: {loss_valid:.4f}, accuracy_valid: {acc_valid:.4f}, {(e_time - s_time):.4f}sec') return {'train': log_train, 'valid': log_valid}
# datasetの作成 dataset_train = CreateDataset(X_train, y_train) dataset_valid = CreateDataset(X_valid, y_valid) # モデルの定義 model = SLPNet(300, 4) # 損失関数の定義 criterion = nn.CrossEntropyLoss() # オプティマイザの定義 optimizer = torch.optim.SGD(model.parameters(), lr=1e-1) # デバイスの指定 device = torch.device('cuda') # モデルの学習 for batch_size in [2 ** i for i in range(11)]: print(f'バッチサイズ: {batch_size}') log = train_model(dataset_train, dataset_valid, batch_size, model, criterion, optimizer, 1, device=device)
--- 出力 --- バッチサイズ: 1 epoch: 1, loss_train: 0.3300, accuracy_train: 0.8874, loss_valid: 0.3584, accuracy_valid: 0.8772, 9.0342sec バッチサイズ: 2 epoch: 1, loss_train: 0.3025, accuracy_train: 0.8994, loss_valid: 0.3374, accuracy_valid: 0.8870, 4.6391sec バッチサイズ: 4 epoch: 1, loss_train: 0.2938, accuracy_train: 0.9005, loss_valid: 0.3321, accuracy_valid: 0.8855, 2.4228sec バッチサイズ: 8 epoch: 1, loss_train: 0.2894, accuracy_train: 0.9039, loss_valid: 0.3299, accuracy_valid: 0.8855, 1.2517sec バッチサイズ: 16 epoch: 1, loss_train: 0.2876, accuracy_train: 0.9038, loss_valid: 0.3285, accuracy_valid: 0.8855, 0.7149sec バッチサイズ: 32 epoch: 1, loss_train: 0.2867, accuracy_train: 0.9050, loss_valid: 0.3280, accuracy_valid: 0.8862, 0.4323sec バッチサイズ: 64 epoch: 1, loss_train: 0.2863, accuracy_train: 0.9050, loss_valid: 0.3277, accuracy_valid: 0.8862, 0.2834sec バッチサイズ: 128 epoch: 1, loss_train: 0.2869, accuracy_train: 0.9051, loss_valid: 0.3276, accuracy_valid: 0.8862, 0.2070sec バッチサイズ: 256 epoch: 1, loss_train: 0.2864, accuracy_train: 0.9054, loss_valid: 0.3275, accuracy_valid: 0.8862, 0.1587sec バッチサイズ: 512 epoch: 1, loss_train: 0.2859, accuracy_train: 0.9056, loss_valid: 0.3275, accuracy_valid: 0.8862, 0.2016sec バッチサイズ: 1024 epoch: 1, loss_train: 0.2858, accuracy_train: 0.9056, loss_valid: 0.3275, accuracy_valid: 0.8862, 0.1303sec
79. 多層ニューラルネットワーク
問題78のコードを改変し,バイアス項の導入や多層化など,ニューラルネットワークの形状を変更しながら,高性能なカテゴリ分類器を構築せよ.
多層ニューラルネットワークMLPNet
を新たに定義します。このネットワークは入力層 -> 中間層 -> 出力層の構成とし、中間層のあとにバッチノーマライゼーションを行うことにします。
また、train_model
では新たに学習の打ち切り基準を導入します。今回はシンプルに、検証データの損失が3エポック連続で低下しなかった場合に打ち切るルールとします。
さらに、学習率を徐々に下げるスケジューラも追加し、汎化性能の向上を狙います。
from torch.nn import functional as F class MLPNet(nn.Module): def __init__(self, input_size, mid_size, output_size, mid_layers): super().__init__() self.mid_layers = mid_layers self.fc = nn.Linear(input_size, mid_size) self.fc_mid = nn.Linear(mid_size, mid_size) self.fc_out = nn.Linear(mid_size, output_size) self.bn = nn.BatchNorm1d(mid_size) def forward(self, x): x = F.relu(self.fc(x)) for _ in range(self.mid_layers): x = F.relu(self.bn(self.fc_mid(x))) x = F.relu(self.fc_out(x)) return x
from torch import optim def calculate_loss_and_accuracy(model, criterion, loader, device): model.eval() loss = 0.0 total = 0 correct = 0 with torch.no_grad(): for inputs, labels in loader: inputs = inputs.to(device) labels = labels.to(device) outputs = model(inputs) loss += criterion(outputs, labels).item() pred = torch.argmax(outputs, dim=-1) total += len(inputs) correct += (pred == labels).sum().item() return loss / len(loader), correct / total def train_model(dataset_train, dataset_valid, batch_size, model, criterion, optimizer, num_epochs, device=None): # GPUに送る model.to(device) # dataloaderの作成 dataloader_train = DataLoader(dataset_train, batch_size=batch_size, shuffle=True) dataloader_valid = DataLoader(dataset_valid, batch_size=len(dataset_valid), shuffle=False) # スケジューラの設定 scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, num_epochs, eta_min=1e-5, last_epoch=-1) # 学習 log_train = [] log_valid = [] for epoch in range(num_epochs): # 開始時刻の記録 s_time = time.time() # 訓練モードに設定 model.train() for inputs, labels in dataloader_train: # 勾配をゼロで初期化 optimizer.zero_grad() # 順伝播 + 誤差逆伝播 + 重み更新 inputs = inputs.to(device) labels = labels.to(device) outputs = model.forward(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # 損失と正解率の算出 loss_train, acc_train = calculate_loss_and_accuracy(model, criterion, dataloader_train, device) loss_valid, acc_valid = calculate_loss_and_accuracy(model, criterion, dataloader_valid, device) log_train.append([loss_train, acc_train]) log_valid.append([loss_valid, acc_valid]) # チェックポイントの保存 torch.save({'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict()}, f'checkpoint{epoch + 1}.pt') # 終了時刻の記録 e_time = time.time() # ログを出力 print(f'epoch: {epoch + 1}, loss_train: {loss_train:.4f}, accuracy_train: {acc_train:.4f}, loss_valid: {loss_valid:.4f}, accuracy_valid: {acc_valid:.4f}, {(e_time - s_time):.4f}sec') # 検証データの損失が3エポック連続で低下しなかった場合は学習終了 if epoch > 2 and log_valid[epoch - 3][0] <= log_valid[epoch - 2][0] <= log_valid[epoch - 1][0] <= log_valid[epoch][0]: break # スケジューラを1ステップ進める scheduler.step() return {'train': log_train, 'valid': log_valid}
# datasetの作成 dataset_train = CreateDataset(X_train, y_train) dataset_valid = CreateDataset(X_valid, y_valid) # モデルの定義 model = MLPNet(300, 200, 4, 1) # 損失関数の定義 criterion = nn.CrossEntropyLoss() # オプティマイザの定義 optimizer = torch.optim.SGD(model.parameters(), lr=1e-3) # デバイスの指定 device = torch.device('cuda') # モデルの学習 log = train_model(dataset_train, dataset_valid, 64, model, criterion, optimizer, 1000, device)
--- 出力 --- epoch: 1, loss_train: 1.1176, accuracy_train: 0.6679, loss_valid: 1.1150, accuracy_valid: 0.6572, 0.4695sec epoch: 2, loss_train: 0.8050, accuracy_train: 0.7620, loss_valid: 0.8005, accuracy_valid: 0.7687, 0.4521sec ・・・ epoch: 96, loss_train: 0.1708, accuracy_train: 0.9460, loss_valid: 0.2858, accuracy_valid: 0.9034, 0.4632sec epoch: 97, loss_train: 0.1702, accuracy_train: 0.9466, loss_valid: 0.2861, accuracy_valid: 0.9034, 0.5373sec
97エポックで打ち切りとなりました。 エポックごとの損失と正解率を可視化します。
fig, ax = plt.subplots(1, 2, figsize=(15, 5)) ax[0].plot(np.array(log['train']).T[0], label='train') ax[0].plot(np.array(log['valid']).T[0], label='valid') ax[0].set_xlabel('epoch') ax[0].set_ylabel('loss') ax[0].legend() ax[1].plot(np.array(log['train']).T[1], label='train') ax[1].plot(np.array(log['valid']).T[1], label='valid') ax[1].set_xlabel('epoch') ax[1].set_ylabel('accuracy') ax[1].legend() plt.show()
評価データの正解率を確認します。
def calculate_accuracy(model, loader, device): model.eval() total = 0 correct = 0 with torch.no_grad(): for inputs, labels in loader: inputs = inputs.to(device) labels = labels.to(device) outputs = model(inputs) pred = torch.argmax(outputs, dim=-1) total += len(inputs) correct += (pred == labels).sum().item() return correct / total
# 正解率の確認 acc_train = calculate_accuracy(model, dataloader_train, device) acc_test = calculate_accuracy(model, dataloader_test, device) print(f'正解率(学習データ):{acc_train:.3f}') print(f'正解率(評価データ):{acc_test:.3f}')
--- 出力 --- 正解率(学習データ):0.947 正解率(評価データ):0.921
単層ニューラルネットワークでは評価データの正解率が0.891でしたが、多層にすることによって3ポイント向上しています。
理解を深めるためのオススメ教材
全100問の解答はこちら