エイバースの中の人

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

ディープラーニング

tf2onnxのTransposeの抑制

tf2onnxでONNXに書き出すと、大量のTransposeが生成されます。これは、TensorflowがNHWCなのに対して、ONNXがNCHWなため、並べ替えが発生するためです。この問題は、TransposeOptimizerを使用することで回避することができます。

通常のエクスポートコード
    graph1 = tf.Graph()
    with graph1.as_default():
        tf.import_graph_def(frozen_graph_def)
        onnx_graph = tf2onnx.tfonnx.process_tf_graph(graph1, input_names=input_names, output_names=output_names, opset=10)

        model_proto = onnx_graph.make_model("onnx")
        with open("output.onnx", "wb") as f:
            f.write(model_proto.SerializeToString())

before

TransposeOptimizerを使用
    graph1 = tf.Graph()
    with graph1.as_default():
        tf.import_graph_def(frozen_graph_def)
        onnx_graph = tf2onnx.tfonnx.process_tf_graph(graph1, input_names=input_names, output_names=output_names, opset=10)

        from tf2onnx.optimizer.transpose_optimizer import TransposeOptimizer
        optimizer = TransposeOptimizer()
        opt_model_proto = optimizer.optimize(onnx_graph)

        model_proto = onnx_graph.make_model("onnx")
        with open("output.onnx", "wb") as f:
            f.write(model_proto.SerializeToString())

after

オプティマイズすることで、推論速度は1.5倍程度高速化されます。

tf-onnx introducing many transpose operations
add transpose optimizer, and integrate it after tf graph conversion #108

ChainerのDeeplanv3の入出力解像度の調査

サンプル:examples/deeplab
モデル:chainercv/links/model/deeplab

学習に使用したデータセットによって入力解像度が異なる。入力解像度未満の画像は入力解像度まで引き上げられる。

cityscapes : 1025x2049
ada20k : 513x513
voc : 513x513

入力画像のレンジは-1.0〜1.0。

Deeplabv3のFeatureExtractorは下記の解像度を出力する。

voc : 21x129x129

21はカテゴリ数である。各画素はカテゴリ数分のスケールがかかっているため、/21を行う。

セグメンテーションを可視化する際にはバイリニアで元解像度まで拡大した後、しきい値処理する。

vis_semantic_segmentation.py

PytorchからONNXを書き出す際のワークアラウンド

1. RuntimeError: Only tuples, lists and Variables supported as JIT inputs, but got collections.OrderedDictが発生する

deeplabv3などでは以下のようにOrderedDictに値を代入して返していますが、ONNXエクスポータが扱うことができません。

result = OrderedDict()
x = features["out"]
x = self.classifier(x)
x = F.interpolate(x, size=input_shape, mode='bilinear')
result["out"] = x
return result

そのようなケースでは、下記のように書き換えます。

x = features["out"]
x = self.classifier(x)
x = F.interpolate(x, size=input_shape, mode='bilinear')
return x

RuntimeError: Only tuples, lists and Variables supported as JIT inputs, but got dict #13040

2. KeyError: 'upsample_bilinear2d'が発生する

ONNXのエクスポータにバイリニアが定義されていないため、エラーになります。

x = F.interpolate(x, size=input_shape, mode='bilinear')

nearestに置き換えるか、/usr/local/lib/python3.7/site-packages/torch/onnx/symbolic_opset10.pyに下記を追加します。

upsample_bilinear1d = _interpolate('upsample_bilinear1d', 3, "linear")
upsample_bilinear2d = _interpolate('upsample_bilinear2d', 4, "linear")
upsample_bilinear3d = _interpolate('upsample_bilinear3d', 5, "linear")

upsample_bilinear2d issue when exporting to onnx #22906

PytorchからONNXへの変換時にtrack_running_statsの未定義エラーが発生

PytorchからONNXへの変換時に、Pytorch0.3系で学習したモデルの場合、BatchNorm2dもしくはInstanceNorm2dにおいて
 object has no attribute ‘track_running_stats’
というエラーが発生します。

この問題を解決するには、BatchNorm2dもしくはInstanceNorm2dを探索し、track_running_statsに値を設定します。

def recursion_change_bn(module):
    if isinstance(module, torch.nn.BatchNorm2d) or isinstance(module, torch.nn.InstanceNorm2d):
        module.track_running_stats = 1
    else:
        for i, (name, module1) in enumerate(module._modules.items()):
            module1 = recursion_change_bn(module1)
    return module

for i, (name, module) in enumerate(model._modules.items()):
    module = recursion_change_bn(model)
model.eval()

上記対処をすると、ONNXへの書き出しに成功します。

x = Variable(torch.randn(1, 3, 64, 64))
torch.onnx.export(model, x, 'output.onnx', verbose=True)

参考:‘BatchNorm2d’ object has no attribute ‘track_running_stats’

TensorflowからONNXへのエクスポート

TensorflowからONNXにエクスポートするには、tf2onnxを使用します。

インストール
pip3 install tf2onnx

まず、tf.graph_util.convert_variables_to_constantsを実行することで、Tensorflowのgraphのvariableをconstantに変換しておきます。これをfrozenと呼びます。

ただし、convert_variables_to_constantsにはBatchNormalizationでmoving_varianceのvariableをfrozenできない問題があるため、事前に問題の起こるノードを修正しておきます。(Unable to import frozen graph with batchnorm

# fix batch norm nodes
gd = sess.graph.as_graph_def()
for node in gd.node:
    if node.op == 'RefSwitch':
        node.op = 'Switch'
        for index in range(len(node.input)):
            if 'moving_' in node.input[index]:
                node.input[index] = node.input[index] + '/read'
   elif node.op == 'AssignSub':
        node.op = 'Sub'
        if 'use_locking' in node.attr: del node.attr['use_locking']

# Freeze the graph
output_node_names=["upscale/mul","hourglass/hg_2/after/hmap/conv/BiasAdd","radius/out/fc/BiasAdd"]
frozen_graph_def = tf.graph_util.convert_variables_to_constants(
    sess,
    gd,
    output_node_names
)

次に、tf2onnx.tfonnx.process_tf_graphを実行することで、ONNXに変換します。引数には入力と出力に対応するノードを指定します。

# Convert to onnx
input_names=["import/eye:0"]
output_names=["import/upscale/mul:0","import/hourglass/hg_2/after/hmap/conv/BiasAdd:0","import/radius/out/fc/BiasAdd:0"]
graph1 = tf.Graph()
with graph1.as_default():
    tf.import_graph_def(frozen_graph_def)
    onnx_graph = tf2onnx.tfonnx.process_tf_graph(graph1, input_names=input_names, output_names=output_names)
    model_proto = onnx_graph.make_model("sample")
    with open("sample.onnx", "wb") as f:
        f.write(model_proto.SerializeToString())

出力されたONNXが正しいかどうかを、ONNXRuntimeを使用して検証します。

# Inference
import numpy
import onnxruntime as rt
onnx_sess = rt.InferenceSession("sample.onnx")
for node in onnx_sess.get_inputs():
    print(node.name)
    print(node.shape)
    print(node.type)
X = numpy.random.random((2, 36, 60, 1)).astype(numpy.float32)
pred_onnx = onnx_sess.run(None, {"import/eye:0":X})
print(pred_onnx)

なお、Placeholderがtf.contrib.layers.batch_normのis_trainingに接続されているなどすると、Ifを含むONNXが出力されるため、必要に応じてFalseなどの直値を設定しておきます。

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

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
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