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) です。
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
- padding を増やすと出力が大きくなる(端の情報を保てる)。
P=(K−1)/2にするとサイズ維持(K=3ならP=1)。 - stride を増やすと出力が小さくなる(飛ばしながら適用)。
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)には依存しないのがポイントです(同じカーネルを全位置で使い回す=パラメータ共有)。
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次元に伸ばし、全結合層で分類します。
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 を自分で書かなくても、渡した層を上から順に適用してくれるので、一直線のネットワークを手早く書けます。
複数のブロックを積んで、形がどう変わるかを追ってみましょう。
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=1 の 3×3 畳み込みは H・W を保ち(28→28、14→14)、MaxPool2d(2) が半分にします(28→14→7)。
チャネルは Conv2d の out_ch で 1 → 8 → 16 と増加。最終の (N, 16, 7, 7) を flatten すると 16×7×7 = 784 次元になり、ここに nn.Linear(784, クラス数) をつなげば分類器の完成です。
nn.Sequential は「一直線」の構成に向きます。分岐やスキップ接続(ResNetの残差など)がある場合は、レッスン5のように nn.Module を継承して forward を自分で書きます(レッスン8で扱います)。
7. 練習問題
畳み込みの出力サイズは?
入力の高さ H=32、kernel_size=5、padding=0、stride=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 です。
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はそのまま。
flatten の次元は?
特徴マップが (N, 16, 7, 7) のとき、view(N, -1) で flatten した後の特徴数はいくつですか。
- A. 16
- B. 49
- C. 784
答えと解説を見る
正解:C. 784
16 × 7 × 7 = 784。flatten 後の nn.Linear の入力サイズはこの値にします。
im2col の目的は?
im2col は何のために使われますか。
- A. 画像を白黒に変換するため
- B. 畳み込みを行列積に変換して高速に計算するため
- C. 過学習を防ぐため
答えと解説を見る
正解:B
受容野を列に展開して大きな行列を作り、行列積で全位置をまとめて計算します。GPUの得意な行列積に落とすための技法です。
畳み込み層のパラメータ数は?
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 = 448。
432 はバイアス(+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.Module+forward- im2col は畳み込みを行列積に変換して高速化する技法
腕試し:出力を予想してみよう
このレッスンの仕上げです。次のコードの出力を頭の中で計算してみましょう(「出力サイズの公式」と「パラメータ数の公式」を使います)。
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の読み解きに進みます。
完了するとコース一覧に進捗が記録されます