12. AI技術 機械学習(Deep Learning)入門
Photo by
Alina Grubnyak
on Unsplash
人工知能(Artificial Intelligence: AI) 技術
人工知能というのは情報技術の長年の夢の技術であり,これまで長い歴史がある. 昨今は Deep Learning に代表される機械学習がたいへんに注目を集めているが,他にも多くの要素技術があり,それぞれに得手不得手がある. これらについて,AIに関する機械学習とその他の要素技術について今回から学んでいこう.
Deep Learning (深層学習)入門
今回はまずは近年たいへんに発達し注目されている機械学習技術の一つである Deep Learning について,その本質を簡単に学ぼう. 流行っていることもあって機械学習のライブラリ・フレームワークは多く存在するが,今回はそうしたものを使わずに自分でプログラムして内容を理解する一助としよう.
この Deep Learning というのは,多層の neural network (NN と略されることも多い) を数学的にモデリングして利用するもので, この層の数が多くても内部パラメータを問題なくチューンできることから名前に deep がつくものである. この技術によって画像分類問題で大変画期的な結果が出て以来,大変 hot な分野であることは間違いない.
しかし,その開発が大変勢いある分野であるため,細かい技術論にまどわされて「全体がよくわからない」という者も多いだろう. 先に書いたように,今回は基本的な考え方を理解できるよう,解説と素のプログラミングを重視してすすめよう.
全体像
全体像を示そう. まず,機械学習でやりたいことは何かというと,
やりたいこと = 入力を与えると良い答を返す関数を作ること
なのだ. これがうまくできれば,われわれが知能を使って行っている作業のうち多くをコンピュータに代わりにさせることができる,というわけだ1.
そして,機械学習の様々な技術は,この「関数を作り,改善すること」を如何に上手にやるか,という技術なのだ.
今回扱う deep learning では,関数を neural network で作り,そのパラメータを改善していく技術だ. シンプルな仕組みながらこれが大変うまくいく問題が多く,そのために期待されているのだ.
Neural Network
ニューロン(神経細胞)の構造図, license:public domain, created by LadyofHats
Neural Network とは,上の図のような神経細胞(ニューロン)によって構成された神経網を真似たシンプルな数学モデルだ.
もとの神経網の仕組みをおおまかにいうと,まず,ニューロンの電気信号が軸索を通じてそのニューロン末端のシナプスに届き,その結合を介して信号を他のニューロンに伝えるようになっている. ニューロンはそのようにして複数のニューロンからやってきた信号を受け取るわけだが,集まった信号が一定以上の大きさであれば「反応(興奮や発火と呼ばれる)」して新たに電気信号を作り,他のニューロンへ信号を伝える. あとはこの繰り返し,という仕組みで,この「網の形」と「シナプス結合の強さ」等々によって情報処理がうまくいくようになっている,と考えられている.
これをモデル化して,いわゆる有向グラフの「ノード」をニューロンの信号受信&処理部分とすると次の図のような感じだ.
少し解説しよう.
- 信号を送ってくるノード $i$ からの信号をノード $k$ がどれだけ受け入れるかは調整パラメータである 重み係数 $C_{ik}$ として表され,
- ノード $k$ の発火のしやすさは調整パラメータである バイアス $b_k$ で表される.
- 入力信号が多いと出力信号が作られる「発火」という現象は,入力の合計を $r$ として非線形関数 $\phi(r)$ で表される.
この関数のことを 活性化関数 (activation function)と呼んだりする.
活性化関数にはいくつか提案されているものが有り,典型的なのは次のようなものだ.
活性化関数を変えると NN の挙動が結構変わるので,実際に NN を使う場合はいくつか試してみると良い.
- 活性化関数の例
名前 関数 グラフ シグモイド関数(sigmoid) $\displaystyle \sigma(r) = \frac{1}{ 1 + e^{-r} }$ tanh 関数 $\displaystyle \tanh(r) = \frac{ e^r - e^{-r} }{ e^r + e^{-r}}$ 正規化線形関数(ReLU) $\mbox{ReLU}(r) = \max(0,r)$
そしてこのノードを集めて層を作り,その層を繋げたものが neural network だ.イメージは次のような感じになる.
図: neural network 全体のイメージ
そして,入力に対して出てくる出力が望ましいものになるべく近づくよう,パラメータ $C,b$ を調整していく(これが学習),ということを繰り返すことでこの neural network を望ましいものへと改善していくのだ.
この調整,つまり学習をどう行うかだが,これは最小化問題という文脈の問題を解決する技術が使える. すぐ後で解説するのでそこまで待とう.
今回の最初のターゲット
機械学習の重要な本質の一つは「明示的なアルゴリズムではどうやって解決方法を実現したら良いかわからない問題」に解決方法を与えるところにある. しかしまあ,今回は最初の入門問題として,大変簡単な問題を「解決できないふりをして」取り組むことにしよう.
今回扱う問題は, $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
スカラー実数 $x$ を入力とし出力はスカラー実数であるような,大変単純な NN を今回は考えよう.
まず,NN の構造を説明しよう.
NN は結構多めの「層」をつなげて,そこに現れる多数のパラメータを「学習」によってチューンすることで所望の出力を得られるようにしよう,という関数だ.
その層だが,典型的には一層ずつは以下のような形をしている.
そして今回の問題では,こうしたパラメータをもつ層が 3つであるような NN を考えよう. 大雑把には下図のような設計になる.
より具体的には, 入力を $x$, 出力を $y$, 中間層(ベクトル, 要素数 $n$)の出力をそれぞれ $\boldsymbol{r}^{(1)}$, $\boldsymbol{r}^{(2)}$ として, 密結合係数として $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{r}^{(1)} & = & \mbox{ ReLU }( x \boldsymbol{C}_1 + \boldsymbol{b}_1), \cr \boldsymbol{r}^{(2)} & = & \mbox{ ReLU }( C_2 \boldsymbol{r}^{(1)} + \boldsymbol{b}_2), \cr \mbox{ 出力 } y & = & \boldsymbol{C}_3 \cdot \boldsymbol{r}^{(2)} + b_3 . \end{array}\right.$
となっているケース(ほぼ最小限セットだな)を考える. $n$ はそうだなあ,たぶん 5 ~ 10 ぐらいでうまくいくだろう(かなり無駄が多いけどな).
さて,関数としてこの全体を NN という名前で呼ぶことにしよう. つまり,$\mbox{output } y = \mbox{NN}( H, \mbox{ input } x)$ という感じだ.
ただし $H$ はこの NN のパラメータ $C_1 \cdots C_3$, $b_1 \cdots b_3$ を適当に一つにまとめたデータだ. 今回は,たとえば下図のようにまとめればよいだろう.
このようにパラメータを一つの量としてまとめておくことで,これらのパラメータを同時に更新して NN を「改善」する手法が使いやすくなる.
具体的には,プログラム中で gradient
を利用「できる」ことがその恩恵にあたる.
NN をどう使うの?
使い方は簡単だ.
まず,既知のデータから一つ適当な $x_k$ を関数 NN に入れると値が出てくるので,これとデータに入っている $g(x_k)$ (教師情報. 出力の正解のこと)を比べて誤差 $f(H, x_k)$ を計算する. 今回の場合,$x_k$ も $g(x_k)$ もスカラーなので,誤差関数 $f$ もスカラーで正なものとして定義しておくと良いだろう.
そして,この誤差 $f(H, x_k)$ が小さくなるように NN のパラメータ $H = ($ $\boldsymbol{C}_1$, $\boldsymbol{b}_1$, $C_2$, $\boldsymbol{b}_2$, $\boldsymbol{C}_3$, $b_3$ $)$を修正するのだ!
どうやって? と思うかもしれないが,これは「最小化問題」という情報系の古典的な問題で,いろんな技術が開発されている. 今回は一番シンプルな勾配法
$\left\{\begin{array}{rcl} H_{ \mbox{new}} & = & H + \Delta H, \cr \Delta H & = & - \gamma \, \mbox{ grad}_H \, f(H) . \end{array}\right.$
に沿ってパラメータ $H$ を改善していく手法でいこう.
深層学習の発展: 何層もあるような neural network で作った関数 $f$ に対して $\mbox{ grad}_H \, f(H)$ をどうやって計算したら良いのか? という問題には,back propagation と呼ばれる技術が「再発見」されたことである意味解決した. ちなみにこの back propagation は自動微分2と呼ばれる技術の一部で応用数学では広く知られたものなので,情報系研究者の勉強不足と指摘されてもしかたない. ただし,これだけでは「10層以上の深い層を持つ neural network」の改善がうまくいかず,しばらくこの分野の研究は停滞した.その後,特別な構造のneural network を作ったり ReLU 活性化関数を使うなどの工夫を重ねることで改善がなされて再びこの分野が脚光を浴び,現状に至っている.
さて話を戻そう. この式中の係数 $\gamma > 0$ であるが,これは学習係数と呼ばれるパラメータに対応していて,スカラー関数 $f$ に対して数値計算的には
\[ \gamma = \frac{f(H)}{ \left\| \mbox{grad}_H f(H) \right\|^2 } \]
という感じに計算すると妥当な感じだ3. ただし,この計算式をそのまま使うと $\mbox{grad}_H f(H) \cong \boldsymbol{0}$ の時に不安定になるので, $\mbox{grad}_H f(H)$ がある程度小さい時はこの $\gamma$ を適当な数字に決めてしまうなどの対応をしておくと良い.
そしてこの修正プロセスを,データの個数だけ繰り返せば良いんじゃね? というのが今回の全体の大雑把な NN の使い方だ.
これでうまくいくのかって? まあ,まずはやってみるのが一番だ.
実際にやってみる
あとは少しずつプログラムを作っていくだけだ.
まず,今回 gradient
を使いたいので,その機能が入っているパッケージ ForwardDiff
をインストールしておこう.
また,その時点での計算経過状況がわかる便利な ProgressMeter
もインストールしておこう.
1using Pkg
2Pkg.add("ForwardDiff")
3Pkg.add("ProgressMeter")
次に、パッケージの利用宣言と,問題のパラメータを設定してしまおう.
1using LinearAlgebra
2using ForwardDiff: gradient # 誤差の勾配を求めるのに使う
3
4n = 10 # 中間層のサイズ.これは大変小さいほうだ.
次に、対象関数を教師情報の代わりに作ってしまおう.
1predict(x) = round( sin(π*x)/√3 )
プロットして確認しておこう.
1using Plots
2
3X = 0:0.01:1.0
4plot(X, predict)
次に,NN を作ってしまおう.gradient を計算するライブラリの都合を考慮して,(データに多少無駄が入るが)次のような感じになる.
1relu(x) = (x > 0.0) ? x : 0.0 # まず ReLU を実装して,
2
3# Neural Network.
4# 説明したとおり,パラメータをまとめて大きな行列としている.
5# ここでは下記引数の M のことで,上図や全体では H のこと.
6function nn(M, input)
7 C1 = view( M, :, 1 )
8 b1 = view( M, :, 2 )
9
10 C2 = view( M, :, 3:n+2 )
11 b2 = view( M, :, n+3 )
12
13 C3 = view( M, :, n+4 )
14 b3 = M[1, n+5]
15 # 1 x 1 行列を参照すると行列のままなので,スカラーとしてコピー.
16
17 r1 = relu.( input * C1 + b1 ) # 中間層1 出力
18 r2 = relu.( C2 * r1 + b2 ) # 中間層2 出力
19 r3 = dot(C3, r2) + b3 # 出力
20
21 return r3
22
23end
誤差の計算は,単なる(スカラー値の)差の二乗にしておこう.
1# 損失関数.要は,出力の誤差.
2loss(x,y) = (x - y)^2
さてそろそろ計算そのものの準備に入ろう.まずは,肝心のパラメータ群の初期値を乱数で適当に生成する.
1# NN のパラメータ行列 H の初期値を乱数で生成.これを少しずつ修正する.
2H = rand(n,n+5) .- 0.5
10×15 Matrix{Float64}:
0.324676 -0.317048 -0.383325 … -0.192679 0.251531 -0.355099
0.356019 -0.24486 0.183784 -0.288493 -0.318379 -0.357167
…
-0.280295 0.465438 -0.266818 0.128114 0.0609274 -0.358294
-0.375012 0.481077 -0.357738 0.290997 0.249286 -0.0404348
このパラメータで関数 NN はどうなっているかをプロットして見ておこう.
1# 最初はこんな関数が実現されている(乱数によるのでやるたびに異なる)
2plot( X, x-> nn(H, x))
乱数で作っているので当たり前だけど,ターゲット関数とはまるで異なるよな.
では,肝心の計算だ! かなり簡単だぞ.
1f(H, x) = @inbounds loss(predict(x), nn(H,x))
2# gradient の為に,出力の誤差を関数の形に書く.
3
4using ProgressMeter
5
6@showprogress for i in 1:200000 # 200000個のデータがある想定で.
7 x = rand() # 適当に input x を選んで(本来は集めたデータの入力値)
8 output = f(H,x) # 出力のズレ. 小さくしたい.
9
10 grad_f = @inbounds gradient(H -> f(H,x), H)
11 # 誤差の H に対する勾配
12
13 grad_size = norm(grad_f)^2 # 勾配の大きさ
14
15 if grad_size < 0.05 # 学習係数の調整(数字は適当)
16 γ = 0.1
17 else
18 γ = output / grad_size
19 end
20
21 H += -γ * grad_f # パラメータ H を修正.
22end
Progress: 100%|███████████████████████████████| Time: 0:00:43
教師データとして関数 predict を使っているのは「ズル」と言えばずるいので,真面目にやるならデータを作っておこうw
さて, 学習がうまくいったのかどうか,学習して作り出した近似関数をグラフでチェックしよう.
1plot( X, x-> nn(H, x))
おお! なんかうまくいったことがわかる. なんにも考えないでループを回せばうまくいくんだから,機械学習というのは確かに「良い」方法と言えるのでは,と思うのも無理のないところだ4.
ちなみに,修正後のパラメータ群の数字を以下のようにして見てみると,
1H
10×15 Matrix{Float64}:
2.16088 -0.684512 -0.348622 … -0.384659 -0.418084 0.99499
-0.0369155 -0.0673743 0.188117 -0.443641 -1.16176 -0.26709
1.43753 -0.452245 -1.59859 0.504091 -1.5348 -0.103272
3.13979 -0.994335 -0.889803 -0.0143945 -0.439614 0.158276
-0.353866 -0.0106096 0.539433 -0.651369 -1.34093 -0.388756
-0.460834 -0.00182587 0.0273217 … -0.456312 -0.261016 0.484723
-0.139576 -0.295212 0.41557 -0.266272 -0.57162 0.300701
0.0408971 -0.0544111 0.246577 -0.36382 0.0127106 0.285158
0.253862 -0.42983 0.760308 -1.75478 1.5275 0.253707
-2.71904 1.83835 -0.501698 0.342122 -0.716029 -0.0598684
という感じだ. よーく見ると,たとえば $\boldsymbol{C}_1$ (上の第1列相当), $\boldsymbol{b}_1$ (上の第2列相当) は第 1, 3, 4, 10 成分が比較的大きいので,その 4つの成分で最初の中間層の出力 $\boldsymbol{r}^{(1)}$ の性質がおおよそ見えるのではないか,などということが考察される.そのあたりを追いかけてみるのも面白いだろう.
もっと現実的な問題に適用してみよう!
この
画像ファイル
は
クリエイティブ・コモンズ 表示-継承 3.0 非移植ライセンス
のもとに利用を許諾されています。
例えば,がく片の長さと幅,花びらの長さと幅という 4つの数字と「その花の種類(3種類: ヒオウギアヤメ, ブルーフラッグ, バージニカ)」をデータ化した
UCI のアヤメのデータ
をもとに,その4つの数字だけから花の種類を当てる問題を考えよう.
このデータだけではなく,
UCI の Machine Learning Repository
には機械学習に使えそうな多くの「データ」があるので見てみると良いだろう.
ちなみにこのアヤメのデータは現時点だと「最も人気のある」ものだね.
上のアヤメのデータだが,直接自分でファイルをダウンロードしてそのファイルを読み込む形で Julia に入力してもよいが,これは有名なデータなので
RDatasets
や MLDatasets
5といったいろいろなパッケージがこのデータを自動でダウンロードしてくれる.
今回は下記に示すように, MLDatasets
パッケージをインストールしてそこから読み込もう.
なお,データの使い方等は MLDatasets.jl を見ると,このアヤメ(iris)のデータをどう使えばよいかが書いてある.
まず,MLDatasets
パッケージをインストールしていない人はインストールしよう.
1using Pkg
2Pkg.add("MLDatasets")
それから,「表で表されるような」データを管理するのに適したパッケージ DataFrames
と
機械学習フレームワークである Flux
パッケージもインストールしよう.
この中にある機能や関数をいくつか使うのだ.
1Pkg.add("DataFrames")
2Pkg.add("Flux")
次に,使いそうなパッケージの利用宣言だ.
1using LinearAlgebra
2using DataFrames
3using Flux
4# Flux にも gradient (同じ機能)があるので ForwardDiff は不要.
5using MLDatasets
6using Statistics
7using ProgressMeter
そして,アヤメのデータを読み込もう.
1iris = Iris( as_df = false )
2# アヤメのデータをダウンロードし,かつ,iris という変数に入れる.
3# 今回は単なる配列形式でのダウンロードで良いだろう.
データに初めてアクセスする際に,ネットワーク経由でデータが(原理的には)一回だけダウンロードされる.
その際, おそらく次のように「データセットをダウンロードしますか?」と聞いてくる.
環境によっては,このメッセージが返ってくるまで数分かかることもあるので,じっと待とう!
このメッセージが出てきたら,その中に含まれる stdin>
の右側にある入力欄に y
(とEnterキーを)を入力しよう.
This program has requested access to the data dependency Iris.
which is not currently installed. It can be installed automatically, and you will not see this message again.
Dataset: The Iris dataset
Website: https://archive.ics.uci.edu/ml/datasets/Iris
Do you want to download the dataset from ["https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data"] to "c:\julia\PKG\v1.10\scratchspaces\124859b0-ceae-595e-8997-d05f6a7a8dfe\datadeps\Iris"?
[y/n]
stdin> □□□□□□□□□
するとダウンロードが始まり,完了すると次のような出力が出るはずだ.
dataset Iris:
metadata => Dict{String, Any} with 4 entries
features => 4×150 Matrix{Float64}
targets => 1×150 Matrix{InlineStrings.String15}
dataframe => nothing
次に,使いやすいようにデータを分離して変数に格納しておこう.
1features_raw = iris.features
2labels_raw = reshape(iris.targets, :, 1)
3# 花の4つの数字情報と,花の種類を配列に入れる.
4# 扱いやすいよう,種類の配列を転置しておく.
150×1 Matrix{InlineStrings.String15}:
"Iris-setosa"
"Iris-setosa"
"Iris-setosa"
…略…
2次元配列 features_raw
の i列目に i番目の花の 4つの数字データが入り,1次元配列 labels_raw[i]
にそのアヤメの種類が入るという格好だ.
ちなみに,150個あるデータのうち,最初の50個が "iris setona"(ヒオウギアヤメ)のもので, 次の50個が "iris versicolor"(ブルーフラッグ アヤメ), 最後の50個が "iris virginica"(バージニカ アヤメ)のものである.
データの軽い事前チェック
とりあえずこれらのデータで花の種類を分類できそうかどうか,次のようにしてちょっとグラフで見てみよう. ただし,われわれは3次元グラフまでしか理解できないので,とりあえず 3つの数字(がくの長さ,幅,花びらの長さ)でプロットしてみる.
まず,面倒だが,データを花の種類ごとに分けてみる.
1X1 = features_raw[1, 1:50]
2Y1 = features_raw[2, 1:50]
3Z1 = features_raw[3, 1:50]
4W1 = features_raw[4, 1:50]
5
6X2 = features_raw[1, 51:100]
7Y2 = features_raw[2, 51:100]
8Z2 = features_raw[3, 51:100]
9W2 = features_raw[4, 51:100]
10
11X3 = features_raw[1, 101:150]
12Y3 = features_raw[2, 101:150]
13Z3 = features_raw[3, 101:150]
14W3 = features_raw[4, 101:150]
そしてこれを以下のようにプロットしてみよう.
1using Plots
2
3default( markersize = 4, camera = (30,10) )
4
5scatter( X1, Y1, Z1, xaxis=("sepal length (cm)"), yaxis=("sepal width (cm)"), zaxis=("petal length (cm)"), label = "Iris setosa" )
6scatter!( X2, Y2, Z2, label = "Iris versicolor" )
7scatter!( X3, Y3, Z3, label = "Iris virginica" )
ふむ,目で見ても結構別れているので,NN に学習させてもうまくいくと期待して良さそうな気がするね.
話を戻して,まずは文字で記載されている分類データを確率分布表記に.
アヤメの種類が文字列のままだと使いにくいので,次の形に変換しておこう.
1# 名前をもらって,どの分類かに書き換える関数.
2function RawToNum(str)
3 if str == "Iris-setosa"
4 return [ 1.0, 0, 0 ]
5 elseif str == "Iris-versicolor"
6 return [ 0, 1.0, 0 ]
7 else
8 return [ 0, 0, 1.0 ]
9 end
10end
11
12# この labels にベクトルの形で分類データが入る.
13labels = RawToNum.(labels_raw)
150×1 Matrix{Vector{Float64}}:
[1.0, 0.0, 0.0]
[1.0, 0.0, 0.0]
[1.0, 0.0, 0.0]
⋮
[0.0, 0.0, 1.0]
[0.0, 0.0, 1.0]
[0.0, 0.0, 1.0]
なぜこんな形で分類結果を表記するのかは,NN の出力と合わせるためだ.
あとでわかってくるが,これは確率分布表記をしていると思えば良い.
たとえば最初の [1.0, 0.0, 0.0]
というのはこのデータがヒオウギアヤメである確率が 1.0, ブルーフラッグである確率が 0, バージニカである確率が 0 だ,という意味だ.
データの一部を,あとで検証に使えるように触らずにとっておく.
さて次に,このデータのうち一部を「学習後の NN の能力チェック用」に分離してとっておき,それには NN の学習が済むまで参照しないようにしよう. 全部でたった 150個しかデータがないのでもったいないが,これを別にしておかないと能力チェックが困難になってしまう.
1# 学習後のチェックに使うデータをとっておく.学習には用いない.
2# 3種類の花のデータを5個ずつ,チェック用に抜き出しておく.
3
4toCheckIDs = vcat( 46:50, 96:100, 146:150 )
5features_raw_toCheck = features_raw[:, toCheckIDs ]
6labels_toCheck = labels[toCheckIDs]
15-element Vector{Vector{Float64}}:
[1.0, 0.0, 0.0]
[1.0, 0.0, 0.0]
⋮
残りは学習に使えるデータだ.
1# 学習に使うデータ.
2
3SampleIDs = setdiff( 1:150, toCheckIDs )
4features_raw_sample = features_raw[:, SampleIDs ]
5labels_sample = labels[SampleIDs]
135-element Vector{Vector{Float64}}:
[1.0, 0.0, 0.0]
[1.0, 0.0, 0.0]
⋮
データの数字の大きさをなるべく揃える
次に,花のがく片の長さ…等の数字を正規化しよう. それぞれの数字の大きさ等が異なるのに NN で混ぜるのは NN の性能を下げるだけなので,こうしておこう.
1# 入力データの数字的な偏りをなるべくなくしておく.
2# ただし,事前に使える平均,偏差はサンプル値からしかとれない.
3
4Std = [ std( features_raw_sample[i, :] ) for i in 1:4 ]
5Mean = [ mean( features_raw_sample[i, :] ) for i in 1:4 ]
6
7features_toCheck = (features_raw_toCheck .- Mean) ./ Std
8features_sample = (features_raw_sample .- Mean) ./ Std
ニューラルネットワーク(NN)を作るぞ
さて,肝心の NN そのものを定義しよう. 先の例題よりずっと難しいはずの問題だから,少し n を大きくし,また 中間層も増やそう.
1# NN の定義.少し層を増やした.まあこれでも小さい方だな.
2
3n = 20
4
5function nn(M, input)
6 Ci = view( M, :, 1:4 )
7 bi = view( M, :, 5 )
8
9 C1 = view( M, :, 6:n+5 )
10 b1 = view( M, :, n+6 )
11
12 C2 = view( M, :, n+7:2n+6 )
13 b2 = view( M, :, 2n+7 )
14
15 C3 = view( M, :, 2n+8:3n+7 )
16 b3 = view( M, :, 3n+8 )
17
18 C4 = view( M, :, 3n+9:4n+8 )
19 b4 = view( M, :, 4n+9 )
20
21 C5 = view( M, :, 4n+10:5n+9 )
22 b5 = view( M, :, 5n+10 )
23
24 Co = ( view( M, :, 5n+11:5n+13 ) )' # 転置
25 bo = view( M, 1:3, 5n+14 )
26
27 r1 = sigmoid.( Ci * input + bi ) # 入力値処理
28
29 r2 = sigmoid.( C1 * r1 + b1 )
30 r3 = sigmoid.( C2 * r2 + b2 ) # 中間層.ReLU や TanhShrink だとうまくいかない.
31 r4 = sigmoid.( C3 * r3 + b3 )
32 r5 = sigmoid.( C4 * r4 + b4 )
33 r6 = sigmoid.( C5 * r5 + b5 )
34
35 output = softmax( Co * r6 + bo ) # 出力値処理
36
37 return output
38end
あとはこの NN のパラメータの初期値を用意すれば良い.
1# NN のパラメータ行列 H の初期値を乱数で生成.これを少しずつ修正する.
2H = 10.0 * ( rand(n,5n+14) .- 0.5 )
20×114 Matrix{Float64}:
-2.69872 4.57275 -1.22044 … -1.91681 3.91092 -3.5388
-2.9915 -1.99061 1.77325 2.48023 3.70738 4.05232
-3.27424 2.83662 -1.68226 -0.481219 0.226912 3.85234
4.80131 1.52694 -0.40866 3.49361 -1.90544 0.247762
-2.86762 -4.7581 3.69804 0.0691466 1.67901 -4.7041
…
-3.76627 4.71857 -0.785994 … -3.53204 0.51036 -1.61283
4.65462 0.155111 -1.06677 -4.4002 4.79697 -3.02118
-0.904377 1.00613 0.0831876 2.2009 3.73551 -0.146672
-2.16668 4.22355 -4.95464 0.900292 -1.46091 0.253256
3.45072 0.147795 4.43228 2.97151 -2.85828 -1.84148
あとは誤差(損失関数)を定義する.
あとは誤差(損失関数)を定義しないといけないね.
1# 結果の誤差は,分類問題での定番である crossentropy にて.
2loss( output, v_true ) = Flux.crossentropy( output, v_true )
もう NN に学習させることができるぞ.
少し強引だが,下記のように学習させてしまおう.
1# NN の学習
2
3# 誤差の式を簡単に書いておいて…
4g(H, i) = loss( nn(H, features_sample[:, i]), labels_sample[i] )
5
6itr = 500
7datasize = size(labels_sample)[1]
8
9@showprogress for i in 1:(itr * datasize)
10
11 num = rand(1:datasize) # データの順序に依存しないように乱数で
12
13 grad_f = Flux.gradient(M -> g(M, num), H)[1]
14 # 誤差の H に対する勾配.
15 # 今回は Flux の gradient を使う(機能は同じ.こちらは最後に [1] をつけて結果を取り出すところが違う)
16
17 grad_size = norm(grad_f)^2 # 勾配の大きさ
18
19 if grad_size < 0.01 # 学習係数の調整(数字は適当)
20 γ = 0.1
21 else
22 γ = g(H,num) / grad_size
23 end
24
25 H += -γ * grad_f # パラメータ H を修正.
26end
Progress: 100%|███████████████████████████████| Time: 0:00:35
学習はうまくいったのか? チェックしよう.
うまく学習できたか,少し見てみよう. まずはサンプルデータの1番目だ.
1# チェックのための簡易表記
2nn_sample(i) = nn( H, features_sample[:, i] )
3
4nn_sample(1)
3-element Vector{Float64}:
0.9998904937581873
6.869281427483038e-5
4.081342753802132e-5
この数字は,NN が「1番目のデータはどの分類のアヤメであるかの確率出力を出した」と思えば良い. だからこの場合はこのデータは 1種類目のアヤメ,つまりヒオウギアヤメである確率が高いと言っているわけだ.
実際は
1labels_sample[1]
3-element Vector{Float64}:
1.0
0.0
0.0
であるので,確かにそうであることがわかる.
あと 2点ほど手動でチェックしてみよう.次に,2種類目のアヤメのデータであるはずの 70番目のデータに対する NN の出力を見ると,
1nn_sample(70)
3-element Vector{Float64}:
3.126439966633309e-6
0.9999084714113737
8.840214865961478e-5
となっており,やはりこの場合も正しく学習できていることがわかる.
同様に,3種類目のアヤメのデータである 130番目のデータを NN に入力すると,
1nn_sample(130)
3-element Vector{Float64}:
4.9789523758347484e-5
3.954795680903534e-6
0.9999462556805607
となり,このデータに対してもやはり正しく学習できていることが確認できる.
この問題設定だと,おそらくほぼすべてのデータに対して NN が正しく学習できているだろう. 初期値が乱数によるので,もちろん人によって結果は微妙に異なるが.
これは次のように確かめられる. まず,NN の確率出力をもらってどう判断するか,という操作を次のような関数にしよう.
1# 出力値による判断を明確にする関数
2function decision(x)
3 v = similar(x)
4 max_i = argmax(x)
5
6 for i in 1:size(x)[1]
7 if i == max_i
8 v[i] = 1.0
9 else
10 v[i] = 0.0
11 end
12 end
13
14 return v
15end
こうしておいて,学習に使ったサンプルデータと,NN の出力による判断値の違いを次のようにまとめる.
1error_sample = labels_sample - decision.( nn_sample.(1:135) )
135-element Vector{Vector{Float64}}:
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
⋮
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
たしかに学習はほとんどうまくいっているように見える.ミスが少ないことを確かめておこう.
1sum( norm.(error_sample) )
0.0
うむ,学習に使ったデータ, つまり,既知のデータに対しては(教員のこのケースだと) 学習後の NN は 100% うまく判定できることがこれで確認できた.
学習した NN は未知のデータを上手く扱えるか?
では,肝心の,「初めてみるデータに対して, NN は正しく判定できるか」をチェックしよう. まずは手動でいくつか確認してみよう.
1# 初めて見るデータに対しての NN の判断
2nn_toCheck(i) = nn( H, features_toCheck[:, i])
3
4nn_toCheck(1)
3-element Vector{Float64}:
0.9998873691525286
7.421020842044769e-5
3.8420639050910044e-5
これは本来の 46番目のデータ(花は一種類目であるヒオウギアヤメ)に対する NN の判定だ. ふむ,確かに正しいぞ.
ほかも見てみよう.
1nn_toCheck(6)
3-element Vector{Float64}:
5.4814002385579855e-6
0.9998625671425413
0.0001319514572201146
そしてこれは本来の 96番目のデータ(花は二種類目)に対する NN の判定だ. これも確かに正しい.
次はどうかな.
1nn_toCheck(11)
3-element Vector{Float64}:
0.00011948890769570311
4.581470539412908e-6
0.9998759296217649
これは本来の 146番目のデータ(花は三種類目)に対する NN の判定だ. そしてこれも確かに正しい.
では,NN が学習過程で触ることのなかった未知の 15個のデータに対して,一気にその判定結果をチェックしよう.
1error_toCheck = labels_toCheck - decision.( nn_toCheck.(1:15) )
15-element Vector{Vector{Float64}}:
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, -1.0, 1.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
[0.0, 0.0, 0.0]
おお. 判定ミスはたったの一箇所だ. 未知のデータに対しては,14/15 = 93.3% の正解率というところか. こんな簡単な作りの NN に 135個のデータを与えて学習させることでこの判断正解率なので,なかなか良いと考えて良さそうだ. もっとたくさんデータが与えて学習させれば,よりしっかりとした NN を作ることができるだろうと期待できる.
というわけで,今回は十分に良い機械学習ができたと言えよう. ただし,過学習(過学習についてはレポートにて)の恐れはあるのでそこは調べておいて,機械学習の現場では忘れないようにしよう.
レポート
以下の課題について能う限り賢明な調査と考察を行い,
2024-AppliedMath7-Report-12
という題名をつけて e-mail にて教官宛にレポートとして提出せよ. なお,レポートを e-mail の代わりに TeX で作成した書面にて提出してもよい.
注意
近年はセキュリティ上の懸念から,実行形式のプログラムなどをメールに添付するとそのメールそのものの受信を受信側サーバが拒絶したりする.
そういうことを避けるため,レポートをメールで提出するときは添付ファイルにそういった懸念のあるファイルが無いようにしよう.
まあ要するに,レポートは pdf ファイルにして,それをメールに添付して送るのが良い ということだと思っておこう.
課題
- 最初のターゲット問題の2次元バージョンに取り組んでみよう.
具体的には,図
の中で定義される関数 $S_c$ に従ってデータ $D = \{ \boldsymbol{x}_k, \boldsymbol{y}_k = S_c(\boldsymbol{x}_k) \}_{k=1}^N$ が $N \cong 10,000$ 程度で得られているとする($N$ は好きに設定して良い).
関数 $S_c$ が未知である想定のもとに,今回の授業と同様に NN を構成し,データ $D$ を使ってパラメータを学習させて,NN が関数 $S_c$ を近似的に構成するようにしてみよう.
注: この問題は,2つのパラメータ( $\Omega$ 上の座標 $(x,y)$ のこと)から 2種類の分類(0 か 1 か)ができるように,という機械学習を行うことに相当する. - 資料入門2 機械学習用フレームワークの利用を読み,package
Flux
を一通り使ってみてその様子を報告せよ. - 「過学習」について,文献などを用いて調べ,自分なりに対応策を考えてみよう.考えた対応策がうまくいくかは,細かい工夫によって随分変わったりするので,ここではそこまで実現可能性や効率等についてあまり考えなくて良い.
-
頭脳労働の多くはこうした能力に大いに依存している.例えば翻訳,プログラミング,検査,その他,具体的な例はいくらでもあるだろう. ↩︎
-
とはいえ,実は自動微分は「各分野で再発見されることで有名な技術」で,講演時に「20回以上再発見されている」と言っている人が居た. この回数が本当なのかはともかく,再発見されることが多いのは確かだ.ちなみに,自動微分そのものについては例えば Griewank, Andreas. "On automatic differentiation." Mathematical Programming: recent developments and applications 6.6 (1989): 83-107, なんかが図もあってが分かりやすい. ↩︎
-
この $\gamma$ の式は,実現を期待している $f(H+\Delta H) \cong 0$ の左辺を Tayler 展開した 1次近似式 $f(H) + \mbox{grad}_H f(H) \cdot \Delta H\cong 0$ の $\Delta H$ に勾配法が提案する $\Delta H = - \gamma \, \mbox{ grad}_H f(H)$ を代入すると得られる. ↩︎
-
もちろん,実際の問題はデータ集めからして大変なわけで,そうそうお気楽に考えてはいかんが. ↩︎
-
MLDatasets
パッケージの機能は以前はFlux
パッケージに含まれていたものだ.今でも一応含まれているが,いずれ削除されるだろうから今から分けておくのがベターだね. ↩︎