📚 背景知識(読んでから問題へ)
SimpleCNNクラスの骨組みを組み立て、ダミーTensorでshapeを確認しました。今日はDay 112で学んだDataset/DataLoaderと学習ループの知識を画像データに応用し、FashionMNIST(Tシャツ・スニーカーなど10種類のファッションアイテム、28×28グレースケール画像)でSimpleCNNを実際に学習させます。torchvision.datasets: MNIST・FashionMNIST・CIFAR10などの定番画像データセットを、自分でDatasetを自作せずにdatasets.FashionMNIST(root=..., train=True, download=True)の1行で取得できる。今日のFashionMNISTは(1,28,28)・10クラスで、Day 114のSimpleCNNの想定とそのまま一致する。transforms.ToTensor(): PIL Image(0~255)をfloat32 Tensor(0.0~1.0)に変換transforms.Normalize(mean, std): (x-mean)/stdで値を0付近に揃える正規化nn.CrossEntropyLoss(): 多クラス分類の損失関数。ラベルはクラス番号のまま渡してよい(One-Hot不要)model.eval() / torch.no_grad(): 評価モードへの切り替え、勾配計算の停止
🧵 前処理パイプライン: 生画像 → 学習可能なTensor
🎯 問題
transforms.Composeで前処理パイプラインを作り、datasets.FashionMNISTで学習用・テスト用データセットを読み込むbatch_size=64で学習用(shuffle=True)・テスト用(shuffle=False)のローダーを作り、1バッチのshapeを確認するSimpleCNNを使い、CrossEntropyLoss・Adamで3エポック学習し、各エポックの平均損失をprintするmodel.eval()+torch.no_grad()でテストデータの正解率を計算する💡 ヒント
タスク1〜2はDay 112で表形式データに対して行った「Datasetを作ってからDataLoaderに包む」流れと全く同じで、対象が画像に変わっただけです。torchvisionがDataset部分を用意してくれるので、自分で書くのはtransformの定義だけです。タスク3〜4は「学習ループ=パラメータを動かすフェーズ」「評価ループ=パラメータを動かさず答え合わせだけするフェーズ」という役割の違いを意識すると、torch.no_grad()をどこに付けるべきかが自然に分かります。
download=Trueは初回実行時のみデータをダウンロードする。既にダウンロード済みなら何もしない- バッチの
images.shapeは(batch_size, C, H, W)、labels.shapeは(batch_size,)。SimpleCNNのforwardにそのまま渡せる - 1エポックの平均損失は「
total_loss += loss.item()を毎バッチ積算し、最後にtotal_loss / len(train_loader)で割る」のが定番パターン - 正解数のカウントは
(preds == labels).sum().item()のように、比較結果のブールTensorをsum()すればよい nn.CrossEntropyLoss()はラベルを「クラス番号(0〜9の整数)」のまま受け取る。One-Hotエンコーディングは不要
# タスク1: データセットの読み込み transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) train_dataset = datasets.FashionMNIST(root="./data", train=True, download=True, transform=transform) test_dataset = datasets.FashionMNIST(root="./data", train=False, download=True, transform=transform) # タスク2: DataLoader train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False) # タスク3: 学習ループ model = SimpleCNN() # Day 114で定義したクラス criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) for epoch in range(3): total_loss = 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() print(f"Epoch {epoch+1}: avg_loss={total_loss/len(train_loader):.4f}") # タスク4: 評価ループ model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: outputs = model(images) preds = torch.argmax(outputs, dim=1) correct += (preds == labels).sum().item() total += labels.size(0) print(f"accuracy = {correct/total:.4f}")
✅ 模範解答
タスク1: FashionMNISTのロードと前処理
import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms transform = transforms.Compose([ transforms.ToTensor(), # PIL Image(0~255) → Tensor(0.0~1.0) transforms.Normalize((0.5,), (0.5,)) # (x-0.5)/0.5 → 概ね -1.0〜1.0 ]) train_dataset = datasets.FashionMNIST(root="./data", train=True, download=True, transform=transform) test_dataset = datasets.FashionMNIST(root="./data", train=False, download=True, transform=transform) print(len(train_dataset)) # 60000 image0, label0 = train_dataset[0] print(image0.shape, label0) # torch.Size([1, 28, 28]) 9
タスク2: DataLoaderの作成
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False) images, labels = next(iter(train_loader)) print(images.shape) # torch.Size([64, 1, 28, 28]) print(labels.shape) # torch.Size([64])
タスク3: 学習ループの実装
# SimpleCNNはDay 114で定義したクラスをそのまま使う class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(1, 8, kernel_size=3) self.conv2 = nn.Conv2d(8, 16, kernel_size=3) self.pool = nn.MaxPool2d(2) self.relu = nn.ReLU() self.flatten = nn.Flatten() self.fc = nn.Linear(16 * 5 * 5, 10) def forward(self, x): x = self.pool(self.relu(self.conv1(x))) x = self.pool(self.relu(self.conv2(x))) x = self.flatten(x) x = self.fc(x) return x model = SimpleCNN() criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) for epoch in range(3): total_loss = 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() avg_loss = total_loss / len(train_loader) print(f"Epoch {epoch+1}: avg_loss={avg_loss:.4f}") # Epoch 1: avg_loss=0.5721 # Epoch 2: avg_loss=0.3842 # Epoch 3: avg_loss=0.3391 # (実際の値は初期化・環境により変動する)
タスク4: 評価ループとテスト精度の計算
model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: outputs = model(images) preds = torch.argmax(outputs, dim=1) correct += (preds == labels).sum().item() total += labels.size(0) accuracy = correct / total print(f"accuracy = {accuracy:.4f}") # accuracy = 0.8734(3エポックのみ・実際の値は変動する)
🔁 学習ループの5ステップ(Day 112の復習+画像版)
📊 学習結果のイメージ(loss推移・最終精度)
模範解答の実行例(実際の数値は初期化・環境により変動します)。
🪜 Step-by-Step 解説
1transforms.Composeで前処理パイプラインを組む
transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ])
Composeは複数の変換を順番に適用するパイプラインとしてまとめる。NormalizeはTensor(float)を前提にした演算なので、必ずToTensor()の後に置く必要がある。2datasets.FashionMNISTにtransformを渡す
train_dataset = datasets.FashionMNIST(root="./data", train=True, download=True, transform=transform)
Datasetは__getitem__が呼ばれた瞬間に画像を読み込んで返す設計になっており、transformを渡しておくとその読み込みのタイミングで自動的に前処理まで適用される。Day 112で自作したDatasetの__getitem__をtorchvisionが肩代わりしているイメージ。3DataLoaderでミニバッチに分割する
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
DataLoaderは64枚ずつランダムに取り出しバッチTensorにまとめる。shuffle=Trueは毎エポックで並び順を変え、データの順番自体への過学習を防ぐ。テスト時はshuffle=Falseでよい。4学習ループの5ステップを画像データに適用する
optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step()
model(images)でforward(Conv→ReLU→Pool→Flatten→Linear)が実行され(64,10)のスコアが返る。criterionがスコアとラベルを比較し損失にまとめ、backward()で全パラメータの勾配が計算され、step()でパラメータが更新される。5model.eval() + torch.no_grad()で評価する
model.eval() with torch.no_grad(): ...
torch.no_grad()は勾配計算を止めメモリ・計算量を節約する。model.eval()は今日のモデルには直接効果はないが、Dropoutやバッチ正規化を含むモデルでは動作が変わるため評価時に呼ぶ習慣を身につけておく。🧮 数学・統計の補足(文系向け)
(x-mean)/std は「テストの点数を偏差値に変換する」のと同じ計算: Day 029の偏差値変換は「平均点を50、ばらつきを10に揃えることで異なるテスト同士を公平に比較できるようにする」計算でした。画像の正規化(x-0.5)/0.5も発想は同じで、「ピクセル値の中心を0付近に、ばらつきを一定の範囲に揃えることでニューラルネットが学習しやすい数値スケールに整える」操作です。(64,10)は「64枚それぞれについて10クラスのどれである可能性が高いかのスコア」。CrossEntropyLossは内部でSoftmax関数によりこれを「合計が1になる確率」に変換し、正解クラスの確率が高いほど損失を小さく、低いほど損失を大きくする。「答えを外すほど強くペナルティを受ける」仕組みです。🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 画像認識(CV) / ☐ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 時系列(Time Series)
🧱 4点セットの型
Dataset+transform → DataLoader → 学習ループ → 評価ループ
今日書いた構造はKaggleの画像コンペのほぼ全てで使われる定番テンプレート。データセットが独自の医療画像・衛星画像に変わっても骨組み自体はほとんど変わらない。
📐 正規化の統計量
実データの平均・標準偏差を使う
今日は簡易的にmean=0.5・std=0.5を使ったが、実務・Kaggleでは学習データ全体の実際の統計量を計算するか、ImageNet事前学習モデルを使う場合はImageNetの統計量を使うのが定石。
⏱️ エポック数とEarly Stopping
検証lossが下がらなくなったら止める
今日は3エポックのみで簡易的に学習させたが、実コンペでは学習曲線を見ながら早期終了する。考え方はGBDTの早期終了(Day 091)と同じ。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
NormalizeをToTensorより先に書いてしまう | 変換の順序を意識せずに並べる | Compose内の変換は上から順に適用される。Normalizeは必ずToTensor()の後に置く |
評価ループでもzero_grad()やbackward()を書いてしまう | 学習ループのコードをそのままコピーする | 評価はパラメータを更新しない。torch.no_grad()の中ではbackward()は呼べない |
CrossEntropyLossにOne-Hotラベルを渡してしまう | 分類=One-Hotという固定観念 | nn.CrossEntropyLoss()はクラス番号(整数)をそのまま受け取る設計。One-Hot化すると逆にエラーになる |
total_lossにloss(Tensorのまま)を足し続ける | .item()の意味を意識せずコピーする | Tensorのままだと計算グラフの情報を保持しメモリを圧迫する。.item()でfloatに変換してから足し合わせる |
model.eval()を呼んだら学習を再開できないと思う | 「eval」という名前から終わりを連想する | 評価モードに切り替えるだけで、再度model.train()を呼べば学習モードに戻る |
🚀 次のステップ
- 発展:
nn.CrossEntropyLoss()が内部でSoftmaxを計算していることを確認するために、torch.softmax(outputs, dim=1)を自分で挟んで確率に変換してからargmaxを取っても同じ予測クラスになることを確かめてみましょう - 次回予告: Day 116 — CNN入門③(Data Augmentation・学習曲線の可視化・過学習の兆候の見つけ方)。今日3エポックで止めた学習をもっと長く回すとどうなるか、Augmentationで精度がどう変わるかを実験します
Phase 5 の学習マップ(全20テーマ予定)
Day 114でCNNの構造を組み、Day 115で実際にFashionMNISTを学習させるところまで到達。Phase 5内ではNLPより先に画像認識(CV)から着手している。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: