📚 背景知識(読んでから問題へ)
W1, b1, W2, b2 は自分で数値を決め打ちし、順伝播・逆伝播も自分の頭の中で追いました。今日から導入する PyTorch は、この「手計算の面倒な部分」をすべて自動化してくれるディープラーニング用のライブラリです。numpyとPyTorchの関係
これまで使ってきた numpy の配列(ndarray)と、PyTorchの配列(Tensor)は、見た目も使い方もほぼ同じです。違いは1点だけ ── PyTorchのTensorは「自分がどんな計算を経てこの値になったか」という計算の履歴(計算グラフ)を裏側で記録できるという点です。この記録機能を自動微分(autograd)と呼びます。
Tensor: PyTorchの配列。numpyのndarrayの上位互換
requires_grad: そのTensorに対する計算過程を記録し、あとで微分できるようにするフラグ
計算グラフ:
y = w*x + b のような一連の計算の「つながり」を裏側に記録したもの.backward(): 計算グラフを出力から入力へ逆向きにたどり、連鎖律で各変数の勾配を自動計算する処理(Day 110の「組立ラインの比喩」そのもの)
.grad:
.backward() のあとに勾配が格納される場所nn.Module: 重み・バイアスをまとめて管理し、順伝播の定義だけ書けばよくなるPyTorchの部品
Optimizer:
.grad の値を使って実際に重みを更新する係(SGD・Adam等)
🗂️ ndarray ⇔ Tensor 対応表
まず「何が同じで、何が違うのか」をデータ構造のレベルで整理します。
| 項目 | numpy の ndarray | PyTorch の Tensor |
|---|---|---|
| 基本的な使い方 | np.array([1,2,3]) | torch.tensor([1,2,3]) |
| 四則演算・reshape・スライス | できる | できる(ほぼ同じ書き方) |
| GPU計算 | 基本的にできない | .to('cuda') でGPUに載せられる |
| 計算の履歴を記録する | できない | requires_grad=True で記録できる |
| 記録した計算から微分を自動計算 | 自分で数式を書いて計算 | .backward() の1行で自動計算 |
🕸️ 計算グラフのイメージ: y = w*x + b → loss
タスク3で使う loss = (w*x + b - target)^2 の計算を、PyTorchが裏側でどう記録しているかを図にしたものです。矢印は「順伝播」の向き、点線は .backward() が勾配を流す「逆伝播」の向きを表します。
requires_grad=True を付けた w と b だけが「レシート」に記録され、loss.backward() を呼ぶと出力(loss)から入力(w, b)へ向かって矢印を逆にたどりながら連鎖律で勾配が計算されます。x や target は学習対象ではないため記録の必要がありません。🎯 問題
loss.backward() で得た w.grad・b.grad を、自分で連鎖律を展開した手計算の値と突き合わせるnn.Module + SGDでランダム初期化から自動的に学習させ、正解率が0.5→1.0に収束する過程を確認する💡 ヒント
タスク1・2は「numpyの書き方をTensorに置き換えるだけ」で解けます。タスク3は、Day 110の数学補足で出てきた ∂L/∂w = ∂L/∂out × ∂out/∂w を自分で数式展開して手計算し、PyTorchの答えと一致するか照合するだけです。タスク4は、Day 110で「手設計」した部分を「学習」に置き換える回で、Day 110の「発展」で予告していた内容そのものです。
- Tensor作成:
torch.tensor(data)、numpyとの変換はtorch.from_numpy(arr)とtensor.numpy() - 自動微分:
requires_grad=Trueを付けたTensorだけが.gradを持てる。loss.backward()のあとにw.gradを見る - 学習ループの基本形(今後ずっと使う「型」なので暗記する):
zero_grad() → forward → loss計算 → backward() → step() nn.Linear(in_features, out_features)が「重み+バイアスを持つ1層」を表す。torch.sigmoid()を挟んで活性化する
import torch import torch.nn as nn # --- タスク3: 自動微分の確認 --- x = torch.tensor(2.0) w = torch.tensor(3.0, requires_grad=True) b = torch.tensor(1.0, requires_grad=True) target = torch.tensor(10.0) y_pred = w * x + b loss = (y_pred - target) ** 2 loss.backward() print(w.grad, b.grad) # --- タスク4: XORをnn.Moduleで学習 --- X = torch.tensor([[0.,0.],[0.,1.],[1.,0.],[1.,1.]]) y = torch.tensor([[0.],[1.],[1.],[0.]]) class XORNet(nn.Module): def __init__(self): super().__init__() self.hidden = nn.Linear(2, 2) self.output = nn.Linear(2, 1) def forward(self, x): h = torch.sigmoid(self.hidden(x)) out = torch.sigmoid(self.output(h)) return out model = XORNet() criterion = nn.BCELoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.5) for epoch in range(5000): optimizer.zero_grad() pred = model(X) loss = criterion(pred, y) loss.backward() optimizer.step()
✅ 模範解答
タスク1: なぜTensorという独自の型が必要か
ndarrayはCPU上でしか動きません。ニューラルネットワークの学習は「同じ行列演算を何万回も繰り返す」作業なので、大量のコアを持つGPUで並列化すると桁違いに速くなります。Tensorは .to('cuda') の1行でGPUメモリに乗せられますが、numpyにはこの機能がありません。requires_grad=True を付けるだけで計算グラフを裏側に記録し続け、.backward() の1行で全パラメータの勾配を自動計算します。これがPyTorchを使う最大の理由です。タスク2: Tensorの基本操作
import torch import numpy as np # 1. Tensor作成とshape確認 t = torch.tensor([[1, 2, 3], [4, 5, 6]]) print(t.shape) # torch.Size([2, 3]) # numpyなら: np.array([[1,2,3],[4,5,6]]).shape → (2, 3) # 2. 転置とreshape t_T = t.T print(t_T.shape) # torch.Size([3, 2]) t_flat = t.reshape(-1) # 1次元に変形(numpyのreshape(-1)と同じ) print(t_flat) # tensor([1, 2, 3, 4, 5, 6]) # 3. numpy ⇔ Tensor 相互変換 arr = np.array([1.0, 2.0, 3.0]) t_from_np = torch.from_numpy(arr) # ndarray → Tensor print(t_from_np) back_to_np = t_from_np.numpy() # Tensor → ndarray print(back_to_np, type(back_to_np))
タスク3: 自動微分と手計算の照合
import torch x = torch.tensor(2.0) w = torch.tensor(3.0, requires_grad=True) b = torch.tensor(1.0, requires_grad=True) target = torch.tensor(10.0) y_pred = w * x + b # 7.0 loss = (y_pred - target) ** 2 # (7-10)^2 = 9.0 loss.backward() print(f"PyTorchが計算した dLoss/dw: {w.grad.item()}") # -12.0 print(f"PyTorchが計算した dLoss/db: {b.grad.item()}") # -6.0
∂loss/∂w = 2 × (w*x + b - target) × x
∂loss/∂b = 2 × (w*x + b - target) × 1
w*x + b - target = 3×2 + 1 - 10 = -3
∂loss/∂w = 2 × (-3) × 2 = -12 ← PyTorchの結果と一致
∂loss/∂b = 2 × (-3) × 1 = -6 ← PyTorchの結果と一致
.backward()が行っているのは、まさにこの連鎖律の展開を全自動でやっているだけだと確認できます。
タスク4: XORをPyTorchで学習させる
import torch import torch.nn as nn X = torch.tensor([[0., 0.], [0., 1.], [1., 0.], [1., 1.]]) y = torch.tensor([[0.], [1.], [1.], [0.]]) class XORNet(nn.Module): def __init__(self): super().__init__() self.hidden = nn.Linear(2, 2) # 入力2 → 隠れ2(重み・バイアスはランダム初期化) self.output = nn.Linear(2, 1) # 隠れ2 → 出力1 def forward(self, x): h = torch.sigmoid(self.hidden(x)) out = torch.sigmoid(self.output(h)) return out torch.manual_seed(42) model = XORNet() criterion = nn.BCELoss() # 二値分類用の損失関数 optimizer = torch.optim.SGD(model.parameters(), lr=0.5) for epoch in range(5000): optimizer.zero_grad() pred = model(X) loss = criterion(pred, y) loss.backward() optimizer.step() if epoch % 1000 == 0: acc = ((pred > 0.5).float() == y).float().mean() print(f"epoch {epoch:4d} loss={loss.item():.4f} acc={acc.item():.2f}") final_pred = model(X) final_acc = ((final_pred > 0.5).float() == y).float().mean() print(f"\n最終正解率: {final_acc.item():.2f}")
Day 110では「OR=h1、NAND=h2」と自分で決め打ちしましたが、今回は torch.manual_seed(42) によるランダムな初期値から出発し、勾配降下法だけで自動的に同じ役割を果たす重みへ収束していきます。
🕸️ 今日「学習」させたネットワークの構造
Day 110では人間が「h1=OR、h2=NAND」と設計しましたが、今回は nn.Linear がランダムな初期値からスタートし、optimizer.step() の繰り返しだけで同じ役割を果たす重みへ自動的に近づいていきます。
📊 学習曲線: lossが下がり、正解率が1.0に収束する様子
5000エポックの学習ループの中で、1000エポックごとに記録したlossの推移です(縦軸は対数スケール寄りに強調表示。値が小さいほどグラフの下側に位置します)。
🪜 Step-by-Step 解説
1Tensorはnumpyの「記憶力付きバージョン」と捉える
t = torch.tensor([[1, 2, 3], [4, 5, 6]])
2requires_grad=True で計算履歴の記録を開始する
w = torch.tensor(3.0, requires_grad=True)
.backward() を呼んでも勾配を持てません。3.backward() で連鎖律を自動的に逆向きにたどる
loss.backward()
print(w.grad)
loss から w まで、どんなに層が深くても、.backward() は連鎖律を使って自動的に勾配を計算します。タスク3で手計算と一致したのは偶然ではなく、.backward() の中身が連鎖律そのものだからです。4nn.Module でネットワークの「形」を定義する
class XORNet(nn.Module): def __init__(self): super().__init__() self.hidden = nn.Linear(2, 2) self.output = nn.Linear(2, 1) def forward(self, x): h = torch.sigmoid(self.hidden(x)) return torch.sigmoid(self.output(h))
nn.Linear(2, 2) は、Day 110で手書きした W1, b1 に相当する重み・バイアスをランダムな値で自動生成し、管理してくれる部品です。forward メソッドには「入力から出力までの計算の流れ」だけを書けばよく、パラメータの保持や requires_grad=True の設定はPyTorchが裏で行います。5学習ループの「型」を繰り返す
optimizer.zero_grad() pred = model(X) loss = criterion(pred, y) loss.backward() optimizer.step()
zero_grad() を忘れると前回の勾配が蓄積されてしまう点が典型的なミスなので、必ずこの順番で覚えてください。🧮 数学・統計の補足(文系向け)
y = w*x + b を計算すると、PyTorchは裏で「yはw*xとbの足し算からできた」「w*xはwとxの掛け算からできた」というレシートを自動保存します。.backward() はこのレシートを出力側から入力側へ逆順にたどりながら、連鎖律(∂L/∂w = ∂L/∂out × ∂out/∂w)を機械的に適用していきます。.grad は「上書き」ではなく「加算」される仕組みです。複数の入力から来た勾配を1つのパラメータで合算したいケース(RNN等)に対応するためですが、そのぶん毎回のループの最初に zero_grad() で前回分をリセットしないと、勾配がループのたびに積み重なり正しい更新ができなくなります。🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☐ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☑ 時系列(Time Series)
🧱 すべてのDL系コンペの基礎
Tensor → nn.Module → optimizer
NLP(Day 116〜)・CV(Day 121〜)・時系列DLモデルは、すべて今日学んだ「学習ループの型」の上に成り立っている。
🎯 評価指標に合わせた自作Loss
nn.Moduleを継承
Kaggleの評価指標が標準搭載されていない場合、自作Lossを書いて .backward() で自動微分させることができる。
⚡ GPU学習
model.to('cuda')
Kaggle Notebooksの無料GPU枠で学習を大幅に高速化できる。Day 112以降で本格的に使う。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| Tensorとndarrayはまったく別物だと身構える | 名前が違うため新しい概念に見える | 使い方はほぼ同じ。違いはGPU対応と自動微分の2点だけ |
requires_grad=True をつけ忘れて .grad が None になる | デフォルトでは記録されないことを知らない | 学習対象のパラメータには明示的にrequires_grad=Trueが必要。nn.Linear等は内部で自動的に付けてくれる |
optimizer.zero_grad() を書き忘れる | 手順の1つを見落としがち | 勾配は加算方式なので、書き忘れると前回までの勾配が蓄積し、学習が正しく進まない |
.backward() を呼ぶだけで重みが更新されると思う | 「逆伝播=学習」と混同する | .backward()は勾配計算のみ。実際に重みを動かすのはoptimizer.step()の役割で、両方セットで学習が進む |
| Tensorの中身をprintしただけで値を取り出せたと思う | tensor(7.0, grad_fn=<...>)の表示に戸惑う | requires_grad付きTensorは計算グラフ情報も一緒に持つ。数値だけ取り出すには.item()や.detach().numpy()を使う |
🚀 次のステップ
- 発展:
torch.optim.SGDをtorch.optim.Adamに変えて、同じXORタスクが何エポックで収束するか比較してみましょう。学習率lrを大きく/小さくすると収束の速さがどう変わるかも確認してみてください - 次回予告: Day 112 — PyTorch入門②(Dataset・DataLoader・本格的な学習ループ)。今日は4行だけのXORデータを直接扱いましたが、実際のKaggleコンペでは数万〜数百万行のデータをミニバッチに分けて学習する必要があります。
DatasetとDataLoaderを使い、表形式データをバッチ処理する本格的な学習パイプラインを組みます
Phase 5 の学習マップ(全20テーマ予定)
本日(Day 111)は「1-5 DL基礎」の2テーマ目。Day 110で理解した仕組みを、今日はPyTorchの型に落とし込んだ。次回はミニバッチ処理を伴う実務的な学習パイプラインへ進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: