畳み込みニューラルネットワークにおけるバッチ正規化
バッチ正規化は、畳み込みニューラルネットワークにおいて一般的に言及される用語です。この記事では、それが実際に何を意味するのか、そして畳み込みニューラルネットワークのパフォーマンスや全体的な挙動に対する影響があるのかどうかを探っていきます。
前提条件
- Python:ここでコードを実行するには、あなたのマシンにPythonがインストールされている必要があります。読者は続ける前に基本的なPythonコーディングの経験が必要です。
- ディープラーニングの基礎:この記事では、ディープラーニング理論を適用するために重要な概念を扱っており、読者は関連する用語や基本的な理論に関してある程度の経験があることが期待されます。
用語の正規化
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
import seaborn as sns
from torchvision.utils import make_grid
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')
統計における正規化は、データまたは値のセットを0から1の範囲内に制約するプロセスを指します。やや不便なことに、一部では正規化はデータの分布の平均をゼロに、標準偏差を1に設定するプロセスも指します。
実際には、分布の平均を0に、標準偏差を1に設定するこのプロセスは、標準化と呼ばれます。しかし、いくつかの例外があるため、正規化またはzスコア正規化とも呼ばれます。この区別を学び、それを心に留めておくことが重要です。
データ前処理
データ前処理とは、機械学習や深層学習アルゴリズムにデータを供給する前にデータを準備するために行うステップを指します。前のセクションで言及された2つのプロセス(正規化と標準化)は、データ前処理のステップです。
最小-最大正規化
最小-最大正規化は、データを正規化する最も一般的な方法の1つです。その名前の通り、データセット内の最小値を0、最大値を1に設定し、それに応じてデータポイントを0から1の範囲内に制約します。以下の方程式は、最小-最大正規化プロセスの数学的な説明を提供します。基本的には、データセット内の最小値を各データポイントから引き、その後に範囲(最大値 - 最小値)で割ることを含みます。
以下の関数を使うことで、最小最大正規化のプロセスを再現できます。この関数を利用することで、背後で実際に何が起こっているのかについての直感を養うことができます。
def min_max_normalize(data_points: np.array):
"""
This function normalizes data by constraining
data points between the range of 0 & 1
"""
# convert list to numpy array
if type(data_points) == list:
data_points = np.array(data_points)
else:
pass
# create a list to hold normalized data
normalized = []
# derive minimum and maximum values
minimum = data_points.min()
maximum = data_points.max()
# convert to list for iteration
data_points = list(data_points)
# normalizing data
for value in data_points:
normalize = (value-minimum)/(maximum-minimum)
normalized.append(round(normalize, 2))
return np.array(normalized)
NumPyを使用してランダムな値の配列を作成し、その後、上で定義した最小-最大正規化関数を使用してそれらを正規化してみましょう。
# creating a random set of data points
data = np.random.rand(50)*20
# normalizing data points
normalized = min_max_normalize(data)
下のプロットから、正規化の前は値が0から20の範囲であり、データポイントの大多数が5から10の間にあることがわかります。しかし、正規化後は、値が0から1の間に変わり、大多数のデータポイントの値が0.25から0.5の間にあることがわかります。注:このコードを実行すると、データ分布はこの記事で使用されているものとは異なるので注意してください。これはランダムに生成されています。
# visualising distribution
figure, axes = plt.subplots(1, 2, sharey=True, dpi=100)
sns.histplot(data, ax=axes[0])
axes[0].set_title('unnormalized')
sns.histplot(normalized, ax=axes[1])
axes[1].set_title('min-max normalized')
Zスコア正規化
Zスコア正規化、または標準化とは、データ分布の平均を0、標準偏差を1に設定するプロセスです。以下の方程式は、Zスコア正規化を支配する数学的な方程式で、正規化する値から分布の平均を引いた後、分布の標準偏差で割ることを含みます。
下記に定義された関数はzスコア正規化プロセスを再現します。この関数を使用することで、実際に何を含むのかをより詳しく見ることができます。
def z_score_normalize(data_points: np.array):
"""
This function normalizes data by computing
their z-scores
"""
# convert list to numpy array
if type(data_points) == list:
data_points = np.array(data_points)
else:
pass
# create a list to hold normalized data
normalized = []
# derive mean and and standard deviation
mean = data_points.mean()
std = data_points.std()
# convert to list for iteration
data_points = list(data_points)
# normalizing data
for value in data_points:
normalize = (value-mean)/std
normalized.append(round(normalize, 2))
return np.array(normalized)
前のセクションで生成されたデータ分布を使用して、z-score関数を用いてデータポイントを正規化してみましょう。
# normalizing data points
z_normalized = z_score_normalize(data)
# check the mean value
z_normalized.mean()
>>>> -0.0006
# check the standard deviation
z_normalized.std()
>>>> 1.0000
再度、視覚化から、元の分布の値が0から20の範囲であるのに対し、zスコアで正規化された値は現在0を中心に(平均0)-1.5から1.5の範囲にあることがわかります。これはより適切な範囲です。
# visualizing distributions
figure, axes = plt.subplots(1, 2, sharey=True, dpi=100)
sns.histplot(data, ax=axes[0])
axes[0].set_title('unnormalized')
sns.histplot(z_normalized, ax=axes[1])
axes[1].set_title('z-score normalized')
前処理の理由
機械学習におけるデータを考えると、個々のデータポイントを特徴として見ます。これらの特徴は通常、同じ尺度にありません。例えば、3つの寝室と400平方フィートの大きさのリビングルームがある家を考えてみましょう。これら2つの特徴は、あまりにも異なる尺度にあるため、勾配降下法で最適化される予定の機械学習アルゴリズムに投げ込むと、最適化は非常に面倒になります。大きな尺度を持つ特徴が他のすべてよりも優先されるからです。最適化プロセスを容易にするためには、すべてのデータポイントを同じ尺度内に収めておくことが良いアイデアです。
畳み込み層における正規化
画像内のデータポイントはピクセルです。ピクセルの値は通常0から255の範囲です。そのため、畳み込みニューラルネットワークに画像を投入する前に、すべてのピクセルを管理可能な範囲に収めるような方法で正規化することが良い考えです。
たとえこれが行われたとしても、畳み込みネットワークを訓練する際には、重み(フィルター内の要素)が大きくなりすぎてしまう可能性があり、その結果、広い範囲にわたってピクセルが分散されたフィーチャーマップが生成されることがあります。これにより、前処理ステップで行われた正規化はほぼ無意味になります。さらに、これは最適化プロセスを妨げ、遅くするか、ひどい場合には不安定な勾配という問題を引き起こし、畳み込みネットワークが重みをさらに最適化することを根本的に妨げる可能性があります。
この問題を防ぐために、各層に正規化が導入されます。この正規化はバッチ正規化と呼ばれます。
バッチ正規化のプロセス
これらの2つのパラメータ、アルファとベータは学習可能なパラメータであり、コネクショナルネットワークはこれらを使用して特徴マップのピクセル値が管理可能な範囲内に収まるようにし、勾配の不安定性の問題を改善します。
バッチ正規化の実践
畳み込み層におけるバッチ正規化の効果を正しく評価するためには、バッチ正規化なしの畳み込みネットワークとバッチ正規化ありの畳み込みネットワークの2つをベンチマークする必要があります。これには、LeNet-5アーキテクチャとMNISTデータセットを使用します。
データセットと畳み込みニューラルネットワーククラス
この記事では、以前に述べたように、ベンチマーク目的でMNISTデータセットが使用されます。このデータセットは、手書きの数字0から9までの画像で構成されており、各画像は28 x 28ピクセルのサイズです。
MNISTデータセットのサンプル画像。
以下のコードブロックを使用して、PyTorchにロードできます。トレーニングセットは60,000枚の画像で構成されており、検証セットは10,000枚の画像で構成されています。このデータセットをLeNet-5と一緒に使用するため、画像はtransformsパラメータで定義されているように32 x 32ピクセルにリサイズする必要があります。
# loading training data
training_set = Datasets.MNIST(root='./', download=True,
transform=transforms.Compose([transforms.ToTensor(),
transforms.Resize((32, 32))]))
# loading validation data
validation_set = Datasets.MNIST(root='./', download=True, train=False,
transform=transforms.Compose([transforms.ToTensor(),
transforms.Resize((32, 32))]))
私たちのコンボリューショナルネットワークのトレーニングと活用のために、‘ConvolutionalNeuralNet()’という適切に名付けられたクラスを使用します。このクラスには、トレーニングされたコンボリューショナルネットワークを使用してインスタンスをトレーニングおよび分類するのに役立つメソッドが含まれています。train()メソッドには、init_weights()やaccuracyのような内部ヘルパー関数も含まれています。
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)
Lenet-5
LeNet-5(Y. Lecun et al)は、手書きの数字の画像を認識/分類するために特別に設計された最も初期の畳み込みニューラルネットワークの一つです。そのアーキテクチャは上の画像に示されており、PyTorchでの実装は次のコードブロックに提供されています。
class LeNet5(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 6, 5)
self.pool1 = nn.AvgPool2d(2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.pool2 = nn.AvgPool2d(2)
self.linear1 = nn.Linear(5*5*16, 120)
self.linear2 = nn.Linear(120, 84)
self.linear3 = nn. Linear(84, 10)
def forward(self, x):
x = x.view(-1, 1, 32, 32)
#----------
# LAYER 1
#----------
output_1 = self.conv1(x)
output_1 = torch.tanh(output_1)
output_1 = self.pool1(output_1)
#----------
# LAYER 2
#----------
output_2 = self.conv2(output_1)
output_2 = torch.tanh(output_2)
output_2 = self.pool2(output_2)
#----------
# FLATTEN
#----------
output_2 = output_2.view(-1, 5*5*16)
#----------
# LAYER 3
#----------
output_3 = self.linear1(output_2)
output_3 = torch.tanh(output_3)
#----------
# LAYER 4
#----------
output_4 = self.linear2(output_3)
output_4 = torch.tanh(output_4)
#-------------
# OUTPUT LAYER
#-------------
output_5 = self.linear3(output_4)
return(F.softmax(output_5, dim=1))
上述の定義されたLeNet-5アーキテクチャを使用して、コードブロックに表示されているパラメータを持つ畳み込みニューラルネットクラスのメンバーであるmodel_1をインスタンス化します。このモデルはベンチマーク目的の基準として機能します。
# training model 1
model_1 = ConvolutionalNeuralNet(LeNet5())
log_dict_1 = model_1.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64,
training_set=training_set, validation_set=validation_set)
10エポックのトレーニングを行い、得られたメトリックログから精度を可視化した後、トレーニング精度と検証精度の両方がトレーニングの過程で向上したことがわかります。我々の実験では、検証精度は最初のエポック後に約93%で始まり、その後の9回の繰り返しの間に着実に増加し、エポック10では98%をわずかに超えるところで終了しました。
sns.lineplot(y=log_dict_1['training_accuracy_per_epoch'], x=range(len(log_dict_1['training_accuracy_per_epoch'])), label='training')
sns.lineplot(y=log_dict_1['validation_accuracy_per_epoch'], x=range(len(log_dict_1['validation_accuracy_per_epoch'])), label='validation')
plt.xlabel('epoch')
plt.ylabel('accuracy')
バッチ正規化されたLeNet-5
この記事のテーマは畳み込み層におけるバッチノーマライゼーションに焦点を当てているため、バッチノームは上記の画像に示されているこのアーキテクチャ内の2つの畳み込み層にのみ適用されます。
class LeNet5_BatchNorm(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 6, 5)
self.batchnorm1 = nn.BatchNorm2d(6)
self.pool1 = nn.AvgPool2d(2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.batchnorm2 = nn.BatchNorm2d(16)
self.pool2 = nn.AvgPool2d(2)
self.linear1 = nn.Linear(5*5*16, 120)
self.linear2 = nn.Linear(120, 84)
self.linear3 = nn. Linear(84, 10)
def forward(self, x):
x = x.view(-1, 1, 32, 32)
#----------
# LAYER 1
#----------
output_1 = self.conv1(x)
output_1 = torch.tanh(output_1)
output_1 = self.batchnorm1(output_1)
output_1 = self.pool1(output_1)
#----------
# LAYER 2
#----------
output_2 = self.conv2(output_1)
output_2 = torch.tanh(output_2)
output_2 = self.batchnorm2(output_2)
output_2 = self.pool2(output_2)
#----------
# FLATTEN
#----------
output_2 = output_2.view(-1, 5*5*16)
#----------
# LAYER 3
#----------
output_3 = self.linear1(output_2)
output_3 = torch.tanh(output_3)
#----------
# LAYER 4
#----------
output_4 = self.linear2(output_3)
output_4 = torch.tanh(output_4)
#-------------
# OUTPUT LAYER
#-------------
output_5 = self.linear3(output_4)
return(F.softmax(output_5, dim=1))
以下のコードセグメントを使用して、バッチ正規化を含むmodel_2をインスタンス化し、model_1と同じパラメータでトレーニングを開始できます。その後、精度スコアを出力します…
# training model 2
model_2 = ConvolutionalNeuralNet(LeNet5_BatchNorm())
log_dict_2 = model_2.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64,
training_set=training_set, validation_set=validation_set)
プロットを見ると、バッチ正規化のないモデルと同様に、トレーニングと検証の精度がトレーニングの過程でどちらも増加したことが明らかです。最初のエポックの後、検証精度は95%を少し上回っており、同じ時点でのmodel_1よりも3パーセントポイント高く、その後徐々に増加し、約98.5%に達し、model_1よりも0.5%高くなりました。
sns.lineplot(y=log_dict_2['training_accuracy_per_epoch'], x=range(len(log_dict_2['training_accuracy_per_epoch'])), label='training')
sns.lineplot(y=log_dict_2['validation_accuracy_per_epoch'], x=range(len(log_dict_2['validation_accuracy_per_epoch'])), label='validation')
plt.xlabel('epoch')
plt.ylabel('accuracy')
モデルの比較
両モデルを比較すると、バッチ正規化された畳み込み層を持つLeNet-5モデルが、バッチ正規化された畳み込み層なしの通常モデルを上回ったことが明らかです。したがって、この場合、バッチ正規化がパフォーマンスの向上に寄与したと言っても安全です。
正規のLeNet-5モデルとバッチ正規化されたLeNet-5モデルのトレーニング損失と検証損失を比較すると、バッチ正規化モデルは通常のモデルよりも早く低い損失値を達成することがわかります。これは、バッチ正規化がモデルが重みを適切な方向に最適化する速度を増加させることを示唆しており、言い換えれば、バッチ正規化はConvNetが学習する速度を増加させます。
トレーニングと検証の損失。
最後の remarks
この記事では、機械学習/深層学習の文脈における正規化が何を意味するのかを探りました。また、データ前処理のステップとしての正規化プロセスや、バッチ正規化のプロセスを通じて正規化を前処理を超えて畳み込み層にどのように適用できるかについても検討しました。
その後、バッチ正規化のプロセス自体を調査し、MNISTデータセット上でバッチ正規化のないLeNet-5畳み込みネットワークとバッチ正規化のあるものの2つのバリエーションを比較して、その効果を評価しました。結果から、バッチ正規化がパフォーマンスの向上と重みの最適化速度の向上に寄与したことが推測されました。また、内部共変量シフトを防ぐという提案もありましたが、その意見には合意が得られていない可能性もあります。












