エイバースの中の人

アプリとWEBサービスを作っているABARSのBLOGです。

ディープラーニング

個人識別のための顔認識の学習済みモデル

個人識別のために顔認識を行う場合、FineTuningを行う方法と、FeatureExtractorを使う方法があります。

前者は、予め決まっている人物を識別したい場合に使用し、VGG16等をベースにFineTuningを行っておき、学習後の学習済みモデルを使用して人物を特定します。後者は再学習は不要な方法であり、事前に膨大な顔画像で学習させておいた学習済みモデルを使用して、その中間層のデータを取得することで、顔の特徴を示す特徴ベクトルを取得し、認識したい人物のベクトルと、カメラから取得した人物のベクトルとの距離を計算することで個人を識別します。

CNNをFeatureExtractorとして使う場合の学習済みモデルとしては、VGG Faceがあります。これは、2015年に公開されており、ネットワーク構造はVGG16です。しかし、VGG Faceは商用利用不可となっています。

そこでTensorFlowで動く商用利用可のモデルとして2017年に公開されたのが、FaceNetです。ネットワーク構造はInception ResNetとなっています。

2018年にはVGG Face2が公開されました。VGG Faceから精度が向上し、商用利用可のライセンスとなっています。ネットワーク構造はResNetとSENetの2バージョンがあります。

VGG Face2のデータセットはクリーニングされており精度が高いため、FaceNetの方でもVGG Face2のデータセットで再学習が行われ、精度が改善しています。

KerasでVGG FaceおよびVGG Face2を使うには、keras-vggfaceを使用することができます。VGG16、ResNet、SENetに対応しています。

test.pyをコメントアウトして実行すると、以下のようにデータセットの中から最も近い人を表示します。

('\n', 'RESNET50')
('\n', array([[1.2077236e-07, 6.9855632e-06, 9.1819614e-01, ..., 1.4105116e-07,
        9.3622623e-07, 9.7075758e-07]], dtype=float32))
('\n', 'Predicted:', [[[' A._J._Buckley', 0.91819614], [' Billy_Butler', 0.007661988], [' Chris_Hardwick', 0.007469104], [' A._J._Pierzynski', 0.0045922087], [' Michael_Voltaggio', 0.0044681374]]])
.('\n', 'VGG16')
('\n', array([[9.7901160e-01, 4.9870639e-08, 7.0865167e-07, ..., 5.4379225e-08,
        7.6642658e-07, 3.7070203e-07]], dtype=float32))
('\n', 'Predicted:', [[['A.J._Buckley', 0.9790116], ['David_Denman', 0.0014457349], ['Carmine_Giovinazzo', 0.0008676527], ['Robert_Buckley', 0.0007245681], ['Eddie_Cahill', 0.00041833066]]])

特徴ベクトルを取得するには、include_top=FalseにしてFC層を除外した上で推論します。VGG Face(VGG16)の場合は4096次元、VGG Face2(ResNet)の場合は2048次元になります。

model = VGGFace(include_top=False, input_shape=(224, 224, 3), pooling='avg',model='vgg16')
print model.predict(x)[0]

IMDB-WIKIによる高精度な年齢・性別推定

現在、公開されている学習済みモデルの中で、最も高精度な年齢・性別推定器であると言われているのが、IMDB-WIKI – 500k+ face images with age and gender labelsです。

imdb


Adience Benchmarkにおける認識精度は性別で91%、年齢で64%です。(Understanding and Comparing Deep Neural Networks for Age and Gender Classification

VGGベースで実装されており、モデルサイズは500MBと大きいのですが、NVIDIA ChaLearn LAP 2015 Best Paper Awardを獲得しており、miniXceptionやage/gender.netに比べ、とても安定した出力を得ることができます。

年齢に関しては0歳〜100歳までの推定確率が出力されるため、年齢ベクトルと内積することで推定年齢を出力します。性別に関してはfemaleとmaleの2カテゴリです。

学習は50万枚の顔画像を使用しており、データセットとラベルもダウンロード可能です。

FDDBのアノテーションを可視化

FDDB + YoloSmallを使用した顔認識の再学習に使用するアノテーションを可視化してみました。



再学習のコードと学習済みモデルは以下にあります。
abars/YoloKerasFaceDetection

VGG16で顔検索エンジンを作る

VGG16で顔画像から特徴量を抽出して顔検索エンジンを作ってみました。

demo

(image from adience_benchmark)

まず、AdienceBenchmarkOfUnfilteredFacesForGenderAndAgeClassificationから顔のデータベースをダウンロードし、feature_extract.pyでImagenetで学習したVGG16の4096次元の特徴ベクトルを取得します。

次に、YoloFaceでWebカメラもしくは画像から顔を検出し、VGG16の4096次元の特徴ベクトルを取得し、事前に計算しておいた顔のデータベースの特徴量とのベクトル間の距離を計算します。ベクトル間の距離が近い順に5件、検索結果として表示しています。

face_search.py captureを使うと、自分の顔もデータベースに登録することができます。再学習不要で認識対象を動的に追加できるので、エッジ側で使うには向いているかなと思います。

abars/FaceSearchVGG16

Kerasで表情を検出する

oarriaga/face_classificationに表情を検出する学習済みモデルがあります。
emotion

FER2013 datasetをベースに学習しており、以下の7カテゴリを検出可能です。

'angry'
'disgust'
'fear'
'happy'
'sad'
'surprise'
'neutral'

ネットワークは64x64x1を入力するXceptionV1となっています。入力画素のレンジは(-1,1)です。容量も1MB未満でとても軽量なので、モバイルでも動作させやすいと思います。

他の表情を検出する学習済みモデルとしては、Emotion Classification CNN - RGBがあり、こちらは227x227x3を入力するモデルです。両方試した印象としては、oarriaga/face_classificationの方がneutralとhappyを正しく認識するように思えました。

この結果を見ると、顔の認識はグレースケールに変換してしまった方が過学習を抑制できてロバストになるのかなと思いました。

人物検出用のデータセット一覧

DarknetやKerasで使える人物検出用のデータセットをまとめました。

Unfiltered faces for gender and age classification dataset

https://www.openu.ac.il/home/hassner/Adience/data.html#agegender

顔画像に対して、人物の年齢と性別が記載されたデータセット。1.76GB。11524枚。

face

学習済みモデル。
Age and Gender Classification using Convolutional Neural Networks

FDDB Dataset

http://vis-www.cs.umass.edu/fddb/

写真に対して、顔の位置が記載されたデータセット。顔の位置は楕円で記載されている。同時に映る人物が少なめなのでYolo向き。75MB。2845枚。

fddb

学習済みモデル。
dannyblueliu/YOLO-version-2-Face-detection

Widerface Dataset

http://mmlab.ie.cuhk.edu.hk/projects/WIDERFace/

写真に対して、顔の位置が記載されたデータセット。パレードなど、同時に映る人物が多めなので、Yolo向きではないかも。1.56GB。12880枚。

widerface

IMDB-WIKI Dataset

https://data.vision.ee.ethz.ch/cvl/rrothe/imdb-wiki/

Wikiの顔画像に対して生年月日と撮影日が記載されたデータセット。Download Faces Onlyで7GB。460723枚。ラベルはMatlab形式。

imdb_wiki

学習済みモデル
Real-time face detection and emotion/gender classification using fer2013/imdb datasets with a keras CNN model and openCV.

ラベルの展開方法
顔画像から年齢・性別を推定するためのデータセットIMDB-WIKI

Vivahand Dataset

http://cvrr.ucsd.edu/vivachallenge/index.php/hands/hand-detection/

車内の画像に対して、手の位置が記載されたデータセット。運転席と助手席もラベル分けされている。3.05GB。5500枚。

vivahand

Hand Dataset

http://www.robots.ox.ac.uk/~vgg/data/hands/

Matlab形式だったので評価できず。194.5MB。4069枚。

handdataset

VGG16におけるKerasの前処理でmeanを引くかどうか

Fine-tuning a Keras model. Updated to the Keras 2.0 APIなど、Kerasのチュートリアルでは、学習画像に以下のような前処理を行っています。

# prepare data augmentation configuration
train_datagen = ImageDataGenerator(
    rescale=1. / 255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1. / 255)

これは、VGG16の学習済みのネットに対して、RGB順で0〜1.0の値を入力しています。

しかし、KerasにおけるVGG16の重みは、Oxford大学のVGGによりCreative Commons Attribution Licenseの下で公開されたものを移植しています。そのため、本来、期待する前処理は、BGR順で0〜255の値からImageNetのmeanを引いた値となります。ただし、CaffeModelからKerasModelへの変換の過程でRGB順への補正は行われているようですので、RGB順で0〜255がKerasとして期待する入力となります。

def preprocess_input(img):
  #img = img[...,::-1]  #RGB2BGR
  #img = img - (104,117,123) #BGR mean value of VGG16
  img = img - (123,117,104) #RGB mean value of VGG16
  return img

train_datagen = ImageDataGenerator(
   preprocessing_function=preprocess_input,
   shear_range=0.2,
   zoom_range=0.2,
   horizontal_flip=True
)

test_datagen = ImageDataGenerator(
   preprocessing_function=preprocess_input,
)

それでは、この変更でどれくらい性能が変わるのでしょうか。AgeGender NetのAgeのClassification問題に対して比較してみました。

変更前(0〜1.0、RGB順、lr=0.01)
agegender_age_vgg16


変更後(0〜255 - mean、RGB順、lr=0.01)
agegender_age_vgg16_with_preprocseeing_rgb

変更後(0〜255 - mean、BGR順、lr=0.01)
agegender_age_vgg16_with_preprocess


結論としては、ラーニングレートが変わるだけで、どちらを使っても問題ないようです。VGG16の畳み込みで画素間差分を取っている過程でDC値の意味が消失しがちなのと、最後の内積のパラメータで調整できてしまうのではないかと思います。個人的には、正しい前処理をした方が一貫性があって気分はよいです。

尚、本件は、チュートリアルの掲示板でも議論になっていますが、特に結論は出ていないようです。

a-ozbek commented on 6 Feb 2017 •  edited 
Excuse me if this issue was brought up before about this script. I couldn't find a resolution to this in the comments.

In this script, during the fine-tuning, the "train_generator" and "validation_generator" do not seem to do VGG16 pre-processing which is

      # 'RGB'->'BGR'  
        x = x[:, :, :, ::-1]  
        # Zero-center by mean pixel  
        x[:, :, :, 0] -= 103.939  
        x[:, :, :, 1] -= 116.779  
        x[:, :, :, 2] -= 123.68  
Isn't it wrong to do fine-tuning of VGG16 without this pre-processing step?
aidiary commented on 16 Feb 2017
@a-ozbek

I have the same question.
I have experimented with and without this pre-processing and get the slightly better result in the case of without this pre-processing...

without pre-processing => val_acc = 93.5%@ epoch 50
with pre-processing      => val_acc = 92.8% @ epoch 50

TensorFlowでcudnn64_6.dllが見つからない

マイナーバージョンがDLL名に含まれているという仕様なので、新しいCUDNNを入れると、cudnn64_7.dllが入り、cudnn64_6.dllを使用しているTensorFlowでエラーが出ます。

Getting the given error while installing gpu version of tensorflow によると、リネームするとよいようです。

if you not get the cudnn64_6.dll, then use below method

you should rename the cudnn64_7.dll or cudnn64_8.dll as cudnn64_6.dll in the below path of your system

C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\bin

Unpredictable CUDNN_STATUS_NOT_INITIALIZED on Windowsが出る問題はまだ解決していません。

Python3.5に対応したdarknet2caffe

marvis/pytorch-caffe-darknet-convertのdarknet2caffeがPython3.5に対応していなかったので、ForkしてPython2.7とPython3.5に対応したabars/darknet2caffeを作りました。

主な修正点は、printの記述方法の変更と、除算の//による整数化です。WindowsのTensorflowが3.5必須なのに対して、CoreMLが2.7必須と混在してきているのが悩ましいですね。

Kerasで顔画像から年齢と性別を推定する

Kerasで顔画像から年齢と性別を推定してみました。リポジトリはYoloKerasFaceDetectionです。データセットには、 AdienceBenchmarkOfUnfilteredFacesForGenderAndAgeClassificationを使用させて頂きました。

年齢と性別の推定には顔領域の画像を与える必要があるため、Webカメラを使用する場合、顔領域を検出して切り出す必要があります。一般にはOpenCVを使用しますが、OpenCVは眼鏡付きの顔の認識精度が低いため、今回、顔検出にはYoloを使用しています。顔検出の係数はYOLOライセンスのYOLO-version-2-Face-detectionを使用させて頂きました。そのため、Caffeも必要です。

学習済みモデルを同じフォルダにおいた後、以下のコマンドでWebカメラから年齢と性別の推定が可能です。

python agegender_demo.py keras

年齢のカテゴリは以下の8カテゴリです。

age 0-2
age 4-6
age 8-13
age 15-20
age 25-32
age 38-43
age 48-53
age 60-

性別のカテゴリは以下の2カテゴリです。

female
male

学習はVGG16をファインチューニングしました。学習のコードはagegender_train.pyにあります。

最初は年齢と性別を同時に認識させたのですが、うまく精度が出ませんでした。そのため、年齢と性別を別のネットワークで学習させるように変更しました。ただ、分解してもAge Netが過学習ぎみなので、もう少し追い込む必要がありそうです。

Age & Gender
agegender_agegender_vgg16
Age
agegender_age_vgg16

Gender
agegender_gender_vgg16


先行事例としては、Age and Gender Classification using Convolutional Neural Networksがあり、そこからTensorFlow + InceptionV3版のAge/Gender detection in Tensorflowがあります。また、MicrosoftのVison APIのデモがHow-Old.netにあります。

リファレンスモデルも以下のコマンドで実行できます。自己学習モデルは現状はまだ精度が出ていないので、Age and Gender Classification using Convolutional Neural Networksのリファレンスモデルを使用することをオススメします。

python agegender_demo.py caffe

実行すると以下のように顔検出と年齢・性別検出を行います。

demo
(出典:WIDER face dataset

VGG16を特徴検出器として使う話

VGG16というConvolutional Neural Networkがあります。VGG16は224x224x3画素の画像を入力して、1000クラスの推定確率を出力するネットワークです。3x3のカーネルの畳み込みと2x2のプーリングを繰り返すことで、4096次元のベクトルを計算し、最後の全結合層で1000クラスの推定確率を計算します。


vgg16
(出典:VGG in TensorFlow


VGG16は深いネットワークなため、全体を再学習するには十分なデータが必要です。そのため、1000クラスに含まれていないオリジナルな画像を学習させようとした場合、画像の量が不足してうまく認識できません。

そこで、最後の全結合層だけを再学習するのが転移学習、最後の全結合層とその直前の畳み込み層を再学習するのがファインチューニングです。(VGG16のFine-tuningによる犬猫認識 (2)

なぜこれがうまくいくのかといえば、VGG16の前半が膨大な画像で学習しているため、画像に特化した特徴検出器になっているためです。最後の全結合層だけを見ると、やっていることは、VGG16が計算した4096次元のベクトルと、クラスごとの4096次元のベクトルの内積です。つまり、特徴空間でコサイン類似度を計算していることになります。

これより、新しいクラスを認識させたい場合、全体を再学習する必要はなく、クラスに固有の4096次元のベクトルを用意すれば十分ということになります。これを応用すると、データベース内の全ての画像に対してVGG16で計算した4096次元のベクトルをデータベースに格納し、ユーザから入力された画像に対してVGG16で計算した4096次元のベクトルとの内積を取るだけで、画像検索エンジンを作ることができます。(1時間で画像検索エンジンを作る

結局のところ、CNNはKL変換のような基底系を計算しているわけで、KL変換との違いは非直交であることと、非線形関数が入っていることであるといえます。

もちろん、新しいクラスの画像が加われば、最適な基底系は変化するため、全結合層だけでなく、もう一段上の畳み込み層まで含めて再学習すれば、より精度は向上します。

しかし、一見、再学習が必要そうな個人認証のような分野に対しても、再学習せずに特徴量の内積だけで実装可能というのは、可能性が広がって面白いのではないかと思います。

VGG16を使用した検索エンジンのデモとしては、KawaiiSearchがあります。リポジトリはblan4/KawaiiSearchです。ファッションECなどでも使用できそうです。

68747470733a2f2f692e696d6775722e636f6d2f653962707757592e706e67


68747470733a2f2f692e696d6775722e636f6d2f6444414a4375592e706e67

また、VGG16を使用した顔検索をabars/FaceSearchVGG16で実験中です。

keras2caffeでFlattenを含むと変換できない

uhfband/keras2caffeで以下のネットワークがうまく変換できません。

  model.add(InputLayer(input_shape=input_shape))
  model.add(Conv2D(32, kernel_size=(3, 3),use_bias=False))
  model.add(Activation('relu'))
  model.add(Conv2D(64, (3, 3),use_bias=False))
  model.add(Activation('relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Dropout(0.25))
  model.add(Flatten())
  model.add(Dense(128))
  model.add(Activation('relu'))
  model.add(Dropout(0.5))
  model.add(Dense(num_classes))
  model.add(Activation('softmax'))

これは、Kerasがchannels_lastであるinput_shape=(224, 224, 3)、Caffeがchannels_firstであるinput_shape=(3, 224, 224)で画像を扱うため、keras2caffeの内部でtransposeしているためです。Flattenは元のベクトルをそのまま1次元に変換するため、channels_lastとchannels_firstが異なると、異なるベクトルに変換され、その後のDenseでのInnerProductで正しくない値になります。

このネットをうまく変換するには、FlattenのDenseでInnnerProductのweightを並び替える必要があります。今回のネットワークでは、パッチワーク的に以下の変換をかけると動作します。

elif layer_type=='Dense':
    caffe_net[name] = L.InnerProduct(caffe_net[outputs[bottom]], 
    	num_output=config['units'], weight_filler=dict(type='xavier'))
    
    if config['use_bias']:
        weight=np.array(blobs[0]).transpose(1, 0)
        print(weight.shape)
        if weight.shape[1]==9216:
            for i in range(128):
                weight[i]=np.array(weight[i].reshape(12,12,64).transpose(2,0,1).reshape(9216))
        net_params[name] = (weight, np.array(blobs[1]))
    else:
        net_params[name] = (blobs[0])


VGG16もFlattenの後にDenseが続く構造なため、同様の変換が必要です。変換を一般化するには、Flattenでlayer.input_shapeを保存しておき、その次のDenseでweightの並び替えを行います。

abars/keras2caffeの該当コミット

他、Keras2caffeはConvの中にactivationを含む場合や、InputLayerが存在しない場合に変換に失敗するため、いくつか修正が必要です。

上記修正は本家にプルリクエストを出してみました。
uhfband/keras2caffe/pull/1

---2018/1/2追記
マージして頂けましたので、最新版ではVGG16が動作します。

Kerasでアニメキャラ識別器を作る

すでに先行事例が多いですが、Kerasでアニメキャラ識別器を作りました。ソースコードはgithubに上げてあります。
abars/AnimeFaceClassifier

result

Hatsune Miku / Crypton Future Media inc. / CC BY-NC

OpenCVでアニメキャラの顔を検出後、CNNにかけて、キャラを当てます。データセットはanimeface-character-datasetを使用させて頂きました。

32x32に縮小した画像を入力して2層の畳み込みを行うsmall_cnnで学習をかけた場合、認識精度は70%程度です。学習はMac Pro 2013のFireProD300で2日程度かかりました。

認識精度を上げるため、VGG16を使用したファインチューニングを実験し、テストデータセットでの認識精度は82%まで上がりました。ただ、small_cnnとは異なり、初音ミクとランカ・リーを間違えることがあります。InceptionV3なども試した方がよいかもしれません。

Yoloで顔検出を行う

YOLO-version-2-Face-detectionにあるcfgファイルとweightファイルを使用すると、Yolo v1を使用して顔検出を行うことができます。

実行コマンド
./darknet yolo test yolo-face.cfg yolo-face_final.weights data/person.jpg

公式デモ


学習にはFDDB使用しているようです。顔画像のデータセットは、WiderFaceDatasetも有名ですね。

このネットはAnchorが含まれていないYolo v1の形式になっています。darknet2caffeを使用すれば、caffemodelに変換可能です。変換後のCaffeModelは178.1MBです。

python darknet2caffe.py yolo-face.cfg yolo-face_final.weights face.prototxt face.caffemodel

変換後の係数は、caffe-yoloで動作します。その際、num_class=1、grid_size=11に対応するよう、yolo_main.pyを書き換える必要があります。

python yolo_main.py -m face.prototxt -w face.caffemodel -i faces.jpg

初音ミクの顔も検出できたので、意外とアニメ顔もいけるかもしれません。

OpenCVのFace DetectionはFace Detection using Haar Cascadesというアルゴリズムを使用していますが、Yoloが動く環境であれば、OpenCVを使用しなくてもより手軽に顔検出を行うことができそうです。



また、現在、再学習をYoloKerasFaceDetectionで実験中です。

Macでkerasを使う

インストール

KerasとTensorflowのインストールを行います。

pip install keras
pip install tensorflow

OpenCLバックエンド用にPlaidMLを入れます。

pip install plaidml-keras
plaidml-setup 

サンプルの実行

MNISTのサンプルコードをダウンロードします。
keras/examples/mnist_cnn.py

import kerasの前にバックエンドを選択します。

import plaidml.keras
plaidml.keras.install_backend()

実行します。

python mnist_cnn.py

学習が行われます。

python mnist_cnn.py 
x_train shape: (60000, 28, 28, 1)
60000 train samples
10000 test samples
INFO:plaidml:Opening device "amd_radeon_hd_-_firepro_d300_compute_engine.1"
Train on 60000 samples, validate on 10000 samples
Epoch 1/12
INFO:plaidml:Analyzing Ops: 85 of 285 operations complete
59904/60000 [============================>.] - ETA: 0s - loss: 0.3243 - acc: 0.9024INFO:plaidml:Analyzing Ops: 85 of 285 operations complete
60000/60000 [==============================] - 43s - loss: 0.3241 - acc: 0.9025 - val_loss: 0.0758 - val_acc: 0.9751
Epoch 2/12
60000/60000 [==============================] - 34s - loss: 0.1109 - acc: 0.9673 - val_loss: 0.0546 - val_acc: 0.9831
Epoch 3/12
60000/60000 [==============================] - 34s - loss: 0.0847 - acc: 0.9753 - val_loss: 0.0444 - val_acc: 0.9847
Epoch 4/12
60000/60000 [==============================] - 34s - loss: 0.0723 - acc: 0.9789 - val_loss: 0.0377 - val_acc: 0.9873
Epoch 5/12
60000/60000 [==============================] - 34s - loss: 0.0622 - acc: 0.9813 - val_loss: 0.0350 - val_acc: 0.9882
Epoch 6/12
60000/60000 [==============================] - 34s - loss: 0.0563 - acc: 0.9839 - val_loss: 0.0326 - val_acc: 0.9890
Epoch 7/12
60000/60000 [==============================] - 34s - loss: 0.0525 - acc: 0.9839 - val_loss: 0.0314 - val_acc: 0.9888
Epoch 8/12
60000/60000 [==============================] - 34s - loss: 0.0474 - acc: 0.9857 - val_loss: 0.0302 - val_acc: 0.9892
Epoch 9/12
60000/60000 [==============================] - 34s - loss: 0.0446 - acc: 0.9868 - val_loss: 0.0312 - val_acc: 0.9896
Epoch 10/12
60000/60000 [==============================] - 34s - loss: 0.0418 - acc: 0.9878 - val_loss: 0.0304 - val_acc: 0.9897
Epoch 11/12
60000/60000 [==============================] - 34s - loss: 0.0379 - acc: 0.9886 - val_loss: 0.0294 - val_acc: 0.9906
Epoch 12/12
60000/60000 [==============================] - 34s - loss: 0.0377 - acc: 0.9890 - val_loss: 0.0295 - val_acc: 0.9901
Test loss: 0.0294891050935
Test accuracy: 0.9901

重み保存と変換

重み保存と読み込みをするには、以下のコマンドを使用します。
Keras FAQ: Kerasに関するよくある質問

json_string = model.to_json()
model.save_weights('my_model_weights.h5')

from keras.models import model_from_json
model = model_from_json(json_string)
model.load_weights('my_model_weights.h5')

重み変換をするには、以下のスクリプトを使用します。
keras2caffe (MIT license)
import keras2caffe
keras2caffe.convert(model,"my_model.prototxt","my_model.caffemodel")

尚、重み変換をする場合はInputLayerが存在しないとエラーになります。また、activationは分離して記載する必要があります。KerasのSequentialモデルでInputLayerを明示的に追加するを参考に以下のようにInputLayerの追加と、activationの分離を行います。

model = Sequential()
model.add(InputLayer(input_shape=input_shape))
model.add(Conv2D(32, kernel_size=(3, 3)))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes))
model.add(Activation('softmax'))

Fine Tuning

少ない画像から画像分類を学習させる方法(kerasで転移学習:fine tuning)
Kerasではtrainフォルダとvalidationフォルダにカテゴリ別のフォルダを作成し、画像を入れておくと、カテゴリ識別を学習させることができます。例えば、AnimeFace Character Datasetのthumbを適当なスクリプトでtrainとvalidationに分解します。また、その他のカテゴリを学習させるには、Bing Search APIが教師データ作成に便利です。

Yolo

basic-yolo-kerasを使用してYoloの係数を学習することができます。実際の学習を走らせる時はconfig.jsonのwarmup_epochsを0にします。ただし、plaidmlのバックエンドだと動作しないため、実用的な速度で動かすにはCUDAが必要です。

python train.py -c config.json
python predict.py -c config.json -w full_yolo_raccoon.h5 -i raccoon_dataset/images/raccoon-1.jpg

Windows10にDarknetを入れる

Gigabyte Aorusゲームボックスを使うため、Windows10にDarknetを入れたのでメモです。PCはNUCです。



Thunderbolt Driverのインストール


NUCのサイトからtbt_win10_64_17.2.71.250.zipをダウンロードしてインストール。

VisualStudio 2015のインストール


MSDNからVisual Studio 2015をダウンロード。2017のダウンロードページに飛ばされるので、最下部の以前のバージョンからダウンロード。(以前のバージョンをお使いになりたいですか?

OpenCVのインストール


opencv/opencvからopencv-3.1.0.exeを展開。opencv/build/vc14/binにパスを通す。

CUDAのインストール


CUDA Toolkit Downloadからインストール。バージョンは9.0ではなく8.0が必要。cuDNNをダウンロード。登録が必要。cuda/binにパスを通す。尚、後述するようにDarknetのcuDNN対応がまだ微妙なため、Darknetだけを使用する場合はCUDA Toolkitだけをインストールするのでもよい。Tensorflowを使用する場合は必要。

Darknetのインストール


AlexeyAB/darknetをClone。build/darknet/darknet.slnを開き、opencv/build/vc14/libとopencv/build/includeとcuda/includeとcuda/libにパスを通す。CUDNNを有効にすると正しく認識が動作しないため、CUDNNのdefineを外し、GPUのみdefineを残す。ビルドするとx64フォルダにdarknet.exeができる。

パフォーマンス


WindowsのパフォーマンスモニタではCUDAのGPU稼働率を見ることはできないが、GPUが熱を持っているので稼働はわかる。TinyYoloで100Epocが、CPU(Xeon)だと12時間、GPU(1070)だと3分。240倍の性能。

Anacondaのインストール


Anacondaの公式サイトから3.6のバージョンを入手。Python 3.5の環境に変更。

conda install python=3.5
conda install pip


Kerasのインストール


以下のコマンドでKerasとTensorflowをインストール。

pip install keras
pip install tensorflow-gpu


Pytorchのインストール


以下のコマンドでtorchをインストール。

conda install -c peterjc123 pytorch 


Caffeのインストール


BVLC/caffeからビルド済みバイナリのVisual Studio 2015, CPU only, Python 3.5: Caffe Releaseをダウンロード。Pythonから使えるようにするためにcaffe/python/caffeをc:/users/user_bane/Anaconda/Python3.5/Lib/site-packagesにコピー。import caffeでDLL load failedが発生するため、caffe-windowsからthirdparty20170624.rarをダウンロードして./windows/thirdparty/binsにパスを通す。

Darknetを使用してYoloの係数を学習する

Darknetの概要


DarknetはYoloの開発者が開発しているディープラーニングのフレームワークです。C++で記述されており、簡単にビルドすることができます。Yoloの歴史についてはYOLO9000: Better, Faster, Strongerの資料が詳しいです。

Darknetを使用してYoloの係数を学習するチュートリアルは以下が詳しいです。
How to train YOLOv2 to detect custom objects

Darknetのインストール


環境構築はMacかUbuntuが簡単です。まず、リポジトリをCloneします。

git clone https://github.com/AlexeyAB/darknet.git

GNUmakeを使用してmakeします。darknetのバイナリが生成されます。

make

nVidiaのGPUであればMakefileを書き換えることでCUDAを有効にすることができます。今回はAMDのGPUを搭載したMacなため、CPUで動かします。

学習させるデータの準備


学習させるデータをダウンロードします。上記記事のサンプルデータがテストには最適です。
https://timebutt.github.io/content/other/NFPA_dataset.zip

ただし、pos-234.jpgの拡張子が大文字のJPGになっているため、小文字のjpgに変換する必要があります。また、train.txtとtest.txtの改行コードをWindows StyleからLinux Styleに変換する必要があります。

データセットには、入力画像のjpgと、バウンディングボックスのtxtが含まれています。バウンディングボックスのtxtのフォーマットは以下のようになります。画像のどの位置にどのカテゴリのオブジェクトが存在するかが記載されています。同様の形式で、jpgとtxtのペアを準備することで、任意の画像で学習させることができます。

[category number] [object center in X] [object center in Y] [object width in X] [object width in Y]

YoloV2での学習


次に、obj.data、obj.names、yolo-obj.cfgを準備します。

obj.dataには学習のための基本情報が含まれます。上から、識別クラス数、入力画像のファイルリスト、評価画像のファイルリスト、クラス名称、係数の出力ディレクトリです。trainやvalidはdarknetのバイナリからの相対パスです。

classes=1
train=data/nfpa/train.txt
valid=data/nfpa/test.txt
names=data/nfpa/obj.names
backup=backup/

obj.namesにはカテゴリ名称が含まれます。

NFPA

yolo-obj.cfgにはYoloのネットワーク構成が含まれます。yolo-voc.cfgをコピーして、batchを64に、subdivisionsを8に、classesを識別させたいクラス数に書き換えます。最終段のfiltersを(classes + 5)*5に書き換えます。

[net]
# Testing
batch=64 #書き換える
subdivisions=8 #書き換える
# Training
# batch=64
# subdivisions=8
height=416
width=416
channels=3

省略...

[convolutional]
size=1
stride=1
pad=1
filters=30 #書き換える
activation=linear

[region]
anchors =  1.3221, 1.73145, 3.19275, 4.00944, 5.05587, 8.09892, 9.47112, 4.84053, 11.2364, 10.0071
bias_match=1
classes=1 #書き換える
coords=4
num=5
softmax=1
jitter=.3
rescore=1

省略...

収束を速めるため、初期係数列をダウンロードしておきます。
darknet19_448.conv.23

最後に、以下のコマンドで学習を行います。

./darknet detector train data/nfpa/obj.data data/nfpa/yolo-obj.cfg darknet19_448.conv.23

学習にはハイエンドのGPUで1時間かかるということで、CPUだけだと厳しい感はあります。
学習結果は以下のコマンドで確認します。

./darknet detector test data/nfpa/obj.data data/nfpa/yolo-obj.cfg yolo-obj1000.weights testimage.jpg

学習済みのTiny-Yoloを動かしてみるには、ofxDarknetからtiny-yolo-voc.weightsをダウンロードした後、以下のコマンドで確認します。

./darknet detector test cfg/voc.data cfg/tiny-yolo-voc.cfg tiny-yolo-voc.weights data/person.jpg

上記ページのCOCO版のtiny-yolo.weightは学習時のcfgが異なるためか、正しく動作しないため、digitalbrain79/pyyoloのウエイトファイルを使用します。

./darknet detector test cfg/coco.data cfg/tiny-yolo.cfg tiny-yolo.weights data/person.jpg

YoloV1での学習


係数をCaffemodelに変換するには、pytorch-caffe-darknet-convertを使用します。変換にはCaffeのInstallが必要です。

python darknet2caffe.py tiny-yolo.cfg tiny-yolo.weights out.prototxt out.caffemodel

ただし、YoloV2にはCaffeではサポートされていないRegion Layerが含まれています。そのため、Caffeで動かす場合は、cfg/yolov1を使用する必要があります。Tiny-Cocoのweightsはyolov1のページからダウンロードできます。

動作テスト
./darknet coco test cfg/yolov1/tiny-coco.cfg tiny-coco.weights data/giraffe.jpg

Caffeモデルへの変換
python darknet2caffe.py tiny-coco.cfg tiny-coco.weights out.prototxt out.caffemodel

Yolov1で学習するには以下のコマンドを使用します。使用する画像の設定はsrc/yolo.cで行う必要があります。(You Only Look Once:Unified, 統合されたリアルタイムオブジェクトの検出

char *train_images = "/home/pjreddie/data/voc/test/train.txt";
char *backup_directory = "/home/pjreddie/backup/";

cfgのclassesとconnectedのoutputを書き換えます。outputはcfgのside*side*((coords+1)*n+classes)です。nfpaの1カテゴリでtiny-yoloを使用する場合は539になります。

VOCの学習
./darknet yolo train cfg/yolov1/yolo.train.cfg

yolo.cfgとyolo.train.cfgの違いはbatchとsubdivisionsだけです。

nfpaの学習
./darknet yolo train cfg/yolov1/tiny-yolo-nfpa.cfg

学習はMac Pro 2013だとシングルスレッドで24時間で200 Epoc、OpenMPで12時間で1400Epocでした。GTX1070を使えば、1時間で2000Epoc回すことができました。Darknetで2クラスの物体をトレーニングによると8000Epoc近く回す必要があるみたいなので、CPUで8000Epoc回すには3日程度必要です。GPUだと4時間程度で終わります。

クラス数1の学習結果のテストを行うにはyolo.cのdraw_detectionsの最後の引数の20をl.classesに書き換える必要があります。

学習結果のテスト
./darknet yolo test cfg/yolov1/tiny-yolo-nfpa.cfg backup/tiny-yolo-nfpa_200.weights data/nfpa/pos-1.jpg

500Epoc
500
1000Epoc
1000
2000Epoc
2000

2000 Epocまでいけばわりと認識できるようです。

(補足)Caffeのインストール


darknet2caffe.pyの実行にはCaffeが必要です。CaffeのインストールにはMakefile.configに以下の設定が必要でした。PYTHON_LIBのパスが合ってないと、import CaffeでSegmentation Faultが起きるようです。リンカフラグへのlibstdc++の設定は不要でした。

CPU_ONLY := 1
OPENCV_VERSION := 3
WITH_PYTHON_LAYER := 1
PYTHON_LIB :=  /usr/local/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/
INCLUDE_DIRS += /usr/local/Cellar/boost@1.59/1.59.0/include /usr/local/Cellar/boost-python@1.59/1.59.0/include
LIBRARY_DIRS += /usr/local/Cellar/boost@1.59/1.59.0/lib /usr/local/Cellar/boost-python@1.59/1.59.0/lib
INCLUDE_DIRS += /usr/local/lib/python2.7/site-packages/numpy/core/include

(補足)Darknetのマルチスレッド化


Darknetはデフォルトでは1スレッドで動作します。CPUでマルチスレッド化するには、OpenMPを有効化します。XcodeのデフォルトではOpenMPが使用できないため、最新のLLVMをインストールします。
brew install llvm

Makefileを書き換えます。
OPENMP=1
CC=/usr/local/opt/llvm/bin/clang
CPP=/usr/local/opt/llvm/bin/clang++
LDFLAGS+= -L/usr/local/opt/llvm/lib -Wl,-rpath,/usr/local/opt/llvm/lib

YOLOの出力ベクトルからバウンディングボックスを計算

YOLOは、シンプルなCNNでオブジェクトのカテゴリとバウンディングボックスを検出することができるネットワークです。概要はChainerでYOLOからどうぞ。

YOLOの入力は448x448ピクセルの画像を、RRR...GGG...BBB...で並べて与えます。その際、入力画素のレンジは+-1.0に正規化する必要があるので、RGBが8bitの場合は、normalizedRGB=RGB/255.0f*2-1.0fとします。また、Bitmapの上下のスキャン順にも注意します。

対して、YOLOの出力は1470次元のベクトルであり、バウンディングボックスを計算するには、やや複雑な計算が必要です。

バウンディングボックスの計算方法はYOLOtiny_chainer/predict.pyのコードを参考にさせて頂きました。

YOLOの出力は、順に以下の項目で構成されます。

  probs=ans[0:980].reshape((7,7,20))     # class probabilities
  confs=ans[980:1078].reshape((7,7,2))   # confidence score for Bounding Boxes
  boxes = ans[1078:].reshape((7,7,2,4))  # Bounding Boxes positions (x,y,w,h)


probsは64x64ピクセルを1グリッドとして、画像を7x7個のグリッドに区切ったそれぞれで、以下の20カテゴリの推定確率が入っています。

  classes = ["aeroplane", "bicycle", "bird", "boat", "bottle",
             "bus", "car", "cat", "chair", "cow",
             "diningtable", "dog", "horse", "motorbike", "person",
             "pottedplant", "sheep", "sofa", "train","tvmonitor"]


confsには7x7個のグリッドにつき、2つのバウンディングボックスの信頼度が入っています。

boxesには7x7個のグリッドの2つのバウンディングボックスの座標が、(x,y,w,h)の順に入っています。wとhはsqrtがかかっています。そのため、有効なバウンディングボックスの座標は以下で計算することができます。

for(int by=0;by<7;by++){
  for(int bx=0;bx<7;bx++){
    for(int box=0;box<2;box++){
       for(int category=0;category<20;category++){
         if(confs[by][bx][box]*probs[by][bx][category]>0.1f){
           x= (bx+boxes[by][bx][box][0])*(448/7);
           y = (by+boxes[by][bx][box][1])*(448/7);
           w = pow(box[by][bx][box][2],2)*448;
           h = pow(box[by][bx][box][3],2)*448;
           x1 = x - w/2;
           x2 = x + w/2;
           y1 = y - h/2;
           y2 = y + h/2;
           class = classes[category];
        }
     }
  }
}


尚、このままでは、1つのオブジェクトに対して複数のバウンディングボックスが描画されてしまいます。この問題を解決するには、You Only Look Once:Unified, 統合されたリアルタイムオブジェクトの検出を参考に、計算したバウンディングボックスに対してNon-Maximum suppressionを適用する必要があります。Non-Maximum suppressionでは、クラスごとに推定確率の高い順にバウンディングボックスをソートし、iou関数でバウンディングボックスの重なりを検出、バウンディングボックスが重なっている場合は除外します。

    # suppress non-maximal boxes
    for c in range(nb_class):
        sorted_indices = list(reversed(np.argsort([box.classes[c] for box in boxes])))

        for i in range(len(sorted_indices)):
            index_i = sorted_indices[i]
            
            if boxes[index_i].classes[c] == 0: 
                continue
            else:
                for j in range(i+1, len(sorted_indices)):
                    index_j = sorted_indices[j]
                    
                    if bbox_iou(boxes[index_i], boxes[index_j]) >= nms_threshold:
                        boxes[index_j].classes[c] = 0

basic-yolo-kerasにおけるNon-Maximum suppressionの実装

ディープラーニングの重みを圧縮するDeep Compression

DEEP COMPRESSION: COMPRESSING DEEP NEURAL NETWORKS WITH PRUNING, TRAINED QUANTIZATION AND HUFFMAN CODING

ディープラーニングの重みの圧縮に関する論文です。Network pruning(刈り込み)と非線形量子化によってAlexNetの係数を240MBから6.9MBまで圧縮します。

以下、論文からアーキテクチャを意訳してみました。

architecture


Network Pruning

Network pruningはCNNモデルの圧縮において広く利用されています。1989年にLeCunによって、ネットワークの複雑さの削減とオーバーフィッティングのために有効であると証明されました。また、2015年にHanによって、近代のState-of-the-artなCNN modelsに対して適用されました。

Pruningでは、まず、connectivityを通常のネットワークトレーニングによって学習します。次に、小さい重みのconnectionを刈り込みます。全てのしきい値以下の重みがネットワークから取り除かれます。最後に、残っているスパースなconnectionに対して、再学習を行います。Pruningによって、AlexNetのパラメータは1/9〜1/13まで削減されます。

Pruningによってスパースになった重みは、圧縮行格納方式 CSR(compressed sparse row)もしくは圧縮列格納方式 CSC(compressed sparse column)フォーマットで符号化します。その結果、2a+n+1個の値が残ります。aは非0の要素数、nはrowかcolumnの数です。

さらに圧縮するため、非0重みの出現位置を、絶対位置ではなく相対位置インデックスで符号化します。畳み込み層に対しては8bit、全結合層に対しては5bitで符号化します。8bitもしくは5bitを超えた値が出現した場合、0を符号化することで、残差が8bitもしくは3bitを超えないようにします。

padding


Trained Quantization and Weight Sharing

ネットワークの量子化と重みの共有化によって、刈り込まれたネットワークの重みをさらに圧縮します。保存すべき複数のconnectionで共有される効果的な重みを見つけ、共有された重みをfine-tuneします。

quantization


Weight Sharingの概念を図3に示します。4入力、4出力のニューロンがあり、重みは4x4行列となっています。左上が係数の行列、左下が勾配の行列です。重みは量子化され、4つのbinsに区分けされます。各binを色分けして示しています。同じbinに格納された重みは、共通の重み(セントロイド)に量子化されます。その結果、重みは共有された重みテーブルのインデックスとなります。updateによって、勾配はグループごとに平均化され、学習率を乗じ、セントロイドを更新します。

刈り込みされたAlexNetにおいて、1つの畳込み層につき、重みは256 shared weightsまで非線形量子化され、各重みは8bitとなります。また、1つの全結合層につき、重みは32 shared weightsまで非線形量子化され、各重みは5bitになります。

quantized_result


量子化によって、4x4行列が、4セントロイドと2bitインデックスになるため、データサイズは1/3まで削減されます。

ハフマン符号化

量子化された重みと、疎行列のインデックスをハフマン符号化することで、データサイズは20%〜30%削減されます。

MacでPaintsChainerを動かす

PaintsChainerがすごすぎたので、ソースコードをダウンロードしてMacで動かしてみました。

paints_chainer


手順としては、PaintsChainerをCloneした後、学習済みデータをcgi-bin/paint_x2_unet/models/に置きます。

次に、Chainerをpip install chainerでインストールします。

PaintsChainerのPython 3.0に対して、MacのデフォルトのPythonは2.7なので、server.pyのhttp.serverをBaseHTTPServerに書き換える必要があります。


diff --git a/server.py b/server.py
index 52b37be..d6622a5 100644
--- a/server.py
+++ b/server.py
@@ -1,7 +1,11 @@
#!/usr/bin/env python

-import http.server
-import socketserver
+#import http.server
+#import socketserver
+#from urllib.parse import parse_qs
+
+import BaseHTTPServer
+import CGIHTTPServer

import os, sys
import base64
@@ -10,7 +14,6 @@ import json
import argparse

from cgi import parse_header, parse_multipart
-from urllib.parse import parse_qs


#sys.path.append('./cgi-bin/wnet')
@@ -19,13 +22,13 @@ import cgi_exe



-class MyHandler(http.server.CGIHTTPRequestHandler):
+class MyHandler(CGIHTTPServer.CGIHTTPRequestHandler):
def __init__(self,req,client_addr,server):
- http.server.CGIHTTPRequestHandler.__init__(self,req,client_addr,server)
+ CGIHTTPServer.CGIHTTPRequestHandler.__init__(self,req,client_addr,server)

def parse_POST(self):
ctype, pdict = parse_header(self.headers['content-type'])
- pdict['boundary'] = bytes(pdict['boundary'], "utf-8")
+ #pdict['boundary'] = bytes(pdict['boundary'], "utf-8")
if ctype == 'multipart/form-data':
postvars = parse_multipart(self.rfile, pdict)
elif ctype == 'application/x-www-form-urlencoded':
@@ -80,7 +83,10 @@ class MyHandler(http.server.CGIHTTPRequestHandler):
else:
paintor.colorize(id_str, blur=blur)

- content = bytes("{ 'message':'The command Completed Successfully' , 'Status':'200 OK','success':true , 'used':"+str(args.gpu)+"}", "UTF-8")
+ #content = bytes("{ 'message':'The command Completed Successfully' , 'Status':'200 OK','success':true , 'used':"+str(args.gpu)+"}", "UTF-8")
+
+ content = "{ 'message':'The command Completed Successfully' , 'Status':'200 OK','success':true , 'used':"+str(args.gpu)+"}"
+
self.send_response(200)
self.send_header("Content-type","application/json")
self.send_header("Content-Length", len(content))
@@ -102,7 +108,9 @@ print('GPU: {}'.format(args.gpu))

paintor = cgi_exe.Paintor( gpu = args.gpu )

-httpd = http.server.HTTPServer(( args.host, args.port ), MyHandler)
+#httpd = http.server.HTTPServer(( args.host, args.port ), MyHandler)
+httpd = BaseHTTPServer.HTTPServer((args.host, args.port ), MyHandler)
+
print('serving at', args.host, ':', args.port, )
httpd.serve_forever()


また、MacはAMDのGPUを搭載しておりCUDAが動かないので、GPUを切ります。cuda.と.get()をコメントアウトします。


diff --git a/cgi-bin/paint_x2_unet/cgi_exe.py b/cgi-bin/paint_x2_unet/cgi_exe.py
index 33c381b..86bc4d7 100755
--- a/cgi-bin/paint_x2_unet/cgi_exe.py
+++ b/cgi-bin/paint_x2_unet/cgi_exe.py
@@ -30,11 +30,11 @@ class Paintor:
self.gpu = gpu

print("load model")
- cuda.get_device(self.gpu).use()
+ #cuda.get_device(self.gpu).use()
self.cnn_128 = unet.UNET()
self.cnn = unet.UNET()
- self.cnn_128.to_gpu()
- self.cnn.to_gpu()
+ #self.cnn_128.to_gpu()
+ #self.cnn.to_gpu()
lnn = lnet.LNET()
#serializers.load_npz("./cgi-bin/wnet/models/model_cnn_128_df_4", cnn_128)
#serializers.load_npz("./cgi-bin/paint_x2_unet/models/model_cnn_128_f3_2", cnn_128)
@@ -56,7 +56,7 @@ class Paintor:


def liner(self, id_str):
- cuda.get_device(self.gpu).use()
+ #cuda.get_device(self.gpu).use()

image1 = cv2.imread(path1, cv2.IMREAD_GRAYSCALE)
image1 = np.asarray(image1,self._dtype)
@@ -64,16 +64,16 @@ class Paintor:
image1 = image1[:, :, np.newaxis]
img = image1.transpose(2, 0, 1)
x = np.zeros((1, 3, img.shape[1], img.shape[2] )).astype('f')
- x = cuda.to_gpu(x)
+ #x = cuda.to_gpu(x)

y = lnn.calc(Variable(x), test=True)
- output = y.data.get()
+ output = y.data#.get()

self.save_as_img( output[0], self.root + "line/"+id_str+".jpg" )


def colorize_s( self, id_str, blur=0, s_size=128):
- cuda.get_device(self.gpu).use()
+ #cuda.get_device(self.gpu).use()

dataset = ImageAndRefDataset([id_str+".png"],self.root+"line/",self.root+"ref/" )
test_in_s, test_in = dataset.get_example(0, minimize=True, blur=blur, s_size=s_size)
@@ -81,30 +81,30 @@ class Paintor:

x[0,:] = test_in_s

- x = cuda.to_gpu(x)
+ #x = cuda.to_gpu(x)
y = self.cnn_128.calc(Variable(x), test=True )
- output = y.data.get()
+ output = y.data#.get()

self.save_as_img( output[0], self.outdir_min + id_str + ".png" )

def colorize_l( self, id_str ):
- cuda.get_device(self.gpu).use()
+ #cuda.get_device(self.gpu).use()

dataset = ImageAndRefDataset([id_str+".png"],self.root+"line/",self.root+"out_min/" )
test_in, test_in_ = dataset.get_example(0,minimize=False)
x = np.zeros((1, 4, test_in.shape[1], test_in.shape[2] )).astype('f')
x[0,:] = test_in

- x = cuda.to_gpu(x)
+ #x = cuda.to_gpu(x)
y = self.cnn.calc(Variable(x), test=True )

- output = y.data.get()
+ output = y.data#.get()

self.save_as_img( output[0], self.outdir + id_str + ".jpg" )


def colorize( self, id_str, blur=0, s_size=128):
- cuda.get_device(self.gpu).use()
+ #cuda.get_device(self.gpu).use()

dataset = ImageAndRefDataset([id_str+".png"],self.root+"line/",self.root+"ref/" )
test_in_s, test_in = dataset.get_example(0,minimize=True)
@@ -117,20 +117,21 @@ class Paintor:
x[0,:] = line
input_bat[0,0,:] = line2

- x = cuda.to_gpu(x)
+ #x = cuda.to_gpu(x)
y = self.cnn_128.calc(Variable(x), test=True )

- output = y.data.get()
+ output = y.data#.get()

self.save_as_img( output[0], self.outdir_min + id_str +"_"+ str(0) + ".png" )

for ch in range(3):
input_bat[0,1+ch,:] = cv2.resize(output[0,ch,:], (test_in.shape[2], test_in.shape[1]), interpolation = cv2.INTER_CUBIC)

- x = cuda.to_gpu(input_bat)
+ #x = cuda.to_gpu(input_bat)
+ x = input_bat
y = self.cnn.calc(Variable(x), test=True )

- output = y.data.get()
+ output = y.data#.get()

self.save_as_img( output[0], self.outdir + id_str +"_"+ str(0) + ".jpg" )

diff --git a/cgi-bin/paint_x2_unet/img2imgDataset.py b/cgi-bin/paint_x2_unet/img2imgDataset.py
index d26f580..e98238e 100755
--- a/cgi-bin/paint_x2_unet/img2imgDataset.py
+++ b/cgi-bin/paint_x2_unet/img2imgDataset.py
@@ -41,11 +41,11 @@ class ImageAndRefDataset(chainer.dataset.DatasetMixin):
if minimize :
if image1.shape[0] < image1.shape[1]:
s0 = s_size
- s1 = int( image1.shape[1]*(s_size/image1.shape[0]) )
+ s1 = int( image1.shape[1]*(1.0*s_size/image1.shape[0]) )
s1 = s1-s1%16
else:
s1 = s_size
- s0 = int( image1.shape[0]*(s_size/image1.shape[1]) )
+ s0 = int( image1.shape[0]*(1.0*s_size/image1.shape[1]) )
s0 = s0-s0%16

_image1 = image1.copy()


後は、python server.pyで動作します。ブラウザからhttp://localhost:8000/static/などでアクセスします。

推論側はGPU無効でも数十秒で返ってくるため、クライアントサイドでも動かせそうな印象です。ただ、学習済みデータが134.3MBもあるので、これをそのままクライアントに転送するのが難しそうですね。負荷自体は低いので、アプリ化などは容易そうです。

テクノロジーは元記事が詳しいです。

学習側は、画像をYUV変換して、Yをベースに線に変換しているようです。また、乱数で2値化したり、左右反転したり、ノイズを足したりして、データ量を増やしていますね。

ネットワークの構造はU-netでした。Batch Normalization重要そうです。評価関数は元画像との二乗誤差+アドバイサリーロスを10:1で混合。アドバイサリーロスについては、生成した画像が偽物であると推定させることで、競わせるようです。ソースコードではtrain.pyでCNNが生成した画像をy_fake、正解画像をy_realとして、アドバイサリーネットに入力し、見分けられない画像を生成させようとしています。

ユーザによる色の入力はどう実装しているか不思議だったのですが、線画に元データからランダムに色付きピクセルを追加したものを学習データとしているようです。

いきなり512x512にしないで、128x128を経由しているのは過学習対策なんですかね。ノウハウがありそうです。

Chainerの係数列はNumPyのnpzフォーマットのようです。ダンプしてみたい。

これだけ少ないコード量で、これほどのサービスが作れるのは驚きです。ただ、ソースコードにうまく学習させるためのノウハウが散りばめられており、一朝一夕ではないなという印象です。
Search
Profile

abars

アプリとWEBサービスを開発しています。最近はUnityとGAE/pyが主戦場。

ブラウザ向けMMOのメトセライズデストラクタ、イラストSNSのイラストブック、東証の適時開示情報を検索できるTDnetSearchを開発しています。

かつてエンターブレインのTECH Win誌でATULADOを連載しました。

サイト:ABARS
Twitter:abars
Github:abars

Twitter
TopHatenar
HotEntry
Counter

アクセス解析付きカウンター。あなたのBLOGにもどうですか?登録はこちらから。

TOP/ BLOG/ LECTURE/ ONLINE/ RUINA/ ADDON/ THREAD/ METHUSELAYZE/ IPHONE/ MET_IPHONE/ ENGLISH/ RANKING