- DeepLearning(1): まずは順伝播(上)
- [DeepLearning(2): まずは順伝播(下)]
(http://qiita.com/eijian/items/24d7e6aee332d59509ec) - DeepLearning(3): そして逆伝播(でも全結合層まで)
- DeepLearning(4): CNNの逆伝播完成?
- DeepLearning(5): スペースリーク解消!
さて、今回からは新たなネタとしてDeepLearningに取り組もう。DeepLearningといえばすでにブームは去って、今は現実の課題にどんどん活用されている状況だ。使いやすく性能のいいツールやライブラリがいろいろあり、ちょっと学べば簡単にその恩恵に与ることができる(かもしれない)。
しかし、仕組みがわからないのをただ使うのは面白くないし個人的に写真の自動分類をしたいというニーズもあるし、遅まきながら自分で作ってみよう、というわけだ。
目標と参考資料
最終的には「一般物体認識」まで行きたい(というかそうでないと写真の分類にならない)が、目標が高すぎると頓挫するのでここでは以下の2つのステップまでとする。
「自分で作ってみよう」と大口を叩いていながら他の方の実装を写経するとは詐欺的だが、そこは当方素人なので。。。以後、yusugomoriさんのpython実装を「yusugomori実装」と表記する。
作るのは、畳み込みニューラルネットワーク(Convolutional Neural Network, CNN)による画像認識プログラムとする。以下、参考にした書籍やWebサイトを列挙。
- 書籍
- Deep Learning with Java : yusugomoriさんの書籍、日本語版が9月に出る予定
- 深層学習 : 専門的な本、最初に買ったが予備知識なしではかなり難しかった
- 機械学習と深層学習 ―C言語によるシミュレーション : 具体的な実装のための理解には役立つ
- 初めてのデーィプラーニング : 理論の大枠をざっと理解するにはよかった
- Webサイト
ひとつの本やWebサイトだけでは理解が進まないので、多数に目を通して同じ概念をいろいろな説明で読むほうがよいと思う。
まずは型を考える
(ちゃんと理解できているとは言い難いが)CNNによる画像認識は次の図のように「画像」を各層で変換していって最終的に分類結果(どれに該当するかを確率で示す)を得るようだ。
そこでまずは「画像(Image)」型と「層(Layer)」型を定義しよう。カラー画像だと、$X \times Y$二次元のドットで構成された平面(Plain)が赤緑青の3色(チャネルとする)集まってできている。一般の画像ファイルでは各ドットを0-255の整数で表すことが多いが、CNNでは強さを実数で表すようだ。畳み込み層の変換後のデータも多チャンネルの「画像」とみなせそうなので、Image
は次のように定義した。実体はDouble
の3次元リストだ。
(Image.hs)
type Plain = [[Double]] -- 2D: X x Y pixels
type Image = [Plain] -- n channel
わざわざImage
をPlain
のリストにしたのは、Plain
ごとに処理することが多そうだからだ。あとついでに、「学習」の際に必要となる教師データも定義しておく。
(Image.hs)
type Class = [Double] -- trainer vector
type Trainer = (Image, Class)
実数のリストだが、教師データは要素のどれか一つが1.0、他が0.0となる。
次にレイヤを考える。CNNでは次のようなレイヤが使用される。
- 畳み込み層 (convolution layer)
- プーリング層 (pooling layer)
- 活性化層/活性化関数 (activation layer)
- 全結合層 (fully connected layer)
このうち、活性化層というのは一般的な表現かどうかよくわからない。最初は型クラスとしてLayer
を定義し各層を型としてやればいいと考えた。が、各層をつなげて一連の変換処理を表現するのに「リスト」を使いたかったので(私の知識範囲では)うまい方法が思いつかなかった。ということで、Layer
型を次のように代数データ型で定義することにした。
(LayerType.hs)
data Layer = NopLayer
| ConvLayer Int [FilterC]
| MaxPoolLayer Int
| ActLayer ActFunc
| HiddenLayer [FilterH]
| FlattenLayer (Image -> Image)
NopLayer
は何も変換しないダミーの層(テスト用)、プーリング層は実用上Max Poolingだけでよいと考えてMaxPoolLayer
、全結合層は隠れ層ということでHiddenLayer
としている。各行に出てくる引数の型はそれぞれ以下のように定義してある。ActFunc
は活性化関数、FilterC
は畳み込み層のためのフィルタ、FilterH
は全結合層(隠れ層)のためのフィルタ、をそれぞれ表す型だ。
(LayerType.hs)
-- for Activation Layer
type ActFunc = [Double] -> [Double]
-- filter for Convolution Layer
type Kernel = [[Double]]
type Bias = Double
type FilterC = (Kernel, Bias)
-- filter for Hidden Layer
type FilterH = [Double]
学習用プログラム
deep learningで画像認識させるためのステップは大雑把には以下の手順を踏む必要があると思っている。
- 大量のサンプル画像を用意し、それらを分類する。
- サンプル画像から「教師データ」を作成する。
- 教師データを使って「学習」を行う。(学習とは、各層で使われるフィルタを
教師データを元に調整していくこと、と理解) - 画像を調整済みフィルタを使って分類する(各分類の確率を計算する)。
1と2はプログラム以前の準備段階だ。が、yusugomori実装では教師データと学習状況の確認に使うテストデータをプログラム内で生成している。最終的には学習データとテストデータは外から与える形にしたいが、まずは同じようにプログラム内で生成するようにしよう。
教師データの生成
ゆくゆく教師データを外から与えることも考え、「画像データの集合」の型??Pool
を作ろう。これらの集合から幾つかの画像を取り出して学習に使いたい。そこで、データに追番をつけてMap
に登録することにした。とりあえずはオンメモリで処理できれば良いので以下のようにプール(オンメモリ版、MemPool
)を定義した。
(Pool.hs)
class Pool p where
getImages :: p -> Int -> Int -> IO [Trainer]
nSample :: p -> Int
newtype MemPool = MemPool { m :: Map.Map Int Trainer }
yusugomori実装に倣って教師データを生成する関数も用意した。
(Pool.hs)
initSamplePool :: Int -> (Int, Int) -> Int -> Double -> Int -> IO MemPool
initSamplePool c (sx, sy) o p n = do
s0 <- forM [0..(n-1)] $ \i -> do
let cl = i `mod` o -- class of this image
-- Image data
s1 <- forM [1..c] $ \j -> do
s2 <- forM [0..(sy-1)] $ \y -> do
let p' = if y `div` st == cl then p else (1-p)
s3 <- forM [1..sx] $ \x -> do
a <- pixel p'
return a
return s3
return s2
-- Trainer data
e1 <- forM [0..(o-1)] $ \j -> do
return $ if j == cl then 1.0 else 0.0
return (s1, e1)
return (MemPool (Map.fromList $ zip [0..] s0))
where
st = sy `div` o
pixel :: Double -> IO Double
pixel p = do
v <- MT.randomIO :: IO Double
let v' = if v < p then 0.5 else 0.0
return v'
結構ごちゃごちゃしているが、以下のような3種類の画像データを生成してプールとして返しているのだ。
実際は上記のようなキッチリした画像ではなく、灰色部分に一定割合で白が混ざっていて、白色部分は逆に灰色が混ざっている。教師データではその割合を5%、テストデータは10%としている。
つぎに、プールから指定枚数の画像を取り出す関数getImages
とプール内の画像総数を返す関数nSample
を示そう。
(Pool.hs)
{-
getImages
IN : pool
epoch number
batch size
-}
instance Pool MemPool where
getImages p@(MemPool m) e b = do
let s = nSample p
o = (e-1) * b `mod` s
mx0 = o + b - 1
mx2 = mx0 - s
mx1 = if mx2 < 0 then mx0 else s - 1
im0 = mapMaybe (\x -> Map.lookup x m) [o..mx1]
im1 = mapMaybe (\x -> Map.lookup x m) [0..mx2]
return (im0 ++ im1)
nSample (MemPool m) = Map.size m
getImages
は若干ややこしいが、要は教師データを満遍なく学習させたいのでepoch毎に違う画像を取り出すようにしている(最初のepochで1番目から10個取り出したら次のepochでは11番目から10個取り出す)。ゆくゆくは順番に取り出すのではなくランダムに取り出すオプションも実装したい。
main関数(全体の流れ)
ここで、全体の流れを説明しておきたいのでmain
関数の話をしよう。大雑把には次のような流れだ。
- パラメータ定義(
main
以前):フィルタの大きさとかバッチサイズとか
(将来的にパラメータを設定ファイルから読み込むようにしたい) - 教師データ、テストデータの準備
- 各レイヤの初期フィルタの生成
- レイヤの組み立て
- 学習の繰り返し
まずパラメータの説明から。画像は3種類に分類する(k)。教師データが50x3=150個、テストデータが10x3=30個だ。画像サイズ、フィルタは先に示した各レイヤの仕様どおり。なおバッチサイズは教師データが少ないので毎回全教師データを学習するために150個としている。
(Main-cnn.hs)
-- PARAMETERS
-- training/test dataset
k = 3 -- number of class
n = 50 -- number of teacher data for each class
m = 10 -- number of test data for each class
train_N = n * k
test_N = m * k
-- image size
image_size = [12, 12]
channel = 1
-- filter specs
n_kernels = [10, 20]
kernel_sizes = [3, 2]
pool_sizes = [2, 2]
n_hidden = 20
n_out = k
-- loop parameters
epochs = 200
opStep = 5
epoch0 = 1
batch = 150
-- others
learning_rate = 0.1
次にメインルーチンだ。教師データとテストデータは次のように生成している。説明はいらないだろう。
(Main-cnn.hs)
main :: IO ()
main = do
putStrLn "Building the model..."
poolT <- initSamplePool 1 (12, 12) 3 0.95 train_N -- trainer data pool
poolE <- initSamplePool 1 (12, 12) 3 0.90 test_N -- test data pool
sampleE <- getImages poolE 1 test_N
次はレイヤの定義だ。まずフィルタを初期化してからレイヤを複数並べる。
(Main-cnn.hs)
fc1 <- initFilterC 10 1 12 12 3 2
fc2 <- initFilterC 20 10 5 5 2 2
fh1 <- initFilterH n_hidden (2*2*20)
fh2 <- initFilterH n_out n_hidden
let layers = [ConvLayer 3 fc1, ActLayer relu, MaxPoolLayer 2,
ConvLayer 2 fc2, ActLayer relu, MaxPoolLayer 2,
FlattenLayer flatten,
HiddenLayer fh1, ActLayer relu,
HiddenLayer fh2, ActLayer softmax]
ここでは、畳み込み層1(活性化関数ReLU)→プーリング層1→畳み込み層2(活性化関数ReLU)→プーリング層2→隠れ層(活性化関数ReLU)→出力層(活性化関数softmax) という多層構造にした。なお、FlattenLayer
は畳み込み+プーリングの処理から全結合へデータ形式を変換する処理である。
最後が学習の繰り返しだ。
(Main-cnn.hs)
tm0 <- getCurrentTime
putStatus 0 tm0 [([0.0], 0.0)] layers
loop is tm0 layers batch poolT sampleE
時間の記録と学習前の状態をダミーで表示したあと、繰り返し学習するloop
を呼んでいる。loop
の詳細は次の通り。
(Main-cnn.hs)
loop :: Pool p => [Int] -> UTCTime -> [Layer] -> Int -> p -> [Trainer]
-> IO ()
loop [] _ _ _ _ _ = putStr ""
loop (i:is) tm0 ls b pt se = do
teachers <- getImages pt i b
ops <- mapM (train ls) teachers
let (_, dls) = unzip ops
ls' = updateLayer ls dls -- dls = diff of layers
if i `mod` opStep == 0 then putStatus i tm0 (evaluate ls' se) ls'
else putStr ""
loop is tm0 ls' b pt se
大したことはやってなくて、epoch毎に繰り返しloop
を呼び、epoch番号がなくなったら(空リストになったら)、繰り返しを終了する。中身は、学習する教師データをプールから取り出して、それぞれ学習をし、各層を学習結果を使って更新する。とはいえ、まだ「学習」は実装しておらず、この部分はダミーである。
ここまでがmain関数だ。
長くなってしまったので、各層(レイヤ)の処理については次に回すことにする。
まとめ
今回はCNNの画像認識プログラムの初回として以下に取り組んだ。
- CNNで使われるいろいろな型を定義した。
- 入力値を変換するいろいろな「層」を定義した。
- これらを使って順伝播の処理を実装した。
次回は各層についての説明。
(ここまでのソースはこちら)