gorogoroyasu

福岡の開発会社で働いている。

IteratorをGeneratorと関数内static変数で実装する

Excel のカラムは、 A から始まって Z までいくと次は AA となり、 AZ、BA、BZ・・・と続いていきます。

以前行った案件でExcel出力を行う際に github.com

を使わせていただいていましたが、

        'col' => 'B',

を指定するのがかなり大変でした。

というのも、カラムが AX ぐらいまであり、今回の改修で、そのうちのいくつかの項目を削ることになったのです。

つまり、

        'col' => 'D',

という値を消すと、以降の

        'col' => 'E',
        'col' => 'F',
        'col' => 'G',

をすべて

        'col' => 'D',
        'col' => 'E',
        'col' => 'F',

に書き換えなくてはなりません。
そして、今回、機能追加を行うことになりました。
そんなことやってられるかー (怒)

ということで、2つ対策を考えてみました。

俗に言う、Enum ってやつなのかな?
イメージとしては、Golang のiota 的なやつ。

package main

import (
    "fmt"
)

const (
    a = iota
    b
    c
    d
)
func main() {
    fmt.Println(a, b, c, d)
        # => 0, 1, 2, 3
}

今回は、 Excel のカラムに沿った値を返すところが違います。

Generator を使う解決策

class GenColumn
{
    public function __construct()
    {
        $this->gen = $this->excelColumnGenerator();
    }

    public function getCurrent()
    {
        return $this->gen->current();
    }

    public function getNext()
    {
        $this->gen->next();
        return $this->gen->current();
    }

    private function excelColumnGenerator()
    {
        $ary = [];
        for ($a="A"; $a != "AZ"; $a++) {
            $ary[] = $a;
        }
        foreach($ary as $column) {
            yield $column;
        }
    }

}

$column = new GenColumn();
print($column->getCurrent());
print($column->getNext());
print($column->getCurrent());
print($column->getNext());
print($column->getCurrent());

getCurrent() というメソッドを追加して、現在の値を取得することができるようにしています。

これを 関数内static変数を使うバージョンで書き直すと、

関数内static変数を使う解決策

class GenColumn
{
    private static $ary = [];
    
    private static function genAry()
    {
        $ary = [];
        for ($a="A"; $a != "AZ"; $a++) {
            $ary[] = $a;
        }
        self::$ary = $ary;
    }

    public function getColumnName($current=false)
    {
        static $a=1;
        if (empty(self::$ary)) {
            self::genAry();
        }
        if ($current) {
            return self::$ary[$a - 1];
        }
        
        $a++;
        return self::$ary[$a - 1];
    }
}
$column = new GenColumn();
print($column->getColumnName(true));
print($column->getColumnName(false));
print($column->getColumnName(true));
print($column->getColumnName(false));
print($column->getColumnName(true));

という風に書けます。

両者に一長一短があります。

ちなみに、10000回ループした際の実行速度(5回の平均)は

Generator: 0.089s 関数内static変数: 0.023s

と、関数内static変数の方で実装したほうが4倍ほど早かったです。

メリットとデメリット

Generator を使う解決策は、コードが長くなりますが、 - $column->getCurrent() - $column->getNext() と、行う操作にわかりやすい名前をつけることができます。

一方、コードが長くなり、実行に時間がかかります。

関数内static変数 を使う解決策は、コードは短く実行時間も早いです。 ただし、現在の値とインクリメントした値を取得する際に、 $column->getColumnName($current) という風に、$current の true/false で取れる値が変わります。 可読性は決して高くないです。

まとめ

結局、2つの実装方法を検討しましたが、より安全性が高い Generator で実装を行いました。 ただ、 関数内static変数 の使い方はあまり良く知らなかったので、調べてみてよかったです。

tensorflowのrandom crop で、2枚の画像の同じ箇所を切り取る

画像が2枚あります。
画像A が入力データ 画像B が正解データ。

