FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

CNTK 2.0 のチュートリアル試行: CNTK 103 Recognize hand written digits (OCR) with MNIST data

CNTK 2.0のチュートリアルを試行する。

今回はある程度翻訳という形で実施してみた。だいぶ誤訳はあるだろうが、ザックリ概要をつかむという形でやっていきたい。

CNTK 103のチュートリアルは4編で構成されている。MNISTの数値認識のチュートリアルについて、

  • 103 Part-A : MNIST data preparation
  • 103 Part-B : Multi-class logistic regression classifier
  • 103 Part-C : Multi-layer perception classifier
  • 103 Part-D : Convolutional neural network classifier

となっている。ここでは、すべての章を詳細にみていくと大変なので、ザックリと1つの項目にまとめた。 最初の準備段階のところとかはかなり被っているので省略している。 それぞれ、MNISTの問題を

  • ロジスティック回帰分析を用いて実現する
  • パーセプション分類器(MLP)を用いて実現する
  • 畳み込みニューラルネットワークを使って実現する

と3種類の方法をCNTKで実装し比較検討を行っている。

github.com

以降の文章については、CNTK 2.0の翻訳を含んでいるが、誤訳を含んでいる可能性がある。勉強しながら書き進めていった翻訳兼メモのため、文章の構成にはあまり力を注いでいない。

問題がある場合は指摘していただけるとありがたいです。


from IPython.display import display, Image

CNTK 103 Part A: MNIST Data Loader

CNTKでのMNISTを実施する。CNTK 101とCNTK 102のようなデータではなく、実際のデータを使用して実施していく。

  • Part A: MNISTデータベースについてまずは慣れる。
  • 後続のPart: MNISTを使用して異なるネットワークを構成する。
# 後々使用する関連モジュールをインポートする
from __future__ import print_function
import gzip
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
import numpy as np
import os
import shutil
import struct
import sys
import time

try: 
    from urllib.request import urlretrieve 
except ImportError: 
    from urllib import urlretrieve

# matplotlibをインラインで利用できるようにする
%matplotlib inline

データダウンロード

MNISTデータベースはマシンラーニングアルゴリズムにおいて非常に一般的に使用されている手書きデータである。 トレーニングには60000個のデータを使用しており、10000個のデータをテストに利用している。各データには28x28ピクセルのデータで構成されている。 このデータセットはトレーニングのデータを可視化しやすいものである。

# Functions to load MNIST images and unpack into train and test set.
# - loadData reads image data and formats into a 28x28 long array
# - loadLabels reads the corresponding labels data, 1 for each image
# - load packs the downloaded image and labels data into a combined format to be read later by 
#   CNTK text reader 

def loadData(src, cimg):
    print ('Downloading ' + src)
    gzfname, h = urlretrieve(src, './delete.me')
    print ('Done.')
    try:
        with gzip.open(gzfname) as gz:
            n = struct.unpack('I', gz.read(4))
            # Read magic number.
            if n[0] != 0x3080000:
                raise Exception('Invalid file: unexpected magic number.')
            # Read number of entries.
            n = struct.unpack('>I', gz.read(4))[0]
            if n != cimg:
                raise Exception('Invalid file: expected {0} entries.'.format(cimg))
            crow = struct.unpack('>I', gz.read(4))[0]
            ccol = struct.unpack('>I', gz.read(4))[0]
            if crow != 28 or ccol != 28:
                raise Exception('Invalid file: expected 28 rows/cols per image.')
            # Read data.
            res = np.fromstring(gz.read(cimg * crow * ccol), dtype = np.uint8)
    finally:
        os.remove(gzfname)
    return res.reshape((cimg, crow * ccol))

def loadLabels(src, cimg):
    print ('Downloading ' + src)
    gzfname, h = urlretrieve(src, './delete.me')
    print ('Done.')
    try:
        with gzip.open(gzfname) as gz:
            n = struct.unpack('I', gz.read(4))
            # Read magic number.
            if n[0] != 0x1080000:
                raise Exception('Invalid file: unexpected magic number.')
            # Read number of entries.
            n = struct.unpack('>I', gz.read(4))
            if n[0] != cimg:
                raise Exception('Invalid file: expected {0} rows.'.format(cimg))
            # Read labels.
            res = np.fromstring(gz.read(cimg), dtype = np.uint8)
    finally:
        os.remove(gzfname)
    return res.reshape((cimg, 1))

def try_download(dataSrc, labelsSrc, cimg):
    data = loadData(dataSrc, cimg)
    labels = loadLabels(labelsSrc, cimg)
    return np.hstack((data, labels))

データのダウンロード

MNISTデータはトレーニングデータとテストデータから構成されている。 60000個のイメージデータと10000個のテストデータから構成されている。 まずはこのデータをダウンロードする。

# URLs for the train image and labels data
url_train_image = 'http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz'
url_train_labels = 'http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz'
num_train_samples = 60000

print("Downloading train data")
train = try_download(url_train_image, url_train_labels, num_train_samples)


url_test_image = 'http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz'
url_test_labels = 'http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz'
num_test_samples = 10000

print("Downloading test data")
test = try_download(url_test_image, url_test_labels, num_test_samples)
Downloading train data
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
Done.
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
Done.
Downloading test data
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
Done.
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz
Done.

データの可視化

