実践編Practice
深層学習の一気通貫(CNN)
小さな画像データセットを題材に、データの前処理から畳み込みニューラルネットワーク(CNN)の学習・評価までを1本の流れでたどります。前処理・深層学習・評価という単体で学んだ技術を、手描き数字の分類という具体的な課題でつなぐ総合演習です。すべて手元やColabでそのまま動かせるノートブックつきで進めます。
この教材は日本語のみで提供していますThis learning material is available in Japanese only.
課題設定The task
手描き数字を、CNNで見分ける
深層学習と聞くと、巨大なモデルと大量のGPU計算を思い浮かべるかもしれません。しかし本質は、単体の技術を1本の流れに組み立てることにあります。この演習では、8×8ピクセルの手描き数字画像という小さなデータを使い、前処理からCNNの学習・評価までを一気通貫でたどります。データが小さいので、特別な環境がなくてもノートPCやColabで数分あれば最後まで動きます。
やることは、次の一続きの流れです。深層学習の実務でくり返し現れる、基本の型です。
- データ入手:scikit-learn 付属の手描き数字データ(digits)を読み込む。画像と正解ラベルの組。
- 前処理:画素値を0から1に正規化し、CNNが受け取る形(チャネル付きの配列)に整え、学習用・検証用・テスト用に分ける。
- モデル定義:畳み込み層・プーリング層・全結合層を PyTorch で組み、小さなCNNを作る。
- 学習と評価:損失を下げるよう重みを更新し、テストデータで正解率・混同行列・分類レポートを確かめる。
最終的に得られるのは、手描き数字を9割前後の精度で当てる分類器と、「どの数字を取り違えやすいか」「なぜそうなるか」という読み解きです。小さなデータでも、深層学習の流れそのものは大きな課題とまったく同じです。
この演習のパイプライン
使う技術Techniques used
組み合わせる3つのモジュール
この演習は、単体で学んだ次の3モジュールを1本につなぎます。まだ学んでいない技術があれば、先にそのモジュールで基礎を押さえてから戻ってくると、各ステップの意味がはっきりします。CNNの畳み込みの仕組みそのものは深層学習モジュールで詳しく扱っているので、ここでは流れを組み立てることに集中します。
ノートブックで実行Run the notebook
このページのコードを、そのまま動かす
深層学習の一気通貫(CNN)ノートブック
このページで読むコードと同じ内容を、上から順に実行できるノートブックにまとめてあります。Colab で開けば、インストール込みで環境構築なしにそのまま動かせます。手元の Jupyter で動かしたい場合は .ipynb をダウンロードしてください。
最初のセルで !pip install torch scikit-learn matplotlib を実行します。digits データは scikit-learn に同梱されているため、データのダウンロードは不要で、完全にオフラインでも動きます。CPU だけで数分あれば最後まで実行できます。
データ入手:手描き数字を読み込む
Collect the image data何を・なぜWhat & why
まず題材となる画像データを用意します。ここでは sklearn.datasets.load_digits を使います。これは 8×8ピクセル・グレースケールの手描き数字(0から9)を1797枚集めたデータセットで、各画素は0から16の整数、正解ラベル(どの数字か)も付いています。scikit-learn に同梱されているので、ネットワーク接続なしにすぐ読み込めます。小さいので、CNNの流れを試すのにちょうどよい素材です。
読み込んだら、まず形と中身を確認します。深層学習では「入力の形(shape)」を取り違えると動かないので、最初にここを押さえるのが定石です。
import numpy as np
from sklearn.datasets import load_digits
digits = load_digits()
X = digits.images # (1797, 8, 8) 各画素は 0..16 の整数
y = digits.target # (1797,) 正解ラベル 0..9
print("images:", X.shape, "labels:", y.shape, "classes:", np.unique(y))
print("pixel min/max:", X.min(), X.max())
images: (1797, 8, 8) labels: (1797,) classes: [0 1 2 3 4 5 6 7 8 9]
pixel min/max: 0.0 16.01枚が8×8の小さな画像で、全部で1797枚、10クラス(0から9)がほぼ均等に含まれています。試しに1枚を matplotlib で表示すると、粗いながらも数字の形が見て取れます。
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 5, figsize=(8, 2))
for i in range(5):
ax[i].imshow(X[i], cmap="gray_r")
ax[i].set_title(f"label: {y[i]}")
ax[i].axis("off")
plt.tight_layout()
plt.show()
この Step のまとめ
load_digits で 1797 枚の 8×8 手描き数字を読み込みました。入力は (枚数, 高さ, 幅) の3次元配列、画素値は 0 から 16。次の Step で、これをCNNが受け取れる形に整えて分割します。
前処理:正規化・形状変換・分割
Preprocess and split何を・なぜWhat & why
生のデータをそのままCNNに渡すことはできません。3つの下ごしらえをします。
- 正規化:画素値を 0 から 16 の整数から 0 から 1 の実数に直します(16で割る)。値の範囲をそろえると学習が安定し、収束が速くなります。
- 形状変換:CNNは
(枚数, チャネル, 高さ, 幅)の4次元配列を受け取ります。今回は白黒なのでチャネルは1。(1797, 8, 8)を(1797, 1, 8, 8)にします。 - 分割:データを学習用・検証用・テスト用の3つに分けます。学習用でモデルを訓練し、検証用で学習中の様子を見て、テスト用は最後の評価まで一切触れません。
train_test_splitのstratifyで、各クラスの比率を分割後もそろえます。
「テスト用は最後まで見ない」のが鉄則です。テストで何度も調整すると、テストに合わせ込んでしまい、本当の汎化性能が分からなくなります。
import torch
from sklearn.model_selection import train_test_split
# 正規化(0..1)と形状変換(チャネル次元を追加)
X = (X / 16.0).astype("float32")
X = X[:, None, :, :] # (N, 1, 8, 8)
# まずテストを切り分け、残りを学習用と検証用に分ける(stratifyで比率維持)
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y)
X_tr, X_va, y_tr, y_va = train_test_split(
X_tr, y_tr, test_size=0.2, random_state=42, stratify=y_tr)
# PyTorch のテンソルに変換
Xtr = torch.tensor(X_tr); ytr = torch.tensor(y_tr)
Xva = torch.tensor(X_va); yva = torch.tensor(y_va)
Xte = torch.tensor(X_te); yte = torch.tensor(y_te)
print("train:", X_tr.shape[0], "val:", X_va.shape[0], "test:", X_te.shape[0])
train: 1149 val: 288 test: 360この Step のまとめ
画素を 0 から 1 に正規化し、(N, 1, 8, 8) の形に整え、学習1149・検証288・テスト360 に分けました。stratify で各数字の比率は3分割後も保たれます。下ごしらえが済んだので、いよいよモデルを組みます。
CNNを定義する
Define the CNN何を・なぜWhat & why
いよいよ本体のCNNを組みます。畳み込みニューラルネットワークは、画像の局所的な模様(線や角)を畳み込み層でとらえ、プーリング層で少しずつ位置のずれに強くしながら情報をまとめ、最後に全結合層で分類します。小さな8×8画像なので、層は最小限で十分です。次の構成にします。
- 畳み込み層1:
Conv2d(1, 8, kernel_size=3, padding=1)。1チャネルの入力から8枚の特徴マップを作る。 - プーリング:
MaxPool2d(2)で 8×8 を 4×4 に縮小。 - 畳み込み層2:
Conv2d(8, 16, kernel_size=3, padding=1)。8チャネルから16チャネルへ。 - プーリング:
MaxPool2d(2)で 4×4 を 2×2 に縮小。ここで16 × 2 × 2 = 64個の数値になる。 - 全結合層:
Linear(64, 32)とLinear(32, 10)。最後は10クラス分の出力。
活性化関数には ReLU を使います。負の値を0にする単純な関数で、非線形性を与えつつ学習を安定させます。padding=1 は畳み込みで画像の端が縮まないようにする設定です。
import torch.nn as nn
torch.manual_seed(42)
class SmallCNN(nn.Module):
def __init__(self, n_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 8, kernel_size=3, padding=1), # 8x8 -> 8x8
nn.ReLU(),
nn.MaxPool2d(2), # 8x8 -> 4x4
nn.Conv2d(8, 16, kernel_size=3, padding=1), # 4x4 -> 4x4
nn.ReLU(),
nn.MaxPool2d(2), # 4x4 -> 2x2
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(16 * 2 * 2, 32),
nn.ReLU(),
nn.Linear(32, n_classes),
)
def forward(self, x):
return self.classifier(self.features(x))
model = SmallCNN()
n_params = sum(p.numel() for p in model.parameters())
print("params:", n_params)
params: 3658この Step のまとめ
畳み込み2層・プーリング2層・全結合2層の小さなCNNを組みました。学習するパラメータはわずか 3,658 個で、これでも digits なら十分に働きます。プーリングで 8×8 → 4×4 → 2×2 と縮み、最後の全結合層の入力が 16×2×2=64 になる点が形合わせの要です。
学習:損失を下げて重みを育てる
Train the network何を・なぜWhat & why
学習とは、モデルの予測と正解のずれ(損失)を小さくするように、重みを少しずつ調整する作業です。多クラス分類なので損失関数は CrossEntropyLoss(交差エントロピー)を使います。重みの更新には Adam という最適化手法を使います。学習は次の流れをくり返します。
- データを小さなまとまり(ミニバッチ)に分けて順に入力する。
- 予測を出し、正解との損失を計算する(順伝播)。
- 損失を各重みで微分し(
loss.backward()、誤差逆伝播)、その向きに重みを更新する(optimizer.step())。 - データ全体を1周するのが1エポック。これを何エポックか繰り返す。
各エポックの終わりに、学習には使っていない検証データで正解率を測り、学習が進んでいるかを見守ります。optimizer.zero_grad() で勾配を毎回リセットするのを忘れないのが定石です。
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
n_epochs = 30
batch = 64
n = Xtr.shape[0]
for epoch in range(1, n_epochs + 1):
model.train()
perm = torch.randperm(n) # 毎エポック順番をシャッフル
total = 0.0
for i in range(0, n, batch):
idx = perm[i:i+batch]
xb, yb = Xtr[idx], ytr[idx]
optimizer.zero_grad()
out = model(xb)
loss = criterion(out, yb)
loss.backward() # 誤差逆伝播
optimizer.step() # 重みを更新
total += loss.item() * xb.size(0)
# 検証データで正解率を測る(勾配は不要)
model.eval()
with torch.no_grad():
va_pred = model(Xva).argmax(1)
va_acc = (va_pred == yva).float().mean().item()
if epoch % 5 == 0 or epoch == 1:
print(f"epoch {epoch:2d} train_loss {total/n:.4f} val_acc {va_acc:.3f}")
epoch 1 train_loss 2.3019 val_acc 0.101
epoch 5 train_loss 1.9005 val_acc 0.545
epoch 10 train_loss 0.7289 val_acc 0.840
epoch 15 train_loss 0.4439 val_acc 0.872
epoch 20 train_loss 0.3153 val_acc 0.920
epoch 25 train_loss 0.2429 val_acc 0.924
epoch 30 train_loss 0.1983 val_acc 0.948最初のエポックでは正解率が約0.1(10クラスをでたらめに当てる水準)ですが、エポックを重ねるごとに損失が下がり、検証の正解率が上がっていきます。30エポックで検証正解率は0.95近くに達しました。乱数の状態によって数値は多少変わりますが、右肩上がりの傾向は変わりません。
この Step のまとめ
順伝播・損失計算・誤差逆伝播・重み更新という基本のループを回し、損失が下がり検証正解率が上がる様子を確認しました。この4手順は、どんなに大きな深層学習モデルでも変わらない中心の仕組みです。
評価:テストデータで実力を測る
Evaluate on the test set何を・なぜWhat & why
学習が終わったら、これまで一度も使っていないテストデータで最終評価をします。正解率だけでは「どの数字を、何と取り違えたか」が分かりません。そこで混同行列(confusion_matrix)と分類レポート(classification_report:クラスごとの適合率・再現率・F値)も見ます。機械学習モジュールで学んだ評価指標が、そのまま深層学習でも役立ちます。
from sklearn.metrics import confusion_matrix, classification_report
model.eval()
with torch.no_grad():
pred = model(Xte).argmax(1).numpy()
acc = (pred == y_te).mean()
print("test accuracy:", round(float(acc), 3))
print(confusion_matrix(y_te, pred))
print(classification_report(y_te, pred, digits=2))
test accuracy: 0.914
[[35 0 0 0 1 0 0 0 0 0]
[ 0 26 2 2 0 1 1 0 2 2]
[ 0 0 34 0 0 0 0 0 1 0]
[ 0 0 0 36 0 0 0 0 0 1]
[ 0 0 0 0 36 0 0 0 0 0]
[ 0 0 0 0 0 34 0 0 0 3]
[ 1 0 0 0 1 0 34 0 0 0]
[ 0 0 0 0 0 0 0 36 0 0]
[ 0 6 1 0 1 0 0 0 25 2]
[ 0 0 0 0 2 1 0 0 0 33]]
precision recall f1-score support
0 0.97 0.97 0.97 36
1 0.81 0.72 0.76 36
2 0.92 0.97 0.94 35
3 0.95 0.97 0.96 37
4 0.88 1.00 0.94 36
5 0.94 0.92 0.93 37
6 0.97 0.94 0.96 36
7 1.00 1.00 1.00 36
8 0.89 0.71 0.79 35
9 0.80 0.92 0.86 36
accuracy 0.91 360
macro avg 0.91 0.91 0.91 360
weighted avg 0.91 0.91 0.91 360テスト正解率は約0.91。混同行列の対角成分(正しく当てた数)が大きく、全体としてよく分類できています。一方で対角から外れた数字を読むと、取り違えの癖が見えます。たとえば「8」を「1」と、「1」を「8」と間違える例がいくつかあります。8×8と解像度が粗いため、形の似た数字は人間でも紛らわしく、モデルも同じところでつまずきます。誤分類の実例を数件、取り出してみましょう。
# 誤分類したテスト例のインデックスを取り出す
wrong = np.where(pred != y_te)[0]
print("misclassified:", len(wrong), "of", len(y_te))
for i in wrong[:5]:
print(f"index {i:3d} true {y_te[i]} pred {pred[i]}")
misclassified: 31 of 360
index 2 true 8 pred 1
index 3 true 1 pred 8
index 18 true 9 pred 4
index 22 true 8 pred 9
index 25 true 5 pred 9この Step のまとめ
正解率・混同行列・分類レポートで、モデルの実力と取り違えの癖を確かめました。数字の「8」と「1」、「9」と「4」のように形の似たクラスで誤りが出やすい、という具体的な傾向が読み取れます。次はこの結果をどう解釈するかを考えます。
考察:何が言えて、どこへ進むか
Discussion & next steps読み解きReading the result
小さなCNNでも、前処理から学習・評価までを正しくつなげば、手描き数字を9割前後の精度で分類できました。要点は、深層学習の実務がいつも同じ型を踏むことです。データを整え、入力の形を合わせ、損失を下げ、触っていないデータで評価する。この流れは、8×8の digits でも、もっと大きな画像でもまったく同じです。今回の小ささは、その型を素早く一巡するのに向いていました。
注意点Caveats
気をつけることLimits
- 過学習:学習を長く回しすぎると、学習データには強いのに新しいデータで精度が落ちることがあります。学習と検証の正解率の差を見張るのが目安です。
- データ量:digits は1797枚と小さく、簡単な課題です。難しい画像ではもっと多くのデータと大きなモデルが要ります。
- 乱数依存:初期値やシャッフルの乱数で数値は少し動きます。
manual_seedで固定しても環境差は残ります。
次に試すことToward more
- 層やチャネルを増やして、精度がどう変わるかを見る。
Dropoutやデータ拡張で過学習を抑える。- より大きな MNIST や Fashion-MNIST に同じ流れを当てはめる。
精度の数字を鵜呑みにしない
正解率が高くても、それは今回のデータでの話です。実際に使う場面のデータが学習データと違えば、性能は変わります。混同行列で「どこを間違えるか」まで見て、用途に照らして十分かを判断することが大切です。数字は出発点であって、結論ではありません。
発展課題Try more
こう変えたら、どうなる?
一気通貫の流れができたら、少しずつ条件を変えて挙動を確かめましょう。答えは用意しません。手を動かして、数字がどう動くかを観察するのが目的です。
MNIST・Fashion-MNIST に広げる
- 28×28の MNIST(手書き数字)や Fashion-MNIST(衣類画像)に同じ流れを当てはめる。入力の大きさが変わるので、プーリング後の形と全結合層の入力サイズを合わせ直す。
torchvision.datasetsから取得できます。
ネットワークを深くする
- 畳み込み層やチャネル数を増やし、精度と学習時間がどう変わるかを比べる。大きくすれば必ず良くなるわけではない点にも注目。
過学習を抑える
nn.Dropoutを全結合層の前に入れる、あるいは画像を少し回転・平行移動するデータ拡張を加えて、検証・テストの精度が上がるかを見る。
学習曲線を描く
- エポックごとの学習損失と検証正解率を記録し、
matplotlibで折れ線に描く。過学習が始まる点(検証が頭打ちになる時期)を目で確かめる。
まとめWrap-up
つないで見えたもの、次へ
この演習では、単体で学んだ3つの技術を1本の流れにつなぎ、小さな画像データから手描き数字の分類器を作りました。振り返ると、それぞれのモジュールが次の役割を担っていました。
- 前処理:画素を正規化し、CNNが受け取る形に整え、学習・検証・テストに分けた。
- 深層学習:畳み込み・プーリング・全結合でCNNを組み、損失を下げて学習した。
- 機械学習(評価):正解率・混同行列・分類レポートで実力と癖を測った。
一つの技術だけでは完結しなかった課題が、順につなぐと最後まで解けました。各技術の詳しい理論やAPIは、下の各モジュールに戻ると確認できます。
次の演習・段階へ
- 次の演習:開発活動の時系列を読み解く(演習一覧)
- MNIST や Fashion-MNIST で、より本格的な画像分類に挑む。
- 層やハイパーパラメータを差し替えて、精度がどう動くかを体で覚える。
おつかれさまでした
ノートブックを開き、エポック数や層の構成を少し変えて、もう一度上から実行してみてください。検証正解率の伸び方や、混同行列の取り違えの場所がどう動くかを見ると、深層学習のパイプライン全体の勘所がつかめます。参考書としては、斎藤康毅『ゼロから作る Deep Learning』が、畳み込みや誤差逆伝播を手を動かして理解するのに向いています。