線形回帰処理のパフォーマンス比較②(勾配法による数値解)|Pythonにおける処理高速化をラフに考える #6

f:id:lib-arts:20201113203327j:plain

このシリーズではPythonの処理高速化についてラフに取り扱っています。
#5では単回帰分析の解析解の計算にあたってのパフォーマンス比較を行いました。

#6では単回帰分析の勾配法を用いた数値解のパフォーマンス比較を行います。
以下、目次になります。
1. 問題設定&NumPyを用いた勾配法の実装
2. DeepLearningフレームワークを用いた実装
3. まとめ


1. 問題設定&NumPyを用いた勾配法の実装
1節では問題設定とNumPyを用いた勾配法の実装について取り扱います。簡易化のため、問題設定は#5を引き継ぐものとします。

f:id:lib-arts:20201120182432p:plain

上記の問題における勾配法の実装をサンプル数を変えつつ実行してみます。

まず、誤差関数として最小二乗誤差を用いたとすると、E = \sum(y_i - ax_i -b)^2となるので、パラメータのabに関する偏微分は下記のようになります。

\displaystyle \frac{\partial E}{\partial a} = -2 \sum(y_i - ax_i -b) x_i

\displaystyle \frac{\partial E}{\partial b} = -2 \sum(y_i - ax_i -b)

上記が0になるとして、abについて解いたのが#5で用いた正規方程式ですが、勾配法は上記で計算した勾配のみを用いて繰り返し演算によって結果を求めます。

%%time

a, b = 0, 0
eta = 0.01
for i in range(1000):
    a = a - eta*np.sum(-2*(y-a*x-b)*x)/x.shape[0]
    b = b - eta*np.sum(-2*(y-a*x-b))/x.shape[0]

print(x.shape[0])
print(a)
print(b)

abの初期値をそれぞれ0にした上で、勾配法による繰り返しに基づいたパラメータ計算を実装したのが上記になります。

f:id:lib-arts:20201121173506p:plain

実行結果は上記になり、概ね元にした数式であるy=2x+1に収束していることが確認できます。また、勾配をサンプル数で割っているのはサンプル数に応じて勾配が大きくなるためです。学習率で調整するという方法もありますが、ミニバッチ法とは異なり今回はサンプル数に応じたバッチを作成し比較するため、実装の簡易化のためにもこのような実装としました。さて、100サンプルでの実行について確認できたので、以下サンプル数を増やして実験してみます。

f:id:lib-arts:20201121173919p:plain

f:id:lib-arts:20201121173950p:plain

上記はサンプル数1,000と10,000で実行した結果になります。多少実行時間が伸びているものの、それほど大きくはなっていないことが確認できます。

f:id:lib-arts:20201121174138p:plain

f:id:lib-arts:20201121174203p:plain

次に上記はサンプル数100,000と1,000,000で試した結果になります。100,000サンプル周辺から、サンプル数の増加にある程度比例して処理時間がかかっていることが確認できます。

一通りの問題設定とNumPyによる勾配法の実装について確認できたので、1節はここまでとします。

 

2. DeepLearningフレームワークを用いた実装
2節ではDeepLearningフレームワークとの比較として、PyTorchで実行を行ってみます。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
import torch
import torch.nn as nn
%matplotlib inline

np.random.seed(0)

x = np.arange(-1, 9, 0.1)
x_ = torch.tensor(x.reshape(x.shape[0],1).astype("float32"))
y = x*2 + 1 + np.random.normal(0, 0.1, x.shape[0])
y_ = torch.tensor(y.reshape(y.shape[0],1).astype("float32"))

plt.scatter(x,y)
plt.show()

f:id:lib-arts:20201121211155p:plain

まずは問題設定ですが、PyTorchの形式にxとyを変換するにあたって、x_とy_のところだけ書き換えています。

class SimpleReg(nn.Module):

    def __init__(self, input_size, output_size):
        super(SimpleReg, self).__init__()
        self.linear = nn.Linear(input_size, output_size)

    def forward(self, x):
        out = self.linear(x)
        return out

reg_model = SimpleReg(1, 1)
loss_func = nn.MSELoss()
optimizer = torch.optim.SGD(reg_model.parameters(), lr=0.01)

また、学習の設定は上記のように行います。誤差関数はMSELossということで最小二乗誤差、最適化はSGDということで勾配法を用いています。

%%time

for i in range(1000):

    out = reg_model(x_)
    loss = loss_func(out, y_)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (i + 1) % 100 == 0:
        print('Epoch{}: , Loss: {:.3f}'.format(i+1, loss.item()))

print(reg_model.linear.weight)
print(reg_model.linear.bias)

f:id:lib-arts:20201121211334p:plain

学習の実行は上記のように行っています。lossが徐々に小さくなっていく過程が確認できます。途中経過の分、遅くなる可能性を考慮した上で、以下途中経過の出力を消去した上でサンプル数を増やし実行を行います。

f:id:lib-arts:20201121211708p:plain

f:id:lib-arts:20201121211727p:plain

サンプルが100と1,000での実行結果が上記です。それぞれ0.1秒〜1秒ほどの実行時間であり、NumPyの実行よりも1桁程度時間がかかっています。もう少しサンプルを増やして確認してみます。

f:id:lib-arts:20201121211932p:plain

f:id:lib-arts:20201121211954p:plain

サンプルが10,000と100,000での実行結果が上記です。1秒〜5秒程度の時間で、こちらも今回の実験においてはNumPyの方が速いという結果になりました。


3. まとめ
#6では回帰分析における勾配法の実行速度の確認を行いました。今回はNumPyベースの結果の方がPyTorchベースよりも速いという結果になりましたが、他の例も含めて色々と確認していければと思います。
#7では、もう少し複雑な例で確認するということでMLP(Multi Layer Perceptron)の誤差逆伝播について確認してみたいと思います。