Day 113 — PyTorch入門③ — GPU学習・モデルの保存/読み込み・チェックポイント

2026-08-03 紫 / Phase 5 理論→コーディング device / state_dict / checkpoint / MPS

📚 背景知識(読んでから問題へ)

🟣
Day 111〜112で、PyTorchの学習ループの「型」(DatasetDataLoaderzero_grad → forward → backward → step)を組み立てました。ここまでは全部CPU上で計算していましたが、実際のKaggleコンペ(特に画像・NLP)では、CPUだけでは学習に何時間・何日もかかることがあります。今日はGPUで学習を高速化する方法と、学習したモデルをファイルに保存し、あとで読み込んで使う方法を学びます。
🖥️
あなたの環境(M1 MacBook Pro・メモリ8GB)について: Kaggle Notebooks上ではNVIDIA GPU(cuda)が使えますが、手元のM1 Macでは代わりに Apple Silicon 用の mps(Metal Performance Shaders)バックエンドが使えます。メモリ8GBという制約上、大きいモデルや大きいバッチサイズはメモリ不足になりやすいので、手元では小さいbatch_sizeで動作確認だけ行い、本格的な学習はKaggle NotebooksのGPUに任せるのが現実的です。
🔑
キーワード
device: 計算をどのハードウェア(cpu/cuda/mps)上で行うかを表すオブジェクト。モデルとデータを同じdeviceに載せる必要がある
state_dict: モデルの全パラメータの値を、レイヤー名をキーとした辞書として取り出したもの
torch.save / torch.load: Pythonオブジェクトをファイルに書き出す/読み込む関数
チェックポイント: モデルの重み・optimizerの状態・epoch数などをまとめて保存し、学習を再開できるようにしたもの

⚖️ CPUとGPU、何が違うのか

ニューラルネットの学習の中身は、そのほとんどが「入力ベクトル × 重み行列」という行列の掛け算です。この計算とCPU/GPUの設計思想の相性を比較します。

項目CPUGPU
コア数少数(4〜16個程度・賢い)大量(数千個・単純)
得意な処理複雑な分岐・順次処理大量の同時並列計算(行列演算)
ニューラルネット学習との相性普通非常に良い(数十〜数百倍速いことも)
Kaggle Notebooksでの入手性常に使える週あたり利用時間に上限あり
あなたの手元環境cpumps(M1 Mac)

💾 今日扱うデータ構造: チェックポイント辞書のスキーマ

今日のタスク4で保存する「チェックポイント」は、以下4つのキーを持つ1つの辞書です。これをtorch.save()で丸ごと1ファイルに書き出します。

キー意味
epochintその時点で完了していたepoch番号。再開時のfor文の開始位置を決める
model_state_dictdict(Tensorの辞書)モデルの全重み・バイアスの値。model.state_dict()の中身そのもの
optimizer_state_dictdictAdam等の内部状態(勾配の移動平均など)。optimizer.state_dict()の中身そのもの
train_lossfloatその時点でのtrain lossの値。ログ確認・デバッグ用

🎯 問題

1
GPUはなぜ速いのか(理論): 学習がGPUで速くなる理由と、Kaggleで常にGPUを使うわけではない理由を、モデル・データ規模の観点から説明する
2
device-agnosticなコード(コーディング): cudampscpu の順で自動判定する get_device() を実装し、モデルと入力Tensorを載せる
3
モデルの保存と読み込み(コーディング): state_dict() をファイルに保存し、新しいインスタンスに読み込んで出力が一致することを確認する
4
チェックポイントで学習再開(コーディング・メイン): 10 epochごとにチェックポイントを保存し、途中のepochから学習を再開できる load_checkpoint() を実装する

💡 ヒント

ヒント1方向性

タスク1は「工場のラインを想像してください。CPUは熟練の職人が1人で丁寧に1つずつ作る、GPUは経験の浅い作業員が1000人で単純作業を同時にこなす」という比喩で考えると整理しやすいです。単純作業(行列の掛け算)が大量にあるときはGPUが圧倒的に有利ですが、モデルもデータもごく小さい場合は、GPUにデータを転送するオーバーヘッドの方が計算時間より大きくなり、CPUの方が速いことすらあります。タスク2〜4は「モデルとデータを同じ場所(device)に置く」「重みだけを取り出して保存する」という2原則さえ押さえれば、Day 111〜112のコードに数行足すだけです。

