pyhaya’s diary

プログラミング、特にPythonについての記事を書きます。Djangoや機械学習などホットな話題をわかりやすく説明していきたいと思います。

TensorFlowでUNetを構築する

この記事では、Tensorflowを使ってUNetを構築し、最終的には画像から猫を認識するように訓練するやり方を紹介します。

環境

  • Ubuntu 18.04
  • Python 3.6.8
  • Tensorflow 1.12.0
  • GeForce RTX2080

UNetとは何か

セマンティック・セグメンテーション(semantic segmentation)

UNetというのは機械学習モデルの名前で、セマンティク・セグメンテーションを行うために使われます。セマンティック・セグメンテーションというのは、画像をピクセル単位でいくつかのクラスに分類する画像処理の手法です。例えば下のような人と馬の画像を処理すると右のように人(薄ピンク)と馬(ピンク)、そして背景(黒)をピクセル単位で分類します。

f:id:pyhaya:20190502214459p:plain
https://nicolovaligi.com/deep-learning-models-semantic-segmentation.html から引用
似たようなタスクとして、画像から物体を認識する場合に物体があると考えられる領域を長方形で認識して表示するものがあります(下図)。これと比較するとセマンティック・セグメンテーションではより高度な処理を行っていることがわかります。
f:id:pyhaya:20190502215611j:plain
https://gigazine.net/news/20140920-revolutionary-machine-vision/ から引用

UNetの構造

UNetは2015年にドイツの大学の研究グループが発表したネットワークです。名前の由来はそのネットワークの形状で、U字型をしているためにこのように呼ばれています。

f:id:pyhaya:20190502225111p:plain
論文から引用

arxiv.org

UNetは基本的には畳み込みニューラルネットワーク(CNN)で、その特徴は大きく分けて次のような2種類の処理に分けて考えることができます。

1. ダウンサンプリング
畳み込みでfeature mapを倍にしながらmax poolingで画像サイズを小さくしていく
2. アップサンプリング
transpose convolution*1で画像サイズをもとに戻していく。このときダウンサンプリング中のデータを加えながら処理を進めていく(図の灰色矢印)

論文では入力画像は大きさが572x572でチャネル数が1なのでグレースケールの画像になっています。そして最終的には出力が388x388でチャネル数が2になっています。これは少し説明が必要で、論文ではゼロパディングをしていないので、出力が入力よりも小さくなります。まず、この論文では一つの画像を一度にセグメンテーションするのではなく、いくつかの388x388の領域に分割し、最後に出力結果をつなぎ合わせて最終的なセグメンテーション結果とします。そして388xx388の大きさの領域をセグメンテーションするためにその周りを含めて572x572の領域をネットワークに入れます。処理したい画像領域が元の画像の端で、572x572に拡大できないときには、足りない部分を元の画像の端を鏡面とした鏡映操作をして補います。

f:id:pyhaya:20190502231618p:plain
論文より引用

出力のチャネル数2は判別するクラスの数によります。この場合には判別するクラスが2つとなっているため出力のチャネル数が2になっています。この2つのクラスをクラス1, クラス2と書くことにすると、第一チャネルはクラス1に分類される部分だけ1でほかはゼロ、そして第二チャネルはクラス2に分類されるピクセルだけ1でほかはゼロというようなOne-Hot表現になっています。

UNetのPython(Tensorflow)での実装

では、Tensorflowを使ってUNetを実装してみます。実装では
github.com
を参考にさせていただきました。

UNet本体

UNetの本体はTensorflowで書くと下のようにかけます。

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

import main

