1. はじめに
NVIDIA DLI — Generative AI 試験対策シリーズの第1課では、PyTorch の基礎をしっかりと身につけます。これは、Diffusion Models から Large Language Models(LLMs)まで、DLIコース全体で使用される主要フレームワークです。
NVIDIA DLI の評価試験では、選択問題ではなく、直接コードを書く必要があります。そのため、PyTorch の基本パターンを習得することが前提条件となります。
試験のヒント: NVIDIA DLI の評価試験では、PyTorch コードを直接書いてデバッグする必要があります。ドキュメントを参照せずに トレーニングループ、nn.Module、テンソル操作を書けるようにしておきましょう。

2. PyTorch テンソルと Autograd
2.1 テンソルの基礎
テンソルは PyTorch の中核データ構造で、NumPy 配列に似ていますが、GPU 上で動作し、自動微分をサポートします。
import torch
# リストからテンソルを作成
x = torch.tensor([1.0, 2.0, 3.0])
# 特定の形状でテンソルを作成
zeros = torch.zeros(3, 4) # shape: (3, 4)
ones = torch.ones(2, 3, 4) # shape: (2, 3, 4)
rand = torch.randn(64, 3, 32, 32) # 64枚のRGB 32x32画像のバッチ
# 形状とdtypeを確認
print(rand.shape) # torch.Size([64, 3, 32, 32])
print(rand.dtype) # torch.float32
print(rand.device) # cpu
2.2 テンソル操作とブロードキャスティング
PyTorch は NumPy と同様のブロードキャスティングをサポートしており、異なる形状のテンソル間での演算が可能です。
# リシェイプ操作
x = torch.randn(2, 3, 4)
y = x.view(2, 12) # (2, 12)にリシェイプ
z = x.permute(0, 2, 1) # 次元を入れ替え: (2, 4, 3)
w = x.unsqueeze(0) # 次元を追加: (1, 2, 3, 4)
# 行列乗算
a = torch.randn(3, 4)
b = torch.randn(4, 5)
c = a @ b # shape: (3, 5)
# または: c = torch.matmul(a, b)
# ブロードキャスティング
x = torch.randn(64, 256) # (batch, features)
bias = torch.randn(256) # (features,)
result = x + bias # biasがブロードキャスト: (64, 256)
| 操作 | 構文 | 備考 |
|---|---|---|
| リシェイプ | x.view() / x.reshape() | view は連続メモリが必要 |
| 転置 | x.permute() / x.T | permute は複数次元に対してより柔軟 |
| 次元追加 | x.unsqueeze(dim) | ブロードキャスティングの準備によく使用 |
| 次元削除 | x.squeeze(dim) | サイズ1の次元を削除 |
| 行列乗算 | a @ b | torch.matmul と同等 |
| 結合 | torch.cat([a, b], dim=0) | 指定した次元に沿って結合 |
2.3 Autograd — 自動微分
Autograd は PyTorch の自動勾配計算システムです。requires_grad=True を設定すると、PyTorch はそのテンソルに対するすべての操作を追跡し、計算グラフを構築します。
# 基本的なautograd
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x ** 2 + 3 * x # y = x² + 3x
loss = y.sum() # スカラー出力
loss.backward() # 勾配を計算
print(x.grad) # dy/dx = 2x + 3 → tensor([7., 9.])
計算グラフ:
x (requires_grad=True)
│
├──→ x ** 2 ──→ + ──→ y ──→ sum() ──→ loss
│ ↑ │
└──→ 3 * x ─────┘ backward()
│
x.grad = 2x + 3
試験のヒント: DLI の評価試験では、「trying to backward through the graph a second time」というエラーに遭遇する場合があります。解決策:
loss.backward(retain_graph=True)を使用するか、フォワードパスを再計算してください。これは評価試験で非常によくあるエラーです。
3. nn.Module とネットワーク構築
3.1 nn.Module パターン
PyTorch のすべてのニューラルネットワークは nn.Module を継承します。これは必ず覚えるべきパターンです:
import torch.nn as nn
class SimpleNet(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__() # 必ずsuper().__init__()を呼ぶこと
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
# 使用方法
model = SimpleNet(784, 256, 10)
x = torch.randn(32, 784) # 32個のバッチ
output = model(x) # shape: (32, 10)
3.2 よく使われるレイヤー
| レイヤー | 用途 | 主なパラメータ |
|---|---|---|
nn.Linear(in, out) | 全結合層 | in_features, out_features |
nn.Conv2d(in_ch, out_ch, k) | 2D 畳み込み | in_channels, out_channels, kernel_size |
nn.BatchNorm2d(ch) | バッチ正規化 | num_features |
nn.GroupNorm(g, ch) | グループ正規化 | num_groups, num_channels |
nn.ReLU() | 活性化関数 | inplace(オプション) |
nn.Dropout(p) | 正則化 | p = ドロップ確率 |
nn.Embedding(V, D) | トークン埋め込み | num_embeddings, embedding_dim |
3.3 nn.Sequential — 簡易モデル構築
シンプルなモデルの場合、クラスを作成する代わりに nn.Sequential を使用できます:
# nn.Sequentialによる簡易アプローチ
model = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 10)
)
# モデルの確認
print(model)
# パラメータ数のカウント
total_params = sum(p.numel() for p in model.parameters())
print(f"Total params: {total_params:,}")
3.4 コード:MNIST向けMLP
import torch
import torch.nn as nn
from torchvision import datasets, transforms
# データ
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_data = datasets.MNIST('./data', train=True, download=True,
transform=transform)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64,
shuffle=True)
# モデル
class MNISTClassifier(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.layers = nn.Sequential(
nn.Linear(28 * 28, 512),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(512, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 10)
)
def forward(self, x):
x = self.flatten(x) # (B, 1, 28, 28) → (B, 784)
return self.layers(x) # (B, 784) → (B, 10)
4. トレーニングループパターン
4.1 損失関数
| 損失関数 | 用途 | 入力形状 |
|---|---|---|
nn.CrossEntropyLoss() | 多クラス分類 | logits (B, C), labels (B,) |
nn.MSELoss() | 回帰、Diffusionノイズ予測 | (B, *) vs (B, *) |
nn.BCEWithLogitsLoss() | 二値 / マルチラベル分類 | logits (B, C), labels (B, C) |
nn.L1Loss() | 回帰(外れ値に強い) | (B, *) vs (B, *) |
試験のヒント:
nn.CrossEntropyLossは内部に softmax を含んでいるため、出力層にsoftmaxを追加しないでください。これは評価試験で多くの人が犯す間違いです。nn.MSELossは Diffusion Models(ノイズ予測)を学ぶ際に非常に重要になります。
4.2 オプティマイザ
# SGD — 基本的、学習率を手動で管理
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# Adam — 最も人気、適応的学習率
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
# AdamW — Adam + 適切なweight decay(Transformerに使用)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)
| オプティマイザ | 使用場面 | 特徴 |
|---|---|---|
| SGD | CNN、細かい制御が必要な場合 | lr の慎重なチューニングが必要、momentumを追加 |
| Adam | ほとんどのタスクのデフォルト | 高速な収束、チューニングが最小限 |
| AdamW | Transformers、LLMs、Diffusion | 分離されたweight decay、Adamより正確 |
4.3 完全なトレーニングループ
これは最も重要なパターンです。評価試験では完全なトレーニングループを書けるようにしておく必要があります:
import torch
import torch.nn as nn
# セットアップ
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = MNISTClassifier().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
# トレーニングループ
num_epochs = 10
for epoch in range(num_epochs):
model.train() # トレーニングモードを有効化(dropout、batchnormが有効)
total_loss = 0
for batch_idx, (images, labels) in enumerate(train_loader):
images, labels = images.to(device), labels.to(device)
# フォワードパス
outputs = model(images)
loss = criterion(outputs, labels)
# バックワードパス
optimizer.zero_grad() # 重要:勾配をリセット
loss.backward() # 勾配を計算
optimizer.step() # 重みを更新
total_loss += loss.item()
avg_loss = total_loss / len(train_loader)
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")
# 評価
model.eval() # dropoutを無効化、batchnormはrunning statsを使用
with torch.no_grad(): # 勾配を計算しない → メモリ節約
correct = 0
total = 0
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f"Accuracy: {100 * correct / total:.2f}%")
トレーニングループのフロー:
┌─────────────────────────────────────────────┐
│ 各エポック │
│ ┌────────────────────────────────────────┐ │
│ │ 各バッチ │ │
│ │ │ │
│ │ 1. images, labels = batch.to(device) │ │
│ │ │ │ │
│ │ 2. outputs = model(images) フォワード │ │
│ │ │ │ │
│ │ 3. loss = criterion(outputs, labels) │ │
│ │ │ │ │
│ │ 4. optimizer.zero_grad() リセット │ │
│ │ │ │ │
│ │ 5. loss.backward() バックワード│ │
│ │ │ │ │
│ │ 6. optimizer.step() 更新 │ │
│ │ │ │ │
│ └──────────────┼─────────────────────────┘ │
│ ▼ │
│ 次のエポック │
└─────────────────────────────────────────────┘
試験のヒント:
zero_grad() → backward() → step()の順序は必須です。zero_grad()を忘れると、勾配がバッチ間で蓄積され、モデルが収束しなくなります。これはDLI評価試験でのバグ第1位です。トレーニング前にmodel.train()、評価前にmodel.eval()+torch.no_grad()を必ず覚えておきましょう。
4.4 GPUアクセラレーション
# GPUの確認
print(torch.cuda.is_available()) # True/False
print(torch.cuda.device_count()) # GPUの数
print(torch.cuda.get_device_name(0)) # GPU名
# モデルとデータをGPUに移動
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
# トレーニングループ内 — データも同じデバイスに配置する必要あり
images = images.to(device)
labels = labels.to(device)
試験のヒント: よくあるエラー:モデルはGPU上にあるが、データはまだCPU上にある(またはその逆)。PyTorch は "Expected all tensors to be on the same device" というエラーを出します。モデルとデータが同じ
deviceにあることを必ず確認してください。
5. CNNアーキテクチャ
5.1 畳み込み層
畳み込みは入力画像上でカーネル(フィルタ)をスライドさせて特徴を抽出します。各カーネルは特定のパターン(エッジ、テクスチャ、形状)を検出します。
# 基本的なConv2d
conv = nn.Conv2d(
in_channels=3, # RGB入力
out_channels=32, # 32フィルタ → 32個の特徴マップ
kernel_size=3, # 3×3カーネル
stride=1, # ステップサイズ
padding=1 # 空間サイズを維持するためのゼロパディング
)
# 出力形状の計算式:
# H_out = (H_in + 2*padding - kernel_size) / stride + 1
# 例: (32 + 2*1 - 3) / 1 + 1 = 32 (サイズが維持される)
畳み込み操作:
入力 (3チャンネル) カーネル (3×3) 出力 (1特徴マップ)
┌─────────────┐ ┌───────┐ ┌──────────┐
│ ■ ■ ■ ■ ■ ■│ × │ w w w │ = │ ○ ○ ○ ○ │
│ ■ ■ ■ ■ ■ ■│ │ w w w │ │ ○ ○ ○ ○ │
│ ■ ■ ■ ■ ■ ■│ │ w w w │ │ ○ ○ ○ ○ │
│ ■ ■ ■ ■ ■ ■│ └───────┘ │ ○ ○ ○ ○ │
│ ■ ■ ■ ■ ■ ■│ └──────────┘
│ ■ ■ ■ ■ ■ ■│ 32カーネル → 32特徴マップ
└─────────────┘
形状の変化: (B, 3, 32, 32) → Conv2d(3, 32, 3, padding=1) → (B, 32, 32, 32)
5.2 プーリング層
プーリングは空間次元を縮小し、計算量の削減と受容野の拡大に役立ちます:
| プーリング | 仕組み | 使用場面 |
|---|---|---|
nn.MaxPool2d(2) | 各2×2ウィンドウ内の最大値を取得 | 特徴検出、標準的なCNN |
nn.AvgPool2d(2) | 各2×2ウィンドウ内の平均値を取得 | より滑らかな特徴 |
nn.AdaptiveAvgPool2d((1,1)) | 入力に関係なく固定サイズにプーリング | 全結合層の前 |
5.3 バッチ正規化 vs グループ正規化
この知識は、後のレッスンの Diffusion Models セクションで極めて重要です。
| 特性 | BatchNorm | GroupNorm |
|---|---|---|
| 正規化の対象 | バッチ次元 (N) | チャンネルグループ (C/G) |
| バッチサイズへの依存 | あり — 小さいバッチではノイズが多い | なし — どのバッチサイズでも安定 |
| トレーニング vs 推論 | 異なる(running stats) | 同じ |
| よく使われる場面 | 従来のCNN(ResNet) | Diffusion Models、小グループ |
| 構文 | nn.BatchNorm2d(C) | nn.GroupNorm(G, C) |
# BatchNorm — バッチ全体で正規化
bn = nn.BatchNorm2d(64) # 64チャンネル
# GroupNorm — チャンネルのグループ内で正規化
gn = nn.GroupNorm(
num_groups=32, # 64チャンネルを32グループに分割(2ch/グループ)
num_channels=64
)
# 両方とも入力形状: (B, C, H, W)
x = torch.randn(8, 64, 16, 16)
print(bn(x).shape) # (8, 64, 16, 16)
print(gn(x).shape) # (8, 64, 16, 16)
BatchNorm vs GroupNorm:
BatchNorm: ↓方向(バッチ軸)で正規化 GroupNorm: →方向(チャンネルグループ)で正規化
┌────────────────────────┐ ┌────────────────────────┐
│ Sample 1: [c1 c2 c3 c4]│ │ Sample 1: [c1 c2│c3 c4]│
│ Sample 2: [c1 c2 c3 c4]│ ← 各列を │ group1│group2 │ ← 各グループを
│ Sample 3: [c1 c2 c3 c4]│ 正規化 │ │ │ 正規化
│ Sample 4: [c1 c2 c3 c4]│ │ Sample 2: [c1 c2│c3 c4]│
└────────────────────────┘ └────────────────────────┘
→ Diffusion Modelsではbatch_sizeが通常小さく、
ノイズレベルが異なるため、BatchNormの統計が不安定になる
→ GroupNormを使用
試験のヒント: Diffusion Models用のU-Net(後のレッスン)を構築する際は、常にBatchNormではなく GroupNorm を使用します。理由:Diffusionのトレーニングは通常小さなバッチサイズを使用し、各サンプルのノイズレベルが異なるため、BatchNormの統計が不安定になります。ルール:Diffusion = GroupNorm。
5.4 コード:画像分類用のシンプルなCNN
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
# Convブロック1: (B, 1, 28, 28) → (B, 32, 14, 14)
self.block1 = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2)
)
# Convブロック2: (B, 32, 14, 14) → (B, 64, 7, 7)
self.block2 = nn.Sequential(
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2)
)
# Convブロック3: (B, 64, 7, 7) → (B, 128, 1, 1)
self.block3 = nn.Sequential(
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.AdaptiveAvgPool2d((1, 1)) # グローバル平均プーリング
)
# 分類器
self.classifier = nn.Linear(128, num_classes)
def forward(self, x):
x = self.block1(x) # (B, 32, 14, 14)
x = self.block2(x) # (B, 64, 7, 7)
x = self.block3(x) # (B, 128, 1, 1)
x = x.view(x.size(0), -1) # (B, 128)
x = self.classifier(x) # (B, 10)
return x
CNNアーキテクチャのフロー:
入力: (B, 1, 28, 28)
│
┌────▼────────────────────────┐
│ Conv2d(1→32, 3×3, pad=1) │
│ BatchNorm2d(32) │ ブロック1
│ ReLU │
│ MaxPool2d(2) │
└────┬────────────────────────┘
│ (B, 32, 14, 14)
┌────▼────────────────────────┐
│ Conv2d(32→64, 3×3, pad=1) │
│ BatchNorm2d(64) │ ブロック2
│ ReLU │
│ MaxPool2d(2) │
└────┬────────────────────────┘
│ (B, 64, 7, 7)
┌────▼────────────────────────┐
│ Conv2d(64→128, 3×3, pad=1) │
│ BatchNorm2d(128) │ ブロック3
│ ReLU │
│ AdaptiveAvgPool2d(1,1) │
└────┬────────────────────────┘
│ (B, 128, 1, 1)
┌────▼────────────────────────┐
│ Flatten → (B, 128) │
│ Linear(128, 10) │ 分類器
└────┬────────────────────────┘
│ (B, 10)
▼
出力 logits
6. チートシート
| 概念 | コードパターン | 覚えるべきこと |
|---|---|---|
| モデル作成 | class MyModel(nn.Module) | 必ず super().__init__() を呼ぶ |
| フォワードパス | output = model(x) | モデルを関数として呼ぶ、.forward() を直接呼ばない |
| トレーニングモード | model.train() | dropout、batchnormのトレーニング統計を有効化 |
| 評価モード | model.eval() + torch.no_grad() | dropoutを無効化、running statsを使用 |
| トレーニングループ | zero_grad → forward → loss → backward → step | 順序が重要 |
| GPU転送 | .to(device) | モデルとデータの両方 |
| 分類損失 | nn.CrossEntropyLoss() | softmaxを内包 |
| Diffusion損失 | nn.MSELoss() | ノイズ予測、MSEを計算 |
| Diffusion正規化 | nn.GroupNorm(G, C) | バッチサイズに依存しない |
| Transformerオプティマイザ | AdamW | 適切なweight decay |
7. 練習問題
以下の問題はNVIDIA DLIコーディング評価試験のスタイルを模擬しています。コードを読み、バグを見つけ、完全なコードを書く必要があります。
Q1:壊れたトレーニングループを修正する
以下のコードにはモデルが収束しないバグがあります。エラーを見つけて修正してください:
for epoch in range(10):
model.train()
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
回答 Q1 を表示
バグ: loss.backward() の前に optimizer.zero_grad() がありません。勾配をリセットしないと、バッチ間で勾配が蓄積され、モデルが収束しないか、誤った収束をします。
for epoch in range(10):
model.train()
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad() # ← この行を追加
loss.backward()
optimizer.step()
標準的な順序:zero_grad() → backward() → step()。実際には、zero_grad() を forward の前に置くこともできますが、backward() の前には必ず置く必要があります。
Q2:3層CNNを実装する
RGB画像 (3, 64, 64) を入力として5クラスを出力するCNNクラスを書いてください。要件:
- 3つの畳み込みブロック、各ブロック:Conv2d → BatchNorm2d → ReLU → MaxPool2d(2)
- チャンネル数:3 → 32 → 64 → 128
- 最後に AdaptiveAvgPool2d + Linear
回答 Q2 を表示
class ThreeLayerCNN(nn.Module):
def __init__(self, num_classes=5):
super().__init__()
self.features = nn.Sequential(
# ブロック1: (B, 3, 64, 64) → (B, 32, 32, 32)
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2),
# ブロック2: (B, 32, 32, 32) → (B, 64, 16, 16)
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2),
# ブロック3: (B, 64, 16, 16) → (B, 128, 8, 8)
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2),
)
self.pool = nn.AdaptiveAvgPool2d((1, 1))
self.classifier = nn.Linear(128, num_classes)
def forward(self, x):
x = self.features(x) # (B, 128, 8, 8)
x = self.pool(x) # (B, 128, 1, 1)
x = x.view(x.size(0), -1) # (B, 128)
x = self.classifier(x) # (B, 5)
return x
# 検証
model = ThreeLayerCNN(num_classes=5)
x = torch.randn(4, 3, 64, 64)
print(model(x).shape) # torch.Size([4, 5])
Q3:ネットワークを通過するテンソルの形状を追跡する
以下のモデルと入力形状 (8, 1, 32, 32) が与えられています。各ステップの形状を記録してください:
model = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=5, stride=2, padding=2), # ステップA
nn.ReLU(),
nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=0), # ステップB
nn.ReLU(),
nn.AdaptiveAvgPool2d((4, 4)), # ステップC
nn.Flatten(), # ステップD
nn.Linear(32 * 4 * 4, 10), # ステップE
)
回答 Q3 を表示
出力サイズの計算式:H_out = (H_in + 2*padding - kernel_size) / stride + 1
入力: (8, 1, 32, 32)
ステップA: Conv2d(1, 16, k=5, s=2, p=2)
H = (32 + 2*2 - 5) / 2 + 1 = 16
→ (8, 16, 16, 16)
ステップB: Conv2d(16, 32, k=3, s=1, p=0)
H = (16 + 2*0 - 3) / 1 + 1 = 14
→ (8, 32, 14, 14)
ステップC: AdaptiveAvgPool2d((4, 4))
→ (8, 32, 4, 4)
ステップD: Flatten()
→ (8, 512) # 32 * 4 * 4 = 512
ステップE: Linear(512, 10)
→ (8, 10)
重要なポイント: AdaptiveAvgPool2d は入力に関係なく常に固定サイズを出力します — 入力サイズが変わる場合に非常に便利です。
Q4:GroupNorm vs BatchNorm — どちらをいつ使うか?
Diffusion Model用のU-Netを構築しています。各ブロックは Conv2d → ??? → SiLU です。どの正規化を選択しますか?理由は?1つのブロックのコードを書いてください。
回答 Q4 を表示
GroupNormを選択します。 理由:
- 小さなバッチサイズ: Diffusionのトレーニングは通常バッチサイズ1〜8で、画像が大きいため → BatchNormの統計が不安定になる
- 異なるノイズレベル: バッチ内の各サンプルは異なるタイムステップ(ノイズレベル)を持つ → バッチ全体での正規化は適切でない
- 推論の一貫性: GroupNormはトレーニング時と推論時で同じ動作をする
class DiffusionBlock(nn.Module):
def __init__(self, in_channels, out_channels, num_groups=32):
super().__init__()
self.conv = nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1)
self.norm = nn.GroupNorm(num_groups, out_channels)
self.act = nn.SiLU() # DiffusionではReLUよりSiLUが一般的
def forward(self, x):
x = self.conv(x)
x = self.norm(x)
x = self.act(x)
return x
# 使用例
block = DiffusionBlock(64, 128, num_groups=32)
x = torch.randn(2, 64, 32, 32) # batch_size = 2、小さい!
print(block(x).shape) # (2, 128, 32, 32)
ルール: すべてのDiffusionアーキテクチャ(U-Net、DiT)では、常に nn.GroupNorm を使用します。活性化関数は通常、ReLUではなく nn.SiLU()(Swish)です。
Q5:勾配の問題をデバッグ — detach() vs torch.no_grad()
以下のコードには何が問題がありますか?feature_extractor の出力に勾配があってはなりません(バックボーンをフリーズ)が、classifier はトレーニングする必要があります。
feature_extractor = pretrained_model.features
classifier = nn.Linear(512, 10).to(device)
optimizer = torch.optim.Adam(classifier.parameters(), lr=1e-3)
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
# 特徴抽出(フリーズされるべき)
with torch.no_grad():
features = feature_extractor(images)
# 分類
outputs = classifier(features)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward() # ← 問題はありますか?
optimizer.step()
回答 Q5 を表示
問題: このコードは実際にはこのケースでは正しく動作します! torch.no_grad() は feature_extractor の勾配計算を防止し、features テンソルは requires_grad を持ちません。勾配は classifier を通じて正常に流れます。
ただし、2つのアプローチがあり、その違いを理解する必要があります:
# アプローチ1: torch.no_grad() — 勾配を計算しない、メモリ節約
with torch.no_grad():
features = feature_extractor(images)
# features.requires_grad = False
# 勾配はfeature_extractorには逆伝播しない
# ✅ 完全にフリーズしてGPUメモリを節約したい場合に使用
# アプローチ2: .detach() — テンソルを計算グラフから切り離す
features = feature_extractor(images).detach()
# features.requires_grad = False
# Feature extractorはフォワード計算を実行(グラフ用のメモリを使用)
# しかし勾配は.detach()で切断される
# ⚠️ グラフが構築されてから切断されるため効率が低い
# アプローチ3: パラメータをフリーズ — 最も一般的なアプローチ
for param in feature_extractor.parameters():
param.requires_grad = False
# ✅ 最も明示的、ファインチューニングで一般的に使用
| アプローチ | 勾配の流れ | メモリ | 使用場面 |
|---|---|---|---|
torch.no_grad() | グラフは計算されない | 最も効率的 | 推論、フリーズされた特徴 |
.detach() | detach地点で切断 | より高コスト | 部分的な勾配の流れが必要な場合 |
| パラメータフリーズ | 重みは更新されない | グラフは構築される | 明示的なファインチューニング |
8. まとめ
第1課では、NVIDIA DLI Generative AI コースに必要なすべてのPyTorchの基礎を学びました。以下のことができるようにしておきましょう:
- ドキュメントを参照せずに完全なトレーニングループを書ける
__init__とforwardを持つ nn.Module クラスを作成できる- 各レイヤーを通過するテンソルの形状を追跡できる
- GroupNorm vs BatchNorm を区別できる — 特にDiffusion Modelsで重要
- よくあるエラーをデバッグできる:
zero_grad()の欠落、デバイスの不一致、勾配の問題
次のレッスン:第2課 — TransformerアーキテクチャとAttentionメカニズム — TransformerとLLMsの基礎。