Lesson 7 / 12

CNNを実装する

このレッスンで学ぶこと

  • nn.Conv2d の引数(チャネル・カーネル・stride・padding)を読める
  • 畳み込みの出力サイズの公式で H・W を計算できる
  • 畳み込み層のパラメータ数を計算できる
  • プーリング・flatten で形がどう変わるか追える
  • nn.Sequential で多層CNNを組み、形の変化を追える
  • im2col が「畳み込みを行列積に変換する」技法だと理解できる

1. CNN(畳み込みニューラルネット)とは

📘 前提:このレッスンはCNNの理論(畳み込み・プーリング・チャネルの考え方)を既習として、PyTorchでの実装と「形の追い方」に集中します。 理論があいまいなら、先に E資格対策ページ 「畳み込みニューラルネットワーク」 に目を通すとスムーズです。

画像のような格子状のデータでは、全結合だけだとパラメータが爆発し、位置のずれにも弱くなります。 畳み込み層(Convolution)は、小さなカーネル(フィルタ)を画像の上で滑らせ、 局所的なパターン(エッジ・模様など)を検出します。同じカーネルを全域で使い回す(パラメータ共有)ので効率的です。

画像データは (N, C, H, W) の4次元で扱います(バッチ・チャネル・高さ・幅)。 ここからは、この形が層を通ってどう変わるかを読み解きます。

2. nn.Conv2d の基本

nn.Conv2d(in_ch, out_ch, kernel_size, stride, padding)in_ch は入力チャネル、out_ch は出力チャネル(=カーネルの枚数)、kernel_size はカーネルの一辺。 重みの形は (out_ch, in_ch, K, K) です。

畳み込みの出力サイズの図:5×5入力に3×3カーネルを適用すると3×3出力になる
3×3カーネルを5×5入力に滑らせると、出力は3×3。出力サイズは公式 (H+2P−K)/S+1 で決まる
conv_basic.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
torch.manual_seed(0)

x = torch.randn(8, 1, 28, 28)          # (N=8, C=1, H=28, W=28)

conv = nn.Conv2d(1, 16, kernel_size=3)             # padding=0, stride=1
print("no pad   :", conv(x).shape)              # 28-3+1 = 26
print("weight   :", conv.weight.shape)            # (out, in, K, K)

conv_p = nn.Conv2d(1, 16, 3, padding=1)            # paddingで端を保つ
print("pad=1    :", conv_p(x).shape)            # 28のまま

conv_s = nn.Conv2d(1, 16, 3, stride=2, padding=1)  # strideで縮小
print("stride=2 :", conv_s(x).shape)            # 28 -> 14
▼ 出力
no pad   : torch.Size([8, 16, 26, 26])
weight   : torch.Size([16, 1, 3, 3])
pad=1    : torch.Size([8, 16, 28, 28])
stride=2 : torch.Size([8, 16, 14, 14])

💡 チャネルは「最後の次元」ではない: 全結合では特徴は最後の次元でしたが、画像では (N, C, H, W)2番目がチャネルです。 Conv2d(1, 16, ...) は「1チャネル入力 → 16チャネル出力」。HとWはカーネルとpadding/strideで決まります。

3. 出力サイズの公式(最重要)

