case-kの備忘録

備忘録です。GCPやデータ分析系のことを呟きます。

Chainerで癌の良性・悪性を分類予測してみた

Chainerで癌の良性・悪性の分類予測を試してみたいと思います。

Chainerとは

Chainerは、Preferred Networks社が開発を進めているディープラーニングニューラルネットワーク)に特化したフレームワークです。
ディープラーニングについては以下の記事を見て頂けると嬉しいです。
case-k.hatenablog.com

Chainerのメリット

ChainerのメリットとしてはDefine by Runと呼ばれる仕組みがあります。TensorFlowや他のディープラーニングに特化したフレームワークと比較して
学習途中に数値やサイズの確認が出来るためデバックがしやすいです。また、Chainerの構成はシンプルなのでパラメータのチューニングも理解しやすいのがメリットです。

Chainer構造理解

Chainerの構成要素は以下となります。詳細については実装編で説明させて頂きますが簡単に全体像を把握できるように内容を記載します。

  • modelの宣言:入力層・中間層・出力層を定義してモデル構築
  • optimizerを定義:パラメータの更新を行うアルゴリズムの選択
  • iteratorを定義:バッチサイズを指定
  • updaterを定義:optimizerの設定や使用するデバイス(CPUやGPU)を選択します。
  • Trainerとextensionsの設定:epochの回数を指定

実装編

それでは実際にChainerで癌の分類予測問題を解きたいと思います。まずは必要となるライブラリをインポートします。

# library
from sklearn.datasets import load_breast_cancer
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline

import chainer
import chainer.links as L
import chainer.functions as F
from chainer import training
from chainer.training import extensions
import json

cancerデータを取得しどのようなデータ確認します。

cancer = load_breast_cancer()
print("cancer kesy:{}".format(cancer.keys()))
print("cancer target_names:{}".format(cancer.target_names))
print("cancer target:{}".format(np.unique(cancer.target)))
print("cancer feasture_names:{}".format(np.unique(cancer.feature_names)))
print("cancer feasture_names shape:{}".format(np.unique(cancer.feature_names.shape)))

# Out
cancer kesy:dict_keys(['DESCR', 'target_names', 'target', 'feature_names', 'data'])
cancer target_names:['malignant' 'benign']
cancer target:[0 1]
cancer feasture_names:['area error' 'compactness error' 'concave points error' 'concavity error'
 'fractal dimension error' 'mean area' 'mean compactness'
 'mean concave points' 'mean concavity' 'mean fractal dimension'
 'mean perimeter' 'mean radius' 'mean smoothness' 'mean symmetry'
 'mean texture' 'perimeter error' 'radius error' 'smoothness error'
 'symmetry error' 'texture error' 'worst area' 'worst compactness'
 'worst concave points' 'worst concavity' 'worst fractal dimension'
 'worst perimeter' 'worst radius' 'worst smoothness' 'worst symmetry'
 'worst texture']
cancer feasture_names shape:[30]

2クラス分類問題で、入力層は30となります。扱いやすいよう、DataFrameにします。

# function  
def get_target_names(x):
    if x == 0:
        return "malignant"
    if x == 1:
        return "benign"

cancer_data = pd.DataFrame(columns=cancer['feature_names'],data = cancer['data'])
cancer_data['target'] = cancer['target']
cancer_data["target_names"] = cancer_data['target'].apply(lambda x : get_target_names(x))
cancer_data.head(5)

f:id:casekblog:20180826185125p:plain:w500


データを確認してみます。

print(cancer_data.shape)

# Out
(569, 32)

32カラムで569レコードのデータセットができました。入力変数と教師データ(出力変数)に切り分けます。

# 教師データ
t = cancer_data.iloc[:,-2]
x = cancer_data.iloc[:,0:-2]

print(t.shape)
print(x.shape)

# Out
(569,)
(569, 30)

入力変数と教師データに切り分けることができました。
しかし、このままではChainerで学習させることはできません。Chainerで計算できるデータ形式に変換する必要があります。

Chainerで計算できるデータ形式に変換

Chainerで計算を行うために、下記の3点を満たしている必要があります。

  • 入力変数や教師データがNumpyで定義されているか
  • 分類の場合、ラベルが0から始まっているか
  • 入力変数が float32、教師データが回帰の場合 float32、分類の場合 int32 で定義されているか

作ったデータが上記のルールに従っているか確かめmす。

print("type check:{}".format(type(t)))
print("type check:{}".format(type(x)))
print("label check:{}".format(np.unique(t.values)))
print("dtype check:{}".format(t.dtype))
print("dtype check:{}".format(x.values.dtype))

# Out
type check:<class 'pandas.core.series.Series'>
type check:<class 'pandas.core.frame.DataFrame'>
label check:[0 1]
dtype check:int64
dtype check:float64

データ形式や要素のデータ型がChainerで計算できるデータ形式
なっていないため、変換する必要があります。

x = np.array(x.astype('float32'))
t = np.array(t.astype('int32'))

print("type check:{}".format(type(t)))
print("type check:{}".format(type(x)))
print("label check:{}".format(np.unique(t)))
print("dtype check:{}".format(t.dtype))
print("dtype check:{}".format(x.dtype))

# Out 
type check:<class 'numpy.ndarray'>
type check:<class 'numpy.ndarray'>
label check:[0 1]
dtype check:int32
dtype check:float32

正しくデータ形式を変換させることができました。

Chainerで使用するデータセットの形式

メモリに乗の小さなデータの場合は、入力変数と教師データをタプルで1セットにし、リスト化しておくことがChainer推奨のデータ形式となります。