f:id:adiboy:20181026102449p:plain 画像引用: https://helpx.adobe.com/jp/photoshop-elements/kb/222341.html

この画像をrandom crop するときに、どのようにするとうまくcrop できるのか試しました。
そして、たどり着いた答えが以下のコード。
もっといい方法があるのかもですが、とりあえずこれで動きます。

import tensorflow as tf
import numpy as np
import random

# この二つの配列を、画像と見立てる
img1 = np.arange(300).reshape((10, 10, 3))
img2 = np.arange(300).reshape((10, 10, 3)) + 300

i1 = tf.constant(img1)
i2 = tf.constant(img2)

with tf.Session() as s:
    s.run(tf.global_variables_initializer())
    seed = random.randint(0, 1000)
    croped_img1 = tf.random_crop(i1, [5, 5, 3], seed=seed)
    croped_img2 = tf.random_crop(i2, [5, 5, 3], seed=seed)
    c1 = s.run(croped_img1)
    c2 = s.run(croped_img2)    
"""
c2 - c1 => array([[[300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300]],

       [[300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300]],

       [[300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300]],

       [[300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300]],

       [[300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300],
        [300, 300, 300]]])
"""

おそらくこれで切れているんだと思います。 ということで、random_crop に seed を入れてあげれば解決。 以上。

追記。

俺のやりたかったことは、そんなんじゃない!!!! こんな記事、嘘っぱちだ。

dataset API の中で、どうやってそれ使えばいいんだ(怒)

ということで、調べた。

import tensorflow as tf
import numpy as np
import random

# 画像 (10枚)
img1 = np.arange(3000).reshape((-1, 10, 10, 3))
img2 = np.arange(3000).reshape((-1, 10, 10, 3)) + 300

def _parse_function(img1, img2):
    r = tf.get_seed(1)
    img1 = tf.random_crop(img1, [5, 5, 3], seed=r[0])
    img2 = tf.random_crop(img2, [5, 5, 3], seed=r[0])
    return img1, img2

dataset = tf.data.Dataset.from_tensor_slices((i1, i2))
dataset = dataset.map(_parse_function, num_parallel_calls=2)
dataset = dataset.shuffle(buffer_size=(len(img1))).batch(1).prefetch(1).repeat()
iterator = dataset.make_one_shot_iterator().get_next()

with tf.Session() as sess:
    for i in range(5):
        x, y = sess.run(iterator)
        print(x[0, 0, 0])
print(y - x)

結果

[2823 2824 2825]
[675 676 677]
[1932 1933 1934]
[2232 2233 2234]
[2436 2437 2438]
[[[[300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]]

  [[300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]]

  [[300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]]

  [[300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]]

  [[300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]
   [300 300 300]]]]

ということで、ランダムに画像を crop しつつ、対象の2枚の画像については同じ箇所で切り出すことができた。

メモ golang の defer 2

package main

import (
    "fmt"
)

type Hoge struct {
    a string
}


func main() {
    h := &Hoge{"a"}
    defer fmt.Println("no func", h.a)
    defer func () {fmt.Println("with func", h.a)}()
    h.a = "b"
}

出力は、

with func b
no func a

以前の記事に書いたが、defer は、後入れ先出しで処理される。
よって、 with func が先に実行され、
no func が後で実行される。

1つ目の defer、with func については、h.a = "b" の結果が表示され、 2つ目のdefer、 no func については、 h.a = "a" の結果が表示されている。

これは、 defer 文が、式の実行を遅らせるだけで、式の評価自体は defer 文が呼ばれた段階で行われていることを示唆している。
たぶん。

メモ golang のdefer

defer は便利でよく使うので、特性をちゃんと把握しておく必要がある。

  1. defer の実行順
package main

import (
    "fmt"
)

func main() {
    for i:=0; i<3; i++ {
    fmt.Println("1")
    defer fmt.Println(fmt.Sprintf("2-%d", i))
    fmt.Println("3")
    }
}

結果

1
3
1
3
1
3
2-2
2-1
2-0

ということで、後入先出になってることがわかる。

  1. 他の関数内のdefer はいつ呼ばれるのか?
package main

import (
    "fmt"
)

func de(s string) {
    defer fmt.Println(s)
}

func main() {
    for i:=0; i<3; i++ {
    fmt.Println("1")
    de("2")
    }
}

結果

1
2
1
2
1
2

となった。 関数のスコープを抜ける時に呼ばれるらしい。 なるほど。

この辺は、ドキュメントにも書いてあると思うが、 簡単な実験で挙動を確認できるので、サンプルを書いてみると良いと思う。
続けていこう。

"Key (id)=(1) already exists" error occurred in CakePHP3 model test.

久々にCakePHP でテストを書いている。
Fixture を整えて、TestCase を書いた。
そして、値を save するメソッドをテストした。

すると、

duplicate key value violates unique constraint "hoges_pkey"
DETAIL:  Key (id)=(1) already exists.

というエラーが出た。
悲しい。

書いてたFixture は、こんな感じ。

<?php
namespace App\Test\Fixture;

use Cake\TestSuite\Fixture\TestFixture;


class HogesFixture extends TestFixture
{

    /**
     * Fields
     *
     * @var array
     */
    // @codingStandardsIgnoreStart
    public $fields = [
        'id' => ['type' => 'integer', 'length' => 10, 'autoIncrement' => true, 'default' => null, 'null' => false, 'comment' => null, 'precision' => null, 'unsigned' => null],
        'user_id' => ['type' => 'integer', 'length' => 10, 'default' => null, 'null' => false, 'comment' => null, 'precision' => null, 'unsigned' => null, 'autoIncrement' => null],
        'fuga' => ['type' => 'text', 'length' => null, 'default' => null, 'null' => false, 'collate' => null, 'comment' => null, 'precision' => null],
        'modified' => ['type' => 'timestamp', 'length' => null, 'default' => null, 'null' => true, 'comment' => null, 'precision' => null],
        'deleted' => ['type' => 'timestamp', 'length' => null, 'default' => null, 'null' => true, 'comment' => null, 'precision' => null],
        '_constraints' => [
            'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
        ],
    ];
    // @codingStandardsIgnoreEnd

    /**
     * Init method
     *
     * @return void
     */
    public function init()
    {
        $this->records = [
            [
                'id' => 1,
                'user_id' => 1,
                'fuga' => 'a',
                'modified' => 1539138087,
                'deleted' => 1539138087
            ],
            [
                'id' => 2,
                'user_id' => 1,
                'fuga' => 'b',
                'modified' => 1539138087,
                'deleted' => 1539138087
            ],
            [
                'id' => 3,
                'user_id' => 1,
                'fuga' => 'c',
                'modified' => 1539138087,
                'deleted' => 1539138087
            ],
        ];
        parent::init();
    }
}

そして、原因は Fixture にあった。

解決策は、

    public function init()
    {
        $this->records = [
            [
//                'id' => 1,
                'user_id' => 1,
                'fuga' => 'a',
                'modified' => 1539138087,
                'deleted' => 1539138087
            ],
            [
//                'id' => 2,
                'user_id' => 1,
                'fuga' => 'b',
                'modified' => 1539138087,
                'deleted' => 1539138087
            ],
            [
//                'id' => 3,
                'user_id' => 1,
                'fuga' => 'c',
                'modified' => 1539138087,
                'deleted' => 1539138087
            ],
        ];
        parent::init();
    }

$this->records に入れた連想配列内の、 id を消すこと。

普通に考えたらそうだ。
ID は、 データベースのsequenceの機能を使うので、idの発行をCakePHPに任せたら、 1からID が付与され、自動的にインクリメントされる。

一方、ID に値を入れてしまうと、Database の Sequence の機能を使わずに値が保存される。
よって、Database は、次の値は1だと思っている。しかし、1はすでに存在している。
だから、エラーになる。

細かいところだが、ハマりどころだ。

やっかいなことに、 Fixture を bake すると、デフォルトで値が挿入されるので、
Bake のテンプレートを書き換えるか (やったことない) いちいちコメントアウト (もしくは削除) する必要がある。

参考にしたのはこちら stackoverflow.com

iPhone の充電ができないときに調べるべきただ一つのこと

ここ2ヶ月ぐらいずっと、iPhone の調子がおかしかった。
これは、ライトニングケーブルが断線してるな。
勘のいいボクはすぐに気づいた。

最近買った、Magic Trackpad 2 付属のライトニングケーブルを使ってみた。
そのときはうまく充電できた。
しかし、その能力も長くは続かなかった。

それからは、iPhone を省電力モードにして、だましだまし使ってきた。
電池の色が黄色いのがデフォルトだった。
いつも、何度かケーブルを抜き差しして、ちょうどよい角度を探していた。

そして、先週の週末、夜、充電できなくなった。

充電は残り3%。
絶体絶命だ。
このまま電池が切れ、二度と充電できない状況になったら、
二段階認証 が一切できなくなる。
念の為、Google の 二段階認証 は解除した。
Authy に移行していなかった自分に腹が立つ。
また、最近 Google Photos にアップロードしていない写真たちも消滅する。
LINE も移行しなければならない。
ボクは iPhone に頼りすぎていたことに気づいた。
そして、バックアップの重要性を思い出した。

時刻は21:05。
Apple Store はすでにしまっている。
ボクは絶望に暮れた。
念の為、ビッグカメラの営業時間を調べた。
こちらも21:00 まで。
終わった。 そう思った。
しかし、次の瞬間、ボクの脳裏に、ひらめきが走った。

ヨドバシカメラApple 製品おいてるゾーンあるよな。
時間を確認する。すると、22:00 まで開いている。
自宅からヨドバシカメラまで、チャリで15分。
余裕で間に合う。

勢いで直行した。修理できない可能性はあるが仕方ない。
最悪iPhone8 を買って帰ろう。 と心の中で決めていた。

ヨドバシカメラでの体験は、最悪だった。
修理ができないのは仕方ない。
しかし、あんなに広いスペースを使って製品を展示いるにもかかわらず、販売すらしていないのだ。

値段を聞いたら、「展示してあるiPhoneApple.co.jp にアクセスすれば見れますよ」 と言われた。
そんな事も知らないんですか? 的なテンションで。

知っとるがな!w

泣く泣くヨドバシカメラを立ち去り、家路についた。
クロアチアが勝つかフランスが勝つか よりもiPhone の充電のほうが気になった。

なんとか充電してください!!!

藁にもすがる思いで何度もコネクタを抜き差しした。
しかし、何もおこらない。

ポケモンを捕まえるときに、 A B ボタンを連打したら捕まえやすくなる みたいな噂があったが、あれと同じぐらい無駄なことをしていると感じた。
ちなみに、ポケモンに関して言うと、ホウオウをモンスターボールでゲットしたことがあることを思い出した。

話を戻す。

数十分の格闘の末、iPhone が震えた。
その角度をキープしつつ、フル充電した。

次の日、起きてすぐ Apple Store に向かった。
予約せずに行ったので、予約してないやつらが並ばされる長い列の最後尾に追加された。
暇そうにしている店員に腹が立つ。

しかし、もうすぐiPhone を修理できる。
そう信じて並んだ。
並び続けた。

30分ぐらいして自分の番がきた。

店員に言った。 「充電ができなくなりました。交換だと思います。」
3秒ぐらいiPhone を見回した店員は、言った。 「あー、Dock に誇り溜まってますねー」

そして、ホコリを取り除いてくれた。 結局、原因はDock のホコリだった。

ボクのiPhone は、生き返ったかのように充電できるようになった。