ヒント2アプローチ
  • device判定: torch.cuda.is_available()torch.backends.mps.is_available() という2つの関数がそれぞれTrue/Falseを返す
  • .to(device) は「新しいTensorを返す」(Tensorの場合)か「その場でモデルを書き換える」(モデルの場合)かの違いに注意。model = model.to(device) のように代入し直せば両方に安全に対応できる
  • 保存には2つの流儀がある: torch.save(model.state_dict(), path)(軽量・推奨)と torch.save(model, path)(クラス定義ごと保存・非推奨)
  • 読み込みは model.load_state_dict(torch.load(path)) の2段階
  • チェックポイントは辞書にまとめて1つのファイルとして torch.save({...}, path) の1行で保存できる。optimizerにもstate_dict()/load_state_dict()が同じように存在する
ヒント3コード骨格
def get_device():
    if torch.cuda.is_available():
        return torch.device("cuda")
    elif torch.backends.mps.is_available():
        return torch.device("mps")
    else:
        return torch.device("cpu")

device = get_device()
model = HousePriceNet().to(device)

# 保存
torch.save(model.state_dict(), "house_price_model.pth")

# 読み込み
loaded_model = HousePriceNet().to(device)
loaded_model.load_state_dict(torch.load("house_price_model.pth", map_location=device))
loaded_model.eval()

# チェックポイント保存
checkpoint = {
    "epoch": epoch,
    "model_state_dict": model.state_dict(),
    "optimizer_state_dict": optimizer.state_dict(),
    "train_loss": loss.item(),
}
torch.save(checkpoint, f"checkpoint_epoch{epoch}.pth")

模範解答

タスク1: GPUが速い理由・常には使わない理由

GPUが速い理由: ニューラルネットワークの学習の大部分は「入力ベクトル × 重み行列」という行列の掛け算で、各要素の計算が互いに独立しているため並列化しやすい性質を持っています。CPUは数個〜十数個の賢いコアで順番に処理するのに対し、GPUは数千個の単純なコアで一斉に処理できるため、行列演算のような「同じ計算を大量に繰り返す」処理では圧倒的に高速になります。
🐢
常にGPUを使わない理由: GPUを使うにはCPU側からGPU側へデータを転送する必要があり、一定のオーバーヘッドがかかります。Day 112のようにモデルもデータも非常に小さい場合、この転送時間の方が計算時間そのものより長くなり、CPUの方が速いことすらあります。また、Kaggle NotebooksのGPUには週あたりの利用時間上限があるため、小さい実験はCPUで、GPUは本番の長時間学習のために温存する、という使い分けも実務では行われます。

タスク2: device-agnosticなコード

import torch

def get_device():
    if torch.cuda.is_available():
        return torch.device("cuda")
    elif torch.backends.mps.is_available():
        return torch.device("mps")
    else:
        return torch.device("cpu")

device = get_device()
print(device)  # 例: device(type='mps')(M1 Macの場合)/ device(type='cpu')(GPUなし環境)

model = HousePriceNet().to(device)
dummy_input = torch.randn(4, 3).to(device)  # バッチサイズ4、特徴量3個
output = model(dummy_input)
print(output.device)  # モデルと同じdeviceに出力があることを確認

タスク3: モデルの保存と読み込み

import torch

torch.manual_seed(42)
model = HousePriceNet().to(device)
# (本来はDay 112のように学習済みである想定)

# --- 保存 ---
torch.save(model.state_dict(), "house_price_model.pth")

# --- 読み込み ---
loaded_model = HousePriceNet().to(device)
loaded_model.load_state_dict(torch.load("house_price_model.pth", map_location=device))
loaded_model.eval()

# --- 出力が一致することの確認 ---
test_input = torch.tensor([[90.0, 3, 10]], dtype=torch.float32).to(device)
with torch.no_grad():
    out_original = model(test_input)
    out_loaded = loaded_model(test_input)

print(torch.allclose(out_original, out_loaded))  # True

タスク4: チェックポイントを使った学習再開

import torch, os
import torch.nn as nn
from torch.utils.data import DataLoader
from sklearn.model_selection import train_test_split

def save_checkpoint(path, epoch, model, optimizer, train_loss):
    torch.save({
        "epoch": epoch,
        "model_state_dict": model.state_dict(),
        "optimizer_state_dict": optimizer.state_dict(),
        "train_loss": train_loss,
    }, path)

def load_checkpoint(path, model, optimizer, device):
    checkpoint = torch.load(path, map_location=device)
    model.load_state_dict(checkpoint["model_state_dict"])
    optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
    return checkpoint["epoch"], checkpoint["train_loss"]

device = get_device()
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)

torch.manual_seed(42)
model = HousePriceNet().to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

start_epoch = 0
if os.path.exists("checkpoint_epoch40.pth"):
    start_epoch, last_loss = load_checkpoint("checkpoint_epoch40.pth", model, optimizer, device)
    print(f"epoch {start_epoch} から再開(train_loss={last_loss:.1f})")
    start_epoch += 1

for epoch in range(start_epoch, 50):
    model.train()
    train_losses = []
    for batch_X, batch_y in train_loader:
        batch_X, batch_y = batch_X.to(device), batch_y.to(device)
        optimizer.zero_grad()
        pred = model(batch_X)
        loss = criterion(pred, batch_y)
        loss.backward()
        optimizer.step()
        train_losses.append(loss.item())

    avg_train_loss = sum(train_losses) / len(train_losses)

    if (epoch + 1) % 10 == 0:
        save_checkpoint(f"checkpoint_epoch{epoch}.pth", epoch, model, optimizer, avg_train_loss)
        print(f"epoch {epoch:3d}  train_loss={avg_train_loss:9.1f}  → チェックポイント保存")

# 出力例(初回実行時):
# epoch   9  train_loss= 12043.5  → チェックポイント保存
# epoch  19  train_loss=  7210.8  → チェックポイント保存
# epoch  29  train_loss=  5890.1  → チェックポイント保存
# epoch  39  train_loss=  5210.4  → チェックポイント保存
# epoch  49  train_loss=  4890.7  → チェックポイント保存
#
# (再実行時、checkpoint_epoch40.pthが存在すれば)
# epoch 40 から再開(train_loss=5210.4)

🔗 モデルの保存・復元フロー全体像

学習中のmodel on device state_dict() 重みの辞書 torch.save() .pth ファイル 再起動後 新しいmodel load_state_dict 同じクラス定義(HousePriceNet)が両端に必要 —— コードとファイルは常にセットで管理する

📊 GPU高速化のイメージ(概念図)

下の数値はイメージをつかむための概念的な例です(実際の倍率はモデル規模・データ量・GPU種類によって大きく変わります)。「単純作業を並列にこなす」性質が強く出るほど、GPUの優位性が大きくなります。

Day112規模(3特徴量・200行)
CPU ≈ GPU(転送コストが支配的)
差はほぼ無し〜CPU優位
中規模Tabular NN(数百特徴量)
GPU 数倍速い
GPUが有利になり始める
CNN・NLP(画像/大規模モデル)
GPU 数十〜数百倍速い
GPU無しでは現実的に学習不能
💡
今日(Day 113)扱った HousePriceNet のような小さいモデルでは、GPUの恩恵はほぼありません。Day 114以降のCNN(畳み込みニューラルネットワーク)でモデルとデータの規模が一気に大きくなった時、今日書いた get_device() のdevice-agnosticなコードがそのまま効いてきます。

🪜 Step-by-Step 解説

1get_device() で実行環境を自動判定する

def get_device():
    if torch.cuda.is_available():
        return torch.device("cuda")
    elif torch.backends.mps.is_available():
        return torch.device("mps")
    else:
        return torch.device("cpu")
🔧
なぜこうするか: Kaggle Notebooks上はcuda、あなたのM1 Mac上ではmps、どちらもないクラウド環境ではcpuと、実行環境ごとに使えるハードウェアが変わります。コードに"cuda"と直接書くとGPUがない環境でエラーになります。優先順位をつけてフォールバックすることで、どの環境でもコードを書き換えずにそのまま動くようになります。

2モデルとデータを同じdeviceに載せる

model = HousePriceNet().to(device)
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
📦
なぜこうするか: PyTorchは「モデルの重みがGPU上にあるのに入力データがCPU上にある」状態を許しません(RuntimeErrorになります)。学習ループの中でバッチを取り出すたびに.to(device)するのが定石です。Day 112のコードには無かったこの1行が、今日追加された最大のポイントです。

3state_dict だけを保存する理由

torch.save(model.state_dict(), "house_price_model.pth")
🚧
なぜこうするか: torch.save(model, path)(モデルそのものを保存)はモデルのクラス定義が読み込み側にも存在しないと復元できず、PyTorchのバージョンが変わると壊れることもあります。state_dict()だけを保存する方式は「読み込み側で同じ構造のモデルを新しく作ってから、数値だけ流し込む」2段階になるため、コードの変更に強くファイルサイズも小さく済みます。Kaggle実務でもほぼ必ずこちらが使われます。

4チェックポイントに「再開に必要な全情報」を詰め込む

checkpoint = {
    "epoch": epoch,
    "model_state_dict": model.state_dict(),
    "optimizer_state_dict": optimizer.state_dict(),
    "train_loss": train_loss,
}
🔁
なぜこうするか: モデルの重みだけ保存して学習を再開すると、Adamのような最適化アルゴリズムが内部に持つ「勾配の移動平均」がリセットされ、再開直後に学習が不安定になることがあります。optimizer.state_dict()も一緒に保存・復元することで、まるで学習を一度も止めなかったかのように継続できます。epochを保存するのは再開位置を正しく決めるためです。

🧮 数学・統計の補足(文系向け)

🍎
GPU並列化の直感: 「100人の作業員が、1人1個ずつ同時にリンゴの皮をむけば、1人が100個を順番にむくより100倍速い」——これがGPUの本質です。ただし「まず倉庫からリンゴを100人全員に配る(データ転送)」という準備時間がかかります。リンゴが3個しかないのに100人集めても、配る手間の方が大きくて逆に遅くなる、というのがタスク1の「小さいモデルではGPUが遅いこともある」の直感です。
📏
state_dictは「体重測定の記録」のようなもの: モデルのクラス定義(HousePriceNetのコード)は「人体の設計図」、state_dict()は「今この人の体重・身長を測った記録」に例えられます。設計図さえ手元にあれば、記録を読み込むだけで「同じ体格の人」を再現できます。逆に記録だけあっても設計図がなければ何も再現できません。だから「モデルのコード」と「保存した.pthファイル」は必ずセットで管理する必要があります。

🏆 Kaggleでの実践的な使い方

よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☑ 時系列(Time Series)

標準構成

💾 学習・推論の2段構成

学習用ノートブック → .pth → 推論用ノートブック

「学習用ノートブック」で保存した.pthファイルをKaggle Datasetとしてアップロードし、「推論専用ノートブック(インターネット接続なし)」で読み込んで予測するのが標準的な構成。

拡張ポイント

🎯 Fold別モデルの保存

model_fold0.pth, model_fold1.pth, ...

K-Foldで学習した各Foldのモデルを個別に保存し、推論時に全Foldを読み込んでアンサンブルするのが定番戦略。

実務装備

⏹️ Best Model保存

validが一番良かった時点だけ保存

validのlossが更新されるたびに上書き保存し、学習終了後は「一番良かった時点」のモデルを使う。Day 112で見た過学習への対処そのもの。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
モデルだけ.to(device)してデータを忘れる「モデルを乗せれば全部GPUで動く」と誤解する入力Tensor(batch_X, batch_y)も毎バッチ.to(device)しないとRuntimeErrorになる
torch.save(model, path)を使ってしまう手軽に見えて直感的クラス定義に依存し壊れやすいため、Kaggle実務ではstate_dict()方式が標準
別のマシン(GPUなし環境)で読み込んでエラーになる保存した環境と同じ前提で読み込もうとするtorch.load(path, map_location=device)で読み込み先のdeviceを明示的に指定する
optimizerの状態を保存し忘れて再開後に性能が落ちる「重みさえ戻せば十分」と思い込むAdam等はoptimizer自身も内部状態を持つため、optimizer.state_dict()も一緒に保存・復元する
推論時にmodel.eval()を呼び忘れる保存・読み込みだけに気を取られるDay 112で学んだ通り、推論・検証時は必ずmodel.eval()を呼ぶ習慣をここでも継続する

🚀 次のステップ

  • 発展: torch.save(model, path)state_dict()方式それぞれでファイルを保存し、os.path.getsize()でファイルサイズを比較してみましょう
  • 次回予告: Day 114 — CNN入門①(畳み込み・プーリングの直感、画像データの扱い方)。Phase 5の「1-5 DL基礎」を終え、「6-10 NLP」の前に、画像認識(CV)の基礎である畳み込みニューラルネットワーク(CNN)へ進みます

Phase 5 の学習マップ(全20テーマ予定)

1-5 DL基礎(PyTorch)
6-10 NLP
11-15 CV
16-18 時系列
19-20 マルチモーダル

本日(Day 113)は「1-5 DL基礎」の最終回。GPU活用とモデル保存/読み込みで、PyTorchの基本装備が一通り揃った。次回からCNN(画像認識)に進む。

📝 自己評価(解いた後に記入)

自分の回答・気づき・メモ: