赛尔笔记 | BiLSTM介绍及代码实现

2018 年 10 月 24 日 哈工大SCIR

作者:哈工大SCIR硕士生吴洋

代码:哈工大SCIR博士生袁建华


各位读者,大家好。自即日起,我中心公众号(哈工大SCIR)将推出“赛尔笔记”栏目。


该栏目将每月推送一期由师生精心准备的学习笔记。赛尔笔记主要内容是分享论文写作、阅读及PPT制作、演讲经验及技巧,常用模型如RNN, LSTM, GAN和RL等学习笔记。

一、介绍

1.1 文章组织

本文简要介绍了BiLSTM的基本原理,并以句子级情感分类任务为例介绍为什么需要使用LSTM或BiLSTM进行建模。在文章的最后,我们给出在PyTorch下BiLSTM的实现代码,供读者参考。

1.2 情感分类任务

自然语言处理中情感分类任务是对给定文本进行情感倾向分类的任务,粗略来看可以认为其是分类任务中的一类。对于情感分类任务,目前通常的做法是先对词或者短语进行表示,再通过某种组合方式把句子中词的表示组合成句子的表示。最后,利用句子的表示对句子进行情感分类。


举一个对句子进行褒贬二分类的例子。

句子:我爱赛尔

情感标签:褒义

1.3 什么是LSTM和BiLSTM?

LSTM的全称是Long Short-Term Memory,它是RNN(Recurrent Neural Network)的一种。LSTM由于其设计的特点,非常适合用于对时序数据的建模,如文本数据。BiLSTM是Bi-directional Long Short-Term Memory的缩写,是由前向LSTM与后向LSTM组合而成。两者在自然语言处理任务中都被用来建模上下文信息。

1.4 为什么使用LSTM与BiLSTM?

将词的表示组合成句子的表示,可以采用相加的方法,即将所有词的表示进行加和,或者取平均等方法,但是这些方法没有考虑到词语在句子中前后顺序。如句子“我不觉得他好”。“不”字是对后面“好”的否定,即该句子的情感极性是贬义。使用LSTM模型可以更好的捕捉到较长距离的依赖关系。因为LSTM通过训练过程可以学到记忆哪些信息和遗忘哪些信息。


但是利用LSTM对句子进行建模还存在一个问题:无法编码从后到前的信息。在更细粒度的分类时,如对于强程度的褒义、弱程度的褒义、中性、弱程度的贬义、强程度的贬义的五分类任务需要注意情感词、程度词、否定词之间的交互。举一个例子,“这个餐厅脏得不行,没有隔壁好”,这里的“不行”是对“脏”的程度的一种修饰,通过BiLSTM可以更好的捕捉双向的语义依赖。

二、BiLSTM原理简介

2.1 LSTM介绍

2.1.1 总体框架

LSTM模型是由时刻的输入词,细胞状态 ,临时细胞状态,隐层状态,遗忘门,记忆门,输出门组成。LSTM的计算过程可以概括为,通过对细胞状态中信息遗忘和记忆新的信息使得对后续时刻计算有用的信息得以传递,而无用的信息被丢弃,并在每个时间步都会输出隐层状态,其中遗忘,记忆与输出由通过上个时刻的隐层状态和当前输入计算出来的遗忘门,记忆门,输出门来控制。


总体框架如图1所示。


图1. LSTM总体框架

2.1.2 详细介绍计算过程

计算遗忘门,选择要遗忘的信息。

输入:前一时刻的隐层状态,当前时刻的输入词

输出:遗忘门的值


图2. 计算遗忘门


计算记忆门,选择要记忆的信息。

输入:前一时刻的隐层状态,当前时刻的输入词

输出:记忆门的值临时细胞状态

图3. 计算记忆门和临时细胞状态


计算当前时刻细胞状态

输入:记忆门的值,遗忘门的值临时细胞状态,上一刻细胞状态

输出:当前时刻细胞状态

图4. 计算当前时刻细胞状态


计算输出门和当前时刻隐层状态

输入:前一时刻的隐层状态,当前时刻的输入词 ,当前时刻细胞状态

输出:输出门的值,隐层状态

图5. 计算输出门和当前时刻隐层状态


最终,我们可以得到与句子长度相同的隐层状态序列{, ..., }。

2.2 BiLSTM介绍

前向的LSTM与后向的LSTM结合成BiLSTM。比如,我们对“我爱中国”这句话进行编码,模型如图6所示。


图6. 双向LSTM编码句子


前向的依次输入“我”,“爱”,“中国”得到三个向量{}。后向的依次输入“中国”,“爱”,“我”得到三个向量{}。最后将前向和后向的隐向量进行拼接得到{[], [], []},即{}。


对于情感分类任务来说,我们采用的句子的表示往往是[]。因为其包含了前向与后向的所有信息,如图7所示。

图7. 拼接向量用于情感分类


三、BiLSTM代码实现样例

3.1 模型搭建

使用PyTorch搭建BiLSTM样例代码。代码地址为https://github.com/albertwy/BiLSTM/。


  
  
    
  1. class BLSTM(nn.Module):

  2.    """

  3.        Implementation of BLSTM Concatenation for sentiment classification task

  4.    """

  5.    def __init__(self, embeddings, input_dim, hidden_dim, num_layers, output_dim, max_len=40, dropout=0.5):

  6.        super(BLSTM, self).__init__()

  7.        self.emb = nn.Embedding(num_embeddings=embeddings.size(0),

  8.                                embedding_dim=embeddings.size(1),

  9.                                padding_idx=0)

  10.        self.emb.weight = nn.Parameter(embeddings)

  11.        self.input_dim = input_dim

  12.        self.hidden_dim = hidden_dim

  13.        self.output_dim = output_dim

  14.        # sen encoder

  15.        self.sen_len = max_len

  16.        self.sen_rnn = nn.LSTM(input_size=input_dim,

  17.                               hidden_size=hidden_dim,

  18.                               num_layers=num_layers,

  19.                               dropout=dropout,

  20.                               batch_first=True,

  21.                               bidirectional=True)

  22.        self.output = nn.Linear(2 * self.hidden_dim, output_dim)

  23.    def bi_fetch(self, rnn_outs, seq_lengths, batch_size, max_len):

  24.        rnn_outs = rnn_outs.view(batch_size, max_len, 2, -1)

  25.        # (batch_size, max_len, 1, -1)

  26.        fw_out = torch.index_select(rnn_outs, 2, Variable(torch.LongTensor([0])).cuda())

  27.        fw_out = fw_out.view(batch_size * max_len, -1)

  28.        bw_out = torch.index_select(rnn_outs, 2, Variable(torch.LongTensor([1])).cuda())

  29.        bw_out = bw_out.view(batch_size * max_len, -1)

  30.        batch_range = Variable(torch.LongTensor(range(batch_size))).cuda() * max_len

  31.        batch_zeros = Variable(torch.zeros(batch_size).long()).cuda()

  32.        fw_index = batch_range + seq_lengths.view(batch_size) - 1

  33.        fw_out = torch.index_select(fw_out, 0, fw_index)  # (batch_size, hid)

  34.        bw_index = batch_range + batch_zeros

  35.        bw_out = torch.index_select(bw_out, 0, bw_index)

  36.        outs = torch.cat([fw_out, bw_out], dim=1)

  37.        return outs

  38.    def forward(self, sen_batch, sen_lengths, sen_mask_matrix):

  39.        """

  40.        :param sen_batch: (batch, sen_length), tensor for sentence sequence

  41.        :param sen_lengths:

  42.        :param sen_mask_matrix:

  43.        :return:

  44.        """

  45.        ''' Embedding Layer | Padding | Sequence_length 40'''

  46.        sen_batch = self.emb(sen_batch)

  47.        batch_size = len(sen_batch)

  48.        ''' Bi-LSTM Computation '''

  49.        sen_outs, _ = self.sen_rnn(sen_batch.view(batch_size, -1, self.input_dim))

  50.        sen_rnn = sen_outs.contiguous().view(batch_size, -1, 2 * self.hidden_dim)  # (batch, sen_len, 2*hid)

  51.        ''' Fetch the truly last hidden layer of both sides

  52.        '''

  53.        sentence_batch = self.bi_fetch(sen_rnn, sen_lengths, batch_size, self.sen_len)  # (batch_size, 2*hid)

  54.        representation = sentence_batch

  55.        out = self.output(representation)

  56.        out_prob = F.softmax(out.view(batch_size, -1))

  57.        return out_prob


__init__()函数中对网络进行初始化,设定词向量维度,前向/后向LSTM中隐层向量的维度,还有要分类的类别数等。


bi_fetch()函数的作用是将拼接起来并返回拼接后的向量。由于使用了batch,所以需要使用句子长度用来定位开始padding时前一个时刻的输出的隐层向量。


forward()函数里进行前向计算,得到各个类别的概率值。

3.2 模型训练

  
  
    
  1. def train(model, training_data, args, optimizer, criterion):

  2.    model.train()

  3.    batch_size = args.batch_size

  4.    sentences, sentences_seqlen, sentences_mask, labels = training_data

  5.    # print batch_size, len(sentences), len(labels)

  6.    assert batch_size == len(sentences) == len(labels)

  7.    ''' Prepare data and prediction'''

  8.    sentences_, sentences_seqlen_, sentences_mask_ = \

  9.        var_batch(args, batch_size, sentences, sentences_seqlen, sentences_mask)

  10.    labels_ = Variable(torch.LongTensor(labels))

  11.    if args.cuda:

  12.        labels_ = labels_.cuda()

  13.    assert len(sentences) == len(labels)

  14.    model.zero_grad()

  15.    probs = model(sentences_, sentences_seqlen_, sentences_mask_)

  16.    loss = criterion(probs.view(len(labels_), -1), labels_)

  17.    loss.backward()

  18.    optimizer.step()


代码中training_data是一个batch的数据,其中包括输入的句子sentences(句子中每个词以词下标表示),输入句子的长度sentences_seqlen,输入的句子对应的情感类别labels。 训练模型前,先清空遗留的梯度值,再根据该batch数据计算出来的梯度进行更新模型。


  
  
    
  1.    model.zero_grad()

  2.    probs = model(sentences_, sentences_seqlen_, sentences_mask_)

  3.    loss = criterion(probs.view(len(labels_), -1), labels_)

  4.    loss.backward()

  5.    optimizer.step()


3.3 模型测试

以下是进行模型测试的代码。


  
  
    
  1. def test(model, dataset, args, data_part="test"):

  2.    """

  3.    :param model:

  4.    :param args:

  5.    :param dataset:

  6.    :param data_part:

  7.    :return:

  8.    """

  9.    tvt_set = dataset[data_part]

  10.    tvt_set = yutils.YDataset(tvt_set["xIndexes"],

  11.                              tvt_set["yLabels"],

  12.                              to_pad=True, max_len=args.sen_max_len)

  13.    test_set = tvt_set

  14.    sentences, sentences_seqlen, sentences_mask, labels = test_set.next_batch(len(test_set))

  15.    assert len(test_set) == len(sentences) == len(labels)

  16.    tic = time.time()

  17.    model.eval()

  18.    ''' Prepare data and prediction'''

  19.    batch_size = len(sentences)

  20.    sentences_, sentences_seqlen_, sentences_mask_ = \

  21.        var_batch(args, batch_size, sentences, sentences_seqlen, sentences_mask)

  22.    probs = model(sentences_, sentences_seqlen_, sentences_mask_)

  23.    _, pred = torch.max(probs, dim=1)

  24.    if args.cuda:

  25.        pred = pred.view(-1).cpu().data.numpy()

  26.    else:

  27.        pred = pred.view(-1).data.numpy()

  28.    tit = time.time() - tic

  29.    print "  Predicting {:d} examples using {:5.4f} seconds".format(len(test_set), tit)

  30.    labels = numpy.asarray(labels)

  31.    ''' log and return prf scores '''

  32.    accuracy = test_prf(pred, labels)

  33.    return accuracy

  
  
    
  1. def cal_prf(pred, right, gold, formation=True, metric_type=""):

  2.    """

  3.    :param pred: predicted labels

  4.    :param right: predicting right labels

  5.    :param gold: gold labels

  6.    :param formation: whether format the float to 6 digits

  7.    :param metric_type:

  8.    :return: prf for each label

  9.    """

  10.    num_class = len(pred)

  11.    precision = [0.0] * num_class

  12.    recall = [0.0] * num_class

  13.    f1_score = [0.0] * num_class

  14.    for i in xrange(num_class):

  15.        ''' cal precision for each class: right / predict '''

  16.        precision[i] = 0 if pred[i] == 0 else 1.0 * right[i] / pred[i]

  17.        ''' cal recall for each class: right / gold '''

  18.        recall[i] = 0 if gold[i] == 0 else 1.0 * right[i] / gold[i]

  19.        ''' cal recall for each class: 2 pr / (p+r) '''

  20.        f1_score[i] = 0 if precision[i] == 0 or recall[i] == 0 \

  21.            else 2.0 * (precision[i] * recall[i]) / (precision[i] + recall[i])

  22.        if formation:

  23.            precision[i] = precision[i].__format__(".6f")

  24.            recall[i] = recall[i].__format__(".6f")

  25.            f1_score[i] = f1_score[i].__format__(".6f")

  26.    ''' PRF for each label or PRF for all labels '''

  27.    if metric_type == "macro":

  28.        precision = sum(precision) / len(precision)

  29.        recall = sum(recall) / len(recall)

  30.        f1_score = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0

  31.    elif metric_type == "micro":

  32.        precision = 1.0 * sum(right) / sum(pred) if sum(pred) > 0 else 0

  33.        recall = 1.0 * sum(right) / sum(gold) if sum(recall) > 0 else 0

  34.        f1_score = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0

  35.    return precision, recall, f1_score

四、总结

本文中,我们结合情感分类任务介绍了LSTM以及BiLSTM的基本原理,并给出一个BiLSTM样例代码。除了情感分类任务,LSTM与BiLSTM在自然语言处理领域的其它任务上也得到了广泛应用,如机器翻译任务中使用其进行源语言的编码和目标语言的解码,机器阅读理解任务中使用其对文章和问题的编码等。

五、参考资料

http://colah.github.io/posts/2015-08-Understanding-LSTMs/


本期责任编辑:崔一鸣

本期编辑:刘元兴



“哈工大SCIR”公众号

主编:车万翔

副主编: 张伟男,丁效

责任编辑: 张伟男,丁效,刘一佳,崔一鸣

编辑: 李家琦,吴洋,刘元兴,蔡碧波,孙卓,赖勇魁


长按下图并点击 “识别图中二维码”,即可关注哈尔滨工业大学社会计算与信息检索研究中心微信公共号:”哈工大SCIR” 。

登录查看更多
23

相关内容

BiLSTM是Bi-directional Long Short-Term Memory的缩写,是由前向LSTM与后向LSTM组合而成。在自然语言处理任务中都常被用来建模上下文信息。
【ICML2020-西电】用于语言生成的递归层次主题引导RNN
专知会员服务
21+阅读 · 2020年6月30日
【实用书】学习用Python编写代码进行数据分析,103页pdf
专知会员服务
190+阅读 · 2020年6月29日
《代码整洁之道》:5大基本要点
专知会员服务
49+阅读 · 2020年3月3日
Transformer文本分类代码
专知会员服务
116+阅读 · 2020年2月3日
【MIT深度学习课程】深度序列建模,Deep Sequence Modeling
专知会员服务
76+阅读 · 2020年2月3日
六篇 EMNLP 2019【图神经网络(GNN)+NLP】相关论文
专知会员服务
71+阅读 · 2019年11月3日
注意力机制介绍,Attention Mechanism
专知会员服务
165+阅读 · 2019年10月13日
计算机视觉最佳实践、代码示例和相关文档
专知会员服务
17+阅读 · 2019年10月9日
赛尔笔记 | Attention!注意力机制可解释吗?
哈工大SCIR
22+阅读 · 2019年9月27日
【论文笔记】基于强化学习的句子摘要排序
【论文笔记】基于LSTM的问答对排序
专知
12+阅读 · 2019年9月7日
R语言自然语言处理:情感分析
R语言中文社区
16+阅读 · 2019年4月16日
赛尔笔记 | CNN介绍及代码实现
哈工大SCIR
7+阅读 · 2019年1月23日
赛尔推荐 | 第9期
哈工大SCIR
13+阅读 · 2018年4月20日
基础 | 基于注意力机制的seq2seq网络
黑龙江大学自然语言处理实验室
16+阅读 · 2018年3月7日
专栏 | Bi-LSTM+CRF在文本序列标注中的应用
机器之心
14+阅读 · 2018年1月3日
循环神经网络的介绍、代码及实现
AI研习社
3+阅读 · 2017年11月21日
Arxiv
5+阅读 · 2018年5月10日
Arxiv
15+阅读 · 2018年2月4日
Arxiv
4+阅读 · 2018年1月29日
VIP会员
相关VIP内容
【ICML2020-西电】用于语言生成的递归层次主题引导RNN
专知会员服务
21+阅读 · 2020年6月30日
【实用书】学习用Python编写代码进行数据分析,103页pdf
专知会员服务
190+阅读 · 2020年6月29日
《代码整洁之道》:5大基本要点
专知会员服务
49+阅读 · 2020年3月3日
Transformer文本分类代码
专知会员服务
116+阅读 · 2020年2月3日
【MIT深度学习课程】深度序列建模,Deep Sequence Modeling
专知会员服务
76+阅读 · 2020年2月3日
六篇 EMNLP 2019【图神经网络(GNN)+NLP】相关论文
专知会员服务
71+阅读 · 2019年11月3日
注意力机制介绍,Attention Mechanism
专知会员服务
165+阅读 · 2019年10月13日
计算机视觉最佳实践、代码示例和相关文档
专知会员服务
17+阅读 · 2019年10月9日
相关资讯
赛尔笔记 | Attention!注意力机制可解释吗?
哈工大SCIR
22+阅读 · 2019年9月27日
【论文笔记】基于强化学习的句子摘要排序
【论文笔记】基于LSTM的问答对排序
专知
12+阅读 · 2019年9月7日
R语言自然语言处理:情感分析
R语言中文社区
16+阅读 · 2019年4月16日
赛尔笔记 | CNN介绍及代码实现
哈工大SCIR
7+阅读 · 2019年1月23日
赛尔推荐 | 第9期
哈工大SCIR
13+阅读 · 2018年4月20日
基础 | 基于注意力机制的seq2seq网络
黑龙江大学自然语言处理实验室
16+阅读 · 2018年3月7日
专栏 | Bi-LSTM+CRF在文本序列标注中的应用
机器之心
14+阅读 · 2018年1月3日
循环神经网络的介绍、代码及实现
AI研习社
3+阅读 · 2017年11月21日
Top
微信扫码咨询专知VIP会员