PyTorch (16) 文字レベルRNNで人名の分類
前回からずいぶん時間が空いてしまった (^^;) 今回からテキストや音声など系列データを扱う手法に進んでいこうと思っています。
最初のお題として文字レベルのRecurrent Neural Network (RNN) を試しました。PyTorchチュートリアルの Classifying Names with a Character-Level RNN です。
このチュートリアルは、人名から国籍を推定するというタスクです。データとして数千の人名を18の国籍に分類したデータが提供されています。たとえば、Hintonさんだったらスコットランド人、Schmidhuberさんだったらドイツ人、Moriさんだったら日本人という感じ。
RNNなどの系列データを扱うニューラルネットでは、系列の要素として文字、単語、n-gramなどいろいろ取りえますが、今回は文字です。つまり、人名を文字の系列データとみなします。
このチュートリアルは、PyTorchの nn.RNN
モジュールを使わずにスクラッチでシンプルなRNNを実装していますが、それに加えて、nn.RNN
や nn.LSTM
を使った実装も試してみました!
最近、Jupyter NotebookをGoogleのクラウド上で提供しているGoogle Colabにハマっていて、今回もそれで実験しています。このサービスは連続12時間までならGPUも無料で使えるのでこのブログで取り扱っている内容くらいなら十分使えます。Googleアカウントを持っていれば誰でも使えるのでおすすめ!
180323-character-level-rnn.ipynb - Google ドライブ
あと今回から記事にコード載せて解説するスタイル止めようと思います。元記事の説明の方が詳しいし、面倒くさくなったので(ボソ)。最近は、Deep Learning流行ってるせいですごく詳しく説明した記事もたくさんありますしね。
日付みるとわかるように3/23に実験してブログ書くの面倒くさくなって1ヶ月放置してました (^^;)
RNNの構造の違い
このチュートリアルで気になったのはRNNの構造です。
出力された隠れ状態を入力に戻すというのは一般的なRNNだと思うのですが、inputとhiddenをマージしてから次の隠れ状態や出力を求める というのがちょっと違和感を感じました。
nn.RNN
の実装だと下のように入力は i2h()
(w_ihとb_ih)で処理して、前の隠れ状態は h2h()
(w_hhとb_hh)で処理してそれを足し算して次の隠れ状態を求めるというのが一般的な実装だと思ってたので(出力はさらに h2o()
が入る)。
w_ihとw_hhの行列を一つにまとめただけで理論的には同じことかな?
http://pytorch.org/docs/stable/nn.html#torch.nn.RNN
今回のチュートリアルに合わせてテンソルサイズなどを図示してみました。
系列データの分類を行う場合は、系列を全部入れ終わったあとに最後の出力のみを使うmany to one型が一般的です。
The Unreasonable Effectiveness of Recurrent Neural Networks
nn.RNN
を使った実装
PyTorchのモジュール nn.RNN
を使うと下のようになります。nn.RNN
を使うと系列の各要素を順番に処理するforループを自分で書く必要がなくなるのですごく楽になる!
class SimpleRNN(nn.Module): def __init__(self, input_size, hidden_size, output_size): super(SimpleRNN, self).__init__() self.hidden_size = hidden_size self.rnn = nn.RNN(input_size, hidden_size, num_layers=1) self.out = nn.Linear(hidden_size, output_size) self.softmax = nn.LogSoftmax(dim=2) # outは3Dtensorになるのでdim=2 def forward(self, input, hidden): # nn.RNNは系列をまとめて処理できる # outputは系列の各要素を入れたときの出力 # hiddenは最後の隠れ状態(=最後の出力) output[-1] == hidden output, hidden = self.rnn(input, hidden) # RNNの出力がoutput_sizeになるようにLinearに通す output = self.out(output) # 活性化関数 output = self.softmax(output) return output, hidden def initHidden(self): # 最初に入力する隠れ状態を初期化 # (num_layers, batch, hidden_size) return Variable(torch.zeros(1, 1, self.hidden_size))
今回はミニバッチサイズ1としています。つまり、系列(人名)は1つずつRNNに入力しています。系列長(人名の長さ)が異なった複数の系列データをミニバッチとしてまとめてRNNに入力する方法は調査中!また取り上げたい。
nn.LSTM
を使った実装
LSTMの場合は、内部状態としてhに加えてcというのが増えるだけであとは大体同じ。 (h, c)
の渡し方は人によって書き方が違うけどどうやるのがよいだろう?タプルでまとめるのがよいのかな・・・
class SimpleLSTM(nn.Module): def __init__(self, input_size, hidden_size, output_size): super(SimpleLSTM, self).__init__() self.hidden_size = hidden_size self.lstm = nn.LSTM(input_size, hidden_size, num_layers=1) self.out = nn.Linear(hidden_size, output_size) self.softmax = nn.LogSoftmax(dim=2) # outは3Dtensorになるのでdim=2 def forward(self, input, h, c): # nn.RNNは系列をまとめて処理できる # outputは系列の各要素を入れたときの出力 # hiddenは最後の隠れ状態(=最後の出力) output[-1] == hidden[0] output, (h, c) = self.lstm(input, (h, c)) # RNNの出力がoutput_sizeになるようにLinearに通す output = self.out(output) # 活性化関数 output = self.softmax(output) return output, (h, c) def initHidden(self): # 最初に入力する隠れ状態を初期化 # LSTMの場合は (h, c) と2つある # (num_layers, batch, hidden_size) h = Variable(torch.zeros(1, 1, self.hidden_size)) c = Variable(torch.zeros(1, 1, self.hidden_size)) if cuda: h = h.cuda() c = c.cuda() return (h, c)
LSTMは学習が難しいみたいでSGDだとあまりうまくいかなかった。Adamに変えたらRNNよりlossが減ったみたい。ただこれは訓練lossなので過学習してるかも(笑)
ためしに自分の名前で予測してみました。
> Mori (-0.11) Japanese (-2.37) Italian (-5.35) Russian > Koichiro (-0.13) Japanese (-3.53) Italian (-3.61) English
Moriは訓練データに入ってるけどKoichiroは入っていません。どちらも正解していた!