ゼロから作るRNN1

機械学習 Python R

ゼロから作るDeep Learning 2のRNN に関するコードを写経、解説していきます。お仕事で使う機会があったけど理解があやふやだったので、忘備録気味です。keras を使う際のパラメーターが何をするパラメーターか解説したいと思います。
今回は、シンプルなRNNそのものについてのお話です。参考書は以下です。

ゼロから作るdeep learning

RNNとは?

大体の解説や、使い方は以下の記事を参考にしてください。

リカレントニューラルネットワーク(RNN)
ニューラルネットワークモデルの一つに、リカレントニューラルネットワーク(RNN)と呼ばれるものがあります。自己相関の高いデータに対して有用なモデルです。RNN, LSTM, GRUの解説をして、映画レビューの分類問題で3つのモデルの特徴を掴みます。

RNNとは、リカレントニューラルネットワーク(Recurrent Neural Network )の略語です。ニューラルネットワークの変種で、データ同士に関係があるという前提のモデルです。RNN層を計算グラフで描くと以下のようになります。

RNN計算グラフ
RNNの計算グラフ

\(x_t \)が入力で\(h_t \)が出力です。大きな特徴は、出力\(h_t \)を入力 \( x_{t+1 }\)に加える事です。過去の情報を未来へ伝えていくイメージです。RNN層はそれ自体がモデルのような雰囲気がありますが、あくまで1つの層です。

RNNの実装

上に載せた計算グラフのようなモデルをpython で実装します。ニューラルネットワークとの違いは、活性化関数が\( \tanh \)である点と、過去の出力 \(h_{t-1} \)も全結合されている点です。誤差逆伝搬で必要な\( \tanh(x) =\frac{\sinh (x)}{\cosh (x) } \) の微分は以下のように計算出来ます。
$$\begin{eqnarray}
\tanh ^{,} (x)&=& 1- \sinh (x) \frac{ \sinh (x) }{\cosh ^2 (x) }\\
&=& 1-\tanh ^2 (x)
\end{eqnarray} $$
RNN層の時刻\(t \)の出力は以下の形をしている事に注意1 しましょう。
$$\begin{eqnarray}
h_t = \tanh \left(x_t W_{x_t} +h_{t-1} W_{t-1} +b_t \right)
\end{eqnarray} $$
それぞれの変数での微分は以下のようになります。
$$\begin{eqnarray}
\frac{\partial h_t}{\partial W_x} &=& \tanh ^{,} (x) \frac{\partial \left( x W_{x} +h W_{h} +b \right) }{\partial W_x} \\
&=& \tanh ^{,} (x) *x^{T} \\
\nabla _{x}h_t&=& \tanh ^{,}(x) \nabla _{x} \left( x W_{x} +h W_{h} +b \right) \\
&=& \tanh ^{,} (x) W_x ^{T} \\
\nabla _{b} h_t&=& \tanh ^{,}(x) \nabla _{b}\left( x W_{x} +h W_{h} +b \right) \\
&=&\sum \tanh ^{,}(x)
\end{eqnarray} $$
それではRNN層を実装します。ニューラルネットワークと殆ど同じ2 なので簡単です。

import numpy as np 

class RNN:
    def __init__(self, Wx, Wh, b):
        self.params =[Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.cache = None
        
    def forward(self, x, h_prev):
        Wx, Wh, b = self.params
        t = np.dot(h_prev, Wh) + np.dot(x, Wx) + b
        h_next = np.tanh(t)
        
        self.cache = (x, h_prev,h_next)
        return h_next
        
    def backward(self, dh_next):
        Wx, Wh, b = self.params
        x, h_prev,h_next = self.cache 
        
        dt = dh_next * (1- h_next **2)
        db = np.sum(dt , axis= 0 )
        dWh = np.dot(h_prev.T ,dt )
        dh_prev = np.dot(dt, Wh.T)
        dWx = np.dot(x.T, dt)
        dx = np.dot(dt,Wx.T)
        
        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db
        return dx, dh_prev

これでRNN層が実装出来ます。

ニューラルネットワークでも同様ですが、誤差逆伝搬では、後ろにある層から微分流れてきます。式で書くと以下のような状況になっています。本当の出力を\(O \)で表します。
$$\begin{eqnarray}
\nabla _{x} O= dh_{next}\nabla _{x} \tanh \left(x W_{x} +h W_{h} +b \right)
\end{eqnarray} $$
\(dh_{next} \)には色々な微分係数が入っています。

実際に使う際3には、勾配消失が起きる事を進歩居してバッチ学習をします。次は、バッチ学習が可能なRNNを実装しましょう。

バッチ学習用のRNN

例えば、\(t \)が\( 0\)から\(100000 \)まであるようなデータ\(x_t \)を一回で学習しようとすると、勾配が\(t=0 \)まで伝わる前に\( 0\)になってしまい、前半のデータの情報を学習に取り込むことが出来ません。
そこで、例えば\( t=10 \)毎にデータを区切って、それぞれに対して学習をします。ただし、区切るのは微分の情報だけで、順方向の情報はそのまま流します。
例えば、\(t=1000\)のデータで, 100個毎に区切り、バッチ数を2とすると、データを800個に分ける事になります。
$$\begin{eqnarray}
( x_0 , \cdots , x_9 ) \\
(x_{100} , \cdots, x_{109}) \\
\vdots\\
( x_{800} , \cdots , x_{809} ) \\
(x_{900} , \cdots, x_{999}) \\
\end{eqnarray}$$
この手法を TBTT(Truncated Backpropagation Through Time)と呼んだりします。
これをTimeRNN というクラスで実装します。

class TimeRNN:
    def __init__(self, Wx, Wh, b):
        self.params =[Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.layers = None
    
        self.h = None 
        self.dh = None
        
    def forward(self, xs):
        Wx, Wh, b = self.params
        N, T, D = xs.shape
        D, H = Wx.shape
        
        self.layers =[] #RNNたちのいれもの
        hs = np.empty((N,T,H), dtype = "f")
        self.h = np.zeros((N, H), dtype='f')
            
        for t in range(T):
            layer =RNN(*self.params)
            self.h = layer.forward(xs[:,t,:], self.h)
            hs[:,t,:] =self.h
            self.layers.append(layer)
            
        return hs
        
    def backward(self, dhs):
        Wx, Wh, b = self.params
        N, T, D = dhs.shape
        D, H = Wx.shape 
        
        dxs = np.empty((N,T,D), dtype="f")
        dh = 0
        grads =[0, 0, 0]
        for t in range(T):
            layer =self.layers[t]
            dx, dh = layer.backward(dhs[:,t,:] + dh)#バッチ内で未来から勾配が流れてくるのでdhを足す
            dxs[:,t,:] = dx
            
            for i, grad in enumerate(layer.grads):
                grads[i]+= grad #データが沢山あるので、それぞれの和がレイヤーとしての勾配になる
                
        for i, grad in enumerate(grads):
            self.grads[i][...] = grad
        
        return dxs

誤差逆伝搬が1つのバッチで完結していることに注目です。

次回の記事はこちら。
https://masamunetogetoge.com/makernn2

まとめ

  • RNNの概要を説明した
  • シンプルなRNN層を実装した
  • 勾配消失を防ぐためのバッチ学習に対応したRNNを実装した
  1. 出てくる文字は出力と大文字以外はベクトルです。
  2. ニューラルネットワークの実装はこちらからどうぞhttps://masamunetogetoge.com/make-neuralnet-1
  3. そもそもこのようなシンプルなRNNは使われませんが
タイトルとURLをコピーしました