ニューラルネットワークコンソールで、シンプルRNNで予測みたいなことをやってみました。
今度は、それと同じようなことを、NNabla(Neural Network Libraries)でもやってみようかなと思います。
ただ、全く同じではなく、学習・評価用のSINカーブは、ニューラルネットワークコンソールで使ったCSVファイルをそのまま使うのではなくて、プログラムで生成させるように変えてます。
理由は、ニューラルネットワークコンソールで作ったデータは、あまりにきれいすぎたので、インンプットデータにノイズを入れて、ちょっと難易度をあげてみようかなと思ったからです。
さて、やってみます。
先に結果を示しておきます。
こんな感じのグラフになりました。
正直、すごい微妙です。
赤線が正解で、黒線が予測結果なんですけどね。
なんとなくあっているような、外れているような・・(笑)
こういう微妙な結果になった原因が、ノイズを加えたためか、自分の組んだシンプルRNNがよくないのか、それともどこかプログラムの他の部分に問題があるのか・・、それとも全部なのか・・原因の心当たりがありすぎて、今のところ、よくわかりません。
結構、七転び八起き的な苦労もありましたので。
F.recurrent_inputとF.recurrent_outputはエクスポートが使えない
これが、つまづき初めです。
例によって、ニューラルネットワークコンソールから、Pythonコードをエクスポートして、それを修正しながらやる想定で、最初はソースを書きました。
それで実行してみたんです。
ところが、NNablaに存在しないモジュールがある(has no attribute)というエラーがでて学習してくれません。
エラーになるのは2箇所、「F.recurrent_input」と「F.recurrent_output」です。
困ったことに、ニューラルネットワークコンソールのRNNの肝になる部分です。
リファレンスを検索してもヒットしないので、これは現バージョンでは使えないような感じなので、今回はエクスポートはあきらめました。
シンプルRNNをRecurrentInput等を使わないで書く
方向転換して、シンプルRNNをRecurrentInput等を使わないで書くことにします。
幸い、チュートリアルに、シンプルRNNの例がのっていました。
それを参考に組んでみます。
まず、ソースです。
import nnabla as nn
import nnabla.functions as F
import nnabla.parametric_functions as PF
import nnabla.solvers as S
from nnabla.utils.data_iterator import data_iterator_simpleimport numpy as np
import matplotlib.pyplot as pltdef rnn(xs, h0, hidden=16):
hs =
with nn.parameter_scope("rnn"):
h = h0
# 時系列データを繰り返し処理
for x in xs:
# 時系列にしたがってパラメータを再利用するらしい
with nn.parameter_scope("x2h"):
x2h = PF.affine(x, hidden, with_bias=False)
with nn.parameter_scope("h2h"):
h2h = PF.affine(h, hidden)
h = F.tanh(x2h + h2h)
hs.append(h)
with nn.parameter_scope("classifier"):
y = PF.affine(h, 1)
return y
def loss(y, t):
loss = F.reduce_mean(F.squared_error(y, t))
return loss
チュートリアルによると、これでシンプルRNN(elman_net)になっている様です。
時系列データが、「xs」に渡ってくるので、その行数(つまり、時間軸分)繰り返し処理をして、新しいデータ「x」と、前のステップで処理済の「h」・・これが過去の隠れ層にあたるわけですが・・を、Affineレイヤーで処理してくっつける。
このループでRNNを実現しているわけですね。
loss(損失関数)は、回帰問題なので「SquaredError」です。
これはお決まりですね。
学習してみる
次は学習処理です。
ソースを示します。
def training(seq_x,h0,t,data,steps,loss,learning_rate):
solver = S.Sgd(learning_rate)
solver.set_parameters(nn.get_parameters())
for i in range(steps):
bdy,t.d = data.next()
h0.d = 0
for x, subbdy in zip(seq_x, bdy):
x.d = subbdy
loss.forward()
solver.zero_grad()
loss.backward()
solver.update()
if i % 10 == 0:
print(i, loss.d)
x.dに学習データをセットして、loss.forward() したあとに、solver.zero_grad()して、 loss.backward()、solver.update() を行う。
これは、NNablaを使った普通の学習のパターンではあります。
ちなみに、solver.zero_grad()は「バックプロパゲーションを実行する前に、すべてのパラメータのグラディエントバッファをゼロに初期化する処理です。
今回で特殊なのは、loss.forward()の前のループです。
for x, subbdy in zip(seq_x, bdy):
x.d = subbdy
これは一見すると、bdyを分割してsubbdyにしているように見えますが、実は、1回しかまわってません。
なぜなら、zip()は引数であたえた2つのうち、小さい方の回数分処理するからです。
じゃあ、なぜ、こんな形にしているかというと、x.dにsubbdyをセットする方法が他になかったからです。(思いつかなかった・・からです)
ちょっと、トリッキーですけどね。
データを作る処理
Sinデータを作る処理です。
ソースを示します。
def data_load(data,target,batch_size=1, shuffle=False, rng=None):
def load_func(index):
img = data[index]
label = target[index]
return img[None], np.array([label]).astype(np.int32)
return data_iterator_simple(load_func, len(target), batch_size, shuffle, rng, with_file_cache=False)
def sin(x, T=100):
return np.sin(2.0 * np.pi * x / T)
def create_sin_with_noise(T=100, ampl=0.05):
x = np.arange(0, 2 * T + 1)
noise = ampl * np.random.uniform(low=-1.0, high=1.0, size=len(x))
return sin(x) + noise
data_load()は、NNablaでデータを処理するための、DataIteratorに変換するための関数で、これも、ほぼ定形処理のようなものです。
sin()とcretate_sin_with_noise()でノイズ付きのSinカーブデータをつくってますが、これは見たままの処理なので、解説ははぶきます。
さて、メインの処理
最初にソースを示します。
if __name__ == '__main__':
#①データの生成
np.random.seed(0)
T = 100
f = create_sin_with_noise(T)
length_of_sequences = 2 * T
maxlen = 25
data_train =
target_train = []
for i in range(0, length_of_sequences - maxlen + 1):
data_train.append(f[i: i + maxlen])
target_train.append(f[i + maxlen])
X = data_load(data_train,target_train)
Y = data_load(data_train,target_train)#②シンプルRNNモデルを構築して学習する
nn.clear_parameters()
body, label = X.next()
n_hidden = 16
seq_x = [nn.Variable(subbody.shape) for subbody in body]
h0 = nn.Variable*1
y = rnn(seq_x,h0, n_hidden)
t = nn.Variable(label.shape)
loss = loss(y, t)learning_rate = 1e-1
train_step = training(seq_x,h0,t,X,100,loss,learning_rate)#③学習済モデルを用いて評価を行う
predicted = [f[i] for i in range(maxlen)]
for i in range(length_of_sequences - maxlen + 1):
bdy, t.d = Y.next()
for x, subbdy in zip(seq_x, bdy):
x.d = subbdy
y.forward()
# 予測結果でグラフを書くために保存する
predicted.append(y.d.min())#④グラフで可視化
plt.rc('font', family='serif')
plt.figure()
plt.ylim([-1.5, 1.5])
plt.plot(create_sin_with_noise(T), linestyle='dotted', color='red')
plt.plot(predicted, color='black')
plt.show()
以後、例によって簡単に補足説明します。
①データの生成
ここは上に定義した関数を使って、SINデータを生成して、DataIteratorに変換しているだけなので、ほぼ、見たとおりです。
以下で、全体で200回分のSinカーブデータを全体として、時系列データひと単位では、25行1列のデータとして扱うことを指定しています。
length_of_sequences = 2 * T
maxlen = 25
この25という数字は色々なパターンで試してみて、一番結果が良かった数字を採用しています。
最後に全く同じ元データから、X(学習用)とY(評価用)を作っています。
X = data_load(data_train,target_train)
Y = data_load(data_train,target_train)
これは一見、Xを使い回せばよさげに見えるのですが、こうしておかないと、評価時にXが意図する位置から始まらないので、グラフが大きくずれてしまいます。
②シンプルRNNモデルを構築して学習する
ほぼ、チュートリアルに沿った形で、定形パターンに近いです。
nn.clear_parameters() を最初に読んで、パラメータ領域をクリアする必要があるのもお約束です。
例にによって、最初のミニバッチ1回分を、X.next()で取り出して、その情報を使ってネットワークを構築します。
CNNの時と違って、xの定義が、以下のようにする必要がある点だけ注意がいります。
seq_x = [nn.Variable(subbody.shape) for subbody in body]
マジックナンバーのパラメータが2箇所あります。
n_hidden = 16
train_step = training(seq_x,h0,t,X,100,loss,learning_rate)
この16と100も数字を変えると結果が変わります。
何回か試して、一番マシな結果になる数字がこの2つだったというわけです。
RNNは、なかなか繊細で、ちょっとしたパラメータ値の変更で結果のブレが大きくでるような気はしますね。
③学習済モデルを用いて評価を行う
ここは、学習済のモデルを使って評価をして、グラフ表示用の配列に結果をセットしているだけです。
最初のところで、そのグラフ表示用の配列の最初の25行分に元データをセットしています。
これは、学習時に一回X.next()をやって、学習している関係で、予測結果出力がmaxlen*2回めからしか出力されずに、グラフがずれてしまうので、そこをあわせるためにやってます。
predicted = [f[i] for i in range(maxlen)]
あと、グラフ用の結果を保存する部分で、以下のようなコードになってます。
predicted.append(y.d.min())
これも、結果としてでてくるのは0.812345678 みたいな一つだけのデータなのですが、グラフに出すためにスカラにする必要があり、min()かmax()ではきださせるのが一番カンタンなのでやっているだけです。
別に「y.d」に複数の結果が出力されているわけではなので、あとで忘れて思い違いしないように書いておきます。
④グラフで可視化
ここは「matplotlib」で、グラフを表示しているだけです。
比較しやすいように、色と線の形状を変更しています。
まあ、こんな感じです。
TOYプログラムでもあり、あまり精度のよい予測はできなかったですが、一応、時系列データの処理はできているみたいなので、良しとしておこうかな。
関連情報
NNabla(Neural Network Libraries)の関連記事の一覧はこちらです。
ニューラルネットワークコンソールの関連記事の一覧はこちらです。
*1:body.shape[0], n_hidden