エイバースの中の人

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

ディープラーニング

WindowsのPythonにdlibをインストール

WindowsのPythonにdlibをインストールする場合、Links for dlibからPythonのバージョンに対応したビルド済みバイナリのURLを取得します。

Python 3.5 + 64bitの場合、以下のコマンドでインストールすることができます。cp35がPythonのバージョン、amd64が64bitバイナリの指定です。

pip install https://files.pythonhosted.org/packages/38/18/92fc25855307bcf582a30034ae657fda205de4f29773323bb388e592f17c/dlib-19.4.0-cp35-cp35m-win_amd64.whl#sha256=67e7d86eedaef650788b058d9e086198ead583f5bb3041fd9a431ae50658e9f4

KerasでHDF5Matrixを使用して学習を高速化

小さな画像が大量にある場合、ImageDataGeneratorを使用するとディスクのランダムアクセスネックでGPU使用率が伸びず、学習速度が低下します。学習画像を事前にHDF5に書き込んでおくことで、ランダムアクセスを抑制し、高速化することができます。

まず、flow_from_directoryから取得できる学習用データからh5pyを使用してHDF5を作成します。以下の例ではtrain.h5を作成します。同様にvalidation.h5も作成します。

train_generator = train_datagen.flow_from_directory(
   INPUT_PATH+'/annotations/'+ANNOTATIONS+'/train',
   target_size=(IMAGE_SIZE, IMAGE_SIZE),
   batch_size=BATCH_SIZE,
   class_mode='categorical',
   shuffle=True
)

training_data_n = len(train_generator.filenames)
training_class_n=len(train_generator.class_indices)

import h5py
f = h5py.File("train.h5", 'w')
train_x = f.create_dataset('training_x', (training_data_n,IMAGE_SIZE,IMAGE_SIZE,3), dtype='f')
train_y = f.create_dataset('training_y', (training_data_n,training_class_n), dtype='f')

cnt=0
for x_batch, y_batch in train_generator:
  for i in range(BATCH_SIZE):
    train_x[cnt] = x_batch[i]
    train_y[cnt] = y_batch[i]
    cnt = cnt+1
    if cnt>=training_data_n:
        break
  if cnt>=training_data_n:
    break

f.close()

学習時はHDF5Matrixを使用します。

  from keras.utils.io_utils import HDF5Matrix
  x_train = HDF5Matrix("train.h5", 'training_x')
  y_train = HDF5Matrix("train.h5", 'training_y')
  x_validation = HDF5Matrix("validation.h5", 'validation_x')
  y_validation = HDF5Matrix("validation.h5", 'validation_y')
  fit = model.fit(
    epochs=EPOCS,
    x=x_train, 
    y=y_train,
    validation_data=(x_validation,y_validation),
    batch_size=BATCH_SIZE,
    shuffle='batch'
  )

VGGFace2を使用した性別推定モデル(3146003画像)では160GBのHDF5を生成し、学習時間を1EPOCで3時間から30分に削減でき、6倍高速化できました。評価環境はGeforceRTX2080、32GBメモリ、1TB SSDです。

floatではなく8bit unsigned intでhdf5ファイルに書き込むこともできるようですが、以下のコードで試してみたところ、学習がほとんど進まなくなってしまいました。

train_x = f.create_dataset('training_x', (training_data_n,IMAGE_SIZE,IMAGE_SIZE,3), dtype='uint8')
train_x[cnt] = x_batch[i].astype(np.uint8)

学習時のノーマライズは下記のように行いましたが、どこかに問題があるようです。

ds_norm = lambda x: x / 255.0
x_train = HDF5Matrix(HDF5_PATH, 'training_x', normalizer=ds_norm)

KerasでGPUのメモリ使用量を抑制する

kerasではデフォルトでGPUメモリを100%確保します。ディスクネックでGPU使用率が低く、複数の学習を同時に走らせたい場合は、モデル作成前に以下のようにすることで、GPUのメモリ使用率を抑制することができます。

import tensorflow as tf
import keras.backend as backend

config = tf.ConfigProto()
config.gpu_options.per_process_gpu_memory_fraction = 0.5
config.gpu_options.allow_growth = True
sess = tf.Session(config=config)
backend.set_session(sess)

keras2caffeでKeyErrorが起きる

Caffe 1.0.274でkeras2caffeを使った際、KeyErrorが発生します。

Traceback (most recent call last):
  File "convert_to_caffemodel.py", line 76, in 
    keras2caffe.convert(keras_model, PROTOTXT, WEIGHTS)
  File "keras2caffe\convert.py", line 76, in convert
    if layer_type=='InputLayer' or not hasattr(caffe_net, 'data'):
  File "Anaconda3\envs\py35\lib\site-packages\caffe\net_spec.py", line 180, in __getattr__
    return self.tops[name]
KeyError: 'data'

これは、hasattrがgetattrで実装されていますが、caffeがKeyErrorをAttributeErrorに置き換えていないために発生します。

How to make a class which has __getattr__ properly pickable?

class NetSpec(object):
    """A NetSpec contains a set of Tops (assigned directly as attributes).
    Calling NetSpec.to_proto generates a NetParameter containing all of the
    layers needed to produce all of the assigned Tops, using the assigned
    names."""

    def __init__(self):
        super(NetSpec, self).__setattr__('tops', OrderedDict())

    def __setattr__(self, name, value):
        self.tops[name] = value

    def __getattr__(self, name):
        return self.tops[name]

そこで、NetSpecでKeyErrorをAttributeErrorに変換します。

class NetSpec(object):
    """A NetSpec contains a set of Tops (assigned directly as attributes).
    Calling NetSpec.to_proto generates a NetParameter containing all of the
    layers needed to produce all of the assigned Tops, using the assigned
    names."""

    def __init__(self):
        super(NetSpec, self).__setattr__('tops', OrderedDict())

    def __setattr__(self, name, value):
        self.tops[name] = value

    def __getattr__(self, name):
        try:
            return self.tops[name]
        except KeyError:
            raise AttributeError(name)

これで正常にcaffemodelに変換することができます。

RTX2080でTensorflow-gpu

機械学習用にRTX2080を搭載したTridentXを購入したので、KerasとTensorflowをインストールしました。



RTX2080の性能を発揮するにはCUDA10が必要ですが、公式のtensorflow-gpuはPython3.5 + CUDA9でビルドされています。そのため、CUDA10を使用してビルドされたtensorflowを別途、入手する必要があります。

ビルド済みバイナリは以下に公開されています。

https://github.com/fo40225/tensorflow-windows-wheel

Anaconda(Python3.7)で以下のようにインストールします。

pip install https://github.com/fo40225/tensorflow-windows-wheel/raw/master/1.12.0/py37/GPU/cuda100cudnn73sse2/tensorflow_gpu-1.12.0-cp37-cp37m-win_amd64.whl


CUDAのランタイムとcuDNNは以下からインストールできます。

CUDA Toolkit 10.0
cuDNN for CUDA10

また、最新のKeras 2.2.4で185万枚の画像からトレーニングしようとした場合、1050tiで1EPOCHで45分だったのが、2080の1EPOCHで50時間必要になってしまい、CUDA10の問題かと思ったのですが、pip install Keras==2.1.4で2.1.4にダウングレードしたら2080で1EPOCHが15分になりました。データがメモリに乗る2週目からは7分程度で1EPOCHが回ります。トータルで6倍程度の高速化のイメージです。

VGGFace2に年齢ラベルを付加

商用利用可能(The VGGFace2 dataset is available to download for commercial/research purposes under a Creative Commons Attribution-ShareAlike 4.0 International License. )な顔画像の大規模データセットとして、VGGFace2がありますが、メタデータとして年齢が含まれていないという問題があります。

