1
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?

【AI超入門④-⑤】PyTorchでニューラルネットワークを作る データの準備から学習・評価まで

1
Last updated at Posted at 2026-11-03

最初に

前回は、勾配降下法について解説しました

損失関数で予測と正解のズレを測り、誤差逆伝播法で勾配を計算し、重みやバイアスを更新する
最後には、小さなニューラルネットワークも動かしました

前回の記事はこちら

今回は、その続きを見ていきます

前回は、学習に使っていない入力を2件だけ渡して、予測を確認しました
ただ、実際にモデルを作るときには、もう少し丁寧に確認したいところです

「学習に使ったデータ以外でも、うまく予測できているのか?」

この点を確かめるために、データの準備から学習・評価までを順番に進めます
今回は、Pythonの基本的な変数・繰り返し・関数を知っている方を想定しています

今回作るもの

前回と同じく、1つの数値から別の数値を予測する、回帰のニューラルネットワークを作ります

題材は「入力を2倍して1を足す」という関係です
今回は、正解の値に少しだけランダムなばらつきを加えます

正解の値 = 入力 × 2 + 1 + 小さなばらつき

同じような入力でも、観測される値が少しずれる状況を、人工的なデータで表現します

項目 前回 今回
データ 手で用意した5件 プログラムで作る300件
正解の値 すべて式どおり ランダムなばらつきがある
学習 5件すべてを使って毎回更新 最大32件ずつ取り出して更新
評価 新しい入力を2件確認 検証用・テスト用のデータを分ける

この単純な関係には、線形回帰などのモデルも使えます
今回は、前回のコードを土台に、ニューラルネットワークを作る一連の手順を理解することが目的です

作成の全体像

まず、今回の流れを確認します

モデルを作る作業には、学習処理だけでなく、データの分け方や、学習後の確かめ方も含まれます

1 入力と正解を用意する

教師あり学習では、「入力」と、それに対応する「正解」を1組にして用意します

例えば、住宅の条件から家賃を予測するなら、部屋の広さなどが入力、実際の家賃が正解になります
今回は仕組みを追いやすいように、入力も正解も1個の数値にします

x = torch.rand(300, 1, generator=data_generator, dtype=torch.float32)
noise = 0.1 * torch.randn(
    300, 1, generator=data_generator, dtype=torch.float32
)
y = 2 * x + 1 + noise

※ 途中に出てくるコードは説明用の抜粋です
実行するときは、後半のコード全体を使用してください

xは0以上1未満の入力を300件、noiseは平均0・標準偏差0.1の正規分布から作るばらつきです
0.1はばらつきの上限ではありません

入力と正解は、どちらも次の形になります

(300, 1)
   │  └─ 1件につき数値が1個
   └──── データが300件

xの1行目にはyの1行目が対応する、という関係が大切です

正解を作るための式はデータの準備に使います
その後、モデルは入力と正解の組を使って学習し、予測時には入力だけを受け取ります

数値の型と形をそろえる

今回の入力と正解は、torch.float32という小数を扱える型にそろえます
また、モデルの出力と正解の形を、どちらも「件数 × 1」にします

例えば32件を処理するときは、次の対応になります

入力の形   : (32, 1)
予測の形   : (32, 1)
正解の形   : (32, 1)

正解だけを(32,)にすると、自動的な形の拡張によって、意図した1対1の比較にならない場合があります
MSEを計算する前に、予測と正解の形を確認しましょう

2 データを学習用・検証用・テスト用に分ける

ここが、前回から大きく変わるところです

練習した問題で高得点を取れても、初めて見る問題で同じように解けるとは限りません
モデルについても、重みの更新に使うデータと、性能を確かめるデータを分けて考えます

今回は、300件を次のように分けます

データ 件数 役割
学習用データ 180件 勾配を計算し、重みやバイアスを更新する
検証用データ 60件 学習途中の状態を比較し、採用するモデルを選ぶ
テスト用データ 60件 モデルを選び終えてから、最終的な性能を測る

6:2:2は今回の例であり、どの問題でもこの比率が正解というわけではありません

indices = torch.randperm(300, generator=data_generator)
train_idx = indices[:180]
val_idx = indices[180:240]
test_idx = indices[240:]

torch.randpermで、0〜299の番号をランダムな順番に並べます
その番号を3つに分け、入力と正解に同じ番号を使って取り出します

