機械学習: 入門

機械学習入門

近年たいへんに発達し,かつ注目されている機械学習について学ぼう.

Neural network と深層学習

機械学習という分野は広くて様々な技術を含むが,最近注目されているのは neural network (NN と略されることも多い) を用いた深層学習だろう. この技術によって画像分類問題で大変画期的な結果が出て以来,大変 hot な分野であることは間違いない.

しかし,その開発が大変勢いある分野であるため,細かい技術論にまどわされて「全体がよくわからない」という者も多いだろう.

そこで今回は,その「基本的な考え方」を授業時に簡単に解説し,そして実際に自分でプログラムして動かしてみよう. なおその際,機械学習用に作られた便利なライブラリ等は利用しないでやってみる(最近のライブラリというかフレームワークは「プロ用としてよく出来ている」だけに高度かつ便利すぎて,基礎を学ぶにはかえって適してないように思われる).

今回のターゲット問題

機械学習のの重要な本質の一つは「明示的なアルゴリズムではどうやって解決方法を実現したら良いかわからない問題」に解決方法を与えるところにある. しかしまあ,そういう問題は扱いそのものがそれなりに面倒だったりするので,今回は大変簡単な問題を「解決できないふりをして」取り組むターゲットにしてみよう.

今回扱う問題は, $x \in [0,1]$ に対して実際は

$g(x) = \left\{\begin{array}{rcl} 1 & : & 1 / 3 < x < 2 / 3, \cr 0 & : & \mbox{ otherwise } \end{array}\right.$

である関数を,その関数形を知らない状態で データ $\{ x_k, g(x_k) \}_{k = 1}^{N_d}$ だけをもらってその情報から近似関数を作ろう,という問題としよう. もちろん,この問題は通常は「補間」技術を用いたほうが筋も結果も計算時間も良いのだが,今回は敢えて機械学習の練習問題としてこれを扱おう.

今回用意する neural netowrk

スカラー実数 $v^{(0)}$ を入力とし,中間層は 3つで, 全体の出力はスカラー実数であるような,大変単純な NN を今回は考えよう.

より具体的には,中間層(ベクトル, 要素数 $n$)の出力をそれぞれ $\boldsymbol{v}^{(1)}$, $\boldsymbol{v}^{(2)}$, ${v}^{(3)}$ (これは最終出力そのもの), として, 密結合係数として $n$ 次元ベクトル $\boldsymbol{C}_1, \boldsymbol{C}_3$, $n \times n$ 行列 $C_2$, バイアスとして $n$ 次元ベクトル $\boldsymbol{b}_1$, $\boldsymbol{b}_2$, スカラー $b_3$ を用意して,NN が

$\left\{\begin{array}{rcl} \boldsymbol{v}^{(1)} & = & \mbox{ ReLU }( \boldsymbol{C}_1 v^{(0)} + \boldsymbol{b}_1), \cr \boldsymbol{v}^{(2)} & = & \mbox{ ReLU }( C_2 \boldsymbol{v}^{(1)} + \boldsymbol{b}_2), \cr \mbox{ output: } {v}^{(3)} & = & \boldsymbol{C}_3 \cdot \boldsymbol{v}^{(2)} + b_3 . \end{array}\right.$

となっているケース(ほぼ最小限セットだな)を考える. $n$ はそうだなあ,たぶん 5 ~ 10 ぐらいでうまくいくだろう(かなり無駄が多いけどな).

一応,関数としてこれを NN という名前で呼ぶことにしよう.つまり,$\mbox{output } v^{(3)} = \mbox{NN}( H, \mbox{ input } v^{(0)})$ という感じだ.ただし $H$ は $C_1 \cdots C_3$, $b_1 \cdots b_3$ をまとめたものだ.

ちなみに ReLU 関数は活性化関数と呼ばれる関数の一つで次のように定義され,まあ,なんというか,アナログ性を消さないようにしてある if 文の役目を持つ関数と思って良い.

$\mbox{ ReLU } (x) = \left\{\begin{array}{rcl} 0 & : & x < 0, \cr x & : & 0 \leq x . \end{array}\right.$

NN をどう使うの?

使い方は簡単だ.

まず,既知のデータから一つ適当な $x_k$ を関数 NN に入れると値が出てくるので,これとデータに入っている $g(x_k)$ (教師情報)を比べて誤差を計算する.

そして,この誤差が小さくなるように 密結合係数 $n$ 次元ベクトル $\boldsymbol{C}_1, \boldsymbol{C}_3$, $n \times n$ 行列 $C_2$, バイアス $n$ 次元ベクトル $\boldsymbol{b}_1$, $\boldsymbol{b}_2$, スカラー $b_3$ を修正するのだ!
注: どうやって? と思うかもしれないが,これは「最小化問題」という古典的な(情報系の)問題で,いろんな技術が開発されている.今回は一番シンプルな勾配法を用いるよ.

そしてこの修正プロセスを,データの個数だけ繰り返せば良いんじゃね? というのが全体の使い方だ.

これでうまくいくのかって? まあ,まずはやってみるのが一番だ.

実際にやってみる

あとは少しずつプログラムを作っていくだけだ. まずは、問題のパラメータを設定してしまおう.

1
2
3
4
using LinearAlgebra
using ForwardDiff: gradient  # 誤差の勾配を求めるのに使う

n = 10 # 中間層のサイズ.これは大変小さいほうだ.

次に、対象関数を教師情報の代わりに作ってしまおう.

1
predict(x) = round( sin(π*x)/√3 )

プロットして確認しておこう.

1
2
3
4
using Plots

X = 0:0.01:1.0
plot(X, x-> predict(x))

target

次に,NN を作ってしまおう.gradient を計算するライブラリの都合を考慮して,(データに多少無駄が入るが)次のような感じになる.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
relu(x) = (x > 0.0) ? x : 0.0 # まず ReLU を実装して,

# Neural Network. 
# gradient が一つしか引数を持てないので,係数行列を3次元方向に重ねて大きな3次元行列 M としている.

function nn(M, input)
    C1 = view( M, :, 1, 1 )
    b1 = view( M, :, 1, 2 )

    C2 = view( M, :, :, 3 )
    b2 = view( M, :, 1, 4 )

    C3 = view( M, 1, :, 5 ) # 数学的には 1 x n 行列になるはずだが,n次元ベクトルに翻訳されてしまう
    b3 = M[1,1,6]           # 1 x 1 行列を参照すると行列のままなので,ここはスカラーとしてコピー.

    r1 = relu.( C1 * input + b1 ) # 中間層1
    r2 = relu.( C2 * r1 + b2 )    # 中間層2
    r3 = C3' * r2 + b3    # C3 が上の理由でベクトルになっているので転置する.

    return r3
end

誤差の計算は,単なる(スカラー値の)差の二乗にしておこう.

1
2
# 損失関数.要は,出力の誤差.
loss(x,y) = (x - y)^2

さてそろそろ計算そのものの準備に入ろう.まずは,肝心のパラメータ群の初期値を乱数で適当に生成する.

1
2
# NN 中の パラメータ行列を最初は乱数で適当に生成する.これが少しずつ修正される.
H = rand(n,n,6) .- 0.5

10×10×6 Array{Float64,3}: [:, :, 1] = 0.338546 0.311262 0.211556 … -0.419288 -0.168401 0.156182 0.0648416 -0.386509 -0.417286 -0.446714 -0.262055 0.24577 -0.409871 -0.0705965 -0.345531 -0.0766839 0.163789 -0.0895814 …

このパラメータで関数 NN はどうなっているかをプロットして見ておこう.

1
2
# 最初はこんな関数が実現されている(乱数によるのでやるたびに異なる)
plot( X, x-> nn(H, x))

initial nn

乱数で作っているので当たり前だけど,ターゲット関数とはまるで異なるよな.

では,肝心の計算だ! かなり簡単だぞ.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
using ProgressMeter

γ = 0.1 # 学習係数などと呼ばれる量

# NN の「中身」を少しずつ改善する
@showprogress for i in 1:200000
   global H
   x = rand()  # 適当に input x を選んで
   f(H) = loss(predict(x), nn(H,x))  # その input x とパラメータ行列 H でどれくらい出力誤差があるか
   H += - γ * gradient(f, H)  # パラメータを修正する.これは単なる勾配法.
end

Progress: 100%|█████████████████████████████████████████| Time: 0:03:39

ちなみに,教師データとして関数 predict を使っているのは「ズル」と言えばずるいので,真面目にやるならデータを作っておこうw

さてうまくいったのか,グラフを見てチェックしよう.

1
plot( X, x-> nn(H, x))

final nn

おお! なんかうまくいったことがわかる. なんにも考えないでループを回してうまくいくんだから,これは確かに「いい」方法なのかも,と思わされるよな.

ちなみに,修正後のパラメータ群を以下のようにしてちょっと見てみると,

1
H

10×10×6 Array{Float64,3}: [:, :, 1] = 8.16321 0.311262 0.211556 … -0.419288 -0.168401 0.156182 0.0648416 -0.386509 -0.417286 -0.446714 -0.262055 0.24577 -0.409871 -0.0705965 -0.345531 -0.0766839 0.163789 -0.0895814 -0.120206 -0.494872 0.339559 -0.137791 0.200478 0.35665 …

という感じで,意外に数字が散らばっていて,その特徴を掴むにはまたちょっと工夫が要りそうだ.

Report No.13

  1. 近似対象の関数を変えたり,中間層の大きさや数を変えたりして自分でやってみよう.
  2. 異なる種類の問題にこの考え方を使えないか,検討してみよう.例えば,4つの数字と「その花の種類」をデータ化した UCI の iris データ をもとに,その4つの数字だけから花の種類を当てられるようにできるだろうか.