我是靠谱客的博主 鳗鱼可乐,最近开发中收集的这篇文章主要介绍Python深度学习(不用 Sequential 模型的解决方案:Keras 函数式 API)--学习笔记(十五)第7章 高级的深度学习最佳实践,觉得挺不错的,现在分享给大家,希望可以做个参考。
概述
第7章 高级的深度学习最佳实践
7.1 不用Sequential模型的解决方案:Keras函数式API
- 到目前为止,介绍的所有神经网络都是用Sequential模型实现的。Sequential模型假设,网络只有一个输入和一个输出,而且网络是层的线性堆叠。
- 这是一个经过普遍验证的假设。这种网络配置非常常见,以至于前面只用Sequential模型类就能够涵盖许多主题和实际应用。但有些情况下这种假设过于死板。有些网络需要多个独立的输入,有些网络则需要多个输出,而有些网络在层与层之间具有内部分支,这使得网络看起来像是层构成的图(graph),而不是层的线性堆叠。
- 例如,有些任务需要多模态(multimodal)输入。这些任务合并来自不同输入源的数据,并使用不同类型的神经层处理不同类型的数据。假设有一个深度学习模型,试图利用下列输入来预测一件二手衣服最可能的市场价格:用户提供的元数据(比如商品品牌、已使用年限等)、用户提供的文本描述与商品照片。如果你只有元数据,那么可以使用one-hot编码,然后用密集连接网络来预测价格。如果你只有文本描述,那么可以使用循环神经网络或一维卷积神经网络。如果你只有图像,那么可以使用二维卷积神经网络。但怎么才能同时使用这三种数据呢?一种朴素的方法是训练三个独立的模型,然后对三者的预测做加权平均。但这种方法可能不是最优的,因为模型提取的信息可能存在冗余。更好的方法是使用一个可以同时查看所有可用的输入模态的模型,从而联合学习一个更加精确的数据模型。
- 同样,有些任务需要预测输入数据的多个目标属性。给定一部小说的文本,你可能希望将它按类别自动分类(比如爱情小说或惊悚小说),同时还希望预测其大致的写作日期。当然,你可以训练两个独立的模型:一个用户划分类别,一个用于预测日期。但由于这些属性并不是统计无关的,你可以构建一个更好的模型,用这个模型来学习同时预测类别和日期。这种联合模型将有两个输出,或者说两个头(head)。因为类别和日期之间具有相关性,所以知道小说的写作日期有助于模型在小说类别的空间中学到丰富而又准确的表示,反之亦然。
- 此外,许多最新开发的神经架构要求非线性的网络拓扑结构,即网络结构为有向无环图。比如,Inception系列网络(由Google的Szegedy等人开发)依赖于Inception模块,其输入被多个并行的卷积分支所处理,然后将这些分支的输出合并为单个张量。最近还有一种趋势是向模型中添加残差连接(residual connection),它最早出现于ResNet系列网络(由微软的何凯明等人开发)。残差连接是将前面的输出张量与后面的输出张量相加,从而将前面的表示重新诸如下游数据流中,这有助于防止信息处理流程中的信息损失。
- 这三个重要的使用案例(多输入模型、多输出模型和类图模型),只用Keras中的Sequential模型类是无法实现的。但是还有另一种更加通用、更加灵活的使用Keras的方式,就是函数式API(function 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
seq_model = Sequential() # 前面学过的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'))
print(seq_model.summary())
# 对应的函数式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 = Model(input_tensor, output_tensor)
print(model.summary())
- 这里只有一点可能看起来有点神奇,就是将Model对象实例化只用了一个输入张量和一个输出张量。Keras会在后台检索从input_tensor到output_tensor所包含的每一层,并将这些层组合成一个类图的数据结构,即一个Model。当然,这种方法有效的原因在于,output_tensor是通过对input_tensor进行多次变换得到的。如果你试图利用不相关的输入和输出来构建一个模型,那么会得到RuntimeError。
- 对这种Model实例进行编译、训练或评估时,其API与Sequential模型相同。
model.compile(optimizer='rmsprop', loss='categorical_crossentropy') # 编译模型
import numpy as np
# 生成用于训练的虚构Numpy数据
x_train = np.random.random((1000, 64))
y_train = np.random.random((1000, 10))
# 训练10轮模型
model.fit(x_train, y_train, epochs=10, batch_size=128)
score = model.evaluate(x_train, y_train)
7.1.2 多输入模型
- 函数API可用于构建具有多个输入的模型。通常情况下,这种模型会在某一时刻用一个可以组合多个张量的层将不同的输入分支合并,张量组合方式可能是相加、连接等。这通常利用Keras的合并运算来实现,比如keras.layers.add、keras.layers.concatenate等。我们来看一个非常简单的多输入模型示例-一个问答模型。
- 典型的问答模型有两个输入:一个自然语言描述的问题和一个文本片段(比如新闻文章),后者提供用于回答问题的信息。然后模型要生成一个回答,在最简单的情况下,这个回答只包含一个词,可以通过对某个预定义的词表做softmax得到。
- 设置两个分支,首先将文本输入和问题输入分别编码为表示向量,然后连接这些向量,最后,在连接好的表示上添加一个softmax分类器。
# 用函数式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_inpit = Input(shape=(None, ), dtype='int32', name='text')
# 将输入嵌入长度为64的向量
embedded_text = layers.Embedding(text_vocabulary_size, 64)(text_inpit)
# 利用LSTM将向量编码为单个向量
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)
# 在上面添加一个softmax分类器
answer = layers.Dense(answer_vocabulary_size, activation='softmax')(concatenated)
# 在模型实例化时,指定两个输入和输出
model = Model([text_inpit, question_input], answer)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc'])
- 接下来要如何训练这个双输入模型呢?有两个可用的API:我们可以向模型输入一个由Numpy数组组成的列表,或者也可以输入一个将输入名称映射为Numpy数组的字典。当然,只有输入具有名称时才能使用后一种方法。
# 将数据输入到多输入模型中
import numpy as np
import keras
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)
7.1.3 多输出模型
- 利用相同的方法,我们还可以使用函数式API来构建具有多个输出(或多头)的模型。一个简单的例子就是一个网络试图同时预测数据的不同性质,比如一个网络,输入某个匿名人士的一系列社交媒体发帖,然后尝试预测那个人的属性,比如年龄、性别和收入水平。
# 用函数式API实现一个三输出模型
from keras import layers
from keras import Input
from keras.models import Model
vocabulary_size = 5000
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.GlobalMaxPool1D()(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'})
- 注意,严重不平衡的损失贡献会导致模型表示针对单个损失值最大的任务优先进行优化,而不考虑其他任务的优化。为了解决这个问题,我们可以为每个损失值对最终损失的贡献分配不同大小的重要性。如果不同的损失具有不同的取值范围,那么这一方法尤其有用。比如,用于年龄回归任务的均方误差(MSE)损失值通常在3~5左右,而用于性别分类任务的交叉熵损失值可能低至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.})
- 与多输入模型相同,多输出模型的训练输入数据可以是Numpy数组组成的列表或字典。
# 将数据输入到多输出模型中
# 假设age_targets、income_targets、gender_targets都是Numpy数组
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,我们不仅可以构建多输入和多输出的模型,而且还可以实现具有复杂的内部拓扑结构的网络。Keras中的神经网络可以是层组成的任意有向无环图(directed acyclic graph)。无环(acyclic)这个限定词很重要,即这些图不能有循环。张量x不能成为生成x的某一层的输入。唯一允许的处理循环(即循环连接)是循环层的内部循环。
- 一些常见的神经网络组建都以图的形式实现。两个著名的组件是Inception模块和残差连接。
1.Inception模块
- Inception是一种流行的卷积神经网络的架构类型,它由Google的Christian Szegedy及其同事在2013-2014年开发,其灵感来源于早期network-in-network架构。它是模块的堆叠,这些模块本身看起来像是小型的独立网络,被分为多个并行分支。Inception模块最基本的形式包含3~4个分支,首先是一个1x1的卷积,然后是一个3x3的卷积,最后将所得到的特征连接在一起。这种设置有助于网络分别学习空间特征和逐通道的特征,这比联合学习者两种特征更加有效。Inception模块也可能具有更复杂的形式,通常会包含池化运算、不同尺寸的空间卷积(比如在某些分支上使用5x5的卷积代替3x3的卷积)和不包含空间卷积的分支(只有一个1x1卷积)。
- 1x1卷积的作用:我们已经知道,卷积能够在输入张量的每一个方块周围提取空间图块,并对所有图块应用相同的变换。极端情况是提取的图块只包含一个方块。这时卷积运算等价于让每个方块向量经过一个Dense层:它计算得到的特征能够将输入张量通道中的信息混合在一起,但不会将跨空间的信息混合在一起(因为它一次只查看一个方块)。这种1x1卷积(也叫作逐点卷积(pointwise convolution))是Inception模块的特色,它有助于区分开通道特征学习和空间特征学习。如果你假设每个通道在跨越空间时是高度自相关的,但不同的通道之间可能并不高度相关,那么这种做法是合理的。
from keras import layers
# 假设有一个四维的输入张量x
# 每个分支都有相同的步幅值(2),这对于保持所有分支输出具有相同的尺寸是很有必要的,这样才能将它们连接在一起
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模块替换为深度可分离卷积,其中包括一个逐深度卷积(即一个空间卷积,分别对每个输入通道进行处理)和后面一个逐点卷积。这个深度可分离卷积实际上是Inception模块的一种极端形式,其空间特征和通道特征被完全分离。Xception的参数个数与Inception V3大致相同,但因为它对模型参数的使用更加高效,所以在ImageNet以及其他大规模数据上的运行性能更好,精度也更高。
2 残差连接
- 残差连接(residual connection)是一种常见的类图网络组件,在2015年之后的许多网络架构(包括Xception)中都可以见到。2015年末,来自微软的何凯明等人在ILSVRC ImageNet挑战赛中获胜,其中引入了这一方法。残差连接解决了困扰所有大规模深度学习模型的两个共性问题:梯度消失和表示瓶颈。通常来说,向任何多于10层的模型中添加残差连接,都可能会有所帮助。
- 残差连接是让前面某层的输出作为后面某层的输入,从而在序列网络中有效地创造了一条捷径。前面层的输出没有与后面层的激活连接在一起,而是与后面层的激活相加(这里假设两个激活的形状相同)。如果它们的形状不同,我们可以用一个线性变换将前面层的激活改变成目标形状(例如,这个线性变换可以是不带激活的Dense层;对于卷积特征图,可以是不带激活的1x1卷积)。
- 如果特征图的尺寸相同,在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)
# 将原始x与输出特征相加
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)
# 使用1x1卷积,将原始x张量线性下采样为与y具有相同形状
residual = layers.Conv2D(128, 1, strides=2, padding='same')(x)
# 将残差张量与输出特征相加
y = layers.add([y, residual])
- 深度学习中的表示瓶颈:在Sequential模型中,每个连续的表示层都构建于前一层之上,这意味着它只能访问前一层激活中包含的信息。如果某一层太小(比如特征维度太低),那么模型将会受限于该层激活中能够塞入多少信息。可以通过类比信号处理来理解这个概念:假设你有一条包含一系列操作的音频处理流水线,每个操作的输入都是前一个操作的输出,如果某个操作将信号裁剪到低频范围(比如0~15kHz),那么下游操作将永远无法恢复那些被丢弃的频段。任何信息的丢失都是永久性的。残差连接可以将较早的信息重新诸如到下游数据中,从而部分解决了深度学习模型这一问题。
- 深度学习中的梯度消失:反向传播是用于训练深度神经网络的主要算法,其工作原理是将来自输出损失的反馈信号向下传播到更底部的层。如果这个反馈信号的传播需要经过很多层,那么信号可能会变得非常微弱,甚至完全丢失,导致网络无法训练。这个问题被称为梯度消失(vanishing gradient)。深度网络中存在这个问题,在很长序列上的循环网络也存在这个问题。在这两种情况下,反馈信号的传播都必须通过一长串操作。我们已经知道LSTM层是如何在循环网络中解决这个问题的:它引入了一个携带轨道(carry track),可以在与主处理轨道平行的轨道上传播信息。残差连接在前馈深度网络中的工作原理与此类似,但它更加简单:它引入了一个纯线性的信息携带轨道,与主要的层堆叠方向平行,从而有助于跨越任意深度的层来传播梯度。
7.1.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层实例化一次
lstm = layers.LSTM(32)
# 构建模型的左分支:输入是长度128的向量组成的变长序列
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_input, right_input], axis=-1)
predictions = layers.Dense(1, activation='sigmoid')(merged)
# 将模型实例化并训练:训练这种模式时,基于两个输入对LSTM层的权重进行更新
model = Model([left_input, right_input], predictions)
model.fit([left_data, right_data], targets)
- 自然地,一个层实例可能被多次重复使用,它可以被调用任意多次,每次都重复使用一组相同的权重。
7.1.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网络(只包括卷积基)
xception_base = applications.Xception(weights=None, include_top=False)
# 输入时250x250的RGB图像
left_input = Input(shape=(250, 250,3))
right_input = Input(shape=(250, 250, 3))
left_features = xception_base(left_input)
right_features = xception_base(right_input)
# 合并后特征包含来自左右两个视觉输入中的信息
merged_features = layers.concatenate([left_features, right_features], axis=-1)
最后
以上就是鳗鱼可乐为你收集整理的Python深度学习(不用 Sequential 模型的解决方案:Keras 函数式 API)--学习笔记(十五)第7章 高级的深度学习最佳实践的全部内容,希望文章能够帮你解决Python深度学习(不用 Sequential 模型的解决方案:Keras 函数式 API)--学习笔记(十五)第7章 高级的深度学习最佳实践所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复