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

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

メモ: PyTorchの自動微分について

はじめに

PyTorchの自動微分についてのメモを記す。

自動微分

アルゴリズムによって定義された関数からその関数の偏導関数値を計算するアルゴリズムを導出するための技術。 数値微分や数式微分とも異なる方法で、これら方法の問題点(誤差や桁落ちによる精度低下、プログラムから微分表現を導けない)を 解消できる手段とのこと。

PyTorchでの自動微分の例

  • tensorを作るときに、requires_grad=True とする。
  • テンソル演算する。 (ex. y = x **2)
  • y.backward()で勾配を計算する。
  • x.gradで勾配値が得られる
import torch

x = torch.tensor(1.0, requires_grad=True) # require_grad=Trueが必要。defaultはFalse
y = x ** 2
y.backward()  # 勾配を求める
print(x.grad) # xにおける勾配の値がx.gradに入っている

x = torch.tensor(1.0, requires_grad=True)
y = 2 ** x
y.backward()
print(x.grad)

x1 = torch.tensor(1.0, requires_grad=True)
x2 = torch.tensor(1.0, requires_grad=True)
y  = x1**2 + torch.sqrt(x1*x2) # 偏微分のケース
y.backward()
print(x1.grad, x2.grad)

実行結果

tensor(2.)
tensor(1.3863)
tensor(2.5000) tensor(0.5000)

参考

スキナー箱のQ学習

はじめに

ネズミがボタンを押して餌を得るという問題について強化学習(Q学習)を適用してみる。 本記事の内容に関して下記書籍を参考にした。

ネズミの学習問題

対象とする問題は下記の通り。

  • 箱の中にネズミが1匹いる
  • 箱の中にネズミ餌のディスペンサーが1つある
  • ディスペンサーには「電源ボタン」と「餌ボタン」がある
  • 「電源ボタン」をネズミが押すたびに電源のON/OFFが切り替わる
  • ディスペンサーの電源がONのときにネズミが「餌ボタン」を押すと餌が出てくる

ネズミは餌を得る手順を学習できるか。

ディスペンサーのモデル

まず、電源のON/OFF状態を保持し、ON状態で「餌ボタン」が押されると餌(報酬)を返すクラスを作成する。

import numpy

class Dispenser(object):
    def __init__(self, init_state):
        """
        初期のON/OFF状態を設定する
        init_state: 0->電源OFF、1->電源ON
        """
        self._state  = init_state
        self._button = self.powerbutton(init_state)

    def powerbutton(self, init_state):
        """
        ON/OFFを切り替えるgenerator
        0: 電源OFF、1: 電源ON を返す。内部的には-1, 1で切り替えている
        """
        state = -1 if init_state==0 else init_state
        while True:
            state = -1 * state
            yield numpy.maximum(0, state)

    def push_powerbutton(self):
        self._state = self._button.__next__()

    def step(self, action):
        """
        action: 0->電源ボタンを押す 1->餌ボタンを押す
        """
        if action == 0:
            self.push_powerbutton()
            reward = 0
        else:
            reward = 1 if self._state==1 else 0
        return self._state, reward

挙動としては下記。電源ボタンを押すとON/OFFが切り替わり、ONのとき餌ボタンを押すと餌(報酬 1)が得られる。 具体的には(状態, 報酬)のタプルが返る。これだけ。

>>> env = Dispenser(0) # 電源OFFを初期状態としてディスペンサーオブジェクトを作成
>>> env.step(0)  # 0:電源ボタンを押す
(1, 0) # 状態:電源ON, 報酬:0 が返る

>>> env.step(1) # 1: 餌ボタンを押す
(1, 1) # 状態:電源ON, 報酬:1が返る

Q値

各状態および行動に対するQ値の表を次の通り決める。

Q(0, 0): 状態 電源OFF、行動 電源ボタンを押す

Q(0, 1): 状態 電源OFF、行動 餌ボタンを押す

Q(1, 0): 状態 電源ON、 行動 電源ボタンを押す

Q(1, 1): 状態 電源ON、 行動 餌ボタンを押す

各Q値は下記の通り配列で保持することとする。初期値は全て0。

qtable = numpy.zeros((2, 2))

Q値に従って次の行動を決める

Q値および現在の状態から次に取る行動を決める。基本的にはQ値が最大の行動を取るが、 時折ランダムな行動を取る  \epsilon - グリーディー法を取り入れることとする。

def get_action(qtable, state, episode):
    """
    現在の状態とQ値から次の行動を決める
    episode回数が増えるごとにランダム行動を取らなくする
    """
    ## ε- greedy法 行動を重ねる(episodeが増える)毎にランダム行動を取らなくする
    epsilon = 0.5 * (1 / (episode + 1))
    if epsilon <= numpy.random.uniform(0, 1):
        ## まずQ値が最大の要素を確認(必ずしも1つでないので)
        ## 最大値がTrue、それ以外はFalseとなっているリストを得る
        qmax_bool_list = qtable[state]==qtable[state].max()
        ## Qが最大値の要素のインデックスを得る(複数あるときはその中からランダムに選ぶことになる)
        choices = numpy.where(qmax_bool_list)[0] 
    else:
        ## ランダム行動。全要素のインデックスを得る
        choices = numpy.arange(len(qtable[state]))
    return numpy.random.choice(choices) # 候補の中から1つランダムに選ぶ

Q値を更新する

状態、行動および行動を取った結果の報酬から下記式に従いQ値を更新する。  s_tは状態、 a_tは行動、 rは報酬、 \text{max}Qは次の状態での最大のQ値。  \alphaは学習率、 \gammaは割引率と呼ばれる。

\displaystyle Q(s_t, a_t) \gets (1 - \alpha) Q(s_t, a_t) + \alpha (r + \gamma \text{max} Q)

def update_qtable(qtable, state, action, next_state, reward):
    gamma = 0.9
    alpha = 0.5
    next_qmax = max(qtable[next_state])
    qtable[state, action] = (1 - alpha) * qtable[state, action] +\
                            alpha * (reward + gamma * next_qmax)
    return qtable

学習する

1episodeあたり5回行動することとして、10episode学習させる。 各episodeの初期のディスペンサー電源ON/OFFはランダムに決める。

MAX_STEP_NUMBER = 5 # 1episodeあたり5回行動することとする
MAX_EPISODE_NUMBER = 10 # episodeは10までとする

for episode in range(MAX_EPISODE_NUMBER):
    total_reward = 0
    state = numpy.random.choice([0, 1]) # ディスペンサー電源初期状態はランダム
    env = Dispenser(state)
    
    for step in range(MAX_STEP_NUMBER):
        action = get_action(qtable, state, episode) # q値、状態から行動を決める
        next_state, reward = env.step(action) # 行動を取った結果を得る
        print(state, action, reward)
        total_reward += reward
        qtable = update_qtable(qtable, state, action, next_state, reward) # Q値の更新
        state = next_state

    print('episode : %d total reward %d' %(episode+1, total_reward))
    print(qtable)

学習結果

初期が電源OFFならtotal rewardは最大4、ONなら最大5となるが、episode 9, 10ではいずれもrewardは最大となっている。 初期ON/OFFがランダムでも正しく行動が取られている。

>>>
0 1 0
0 0 0
1 1 1
1 1 1
1 1 1
episode : 1 total reward 3
[[ 0.       0.     ]
 [ 0.       1.42625]]
0 0 0
1 0 0
0 0 0
1 1 1
1 0 0
episode : 2 total reward 1
[[ 0.96271875  0.        ]
 [ 0.57763125  1.8549375 ]]
.
.
.
0 0 0
1 1 1
1 1 1
1 1 1
1 1 1
episode : 9 total reward 4
[[ 6.00931629  1.23142328]
 [ 1.25417229  7.85361236]]
1 1 1
1 1 1
1 1 1
1 1 1
1 1 1
episode : 10 total reward 5
[[ 6.00931629  1.23142328]  # 電源OFF時のQ値、電源ボタンを押すQ値が大きい
 [ 1.25417229  8.33916616]] # 電源ON時のQ値、餌ボタンを押すQ値が大きい

全コード

import numpy

class Dispenser(object):
    def __init__(self, init_state):
        """
        初期のON/OFF状態を設定する
        init_state: 0->電源OFF、1->電源ON
        """
        self._state  = init_state
        self._button = self.powerbutton(init_state)

    def powerbutton(self, init_state):
        """
        ON/OFFを切り替えるgenerator
        0: 電源OFF、1: 電源ON を返す。内部的には-1, 1で切り替えている
        """
        state = -1 if init_state==0 else init_state
        while True:
            state = -1 * state
            yield numpy.maximum(0, state)

    def push_powerbutton(self):
        self._state = self._button.__next__()

    def step(self, action):
        """
        action: 0->電源ボタンを押す 1->餌ボタンを押す
        """
        if action == 0:
            self.push_powerbutton()
            reward = 0
        else:
            reward = 1 if self._state==1 else 0
        return self._state, reward

def get_action(qtable, state, episode):
    ## ε- greedy法 行動を重ねる(episodeが増える)毎にランダム行動を取らなくする
    epsilon = 0.5 * (1 / (episode + 1))
    if epsilon <= numpy.random.uniform(0, 1):
        ## Q値に従った行動。Q値が最大の行動を取る
        ## まずQ値が最大の要素を確認(必ずしも1つでないので)
        qmax_bool_list = qtable[state]==qtable[state].max()
        # 複数あるときはその中からランダムに選ぶ
        choices = numpy.where(qmax_bool_list)[0] 
    else:
        ## ランダム行動
        choices = numpy.arange(len(qtable[state]))
    return numpy.random.choice(choices)

def update_qtable(qtable, state, action, next_state, reward):
    gamma = 0.9
    alpha = 0.5
    next_qmax = max(qtable[next_state])
    qtable[state, action] = (1 - alpha) * qtable[state, action] +\
                            alpha * (reward + gamma * next_qmax)
    return qtable

qtable = numpy.zeros((2, 2))
MAX_STEP_NUMBER = 5 # 1episodeあたり5回行動することとする
MAX_EPISODE_NUMBER = 10 # episodeは10までとする

for episode in range(MAX_EPISODE_NUMBER):
    total_reward = 0
    state = numpy.random.choice([0, 1]) # ディスペンサー電源初期状態はランダム
    env = Dispenser(state)
    
    for step in range(MAX_STEP_NUMBER):
        action = get_action(qtable, state, episode) # q値、状態から行動を決める
        next_state, reward = env.step(action) # 行動を取った結果を得る
        print(state, action, reward)
        total_reward += reward
        qtable = update_qtable(qtable, state, action, next_state, reward) # Q値の更新
        state = next_state

    print('episode : %d total reward %d' %(episode+1, total_reward))
    print(qtable)

ニューラルネットワークによる手書き数字の認識

はじめに

scikit-learnライブラリに含まれる手書き数字データを用いて、ニューラルネットワークによる 手書き数字認識をやってみる。

データの準備

手書き数字データはサイズ 8 x 8で、グレースケールの階調が17段階となっている。具体的な手書き数字画像は scikit-learnのexampleで確認できる。 入力の個数は 8 x 8 = 64個で、出力は0〜9の10個となっている。データをPyTorchで扱うために、入力はFloatTensor、出力はLongTensorに 変換している。

## 必要なモジュールのインポート
import numpy
import torch
import torch.nn as nn
import torch.nn.functional as F
from matplotlib import pyplot as plt
from sklearn.datasets import load_digits # 手書き数字データ読み込み
from sklearn.model_selection import train_test_split # 手書き数字データを学習用とテスト用に分類する

## データの準備
digits = load_digits()
x_train, x_test, y_train, y_test = train_test_split(digits.data, digits.target, test_size=0.2) # 2割がテストデータ
x_train = torch.FloatTensor(x_train)
y_train = torch.LongTensor(y_train)
x_test  = torch.FloatTensor(x_test)
y_test  = torch.LongTensor(y_test)

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

入力64個、出力10個。中間層は2層とし、ノード数は100個とした。 中間層の活性化関数はReLUとし、出力層はl3の計算値をそのまま出力とした。

class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.l1 = nn.Linear(64, 100)
        self.l2 = nn.Linear(100, 100)
        self.l3 = nn.Linear(100, 10)

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

損失関数、最適化関数

損失関数にnn.CrossEntropyLossを使用した。learning_rateは試行錯誤して 良さそうなところに決めた。

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

学習

epoch数は試行錯誤して良さそうなところに設定した。

epoch = 200
loss_log = [] # 学習状況のプロット用
for t in range(epoch):
    optimizer.zero_grad()
    y_model = model(x_train)
    loss = criterion(y_model, y_train)
    loss.backward()
    optimizer.step()
    loss_log.append(loss.item())

学習結果の確認

## 学習状況のプロット
plt.plot(loss_log)
plt.xlabel('epoch')
plt.ylabel('loss')
plt.yscale('log')
plt.show()

## 正答率の確認
outputs = model(x_test)
_, predicted = torch.max(outputs.data, 1)
correct = (y_test == predicted).sum().item() # Tensorの比較で正答数を確認
print("正答率: {} %".format(correct/len(predicted)*100.0))

結果

epoch数5000までやってみたが、学習状況はなかなか落ち着いてこなかった。いっぽうで、正答率はepoch数が100でも5000でも ほとんど変わらなかった。

学習状況

f:id:schemer1341:20190113220841p:plain:w500
図 学習状況 epoch数に対するlossの変化

正答率

>>> 正答率: 98.61111111111111 % # epoch数 100
>>> 正答率: 98.05555555555556 % # epoch数 5000

所感

人でも実際に見間違うことはあるだろうから、そんなに悪くない正答率なのではないかと思う。 畳み込みニューラルネットワークを利用したら精度が上がる?画像が 8 x 8 と小さいのであまり効果はなさそうに思われる。

参考

ニューラルネットワークによるばらつきのあるデータへの近似

はじめに

前回は関数値そのものを学習させたが、今回は関数値をもとに作成したばらつきのあるデータを学習させて 関数をニューラルネットワークで表現できるかを見てみる。

以下の記事で同様の内容がtensorflowを用いて紹介されているが、 ここではPyTorchを使う。

ニューラルネットワークは任意の関数を表現できるのか? - Qiita

データの準備

ここでの関数はnumpy.sin(x)としている。xの値はランダムに選んだ。yの値は関数値に、正規分布に従う 乱数を加えてデータをばらつかせた。

## 必要なモジュールのインポート
import numpy
import torch
import torch.nn as nn
import torch.nn.functional as F
from matplotlib import pyplot as plt

## データ作成
data_num = 200
x = numpy.random.rand(data_num)*6 # 0 - 6 の範囲の一様乱数
y = numpy.sin(x) + numpy.random.randn(data_num)*0.3 # 正規分布に従う乱数を足す
x_train = torch.FloatTensor(x.reshape(data_num, 1))
y_train = torch.FloatTensor(y.reshape(data_num, 1))

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

入力1つ、出力1つ。中間層は1層とし、ノード数は2個とした。 中間層の活性化関数はReLUとし、出力層はl2の計算値をそのまま出力させた。。

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

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

ニューラルネットワークモデルの登録

損失関数に平均二乗誤差nn.MSELossを使用した。learning_rateは試行錯誤して 良さそうなところに決めた。

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

学習

epoch数は試行錯誤して設定したが、計算の初期値次第で学習状況は多少変わる模様。

epoch = 100
for t in range(epoch):
    optimizer.zero_grad()
    y_model = model(x_train)
    loss = criterion(y_model, y_train)
    loss.backward()
    optimizer.step()
    print(t, loss.item()) # 学習状況の表示

学習結果の確認

plt.plot(x, y, '.', label='data')
x = numpy.linspace(0, 6, data_num)
x_model = torch.FloatTensor(x.reshape(data_num, 1))
y_model = model(x_model).detach().numpy().reshape(1, data_num)[0]
plt.plot(x, y_model, label='model', linewidth=3)
plt.legend()
plt.xlabel('x')
plt.ylabel('y')
plt.show()

結果

計算させるたびに多少結果が変わるが、下図のようにだいぶ無理矢理な感じとなった。epoch数を増やしてもほとんど状況に変化なかった。

f:id:schemer1341:20190109234003p:plain:w500
図1 データとモデル値の比較

なお、活性化関数をsigmoidに変えると無理矢理感は低下した。

f:id:schemer1341:20190109235006p:plain:w500
図2 活性化関数をsigmoidとした場合のデータとモデル値の比較

さらに中間層を2層(ノード数 20 ,40)、epoch数=1000で計算するとフィット感は改善した。

f:id:schemer1341:20190110000954p:plain:w500
図3 中間層数、epoch数を増やした場合のデータとモデル値の比較

epoch数を5000回まで増やすと合わせすぎた感じとなり、いわゆる過学習状態になっていると思われる。。

f:id:schemer1341:20190110001406p:plain:w500
図4 epoch数をさらに増やした場合のデータとモデル値の比較

所感

epoch数、中間層ノード数、層の深さなど、適切に選ぶにはそこそこの知識・経験が要りそう。

参考

ニューラルネットワークは任意の関数を表現できるのか? - Qiita

メモ: PyTorch TensorDataset、DataLoader について

はじめに

PyTorchのtorch.utils.data.TensorDatasettorch.utils.data.DataLoaderの使い方についてのメモを記す。

torch.utils.data.TensorDataset

同じ要素数の2つのtensorを渡し、その組を得る。

import numpy
import torch
import torch.utils.data

x = numpy.array([[1], [2], [3], [4], [5], [6]])
y = numpy.array([[10], [20], [30], [40], [50], [60]])
x = torch.tensor(x)
y = torch.tensor(y)
dataset = torch.utils.data.TensorDataset(x, y)

中身をそのまま表示させると、TensorDatasetというオブジェクトであることが示される。

>>> dataset
<torch.utils.data.dataset.TensorDataset object at 0x7f4e91fdca20>

[ ]で要素を取り出せる。Tensorの組がタプルになっている。

>>> dataset[0]
(tensor([1.]), tensor([10.]))

>>> for x, y in dataset:
...     print(x, y)
... 
tensor([1.]) tensor([10.])
tensor([2.]) tensor([20.])
tensor([3.]) tensor([30.])
tensor([4.]) tensor([40.])
tensor([5.]) tensor([50.])
tensor([6.]) tensor([60.])

torch.utils.data.DataLoader

torch.utils.data.DataLoaderにTensorDatasetを渡すと得られ、ミニバッチを返すiterableなオブジェクトとなる。 batch_sizeでミニバッチのデータの数を指定できる(defaultは1)。shuffleをTrueとするとTensorDatasetの中身の 組の順序がシャッフルされる(defaultはFalse)。

batch_size  = 3 # ミニバッチのデータの数
data_loader = torch.utils.data.DataLoader(dataset, 
                       batch_size=batch_size, shuffle=True)

forで中身を取り出すと次のようにミニバッチサイズ数ごとにデータが取り出される(この例では3つ)。順序が シャッフルされていることが確認できる。

>>> for x, y in data_loader:
...     print(x, y)
... 
tensor([[5.], [4.], [1.]]) tensor([[50.], [40.], [10.]])
tensor([[3.], [6.], [2.]]) tensor([[30.], [60.], [20.]])

ミニバッチ数を4、shuffle=Falseとすると次の通りとなる。データは全部で6組なので、2回めに取り出す際は残りの2組だけとなっている。 データの順序はシャッフルされずにもとのままとなっている。

>>> data_loader = torch.utils.data.DataLoader(dataset,
... batch_size=4, shuffle=False)
>>> for x, y in data_loader:
...     print(x, y)
... 
tensor([[1.], [2.], [3.], [4.]]) tensor([[10.], [20.], [30.], [40.]])
tensor([[5.], [6.]]) tensor([[50.], [60.]])

DataLoaderを用いた例

ニューラルネットワークによる関数近似 - 化学系エンジニアがAIを学ぶ

参考

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

はじめに

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()

ニューラルネットワークで論理演算子 OR を学習する

はじめに

業務にて深層強化学習の利用を目指す。まずは簡単な例からということで、PyTorchを用いてニューラルネットワークで論理演算子 OR を学習するプログラムを作成する。

論理演算子 OR のモデル

0または1を持つ2つの入力x1, x2の少なくとも一方が1である場合に1を、そうでない場合に0を返すモデルを作成する。

(x1, x2) → model → 0 または1

具体的には、

(0, 0) → model → 0

(1, 0) → model → 1

(0, 1) → model → 1

(1, 1) → model → 1

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

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

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

入力2つ、出力は2つとしている。中間層は1層とし、そのノード数は3つとしている。出力は答えが0のときは出力の1つめが大きくなるように、答えが1のときは出力の2つめが大きくなるように、というように分類問題として扱う(後に示すとおり分類問題に合わせた損失関数を選ぶ必要がある)。中間層の活性化関数はReLUとし、出力層はl2の計算値をそのまま出力している。

class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.l1 = nn.Linear(2, 3)
        self.l2 = nn.Linear(3, 2)

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

データの準備

torch.tensorとしてデータを準備するが、入力データx_trainはtorch.float型で、分類問題とするため出力データy_trainは整数型(long)にする必要がある。学習データx_trainy_trainは下記のパターンに対応する。

(0, 0) → model → 0

(1, 0) → model → 1

(0, 1) → model → 1

(1, 1) → model → 1

x_train = torch.tensor([[0,0],[0,1],[1,0],[1,1]], dtype=torch.float)
y_train = torch.LongTensor(numpy.array([0,1,1,1]))

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

損失関数と最適化関数を適宜選ぶ必要がある。最適化関数についてはlearning_rateを設定する必要がある。

model = Model() #モデル登録
criterion  = nn.CrossEntropyLoss() #損失関数 (クロスエントロピー誤差)
learning_rate = 1e-1
optimizer  = torch.optim.Adam(model.parameters(), lr=learning_rate) #最適化関数 (Adam)

学習

optimizer.zero_grad()をして、loss = criterion(y_model, y_train)でモデル計算値と答えの誤差を求め、 loss.backward()して、optimizer.step()を繰り返せばよい。

loss_log = [] #学習状況のプロット用
epoch = 100
for t in range(epoch):
    optimizer.zero_grad() #計算された勾配を0にリセットする
    y_model = model(x_train) #モデルの計算値算出
    loss = criterion(y_model, y_train) #モデル計算値と答えを比較し誤差を求める
    loss.backward() #誤差からバックプロパゲーション
    optimizer.step() #バックプロパゲーションから重み更新
    print(t, loss.item()) #各エポックで誤差を表示
    loss_log.append(loss.item()) #学習状況の記録

学習結果の確認

## 学習状況のプロット
plt.plot(loss_log)
plt.xlabel('epoch')
plt.ylabel('loss')
plt.yscale('log')
plt.show()

##学習したモデルの正答率を出す
outputs = model(x_train) # 2つのnodeの出力のtensorが得られる。大きい方がモデルの出した答え
_, predicted = torch.max(outputs.data, 1) # modelの答えを0か1かのtensorに変換する
correct = (y_train == predicted).sum().item() # 正答数を求める。
print("正答率: {} %".format(correct/len(predicted)*100.0))

結果

学習状況

変化が小さくなるepoch数60回あたりで学習が十分できていると思われる。 ただし計算させるたびにlossの変化は変わるし、上で設定したlearning_rateの値次第でも変わる。

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

学習したモデルの正答率

正答率: 100.0 %

参考

全コード

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

class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.l1 = nn.Linear(2, 3)
        self.l2 = nn.Linear(3, 2)

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

x_train = torch.tensor([[0,0],[0,1],[1,0],[1,1]], dtype=torch.float)
y_train = torch.LongTensor(numpy.array([0,1,1,1]))

model = Model() #モデル登録
criterion  = nn.CrossEntropyLoss() #損失関数 "criterion"の変数名とするのが一般的な模様
learning_rate = 1e-1
optimizer  = torch.optim.Adam(model.parameters(), lr=learning_rate) #最適化関数

loss_log = [] #学習状況のプロット用
epoch = 100
for t in range(epoch):
    optimizer.zero_grad() #計算された勾配を0にリセットする
    y_model = model(x_train) #モデルの計算値算出
    loss = criterion(y_model, y_train) #モデル計算値と答えを比較し誤差を求める
    loss.backward() #誤差からバックプロパゲーション
    optimizer.step() #バックプロパゲーションから重み更新
    print(t, loss.item()) #各エポックで誤差を表示
    loss_log.append(loss.item()) #学習状況の記録

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

##学習したモデルの正答率を出す
outputs = model(x_train) # 2つのnodeの出力のtensorが得られる。大きい方がモデルの出した答え
_, predicted = torch.max(outputs.data, 1) # modelの答えを0か1かのtensorに変換する
correct = (y_train == predicted).sum().item() # 正答数を求める。
print("正答率: {} %".format(correct/len(predicted)*100.0))