📚 背景知識(読んでから問題へ)
Dataset → DataLoader → zero_grad → forward → backward → step)を組み立てました。ここまでは全部CPU上で計算していましたが、実際のKaggleコンペ(特に画像・NLP)では、CPUだけでは学習に何時間・何日もかかることがあります。今日は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の設計思想の相性を比較します。
| 項目 | CPU | GPU |
|---|---|---|
| コア数 | 少数(4〜16個程度・賢い) | 大量(数千個・単純) |
| 得意な処理 | 複雑な分岐・順次処理 | 大量の同時並列計算(行列演算) |
| ニューラルネット学習との相性 | 普通 | 非常に良い(数十〜数百倍速いことも) |
| Kaggle Notebooksでの入手性 | 常に使える | 週あたり利用時間に上限あり |
| あなたの手元環境 | cpu | mps(M1 Mac) |
💾 今日扱うデータ構造: チェックポイント辞書のスキーマ
今日のタスク4で保存する「チェックポイント」は、以下4つのキーを持つ1つの辞書です。これをtorch.save()で丸ごと1ファイルに書き出します。
| キー | 型 | 意味 |
|---|---|---|
epoch | int | その時点で完了していたepoch番号。再開時のfor文の開始位置を決める |
model_state_dict | dict(Tensorの辞書) | モデルの全重み・バイアスの値。model.state_dict()の中身そのもの |
optimizer_state_dict | dict | Adam等の内部状態(勾配の移動平均など)。optimizer.state_dict()の中身そのもの |
train_loss | float | その時点でのtrain lossの値。ログ確認・デバッグ用 |
🎯 問題
cuda → mps → cpu の順で自動判定する get_device() を実装し、モデルと入力Tensorを載せるstate_dict() をファイルに保存し、新しいインスタンスに読み込んで出力が一致することを確認するload_checkpoint() を実装する💡 ヒント
タスク1は「工場のラインを想像してください。CPUは熟練の職人が1人で丁寧に1つずつ作る、GPUは経験の浅い作業員が1000人で単純作業を同時にこなす」という比喩で考えると整理しやすいです。単純作業(行列の掛け算)が大量にあるときはGPUが圧倒的に有利ですが、モデルもデータもごく小さい場合は、GPUにデータを転送するオーバーヘッドの方が計算時間より大きくなり、CPUの方が速いことすらあります。タスク2〜4は「モデルとデータを同じ場所(device)に置く」「重みだけを取り出して保存する」という2原則さえ押さえれば、Day 111〜112のコードに数行足すだけです。
- 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()が同じように存在する
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が速い理由・常には使わない理由
タスク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)
🔗 モデルの保存・復元フロー全体像
📊 GPU高速化のイメージ(概念図)
下の数値はイメージをつかむための概念的な例です(実際の倍率はモデル規模・データ量・GPU種類によって大きく変わります)。「単純作業を並列にこなす」性質が強く出るほど、GPUの優位性が大きくなります。
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")
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)
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,
}
optimizer.state_dict()も一緒に保存・復元することで、まるで学習を一度も止めなかったかのように継続できます。epochを保存するのは再開位置を正しく決めるためです。🧮 数学・統計の補足(文系向け)
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テーマ予定)
本日(Day 113)は「1-5 DL基礎」の最終回。GPU活用とモデル保存/読み込みで、PyTorchの基本装備が一通り揃った。次回からCNN(画像認識)に進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: