手書き数字を認識する
最初の本物の分類タスクです。ネットワークに 8×8 ピクセルの手書き数字を認識させます。分類と回帰の違い、softmax と交差エントロピーが何をしているのか、なぜミニバッチで学習するのかを説明し、最後に混同行列でどこを間違えているのかを見ます。
- 約 45 分
- 難易度:中級
- 検証:2026-09-14 torch 2.14、scikit-learn 1.9、CPU、乱数シード固定
コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。
これまでの課のタスクはどれもとても小さなものでした。50 点の住宅価格、4 点の XOR。この課では本物のデータを扱います。1797 枚の手書き数字の画像で、ネットワークに各画像が 0 から 9 のどれを書いたものかを認識させます。
これは分類タスクです。住宅価格の予測が出力するのは連続した数で、回帰と呼ばれます。数字の認識は 10 個のクラスから一つを選ぶ必要があり、出力の形も損失関数も替えなければなりません。この課では何に替えるのか、そしてなぜかを説明します。
データは scikit-learn に付属する手書き数字のデータセットで、インストールすれば入っていて、ダウンロードは不要です。
uv add torch scikit-learn
python digit_classifier.py
データはどんなものか
digits = load_digits()
数据:1797 张图,每张 8×8 像素,像素值 0~16
第一张图,标签是 0:
::**==
**##++##::
..##.. ++==
::** ====
::== ====
::++ **--
..##::++**
--**++
各画像は 8×8 の計 64 ピクセルで、各ピクセルは 0 から 16 の整数、数が大きいほど黒くなります。スクリプトではいくつかの文字で濃淡を描いていて、0 だとわかります。
ネットワークの入力はこの 64 個の数を 1 行に並べたものです。16 で割って 0 から 1 の間に縮めます。理屈は第 2 課の標準化と同じです。
X = torch.tensor(digits.data / 16.0, dtype=torch.float32)
y = torch.tensor(digits.target, dtype=torch.long)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)
训练集 1347 张,测试集 450 张
4 分の 1 の画像はテストセットとして取っておき、学習中はネットワークに一切見せません。最後にこれを使って、ネットワークが本当に数字の認識を学んだのか、それとも学習セットの画像を覚えただけなのかを確かめます。これはモジュール 06 で評価セットを作ったのと同じ理屈です。確認用の問題が練習問題に出てきてはいけないのです。
ネットワーク:10 個のスコアを出力する
model = nn.Sequential(
nn.Linear(64, 64), # 64 个像素 → 64 个隐藏神经元
nn.ReLU(),
nn.Linear(64, 10), # → 10 个输出,分别对应数字 0~9
)
模型共 4810 个参数
nn.Sequential はいくつかの層を順番につなげるもので、クラスを自分で書く手間を省けます。パラメータを計算してみましょう。一つ目の層は 64×64 の重みと 64 のバイアスで 4160 個、二つ目の層は 64×10 と 10 で 650 個、合計 4810 個です。
これまでと違う点が二つあります。
活性化関数が ReLU に替わった。tanh よりさらに単純で、入力が 0 より大きければそのまま出力し、そうでなければ 0 を出力します。計算が速く、層が多いときに tanh より学習させやすいので、今のネットワークで最もよく使われているのは、これとその変種です。
出力が 10 個の数で、数字一つにつき一つです。これはスコア(logits)と呼ばれ、どんな正の数でも負の数でもよく、どれが最も大きいかで、ネットワークがどの数字だと考えているかが決まります。
softmax:スコアを確率に変える
10 個のスコアはそのままでは解釈しにくいものです。softmax はそれを 10 個の確率に変えます。まず各スコアの指数を取り(正の数にするため)、それからすべての合計で割ります(足して 1 にするため)。元のスコアが大きいほど、確率も大きくなります。
probs = torch.softmax(model(X_test[:1]), dim=1)[0]
学習した後の、テストセットの最初の画像の確率です。
测试集第一张图(标签 2)的预测概率:
0:0.00 1:0.00 2:1.00 3:0.00 4:0.00 5:0.00 6:0.00 7:0.00 8:0.00 9:0.00
ネットワークはこれが 2 だとほぼ 100% 確信していて、実際に 2 です。
これとまったく同じものを、モジュール 01 第 2 課で見ました。LLM が各ステップで出力しているのは語彙表の各トークンのスコアで、softmax で確率に変え、その確率に従って次のトークンを選んでいます。違いは、LLM の「クラス」が十数万個あることだけです。
交差エントロピー:分類の損失
回帰では平均二乗誤差を使い、分類では交差エントロピー(cross entropy)を使います。計算の仕方は単純です。ネットワークが正解に与えた確率を見て、その対数を取り、符号を反転させます。
正确答案的概率 损失 = -log(概率)
1.00 0.00
0.90 0.11
0.50 0.69
0.10 2.30
0.01 4.61
正解の確率が 1 に近いほど損失は 0 に近づき、確率が小さいほど損失は大きくなり、しかも急速に増えます。「自信満々に間違える」ネットワークは重く罰せられます。
覚えておく価値のある数値がもう一つあります。学習を始めたばかりのとき、ネットワークは何もわかっておらず、10 個のクラスの確率はどれも 0.1 くらいなので、損失はおよそ -log(0.1) = 2.30 です。10 クラス分類のネットワークを学習させて、最初のステップの損失が 2.3 からかけ離れていたら、たいていどこかの書き間違いです。
PyTorch の nn.CrossEntropyLoss は softmax と交差エントロピーをまとめて計算するので、モデルの最後の層はスコアをそのまま出力し、自分で softmax を加えてはいけません。
loss_fn = nn.CrossEntropyLoss() # 分类问题用交叉熵
モジュール 09 で GPT を学習させるときもこれを使い、そのときのクラスは語彙表の一つ一つのトークンです。
ミニバッチでの学習
これまでの課では、各ステップですべてのデータを使って勾配を計算していました。この課では毎回 64 枚の画像を取るように変えます。
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(1, 31):
# 每一轮把训练集打乱,每次取 64 张图更新一次参数
order = torch.randperm(len(X_train))
for i in range(0, len(X_train), 64):
idx = order[i:i + 64]
loss = loss_fn(model(X_train[idx]), y_train[idx])
optimizer.zero_grad()
loss.backward()
optimizer.step()
これをミニバッチ(mini-batch)での学習と呼びます。1347 枚の画像を 64 枚ずつにすると、1 エポックでパラメータを 22 回更新します。すべての学習データを一通り使うことを 1 エポック(epoch)と呼びます。
なぜ一度にすべてのデータを使わないのでしょうか。データセットが大きいと、一度にはそもそもメモリに収まりません。LLM の学習データは何兆トークンもあります。それに、ミニバッチの勾配には多少ノイズがありますが、頻繁に更新できるので、1 エポックに 1 回しか更新しないより速く学べることが多いのです。各エポックの始めに順番をシャッフルするのは、どのバッチもランダムな少数のサンプルにして、ネットワークがデータの並び順に頼らないようにするためです。
オプティマイザも Adam に替えています。SGD をもとに、パラメータごとに歩幅を個別に調整します。ずっと同じ方向に進んでいるパラメータは歩幅を大きくし、行ったり来たりしているパラメータは歩幅を小さくします。第 2 課の「一つの学習率では二つの方向を面倒見られない」という問題を、自動でかなり和らげてくれます。実際の学習では、Adam とその変種 AdamW が最もよく使われる既定の選択です。
学習の結果
训练:
第 1 轮 最后一批的损失 0.1804 训练集准确率 87.8% 测试集准确率 87.6%
第 2 轮 最后一批的损失 0.0335 训练集准确率 92.7% 测试集准确率 92.4%
第 5 轮 最后一批的损失 0.3066 训练集准确率 97.6% 测试集准确率 96.7%
第 10 轮 最后一批的损失 0.0285 训练集准确率 98.7% 测试集准确率 96.0%
第 20 轮 最后一批的损失 0.0035 训练集准确率 99.7% 测试集准确率 97.3%
第 30 轮 最后一批的损失 0.0001 训练集准确率 100.0% 测试集准确率 97.8%
私のパソコンでは、スクリプト全体が 5 秒もかからずに終わりました。
見るべき点が二つあります。
一つ目に、「最後のバッチの損失」があちこちに跳んでいます。第 2 エポックでは 0.03 なのに、第 5 エポックではかえって 0.31 です。これがミニバッチのノイズです。どのバッチも 64 枚の画像しかなく(最後のバッチは 1347 - 21×64 = 3 枚しかない)、たまたま認識しにくい画像を何枚か引けば損失は高くなります。学習がうまくいっているかは、あるバッチの損失ではなく、データセット全体での指標で判断すべきです。
二つ目に、学習セットの正答率は 100% に達し、テストセットは 97.8% です。ネットワークは見たことのある画像ではすべて正解し、見たことのない画像では少し間違えています。この差は次の課で詳しく扱います。
どこを間違えているのか
正答率は一つの数にすぎません。ネットワークがどこを間違えているかを知るには、混同行列を見ます。行が本当の数字、列がネットワークの予測した数字で、対角線上が正解、それ以外の位置が不正解です。
测试集 450 张里错了 10 张。混淆矩阵(行是真实数字,列是预测数字):
0 1 2 3 4 5 6 7 8 9
0 37 . . . . . . . . .
1 . 42 . . . . . . 1 .
2 . . 44 . . . . . . .
3 . . 1 44 . . . . . .
4 . . . . 38 . . . . .
5 . . . . . 47 . . . 1
6 . 1 . . . . 51 . . .
7 . 1 . . . . . 47 . .
8 . 2 1 . . . . . 45 .
9 . . . . . 1 . 1 . 45
10 個の誤りのうち 4 個は 1 と間違えられたものです(6 と 7 が一つずつ、8 が二つ)。そのうちの一枚を描いてみます。
一张判错的图:真实是 7,模型认为是 1
..####::
::++==
**--..
==######++
++####--
##==
##..
..##
この 7 は真ん中に横棒が一本余分にあり、下半分はただの一本の縦線です。8×8 の解像度なら、1 だと言ってもそれほどおかしくありません。間違えたサンプルを見ることは、モデルを理解する最もよい方法です。モデルの問題のこともあれば、データそのものがあいまいなこともあり、ラベルが間違っていることさえあります。これはモジュール 06 で評価の失敗例を見るのと同じです。
練習問題
- 隠れ層を 64 個のニューロンから 16 個、256 個に変えると、学習セットとテストセットの正答率はそれぞれどう変わりますか。
AdamをSGDに替え、学習率は同じく 0.01 にして、30 エポック後のテストセットの正答率はいくつになりますか。学習率をいくつにすれば、SGD は Adam に追いつけますか。- 学習を始める前に、学習セット全体での損失を一度計算してください。2.30 に近いでしょうか。
確認テスト
1. 分類タスクの出力は回帰タスクとどう違いますか?softmax は何をしていますか?
回帰は連続した数を一つ出力します。分類はクラスごとにスコアを一つ出力し、softmax はそれらのスコアの指数を取って合計で割り、足して 1 になる確率に変えます。スコアが大きいクラスほど確率も大きくなります。
2. 交差エントロピーの損失はどう計算しますか?10 クラス分類のネットワークの学習開始時の損失はおよそいくつですか?
ネットワークが正解に与えた確率を取り、-log(確率) を計算します。正解の確率が 1 に近いほど損失は 0 に近づきます。学習開始時は 10 個のクラスの確率がどれも 0.1 付近なので、損失はおよそ -log(0.1) ≈ 2.30 です。
3. 「最後のバッチの損失」が上がったり下がったりするのはなぜですか?学習の効果はどう判断しますか?
どのバッチもデータのごく一部にすぎず、たまたま難しいサンプルを引けば損失が高くなり、しかも最後のバッチには数枚の画像しかありません。効果は、正答率やすべてのデータでの平均損失のような、学習セット全体とテストセットでの指標で判断すべきで、あるバッチの損失で判断してはいけません。
質問と議論
このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。
質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。
議論を読み込んでいます…