RNN输入数据加工问题与循环流程分析(torch)

本文介绍了如何处理RNN输入数据,包括文本数据转化为dataset和批处理,以及如何验证RNN内部循环过程。通过实例展示了RNN模型的建立和前向传播,手动计算与模型输出的对比,证明了RNN的工作原理。此外,讨论了torch的pack/pad序列优化技术,避免了在处理不同长度序列时填充0的无效计算,提高了资源利用率。
摘要由CSDN通过智能技术生成

1、RNN输入数据加工问题

1.1 文本样例数据

# 假设训练样本text,为4行文本,每个词的词向量为torch.size(1),单元素0维量,tensor.item()获取其中元素
'''
text = [我,
        我 爱 你,
        爱 ,
        你
       ]
  --》[[1],[1,2,3],[2],[3]]
'''

1.2 形成dataset

import torch
from torch.nn.utils.rnn import pack_padded_sequence,pad_packed_sequence
import torch.nn as nn
from torch.utils.data import Dataset,DataLoader

# 文本长度不一致情况,按最大长度补充0
text = [[1], [1, 2, 3], [2], [3]]
feature_size = 1  # 句子序列,每一个词代表一个feature,

# 提取序列数据集
class Datasets:

    def __init__(self, data, feature_size):
        self.data = data
        self.feature_size = feature_size
        self.max_seq_len = max([len(i) for i in text])  # 所有样本中最长的序列,这里指最长文本
        self.data_len = len(data)

    def __getitem__(self, index):
        seq_zero = torch.zeros(size=(self.max_seq_len, self.feature_size))  # 构建等长全零序列
        seq_data = torch.tensor(self.data[index]).reshape(-1, self.feature_size)
        seq_zero[0:seq_data.shape[0], 0:seq_data.shape[1]] = seq_data  # 将全零对应位置填充序列值
        return seq_zero

    def __len__(self):
        return self.data_len
   

data = Datasets(text, feature_size=feature_size)
print(data[0])
print(data[1])
''' 如结果所示,已将序列按序列最大长度,进行等长处理
tensor([[1.],
        [0.],
        [0.]])
        
tensor([[1.],
        [2.],
        [3.]])       
'''

1.3 形成批数据

# 形成序列批数据
batch_size = 2  # 每批数据训练样本的个数
dataloader = DataLoader(dataset=data, batch_size=batch_size)

for i in dataloader:
    print(i)
    break
    

'''
tensor([[[1.],
         [0.],
         [0.]],

        [[1.],
         [2.],
         [3.]]])
'''

2、验证RNN内部数据循环过程(RNN原理)

2.1 建立RNN模型

# 构建RNN网络,只进行一次 RNN 循环,未加输出层
class RNN(nn.Module):

    def __init__(self):
        super().__init__()
        self.Rnn = nn.RNN(input_size=1  # 输出特征尺寸,这里只每个词的词向量size
                          , hidden_size=2  # 隐藏层输出尺寸,每个x经过循环层后的输出结果
                          , num_layers=1  # 循环层的个数,如果>=2,后一个RNN层将接收上一个RNN的输出结果
                          , nonlinearity='tanh'  # 非线性激活(tanh/relu)
                          , bias=False  # 是否添加偏置
                          , batch_first=True  # 输入输出的顺序batch开头, (batch,seq, feature_size)
                          , dropout=0  # 在每个RNN引入dropout(最后一个RNN除外)
                          , bidirectional=False  # 是否设置双向RNN
                          )

    def forward(self, x):
        x = self.Rnn(x)
        return x

2.2 rnn 向前传播结果

# 实例化RNN 网络
rnn = RNN()

for i in dataloader:
    out, hn = rnn(i)
    print(out)  # 中间每一步ht(包含前面步的结果) 形状为(batch,seq_len,hidden_size)
    print(hn)  # 最后一步ht|t=seq_len 形状为(batch,hidden_size)
    break
'''    
  tensor([[[ 0.2950, -0.1102],
         [-0.1728, -0.0011],
         [ 0.0981, -0.0234]],

        [[ 0.2950, -0.1102],
         [ 0.4083, -0.2188],
         [ 0.5828, -0.3432]]], grad_fn=<TransposeBackward1>)
         
         
tensor([[[ 0.0981, -0.0234],
         [ 0.5828, -0.3432]]], grad_fn=<StackBackward>)
 '''

2.3 手动进行rnn 各节点的计算

rnn 单层循环计算流程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

h t = t a n h ( W x t + b i h + U h ( t − 1 ) + b h h ) h t : t 时刻的隐藏层状态 X t : t 时刻的输入 h t − 1 : 前一时刻的隐藏层状态 b : 偏置 \begin{array}{l} h_t = tanh(Wx_t + b_{ih} + Uh_{(t-1)} + b_{hh})\\ ht: t时刻的隐藏层状态\\ X_t:t时刻的输入\\ h_{t-1}:前一时刻的隐藏层状态\\ b:偏置 \end{array} ht=tanh(Wxt+bih+Uh(t1)+bhh)ht:t时刻的隐藏层状态Xt:t时刻的输入ht1:前一时刻的隐藏层状态b:偏置

# rnn.Rnn.all_weights[k] 循环网络参数  k: 代表第k个循环层,这里是单层循环 k=0
#[[weight_ih_l,weight_hh_l,bias_ih_l,bias_hh_l]] 
W = rnn.Rnn.all_weights[0][0].data 
U = rnn.Rnn.all_weights[0][1].data


# 针对样本1 序列为[1,0,0]
h0 = torch.zeros(1, 2)
test1 = torch.tensor([[1], [0], [0]], dtype=torch.float)

# 第一时刻(第一步) h1 = W*x1+U*h0
h1 = torch.tanh(torch.mm(test1[0].unsqueeze(dim=0), torch.t(W)) + torch.mm(h0, torch.t(U)))

# 第二时刻(第二步) h2 = W*x2+U*h1
h2 = torch.tanh(torch.mm(test1[1].unsqueeze(dim=0), torch.t(W)) + torch.mm(h1, torch.t(W)))

# 第三时刻(第三步) h3 = W*x3+U*h2
h3 = torch.tanh(torch.mm(test1[2].unsqueeze(dim=0), torch.t(W)) + torch.mm(h2, torch.t(U)))

out1 = torch.stack((h1, h2, h3), dim=0)
print(out1)

'''
tensor([[[ 0.2950, -0.1102]],

        [[-0.1728, -0.0011]],

        [[ 0.0981, -0.0234]]])
'''

2.4 结果验证

# 针对样本1 [1,0,0],rnn模型向前传播的每个节点的结果,和手动计算ht=xt*W+U*ht-1的结果完全一样,从而侧面验证RNN原理图

在这里插入图片描述

3、torch内部pack/pad 优化短序列填充部分的计算(计算太多0浪费资源)

'''
如上图所示,填充的0特征也一直参与计算,但实际中0只是为了填充矩阵形状,计算无意义,浪费资源
为优化此类问题,torch提供了pack/pad函数
'''

a = torch.tensor([[[1.],
                   [0.],
                   [0.]],

                  [[1.],
                   [2.],
                   [3.]]])

print(rnn(a)[0])
# rnn模型计算结果,序列中填充的0也参与了结果运算,ht=U*h(t-1)+W*0 x特征不起作用,且一直累加h(t-1)*U
'''
tensor([[[ 0.1739, -0.0812],
         [-0.1102,  0.1351],
         [ 0.1213, -0.1079]],

        [[ 0.1739, -0.0812],
         [ 0.2361, -0.0268],
         [ 0.4040, -0.0814]]], grad_fn=<TransposeBackward1>)
'''

# 使用pack_padded_sequence重新打包张量 a
sort = sorted([(torch.sum(a[i] > 0).item(), i) for i in range(a.shape[0])], key=lambda x: x[0], reverse=True)
sort_index = [i[1] for i in sort]
sort_len = [i[0] for i in sort]

pack_a = pack_padded_sequence(input=a[sort_index]  # 经过填充的seq(按序列原本长度有从大到小排序)
                              , lengths=sort_len  # 每一个序列填充前的长度
                              , batch_first=True  # 输入输出的顺序batch开头, (batch,seq, feature_size)
                              , enforce_sorted=True  # 序列是否按填充前长度排序.
                              )
print(pack_a)

'''
PackedSequence(data=tensor([[1.],
        [1.],
        [2.],
        [3.]]), batch_sizes=tensor([2, 1, 1]), sorted_indices=None, unsorted_indices=None)
        
解读:原本一批数据batch_size=2,样本1[1,0,0]和样本2[1,2,3]
     两两组合着一起运算(像多线程一样)[1,1]两个样本同时计算第一个节点数据
     [0,2],[0,3]计算剩下节点数据,因为batch_size=2,所有torch相当于两个样本一起计算的
     
     经过pack压缩后,样本1 [1],样本2[1,2,3], batch_size=tensor([2, 1, 1])
     这样计算第一个节点时按batch_size=2 [1,1]一起计算
     计算剩下节点时,batch_size=1,[2],[3]单独计算剩下节点
'''


# rnn运行结果,可以看到,第一个样本只计算量x1=1,第二个样本计算了x1=1,x2=2,x3=3三个ht
print(rnn(pack_a))
'''
PackedSequence(data=tensor([[-0.4410,  0.5417],

        [-0.4410,  0.5417],
        [-0.7087,  0.9123],
        [-0.8708,  0.9822]], grad_fn=<CatBackward>), batch_sizes=tensor([2, 1, 1]), sorted_indices=None, unsorted_indices=None)
'''


# 将压缩后的结果,填充,就和不压缩前模型运算的形状一样了
print(pad_packed_sequence(sequence=rnn(pack_a)[0]
                          , batch_first=True
                          , padding_value=0.0
                          , total_length=None))
'''
(tensor([[[-0.4410,  0.5417],
         [-0.7087,  0.9123],
         [-0.8708,  0.9822]],

        [[-0.4410,  0.5417],
         [ 0.0000,  0.0000],
         [ 0.0000,  0.0000]]], grad_fn=<TransposeBackward0>), tensor([3, 1]))

'''

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值