機械学習ある程度やっている人が稀に遭遇する現象
あれ, testデータの方がtrainデータよりaccuracy高くね?
なんか変じゃね?すごい不安, このままリリースして問題ないの?
この現象, もしかするとdropoutが原因かもしれません。
この記事の内容を要約すると,
「modelのtrain時とeval時でbatch normalizationやdropoutの挙動が異なることから, testの方がtrainよりaccuracyが高くなる現象が発生している場合があるため, 評価方法を見直してみましょう」です。
では実際に, この現象を再現してみましょう。
まずはデータセットを作ります。
import numpy as np
import torch
def make_easy_dataset(X_length):
theta = np.linspace(0,50*np.pi, 5000)
sin = np.sin(theta)
X, y = [], []
for i in range(len(theta) - X_length - 1):
X.append([sin[i : i + X_length]])
label = [0,1]
if sin[i + X_length + 1] > sin[i + X_length]:
label = [1,0]
y.append(label)
return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32)
class MyEasyDataset(torch.utils.data.Dataset):
def __init__(self, data, label, transform=None):
self.transform = transform
self.data_num = data.shape[0]
self.data = torch.tensor(data)
self.label = torch.tensor(label)
def __len__(self):
return self.data_num
def __getitem__(self, idx):
out_data = self.data [idx]
out_label = self.label[idx]
if self.transform:
out_data = self.transform(out_data)
return out_data, out_label
inputはsin波, prediction対象は sin波の値が次に上昇するか否かです。
次はmodelを定義します。
簡単な1次元Convolutionを任意層重ねたもの, 簡単なFullConnect層を任意層重ねたもの, 複数のモデルを束ねるものです。
import torch
from torch import nn
class SimpleCNN1d(nn.Module):
def __init__(self, in_channel:int, out_channel:int, num_block:int, kernel_size:int, stride:int=1, dropout:float=0.0):
super(SimpleCNN1d, self).__init__()
self.in_channel = in_channel
self.out_channel = out_channel
self.num_block = num_block
self.kernel_size = kernel_size
self.dropout = dropout
layers = []
for i in range(num_block):
if i != 0:
layers += [nn.Conv1d(out_channel, out_channel, kernel_size, stride), nn.ReLU()]
else:
layers += [nn.Conv1d( in_channel, out_channel, kernel_size, stride), nn.ReLU()]
if dropout > 0:
layers += [nn.Dropout(dropout)]
self.net = nn.Sequential(*layers)
def forward(self, x):
return self.net(x)
class FC_classifier(nn.Module):
def __init__(self, in_channel:int, in_length:int, out_channel:int, num_block:int, dropout:float=0.0):
super(FC_classifier, self).__init__()
self.in_dim = in_channel * in_length
self.out_channel = out_channel
self.num_block = num_block
self.flatten = nn.Flatten()
layers = []
for i in range(num_block):
in_dim_ = self.in_dim // (i + 1)
if in_dim_ < 1:
in_dim_ = 1
out_dim_ = in_dim_ // 2
if out_dim_ < out_channel or i == num_block-1:
out_dim_ = out_channel
layers += [nn.Linear(self.in_dim, out_dim_)]
if dropout > 0:
layers += [nn.Dropout(dropout)]
layers += [nn.Hardsigmoid()]
self.net = nn.Sequential(*layers)
def forward(self, x):
x = self.flatten(x)
out = self.net(x)
return out
class CombinedNet(nn.Module):
def __init__(self, models):
super(CombinedNet, self).__init__()
layers = []
for model in models:
layers += [model]
self.net = nn.Sequential(*layers)
def forward(self, x):
return self.net(x)
このネットワークを組み合わせて, 実際に学習してみましょう。
dropoutを有効にして, 実際にvalidation(test) accuracyの方がtrain accuracyより大きくなる現象を再現しましょう。
import torch
from sklearn.model_selection import train_test_split
from matplotlib import pyplot as plt
import make_easy_dataset as med
import DNN_module
def accuracy(logits, correct_logits):
indices_max = logits.max(dim=1).indices
indices_correct = correct_logits.max(dim=1).indices
return (indices_max == indices_correct).sum() / logits.shape[0]
def train(dataloader, model, loss_func, optimizer, eval_mode=False):
if eval_mode:
model.eval()
else:
model.train()
ave_loss, num_batch, ave_acc = 0, 0, 0
for data in dataloader:
X, y = data
out = model(X.to(device))
loss = loss_func(out, y.to(device))
acc = accuracy(out, y.to(device))
if not eval_mode:
loss.backward()
optimizer.step()
ave_loss += loss
ave_acc += acc
num_batch += 1
ave_loss /= num_batch
ave_acc /= num_batch
if eval_mode:
model.train()
return float(ave_loss), float(ave_acc)
if __name__ == "__main__":
device = torch.device('cuda:0') if torch.cuda.is_available() else torch.device('cpu')
print(device)
data_len = 150
X, y = med.make_easy_dataset(data_len)
X_train, X_valid, y_train, y_valid = train_test_split(X, y, stratify=y, train_size=0.5)
dataset_train = med.MyEasyDataset(X_train, y_train)
dataset_valid = med.MyEasyDataset(X_valid, y_valid)
dataloader_train = torch.utils.data.DataLoader(dataset_train, batch_size=256)
dataloader_valid = torch.utils.data.DataLoader(dataset_valid, batch_size=256)
num_block = 2
kernel_size = 10
base_model = DNN_module.SimpleCNN1d( in_channel = 1,
out_channel = 1,
num_block = num_block,
kernel_size = kernel_size,
dropout = 0.5,
)
base_out_data_len = data_len - (kernel_size - 1)*num_block
classifier = DNN_module.FC_classifier( in_channel = 1,
in_length = base_out_data_len,
out_channel = 2,
num_block = 1,
dropout = 0.5,
)
combined_model = DNN_module.CombinedNet([base_model, classifier]).to(device)
optimizer = torch.optim.Adam(combined_model.parameters(), lr=1e-4)
loss_func = torch.nn.BCELoss().to(device)
histry_train_loss, histry_train_eval_loss, histry_valid_loss = [], [], []
histry_train_acc , histry_train_eval_acc , histry_valid_acc = [], [], []
epochs = 100
for t in range(epochs):
loss_train , acc_train = train(dataloader_train, combined_model, loss_func, optimizer)
loss_train_eval, acc_train_eval = train(dataloader_train, combined_model, loss_func, optimizer, eval_mode=True)
loss_valid , acc_valid = train(dataloader_valid, combined_model, loss_func, optimizer, eval_mode=True)
histry_train_acc .append(acc_train)
histry_train_eval_acc .append(acc_train_eval)
histry_valid_acc .append(acc_valid)
histry_train_loss .append(loss_train)
histry_train_eval_loss.append(loss_train_eval)
histry_valid_loss .append(loss_valid)
plt.plot(histry_train_acc , color='blue' , label='train acc')
plt.plot(histry_valid_acc , color='orange' , label='valid acc')
plt.plot(histry_train_eval_acc, color='skyblue', label='train eval acc')
plt.legend(); plt.ylim(0,1)
plt.savefig('acc_histry.png'); plt.clf(); plt.close()
plt.plot(histry_train_loss , color='blue' , label='train loss')
plt.plot(histry_valid_loss , color='orange' , label='valid loss')
plt.plot(histry_train_eval_loss, color='skyblue', label='train eval loss')
plt.legend()
plt.savefig('loss_histry.png'); plt.clf(); plt.close()
学習結果を以下に示しました。
通常のtrain(model.train()でmodel(train_data)) : 青色
valid(model.eval()でmodel(valid_data)):オレンジ色
eval modeでtrain(model.eval()でmodel(train_data)):水色
見た目から簡単に分かることとして, 青線だけは水色・オレンジ色とは明らかに異なっています。
青線のみはmodel.train()のtrain modeに入力された結果, 水色・オレンジ色はmodel.eval()のeval modeに入力された結果です。
model.eval()モードという同じ基準下で, 水色(train data)とオレンジ色(valid data)を比較すると, 乖離ない学習曲線が見えます。
同様の現象がLossにも確認できます。
さて, この現象はどうして発生したのか?
理由は, model.train()においてはdropoutが有効で, model.eval()ではdropoutが無効化されているということが原因として挙げられます。
dropoutは, networkの重みを確率的に無視するという機能を持ちます。
dropout値が大きすぎる, あるいは重要なlayerの重みを無視してしまった場合, model.eval()と出力が大きく異なるという現象が発生します。
このような現象に遭遇した場合には, 学習時間が倍程度になりますが, model.train()で学習した後に, model.eval()でdropout無効化してtrainデータを再評価することが必要です。
通常は, 学習時間が2倍になることから, このような評価方法は実施しません。
また, dropoutが有効であっても, 他のnetworkや残った重みがカバーしてくれるため, 一般的なdropout値であれば問題になることは少ないです。
どうしてもtestデータ validationデータがtrain accuracyよりも高いことが不安で, この問題を修正しないと製品リリース許可出なそう, みたいな時には, 是非ともお試しください。