keras之父《python深度学习》笔记 第七章

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

本章将介绍几种强大的工具,可以让你朝着针对困难问题来开发最先进模型这一目标更近
一步。利用Keras 函数式API,你可以构建类图(graph-like)模型、在不同的输入之间共享某一层,
并且还可以像使用Python 函数一样使用Keras 模型。Keras 回调函数和TensorBoard 基于浏览器
的可视化工具,让你可以在训练过程中监控模型。我们还会讨论其他几种最佳实践,包括批标
准化、残差连接、超参数优化和模型集成。。


一、Keras 函数式API

1.Sequential 模型的局限性

本书之前的例子都是用Sequential 模型实现的,基本是就是简单的堆叠层,而且网络只有一个输入输出。

Sequential 模型
对于普通的常见的问题,这种模型是可以处理的。但是有些任务需要多个输入、多个输出。或者网络层与层之间有分支,这种网络更像是层构成的图,而不是层的线性堆叠。

很常见的例子,有些任务是多模态输入,可能有多个数据源。比如预测一件衣服的价格,可能的数据源有商品本身的元数据(商品品牌、使用年限)、用户的文字描述以及衣服照片。元数据可以onehot之后用全连接神经网络处理,文本描述可以用循环神经网络或者一维卷积网络处理。而图像可以用二维卷积网络来处理。而这三个数据源同时处理的话,可能就需要联合学习,三个输入分支训练一个新的模型。

多输入联合学习

同样,有些任务需要预测输入数据的多个目标属性。给定一部小说的文本,你可能希望将
它按类别自动分类(比如爱情小说或惊悚小说),同时还希望预测其大致的写作日期。当然,你
可以训练两个独立的模型:一个用于划分类别,一个用于预测日期。但由于这些属性并不是统
计无关的,你可以构建一个更好的模型,用这个模型来学习同时预测类别和日期。这种联合模
型将有两个输出,或者说两个头(head)。因为类别和日期之间具有相关性,所以知
道小说的写作日期有助于模型在小说类别的空间中学到丰富而又准确的表示,反之亦然。

多输出任务
此外,许多最新开发的神经架构要求非线性的网络拓扑结构,即网络结构为有向无环图。
比如,Inception 系列网络(由Google 的Szegedy 等人开发)依赖于Inception 模块,其输入被
多个并行的卷积分支所处理,然后将这些分支的输出合并为单个张量。最近还有一种趋势是向模型中添加残差连接(residual connection),它最早出现于ResNet 系列网络(由微软的何恺明等人开发)。残差连接是将前面的输出张量与后面的输出张量相加,从而将前面的表示重新注入下游数据流中,这有助于防止信息处理流程中的信息损失。这种类图网络还有许多其他示例。

nception 模块结构

残差网络结构
这三个重要的使用案例(多输入模型、多输出模型和类图模型),只用Keras 中的Sequential
模型类是无法实现的。但是还有另一种更加通用、更加灵活的使用Keras 的方式,就是函数式
API(functional API)。本节将会详细介绍函数式API 是什么、能做什么以及如何使用它。

2.函数式API 简介

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

from keras import Input, layers
input_tensor = Input(shape=(32,))
dense = layers.Dense(32, activation='relu')
output_tensor = dense(input_tensor)

我们首先来看一个最简单的示例,并列展示一个简单的Sequential 模型以及对应的函数
式API 实现。

from keras.models import Sequential, Model
from keras import layers
from keras import Input
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'))
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()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 64) 0
_________________________________________________________________
dense_1 (Dense) (None, 32) 2080
_________________________________________________________________
dense_2 (Dense) (None, 32) 1056
_________________________________________________________________

dense_3 (Dense) (None, 10) 330
=================================================================
Total params: 3,466
Trainable params: 3,466
Non-trainable params: 0
_________________________________________________________________

这里只有一点可能看起来有点神奇,就是将Model 对象实例化只用了一个输入张量和
一个输出张量。Keras 会在后台检索从input_tensor 到output_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".

这个报错告诉我们,Keras 无法从给定的输出张量到达input_1。
对这种Model 实例进行编译、训练或评估时,其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)