畳み込み後の高さ・幅は、次の公式で決まります(// は切り捨て割り算)。コードを読むうえで必ず押さえたい計算です。

出力サイズ = (H + 2P − K) / S + 1(小数は切り捨て)

H=入力サイズ、P=padding、K=カーネルサイズ、S=stride

out_size.py(Python・読むだけ)
def out_size(H, K, P, S):
    return (H + 2*P - K) // S + 1

print(out_size(28, 3, 0, 1))   # 26  (padなし)
print(out_size(28, 3, 1, 1))   # 28  (pad=1で維持)
print(out_size(28, 3, 1, 2))   # 14  (stride=2で半分)
▼ 出力
26
28
14

4. 畳み込み層のパラメータ数

出力サイズと並んでパラメータ数の計算も問われます。レッスン5では全結合層を数えましたが、畳み込み層も公式は単純です。

パラメータ数 = (in_ch × K × K + 1) × out_ch

カーネル1枚は in_ch×K×K 個の重み+1 個のバイアス。それが out_ch 枚ぶん。 画像のサイズ(H・W)には依存しないのがポイントです(同じカーネルを全位置で使い回す=パラメータ共有)。

conv_params.py(PyTorch・読むだけ)
import torch
import torch.nn as nn

conv = nn.Conv2d(3, 64, kernel_size=3, padding=1)
print("weight:", conv.weight.shape)   # (out, in, K, K) = (64, 3, 3, 3)
print("bias  :", conv.bias.shape)     # (64,)
print("params:", sum(p.numel() for p in conv.parameters()))  # (3*3*3+1)*64 = 1792

# 参考:32×32画像で同じ「3ch→64ch」を全結合でつなぐと…
in_f = 3 * 32 * 32          # 3072
out_f = 64 * 32 * 32         # 65536
print("全結合なら:", in_f * out_f + out_f)   # 約2億
▼ 出力
weight: torch.Size([64, 3, 3, 3])
bias  : torch.Size([64])
params: 1792
全結合なら: 201392128
理論の確認: 畳み込みは 1792 パラメータ、同じ「3ch→64ch」を全結合でやると約2億。 これがパラメータ共有の威力で、CNNが画像で全結合より圧倒的に効率的な理由です。

5. プーリングと flatten

プーリングMaxPool2d)は領域内の最大値を取り、特徴マップを縮小します。 MaxPool2d(2)2プーリング窓のサイズ(2×2)で、ストライドも既定で同じ 2 になるため、縦横がそれぞれ半分になります。 畳み込み・プーリングで特徴を抽出したら、最後に flatten(viewで1次元に伸ばし、全結合層で分類します。

small_cnn.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
torch.manual_seed(0)

class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 8, 3, padding=1)  # 28 -> 28, 8ch
        self.pool  = nn.MaxPool2d(2)              # 2×2窓で最大値→縦横半分(28->14)。strideも既定で2
        self.fc    = nn.Linear(8 * 14 * 14, 10)   # flatten後 -> 10クラス

    def forward(self, x):
        x = self.pool(torch.relu(self.conv1(x)))  # (N, 8, 14, 14)
        x = x.view(x.size(0), -1)             # flatten (N, 8*14*14)
        return self.fc(x)                       # (N, 10)

model = CNN()
out = model(torch.randn(8, 1, 28, 28))
print("flatten次元:", 8 * 14 * 14)   # 1568
print("out.shape :", out.shape)      # (8, 10)
▼ 出力
flatten次元: 1568
out.shape : torch.Size([8, 10])

⚠ flatten 次元のミス(つまずきやすい点): nn.Linear の入力サイズは、直前の特徴マップ (C, H, W) を全部かけた数です。 ここでは 8 × 14 × 14 = 1568。畳み込み・プーリングで H・W がどう変わったかを正しく追えないと、この数を間違えてshapeエラーになります。 x.view(x.size(0), -1)-1 は「残りを自動計算」の意味です。

💡 プーリングの種類:

  • nn.MaxPool2d(2) … 2×2領域の最大値。最も一般的(強い特徴を残す)。
  • nn.AvgPool2d(2) … 2×2領域の平均値。なめらかに縮小したいとき。
  • nn.AdaptiveAvgPool2d(1)Global Average Pooling。各チャネルを1つの値に潰して (N, C, 1, 1) に。flatten の代わりに使うと入力サイズに依存せず全結合へつなげます(ResNet等で定番)。

im2col とは

畳み込みは、見た目は「滑らせる」ですが、実装では行列積に変換して高速に計算します。これが im2col。 各受容野(カーネルが重なる小領域)を1列に展開して大きな行列を作り、カーネルを並べた行列との行列積で全位置を一気に計算します。 GPU が得意な行列積に落とせるので速い、という発想です(実装を書けることより、「行列積に変換する技法」という考え方を押さえておくのが大切です)。

6. nn.Sequential で組み立てる

nn.Sequential は、層を順番に並べるだけで「つなげて呼べる」ようにするコンテナです。 __init__forward を自分で書かなくても、渡した層を上から順に適用してくれるので、一直線のネットワークを手早く書けます。 複数のブロックを積んで、形がどう変わるかを追ってみましょう。

sequential_cnn.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
torch.manual_seed(0)

model = nn.Sequential(
    nn.Conv2d(1, 8, 3, padding=1),    # (N,1,28,28) -> (N,8,28,28)
    nn.ReLU(),
    nn.MaxPool2d(2),                  # -> (N,8,14,14)
    nn.Conv2d(8, 16, 3, padding=1),   # -> (N,16,14,14)
    nn.ReLU(),
    nn.MaxPool2d(2),                  # -> (N,16,7,7)
)

x = torch.randn(4, 1, 28, 28)
feat = model(x)
print("特徴マップ:", feat.shape)                       # (4, 16, 7, 7)
print("flatten  :", feat.view(feat.size(0), -1).shape)  # (4, 784)
▼ 出力
特徴マップ: torch.Size([4, 16, 7, 7])
flatten  : torch.Size([4, 784])

💡 形の追い方: padding=13×3 畳み込みは H・W を保ち(28→28、14→14)、MaxPool2d(2) が半分にします(28→14→7)。 チャネルは Conv2dout_ch1 → 8 → 16 と増加。最終の (N, 16, 7, 7) を flatten すると 16×7×7 = 784 次元になり、ここに nn.Linear(784, クラス数) をつなげば分類器の完成です。

補足: nn.Sequential は「一直線」の構成に向きます。分岐やスキップ接続(ResNetの残差など)がある場合は、レッスン5のように nn.Module を継承して forward を自分で書きます(レッスン8で扱います)。

7. 練習問題

問題 1

畳み込みの出力サイズは?

入力の高さ H=32kernel_size=5padding=0stride=1 のとき、出力の高さはいくつですか。

  • A. 30
  • B. 28
  • C. 32
答えと解説を見る

正解:B. 28

公式 (H+2P−K)/S+1 = (32+0−5)/1+1 = 28。padなし・stride1では H−K+1 です。

問題 2

Conv2d の出力 shape は?

nn.Conv2d(3, 32, 3, padding=1)(4, 3, 64, 64) を通すと、出力 shape はどれですか。

  • A. (4, 32, 64, 64)
  • B. (4, 32, 62, 62)
  • C. (4, 3, 64, 64)
答えと解説を見る

正解:A. (4, 32, 64, 64)

チャネルは 3 → 32。H・W は (64+2−3)/1+1 = 64(pad=1, K=3 でサイズ維持)。 バッチ4はそのまま。

問題 3

flatten の次元は?

特徴マップが (N, 16, 7, 7) のとき、view(N, -1) で flatten した後の特徴数はいくつですか。

  • A. 16
  • B. 49
  • C. 784
答えと解説を見る

正解:C. 784

16 × 7 × 7 = 784。flatten 後の nn.Linear の入力サイズはこの値にします。

問題 4

im2col の目的は?

im2col は何のために使われますか。

  • A. 画像を白黒に変換するため
  • B. 畳み込みを行列積に変換して高速に計算するため
  • C. 過学習を防ぐため
答えと解説を見る

正解:B

受容野を列に展開して大きな行列を作り、行列積で全位置をまとめて計算します。GPUの得意な行列積に落とすための技法です。

問題 5

畳み込み層のパラメータ数は?

nn.Conv2d(3, 16, kernel_size=3) のパラメータ数(重み+バイアス)はいくつですか。

  • A. 432
  • B. 144
  • C. 448
答えと解説を見る

正解:C. 448

(in_ch×K×K + 1)×out_ch = (3×3×3 + 1)×16 = 28×16 = 448432バイアス(+1)の数え忘れ(27×16)。画像のH・Wには依存しません。

8. まとめ

このレッスンのポイント

  • 画像は (N, C, H, W)。チャネルは2番目の次元
  • nn.Conv2d(in_ch, out_ch, K, stride, padding)。重みは (out, in, K, K)
  • 出力サイズ (H+2P−K)/S+1。pad=(K−1)/2 でサイズ維持、strideで縮小
  • パラメータ数 (in_ch×K×K+1)×out_ch(画像サイズに依存しない=パラメータ共有)
  • プーリングで縮小、最後に flatten(view)して全結合へ
  • nn.Linear の入力=直前の C×H×W(ここを間違えるとshapeエラー)
  • nn.Sequential で一直線のCNNを手早く組める。分岐は nn.Moduleforward
  • im2col は畳み込みを行列積に変換して高速化する技法

腕試し:出力を予想してみよう

このレッスンの仕上げです。次のコードの出力を頭の中で計算してみましょう(「出力サイズの公式」と「パラメータ数の公式」を使います)。

challenge.py(PyTorch・予想してみよう)
import torch
import torch.nn as nn

conv = nn.Conv2d(3, 10, kernel_size=5, stride=1, padding=0)
pool = nn.MaxPool2d(2)
x = torch.randn(2, 3, 32, 32)
y = pool(conv(x))

print(y.shape)                                  # ① 出力の形は?
print(sum(p.numel() for p in conv.parameters()))  # ② convのパラメータ数は?
答えと解説を見る
torch.Size([2, 10, 14, 14])
760

① 畳み込み:(32+0−5)/1+1 = 28、チャネルは 3→10(2,10,28,28)。次に MaxPool2d(2)28→14(2, 10, 14, 14)
② パラメータ数 (3×5×5+1)×10 = 76×10 = 760

次は、この CNN を学習・評価する際の作法(train/eval・過学習対策)と、転移学習・ResNetの読み解きに進みます。

完了するとコース一覧に進捗が記録されます