CNN による画像分類で使われる前処理・テスト時処理まとめ

とりあえず ImageNet 系の論文で、目に入ったものから順々にまとめていきます。情報・ツッコミ歓迎。

前処理・Data Augmentation

Mean Subtraction

入力画像から平均を引く。[103.939, 116.779, 123.68] を各ピクセルから引く。VGG はこれ。

Per-pixel Mean Subtraction

入力画像から平均を引く。ピクセル・チャンネルごとに計算された平均を引く。即ち、224x224x3 個の値について個別に平均を計算し用いる。AlexNet 論文から使われており、ResNet もこれ

Random Crop

256x256 ピクセルに画像をリサイズし、そこから 224x224 のパッチをランダムに取り出す。AlexNet 論文で使われていた。ちなみに Chainer の ImageNet サンプルはこれと Horizontal Flip をやっている(これしかやっていないので相応の精度しか出ない)。

Horizontal Flip

画像をランダムにフリップする。ImageNet では縦方向はなく水平方向の flip のみが使われるようであり、AlexNet 論文以降必ず使われているようだ。

Scale Augmentation

まず画像をリサイズする。[256, 480] からランダムに短辺の長さを選ぶ。次に、224x224 のパッチをそこからランダムサンプルする。

VGG 論文から使われており、ResNet 本家論文でも使われる。

Aspect Ratio Augmentation

上の Scale Augmentation に加え画像のアスペクト比を [¾, 4/3] で変換する・・・ぽい?(論文の記述が短くわかりにくい)

GoogLeNet の論文で使われている。FAIR の ResNet 再現記事でも使われており、本家から 1.2% の精度向上に寄与したらしい。

Color Augmentation

AlexNet 論文で行われた方法は以下である。各ピクセルの RGB を 3 次元のベクトルの集合だと考え PCA をかける。ガウス分布でノイズを生成し、固有ベクトル方向にノイズを加える。乱数は各ピクセルではなくパッチ全体に対して共通。論文によると AlexNet の精度への寄与は 1% 程度。ResNet 本家論文でも使われている。PCA した結果はここにあったり。

“Some Improvements on Deep Convolutional Neural Network Based Image Classification” 論文で行われた方法は以下である。contrast, brightness, color を、[0.5, 1.5] の間でランダムに変更する。変更の順番もランダムに選択する。(ご丁寧に、Python image library (PIL) を使ったと書いてある。)その後で、AlexNet 論文と同様の操作を行う。FAIR の ResNet 再現記事ではこれを利用したと書いてあったが、精度への寄与はとても小さかったらしい。

テスト時

Ensemble

複数のモデルを独立に学習し、それらの予測を平均する。

GoogLeNet 論文では、ネットワークは同じだが入力の処理法を変えた 7 つのモデルをアンサンブルしているらしい。GoogLeNet の論文は全体的に詳細が濁されているのでよくわからない。single crop だと top-5 error が 2% 弱程度向上。

ResNet 本家論文では、34B, 34C, 50, 101, 152×2 の 6 モデルをアンサンブルしている。fully-convolutional-form で top-5 error に 1% 弱程度向上。

10-crop Testing

テスト画像 1 つから data augmentation と類似した手順で 10 個のパッチを切り出し、それぞれに対する予測を平均して答える。

AlexNet 論文では、(4 スミ+中央)×(反転の有無)で 10 個のパッチを切り出している。GoogLeNet は 144 パッチも試してる。

GoogLeNet 論文では、crop 数の精度への影響が載っている。top-5 error で、10 crops で約 1% 弱向上、144 crops で 2% 強向上(下表は GoogLeNet 論文より引用)。

f:id:iwiwi:20161231213228p:plain

Fully-convolutional-form Testing

まず、全結合層たちを畳み込み層とみなす。例えば、直前の画像サイズが s×s であれば、最初の全結合層は s×s → 1×1 の畳み込み層であるとみなす。すると、ネットワークは fully convolutional (全レイヤーが畳み込み)となる。fully convolutional なネットワークの利点は、入力の画像サイズが変化しても適用することができることである(ただし、出力サイズも変化する)。

テスト画像をリサイズする。この時の画像サイズは、学習で使っている画像サイズと一致しているとは限らないし、正方形とも限らない。例えば、短辺を 480 にする。ネットワークをこの画像に適用する。出力を average pooling してそれを予測とする。

さらに、テスト画像のサイズを複数試し、その結果の平均を用いる。例えば、ResNet 本家論文では短辺を 224, 256, 384, 480, 640 になるようにリサイズしている。また、horizontal flip も試して平均を取る。

VGG から使われている。ResNet 本家論文を見るところ、10-crop Testing と比べて、大体 2% 程度精度に寄与している。