# Plot a random image
sample_number = 5001
plt.imshow(train[sample_number,:-1].reshape(28,28), cmap="gray_r")
plt.axis('off')
print("Image Label: ", train[sample_number,-1])

f:id:msyksphinz:20170709223811p:plain

イメージの保存

MNISTのデータは、ベクトルにフラットに保存されている(28x28のイメージデータは、784個のデータポイントに変換されて保存されている)。

ラベルはワンホットデータに変換されている(10個の分類データで、3は0001000000として表現されている)

# Save the data files into a format compatible with CNTK text reader
def savetxt(filename, ndarray):
    dir = os.path.dirname(filename)

    if not os.path.exists(dir):
        os.makedirs(dir)

    if not os.path.isfile(filename):
        print("Saving", filename )
        with open(filename, 'w') as f:
            labels = list(map(' '.join, np.eye(10, dtype=np.uint).astype(str)))
            for row in ndarray:
                row_str = row.astype(str)
                label_str = labels[row[-1]]
                feature_str = ' '.join(row_str[:-1])
                f.write('|labels {} |features {}\n'.format(label_str, feature_str))
    else:
        print("File already exists", filename)
# Save the train and test files (prefer our default path for the data)
data_dir = os.path.join("..", "Examples", "Image", "DataSets", "MNIST")
if not os.path.exists(data_dir):
    data_dir = os.path.join("data", "MNIST")

print ('Writing train text file...')
savetxt(os.path.join(data_dir, "Train-28x28_cntk_text.txt"), train)

print ('Writing test text file...')
savetxt(os.path.join(data_dir, "Test-28x28_cntk_text.txt"), test)

print('Done')
Writing train text file...
File already exists data\MNIST\Train-28x28_cntk_text.txt
Writing test text file...
File already exists data\MNIST\Test-28x28_cntk_text.txt
Done
from IPython.display import Image

CNTK 103: Part-B - 回帰分析

CNTK 103: Part-C - マルチレイヤパーセプションによるMNIST

CNTK 103: Part-D - 畳み込みニューラルネットワークによるMNIST

まずはMNISTに必要なデータを用意ししている。手書きの文字を認識する。

# Figure 1
Image(url= "http://3.bp.blogspot.com/_UpN7DfJA0j4/TJtUBWPk0SI/AAAAAAAAABY/oWPMtmqJn3k/s1600/mnist_originals.png", width=200, height=200)

目的 MNISTのデータセットを使って、数字の認識分類器のトレーニングを実施する。

アプローチ チュートリアルで説明した同じような手順を踏む。データ読み込み、データ処理、モデルの生成、モデルパラメータの学習、モデルの評価である。

  • データ読み込み: CNTKのテキストリーダを使用する。
  • データのプリプロセッシング: Part-Aで紹介済み。

ロジスティック回帰

ロジスティック回帰(LR)は基本的な機械学習の手法であり、線形の重み分析を行い、各クラスについて確率に基づく予測を行う。

ロジスティック回帰には2種類が存在し、Binary LRは2つのクラスを予測する場合でも1つの出力を生成し、Multinomial LRは複数のクラスを予測する場合では複数の出力を生成する。

Image(url="https://camo.githubusercontent.com/2eb7e133489dbf4eb253c271b2688056be84dbe0/687474703a2f2f7777772e636e746b2e61692f6a75702f636e746b313033625f54776f466f726d734f664c522d76332e706e67")

# 関連するコンポーネントをロードする
from __future__ import print_function # Use a function definition from future version (say 3.x from 2.7 interpreter)
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
import numpy as np
import sys
import os

import cntk as C

%matplotlib inline
# Select the right target device when this notebook is being tested:
if 'TEST_DEVICE' in os.environ:
    if os.environ['TEST_DEVICE'] == 'cpu':
        C.device.try_set_default_device(C.device.cpu())
    else:
        C.device.try_set_default_device(C.device.gpu(0))

初期化

# 常に同じ乱数が生成できるように調整
np.random.seed(0)

# データ次元を定義する
input_dim = 784
num_output_classes = 10

データ読み込み

MNISTのデータは60000個のトレーニングセットを持っており、10000個のテストセットを持っている。

# テキストフォーマットのCTFを読み込む(ファイルからのCFTデシリアライザを読み込む)
def create_reader(path, is_training, input_dim, num_label_classes):
    
    labelStream = C.io.StreamDef(field='labels', shape=num_label_classes, is_sparse=False)
    featureStream = C.io.StreamDef(field='features', shape=input_dim, is_sparse=False)
    
    deserailizer = C.io.CTFDeserializer(path, C.io.StreamDefs(labels = labelStream, features = featureStream))
            
    return C.io.MinibatchSource(deserailizer,
       randomize = is_training, max_sweeps = C.io.INFINITELY_REPEAT if is_training else 1)
# トレーニングデータとテストデータの生成
# それぞれ配列を定義し格納する。
data_found = False

for data_dir in [os.path.join("..", "Examples", "Image", "DataSets", "MNIST"),
                 os.path.join("data", "MNIST")]:
    train_file = os.path.join(data_dir, "Train-28x28_cntk_text.txt")
    test_file = os.path.join(data_dir, "Test-28x28_cntk_text.txt")
    if os.path.isfile(train_file) and os.path.isfile(test_file):
        data_found = True
        break
        
if not data_found:
    raise ValueError("Please generate the data by completing CNTK 103 Part A")
    
print("Data directory is {0}".format(data_dir))
Data directory is data\MNIST

ロジスティック回帰のモデル生成

ロジスティック回帰(logistic regression: LR)ネットワークは機械学習の世界において過去10年で使われてきた、シンプルなビルディングブロックを使用した効率的なネットワークである。

Image(url="https://camo.githubusercontent.com/0fc29e9645a061453403af37d46750beb2d91356/68747470733a2f2f7777772e636e746b2e61692f6a75702f636e746b313033625f4d4e4953545f4c522e706e67")

input = C.input_variable(input_dim)
label = C.input_variable(num_output_classes)

def create_model(features):
    with C.layers.default_options(init = C.glorot_uniform()):
        r = C.layers.Dense(num_output_classes, activation = None)(features)
        return r

# Scale the input to 0-1 range by dividing each pixel by 255.
z = create_model(input/255.0)
# Define a utility function to compute the moving average sum.
# A more efficient implementation is possible with np.cumsum() function
def moving_average(a, w=5):
    if len(a) < w:
        return a[:]    # Need to send a copy of the array
    return [val if idx < w else sum(a[(idx-w):idx])/w for idx, val in enumerate(a)]


# Defines a utility that prints the training progress
def print_training_progress(trainer, mb, frequency, verbose=1):
    training_loss = "NA"
    eval_error = "NA"

    if mb%frequency == 0:
        training_loss = trainer.previous_minibatch_loss_average
        eval_error = trainer.previous_minibatch_evaluation_average
        if verbose: 
            print ("Minibatch: {0}, Loss: {1:.4f}, Error: {2:.2f}%".format(mb, training_loss, eval_error*100))
        
    return mb, training_loss, eval_error
# ネットワークの初期化を行う
minibatch_size = 64
num_samples_per_sweep = 60000
num_sweeps_to_train_with = 10
num_minibatches_to_train = (num_samples_per_sweep * num_sweeps_to_train_with) / minibatch_size

loss = C.cross_entropy_with_softmax(z, label)
label_error = C.classification_error(z, label)

# モデルトレーニングを駆動させるためのトレーナをインスタンスする
learning_rate = 0.2
lr_schedule = C.learning_rate_schedule(learning_rate, C.UnitType.minibatch)
learner = C.sgd(z.parameters, lr_schedule)
trainer = C.Trainer(z, (loss, label_error), [learner])

# トレーニングデータセットを生成する
reader_train = create_reader(train_file, True, input_dim, num_output_classes)

# データストリームを入力とラベルにマップする
input_map = {
    label  : reader_train.streams.labels,
    input  : reader_train.streams.features
} 

# トレーニングを実行する
training_progress_output_freq = 500

plotdata = {"batchsize":[], "loss":[], "error":[]}

for i in range(0, int(num_minibatches_to_train)):
    
    # トレーニングデータファイルにミニバッチを適用する
    data = reader_train.next_minibatch(minibatch_size, input_map = input_map)
    
    trainer.train_minibatch(data)
    batchsize, loss, error = print_training_progress(trainer, i, training_progress_output_freq, verbose=1)
    
    if not (loss == "NA" or error =="NA"):
        plotdata["batchsize"].append(batchsize)
        plotdata["loss"].append(loss)
        plotdata["error"].append(error)
Minibatch: 0, Loss: 2.2132, Error: 79.69%
Minibatch: 500, Loss: 0.5081, Error: 12.50%
Minibatch: 1000, Loss: 0.2309, Error: 4.69%
Minibatch: 1500, Loss: 0.4300, Error: 15.62%
Minibatch: 2000, Loss: 0.1918, Error: 6.25%
Minibatch: 2500, Loss: 0.1843, Error: 7.81%
Minibatch: 3000, Loss: 0.1117, Error: 1.56%
Minibatch: 3500, Loss: 0.3094, Error: 12.50%
Minibatch: 4000, Loss: 0.3554, Error: 10.94%
Minibatch: 4500, Loss: 0.2465, Error: 4.69%
Minibatch: 5000, Loss: 0.1988, Error: 3.12%
Minibatch: 5500, Loss: 0.1277, Error: 4.69%
Minibatch: 6000, Loss: 0.1448, Error: 3.12%
Minibatch: 6500, Loss: 0.2789, Error: 9.38%
Minibatch: 7000, Loss: 0.1692, Error: 6.25%
Minibatch: 7500, Loss: 0.3069, Error: 9.38%
Minibatch: 8000, Loss: 0.1194, Error: 3.12%
Minibatch: 8500, Loss: 0.1464, Error: 4.69%
Minibatch: 9000, Loss: 0.1096, Error: 3.12%
# Compute the moving average loss to smooth out the noise in SGD
plotdata["avgloss"] = moving_average(plotdata["loss"])
plotdata["avgerror"] = moving_average(plotdata["error"])

# Plot the training loss and the training error
import matplotlib.pyplot as plt

plt.figure(1)
plt.subplot(211)
plt.plot(plotdata["batchsize"], plotdata["avgloss"], 'b--')
plt.xlabel('Minibatch number')
plt.ylabel('Loss')
plt.title('Minibatch run vs. Training loss')

plt.show()

plt.subplot(212)
plt.plot(plotdata["batchsize"], plotdata["avgerror"], 'r--')
plt.xlabel('Minibatch number')
plt.ylabel('Label Prediction Error')
plt.title('Minibatch run vs. Label Prediction Error')
plt.show()

f:id:msyksphinz:20170709223851p:plain

f:id:msyksphinz:20170709223857p:plain

ロジスティック回帰の評価 / テスト

トレーニングの完了したネットワークで、テストを実施する。

# トレーニングデータを読み込む
reader_test = create_reader(test_file, False, input_dim, num_output_classes)

test_input_map = {
    label  : reader_test.streams.labels,
    input  : reader_test.streams.features,
}

# テストデータを読み込む
test_minibatch_size = 512
num_samples = 10000
num_minibatches_to_test = num_samples // test_minibatch_size
test_result = 0.0

for i in range(num_minibatches_to_test):
    
    # test_minibatch_size のバッチサイズでテストデータをロードする。
    # 784ピクセルのMNISTデータをロードする
    data = reader_test.next_minibatch(test_minibatch_size,
                                      input_map = test_input_map)

    eval_error = trainer.test_minibatch(data)
    test_result = test_result + eval_error

# 全てのミニバッチテストに対して平均エラー率を計算する
print("Average test error: {0:.2f}%".format(test_result*100 / num_minibatches_to_test))
Average test error: 7.41%

マルチレイヤパーセプションのモデル生成

マルチレイヤパーセプションは2レイヤの隠しレイヤ(num_hidden_layers)の、比較的シンプルなネットワークである。

Image(url="https://camo.githubusercontent.com/173a509c6aab581dc9f2e872f56a349883b1776d/687474703a2f2f636e746b2e61692f6a75702f636e746b313033635f4d4e4953545f4d4c502e706e67")

num_hidden_layers = 2
hidden_layers_dim = 400

input = C.input_variable(input_dim)
label = C.input_variable(num_output_classes)

def create_model(features):
    with C.layers.default_options(init = C.layers.glorot_uniform(), activation = C.ops.relu):
            h = features
            for _ in range(num_hidden_layers):
                h = C.layers.Dense(hidden_layers_dim)(h)
            r = C.layers.Dense(num_output_classes, activation = None)(h)
            return r
        
z = create_model(input)

# Scale the input to 0-1 range by dividing each pixel by 255.
z = create_model(input/255.0)
loss = C.cross_entropy_with_softmax(z, label)
label_error = C.classification_error(z, label)

# Instantiate the trainer object to drive the model training
learning_rate = 0.2
lr_schedule = C.learning_rate_schedule(learning_rate, C.UnitType.minibatch)
learner = C.sgd(z.parameters, lr_schedule)
trainer = C.Trainer(z, (loss, label_error), [learner])

# Initialize the parameters for the trainer
minibatch_size = 64
num_samples_per_sweep = 60000
num_sweeps_to_train_with = 10
num_minibatches_to_train = (num_samples_per_sweep * num_sweeps_to_train_with) / minibatch_size

# Create the reader to training data set
reader_train = create_reader(train_file, True, input_dim, num_output_classes)

# Map the data streams to the input and labels.
input_map = {
    label  : reader_train.streams.labels,
    input  : reader_train.streams.features
} 

# Run the trainer on and perform model training
training_progress_output_freq = 500

plotdata = {"batchsize":[], "loss":[], "error":[]}

for i in range(0, int(num_minibatches_to_train)):
    
    # Read a mini batch from the training data file
    data = reader_train.next_minibatch(minibatch_size, input_map = input_map)
    
    trainer.train_minibatch(data)
    batchsize, loss, error = print_training_progress(trainer, i, training_progress_output_freq, verbose=1)
    
    if not (loss == "NA" or error =="NA"):
        plotdata["batchsize"].append(batchsize)
        plotdata["loss"].append(loss)
        plotdata["error"].append(error)
Minibatch: 0, Loss: 2.3329, Error: 95.31%
Minibatch: 500, Loss: 0.2762, Error: 7.81%
Minibatch: 1000, Loss: 0.0661, Error: 1.56%
Minibatch: 1500, Loss: 0.1294, Error: 6.25%
Minibatch: 2000, Loss: nan, Error: 92.19%
Minibatch: 2500, Loss: nan, Error: 93.75%
Minibatch: 3000, Loss: nan, Error: 90.62%
Minibatch: 3500, Loss: nan, Error: 85.94%
Minibatch: 4000, Loss: nan, Error: 92.19%
Minibatch: 4500, Loss: nan, Error: 90.62%
Minibatch: 5000, Loss: nan, Error: 95.31%
Minibatch: 5500, Loss: nan, Error: 84.38%
Minibatch: 6000, Loss: nan, Error: 87.50%
Minibatch: 6500, Loss: nan, Error: 87.50%
Minibatch: 7000, Loss: nan, Error: 82.81%
Minibatch: 7500, Loss: nan, Error: 95.31%
Minibatch: 8000, Loss: nan, Error: 89.06%
Minibatch: 8500, Loss: nan, Error: 96.88%
Minibatch: 9000, Loss: nan, Error: 92.19%
# Compute the moving average loss to smooth out the noise in SGD
plotdata["avgloss"] = moving_average(plotdata["loss"])
plotdata["avgerror"] = moving_average(plotdata["error"])

# Plot the training loss and the training error
import matplotlib.pyplot as plt

plt.figure(1)
plt.subplot(211)
plt.plot(plotdata["batchsize"], plotdata["avgloss"], 'b--')
plt.xlabel('Minibatch number')
plt.ylabel('Loss')
plt.title('Minibatch run vs. Training loss')

plt.show()

plt.subplot(212)
plt.plot(plotdata["batchsize"], plotdata["avgerror"], 'r--')
plt.xlabel('Minibatch number')
plt.ylabel('Label Prediction Error')
plt.title('Minibatch run vs. Label Prediction Error')
plt.show()

f:id:msyksphinz:20170709223912p:plain

f:id:msyksphinz:20170709223915p:plain

マルチレイヤパーセプションの評価/テスト

# トレーニングデータの読み込み
reader_test = create_reader(test_file, False, input_dim, num_output_classes)

test_input_map = {
    label  : reader_test.streams.labels,
    input  : reader_test.streams.features,
}

# トレーニングのモデル読み込み
test_minibatch_size = 512
num_samples = 10000
num_minibatches_to_test = num_samples // test_minibatch_size
test_result = 0.0

for i in range(num_minibatches_to_test):
    
    # We are loading test data in batches specified by test_minibatch_size
    # Each data point in the minibatch is a MNIST digit image of 784 dimensions 
    # with one pixel per dimension that we will encode / decode with the 
    # trained model.
    data = reader_test.next_minibatch(test_minibatch_size,
                                      input_map = test_input_map)

    eval_error = trainer.test_minibatch(data)
    test_result = test_result + eval_error

# Average of evaluation errors of all test minibatches
print("Average test error: {0:.2f}%".format(test_result*100 / num_minibatches_to_test))
Average test error: 90.20%

畳み込みニューラルネットワークのモデル生成

入力次元

畳み込みニューラルネットワークの入力データは3Dマトリックス(チャネル,画像の幅,画像の高さ)にて構成される

# 乱数をシードを固定して再現性を持たせる
np.random.seed(0)

# データの次元を定義する
input_dim_model = (1, 28, 28)    # 28 x 28 with 1 チャネルの画像を定義する
input_dim = 28*28                # 画像データをベクトルとして定義する
num_output_classes = 10

CNNは複数のレイヤから構成されるネットワークであり、あるレイヤの出力が次のレイヤの入力になるようなネットワークである。 これはマルチレイヤパーセプション(Multi Layer Perception: MLP)と似ている。 MLPはすべてのピクセルの入力ペアを出力をすべて接続されている。したがって、組み合わせ爆発が発生するのと、オーバフィッティングが発生しやすい。 畳み込みレイヤは、ピクセルの空間は位置関係を活用して、ネットワーク中のパラメータの数を減らすことができる。 フィルタのサイズは畳み込みレイヤのパラメータである。

畳み込みレイヤ

畳み込みレイヤはフィルタの集合である。各フィルタは重み \bf W と バイアス bから構成される。 これらのフィルタは画像内をスキャンし、画像とのドットプロダクトを計算し、バイアスを加算する。ドットプロダクトの値を活性化関数に渡す。

Image(url="https://www.cntk.ai/jup/cntk103d_conv2d_final.gif", width= 300)

畳み込みレイヤには以下の特徴がある。

  • 入力と出力が完全接続されたレイヤと異なり、畳み込みノードは入力ノードの一部とその隣接するローカルノードでのみ接続される。これはreceptive field(RF)と呼ばれる。上図では3x3の領域がRF領域である。RGBの場合には、画像は3つの3×3の領域があり、3つの色チャネルのそれぞれが1つ存在する。

  • Denseレイヤのような単一の重みレイヤの代わりに、畳み込みレイヤは複数の重みの集合を持っている(上図ではそれぞれ別の色で表現されている)。これをフィルタと呼ぶ。各フィルタは入力画像の受信フィールドの特徴を検出する。畳み込みの出力はサブレイヤの一部である(下記のアニメーションに示す)。ここでnはフィルタの数である。

  • サブレイヤの中では、各ノードが自分自身の重み情報を持つ代わりに、サブレイヤ中では同じ重み情報が利用される。これによりパラメータの数を減らし、学習のオーバフィッティングを防いでいる。この軽量化により、ディープラーニングのさまざな実際の問題で問題を解決可能にしている:

  • より大きな画像の処理 (512 x 512など)
  • より大きなフィルタサイズを使用する (11 x 11など)
  • より多くのフィルタを学習させる(128など)
  • よりレイヤ数の深いアーキテクチャを実現する(100+のレイヤなど)
  • 変換の不変性を達成する(画像内のどの領域にあっても認識することができるような能力)

Strides and Pad parameters

ストライドとPadパラメータ

フィルタをどのように配置するのか? 一般的に、フィルタはおーばラップしたタイルのように配置され、左から右方向へ、上から下へと進んでいく。 各畳み込みレイヤはfilter_shapeを指定するパラメータを持っており、ほとんど自然な画像の場合にフィルタの幅と高さを指定するパラメータを持っている。また、フィルタが画像内を移動する場合にどの程度のステップで移動するかを指定するためのパラメータ(stride)が存在する。 booleanのパラメータであるpadは入力値が境界線の近くでのタイルどりを可能にするために、入力がパディングされるかどうかを制御する。

上記のアニメーションではfilter_shape = (3, 3), strides = (2, 2), pad = Falseの場合の処理を表現している。 次のアニメーションでは、padTrueに設定している。 最初のアニメーションではstrideは2だが、次のアニメーションではstrideは1である。 ここで、出力値の形(最も下のレイヤ)はそれぞれのストライドの設定により異なっている。 padおよびstrideの値の決定は、出力レイヤの必要な形により決定される。

# Plot images with strides of 2 and 1 with padding turned on
images = [("https://www.cntk.ai/jup/cntk103d_padding_strides.gif" , 'With stride = 2'),
          ("https://www.cntk.ai/jup/cntk103d_same_padding_no_strides.gif", 'With stride = 1')]

for im in images:
    print(im[1])
    display(Image(url=im[0], width=200, height=200))
With stride = 2

With stride = 1

CNNモデルの構築

本CNNのチュートリアルでは、最初に2つのコンテナを定義する。 1つ目のコンテナはMNIST画像を入力するためのもので、2つ目のものは10個の数字へのラベリングを行うためのものである。 データを読み込んだ時、readerは自動的に1画像あたり784ピクセルinput_dim_modelのタプルへと変換する (この例では (1, 28, 28)

x = C.input_variable(input_dim_model)
y = C.input_variable(num_output_classes)

最初のモデルは、単純な畳み込みのみのモデルである。 ここでは2つの畳み込みモデルを持っており、MNISTの10個の数値を検出することが目的であるから、幅10のベクトルがネットワークの出力であり、各ベクトルの要素が各数字に相当する。 これは、num_output_classのdenseレイヤの出力を使って、最後の畳み込み層の出力を反映することで可能となる。 このチュートリアルの前に、ロジスティック回帰およびMLPでは最後のレイヤでクラスの数分だけマッピングされることを見た。 また、トレーニング中にcross entropy損失関数を使用したsoftmax関数を利用することにより、最後のdenseレイヤは活性化関数を持っていない。

以下の図は我々が構築しようとしているネットワークのモデルである。 このモデル内のパラメータは実験に使用されることに注意すること。 これらはネットワークのハイパーパラメータと呼ばれる。 フィルタの形が増大すると、モデルパラメータの数も増え、計算時間も増大し、モデルがデータにフィットすることを助ける。 しかし、オーバフィッティングのリスクもある。 典型的に、より深い層のレイヤのフィルタの数はそれより前のレイヤのフィルタよりも大きい。 私たちは最初のレイヤで8、次のレイヤで16のフィルタを選択した。 これらのハイパーパラメータはモデル構築中に実験されるべきである。

Image(url="https://camo.githubusercontent.com/a157ed88853727d46ecbdc11fa0dcc868804c247/68747470733a2f2f7777772e636e746b2e61692f6a75702f636e746b313033645f636f6e766f6e6c79322e706e67")

x = C.input_variable(input_dim_model)
y = C.input_variable(num_output_classes)

# function to モデルの構築

def create_model(features):
    with C.layers.default_options(init=C.glorot_uniform(), activation=C.relu):
            h = features
            h = C.layers.Convolution2D(filter_shape=(5,5), 
                                       num_filters=8, 
                                       strides=(2,2), 
                                       pad=True, name='first_conv')(h)
            h = C.layers.Convolution2D(filter_shape=(5,5), 
                                       num_filters=16, 
                                       strides=(2,2), 
                                       pad=True, name='second_conv')(h)
            r = C.layers.Dense(num_output_classes, activation=None, name='classify')(h)
            return r
        
# モデルのインスタンス
z = create_model(x)

# ネットワークの出力形式をプリントする。
print("Output Shape of the first convolution layer:", z.first_conv.shape)
print("Bias value of the last dense layer:", z.classify.b.value)

# ネットワーク中のパラメータ
C.logging.log_number_of_parameters(z)
Output Shape of the first convolution layer: (8, 14, 14)
Bias value of the last dense layer: [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
Training 11274 parameters in 6 parameter tensors.

モデルパラメータの学習

前回のチュートリアルと同様に、softmax関数を使用してevidenceもしくは活性化された値を確率に変換する。

トレーニング

CNTK 102と同様に、cross-entropyを最小化することを目的とする。 最終的な値が想定と異なるならば、CNTK 102を再度見てほしい。 複数のモデルを効率的にビルドするために、いくつかのサポート関数を用意している。

def create_criterion_function(model, labels):
    loss = C.cross_entropy_with_softmax(model, labels)
    errs = C.classification_error(model, labels)
    return loss, errs # (model, labels) -> (loss, error metric)

def train_test(train_reader, test_reader, model_func, num_sweeps_to_train_with=10):
    
    # Instantiate the model function; x is the input (feature) variable 
    # We will scale the input image pixels within 0-1 range by dividing all input value by 255.
    model = model_func(x/255)
    
    # Instantiate the loss and error function
    loss, label_error = create_criterion_function(model, y)
    
    # Instantiate the trainer object to drive the model training
    learning_rate = 0.2
    lr_schedule = C.learning_rate_schedule(learning_rate, C.UnitType.minibatch)
    learner = C.sgd(z.parameters, lr_schedule)
    trainer = C.Trainer(z, (loss, label_error), [learner])
    
    # Initialize the parameters for the trainer
    minibatch_size = 64
    num_samples_per_sweep = 60000
    num_minibatches_to_train = (num_samples_per_sweep * num_sweeps_to_train_with) / minibatch_size
    
    # Map the data streams to the input and labels.
    input_map={
        y  : train_reader.streams.labels,
        x  : train_reader.streams.features
    } 
    
    # Uncomment below for more detailed logging
    training_progress_output_freq = 500
     
    # Start a timer
    start = time.time()

    for i in range(0, int(num_minibatches_to_train)):
        # Read a mini batch from the training data file
        data=train_reader.next_minibatch(minibatch_size, input_map=input_map) 
        trainer.train_minibatch(data)
        print_training_progress(trainer, i, training_progress_output_freq, verbose=1)
     
    # Print training time
    print("Training took {:.1f} sec".format(time.time() - start))
    
    # Test the model
    test_input_map = {
        y  : test_reader.streams.labels,
        x  : test_reader.streams.features
    }

    # Test data for trained model
    test_minibatch_size = 512
    num_samples = 10000
    num_minibatches_to_test = num_samples // test_minibatch_size

    test_result = 0.0   

    for i in range(num_minibatches_to_test):
    
        # We are loading test data in batches specified by test_minibatch_size
        # Each data point in the minibatch is a MNIST digit image of 784 dimensions 
        # with one pixel per dimension that we will encode / decode with the 
        # trained model.
        data = test_reader.next_minibatch(test_minibatch_size, input_map=test_input_map)
        eval_error = trainer.test_minibatch(data)
        test_result = test_result + eval_error

    # Average of evaluation errors of all test minibatches
    print("Average test error: {0:.2f}%".format(test_result*100 / num_minibatches_to_test))

トレーニングの実行とテストの実施

これで、畳み込みニューラルネットワークの構築が完了した。

def do_train_test():
    global z
    z = create_model(x)
    reader_train = create_reader(train_file, True, input_dim, num_output_classes)
    reader_test = create_reader(test_file, False, input_dim, num_output_classes)
    train_test(reader_train, reader_test, z)
    
do_train_test()
Minibatch: 0, Loss: 2.3157, Error: 96.88%
Minibatch: 500, Loss: 0.1691, Error: 9.38%
Minibatch: 1000, Loss: 0.0890, Error: 1.56%
Minibatch: 1500, Loss: 0.1083, Error: 3.12%
Minibatch: 2000, Loss: 0.0115, Error: 0.00%
Minibatch: 2500, Loss: 0.0058, Error: 0.00%
Minibatch: 3000, Loss: 0.0049, Error: 0.00%
Minibatch: 3500, Loss: 0.0750, Error: 3.12%
Minibatch: 4000, Loss: 0.0220, Error: 0.00%
Minibatch: 4500, Loss: 0.0192, Error: 1.56%
Minibatch: 5000, Loss: 0.0101, Error: 0.00%
Minibatch: 5500, Loss: 0.0014, Error: 0.00%
Minibatch: 6000, Loss: 0.0048, Error: 0.00%
Minibatch: 6500, Loss: 0.0098, Error: 0.00%
Minibatch: 7000, Loss: 0.0079, Error: 0.00%
Minibatch: 7500, Loss: 0.0038, Error: 0.00%
Minibatch: 8000, Loss: 0.0139, Error: 0.00%
Minibatch: 8500, Loss: 0.0058, Error: 0.00%
Minibatch: 9000, Loss: 0.0148, Error: 0.00%
Training took 17.5 sec
Average test error: 1.50%
print("Bias value of the last dense layer:", z.classify.b.value)
Bias value of the last dense layer: [-0.02818329 -0.08973262  0.02727109 -0.14188322  0.0115815  -0.09806534
  0.00059815 -0.09239398  0.39416206  0.01649885]
out = C.softmax(z)
# Read the data for evaluation
reader_eval=create_reader(test_file, False, input_dim, num_output_classes)

eval_minibatch_size = 25
eval_input_map = {x: reader_eval.streams.features, y:reader_eval.streams.labels} 

data = reader_eval.next_minibatch(eval_minibatch_size, input_map=eval_input_map)

img_label = data[y].asarray()
img_data = data[x].asarray()

# reshape img_data to: M x 1 x 28 x 28 to be compatible with model
img_data = np.reshape(img_data, (eval_minibatch_size, 1, 28, 28))

predicted_label_prob = [out.eval(img_data[i]) for i in range(len(img_data))]
# Find the index with the maximum value for both predicted as well as the ground truth
pred = [np.argmax(predicted_label_prob[i]) for i in range(len(predicted_label_prob))]
gtlabel = [np.argmax(img_label[i]) for i in range(len(img_label))]
print("Label    :", gtlabel[:25])
print("Predicted:", pred)
Label    : [7, 2, 1, 0, 4, 1, 4, 9, 5, 9, 0, 6, 9, 0, 1, 5, 9, 7, 3, 4, 9, 6, 6, 5, 4]
Predicted: [7, 2, 1, 0, 4, 1, 4, 9, 5, 9, 0, 6, 9, 0, 1, 5, 9, 7, 3, 4, 9, 6, 6, 5, 4]
# Plot a random image
sample_number = 5
plt.imshow(img_data[sample_number].reshape(28,28), cmap="gray_r")
plt.axis('off')

img_gt, img_pred = gtlabel[sample_number], pred[sample_number]
print("Image Label: ", img_pred)

f:id:msyksphinz:20170709223929p:plain

プーリングレイヤ

多くの場合に、特にディープネットワークを制御する場合にはパラメータの数を制御する必要がある。 全ての畳み込みレイヤの出力(各レイヤ、フィルタの出力に相当する)では、プーリングレイヤを配置することができる。 プーリングレイヤの典型的な特徴として、

  • 前のレイヤの次元数を削減する(ネットワークを高速化する)
  • 画像内のオブジェクトの位置ずれに対しても対応できるネットワークを構築する。例えば、もし画像の位置が中央ではなく横にずれていても、分類器が分類処理を問題なく実行できるようにする。

プーリングノードの計算は、通常のフィードフォワード処理よりも簡単である。 プーリングノードには重みも、バイアスも活性化関数も存在しない。 プーリングノードは簡単な集合関数(max, 平均など)を使用して出力を計算する。 最も共通して使用されるのはmaxである。入力のフィルタ位置に対応する入力値の最大値を出力する。

以下の図は4x4の領域を持つ入力値である。 maxプーリングウィンドウサイズは2x2であり、左上のこーなから処理を開始する。 ウィンドウ内の最大値がその領域の出力値となる。 モデルは指定されたストライドパラメータでシフトしていき、maxのプーリング操作が繰り返される。

もう一つのプーリング関数は"average"である。 averageはmaxの代わりに使用される。2つのプーリング処理をそれぞれアニメーションでまとめた。

# Plot images with strides of 2 and 1 with padding turned on
images = [("https://www.cntk.ai/jup/c103d_max_pooling.gif" , 'Max pooling'),
          ("https://www.cntk.ai/jup/c103d_average_pooling.gif", 'Average pooling')]

for im in images:
    print(im[1])
    display(Image(url=im[0], width=200, height=200))
Max pooling

Average pooling

典型的な畳み込みネットワーク

典型的なCNNは複数の畳み込みレイヤとプーリングレイヤで構成されており、最後にdense出力レイヤが接続されている。 多くの分類器のディープネットワーク(VGG, AlexNetなど)のバリエーションを見つけることができるだろう。 CNTK_103Cで使用したMLPネットワークは対照的に、2つのdenseレイヤで構成されており、最後にdense出力レイヤが接続されている。

説明の図では2次元の画像処理のネットワークとして表現しているが、CNTKのコンポーネントはどのような次元のデータでも取り扱うことができる。 上位の根とワークでは2つの畳み込みレイヤと2つのmax-プーリングレイヤで構成されている。 典型的な戦略としては、より深いレイヤになるにつれてフィルタの数を増やし、中間レイヤの空間的なサイズを減らすことである。

タスク: MaxPoolingを利用したネットワークの構築

CNTKのMaxPooling関数を使用してこのタスクを実現する。create_model関数を以下のように変更して、MaxPooling操作を追加する。

# function to build model
def create_model(features):
    with C.layers.default_options(init = C.layers.glorot_uniform(), activation = C.relu):
            h = features
            
            h = C.layers.Convolution2D(filter_shape=(5,5), 
                                       num_filters=8, 
                                       strides=(1,1), 
                                       pad=True, name="first_conv")(h)
            h = C.layers.MaxPooling(filter_shape=(2,2), 
                                    strides=(2,2), name="first_max")(h)
            h = C.layers.Convolution2D(filter_shape=(5,5), 
                                       num_filters=16, 
                                       strides=(1,1), 
                                       pad=True, name="second_conv")(h)
            h = C.layers.MaxPooling(filter_shape=(3,3), 
                                    strides=(3,3), name="second_max")(h)
            r = C.layers.Dense(num_output_classes, activation = None, name="classify")(h)
            return r
        
do_train_test()
Minibatch: 0, Loss: 2.3284, Error: 95.31%
Minibatch: 500, Loss: 0.1598, Error: 7.81%
Minibatch: 1000, Loss: 0.0865, Error: 3.12%
Minibatch: 1500, Loss: 0.1355, Error: 3.12%
Minibatch: 2000, Loss: 0.0236, Error: 1.56%
Minibatch: 2500, Loss: 0.0036, Error: 0.00%
Minibatch: 3000, Loss: 0.0031, Error: 0.00%
Minibatch: 3500, Loss: 0.0744, Error: 3.12%
Minibatch: 4000, Loss: 0.0186, Error: 1.56%
Minibatch: 4500, Loss: 0.0503, Error: 1.56%
Minibatch: 5000, Loss: 0.0300, Error: 1.56%
Minibatch: 5500, Loss: 0.0176, Error: 0.00%
Minibatch: 6000, Loss: 0.0137, Error: 0.00%
Minibatch: 6500, Loss: 0.0308, Error: 1.56%
Minibatch: 7000, Loss: 0.0204, Error: 1.56%
Minibatch: 7500, Loss: 0.0040, Error: 0.00%
Minibatch: 8000, Loss: 0.0046, Error: 0.00%
Minibatch: 8500, Loss: 0.0363, Error: 3.12%
Minibatch: 9000, Loss: 0.0049, Error: 0.00%
Training took 18.4 sec
Average test error: 1.15%