エイバースの中の人

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

TensorFlow Graphicsによる3DメッシュへのCNNの適用

TensorFlow Graphicsは3Dメッシュに対してCNNを適用できるようにするフレームワークです。従来、TensorFlowで3Dメッシュを扱う場合は、ボクセルに変換して扱っていました。TensorFlow Graphicsでは、新たに、微分可能レンダラと、メッシュに対するGraph Convolutionを実装することで、3Dメッシュをそのまま扱えるようにしています。mesh_segmentationによる3Dメッシュの各ポリゴンへのラベリングなど、今までできなかった処理が可能になります。

従来のコンボリューションは2D画像に対して適用していました。

cat_image_convolutions

グラフコンボリューションでは、3Dメッシュの頂点に連結している頂点に対して重みをかけて畳み込みを行います。畳み込みを行うと、チャンネル方向に3Dメッシュが増殖していくイメージになります。

cat_mesh_convolutions

グラフコンボリューションによって、3Dメッシュの部位ラベルの付与が可能になります。

mesh_segmentation

頂点と頂点の接続関係はマトリックスで定義します。頂点iと頂点jが接続されている場合、A[i,j]に値が入ります。性質上、対角には必ず値が入ります。

A[i, j] = w[i,j] if vertex i and vertex j share an edge,
A[i, i] = w[i,i] for each vertex i,
A[i, j] = 0 otherwise.
where, w[i, j] = 1/(degree(vertex i)), and sum(j)(w[i,j]) = 1

モデルデータは、頂点列とポリゴンを構成する頂点ID、頂点の接続情報から構成され、tfrecordsに格納されます。fbxなどからの変換ツールはまだリリースされていません。(3D format to .tfrecords

それでは、MacにTensorFlow Graphicsをインストールしてみます。

TensorFlow Graphicsをインストールします。

pip3 install tensorflow-graphics

Jupyter Notebookをインストールします。

pip3 install jupyter

Colabtoolsをインストールします。

git clone https://github.com/googlecolab/colabtools.git
cd colabtools
python setup.py install

Colabtoolsをインストールしていない場合は、threejsを使ったモデルビューワで下記のエラーが発生します。

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
 in 
     19     'vertex_colors': mesh_viewer.SEGMENTATION_COLORMAP[test_labels[0, ...]],
     20 }
---> 21 input_viewer = mesh_viewer.Viewer(input_mesh_data)

/usr/local/lib/python3.7/site-packages/tensorflow_graphics/notebooks/mesh_viewer.py in __init__(self, source_mesh_data)
     56 
     57   def __init__(self, source_mesh_data):
---> 58     context = threejs_visualization.build_context()
     59     self.context = context
     60     light1 = context.THREE.PointLight.new_object(0x808080)

/usr/local/lib/python3.7/site-packages/tensorflow_graphics/notebooks/threejs_visualization.py in build_context()
     72   """Builds a javascript context."""
     73   threejs_url = 'https://www.gstatic.com/external_hosted/threejs-r98/'
---> 74   _publish.javascript(url=threejs_url + 'three.min.js')
     75   _publish.javascript(url=threejs_url + 'examples/js/controls/OrbitControls.js')
     76   return _js_builder.Js(mode=_js_builder.PERSISTENT)

NameError: name '_publish' is not defined

サンプルをcloneします。

git clone https://github.com/tensorflow/graphics.git

サンプルを実行します。

cd tensorflow_graphics
cd notebooks
jupyer notebook

notebooks/mesh_segmentation_demo.ipynbがmesh_segmentationの推論サンプルです。まだ学習側のコードは公開されていませんが、ディープラーニングの応用例が3Dにまで広がっていく未来が楽しみです。

notebooks/spherical harmonics optimization.ipynbが球面調和ライティングの環境マップを計算するサンプルです。TensorFlow Graphicsに含まれる微分可能レンダラーを使用することで、与えられた画像から、環境マップテクスチャを推定します。

学習の推移です。

spherical_harmonics2

学習結果のテクスチャです。

spherical_harmonics3

