IT技術者のAIに関する雑記

AI関連のMicrosoft技術やデータサイエンスの話をメインに。

Cognitive Toolkitを使ってみる ~ 深層学習を数値データの二値分類から初めてみよう

深層学習の入門としては、画像識別アルゴリズム実験用の画像データセットとして公開されているMNISTデータセット(http://yann.lecun.com/exdb/mnist/)を使った画像識別が、いわゆるプログラミング言語Hello Worldに相当するものとされています。

ただし、二次元数値データの二値分類を深層学習で行い、その動作を確認してみることは、深層学習を始めるうえでのより簡単な一歩になると思います。深層学習の最初の一歩をここから始め、その後に画像識別や言語識別のような、より深層学習の力が発揮できる領域に入ってみてはいかがでしょうか。

ここでは深層学習の最初の一歩として、Microsoft Cognitive ToolkitのPython APIを使った順伝播型ネットワーク(feed forward network)による二値分類のチュートリアルを実行してみます。使用するチュートリアルは以下になります。

CNTK 102: Feed Forward Network with Simulated Data:
https://github.com/Microsoft/CNTK/blob/v2.0.rc2/Tutorials/CNTK_102_FeedForward.ipynb

シナリオ

被験者の年齢と腫瘍の大きさから、良性腫瘍と悪性腫瘍を深層学習にて分類します。データはプログラムで生成したシミュレーションデータとします。

必要なモジュールのインポート

以下のモジュールをインポートしておきます。なお、「%matplotlib inline」はJupyter Notebookを使用する場合にのみ必要です。

from __future__ import print_function
import matplotlib.pyplot as plt
%matplotlib inline

import numpy as np
import sys
import os

import cntk as C
from cntk import Trainer, learning_rate_schedule, UnitType, sgd
from cntk.initializer import glorot_uniform
from cntk.layers import default_options, Dense

バイスの設定

プログラムを実行するデバイス(CPUもしくはGPU)を指定します。オプションとして、GPUをロックすることもできます(デフォルトはOFFです)。

if 'TEST_DEVICE' in os.environ:
    import cntk
    if os.environ['TEST_DEVICE'] == 'cpu':
        cntk.device.try_set_default_device(cntk.device.cpu())
    else:
        cntk.device.try_set_default_device(cntk.device.gpu(0))

シミュレーションデータの生成

二次元データの二値分類を行うため、特徴数(input_dim)として2、ラベル数(num_output_classes)として2を指定します。なお、同じ乱数を発生するようにseedを固定します。

np.random.seed(0)

input_dim = 2
num_output_classes = 2


シミュレーションデータを生成する関数を定義します。

def generate_random_data_sample(sample_size, feature_dim, num_classes): 
    Y = np.random.randint(size=(sample_size, 1), low=0, high=num_classes)

    X = (np.random.randn(sample_size, feature_dim)+3) * (Y+1)
    X = X.astype(np.float32)    

    class_ind = [Y==class_number for class_number in range(num_classes)]
    Y = np.asarray(np.hstack(class_ind), dtype=np.float32)

    return X, Y


サンプル数を64として、シミュレーションデータを生成します。

mysamplesize = 64
features, labels = generate_random_data_sample(mysamplesize, input_dim, num_output_classes)


シミュレーションデータを可視化します。X軸を被験者の年齢、Y軸を腫瘍の大きさとし、悪性腫瘍を赤点、良性腫瘍を青点としています。

colors = ['r' if l == 0 else 'b' for l in labels[:,0]]

plt.scatter(features[:,0], features[:,1], c=colors)
plt.xlabel("Scaled age (in yrs)")
plt.ylabel("Tumor size (in cm)")
plt.show()


ここまでのプログラムを実行すると、以下のようなグラフが表示されます。
f:id:masakykato:20170509185143p:plain

このプログラムにより、どのようなシミュレーションデータが生成されるかが把握できるかと思います。

順伝播型ネットワークモデルの作成

このチュートリアルでは、フルコネクトの順伝播型ネットワークを使用します。フルコネクトとは、下図のようにすべての隣接層のノードが接続された(つまり、前層のすべてのノードが次層の各ノードの入力になる)ネットワークのことであり、順伝播型とは情報が入力側から出力側に一方向にのみ伝播するネットワークのことです。

f:id:masakykato:20170510111801p:plain:w300


隠れ層の数(num_hidden_layers)を2、各隠れ層のノード数(hidden_layers_dim)を50とします。

num_hidden_layers = 2
hidden_layers_dim = 50


ネットワークへの入力変数(特徴やラベル)を定義します。Cognitive Toolkitでは、モデルの訓練やモデルのテストで使用する様々な観測値を、ネットワークへの入力変数に格納します。したがって、入力変数の次元は使用する訓練データの次元と一致します。訓練データは「被験者の年齢」と「腫瘍の大きさ」という次元を持つため、次元数は2になります。

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


ここからは順伝播型のニューラルネットワークモデルを作成していきます。

順伝播型ネットワークでは、n+1番目の層のノードの出力{ \displaystyle \boldsymbol{z^{(n+1)}}}はn番目の層のノードの出力{ \displaystyle \boldsymbol{z^{(n)}}}から
{ \displaystyle \boldsymbol{u^{(n+1)}=W^{(n+1)}z^{(n)}+b^{(n+1)}}}
{ \displaystyle \boldsymbol{z^{(n+1)}=f(u^{(n+1)})}}

で計算されます。ここで、{ \displaystyle \boldsymbol{W}}は重み、{ \displaystyle \boldsymbol{b}}はバイアス、{ \displaystyle \boldsymbol{f}}は活性化関数です。活性化関数としてはシグモイド関数や双曲線正接関数がよく使われます。

活性化関数としてシグモイド関数を使った場合、最終的な出力は{ \displaystyle \boldsymbol{u^{(n+1)}=W^{(n+1)}z^{(n)}+b^{(n+1)}}}として

def linear_layer(input_var, output_dim):
    input_dim = input_var.shape[0]
    
    weight = C.parameter(shape=(input_dim, output_dim))
    bias = C.parameter(shape=(output_dim))

    return bias + C.times(input_var, weight)


{ \displaystyle \boldsymbol{z^{(n+1)}=f(u^{(n+1)})}}として

def dense_layer(input_var, output_dim, nonlinearity):
    l = linear_layer(input_var, output_dim)
    
    return nonlinearity(l)


とすることにより、

def fully_connected_classifier_net(input_var, num_output_classes, hidden_layer_dim, 
                                   num_hidden_layers, nonlinearity):
    
    h = dense_layer(input_var, hidden_layer_dim, nonlinearity)
    for i in range(1, num_hidden_layers):
        h = dense_layer(h, hidden_layer_dim, nonlinearity)
    
    return linear_layer(h, num_output_classes)

z = fully_connected_classifier_net(input, num_output_classes, hidden_layers_dim, 
                                   num_hidden_layers, C.sigmoid)


と記述できます。

このネットワークモデルは、Cognitive ToolkitのLayer Libraryを使うと、以下のように、より簡単に記述できます。

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


ここでは、デフォルトのオプションで、重みとバイアスのイニシャライザとしてGlorotイニシャライザ(fanIn+fanOutでスケーリングした一様分布)を指定し、活性化関数としてシグモイド関数を指定しています。

ネットワークモデルの訓練の準備

順伝播型ネットワークモデルの訓練では、交差エントロピーを誤差関数とし、重み{ \displaystyle \boldsymbol{W}}とバイアス{ \displaystyle \boldsymbol{b}}に対して最小化させます。

交差エントロピーは、最終層の出力に対してソフトマックス関数で予測確率{ \displaystyle \boldsymbol{p}}
{ \displaystyle \boldsymbol{p = softmax(z)}}

と定義することで、以下のように計算できます。
{ \displaystyle \boldsymbol{H(p)= - \displaystyle{\Sigma_{j=1}^{C}}y_i log(p_j)}}

ここで、{ \displaystyle \boldsymbol{C}}はラベルの次元数、{ \displaystyle \boldsymbol{y_i}}はラベルを表しています。

Cognitive Toolkitは交差エントロピーを計算するパッケージを持っているため、ネットワークの出力と訓練データのラベルを引数にして記述できます。

loss = C.cross_entropy_with_softmax(z, label)


同じく、ネットワークの出力に対する誤差も、ネットワークの出力と訓練データのラベルを引数にして記述できます。

eval_error = C.classification_error(z, label)


確率的勾配降下法(SGD: Stochastic Gradient Descent)は誤算関数を最小化させる最も一般的な手法です。ここではSGDを使用して、ネットワークモデルの訓練を行います。確率的勾配降下法では、重みとバイアスの初期値(ここではGlorotイニシャライザにより初期化)から始め、訓練データごとに予測されたラベル値と正しいラベル値に対する誤差関数を評価し、重みとバイアスの値を調整する処理を繰り返します。

このチュートリアルでは、個々の訓練データに対してではなく、より計算効率を高めるために、訓練データを特定のサンプル数でまとめたミニバッチに対してSGDを適用します。

SGDにおける重要なパラメータとして、学習率があります。これは誤差関数を最小化させる(数学的には極小解に収束させるという言い方をします)ために重要なパラメータです。この値が大きいとうまく収束できず、小さいと適切な極小解に収束できない、もしくは収束が遅くなるという性質を持つため、この値の調整は非常に重要です。

このチュートリアルでは0.5という値においています。

learning_rate = 0.5


Cognitive Toolkitでは、SGDを使った訓練オブジェクトを作成し、そのオブジェクトに対して訓練データを投入します。訓練オブジェクトは以下のように記述できます。

lr_schedule = C.learning_rate_schedule(learning_rate, UnitType.minibatch) 
learner = C.sgd(z.parameters, lr_schedule)
trainer = Trainer(z, (loss, eval_error), [learner])


その他、訓練の結果を可視化するために役立つ関数として、移動平均を計算する関数と、訓練の進捗を出力する関数を定義しておきます。

def moving_average(a, w=10):    
    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)]


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: {}, Train Loss: {}, Train Error: {}".format(mb, training_loss, eval_error))
        
    return mb, training_loss, eval_error

ネットワークモデルの訓練の実行

ミニバッチに含まれる訓練データ数(minibatch_size)、全訓練データ数(num_samples)を定義します。また、処理を行う回数(num_minibatches_to_train)も定義しておきます。

minibatch_size = 25
num_samples = 20000
num_minibatches_to_train = num_samples / minibatch_size


訓練を実行します。訓練データは、ミニバッチの大きさの訓練データを毎回プログラムで生成しています。

training_progress_output_freq = 20

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

for i in range(0, int(num_minibatches_to_train)):
    features, labels = generate_random_data_sample(minibatch_size, input_dim, num_output_classes)
    
    # Specify the input variables mapping in the model to actual minibatch data for training
    trainer.train_minibatch({input : features, label : labels})
    batchsize, loss, error = print_training_progress(trainer, i, 
                                                     training_progress_output_freq, verbose=0)
    
    if not (loss == "NA" or error =="NA"):
        plotdata["batchsize"].append(batchsize)
        plotdata["loss"].append(loss)
        plotdata["error"].append(error)


訓練結果の可視化をします。1つ目のグラフでは、X軸を実行したミニバッチの件数、Y軸を誤差関数の値とし、2つ目のグラフではY軸を移動平均の値としています。

plotdata["avgloss"] = moving_average(plotdata["loss"])
plotdata["avgerror"] = moving_average(plotdata["error"])


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:masakykato:20170517184156j:plain:w400

f:id:masakykato:20170517184205j:plain:w400

訓練済みネットワークモデルの評価

ネットワークモデルの訓練が完了しましたので、ここではテストデータを使い、訓練済みネットワークモデルを評価します。

テストデータも訓練データと同様に、プログラムで生成したシミュレーションデータとしています。

test_minibatch_size = 25
features, labels = generate_random_data_sample(test_minibatch_size, input_dim, num_output_classes)


訓練済みのネットワークモデルに対して、テストデータを投入します。

trainer.test_minibatch({input : features, label : labels})


ここまでのプログラムを実行すると、テストデータを使用した場合の誤差率が表示されます。

個々のテストデータに対する正誤を確認する場合には、以下のコードを記述して実行します。

out = C.softmax(z)

predicted_label_probs = out.eval({input : features})
print("Label    :", [np.argmax(label) for label in labels])
print("Predicted:", [np.argmax(row) for row in predicted_label_probs])


以上で、深層学習による二値分類のチュートリアルが終了しました。このチュートリアルをベースとして、学習率を変更した時の精度の変化や、確率的勾配効果法以外の手法を試してみるのもよいと思います。