読者です 読者をやめる 読者になる 読者になる

終末 A.I.

Deep Learning を中心に、週末に機械学習するエンジニアのブログ

TensorFlowでDNNをスクラッチするためのオレオレチュートリアル

はじめに

Chainer もなんとなしに慣れてきたので(使いこなせているレベルではありませんが)、他のDNN Framework も触ってみようと思いたち、昨日からTensorFlowをいじってみています。 最終的なアウトプットは同じなので、クラス構造などが結構似通っていて、思ったより学習コストが低かったのですが、一点苦戦したのがTensorFlowのチュートリアルの微妙さです。

以上...!

といった感じで、普通のDNNはどう書いたらいいの?な質問に答えてくれるのにぴったりなコンテンツが残念ながらありません。 Easy ML with tf.contrib.learn あたりが求めているものに近いのですが、tf.contrib.learn という3分クッキング用モジュールを使用しているため、汎用性のある使い方を知ることができません。

仕方がないので自力でどうにかするしかないかということで、tf.contrib.learn Quickstart を tf.contrib.learn モジュールを一切使用せずに置き換えたチュートリアルを作ってみました。

ゴール

tf.contrib.learn Quickstart に掲載されている、アイリスデータを3つの中間層を持つDNNで学習し精度と予測結果を出力するコードを、tf.contrib.learn モジュールを一切使用せずに置き換えます。

イメージとしては、元のコードとほとんど同じ形式で処理を呼び出し、同じ結果が得られるようなコードを実装していきます。

def main(args):
    # Load datasets.
    x_train, y_train = load_csv(filename=IRIS_TRAINING)
    x_test, y_test = load_csv(filename=IRIS_TEST)

    # Build 3 layer DNN with 10, 20, 10 units respectively.
    classifier = Classifier(hidden_units=[10, 20, 10], n_classes=CLASS_SIZE)

    # Fit model.
    classifier.fit(x_train, y_train, steps=200)

    # Evaluate accuracy.
    accuracy_score = classifier.evaluate(x_test, y_test)[0]
    print('Accuracy: {0:f}'.format(accuracy_score))

    # Classify two new flower samples.
    new_samples = np.array([[6.4, 3.2, 4.5, 1.5], [5.8, 3.1, 5.0, 1.7]], dtype=float)
    y = classifier.predict(new_samples)
    print ('Predictions: {}'.format(str(y)))

ひとまず完成品はTensorFlow basic DNNに掲載してありますので、コード読めばわかるという方はそちらをどうぞ。

TensorFlowのバージョンはr0.9、実行環境はMac x64 の CPUになります。基本的なことしかしていないので、環境やバージョンを変えても動くとは思います(思いたいです)。

データの読み込み

まずは、必要なデータの読み込みを行っていきましょう。クイックスタートのコードはここでさっそく専用のcsvロード関数を使用しています。

とはいえやっていることは簡単で、読み込んでいるアイリスデータのcsvの書き方に依存してはいますが、下記のように読み込んだデータを説明変数群と目的変数とに分離しているだけです。データのヘッダ部分はサンプル数および説明変数の数が記載されているので、データ配列を初期化するために読み込む必要があります。

def load_csv(filename):
    file = pd.read_csv(filename, header=0)

    # get sample's metadata
    n_samples = int(file.columns[0])
    n_features = int(file.columns[1])

    # divide samples into explanation variables and target variable
    data = np.empty((n_samples, n_features))
    target = np.empty((n_samples,), dtype=np.int)
    for i, row in enumerate(file.itertuples()):
        target[i] = np.asarray(row[-1], dtype=np.int)
        data[i] = np.asarray(row[1:n_features+1], dtype=np.float64)
    return (data, target)

DNNモデルクラスの作成

データは問題なく読み込むことができたと思うので、ここから最も重要なモデルクラスを実装していきましょう。TensorFlow にはモデルのベースとなるようなクラス(Chainer の Chain クラス等)のようなものはありませんが、使い方が分かってしまえば特に苦労せず使いまわしやすい実装を行うことができます。

上記のクイックスタートのような処理を行うには、学習と予測、精度算出ができればよいので、早速1つずつ実装していきましょう。

学習モデルの作成

学習を始める前に必要なのは学習モデルを構築することです。TensorFlowではVariableクラスを作成して、それぞれの Variable オブジェクトのリンク関係を定義することによりモデルを構築していきます。3つの中間層を持つDNNは下記のようなコードで表現することができます。

また、Placeholderと呼ばれる学習データを扱うための変数を定義する必要があり、下記のコードでは引数xがそれに当たります。

簡単にコードを説明していきましょう。見たまんまなのですが、入力層の後、3層の中間層、softmax出力層のあるDNNを定義しています。いずれも入力値xに重みWをかけバイアスbを足して活性化関数を通したあとに次の層に渡すという構成になっています。活性化関数は中間層の第一層は恒等関数、第二層と第三層はReLU、出力層はsoftmaxを使用しています。

重みやバイアスを定義する際に使用している tf.truncated_normal や tf.zeros は変数初期化用に用意されている関数で、numpy の初期化関数のように使うことができます。tf.truncated_normal のstddev を求めるための get_stddev 関数は元のコードと結果が近くなるように tf.contrib.layers.initializerz を参考に実装しています。効率良く学習するための変数初期化方法の例がのっていますので、興味がある方は眺めてみることをオススメします。

name_scope の内側で各層の変数を定義していますが、このことにより階層構造で内部に変数を記憶させることができます。

注意すべき点は、学習する際にバッチ単位で毎回下記の関数が呼ばれるわけではなく、Theano 等のようにあくまでこのコードではリンク関係を定義しているだけで、実際の計算自体は TensorFlow の内部で行われるということです。そのため学習途中のパラメーターを見たい場合は、下記のコードにprint文を追加することでは実現できず、Summary クラスを利用して観測するなど独特な方法で実現する必要があります。

def inference(self, x):
    hidden = []

    # Input Layer
    with tf.name_scope("input"):
        weights = tf.Variable(tf.truncated_normal([IRIS_DATA_SIZE, self._hidden_units[0]], stddev=get_stddev(IRIS_DATA_SIZE, self._hidden_units[0])), name='weights')
        biases = tf.Variable(tf.zeros([self._hidden_units[0]]), name='biases')
        input = tf.matmul(x, weights) + biases

    # Hidden Layers
    for index, num_hidden in enumerate(self._hidden_units):
        if index == len(self._hidden_units) - 1: break
        with tf.name_scope("hidden{}".format(index+1)):
            weights = tf.Variable(tf.truncated_normal([num_hidden, self._hidden_units[index+1]], stddev=get_stddev(num_hidden, self._hidden_units[index+1])), name='weights')
            biases = tf.Variable(tf.zeros([self._hidden_units[index+1]]), name='biases')
            inputs = input if index == 0 else hidden[index-1]
            hidden.append(tf.nn.relu(tf.matmul(inputs, weights) + biases, name="hidden{}".format(index+1)))

    # Output Layer
    with tf.name_scope('output'):
        weights = tf.Variable(tf.truncated_normal([self._hidden_units[-1], self._n_classes], stddev=get_stddev(self._hidden_units[-1], self._n_classes)), name='weights')
        biases = tf.Variable(tf.zeros([self._n_classes]), name='biases')
        logits = tf.nn.softmax(tf.matmul(hidden[-1], weights) + biases)

    return logits

いざ、学習

学習モデルができたところで、いよいよ訓練データを使用して学習していきましょう。学習に必要なものは、学習データと損失関数と最適化アルゴリズムです。

まず損失関数から見ていきましょう。損失関数はlossとして実装しています。出力値とラベルのベクトルの各行の積をとり、バッチ内での平均値を返す仕組みになっています。tf.reduce_mean は TensorFlow 内で Reduction と呼ばれる関数群の一つで、テンソルの各次元単位で平均を求めることができます。今回は何も指定していないので、全ての値の平均値を返すようになっています。

続いて、本丸のfit関数を見ていきましょう。最初にやっているのは、学習データを格納するための Placeholder の定義です。shapeの1番目は、バッチ学習用の次元なのでサイズを None に指定しています。次に、定義した Placeholder を上記で作成した学習モデルに通すことで結果が、結果と正解ラベルを損失関数に通すことで損失値が求められるというリンク関係を定義しています。最後に、SGDを使用して損失値が最小化するように学習させるという学習用オペレーションを定義して準備は完了です。