class UNet:
  def __init__(self, classes):
    self.IMAGE_DIR = './dataset/raw_images'
    self.SEGMENTED_DIR = './dataset/segmented_images'
    self.VALIDATION_DIR = './dataset/validation'
    self.classes = classes
    self.X = tf.placeholder(tf.float32, [None, 128, 128, 3]) 
    self.y = tf.placeholder(tf.int16, [None, 128, 128, self.classes])
    self.is_training = tf.placeholder(tf.bool)

  @staticmethod
  def conv2d(
    inputs, filters, kernel_size=3, activation=tf.nn.relu, l2_reg=None, 
    momentum=0.9, epsilon=0.001, is_training=False,
    ):
    """
    convolutional layer. If the l2_reg is a float number, L2 regularization is imposed.
    
    Parameters
    ----------
      inputs: tf.Tensor
      filters: Non-zero positive integer
        The number of the filter 
      activation: 
        The activation function. The default is tf.nn.relu
      l2_reg: None or float
        The strengthen of the L2 regularization
      is_training: tf.bool
        The default is False. If True, the batch normalization layer is added.
      momentum: float
        The hyper parameter of the batch normalization layer
      epsilon: float
        The hyper parameter of the batch normalization layer

    Returns
    -------
      layer: tf.Tensor
    """
    regularizer = tf.contrib.layers.l2_regularizer(scale=l2_reg) if l2_reg is not None else None
    layer = tf.layers.conv2d(
      inputs=inputs,
      filters=filters,
      kernel_size=kernel_size,
      padding='SAME',
      activation=activation,
      kernel_regularizer=regularizer
    )

    if is_training is not None:
      layer = tf.layers.batch_normalization(
        inputs=layer,
        axis=-1,
        momentum=momentum,
        epsilon=epsilon,
        center=True,
        scale=True,
        training=is_training
      )

    return layer

  @staticmethod
  def trans_conv(inputs, filters, activation=tf.nn.relu, kernel_size=2, strides=2, l2_reg=None):
    """
    transposed convolution layer.

    Parameters
    ---------- 
      inputs: tf.Tensor
      filters: int 
        the number of the filter
      activation: 
        the activation function. The default function is the ReLu.
      kernel_size: int
        the kernel size. Default = 2
      strides: int
        strides. Default = 2
      l2_reg: None or float 
        the strengthen of the L2 regularization.

    Returns
    -------
      layer: tf.Tensor
    """
    regularizer = tf.contrib.layers.l2_regularizer(scale=l2_reg) if l2_reg is not None else None

    layer = tf.layers.conv2d_transpose(
      inputs=inputs,
      filters=filters,
      kernel_size=kernel_size,
      strides=strides,
      kernel_regularizer=regularizer
    )

    return layer

  @staticmethod
  def pooling(inputs):
    return tf.layers.max_pooling2d(inputs=inputs, pool_size=2, strides=2)


  def UNet(self, is_training, l2_reg=None):
    """
    UNet structure.

    Parameters
    ----------
      l2_reg: None or float
        The strengthen of the L2 regularization.
      is_training: tf.bool
        Whether the session is for training or validation.

    Returns
    -------
      outputs: tf.Tensor
    """
    conv1_1 = self.conv2d(self.X, filters=64, l2_reg=l2_reg, is_training=is_training)
    conv1_2 = self.conv2d(conv1_1, filters=64, l2_reg=l2_reg, is_training=is_training)
    pool1 = self.pooling(conv1_2)

    conv2_1 = self.conv2d(pool1, filters=128, l2_reg=l2_reg, is_training=is_training)
    conv2_2 = self.conv2d(conv2_1, filters=128, l2_reg=l2_reg, is_training=is_training)
    pool2 = self.pooling(conv2_2)

    conv3_1 = self.conv2d(pool2, filters=256, l2_reg=l2_reg, is_training=is_training)
    conv3_2 = self.conv2d(conv3_1, filters=256, l2_reg=l2_reg, is_training=is_training)
    pool3 = self.pooling(conv3_2)

    conv4_1 = self.conv2d(pool3, filters=512, l2_reg=l2_reg, is_training=is_training)
    conv4_2 = self.conv2d(conv4_1, filters=512, l2_reg=l2_reg, is_training=is_training)
    pool4 = self.pooling(conv4_2)

    conv5_1 = self.conv2d(pool4, filters=1024, l2_reg=l2_reg)
    conv5_2 = self.conv2d(conv5_1, filters=1024, l2_reg=l2_reg)
    concat1 = tf.concat([conv4_2, self.trans_conv(conv5_2, filters=512, l2_reg=l2_reg)], axis=3)

    conv6_1 = self.conv2d(concat1, filters=512, l2_reg=l2_reg)
    conv6_2 = self.conv2d(conv6_1, filters=512, l2_reg=l2_reg)
    concat2 = tf.concat([conv3_2, self.trans_conv(conv6_2, filters=256, l2_reg=l2_reg)], axis=3)

    conv7_1 = self.conv2d(concat2, filters=256, l2_reg=l2_reg)
    conv7_2 = self.conv2d(conv7_1, filters=256, l2_reg=l2_reg)
    concat3 = tf.concat([conv2_2, self.trans_conv(conv7_2, filters=128, l2_reg=l2_reg)], axis=3)

    conv8_1 = self.conv2d(concat3, filters=128, l2_reg=l2_reg)
    conv8_2 = self.conv2d(conv8_1, filters=128, l2_reg=l2_reg)
    concat4 = tf.concat([conv1_2, self.trans_conv(conv8_2, filters=64, l2_reg=l2_reg)], axis=3)

    conv9_1 = self.conv2d(concat4, filters=64, l2_reg=l2_reg)
    conv9_2 = self.conv2d(conv9_1, filters=64, l2_reg=l2_reg)
    outputs = self.conv2d(conv9_2, filters=self.classes, kernel_size=1, activation=None)

    return outputs

  def train(self, parser):
    """
    training operation
    argument of this function are given by functions in main.py

    Parameters
    ----------
      parser: 
        the paser that has some options
    """
    epoch = parser.epoch
    l2 = parser.l2
    batch_size = parser.batch_size
    train_val_rate = parser.train_rate

    output = self.UNet(l2_reg=l2, is_training=self.is_training)
    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.y, logits=output))
    update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
    with tf.control_dependencies(update_ops):
      train_ops = tf.train.AdamOptimizer(parser.learning_rate).minimize(loss)

    init = tf.global_variables_initializer()
    saver = tf.train.Saver(max_to_keep=100)
    all_train, all_val = main.load_data(self.IMAGE_DIR, self.SEGMENTED_DIR, n_class=2, train_val_rate=train_val_rate)
    with tf.Session() as sess:
      init.run()
      for e in range(epoch):
        data = main.generate_data(*all_train, batch_size)
        val_data = main.generate_data(*all_val, len(all_val[0]))
        for Input, Teacher in data:
          sess.run(train_ops, feed_dict={self.X: Input, self.y: Teacher, self.is_training: True})
          ls = loss.eval(feed_dict={self.X: Input, self.y: Teacher, self.is_training: None})
          for val_Input, val_Teacher in val_data:
            val_loss = loss.eval(feed_dict={self.X: val_Input, self.y: val_Teacher, self.is_training: None})

        print(f'epoch #{e + 1}, loss = {ls}, val loss = {val_loss}')
        if e % 100 == 0:
          saver.save(sess, f"./params/model_{e + 1}epochs.ckpt")

      self.validation(sess, output)

  def validation(self, sess, output):
    val_image = main.load_data(self.VALIDATION_DIR, '', n_class=2, train_val_rate=1)[0]
    data = main.generate_data(*val_image, batch_size=1)
    for Input, _ in data:
      result = sess.run(output, feed_dict={self.X: Input, self.is_training: None}) 
      break
    
    result = np.argmax(result[0], axis=2)
    ident = np.identity(3, dtype=np.int8)
    result = ident[result]*255

    plt.imshow((Input[0]*255).astype(np.int16))
    plt.imshow(result, alpha=0.2)
    plt.show()

長いけれども、やっていることは大したことなく、クラス内部に畳み込み層、transpose convolution層、プーリング層をメソッドとして定義しておいてUNetメソッドで本体を定義しています。

論文とこの実装は違っているところもあります。

trainメソッドで実際の学習を実行します。

学習データの作成

次に学習データをUNetに流し込む部分を書かなければいけませんが、その前に、学習データを作成する必要があります。画像のセグメンテーションにはlabelmeというフリーソフトを使いました。
github.com

GitHubに書いてあるインストール方法でインストールし、セグメンテーションしました。

f:id:pyhaya:20190504195754p:plainf:id:pyhaya:20190504195731p:plain
公開されているようなセマンティック・セグメンテーションのデータセットに比べると雑ですが、これで試してみます。

