0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python初心者の備忘録 #28 ~深層学習超入門編04~

0
Last updated at Posted at 2026-03-31

はじめに

今回私は最近はやりのchatGPTに興味を持ち、深層学習について学んでみたいと思い立ちました!
深層学習といえばPythonということなので、最終的にはPythonを使って深層学習ができるとこまでコツコツと学習していくことにしました。
ただ、勉強するだけではなく少しでもアウトプットをしようということで、備忘録として学習した内容をまとめていこうと思います。
この記事が少しでも誰かの糧になることを願っております!
※投稿主の環境はWindowsなのでMacの方は多少違う部分が出てくると思いますが、ご了承ください。
最初の記事:Python初心者の備忘録 #01
前の記事:Python初心者の備忘録 #27 ~深層学習超入門編03~
次の記事:まだ

今回はCNNの基礎Max Poolingについてまとめております。

■学習に使用している資料

Udemy:①米国AI開発者がやさしく教える深層学習超入門第二弾【Pythonで実践】

■CNN(Convolutional Neural Network)

▶畳み込み層(Convolutional Layer)

  • CNNでは畳み込み層を利用してモデルの構築を行っていく
  • 畳み込み層は位置情報を維持したまま学習することが可能で、主に画像認識で使用される
  • 畳み込み層をいくつも重ねることでCNNを構築していく

image.png
※今までの記事で紹介していた全結合では、データを1列に変換してから学習していたので、どの要素がどこにあるのかは判別できなくなっていた

▶畳み込み層の操作

  • フィルタ(カーネル)と呼ばれる小さな行列を入力データ上をスライドし、フィルタと入力データの要素ごとの積の総和(畳み込み)を計算する
    ※カーネルをどれだけずらすかはハイパーパラメータ次第

image.png

  • 畳み込みは、入力(データ)からフィルタによって特徴量を抽出している
    例えば[上段が-1、中段は0、下段は1]のようなフィルタで畳み込みを行えば、横のエッジが強調され、全ての段が[-1, 0 1]のようなフィルタだと縦のエッジが強調される

横のエッジを強調
image.png
image.png
image.png
縦エッジを強調
image.png

Pythonで畳み込み操作を実装

  • 任意のフィルターで畳み込み処理を行う関数を実装する
    ※データはMNISTを使用

image.png

import time

import torch
from torch import nn
from torch.nn import functional as F
from torch.utils.data import DataLoader
from torch import optim
import torchvision
from torchvision import transforms
import matplotlib.pyplot as plt

%load_ext autoreload
%autoreload 2
import utils

# MNISTの画像を使用
dataset = torchvision.datasets.MNIST(root='./mnist_data', train=True, download=True)
im, label = dataset[0]
im = transforms.ToTensor()(im)[0, :, :]

# フィルターを設定
top_edge_filter = torch.tensor([[-1, -1, -1],
                               [0, 0, 0],
                               [1, 1, 1]])

# データの単位当たりの値を描画可能
import pandas as pd
df = pd.DataFrame(im)
df.style.format(precision=2).set_properties(**{'font-size':'7pt'}).background_gradient('Greys_r')

image.png

# 畳み込み処理
def apply_filter(im, filter):
    im_h, im_w = im.shape
    f_h, f_w = filter.shape
    output_data = []
    # filterは3×3なので、データのshapeいっぱいに繰り返さなくても全体をfilteringできる
    for i in range(im_h - f_h + 1):
        row = []
        for j in range(im_w - f_w + 1):
            row.append((im[i:i+f_h, j:j+f_w] * filter).sum().item())
        output_data.append(row)
    return torch.tensor(output_data)

# 畳み込みを実施
filtered_im = apply_filter(im, top_edge_filter)

# 畳み込みの処理後を描画
plt.imshow(filtered_im, cmap='gray')

image.png

# left edgeフィルタでも同様に畳み込み処理を行う
left_edge_filter = torch.tensor([[-1, 0, 1],
                               [-1, 0, 1],
                               [-1, 0, 1]])
filtered_im = apply_filter(im, left_edge_filter)
plt.imshow(filtered_im, cmap='gray')

