Day 111 — PyTorch入門① — Tensor・自動微分の仕組み

2026-08-01 紫 / Phase 5 コーディング Tensor / requires_grad / backward / nn.Module

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

🟣
Day 110 では、XOR問題を手計算・手設計で解きました。重み 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 の ndarrayPyTorch の Tensor
基本的な使い方np.array([1,2,3])torch.tensor([1,2,3])
四則演算・reshape・スライスできるできる(ほぼ同じ書き方)
GPU計算基本的にできない.to('cuda') でGPUに載せられる
計算の履歴を記録するできないrequires_grad=True で記録できる
記録した計算から微分を自動計算自分で数式を書いて計算.backward() の1行で自動計算

🕸️ 計算グラフのイメージ: y = w*x + bloss

タスク3で使う loss = (w*x + b - target)^2 の計算を、PyTorchが裏側でどう記録しているかを図にしたものです。矢印は「順伝播」の向き、点線は .backward() が勾配を流す「逆伝播」の向きを表します。

.backward() が勾配を逆向きに流す w requires_grad x b requires_grad target w*x 中間ノード y_pred = w*x+b loss (y_pred-target)² 実線=順伝播(forward) 点線の黄色矢印=逆伝播(.backward())
💡
直感: requires_grad=True を付けた wb だけが「レシート」に記録され、loss.backward() を呼ぶと出力(loss)から入力(w, b)へ向かって矢印を逆にたどりながら連鎖律で勾配が計算されます。xtarget は学習対象ではないため記録の必要がありません。

🎯 問題

1
ndarray ⇔ Tensor 対応表の理解(理論): 上の対応表を踏まえ、なぜPyTorchが独自のTensor型を使うのかを「GPU計算」「自動微分」の2観点で説明する
2
Tensorの基本操作(コーディング): Tensorの作成・shape確認・転置・reshape・numpyとの相互変換を実装する
3
自動微分と手計算の照合(コーディング): loss.backward() で得た w.gradb.grad を、自分で連鎖律を展開した手計算の値と突き合わせる
4
XORをPyTorchで学習させる(コーディング・メイン): Day 110で手設計した重みを、nn.Module + SGDでランダム初期化から自動的に学習させ、正解率が0.5→1.0に収束する過程を確認する

💡 ヒント

ヒント1方向性

タスク1・2は「numpyの書き方をTensorに置き換えるだけ」で解けます。タスク3は、Day 110の数学補足で出てきた ∂L/∂w = ∂L/∂out × ∂out/∂w を自分で数式展開して手計算し、PyTorchの答えと一致するか照合するだけです。タスク4は、Day 110で「手設計」した部分を「学習」に置き換える回で、Day 110の「発展」で予告していた内容そのものです。

ヒント2アプローチ
  • 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() を挟んで活性化する
ヒント3コード骨格
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という独自の型が必要か

GPU計算の観点: numpyのndarrayはCPU上でしか動きません。ニューラルネットワークの学習は「同じ行列演算を何万回も繰り返す」作業なので、大量のコアを持つGPUで並列化すると桁違いに速くなります。Tensorは .to('cuda') の1行でGPUメモリに乗せられますが、numpyにはこの機能がありません。
🔁
自動微分の観点: Day 110では「XORはOR/NANDの組み合わせ」という知識で手設計を回避しましたが、実際のネットワークは何百万個ものパラメータを持ち、手で微分を導出するのは不可能です。Tensorは 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*x + b - target)²
∂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) によるランダムな初期値から出発し、勾配降下法だけで自動的に同じ役割を果たす重みへ収束していきます。

🕸️ 今日「学習」させたネットワークの構造

x1 x2 入力層 h1 nn.Linear h2 nn.Linear 隠れ層(重みはランダム初期化 → 学習で更新) out BCELoss 出力層 Day 110: 手設計 → Day 111: SGDが自動で重みを発見

Day 110では人間が「h1=OR、h2=NAND」と設計しましたが、今回は nn.Linear がランダムな初期値からスタートし、optimizer.step() の繰り返しだけで同じ役割を果たす重みへ自動的に近づいていきます。

📊 学習曲線: lossが下がり、正解率が1.0に収束する様子

5000エポックの学習ループの中で、1000エポックごとに記録したlossの推移です(縦軸は対数スケール寄りに強調表示。値が小さいほどグラフの下側に位置します)。

