原文:
annas-archive.org/md5/39be8fc3387902d01265692ab3d9cda6
译者:飞龙
第十章:使用高级 RNN 预测时间序列
本章涵盖了递归神经网络的高级技术。
在第二章中看到的技术,使用前馈网络分类手写数字,对于前馈网络,例如通过增加更多层次或添加 Dropout 层等,已经成为递归网络面临的挑战,并且需要一些新的设计原则。
由于增加新层会加剧消失/爆炸梯度问题,一种基于身份连接的新技术,如在第七章中所述,使用残差网络分类图像,已证明能够提供最先进的结果。
涵盖的主题包括:
-
变分 RNN
-
堆叠 RNN
-
深度过渡 RNN
-
高速公路连接及其在 RNN 中的应用
RNN 的 Dropout
Dropout 在神经网络中的应用一直是研究的主题,因为简单地将 Dropout 应用于递归连接会引入更多的不稳定性和训练 RNN 的困难。
一种解决方案已经被发现,它源自变分贝叶斯网络理论。最终的思想非常简单, consiste of 保持相同的 Dropout 掩码用于整个 RNN 训练序列,如下图所示,并在每个新序列上生成新的 Dropout 掩码:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00179.jpeg
这种技术被称为变分 RNN。对于前图中具有相同箭头的连接,我们将为整个序列保持噪声掩码不变。
为此,我们将引入符号变量_is_training
和_noise_x
,在训练过程中为输入、输出和递归连接添加随机(变分)噪声(Dropout):
_is_training = T.iscalar('is_training')
_noise_x = T.matrix('noise_x')
inputs = apply_dropout(_is_training, inputs, T.shape_padright(_noise_x.T))
RNN 的深度方法
深度学习的核心原理是通过增加更多层次来提升网络的表示能力。对于 RNN,增加层数有两种可能的方式:
- 第一个方法被称为堆叠或堆叠递归网络,其中第一个递归网络的隐藏层输出作为第二个递归网络的输入,依此类推,多个递归网络层叠在一起:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00180.jpeg
对于深度d和T时间步长,输入与输出之间的最大连接数为d + T – 1:
-
第二种方法是深度过渡网络,它通过向递归连接中添加更多层次来实现:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00181.jpeg
图 2
在这种情况下,输入与输出之间的最大连接数为d x T,已被证明更加强大。
两种方法都能提供更好的结果。
然而,在第二种方法中,随着层数的增加,训练变得更加复杂和不稳定,因为信号会更快地消失或爆炸。我们将在稍后通过处理递归高速公路连接的原理来解决这个问题。
首先,像往常一样,将作为词汇索引值数组的单词序列,维度为(batch_size, num_steps
),嵌入到维度为(num_steps, batch_size, hidden_size
)的输入张量中:
embedding = shared_uniform(( config.vocab_size,config.hidden_size), config.init_scale)
params = [embedding]
inputs = embedding[_input_data.T]
符号输入变量_lr
使得在训练过程中可以减少学习率:
_lr = theano.shared(cast_floatX(config.learning_rate), 'lr')
我们从第一种方法开始,即堆叠递归网络。
堆叠递归网络
要堆叠递归网络,我们将下一个递归网络的隐藏层连接到前一个递归网络的输入:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00182.jpeg
当层数为一时,我们的实现就是前一章中的递归网络。
首先,我们在简单的 RNN 模型中实现了 dropout:
def model(inputs, _is_training, params, batch_size, hidden_size, drop_i, drop_s, init_scale, init_H_bias):
noise_i_for_H = get_dropout_noise((batch_size, hidden_size), drop_i)
i_for_H = apply_dropout(_is_training, inputs, noise_i_for_H)
i_for_H = linear.model(i_for_H, params, hidden_size,
hidden_size, init_scale, bias_init=init_H_bias)
# Dropout noise for recurrent hidden state.
noise_s = get_dropout_noise((batch_size, hidden_size), drop_s)
def step(i_for_H_t, y_tm1, noise_s):
s_lm1_for_H = apply_dropout(_is_training,y_tm1, noise_s)
return T.tanh(i_for_H_t + linear.model(s_lm1_for_H,
params, hidden_size, hidden_size, init_scale))
y_0 = shared_zeros((batch_size, hidden_size), name='h0')
y, _ = theano.scan(step, sequences=i_for_H, outputs_info=[y_0], non_sequences = [noise_s])
y_last = y[-1]
sticky_state_updates = [(y_0, y_last)]
return y, y_0, sticky_state_updates
我们在 LSTM 模型中做同样的事情:
def model(inputs, _is_training, params, batch_size, hidden_size, drop_i, drop_s, init_scale, init_H_bias, tied_noise):
noise_i_for_i = get_dropout_noise((batch_size, hidden_size), drop_i)
noise_i_for_f = get_dropout_noise((batch_size, hidden_size), drop_i) if not tied_noise else noise_i_for_i
noise_i_for_c = get_dropout_noise((batch_size, hidden_size), drop_i) if not tied_noise else noise_i_for_i
noise_i_for_o = get_dropout_noise((batch_size, hidden_size), drop_i) if not tied_noise else noise_i_for_i
i_for_i = apply_dropout(_is_training, inputs, noise_i_for_i)
i_for_f = apply_dropout(_is_training, inputs, noise_i_for_f)
i_for_c = apply_dropout(_is_training, inputs, noise_i_for_c)
i_for_o = apply_dropout(_is_training, inputs, noise_i_for_o)
i_for_i = linear.model(i_for_i, params, hidden_size, hidden_size, init_scale, bias_init=init_H_bias)
i_for_f = linear.model(i_for_f, params, hidden_size, hidden_size, init_scale, bias_init=init_H_bias)
i_for_c = linear.model(i_for_c, params, hidden_size, hidden_size, init_scale, bias_init=init_H_bias)
i_for_o = linear.model(i_for_o, params, hidden_size, hidden_size, init_scale, bias_init=init_H_bias)
# Dropout noise for recurrent hidden state.
noise_s = get_dropout_noise((batch_size, hidden_size), drop_s)
if not tied_noise:
noise_s = T.stack(noise_s, get_dropout_noise((batch_size, hidden_size), drop_s),
get_dropout_noise((batch_size, hidden_size), drop_s), get_dropout_noise((batch_size, hidden_size), drop_s))
def step(i_for_i_t,i_for_f_t,i_for_c_t,i_for_o_t, y_tm1, c_tm1, noise_s):
noise_s_for_i = noise_s if tied_noise else noise_s[0]
noise_s_for_f = noise_s if tied_noise else noise_s[1]
noise_s_for_c = noise_s if tied_noise else noise_s[2]
noise_s_for_o = noise_s if tied_noise else noise_s[3]
s_lm1_for_i = apply_dropout(_is_training,y_tm1, noise_s_for_i)
s_lm1_for_f = apply_dropout(_is_training,y_tm1, noise_s_for_f)
s_lm1_for_c = apply_dropout(_is_training,y_tm1, noise_s_for_c)
s_lm1_for_o = apply_dropout(_is_training,y_tm1, noise_s_for_o)
i_t = T.nnet.sigmoid(i_for_i_t + linear.model(s_lm1_for_i, params, hidden_size, hidden_size, init_scale))
f_t = T.nnet.sigmoid(i_for_o_t + linear.model(s_lm1_for_f, params, hidden_size, hidden_size, init_scale))
c_t = f_t * c_tm1 + i_t * T.tanh(i_for_c_t + linear.model(s_lm1_for_c, params, hidden_size, hidden_size, init_scale))
o_t = T.nnet.sigmoid(i_for_o_t + linear.model(s_lm1_for_o, params, hidden_size, hidden_size, init_scale))
return o_t * T.tanh(c_t), c_t
y_0 = shared_zeros((batch_size,hidden_size), name='h0')
c_0 = shared_zeros((batch_size,hidden_size), name='c0')
[y, c], _ = theano.scan(step, sequences=[i_for_i,i_for_f,i_for_c,i_for_o], outputs_info=[y_0,c_0], non_sequences = [noise_s])
y_last = y[-1]
sticky_state_updates = [(y_0, y_last)]
return y, y_0, sticky_state_updates
运行我们的堆叠网络:
python train_stacked.py --model=rnn
python train_stacked.py --model=lstm
对于 RNN,我们得到了 15,203,150 个参数,在 CPU 上的速度为 326 每秒字数(WPS),在 GPU 上的速度为 4,806 WPS。
对于 LSTM,参数数量为 35,882,600,在 GPU 上的速度为 1,445 WPS。
堆叠 RNN 没有收敛,正如我们预想的那样:随着深度增加,梯度消失/爆炸问题加剧。
LSTM,旨在减少此类问题,在堆叠时的收敛效果远好于单层网络。
深度转移递归网络
与堆叠递归网络相反,深度转移递归网络通过在递归连接中增加更多的层次或微时间步,来沿时间方向增加网络的深度。
为了说明这一点,让我们回到递归网络中转移/递归连接的定义:它以前一状态https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00183.jpeg和时间步t时的输入数据https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00184.jpeg为输入,预测其新状态https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00185.jpeg。
在深度转移递归网络(图 2)中,递归转移通过多个层次开发,直到递归深度L:初始状态被设置为最后一个转移的输出:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00186.jpeg
此外,在转移中,计算多个状态或步骤:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00187.jpeg
最终状态是转移的输出:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00188.jpeg
高速公路网络设计原理
在转移连接中增加更多层会在长时间依赖中增加梯度消失或爆炸问题,特别是在反向传播过程中。
在 第四章,使用递归神经网络生成文本 中,已经介绍了 LSTM 和 GRU 网络作为解决方案来应对这个问题。二阶优化技术也有助于克服这个问题。
一个更一般的原理,基于 恒等连接,用于改善深度网络的训练,第七章,使用残差网络分类图像,也可以应用于深度过渡网络。
这是理论上的原理:
给定一个输入 x 到隐藏层 H,并带有权重 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00189.jpeg:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00190.jpeg
一个高速公路网络设计包括将原始输入信息(通过恒等层)添加到一层或一组层的输出,作为快捷通道:
y = x
两个混合门,变换门 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00191.jpeg 和 传递门,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00192.jpeg 学会调节隐藏层中变换的影响,以及允许通过的原始信息量:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00193.jpeg
通常,为了减少总参数量以便加速训练网络,carry
门被设置为 transform
门的互补:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00194.jpeg
递归高速公路网络
因此,让我们将高速公路网络设计应用于深度过渡递归网络,从而定义 递归高速公路网络(RHN),并根据给定的过渡输入预测输出 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00185.jpeg 和 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00183.jpeg:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00186.jpeg
过渡是通过多个步骤的高速公路连接构建的:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00195.jpeghttps://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00188.jpeg
这里的变换门如下:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00196.jpeg
为了减少权重数量,carry
门被作为 transform
门的互补:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00194.jpeg
为了在 GPU 上更快地计算,最好将不同时间步长的输入上的线性变换通过单次大矩阵乘法计算,即一次性计算所有时间步长的输入矩阵 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00199.jpeg 和 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00200.jpeg,因为 GPU 会更好地并行化这些操作,并将这些输入提供给递归:
y_0 = shared_zeros((batch_size, hidden_size))
y, _ = theano.scan(deep_step_fn, sequences = [i_for_H, i_for_T],
outputs_info = [y_0], non_sequences = [noise_s])
每个步骤之间有深度过渡:
def deep_step_fn(i_for_H_t, i_for_T_t, y_tm1, noise_s):
s_lm1 = y_tm1
for l in range(transition_depth):
if l == 0:
H = T.tanh(i_for_H_t + linear(s_lm1, params, hidden_size, hidden_size, init_scale))
Tr = T.nnet.sigmoid(i_for_T_t + linear(s_lm1, params, hidden_size, hidden_size, init_scale))
else:
H = T.tanh(linear(s_lm1, params, hidden_size, hidden_size, init_scale, bias_init=init_H_bias))
Tr = T.nnet.sigmoid(linear(s_lm1, params, hidden_size, hidden_size, init_scale, bias_init=init_T_bias))
s_l = H * Tr + s_lm1 * ( 1 - Tr )
s_lm1 = s_l
y_t = s_l
return y_t
RHN 的递归隐藏状态是粘性的(一个批次的最后一个隐藏状态会传递到下一个批次,作为初始隐藏状态使用)。这些状态被保存在一个共享变量中。
让我们运行模式:
python train_stacked.py
堆叠的 RHN 的参数数量为84,172,000,其在 GPU 上的速度为420 wps。
该模型是当前在文本上递归神经网络准确度的最新最先进模型。
深入阅读
您可以参考以下主题以获取更多见解:
-
高速公路网络:
arxiv.org/abs/1505.00387
-
深度门控 LSTM:
arxiv.org/abs/1508.03790
-
学习递归神经网络中的长期记忆:
arxiv.org/abs/1412.7753
-
网格长短期记忆,Nal Kalchbrenner, Ivo Danihelka, Alex Graves
-
Zilly, J, Srivastava, R, Koutnik, J, Schmidhuber, J., 递归高速公路网络,2016
-
Gal, Y, 递归神经网络中丢弃法的理论基础应用,2015。
-
Zaremba, W, Sutskever, I, Vinyals, O, 递归神经网络正则化,2014。
-
Press, O, Wolf, L, 利用输出嵌入改进语言模型,2016。
-
门控反馈递归神经网络:Junyoung Chung, Caglar Gulcehre, Kyunghyun Cho, Yoshua Bengio 2015
-
时钟工作递归神经网络:Jan Koutník, Klaus Greff, Faustino Gomez, Jürgen Schmidhuber 2014
摘要
一种经典的丢弃法可用于提高网络的鲁棒性,避免递归转换的不稳定性和破坏,且可以在递归网络的序列或批次中应用。例如,当应用于单词输入/输出时,相当于从句子中去除相同的单词,将其替换为空值。
深度学习中堆叠层的原则,通过在深度方向堆叠递归网络而不产生负担,能够提高准确性。
将相同的原则应用于递归网络的转换中,会增加消失/爆炸问题,但通过引入具有身份连接的高速公路网络来抵消这一问题。
递归神经网络的高级技术在序列预测中给出了最先进的结果。
第十一章 从环境中学习强化
监督学习和无监督学习描述了训练过程中标签或目标的存在与否。对于代理来说,更自然的学习环境是在正确决策时获得奖励。在复杂环境中,这种奖励,例如正确打网球,可能是多个动作的结果,延迟或累积。
为了优化人工代理从环境中获取的奖励,强化学习(RL)领域出现了许多算法,如 Q 学习或蒙特卡洛树搜索,并且随着深度学习的出现,这些算法已经演变为新的方法,如深度 Q 网络,策略网络,值网络和策略梯度。
我们将首先介绍强化学习框架及其在虚拟环境中的潜在应用。然后,我们将发展其算法及其与深度学习的整合,后者解决了人工智能中最具挑战性的问题。
本章涵盖的要点如下:
-
强化学习
-
模拟环境
-
Q 学习
-
蒙特卡洛树搜索
-
深度 Q 网络
-
策略梯度
-
异步梯度下降
为了简化本章中神经网络的开发,我们将使用 Keras,这是基于我在第五章中介绍的 Theano 之上的高级深度学习库,使用双向 LSTM 分析情感。
强化学习任务
强化学习包括训练一个代理,它只需偶尔从环境中获得反馈,就可以学会在结束时获得最佳反馈。代理执行动作,修改环境的状态。
在环境中导航的动作可以表示为从一个状态到另一个状态的有向边,如下图所示:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00201.jpeg
一个机器人在真实环境中工作(步行机器人,电机控制等)或虚拟环境中(视频游戏,在线游戏,聊天室等),必须决定哪些动作(或按键)可以获得最大的奖励:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00202.jpeg
模拟环境
虚拟环境使得能够模拟数以万计甚至百万级的游戏过程,仅需计算成本。为了评估不同强化学习算法的性能,研究界开发了模拟环境。
为了找到能够很好地泛化的解决方案,Open-AI,一个由商业巨头埃隆·马斯克支持的非盈利人工智能研究公司,致力于以有益于全人类的方式小心推广和开发友好的人工智能,已经在其开源模拟环境Open-AI Gym(gym.openai.com/
)中收集了一系列强化学习任务和环境,供我们在其中测试自己的方法。在这些环境中,你会找到:
-
来自 Atari 2600 的视频游戏,Atari 公司于 1977 年发布的家庭视频游戏机,封装了来自街机学习环境的模拟器,这是最常见的强化学习基准环境之一:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00203.jpeg
-
MuJoCo,评估智能体在连续控制任务中的物理模拟器:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00204.jpeg
-
其他著名游戏,如 Minecraft、足球、Doom 等:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00205.jpeg
让我们安装 Gym 及其 Atari 2600 环境:
pip install gym
pip install gym[atari]
也可以通过以下方式安装所有环境:
pip install gym[all]
与 Gym 环境交互非常简单,只需使用env.step()
方法,给定我们为智能体选择的动作,该方法返回新的状态、奖励,以及游戏是否已结束。
例如,让我们采样一个随机动作:
import gym
env = gym.make('CartPole-v0')
env.reset()
for _ in range(1000):
env.render()
action = env.action_space.sample()
next_state, reward, done, info = env.step(action)
if done:
env.reset()
Gym 还提供了复杂的监控方法,可以录制视频和算法表现。这些记录可以上传到 Open-AI API,与其他算法一起评分。
你也可以看看:
-
3D 赛车模拟器 Torcs(
torcs.sourceforge.net/
),比起简单的 Atari 游戏,它在动作的离散化上更小,更加真实,但奖励更稀疏,而且比 MuJoCo 中的连续电机控制动作还少:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00206.jpeg -
一个名为迷宫的 3D 环境,用于随机生成迷宫:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00207.jpeg
Q 学习
解决游戏的一个主要方法是 Q 学习方法。为了完全理解这一方法,以下是一个简单的例子,环境的状态数限制为 6,状态0为入口,状态5为出口。在每个阶段,一些动作可以使智能体跳到另一个状态,如下图所示:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00208.jpeg
当智能体从状态4跳到状态5时,奖励是 100。在其他状态中没有奖励,因为在这个例子中游戏的目标是找到出口。奖励是时间延迟的,智能体必须从状态 0 滚动通过多个状态到达状态 4,才能找到出口。
在这种情况下,Q 学习的任务是学习一个 Q 矩阵,表示状态-动作对的价值:
-
Q 矩阵的每一行对应智能体可能处于的一个状态
-
每一列表示从该状态出发到达的目标状态
代表选择该状态中的行动将如何使我们接近出口的价值。如果从状态i到状态j没有任何行动,我们在 Q 矩阵的位置*(i,j)定义为零或负值。如果从状态i到状态j有一个或多个可能的行动,那么 Q 矩阵中的值将被选择来表示状态j*如何帮助我们实现目标。
例如,离开状态 3 到状态 0 将使代理远离出口,而离开状态 3 到状态 4 将使我们更接近目标。一个常用的算法,称为贪婪算法,在离散空间中估计Q,由递归的贝尔曼方程给出,已被证明收敛:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00209.jpeg
在这里,S’是在状态S上执行动作a后的新状态;r定义了从状态S到*S’*路径上的奖励(在这种情况下为空),https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00210.jpeg是折扣因子,用于阻止到图中距离太远的状态的动作。多次应用该方程将导致以下 Q 值:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00211.jpeg
在 Q 学习中,Q代表质量,表示行动获得最佳奖励的能力。由于延迟奖励被折扣,这些值对应于每对(状态,行动)的最大折扣未来奖励。
注意,只要我们知道搜索子树的输出节点的状态值,完整图形的结果就不是必需的:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00212.jpeg
在这张图中,节点 1 和 3 的值为 10 是最优状态值函数 v(s);也就是说,在完美游戏/最佳路径下的游戏结果。实际上,确切的值函数是未知的,但是是近似的。
这种近似与 DeepMind 算法 AlphaGo 中的**蒙特卡洛树搜索(MCTS)**结合使用,以击败围棋世界冠军。MCTS 包括在给定策略下对动作进行抽样,从而仅保留当前节点到估计其 Q 值的最可能动作在贝尔曼方程中:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00213.jpeg
深度 Q 网络
尽管可能的行动数量通常有限(键盘键数或移动数),但可能的状态数量可能极其庞大,搜索空间可能非常庞大,例如,在配备摄像头的机器人在真实环境或现实视频游戏中。自然而然地,我们会使用计算机视觉神经网络,例如我们在第七章中用于分类的那些网络,来代表给定输入图像(状态)的行动价值,而不是一个矩阵:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00214.jpeg
Q 网络被称为状态-动作值网络,它根据给定的状态预测动作值。为了训练 Q 网络,一种自然的方式是通过梯度下降使其符合贝尔曼方程:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00215.jpeg
请注意,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00216.jpeg被评估并固定,而下降是针对https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00217.jpeg中的导数计算的,且每个状态的值可以估算为所有状态-动作值的最大值。
在用随机权重初始化 Q 网络后,初始预测是随机的,但随着网络的收敛,给定特定状态的动作将变得越来越可预测,因此对新状态的探索减少。利用在线训练的模型需要强迫算法继续探索:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00218.jpeg 贪心方法包括以概率 epsilon 做一个随机动作,否则跟随 Q 网络给出的最大奖励动作。这是一种通过试错学习的方式。在经过一定数量的训练轮次后,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00218.jpeg会衰减,以减少探索。
训练稳定性
有不同的方法可以在训练过程中改善稳定性。在线训练,即在玩游戏时训练模型,遗忘之前的经验,只考虑最后一条经验,对于深度神经网络来说是根本不稳定的:时间上接近的状态(例如最新的状态)通常是高度相似或相关的,训练时使用最新的状态不容易收敛。
为了避免这种失败,一个可能的解决方案是将经验存储在回放记忆中,或者使用人类游戏记录数据库。批量处理和打乱从回放记忆或人类游戏记录数据库中抽取的随机样本能够实现更稳定的训练,但属于离策略训练。
改善稳定性的第二个解决方案是将参数https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00219.jpeg的值固定在目标评估https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00216.jpeg中,并进行数千次更新https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00217.jpeg,以减少目标值和 Q 值之间的相关性:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00220.jpeg
通过 n 步 Q 学习,能够更高效地训练,将奖励传播到n个先前的动作,而不是一个:
Q 学习公式:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00221.jpeg
n 步 Q 学习公式:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00222.jpeg
在这里,每一步都会受益于n个后续奖励:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00223.jpeg
训练稳定性和效率的最终解决方案是异步梯度下降,通过多个代理并行执行,在环境的多个实例上进行,并采用不同的探索策略,这样每次梯度更新之间的相关性就更小:每个学习代理在同一台机器的不同线程中运行,与其他代理共享其模型和目标模型参数,但计算环境的不同部分的梯度。并行的行为体学习者具有稳定化效果,支持策略强化,减少训练时间,并在 GPU 或多核 CPU 上表现出可比的性能,这非常棒!
稳定化效果导致更好的数据效率:数据效率通过收敛到期望的训练损失或准确率所需的训练周期(一个周期是完整的训练数据集被算法展示一次)来衡量。总训练时间受数据效率、并行性(线程数或机器数)以及并行性开销(在给定核心数、机器数和算法分布效率的情况下,随着线程数增加呈亚线性增长)影响。
让我们实际看一下。为了实现多个代理探索环境的不同部分,我们将使用 Python 的多进程模块运行多个进程,其中一个进程用于更新模型(GPU),n个进程用于代理进行探索(CPU)。多进程模块的管理器对象控制一个持有 Q 网络权重的服务器进程,以便在进程之间共享。用于存储代理经验并在模型更新时一次性提供的通信通道,通过一个进程安全的队列实现:
from multiprocessing import *
manager = Manager()
weight_dict = manager.dict()
mem_queue = manager.Queue(args.queue_size)
pool = Pool(args.processes + 1, init_worker)
for i in range(args.processes):
pool.apply_async(generate_experience_proc, (mem_queue, weight_dict, i))
pool.apply_async(learn_proc, (mem_queue, weight_dict))
pool.close()
pool.join()
现在,让我们生成经验并将其排入公共队列对象中。
为了这个目的,在每个代理创建其游戏环境时,编译 Q 网络并从管理器加载权重:
env = gym.make(args.game)
load_net = build_networks(observation_shape, env.action_space.n)
load_net.compile(optimizer='rmsprop', loss='mse', loss_weights=[0.5, 1.])
while 'weights' not in weight_dict:
time.sleep(0.1)
load_net.set_weights(weight_dict['weights'])
为了生成一次经验,代理选择一个动作并在其环境中执行:
observation, reward, done, _ = env.step(action)
每个代理的经验都存储在一个列表中,直到游戏结束或列表长度超过n_step,以便使用n 步 Q 学习评估状态-动作值:
if done or counter >= args.n_step:
r = 0.
if not done:
r = value_net.predict(observations[None, ...])[0]
for i in range(counter):
r = n_step_rewards[i] + discount * r
mem_queue.put((n_step_observations[i], n_step_actions[i], r))
偶尔,代理会从学习进程中更新其权重:
load_net.set_weights(weight_dict['weights'])
现在,让我们看看如何更新学习代理中的权重。
带有 REINFORCE 算法的策略梯度
策略梯度(PG)/ REINFORCE 算法的想法非常简单:它在强化学习任务中,重新使用分类损失函数。
让我们记住,分类损失是由负对数似然给出的,使用梯度下降来最小化它时,遵循的是负对数似然相对于网络权重的导数:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00224.jpeg
这里,y 是选择的动作,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00225.jpeg 是给定输入 X 和权重 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00219.jpeg 的该动作的预测概率。
REINFORCE 定理引入了强化学习中的等价物,其中 r 是奖励。以下是导数:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00226.jpeg
表示网络权重相对于预期奖励的导数的无偏估计:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00227.jpeg
因此,遵循导数将鼓励代理最大化奖励。
这样的梯度下降使我们能够优化我们代理的策略网络:策略 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00228.jpeg 是一个合法动作的概率分布,用于在在线学习期间采样要执行的动作,并且可以通过参数化神经网络进行近似。
在连续案例中特别有用,例如在运动控制中,离散化的动作空间可能导致一些次优伪影,并且在无限动作空间下,无法对动作-值网络 Q 进行最大化。
此外,可以通过递归(LSTM,GRU)增强策略网络,使代理根据多个先前的状态选择其动作。
REINFORCE 定理为我们提供了一个梯度下降方法,用于优化参数化的策略网络。为了鼓励在这种基于策略的情况下进行探索,也可以向损失函数中添加正则化项——策略的熵。
在此策略下,可以计算每个状态的值 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00229.jpeg:
-
可以通过使用该策略从该状态进行游戏
-
或者,如果通过梯度下降将其参数化为状态价值网络,当前参数作为目标,就像在上一节中看到的带有折扣奖励的状态-动作价值网络一样:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00230.jpeg
这个值通常作为强化基准 b 来减少策略梯度估计的方差,Q 值可以作为期望奖励:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00231.jpeg
REINFORCE 导数中的第一个因子:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00232.jpeg
被称为动作 a 在状态 s 中的优势。
策略网络和价值网络的梯度下降可以通过我们的并行演员学习器异步执行。
让我们在 Keras 中创建我们的策略网络和状态价值网络,共享它们的第一层:
from keras.models import Model
from keras.layers import Input, Conv2D, Flatten, Dense
def build_networks(input_shape, output_shape):
state = Input(shape=input_shape)
h = Conv2D(16, (8, 8) , strides=(4, 4), activation='relu', data_format="channels_first")(state)
h = Conv2D(32, (4, 4) , strides=(2, 2), activation='relu', data_format="channels_first")(h)
h = Flatten()(h)
h = Dense(256, activation='relu')(h)
value = Dense(1, activation='linear', name='value')(h)
policy = Dense(output_shape, activation='softmax', name='policy')(h)
value_network = Model(inputs=state, outputs=value)
policy_network = Model(inputs=state, outputs=policy)
train_network = Model(inputs=state, outputs=[value, policy])
return value_network, policy_network, train_network
我们的学习过程还构建了模型,将权重共享给其他进程,并为训练编译它们,并计算各自的损失:
_, _, train_network = build_networks(observation_shape, env.action_space.n)
weight_dict['weights'] = train_net.get_weights()
from keras import backend as K
def policy_loss(advantage=0., beta=0.01):
def loss(y_true, y_pred):
return -K.sum(K.log(K.sum(y_true * y_pred, axis=-1) + \K.epsilon()) * K.flatten(advantage)) + \
beta * K.sum(y_pred * K.log(y_pred + K.epsilon()))
return loss
def value_loss():
def loss(y_true, y_pred):
return 0.5 * K.sum(K.square(y_true - y_pred))
return loss
train_net.compile(optimizer=RMSprop(epsilon=0.1, rho=0.99),
loss=[value_loss(), policy_loss(advantage, args.beta)])
策略损失是一个 REINFORCE 损失加上一个熵损失,以鼓励探索。值损失是一个简单的均方误差损失。
将经验队列化成一个批次,我们的学习过程会在这个批次上训练模型并更新权重字典:
loss = train_net.train_on_batch([last_obs, advantage], [rewards, targets])
运行完整代码:
pip install -r requirements.txt
python 1-train.py --game=Breakout-v0 --processes=16
python 2-play.py --game=Breakout-v0 --model=model-Breakout-v0-35750000.h5
学习大约花费了 24 小时。
基于策略的优势演员评论员通常优于基于值的方法。
相关文献
你可以参考以下文章:
-
连接主义强化学习的简单统计梯度跟踪算法,罗纳德·J·威廉姆斯,1992 年
-
带有函数逼近的强化学习的策略梯度方法,理查德·S·萨顿,戴维·麦卡勒斯特,萨廷德·辛格,伊沙·曼索尔,1999 年
-
通过深度强化学习玩 Atari 游戏,沃洛基米尔·姆尼赫,科雷·卡武克乔乌格,戴维·西尔弗,亚历克斯·格雷夫斯,伊欧尼斯·安东诺格鲁,丹·维尔斯特拉,马丁·里德米勒,2013 年
-
通过深度神经网络和树搜索掌握围棋游戏,戴维·西尔弗,阿贾·黄,克里斯·J·马迪森,阿瑟·格兹,洛朗·西弗,乔治·范登·德里斯切,朱利安·施里特维泽,伊欧尼斯·安东诺格鲁,维达·潘内谢尔瓦姆,马克·兰特托,桑德·迪尔曼,多米尼克·格雷韦,约翰·纳姆,纳尔·卡尔赫布雷纳,伊利亚·苏茨克弗,蒂莫西·利利克拉普,马德琳·利奇,科雷·卡武克乔乌格,托雷·格雷佩尔和德米斯·哈萨比斯,2016 年
-
深度强化学习的异步方法,沃洛基米尔·姆尼赫,阿德里亚·普伊格多梅内奇·巴迪亚,梅赫迪·米尔扎,亚历克斯·格雷夫斯,蒂姆·哈利,蒂莫西·P·利利克拉普,戴维·西尔弗,科雷·卡武克乔乌格,2016 年 2 月
-
使用 KeRLym 进行深度强化学习无线电控制和信号检测,Gym RL 代理蒂莫西·J·奥谢和 T·查尔斯·克兰西,2016 年
总结
强化学习描述了优化代理通过奖励偶然获得任务的过程。通过深度神经网络开发了在线、离线、基于值或基于策略的算法,应用于各种游戏和模拟环境。
策略梯度是一种强行求解方案,需要在训练过程中进行动作采样,适用于小的动作空间,尽管它们为连续搜索空间提供了初步的解决方案。
策略梯度也可以用于训练神经网络中的非可微随机层,并通过这些层进行反向传播梯度。例如,当通过一个模型的传播需要按照参数化子模型进行采样时,来自顶层的梯度可以被视为底层网络的奖励。
在更复杂的环境中,当没有明显的奖励时(例如,从环境中存在的物体推理和理解可能的动作),推理帮助人类优化他们的动作,目前的研究尚未提供任何解决方案。当前的强化学习算法特别适用于精确的操作、快速的反应,但没有长期规划和推理。此外,强化学习算法需要大量的数据集,而模拟环境能够轻松提供这些数据集。但这也引出了现实世界中扩展性的问题。
在下一章,我们将探讨最新的解决方案,用于生成与现实世界数据无法区分的新数据。
第十二章:使用无监督生成网络学习特征
本章重点介绍一种新型模型——生成模型,包括 限制玻尔兹曼机、深度信念网络、变分自编码器、自回归模型 和 生成对抗网络。对于前者,我们将介绍其理论,而后者则通过实践代码和建议详细解释。
这些网络不需要任何标签进行训练,这就是所谓的 无监督学习。无监督学习帮助从数据中计算特征,而不受标签的偏差影响。这些模型是生成式的,因为它们经过训练以生成听起来真实的新数据。
以下内容将会涵盖:
-
生成模型
-
无监督学习
-
限制玻尔兹曼机
-
深度信念网络
-
生成对抗模型
-
半监督学习
生成模型
神经处理中的生成模型是一个模型,给定一个噪声向量 z 作为输入,生成数据:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00233.jpeg
训练的目的是找到能够生成尽可能接近真实数据的数据的参数。
生成网络的应用包括数据维度减少、合成数据生成、无监督特征学习和预训练/训练效率。预训练有助于泛化,因为预训练侧重于数据中的模式,而不是数据与标签之间的关系。
限制玻尔兹曼机
限制玻尔兹曼机是最简单的生成网络,由一个完全连接的隐藏层组成,如图所示:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00234.jpeg
完全玻尔兹曼机还具有隐藏到隐藏和可见到可见的循环连接,而 限制 版本则没有任何这种连接。
在一般情况下,RBM 被定义为 基于能量的模型,这意味着它们通过能量函数定义了一个概率分布:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00235.jpeghttps://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00236.jpeg
Z 是 配分函数,E(v) 是 自由能 函数(不依赖于隐藏状态)。
注意
最小化负对数似然等价于最小化能量函数。
RBM 定义了一个作为模型参数线性函数的能量函数:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00237.jpeghttps://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00238.jpeg
能量与自由能之间的关系由以下公式给出:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00239.jpeg
在 RBM 的情况下:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00240.jpeg
这里 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00241.jpeg 表示对第 i 个隐藏神经元的可能值进行求和。
RBM 通常考虑在特定情况下,其中 v
和 h
是 {0,1} 中的二项值,因此:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00242.jpeg
该模型是对称的,遵循模型中的对称性:隐藏层和可见层在能量函数中占据相同的位置:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00243.jpeg
RBM 在两个方向上都作为一个简单的随机完全连接层工作(从输入到隐藏,从隐藏到输入)。
RBM 的负对数似然的梯度或导数有两个项,分别定义为正相位和负相位,其中第一项增加数据的概率,第二项减少生成样本的概率:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00244.jpeg
在这里,求和是对所有可能的输入 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00245.jpeg 按其概率(期望)加权。在最小值处,任何自由能的增加都会减少总数据的期望。
实际上,负相位中的这种求和可以转化为对V观察到的*(v,h)*的求和:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00246.jpeg
为了在实践中计算这样的求和,观察到的样本*(v,h)*的概率必须满足 p(v | h),由上式给出,同时满足 p(h | v)。
采样是通过对比散度算法进行的,实践中:v 从数据集中采样,而 h 则根据上述分布在给定 v 的条件下绘制。这个操作会重复进行,以产生给定 h 的新 v,然后是给定 v 的新 h。在实践中,这足以生成与真实分布非常接近的样本。这些观察到的 v 和 h 样本被称为负粒子,成本函数中的第二项减少这些生成样本的概率,而第一项则增加数据的概率。
这里是计算带有负粒子的配分函数的结果:
W = shared_glorot_uniform((n_visible, n_hidden), name='W')
hbias = shared_zeros(n_hidden, name='hbias')
vbias = shared_zeros(n_visible, name='vbias')
params = [W, hbias, vbias]
def sample_h_given_v(v0_sample):
pre_sigmoid_h1 = T.dot(v0_sample, W) + hbias
h1_mean = T.nnet.sigmoid(pre_sigmoid_h1)
h1_sample = theano_rng.binomial(size=h1_mean.shape, n=1, p=h1_mean, dtype=theano.config.floatX)
return [pre_sigmoid_h1, h1_mean, h1_sample]
def sample_v_given_h(h0_sample):
pre_sigmoid_v1 = T.dot(h0_sample, W.T) + vbias
v1_mean = T.nnet.sigmoid(pre_sigmoid_v1)
v1_sample = theano_rng.binomial(size=v1_mean.shape, n=1, p=v1_mean, dtype=theano.config.floatX)
return [pre_sigmoid_v1, v1_mean, v1_sample]
def gibbs_hvh(h0_sample):
pre_sigmoid_v1, v1_mean, v1_sample = sample_v_given_h(h0_sample)
pre_sigmoid_h1, h1_mean, h1_sample = sample_h_given_v(v1_sample)
return [pre_sigmoid_v1, v1_mean, v1_sample,
pre_sigmoid_h1, h1_mean, h1_sample]
chain_start = persistent_chain
(
[
pre_sigmoid_nvs,
nv_means,
nv_samples,
pre_sigmoid_nhs,
nh_means,
nh_samples
],
updates
) = theano.scan(
gibbs_hvh,
outputs_info=[None, None, None, None, None, chain_start],
n_steps=k,
name="gibbs_hvh"
)
chain_end = nv_samples[-1]
def free_energy(v_sample):
wx_b = T.dot(v_sample, W) + hbias
vbias_term = T.dot(v_sample, vbias)
hidden_term = T.sum(T.log(1 + T.exp(wx_b)), axis=1)
return -hidden_term - vbias_term
cost = T.mean(free_energy(x)) - T.mean(free_energy(chain_end))
在 MNIST 数据集上训练 15 轮后的过滤器图像:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00247.jpeg
还有一小批负粒子(每行之间有 1,000 步的采样):
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00248.jpeg
深度信念网络
深度信念网络(DBN)是多个 RBM 堆叠在一起,旨在增强它们的表示能力,更好地捕捉数据中的模式。
训练过程是逐层进行的,首先假设只有一个 RBM,带有隐藏状态 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00249.jpeg。一旦 RBM 的权重被训练好,这些权重将保持固定,第一个 RBM 的隐藏层 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00249.jpeg 被视为第二个 RBM 的可见层,第二个 RBM 有一个隐藏状态 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00250.jpeg。每个新的 RBM 将捕捉到之前的 RBM 没有捕捉到的模式,如下图所示:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00251.jpeg
可以证明,在堆叠中每增加一个新的 RBM,会减少负对数似然值。
最后一步是可以将这些权重应用到分类网络中,通过在最终的隐藏状态上简单地添加一个线性层和一个 Softmax 层,然后像往常一样通过梯度下降训练微调所有权重。
对数据维度的应用保持不变,将所有层展开以产生解码器网络,权重等于编码器网络中的权重转置(初始多层 RBM):
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00252.jpeg
这种展开的网络被称为自编码器。
实际上,如果没有贪婪的逐层训练,直接通过梯度下降进行训练需要找到合适的初始化,这可能会更加棘手,因为权重初始化必须足够接近解决方案。这就是为什么常用的自编码器方法是分别训练每个 RBM。
生成对抗网络
由于之前模型中的分区函数不可解且需要使用吉布斯采样的对比散度算法,博弈论最近为学习生成模型提供了一类新方法,即生成对抗网络(GANs),并且这种方法今天取得了巨大成功。
生成对抗网络由两个模型组成,这两个模型交替训练以相互竞争。生成器网络G的优化目标是通过生成难以被判别器D与真实数据区分的数据,来重现真实数据的分布。与此同时,第二个网络 D 的优化目标是区分真实数据和由 G 生成的合成数据。总体而言,训练过程类似于一个双人博弈的最小-最大游戏,目标函数如下:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00253.jpeg
在这里,x是真实数据,来自真实数据分布,z是生成模型的噪声向量。从某种意义上来说,判别器和生成器可以看作是警察和小偷:为了确保训练正确进行,警察的训练次数是小偷的两倍。
让我们通过图像作为数据的案例来说明 GANs。特别地,仍然采用第二章的例子,使用前馈网络分类手写数字,关于 MNIST 数字,考虑训练一个生成对抗网络,根据我们想要的数字生成图像。
GAN 方法包括使用第二个模型——判别网络,来训练生成模型,判别输入数据是否为真实或伪造。在这种情况下,我们可以简单地重用我们的 MNIST 图像分类模型作为判别器,进行 real
或 fake
的预测输出,并且将其条件化为应生成的数字标签。为了将网络条件化为标签,数字标签与输入数据进行拼接:
def conv_cond_concat(x, y):
return T.concatenate([x, y*T.ones((x.shape[0], y.shape[1], x.shape[2], x.shape[3]))], axis=1)
def discrim(X, Y, w, w2, w3, wy):
yb = Y.dimshuffle(0, 1, 'x', 'x')
X = conv_cond_concat(X, yb)
h = T.nnet.relu(dnn_conv(X, w, subsample=(2, 2), border_mode=(2, 2)), alpha=0.2 )
h = conv_cond_concat(h, yb)
h2 = T.nnet.relu(batchnorm(dnn_conv(h, w2, subsample=(2, 2), border_mode=(2, 2))), alpha=0.2)
h2 = T.flatten(h2, 2)
h2 = T.concatenate([h2, Y], axis=1)
h3 = T.nnet.relu(batchnorm(T.dot(h2, w3)))
h3 = T.concatenate([h3, Y], axis=1)
y = T.nnet.sigmoid(T.dot(h3, wy))
return y
提示
注意使用了两个泄漏修正线性单元(leaky ReLU),泄漏系数为 0.2,作为前两个卷积的激活函数。
为了根据噪声和标签生成图像,生成器网络由一系列反卷积组成,使用一个包含 100 个从 0 到 1 之间的实数的输入噪声向量 z:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00254.jpeg
在 Theano 中创建反卷积时,创建一个虚拟的卷积前向传播,并将其梯度作为反卷积的使用。
def deconv(X, w, subsample=(1, 1), border_mode=(0, 0), conv_mode='conv'):
img = gpu_contiguous(T.cast(X, 'float32'))
kerns = gpu_contiguous(T.cast(w, 'float32'))
desc = GpuDnnConvDesc(border_mode=border_mode, subsample=subsample,
conv_mode=conv_mode)(gpu_alloc_empty(img.shape[0], kerns.shape[1], img.shape[2]*subsample[0], img.shape[3]*subsample[1]).shape, kerns.shape)
out = gpu_alloc_empty(img.shape[0], kerns.shape[1], img.shape[2]*subsample[0], img.shape[3]*subsample[1])
d_img = GpuDnnConvGradI()(kerns, img, out, desc)
return d_img
def gen(Z, Y, w, w2, w3, wx):
yb = Y.dimshuffle(0, 1, 'x', 'x')
Z = T.concatenate([Z, Y], axis=1)
h = T.nnet.relu(batchnorm(T.dot(Z, w)))
h = T.concatenate([h, Y], axis=1)
h2 = T.nnet.relu(batchnorm(T.dot(h, w2)))
h2 = h2.reshape((h2.shape[0], ngf*2, 7, 7))
h2 = conv_cond_concat(h2, yb)
h3 = T.nnet.relu(batchnorm(deconv(h2, w3, subsample=(2, 2), border_mode=(2, 2))))
h3 = conv_cond_concat(h3, yb)
x = T.nnet.sigmoid(deconv(h3, wx, subsample=(2, 2), border_mode=(2, 2)))
return x
真实数据由元组 (X,Y) 给出,而生成的数据则由噪声和标签 (Z,Y) 构建:
X = T.tensor4()
Z = T.matrix()
Y = T.matrix()
gX = gen(Z, Y, *gen_params)
p_real = discrim(X, Y, *discrim_params)
p_gen = discrim(gX, Y, *discrim_params)
生成器和判别器模型在对抗学习中竞争:
-
判别器被训练为将真实数据标记为真实(
1
),并将生成数据标记为生成(0
),从而最小化以下成本函数:d_cost = T.nnet.binary_crossentropy(p_real, T.ones(p_real.shape)).mean() \ + T.nnet.binary_crossentropy(p_gen, T.zeros(p_gen.shape)).mean()
-
生成器被训练成尽可能欺骗判别器。生成器的训练信号由判别器网络(p_gen)提供给生成器:
g_cost = T.nnet.binary_crossentropy(p_gen,T.ones(p_gen.shape)).mean()
和通常一样,计算每个模型的参数成本,并交替优化每个模型的权重,判别器的训练次数是生成器的两倍。在 GANs 的情况下,判别器和生成器之间的竞争不会导致每个损失的减少。
从第一轮开始:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00255.jpeg
到第 45 轮:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00256.jpeg
生成的示例看起来更接近真实数据:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00257.jpeg
改进 GANs
GANs 是近期出现的、非常有前景的技术,但目前仍在进行深入研究。然而,仍然有方法可以改进之前的结果。
首先,和 RBM 及其他网络一样,GANs 可以通过堆叠来增加它们的生成能力。例如,StackGan 模型提出使用两个堆叠的 GANs 进行高质量图像生成:第一个 GAN 生成粗糙的低分辨率图像,而第二个 GAN 将这个生成的样本作为输入,生成更高定义和更具真实感的图像,其中物体的细节更加精确。
GAN 的一个主要问题是模型崩溃,这使得它们很难训练。模型崩溃发生在生成器开始忽视输入噪声并学习仅生成一个样本时,这个样本总是相同的。生成中的多样性崩溃了。解决这个问题的一种非常有效的方法来自 S-GAN 模型,它通过向生成器中添加一个第三个网络来进行训练。这个网络的目的是根据输入预测噪声:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00258.jpeg
为了与生成器一起优化这个第三个网络,会向生成器损失中添加一个熵损失,以鼓励生成的图像 x 足够依赖噪声 z。换句话说,条件熵 H(x | z) 必须尽可能低:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00259.jpeg
这个第三个网络预测一个辅助分布 Q,用来逼近真实后验 P(z | x),并且可以证明它是 H(x | z) 的变分上界。这样的损失函数有助于生成器不忽视输入噪声。
半监督学习
最后但同样重要的是,这种生成对抗网络可以用来增强监督学习本身。
假设目标是分类 K 类,并且有一定数量的标记数据。可以将一些来自生成模型的生成样本添加到数据集中,并将它们视为属于 (K+1)th 类,即伪数据类。
将新分类器在两个数据集(真实数据和伪数据)之间的训练交叉熵损失分解为以下公式:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00260.jpeg
这里,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00261.jpeg 是模型预测的概率:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00262.jpeg
请注意,如果我们知道数据是真实的:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00263.jpeg
而在真实数据(K 类)上的训练会导致以下损失:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00264.jpeg
因此,全球分类器的损失可以重写为:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00265.jpeg
损失的第二项对应于 GAN 的标准无监督损失:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00266.jpeg
监督损失和无监督损失之间引入的交互作用仍然不完全理解,但当分类问题不简单时,无监督损失是有帮助的。
进一步阅读
您可以参考以下主题以获取更多见解:
-
Deeplearning.net RBM 教程:
deeplearning.net/tutorial/rbm.html
-
Deeplearning.net 深度信念网络教程:
deeplearning.net/tutorial/DBN.html
-
Deeplearning.net 使用 RBM-RNN 生成的教程:
deeplearning.net/tutorial/rnnrbm.html
-
建模高维序列中的时间依赖性:应用于多声部音乐生成与转录,Nicolas Boulanger-Lewandowski,Yoshua Bengio,Pascal Vincent,2012
-
生成对抗网络,Ian J. Goodfellow,Jean Pouget-Abadie,Mehdi Mirza,Bing Xu,David Warde-Farley,Sherjil Ozair,Aaron Courville,Yoshua Bengio,2014
-
生成对抗网络将 改变世界,Nikolai Yakovenko,2016
medium.com/@Moscow25/
-
像素递归神经网络,Aaron van den Oord,Nal Kalchbrenner,Koray Kavukcuoglu,2016
-
InfoGAN:通过信息最大化生成对抗网络进行可解释的表示学习,Xi Chen,Yan Duan,Rein Houthooft,John Schulman,Ilya Sutskever,Pieter Abbeel,2016
-
StackGAN:使用堆叠生成对抗网络将文本转换为逼真的图像合成,Han Zhang,Tao Xu,Hongsheng Li,Shaoting Zhang,Xiaolei Huang,Xiaogang Wang,Dimitris Metaxas,2016
-
堆叠生成对抗网络,Xun Huang,Yixuan Li,Omid Poursaeed,John Hopcroft,Serge Belongie,2016
-
神经对话生成的对抗学习,Jiwei Li,Will Monroe,Tianlin Shi,Sébastien Jean,Alan Ritter,Dan Jurafsky,2017
-
改进的 GAN 训练技术,Tim Salimans,Ian Goodfellow,Wojciech Zaremba,Vicki Cheung,Alec Radford,Xi Chen,2016
-
无监督表示学习与深度卷积生成对抗网络,Alec Radford,Luke Metz,Soumith Chintala,2015
摘要
生成对抗网络如今是一个非常活跃的研究领域。它们属于生成模型家族,包括 RBM 和深度置信网络。
生成模型旨在生成更多数据,或以无监督的方式学习更好的特征,用于监督学习和其他任务。
生成模型可以根据一些环境输入进行条件化,并尝试找到真实数据背后的隐藏变量。
这些模型是最先进的,完成了与 Theano 的深度学习网络概述。下一章将介绍一些高级概念,以扩展 Theano 并探讨深度学习的未来。
第十三章:使用 Theano 扩展深度学习
本章为进一步深入 Theano 和深度学习提供了线索。首先,它介绍了如何在 Python 或 C 语言中为 Theano 计算图创建新的操作符,无论是针对 CPU 还是 GPU。然后,研究了与其他深度学习框架的交互,借助代码库和库的支持,实现与其他技术的双向转换。
最后,为了完善 Theano 深度学习领域所提供的可能性,我们发展了一个新的通用人工智能领域的概念。
本章涵盖的主题如下:
-
为 Theano 计算图编写新操作符
-
针对 CPU 和 GPU 的 Python 代码
-
针对 CPU 和 GPU 的 C 语言 API
-
与其他深度学习框架共享模型
-
云 GPU
-
元学习、渐进学习和引导学习
-
一般人工智能
本章全面概述了使用 Theano 进行深度学习的内容。
Python 中的 Theano 操作(针对 CPU)
作为一个数学编译引擎,Theano 的目的是以最优的方式为目标平台编译计算图。
新操作符的开发可以在 Python 或 C 语言中进行编写,进行 CPU 或 GPU 的编译。
首先,我们解决最简单的情况,在 CPU 的 Python 中,它将使你能够非常容易和快速地添加新操作。
为了固定概念,让我们实现一个简单的仿射操作符,执行仿射变换 a * x + b,其中 x 为输入。
操作符是通过从通用的theano.Op
类派生的类来定义的:
import theano, numpy
class AXPBOp(theano.Op):
"""
This creates an Op that takes x to a*x+b.
"""
__props__ = ("a", "b")
def __init__(self, a, b):
self.a = a
self.b = b
super(AXPBOp, self).__init__()
def make_node(self, x):
x = theano.tensor.as_tensor_variable(x)
return theano.Apply(self, [x], [x.type()])
def perform(self, node, inputs, output_storage):
x = inputs[0]
z = output_storage[0]
z[0] = self.a * x + self.b
def infer_shape(self, node, i0_shapes):
return i0_shapes
def grad(self, inputs, output_grads):
return [self.a * output_grads[0]]
mult4plus5op = AXPBOp(4,5)
x = theano.tensor.matrix()
y = mult4plus5op(x)
f = theano.function([x], y)
res = f(numpy.random.rand(3,2))
让我们理解这个例子。
__props__
属性被设置为操作符依赖的两个参数名,a
和b
。它将自动为我们生成__eq__()
、__hash__()
和__str_()
方法,因此,如果我们创建两个具有相同a
和b
参数值的不同对象,Theano 会将它们视为相等的操作符:
>>> mult4plus5op2 = AXPBOp(4,5)
>>> mult4plus5op == mult4plus5op2
True
>>> hash(mult4plus5op)
-292944955210390262
>>> hash(mult4plus5op2)
-292944955210390262
此外,打印操作时,参数a
和b
将会出现:
>>> theano.printing.pprint(y)
AXPBOp{a=4, b=5}.0
>>> theano.printing.pydotprint(y)
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00267.jpeg
如果未指定__props__
,则需要手动定义__eq__()
、__hash__()
和__str_()
方法。
make_node()
方法创建将被包含在计算图中的节点,并在将mult4plus5op
对象应用于输入x
时运行。节点创建通过theano.Apply()
方法执行,该方法的参数是输入变量和输出类型。为了强制输入为变量,调用as_tensor_variable()
方法,将任何 NumPy 数组转换为变量。这是我们定义输出类型的地方,给定输入时还需检查输入是否与操作符兼容,否则会引发 TypeError。
请注意,正如我们之前为 __eq__()
方法使用 __props__
属性那样,自动生成 make_node()
方法也是可能的,但在这种情况下,itypes
和 otypes
属性用于定义输入和输出的类型:
itypes = [theano.tensor.dmatrix]
otypes = [theano.tensor.dmatrix]
perform()
方法定义了在 Python 中执行的该操作符的计算。由于可以实现多个输入并返回多个输出的操作符,因此输入和输出以列表形式给出。第二个输出将存储在 output_storage[1][0]
中。输出可能已经由前面的值分配,以便重用内存。它们将始终是合适的 dtype
对象,但不一定是正确的形状和步幅。它们不符合正确形状时,最好重新分配。
最后的两个方法,infer_shape()
和 grad()
,是可选的。第一个方法用于输出无需计算时,仅需形状信息来进行计算的情况——这种情况通常发生在 Theano 优化过程中。第二个方法用于在 grad()
方法下对输出进行求导:
>>> dy=theano.tensor.grad(y.sum(), x)
>>> theano.printing.pprint(dy)
'(TensorConstant{4} * fill(AXPBOp{a=4, b=5}(<TensorType(float32, matrix)>), fill(Sum{acc_dtype=float64}(AXPBOp{a=4, b=5}(<TensorType(float32, matrix)>)), TensorConstant{1.0})))'
>>> df = theano.function([x], dy)
>>> theano.printing.debugprint(df)
Alloc [id A] '' 2
|TensorConstant{(1, 1) of 4.0} [id B]
|Shape_i{0} [id C] '' 1
| |<TensorType(float32, matrix)> [id D]
|Shape_i{1} [id E] '' 0
|<TensorType(float32, matrix)> [id D]
以相同的方式,可以定义操作符的 R-操作符函数。
用于 GPU 的 Theano 操作符(Op)
让我们看看在 GPU config
模式下运行该操作符时会发生什么:
>>> y = mult4plus5op(2 * x) + 4 * x
>>> f = theano.function([x], y)
>>> theano.printing.debugprint(f)
HostFromGpu(gpuarray) [id A] '' 6
|GpuElemwise{Composite{(i0 + (i1 * i2))}}[(0, 0)]<gpuarray> [id B] '' 5
|GpuFromHost<None> [id C] '' 4
| |AXPBOp{a=4, b=5} [id D] '' 3
| |HostFromGpu(gpuarray) [id E] '' 2
| |GpuElemwise{mul,no_inplace} [id F] '' 1
| |GpuArrayConstant{[[ 2.]]} [id G]
| |GpuFromHost<None> [id H] '' 0
| |<TensorType(float32, matrix)> [id I]
|GpuArrayConstant{[[ 4.]]} [id J]
|GpuFromHost<None> [id H] '' 0
由于我们仅在 Python 中定义了新操作符的 CPU 实现,而完整的图计算是在 GPU 上运行的,因此数据会在图的中间来回传输到 CPU,以应用我们新的 CPU 操作符:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00268.jpeg
为了避免图中传输的不效率问题,让我们为 GPU 创建相同的 Python 操作符。
为此,您只需修改操作符的 make_node()
和 perform()
方法,如下所示:
from theano.gpuarray.type import get_context
def make_node(self, x):
x = as_gpuarray_variable(x, self.context_name)
x_arg = pygpu.elemwise.arg('x', 'float32', read=True)
c_arg = pygpu.elemwise.arg('c', 'float32', read=True, write=True)
self.my_op = pygpu.elemwise.GpuElemwise(get_context(self.context_name), "c = " + str(self.a) + " * x + " + str(self.b), [x_arg, c_arg], convert_f16=True)
return Apply(self, [x], [x.type()])
def perform(self, node, inputs, output_storage):
x = inputs[0]
z = output_storage[0]
z[0] = pygpu.empty(x.shape, dtype=x.dtype, context=get_context(self.context_name))
self.my_op( x, z[0])
没有太多变化。
在 make_node()
方法中,as_tensor_variable()
被 as_gpuarray_variable()
替换,后者需要上下文,这也是 GPU 变量类型定义的一部分。get_context()
方法将我们为设备选择的上下文名称转换为 pygpu
库中的 GPUContext
。
在 perform()
方法中,计算在 GPU 上执行,得益于 pygpu
库,它包含了 GPU 上的逐元素操作符以及 基本线性代数子程序 (BLAS) 方法,如 通用矩阵乘矩阵运算 (GEMM) 和 通用矩阵向量乘法 (GEMV) 操作。
现在,让我们看一下当这个新操作符在 GPU 上的更大图中时,编译后的图是什么样的:
HostFromGpu(gpuarray) [id A] '' 4
|GpuElemwise{Add}[(0, 1)]<gpuarray> [id B] '' 3
|GpuArrayConstant{[[ 4.]]} [id C]
|GpuAXPBOp{a=4, b=5, context_name='dev0'} [id D] '' 2
|GpuElemwise{Mul}[(0, 1)]<gpuarray> [id E] '' 1
|GpuArrayConstant{[[ 2.]]} [id F]
|GpuFromHost<dev0> [id G] '' 0
|<TensorType(float32, matrix)> [id H]
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00269.jpeg
为了提高可读性,我们将操作符类的名称前缀加上了 Gpu,例如,GpuAXPBOp。
用于 CPU 的 Theano 操作符(Op)
另一个低效之处在于,操作符的 Python 实现每次执行计算时都会增加显著的开销,即在图中每次操作符实例化时都会发生这种情况。与图的其他部分不同,Python 代码不会像 Theano 将其他图部分用 C 编译一样进行编译,而是当 C 实现被包装到 Python 中并交换数据时,会发生这种开销。
为了弥补这一点,可以直接编写一些 C 代码,将其并入图的其他代码中并一起编译。
当直接用 C 实现操作符时,NumPy 是管理数组的底层库,NumPy-API 扩展了 Python C-API。定义新 C 操作符的 Python 类不必实现perform()
方法;相反,它返回要并入c_code()
、c_support_code()
和c_support_code_apply()
方法的 C 代码:
def c_code_cache_version(self):
return (6, 0)
def c_support_code(self):
c_support_code = """
bool same_shape(PyArrayObject* arr1, PyArrayObject* arr2)
{
if( PyArray_NDIM(arr1) != PyArray_NDIM(arr2)) {
return false;
}
for(int i = 0; i < PyArray_NDIM(arr2) ; i++) {
if (PyArray_DIMS(arr1)[0] == PyArray_DIMS(arr2)[0]) {
return false;
}
}
return true;
}
"""
return c_support_code
def c_support_code_apply(self, node, name):
dtype_x = node.inputs[0].dtype
dtype_z = node.outputs[0].dtype
a = self.a
b = self.b
c_support_code = """
void elemwise_op_%(name)s(npy_%(dtype_x)s* x_ptr, npy_intp* x_str, int itemsize_x,
npy_%(dtype_z)s* z_ptr, npy_intp* z_str, int itemsize_z,
int nbDims, npy_intp* dims)
{
npy_intp stride_x = (npy_intp)(1);
npy_intp stride_z = (npy_intp)(1);
for (int i = 0; i < nbDims; i ++) {
stride_x = stride_x * x_str[i] / itemsize_x;
stride_z = stride_z * z_str[i] / itemsize_z;
}
for (int i=0; i < dims[0]; i++)
if (nbDims==1) {
z_ptr[i * z_str[0]/itemsize_z] = x_ptr[i * x_str[0] / itemsize_x] * ((npy_%(dtype_z)s) %(a)s) + ((npy_%(dtype_z)s)%(b)s);
} else {
elemwise_op_%(name)s( x_ptr + i * stride_x , x_str + 1, itemsize_x,
z_ptr + i * stride_z , z_str + 1, itemsize_z,
nbDims - 1, dims + 1 );
}
}
"""
return c_support_code % locals()
def c_code(self, node, name, inp, out, sub):
x = inp[0]
z = out[0]
dtype_x = node.inputs[0].dtype
dtype_z = node.outputs[0].dtype
itemsize_x = numpy.dtype(dtype_x).itemsize
itemsize_z = numpy.dtype(dtype_z).itemsize
typenum_z = numpy.dtype(dtype_z).num
fail = sub['fail']
c_code = """
// Validate that the output storage exists and has the same
// dimension as x.
if (NULL == %(z)s || !(same_shape(%(x)s, %(z)s)))
{
/* Reference received to invalid output variable.
Decrease received reference's ref count and allocate new
output variable */
Py_XDECREF(%(z)s);
%(z)s = (PyArrayObject*)PyArray_EMPTY(PyArray_NDIM(%(x)s),
PyArray_DIMS(%(x)s),
%(typenum_z)s,
0);
if (!%(z)s) {
%(fail)s;
}
}
// Perform the elemwise operation
((npy_%(dtype_z)s *)PyArray_DATA(%(z)s))[0] = 0;
elemwise_op_%(name)s((npy_%(dtype_x)s*)PyArray_DATA(%(x)s), PyArray_STRIDES(%(x)s), %(itemsize_x)s,
(npy_%(dtype_z)s*)PyArray_DATA(%(z)s), PyArray_STRIDES(%(z)s), %(itemsize_z)s,
PyArray_NDIM(%(x)s), PyArray_DIMS(%(x)s) );
"""
return c_code % locals()
现在我们来讨论不同的部分:
当实现c_code_cache_version()
时,Theano 会缓存已编译的代码,以便下次将操作符纳入图中时节省一些编译时间,但每当我们修改 C 操作符的代码时,版本号必须递增。
放置在c_support_code()
和c_support_code_apply()
方法中的代码包含在 C 程序的全局作用域中。放置在c_support_code_apply()
和c_code()
方法中的代码必须是特定于图中每个操作符应用的;特别是,在这种情况下,它们取决于输入的类型。由于c_support_code_apply()
代码包含在全局作用域中,方法的名称与操作符名称相同。
PyArray_NDIM
、PyArray_DIMS
、PyArray_STRIDES
和PyArray_DATA
是宏,用于访问每个 NumPy 数组(C 中的PyArrayObject
)的维数、维度、步长和数据。PyArray_EMPTY
是 C 中等同于 Python numpy.empty()
方法的函数。
NumPy 的PyArrayObject
类继承自 Python C-API 中的PyObject
类。Py_XDECREF
宏允许我们在为新的输出数组分配内存之前减少输出的引用计数。像 Python C-API 一样,NumPy C-API 要求正确计数对象的引用。Theano 不能保证输出数组已被分配,也不能保证它是否已经按正确的形状分配。这就是为什么在c_code()
开始时会进行测试。
注意数组可以是有步长的,因为它们可以是数组(张量)的视图(或子张量)。也可以实现创建视图或修改输入的操作。
还有一些其他可能的方法可以进一步优化 C 实现:c_libraries()
和c_lib_dirs()
用于使用外部库,c_code_cleanup()
用于销毁内存分配,c_init_code()
用于在初始化时执行某些代码。
最后,也可以在代码中引用一些 C 文件,以减少 Python 类的负担。我们不详细说明这三种特性。
Theano 在 C 中的 GPU 操作
正如你想象的那样,可以结合两种优化:
-
通过直接使用 C 编程减少 Python/C 开销
-
编写 GPU 代码
为 GPU 编写 CUDA 代码时,必须将要在 GPU 上多个核心并行运行的代码包装成一种特殊的函数类型,称为 内核。
为此,__init__()
、make_node()
和 c_code_cache_version()
方法与我们在 GPU 上的 Python 示例相同,但新增了 gpu_kernels()
方法,用于定义新的 GPU 内核,以及 c_code()
方法(它再次替代了 perform()
方法),用于实现 C 代码,也称为 主机代码,该代码负责协调何时以及如何调用 GPU 上的不同内核:
def gpu_kernels(self, node, name):
code = """
KERNEL void axpb(GLOBAL_MEM %(ctype)s *x, GLOBAL_MEM %(ctype)s *z, ga_size n, ga_size m) {
for (ga_size i = LID_0; i < n; i += LDIM_0) {
for (ga_size j = LID_0; j < m; j += LDIM_0) {
z[i*m + j] = %(write_a)s( 2 * x[i*m + j] );
}
}
}""" % dict(ctype=pygpu.gpuarray.dtype_to_ctype(self.dtype),
name=name, write_a=write_w(self.dtype))
return [Kernel(
code=code, name="axpb",
params=[gpuarray.GpuArray, gpuarray.GpuArray, gpuarray.SIZE, gpuarray.SIZE],
flags=Kernel.get_flags(self.dtype),
objvar='k_axpb_' + name)]
def c_code(self, node, name, inp, out, sub):
n, = inp
z, = out
dtype_n = node.inputs[0].dtype
fail = sub['fail']
ctx = sub['params']
typecode = pygpu.gpuarray.dtype_to_typecode(self.dtype)
sync = bool(config.gpuarray.sync)
kname = self.gpu_kernels(node, name)[0].objvar
s = """
size_t dims[2] = {0, 0};
size_t ls, gs;
int err;
dims[0] = %(n)s->ga.dimensions[0];
dims[1] = %(n)s->ga.dimensions[1];
Py_CLEAR(%(z)s);
%(z)s = pygpu_zeros(2, dims,
%(typecode)s,
GA_C_ORDER,
%(ctx)s, Py_None);
if (%(z)s == NULL) {
%(fail)s
}
ls = 1;
gs = 256;
err = axpb_call(1, &gs, &ls, 0, %(n)s->ga.data, %(z)s->ga.data, dims[0], dims[1]);
if (err != GA_NO_ERROR) {
PyErr_Format(PyExc_RuntimeError,
"gpuarray error: kEye: %%s. n%%lu, m=%%lu.",
GpuKernel_error(&%(kname)s, err),
(unsigned long)dims[0], (unsigned long)dims[1]);
%(fail)s;
}
if(%(sync)d)
GpuArray_sync(&%(z)s->ga);
""" % locals()
return s
A new GPU computation kernel is defined under the name axpb, and it is a simple C code with special GPU types and two macros: KERNEL to designate the kernel function (hiding the CUDA __global__ declaration for kernels) and GLOBAL_MEM for the variables defined globally, available both on the CPU and the GPU (in opposition to variables inside the kernel function that, by default, are local to the thread executed on a GPU core).
请注意,我只实现了矩阵(即二维)输入的操作符,256 个线程将并行执行相同的操作,而这些操作本可以被分成不同的组,并分配给不同的线程。
在 CPU 上运行的主机代码管理 CPU 和 GPU 上的内存,并启动在 GPU 设备上执行的内核函数。
新 GPU 数组的分配是通过 pygpu_zeros()
方法执行的,使用 CUDA 时,它会在后台调用 cudamalloc()
方法,直接在 GPU 内存中分配数组。操作符实例无需管理分配给输出的内存释放以及 GPU 和 CPU 之间的数据传输,因为这是 Theano 优化的职责,决定何时插入传输操作符 HostFromGpu
和 GpuFromHost
。
在 C 代码中调用内核是通过 axpb_call()
来执行的,即内核的名称后面跟着 _call()
。注意,调用中有四个参数,比定义内核方法时的参数多。这四个参数定义了 libgpuarray
如何在核心上执行或部署内核。
为了理解 GPU 的并行编程执行配置,首先让我们明确一些关于 GPU 的基本概念。CUDA GPU 由 流式多处理器(SM)组成,其规格由计算能力、波大小、网格大小、块大小、每个 SM 和每个块的最大线程数、共享和本地内存大小以及最大寄存器数等参数来定义:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00270.jpeg
(来源:en.wikipedia.org/wiki/CUDA
)
在执行过程中,多处理器以 单指令多数据 (SIMD) 方式执行一组 32 个线程的指令(如前表所示)。在进行并行执行编程时,你需要将线程组织成尽可能接近底层架构的块。例如,对于矩阵的逐元素操作,比如我们的 AXPBOp,可以认为每个线程将对矩阵中的一个元素执行操作。所以,对一个 224 x 224 的图像进行计算将需要 50,176 个线程。假设 GPU 有 8 个多处理器,每个多处理器有 1024 个核心。在执行配置中,你可以定义一个 256 线程的块大小,并且执行完整计算所需的块数将是 196 个。为了简化并行程序的开发,块可以组织成一个多维网格(对于 CC 大于 2.0 的情况,最多支持 3 维,如前表所示),在图像输入的情况下,使用 14 x 14 块的二维网格是自然的。你可以自行组织线程到网格上的块中,但最佳的线程组织方式是遵循底层数据的维度,因为这样更容易将数据划分并分配给不同的线程。
每个线程执行时都会提供值,用于访问它在网格中的位置,这些值可以在代码中使用:
-
gridDim.x
、gridDim.y
、gridDim.z
线程块网格的维度 -
blockIdx.x
、blockIdx.y
、blockIdx.z
块在网格中的坐标 -
blockDim.x
、blockDim.y
、blockDim.z
块的维度 -
threadIdx.x
、threadIdx.y
、threadIdx.z
线程在块中的坐标
对于我们的逐元素 AXPBOp,每个线程处理一个元素,线程可以根据以下行索引获取数据元素:
int i = blockIdx.x*blockDim.x + threadIdx.x;
部署时,内核调用中的前四个新参数对应于:
-
网格/块的维度,在本例中是 2,表示输入为图像/矩阵
-
启动网格的大小,在本例中是 {14, 14}。一旦每块线程的数量被定义(在本例中为 256),每个网格的块数就由问题的大小来决定(这里是矩阵的大小)。
-
启动块的大小,在本例中是 {16, 16},用于每块 256 个线程,通常设置为 128 或 256。最好选择一个 warp 大小的倍数,因为执行是按 warp 进行的;如果你设置为 250,那么 201 个块将表现不佳:每个块的一个 warp 将无法充分发挥并行潜力。可以尝试不同的 32 的倍数,选择最有效的执行结果。
-
分配动态共享内存的数量,这是在定义共享内存(使用
LOCAL_MEM
宏)时需要的,尤其是在共享内存的大小在编译时未知的情况下。共享内存指定的是在同一线程块内共享的内存。在计算能力 2.x 和 3.x 的设备上,每个多处理器有 64KB 的片上内存,可以在 L1 缓存和共享内存之间进行分配(16K、32K 或 48K)。L1 缓存将线程在一个 warp 中的全局内存访问合并为尽可能少的缓存行。由于缓存的存在,每个线程之间的对齐差异对性能的影响可以忽略不计。第二和第三维度的跨步访问会引发效率问题;在这种情况下,使用共享内存使得你可以以合并的方式从全局内存中提取一个二维块到共享内存,并让连续的线程遍历共享内存块:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00271.jpeg
通过共享内存进行合并转置,NVIDIA 并行计算
当数据的维度不能整除为块大小与网格大小的乘积时,处理边界数据的线程将比其他线程执行得更快,并且内核代码必须以检查越界内存访问的方式编写。
在并行编程时,竞态条件、共享内存中的内存银行冲突以及数据无法保留在可用的寄存器中的情况是一些新的问题需要检查。合并全局内存访问是实现良好性能的最关键方面。NVIDIA® Nsight™ 工具将帮助你开发、调试和分析在 CPU 和 GPU 上执行的代码。
模型转换
当模型被保存时,得到的数据仅仅是一个数组列表,即权重向量(用于偏置)和矩阵(用于乘法运算)以及每一层的名称。将模型从一个框架转换到另一个框架非常简单:它包括加载一个数值数组并检查层的名称。以下是一些从 Caffe 深度学习框架(用 C++编写)到其他框架的转换示例:
要在 Torch 深度学习框架(用 Lua 编写)与 Theano 之间转换变量,你只需要一个工具将数据从 Lua 转换到 Python NumPy:
github.com/imodpasteur/lutorpy
要在 Tensorflow 和 Theano 之间转换模型,我建议您使用 Keras 库,它将保持最新,并允许您在 Theano 或 Tensorflow 中训练模型。例如,要将模型从 Tensorflow 转换为 Theano,请保持您的 Keras 安装配置为 Theano,如我们在第五章中所见,使用双向 LSTM 分析情感,加载 Tensorflow 权重,并按如下方式修改层名称:
from keras import backend as K
from keras.utils.conv_utils import convert_kernel
from keras.models import Model
# build your Keras model HERE
# then
model.load_weights('my_weights_tensorflow.h5')
for layer in model.layers:
if layer.__class__.__name__ in ['Convolution1D', 'Convolution2D']:
original_w = K.get_value(layer.W)
converted_w = convert_kernel(original_w)
K.set_value(layer.W, converted_w)
model.save_weights('my_weights_theano.h5')
一个镜像操作序列使我们能够反向操作,从 Theano 转换为 Tensorflow。
使用 Keras 设计网络的另一个优点是能够直接在云中进行训练,利用 Google Cloud 机器学习引擎,内置张量处理单元(TPU),作为 GPU 的替代方案,从底层为机器学习设计。
让我们以第五章中的示例为例,使用双向 LSTM 分析情感。
为了在云中训练模型,我在 Google 控制台中创建了一个名为DeepLearning Theano的项目,地址是console.cloud.google.com/iam-admin/projects
,并在项目的 API 管理器中启用了机器学习引擎 API。可能会有一些安装要求,您可以通过以下链接查看相关说明:cloud.google.com/ml-engine/docs/quickstarts/command-line
,例如 Google Cloud SDK 和项目配置。通过gcloud
init
命令,您可以重新初始化 SDK 配置,切换到DeepLearning Theano项目。
让我们将数据上传到云中新创建的存储桶,具体取决于您选择的区域(这里是europe-west1
):
gsutil mb -l europe-west1 gs://keras_sentiment_analysis
gsutil cp -r sem_eval2103.train gs://keras_sentiment_analysis/sem_eval2103.train
gsutil cp -r sem_eval2103.dev gs://keras_sentiment_analysis/sem_eval2103.dev
gsutil cp -r sem_eval2103.test gs://keras_sentiment_analysis/sem_eval2103.test
由于模型是在云中的实例上执行的,因此需要:
-
修改 Python 脚本,使其从远程存储桶加载文件流,而不是从本地目录加载,使用库
tensorflow.python.lib.io.file_io.FileIO(train_file, mode='r')
,而不是标准方法open(train_file, mode='r')
,两者的 mode 参数使用相同,'r’表示读取,w
表示写入, -
定义
setup.py
文件以配置云实例环境中所需的库:from setuptools import setup, find_packages setup(name='example5', version='0.1', packages=find_packages(), description='keras on gcloud ml-engine', install_requires=[ 'keras', 'h5py', 'nltk' ], zip_safe=False)
-
定义云部署配置文件
cloudml-gpu.yaml
:trainingInput: scaleTier: CUSTOM # standard_gpu provides 1 GPU. Change to complex_model_m_gpu for 4 GPUs masterType: standard_gpu runtimeVersion: "1.0"
在将训练提交给 Google ML Cloud 之前,先在本地检查训练是否正常工作,请运行以下命令:
gcloud ml-engine local train --module-name 7-google-cloud.bilstm \
--package-path ./7-google-cloud -- --job-dir ./7-google-cloud \
-t sem_eval2103.train -d sem_eval2103.dev -v sem_eval2103.test
如果本地一切正常,我们就可以将其提交到云端:
JOB_NAME="keras_sentiment_analysis_train_$(date +%Y%m%d_%H%M%S)"
gcloud ml-engine jobs submit training $JOB_NAME \
--job-dir gs://keras_sentiment_analysis/$JOB_NAME \
--runtime-version 1.0 \
--module-name 7-google-cloud.bilstm \
--package-path ./7-google-cloud \
--region europe-west1 \
--config=7-google-cloud/cloudml-gpu.yaml \
-- \
-t gs://keras_sentiment_analysis/sem_eval2103.train \
-d gs://keras_sentiment_analysis/sem_eval2103.dev \
-v gs://keras_sentiment_analysis/sem_eval2103.test
gcloud ml-engine jobs describe $JOB_NAME
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00272.jpeg
注意
请注意,Google ML Cloud 使用 Tensorflow 作为后端。
人工智能的未来
第二章,使用前馈网络分类手写数字介绍了多种优化技术(如 Adam、RMSProp 等),并提到了二阶优化技术。一个推广是还要学习更新规则:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00273.jpeg
这里,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00274.jpeg是优化器的参数,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00275.jpeg用来从不同的问题实例中学习,类似于优化器从问题到问题的泛化或迁移学习,从而在新问题上学习得更好。在这种学习如何学习或元学习框架下,目标是最小化学习正确的时间,因此需要在多个时间步长上定义:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00276.jpeg
其中:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00277.jpeg
循环神经网络可以作为优化器模型来使用https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00275.jpeg。这种解决多目标优化问题的泛化技术能提高神经网络的学习速度。
研究人员进一步探索,寻找通用人工智能,旨在实现具有人类水平技能的人工智能,具备自我提升的能力,并能以渐进的方式获得新技能,利用其内在的、之前学到的技能来寻找新的优化问题的解决方案。
技能可以被定义为一种智能工具,用于缩小或约束搜索空间,并限制机器人在无限可能性的世界中的行为。
构建通用人工智能要求你定义具有内在技能的智能架构,这些技能将由程序员硬编码到机器人中,并帮助解决较小的子问题,还需要定义新技能将被获得的顺序,即在人工智能学校中可以教授的课程路线图。而渐进学习是通过使用更简单的技能逐步学习技能,引导学习则涉及一个已经发现技能的教师,将这些技能教授给其他人工智能。
在自然语言翻译任务中,已经证明较小的网络能从较大的网络中更快、更好地学习,后者作为导师,已经学会了翻译并生成翻译供较小的网络学习,而不是直接从真实的人类翻译集学习。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00278.jpeg
上图表示 GoodAI 路线图研究所,用于评估人工智能的学习路线图。
自我探索、与导师的沟通以及采纳负面和正面反馈是朝向自主智能发展的一些思想,这种智能将能够自我发展,而当前的深度学习网络为这一未来铺平了道路。
在朝着这一目标努力的公司中,值得一提的是 GoodAI 以及亚马逊及其 Echo 产品和其背后的语音控制助手技术 Alexa,后者已经学会了超过 10,000 项技能,帮助你组织生活。Alexa 的知识已经变得如此庞大,以至于很难深入了解并找出它的局限性。为开发者提供的测试环境使他们能够将这些技能集成到更高层次的智能工具中:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/dl-theano/img/00279.jpeg
进一步阅读
你可以参考以下文章以了解更多:
-
CUDA C 和 C++的简单介绍,
devblogs.nvidia.com/parallelforall/easy-introduction-cuda-c-and-c/
-
如何高效访问 CUDA C/C++ 内核中的全局内存,
devblogs.nvidia.com/parallelforall/how-access-global-memory-efficiently-cuda-c-kernels/
-
在 CUDA C/C++ 中使用共享内存,
devblogs.nvidia.com/parallelforall/using-shared-memory-cuda-cc/
-
另一个 Tensorflow 初学者指南(第**4 部分 - Google Cloud ML + GUP + Keras),
liufuyang.github.io/2017/04/02/just-another-tensorflow-beginner-guide-4.html
-
通过梯度下降学习的学习,Marcin Andrychowicz、Misha Denil、Sergio Gomez、Matthew W. Hoffman、David Pfau、Tom Schaul、Brendan Shillingford 和 Nando de Freitas,2016
-
一种搜索通用人工智能的框架,Marek Rosa 和 Jan Feyereisl,The GoodAI Collective,2016
摘要
本章总结了我们对 Theano 深度学习概述的内容。
Theano 的第一组扩展,在 Python 和 C 中为 CPU 和 GPU 开发,已经在这里公开,用于为计算图创建新操作符。
将已学习的模型从一个框架转换到另一个框架并不是一项复杂的任务。Keras,这本书中多次提到的高层次库,作为 Theano 引擎之上的抽象层,提供了一种简单的方法来同时使用 Theano 和 Tensorflow,并推动模型在 Google ML Cloud 中的训练。
最后,本书中呈现的所有网络都是通用智能的基础,这些网络可以利用这些初步技能,如视觉或语言理解与生成,去学习更广泛的技能,这些技能仍然来自于现实世界数据或生成的数据的经验。