image.png

▶畳み込み層のパラメータ

  • フィルターの値が重みのパラメータとなるが、実際にはバイアス項も存在する
  • フィルターで計算された値に対してバイアスが加算される

▶複数チャネルの畳み込み層

  • 今までは1つの畳み込み層で1つのフィルターを使用して出力していたが、実際は1つの畳み込み層で複数フィルターを適用して、複数のチャネルとしてstackして出力する

image.png

  • もちろん入力が複数チャネルの場合もあり、それぞれのチャネルに対してそれぞれのフィルターで処理を行い、チャネル毎に足し合わせて各フィルター毎に1つのチャネルを出力する

image.png

▶畳み込み層の図

  • 一般的に2つの図式方法があり、LeNetとAlexNetという2つのモデルの論文内で使用された図式がそのままスタンダードとなっている
    ※それぞれ特定の論文をもとにしているだけなので、論文によっては独自の表現方法がされている

image.png

上記図を簡単に描画できるサイト
https://alexlenail.me/NN-SVG/LeNet.html

▶ストライド(stride)

  • 畳み込み層におけるハイパーパラメータの1つ
  • フィルターをスライドする際のステップ数のことで、端数に対しては処理を行わず無視をする(出力に反映されない)

image.png

▶パディング(padding)

  • 畳み込み層におけるハイパーパラメータの1つ
  • 入力データの周囲に一定の幅の枠を追加することで、一般的に追加した枠は0(黒)とする(ゼロパディング)
  • パディングによって出力サイズと入力サイズを一致させる(大きさを維持する)ことが可能で、端数が失われることを防ぐこともできる

image.png

▶畳み込み層の出力サイズ

  • 出力サイズの計算式は下記のようになる

image.png

▶CNN

  • 畳み込み層を主要構成要素とするNNで、主に画像認識などに使われる
  • ストライド=2、パディング=1、フィルタサイズ=3を設定して、サイズを半分にしていくことが多く、フィルタの数は倍増していくことが多い
    ※サイズは半分に圧縮しているが様々なフィルタを利用して、多くの情報(特徴量)を残していく
  • 最後の方の層は畳み込み層ではなく、全結合を用いるのが一般的
  • 層が深くなるにつれ、情報が圧縮されているイメージ

image.png

畳み込み層とReLU層

  • 通常、畳み込み層の後にReLU層を組み合わせる
  • ReLUによって非線形となり、より複雑なパターンをとらえることが可能になる
  • 一部の出力(負の値)が0になることで、スパース性が増し、モデルの表現力は向上し、過学習を防ぐことができる

image.png
※スパース性:物事の本質的な特徴量を決定づける要素が少ない様子 ⇒ 余計な特徴量が排除できている

PythonでCNN

  • nn.Conv2d()でレイヤーのインスタンスを生成
    • 引数を設定(in_channels,out_channels,kernel_size,stride,padding)
  • 畳み込み層の後にReLU層を挿入する
  • 入力サイズ<フィルタサイズの場合はエラーが出るが、パディングを設定することで回避する
  • 下記画像のCNNを作成する

image.png

# Conv layer
conv_layer = nn.Conv2d(1, 4, kernel_size=3, stride=2, padding=1)
list(conv_layer.parameters()) # parameter数は3x3のカーネルが4つ

