データ数について

Deep Learningの精度を決めるのは、結局のところデータ数のようです。

近年、AIが注目を浴びた大きな要因は、インターネットを用いて大量のデータを集められるようになったからだ、と言われます。
もちろん、Deep Learningの様々なモデルや技術が研究・開発された結果、より高い汎化性能を獲得できるようになった要因はあると思われますが、むしろ近年のPCの発展、特にGPUの活躍により、計算速度が極めて早くなり、少々力業なニューラルネットワークが実装できるようになった結果、ネットワークの階層を深く出来るようになった事の方が、モデルの工夫や新しい活性化関数、アップデート関数の採用よりも大きく寄与しているのだと思います。

と言いますのも。幾つかディープラーニングのモデル、特にCNNを試してみましたが、データ数が少なければ、やはりいくらモデルを工夫したとしても、さほど変化は得られないなという実感を、最近抱いているわけです。そんなことは百も承知で、それでもAIを用いたい課題は存在して、それに対して用意できるデータ数が限られている場面も多く存在し、今ある材料で何とかよい精度を得るために、モデルや学習方法を工夫する訳ではあります。しかしながらその労力に見合うだけの結果を得る事は、なかなかに難しいようです。ディープラーニングを試してみたけど、まだまだだなという感想を持つ人の多くは、このような状況で諦めているのではと思います。世の中にはインターネットで集められないデータを用いたい状況も数多く存在し、そのような課題に対してはまだまだディープラーニングが上手く機能しない状況があるにも関わらず、上司からAIの活用を義務付けられ、板挟みに合っているエンジニアもさぞ多いだろうなと想像します。

AIが従来のプログラムと異なる優れた点(または、AIに期待する点)は、汎化性能であると思われます。コンピュータにおけるデータ処理は多くの事はプログラムでき、その大半はエクセルマクロのような、小規模のプログラムでも対応できます。VBAは使ってみると大変使い勝手がよく、マクロの記録機能も大変便利で、私のような素人さんでも結構なレベルで使いこなす事ができます。しかしながら課題に対して、その都度プログラムを作成する必要があるので、言ってみれば解決できる課題は一つだけです。人間は様々な課題に対して柔軟に、個別に対応する事ができるため、人工”知能”に求められるものはそのような対応力、汎化的な能力とも言えます。果たして現在のディープラーニングの技術はそういったものになっているでしょうか。出来る事は極めて大量のデータを用意して学習し、新しいデータの結果を予測する、というただ一つの事です。これは従来の解析手法の一つであった”外挿”と、大して変わらないかもしれません。実際使っていると、これはディープラーニングが解くべき課題ではないのに、”とりあえずディープラーニングでやってみました!”的な報告に出くわす事が良くあります。ディープラーニングが活躍できる問題と、従来の手法で解くべき問題は良く考えた方が良さそうです。

今現在、私はディープラーニングが解くべき問題と従来の手法で解くべき問題の境目は、結果に厳密性を求めるか否かのところにあって、判断を求める場合は前者、厳密さを求める場合は後者にあるのではと思っています。ディープラーニングが割り出す答えはあくまで判断材料であり(悪く言えば偽物であり)、結論を一旦保留して、最終的には人間が判断をするための材料として用いるのであれば大いに活用できるけれども、その結果を丸々信じなくてはならないような課題にはやはり使えないのではと思います。出てきた答えは偽物ですから。いくら本物のように見えても。

そのあたりを混同しないように、今後もディープラーニングを楽しんでいこうと思います。データが少なくても高い汎化性能が得られるモデルを開発したいですね。今のところアーギュメンテーション工夫するくらいしかアイデアはありませんが。

バッチ正規化

対象となる層の活性を正規化し、入力の偏りによる内部共変量シフトを抑制する。
重みの初期値をあまり気にしなくてもよくなり、過学習も抑制される。

入力の平均値と分散を求め、各入力に対して、平均値を引いた後、標準偏差で割る(標準化)。その後、スケール、平行移動して、出力の分布を作成する。

\( \displaystyle \mu = \frac{1}{n} \sum^n_{i=1}x_i \)
\( \displaystyle \sigma ^2 = \frac{1}{n} \sum^2_{i=1}(x_i-\mu)^2\)
\( \displaystyle \hat{x_i} = \frac{x_i-\mu}{\sqrt{\sigma^2+\epsilon}} \)
\( \displaystyle y_i = \gamma\hat{x_i} + \beta \)

\( y_i \)が出力の分布。\( \gamma \)と\( \beta \)は重みと共に学習されていくパラメータなので、推論時にはこれらの保存も必要。

初期値を気にしなくて済むのは良いですね。重みが無駄に偏る心配がないのは良いかと。その分、\( \gamma \)と\( \beta \)を保存しておかなくてはならないのが面倒ですね。

Adam

重みの更新方法の一つ。
MomemtumとRMSPropを組み合わせた方法。
勾配の2乗の移動平均だけでなく、勾配の移動平均も保持する。

\( m_t = \rho _1 m_{t-1} + (1-\rho _1)\frac{\partial L}{\partial W_t} \)
\( v_t = \rho _2 v_{t-1} + (1-\rho _2)\frac{\partial L}{\partial W_t} \odot \frac{\partial L}{\partial W_t} \)
\( \hat{m_t} = \frac{m_t}{1-\rho ^2_1} \)
\( \hat{v_t} = \frac{v_t}{1-\rho ^t_2}\)

\( W_{t+1} = W_t – \eta\frac{1}{\sqrt{\hat{v_t}} +\delta} \odot \hat{m_t} \)

\( \rho _1 \)、\( \rho _2 \)は共に減衰率で1に近いハイパーパラメータ。\( t \)はステップ数。\( \odot \)はアダマール積。\( \delta \)は0割り防止の小さい数。

正則化

計算した重みに制限を加える事で過学習を抑制する方法。
weight decay, dropout, early stoppingがそれに当たる。
過学習を抑制する工夫という意味では、重みにノイズを加えたりするのも含まれるのかな?

荷重減衰 (Weight decay)
損失関数が大きくなり過ぎないよう、荷重を減らす作用をする項を加える。
\( L = L + \frac{1}{2} \lambda \sum^{n}_{l=1} \sum_{i, j} (w^{l}_{ij})^2 \)

\( L \)は損失関数、\( n \)はレイヤの数、\( w^{l}_{ij}\)は\( l \)番目の層の重み行列。\( \lambda \)は手入力するパラメータで、小さい値を入れる。

重み\( W \)に対する損失関数の勾配は\( \frac{\partial L}{\partial W} \)で、重みの更新は\( W \gets W – \eta \frac{\partial L}{\partial W} \)だったので、正則化を考慮すると、\( W \gets W – \eta (\frac{\partial L}{\partial W}+\lambda W) \)となる。\( W\)が正の場合は更新された\( W \)は小さく、負の場合は大きくなる。

…けど。重みを小さくすればなぜ過学習が防げるのかがいまいち分からんね。過学習はトレーニングデータの特徴を過度に認識すると現れるので、トレーニングデータが少ないと起こりやすい。”過度に認識”の部分が”重みを大きく”の部分に対応すると考えれば、そういうものか、と思う気もするけど、認識した学習を訛らせる訳なので、効率は悪くなるのでは?だんだん重みが小さくなると、今度は小さくなり過ぎて学習がサチってしまわないかな?

最尤推定法

母集団の分布の形が分かっている(正規分布している、など)が、その母数が未知である(平均値や分散は未知)の場合、n個の測定値から母数を推定する方法。

尤度関数 (ゆうどかんすう, Likelihood function)

\( \displaystyle L(\theta) =L(\theta; x_1, x_2, x_3, … ,x_n) = \prod_{i=1}^n p(x_i;\theta) \)

