このコマでは、PyTorchの全体像をつかみ、NumPyとの違いを理解しつつ、最終的に線形回帰モデルを実装する。さらに、学習ループを段階的に簡素化して、PyTorchらしい書き方へ移行する。
進め方(ハンズオン)¶
上から順にセルを実行し、説明セル→コードセルのセットで手を動かすこと。途中の結果が想定と違う場合は、shape・dtype・device・requires_grad を最初に疑う。
章末の「ハンズオン問題」は、問題文に示した答え欄にのみ(整数・小数・True/False のいずれか一つ)を記入する形式である。コードセルは自分の検算用に使い、提出物に答えを直書きしない運用としてもよい。
このノートはCUDAが利用できる環境で実行すること.colabのランタイム変更(CPU→GPU)は以下を参考にしてください.
概要¶
PyTorchは、深層学習や機械学習を実装するためのPythonライブラリである。単なる数値計算ライブラリではなく、モデル定義・勾配計算・パラメータ更新・GPU活用までを一貫して扱える点が特徴である。
とくに、次の4点が学習実装において重要である。
Tensorを中心に数値計算を統一的に扱える自動微分(Autograd)により勾配計算を自動化できる
GPUを活用して高速に学習できる
研究用途から実運用まで幅広く使われる
このノートでは、Tensor操作とNumPyとの対応を押さえたうえで、線形回帰を手作業の勾配降下から nn / DataLoader へと段階的に書き換える。中盤では小さなMLPや train/eval、損失の reduction に触れ、後半では数値答案形式の演習と、NMF・ロジスティック回帰の発展課題で実装の幅を広げる。
PyTorchの主要コンセプト¶
PyTorchを理解するには、まず次の用語を押さえるとよい。
Tensor: PyTorchにおける基本データ構造。NumPy配列に近いが、GPU上での計算や自動微分に対応している。
Autograd: 計算グラフを追跡し、微分を自動で求める仕組み。
requires_grad=Trueを付けたTensorが対象になる。nn.Module: モデルを部品化して定義するための基底クラス。
nn.Linearやnn.Sequentialで構築できる。Optimizer: 勾配を使ってパラメータを更新するアルゴリズム。
SGDやAdamなどがある。Loss関数: 予測と正解のズレを数値化する関数。回帰では
MSELossがよく使われる。
さらに、活性化関数は主に次の2箇所に実装されている。
torch.nn(例:nn.ReLU,nn.Sigmoid,nn.Tanh)torch.nn.functional(例:F.relu,F.sigmoid,F.tanh)
nn はレイヤとしてモデル定義に組み込みたいときに使いやすく、functional は順伝播内で関数として直接呼びたいときに便利である。
これらを組み合わせることで、実用的な学習コードを短く、見通しよく書ける。
PyTorchの利点¶
書きやすい: Pythonらしい文法で直感的に記述できる。デバッグもしやすく、学習過程を追いやすい。
柔軟: 実験的なモデルを素早く試せる。研究段階の試行錯誤に強い。
高速: GPU活用で大規模データにも対応できる。CPUとGPUの切り替えも比較的容易である。
エコシステム:
torchvisionなど周辺ライブラリが充実している。実務での再利用性: 学習コードを保守しやすく、推論パイプラインへ接続しやすい。
どんなところで使われるか¶
PyTorchは以下のような分野で活用される。
画像認識(分類、物体検出、セグメンテーション)
自然言語処理(翻訳、要約、チャットボット)
音声処理(音声認識、音声合成)
強化学習(ゲームAI、ロボット制御)
生成AI(画像生成、テキスト生成)
数式で学んだ勾配降下法をコードで確かめるツールとしても使いやすい。研究現場では新規モデルの試作に、実務では既存モデルの微調整や推論サービス構築に広く利用される。
import numpy as np
import torch
from torch import nn
print(f"PyTorch: {torch.__version__}")
print(f"NumPy : {np.__version__}")
print(f"CUDA利用可能: {torch.cuda.is_available()}")PyTorch: 2.11.0
NumPy : 2.4.4
CUDA利用可能: False
以降のセルでは、Tensor の基本操作から始める。計算結果がどのように変化するかを必ず手元で確かめること。
torch.tensorの使い方¶
Tensor は多次元配列であり、NumPy配列に近い操作感を持つ。まずは生成・形状・dtypeを確認する。
Tensorで最初に押さえるポイント¶
shape: データの次元構造。全結合層などはこの形に依存して計算する。dtype: 数値型。通常はfloat32を使う。device:cpuかcudaか。モデルとデータは同じdeviceに置く必要がある。requires_grad: 勾配を追跡するかどうか。学習対象パラメータではTrueを使う。
この4つを意識すると、学習コードで起こりやすいエラーの多くを回避できる。
import torch
print(f"PyTorchのバージョン: {torch.__version__}")
print(f"CUDAの利用可能性: {torch.cuda.is_available()}")
print(f"CUDAのバージョン: {torch.version.cuda}")
print(f"CUDNNが有効: {torch.backends.cudnn.enabled}")
print(f"CUDNNのバージョン: {torch.backends.cudnn.version()}")PyTorchのバージョン: 2.11.0
CUDAの利用可能性: False
CUDAのバージョン: None
CUDNNが有効: True
CUDNNのバージョン: None
x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
y = torch.ones((2, 2))
print("x =\n", x)
print("y =\n", y)
print("shape:", x.shape)
print("dtype:", x.dtype)
print("x + y =\n", x + y)
print("x @ y =\n", x @ y)x =
tensor([[1., 2.],
[3., 4.]])
y =
tensor([[1., 1.],
[1., 1.]])
shape: torch.Size([2, 2])
dtype: torch.float32
x + y =
tensor([[2., 3.],
[4., 5.]])
x @ y =
tensor([[3., 3.],
[7., 7.]])
GPUが使える場合はデバイスを移動できる
device = "cuda" if torch.cuda.is_available() else "cpu"
x_on_device = x.to(device)
print("device:", x_on_device.device)device: cpu
NumPyで実装したプログラムをPyTorchで再現してみよう¶
NumPyとPyTorchの違い¶
両者は似ているが、目的が異なる。
NumPy: 汎用数値計算に強い。高速な配列計算や線形代数の土台として広く利用される。
PyTorch: 機械学習向け機能(自動微分、GPU、モデル管理)が強い。
とくに重要な違いは「微分を追跡するかどうか」である。NumPyは通常、勾配を自動で計算しない。一方PyTorchは計算履歴を保持できるため、backward() だけで勾配を求められる。
また、推論時など微分が不要な場面では、次の方法で自動微分をoffにできる。
with torch.no_grad():ブロック内の計算を勾配追跡しないtensor.detach()既存の計算グラフからTensorを切り離す
まずは同じ計算を両方で書いてみる。
a_np = np.array([1.0, 2.0, 3.0])
b_np = np.array([4.0, 5.0, 6.0])
print("NumPy dot:", a_np @ b_np)
a_t = torch.tensor([1.0, 2.0, 3.0])
b_t = torch.tensor([4.0, 5.0, 6.0])
print("PyTorch dot:", torch.dot(a_t, b_t).item())
# 自動微分の例(requires_grad=True のTensorは勾配追跡される)
w = torch.tensor(2.0, requires_grad=True)
z = w ** 2 + 3 * w
z.backward()
print("dz/dw:", w.grad.item())
# 自動微分をoffにする例1: no_gradコンテキストを使う
with torch.no_grad():
z_no_grad = w ** 2 + 3 * w
print("no_gradで計算したTensorのrequires_grad:", z_no_grad.requires_grad)
# 自動微分をoffにする例2: detachで計算グラフから切り離す
z_detached = z.detach()
print("detachしたTensorのrequires_grad:", z_detached.requires_grad)NumPy dot: 32.0
PyTorch dot: 32.0
dz/dw: 7.0
自動微分(Autograd)の動き¶
requires_grad=True のTensorを含む計算は、計算グラフとして記録される。loss.backward() を呼ぶと、連鎖律により勾配が葉ノードへ伝播する。
通常、
.gradが蓄積されるのは葉Tensorだけである同じ損失から複数回
backward()する場合はretain_graph=Trueが必要になることがある
勾配は加算される。ミニバッチ学習では、各ステップの先頭で optimizer.zero_grad() を忘れないこと。
x = torch.tensor(2.0, requires_grad=True)
y = x * x
z = y + 1.0
z.backward()
print("dz/dx =", x.grad.item()) # 2x で x=2 なら 4
NumPyとの連携¶
PyTorchとNumPyは相互変換が容易である。前処理をNumPyで行い、学習をPyTorchで行う、といった使い分けもよく行われる。
arr = np.array([[1, 2], [3, 4]], dtype=np.float32)
tensor_from_np = torch.from_numpy(arr) # NumPy配列からTensorを作成
print("NumPy -> Tensor")
print(tensor_from_np)
back_to_np = tensor_from_np.numpy() # TensorからNumPy配列に変換
print("Tensor -> NumPy")
print(back_to_np)
# メモリ共有の確認
arr[0, 0] = 99
print("NumPy変更後のTensor:") # NumPy配列を変更するとTensorも変更される
print(tensor_from_np)
tensor_from_np[0, 1] = 88
print("Tensor変更後のNumPy:") # Tensorを変更するとNumPy配列も変更される
print(back_to_np)NumPy -> Tensor
tensor([[1., 2.],
[3., 4.]])
Tensor -> NumPy
[[1. 2.]
[3. 4.]]
NumPy変更後のTensor:
tensor([[99., 2.],
[ 3., 4.]])
Tensor変更後のNumPy:
[[99. 88.]
[ 3. 4.]]
NumPyで学んだ関数をPyTorchで使う対応例¶
numpy_introduction.ipynb と numpy_ufunc.ipynb で扱った関数の多くは、PyTorchにも同名または近い形で存在する。
代表例は次の通りである。
配列作成:
np.zeros,np.ones,np.arange,np.reshape→torch.zeros,torch.ones,torch.arange,torch.reshape線形代数:
np.dot,np.outer→torch.matmul/@,torch.outerufunc:
np.log,np.exp,np.sin,np.cos,np.tan,np.floor,np.isnan→torch.log,torch.exp,torch.sin,torch.cos,torch.tan,torch.floor,torch.isnan統計量:
np.sum,np.min,np.max,np.argmax,np.argmin,np.mean,np.var,np.std→torch.sum,torch.min,torch.max,torch.argmax,torch.argmin,torch.mean,torch.var,torch.std
以下のセルで、実際の対応をまとめて確認する。
1) 配列作成と形状変更(np.zeros, np.ones, np.arange, np.reshape)¶
NumPyで配列を初期化したり reshape したりする処理は、torch.zeros / torch.ones / torch.arange / torch.reshape でほぼ同じように書ける。
# 1) 配列作成と形状変更(np.zeros / np.ones / np.arange / np.reshape に対応)
print("torch.zeros(5):", torch.zeros(5))
print("torch.ones((2, 3)):\n", torch.ones((2, 3)))
a = torch.arange(0, 10)
print("torch.arange(0, 10):", a)
print("torch.reshape(a, (2, 5)):\n", torch.reshape(a, (2, 5)))2) 線形代数(np.dot, np.outer に対応)¶
ベクトル同士の内積は @(または torch.matmul)で計算できる。外積は torch.outer を使う。
u = torch.tensor([1.0, 2.0, 3.0])
v = torch.tensor([4.0, 5.0, 6.0])
print("u @ v (dot):", (u @ v).item())
print("torch.outer(u, v):\n", torch.outer(u, v))3) ufunc対応(np.log, np.exp, np.sin, np.cos, np.tan, np.floor, np.isnan)¶
NumPyで学んだ要素ごとの関数は、PyTorchでもほぼ同名の torch.* として使える。
x = torch.tensor([1.0, torch.e, torch.e**2])
print("torch.log(x):", torch.log(x))
y = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
print("torch.exp(y):", torch.exp(y))
angles_deg = torch.tensor([0.0, 30.0, 45.0, 60.0, 90.0])
angles_rad = torch.deg2rad(angles_deg)
print("torch.sin(angles_rad):", torch.sin(angles_rad))
print("torch.cos(angles_rad):", torch.cos(angles_rad))
print("torch.tan(angles_rad):", torch.tan(angles_rad))
z = torch.tensor([-1.8, -0.2, 0.0, 1.2, 3.9])
print("torch.floor(z):", torch.floor(z))
w = torch.tensor([0.0, float("nan"), 1.5, float("nan"), -3.0])
print("torch.isnan(w):", torch.isnan(w))
print("NaNを除いた要素:", w[~torch.isnan(w)])4) 統計量とaxis指定(np.sum, np.min, np.max, np.argmax, np.argmin, np.mean, np.var, np.std)¶
NumPyの axis に相当する指定は、PyTorchでは dim である。keepdims=True は keepdim=True に対応する。
X = torch.tensor([[0.5, 1.0, 2.0], [2.0, 2.5, 0.1]])
print("torch.sum(X):", torch.sum(X).item())
print("torch.sum(X, dim=0):", torch.sum(X, dim=0))
print("torch.sum(X, dim=1, keepdim=True):\n", torch.sum(X, dim=1, keepdim=True))
print("torch.min(X):", torch.min(X).item())
print("torch.max(X):", torch.max(X).item())
print("torch.argmax(X):", torch.argmax(X).item())
print("torch.argmin(X):", torch.argmin(X).item())
print("torch.mean(X):", torch.mean(X).item())
print("torch.var(X):", torch.var(X, unbiased=False).item())
print("torch.std(X):", torch.std(X, unbiased=False).item())ブロードキャスト(NumPyと同様)¶
PyTorchもNumPyと同様に、形状の異なるTensor同士の演算でブロードキャストが働く。ミニバッチ学習では、バッチ次元と特徴次元の組み合わせでよく使われる。
注意点として、意図しない大きな中間Tensorが作られないよう、演算前に shape を確認する習慣をつけるとよい。
a = torch.arange(6).reshape(2, 3)
b = torch.tensor([10.0, 20.0, 30.0])
# bは最後の次元が一致するので行方向に足し込まれる
print("a shape:", a.shape, "b shape:", b.shape)
print("a + b:\n", a + b)torch.cat と torch.stack¶
複数Tensorを結合するときは torch.cat(既存次元に沿って連結)や torch.stack(新しい次元を追加して積む)を使う。バッチ方向にデータを足し合わせる処理で頻出する。
p = torch.tensor([[1.0, 2.0]])
q = torch.tensor([[3.0, 4.0]])
print("cat dim=0:\n", torch.cat([p, q], dim=0))
print("stack shape:", torch.stack([p.squeeze(0), q.squeeze(0)], dim=0).shape)
dtype と型変換¶
モデルとデータの dtype が一致していないと、演算エラーや暗黙の型変換で性能が落ちることがある。通常は float32 に揃える。
tensor.float()または.to(torch.float32)でfloat32へmodel.to(device)と同様にdtypeも揃える
x_int = torch.tensor([1, 2, 3])
x_f = x_int.float()
print("dtype:", x_int.dtype, "->", x_f.dtype)インプレース演算と勾配¶
*=, += のようなインプレース演算は、自動微分の計算グラフを壊す場合がある。学習対象のTensorには避け、w = w - lr * w.grad のように代入で更新するか、optimizer.step() に任せる。
推論や指標計算では torch.no_grad() や detach() を使い、不要な勾配記録を止める。
線形回帰をPyTorchで実装する¶
ここからは、疑似データに対して y = wx + b を学習する。まずは完全手作業の学習ループを実装する。
この段階では「どこで勾配を計算し、どこで重みを更新し、どこで勾配を初期化するか」を明示的に書く。後半で同じ処理をPyTorchの標準機能に任せ、記述を短くしていく。
PyTorchによる勾配降下法を用いた学習の仕組み¶
勾配降下法では、損失関数 を小さくする向きにパラメータを更新する。線形回帰では、パラメータは重み とバイアス である。
順伝播: 入力 から予測値 を計算する
損失計算: と正解 の差から損失(ここではMSE)を計算する
逆伝播:
loss.backward()で , を自動計算する更新:
w = w - lr * gradの形でパラメータを更新する勾配初期化: 次の反復に備えて勾配を0に戻す
この5ステップを1エポックごとに繰り返すことで、予測精度が徐々に向上する。まずは次セルで、この流れを手作業で実装する。
# 乱数シードを固定して、毎回同じ結果を再現できるようにする
torch.manual_seed(0)
# 入力データxを-1から1まで100点作る(形状は[100, 1])
x_train = torch.linspace(-1, 1, 100).unsqueeze(1)
# 正規分布ノイズを作る(x_trainと同じ形状)
noise = 0.2 * torch.randn_like(x_train)
# 正解データyを y = 2.5x + 0.8 + ノイズ で作る
y_train = 2.5 * x_train + 0.8 + noise
# 重みwをランダム初期化し、勾配計算対象にする
w = torch.randn(1, requires_grad=True)
# バイアスbを0で初期化し、勾配計算対象にする
b = torch.zeros(1, requires_grad=True)
# 学習率(1回の更新幅)を設定する
lr = 0.1
# 全データを使った反復回数(エポック数)を設定する
epochs = 100
# エポック数だけ学習を繰り返す
for epoch in range(epochs):
# 現在のw, bで予測値を計算する(順伝播)
pred = w * x_train + b
# 予測値と正解値の平均二乗誤差を計算する
loss = ((pred - y_train) ** 2).mean()
# lossをw, bで微分し、勾配を計算する(逆伝播)
loss.backward()
# 勾配追跡を無効にしてパラメータを更新する
with torch.no_grad():
# 重みwを勾配降下法で更新する
w -= lr * w.grad
# バイアスbを勾配降下法で更新する
b -= lr * b.grad
# 次のエポックのためにwの勾配を0に戻す
w.grad.zero_()
# 次のエポックのためにbの勾配を0に戻す
b.grad.zero_()
# 20エポックごとに損失を表示して学習の進み具合を確認する
if epoch % 20 == 0:
print(f"epoch={epoch:3d}, loss={loss.item():.4f}")
# 学習後の重みとバイアスを表示する
print(f"learned w={w.item():.3f}, b={b.item():.3f}")epoch= 0, loss=1.1646
epoch= 20, loss=0.0702
epoch= 40, loss=0.0437
epoch= 60, loss=0.0421
epoch= 80, loss=0.0420
learned w=2.499, b=0.807
nn.Module でモデルをクラス化する¶
nn.Linear だけでも足りるが、複数層や独自の順伝播を書くときは nn.Module を継承したクラスにまとめる。forward に計算の流れを書き、パラメータは nn.Linear などの子モジュールに保持させる。
class ScalarLinear(nn.Module):
"""y = w x + b の1次元回帰用の最小モデル"""
def __init__(self) -> None:
super().__init__()
self.lin = nn.Linear(1, 1)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.lin(x)
demo_model = ScalarLinear()
print(demo_model)学習ループをPyTorch機能で簡素化する(ステップ1)¶
次は nn.Module と torch.optim を使って、更新処理をライブラリに任せる。
# 入力1次元・出力1次元の線形モデルを作る(内部に重みとバイアスを持つ)
model = nn.Linear(in_features=1, out_features=1)
# 回帰でよく使う平均二乗誤差を損失関数として定義する
criterion = nn.MSELoss()
# 確率的勾配降下法(SGD)でモデルのパラメータを更新する設定を行う
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
# 学習の反復回数を設定する
epochs = 100
# エポック数だけ学習を繰り返す
for epoch in range(epochs):
# モデルに入力を渡して予測値を計算する(順伝播)
pred = model(x_train)
# 予測値と正解値から損失を計算する
loss = criterion(pred, y_train)
# 1つ前のエポックで計算した勾配をリセットする
optimizer.zero_grad()
# 現在の損失を用いて勾配を計算する
loss.backward()
# 計算した勾配を使ってパラメータを1ステップ更新する
optimizer.step()
# 20エポックごとに損失を表示して収束状況を確認する
if epoch % 20 == 0:
print(f"epoch={epoch:3d}, loss={loss.item():.4f}")
# 学習後の重みを取り出す
w2 = model.weight.item()
# 学習後のバイアスを取り出す
b2 = model.bias.item()
# 学習結果を表示する
print(f"learned w={w2:.3f}, b={b2:.3f}")epoch= 0, loss=2.1109
epoch= 20, loss=0.1323
epoch= 40, loss=0.0474
epoch= 60, loss=0.0423
epoch= 80, loss=0.0420
learned w=2.498, b=0.807
活性化関数はどこに実装されているか¶
PyTorchでは、活性化関数を torch.nn と torch.nn.functional の2通りで扱える。
モデルの部品として宣言したい場合:
nn.ReLU()のようにtorch.nnを使う計算時に関数として直接使いたい場合:
F.relu(x)のようにtorch.nn.functionalを使う
import torch.nn.functional as F
x_demo = torch.tensor([-1.0, 0.0, 1.0])
# torch.nn を使う例(モジュールとして利用)
relu_module = nn.ReLU()
print("nn.ReLU() :", relu_module(x_demo))
# torch.nn.functional を使う例(関数として利用)
print("F.relu(x_demo) :", F.relu(x_demo))
# ほかの活性化関数の例
print("nn.Sigmoid() :", nn.Sigmoid()(x_demo))
print("torch.sigmoid():", torch.sigmoid(x_demo))学習ループをさらに簡素化する(ステップ2)¶
データセットとデータローダを使って、ミニバッチ学習へ拡張する。実務ではこの形が基本になる。
# DataLoaderとTensorDatasetを使うためのユーティリティを読み込む
from torch.utils.data import DataLoader, TensorDataset
# 入力と正解を1つのデータセットとしてまとめる
dataset = TensorDataset(x_train, y_train)
# ミニバッチサイズ16、各エポックでシャッフルするローダーを作る
loader = DataLoader(dataset, batch_size=16, shuffle=True)
# 線形回帰モデルを作る
model = nn.Linear(1, 1)
# 損失関数に平均二乗誤差を使う
criterion = nn.MSELoss()
# 最適化アルゴリズムにSGDを使う
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
# 60エポック学習する
for epoch in range(60):
# ミニバッチごとに学習を進める
for xb, yb in loader:
# ミニバッチ入力から予測を計算する
pred = model(xb)
# ミニバッチ単位で損失を計算する
loss = criterion(pred, yb)
# 前回計算した勾配を消去する
optimizer.zero_grad()
# 現在のミニバッチ損失から勾配を計算する
loss.backward()
# 勾配を使ってパラメータを更新する
optimizer.step()
# 15エポックごとに直近ミニバッチの損失を表示する
if epoch % 15 == 0:
print(f"epoch={epoch:3d}, last_batch_loss={loss.item():.4f}")
# 学習後の重みとバイアスを表示する
print(f"learned w={model.weight.item():.3f}, b={model.bias.item():.3f}")epoch= 0, last_batch_loss=0.6001
epoch= 15, last_batch_loss=0.0306
epoch= 30, last_batch_loss=0.0318
epoch= 45, last_batch_loss=0.0921
learned w=2.507, b=0.795
非線形を入れた小さな回帰モデル(MLP)¶
線形層の間に活性化関数を挟むと、非線形な入出力関係を近似できる。ここでは1入力・1出力の回帰の例として、Linear -> ReLU -> Linear の2層を示す。
分類タスクでは出力に Sigmoid や Softmax を使うが、回帰では最後の層は線形のままにすることが多い。
class TinyMLPRegressor(nn.Module):
def __init__(self) -> None:
super().__init__()
self.net = nn.Sequential(
nn.Linear(1, 8),
nn.ReLU(),
nn.Linear(8, 1),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
mlp = TinyMLPRegressor()
opt = torch.optim.Adam(mlp.parameters(), lr=0.05)
crit = nn.MSELoss()
for _ in range(200):
pred = mlp(x_train)
loss = crit(pred, y_train)
opt.zero_grad()
loss.backward()
opt.step()
print("MLP final loss:", loss.item())train と eval、推論時の no_grad¶
Dropout や BatchNorm などの層は、訓練時と評価時で挙動が変わる。model.train() と model.eval() を切り替える。
推論や検証では、計算速度とメモリのために torch.no_grad() で勾配を記録しない。
mlp.train()
assert mlp.training is True
mlp.eval()
assert mlp.training is False
with torch.no_grad():
y_hat = mlp(x_train[:5])
print("推論(先頭5件):", y_hat.squeeze().tolist())損失関数の reduction と最適化アルゴリズム¶
nn.MSELoss などには reduction 引数があり、mean(既定)・sum・none を選べる。ミニバッチごとに損失を足し合わせたい場合などに使い分ける。
最適化は SGD に加え、Adam や AdamW が広く使われる。学習率のスケジューリングは torch.optim.lr_scheduler で行える。
乱数の再現性(CPU)¶
実験の再現性のため、torch.manual_seed に加えてNumPyの乱数も固定することがある。GPUを使う場合はCUDA用のseed設定も必要になる。
モデルとデータを device に揃える¶
GPUを使うときは、入力Tensorとモデルパラメータの両方を model.to(device) および x = x.to(device) で同じ device に送る。片方だけCPUに残っていると、演算時に例外になる。
device = "cuda" if torch.cuda.is_available() else "cpu"
model_cpu = nn.Linear(1, 1)
model_dev = model_cpu.to(device)
print("model device:", next(model_dev.parameters()).device)
学習済みパラメータの保存(state_dict)¶
学習結果は model.state_dict() で辞書として取得し、torch.save でファイルに書き出す。読み込みは load_state_dict を使う。
m = nn.Linear(1, 1)
buf = m.state_dict()
print("state_dict keys:", list(buf.keys()))
勾配累積(大きなバッチを擬似的に扱う)¶
GPUメモリが足りないとき、小さなミニバッチを複数回 backward() し、一定回数ごとに optimizer.step() する手法がある。このときは各ミニバッチ後に loss = loss / accum_steps のように損失をスケールし、最後に一度だけ更新するなど、実装ルールを統一する必要がある。
練習問題¶
各問には一意な正解がある(整数・小数第1位までの実数・True/False のいずれか)。問題文末尾の「答え:」行にのみ記入すること。
コードセルは検算用である。講義中に答えを表示するコードを実行しないよう注意すること。
【問題1】Tensorの合計¶
次のコードをそのまま実行したあと、t.sum().item() の値(整数)を答えよ。
torch.manual_seed(0)
t = torch.arange(12).reshape(3, 4)答え(整数のみ):
torch.manual_seed(0)
t = torch.arange(12).reshape(3, 4)
# この下にコードを書き、t.sum().item() を求めよ(答えは問題文の欄にのみ記入)
torch.manual_seed(0)
t = torch.arange(12).reshape(3, 4)
# この下にコードを書き、t[2, 0].item() を求めよ
【問題3】勾配の値¶
次を実行したあと、x.grad.item() の値(浮動小数)を答えよ。小数第1位まででよい(四捨五入)。
x = torch.tensor(3.0, requires_grad=True)
y = x ** 3 - 2 * x
y.backward()答え(例: 25.0 のように小数第1位まで):
x = torch.tensor(3.0, requires_grad=True)
y = x ** 3 - 2 * x
y.backward()
# この下にコードを書き、x.grad を小数第1位に四捨五入した値を求めよ
【問題4】ReLUの一致¶
x = torch.tensor([-2.0, -0.5, 0.0, 0.5, 2.0]) とする。torch.allclose(nn.ReLU()(x), torch.nn.functional.relu(x)) の結果は True と False のどちらか。該当する方を答えよ。
答え(True または False のどちらか一方):
import torch.nn.functional as F
x = torch.tensor([-2.0, -0.5, 0.0, 0.5, 2.0])
# この下にコードを書き、torch.allclose(nn.ReLU()(x), F.relu(x)) の結果を求めよ
【問題5】線形層の出力¶
次を実行したあと、m(torch.tensor([[3.0]])) の出力値を答えよ。小数第1位まででよい。
m = nn.Linear(1, 1, bias=False)
nn.init.constant_(m.weight, 2.0)答え(小数第1位まで):
m = nn.Linear(1, 1, bias=False)
nn.init.constant_(m.weight, 2.0) # mの重みはランダムに初期化されているのだが,このコードで重みを2.0に固定している
# この下にコードを書き、m(torch.tensor([[3.0]])) を小数第1位に四捨五入した値を求めよ
【問題6】ブロードキャスト後の形状¶
a = torch.zeros(4, 1) と b = torch.ones(1, 5) について、(a + b).shape は torch.Size 型で返る。要素数は何か(整数)を答えよ。
答え(要素数の整数):
a = torch.zeros(4, 1)
b = torch.ones(1, 5)
# この下にコードを書き、(a + b) の要素数 numel を求めよ
【問題7】no_grad 内の requires_grad¶
x = torch.tensor(1.0, requires_grad=True) とする。with torch.no_grad(): z = x * 2 のあと、z.requires_grad は True と False のどちらか。答えよ。
答え(True または False):
x = torch.tensor(1.0, requires_grad=True)
# この下に with torch.no_grad(): を使い z = x * 2 を計算し、z.requires_grad の値を求めよ
発展問題¶
ここからは制約付きの最適化や小規模分類を、自動微分で実装する。各問も数値閾値または真偽で採点できる形にしてある。
【発展問題1】非負値行列因子分解(NMF)を勾配法で当てはめる¶
非負行列 (要素すべて )に対し、、 を 非負 とし、 を最小化する問題を考える。これが非負値行列因子分解(NMF)の基本的な定式化である。
PyTorchの自動微分で勾配降下する場合、更新後に W と H を clamp(min=0) して非負制約を満たす(射影勾配法)。
問題: 下の解答セルで、与えられた非負行列の積として合成した観測行列 に対し、 で 2000ステップ 学習し、再構成誤差 torch.mean((V - W @ H) ** 2).item() が 0.05 未満 になるようにせよ。乱数シードは固定済みである。
採点: 上記の平均二乗誤差が 0.05 未満 なら正解(真偽一つ)。
torch.manual_seed(7)
m, n, r = 8, 6, 2
W_true = torch.rand(m, r)
H_true = torch.rand(r, n)
V = W_true @ H_true
W = torch.rand(m, r, requires_grad=True)
H = torch.rand(r, n, requires_grad=True)
# TODO: オプティマイザを定義し、損失 mean((V - W @ H) ** 2) を 2000 ステップ最小化せよ。
# 各ステップの末尾で W.clamp_(0), H.clamp_(0) により非負制約を満たすこと。
mse = torch.mean((V - W @ H) ** 2).item()
print("再構成MSE:", mse)
print("正解条件( MSE < 0.05 ):", mse < 0.05)
【発展問題2】2次元ロジスティック回帰の分類精度¶
ロジスティック回帰について自分で調べてから,以下の問題に挑戦しよう!
2クラス線形分類のデータを次で生成する(シード固定)。
torch.manual_seed(123)
n = 200
X = torch.cat([torch.randn(n // 2, 2) + torch.tensor([2.0, 2.0]), torch.randn(n // 2, 2) - torch.tensor([2.0, 2.0])], dim=0)
y = torch.cat([torch.zeros(n // 2, 1), torch.ones(n // 2, 1)], dim=0)nn.Linear(2, 1) と BCEWithLogitsLoss を用い、torch.optim.SGD で 500エポック 学習したあと、学習データ上の分類精度(((logits > 0).float() == y).float().mean().item())が 0.92 以上 なら正解とする。
採点基準: 上記の精度が 0.92 以上なら正解(一つの条件)。
torch.manual_seed(123)
n = 200
X = torch.cat(
[
torch.randn(n // 2, 2) + torch.tensor([2.0, 2.0]),
torch.randn(n // 2, 2) - torch.tensor([2.0, 2.0]),
],
dim=0,
)
y = torch.cat([torch.zeros(n // 2, 1), torch.ones(n // 2, 1)], dim=0)
model = nn.Linear(2, 1)
# TODO: BCEWithLogitsLoss と SGD(lr=0.5) を用い、500エポック学習せよ。
with torch.no_grad():
logits = model(X)
acc = ((logits > 0).float() == y).float().mean().item()
print("accuracy:", acc)
print("正解条件( acc >= 0.92 ):", acc >= 0.92)
まとめ¶
PyTorchは、Tensor・自動微分・GPU活用が強みである。
NumPyと似た操作感を持ちながら、
nn・DataLoader・最適化器まで一気通貫で扱える。線形回帰から小さなMLP、さらに制約付き最適化や分類へと、コードの雛形を拡張できる。