# CNN
conv_model = nn.Sequential(
    # 1x28x28
    nn.Conv2d(1, 4, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    # 4x14x14
    nn.Conv2d(4, 8, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    # 8x7x7
    nn.Conv2d(8, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    # 16x4x4
    nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    # 32x2x2
    
    # nn.flatten()
    # # 128
    # nn.Linear(128, 10)
    # 上のやり方で全結合することが一般的だが、下記のように全てnn.Conv2d()でやってもいい
    nn.Conv2d(32, 10, kernel_size=3, stride=2, padding=1),
    # 10x1x1
    nn.Flatten()
    # 10
)

# モデルの総パラメータ数
torch.tensor([params.numel() for params in conv_model.parameters()]).sum()  # -> tensor(9034)

# サンプルのテンサーで順伝搬し,outputの形状を確認
X = torch.randn((8, 1, 28, 28))
output = conv_model(X)
output.shape  # -> torch.Size([8, 10])

構築したCNNでMNISTを学習

※学習に当たって必要なコードが書かれた下記utilsファイルを任意の場所に格納し、importしておいてください。

utils.py
from tqdm import tqdm
import torch
from torch.utils.data import DataLoader, Dataset
from torch.nn import init
from torch import nn, optim
from functools import partial
import matplotlib.pyplot as plt

def get_conv_model(in_ch=1):
    """
    Create and return a convolutional neural network model with the given number of input channels.

    The weights of the convolutional and linear layers are initialized using Kaiming normal initialization.

    Parameters:
    - in_ch (int, optional): The number of input channels to the network. Default is 1.

    Returns:
    - nn.Sequential: The constructed convolutional neural network model.
    
    Structure:
    - Input: [in_ch x 28 x 28]
    - Conv1 -> BatchNorm -> ReLU: [4 x 14 x 14]
    - Conv2 -> BatchNorm -> ReLU: [8 x 7 x 7]
    - Conv3 -> BatchNorm -> ReLU: [16 x 4 x 4]
    - Conv4 -> BatchNorm -> ReLU -> AdaptiveAvgPool -> Flatten: [32]
    - Linear: [10]
    """
    conv_model =  nn.Sequential(
    # 1x28x28
    nn.Conv2d(in_ch, 4, kernel_size=3, stride=2, padding=1),
    nn.BatchNorm2d(4),
    nn.ReLU(),
    # 4x14x14
    nn.Conv2d(4, 8, kernel_size=3, stride=2, padding=1),
    nn.BatchNorm2d(8),
    nn.ReLU(),
    # 8x7x7
    nn.Conv2d(8, 16, kernel_size=3, stride=2, padding=1),
    nn.BatchNorm2d(16),
    nn.ReLU(),
    # 16x4x4
    nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1),
    nn.BatchNorm2d(32),
    nn.ReLU(),
    # 32x2x2 -> GAP -> 32 x 1 x 1
    nn.AdaptiveAvgPool2d(1),
    nn.Flatten(),
    nn.Linear(32, 10)
    # 10
    )
    for layer in conv_model:
        if isinstance(layer, nn.Linear) or isinstance(layer, nn.Conv2d):
            init.kaiming_normal_(layer.weight)
    return conv_model

def learn(model, train_loader, val_loader, optimizer, loss_func, num_epoch, early_stopping=None, save_path=None, scheduler=None):
    """
    Train and validate a given PyTorch model.
    
    Parameters:
    - model: PyTorch model to train. Model needs to be on GPU beforehand if it's supposed to be trained on GPU.
    - train_loader: DataLoader for training data.
    - val_loader: DataLoader for validation data.
    - optimizer: PyTorch optimizer.
    - loss_func: PyTorch loss function.
    - num_epoch: Number of epochs for training.
    - early_stopping: Number of epochs with no improvement to stop training. None means no early stopping.
    - save_path: Path to save the best model.
    - scheduler: Learning rate scheduler. None means no scheduler.
    
    Returns:
    - train_losses: List of training losses.
    - val_losses: List of validation losses.
    - val_accuracies: List of validation accuracies.
    """
    
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 
    # ログ
    train_losses = []
    val_losses = []
    val_accuracies = []
    
    best_val_loss = float('inf')
    no_improve = 0 # カウント用変数
    
    for epoch in range(num_epoch):
        model.train()
        running_loss = 0.0
        running_val_loss = 0.0
        running_val_acc = 0.0
        
        for train_batch, data in tqdm(enumerate(train_loader), total=len(train_loader), desc="Training", leave=False):
            
            X, y = data
            X, y = X.to(device), y.to(device)
            optimizer.zero_grad()
            # forward
            preds = model(X)
            loss = loss_func(preds, y)
            running_loss += loss.item()
    
            # backward
            loss.backward()
            optimizer.step()
            
        model.eval()
        # validation
        with torch.no_grad():
            for val_batch, data in tqdm(enumerate(val_loader), total=len(val_loader), desc="Validation", leave=False):
                X_val, y_val = data
                X_val, y_val = X_val.to(device), y_val.to(device)
                preds_val = model(X_val)
                val_loss = loss_func(preds_val, y_val)
                running_val_loss += val_loss.item()
                val_accuracy = torch.sum(torch.argmax(preds_val, dim=-1) == y_val) / y_val.shape[0]
                running_val_acc += val_accuracy.item()
    
        train_losses.append(running_loss/(train_batch + 1))
        val_losses.append(running_val_loss/(val_batch + 1))
        val_accuracies.append(running_val_acc/(val_batch + 1))
        print(f'epoch: {epoch}: train error: {train_losses[-1]}, validation error: {val_losses[-1]}, validation accuracy: {val_accuracies[-1]}')
    
        if val_losses[-1] < best_val_loss:
            best_val_loss = val_losses[-1]
            no_improve = 0
            if save_path is not None:
                state = {
                'model_state_dict': model.state_dict(),
                'optimizer_state_dict': optimizer.state_dict(),
                'val_loss': val_losses[-1]
                }
                torch.save(state, save_path)
        else:
            no_improve += 1
    
        if early_stopping and no_improve >= early_stopping:
            print('Stopping early')
            break
        if scheduler:
            scheduler.step()

    return train_losses, val_losses, val_accuracies

class ActivationStatistics:
    """
    A utility class for gathering statistics on activations produced by ReLU layers in a model.

    Attributes:
    - model (nn.Module): The model whose activations are to be tracked.
    - act_means (List[List[float]]): List of means for each ReLU layer's activations.
    - act_stds (List[List[float]]): List of standard deviations for each ReLU layer's activations.

    Methods:
    - register_hook: Register hooks on ReLU layers of the model to gather statistics.
    - save_out_stats: Callback method to save statistics of activations.
    - get_statistics: Return collected activation means and standard deviations.
    - plot_statistics: Plot the activation statistics using matplotlib.

    Usage:
        model = ... # some PyTorch model
        act_stats = ActivationStatistics(model)
        ... # Run the model, gather statistics
        act_stats.plot_statistics()
    """
    def __init__(self, model):
        self.model = model
        self.act_means = [[] for module in self.model if isinstance(module, nn.ReLU)]
        self.act_stds = [[] for module in self.model if isinstance(module, nn.ReLU)]
        self.register_hook()

    def register_hook(self):
        relu_layers = [module for module in self.model if isinstance(module, nn.ReLU)]
        for i, relu in enumerate(relu_layers):
            relu.register_forward_hook(partial(self.save_out_stats, i))

    def save_out_stats(self, i, module, inp, out):
        self.act_means[i].append(out.detach().cpu().mean().item())
        self.act_stds[i].append(out.detach().cpu().std().item())

    def get_statistics(self):
        return self.act_means, self.act_stds

    def plot_statistics(self):
        fig, axs = plt.subplots(1, 2, figsize=(15, 5))
        for act_mean in self.act_means:
            axs[0].plot(act_mean)
        axs[0].set_title('Activation means')
        axs[0].legend(range(len(self.act_means)))

        for act_std in self.act_stds:
            axs[1].plot(act_std)
        axs[1].set_title('Activation stds')
        axs[1].legend(range(len(self.act_stds)))

        plt.show()
        

def lr_finder(model, train_loader, loss_func, lr_multiplier=1.2):
    """
    Find an optimal learning rate using the learning rate range test.
    
    Parameters:
    - model: PyTorch model.
    - train_loader: DataLoader for training data.
    - loss_func: PyTorch loss function.
    - lr_multiplier: Multiplier to increase the learning rate at each step.
    
    Returns:
    - lrs: List of tested learning rates.
    - losses: List of losses corresponding to the learning rates.
    """
    lr = 1e-8
    max_lr = 10
    opt = torch.optim.SGD(model.parameters(), lr=lr)
    losses = []
    lrs = []

    for train_batch, data in enumerate(train_loader):
        X, y = data
        
        opt.zero_grad()
        # forward
        preds = model(X)
        loss = loss_func(preds, y)
        losses.append(loss.item())
        lrs.append(lr)

        # backward
        loss.backward()
        opt.step()

        lr *= lr_multiplier

        for param_group in opt.param_groups:
            param_group['lr'] = lr
        if lr > max_lr:
            break

    return lrs, losses
# データ準備
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))])
train_dataset = torchvision.datasets.MNIST('./mnist_data', train=True, download=True, transform=transform)
val_dataset = torchvision.datasets.MNIST('./mnist_data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=128, num_workers=4)
X_train, y_train = next(iter(train_loader))

# GPUで処理する場合は以下
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# conv_model.to(device)
opt = optim.SGD(conv_model.parameters(), lr=0.03, )
# start = time.time()

# 学習
train_losses, val_losses, val_accuracies = utils.learn(conv_model, train_loader, val_loader, opt, F.cross_entropy, num_epoch=5)
# 処理時間の測定
# end = time.time()
# print(end - start)

# 学習結果を描画
plt.plot(train_losses)
plt.plot(val_losses)

image.png

畳み込み層と全結合層の関係

  • 今までのNNで扱ってきた全結合層に対して、まったく新しい畳み込み層という形式が出てきたように見えるが、畳み込み層の操作は全結合層の行列積と考え、全結合層の特殊なケースと捉えることも可能
  • 下図では上は畳み込み層、下は全結合層の操作を表現しているが、畳み込み層のフィルターを全結合層の重みとして表すことができ、計算結果も同じものになる

image.png

■プーリング層(Pooling Layer)

▶プーリング層とは

  • CNNにおいて、特徴マップのサイズを縮小する目的で使用するもので、特徴をよりロバストにすることができる
  • 小さなカーネルをスライドさせ、その中の最大値(Max Pooling)や平均(Average Pooling)を出力することでサイズを小さくする
  • プーリング層には学習するパラメータが存在しないので、使い勝手がいい
  • プーリング層ではどうしても情報の欠落が発生してしまうので、近年では畳み込み層でのストライドの方が好まれて使われる

image.png

サイズの削減方法

  • 一般的なCNNでは層が深くなるにつれてサイズ(H, W)が減少していく
    • 畳み込み層でストライド=2に設定する
    • プーリング層でストライド=2に設定する

image.png

PythonでMax Poolingを実装

import torch
from torch import nn
from torch.nn import functional as F
from torch.utils.data import DataLoader
import torchvision
from torchvision import transforms
import matplotlib.pyplot as plt

# Max Pooling
def max_pooling(X, kernel_size=2, stride=2):
    X_h, X_w = X.shape
    output_data = []

    for i in range(0, X_h - kernel_size + 1, stride):
        row = []
        for j in range(0, X_w - kernel_size + 1, stride):
            row.append(X[i:i+kernel_size, j:j+kernel_size].max().item())
        output_data.append(row)
    return torch.tensor(output_data)
    
# データ準備
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))])
train_dataset = torchvision.datasets.MNIST('./mnist_data', train=True, download=True, transform=transform)
val_dataset = torchvision.datasets.MNIST('./mnist_data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=128, num_workers=4)

# データの中身を確認
X, y = train_dataset[0]
X = X / 2 + 0.5  # 正規化を戻す
X_ = X[0, :, :]
plt.imshow(X_, cmap='gray')

image.png

# サンプルのデータでmax poolingを実施
# 以下 conv -> ReLU -> max pooling
top_edge_filter = torch.tensor([[-1, -1, -1],
                               [0, 0, 0],
                               [1, 1, 1]])

def apply_filter(im, filter):
    im_h, im_w = im.shape
    f_h, f_w = filter.shape
    output_data = []
    for i in range(im_h - f_h + 1):
        row = []
        for j in range(im_w - f_w + 1):
            row.append((im[i:i+f_h, j:j+f_w] * filter).sum().item())
        output_data.append(row)
    return torch.tensor(output_data)

def relu(X):
    return torch.clamp(X, min=0)

# 畳み込みの出力
conv_out = apply_filter(X_, top_edge_filter)
plt.imshow(conv_out, cmap='gray')

image.png

# ReLUの出力
relu_out = relu(conv_out)
plt.imshow(relu_out, cmap='gray')

image.png

# Poolingの出力
max_out = max_pooling(relu_out)
plt.imshow(max_out, cmap='gray')

image.png

PytorchモジュールでMax Pooling

  • nn.MaxPool2dクラスを使用して、引数に[kernel_size,stride,padding]を指定する
  • F.max_pool2dクラスを使用してもOK、引数に[kernel_size,stride,padding]を指定する
# strideの代わりにmax pooingで次元削減を行う
conv_model = nn.Sequential(
    # 1x28x28
    nn.Conv2d(1, 4, kernel_size=3, stride=1, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2, 2),
    # 4x14x14
    nn.Conv2d(4, 8, kernel_size=3, stride=1, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2, 2),
    # 8x7x7
    nn.Conv2d(8, 16, kernel_size=3, stride=1, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2, 2),
    # 16x3x3
    nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2, 2),
    # 32x1x1
    nn.Conv2d(32, 10, kernel_size=3, stride=1, padding=1),
    # 10x1x1
    nn.Flatten()
    # 10
)

# F.max_pool2d()を使用したケース
class ConvModel(nn.Module):
    def __init__(self, in_ch):
        super().__init__()
        self.conv1 = nn.Conv2d(in_ch, 4, kernel_size=3, stride=1, padding=1)
        self.conv2 = nn.Conv2d(4, 8, kernel_size=3, stride=1, padding=1)
        self.conv3 = nn.Conv2d(8, 16, kernel_size=3, stride=1, padding=1)
        self.conv4 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
        self.conv5 = nn.Conv2d(32, 10, kernel_size=3, stride=1, padding=1)
        self.flatten = nn.Flatten()

    def forward(self, X):
        X = F.relu(self.conv1(X))
        X = F.max_pool2d(X, 2)
        X = F.relu(self.conv2(X))
        X = F.max_pool2d(X, 2)
        X = F.relu(self.conv3(X))
        X = F.max_pool2d(X, 2)
        X = F.relu(self.conv4(X))
        X = F.max_pool2d(X, 2)
        X = F.relu(self.conv5(X))
        X = self.flatten(X)
        return X
        
# サンプルデータで順伝搬
X, y = next(iter(train_loader))
print(X.shape)  # -> torch.Size([128, 1, 28, 28])

# nn.MaxPool2d()
preds = conv_model(X)
print(preds.shape)  # -> torch.Size([128, 10])

# F.max_pool2d()
conv_model2 = ConvModel(in_ch=1)
preds = conv_model2(X)
print(preds.shape)  # -> torch.Size([128, 10])

▶Global Average Pooling(GAP)

  • 入力の各channelに対してそのchannelの全ての値の平均値(1つのスカラー)を計算し出力する方式
  • 空間的な次元(H, W)を除去し、各channelの情報を集約する
  • CNNの全結合層の手前や、代わりとして使うことが多い
    • これによりパラメータ数が削減でき、過学習を防ぐことができる
    • CNNの入力サイズが変動しても対応することができるようになる

なぜ動的な入力サイズに対応できるのか

  • GAP層を使わない通常のCNN(最終層に全結合を用いたCNN)では、最後のflattenで特定の入力サイズを要求するので、最初の入力サイズが固定されてしまう
  • 全結合層の手前にGAP層を挿入することで入力サイズが変動しても対応できるようになる
  • 最後の畳み込み層の出力channel数をクラス数と一致させれば、全結合層をGAP層に置き換えることも可能

image.png

  • 上図のの通り、GAP無しでは入力サイズが変わるとflatten後の128も変わってしまうので全結合の際にエラーになる。
    GAPありでは入力サイズに関係なく1×1×32になるので、どのような入力であったとしても32を固定で用意することができる。