loss の推移(エポックごと) epoch 0.714 0.042 0.013 0.008 0.005 0 1000 2000 3000 4000 acc=1.00到達
epoch 0(初期・ランダム)
acc 0.50
loss 0.714
epoch 1000〜4000(学習後)
acc 1.00
loss ≈ 0.005〜0.042
💡
わずか1000エポックほどでlossが0.71から0.04まで急落し、正解率は0.50(ランダムに近い)から1.00へと到達します。Day 110では75%が理論上の上限だった単層パーセプトロンと違い、隠れ層を持つこのネットワークは誰も手で教えていないのに、勾配降下法だけでXORを解く重みを自力で発見します

🪜 Step-by-Step 解説

1Tensorはnumpyの「記憶力付きバージョン」と捉える

t = torch.tensor([[1, 2, 3], [4, 5, 6]])
🔧
なぜこうするか: これまでの学習資産(numpyの操作感覚)をそのまま使い回せるよう、PyTorchはAPIをnumpyに似せて設計されています。「新しいライブラリ」ではなく「記録機能が追加されたnumpy」として捉えると学習コストが下がります。

2requires_grad=True で計算履歴の記録を開始する

w = torch.tensor(3.0, requires_grad=True)
📝
なぜこうするか: このフラグを付けたTensorから作られた計算結果はすべて、裏側で「計算グラフ」としてつながりが記録されます。記録されていない普通のTensorは .backward() を呼んでも勾配を持てません。

3.backward() で連鎖律を自動的に逆向きにたどる

loss.backward()
print(w.grad)
⛓️
なぜこうするか: Day 110で「組立ラインの比喩」として説明した逆伝播が、ここで実際に動いています。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()
🔁
なぜこうするか: この5行の並びは、今後どんなに複雑なモデル(CNN・Transformer等)を組んでも変わらない「学習ループの型」です。zero_grad() を忘れると前回の勾配が蓄積されてしまう点が典型的なミスなので、必ずこの順番で覚えてください。

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

🧾
自動微分(Autograd)の直感: 「計算をするたびに、レシート(どの数値からどの計算を経てこの値になったか)を裏側で自動的に控えておく仕組み」。y = w*x + b を計算すると、PyTorchは裏で「yw*xbの足し算からできた」「w*xwxの掛け算からできた」というレシートを自動保存します。.backward() はこのレシートを出力側から入力側へ逆順にたどりながら、連鎖律(∂L/∂w = ∂L/∂out × ∂out/∂w)を機械的に適用していきます。
なぜ zero_grad() が必要か: Tensorの .grad は「上書き」ではなく「加算」される仕組みです。複数の入力から来た勾配を1つのパラメータで合算したいケース(RNN等)に対応するためですが、そのぶん毎回のループの最初に zero_grad() で前回分をリセットしないと、勾配がループのたびに積み重なり正しい更新ができなくなります。

🏆 Kaggleでの実践的な使い方

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

土台

🧱 すべてのDL系コンペの基礎

Tensor → nn.Module → optimizer

NLP(Day 116〜)・CV(Day 121〜)・時系列DLモデルは、すべて今日学んだ「学習ループの型」の上に成り立っている。

カスタムLoss

🎯 評価指標に合わせた自作Loss

nn.Moduleを継承

Kaggleの評価指標が標準搭載されていない場合、自作Lossを書いて .backward() で自動微分させることができる。

高速化

⚡ GPU学習

model.to('cuda')

Kaggle Notebooksの無料GPU枠で学習を大幅に高速化できる。Day 112以降で本格的に使う。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
Tensorとndarrayはまったく別物だと身構える名前が違うため新しい概念に見える使い方はほぼ同じ。違いはGPU対応と自動微分の2点だけ
requires_grad=True をつけ忘れて .gradNone になるデフォルトでは記録されないことを知らない学習対象のパラメータには明示的に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.SGDtorch.optim.Adam に変えて、同じXORタスクが何エポックで収束するか比較してみましょう。学習率 lr を大きく/小さくすると収束の速さがどう変わるかも確認してみてください
  • 次回予告: Day 112 — PyTorch入門②(Dataset・DataLoader・本格的な学習ループ)。今日は4行だけのXORデータを直接扱いましたが、実際のKaggleコンペでは数万〜数百万行のデータをミニバッチに分けて学習する必要があります。DatasetDataLoader を使い、表形式データをバッチ処理する本格的な学習パイプラインを組みます

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

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

本日(Day 111)は「1-5 DL基礎」の2テーマ目。Day 110で理解した仕組みを、今日はPyTorchの型に落とし込んだ。次回はミニバッチ処理を伴う実務的な学習パイプラインへ進む。

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

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