技術は使ってなんぼ

自分が得たものを誰かの役に立てたい

PyTorchで作るRNN 前編

あけましておめでとうございます。


本年も変わらず技術ブログを書いていきますので、よろしくお願いします。


さて前回のブログでTransformerを理解したいという目標を話しました。


で、論文やらqiitaを読んで理解しようとしましたが・・・


よくわかりませんでした。。


そもそも時系列データの分析や予測をやったことがないので、イメージしづらいんですよね。


なので、Transformerを理解するために、段階的な理解を進めていこうと思います。


今回は時系列といえば最初にぶち当たるであろう「RNN」をPyTorchを使って実装し、理解を深めたいと思います。

RNNとは?

まず唐突に出てきた「RNN」についてざっくり説明します。


RNNとは「Recurrent Neural Net」の略で、直訳すると「再帰ニューラルネット」となります。


基本的にはニューラルネットワークを用いた予測モデルなのですが、通常のニューラルネットワークと異なるのは、一つ前のデータを入力データとして継承するところです。


具体的には下図のようなイメージです。

RNN構造概要


時系列データというものは、ある時間の経過とともに値が変化していくデータなので、ある時刻の過去の情報を反映させて学習させるという再帰的なアルゴリズムが、RNNの特徴であり強みとなります。


RNNはある時刻の一つ前の情報を反映しますが、ある程度まとまった期間で反映させるモデルのことをLSTM(Long Short Term Memory)といいます。


こちらはまた別の機会に紹介しようと思います。

ところで、なんでPyTorch?

理由は大きく3つです。

  • フレームワークの中でも割と泥臭い書き方で、中でどういう処理やってるのかがわかりやすい。
  • 数年前はTensorflowが主流でしたが、ここ数年でユーザー数が増えている。

  • 私の好み。(TensorflowやKerasはあっさりしすぎて不安になる)


ちなみに本実装ではPyTorch標準のRNNモジュール(torch.nn.RNN)を使わずに実装していきます。


理由は

  • 理解を深めるため、ある程度泥臭くやってみたい
  • RNN内をオリジナルにカスタマイズできるようにしておきたい


2の具体例をあげると、例えば活性化関数ですが、PyTorch標準のRNNを使うと現状tanhかReLUしか選べません。


tanhは指数関数を使用するため、演算処理が重いですが、hardtanhという指数関数を使わずに直線式で近似する手法があります。


tanhをhardtanhに切り替えて処理負荷を減らしたいと思っても、現状のPyTorchでは実装できないわけです。

実装①


タイトルにもある通り、実装内容を全て書くと大変なボリュームになってしまうことがわかったので分割してアップしていこうと思います。


本ブログではモデルの実装内容だけを書き、次回のブログで実行処理内容と実行結果を書こうと思います。


というわけで実装内容はこちらです。

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class RNNHardCell(nn.Module):
    def __init__(self, n_input:int, n_hidden:int, state=None) -> None:
        super(RNNHardCell, self).__init__()
        self.n_input = n_input
        self.n_hidden = n_hidden
        self.in_h = nn.Linear(self.n_input, self.n_hidden, bias=True)
        self.h_h = nn.Linear(self.n_hidden, self.n_hidden, bias=True)
        self.state = state
        self.register_parameter()

    def register_parameter(self) -> None:
        stdv = 1.0 / math.sqrt(self.n_hidden)
        for weight in self.parameters():
            nn.init.uniform_(weight, -stdv, stdv)
    
    def forward(self, x, state=None):
        self.state = state
        if self.state is None:
            self.state = F.hardtanh(self.in_h(x))
        else:
            self.state = F.hardtanh(self.in_h(x) + self.h_h(self.state))
        return self.state

実装の解説①

ではざっくり解説していきます。

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class RNNHardCell(nn.Module):
    def __init__(self, n_input:int, n_hidden:int, state=None) -> None:
        super(RNNHardCell, self).__init__()
        self.n_input = n_input
        self.n_hidden = n_hidden
        self.in_h = nn.Linear(self.n_input, self.n_hidden, bias=True)
        self.h_h = nn.Linear(self.n_hidden, self.n_hidden, bias=True)
        self.state = state
        self.register_parameter()

まずはRNNの構造を作っていきます。


PyTorchの書き方については説明しませんので、ご存知ない方は「こういう書き方をするんだ」ぐらいに納得してください。


__init__では引数にインプットの数(n_input)と隠れ層の数(n_hidden)を設定します。stateについては後々説明します。


nn.Linearがニューラルネットの線形回帰(y=ax+b)で、入出力の引数にn_inputとn_hiddenを渡します。


biasというのがy=ax+bのbになります。Falseを入れるとy=axとして計算することになります。

    def register_parameter(self) -> None:
        stdv = 1.0 / math.sqrt(self.n_hidden)
        for weight in self.parameters():
            nn.init.uniform_(weight, -stdv, stdv)
    
    def forward(self, x, state=None):
        self.state = state
        if self.state is None:
            self.state = F.hardtanh(self.in_h(x))
        else:
            self.state = F.hardtanh(self.in_h(x) + self.h_h(self.state))
        return self.state

register_parameter関数は__init__時に呼び出していましたが、構築したモデルのニューラルネットワークの重み(parameter)を初期化しています。


初期化の仕方が標準偏差に基づいた分布内でばらつくように作成しています。


そしてforward関数にて__init__で作成したLinear関数を使い、活性化関数であるhardtanhに通すような処理をしています。


ここでstateですが、「前回の出力結果」を格納するものになります。


一番最初に実行する時はNoneですので、if文のルートを通ります。


で2回目以降には格納されているので、else文を通り、再帰的な演算(+self.h_h(self.state)))をします。


この計算式については論文でもネットでも検索したら出てくるので、理解を深めたい方は調べてみてください。


これでRNNのモデルの構築と、必要な機能の実装は完了です。


それでは次回以降に続きをやっていきます。