x_train, y_train = x[train_idx], y[train_idx]

入力と正解を別々にシャッフルすると、組み合わせが壊れるため注意しましょう

今回のように独立に生成したデータではランダムに分割できます
ただし、実データでは同じ人の記録や連続した時系列など、データ同士の関係に応じて分け方も考える必要があります

検証用とテスト用を分ける理由

検証用データでは、例えば「50エポック目と100エポック目では、どちらのモデルがよいか」を比較します

一方、テスト結果を見て何度もモデルを調整すると、テストデータの特徴に合わせて選ぶことになります
そのため、試行錯誤には検証用データを使い、テスト用データは最後の確認に残します

検証用の正解も評価に使いますが、その損失でbackward()やoptimizer.step()は実行しません
検証結果はモデル選択に使われる、という区別です

3 ミニバッチでデータを取り出す

前回は、5件すべてをまとめて使い、1回ずつ重みを更新しました

今回は学習用の180件を、最大32件ずつ取り出します
この小さなまとまりをミニバッチと呼びます

train_loader = DataLoader(
    TensorDataset(x_train, y_train),
    batch_size=32,
    shuffle=True,
    generator=torch.Generator().manual_seed(1),
    num_workers=0,
)

TensorDatasetは入力と正解を組にして扱うためのものです
DataLoaderは、そこから指定した件数ずつ取り出す役割を持ちます

shuffle=Trueにすると、学習用データを取り出す順番を毎周入れ替えます
学習用・検証用・テスト用の所属を入れ替える設定ではありません
num_workers=0では、データの読み込みに別のプロセスを起動しません

バッチサイズとエポックの違い

用語 意味 今回の設定
バッチサイズ 1回の更新に使うデータの件数 最大32件
エポック 学習用データ全体を1周する単位 200周する

今回の設定では、1エポックが次のようになります

32件 → 更新
32件 → 更新
32件 → 更新
32件 → 更新
32件 → 更新
20件 → 更新
─────────────
合計180件で1エポック

最後の20件も使うので、1エポックで6回更新します
200エポックなら合計1,200回です

「200エポック」と「重みを200回更新すること」は、このコードでは異なります

4 モデルと学習の設定を決める

モデルの構成は、前回と同じです

入力1個 → 隠れ層1層(ニューロン8個・ReLU) → 出力1個
model = nn.Sequential(
    nn.Linear(1, 8),
    nn.ReLU(),
    nn.Linear(8, 1),
).to(device)

loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

nn.Linear(1, 8)の「1」は1件あたりの入力数、「8」は隠れ層のニューロン数です
データの件数やバッチサイズを表しているわけではありません

数値の予測なので、損失関数には前回と同じ平均二乗誤差(MSE)を使います
更新方法はSGD、学習率は0.05とします

今回はDataLoaderでミニバッチを取り出すので、ミニバッチごとに勾配を計算して更新する学習になります

5 学習と検証を繰り返す

学習中の基本的な処理は、前回と共通です

model.train()
for x_batch, y_batch in train_loader:
    x_batch = x_batch.to(device)
    y_batch = y_batch.to(device)
    optimizer.zero_grad()
    prediction = model(x_batch)
    loss = loss_fn(prediction, y_batch)
    loss.backward()
    optimizer.step()

違いは、学習用データ全体の代わりに、取り出したミニバッチを渡すことです

ミニバッチを取り出す
    ↓
予測 → 損失計算 → 勾配計算 → 重みの更新
    ↓
次のミニバッチへ

学習用データを1周したら、その時点のモデルで学習用・検証用それぞれのMSEを計算します
表示する学習MSEは、エポック終了時のモデルで学習用180件を評価し直した値です

評価中は重みを更新しない

評価処理では、次の形を使います

model.eval()
with torch.no_grad():
    prediction = model(x.to(device))

model.eval()は評価モードへの切り替え、torch.no_grad()は勾配計算用の履歴を記録しない設定です
2つは別の役割を持ちます
今回のモデルはLinearとReLUだけなのでモードによる層の動作の違いはありませんが、学習時と評価時を切り替える形にします

評価では予測と正解を比較するだけです
重みの更新は行いません

検証結果がよかったモデルを控えておく

今回は、検証MSEがそれまでの最小値を更新したときに、その時点の重みをコピーします

if val_mse < best_val_mse:
    best_val_mse = val_mse
    best_state = deepcopy(model.state_dict())
    best_epoch = epoch

state_dict()からモデルの重みなどの状態を取り出し、deepcopyで独立したコピーを作ります
単にmodel.state_dict()を変数へ代入するだけでは、その後の更新も反映されてしまいます

学習後は、控えておいた状態を読み戻してからテストします

model.load_state_dict(best_state)

ここでは200エポックすべてを実行してからモデルを選びます
途中で学習を打ち切る処理ではありません

6 テスト用データで評価する

最終評価では、MSEに加えて 平均絶対誤差(MAE) も確認します

指標 計算方法 読み取り方
MSE 予測と正解の差を二乗して平均する 大きなずれが強く反映される
MAE 予測と正解の差の絶対値を平均する 元の数値の単位で、平均的なずれを読める

どちらも、この例では小さいほど予測が正解に近い指標です

例えばMAEが0.07なら、テストデータでは予測と正解の差の絶対値が平均約0.07だった、という意味です
正解率が93%という意味ではありません

単純な予測方法とも比べる

MSEだけを見ても、その値が十分小さいのかは判断しにくいことがあります

そこで今回は、「どんな入力にも、学習用の正解の平均値を返す」という単純な予測と比べます
このような比較の基準をベースラインと呼びます

baseline = y_train.mean()
baseline_mse = (baseline - y_test).square().mean().item()

平均値を計算するのは、学習用の正解だけです
ニューラルネットワークとベースラインを、同じテスト用データ・同じMSEで評価します

実際に動かしてみる

実行環境

次の環境で、掲載コードのGPU実行とCPU実行を確認しています

項目 確認した環境
Python 3.10.6
PyTorch 2.10.0+cu130
GPU NVIDIA GeForce RTX 3090

前回のPyTorch環境をそのまま使えます
追加のライブラリや、外部データのダウンロードは必要ありません

PyTorchをこれから導入する場合は、公式のインストール案内でOSやGPUに合う方法を選んでください
前回と同じ2.10.0の導入方法は、過去バージョンの案内で確認できます

コードはCUDAを利用できる場合にGPUを使い、利用できない場合はCPUを使います

コード全体

以下をneural_network_workflow.pyとして保存してください

"""データの準備から学習・評価までを確認するPyTorchの入門例。"""

from copy import deepcopy
import platform

import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset


def evaluate(model, x, y, device):
    """重みを更新せず、指定したデータ全体のMSEとMAEを計算する。"""
    model.eval()
    with torch.no_grad():
        prediction = model(x.to(device))
        error = prediction - y.to(device)
        mse = error.square().mean().item()
        mae = error.abs().mean().item()
    return mse, mae


def main():
    # 1. 実行環境と初期値を設定する
    torch.manual_seed(0)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"Python: {platform.python_version()}")
    print(f"PyTorch: {torch.__version__}")
    print(f"使用デバイス: {device}")
    if device.type == "cuda":
        print(f"GPU: {torch.cuda.get_device_name(device)}")

    # 2. 入力と正解を300件作る。データ作成用の乱数は独立させる
    data_generator = torch.Generator().manual_seed(0)
    x = torch.rand(300, 1, generator=data_generator, dtype=torch.float32)
    noise = 0.1 * torch.randn(
        300, 1, generator=data_generator, dtype=torch.float32
    )
    y = 2 * x + 1 + noise

    # 3. 同じインデックスで、入力と正解を一緒に分割する
    indices = torch.randperm(300, generator=data_generator)
    train_idx = indices[:180]
    val_idx = indices[180:240]
    test_idx = indices[240:]
    x_train, y_train = x[train_idx], y[train_idx]
    x_val, y_val = x[val_idx], y[val_idx]
    x_test, y_test = x[test_idx], y[test_idx]
    print(f"件数: 学習={len(x_train)}, 検証={len(x_val)}, テスト={len(x_test)}")
    print(f"学習用の形状: 入力={tuple(x_train.shape)}, 正解={tuple(y_train.shape)}")

    # 4. CPU上のデータを、最大32件ずつ取り出せるようにする
    train_loader = DataLoader(
        TensorDataset(x_train, y_train),
        batch_size=32,
        shuffle=True,
        generator=torch.Generator().manual_seed(1),
        num_workers=0,
    )

    # 5. 前回と同じ、小さなニューラルネットワークを作る
    model = nn.Sequential(
        nn.Linear(1, 8),
        nn.ReLU(),
        nn.Linear(8, 1),
    ).to(device)
    loss_fn = nn.MSELoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

    initial_mse, _ = evaluate(model, x_train, y_train, device)
    print(f"\n学習前の学習データMSE: {initial_mse:.6f}")

    # 6. 学習し、検証用データで最も良かった重みを控える
    best_val_mse = float("inf")
    best_state = None
    best_epoch = 0
    epochs = 200

    for epoch in range(1, epochs + 1):
        model.train()
        for x_batch, y_batch in train_loader:
            x_batch = x_batch.to(device)
            y_batch = y_batch.to(device)
            optimizer.zero_grad()
            prediction = model(x_batch)
            loss = loss_fn(prediction, y_batch)
            loss.backward()
            optimizer.step()

        # 両方とも、そのエポック終了時点のモデルで測る
        train_mse, _ = evaluate(model, x_train, y_train, device)
        val_mse, _ = evaluate(model, x_val, y_val, device)

        if val_mse < best_val_mse:
            best_val_mse = val_mse
            best_state = deepcopy(model.state_dict())
            best_epoch = epoch

        if epoch == 1 or epoch % 50 == 0:
            print(
                f"Epoch {epoch:3d}: "
                f"学習MSE={train_mse:.6f}, 検証MSE={val_mse:.6f}"
            )

    # 7. 検証で選んだモデルに戻し、最後にテスト用データで評価する
    model.load_state_dict(best_state)
    test_mse, test_mae = evaluate(model, x_test, y_test, device)
    print(f"\n採用したエポック: {best_epoch}")
    print(f"採用時の検証MSE: {best_val_mse:.6f}")
    print(f"テストMSE: {test_mse:.6f}")
    print(f"テストMAE: {test_mae:.6f}")

    # 学習用の正解の平均だけを返す、単純な予測とも比較する
    baseline = y_train.mean()
    baseline_mse = (baseline - y_test).square().mean().item()
    print(f"平均値だけで予測した場合のテストMSE: {baseline_mse:.6f}")

    # 8. 入力だけを渡し、学習済みモデルで予測する
    x_new = torch.tensor([[0.2], [0.8]], dtype=torch.float32, device=device)
    model.eval()
    with torch.no_grad():
        predictions = model(x_new).cpu().flatten().tolist()
    for value, predicted in zip([0.2, 0.8], predictions):
        print(f"新しい入力: {value:.1f}, 予測: {predicted:.3f}")


if __name__ == "__main__":
    main()

torch.manual_seedやtorch.Generator().manual_seed(...)は、乱数の初期値を指定する処理です
今回はモデルの初期化、データの作成・分割、学習時の並び順を比較しやすくしています
ただし、実行環境やPyTorchのバージョンが違う場合に、結果の完全な一致を保証するものではありません

また、この小さな例では評価用のデータをまとめてデバイスに移しています
大量のデータを扱う場合には、評価もミニバッチに分けて計算します

実行方法

ファイルを保存したフォルダで、次のコマンドを実行します

python neural_network_workflow.py

実行結果

以下は、上記環境のGPUで取得した実行結果です

Python: 3.10.6
PyTorch: 2.10.0+cu130
使用デバイス: cuda
GPU: NVIDIA GeForce RTX 3090
件数: 学習=180, 検証=60, テスト=60
学習用の形状: 入力=(180, 1), 正解=(180, 1)

学習前の学習データMSE: 3.421679
Epoch   1: 学習MSE=0.175833, 検証MSE=0.210497
Epoch  50: 学習MSE=0.012736, 検証MSE=0.012516
Epoch 100: 学習MSE=0.012916, 検証MSE=0.012815
Epoch 150: 学習MSE=0.012752, 検証MSE=0.012504
Epoch 200: 学習MSE=0.012830, 検証MSE=0.012513

採用したエポック: 87
採用時の検証MSE: 0.012477
テストMSE: 0.008248
テストMAE: 0.070116
平均値だけで予測した場合のテストMSE: 0.325076
新しい入力: 0.2, 予測: 1.395
新しい入力: 0.8, 予測: 2.613

最初に、使用デバイスとデータ件数を確認します
cpuと表示された場合は、CPUで処理しています

学習前の学習データMSEは約3.42でしたが、学習後は約0.013まで小さくなっています
検証MSEも小さくなり、この実行では87エポック目の状態が選ばれました

200エポック目ではなく、検証用データで最もよかった87エポック目を使って、テストしています

テストMSEは約0.0082で、平均値だけを返すベースラインの約0.3251より小さくなりました
このデータでは、入力に応じて予測を変えるモデルが、一定値を返す方法よりよい結果になっています

テストMAEは約0.0701なので、テスト用60件に対する平均的なずれの大きさは約0.07と読めます

最後の2件は、新しい入力だけを渡した予測例です
ばらつきの中心となる関係では、0.2に対して1.4、0.8に対して2.6になります
予測も、その近くの値になっています
ただし、実際の正解データにはランダムなばらつきが含まれます

学習結果を見るときのポイント

損失が0にならなくても失敗とは限らない

今回の正解には、入力だけからは分からないランダムなばらつきを加えています
新しいデータのばらつきまで、毎回ぴったり当てることはできません

そのため、学習用データの損失を0に近づけることだけを目標にせず、検証用データでもよい予測ができているかを見ます

学習用データの細かな特徴に合わせすぎ、別のデータで予測が悪くなることを過学習と呼びます

学習と検証の両方を見る

学習の進み方は、おおむね次のように読み取れます

観察した変化 考えられる状態
学習MSEも検証MSEも下がる 学習に使わないデータでも予測が改善している
学習MSEは下がるが、検証MSEは悪化し続ける 過学習の可能性がある
どちらも高いまま 学習の設定やデータ、モデルを確認する必要がある

ミニバッチ学習では数値が上下することもあるので、1回の変化だけで判断せず、複数エポックにわたる傾向を見ます

今回の実行結果は、検証MSEがおおむね横ばいになった例です
過学習がはっきり進行する様子を示したものではありません

テスト結果から言える範囲を考える

今回確かめたのは、同じ作り方・同じ入力範囲のデータに対する性能です
テストMSEが検証MSEより小さいのも、それぞれ異なる60件を使っているため起こり得ます

これだけで、0〜1の範囲を大きく外れた入力や、別の規則で作られたデータでもうまく予測できると判断することはできません
使う場面を考えながら、評価するデータを用意することが大切です

実際のデータを使うときの前処理

今回は、欠けた値がなく、入力も0〜1の範囲にあるデータを作りました
実際のデータでは、空欄の扱いや、数値の大きさをそろえる前処理なども必要になります

例えば標準化では、平均と標準偏差を使って数値の尺度をそろえます
この平均や標準偏差は学習用データだけで求め、検証用・テスト用にも同じ値を使います

評価用データの情報まで使って前処理を決めると、評価が実力以上によく見える原因になります
こうした情報の混入を、データリークと呼びます

まずデータを分け、学習用データから前処理の基準を決める
この順番も、モデル作成の大事な部分です

最後に

今回は、前回の小さなニューラルネットワークを使いながら、データの準備から学習・評価まで進めました

入力と正解をそろえ、データを用途ごとに分け、ミニバッチで学習する
そして、検証結果からモデルを選び、最後にテスト用データで確かめる

ニューラルネットワークを作るときは、学習を動かすことと同じくらい、結果をどう確かめるかが大切です

これで、これまで登場したデータ、ニューラルネットワーク、活性化関数、損失関数、勾配降下法が、実際の作業としてつながりました

次回は、学習用データではうまくいくのに、別のデータでは予測が悪くなる「過学習」を、学習曲線を見ながらもう少し詳しく確認していく予定です
またゆっくり記事を書きます

本記事では、構成の整理・サンプルコードの作成に生成AIを使用しています

株式会社ONE WEDGE

https://onewedge.co.jp/
_______
株式会社ONE WEDGEは、Webシステム開発・SES・AI/DX支援を行うIT企業です。
生成AIを活用した業務効率化や次世代システム開発にも注力しており、企業の課題解決だけでなく、エンジニア一人ひとりの成長にも本気で向き合っています。
また、技術は「一人で学ぶもの」ではなく、「仲間と成長するもの」だと考え、社内外でのコミュニティづくりにも力を入れています。
_______ 

1
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
1
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?