Help us understand the problem. What is going on with this article?

TensorFlowのPythonコードの初歩を噛み砕いてみる

More than 3 years have passed since last update.

こんにちは。
本稿では、TensorFlow(以下TF)の基本的な書き方と文法について、噛み砕いて説明します。
対象は、TFを初めて触れる、かつPythonもあまり書いたことない方向けです。

背景として、私自身がまともにPythonのコードを書いたのが、TFに触れた時でした。
その時に残したノートなので、同じような方のTF理解の一助になるかもしれません。

見本のコードはsergeant-wizardさんの「ニューラルネットワークでプロ野球選手の給与を査定してみる」を使わせて頂きました。
掲載許可をいただき、ありがとうございました。

(1) ライブラリのインポート

import tensorflow as tf
import numpy

numpyも使うので、忘れずにインポートすること。

(2) データの規模とNNの次元設定

SCORE_SIZE = 33
HIDDEN_UNIT_SIZE = 32
TRAIN_DATA_SIZE = 90

SCORE_SIZE: 入力データの次元
HIDDEN_UNIT_SIZE: 隠れ層(中間層)のノード数
TRAIN_DATA_SIZE: 学習データのサンプル数
変数名は、自分で設定してOK!

(3) データの規模とNNの次元設定

raw_input = numpy.loadtxt(open(`input.csv`), delimiter=`,`)
[salary, score]  = numpy.hsplit(raw_input, [1])

numpy.loadtxt: numpyライブラリにあるデータロード関数。

delimiterは、データ区切りの指定。
著者は、input.csvにデータ格納した。
http://goo.gl/h5g8cJ
raw_inputは配列型データを格納する。

numpy.hsplit: 垂直方向(列方向)に割線を入れ、水平方向(行方向に)配列を分割する関数

raw_input:分割対象の配列
[1]:分割の境界線。0列目を選択してsalaryに,1列目からをscoreに代入する。したがって、salaryは成分数=人数(=94)のベクトル、scoreは94 x 33の配列にそれぞれなる。
ここで、salaryが教師データ、scoreが入力データになる点に注意する。
http://goo.gl/hoMyGH

(4) 訓練データを整形する(訓練用の選手とテスト用の選手に分ける)

[salary_train, salary_test] = numpy.vsplit(salary, [TRAIN_DATA_SIZE])
[score_train, score_test] = numpy.vsplit(score, [TRAIN_DATA_SIZE])

numpy.vsplit:水平方向にメスを入れて、垂直方向に配列を分ける関数

salary_train: 成分数89のベクトル
salary_test: 成分数5のベクトル
score_train: 89 x 33 の配列
score_test: 5 x 33 の配列

(5) 隠れ層のパラメータ指定と活性化関数の計算を定義する

def inference(score_placeholder):
  with tf.name_scope('hidden1') as scope:
    hidden1_weight = tf.Variable(tf.truncated_normal([SCORE_SIZE, HIDDEN_UNIT_SIZE], stddev=0.1), name=`hidden1_weight`)
    hidden1_bias = tf.Variable(tf.constant(0.1, shape=[HIDDEN_UNIT_SIZE]), name=`hidden1_bias`)
    hidden1_output = tf.nn.relu(tf.matmul(score_placeholder, hidden1_weight) + hidden1_bias)
  with tf.name_scope('output') as scope:
    output_weight = tf.Variable(tf.truncated_normal([HIDDEN_UNIT_SIZE, 1], stddev=0.1), name=`output_weight`)
    output_bias = tf.Variable(tf.constant(0.1, shape=[1]), name=`output_bias`)
    output = tf.matmul(hidden1_output, output_weight) + output_bias
  return tf.nn.l2_normalize(output, 0)

inference: 関数名(任意につけられる)
score_placeholder:下のコードで定義されるデータ。

placeholder型のデータには、学習の元になるデータ/更新データ(つまり、入力データと教師データ)を指定する。TF独特のデータ型である。
TFでは学習の更新の際に、feedという仕組みで元データを取り込む。placeholder型はfeedと連携しており、入力データが関連の計算に反映されるようになる。
http://goo.gl/uhHk3o

  with tf.name_scope('hidden1') as scope:

with: pythonの文法。with以下の命令を、文脈外のコマンドに持ち越さないという命令。
tf.name_scope: TFの名前管理関数。この関数下の入れ子の命令に、ひとつの文脈・グループを形成する。

