Fork me on GitHub

深度学习之训练一个人脸性别分类器

0. Whatever


深度学习越来越火,人人都能玩!

在本文中我将通过深度学习方法训练一个性别分类器,即给定一张人脸图像,判断其性别。

本文使用的深度学习框架:基于TheanoKeras(github:keras

1. Data Preprocess


数据来源:互联网明星图片。男性照片25个类,共3690张;女性照片15个类,共4309张。所有图片经过人脸检测对齐归一化到141*165尺寸大小。样例如下:

female-face

生成图片路径和label文件格式如下:

...
E:\face data\gender_pic\female\163\38243_big.jpg,0
E:\face data\gender_pic\female\163\38244_big.jpg,0
E:\face data\gender_pic\female\163\38246_big.jpg,0
E:\face data\gender_pic\female\163\38249_big.jpg,0
E:\face data\gender_pic\male\1\107_big.jpg,1
E:\face data\gender_pic\male\1\10_big.jpg,1
E:\face data\gender_pic\male\1\114_big.jpg,1
E:\face data\gender_pic\male\1\117_big.jpg,1
...

本文中用到的数据集存在很多不足:类别太少、年龄分布不均匀等等,但是不管怎么样,先跑起来再说。下面通过csv文件载入所有人脸图片和label信息,shuffle并划分训练集和测试集。

from PIL import Image
from sklearn.cross_validation import train_test_split
from sklearn.utils import shuffle

def load_data(csv_file):
    x = []
    y = []
    with open(csv_file, 'r') as f:
        for line in f:
            img = Image.open(line[:-4])
            img.load()
            r, g, b = img.split()
            channel = []
            channel.append(np.array(r))
            channel.append(np.array(g))
            channel.append(np.array(b))
            x.append(channel)
            y.append(line[-3])
    return np.array(x), np.array(y)


X, y = load_data(csv_file)
X = X.reshape(-1, 3*141*165)
X, y = shuffle(X, y, random_state=55)
x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=57)
x_train = x_train.reshape(-1, 3, 141, 165)
x_test = x_test.reshape(-1, 3, 141, 165)

X_train = x_train.astype("float32")
X_test = x_test.astype("float32")
X_train /= 255
X_test /= 255

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

2. Network Architecture


四个卷积层加上两个全连接层,实际上这是一个比较大的模型,在数据集较小的情况下很容易过拟合,但是有很多防止过拟合的手段值得我们探究。

from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation, Flatten
from keras.layers.convolutional import Convolution2D, MaxPooling2D

nb_classes = 2
model = Sequential()

model.add(Convolution2D(32, 3, 12, 12, border_mode='full'))
model.add(Activation('relu'))
model.add(MaxPooling2D(poolsize=(2, 2)))
model.add(Dropout(0.25))

model.add(Convolution2D(48, 32, 5, 5))
model.add(Activation('relu'))
model.add(MaxPooling2D(poolsize=(2, 2)))
model.add(Dropout(0.25))

model.add(Convolution2D(64, 48, 3, 3, border_mode='full'))
model.add(Activation('relu'))
model.add(MaxPooling2D(poolsize=(2, 2)))
model.add(Dropout(0.25))

model.add(Convolution2D(80, 64, 3, 3))
model.add(Activation('relu'))
model.add(MaxPooling2D(poolsize=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(80*8*10, 1000))
model.add(Activation('relu'))
model.add(BatchNormalization((1000,)))
model.add(Dropout(0.5))

model.add(Dense(1000, 1000))
model.add(Activation('relu'))
model.add(BatchNormalization((1000,)))
model.add(Dropout(0.5))

model.add(Dense(1000, nb_classes))
model.add(Activation('softmax'))

3. 训练与测试模型


训练卷积神经网络涉及很多名词,下面一一简单介绍:

梯度下降流:一种类似于下山的优化方法,每更新一次模型就相当于往前走一步,通过梯度计算前进方向,这样一步一步逼近山谷(即最优点)。

mini-batch:在神经网络的训练中,数据集往往比较大(几万张甚至几百万张图片),如果按照传统方法遍历完所有数据后计算梯度然后再更新模型的话会非常慢,因此才有了mini-batch方法,即将数据集分为一个个小batch,每遍历完一个batch更新一次模型,更新完所有batch为一次迭代。

momentum:顾名思义,给梯度下降流一个动量,或者说是加速度

$$v(t) = \alpha v(t-1) - \beta \frac{\partial E}{\partial w}(t)$$

其中$\alpha$是momentum factor(略小于1),$\beta$是learning rate.

引入momentum可以使训练过程加速度过平坦区域和阻尼振荡区域,并允许我们使用更大的学习率。

Adagrad:一种自适应的基于梯度下降流的更新梯度方法,好处是不用自己调整学习率和momentum。但是这种方法对初始参数及其对应的梯度很敏感。

adagrad

Adadelta:在Adagrad方法上的继续改进,解决了Adagrad方法对初始参数敏感的问题。

RMS

adadelta

early stop:在训练过程中,一旦模型性能超过patience(自设参数)次数没有提高,那么我们可以认为模型开始过拟合了,此时可以停止继续训练。

batch normalization:简单的说,这个方法就是normalizing layer inputs。好处是不仅可以使模型加速收敛,还能起到regularizer的作用。

from keras.optimizers import SGD, Adadelta, Adagrad
from keras.utils import np_utils, generic_utils
from keras.callbacks import ModelCheckpoint, EarlyStopping
from keras.layers.normalization import BatchNormalization

opt = Adadelta()

fname = 'gender_best_weights.hdf5'
if os.path.isfile(fname):
    model.load_weights(fname)
    print "load weights successful!"
model.compile(loss='categorical_crossentropy', optimizer=opt)

print "X_train shape: {0}".format(X_train.shape)
print "y_train shape: {0}".format(Y_train.shape)
print "X_test shape: {0}".format(X_test.shape)
print "y_test shape: {0}".format(Y_test.shape)

checkpointer = ModelCheckpoint(filepath=fname, verbose=1, save_best_only=True)
early_stop = EarlyStopping(patience=20, verbose=1)
model.fit(X_train, Y_train, batch_size=128, nb_epoch=100, show_accuracy=True, verbose=1, validation_data=(X_test, Y_test), callbacks=[checkpointer, early_stop])

score = model.evaluate(X_test, Y_test, batch_size=128, show_accuracy=True, verbose=1)
print "Test score: {0}".format(score)

4. 结果


共迭代49次,在第29次迭代得到best model,在测试集上的准确率是91.6%

1600/1600 [==============================] - 20s - loss: 0.3370 - acc.: 0.9163

Test score: (0.33696633219718936, 0.91625000000000001)

显然还有很多可以提升的地方,有时间继续改进。

源代码在这里:戳我

参考文献

  1. Batch normalization: Accelerating deep network training by reducing internal covariate shift

  2. ADADELTA: An adaptive learning rate method

转载请注明出处:BackNode

My zhiFuBao

Buy me a cup of coffee

blogroll

social