Masato Hagiwara作者Geek AI 路编译

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

本文介绍了如何利用 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」对应的是「非常积极」。所以你刚刚训练的模型正确地预测出了这是一个非常正面的电影评论。


原文链接:http://www.realworldnlpbook.com/blog/training-sentiment-analyzer-using-allennlp.html

工程AllenNLP情感分类情感分析
6
相关数据
深度学习技术

深度学习(deep learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。 深度学习是机器学习中一种基于对数据进行表征学习的算法,至今已有数种深度学习框架,如卷积神经网络和深度置信网络和递归神经网络等已被应用在计算机视觉、语音识别、自然语言处理、音频识别与生物信息学等领域并获取了极好的效果。

文本分割技术

文本分割是将书面文本分割成有意义的单位的过程,如单词、句子或主题。这个术语既适用于人类阅读文本时使用的心理过程,也适用于计算机中实现的人工过程,计算机是自然语言处理的主题。这个问题并不简单,因为虽然有些书面语言有明确的词界标记,例如书面英语的单词空间和阿拉伯语独特的最初、中间和最后的字母形状,但这种信号有时是含糊不清的,在所有书面语言中都不存在。

人工智能技术

在学术研究领域,人工智能通常指能够感知周围环境并采取行动以实现最优的可能结果的智能体(intelligent agent)

基准技术

一种简单的模型或启发法,用作比较模型效果时的参考点。基准有助于模型开发者针对特定问题量化最低预期效果。

参数技术

在数学和统计学裡,参数(英语:parameter)是使用通用变量来建立函数和变量之间关系(当这种关系很难用方程来阐述时)的一个数量。

分类数据技术

一种特征,拥有一组离散的可能值。以某个名为 house style 的分类特征为例,该特征拥有一组离散的可能值(共三个),即 Tudor, ranch, colonial。通过将 house style 表示成分类数据,相应模型可以学习 Tudor、ranch 和 colonial 分别对房价的影响。 有时,离散集中的值是互斥的,只能将其中一个值应用于指定样本。例如,car maker 分类特征可能只允许一个样本有一个值 (Toyota)。在其他情况下,则可以应用多个值。一辆车可能会被喷涂多种不同的颜色,因此,car color 分类特征可能会允许单个样本具有多个值(例如 red 和 white)。

TensorFlow技术

TensorFlow是一个开源软件库,用于各种感知和语言理解任务的机器学习。目前被50个团队用于研究和生产许多Google商业产品,如语音识别、Gmail、Google 相册和搜索,其中许多产品曾使用过其前任软件DistBelief。

词性标注技术

词性标注是指为分词结果中的每个单词标注一个正确的词性的程序,也即确定每个词是名词、动词、形容词或其他词性的过程。

张量技术

张量是一个可用来表示在一些矢量、标量和其他张量之间的线性关系的多线性函数,这些线性关系的基本例子有内积、外积、线性映射以及笛卡儿积。其坐标在 维空间内,有 个分量的一种量,其中每个分量都是坐标的函数,而在坐标变换时,这些分量也依照某些规则作线性变换。称为该张量的秩或阶(与矩阵的秩和阶均无关系)。 在数学里,张量是一种几何实体,或者说广义上的“数量”。张量概念包括标量、矢量和线性算子。张量可以用坐标系统来表达,记作标量的数组,但它是定义为“不依赖于参照系的选择的”。张量在物理和工程学中很重要。例如在扩散张量成像中,表达器官对于水的在各个方向的微分透性的张量可以用来产生大脑的扫描图。工程上最重要的例子可能就是应力张量和应变张量了,它们都是二阶张量,对于一般线性材料他们之间的关系由一个四阶弹性张量来决定。

验证集技术

验证数据集是用于调整分类器超参数(即模型结构)的一组数据集,它有时也被称为开发集(dev set)。

神经网络技术

(人工)神经网络是一种起源于 20 世纪 50 年代的监督式机器学习模型,那时候研究者构想了「感知器(perceptron)」的想法。这一领域的研究者通常被称为「联结主义者(Connectionist)」,因为这种模型模拟了人脑的功能。神经网络模型通常是通过反向传播算法应用梯度下降训练的。目前神经网络有两大主要类型,它们都是前馈神经网络:卷积神经网络(CNN)和循环神经网络(RNN),其中 RNN 又包含长短期记忆(LSTM)、门控循环单元(GRU)等等。深度学习是一种主要应用于神经网络帮助其取得更好结果的技术。尽管神经网络主要用于监督学习,但也有一些为无监督学习设计的变体,比如自动编码器和生成对抗网络(GAN)。

准确率技术

分类模型的正确预测所占的比例。在多类别分类中,准确率的定义为:正确的预测数/样本总数。 在二元分类中,准确率的定义为:(真正例数+真负例数)/样本总数

映射技术

映射指的是具有某种特殊结构的函数,或泛指类函数思想的范畴论中的态射。 逻辑和图论中也有一些不太常规的用法。其数学定义为:两个非空集合A与B间存在着对应关系f,而且对于A中的每一个元素x,B中总有有唯一的一个元素y与它对应,就这种对应为从A到B的映射,记作f:A→B。其中,y称为元素x在映射f下的象,记作:y=f(x)。x称为y关于映射f的原象*。*集合A中所有元素的象的集合称为映射f的值域,记作f(A)。同样的,在机器学习中,映射就是输入与输出之间的对应关系。

分类问题技术

分类问题是数据挖掘处理的一个重要组成部分,在机器学习领域,分类问题通常被认为属于监督式学习(supervised learning),也就是说,分类问题的目标是根据已知样本的某些特征,判断一个新的样本属于哪种已知的样本类。根据类别的数量还可以进一步将分类问题划分为二元分类(binary classification)和多元分类(multiclass classification)。

过拟合技术

过拟合是指为了得到一致假设而使假设变得过度严格。避免过拟合是分类器设计中的一个核心任务。通常采用增大数据量和测试样本集的方法对分类器性能进行评价。

自然语言处理技术

自然语言处理(英语:natural language processing,缩写作 NLP)是人工智能和语言学领域的分支学科。此领域探讨如何处理及运用自然语言;自然语言认知则是指让电脑“懂”人类的语言。自然语言生成系统把计算机数据转化为自然语言。自然语言理解系统把自然语言转化为计算机程序更易于处理的形式。

暂无评论
暂无评论~