訓練してみる

上の要領で作ったデータを使って(76枚の画像データ)実際に訓練してみました。画像データが少ないのでそこまでうまくは行かないと思いますがこれでどの程度まで行くのか見てみます。

使ったパラメータは

  • 学習レート:0.0001
  • レーニングデータ:90%
  • バッチサイズ:20
  • L2正則化: 0.05
  • エポック数:100

のようになっています。結果を見るために適当なネコ画像を拾ってきて確認してみると下のようになっています。
f:id:pyhaya:20190512124258p:plain

猫の背中はよく認識できいますが、その他の部分はまだまだです。人間の視点から見ると猫といったら耳だろという感じですが、このモデルからしたら背中の方が認識しやすいようです。最もこれはこのモデルで判別するのが背景か猫の2択だけであるということも関係している可能性があります。つまり分類対象に犬などを入れたら状況は全然変わってくるでしょう(背中だけ見て犬猫を分類しろと言われたら難しい気がします)。

また、ここには載せていませんが、ロスを見ると完全に過学習しているような振る舞いをしておりやはりデータ数が足りないというのがネックになっています。今後はデータを増やすか水増しするかしていく予定です。

コード全体

この記事で用いたコードの全体はGitHubで公開しています。画像の前処理等が知りたい人は下のリンクへどうぞ。
github.com

*1:畳み込みの逆の操作のようなもの(数学的な逆演算ではない)、日本語訳がわからない

Dockerを動かしてみる

Dockerを勉強し始めたので、学習記録としてまとめておきます。内容は基本的に
knowledge.sakura.ad.jp
のDocker入門で勉強したものを基礎としており、自分が引っかかったとことを付け足して書いています。

環境

Dockerのインストール

この記事を参考にしてインストールした。

qiita.com

Dockerイメージをダウンロードしてみる

まず最初に、軽量なWebサーバとして有名なNginxのDockerイメージを入れてみました。
hub.docker.com
まず、DockerHubのアカウントを作成しました。作成できたら、ここのIDとパスワードを使ってdockerでログインします。ログインをすることで、Docker Hubからイメージをpullすることができるようになります。(ログインしていない状態でpullしようとすると権限がありません、と怒られます。)

$ docker login
Authenticating with existing credentials...
Stored credentials invalid or expired
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username:      
Password: 

では、次にNginxのリポジトリをpullしてきます。

$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
27833a3ba0a5: Pull complete 
ea005e36e544: Pull complete 
d172c7f0578d: Pull complete 
Digest: sha256:e71b1bf4281f25533cf15e6e5f9be4dac74d2328152edf7ecde23abc54e16c1c
Status: Downloaded newer image for nginx:latest
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              27a188018e18        12 days ago         109MB

Nginxを動かしてみる

$ docker run -d --name nginx-container -p 8181:80 nginx

このようにすることで、Dockerイメージを動かすことができます。プロセスを確認すると、

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
21f11ea7bc28        nginx               "nginx -g 'daemon of…"   2 minutes ago       Up 2 minutes        0.0.0.0:8181->80/tcp   nginx-container

STATUSがUP、つまり現在動いていることがわかります。このとき、このプロセスが使っているのが、0.0.0.0:8181ポートであることがわかります。このポートにアクセスすれば「Welcome to nginx」のページが現れます。しかし、ファイアーウォールの設定でこのポートが閉じてしまっているときには接続がうまく行きませんので、開く必要があります。

$ sudo ufw status
状態:非アクティブ
$ sudo ufw enable    # ファイアウォールが有効になっていなかったので、有効にする
$ sudo ufw allow 8181
$ sudo ufw reload
$ sudo ufw status
状態: アクティブ

To                         Action      From
--                         ------      ----
8181                       ALLOW       Anywhere                           
8181 (v6)                  ALLOW       Anywhere (v6)  

これでアクセスすれば「Welcome to nginx」のページが現れます。

Dockerを停止する

$ docker stop nginx-container

Docker/Kubernetes 実践コンテナ開発入門

Docker/Kubernetes 実践コンテナ開発入門

ABC022-B Bumble Beeを解く

今回のエントリーはAtCoder Beginners Contestの過去問を扱います。今回扱うのは第22回のコンテストのB問題です。B問題にしては入力が大きく、計算量を意識するよい練習となります。

問題文

高橋君はマルハナバチ(Bumblebee)という種類のミツバチです。

今日も花の蜜を求めて異なるN個の花を訪れました。


高橋君がi番目に訪れた花の種類はA_iです。i 番目の花は、i>j かつi番目の花の種類とj番目の花の種類が同じになるようなjが存在すれば受粉します。

高橋君が訪れたN個の花の種類の情報が与えられるので、そのうちいくつの花が受粉したか求めてください。


なお、高橋君以外による受粉や自家受粉を考える必要はありません。


入力
入力は以下の形式で標準入力から与えられる

N
A1
A2
:
AN
  • 1行目には高橋君が訪れた花の個数を表す整数N(1\leq N\leq10^5)が与えられる。
  • 2行目からのN行のうちi行目にはi番目に高橋君が訪れた花の種類を表す整数A_i(1\leq A_i\leq10^5)が与えられる。


出力
受粉した花の個数を1行で出力せよ。出力の末尾にも改行を入れること。

この問題のリンクは
atcoder.jp

です。

考察

花を表す整数が「1, 2, 3, 2, 1」の場合には、4番目に現れる「2」と5番目に現れる「1」で受粉が起こります。なので、出力は2となります。

戦略1

この実験からすぐ思いつくのは、次のような戦略です。

入力を一つずつ受け取って、その番号がすでに一度でも出ていれば受粉する。これを1つずつ数えていけばよい

入力の個数はNなので、入力を一つ一つ受け取るのにNに比例した時間がかかります。加えて、入力一つ一つに対して既に出てきているかを調べる必要がありますが、これは最大でNのオーダーだとすると、全部で N^2のオーダーとなります。

N \leq 10^5なのでこれでは 10^{10}の時間がかかってしまうので、これでは制限時間に引っ掛かります。つまり、この戦略で行くには、数字がすでに出ているかを調べる操作を\log N以下の時間で済ませる方法を考える必要があります。

戦略2

もう1つ考えられる戦略が、計算をいくつかのパートに分割することです。具体的には、

  • 入力値を全部配列に入れる
  • 配列をソートする
  • それぞれの数字が何回出てきているか数える

という計算に分割します。配列をソートする操作はいろいろあり、ソートのための関数が言語に備え付けられている場合がほとんどです(計算量は速いものはN\log N)。ここでのネックはソートした配列にそれぞれの数字が何回出てきているかを調べる部分です。

その前に、「それぞれの数字が何回出てきているか数える」ことでなぜうまくいくか簡単に説明します。例えば、配列に「1」が2回出てきたとします。その時、2つの「1」のうち、片方は最初にもう片方はそのあとに出てきたはずです。後に出てきた「1」では受粉が起こるので、各数字が出てきている回数から1を引いた数をすべての数字に対して足せば、求める答えが出てくるはずです。

数えるのは、配列を最初から1回だけ見ていけばよいので(ソートされているため)、この部分の計算量は O(N)で済みそうです。

解答例(C++14)

戦略1

この戦略では、すでに出てきた数字をどのような形で保存するかが重要になります。vectorに入れてしまうと、新しい入力を受け取って、それがすでに出てきたか探索するためにO(N)の時間がかかってしまうので、上で述べたように間に合いません。

しかしmapを使えば、探索は O(\log N)なので間に合います。

#include <bits/stdc++.h>

using namespace std;

int main() {
	int n; cin >> n;
	map<int, int> m;
	int res = 0;    // 求める答え

	for (int i = 0; i < n; i++) {
		int a; cin >> a;
		if (m.find(a) != m.end()) {    // すでに出てきていればresを1増やす
			res++;
		}
		else {
			m[a] = 1;    // まだ出てきていなければmapに加える
		}
	}

	cout << res << endl;
}

戦略2

配列をソートしたあと、ループを回して各数字が何回出てきているか数えてもよいのですが、C++にはuniqueという、配列の重複要素を除いた要素を先頭に集めてくれる便利な関数があるので、これを使います。

#include <bits/stdc++.h>

using namespace std;

int main()
{
  int n;
  cin >> n;
  vector<int> v;

  for (int i = 0; i < n; i++)
  {
    int a;
    cin >> a;
    v.push_back(a);
  }

  sort(v.begin(), v.end());
  auto it = unique(v.begin(), v.end()); 
  v.erase(it, v.end());    // itから先は一度出た数字が並んでいるので、消去する。

  cout << n - v.size() << endl;
}

解答例(Python 3)

おまけとして、上のそれぞれの解法をPythonで実装したものも載せておきます。

戦略1

n = int(input())
m = dict()

res = 0
for i in range(n):
  a = int(input())
  if (a in m.keys()):
    res += 1
  else:
    m[a] = 1

print(res)

戦略2

n = int(input())
v = []
for i in range(n):
  a = int(input())
  v.append(a)

print(n - len(set(v)))

Tensorflowで犬猫画像分類する

最近Tensorflowを勉強していて、試しに定番の(?)犬猫の画像分類をしてみました。僕がやったことをまとめると

  • CNN
  • tf.kerasは使わない
  • TFRecordにデータを保存してそこからデータを引っ張り出してくる
  • もちろんBatch

こんな感じのことを書きます。なのでこの記事の位置づけは、画像解析手法を書くというよりかはTensorflowの使い方みたいな感じです。

環境

使ったデータ

データの出処

今回使ったデータはKaggleからダウンロードしました。
www.kaggle.com

データのディレクトリ関係

今回の分析でのディレクトリ構造は下のようになっています

.
├── log
└── training_set
|    ├── cats
|    └── dogs
└── tf_cnn.ipynb

catsディレクトリとdogsディレクトリにはそれぞれ約4000枚のJPEG画像が保存されています。中身を見てみると、必ずしも犬・猫のみが写ったものだけではなく、人も一緒に写っていたり、イラストだったりします。

TFRecordにデータを保存

今回はデータを一度TFRecordにバイナリ形式で保存します。まずは各画像ファイルへのパスとラベルをリストの中に収納します。ラベルは猫が1、犬が0となっています。

import numpy as np
import tensorflow as tf


cat_dir = './training_set/cats/'
dog_dir = './training_set/dogs/'

image_paths = []
labels = []

for fname in os.listdir(cat_dir):
    if '.jpg' in fname:
        image_paths.append(cat_dir + fname)
        labels.append(1)
        
for fname in os.listdir(dog_dir):
    if '.jpg' in fname:
        image_paths.append(dog_dir + fname)
        labels.append(0)

# シャッフルする
shuffle_ind = np.random.permutation(len(labels))
image_paths = np.array(image_paths)[shuffle_ind]
labels = np.array(labels)[shuffle_ind]

リストの後ろから1000個のファイルをテストデータとして切り分けてそれぞれ別ファイルに保存します。

def _bytes_feature(value):
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

def _float_feature(value):
    return tf.train.Feature(float_list=tf.train.FloatList(value=[value]))

def _int64_feature(value):
    return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))


from PIL import Image

# トレーニングデータの保存
with tf.python_io.TFRecordWriter('training_data.tfrecords') as writer:
    for fname, label in zip(image_paths[:-1000], labels[:-1000]):
        image = Image.open(fname)
        image_np = np.array(image)
        image_shape = image_np.shape
        image = open(fname, 'rb').read()

        feature = {
            'height' : _int64_feature(image_shape[0]),
            'width' : _int64_feature(image_shape[1]),
            'channel' : _int64_feature(image_shape[2]),
            'image_raw' : _bytes_feature(image),    # 画像はバイトとして保存する
            'label' : _int64_feature(label)
        }
        tf_example = tf.train.Example(features=tf.train.Features(feature=feature))
        writer.write(tf_example.SerializeToString())

# テストデータの保存
with tf.python_io.TFRecordWriter('test_data.tfrecords') as writer:
    for fname, label in zip(image_paths[-1000:], labels[-1000:]):
        image = Image.open(fname)
        image_np = np.array(image)
        image_shape = image_np.shape
        image = open(fname, 'rb').read()

        feature = {
            'height' : _int64_feature(image_shape[0]),
            'width' : _int64_feature(image_shape[1]),
            'channel' : _int64_feature(image_shape[2]),
            'image_raw' : _bytes_feature(image),
            'label' : _int64_feature(label)
        }
        tf_example = tf.train.Example(features=tf.train.Features(feature=feature))
        writer.write(tf_example.SerializeToString())

これでデータの保存はできました。

CNNによるモデルの構築

次に、画像の分類に用いるモデルをCNNで構築します。

tf.reset_default_graph()

X = tf.placeholder(tf.float32, shape=[None, 150, 150, 3])
y = tf.placeholder(tf.int32, shape=[None])

with tf.name_scope('layer1'):
    conv1 = tf.layers.conv2d(X, filters=32, kernel_size=4, strides=1, activation=tf.nn.relu, name='conv1')
    pool1 = tf.nn.max_pool(conv1, ksize=[1,2,2,1], strides=[1,2,2,1], padding='VALID', name='pool1')
    
with tf.name_scope('layer2'):
    conv2 = tf.layers.conv2d(pool1, filters=64, kernel_size=3, strides=1, activation=tf.nn.relu, name='conv2')
    pool2 = tf.nn.max_pool(conv2, ksize=[1,2,2,1], strides=[1,2,2,1], padding='VALID', name='pool2')
    
with tf.name_scope('layer3'):
    conv3 = tf.layers.conv2d(pool2, filters=128, kernel_size=3, strides=1, activation=tf.nn.relu, name='conv3')
    pool3 = tf.nn.max_pool(conv3, ksize=[1,2,2,1], strides=[1,2,2,1], padding='VALID', name='pool3')
    
with tf.name_scope('dense'):
    flatten = tf.reshape(pool3, shape=[-1, 32768], name='flatten')
    dense1 = tf.layers.dense(flatten, 512, activation=tf.nn.relu, name='dense1')
    dense2 = tf.layers.dense(dense1, 2, activation=None, name='dense2')
    output = tf.nn.softmax(dense2, name='output')
    
with tf.name_scope('train'):
    xentropy = tf.losses.sparse_softmax_cross_entropy(logits=dense2, labels=y)
    loss = tf.reduce_mean(xentropy)
    optimizer = tf.train.AdamOptimizer()
    training_op = optimizer.minimize(loss)

with tf.name_scope('eval'):
    correct = tf.nn.in_top_k(dense2, y, 1)
    acc = tf.reduce_mean(tf.cast(correct, tf.float32))
    
with tf.name_scope('save'):
    train_acc = tf.summary.scalar('train_acc', acc)
    valid_acc = tf.summary.scalar('valid_acc', acc)
    file_writer = tf.summary.FileWriter('./log/190401/', tf.get_default_graph())
    saver = tf.train.Saver()

入力画像の形状は(150, 150, 3)で、カラー画像です。3つの畳み込み層と3つのプーリング層を重ね、最後に全結合層で長さ2のベクトルを出力しています。出力ベクトルのそれぞれの要素は、画像が犬である確率と猫である確率をそれぞれ表しています。

訓練はAdamを用いて行います。そして訓練の途中結果とパラメータを保存するためにFileWriterとSaverを用意します。

訓練する

では実際に訓練をします。まず、TFRecordからデータを取り出すための準備をしておきます。

image_feature_description = {
    'height' : tf.FixedLenFeature([], tf.int64),
    'width' : tf.FixedLenFeature([], tf.int64),
    'channel' : tf.FixedLenFeature([], tf.int64),
    'image_raw' : tf.FixedLenFeature([], tf.string),
    'label' : tf.FixedLenFeature([], tf.int64),
}

def _parse_fun(example_proto):
    feature = tf.parse_single_example(example_proto, image_feature_description)
    feature['image_raw'] = tf.image.decode_jpeg(feature['image_raw'])
    feature['image_raw'] = tf.cast(feature['image_raw'], tf.float32) / 255.0    #floatにキャストしてから255で割って正規化
    feature['image_raw'] = tf.image.resize_images(feature['image_raw'], (150, 150))    #150x150にリサイズ
    
    feature['label'] = tf.cast(feature['label'], tf.int32)
    
    return feature

では実際に訓練します。

epochs = 31
batch_size = 500

with tf.Session() as sess:
    raw_image_dataset = tf.data.TFRecordDataset('training_data.tfrecords')
    test_dataset = tf.data.TFRecordDataset('test_data.tfrecords')
    
    parsed_image_dataset = raw_image_dataset.map(_parse_fun)
    test_dataset = test_dataset.map(_parse_fun).batch(100)
    batched_dataset = parsed_image_dataset.batch(batch_size)
    
    init = tf.global_variables_initializer()
    init.run()

    for epoch in range(epochs):
        iterator = batched_dataset.make_one_shot_iterator()
        test_iter = test_dataset.make_one_shot_iterator()
        while True:
            try:
                batched = iterator.get_next()
                batched_eval = sess.run(batched)
                X_batch = batched_eval['image_raw']
                y_batch = batched_eval['label']
                sess.run(training_op, feed_dict={X: X_batch, y: y_batch})
            except tf.errors.OutOfRangeError:
                break
        
        if epoch % 5 == 0:
            print(f"finished epoch #{epoch}")
            test_data = test_iter.get_next()
            test_data_eval = sess.run(test_data)
            X_test = test_data_eval['image_raw']
            y_test = test_data_eval['label']
            train_acc_str = train_acc.eval(feed_dict={X: X_batch, y: y_batch})
            valid_acc_str = valid_acc.eval(feed_dict={X: X_test, y: y_test})
            file_writer.add_summary(train_acc_str, epoch)
            file_writer.add_summary(valid_acc_str, epoch)
            save_path = saver.save(sess, './log/190401/model_ckpt_{}.ckpt'.format(epoch))

file_writer.close()

