はじめに
Global Average Pooling(GAP)はCNNの出力を効果的に圧縮し、過学習を抑制するための重要な手法です。本記事ではGAPについて解説していきます。
GAPとは
GAPは特にCNNの最終段階で用いられて、入力画像の特徴マップの空間的な次元を削減することで過学習を防ぐ効果があります。
GAPは各特徴マップに対してすべての値の平均を計算します。
さらにGAPは、分類タスクにおいて特に有効です。
例)チャネル数が100、画像サイズが7×7の場合
①画像サイズが7×7から49個の値の平均を計算します。
②同じ処理を100チャネル分行います。
③最終的に100個の値からなる1次元のベクトルになります。
GAPを使う利点
メリット
・パラメータ数が大幅に減少するため、学習速度が向上します。
・モデルがシンプルになるため過学習を防げます。
・画像中のオブジェクトの位置に依存しない特徴抽出が可能です。
・モデル全体が一貫して学習されるため、特徴抽出と分類を統一的に行えます。
デメリット
・特徴マップの情報を平均化するため、局所的な情報が失われる可能性があります。
・回帰タスクや細かい位置情報が必要なタスクには適さない場合があります。
使用するデータ
今回使用するデータはSIGNATEに公開されている モノクロ画像の感情分類 です。
こちらテーブルと画像の2種類のデータがあります。
train.zip、test.zip、train_master.tsvをそれぞれダウンロードします。
画像は学習用と評価用がそれぞれ312枚あります。
実装方法
以下に全体コードとその説明を記載しています。
今回はGoogle Colabでコードを書いていきます。
まず画像が入っているフォルダのパスを指定します。
from google.colab import drive
drive.mount("/content/drive/")
path = "/content/drive/data"
必要なライブラリをインポートします。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import glob
from PIL import Image
from sklearn.preprocessing import LabelEncoder
import torch
from torch import optim,nn
from torchvision import transforms
from torch.utils.data import DataLoader,Dataset
データフレームの作成し、正解ラベルを数値に変換しています。
さらに画像のファイルのパスを取得し、ソートします。
df = pd.read_csv(f"{path}/train_master.tsv",sep = "\t")
le = LabelEncoder()
label = le.fit_transform(df["expression"])
file_path = sorted(glob.glob(f"{path}/train/*jpg"))
PytorchのDatasetクラスを継承したカスタムデータセットクラスを定義します。このようにすることでデータの変換が容易に行えます。
class NewDataset(Dataset):
def __init__(self,file_path,label,transform = None):
self.file_path = file_path
self.label = label
self.transform = transform
def __len__(self):
return len(self.file_path)
def __getitem__(self,idx):
file_path = self.file_path[idx]
image = Image.open(file_path)
label = self.label[idx]
if self.transform:
image = self.transform(image)
return image,label
transform = transforms.Compose([
transforms.Resize((128,128)),
transforms.ToTensor(),
transforms.Normalize(0.5,0.5)
])
先ほど定義したNewDatasetクラスを使って、画像ファイルパス、ラベル、変換を渡し、新しいデータセットインスタンスdatasetを作成します。
dataset = NewDataset(file_path,label,transform)
データの分割し、データローダーを作成をします。
train,test = train_test_split(dataset,test_size = 0.2)
train_batch = DataLoader(
train,
batch_size = 16,
shuffle = True,
num_workers = 2
)
test_batch = DataLoader(
test,
batch_size = 16,
shuffle = False,
num_workers = 2
)
PyTorchのnn.Moduleを継承したカスタムCNN(畳み込みニューラルネットワーク)モデルを定義します。
class Model(nn.Module):
def __init__(self):
super(Model,self).__init__()
self.conv1 = nn.Conv2d(1,64,kernel_size = 3,stride = 1,padding = 1)
self.conv2 = nn.Conv2d(64,64,kernel_size = 3,stride = 1,padding = 1)
self.conv3 = nn.Conv2d(64,128,kernel_size = 3,stride = 1,padding = 1)
self.conv4 = nn.Conv2d(128,128,kernel_size = 3,stride = 1,padding = 1)
self.conv5 = nn.Conv2d(128,256,kernel_size = 3,stride = 1,padding = 1)
self.conv6 = nn.Conv2d(256,256,kernel_size = 3,stride = 1,padding = 1)
self.pool = nn.MaxPool2d(2)
self.relu = nn.ReLU()
self.drop = nn.Dropout2d(0.3)
self.gap = nn.AdaptiveAvgPool2d(1)
self.fc1 = nn.Linear(256,4)
def forward(self,x):
x = self.relu(self.conv1(x))
x = self.relu(self.conv2(x))
x = self.drop(x)
x = self.pool(x)
x = self.relu(self.conv3(x))
x = self.relu(self.conv4(x))
x = self.pool(x)
x = self.drop(x)
x = self.relu(self.conv5(x))
x = self.relu(self.conv6(x))
x = self.pool(x)
x = self.drop(x)
x = self.gap(x)
x = x.view(x.size(0), -1)
x = self.fc1(x)
return x
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = Model().to(device)
損失関数とオプティマイザの定義をします。
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(),lr = 0.001)
モデルのトレーニングと評価を行います。
train_loss_list = []
test_loss_list = []
train_acc_list = []
test_acc_list = []
epochs = 50
for epoch in range(1,epochs+1):
print("----------------------")
print(f"{epoch}/{epochs}")
train_loss = 0
test_loss = 0
train_acc = 0
test_acc = 0
model.train()
for data,label in train_batch:
data = data.to(device)
label = label.to(device)
optimizer.zero_grad()
ypred = model(data)
loss = criterion(ypred,label)
loss.backward()
optimizer.step()
train_loss += loss.item()
ypred_label = torch.max(ypred,1)[1]
train_acc += torch.sum(ypred_label == label).item() / len(label)
epoch_train_loss = train_loss / len(train_batch)
epoch_train_acc = train_acc / len(train_batch)
model.eval()
with torch.no_grad():
for data,label in test_batch:
data = data.to(device)
label = label.to(device)
ypred = model(data)
loss = criterion(ypred,label)
test_loss += loss.item()
ypred_label = torch.max(ypred,1)[1]
test_acc += torch.sum(ypred_label == label).item() /len(label)
epoch_test_loss = test_loss / len(test_batch)
epoch_test_acc = test_acc / len(test_batch)
print(f"train_loss:{epoch_train_loss}")
print(f"train_acc:{epoch_train_acc}")
print(f"test_loss:{epoch_test_loss}")
print(f"test_acc:{epoch_test_acc}")
train_loss_list.append(epoch_train_loss)
train_acc_list.append(epoch_train_acc)
test_loss_list.append(epoch_test_loss)
test_acc_list.append(epoch_test_acc)
GAP使用部分の解説
nn.AdaptiveAvgPool2dの引数に指定したい高さと幅の値を入れます。
nn.AdaptiveAvgPool2d(1)とすることで、各チャネルを1×1にプーリングするので、各チャネルの平均値を得ることが可能です。
self.gap = nn.AdaptiveAvgPool2d(1)
x = x.view(x.size(0), -1)
GAP層の出力は(バッチサイズ、チャネル数、1、1)の形状を持ちます。
これを全結合層に渡すため(バッチサイズ、チャネル数)という形に変化します。
x = x.view(x.size(0), -1) はそのための処理です。
まとめ
本記事ではGAPを使用した画像分類の実装方法を解説しました。
GAPは画像分類のタスクに特に有効であり、過学習の抑制や学習速度の向上など様々なメリットがあります。
今後、画像分類のタスクを実装する機会があれば、是非ご活用ください。