そこで、現在、もっとも高精度と思われるIMDBの学習済みモデルを使用して、年齢ラベルを付与しました。付与されたラベルの分布は以下となります。若年層が含まれていないため、APPA-REAL (real and apparent age)あたりと合わせて使用すると良さそうです。

estimated_distribution


メタデータ:identity_meta_with_estimated_age.csv
リポジトリ:VGGFace2AgeLabel

GlobalAveragePoolingを使用してVGG16のモデルサイズを小さくする

物体識別を行う際、VGG16による転移学習を行いますが、生成されるモデルのサイズが大きすぎるという問題があります。VGG16のモデルサイズが大きくなる原因は、全結合層です。

以下はVGG16のSummaryです。最終段で、25088 * 4096の内積が発生しており、重みで392MB近く消費します。モデルサイズは528MBになります。

N_CATEGORIES = 64
IMAGE_SIZE = 224
input_tensor = Input(shape=(IMAGE_SIZE, IMAGE_SIZE, 3))
original_model = VGG16(weights='imagenet', include_top=True,input_tensor=input_tensor)
original_model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 224, 224, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 224, 224, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 224, 224, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 112, 112, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 112, 112, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 112, 112, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 56, 56, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 56, 56, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 28, 28, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 28, 28, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 14, 14, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 7, 7, 512)         0         
_________________________________________________________________
flatten (Flatten)            (None, 25088)             0         
_________________________________________________________________
fc1 (Dense)                  (None, 4096)              102764544 
_________________________________________________________________
fc2 (Dense)                  (None, 4096)              16781312  
_________________________________________________________________
predictions (Dense)          (None, 1000)              4097000   
=================================================================
Total params: 138,357,544
Trainable params: 138,357,544
Non-trainable params: 0
_________________________________________________________________

VGG16を使用して、転移学習を行う場合、全結合層を置き換えるのが一般的です。64カテゴリで学習を行った場合、モデルサイズは154MBになります。

  N_CATEGORIES = 64
  IMAGE_SIZE = 224
  input_tensor = Input(shape=(IMAGE_SIZE, IMAGE_SIZE, 3))
  base_model = VGG16(weights='imagenet', include_top=False,input_tensor=input_tensor)
  x = base_model.output
  x = Flatten()(x)
  x = Dense(1024, activation='relu')(x)
  predictions = Dense(N_CATEGORIES, activation='softmax')(x)
  model = Model(inputs=base_model.input, outputs=predictions)
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 224, 224, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 224, 224, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 224, 224, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 112, 112, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 112, 112, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 112, 112, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 56, 56, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 56, 56, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 28, 28, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 28, 28, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 14, 14, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 7, 7, 512)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 25088)             0         
_________________________________________________________________
dense_1 (Dense)              (None, 1024)              25691136  
_________________________________________________________________
dense_2 (Dense)              (None, 64)                65600     
=================================================================
Total params: 40,471,424
Trainable params: 40,471,424
Non-trainable params: 0
_________________________________________________________________

GlobalAveragePoolingは2014年に提案されました。GlobalAveragePoolingを使用することで、全結合層を省略して、モデルサイズを小さくすることができます。GlobalAveragePoolingは特徴マップをチャンネルごとに平均化するレイヤーです。1x1の畳み込みで認識カテゴリ数分の特徴マップに集約したあと、平均化しsoftmaxをかけることで、全結合層を代用することができます。モデルサイズは56MBになります。