Chainer で全結合層を畳み込み層にするコードの例を mitmul さんから貰ったので貼っておきます。

def linear2conv(model, name, feature_size=1):
    """Convert Linear layer to 1x1 Convolution Layer
    args:
        model (chainer.Chain): The input model
        name (str): Name of a Linear link that will be converted to 1x1 conv
            ex. '/predictor/trunk/fc6'
        feature_size (int): The feature map size of convolution layer
    """
    target = model
    lnames = [n for n in name.split('/') if len(n) > 0]
    for l in lnames[:-1]:
        target = target.__dict__[l]
    t = target.__dict__[lnames[-1]]
    assert ('Linear' in str(t.__class__))
    out_dim, in_dim = t.W.data.shape
    in_dim = in_dim // (feature_size ** 2)
    conv = L.Convolution2D(in_dim, out_dim, feature_size)
    W = t.W.data.reshape((out_dim, in_dim, feature_size, feature_size))
    conv.W.data, conv.b.data = W, t.b.data
    target.__dict__[lnames[-1]] = conv
    return model

参考文献

謝辞

  • 宮戸さん
  • mitmul さん

スヌープモードを設定して QPI を爆速で超える

スヌープモードの影響

マルチ CPU のマシンでメモリ帯域を測ると、別ソケット側のメモリとの帯域が死ぬほど遅いという現象がある。これは、CPU のスヌープモードがデフォルトの「Early Snoop」になっているからである。BIOS からこれを「Home Snoop」に変更することによって、帯域は大きく向上する。

software.intel.com

この記事によると、Early Snoop では 6GB/s 程度であったのが、Home Snoop では 25GB/s 程度になっている。

ただし、Home Snoop にすると、代償として同一 NUMA ノード内でのレイテンシが 10% 程度悪くなってしまう。

スヌープとは

キャッシュメモリ - Wikipedia

キャッシュコヒーレンシを保つ方法の 1 つがスヌープ方式(スヌープキャッシュ)である。各キャッシュメモリが、自身とよそのキャッシュラインの状況を管理し、メッセージにより情報交換を行う方式である。この情報交換の方式に、MESI プロトコル等が含まれる。

スヌープ方式以外のアプローチは、ディレクトリ方式、共有キャッシュなどである。

スヌープモードとは

software.intel.com

技術的詳細は分からないが、スヌープモードを変更すると、このスヌープのアルゴリズムが切り替わると思われる。ちなみに、上述の Early Snoop, Home Snoop の他に Cluster on Die (COD) モードというのがある。これは、他の NUMA ノードへのアクセスがかなり少ない仮定を置いたモードのようで、仮想マシンを扱う場合などを想定しているようだ。

数列の和を計算するアルゴリズム

数列の和の計算にアルゴリズムなんて考える余地はあるのか?と思ったが、誤差についても考える場合、単純な方法以外にも複数のアルゴリズムが存在し使われているということを教えてもらった。

Kahan summation はより高い精度を達成できるが、pairwise summation のほうが高速である。NumPy の sum では pairwise summation algorithm が使われているようだ。

ちなみに、分散を計算する際には専用のアルゴリズムもある。

Cython の落とし穴

ポインタを使ってあれこれしたいときの落とし穴を会社で教えてもらった。

numpy の ndarray.data の挙動が型を指定するかによって変わる

tutorials NumpyPointerToC · cython/cython Wiki · GitHub

ここに書かれた方法2でポインタを取り出したい時、引数の型の指定を取り除くと、挙動が変わってしまう。コンパイルエラーも起きず、実行もできるが、ndarray の中が書き換わらないというかなり不思議な状態になる。

これは Cython (cimport) 側の numpy と Python (import) 側の numpy の両方で data が定義されており、Cython 側の定義が優先され上書きされるからである。

void* に Python の int を代入しようとすると意図しない挙動になる

a が Python object の int でありメモリアドレスを表現しているとする(例えば、cupy の ndarray.data.ptr)。から void* に直接キャストしようとすると、Python object のポインタをとってきてしまうようだ。正解は、size_t に一度キャストして、void* にキャストするということである。つまり、以下の2つのコードの挙動は異なる。

<void*>a
<void*><size_t>a

Regularized Greedy Forest (RGF) のロス関数をカスタマイズする

Regularized greedy forest (RGF) in C++

RGF の公式実装のロス関数をカスタマイズするには、C++ のコードを直接書き換えることになる。とはいえそんなに難しくない。以下の方法がお手軽。

  • src/comm/AzLoss.cpp を開く
  • AzLoss::getLosses 関数に loss_type == AzLoss_Xtemp という場合分けを追加
  • o._loss1 にロス関数の 1 階微分を代入する
  • o.loss2 にロス関数の 2 階微分を代入する
  • 使用時には loss に 'Xtemp' を指定する

getLoss の方には追加せずとも動いた。getLoss と getLosses とか、_loss1 と loss2 とか、命名が滅茶苦茶すぎてヤバい……。

malloc にわざと失敗させる

#define _GNU_SOURCE

#include <dlfcn.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>

typedef void* (*malloc_t)(size_t);

static malloc_t libc_malloc = NULL;
static unsigned long malloc_max_called = 1000 * 1000;

void initialize() {
  libc_malloc = (malloc_t)dlsym(RTLD_NEXT, "malloc");
  if (libc_malloc == NULL) {
    perror("dlsym");
    exit(1);
  }

  char const* const s = getenv("MALLOC_MAX_CALLED");
  if (s == NULL) return;

  sscanf(s, "%lu", &malloc_max_called);
}

unsigned long count = 0;

void* malloc(size_t n) {
  if (libc_malloc == NULL) initialize();
  if (count >= malloc_max_called) return NULL;

  count++;
  return libc_malloc(n);
}
% gcc -Wall -g -fPIC -shared failing_malloc.c -o failing_malloc.so -ldl
% MALLOC_MAX_CALLED=1000 LD_PRELOAD=./failing_malloc.so ./a.out

機械学習アルゴリズムの直感を養えるデモ・記事

発見次第更新予定

デモ

Neural Network

Gradient Boosting

t-SNE

リンク集

記事

Chainerを明示的にCUDA無しを指定してインストールする

python setup.py --cupy-no-cuda install

github.com

nvvp とか使いたさに中途半端に手元のマシンに CUDA を入れているとこういう指定が必要になる。

Deep Learning のデバッグ

ミニバッチ化時にミスることが多いので、以下のようなことをすると良いらしい。

  • データごとの計算を書いてみて、1 つずつ計算したものと照合する
  • ミニバッチサイズを 1 にして計算したものの和or平均とミニバッチで計算したものを比較する

numpy の行列乗算:matmul, dot, @

stackoverflow.com

dot と matmul

2 次元では完全に同一。3 次元以上では異なる挙動をする。

  • dot は a の最後の軸と b の最後から 2 番目の軸を掛け合わせる
  • matmul は行列の配列だとみなして行列積を計算する

@ 演算子

Python 3.5 以降では @ 演算子や @= 演算子が存在する。これは __matmul__ を呼ぶが、numpy では matmul に相当するっぽい。

t-SNE の実装はどれを使うべきなのか?

scikit-learn の問題点

scikit-learn 信者としてはとりあえず scikit-learn の実装を使いたくなるが、scikit-learn の実装はおすすめできないらしい。

  • -https://www.red dit.com/r/MachineLearning/comments/47kf7w/scikitlearn_tsne_implementation/ (はてなブログはred ditのURLを貼るとbad requestになり投稿できない謎仕様)

Besides being slower, sklearn's t-SNE implementation is fine once you realize the default learning rate is way too high for most applications. The definitive bh_tsne implementation by the author sets the learning rate to 200, and the original 2008 paper describes setting it to 100, while sklearn is set to 1000.

  • 遅い
  • デフォルト値の learning rate が大きすぎる

とのこと。それに加えて、自分の経験としては、Barnes Hut 木を指定してもメモリをもりもり確保して(即 Θ(n^2) のメモリを確保してる気がする)メモリ不足で死ぬ。だめ。

公式実装に基づくものたち

自分の結論

$ pip install bhtsne

からの

import sklearn.base
import bhtsne
import numpy as np


class BHTSNE(sklearn.base.BaseEstimator, sklearn.base.TransformerMixin):

    def __init__(self, dimensions=2, perplexity=30.0, theta=0.5, rand_seed=-1):
        self.dimensions = dimensions
        self.perplexity = perplexity
        self.theta = theta
        self.rand_seed = rand_seed

    def fit_transform(self, x):
        return bhtsne.tsne(
            x.astype(np.float64), dimensions=self.dimensions, perplexity=self.perplexity, theta=self.theta,
            rand_seed=self.rand_seed)

バイナリ探偵をする時に使うコマンド

共有ライブラリ編

  • env | grep LD
  • md5sum
    • 一致してるものを調べる
  • ls -l
  • readelf -a hogehoge.so
    • readelf -a hogehoge.so | grep SONAME
    • SONAME を調べる
  • find build/ -name '*so' | xargs ldd | less
    • 依存関係を調べる
    • ldd `which hoge` とかも
  • LD_DEBUG=all <コマンドを実行>
    • 共有ライブラリの読み込みに関して大量のデバッグログが吐かれる
  • objdump -d hogehoge.so | less
    • アセンブラを読む
    • 特に、<関数名> で検索することが多い
  • man ld-linux.so
  • readeof -d hoge.so
  • nm -D hoge.so