手書き文字を作れるJavascriptをつくってTensorFlowで予測させてみた(2)

この前、「手書き文字を作れるJavascriptをつくってTensorFlowで予測させてみた」という投稿でブラウザ上で手書きした文字画像を、MNISTで訓練したモデルで予測してみましたが、ものすごく精度が悪かったです。今回改めて、CNNを使ってやってみたらかなり精度が上がりました。何%か測ったりしてませんが、自分の手書きだと90%は超える感じでした。やっぱりCNNはすごいなーと思いました。でももしかしたら前回のものにミスがあり、CNNではなくても精度は本当はもっと高い可能性はあります。

もうちょっとやるとしたら、文字を画像の中心に適度な大きさで書く必要があり、例えば右上に小さく2と書いても認識されません。あとは、現在はMNISTに合わせて、手書き文字画像も背景黒、文字色白で作成するように固定していますが、これらの色を変えても認識するようにしたいです。今度やってみます。

Github

https://github.com/endoyuta/mnist_test

index.html

<html>
<head>
<title>MNIST TEST</title>
</head>
<body>
<h1>MNIST TEST</h1>
<canvas id="canvas1" width="400" height="400" style="border: 1px solid #999;"></canvas><br><br>
<input id="clear" type="button" value="Clear" onclick="canvasClear();">
<input id="submit" type="button" value="Submit" onclick="saveImg();"><br><br>
<img id="preview"><span id="answer"></span>
<script src="http://code.jquery.com/jquery-3.1.1.min.js"></script>
<script>
var url = 'http://127.0.0.1:8000/cgi-bin/mnist.py';
var lineWidth = 40;
var lineColor = '#ffffff';
var imgW = imgH = 28;

var canvas = document.getElementById('canvas1');
var ctx = canvas.getContext('2d');
var cleft = canvas.getBoundingClientRect().left;
var ctop = canvas.getBoundingClientRect().top;
var mouseX = mouseY = null;

canvasClear();
canvas.addEventListener('mousemove', mmove, false);
canvas.addEventListener('mousedown', mdown, false);
canvas.addEventListener('mouseup', mouseInit, false);
canvas.addEventListener('mouseout', mouseInit, false); 

function mmove(e){
    if (e.buttons == 1 || e.witch == 1) {
        draw(e.clientX - cleft, e.clientY - ctop);
    };
}

function mdown(e){
    draw(e.clientX - cleft, e.clientY - ctop);
}

function draw(x, y){
    ctx.beginPath();
    if(mouseX === null) ctx.moveTo(x, y);
    else ctx.moveTo(mouseX, mouseY);
    ctx.lineTo(x, y);
    ctx.lineCap = "round";
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = lineColor;
    ctx.stroke();
    mouseX = x;
    mouseY = y;
}

function mouseInit(){
    mouseX = mouseY = null;
}

function canvasClear(){
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    $('#preview').attr('src', '');
    $('#answer').empty();
}

function toImg(){
    var tmp = document.createElement('canvas');
    tmp.width = imgW;
    tmp.height = imgH;
    var tmpCtx = tmp.getContext('2d');
    tmpCtx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, imgW, imgH);
    var img = tmp.toDataURL('image/jpeg');
    $('#preview').attr('src', img)
    return img;
}

function saveImg(){
    var img = toImg();
    console.log(img);
    $.ajax({
        url: url,
        type: 'POST',
        data: {img: img},
        dataType: 'json',
        success: function(data){
            if(data.status){
                $('#answer').html('は、' + data.num + 'です');
            }else{
                $('#answer').html('は、分かりません');
            }
        },
    });
}
</script>
</body>
</html>

cgi-bin/mnist.py

#!/usr/bin/env python

import sys
import os
import cgi
import json
import cgitb
cgitb.enable()

from PIL import Image
import numpy as np
from io import BytesIO
from binascii import a2b_base64

import mytensor

print('Content-Type: text/json; charset=utf-8')
print()

if os.environ['REQUEST_METHOD'] == 'POST':
    data = cgi.FieldStorage()
    img_str = data.getvalue('img', None)
    if img_str:
        b64_str = img_str.split(',')[1]
        img = Image.open(BytesIO(a2b_base64(b64_str))).convert('L')
        img_arr = np.array(img).reshape(1, -1)
        img_arr = img_arr / 255
        result = mytensor.predict(img_arr)
        print(json.dumps({'status': True, 'num': result}))
        sys.exit()
print(json.dumps({'status': False, 'num': False}))

cgi-bin/mytensor.py

import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

def _weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)

def _bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)

def _conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def interface():
    x = tf.placeholder(tf.float32, shape=[None, 784])
    y_ = tf.placeholder(tf.float32, shape=[None, 10])

    x_image = tf.reshape(x, [-1, 28, 28, 1])
    W_conv1 = _weight_variable([5, 5, 1, 32])
    b_conv1 = _bias_variable([32])
    h_conv1 = tf.nn.relu(_conv2d(x_image, W_conv1) + b_conv1)
    h_pool1 = tf.nn.max_pool(h_conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    W_conv2 = _weight_variable([5, 5, 32, 64])
    b_conv2 = _bias_variable([64])
    h_conv2 = tf.nn.relu(_conv2d(h_pool1, W_conv2) + b_conv2)
    h_pool2 = tf.nn.max_pool(h_conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    W_fc1 = _weight_variable([7 * 7 * 64, 1024])
    b_fc1 = _bias_variable([1024])
    h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
    h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

    keep_prob = tf.placeholder(tf.float32)
    h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

    W_fc2 = _weight_variable([1024, 10])
    b_fc2 = _bias_variable([10])
    y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2

    cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(y_conv, y_))
    train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
    correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    saver = tf.train.Saver()

    class CNNModel():
        pass
    model = CNNModel()
    model.x = x
    model.y_ = y_
    model.keep_prob = keep_prob
    model.y_conv = y_conv
    model.train_step = train_step
    model.accuracy = accuracy
    model.saver = saver
    return model

def predict(img):
    ckpt = tf.train.get_checkpoint_state('./cgi-bin/ckpt')
    if not ckpt: return False
    m = interface()
    with tf.Session() as sess:
        m.saver.restore(sess, ckpt.model_checkpoint_path)
        result = sess.run(m.y_conv, feed_dict={m.x: img, m.keep_prob:1.0})
        return int(np.argmax(result))

def train():
    if tf.train.get_checkpoint_state('./ckpt'):
        print('train ok')
        return
    mnist = input_data.read_data_sets('./mnist', one_hot=True, dtype=tf.float32)
    m = interface()
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        for i in range(20000):
            batch = mnist.train.next_batch(100)
            m.train_step.run(feed_dict={m.x: batch[0], m.y_: batch[1], m.keep_prob: 0.5})
            if i % 100 == 0:
                train_accuracy = m.accuracy.eval(feed_dict={m.x:batch[0], m.y_: batch[1], m.keep_prob: 1.0})
                print("step %d, training accuracy %g"%(i, train_accuracy))
        m.saver.save(sess, './ckpt/model.ckpt')
    print('train ok')

if __name__ == '__main__':
    train()

TensorFlow – Local Response Normalization(LRN)(局所的応答正規化)

参考:theanoで局所コントラスト正規化(Local Contrast Normalization)を使う

正規化の方法にはいろいろあり、代表的なものを挙げると

Global Contrast Normalization(GCN)
Local Contrast Normalization(LCN)
Local Response Normalization(LRN)
ZCA whitening
Local mean subtraction

CNN内の正規化層としては、LCNやらLRNが使われる。

LCNはその名の通り、特徴マップの局所領域内でコントラストを正規化する。この処理は一つの特徴マップ内で完結するので、すべての特徴マップに対して独立して行う。
対してLRNでは、同一位置における異なる特徴マップ間で正規化する。どちらもいくつかのハイパーパラメータはあるが、学習の対象となるパラメータはないので、誤差伝播が容易に可能である。

参考:theanoでLocal Response Normalization(LRN)を使う

LRNは端的に述べると、「同一位置(ピクセル)において複数の特徴マップ間で正規化する」ということだそうだ。元の論文にも書いてあるが、LRNは”brightness normalization”であり、LCNのように輝度の平均を減算して0にしないことがミソらしい。


$$\displaystyle
b^i_{x,y}=a^i_{x,y}/ \left( k+\alpha \sum^{min(N-1,i+\frac{n}{2})}_{j=max(0,i-\frac{n}{2})} (a^j_{x,y})^2 \right)^\beta
$$

k, n, α, βがパラメータである{a^i_{x,y}}はi番目の特徴マップの(x,y)のピクセルを、Nは特徴マップの総数を表す。
summationの部分は、「i番目の特徴マップに対して、n近傍の特徴マップの二乗和をとる」という意味である。

参考:tf.nn.local_response_normalization(input, depth_radius=None, bias=None, alpha=None, beta=None, name=None)

Local Response Normalization.

The 4-D input tensor is treated as a 3-D array of 1-D vectors (along the last dimension), and each vector is normalized independently. Within a given vector, each component is divided by the weighted, squared sum of inputs within depth_radius. In detail,

翻訳結果

4次元入力テンソルは、(最後の次元に沿って)1次元ベクトルの3次元配列として扱われ、各ベクトルは独立して正規化されます。 所与のベクトル内で、各成分は、depth_radius内の入力の加重二乗和で除算される。

sqr_sum[a, b, c, d] =
    sum(input[a, b, c, d - depth_radius : d + depth_radius + 1] ** 2)
output = input / (bias + alpha * sqr_sum) ** beta

使用例

norm1 = tf.nn.lrn(pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name='norm1')

Python3で関数をつくってみる

def lrn(input, depth_radius, bias, alpha, beta):
    input_t = input.transpose([2, 0, 1])
    sqr_sum = np.zeros(input_t.shape)
    for i in range(input_t.shape[0]):
        start_idx = i - depth_radius
        if start_idx < 0: start_idx = 0
        end_idx = i + depth_radius + 1
        sqr_sum[i] = sum(input_t[start_idx : end_idx] ** 2)
    return (input_t / (bias + alpha * sqr_sum) ** beta).transpose(1, 2, 0)

使ってみる

import tensorflow as tf
import numpy as np
from PIL import Image

input = np.array([
    [
        [230,230,230],[210,210,210],[190,190,190],[170,170,170],[150,150,150]
    ],
    [
        [230,230,230],[210,210,210],[190,190,190],[170,170,170],[150,150,150]
    ],
    [
        [230,230,230],[210,210,210],[190,190,190],[170,170,170],[150,150,150]
    ],
    [
        [230,230,230],[210,210,210],[190,190,190],[170,170,170],[150,150,150]
    ],
    [
        [230,230,230],[210,210,210],[190,190,190],[170,170,170],[150,150,150]
    ],
])

depth_radius = 2
bias = 1.0
alpha = 0.001 / 9.0
beta = 0.75

def lrn(input, depth_radius, bias, alpha, beta):
    input_t = input.transpose([2, 0, 1])
    sqr_sum = np.zeros(input_t.shape)
    for i in range(input_t.shape[0]):
        start_idx = i - depth_radius
        if start_idx < 0: start_idx = 0
        end_idx = i + depth_radius + 1
        sqr_sum[i] = sum(input_t[start_idx : end_idx] ** 2)
    return (input_t / (bias + alpha * sqr_sum) ** beta).transpose(1, 2, 0)

output = lrn(input, depth_radius, bias, alpha, beta)
print(output)
Image.fromarray(np.uint8(output)).save('./img/lrn.jpg')

結果

[[[ 25.64542621  25.64542621  25.64542621]
  [ 26.62530279  26.62530279  26.62530279]
  [ 27.69886502  27.69886502  27.69886502]
  [ 28.8700044   28.8700044   28.8700044 ]
  [ 30.13193797  30.13193797  30.13193797]]

 [[ 25.64542621  25.64542621  25.64542621]
  [ 26.62530279  26.62530279  26.62530279]
  [ 27.69886502  27.69886502  27.69886502]
  [ 28.8700044   28.8700044   28.8700044 ]
  [ 30.13193797  30.13193797  30.13193797]]

 [[ 25.64542621  25.64542621  25.64542621]
  [ 26.62530279  26.62530279  26.62530279]
  [ 27.69886502  27.69886502  27.69886502]
  [ 28.8700044   28.8700044   28.8700044 ]
  [ 30.13193797  30.13193797  30.13193797]]

 [[ 25.64542621  25.64542621  25.64542621]
  [ 26.62530279  26.62530279  26.62530279]
  [ 27.69886502  27.69886502  27.69886502]
  [ 28.8700044   28.8700044   28.8700044 ]
  [ 30.13193797  30.13193797  30.13193797]]

 [[ 25.64542621  25.64542621  25.64542621]
  [ 26.62530279  26.62530279  26.62530279]
  [ 27.69886502  27.69886502  27.69886502]
  [ 28.8700044   28.8700044   28.8700044 ]
  [ 30.13193797  30.13193797  30.13193797]]]

tf.nn.lrnを使ってみる

コード

import tensorflow as tf
import numpy as np
from PIL import Image

input = np.array([
    [
        [230,230,230],[210,210,210],[190,190,190],[170,170,170],[150,150,150]
    ],
    [
        [230,230,230],[210,210,210],[190,190,190],[170,170,170],[150,150,150]
    ],
    [
        [230,230,230],[210,210,210],[190,190,190],[170,170,170],[150,150,150]
    ],
    [
        [230,230,230],[210,210,210],[190,190,190],[170,170,170],[150,150,150]
    ],
    [
        [230,230,230],[210,210,210],[190,190,190],[170,170,170],[150,150,150]
    ],
])

depth_radius = 2
bias = 1.0
alpha = 0.001 / 9.0
beta = 0.75

input_for_tf = np.zeros([1, input.shape[0], input.shape[1], input.shape[2]])
input_for_tf[0] = input
output2 = tf.nn.lrn(input_for_tf, depth_radius, bias=bias, alpha=alpha, beta=beta)
with tf.Session() as sess:
    out = sess.run(output2)
    print(out)
    Image.fromarray(np.uint8(out[0])).show()

結果

[[[[ 25.64542389  25.64542389  25.64542389]
   [ 26.62529945  26.62529945  26.62529945]
   [ 27.6988678   27.6988678   27.6988678 ]
   [ 28.87000465  28.87000465  28.87000465]
   [ 30.13193893  30.13193893  30.13193893]]

  [[ 25.64542389  25.64542389  25.64542389]
   [ 26.62529945  26.62529945  26.62529945]
   [ 27.6988678   27.6988678   27.6988678 ]
   [ 28.87000465  28.87000465  28.87000465]
   [ 30.13193893  30.13193893  30.13193893]]

  [[ 25.64542389  25.64542389  25.64542389]
   [ 26.62529945  26.62529945  26.62529945]
   [ 27.6988678   27.6988678   27.6988678 ]
   [ 28.87000465  28.87000465  28.87000465]
   [ 30.13193893  30.13193893  30.13193893]]

  [[ 25.64542389  25.64542389  25.64542389]
   [ 26.62529945  26.62529945  26.62529945]
   [ 27.6988678   27.6988678   27.6988678 ]
   [ 28.87000465  28.87000465  28.87000465]
   [ 30.13193893  30.13193893  30.13193893]]

  [[ 25.64542389  25.64542389  25.64542389]
   [ 26.62529945  26.62529945  26.62529945]
   [ 27.6988678   27.6988678   27.6988678 ]
   [ 28.87000465  28.87000465  28.87000465]
   [ 30.13193893  30.13193893  30.13193893]]]]

おーほぼほぼ同じだ。適当な別の配列でも試してみよう。

適当な配列でも試してみる

適当な配列

input = np.zeros([1, 5, 5, 3])
num = 1
for h in range(5):
    for w in range(5):
        for c in range(3):
            input[0][h][w][c] = num
            num += 1

結果

[[[[  1.   2.   3.]
   [  4.   5.   6.]
   [  7.   8.   9.]
   [ 10.  11.  12.]
   [ 13.  14.  15.]]

  [[ 16.  17.  18.]
   [ 19.  20.  21.]
   [ 22.  23.  24.]
   [ 25.  26.  27.]
   [ 28.  29.  30.]]

  [[ 31.  32.  33.]
   [ 34.  35.  36.]
   [ 37.  38.  39.]
   [ 40.  41.  42.]
   [ 43.  44.  45.]]

  [[ 46.  47.  48.]
   [ 49.  50.  51.]
   [ 52.  53.  54.]
   [ 55.  56.  57.]
   [ 58.  59.  60.]]

  [[ 61.  62.  63.]
   [ 64.  65.  66.]
   [ 67.  68.  69.]
   [ 70.  71.  72.]
   [ 73.  74.  75.]]]]

自作関数の結果

[[[  0.99883492   1.99766984   2.99650476]
  [  3.97452398   4.96815498   5.96178597]
  [  6.88892644   7.87305879   8.85719114]
  [  9.70624045  10.6768645   11.64748854]
  [ 12.39542092  13.34891484  14.30240875]]

 [[ 14.93127874  15.86448366  16.79768858]
  [ 17.29503693  18.20530203  19.11556713]
  [ 19.47436707  20.35956557  21.24476407]
  [ 21.46299164  22.32151131  23.18003097]
  [ 23.25997335  24.09068669  24.92140002]]

 [[ 24.86882105  25.67104108  26.47326112]
  [ 26.29652922  27.06995655  27.84338388]
  [ 27.55264319  28.29730922  29.04197525]
  [ 28.64841279  29.36462311  30.08083343]
  [ 29.59606986  30.28435055  30.97263125]]

 [[ 30.40824252  31.06929128  31.73034003]
  [ 31.09750348  31.7321464   32.36678933]
  [ 31.67603931  32.28519391  32.89434852]
  [ 32.15542323  32.74006729  33.32471135]
  [ 32.54647174  33.10761781  33.66876387]]

 [[ 32.85916694  33.39784181  33.93651668]
  [ 33.10262795  33.61985651  34.13708507]
  [ 33.28511771  33.78191052  34.27870332]
  [ 33.41407404  33.89141795  34.36876187]
  [ 33.49615612  33.95500757  34.41385903]]]

tf.nn.lrnの結果

[[[[  0.99883491   1.99766982   2.99650478]
   [  3.97452402   4.96815491   5.96178627]
   [  6.88892651   7.8730588    8.85719109]
   [  9.70624065  10.67686462  11.64748859]
   [ 12.39542103  13.34891415  14.30240822]]

  [[ 14.93127823  15.86448288  16.79768753]
   [ 17.29503632  18.20530128  19.11556625]
   [ 19.47436714  20.35956573  21.24476433]
   [ 21.46299171  22.32151222  23.18003082]
   [ 23.25997543  24.09068871  24.92140198]]

  [[ 24.86882019  25.67103958  26.47325897]
   [ 26.29652977  27.06995773  27.8433857 ]
   [ 27.55264282  28.29730988  29.04197502]
   [ 28.6484127   29.36462212  30.08083344]
   [ 29.59606934  30.28435135  30.97263145]]

  [[ 30.40824318  31.06929207  31.73034096]
   [ 31.09750175  31.73214531  32.36678696]
   [ 31.67603874  32.2851944   32.89434814]
   [ 32.15542221  32.74006653  33.32471085]
   [ 32.54647446  33.10762024  33.66876602]]

  [[ 32.85916519  33.39783859  33.93651581]
   [ 33.1026268   33.61985397  34.13708496]
   [ 33.2851181   33.78191376  34.2787056 ]
   [ 33.41407394  33.89141846  34.36876297]
   [ 33.49615479  33.95500946  34.41386032]]]]

ほぼ同じ。

大きい写真にLRNをしてみて結果をみてみる

画像はこれです。

コード

import tensorflow as tf
import numpy as np
from PIL import Image

fpath = './img/sample_pic.jpg'
jpg = tf.read_file(fpath)
img = tf.image.decode_jpeg(jpg, channels=3)
input = tf.cast(tf.reshape(img, [1, 600, 800, 3]), dtype=tf.float32)

depth_radius = 2
bias = 1.0
alpha = 0.001 / 9.0
beta = 0.75

output = tf.nn.lrn(input, depth_radius, bias=bias, alpha=alpha, beta=beta)
with tf.Session() as sess:
    out = sess.run(output)
    print(out)
    Image.fromarray(np.uint8(out[0])).save('.・img/lrn_tf.jpg')

結果

これをCNNに入れ込むと効果が高まるって気づいた人すごいっす。

TensorFlow – tf.nn.max_pool(value, ksize, strides, padding, data_format=’NHWC’, name=None)

tf.nn.max_pool(value, ksize, strides, padding, data_format=’NHWC’, name=None)

  • value: A 4-D Tensor with shape [batch, height, width, channels] and
    type tf.float32.
  • ksize: A list of ints that has length >= 4. The size of the window for
    each dimension of the input tensor.
  • strides: A list of ints that has length >= 4. The stride of the sliding
    window for each dimension of the input tensor.
  • padding: A string, either 'VALID' or 'SAME'. The padding algorithm.
    See the comment here
  • data_format: A string. ‘NHWC’ and ‘NCHW’ are supported.
  • name: Optional name for the operation.

max poolingは、特定の領域から最大の数字を取り出します。ksizeが領域の大きさ、stridesが間隔です。ksizeが[1, 2, 2, 1]で、stridesが[1, 2, 2, 1]であれば、2 x 2のmax poolingです。2 x 2の領域から最大値をとります。間隔が2の場合出力サイズは半分になります。ksizeが[1, 3, 3, 1]で、stridesが[1, 2, 2, 1]の場合、3 x 3の領域から最大値をとり、間隔が2になるので、出力サイズは半分のままですが、数値を広範囲から取得することになるので、よりちょっとの差に動じなくなります。

tf.nn.max_poolの動きを確認してみます。
この画像を使います。

画像を読み込む

とりあえず画像を読み込んで、shapeを表示させてみます。

import tensorflow as tf
import numpy as np
from PIL import Image

fpath = './img/sample_pic.jpg'
jpg = tf.read_file(fpath)
img_arr = tf.image.decode_jpeg(jpg, channels=3)

with tf.Session() as sess:
    img = sess.run(img_arr)
    print(img.shape)

結果

(600, 800, 3)

Max poolingしてみる

import tensorflow as tf
import numpy as np
from PIL import Image

fpath = './img/sample_pic.jpg'
jpg = tf.read_file(fpath)
img_arr = tf.image.decode_jpeg(jpg, channels=3)
img_4d = tf.cast(tf.reshape(img_arr, [1, 600, 800, 3]), tf.float32)
pool = tf.nn.max_pool(img_4d, [1, 2, 2, 1], [1, 2, 2, 1], 'SAME')

with tf.Session() as sess:
    img, pool = sess.run([img_arr, pool])
    print(img.shape)
    print(pool.shape)

結果

(600, 800, 3)
(1, 300, 400, 3)

おーできてるっぽい。

画像を表示してみる。

Image.fromarray(np.uint8(pool.reshape(pool.shape[1:4]))).save('./img/maxpool1.jpg')

結果

では、わかり易くksizeを[1, 10, 10, 1]でやってみます。

import tensorflow as tf
import numpy as np
from PIL import Image

fpath = './img/sample_pic.jpg'
jpg = tf.read_file(fpath)
img_arr = tf.image.decode_jpeg(jpg, channels=3)
img_4d = tf.cast(tf.reshape(img_arr, [1, 600, 800, 3]), tf.float32)
pool = tf.nn.max_pool(img_4d, [1, 10, 10, 1], [1, 2, 2, 1], 'SAME')

with tf.Session() as sess:
    img, pool = sess.run([img_arr, pool])
    print(img.shape)
    print(pool.shape)
    Image.fromarray(np.uint8(pool.reshape(pool.shape[1:4]))).save('./img/maxpool2.jpg')

結果

(600, 800, 3)
(1, 300, 400, 3)

TensorFlow – weight decay

機械学習のweight decayは、重みの2乗ノルム(L2ノルム)を損失関数に加えること。これによって重みが大きいと損失関数の値が大きくなるので、重みが大きくなりすぎないようになる。過学習は重みが大きくなることで発生することが多いからこういうことする。L2ノルムは、各次元の値の2乗の和。

サンプル(TensorFlow使ってない)

weight_decay = 0
for idx in range(1, self.hidden_layer_num + 2):
    W = self.params['W' + str(idx)]
    weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2)
