内容参考以及代码整理自“深度学习四大名“著之一《Python深度学习》
查看完整代码,请看: https://github.com/ubwshook/MachineLearning
一、Keras函数式API
我们之前讨论的深度学习模型都是用Sequential模型实现的。Sequential模型假设,网络只有一个输入和一个输出,而且网络是层的线性堆叠。但有些网络需要多个独立的输入,有些网络则需要多个输出,而有些网络在层与层之间具有内部分支,这使得网络看起来像是层构成的图,而不是线性堆叠。
使用函数式API,你可以直接操作向量,也可以把层当作函数来使用,接收张量并返回张量,因此叫函数式API。
from keras import Input, layers
input_tensor = Input(shape=(32,)) # 一个张量
dense = layers.Dense(32, activation='relu') # 一个层是一个函数
output_tensor = dense(input_tensor) # 可以在一个张量上调用一个层,返回时一个张量
1.函数式API简介
一个简单的示例,展示简单Sequential模型以及对应函数式API实现
from keras.models import Sequential, Model
from keras import layers
from keras import Input
# Sequential模型方式构建模型
seq_model = Sequential()
seq_model.add(layers.Dense(32, activation='relu', input_shape=(64,)))
seq_model.add(layers.Dense(32, activation='relu'))
seq_model.add(layers.Dense(10, activation='softmax'))
# 函数式API实现模型
input_tensor = Input(shape=(64,))
x = layers.Dense(32, activation='relu')(input_tensor)
x = layers.Dense(32, activation='relu')(x)
output_tensor = layers.Dense(10, activation='softmax')(x)
model = Model(input_tensor, output_tensor)
model.summary() # 查看模型
函数式API的编译、训练或评估,API与Sequential相同。
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
import numpy as np
x_train = np.random.random((1000, 64))
y_train = np.random.random((1000, 10))
model.fit(x_train, y_train, epochs=10, batch_size=128)
score = model.evaluate(x_train, y_train)
2.多输入模型
一个简单的多输入模型例示—— 一个问答模型:一个自然语言描述的问题和一个文本片段(比如新闻文章),后者提供用于回答问题的信息。然后模型要生成一个回答,最简单的情况下,这个回答只包含一个词,可以通过摸个预定义的词表做softmax得到。
from keras.models import Model
from keras import layers
from keras import Input
text_vocabulary_size = 10000
question_vocabulary_size = 10000
answer_vocabulary_size = 500
# 文本输入时一个长度可变的整数序列。可以对输入进行命名。
text_input = Input(shape=(None,), dtype='int32', name='text')
# 输入嵌入长度为64的向量
embedded_text = layers.Embedding(64, text_vocabulary_size)(text_input)
# 利用LSTM将向量编码为单个向量
encoded_text = layers.LSTM(32)(embedded_text)
# 问题文本做相同的处理
question_input = Input(shape=(None,), dtype='int32', name='question')
embedded_question = layers.Embedding(32, question_vocabulary_size)(question_input)
encoded_question = layers.LSTM(16)(embedded_question)
# 将编码后的问题和文本连接起来
concatenated = layers.concatenate([encoded_text, encoded_question], axis=-1)
# 最后加上softmax分类器
answer = layers.Dense(answer_vocabulary_size, activation='softmax')(concatenated)
model = Model([text_input, question_input], answer)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc'])
我们如何训练这个双输入的API模型?有两个可用的API:我们可以向模型输入一个由Numpy数组组成的列表,或者也可以输入一个名称映射为Numpy数据的字典。当然,只有输入具有名称时才能使用后一种方法。
import numpy as np
num_samples = 1000
max_length = 100
text = np.random.randint(1, text_vocabulary_size, size=(num_samples, max_length))
question = np.random.randint(1, question_vocabulary_size, size=(num_samples, max_length))
answers = np.random.randint(0, 1, size=(num_samples, answer_vocabulary_size))
# 使用输入组合的列表来拟合
model.fit([text, question], answers, epochs=10, batch_size=128)
# 使用输入组成的字典来拟合,这种方式只能应用于对输入命名的情况
model.fit({'text': text, 'question': question}, answers, epochs=10, batch_size=128)
3.多输出模型
多输出模型,一个简单的例子就是一个网络试图预测数据的不同性质,比如一个网络,输入摸个匿名人士的一系列社交媒体发帖,然后尝试预测人的属性,比如年龄、性别和收入水平。
from keras import layers
from keras import Input
from keras.models import Model
vocabulary_size = 50000
num_income_groups = 10
posts_input = Input(shape=(None,), dtype='int32', name='posts')
embedded_posts = layers.Embedding(256, vocabulary_size)(posts_input)
x = layers.Conv1D(128, 5, activation='relu')(embedded_posts)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.GlobalMaxPooling1D()(x)
x = layers.Dense(128, activation='relu')(x)
# 分别有3个输出层,都有自己名字
age_prediction = layers.Dense(1, name='age')(x)
income_prediction = layers.Dense(num_income_groups, activation='softmax', name='income')(x)
gender_prediction = layers.Dense(1, activation='sigmoid', name='gender')(x)
model = Model(posts_input,
[age_prediction, income_prediction, gender_prediction])
这种训练模型需要能够对网络的各个头指定不同的损失函数,例如,年龄预测是标量回归任务,而性别预测是二分类任务,二者需要的不同训练过程。但是梯度下降要求讲一个标量最小化,所以为了能够训练模型,我们必须将这些损失合并为单个标量。合并不同损失最简单的方法就是对所有损失求和。在Keras中,你可以在编译时使用损失组成的列表或字典为不同输出指定损失函数,然后将得到损失值相加得到一个全局损失,并在训练中将这个损失最小化。
model.compile(optimizer='rmsprop', loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'])
model.compile(optimizer='rmsprop', loss={'age': 'mse', 'income': 'categorical_crossentropy','gender': 'binary_crossentropy'})
为了平衡不同损失的贡献,我们可以分配给各个损失不同的权重。
model.compile(optimizer='rmsprop', loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'], loss_weights=[0.25, 1., 10.])
model.fit(posts, [age_targets, income_targets, gender_targets], epochs=10, batch_size=64)
model.fit(posts, {'age': age_targets, 'income': income_targets, 'gender': gender_targets}, epochs=10, batch_size=64)
4.有向无环图模型
函数式API用于实现多输入和多输出模型,可以实现具有复杂的内部拓扑结构的网络。一些常见的神经网络组件都是以图的形式实现的。
Inception模块
Inception是一种流行的卷积神经网络的构架类型。它是模块的堆叠,这些模块本身看起来像是小型独立网络,被分为多个并行分支。Inception模块最基本的形式包括3~4个分支,首先是1 * 1 卷积,然后是一个 3 * 3 卷积, 最后将所得到的特征连接在一起。这种设置有助于网络分别学习空间特征和逐通道的特征,这比联合学习这两种特征更加有效。Inception模块可能具有更复杂的形式,通常会包含池化运算、不同尺寸的空间卷积和不同空间卷积的分支。
我们可以使用函数式API来实现以上结构。
from keras import layers
branch_a = layers.Conv2D(128, 1, activation='relu', strides=2)(x)
branch_b = layers.Conv2D(128, 1, activation='relu')(x)
branch_b = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_b)
branch_c = layers.AveragePooling2D(3, strides=2)(x)
branch_c = layers.Conv2D(128, 3, activation='relu')(branch_c)
branch_d = layers.Conv2D(128, 1, activation='relu')(x)
branch_d = layers.Conv2D(128, 3, activation='relu')(branch_d)
branch_d = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_d)
output = layers.concatenate([branch_a, branch_b, branch_c, branch_d], axis=-1)
残差连接
残差连接是一种常见的类图网络组建。残差桥接解决了困扰所有大规模深度学习的两个共性问题:梯度消失和表示瓶颈。通常来说,向任何多于10层的模型中添加残差连接,都可能有所帮助。
深度学习的表示瓶颈: 在Sequential模型中,每个连续的表示都构建于前一层之上,这意味着它只能访问前一层激活中包含的信息。如果抹一层太小,那么模型将会受限于该层激活中能够塞入多少信息。
梯度消失: 反向传播是用于训练深度神经网络的主要算法,其中工作原理是将来自输出损失的反馈向下传播到更底部的底层。如果这个反馈信信号需要经过很多层,那么这个信号可能变的非常微弱,甚至完全丢失,导致网络无法训练。
残差连接是让前面磨蹭的输出作为后面某层的输入,从而在序列网络中有效地创造一条捷径。前面层的输出没有与后面层的激活连接在一起,而是与后面层的激活相加。如果他们的形状不同,我们可以用一个线性变换将前面的激活改变成目标形状。
恒等残差
x = ...
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.add([y, x])
线性残差连接
x = ...
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.MaxPooling2D(2, strides=2)(y)
residual = layers.Conv2D(128, 1, strides=2, padding='same')(x)
y = layers.add([y, residual])
5.共享层权重
函数式API还有一个重要的特性,就是能够多次重复使用一个层实例。如果你对一个层实例调用两次,而不是每次调用都实例化一个锌层,那么每次调用可以重复使用相同的权重。这样你可以构建具有共享分支的模型,即每个分支都共享相同的知识并执行相同的运算。也就是说,这些分支共享相同的表示,并同时对不同的输入集合学习这些表示。
举例,一个模型想要评估两个句子之间的语义相似度。这个模型有两个输入,并输出一个范围在0~1的分数,0表示两个句子毫不相关,1表示两个句子完全相同或只是换了一种表述。这种模型在许多应用中都很有用,其中包含在对货系统中删除重复语言查询。
在这种场景下,两个输入时可以换换的,因为语义相似度是一种对称关系。因此学习单独模型分别来处理两个输入时没有道理的。需要使用一个LSTM层来处理两个句子。这个LSTM层表示同时基于两个输入来学习。我们将其称为连体LSTM或共享LSTM模型。
from keras import layers
from keras import Input
from keras.models import Model
# 将一个LSTM实例化一次
lstm = layers.LSTM(32)
# 输入时长度128的向量组成的边长序列
left_input = Input(shape=(None, 128))
left_output = lstm(left_input)
# 构建右侧分支,也是用lstm,共享该层
right_input = Input(shape=(None, 128))
right_output = lstm(right_input)
# 合并两个分支
merged = layers.concatenate([left_output, right_output], axis=-1)
predictions = layers.Dense(1, activation='sigmoid')(merged)
# 训练模型
model = Model([left_input, right_input], predictions)
model.fit([left_data, right_data], targets)
6.将模型作为层
函数式API可以像层一样使用模型。
from keras import layers
from keras import applications
from keras import Input
# 使用keras中的xception模型
xception_base = applications.Xception(weights=None,include_top=False)
# 左右分支共享
left_input = Input(shape=(250, 250, 3))
right_input = Input(shape=(250, 250, 3))
left_features = xception_base(left_input)
right_input = xception_base(right_input)
merged_features = layers.concatenate([left_features, right_input], axis=-1
二、Keras回调函数
有时候训练足够多的轮次,这时候模型已经开始过拟合,想要重新启动一次训练,并记录过拟合开始的轮数,这时候用就可以使用回调函数。回调函数是调用fit时,传入模型的一个对象,他在训练过程中的不同时间点都会被模型调用。它可以访问关于模型状态与性能的所有数据,可以采取行动: 中断训练、保存模型、加载一组不同的权重会改变模型的状态。回调函数用法示例如下:
- 模型检查点:在训练过程中的不同时间点保存模型的当前权重
- 提前终止:如果验证损失不再改善,则中断训练
- 在训练过程中调节默写参数值: 比如优化器的学习率
- 在训练过程中记录训练和验证指标,或将模型学到的表示可视化:你熟悉的Keras进度条就是一个回调函数。
下面介绍几个回调函数。
1.ModelCheckpoint与EarlyStopping回调函数。
EarlyStoping可以中断训练,可以在训练出现拟合的时候中断训练,从而避免用跟梢的轮次重新训练模型。这个回调函数通常与ModelCheckpoint结合使用,后者可以在训练过程中持续不断的保存模型。
import keras
callbacks_list = [keras.callbacks.EarlyStopping(monitor='acc', # 监控模型的指标,这里是精度
patience=1,), # 如果精度多余一轮的时间不改善,中断训练
keras.callbacks.ModelCheckpoint(filepath='my_model.h5', # 保存模型文件的名字
monitor='val_loss', # 如果val_loss没有改善就不覆盖模型文件
save_best_only=True,)]
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
model.fit(x, y, epochs=10, batch_size=32, callbacks=callbacks_list, validation_data=(x_val, y_val))
2.ReduceLROnOlateau回调函数
如果验证损失不再改善,你可以使用这个回调函数来降低学习率。在训练过程中如果出现了平台损失,那么增大或减小学习率都是跳出局部最小值的有效策略。
callbacks_list = [keras.callbacks.ReduceLROnPlateau(monitor='val_loss', # 监控模型的验证损失
factor=0.1, # 触发时将学习率除以10
patience=10,)] # 如果验证损失在10轮内都米有改善就触发回调函数
model.fit(x, y, epochs=10, batch_size=32, callbacks=callbacks_list, validation_data=(x_val, y_val))
3.定制自己的回调函数
回调函数的实现方式是穿件keras.callbacks.Callback类的子类。然后你可以实现下面这些方法,他们分别在训练过程中的不同时间点被调用。
- on_epoch_begin: 每轮开始
- on_epoch_end: 每轮结束
- on_batch_begin: 每个批量处理开始
- on_batch_end: 在处理每个批量之后被调用
- on_train_begin: 在训练开始时被调用
- on_train_end: 在伦莲结束时被调用。
回调函数还可以访问下列属性。
- self.model:调用回调函数的模型实例
- self.validation_data: 传入fit作为验证数据的值。
下面是一个自定义回调函数,他可以在每轮结束后将模型每层的激活保存到硬盘里。这个激活是对验证集的第一个样本计算得到的。
import keras
import numpy as np
class ActivationLogger(keras.callbacks.Callback):
# 告诉回调函数是哪个模型在调用它
def set_model(self, model):
self.model = model
layer_outputs = [layer.output for layer in model.layers]
self.activations_model = keras.models.Model(model.input, layer_outputs) # 返回每层激活
def on_epoch_end(self, epoch, logs=None):
if self.validation_data is None:
raise RuntimeError('Requires validation_data.')
validation_sample = self.validation_data[0][0:1] # 获取验证数据的第一个输入样本
activations = self.activations_model.predict(validation_sample) # 将数据保存到硬盘
f = open('activations_at_epoch_' + str(epoch) + '.npz', 'w')
np.savez(f, activations)
f.close()
三、TensorFlow可视化框架
TensorBoard的主要用途是,在训练过程中帮助你可以可视化的方法监控模型内部发生了什么。主要功能:
- 在训练过程中可以可视化的方式监控指标
- 将模型架构可视化
- 将激活和梯度的直方图可视化
- 以三维的形式研究嵌入
这里我们对IMDB情感分析人物训练一个一维卷积神经网络。采用了词嵌入,我们可以可视化的观察词嵌入的情况
import keras
from keras import layers
from keras.preprocessing import sequence
from imdb import load_local
from keras.utils import plot_model
import os
os.environ["PATH"] += os.pathsep + 'D:\\Program Files (x86)\\Graphviz2.38\\bin'
max_features = 500
max_len = 500
(x_train, y_train), (x_test, y_test) = load_local(num_words=max_features)
x_train = sequence.pad_sequences(x_train, maxlen=max_len)
x_test = sequence.pad_sequences(x_test, maxlen=max_len)
model = keras.models.Sequential()
model.add(layers.Embedding(max_features, 128, input_length=max_len, name='embed'))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.MaxPooling1D(5))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))
model.summary()
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
plot_model(model, show_shapes='True', to_file='model.png')
callbacks = [keras.callbacks.TensorBoard(log_dir='my_log_dir', # 日志写入的位置
histogram_freq=1, # 每一轮之后记录激活
embeddings_freq=1, # 每一轮之后记录嵌入数据
embeddings_data=x_train[:100].astype("float32"),)]
history = model.fit(x_train, y_train, epochs=20, batch_size=128, validation_split=0.2,
callbacks=callbacks)
TensorBoard 词嵌入分布
使用命令启动TensorBoard, 指定日志目录以及服务器IP地址
tensorboard --logdir=E:\git_code\MachineLearning\keras\my_log_dir --host=127.0.0.1
在浏览其中打开 http://127.0.0.1:6006 来查看训练模型。
并且可以绘制模型图:
from keras.utils import plot_model
plot_model(model, show_shapes='True', to_file='model.png')
四、让模型性能发挥到极致
1.高级架构模式
批标准化
标准化:将数据减去器平均值使其中心为0,然后除以器标准差使其标准差为1。
noralized_data = (data - np.mean(data, axis=...)) / np.std(data, axis=...)
之前的标准化都是在数据输入模型前做的。但在网络每一次变化之后都应该考虑标准化。批标准化,可以及时在训练过程中均值和方差随时间变化,他也可以自适应的将数据标准化。批标准化的原理是: 训练过程中内部保存已读取每批数据均值和方差的指数移动平均值。批标准化的主要效果是,它有助于梯度传播,因此允许更深的网络。
BatchNormalization层通常在卷积层或密集连接层之后使用。
conv_model.add(layers.Conv2D(32, 3, activation='relu'))
conv_model.add(layers.BatchNormalization())
dense_model.add(layers.Dense(32, activation='relu'))
dense_model.add(layers.BatchNormalization())
深度可分离卷积
如果有一个层可以代替Conv2D,并可以让模型更加轻量、速度更快,还可以让任务性能提升几个百分点,那就是深度可分离卷积层(SeparableConv2D)。它对输入的每个通道分别执行空间卷积,然后通过逐点积(1 * 1卷积)将输出通道混合。这相当于将空间特征学习和通道特征学习分开,如果你假设输入中的空间位置高度相关,但不同的通道之间相对独立,那么这么做事很有意义的。如果有用有限的数据从头开始训练小型模型,这些优点救护变得很重要。
from keras.models import Sequential, Model
from keras import layers
height = 64
width = 64
channels = 3
num_classes = 10
model = Sequential()
model.add(layers.SeparableConv2D(32, 3, activation='relu', input_shape=(height, width, channels,)))
model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))
model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))
model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.GlobalAveragePooling2D())
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(num_classes, activation='softmax'))
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
2.超参数优化
构建深度学习模型时,需要制定一些参数,比如堆叠多少层,每层应该包含多少个单元或过滤器?激活应该使用relu还是其他函数?在某一层之后是否应该使用BatchNormalization?应该使用多的dropout比率。这些架构层面的参数叫做超参数。
参数优化过程:
- 选择一组超参数(自动选择)
- 构建相应的模型。
- 将模型在训练数据上拟合,并衡量器在验证数据上的最终性能。
- 选择要尝试的一下组超参数(自动选择)
- 重复上述过程
- 最后,衡量模型在测试数据上的性能。
3.模型集成
模型集成是指将一系列不同的模型预测结果汇集到一起,从而得到更加的预测结果。每个模型都从不同的角度来做出预测,可能只得到了真相的一部分。
要想将一组分类器的预测记过汇集咋一起,使用分类器集成,最简单方法就是取预测结果的平均值:
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)
final_preds = 0.25 * (preds_a + preds_b + preds_c + preds_d)
或者使用加群平均,权重在验证数据上学习得到:
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)
final_preds = 0.5 * preds_a + 0.25 * preds_b + 0.1 * preds_c + 0.15 * preds_d
网友评论