Chuyển đến nội dung chính

第1課:PyTorchとニューラルネットワークの基礎

PyTorchテンソル、autograd、nn.Module。ニューラルネットワークをゼロから構築。 トレーニングループ、損失関数、オプティマイザ。GPUアクセラレーションの基礎。 CNNアーキテクチャ、プーリング、バッチ正規化。

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.Tpermute は複数次元に対してより柔軟
次元追加x.unsqueeze(dim)ブロードキャスティングの準備によく使用
次元削除x.squeeze(dim)サイズ1の次元を削除
行列乗算a @ btorch.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)
オプティマイザ使用場面特徴
SGDCNN、細かい制御が必要な場合lr の慎重なチューニングが必要、momentumを追加
Adamほとんどのタスクのデフォルト高速な収束、チューニングが最小限
AdamWTransformers、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 セクションで極めて重要です。

特性BatchNormGroupNorm
正規化の対象バッチ次元 (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を選択します。 理由:

  1. 小さなバッチサイズ: Diffusionのトレーニングは通常バッチサイズ1〜8で、画像が大きいため → BatchNormの統計が不安定になる
  2. 異なるノイズレベル: バッチ内の各サンプルは異なるタイムステップ(ノイズレベル)を持つ → バッチ全体での正規化は適切でない
  3. 推論の一貫性: 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の基礎。