return self.last_layer.forward(y, t) + weight_decay

サンプル(TensorFlow)

def _variable_with_weight_decay(name, shape, stddev, wd):
  """Helper to create an initialized Variable with weight decay.

  Note that the Variable is initialized with a truncated normal distribution.
  A weight decay is added only if one is specified.

  Args:
    name: name of the variable
    shape: list of ints
    stddev: standard deviation of a truncated Gaussian
    wd: add L2Loss weight decay multiplied by this float. If None, weight
        decay is not added for this Variable.

  Returns:
    Variable Tensor
  """
  dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
  var = _variable_on_cpu(
      name,
      shape,
      tf.truncated_normal_initializer(stddev=stddev, dtype=dtype))
  if wd is not None:
    weight_decay = tf.mul(tf.nn.l2_loss(var), wd, name='weight_loss')
    tf.add_to_collection('losses', weight_decay)
  return var

上記の、_variable_on_cpu()は、下記。

def _variable_on_cpu(name, shape, initializer):
  """Helper to create a Variable stored on CPU memory.

  Args:
    name: name of the variable
    shape: list of ints
    initializer: initializer for Variable

  Returns:
    Variable Tensor
  """
  with tf.device('/cpu:0'):
    dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
    var = tf.get_variable(name, shape, initializer=initializer, dtype=dtype)
  return var

