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)
ですね。

Add関数

Add関数を実装。

class AddLayer:
    def forward(self, x, y):
        return x + y
    def backward(self, dLda):
        dLdx = dLda * 1
        dLdy = dLda * 1
        return dLdx, dLdy

足し算の関数です。
forwardはただ加算して値を返すだけ。
x + y = a
で、最終結果Lに対するxの勾配、yの勾配を求めます。
dL/dx = dL/da x da/dx = dL/da x 1
dL/dy = dL/da x da/dy = dL/da x 1
なので、一つ上流の勾配であるdL/daをそのまま伝播させれば良いという事になります。

では試してみましょう。

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

>> forward = [[4] [6]]
   dLdx = [5]
   dLdy = [5]

できた。

レイヤ

ニューラルネットワークでは入力層から出力層の間に複数の中間層を設ける事ができ、各層を入力層から順番に1層目、2層目、、、と数えていきます。各層のノードは次の層のノードにつながっていて、次の層のノードは前の層の全ノードと重みとの内積を取り、それに活性化関数をかけてノードの値を決定していきます(全結合型)。
この内積を取る部分や活性化関数で処理する部分をレイヤと呼びます。
ニューラルネットワークでは重みを更新する必要があり、誤差逆伝播法を用いるため、各レイヤはそれぞれ順方向と逆方向の関数を含みます。
内積を取る部分はAffineレイヤ、活性化関数はReLUやSigmoidなどを使いますので、これらをクラスとして実装していきます。

Pythonのファイルをコンパイル

Pythonで作ったプログラムを、そのうちPythonが入っていないPCでも実行したくなるよね、という事で、Exe化するソフトを使ってみました。
Pyinstallerというソフトです。

Pyinstallerのサイトはこちら

インストールは下記の通り。

pip install pyinstaller

使い方は下記の通り。

pyinstaller test.py

一つのファイルにまとめるオプションや、コンソールを開かないオプションなどがあるようです。

pyinstaller --onefile --noconsole test.py

一つのファイルにまとめる事が出来るのは便利そうですよね。
で、早速やってみたところ …

OSError: Python library not found: libpython3.6.so.1.0, libpython3.6mu.so.1.0, libpython3.6m.so.1.0

あれ?うまく行ってない。Pythonライブラリの3.6が入っていないようです。
そんなばかな、うちのPCには3.6が入っているはずです。

python -V
Python 3.6.5 :: Anaconda, Inc.

ほらね。

しばらく調べたところ、、、
私のPCの/usr/lib64/にはlibpython2.7.soが入っている。
あれ?3.6.5はどこ?

大分調べたところ、anacondaをインストールしたフォルダにあるようで、ここはリンクが貼られていない様子。
私はhomeにanacondaをインストールしたので、下記のところにありましたよ。

/home/user/anaconda3/lib

なるほどーそうかー。
これをLD_LIBRARY_PATHに追加すればよいようです。
.bashrcに下記を書き加えます。書き換えるファイルはOSなどの環境によって違いますね。

export LD_LIBRARY_PATH="/home/user/anaconda3/lib:$LD_LIBRARY_PATH"

ターミナルをいったん閉じてまた開ける。
再度pyinstallerを実行して、無事にexeがdistフォルダの中にできました。結構時間かかりますね。
anacondaをインストールした時に、インストーラが.bachrcを書き換えたのは見ていたのですが、binを追加しただけで、pathは追加されていなかったようです。
出来たファイルを実行してみましょう。

./test

うまく行きました。さほど深くはまらずにできて良かった。
これ、linux上でコンパイルしたんですが、windowsでも動くのかな?と思い、windows10にコピーしてきてやってみましたが、それはダメでした。
そりゃそうか。
試しに他のlinuxにtestをコピーしてみたら、無事動きました。

ところで、コンパイルの方法にはバイトコンパイルというものもあるそうです。
バイトコンパイル

python -m compileall test.py

__pycache__の中に.pycというファイルがあり、これが実行形式なのだそう。
実行環境が改善する事があるそうですが、、、今の私の小さいプログラムだとありがたみが分かりませんね。

Cupyを使ってみる

Pythonでの計算はnumpyを使って行列計算を行うと非常に早く実行できますが、それでも桁が大きくなってくるとそれなりに待たされてしまいます。
GPUを使った行列計算はCPUを使ったものよりも、劇的に早くなる可能性を秘めているとの事ですが、なかなか実装が難しそう。
そんな中、CupyというNumpyと互換性のあるライブラリがChainerの一部として存在するそうです。
とても簡単に実行できるのだそう。やってみようやってみよう。

Cupyのサイトはこちら

Cupyのインストールはpipで行えるそう。

pip install cupy-cuda91

特に問題なくインストールも完了。
cuda[xx]の部分はバージョンを入れるようです。
ちなみにcudaのバージョンを調べるのは

nvcc -V

で出来ましたよ。

nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2017 NVIDIA Corporation
Built on Fri_Nov__3_21:07:56_CDT_2017
Cuda compilation tools, release 9.1, V9.1.85

私の環境では9.1ですね。
それではCupyを使って計算速度を測ってみましょう。
ちなみに私のPCの環境は、

CPU : Intel Core i7 6800K 6コア 12スレッド 3.4GHz
GPU : GeForce GTX 1080Ti 11GB
メモリー : 32GB DDR4-2400
ストレージ : 480GB SSD
OS : CentOS 7.4

となっておりました。いろいろ使えそう。

ちなみにそれぞれのコマンドラインからの調べ方は
CPCの種類の確認 :

cat /proc/cpuinfo

GPUの種類の確認 :

lspci | grep -i VGA

メモリーの確認 :

cat /proc/meminfo

OS :

cat /etc/redhat-release

で調べられます。すぐ忘れるのでメモ必須です。

では早速Cupyを試しましょう。サイトにある例を少しもじります。
10000 x 10000の行列をfloatで作成し、内積を取ります。
timeを使って時間を測り、時間を出力します。
まずはnumpyから。

import numpy as np
import time
N_size = 10000
x = np.random.rand(N_size, N_size).astype('f4')
y = np.random.rand(N_size, N_size).astype('f4')

start = time.time()
np.dot(x, y)
elapsed_time = time.time() - start
print("numpy :{0}".format(elapsed_time))

test.pyと名前つけて保存して実行。

python test.py
numpy :4.0388100147247314

10000 x 10000なのでそれなりにかかりますよね。

ではこれをCupyに変えてみます。

import cupy as cp
import time
N_size = 10000
x = cp.random.rand(N_size, N_size).astype('f4')
y = cp.random.rand(N_size, N_size).astype('f4')

start = time.time()
cp.dot(x, y)
elapsed_time = time.time() - start
print("cupy :{0}".format(elapsed_time))

ほんとにそのままですね。これを保存して実行。

python test.py
cupy :0.1345818042755127

うん、早くなった。
更に、numpyで用意していた変数をGPUに移すには以下のようにするらしいです。

import numpy as np
import cupy as cp
import time
N_size = 10000
x = np.random.rand(N_size, N_size).astype('f4')
y = np.random.rand(N_size, N_size).astype('f4')

x_cp = cp.asarray(x)
y_cp = cp.asarray(y)

start = time.time()
cp.dot(x_cp, y_cp)
elapsed_time = time.time() - start
print("cupy :{0}".format(elapsed_time))
python test.py
cupy :0.17929410934448242

あれ?さっきより遅くなった。そういう、、、ものなのかな?

numpyを使ってみる

numpyをインポートします。

行列計算を行っていくにはnumpyが基本になります。
numpyで変数を定義し、デフォルトで配列を使っていくイメージです。
配列の演算を行うとき、Cではforを使って一つずつ回していたものをなるべくnumpyの関数を用いて実装する事で、処理が高速になります。

例えばベクトルの内積はこちら。Notebookに下記のように打ち込んで、Runを押すと結果が見れます。

import numpy as np
x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
z = np.dot(x, y)
print(z)

>> 32

内積なので、1×4 + 2×5 + 3×6 = 4 + 10 + 18 = 32 ですね。うん合ってる。
ベクトルの内積だけでなく、行列の内積も同じです。

import numpy as np
x = np.array([[1, 2], [3, 4], [5, 6]])
y = np.array([[7, 8, 9], [10, 11, 12]])
z = np.dot(x, y)
print(z)

>> [ [ 27  30  33]
     [ 61  68  75]
     [ 95 106 117] ]

行列計算になると一気に数字が増えますね。
この例ですと、xは3×2行列、yは2×3行列となり、zは3×3行列となります。
行の数と列の数を明示的に示す場合に、

x = np.array([ [1, 2],
               [3, 4], 
               [5, 6] ])

とあらわす場合もあるようです。
行 : 横長1セットが3つ、
列 : 縦長1セットが2つ、
ですので、3×2行列です。
数学での行列計算と同じく、xの列の数とyの行の数が一致しないと計算できないので注意してください。

場合によっては転置が必要な場合もあります。
Pythonでの転置行列は.Tです。

print(x.T)
>> [ [1 3 5]
     [2 4 6] ]

この辺りをうまく使って、for文をなるべく回避するのが、Pythonプログラムの基本のようです。