目录

一、简介二、前置知识三、解析3.1 所有参数3.2 输入参数3.3 输出参数

四、通过例子来进一步理解 nn.RNN五、从零开始手写一个单隐层单向RNN最后

一、简介

torch.nn.RNN 用于构建循环层,其中的计算规则如下:

h

t

=

tanh

(

W

i

h

x

t

+

b

i

h

+

W

h

h

h

t

1

+

b

h

h

)

(1)

\boldsymbol{h}_{t}=\tanh({\bf W}_{ih}\boldsymbol{x}_t+\boldsymbol{b}_{ih}+{\bf W}_{hh}\boldsymbol{h}_{t-1}+\boldsymbol{b}_{hh}) \tag{1}

ht​=tanh(Wih​xt​+bih​+Whh​ht−1​+bhh​)(1)

其中

h

t

\boldsymbol{h}_{t}

ht​ 是

t

t

t 时刻的隐层状态,

x

t

\boldsymbol{x}_{t}

xt​ 是

t

t

t 时刻的输入。下标

i

i

i 是

i

n

p

u

t

input

input 的简写,下标

h

h

h 是

h

i

d

d

e

n

hidden

hidden 的简写。

W

,

b

{\bf W},\boldsymbol{b}

W,b 分别是权重和偏置。

二、前置知识

先回顾一下普通的神经网络,我们在训练它的过程中通常会投喂一小批量的数据。不妨设

batch_size

=

N

\text{batch\_size}=N

batch_size=N,则投喂的数据的形式为:

X

=

[

x

1

T

x

N

T

]

N

×

d

{\bf X}= \begin{bmatrix} \boldsymbol{x}_1^{\text T} \\ \vdots \\ \boldsymbol{x}_N^{\text T} \end{bmatrix}_{N\times d}

X=⎣

⎡​x1T​⋮xNT​​⎦

⎤​N×d​

其中

x

i

=

(

x

i

1

,

x

i

2

,

,

x

i

d

)

T

\boldsymbol{x}_i=(x_{i1},x_{i2},\cdots,x_{id})^{\text T}

xi​=(xi1​,xi2​,⋯,xid​)T 为特征向量,维数为

d

d

d。

在处理序列问题中,我们会将词元转化成对应的特征向量。例如在处理一个英文句子时,我们通常会通过某种手段将每个单词转化为合适的特征向量。设序列(句子)长度为

L

L

L,于是在此情景下,一个句子可以表示为:

seq

i

=

[

x

i

1

T

x

i

L

T

]

L

×

d

\text{seq}_i= \begin{bmatrix} \boldsymbol{x}_{i1}^{\text T} \\ \vdots \\ \boldsymbol{x}_{iL}^{\text T} \end{bmatrix}_{L\times d}

seqi​=⎣

⎡​xi1T​⋮xiLT​​⎦

⎤​L×d​

其中的每个

x

i

j

,

j

=

1

,

,

L

\boldsymbol{x}_{ij},\;j=1,\cdots, L

xij​,j=1,⋯,L 都对应了句子

seq

i

\text{seq}_i

seqi​ 中的一个单词。在上述约定下,我们在

t

t

t 时刻投喂给RNN的数据为:

X

t

=

[

x

1

t

T

x

N

t

T

]

N

×

d

(2)

{\bf X}_t= \begin{bmatrix} \boldsymbol{x}_{1t}^{\text T} \\ \vdots \\ \boldsymbol{x}_{Nt}^{\text T} \end{bmatrix}_{N\times d}\tag{2}

Xt​=⎣

⎡​x1tT​⋮xNtT​​⎦

⎤​N×d​(2)

从而

(

1

)

(1)

(1) 式改写为

H

t

=

tanh

(

X

t

W

i

h

+

b

i

h

+

H

t

1

W

h

h

+

b

h

h

)

(3)

{\bf H}_t=\tanh({\bf X}_t{\bf W}_{ih}+\boldsymbol{b}_{ih}+{\bf H}_{t-1}{\bf W}_{hh}+\boldsymbol{b}_{hh})\tag{3}

Ht​=tanh(Xt​Wih​+bih​+Ht−1​Whh​+bhh​)(3)

其中

H

t

,

H

t

1

{\bf H}_t,{\bf H}_{t-1}

Ht​,Ht−1​ 的形状为

N

×

h

N\times h

N×h,

W

i

h

{\bf W}_{ih}

Wih​ 的形状为

d

×

h

d\times h

d×h,

W

h

h

{\bf W}_{hh}

Whh​ 的形状为

h

×

h

h\times h

h×h,

b

i

h

,

b

h

h

\boldsymbol{b}_{ih},\boldsymbol{b}_{hh}

bih​,bhh​ 的形状为

1

×

h

1\times h

1×h,求和时利用广播机制。

在 nn.RNN 中,我们是一次性将所有时刻的数据全部投喂进去,数据形式为:

X

=

[

seq

1

,

seq

2

,

,

seq

N

]

N

×

L

×

d

or

X

=

[

X

1

,

X

2

,

,

X

L

]

L

×

N

×

d

{\bf X}=[\text{seq}_1,\text{seq}_2,\cdots,\text{seq}_N]_{N\times L\times d}\quad\text{or}\quad {\bf X}=[{\bf X}_1,{\bf X}_2,\cdots,{\bf X}_L]_{L\times N\times d}

X=[seq1​,seq2​,⋯,seqN​]N×L×d​orX=[X1​,X2​,⋯,XL​]L×N×d​

其中左边代表 batch_first=True 的情形,右边代表 batch_first=False 的情形。

注意: 在一个 batch 中,所有 sequence 的长度要保持相同,即

L

L

L 需一致。

三、解析

3.1 所有参数

有了前置知识后,我们就能很方便的解释这些参数了。

input_size:即

d

d

d;hidden_size:即

h

h

h;num_layers:即RNN的层数。默认是

1

1

1 层。该参数大于

1

1

1 时,会形成 Stacked RNN,又称多层RNN或深度RNN;nonlinearity:即非线性激活函数。可以选择 tanh 或 relu,默认是 tanh;bias:即偏置。默认启用,可以选择关闭;batch_first:即是否选择让 batch_size 作为输入的形状中的第一个参数。当 batch_first=True 时,输入应具有

N

×

L

×

d

N\times L\times d

N×L×d 这样的形状,否则应具有

L

×

N

×

d

L\times N\times d

L×N×d 这样的形状。默认是 False;dropout:即是否启用 dropout。如要启用,则应设置 dropout 的概率,此时除最后一层外,RNN的每一层后面都会加上一个dropout层。默认是

0

0

0,即不启用;bidirectional:即是否启用双向RNN,默认关闭。

3.2 输入参数

这里我们只考虑有 batch 的情况。

当 batch_first=True 时,输入 input 应具有形状

N

×

L

×

d

N\times L\times d

N×L×d,否则应具有形状

L

×

N

×

d

L\times N\times d

L×N×d。

h_0 为初始时刻的隐状态。当RNN为单向RNN时,h_0 的形状应为

num_layers

×

N

×

h

\text{num\_layers}\times N\times h

num_layers×N×h;当RNN为双向RNN时,h_0 的形状应为

(

2

num_layers

)

×

N

×

h

(2\cdot \text{num\_layers})\times N\times h

(2⋅num_layers)×N×h。如不提供该参数的值,则默认为全0张量。

3.3 输出参数

这里我们只考虑有 batch 的情况。

当RNN为单向RNN时:若 batch_first=True,输出 output 具有形状

N

×

L

×

h

N\times L\times h

N×L×h,否则具有形状

L

×

N

×

h

L\times N\times h

L×N×h。当 batch_first=False 时,output[t, :, :] 代表时刻

t

t

t 时,RNN最后一层(之所以用最后一层这个术语是因为有可能出现Stacked RNN情形)的输出

h

t

\boldsymbol{h}_t

ht​。h_n 代表最终的隐状态,形状为

num_layers

×

N

×

h

\text{num\_layers}\times N\times h

num_layers×N×h。

当RNN为双向RNN时:若 batch_first=True,输出 output 具有形状

N

×

L

×

2

h

N\times L\times 2h

N×L×2h,否则具有形状

L

×

N

×

2

h

L\times N\times 2h

L×N×2h。h_n 的形状为

(

2

num_layers

)

×

N

×

h

(2\cdot \text{num\_layers})\times N\times h

(2⋅num_layers)×N×h。

事实上,对于单向RNN,有

output

=

[

H

1

,

H

2

,

,

H

L

]

L

×

N

×

h

,

h_n

=

[

H

L

]

1

×

N

×

h

\text{output}=[{\bf H}_1,{\bf H}_2,\cdots,{\bf H}_L]_{L\times N\times h},\quad \text{h\_n}=[{\bf H}_L]_{1\times N\times h}

output=[H1​,H2​,⋯,HL​]L×N×h​,h_n=[HL​]1×N×h​

四、通过例子来进一步理解 nn.RNN

以单隐层单向RNN为例(接下来的例子都默认 batch_first=False)。

假设有一个英文句子:He ate an apple.,忽略 . 并设置词元为单词(word)时,该序列的长度为

4

4

4。简便起见,我们假设每个词元都对应了一个

6

6

6 维的特征向量,则上述的序列可写成:

import torch

import torch.nn as nn

torch.manual_seed(42)

seq = torch.randn(4, 6) # 只是为了举例

print(seq)

# tensor([[ 1.9269, 1.4873, 0.9007, -2.1055, 0.6784, -1.2345],

# [-0.0431, -1.6047, 0.3559, -0.6866, -0.4934, 0.2415],

# [-1.1109, 0.0915, -2.3169, -0.2168, -0.3097, -0.3957],

# [ 0.8034, -0.6216, -0.5920, -0.0631, -0.8286, 0.3309]])

将这个句子视为一个 batch,即(注意形状为

L

×

N

×

d

L\times N\times d

L×N×d):

inputs = seq.unsqueeze(1)

print(inputs)

# tensor([[[ 1.9269, 1.4873, 0.9007, -2.1055, 0.6784, -1.2345]],

# [[-0.0431, -1.6047, 0.3559, -0.6866, -0.4934, 0.2415]],

# [[-1.1109, 0.0915, -2.3169, -0.2168, -0.3097, -0.3957]],

# [[ 0.8034, -0.6216, -0.5920, -0.0631, -0.8286, 0.3309]]])

print(inputs.shape)

# torch.Size([4, 1, 6])

有了 inputs,我们还需要初始化隐状态 h_0,不妨设

h

=

3

h=3

h=3:

h_0 = torch.randn(1, 1, 3)

print(h_0)

# tensor([[[ 1.3525, 0.6863, -0.3278]]])

接下来创建RNN层,事实上只需要输入 input_size 和 hidden_size 即可:

rnn = nn.RNN(6, 3)

观察输出:

outputs, h_n = rnn(inputs, h_0)

print(outputs)

# tensor([[[-0.5428, 0.9207, 0.7060]],

# [[-0.2245, 0.2461, -0.4578]],

# [[ 0.5950, -0.3390, -0.4598]],

# [[ 0.9281, -0.7660, 0.5954]]], grad_fn=)

print(h_n)

# tensor([[[ 0.9281, -0.7660, 0.5954]]], grad_fn=)

五、从零开始手写一个单隐层单向RNN

首先写好框架:

class RNN(nn.Module):

def __init__(self, input_size, hidden_size):

super().__init__()

pass

def forward(self, inputs, h_0):

pass

我们的计算遵循

(

3

)

(3)

(3) 式,即:

H

t

=

tanh

(

X

t

W

i

h

+

b

i

h

+

H

t

1

W

h

h

+

b

h

h

)

{\bf H}_t=\tanh({\bf X}_t{\bf W}_{ih}+\boldsymbol{b}_{ih}+{\bf H}_{t-1}{\bf W}_{hh}+\boldsymbol{b}_{hh})

Ht​=tanh(Xt​Wih​+bih​+Ht−1​Whh​+bhh​)

class RNN(nn.Module):

def __init__(self, input_size, hidden_size):

super().__init__()

self.W_ih = torch.randn(input_size, hidden_size)

self.W_hh = torch.randn(hidden_size, hidden_size)

self.b_ih = torch.randn(1, hidden_size)

self.b_hh = torch.randn(1, hidden_size)

def forward(self, inputs, h_0):

L, N, d = inputs.shape # 分别对应序列长度、批量大小和特征维度

H = h_0[0] # 因为h_0的形状为(1,N,h),我们需要使用(N,h)去计算

outputs = torch.zeros(L, N, H.shape[1])

for t in range(L):

X_t = inputs[t]

H = torch.tanh(X_t @ self.W_ih + self.b_ih + H @ self.W_hh + self.b_hh)

outputs[t] = H

h_n = outputs[-1].unsqueeze(0) # h_n实际上就是h_L,但此时的形状为(N,h)

return outputs, h_n

为了检验我们的RNN是正确的,我们需要使用相同的输入来验证我们的输出是否与之前的一致。

torch.manual_seed(42)

seq = torch.randn(4, 6)

inputs = seq.unsqueeze(1)

h_0 = torch.randn(1, 1, 3)

# 保持RNN内部参数:权重和偏置一致

rnn = nn.RNN(6, 3)

params = [param.data.T for param in rnn.parameters()]

my_rnn = RNN(6, 3)

my_rnn.W_ih = params[0]

my_rnn.W_hh = params[1]

my_rnn.b_ih[0] = params[2]

my_rnn.b_hh[0] = params[3]

outputs, h_n = my_rnn(inputs, h_0)

print(outputs)

# tensor([[[-0.5428, 0.9207, 0.7060]],

# [[-0.2245, 0.2461, -0.4578]],

# [[ 0.5950, -0.3390, -0.4598]],

# [[ 0.9281, -0.7660, 0.5954]]])

print(h_n)

# tensor([[[ 0.9281, -0.7660, 0.5954]]])

可以看出结果与之前的一致,这说明我们构造的RNN是正确的。

最后

博主才疏学浅,如有错误请在评论区指出,感谢!

推荐阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: