文章目录
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(t−1)+bhh)ht:t时刻的隐藏层状态Xt:t时刻的输入ht−1:前一时刻的隐藏层状态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]))
'''