PyTorch nn.RNN 参数全解析

PyTorch nn.RNN 参数全解析全面解析 torch nn 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(Wihxt+bih+Whhht1+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=
x1TxNT
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=
xi1TxiLT
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=
x1tTxNtT
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(XtWih+bih+Ht1Whh+bhh)(3)

其中 H t , H t − 1 {\bf H}_t,{\bf H}_{t-1} Ht,Ht1 的形状为 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×dorX=[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:即非线性激活函数。可以选择 tanhrelu,默认是 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 (2num_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 hth_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×2hh_n 的形状为 ( 2 ⋅ num_layers ) × N × h (2\cdot \text{num\_layers})\times N\times h (2num_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_sizehidden_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(XtWih+bih+Ht1Whh+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是正确的。

最后

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/176412.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月26日 下午9:59
下一篇 2026年3月26日 下午9:59


相关推荐

  • img 转化成iso镜像的办法「建议收藏」

    img 转化成iso镜像的办法「建议收藏」最近在使用KVM启用虚拟机,然后将里面的环境和配置配置成我们公司需要的环境,再打包成iso镜像,之后再次生成新的虚拟机。但是KVM启动出的镜像生成的是img镜像,需要将img镜像转换成iso镜像

    2022年8月3日
    9
  • linux 复制文件夹内所有文件到另一个文件夹

    linux 复制文件夹内所有文件到另一个文件夹cp-Rf/home/user1/*/root/temp/将/home/user1目录下的所有东西拷到/root/temp/下而不拷贝user1目录本身。即格式为:cp-Rf原路径/目的路径/

    2022年8月23日
    7
  • linux lefse分析,科学网-linux本地化进行lefse分析-林国鹏的博文

    linux lefse分析,科学网-linux本地化进行lefse分析-林国鹏的博文注:参考来自网络,如侵权则删。##对应于上述A-F6个模块,本地版的命令行操作示例如下#A,设置LEfSe的数据格式,详情format_input.py-h#-c,指定class的行(必须指定);-s,指定sub_class的行(可缺省);#-u,指定subject_id的行(可缺省);-o,设置归一化值,默认-1即不执行标准化#注:版本问题,有时format_in…

    2022年4月29日
    55
  • eplan win10 能用激活码激活码_通用破解码

    eplan win10 能用激活码激活码_通用破解码,https://javaforall.net/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

    2022年3月17日
    594
  • 关于setContentView方法

    关于setContentView方法转载请以链接形式标明出处:本文出自:103style的博客baseonAndroid-29文中相关的源码有删减可以带着以下问题来看本文:为什么从代码设置属性和主题,得在setContentView之前?setContentView添加的View加载完成的回调方法?setContentView的执行流程?LayoutInflater的inflate方法不…

    2022年6月26日
    29
  • 深入理解HandlerThread

    深入理解HandlerThread以往遇到HandlerThread,对它的认识只是停留在MessageLooperHandler上,知道它有自己的消息队列,仅此而已。随着编程的深入,个人已不再满足表面上的理解,所以再次翻开HandlerThread源码,做梳理记录。HandlerThread集成Thread,并重写了Thread类的run方法(如果我们自定义一个类继承HandlerThread,就用不到run函数了):

    2022年7月12日
    23

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号