RNN输入数据加工问题与循环原理流程分析(torch)
RNN输入数据加工问题与循环流程分析(torch)
-
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.]]]) '''
-
验证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 = tanh(W_{ih} x_t + b_{ih} + W_{hh} h_{(t-1)} + b_{hh}) W = rnn.Rnn.all_weights[0][0].data # [weight_ih_l[k],weight_hh_l[k],bias_ih_l[k],bias_hh_l[k]] k: 代表第k个rnn_layer 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(U))) # 第三时刻(第三步) 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原理图
-
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])) '''