第5章 分類モデルを開発してみよう

5.1 事前準備

[自分へのメモ] パッケージやライブラリは必要になったときに import することにする。

5.2 Layerクラスの開発

In [6]:
import numpy as np

class Layer:
    
    def __init__(self, prev_layer_size, layer_size, activation_fn):
        np.random.seed(1)
        self.W = np.random.randn(prev_layer_size, layer_size) / np.sqrt(prev_layer_size) # Xavier initialization
        self.b = np.zeros((layer_size))
        self.activation_fn = activation_fn
        
    def forward(self, A_prev):
        Z = np.dot(A_prev, self.W) + self.b
        A = self.activation_fn(Z)
        return A

5.3 順伝播を体験しよう

Layer 0 → Layer 1

In [7]:
np.random.seed(1)
batch_size = 2
input_layer_size = 2
A0 = np.random.randn(batch_size, input_layer_size)
print(A0.shape)
print('Output of Layer 0: \n', A0)
(2, 2)
Output of Layer 0: 
 [[ 1.62434536 -0.61175641]
 [-0.52817175 -1.07296862]]
In [8]:
relu_fn = lambda Z: np.maximum(Z, 0)   # ReLU Function

layer1 = Layer(2, 3, relu_fn)
print('Weights of Layer 1:\n', layer1.W.shape, '\n', layer1.W)
print('bias of Layer 1:\n', layer1.b.shape, '\n', layer1.b)
Weights of Layer 1:
 (2, 3) 
 [[ 1.14858562 -0.43257711 -0.37347383]
 [-0.75870339  0.6119356  -1.62743362]]
bias of Layer 1:
 (3,) 
 [0. 0. 0.]
In [9]:
# forward propagation
A1 = layer1.forward(A0)

print('Output of Layer 1:\n', A1.shape, '\n', A1)
Output of Layer 1:
 (2, 3) 
 [[2.32984139 0.         0.38894247]
 [0.20741445 0.         1.94344353]]

Layer 1 → Layer 2

In [10]:
layer2 = Layer(3, 4, relu_fn)

print('Weights of Layer 2: \n', layer2.W.shape, '\n', layer2.W)
print('bias of layer2: \n', layer2.b.shape, '\n', layer2.b)
Weights of Layer 2: 
 (3, 4) 
 [[ 0.93781623 -0.35319773 -0.3049401  -0.61947872]
 [ 0.49964333 -1.32879399  1.00736754 -0.43948301]
 [ 0.18419731 -0.14397405  0.84414841 -1.18942279]]
bias of layer2: 
 (4,) 
 [0. 0. 0. 0.]
In [11]:
# forward propagation
A2 = layer2.forward(A1)
print('Outputs of Layer 2:\n', A2.shape, '\n', A2)
Outputs of Layer 2:
 (2, 4) 
 [[2.25660524 0.         0.         0.        ]
 [0.5524937  0.         1.57730579 0.        ]]

Layer 2 → Layer 3

In [14]:
from scipy.special import softmax

softmax_fn = lambda Z: softmax(Z, axis=1)
In [15]:
layer3 = Layer(4, 3, softmax_fn)

print('Weights of Layer 3:\n', layer3.W.shape, '\n', layer3.W)
print('bias of Layer 3:\n', layer3.b.shape, '\n', layer3.b)
Weights of Layer 3:
 (4, 3) 
 [[ 0.81217268 -0.30587821 -0.26408588]
 [-0.53648431  0.43270381 -1.15076935]
 [ 0.87240588 -0.38060345  0.15951955]
 [-0.12468519  0.73105397 -1.03007035]]
bias of Layer 3:
 (3,) 
 [0. 0. 0.]
In [16]:
# forward propagation
A3 = layer3.forward(A2)
print('Outputs of Layer 3:\n', A3.shape, '\n', A3)
Outputs of Layer 3:
 (2, 3) 
 [[0.85589266 0.06865854 0.0754488 ]
 [0.79748189 0.05958264 0.14293548]]

5.4 SimpleClassifier クラスの開発

In [37]:
# SimpleClassifier
class SimpleClassifier:
    def __init__(self, input_layer_size, output_layer_size, hidden_layers_sizes, activation_fn=relu_fn):
        layer_sizes = [ input_layer_size, *hidden_layers_sizes ]
        self.layers = [ Layer(layer_sizes[i], layer_sizes[i+1], activation_fn) for i in range(len(layer_sizes) - 1) ]
        output_layer = Layer(layer_sizes[-1], output_layer_size, softmax_fn)
        self.layers.append(output_layer)
        
    def forward(self, A0):
        A = A0
        for layer in self.layers:
            A = layer.forward(A)
        Y_hat = A
        return Y_hat
    
    def predict(self, X):
        Y_hat = self.forward(X)
        return Y_hat
    
    def evaluate_accuracy(self, X, Y):
        predict = np.argmax(self.predict(X), axis=1)
        actual = np.argmax(Y, axis=1)
        num_corrects = len(np.where(predict==actual)[0])  # np.where() 条件に合う要素のインデックスを返す
        accuracy = num_corrects / len(X)
        return accuracy

5.5 画像分類を体験しよう

docker イメージをビルドする際の Dockerfile の中で次のように記述されていて、pip でmnistがインストールされている。 ここではこのmnistパッケージを利用する。

# install python3 packages
RUN pip install \
    'adversarial-robustness-toolbox==v1.3' \
    'mnist==v0.2.2' \
    'japanize-matplotlib==v1.1.2'

In [20]:
import mnist

X_train, Y_train = mnist.train_images(), mnist.train_labels()
X_test, Y_test = mnist.test_images(), mnist.test_labels()
X_train, X_test = X_train / 255.0, X_test / 255.0
In [22]:
print(X_train.shape, Y_train.shape)
print(X_test.shape, Y_test.shape)
(60000, 28, 28) (60000,)
(10000, 28, 28) (10000,)
In [27]:
%matplotlib inline
import matplotlib.pyplot as plt

fig, ax = plt.subplots(1,1, figsize=(4,4))
ax.imshow(X_test[0], cmap=plt.cm.gray)
ax.axis('off')
ax.text(0.5, -0.1, str(Y_test[0]), fontsize=24, ha='center', transform=ax.transAxes)

plt.show()
In [28]:
# list 5.18 画像データをフラットに変換する
# p.102

X_train_flat = X_train.reshape(-1, 28* 28)
X_test_flat = X_test.reshape(-1, 28*28)

print(X_train_flat.shape, X_test_flat.shape)
(60000, 784) (10000, 784)
In [31]:
# list 5.18 正解ラベルを one hot vector 化する
# p.102

num_classes = 10   # 0, 1, ..., 9 に分類するので 10 classes classification

Y_train_ohe = np.eye(num_classes)[Y_train]
Y_test_ohe = np.eye(num_classes)[Y_test]

print(Y_train_ohe.shape, Y_test_ohe.shape)
(60000, 10) (10000, 10)
In [38]:
# list 5.22 MNISTの分類モデルを作成する
# p.103

mnist_classifier = SimpleClassifier(X_test_flat.shape[1], num_classes, [64, 32]) # 784 --> 10, hidden_unit=64,32
In [39]:
# list 5.23 推論と分類
# p.104

Y_hat = mnist_classifier.predict(X_test_flat[0:1])   # 最初の検証用画像データを用いて推論してみる
print(Y_hat[0])
print('分類結果: ', np.argmax(Y_hat[0]))   # 2 <--間違い (まだ学習していないので、あてずっぽうと同じ)
[0.10292778 0.06061532 0.1338951  0.10314735 0.10864224 0.09773565
 0.11141222 0.08672163 0.09830012 0.09660259]
分類結果:  2
In [42]:
# list 5.24 全テストデータを分類して精度を算出する
# p.105

accuracy = mnist_classifier.evaluate_accuracy(X_test_flat, Y_test_ohe)
print(f'正解率: {accuracy*100:.2f} %')
正解率: 8.63 %

5.6 活性化関数によるモデルの表現力の変化を体験しよう

Layer 0 (2 nodes) → Layer 1 (2 nodes) → Layer 2 (1 node)

Layer 1 の活性化関数 (activation function) を次の3種類に変更して実験する。

  1. $ y = x$
  2. $y = 0.5 x + 1$
  3. $y = ReLU(x) = \begin{cases} x \quad x \geqq 0 \\ 0 \quad x < 0 \\ \end{cases}$

Layer 2 の活性化関数は無し ($y=x$)とする。

In [53]:
# list 5.25 実験用のプログラム
# p.105-106
# [自分へのメモ] 独自のコードに直している

# case 1
classifier1 = SimpleClassifier(2, 1, [2])
classifier1.layers[1].activation_fn = lambda x: x   # y = x
classifier1.layers[0].activation_fn = lambda x: x   # y = x
    
# case 2
classifier2 = SimpleClassifier(2,1,[2])
classifier2.layers[1].activation_fn = lambda x: x   # y = x
classifier2.layers[0].activation_fn = lambda x: 0.5 * x + 1

# case 3
classifier3 = SimpleClassifier(2,1,[2])
classifier3.layers[1].activation_fn = lambda x: x
classifier3.layers[0].activation_fn = lambda x: np.maximum(x, 0)

xmin = -5
xmax = 5
nx = 100

xs = np.linspace(xmin, xmax, nx)
X_input = np.array([ np.array([x, 1]) for x in xs ])

y1 = classifier1.predict(X_input)
y2 = classifier2.predict(X_input)
y3 = classifier3.predict(X_input)
In [62]:
print(y3.shape)
print(y3[0:10])
(100, 1)
[[-0.60741706]
 [-0.58851575]
 [-0.56961444]
 [-0.55071313]
 [-0.53181182]
 [-0.51291051]
 [-0.4940092 ]
 [-0.4751079 ]
 [-0.45620659]
 [-0.43730528]]
In [63]:
# 1次元ベクトルに変換する
y3_flat = np.squeeze(y3)
print(y3_flat.shape)
print(np.squeeze(y3_flat[0:10]))
(100,)
[-0.60741706 -0.58851575 -0.56961444 -0.55071313 -0.53181182 -0.51291051
 -0.4940092  -0.4751079  -0.45620659 -0.43730528]
In [56]:
%matplotlib inline
import matplotlib.pyplot as plt

fig, ax = plt.subplots(1,1,figsize=(6,6))
ax.set_xticks(np.arange(xmin, xmax+1, 1))
ax.set_yticks(np.arange(xmin, xmax+1, 1))
ax.set_xlabel('x1')
ax.set_ylabel('y')
ax.plot(xs, np.squeeze(y1),c='r',label='y1')
ax.plot(xs, np.squeeze(y2),c='b',label='y2')
ax.plot(xs, np.squeeze(y3),c='g',label='y3')

ax.legend()
plt.show()

活性化関数 (activation function) として、非線形関数を用いないと、得られる分類境界が線形(直線)となってしまう。 活性化関数が線形関数だと、層を複数重ねても全く意味がない。いくつ層を重ねても単なる行列の乗算となり、1個の行列と同じ意味となるので。

非線形関数 ReLU を用いた場合は、境界が直線ではないことがわかる。