アラカン"BOKU"のITな日常

文系システムエンジニアの”BOKU”が勉強したこと、経験したこと、日々思うことを書いてます。

シンプルRNNで予測みたいなことをやってみる:NNabla(Neural Network Libraries)バージョン

ニューラルネットワークコンソールで、シンプルRNNで予測みたいなことをやってみました。

arakan-pgm-ai.hatenablog.com

 

今度は、それと同じようなことを、NNabla(Neural Network Libraries)でもやってみようかなと思います。  

ただ、全く同じではなく、学習・評価用のSINカーブは、ニューラルネットワークコンソールで使ったCSVファイルをそのまま使うのではなくて、プログラムで生成させるように変えてます。 

理由は、ニューラルネットワークコンソールで作ったデータは、あまりにきれいすぎたので、インンプットデータにノイズを入れて、ちょっと難易度をあげてみようかなと思ったからです。 

さて、やってみます。

 

先に結果を示しておきます。

 

こんな感じのグラフになりました。 

正直、すごい微妙です。

f:id:arakan_no_boku:20171015222504j:plain

 

赤線が正解で、黒線が予測結果なんですけどね。 

なんとなくあっているような、外れているような・・(笑) 

こういう微妙な結果になった原因が、ノイズを加えたためか、自分の組んだシンプル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_simple

import numpy as np
import matplotlib.pyplot as plt

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

 

この16100も数字を変えると結果が変わります。 

何回か試して、一番マシな結果になる数字がこの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)の関連記事の一覧はこちらです。

arakan-pgm-ai.hatenablog.com

 

ニューラルネットワークコンソールの関連記事の一覧はこちらです。

arakan-pgm-ai.hatenablog.com

f:id:arakan_no_boku:20170910161122j:plain

*1:body.shape[0], n_hidden