PyTorchにおけるトレーニング、バリデーション、および精度
紹介
ディープラーニングに関して言えば、ほとんどのフレームワークには、トレーニング、バリデーション、および精度に関する関数またはメソッドがプリパッケージされていません。これらの機能を始めることは、多くのエンジニアにとってデータサイエンスの問題に初めて取り組む際の課題となることがあります。ほとんどの場合、これらのプロセスは手動で実装する必要があります。この時点で、それは厄介になります。これらの関数を書くには、実際にプロセスが何を含むのかを理解する必要があります。この初心者向けのチュートリアル記事では、上記のプロセスを高レベルで理論的に検討し、それをPyTorchで実装し、最後にそれらを組み合わせて分類タスクのために畳み込みニューラルネットワークをトレーニングします。
前提条件
- データセット: データセットを準備し、前処理を行います(例:正規化、サイズ変更)。
- データローダー: トレーニングおよび検証データのバッチ処理とシャッフルに
DataLoaderを使用してください。 - モデル:
torch.nn.Moduleを使用してニューラルネットワークモデルを定義します。 - 損失関数: 適切な損失関数を選択してください(例:分類の場合は
nn.CrossEntropyLoss)。 - 最適化手法: 最適化手法を選択します (例:
Adam,SGD). - 訓練ループ: モデルの重みを更新し、損失を計算し、学習率を調整するためのループを実装します。
- 検証ループ: 検証データセットに対するモデルのパフォーマンスを評価します。
- 精度メトリック: 分類タスクのために、
torch.maxを使用して精度を計算します。
インポートとセットアップ
以下は、このタスクで使用するいくつかのインポートされたライブラリです。それぞれはGradient Notebookの深層学習ランタイムにあらかじめインストールされているので、上のリンクを使って無料のGPUでこのチュートリアルを素早く始めてください。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import torchvision.datasets as Datasets
from torch.utils.data import Dataset, DataLoader
import numpy as np
import matplotlib.pyplot as plt
import cv2
from tqdm.notebook import tqdm
if torch.cuda.is_available():
device = torch.device('cuda:0')
print('Running on the GPU')
else:
device = torch.device('cpu')
print('Running on the CPU')
神経ネットワークの解剖学
まず、ニューラルネットワークから派生したモデルについて言及する際に、多層パーセプトロン、畳み込みニューラルネットワーク、または生成的敵対的ネットワークなどであっても、これらのモデルは単に「数値」で構成されており、これらの数値は重みとバイアスで構成され、総称してパラメータと呼ばれます。2000万のパラメータを持つニューラルネットワークは、単に2000万の数値を持つものであり、それぞれの数値がネットワークを通過するデータの各インスタンスに影響を与えます(重みには乗算、バイアスには加算)。この論理に従えば、28 x 28ピクセルの画像が2000万のパラメータを持つ畳み込みニューラルネットワークを通過すると、すべての784ピクセルは実際にすべての2000万のパラメータに遭遇し、何らかの形で変換されることになります。
モデルの目標
以下のカスタムビルドされた畳み込みネットワークを考えてみてください。出力層は2要素のベクトル表現を返すため、その目的はバイナリ分類タスクを解決するのを助けることであると結論付けることができます。
class ConvNet(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 8, 3, padding=1)
self.batchnorm1 = nn.BatchNorm2d(8)
self.conv2 = nn.Conv2d(8, 8, 3, padding=1)
self.batchnorm2 = nn.BatchNorm2d(8)
self.pool2 = nn.MaxPool2d(2)
self.conv3 = nn.Conv2d(8, 32, 3, padding=1)
self.batchnorm3 = nn.BatchNorm2d(32)
self.conv4 = nn.Conv2d(32, 32, 3, padding=1)
self.batchnorm4 = nn.BatchNorm2d(32)
self.pool4 = nn.MaxPool2d(2)
self.conv5 = nn.Conv2d(32, 128, 3, padding=1)
self.batchnorm5 = nn.BatchNorm2d(128)
self.conv6 = nn.Conv2d(128, 128, 3, padding=1)
self.batchnorm6 = nn.BatchNorm2d(128)
self.pool6 = nn.MaxPool2d(2)
self.conv7 = nn.Conv2d(128, 2, 1)
self.pool7 = nn.AvgPool2d(3)
def forward(self, x):
#-------------
# INPUT
#-------------
x = x.view(-1, 3, 32, 32)
#-------------
# LAYER 1
#-------------
output_1 = self.conv1(x)
output_1 = F.relu(output_1)
output_1 = self.batchnorm1(output_1)
#-------------
# LAYER 2
#-------------
output_2 = self.conv2(output_1)
output_2 = F.relu(output_2)
output_2 = self.pool2(output_2)
output_2 = self.batchnorm2(output_2)
#-------------
# LAYER 3
#-------------
output_3 = self.conv3(output_2)
output_3 = F.relu(output_3)
output_3 = self.batchnorm3(output_3)
#-------------
# LAYER 4
#-------------
output_4 = self.conv4(output_3)
output_4 = F.relu(output_4)
output_4 = self.pool4(output_4)
output_4 = self.batchnorm4(output_4)
#-------------
# LAYER 5
#-------------
output_5 = self.conv5(output_4)
output_5 = F.relu(output_5)
output_5 = self.batchnorm5(output_5)
#-------------
# LAYER 6
#-------------
output_6 = self.conv6(output_5)
output_6 = F.relu(output_6)
output_6 = self.pool6(output_6)
output_6 = self.batchnorm6(output_6)
#--------------
# OUTPUT LAYER
#--------------
output_7 = self.conv7(output_6)
output_7 = self.pool7(output_7)
output_7 = output_7.view(-1, 2)
return F.softmax(output_7, dim=1)
この畳み込みネットワークをトレーニングして、猫(ラベル0)を犬(ラベル1)から正しく区別できるようにしたいと仮定します。基本的に、私たちが低レベルでやろうとしているのは、猫の画像がネットワークを通過するたびに、そのすべてのピクセルがこの畳み込みネットワークの197,898のパラメータと相互作用し、出力ベクトルの最初の要素(インデックス0)が2番目の要素(インデックス1)よりも大きくなるようにすることです{例えば [0.65, 0.35]}。そうでなければ、犬の画像が通過すると、2番目の要素(インデックス1)がより大きくなることが期待されます{例えば [0.20, 0.80]}。
今、私たちはモデルの目的の膨大さを認識し始めることができ、はい、私たちが前の段落で説明したことを可能にする数百万の組み合わせの中に197,898の数/パラメータの組み合わせが存在します。この順列を探すことが、単にトレーニングプロセスが含むものです。
正しい組み合わせ
ニューラルネットワークがインスタンス化されるたびに、そのパラメータはすべてランダム化されます。または、特定の技術を通じてパラメータを初期化する場合、それらはランダムではなく、その特定の技術に特有の初期化に従います。それにもかかわらず、初期化時にネットワークパラメータは目標モデルに適合しないため、その状態でモデルを使用するとランダムな分類結果が得られることになります。
今の目標は、目的を達成するために必要な197,898のパラメータの正しい組み合わせを見つけることです。そのために、トレーニング画像をバッチに分割し、各バッチを畳み込みネットワークに通し、分類がどれだけ間違っているかを測定します(フォワードプロパゲーション)。次に、目的に最も合う方向に197,898のパラメータを少しずつ調整し(バックプロパゲーション)、トレーニングデータが尽きるまで他のバッチも同様に繰り返します。このプロセスを最適化と呼びます。
def train(network, training_set, batch_size, optimizer, loss_function):
"""
This function optimizes the convnet weights
"""
# creating list to hold loss per batch
loss_per_batch = []
# defining dataloader
train_loader = DataLoader(training_set, batch_size)
# iterating through batches
print('training...')
for images, labels in tqdm(train_loader):
#---------------------------
# sending images to device
#---------------------------
images, labels = images.to(device), labels.to(device)
#-----------------------------
# zeroing optimizer gradients
#-----------------------------
optimizer.zero_grad()
#-----------------------
# classifying instances
#-----------------------
classifications = network(images)
#---------------------------------------------------
# computing loss/how wrong our classifications are
#---------------------------------------------------
loss = loss_function(classifications, labels)
loss_per_batch.append(loss.item())
#------------------------------------------------------------
# computing gradients/the direction that fits our objective
#------------------------------------------------------------
loss.backward()
#---------------------------------------------------
# optimizing weights/slightly adjusting parameters
#---------------------------------------------------
optimizer.step()
print('all done!')
return loss_per_batch
適切な一般化
最適化されたパラメータがトレーニングセット以外のデータに対して機能することを確実にするためには、異なる画像セットの分類にそれらを利用し、トレーニングセットでのパフォーマンスと同様の比較可能な性能を持つことを確認する必要があります。この際、パラメータに対しての最適化は行われません。このプロセスは検証と呼ばれ、この目的のために使用されるデータセットは検証セットと呼ばれます。
def validate(network, validation_set, batch_size, loss_function):
"""
This function validates convnet parameter optimizations
"""
# creating a list to hold loss per batch
loss_per_batch = []
# defining model state
network.eval()
# defining dataloader
val_loader = DataLoader(validation_set, batch_size)
print('validating...')
# preventing gradient calculations since we will not be optimizing
with torch.no_grad():
# iterating through batches
for images, labels in tqdm(val_loader):
#--------------------------------------
# sending images and labels to device
#--------------------------------------
images, labels = images.to(device), labels.to(device)
#--------------------------
# making classsifications
#--------------------------
classifications = network(images)
#-----------------
# computing loss
#-----------------
loss = loss_function(classifications, labels)
loss_per_batch.append(loss.item())
print('all done!')
return loss_per_batch
パフォーマンスの測定
バランスの取れた訓練セットの文脈で分類タスクに取り組む際、モデルのパフォーマンスは選択した指標としての正確性を使用して最もよく測定されます。ラベルは整数であり、本質的には最高の確率/値を持つインデックスへのポインタですので、正確性を導出するためには、画像が畳み込みネットワークを通過したときの出力ベクトル表現における最大値のインデックスを画像のラベルと比較する必要があります。正確性は訓練セットと検証セットの両方で測定されます。
def accuracy(network, dataset):
"""
This function computes accuracy
"""
# setting model state
network.eval()
# instantiating counters
total_correct = 0
total_instances = 0
# creating dataloader
dataloader = DataLoader(dataset, 64)
# iterating through batches
with torch.no_grad():
for images, labels in tqdm(dataloader):
images, labels = images.to(device), labels.to(device)
#-------------------------------------------------------------------------
# making classifications and deriving indices of maximum value via argmax
#-------------------------------------------------------------------------
classifications = torch.argmax(network(images), dim=1)
#--------------------------------------------------
# comparing indicies of maximum values and labels
#--------------------------------------------------
correct_predictions = sum(classifications==labels).item()
#------------------------
# incrementing counters
#------------------------
total_correct+=correct_predictions
total_instances+=len(images)
return round(total_correct/total_instances, 3)
データセット
これらすべてのプロセスが相互に作用している様子を観察するために、実際のデータセットに適用します。この目的のためにCIFAR-10データセットが使用されます。これは、以下の表に示された10の異なるクラスからの32 x 32ピクセルの画像を含むデータセットです。
データセットは、次のようにPyTorchにロードできます…
loading training data
training_set = Datasets.CIFAR10(root='./', download=True,
transform=transforms.ToTensor())
loading validation data
validation_set = Datasets.CIFAR10(root='./', download=True, train=False,
transform=transforms.ToTensor())
ラベル
説明
飛行機
1
自動車
2
鳥
3
猫
4
鹿
5
dog
6
カエル
7
馬
8
船
9
トラック
畳み込みネットワークアーキテクチャ
これは10クラスの分類タスクであるため、次のコードセルで行われているように、convnetを10要素のベクターを出力するように修正する必要があります。
class ConvNet(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 8, 3, padding=1)
self.batchnorm1 = nn.BatchNorm2d(8)
self.conv2 = nn.Conv2d(8, 8, 3, padding=1)
self.batchnorm2 = nn.BatchNorm2d(8)
self.pool2 = nn.MaxPool2d(2)
self.conv3 = nn.Conv2d(8, 32, 3, padding=1)
self.batchnorm3 = nn.BatchNorm2d(32)
self.conv4 = nn.Conv2d(32, 32, 3, padding=1)
self.batchnorm4 = nn.BatchNorm2d(32)
self.pool4 = nn.MaxPool2d(2)
self.conv5 = nn.Conv2d(32, 128, 3, padding=1)
self.batchnorm5 = nn.BatchNorm2d(128)
self.conv6 = nn.Conv2d(128, 128, 3, padding=1)
self.batchnorm6 = nn.BatchNorm2d(128)
self.pool6 = nn.MaxPool2d(2)
self.conv7 = nn.Conv2d(128, 10, 1)
self.pool7 = nn.AvgPool2d(3)
def forward(self, x):
#-------------
# INPUT
#-------------
x = x.view(-1, 3, 32, 32)
#-------------
# LAYER 1
#-------------
output_1 = self.conv1(x)
output_1 = F.relu(output_1)
output_1 = self.batchnorm1(output_1)
#-------------
# LAYER 2
#-------------
output_2 = self.conv2(output_1)
output_2 = F.relu(output_2)
output_2 = self.pool2(output_2)
output_2 = self.batchnorm2(output_2)
#-------------
# LAYER 3
#-------------
output_3 = self.conv3(output_2)
output_3 = F.relu(output_3)
output_3 = self.batchnorm3(output_3)
#-------------
# LAYER 4
#-------------
output_4 = self.conv4(output_3)
output_4 = F.relu(output_4)
output_4 = self.pool4(output_4)
output_4 = self.batchnorm4(output_4)
#-------------
# LAYER 5
#-------------
output_5 = self.conv5(output_4)
output_5 = F.relu(output_5)
output_5 = self.batchnorm5(output_5)
#-------------
# LAYER 6
#-------------
output_6 = self.conv6(output_5)
output_6 = F.relu(output_6)
output_6 = self.pool6(output_6)
output_6 = self.batchnorm6(output_6)
#--------------
# OUTPUT LAYER
#--------------
output_7 = self.conv7(output_6)
output_7 = self.pool7(output_7)
output_7 = output_7.view(-1, 10)
return F.softmax(output_7, dim=1)
結合プロセス
上記で定義された畳み込みネットワークを訓練するためには、モデルをインスタンス化し、訓練関数を使用して畳み込みネットワークの重みを最適化し、各バッチでの分類の誤り(損失)を記録する必要があります。その後、検証関数を利用して、訓練セット以外のデータに対して畳み込みネットワークが機能することを確認し、同様に分類の誤りを記録します。それにより、訓練セットと検証セットでの畳み込みネットワークの精度を導出し、最適化プロセスを追跡するために各ステップでの損失を記録します。
instantiating model
model = ConvNet()
defining optimizer
optimizer = torch.optim.Adam(model.parameters(), lr=3e-4)
training/optimizing parameters
training_losses = train(network=model, training_set=training_set,
batch_size=64, optimizer=optimizer,
loss_function=nn.CrossEntropyLoss())
validating optimizations
validation_losses = validate(network=model, validation_set=validation_set,
batch_size=64, loss_function=nn.CrossEntropyLoss())
deriving model accuracy on the traininig set
training_accuracy = accuracy(model, training_set)
print(f'training accuracy: {training_accuracy}')
deriving model accuracy on the validation set
validation_accuracy = accuracy(model, validation_set)
print(f'validation accuracy: {validation_accuracy}')
上記のコードブロックを実行すると、トレーニングとバリデーションの精度が理想的でない可能性が高いです。しかし、最初の4行のコードを取り出して新しいコードセルに貼り付け、トレーニング、バリデーション、精度の関数を再実行すると、パフォーマンスが向上します。このデータセット全体を何度も通過するプロセスは、エポックとして知られています。基本的に、プロセスを5回実行すれば、モデルは5エポックのトレーニングを受けたことになります。
最初の4行のコードを移動する理由は、トレーニング関数を実行する時点で、モデル内のランダムに初期化された重みが最適化されているからです。その後、モデルのインスタンス化を同じセルのままでコードセルを再実行すると、その重みは再びランダム化され、以前の最適化が無効になります。すべてのプロセスが適切に同期されていることを確実にするために、それを関数やクラスにパッケージ化するのが良いアイデアです。個人的には、すべてのプロセスをきれいにまとめておけるのでクラスを使うことを好みます。
class ConvolutionalNeuralNet():
def __init__(self, network):
self.network = network.to(device)
self.optimizer = torch.optim.Adam(self.network.parameters(), lr=1e-3)
def train(self, loss_function, epochs, batch_size,
training_set, validation_set):
# creating log
log_dict = {
'training_loss_per_batch': [],
'validation_loss_per_batch': [],
'training_accuracy_per_epoch': [],
'validation_accuracy_per_epoch': []
}
# defining weight initialization function
def init_weights(module):
if isinstance(module, nn.Conv2d):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
elif isinstance(module, nn.Linear):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
# defining accuracy function
def accuracy(network, dataloader):
network.eval()
total_correct = 0
total_instances = 0
for images, labels in tqdm(dataloader):
images, labels = images.to(device), labels.to(device)
predictions = torch.argmax(network(images), dim=1)
correct_predictions = sum(predictions==labels).item()
total_correct+=correct_predictions
total_instances+=len(images)
return round(total_correct/total_instances, 3)
# initializing network weights
self.network.apply(init_weights)
# creating dataloaders
train_loader = DataLoader(training_set, batch_size)
val_loader = DataLoader(validation_set, batch_size)
# setting convnet to training mode
self.network.train()
for epoch in range(epochs):
print(f'Epoch {epoch+1}/{epochs}')
train_losses = []
# training
print('training...')
for images, labels in tqdm(train_loader):
# sending data to device
images, labels = images.to(device), labels.to(device)
# resetting gradients
self.optimizer.zero_grad()
# making predictions
predictions = self.network(images)
# computing loss
loss = loss_function(predictions, labels)
log_dict['training_loss_per_batch'].append(loss.item())
train_losses.append(loss.item())
# computing gradients
loss.backward()
# updating weights
self.optimizer.step()
with torch.no_grad():
print('deriving training accuracy...')
# computing training accuracy
train_accuracy = accuracy(self.network, train_loader)
log_dict['training_accuracy_per_epoch'].append(train_accuracy)
# validation
print('validating...')
val_losses = []
# setting convnet to evaluation mode
self.network.eval()
with torch.no_grad():
for images, labels in tqdm(val_loader):
# sending data to device
images, labels = images.to(device), labels.to(device)
# making predictions
predictions = self.network(images)
# computing loss
val_loss = loss_function(predictions, labels)
log_dict['validation_loss_per_batch'].append(val_loss.item())
val_losses.append(val_loss.item())
# computing accuracy
print('deriving validation accuracy...')
val_accuracy = accuracy(self.network, val_loader)
log_dict['validation_accuracy_per_epoch'].append(val_accuracy)
train_losses = np.array(train_losses).mean()
val_losses = np.array(val_losses).mean()
print(f'training_loss: {round(train_losses, 4)} training_accuracy: '+
f'{train_accuracy} validation_loss: {round(val_losses, 4)} '+
f'validation_accuracy: {val_accuracy}\n')
return log_dict
def predict(self, x):
return self.network(x)
上記のクラスでは、train()メソッドの中でトレーニングプロセスとバリデーションプロセスを統合しています。これは、これらのプロセスが相互に連携して動作するため、複数の属性呼び出しを行うことを防ぐためです。また、精度がtrain()メソッド内のヘルパー関数として追加されており、トレーニングとバリデーションの直後に精度が計算されることにも注意してください。ウェイト初期化機能も定義されており、Xavierウェイト初期化手法を使用してウェイトを初期化するのに役立ちます。ネットワークのウェイトの初期化方法に対してある程度の制御を持つことは、通常良いプラクティスとされています。さらに、ConvNetのトレーニング中にすべての損失と精度を追跡するためのメトリックログも定義されています。
training model
model = ConvolutionalNeuralNet(ConvNet())
log_dict = model.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64,
training_set=training_set, validation_set=validation_set)
上記のパラメータを用いて10エポック(10サイクル)で畳み込みニューラルネットワークをトレーニングした結果、以下のメトリクスが得られました。トレーニングと検証の精度は、トレーニングの過程で向上し、これは畳み込みネットワークのパラメータが適切に調整・最適化されていることを示しています。検証精度は約58%から始まり、10エポック目には72%に達しました。
しかし、検証曲線が平坦になり始めたとはいえ、コンボリューショナルネットワークは徹底的にはトレーニングされていないことに注意すべきです。まだ上昇トレンドにあるため、過学習が発生する前にコンボリューショナルネットワークはさらに数エポックのトレーニングを受けることができるでしょう。損失のプロットも同様の状況を示しており、10エポックの終わりにおけるバッチごとの検証損失は依然として下降トレンドにあります。
最後の remarks
この記事では、ニューラルネットワークの訓練における3つの重要なプロセス、訓練、検証、精度について探りました。それぞれのプロセスが何を含むのか、またそれらをPyTorchでどのように実装できるのかを高いレベルで説明しました。そして、これら3つのプロセスをクラスに統合し、畳み込みニューラルネットワークの訓練に使用しました。読者は、今後自分のPyTorchコードでこれらの機能を実装できるようになることを期待するべきです。



