(43)【PyTorch】Sin関数を近似するモデルを作る。(高機能版)

投稿者: | 2025年5月16日

751 views

この記事は最終更新から 392日 が経過しています。

やりたいこと

前回の投稿 (42)【PyTorch】Sin関数を近似するモデルを作る。 で作ったプログラムに、以下の機能を追加したい。

1) 損失関数出力値を記録し、グラフ表示したい。
2) コマンドライン引数で、エポック数を指定したい。
3) コマンドライン引数で、学習率の初期値を指定したい。
4) コマンドライン引数で、隠れ層のニューロン数を指定したい。
5) コマンドライン引数で、勾配降下法の最適化アルゴリズム種別を指定したい。

今回は、上記の 5機能を追加実装することにした。

やってみる

1) プログラムソースコード

コマンドライン引数の仕様は下記の通り。

option用途データ型初期値
-eエポック数int1000
-r学習率float0.01
-n隠れ層のニューロン数int32
-o最適化アルゴリズムtext (Adam, Adagrad, RMSprop)Adam

コマンドラインから以下のように使えばよい。

$ python main.py -e 300 -r 0.05 -n 96 -o Adagrad

コード量が増えてしまったが、必要な機能を組み込んで大きくなってしまうのは仕方がないことだ…

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import argparse
import io
from PIL import Image

#//////////////////////////////////////////////////////////////////////////
# ニューラルネット定義
class SineApproximator(nn.Module):
    def __init__(self, nNeurons):
        super().__init__()
        self.model = nn.Sequential(
            nn.Linear( 1,       nNeurons), nn.Tanh(),
            nn.Linear(nNeurons, nNeurons), nn.Tanh(),
            nn.Linear(nNeurons, 1)
        )
    def forward(self, x):
        return self.model(x)

#//////////////////////////////////////////////////////////////////////////
# 実行結果ファイルを出力
def outputFiles( fBody, images, losses, hParams):
    # アニメーションGIF出力
    images[0].save(f"sine_training_{fBody}.gif", save_all=True, append_images=images[1:], duration=100, loop=0)
    # 損失関数グラフPNG出力
    fig = showLossGraph(losses, hParams)
    fig.savefig(f"loss_graph_{fBody}.png")
    plt.close(fig)

#//////////////////////////////////////////////////////////////////////////
# プロット画像をPIL形式で生成
def create_plot_image(x_train, y_true, x_test, y_pred, epoch, hParams):
    plt.figure(figsize=(8, 4))
    plt.plot(x_train, y_true, label="True sin(x)", color="blue")
    plt.plot(x_test,  y_pred, label=f"Predicted", linestyle="dashed", color="red")
    plt.title(f"Epoch {epoch}")
    plt.ylim(-1.0, 1.0)  # Y軸スケールを固定
    plt.legend(loc='upper right')
    plt.grid(True)

    # パラメータ情報を左下に表示
    param_text = (
        f"Epochs: {hParams['nEpochs']}\n"
        f"Learning Rate: {hParams['learningRate']}\n"
        f"Optimizer: {hParams['optimizerName']}\n"
        f"Neurons: {hParams['nNeurons']}"
    )
    plt.text(0.02, 0.05, param_text, transform=plt.gca().transAxes, fontsize=9, va='bottom', ha='left', bbox=dict(facecolor='white', alpha=0.7))

    plt.tight_layout()
    buf = io.BytesIO()
    plt.savefig(buf, format="png")
    buf.seek(0)
    img = Image.open(buf).convert("RGB")
    plt.close()
    return img

#//////////////////////////////////////////////////////////////////////////
# グラフ表示
def showLossGraph( losses, hParams ):
    # 結果の可視化(損失関数の推移:通常スケールと対数スケールを並列表示)
    fig, axs = plt.subplots(1, 2, figsize=(12, 4))  # 横に2つ、等幅で並べる
    # パラメータ表示用テキスト
    param_text = (
        f"Epochs: {hParams['nEpochs']}\n"
        f"Learning Rate: {hParams['learningRate']}\n"
        f"Optimizer: {hParams['optimizerName']}\n"
        f"Neurons: {hParams['nNeurons']}"
    )
    # 線形スケール
    axs[0].plot(range(len(losses)), losses, color='green')
    axs[0].set_title("Loss over Epochs (Linear Scale)")
    axs[0].set_xlabel("Epoch")
    axs[0].set_ylabel("Loss (MSE)")
    axs[0].set_ylim(0, 1)  # ← Y軸を0〜1に固定
    axs[0].grid(True)
    axs[0].text(0.98, 0.96, param_text, transform=axs[0].transAxes, fontsize=9, va='top', ha='right', bbox=dict(facecolor='white', alpha=0.7))
    # 対数スケール
    axs[1].plot(range(len(losses)), losses, color='purple')
    axs[1].set_yscale('log')
    axs[1].set_title("Loss over Epochs (Log Scale)")
    axs[1].set_xlabel("Epoch")
    axs[1].set_ylabel("Loss (log MSE)")
    axs[1].set_ylim(1e-5, 1)  # ← Y軸をlogスケールでも0~1相当の範囲に固定
    axs[1].grid(True)
    axs[1].text(0.02, 0.04, param_text, transform=axs[1].transAxes, fontsize=9, va='bottom', ha='left', bbox=dict(facecolor='white', alpha=0.7))
    # 表示実行
    plt.tight_layout()
    #plt.show()
    return fig

#//////////////////////////////////////////////////////////////////////////
def execute( hParams ):
    #---------------------------------------------------------------------------
    # データ作成
    x_all = np.linspace(-4 * np.pi, 4 * np.pi, 1000)    # -2π~2πの範囲で等間隔に 1000個の点
    y_all = np.sin(x_all)                               # y=sin(x)
    # 偶数番目 → 訓練用        [start:stop:step]
    x_train = x_all[::2]        # [先頭:終端:2step]
    y_train = y_all[::2]        # [先頭:終端:2step]
    # 奇数番目 → テスト用
    x_test = x_all[1::2]        # [先頭+1:終端:2step]
    y_test = y_all[1::2]        # [先頭+1:終端:2step]
    # Tensor化
    x_tensor      = torch.tensor(x_train, dtype=torch.float32).unsqueeze(1)     # 入力データ(train用)
    y_tensor      = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)     # 正解データ(train用)
    x_tensor_test = torch.tensor(x_test,  dtype=torch.float32).unsqueeze(1)     # 入力データ(test用)
    #---------------------------------------------------------------------------
    # モデル・インスタンスを生成
    model = SineApproximator(hParams['nNeurons'])
    criterion = nn.MSELoss()                                    # 損失関数:   MSE
    # 最適化手法を選択
    optimAdam    = lambda: optim.Adam(   model.parameters(), lr=hParams['learningRate'])
    optimAdagrad = lambda: optim.Adagrad(model.parameters(), lr=hParams['learningRate'])
    optimRMSprop = lambda: optim.RMSprop(model.parameters(), lr=hParams['learningRate'], alpha=0.99)
    optimizers = {
        'Adam':    optimAdam,
        'Adagrad': optimAdagrad,
        'RMSprop': optimRMSprop,
    }
    optimizer = optimizers.get(hParams['optimizerName'], optimAdam)()      # 不正指定の場合は Adam
    #---------------------------------------------------------------------------
    # 学習ループ
    images = []                     # GIF画像データ保存リスト
    losses = []                     # 各エポックの損失関数の出力値を記録するリスト
    for epoch in range(hParams['nEpochs']):
        model.train()
        optimizer.zero_grad()
        outputs = model(x_tensor)
        loss = criterion(outputs, y_tensor)
        loss.backward()
        optimizer.step()
        losses.append(loss.item())  # 今回の損失関数の出力値を記録
        # 画像生成
        if epoch % 10 == 0:
            model.eval()
            with torch.no_grad():
                y_pred = model(x_tensor_test).numpy()   # 推論
                img = create_plot_image(x_train, y_train, x_test, y_pred, epoch, hParams)
                images.append(img)
        # 経過表示
        if epoch % 100 == 0:
            print(f"Epoch {epoch}, Loss: {loss.item():.6f}")
    #---------------------------------------------------------------------------
    # 結果ファイル出力
    fBody = f"_o{hParams['optimizerName']}_n{hParams['nNeurons']}_e{hParams['nEpochs']}_r{hParams['learningRate']}"
    outputFiles(fBody, images, losses, hParams)

#//////////////////////////////////////////////////////////////////////////
def main():
    # コマンドライン引数を取得
    parser = argparse.ArgumentParser();
    parser.add_argument('-e', type=int,   default=1000,   help='# of epochs')
    parser.add_argument('-r', type=float, default=0.01,   help='learning rate')
    parser.add_argument('-o', type=str,   default='Adam', help='optimizer name')
    parser.add_argument('-n', type=int,   default=32,     help='# of neurons')
    args = parser.parse_args()
    hParams = {
        'nEpochs':       args.e,
        'learningRate':  args.r,
        'optimizerName': args.o,
        'nNeurons':      args.n
    }
    # 実行
    execute(hParams)

#//////////////////////////////////////////////////////////////////////////
if __name__ == "__main__":
    main()

2) 実行結果

(1) Adam, 学習率:0.01, ニューロン数:32, epoch数:1000

$ python main.py -o Adam -r 0.01 -n 32 -e 1000


(2) Adam, 学習率:0.01, ニューロン数:64, epoch数:1000

$ python main.py -o Adam -r 0.01 -n 64 -e 1000


(3) Adam, 学習率:0.01, ニューロン数:128, epoch数:1000

$ python main.py -o Adam -r 0.01 -n 128 -e 1000


(4) Adam, 学習率:0.005, ニューロン数:64, epoch数:1000

学習率を 0.01 から 0.005 に少し小さくしてみた。

Adam(Adaptive Moment Estimation)アルゴリズムは以下の特徴を持つ。
・過去の勾配の平均と分散を使って、最適解周辺のギッタンバッタンの振動を抑えて滑らかに進む。 Momentum
・個々のニューロンごとに学習率を最適化する。

$ python main.py -o Adam -r 0.005 -n 64 -e 1000


(5) Adam, 学習率:0.001, ニューロン数:64, epoch数:1000

さらに、学習率を 0.005 から 0.001 に小さくしてみた。
損失関数出力値を見ると…
スパイクノイズのような現象は抑えられているが
誤差値は高止まりしている。

$ python main.py -o Adam -r 0.001 -n 64 -e 1000


なかなか学習が進まず苦しそうだ・・・
局所最小をうまく乗り越えられず弱々しくもがいているのだろう。


アクセス数(直近7日): ※試験運用中、BOT除外簡易実装済
  • 2026-06-19: 0回
  • 2026-06-18: 2回
  • 2026-06-17: 0回
  • 2026-06-16: 0回
  • 2026-06-15: 0回
  • 2026-06-14: 1回
  • 2026-06-13: 0回
  • コメントを残す

    メールアドレスが公開されることはありません。 が付いている欄は必須項目です