深層学習の研究と実装には、効率的なフレームワークが不可欠です。NumPyでのスクラッチ実装は学習に最適ですが、GPU対応、自動微分、データ読み込みの効率化など、実用的な問題に対処するには専用のフレームワークが必要です。
PyTorchは2016年にFacebook AI Research(現Meta AI)が公開した深層学習フレームワークで、研究コミュニティで最も広く使われています。その人気の理由は「Pythonらしい直感的なAPI」と「動的計算グラフ」にあります。NumPyに近い感覚でテンソル演算ができ、通常のPythonコードを書くように計算グラフが構築されるため、デバッグが容易です。
PyTorchを理解すると、以下のことが可能になります。
- GPU上での高速な学習: CPU比で10〜100倍の速度向上
- 自動微分: 逆伝播の手動実装が不要。任意の計算グラフの勾配を自動計算
- 豊富な事前学習モデル: torchvisionやHugging Faceの事前学習モデルを数行で利用
- 研究の最前線: 最新の論文の多くがPyTorchで実装を公開
本記事の内容
- Tensor の基本操作とNumPyとの対応
- Autograd(自動微分)の仕組み
- nn.Moduleによるモデル設計
- DatasetとDataLoaderによるデータ管理
- 学習ループの設計パターン
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
- ニューラルネットワークの基礎 — MLPの構造と順伝播
- 誤差逆伝播法の理論と実装 — 自動微分の理論的基盤
Tensorの基礎
Tensorとは
PyTorchのTensorは、NumPyのndarrayに相当する多次元配列です。違いは主に2つあります。
- GPU上での計算が可能:
.to('cuda')でGPUメモリに転送し、並列計算が可能 - 自動微分に対応:
requires_grad=Trueで計算グラフを記録し、勾配を自動計算
NumPyとの対応
PyTorchのTensor APIはNumPyと非常に似ています。以下にNumPyとの対応を示します。
import numpy as np
# NumPyでのテンソル操作(PyTorchの対応を注記)
# PyTorch: import torch
# --- 生成 ---
# np.array → torch.tensor
a = np.array([1.0, 2.0, 3.0])
print(f"1次元テンソル: {a}, shape: {a.shape}, dtype: {a.dtype}")
# np.zeros → torch.zeros
b = np.zeros((3, 4))
print(f"ゼロテンソル: shape={b.shape}")
# np.ones → torch.ones
c = np.ones((2, 3))
print(f"1テンソル: shape={c.shape}")
# np.random.randn → torch.randn
d = np.random.randn(3, 4)
print(f"正規乱数テンソル: shape={d.shape}")
# np.arange → torch.arange
e = np.arange(0, 10, 2)
print(f"等間隔テンソル: {e}")
# np.linspace → torch.linspace
f = np.linspace(0, 1, 5)
print(f"等分割テンソル: {f}")
# --- 演算 ---
x = np.random.randn(3, 4)
y = np.random.randn(3, 4)
# 要素ごとの演算(+, -, *, / はそのまま対応)
z = x + y
print(f"加算: shape={z.shape}")
# 行列積: np.matmul / @ → torch.matmul / @
A = np.random.randn(3, 4)
B = np.random.randn(4, 5)
C = A @ B # torch.matmul(A, B) / A @ B
print(f"行列積: {A.shape} @ {B.shape} = {C.shape}")
# リシェイプ: np.reshape → torch.reshape / .view
x_flat = x.reshape(-1) # torch: x.view(-1)
print(f"flatten: {x.shape} → {x_flat.shape}")
# 転置: np.transpose / .T → torch.transpose / .T
x_t = x.T # torch: x.T / x.transpose(0, 1)
print(f"転置: {x.shape} → {x_t.shape}")
# --- NumPyとの相互変換 ---
# torch.from_numpy(np_array) → Tensor
# tensor.numpy() → NumPy array
# ※ メモリを共有するため、一方を変更すると他方も変わる
print("\n--- NumPyテンソルのデバイス ---")
print(f"NumPyはCPUのみ。PyTorchは .to('cuda') でGPUに転送可能")
このコードはNumPyを使ってPyTorchのTensorと対応する操作を示しています。NumPyに慣れていれば、PyTorchの関数名はほぼ同じなので、移行はスムーズです。主な違いは、PyTorchでは .view() がNumPyの .reshape() に対応すること、テンソルにデバイス(CPU/GPU)の概念があること、そして requires_grad で自動微分を有効にできることです。
データ型とデバイス
PyTorchのTensorにはデータ型(dtype)とデバイス(device)の2つの属性があります。
| 属性 | 説明 | 例 |
|---|---|---|
| dtype | データの型 | torch.float32(デフォルト), torch.int64 |
| device | 計算デバイス | torch.device('cpu'), torch.device('cuda:0') |
ニューラルネットワークの重みは通常 float32 を使いますが、最近は float16(半精度)や bfloat16 での混合精度学習(Mixed Precision Training)も一般的です。
TensorはPyTorchの基盤であり、次に説明する自動微分の仕組みもTensor上に構築されています。
Autograd(自動微分)
自動微分の仕組み
PyTorchのAutogradは、Tensorに対する全ての操作を記録し、連鎖律を自動適用して勾配を計算します。これは前の記事で学んだ誤差逆伝播法の一般化です。
import numpy as np
# PyTorchの自動微分をNumPyで概念的に再現
class AutogradTensor:
"""自動微分をシミュレートするテンソル"""
def __init__(self, data, requires_grad=False, _children=(), _op=''):
self.data = np.array(data, dtype=np.float64)
self.grad = np.zeros_like(self.data)
self.requires_grad = requires_grad
self._backward = lambda: None
self._prev = set(_children)
self._op = _op
def __add__(self, other):
other = other if isinstance(other, AutogradTensor) else AutogradTensor(other)
out = AutogradTensor(self.data + other.data, _children=(self, other), _op='+')
def _backward():
if self.requires_grad:
self.grad += out.grad
if other.requires_grad:
other.grad += out.grad
out._backward = _backward
out.requires_grad = True
return out
def __mul__(self, other):
other = other if isinstance(other, AutogradTensor) else AutogradTensor(other)
out = AutogradTensor(self.data * other.data, _children=(self, other), _op='*')
def _backward():
if self.requires_grad:
self.grad += other.data * out.grad
if other.requires_grad:
other.grad += self.data * out.grad
out._backward = _backward
out.requires_grad = True
return out
def __pow__(self, power):
out = AutogradTensor(self.data ** power, _children=(self,), _op=f'**{power}')
def _backward():
if self.requires_grad:
self.grad += power * self.data ** (power - 1) * out.grad
out._backward = _backward
out.requires_grad = True
return out
def backward(self):
"""トポロジカルソートで逆順に勾配を計算"""
topo = []
visited = set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._prev:
build_topo(child)
topo.append(v)
build_topo(self)
self.grad = np.ones_like(self.data)
for v in reversed(topo):
v._backward()
# --- デモ: f(x, y) = x^2 * y + y + 2 の勾配 ---
x = AutogradTensor(3.0, requires_grad=True)
y = AutogradTensor(4.0, requires_grad=True)
# 順伝播
z = x ** 2 * y + y + AutogradTensor(2.0)
# 逆伝播
z.backward()
print("=== 自動微分のデモ ===")
print(f"f(x, y) = x^2 * y + y + 2")
print(f"x = {x.data}, y = {y.data}")
print(f"f(3, 4) = 9*4 + 4 + 2 = {z.data}")
print(f"df/dx = 2*x*y = 2*3*4 = {x.grad} (解析解: 24)")
print(f"df/dy = x^2 + 1 = 9+1 = {y.grad} (解析解: 10)")
このコードは、PyTorchの Autograd がどのように動作するかを概念的に再現しています。各演算が「計算グラフのノード」を生成し、backward() を呼ぶとトポロジカルソート(逆順)で各ノードの _backward が実行されます。PyTorchでは torch.Tensor に requires_grad=True を設定するだけで同じ仕組みが自動的に動きます。計算結果の勾配が解析解($\partial f/\partial x = 24$, $\partial f/\partial y = 10$)と一致していることが確認できます。
勾配の計算フロー
PyTorchでの勾配計算のフローは以下の通りです。
requires_grad=Trueのテンソルに対する操作が計算グラフに記録される- スカラーの損失値に対して
.backward()を呼ぶ - 計算グラフを逆順にたどり、連鎖律で各パラメータの勾配を計算
- 勾配は各テンソルの
.grad属性に蓄積される
注意点として、.grad は .backward() を呼ぶたびに加算されます。学習ループでは毎回 optimizer.zero_grad() で勾配をリセットする必要があります。
自動微分の仕組みを理解したところで、次にPyTorchでニューラルネットワークを構築する標準的な方法を見ていきましょう。
nn.Moduleによるモデル設計
nn.Moduleの基本
PyTorchでは torch.nn.Module を継承してモデルを定義します。NumPyでの概念的な実装を示します。
import numpy as np
# PyTorchの nn.Module に対応する概念的な実装
class Module:
"""PyTorchの nn.Module を模擬"""
def __init__(self):
self._parameters = {}
self._modules = {}
def __call__(self, *args, **kwargs):
return self.forward(*args, **kwargs)
def forward(self, x):
raise NotImplementedError
def parameters(self):
"""全パラメータを返す"""
params = list(self._parameters.values())
for module in self._modules.values():
params.extend(module.parameters())
return params
class Linear(Module):
"""全結合層: torch.nn.Linear に対応"""
def __init__(self, in_features, out_features):
super().__init__()
# He初期化
self.weight = np.random.randn(out_features, in_features) * np.sqrt(2.0 / in_features)
self.bias = np.zeros(out_features)
self._parameters = {'weight': self.weight, 'bias': self.bias}
def forward(self, x):
# x: (batch, in_features) → (batch, out_features)
return x @ self.weight.T + self.bias
class ReLU(Module):
"""ReLU活性化関数: torch.nn.ReLU に対応"""
def forward(self, x):
return np.maximum(0, x)
class Sequential(Module):
"""層の連結: torch.nn.Sequential に対応"""
def __init__(self, *layers):
super().__init__()
self.layers = layers
for i, layer in enumerate(layers):
self._modules[str(i)] = layer
def forward(self, x):
for layer in self.layers:
x = layer(x)
return x
# --- モデル定義の例 ---
# PyTorchでは:
# model = nn.Sequential(
# nn.Linear(2, 64),
# nn.ReLU(),
# nn.Linear(64, 32),
# nn.ReLU(),
# nn.Linear(32, 1),
# )
model = Sequential(
Linear(2, 64),
ReLU(),
Linear(64, 32),
ReLU(),
Linear(32, 1),
)
# テスト
x_test = np.random.randn(4, 2) # バッチサイズ4, 2次元入力
output = model(x_test)
print(f"入力: {x_test.shape}")
print(f"出力: {output.shape}")
print(f"パラメータ数: {sum(p.size for p in model.parameters())}")
このコードでは、PyTorchの nn.Module、nn.Linear、nn.ReLU、nn.Sequential をNumPyで概念的に再現しています。Sequential にモジュールを渡すだけでネットワークが構築され、model(x) で推論が実行されます。PyTorchでは、これに加えて自動微分、GPU転送、パラメータ管理が透過的に行われます。
カスタムモデルの定義
nn.Sequential では対応できない複雑なアーキテクチャ(残差接続など)の場合、forward メソッドを直接定義します。
import numpy as np
# カスタムモデルの例(ResNet風の残差ブロック)
# PyTorchでは:
# class ResBlock(nn.Module):
# def __init__(self, dim):
# super().__init__()
# self.fc1 = nn.Linear(dim, dim)
# self.fc2 = nn.Linear(dim, dim)
#
# def forward(self, x):
# residual = x
# x = torch.relu(self.fc1(x))
# x = self.fc2(x)
# return torch.relu(x + residual)
# NumPyでの概念的な実装
class ResBlock:
def __init__(self, dim):
self.W1 = np.random.randn(dim, dim) * np.sqrt(2.0 / dim)
self.b1 = np.zeros(dim)
self.W2 = np.random.randn(dim, dim) * np.sqrt(2.0 / dim)
self.b2 = np.zeros(dim)
def forward(self, x):
residual = x
h = np.maximum(0, x @ self.W1.T + self.b1) # ReLU(FC1)
h = h @ self.W2.T + self.b2 # FC2
return np.maximum(0, h + residual) # ReLU(h + residual)
# テスト
block = ResBlock(32)
x = np.random.randn(4, 32)
out = block.forward(x)
print(f"ResBlock: 入力 {x.shape} → 出力 {out.shape}")
print(f"残差接続により入出力のshapeが同じ")
この例はPyTorchの nn.Module で残差ブロックを実装する際のパターンを示しています。forward メソッドに計算の流れを直接書くことで、if文やfor文、条件分岐を含む任意の計算グラフを記述できます。これがPyTorchの「動的計算グラフ」の利点です。
DatasetとDataLoader
効率的なデータ管理
大規模なデータセットでは、全データをメモリに載せるのは現実的でありません。PyTorchの Dataset と DataLoader は、データの遅延読み込みとミニバッチ生成を効率的に行う仕組みです。
import numpy as np
# PyTorchの Dataset / DataLoader を概念的に再現
class Dataset:
"""torch.utils.data.Dataset に対応"""
def __len__(self):
raise NotImplementedError
def __getitem__(self, idx):
raise NotImplementedError
class MoonsDataset(Dataset):
"""月型データセット"""
def __init__(self, n_samples=1000, noise=0.2):
from sklearn.datasets import make_moons
self.X, self.y = make_moons(n_samples=n_samples,
noise=noise, random_state=42)
self.X = self.X.astype(np.float32)
self.y = self.y.astype(np.float32)
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
class DataLoader:
"""torch.utils.data.DataLoader に対応"""
def __init__(self, dataset, batch_size=32, shuffle=True):
self.dataset = dataset
self.batch_size = batch_size
self.shuffle = shuffle
def __iter__(self):
indices = np.arange(len(self.dataset))
if self.shuffle:
np.random.shuffle(indices)
for start in range(0, len(indices), self.batch_size):
batch_idx = indices[start:start + self.batch_size]
batch_x = np.array([self.dataset[i][0] for i in batch_idx])
batch_y = np.array([self.dataset[i][1] for i in batch_idx])
yield batch_x, batch_y
def __len__(self):
return (len(self.dataset) + self.batch_size - 1) // self.batch_size
# --- 使用例 ---
dataset = MoonsDataset(n_samples=500, noise=0.2)
dataloader = DataLoader(dataset, batch_size=64, shuffle=True)
print(f"データセットサイズ: {len(dataset)}")
print(f"バッチ数: {len(dataloader)}")
# 1エポック分のデータを走査
for batch_idx, (X_batch, y_batch) in enumerate(dataloader):
if batch_idx == 0:
print(f"バッチ0: X.shape={X_batch.shape}, y.shape={y_batch.shape}")
if batch_idx == len(dataloader) - 1:
print(f"最終バッチ: X.shape={X_batch.shape}, y.shape={y_batch.shape}")
このコードは、PyTorchの Dataset と DataLoader の仕組みを概念的に再現しています。Dataset はデータへのアクセスインターフェースを定義し、DataLoader はシャッフルとバッチ分割を自動化します。PyTorchの実装では、これに加えてマルチプロセスでのデータ読み込み(num_workers)やピンメモリ(pin_memory)による転送高速化が利用できます。
DataLoaderの仕組みを理解したところで、これまでの要素を組み合わせた実践的な学習ループを構築しましょう。
学習ループの設計パターン
標準的な学習ループ
PyTorchの学習ループは以下のパターンに従います。NumPyでの概念的な実装を示します。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
np.random.seed(42)
# --- 簡易的なフレームワーク ---
def sigmoid(z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
class SimpleMLP:
"""学習ループのデモ用MLP"""
def __init__(self, dims):
self.Ws = []
self.bs = []
for i in range(len(dims) - 1):
self.Ws.append(np.random.randn(dims[i+1], dims[i])
* np.sqrt(2.0 / dims[i]))
self.bs.append(np.zeros((dims[i+1], 1)))
def forward(self, X):
"""X: (features, batch)"""
self.hs = [X]
self.zs = []
h = X
for i in range(len(self.Ws)):
z = self.Ws[i] @ h + self.bs[i]
self.zs.append(z)
if i < len(self.Ws) - 1:
h = np.maximum(0, z)
else:
h = sigmoid(z)
self.hs.append(h)
return h
def backward(self, t, lr):
m = t.shape[1]
delta = self.hs[-1] - t
for i in range(len(self.Ws) - 1, -1, -1):
dW = (1/m) * delta @ self.hs[i].T
db = (1/m) * np.sum(delta, axis=1, keepdims=True)
if i > 0:
delta = (self.Ws[i].T @ delta) * (self.zs[i-1] > 0).astype(float)
self.Ws[i] -= lr * dW
self.bs[i] -= lr * db
def compute_loss(self, y, t):
eps = 1e-8
return -np.mean(t * np.log(y + eps) + (1-t) * np.log(1-y + eps))
# データ準備
X_data, y_data = make_moons(n_samples=1000, noise=0.2, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
X_data, y_data, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# --- 学習ループ(PyTorch風のパターン) ---
model = SimpleMLP([2, 64, 32, 1])
lr = 0.5
n_epochs = 100
batch_size = 64
train_losses = []
test_losses = []
train_accs = []
test_accs = []
for epoch in range(n_epochs):
# シャッフル
indices = np.random.permutation(len(X_train))
epoch_loss = 0
n_batches = 0
# ミニバッチ学習
for start in range(0, len(X_train), batch_size):
batch_idx = indices[start:start + batch_size]
X_batch = X_train[batch_idx].T
y_batch = y_train[batch_idx].reshape(1, -1)
# PyTorch風のステップ:
# 1. optimizer.zero_grad() → (勾配リセット、ここでは不要)
# 2. output = model(input) → 順伝播
# 3. loss = criterion(output, target) → 損失計算
# 4. loss.backward() → 逆伝播
# 5. optimizer.step() → パラメータ更新
y_pred = model.forward(X_batch)
loss = model.compute_loss(y_pred, y_batch)
model.backward(y_batch, lr)
epoch_loss += loss
n_batches += 1
# エポック終了時の評価
train_loss = epoch_loss / n_batches
# テスト評価
y_test_pred = model.forward(X_test.T)
test_loss = model.compute_loss(y_test_pred, y_test.reshape(1, -1))
train_acc = np.mean(
(model.forward(X_train.T) > 0.5).astype(int)
== y_train.reshape(1, -1))
test_acc = np.mean(
(y_test_pred > 0.5).astype(int) == y_test.reshape(1, -1))
train_losses.append(train_loss)
test_losses.append(test_loss)
train_accs.append(train_acc)
test_accs.append(test_acc)
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1:3d}: "
f"train_loss={train_loss:.4f}, test_loss={test_loss:.4f}, "
f"train_acc={train_acc:.1%}, test_acc={test_acc:.1%}")
# --- 学習曲線の可視化 ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))
ax = axes[0]
ax.plot(train_losses, linewidth=2, label="Train Loss", color="tab:blue")
ax.plot(test_losses, linewidth=2, label="Test Loss", color="tab:orange")
ax.set_xlabel("Epoch", fontsize=12)
ax.set_ylabel("Loss", fontsize=12)
ax.set_title("Training & Test Loss", fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax = axes[1]
ax.plot(train_accs, linewidth=2, label="Train Accuracy", color="tab:blue")
ax.plot(test_accs, linewidth=2, label="Test Accuracy", color="tab:orange")
ax.set_xlabel("Epoch", fontsize=12)
ax.set_ylabel("Accuracy", fontsize=12)
ax.set_title("Training & Test Accuracy", fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_ylim(0.5, 1.05)
plt.tight_layout()
plt.savefig("pytorch_training_loop.png", dpi=150, bbox_inches="tight")
plt.show()
この学習曲線から、標準的な学習ループの振る舞いが確認できます。
-
損失曲線(左図): 訓練損失(青)とテスト損失(オレンジ)が共に減少しており、モデルが正しく学習していることがわかります。両者の差が小さいことから、過学習は起きていません。ミニバッチ学習のため、訓練損失にはわずかな振動が見られます
-
精度曲線(右図): 訓練精度とテスト精度が共に上昇し、高い精度に収束しています。テスト精度が訓練精度にほぼ追従していることから、汎化性能も良好です
PyTorchの実践的なTips
よくあるパターン
| パターン | 説明 |
|---|---|
model.train() / model.eval() |
Dropout, BatchNormの振る舞いを切り替え |
with torch.no_grad(): |
推論時に計算グラフの構築を無効化(メモリ節約) |
torch.save() / torch.load() |
モデルの保存と読み込み |
lr_scheduler |
学習率のスケジューリング |
torch.cuda.amp |
混合精度学習でGPUメモリを節約 |
デバッグのポイント
- 次元のミスマッチ:
print(tensor.shape)を多用してshapeを確認する - NaN / Inf: 学習率が大きすぎるか、損失関数にlog(0)が含まれていないか確認
- 勾配の消失/爆発:
torch.nn.utils.clip_grad_norm_で勾配クリッピング - model.eval()の忘れ: 推論時にDropoutやBatchNormが学習モードのままだとバグる
まとめ
本記事では、PyTorchの基礎をTensorから学習ループまで体系的に解説しました。
- TensorはNumPyのndarrayに対応し、GPU計算と自動微分をサポートする
- Autogradは計算グラフを動的に構築し、
.backward()で全パラメータの勾配を自動計算する - nn.Moduleはモデルの標準的な定義方法であり、
forwardメソッドに計算の流れを記述する - Dataset/DataLoaderはデータの遅延読み込みとミニバッチ生成を効率化する
- 学習ループは
zero_grad → forward → loss → backward → stepの5ステップパターン
次のステップとして、以下の記事も参考にしてください。
- 過学習の対策まとめ — Dropout、正則化のPyTorch実装
- CNNの基礎 — PyTorchでのCNN実装
- 転移学習の理論と実践 — torchvisionの事前学習モデルの活用