第七章 高级的深度学习实践

7.1 不用 Sequential 模型的解决方案:Keras 函数式 API

此前介绍的所有神经网络都是用 Sequential 模型实现的。Sequential 模型假设,网络只有一个输入和一个输出,而且网络是层的线性堆叠。但在很多任务中,这种简单的模型结构不能贴合实际的需求。有些任务需要多模态(multimodal) 输入(数据、文本和图像),有些任务需要预测输入数据的多个目标属性。同时,许多最新开发的神经架构要求非线性的网络拓扑结构。为此,本章介绍了另一种更加通用、更加灵活的方法,就是 函数式 API(functional API)

7.1.1 函数式 API 简介

使用函数式 API,可以直接操作张量,也可以把层当作函数来使用,接收张量并返回张量(因此得名函数式 API)。

from keras import Input, layers

input_tensor = Input(shape=(32,))             # 确定输入张量的形状
dense = layers.Dense(32, activation='relu')   # 将层实例化成一个函数
output_tensor = dense(input_tensor)           # 在一个张量上调用一个层,并返回一个张量
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 类将输入张量和输出张量转换为一个模型

这里只用了一个输入张量和一个输出张量就将 Model 对象实例化了。在这个过程中,Keras 会在后台检索从 input_tensoroutput_tensor 所包含的每一层,并将这些层组合成一个类图的数据结构,即一个 Model。当然,这种方法有效的原因在于,output_tensor 是通过对 input_tensor 进行多次变换得到的。如果利用不相关的输入和输出来构建一个模型,那么会得到 RuntimeError

>>> unrelated_input = Input(shape=(32,))
>>> bad_model = model = Model(unrelated_input, output_tensor)
RuntimeError: Graph disconnected: cannot obtain value for tensor Tensor("input_1:0", shape=(?, 64), dtype=float32) at layer "input_1".

对这种 Model 实例进行编译、训练和评估的过程与 Sequential 模型相同。

model.compile(optimizer='rmsprop', loss='categorical_crossentropy')   # 编译

model.fit(x_train, y_train, epochs=10, batch_size=128)   # 训练

score = model.evaluate(x_train, y_train)  # 评估

7.1.2 多输入模型

本节介绍的模型是:输入一个自然语言描述的问题和一个文本片段,然后模型要生成一个回答(阅读理解)。在最简单的情况下,这个回答只包含一个词,可以通过对某个预定义的词表做 softmax 得到。
在这里插入图片描述

1、用函数式 API 实现双输入问答模型

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')   # 文本输入是一个长度可变的整数序列,对输入命名
embedded_text = layers.Embedding(
    text_vocabulary_size, 64)(text_input) 
encoded_text = layers.LSTM(32)(embedded_text)   # 利用 LSTM 将向量编码为单个向量

""" 问题输入的分支 """
question_input = Input(shape=(None,),
                       dtype='int32',
                       name='question') 
embedded_question = layers.Embedding(
    question_vocabulary_size, 32)(question_input)
encoded_question = layers.LSTM(16)(embedded_question)

""" 模型输出 """
concatenated = layers.concatenate([encoded_text, encoded_question], axis=-1)   # 将两者连接起来
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'])

2、训练模型

""" 使用输入组成的列表来拟合 """
model.fit([text, question], answers, epochs=10, batch_size=128) 

""" 使用输入组成的字典来拟合(需要先对输入命名) """
model.fit({'text': text, 'question': question}, answers,
          epochs=10, batch_size=128)

7.1.3 多输出模型

本节介绍的模型是:输入社交媒体上帖子的文本,预测发帖人的属性(年龄、性别和收入)。
在这里插入图片描述

1、用函数式 API 实现一个三输出模型

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)

""" 三个输出分支 """
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'})

注意,严重不平衡的损失贡献会导致模型表示针对单个损失值最大的任务优先进行优化,
而不考虑其他任务的优化(尤其是在损失值具有不同的取值范围的情况下)。
这时可以为损失函数添加权重

2、多输出模型的编译选项:损失加权

model.compile(optimizer='rmsprop',
              loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'],
              loss_weights=[0.25, 1., 10.])

model.compile(optimizer='rmsprop', 
              loss={'age': 'mse',
                    'income': 'categorical_crossentropy',
                    'gender': 'binary_crossentropy'}, 
              loss_weights={'age': 0.25,
                            'income': 1., 
                            'gender': 10.})

3、将数据输入到多输出模型中

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)

7.1.4 层组成的有向无环图

可以利用函数式 API 组成的任意 有向无环图(directed acyclic graph)无环(acyclic) 是指图中没有循环(即张量 x 不能成为生成 x 的某一层的输入)。唯一允许的处理循环(即循环连接)是循环层的内部循环。

一、Inception 模块

此处以 Inception V3,结构如下。在这里插入图片描述

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) 

关于 1×1 卷积的作用此处引用原文:

我们已经知道,卷积能够在输入张量的每一个方块周围提取空间图块,并对所有图块应用相同的变换。极端情况是提取的图块只包含一个方块。这时卷积运算等价于让每个方块向量经过一个 Dense 层:它计算得到的特征能够将输入张量通道中的信息混合在一起,但不会将跨空间的信息混合在一起(因为它一次只查看一个方块)。这种 1×1 卷积[也叫作逐点卷积(pointwise convolution)] 是 Inception 模块的特色,它有助于区分开通道特征学习和空间特征学习。如果你假设每个通道在跨越空间时是高度自相关的,但不同的通道之间可能并不高度相关,那么这种做法是很合理的

二、残差连接

残差连接(residual connection) 是一种常见的类图网络组件,在 2015 年之后的许多网络架构(包括 Xception)中都可以见到。残差连接解决了困扰所有大规模深度学习模型的两个共性问题:梯度消失表示瓶颈。具体操作是将前面某层的输出作为后面某层的输入

如果特征图的尺寸相同,用的是 恒等残差连接(identity residual connection)

from keras import layers

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 与输出特征相加

如果特征图的尺寸不同,用的是 线性残差连接(linear residual connection)

from keras import layers

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])   # 将残差张量与输出特征相加

线性连接的模型中,每层都会筛选对应的特征,再传递给下一层。这意味着,下一层只能访问前一层激活中包含的信息,任何信息一旦丢失,下一层就无法访问。这就造成了深度学习中的 表示瓶颈。而残差连接可以将较早的信息重新注入到下游数据中,从而部分解决了这一问题。

7.1.5 共享层权重

函数式 API 还有一个重要特性,那就是能够多次重复使用一个层实例,每次调用都可以重复使用相同的权重。这样就构建了具有共享分支的模型,即几个分支全都共享相同的知识并执行相同的运算。也就是说,这些分支共享相同的表示,并同时对不同的输入集合学习这些表示

本节介绍的模型是:输入两个句子进行语义比较,输出相似度(0~1):

在这里插入图片描述

from keras import layers
from keras import Input
from keras.models import Model

lstm = layers.LSTM(32) 

left_input = Input(shape=(None, 128)) 
left_output = lstm(left_input)

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)

上述代码中的 LSTM 层同时从两个输入里学习表示(即它的权重)。我们将其称为 连体 LSTM(Siamese LSTM)共享LSTM(shared LSTM) 模型。

7.1.6 将模型作为层

在函数式 API 中,可以像使用层一样使用模型。在调用模型实例时,就是在重复使用模型的权重,正如在调用层实例时,就是在重复使用层的权重。

y = model(x)

y1, y2 = model([x1, x2])

7.1.7 小结

1、函数式 API
2、使用函数式 API 来构建多输入模型多输出模型具有复杂的内部网络拓扑结构的模型
3、调用相同的层实例模型实例,在不同的处理分支之间重复使用层或模型的权重

7.2 使用 Keras 回调函数和 TensorBoard 来检查并监控深度学习模型

7.2.1 训练过程中将回调函数作用于模型

回调函数(callback) 是在调用 fit 时传入模型的一个对象(即实现特定方法的类实例),它在训练过程中的不同时间点都会被模型调用。它可以访问关于模型状态与性能的所有可用数据,还可以采取行动:中断训练保存模型加载一组不同的权重改变模型的状态

回调函数的一些用法示例如下所示:
1、模型检查点(model checkpointing):在训练过程中的不同时间点保存模型的当前权重。
2、提前终止(early stopping):如果验证损失不再改善,则中断训练(当然,同时保存在训练过程中得到的最佳模型)。
3、在训练过程中动态调节某些参数值:比如优化器的学习率。
4、在训练过程中记录训练指标和验证指标,或将模型学到的表示可视化(这些表示也在不
断更新)。

一、ModelCheckpoint 与 EarlyStopping 回调函数

如果监控的目标指标在设定的轮数内不再改善,可以用 EarlyStopping 回调函数来中断训练。同时利用 ModelCheckpoint 保存训练过程中的模型(也可以只保留最佳性能的模型)。

import keras

""" 回调函数列表 """
callbacks_list = [ 
    keras.callbacks.EarlyStopping( 
        monitor='acc',   # 监控验证集精度
        patience=1,      # 等待1轮(精度在两轮内没有改善就中断训练)
    ),
    keras.callbacks.ModelCheckpoint( 
        filepath='my_model.h5',   # 目标模型文件的保存路径
        monitor='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))    # 回调函数需要监控验证集精度和损失

二、 ReduceLROnPlateau 回调函数

在训练过程中如果验证损失不再改善,出现了损失平台(loss plateau),可以使用 ReduceLROnPlateau 回调函数来降低学习率。

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))

三、编写你自己的回调函数

可以通过创建 keras.callbacks.Callback 类 的子类来编写需要的回调函数。这个子类可以包括以下六种方法:

on_epoch_begin   # 在每轮开始时被调用
on_epoch_end     # 在处理每个批量之前被调用

on_batch_begin   # 在训练开始时被调用
on_batch_end     # 在每轮结束时被调用

on_train_begin   # 在处理每个批量之后被调用
on_train_end     # 在训练结束时被调用

这些方法被调用时都有一个 logs 参数,这个参数是一个字典,里面包含前一个批量、前
一个轮次或前一次训练的信息
,即训练指标和验证指标等。此外,回调函数还可以访问下列属性
1、self.model:调用回调函数的模型实例
2、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]   # 获取验证集 x_val的第一个样本
        activations = self.activations_model.predict(validation_sample)
        f = open('activations_at_epoch_' + str(epoch) + '.npz', 'w')   # 可写入
        np.savez(f, activations)
        f.close()

7.2.2 TensorBoard 简介:TensorFlow 的可视化框架

TensorBoard 是一个内置于 TensorFlow 中的基于浏览器的可视化工具(只有当 Keras 使用 TensorFlow 后端时,这一方法才能用于 Keras 模型)。它可以在训练过程中帮助你以可视化的方法监控模型内部发生的一切。功能包括:

1、在训练过程中以可视化的方式监控指标
2、将模型架构可视化
3、将激活梯度的直方图可视化。
4、以三维的形式研究嵌入

以 IMDB 情感分析任务为例。

1、使用了 TensorBoard 的文本分类模型

import keras
from keras import layers
from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 2000 
max_len = 500 

(x_train, y_train), (x_test, y_test) = imdb.load_data(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'])

2、为 TensorBoard 日志文件创建一个目录

$ mkdir my_log_dir

3、使用一个 TensorBoard 回调函数来训练模型

callbacks = [
    keras.callbacks.TensorBoard(
    log_dir='my_log_dir',   # 日志文件将被写入这个位置
    histogram_freq=1,       # 每一轮之后记录激活直方图
    embeddings_freq=1,      # 每一轮之后记录嵌入数据
    )
]

history = model.fit(x_train, y_train,
                    epochs=20,
                    batch_size=128,
                    validation_split=0.2,  # 验证集占输入数据集的比例
                    callbacks=callbacks)

4、读取回调函数当前正在写入的日志

$ tensorboard --logdir=my_log_dir

然后可以用浏览器打开 http://localhost:6006,并查看模型的训练过程。

除了训练指标和验证指标的实时图表 SCALARS 之外,还可以查看每层的激活值的直方图 HISTOGRAMS,词嵌入的嵌入位置和空间关系 EMBEDDINGS(可选的降维算法有主成分分析和 t-分布随机近邻嵌入),以及底层 TensorFlow 运算图的交互式可视化 GRAPHS

Keras 还提供了另一种更简洁的方法——keras.utils.plot_model 函数,它可以将模型绘制为层组成的图,而不是 TensorFlow 运算组成的图。使用这个函数需要安装 Pythonpydot 库和 pydot-ng 库,还需要安装 graphviz 库。

from keras.utils import plot_model

plot_model(model, show_shapes=True, to_file='model.png')

在这里插入图片描述

7.2.3 小结

1、利用 回调函数 在训练过程中监控模型并根据模型状态自动采取行动。
2、使用 TensorBoard 将模型活动可视化。

7.3 让模型性能发挥到极致

7.3.1 高级架构模式

一、批标准化

批标准化(batch normalization) 是 Ioffe 和 Szegedy 在 2015 年提出的一种层的类型。其工作原理是,训练过程中在内部保存已读取每批数据均值和方差的指数移动平均值。批标准化的主要效果是,它有助于梯度传播,允许更深的网络。

BatchNormalization 层 通常在卷积层或密集连接层之后使用。它接收一个 axis 参数,用来指定应该对哪个特征轴做标准化。这个参数的默认值是 -1,即输入张量的最后一个轴。对于 Dense 层、Conv1D 层、RNN 层和将 data_format 设为 “channels_last”(通道在后)的 Conv2D 层,这个默认值都是正确的。但有对于将 data_format 设为 channels_first"(通道在前)的 Conv2D 层,这时特征轴是编号为 1 的轴(0轴为batch_size),因此 BatchNormalizationaxis 参数应该相应地设为 1。

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())

二、深度可分离卷积

深度可分离卷积(depthwise separable convolution)层SeparableConv2D)可以替代 Conv2D,让模型更加轻量(即更少的可训练权重参数)、速度更快(即更少的浮点数运算),还可以让任务性能提高几个百分点。这个层对输入的每个通道分别执行空间卷积,然后通过逐点卷积(1×1 卷积)将输出通道混合。这相当于将空间特征学习和通道特征学习分开
在这里插入图片描述

将大小为 DF×DF×M 的张量输入到 N 通道、卷积核大小为 DK×DK×M 的 Conv2D 层中。该层的参数量为:DK×DK×M×N。如果使用深度可分离卷积,参数量为:DK×DK×M+M×N。可以参考知乎的这篇文章

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')

7.3.2 超参数优化

模型在架构层面的参数叫作 超参数(hyperparameter),包括:模型层数,每层单元数、卷积核大小、激活函数选择、是否使用 BN、dropout 比率、学习率设置等等。

该书的作者认为超参数优化问题应交由计算机自主完成,并提及了两个用于超参数优化的 Python 库:HyperoptHyperas

7.3.3 模型集成

模型集成 是指将一系列不同模型的预测结果汇集到一起,从而得到更好的预测结果。集成依赖于这样的假设,即对于独立训练的不同良好模型,它们表现良好可能是因为不同的原因:每个模型都从略有不同的角度观察数据来做出预测,得到了“真相”的一部分,但不是全部真相。

保证集成方法有效的关键在于这组分类器的多样性(diversity)。这里的多样性指的是使用非常不同的架构,甚至使用不同类型的机器学习方法,而不是同一个模型的不同训练结果。

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

7.3.4 小结

1、使用残差连接批标准化深度可分离卷积构建高性能的深度卷积神经网络。
2、超参数自动优化
3、使用模型集成获取更好的结果。

小结

1、使用 函数式 API 构建多输入、多输出模型以及有向无环图,实现共享层权重,将模型作为函数调用。
2、使用 回调函数 在训练过程中监控模型,并根据模型状态采取行动。
3、利用 TensorBoard 实现指标、激活直方图甚至嵌入空间可视化。
4、使用 批标准化深度可分离卷积超参数优化模型集成 来提高训练结果。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