写给程序员的机器学习入门 (七) - 双向递归模型 (BRNN) - 根据上下文补全单词(一)


风晓
风晓 2023-12-31 09:57:36 53303 赞同 0 反对 0
分类: 资源
这一篇将会介绍什么是双向递归模型和如何使用双向递归模型实现根据上下文补全句子中的单词。

双向递归模型

到这里为止我们看到的例子都是按原有顺序把输入传给递归模型的,例如传递第一天股价会返回根据第一天股价预测的涨跌,再传递第二天股价会返回根据第一天股价和第二天股价预测的涨跌,以此类推,这样的模型也称单向递归模型。如果我们要根据句子的一部分预测下一个单词,可以像下图这样做,这时 天气 会根据 今天 计算, 很好 会根据 今天 和 天气 计算:

那么如果想要预测在句子中间的单词呢?例如给出 今天 和 很好 预测 天气,因为只能根据前面的单词预测,单向递归模型的效果会打折,这时候双向递归模型就派上用场了。双向递归模型 (BRNN, Bidirectional Recurrent Neural Network) 会先按原有顺序把输入传给递归模型,然后再按反向顺序把输入传给递归模型,然后合并正向输出和反向输出。如下图所示,hf 代表正向输出,hb 代表反向输出,把它们合并到一块就可以实现根据上下文预测中间的内容,今天 会根据反向的 天气 和 很好 计算,天气 会根据正向的 今天 和反向的 很好 计算,很好 会根据正向的 今天 和 天气 计算。

在 pytorch 中使用双向递归模型非常简单,只要在创建的时候传入参数 bidirectional = True 即可:

self.rnn = nn.GRU(
    input_size = 20,
    hidden_size = 50,
    num_layers = 1,
    batch_first = True,
    bidirectional = True
)

单向递归模型会返回维度为 批次大小,输入次数,隐藏值数量 的 tensor,而双向递归模型会返回维度为 批次大小,输入次数,隐藏值数量*2 的 tensor。

你可能还会有疑问,双向递归模型会怎样处理批次呢?如果批次中每组数据的输入次数都不一样,那么反向计算的时候会不会从那些填充的 0 开始计算呢?以下是一个小实验,我们可以看到反向计算的时候 pytorch 会跳过结尾的填充值,不需要做特殊的处理🥳。

>>> import torch
>>> from torch import nn
>>> x = torch.zeros((3, 3, 1))
>>> lengths = torch.tensor([1, 2, 3])
>>> rnn = torch.nn.GRU(input_size=1, hidden_size=1, batch_first=True, bidirectional=True)
>>> packed = nn.utils.rnn.pack_padded_sequence(x, lengths, batch_first=True, enforce_sorted=False)
>>> output, hidden = rnn(packed)
>>> unpacked, _ = torch.nn.utils.rnn.pad_packed_sequence(output, batch_first=True)
>>> unpacked
tensor([[[0.2916, 0.2377],
         [0.0000, 0.0000],
         [0.0000, 0.0000]],

        [[0.2916, 0.2239],
         [0.3949, 0.2377],
         [0.0000, 0.0000]],

        [[0.2916, 0.2243],
         [0.3949, 0.2239],
         [0.4263, 0.2377]]], grad_fn=<IndexSelectBackward>)

此外,如果你想使用双向递归模型来实现分类(例如文本情感分类),那么可以只抽出 (torch.gather) 每组数据的最后一个正向隐藏值和第一个反向隐藏值,然后把它们组合 (torch.cat) 一起传递到多层线性模型,尽管大多数情况下单向递归模型足以实现分类功能。提取组合的代码例子如下 (unpacked 来源于上一个例子):

>>> hidden_size = unpacked.shape[2]//2
>>> forward_last = unpacked[:,:,:hidden_size].gather(1, (lengths - 1).reshape(-1, 1, 1).repeat(1, 1, hidden_size))
>>> forward_last
tensor([[[0.2916]],

        [[0.3949]],

        [[0.4263]]], grad_fn=<GatherBackward>)
>>> backward_first = unpacked[:,:1,hidden_size:]
>>> backward_first
tensor([[[0.2377]],

        [[0.2239]],

        [[0.2243]]], grad_fn=<SliceBackward>)
>>> combined = torch.cat((forward_last, backward_first), dim=2)
>>> combined
tensor([[[0.2916, 0.2377]],

        [[0.3949, 0.2239]],

        [[0.4263, 0.2243]]], grad_fn=<CatBackward>)
>>> combined.shape
torch.Size([3, 1, 2])

例子 - 根据上下文补全单词

还记得我们小学语文做的填空题吗,这回我们试试写一个程序帮我们自动填空吧👦,为了这个例子我消耗了一个多月的时间,走了很多冤枉路,下图是最终使用的训练流程和模型结构:

以下是踩过的坑一览🤕:

  • 输入和输出的编码需要分开 (使用不同的向量)
  • 输入的编码最好不固定 (跟随训练调整),输出的编码需要固定 (否则模型会作弊让所有单词的输出编码相同)
  • 输出的编码只能由 0 和 1 组成,不能直接使用浮点数组成的向量 (模型无法调整所有输出到精确的向量,只能调整到方向大致相同的值然后用 Sigmoid 处理)
  • 输出的编码向量长度大约 50 以上即可避免 13000 个单词转换到 0 和 1 以后的编码冲突,向量长度越大效果越好但需要更多内存和训练时间
  • 每个句子需要添加表示开头和结尾的符号 (<BEG> 与 <EOF>),它们会当作预测第一个单词和最后一个单词的输入,比使用 0 效果要好一些
  • 输出中表示开头和结尾的向量不参与损失的计算 (预测它们没有意义)
  • 根据预测输出的向量查找对应的单词可以计算欧几里得距离,找出距离最接近的单词
  • 参数调整器可以使用 Adam,在这个例子中比 Adadelta 快一些

这个例子最大的特点是输出的编码使用了 Embedding 的变种,使得编码近似于 binary。传统的做法是使用 onehot + softmax,但随着单词数量增多需要的处理时间和内存大小会暴增,我目前的机器是训练不过来的。输出编码使用 Embedding 变种的好处还有可以同时找出接近的单词,但计算欧几里得距离的效率会比 onehot + softmax 直接得出最可能单词索引的时间差很多。

首先我们需要使用 word2vec 生成输出使用的编码,来源是京东商品评论(下载地址请参考上一篇文章),每个单词对应一个长度 100 的向量:

import jieba
f = open('chinese.text8', 'w')
for line in open('goods_zh.txt', 'r'):
    line = "".join(line.split(',')[:-2])
    words = list(jieba.cut(line))
    words = [w for w in words if not (w.isascii() or w in (",", "。", "!"))]
    words.insert(0, "<BEG>")
    words.append("<EOF>")
    f.write(" ".join(words))
    f.write(" ")

import torch
from gensim.models import word2vec
sentences = word2vec.Text8Corpus('chinese.text8')
model = word2vec.Word2Vec(sentences, size=100)

生成编码以后我们需要把编码中的浮点数转换为 0 或者 1,执行以下代码后编码中小于 0 的值会当作 0,大于或等于 0 的值会当作 1:

v = torch.tensor(model.wv.vectors)
v1 = (v > 0).float()
model.wv.vectors = v1.numpy()

然后再来测试一下编码是否有冲突(两个单词对应完全相同的向量),如果它们输出相同那就代表没有问题:

print("wv shape:", v1.shape)
print("wv unique shape:", v1.unique(dim=0).shape)

最后保存编码模型到硬盘:

model.save("chinese.model")

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓L1
粉丝 1 资源 2038 + 关注 私信
最近热门资源
银河麒麟桌面操作系统备份用户数据  124
统信桌面专业版【全盘安装UOS系统】介绍  119
银河麒麟桌面操作系统安装佳能打印机驱动方法  110
银河麒麟桌面操作系统 V10-SP1用户密码修改  104
最近下载排行榜
银河麒麟桌面操作系统备份用户数据 0
统信桌面专业版【全盘安装UOS系统】介绍 0
银河麒麟桌面操作系统安装佳能打印机驱动方法 0
银河麒麟桌面操作系统 V10-SP1用户密码修改 0
作者收入月榜
1

prtyaa 收益393.62元

2

zlj141319 收益218元

3

1843880570 收益214.2元

4

IT-feng 收益209.03元

5

风晓 收益208.24元

6

777 收益172.71元

7

Fhawking 收益106.6元

8

信创来了 收益105.84元

9

克里斯蒂亚诺诺 收益91.08元

10

技术-小陈 收益79.5元

请使用微信扫码

加入交流群

请使用微信扫一扫!