0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

486SX 20MHzでバックプロパゲーションを回していた人間が、30年後にPyTorchを触ってみた

0
Posted at

はじめに

最近、2D CNNの実装を複数の言語で比較する記事を書こうと思い立った。
その準備としてPyTorchを触り始めたのだが、
「あ、これ30年前にCで書いたやつだ」という感覚があった。

学生時代、ニューラルネットの本を片手に
バックプロパゲーション法をC言語でゴリゴリ実装した経験がある。
当時と今で、何がどう変わったのか。
エッセイとして書き残しておこうと思う。


当時のBP法、何が大変だったか

実装したのは大学時代、今から約30年前。
参考にしたのはニューラルネットの入門書で、
サンプルコードを見ながらC言語で写経+改造した。

大変だったのは主に3つ。

活性化関数の微分(シグモイド)

自動微分などという概念はなく、
活性化関数の微分を自分で計算してコードに書く必要があった。

例えばシグモイド関数:

sigmoid(x) = 1 / (1 + e^(-x))

その微分:

sigmoid'(x) = sigmoid(x) × (1 - sigmoid(x))

これを連鎖律で層をさかのぼりながら
何重にも適用してパラメータを更新していく。
数式とコードを行き来しながら書くのは、なかなか骨が折れた。

行列演算のforループ

今ならNumPyやPyTorchで一行で書ける行列積も、
当時はC言語で2次元配列をループで回していた。

// こんな感じのコードをひたすら書いていた
for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
        result[i][j] = 0;
        for (int k = 0; k < inner; k++) {
            result[i][j] += a[i][k] * b[k][j];
        }
    }
}

これが層の数だけ出てくる。
インデックスのi, j, kを取り違えるバグが頻発して、
デバッグに時間を溶かした記憶がある。

ハードウェアの制約

動作環境はEPSON PC-486SX 20MHz。
入力ノード5個・出力ノード3個程度の極小ネットワークでも、
学習完了まで約2時間かかった。
「とりあえず動かして、寝て、朝結果を確認する」という時代だった。


30年後、PyTorchを触ってみた

環境構築

まず環境構築から。
DevContainerを使えばワンクリックで環境が揃う。
当時はコンパイラの設定だけで一苦労だったのに、と思わずにはいられない。

.devcontainer/devcontainer.json:

{
  "name": "PyTorch CPU",
  "image": "mcr.microsoft.com/devcontainers/python:3.11",
  "postCreateCommand": "pip install -r requirements.txt",
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.vscode-pylance"
      ]
    }
  }
}

requirements.txt:

# PyTorch CPU版
torch==2.3.0+cpu
torchvision==0.18.0+cpu
--extra-index-url https://download.pytorch.org/whl/cpu

コード

今回はMNIST(手書き数字)をCNNで学習してみた。

import time
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# -----------------------------------------------
# 1. データの準備
#    当時は手動でデータを読み込んでいたが、
#    今はダウンロードから前処理まで自動でやってくれる
# -----------------------------------------------
transform = transforms.Compose([
    transforms.ToTensor(),                # 画像を0〜1のテンソルに変換
    transforms.Normalize((0.5,), (0.5,))  # 正規化(-1〜1に収める)
])

train_dataset = datasets.MNIST(
    root='./data', train=True, download=True, transform=transform
)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)

test_dataset = datasets.MNIST(
    root='./data', train=False, download=True, transform=transform
)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

# -----------------------------------------------
# 2. CNNモデルの定義
#    当時は全結合+手書きの行列演算だったが、
#    今は「層を積み重ねる」だけで書ける
# -----------------------------------------------
class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Sequential(
            # 畳み込み層1:特徴を抽出する
            nn.Conv2d(1, 16, kernel_size=3, padding=1),
            nn.ReLU(),       # 活性化関数(当時はシグモイドを手計算していた)
            nn.MaxPool2d(2), # 28x28 → 14x14

            # 畳み込み層2:より複雑な特徴を抽出
            nn.Conv2d(16, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2), # 14x14 → 7x7
        )
        self.fc = nn.Sequential(
            # 全結合層:当時のBP法に一番近い部分
            nn.Flatten(),
            nn.Linear(32 * 7 * 7, 128),
            nn.ReLU(),
            nn.Linear(128, 10)  # 出力:0〜9の10クラス
        )

    def forward(self, x):
        x = self.conv(x)
        x = self.fc(x)
        return x

model = SimpleCNN()

# -----------------------------------------------
# 3. 損失関数とオプティマイザの設定
# -----------------------------------------------
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# -----------------------------------------------
# 4. 学習ループ
#    当時は手書きの勾配計算・パラメータ更新だったが、
#    今は3行で終わる
# -----------------------------------------------
print("学習開始")
start = time.time()

for epoch in range(5):
    total_loss = 0
    for images, labels in train_loader:
        optimizer.zero_grad()         # 勾配をリセット
        outputs = model(images)       # 順伝播
        loss = criterion(outputs, labels)
        loss.backward()               # 逆伝播(勾配を自動計算!)
        optimizer.step()              # パラメータを更新
        total_loss += loss.item()

    print(f"Epoch {epoch+1}/5: Loss = {total_loss / len(train_loader):.4f}")

elapsed = time.time() - start
print(f"\n学習時間: {elapsed:.1f}")
print(f"(参考:486SX 20MHz時代は同等処理に約2時間かかっていた)")

# -----------------------------------------------
# 5. 評価
# -----------------------------------------------
model.eval()
correct = 0
total = 0

with torch.no_grad():
    for images, labels in test_loader:
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print(f"\nテスト精度: {100 * correct / total:.2f}%")

実行結果

実行環境:Ryzen 5 7530U(CPUのみ)

学習時間  : 99秒
テスト精度 : 99.03%

正直、衝撃だった。

自動微分(autograd)
あの手計算していたシグモイドの微分も、
連鎖律の展開も、全部やってくれる。
loss.backward() の一行で。

行列演算
3重ループで書いていた行列積が:

result = torch.matmul(a, b)

これだけ。

コード量
当時は数百行書いていた処理が、数十行で済む。
しかも読みやすい。


何が変わって、何が変わっていないか

変わったもの:

項目 当時(486SX時代) 現代(PyTorch)
勾配計算 手書き 自動微分(autograd)
行列演算 forループ 1行
コード量 数百行 数十行
学習時間 極小ネットワークで2時間 CNNで99秒
精度 夢のような数字 99.03%

変わっていないもの:

  • 行列演算が本質であること
  • シグモイドなどの活性化関数で非線形性を出すこと
  • 連鎖律でパラメータを更新する仕組み
  • 「ネットワーク設計 → 学習 → 評価」の流れ

ライブラリが隠蔽しているだけで、
PyTorchの裏側では30年前とやっていることは同じだ。
むしろ当時ゴリゴリ書いた経験があると、
「autogradが何を肩代わりしてくれているか」が
肌感覚でわかるという副産物がある。

手書き数字の認識を99%の精度で、CPUのみで99秒。
当時の極小ネットワークが2時間かかっていたことを思えば、隔世の感がある。
しかもこの精度、当時は夢のような数字だった。

ハードウェアの進化とは、恐ろしいものだ。


おわりに・次回予告

今回はエッセイとして書いたが、
次回からは実際にC++(libtorch)やONNX Runtimeでの
推論実装と比較をしていく予定。

学習はPythonで統一し、推論部分を各言語で比較する。
30年前はCで全部書いていたのに、
2026年にC++で書くとどうなるのか、自分でも楽しみだ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?