Caffe : Notebook Example : スタイル認識のための事前訓練されたネットワークの再調整

Caffe : Notebook Example : スタイル認識のための事前訓練されたネットワークの再調整 (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
日時 : 04/05/2017

* 本ページは、jupyter サンプル: examples/02-fine-tuning.ipynb – Fine-tuning a Pretrained Network for Style Recognition を実行・翻訳した上で適宜、補足説明したものです:
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

ImageNet で訓練された CaffeNet を新しいデータで再調整する。

このサンプルでは、現実世界のアプリケーションで特に有用な一般的なアプローチを探ります : 事前訓練された Caffe ネットワークを取得して貴方のカスタム・データ上でパラメータを再調整します。

このアプローチの優位点は、事前訓練されたネットワークは画像の巨大なセット上で学習しているので、中間層は一般的な外観の “意味” をとらえていることです。それを、ブラックボックスとして扱える非常にパワフルな一般的な視覚的特徴として考えてください。それの上に、比較的小さな量のデータだけがその目的タスク上での良いパフォーマンスのために必要です。

最初に、データを準備する必要があります。これは次のパートを含みます : (1) ImageNet ilsvrc 事前訓練されたモデルを提供されている shell スクリプトで取得する。(2) このデモのための Flickr スタイル・データセット全体のサブセットをダウンロードする。(3) ダウンロードした Flickr データセットを Caffe が消費可能なデータベースにコンパイルする。

caffe_root = '../'  # this file should be run from {caffe_root}/examples (otherwise change this line)

import sys
sys.path.insert(0, caffe_root + 'python')
import caffe

caffe.set_device(0)
caffe.set_mode_gpu()

import numpy as np
from pylab import *
%matplotlib inline
import tempfile

# Helper function for deprocessing preprocessed images, e.g., for display.
def deprocess_net_image(image):
    image = image.copy()              # don't modify destructively
    image = image[::-1]               # BGR -> RGB
    image = image.transpose(1, 2, 0)  # CHW -> HWC
    image += [123, 117, 104]          # (approximately) undo mean subtraction

    # clamp values in [0, 255]
    image[image < 0], image[image > 255] = 0, 255

    # round and cast from float32 to uint8
    image = np.round(image)
    image = np.require(image, dtype=np.uint8)

    return image

 

1. セットアップとデータセット・ダウンロード

この exercise に必要なデータをダウンロードします。

  • get_ilsvrc_aux.sh : ImageNet データ mean, ラベル, etc. をダウンロード
  • download_model_binary.py : 事前訓練した参照モデルをダウンロード
  • finetune_flickr_style/assemble_data.py : style 訓練/テスト・データをダウンロード

この exercise のために完全なデータセットの小さなサブセットだけをダウンロードします : 20 スタイル・カテゴリの 5 つから、80K 画像の 2000 だけ。(完全なデータセットをダウンロードするためには、下のセルで full_dataset = True を設定してください。)

# Download just a small subset of the data for this exercise.
# (2000 of 80K images, 5 of 20 labels.)
# To download the entire dataset, set `full_dataset = True`.
full_dataset = False
if full_dataset:
    NUM_STYLE_IMAGES = NUM_STYLE_LABELS = -1
else:
    NUM_STYLE_IMAGES = 2000
    NUM_STYLE_LABELS = 5

# This downloads the ilsvrc auxiliary data (mean file, etc),
# and a subset of 2000 images for the style recognition task.
import os
os.chdir(caffe_root)  # run scripts from caffe root
!data/ilsvrc12/get_ilsvrc_aux.sh
!scripts/download_model_binary.py models/bvlc_reference_caffenet
!python examples/finetune_flickr_style/assemble_data.py \
    --workers=-1  --seed=1701 \
    --images=$NUM_STYLE_IMAGES  --label=$NUM_STYLE_LABELS
# back to examples
os.chdir('examples')
Downloading...
--2017-04-05 18:39:19--  http://dl.caffe.berkeleyvision.org/caffe_ilsvrc12.tar.gz
dl.caffe.berkeleyvision.org (dl.caffe.berkeleyvision.org) をDNSに問いあわせています... 169.229.222.251
dl.caffe.berkeleyvision.org (dl.caffe.berkeleyvision.org)|169.229.222.251|:80 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 17858008 (17M) [application/octet-stream]
`caffe_ilsvrc12.tar.gz' に保存中

100%[======================================>] 17,858,008  1.93MB/s   時間 15s    

2017-04-05 18:39:35 (1.11 MB/s) - `caffe_ilsvrc12.tar.gz' へ保存完了 [17858008/17858008]

Unzipping...
Done.
Model already exists.
Downloading 2000 images with 7 workers...
Writing train/val for 1995 successfully downloaded images.

`weights` を定義します, ダウンロードしたばかりの ImageNet 事前訓練された重みへのパスです、そしてそれが存在することを確認します。

import os
weights = os.path.join(caffe_root, 'models/bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel')
assert os.path.exists(weights)

ilsvrc12/synset_words.txt から 1000 ImageNet ラベルを、そして finetune_flickr_style/style_names.txt から 5 スタイル・ラベルをロードします。

# Load ImageNet labels to imagenet_labels
imagenet_label_file = caffe_root + 'data/ilsvrc12/synset_words.txt'
imagenet_labels = list(np.loadtxt(imagenet_label_file, str, delimiter='\t'))
assert len(imagenet_labels) == 1000
print 'Loaded ImageNet labels:\n', '\n'.join(imagenet_labels[:10] + ['...'])

# Load style labels to style_labels
style_label_file = caffe_root + 'examples/finetune_flickr_style/style_names.txt'
style_labels = list(np.loadtxt(style_label_file, str, delimiter='\n'))
if NUM_STYLE_LABELS > 0:
    style_labels = style_labels[:NUM_STYLE_LABELS]
print '\nLoaded style labels:\n', ', '.join(style_labels)
Loaded ImageNet labels:
n01440764 tench, Tinca tinca
n01443537 goldfish, Carassius auratus
n01484850 great white shark, white shark, man-eater, man-eating shark, Carcharodon carcharias
n01491361 tiger shark, Galeocerdo cuvieri
n01494475 hammerhead, hammerhead shark
n01496331 electric ray, crampfish, numbfish, torpedo
n01498041 stingray
n01514668 cock
n01514859 hen
n01518878 ostrich, Struthio camelus
...

Loaded style labels:
Detailed, Pastel, Melancholy, Noir, HDR

 

2. net を定義して実行する

caffenet を定義することから始めます、CaffeNet アーキテクチャ (AlexNet の小さな変更) を初期化する関数で、データと出力クラスの数を指定する引数をとります。

from caffe import layers as L
from caffe import params as P

weight_param = dict(lr_mult=1, decay_mult=1)
bias_param   = dict(lr_mult=2, decay_mult=0)
learned_param = [weight_param, bias_param]

frozen_param = [dict(lr_mult=0)] * 2

def conv_relu(bottom, ks, nout, stride=1, pad=0, group=1,
              param=learned_param,
              weight_filler=dict(type='gaussian', std=0.01),
              bias_filler=dict(type='constant', value=0.1)):
    conv = L.Convolution(bottom, kernel_size=ks, stride=stride,
                         num_output=nout, pad=pad, group=group,
                         param=param, weight_filler=weight_filler,
                         bias_filler=bias_filler)
    return conv, L.ReLU(conv, in_place=True)

def fc_relu(bottom, nout, param=learned_param,
            weight_filler=dict(type='gaussian', std=0.005),
            bias_filler=dict(type='constant', value=0.1)):
    fc = L.InnerProduct(bottom, num_output=nout, param=param,
                        weight_filler=weight_filler,
                        bias_filler=bias_filler)
    return fc, L.ReLU(fc, in_place=True)

def max_pool(bottom, ks, stride=1):
    return L.Pooling(bottom, pool=P.Pooling.MAX, kernel_size=ks, stride=stride)

def caffenet(data, label=None, train=True, num_classes=1000,
             classifier_name='fc8', learn_all=False):
    """Returns a NetSpec specifying CaffeNet, following the original proto text
       specification (./models/bvlc_reference_caffenet/train_val.prototxt)."""
    n = caffe.NetSpec()
    n.data = data
    param = learned_param if learn_all else frozen_param
    n.conv1, n.relu1 = conv_relu(n.data, 11, 96, stride=4, param=param)
    n.pool1 = max_pool(n.relu1, 3, stride=2)
    n.norm1 = L.LRN(n.pool1, local_size=5, alpha=1e-4, beta=0.75)
    n.conv2, n.relu2 = conv_relu(n.norm1, 5, 256, pad=2, group=2, param=param)
    n.pool2 = max_pool(n.relu2, 3, stride=2)
    n.norm2 = L.LRN(n.pool2, local_size=5, alpha=1e-4, beta=0.75)
    n.conv3, n.relu3 = conv_relu(n.norm2, 3, 384, pad=1, param=param)
    n.conv4, n.relu4 = conv_relu(n.relu3, 3, 384, pad=1, group=2, param=param)
    n.conv5, n.relu5 = conv_relu(n.relu4, 3, 256, pad=1, group=2, param=param)
    n.pool5 = max_pool(n.relu5, 3, stride=2)
    n.fc6, n.relu6 = fc_relu(n.pool5, 4096, param=param)
    if train:
        n.drop6 = fc7input = L.Dropout(n.relu6, in_place=True)
    else:
        fc7input = n.relu6
    n.fc7, n.relu7 = fc_relu(fc7input, 4096, param=param)
    if train:
        n.drop7 = fc8input = L.Dropout(n.relu7, in_place=True)
    else:
        fc8input = n.relu7
    # always learn fc8 (param=learned_param)
    fc8 = L.InnerProduct(fc8input, num_output=num_classes, param=learned_param)
    # give fc8 the name specified by argument `classifier_name`
    n.__setattr__(classifier_name, fc8)
    if not train:
        n.probs = L.Softmax(fc8)
    if label is not None:
        n.label = label
        n.loss = L.SoftmaxWithLoss(fc8, n.label)
        n.acc = L.Accuracy(fc8, n.label)
    # write the net to a temporary file and return its filename
    with tempfile.NamedTemporaryFile(delete=False) as f:
        f.write(str(n.to_proto()))
        return f.name

さて、CaffeNet を作成しましょう、これはラベル付けされていない “ダミーデータ” を入力として取り、入力画像を外部に設定してそれが予測する ImageNet クラスが何かを見ることを可能にします。

dummy_data = L.DummyData(shape=dict(dim=[1, 3, 227, 227]))
imagenet_net_filename = caffenet(data=dummy_data, train=False)
imagenet_net = caffe.Net(imagenet_net_filename, weights, caffe.TEST)

関数 style_net を定義します、これは Flickr スタイル・データセットからのデータ上で caffenet を呼び出します。新しいネットワークもまた CaffeNet アーキテクチャを持ちますが、入力と出力に違いがあります :

  • 入力はダウンロードした Flickr スタイル・データです、ImageData 層で提供されます。
  • 出力は元の 1000 ImageNet クラスではなく 20 クラスに渡る分布です。
  • 分類層は fc8 から fc8_flicker と名前を変更しています、これは Caffe に ImageNet 事前訓練済みのモデルから元の分類器 (fc8) 重みをロードしないように Caffe に伝えるためです。
def style_net(train=True, learn_all=False, subset=None):
    if subset is None:
        subset = 'train' if train else 'test'
    source = caffe_root + 'data/flickr_style/%s.txt' % subset
    transform_param = dict(mirror=train, crop_size=227,
        mean_file=caffe_root + 'data/ilsvrc12/imagenet_mean.binaryproto')
    style_data, style_label = L.ImageData(
        transform_param=transform_param, source=source,
        batch_size=50, new_height=256, new_width=256, ntop=2)
    return caffenet(data=style_data, label=style_label, train=train,
                    num_classes=NUM_STYLE_LABELS,
                    classifier_name='fc8_flickr',
                    learn_all=learn_all)

上で定義した style_net 関数を使って untrained_style_net を初期化します、これは style データセットから入力画像をそして事前訓練された ImageNet モデルから重みを用いています。

style 訓練データのバッチを得るために untrained_style_net 上で forward を呼び出します。

untrained_style_net = caffe.Net(style_net(train=False, subset='train'),
                                weights, caffe.TEST)
untrained_style_net.forward()
style_data_batch = untrained_style_net.blobs['data'].data.copy()
style_label_batch = np.array(untrained_style_net.blobs['label'].data, dtype=np.int32)

50 のバッチから style net 訓練画像の一つを選び取ります (ここでは任意に #8 を選択します)。それを表示して、そしてそれを imagenet_net、ImageNet 事前訓練済みネットワークを通して 1000 ImageNet クラスから top 5 予測クラスを見るために 実行します。

以下で画像を選択しますがネットワークの予測は合理的なようです、画像は浜辺のもので、”砂州 (sandbar)” と “海辺 (seashore)” の両者とも ImageNet-1000 カテゴリです。他の画像については、予測はこのように良くはないでしょう、時にネットワークが画像に存在する物体(群)を認識することに実際に失敗することによりますが、しかし多分全ての画像が (幾分任意に選択された) 1000 ImageNet カテゴリからの物体を含むわけではないという事実によります。

バッチの他の画像のための予測を見るために batch_index 変数をそのデフォルト設定 8 を 0-49 (バッチサイズは 50 なので) の他の値に変更することによって修正しましょう。(50 画像のこのバッチを越えて進むには、最初に上のセルを実行してデータの新しいバッチを style_net にロードします。)

def disp_preds(net, image, labels, k=5, name='ImageNet'):
    input_blob = net.blobs['data']
    net.blobs['data'].data[0, ...] = image
    probs = net.forward(start='conv1')['probs'][0]
    top_k = (-probs).argsort()[:k]
    print 'top %d predicted %s labels =' % (k, name)
    print '\n'.join('\t(%d) %5.2f%% %s' % (i+1, 100*probs[p], labels[p])
                    for i, p in enumerate(top_k))

def disp_imagenet_preds(net, image):
    disp_preds(net, image, imagenet_labels, name='ImageNet')

def disp_style_preds(net, image):
    disp_preds(net, image, style_labels, name='style')
batch_index = 8
image = style_data_batch[batch_index]
plt.imshow(deprocess_net_image(image))
print 'actual label =', style_labels[style_label_batch[batch_index]]
actual label = Melancholy

disp_imagenet_preds(imagenet_net, image)
top 5 predicted ImageNet labels =
	(1) 69.89% n09421951 sandbar, sand bar
	(2) 21.75% n09428293 seashore, coast, seacoast, sea-coast
	(3)  3.22% n02894605 breakwater, groin, groyne, mole, bulwark, seawall, jetty
	(4)  1.89% n04592741 wing
	(5)  1.23% n09332890 lakeside, lakeshore

untrained_style_net の予測を見ることもできますが、分類器はまだ訓練されていないので興味深いものを見ることはないでしょう。実際に、分類器をゼロ初期化しましたから (caffenet 定義参照 — 最後の InnerProduct 層には weight_filter は渡されません)、softmax 入力は全てゼロであるべきでそれゆえに (N ラベルに対する) 各ラベルに対して 1/N の予測確率を見るでしょう。N = 5 に設定しましたので、各クラスに対して 20% の予測確率を得ます。

disp_style_preds(untrained_style_net, image)
top 5 predicted style labels =
	(1) 20.00% Detailed
	(2) 20.00% Pastel
	(3) 20.00% Melancholy
	(4) 20.00% Noir
	(5) 20.00% HDR

分類層のすぐ前の層 fc7 の活性はImageNet 事前訓練済みモデルのそれらと同じである (または極めて近い) ことも検証できます、何故ならば両者のモデルとも conv1 から fc7 層を通して同じ事前訓練された重みを使用しているからです。

diff = untrained_style_net.blobs['fc7'].data[0] - imagenet_net.blobs['fc7'].data[0]
error = (diff ** 2).sum()
assert error < 1e-8

untrained_style_net をメモリをセーブするために削除します。(imagenet_net はそのままにしておきます後でまた使いますので。)

del untrained_style_net

 

3. style 分類器を訓練する

さて、私たちの Caffe solver を作成するために関数 solver を定義します、これはネットワークを訓練する (その重みを学習する) ために使用されます。この関数では学習、表示、そして "スナップショットを撮る" ために使われる各種パラメータのための値を設定します -- それらが意味するところの説明のためにはインライン・コメントを見てください。学習パラメータの幾つかと遊んでみることを望むかもしれません、ここでの結果を改善できるかを見るために!

from caffe.proto import caffe_pb2
​
def solver(train_net_path, test_net_path=None, base_lr=0.001):
    s = caffe_pb2.SolverParameter()
​
    # Specify locations of the train and (maybe) test networks.
    s.train_net = train_net_path
    if test_net_path is not None:
        s.test_net.append(test_net_path)
        s.test_interval = 1000  # Test after every 1000 training iterations.
        s.test_iter.append(100) # Test on 100 batches each time we test.
​
    # The number of iterations over which to average the gradient.
    # Effectively boosts the training batch size by the given factor, without
    # affecting memory utilization.
    s.iter_size = 1
    
    s.max_iter = 100000     # # of times to update the net (training iterations)
    
    # Solve using the stochastic gradient descent (SGD) algorithm.
    # Other choices include 'Adam' and 'RMSProp'.
    s.type = 'SGD'
​
    # Set the initial learning rate for SGD.
    s.base_lr = base_lr
​
    # Set `lr_policy` to define how the learning rate changes during training.
    # Here, we 'step' the learning rate by multiplying it by a factor `gamma`
    # every `stepsize` iterations.
    s.lr_policy = 'step'
    s.gamma = 0.1
    s.stepsize = 20000
​
    # Set other SGD hyperparameters. Setting a non-zero `momentum` takes a
    # weighted average of the current gradient and previous gradients to make
    # learning more stable. L2 weight decay regularizes learning, to help prevent
    # the model from overfitting.
    s.momentum = 0.9
    s.weight_decay = 5e-4
​
    # Display the current training loss and accuracy every 1000 iterations.
    s.display = 1000
​
    # Snapshots are files used to store networks we've trained.  Here, we'll
    # snapshot every 10K iterations -- ten times during training.
    s.snapshot = 10000
    s.snapshot_prefix = caffe_root + 'models/finetune_flickr_style/finetune_flickr_style'
    
    # Train on the GPU.  Using the CPU to train large networks is very slow.
    s.solver_mode = caffe_pb2.SolverParameter.GPU
    
    # Write the solver to a temporary file and return its filename.
    with tempfile.NamedTemporaryFile(delete=False) as f:
        f.write(str(s))
        return f.name

さて style net の分類層を訓練するために solver を起動します。

念のため、コマンドライン・ツールのみを使用してネットワークを訓練することを望むのであれば、これがそのコマンドです :

build/tools/caffe train \
    -solver models/finetune_flickr_style/solver.prototxt \
    -weights models/bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel \
    -gpu 0

けれども、このサンプルでは Python を使用して訓練します。

最初に run_solvers を定義します、これは関数で、solver のリストを取り round robin 流儀で各一つのステップを取り、各反復で精度と損失の値を記録します。最後に、学習された重みがファイルに保存されます。

def run_solvers(niter, solvers, disp_interval=10):
    """Run solvers for niter iterations,
       returning the loss and accuracy recorded each iteration.
       `solvers` is a list of (name, solver) tuples."""
    blobs = ('loss', 'acc')
    loss, acc = ({name: np.zeros(niter) for name, _ in solvers}
                 for _ in blobs)
    for it in range(niter):
        for name, s in solvers:
            s.step(1)  # run a single SGD step in Caffe
            loss[name][it], acc[name][it] = (s.net.blobs[b].data.copy()
                                             for b in blobs)
        if it % disp_interval == 0 or it + 1 == niter:
            loss_disp = '; '.join('%s: loss=%.3f, acc=%2d%%' %
                                  (n, loss[n][it], np.round(100*acc[n][it]))
                                  for n, _ in solvers)
            print '%3d) %s' % (it, loss_disp)     
    # Save the learned weights from both nets.
    weight_dir = tempfile.mkdtemp()
    weights = {}
    for name, s in solvers:
        filename = 'weights.%s.caffemodel' % name
        weights[name] = os.path.join(weight_dir, filename)
        s.net.save(weights[name])
    return loss, acc, weights

style 認識タスクのための net を訓練するために solver を作成して実行しましょう。2つの solver を作成します -- 一つ (style_solver) は ImageNet 事前訓練された重みに初期化された (これは copy_from メソッド呼び出しで成されます) train net を持ち、他方 (scratch_style_solver) はランダムに初期化された net から開始します。

訓練の間、ImageNet 事前訓練された net は scratch net よりもより速く学習してより良い精度を得ることを見るはずです。

niter = 200  # 訓練のための反復数

# 前のように style_solver をリセットする
style_solver_filename = solver(style_net(train=True))
style_solver = caffe.get_solver(style_solver_filename)
style_solver.net.copy_from(weights)

# 参考のため、事前訓練された ImageNet 重みから初期化されていない
# solver も作成します
scratch_style_solver_filename = solver(style_net(train=True))
scratch_style_solver = caffe.get_solver(scratch_style_solver_filename)

print 'Running solvers for %d iterations...' % niter
solvers = [('pretrained', style_solver),
           ('scratch', scratch_style_solver)]
loss, acc, weights = run_solvers(niter, solvers)
print 'Done.'

train_loss, scratch_train_loss = loss['pretrained'], loss['scratch']
train_acc, scratch_train_acc = acc['pretrained'], acc['scratch']
style_weights, scratch_style_weights = weights['pretrained'], weights['scratch']

# Delete solvers to save memory.
del style_solver, scratch_style_solver, solvers
Running solvers for 200 iterations...
  0) pretrained: loss=1.609, acc=30%; scratch: loss=1.609, acc=30%
 10) pretrained: loss=1.447, acc=52%; scratch: loss=1.626, acc=16%
 20) pretrained: loss=1.096, acc=54%; scratch: loss=1.646, acc= 8%
 30) pretrained: loss=1.167, acc=58%; scratch: loss=1.615, acc=20%
 40) pretrained: loss=0.804, acc=70%; scratch: loss=1.582, acc=28%
 50) pretrained: loss=1.057, acc=60%; scratch: loss=1.608, acc=34%
 60) pretrained: loss=1.056, acc=58%; scratch: loss=1.615, acc=20%
 70) pretrained: loss=0.924, acc=60%; scratch: loss=1.595, acc=24%
 80) pretrained: loss=1.036, acc=56%; scratch: loss=1.584, acc=32%
 90) pretrained: loss=1.182, acc=48%; scratch: loss=1.609, acc=20%
100) pretrained: loss=1.045, acc=58%; scratch: loss=1.591, acc=28%
110) pretrained: loss=1.106, acc=64%; scratch: loss=1.577, acc=34%
120) pretrained: loss=0.993, acc=56%; scratch: loss=1.596, acc=20%
130) pretrained: loss=0.897, acc=72%; scratch: loss=1.589, acc=22%
140) pretrained: loss=1.059, acc=56%; scratch: loss=1.608, acc=14%
150) pretrained: loss=0.680, acc=72%; scratch: loss=1.617, acc=16%
160) pretrained: loss=0.897, acc=66%; scratch: loss=1.596, acc=20%
170) pretrained: loss=0.955, acc=70%; scratch: loss=1.612, acc=20%
180) pretrained: loss=1.239, acc=50%; scratch: loss=1.614, acc=18%
190) pretrained: loss=0.767, acc=74%; scratch: loss=1.612, acc=18%
199) pretrained: loss=0.981, acc=64%; scratch: loss=1.612, acc=18%
Done.

2つの訓練手続きから生成された訓練損失と精度を見てみましょう。ImageNet で事前訓練されたモデルの損失値 (青色) がどれほど迅速に落ち、そしてランダムに初期化されたモデルの損失値 (オレンジ色) が分類層だけの訓練からはほとんど改善されないことに気がつくでしょう。

plot(np.vstack([train_loss, scratch_train_loss]).T)
xlabel('Iteration #')
ylabel('Loss')
<matplotlib.text.Text at 0x7f9e84fc2d50>

plot(np.vstack([train_acc, scratch_train_acc]).T)
xlabel('Iteration #')
ylabel('Accuracy')
<matplotlib.text.Text at 0x7f9e85814f10>

訓練の 200 反復を実行した後テスティング精度を見てみましょう。5 クラスの中で分類していて、20 % の偶然 (chance) 精度を与えていることに注意しましょう。両者の結果が偶然精度 (20%) よりも良いものであることを期待し、そして更に ImageNet 事前訓練による初期化を使用した訓練の結果がスクラッチから訓練したものよりも遥に良いことも期待します。見てみましょう。

def eval_style_net(weights, test_iters=10):
    test_net = caffe.Net(style_net(train=False), weights, caffe.TEST)
    accuracy = 0
    for it in xrange(test_iters):
        accuracy += test_net.forward()['acc']
    accuracy /= test_iters
    return test_net, accuracy
test_net, accuracy = eval_style_net(style_weights)
print 'Accuracy, trained from ImageNet initialization: %3.1f%%' % (100*accuracy, )
scratch_test_net, scratch_accuracy = eval_style_net(scratch_style_weights)
print 'Accuracy, trained from   random initialization: %3.1f%%' % (100*scratch_accuracy, )

