Keras使用进阶(Ⅰ)
作者&编辑:李中梁
前言
在一年多前笔者写过一篇keras使用入门指南:超快速!10分钟入门Keras指南。忆往事悠悠,某日打开自己博客发现文章阅读量快3W了。恰好这大半年的research也用到了keras高级点的特性,寻思着是时候写一篇keras使用进阶的文章来记录下自己“摸鱼”的日子......
用keras训练多标签数据
通常用keras做分类任务,一张图像往往只对应着一种类别,但是在实际的问题中,可能你需要预测出一张图像的多种属性。例如在pyimagesearch的《multi-label-classification-with-keras》这篇文章中提出了一个衣服数据集,整个数据集有两种属性,一种是颜色(blue, red, black),另一种是衣服的类型(dress, jeans, shirt) 。如假设one-hot-vector编码顺序是(blue, red, black, dress, jeans, shirt)则black jeans的 label就是[0,0,1,0,1,0]。
那么面对这样的多标签任务如何使用keras进行CNN模型的搭建与训练呢?
首先我们搭建一个单输入(一张图像)多输出(图像的多个属性,比如衣服的颜色,类型)的CNN。
def GenModel(img_width = 512, img_height = 512 , model_name = 'AlexNet'):
# 指定输入图像大小
image_input_shape = (img_width, img_height, 3)
# 定义网络(AlexNet)
if model_name == 'AlexNet':
print('\n--------Start build model ', model_name, '--------\n')
# 定义网络输入
image_input = Input(shape=image_input_shape, name='image_input')
# 定义主卷积(图像)网络卷积->激活->池化
conv_image = Conv2D(96, (11, 11), strides = (4, 4), padding = 'valid', activation = 'relu')(image_input)
conv_image = MaxPooling2D(pool_size = (3, 3), strides = (2, 2))(conv_image)
conv_image = Conv2D(256, (5, 5), strides = (1, 1), padding = 'same', activation = 'relu')(conv_image)
conv_image = MaxPooling2D(pool_size = (3, 3), strides = (2, 2))(conv_image)
conv_image = Conv2D(384, (3,3), strides = (1, 1), padding = 'same', activation = 'relu')(conv_image)
conv_image = Conv2D(384, (3,3), strides = (1, 1), padding = 'same', activation = 'relu')(conv_image)
conv_image = Conv2D(384, (3,3), strides = (1, 1), padding = 'same', activation = 'relu')(conv_image)
conv_image = MaxPooling2D(pool_size = (3, 3), strides = (2, 2))(conv_image)
conv_image = Flatten()(conv_image)
# 定义2个输出,分别用于预测衣服的颜色和类型
out_color = Dense(4096, activation='relu',)(conv_image)
out_color = Dense(512, activation='relu',)(out_color)
out_color = Dense(3, activation='sigmoid', name='out_age')(out_color)
out_type = Dense(4096, activation='relu',)(conv_image)
out_type = Dense(512, activation='relu',)(out_sex)
out_type = Dense(3, activation='sigmoid', name='out_sex')(out_sex)
# 定义整个单输入,2输出的模型
model=Model(inputs = image_input, outputs = [out_color, out_type])
return model
然后对模型进行编译
# 编译模型,定义损失函数
opt=Adadelta()
print('\n-------- optimizer: %s --------\n'%(opt.__class__.__name__) )
model.compile( optimizer = opt,
loss = {'out_color': 'categorical_crossentropy', 'out_type': 'categorical_crossentropy'},
loss_weights = {'out_color': out_color_weight, 'out_type': out_type_weight, metrics = ['accuracy']) # 这里loss_weights需要自己手动设定下
最后将数据集载入模型进行训练和预测
# grab the image paths and randomly shuffle them
print("[INFO] loading images...")
imagePaths = sorted(list(paths.list_images(args["dataset"])))
random.seed(42)
random.shuffle(imagePaths)
# initialize the data and labels
data = []
labels = []
# loop over the input images
for imagePath in imagePaths:
# load the image, pre-process it, and store it in the data list
image = cv2.imread(imagePath)
image = cv2.resize(image, (IMAGE_DIMS[1], IMAGE_DIMS[0]))
image = img_to_array(image)
data.append(image)
# extract set of class labels from the image path and update the
# labels list
l = label = imagePath.split(os.path.sep)[-2].split("_")
labels.append(l)
# scale the raw pixel intensities to the range [0, 1]
data = np.array(data, dtype="float") / 255.0
labels = np.array(labels)
print("[INFO] data matrix: {} images ({:.2f}MB)".format(
len(imagePaths), data.nbytes / (1024 * 1000.0)))
print(labels)
# binarize the labels using scikit-learn's special multi-label
# binarizer implementation
print("[INFO] class labels:")
mlb = MultiLabelBinarizer()
labels = mlb.fit_transform(labels)
print(labels)
# loop over each of the possible class labels and show them
for (i, label) in enumerate(mlb.classes_):
print("{}. {}".format(i + 1, label))
# partition the data into training and testing splits using 80% of
# the data for training and the remaining 20% for testing
(trainX, testX, trainY, testY) = train_test_split(data,
labels, test_size=0.2, random_state=42)
# construct the image generator for data augmentation
aug = ImageDataGenerator(rotation_range=25, width_shift_range=0.1,
height_shift_range=0.1, shear_range=0.2, zoom_range=0.2,
horizontal_flip=True, fill_mode="nearest")
# train the network
print("[INFO] training network...")
H = model.fit_generator(
aug.flow(trainX, trainY, batch_size=BS),
validation_data=(testX, testY),
steps_per_epoch=len(trainX) // BS,
epochs=EPOCHS, verbose=1)
使用Lambda层让你的keras网络更加灵活
keras的lambda层可以算是自定义keras的神器,一起来看下。
keras的Lambda层的导入方法和函数原型:
from keras.layers.core import Lambda
keras.layers.core.Lambda(function, output_shape=None, mask=None, arguments=None)
参数的含义:
function: 要实现的函数,该函数仅接受一个变量,即上一层的输出
output_shape: 函数应该返回值的shape,可以是一个tuple,也可以是一个根据输入shape`
mask: 掩膜
arguments: 可选参数,字典,用来记录向函数中传递的其他关键字参数
例子:
# 增加一个x -> x^2 的层
model.add(Lambda(lambda x: x ** 2))
# add a layer that returns the concatenation
# of the positive part of the input and
# the opposite of the negative part
def antirectifier(x):
x -= K.mean(x, axis=1, keepdims=True)
x = K.l2_normalize(x, axis=1)
pos = K.relu(x)
neg = K.relu(-x)
return K.concatenate([pos, neg], axis=1)
def antirectifier_output_shape(input_shape):
shape = list(input_shape)
assert len(shape) == 2 # only valid for 2D tensors
shape[-1] *= 2
return tuple(shape)
model.add(Lambda(antirectifier,
output_shape=antirectifier_output_shape))
# 对于简单的定制操作,可以通过使用layers.core.Lambda层来完成。
# 该方法的适用情况:仅对流经该层的数据做个变换,而这个变换本身没有需要学习的参数
# 切片后再分别进行embedding和average pooling
import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Activation,Reshape
from keras.layers import merge
from keras.utils import plot_model
from keras.layers import *
from keras.models import Model
def get_slice(x, index):
return x[:, index]
keep_num = 3
field_lens = 90
input_field = Input(shape=(keep_num, field_lens))
avg_pools = []
for n in range(keep_num):
block = Lambda(get_slice,output_shape=(1,field_lens),arguments={'index':n})(input_field)
x_emb = Embedding(input_dim=100, output_dim=200, input_length=field_lens)(block)
x_avg = GlobalAveragePooling1D()(x_emb)
avg_pools.append(x_avg)
output = concatenate([p for p in avg_pools])
model = Model(input_field, output)
plot_model(model, to_file='model/lambda.png',show_shapes=True)
plt.figure(figsize=(21, 12))
im = plt.imread('model/lambda.png')
plt.imshow(im)
# 对于具有可训练权重的定制层,需要自己来实现。
from keras import backend as K
from keras.engine.topology import Layer
import numpy as np
class MyLayer(Layer):
def __init__(self, output_dim, **kwargs):
self.output_dim = output_dim
super(MyLayer, self).__init__(**kwargs)
def build(self, input_shape):
# Create a trainable weight variable for this layer.
self.kernel = self.add_weight(name='kernel',
shape=(input_shape[1], self.output_dim),
initializer='uniform',
trainable=True)
super(MyLayer, self).build(input_shape) # Be sure to call this somewhere!
def call(self, x):
return K.dot(x, self.kernel)
def compute_output_shape(self, input_shape):
return (input_shape[0], self.output_dim)
总结一下,keras的Lambda层就是一个层,允许用户自定义对上层输入数据的操作,自定义操作通过keras.layers.core.Lambda的function进行。
使用回调函数
model的fit()方法有一个参数是callbacks,这个参数可以传入一些其他待执行的函数,在训练过程中,每一个epoch会调用一次列表中的callbacks,这样我们就可以实现很多骚操作在训练过程中了。
在下面这个例子中设置monitor=’val_acc’来保存训练过程中验证集准确率最高的模型
checkpoint = ModelCheckpoint(filepath='./best_model.weights',
monitor='val_acc',
verbose=1,
save_best_only=True)
model.fit(x_train,
y_train,
epochs=10,
validation_data=(x_test, y_test),
callbacks=[checkpoint])
深度学习模型有时需要训练很长时间,如果中断了就要亲命了。
那么如何在训练途中保存想要的模型呢?
# checkpoint
# https://keras.io/zh/callbacks/
# 如果验证损失下降, 那么在每个训练轮之后保存模型
ArgName=',epo='+str(epoch)+',bsize='+str(batch_size)+',lr='+str(LearningRate)+',DropRate='+str(DropoutRate)
FileNamePy=os.path.basename(__file__).split('.')[-2]
checkpoint_filepath = FileNamePy+ArgName
checkpointer_val_best = ModelCheckpoint(filepath=checkpoint_filepath, monitor='val_acc',
verbose=1, save_best_only=True, mode='max', save_weights_only=True)
callbacks_list = [checkpointer_val_best]
hist=model.fit_generator(
train_generator,
steps_per_epoch=nb_train_samples,
epochs=epoch,
validation_data=validation_generator,
validation_steps=nb_validation_samples,
callbacks = callbacks_list)
这种方法虽然简单,但是有一个明显的缺点,就是里边的指标是由compile的metrics来确定的,而Keres中自定义一个metric,需要写成张量运算才行,也就是说如果你期望的指标并不能写成张量运算(比如bleu等指标),那么就没法写成一个metric函数了,也就不能用这个方案了。
by:苏剑林
苏剑林大神提供了一个方案:自己写回调器
from keras.callbacks import Callback
def evaluate(): # 评测函数
pred = model.predict(x_test)
return np.mean(pred.argmax(axis=1) == y_test) # 爱算啥就算啥
# 定义Callback器,计算验证集的acc,并保存最优模型
class Evaluate(Callback):
def __init__(self):
self.accs = []
self.highest = 0.
def on_epoch_end(self, epoch, logs=None):
acc = evaluate()
self.accs.append(acc)
if acc >= self.highest: # 保存最优模型权重
self.highest = acc
model.save_weights('best_model.weights')
# 爱运行什么就运行什么
print 'acc: %s, highest: %s' % (acc, self.highest)
evaluator = Evaluate()
model.fit(x_train,
y_train,
epochs=10,
callbacks=[evaluator])
训练过程中还有可能对超参数进行微调,比如最常见的一个需求是根据epoch来调整学习率,这可以简单地通过LearningRateScheduler来实现,它也属于回调器之一。这个方案也是苏神的~
from keras.callbacks import LearningRateScheduler
def lr_schedule(epoch):
# 根据epoch返回不同的学习率
if epoch < 50:
lr = 1e-2
elif epoch < 80:
lr = 1e-3
else:
lr = 1e-4
return lr
lr_scheduler = LearningRateScheduler(lr_schedule)
model.fit(x_train,
y_train,
epochs=10,
callbacks=[evaluator, lr_scheduler])
更多例子可以看苏神博客:https://www.spaces.ac.cn/
输出优化器的名字
下面简单介绍一个小技巧,可能一些初学者还没有用过。通常我们import了一个优化器但是需要以字符串的形式知道这个优化器的名字进行一些记录类的操作,那么怎么实现呢?
from keras.optimizers import Adam
opt=Adam()
opt_name=opt.__class__.__name__
print(opt_name)
写在最后
keras可以说是深度学习最容易上手的深度学习框架了。虽然一些不了解它的人会“诟病”它的不灵活性。但其实它是非常灵活的,用苏神的话来说就是:tf能实现的,keras也可以~
这篇文章叫《Keras使用进阶(Ⅰ)》,讲道理应该有《Keras使用进阶(Ⅱ)》,只是下一篇更新时间的话......嘻嘻~
参考资料&延伸阅读
[keras作者fchollet的学习资源推荐]
(https://github.com/fchollet/keras-resources)
[Multi_Label_Classification_Keras](https://github.com/ItchyHiker/Multi_Label_Classification_Keras)
[keras官方使用自定义层文档]
(https://keras.io/layers/writing-your-own-keras-layers/)
[让Keras更酷一些!Keras模型杂谈 - 科学空间|Scientific Spaces](https://www.spaces.ac.cn/archives/5765)
[UCF课程:高级计算机视觉(Keras) by Mubarak Shah](https://www.bilibili.com/video/av24577241)
[某位博主写的Lambda层例子]
(https://www.cnblogs.com/jins-note/p/9734771.html)
[keras官方callbacks文档]
(https://keras.io/zh/callbacks/)
与我交流
github: https://github.com/keloli
blog: https://www.jianshu.com/u/d055ee434e59
往期回顾之作者李中梁
【1】【TPAMI重磅综述】 SIFT与CNN的碰撞:万字长文回顾图像检索任务十年探索历程(上篇)
【2】【TPAMI重磅综述】 SIFT与CNN的碰撞:万字长文回顾图像检索任务十年探索历程(下篇)
【5】Dropout VS BN: 别在你的网络中使用Dropout
【6】理解Batch Normalization(含实现代码)
机器学习算法工程师
一个用心的公众号
长按,识别,加关注
进群,学习,得帮助
你的关注,我们的热度,
我们一定给你学习最大的帮助