(人脸识别2-6)——利用keras训练识别模型

利用keras训练识别模型

本篇打算利用keras库对前面收集到的模型进行训练,这里训练将会用到卷积神经网络的知识,相关的东西看前面的文章。(人脸识别2-4)——关于CNN卷积神经网络

声明:这篇文章特别长,而且需要对前面的神经网络有了解。来杯咖啡慢慢看

1. 代码

直接开始上可运行代码:


# -*- coding: utf-8 -*-
import random

import numpy as np
from sklearn.cross_validation import train_test_split
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Convolution2D, MaxPooling2D
from keras.optimizers import SGD
from keras.utils import np_utils
from keras.models import load_model
from keras import backend as K

from loadData import load_dataset, resize_image, IMAGE_SIZE


class Dataset:
    def __init__(self, path_name):
        # 训练集
        self.train_images = None
        self.train_labels = None

        # 验证集
        self.valid_images = None
        self.valid_labels = None

        # 测试集
        self.test_images = None
        self.test_labels = None

        # 数据集加载路径
        self.path_name = path_name

        # 当前库采用的维度顺序
        self.input_shape = None

    def loadAllData(self,path_name):
        positive_data_images,positive_data_labels=load_dataset(path_name,'traindata')
        negative_data_images,negative_data_labels=load_dataset(path_name,'testdata')
        images =np.concatenate((positive_data_images, negative_data_images), axis=0)
        labels=np.concatenate((positive_data_labels, negative_data_labels), axis=0)
        return images,labels


    # 加载数据集并按照交叉验证的原则划分数据集并进行相关预处理工作
    def load(self, img_rows=IMAGE_SIZE, img_cols=IMAGE_SIZE,
             img_channels=3, nb_classes=2):
        # 加载数据集到内存
        images, labels = self.loadAllData(self.path_name)
        print 'images,labels'
        print images,labels

        train_images, valid_images, train_labels, valid_labels = train_test_split(images, labels, test_size=0.3,
                                                                                  random_state=random.randint(0, 100))
        _, test_images, _, test_labels = train_test_split(images, labels, test_size=0.5,
                                                          random_state=random.randint(0, 100))

        # 当前的维度顺序如果为'th',则输入图片数据时的顺序为:channels,rows,cols,否则:rows,cols,channels
        # 这部分代码就是根据keras库要求的维度顺序重组训练数据集
        if K.image_dim_ordering() == 'th':
            train_images = train_images.reshape(train_images.shape[0], img_channels, img_rows, img_cols)
            valid_images = valid_images.reshape(valid_images.shape[0], img_channels, img_rows, img_cols)
            test_images = test_images.reshape(test_images.shape[0], img_channels, img_rows, img_cols)
            self.input_shape = (img_channels, img_rows, img_cols)
        else:
            train_images = train_images.reshape(train_images.shape[0], img_rows, img_cols, img_channels)
            valid_images = valid_images.reshape(valid_images.shape[0], img_rows, img_cols, img_channels)
            test_images = test_images.reshape(test_images.shape[0], img_rows, img_cols, img_channels)
            self.input_shape = (img_rows, img_cols, img_channels)

            # 输出训练集、验证集、测试集的数量
            print(train_images.shape[0], 'train samples')
            print(valid_images.shape[0], 'valid samples')
            print(test_images.shape[0], 'test samples')

            # 我们的模型使用categorical_crossentropy作为损失函数,因此需要根据类别数量nb_classes将
            # 类别标签进行one-hot编码使其向量化,在这里我们的类别只有两种,经过转化后标签数据变为二维
            train_labels = np_utils.to_categorical(train_labels, nb_classes)
            valid_labels = np_utils.to_categorical(valid_labels, nb_classes)
            test_labels = np_utils.to_categorical(test_labels, nb_classes)

            # 像素数据浮点化以便归一化
            train_images = train_images.astype('float32')
            valid_images = valid_images.astype('float32')
            test_images = test_images.astype('float32')

            # 将其归一化,图像的各像素值归一化到0~1区间
            train_images /= 255
            valid_images /= 255
            test_images /= 255

            self.train_images = train_images
            self.valid_images = valid_images
            self.test_images = test_images
            self.train_labels = train_labels
            self.valid_labels = valid_labels
            self.test_labels = test_labels


# CNN网络模型类
class Model:
    def __init__(self):
        self.model = None

    def evaluate(self, dataset):
        score = self.model.evaluate(dataset.test_images, dataset.test_labels, verbose=1)
        print("%s: %.2f%%" % (self.model.metrics_names[1], score[1] * 100))

    MODEL_PATH = './face.model.h5'

    def save_model(self, file_path=MODEL_PATH):
        self.model.save(file_path)

    def load_model(self, file_path=MODEL_PATH):
        self.model = load_model(file_path)

    # 建立模型
    def build_model(self, dataset, nb_classes=2):
        # 构建一个空的网络模型,它是一个线性堆叠模型,各神经网络层会被顺序添加,专业名称为序贯模型或线性堆叠模型
        self.model = Sequential()

        # 以下代码将顺序添加CNN网络需要的各层,一个add就是一个网络层
        self.model.add(Convolution2D(32, 3, 3, border_mode='same',
                                     input_shape=dataset.input_shape))  # 1 2维卷积层
        self.model.add(Activation('relu'))  # 2 激活函数层

        self.model.add(Convolution2D(32, 3, 3))  # 3 2维卷积层
        self.model.add(Activation('relu'))  # 4 激活函数层

        self.model.add(MaxPooling2D(pool_size=(2, 2)))  # 5 池化层
        self.model.add(Dropout(0.25))  # 6 Dropout层

        self.model.add(Convolution2D(64, 3, 3, border_mode='same'))  # 7  2维卷积层
        self.model.add(Activation('relu'))  # 8  激活函数层

        self.model.add(Convolution2D(64, 3, 3))  # 9  2维卷积层
        self.model.add(Activation('relu'))  # 10 激活函数层

        self.model.add(MaxPooling2D(pool_size=(2, 2)))  # 11 池化层
        self.model.add(Dropout(0.25))  # 12 Dropout层

        self.model.add(Flatten())  # 13 Flatten层
        self.model.add(Dense(512))  # 14 Dense层,又被称作全连接层
        self.model.add(Activation('relu'))  # 15 激活函数层
        self.model.add(Dropout(0.5))  # 16 Dropout层
        self.model.add(Dense(nb_classes))  # 17 Dense层
        self.model.add(Activation('softmax'))  # 18 分类层,输出最终结果

        # 输出模型概况
        self.model.summary()

    # 训练模型
    def train(self, dataset, batch_size=20, nb_epoch=8, data_augmentation=True):
        sgd = SGD(lr=0.01, decay=1e-6,
                  momentum=0.9, nesterov=True)  # 采用SGD+momentum的优化器进行训练,首先生成一个优化器对象
        self.model.compile(loss='categorical_crossentropy',
                           optimizer=sgd,
                           metrics=['accuracy'])  # 完成实际的模型配置工作

        # 不使用数据提升,所谓的提升就是从我们提供的训练数据中利用旋转、翻转、加噪声等方法创造新的
        # 训练数据,有意识的提升训练数据规模,增加模型训练量
        if not data_augmentation:
            self.model.fit(dataset.train_images,
                           dataset.train_labels,
                           batch_size=batch_size,
                           nb_epoch=nb_epoch,
                           validation_data=(dataset.valid_images, dataset.valid_labels),
                           shuffle=True)
        # 使用实时数据提升
        else:
            # 定义数据生成器用于数据提升,其返回一个生成器对象datagen,datagen每被调用一
            # 次其生成一组数据(顺序生成),节省内存,其实就是python的数据生成器
            datagen = ImageDataGenerator(
                featurewise_center=False,  # 是否使输入数据去中心化(均值为0),
                samplewise_center=False,  # 是否使输入数据的每个样本均值为0
                featurewise_std_normalization=False,  # 是否数据标准化(输入数据除以数据集的标准差)
                samplewise_std_normalization=False,  # 是否将每个样本数据除以自身的标准差
                zca_whitening=False,  # 是否对输入数据施以ZCA白化
                rotation_range=20,  # 数据提升时图片随机转动的角度(范围为0~180)
                width_shift_range=0.2,  # 数据提升时图片水平偏移的幅度(单位为图片宽度的占比,0~1之间的浮点数)
                height_shift_range=0.2,  # 同上,只不过这里是垂直
                horizontal_flip=True,  # 是否进行随机水平翻转
                vertical_flip=False)  # 是否进行随机垂直翻转

            # 计算整个训练样本集的数量以用于特征值归一化、ZCA白化等处理
            datagen.fit(dataset.train_images)

            # 利用生成器开始训练模型
            self.model.fit_generator(datagen.flow(dataset.train_images, dataset.train_labels,
                                                  batch_size=batch_size),
                                     samples_per_epoch=dataset.train_images.shape[0],
                                     nb_epoch=nb_epoch,
                                     validation_data=(dataset.valid_images, dataset.valid_labels))

            # 识别人脸

    def face_predict(self, image):
        # 依然是根据后端系统确定维度顺序
        if K.image_dim_ordering() == 'th' and image.shape != (1, 3, IMAGE_SIZE, IMAGE_SIZE):
            image = resize_image(image)  # 尺寸必须与训练集一致都应该是IMAGE_SIZE x IMAGE_SIZE
            image = image.reshape((1, 3, IMAGE_SIZE, IMAGE_SIZE))  # 与模型训练不同,这次只是针对1张图片进行预测
        elif K.image_dim_ordering() == 'tf' and image.shape != (1, IMAGE_SIZE, IMAGE_SIZE, 3):
            image = resize_image(image)
            image = image.reshape((1, IMAGE_SIZE, IMAGE_SIZE, 3))

            # 浮点并归一化
        image = image.astype('float32')
        image /= 255

        # 给出输入属于各个类别的概率,我们是二值类别,则该函数会给出输入图像属于0和1的概率各为多少
        result = self.model.predict_proba(image)
        print('result:', result)

        # 给出类别预测:0或者1
        result = self.model.predict_classes(image)

        # 返回类别预测结果
        return result[0]




import os
if __name__ == '__main__':
    path_name = os.getcwd()
    dataset = Dataset(path_name)
    dataset.load()
    # 先前添加的测试build_model()函数的代码

    model = Model()
    model.build_model(dataset)

    # 测试训练函数的代码
    model.train(dataset)
    model.save_model(file_path = './face.model.h5')
    model.evaluate(dataset)


这里再次说一下我们的目录结构,不然程序无法读取数据

这里写图片描述

其中openvideo存放在我们的运行的py文件代码,traindata存放训练集图片,测试集存放测试图片

2. 代码解释

到目前为止,数据加载的工作已经完成,我们只需调用这个接口即可。关于训练集的使用,我们需要拿出一部分用于训练网络,建立识别模型;另一部分用于验证模型。同时我们还有一些其它的比如数据归一化等预处理的工作要做,因此,我们把这些工作封装成一个dataset类来完成

我们构建了一个Dataset类,用于数据加载及预处理。其中,init()为类的初始化函数,load()则完成实际的数据加载及预处理工作。加载前面已经说过很多了,就不多说了。关于预处理,我们做了几项工作:

  • 按照交叉验证的原则将数据集划分成三部分:训练集、验证集、测试集;

  • 按照keras库运行的后端系统要求改变图像数据的维度顺序;

  • 将数据标签进行one-hot编码,使其向量化

  • 归一化图像数据

1) 第一项工作

先简单说说什么是交叉验证?交叉验证属于机器学习中常用的精度测试方法,它的目的是提升模型的可靠和稳定性。我们会拿出大部分数据用于模型训练,小部分数据用于对训练后的模型验证,验证结果会与验证集真实值(即标签值)比较并计算出差平方和,此项工作重复进行,直至所有验证结果与真实值相同,交叉验证结束,模型交付使用。在这里我们导入了sklearn库的交叉验证模块,利用函数train_test_split()来划分训练集和验证集,具体语句如下:

train_images, valid_images, train_labels, valid_labels = train_test_split(images, labels, test_size = 0.2, 
                                                                                  random_state = random.randint(0, 100))

train_test_split()会根据test_size参数按比例划分数据集(不要被test_size的外表所迷惑,它只是用来指定数据集划分比例的,本质上与测试无关,划分完了你爱咋用就咋用),在这里我们划分出了30%的数据用于验证,70%用于训练模型。参数random_state用于指定一个随机数种子,从全部数据中随机选取数据建立训练集和验证集,所以你将会看到每次训练的结果都会稍有不同。当然,为了省事,测试集我也调用了这个函数:

_, test_images, _, test_labels = train_test_split(images, labels, test_size = 0.5, 
                                                          random_state = random.randint(0, 100))

在这里,测试集我选择的比例为0.5,所以前面的“, test_images, , test_labels”语句你调个顺序也成,即“test_images, , test_labels, ”,但是如果你改成其它数值,就必须严格按照代码给出的顺序才能得到你想要的结果。train_test_split()函数会按照训练集特征数据(这里就是图像数据)、测试集特征数据、训练集标签、测试集标签的顺序返回各数据集。所以,看你的选择了。

2) 第二项工作

我们前面不止一次说过keras建立在tensorflow或theano基础上,换句话说,keras的后端系统可以是tensorflow也可以是theano。后端系统决定了图像数据输入CNN网络时的维度顺序,tensorflow的维度顺序为行数(rows)、列数(cols)、通道数(颜色通道,channels);theano则是通道数、行数、列数。所以,我们通过调用image_dim_ordering()函数来确定后端系统的类型(‘th’代表theano,’tf’代表tensorflow),然后我们再通过numpy提供的reshape()函数重新调整数组维度。

3) 第三项工作

对标签集进行one-hot编码的原因是我们的训练模型采用categorical_crossentropy作为损失函数(多分类问题的常用函数,后面会详解),这个函数要求标签集必须采用one-hot编码形式。所以,我们对训练集、验证集和测试集标签均做了编码转换。那么什么是one-hot编码呢?one-hot有的翻译成独热,有的翻译成一位有效,个人感觉一位有效更直白一些。因为one-hot编码采用状态寄存器的组织方式对状态进行编码,每个状态值对应一个寄存器位,且任意时刻,只有一位有效。对于我们的程序来说,我们类别状态只有两种(nb_classes = 2):0和1,0代表我,1代表闺女。one-hot编码会提供两个寄存器位保存这两个状态,如果标签值为0,则编码后值为[1 0],代表第一位有效;如果为1,则编码后值为[0 1],代表第2为有效。换句话说,one-hot编码将数值变成了位置信息,使其向量化,这样更方便CNN操作。

4) 第四项工作

数据集先浮点后归一化的目的是提升网络收敛速度,减少训练时间,同时适应值域在(0,1)之间的激活函数,增大区分度。其实归一化有一个特别重要的原因是确保特征值权重一致。举个例子,我们使用mse这样的均方误差函数时,大的特征数值比如(5000-1000)2与小的特征值(3-1)2相加再求平均得到的误差值,显然大值对误差值的影响最大,但大部分情况下,特征值的权重应该是一样的,只是因为单位不同才导致数值相差甚大。因此,我们提前对特征数据做归一化处理,以解决此类问题。关于归一化的详细介绍有兴趣的请参考如下链接:

深入理解CNN细节之数据预处理

3. 神经网络搭建

数据准备工作到此完成,接下来就要进入整个系列最关键的一个节点——建立我们自己的卷积神经网络模型,激动吧;?与数据集加载及预处理模块一样,我们依然将模型构建成一个类来使用,新建的这个模型类添加在Dataset类的下面的model里面

#CNN网络模型类            
class Model:
    def __init__(self):
        self.model = None 

    #建立模型
    def build_model(self, dataset, nb_classes = 2):
        #构建一个空的网络模型,它是一个线性堆叠模型,各神经网络层会被顺序添加,专业名称为序贯模型或线性堆叠模型
        self.model = Sequential() 

        #以下代码将顺序添加CNN网络需要的各层,一个add就是一个网络层
        self.model.add(Convolution2D(32, 3, 3, border_mode='same', 
                                     input_shape = dataset.input_shape))    #1 2维卷积层
        self.model.add(Activation('relu'))                                  #2 激活函数层

        self.model.add(Convolution2D(32, 3, 3))                             #3 2维卷积层                             
        self.model.add(Activation('relu'))                                  #4 激活函数层

        self.model.add(MaxPooling2D(pool_size=(2, 2)))                      #5 池化层
        self.model.add(Dropout(0.25))                                       #6 Dropout层

        self.model.add(Convolution2D(64, 3, 3, border_mode='same'))         #7  2维卷积层
        self.model.add(Activation('relu'))                                  #8  激活函数层

        self.model.add(Convolution2D(64, 3, 3))                             #9  2维卷积层
        self.model.add(Activation('relu'))                                  #10 激活函数层

        self.model.add(MaxPooling2D(pool_size=(2, 2)))                      #11 池化层
        self.model.add(Dropout(0.25))                                       #12 Dropout层

        self.model.add(Flatten())                                           #13 Flatten层
        self.model.add(Dense(512))                                          #14 Dense层,又被称作全连接层
        self.model.add(Activation('relu'))                                  #15 激活函数层   
        self.model.add(Dropout(0.5))                                        #16 Dropout层
        self.model.add(Dense(nb_classes))                                   #17 Dense层
        self.model.add(Activation('softmax'))                               #18 分类层,输出最终结果

        #输出模型概况
        self.model.summary()