函数式API 可用于构建具有多个输入的模型。通常情况下,这种模型会在某一时刻用一个
可以组合多个张量的层将不同的输入分支合并,张量组合方式可能是相加、连接等。这通常利
用Keras 的合并运算来实现,比如keras.layers.add、keras.layers.concatenate 等。
我们来看一个非常简单的多输入模型示例——一个问答模型。
典型的问答模型有两个输入:一个自然语言描述的问题和一个文本片段(比如新闻文章),
后者提供用于回答问题的信息。然后模型要生成一个回答,在最简单的情况下,这个回答只包
含一个词,可以通过对某个预定义的词表做softmax 得到

问答模型示例
接下来我们将用函数式API给上图任务构建模型。我们设置两个分支,将文本和问题输入,然后使用文本向量化进行编码,经过处理最后用一个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')
embedded_text = layers.Embedding(
text_vocabulary_size, 64)(text_input)
encoded_text = layers.LSTM(32)(embedded_text)
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'])

接下来要如何训练这个双输入模型呢?有两个可用的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(answer_vocabulary_size, size=(num_samples))
answers = keras.utils.to_categorical(answers, 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)
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])

对于多输出问题要指定多个损失函数,根据不同任务的特点来确定。下面两种方式都可以,由于梯度下降问题是要求标量最小化,所以为了满足要求我们将这些损失求和,然后就可以得到一个损失,我们训练的目标就是把这个损失最小化。

model.compile(optimizer='rmsprop',
loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'])
model.compile(optimizer='rmsprop',
loss={'age': 'mse',
'income': 'categorical_crossentropy',
'gender': 'binary_crossentropy'})

直接相加是一种简单粗暴的处理。因为每个任务的损失值取值范围可能不一样,在keras中多任务可以给定各自的取值范围。比如年龄回归的均方误差通常为3~5,性别的MSE损失只有0.1,为了平衡不同损失的贡献,我们让交叉熵的损失权重为10,而MSE的权重为0.5。

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.})
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,我们不仅可以构建多输入和多输出的模型,而且还可以实现具有复杂
的内部拓扑结构的网络。Keras 中的神经网络可以是层组成的任意有向无环图(directed acyclic
graph)。无环(acyclic)这个限定词很重要,即这些图不能有循环。张量x 不能成为生成x 的
某一层的输入。唯一允许的处理循环(即循环连接)是循环层的内部循环。
一些常见的神经网络组件都以图的形式实现。两个著名的组件是Inception 模块和残差连接。
为了更好地理解如何使用函数式API 来构建层组成的图,我们来看一下如何用Keras 实现这二者。

(1)Inception 模块
Inception 是一种流行的卷积神经网络的架构类型,它由Google 的Christian Szegedy 及其
同事在2013—2014 年开发,其灵感来源于早期的network-in-network 架构。 它是模块的堆叠,
这些模块本身看起来像是小型的独立网络,被分为多个并行分支。Inception 模块最基本的形式
包含3~4 个分支,首先是一个1×1 的卷积,然后是一个3×3 的卷积,最后将所得到的特征连
接在一起。这种设置有助于网络分别学习空间特征和逐通道的特征,这比联合学习这两种特征更
加有效。Inception 模块也可能具有更复杂的形式,通常会包含池化运算、不同尺寸的空间卷积
(比如在某些分支上使用5×5 的卷积代替3×3 的卷积)和不包含空间卷积的分支(只有一个
1×1 卷积)。下图给出这种模型的结构示例:

Inception结构

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)

注意,完整的Inception V3架构内置于Keras中,位置在keras.applications.inception_v3.
InceptionV3,其中包括在ImageNet 数据集上预训练得到的权重。与其密切相关的另一个模
型是Xception,它也是Keras 的applications 模块的一部分。Xception 代表极端Inception
(extreme inception),它是一种卷积神经网络架构,其灵感可能来自于Inception。Xception 将分别进行通道特征学习与空间特征学习的想法推向逻辑上的极端,并将Inception 模块替换为深度
可分离卷积,其中包括一个逐深度卷积(即一个空间卷积,分别对每个输入通道进行处理)和
后面的一个逐点卷积(即一个1×1 卷积)。这个深度可分离卷积实际上是Inception 模块的一种
极端形式,其空间特征和通道特征被完全分离。Xception 的参数个数与Inception V3 大致相同,
但因为它对模型参数的使用更加高效,所以在ImageNet 以及其他大规模数据集上的运行性能更
好,精度也更高。

