化学系エンジニアがAIを学ぶ

PyTorchでディープラーニング、強化学習を学び、主に化学工学の問題に取り組みます

ニューラルネットワークによる関数近似

はじめに

PyTorchを用いてニューラルネットワーク関数近似を行うプログラムを作成する。

必要なモジュールのインポート

必ずしも必要でないが、ここではtorch.utils.dataをインポートしtorch.utils.data.TensorDatasettorch.utils.data.DataLoaderを 用いて、ミニバッチ学習をさせることにする。 TensorDataset、DataLoaderの使い方について→メモ: PyTorch TensorDataset、DataLoader について - 化学系エンジニアがAIを学ぶ

import numpy
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data
from matplotlib import pyplot as plt

ニューラルネットワークモデルの定義

入力1つ、出力1つとしている。中間層は2層とし、それぞれのノード数は20, 40個としている。 中間層の活性化関数はReLUとし、出力層はl3の計算値をそのまま出力している。

class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.l1 = nn.Linear(1, 20)
        self.l2 = nn.Linear(20, 40)
        self.l3 = nn.Linear(40, 1)

    def forward(self, x):
        x = F.relu(self.l1(x))
        x = F.relu(self.l2(x))
        x = self.l3(x)
        return x

データの準備

xy=f(x)の組としてTensorDatasetを使用する。ここでは関数としてnumpy.exp(x)を入れているが、 ここに任意の関数を入れることができる。 xはn個の0〜1の間のランダム値を取る。1次元の配列のままでは扱えないので (n, 1)の形にreshapeさせている。さらにtorch.FloatTensorの形にしてTensorDatasetを得ている。

def get_dataset(n):
    x = numpy.random.random(n) #0〜1のランダム値を取る
    y = numpy.exp(x) #ここを変えれば各種関数で計算できる
    x = x.reshape(n, 1) #(n, 1)の配列にする必要がある
    y = y.reshape(n, 1) #(n, 1)の配列にする必要がある
    x = torch.FloatTensor(x) # floatでないといけない
    y = torch.FloatTensor(y) # floatでないといけない
    return torch.utils.data.TensorDataset(x, y)

TensorDatasetをtorch.utils.data.DataLoaderを渡すことでミニバッチを返すiterableオブジェクトが得られる。

data_number = 10000 #準備するデータの数
batch_size  = 1000 # 1つのミニバッチのデータの数
data_loader = torch.utils.data.DataLoader(get_dataset(data_number), batch_size=batch_size)

損失関数、最適化関数の設定

損失関数には平均2乗誤差、最適化関数にAdamを使用。

model = Model()
criterion = nn.MSELoss()
learning_rate = 1e-2
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

学習

DataLoaderを用いて1ミニバッチごとに学習を繰り返す。

loss_log = [] # 学習状況のプロット用
epoch = 100
for t in range(epoch):
    for xt, yt in data_loader: # 1ミニバッチずつ計算
        optimizer.zero_grad() 
        y_model = model(xt)
        loss = criterion(y_model, yt)
        loss.backward()
        optimizer.step()
    print(t, loss.item())
    loss_log.append(loss.item())

学習結果の確認

##学習したモデルと関数を比較する
x = numpy.linspace(0, 1, 100)
plt.plot(x, numpy.exp(x), label='exp(x)')
x_model = torch.FloatTensor(x.reshape(100,1))
y_model = model(x_model)
y = y_model.detach().numpy().reshape(1,100)[0] # プロット用に変換、detach()が必要
plt.plot(x, y, label='model')
plt.legend()
plt.xlabel('x')
plt.ylabel('y')
plt.show()

y_modelの中身をnumpyの1次元配列に変換している。 detach()なしでやると以下のErrorが出る。これを参考にdetach()を入れた。

RuntimeError: Can't call numpy() on Variable that requires grad. Use var.detach().numpy() instead.

結果

学習状況

以下のコードで学習状況を確認できる。 変化が小さくなるepoch数70回あたりで学習が十分できていると思われる。 learnig_rate = 1e-1では loss が不安定に変化したため 1e-2 に設定した。

plt.plot(loss_log)
plt.xlabel('epoch')
plt.ylabel('loss')
plt.yscale('log')
plt.show()

f:id:schemer1341:20190104212742p:plain:w500
図1 学習状況(epochに対するlossの変化)

学習したモデルと関数値との比較

ほぼ一致していることが確認できる。

f:id:schemer1341:20190104213229p:plain:w500
図2 学習したモデルと関数値との比較

その他の関数での例

関数get_dataset()内の数学関数部分を変えれば計算できる。

sin(x)

ほぼ一致している。

f:id:schemer1341:20190105011912p:plain:w500
図3 sin(x)との比較

sqrt(s)

x = 0 付近でずれあるが、その他はほぼ一致している。

f:id:schemer1341:20190105011751p:plain:w500
図4 sqrt(x)との比較

exp(cos(x))

この例ではxの範囲を0〜5としている。x = 0 および x = 5 付近でずれが大きめ。

f:id:schemer1341:20190105012215p:plain:w500
図5 exp(cos(x))との比較

外挿領域の予測精度

exp(x)を 0 <= x < 1 の範囲で学習させたが、試しにその範囲外を計算してみると合わない。

f:id:schemer1341:20190105013433p:plain:w500
図6 exp(x)との比較 x = 0 〜 2

所感

sqrt(x)やexp(cos(x))ではモデルと関数値がずれる部分が生じたが、これを合わせるためにはどうするのか?層を増やす?中間層のnode数を増やす?

参考

全コード

# -*- coding: utf-8 -*-
import numpy
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data
from matplotlib import pyplot as plt

class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.l1 = nn.Linear(1, 20)
        self.l2 = nn.Linear(20, 40)
        self.l3 = nn.Linear(40, 1)

    def forward(self, x):
        x = F.relu(self.l1(x))
        x = F.relu(self.l2(x))
        x = self.l3(x)
        return x

def get_dataset(n):
    x = numpy.random.random(n) #0〜1のランダム値を取る
    y = numpy.exp(x) #ここを変えれば各種関数で計算できる
    x = x.reshape(n, 1) #(n, 1)の配列にする必要がある
    y = y.reshape(n, 1) #(n, 1)の配列にする必要がある
    x = torch.FloatTensor(x) # floatでないといけない
    y = torch.FloatTensor(y) # floatでないといけない
    return torch.utils.data.TensorDataset(x, y)

data_number = 10000 #準備するデータの数
batch_size  = 1000 # 1つのミニバッチのデータの数
data_loader = torch.utils.data.DataLoader(get_dataset(data_number), batch_size=batch_size)

model = Model()
criterion = nn.MSELoss()
learning_rate = 1e-2
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

loss_log = [] # 学習状況のプロット用
epoch = 100
for t in range(epoch):
    for xt, yt in data_loader: # 1ミニバッチずつ計算
        optimizer.zero_grad() 
        y_model = model(xt)
        loss = criterion(y_model, yt)
        loss.backward()
        optimizer.step()
    print(t, loss.item())
    loss_log.append(loss.item())

x = numpy.linspace(0, 1, 100)
plt.plot(x, numpy.exp(x), label='exp(x)')
x_model = torch.FloatTensor(x.reshape(100,1))
y_model = model(x_model)
y = y_model.detach().numpy().reshape(1,100)[0] # プロット用に変換、detach()が必要
plt.plot(x, y, label='model')
plt.legend()
plt.xlabel('x')
plt.ylabel('y')
plt.show()