P7毕业项目,猫狗大战。详解,含全部代码

准备工作:

1、申请一个aws云机器,因为本地训练实在太慢了,当然,土豪请略过……

2、申请一个kaggle账号,数据集可以利用winscp上传到云端,但上传速度过慢,建议利用kaggle-cli下载数据集到aws云端(申请kgggle账号时注册码无法显示,你需要下载一个插件,用chrome浏览器。)

3、利用putty连接到云端后,启用tensorflow_p36的虚拟环境,升级并安装相关的包,比如sklearn,opencv等等;

4、下载数据集到云端:

kg download -u <username> -p <password> -c dogs-vs-cats-redux-kernels-edition -f <filename>

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[ ]:




正式报告:

毕业报告

  1. 问题定义
    1. 项目概览

猫狗大战项目(dogs vs cats)主要是解决计算机识别猫和狗的问题。给定了一个中等规模的数据集,数据中通过文件名标定了猫和狗的标签,同时包含了一些干扰和异常数据,要求开发一个模型或者识别程序,能够从数据集中学习,而后对测试数据进行区分,最终以模型在测试数据上的识别准确率来衡量模型性能。

项目来自于kaggle比赛,相关数据集下载自kaggle。该项目本质是一个图片二分类问题,多年以来,传统的编程方法对于图片分类一直没有好的方法;近年来,随着计算机的飞速发展,深度学习的方法大放异彩,在计算机视觉领域取得了飞速进展,利用多层神经网络学习一定的数据量后,模型可具备较高的性能,在某些应用领域甚至超越了人类。

本项目利用keras封装后的tensorflow接口,搭建一个深度学习网络模型,利用kaggle比赛《Dogs vs. Cats Redux: Kernels Edition》中的数据集对模型进行训练、优化,利用优化后的模型对未曾见过的猫狗图片(test数据集)进行分类。

    1. 问题说明

该项目本质是一个图像分类问题。采用深度学习的方法,构建一个多层的神经网络模型,通过训练数据的学习,使得模型能够区分猫和狗的特征,进而识别猫和狗。图像处理和特征提取,不可避免要用到卷积神经网络CNN,借鉴成熟的VGG、Inception、Resnet等著名的神经网络构建技巧,构建一个卷积神经网络,不断调整其参数,通过多代次的训练,最后达到项目要求。

    1. 评估指标讨论

模型的评估指标选择:

识别正确率: 对于给定的测试数据集,分类器正确分类的样本数与总样本数之比。其中识别正确率越高,模型性能越好。

模型的损失函数选择:

平均交叉熵:模型在测试数据集样本上的归属概率输出与真实数据样本归属的差异程度。平均交叉熵越小,模型性能越好。计算公式如下所示:

其中y:图片为狗时值为1,否则为0;

a为神经网络实际输出即模型判断一张图片是狗的概率;

n测试集中图片的个数

当logloss较小时,模型表现能力强,正确预测猫狗图片的能力强;当logloss较大时,模型表现能力差,正确预测猫狗图片的能力弱。

模型的优化方向即是使交叉熵输出最小。

  1. 问题分析与解决
    1. 总体思路

主要数据预处理阶段和模型构建阶段需要对相关算法予以整理和思考。基于kerastensorflow灵活性,解决该问题可能存在多种方法,需要在实际过程中根据自身掌握情况进行权衡和选择。

问题解决整体思路如下所示:

1 总体解决思路

在实施过程中:

  1. 上传数据集。初始方案是从本机上传到云端,但速度太慢,不得已放弃,后来采用的kaggle-cli库直接下载到AWS云端;
  2. 图片进行复制操作。初始方案是利用winscp对文件进行复制操作,后因速度太慢放弃;于是写了一个filesplit()函数直接代码处理,效率大大提高。
    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

模型数据集总体上难度不高,虽然存在个别异常值但主体清晰,分辨率较高。对于异常值应当进行删除。

    1. 数据预处理
        1. 异常值检

从数据集-宽散点图来看,图片大部分长款分布在500*500以内,存在个别异常值,经查看本地数据集,发现两张图片分别为cat.835.jpg和dog.2317.jpg,并非异常值。如下所示:

7 cat.835.jpg和dog.2317.jpg

ImageNet数据集中包含有猫狗的具体分类,对一个图片在载有ImageNet上预训练权值的Inception\VGG19\Resnet50模型上进行预测,如果其预测结果top50不包含猫狗真实的标签分类(图片预测值前50都没有正常分类),那么就将其视为异常值最终异常值取值为上述三个模型检测出异常值的并集。而后再对并集做手动检测处理

        1. 数据送入模型

