Aidemy Tech Blog

機械学習・ディープラーニング関連技術の活用事例や実装方法をまとめる、株式会社アイデミーの技術ブログです。

学習時間と正解率から考察する画像認識の機械学習モデル比較【k-NNとMLPとCNN】

データサイエンスは原則に従った技法に基づいて、データから情報や知識を抽出することです。しかし社会やビジネスの中で用いる時には、様々な条件によって選択肢は変わるため、単に情報を抽出すればいいわけではありません。自分の条件・要求を考慮し、どのモデルが一番適しているのかを比較し選択する必要があります。
今回はsk-learn内に存在するMINSTデータで考えていきましょう。

MINSTデータセットを様々な方法で学習する

今回はk-NN,MLP(多層パーセプトロン),CNN(畳み込みニューラルネットワーク)の三つの方法でMINSTデータセットを学習していきます。
それぞれの仕組みについては以下のサイトにわかりやすく載っているので参考にしてください。
k-NN

qiita.com

MLP

qiita.com

CNN

qiita.com

環境について
使用言語
python3系

使用したパッケージ

numpy(np): 配列操作
time: 時間測定
scikit-learn(sklearn): 機械学習のユーティリティ
matplotlib(plt):グラフの作成
tensorflow(tf): 深層学習のユーティリティ(MLP,CNN)

それぞれを実装し、比較しどの方法がいいかを検討する。

k-NN


コード中のコメントの1,2,4,5の部分はこの後のMLP,CNNでもほとんど同じためMNP,CNNでは3の部分のみを示すことにします。
今回はそれぞれを比較するため、単体のグラフは載せずまとめたものを載せることにします。コードに興味のない人は読み飛ばしてください。

#1,MINSTデータセットを読み込む
def load_mnist():
    mnist = fetch_mldata('MNIST original')
    mnist_X, mnist_y = shuffle(mnist.data.astype('float32'),
                               mnist.target.astype('int32'), random_state=42)

    mnist_X = mnist_X / 255.0

    return train_test_split(mnist_X, mnist_y,
                test_size=0.2,
                random_state=42)

#2,データの量を選択しk-NNの実行
def reputation(a):
    train_X, test_X, train_y, test_y = load_mnist()

    train_X_mini = train_X[:a]
    train_y_mini = train_y[:a]
    test_X_mini = test_X[:a]
    test_y_mini = test_y[:a]

    pred_y = k_NN(train_X_mini, train_y_mini, test_X_mini)
    return f1_score(test_y_mini, pred_y, average='macro')


#3,k-NN
def k_NN(train_X, train_y, test_X):
    k=5
    #訓練データの正規化
    norm = np.linalg.norm(train_X, ord=2, axis=1)
    norm2 = np.linalg.norm(test_X, ord=2, axis=1)
    #テストデータを正規化
    normalized_train_X = train_X / norm[:, np.newaxis]
    normalized_test_X = test_X / norm2[:, np.newaxis]
    #結果を格納する配列
    pred_y=np.array([])
    for i in range(test_X.shape[0]):
        
        #i番目のテストデータと各訓練データとのコサイン距離
        distances=np.dot(normalized_test_X[i],normalized_train_X.T)
        #結果を降順でソート
        list_x=distances.argsort()[::-1][:k]
        #上位k個で多数決を実施
        count=np.zeros(10)
        for j in train_y[list_x]:
            count[j] +=1
        #結果を格納
        pred_y=np.append(pred_y,np.argmax(count))
    
    return pred_y

#4,main
a=[10,100,500,1000,3000,6000]
test_accuracy=([])
road_time=([])
for i in range(0,len(a)):
    #実行のタイム計測
    start = time.time()
    test_accuracy.append(reputation(a[i]))
    elapsed_time = time.time() - start
    road_time=np.append(road_time,elapsed_time)

#5,グラフ表示
plt.plot(a, road_time,label="time")
plt.ylabel("time")
plt.xlabel("number")
plt.legend()
plt.show()
plt.plot(a, test_accuracy,label="test accuracy")
plt.ylabel("accuracy")
plt.xlabel("number")
plt.legend()

MLP

def MLP(train_X, train_y, test_X):
    
    np.random.seed(0)
    # Step1. プレースホルダーと変数の定義
    ## Placeholders
    x = tf.placeholder(tf.float32, [None, 784])
    t = tf.placeholder(tf.float32, [None, 10])
    
    ## 変数
    W1 = tf.Variable(np.random.uniform(low=-0.08, high=0.08, size=(784, 200)).astype('float32'), name='W1')
    b1 = tf.Variable(np.zeros(200).astype('float32'), name='b1')
    W2 = tf.Variable(np.random.uniform(low=-0.08, high=0.08, size=(200, 10)).astype('float32'), name='W2')
    b2 = tf.Variable(np.zeros(10).astype('float32'), name='b2')
    params = [W1, b1, W2, b2]

    # Step2. グラフの定義
    u1 = tf.matmul(x, W1) + b1
    z1 = tf.nn.sigmoid(u1)
    u2 = tf.matmul(z1, W2) + b2
    y = tf.nn.softmax(u2)

    # Step3. 誤差関数の定義
    cost = -tf.reduce_mean(tf.reduce_sum(t*tf.log(y)))# tf.log(0)によるnanを防ぐ

    # Step4. 更新則の設定
    gW1, gb1, gW2, gb2 = tf.gradients(cost, params)
    
    updates = [
        W1.assign_add(-0.01*gW1),
        b1.assign_add(-0.01*gb1),
        W2.assign_add(-0.01*gW2),
        b2.assign_add(-0.01*gb2)
    ]

    train = tf.group(*updates)

    valid = tf.argmax(y, 1)

    
    n_epochs = 10
    batch_size = 100
    n_batches = train_X.shape[0] // batch_size

    # Step5. 学習
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        for epoch in range(n_epochs):
            train_X, train_y = shuffle(train_X, train_y, random_state=0)
            for i in range(n_batches):
                start = i * batch_size
                end = start + batch_size
                train_t=np.zeros((100,10))
                train_t[np.arange(100),train_y[start:end]]=1
                sess.run(train, feed_dict={x: train_X[start:end], t: train_t})
        pred_y= sess.run(valid, feed_dict={x: test_X})
    
    
    return pred_y

CNN

def CNN(train_X, train_y, test_X):
    
    #convoluiton層
    class Conv:
        def __init__(self, filter_shape, function=lambda x: x, strides=[1,1,1,1], padding='VALID'):
            # Xavier Initialization
            fan_in = np.prod(filter_shape[:3])
            fan_out = np.prod(filter_shape[:2]) * filter_shape[3]
            self.W = tf.Variable(rng.uniform(
                            low=-np.sqrt(6/(fan_in + fan_out)),
                            high=np.sqrt(6/(fan_in + fan_out)),
                            size=filter_shape
                        ).astype('float32'), name='W')
            self.b = tf.Variable(np.zeros((filter_shape[3]), dtype='float32'), name='b') # バイアスはフィルタごとなので, 出力フィルタ数と同じ次元数
            self.function = function
            self.strides = strides
            self.padding = padding

        def f_prop(self, x):
            u = tf.nn.conv2d(x, self.W, strides=self.strides, padding=self.padding) + self.b
            return self.function(u)
        
    #pooling層
    class Pooling:
        def __init__(self, ksize=[1,2,2,1], strides=[1,2,2,1], padding='VALID'):
            self.ksize = ksize
            self.strides = strides
            self.padding = padding

        def f_prop(self, x):
            return tf.nn.max_pool(x, ksize=self.ksize, strides=self.strides, padding=self.padding)
        
    #平滑化   
    class Flatten:
        def f_prop(self, x):
            return tf.reshape(x, (-1, np.prod(x.get_shape().as_list()[1:])))
        
        
    #全結合
    class Dense:
        def __init__(self, in_dim, out_dim, function=lambda x: x):
            # Xavier Initialization
            self.W = tf.Variable(rng.uniform(
                            low=-np.sqrt(6/(in_dim + out_dim)),
                            high=np.sqrt(6/(in_dim + out_dim)),
                            size=(in_dim, out_dim)
                        ).astype('float32'), name='W')
            self.b = tf.Variable(np.zeros([out_dim]).astype('float32'))
            self.function = function

        def f_prop(self, x):
            return self.function(tf.matmul(x, self.W) + self.b)
        
    rng = np.random.RandomState(1234)
    random_state = 42
        
    #計算グラフ構築 & パラメータの更新設定
    layers = [                            # (縦の次元数)x(横の次元数)x(チャネル数)
        Conv((5, 5, 1, 20), tf.nn.relu),  # 28x28x 1 -> 24x24x20
        Pooling((1, 2, 2, 1)),            # 24x24x20 -> 12x12x20
        Conv((5, 5, 20, 50), tf.nn.relu), # 12x12x20 ->  8x 8x50
        Pooling((1, 2, 2, 1)),            #  8x 8x50 ->  4x 4x50
        Flatten(),
        Dense(4*4*50, 10, tf.nn.softmax)
    ]

    x = tf.placeholder(tf.float32, [None, 28, 28, 1])
    t = tf.placeholder(tf.float32, [None, 10])

    def f_props(layers, x):
        for layer in layers:
            x = layer.f_prop(x)
        return x

    y = f_props(layers, x)

    cost = -tf.reduce_mean(tf.reduce_sum(t * tf.log(y),1)) # tf.log(0)によるnanを防ぐ
    train = tf.train.GradientDescentOptimizer(0.01).minimize(cost)

    valid = tf.argmax(y, 1)
    
    #学習
    n_epochs = 10
    batch_size = 100
    n_batches = train_X.shape[0]//batch_size

    init = tf.global_variables_initializer()
    with tf.Session() as sess:
        sess.run(init)
        for epoch in range(n_epochs):
            train_X, train_y = shuffle(train_X, train_y, random_state=0)
            for i in range(n_batches):
                start = i * batch_size
                end = start + batch_size
                sess.run(train, feed_dict={x: train_X[start:end], t: train_y[start:end]})
            pred_y= sess.run(valid, feed_dict={x: test_X})
            
    return pred_y

考察

k-NN,MLP,CNNのそれぞれの実行結果をまとめたものが以下のグラフとなります。
f:id:t_yamaho:20170729191156p:plainf:id:t_yamaho:20170729191202p:plain
number:学習データとテストデータの合計
time:k-NN,MLP,CNNの実行時間
accuracy:テストデータの精度

このグラフから読み取れることは
time

  • CNNの実行時間の増加速度が極めて大きい
  • MLPは実行時間の増加速度が小さい
  • numberがさらに大きくなる場合k-NNとMLPの差は大きくなりそうです。

accuracy

  • 範囲内では一番k-NNの精度が高い
  • k-NNとMLPの精度の差は小さい
  • CNNはデータが多くなれば精度が高くなりそうです。

以上より以下のように考えることができます。

  • データがとても大きくない限りはk-NNを使うのが良い
  • データが膨大にある時に精度よりも時間を優先する場合はMLPを用いるのが良い


CNNはニューラルネットワークの多層化により時間がかかるため、今回のような単純な画像認識ではただ時間がかかるだけという結果になってしまいました。