pit-rayの備忘録

知識のあうとぷっと

【Python】二次元配列に対応したソフトマックス関数

近頃、ディープラーニングに興味があり、そのアウトプットの一環としての記事になります。
特に新しい内容ではありません。
特に配列の基本が分かっていれば読む必要はありません。


では書きます。

ソフトマックス関数とは

多値分類に限定するとニューラルネットワークの入力層から得られたデータを、出力層にてどのように料理するかを決める活性化関数は主に二つあります。

活性化関数 処理
恒等関数 入力信号をそのまま出力
ソフトマックス関数 入力信号を正規化して出力

(二値分類の場合は、シグモイド関数などを用いる場合があります。)

データを正規化するメリットは、そのデータを確率的に比較できる点が挙げられます。
例えば、対象のものが犬か猫か判別する場合、
( 犬, 猫 ) = ( 0.3, 4.5 )
このような入力信号の場合、これを正規化すると
( 犬, 猫 ) = ( 0.0625, 0.9375 )
これは犬の確率は6.25%で、猫の確率は93.75%だといえます。

このように正規化することで、データを確率的に見ることができます。
しかし、推論においてはデータの大小が分かれば判別できるため、一般的にはソフトマックス関数は使用しません。
一方学習においては、損失関数を適用するため、正規化したものが必要になります。

では早速ソフトマックス関数の実装を考えていきます。
まず、ソフトマックス関数は次の式で表されます。
  y _ k =  \displaystyle{ \frac{e^{a _ k} }{ \displaystyle{\sum_{i=1}^n e^{a _ i}}}}

k というのは、上の例でいうと猫や犬などのクラスインデックスです。
つまり、k 番目の出力を求める計算式を示しています。
このように指数関数を通したデータを用いて、正規化します。

一次元のソフトマックス関数を実装する

numpyを用いて実装すると次のようになります。

import numpy as np

def softmax( x ):
    return np.exp( x ) / np.sum( np.exp( x ) )

しかしこのような実装ではうまくいきません。
なぜなら、指数関数は簡単に桁が膨大になり、オーバーフローしやすいからです。

この対策として、データを定数の足し算として扱う方法があります。
先ほどのソフトマックス関数を少しだけ変形します。

\ \ \ \ y _ k =  \displaystyle{ \frac{e^{a _ k} }{ \displaystyle{\sum_{i=1}^n e^{a _ i}}}}
= \displaystyle{ \frac{Ce^{a _ k} }{ \displaystyle{C\sum_{i=1}^n e^{a _ i}}}}
= \displaystyle{ \frac{e^{a _ k+ \log C} }{ \displaystyle{\sum_{i=1}^n e^{a _ i + \log C }}}}
= \displaystyle{ \frac{e^{a _ k+ C'} }{ \displaystyle{\sum_{i=1}^n e^{a _ i + C' }}}}

よって、変数 a に同じ量だけ加算しても結果は変わりません。

今後は、それぞれの入力信号を最大の値で引き算したものをデータとして使っていきます。
Pythonで実装すると次のようになります。

import numpy as np

def softmax( x ):
    x = x - np.max( x ) #New
    return np.exp( x ) / np.sum( np.exp( x ) )

これが一次元配列に対するソフトマックス関数になります。

二次元に対応したソフトマックス関数を実装する

numpy.ndarrayのメンバであるndimで場合分けをします。

import numpy as np

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

    x = x - np.max( x )
    return np.exp( x ) / np.sum( np.exp( x ) )

この実装は間違っています。
まずは次の図をご覧ください。
f:id:pit-ray:20190303195318j:plain:w250
これは二次元の場合のaxis(軸)の方向を示しています。
データはaxisが1の方向に並んでいます。


では、ほかの例に関して詳しく見ていきます。
maxメソッドをaxis = 1で実行すると次のようになります。
f:id:pit-ray:20190303195806j:plain:w300
上の例では、maxで得られる配列は( 5, 3 )という配列になります。
しかし、これを元の配列から引くと問題が生じます。
max配列は横に並ぶからです。

理想
f:id:pit-ray:20190401170720j:plain

実際
f:id:pit-ray:20190401170735j:plain

このようになってしまいます。

見てわかる通り、max関数を通す前に転置すればうまくいきます。
実際のコードは次のようになります。

import numpy as np

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 ) )

一度転置することでmaxで得られた横向きの配列が、元のデータに合うようにブロードキャストされます。
転置するのでデータは縦向きになり、axis = 0であることに注意しましょう。

他にmaxで得られた配列を転置でもいいかと思います。

以上が二次元配列に対応したソフトマックス関数になります。
maxで得られる形に注意しろというだけの中身のない記事でした。

参考文献

斎藤 康毅 『ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装』

(はてなブログのtex構文を使ってみたかっただけ)