dataset = list(zip(x, t))

次に教師データと検証データに分割したいと思います。

# 訓練データのサンプル数
n_train = int(len(dataset) * 0.7)

# 訓練データ(train)と検証データ(test)に分割
train, test = chainer.datasets.split_dataset_random(dataset, n_train, seed=1)

以上でChainerで必要とされるデータ形式が完成しました。
ここからは、modelの宣言、optimizerを定義、iteratorを定義、updaterを定義していきます。
まずはモデルの定義です。

モデルの定義

ここでは入力層・隠れ層・出力層を定義し損失関数を求めています。
詳しくは以下をご確認下さい。
case-k.hatenablog.com

# ニューラルネットワークのモデルを定義
class NN(chainer.Chain):

    # モデルの構造
    def __init__(self, n_mid_units=5, n_out=2):
        super().__init__()
        with self.init_scope():
            self.fc1 = L.Linear(None, n_mid_units)  
            self.fc2 = L.Linear(None, n_out) 

    # 順伝播
    def __call__(self, x):
        u1 = self.fc1(x)
        z1 = F.relu(u1)
        u2 = self.fc2(z1)
        return u2

モデルを定義できたのでインスタンス化します。

np.random.seed(1)

# インスタンス化
nn = NN()
model = L.Classifier(nn)

これでmodelの定義とインスタンス化は完了です。

モデルの定義

次は「optimizer」を定義します。

Optimizerの定義

optimizerとはパラメータの更新を行う箇所で、パラメータの最適化を行うための最適化のアルゴリズムを選択します。

optimizer = chainer.optimizers.SGD() 
optimizer.setup(model)

今回は 確率的勾配降下法SGD)を使用しました。
確率的勾配降下法については別途記事を書きたいと思います。
アルゴリズムを定義するだけでは、モデルに反映されないため
optimizer.setup(model)でアルゴリズムをモデルに適用させます。


次はIteratorを定義する必要があります。

Iteratorの定義

Iteratorでは「バッチサイズ」を決めます。順伝播で評価関数を計算するとき全てのサンプルを使用するのではなく、ミニバッチと呼ばれるサンプルの一部のデータセットのみで評価関数の計算を行いパラメータの学習を行います。

バッチサイズとは

「バッチサイズ」とはランダムに抽出したサンプルの一部のデータセットのことです。
100万レコードある場合、100万レコードいっぺんに実施することはメモリやCPUが足りません。
なので100サンプルずつ実施するようにします。この1回の試行で利用するデータのサイズをバッチサイズといいます。
またバッチサイズ1回行うことを1 epochといいます。
100万レコードを、100サンプルずつ行う場合1万epochとなります。

batchsize = 10
train_iter = chainer.iterators.SerialIterator(train, batchsize)
test_iter  = chainer.iterators.SerialIterator(test,  batchsize, repeat=False, shuffle=False)

Iteratorを定義できました。次はupdaterの定義です。

updaterの定義

Updaterでは、Optimizerの設定・デバイス(CPUやGPU)の設定を行うことができます。
CPUを利用する場合、device=-1とオプションに指定し、GPUを使用する場合にはdevice=0
と明示しておく必要があります。deviceを指定しない場合は、CPUが使用されます。
今回はCPUを利用します。

updater = training.StandardUpdater(train_iter, optimizer, device=-1)

updaterを定義できました。次はTrainerとextensionsの設定を行います。

Trainerとextensionsの設定

Trainerでは、エポック(ミニバッチを全て処理して1エポック)の回数や、
そのextensionsでオプションを指定することにより、結果をログ出力や標準出力できます。

# エポックの数
epoch = 50

# trainerの宣言
trainer = training.Trainer(updater, (epoch, 'epoch'), out='result/cancer')

# 検証データで評価
trainer.extend(extensions.Evaluator(test_iter, model, device=-1))

# 学習の経過をtrainerのoutで指定したフォルダにlogというファイル名で記録する
trainer.extend(extensions.LogReport(trigger=(1, 'epoch')))

# 1エポックごと(trigger)に、trainデータに対するlossと、testデータに対するloss、経過時間(elapsed_time)を標準出力させる
trainer.extend(extensions.PrintReport(['epoch', 'main/accuracy', 'validation/main/accuracy', 'main/loss', 'validation/main/loss', 'elapsed_time']), trigger=(1, 'epoch'))

# 実行
trainer.run()

f:id:casekblog:20180826184919p:plain:w500
実行結果を確認してみてみましょう。Trainerを使用すると、事前に定義しておいたresult/cancerというディレクトリができ、その中のlogというファイルが生成されます。
logのファイルを確認してみてみましょう。

with open('result/cancer/log') as f:
    logs = json.load(f)
    results = pd.DataFrame(logs)

# 結果の確認
results
# accuracy(精度)を表示
results[['main/accuracy', 'validation/main/accuracy']].plot()

f:id:casekblog:20180826185111p:plain:w500


精度は訓練データに対しては62%ほどであり、
検証データに対して65%ほどであることが分かります。


損失関数も確認してみたいと思います。

# loss(損失関数)を表示
results[['main/loss', 'validation/main/loss']].plot()

f:id:casekblog:20180826185015p:plain:w500

とりあえず一連の流れは理解できました。
正直色々なアルゴリズムが多く何が最適なのか判断が難しいですね。調べたところ画像認識ではChainerが優れているようです。
次はChainerで精度を向上させる方法や予測問題・他のフレームワークとも比較してみたいですね。

# 追記
ランダムフォレストでも試してみました。

case-k.hatenablog.com