1. nn.Module とは
PyTorch のモデルは nn.Module を継承したクラスとして書きます。
__init__ で使う層を用意し、forward で順伝播の流れを書く——この2つが基本構造です。
ここで紛らわしいのが nn.Module と nn.Linear の関係です。
どちらも nn(=torch.nn というモジュール)の中にある名前なので、その意味では両方とも「nn の属性」で合っています。
ただし両者は対等な部品ではありません。nn.Module は「すべての層・モデルの親クラス(基底クラス)」で、
nn.Linear はその nn.Module を継承して作られた一種——という上下(親子)の関係にあります。
💡 「〜は nn.Module の一種」という継承関係(is-a):
nn.Module… 親クラス。共通の土台で、パラメータの管理やforward呼び出しの仕組みを持つnn.Linear/nn.Conv2d/nn.ReLU… 親を継承した子(PyTorch 既製の層)- 自分で書く
class Net(nn.Module)… 同じく親を継承した子(層を組み合わせた自作モデル)
つまり「層も実は nn.Module」とは、「nn.Linear も nn.Module を継承している=nn.Module の一種だ」という意味です。
だからモデルは「nn.Module(自作の Net)の中に nn.Module(nn.Linear などの層)を入れ子にしたもの」になります。
後で print(model) が層を一覧表示できるのも、この入れ子構造を nn.Module が把握しているおかげです。
L04 の autograd がこの上で自動的に効くので、私たちは順伝播だけ書けば、逆伝播は PyTorch がやってくれます。
2. モデルを定義する
2層の全結合ネットワークを定義します。__init__ の先頭で super().__init__() を必ず呼ぶこと(これを忘れると層が登録されません)。
forward は「入力 x をどう流すか」を書くだけ。呼び出しは model(x) で、内部的に forward が走ります。
💡 super().__init__() は何をしている?
これは親クラス nn.Module の初期化処理を呼ぶ命令です。
nn.Module は内部に「登録された層・パラメータを記録しておく箱」を持っており、その箱は super().__init__() が用意します。
この箱が準備できているからこそ、続く self.fc1 = nn.Linear(...) が単なる代入ではなく
「学習対象のパラメータを持つ層」として自動登録されます。
もし super().__init__() を書かないと、箱が無いまま代入することになり、
model.parameters() に層が現れない/エラーになります。
「nn.Module を継承したら __init__ の先頭で super().__init__()」は定型のお約束として覚えてください。
import torch
import torch.nn as nn
torch.manual_seed(0)
class Net(nn.Module):
def __init__(self):
super().__init__() # お約束。これを忘れると層が登録されない
self.fc1 = nn.Linear(4, 8) # 全結合 4 -> 8
self.fc2 = nn.Linear(8, 3) # 全結合 8 -> 3(3クラス分類)
def forward(self, x):
x = torch.relu(self.fc1(x)) # 1層目 + ReLU
return self.fc2(x) # 2層目(出力=ロジット)
model = Net()
x = torch.randn(5, 4) # 入力 (N=5, 特徴4)
out = model(x) # forward が走る
print("out.shape:", out.shape) # (5, 3)
print(model)
out.shape: torch.Size([5, 3])
Net(
(fc1): Linear(in_features=4, out_features=8, bias=True)
(fc2): Linear(in_features=8, out_features=3, bias=True)
)
💡 出力の形=最後の層の out_features:
入力 (5, 4) が fc1 で (5, 8)、ReLU で形そのまま、fc2 で (5, 3)。
3クラス分類なら最後の層の出力は3。この出力は softmax 前の生の値で、ロジット(logits)と呼びます。
forward で使った ReLU には書き方が3通りあり、どれも計算は同じです。
① 関数版 torch.relu(x)(このレッスンで使用)/
② torch.nn.functional の F.relu(x)(import torch.nn.functional as F が必要)/
③ クラス版 nn.ReLU() を作って nn.ReLU()(x) と呼ぶ(層として __init__ に置き nn.Sequential に並べたいとき向け)。
ReLU は学習パラメータを持たない処理なので、forward の中で関数版を直接呼ぶのが手軽です。
3. nn.Linear の中身(重みの形に注意)
nn.Linear(in, out) は内部に重み weight とバイアス bias を持ちます。
ここで重要な注意:weight の形は (out, in) です(L02 で手書きした (in, out) と逆向き)。
PyTorch は内部で x · weightᵀ + bias と転置して掛けるため、結果は同じく (N, out) になります。
import torch
import torch.nn as nn
torch.manual_seed(0)
fc1 = nn.Linear(4, 8)
print("weight.shape:", fc1.weight.shape) # (out, in) = (8, 4)
print("bias.shape :", fc1.bias.shape) # (out,) = (8,)
# パラメータ数: fc1=4*8+8=40, fc2=8*3+3=27 → 合計67
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(4, 8)
self.fc2 = nn.Linear(8, 3)
def forward(self, x):
return self.fc2(torch.relu(self.fc1(x)))
model = Net()
total = sum(p.numel() for p in model.parameters())
print("パラメータ総数:", total) # 67
weight.shape: torch.Size([8, 4])
bias.shape : torch.Size([8])
パラメータ総数: 67
💡 p.numel() と model.parameters():
numel は number of elements の略で、テンソルの要素数(全成分の個数)を返します。
たとえば fc1.weight は形 (8, 4) なので numel() は 8×4=32、fc1.bias は (8,) なので 8 です。
一方 model.parameters() はモデルが持つ全パラメータ(各層の weight と bias)を順に取り出すイテレータ。
だから sum(p.numel() for p in model.parameters()) は「全パラメータの要素数を合計する」=パラメータ総数を一発で求める定番の書き方です。
in×out + out(重み+バイアス)。
fc1 は 4×8+8=40、fc2 は 8×3+3=27、合計 67。
パラメータ数を問う問題では、この式(バイアスを忘れない)を使います。
4. 損失関数:CrossEntropyLoss と MSELoss
出力と正解のズレを測るのが損失関数です。分類は nn.CrossEntropyLoss、回帰は nn.MSELoss が定番。
最重要の注意点:CrossEntropyLoss は内部で softmax(正確には log-softmax)を適用します。
つまりモデルの出力はロジット(softmax前)を渡すのが正解です。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 分類:出力はロジット (N, クラス数)、正解はクラス番号 (N,)
logits = torch.randn(5, 3) # 5サンプル・3クラスのロジット
target = torch.tensor([0, 2, 1, 0, 1]) # 各サンプルの正解クラス番号
ce = nn.CrossEntropyLoss()
print("CE loss:", round(ce(logits, target).item(), 4))
# 回帰:出力と正解はどちらも同じ形の実数
pred = torch.randn(5, 1)
y = torch.randn(5, 1)
mse = nn.MSELoss()
print("MSE loss:", round(mse(pred, y).item(), 4))
CE loss: 0.9111
MSE loss: 2.0292
💡 CrossEntropyLoss が内部でしている3ステップ:
- ① log-softmax:ロジットを「確率の対数」に変換する(softmax を取ってから log を取る計算を、数値的に安定な形でまとめて実行)
- ② 正解クラスの値を取り出す:
targetが指すクラスの対数確率だけを拾う - ③ 符号を反転して平均:
−(対数確率)をサンプル全体で平均する=負の対数尤度(NLL)
まとめると CrossEntropyLoss = log_softmax + NLLLoss。
softmax は①の中に既に含まれているので、モデル側で softmax を付ける必要はありません。
この「内部で softmax 済み」を知らずに、モデルの最後へ自分で softmax を付けてから渡すと——softmax が2回かかります(二重適用)。
同じ logits・target で比べると、損失の値が変わってしまうのが確認できます。
# 正しい:ロジットをそのまま渡す
print(round(ce(logits, target).item(), 4))
# 誤り:自分で softmax してから渡す(softmax が二重にかかる)
probs = torch.softmax(logits, dim=1)
print(round(ce(probs, target).item(), 4))
0.9111
0.9589
0.9111(正しい)に対し、二重適用すると 0.9589 とズレます。softmax を2回かけると確率分布がさらに平らに潰れ、
正解クラスと不正解クラスの差(モデルが学ぶべき手がかり)がぼやけて、勾配が弱まり学習が進みにくくなるのが原因です。
⚠ 実装でつまずきやすい点:softmax の二重適用
- モデルの最後に softmax を付けない。ロジットのまま
CrossEntropyLossへ渡す(softmax は損失の内部で行われる)。 - 正解(target)はクラス番号の整数(one-hot ではない)。形は
(N,)、型は long。 - 自分で
log_softmaxを書きたいときは、損失をnn.NLLLossにする(CrossEntropyLoss = log_softmax + NLLLoss)。 - 推論で「確率が見たい」ときは、損失計算とは別に
torch.softmax(logits, dim=1)を使う(学習の損失には通さない)。
5. 練習問題
出力の shape は?
次のモデルに (16, 10) の入力を通すと、出力 shape はどれですか。
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(10, 32)
self.fc2 = nn.Linear(32, 2)
def forward(self, x):
return self.fc2(torch.relu(self.fc1(x)))
out = Net()(torch.randn(16, 10))
- A. (16, 32)
- B. (16, 2)
- C. (32, 2)
答えと解説を見る
正解:B. (16, 2)
バッチ16はそのまま。特徴は 10 → 32 →(ReLU)→ 2 と流れ、最後の層の出力2が残ります。
CrossEntropyLoss に渡すのは?
分類で nn.CrossEntropyLoss を使うとき、モデルの出力はどうあるべきですか。
- A. softmax を通した確率
- B. softmax を通す前のロジット
- C. one-hot ベクトル
答えと解説を見る
正解:B. softmax を通す前のロジット
CrossEntropyLoss は内部で log-softmax を行うため、ロジットをそのまま渡すのが正解。
自分で softmax を付けると二重適用になり、学習が崩れます。正解ラベルはクラス番号(整数)です。
パラメータ数は?
nn.Linear(100, 10) 1層が持つパラメータ数(重み+バイアス)はいくつですか。
- A. 1000
- B. 1010
- C. 110
答えと解説を見る
正解:B. 1010
重みは in×out = 100×10 = 1000、バイアスは out = 10。合計 1010。
バイアスを忘れないのがコツです。
6. まとめ
このレッスンのポイント
- モデルは
nn.Moduleを継承。__init__で層を用意、forwardで流れを書く __init__冒頭のsuper().__init__()は必須- 呼び出しは
model(x)(内部でforwardが走る) nn.Linear(in, out)のweightは形(out, in)、計算はx·Wᵀ+b- 1層のパラメータ数は
in×out + out(バイアスを忘れない) - 分類は
CrossEntropyLoss(softmaxを内包=ロジットを渡す/正解はクラス番号)、回帰はMSELoss
腕試し:出力を予想してみよう
このレッスンの仕上げです。次のコードを頭の中で実行して、出力を予想してみましょう (PyTorchはブラウザでは動かせないので、メモに書き出してから答えで確認するのがおすすめ)。
import torch
import torch.nn as nn
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(4, 8)
self.fc2 = nn.Linear(8, 5) # 出力 3 → 5 に変更
def forward(self, x):
return self.fc2(torch.relu(self.fc1(x)))
model = Net()
out = model(torch.randn(10, 4)) # 入力 (10, 4)
print(out.shape) # ① どんな形?
print(sum(p.numel() for p in model.parameters())) # ② パラメータ総数は?
答えと解説を見る
torch.Size([10, 5])
85
① バッチ 10 はそのまま、特徴は 4 → 8 →(ReLU)→ 5 と流れ、最後の層の出力 5 が残るので torch.Size([10, 5])。
② fc1 = 4×8+8 = 40、fc2 = 8×5+5 = 45、合計 85(バイアスを忘れない)。
おまけ:もし forward から torch.relu(...) を外すと、出力の形は (10, 5) のままですが、線形層だけが重なって実質1つの線形変換に潰れ、層を増やした意味が無くなります(非線形が表現力を生む)。
次は、このモデルを実際に学習させる学習ループ(最適化・正則化・DataLoader)を読み解きます。
完了するとコース一覧に進捗が記録されます