この記事はTensorFlow Advent Calendar 2018 12/4の記事です。他の日付の記事は下記を参照ください。 今回、12/4の8:00時点で12/4に空きがあったので急遽記事を投入することにしました。このような背景もありまして、基本的に本記事の内容は、これまで自分が触ってきたTensorFlow Eager Executionの使用感を踏まえた極普通のことしか書けませんでした。申し訳ありません。 まず、TensorFlowと言えば 1.静的グラフで実現したいデータの流れと処理を定義して
2.実際にデータを流す 「Define and Run」というパラダイムを有していました。
上記の2つ目のセル この方式では、どのような計算処理がどのような順番で行われるのかを予め宣言しておくことで、プログラム側がどの変数がどのような計算をなされたのか、その履歴を把握することができます(履歴も何もグラフとして宣言されている)。なぜ計算履歴を把握できると嬉しいのかというと、ニューラルネットワークの学習の要である後方自動微分(バックプロパゲーション)を「微分の連鎖規則」を用いて実装できるからです(もしも、少ない計算量で、複雑な合成関数の微分値を求める方法が他にあればこんなことはしなくて良いはず。もちろんそんな方法を見つけられたら凄まじい論文になるだろうと思う)。 さて、今回紹介するEager Executionでは 1.データの処理を実際に行う事によって、データの流れが定義される 「Define by Run」というパラダイムを実現しています。
微分の計算を行うために計算の履歴をしっかり保存することが必須要件でした。しかし、こちらのパラダイムでは「計算処理を実行したら、その都度その計算履歴を(捨て去るまで)保存する」という方式になります(それがどのように実装されているかはさておいて…)。この方式のおかげで、学習の途中でニューラルネットワークの構造を変更したりするような場合にも対応できるようになりました(後述の通り、上記の記述は計算履歴を覚えてない。計算履歴を覚えさせる場合には特別な宣言が必要となる)。 なぜこのタイミングでEager Executionなるものを世の中に送り出すことになったのでしょうか。Googleの立場になってこたえると「PyTorchがすごい伸びてるので焦って世論に合わせた」というのが正直なところかと思います(個人の感想です)。こんなことは誰も聞いていないと思いますので、ユーザーの立場になって「なぜEager Executionを使いたいと思うのか(何が嬉しいのか)」について書きます。 まず「Define by Run」というパラダイムによって、既に述べた通り、学習の途中でニューラルネットワークの構造を変更したりすることが容易になりました。また、Python上で扱う変数と、機械学習モデルとしての変数が紐付いているため、同じパラメータを何度も繰り返し使うような構造のニューラルネットワーク(RNNなど)も書きやすくなります。また、「ある条件のときには畳み込み層を通し、他の条件のときには全結合層を通す」であるとかをPythonのif文で簡単に書けます。
上記は入力 正直、計算グラフを動的に変えなければならないようなケースは個人的にはかなり限定的だと思っています。実際、多くのディープラーニングユーザーはDefine and RunであるTensorFlowをラッピングしたKerasを好んで利用していますし、それで事足りているケースがほとんどではないでしょうか。すなわち「Define by Runじゃないと絶対にできない」というようなものと向き合わなければならない状況の人に向けて、今回Eager Executionが導入されたというわけではないのです(個人の感想です)。 なぜEager Executionを導入したのか(あるいはユーザーが使いたがると考えたのか)というと、間違いなくPythonユーザーが慣れ親しんだ構文で、NumPyライクに使えるという点が大きいと思われます。
TFに集まる不平の多くは、「扱うモデルが複雑になっていくにつれてデバッグが極めて困難になり、Define and Runというパラダイムによって個々の計算処理をその都度評価して、実装の確認をすることが難しい」という点にあるのではないかと思われます(生TFを書いたことがある人ならば、自分が思った通りの計算処理を実装できているか不安になり、更にその確認にとても苦労したはずだ)。
結局慣れ親しんでいる方法であるというのが大きい 既に先に述べましたが、ニューラルネットワークの学習の要である自動微分をEagerではどのように使うのかを確認しましょう。「先程Eagerでは計算される度に計算履歴を構築・保持する」と言いましたが半分ウソです。実際には「これから行う計算は履歴を保持してください」と伝えないと保持しません。そのためには
PyTorchでは 実際のところ、上記のように
さて、ここまでの知識で簡単な線形モデルを実装してみましょう。線形モデルとは下記の形で書けるモデルを言います。 $$
f(W, b, x) = Wx + b
$$ ここで $x$ は入力データです。パラメータが $W$ と $b$ になります。学習したいパラメータを クラスを使う必然性は特に無いのですが、後々の書き方に合わせてこうしておきましょう。線形モデルを使った回帰では、手元のデータを $D = \{(x_1, y_1), \cdots, (x_N, y_N)\}$ として、例えば下記の損失関数を最小化します(最小二乗法)。 $$
{\rm Loss} (W, b) = \sum_{i=1}^N (y_i - f(W, b, x_i))^2
$$ ここで $\rm Loss$が$W, b$だけの関数になっているのは、学習データを外から「定数として」与えているからです。別に$x$の関数とみなしても構いませんが、基本的に $x$ に関する微分を求めたりすることは無いので、着目しているパラメータだけに集中することにしましょう。 今損失関数に関して下記のように記述しておきましょう。 学習では損失関数のパラメータ$W$と$b$による微分を利用することになりますので、Eagerを使う場合には、この時点で「 $$
\begin{align}
W &\leftarrow W - \epsilon \frac{\partial {\rm Loss}(W, b)}{\partial W} \\
b &\leftarrow b - \epsilon \frac{\partial {\rm Loss}(W, b)}{\partial b}
\end{align}
$$ となるので、これを実装しましょう。 とすれば完了です。あとは学習ループを回すだけです。
学習前
学習後 もちろん実装の方法はこの限りではありません。何をpythonの関数で囲っておくかは自由ですし、ミニバッチ学習に対応したければ今回の 上記の記述方法は、TensorFlowの自動微分機能以外は、基本的に数式をスクラッチ気味で実装していました。機械学習の多くの場面では、同様の最適化手法を使いまわすことが多いため、当然のことながら既存の実装が存在しています。モデルと損失関数を書くところまでは先程と同様としておきます。 先程はパラメータの更新式を などと宣言します。このクラスのメソッド あとは同様に学習ループを回してやれば良いです。 次は、極めて基本的な使い方となるであろう方法を紹介します。
先程まではモデルとして このコードでは出力の次元が あとの学習コードは先程と変わらず、自前で更新式を書いてもいいですし と書いておき、学習コードについて今回は更に楽をする方法を紹介します。 先程は ここで 従って学習コードとしては下記のような形になります。 かなりシンプルになりましたね。Keras APIとEagerのAPIを活用すると、だいぶコードが抽象化されました。 毎度、似たような学習コードを書くのは面倒です。オリジナルの更新式や、オリジナルの損失関数を使わない限りは上記のような記述をすることは実際には必要なく、 あるいは 学習中にモデルの構造をあれこれ変えたり、あるいはGANのように交互に更新が必要であったりしない場合は、典型的な学習の方法が Define by Runでなければならないモデルはさほど多くなく、モデルを構築している途中でのデバッグが楽というのがEagerの大きな特徴でもあります。複雑なモデルでも学習方法は共通であるケースが多いので、Kerasの高レベルAPIが使えるときは積極的に使っていきましょう。 TensorFlow Eagerは使いやすい反面、Static Graphに比べて処理速度は遅いと言わざるを得ません。Pythonは決して処理の速い言語ではなく、PyTorchも含めなるべく処理をCやC++等のより低レベルな言語に任せるのが主流になっています。今後はTensorFlow Eagerで人間に優しいAPIが整備されながらも、裏では高速処理を実現するためのStatic Graphとの互換性に関しても開発が進んでいくはずです( いずれにしても今年でTensorFlow 2.0として大幅に変動を遂げるはずですので見守っていきましょう。 下記のレポジトリにTensorFlow Eagerでの基本的なモデルの実装を置いておきます(随時更新)。 また公式より はじめに
TensorFlow Eager Execution概要
What's Eager Execution
print(z)
の出力はx+y
の計算結果ではなく、add
という計算が定義されていますとしか出ない。3つ目のセルでsess.run(z)
を実行することで、定義された計算が実際に実行される。tf.enable_eager_execution()
によってEagerの利用を宣言します。Define and Runでは計算処理の定義を行っていただけのコードが、Eagerでは直ちに計算処理が実行されているのがprint(z)
の出力からわかります。Why Eager Execution
計算グラフを動的に変えられる
x
の正負に応じて処理が変わるモデルである。Pythonのif文で簡単に処理内容を分岐させられる。もちろんニューラルネットワークの計算処理の中身に if 文を入れることも可能である(これまでは if 文に相当する条件分岐そのものを「計算グラフとして定義する」必要があった)。PythonicかつNumPyライクに使える
Eager Execution 実践
自動微分
tf.GradientTape()
というものを利用します。tf.GradientTape()
を利用することで計算履歴を保持することができる。上記のようにコンテキストを用いることで、コンテキスト内の計算処理のみを見張るようにプログラムに命令できる。tape.watch(x)
は x
の微分に関して計算できるように準備しておけということである。x
について見張らせて置きつつ a
についての微分を要求しても None
が返ってきます。torch.Tensor
自体に計算履歴を保持するような仕組みが備わっていますが、TensorFlow Eagerはそうではないことに注意しましょう(これはどちらの方法でもメリットデメリットがあります。PyTorchの方式は微分周りの処理をtorch.Tensor
自体にメソッドとして持たせることができ、実装が多くの場面で楽ですが、意図しない計算履歴の保持に拠るメモリの消費が起こる可能性があります)。with tf.GradientTape() as tape
コンテキスト内部で、どの変数による微分を後々計算するのかを考えながら実装を行うのはまあまあ面倒なところです。実際のところは tape.watch()
を使わずとも後々、どの変数で微分計算を行うのかを一括で指示する方法があります。tf.GradientTape()
は tf.Variable
型の変数に関する微分はいつでも行えるように準備してくれます。機械学習においては「学習パラメータについての微分」を求めるのが大半の使い方なので、学習パラメータをtf.Variable
で与えるようにすればOKということです。低レベルEager
tf.Variable
で宣言すればよかったので、上記のモデルの実装は下記のようになります。class Model(object):
def __init__(self):
self.W = tf.Variable(5.0)
self.b = tf.Variable(0.0)
def __call__(self, x):
return self.W * x + self.b
def loss(model, x, desired_y):
predicted_y = model(x)
return tf.reduce_mean(tf.square(predicted_y - desired_y))
loss
関数を呼び出すときに tf.GradientTape
コンテキストで計算を見張らせればイイな」と思えるようにならなければいけません。具体的なパラメータの更新の仕方はdef train(model, inputs, outputs, learning_rate):
with tf.GradientTape() as tape:
current_loss = loss(model, inputs, outputs)
# 2つ目の引数をリストで渡すと、それぞれの変数での微分を返してくれる
dW, db = tape.gradient(current_loss, [model.W, model.b])
# model.W = model.W - learning_rate * dW
# model.b = model.b - learning_rate * db
model.W.assign_sub(learning_rate * dW)
model.b.assign_sub(learning_rate * db)
epochs = range(10)
for epoch in epochs:
current_loss = loss(model, inputs, outputs)
train(model, inputs, outputs, learning_rate=0.1)
train
関数を改造する必要があるでしょう。Optimizer利用
class Model(object):
def __init__(self):
self.W = tf.Variable(5.0)
self.b = tf.Variable(0.0)
def __call__(self, x):
return self.W * x + self.b
def loss(model, x, desired_y):
predicted_y = model(x)
return tf.reduce_mean(tf.square(predicted_y - desired_y))
def train()
関数として自分で書き下しましたが、tf.train.
モジュールを使えば基本的な更新方法を流用することが可能です。典型的にはoptimizer
という変数名にしてoptimizer = tf.train.GradientDescentOptimizer(learning_rate=1e-4)
apply_gradients
に現在のパラメータのリストと現在のパラメータにおける勾配のリストをzip
して渡してやれば更新を行ってくれます。よってdef train()
関数は下記のように書き換えられます。optimizer = tf.train.GradientDescentOptimizer(learning_rate=1e-4)
def train(model, inputs, outputs, learning_rate):
with tf.GradientTape() as tape:
current_loss = loss(model, inputs, outputs)
# 2つ目の引数をリストで渡すと、それぞれの変数での微分を返してくれる
dW, db = tape.gradient(current_loss, [model.W, model.b])
# 現在のパラメータのリストとその勾配のリストを(indexを対応付けて)渡す
optimizer.apply_gradients(zip([model.W, model.b], [dW, db]))
tf.keras.layersl
+ tf.train
tf.Variable
剥き出しで実装していました。また計算処理としてtf.matmul
などの低レイヤー関数も利用していました。行列演算であるとか、畳み込み計算であるとか、元々よく使われるパラメータと計算の組をセットにして実装されているtf.keras.layers
モジュールを活用します。model = tf.keras.layers.Dense(output_size)
output_size
となる線形変換を定義できます(入力のサイズはまだ未定で、データが初めて入力されるタイミングで決定される)。更にtf.keras
のモジュールを使って損失関数も書いておきましょう。def loss_fn(model, x, desired_y):
predict_y = model(x)
return tf.keras.losses.mean_squared_error(predict_y, desired_y)
tf.train
モジュールでoptimizer
を定義しても構いません。先程と同様にひとまずoptimizer = tf.train.GradientDescentOptimizer(learning_rate=1e-4)
tf.GradientTape
コンテキストで微分計算を見張らせていましたが、tf.contrib.eager
にはtf.keras.layers
で定義されたモデルのパラメータについて自動で見張って、損失と勾配を計算してくれる関数tf.contrib.eager.implicit_value_and_gradients
が実装されています。value_and_grads = tf.contrib.eager.implicit_value_and_gradients(loss_fn)
tf.contrib.eager.implicit_value_and_gradients
には自分で定義したPython関数であるloss_fn
を渡していることに注意してください。こいつは自分で記述したloss_fn
関数のラッパーであり、引数はそのまま受け継ぎます。
何が嬉しいのかというと、tf.contrib.eager.implicit_value_and_gradients
は渡してあげたloss_fn
関数内での計算において「tf.keras.layers
内のパラメータ」を認識し、それに対応する勾配をちゃんと自動で取ってきてくれるところです。こいつに元々のloss_fn
の引数を渡してやると、元々定義されていたloss_fn
の返り値(損失の値)と「現在のパラメータの値と勾配のリストをzip
したもの」を返してくれます。for epoch in range(num_epochs):
loss, grads = value_and_grads(model=model, x=x_train_, y=y_train_)
optimizer.apply_gradients(grads)
Keras での学習コード抽象化
tf.keras.models
モジュールで更に楽をできます。tf.keras.Sequential
とtf.keras.Model
の2種類があるので見ていきましょう。
class Model(tf.keras.Model):
def __init__(self):
super(Model, self).__init__(name='mnist_mlp')
self.dense1 = tf.keras.layers(512, activation='relu')
self.dense2 = tf.keras.layers(256, activation='relu')
self.dense3 = tf.keras.layers(10, activation=None)
def call(self, x):
x = self.dense1(x)
x = self.dense2(x)
x = self.dense3(x)
return x
model = Model()
model = tf.keras.Sequential([
tf.keras.layers(512, activation='relu'),
tf.keras.layers(256, activation='relu'),
tf.keras.layers(10, activation=None)
])
tf.keras.Model.fit()
に提供されているため、下記のコードで利用することができます。
optimizer = tf.train.AdamOptimizer(learning_rate)
model.compile(optimizer=optimizer,
loss='categorical_crossentropy',
metrics=["accuracy"])
model.fit(x=x_train_eager.numpy(),
y=y_train_eager.numpy(),
validation_split=0.2,
epochs=num_epochs,
batch_size=batch_size)
最後に
tf.autograph
について触れたかったのですが、勉強する時間が取れませんでした…)。TensorFlow Eager Tutorial
tf.keras
の利用を勧めるアナウンスが入りました。