PyTorchをもう少し学ぶ

しむどん 2026-01-28

https://www.symdon.info/ja/posts/1768868240/ では、PyTorchの基本的な使い方を学びながらニューラルネットワークを構築した。その続きとして、今回もPyTorchを少しだけ学ぶ事にした。 scikit-learn にはアイリスの分類問題用のデータセット( sklearn/datasets/data/iris.csv )があるから、今回はそれを使用する。

import sklearn.datasets

iris_dataset = sklearn.datasets.load_iris()

このアイリスデータは、150件のサンプルしかない。元のCSVは以下のようになっている。

150,4,setosa,versicolor,virginica
5.1,3.5,1.4,0.2,0
4.9,3.0,1.4,0.2,0
4.7,3.2,1.3,0.2,0

〜省略〜

先頭行はヘッダとして、中にどのようなデータが含まれているのかを記述している。

ヘッダーの値 意味
150 150件のサンプル
4 4つの特徴量(萼片の長さ1、萼片の幅、花弁の長さ、花弁の幅)
setosa
versicolor
virginica

setosa, versicolor, virginica

このデータを訓練用と評価用に分ける。8対2になるように分ける事が多いようだ。

訓練用データを取得する。

training_iris_data = iris_dataset.data[:120]
training_iris_target = iris_dataset.target[:120]

評価用データを取得する。

testing_iris_data = iris_dataset.data[120:]
testing_iris_target = iris_dataset.target[120:]

訓練用のデータの先頭の1行を使用し、線形変換を行う手順を確認する。

index = 0
current_data = training_iris_data[index]
current_target = training_iris_target[index]

これらのデータをPyTorchのテンソル型に変換しておく。

input_data = torch.tensor(current_data, dtype=torch.float32)
expect_data = torch.tensor([current_target], dtype=torch.float32)

重みとバイアスを定義しておく。

weight = torch.tensor([[.4, .5, .6, .7]], dtype=torch.float32, requires_grad=True)
bias = torch.tensor([.1], dtype=torch.float32, requires_grad=True)

線形変換を実行する。

output_data = torch.nn.functional.linear(input_data, weight, bias)

損失を計算する。

loss = torch.nn.functional.mse_loss(output_data, expect_data)

計算グラフを遡り、勾配を計算する。

loss.backward()

計算した勾配を、重みとバイアスに反映する。学習率は 0.01 とする。

lr = 0.01

with torch.no_grad():
    weight -= lr * weight.grad
    bias -= lr * bias.grad

勾配の生産結果をリセットする。

weight.grad.zero_()
bias.grad.zero_()

一通りの流れが出来た。全体を実装し訓練を実行してみる。

weight = torch.tensor([[.4, .5, .6, .7]], dtype=torch.float32, requires_grad=True)
bias = torch.tensor([.1], dtype=torch.float32, requires_grad=True)

lr = 0.01

for idx, current_data in enumerate(training_iris_data):
    current_target = training_iris_target[idx]
    input_data = torch.tensor(current_data, dtype=torch.float32)
    expect_data = torch.tensor([current_target], dtype=torch.float32)
    output_data = torch.nn.functional.linear(input_data, weight, bias)
    loss = torch.nn.functional.mse_loss(output_data, expect_data)
    loss.backward()
    with torch.no_grad():
        weight -= lr * weight.grad
        bias -= lr * bias.grad
    weight.grad.zero_()
    bias.grad.zero_()

評価用データを使って、この重みとバイアスが丁度良い塩梅に調整されているかを確認する。

index = 0
current_data = testing_iris_data[index]
current_target = testing_iris_target[index]

これらのデータをPyTorchのテンソル型に変換しておく。

input_data = torch.tensor(current_data, dtype=torch.float32)
expect_data = torch.tensor([current_target], dtype=torch.float32)

推論を行う。

with torch.no_grad():
    output_data = torch.nn.functional.linear(input_data, weight, bias)

出力された output_data が期待値である expect_data に近いかを確認する。

>>> output_data
tensor([0.4197])
>>>
>>> expect_data
tensor([2.])

全然、近くない。このような単純な仕組みじゃ機能しないという事なのか。

ChatGPTに聞いてみると、以下の回答が返ってきた。

- 非線形性の欠如: linear 層だけだと入力と出力の関係が線形でしか表現できず、3クラスを分ける境界を作れません。
- 出力形状: 多クラス分類には通常、出力をクラス数と同じ次元のベクトルにして、ソフトマックスなどの非線形性と組み合わせて判定します。
- 損失関数: 現在は MSE を使っていますが、分類には CrossEntropyLoss のような分類向け損失を使うのが一般的です。

ふむ、いろいろ足りていないようだ。考えてみると確かにいろいろとダメな所がある。

  1. データセットの先頭120件を訓練用データとして使ってしまっているため、訓練用データや評価用データに偏りがある。 CSVデータは先頭からsetosa、versicolor、virginicaで並んでいるため、評価用のデータはvirginicaばかりになっている。
  2. データを標準化していないため、学習を安定させられない。 勾配降下法を用いるニューラルネットでは、異なるスケールの特徴が混ざると学習が不安定になりがち。
  3. 三値のクラス分類は、1層の線形層だけでは表現力不足で上手くクラス分けできない。 通常は非線形層を導入する。
  4. 三値のクラス分類を判断する際の出力が1次元であるため、損失をうまく計算できていない。 三値のクラス分類では、出力は3次元のベクトルにするのが良さそう。
  5. 分類タスクは、交差エントロピー損失(CrossEntropyLoss)を使うのが望ましい。 平均二乗誤差では平均からの差を二乗したものを扱うため、結果は確率分布に近付かない。 交差エントロピー損失は、そのデメリットがない。

それでは、このダメな所を直していこう。

まずは、訓練用データや評価用データに偏りがないように、それぞれのデータを作ろう。 つまりデータを分割する際に「各クラスの割合」を保ったまま訓練データと評価データに分ける。 これは層化分割(stratified split)と呼ばれる。 sklearn.model_selection.train_test_split() は、この層化分割が実装されているため、ここではこれを使用する。

import sklearn.datasets
import sklearn.model_selection

iris = sklearn.datasets.load_iris()
X, y = iris.data, iris.target

X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(
  X, y, test_size=0.2, random_state=42, stratify=y)  # 層化分割

つぎにデータを標準化する。ここではZスコア正規化を行う。Zスコア正規化は平均が0、分散が1の範囲になるように、データのスケールを揃える事ができる。 Zスコア正規化の処理は sklearn.preprocessing.StandardScaler() で実装されているため、ここではこれを使う。

import sklearn.preprocessing

scaler = sklearn.preprocessing.StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

各値をテンソル型に変換し、PyTorchで使用できるようにする。

import torch

X_train_t = torch.tensor(X_train, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.long)
X_test_t  = torch.tensor(X_test, dtype=torch.float32)
y_test_t  = torch.tensor(y_test, dtype=torch.long)

各層で使用する重みとバイアスを用意する。また学習率も定義しておく。重みとバイアスは中間層の出力が非葉テンソルになってしまわないように、 torch.nn.parameter.Parameter() でパラメータ化しておく。

in_dim, hidden_dim, out_dim = 4, 8, 3  # 初期パラメータ(4->8->3)

import torch.nn.parameter

W1 = torch.nn.parameter.Parameter(
    torch.randn(hidden_dim, in_dim, requires_grad=True) * 0.01)

b1 = torch.nn.parameter.Parameter(
    torch.zeros(hidden_dim, requires_grad=True))

W2 = torch.nn.parameter.Parameter(
    torch.randn(out_dim, hidden_dim, requires_grad=True) * 0.01)

b2 = torch.nn.parameter.Parameter(
    torch.zeros(out_dim, requires_grad=True))

lr = 0.01  # 学習率

単発オンライン学習を想定して処理の流れを確認していく。 訓練用データから入力用と出力として期待するデータを、それぞれ1つずつだけ取り出しておく。

i = 0

xb = X_train_t[i].view(1, -1)  # (1, in_dim)
yb = y_train_t[i].view(1)      # (1,)

順伝播を計算する。

z1 = xb @ W1.t() + b1  # (1, hidden_dim)
a1 = torch.nn.functional.relu(z1)
logits = a1 @ W2.t() + b2  # (1, out_dim)

交差エントロピー損失を計算する。

loss = torch.nn.functional.cross_entropy(logits, yb)

勾配を計算する。

loss.backward()

勾配降下法で重みとバイアスを更新する。

with torch.no_grad():
    W1 -= lr * W1.grad
    b1 -= lr * b1.grad
    W2 -= lr * W2.grad
    b2 -= lr * b2.grad

勾配をリセットする。

W1.grad.zero_()
b1.grad.zero_()
W2.grad.zero_()
b2.grad.zero_()

これをデータ数分繰り返す。

実際にやってみたが、訓練データを1巡しただけ(つまりエポック=1)では、あまり正答率は良くなかった。通常、オンライン学習ではエポックを回して安定させるのが一般的なので、エポック数を50として、50回繰り返し訓練する事にした。また各エポックでデータの順序をシャッフルしておくと、偏りに引きずられにくい。 また学習率も調整して 0.1 にした。

できたコードの全体を掲載する。

import sklearn.datasets
import sklearn.model_selection

iris = sklearn.datasets.load_iris()
X, y = iris.data, iris.target

X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(
  X, y, test_size=0.2, random_state=42, stratify=y)  # 層化分割

import sklearn.preprocessing

scaler = sklearn.preprocessing.StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

import torch

X_train_t = torch.tensor(X_train, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.long)
X_test_t  = torch.tensor(X_test, dtype=torch.float32)
y_test_t  = torch.tensor(y_test, dtype=torch.long)

in_dim, hidden_dim, out_dim = 4, 8, 3  # 初期パラメータ(4->8->3)

import torch.nn.parameter

W1 = torch.nn.parameter.Parameter(
    torch.randn(hidden_dim, in_dim, requires_grad=True) * 0.01)

b1 = torch.nn.parameter.Parameter(
    torch.zeros(hidden_dim, requires_grad=True))

W2 = torch.nn.parameter.Parameter(
    torch.randn(out_dim, hidden_dim, requires_grad=True) * 0.01)

b2 = torch.nn.parameter.Parameter(
    torch.zeros(out_dim, requires_grad=True))

lr = 0.1  # 学習率

for batch_count in range(50):
    perm = torch.randperm(X_train_t.size(0))
    for i in perm.tolist():
        xb = X_train_t[i].view(1, -1)  # (1, in_dim)
        yb = y_train_t[i].view(1)      # (1,)

        z1 = xb @ W1.t() + b1  # (1, hidden_dim)
        a1 = torch.nn.functional.relu(z1)
        logits = a1 @ W2.t() + b2  # (1, out_dim)

        loss = torch.nn.functional.cross_entropy(logits, yb)

        loss.backward()

        with torch.no_grad():
            W1 -= lr * W1.grad
            b1 -= lr * b1.grad
            W2 -= lr * W2.grad
            b2 -= lr * b2.grad

        # 勾配クリア
        W1.grad.zero_()
        b1.grad.zero_()
        W2.grad.zero_()
        b2.grad.zero_()


# 評価
correct_count = 0
with torch.no_grad():
    for i in range(X_test_t.size(0)):
        xb = X_test_t[i].view(1, -1)  # (1, in_dim)
        yb = y_test_t[i].view(1)      # (1,)

        z1 = xb @ W1.t() + b1  # (1, hidden_dim)
        a1 = torch.nn.functional.relu(z1)
        logits = a1 @ W2.t() + b2  # (1, out_dim)

        pred = logits.argmax(dim=1)
        if int(yb[0]) == int(pred[0]):
            correct_count += 1

correct_rate = correct_count / X_test_t.size(0)
print(f"correct rate = {correct_rate} ({correct_count} / {X_test_t.size(0)})")

これだと正答率はほぼ100%になった。


1

萼片 :: 花の外側の葉っぱみたいな所