提升keras准确率和速度的小tips

这里记录一下对于新手(对,说的就是本人)学习kears框架时用来提升准确率的一些tip,但这里都是"术"的层面,而对于"道",还是要看数学.全文以深度学习界的"hello world"-手写数字识别为例.

首先载入所需要的库:

import numpy as np
from keras.datasets import mnist
from keras.optimizers import SGD
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers.core import Dense,Dropout,Activation

然后编写函数加载数据:

def load_data():
  (x_train,y_train),(x_test,y_test) = mnist.load_data()
  number = 10000
  x_train = x_train[:number] # 完整训练数据有6w,这里取前1w
  y_train = y_train[:number]  
  x_train = x_train.reshape(number,28*28) # 原始数据是3维,这里变成2维
  x_test=x_test.reshape(x_test.shape[0],28*28)
  x_train = x_train.astype('float32')
  x_test = x_test.astype('float32')
  y_train = np_utils.to_categorical(y_train,10) # 原始数据是1,2...9这样的数字,to_categorical将其变成向量,对应的数字位置为1,其余为0
  y_test = np_utils.to_categorical(y_test,10)
  x_train = x_train / 255
  x_test = x_test / 255
  return (x_train,y_train),(x_test,y_test)

(x_train,y_train),(x_test,y_test) = load_data()

选择合适的loss函数

对于loss函数,如果之前有学过经典机器学习的小伙伴一定最熟悉MSE(均方误差),所以先用这个实现一个版本:

model = Sequential()
model.add(Dense(input_dim=28*28,units=689,activation='sigmoid'))
model.add(Dense(units=689,activation='sigmoid'))
model.add(Dense(units=689,activation='sigmoid'))
model.add(Dense(units=10,activation='softmax')) # 输出层10个节点
model.compile(loss='mse',optimizer=SGD(lr=0.1),metrics=['accuracy'])
model.fit(x_train,y_train,batch_size=100,epochs=20)

train_result = model.evaluate(x_train,y_train,batch_size=10000)
test_result = model.evaluate(x_test,y_test,batch_size=10000)
print('Train Accc:',train_result[1])
print('Test Accc:',test_result[1])

这里activation函数使用sigmoid,optimizer使用SGD(随机梯度下降),loss选择MSE,2个隐藏层,隐藏层节点数量689,程序输出如下:

Train Accc: 0.12860000133514404
Test Accc: 0.1362999975681305

这里可以看出,基本凉凉.不论是在测试集还是训练集准确度都很低.但是,如果把loss函数换成categorical_crossentropy,输出就变成:

Train Accc: 0.8550000190734863
Test Accc: 0.8374000191688538

很明显的提升,这也说明,MSE对于分类问题不是很有好.

合适的隐藏层数量

对于初学者有种幻想,层数越多精度就会越高.这里增加一下层数试试:

model = Sequential()
model.add(Dense(input_dim=28*28,units=689,activation='sigmoid'))
for _ in range(10):
    model.add(Dense(units=689,activation='sigmoid')) # 来个10层
model.add(Dense(units=10,activation='softmax')) # 输出层10个节点
model.compile(loss='categorical_crossentropy',optimizer=SGD(lr=0.1),metrics=['accuracy'])
model.fit(x_train,y_train,batch_size=100,epochs=20)

train_result = model.evaluate(x_train,y_train,batch_size=10000)
test_result = model.evaluate(x_test,y_test,batch_size=10000)
print('Train Accc:',train_result[1])
print('Test Accc:',test_result[1])

结果如下:

Train Accc: 0.09910000115633011
Test Accc: 0.10320000350475311

WTF?!准确度反而降低了??这里其实和sigmoid这个函数有关,这个函数会导致vanish gradient problem.简言之就是使用这个函数进行训练时层数越多,每次参数变化引起结果变化的程度就越小,因为sigmoid函数会把不论大小的输入都转化到0-1这个区间中,具体看其函数图像就可以明白了.

合适的激活函数

sigmoid函数其实比较少用了,现在更常用的是relu函数,可以避免vanish gradient problem.此外,relu其实是Maxout的一个特例:

model = Sequential()
model.add(Dense(input_dim=28*28,units=689,activation='relu'))
for _ in range(10):
    model.add(Dense(units=689,activation='relu')) # 来个10层
model.add(Dense(units=10,activation='softmax')) # 输出层10个节点
model.compile(loss='categorical_crossentropy',optimizer=SGD(lr=0.1),metrics=['accuracy'])
model.fit(x_train,y_train,batch_size=100,epochs=20)

train_result = model.evaluate(x_train,y_train,batch_size=10000)
test_result = model.evaluate(x_test,y_test,batch_size=10000)
print('Train Accc:',train_result[1])
print('Test Accc:',test_result[1])

结果如下,可以看到增加10层使用relu函数不受影响.

Train Accc: 0.9959999918937683
Test Accc: 0.9541000127792358

合适的batch_size

batch_size影响每次训练使用的数据量,比如极端情况下:

model = Sequential()
model.add(Dense(input_dim=28*28,units=689,activation='relu'))
for _ in range(10):
    model.add(Dense(units=689,activation='relu')) # 来个10层
model.add(Dense(units=10,activation='softmax')) # 输出层10个节点
model.compile(loss='categorical_crossentropy',optimizer=SGD(lr=0.1),metrics=['accuracy'])
model.fit(x_train,y_train,batch_size=10000,epochs=20)

train_result = model.evaluate(x_train,y_train,batch_size=10000)
test_result = model.evaluate(x_test,y_test,batch_size=10000)
print('Train Accc:',train_result[1])
print('Test Accc:',test_result[1])

这里把batch_size的值改成和整个训练集一样大,结果如下:

Train Accc: 0.2732999920845032
Test Accc: 0.26249998807907104

又凉凉了,所以 batch_size 过大速度快,但会影响精度.而过小则速度会慢,特别是使用GPU的时候,如果这个值设定的过小不能完全发挥GPU的加速功能.

合适的optimizer

目前最常用的优化函数是adam,adam=RMSProp+Momentum,这里替换掉SGD:

model = Sequential()
model.add(Dense(input_dim=28*28,units=689,activation='relu'))
for _ in range(10):
    model.add(Dense(units=689,activation='relu')) # 来个10层
model.add(Dense(units=10,activation='softmax')) # 输出层10个节点
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
model.fit(x_train,y_train,batch_size=100,epochs=20)

train_result = model.evaluate(x_train,y_train,batch_size=10000)
test_result = model.evaluate(x_test,y_test,batch_size=10000)
print('Train Accc:',train_result[1])
print('Test Accc:',test_result[1])

输出如下,精度差不多但是训练的速度提升了很多:

Train Accc: 0.9906999754905701
Test Accc: 0.9217000007629395

Dropout层

当训练样本过少时候,往往会出现过拟合的现象,这时可以使用Dropout层来"限制学习能力".这个方法原理是在每次更新参数之前根据概率丢掉某些neuron,使整个网络结构发生了改变,在每一个mini-batch上重复这个行为,得到不同的结果,相当于训练出了很多个不同的网络,然后再把结果平均得到最终结果(ensemble的理念).这个方法会降低在训练集上的精准度.

为了模拟过拟合,我们在处理数据集时候添加噪声:

def load_data():
  (x_train,y_train),(x_test,y_test) = mnist.load_data()
  number = 10000
  x_train = x_train[:number] # 完整训练数据有6w,这里取前1w
  y_train = y_train[:number]  
  x_train = x_train.reshape(number,28*28) # 原始数据是3维,这里变成2维
  x_test=x_test.reshape(x_test.shape[0],28*28)
  x_train = x_train.astype('float32')
  x_test = x_test.astype('float32')
  y_train = np_utils.to_categorical(y_train,10) # 原始数据是1,2...9这样的数字,to_categorical将其变成向量,对应的数字位置为1,其余为0
  y_test = np_utils.to_categorical(y_test,10)
  x_train = x_train / 255
  x_test = x_test / 255
  x_test=np.random.normal(x_test) # 加噪声
  return (x_train,y_train),(x_test,y_test)

(x_train,y_train),(x_test,y_test) = load_data()

然后经过2层relu+adam+categorical_crossentropy+batch_size=100结果如下:

Train Accc: 0.9894000291824341
Test Accc:  0.5034999847412109

可以看到训练集精度很高而测试集准确度一般,添加Dropout层:

model = Sequential()
model.add(Dense(input_dim=28*28,units=689,activation='relu'))
model.add(Dropout(0.7))
model.add(Dense(units=689,activation='relu'))
model.add(Dropout(0.7))
model.add(Dense(units=689,activation='relu'))
model.add(Dropout(0.7))
model.add(Dense(units=10,activation='softmax')) # 输出层10个节点
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
model.fit(x_train,y_train,batch_size=100,epochs=20)

train_result = model.evaluate(x_train,y_train,batch_size=10000)
test_result = model.evaluate(x_test,y_test,batch_size=10000)
print('Train Accc:',train_result[1])
print('Test Accc:',test_result[1])

其中Dropout层添加到每个隐藏层之间,常用的概率值是0.5左右,结果如下:

Train Accc: 0.9824030212854031
Test Accc: 0.6224999713897705

可以看到测试集精度提升了一些.当然,除了Dropout之外可以Early Stopping和Regularization(正则化,简单说就是使用某种方法使结果越来越接近0,Weight Decay,但这个帮助并不显著.)

特征工程!

在上面的测试准确率可以达到90%以上,但细心的小伙伴应该发现了,在加载数据时候有这样2行代码:

x_train = x_train / 255
x_test = x_test / 255

这两行代码就是对原始数据进行了缩放,将原始值在0-255之间的数据转化到0-1这个区间.如果没有这个处理,那么依然使用2层relu+adam+categorical_crossentropy+batch_size=100结果如下:

Train Accc: 0.10010000318288803
Test Accc: 0.09799999743700027

准确度居然降到和sigmoid+SGD+mse差不多的程度.所以,如果当程序在训练集上的准确度都很低的话,除了调整参数还需要进一步考虑特征工程是否合理合适了.