Accuracy, trained from ImageNet initialization: 53.4%
Accuracy, trained from random initialization: 23.6%

 

4. style のための end-to-end 再調整

最後に、両方の net を再度訓練します、学習したばかりの重みから開始します。今回の唯一の違いはネットワークの全ての層における学習を有効にして重みを "end-to-end" で学習することで、入力画像に RGB conv1 フィルタを直接適用することから始めます。learn_all=True 引数をこのノートブックで先に定義した style_net 関数に渡します、これは全てのパラメータに正の (non-zero) lr_mult 値を適用することを伝えます。デフォルトでは、learn_all=False, 事前学習された層 (conv1 から fc7) における全てのパラメータは凍らされ (lr_mult = 0)、そして分類層 fc8_flickr のみを学習します。

両者のネットワークは前の訓練セッションの最後に獲得された精度からおおよそ開始され、end-to-end 訓練で本質的に改善することに注意してください。より科学的に言えば、単に2倍訓練したという理由で結果が良くないことを確認するために end-to-end 訓練なしで同じ追加の訓練手続きを追随することを望みます。Feel free to try this yourself!

end_to_end_net = style_net(train=True, learn_all=True)
​
# Set base_lr to 1e-3, the same as last time when learning only the classifier.
# You may want to play around with different values of this or other
# optimization parameters when fine-tuning.  For example, if learning diverges
# (e.g., the loss gets very large or goes to infinity/NaN), you should try
# decreasing base_lr (e.g., to 1e-4, then 1e-5, etc., until you find a value
# for which learning does not diverge).
base_lr = 0.001
​
style_solver_filename = solver(end_to_end_net, base_lr=base_lr)
style_solver = caffe.get_solver(style_solver_filename)
style_solver.net.copy_from(style_weights)
​
scratch_style_solver_filename = solver(end_to_end_net, base_lr=base_lr)
scratch_style_solver = caffe.get_solver(scratch_style_solver_filename)
scratch_style_solver.net.copy_from(scratch_style_weights)
​
print 'Running solvers for %d iterations...' % niter
solvers = [('pretrained, end-to-end', style_solver),
           ('scratch, end-to-end', scratch_style_solver)]
_, _, finetuned_weights = run_solvers(niter, solvers)
print 'Done.'
​
style_weights_ft = finetuned_weights['pretrained, end-to-end']
scratch_style_weights_ft = finetuned_weights['scratch, end-to-end']
​
# Delete solvers to save memory.
del style_solver, scratch_style_solver, solvers
Running solvers for 200 iterations...
  0) pretrained, end-to-end: loss=0.657, acc=76%; scratch, end-to-end: loss=1.580, acc=30%
 10) pretrained, end-to-end: loss=1.218, acc=56%; scratch, end-to-end: loss=1.636, acc=16%
 20) pretrained, end-to-end: loss=0.902, acc=62%; scratch, end-to-end: loss=1.628, acc= 8%
 30) pretrained, end-to-end: loss=1.007, acc=70%; scratch, end-to-end: loss=1.592, acc=20%
 40) pretrained, end-to-end: loss=0.824, acc=72%; scratch, end-to-end: loss=1.563, acc=30%
 50) pretrained, end-to-end: loss=0.842, acc=68%; scratch, end-to-end: loss=1.596, acc=38%
 60) pretrained, end-to-end: loss=0.824, acc=68%; scratch, end-to-end: loss=1.528, acc=32%
 70) pretrained, end-to-end: loss=0.535, acc=82%; scratch, end-to-end: loss=1.538, acc=32%
 80) pretrained, end-to-end: loss=0.561, acc=76%; scratch, end-to-end: loss=1.466, acc=34%
 90) pretrained, end-to-end: loss=0.727, acc=70%; scratch, end-to-end: loss=1.486, acc=36%
100) pretrained, end-to-end: loss=0.493, acc=84%; scratch, end-to-end: loss=1.470, acc=34%
110) pretrained, end-to-end: loss=0.852, acc=66%; scratch, end-to-end: loss=1.591, acc=24%
120) pretrained, end-to-end: loss=0.513, acc=80%; scratch, end-to-end: loss=1.490, acc=32%
130) pretrained, end-to-end: loss=0.481, acc=82%; scratch, end-to-end: loss=1.511, acc=24%
140) pretrained, end-to-end: loss=0.562, acc=80%; scratch, end-to-end: loss=1.477, acc=32%
150) pretrained, end-to-end: loss=0.505, acc=78%; scratch, end-to-end: loss=1.446, acc=36%
160) pretrained, end-to-end: loss=0.435, acc=86%; scratch, end-to-end: loss=1.360, acc=42%
170) pretrained, end-to-end: loss=0.656, acc=74%; scratch, end-to-end: loss=1.671, acc=26%
180) pretrained, end-to-end: loss=0.446, acc=80%; scratch, end-to-end: loss=1.545, acc=26%
190) pretrained, end-to-end: loss=0.359, acc=84%; scratch, end-to-end: loss=1.424, acc=46%
199) pretrained, end-to-end: loss=0.444, acc=80%; scratch, end-to-end: loss=1.389, acc=42%
Done.

さて end-to-end 再調整モデルをテストしましょう。全ての層が style 認識タスクに手元で最適化されたので、両方の net に上のものよりもより良い結果を得ることを期待します、それらは (ImageNet 事前訓練あるいはランダムに重みが初期化された top 上の) style タスクのために訓練された分類層のみを持つ net で獲得されたものでした。

test_net, accuracy = eval_style_net(style_weights_ft)
print 'Accuracy, finetuned from ImageNet initialization: %3.1f%%' % (100*accuracy, )
scratch_test_net, scratch_accuracy = eval_style_net(scratch_style_weights_ft)
print 'Accuracy, finetuned from   random initialization: %3.1f%%' % (100*scratch_accuracy, )
Accuracy, finetuned from ImageNet initialization: 53.8%
Accuracy, finetuned from   random initialization: 40.0%

We'll first look back at the image we started with
and check our end-to-end trained model's predictions.

plt.imshow(deprocess_net_image(image))
disp_style_preds(test_net, image)
top 5 predicted style labels =
	(1) 99.09% Melancholy
	(2)  0.46% Pastel
	(3)  0.36% HDR
	(4)  0.08% Detailed
	(5)  0.00% Noir

Whew, 前よりもかなり良いようです!しかしこの画像は訓練セットからのもので net は訓練時にそのラベルを見ています。

最後に、テストセット (モデルがまた見ていない画像) から画像を選び取ってそれのための end-to-end に再調整した style モデルの予測を見てみます。

batch_index = 14
image = test_net.blobs['data'].data[batch_index]
plt.imshow(deprocess_net_image(image))
print 'actual label =', style_labels[int(test_net.blobs['label'].data[batch_index])]
actual label = Pastel

disp_style_preds(test_net, image)
top 5 predicted style labels =
	(1) 99.80% Pastel
	(2)  0.18% Melancholy
	(3)  0.01% Detailed
	(4)  0.01% HDR
	(5)  0.00% Noir

スクラッチから訓練したネットワークの予測を見ることもできます。この場合はそれを見れます、scratch network はまた画像 (Pastel) のために正しいラベルを予測します、しかしその予測の信頼度は事前学習された net よりもかなり低いです。

disp_style_preds(scratch_test_net, image)
top 5 predicted style labels =
	(1) 41.60% Pastel
	(2) 22.38% Melancholy
	(3) 19.43% Detailed
	(4) 13.55% HDR
	(5)  3.04% Noir

もちろん、上の画像に対して ImageNet モデル予測を再度見ることもできます :

disp_imagenet_preds(imagenet_net, image)
top 5 predicted ImageNet labels =
	(1) 28.13% n04275548 spider web, spider's web
	(2)  7.97% n07714990 broccoli
	(3)  6.01% n02219486 ant, emmet, pismire
	(4)  5.24% n02206856 bee
	(5)  4.23% n04209239 shower curtain

So we did finetuning and it is awesome. Let's take a look at what kind of results we are able to get with a longer, more complete run of the style recognition dataset. Note: the below URL might be occasionally down because it is run on a research machine.

http://demo.vislab.berkeleyvision.org/

 

以上