確率密度関数が\( p(x, \theta) \)で与えられ、母集団から\( n \)個の標本をランダムで抽出して、その値を\( {x_1, x_2, x_3, …, x_n} \)と表記される時の尤度関数は\( L(θ)=L(\theta; x_1, x_2, x_3, …, x_n) = \prod_{i=1}^{n} p(x_i;\theta) \)と表します。\( {x_1, x_2, x_3, …, x_n} \)はサンプリングされた確定値で、未知数は\( \theta \)の方です。

最尤推定法 (さいゆうすいていほう, Maximum likelihood estimate)

\( \theta \)の最もそれっぽい値、\( \hat{\theta} \)を推定する方法。尤度関数の\( \theta \)を変えていくと当然ながら\( L(\theta) \)の計算値は変化していきます。右辺は\( {x_1, x_2, x_3, …, x_n} \)が観測される確率なので、\( L(\theta) \)の関数と合っていなければ小さく、合っていれば大きくなります。従って\( L(\theta) \)の最大値を取れば、それが一番もっともらしい\( \theta = \hat{\theta} \)となります。最大値は凸関数であれば微分が0になる点が最大値なので、

\( \displaystyle \frac{\partial}{\partial \theta} L(\theta) = 0 \)

を解けばよい事になります。積の微分は計算が大変なので、

\( \displaystyle \frac{\partial}{\partial \theta} l(\theta) = \frac{\partial}{\partial \theta} \log L(\theta) = 0 \)

を代わりに解いても、同じ\( \hat{\theta} \)が得られます。

母集団が、平均値\( \mu \)、分散\( \sigma^2 \)であり、\( \sigma \)が既知で\( \mu \)を推定する場合、

\( \displaystyle \frac{\partial}{\partial \mu} l(\mu) = \frac{1}{\sigma^2}\sum^{n}_{i=1}(x_i-\mu) = 0 \)

を解けば良いので、

\( \displaystyle \mu = \frac{1}{n} \sum^{n}_{i=1}x_i \)

となります。

Softmax-with-Lossレイヤ

Softmax-with-Lossレイヤを実装

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 
    x = x - np.max(x)
    return np.exp(x) / np.sum(np.exp(x))

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None
        self.t = None

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        return self.loss

    def backward(self, dy=1):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        return dx

Softmaxレイヤは入力された値を正規化し、出力の和が1になるようにします。
入力に対してSoftmaxで処理し、誤差を求める関数で、出力層に配置します。

\( y(k) = \frac{\exp(a_k)}{\sum^n_{i=1}\exp(a_i)} \)

多値の識別問題に使います。
正規化するだけなので、重みの順序は変わりません。

損失関数として交差エントロピー誤差を使います。
logの中身が0にならないよう、1e-7といった小さい値を足しておきます。
損失関数を使う事により、学習の進捗が浮動小数点で扱えるようになり、離散的な問題の最適化においても、プラトーに遭遇しにくくなるようです。

ニューラルネットワークはとにかく0, 1のような離散数を出さないようにする工夫が多いですね。

Sigmoidレイヤ

Sigmoidレイヤを実装

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

class Sigmoid:
    def __init__(self):
        self.y = None

    def forward(self, x):
        out = sigmoid(x)
        self.y = y
        return y

    def backward(self, dy):
        dx = dy * (1.0 - self.y) * self.y
        return dx

不思議な形をしたシグモイド。
二値分類問題ではシグモイド関数を使います。
恒等関数でも良いような気もするけど、非線形関数を使う事が機械学習のポイントです。

変な形をしているだけに、使い方が難しいんじゃないかと思うのです。
これも勉強を進めていけば、しっかりと使い道が分かるのようになるのだろうか??

ReLUレイヤ

ReLUレイヤを実装

class ReLU:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0)
        y = x.copy()
        y[self.mask] = 0
        return y

    def backward(self, dy):
        dy[self.mask] = 0
        dx = dy
        return dx

maskの使い方が面白いです。
ReLUはforwardではxが0以下の時は0を返し、0より大きいときは恒等関数になります。
通常はif文を使って書けばよいような気がしますが、maskを使うと上記のように書けます。
x<=0の時maskはtrueになって、それ以外の時はfalseになるので、yにxをいれておいて、maskがtrueの部分だけ0にすればよいわけですね。 ... if文で良いのでは ...? ところでReLUってどう読むんでしょうか? スペルアウトはRectified Linear Unitなのだそうです。 言葉を作るのは勝手ですが、読み方までしっかり決めておいてほしいですよね。

Affineレイヤ

Affineレイヤを実装。

class Affine:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        self.x = None

    def forward(self, x):
        self.x = x
        return np.dot(self.x, self.W) + self.b

    def backward(self, dy):
        dx = np.dot(dy, self.W.T)
        self.dW = np.dot(self.x.T, dy)
        self.db = np.sum(dy, axis=0)
        return dx

Affine変換とかあるじゃないですか。Affineで変な響きだし、Affineてなによ、と思ったら、一次関数のようです。

Ax+b

線形変換かよ。
ただ、ニューラルネットワークでは良く使うようです。
Wはウエイト(傾き)、xは入力、bはバイアス(切片)に対応します。
全て多次元配列ですので要素数の一致が大事ですね。

クラスAffineの中に、forward関数を定義します。
xを引き数にして、xとWの内積にbを足しますので、xの列数、Wの列数、xとWの内積がbの行列数と一致している必要があります。

同様にbackward関数は誤差逆伝播法で使うので定義します。

 dL/dx = dL/dy dot W.T
 dL/dW = X.T dot dL/dy
 dL/db = sum(dL/dy)

ですね。

早速試してみます。

x = np.array([[1, 2]])
W = np.array([[4, 5, 6],
              [7, 8, 9]])
b = np.array([[10, 11, 12]])

affine = Affine(W, b)
print("forward=", affine.forward(x))

dy = np.array([[1, 2, 3]])
dx = affine.backward(dy)
print("dx=", dx)

forward= [[28 32 36]]
dx= [[32 50]]

forwardはx dot W + bなので、
(1×4+2×7+10, 1×5+2×8+11, 1×6+2×9+12) = (28, 32, 36)
でOK。

backwardはdyとして(1, 2, 3)が返ってきた場合、
(1×4+2×5+3×6, 1×7+2×8+3×9) = (32, 50)
なのでOKですかね。

Multiply関数

Multiply関数を実装。

class MultiLayer:
    def forward(self, x, y):
        self.x = x
        self.y = y
        return x * y
    def backward(self, dLda):
        dLdx = dLda * y
        dLdy = dLda * x
        return dLdx, dLdy

掛け算の関数です。
forwardはただ掛け算するだけですが、backwardの時に使うのでxとyを保存しておきます。

最終結果Lに対するxの勾配、yの勾配は、
dL/dx = dL/da x da/dx = dL/da x y
dL/dy = dL/da x da/dy = dL/da x x
なので、一つ上流の勾配であるdL/daに、xとyを対応させれば良いという事になります。

では試してみましょう。

mul = MultiLayer()
x = np.array([[1],[2]])
y = np.array([[3],[4]])
dLda = np.array([5])
print("forward = ", mul.forward(x, y))
dLdx , dLdy = mul.backward(dLda)
print("dLdx = ", dLdx)
print("dLdy = ", dLdy)

>> forward = [[3] [8]]
   dLdx = [[15] [20]]
   dLdy = [[ 5] [10]]

forwardは(1×3, 2×4) = (3, 8)ですね。
backwardは1行目が(5×3, 5×4) = (15, 20)
2行目が(5×1, 5×2) = ( 5, 10)
ですね。