モジュール 08 · 第 5 回

手書き数字を認識する

最初の本物の分類タスクです。ネットワークに 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 で評価の失敗例を見るのと同じです。

練習問題

  1. 隠れ層を 64 個のニューロンから 16 個、256 個に変えると、学習セットとテストセットの正答率はそれぞれどう変わりますか。
  2. AdamSGD に替え、学習率は同じく 0.01 にして、30 エポック後のテストセットの正答率はいくつになりますか。学習率をいくつにすれば、SGD は Adam に追いつけますか。
  3. 学習を始める前に、学習セット全体での損失を一度計算してください。2.30 に近いでしょうか。

確認テスト

1. 分類タスクの出力は回帰タスクとどう違いますか?softmax は何をしていますか?

回帰は連続した数を一つ出力します。分類はクラスごとにスコアを一つ出力し、softmax はそれらのスコアの指数を取って合計で割り、足して 1 になる確率に変えます。スコアが大きいクラスほど確率も大きくなります。

2. 交差エントロピーの損失はどう計算しますか?10 クラス分類のネットワークの学習開始時の損失はおよそいくつですか?

ネットワークが正解に与えた確率を取り、-log(確率) を計算します。正解の確率が 1 に近いほど損失は 0 に近づきます。学習開始時は 10 個のクラスの確率がどれも 0.1 付近なので、損失はおよそ -log(0.1) ≈ 2.30 です。

3. 「最後のバッチの損失」が上がったり下がったりするのはなぜですか?学習の効果はどう判断しますか?

どのバッチもデータのごく一部にすぎず、たまたま難しいサンプルを引けば損失が高くなり、しかも最後のバッチには数枚の画像しかありません。効果は、正答率やすべてのデータでの平均損失のような、学習セット全体とテストセットでの指標で判断すべきで、あるバッチの損失で判断してはいけません。

質問と議論

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

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

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