名前の管理をするメリットは、描画である。TensorFlowはTensorBoardにて構築した学習器のモデルを図示することができる。名前を管理しておくと、描画のアウトプットがわかりやすくなる。
ここでは、以下のweight や biasへの代入行為がname_scope関数下にネストされている。
すわなち、これらの命令が'hidden1'という隠れ層のパラメータ計算に関する文脈に入っていることを定義づけている。
http://goo.gl/AYodFB

    hidden1_weight = tf.Variable(tf.truncated_normal([SCORE_SIZE, HIDDEN_UNIT_SIZE], stddev=0.1), name=`hidden1_weight`)
    hidden1_bias = tf.Variable(tf.constant(0.1, shape=[HIDDEN_UNIT_SIZE]), name=`hidden1_bias`)
    hidden1_output = tf.nn.relu(tf.matmul(score_placeholder, hidden1_weight) + hidden1_bias)

入力層→隠れ層の計算である。

tf.Variable: TFの変数(Variable)クラスを適用できる

変数としての生成のほか、様々な機能を有する。
例えば、assignという命令により、変数の値を上書きできる。(詳細は下リンク)
http://goo.gl/nUJafs

tf.truncated_normal:正規分布の乱数を返してくれる

[SCORE_SIZE, HIDDEN_UNIT_SIZE]:欲しい乱数の配列サイズ。
stddev:正規分布の標準偏差を指定する。標準正規分布では、「mean=0.0, stddev=1.0」と指定する。
NNの重み${\bf W}$の初期値を乱数で生成している。この段階で、このプログラムが事前学習ないNNであると解釈できる。
hidden1_weightは、重み${\bf W}$の行列と解釈できる。
nameは単純にこの関数の実行に名前をつけている。
http://goo.gl/oZkcvs

tf.constant:定数を生成する関数

0.1:定数0.1を生成する。
shape: 定数を作るサイズ。隠れ層のユニット数(HIDDEN_UNIT_SIZE)だけ作り、hidden1_biasに代入する。
hidden1_biasは隠れ層のバイアス項であり、ここでは初期値を0.1と設定した。

tf.nn.relu:活性化関数のひとつであるReLUを計算する関数
(本稿はコードの解説を目的とするので、ReLUの意味は、別途学んでくださいm(_ _)m)

tf.matmul: 行列(ベクトル)の積(ベクトル同士だったら、内積)を計算する関数

ここでは、行列score_placeholderと行列hidden1_weightの積を計算する。下の定義でわかるが、score_placeholderの列数がSCORE_SIZEと定義されるので、この定義が成立する。
結果的に、outputとして、ユニット数分の計算結果が、ベクトルとして算出される。

  with tf.name_scope('output') as scope:
    output_weight = tf.Variable(tf.truncated_normal([HIDDEN_UNIT_SIZE, 1], stddev=0.1), name=`output_weight`)
    output_bias = tf.Variable(tf.constant(0.1, shape=[1]), name=`output_bias`)
    output = tf.matmul(hidden1_output, output_weight) + output_bias
  return tf.nn.l2_normalize(output, 0)

隠れ層→出力層の計算である。
outputはスカラーで算出される。つまり、出力層のユニットは1であり、これが選手の年俸(教師データ)との比較対象になる。
このケースでは、出力層に対して活性化関数を恒等写像にして処理している。
tf.nn.l2_normalize:正規化を計算して返す関数

ベクトルであれば、ベクトルの各成分にノルムを除して求める。(ベクトルの大きさが1になる変形のやーつ)
行列、テンソル以降も同様。
スカラーであれば、1が返る。ここでは、元データも正規化しているため、正規化した値同士の比較が必要になる。
https://goo.gl/NEFajc

(6) 誤差関数の定義

def loss(output, salary_placeholder, loss_label_placeholder):
  with tf.name_scope('loss') as scope:
    loss = tf.nn.l2_loss(output - tf.nn.l2_normalize(salary_placeholder, 0))
    tf.scalar_summary(loss_label_placeholder, loss)
  return loss

tf.nn.l2_loss: 2乗誤差を計算する関数。

データ型に制限があるので注意する。
主な誤差関数には二乗誤差関数交差エントロピー誤差関数がある。二乗誤差関数は数値予測タスク、交差エントロピー誤差関数はクラス分類タスクにそれぞれ用いる。
salary_placeholderという指定の仕方は教師データとしての給与データをこの変数に更新ごとに投入するためである。
http://goo.gl/V67M7c

tf.scalar_summary:対象のスカラーに文字列を付けて、値の意味付けを記録する関数。

loss_label_placeholderは、文字列のplaceholderとして、下に定義されている。
http://goo.gl/z7JWNe

(7) SGD(確率的勾配降下法)の適用

def training(loss):
  with tf.name_scope('training') as scope:
    train_step = tf.train.GradientDescentOptimizer(0.01).minimize(loss)
  return train_step

tf.train.GradientDescentOptimizer: SGDのアルゴリズムを格納するクラスである。

0.01: 学習係数εを表す
minimize(loss):トリッキーだが、tf.train.GradientDescentOptimizerというクラスに格納されている関数で、対象の変数(ここではloss)を最小化させる
計算過程を格納したオブジェクトなので、微分計算後の更新した重みパラメータが返される。
http://goo.gl/5XENkX

