{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "name": "Medical AI Course Materials : 03_Introduction_to_Neural_Network.ipynb", "version": "0.3.2", "provenance": [], "collapsed_sections": [] }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.2" } }, "cells": [ { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "Da9LzFk5G38W" }, "source": [ "# ニューラルネットワークの基礎\n", "\n", "ここでは,ニューラルネットワーク (Neural Network) についてその概要を紹介していきます.画像認識などに用いられる Convolutional Neural Network (CNN) や,自然言語処理などに用いられる Recurrent Neural Network (RNN) といった手法は,ニューラルネットワークの一種です.\n", "\n", "ここではまず,最もシンプルな全結合型と呼ばれるニューラルネットワークの構造について説明を行ったあと,複数の入力データと望ましい出力の組からなる学習用データセットを準備したとき,どうやってニューラルネットワークを学習させればよいのか(教師あり学習の仕組み)について解説を行います.\n", "\n", "ニューラルネットワークによって表現される複雑な関数を,現実的な時間で学習するための誤差逆伝播法(バックプロパゲーション)と呼ばれるアルゴリズムについても紹介します.\n", "\n", "まずはニューラルネットワークをブラックボックスとして扱ってしまうのではなく,一つ一つ内部で行われる計算を丁寧に調べます.そして,パラメータで特徴づけられた関数で表される線形変換とそれに続く非線形変換を組み合わせて,全体として微分可能な一つの関数を表していることを理解していきます.\n" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "LICMyO4rKUX9" }, "source": [ "## ニューラルネットワークの構造\n", "\n", "まずはニューラルネットワークの構造を図式化して見てみましょう.入力変数が{年数,アルコール度数,色合い,匂い}の4変数,出力変数が{白ワイン,赤ワイン}の2変数の場合を示します.\n", "\n", "![ニューラルネットワークの基本構造](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/01.png)\n", "\n", "この図のひとつひとつの丸い部分のことを**ノード**もしくは**ユニット**と呼び,その縦方向の集まりを**層**と呼びます.そして,一番初めの層を**入力層(input layer)**,最後の層を**出力層(output layer)**,そしてその間を**中間層(intermediate layer)**もしくは**隠れ層(hidden layer)**と呼びます.このモデルは入力層,中間層,出力層の3層の構造となっていますが,中間層の数を増やすことでさらに多層のニューラルネットワークを定義することもできます.この例では各層間の全てのノードが互いに結合されているため,**全結合型のニューラルネットワーク**とも呼び,ニューラルネットワークの最も基礎的な構造です.\n", "\n", "入力変数は前章までと同様ですが,出力変数の扱い方がこれまでと異なります.例えば,上図では出力層の各ノードがそれぞれ白ワインと赤ワインに対応しており,カテゴリの数だけ出力の変数があるということになります.なぜこのような構造となっているのでしょうか." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "meyqKCr-NUDs" }, "source": [ "![ニューラルネットワークの出力値](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/02.png)\n", "\n", "まず,最終層にどのような値が入るのか,具体例を見てみましょう.例えば,年数が3年物でアルコール度数が14度,色合いが0.2,匂いが0.8で表されるワインがあるとします.内部の計算は後述するとして,このようなデータをニューラルネットワークに与えたときに結果として得られる値に着目してみましょう.上図では,白ワイン $y_{1} = 0.15$, 赤ワイン $y_{2}= 0.85$ となっています.このとき,出力値の中で最も大きな値となっている変数に対応するクラス,すなわち今回の例では「赤ワイン」をこの分類問題におけるこのニューラルネットワークの**予測結果**とすることができます.\n", "\n", "ここで出力層の全ての値を合計してみると,1になっていることに気づきます.これは偶然ではなく,そうなるように出力層の値を計算しているためです* .つまり,出力層のそれぞれのノードが持つ数値は,入力がそれぞれのクラスに属している確率を表していたのでした.そのため,カテゴリ数と同じ数だけ出力層にはノードが必要となります.\n", "\n", "それでは,ここからニューラルネットワークの内部で行われる計算を詳しく見ていきましょう.ニューラルネットワークの各層は,前の層の値に線形変換と非線形変換を順番に施すことで計算されています.まずは,ここで言う線形変換とは何を表すのか,から見ていきましょう.\n", "\n", "\\* 具体的には,Softmax関数という活性化関数(これも後述します)をニューラルネットワークの出力ベクトルに適用することで,出力層における全ノードの値の合計が1になるようにします." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "X9-RYTlXNzNQ" }, "source": [ "### 線形変換\n", "\n", "ここでは,ニューラルネットワークの各層で行われる線形変換について説明します.\n", "\n", "![ニューラルネットワークの線形変換](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/linear_transformation.png)\n", "\n", "ここで言う線形変換* とは,重み行列 (${\\bf W}$) $\\times$ 入力ベクトル (${\\bf h}$) $+$ バイアスベクトル (${\\bf b}$) のような計算のことを指しています.このとき,この変換の入力が${\\bf h}$,パラメータが${\\bf W}$と${\\bf b}$となります.ここでの掛け算($\\times$)は行列の掛け算であることに注意してください.また,これからは,$h$ が文字としてよく登場しますが,これは隠れ層 (hidden layer) の頭文字である $h$ から来ています.ただし,表記を簡潔にするため以下では入力層(上図における $x_1, x_2, x_3, x_4$)を**0層目の隠れ層と考える**ことにして, $h_{01}, h_{02}, h_{03}, h_{04}$ と表記します.では上図で表される計算を数式で記述してみましょう.\n", "\n", "(* 通常数学では線形変換とは ${\\bf w} \\times {\\bf h}$ のことを指し,この変換は厳密には「アファイン変換(もしくは アフィン変換)」と呼ばれるものです.しかし,深層学習の文脈ではこの変換も線形変換と呼ぶことも多いです.)\n", "\n", "$$\n", "\\begin{aligned}\n", "u_{11}&=w_{11}h_{01}+w_{12}h_{02}+w_{13}h_{03}+w_{14}h_{04}+b_{1} \\\\\n", "u_{12}&=w_{21}h_{01}+w_{22}h_{02}+w_{23}h_{03}+w_{24}h_{04}+b_{2} \\\\\n", "u_{13}&=w_{31}h_{01}+w_{32}h_{02}+w_{33}h_{03}+w_{34}h_{04}+b_{3}\n", "\\end{aligned}\n", "$$\n", "\n", "バイアス($b_1, b_2, b_3$)は上図では省略されていることに注意してください.さて,以上の4つの式は,ベクトルと行列の計算として以下のように書き直すことができます.\n", "\n", "$$\n", "\\begin{aligned}\n", "\\begin{bmatrix}\n", "u_{11} \\\\\n", "u_{12} \\\\\n", "u_{13}\n", "\\end{bmatrix}&=\\begin{bmatrix}\n", "w_{11} & w_{12} & w_{13} & w_{14} \\\\\n", "w_{21} & w_{22} & w_{23} & w_{24} \\\\\n", "w_{31} & w_{32} & w_{33} & w_{34}\n", "\\end{bmatrix}\\begin{bmatrix}\n", "h_{01} \\\\\n", "h_{02} \\\\\n", "h_{03} \\\\\n", "h_{04}\n", "\\end{bmatrix}+\\begin{bmatrix}\n", "b_{1} \\\\\n", "b_{2} \\\\\n", "b_{3}\n", "\\end{bmatrix}\\\\\n", "{\\bf u}_{1}&={\\bf W}{\\bf h}_{0}+{\\bf b}\n", "\\end{aligned}\n", "$$\n", "\n", "本来は ${\\bf W}$ や ${\\bf b}$ にも,どの層とどの層の間の計算に用いられるものなのかを表す添え字をつけるべきですが,ここでは簡単のため省略しています." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "JmlTx1YF4xMD" }, "source": [ "### 非線形変換\n", "\n", "次に,非線形変換について説明します.線形変換のみでは,下図右のように入力と出力の間が非線形な関係である場合,両者の間の関係を適切に表現することができません.\n", "\n", "![入力と出力の関係](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/04.png)\n", "\n", "そこで,ニューラルネットワークでは各層で線形変換に引き続いて非線形変換を施すことで,全体の関数が非線形性を持つようにしています.この非線形変換を行う関数を,ニューラルネットワークの文脈においては **活性化関数** と呼びます.\n", "\n", "上図の線形変換の結果 $u_{11}, u_{12}, u_{13}$ に活性化関数を使って非線形変換を行った結果を $h_{11}, h_{12}, h_{13}$ と書き,これらを活性値(activation)と呼びます(下図参照).これが次の層への入力となります.\n", "\n", "![活性値](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/activation.png)\n", "\n", "活性化関数の具体例としては,下図に示す **ロジスティックシグモイド関数**(以下シグモイド関数)\n", "\n", "![シグモイド関数](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/05.png)\n", "\n", "が従来,よく用いられてきました.しかし近年,層数が多いニューラルネットワークではシグモイド関数は活性化関数としてほとんど用いられていません.その理由の一つは,シグモイド関数を活性化関数に採用することで **勾配消失** という現象が起きやすくなり,学習が進行しなくなる問題が発生することがあったためです.これは後で詳述します.これを回避するために,**Rectified Linear Unit (ReLU)** という関数がよく用いられます.これは,以下のような形をした関数です.\n", "\n", "![ReLU関数](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/06.png)\n", "\n", "ここで, ${\\rm max}(0, u)$ は,$0$と$u$を比べて大きな方を返す関数です.すなわち,ReLUは入力が負の値の場合には出力は0で一定であり,正の値の場合は入力をそのまま出力するという関数です.シグモイド関数では,入力が小さな,もしくは大きな値をとった際に,勾配がどんどん小さくなってしまうだろうことがプロットからも見て取れます.それに対し,ReLU関数は入力の値がいくら大きくなっても,一定の勾配が発生します.これがのちほど紹介する勾配消失という問題に有効に働きます." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "s-eFDAuJ-Cse" }, "source": [ "### 数値を見ながら計算の流れを確認\n", "\n", "ここで,下図に書き込まれた具体的な数値を使って,入力 $x_1, x_2, x_3$ から出力 $y$ が計算される過程を確認してみましょう.今は計算を簡略化するためバイアス ${\\bf b}$ の計算は省略します(バイアスが全て0であるとします).数値例として,${\\bf x} = \\begin{bmatrix} 2 & 3 & 1 \\end{bmatrix}^T$ が与えられた時の出力 $y$ の計算手順を一つ一つ追いかけてみましょう.\n", "\n", "![出力までの計算例](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/output.png)\n", "\n", "前章で解説した重回帰分析では,目的関数のパラメータについての導関数を0とおいて解析的に最適なパラメータを計算できましたが,ニューラルネットワークでは一般的に,解析的にパラメータを解くことはできません.その代わり,この導関数の値(勾配)を利用した別の方法でパラメータを逐次的に最適化していきます.\n", "\n", "このため,ニューラルネットワークの場合は,**まずパラメータを乱数で初期化し,ひとまずデータを入力して目的関数の値を計算します**.次にその関数の勾配を計算して,それを利用してパラメータを更新し,その更新後の新しいパラメータを使って再度入力データを処理して目的関数の値を計算し…といったことを繰り返し行っていくことになります.\n", "\n", "今,パラメータを初期化した結果,上の図のグラフの枝に与えられているような数値になった状態で,入力層の値に線形変換を施すところまでを考えてみましょう.この計算は,以下のようになります.\n", "\n", "$$\n", "\\begin{aligned}\n", "u_{11}&=3\\times 2+1\\times 3+2\\times 1=11\\\\\n", "u_{12}&=-2\\times 2-3\\times 3-1\\times 1=-14\n", "\\end{aligned}\n", "$$\n", "\n", "次に非線形変換を行う活性化関数としてReLU関数を採用し,以下のように中間層の値を計算してみましょう.\n", "\n", "$$\n", "\\begin{aligned}\n", "h_{11} &= \\max(0, 11) = 11 \\\\\n", "h_{12} &= \\max(0, -14) = 0\n", "\\end{aligned}\n", "$$\n", "\n", "同様に,出力層の $y$ の値までを計算すると,\n", "\n", "$$\n", "y = 3 \\times 11 + 2 \\times 0 = 33\n", "$$\n", "\n", "となります.\n", "\n", "さて,次節からは,パラメータを,どうやって更新していくかを見てみましょう.\n" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "ivVppUOibSHy" }, "source": [ "## 目的関数\n", "\n", "ニューラルネットワークでも,微分可能でさえあれば解きたいタスクに合わせて様々な目的関数を利用することができます." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "BCGZbTKcbUpe" }, "source": [ "### 平均二乗誤差\n", "\n", "例えば,出力層に$N$個の値を持つニューラルネットワークで回帰問題を解く場合を考えてみましょう.$N$個の出力それぞれ($y_n (n=1, 2, \\dots, N)$)に対して望ましい出力($t_n (n=1, 2, \\dots, N)$)が与えられたとき,目的関数をそれぞれの出力($y_n$)と対応する正解($t_n$)の間の **平均二乗誤差(mean squared error)** とすることで,回帰問題を解くことができます.\n", "\n", "$$\n", "\\mathcal{L} = \\dfrac{1}{N} \\sum_{n=1}^{N}(t_{n} - y_{n})^{2}\n", "$$\n", "\n", "これを最小にするようにニューラルネットワーク中のパラメータを決定するわけです.例えば,上図の例で正解として $t = 20$ が与えられたときの目的関数の値は,\n", "\n", "$$\n", "\\mathcal{L} = \\dfrac{1}{1} (20 - 33)^2 = 169\n", "$$\n", "\n", "です.これを小さくするような重み行列の値を探せばよいということです." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "WqFc0jYJFLYz" }, "source": [ "### 交差エントロピー\n", "\n", "一方,分類問題の場合はしばしば **交差エントロピー(cross entropy)** が目的関数として利用されます.\n", "\n", "例として,$N$クラスの分類問題を考えてみましょう.ある入力 $x$ が与えられたとき,ニューラルネットワークの出力層に $N$ 個のノードがあり,それぞれがこの入力が $n$ 番目のクラスに属する確率 $y_n = p(y=n|x)$ を表しているとします.これは,入力 $x$ が与えられたという条件のもとで,予測クラスを意味する $y$ が $n$ であるような確率ということです.\n", "\n", "ここで,$x$ が所属するクラスについての正解が, ${\\bf t} = \\begin{bmatrix} t_1 & t_2 & \\dots & t_N \\end{bmatrix}^T$ というベクトルで与えられているとします.ただし,このベクトルは $t_n (n=1, 2, \\dots, N)$ のいずれか1つだけが1であり,それ以外は0であるようなベクトルであるとします. これを **1-hotベクトル** と呼びます.そして,この1つだけ値が1となっている要素は,その要素のインデックスに対応したクラスが正解であることを意味します.例えば,$t_3 = 1$であれば3というインデックスに対応するクラスが正解であるということになります.\n", "\n", "さて,このような準備を行うと,交差エントロピーは以下のように計算できるものとして記述することができます.\n", "\n", "$$\n", "\\mathcal{L} = - \\frac{1}{N} \\sum_{n=1}^{N}t_{n}\\log y_{n}\n", "$$" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "kxqfo4Yrekaw" }, "source": [ "#### 補足:交差エントロピーについて\n", "\n", "以下は,交差エントロピーの定義について知りたい方だけ参考にしてください.情報理論などで交差エントロピーの定義を知っている方は上の式で表されるものが交差エントロピーとは違うようにみえるかもしれません.しかしこれは,以下のように説明できます.今,$q(y|x)$をニューラルネットワークのモデルが定義する条件付き確率とし,$p(y|x)$を実データの条件付き確率とします.ここで,$p(y|x)$は実際には未知であるため,代わりに学習データの経験分布 \n", "\n", "$$\n", "\\hat{p}(y|x) = \\frac{1}{N} \\sum_{n=1}^N I(x =x_n, y=y_n)\n", "$$\n", "\n", "を用いることとします.ただし $I$ はディラック関数とよばれ,その等号が成立する時,値が$\\infty$,それ以外では$0$であるような関数で,その定義域全体にわたる積分は1になるものです.この時,確率分布 $\\hat{p}(y|x)$ と $q(y|x)$ の間のKLダイバージェンス(確率分布間の距離を測り、確率分布が一致する時、またその時のみ$0$となり、それ以外は正の値をとる)は\n", "\n", "$$\n", "KL(p||q) = \\int_{x, y} \\hat{p}(y|x) \\log \\frac{\\hat{p}(y|x)}{q(y|x)} dx dy\n", "$$\n", "\n", "と定義されます.ここでディラックのデルタ関数の定義を用い,また$q$に依存する項だけを抜き出すと,先程の交差エントロピーの目的関数が導出されます.\n" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "Drh35j_lCYiy" }, "source": [ "## ニューラルネットワークの最適化\n", "\n", "目的関数の値を最小にするようなパラメータの値を決定することが,ニューラルネットワークの学習の目的であるとわかりました.では,どのようにしてそのパラメータを探し当てればよいのでしょうか.ある目的関数が与えられたもとで,その目的関数が望ましい値をとるようにニューラルネットワークのパラメータを決定することを,ニューラルネットワークの最適化といいます.\n", "\n", "最適化の方法を考える前に,まず最適化の対象とはなんであったか,再度確認しましょう.「ニューラルネットワークを最適化する」とは,すなわち「ニューラルネットワークが内部で用いている全てのパラメータの値を適切に決定する」という意味です.では,ニューラルネットワークにおけるパラメータとは,何だったでしょうか.それは,ここまで紹介したシンプルな全結合型ニューラルネットワークの場合,各層の線形変換に用いられていた ${\\bf W}$ と ${\\bf b}$ のことを指します.\n", "\n", "ニューラルネットワークの各パラメータを,目的関数に対する勾配を0とおいて解析的に解くことは,一般的には困難です.しかし,実データをニューラルネットワークに入力すれば,その入力の値における目的関数の勾配を数値的に求めることは可能です.この値が分かれば,パラメータをどのように変化させれば目的関数の値を小さくすることができるのかが分かります.そこで,この勾配を使ってパラメータを繰り返し少しずつ更新していくことで,ニューラルネットワークの最適化を行うことができるのです.この方法について順を追って考えていきましょう.\n", "\n", "まず,以下の図を見てください.図中の点線は,パラメータ $w$ を変化させた際の目的関数 $\\mathcal{L}$ の値を表しています.この例では簡単のため二次関数の形になっていますが,ニューラルネットワークの目的関数は実際には多次元で,かつもっと複雑な形をしていることがほとんどでしょう.しかし,ここでは説明のためこのようにシンプルな形を想像してみましょう.さて,この目的関数が最小値を与えるような $w$ は,どのようにして発見できるでしょうか.\n", "\n", "![パラメータと目的関数の関係(イメージ)](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/13.png)\n", "\n", "前節で説明したように,ニューラルネットワークのパラメータはまず乱数で初期化されます.ここでは,例として $w=3$ という初期化が行われたと考えてみましょう.そうすると,$w=3$における$\\mathcal{L}$の勾配 $\\frac{\\partial \\mathcal{L}}{\\partial w}$ が求まります.ニューラルネットワークの目的関数は,全てのパラメータについて微分可能である* ためです.さて,ここでは仮に $w=3$ における $\\frac{\\partial \\mathcal{L}}{\\partial w}$ が $3$ であったとしましょう(このことを$\\frac{\\partial \\mathcal{L}}{\\partial w} |_{w=3} = 3$と書きます).すると,以下の図のように,この $3$ という値は $w=3$ における $\\mathcal{L}(w)$ という関数の接線の傾き(勾配; gradient)を表しています.\n", "\n", "(* 厳密には損失関数に微分不可能な点が存在する可能性はあります.例えばReLUは $x=0$ の点で微分不可能なため,ReLUを含んだニューラルネットワークには微分不可能な点があります.しかし,通常使うニューラルネットワークの場合,そのような微分不可能な点はわずかしかないため,以下に説明する最適化の方法の中では,無視できます.)\n", "\n", "![目的関数の接線の傾き](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/11.png)\n", "\n", "傾きとは,$w$ を増加させた際に $\\mathcal{L}$ が増加する方向を意味しているので,今は $\\mathcal{L}$ の値を小さくしたいわけですから,この傾きの逆方向へ $w$ を変化させる,すなわち $w$ **から** $\\partial \\mathcal{L} / \\partial w$ **を引けばよい** ことになります.\n", "\n", "これがニューラルネットワークのパラメータを目的関数の勾配を用いて更新していく際の基本的な考え方です.このときの $w$ のステップサイズ(更新量)のスケールを調整するために,勾配に **学習率 (learning rate)** と呼ばれる値を乗じるのが一般的です.\n", "\n", "例えば,今学習率を $0.5$ に設定してみます.そうすると,$w$の更新量は **学習率** $\\times$ **勾配** で決まるので,$0.5 \\times 3 = 1.5$ となります.現在 $w=3$ なので,**この値を引いて** $w \\leftarrow w - 1.5$ と更新した後は, $w=1.5$ となります.上の図は,この1度の更新を行ったあとの状態を表しています.\n", "\n", "1度目の更新を行って,$w$ が $w = 1.5$ の位置に移動しました.そこで,再度この点においても勾配を求めてみます.今度は $-1$ になっていたとしましょう.すると **学習率** $\\times$ **勾配** は $0.5 \\times -1 = -0.5$ となります.これを再び用いて,$w \\leftarrow w - (-0.5)$ と2度目の更新を行うと,今度は $w = 2$ の位置にくるでしょう.このようにして,2回更新したあとは,以下の図のようになります.\n", "\n", "![パラメータの更新](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/12.png)\n", "\n", "徐々に$\\mathcal{L}$が最小値をとるときの$w$の値に近づいていっていることが見て取れます.\n", "\n", "こうして,**学習率** $\\times$ **勾配** を更新量としてパラメータを変化させていくと,パラメータ $w$ を求めたい $\\mathcal{L}$ の最小値を与える $w$ に徐々に近づけていくことができます.このような勾配を用いた目的関数の最小化手法を **勾配降下法** と呼びます.ニューラルネットワークは,基本的に **微分可能な関数のみを層間をつなぐ関数として用いて** 設計されるため,登場する関数はすべて微分可能であり,学習データセットを用いて勾配降下法によってパラメータを最適化する方法が適用可能なのです.\n", "\n", "ただし,通常ニューラルネットワークを勾配降下法で最適化する場合は,データを一つ一つ用いてパラメータを更新するのではなく,いくつかのデータをまとめて入力し,それぞれの勾配を計算したあと,その勾配の平均値を用いてパラメータの更新を行う方法がよく行われます.これを **ミニバッチ学習** と呼びます.これは,学習データセットから一様ランダムに $k (>0)$ 個のデータを抽出し,その $k$ 個のデータに対する目的関数の平均の値を小さくするようパラメータを更新することを,異なる $k$ 個のデータの組み合わせに対して繰り返し行う方法です.結果的にはデータセットに含まれる全てのデータを使用していきますが,1度の更新に用いるデータは $k$ 個ずつということになります.実際の実装では,データセット内のサンプルのインデックスをまずランダムにシャッフルして並べた配列を作り,その配列の先頭から $k$ 個ずつインデックスを取り出し,対応するデータを使ってミニバッチを構成します.こうして,全てのインデックスを使い切ること,すなわちデータセット内のデータを1度ずつ,すべてパラメータ更新に用い終えることを **1エポックの学習** と呼びます.そして,この $k$ をバッチサイズもしくはミニバッチサイズと呼び,このような学習方法を指して,**確率的勾配降下法 (SGD: Stocastic Gradient Descent)** という名前が用いられます.現在ほとんど全てのニューラルネットワークの最適化手法はこのSGDをベースとした手法となっています.SGDを用いると,全体の計算時間が劇的に少なくできるだけでなく,下図のように目的関数が凸関数でなかったとしても,多くの場合うまくいくことが経験的に知られており,その理論的な裏付けをしようという試みが近年盛んに行われています.\n", "\n", "![局所最適解と大域最適解](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/14.png)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "gtJOsYWkKPDj" }, "source": [ "### パラメータ更新量の算出\n", "\n", "それでは今,下図のような3層の全結合型ニューラルネットワークを考え,1層目と2層目の間の線形変換が ${\\bf w}_1, {\\bf b}_1$,2層目と3層目の間の線形変換が ${\\bf w}_2, {\\bf b}_2$ というパラメータによって表されているとします(図ではバイアス ${\\bf b}_1, {\\bf b}_2$ は省略されています).また,これらをまとめて $\\boldsymbol{\\Theta}$ と表すことにします.\n", "\n", "![パラメータ更新の例](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/08.png)\n", "\n", "入力ベクトルは ${\\bf x}$,ニューラルネットワークの出力は ${\\bf y} \\in \\mathbb{R}^N$($N$ 次元実数ベクトルという意味)とし,入力 ${\\bf x}$ に対応した“望ましい出力”である教師ベクトルを ${\\bf t}$ とします.ここで,目的関数には前述の平均二乗誤差関数を用いることとしましょう.\n", "\n", "さて,パラメータをそれぞれ適当な乱数で初期化したあと,入力 ${\\bf x}$ が与えられたときの目的関数の各パラメータについての勾配を計算して,それぞれのパラメータについて更新量を算出してみましょう.\n", "\n", "まず,目的関数を改めてベクトル表記を用いて書き下すと,以下のようになります.\n", "\n", "$$\n", "\\mathcal{L}({\\bf y}, {\\bf t}) = \\frac{1}{N} || {\\bf t} - {\\bf y} ||_2^2\n", "$$\n", "\n", "$|| {\\bf t} - {\\bf y} ||_2^2$はここでは$({\\bf t} - {\\bf y})^T({\\bf t} - {\\bf y})$と同等の意味となります.さらに,ニューラルネットワーク全体を $f$ と書くことにすると,出力 ${\\bf y}$ は\n", "\n", "$$\n", "\\begin{aligned}\n", "{\\bf y} &= f({\\bf x}; \\boldsymbol{\\Theta}) \\\\\n", "&= a_2 ( {\\bf w}_2 a_1({\\bf w}_1 {\\bf x} + {\\bf b}_1) + {\\bf b}_2 )\n", "\\end{aligned}\n", "$$\n", "\n", "と書くことができます.ここで,$a_1, a_2$ はそれぞれ,1層目と2層目の,および2層目と3層目の間で線形変換のあとに施される非線形変換(活性化関数)を意味しています.以下,簡単のために,各層間で行われた線形変換の結果を ${\\bf u}_1, {\\bf u}_2$とし,中間層の値,すなわち ${\\bf u}_1$ に活性化関数を適用した結果を ${\\bf h}_1$ と書きます.ただし,${\\bf u}_2$ に活性化関数を適用した結果は ${\\bf y}$ と表記します.すると,これらの関係は以下のように整理することができます.\n", "\n", "$$\n", "\\begin{aligned}\n", "{\\bf y} &= a_2({\\bf u}_2) \\\\\n", "{\\bf u}_2 &= {\\bf w}_2 {\\bf h}_1 + {\\bf b}_2 \\\\\n", "{\\bf h}_1 &= a_1({\\bf u}_1) \\\\\n", "{\\bf u}_1 &= {\\bf w}_1 {\\bf x} + {\\bf b}_1\n", "\\end{aligned}\n", "$$" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "k1L6xptUs3l4" }, "source": [ "#### パラメータ ${\\bf w}_2$ の更新量\n", "\n", "それではまず,出力層に近い方のパラメータ,${\\bf w}_2$ についての $\\mathcal{L}$ の勾配を求めてみましょう.これは,合成関数の偏微分なので,連鎖律(chain rule)を用いて以下のように展開できます.\n", "\n", "$$\n", "\\begin{aligned}\n", "\\frac{\\partial \\mathcal{L}}{\\partial {\\bf w}_2}\n", "&= \\frac{\\partial \\mathcal{L}}{\\partial {\\bf y}} \\frac{\\partial {\\bf y}}{\\partial {\\bf w}_2} \\\\\n", "&= \\frac{\\partial \\mathcal{L}}{\\partial {\\bf y}} \\frac{\\partial {\\bf y}}{\\partial {\\bf u}_2} \\frac{\\partial {\\bf u}_2}{\\partial {\\bf w}_2}\n", "\\end{aligned}\n", "$$\n", "\n", "この3つの偏微分はそれぞれ,\n", "\n", "$$\n", "\\begin{aligned}\n", "\\frac{\\partial \\mathcal{L}}{\\partial {\\bf y}}\n", "&= -\\frac{2}{N} ({\\bf t} - {\\bf y}) \\\\\n", "\\frac{\\partial {\\bf y}}{\\partial {\\bf u}_2}\n", "&= \\frac{\\partial a_2({\\bf u}_2)}{\\partial {\\bf u}_2} \\\\\n", "\\frac{\\partial {\\bf u}_2}{\\partial {\\bf w}_2} \n", "&= {\\bf h}_1\n", "\\end{aligned}\n", "$$\n", "\n", "と求まります.ここで,活性化関数の入力に関する出力の勾配\n", "\n", "$$\n", "\\frac{\\partial a_2({\\bf u}_2)}{\\partial {\\bf u}_2}\n", "$$\n", "\n", "が登場しました.これは,例えば活性化関数にシグモイド関数を用いる場合は,\n", "\n", "$$\n", "a_2({\\bf u}_2) = \\frac{1}{1 + \\exp(-{\\bf u}_2)}\n", "$$\n", "\n", "の微分ですから,すなわち\n", "\n", "$$\n", "\\begin{aligned}\n", "\\frac{\\partial a_2({\\bf u}_2)}{\\partial {\\bf u}_2}\n", "&= -\\frac{-(\\exp(-{\\bf u}_2))}{(1 + \\exp(-{\\bf u}_2))^2} \\\\\n", "&= \\frac{1}{1 + \\exp(-{\\bf u}_2)} \\cdot \\frac{\\exp(-{\\bf u}_2)}{1 + \\exp(-{\\bf u}_2)} \\\\\n", "&= \\frac{1}{1 + \\exp(-{\\bf u}_2)} \\cdot \\frac{1 + \\exp(-{\\bf u}_2) - 1}{1 + \\exp(-{\\bf u}_2)} \\\\\n", "&= \\frac{1}{1 + \\exp(-{\\bf u}_2)} (1 - \\frac{1}{1 + \\exp(-{\\bf u}_2)}) \\\\\n", "&= a_2({\\bf u}_2)(1 - a_2({\\bf u}_2))\n", "\\end{aligned}\n", "$$\n", "\n", "となります.シグモイド関数の勾配は,このようにシグモイド関数の出力値を使って簡単に計算することができます.\n", "\n", "これで ${\\bf w}_2$ の勾配を計算するのに必要な値は全て出揃いました.では実際にNumPyを使ってこれらを計算してみましょう.ここでは簡単のために,バイアスベクトルはすべて0で初期化されているとします." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "# 入力\n", "x = np.array([2, 3, 1])\n", "\n", "# 正解\n", "t = np.array([20])" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "VOznWs5PV3pQ" }, "source": [ "まず,NumPyモジュールを読み込んでから,入力の配列を定義します.ここでは,上図と同じになるように `2, 3, 1` の3つの値を持つ3次元ベクトルを定義しています.また,正解として仮に `20` を与えることにしました.次に,パラメータを定義します." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# 1-2層間のパラメータ\n", "w1 = np.array([[3, 1, 2], [-2, -3, -1]])\n", "b1 = np.array([0, 0])\n", "\n", "# 2-3層間のパラメータ\n", "w2 = np.array([[3, 2]])\n", "b2 = np.array([0])" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "2xT4AnmBXwIb" }, "source": [ "ここでは,以下の4つのパラメータを定義しました.\n", "\n", "**1層目と2層目の間の線形変換のパラメータ**\n", "\n", "${\\bf w}_1 \\in \\mathbb{R}^{2 \\times 3}$ : 3次元ベクトルを2次元ベクトルに変換する行列\n", "\n", "${\\bf b}_1 \\in \\mathbb{R}^2$ : 2次元バイアスベクトル\n", "\n", "**2層目と3層目の間の線形変換のパラメータ**\n", "\n", "${\\bf w}_2 \\in \\mathbb{R}^{1 \\times 2}$ : 2次元ベクトルを1次元ベクトルに変換する行列\n", "\n", "${\\bf b}_2 \\in \\mathbb{R}^1$ : 1次元バイアスベクトル\n", "\n", "それでは,各層の計算を実際に実行してみましょう." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": "[0.95257194]\n" } ], "source": [ "# 中間層の計算\n", "u1 = w1.dot(x) + b1\n", "h1 = 1. / (1 + np.exp(-u1))\n", "\n", "# 出力の計算\n", "u2 = w2.dot(h1) + b2\n", "y = 1. / (1 + np.exp(-u2))\n", "\n", "print(y)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "8t-YjO8BZ6kU" }, "source": [ "出力は $0.95257194$ と求まりました.つまり,$f([2, 3, 1]^T) = 0.95257194$ ということになります.次に,上で求めた\n", "\n", "$$\n", "\\frac{\\partial \\mathcal{L}}{\\partial {\\bf w}_2}\n", "= \\frac{\\partial \\mathcal{L}}{\\partial {\\bf y}} \\frac{\\partial {\\bf y}}{\\partial {\\bf u}_2} \\frac{\\partial {\\bf u}_2}{\\partial {\\bf w}_2}\n", "$$\n", "\n", "の右辺の3つの偏微分をそれぞれ計算してみましょう." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# dL / dy\n", "g_Ly = -2 / 1 * (t - y)\n", "\n", "# dy / du_2\n", "g_yu2 = y * (1 - y)\n", "\n", "# du_2 / dw_2\n", "g_u2w2 = h1" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "uPmRfu3_aXJG" }, "source": [ "これらを掛け合わせれば,求めたかったパラメータ ${\\bf w}_2$ についての勾配を得ることができます." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": "[-1.72104507e+00 -1.43112111e-06]\n" } ], "source": [ "# dL / dw_2: 求めたい勾配\n", "g_Lw2 = g_Ly * g_yu2 * g_u2w2\n", "\n", "print(g_Lw2)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "XN293R5NU1zP" }, "source": [ "勾配が求まりました.これが $\\partial \\mathcal{L} / \\partial {\\bf w}_2$ の値です.これを学習率でスケールさせたものを使えば,パラメータ ${\\bf w}_2$ を更新することができます.更新式は,具体的には以下のようになります.\n", "\n", "$$\n", "{\\bf w}_2 \\leftarrow {\\bf w}_2 - \\eta \\frac{\\partial \\mathcal{L}}{\\partial {\\bf w}_2}\n", "$$\n", "\n", "ここでは学習率を $\\eta$ で表記しました." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "SC0RoRpucZh9" }, "source": [ "#### パラメータ ${\\bf w}_1$ の更新量\n", "\n", "次に,${\\bf w}_1$ の更新量も求めてみましょう.そのためには,${\\bf w}_1$ で目的関数 $\\mathcal{L}$ を偏微分した値が必要です.これは以下のように計算できます.\n", "\n", "$$\n", "\\begin{aligned}\n", "\\frac{\\partial \\mathcal{L}}{\\partial {\\bf w}_1}\n", "&= \\frac{\\partial \\mathcal{L}}{\\partial {\\bf y}} \\frac{\\partial {\\bf y}}{\\partial {\\bf w}_1} \\\\\n", "&=\n", "\\frac{\\partial \\mathcal{L}}{\\partial {\\bf y}}\n", "\\frac{\\partial {\\bf y}}{\\partial {\\bf u}_2}\n", "\\frac{\\partial {\\bf u}_2}{\\partial {\\bf w}_1} \\\\\n", "&=\n", "\\frac{\\partial \\mathcal{L}}{\\partial {\\bf y}}\n", "\\frac{\\partial {\\bf y}}{\\partial {\\bf u}_2}\n", "\\frac{\\partial {\\bf u}_2}{\\partial {\\bf h}_1}\n", "\\frac{\\partial {\\bf h}_1}{\\partial {\\bf w}_1} \\\\\n", "&=\n", "\\frac{\\partial \\mathcal{L}}{\\partial {\\bf y}}\n", "\\frac{\\partial {\\bf y}}{\\partial {\\bf u}_2}\n", "\\frac{\\partial {\\bf u}_2}{\\partial {\\bf h}_1}\n", "\\frac{\\partial {\\bf h}_1}{\\partial {\\bf u}_1}\n", "\\frac{\\partial {\\bf u}_1}{\\partial {\\bf w}_1}\n", "\\end{aligned}\n", "$$\n", "\n", "この5つの偏微分のうち初めの2つはすでに求めました.残りの3つは,それぞれ,\n", "\n", "$$\n", "\\begin{aligned}\n", "\\frac{\\partial {\\bf u}_2}{\\partial {\\bf h}_1}\n", "&= {\\bf w}_2 \\\\\n", "\\frac{\\partial {\\bf h}_1}{\\partial {\\bf u}_1}\n", "&= {\\bf h}_1(1 - {\\bf h}_1) \\\\\n", "\\frac{\\partial {\\bf u}_1}{\\partial {\\bf w}_1}\n", "&= {\\bf x}\n", "\\end{aligned}\n", "$$\n", "\n", "と計算できます.では,さっそく実際にNumPyを用いて計算を実行してみましょう." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": "[[-1.72463398e-04 -2.58695098e-04 -8.62316992e-05]\n [-5.72447970e-06 -8.58671954e-06 -2.86223985e-06]]\n" } ], "source": [ "g_u2h1 = w2\n", "g_h1u1 = h1 * (1 - h1)\n", "g_u1w1 = x\n", "\n", "# 上から du1 / dw1 の直前までを一旦計算\n", "g_Lu1 = g_Ly * g_yu2 * g_u2h1 * g_h1u1\n", "\n", "# g_u1w1は (3,) というshapeなので,g_u1w1[None]として(1, 3)に変形\n", "g_u1w1 = g_u1w1[None]\n", "\n", "# dL / dw_1: 求めたい勾配\n", "g_Lw1 = g_Lu1.T.dot(g_u1w1)\n", "\n", "print(g_Lw1)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "Al0CmLzTtyvR" }, "source": [ "これが $\\partial \\mathcal{L} / \\partial {\\bf w}_1$ の値です.これを用いて,${\\bf w}_2$ と同様に以下のような更新式でパラメータ ${\\bf w}_1$ の更新をすることができます.\n", "\n", "$$\n", "{\\bf w}_1 \\leftarrow {\\bf w}_1 - \\eta \\frac{\\partial \\mathcal{L}}{\\partial {\\bf w}_1}\n", "$$" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "HLAwYypPshj8" }, "source": [ "#### 学習率(learning rate)について\n", "\n", "学習率が大きすぎると,繰り返しパラメータ更新を行っていく中で目的関数の値が振動したり,発散したりしてしまいます.小さすぎると,収束に時間がかかってしまいます.そのため,この学習率を適切に決定することがニューラルネットワークの学習においては非常に重要となります.多くの場合,学習がきちんと進むもっとも大きな値を経験的に探すということが行われます.シンプルな画像認識のタスクなどでは大抵,0.1~0.01などが最初に用いられる場合が比較的多く見られます." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "zIv_rP32uH1y" }, "source": [ "## 誤差逆伝播法(バックプロパゲーション)\n", "\n", "ここまでで,各パラメータについての目的関数の導関数を手計算により導出して実際に勾配の数値計算を行うということを体験しました.では,もっと層数の多いニューラルネットワークの場合は,どうなるでしょうか.同様に手計算によって導関数を求めることももちろん可能ですが,ニューラルネットワークが微分可能な関数を繰り返し適用するものであるという性質を用いると,コンピュータによって自動的に勾配を与える関数を導き出すことが可能です.合成関数の偏微分は,連鎖律によって複数の偏微分の積の形に変形できたことを思い出しましょう.\n", "\n", "下図は,ここまでの説明で用いていた3層の全結合型ニューラルネットワークの出力を得るための計算と,その値を使って目的関数の値を計算する過程を青い矢印で,そして前節で手計算によって行った各パラメータによる目的関数の偏微分を計算する過程を赤い矢印で表現した動画となっています.\n", "\n", "\n", "![誤差逆伝播法(Backpropagation)の計算過程](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/backpropagation.gif)" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "R8YaTki1Dhhx" }, "source": [ "まず,目的関数の出力を $l = \\mathcal{L}({\\bf y}, {\\bf t})$ とします.この図の丸いノードは変数を表し,四角いノードは関数を表しています.今,一つの巨大な合成関数として見ることができるニューラルネットワーク全体を $f$ と表し,その中で各層間の線形変換に用いられる関数を $f_1$, $f_2$,非線形変換をそれぞれ $a_1$, $a_2$ と表します.このとき,前節で行った更新量の算出はどのように捉えることができるでしょうか.\n", "\n", "今,上図の青い矢印で表されるように,新しい入力 ${\\bf x}$ がニューラルネットワークに与えられ,それが順々に出力側に伝わっていき,最終的に目的関数の値 $l$ まで計算が終わったとします.ここまでを **順伝播(forward propagation)** といいます.\n", "\n", "すると次は,目的関数の出力の値を小さくするような各パラメータの更新量を求めたいということになりますが,このために必要な目的関数の勾配は,各パラメータの丸いノードより先の部分(出力側)にある関数の勾配だけで計算できることが分かります.具体的には,それらを全て掛け合わせたものになっています.つまり,上図の赤い矢印で表されるように,出力側から入力側に向かって,**順伝播とは逆向きに**,各関数における入力についての勾配を求めて掛け合わせていけば,パラメータについての目的関数の勾配が計算できるわけです.\n", "\n", "このように,微分の連鎖律の仕組みを用いて,ニューラルネットワークを構成する関数が持つパラメータについての目的関数の勾配を,**順伝播で通った経路を逆向きにたどるようにして**途中の関数の勾配の掛け算によって求めるアルゴリズムを **誤差逆伝播法(backpropagation)** と呼びます.\n" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "uHGp6qIBcKOI" }, "source": [ "## 勾配消失\n", "\n", "活性化関数について初めに触れた際,シグモイド関数には勾配消失という現象が起きやすくなるという問題があり,現在はあまり使われていないと説明をしました.その理由についてもう少し詳しく見ていきましょう.\n", "\n", "上で既に計算した,シグモイド関数の導関数を思い出してみます.\n", "\n", "$$\n", "\\begin{aligned}\n", "f\\left( u\\right) &=\\dfrac {1}{1+e^{-u}} \\\\\n", "f'\\left( u\\right) &= f\\left( u\\right) \\left( 1-f\\left( u\\right) \\right)\n", "\\end{aligned}\n", "$$\n", "\n", "さて,この導関数の値を入力変数に関してプロットしてみると,下記のようになります.\n", "\n", "![シグモイド関数の導関数](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/09.png)\n", "\n", "この図の上2つは,導関数を構成する2つの部分 $f(u)$ と $1 - f(u)$ の値を別々にプロットしたもので,中央下の図が実際の導関数の値となります.上図中央下の導関数の形を見ると,入力が原点から遠くなるにつれ勾配の値がどんどん減少し,0に漸近していくことが見て取れます.\n", "\n", "各パラメータの更新量を求めるには,前節で説明したように,**そのパラメータよりも先の全ての関数の勾配を掛け合わせる**必要がありました.このとき,活性化関数にシグモイド関数を用いていると,勾配は必ず**最大でも0.25**という値にしかなりません.すると,非線形変換が計算グラフ中に現れるたびに,目的関数の勾配は,多くとも0.25倍されてしまいます.これは,層数が増えていけばいくほど,この最大でも0.25という値が繰り返し掛け合わされることになるため,入力に近い層に流れていく勾配がどんどん0に近づいていってしまいます.\n", "\n", "具体例を見てみましょう.今回は3層のニューラルネットワークを用いて説明を行っていましたが,4層の場合を考えてみます,すると,一番入力に近い線形変換のパラメータの勾配は,多くとも目的関数の勾配を $0.25 \\times 0.25 = 0.0625$ 倍したものということになります.層数が一つ増えるたびに,指数的に勾配が小さくなるということがよく分かります.\n", "\n", "ディープラーニングでは,4層よりもさらに多くの層を積み重ねたニューラルネットワークが用いられます.そうすると,活性化関数としてシグモイド関数を使用した場合,**目的関数の勾配が入力に近い関数が持つパラメータへほぼ全く伝わらなくなってしまいます**,あまりにも小さな勾配しか伝わってこなくなると,パラメータの更新量がほとんど0になるため,どんなに目的関数が大きな値になっていても,入力層に近い関数が持つパラメータは変化しなくなります.つまり初期化時からほとんど値が変わらなくなるということになり,学習が行われていないという状態になるわけです.これを **勾配消失(vanishing gradient)** と呼び,長らく深い(十数層を超える)ニューラルネットワークの学習が困難であった一つの要因でした." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "IRvZlChZ9Qna" }, "source": [ "## 「レイヤ」が指すもの\n", "\n", "ここまでの解説では,前々節の誤差逆伝播法の説明に登場した図のようなグラフにおける丸いノード(中間出力などの値)の数を指して○層(○-layer)のニューラルネットワーク,という言い方をしてきました.しかし,このグラフにおける四角いノード(関数)を指して層(layer)と呼ぶ場合もあります.そしてニューラルネットワークの実装に使われるフレームワークでは,多くの場合,層タイプ(layer type)として様々な関数がまとめられています.そこで,以下ではこの関数に対して「層」もしくは「レイヤ」という言葉を用いることに注意してください." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "7dD2mgCOTvdW" }, "source": [ "## 様々なレイヤタイプ\n", "\n", "ここまでで,ニューラルネットワークには大別して線形変換を行う関数と非線形変換を行う関数という二種類の関数が含まれることを説明し,線形変換の例として **全結合層(fully-connected layer)** のみを用いてきました.\n", "\n", "しかし,ニューラルネットワークの構成要素として用いることができるレイヤは,全結合層と活性化関数だけではありません.画像認識や画像生成,画像の変換や超解像(低解像の画像から解像度を高めた画像を作る技術)など,様々なタスクで入力に画像というデータ形式が用いられますが,こういったタスクではしばしば画像というデータ形式の特徴と相性の良い変換である畳み込み層(convolution layer)というものが全結合層の代わりにニューラルネットワークの中で線形変換を担います.\n", "\n", "また,ニューラルネットワークの構成要素となるレイヤには,微分可能な関数であればなんであろうと用いることができます.そこで,いわゆる **畳み込みニューラルネットワーク(convolutional neural networks; CNN)** と呼ばれるネットワークアーキテクチャの種別では,この畳込み層に加えてプーリング層(pooling layer)というレイヤがしばしば用いられてきました.\n", "\n", "本節では,このCNNでしばしば用いられる畳み込み層とプーリング層について,その計算の概要を説明します." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "w-3rsIzXpCBo" }, "source": [ "### 畳み込み層(convolution layer)\n", "\n", "全結合層では,入力${\\bf x}$の各要素と出力${\\bf y}$の各要素が,互いにすべて結合していました.\n", "そのため,一つの出力ユニットの値は,全ての入力ユニットの値の影響を受けます.\n", "一方,一つの出力ユニットの値を計算するために,入力ユニットのうちの一部を局所的にしか用いないこともあります.\n", "2次元畳み込み層では,パラメータはカーネル(もしくはフィルタ)と呼ばれる小さな画像パッチのようなものの集合として用意されます.\n", "そして,このカーネルの大きさが入力より小さい場合は,一つの出力ユニットの値は,入力ユニットの内カーネルでカバーされる一部分を元にして計算されます.\n", "\n", "下の図は,$3 \\times 3$の大きさの小さな画像パッチのような畳み込みカーネルを2つ持つ畳み込み層での計算を模式的に表しています.\n", "\n", "![畳み込み層の計算過程](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/convolution.gif)\n", "\n", "(図は[CS231n Convolutional Neural Networks for Visual Recognition\n", "](http://cs231n.github.io/convolutional-networks/)より引用)\n", "\n", "一番左の青いブロックが並ぶ列は,入力画像を表しています.\n", "入力は 幅・高さが$5 \\times 5$ の大きさの3チャンネル画像です.\n", "上図ではまず,**パディング(padding)**と呼ばれる処理をして,入力画像の周囲に値が0となる領域を付け足しています.\n", "今回は幅1の領域をパディングしています.\n", "\n", "中央2列は,それぞれ1列ごとに1つの畳み込みカーネルを表しています.\n", "1つの畳み込みカーネルは,入力の画像(もしくは**特徴マップ(feature map)**と呼ばれる一つ前の層の出力)が持つのと同数のチャンネルを持ちます.\n", "ここでは入力画像は3チャンネルだったので,各カーネルは3チャンネル分の値を持っています.\n", "これが縦に並んだ3つの赤い$3 \\times 3$の大きさのブロックで表されています.\n", "各カーネルの各チャンネルは,対応する入力のチャンネルに対して自らをスライドさせていくような形で計算を行います.\n", "このときのスライド幅を**ストライド(stride)**と呼びます.\n", "図では,2個飛びに入力画像上を$3 \\times 3$の枠が移動しているので,ストライドは2です.\n", "ここで行われる計算は,入力のある位置にカーネルを重ね合わせた際に,重なっている$3 \\times 3 = 9$個の要素を互いに掛け合わせて,結果を全て足すという計算です.\n", "例えば,上の図では1つ目のカーネル(w0)の1つ目のチャンネルは,\n", "\n", "$$\n", "\\left[\n", "\\begin{matrix}\n", "1 & 1 & -1 \\\\\n", "0 & -1 & 0 \\\\\n", "1 & -1 & -1\n", "\\end{matrix}\n", "\\right]\n", "$$\n", "\n", "という値を持っています.\n", "また,それに対応する入力画像の1チャンネル目の左上から$3 \\times 3$の小領域には\n", "\n", "$$\n", "\\left[\n", "\\begin{matrix}\n", "0 & 0 & 0 \\\\\n", "0 & 0 & 1 \\\\\n", "0 & 2 & 0\n", "\\end{matrix}\n", "\\right]\n", "$$\n", "\n", "という値が並んでいます.\n", "そのため,それぞれ重なりあっている値同士を掛け合わせると,\n", "\n", "$$\n", "\\left[\n", "\\begin{matrix}\n", "1 \\times 0 & 1 \\times 0 & -1 \\times 0 \\\\\n", "0 \\times 0 & -1 \\times 0 & 0 \\times 1 \\\\\n", "1 \\times 0 & -1 \\times 2 & -1 \\times 0\n", "\\end{matrix}\n", "\\right]\n", "=\n", "\\left[\n", "\\begin{matrix}\n", "0 & 0 & 0 \\\\\n", "0 & 0 & 0 \\\\\n", "0 & -2 & 0\n", "\\end{matrix}\n", "\\right]\n", "$$\n", "\n", "となります.\n", "この9つの値を全て足したものが,まずこのカーネルの1つ目のチャンネルの1つ目の計算結果となります.\n", "計算すると,結果は-2です.\n", "これを2チャンネル目,3チャンネル目の値でも同様に行うと,2チャンネル目は0,3チャンネル目は1という計算結果になります.\n", "そして,最後にこの全てのチャンネルの結果を足し合わせます.\n", "その結果は-1となります.\n", "これにカーネルごとに用意されるバイアスの値(上図では1つ目のカーネルのバイアスは1)を足したものが,このカーネルの入力の左上隅の領域に対する出力結果となります.結果は,0です.一番右の列の緑色のブロックはこの畳込み層の出力値を表しており,1つ目のブロックの左上のマスを見ると,確かに0となっていることが分かります.\n", "\n", "同様に,他のマスの値もいくつか計算してみましょう.\n", "\n", "畳み込み層で行われている計算は一見複雑に感じられますが,カーネルの中の一つの値は入力に掛け合わされているだけであり,バイアスもその結果に足し合わされているだけです.\n", "このため,細かく見れば線形変換のみで構成されていることが分かります.\n", "よって,畳み込み層で行われる変換全体は,パラメータおよび入力について微分することができます.\n", "\n", "最後に,畳み込み層はカーネルを問題に合わせて適切に定義することで,多様なデータ形式を扱うことができます.\n", "例えば,1次元のカーネルを使うと1次元の系列データなどにも適用でき,3次元のカーネルを用いれば動画やボクセルのようなデータにも適用できます." ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "SUEmd9pOKe2D" }, "source": [ "### プーリング層(pooling layer)\n", "\n", "\n", "プーリングは,主に特徴マップに対して行われる操作で,特徴マップの空間方向の次元(spatial dimension,入力画像における幅・高さに対応する次元)の大きさを削減し計算量を抑えたり,微小な平行移動について出力を不変とすることで画像認識タスクなどにおけるロバスト性の向上のためなどに用いられます.\n", "\n", "プーリングの計算方法は畳み込みと似ています.ただしパラメータはなく,畳み込みカーネルを用いて行われた計算の部分が,対応する入力の部分領域に対する平均の計算や最大値計算などに置き換えられたものです.部分領域ごとの平均値を計算していくプーリングを平均値プーリング(average pooling),最大値を計算するプーリングを最大値プーリング(max pooling)と呼びます.\n", "\n", "以下の図のうち左側の部分では,プーリング層を用いたダウンサンプリング(解像度を下げること)を $224 \\times 224$ サイズの,$64$ チャンネルの特徴マップに適用し,縦横半分の大きさ ($112 \\times 112$サイズ)の特徴マップにしています.このとき,チャンネル数(depthとも呼ばれる)は維持されています.\n", "\n", "この図のうち右側の部分は,左側の直方体の64チャンネルのうちの一つ,濃い色になっているチャンネルの $4 \\times 4$ の部分領域を抜き出してきて模式的に表したものに,$2 \\times 2$ 領域ごとに最大値を計算しその領域の代表値とする最大値プーリングを領域を2つ飛びで(ストライド2で)ずらしながら行った際の結果を表しています.\n", "\n", "![プーリング層の計算過程](https://github.com/japan-medical-ai/medical-ai-course-materials/raw/master/notebooks/images/3/pooling.png)\n", "\n", "(図は[CS231n Convolutional Neural Networks for Visual Recognition\n", "](http://cs231n.github.io/convolutional-networks/)より引用)\n", "\n" ] } ] }