はじめてのTensorFlow

本記事では、機械学習ライブラリとして有名なTensorFlow(以下、TF)をはじめて使う人向けに、公式のドキュメント(Getting Started  |  TensorFlow)に基づいて、TFの基本的な使い方を説明していきます。

TFのインストール

まずは、TFをインストールしましょう。インストールは、こちら(Installing TensorFlow  |  TensorFlow)でそれぞれのOSに沿ったインストール方法が紹介されているので、詳細はこちらをご覧ください。

ここでは、MAC OS XのAnacondaを用いたCPUのみを使用する環境を構築します。以下の4つの手順を実行してください。

1.Anacondaをインストールします。こちら(Downloads | Anaconda)からOSを選択すればインストールすることができます。

2.TFを実行する仮想環境を作ります。

conda create -n tensorflow

3.上で作成した仮想環境に移ります。

source activate tensorflow

4.TFをインストールします。

pip install --ignore-installed --upgrade TF_PYTHON_URL

ここで、TF_PYTHON_URLはPython2.7系を用いる場合、

https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-1.3.0-py2-none-any.whl

Python3.4、3.5、3.6系を用いる場合、

https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-1.3.0-py3-none-any.whl

に置き換えてください。

これで、準備は終了です。

TFとは

TFは様々なAPIを提供していますが、主に機械学習エンジニアが使用することを推奨しています。TFの最も低いレベルのAPIである、TensorFlow Core(以下、TFC)はプログラミングの基本的な機能を提供します。一方、高いレベルのAPIは、データセットや推定、訓練などを容易にする機能を提供します。

TFにおけるデータの中心的な単位として、テンソル(Tensor)というものが挙げられます。テンソルは、配列の形で格納された値の集合から成ります。テンソルのランクは、配列の次元の数と一致します。以下にテンソルの例を見てみます。

3 # ランク0のテンソル; 型が[]のスカラー
[1., 2., 3.] # ランク1のテンソル; 型が[3]のベクトル
[[1., 2., 3.], [4., 5., 6.]] # ランク2のテンソル; 型が[2, 3]の行列
[[[1., 2., 3.]], [[7., 8., 9.]]] # ランク3のテンソル; 型[2, 1, 3]

TFCチュートリアル

TFのインポート

TFのクラスやメソッドを利用するためには、まずはコード内でTFをインポートしなければなりません。

import tensorflow as tf

計算グラフ

計算グラフ(Computational Graph)は、TFの一連の操作をグラフのノードへアレンジしたものです。

簡単な計算グラフのビルド例を挙げます。まず2つのfloat型のテンソル、node1とnode2を作成します。

node1 = tf.constant(3.0, dtype=tf.float32)
node2 = tf.constant(4.0) # 暗にtf.float32を宣言
print(node1, node2)

この結果は、以下のようになります。

Tensor("Const:0", shape=(), dtype=float32) Tensor("Const_1:0", shape=(), dtype=float32)

ここで、ノードをprintしたとき、内部に格納されている値である$3.0$、$4.0$が出力されていないことに注意してください。代わりに、ノードが評価された時に、値が返ります。ノードを評価するためには、セッションの中で計算グラフを走らせなければなりません。

以下のコードでは、1行目でセッションオブジェクトを作成し、2行目でnode1とnode2を評価するための計算グラフを走らせるrunメソッドを用います。

sess = tf.Session()
print(sess.run([node1, node2]))

これにより、内部に格納されている値が表示されることが確認できます。

[3.0, 4.0]

より複雑な例を見てみましょう。上記で作成した二つの定数ノードを組み合わせて、新しいグラフを作ります。

node3 = tf.add(node1, node2)
print("node3:", node3)
print("sess.run(node3):", sess.run(node3))

実行結果はこのようになります。

node3: Tensor("Add:0", shape=(), dtype=float32)
sess.run(node3): 7.0

TFは、TensorBoardという計算グラフを可視化することのできる機能を提供しています。上の例では、次のような図が得られます。

f:id:hiyoko9t:20171004161647p:plain

このグラフは、常に定数の結果を出力する簡単なグラフです。

グラフは、外部の入力をパラメータ化することもできます。これを、プレースホルダー(placeholders)と呼びます。プレースホルダーは、値を後で返すことを保証します。言葉で書くとよくわかりませんね。実際に、例を見ていきましょう。

a = tf.placeholder(tf.float32)
b = tf.placeholder(tf.float32)
adder_node = a + b  # tf.add(a, b)のショートカット

このコードでは、$a$、$b$という値の殻を作っておき、adder_nodeで$a$、$b$に対する操作を定義しています。実際に、プレースホルダーに具体的な値を与え、runメソッドを実行することにより、このグラフを評価することができます。

print(sess.run(adder_node, {a: 3, b: 4.5}))
print(sess.run(adder_node, {a: [1, 3], b: [2, 4]}))

この実行結果は、以下のようになり、足し算ができていることがわかります。

7.5
[ 3.  7.]

この計算グラフは、TensorBoardにより、次のようにあらわされます。

f:id:hiyoko9t:20171004163014p:plain

他の操作を加えて、もう少し複雑な計算グラフを見ていきましょう。

add_and_triple = adder_node * 3
print(sess.run(add_and_triple, {a: 3, b: 4.5}))

これは、二つの引数を足し合わせた後、3倍する操作を定義しています。

よって、結果としては、以下が出力されます。

22.5

この計算グラフは、TensorBoardを用いると、次のようにあらわされます。 f:id:hiyoko9t:20171004163338p:plain

機械学習では、上記のように任意の入力が可能なモデルを得たい場合があります。モデルを訓練可能とするために、同じ入力に対して新しい出力を得ることができるようにグラフを修正する必要があります。そこで、変数(Variables)を用いることにより、訓練可能なパラメータをグラフに加えることができます。変数を用いた例を挙げます。

W = tf.Variable([.3], dtype=tf.float32)
b = tf.Variable([-.3], dtype=tf.float32)
x = tf.placeholder(tf.float32)
linear_model = W * x + b

定数は、tf.constantが呼ばれたときに初期化され、その値が変更されることはありませんでした。一方、変数はtf.Variableが呼ばれたときには初期化されません。TFのプログラムの中で変数を初期化するには、次の操作が必要となります。

init = tf.global_variables_initializer()
sess.run(init)

ここで、initはすべてのグローバル変数を初期化するTFの部分グラフを扱っていることに注意してください。sess.runが呼ばれるまでは、変数は初期化されません。

ここで、$x$はプレースホルダーなので、ある特定の値に対して、linear_modelを評価することができます。

print(sess.run(linear_model, {x: [1, 2, 3, 4]}))

出力結果は、以下のとおりです。

[ 0.          0.30000001  0.60000002  0.90000004]

ここで、線形モデルを作ることができましたが、このモデルが良いかどうかはわかりません。訓練データに対してモデルを評価するために、目的値を与えるためのプレースホルダー$y$が必要となります。また、ロス関数を与える必要もあります。

ロス関数は、現在のモデルが与えられたデータからどれだけ離れているかを測ります。ここでは、線形回帰に対する標準的なロスモデル、すなわち現在のモデルと与えられたデータの差の二乗和を用います。linear_model - yは、それぞれの要素が対応するエラーをあらわすベクトルとなります。tf.squareを用いて、この二乗誤差を得ることができます。さらに、tf.reduce_sumを用いて、すべてのサンプルのエラーを足し合わせて、スカラーにすることができます。

y = tf.placeholder(tf.float32)
squared_deltas = tf.square(linear_model - y)
loss = tf.reduce_sum(squared_deltas)
print(sess.run(loss, {x: [1, 2, 3, 4], y: [0, -1, -2, -3]}))

この結果は、以下のとおりとなります。

23.66

ここで$W$と$b$の値を適切な値、$-1$と$1$、を定めることで、このモデルを改善することができます。変数は、tf.Variableで初期値を与えられますが、tf.assignを用いることで、変更することができます。以下で、変数を変更してみます。

fixW = tf.assign(W, [-1.])
fixb = tf.assign(b, [1.])
sess.run([fixW, fixb])
print(sess.run(loss, {x: [1, 2, 3, 4], y: [0, -1, -2, -3]}))

結果は、次のようになります。

0.0

ここでは、「完全な」$W$と$b$の値を予測しましたが、機械学習ではこれらのモデル(を決定するパラメータ)を自動的に求めます。次の章では、その方法を見ていきます。

tf.train API

機械学習の詳細な議論は、このページでは行いませんが、TFは、ロス関数を最小化するようにゆっくりと変数を変化させるオプティマイザを提供しています。もっともシンプルなオプティマイザは、勾配降下法であり、これはロス関数の微分に基づいて変数を修正します。一般に微分を計算することは、面倒であり、また誤差が生じやすい部分となります。TFでは、tf.gradientsという関数を用いることにより、この微分を自動で得ることができます。簡単なオプティマイザの例を挙げます。

optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)
sess.run(init) # 変数のリセット
for i in range(1000):
  sess.run(train, {x: [1, 2, 3, 4], y: [0, -1, -2, -3]})

print(sess.run([W, b]))

これにより、モデルのパラメータは以下のように求められます。

[array([-0.9999969], dtype=float32), array([ 0.99999082],
 dtype=float32)]

これで、実際の機械学習を行ったこととなります。これは、非常に簡単な例であり、TFのコアなコードを使用していませんが、より複雑なモデルやメソッドを実装するときには、必要に応じてコードを追加しなければなりません。TFでは、より抽象化されたAPIが提供されているので、次章では、その使い方を見ていきます。

完全なコード

線形回帰モデルの、完全なコードは次の通りです。

import tensorflow as tf

# モデルパラメータ
W = tf.Variable([.3], dtype=tf.float32)
b = tf.Variable([-.3], dtype=tf.float32)
# モデルの入出力
x = tf.placeholder(tf.float32)
linear_model = W * x + b
y = tf.placeholder(tf.float32)

# ロス
loss = tf.reduce_sum(tf.square(linear_model - y)) # sum of the squares
# オプティマイザ
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

# 訓練データ
x_train = [1, 2, 3, 4]
y_train = [0, -1, -2, -3]
# 訓練ループ
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init) # reset values to wrong
for i in range(1000):
  sess.run(train, {x: x_train, y: y_train})

# 訓練精度の評価
curr_W, curr_b, curr_loss = sess.run([W, b, loss], {x: x_train, y: y_train})
print("W: %s b: %s loss: %s"%(curr_W, curr_b, curr_loss))
W: [-0.9999969] b: [ 0.99999082] loss: 5.69997e-11

ここで、ロス関数の値が非常に小さくなっていることに注目してください。実際に、このコードを実行したときは、ランダムに初期化された変数のために異なった結果が得られるかもしれません。

これを、TensorBoardで可視化したものが、次の図となります。

f:id:hiyoko9t:20171005181309p:plain

tf.estimator

tf.estimatorは、機械学習の仕組みを簡単にした高いレベルのTFのライブラリです。
機能としては、次のものを含みます。

  1. 訓練ループを回す
  2. 評価ループを回す
  3. データセットを管理する

基本的な使い方

tf.estimatorを用いて、線形回帰問題がどれぐらいシンプルにかけるかを見てみましょう。

import tensorflow as tf
# NumPy はデータのロード、計算、前処理によく用いられます。
import numpy as np

# 特徴量のリストの宣言。ここでは、一つの特徴量だけですが、他にも多くの、より複雑で有用な型が存在します。
feature_columns = [tf.feature_column.numeric_column("x", shape=[1])]

# 推定器による訓練と評価。線形回帰や線形分類器、ニューラルネットワーク回帰、分類器など多くの準備された型が存在します。(ここでは線形回帰を用います)
estimator = tf.estimator.LinearRegressor(feature_columns=feature_columns)

# TFは、多くのデータセットの読み込みやセットアップに役立つメソッドが準備されています。ここでは、訓練データと評価データを使用します。
x_train = np.array([1., 2., 3., 4.])
y_train = np.array([0., -1., -2., -3.])
x_eval = np.array([2., 5., 8., 1.])
y_eval = np.array([-1.01, -4.1, -7, 0.])
input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_train}, y_train, batch_size=4, num_epochs=None, shuffle=True)
train_input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_train}, y_train, batch_size=4, num_epochs=1000, shuffle=False)
eval_input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_eval}, y_eval, batch_size=4, num_epochs=1000, shuffle=False)

# 訓練データを用いて、訓練を1000ステップ実行
estimator.train(input_fn=input_fn, steps=1000)

# モデルの性能を評価
train_metrics = estimator.evaluate(input_fn=train_input_fn)
eval_metrics = estimator.evaluate(input_fn=eval_input_fn)
print("train metrics: %r"% train_metrics)
print("eval metrics: %r"% eval_metrics)

これを実行した結果が、次のようになります。

train metrics: {'loss': 1.2712867e-09, 'global_step': 1000}
eval metrics: {'loss': 0.0025279333, 'global_step': 1000}

訓練と比較して、評価のロスが大きいことに気づかれるかもしれません。しかし、ロスが$0$に近づいていることから、正しく学習できたことがわかります。

カスタムモデル

tf.estimatorは、先の準備された形でしか使えないものではありません。今、TFでビルドしないモデルを作りたいとします。ここでは、低いレベルのTF APIを使用することにより、先の線形回帰モデルと等価なモデルを作成します。

カスタムされたモデルを使うには、tf.estimator.Estimatorを用います。tf.estimator.LinearRegressorは、tf.estimator.Estimatorのサブクラスとなります。ここでは、model_fnという関数を用いて、予測、訓練、ロスの評価をどう行うかを定義します。

import numpy as np
import tensorflow as tf

# 特徴量の宣言
def model_fn(features, labels, mode):
  # 線形モデルと予測変数のビルド
  W = tf.get_variable("W", [1], dtype=tf.float64)
  b = tf.get_variable("b", [1], dtype=tf.float64)
  y = W * features['x'] + b
  # ロス
  loss = tf.reduce_sum(tf.square(y - labels))
  # 訓練
  global_step = tf.train.get_global_step()
  optimizer = tf.train.GradientDescentOptimizer(0.01)
  train = tf.group(optimizer.minimize(loss),
                   tf.assign_add(global_step, 1))
  # 推定器との接続
  return tf.estimator.EstimatorSpec(
      mode=mode,
      predictions=y,
      loss=loss,
      train_op=train)

estimator = tf.estimator.Estimator(model_fn=model_fn)
# データセットの定義
x_train = np.array([1., 2., 3., 4.])
y_train = np.array([0., -1., -2., -3.])
x_eval = np.array([2., 5., 8., 1.])
y_eval = np.array([-1.01, -4.1, -7, 0.])
input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_train}, y_train, batch_size=4, num_epochs=None, shuffle=True)
train_input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_train}, y_train, batch_size=4, num_epochs=1000, shuffle=False)
eval_input_fn = tf.estimator.inputs.numpy_input_fn(
    {"x": x_eval}, y_eval, batch_size=4, num_epochs=1000, shuffle=False)

# 訓練
estimator.train(input_fn=input_fn, steps=1000)
# モデルの評価
train_metrics = estimator.evaluate(input_fn=train_input_fn)
eval_metrics = estimator.evaluate(input_fn=eval_input_fn)
print("train metrics: %r"% train_metrics)
print("eval metrics: %r"% eval_metrics)

これを実行することにより、次の結果が得られます。

train metrics: {'loss': 1.227995e-11, 'global_step': 1000}
eval metrics: {'loss': 0.01010036, 'global_step': 1000}

model_fn()関数に注目すると、本記事で作成した訓練ループと非常によく似ていることがわかります。

まとめ

本記事では、Tensorflowのインストールから線形モデルに対する訓練、評価という機械学習の一連の流れを見てきました。 公式のドキュメントでは、MNISTを用いた画像分類やTensorBoardを用いたグラフの可視化、CNNの構築など中々のボリュームのチュートリアルがあるので、興味のある人はぜひいっしょに頑張りましょう。