冬令营简记【三】—— 卷积神经网络实例讲解

前言

先上代码,我们一点一点针对代码做详细解析,最后针对一些参数来优化我们的代码,结合我们遇到的问题讲一下实际项目中应该注意的点。

代码全貌

这是司老师授课的代码,非博主原创。先预览一下代码全貌,在这里没有做详细的注释,具体代码意思随后我们详解。

#python -m pip install h5py
from __future__ import print_function  
import numpy as np
np.random.seed(1337)  # for reproducibility  用于指定随机数生成时所用算法开始的整数值,如果使用相同的seed()值,则每次生成的随即数都相同
  
from PIL import Image  

from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.optimizers import SGD  
from keras.utils import np_utils
from keras import backend as K

'''
Olivetti Faces是纽约大学的一个比较小的人脸库,由40个人的400张图片构成,即每个人的人脸图片为10张。每张图片的灰度级为8位,每个像素的灰度大小位于0-255之间。整张图片大小是1190 × 942,一共有20 × 20张照片。那么每张照片的大小就是(1190 / 20)× (942 / 20)= 57 × 47 。
'''

# There are 40 different classes  
nb_classes = 40  # 40个类别
epochs = 40  # 进行40轮次训
batch_size = 40  # 每次迭代训练使用40个样本,即一次丢进网络40个样本。丢8次,就完成了320张图片的一次训练,即完成一次epoch
  
# input image dimensions  
img_rows, img_cols = 57, 47  
# number of convolutional filters to use  
nb_filters1, nb_filters2 = 5, 10  # 卷积核的数目(即输出的维度)
# size of pooling area for max pooling  
nb_pool = 2  
# convolution kernel size  
nb_conv = 3  # 单个整数或由两个整数构成的list/tuple,卷积核的宽度和长度。如为单个整数,则表示在各个空间维度的相同长度。
  
def load_data(dataset_path):  
    img = Image.open(dataset_path)  
    img_ndarray = np.asarray(img, dtype = 'float64') / 255  # asarray,将数据转化为np.ndarray,但使用原内存
    # 400 pictures, size: 57*47 = 2679  
    faces = np.empty((400, 2679)) 
    for row in range(20):  
        for column in range(20):
           faces[row * 20 + column] = np.ndarray.flatten(img_ndarray[row*57 : (row+1)*57, column*47 : (column+1)*47]) 
           # flatten将多维数组降为一维
  
    label = np.empty(400)  
    for i in range(40):
        label[i*10 : i*10+10] = i  
    label = label.astype(np.int)  
  
    #train:320,valid:40,test:40  
    train_data = np.empty((320, 2679))  
    train_label = np.empty(320)  
    valid_data = np.empty((40, 2679))  
    valid_label = np.empty(40)  
    test_data = np.empty((40, 2679))  
    test_label = np.empty(40)  
  
    for i in range(40):
        train_data[i*8 : i*8+8] = faces[i*10 : i*10+8] # 训练集中的数据
        train_label[i*8 : i*8+8] = label[i*10 : i*10+8]  # 训练集对应的标签
        valid_data[i] = faces[i*10+8] # 验证集中的数据
        valid_label[i] = label[i*10+8] # 验证集对应的标签
        test_data[i] = faces[i*10+9] 
        test_label[i] = label[i*10+9]   
    
    train_data = train_data.astype('float32')
    valid_data = valid_data.astype('float32')
    test_data = test_data.astype('float32')
       
    rval = [(train_data, train_label), (valid_data, valid_label), (test_data, test_label)]  
    return rval  
  
def set_model(lr=0.005,decay=1e-6,momentum=0.9): 
    model = Sequential()
    if K.image_data_format() == 'channels_first':
        model.add(Conv2D(5, kernel_size=(3, 3), input_shape = (1, img_rows, img_cols)))
    else:
        model.add(Conv2D(5, kernel_size=(3, 3), input_shape = (img_rows, img_cols, 1)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))  
    model.add(Conv2D(10, kernel_size=(3, 3)))  
    model.add(Activation('tanh'))  
    model.add(MaxPooling2D(pool_size=(2, 2)))  
    model.add(Dropout(0.25))  
    model.add(Flatten())      
    model.add(Dense(1000)) #Full connection  
    model.add(Activation('tanh')) 
    model.add(Dropout(0.5))  
    model.add(Dense(nb_classes))  
    model.add(Activation('softmax'))  
    sgd = SGD(lr=lr, decay=decay, momentum=momentum, nesterov=True)  
    model.compile(loss='categorical_crossentropy', optimizer=sgd)  
    return model  
  
def train_model(model,X_train, Y_train, X_val, Y_val):  
    model.fit(X_train, Y_train, batch_size = batch_size, epochs = epochs,  
          verbose=1, validation_data=(X_val, Y_val))  
    model.save_weights('model_weights.h5', overwrite=True)  
    return model  
  
def test_model(model,X,Y):  
    model.load_weights('model_weights.h5')  
    score = model.evaluate(X, Y, verbose=0)
    return score  
  
if __name__ == '__main__':  
    # the data, shuffled and split between tran and test sets  
    (X_train, y_train), (X_val, y_val),(X_test, y_test) = load_data('olivettifaces.gif')  
    
    if K.image_data_format() == 'channels_first':
        X_train = X_train.reshape(X_train.shape[0], 1, img_rows, img_cols)  
        X_val = X_val.reshape(X_val.shape[0], 1, img_rows, img_cols)  
        X_test = X_test.reshape(X_test.shape[0], 1, img_rows, img_cols)  
        input_shape = (1, img_rows, img_cols)
    else:
        X_train = X_train.reshape(X_train.shape[0], img_rows, img_cols, 1)  
        X_val = X_val.reshape(X_val.shape[0], img_rows, img_cols, 1)  
        X_test = X_test.reshape(X_test.shape[0], img_rows, img_cols, 1)  
        input_shape = (img_rows, img_cols, 1) # 1 为图像像素深度
    
    print('X_train shape:', X_train.shape)
    print(X_train.shape[0], 'train samples') 
    print(X_val.shape[0], 'validate samples')  
    print(X_test.shape[0], 'test samples')
  
    # convert class vectors to binary class matrices  
    Y_train = np_utils.to_categorical(y_train, nb_classes)  
    Y_val = np_utils.to_categorical(y_val, nb_classes)  
    Y_test = np_utils.to_categorical(y_test, nb_classes)  
  
    model = set_model()
    train_model(model, X_train, Y_train, X_val, Y_val)   
    score = test_model(model, X_test, Y_test)  
  
    model.load_weights('model_weights.h5')  
    classes = model.predict_classes(X_test, verbose=0)  
    test_accuracy = np.mean(np.equal(y_test, classes))  
    print("accuarcy:", test_accuracy)
    for i in range(0,40):
        if y_test[i] != classes[i]:
            print(y_test[i], '被错误分成', classes[i]);
    
图1

 代码解析

拿到代码首先我们从主函数看起:

Ⅰ将图片分为训练集,内测集(验证集),公测集(测试集),每人10张图,相应的布局是8+1+1。

 (X_train, y_train), (X_val, y_val),(X_test, y_test) = load_data('olivettifaces.gif')  

进入load_data函数我们来看:


def load_data(dataset_path):
    img = Image.open(dataset_path)
    #  asarray,将数据转化为np.ndarray,但使用原内存 ,/255用来做归一化处理
    img_ndarray = np.asarray(img, dtype='float64') / 255  

    # 400 pictures, size: 57*47 = 2679,每张图2679个像素点
    #初始化一个矩阵400*2679  每一行用于存放一张人脸
    faces = np.empty((400, 2679))
    
    # flatten将多维数组降为一维
    for row in range(20):
        for column in range(20):
            faces[row * 20 + column] = np.ndarray.flatten(img_ndarray[row * 57: (row + 1) * 57, column * 47: (column + 1) * 47])
            

    #每个人一个标签,标签记为他的index(0--39)
    label = np.empty(400)
    for i in range(40):
        label[i * 10: i * 10 + 10] = i#每十个图的标签一样
    label = label.astype(np.int)

    # train:320,valid:40,test:40
    train_data = np.empty((320, 2679))
    train_label = np.empty(320)
    valid_data = np.empty((40, 2679))
    valid_label = np.empty(40)
    test_data = np.empty((40, 2679))
    test_label = np.empty(40)

    for i in range(40):
        train_data[i * 8: i * 8 + 8] = faces[i * 10: i * 10 + 8]  # 训练集中的数据
        train_label[i * 8: i * 8 + 8] = label[i * 10: i * 10 + 8]  # 训练集对应的标签
        valid_data[i] = faces[i * 10 + 8]  # 验证集中的数据
        valid_label[i] = label[i * 10 + 8]  # 验证集对应的标签
        test_data[i] = faces[i * 10 + 9]# 测试集中的数据
        test_label[i] = label[i * 10 + 9]# 测试集对应的标签

    #一开始用float64,后来再搞成float32,可以避免掉一些精度丢失,使得结果更精确
    train_data = train_data.astype('float32')
    valid_data = valid_data.astype('float32')
    test_data = test_data.astype('float32')

    rval = [(train_data, train_label), (valid_data, valid_label), (test_data, test_label)]
    return rval

load_data函数调用完实际上是对数据即图片进行了预处理。

 

Ⅱ根据channels_first和channels_last的不同即图像维度顺序的不同,进行安排,并输出响应的信息。

X_train shape: (320, 57, 47, 1)
320 train samples
40 validate samples
40 test samples
Train on 320 samples, validate on 40 samples
Epoch 1/40

if K.image_data_format() == 'channels_first':
        X_train = X_train.reshape(X_train.shape[0], 1, img_rows, img_cols)  
        X_val = X_val.reshape(X_val.shape[0], 1, img_rows, img_cols)  
        X_test = X_test.reshape(X_test.shape[0], 1, img_rows, img_cols)  
        input_shape = (1, img_rows, img_cols)
else:
        X_train = X_train.reshape(X_train.shape[0], img_rows, img_cols, 1)  
        X_val = X_val.reshape(X_val.shape[0], img_rows, img_cols, 1)  
        X_test = X_test.reshape(X_test.shape[0], img_rows, img_cols, 1)  
        input_shape = (img_rows, img_cols, 1) # 1 为图像像素深度

print('X_train shape:', X_train.shape)
print(X_train.shape[0], 'train samples')
print(X_val.shape[0], 'validate samples')
print(X_test.shape[0], 'test samples')

Ⅲ标签为多类模式,即one-hot编码的向量,而不是单个数值.可以使用工具中的to_categorical函数完成该转换

# convert class vectors to binary class matrices
Y_train = np_utils.to_categorical(y_train, nb_classes)
Y_val = np_utils.to_categorical(y_val, nb_classes)
Y_test = np_utils.to_categorical(y_test, nb_classes)

Ⅳ建立网络,训练,验证,测试。输出准确率。

model = set_model()
train_model(model, X_train, Y_train, X_val, Y_val)    
score = test_model(model, X_test, Y_test)    
#print(model.summary())   #打印网络结构 
model.load_weights('model_weights.h5')    
classes = model.predict_classes(X_test, verbose=0)    
test_accuracy = np.mean(np.equal(y_test, classes))    
print("accuarcy:", test_accuracy)
for i in range(0, 40):
    if y_test[i] != classes[i]:
        print(y_test[i], '被错误分成', classes[i]);

我们先看建立网络:

图2

 每一层的配置信息:

图3

我们先用5个3×3的卷积核对57×47的图进行卷积,这时候,我们得到了新图---55(行)×45(列)×5(通道数)。

对于每一个通道来讲,共有55×45个神经元。因为卷积核3×3,所以9个参数公用,对于每一个神经元我们不要忽略掉w0这个参数,因为x0默认为1,所以w0也可以叫做偏置,其实每个神经元就有10个参数,每个神经元的参数除了偏置外的其他9个都是一样的。所以一共有  9(公用)+55×45=2484个参数。但为了进一步减少参数,通常我们这样简化:由同一个卷积核得到的神经元的偏置也设置为同一个参数,即9(×1)+1=10个。(注:×1是原图的通道数)下面若不特殊说明,我们就默认把偏置也考虑成公有来阐述。

再考虑有5个卷积核形成5个通道,所以这层一共有50个参数。详细见图4

图4

同理:面对27(行)×22(列)×5(通道数)用10个3×3的卷积核卷积后得到25(行)×20(列)×10(通道数)。

每一个卷积核分别卷积5个通道再加和,具体实现见模型图。(9×5+1)×10=460个参数,详见图5

 

图5

多通道多个卷积核
上面的例子展示了在5个通道上的卷积操作,有10个卷积核,生成10个通道。

其中需要注意的是,所谓的1个卷积核,其实是一个5*3*3的卷积核,5代表一开始卷积的通道数,3*3是卷积核的尺寸,实际卷积的时候其实就是5个3*3的卷积核(这5个3*3的卷积核的参数是不同的)分别去卷积对应的5个通道,然后相加,再加上偏置b,注意b对于这5通道而言是共享的,所以b的个数=最终的通道的个数=卷积核的个数。

def set_model(lr=0.005, decay=1e-6, momentum=0.9):
    model = Sequential()
    if K.image_data_format() == 'channels_first':
        model.add(Conv2D(5, kernel_size=(3, 3), input_shape=(1, img_rows, img_cols)))
    else:
        model.add(Conv2D(5, kernel_size=(3, 3), input_shape=(img_rows, img_cols, 1)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Conv2D(10, kernel_size=(3, 3)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    model.add(Flatten())
    model.add(Dense(1000))  # Full connection
    model.add(Activation('tanh'))
    model.add(Dropout(0.5))
    model.add(Dense(nb_classes))
    model.add(Activation('softmax'))
    sgd = SGD(lr=lr, decay=decay, momentum=momentum, nesterov=True)
    model.compile(loss='categorical_crossentropy', optimizer=sgd)
    return model

 

代码优化和注意

人与别人本就是不同的,依赖于少量的不同就能区分开两个人。而人与自己的相同之处是我们应该关注的,我们应该先将人的与自己的相同之处考虑完,能够识别他是他,再去考虑他与别人的不同(这是老师讲的,我还没搞懂这是为啥emm)

当一个网络搭建完准确率不高的时候,应该着手于调整网络结构,当准确率达到90%(这个阈值自己把握)以上,应该关注调参。

1.卷积核数目的选择

一开始我们选择5个卷积核,再用10个卷积核。是考虑到人有五官,而我们靠五官来认识一个人。而事实上将卷积核调整到先10后20,我们在实验上取得了更好的效果。这是因为一开始用的5个卷积核是远远不够的,事实上我们认识人并不仅仅依赖于五官,比如还有瞳距,眉间距等,五个卷积核远远不够,所以应该适量的增大卷积核。

2.卷积核大小的选择

我们将3*3的卷积核大小改为5*5,发现效果变差。

卷积核大小意味着提取特征的大小,所以卷积核的大小是和要提取的特征大小有关的,并且还与整张图的大小有关。试想一下,有一张100*100的人脸图,我们在上面提取人脸上的一颗痣,那我们的卷积核应该选择多大呢?再对比当下的57*47提取人脸特征来说,我们的卷积核又应该选多大呢?这就是信息密度小和大的区别了。信息密度小的时候可以选择适当大的卷积核来提取特征,而信息密度大的时候,如果选择过大的卷积核往往会提取不清,造成不好的效果。【试想一下在47*57的图像里人的眼珠只有几个像素点,如果提取眼珠的特征的时候,我们用了较大的卷积核,那么可能卷积核的大小甚至包涵了眼睑甚至眉毛】

在信息密度比较大的当下例子里,人与自己的图像尽管表情不一,但是再不一致的表情,在微元的思想下,也是一致的,如果卷积核取的小,那么哈哈大笑的嘴角和微微上扬的嘴角也会是一样,这样,每一处微小的特征组合起来,所以尽管人表情不一,也能将人把自己识别出来。

但是会有人发问:那么每个人的微元和别人的岂不是也一样?也许上述的调参把自己能识别出了,甚至把别人也识别成了自己,那么下面就要抓住自己和别人的不同,把自己和别人区分开。

3.激励函数的选择

我们在这里选择tanh的激励函数,是因为relu、sigmoid等,在图像上很明显的没有负值的效果,也就是说图与图的相同之处大家都有“支持”的意见,在图与图的不同的对比上只有tanh有“反对”的意见,其他激励函数做到是不那么有力的“支持”。为了把不同之处区别开来,我们选择对于不同勇敢发声的tanh

值得注意的是:以上所有参数的调整都是有相互制约的作用,互相牵制,互相影响,调整一个参数,往往需要回头调整之前调整过的参数。

4.池化大小以及层数 

池化就是抓出最重要的特征,丢弃掉不重要的特征。说白了,就是一个舍弃的过程,如果采用2*2 的池化,也就是丢了75%的特征,如果采用5*5,也就是丢弃了24/25的特征这是很恐怖的,也就是基本上所有特征都丢弃了,一般我们采用2*2的池化,至于层数,有时候一层卷积一层池化,有时候几层卷积一层池化,至于几层池化一起做的情况我们没必要使用,因为这个和增大池化大小没什么区别。至于卷积和池化的配合是怎样的,这个我是在实验中去实践的,有时候很难定量的凭空喊出我们在意的特征到底是多少,所以唯结果论的指导也非常重要。

5.dropout

dropout本来是为了处理过拟合问题的措施,但在这里也同时避免了欠拟合的问题。至于%的选择,就要考虑样本的大小和网络的参数个数了。但是值得注意的是在代码实现的时候,为了方便调参,我们设定随机数种子,让每次运行程序的时候,产生的随机数序列是一致的,这次运行程序中的每次dropout形成的网络都是一样的。

如果不这样设定,例如:调参A之前准确率85%,改变参A之后,重新跑代码,准确率95%。那么我们也不能确认这是参数不一样的作用,因为两次跑代码的过程中,dropout形成的网络也不一样了,所以我们用如下操作;

np.random.seed(1337)  # 用于指定随机数生成时所用算法开始的整数值,如果使用相同的seed()值,则每次生成的随即数都相同

确保每次跑代码dropout的结果都和上次跑的是一样的,这并不影响dropout的初衷,因为确确实实dropout随机剪掉了一些网络线,我们这样做是为了便于调参而已。

6.关于学习率,训练轮数

我常常采用比较大的学习率和较多的学习轮数,目的在忽略掉小波折,找到曲线收敛的位置,大致确定好曲线在哪个地方收敛,再降低学习率,慢慢找到loss的波谷。

7.batch_size和喂数据顺序

总的训练样本个数是一定的,所以确定了batch_size,也就确定了一次epoch喂几次数据了。关于这两个参数的权衡我没有采用极端,但是最佳的点在哪里,我还是用来实验和为结果论做指导的。但是有一点要注意就是喂数据的顺序。

假设我们每次喂40个数据(batch_size=40),每个人也就是8张图,也就是5个人的量。M(n)表示第M个人的第N个数据

我们有这样的喂法:

Ⅰ每次喂数据规定好是哪五个人,然后针对这5个人,按照这样的顺序:

第0个人的0号照片,第1个人的0号照片,第2个人的0号照片,第3个人的0号照片,,,

0(0)  1(0)  2(0)  3(0)  4(0)
0(1)  1(1)  2(1)  3(1)  4(1)
0(2)  1(2)  2(2)  3(2)  4(2)
0(3)  1(3)  2(3)  3(3)  4(3)
0(4)  1(4)  2(4)  3(4)  4(4)
............................

0(7)  1(7)  2(7)  3(7)  4(7)

Ⅱ每次喂数据规定好是哪五个人,然后针对这5个人,按照这样的顺序:

第0个人的0号照片,第0个人的1号照片,第0个人的2号照片,第0个人的3号照片,,,

0(0)  0(1)  0(2)  0(3)  0(4)  0(5)  0(6)  0(7)
......................................................

 Ⅲ 每次喂数据规定好是哪五个人,然后针对这5个人的一共四十张图,按照无序来喂

不举例子

Ⅳ 每次喂数据完全无序,每个网络随便喂进320张图中的随便40张

不举例子

 针对于这些情况:

对于Ⅳ,如果完全打乱,不能保证每次喂数据对某一个人有充足的样本进行训练,最差情况比如:每次喂数据对每个人都只有一张照片,共40张,这样的效果很差,每次喂的数据找不到认出其中一个人的足够的特征,我们早就讲过要多多关注怎样识别出自己,再去区分自己与别人的不同。所以每次喂应该给安排对于一个人足够的数据,所以我们排除Ⅳ这种情况。面对前三种情况,我们先来分析Ⅱ号,Ⅱ不可取,这样输入完一个人,参数就相当于只适应了这一个人,再输入另一个人的时候,参数又会去适应另一个人。对于Ⅲ:无序状态下,可能会随机出一个人的几张照片相邻的情况,就会出现Ⅱ号的局面。对于Ⅰ:是最中肯的输入数据的顺序。如果每次喂的数据不止40,足够大时候,那么Ⅲ和Ⅰ基本一样了,那时候Ⅲ随机出一个人的几张照片相邻的情况比较小概率。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值