はじめに
頑張れば、何かがあるって、信じてる。nikkieです。
週次ブログ駆動開発、「自然言語処理のタスクをするkeras(tensorflow
)製のモデルをpytorch
でも書いてみる」の後編です。
前編はこちら:
更新履歴
- 2020/04/12 末尾のグラフの直後に改行を追加して体裁修正
続・kerasからtorchへ、しかし ...!
前編から再掲しますが、以下の方針で書いています:
- ロイター通信のデータと全く同じデータは
torch
にはなさそうなので、データのロードはkeras
を使用 - 2層のMLPを作るところを
torch
で書き換え
スクリプト全容
import numpy as np from tensorflow.keras.datasets import reuters import torch import torch.optim as optim import torch.nn as nn import torch.nn.functional as F from torch.utils.data.dataset import random_split from keras_mlp import TokenizePreprocessor np.random.seed(42) torch.manual_seed(1234) MAX_WORDS = 1000 DROPOUT = 0.5 BATCH_SIZE = 32 EPOCHS = 5 def convert_to_torch_tensors(texts, labels): torch_tensors = [] for text, label in zip(texts, labels): text_tensor = torch.tensor(text) torch_tensor = (label, text_tensor) torch_tensors.append(torch_tensor) return torch_tensors class MLPNet(nn.Module): def __init__(self, max_words, number_of_classes, drop_out): super(MLPNet, self).__init__() self.fc1 = nn.Linear(max_words, 512) self.fc2 = nn.Linear(512, number_of_classes) self.dropout1 = nn.Dropout(drop_out) def forward(self, x): x = F.relu(self.fc1(x)) x = self.dropout1(x) return F.softmax(self.fc2(x), dim=1) (x_train, y_train), (x_test, y_test) = reuters.load_data(num_words=MAX_WORDS) tokenizer = TokenizePreprocessor.initialize_tokenizer(MAX_WORDS) preprocessor = TokenizePreprocessor(tokenizer) x_train = preprocessor.convert_text_to_matrix(x_train, "binary") x_test = preprocessor.convert_text_to_matrix(x_test, "binary") number_of_classes = np.max(y_train) + 1 train_dataset = convert_to_torch_tensors(x_train, y_train) test_dataset = convert_to_torch_tensors(x_test, y_test) train_length = int(len(train_dataset) * 0.9) train_dataset, val_dataset = random_split( train_dataset, [train_length, len(train_dataset) - train_length] ) train_loader = torch.utils.data.DataLoader( dataset=train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=1 ) val_loader = torch.utils.data.DataLoader( dataset=val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=1 ) test_loader = torch.utils.data.DataLoader( dataset=test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=1 ) device = "gpu" if torch.cuda.is_available() else "cpu" net = MLPNet(MAX_WORDS, number_of_classes, DROPOUT).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(net.parameters()) train_loss_list, train_acc_list, val_loss_list, val_acc_list = [], [], [], [] for epoch in range(EPOCHS): train_loss, train_acc, val_loss, val_acc = 0, 0, 0, 0 net.train() for i, (labels, text_tensors) in enumerate(train_loader): labels, text_tensors = labels.to(device), text_tensors.to(device) optimizer.zero_grad() # mode="binary"で指定したことでdouble(torch.float64)が渡ってきてエラーになることへの対応 outputs = net(text_tensors.float()) loss = criterion(outputs, labels) train_loss += loss.item() acc = (outputs.max(1)[1] == labels).sum() train_acc += acc.item() loss.backward() optimizer.step() avg_train_loss = train_loss / len(train_loader.dataset) avg_train_acc = train_acc / len(train_loader.dataset) net.eval() with torch.no_grad(): for labels, texts in val_loader: labels, texts = labels.to(device), texts.to(device) outputs = net(texts.float()) loss = criterion(outputs, labels) val_loss += loss.sum() acc = (outputs.max(1)[1] == labels).sum() val_acc += acc.item() avg_val_loss = val_loss / len(val_loader.dataset) avg_val_acc = val_acc / len(val_loader.dataset) print( f"Epoch [{epoch+1}/{EPOCHS}], ", f"Loss: {avg_train_loss:.4f}, Acc: {avg_train_acc:.4f}, ", f"val Loss: {avg_val_loss:.4f}, val Acc: {avg_val_acc:.4f}", ) train_loss_list.append(avg_train_loss) train_acc_list.append(avg_train_acc) val_loss_list.append(avg_val_loss) val_acc_list.append(avg_val_acc) net.eval() with torch.no_grad(): total = 0 test_acc = 0 for labels, texts in test_loader: labels, texts = labels.to(device), texts.to(device) outputs = net(texts.float()) test_acc += (outputs.max(1)[1] == labels).sum().item() total += labels.size(0) print(f"test_accuracy: {100 * test_acc / total} %")
前編では、以下の2点について見ています:
- データの準備(
convert_to_torch_tensors
関数) - モデル作成(
MLPNet
クラス)
学習部分のコード
for epoch in range(EPOCHS):
の中に書いている部分です。
net.train()
以降で学習用データを使った重みの更新を書きnet.eval()
以降でバリデーションデータ1を使っての性能評価結果を書いています
keras
に比べて学習部分で書くコード量はだいぶ多く、ここが一番違うと感じました。
少しだけtorch
でMNISTのコードを眺めてすぐ撤退した思い出があるのですが、「学習部分を細かく記述というkeras
との違いが、あの時は受け入れられなかったのだな」と気づきました。
学習が終わった後はテスト用データを使って性能を確認します(for
文の外のnet.eval()
以降の部分)。
スクリプト実行
いよいよ動かすときです。
ちなみに、上記のコードに至るまでに、スクリプトを実行した際に、
RuntimeError: Expected object of scalar type Float but got scalar type Double for argument #2 'mat1' in call to _th_addmm
が発生しました。
outputs = net(text_tensors)
が原因で、.float()
と型を変換する必要がありました2。
それでは、動かしてみましょう。
$ python reuters/torch_mlp.py Epoch [1/5], Loss: 0.1055, Acc: 0.5286, val Loss: 0.1071, val Acc: 0.5439 Epoch [2/5], Loss: 0.1026, Acc: 0.5909, val Loss: 0.1043, val Acc: 0.6340 Epoch [3/5], Loss: 0.1010, Acc: 0.6399, val Loss: 0.1042, val Acc: 0.6385 Epoch [4/5], Loss: 0.1007, Acc: 0.6504, val Loss: 0.1037, val Acc: 0.6552 Epoch [5/5], Loss: 0.0998, Acc: 0.6792, val Loss: 0.1023, val Acc: 0.6963 test_accuracy: 67.80943900267141 %
正解率がいま一つ🙄ですね。
keras
の場合は、例えばテスト用データでは79%程度の正解率が出ていました。
書き換えでの落とし穴
実は書いたコードに以下の落とし穴がありました:
torch
のtorch.nn.CrossEntropyLoss
にはsoftmaxの計算が含まれる(つまり、F.softmax(self.fc2(x), dim=1)
という適用は不要)
落とし穴:torch.nn.CrossEntropyLoss
以下のQiita記事に助けられました(多謝):
class MLPNet(nn.Module): def __init__(self, max_words, number_of_classes, drop_out): # 変更なし def forward(self, x): x = F.relu(self.fc1(x)) x = self.dropout1(x) return self.fc2(x) # F.softmaxを適用しない
ハマったときに調べた:Adamの学習率のデフォルト値
tensorflow.keras.optimizers.Adam
:learning_rate=0.001
torch.optim.Adam
:lr=0.001
パラメタの指定が異なるところを比べていき、「optimizerは?」となりました。
ドキュメントを確認したところ、デフォルト値は同じでした。
再度スクリプト実行
$ python reuters/torch_mlp.py Epoch [1/5], Loss: 0.0461, Acc: 0.6739, val Loss: 0.0308, val Acc: 0.7720 Epoch [2/5], Loss: 0.0265, Acc: 0.8025, val Loss: 0.0255, val Acc: 0.8087 Epoch [3/5], Loss: 0.0191, Acc: 0.8527, val Loss: 0.0231, val Acc: 0.8265 Epoch [4/5], Loss: 0.0143, Acc: 0.8905, val Loss: 0.0230, val Acc: 0.8309 Epoch [5/5], Loss: 0.0115, Acc: 0.9059, val Loss: 0.0233, val Acc: 0.8287 test_accuracy: 79.78628673196795 %
学習用データ、テスト用データでkeras
の場合と同程度の正解率が出るようになりました!
(グラフはkeras_mlp.pyのplot_accuracy
関数を使って描画しました)
おまけ:再現性の確保
※前掲のスクリプトには含まれています
np.random.seed(42) torch.manual_seed(1234)
TensorFlow
2系では再現性の確保のためのシードの固定を結構調べる必要があったのですが、torch
では一発で見つかりました。
Reproducibility — PyTorch master documentation
終わりに
年始で時間があったこともあり、今回のネタを選んだところ、動作するコードは半日程度で準備できました。
そこからkeras
をまずQiitaにアウトプットし、torch
をこのブログにと書いていったのですが、アウトプットの分量が思っていたより多くなり、時間があったからなんとか収まりました。
今回作ったモデルはシンプルなので、追加で試せそうなことはいくつもあります。
なのでこれは始まりということで、モデルの改良で引き続き手を動かしていきます。
今回のコードはリポジトリに入れました:
ブログ駆動開発2回目は以上です。
それでは、翌週のブログ駆動開発(自然言語処理編)でお会いしましょう。
試してみたい情報
この記事の下書きを書いてから公開までの間に見つけた情報です。
●KerasからPyTorchに移った人へ
— HELLO CYBERNETICS (@ML_deep) 2020年1月4日
学習ラッパーオススメNo.1はCatalystになります。
KerasユーザーがPyTorchを敬遠する一番大きな理由は、学習ループを毎回書かなければならないことだと思います。Kerasやsklearnみたいに設定を外で決めたら、fit関数で実行したいという思いにCatalystは応えます。
ただ公式だとGPUとCPUをそれぞれ初期化するのはあんまおすすめしていないらしい
— Teppei Kurita (@kuritateppei) 2020年1月4日
torch.manual_seed(seed)
これでCPU,GPUともに乱数シードが固定されるのだけど以下のオプションが必要
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
-
チュートリアルにならって
random_split
を使い、学習用データの10%をバリデーションデータとして取り分けました↩ -
ref: https://stackoverflow.com/a/56741419 。この記事を書いていて思ったのですが、
keras
のsequences_to_matrix
の返り値をtensor
にする際に、型を指定する必要があったのかもしれません↩