Recurrent Neural Networks (2) – Python, Numpy と Theano による RNN の実装(翻訳/要約)
* Recurrent Neural Networks Tutorial, Part 2 – Implementing a RNN with Python, Numpy and Theano の簡単な要約です。
* 画像図の引用はしませんので、原文を参照しながらお読みください。
Code to follow along is on Github.
RNN 全部をスクラッチから実装します。Python を使用して(GPU 上で演算を実行できるライブラリ)Theano で実装を最適化します。
言語モデル
目標は RNN を使用して言語モデルを構築することです。これはこういう意味です。 単語の文があるとしましょう。言語モデルは文を観測する確率を次のように予測することを可能にします :
言葉で言うならば、文の確率はそれより前に来る単語群が与えられた時の各単語の確率の生産物です。従って、文 “He went to buy some chocolate” の確率は “He went to buy some” が与えられた時の “chocolate” の確率になり、(“He went to buy some” は)“He went to buy” が与えられた時の “some” の確率を乗算したもので…等々。
何故それが有用なのでしょうか?文の観測に確率を割り当てることを何故望むのでしょうか?
まず、そのようなモデルはスコアリング・メカニズムとして使用できます。例えば、機械翻訳システムは典型的には入力文に対して複数の候補を生成します。 最もありそうな文を選択するために言語モデルを使用できるでしょう。直感的には、(確率的に)最もありそうな文は文法的にも正しいことがありがちです。同様なスコアリングは音声認識システムでも起きるでしょう。
しかし言語モデル問題の解決はまた cool な副次効果もあります。先行する単語群が与えられた時の単語の確率を予測できるのですから、新しいテキストを生成することも可能です。それは 生成モデル です。
単語群の既存の文が与えられた時、予測された確率から次の単語をサンプリングしてこのプロセスを完全な文を得るまで繰り返します。Andrej Karparthy による RNN の有効性 は言語モデルができることを示します。彼のモデルは full の単語群に対して単一の文字上で訓練され、Shakespeare から Linux コードまで何でも生成できます。
上の等式において、各単語の確率は全ての前の単語上と条件付けられている点に注意してください。実践的には、多くのモデルは計算上のあるいはメモリ制約のためにそのような long-term 依存を表現することに苦戦しています。それらは典型的には2、3の前の単語のみを参照することに制限されています。RNN は、理論的には、そのような long-term 依存を捕捉できますが、実践的にはもう少し複雑です。
データの訓練と前処理
私たちの言語モデルを訓練するためにはそこから学習するためのテキストが必要です。幸いなことに言語モデルを訓練するためのラベルは不要で、生テキストのみが必要です。15,000 の長めの reddit コメントを dataset available on Google’s BigQuery からダウンロードしました。私たちのモデルにより生成されたテキストは reddit コメンテーターっぽいでしょう(願わくば)!しかし多くの機械学習プロジェクトのようにデータを適正なフォーマットにするための幾つかの前処理が最初に必要です。
1. テキストをトークン化する
生テキストがありますが、単語毎ベースで予測することを望みます。これはコメントを文に、文を単語にトークン化しなければならないことを意味しています。スペース(空白)で各コメントを分割することもできますが、それは句読点を正しく扱えないでしょう。文 “He left!” は 3 トークンであるべきです: “He”, “left”, “!” 。NLTK の word_tokenize と sent_tokenize メソッドを使用します。
2. 稀な単語を取り除く
テキストの殆どの単語は1回か2回現れるだけです。これらの稀な単語を取り除くことは良い考えです。大規模な語彙を持つことはモデルの訓練を遅くし、そしてそのような単語のための多くの文脈上のサンプルを持たないために、それらを正しく使うことを学習することができないでしょう。これは人間の学習方法と良く似ています。単語を適正に使用する方法を実際に理解するためには異なるコンテキストでそれを見る必要があります。
私たちのコードでは語彙を vocabulary_size 最も一般的な単語に制限します。(8000 に設定しましたが、自由に変更してください。)語彙に含まれていない全ての単語は UNKNOWN_TOKEN で置き換えます。例えば、単語 “nonlinearities” が語彙に含まれていないならば、文 “nonlineraties are important in neural networks” は “UNKNOWN_TOKEN are important in Neural Networks” となります。単語 UNKNOWN_TOKEN は語彙の一部となり他の単語と同様に予測します。新しいテキストを生成する時に UNKNOWN_TOKEN を再度置換できます、例えばランダムにサンプリングされた語彙にない単語を選ぶか、あるいは unknown token を含まない文を得るまで文を生成することもできるでしょう。
3. Prepend special start and end tokens
またどの単語が文の start と end になる傾向があるかも学習したいです。これをするために各文に特殊な SENTENCE_START トークンを先頭に追加して、特殊な SENTENCE_END トークンを追加します。最初のトークンが SENTENCE_START であるとして、次の単語は何になりがちでしょう(文の実際の最初の単語)?
4. 訓練データ行列の構築
RNN への入力はベクトルであり、文字列ではありません。そのため、単語とインデックス間のマッピングを作成します、index_to_word と word_to_index です。例えば、単語 “friendly” は index 2001 かもしれません。訓練サンプル x は [0, 179, 341, 416] のように見えるかもしれません、ここで 0 は SENTENCE_START に相当します。該当ラベル y は [179, 341, 416, 1] のようなものです。私たちの目標は次の単語を予測することを思い出してください、従って y は、最後の要素が SENTENCE_END トークンである、一つ位置をシフトしたベクトル x です。換言すれば、上の単語 179 への正しい予測は 341 になります、実際の次の単語です。
vocabulary_size = 8000 unknown_token = "UNKNOWN_TOKEN" sentence_start_token = "SENTENCE_START" sentence_end_token = "SENTENCE_END" # データを読み、SENTENCE_START と SENTENCE_END を追加します。 print "Reading CSV file..." with open('data/reddit-comments-2015-08.csv', 'rb') as f: reader = csv.reader(f, skipinitialspace=True) reader.next() # full コメントを文群に分割します。 sentences = itertools.chain(*[nltk.sent_tokenize(x[0].decode('utf-8').lower()) for x in reader]) # SENTENCE_START と SENTENCE_END を追加します。 sentences = ["%s %s %s" % (sentence_start_token, x, sentence_end_token) for x in sentences] print "Parsed %d sentences." % (len(sentences)) # 文を単語にトークン化します。 tokenized_sentences = [nltk.word_tokenize(sent) for sent in sentences] # 単語の頻度をカウントします。 word_freq = nltk.FreqDist(itertools.chain(*tokenized_sentences)) print "Found %d unique words tokens." % len(word_freq.items()) # 最も一般的な単語群を得て、index_to_word と word_to_index ベクトルを構築します。 vocab = word_freq.most_common(vocabulary_size-1) index_to_word = [x[0] for x in vocab] index_to_word.append(unknown_token) word_to_index = dict([(w,i) for i,w in enumerate(index_to_word)]) print "Using vocabulary size %d." % vocabulary_size print "The least frequent word in our vocabulary is '%s' and appeared %d times." % (vocab[-1][0], vocab[-1][1]) # 語彙になり全ての単語を unknown token で置換します。 for i, sent in enumerate(tokenized_sentences): tokenized_sentences[i] = [w if w in word_to_index else unknown_token for w in sent] print "\nExample sentence: '%s'" % sentences[0] print "\nExample sentence after Pre-processing: '%s'" % tokenized_sentences[0] # 訓練データを作成します。 X_train = np.asarray([[word_to_index[w] for w in sent[:-1]] for sent in tokenized_sentences]) y_train = np.asarray([[word_to_index[w] for w in sent[1:]] for sent in tokenized_sentences])
ここにテキストからの実際の訓練サンプルを示します :
x: SENTENCE_START what are n't you understanding about this ? ! [0, 51, 27, 16, 10, 856, 53, 25, 34, 69] y: what are n't you understanding about this ? ! SENTENCE_END [51, 27, 16, 10, 856, 53, 25, 34, 69, 1]
RNN を構築する
言語モデルのための RNN がどのようなものか見てみましょう。
入力 は単語のシーケンスで各
は単一の単語です。しかしもう一つあります: 行列乗算であるため、入力として単語 index (like 36) を単純には使用できません。代わりに、各単語をサイズ vocabulary_size の one-hot ベクトルで表します。例えば、index 36 の単語は全て 0 で位置 36 が 1 のベクトルになるでしょう。従って、各
はベクトルとなり、
は各行が単語を表す行列です。この変換を前処理で行う代わりに、NN コードで遂行します。ネットワークの出力
も同様のフォーマットです。各
は vocabulary_size 要素のベクトルで各要素はその単語が文の次の単語となる確率を表します。
RNN のための等式です :
行列とベクトルの次元を書き出すことは有用です。語彙サイズ と隠れ層サイズ
をすることを仮定しましょう。隠れ層サイズをネットワークの “メモリ” と考えて良いです。それを大きくすることはより複雑なパターンの学習を可能にしますが、追加の計算もまた引き起こします。従って :
これは価値ある情報です。 と
はデータから学習したい、ネットワークのパラメータであることを思い出してください。従って、総計
パラメータを学習する必要があります。
と
の場合にはそれは 1,610,000 になります。
次元はまたモデルのボトルネックも教えてくれます。 は one-hot ベクトルなので、それを
で乗算することは U のカラムを選択することと本質的に同じなので、フルに乗算を遂行する必要はありません。そしてネットワークの最大の行列乗算は
になります。それが語彙サイズをできれば小さくした理由です。
初期化
RNN クラスを宣言してパラメータの初期化をすることから始めます。このクラスを RNNNumpy と呼びます、それは後で Theano 版を実装するからです。パラメータ と
の初期化は少しばかり技巧的です。0 で初期化することはできません、何故ならそれは層全部で対称計算 (symmetric calculations) を引き起こす結果になるからです。(訳注: i.e. 上手く学習できない。)それらをランダムに初期化しなければなりません。適正な初期化は訓練結果にインパクトがあるようですのでこの分野では沢山の研究があります。最良の初期化は活性化関数(私たちの場合は
)に依存することは判明していて、一つの 推奨される アプローチは重みを
からの間隔においてランダムに初期化することです。ここで n は前の層からの入ってくる接続 (incoming connections) の数です。これは非常に複雑に見えるかもしれませんが、必要以上にそれを心配しないでください。パラメータを小さなランダム値で初期化する限りは、通常は上手く動作します。
class RNNNumpy: def __init__(self, word_dim, hidden_dim=100, bptt_truncate=4): # インスタンス値を割り当てます self.word_dim = word_dim self.hidden_dim = hidden_dim self.bptt_truncate = bptt_truncate # ネットワーク・パラメータをランダムに初期化 self.U = np.random.uniform(-np.sqrt(1./word_dim), np.sqrt(1./word_dim), (hidden_dim, word_dim)) self.V = np.random.uniform(-np.sqrt(1./hidden_dim), np.sqrt(1./hidden_dim), (word_dim, hidden_dim)) self.W = np.random.uniform(-np.sqrt(1./hidden_dim), np.sqrt(1./hidden_dim), (hidden_dim, hidden_dim))
上で、word_dim は語彙のサイズで、hidden_dim は隠れ層のサイズです。(選択可能です。)bptt_truncate パラメータについては今のところは心配しないでください、それが何かは後で説明します。
フォワード・プロパゲーション (Forward Propagation)
次に、上述の等式で定義される(単語の確率を予測する)forward propagation を実装しましょう。
def forward_propagation(self, x): # 時間ステップの総数 T = len(x) # forward propagation の間、全ての隠れ状態は s に保存します、何故なら後で必要になるので。 # 初期隠れ (initial hidden) のための追加要素を追加します、これは 0 に設定します。 s = np.zeros((T + 1, self.hidden_dim)) s[-1] = np.zeros(self.hidden_dim) # 各時間ステップにおける出力。再度、後のために保存します。 o = np.zeros((T, self.word_dim)) # 各時間ステップのために… for t in np.arange(T): # U を x[t] でインデックスしていることに注意してください。 # これは U に one-hot ベクトルを乗算することと同じです。 s[t] = np.tanh(self.U[:,x[t]] + self.W.dot(s[t-1])) o[t] = softmax(self.V.dot(s[t])) return [o, s] RNNNumpy.forward_propagation = forward_propagation
計算された出力だけでなく、隠れ状態も返します。それらは勾配を計算するために後で使います、そしてそれらをここで返すことにより計算の重複を回避します。各 は語彙の単語を表す確率のベクトルです。しかし時に、例えばモデルを評価する時に、望むことは最も高い確率の次の単語です。関数 predict を呼び出します :
def predict(self, x): # forward propagation を遂行して最も高いスコアの index を返します。 o, s = self.forward_propagation(x) return np.argmax(o, axis=1) RNNNumpy.predict = predict
新たに実装したメソッドを試してサンプル出力を見てみましょう :
np.random.seed(10) model = RNNNumpy(vocabulary_size) o, s = model.forward_propagation(X_train[10]) print o.shape print o
(45, 8000) [[ 0.00012408 0.0001244 0.00012603 ..., 0.00012515 0.00012488 0.00012508] [ 0.00012536 0.00012582 0.00012436 ..., 0.00012482 0.00012456 0.00012451] [ 0.00012387 0.0001252 0.00012474 ..., 0.00012559 0.00012588 0.00012551] ..., [ 0.00012414 0.00012455 0.0001252 ..., 0.00012487 0.00012494 0.0001263 ] [ 0.0001252 0.00012393 0.00012509 ..., 0.00012407 0.00012578 0.00012502] [ 0.00012472 0.0001253 0.00012487 ..., 0.00012463 0.00012536 0.00012665]]
(訳注: 動作確認済み、コンソール出力一致。)
文の各単語のために(上では 45)、モデルは次の単語の確率を表す 8000 の予測を作成しました。 をランダム値に初期化したので、現時点でこれらの予測は完全にランダムであることに注意してください。次は、各単語のための最も高い確率予測の index を当てます :
predictions = model.predict(X_train[10]) print predictions.shape print predictions
(45,) [1284 5221 7653 7430 1013 3562 7366 4860 2212 6601 7299 4556 2481 238 2539 21 6548 261 1780 2005 1810 5376 4146 477 7051 4832 4991 897 3485 21 7291 2007 6006 760 4864 2182 6569 2800 2752 6821 4437 7021 7875 6912 3575]
(訳注: 動作確認済み、コンソール出力一致。)
損失を計算する
ネットワークを訓練するためにはそれが起こすエラーを計測する方法が必要です。これを損失関数 と呼び、目標は、訓練データのための損失関数を最小化するパラメータ
と
を見つけることです。損失関数に対する一般的な選択は 交差エントロピー損失 です。もし
訓練サンプル(テキストの単語)と
クラス(語彙のサイズ)を持つ場合、予測
と真のラベル
に関連する損失は次で与えられます :
式は少し複雑に見えますが、実際にそれが行っていることは訓練サンプルに渡って合計して予測がどれだけ間違っているかをベースにして損失に加算しているだけです。(正しい単語)と
(予測)が離れれば離れるほど、損失が大きくなっていきます。関数 calculate_loss を実装します :
def calculate_total_loss(self, x, y): L = 0 # 各文に対して… for i in np.arange(len(y)): o, s = self.forward_propagation(x[i]) # 「正しい」単語の予測だけに注意を払います。 correct_word_predictions = o[np.arange(len(y[i])), y[i]] # どれだけ間違えたかをベースに損失に加算します。 L += -1 * np.sum(np.log(correct_word_predictions)) return L def calculate_loss(self, x, y): # 訓練サンプルの数で損失合計を除算する N = np.sum((len(y_i) for y_i in y)) return self.calculate_total_loss(x,y)/N RNNNumpy.calculate_total_loss = calculate_total_loss RNNNumpy.calculate_loss = calculate_loss
ステップバックしてランダムな予測のための損失とは何かを考えてみましょう。それはベースラインを与えてくれて実装が正しいことを確かにしてくれます。語彙に 単語を持ちますから、各単語は(平均して)確率
で予測されます、これは
の損失を生むでしょう :
# 時間の節約のために 1000 サンプルに制限します。 print "Expected Loss for random predictions: %f" % np.log(vocabulary_size) print "Actual loss: %f" % model.calculate_loss(X_train[:1000], y_train[:1000])
Expected Loss for random predictions: 8.987197 Actual loss: 8.987440
(訳注: 動作確認済み、コンソール出力一致。)
非常に近いです! フル・データセット上の損失を評価することは高コストな演算で、沢山のデータを持つならば数時間かかるかもしれません。
SGD と Backpropagation Through Time (BPTT) により RNN を訓練する
訓練データ上の合計損失を最小化するパラメータ と
を見つけることを望んでいることを思い出してください。これを行なう最も一般的な方法は SGD (Stochastic Gradient Descent) – 確率的勾配降下法です。SGD の裏にある考えは非常に簡単です。訓練サンプルに渡って反復して、そして各反復においてエラーを減少させる方向にパラメータを少し押してやります。これらの方向は損失上の勾配で与えられます :
. SGD はまた学習率を必要とします、これは各反復においてどの位の大きさのステップをするかを定義します。SGD は、NN のためだけではなく、多くの他の機械学習アルゴリズムのためにも最も人気のある最適化手法です。それ自体、バッチ、並列性そして適応可能な学習率を使用して SGD をどのように最適化するかについては多くの研究があります。基本的な考え方は単純ですが、実際に効率的な方法で SGD を実装することは非常に複雑です。SGD についてもっと学びたければ これ が始めるに良い地点です。最適化のバックグラウンドさえなくても理解できるはずの SGD の単純版を実装します。
しかし上述したそれらの勾配をどのように計算するのでしょうか?伝統的な NN ではこれを backpropagation アルゴリズムを通して行ないます。RNN では、Backpropagation Through Time (BPTT) と呼ばれる、このアルゴリズムを少し修正した版を使用します。パラメータはネットワークの全ての時間ステップで共有されますので、各出力における勾配は現在の時間ステップの計算上だけではなく、前の時間ステップ群にも依存します。微積分を知っていれば、それは実際に chain rule (合成関数の微分の連鎖律 )を適用するだけです。チュートリアルの次のパートはすべて BPTT についてですので、導関数についてはここでは深入りしません。backpropagation への一般的なイントロは これ と この投稿 をチェックしてください。今のところは BPTT をブラックボックスとして扱ってかまいません。それは訓練サンプル を入力として取り、勾配
を返します。
def bptt(self, x, y): T = len(y) # forward propagation を遂行する o, s = self.forward_propagation(x) # これらの変数の勾配を accumulate する dLdU = np.zeros(self.U.shape) dLdV = np.zeros(self.V.shape) dLdW = np.zeros(self.W.shape) delta_o = o delta_o[np.arange(len(y)), y] -= 1. # For each output backwards... for t in np.arange(T)[::-1]: dLdV += np.outer(delta_o[t], s[t].T) # Initial delta 計算 delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2)) # Backpropagation through time (for at most self.bptt_truncate steps) for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]: # print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step) dLdW += np.outer(delta_t, s[bptt_step-1]) dLdU[:,x[bptt_step]] += delta_t # 次のステップのための delta を更新する delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2) return [dLdU, dLdV, dLdW] RNNNumpy.bptt = bptt
勾配チェック
backpropagation を実装する時はいつでも勾配チェックも実装することは良い考えです、これは貴方の実装が正しいことを検証する方法です。勾配チェックの背後の考えはパラメータの導関数はその点での傾きに等しいことです、これはパラメータを少し変更して変更で除算することにより見積もることができます。
それから backpropagation を使用して計算した勾配と上の方法で見積もった勾配を比較します。大きな違いがなければ良いでしょう。近似は全てのパラメータに対しての合計損失を計算する必要があるので、勾配チェックは非常に高コストです。(忘れないでください、上のサンプルで 100 万以上のパラメータを持ちました。)
そこで小さい語彙のモデルで遂行するのが良いアイデアです。
def gradient_check(self, x, y, h=0.001, error_threshold=0.01): # backpropagation を使用して勾配を計算します。これらが正しいかチェッカーが欲しいです。 bptt_gradients = self.bptt(x, y) # チェックしたいパラメータのリスト。 model_parameters = ['U', 'V', 'W'] # 各パラメータのための勾配チェック for pidx, pname in enumerate(model_parameters): # Get the actual parameter value from the mode, e.g. model.W parameter = operator.attrgetter(pname)(self) print "Performing gradient check for parameter %s with size %d." % (pname, np.prod(parameter.shape)) # Iterate over each element of the parameter matrix, e.g. (0,0), (0,1), ... it = np.nditer(parameter, flags=['multi_index'], op_flags=['readwrite']) while not it.finished: ix = it.multi_index # Save the original value so we can reset it later original_value = parameter[ix] # Estimate the gradient using (f(x+h) - f(x-h))/(2*h) parameter[ix] = original_value + h gradplus = self.calculate_total_loss([x],[y]) parameter[ix] = original_value - h gradminus = self.calculate_total_loss([x],[y]) estimated_gradient = (gradplus - gradminus)/(2*h) # Reset parameter to original value parameter[ix] = original_value # The gradient for this parameter calculated using backpropagation backprop_gradient = bptt_gradients[pidx][ix] # calculate The relative error: (|x - y|/(|x| + |y|)) relative_error = np.abs(backprop_gradient - estimated_gradient)/(np.abs(backprop_gradient) + np.abs(estimated_gradient)) # If the error is to large fail the gradient check if relative_error > error_threshold: print "Gradient Check ERROR: parameter=%s ix=%s" % (pname, ix) print "+h Loss: %f" % gradplus print "-h Loss: %f" % gradminus print "Estimated_gradient: %f" % estimated_gradient print "Backpropagation gradient: %f" % backprop_gradient print "Relative Error: %f" % relative_error return it.iternext() print "Gradient check for parameter %s passed." % (pname) RNNNumpy.gradient_check = gradient_check # To avoid performing millions of expensive calculations we use a smaller vocabulary size for checking. grad_check_vocab_size = 100 np.random.seed(10) model = RNNNumpy(grad_check_vocab_size, 10, bptt_truncate=1000) model.gradient_check([0,1,2,3], [1,2,3,4])
訳注: 実行結果 :
Performing gradient check for parameter U with size 1000. rnn_numpy.py:115: RuntimeWarning: invalid value encountered in double_scalars relative_error = np.abs(backprop_gradient - estimated_gradient)/(np.abs(backprop_gradient) + np.abs(estimated_gradient)) Gradient check for parameter U passed. Performing gradient check for parameter V with size 1000. Gradient check for parameter V passed. Performing gradient check for parameter W with size 100. Gradient check for parameter W passed.
SGD 実装
パラメータに対する勾配を計算できるようになったので
SGD を実装できます :
1. 関数 sdg_step は勾配を計算して一つのバッチに対して更新を遂行します。
2. 外部ループ (outer loop) は訓練セットを通して反復して学習率を調整します。
# SGD の 1 ステップを実行します。 def numpy_sdg_step(self, x, y, learning_rate): # 勾配を計算します。 dLdU, dLdV, dLdW = self.bptt(x, y) # 勾配と学習率に従ってパラメータを変更します。 self.U -= learning_rate * dLdU self.V -= learning_rate * dLdV self.W -= learning_rate * dLdW RNNNumpy.sgd_step = numpy_sdg_step
# Outer SGD ループ # - model: RNN モデル・インスタンス # - X_train: 訓練データ・セット # - y_train: 訓練データ・ラベル # - learning_rate: SGD のための初期学習率 # - nepoch: 完全なデータセットを通して反復するための回数 # - evaluate_loss_after: この多くの epoch 後の損失を評価する def train_with_sgd(model, X_train, y_train, learning_rate=0.005, nepoch=100, evaluate_loss_after=5): # We keep track of the losses so we can plot them later losses = [] num_examples_seen = 0 for epoch in range(nepoch): # Optionally evaluate the loss if (epoch % evaluate_loss_after == 0): loss = model.calculate_loss(X_train, y_train) losses.append((num_examples_seen, loss)) time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') print "%s: Loss after num_examples_seen=%d epoch=%d: %f" % (time, num_examples_seen, epoch, loss) # Adjust the learning rate if loss increases if (len(losses) > 1 and losses[-1][1] > losses[-2][1]): learning_rate = learning_rate * 0.5 print "Setting learning rate to %f" % learning_rate sys.stdout.flush() # For each training example... for i in range(len(y_train)): # One SGD step model.sgd_step(X_train[i], y_train[i], learning_rate) num_examples_seen += 1
Done ! ネットワークを訓練するのにどの程度長くかかるか感じ取ってみましょう :
np.random.seed(10) model = RNNNumpy(vocabulary_size) %timeit model.sgd_step(X_train[10], y_train[10], 0.005)
Uh-oh, bad news. 私のラップトップで SGD の 1 ステップは約 350 ミリ秒かかりました。
訓練データには約 80,000 サンプルがありますから、1 epoch(全てのデータセットに渡る反復)は数時間かかります。複数の epoch は数日あるいは数週間さえかかるでしょう!そして私たちは依然として、多くの企業や研究者がそこで使用しているものと比較して、小さなデータセットで作業しています。What now?
幸いなことにコードをスピードアップする多くの方法があります。同じモデルにこだわってコードをより速く動作することもできますし、あるいは計算上のコストを下げるようにモデルを修正しても良いですし、あるいは両方でも良いです。
研究者はモデルを計算上のコストを下げるような沢山の方法を同定してきました、例えば大規模な行列乗算を回避するために階層 softmax を使用したり射影 (projection) 層を追加することによってです。(これ または これ を参照). しかしモデルを単純なままに保持して最初のルートを進みます: GPU を使用して実装を速くします。けれどもそれを行なう前に、小さなデータセットで SGD を実行して損失が実際に減少することをチェックしてみましょう:
np.random.seed(10) # Train on a small subset of the data to see what happens model = RNNNumpy(vocabulary_size) losses = train_with_sgd(model, X_train[:100], y_train[:100], nepoch=10, evaluate_loss_after=1)
2015-09-30 10:08:19: Loss after num_examples_seen=0 epoch=0: 8.987425 2015-09-30 10:08:35: Loss after num_examples_seen=100 epoch=1: 8.976270 2015-09-30 10:08:50: Loss after num_examples_seen=200 epoch=2: 8.960212 2015-09-30 10:09:06: Loss after num_examples_seen=300 epoch=3: 8.930430 2015-09-30 10:09:22: Loss after num_examples_seen=400 epoch=4: 8.862264 2015-09-30 10:09:38: Loss after num_examples_seen=500 epoch=5: 6.913570 2015-09-30 10:09:53: Loss after num_examples_seen=600 epoch=6: 6.302493 2015-09-30 10:10:07: Loss after num_examples_seen=700 epoch=7: 6.014995 2015-09-30 10:10:24: Loss after num_examples_seen=800 epoch=8: 5.833877 2015-09-30 10:10:39: Loss after num_examples_seen=900 epoch=9: 5.710718
(訳注: 以下は検証結果)
2016-03-20 20:41:27: Loss after num_examples_seen=0 epoch=0: 8.987425 2016-03-20 20:41:49: Loss after num_examples_seen=100 epoch=1: 8.976270 2016-03-20 20:42:12: Loss after num_examples_seen=200 epoch=2: 8.960212 2016-03-20 20:42:33: Loss after num_examples_seen=300 epoch=3: 8.930430 2016-03-20 20:42:57: Loss after num_examples_seen=400 epoch=4: 8.862264 2016-03-20 20:43:24: Loss after num_examples_seen=500 epoch=5: 6.913570 2016-03-20 20:43:55: Loss after num_examples_seen=600 epoch=6: 6.302493 2016-03-20 20:44:25: Loss after num_examples_seen=700 epoch=7: 6.014995 2016-03-20 20:44:52: Loss after num_examples_seen=800 epoch=8: 5.833877
Good, 私たちの実装は少なくとも何か有用なことをして損失を減らしているようです、望んだように。
ネットワークを Theano と GPU で訓練する
以前に Theno について tutorial を書きました、そしてロジックは正確に同じままなのでここでは再度最適化されたコードを通り抜けはしません。numpy 計算を相当する Theano の計算に置き換える、RNNTheano クラスを定義しました。
np.random.seed(10) model = RNNTheano(vocabulary_size) %timeit model.sgd_step(X_train[10], y_train[10], 0.005)
今回は、一つの SGD ステップは私の Mac (without GPU) で 70ms、GPU 装備の Amazon EC2 インスタンス g2.2xlarge 上で 23 ms です。それは初期実装の 15x の改善でモデルを数週間の代わりに数時間/数日で訓練できることを意味しています。依然として数多くの可能な最適化がありますが、当面はこれで十分です。
モデルを訓練するのに数日間を費やすことを貴方が回避することを助けるため、Theano モデルを 50 次元の隠れ層と 8000 の語彙で事前訓練しました。50 epoch に対して約 20 時間の訓練をしました。損失は依然として減少していて、より長時間の訓練は間違いなくより良いモデルになるでしょう。貴方自身で自由に試して長時間訓練してみてください。モデル・パラメータは Github レポジトリの data/trained-model-theano.npz で見つかります。そして load_model_parameters_theano メソッドを使用してそれらをロードします。
from utils import load_model_parameters_theano, save_model_parameters_theano model = RNNTheano(vocabulary_size, hidden_dim=50) # losses = train_with_sgd(model, X_train, y_train, nepoch=50) # save_model_parameters_theano('./data/trained-model-theano.npz', model) load_model_parameters_theano('./data/trained-model-theano.npz', model)
テキストを生成する
モデルを持った今、それに私たちのために新しいテキストを生成することを頼むことができます。新しい文を生成するためのヘルパー関数を実装しましょう:
def generate_sentence(model): # We start the sentence with the start token new_sentence = [word_to_index[sentence_start_token]] # Repeat until we get an end token while not new_sentence[-1] == word_to_index[sentence_end_token]: next_word_probs = model.forward_propagation(new_sentence) sampled_word = word_to_index[unknown_token] # We don't want to sample unknown words while sampled_word == word_to_index[unknown_token]: samples = np.random.multinomial(1, next_word_probs[-1]) sampled_word = np.argmax(samples) new_sentence.append(sampled_word) sentence_str = [index_to_word[x] for x in new_sentence[1:-1]] return sentence_str num_sentences = 10 senten_min_length = 7 for i in range(num_sentences): sent = [] # We want long sentences, not sentences with one or two words while len(sent) < senten_min_length: sent = generate_sentence(model) print " ".join(sent)
2、3の選択された(検閲された)文です。大文字にしてあります。
- Anyway, to the city scene you’re an idiot teenager.
- What ? ! ! ! ! ignore!
- Screw fitness, you’re saying: https
- Thanks for the advice to keep my thoughts around girls.
- Yep, please disappear with the terrible generation.
生成テキストを見ると注意すべき2、3の興味深いことがあります。モデルはシンタックスを成功的に学習しています。コンマを(通常は and や or の前に)正しく置いて文を句点で終了させます。時々それは複数の感嘆符やスマイリーのようなインターネット・スピーチを真似します。
けれども、生成テキストの大半は意味をなさないか、文法的なエラーがあります。(上では実際には最良なものを選んでいます。)一つの理由はネットワークを十分に訓練していないことです。(あるいは十分な訓練データを使用していないことです。)それは多分正しいです、しかし主要な理由ではおそらくないでしょう。私たちの vanilla RNN は意味のあるテキストを生成できません、何故なら幾つかのステップが離れた単語間の依存性を学習できないからです。それは RNN が最初に創られた時に人気の獲得に失敗した理由でもあります。それらは理論的には美しいですが実践的には上手く動作しませんでした、そして私たちは何故かを直ちに理解しませんでした。
幸いなことに、RNN を訓練する難しさは現在では 非常に良く理解されて います。このチュートリアルの次のパートでは Backpropagation Through Time (BPTT) アルゴリズムをより詳しく探求します。そして vanishing gradient problem(勾配消失問題)と呼ばれるものをデモします。これは、LSTM のようなより洗練された RNN モデルへと進む動機となります。これは NLP の多くのタスクに対して最新の技術です(そして非常に良い reddit コメントを生成できます!)。このチュートリアルで学んだこと全てはLSTM と他の RNN モデルにも当てはまりますので、vanilla RNN の結果が期待以下だとしてもがっかりしないでください。
以上