日陰のテクノロジ

卒業研究の備忘録と世間一般にタブーとされている技術について

制服JKの画像から学校名を推定できるCNNを作った

はじめに

女子高生を見て、どこの学校の制服なのか気になったことがないだろうか。

もちろんヤバい意味ではなく、見覚えはあるがどこの学校だったかど忘れした、というような知的好奇心から純粋に気になるシチュエーションのことだ。

最近ふと、CNN (Convolutional Neural Network)を使えば、制服の画像から学校名を推定できるのではないかと思ったので、機械学習への理解を深めるために制服JKの画像から学校名を推定できる装置を作ってみた。あくまで機械学習への理解を深めるためにだ。

エビデンス

画像分類で定番のCNNをKerasを用いて構築して学習を行った。

学習用データ集め

主にTwitterから女子高生の画像を収集した。何度か自分が何をやっているのかわからなくなったが、なんとか続行して3校の女子高生の画像を数枚集めた。 f:id:yoidea:20171119183040p:plain:h200

前処理

プリクラや集合写真が混在するデータをそのまま学習するのには無理があると判断したため、前処理を人力で行うことにした。

女子高生の画像から最も特徴が現れると思われる部分を切り出す。以下のイラスト(実際の画像を使うと問題があるため)を参考にして欲しい。

f:id:yoidea:20171119171552p:plain:h200

基本的には正面からの立ち姿が好ましい。首下からスカートの終端までを切り取る。

f:id:yoidea:20171119171558p:plain:h200

このような作業をひたすら行った。単純作業ではあったが不思議と疲れなかった。

最終的に3校各20枚、合計60のデータセットが完成した。

f:id:yoidea:20171119184449p:plain:h200

学習

Kerasを用いて、4層の畳み込み層と全結合層を持つ定番のネットワークを構築した。学習データが少ないので、ドロップアウトを極端に大きくしてみた。

詳しくはGithubを参照して頂きたい。

github.com

結果

テスト画像を用意

学習データとは別に女子高生の画像を用意した。TwitterGoogle画像検索を駆使して再度画像を集めた。

f:id:yoidea:20171119190823p:plain:h200

前処理

前処理を行う必要がある。将来的には自動化したいが試作品なので手動で行う。

3校各4枚、合計12枚のテストデータが完成した。

f:id:yoidea:20171119180021p:plain:h200

分類

プログラムを起動して、前処理を行った画像のパスを入力すると、学校名が推定された。

思いの外良い精度で驚いた。

>> test/uniformA1.jpg
○○○高校
 99% #############################

○○北高校
  0% 

○○南高校
  0% 
>> test/uniformB1.jpg
○○○高校
 39% ###########

○○北高校
 59% #################

○○南高校
  1%
>> test/uniformC1.jpg
○○○高校
  0% 

○○北高校
  0% 

○○南高校
100% ##############################

使用方法

手元でも動かしてみたいという方は是非プログラムをダウンロードしてみて欲しい。(くれぐれも悪用しないように

依存関係

  • Python 3.0 以上
  • Keras 2.0 以上 (Tensorflow backend)
  • Pillow
  • numpy
  • tqdm
  • h5py

ダウンロード

gitでリポジトリをクローンする。もしくはDownload ZIPからダウンロードしても問題ない。

git clone https://github.com/yoidea/JK_uniform_classifier.git
cd JK_uniform_classifier/

データ集め

まず、制服の女子高生の画像を収集する。(くれぐれも違法行為を行わないように

データセット用のディレクトリを作成して、その中に1校につき1ディレクトリを作成して、上記のように前処理をした画像を格納する。

ls images/
南高校     北高校     西高校     東高校
ls images/北高校/
01.jpg     02.jpg     ...     99.jpg

学習

引数を与えてtrain.pyを実行して学習を開始する。

例えば、データセット用のディレクトリがimages/で、バッチサイズが10枚、エポック数が3000回の場合は以下のように指定する。

python train.py --input images/ --batch 10 --epoch 3000

推定

学習が完了すると、train.pyと同じディレクトリにmodel.jsonnames.jsonweights.hdf5が生成される。それを確認してclassify.pyを実行する。

python classify.py
Using TensorFlow backend.
Enter the file name (*.jpg)
>> 

そうすると、プロンプトが出現するので、判定したい画像のパスを入力する。

>> test/uniform.jpg
南高校
 10% ###

北高校
 60% ##################

西高校
 20% ######

東高校
 10% ###

その制服が各学校のものである確率が出力される。

今後の課題

判定できる学校数を増やす

3校だからたった20枚の画像で良い精度がでているのではないか、と思われるかもしれない。全くその通りだと思う。

制服は特性上、似ているものが存在し、学校数が増えれば増えるほど正確な判定が困難になる。

そこで、画像と位置情報を組み合わせることで解決したいと考えている。具体的には、撮影した画像の位置情報から半径20km県内の学校のみを対象にする、もしくは画像と一緒に位置情報もネットワークに入力する、などを考えている。

ご協力のお願い

このプログラムは機械学習を噛じったばかりの素人が作成している。そのため、野蛮なコードかもしれない。

問題点や違和感等、何かあれば是非IssueやPullRequestを投げて欲しい。(本音はGithubに友達がいないので会話してみたいだけ)

シェルにログインボーナス機能を追加してみた

はじめに

プログラミングの勉強のために毎日シェルにログインしたいモチベーションが続かない

そんな悩みをお持ちではないだろうか?

毎日ログインするぞと決断しては3日で続かなくなる。というのを僕は繰り返していた。

そこで、如何にしてログインをシェルにログインするモチベーションを向上させるかを考えたところ、ソーシャルゲームのようなログインボーナスがあれば毎日ログインする気になるのではないかという結論に至った。

そこで、シェルにログインボーナスを追加するだけの簡易なプログラムをPythonで作ってみた。

github.com

デモ

機能は至ってシンプルで、シェルにログインすると、通算・連続・当日のログイン回数を表示する。日本人は数字が好きなのでこれだけでもモチベーションが向上するだろう。 f:id:yoidea:20170716234754p:plain

また、特定回数ログインするとお祝いのメッセージが表示される。正直なところボーナスといっても何を与えたら良いのかわからず、祝辞を表示するぐらいしか思い浮かばなかった。とは言え、長期間ログインすると豪華なアスキーアートが表示される。 f:id:yoidea:20170716234805p:plain f:id:yoidea:20170716234817p:plain

インストー

動作環境

Python3またはPython2のインストールされたMacLinuxで動作する。.bash_profileが利用できる環境であればWindowsでも動作するだろう。

Pythonのバージョンを確認するには、

$ python -V

として、「Python 3.x.x」と表示されればPython3、「Python 2.x.x」と表示されればPython2であることがわかる。「-bash: python: command not found」と表示された場合はPython自体がインストールされていない。

ダウンロード

まずはGitでリポジトリからダウンロードする。

$ git clone https://github.com/yoidea/login_bonus_for_shell.git
$ cd login_bonus_for_shell

自動インストー

install.shを実行する。

$ sh install.sh

「インストール完了」と表示されれば無事インストール完了。

手動インストー

任意の名前のディレクトリ(例: ~/LBFS)を作成して、そのなかにファイルをコピーする。

$ mkdir ~/LBFS
$ cp ./* ~/LBFS
Python3の場合

.bash_profileに以下を追加してインストール完了

cd ~/LBFS
python logger.py
cd ~/
Python2の場合

.bash_profileに以下を追加してインストール完了

cd ~/LBFS
python logger_for_python2.py
cd ~/

カスタマイズ

祝辞や定型文はjson形式で保存されており、それを変更することでオリジナルのメッセージに変更することができる。 祝辞はバリエーションが貧弱なのでオリジナルメッセージを追加することを推奨する。

祝辞

eventlist.json内にお祝いの条件とメッセージが一覧で保存されている。

{
    "events": [
        {
            "condition": "continuous",
            "value": 3,
            "message": "🎉おめでとうございます🎉\n連続3日目のログインです!"
        },

定型文

message.json内に定型文が保存されている。

{
    "head": "*---*---*---* ログインボーナス *---*---*---*\n継続は力なり!毎日ログインしましょう",
    "total": {
        "info": " +   通算 <",
        "unit": "> 日目"
    },
    "continuous": {
        "info": " +   連続 <",
        "unit": "> 日目"
    },
    "today": {
        "info": " +   本日 <",
        "unit": "> 回目"
    },
    "foot": "*---*---*---*---*---*--*---*---*---*---*---*"
}

MacにPyenvとTensorflowをインストールする

はじめに

卒業研究で機械学習を利用した研究を行うことになった。最近話題のTensorflowを使いたいが、手元のMacはPython2がプリインストールされており、まず環境構築から始める必要があったのでその過程を記録しておく。

現在インストールされているPython2を消してPython3をインストールする方法もあるが、Python2が必要になることもあるかもしれないので、Pyenvを使ってバージョンを切り替えられるようにしようと思う。

環境はMacOS 10.12.5、Homebrew 1.2.2で行った。

Pyenvのインストー

インストー

PyenvはPythonのバージョン管理ができるツールだ。インストールはHomebrewから簡単にできる。

$ brew update
$ brew install pyenv

初期化設定

Terminalログイン時にPyenvを初期化するようにする。

$ vi ~/.bash_profile

.bash_profileの最後の行に以下を追加すればよい。

eval "$(pyenv init -)"

内容をすぐに適用させたいので.bash_profileを読み込む。

$ source ~/.bash_profile

Python3のインストー

PyenvからPythonをインストールする。現時点で最新のPython3.6にする。

$ pyenv install 3.6.1

インストールできたら、

$ pyenv versions

として、

* system
  3.6.1 (set by /Users/ユーザ名/.pyenv/version)

となっていることを確認する。

$ pyenv global 3.6.1

を実行してPythonのバージョンを3.6に切り替える。

すぐに試したいので、

pyenv rehash

として設定内容を適用する。

これでPythonのインストールは完了、Pythonを実行して、

Python 3.6.1 (default, May 11 2017, 19:41:23) 
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 

と表示されれば成功。

pipのインストー

これまたEasy Installを使うと簡単にインストールできる。

easy_install pip

念のために確認で、

$ pip -V

として、

$ pip 9.0.1 from /Users/ユーザ名/.pyenv/versions/3.6.1/lib/python3.6/site-packages (python 3.6)

が表示されていれば問題なし。

Tensorflowのインストー

これもpipから一瞬でインストールできる。

$ pip install tensorflow

インストールできたらPythonを実行し無事importできれば完了。

$ python
Python 3.6.1 (default, May 11 2017, 19:41:23) 
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import tensorflow
>>> 

今後の課題

次回はTensorflowになれるために、AND, ORをニューラルネットワークで学習させたいと思う。

yoidea.hatenablog.com

Tensorflowで最もシンプルにAND, ORを学習する

はじめに

機械学習のライブラリになれるためにTensorflowを使ってAND, ORを学習した。

環境はMacOS 10.12.5、Python 3.6.1、tensorflow 1.1.0で行った。

yoidea.hatenablog.com

ニューラルネットワーク

ニューラルネットワークについて自身で説明しようと思っていたが、どうも良い文章がかけなかった。もう少し勉強してから書こうと思う。

ANDを実装

Tensorflow読み込み

import モジュール名 as オブジェクト名

とすると、モジュールを自分で名前をつけたオブジェクトとして読み込むことができる。

今回はTensorflowをtfという名前で読み込んでみる。

import tensorflow as tf

ネットワークを定義

ANDは2入力なので入力変数は、要素数2のベクトルと考える。

 \displaystyle
  \mathbf{x} = \begin{pmatrix} x_{1} \\ x_{2} \end{pmatrix}

Tensorflowを利用すると上式のような変数を簡単に作成できる。

tf.placeholder(データ型, [列数, 行数])

とすると、指定したデータ型と大きさの行列をメモリ空間に確保できる。

データ型はtf.float32で問題ないだろう。要素数2のベクトルを作成する。

x = tf.placeholder(tf.float32, [None, 2])

重みは1行2列の行列と考える。

 \displaystyle
  W = \begin{pmatrix} W_{1} & W_{2} \end{pmatrix}
tf.random_normal([列数, 行数])

とすると、指定した大きさの乱数の入った行列を作成できる。

tf.Variable()の引数に、1行2列の乱数配列を格納して重み変数を作成する。

w = tf.Variable(tf.random_normal([2, 1]))

また、バイアス項として1行1列の乱数配列を作成する。

b = tf.Variable(tf.random_normal([1, 1]))

出力変数は、入力変数に重みを掛けてバイアス項を加えたものに活性化関数をかけたものと考える。

 \displaystyle
  \mathbf{z} = sigmoid(\mathbf{y})
 \displaystyle
  \mathbf{y} = W \mathbf{x} + B
tf.matmul(行列1, 行列2)

とすると、行列の掛け算ができる。また、行列の足し算は「+」演算子をそのまま使用できる。

tf.sigmoid(行列)

とすると、活性化関数としてsigmoid関数をかけることができる。

これらを用いて上式を表す。

z = tf.sigmoid(tf.matmul(x, w) + b)

教師データを格納するために要素数1のベクトルを作成する。

t = tf.placeholder(tf.float32, [None, 1])

損失関数を定義する。オーソドックスに2乗誤差の合計でいいだろう。

 \displaystyle
  l = \sum_{i = 1}^{4} (t_{i} - y_{i})^{2}
tf.reduce_sum(行列)

で合計を求められる。

tf.pow(行列, 指数)

でべき乗を求められる。

tf.subtract(行列1, 行列2)

で差を求められる。

これらを用いて二乗誤差を計算する。

l = tf.reduce_sum(tf.pow(tf.subtract(t, y), 2))

入力値・出力値の宣言

入力値と出力値の組み合わせを配列に格納する。すべての入力パターンに対する出力値なので2次元配列になる。

ANDの場合は、

入力1入力2出力
000
010
100
111

なので以下のような配列で表す。

inputs = [[0, 0], [0, 1], [1, 0], [1, 1]]
outputs = [[0], [0], [0], [1]]

最適化方法の設定

Tensorflowで最適化のメソッドが用意されているため、1行の記述で簡単に最適化方法が設定できる。

tf.train.GradientDescentOptimizer(学習レート).minimize(損失)

とすると、損失を小さくするように最急降下法で最適化される。

適当な変数を宣言して損失関数についてこれを適用する。

train = tf.train.GradientDescentOptimizer(0.1).minimize(l)

セッションの作成・初期化

sess = tf.Session()

として、セッションを作成する。

sess.run()の引数によって様々な操作ができる。

sess.run(tf.initialize_all_variables())

とすると、変数が初期化される。どうやら学習を開始するには初期化が必要なようだ。

学習ループ

最適化を繰り返し行い学習をしていく。

sess.run(最適化関数, 入力変数)

とすると、最適化処理が実行される。入力変数はfeed_dict = {x: inputs, t: outputs}という形で入力値・出力値を対応させる。

念のため10000回ループさせてみる。

for step in range(10000):
    sess.run(train, feed_dict = {x: inputs, t: outputs})

途中経過が知りたいので1000回に1回学習過程を表示するようにしたい。

sess.run(損失, 入力変数)

とすると、現在の損失の値が得られる。

sess.run(出力, 入力変数)

とすると、入力に対する現在の出力が得られる。

for step in range(10000):
    sess.run(train, feed_dict = {x: inputs, t: outputs})
    if step % 1000 == 0:
        print("step : " + str(step))
        print("loss = " + str(sess.run(l, feed_dict = {x: inputs, t: outputs})))
        for i in inputs:
            print("x = " + str(i) + " : y = " + str(sess.run(z, feed_dict = {x: [i]})))

これで途中経過が観察できるはずだ。

完成したソースコードは以下のようになった。

import tensorflow as tf

x = tf.placeholder(tf.float32, [None, 2])
w = tf.Variable(tf.random_normal([2, 1]))
b = tf.Variable(tf.random_normal([1, 1]))
z = tf.sigmoid(tf.matmul(x, w) + b)
t = tf.placeholder(tf.float32, [None, 1])
l = tf.reduce_sum(tf.pow(tf.subtract(z, t), 2))

inputs = [[0, 0], [0, 1], [1, 0], [1, 1]]
outputs = [[0], [0], [0], [1]]

train = tf.train.GradientDescentOptimizer(0.1).minimize(l)

sess = tf.Session()
sess.run(tf.initialize_all_variables())

for step in range(10000):
    sess.run(train, feed_dict = {x: inputs, t: outputs})
    if step % 1000 == 0:
        print("step : " + str(step))
        print("loss = " + str(sess.run(l, feed_dict = {x: inputs, t: outputs})))
        for i in inputs:
            print("x = " + str(i) + " : y = " + str(sess.run(z, feed_dict = {x: [i]})))

ORを実装

ORの場合は、

入力1入力2出力
000
011
101
111

なので以下の入出力の配列を以下のように変えるだけでよい。

inputs = [[0, 0], [0, 1], [1, 0], [1, 1]]
outputs = [[0], [1], [1], [1]]

実行結果

AND学習結果

$ python tensorflow_and.py
step : 0
loss = 1.10273
x = [0, 0] : y = [[ 0.30137503]]
x = [0, 1] : y = [[ 0.06085767]]
x = [1, 0] : y = [[ 0.86426908]]
x = [1, 1] : y = [[ 0.48888674]]
step : 1000
loss = 0.0606695
x = [0, 0] : y = [[ 0.00442047]]
x = [0, 1] : y = [[ 0.13255274]]
x = [1, 0] : y = [[ 0.13271405]]
x = [1, 1] : y = [[ 0.84041691]]
step : 2000
loss = 0.0287164
x = [0, 0] : y = [[ 0.00124461]]
x = [0, 1] : y = [[ 0.09150107]]
x = [1, 0] : y = [[ 0.09150464]]
x = [1, 1] : y = [[ 0.89059579]]
step : 3000
loss = 0.0183822
x = [0, 0] : y = [[ 0.00059863]]
x = [0, 1] : y = [[ 0.07328387]]
x = [1, 0] : y = [[ 0.07328429]]
x = [1, 1] : y = [[ 0.91258895]]
step : 4000
loss = 0.0133999
x = [0, 0] : y = [[ 0.00035929]]
x = [0, 1] : y = [[ 0.06260382]]
x = [1, 0] : y = [[ 0.06260396]]
x = [1, 1] : y = [[ 0.9254263]]
step : 5000
loss = 0.0104966
x = [0, 0] : y = [[ 0.00024315]]
x = [0, 1] : y = [[ 0.05542824]]
x = [1, 0] : y = [[ 0.0554283]]
x = [1, 1] : y = [[ 0.93403047]]
step : 6000
loss = 0.00860571
x = [0, 0] : y = [[ 0.00017737]]
x = [0, 1] : y = [[ 0.05020063]]
x = [1, 0] : y = [[ 0.05020065]]
x = [1, 1] : y = [[ 0.94028848]]
step : 7000
loss = 0.0072804
x = [0, 0] : y = [[ 0.00013618]]
x = [0, 1] : y = [[ 0.04618241]]
x = [1, 0] : y = [[ 0.04618241]]
x = [1, 1] : y = [[ 0.94509327]]
step : 8000
loss = 0.00630194
x = [0, 0] : y = [[ 0.00010851]]
x = [0, 1] : y = [[ 0.04297351]]
x = [1, 0] : y = [[ 0.04297351]]
x = [1, 1] : y = [[ 0.94892669]]
step : 9000
loss = 0.00555099
x = [0, 0] : y = [[  8.89249277e-05]]
x = [0, 1] : y = [[ 0.04033687]]
x = [1, 0] : y = [[ 0.04033687]]
x = [1, 1] : y = [[ 0.95207453]]

OR学習結果

$ python tensorflow_or.py
step : 0
loss = 0.885255
x = [0, 0] : y = [[ 0.7940169]]
x = [0, 1] : y = [[ 0.8566581]]
x = [1, 0] : y = [[ 0.61242062]]
x = [1, 1] : y = [[ 0.7101258]]
step : 1000
loss = 0.0355569
x = [0, 0] : y = [[ 0.14283016]]
x = [0, 1] : y = [[ 0.91314256]]
x = [1, 0] : y = [[ 0.91276491]]
x = [1, 1] : y = [[ 0.99848747]]
step : 2000
loss = 0.0154843
x = [0, 0] : y = [[ 0.09367228]]
x = [0, 1] : y = [[ 0.94210422]]
x = [1, 0] : y = [[ 0.94205385]]
x = [1, 1] : y = [[ 0.99960953]]
step : 3000
loss = 0.0096981
x = [0, 0] : y = [[ 0.07396416]]
x = [0, 1] : y = [[ 0.9540332]]
x = [1, 0] : y = [[ 0.95401716]]
x = [1, 1] : y = [[ 0.99981457]]
step : 4000
loss = 0.00700942
x = [0, 0] : y = [[ 0.06280303]]
x = [0, 1] : y = [[ 0.96085531]]
x = [1, 0] : y = [[ 0.96084803]]
x = [1, 1] : y = [[ 0.99988878]]
step : 5000
loss = 0.00546895
x = [0, 0] : y = [[ 0.05542986]]
x = [0, 1] : y = [[ 0.96538645]]
x = [1, 0] : y = [[ 0.96538246]]
x = [1, 1] : y = [[ 0.99992454]]
step : 6000
loss = 0.00447474
x = [0, 0] : y = [[ 0.05011057]]
x = [0, 1] : y = [[ 0.96866691]]
x = [1, 0] : y = [[ 0.96866459]]
x = [1, 1] : y = [[ 0.99994481]]
step : 7000
loss = 0.00378169
x = [0, 0] : y = [[ 0.04604707]]
x = [0, 1] : y = [[ 0.97117931]]
x = [1, 0] : y = [[ 0.97117776]]
x = [1, 1] : y = [[ 0.99995744]]
step : 8000
loss = 0.00327177
x = [0, 0] : y = [[ 0.04281572]]
x = [0, 1] : y = [[ 0.97318089]]
x = [1, 0] : y = [[ 0.97317988]]
x = [1, 1] : y = [[ 0.99996603]]
step : 9000
loss = 0.00288129
x = [0, 0] : y = [[ 0.0401685]]
x = [0, 1] : y = [[ 0.97482318]]
x = [1, 0] : y = [[ 0.9748224]]
x = [1, 1] : y = [[ 0.99997211]]

大体いい感じに学習できているのが確認できた。

今後の課題

次回は2層のニューラルネットワークでは解決できないXORの学習を行いたい。