📚 背景知識(読んでから問題へ)
model(X) に一発で全部渡して学習しました。しかし実際のKaggleコンペのデータは数万〜数百万行あります。これを一度にすべてメモリに載せて計算しようとすると、メモリが足りなくなったり、1回のパラメータ更新に時間がかかりすぎたりします。そこで実務では、データをミニバッチ(mini-batch)に分割して少しずつ学習します。この面倒な作業を自動化してくれるのが Dataset と DataLoader です。Dataset: 「データが何件あるか」「i番目のデータの取り出し方」を定義する部品
DataLoader: Datasetからミニバッチを自動的に切り出し、シャッフルまでしてくれる部品
batch_size: 1回の重み更新に使うデータ件数
shuffle=True: epochごとにデータの並び順をランダムに変える設定(学習データ側で使う)
train/valid split: 学習に使うデータと、未知データへの予測力を確認するためのデータに分けること
⚖️ 3つの学習方法の違い
「1回の重み更新にどれだけのデータを使うか」で、勾配降下法は3種類に分かれます。
| 方式 | 1回の更新に使うデータ量 | 特徴 |
|---|---|---|
| バッチ勾配降下法(Full-batch) | 全データ | 勾配は正確だが、1回の更新が重く遅い。メモリを大量に使う |
| 確率的勾配降下法(SGD, 1件ずつ) | 1行だけ | 更新は速いが、勾配のブレ(ノイズ)が大きく不安定 |
| ミニバッチ勾配降下法 | 数十〜数百行 | 両者のいいとこ取り。実務・Kaggleではほぼ常にこれを使う |
🏠 使用データ: 疑似House Price(200件)
Day 85で扱ったHouse Priceテーマの簡易版です。面積・部屋数・築年数から価格を予測する回帰タスクとして扱います。
| カラム | 型 | 意味 |
|---|---|---|
area_sqm | float | 面積(㎡)。平均90、標準偏差25の正規分布から生成、30〜200にクリップ |
rooms | int | 部屋数(1〜5のランダム整数) |
age_years | int | 築年数(0〜39のランダム整数) |
price_man_yen | float | 価格(万円)。area×60 + rooms×300 − age×15 + ノイズ で生成、目的変数 |
import numpy as np import pandas as pd np.random.seed(42) n = 200 df = pd.DataFrame({ "area_sqm": np.random.normal(90, 25, n).clip(30, 200), "rooms": np.random.randint(1, 6, n), "age_years": np.random.randint(0, 40, n), }) df["price_man_yen"] = ( df["area_sqm"] * 60 + df["rooms"] * 300 - df["age_years"] * 15 + np.random.normal(0, 300, n) ).clip(500, None)
🎯 問題
torch.utils.data.Dataset でラップした HouseDataset クラスを実装するbatch_size=32, shuffle=True でミニバッチを切り出し、各バッチのshapeを確認する(端数バッチにも注目)💡 ヒント
タスク1は「バケツで水を運ぶとき、1滴ずつ運ぶのは遅すぎる、貯水池ごと運ぶのは重すぎる、ちょうどいい大きさのバケツで何往復もするのが効率的」という比喩で考えると整理しやすいです。タスク2・3・4は、Day 111で書いた学習ループの「型」(zero_grad → forward → backward → step)を、1バッチずつ回すループの中に入れ子にするだけです。
Datasetの最低限の実装は3つのメソッドだけ:__init__,__len__(self),__getitem__(self, idx)DataLoader(dataset, batch_size=32, shuffle=True)を作れば、for X, y in loader:でバッチが自動的に流れてくる- 検証時は勾配計算が不要なので
with torch.no_grad():で囲む。model.eval()/model.train()の切り替えもセットで習慣化する train_test_split(df, test_size=40, random_state=42)のように行数指定もできる
class HouseDataset(Dataset): def __init__(self, df): self.X = torch.tensor(df[["area_sqm", "rooms", "age_years"]].values, dtype=torch.float32) self.y = torch.tensor(df[["price_man_yen"]].values, dtype=torch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] train_df, valid_df = train_test_split(df, test_size=40, random_state=42) train_loader = DataLoader(HouseDataset(train_df), batch_size=32, shuffle=True) valid_loader = DataLoader(HouseDataset(valid_df), batch_size=32, shuffle=False) for epoch in range(50): model.train() for batch_X, batch_y in train_loader: optimizer.zero_grad() pred = model(batch_X) loss = criterion(pred, batch_y) loss.backward() optimizer.step() model.eval() with torch.no_grad(): valid_losses = [criterion(model(vX), vy).item() for vX, vy in valid_loader]
✅ 模範解答
タスク1: なぜミニバッチが必要か
タスク2: HouseDataset の実装
import torch from torch.utils.data import Dataset class HouseDataset(Dataset): def __init__(self, df): feature_cols = ["area_sqm", "rooms", "age_years"] self.X = torch.tensor(df[feature_cols].values, dtype=torch.float32) self.y = torch.tensor(df[["price_man_yen"]].values, dtype=torch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] dataset = HouseDataset(df) print(len(dataset)) # 200 print(dataset[0]) # (tensor([...3個の特徴量...]), tensor([価格]))
タスク3: DataLoader でミニバッチを確認
from torch.utils.data import DataLoader loader = DataLoader(dataset, batch_size=32, shuffle=True) for i, (batch_X, batch_y) in enumerate(loader): print(f"batch {i}: X.shape={tuple(batch_X.shape)} y.shape={tuple(batch_y.shape)}") # 出力例(200件 ÷ 32 = 6余り8 なので、バッチは7個。最後だけ8件になる): # batch 0: X.shape=(32, 3) y.shape=(32, 1) # ... # batch 6: X.shape=(8, 3) y.shape=(8, 1)
タスク4: train/valid split + 本格的な学習ループ
import torch import torch.nn as nn from torch.utils.data import DataLoader from sklearn.model_selection import train_test_split train_df, valid_df = train_test_split(df, test_size=40, random_state=42) train_loader = DataLoader(HouseDataset(train_df), batch_size=32, shuffle=True) valid_loader = DataLoader(HouseDataset(valid_df), batch_size=32, shuffle=False) class HousePriceNet(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential( nn.Linear(3, 16), nn.ReLU(), nn.Linear(16, 1), ) def forward(self, x): return self.net(x) torch.manual_seed(42) model = HousePriceNet() criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.01) for epoch in range(50): model.train() train_losses = [] for batch_X, batch_y in train_loader: optimizer.zero_grad() pred = model(batch_X) loss = criterion(pred, batch_y) loss.backward() optimizer.step() train_losses.append(loss.item()) model.eval() with torch.no_grad(): valid_losses = [criterion(model(vX), vy).item() for vX, vy in valid_loader] if epoch % 10 == 0 or epoch == 49: print(f"epoch {epoch:3d} train_loss={sum(train_losses)/len(train_losses):9.1f} " f"valid_loss={sum(valid_losses)/len(valid_losses):9.1f}") # 出力例: # epoch 0 train_loss= 42381.2 valid_loss= 38214.5 # epoch 10 train_loss= 9123.4 valid_loss= 9870.1 # epoch 20 train_loss= 6210.8 valid_loss= 7433.2 # epoch 30 train_loss= 5480.1 valid_loss= 7290.6 # epoch 40 train_loss= 5102.3 valid_loss= 7350.9 ← validが下げ止まる # epoch 49 train_loss= 4890.7 valid_loss= 7402.5 ← 過学習の兆候
🔗 今日組み立てたパイプライン全体像
📊 学習曲線: trainは下がり続け、validは下げ止まる
50 epochの学習で、10epochごとに記録したtrain/validのMSE lossです。epoch 30あたりからvalidがtrainより下げ止まり、過学習の兆候が現れます。
🪜 Step-by-Step 解説
1Dataset は「データの取り出し方の説明書」
class HouseDataset(Dataset): def __init__(self, df): ... def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx]
__len__で、「i番目のデータを取り出すにはどうすればよいか」を__getitem__で知りたいだけです。この2つさえ実装すれば、画像でもテキストでも表形式データでも、DataLoaderが同じ仕組みでバッチを切り出せます。2DataLoader にバッチ化とシャッフルを丸投げする
loader = DataLoader(dataset, batch_size=32, shuffle=True)
3学習ループを「epochループ × バッチループ」の二重構造にする
for epoch in range(50): for batch_X, batch_y in train_loader: ... # Day 111の5行の型がそのまま入る
zero_grad → forward → backward → stepという中身の型は一切変わりません。この二重ループの形は、Kaggleで使うほぼ全てのPyTorchコードの土台になります。4model.eval() と torch.no_grad() でtrainとvalidを切り分ける
model.eval() with torch.no_grad(): valid_losses = [criterion(model(vX), vy).item() for vX, vy in valid_loader]
optimizer.step()で重みを更新してはいけません。torch.no_grad()は計算グラフの記録自体を止めるため、無駄なメモリ消費も防げます。model.eval()は今回直接効果はありませんが、DropoutやBatchNormを使うDay 113以降で必須になる作法として、今のうちから習慣にしておきます。5train_lossとvalid_lossの推移から過学習を読み取る
🧮 数学・統計の補足(文系向け)
MSELoss の中身: MSE = (1/n)Σ(yᵢ − ŷᵢ)² 。これは「予測と正解のズレを2乗して平均する」というDay 50で学んだ回帰の評価指標RMSEの2乗版そのもので、nn.MSELoss()はこれを1バッチ分について自動計算しているだけです。🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☑ 時系列(Time Series)
🧱 全DLパイプラインの3点セット
Dataset → DataLoader → train/valid split
画像コンペでもNLPコンペでも一字一句同じ構造で書かれる、DLパイプラインの土台。
🎯 カスタムDataset
__getitem__に前処理を書き込む
画像コンペでは画像読み込み+Augmentation、NLPコンペではトークナイズをここに書く。今日学んだ表形式版がその最も単純な形。
⏹️ Early Stoppingの土台
validの下げ止まりを検知
今日確認した「validのlossが下げ止まる」瞬間を検知して学習を止める仕組みは、後日のテーマで実装する。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
validデータでもshuffle=Trueにしてしまう | trainと同じ設定をコピペする | validはシャッフルしても結果に影響しないが、慣習としてshuffle=Falseにする(順序を保って結果を後から突き合わせやすくするため) |
model.eval()を呼び忘れる | 「学習ループが動けばOK」と思ってしまう | Dropout・BatchNormを使うモデルではtrain()/eval()の切り替え忘れが予測精度のバグに直結する。今日は影響なくても習慣化する |
valid側でもoptimizer.step()を呼んでしまう | trainのコードをそのままコピーする | validは「モデルの性能を測るだけ」の場。重み更新はtrainループの中だけで行う |
| 最後のバッチのサイズが揃わないことに気づかず処理が落ちる | batch_size=32なら常に32件だと思い込む | データ件数がbatch_sizeで割り切れない場合、最後のバッチは端数になる。drop_last=Trueで切り捨てることも可能 |
| train_lossだけを見て「うまく学習できた」と判断する | validを見る習慣がない | train_lossが下がってもvalid_lossが下げ止まっていれば過学習。必ず両方を並べて確認する |
🚀 次のステップ
- 発展:
batch_sizeを8・32・128と変えて、学習の安定性(loss推移のギザギザ具合)と収束速度がどう変わるか比較してみましょう - 次回予告: Day 113 — PyTorch入門③(GPU学習・モデルの保存/読み込み・チェックポイント)。今日はCPU上で学習しましたが、Kaggle Notebooksの無料GPUを使う
model.to('cuda')の書き方と、学習済みモデルをtorch.save/torch.loadで保存・復元する方法を学びます
Phase 5 の学習マップ(全20テーマ予定)
本日(Day 112)は「1-5 DL基礎」の続き。Day 111の学習ループを、実務規模のミニバッチ処理に拡張した。次回はGPU活用とモデル保存へ進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: