私有变量 状态模式 javafx highcharts jvm drupal7 mono swift2 angular视频 abaqus是什么软件 cos图像和sin图像 input取消边框 linux安装 远程登录linux python源码下载 h5模板 h370主板 存储过程写法 pdf密码移除 大势至usb监控 php取整 粉碎文件工具 羽化快捷键 苹果8怎么截屏 php上传文件 top命令详解 惠普打印机怎么加粉 js数组操作 任务栏跑到右侧怎么办 id页码怎么设置 唯品会客服在哪 xd下载 迅雷去广告 sql四舍五入 电视应用安装器 php时间戳转换日期 gtx1030显卡 mysql注释 数据软件 php聊天室
当前位置: 首页 > 学习教程  > python

深度学习06 - LSTM网络-处理可变长序列输入问题

2021/2/6 22:23:08 文章标签: 测试文章如有侵权请发送至邮箱809451989@qq.com投诉后文章立即删除

1、问题 RNN的输入是按照批次来进行输入的,默认是每一批次的数据是大小相同的,但是在某些时候,比如语音识别或nlp等领域输入的数据每一批次,每一组的特征数是不同的(例如每次说的话包含的单词个数是不同的),我们需要进…

1、问题

RNN的输入是按照批次来进行输入的,默认是每一批次的数据是大小相同的,但是在某些时候,比如语音识别或nlp等领域输入的数据每一批次,每一组的特征数是不同的(例如每次说的话包含的单词个数是不同的),我们需要进行处理
在这里插入图片描述

2、解决问题

参考文档:序列长度不固定怎么办
需要使用到的函数:

torch.nn.utils.rnn.pad_sequence()
torch.nn.utils.rnn.pack_padded_sequence()
torch.nn.utils.rnn.pad_packed_sequence()

1、pad_sequence

我们构造如下矩阵,查看此函数的作用:

import torch
from torch import nn
import torch.nn.utils.rnn as rnn_utils

train_x = [torch.tensor([1, 1, 1, 1, 1, 1, 1]),
           torch.tensor([2, 2, 2, 2, 2, 2]),
           torch.tensor([3, 3, 3, 3, 3]),
           torch.tensor([4, 4, 4, 4]),
           torch.tensor([5, 5, 5]),
           torch.tensor([6, 6]),
           torch.tensor([7])]
x = rnn_utils.pad_sequence(train_x, batch_first=True)
print(x)

结果:

tensor([[1, 1, 1, 1, 1, 1, 1],
[2, 2, 2, 2, 2, 2, 0],
[3, 3, 3, 3, 3, 0, 0],
[4, 4, 4, 4, 0, 0, 0],
[5, 5, 5, 0, 0, 0, 0],
[6, 6, 0, 0, 0, 0, 0],
[7, 0, 0, 0, 0, 0, 0]])

我们看到这个函数的作用就是在每一批数据的后面进行补0,直到和最长序列长度相同,我们引入如下代码:

这样做的主要目的是为了让 DataLoader 可以返回 batch,因为 batch 是一个高维的 tensor,其中每个元素的数据必须长度相同。
为了证明DataLoader中一定是同一维度的数据:

import torch
from torch import nn
import torch.nn.utils.rnn as rnn_utils
from torch.utils.data import DataLoader
import torch.utils.data as data

train_x = [torch.tensor([1, 1, 1, 1, 1, 1, 1]),
           torch.tensor([2, 2, 2, 2, 2, 2]),
           torch.tensor([3, 3, 3, 3, 3]),
           torch.tensor([4, 4, 4, 4]),
           torch.tensor([5, 5, 5]),
           torch.tensor([6, 6]),
           torch.tensor([7])]

x = rnn_utils.pad_sequence(train_x, batch_first=True)

class MyData(data.Dataset):
    def __init__(self, data_seq):
        self.data_seq = data_seq

    def __len__(self):
        return len(self.data_seq)

    def __getitem__(self, idx):
        return self.data_seq[idx]


if __name__=='__main__':
    data = MyData(train_x)
    data_loader = DataLoader(data, batch_size=2, shuffle=True)
    batch_x = iter(data_loader).next()
    print(batch_x)
    print('END')

函数的具体做法就是将我们的数据打包放入DataLoader中,然后进行按批次迭代输出
运行报错:

RuntimeError: invalid argument 0: Sizes of tensors must match except in dimension
0.Got 3 and 7 in dimension 1 at

我们看到原因是:Sizes of tensors must match except in dimension
所以输入进DataLoader中的数据必须是整齐的

**具体要怎么做呢?

我们注意到DataLoader中有个参数 collate_fn,专门用来把 Dataset 类的返回值拼接成
tensor,我们不设置的时候,会调用 default 的函数,这次我们的训练数据长度不一,default 函数就 hold
不住了,因此我们要自定义一个 collate_fn,并在 DataLoader 中设置这个参数,再运行就不会报错了(注意代码中对 data
先按照长度降序排列了一下,后面会讲到原因)。

def collate_fn(data):
    data.sort(key=lambda x: len(x), reverse=True)
    data = rnn_utils.pad_sequence(data, batch_first=True, padding_value=0)
    return data

if __name__=='__main__':
    data = MyData(train_x)
    data_loader = DataLoader(data, batch_size=3, shuffle=True, 
                             collate_fn=collate_fn)
    batch_x = iter(data_loader).next()
    print(batch_x)
    print('END')

输出结果:

tensor([[4, 4, 4, 4],
[5, 5, 5, 0],
[6, 6, 0, 0]])
END

结果解释:
我们定义了collate_fn函数,因此DataLoader会将每一批次的数据都经历一次这个函数,然后再进行输出,经历这个函数就是将每一批次的数据补0补充到最大长度,因此出现了5后面补一个0,6后面补两个0的情况,引入4正好是4个数

2、pack_padded_sequence

我们通过 pad_sequence 得到了 padded_sequence,那么直接扔进 RNN 训练不就完了吗?为啥还要用 pack_padded_sequence?这个 pack 又是什么意思呢?
我们回忆一下 RNN 是如何训练的,首先考虑单个训练数据,也就是batch_size=1 的情况:每次网络吃进一个 time step 的数据+该数据对应的 hidden state,然后输出,再继续吃进去第二个 time step 的数据 + hidden state,再输出,以此类推;如果换成 mini-batch 的训练模式则是:网络每次吃进去一组同样 time step 的数据,也就是mini-batch 中所有 sequence 中相同下标的数据,加上它们对应的 hidden state,获得一个 mini-batch 的输出,然后再移到下一个 time step,再读入 mini-batch 中所有该 time step 的数据,再输出……
因此,以上面 pad_sequence的输出为例,数据将会按照如图所示的方式读取:
在这里插入图片描述
网络读取数据的顺序是:[1, 3, 6],[1, 3, 6],[1, 3, 0],[1, 3, 0],[1, 3, 0],[1, 0, 0],[1, 0, 0]。而该 mini-batch 中的 0 是没有意义的 padding,只是为了用来让它和最长的数据对齐而已,显然这种做法浪费了大量计算资源。因此,我们将用到 pack_padded_sequence 。即,不光要 padd,还要 pack。

pack_padded_sequence 有三个参数:input, lengths, batch_first 。input 是上一步加过 padding 的数据,lengths 是各个 sequence 的实际长度,batch_first是数据各个 dimension 按照 [batch_size, sequence_length, data_dim]顺序排列。

如batch_x为如下序列:

batch_x
Out[2]:
tensor([[1, 1, 1, 1, 1, 1, 1],
[3, 3, 3, 3, 3, 0, 0],
[6, 6, 0, 0, 0, 0, 0]])

我们设置的length应该是:lengths=[7, 5, 2]

因此,我们将shuffle设置为不打乱顺序,输出前三个批次,然后pack看一下效果:

if __name__=='__main__':
    data = MyData(train_x)
    data_loader = DataLoader(data, batch_size=3, shuffle=False,
                             collate_fn=collate_fn)
    batch_x = iter(data_loader).next()
    batch_x = rnn_utils.pack_padded_sequence(batch_x,[7,6,5],batch_first=True)
    print(batch_x)
    print('END')

输出:

PackedSequence(data=tensor([1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 1]), batch_sizes=tensor([3, 3, 3, 3, 3, 2, 1]), sorted_indices=None, unsorted_indices=None)
END

我们发现,它的输出有两部分,分别是 data 和 batch_sizes,第一部分为原来的数据按照 time step 重新排列,而 padding 的部分,直接空过了。batch_sizes则是每次实际读入的数据量,也就是说,RNN 把一个 mini-batch sequence 又重新划分为了很多小的 batch,每个小 batch 为所有 sequence 在当前 time step 对应的值,如果某 sequence 在当前 time step 已经没有值了,那么,就不再读入填充的 0,而是降低 batch_size。
batch_size相当于是对训练数据的重新划分。这也是为什么前面在 collate_fn中我们要对 mini-batch 中的 sequence 按照长度降序排列,是为了方便我们取每个 time step 的batch,防止中间夹杂着 padding。

而每个 mini-batch 中 sequence 的真实 length 又如何获得呢?这就要重新修改
collate_fn了,我们在其中加入data_length=[len(sq) for sq in data] 修改后的代码如下:

def collate_fn(data):
    data.sort(key=lambda x: len(x), reverse=True)
    data_length = [len(sq) for sq in data]
    data = rnn_utils.pad_sequence(data, batch_first=True, padding_value=0)
    return data,data_length

if __name__=='__main__':
    data = MyData(train_x)
    data_loader = DataLoader(data, batch_size=3, shuffle=False,
                             collate_fn=collate_fn)
    batch_x,data_length = iter(data_loader).next()
    batch_x = rnn_utils.pack_padded_sequence(batch_x,[7,6,5],batch_first=True)
    print(batch_x)
    print(data_length)
    print('END')

输出结果:

PackedSequence(data=tensor([1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 1]), batch_sizes=tensor([3, 3, 3, 3, 3, 2, 1]), sorted_indices=None, unsorted_indices=None)
[7, 6, 5]
END

3、pad_packed_sequence

一看名字就知道,这个函数和前面的函数是一对。有点像西游记里的奔波儿灞和灞波儿奔。
上文的例子中,我们为了直观,没有考虑到 RNN 对数据维度的要求,因此在这里我们要重新改写 collate_fn使其返回的数据符合 [batch, sequence_len, input_size]的格式(我们设置网络为 batch_first的模式,更符合习惯)。在例子中,每个 sequence 的元素维度都是1,因此只需要在 tensor 末尾加一维就好了,即对返回的数据 unsqueeze(-1) 一下(也可以在数据库的类中,对 _getitem_的返回值 unsqueeze)。

总结:
(1)我们之前并没有考虑到RNN的输入格式,需要转变格式为[batch, sequence_len, input_size],即batch_first = True
(2)因为我们之前的数据并没有input_size这一项,所以直接在最后加一项1即可,即使用unsqueeze进行加一列

代码:

def collate_fn(data):
    data.sort(key=lambda x: len(x), reverse=True)
    data_length = [len(sq) for sq in data]
    data = rnn_utils.pad_sequence(data, batch_first=True, padding_value=0)
    # print(data)
    return data.unsqueeze(-1),data_length

if __name__=='__main__':
    data = MyData(train_x)
    data_loader = DataLoader(data, batch_size=3, shuffle=False,
                             collate_fn=collate_fn)
    batch_x,data_length = iter(data_loader).next()
    batch_x = rnn_utils.pack_padded_sequence(batch_x,[7,6,5],batch_first=True)
    print(batch_x)
    print(data_length)
    print('END')

PackedSequence(data=tensor([[1],
[2],
[3],
[1],
[2],
[3],
[1],
[2],
[3],
[1],
[2],
[3],
[1],
[2],
[3],
[1],
[2],
[1]]), batch_sizes=tensor([3, 3, 3, 3, 3, 2, 1]), sorted_indices=None, unsorted_indices=None)
END

相当于输出:1,2,3 1,2,3。。。。

接下来,我们随机初始化 hidden state 和 cell state (维度为:num_layers * num_directions, batch, hidden_size), 和batch_x_pack一起送入LSTM中。

if __name__=='__main__':
    data = MyData(train_x)
    data_loader = DataLoader(data, batch_size=3, shuffle=True,
                             collate_fn=collate_fn)
    batch_x, batch_x_len = iter(data_loader).next()
    print(batch_x)
    batch_x_pack = rnn_utils.pack_padded_sequence(batch_x,
                                                  batch_x_len, batch_first=True)

    net = nn.LSTM(1, 10, 2, batch_first=True)
    h0 = torch.rand(2, 3, 10)
    #随机初始化h0,在2~3之间,初始10个数
    c0 = torch.rand(2, 3, 10)
    out, (h1, c1) = net(batch_x_pack, (h0, c0))

其中 LSTM 输入为 1 维,hidden size 为 10 ,总共两层。经过一次前向传播,我们得到 out。out 和 batch_x_pack一样,分为两部分: data 和 batch_sizes。观察一下它这两部分:

   print(out.data.shape)
    print(batch_x_pack.data.shape)
    print(out.batch_sizes)
    print(batch_x_pack.batch_sizes)
    print('END')

结果:

tensor([[[3.],
[3.],
[3.],
[3.],
[3.]],
[[6.],
[6.],
[0.],
[0.],
[0.]],
[[7.],
[0.],
[0.],
[0.],
[0.]]])
torch.Size([8, 10])
torch.Size([8, 1])
tensor([3, 2, 1, 1, 1])
tensor([3, 2, 1, 1, 1])

说明:往模型里面喂的数据是:[33333],[66],[7] size=[81]
经过一层后数据变为[8
10]
输入的批次是[3(3,6,7),2(2,6),1(7),…]
经过一层后批次没变,还是这么输入的

输入的 mini-batch 中,统计所有 time step 共有 14 个非零的数据,而 LSTM 的 hidden unit 有10维,故 out.data.shape为 torch.Size([14, 10])。而out.batch_sizes则和 batch_x_pack.batch_sizes相同,都是 tensor([3, 3, 2, 2, 2, 1, 1])。

pad_packed_sequence 执行的是 pack_padded_sequence 的逆操作,执行下面的代码,观察输出。

print("outpad:")
    out_pad, out_len = rnn_utils.pad_packed_sequence(out, batch_first=True)
    print(out_pad.shape)
    print("out")
    print(out.data.shape)
    print(out_len)

结果:

tensor([[[4.],
[4.],
[4.],
[4.]],
[[5.],
[5.],
[5.],
[0.]],
[[7.],
[0.],
[0.],
[0.]]])

outpad:
torch.Size([3, 4, 10])
out
torch.Size([8, 10])
tensor([4, 3, 1])

解释:
可以看出pad_packed_sequence将数据解压缩了
原数据:[4444],[555],[7]
本来数据隐藏层是810经过解压缩,变成了34*10(末尾加0)
tensor三维也变成了[4,3,1](末尾不加0)

我们发现,经过这样的操作后out_pad 形状变成了[3, 7, 10],仿佛我们直接输入加了padding 的 mini-batch ,mini-batch 中有 3 个 sequence,每个 sequence 有 7 个 time step,每个 time step 数据从输入的 1 维,映射成 LSTM 的 10 维,此外它还输出了 out_len,为 [7, 5, 2],即每个 sequence 的真实长度。 为了放心,我们再看一下out_pad[1]是什么:

tensor([[[3.],
         [3.],
         [3.],
         [3.],
         [3.]],

        [[4.],
         [4.],
         [4.],
         [4.],
         [0.]],

        [[6.],
         [6.],
         [0.],
         [0.],
         [0.]]])
tensor([[ 0.0637,  0.0351,  0.0518,  0.0909, -0.0736,  0.1846,  0.1586,  0.1439,
          0.1500,  0.1125],
        [ 0.0596,  0.0215, -0.0283,  0.0996, -0.2295,  0.2079,  0.1067,  0.0707,
          0.0809,  0.1006],
        [ 0.0633,  0.0118, -0.0592,  0.1024, -0.3106,  0.1751,  0.0915,  0.0189,
          0.0553,  0.1168],
        [ 0.0670,  0.0054, -0.0690,  0.1071, -0.3524,  0.1528,  0.0838, -0.0115,
          0.0442,  0.1351],
        [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000,  0.0000,  0.0000,  0.0000,
          0.0000,  0.0000]], grad_fn=<SelectBackward>)

Process finished with exit code 0

out_pad[1]实质上是4的维度上的[7,10],所以最后一行为全0,说明4仅在最后补了0

3、整体代码

import torch
from torch import nn
import torch.nn.utils.rnn as rnn_utils
from torch.utils.data import DataLoader
import torch.utils.data as data

train_x = [torch.Tensor([1, 1, 1, 1, 1, 1, 1]),
           torch.Tensor([2, 2, 2, 2, 2, 2]),
           torch.Tensor([3, 3, 3, 3, 3]),
           torch.Tensor([4, 4, 4, 4]),
           torch.Tensor([5, 5, 5]),
           torch.Tensor([6, 6]),
           torch.Tensor([7])
           ]

x = rnn_utils.pad_sequence(train_x, batch_first=True)


class MyData(data.Dataset):
    def __init__(self, data_seq):
        self.data_seq = data_seq

    def __len__(self):
        return len(self.data_seq)

    def __getitem__(self, idx):
        return self.data_seq[idx]


def collate_fn(data):
    data.sort(key=lambda x: len(x), reverse=True)
    data_length = [len(sq) for sq in data]
    data = rnn_utils.pad_sequence(data, batch_first=True, padding_value=0)
    return data.unsqueeze(-1), data_length


if __name__=='__main__':
    data = MyData(train_x)
    data_loader = DataLoader(data, batch_size=3, shuffle=True,
                             collate_fn=collate_fn)
    batch_x, batch_x_len = iter(data_loader).next()
    batch_x_pack = rnn_utils.pack_padded_sequence(batch_x,
                                                  batch_x_len, batch_first=True)

    net = nn.LSTM(1, 10, 2, batch_first=True)
    h0 = torch.rand(2, 3, 10)
    c0 = torch.rand(2, 3, 10)
    out, (h1, c1) = net(batch_x_pack, (h0, c0))
    out_pad, out_len = rnn_utils.pad_packed_sequence(out, batch_first=True)
    print('END')

4、总结

上面三个函数相互配合,可以在 sequence 长度变化时,成批读入数据,训练 RNN。第一个函数用于给 mini-batch 中的数据加 padding,让 mini-batch 中所有 sequence 的长度等于该 mini-batch 中最长的那个 sequence 的长度。
第二、三个函数,用于提高效率,避免 LSTM 前向传播时,把加入在训练数据中的 padding 考虑进去。因此第二、三个函数理论上可以不用,但为了提高效率最好还是用。
除此之外,本文还介绍了 DataLoader的collate_fn参数,用于把 Dataset类的 getitem 方法的返回的 batchsize 个值拼接成一个 tensor。


本文链接: http://www.dtmao.cc/news_show_2000053.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?