TensorFlow の ObjectDetection API を Fine Tuning する

TensorFlow には、Object Detection を行うためのコードが用意されています。
今回は、TensorFlow 1.8.0 でObject Detection を行ってみました。

github.com

実行した環境は以下の通り。

Ubuntu 16.04 に Mac Book Pro から ssh で接続
nvidia-docker: 18.03.1-ce
CUDA: 9
cudnn: 7
tensorflow-gpu: 1.8.0

参考にしたもの

自分は、以下の動画を見ながらどのように行えばよいかを学びました。
非常に丁寧に説明されているので、おすすめです。
ただ、問題は解説がWindows 向けにされていること。
なので、今回は Ubuntu でどのようにやればよいかを書いていきたいと思います。

www.youtube.com

準備

実行するソースコードの準備

上記の環境の任意のディレクトリに、 github.com をclone します。

cd /path/to/dir
git clone https://github.com/tensorflow/models

また、先程の動画の作者の方が作られたレポジトリがあるので、そのコードをclone します。

git clone https://github.com/EdjeElectronics/TensorFlow-Object-Detection-API-Tutorial-Train-Multiple-Objects-Windows-10.git

その後、TensorFlow-Object-Detection-API-Tutorial-Train-Multiple-Objects-Windows-10 ディレクトリの中身を
models/research/object_detection にコピーします。

