自然言語処理編 - LSTM モデル学習 その肆
前の記事では、モデル学習の改善を行った。 この記事では、推論の正解率改善を体験する。 改善と言っても、既に機能として存在するので、パラメータを変更するだけとなる。 試す機能は、LSTM の多層化と、双方向化とする。
LSTM の多層化
LSTM の層数を 2 にするため、
PosTaggerLSTM の引数に num_layers=2
を追加することと、
保存するモデルファイル名を変えている。
それ以外は、変更していない。
train_layers.py
import bz2
import logging
import numpy as np
import time
import torch
from torch.utils.data import TensorDataset, DataLoader
from torch.utils.tensorboard import SummaryWriter
from pos_tagger_lstm import PosTaggerLSTM
# ログ設定
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
# 処理済みの単語を重複なく保持する入出力ファイル名
WORD_FILE = 'lstm-word.txt.bz2'
# 処理済みの品詞を重複なく保持する入出力ファイル名
POS_FILE = 'lstm-pos.txt.bz2'
# 学習用訓練データ出力ファイル名
X_TRAIN_FILE = 'lstm-optim-x-train.npy'
# 学習用教師データ出力ファイル名
Y_TRAIN_FILE = 'lstm-optim-y-train.npy'
# バッチサイズ
BATCH_SIZE = 128
# 学習率
LEARNING_RATE = 1.0
# エポック数
EPOCHS = 10
def main():
"""LSTM モデル学習を行う"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 単語 ID 辞書を読み込む
with bz2.open(WORD_FILE, 'rt', encoding='utf-8') as f:
word_ids = {k: int(v) for k, v in (line.split('\t') for line in f)}
# 品詞 ID 辞書を読み込む
with bz2.open(POS_FILE, 'rt', encoding='utf-8') as f:
pos_ids = {k: int(v) for k, v in (line.split('\t') for line in f)}
# 訓練データファイルを読み込む
x_train = np.load(X_TRAIN_FILE)
x_train_tensor = torch.from_numpy(x_train)
# 教師データファイルを読み込む
y_train = np.load(Y_TRAIN_FILE)
y_train_tensor = torch.from_numpy(y_train)
# バッチ処理用にデータを準備する
train_dataset = TensorDataset(x_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, pin_memory=True)
print(f'word_ids: {len(word_ids)}, pos_ids: {len(pos_ids)}, x_train: {len(x_train)}, y_train: {len(y_train)}')
# モデルに、単語 ID 数と、品詞 ID 数を指定する
# LSTM の層数を 2 に指定する
model = PosTaggerLSTM(len(word_ids) + 1, len(pos_ids), 100, num_layers=2).to(device)
# 最適化アルゴリズムに確率的勾配降下法を設定する
optimizer = torch.optim.SGD(model.parameters(), lr=LEARNING_RATE)
# 学習率スケジューラを設定する
# 監視指標(scheduler.step 関数で渡す値)の変化率が threshold 値を下回ると、学習率に factor を欠けた値で学習率を更新する
# threshold と比較する値(変化率)は、(前回の監視指標 - 現在の監視指標) / 前回の監視指標 となる
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.9, patience=0,
threshold=0.2, min_lr=0.01)
# 損失関数に交差エントロピー損失を設定する
# ignore_index=-1 で、教師データのパディング値である、-1 を無視する
criterion = torch.nn.CrossEntropyLoss(ignore_index=-1)
# TensorBoard ログ書き込み用
writer = SummaryWriter()
# 前回エポックの損失値(デバッグ表示用)
epoch_loss_old = 0.00001
for epoch in range(EPOCHS):
epoch_start_time = time.time()
# 損失の合計値
loss_sum = 0.0
for i, (x, y) in enumerate(train_loader):
x = x.to(device)
y = y.to(device)
# 訓練する
output = model(x)
# 損失値(予測結果と正解データとの間の誤差)を計算する
loss = criterion(output.view(-1, output.size(2)), y.view(-1))
# 勾配を初期化する
optimizer.zero_grad()
# 勾配を計算する
loss.backward()
# 予測結果と正解データとの誤差 (損失) を小さくするように、モデルのパラメータを調整する
optimizer.step()
# 損失の合計値を更新する
loss_sum += loss.item() * len(y)
if i >= 1 and i % (1000 // BATCH_SIZE) == 0:
print(f'{i * BATCH_SIZE}, Loss: {loss_sum / ((i + 1) * BATCH_SIZE):.4f}')
epoch_loss = loss_sum / len(x_train)
# 学習率を取得する
lr = optimizer.param_groups[0]['lr']
# TensorBoard 用ログを書き込む
writer.add_scalar('Loss', loss_sum / len(x_train), epoch + 1)
writer.add_scalar('Learning Rate', lr, epoch + 1)
writer.flush()
# 学習率を更新する(スケジューラによって引数が異なる)
scheduler.step(epoch_loss)
# この値が、スケジューラの threshold 値を下回ると、スケジューラが学習率が変更する
print(f'rel_change: {(epoch_loss_old - epoch_loss) / epoch_loss_old}')
epoch_loss_old = epoch_loss
# モデルの保存
outfile = f'lstm-layer2-epoch{epoch + 1:02d}.model'
torch.save({
'epoch': epoch + 1,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': scheduler.state_dict(),
}, outfile)
elapsed_time = time.time() - epoch_start_time
print(
f'Epoch: {epoch + 1}, Loss: {loss_sum / len(x_train):.4f}, Learning Rate: {lr:.6f}, Time: {elapsed_time:.2f} s')
if __name__ == '__main__':
start_time = time.time()
main()
print(f'経過時間: {time.time() - start_time:.2f} s')
以下のような実行結果となった。 学習は、T4 GPU で行っている。
Epoch: 1, Loss: 1.0046, Learning Rate: 1.000000, Time: 15.48 s
Epoch: 2, Loss: 0.4056, Learning Rate: 1.000000, Time: 14.40 s
Epoch: 3, Loss: 0.2609, Learning Rate: 1.000000, Time: 14.75 s
Epoch: 4, Loss: 0.2015, Learning Rate: 1.000000, Time: 15.25 s
Epoch: 5, Loss: 0.1669, Learning Rate: 1.000000, Time: 15.51 s
Epoch: 6, Loss: 0.1423, Learning Rate: 0.900000, Time: 15.12 s
Epoch: 7, Loss: 0.1272, Learning Rate: 0.900000, Time: 14.85 s
Epoch: 8, Loss: 0.1150, Learning Rate: 0.810000, Time: 15.09 s
Epoch: 9, Loss: 0.1056, Learning Rate: 0.729000, Time: 14.81 s
Epoch: 10, Loss: 0.0990, Learning Rate: 0.729000, Time: 14.88 s
推論をする
変更は、学習時と同様に、
PosTaggerLSTM の引数に num_layers=2
を追加することと、
読み込みモデルデータを変えている。
infer_layers.py
import bz2
import logging
import numpy as np
import time
import torch
from torch.utils.data import TensorDataset, DataLoader
from pos_tagger_lstm import PosTaggerLSTM
# ログ設定
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
# 処理済みの単語を重複なく保持する入出力ファイル名
WORD_FILE = 'lstm-word.txt.bz2'
# 処理済みの品詞を重複なく保持する入出力ファイル名
POS_FILE = 'lstm-pos.txt.bz2'
# テスト用訓練データ出力ファイル名
X_TEST_FILE = 'lstm-optim-x-test.npy'
# テスト用教師データ出力ファイル名
Y_TEST_FILE = 'lstm-optim-y-test.npy'
# モデルファイル名
MODEL_FILE = 'lstm-layer2-epoch10.model'
# バッチサイズ
BATCH_SIZE = 128
def main():
"""
LSTM モデル推論を行う
"""
print('Loading datas...')
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 単語 ID 辞書を読み込む
with bz2.open(WORD_FILE, 'rt', encoding='utf-8') as f:
word_ids = {line.split('\t')[0]: int(line.split('\t')[1]) for line in f}
# 品詞 ID 辞書を読み込む
with bz2.open(POS_FILE, 'rt', encoding='utf-8') as f:
pos_ids = {line.split('\t')[0]: int(line.split('\t')[1]) for line in f}
# テスト用訓練データファイルを読み込む
x_test = np.load(X_TEST_FILE)
x_test_tensor = torch.from_numpy(x_test)
# テスト用教師データファイルを読み込む
y_test = np.load(Y_TEST_FILE)
y_test_tensor = torch.from_numpy(y_test)
train_dataset = TensorDataset(x_test_tensor, y_test_tensor)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=False, pin_memory=True)
model = PosTaggerLSTM(len(word_ids) + 1, len(pos_ids), 100, num_layers=2).to(device)
checkpoint = torch.load(MODEL_FILE, weights_only=True, map_location=device)
model.load_state_dict(checkpoint['model_state_dict'])
data_count = 0
model.eval()
print('Start inference...')
with torch.no_grad():
start_time = time.time()
ok = 0
for x, y in train_loader:
x = x.to(device)
y = y.to(device)
output = model(x)
# 推論結果から、回答を取り出す
ans = torch.argmax(output, dim=2)
# 教師データから、パディング値を除外するための配列を作成する(パディング値なら False となる配列)
mask = y != -1
# 回答から、正解でかつ、パディングデータでないものを抽出する
# * mask の部分は、AND 条件と判定される。
ok += ((ans == y) * mask).sum().item()
# mask の値が、True の項目合計数を求める
data_count += mask.sum().item()
print(f'OK: {ok}, Total: {data_count}, Accuracy: {ok / data_count:.4f}')
elapsed_time = time.time() - start_time
print(f'経過時間: {elapsed_time:.2f} s ({data_count / elapsed_time:.0f} times/s)')
if __name__ == '__main__':
main()
以下のような実行結果となった。 推論は、CPU で行っている。
OK: 424544, Total: 440767, Accuracy: 0.9632
経過時間: 6.95 s (63419 times/s)
LSTM の多層化と双方向化の組み合わせ
LSTM モデルの多層化と双方向化を試す。
PosTaggerLSTM の引数に num_layers=2
と bidirectional=True
を追加することと、
保存するモデルファイル名を変えている。
train_layers_bidirect.py
import bz2
import logging
import numpy as np
import time
import torch
from torch.utils.data import TensorDataset, DataLoader
from torch.utils.tensorboard import SummaryWriter
from pos_tagger_lstm import PosTaggerLSTM
# ログ設定
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
# 処理済みの単語を重複なく保持する入出力ファイル名
WORD_FILE = 'lstm-word.txt.bz2'
# 処理済みの品詞を重複なく保持する入出力ファイル名
POS_FILE = 'lstm-pos.txt.bz2'
# 学習用訓練データ出力ファイル名
X_TRAIN_FILE = 'lstm-optim-x-train.npy'
# 学習用教師データ出力ファイル名
Y_TRAIN_FILE = 'lstm-optim-y-train.npy'
# バッチサイズ
BATCH_SIZE = 128
# 学習率
LEARNING_RATE = 1.0
# エポック数
EPOCHS = 10
def main():
"""LSTM モデル学習を行う"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 単語 ID 辞書を読み込む
with bz2.open(WORD_FILE, 'rt', encoding='utf-8') as f:
word_ids = {k: int(v) for k, v in (line.split('\t') for line in f)}
# 品詞 ID 辞書を読み込む
with bz2.open(POS_FILE, 'rt', encoding='utf-8') as f:
pos_ids = {k: int(v) for k, v in (line.split('\t') for line in f)}
# 訓練データファイルを読み込む
x_train = np.load(X_TRAIN_FILE)
x_train_tensor = torch.from_numpy(x_train)
# 教師データファイルを読み込む
y_train = np.load(Y_TRAIN_FILE)
y_train_tensor = torch.from_numpy(y_train)
# バッチ処理用にデータを準備する
train_dataset = TensorDataset(x_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, pin_memory=True)
print(f'word_ids: {len(word_ids)}, pos_ids: {len(pos_ids)}, x_train: {len(x_train)}, y_train: {len(y_train)}')
# モデルに、単語 ID 数と、品詞 ID 数を指定する
# LSTM の層数を 2 に指定する
model = PosTaggerLSTM(len(word_ids) + 1, len(pos_ids), 100, num_layers=2, bidirectional=True).to(device)
# 最適化アルゴリズムに確率的勾配降下法を設定する
optimizer = torch.optim.SGD(model.parameters(), lr=LEARNING_RATE)
# 学習率スケジューラを設定する
# 監視指標(scheduler.step 関数で渡す値)の変化率が threshold 値を下回ると、学習率に factor を欠けた値で学習率を更新する
# threshold と比較する値(変化率)は、(前回の監視指標 - 現在の監視指標) / 前回の監視指標 となる
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.9, patience=0,
threshold=0.2, min_lr=0.01)
# 損失関数に交差エントロピー損失を設定する
# ignore_index=-1 で、教師データのパディング値である、-1 を無視する
criterion = torch.nn.CrossEntropyLoss(ignore_index=-1)
# TensorBoard ログ書き込み用
writer = SummaryWriter()
# 前回エポックの損失値(デバッグ表示用)
epoch_loss_old = 0.00001
for epoch in range(EPOCHS):
epoch_start_time = time.time()
# 損失の合計値
loss_sum = 0.0
for i, (x, y) in enumerate(train_loader):
x = x.to(device)
y = y.to(device)
# 訓練する
output = model(x)
# 損失値(予測結果と正解データとの間の誤差)を計算する
loss = criterion(output.view(-1, output.size(2)), y.view(-1))
# 勾配を初期化する
optimizer.zero_grad()
# 勾配を計算する
loss.backward()
# 予測結果と正解データとの誤差 (損失) を小さくするように、モデルのパラメータを調整する
optimizer.step()
# 損失の合計値を更新する
loss_sum += loss.item() * len(y)
if i >= 1 and i % (1000 // BATCH_SIZE) == 0:
print(f'{i * BATCH_SIZE}, Loss: {loss_sum / ((i + 1) * BATCH_SIZE):.4f}')
epoch_loss = loss_sum / len(x_train)
# 学習率を取得する
lr = optimizer.param_groups[0]['lr']
# TensorBoard 用ログを書き込む
writer.add_scalar('Loss', loss_sum / len(x_train), epoch + 1)
writer.add_scalar('Learning Rate', lr, epoch + 1)
writer.flush()
# 学習率を更新する(スケジューラによって引数が異なる)
scheduler.step(epoch_loss)
# この値が、スケジューラの threshold 値を下回ると、スケジューラが学習率が変更する
print(f'rel_change: {(epoch_loss_old - epoch_loss) / epoch_loss_old}')
epoch_loss_old = epoch_loss
# モデルの保存
outfile = f'lstm-layer2-bidirect-epoch{epoch + 1:02d}.model'
torch.save({
'epoch': epoch + 1,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': scheduler.state_dict(),
}, outfile)
elapsed_time = time.time() - epoch_start_time
print(
f'Epoch: {epoch + 1}, Loss: {loss_sum / len(x_train):.4f}, Learning Rate: {lr:.6f}, Time: {elapsed_time:.2f} s')
if __name__ == '__main__':
start_time = time.time()
main()
print(f'経過時間: {time.time() - start_time:.2f} s')
以下のような実行結果となった。 学習は、T4 GPU で行っている。 双方向化すると、学習時間が倍程度になる。
Epoch: 1, Loss: 0.8890, Learning Rate: 1.000000, Time: 30.97 s
Epoch: 2, Loss: 0.3174, Learning Rate: 1.000000, Time: 31.21 s
Epoch: 3, Loss: 0.2007, Learning Rate: 1.000000, Time: 33.28 s
Epoch: 4, Loss: 0.1483, Learning Rate: 1.000000, Time: 33.60 s
Epoch: 5, Loss: 0.1189, Learning Rate: 1.000000, Time: 33.17 s
Epoch: 6, Loss: 0.0974, Learning Rate: 0.900000, Time: 33.59 s
Epoch: 7, Loss: 0.0850, Learning Rate: 0.900000, Time: 33.46 s
Epoch: 8, Loss: 0.0740, Learning Rate: 0.810000, Time: 33.63 s
Epoch: 9, Loss: 0.0668, Learning Rate: 0.810000, Time: 33.62 s
Epoch: 10, Loss: 0.0595, Learning Rate: 0.729000, Time: 33.60 s
推論をする
変更は、学習時と同様に、
PosTaggerLSTM の引数に num_layers=2
と bidirectional=True
を追加することと、
保存するモデルファイル名を変えている。
infer_layers_bidirect.py
import bz2
import logging
import numpy as np
import time
import torch
from torch.utils.data import TensorDataset, DataLoader
from pos_tagger_lstm import PosTaggerLSTM
# ログ設定
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
# 処理済みの単語を重複なく保持する入出力ファイル名
WORD_FILE = 'lstm-word.txt.bz2'
# 処理済みの品詞を重複なく保持する入出力ファイル名
POS_FILE = 'lstm-pos.txt.bz2'
# テスト用訓練データ出力ファイル名
X_TEST_FILE = 'lstm-optim-x-test.npy'
# テスト用教師データ出力ファイル名
Y_TEST_FILE = 'lstm-optim-y-test.npy'
# モデルファイル名
MODEL_FILE = 'lstm-layer2-bidirect-epoch10.model'
# バッチサイズ
BATCH_SIZE = 128
def main():
"""
LSTM モデル推論を行う
"""
print('Loading datas...')
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 単語 ID 辞書を読み込む
with bz2.open(WORD_FILE, 'rt', encoding='utf-8') as f:
word_ids = {line.split('\t')[0]: int(line.split('\t')[1]) for line in f}
# 品詞 ID 辞書を読み込む
with bz2.open(POS_FILE, 'rt', encoding='utf-8') as f:
pos_ids = {line.split('\t')[0]: int(line.split('\t')[1]) for line in f}
# テスト用訓練データファイルを読み込む
x_test = np.load(X_TEST_FILE)
x_test_tensor = torch.from_numpy(x_test)
# テスト用教師データファイルを読み込む
y_test = np.load(Y_TEST_FILE)
y_test_tensor = torch.from_numpy(y_test)
train_dataset = TensorDataset(x_test_tensor, y_test_tensor)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=False, pin_memory=True)
model = PosTaggerLSTM(len(word_ids) + 1, len(pos_ids), 100, num_layers=2, bidirectional=True).to(device)
checkpoint = torch.load(MODEL_FILE, weights_only=True, map_location=device)
model.load_state_dict(checkpoint['model_state_dict'])
data_count = 0
model.eval()
print('Start inference...')
with torch.no_grad():
start_time = time.time()
ok = 0
for x, y in train_loader:
x = x.to(device)
y = y.to(device)
output = model(x)
# 推論結果から、回答を取り出す
ans = torch.argmax(output, dim=2)
# 教師データから、パディング値を除外するための配列を作成する(パディング値なら False となる配列)
mask = y != -1
# 回答から、正解でかつ、パディングデータでないものを抽出する
# * mask の部分は、AND 条件と判定される。
ok += ((ans == y) * mask).sum().item()
# mask の値が、True の項目合計数を求める
data_count += mask.sum().item()
print(f'OK: {ok}, Total: {data_count}, Accuracy: {ok / data_count:.4f}')
elapsed_time = time.time() - start_time
print(f'経過時間: {elapsed_time:.2f} s ({data_count / elapsed_time:.0f} times/s)')
if __name__ == '__main__':
main()
以下のような実行結果となった。
CPU で実行した場合
OK: 428196, Total: 440767, Accuracy: 0.9715
経過時間: 16.66 s (26464 times/s)
T4 GPU で実行した場合
OK: 428196, Total: 440767, Accuracy: 0.9715
経過時間: 4.96 s (88865 times/s)
まとめ
これまで、学習したモデルの正解率を図にしてみる。 レイヤー二層化と、双方向化をしたケースが、最も正解率が高い結果となった。 全体的に、エポック 10 で、正解率が下がっているので、過学習気味なのかもしれない。
推論に掛かる時間は、simple の場合は、5 秒程度で、 二層化+双方向化で、18 秒程度に増加している。 一回の推論は、十分高速であるが、改善の可能性を検討してみたい。 次の記事では推論の効率化を体験してみる。