TensorFlow Graphicsの概要動画です。

TensorFlow+Kerasのresize_bilinearのUpSampling2Dへの置き換え

TensorFlow+Kerasでヒートマップ系のモデルを使用する場合、レイヤーの途中で画像サイズを拡大するためにlambdaを使用したBilinearフィルタが含まれるケースがあります。lambdaを扱えないハードウェアで推論する場合、この処理を規定のレイヤーに置き換える必要があります。

up2 = Lambda(
     lambda x: tf.image.resize_bilinear(
        x[0],
         x[1].shape.as_list()[1:3], 
         align_corners=True))([low3, up1])

単純に、元画像の1/2になっている場合は、UpSampling2Dに置き換えることができます。しかし、縮小時のテンソルが奇数の場合に、丸めが行われ、UpSamping2Dではテンソルサイズが合わない場合があります。たとえば、(5,5) -> (3,3)に縮小されたテンソルだと、(3,3) -> (6,6)に拡大されてしまいます。

この場合、4倍まで出力を拡大したあと、Croppingして、縮小することでサイズをあわせることができます。

from_shape=low3.shape.as_list()[1:3]
to_shape=up1.shape.as_list()[1:3]
crop_h=from_shape[0]*2-to_shape[0]
crop_w=from_shape[1]*2-to_shape[1]
common_upsample = UpSampling2D(size=(4,4), name=prefix_name+"_up_sampling2d")(low3)
common_multiple = Cropping2D((crop_h,crop_w), name=prefix_name+"_crop")(common_upsample)
up2 = AveragePooling2D(pool_size=(2,2), strides=(2,2), padding="same", name=prefix_name+"_downsampling")(common_multiple)

keras2caffeを使う場合は、UpSampling2Dの名前はup_sampling2dをつけておきます。

これで、多様なハードウェアで動作するようなネットワークを構築することができます。なお、出力はBilinearとは異なり、NearestNeighborとなるため、完全に一致はしません。

このように変更したあとに、規定の学習済みモデルを読み込むには、load_weightsでby_name=Trueを指定します。

net.load_weights("keras.h5",by_name=True)

numpy1.15に対応するtensorflowのバージョン

numpy 1.16ではcaffeが動作しないため、numpy 1.15を使いたい場合に対応するtensorflowのバージョンは1.12のようです。

TensorFlow 1.12
scikit-learn 0.20.0
XGBoost 0.81
numpy 1.15.4

ランタイム バージョン リスト

バージョンを指定したインストールは下記のコマンドで行います。

pip install numpy==1.15
pip install tensorflow==1.12

Tensorflow、numpy、cudnn、cuda、caffeのバージョンを揃えないといけないのがとっても大変ですね。

ディープラーニングのモデルを可視化できるnetron

netronはcaffemodelやtfliteファイルのネットワーク構造を可視化できるツールです。ブラウザで動作します。

例えば、darkflowで変換したyolo_tinyを可視化すると下記のようになります。leaky reluがmulとmaxにマッピングされていることが分かります。

yolo_tiny


mobilenetだと下記のようになります。tfliteによってモデルが最適化され、レイヤーが統合されていることが分かります。

mobilenet

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

MojaveでXcodeをインストールしようとすると空き容量が足りないと言われる

MojaveでXcodeをインストールしようとすると、40GBの空き容量があるのに、空き容量が足りないと言われる問題が発生しました。

Mojaveでは1時間に1つ、TimeMachineのローカルバックアップを作成します。「このMacについて」で表示される空き容量にはこのローカルバックアップはカウントされないため、空き容量があるものの、AppStoreアプリから見たときに空き容量が不足して見えるようです。

"Not enough free space" when trying to install XCode even though there's ~7x more free space than required

このバックアップは下記コマンドで削除できます。

for d in `tmutil listlocalsnapshots / | awk -F'.' '{print $4}'`; do sudo tmutil deletelocalsnapshots $d; done


ローカルバックアップを削除すると、無事、インストールできました。

UnityのWebCamTextureのパラメータ

UnityのWebCamTextureはデバイスによって回転された画像が取得されるため、画像認識を行う際、適切に回転させる必要があります。デバイスによって取得できる値をまとめました。使用した環境はUnity5.5です。

パラメータ
パラメータ名内容
videoRotationAngle右回りの角度 (度) を返します。多角形を回転させるためにその値を使用し、カメラのコンテンツを正しい向きで表示できます。
videoVerticallyMirroredテクスチャ画像が垂直に向きをかえているかどうか。

実機での取得値
デバイスvideoRotationAnglevideoVerticallyMirrored
MacPro20130False
iPadPro 2018 Landscape Incam180True
iPadPro 2018 Landscape Outcam0True
iPadPro 2018 Portrait Incam90True
iPadPro 2018 Portrait Outcam90True
GeminiPDA Landscape Incam180False
GeminiPDA Landscape Outcam--
GalaxyS8 Portrait Incam90False
GalaxyS8 Portrait Outcam270False

コードとしては、angle=90もしくはangle=270の場合は出力のxy軸を反転、angle=90もしくはangle=180の場合は出力の垂直反転でいけるようです。

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に変換することができます。

MacOS Mojaveで英語キーボードとして認識される

MacにChromeRemoteDesktopでログインしたあと、JISキーボードが英語キーボードとして認識される問題が起きました。この問題は再起動しても解決しませんでした。

Mojave以前の場合は「キーボードの種類を変更ボタン」がキーボード設定にあったのですが、Mojaveではなくなっているため、以下からキーボード設定アシスタントを実行する必要があります。

/system/Library/CoreService

キーボード設定アシスタントの指示に従うと、JISキーボードとして認識されました。

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
__________________________________________________________________________________________________

AppEngineからCloudStorageを使う

AppEngineのPython StandardEnvironmentからCloudStorageを使用します。DataStoreからCloudStorageに移行することで、保存データのバイト単価を1/10にすることができます。

CloudStorageを使用するには、appengine-gcs-clientをダウンロードして、アプリケーションのlibフォルダにcloudstorageフォルダをコピーします。

CloudStorageはバケットというフォルダのようなものに書き込みを行います。書き込み先のバケットをget_default_gcs_bucket_nameで取得することで、開発サーバでもCloudStorageは使用可能です。本番環境ではCloudStorageにあるアプリ名と同じ名称のデフォルトバケットに書き込まれます。

from google.appengine.api import app_identity

if 'lib' not in sys.path:
		sys.path[0:0] = ['lib']

import cloudstorage as gcs 

class ChunkManager(db.Model):
	def gcs_get_bucket_name(self):
		bucket_name = os.environ.get('BUCKET_NAME',	
					app_identity.get_default_gcs_bucket_name())
		return "/"+bucket_name+"/"

	def gcs_upload(self, filename, data):
		write_retry_params = gcs.RetryParams(backoff_factor=1.1)
		gcs_file = gcs.open(self.gcs_get_bucket_name()+filename,
						'w',
						content_type='application/octet-stream',
						retry_params=write_retry_params)
		gcs_file.write(data)
		gcs_file.close()

	def gcs_contents(self, filename):
		gcs_file = gcs.open(self.gcs_get_bucket_name()+filename)
		contents = gcs_file.read()
		gcs_file.close()
		return contents

	def gcs_download(self, filename, out):
		contents = self.gcs_contents(filename)
		out.write(contents)

	def gcs_delete(self, filename):
		try:
			gcs.delete(self.gcs_get_bucket_name()+filename)
		except gcs.NotFoundError:
			pass

参考:Google App EngineからGoogle Cloud Storageを利用する

AppEngineのDataStoreに新しいプロパティを追加する

既存のDataStoreのエンティティに新しいプロパティを追加した場合、保存済みのデータにはNoneが代入されます。Noneが代入されたプロパティはIndexに格納されていないため、Queryで参照することができません。

デザインパターンとしては、プロパティの追加に備えて、スキーマにversionというIntegerPropertyを追加しておきます。新しいプロパティを追加する際、versionが一定以下の場合をクエリして、新しいプロパティを初期化してputします。

しかし、そもそもversionを書き込んでいないスキーマの場合、まずはversionをスキーマに追加する必要があります。そのためには、全てのDataStoreのエンティティをループで回してversionを書き込んでputする必要があります。AppEngineのQueryは1000件以上取得できないため、このような場合はDataStoreのCursorを使用します。また、リクエストの60秒制約を解決するためにdeferred queueを使います。

以下、Python2.7 + AppEngine standard environment + dbの例です。

app.yamlにdeferredを追加します。

- url: /_ah/queue/deferred
  script: google.appengine.ext.deferred.deferred.application
  login: admin

builtins:
- deferred: on

cursorで回しながらversionを追加して上書きします。

from google.appengine.ext import deferred

def update_schema_loop(xbrl,num_fetched,num_updated,batch_size,tag):
	i=0

	to_put = []
	for one in xbrl:
		one=db.get(one.key())	#強い整合性を保証
		if not one.version:	#Noneチェック
			one.version=1	#新しいプロパティ
			to_put.append(one)
		i=i+1
		if i>=batch_size:
			break

	num_fetched += i

	if to_put:
		db.put(to_put)
		num_updated += len(to_put)

	return xbrl,num_fetched,num_updated,i

def update_schema_pdf(cursor=None, num_fetched=0, num_updated=0, batch_size=100):
	data_list = PdfDocument.all()
	data_list.with_cursor(start_cursor=cursor)
	data_list,num_fetched,num_updated,detected_cnt=update_schema_loop(data_list,num_fetched,num_updated,batch_size,"pdf")
	next_cursor = data_list.cursor()
	more=(detected_cnt!=0)
	if more:
		deferred.defer(
			update_schema_pdf, cursor=next_cursor, num_fetched=num_fetched, num_updated=num_updated)
	else:
		count = PdfDocument.all().count(limit=1000000)
		logging.info(
			'Complete update schema pdf with {} fetchs {} updates of {}!'.format(
				num_fetched,num_updated,count))

class UpdateSchemaHandler(webapp2.RequestHandler):
	"""Queues a task to start updating the model schema."""
	def get(self):
		deferred.defer(update_schema_pdf)
		self.response.write("""
		Schema update started. Check the console for task progress.
		""")

参考:Updating Your Model's Schema

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分続くと停止という制約はありますが、とても簡単に学習環境を増設できて便利です。

2018年に買ってよかったもの

iPad Pro 2018。2016モデルから買い替えました。今回もLTEモデル。圧倒的なGPU性能でPCよりも高速にCNNが動くという。今年はPCで生活していましたが、来年はDropBox+PowerPointでiPadでの軽量生活に挑戦します。といいつつ、Unityも動くSurfaceGOも気になるので、LTEモデルが一般販売されたら購入するかもしれません。



SurfaceBook2。Yoloの学習用と、WindowsMRでのBeatSaber用に買いました。キーボードの品質がとても高いです。



メガネに優しいDELLのWindowsMRです。BeatSaber用です。今年のベストゲームはBeatSaberで、音楽ゲームの未来を見ました。来年のOculusQuestでBeatSaberが発売されたら大ヒットしそうな予感です。



SurfaceBook2の充電器が重いので小型版です。出張用。少し熱くなりますが、問題なく使えています。



機械学習用のSamsungのSSD。青色がおしゃれ。



MacPro 2013に合わせて黒いキーボードを買いました。



複数台を切り替えられるマウスとキーボード。複数OSで開発する際に便利です。



ドアストッパー。シンプルです。



TimeCapsuleからの乗り換え。小型になりました。



GoogleWifiになりバックアップが取れなくなったので、USBメモリにバックアップを取るようにしました。超小型で256GBです。MacPro 2013の容量が256GBなのでちょうどよいです。



スタバの豆を格納するためのキャニスターです。ちょうど、1袋分、格納できるので便利です。



今年のベスト漫画です。働こうと思います。

高精度な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
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