使用 PyTorch RNN 进行文本分类

本文描述如何使用 使用 PyTorch RNN 进行文本分类。

考虑一个客户服务满意度的场景,客户和客服人员进行会话,会话结束后,自动将会话按照客户满意度进行分类。分类结果可以用0-5来表达,0表示最不满意,5表示最满意。

在执行学习任务之前,需要对训练数据进行标注。将对话内容打上满意度标签。最满意是5颗星,最不满意是0颗星。

同时我们还要准备词向量模型,就是能够把每个中文词映射到一个向量上,训练这个词向量模型,是一个需要大量语料的工作,通过观察词语在句子中的上下文的位置,从而确定词语本身的向量。非常遗憾我大中国,没有什么可以使用的开放的中文语料(可悲,可叹,可怜,国家的钱都白花了),我们使用了中文 wiki 提供的“国际援助”。

把对话内容进行分词,然后通过加载词向量模型找到每个词的向量,如果找不到就用一个默认向量替代(惨,说明词向量模型覆盖的不够),然后每个会话就构成了一个二维矩阵,每行是一个词向量,多少行就是这个会话中有多少词,就是所谓的时间序列,因为话是从前往后说。(有倒着说话的吗?倒背如流)

这里使用 GRU,相当于简化的 LSTM 吧,广义上都是 RNN(自己研究吧,真没啥东西,我们只讨论如何正确的使用);一个 RNN 网络实际上就是能够把时间序列数据一个个输入,这里就是一个个词向量进行输入(再加上上下文数据),然后 RNN 网络输出两个值,一个是正常的输出,一个是上下文输出。这里面非常重要的信息是,RNN 输入一定包含两部分,一部分是时间序列中的一个元素;另一部分是上下文。每次 RNN 需要的输入只是时间序列的一个元素,不是整个序列,RNN 可没有这么牛逼(很多讲 RNN 的示例图都画得好像 RNN 能一次吞了整个时间序列数据,给人无限困扰),但是我们在使用 PyTorch 的时候输入数据是整个序列,那是程序抽象出来的方便使用而已。

在计算会话的第一个词向量的时候,是没有上一个词向量的上下文输出,往往会指定一个随机值或者 None作为上下文向量,把第一个词向量和上下文向量提供给 RNN。再进一步,第二个词向量和第一个词向量通过网络计算出来的上下文输出作为 RNN 的网络输入,如此下去,直到把一个对话的词向量都用光;这时候取得最后一个网络输出(注意不是上下文输出),把这个输出做一个跟输出标签层一个全连接的 softmax。这样的 RNN 实际上又加了一个最后的全连接输出层,这个层用来把 RNN 的输出 softmax 化,就是能够把输出变成概率的形式。训练的时候的标签是 one-hot 编码,如 0 颗星就是 【1,0,0,0,0,0】,一颗星就是【0,1,0,0,0,0】,等等,5颗星就是【0,0,0,0,0,1】。0颗星就是在 0 个位置上是 1,5颗星就是在第5个位置上是1。

值得注意的是 RNN 网络的两个输出,一个是个上下文输出,反馈于序列的下一个输入;一个是网络输出,整个序列中只有最后一个网络输出有用处,如果一个会话中有 N 个分词,那么 从 1 到 N-1 的序列产生的网络输出都直接扔掉。只有第 N 个用来做 softmax,然后做损失计算,反向传播计算,调整参数。同样也可以看出来,N 次前向使用 RNN 才反向一次。是不是也还算比较节约的。

这个训练过程有点类似把序列数据进行高密度压缩了,像压缩编码一样。

下面看如何通过 Pytorch 实现:

PyTorch 有提供封装好的 GRU 模型,(当然也有最基础的 RNN 和 LSTM)这个模型提供的便利是,给模型提供输入时候可以一次把完成的时间序列提供,还可以一次提供多个时间序列。输入要求是三个维度,BATCH,TIME SEQUENCE,DATA;BATCH 就是在我们讨论的情景里面就是多少个会话,TIME SEQUENCE 就是一个会话中多少个词向量,DATA 就是词向量。我们经常能看到 很多 RNN 训练中的 DEMO 中提出要把句子补全成一个统一长度,这是完全没必要的,句子多长就告诉 RNN TIME SEQUENCE 就OK,为什么还要统一成一个长度的?画蛇添足?还是我们理解错了?

BATCH 我们都指定是 1,就是并没有一次提供多个会话给 RNN,这样在评估损失的时候貌似好计算一些(别的我们也没有试过,😀)。

