本文写于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训练和运行起来实在是太耗硬件了。
这是我的微信公众号二维码:
欢迎关注! 这是我的微信号二维码,扫一扫可以和我交流: