モジュール 08 · 第 4 回

PyTorch 入門

最初の三つの課で手書きしたものを、PyTorch でもう一度やります。テンソル、自動微分、nn.Module、オプティマイザ。どのステップも手書きの版と照らし合わせ、計算した勾配も学習したパラメータもまったく同じになります。

  • 約 45 分
  • 難易度:入門
  • 検証:2026-09-14 torch 2.14、CPU、乱数シード固定

コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。

最初の三つの課で、線形回帰、勾配降下法、誤差逆伝播法を手書きしました。どれも使えますが遅く、モデルを替えるたびに多くのコードを書き直す必要があります。

PyTorch は現在最もよく使われているディープラーニングのフレームワークです。この課ではその機能をすべて扱うのではなく、一つのことだけをします。最初の三つの課でやったことを PyTorch でもう一度やり、どのステップも手書きの版と照らし合わせるのです。PyTorch がしていることは、あなたが手で書いたものとまったく同じで、ただより速く、より便利な書き方に替わっただけだとわかるはずです。

インストールします(GPU がなくてもかまいません。この課と次のモジュールはすべて CPU で動きます)。

uv add torch

テンソルと自動微分

PyTorch の最も基本的なものはテンソル(tensor)です。多次元の配列で、numpy の配列によく似ています。一つの数は 0 次元のテンソル、数の並びは 1 次元、表は 2 次元です。

テンソルには numpy の配列にない能力があります。自動微分です。テンソルを作るときに requires_grad=True を付けると、PyTorch はそれが参加するすべての演算を記録します。前の課の Num と同じです。

a = torch.tensor(2.0, requires_grad=True)  # requires_grad:记下它参与的运算,以便求导
b = torch.tensor(-3.0, requires_grad=True)
c = torch.tensor(10.0, requires_grad=True)
f = (a * b + c) ** 2
f.backward()
print(f"  f = {f.item()},df/da={a.grad.item()}, df/db={b.grad.item()}, df/dc={c.grad.item()}")
== 1. 同一个算式 f = (a × b + c)²,a=2, b=-3, c=10
  f = 16.0,df/da=-24.0, df/db=16.0, df/dc=8.0

前の課で手書きした Num が計算した結果とまったく同じです。-24、16、8。f.backward() がしていることは、前の課の Num.backward がしていたことです。f から出発して記録した計算の過程に沿い、連鎖律で勾配を伝え戻して、各テンソルの .grad に保存します。.item() は数が一つだけのテンソルを普通の Python の数に変換します。

線形回帰:四つのもの

第 2 課の線形回帰を PyTorch で書き直すと、PyTorch でモデルを学習させる四つの基本的なものを使うことになります。

x = torch.tensor((area - area.mean()) / area.std(), dtype=torch.float32).unsqueeze(1)  # 标准化,形状 (50, 1)
y = torch.tensor(price, dtype=torch.float32).unsqueeze(1)

model = nn.Linear(1, 1)  # 就是 w × x + b
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
loss_fn = nn.MSELoss()
for step in range(100):
    loss = loss_fn(model(x), y)
    optimizer.zero_grad()  # 清空上一步的梯度
    loss.backward()  # 反向传播,算出每个参数的梯度
    optimizer.step()  # 按梯度更新参数:p -= lr × p.grad
  • モデルnn.Linear(1, 1)w × x + b そのもので、入力 1 個、出力 1 個です。wb の二つのパラメータを自分で作って管理し、requires_grad もすでに設定されています。
  • 損失関数nn.MSELoss() は第 1 課の平均二乗誤差です。
  • オプティマイザtorch.optim.SGD がパラメータの更新を担当します。optimizer.step() は第 2 課の w -= lr × dw そのもので、ただ model.parameters() の中のすべてのパラメータに対してこれを行います。
  • 学習のループ:勾配をゼロにし、逆伝播し、パラメータを更新する。3 行で、順番は前の課で手書きしたものとまったく同じです。

unsqueeze(1) は形を (50,) から (50, 1) に変えます。「サンプルが 50 個、各サンプルに特徴が 1 個」という意味です。PyTorch の層はどれも最初の次元をサンプル数とする約束になっていて、これについては後で詳しく説明します。

== 2. 线性回归 100 步后:损失 143.05,换算回原单位 w=1.2571 b=14.806

第 2 課で手書きした勾配降下法の結果とまったく同じです。損失 143.05、w = 1.2571, b = 14.806

自分のネットワーク:nn.Module を継承する

少し複雑なモデルは、nn.Module を継承したクラスを自分で書く必要があります。前の課の XOR のネットワークを書き直してみましょう。

class TinyNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(2, 4)
        self.output = nn.Linear(4, 1)

    def forward(self, x):
        return torch.tanh(self.output(torch.tanh(self.hidden(x))))

__init__ でどんな層があるかを定義し、forward でデータがそれらの層をどう流れるかを書きます。前の課では NeuronLayer の二つのクラスを書きましたが、今は nn.Linear(2, 4) 一つが「入力 2 個のニューロン 4 個からなる一つの層」で、4 個のニューロンの計算をまとめて、1 回の行列の掛け算で済ませます。

nn.Module__init__ で定義したすべての層のパラメータを自動で見つけ、net.parameters() でそれらを取り出せます。数えてみましょう。

== 3. 异或网络,共 17 个参数

前の課で手書きしたネットワークと同じく、17 個のパラメータです。学習させます。

X = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=torch.float32)
Y = torch.tensor([[-1], [1], [1], [-1]], dtype=torch.float32)

optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
for epoch in range(1, 301):
    loss = ((net(X) - Y) ** 2).sum()  # 和第 3 课一样:4 个样本的平方误差之和
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

net(X) が 4 個のサンプルを一度に計算し終えている点に注目してください。前の課では for ループで一つずつ計算していましたが、今は 4 個のサンプルを形が (4, 2) のテンソル一つに入れ、1 回の行列演算ですべて計算しています。

  第   1 轮  损失 4.5469
  第  10 轮  损失 3.7176
  第  50 轮  损失 0.5588
  第 100 轮  损失 0.0514
  第 200 轮  损失 0.0174
  第 300 轮  损失 0.0100
  训练后的预测: ['-0.953', '+0.964', '+0.944', '-0.943']

同じように XOR を学習しました。損失の変わり方は前の課の数字と完全には一致しません。パラメータの初期値がランダムで、PyTorch と私たちの手書きの版では違う乱数を使っているからです。しかし傾向は同じです。最初はゆっくりで、それから急に下がり、最後は 0 に近づきます。

学習し終えて予測をするときは、torch.no_grad() をかぶせています。

with torch.no_grad():  # 只是预测,不需要记录求导信息
    print("  训练后的预测:", [f"{v:+.3f}" for v in net(X).squeeze(1).tolist()])

既定では、PyTorch はすべての演算について微分に必要な情報を記録します。予測をするだけで学習が要らないときは、これをオフにすればメモリを節約でき、速くもなります。

形:最も間違えやすいところ

PyTorch でコードを書くと、エラーの大部分はテンソルの形に関係しています。一つの約束をしっかり覚えておきましょう。最初の次元は一束のサンプルの数です。

batch = torch.randn(32, 2)  # 32 个样本,每个 2 个特征
== 4. 一次喂 32 个样本:输入形状 (32, 2) → 隐藏层 (32, 4) → 输出 (32, 1)

入力はサンプル 32 個で、それぞれ特徴が 2 個。隠れ層を通るとサンプル 32 個で、それぞれ数が 4 個。最後はサンプル 32 個で、それぞれ出力が 1 個です。サンプルの数は過程全体で変わらず、どの層も最後の次元だけを変えます。

サンプルを一つずつ処理するのではなく一束をまとめて処理することは、ディープラーニングが速い重要な理由の一つです。行列演算は CPU でも、特に GPU では高度に最適化されていて、サンプル 32 個を一度に計算するのにかかる時間は、1 個を計算するのとたいして変わりません。

コードを書くときは、要所で x.shape を表示するのが最も効果的なデバッグ方法です。

手書きと PyTorch の対照表

手書きの版 PyTorch
Num、局所微分を記録する テンソル、requires_grad=True
Num.backward() loss.backward()
p.grad = 0.0 optimizer.zero_grad()
p.value -= lr * p.grad optimizer.step()
NeuronLayer の二つのクラス nn.Linear
平均二乗誤差の関数 nn.MSELoss()
サンプルを一つずつループする 一束のサンプルをテンソル一つに入れ、一度に計算する

PyTorch は魔法を使っていません。その最も中心的な部分は、あなたがすでに自分の手で書きました。今はそれを、より速く、より手間のかからない書き方に替えただけです。今後、損失が下がらない、勾配が NaN になるといった奇妙な学習の問題に出会っても、下で何が起きているかを知っていれば、どこから調べればよいかがわかります。

練習問題

  1. 線形回帰のデータを標準化していない面積(area をそのまま使う)に替えてください。発散しないためには学習率をいくつにする必要がありますか。第 2 課の結論と一致しますか。
  2. torch.optim.SGDtorch.optim.Adam に替え、学習率を 0.01 にして、XOR のネットワークを学習させ直してください。損失は速く下がりますか、遅く下がりますか。
  3. わざと形のエラーを起こしてください。XOR の入力 X を形 (4, 3)(各サンプルに特徴 3 個)に変えて実行し、エラーメッセージを読んで、形が合わないことをどう伝えているか見てください。

確認テスト

1. optimizer.zero_grad()、loss.backward()、optimizer.step() はそれぞれ何をしていますか?

zero_grad はすべてのパラメータの勾配をゼロにし、前のステップの勾配と足し合わされないようにします。backward は損失から出発して逆伝播し、各パラメータの勾配を計算して .grad に保存します。step は勾配に従って各パラメータを更新し、最も簡単な SGD なら p = p - lr × p.grad です。

2. PyTorch がテンソルの最初の次元をサンプル数とする約束にしているのはなぜですか?

ディープラーニングはいつも一束のサンプルをまとめて処理し、それらをテンソル一つに入れて行列演算で一緒に計算するほうが、一つずつ計算するよりずっと速いからです。最初の次元をサンプル数と決めておけば、どの層も最後の特徴の次元だけを処理すればよく、サンプル数はネットワーク全体で変わりません。

3. 予測をするとき torch.no_grad() を使うのはなぜですか?

既定では、PyTorch はすべての演算について逆伝播に必要な情報を記録し、それには余分なメモリと時間がかかります。予測をするだけで学習が要らないときは、no_grad でこれをオフにすれば、資源を節約でき、速くもなります。

質問と議論

このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。

質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。

議論を読み込んでいます…