【PyTorch】Global Average Pooling（GAP）を使用した画像分類の実装方法

# はじめに
**Global** **Average** **Pooling**（**GAP**）はCNNの出力を効果的に圧縮し、過学習を抑制するための重要な手法です。本記事では**GAP**について解説していきます。

# GAPとは
GAPは特にCNNの最終段階で用いられて、入力画像の特徴マップの空間的な次元を削減することで過学習を防ぐ効果があります。
GAPは各特徴マップに対してすべての値の平均を計算します。
さらにGAPは、**分類タスク**において特に有効です。


例）チャネル数が100、画像サイズが7×7の場合
①画像サイズが7×7から49個の値の平均を計算します。
②同じ処理を100チャネル分行います。
③最終的に100個の値からなる1次元のベクトルになります。
# GAPを使う利点
**メリット**
・パラメータ数が大幅に減少するため、学習速度が向上します。
・モデルがシンプルになるため過学習を防げます。
・画像中のオブジェクトの位置に依存しない特徴抽出が可能です。
・モデル全体が一貫して学習されるため、特徴抽出と分類を統一的に行えます。

**デメリット**
・特徴マップの情報を平均化するため、局所的な情報が失われる可能性があります。
・回帰タスクや細かい位置情報が必要なタスクには適さない場合があります。
# 使用するデータ
今回使用するデータはSIGNATEに公開されている [**モノクロ画像の感情分類**](https://signate.jp/competitions/404) です。
こちらテーブルと画像の2種類のデータがあります。
train.zip、test.zip、train_master.tsvをそれぞれダウンロードします。
画像は学習用と評価用がそれぞれ312枚あります。

# 実装方法
以下に全体コードとその説明を記載しています。
今回はGoogle Colabでコードを書いていきます。

まず画像が入っているフォルダのパスを指定します。
```python
from google.colab import drive
drive.mount("/content/drive/")
path = "/content/drive/data" 
```
必要なライブラリをインポートします。
```python
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
```
データフレームの作成し、正解ラベルを数値に変換しています。
さらに画像のファイルのパスを取得し、ソートします。
```python
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クラスを継承したカスタムデータセットクラスを定義します。このようにすることでデータの変換が容易に行えます。
```python
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を作成します。
```python
dataset = NewDataset(file_path,label,transform)
```
データの分割し、データローダーを作成をします。
```python
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（畳み込みニューラルネットワーク）モデルを定義します。
```python
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)
```
損失関数とオプティマイザの定義をします。
```python
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(),lr = 0.001)
```
モデルのトレーニングと評価を行います。
```python
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にプーリングするので、各チャネルの平均値を得ることが可能です。
```python
self.gap = nn.AdaptiveAvgPool2d(1)
x = x.view(x.size(0), -1) 
```

GAP層の出力は（**バッチサイズ、チャネル数、1、1**）の形状を持ちます。
これを全結合層に渡すため（**バッチサイズ、チャネル数**）という形に変化します。
**x** **=** **x.view(x.size(0), -1)** はそのための処理です。

# まとめ
本記事ではGAPを使用した画像分類の実装方法を解説しました。
GAPは画像分類のタスクに特に有効であり、過学習の抑制や学習速度の向上など様々なメリットがあります。
今後、画像分類のタスクを実装する機会があれば、是非ご活用ください。