我们通过调用self.model.summary()函数将网络模型基本结构信息展示在我们面前,包括层类型、维度、参数个数、层连接等信息,一目了然,简洁、清晰。通过上图我们可以看出,这个网络模型共18层,包括4个卷积层、5个激活函数层、2个池化层(pooling layer)、3个Dropout层、2个全连接层、1个Flatten层、1个分类层,训练参数为6,489,634个,还是很可观的。

你看,这个实际运作的网络比我们上次给出的那个3层卷积的网络复杂多了,多了池化、Dropout、Dense、Flatten以及最终的分类层,这些都是些什么鬼东西,需要我们逐个理一理:

1)卷积层(convolution layer):这一层前面讲了太多,这里重点讲讲Convolution2D()函数。根据keras官方文档描述,2D代表这是一个2维卷积,其功能为对2维输入进行滑窗卷积计算。我们的脸部图像尺寸为64*64,拥有长、宽两维,所以在这里我们使用2维卷积函数计算卷积。所谓的滑窗计算,其实就是利用卷积核逐个像素、顺序进行计算,如下图:
这里写图片描述

上图选择了最简单的均值卷积核,3x3大小,我们用这个卷积核作为掩模对前面4x4大小的图像逐个像素作卷积运算。首先我们将卷积核中心对准图像第一个像素,在这里就是像素值为237的那个像素。卷积核覆盖的区域(掩模之称即由此来),其下所有像素取均值然后相加:

C(1) = 0 * 0.5 + 0 * 0.5 + 0 * 0.5 + 0 * 0.5 + 237 * 0.5 + 203 * 0.5 + 0 * 0.5 + 123 * 0.5 + 112 * 0.5

结果直接替换卷积核中心覆盖的像素值,接着是第二个像素、然后第三个,从左至右,由上到下……以此类推,卷积核逐个覆盖所有像素。整个操作过程就像一个滑动的窗口逐个滑过所有像素,最终生成一副尺寸相同但已经过卷积处理的图像。上图我们采用的是均值卷积核,实际效果就是将图像变模糊了。显然,卷积核覆盖图像边界像素时,会有部分区域越界,越界的部分我们以0填充,如上图。对于此种情况,还有一种处理方法,就是丢掉边界像素,从覆盖区域不越界的像素开始计算。像上图,如果采用丢掉边界像素的方法,3x3的卷积核就应该从第2行第2列的像素(值为112)开始,到第3行第3列结束,最终我们会得到一个2x2的图像。这种处理方式会丢掉图像的边界特征;而第一种方式则保留了图像的边界特征。在我们建立的模型中,卷积层采用哪种方式处理图像边界,卷积核尺寸有多大等参数都可以通过Convolution2D()函数来指定:

self.model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape = dataset.input_shape))

第一个卷积层包含32个卷积核,每个卷积核大小为3x3,border_mode值为“same”意味着我们采用保留边界特征的方式滑窗,而值“valid”则指定丢掉边界像素。根据keras开发文档的说明,当我们将卷积层作为网络的第一层时,我们还应指定input_shape参数,显式地告知输入数据的形状,对我们的程序来说,input_shape的值为(64,64,3),来自Dataset类,代表64x64的彩色RGB图像。

2)激活函数层:它的作用前面已经说了,这里讲一下代码中采用的relu(Rectified Linear Units,修正线性单元)函数,它的数学形式如下:

ƒ(x) = max(0, x)

这个函数非常简单,其输出一目了然,小于0的输入,输出全部为0,大于0的则输入与输出相等。该函数的优点是收敛速度快,除了它,keras库还支持其它几种激活函数,如下:

  • softplus
  • softsign
  • tanh
  • sigmoid
  • hard_sigmoid
  • linear

它们的函数式、优缺点度娘会告诉你,不多说。对于不同的需求,我们可以选择不同的激活函数,这也是模型训练可调整的一部分,运用之妙,存乎一心,请自忖之。另外再交代一句,其实激活函数层按照我们前文所讲,其属于人工神经元的一部分,所以我们亦可以在构造层对象时通过传递activation参数设置,如下:

self.model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape = dataset.input_shape)) 
self.model.add(Activation('relu'))    #设置为单独的激活层

#通过传递activation参数设置,与上两行代码的作用相同
self.model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape = dataset.input_shape, activation='relu'))

3)池化层(pooling layer):池化层存在的目的是缩小输入的特征图,简化网络计算复杂度;同时进行特征压缩,突出主要特征。我们通过调用MaxPooling2D()函数建立了池化层,这个函数采用了最大值池化法,这个方法选取覆盖区域的最大值作为区域主要特征组成新的缩小后的特征图:
这里写图片描述

显然,池化层与卷积层覆盖区域的方法不同,前者按照池化尺寸逐块覆盖特征图,卷积层则是逐个像素滑动覆盖。对于我们输入的64x64的脸部特征图来说,经过2x2池化后,图像变为32x32大小。

4)Dropout层:随机断开一定百分比的输入神经元链接,以防止过拟合。那么什么是过拟合呢?一句话解释就是训练数据预测准确率很高,测试数据预测准确率很低,用图形表示就是拟合曲线较尖,不平滑。导致这种现象的原因是模型的参数很多,但训练样本太少,导致模型拟合过度。为了解决这个问题,Dropout层将有意识的随机减少模型参数,让模型变得简单,而越简单的模型越不容易产生过拟合。代码中Dropout()函数只有一个输入参数——指定抛弃比率,范围为0~1之间的浮点数,其实就是百分比。这个参数亦是一个可调参数,我们可以根据训练结果调整它以达到更好的模型成熟度。

5)Flatten层:截止到Flatten层之前,在网络中流动的数据还是多维的(对于我们的程序就是2维的),经过多次的卷积、池化、Dropout之后,到了这里就可以进入全连接层做最后的处理了。全连接层要求输入的数据必须是一维的,因此,我们必须把输入数据“压扁”成一维后才能进入全连接层,Flatten层的作用即在于此。该层的作用如此纯粹,因此反映到代码上我们看到它不需要任何输入参数。

6)全连接层(dense layer):全连接层的作用就是用于分类或回归,对于我们来说就是分类。keras将全连接层定义为Dense层,其含义就是这里的神经元连接非常“稠密”。我们通过Dense()函数定义全连接层。这个函数的一个必填参数就是神经元个数,其实就是指定该层有多少个输出。在我们的代码中,第一个全连接层(#14 Dense层)指定了512个神经元,也就是保留了512个特征输出到下一层。这个参数可以根据实际训练情况进行调整,依然是没有可参考的调整标准,自调之。

7)分类层:全连接层最终的目的就是完成我们的分类要求:0或者1,模型构建代码的最后两行完成此项工作:

self.model.add(Dense(nb_classes))                                   #17 Dense层
self.model.add(Activation('softmax'))                               #18 分类层,输出最终结果

第17层我们按照实际的分类要求指定神经元个数,对我们来说就是2,18层我们通过softmax函数完成最终分类。关于softmax函数,其函数式如下
这里写图片描述

这个值其实就是第j个神经元在所有神经元输出中所占的百分比。从分类的角度来说,该神经元的输出值越大,其对应的类别为真实类别的可能性就越大。因此,经过softmax函数,上层的N个输入被映射成N个概率分布,概率之和为1,概率值最大者即为模型预测的类别。

好了,模型构建完毕,接下来构建训练代码,在build_model()函数下面继续添加如下代码:

#训练模型
    def train(self, dataset, batch_size = 20, nb_epoch = 10, data_augmentation = True):        
        sgd = SGD(lr = 0.01, decay = 1e-6, 
                  momentum = 0.9, nesterov = True) #采用SGD+momentum的优化器进行训练,首先生成一个优化器对象  
        self.model.compile(loss='categorical_crossentropy',
                           optimizer=sgd,
                           metrics=['accuracy'])   #完成实际的模型配置工作

        #不使用数据提升,所谓的提升就是从我们提供的训练数据中利用旋转、翻转、加噪声等方法创造新的
        #训练数据,有意识的提升训练数据规模,增加模型训练量
        if not data_augmentation:            
            self.model.fit(dataset.train_images,
                           dataset.train_labels,
                           batch_size = batch_size,
                           nb_epoch = nb_epoch,
                           validation_data = (dataset.valid_images, dataset.valid_labels),
                           shuffle = True)
        #使用实时数据提升
        else:            
            #定义数据生成器用于数据提升,其返回一个生成器对象datagen,datagen每被调用一
            #次其生成一组数据(顺序生成),节省内存,其实就是python的数据生成器
            datagen = ImageDataGenerator(
                featurewise_center = False,             #是否使输入数据去中心化(均值为0),
                samplewise_center  = False,             #是否使输入数据的每个样本均值为0
                featurewise_std_normalization = False,  #是否数据标准化(输入数据除以数据集的标准差)
                samplewise_std_normalization  = False,  #是否将每个样本数据除以自身的标准差
                zca_whitening = False,                  #是否对输入数据施以ZCA白化
                rotation_range = 20,                    #数据提升时图片随机转动的角度(范围为0~180)
                width_shift_range  = 0.2,               #数据提升时图片水平偏移的幅度(单位为图片宽度的占比,0~1之间的浮点数)
                height_shift_range = 0.2,               #同上,只不过这里是垂直
                horizontal_flip = True,                 #是否进行随机水平翻转
                vertical_flip = False)                  #是否进行随机垂直翻转

            #计算整个训练样本集的数量以用于特征值归一化、ZCA白化等处理
            datagen.fit(dataset.train_images)                        

            #利用生成器开始训练模型
            self.model.fit_generator(datagen.flow(dataset.train_images, dataset.train_labels,
                                                   batch_size = batch_size),
                                     samples_per_epoch = dataset.train_images.shape[0],
                                     nb_epoch = nb_epoch,
                                     validation_data = (dataset.valid_images, dataset.valid_labels))

关键解释

下一步可以用测试数据集对其进行测试了。添加测试代码之前,我们需要对训练代码中几个关键函数交代一下。首先是优化器函数,优化器用于训练模型,它的作用就是调整训练参数(权重和偏置值)使其最优,确保e值最小。keras提供了很多优化器,我们在这里采用的SGD就是其中一种,它就是机器学习领域最著名的随机梯度下降法。函数第一个参数lr用于指定学习效率(lr,Learning Rate,参见系列4),其值为大于0的浮点数。decay指定每次更新后学习效率的衰减值,这个值一定很小(1e-6,0.000 001),否则速率会衰减很快。momentum指定动量值。SGD方法有一个明显的缺点就是,它的下降方向完全依赖当前的训练样本(batch),因此其优化十分不稳定。为了解决这个问题,大牛们引进了动量(momentum),用它来模拟物体运动时的惯性,让优化器在一定程度上保留之前的优化方向,同时利用当前样本微调最终的优化方向,这样即能增加稳定性,提高学习速度,又在一定程度上避免了陷入局部最优陷阱。参数momentum即用于指定在多大程度上保留原有方向,其值为0~1之间的浮点数。一般来说,选择一个在0.5~0.9之间的数即可。代码中SGD函数的最后一个参数nesterov则用于指定是否采用nesterov动量方法,nesterov momentum是对传统动量法的一个改进方法,其效率更高,关于它的详细介绍可参考如下链接:

http://www.360doc.com/content/16/1010/08/36492363_597225745.shtml

对于compile()函数,其作用就是编译模型以完成实际的配置工作,为接下来的模型训练做好准备。换句话说,compile之后模型就可以开始训练了。这个函数有一个很重要的参数:loss,它用于指定一个损失函数。所谓损失函数,通俗地说,它是统计学中衡量损失和错误程度的函数,显然,其值越小,模型就越好。如果你仔细阅读了系列4——CNN入门,那么,你肯定能猜到这个函数其实就是我们的优化对象。代码中loss的值为“categorical_crossentropy”,常用于多分类问题,其与激活函数softmax配对使用(我们的类别只有两种,也可采用‘binary_crossentropy’二值分类函数,该函数与sigmoid配对使用,注意如果采用它就不需要one-hot编码)。

接着就是数据提升,我们可以选择不提升,也就是采用原始训练集和验证集,这时我们直接调用model.fit()函数即可开始模型训练。该函数shuffle参数用于指定是否随机打乱数据集。一般来说选择数据提升要比不提升好,这样可以让我们利用有限数量的图片获得无限数量的训练图片。因为我们一旦选择数据提升,ImageDataGenerator()函数返回的生成器会在模型训练时无限生成训练数据,直至所有训练轮次(epoch)结束(对我们的代码来说就是840 x 10,生成了8400张图片)。model.fit_generator()函数使用生成器开始模型训练。

在这里需要重点交代一下batch_size和nb_epoch两个参数。nb_epoch指定模型需要训练多少轮次,使用训练集全部样本训练一次为一个训练轮次。根据模型成熟度,我们可以适当调整该值以增加或减少训练次数。batch_size则是一个影响模型训练结果的重要参数。我们知道,一个训练轮次要经过多次迭代训练才能让模型逐渐趋向本轮最优,这是因为理论上每次迭代训练结束后,模型都应该朝着梯度下降的方向前进一步,直至全部样本训练完毕,模型梯度到达本轮最小点。

之所以说理论上,是因为决定梯度方向的是训练样本,每次迭代训练选取的样本——其决定的下降方向能否很好的代表样本全体,直接决定了模型能否到达正确的极值点。对于小的训练集,我们完全可以采用全数据集的方式进行训练,因为,全数据集确定的方向肯定能代表正确方向。

但这样做对大的训练集就很不现实,因为内存有限,无法一次载入全部数据。于是,批梯度下降法(Mini-batches Learning)应运而生。我们一次选取适当数量的训练样本(视内存大小,可多可少),逐批次迭代,直至本轮全部样本训练完毕。参数batch_size的作用即在于此,其指定每次迭代训练样本的数量。该值的选取非常讲究,不能盲目的增大或减小,因为batch_size太大或太小都会让模型训练效率变慢。显然,batch_size肯定存在一个局部最优值,这需要我们慢慢调试,调试时可从一个小值开始,慢慢加大,直至到达一个合理值(建议编码实现该参数调优)。

现在模型训练的工作已经完成,接下来我们就要考虑模型使用的问题了。要想使用模型,我们必须能够把模型保存下来,因此,我们继续为Model类添加两个函数:

     MODEL_PATH = './me.face.model.h5'
     def save_model(self, file_path = MODEL_PATH):
         self.model.save(file_path)

     def load_model(self, file_path = MODEL_PATH):
         self.model = load_model(file_path)

一个函数用于保存模型,一个函数用于加载模型。keras库利用了压缩效率更高的HDF5保存模型,所以我们用“.h5”作为文件后缀。上述代码添加完毕后,我们接着在文件尾部添加测试代码,把模型训练好并把模型保存下来:

if __name__ == '__main__':
    dataset = Dataset('./data/')    
    dataset.load()

    model = Model()
    model.build_model(dataset)
    model.train(dataset)
    model.save_model(file_path = './model/me.face.model.h5')

执行上述代码,顺利的话,我们应当看到模型保存文件出现在model文件夹下了:
这里写图片描述
好了,接下来我们就要用前面Dataset类提供的测试集测试模型了。首先,我们为Model类添加一个模型评估函数:

     def evaluate(self, dataset):
         score = self.model.evaluate(dataset.test_images, dataset.test_labels, verbose = 1)
         print("%s: %.2f%%" % (self.model.metrics_names[1], score[1] * 100))

然后,继续添加测试代码:

import os
if __name__ == '__main__':    
    path_name = os.getcwd()
    dataset = Dataset(path_name)
    dataset.load()
    # 先前添加的测试build_model()函数的代码

    model = Model()
    model.build_model(dataset)

    # 测试训练函数的代码
    model.train(dataset)
    model.save_model(file_path = './face.model.h5')
    model.evaluate(dataset)

准确率98.9%,相当高的评估结果了。
这里写图片描述

总结

对于文章中提到的各种神经网络层,对于刚接触此类卷积神经网络的人来说非常难以理解,包括本人,所以对于所有的关于代码的解释都来自于http://www.cnblogs.com/neo-T/p/6477378.html
但是代码属于本人修改以适应更多的环境配置,原作者对于数据读取不是很好理解,所以在原作者的基础上加以改进,

虽然总体难以理解,但是如果把神经网络部分抽象为一个借口只是简单的函数调用,便好理解了。

总体思路:
在之前已经获取到训练集和测试集的基础上(本人使用的训练集为自己的人脸图片,测试集为其他人的人脸图片,当然这里取名为训练集和测试集不确切,因为在实际代码中,可以看到,我其实是对两个文件夹内的数据进行加载归一化后,整合成两个特定的images和labels,代码如下:

    def loadAllData(self,path_name):
        positive_data_images,positive_data_labels=load_dataset(path_name,'traindata')
        negative_data_images,negative_data_labels=load_dataset(path_name,'testdata')
        images =np.concatenate((positive_data_images, negative_data_images), axis=0)
        labels=np.concatenate((positive_data_labels, negative_data_labels), axis=0)
        return images,labels

然后再对images和labels进行shuffle打乱处理,实现真正的测试集和训练集。然后再利用原作者的神经网络层进行训练,这里采用cpu的tensorflow进行训练。
总体识别率较高

至此,我们完成了模型建立工作,下一篇讨论如何用它识别出“我”了。


【原创文章】转载请注明出处:http://blog.csdn.net/wearge/article/details/77373822

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值