1. 程式人生 > >影象分類之:經典機器學習 Battle 深度學習

影象分類之:經典機器學習 Battle 深度學習

本文寫於2018.08.31, 生日前夕。

前段時間,有個朋友和我提到,自己最近正打算用機器來判別圖片中的場景是古鎮還是園林,所以我這一期特地寫了一篇文章,來描述影象的分類演算法。由於最近工作略忙,所以文章斷斷續續寫了好久,終於在自己生日前夕完成,希望可以有所幫助,這樣我就可以安心回家吃蛋糕了。

用機器來做圖片分類簡單來講就是給機器輸入一張圖片,機器會輸出這幅圖片裡面的內容。機器學習中提供了很多對資料分類的演算法,像KNN(最近鄰)、Adaboost、Naive Bayes(樸素貝葉斯)、SVM(支援向量機)、ANN(人工神經網路)等以及最近幾年興起的CNN(卷積神經網路)

在這篇文章的實作部分,我會挑出KNN、SVM、ANN來實現,使用的是使用經典的scikit-learn(sklearn)

庫,CNN則使用Keras來實現。

程式設計的環境需求:

  • 系統:windows / linux

  • 直譯器:python 3.6

  • 依賴庫:numpy、opencv-python 3、tensorflow、keras、scikit-learn

資料集選擇:

由於這幾天無法穿越到蘇州去採集大量園林和古鎮的圖片,而且本文還會去實驗多分類的情況,但是既然同是圖片的分類問題,我決定去網上搜集一些和風景相關的資料集。

突然有一天,在我逛GitHub的時候,它就這樣出現了,

在我的世界裡

帶給我驚喜

情不自已

為此,我給作者寫了一封信:

作者在我晚上吃飯的時候給了回覆:

這。。。。。。。

看來這條路沒走順,不如去看看一些知名的資料集裡有沒有自己需要的東西吧,不過有承諾在先,我還是會標註上資料集作者的GitHub地址:(https://github.com/yuweiming70/Landscape-Dataset),畢竟他送了我這麼多精美桌布

牛津大學的17 Category Flower Dataset

(http://www.robots.ox.ac.uk/~vgg/data/flowers/17/index.html)很漂亮,看起來就是我在尋找的資料集(沒錯,我判斷一個東西是不是自己需要的標準就是漂不漂亮)。這個資料集總共17種花,每種花有80張圖片,整個資料集有1360張圖片,為了既達到實驗的目的又不在訓練上耗費太多的時間,我在同一種演算法上選取了前兩種花和前四種花做對比實驗:

由於SVM和ANN的原理會佔用太多的篇幅,並且這篇文章的主要目的是為了講解程式碼實現,所以這裡只介紹下機器學習中最簡單的KNN分類器:

KNN是資料探勘分類技術中最簡單的方法之一,k最近鄰,從名字大致就可以看出它的含義,就是找出K個離自己最近(相似)的資料,在這K個找到的資料中,看看那個類別最多,那麼就認為自己是屬於哪一個類別。

關於相似度度量的方法有很多,常見的有歐氏距離、曼哈頓距離、切比雪夫距離(切比雪夫兄跨界實在太多)、漢明距離、餘弦相似度(夾角餘弦值)等等。

KNN用到的是:歐氏距離(L2)

在資料(向量)只有二維的情況下,兩個資料之間的歐氏距離就是兩個點在二維座標系下的直線距離,用初中一年級的數學公式就可以算出來:

當資料推廣到多維的時候,兩個資料之間的歐式距離就變成了:

距離越小表示兩個向量相似度越大。

KNN有著實現方法簡單、無需訓練的優點,但是由於每次分類都要計算和所有資料之間的相似度,所以當資料維度很大或者資料數量很大的時候,計算會很耗時。

實驗(KNN、SVM、ANN):

現在我要使用sklearn中的KNeighborsClassifier( KNN )、SVC( SVM )、MLPClassifier( 多層感知機分類器:ANN ),來實現這三個演算法,由於sklearn中的演算法模型高度統一化,所以三個程式可以寫在同一個例子中,只是在建立分類器模型的時候略有不同:

引入KNeighborsClassifier、SVC、MLPClassifier模組:

from sklearn.neighbors import  KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.neural_network import MLPClassifier

引入訓練樣本分割函式:

from sklearn.model_selection import train_test_split

引入numpy 和 opencv:

import cv2
import numpy as np

讀取影象函式,返回影象列表和標籤列表:

IMAGE_SIZE = 100

def resize_without_deformation(image, size = (IMAGE_SIZE, IMAGE_SIZE)):
    height, width, _ = image.shape
    longest_edge = max(height, width)
    top, bottom, left, right = 0, 0, 0, 0
    if height < longest_edge:
        height_diff = longest_edge - height
        top = int(height_diff / 2)
        bottom = height_diff - top
    elif width < longest_edge:
        width_diff = longest_edge - width
        left = int(width_diff / 2)
        right = width_diff - left

    image_with_border = cv2.copyMakeBorder(image, top , bottom, left, right, cv2.BORDER_CONSTANT, value=[0, 0, 0])

    resized_image = cv2.resize(image_with_border, size)
    return resized_image

def read_image(size = None):
    data_x, data_y = [], []
    #for i in range(1, 1361):
    for i in range(1, 241):
        try:
            im = cv2.imread('17flowers/image_%s.jpg' % str(i).zfill(4))

            if size is None:
                size = (IMAGE_SIZE, IMAGE_SIZE)
            im = resize_without_deformation(im, size)
            im = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
            data_x.append(np.asarray(im, dtype = np.int8))
            data_y.append(str(int((i-1)/80.0)))
        except IOError as e:
            print(e)
        except:
            print('Unknown Error!')

    return data_x, data_y

raw_images, raw_labels = read_image(size = (IMAGE_SIZE, IMAGE_SIZE))
raw_images, raw_labels = np.asarray(raw_images, dtype = np.float32), np.asarray(raw_labels, dtype = np.int32)

由於這三種分類器只接受一維向量的輸入,所以將圖片拍扁:

raw_images = raw_images.reshape((-1, IMAGE_SIZE * IMAGE_SIZE))

分割訓練集和測試集(訓練集:測試集 = 8 : 2):

train_images, test_images, train_labels, test_labels = train_test_split(raw_images, raw_labels,
                                                                        test_size = 0.2)

圖片資料歸一化:

train_images /= 255.0
test_images /= 255.0

建立分類器模型:

classifier_model = KNeighborsClassifier(n_neighbors = 7)

'''
classifier_model = SVC(C = 1.0,
                       kernel = 'rbf',
                       max_iter = 10000,
                       class_weight = 'balanced')
'''

'''
classifier_model = MLPClassifier(hidden_layer_sizes=(20, 100, 70), activation = 'relu',
                                 solver = 'sgd', batch_size = 5,
                                 learning_rate_init = 0.001, max_iter = 1000,
                                 alpha=1e-4, tol=1e-4, 
                                 random_state=1, shuffle = True,
                                 momentum = 0.8)
'''

訓練:

classifier_model.fit(train_images, train_labels)

計算準確率:

accuracy = classifier_model.score(test_images, test_labels)
print('Accuracy: %s' % str(accuracy))

實驗了幾次,計算準確率平均值大致得到:

  • KNN:     73.8%

  • SVM:     78.5%

  • ANN:     78.9%

上面只是2分類的情況,現在取4種花來訓練,得到準確率:

  • KNN:     40.1%

  • SVM:     40.6%

  • ANN:     41.5%

並且ANN在訓練集上的正確率表現為100%, 很明顯,已經過擬合,即模型已經呈現了記憶效應。

實驗(CNN):

現在來試下卷積神經網路(由於上篇文章已經講解過卷積神經網路的構建過程,這篇文章就不再贅述):

引入相關模組:

import  keras
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dense, Dropout, Flatten
from keras.optimizers import SGD
from keras.utils import np_utils
from sklearn.model_selection import train_test_split
import cv2
import numpy as np

讀取影象函式,返回影象列表和標籤列表:

IMAGE_SIZE = 100

def resize_without_deformation(image, size = (IMAGE_SIZE, IMAGE_SIZE)):
    height, width, _ = image.shape
    longest_edge = max(height, width)
    top, bottom, left, right = 0, 0, 0, 0
    if height < longest_edge:
        height_diff = longest_edge - height
        top = int(height_diff / 2)
        bottom = height_diff - top
    elif width < longest_edge:
        width_diff = longest_edge - width
        left = int(width_diff / 2)
        right = width_diff - left

    image_with_border = cv2.copyMakeBorder(image, top , bottom, left, right, cv2.BORDER_CONSTANT, value=[0, 0, 0])

    resized_image = cv2.resize(image_with_border, size)

    return resized_image

def read_image(size = None):
    data_x, data_y = [], []
    #for i in range(1, 1361):
    for i in range(1, 161):
        try:
            im = cv2.imread('17flowers/image_%s.jpg' % str(i).zfill(4))
            #im = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
            if size is None:
                size = (IMAGE_SIZE, IMAGE_SIZE)
            im = resize_without_deformation(im, size)
            data_x.append(np.asarray(im, dtype = np.int8))
            data_y.append(str(int((i-1)/80.0)))
        except IOError as e:
            print(e)
        except:
            print('Unknown Error!')

    return data_x, data_y

raw_images, raw_labels = read_image(size = (IMAGE_SIZE, IMAGE_SIZE))
raw_images, raw_labels = np.asarray(raw_images, dtype = np.float32), np.asarray(raw_labels, dtype = np.int32)

One-Hot編碼:

ont_hot_labels = np_utils.to_categorical(raw_labels)

分割訓練集和測試集(訓練集:測試集 = 8 : 2):

train_images, test_images, train_labels, test_labels = train_test_split(raw_images, ont_hot_labels,
                                                                        test_size = 0.2)

圖片資料歸一化:

train_images /= 255.0
test_images /= 255.0

構建CNN:

採用了VGG19結構的CNN:

深度很深,訓練時間很長,特別耗記憶體和處理器(訓練的時候記得在電腦下面墊冰塊):

image_classification_model = keras.Sequential()

image_classification_model.add(Conv2D(64,(3,3),strides=(1,1),input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(Conv2D(64,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(MaxPooling2D(pool_size=(2,2)))
image_classification_model.add(Conv2D(128,(3,2),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(Conv2D(128,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(MaxPooling2D(pool_size=(2,2)))
image_classification_model.add(Conv2D(256,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(Conv2D(256,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(Conv2D(256,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(MaxPooling2D(pool_size=(2,2)))
image_classification_model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(MaxPooling2D(pool_size=(2,2)))
image_classification_model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(Conv2D(512,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
image_classification_model.add(MaxPooling2D(pool_size=(2,2)))
image_classification_model.add(Flatten())
image_classification_model.add(Dense(4096,activation='relu'))
image_classification_model.add(Dropout(0.5))
image_classification_model.add(Dense(4096,activation='relu'))
image_classification_model.add(Dropout(0.5))
image_classification_model.add(Dense(len(ont_hot_labels[0]),activation='softmax'))

image_classification_model.summary()

如果想要快速訓練,也可以使用下面的簡化模型,效果也還不錯:

image_classification_model = keras.Sequential()

image_classification_model.add(Conv2D(32, 3, 3, border_mode='valid',
                                  subsample = (1, 1),
                                  dim_ordering = 'tf',
                                  input_shape = (IMAGE_SIZE, IMAGE_SIZE, 3),
                                  activation='relu'))

image_classification_model.add(Conv2D(32, 3, 3,border_mode='valid',
                                  subsample = (1, 1),
                                  dim_ordering = 'tf',
                                  activation = 'relu'))

image_classification_model.add(MaxPooling2D(pool_size=(2, 2)))
image_classification_model.add(Dropout(0.25))

image_classification_model.add(Conv2D(64, 3, 3, border_mode='valid',
                                  subsample = (1, 1),
                                  dim_ordering = 'tf',
                                  activation = 'relu'))

image_classification_model.add(Conv2D(64, 3, 3, border_mode='valid',
                                  subsample = (1, 1),
                                  dim_ordering = 'tf',
                                  activation = 'relu'))

image_classification_model.add(MaxPooling2D(pool_size=(2, 2)))
image_classification_model.add(Dropout(0.25))

image_classification_model.add(Flatten())
image_classification_model.add(Dense(512, activation = 'relu'))
image_classification_model.add(Dropout(0.5))
image_classification_model.add(Dense(len(ont_hot_labels[0]), activation = 'sigmoid'))

image_classification_model.summary()

設定SGD優化器並編譯模型:

learning_rate = 0.01
decay = 1e-6
momentum = 0.9
nesterov = True
sgd_optimizer = SGD(lr = learning_rate, decay = decay,
                momentum = momentum, nesterov = nesterov)
image_classification_model.compile(loss = 'categorical_crossentropy',
                               optimizer = sgd_optimizer,
                               metrics = ['accuracy'])

訓練,這裡只訓練30次:

batch_size = 20
epochs = 30
image_classification_model.fit(train_images, train_labels,
                           epochs = epochs,
                           batch_size = batch_size,
                           shuffle = True,
                           validation_data = (test_images, test_labels))

看看這個模型在測試集上的表現:

score = image_classification_model.evaluate(test_images, test_labels, verbose=0)
print("%s: %.2f%%" % (image_classification_model.metrics_names[1], score[1] * 100))

準確率96.88%

再試下四種花的情況,在測試集上正確率為 70%,在訓練集上正確率為 99.6%,雖然也過擬合,但是比三種經典分類器效果要好很多。

這種感覺就好像CNN把經典機器學習分類器的臉按在地上瘋狂的摩擦,不放潤滑油的那種。

不過反思一下,如果在進SVM、KNN、ANN之前,可以做一些特徵提取,效果應該會更好一些。畢竟CNN訓練和執行起來實在是太耗硬體了。

這是我的微信公眾號二維碼:

歡迎關注!   這是我的微訊號二維碼,掃一掃可以和我交流: