Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
Dive-into-DL-PyTorch
提交
389ba79f
D
Dive-into-DL-PyTorch
项目概览
OpenDocCN
/
Dive-into-DL-PyTorch
通知
9
Star
2
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
D
Dive-into-DL-PyTorch
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
389ba79f
编写于
4月 17, 2019
作者:
S
ShusenTang
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
update doc 6.5
上级
542f0593
变更
2
隐藏空白更改
内联
并排
Showing
2 changed file
with
136 addition
and
95 deletion
+136
-95
docs/chapter06_RNN/6.5_rnn-pytorch.md
docs/chapter06_RNN/6.5_rnn-pytorch.md
+136
-95
img/chapter06/6.5.png
img/chapter06/6.5.png
+0
-0
未找到文件。
docs/chapter06_RNN/6.5_rnn-pytorch.md
浏览文件 @
389ba79f
# 循环神经网络的简洁实现
#
6.5
循环神经网络的简洁实现
本节将使用
Gluon
来更简洁地实现基于循环神经网络的语言模型。首先,我们读取周杰伦专辑歌词数据集。
本节将使用
PyTorch
来更简洁地实现基于循环神经网络的语言模型。首先,我们读取周杰伦专辑歌词数据集。
```
{.python .input n=1}
import d2lzh as d2l
import math
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import loss as gloss, nn, rnn
```
python
import
time
import
math
import
numpy
as
np
import
torch
from
torch
import
nn
,
optim
import
torch.nn.functional
as
F
import
sys
sys
.
path
.
append
(
".."
)
import
d2lzh_pytorch
as
d2l
device
=
torch
.
device
(
'cuda'
if
torch
.
cuda
.
is_available
()
else
'cpu'
)
(corpus_indices, char_to_idx, idx_to_char,
vocab_size) = d2l.load_data_jay_lyrics()
(
corpus_indices
,
char_to_idx
,
idx_to_char
,
vocab_size
)
=
d2l
.
load_data_jay_lyrics
()
```
## 定义模型
##
6.5.1
定义模型
Gluon的
`rnn`
模块提供了循环神经网络的实现。下面构造一个含单隐藏层、隐藏单元个数为256的循环神经网络层
`rnn_layer`
,并对权重做初始化
。
PyTorch中的
`nn`
模块提供了循环神经网络的实现。下面构造一个含单隐藏层、隐藏单元个数为256的循环神经网络层
`rnn_layer`
。
```
{.python .input n=26}
```
python
num_hiddens
=
256
rnn_layer = rnn.RNN(num_hiddens)
rnn_layer
.initialize(
)
# rnn_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens) # 已测试
rnn_layer
=
nn
.
RNN
(
input_size
=
vocab_size
,
hidden_size
=
num_hiddens
)
```
接下来调用
`rnn_layer`
的成员函数
`begin_state`
来返回初始化的隐藏状态列表。它有一个形状为(隐藏层个数, 批量大小, 隐藏单元个数)的元素。
与上一节中实现的循环神经网络不同,这里
`rnn_layer`
的输入形状为(时间步数, 批量大小, 输入个数)。其中输入个数即one-hot向量长度(词典大小)。此外,
`rnn_layer`
作为
`nn.RNN`
实例,在前向计算后会分别返回输出和隐藏状态h,其中输出指的是隐藏层在
**各个时间步**
上计算并输出的隐藏状态,它们通常作为后续输出层的输入。需要强调的是,该“输出”本身并不涉及输出层计算,形状为(时间步数, 批量大小, 隐藏单元个数)。而
`nn.RNN`
实例在前向计算返回的隐藏状态指的是隐藏层在
**最后时间步**
的隐藏状态:当隐藏层有多层时,每一层的隐藏状态都会记录在该变量中;对于像长短期记忆(LSTM),隐藏状态是一个元组(h, c),即hidden state和cell state。我们会在本章的后面介绍长短期记忆和深度循环神经网络。关于循环神经网络(以LSTM为例)的输出,可以参考下图(
[
图片来源
](
https://stackoverflow.com/questions/48302810/whats-the-difference-between-hidden-and-output-in-pytorch-lstm/48305882
)
)。
<div
align=
center
>
<img
width=
"500"
src=
"../../img/chapter06/6.5.png"
/>
</div>
<div
align=
center
>
循环神经网络(以LSTM为例)的输出
</div>
```
{.python .input n=37}
batch_size = 2
state = rnn_layer.begin_state(batch_size=batch_size)
state[0].shape
```
与上一节中实现的循环神经网络不同,这里
`rnn_layer`
的输入形状为(时间步数, 批量大小, 输入个数)。其中输入个数即one-hot向量长度(词典大小)。此外,
`rnn_layer`
作为Gluon的
`rnn.RNN`
实例,在前向计算后会分别返回输出和隐藏状态,其中输出指的是隐藏层在各个时间步上计算并输出的隐藏状态,它们通常作为后续输出层的输入。需要强调的是,该“输出”本身并不涉及输出层计算,形状为(时间步数, 批量大小, 隐藏单元个数)。而
`rnn.RNN`
实例在前向计算返回的隐藏状态指的是隐藏层在最后时间步的可用于初始化下一时间步的隐藏状态:当隐藏层有多层时,每一层的隐藏状态都会记录在该变量中;对于像长短期记忆这样的循环神经网络,该变量还会包含其他信息。我们会在本章的后面介绍长短期记忆和深度循环神经网络
。
来看看我们的例子,输出形状为(时间步数, 批量大小, 输入个数),隐藏状态h的形状为(层数, 批量大小, 隐藏单元个数)
。
```
{.python .input n=38}
```
python
num_steps
=
35
X = nd.random.uniform(shape=(num_steps, batch_size, vocab_size))
batch_size
=
2
state
=
None
X
=
torch
.
rand
(
num_steps
,
batch_size
,
vocab_size
)
Y
,
state_new
=
rnn_layer
(
X
,
state
)
Y.shape, len(state_new), state_new[0].shape
print
(
Y
.
shape
,
len
(
state_new
),
state_new
[
0
].
shape
)
```
输出:
```
torch.Size([35, 2, 256]) 1 torch.Size([2, 256])
```
接下来我们继承
`Block`
类来定义一个完整的循环神经网络。它首先将输入数据使用one-hot向量表示后输入到
`rnn_layer`
中,然后使用全连接输出层得到输出。输出个数等于词典大小
`vocab_size`
。
> 如果`rnn_layer`是`nn.LSTM`实例,那么上面的输出是什么?
```
{.python .input n=39}
# 本类已保存在d2lzh包中方便以后使用
class RNNModel(nn.Block):
def __init__(self, rnn_layer, vocab_size, **kwargs):
super(RNNModel, self).__init__(**kwargs)
接下来我们继承
`Module`
类来定义一个完整的循环神经网络。它首先将输入数据使用one-hot向量表示后输入到
`rnn_layer`
中,然后使用全连接输出层得到输出。输出个数等于词典大小
`vocab_size`
。
```
python
# 本类已保存在d2lzh_pytorch包中方便以后使用
class
RNNModel
(
nn
.
Module
):
def
__init__
(
self
,
rnn_layer
,
vocab_size
):
super
(
RNNModel
,
self
).
__init__
()
self
.
rnn
=
rnn_layer
self
.
hidden_size
=
rnn_layer
.
hidden_size
*
(
2
if
rnn_layer
.
bidirectional
else
1
)
self
.
vocab_size
=
vocab_size
self.dense = nn.Dense(vocab_size)
self
.
dense
=
nn
.
Linear
(
self
.
hidden_size
,
vocab_size
)
self
.
state
=
None
def forward(self, inputs, state):
#
将输入转置成(num_steps, batch_size)后
获取one-hot向量表示
X =
nd.one_hot(inputs.T, self.vocab_size)
Y, s
tate = self.rnn(X
, state)
def
forward
(
self
,
inputs
,
state
):
# inputs: (batch, seq_len)
# 获取one-hot向量表示
X
=
d2l
.
to_onehot
(
inputs
,
vocab_size
)
# X是个list
Y
,
s
elf
.
state
=
self
.
rnn
(
torch
.
stack
(
X
)
,
state
)
# 全连接层会首先将Y的形状变成(num_steps * batch_size, num_hiddens),它的输出
# 形状为(num_steps * batch_size, vocab_size)
output = self.dense(Y.reshape((-1, Y.shape[-1])))
return output, state
def begin_state(self, *args, **kwargs):
return self.rnn.begin_state(*args, **kwargs)
output
=
self
.
dense
(
Y
.
view
(
-
1
,
Y
.
shape
[
-
1
]))
return
output
,
self
.
state
```
## 训练模型
##
6.5.2
训练模型
同上一节一样,下面定义一个预测函数。这里的实现区别在于前向计算和初始化隐藏状态的函数接口。
```
{.python .input n=41}
# 本函数已保存在d2lzh包中方便以后使用
def predict_rnn_
gluon(prefix, num_chars, model, vocab_size, ctx
, idx_to_char,
```
python
# 本函数已保存在d2lzh
_pytorch
包中方便以后使用
def
predict_rnn_
pytorch
(
prefix
,
num_chars
,
model
,
vocab_size
,
device
,
idx_to_char
,
char_to_idx
):
# 使用model的成员函数来初始化隐藏状态
state = model.begin_state(batch_size=1, ctx=ctx)
output = [char_to_idx[prefix[0]]]
state
=
None
output
=
[
char_to_idx
[
prefix
[
0
]]]
# output会记录prefix加上输出
for
t
in
range
(
num_chars
+
len
(
prefix
)
-
1
):
X = nd.array([output[-1]], ctx=ctx).reshape((1, 1))
(Y, state) = model(X, state) # 前向计算不需要传入模型参数
X
=
torch
.
tensor
([
output
[
-
1
]],
device
=
device
).
view
(
1
,
1
)
if
state
is
not
None
:
if
isinstance
(
state
,
tuple
):
# LSTM, state:(h, c)
state
=
(
state
[
0
].
to
(
device
),
state
[
1
].
to
(
device
))
else
:
state
=
state
.
to
(
device
)
(
Y
,
state
)
=
model
(
X
,
state
)
if
t
<
len
(
prefix
)
-
1
:
output
.
append
(
char_to_idx
[
prefix
[
t
+
1
]])
else
:
output.append(int(Y.argmax(
axis=1).asscalar
()))
output
.
append
(
int
(
Y
.
argmax
(
dim
=
1
).
item
()))
return
''
.
join
([
idx_to_char
[
i
]
for
i
in
output
])
```
让我们使用权重为随机值的模型来预测一次。
```
{.python .input n=42}
ctx = d2l.try_gpu()
model = RNNModel(rnn_layer, vocab_size)
model.initialize(force_reinit=True, ctx=ctx)
predict_rnn_gluon('分开', 10, model, vocab_size, ctx, idx_to_char, char_to_idx)
```
python
model
=
RNNModel
(
rnn_layer
,
vocab_size
).
to
(
device
)
predict_rnn_pytorch
(
'分开'
,
10
,
model
,
vocab_size
,
device
,
idx_to_char
,
char_to_idx
)
```
输出:
```
'分开戏想暖迎凉想征凉征征'
```
接下来实现训练函数。算法同上一节的一样,但这里只使用了相邻采样来读取数据。
```
{.python .input n=18}
# 本函数已保存在d2lzh包中方便以后使用
def train_and_predict_rnn_
gluon(model, num_hiddens, vocab_size, ctx
,
```
python
# 本函数已保存在d2lzh
_pytorch
包中方便以后使用
def
train_and_predict_rnn_
pytorch
(
model
,
num_hiddens
,
vocab_size
,
device
,
corpus_indices
,
idx_to_char
,
char_to_idx
,
num_epochs
,
num_steps
,
lr
,
clipping_theta
,
batch_size
,
pred_period
,
pred_len
,
prefixes
):
loss = gloss.SoftmaxCrossEntropyLoss()
model.initialize(ctx=ctx, force_reinit=True, init=init.Normal(0.01))
trainer = gluon.Trainer(model.collect_params(), 'sgd',
{'learning_rate': lr, 'momentum': 0, 'wd': 0})
loss
=
nn
.
CrossEntropyLoss
()
optimizer
=
torch
.
optim
.
Adam
(
model
.
parameters
(),
lr
=
lr
)
model
.
to
(
device
)
state
=
None
for
epoch
in
range
(
num_epochs
):
l_sum
,
n
,
start
=
0.0
,
0
,
time
.
time
()
data_iter = d2l.data_iter_consecutive(
corpus_indices, batch_size, num_steps, ctx)
state = model.begin_state(batch_size=batch_size, ctx=ctx)
data_iter
=
d2l
.
data_iter_consecutive
(
corpus_indices
,
batch_size
,
num_steps
,
device
)
# 相邻采样
for
X
,
Y
in
data_iter
:
for s in state:
s.detach()
with autograd.record():
(output, state) = model(X, state)
y = Y.T.reshape((-1,))
l = loss(output, y).mean()
if
state
is
not
None
:
# 使用detach函数从计算图分离隐藏状态, 这是为了
# 使模型参数的梯度计算只依赖一次迭代读取的小批量序列(防止梯度计算开销太大)
if
isinstance
(
state
,
tuple
):
# LSTM, state:(h, c)
state
=
(
state
[
0
].
detach
(),
state
[
1
].
detach
())
else
:
state
=
state
.
detach
()
(
output
,
state
)
=
model
(
X
,
state
)
# output: 形状为(num_steps * batch_size, vocab_size)
# Y的形状是(batch_size, num_steps),转置后再变成长度为
# batch * num_steps 的向量,这样跟输出的行一一对应
y
=
torch
.
transpose
(
Y
,
0
,
1
).
contiguous
().
view
(
-
1
)
l
=
loss
(
output
,
y
.
long
())
optimizer
.
zero_grad
()
l
.
backward
()
# 梯度裁剪
params = [p.data() for p in model.collect_params().values()]
d2l.grad_clipping(params, clipping_theta, ctx)
trainer.step(1) # 因为已经误差取过均值,梯度不用再做平均
l_sum += l.asscalar() * y.size
n += y.size
d2l
.
grad_clipping
(
model
.
parameters
(),
clipping_theta
,
device
)
optimizer
.
step
()
l_sum
+=
l
.
item
()
*
y
.
shape
[
0
]
n
+=
y
.
shape
[
0
]
try
:
perplexity
=
math
.
exp
(
l_sum
/
n
)
except
OverflowError
:
perplexity
=
float
(
'inf'
)
if
(
epoch
+
1
)
%
pred_period
==
0
:
print
(
'epoch %d, perplexity %f, time %.2f sec'
%
(
epoch + 1,
math.exp(l_sum / n)
, time.time() - start))
epoch
+
1
,
perplexity
,
time
.
time
()
-
start
))
for
prefix
in
prefixes
:
print(' -', predict_rnn_
gluon
(
prefix, pred_len, model, vocab_size,
ctx
, idx_to_char,
print
(
' -'
,
predict_rnn_
pytorch
(
prefix
,
pred_len
,
model
,
vocab_size
,
device
,
idx_to_char
,
char_to_idx
))
```
使用和上一节实验中一样的超参数来训练模型。
使用和上一节实验中一样的超参数
(除了学习率)
来训练模型。
```
{.python .input n=19}
num_epochs, batch_size, lr, clipping_theta = 2
00, 32, 1e2, 1e-2
```
python
num_epochs
,
batch_size
,
lr
,
clipping_theta
=
2
50
,
32
,
1e-3
,
1e-2
# 注意这里的学习率设置
pred_period
,
pred_len
,
prefixes
=
50
,
50
,
[
'分开'
,
'不分开'
]
train_and_predict_rnn_
gluon(model, num_hiddens, vocab_size, ctx
,
train_and_predict_rnn_
pytorch
(
model
,
num_hiddens
,
vocab_size
,
device
,
corpus_indices
,
idx_to_char
,
char_to_idx
,
num_epochs
,
num_steps
,
lr
,
clipping_theta
,
batch_size
,
pred_period
,
pred_len
,
prefixes
)
```
输出:
```
epoch 50, perplexity 10.658418, time 0.05 sec
- 分开始我妈 想要你 我不多 让我心到的 我妈妈 我不能再想 我不多再想 我不要再想 我不多再想 我不要
- 不分开 我想要你不你 我 你不要 让我心到的 我妈人 可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的
epoch 100, perplexity 1.308539, time 0.05 sec
- 分开不会痛 不要 你在黑色幽默 开始了美丽全脸的梦滴 闪烁成回忆 伤人的美丽 你的完美主义 太彻底 让我
- 不分开不是我不要再想你 我不能这样牵着你的手不放开 爱可不可以简简单单没有伤害 你 靠着我的肩膀 你 在我
epoch 150, perplexity 1.070370, time 0.05 sec
- 分开不能去河南嵩山 学少林跟武当 快使用双截棍 哼哼哈兮 快使用双截棍 哼哼哈兮 习武之人切记 仁者无敌
- 不分开 在我会想通 是谁开没有全有开始 他心今天 一切人看 我 一口令秋软语的姑娘缓缓走过外滩 消失的 旧
epoch 200, perplexity 1.034663, time 0.05 sec
- 分开不能去吗周杰伦 才离 没要你在一场悲剧 我的完美主义 太彻底 分手的话像语言暴力 我已无能为力再提起
- 不分开 让我面到你 爱情来的太快就像龙卷风 离不开暴风圈来不及逃 我不能再想 我不能再想 我不 我不 我不
epoch 250, perplexity 1.021437, time 0.05 sec
- 分开 我我外的家边 你知道这 我爱不看的太 我想一个又重来不以 迷已文一只剩下回忆 让我叫带你 你你的
- 不分开 我我想想和 是你听没不 我不能不想 不知不觉 你已经离开我 不知不觉 我跟了这节奏 后知后觉
```
## 小结
*
Gluon的
`rnn`
模块提供了循环神经网络层的实现。
*
Gluon的
`rnn.RNN`
实例在前向计算后会分别返回输出和隐藏状态。该前向计算并不涉及输出层计算。
## 练习
*
与上一节的实现进行比较。看看Gluon的实现是不是运行速度更快?如果你觉得差别明显,试着找找原因。
*
PyTorch的
`nn`
模块提供了循环神经网络层的实现。
*
PyTorch的
`nn.RNN`
实例在前向计算后会分别返回输出和隐藏状态。该前向计算并不涉及输出层计算。
## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/4089)
-----------
> 注:除代码外本节与原书此节基本相同,[原书传送门](https://zh.d2l.ai/chapter_recurrent-neural-networks/rnn-gluon.html)
![](
../img/qr_rnn-gluon.svg
)
img/chapter06/6.5.png
0 → 100644
浏览文件 @
389ba79f
32.4 KB
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录