教程 | 利用AllenNLP,百行Python代码训练情感分类器

选自realworldnlpbook

作者:Masato Hagiwara

机器之心编译

参与:Geek AI、路

本文介绍了如何利用 AllenNLP,使用不到一百行代码训练情感分类器。

什么是情感分析?

情感分析是一种流行的文本分析技术,用来对文本中的主观信息进行自动识别和分类。它被广泛用于量化观点、情感等通常以非结构化方式记录的信息,而这些信息也因此很难用其他方式量化。情感分析技术可被用于多种文本资源,例如调查报告、评论、社交媒体上的帖子等。

情感分析最基本的任务之一是极性分类,换句话说,该任务需要判断语言所表达的观点是正面的、负面的还是中性的。具体而言,可能有三个以上的类别,例如:极其正面、正面、中性、消极、极其消极。这有些类似于你使用某些网站时的评价行为(比如 Amazon),人们可以用星星数表示 5 个等级来对物品进行评论(产品、电影或其他任何东西)。

斯坦福的情感分析树库(TreeBank)

目前,研究人员发布了一些公开的情感分类数据集。在本文中,我们将使用斯坦福的情感分析树库(或称 SST),这可能是最广为使用的情感分析数据集之一。SST 与其它数据集最大的不同之处是,在 SST 中情感标签不仅被分配到句子上,句子中的每个短语和单词也会带有情感标签。这使我们能够研究单词和短语之间复杂的语义交互。例如,对下面这个句子的极性进行分析:

This movie was actually neither that funny, nor super witty.

这个句子肯定是消极的。但如果只看单个单词(「funny」、「witty」)可能会被误导,认为它的情感是积极的。只关注单个单词的朴素词袋分类器很难对上面的例句进行正确的分类。要想正确地对上述例句的极性进行分类,你需要理解否定词(neither ... nor ...)对语义的影响。由于 SST 具备这样的特性,它被用作获取句子句法结构的神经网络模型的标准对比基准(https://nlp.stanford.edu/~socherr/EMNLP2013_RNTN.pdf)。

Pytorch 和 AllenNLP

PyTorch 是我最喜欢的深度学习框架。它提供了灵活、易于编写的模块,可动态运行,且速度相当快。在过去一年中,PyTorch 在科研社区中的使用实现了爆炸性增长。

尽管 PyTorch 是一个非常强大的框架,但是自然语言处理往往涉及底层的公式化的事务处理,包括但不限于:阅读和编写数据集、分词、建立单词索引、词汇管理、mini-batch 批处理、排序和填充等。尽管在 NLP 任务中正确地使用这些构建块是至关重要的,但是当你快速迭代时,你需要一次又一次地编写类似的设计模式,这会浪费很多时间。而这正是 AllenNLP 这类库的亮点所在。

AllenNLP 是艾伦人工智能研究院开发的开源 NLP 平台。它的设计初衷是为 NLP 研究和开发(尤其是语义和语言理解任务)的快速迭代提供支持。它提供了灵活的 API、对 NLP 很实用的抽象,以及模块化的实验框架,从而加速 NLP 的研究进展。

本文将向大家介绍如何使用 AllenNLP 一步一步构建自己的情感分类器。由于 AllenNLP 会在后台处理好底层事务,提供训练框架,所以整个脚本只有不到 100 行 Python 代码,你可以很容易地使用其它神经网络架构进行实验。

代码地址:https://github.com/mhagiwara/realworldnlp/blob/master/examples/sentiment/sst_classifier.py

接下来,下载 SST 数据集,你需要将数据集分割成 PTB 树格式的训练集、开发集和测试集,你可以通过下面的链接直接下载:https://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip。我们假设这些文件是在 data/stanfordSentimentTreebank/trees 下进行扩展的。

注意,在下文的代码片段中,我们假设你已经导入了合适的模块、类和方法(详情参见完整脚本)。你会注意到这个脚本和 AllenNLP 的词性标注教程非常相似——在 AllenNLP 中很容易在只进行少量修改的情况下使用不同的模型对不同的任务进行实验。

数据集读取和预处理

AllenNLP 已经提供了一个名为 StanfordSentimentTreeBankDatasetReader 的便捷数据集读取器,它是一个读取 SST 数据集的接口。你可以通过将数据集文件的路径指定为为 read() 方法的参数来读取数据集:

reader = StanfordSentimentTreeBankDatasetReader()

train_dataset = reader.read('data/stanfordSentimentTreebank/trees/train.txt')
dev_dataset = reader.read('data/stanfordSentimentTreebank/trees/dev.txt')

几乎任何基于深度学习的 NLP 模型的第一步都是指定如何将文本数据转换为张量。该工作包括把单词和标签(在本例中指的是「积极」和「消极」这样的极性标签)转换为整型 ID。在 AllenNLP 中,该工作是由 Vocabulary 类来处理的,它存储从单词/标签到 ID 的映射。

# You can optionally specify the minimum count of tokens/labels.
# `min_count={'tokens':3}` here means that any tokens that appear less than three times
# will be ignored and not included in the vocabulary.
vocab = Vocabulary.from_instances(train_dataset + dev_dataset,
                                  min_count={'tokens': 3})

下一步是将单词转换为嵌入。在深度学习中,嵌入是离散、高维数据的连续向量表征。你可以使用 Embedding 创建这样的映射,使用 BasicTextFieldEmbedder 将 ID 转换为嵌入向量。

token_embedding = Embedding(num_embeddings=vocab.get_vocab_size('tokens'),
                            embedding_dim=EMBEDDING_DIM)
# BasicTextFieldEmbedder takes a dict - we need an embedding just for tokens,
# not for labels, which are used unchanged as the answer of the sentence classification
word_embeddings = BasicTextFieldEmbedder({"tokens": token_embedding})

句子分类模型

LSTM-RNN 句子分类模型

现在,我们来定义一个句子分类模型。这段代码看起来很多,但是别担心,我在代码片段中添加了大量注释:

# Model in AllenNLP represents a model that is trained.
class LstmClassifier(Model):
    def __init__(self,
                 word_embeddings: TextFieldEmbedder,
                 encoder: Seq2VecEncoder,
                 vocab: Vocabulary) -> None:
        super().__init__(vocab)
        # We need the embeddings to convert word IDs to their vector representations
        self.word_embeddings = word_embeddings

        # Seq2VecEncoder is a neural network abstraction that takes a sequence of something
        # (usually a sequence of embedded word vectors), processes it, and returns it as a single
        # vector. Oftentimes, this is an RNN-based architecture (e.g., LSTM or GRU), but
        # AllenNLP also supports CNNs and other simple architectures (for example,
        # just averaging over the input vectors).
        self.encoder = encoder

        # After converting a sequence of vectors to a single vector, we feed it into
        # a fully-connected linear layer to reduce the dimension to the total number of labels.
        self.hidden2tag = torch.nn.Linear(in_features=encoder.get_output_dim(),
                                          out_features=vocab.get_vocab_size('labels'))
        self.accuracy = CategoricalAccuracy()

        # We use the cross-entropy loss because this is a classification task.
        # Note that PyTorch's CrossEntropyLoss combines softmax and log likelihood loss,
        # which makes it unnecessary to add a separate softmax layer.
        self.loss_function = torch.nn.CrossEntropyLoss()

    # Instances are fed to forward after batching.
    # Fields are passed through arguments with the same name.
    def forward(self,
                tokens: Dict[str, torch.Tensor],
                label: torch.Tensor = None) -> torch.Tensor:
        # In deep NLP, when sequences of tensors in different lengths are batched together,
        # shorter sequences get padded with zeros to make them of equal length.
        # Masking is the process to ignore extra zeros added by padding
        mask = get_text_field_mask(tokens)

        # Forward pass
        embeddings = self.word_embeddings(tokens)
        encoder_out = self.encoder(embeddings, mask)
        logits = self.hidden2tag(encoder_out)

        # In AllenNLP, the output of forward() is a dictionary.
        # Your output dictionary must contain a "loss" key for your model to be trained.
        output = {"logits": logits}
        if label is not None:
            self.accuracy(logits, label)
            output["loss"] = self.loss_function(logits, label)

        return output

这里的关键是 Seq2VecEncoder,它基本上使用张量序列作为输入,然后返回一个向量。我们在这里使用 LSTM-RNN 作为编码器(如有需要,可参阅文档 https://allenai.github.io/allennlp-docs/api/allennlp.modules.seq2vec_encoders.html#allennlp.modules.seq2vec_encoders.pytorch_seq2vec_wrapper.PytorchSeq2VecWrapper)。

lstm = PytorchSeq2VecWrapper(
    torch.nn.LSTM(EMBEDDING_DIM, HIDDEN_DIM, batch_first=True))

model = LstmClassifier(word_embeddings, lstm, vocab)

训练

一旦你定义了这个模型,其余的训练过程就很容易了。这就是像 AllenNLP 这样的高级框架的亮点所在。你只需要指定如何进行数据迭代并将必要的参数传递给训练器,而无需像 PyTorch 和 TensorFlow 那样编写冗长的批处理和训练循环。

optimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-5)

iterator = BucketIterator(batch_size=32, sorting_keys=[("tokens", "num_tokens")])
iterator.index_with(vocab)

trainer = Trainer(model=model,
                  optimizer=optimizer,
                  iterator=iterator,
                  train_dataset=train_dataset,
                  validation_dataset=dev_dataset,
                  patience=10,
                  num_epochs=20)

trainer.train()

这里的 BucketIterator 会根据 token 的数量对训练实例进行排序,从而使得长度类似的实例在同一个批中。注意,我们使用了验证集,在测试误差过大时采用了早停法避免过拟合。

如果将上面的代码运行 20 个 epoch,则模型在训练集上的准确率约为 0.78,在验证集上的准确率约为 0.35。这听起来很低,但是请注意,这是一个 5 类的分类问题,随机基线的准确率只有 0.20。

测试

为了测试刚刚训练的模型是否如预期,你需要构建一个预测器(predictor)。predictor 是一个提供基于 JSON 的接口的类,它被用于将输入数据传递给你的模型或将输出数据从模型中导出。接着,我便写了一个句子分类预测器(https://github.com/mhagiwara/realworldnlp/blob/master/realworldnlp/predictors.py#L10),将其用作句子分类模型的基于 JSON 的接口。

tokens = ['This', 'is', 'the', 'best', 'movie', 'ever', '!']
predictor = SentenceClassifierPredictor(model, dataset_reader=reader)
logits = predictor.predict(tokens)['logits']
label_id = np.argmax(logits)

print(model.vocab.get_token_from_index(label_id, 'labels'))

运行这段代码后,你应该看到分类结果为「4」。「4」对应的是「非常积极」。所以你刚刚训练的模型正确地预测出了这是一个非常正面的电影评论。

原文发布于微信公众号 - 机器之心(almosthuman2014)

原文发表时间:2018-10-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏积累沉淀

数据挖掘算法之贝叶斯网络

贝叶斯网络 序 上上周末写完上篇朴素贝叶斯分类后,连着上了七天班,而且有四天都是晚上九点下班,一直没有多少时间学习贝叶斯网络,所以更新慢了点,利用清明节两天假期...

1.1K100
来自专栏机器学习人工学weekly

机器学习人工学weekly-2018/5/13

1. Google I/O召开大会,个人觉得最有意思的是TPU 3.0和AI冒充真人打电话

14350
来自专栏PPV课数据科学社区

如何使用R语言解决可恶的脏数据

在数据分析过程中最头疼的应该是如何应付脏数据,脏数据的存在将会对后期的建模、挖掘等工作造成严重的错误,所以必须谨慎的处理那些脏数据。 脏数据的存在形式主要有如下...

29950
来自专栏人工智能

C+实现神经网络之三—神经网络的训练和测试

前言 在之前的博客中我们已经实现了Net类的设计和前向传播和反向传播的过程。可以说神经网络的核心的部分已经完成。接下来就是应用层面了。要想利用神经网络解决实际的...

22290
来自专栏机器之心

业界 | 谷歌发布语言处理框架SyntaxNet升级版,识别率提高25%

选自research.google 作者: David Weiss等 机器之心编译 参与:李泽南、晏奇 此项升级进一步扩展了 TensorFlow的功能,使这一...

31490
来自专栏ATYUN订阅号

一个简单而强大的深度学习库—PyTorch

AiTechYun 编辑:yuxiangyu 每过一段时间,总会有一个python库被开发出来,改变深度学习领域。而PyTorch就是这样一个库。 在过去的几周...

68360
来自专栏人工智能头条

机器学习在web攻击检测中的应用实践

16450
来自专栏AI研习社

揭开Faiss的面纱 探究Facebook相似性搜索工具的原理

本月初 AI 研习社报道,Facebook 开源了 AI 相似性搜索工具 Faiss。而在一个月之后的今天,Facebook 发布了对 Faiss 的官方原理介...

2K100
来自专栏数据科学与人工智能

【数据】数据预处理

小编邀请您,先思考: 1 数据预处理包括哪些内容? 2 如何有效完成数据预处理? 数据的质量和包含的有用信息量是决定一个机器学习算法能够学多好的关键因素。因此,...

39480
来自专栏AI传送门

keras教程:手把手教你做聊天机器人(下)—— 快速搭建seq2seq模型

33740

扫码关注云+社区

领取腾讯云代金券