(2)残差连接

残差连接(residual connection)是一种常见的类图网络组件,在2015 年之后的许多网络架构
(包括Xception)中都可以见到。2015 年末,来自微软的何恺明等人在ILSVRC ImageNet 挑战赛
中获胜b,其中引入了这一方法。残差连接解决了困扰所有大规模深度学习模型的两个共性问题:
梯度消失和表示瓶颈。通常来说,向任何多于10 层的模型中添加残差连接,都可能会有所帮助。
残差连接是让前面某层的输出作为后面某层的输入,从而在序列网络中有效地创造了一条
捷径。前面层的输出没有与后面层的激活连接在一起,而是与后面层的激活相加(这里假设两
个激活的形状相同)。如果它们的形状不同,我们可以用一个线性变换将前面层的激活改变成目
标形状(例如,这个线性变换可以是不带激活的Dense 层;对于卷积特征图,可以是不带激活
1×1 卷积)。
如果特征图的尺寸相同,在Keras 中实现残差连接的方法如下,用的是恒等残差连接(identity
residual connection)。这个例子假设我们有一个四维输入张量x。

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

如果特征图的尺寸不同,实现残差连接的方法如下,用的是线性残差连接(linear residual
connection)。同样,假设我们有一个四维输入张量x。

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

5.共享层权重

函数式API 还有一个重要特性,那就是能够多次重复使用一个层实例。如果你对一个层实
例调用两次,而不是每次调用都实例化一个新层,那么每次调用可以重复使用相同的权重。这
样你可以构建具有共享分支的模型,即几个分支全都共享相同的知识并执行相同的运算。也就
是说,这些分支共享相同的表示,并同时对不同的输入集合学习这些表示。
举个例子,假设一个模型想要评估两个句子之间的语义相似度。这个模型有两个输入(需
要比较的两个句子),并输出一个范围在0~1 的分数,0 表示两个句子毫不相关,1 表示两个句
子完全相同或只是换一种表述。这种模型在许多应用中都很有用,其中包括在对话系统中删除
重复的自然语言查询。
在这种设置下,两个输入句子是可以互换的,因为语义相似度是一种对称关系,A 相对
于B 的相似度等于B 相对于A 的相似度。因此,学习两个单独的模型来分别处理两个输入句
子是没有道理的。相反,你需要用一个LSTM 层来处理两个句子。这个LSTM 层的表示(即它
的权重)是同时基于两个输入来学习的。我们将其称为连体LSTM(Siamese LSTM)或共享
LSTM(shared LSTM)模型。
使用Keras 函数式API 中的层共享(层重复使用)可以实现这样的模型,其代码如下所示。

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)

自然地,一个层实例可能被多次重复使用,它可以被调用任意多次,每次都重复使用一组
相同的权重。

6.将模型作为层

重要的是,在函数式API 中,可以像使用层一样使用模型。实际上,你可以将模型看作“更
大的层”。Sequential 类和Model 类都是如此。这意味着你可以在一个输入张量上调用模型,
并得到一个输出张量。
y = model(x)
如果模型具有多个输入张量和多个输出张量,那么应该用张量列表来调用模型。
y1, y2 = model([x1, x2])
在调用模型实例时,就是在重复使用模型的权重,正如在调用层实例时,就是在重复使用
层的权重。调用一个实例,无论是层实例还是模型实例,都会重复使用这个实例已经学到的表示,
这很直观。
通过重复使用模型实例可以构建一个简单的例子,就是一个使用双摄像头作为输入的视觉
模型:两个平行的摄像头,相距几厘米(一英寸)。这样的模型可以感知深度,这在很多应用中
都很有用。你不需要两个单独的模型从左右两个摄像头中分别提取视觉特征,然后再将二者合并。
这样的底层处理可以在两个输入之间共享,即通过共享层(使用相同的权重,从而共享相同的
表示)来实现。在Keras 中实现连体视觉模型(共享卷积基)的代码如下所示。

from keras import layers
from keras import applications
from keras import Input
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)

<

font color=#999AAA >示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

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

本节将介绍在训练过程中如何更好地访问并控制模型内部过程的方法。使用model.fit()
或model.fit_generator() 在一个大型数据集上启动数十轮的训练,有点类似于扔一架纸飞
机,一开始给它一点推力,之后你便再也无法控制其飞行轨迹或着陆点。如果想要避免不好的
结果(并避免浪费纸飞机),更聪明的做法是不用纸飞机,而是用一架无人机,它可以感知其环
境,将数据发回给操纵者,并且能够基于当前状态自主航行。我们下面要介绍的技术,可以让
model.fit() 的调用从纸飞机变为智能的自主无人机,可以自我反省并动态地采取行动。

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

训练模型时,很多事情一开始都无法预测。尤其是你不知道需要多少轮才能得到最佳验证
损失。前面所有例子都采用这样一种策略:训练足够多的轮次,这时模型已经开始过拟合,根
据这第一次运行来确定训练所需要的正确轮数,然后使用这个最佳轮数从头开始再启动一次新
的训练。当然,这种方法很浪费。
处理这个问题的更好方法是,当观测到验证损失不再改善时就停止训练。这可以使用Keras
回调函数来实现。回调函数(callback)是在调用fit 时传入模型的一个对象(即实现特定方法
的类实例),它在训练过程中的不同时间点都会被模型调用。它可以访问关于模型状态与性能的
所有可用数据,还可以采取行动:中断训练、保存模型、加载一组不同的权重或改变模型的状态。
回调函数的一些用法示例如下所示。
模型检查点(model checkpointing):在训练过程中的不同时间点保存模型的当前权重。
提前终止(early stopping):如果验证损失不再改善,则中断训练(当然,同时保存在训
练过程中得到的最佳模型)。
在训练过程中动态调节某些参数值:比如优化器的学习率。
在训练过程中记录训练指标和验证指标,或将模型学到的表示可视化(这些表示也在不
断更新):你熟悉的Keras 进度条就是一个回调函数!
keras.callbacks 模块包含许多内置的回调函数,下面列出了其中一些,但还有很多没
有列出来。
keras.callbacks.ModelCheckpoint
keras.callbacks.EarlyStopping
keras.callbacks.LearningRateScheduler
keras.callbacks.ReduceLROnPlateau
keras.callbacks.CSVLogger
下面介绍其中几个回调函数,让你了解如何使用它们:ModelCheckpoint、EarlyStopping
和ReduceLROnPlateau。

(1) ModelCheckpoint 与EarlyStopping 回调函数
如果监控的目标指标在设定的轮数内不再改善,可以用EarlyStopping 回调函数来中断
训练。比如,这个回调函数可以在刚开始过拟合的时候就中断训练,从而避免用更少的轮次重
新训练模型。这个回调函数通常与ModelCheckpoint 结合使用,后者可以在训练过程中持续
不断地保存模型(你也可以选择只保存目前的最佳模型,即一轮结束后具有最佳性能的模型)。

import keras
callbacks_list = [
keras.callbacks.EarlyStopping(
monitor='acc',
patience=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))

(2) ReduceLROnPlateau 回调函数
如果验证损失不再改善,你可以使用这个回调函数来降低学习率。在训练过程中如果出现
了损失平台(loss plateau),那么增大或减小学习率都是跳出局部最小值的有效策略。下面这个
示例使用了ReduceLROnPlateau 回调函数。

callbacks_list = [
keras.callbacks.ReduceLROnPlateau(
monitor='val_loss'
factor=0.1,
patience=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 在训练结束时被调用

这些方法被调用时都有一个logs 参数,这个参数是一个字典,里面包含前一个批量、前
一个轮次或前一次训练的信息,即训练指标和验证指标等。此外,回调函数还可以访问下列属性。
self.model:调用回调函数的模型实例。
self.validation_data:传入 fit作为验证数据的值。
下面是一个自定义回调函数的简单示例,它可以在每轮结束后将模型每层的激活保存到硬
盘(格式为Numpy 数组),这个激活是对验证集的第一个样本计算得到的。
下面是一个自定义回调函数的简单示例,它可以在每轮结束后将模型每层的激活保存到硬
盘(格式为Numpy 数组),这个激活是对验证集的第一个样本计算得到的。

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

关于回调函数你只需要知道这么多,其他的都是技术细节,很容易就能查到。现在,你已
经可以在训练过程中对一个Keras 模型执行任何类型的日志记录或预定程序的干预。

2.TensorBoard 简介:TensorFlow 的可视化框架

想要做好研究或开发出好的模型,在实验过程中你需要丰富频繁的反馈,从而知道模型内
部正在发生什么。这正是运行实验的目的:获取关于模型表现好坏的信息,越多越好。取得进
展是一个反复迭代的过程(或循环):首先你有一个想法,并将其表述为一个实验,用于验证
你的想法是否正确。你运行这个实验,并处理其生成的信息。这又激发了你的下一个想法。在
这个循环中实验的迭代次数越多,你的想法也就变得越来越精确、越来越强大。Keras 可以帮你
在最短的时间内将想法转化成实验,而高速GPU 可以帮你尽快得到实验结果。但如何处理实验
结果呢?这就需要TensorBoard 发挥作用了。

循环迭代提高效果
本节将介绍TensorBoard,一个内置于TensorFlow 中的基于浏览器的可视化工具。注意,只
有当Keras 使用TensorFlow 后端时,这一方法才能用于Keras 模型。
TensorBoard 的主要用途是,在训练过程中帮助你以可视化的方法监控模型内部发生的一切。
如果你监控了除模型最终损失之外的更多信息,那么可以更清楚地了解模型做了什么、没做什么,
并且能够更快地取得进展。TensorBoard 具有下列巧妙的功能,都在浏览器中实现。
在训练过程中以可视化的方式监控指标
将模型架构可视化
将激活和梯度的直方图可视化
以三维的形式研究嵌入
我们用一个简单的例子来演示这些功能:在IMDB 情感分析任务上训练一个一维卷积神经
网络。

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

使用tensorboard首先要创建一个目录
mkdir my_log_dir
我们将日志事件写入硬盘的指定位置。

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)

现在,你可以在命令行启动TensorBoard 服务器,指示它读取回调函数当前正在写入的日志。
在安装TensorFlow 时(比如通过pip),tensorboard 程序应该已经自动安装到计算机里了。
$ tensorboard --logdir=my_log_dir
然后可以用浏览器打开http://localhost:6006,并查看模型的训练过程。除了训
练指标和验证指标的实时图表之外,你还可以访问HISTOGRAMS(直方图)标签页,并查看
美观的直方图可视化,直方图中是每层的激活值

tensorboard的监控指标

tensorboard显示激活的直方图
EMBEDDINGS(嵌入)标签页让你可以查看输入词表中2000 个单词的嵌入位置和空间关系,
它们都是由第一个Embedding 层学到的。因为嵌入空间是128 维的,所以TensorBoard 会使用
你选择的降维算法自动将其降至二维或三维,可选的降维算法有主成分分析(PCA)和t-分布
随机近邻嵌入(t-SNE)。在下图所示的点状云中,可以清楚地看到两个簇:正面含义的词和
负面含义的词。从可视化图中可以立刻明显地看出,将嵌入与特定目标联合训练得到的模型是
完全针对这个特定任务的,这也是为什么使用预训练的通用词嵌入通常不是一个好主意。

tensorboard数据可视化
GRAPHS(图)标签页显示的是Keras 模型背后的底层TensorFlow 运算图的交互式可视化
。可见,图中的内容比之前想象的要多很多。对于你刚刚构建的模型,在Keras 中
定义模型时可能看起来很简单,只是几个基本层的堆叠;但在底层,你需要构建相当复杂的图
结构来使其生效。其中许多内容都与梯度下降过程有关。你所见到的内容与你所操作的内容之
间存在这种复杂度差异,这正是你选择使用Keras 来构建模型、而不是使用原始TensorFlow 从
头开始定义所有内容的主要动机。Keras 让工作流程变得非常简单。

TensorBoard:TensorFlow 图可视化
注意,Keras 还提供了另一种更简洁的方法——keras.utils.plot_model 函数,它可以
将模型绘制为层组成的图,而不是TensorFlow 运算组成的图。使用这个函数需要安装Python 的
pydot 库和pydot-ng 库,还需要安装graphviz 库。我们来快速看一下。

from keras.utils import plot_model
plot_model(model, to_file='model.png')

将模型表示为层组成的图,由plot_model 生成
你还可以选择在层组成的图中显示形状信息。下面这个例子使用plot_model 函数及
show_shapes 选项将模型拓扑结构可视化。

from keras.utils import plot_model
plot_model(model, show_shapes=True, to_file='model.png')

带有形状信息的模型图

Keras 回调函数提供了一种简单方法,可以在训练过程中监控模型并根据模型状态自动
采取行动。
使用 TensorFlow 时,TensorBoard 是一种在浏览器中将模型活动可视化的好方法。在
Keras 模型中你可以通过TensorBoard 回调函数来使用这种方法。

三、让模型性能发挥到极致

读完前几章之后读者应该对神经网络已经做到入门了,接下来要学习如何构建高性能的神经网络。随便跑几个demo不是我们追求的目标,更高更快更强,才是我们追求的目标,而且神经网络是可以做到的。

1.高级架构模式

(1) 批标准化
标准化(normalization)是一大类方法,用于让机器学习模型看到的不同样本彼此之间更加
相似,这有助于模型的学习与对新数据的泛化。最常见的数据标准化形式就是你已经在本书中
多次见到的那种形式:将数据减去其平均值使其中心为0,然后将数据除以其标准差使其标准
差为1。实际上,这种做法假设数据服从正态分布(也叫高斯分布),并确保让该分布的中心为0,同时缩放到方差为1。
normalized_data = (data - np.mean(data, axis=…)) / np.std(data, axis=…)
前面的示例都是在将数据输入模型之前对数据做标准化。但在网络的每一次变换之后都应
该考虑数据标准化。即使输入Dense 或Conv2D 网络的数据均值为0、方差为1,也没有理由
假定网络输出的数据也是这样。
批标准化(batch normalization)是Ioffe 和Szegedy 在2015 年提出的一种层的类型a(在
Keras 中是BatchNormalization),即使在训练过程中均值和方差随时间发生变化,它也可以
适应性地将数据标准化。批标准化的工作原理是,训练过程中在内部保存已读取每批数据均值
和方差的指数移动平均值。批标准化的主要效果是,它有助于梯度传播(这一点和残差连接很
像),因此允许更深的网络。对于有些特别深的网络,只有包含多个BatchNormalization 层
时才能进行训练。例如,BatchNormalization 广泛用于Keras 内置的许多高级卷积神经网络
架构,比如ResNet50、Inception V3 和Xception。

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

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

(2)深度可分离卷积

深度可分离卷积在很多模型上可以替代卷积模型的,而且跟普通卷积比更轻量更快性能更好。
深度可分离卷积的原理是对输入的每个通道执行空间卷积,然后通过逐点卷积(1X1卷积)将输出通道混合。这样做把空间特征学习和通道特征学习分开。如果你输入中的空间位置高度相关,但是不同通道之间相对独立,那么这么做很有意义。

深度可分离卷积结构
如果只用有限的数据从头开始训练小型模型,这些优点就变得尤为重要。例如,下面这个
示例是在小型数据集上构建一个轻量的深度可分离卷积神经网络,用于图像分类任务(softmax
多分类)。

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

对于规模更大的模型,深度可分离卷积是Xception 架构的基础,Xception 是一个高性能的
卷积神经网络,内置于Keras 中。在作者的论文“Xception: deep learning with depthwise separableconvolutions”中,你可以进一步了解深度可分离卷积和Xception 的理论基础。

2.超参数优化

构建深度学习模型时,你必须做出许多看似随意的决定:应该堆叠多少层?每层应该
包含多少个单元或过滤器?激活应该使用relu 还是其他函数?在某一层之后是否应该使用
BatchNormalization ?应该使用多大的dropout 比率?还有很多。这些在架构层面的参数叫
作超参数(hyperparameter),以便将其与模型参数区分开来,后者通过反向传播进行训练。
在实践中,经验丰富的机器学习工程师和研究人员会培养出直觉,能够判断上述选择哪些
可行、哪些不可行。也就是说,他们学会了调节超参数的技巧。但是调节超参数并没有正式成
文的规则。如果你想要在某项任务上达到最佳性能,那么就不能满足于一个容易犯错的人随意
做出的选择。即使你拥有很好的直觉,最初的选择也几乎不可能是最优的。你可以手动调节你
的选择、重新训练模型,如此不停重复来改进你的选择,这也是机器学习工程师和研究人员大
部分时间都在做的事情。但是,整天调节超参数不应该是人类的工作,最好留给机器去做。
因此,你需要制定一个原则,系统性地自动探索可能的决策空间。你需要搜索架构空间,
并根据经验找到性能最佳的架构。这正是超参数自动优化领域的内容。这个领域是一个完整的
研究领域,而且很重要。
超参数优化的过程通常如下所示。
(1) 选择一组超参数(自动选择)。
(2) 构建相应的模型。
(3) 将模型在训练数据上拟合,并衡量其在验证数据上的最终性能。
(4) 选择要尝试的下一组超参数(自动选择)。
(5) 重复上述过程。
(6) 最后,衡量模型在测试数据上的性能。
这个过程的关键在于,给定许多组超参数,使用验证性能的历史来选择下一组需要评估的
超参数的算法。有多种不同的技术可供选择:贝叶斯优化、遗传算法、简单随机搜索等。
训练模型权重相对简单:在小批量数据上计算损失函数,然后用反向传播算法让权重向正
确的方向移动。与此相反,更新超参数则非常具有挑战性。我们来考虑以下两点。
计算反馈信号(这组超参数在这个任务上是否得到了一个高性能的模型)的计算代价可
能非常高,它需要在数据集上创建一个新模型并从头开始训练。
超参数空间通常由许多离散的决定组成,因而既不是连续的,也不是可微的。因此,你
通常不能在超参数空间中做梯度下降。相反,你必须依赖不使用梯度的优化方法,而这
些方法的效率比梯度下降要低很多。
这些挑战非常困难,而这个领域还很年轻,因此我们目前只能使用非常有限的工具来优
化模型。通常情况下,随机搜索(随机选择需要评估的超参数,并重复这一过程)就是最好的
解决方案,虽然这也是最简单的解决方案。但我发现有一种工具确实比随机搜索更好,它就是
Hyperopt。它是一个用于超参数优化的Python 库,其内部使用Parzen 估计器的树来预测哪组超
参数可能会得到好的结果。另一个叫作Hyperas 的库将Hyperopt 与Keras 模型集成在一起。一
定要试试。

超参数优化是深度学习中特别重要的一环,也是提高神经网络效果最重要的。未来的趋势是超参数优选自动化,把人从繁重的调参工作中解放出来。

3.模型集成

想要在一项任务上获得最佳结果,另一种强大的技术是模型集成(model ensembling)。集
成是指将一系列不同模型的预测结果汇集到一起,从而得到更好的预测结果。观察机器学习竞赛,
特别是Kaggle 上的竞赛,你会发现优胜者都是将很多模型集成到一起,它必然可以打败任何单
个模型,无论这个模型的表现多么好。
集成依赖于这样的假设,即对于独立训练的不同良好模型,它们表现良好可能是因为不同
的原因:每个模型都从略有不同的角度观察数据来做出预测,得到了“真相”的一部分,但不
是全部真相。你可能听说过盲人摸象的古代寓言:一群盲人第一次遇到大象,想要通过触摸来
了解大象。每个人都摸到了大象身体的不同部位,但只摸到了一部分,比如鼻子或一条腿。这
些人描述的大象是这样的,“它像一条蛇”“像一根柱子或一棵树”,等等。这些盲人就好比机器
学习模型,每个人都试图根据自己的假设(这些假设就是模型的独特架构和独特的随机权重初
始化)并从自己的角度来理解训练数据的多面性。每个人都得到了数据真相的一部分,但不是
全部真相。将他们的观点汇集在一起,你可以得到对数据更加准确的描述。大象是多个部分的
组合,每个盲人说的都不完全准确,但综合起来就成了一个相当准确的故事。
我们以分类问题为例。想要将一组分类器的预测结果汇集在一起[即分类器集成(ensemble
the classifiers)],最简单的方法就是将它们的预测结果取平均值作为预测结果。

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)

只有这组分类器中每一个的性能差不多一样好时,这种方法才奏效。如果其中一个分类器
性能比其他的差很多,那么最终预测结果可能不如这一组中的最佳分类器那么好。

将分类器集成有一个更聪明的做法,即加权平均,其权重在验证数据上学习得到。通常来
说,更好的分类器被赋予更大的权重,而较差的分类器则被赋予较小的权重。为了找到一组好
的集成权重,你可以使用随机搜索或简单的优化算法(比如Nelder-Mead 方法)。

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

还有许多其他变体,比如你可以对预测结果先取指数再做平均。一般来说,简单的加权平均,
其权重在验证数据上进行最优化,这是一个很强大的基准方法。
想要保证集成方法有效,关键在于这组分类器的多样性(diversity)。多样性就是力量。如
果所有盲人都只摸到大象的鼻子,那么他们会一致认为大象像蛇,并且永远不会知道大象的真
实模样。是多样性让集成方法能够取得良好效果。用机器学习的术语来说,如果所有模型的偏
差都在同一个方向上,那么集成也会保留同样的偏差。如果各个模型的偏差在不同方向上,那
么这些偏差会彼此抵消,集成结果会更加稳定、更加准确。
因此,集成的模型应该尽可能好,同时尽可能不同。这通常意味着使用非常不同的架构,
甚至使用不同类型的机器学习方法。有一件事情基本上是不值得做的,就是对相同的网络,使
用不同的随机初始化多次独立训练,然后集成。如果模型之间的唯一区别是随机初始化和训练
数据的读取顺序,那么集成的多样性很小,与单一模型相比只会有微小的改进。
我发现有一种方法在实践中非常有效(但这一方法还没有推广到所有问题领域),就是将基
于树的方法(比如随机森林或梯度提升树)和深度神经网络进行集成。2014 年,合作者Andrei
Kolev 和我使用多种树模型和深度神经网络的集成,在Kaggle 希格斯玻色子衰变探测挑战赛中
获得第四名。值得一提的是,集成中的某一个模型来源于与其他模型都不相同的方法(它是正
则化的贪婪森林),并且得分也远远低于其他模型。不出所料,它在集成中被赋予了一个很小的
权重。但出乎我们的意料,它极大地改进了总体的集成结果,因为它和其他所有模型都完全不同,
提供了其他模型都无法获得的信息。这正是集成方法的关键之处。集成不在于你的最佳模型有
多好,而在于候选模型集合的多样性。
近年来,一种在实践中非常成功的基本集成方法是宽且深(wide and deep)的模型类型,
它结合了深度学习与浅层学习。这种模型联合训练一个深度神经网络和一个大型的线性模型。
对多种模型联合训练,是实现模型集成的另一种选择。

总结

本章我们学习了以下内容。
如何将模型构建为层组成的图、层的重复使用(层权重共享)与将模型用作 Python 函
数(模型模板)。
你可以使用 Keras 回调函数在训练过程中监控模型,并根据模型状态采取行动。
TensorBoard 可以将指标、激活直方图甚至嵌入空间可视化。
什么是批标准化、深度可分离卷积和残差连接。
为什么应该使用超参数优化和模型集成。
借助这些新工具,你可以在现实世界中更好地利用深度学习,并可以开始构建具有高度
竞争力的深度学习模型。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值