準備が完了したら、変数の更新や計算を行うための Session オブジェクトの用意です。Session オブジェクト自体はクラスの初期化時に作成するようにしていますので、ここでは Session オブジェクトを利用して定義済みの変数の初期化のみを行っています。

変数の初期化まで完了して、いよいよ学習処理を行うことができます。この処理では、指定されたstep回数分学習用オペレーションを実行します。この時、feed_dict に渡しているのは、Placeholder オブジェクトを Key に、実際に使用する学習データを Value に格納した辞書オブジェクトとなります。これにより、与えた学習データによる更新処理を一発で行ってくれます。

get_batch_data でバッチデータを作成しているように見せかけていますが、今のところ全てのデータを一回のバッチとして扱うような実装になっています。そのため、説明変数はそのまま、従属変数もカテゴリ値を 1-of-N なベクトルに変換して返す処理のみを行っています。

一部の Variable オブジェクトをクラス内変数に保存していますが、これは評価用関数でも使い回すための手順なので学習には直接関係ありません。(使いわすための方法としてこれが最適なのかも微妙です。もっと良い書き方がある気がします。)

def loss(self, logits, y):        
    return -tf.reduce_mean(y * tf.log(logits))

def fit(self, x_train=None, y_train=None, steps=200):
    # build model
    x = tf.placeholder(tf.float32, [None, IRIS_DATA_SIZE])
    y = tf.placeholder(tf.float32, [None, CLASS_SIZE])
    logits = self.inference(x)
    loss = self.loss(logits, y)
    train_op = tf.train.GradientDescentOptimizer(0.5).minimize(loss)

    # save variables
    self._x = x
    self._y = y
    self._logits = logits

    # init parameters
    init = tf.initialize_all_variables() 
    self._sess.run(init)

    # train
    for i in range(steps):
        batch_xs, batch_ys = get_batch_data(x_train, y_train)
        self._sess.run(train_op, feed_dict={x: batch_xs, y: batch_ys})

テストデータによる評価

最後にテストデータによる評価に必要な関数を実装していきましょう。テストデータによる正解率の算出とクラス識別を行っていますが、いずれも上記の学習処理内で保存した変数を利用していることに注意してください。x は説明変数、yは目的変数、_logitsはモデルからの出力を表す変数として、それぞれ定義していました。その変数をそのまま流用していますので、このリンク関係をそのまま利用することができます。

識別用の関数が分かりやすいのでそちらから見ていきましょう。識別関数では、出力層の3つのノードからもっとも値の大きいノードを取得しています。この処理を tf.argmax で生成した Variable オブジェクトが、Session.run 関数で入力に指定されたデータを渡した時にどのような値になるかを計算することにより求めています。

正解率の算出用関数も要領は同じです。正解率を求める過程と結果の Variable オブジェクトを定義し、テスト用データでその値を求めるという構造になっています。

上記で行ったモデルの学習も含め、TensorFlowでは、変数のリンク関係を定義し、求めたい変数と計算に用いる入力値を Session.run 関数に渡すことによりその変数の値を計算することができます。

def evaluate(self, x_test=None, y_test=None):
    x_test, y_test = get_test_data(x_test, y_test)

    # build accuracy calculate step
    correct_prediction = tf.equal(tf.argmax(self._logits, 1), tf.argmax(self._y, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    # evaluate
    return self._sess.run([accuracy], feed_dict={self._x: x_test, self._y: y_test})

def predict(self, samples):
    predictions = tf.argmax(self._logits, 1)
    return self._sess.run(predictions, {self._x: samples})

おわりに

以上で、TensorFlow を利用した基本的なDNNの作成は完了です。あとはモデルの定義をCNNやRNNに変更したり、損失関数を変更したり、最適化アルゴリズムを変更するだけで使いまわせるんじゃないかと思います。一覧性が微妙なAPIドキュメントという障害もありますが、ここまでこれば余裕ですよね!

ちなみにパラメーターや学習状況を観測したい場合は、TensorBoard機能を利用すれば良いようですので、興味のある方は僕に教えて下さい。

参考