訓練ではバッチサイズを500として30エポック訓練します。訓練では、イテレータを作っておいてバッチサイズずつデータを取り出して訓練します。イテレータで次のデータを取り出せなくなったらtf.error.OutOfRangeErrorを送出するので、それを受け取ってwhileループを抜けます。エポック数が5の倍数のときに訓練データとテストデータでの精度とモデルの重みパラメータを保存します。

訓練結果をTensorboardで見てみる

訓練したら、その結果をTensorboardで見てみます。シェルから

tensorboard --logdir=./log/190401/

と入力すると、アドレスが出てくるので、そこにアクセスすると、下のような画面が出てきます。

f:id:pyhaya:20190410212610p:plain

左側が訓練データの精度で右側がテストデータの精度です。これを見ると訓練データでは精度がほとんど1になっているのに対してテストデータでは精度が0.68にしかなっておらず、過学習していることがわかります。これを解消するには、モデルのパラメータを減らす方法やドロップアウトなどの正則化をかける方法、そして画像の水増しなどの方法があります。これらの方法についてはまた別の場所で書きます。

scikit-learnとTensorFlowによる実践機械学習

scikit-learnとTensorFlowによる実践機械学習

Google Cloud Platform (GCP)を気軽に勉強する

久しぶりのエントリーです。今日はGoogle Cloud Platform (GCP)について書いてみたいと思います。 GCPというのは、Googleが提供するクラウド上のインフラサービスで、簡単なウェブサービスから、ビッグデータ解析、機械学習など幅広い用途に使えることから今大きな注目を集めています。

提供しているサービス

f:id:pyhaya:20190226135158j:plain

GCPは実に様々なサービスを提供しています。下に例を挙げます。

  • Compute Engine
  • BigQuery : データウェアハウス
  • Cloud Datalab : Jupyter Notebookのような対話型環境
  • Cloud Dataflow : データを分散コンピューティングにより高速に処理
  • Cloud Strage : ストレージサービス

...

全部書いたらきりがないほどあります。ほかに何があるか見たい方は下のリンクをどうぞ
cloud.google.com

何が良いのか

このサービスを用いることで、例えば企業は自前でサーバーを構築・メンテナンスをする必要がなくなり、設計やコードを書くことに集中できるようになります。また、Googleの計算資源を用いることができるので、GPUやTPUといった、特に機械学習ではうれしいサービスを気軽に利用できるようになります。

料金の壁

しかし、このサービスはもちろん無料ではありません。使った時間と資源の量に比例して料金が発生します。GCPには料金の見積もりサービスもついているので、気になる方は計算してみるとよいと思います。

cloud.google.com


試しに、個人の趣味で利用することを想定して、Compute Engineを

  • 1 instance
  • CPU: 60個
  • RAM: 300GB
  • GPU: 8個
  • SSD: 6x375GB
  • 4 hours/month (週1で1時間使用を想定)

で見積もってみると料金は$17.53 / monthでした。これが高いと感じるか安いと感じるかは、人によると思いますが、私の場合は、GCP未経験なのでまずは無料で試してみたいと考えました。

GCPには無料トライアルがありますが、やはり最初は基本的なことを教わりながらやりたいと考えていたらCourseraでよいコースがありました。

CourseraのGCPコース

www.coursera.org

このコースは、Google Cloudが提供しているコースで、全5コースからなり、GCPの基本を学ぶことができます。また、Courseraが提供しているので7日間は無料で受講できます(修了証が欲しければ有料になってしまいますが...)。以下にこのコースの特徴を紹介します。

GCPの全体像を理解できる

全5コースで、GCPを使ったデータ処理の基本を学ぶことができます。計算環境の構築方法から、BigQueryを用いたデータ操作、そしてTensorflowを用いた学習まで一通り学べます。

GCPを実際に使える

これが私にとっては最もありがたかったことですが、コースの課題を解く際にGCPを実際に使うことができます。コースで一時利用用のアカウントを支給してくれるので、実際に講義で学んだことをGCP上で試すことができます。

一つ一つのコースはそんなに重くない

全部で5コースもあると、無料で受講したいと考えている人からすると多すぎるように感じます。しかし、実際には1つ1つのコースはそんなに重くなくて、私の場合には無料期間中に4コース目まで修了できました。

日本語の字幕付き

1コース目はなかった気がするのですが、2コース目からは講義動画で日本語字幕を表示できます。英語が苦手な人でもちゃんと理解ができます。




企業でも最近ではGCPを利用するところも増えていると思うので、勉強しておいて損はないと思います。(最後にとりあえず注意しておくと、Courseraは8日目に入る前に手動でCancel Subscriptionしないと勝手に料金が発生してしまうので、無料にこだわる人はそこだけは注意してください。)

Djangoで家計簿のWebアプリケーションを作る8 Chartjsを使ってグラフを描画する

久しぶりのエントリーです。今回は前々から言っていた、JavaScriptを用いたグラフの描画を行います。これまではPythonのMatplotlibを使ってSVG画像を作り、それをページに表示してきました。これをChartjsを使って書き直すことで、グラフ上にカーソルを移動させたときに値を表示するなどの、画像ではできなかったことを実現します。

前回の記事は下にあります。
pyhaya.hatenablog.com

Chartjsとは

ChartjsとはJavaScriptを使ってHTML上にきれいなグラフを表示させることができるものです。f:id:pyhaya:20190119190946p:plain
Chartjsを使ったグラフは公式サイトに様々載っていますが、どれもとてもきれいです。
www.chartjs.org

これはどうしても使ってみたいと思い、試行錯誤してみました。

どうやってDjangoのプロジェクトでChartjsを使うか

ChartjsはプロットするデータをJSON形式で入力します。「Chartjs 使い方」などと検索するとプロットデータをソースコードにべた書きする例がいくつもヒットします。しかし今回の例だとプロットデータはデータベースからとってきたいのでここが悩みの種です。私はこれまでJavaScriptを触ったことがなかったので、ずいぶん苦労しました。

いろいろ試行錯誤した結果、最終的には下のような方法で実現できました。

調べていると、JSONファイルを出力してそれをJQueryで読み込む例などが出てきたのですが、JS初心者には難しすぎたので(笑)今回は上の方法でやります。

コードを書く

HTMLを編集する

まずはHTMLから整えていきます。前回までのHTMLファイルはさすがにひどかったので今回はBootstrapを組み込んでスタイルを一新しました。

<!DOCTYPE html>
{% load static %}
<html>
    <head>
        <meta charset="utf-8">
        <title>HousekeepingBook</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.1.4/Chart.min.js"></script>
        <link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css">
        <link rel="stylesheet" type="text/css" href="{% static 'money/style.css' %}">
        <script type="text/javascript" src="{% static 'money/data.js' %}"></script>
        <script type="text/javascript" src="{% static 'money/draw.js' %}"></script>
    </head>
    <body>
        <div class="container">
            <div class="row top-bg">
                <div class="col-md-8 mt-2 mb-3">
                    <h1>{{ year }}年 {{ month }}月</h1>
                </div>
                <div class="col-md-2 text-center mt-4">
                    <h4>
                        <a href="/money/{{ prev_year }}/{{ prev_month }}">
                            << {{ prev_month }}月
                        </a>
                    </h4>
                </div>
                <div class="col-md-2 text-center mt-4">
                    <h4>
                        <a href="/money/{{ next_year }}/{{ next_month }}">
                            {{ next_month }}月 >>
                        </a>
                    </h4>
                </div>
            </div>
            <div class="row">
                <div class="col-md-12 mt-4 mb-4">
                    <form action="/money/" method="post">
                        {% csrf_token %}
                        {{ form_add.as_table }}
                        <input type="submit" name="add" value="送信">
                    </form>
                </div>
            </div>
            <div class="row">
                <div class="col-md-6">
                    <h2>支出</h2>
                </div>
                <div class="col-md-6">
                    <h2>支出額の推移</h2>
                </div>
            </div>
            <div class="row">
                <div class="col-md-6">
                    <table>
                        <tr>
                            <th>日付</th>
                            <th>用途</th>
                            <th>カテゴリー</th>
                            <th>金額</th>
                            <th>削除</th>
                        </tr>
                        {% for m in money %}
                        <tr>
                            <td>{{ m.use_date }}</td>
                            <td>{{ m.detail }}</td>
                            <td>{{ m.category }}</td>
                            <td>{{ m.cost }}円</td>
                            <td>
                                <form action="/money/" method="post">
                                    {% csrf_token %}
                                    {{ form_delete.as_table }}
                                    <input type="hidden" name="year" value={{ year }}>
                                    <input type="hidden" name="use_date" value={{ m.use_date }}>
                                    <input type="hidden" name="detail" value={{ m.detail }}>
                                    <input type="hidden" name="cost" value={{ m.cost }}>
                                    <input type="submit" name="delete" value="削除">
                                </form>
                            </td>
                        </tr>
                        {% endfor %}
                    </table>
                </div>
                <div class="col-md-6">
                    <canvas id="data"></canvas>
                    <script type="text/javascript" src="{% static 'money/draw.js' %}"></script>
                </div>
            </div>
        </div>
    </body>
</html>

7行目でChartjsを読み込んでいます。そして次の8行目ではBootstrapを読み込んでいます。グラフ描画に関連しているのは84行目のcanvasタグの部分になります。描画に用いるのはdraw.jsという名前のJavaScriptファイルです。

ちなみにCSSファイルは下のようになっています。

table{
  border-collapse:collapse;
  margin:0 0;
}
th{
  color:#005ab3;
}
td{
  border-bottom:1px dashed #999;
}
th,tr:last-child td{
  border-bottom:2px solid #005ab3;
}
td,th{
  padding:10px;
}

h2 {
    padding: 0.25em 0.5em;
    color: #494949;
    background: transparent;
    border-left: solid 5px #7db4e6;
}

.container {
    max-width: 90%;
}

.top-bg{
    background-color: #b0c4de;
}

JavaScriptコードを書く

PythonでJavaSciptコードを書くのですがさすがに全部書くのは嫌なので、2つに分けます。

  • data.js
  • draw.js

data.jsにプロットするデータが含まれるようにして、他の部分はdraw.jsに書きます。

draw.js

var ctx = document.getElementById("data").getContext('2d');
ctx.canvas.width = 400;
ctx.canvas.height = 290;
var myChart = new Chart(ctx, json);    #変数jsonはdata.jsに書く

PythonJavaScriptコードを生成する

ここが核になります。JavaScriptコードを生成するとかいうとなんか難しく聞こえますが open(file_name, 'w')を使って書き込むだけです。以下がソースコードです。

views.py

import calendar
import datetime
from django.shortcuts import render, redirect
from django.utils import timezone
from django.views import View
import matplotlib.pyplot as plt
import pytz

from .models import Money
from .forms import SpendingForm, DeleteForm
from .utils import index_utils

plt.rcParams['font.family'] = 'IPAPGothic'

TODAY = str(timezone.now()).split('-')

