准备工作:
1、申请一个aws云机器,因为本地训练实在太慢了,当然,土豪请略过……
2、申请一个kaggle账号,数据集可以利用winscp上传到云端,但上传速度过慢,建议利用kaggle-cli下载数据集到aws云端(申请kgggle账号时注册码无法显示,你需要下载一个插件,用chrome浏览器。)
3、利用putty连接到云端后,启用tensorflow_p36的虚拟环境,升级并安装相关的包,比如sklearn,opencv等等;
4、下载数据集到云端:
kg download -u
now,备份你的ami,开始吧。
正式开始:
# coding: utf-8
# # 猫狗大战
# 本质是图片分类问题,根据数据集训练后,检测图片属于哪个类别,共两类,dog and cat.
# ## 1、第一步:数据预处理
# In[1]:
#导入数据
import numpy as np
import tensorflow as tf
from PIL import Image
import matplotlib.pyplot as plt
trainCwd = "./train/"
testCwd = "./test1/"
classes = ['cat','dog']
# ### 1.1定义文件分离函数,将训练数据一部分拆成验证数据。利用keras的flow_from_directory创建datagenerator。
#
# In[2]:
import os
import shutil
def filesplit(trainCwd):
for i in classes:
trainDir = os.path.join(trainCwd,i)
print(trainDir)
if os.path.exists(trainDir)==False:
os.makedirs(trainDir)
print("%s is created"%(trainDir))
filedir = os.listdir(trainCwd) #数据类别
catfolder = os.path.join(trainCwd,"cat")
dogfolder = os.path.join(trainCwd,"dog")
for root,sub_folders,files in os.walk(trainCwd):
for name in files:
try:
if os.path.exists(os.path.join(root,name))==True:
if name[0:3]=="cat":
shutil.move(os.path.join(root,name),catfolder)
if name[0:3]=="dog":
shutil.move(os.path.join(root,name),dogfolder)
except:
pass
if os.path.exists(os.path.join(testCwd,"test"))==False:
os.makedirs(os.path.join(testCwd,"test"))
testFolder = os.path.join(testCwd,"test")
for root,sub_folders,files in os.walk(testCwd):
for name in files:
try:
if os.path.exists(os.path.join(root,name))==True:
shutil.move(os.path.join(root,name),testFolder)
except:
pass
# 定义图片读取函数
# 将图片读入列表中,进行图片和标签的一一对应,并进行shuffle处理。同时拆分出验证集。
# In[3]:
from sklearn.model_selection import train_test_split
def get_file(file_dir,dataName,width,height,test_size=0.2):
images = []
labels = []
temp =[]
X_train = []
Y_train = []
X_val = []
Y_val = []
X_test = []
Y_test =[]
file_name = os.path.join(file_dir+dataName)
if (dataName == "trainData.pkl"):
if os.path.exists(file_name): #判断之前是否有存到文件中
X_train, y_train,X_test, y_test = pickle.load(open(file_name,"rb"))
return X_train, y_train,X_test, y_test
else:
for root,sub_folders,files in os.walk(file_dir):
for name in files:
#img = cv2.imread(os.path.join(root,name))
images.append(os.path.join(root,name))
if name[0:3]=="cat":
labels.append(0)
else:
labels.append(1)
tmp = np.array([images,labels])
tmp = tmp.transpose()
np.random.shuffle(tmp)
x = tmp[:,0]
y = tmp[:,1]
X_train, X_val, Y_train, Y_val = train_test_split(x, y, test_size=test_size,random_state=1)
pickle.dump((X_train, Y_train, X_val, Y_val),open(file_name,"wb"))
print("file created!")
return X_train,Y_train,X_val,Y_val
else:
if os.path.exists(file_name):
X_test,Y_test = pickle.load(open(file_name,"rb"))
else:
for root,sub_folders,files in os.walk(file_dir):
for name in files:
images.append(os.path.join(root,name))
x_test = np.array([images])
pickle.dump(x_test,open(file_name,"wb"))
return x_test
# In[4]:
import math
import cv2
def display_img(img_list, summary = True):
fig = plt.figure(figsize=(15, 3 * math.ceil(len(img_list)/5)))
for i in range(0, len(img_list)):
img = cv2.imread(img_list[i])
img = img[:,:,::-1]#BGR->RGB
if summary:
print("---->image: {} - shape: {}".format(img_list[i], img.shape))
ax = fig.add_subplot(math.ceil(len(img_list)/5),5,i+1)
ax.set_title(os.path.basename(img_list[i]))
ax.set_xticks([])
ax.set_yticks([])
img = cv2.resize(img, (128,128))
ax.imshow(img)
plt.show()
# ### 列出dog 和cat的类别,为预测做准备
# In[5]:
Dogs = [ 'n02085620','n02085782','n02085936','n02086079','n02086240','n02086646','n02086910','n02087046','n02087394','n02088094','n02088238',
'n02088364','n02088466','n02088632','n02089078','n02089867','n02089973','n02090379','n02090622','n02090721','n02091032','n02091134',
'n02091244','n02091467','n02091635','n02091831','n02092002','n02092339','n02093256','n02093428','n02093647','n02093754','n02093859',
'n02093991','n02094114','n02094258','n02094433','n02095314','n02095570','n02095889','n02096051','n02096177','n02096294','n02096437',
'n02096585','n02097047','n02097130','n02097209','n02097298','n02097474','n02097658','n02098105','n02098286','n02098413','n02099267',
'n02099429','n02099601','n02099712','n02099849','n02100236','n02100583','n02100735','n02100877','n02101006','n02101388','n02101556',
'n02102040','n02102177','n02102318','n02102480','n02102973','n02104029','n02104365','n02105056','n02105162','n02105251','n02105412',
'n02105505','n02105641','n02105855','n02106030','n02106166','n02106382','n02106550','n02106662','n02107142','n02107312','n02107574',
'n02107683','n02107908','n02108000','n02108089','n02108422','n02108551','n02108915','n02109047','n02109525','n02109961','n02110063',
'n02110185','n02110341','n02110627','n02110806','n02110958','n02111129','n02111277','n02111500','n02111889','n02112018','n02112137',
'n02112350','n02112706','n02113023','n02113186','n02113624','n02113712','n02113799','n02113978']
Cats=['n02123045','n02123159','n02123394','n02123597','n02124075','n02125311','n02127052']
# In[6]:
from keras.preprocessing import image
from keras.applications.inception_v3 import InceptionV3,preprocess_input
from keras.applications.vgg19 import VGG19
from keras.optimizers import SGD
import numpy as np
from keras.applications.resnet50 import ResNet50
from keras.applications import *
import h5py
from keras.models import *
from keras.layers import *
from keras.applications.xception import Xception
# In[ ]:
#model_vgg = VGG19(weights='imagenet') #当启用vgg进行异常值判断时
model_vgg =Model()
#model_xce = Xception(weights='imagenet')#当启用xception进行异常值判断时
model_xce=Model()
model_res = Model()
#model_res = ResNet50(weights='imagenet')#当启用resnet进行异常值判断时
###不在此处一次进行全部初始化,是因为可以避免占用更多的内存,为后续的训练节省资源。
# #创建generator
# In[8]:
from keras.preprocessing.image import ImageDataGenerator
def creatDataGen(dir,width,height,batch_size):
datagen = ImageDataGenerator()
generator = datagen.flow_from_directory(
dir,
target_size=(width, height),
shuffle=False,
batch_size=batch_size,
class_mode=None)
print("generator created:",generator)
return generator
# ## 预测异常数据
# In[9]:
#得到所有的图像地址列表
train_img_list = []
def get_image_list(path_name, list_name):
for file_name in os.listdir(path_name):
subPath = os.path.join(path_name,file_name)
for file in os.listdir(subPath):
list_name.append(os.path.join(subPath, file))
get_image_list(trainCwd, train_img_list)
with open("./filelist.txt", 'w') as f:
for item in train_img_list:
f.write("{}\n".format(item))
print("train image sample:{}".format(len(train_img_list)))
# In[15]:
#计算得到错误图片,根据前期定义的三个模型,判断后取并集
def getErrorImg(img_path_list,model,preprocess_input,decode_predictions,width,height,top_num=50):
ret_img_path = []
if os.path.exists("./abnormal.txt"):
with open("./abnormal.txt", 'r') as f:
items = f.readlines()
ret_img_path = [item.strip('\n') for item in items]
for index in range(len(img_path_list)):
img = image.load_img(img_path_list[index], target_size=(width, height))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
preds = model.predict(x)
dps = decode_predictions(preds, top = top_num)[0]
for i in range(len(dps)):
if (dps[i][0] in Dogs):
break;
elif (dps[i][0] in Cats):
break;
if i==len(dps)-1:
ret_img_path.append(img_path_list[index])
print(dps[i][0],"abnormal found!")
print("total {} jpgs found",len(ret_img_path))
with open("./abnormal.txt", 'w') as f:
for item in ret_img_path:
f.write("{}\n".format(item))
return ret_img_path
ImgRes = getErrorImg(train_img_list,model_res,resnet50.preprocess_input,resnet50.decode_predictions,224,224)
# In[16]:
imgVgg = getErrorImg(train_img_list,model_vgg,vgg19.preprocess_input,vgg19.decode_predictions,224,224)
# In[17]:
imgXce = getErrorImg(train_img_list,model_xce,xception.preprocess_input,xception.decode_predictions,299,299)
# In[28]:
def joinAbnormal_Imglist():
vgg_xce_union = list(set(imgVgg).union(set(imgXce)))
abnormal_v = list(set(ImgRes).union(set(vgg_xce_union)))
return abnormal_v
abnormal = joinAbnormal_Imglist()
print(len(abnormal))
display_img(abnormal, summary = False)
# # 删除异常值
# In[ ]:
for i in range(len(abnormal)):
os.remove(abnormal[i])
# #创建模型
# In[10]:
def IncepModel(width,height):
input_tensor = Input((height, width, 3))
x = input_tensor
x = Lambda(preprocess_input)(x)
base_model = InceptionV3(input_tensor=x, weights='imagenet', include_top=False)
model = Model(base_model.input, GlobalAveragePooling2D()(base_model.output))
return model
# #VGGModel,vgg模型特征层提取
# In[11]:
def VGGModel(width,height):
input_tensor = Input((height, width, 3))
x = input_tensor
x = Lambda(vgg19.preprocess_input)(x)
base_model = VGG19(input_tensor=x, weights='imagenet', include_top=False)
#base_model = VGG19(weights='imagenet', include_top=False)
model = Model(base_model.input, GlobalMaxPooling2D()(base_model.output))
return model
# Res50模型特征层提取
# In[12]:
def Res50Model():
base_model = ResNet50(weights='imagenet', include_top=False)
model = Model(base_model.input, GlobalAveragePooling2D()(base_model.output))
return model
# 导出模型特征文件,将训练数据和测试数据分别送入各模型,导出最后一层的输出向量结果,将最后一层连接;作为最终输出前的特征向量。
# 在最后一层利用sigmoid函数输出分类结果。
# In[13]:
WIDTH, HEIGHT = 299, 299 #fixed size for InceptionV3
BAT_SIZE = 32
filesplit(trainCwd = trainCwd)
if os.path.exists(os.path.join("./","IncepWeights.h5"))==False:
print("yes ,false!")
train_gen = creatDataGen(trainCwd,WIDTH,HEIGHT,BAT_SIZE)
test_gen = creatDataGen(testCwd,WIDTH,HEIGHT,BAT_SIZE)
model = IncepModel(WIDTH,HEIGHT)
train = model.predict_generator(train_gen)
test = model.predict_generator(test_gen)
with h5py.File("IncepWeights.h5") as h:
h.create_dataset("train", data=train)
h.create_dataset("test", data=test)
h.create_dataset("label", data=train_gen.classes)
if os.path.exists(os.path.join("./","VGG19Weights.h5"))==False:
train_gen2 = creatDataGen(trainCwd,224,224,BAT_SIZE)
test_gen2 = creatDataGen(testCwd,224,224,BAT_SIZE)
model2 = VGGModel()
train2 = model2.predict_generator(train_gen2)
test2 = model2.predict_generator(test_gen2)
with h5py.File("VGG19Weights.h5") as h:
h.create_dataset("train", data=train2)
h.create_dataset("test", data=test2)
h.create_dataset("label", data=train_gen2.classes)
print("VGGweights done!")
if os.path.exists(os.path.join("./","Res50Weights.h5"))==False:
model3 = Res50Model()
train3 = model3.predict_generator(train_gen2)
test3 = model3.predict_generator(test_gen2)
with h5py.File("Res50Weights.h5") as h:
h.create_dataset("train", data=train3)
h.create_dataset("test", data=test3)
h.create_dataset("label", data=train_gen2.classes)
print("Res50weights done!")
# In[14]:
from sklearn.utils import shuffle
def bulid_Input(filename='Res50Weights.h5',base_net_num=1):
np.random.seed(2017)
X_train = []
X_test = []
y_train = []
if base_net_num == 3:
for filename in ["Res50Weights.h5", "VGG19Weights.h5", "IncepWeights.h5"]:
with h5py.File(filename, 'r') as h:
X_train.append(np.array(h['train']))
X_test.append(np.array(h['test']))
y_train = np.array(h['label'])
X_train = np.concatenate(X_train, axis=1)
X_test = np.concatenate(X_test, axis=1)
elif base_net_num ==1:
with h5py.File(filename,'r') as h:
X_train.append(np.array(h['train']))
X_test.append(np.array(h['test']))
y_train = np.array(h['label'])
X_train = np.concatenate(X_train, axis=1)
X_test = np.concatenate(X_test, axis=1)
X_train,y_train = shuffle(X_train,y_train)
return X_train,X_test,y_train
# In[15]:
#X_train,X_test,y_train = bulid_Input("VGG19Weights.h5",base_net_num =1)
#X_train,X_test,y_train = bulid_Input("Res50Weights.h5",base_net_num =1)
#X_train,X_test,y_train = bulid_Input("IncepWeights.h5",base_net_num =1)
X_train,X_test,y_train = bulid_Input(base_net_num =3)
input_tensor = Input(X_train.shape[1:])
x = input_tensor
x = Dropout(0.5)(x)
x = Dense(60,activation = "relu")(x)
x = Dense(1, activation='sigmoid')(x)
model = Model(input_tensor, x)
model.summary()
model.compile(optimizer='adadelta',
loss='binary_crossentropy',
metrics=['accuracy'])
# In[16]:
from keras.callbacks import Callback
class LossHistory(Callback):
def on_train_begin(self, logs={}):
self.losses = {'batch':[], 'epoch':[]}
self.accuracy = {'batch':[], 'epoch':[]}
self.val_loss = {'batch':[], 'epoch':[]}
self.val_acc = {'batch':[], 'epoch':[]}
def on_batch_end(self, batch, logs={}):
self.losses['batch'].append(logs.get('loss'))
self.accuracy['batch'].append(logs.get('acc'))
self.val_loss['batch'].append(logs.get('val_loss'))
self.val_acc['batch'].append(logs.get('val_acc'))
def on_epoch_end(self, batch, logs={}):
self.losses['epoch'].append(logs.get('loss'))
self.accuracy['epoch'].append(logs.get('acc'))
self.val_loss['epoch'].append(logs.get('val_loss'))
self.val_acc['epoch'].append(logs.get('val_acc'))
def loss_plot(self, loss_type):
iters = range(len(self.losses[loss_type]))
plt.figure()
# acc
plt.plot(iters, self.accuracy[loss_type], 'r', label='train acc')
# loss
plt.plot(iters, self.losses[loss_type], 'g', label='train loss')
if loss_type == 'epoch':
# val_acc
plt.plot(iters, self.val_acc[loss_type], 'b', label='val acc')
# val_loss
plt.plot(iters, self.val_loss[loss_type], 'k', label='val loss')
plt.grid(True)
plt.xlabel(loss_type)
plt.ylabel('acc-loss')
plt.legend(loc="upper right")
plt.show()
# In[17]:
from keras.callbacks import TensorBoard
from keras.callbacks import EarlyStopping
history = LossHistory()
early_stopping = EarlyStopping(monitor='val_loss', patience=10, verbose=2)
model.fit(X_train, y_train, batch_size=96, epochs=50, validation_split=0.2,
callbacks=[TensorBoard(log_dir='./temp/log/union'),early_stopping,history])
model.save('model.h5',overwrite=True)
y_pred = model.predict(X_test, verbose=1)
y_pred = y_pred.clip(min=0.005, max=0.995)
# In[18]:
history.loss_plot('epoch')
# # 验证模型
# In[19]:
def build_test_tensor(img):
'''建立三个模型的输入向量
'''
y=[]
img = img.resize((299,299))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = inception_v3.preprocess_input(x)
model = IncepModel(299,299)
incPre = model.predict(x)
print(incPre.shape)
img = img.resize((224,224))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
vgg_x =vgg19.preprocess_input(x)
model2 = VGGModel(224,224)
vggPre = model2.predict(vgg_x)
print(vggPre.shape)
model3 = Res50Model()
resPre = model3.predict(x)
print(resPre.shape)
y.append(np.array(incPre))
y.append(np.array(vggPre))
y.append(np.array(resPre))
y = np.concatenate(y, axis=1)
print(y.shape)
out = y.reshape(4608,)
print(out.shape)
return out
# In[ ]:
test_img_list=[]
get_image_list(testCwd, test_img_list)
def display_pred_img(img_list,model):
for img_path in img_list:
img = Image.open(img_path)
x = build_test_tensor(img)
x = np.expand_dims(x, axis=0)
pred = model.predict(x, verbose=1)
print(pred)
pred = pred.clip(min=0.005, max=0.995)
print(pred)
plt.figure("Image") # 图像窗口名称
plt.imshow(img)
plt.axis('on') # 关掉坐标轴为 off
if pred[0][0]>0.5:
x_title = '{} : {:.2f}% is dog'.format(os.path.basename(img_path), pred[0][0] * 100)
plt.title(x_title) # 图像题目
else:
x_title = '{} : {:.2f}% is cat'.format(os.path.basename(img_path), (1-pred[0][0]) * 100)
plt.title(x_title) # 图像题目
plt.show()
#model = load_model("model.h5")
dis_pred_img_list = test_img_list[:10] + test_img_list[-10:]
display_pred_img(dis_pred_img_list,model)
# 将预测结果写入pred.csv。
# In[31]:
import pandas as pd
from keras.preprocessing.image import *
df = pd.DataFrame({"ID":np.arange(12500)+1,"label":-1.0})
image_size = (224, 224)
gen = ImageDataGenerator()
test_generator = gen.flow_from_directory("test1", image_size, shuffle=False,batch_size=32, class_mode=None)
for i, fname in enumerate(test_generator.filenames):
index = int(fname[fname.rfind('/')+1:fname.rfind('.')])
df.at[index-1, 'label'] = y_pred[i]
df.to_csv('inc_pred.csv', index=None)
df.head()
# In[ ]:
正式报告:
毕业报告
猫狗大战项目(dogs vs cats)主要是解决计算机识别猫和狗的问题。给定了一个中等规模的数据集,数据中通过文件名标定了猫和狗的标签,同时包含了一些干扰和异常数据,要求开发一个模型或者识别程序,能够从数据集中学习,而后对测试数据进行区分,最终以模型在测试数据上的识别准确率来衡量模型性能。
项目来自于kaggle比赛,相关数据集下载自kaggle。该项目本质是一个图片二分类问题,多年以来,传统的编程方法对于图片分类一直没有好的方法;近年来,随着计算机的飞速发展,深度学习的方法大放异彩,在计算机视觉领域取得了飞速进展,利用多层神经网络学习一定的数据量后,模型可具备较高的性能,在某些应用领域甚至超越了人类。
本项目利用keras封装后的tensorflow接口,搭建一个深度学习网络模型,利用kaggle比赛《Dogs vs. Cats Redux: Kernels Edition》中的数据集对模型进行训练、优化,利用优化后的模型对未曾见过的猫狗图片(test数据集)进行分类。
该项目本质是一个图像分类问题。采用深度学习的方法,构建一个多层的神经网络模型,通过训练数据的学习,使得模型能够区分猫和狗的特征,进而识别猫和狗。图像处理和特征提取,不可避免要用到卷积神经网络CNN,借鉴成熟的VGG、Inception、Resnet等著名的神经网络构建技巧,构建一个卷积神经网络,不断调整其参数,通过多代次的训练,最后达到项目要求。
模型的评估指标选择:
识别正确率: 对于给定的测试数据集,分类器正确分类的样本数与总样本数之比。其中识别正确率越高,模型性能越好。
模型的损失函数选择:
平均交叉熵:模型在测试数据集样本上的归属概率输出与真实数据样本归属的差异程度。平均交叉熵越小,模型性能越好。计算公式如下所示:
其中y:图片为狗时值为1,否则为0;
a:为神经网络实际输出即模型判断一张图片是狗的概率;
n:为测试集中图片的个数。
当logloss较小时,模型表现能力强,正确预测猫狗图片的能力强;当logloss较大时,模型表现能力差,正确预测猫狗图片的能力弱。
模型的优化方向即是使交叉熵输出最小。
主要是在数据预处理阶段和模型构建阶段,需要对相关算法予以整理和思考。基于keras和tensorflow的灵活性,解决该问题可能存在多种方法,需要在实际过程中根据自身掌握情况进行权衡和选择。
问题解决整体思路如下所示:
图 1 总体解决思路
在实施过程中:
Cats vs. Dogs(猫狗大战)是Kaggle大数据竞赛的一道赛题,利用给定的数据集,用算法实现猫和狗的识别。 Kaggle提供的数据集包含了猫和狗图片各12500幅,都是以cat.<数字>.jpg或dog.<数字>.jpg命名,因此可以根据文件名分类打标签。
数据集链接:https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/data
数据集包括两部分:train数据集和test数据集。数据集由训练数据和测试数据组成,训练数据包含猫和狗各12500张图片,各占比例50%。测试数据包含12500张猫和狗的图片,具体比例未做统计。训练数据分布如下所示:
图 2 训练数据集分布
训练数据集与测试数据集大小分布如下所示:
图 3训练数据集与测试数据集分布
图片的长-宽scatter分布图如下所示:
图 4 训练数据长宽分布散点图
图像的拍摄角度各异,但基本都是以猫或狗为主体,通过数据增强,如调整亮度、随机裁切、轻度旋转等处理,可进一步增大样本量,使模型有更多的数据样本进行训练,但在本次项目中并未利用数据增强,因为未采用数据增强的模型测试结果已经可以达到要求了。
图像的大小不同,尺寸都在500*500以内,但大小各异,送入模型进行学习前,需要做数据预处理,裁剪成为大小一致的图片,如采用不同的预训练模型时,其对图像的大小有不同的要求。InceptionV3要求图像数据大小为299*299;Vgg19及Resnet50的图像数据大小为224*224;keras库中导入相应模型时,可以同时导入相应的preprocess_input函数,用于图片的预处理。
存在个别异常值,如dog.1895.jpg等,并非拍摄照片;如下所示:
图 5 异常值示例照片1
如dog.6405.jpg(如下所示),在训练中,会干扰模型的迭代。
图 6 异常值示例照片2
模型数据集总体上难度不高,虽然存在个别异常值但主体清晰,分辨率较高。对于异常值应当进行删除。
从数据集长-宽散点图来看,图片大部分长款分布在500*500以内,存在个别异常值,经查看本地数据集,发现这两张图片分别为cat.835.jpg和dog.2317.jpg,并非异常值。如下所示:
图 7 cat.835.jpg和dog.2317.jpg
ImageNet数据集中包含有猫狗的具体分类,对一个图片在载有ImageNet上预训练权值的Inception\VGG19\Resnet50模型上进行预测,如果其预测结果top50不包含猫狗真实的标签分类(图片预测值前50都没有正常分类),那么就将其视为异常值;最终异常值取值为上述三个模型检测出异常值的并集。而后再对并集做手动检测处理。
将原始数据集转换为图片数据和标签数据,供模型使用。数据如何送入模型,对于tensorflow有3种方法:
1、采用TFRecord的方法,将数据集和标签转换为TFRecord,由tensorflow控制数据的流向和吞吐;该方法编码难度较高;
2、直接使用数据和标签feed模型;该方法效率较低,频繁的读写磁盘将极大的拖慢训练效率;
3、采用pickle文件的方法,将所有数据读入numpy array中,再写入到pickle文件里,在使用时,再从pickle文件中读出;该方法效率较高,但对内存要求大;
Keras有两种方法可以参考:
1、采用numpy array的方法。将图片直接读入numpy array中,利用keras的fit函数送入模型训练;
2、利用ImageDataGenerator函数,先将图片分类(分成cat和dog文件夹,分存cat和dog图片),创建train_generator、test_generator;而后利用flow_from_directory,fit_generator函数实现。
在实际应用中,利用keras库结合了1、2两种方法,先利用ImagedataGenerator读入图片,经过模型的特征提取层后,将最后一层的输出向量连接,然后利用numpy array的shuffle方法处理,最后利用fit函数送入模型。因为,如果仅仅采用第二种方法,那么必须将训练集再拆分出一个验证集文件夹,从而创建validation_generator来送入模型,而这实际并不是必要步骤。
将train文件夹中的cat图片(前12500张)和dog图片利用pthon的shutil库函数,分别拷贝进入两个不同的文件夹中(cat及dog),为了便于后期利用keras的flow_from_directory函数送入模型。
对于训练集和验证集的划分,有两种方法:
第一种方法对内存要求高,第二种方法对内存要求低一些。实际选用第二种。
图像预处理既可以手动逐个处理,也可以利用导入模型的preprocess_input函数进行处理。包括手动和自动两种方法:
手动处理:利用PIL库或CV2库的图像处理函数,对图片逐一进行尺寸修正处理,如需数据增强,则进一步进行翻转、裁切、旋转处理,丰富数据样本;
自动处理:利用模型的preprocess_input函数对图像进行预处理;或者利用ImageDataGenerator的flow_from_directory()函数改变图像尺寸。
在编码过程中,数据预处理采用模型自带的preprocess_input函数进行;inceptionV3需要处理为299*299大小, 同时对所有的图片文件还应对其RGB值进行归一化处理。VGG19及Res50需要处理为224*224大小,利用ImageDataGenerator的flow_from_directory()函数改变图像尺寸即可。
在实施过程中,处理过程主要包括以下几个主要步骤:
对图片进行大小修正时,vgg19和Res50模型利用的是在flow_from_directory函数中传入图片尺寸,在该函数中完成对图片的统一大小;对于InceptionV3和vgg19模型,由于模型需要对图片数据还需进行归一化或减均值处理,因此需利用模型的preprocess_input函数;
卷积神经网络最初是为解决图像识别等问题而设计的,当前不仅应用于图像和视频,也可用于时间序列信号。在卷积神经网络中,第一个卷积层会直接接受图像像素级的输入,每一个卷积操作只处理一小块图像,进行卷积变化后再传到后面的网络。每一层卷积都会提取数据中最有效的特征。一般的卷积神经网络由多个卷积层构成,每个卷积层会进行如下几个操作。
卷积层最关键的特征如下,局部连接、权值共享和池化采样。如下所示:
局部连接:CNN通过加强神经网络中相邻层之间节点的局部连接模式来挖掘自然图像的空间局部关联信息,第m层节点的输入是第m-1层节点的一部分,这些节点具有空间相邻的视觉感受野。卷积神经网络中每个神经元的权重个数均为卷积核的大小,即每个神经元只与图片部分像素相连接。
权值共享:一个卷积层可以有多个不同的卷积核,而每个卷积核都对应个滤波后映射出的新图像,同一个新图像中每一个像素都来自相同的卷积核,这就是卷积核的权值共享,用以降低模型复杂度,同时赋予了卷积网络对平移的容忍性。
池化采样:池化层主要是针对卷积后的特征图,按照卷积的方法对图像部分区域求均值或最大值,用来代表其采样的区域。这是为了描述大的图像,对不同位置的特征进行聚合统计,这个均值或者最大值就是聚合统计的方法,也就是池化。
池化采样进一步降低了输出参数量,并赋予模型对于轻度形变的容忍能力,提高了模型的泛化性能。
另外,一个CNN模型一般还包括:
全连接层:两层之间所有的神经元都有连接,也就是FC层。
Dropout:训练时,使用Dropout随机忽略一部分神经元,以避免模型过拟合。一般在模型的全连接层使用dropout,增强了模型的健壮性。
重叠的池化:在CNN中,池化步长一般比池化核尺寸小,这样池化层的输出之间会有重叠和覆盖,提升了特征的丰富性。
近年来,CNN取得了飞速进展,下图展示了CNN的架构演化过程:
图 8 CNN架构演化过程
利用迁移学习,我们可以利用已经存在的相关人物或域的有标记数据去处理新的任务场景,在源问题域中,通过解决源任务所获得的知识,将其用于新的另外的任务。
本项目所用数据集实际是ImageNet数据集的子集,并且是一个二分类问题,因此,问题域实际是ImageNet分类域的子域问题。鉴于传统的经典神经网络均已在ImageNet上训练过,并获得了较好的分类效果,可以利用已训练网络的权值,进行特征提取,模型的最后加入全新的全连接层和分类层,输出分类结果。考虑到多个模型的特征提取结果并不相同,融合多个模型的特征提取结果能够让数据特征更加丰富,因此,可以结合若干成熟模型进行迁移学习。
最终实现过程中,我将vgg19、Resnet50、InceptionV3三类卷积网络的卷积层对图片进行特征提取,并将特征合并,最后通过1个全连接FC层(利用Relu进行激活),最后输出层连接sigmoid激活函数输出分类结果。如下图所示。
图 9模型融合结构图
优化器用来更新和计算模型参数,使其更加逼近或者达到最优值,从而使loss 损失函数最小。 神经网络中最常用优化算法是梯度下降,其核心是:对于每一个变量,按照目标函数在该变量的梯度下降的方向(梯度的反方向)进行更新,学习率决定了每次更新的步长。即在超平面上目标函数沿着斜率下降的方向前进,直到到达超平面的谷底。梯度下降法包括:
1、批量梯度下降(Batch GradientDescent):在整个数据集上对每个参数求目标函数的偏导数,其反方向即为此参数变量的梯度下降方向。批量梯度下降中,每次更新都需要计算整个数据集上求出所有参数变量的偏导数,因此速度比较慢。批量梯度下降对于凸函数可以收敛到全局最小值,对于非凸函数可以收敛到局部最小值。
2、随机梯度下降(Stochastic GradientDescent):相对于批量梯度下降,随机梯度下降每次更新是针对数据集中的一个样本求损失函数,然后对其求相应的偏导数,SGD运行速度大大加快。SGD更新值的方差很大,在频繁的更新之下,目标函数会有剧烈的波动。当降低学习率的时候,SGD 表现出了与批量梯度下降相似的过程。
3、小批量梯度下降法(Mini-batch GradientDescent):在每次更新中,对n 个样本构成的一批数据,计算损失函数,并对相应的参数求导;这种算法降低了参数的方差,使得收敛过程更稳定。小批量梯度下降法,通常使我们训练神经网络的首先算法。
优化的梯度下降学习算法包括:
1、动量法:SGD很难在陡谷(ravines)中找到正确更新方向,SGD在陡谷周围震荡想局部极值处缓慢前进。动量法,就像从高坡推下一个小球,小球在滚动过程中积累了动量,在途中他变得越来越快(直到达到峰值速度)。算法中,参数的更新也是如此,动量项在梯度指向方向相同的方向逐渐增大,对梯度指向改变的方向逐渐减小。由此,将会加快收敛以及减小震荡。
2、Adagrad法:主要功能是,对不同的参数调整学习率,对低频出现的参数进行大的更新,对高频出现的学习率进行小的更新。Adagrad 法大大提升了 SGD的鲁棒性。Adagrad主要优势之一是它不需要对每个学习率进行手工调节。 劣势在于, Adagrad会导致学习率不断的缩小,并最终变为一个无限小值,算法将不能从数据中学到额外的信息。
3、Adadelta、RMSprop:adagrad的改进,解决学习率不断单调下降的问题。
4、适应性动量估计法(Adam):另一种对不同参数计算适应性学习率的方法。除了存储类似于Adadelta法和RMSprop中指数衰减的过去梯度平方均值外,Adam法也存储像动量法中的指数衰减的过去梯度值均值。
上述优化算法,keras中已显性支持,实际训练时,采用SGD优化器或Adam优化器进行优化,采用binary_crossentropy作为代价函数计算logloss分数,评估指标采用对狗的识别准确率。
基准模型采用vgg19,在输入层和隐藏层完全采用vgg19的结构,在最后的输出层改变softmax的激活函数,利用sigmoid函数输出二分类即可。
图 10 VGG模型结构图
上图全程采用3*3的卷积核。采用多个卷积层与非线性的Relu层交替的结构,比单一卷积层的结构更能提取出深层的更好的特征。采用了5个max-pool层,将卷积划分为5个阶段,每个阶段增长1倍,最后到512个。网络权值w和b采用随机初始化的方式进行。最后采用sigmoid激活函数输出。
Google inceptionnet首次出现在ILSVRC2014的比赛中,以较大优势取得第一名。在控制了参数量和计算量的同时,获得了非常好的分类性能,top-5错误率仅为6.67%。详细的网络结构及其子网络结构如下。
图 11 inceptionV3网络架构图
相比于之前的inception网络架构,V3的改进主要在于:
图 12将一个3*3的二维卷积拆分为3*1和1*3的两个1维卷积
2、优化了Inception module的结构,现在的Inception module有35*35、17*17和8*8三种不同结构,这些module只在网络的后部出现,前部还是普通的卷积层,并且在分支中使用了分支结构。如下所示。
图 13三种结构的Inception module
ResNet有2个基本的block,一个是Identity Block,输入和输出的dimension是一样的,所以可以串联多个;另外一个基本block是Conv Block,输入和输出的dimension是不一样的,所以不能连续串联,它的作用本来就是为了改变feature vector的dimension。因为CNN最后都是要把image一点点的convert成很小但是depth很深的feature map,一般的套路是用统一的比较小的kernel(比如VGG都是用3*3),但是随着网络深度的增加,output的channel也增大(学到的东西越来越复杂),所以有必要在进入Identity Block之前,用Conv Block转换一下维度,这样后面就可以连续接Identity Block。总体结构如下所示:
图 14 Resnet50总体结构图
可以看下Conv Block是怎么改变输出维度的,分支结构如下所示。
图 15 Resnet50分支结构
其实就是在shortcut path的地方加上一个conv2D layer(1*1 filter size),然后在main path改变dimension,并与shortcut path对应起来。
我们期望训练后的模型在测试集上的得分表现 score 可以达到 kaggle 排行榜前 10%,即是在 Public Leaderboard 上的 logloss 低于 0.06127。
利用vgg19、Res50、InceptionV3模型进行模型融合,利用迁移学习,将特征提取后再连接,最后加入一个全连接层和一个输出层给出预测结果。直接利用上述模型在imagenet上的预训练权重,取全连接层前的特征输出作为后续模型的输入。模型架构如下:
代码如下所示:
图 16 模型构建关键代码
Keras的便利使得模型在训练完1代后,会自动在验证数据集上进行验证,输出验证logloss损失和准确率。如下所示:
图 17 模型验证输出
训练过程中,发现改变epoch、batch-size的效果很明显,对训练速度有较大影响,但dropout比率、最后一个FC层的节点数目对模型的准确率影响很大。
对于给定的测试数据集,包含12500张猫狗图片,要求将测试结束按照指定的csv格式输出并提交。
代码如下所示。
图 18 测试结果输出代码
为了使用keras的imagedatagenerator函数,将文件存放结构如下所示:
函数代码如下:
数据读入利用keras的ImageDataGenerator产生数据生成器,同时改变图像尺寸。代码如下:
异常值检测主要使用VGG19、xception、Resnet模型在Imagenet上的预训练结果,对train数据集进行检测,检测其图片类别属于dog或者cat的概率。一般取top-30或者top-50的预测结果,如果前30类或者前50类均未预测出dog或者cat,则将其归为异常值。取3类模型的异常值预测结果的并集,而后再手动检测。其中vgg模型检出93张,xception模型检出34张,Resnet50模型检出45张。取并集后,异常数据为94张。从目视检测结果来看,有部分是模型错误,但为了提高模型的准确率避免对模型产生干扰,可以删除。
如下所示:
图 19 异常数据缩略图
选择VGG19、Resnet、inceptionV3三个预训练模型作为融合来源,由于VGG19要求输入减去均值,InceptionV3要求进行归一化处理,因此,对于VGG19模型和InceptionV3模型分别调用其preprocess_input函数进行处理。如下所示:
训练集验证集划分直接利用keras的model.fit函数中的validation_split参数进行设置验证集占比,代码如下:
model.fit(X_train, y_train, batch_size=48, epochs=50, validation_split=0.2)
单独利用vgg模型进行特征提取,而后加上FC层和输出层,训练50个epoch后,结果如下:
模型训练结果如下所示:
图 20模型输出结果
TrainingAccuracy 及Validation Accuracy随训练epoch的变化曲线,如下所示:
图 21 TrainingAccuracy 及Validation Accuracy随训练epoch的变化曲线
Training loss 及Validation loss随训练epoch的变化曲线,如下所示:
图 22 Training loss 及Validation loss随训练epoch的变化曲线
代码段如下所示:
图 23模型部分代码
模型训练在第16个epoch停止,输出结果如下所示:
图 24模型输出结果
TrainingAccuracy 及Validation Accuracy随训练epoch的变化曲线,如下所示:
图 25 TrainingAccuracy 及Validation Accuracy随训练epoch的变化曲线
Training loss 及Validation loss随训练epoch的变化曲线,如下所示:
图 26 Training loss 及Validation loss随训练epoch的变化曲线
使用该模型进行训练时,设置了模型的early_stopping,模型在训练到第24个epoch时停止了训练。代码如下所示:
图 27 early-stopping代码列表
模型输出结果如下所示:
图 28 模型输出结果
TrainingAccuracy 及Validation Accuracy随训练epoch的变化曲线,如下所示:
图 29 TrainingAccuracy 及Validation Accuracy随训练epoch的变化曲线
Training loss 及Validation loss随训练epoch的变化曲线,如下所示:
图 30 Training loss 及Validation loss随训练epoch的变化曲线
融合模型结构:
图 31模型summary
模型结果如下所示:
图 32模型训练结果
TrainingAccuracy 及Validation Accuracy随训练epoch的变化曲线,如下所示:
图 33 TrainingAccuracy 及Validation Accuracy随训练epoch的变化曲线
Training loss 及Validation loss随训练epoch的变化曲线,如下所示:
图 34 Training loss 及Validation loss随训练epoch的变化曲线
Acc-loss随epoch的变化曲线如下所示:
图 35 ACC-Loss 随epoch变化曲线
Keras在训练过程中,能够显式的生成log日志,使用tensorflow的tensorboard来解析这个日志,并通过网页的形式展现出来。Tensorboard会自动绘制ACC和Loss曲线,通过下列曲线,可以观察到模型的训练进展。如下为性能对比曲线颜色图示。
图 36曲线图示
TrainingAccuracy 及Validation Accuracy随训练epoch的变化曲线,如下所示:
图 37训练集正确率及验证集正确率随epoch变化曲线对照
Training loss 及Validation loss随训练epoch的变化曲线,如下所示:
图 38训练集loss及验证集loss随epoch变化曲线对照
可以见到,在本项目数据集支持下,根据验证集上的loss分数,各模型性能排序如下:
InceptionV3模型在正确率上较融合模型及其他模型在前期有一定优势,在损失上又相较其他模型更小,但在最终测试结果上logloss分数排名(越小越好)如下:
得益于结合了优秀的vgg、resnet、inceptionV3三个经典图像分类网络的优势,最终的模型性能达到了预期。训练50个epoch后,模型在验证集上的logloss分数达到了0.0286,准确率达到了0.9924。如下所示。
图 39 训练50个epoch后的模型输出信息
在测试集上,模型的logloss达到了0.04259,准确率未知,达到了预期目的。相比于基准模型结果,该模型大大降低了logloss损失,达到了kaggle的前10%,达到0.04259,小于0.06127阈值。最终模型的在测试集上的成绩达到了kaggle的22名左右。如下图所示。
图 40 kaggle提交情况示意
图 41 kaggle猫狗大战比赛排名情况
从测试数据中抽取部分图片,测试后,部分结果如下所示:
图 42部分图片预测结果
从肉眼常识来看,模型的准确率还是比较高的,有一定的鲁棒性。
至少有两种路径还值得进一步尝试:
在项目实施过程中,印象深刻的几点:
1、Keras大大简化了代码,实现了对tensorflow等机器学习框架的很好的封装。
2、Tensorflow 的TFRecord方式及keras的ImageGenerator库哪种方式供给数据的效率更高?显然第一种的便利性较差,第二种实施更为方便快捷。但运行效率呢?
3、任何机器学习问题,都免不了数据预处理、模型构建、模型训练、测试与验证等步骤;但预处理与模型调参可能会占用大部分时间和精力,数据组织方式也会对模型有较大影响。