class_activation_mapping
(出典:Global Average Pooling Layers for Object Localization

  N_CATEGORIES = 64
  IMAGE_SIZE = 224
  input_tensor = Input(shape=(IMAGE_SIZE, IMAGE_SIZE, 3))
  base_model = VGG16(weights='imagenet', include_top=False,input_tensor=input_tensor)
  x = base_model.output
  x = Convolution2D(N_CATEGORIES, (1, 1), padding='valid', name='conv10')(x)
  x = Activation('relu', name='relu_conv10')(x)
  x = GlobalAveragePooling2D()(x)
  predictions = Activation('softmax', name='loss')(x)
  model = Model(inputs=base_model.input, outputs=predictions)
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 224, 224, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 224, 224, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 224, 224, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 112, 112, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 112, 112, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 112, 112, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 56, 56, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 56, 56, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 28, 28, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 28, 28, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 14, 14, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 7, 7, 512)         0         
_________________________________________________________________
conv10 (Conv2D)              (None, 7, 7, 64)          32832     
_________________________________________________________________
relu_conv10 (Activation)     (None, 7, 7, 64)          0         
_________________________________________________________________
global_average_pooling2d_1 ( (None, 64)                0         
_________________________________________________________________
loss (Activation)            (None, 64)                0         
=================================================================
Total params: 14,747,520
Trainable params: 14,747,520
Non-trainable params: 0
_________________________________________________________________

GlobalAveragePoolingは特徴マップを平均化する機能しかないため、GlobalAveragePoolingへの入力は、自動的にヒートマップが学習されることになります。そのため、画像のどの部分を見てそう判断したかを特徴マップを見て把握できるという特徴があります。この特徴から、内積に比べて過学習も防止すると言われています。

dog_localization
(出典:Global Average Pooling Layers for Object Localization

さらにモデルサイズを小さくしたい場合は、SqueezeNetを使用することになります。畳み込みをチャンネル内とチャンネル間に分解して行うことで、畳込み部分の重みを削減します。モデルサイズは216KBになります。

IMAGE_SIZE=227
import sys
sys.path.append('./keras-squeezenet-master')
from keras_squeezenet import SqueezeNet
input_tensor = Input(shape=(IMAGE_SIZE, IMAGE_SIZE, 3))
base_model = SqueezeNet(weights="imagenet", include_top=False, input_tensor=input_tensor)
x = base_model.output
x = Dropout(0.5, name='drop9')(x)
x = Convolution2D(N_CATEGORIES, (1, 1), padding='valid', name='conv10')(x)
x = Activation('relu', name='relu_conv10')(x)
x = GlobalAveragePooling2D()(x)
predictions = Activation('softmax', name='loss')(x)
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_2 (InputLayer)            (None, 227, 227, 3)  0                                            
__________________________________________________________________________________________________
conv1 (Conv2D)                  (None, 113, 113, 64) 1792        input_2[0][0]                    
__________________________________________________________________________________________________
relu_conv1 (Activation)         (None, 113, 113, 64) 0           conv1[0][0]                      
__________________________________________________________________________________________________
pool1 (MaxPooling2D)            (None, 56, 56, 64)   0           relu_conv1[0][0]                 
__________________________________________________________________________________________________
fire2/squeeze1x1 (Conv2D)       (None, 56, 56, 16)   1040        pool1[0][0]                      
__________________________________________________________________________________________________
fire2/relu_squeeze1x1 (Activati (None, 56, 56, 16)   0           fire2/squeeze1x1[0][0]           
__________________________________________________________________________________________________
fire2/expand1x1 (Conv2D)        (None, 56, 56, 64)   1088        fire2/relu_squeeze1x1[0][0]      
__________________________________________________________________________________________________
fire2/expand3x3 (Conv2D)        (None, 56, 56, 64)   9280        fire2/relu_squeeze1x1[0][0]      
__________________________________________________________________________________________________
fire2/relu_expand1x1 (Activatio (None, 56, 56, 64)   0           fire2/expand1x1[0][0]            
__________________________________________________________________________________________________
fire2/relu_expand3x3 (Activatio (None, 56, 56, 64)   0           fire2/expand3x3[0][0]            
__________________________________________________________________________________________________
fire2/concat (Concatenate)      (None, 56, 56, 128)  0           fire2/relu_expand1x1[0][0]       
                                                                 fire2/relu_expand3x3[0][0]       
...
__________________________________________________________________________________________________
fire9/squeeze1x1 (Conv2D)       (None, 13, 13, 64)   32832       fire8/concat[0][0]               
__________________________________________________________________________________________________
fire9/relu_squeeze1x1 (Activati (None, 13, 13, 64)   0           fire9/squeeze1x1[0][0]           
__________________________________________________________________________________________________
fire9/expand1x1 (Conv2D)        (None, 13, 13, 256)  16640       fire9/relu_squeeze1x1[0][0]      
__________________________________________________________________________________________________
fire9/expand3x3 (Conv2D)        (None, 13, 13, 256)  147712      fire9/relu_squeeze1x1[0][0]      
__________________________________________________________________________________________________
fire9/relu_expand1x1 (Activatio (None, 13, 13, 256)  0           fire9/expand1x1[0][0]            
__________________________________________________________________________________________________
fire9/relu_expand3x3 (Activatio (None, 13, 13, 256)  0           fire9/expand3x3[0][0]            
__________________________________________________________________________________________________
fire9/concat (Concatenate)      (None, 13, 13, 512)  0           fire9/relu_expand1x1[0][0]       
                                                                 fire9/relu_expand3x3[0][0]       
__________________________________________________________________________________________________
drop9 (Dropout)                 (None, 13, 13, 512)  0           fire9/concat[0][0]               
__________________________________________________________________________________________________
conv10 (Conv2D)                 (None, 13, 13, 64)   32832       drop9[0][0]                      
__________________________________________________________________________________________________
relu_conv10 (Activation)        (None, 13, 13, 64)   0           conv10[0][0]                     
__________________________________________________________________________________________________
global_average_pooling2d_2 (Glo (None, 64)           0           relu_conv10[0][0]                
__________________________________________________________________________________________________
loss (Activation)               (None, 64)           0           global_average_pooling2d_2[0][0] 
==================================================================================================
Total params: 755,328
Trainable params: 755,328
Non-trainable params: 0
__________________________________________________________________________________________________

ETLデータセットによる手書き漢字認識の学習

ETLデータセットは産総研が公開している手書き文字認識のデータセットです。ETL8Gを使用して、9561画像から957カテゴリの分類を行います。

ETL8Gはバイナリデータなので、画像を取り出すため、ETL文字データベース (etlcdb)を画像に変換するを参考に抽出しました。

etl

IMAGE_SIZE=48として、最初は以下のネットワークで学習しました。

model.add(Conv2D(32, (3, 3), input_shape=(IMAGE_SIZE,IMAGE_SIZE,1)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(64,(3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Flatten())

model.add(Dense(1024))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Dense(957))

model.add(Activation('softmax'))

全く収束しません。

etl_3x3

試行錯誤した結果、Conv2Dを5x5にしたら収束しました。

model.add(Conv2D(32, (5, 5), input_shape=(IMAGE_SIZE,IMAGE_SIZE,1)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(64,(5, 5)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Flatten())

model.add(Dense(1024))
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Dense(957))

model.add(Activation('softmax'))

etl_5x5

TensorFlowでひらがな・漢字文字認識でも5x5の畳み込みを使用しているようです。3x3だと局所特徴量すぎるのかと思っていたのですが、BatchNormalizationを入れたら3x3でも収束するようです。

model.add(Conv2D(32, (3, 3), input_shape=(IMAGE_SIZE,IMAGE_SIZE,1)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Conv2D(64,(3, 3)))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))

model.add(Flatten())

model.add(Dense(1024))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Dropout(0.2))
model.add(Dense(num_classes))

model.add(Activation('softmax'))

etl_all_3x3_batch

BatchNormalization優秀ですね。

Colaboratoryを使用して無料でTPUを使用した学習を行う

Googleが提供するColaboratoryを使用すると、無料でTPUを使用した学習を行うことができます。

colaboratory

ColaboratoryではKerasがデフォルトでインストールされているため、Pythonのコードをコピペするだけで学習が動作します。

学習にかけるデータセットは接続のたびに初期化されるため、GoogleDriveに配置し、以下のコマンドでマウントします。

from google.colab import drive
drive.mount('/content/drive')

マウントしたフォルダから展開してデータを準備します。

!cp "./drive/My Drive/colaboratory/extract.zip" ./extract.zip
!unzip extract.zip


連続使用は最大12時間、アイドル状態が90分続くと停止という制約はありますが、とても簡単に学習環境を増設できて便利です。

高精度なMNISTの学習と認識

手書き数字認識を行う際、MNISTを使用しますが、現実のデータはデータセットほど綺麗に正規化されていないためうまく認識できないことがあります。

Kerasの標準のサンプルでは、精度は98.40%ですが、ImageDataGeneratorを使用していないため、一部がクロップで欠けているような画像ではうまく認識できなくなります。

mnist_mlp.py

対して、以下のサンプルでは、精度は99.55%になります。ImageDataGeneratorを使用しており、width_shift_rangeやrotation_rangeを使用しているため、汎化性能が高くなっています。また、Pseudo Labelingにより学習データを増強しています。こちらはコンペとしての性能を上げるためであり、汎化性能への寄与は少ないものと考えられます。

MNIST_keras_CNN-99.55%25.ipynb

こちらだと、概ね、多くの手書き文字を正確に認識します。現実のアプリケーションでは、数値的な精度差以上に性能差が出ることがあるのは意識しておく必要がありそうです。

ちなみに、CapsNetは99.66%のようです。

CapsNet-Keras

性別推定モデルにおけるランダムイレージング

SqueezeNetにおける性別推定モデルにおいて、学習時にランダムイレージングを使用して効果を計測しました。ランダムイレージングは、ランダムに一部の領域を塗りつぶすことで、汎化性能を上げる手法です。

使用前
agegender_gender_squeezenet_


使用後
agegender_gender_squeezenet_augumented


ランダムイレージングを使用すると、accuracy for trainingが下がり、accuracy for validationが上がります。ただ、使用前、使用後でaccuracy for validationに大差はないため、このモデルではあまり効果がないようです。

YoloKerasFaceDetection

性別推定モデルのSqueezeNetとMobileNetの比較

IMDB wikiデータセットを使用して性別推定モデルを構築した場合の、lossとaccuracyをSqueezeNetとMobileNetで計測しました。

SqueezeNet

agegender_gender_squeezenet_


MobileNet

agegender_gender_mobilenet_


MobileNetの方がTrainingのaccuracyは高くなるのですが、Validationのaccuracyが伸びていかないため、SqueezeNetでも十分そうです。

SqueezeNetは10.2MB、MobileNetは34.4MBの重みになります。

YoloKerasFaceDetection

Darknetにおける転移学習の効果

Darknetを使用した学習では、引数に学習済み係数を与えて転移学習を行うことができます。Darknetで使用するYOLOv2の前段はVGGライクな特徴検出器となっており、Imagenetなどの大規模データで学習したフィルタ係数を流用することで、効率的に学習を行うことができると言われています。

今回はFDDBを使用した顔検出において、転移学習の効果を確認します。モデルはYOLOv2-tinyを使用します。FDDBのデータセットは2845枚で、3/4をトレーニング、1/4をバリデーションとして用います。

まずは、転移学習をしない場合の学習の推移です。

yolov2-tiny-train-one-class_32600


次に、転移学習をする場合の学習の推移です。

yolov2-tiny-train_431800


転移学習をしない場合は、最初のlossが大きいですが、転移学習をする場合は最初のlossが低い地点からスタートすることがわかります。また、lossの低下速度が速くなっています。

ただ、転移学習をしない場合でも、Geforce1050tiで一晩で学習が終わっており、また、最終的にできあがるモデルの精度もあまり違わないため、Darknetにおいては転移学習をしなくてもよさそうという結論です。

ソースコード:YoloKerasFaceDetection

YAD2KでYolov2の重みをKerasに変換する

YAD2Kを使用することで、Darknetで学習したYolov2の重みをKerasに変換することができます。

変換を行うには以下のコマンドを使用します。
python3 yad2k.py yolov2-tiny.cfg yolov2-tiny.weights yolov2_tiny.h5

注意点として、64bitでビルドしたDarknetを使用している場合、yad2k.pyのweights_file.read(16)をweights_file.read(20)に書き換える必要があります。
    weights_header = np.ndarray(
        shape=(4, ), dtype='int32', buffer=weights_file.read(20))
(参考:Tiny Yolo conversion fails to detect any objects

以下のコマンドでテストを行います。
python3 test_yolo.py yolov2_tiny.h5

MMdnnを使用してモデルをcaffeからkerasに変換する

MMdnnはマイクロソフトが開発しているモデルコンバータです。モデルデータをIRに変換することで、モデルの相互変換を可能にしています。

MMdnn

mmdnnをインストールし、caffemodelとprototxtからhdf5に変換するには以下のようにします。inputShapeを指定しない場合、None例外が発生します。

pip2 install mmdnn
mmconvert --srcFramework caffe --inputWeight face.caffemodel --inputNetwork face.prototxt --dstFramework keras --outputModel face.hdf5 --inputShape 1,3,448,448

CoreMLでfloat配列から推論する

Caffeと同様に、CoreMLでもMLFeatureProviderという形式でデータを与え、predictionFromFeaturesを呼び出すことで、float配列から推論を行うことができます。

任意のfloat配列からMLFeatureProviderを取得するには、MLMultiArrayにfloat配列を書き込み、MLMultiArrayを持つNSMutableDictionaryを作成し、NSMutableDictionaryからMLDictionaryFeatureProviderを生成することになります。このとき、ディクショナリのキーはmlmodelの入力層の名前になります。

NSArray *shape = @[@1, [NSNumber numberWithInt:src_w], [NSNumber numberWithInt:src_z], [NSNumber numberWithInt:src_y], [NSNumber numberWithInt:src_x]];
MLMultiArray* data = [[MLMultiArray alloc] initWithShape:shape dataType:MLMultiArrayDataTypeFloat32 error:&error];
int shape_size=1*src_w*src_z*src_y*src_x*sizeof(float);
memcpy( data.dataPointer, src, shape_size );
NSMutableDictionary *inputDict = [[NSMutableDictionary alloc] init];
inputDict[key] = data;
MLDictionaryFeatureProvider *inFeatures = [[MLDictionaryFeatureProvider alloc] initWithDictionary:inputDict error:&error];

mlmodelに対してpredictionFromFeaturesを呼び出すことで推論します。

id outFeatures = [model predictionFromFeatures:static_cast(inFeatures) error:&error];

処理結果はMLFeatureProviderで出力されます。ディクショナリのキーは出力層の名前になります。

NSSet *keys = [outFeatures featureNames];
MLFeatureValue *value = [outFeatures featureValueForName:key];
MLMultiArray* res=value.multiArrayValue;
double* fsrc = (double*)res.dataPointer;
float* fdst = (float*)dest;
for( int i=0; i<res.count; i++ ){
	fdst[i] = (float)fsrc[i];
}


predictionFromFeaturesとMLFeatureProviderの使い方はcoremltools/coremlpython/のソースコードを参照してください。

CoreMLの出力はdoubleですが、内部的にはMetalPerformanceShaderでhalf floatで実行されるそうです。How can I use half floats with CoreML neural nets?

CaffeModelをmlmodelに変換する

CaffeModelからCoreMLで使えるmlmodelに変換するにはcoremltoolsを使います。

pip install coremltools


PIPが古い場合はpipをupgradeします。

pip install --upgrade pip


以下のスクリプトをpython3で実行することでcaffemodelをmlmodelに変換します。

import coremltools
coremlmodel = coremltools.converters.caffe.convert(
        ("lenet.caffemodel", "lenet.prototxt"))
coremlmodel.save("lenet.mlmodel")


python3 coreml.py 

================= Starting Conversion from Caffe to CoreML ======================
Layer 0: Type: 'Input', Name: 'data'. Output(s): 'data'.
Ignoring batch size and retaining only the trailing 3 dimensions for conversion. 
Layer 1: Type: 'Convolution', Name: 'conv1'. Input(s): 'data'. Output(s): 'conv1'.
Layer 2: Type: 'Pooling', Name: 'pool1'. Input(s): 'conv1'. Output(s): 'pool1'.
Layer 3: Type: 'Convolution', Name: 'conv2'. Input(s): 'pool1'. Output(s): 'conv2'.
Layer 4: Type: 'Pooling', Name: 'pool2'. Input(s): 'conv2'. Output(s): 'pool2'.
Layer 5: Type: 'InnerProduct', Name: 'ip1'. Input(s): 'pool2'. Output(s): 'ip1'.
Layer 6: Type: 'ReLU', Name: 'relu1'. Input(s): 'ip1'. Output(s): 'ip1'.
Layer 7: Type: 'InnerProduct', Name: 'ip2'. Input(s): 'ip1'. Output(s): 'ip2'.
Layer 8: Type: 'Softmax', Name: 'prob'. Input(s): 'ip2'. Output(s): 'prob'.

================= Summary of the conversion: ===================================
Detected input(s) and shape(s) (ignoring batch size):
'data' : 1, 28, 28

Network Input name(s): 'data'.
Network Output name(s): 'prob'.

Xcodeに取り込むと以下のように見えます。

xcode_lenet


アプリに組み込む場合、Xcodeでは拡張子がmlmodelと表示されますが、アプリからはmlmodelcとしてアクセスする必要があります。mlmodelcにはXcodeが自動的に変換します。mlmodelcには手動で変換することもできます。

/Applications/Xcode.app/Contents/Developer/usr/bin/coremlc compile in.mlmodel out.mlmodelc

Compile .mlmodel to .mlmodelc for Xamarin.iOS?

VAEによる正常品のみからの不良品検出

製造工程の不良品を検出する場合、製造工程の不良率は低いため、不良画像をなかなか集めることができません。そのため、正常品のみから不良個所を判定する手法が求められており、AutoEncoderを使用する方法が提案されています。

AutoEncoderでは、入力画像の特徴を表すベクトルを学習します。Encoderでは入力画像を低次元の潜在変数zに写像します。Decoderでは潜在変数zから元解像度まで復元します。学習では、X->Encoder->z->Decoder->xと計算し、Xとxの誤差を最小化します。

AutoEncoderは正常品から学習しているため、不良品の特徴は捉えることができません。そのため、AutoEncoderの出力と、不良品の画像の差分を取ることで、不良個所を判別することができます。

AutoEncoderのzを直接学習せず、正規分布のパラメータを学習するようにしたのがVAEです。VAEを使用してMNISTで不良品検出をするには、以下の記事が参考になります。

Variational Autoencoderを使った画像の異常検知 前編

Colaboratoryに上記サイトのコードをコピペして実行してみます。実行結果が以下の画像です。画像上(old)がVAE、画像下(new)がVAEの損失関数をMVAEのみにしたバージョンです。1の画像を正常画像、9の画像を異常画像として、濃い青の部分が異常個所になります。このように、正常画像のみから異常画像を検出できることがわかります。

2dim


潜在変数zを2次元から4次元に変更して実行してみます。こちらの方が精度が高いように思われます。

4dim


8次元版。次数を上げすぎるとすべての特徴を表現できてしまい、異常検知が困難になります。

8dim


学習自体は正常画像のみで実行できますが、潜在変数をどれくらいの次元数に設定するかの判断のために、いくつかは不良画像が必要かと思われます。

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

個人識別のために顔認識を行う場合、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万枚の顔画像を使用しており、データセットとラベルもダウンロード可能です。
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