class MainView(View):
    def get(self, request, year=TODAY[0], month=TODAY[1]):
        money = Money.objects.filter(use_date__year=year,
                use_date__month=month).order_by('use_date')

        total = index_utils.calc_month_pay(money)
        index_utils.format_date(money)
        form_add = SpendingForm()
        form_delete = DeleteForm()
        next_year, next_month = index_utils.get_next(year, month)
        prev_year, prev_month = index_utils.get_prev(year, month)

        context = {'year' : year,
                'month' : month,
                'prev_year' : prev_year,
                'prev_month' : prev_month,
                'next_year' : next_year,
                'next_month' : next_month,
                'money' : money,
                'total' : total,
                'form_add' : form_add,
                'form_delete' : form_delete,
                }

        self.draw_graph(year, month)

        return render(request, 'money/index.html', context)


    def post(self, request, year=TODAY[0], month=TODAY[1]):
        data = request.POST
        if 'add' in data.keys():
            self.register_payment(data)
        elif 'delete' in data.keys():
            self.delete_payment(data)

        return redirect(to='/money/{}/{}'.format(year, month))


    def register_payment(self, data):
        use_date = data['use_date']
        cost = data['cost']
        detail = data['detail']
        category = data['category']

        use_date = timezone.datetime.strptime(use_date, "%Y/%m/%d")
        tokyo_timezone = pytz.timezone('Asia/Tokyo')
        use_date = tokyo_timezone.localize(use_date)
        use_date += datetime.timedelta(hours=9)

        Money.objects.create(
                use_date = use_date,
                detail = detail,
                cost = int(cost),
                category = category,
                )

        return None


    def delete_payment(self, data):
        year = data['year']
        use_date = data['use_date']
        detail = data['detail']
        cost = data['cost']
        use_date = '/'.join([year, use_date])
        use_date = timezone.datetime.strptime(use_date, '%Y/%m/%d')
        use_date = use_date.date()

        Money.objects.filter(
                use_date__iexact=use_date,
                detail__iexact=detail,
                cost__iexact=cost
                ).delete()

        return None


    #描画に関連する部分
    def draw_graph(self, year, month):
        money = Money.objects.filter(use_date__year=year,
                use_date__month=month).order_by('use_date')

        last_day = calendar.monthrange(int(year), int(month))[1] + 1
        day = [i for i in range(1, last_day)]    #日付
        cost = [0 for i in range(len(day))]    #支出
        for m in money:
            #1日の支出を計算していく
            cost[int(str(m.use_date).split('-')[2])-1] += int(m.cost)

        #文字列に変換してカンマでつなげる
        text_day = ','.join(list(map(str, day)))
        text_cost = ','.join(list(map(str, cost)))

        #JSONテンプレート
        json_template = """var json = {
            type: 'bar',
            data: {
                labels: [
        """ + str(text_day) + """    #x軸
                ],
                datasets: [{
                    label: '支出',
                    data: [
        """ + str(text_cost) + """    #y軸
                    ],
                    borderWidth: 2,
                    strokeColor: 'rgba(0,0,255,1)',    #棒グラフの淵の線の色
                    backgroundColor: 'rgba(0,191,255,0.5)'    #棒グラフの塗りつぶしの色
                }]
            },
            options: {
                scales: {
                    xAxes: [{
                        ticks: {
                            beginAtZero:true
                        },
                        scaleLabel: {
                            display: true,    #x軸のラベルを表示
                            labelString: '日付',
                            fontsize: 18
                        }
                    }],
                    yAxes: [{
                        ticks: {
                            beginAtZero:true
                        },
                        scaleLabel: {
                            display: true,
                            labelString: '支出額 (円)',
                            fontsize: 18
                        }
                    }]
                },
                responsive: true
            }
        }
        """
        with open('money/static/money/data.js', 'w') as f:    #data.jsを開いて書き込む
            f.write(json_template)

とりあえずviews.pyの中身を全部載せていますが、描画に関係しているのは一番下のメソッドのみです。ここでやっていることは上のコメントを読めば大体わかると思いますが、JavaScriptコードをstringで用意しておいて、そこにデータを埋め込んでいく形になっています。ダブルクオーテーション3つでstringを作るとformat関数がうまく動いてくれないのでstringどうしを足しているため汚くなってしまっていますが、やっていることは単純です。

ちゃんと整ったコードは以下のサイトが非常に参考になると思います。
qiita.com

ここまでの結果

ここまでやってページを見てみると、次のようになります。

f:id:pyhaya:20190119194826p:plain
カーソルはグラフの一番高いところに持ってきていますが、ちゃんと金額が表示されていることがわかります。

まとめ

今回は、Chartjsを使ってグラフをより良いものにしました。その方法はJavaScriptコードをPythonから作るというものでした。これは私がJavaScriptわからな過ぎてとった苦肉の策なので、詳しい人から見たらベストプラクティスではないかもしれませんので注意してください(詳しい人いたら教えてください)。

「How Google Works」を読んだ

Kindleで何か面白そうな本ないかなと探していたら、「How Google Works」という本を見つけました。Googleといえば知らない人がいないほどのIT分野の巨人ですよね。Googleでは社員はどのように働いているのか、そこに興味があって購入してみました。

誰が書いているのか

著者をみて見ると何人かの共著であることが見て取れます。どの人もググるとわかりますが、Googleの成長に大きな役割を果たした人ばかりです。
エリック・シュミット - Wikipedia
Jonathan Rosenberg (technologist) - Wikipedia
ラリー・ペイジ - Wikipedia

訳者は土方奈美という方です。訳者に関しては詳しくないのですが、翻訳本でよくある「訳がクソ」という感じは全くありませんでした。

どんな本だったか

私は、この本にエンジニア視点からの働き方というのを期待していました。しかし、実際に読んでみると、どちらかというと経営者の視点からの本でした(副題に「私たちの働き方とマネジメント」とあるので当たり前といえば当たり前なのですが...)。なので、最初はミスったかなと感じながら読んでいたのですが、読んでいるうちに考えは変わっていきました。本の中では繰り返しIT分野の成長がほかの分野と比べていかに急速かというのを述べていますが、このように状況が目まぐるしく変化するような状況では、エンジニアも経営者のような全体を俯瞰するような視点が必要なのだと感じます。

本書では、いくつかのテーマについて、Googleがどのような歴史をたどってきたかも見ながら解説しています。そのテーマとは

です。全体を読んでいて感じたのは、Googleがいかにエンジニアを大事にしているかということです。どうすれば優秀なエンジニアを雇うことができるかどのような環境を作ればエンジニアの能力を最大限発揮させることができるか、そんな考えが一貫して根底にあるような感じがしました。

そして、この本ではGoogleの手掛けてきたプロジェクトがどのようにして進んできたのか、おなじみのGmailGoogle earthなどの例を紹介しています。いくつもの事例を読みながら感じたのは、Google内部での上下関係の希薄さとスピードを重視する姿勢です。ITは目まぐるしいスピードで日々進歩しているのでこのような姿勢が、プラットフォーマーであり続けるために必要不可欠であるということを感じさせられます。

こんな感じのことばかり書いてあると、読んでいて「Google入りてぇ」ってなって勝手に勉強のモチベーションが上がりますwww。同時に日本企業もこんな感じにならないかなとも思うのですが、日本はアメリカとはバックグラウンドが異なるので猿真似してもうまくいかないでしょう。日本企業はこれからどうするべきなのか、そんなことにも考えが行く本でした。