ホーム » Theano

Theano」カテゴリーアーカイブ

Keras : バックエンド

Keras : バックエンド(翻訳/要約)

* 本ページは、Keras 本家サイトの Keras : Backends の簡単な要約です。

 

“バックエンド” とは何か?

Keras はモデル-レベルのライブラリで、深層学習モデルの開発のための高レベルなビルディング・ブロックを提供します。それ自体はテンソル積、畳み込み等々の低レベルな操作は処理しません。代わりに、それを行なう、Keras の “バックエンド・エンジン” としてサービスを提供する、特化され良く最適化されたテンソル操作ライブラリに頼ります。一つの単一のテンソルライブラリを選択して Keras の実装をそのライブラリに結びつける代わりに、Keras は問題をモジュール化の方法で扱い、幾つかの異なるバックエンド・エンジンが Keras にシームレスにプラグインできます。

現時点では、Keras は利用可能な2つのバックエンド実装を持ちます: Theano バックエンドと TensorFlow バックエンドです。

  • Theano は Université de Montréal の LISA/MILA Lab により開発された、オープンソースのシンボリック・テンソル操作フレームワークです。
  • TensorFlow は Google, Inc により開発された、オープンソースのシンボリック・テンソル操作フレームワークです。
 

一つのバックエンドから他の一つに切り替える

Keras を少なくとも一度実行したのであれば、Keras configuration ファイルを以下で見つけるでしょう :

~/.keras/keras.json

もしないのであれば、それを作成できます。

それはおそらくはこのようなものです :

{"epsilon": 1e-07, "floatx": "float32", "backend": "theano"}

単純に backend フィールドを “theano” か “tensorflow” に変更すると、次に任意の Keras コードを実行する時に Keras は新しい configurationi を使用します。

環境変数 KERAS_BACKEND を定義することも可能で、これは config ファイルで定義されたものをオーバーライドします :

KERAS_BACKEND=tensorflow python -c "from keras import backend; print backend._BACKEND"
Using TensorFlow backend.
tensorflow
 

抽象 Keras バックエンドを使用して新しいコードを書く

もし貴方が書く Keras モジュールを Theano と TensorFlow の両者に互換であるようにしないのであれば、抽象 Keras バックエンド API を通してそれらを書かなければなりません。ここでそのイントロを示します。

バックエンド・モジュールを次で import できます:

from keras import backend as K

下のコードは入力プレースホルダーをインスタンス化します。これは tf.placeholder() または T.matrix(), T.tensor3(), 等と等価です。(訳注: 前者は tensorflow プレースホルダー、後者は Theano のシンボリック変数宣言。)

input = K.placeholder(shape=(2, 4, 5))
# also works:
input = K.placeholder(shape=(None, 4, 5))
# also works:
input = K.placeholder(ndim=3)

下のコードは共有変数をインスタンス化しています。tf.variable() または theano.shared() と等価です。

val = np.random.random((3, 4, 5))
var = K.variable(value=val)

# all-zeros variable:
var = K.zeros(shape=(3, 4, 5))
# all-ones:
var = K.ones(shape=(3, 4, 5))

必要な大部分のテンソル演算は TensorFlow か Theano の中にいるように行なわれます :

a = b + c * K.abs(d)
c = K.dot(a, K.transpose(b))
a = K.sum(b, axis=2)
a = K.softmax(b)
a = concatenate([b, c], axis=-1)
# etc...

より詳細は、keras/backend/theano_backend.py と keras/backend/tensorflow_backend.py のコード参照。

 

以上

Keras : FAQ

Keras : FAQ(翻訳/要約)

* 本ページは、Keras 本家サイトの Keras : FAQ の簡単な要約です。

 

Keras FAQ: Frequently Asked Keras Questions

GPU 上で Keras をどのように実行しますか?

Tensorflow バックエンド上で実行しているのであれば、利用可能な GPU が検知されれば貴方のコードは自動的に GPU で動作します。Theano バックエンド上で実行しているのであれば、次のメソッドの一つが使用できます :

メソッド 1: Theano フラグを使用する。

THEANO_FLAGS=device=gpu,floatX=float32 python my_keras_script.py

名前 ‘gpu’ はデバイスの識別子に依存して変更する必要があるかもしれません (e.g. gpu0, gpu1, etc)。

メソッド 2: .theanorc を設定する: Instructions

メソッド 3: コードの冒頭で theano.config.device, theano.config.floatX を手動設定します :

import theano
theano.config.device = 'gpu'
theano.config.floatX = 'float32'

Keras モデルをどのように保存できますか?

Keras モデルを保存するために pickle または cPickle を使うことは推奨されません。

モデルのアーキテクチャのみを、その重みではなく、保存する必要がある場合は、次のようにできます :

# JSON として保存
json_string = model.to_json()

# YAML として保存
yaml_string = model.to_yaml()

そしてこのデータからフレッシュなモデルを構築できます :

# JSON からモデル再構築:
from keras.models import model_from_json
model = model_from_json(json_string)

# YAML からモデルを再構築
model = model_from_yaml(yaml_string)

モデルの重みを保存する必要がある場合は、下のコードで HDF5 でそのようにできます。

HDF5 と Python ライブラリ h5py を最初にインストールする必要があることに注意してください、これらは Keras にバンドルされて入ってはきません。

model.save_weights('my_model_weights.h5')

モデルをインスタンス化するためのコードがあるとすれば、保存した重みを同じアーキテクチャのモデルにロードすることができます :

model.load_weights('my_model_weights.h5')

これはモデルを保存してシリアライズされたデータのみから再構築する方法へとつながります :

json_string = model.to_json()
open('my_model_architecture.json', 'w').write(json_string)
model.save_weights('my_model_weights.h5')

# elsewhere...
model = model_from_json(open('my_model_architecture.json').read())
model.load_weights('my_model_weights.h5')

何故、訓練中の損失はテスト中の損失よりもかなり高いのでしょうか?

Keras モデルは2つのモードがあります: 訓練 (training) とテスト (testing) です。Dropout と L1/L2 重み正則化のような、正則化メカニズムはテスト時には無効にされます。更に、訓練中の損失は訓練データの各バッチ上の損失の平均です。貴方のモデルは時間をかえて変わっていきますから、エポックの最初のバッチ群上の損失は一般に最後のバッチ群のものよりも高くなります。その一方で、エポックに対するテスト中のロスはエポックの最後におけるモデルを使用して計算されますので、より低い損失になります。

どのようにして中間層 (intermediate layer) の出力を視覚化できますか?

ある入力が与えられた時のある層の出力を返す Keras 関数を構築できます、例えば :

from keras import backend as K

# Sequential モデルで
get_3rd_layer_output = K.function([model.layers[0].input],
                                  [model.layers[3].get_output(train=False)])
layer_output = get_3rd_layer_output([X])[0]

# Graph モデルで
get_conv_layer_output = K.function([model.inputs[i].input for i in model.input_order],
                                   [model.nodes['conv'].get_output(train=False)])
conv_output = get_conv_layer_output([input_data_dict[i] for i in model.input_order])[0]

同様に、Theano と TensorFlow 関数を直接構築することもできます。

メモリに fit in しないデータセットで Keras がどのように使えますか?

model.train_on_batch(X, y) そして model.test_on_batch(X, y) を使用してバッチ訓練を行なうことができます。モデル 文書参照。
See the models documentation.

代わりに、訓練データのバッチを生成する generator を書いてメソッド model.fit_generator(data_generator, samples_per_epoch, nb_epoch) を使うこともできます。

実際に CIFAR 10 サンプル でバッチ訓練を見ることができます。

検証ロスがそれ以上減少しない時、どのように訓練を中断できますか?

EarlyStopping callback が使用できます :

from keras.callbacks import EarlyStopping
early_stopping = EarlyStopping(monitor='val_loss', patience=2)
model.fit(X, y, validation_split=0.2, callbacks=[early_stopping])

callbacks 文書 で詳細が見つかります。

どのように検証分割が計算されるのでしょう?

model.fit の validation_split 引数を例えば 0.1 に設定すれば、使用される検証データはデータの最後の 10% になります。それを 0.25 に設定すれば、データの最後の 25 % になります等々。

データは訓練中にシャッフルされますか?

はい、model.fit の shuffle 引数が True (これはデフォルト)に設定されていれば、訓練データは各エポックでランダムにシャッフルされます。

検証データはシャッフルされません。

各エポックでどのように訓練 / 検証損失 / 精度を記録できますか?

model.fit メソッドは History callback を返し、これは連続的な損失 / 精度のリストを含む history 属性を持ちます。

hist = model.fit(X, y, validation_split=0.2)
print(hist.history)

ステートフル RNN をどのように使用できますか?

RNN をステートフルにするということは、各バッチのサンプルのための状態が次のバッチのサンプルのための初期状態として再利用されることを意味しています。

ステートフル RNN を使用する時は、それゆえに次を仮定します :

  • 全てのバッチは同じ数のサンプルを持ちます。
  • もし X1 と X2 がサンプルの連続するバッチであるならば、全ての i について、X2[i] は X1[i] に対する follow-up シーケンスです。

RNN でステートフルネスを使用するためには、次が必要です :

  • batch_input_shape 引数をモデルの最初の層に渡すことによって、明示的に使用するバッチサイズを指定します、それは整数のタプルで、例えば、時間ステップ毎に 16 の特徴で 10 時間ステップのシーケンスの 32-サンプル バッチのためには、(32, 10, 16) 。
  • RNN 層(群)で stateful=True を設定します。

積算された状態をリセットするには :

  • モデルの全ての層の状態をリセットするために model.reset_states() を使用します。
  • 特定のステートフル RNN 層の状態をリセットするためには layer.reset_states() を使用します。

サンプル:

X  # これは shape (32, 21, 16) の入力データ
# 長さ 10 のシーケンスの中でそれをモデルに供給します。

model = Sequential()
model.add(LSTM(32, batch_input_shape=(32, 10, 16), stateful=True))
model.add(Dense(16, activation='softmax'))

model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

# 最初の 10 が与えられた時の 11 番目の時間ステップを予測するネットワークを訓練します。
model.train_on_batch(X[:, :10, :], np.reshape(X[:, 10, :], (32, 16)))

# the state of the network has changed. We can feed the follow-up sequences:
model.train_on_batch(X[:, 10:20, :], np.reshape(X[:, 20, :], (32, 16)))

# LSTM 層の状態をリセットしましょう :
model.reset_states()

# この場合にそれを行なう他の方法
model.layers[0].reset_states()

メソッド predict, fit, train_on_batch, predict_classes, etc. は全てモデルのステートフル層の状態を更新することに注意してください。これはステートフル訓練だけでなく、ステートフル予測も行なうことを可能にします。

どのように Keras を cite(引用/参照) すべきでしょう?

Please cite Keras in your publications if it helps your research. Here is an example BibTeX entry:

@misc{chollet2015keras,
author = {Chollet, François},
title = {Keras},
year = {2015},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/fchollet/keras}}
}

 

以上

Recurrent Neural Networks (2) – Python, Numpy と Theano による RNN の実装 (翻訳/要約)

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 を使用して言語モデルを構築することです。これはこういう意味です。m 単語の文があるとしましょう。言語モデルは文を観測する確率を次のように予測することを可能にします :

\begin{aligned}  P(w_1,...,w_m) = \prod_{i=1}^{m} P(w_i \mid w_1,..., w_{i-1})  \end{aligned}

言葉で言うならば、文の確率はそれより前に来る単語群が与えられた時の各単語の確率の生産物です。従って、文 “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 がどのようなものか見てみましょう。
入力 x は単語のシーケンスで各 x_t は単一の単語です。しかしもう一つあります: 行列乗算であるため、入力として単語 index (like 36) を単純には使用できません。代わりに、各単語をサイズ vocabulary_size の one-hot ベクトルで表します。例えば、index 36 の単語は全て 0 で位置 36 が 1 のベクトルになるでしょう。従って、各 x_t はベクトルとなり、x は各行が単語を表す行列です。この変換を前処理で行う代わりに、NN コードで遂行します。ネットワークの出力 o も同様のフォーマットです。各 o_t は vocabulary_size 要素のベクトルで各要素はその単語が文の次の単語となる確率を表します。

RNN のための等式です :

\begin{aligned}  s_t &= \tanh(Ux_t + Ws_{t-1}) \\  o_t &= \mathrm{softmax}(Vs_t)  \end{aligned}

行列とベクトルの次元を書き出すことは有用です。語彙サイズ C = 8000 と隠れ層サイズ H = 100 をすることを仮定しましょう。隠れ層サイズをネットワークの “メモリ” と考えて良いです。それを大きくすることはより複雑なパターンの学習を可能にしますが、追加の計算もまた引き起こします。従って :

\begin{aligned}  x_t & \in \mathbb{R}^{8000} \\  o_t & \in \mathbb{R}^{8000} \\  s_t & \in \mathbb{R}^{100} \\  U & \in \mathbb{R}^{100 \times 8000} \\  V & \in \mathbb{R}^{8000 \times 100} \\  W & \in \mathbb{R}^{100 \times 100} \\  \end{aligned}

これは価値ある情報です。U,VW はデータから学習したい、ネットワークのパラメータであることを思い出してください。従って、総計 2HC + H^2 パラメータを学習する必要があります。
C=8000H=100 の場合にはそれは 1,610,000 になります。

次元はまたモデルのボトルネックも教えてくれます。x_t は one-hot ベクトルなので、それを U で乗算することは U のカラムを選択することと本質的に同じなので、フルに乗算を遂行する必要はありません。そしてネットワークの最大の行列乗算は Vs_t になります。それが語彙サイズをできれば小さくした理由です。

初期化

RNN クラスを宣言してパラメータの初期化をすることから始めます。このクラスを RNNNumpy と呼びます、それは後で Theano 版を実装するからです。パラメータ U,VW の初期化は少しばかり技巧的です。0 で初期化することはできません、何故ならそれは層全部で対称計算 (symmetric calculations) を引き起こす結果になるからです。(訳注: i.e. 上手く学習できない。)それらをランダムに初期化しなければなりません。適正な初期化は訓練結果にインパクトがあるようですのでこの分野では沢山の研究があります。最良の初期化は活性化関数(私たちの場合は \tanh)に依存することは判明していて、一つの 推奨される アプローチは重みを \left[-\frac{1}{\sqrt{n}}, \frac{1}{\sqrt{n}}\right] からの間隔においてランダムに初期化することです。ここで 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

計算された出力だけでなく、隠れ状態も返します。それらは勾配を計算するために後で使います、そしてそれらをここで返すことにより計算の重複を回避します。各 o_t は語彙の単語を表す確率のベクトルです。しかし時に、例えばモデルを評価する時に、望むことは最も高い確率の次の単語です。関数 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 の予測を作成しました。U,V,W をランダム値に初期化したので、現時点でこれらの予測は完全にランダムであることに注意してください。次は、各単語のための最も高い確率予測の 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]

(訳注: 動作確認済み、コンソール出力一致。)

損失を計算する

ネットワークを訓練するためにはそれが起こすエラーを計測する方法が必要です。これを損失関数 L と呼び、目標は、訓練データのための損失関数を最小化するパラメータ U,VW を見つけることです。損失関数に対する一般的な選択は 交差エントロピー損失 です。もし N 訓練サンプル(テキストの単語)と C クラス(語彙のサイズ)を持つ場合、予測 o と真のラベル y に関連する損失は次で与えられます :

\begin{aligned}  L(y,o) = - \frac{1}{N} \sum_{n \in N} y_{n} \log o_{n}  \end{aligned}

式は少し複雑に見えますが、実際にそれが行っていることは訓練サンプルに渡って合計して予測がどれだけ間違っているかをベースにして損失に加算しているだけです。y(正しい単語)と o(予測)が離れれば離れるほど、損失が大きくなっていきます。関数 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

ステップバックしてランダムな予測のための損失とは何かを考えてみましょう。それはベースラインを与えてくれて実装が正しいことを確かにしてくれます。語彙に C 単語を持ちますから、各単語は(平均して)確率 1/C で予測されます、これは L = -\frac{1}{N} N \log\frac{1}{C} = \log C の損失を生むでしょう :

# 時間の節約のために 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 を訓練する

訓練データ上の合計損失を最小化するパラメータ U,VW を見つけることを望んでいることを思い出してください。これを行なう最も一般的な方法は SGD (Stochastic Gradient Descent) – 確率的勾配降下法です。SGD の裏にある考えは非常に簡単です。訓練サンプルに渡って反復して、そして各反復においてエラーを減少させる方向にパラメータを少し押してやります。これらの方向は損失上の勾配で与えられます : \frac{\partial L}{\partial U}, \frac{\partial L}{\partial V}, \frac{\partial L}{\partial W}. SGD はまた学習率を必要とします、これは各反復においてどの位の大きさのステップをするかを定義します。SGD は、NN のためだけではなく、多くの他の機械学習アルゴリズムのためにも最も人気のある最適化手法です。それ自体、バッチ、並列性そして適応可能な学習率を使用して SGD をどのように最適化するかについては多くの研究があります。基本的な考え方は単純ですが、実際に効率的な方法で SGD を実装することは非常に複雑です。SGD についてもっと学びたければ これ が始めるに良い地点です。最適化のバックグラウンドさえなくても理解できるはずの SGD の単純版を実装します。

しかし上述したそれらの勾配をどのように計算するのでしょうか?伝統的な NN ではこれを backpropagation アルゴリズムを通して行ないます。RNN では、Backpropagation Through Time (BPTT) と呼ばれる、このアルゴリズムを少し修正した版を使用します。パラメータはネットワークの全ての時間ステップで共有されますので、各出力における勾配は現在の時間ステップの計算上だけではなく、前の時間ステップ群にも依存します。微積分を知っていれば、それは実際に chain rule (合成関数の微分の連鎖律 )を適用するだけです。チュートリアルの次のパートはすべて BPTT についてですので、導関数についてはここでは深入りしません。backpropagation への一般的なイントロは これこの投稿 をチェックしてください。今のところは BPTT をブラックボックスとして扱ってかまいません。それは訓練サンプル (x,y) を入力として取り、勾配 \frac{\partial L}{\partial U}, \frac{\partial L}{\partial V}, \frac{\partial L}{\partial W} を返します。

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 を実装する時はいつでも勾配チェックも実装することは良い考えです、これは貴方の実装が正しいことを検証する方法です。勾配チェックの背後の考えはパラメータの導関数はその点での傾きに等しいことです、これはパラメータを少し変更して変更で除算することにより見積もることができます。

\begin{aligned}  \frac{\partial L}{\partial \theta} \approx \lim_{h \to 0} \frac{J(\theta + h) - J(\theta -h)}{2h}  \end{aligned}

それから 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 の結果が期待以下だとしてもがっかりしないでください。

 

以上

AI導入支援 #2 ウェビナー

スモールスタートを可能としたAI導入支援   Vol.2
[無料 WEB セミナー] [詳細]
「画像認識 AI PoC スターターパック」の紹介
既に AI 技術を実ビジネスで活用し、成果を上げている日本企業も多く存在しており、競争優位なビジネスを展開しております。
しかしながら AI を導入したくとも PoC (概念実証) だけでも高額な費用がかかり取組めていない企業も少なくないようです。A I導入時には欠かせない PoC を手軽にしかも短期間で認知度を確認可能とするサービの紹介と共に、AI 技術の特性と具体的な導入プロセスに加え運用時のポイントについても解説いたします。
日時:2021年10月13日(水)
会場:WEBセミナー
共催:クラスキャット、日本FLOW(株)
後援:働き方改革推進コンソーシアム
参加費: 無料 (事前登録制)
人工知能開発支援
◆ クラスキャットは 人工知能研究開発支援 サービスを提供しています :
  • テクニカルコンサルティングサービス
  • 実証実験 (プロトタイプ構築)
  • アプリケーションへの実装
  • 人工知能研修サービス
◆ お問合せ先 ◆
(株)クラスキャット
セールス・インフォメーション
E-Mail:sales-info@classcat.com