rm -r models/research/object_detection/doc
rm -r models/research/object_detection/images
rm -r models/research/object_detection/inference_graph
rm -r models/research/object_detection/training
cp TensorFlow-Object-Detection-API-Tutorial-Train-Multiple-Objects-Windows-10/* models/research/object_detection/

環境の整備

protobuf の準備をします。

apt-get install protobuf-compiler

を行い。 protobuf をインストールした後、

cd path/to/dir
git clone https://github.com/cocodataset/cocoapi.git
cd cocoapi/PythonAPI
make
cp -r pycocotools ../../models/research/
cd models/research
protoc object_detection/protos/*.proto --python_out=.

というコマンドを実行します。 これらのコマンドは、
models/installation.md at master · tensorflow/models · GitHub に載っています。

ラベルの準備

次に、ラベルの準備を行います。 自分は、MacApp Store から¥240 で購入できます。
とても使い勝手がいいのでおすすめです!

RectLabel for object detection

RectLabel for object detection

  • Ryo Kawamura
  • 開発ツール
  • ¥240

RectLabel を使ってラベルをつけていきます。

他にも labelbox.com というツールが有るようで、動画ではこちらを使っています。

こんな感じでラベルをつけていきます。

f:id:adiboy:20180713152914p:plain

ラベルの作成が終わったら、 Ubuntu 上、 /path/to/dir/models/research/object_detection/images に画像をコピーします。
その後、 cd /path/to/dir/models/research/object_detection/images && mv annotations/* ./ && rm -r annotations && mkdir test train を実行します。

動画内で、学習データは200枚以上必要と言っていたので、
頑張ってラベリングしてくださいw

また、画像のサイズは、720x1280 以下にすることが推奨されるようです。

これらが終わったら、適当に jpg と xml のペアを
train, もしくはtest に振り分けていきます。

/path/to/dir/models/research/object_detection/images にて、

import glob, shutil
import numpy as np

xmls = glob.glob('*.xml')
assert len(xmls) > 200

train = np.random.choice(xmls, 200, replace=False)

for xml in xmls:
    name = xml[-4:]
    if xml in train:
        shutil.move(xml, 'train/{}'.format(xml))
        shutil.move('{}.JPG'.format(name), 'train/{}.JPG'.format(name))
    else:
        shutil.move(xml, 'test/{}'.format(xml))
        shutil.move('{}.JPG'.format(name), 'test/{}.JPG'.format(name))

みたいなコードを実行して、適当にディレクトリを振り分けていきます。
上記のコードは、実際に動かしたわけではないので、動作保証はしません。

これで、ラベルの準備は完了です。

fine tuning する対象の重み

今回は、Faster R-CNN をファインチューニングしていこうと思います。
事前に学習された重みは、下記サイトに掲載されているので、

github.com

cd /path/to/dir/models/research/object_detection/
wget http://download.tensorflow.org/models/object_detection/faster_rcnn_inception_v2_coco_2018_01_28.tar.gz
tar -zxvf faster_rcnn_inception_v2_coco_2018_01_28.tar.gz

というふうに、ダウンロードして解凍します。

その後、

cp samples/configs/faster_rcnn_inception_v2_pets.config training
cp data/pet_label_map.pbtxt training/label_map.pbtxt

とし、これらの中身を編集していきます。

training/label_map.pbtxt の中には、アイテムのID と名前を書きます。

item {
  id: 1
  name: 'madatsubomi'
}

item {
  id: 2
  name: 'otachi'
}

item {
  id: 3
  name: 'pijotto'
}

faster_rcnn_inception_v2_pets.config は少し修正箇所が多いのですが、

# 10行目
num_classes を自分が学習させたいラベルの数に書き換える
# 107行目
fine_tune_checkpoint を /path/to/dir/models/research/object_detection/faster_rcnn_inception_v2_coco_2018_01_28/model.ckpt に書き換える
# 122 行目
input_path を、 /path/to/dir/models/research/object_detection/train.record に書き換える (未作成)
#124 行目
label_map_path を /path/to/dir/models/research/object_detection/training/label_map.pbtxt に書き換える
#128行目
num_examples を テストデータの数に書き換える
#236行目
input_path を、/path/to/dir/models/research/object_detection/test.record に書き換える (未作成)
#238行目
label_map_path を /path/to/dir/models/research/object_detection/training/label_map.pbtxt に書き換える

これらの操作により、設定は完了です。

TensorFlow が読み出すデータの作成

generate_tfrecord.py というファイルを編集します。
編集するのは、class_text_to_int という関数の中身です。
この関数の中身を、自分のデータセットと training/label_map.pbtxt に書いた内容を見比べながら編集していきます。

def class_text_to_int(row_label):
    if row_label == 'madatsubomi':
        return 1
    elif row_label == 'otachi':
        return 2
    elif row_label == 'pijotto':
        return 3
    else:
        None

その後、train.record, test.record というファイルを作成していきます。

cd /path/to/dir/models/research/object_detection/
python xml_to_csv.py
python generate_tfrecord.py --csv_input=images/train_labels.csv --image_dir=images/train --output_path=train.record
python generate_tfrecord.py --csv_input=images/test_labels.csv --image_dir=images/test --output_path=test.record

これで、TensorFlow が学習時に読み出すデータが作成できました。

学習の実行

学習を実行します。

cd /path/to/dir/models/research/object_detection/ 
nohup python -u  train.py --logtostderr --train_dir=training \
--pipeline_config_path=training/faster_rcnn_inception_v2_coco.config \
> success.log 2> error.log &

これで、学習が開始されます。
学習の経過を見たい場合は、 tailf error.log を見てみてください。
(なぜ success.log ではなく error.log の方に吐かれるのかは不明)

loss の平均値が 0.05 を下回るようになったら学習完了らしいです。 (動画で言っていた。)

出力

では、出力を確認してみましょう。
出力の確認には、 export_inference_graph.py というファイルを利用します。

python export_inference_graph.py --input_type image_tensor --pipeline_config_path training/faster_rcnn_inception_v2_coco.config \
--trained_checkpoint_prefix training/model.ckpt-X --output_directory inference_graph #ただし X は、 training/model.ckpt-X となっているX の値

その後、 Object_detection_image.py の中の IMAGE_NAME を変更し、

python Object_detection_image.py

を実行します。

そうすると、NN が元画像に box を上書きした画像を出力してくれます。

なにか問題点があったら教えてください。

以上。