在初始化 PyTorch 网络模型的时候可以指定网络输入输出参数中 BATCH 维度的位置,如果不指定 BATCH FIRST
的话,那么数据的第一个维度就是 TIME SEQUENCE,第二个维度才是 BATCH。

PyTorch 官网的的样例中 SEQ2SEQ 用来做翻译,使用了内置的词嵌入构造和查找方法,这样的好处是不用引用第三方词嵌入模型,也不会出现找不到词所对应的词向量的问题,因为这个词向量空间是根据问题域临时定义的,相当小,也正好覆盖问题,但是不足以表达世界的常识。通过翻阅很多 PyTorch 的 GITHUB 项目,我们发现这个例子深深迷惑了众生。PyTorch 样例本意是帮助使用者能够冷启动,不依赖词嵌入模型,但是实际应用中没有词嵌入模型是可能是玩不了的,但是新人很难理解全部概念,如何把样例的词嵌入过程去掉,有很多文章直接把词向量模型直接copy_到 PyTorch 中,这可的确不是一个好办法,貌似重用了原来的代码,但是更容易混淆概念。正确的方法是直接从准备好的词向量模型中查到词向量,把词向量和网络上下文(上一个输出或者随机初始上下文)提供给 RNN。其关键之处在于了解 RNN 需要的输入向量是什么构造,就是前文所指的 BATCH,TIME SEQUENSE 和 DATA,搞清楚了这一点,就不必非要勉强的使用官方的样例程序了。

在使用交叉熵进行损失计算的时候,也有一个值得注意的问题,PyTorch需要的标签值是个标量,它会自动帮你转换成 one-hot 编码。所以训练数据的标签,应该就是 0-5,0表示没有星,5表示5颗星。

根据上面所介绍的思路实现代码:


import torch

class EncoderRNNWithVector(torch.nn.Module):
    def __init__(self, hidden_size, out_size, n_layers=1, batch_size=1):
        super(EncoderRNNWithVector, self).__init__()

        self.batch_size = bactch_size
        self.hidden_size = hidden_size
        self.n_layers = n_layers
        self.out_size = out_size

        # 这里指定了 BATCH FIRST
        self.gru = torch.nn.GRU(hidden_size, hidden_size, n_layers, batch_first=True)

       # 加了一个线性层,全连接
        self.out = torch.nn.Linear(hidden_size, out_size)
        
    def forward(self, word_inputs, hidden):

        # -1 是在其他确定的情况下,PyTorch 能够自动推断出来,view 函数就是在数据不变的情况下重新整理数据维度
        # batch, time_seq, input
        inputs = word_inputs.view(self.batch_size, -1, self.hidden_size)

       # hidden 就是上下文输出,output 就是 RNN 输出
        output, hidden = self.gru(inputs, hidden)

        output = self.out(output)

        # 仅仅获取 time seq 维度中的最后一个向量
        # the last of time_seq
        output = output[:,-1,:]

        return output, hidden

    def init_hidden(self):

        # 这个函数写在这里,有一定迷惑性,这个不是模型的一部分,是每次第一个向量没有上下文,在这里捞一个上下文,仅此而已。
        hidden = torch.autograd.Variable(torch.zeros(self.n_layers, 1, self.hidden_size))
        return hidden

下面这个函数利用上文定义好的 PyTorch 模型进行计算,我们随机生成一些数据和标签。


def _test_rnn_rand_vec():

    import random

    # 这里随机生成一个 Tensor,维度是 1000 x 10 x 200;其实就是1000个句子,每个句子里面有10个词向量,每个词向量 200 维度,其中的值符合 NORMAL 分布。

    _xs = torch.randn(1000, 10, 200)
    _ys = []

# 标签值 0 - 5 闭区间
    for i in range(1000):
        _ys.append(random.randint(0, 5))
    
# 隐层 200,输出 6,隐层用词向量的宽度,输出用标签的值得个数 (one-hot)
    encoder_test = EncoderRNNWithVector(200, 6)

    criterion = torch.nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(encoder_test.parameters(), lr=0.001, momentum=0.9)

    for i in range(_xs.size()[0]):
        
        encoder_hidden = encoder_test.init_hidden()
        
        input_data = torch.autograd.Variable(_xs[i])
        output_labels = torch.autograd.Variable(torch.LongTensor([_ys[i]]))

        #print(output_labels)
        
        encoder_outputs, encoder_hidden = encoder_test(input_data, encoder_hidden)

        optimizer.zero_grad()
        loss = criterion(encoder_outputs, output_labels)
        loss.backward()
        optimizer.step()
        
        print("loss: ", loss.data[0])

    return

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容