PythonでGAPを実装

  • torch.mean()を使って入力tensorの平均を計算を出力することでGAP層を実装する
class GlobalAveragePooling2D(nn.Module):
    def forward(self, X):
        # X.shape = [b, ch, h, w]で、hとwの平均を取りたい
        return torch.mean(X, dim=(2, 3), keepdim=True)

# サンプルのTensorで順伝搬
X = torch.randn((128, 3, 4, 4))
gap_layer = GlobalAveragePooling2D()
gap_out = gap_layer(X)
print(gap_out.shape)  # -> torch.Size([128, 3, 1, 1])

PytorchのモジュールでGAP

  • Pytorchには直接的なGAPの実装はないがtorch.nn.AdaptiveAvgPool2d()を使って同様の機能を実現することが可能
    • AdaptiveAvgPool2d()は引数に出力サイズを指定すると、それに"適応"するようにプーリングを実施する
    • torch.nn.AdaptiveAvgPool2d(1)とすることでGAPを実現できる
# nn.AdaptiveAvgPool2d(1)でGAPを実現する
gap_layer = nn.AdaptiveAvgPool2d(1)
gap_out = gap_layer(X)
print(gap_out.shape)  # -> torch.Size([128, 3, 1, 1])

CNNにGAP層を実装

# GAP層あり
conv_model_gap = nn.Sequential(
    # 1x28x28
    nn.Conv2d(1, 4, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    # 4x14x14
    nn.Conv2d(4, 8, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    # 8x7x7
    nn.Conv2d(8, 16, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    # 16x4x4
    nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1),
    nn.ReLU(),
    # 32x2x2
    nn.AdaptiveAvgPool2d(1),  # GAP
    # 32x1x1
    nn.Flatten(),
    # 32
    nn.Linear(32, 10)
    # 10
)

# サンプルのTensorで順伝搬
# GAP層があるので入力サイズが可変
X = torch.randn((128, 1, 64, 64))
out = conv_model_gap(X)
out.shape  # -> torch.Size([128, 10])

■初期の有名なCNNモデル

  • CNNにかかわらず、深層学習ではモデルを全て一から構築することはほとんどなく、すでに実績のある既存アーキテクチャを利用するのが一般的
  • CNNであれば、画像認識コンペで有名なILSVRC(ImageNet Large Visual Recognition Challenge)の優勝モデルなど

image.png

  • 2012年のAlexNetが初めて深層学習を利用したモデルが優勝したとして、深層学習が注目を浴びるきっかけともなっている

ImageNet Database

  • ILSVRCでも使用される1400万枚以上の画像を含む大規模データベース
  • 約2万のカテゴリに分類され、それぞれの画像に最低一つのラベルがアノテーションされている
  • ILSVRCでは、ImageNetの約100万枚の画像で1000種類の多クラス分類で画像認識
    を行う

▶LeNet

  • Yann LeCun教授らによって1990年代に開発された初期のCNNで、特にMNISTに対するモデルとして広く認知されている
  • 当時はReLUは一般的ではなく、シグモイドやtanhを使用し、Max Poolingではなく平均Poolingを使用している

image.png

PythonでLeNetを実装

  • 当初の実装とは異なるが、現代版としてrelu、maxpoolingを使用
  • nn.Conv2d, nn.Linear, F.max_pool2d, F.relu, nn.Flattenを使用してLeNetを実装
  • 基本的な畳み込み層のblockは、conv2d -> relu -> max poolingとする
  • その後falttenをしてfc(fully connected) -> reluで全結合層を実装する

image.png

import torch
from torch.nn import functional as F
from torch import nn, optim
from torch.utils.data import DataLoader, Subset
import torchvision
from torchvision import models, transforms
from torchvision.models.vgg import VGG11_Weights

%load_ext autoreload
%autoreload 2
import utils

# LeNet
class LeNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(16*5*5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, X):
        X = F.max_pool2d(F.relu(self.conv1(X)), 2)
        X = F.max_pool2d(F.relu(self.conv2(X)), 2)
        X = self.flatten(X)
        X = F.relu(self.fc1(X))
        X = F.relu(self.fc2(X))
        X = self.fc3(X)
        return X
        
lenet = LeNet()
X = torch.randn((128, 1, 32, 32))
out = lenet(X)
out.shape # -> torch.Size([128, 10])

▶AlexNet

  • 2012年のILSVRCにて優勝した深層学習モデルで、深層学習ブームを引き起こした火付け役のモデル
  • 5層の畳み込み層と3層の全結合層からなる

image.png

  • AlexNetは全8層のモデルとなっているが、当時ではとても多いものとなっている。
    ※層を深くすると様々な不具合が生じるのが一般的だったが、それを解決したモデルとなっている。
  • Pytorchのvisionというモジュールに実装されている。
    リポジトリ

▶VGG

  • オックスフォード大のVGGグループによって開発されたCNNで、2014年のILSVRCで2位(1位はGooLeNet)
  • 3x3の小さなフィルタを持つ層を深く積み重ねることにより局所的な特徴と全体的な特徴を共に捉えることができる
  • 全ての畳み込み層が同じフィルタサイズ(=3x3)とストライド(=1)で、プーリングには2x2のフィルタと2のストライドを使用しているため、非常にシンプルな構成
  • VGG16やVGG19など、いくつかバリエーションがある(数字は重みを持つ層の数)

image.png
⇒ PytorchのVGGのリポジトリ

▶Pytorchの既存CNNモデル

  • torchvision.modelsモジュールを使うことで既存のモデルを利用することができる
  • AlexNetやVGGをはじめ多くのモデルが存在する
  • 実際には下記のようにして既存のモデルを使うことが多い

VGG16の例

  • from torchvision.models.vgg import VGG16_Weightsでimport
  • models.vgg16()でインスタンス化
    • weightsVGG16_Weights.IMAGENET1K_V1を指定してImageNetでの学習済みの重みをロードする
  • .features:モデルの特徴マップ抽出部分(CNN + Max Pool)にアクセス可能
  • .classifier:モデルの全結合層部分(Linear)にアクセス可能
  • 学習済みのモデルを使ってさらに学習(転移学習、fine tuning) をすることもある(後述)

.classfierによる最終出力の調整
VGG16_Weights.IMAGENET1K_V1で学習済みモデルを呼び出すのはいいのだが、1Kとあるように1000クラス用のモデルとなっている。
最終的に必要となるクラス数に合わせるためにmodel.classifier[-1] = nn.Linear(4096, XX)と全結合層を最後に追加する必要がある。

# 既存モデルの使用方法
model = models.vgg11(weights=VGG11_Weights.IMAGENET1K_V1)
model.classifier

# データ準備
transform = transforms.Compose([
    transforms.ToTensor(),
    # transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
    # CIFAR10用の正規化の設定
    transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.2023, 0.1994, 0.2010])
])
# CIFAR10:カラー画像のクラス分類でよく使われるデータセット
classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
train_dataset = torchvision.datasets.CIFAR10('./cifar10_data', train=True, download=True, transform=transform)
val_dataset = torchvision.datasets.CIFAR10('./cifar10_data', train=False, download=True, transform=transform)

# データ量が多くて学習できない場合はサブデータを使用する
train_dataset_sub = Subset(train_dataset, range(500))
val_dataset_sub = Subset(val_dataset, range(250))
train_loader = DataLoader(train_dataset_sub, batch_size=16, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset_sub, batch_size=16, num_workers=4)

# 最終層の出力を10クラス分類用にする
model.classifier[-1] = nn.Linear(4096, 10)

# GPUで学習する場合は以下のように.to(device)でGPUにモデルを移動させる
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 
# model.to(device)
opt = optim.SGD(model.parameters(), lr=0.03)

CIFAR10について

学習時のプログレスバーの表示

  • 本記事で共有しているutils.pyを任意の場所に格納して、utilsとimportした状態で、下記コードを適用してください。
import time
start = time.time()
train_losses, val_losses, val_accuracies = utils.learn(model, train_loader, val_loader, opt, F.cross_entropy, 3)
end = time.time()
print(end - start)

次の記事

まだ

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?