将原始数据集转换为图片数据和标签数据,供模型使用数据如何送入模型,对于tensorflow有3种方法:

1、采用TFRecord的方法,将数据集和标签转换为TFRecord,由tensorflow控制数据的流向和吞吐;该方法编码难度较高;

2、直接使用数据和标签feed模型;该方法效率较低,频繁的读写磁盘将极大的拖慢训练效率;

3、采用pickle文件的方法,将所有数据读入numpy array中,再写入到pickle文件里,在使用时,再从pickle文件中读出该方法效率较高,但对内存要求大;

Keras有两种方法可以参考:

1、采用numpy array的方法。将图片直接读入numpy array中,利用kerasfit函数送入模型训练;

2、利用ImageDataGenerator函数,先将图片分类(分成catdog文件夹,分存catdog图片),创建train_generator、test_generator;而后利用flow_from_directoryfit_generator函数实现。

在实际应用中,利用keras库结合了1、2两种方法,先利用ImagedataGenerator读入图片,经过模型的特征提取层后,将最后一层的输出向量连接,然后利用numpy array的shuffle方法处理,最后利用fit函数送入模型。因为,如果仅仅采用第二种方法,那么必须将训练集再拆分出一个验证集文件夹,从而创建validation_generator来送入模型,而这实际并不是必要步骤。

        1. 训练集验证集划分

train文件夹中的cat图片(前12500张)和dog图片利用pthon的shutil库函数,分别拷贝进入两个不同的文件夹中(catdog),为了便于后期利用kerasflow_from_directory函数送入模型。

对于训练集和验证集的划分,有两种方法:

  • 将所有的图像文件和标签读入numpyarray中,使用np.random.shuffle()打乱图片和标签;利用SKlearn的数据预处理模块中的train_test_split划分训练集和验证集。
  • 导出融合模型的特征向量时,将数据和特征同时进行shuffle处理,在模型训练时,在模型的fit函数中,利用validation_split参数设置训练集验证集划分比例。

第一种方法对内存要求高,第二种方法对内存要求低一些。实际选用第二种。

        1. 图像预处理

图像预处理既可以手动逐个处理,也可以利用导入模型的preprocess_input函数进行处理。包括手动和自动两种方法:

手动处理:利用PIL库CV2的图像处理函数,图片逐一进行尺寸修正处理,如需数据增强,则进一步进行翻转、裁切、旋转处理,丰富数据样本;

自动处理:利用模型的preprocess_input函数对图像进行预处理;或者利用ImageDataGeneratorflow_from_directory()函数改变图像尺寸

在编码过程中,数据预处理采用模型自带的preprocess_input函数进行;inceptionV3需要处理为299*299大小, 同时对所有的图片文件还应对其RGB值进行归一化处理。VGG19Res50需要处理为224*224大小,利用ImageDataGeneratorflow_from_directory()函数改变图像尺寸即可

在实施过程中,处理过程主要包括以下几个主要步骤:

  1. 将原始的train和test1文件夹中的图片利用shutil分别复制到相应的文件夹下,为了适应keras的flow_from_directory函数

图片进行大小修正时,vgg19Res50模型利用的是在flow_from_directory函数中传入图片尺寸,在该函数中完成图片的统一大小;对于InceptionV3vgg19模型,由于模型需要对图片数据还需进行归一化减均值处理,因此需利用模型的preprocess_input函数

    1. 模型构建
      1. 卷积神经网络

卷积神经网络最初是为解决图像识别等问题而设计的,当前应用于图像和视频,也可用于时间序列信号。在卷积神经网络中,第一个卷积层会直接接受图像像素的输入,每一个卷积操作只处理一小块图像,进行卷积变化后再传到后面的网络。每一层卷积都会提取数据中有效的特征。一般卷积神经网络由多个卷层构成,每个卷积层会进行如下几个操作。

  1. 图像通过多个不同的卷积核的滤波,并加上偏置,提取出局部特征,每一个卷积核会映射出一个新的2D图像。
  2. 前面卷积核的滤波输出结果,进行非线性的激活函数处理目前常见的Relu函数。
  3. 激活函数的结果再进行池化操作(即降采样目前常见的有最大池化平均池化。
  4. 以上三个步骤构成了一个最常见的卷积层。也可加上一个局部相应归一化层(LRN

卷积层最关键的特征如下,局部连接、权值共享和池化采样。如下所示:

局部连接:CNN通过加强神经网络中相邻层之间节点的局部连接模式来挖掘自然图像的空间局部关联信息,m层节点的输入第m-1节点的一部分,这些节点具有空间相邻的视觉感受野。卷积神经网络中每个神经元的权重个数均为卷积核的大小,即每个神经元只与图片部分像素相连接。

权值共享:一个卷积层可以有多个不同的卷积核,而每个卷积核都对应个滤波后映射出的新图像,同一个新图像中每一个像素都来自相同的卷积核,这就是卷积核的权值共享,用以降低模型复杂度同时赋予了卷积网络对平移的容忍性

池化采样:池化层主要是针对卷积后的特征图,按照卷积的方法对图像部分区域求均值或最大值,用来代表其采样的区域。这是为了描述大的图像,对不同位置的特征进行聚合统计,这个均值或者最大值就是聚合统计的方法,也就是池化。

池化采样进一步降低了输出参数量,并赋予模型对于轻度形变的容忍能力,提高了模型的泛性能。

另外,一个CNN模型一般还包括:

全连接层:两层之间所有的神经元都有连接,也就是FC层。

Dropout:训练时,使用Dropout随机忽略一部分神经元,以避免模型过拟合。一般模型的全连接层使用dropout,增强了模型的健壮性。

重叠的池化:在CNN中,池化步长一般比池化核尺寸小,这样池化层的输出之间会有重叠和覆盖,提升了特征的丰富性。

近年来,CNN取得了飞速进展,下图展示了CNN的架构演化过程:

8 CNN架构演化过程

      1. 迁移学习

利用迁移学习,我们可以利用已经存在的相关人物或域的有标记数据处理新的任务场景,在源问题域中,通过解决源任务所获得的知识将其用于新的另外的任务。

本项目所用数据集实际是ImageNet数据集的子集,并且是一个二分类问题,因此,问题域实际是ImageNet分类域的子问题。鉴于传统的经典神经网络均已在ImageNet上训练过,并获得了较好的分类效果,可以利用已训练网络的权值,进行特征提取,模型的最后加入全新的全连接层和分类层输出分类结果。考虑多个模型特征提取结果并不相同,融合多个模型的特征提取结果能够让数据特征更加丰富,因此,可以结合若干成熟模型进行迁移学习

最终实现过程中,我将vgg19Resnet50InceptionV3三类卷积网络的卷积层对图片进行特征提取,并将特征合并,最后通过1个全连接FC层(利用Relu进行激活),最后输出层连接sigmoid激活函数输出分类结果。下图所示。

9模型融合结构图

      1. 优化器

优化器用来更新和计算模型参数,使其更加逼近或者达到最优值,从而使loss 损失函数最小。 神经网络中最常用优化算法是梯度下降,其核心是:对于每一个变量,按照目标函数在该变量的梯度下降的方向(梯度的反方向)进行更新,学习率决定了每次更新的步长。即在超平面上目标函数沿着斜率下降的方向前进,直到到达超平面的谷底。梯度下降法包括:

1批量梯度下降(Batch GradientDescent):在整个数据集上对每个参数求目标函数的偏导数,其反方向即为此参数变量的梯度下降方向。批量梯度下降中,每次更新都需要计算整个数据集上求出所有参数变量的偏导数,因此速度比较慢。批量梯度下降对于凸函数可以收敛到全局最小值,对于非凸函数可以收敛到局部最小值。  

2、随机梯度下降(Stochastic GradientDescent):相对于批量梯度下降,随机梯度下降每次更新是针对数据集中的一个样本求损失函数,然后对其求相应的偏导数,SGD运行速度大大加快。SGD更新值的方差很大,在频繁的更新之下,目标函数会有剧烈的波动。当降低学习率的时候,SGD 表现出了与批量梯度下降相似的过程。  

3小批量梯度下降法(Mini-batch GradientDescent):在每次更新中,对n 个样本构成的一批数据,计算损失函数,并对相应的参数求导;这种算法降低了参数的方差,使得收敛过程更稳定。小批量梯度下降法,通常使我们训练神经网络的首先算法。

优化梯度下降学习算法包括:

1动量法:SGD很难在陡谷(ravines)中找到正确更新方向,SGD在陡谷周围震荡想局部极值处缓慢前进。动量法,就像从高坡推下一个小球,小球在滚动过程中积累了动量,在途中他变得越来越快(直到达到峰值速度)。算法中,参数的更新也是如此,动量项在梯度指向方向相同的方向逐渐增大,对梯度指向改变的方向逐渐减小。由此,将会加快收敛以及减小震荡。 

2Adagrad法:主要功能是,对不同的参数调整学习率,对低频出现的参数进行大的更新,对高频出现的学习率进行小的更新。Adagrad 法大大提升了 SGD的鲁棒性。Adagrad主要优势之一是它不需要对每个学习率进行手工调节。 劣势在于, Adagrad会导致学习率不断的缩小,并最终变为一个无限小值,算法将不能从数据中学到额外的信息。 

3Adadelta、RMSprop:adagrad的改进,解决学习率不断单调下降的问题。

4适应性动量估计法(Adam):另一种对不同参数计算适应性学习率的方法。除了存储类似于Adadelta法和RMSprop中指数衰减的过去梯度平方均值外,Adam法也存储像动量法中的指数衰减的过去梯度值均值。

上述优化算法,keras中已显性支持,实际训练时,采用SGD优化器或Adam优化器进行优化,采用binary_crossentropy作为代价函数计算logloss分数,评估指标采用对狗识别准确率。

      1. 经典神经网络结构
        1. VGG19结构

基准模型采用vgg19,在输入层和隐藏层完全采用vgg19结构,在最后的输出层改变softmax的激活函数,利用sigmoid函数输出二分类即可。

10 VGG模型结构图

上图全程采用3*3的卷积核。采用多个卷积层与非线性的Relu层交替的结构,比单一卷积层的结构更能提取出深层的更好的特征。采用了5max-pool层,将卷积划分为5个阶段,每个阶段增长1倍,最后到512个。网络权值wb采用随机初始化的方式进行。最后采用sigmoid激活函数输出。

        1. InceptionV3架构

Google inceptionnet首次出现在ILSVRC2014的比赛中,以较大优势取得第一名。在控制了参数量和计算量的同时,获得了非常好的分类性能,top-5错误率仅为6.67%。详细的网络结构及其子网络结构如下。

11 inceptionV3网络架构图

相比于之前的inception网络架构,V3的改进主要在于:

  1. 将一个较大的二维卷积拆分为两个较小的一维卷积,如将5*5的卷积核更改为5*1,1*5的两个卷积核,一方面节约了大量参数,加速运算并减轻了过拟合,同时增加了一层非线性扩展模型表达能力。如下所示。

12将一个3*3的二维卷积拆分为3*1和1*3的两个1维卷积

2、优化了Inception module的结构,现在的Inception module有35*35、17*17和8*8三种不同结构,这些module只在网络的后部出现,前部还是普通的卷积层,并且在分支中使用了分支结构。如下所示。

13三种结构的Inception module

        1. Resnet50架构

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对应起来

 

      1. 基准模型

我们期望训练后的模型在测试集上的得分表现 score 可以达到 kaggle 排行榜前 10%,即是在 Public Leaderboard 上的 logloss 低于 0.06127。

      1. 最终模型概览

利用vgg19Res50InceptionV3模型进行模型融合,利用迁移学习,将特征提取后再连接,最后加入一个全连接层和一个输出层给出预测结果。直接利用上述模型在imagenet上的预训练权重,全连接层前的特征输出作为后续模型的输入。模型架构如下:

 

代码如下所示:

16 模型构建关键代码

    1. 模型验证

Keras的便利使得模型在训练完1代后,会自动验证数据集上进行验证,输出验证logloss损失和准确率。如下所示

 17 模型验证输出

训练过程中,发现改变epoch、batch-size效果很明显,训练速度较大影响,dropout比率最后一个FC层的节点数目模型的准确率影响很大。

    1. 模型测试

对于给定的测试数据集,包含12500张图片,要求将测试结束按照指定的csv格式输出并提交。

  1. 利用predict_generator函数输出test数据集在各个模型上的特征变量,并将其连接起来形成x_test;
  2. 利用模型最后一个FC层输出层在训练数据得到权重,对X_test进行预测,输出y_predict;
  3. 初始化一个空的Dataframe,包含两列(ID,label);填入图片ID将label初始化为-1.0;而后构建一个test_generator,利用一个循环遍历整个generator的文件,y_predict中找出对每个文件的预测结果更新label;
  4. 将更新后的dataframe写入csv文件

代码如下所示

18 测试结果输出代码

  1. 实施过程
    1. 数据预处理过程
      1. 数据切分与读入

为了使用keras的imagedatagenerator函数文件存放结构如下所示:

函数代码如下:

数据读入利用keras的ImageDataGenerator产生数据生成器,同时改变图尺寸代码如下:

      1. 异常值检测

异常值检测主要使用VGG19xception、Resnet模型在Imagenet上的预训练结果,对train数据集进行检测,检测其图片类别属于dog或者cat的概率。一般取top-30或者top-50预测结果,如果前30类或者前50类未预测出dog或者cat,则将其归为异常值。取3类模型的异常值预测结果的并集,而后再手动检测。其中vgg模型检出93张,xception模型检出34张,Resnet50模型检出45张。取并集后,异常数据为94张。从目视检测结果来看,有部分是模型错误,但为了提高模型的准确率避免对模型产生干扰,可以删除。

如下所示

 

19 异常数据缩略图

      1. Preprocess_input处理

选择VGG19Resnet、inceptionV3三个预训练模型作为融合来源,由于VGG19要求输入减去均值,InceptionV3要求进行归一化处理,因此,对于VGG19模型和InceptionV3模型分别调用其preprocess_input函数进行处理。如所示:

      1. 训练集验证集划分

训练集验证集划分直接利用keras的model.fit函数中的validation_split参数进行设置验证集占比,代码如下:

model.fit(X_train, y_train, batch_size=48, epochs=50, validation_split=0.2)

    1. 模型构建过程
      1. 单独利用vgg模型进行预测

单独利用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的变化曲线

 

      1. 单独利用Resnet50模型进行预测

代码段如下所示:

23模型部分代码

模型训练在第16个epoch停止,输出结果如下所示:

24模型输出结果

TrainingAccuracy Validation Accuracy随训练epoch的变化曲线,如下所示:

25 TrainingAccuracy Validation Accuracy随训练epoch的变化曲线

Training loss Validation loss随训练epoch的变化曲线如下所示:

26 Training loss Validation loss随训练epoch的变化曲线

 

 

      1. 单独利用InceptionV3模型进行预测

使用该模型进行训练时,设置了模型的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的变化曲线

      1. 利用融合模型进行预测

融合模型结构:

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变化曲线

      1. 模型性能对比

Keras在训练过程中,能够式的生成log日志,使用tensorflow的tensorboard来解析这个日志,并通过网页的形式展现出来。Tensorboard会自动绘制ACC和Loss曲线,通过下列曲线,可以观察到模型的训练进展如下为性能对比曲线颜色图示。

36曲线图示

TrainingAccuracy Validation Accuracy随训练epoch的变化曲线,如下所示:

37训练集正确率及验证集正确率随epoch变化曲线对照

Training loss Validation loss随训练epoch的变化曲线如下所示:

38训练集loss及验证集loss随epoch变化曲线对照

可以见到,在本项目数据集支持下,根据验证集上的loss分数,各模型性能排序如下:

  1. InceptionV3
  2. 融合模型
  3. Resnet50
  4. Vgg19

InceptionV3模型在正确率上较融合模型及其他模型在前期有一定优势,在损失上又相较其他模型更小,但在最终测试结果上logloss分数排名(越小越好)如下:

  1. 融合模型
  2. InceptionV3
  3. Resnet50
  4. Vgg19
  1. 最终结果
    1. 模型评估与验证

得益于结合了优秀的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. 结论思考
      1. 改进方向

至少种路径值得进一步尝试:

  1. 融合vgg19inceptionV3Resnet50、vgg16Xception等5大经典模型的迁移学习,利用上述模型在imagenet数据集上的预训练权重,图像进行特征提取,最后再加入2-3Dense层,最后进行分类,效果有可能更好;以上这5模型已经集成在keras中
  2. 利用Resnet50在其上做fine tuning,而后进行预测,逐步改进其预测准确率,降低其logloss分数,对模型的微调理解会更加深刻
      1. 思考

项目实施过程中,印象深刻的几点:

1Keras大大简化了代码,实现了对tensorflow等机器学习框架的很好的封装

2、Tensorflow TFRecord方式及keras的ImageGenerator库哪种方式供给数据的效率更高?显然第一种的便利性较差,第二种实施更为方便快捷。但运行效率呢?

3、任何机器学习问题,都免不了数据预处理、模型构建、模型训练、测试与验证等步骤;但预处理与模型调参可能占用大部分时间和精力,数据组织方式也会对模型有较大影响。

 

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值