そして、_variable_with_weight_decay()は下記のように使われる。

kernel = _variable_with_weight_decay('weights',
                                     shape=[5, 5, 3, 64],
                                     stddev=5e-2,
                                     wd=0.0)

(tf.nn.l2_loss()

多分2乗した合計かその半分が出てくるんじゃないかと思うので、確かめてみる。

import tensorflow as tf

wd = tf.constant(0.01)
W = tf.constant([1., 2., 3.])
l2 = tf.nn.l2_loss(W)
weight_decay = tf.mul(l2, wd)

with tf.Session() as sess:
    l2, weight_decay = sess.run([l2, weight_decay])
    print(l2)
    print(weight_decay)

結果

7.0
0.07

2乗和の半分っぽい。

TensorFlowのサンプルのように、wdを0.0にしてるってことは、weight decayに含めないってことか。畳み込み層は含まず、全結合層だけwdを0.004にしてる。

TensorFlowで損失関数にL2ノルムのweight decayを足してるところ

def loss(logits, labels):
  """Add L2Loss to all the trainable variables.

  Add summary for "Loss" and "Loss/avg".
  Args:
    logits: Logits from inference().
    labels: Labels from distorted_inputs or inputs(). 1-D tensor
            of shape [batch_size]

  Returns:
    Loss tensor of type float.
  """
  # Calculate the average cross entropy loss across the batch.
  labels = tf.cast(labels, tf.int64)
  cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
      labels=labels, logits=logits, name='cross_entropy_per_example')
  cross_entropy_mean = tf.reduce_mean(cross_entropy, name='cross_entropy')
  tf.add_to_collection('losses', cross_entropy_mean)

  # The total loss is defined as the cross entropy loss plus all of the weight
  # decay terms (L2 loss).
  return tf.add_n(tf.get_collection('losses'), name='total_loss')

バッチ処理してるので、cross_entropyの平均をとって、weigt decayが入った’losses’というコレクションに入れて、コレクション内の数字を全部足している。

TensorFlow – tf.add_to_collection

tf.add_to_collection(name, value)は、tf.Graph.add_to_collection(name, value)のラッパーだそうです。

引数

  • name: The key for the collection. The GraphKeys class
    contains many standard names for collections.
  • value: The value to add to the collection.

すごい単純だな。

import tensorflow as tf

tf.add_to_collection('hoge', 12)
tf.add_to_collection('hoge', 25)
hoge = tf.get_collection('hoge')
add = tf.add_n(hoge)

with tf.Session() as sess:
    print(sess.run(add))

結果

37

TensorFlow – tf.train.MonitoredTrainingSession

tf.train.MonitoredTrainingSessionを確認します。
訓練をモニターするのに特化したセッションという感じでしょうか?チュートリアルのコードでは下記のような使われ方をしています。

with tf.train.MonitoredTrainingSession(
    checkpoint_dir=FLAGS.train_dir,
    hooks=[tf.train.StopAtStepHook(last_step=FLAGS.max_steps),
           tf.train.NanTensorHook(loss),
           _LoggerHook()],
    config=tf.ConfigProto(
        log_device_placement=FLAGS.log_device_placement)) as mon_sess:
while not mon_sess.should_stop():
    mon_sess.run(train_op)

普通のセッションを使う代わりに使っています。

https://www.tensorflow.org/api_docs/python/train/distributed_execution#MonitoredTrainingSession

tf.train.MonitoredTrainingSession(master=”, is_chief=True, checkpoint_dir=None, scaffold=None, hooks=None, chief_only_hooks=None, save_checkpoint_secs=600, save_summaries_steps=100, config=None)

引数的に、勝手に変数の保存をしてくれたりするようです。

For a chief, this utility sets proper session initializer/restorer. It also creates hooks related to checkpoint and summary saving. For workers, this utility sets proper session creator which waits for the chief to inialize/restore.

引数は下記です。

  • master: String the TensorFlow master to use.
  • is_chief: If True, it will take care of initialization and recovery the
    underlying TensorFlow session. If False, it will wait on a chief to
    initialize or recover the TensorFlow session.
  • checkpoint_dir: A string. Optional path to a directory where to restore
    variables.
  • scaffold: A Scaffold used for gathering or building supportive ops. If
    not specified, a default one is created. It’s used to finalize the graph.
  • hooks: Optional list of SessionRunHook objects.
  • chief_only_hooks: list of SessionRunHook objects. Activate these hooks if
    is_chief==True, ignore otherwise.
  • save_checkpoint_secs: The frequency, in seconds, that a checkpoint is saved
    using a default checkpoint saver. If save_checkpoint_secs is set to
    None, then the default checkpoint saver isn’t used.
  • save_summaries_steps: The frequency, in number of global steps, that the
    summaries are written to disk using a default summary saver. If
    save_summaries_steps is set to None, then the default summary saver
    isn’t used.
  • config: an instance of tf.ConfigProto proto used to configure the session.
    It’s the config argument of constructor of tf.Session.

hookというのはコールバック的な感じで、session.runの前後に実行できるクラスらしい。これを紐づけることができて、リストで複数のhookを登録することができる。カスタマイズできるのが、SessionRunHookで、それ以外に用途が決まっているhookが複数事前に提供されているといったような感じのイメージを持った気がする。

class tf.train.SessionRunHook
class tf.train.StopAtStepHook
class tf.train.NanTensorHook

configは設定で、log_device_placementは、手動でデバイス指定してる場合にTrueにすると、手動設定したデバイスを選んでくれる的なやつっぽい。自分の非力な1体のPC環境ではあまり関係がないと思う。非力な1体のPC環境の場合、hookは自作する_LoggerHook()だけで、configは未設定でもよさそう。

引数でcheckpoint_dirを聞いてるくらいだから、tf.summary.FileWriter(‘.\logs’, sess.graph)みたいのを書かなくてもログが保存されるようになったりしてるのか試してみようと思います。

import tensorflow as tf
from datetime import datetime
import time

logdir = './logs'

a = tf.constant(2)
b = tf.constant(3)
add = tf.add(a, b)

class Hoge(tf.train.SessionRunHook):
    def begin(self):
        self._step = -1

    def before_run(self, run_context):
        self._step += 1
        self._start_time = time.time()
        return tf.train.SessionRunArgs(add)

    def after_run(self, run_context, run_values):
        duration = time.time() - self._start_time
        result = run_values.results
        if self._step % 10 == 0:
            format_str = 'RESULT: {}, STEP:{}, {:%Y-%m-%d %H:%M:%S}, {:.2}'
            print(format_str.format(result, self._step, datetime.now(), duration))

tf.contrib.framework.get_or_create_global_step()
with tf.train.MonitoredTrainingSession(
        checkpoint_dir=logdir,
        hooks=[Hoge()]) as mon_sess:
    for _ in range(50):
        mon_sess.run(add)

結果

RESULT: 5, STEP:0, 2017-01-24 17:01:32, 0.11
RESULT: 5, STEP:10, 2017-01-24 17:01:33, 0.00098
RESULT: 5, STEP:20, 2017-01-24 17:01:33, 0.0
RESULT: 5, STEP:30, 2017-01-24 17:01:33, 0.0
RESULT: 5, STEP:40, 2017-01-24 17:01:33, 0.00098

そして、logsディレクトリにログファイルが登録されていて、TensorBoardでも見られた。
before_runのreturnで、tf.train.SessionRunArgs(add)とやると、after_runのrun_values.resultsに結果が入ってくる。

TensorFlow – Readerクラスでバッチ処理

参考:
TensorFlow : How To : データを読む
Inputs and Readers
TensorFlowチュートリアル – 畳み込みニューラルネットワーク(翻訳)

tf.train.shuffle_batchというのを使う。シャッフルが不要な時は、tf.train.batchを使う。

tf.train.shuffle_batch

tf.train.shuffle_batch(tensors, batch_size, capacity, min_after_dequeue, num_threads=1, seed=None, enqueue_many=False, shapes=None, allow_smaller_final_batch=False, shared_name=None, name=None)

# min_after_dequeue はバッファ、そこからランダムにサンプリングします、がどのくらい大きいかを定義します
# – 大きければより良いシャッフリング、しかし開始が遅くなりメモリがより多く使用されることを意味します。
# capacity は min_after_dequeue よりも大きくなければなりません。
# そして the amount larger は事前読み込みする最大値を決めます。
# 推奨:
# min_after_dequeue + (num_threads + 小さい安全マージン) * batch_size
min_after_dequeue = 10000
capacity = min_after_dequeue + 3 * batch_size
example_batch, label_batch = tf.train.shuffle_batch(
[example, label], batch_size=batch_size, capacity=capacity,
min_after_dequeue=min_after_dequeue)
return example_batch, label_batch

TensorFlowのcifar10のチュートリアルのコード(cifar10_input.py)に下記のように使われている。

# Create a queue that shuffles the examples, and then
# read 'batch_size' images + labels from the example queue.
num_preprocess_threads = 16
if shuffle:
images, label_batch = tf.train.shuffle_batch(
    [image, label],
    batch_size=batch_size,
    num_threads=num_preprocess_threads,
    capacity=min_queue_examples + 3 * batch_size,
    min_after_dequeue=min_queue_examples)
else:
images, label_batch = tf.train.batch(
    [image, label],
    batch_size=batch_size,
    num_threads=num_preprocess_threads,
    capacity=min_queue_examples + 3 * batch_size)

上記コードのmin_after_dequeue(min_queue_examples)は、下記のように取得されている。

# Ensure that the random shuffling has good mixing properties.
min_fraction_of_examples_in_queue = 0.4
min_queue_examples = int(NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN *
                       min_fraction_of_examples_in_queue)
print ('Filling queue with %d CIFAR images before starting to train. '
     'This will take a few minutes.' % min_queue_examples)

上記のNUM_EXAMPLES_PER_EPOCH_FOR_TRAINは下記のように設定されている。

NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN = 50000

バッチもシャッフルしてくれる。シャッフルするために、事前にデータを何個か読み込んでおいて、その中からシャッフルしてくれる。事前読み込み数がmin_after_dequeueで、これが多いと沢山の中からシャッフルできるからシャッフルされ具合がいい感じになる。でも読み込み数が多いからその分遅くなる。でも途中からは裏で読み込んでおいてくれるので遅くならない。ただメモリ使用量は増える。って感じすか??上記事例だとエポック当たり5万件のデータがある場合、それに0.4をかけた数をmin_after_dequeueにしている。そして推奨値の数式に従って、capacityは、min_after_dequeue + 3 * バッチサイズにしている。

バッチ処理してみる

コードサンプル
下記のfile0.csvとfile1.csvはここで使ったものと同じ内容。

import tensorflow as tf

filenames = ["./hoge/file0.csv", "./hoge/file1.csv"]
num_threads = 2
batch_size = 3
num_per_epoch = 16

filename_queue = tf.train.string_input_producer(filenames)
reader = tf.TextLineReader()
key, value = reader.read(filename_queue)
record_defaults = [[1], [1], [1], [1], [1]]
col1, col2, col3, col4, col5 = tf.decode_csv(value, record_defaults=record_defaults)
features = tf.pack([col1, col2, col3, col4])
label = col5

min_after_dequese = int(num_per_epoch * 0.4)
features, label_batch = tf.train.shuffle_batch(
    [features, label],
    batch_size=batch_size,
    num_threads=num_threads,
    capacity=min_after_dequese + 3 * batch_size,
    min_after_dequeue=min_after_dequese)

labels = tf.reshape(label_batch, [batch_size])

with tf.Session() as sess:
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)

    for i in range(num_per_epoch):
        x, t = sess.run([features, labels])
        print(x)
        print(t)

    coord.request_stop()
    coord.join(threads)

結果

[[600 601 602 603]
 [300 301 302 303]
 [100 101 102 103]]
[16 13 11]
[[200 201 202 203]
 [300 301 302 303]
 [400 401 402 403]]
[12  3 14]
[[800 801 802 803]
 [500 501 502 503]
 [600 601 602 603]]
[18  5  6]
[[700 701 702 703]
 [400 401 402 403]
 [200 201 202 203]]
[17  4  2]
[[100 101 102 103]
 [400 401 402 403]
 [800 801 802 803]]
[1 4 8]
[[100 101 102 103]
 [400 401 402 403]
 [700 701 702 703]]
[ 1 14  7]
[[200 201 202 203]
 [500 501 502 503]
 [600 601 602 603]]
[12  5  6]
[[400 401 402 403]
 [200 201 202 203]
 [300 301 302 303]]
[ 4  2 13]
[[300 301 302 303]
 [600 601 602 603]
 [700 701 702 703]]
[3 6 7]
[[800 801 802 803]
 [500 501 502 503]
 [100 101 102 103]]
[ 8 15 11]
[[100 101 102 103]
 [300 301 302 303]
 [600 601 602 603]]
[ 1  3 16]
[[500 501 502 503]
 [100 101 102 103]
 [700 701 702 703]]
[15 11 17]
[[200 201 202 203]
 [800 801 802 803]
 [600 601 602 603]]
[ 2 18 16]
[[700 701 702 703]
 [400 401 402 403]
 [300 301 302 303]]
[17 14 13]
[[100 101 102 103]
 [500 501 502 503]
 [100 101 102 103]]
[11  5  1]
[[200 201 202 203]
 [400 401 402 403]
 [800 801 802 803]]
[12 14  8]

TensorFlow – 学習精度を上げるために画像加工して増やす

参考:Images
上記を見ると色々な加工関数があるんですね。デコードエンコードも色々便利そうなのがあるんですね。

TensorFlowのチュートリアル「Convolutional Neural Networks」で、cifar10の画像を学習精度を上げるために画像を色々加工して増やしています。何をしていて、どうやればいいのか確認します。ソースコードはここです。

Readクラスでファイルから画像・ラベルデータを取り出した後に下記をしています。この5つの関数達を試していこうと思います。

# Randomly crop a [height, width] section of the image.
distorted_image = tf.random_crop(reshaped_image, [height, width, 3])

# Randomly flip the image horizontally.
distorted_image = tf.image.random_flip_left_right(distorted_image)

# Because these operations are not commutative, consider randomizing
# the order their operation.
distorted_image = tf.image.random_brightness(distorted_image, max_delta=63)
distorted_image = tf.image.random_contrast(distorted_image, lower=0.2, upper=1.8)

# Subtract off the mean and divide by the variance of the pixels.
float_image = tf.image.per_image_standardization(distorted_image)

すでに同じことをやってるサイトを発見しました。
参考:画像の水増し方法をTensorFlowのコードから学ぶ

なんか、上記5つの関数のうち最後の1つが参考サイトだと違う。参考サイトだとホワイトニングしている。参考サイトのおかげで答えが分かりましたが、一応自分でも試しておきます。

tf.random_crop

コード

import tensorflow as tf
import numpy as np
from PIL import Image

height = 300
width = 400

fpath = './img/sample_pic.jpg'
jpg = tf.read_file(fpath)
img = tf.image.decode_jpeg(jpg, channels=3)
cropped = tf.random_crop(img, [height, width, 3])

with tf.Session() as sess:
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)

    for i in range(4):
        img = sess.run(cropped)
        print(img.shape)
        Image.fromarray(np.uint8(img)).save('./img/img{}.jpg'.format(i))

結果

seek設定してないけどランダムになった。seekを明示した場合、seekが同じだと同じ結果になるってことらしい。

tf.image.per_image_standardization

これが参考サイトにない。その代りホワイトニングがなくなっている。
コード

height = 300
width = 400

fpath = './img/sample_pic.jpg'
jpg = tf.read_file(fpath)
img = tf.image.decode_jpeg(jpg, channels=3)
cropped = tf.random_crop(img, [height, width, 3])
result = tf.image.per_image_standardization(cropped)

with tf.Session() as sess:
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)

    for i in range(4):
        img = sess.run(result)
        print(img.shape)
        Image.fromarray(np.uint8(img)).save('./img/img{}.jpg'.format(i))

結果は恐ろしい色合いになった。

こんな恐ろしい画像から学習しているんだなー。

チュートリアルの5関数を適用してみる

コード

fpath = './img/sample_pic.jpg'
jpg = tf.read_file(fpath)
img0 = tf.cast(tf.image.decode_jpeg(jpg, channels=3), tf.float32)
img1 = tf.random_crop(img0, [height, width, 3])
img2 = tf.image.random_flip_left_right(img1)
img3 = tf.image.random_brightness(img2, max_delta=63)
img4 = tf.image.random_contrast(img3, lower=0.2, upper=1.8)
img5 = tf.image.per_image_standardization(img4)

with tf.Session() as sess:
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)
    for i in range(4):
        img = sess.run(img5)
        print(img.shape)
        print(img)
        Image.fromarray(np.uint8(img)).save('./img/img{}.jpg'.format(i))

結果

TensorFlow – 画像ファイルをReaderクラスで読み込む

参考:TensorFlowのReaderクラスを使ってみる

こんな風にしたら単一ファイルを読み込めるらしい。

jpeg_r = tf.read_file(fname)
image = tf.image.decode_jpeg(jpeg_r, channels=3)

コード

import tensorflow as tf
import numpy as np
from PIL import Image

fpath = './img/sample_pic.jpg'
jpg = tf.read_file(fpath)
img = tf.image.decode_jpeg(jpg, channels=3)

with tf.Session() as sess:
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)
    img = sess.run(img)
    Image.fromarray(np.uint8(img)).show()

結果

TensorFlow – Readerクラスでデータを読み込んでみる

参考:TensorFlow : How To : データを読む

ファイルからデータを読む場合の処理の流れ

・ファイルパスのリストを、tf.train.string_input_producer 関数に渡す(shuffle=Trueにしたらepochでファイル名をシャッフルする)
・読み込むデータに合わせてreaderを選択する。
・ファイル名キューをreaderのreadメソッドに渡す。readメソッドは、ファイルとレコード識別キーと、スカラ文字列値を返す。
・スカラ文字列を、サンプルを構成するテンソルに変換するためにデコーダと変換 OPs の一つ(あるいはそれ以上)を利用します。

csvファイルを読んでみる

import tensorflow as tf

#ファイルパスのリストをtf.train.string_input_producerに渡す
filename_queue = tf.train.string_input_producer(["./hoge/file0.csv", "./hoge/file1.csv"])

#カンマ区切りCSVファイルは、TextLinerReaderを使う
#TextLinerReaderクラスを呼び出す
reader = tf.TextLineReader()
#readerクラスのreadメソッドにファイル名キューを渡す
#ファイルとレコードの識別キー, スカラ文字列値
key, value = reader.read(filename_queue)

# 空カラムの場合の、デフォルト値。
record_defaults = [[1], [1], [1], [1], [1]]
# decode_csvでファイルの1行の実際のカラム値を取得できる
col1, col2, col3, col4, col5 = tf.decode_csv(
    value, record_defaults=record_defaults)
#col1-col4までが特徴データ、col5がラベルという想定らしい
features = tf.pack([col1, col2, col3, col4])

with tf.Session() as sess:
    # ファイル名キューへのデータ取り込みを開始する。 (Start populating the filename queue.)
    #readを実行するためにrunまたはevalを呼び出す前に、キューのデータを取り込むためには
    #tf.train.start_queue_runnersを呼び出さなければなりません。
    #そうでないとキューからのファイル名を待っている間、readはブロックします。
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)

    for i in range(1200):
        # 一つのインスタンスを取得する。
        example, label = sess.run([features, col5])

    #runまたはevalが終わったら書く必要があるっぽい。
    coord.request_stop()
    coord.join(threads)

上記コードの動きを色々なパターンでチェックしてみまっす

ちなみに、csvファイルは、下記です。

file0.csv

100,101,102,103,1
200,201,202,203,2
300,301,302,303,3
400,401,402,403,4
500,501,502,503,5
600,601,602,603,6
700,701,702,703,7
800,801,802,803,8

file1.csv

100,101,102,103,11
200,201,202,203,12
300,301,302,303,13
400,401,402,403,14
500,501,502,503,15
600,601,602,603,16
700,701,702,703,17
800,801,802,803,18

下記を試してみます。コードは基本上記と同じですが、exmapleとlabelを3回表示させています。結果は、file0.csvとfile1.csvのどちらかの1~3行目を順番に取得していることが分かりました。ファイルから取り出すのは1行目から順番ですが、取り出すファイル自体は勝手にランダムな感じにしてくれてるっぽいです。

import tensorflow as tf

filename_queue = tf.train.string_input_producer(["./hoge/file0.csv", "./hoge/file1.csv"])
reader = tf.TextLineReader()
key, value = reader.read(filename_queue)
record_defaults = [[1], [1], [1], [1], [1]]
col1, col2, col3, col4, col5 = tf.decode_csv(value, record_defaults=record_defaults)
features = tf.pack([col1, col2, col3, col4])

with tf.Session() as sess:
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)

    for i in range(3):
        example, label = sess.run([features, col5])
        print('Step: {}'.format(i))
        print(example)
        print(label)
        print('------------------')

    coord.request_stop()
    coord.join(threads)

結果(下記のときと、file1.csvの内容の時がランダムに変わる)

Step: 0
[100 101 102 103]
1
------------------
Step: 1
[200 201 202 203]
2
------------------
Step: 2
[300 301 302 303]
3
------------------

epochでファイルが変わるか確認する

tf.train.string_input_producerの引数にshuffle=Trueを渡すと、epochでファイル名をシャッフルすると書いてありますので、それも試してみようと思います。まずは、shuffle=Trueを設定しない場合。

import tensorflow as tf

filename_queue = tf.train.string_input_producer(["./hoge/file0.csv", "./hoge/file1.csv"])
reader = tf.TextLineReader()
key, value = reader.read(filename_queue)
record_defaults = [[1], [1], [1], [1], [1]]
col1, col2, col3, col4, col5 = tf.decode_csv(value, record_defaults=record_defaults)
features = tf.pack([col1, col2, col3, col4])

with tf.Session() as sess:
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)

    for i in range(20):
        example, label = sess.run([features, col5])
        print('Step: {}, Label: {}'.format(i, label))

    coord.request_stop()
    coord.join(threads)

結果
下記のようになりました。file0と1だけだと、シャッフルしてるか分かりづらいですね。。とりあえずこれはシャッフルはしてないです。何回かやってみます。

Step: 0, Label: 11
Step: 1, Label: 12
Step: 2, Label: 13
Step: 3, Label: 14
Step: 4, Label: 15
Step: 5, Label: 16
Step: 6, Label: 17
Step: 7, Label: 18
Step: 8, Label: 1
Step: 9, Label: 2
Step: 10, Label: 3
Step: 11, Label: 4
Step: 12, Label: 5
Step: 13, Label: 6
Step: 14, Label: 7
Step: 15, Label: 8
Step: 16, Label: 11
Step: 17, Label: 12
Step: 18, Label: 13
Step: 19, Label: 14

同じコードをもう一度実行したら下記になりました。これぞまさにシャッフルですね。ということは、デフォルトでshuffle=Trueが設定されているようです。

Step: 0, Label: 1
Step: 1, Label: 2
Step: 2, Label: 3
Step: 3, Label: 4
Step: 4, Label: 5
Step: 5, Label: 6
Step: 6, Label: 7
Step: 7, Label: 8
Step: 8, Label: 11
Step: 9, Label: 12
Step: 10, Label: 13
Step: 11, Label: 14
Step: 12, Label: 15
Step: 13, Label: 16
Step: 14, Label: 17
Step: 15, Label: 18
Step: 16, Label: 11
Step: 17, Label: 12
Step: 18, Label: 13
Step: 19, Label: 14

それでは、shuffle=False設定にしてみます。
コードで変更するのは下記だけです。

filename_queue = tf.train.string_input_producer(["./hoge/file0.csv", "./hoge/file1.csv"], shuffle=False)

結果
何回やっても下記になりました。shffle=Falseにすると、最初に取得するファイルも、tf.train.string_input_producerに渡した順番通りで固定のようです。

Step: 0, Label: 1
Step: 1, Label: 2
Step: 2, Label: 3
Step: 3, Label: 4
Step: 4, Label: 5
Step: 5, Label: 6
Step: 6, Label: 7
Step: 7, Label: 8
Step: 8, Label: 11
Step: 9, Label: 12
Step: 10, Label: 13
Step: 11, Label: 14
Step: 12, Label: 15
Step: 13, Label: 16
Step: 14, Label: 17
Step: 15, Label: 18
Step: 16, Label: 1
Step: 17, Label: 2
Step: 18, Label: 3
Step: 19, Label: 4

何をしたらエラーになるか確認してみる

上記のコードで、csvファイルの1行当たりのカラム数が5より大きい場合はエラーになりました。

tensorflow.python.framework.errors_impl.InvalidArgumentError: Expect 5 fields but have 6 in record 0

カラム数が5より小さくてもエラーになりました。

tensorflow.python.framework.errors_impl.InvalidArgumentError: Expect 5 fields but have 4 in record 0

record_defaultsを下記のように変えたらエラーになりました。record_defaultsの形状をみて、想定しているデータの個数やデータ型を想定しているようです。それと合わないデータが入ってたらエラーを出すようです。

record_defaults = [[1.0], [1], [1], [1], [1]]

エラー

ValueError: Tensor conversion requested dtype float32 for Tensor with dtype int32: 'Tensor("DecodeCSV:1", shape=(), dtype=int32)'

record_defaultsを下記のように変えたらエラーになりました。shapeのランクが1じゃないといけないらしいです。shapeが(5, 0)になるからダメで、(5, 1)になるようにしないといけないってことかな?

record_defaults = [1, 1, 1, 1, 1]

エラー

tensorflow.python.framework.errors_impl.InvalidArgumentError: Shape must be rank 1 but is rank 0 for 'DecodeCSV' (op: 'DecodeCSV') with input shapes: [], [], [], [], [], [].

record_defaultsを下記にしたら、エラーになりました。numpyの配列だとダメっぽいです。

record_defaults = np.zeros([5, 1], dtype=np.int)

エラー

TypeError: Expected list for 'record_defaults' argument to 'DecodeCSV' Op, not [[0] [0] [0] [0] [0]].

これだとOKでした。

record_defaults = np.zeros([5, 1], dtype=np.int).tolist()

固定長バイナリデータファイルを読み込む

各レコードがバイトの固定数である、バイナリ・ファイルを読むためには、 tf.decode_raw 演算とともに tf.FixedLengthRecordReader を使用します。decode_raw 演算は文字列から uint8 テンソルに変換します。

例えば、CIFAR-10 データセット は、各レコードがバイトの固定長を使用して表される、ファイルフォーマットを使用します: ラベルのための1 バイト、続いて 3072 バイトの画像データです。ひとたび uint8 テンソルを持てば、標準操作で各部分をスライスし必要に応じて再フォーマットすることができます。CIFAR-10 については、どのように読みデコードするかを tensorflow/models/image/cifar10/cifar10_input.py で見ることができ、このチュートリアル で説明されています。

コードサンプル

import tensorflow as tf
import os

data_dir = './hoge/cifar-10-batches-bin'
label_bytes = 1
height = 32
width = 32
depth = 3
image_bytes = height * width * depth
record_bytes = label_bytes + image_bytes

filenames = [os.path.join(data_dir, 'data_batch_%d.bin' % i)
             for i in range(1, 6)]
for f in filenames:
    if not tf.gfile.Exists(f):
      raise ValueError('Failed to find file: ' + f)

filename_queue = tf.train.string_input_producer(filenames)
reader = tf.FixedLengthRecordReader(record_bytes=record_bytes)
key, value = reader.read(filename_queue)
data = tf.decode_raw(value, tf.uint8)
label = tf.cast(tf.strided_slice(data, [0], [label_bytes], [1]), tf.int32)
img = tf.reshape(
    tf.strided_slice(data, [label_bytes], [record_bytes], [1]),
    [depth, height, width])
uint8image = tf.transpose(img, [1, 2, 0])

with tf.Session() as sess:
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(coord=coord)

    for i in range(1):
        uint8image, label = sess.run([uint8image, label])
        print(label.shape)
        print(uint8image.shape)

    coord.request_stop()
    coord.join(threads)

結果

(1,)
(32, 32, 3)

これで、cifar10のデータを読み込めました。

TensorFlow – tf.strided_slice関数を調べる

tf.strided_sliceを調べます。TensorFlowのGithubにのってる説明ページはこれです。

tf.strided_slice(input_, begin, end, strides=None, begin_mask=0, end_mask=0, ellipsis_mask=0, new_axis_mask=0, shrink_axis_mask=0, var=None, name=None) {#strided_slice}

実験してみる

コード

import tensorflow as tf

tensor = tf.constant([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
result = tf.strided_slice(tensor, [0], [9], [2])

with tf.Session() as sess:
    result = sess.run(result)
    print(result)

結果

[1 3 5 7 9]

とりあえず、inputは元データ、beginは開始位置、endは終了位置、stridesは間隔のようです。以前はstridesを指定しない場合、デフォルトでstridesを1とみなしていたようですが、最近のtensorflowのアップデートで、stridesも明示しないとエラーになるようになったようです。ちなみに、終了位置は普通の配列のスライスと同じで、指定したインデックスのひとつ前までになります。

これだけなら簡単なのですが、他にも色々引数があるし、inputもbeginも多次元配列に対応しているようです。

コード

import tensorflow as tf

tensor = tf.constant([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]])
result = tf.strided_slice(tensor, [0, 5], [2, 8], [1, 1])

with tf.Session() as sess:
    result = sess.run(result)
    print(result)

結果

[[ 6  7  8]
 [16 17 18]]

begin、end、stridesはnumpyのshapeのような感じで入れていくようです。コードだとbeginは[0, 5]ですが、これは1次元目は0から始まり、2次元目は5から始まるということになるようです。

Python3 – cifar10をダウンロードして画像を表示させてみる

参考:
Convolutional Neural Networks
https://github.com/tensorflow/models/tree/master/tutorials/image/cifar10/

上記githubに実際のコードがあります。
Cifar10というのは、10種類のカラー画像が沢山入ってるやつで、機械学習によく使われるようです。場所は下記です。
http://www.cs.toronto.edu/~kriz/cifar.html
Cifar100というのもあるようです。

データは、pythonバージョンとかバイナリバージョンとかありますが、TensorFlowが使ってるのは、バイナリバージョンです。バイナリバージョンの説明として、上記本家サイトに下記のように書いてあります。

The binary version contains the files data_batch_1.bin, data_batch_2.bin, …, data_batch_5.bin, as well as test_batch.bin. Each of these files is formatted as follows:
<1 x label><3072 x pixel>

<1 x label><3072 x pixel>
In other words, the first byte is the label of the first image, which is a number in the range 0-9. The next 3072 bytes are the values of the pixels of the image. The first 1024 bytes are the red channel values, the next 1024 the green, and the final 1024 the blue. The values are stored in row-major order, so the first 32 bytes are the red channel values of the first row of the image.

Each file contains 10000 such 3073-byte “rows” of images, although there is nothing delimiting the rows. Therefore each file should be exactly 30730000 bytes long.

There is another file, called batches.meta.txt. This is an ASCII file that maps numeric labels in the range 0-9 to meaningful class names. It is merely a list of the 10 class names, one per row. The class name on row i corresponds to numeric label i.

ダウンロード&展開

ここにダウンロードのコードをいくつか書きました。ここではTensorFlowのコードサンプルと同じくurllib.request.urlretrieveを使います。

import os
import sys
import urllib.request
import tarfile
 
url = 'http://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz'
dirpath = './hoge'
filename = url.split('/')[-1]
filepath = os.path.join(dirpath, filename)
def _progress(cnt, chunk, total):
  now = cnt * chunk
  if(now > total): now = total
  sys.stdout.write('\rdownloading {} {} / {} ({:.1%})'.format(filename, now, total, now/total))
  sys.stdout.flush()
urllib.request.urlretrieve(url, filepath, _progress)
tarfile.open(filepath, 'r:gz').extractall(dirpath)

展開された内容

$ ls -go
-rw-r--r-- 1       61 6月   5  2009 batches.meta.txt
-rw-r--r-- 1 30730000 6月   5  2009 data_batch_1.bin
-rw-r--r-- 1 30730000 6月   5  2009 data_batch_2.bin
-rw-r--r-- 1 30730000 6月   5  2009 data_batch_3.bin
-rw-r--r-- 1 30730000 6月   5  2009 data_batch_4.bin
-rw-r--r-- 1 30730000 6月   5  2009 data_batch_5.bin
-rw-r--r-- 1       88 6月   5  2009 readme.html
-rw-r--r-- 1 30730000 6月   5  2009 test_batch.bin

最初の1バッチがラベル、その次からは赤、緑、青の順で1024バイトずつ入ってる。画像の大きさは32 x 32(=1024)。画像は各ファイルに1万枚分ある。
(1024 x 3 + 1) x 10000 = 30,730,000(bytes)

試しに画像を表示させてみる

試しにdata_batch_1.binから1枚分取得して表示させてみます。下記のreshapeとかtransposeで頭がこんがらがったのでここテストして頭を整理しました。

import numpy as np
from PIL import Image

path = './hoge/cifar-10-batches-bin/data_batch_1.bin'
data_size = 32 * 32 * 3 + 1
with open(path, 'rb') as f:
    data = np.frombuffer(f.read(), np.uint8, count=data_size)
label = data[0]
img_arr = data[1:]
img = img_arr.reshape(3, 32, 32).transpose(1, 2, 0)
Image.fromarray(img).show()

10枚取得してラベルもあわせて表示してみる

コードサンプル

import numpy as np
from PIL import Image, ImageDraw

path = './hoge/cifar-10-batches-bin/data_batch_1.bin'
names = ['airplane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
label_size = 1
img_size = 32 * 32 * 3
data_size = label_size + img_size
data_num = 10

with open(path, 'rb') as f:
    data = np.frombuffer(f.read(), np.uint8, count=data_size*data_num)
label = np.zeros(data_num)
img_arr = np.zeros([data_num, img_size])
for i in range(data_num):
    start = i * data_size
    label[i] = data[start]
    img_arr[i] = data[start + 1 : start + data_size]
img = img_arr.reshape(data_num, 3, 32, 32).transpose(0, 2, 3, 1)

canvas = Image.new('RGB', (320, 175), (240, 240, 240))
draw = ImageDraw.Draw(canvas)
for i in range(data_num):
    num = i if i < 5 else i - 5
    x = 20 + (32 + 30) * num
    y = 20 if i < 5 else 20 + 32 + 45
    canvas.paste(Image.fromarray(np.uint8(img[i])), (x, y))
    draw.text((x, y + 32 + 10), names[int(label[i])], fill='#000000')
canvas.show()

結果

Numpy – 配列の形を変えるテスト

TensorFlowでは頻繁に配列の形を変換しますが、結構混乱して理解するのに時間がかかります。Numpy.reshapeとかNumpy.transposeの動きをシンプルな配列で確認してみます。

これから操作する配列は、下記のようなCifar10の画像データの構造をイメージしてます。
最初の4個は赤。次の4個は緑、最後の4個は青。
最初は上記が一次元の配列に格納されている状態にして、最終的にはこれを下記の並びにしたいです。縦、横、チャンネルの3次元配列にしたいということです。

[縦、横、チャンネル]

4個ずつ赤・緑・青があるとういことは、2×2の画像サイズになります。よって、配列のshapeは、[2, 2, 3]になることを想定しております。このshapeになりつつ、配列データの構造に不整合がなければ形の変換が成功したことになります。

コード

import numpy as np

arr = np.array([1, 2, 3, 4, 11, 12, 13, 14, 21, 22, 23, 24])

print('これだとダメ')
arr2 = arr.reshape(2, 2, 3)
print(arr2)
print('----------------------')

print('引数の順番に全体を分割していくと考えたら良さそう')
print('今は赤、緑、青の順に並んでるので、最初にチャンネル数で分けるしかない')
print('最初に全体を3つに分割して、次に2つ(縦)に分割して、最初に2つ(横)に分割する')
arr3 = arr.reshape(3, 2, 2)
print(arr3)
print('----------------------')

print('最終的に縦、横、チャンネルの順にしたいのでtransposeで順番変える')
arr4 = arr3.transpose(1, 2, 0)
print(arr4)

結果

これだとダメ
[[[ 1  2  3]
  [ 4 11 12]]

 [[13 14 21]
  [22 23 24]]]
----------------------
引数の順番に全体を分割していくと考えたら良さそう
今は赤、緑、青の順に並んでるので、最初にチャンネル数で分けるしかない
最初に全体を3つに分割して、次に2つ(縦)に分割して、最初に2つ(横)に分割する
[[[ 1  2]
  [ 3  4]]

 [[11 12]
  [13 14]]

 [[21 22]
  [23 24]]]
----------------------
最終的に縦、横、チャンネルの順にしたいのでtransposeで順番変える
[[[ 1 11 21]
  [ 2 12 22]]

 [[ 3 13 23]
  [ 4 14 24]]]

TensorFlow – tf.gfile.Exists

tf.gfile.Existsを調べます。

コード例

if tf.gfile.Exists(FLAGS.train_dir):
  tf.gfile.DeleteRecursively(FLAGS.train_dir)
tf.gfile.MakeDirs(FLAGS.train_dir)

サンプルコード

import tensorflow as tf

dir_path = './hoge'
if tf.gfile.Exists(dir_path):
  tf.gfile.DeleteRecursively(dir_path)
tf.gfile.MakeDirs(dir_path)

hogeディレクトリがない状態で実行すると、hogeディレクトリが作られる。
空のhogeディレクトリがある状態で実行すると、hogeディレクトリが削除されて、また作られる。
適当なファイルやディレクトリを入れたhogeディレクトリがある状態で実行すると、hogeディレクトリが削除されて、また作られる。

なんかただディレクトリあったら消してつくってるだけで、何か特別なファイルがあったらそのファイルだけ消すとかそういう特別なことしてるわけじゃないっぽい。なんでわざわざこれ使うのかな?

TensorFlow – tf.app.flags.FLAGS(ファイル実行時にパラメタを付与できるようにする)

tf.app.flags.FLAGSを使うと、TensorFlowのPythonファイルを実行する際にパラメタを付与できるようになる。

下記のようにすると、パラメタ付与が可能になり、デフォルト値やヘルプ画面の説明文を登録できる。tf.app.flags.DEFINE_stringは、String型用で、他にtf.app.flags.DEFINE_boolean、tf.app.flags.DEFINE_integer等を型に合わせて使う。

tf.app.flags.DEFINE_string('変数名', 'デフォルト値', """説明文""")

コードサンプル (test.py)

import tensorflow as tf

FLAGS = tf.app.flags.FLAGS
tf.app.flags.DEFINE_integer('data_num', 100, """データ数""")
tf.app.flags.DEFINE_string('img_path', './img', """画像ファイルパス""")

def main(argv):
    print(FLAGS.data_num, FLAGS.img_path)

if __name__ == '__main__':
    tf.app.run()

ヘルプを表示する

$ python test.py --help

結果

usage: test.py [-h] [--data_num DATA_NUM] [--img_path IMG_PATH]

optional arguments:
  -h, --help           show this help message and exit
  --data_num DATA_NUM  データ数
  --img_path IMG_PATH  画像ファイルパス

実行例

$ python test.py --data_num 35

結果

35 ./img