(8) 実行体系の記述

TensorFlowでは、学習実行にまつわるアルゴリズム群を、Graphというクラスに格納している。
計算(session.run)はもちろんのこと、名前の通りグラフ描画(session.graph)に必要な情報も整理してくれる。(便利だねっ(・ω<))

with tf.Graph().as_default():
  salary_placeholder = tf.placeholder(`float`, [None, 1], name=`salary_placeholder`)
  score_placeholder = tf.placeholder(`float`, [None, SCORE_SIZE], name=`score_placeholder`)
  loss_label_placeholder = tf.placeholder(`string`, name=`loss_label_placeholder`)

with tf.Graph().as_default(): :グラフクラスを宣言する文
salary_placeholder:年収の教師データを格納するオブジェクト

[None, 1]:列数は1、行数は任意の数という意味。

score_placeholder:入力データを格納するオブジェクト
loss_label_placeholder = アウトプット時の要約情報に組み込み/反映させるための文字列格納オブジェクト

  feed_dict_train={
    salary_placeholder: salary_train,
    score_placeholder: score_train,
    loss_label_placeholder: `loss_train`
  }

feed_dict_train:学習更新ごとに食わせる辞書型データの宣言

辞書型データ(feed_dict_train)に格納している。TFが都度データを読み込むためには、feedを噛ませなければならない。
http://goo.gl/00Ikjg
↑辞書型データの復習
keyに仮のTensor、valueに初期値を入力。
学習データに関する辞書セットである。
このタイプの辞書が必要な理由は、下記のsession.runのところで分かる。

  feed_dict_test={
    salary_placeholder: salary_test,
    score_placeholder: score_test,
    loss_label_placeholder: `loss_test`
  }

feed_dict_test: テストデータに関する辞書データの作成
構成は、上記と同じ。

python
  output = inference(score_placeholder)
  loss = loss(output, salary_placeholder, loss_label_placeholder)
  training_op = training(loss)

outputに、順伝播のNN計算関数inferenceをぶち込む。
lossに、二乗誤差関数の計算関数lossをぶち込む。
training_opに、SGDのアルゴリズム実行関数trainingをぶち込む。

python
  summary_op = tf.merge_all_summaries()

tf.merge_all_summaries: Summary関数の情報を集約してくれる。

http://goo.gl/wQo8Rz

  init = tf.initialize_all_variables()

initに、全ての変数を初期化する関数tf.initialize_all_variablesを代入する。
初期化のinit宣言するタイミングが重要だ。必要な変数を全て定義してから宣言しないと、エラーを返す。

http://goo.gl/S58XJ2

  best_loss = float(`inf`)

浮動小数点数であるbest_lossを宣言。
下記で、誤差の更新をするところがある。
その値にbest_lossを使うのだが、初期値のlossを必ず入れる必要があるので、初期のbest_lossはどんな数よりも高く(inf;無限大)している。

  with tf.Session() as sess:

Sessionは、Graphの中でも中核になるクラスで、実行関係全般の命令が詰まっている。Sessionを宣言しないと、一切のtfオブジェクトがらみ処理が始まらない。

http://goo.gl/pDZeLI

    summary_writer = tf.train.SummaryWriter('data', graph_def=sess.graph_def)

tf.train.SummaryWriter: 要約情報をイベントファイルに書き込む関数。

    sess.run(init)

sess.run: さらに中核になる関数。

最初の引数に書いてある命令を実行する。
Windowsで言えば、.exeみたいなものだ!

    for step in range(10000):
      sess.run(training_op, feed_dict=feed_dict_train)
      loss_test = sess.run(loss, feed_dict=feed_dict_test)

for step in range(10000): 10000回の繰り返し。「89人分のデータを学習し、5人分のデータをテストする。」これで1回です!
feed_dict=feed_dict_train:session.runの重要なオプション

ここが繰り返し説明してきたfeedシステム。ここで必要なデータを取り込んで、学習更新計算が行われる。

      if loss_test < best_loss:
        best_loss = loss_test
        best_match = sess.run(output, feed_dict=feed_dict_test)

誤差の最小値が更新された時に限り、その誤差値と出力層(推定の年俸)を記録する。

      if step % 100 == 0:
        summary_str = sess.run(summary_op, feed_dict=feed_dict_test)
        summary_str += sess.run(summary_op, feed_dict=feed_dict_train)
        summary_writer.add_summary(summary_str, step)

100ステップに1回、要約情報を集めるという命令。
さらに、集めた情報をイベントファイルに書き込む。

    print sess.run(tf.nn.l2_normalize(salary_placeholder, 0), feed_dict=feed_dict_test)
    print best_match

毎ステップごとの教師データと精度の高い出力層のデータを表示する。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away