Python-表情分析的机器学习-三-

35 阅读1小时+

Python 表情分析的机器学习(三)

原文:annas-archive.org/md5/f97ce8c88160e8c42ef68a8f1c0b9e4e

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:朴素贝叶斯

第五章《情感词典和向量空间模型》中,我们研究了使用简单的基于词典的分类器,既使用了手工编写的情感词典,也从标注文本语料库中提取词典。这项调查的结果表明,这样的模型可以产生合理的分数,通过一系列调整(使用词干提取器或改变权重计算方式,例如使用 TF-IDF 分数)在某些情况下可以改善性能,但在其他情况下则不行。现在,我们将转向一系列机器学习算法,看看它们是否能带来更好的结果。

对于我们将要探讨的大多数算法,我们将使用 Python 的 scikit-learn(sklearn)实现。所有这些算法都有广泛的实现。sklearn版本有两个显著优势:它们可以免费获得,并且具有相当一致的接口来处理训练和测试数据,并且可以轻松安装在标准计算机上运行。它们也有一个显著的缺点,即其中一些比专为在配备快速 GPU 或其他高度并行处理器的计算机上运行的版本要慢。幸运的是,大多数算法在标准机器上运行速度相当快,即使是运行速度最慢的,在我们的最大数据集上也能在大约半小时内训练出一个模型。因此,对于我们在这里探讨的任务,即比较各种算法在相同数据集上的性能,优势超过了在非常大的数据集上,其中一些算法将花费不可行的时间的事实。

在本章中,我们将探讨朴素贝叶斯算法。我们将研究在第五章《情感词典和向量空间模型》中使用的各种预处理步骤的影响,但不会探讨这个包提供的所有调整和参数设置。sklearn的各种包提供了一系列选项,这些选项可以影响给定算法的准确度或速度,但通常我们不会尝试所有选项——很容易被参数调整所吸引,希望获得几个百分点的提升,但对于我们的目标,即探讨情感挖掘的方法,考虑数据变化对性能的影响更有用。一旦你选择了算法,那么研究参数变化的影响可能是有价值的,但本书的目的是查看算法对带有情感标签的推文的处理效果,而不是研究算法本身的细节。

我们将本章的起点放在如何准备我们的数据集以匹配sklearn表示上。然后,我们将简要介绍朴素贝叶斯机器学习方法,然后

sklearn.naive_bayes.MultinomialNB中的朴素贝叶斯实现应用于我们的数据集,并考虑为什么算法会表现出这样的行为,以及我们可以做些什么来提高它在我们的数据上的性能。在本章结束时,你将清楚地理解朴素贝叶斯背后的理论,以及将其作为为推文分配情感的方式的有效性。

在本章中,我们将涵盖以下主题:

  • sklearn准备数据

  • 朴素贝叶斯作为机器学习算法

  • 惊叹号地应用贝叶斯定理作为分类器

  • 多标签和多类别数据集

sklearn准备数据

sklearn包期望训练数据由一组数据点组成,其中每个数据点是一个实值向量,以及一组表示每个数据点所属类别的数值标签。我们的数据由推文集合组成,其中每个推文由一组单词和其他值表示,例如[0, 0, 1, 1, 0, 0],其中集合中的每个元素对应一个单一维度。因此,如果某个训练集中的情感集合是['anger', 'fear', 'joy', 'love', 'sadness', 'surprise'],那么[0, 0, 1, 1, 0, 0]集合将表示给定的推文被标记为表达喜悦和爱情。我们将使用 CARER 数据集来说明如何将我们的数据集转换为尽可能符合sklearn包要求的格式。

初始时,我们将数据集表示为在第五章中定义的 DATASET,情感 词典和 向量空间模型。要将数据集转换为适合sklearn的形式,我们必须将标签分配给推文的独热编码转换为单个数值标签,并将代表推文的标记转换为稀疏矩阵。前者是直接的:我们只需枚举值列表,直到遇到非零情况,此时该情况的索引就是所需值。如果有多个非零列,这种编码将只记录找到的第一个——这将扭曲数据,但如果我们使用具有多个标签的数据的独热编码,这是不可避免的。唯一复杂的情况是,如果推文可能没有分配标签,因为在这种情况下,我们将到达列表的末尾而没有返回值。如果allowZeros设置为True,那么我们将返回实际可能情况范围之外的列——也就是说,我们将编码值的缺失作为一个新的显式值:

def onehot2value(l, allowZeros=False):    for i, x in enumerate(l):
        if x == 1:
            return i
    if allowZeros:
        return len(l)
    else:
        raise Exception("No non-zero value found")

我们可以使用它来帮助构建训练集的稀疏矩阵表示,如在第五章的向量空间部分所述,第五章情感词典和向量空间模型。要创建稀疏矩阵,你必须收集所有数据非零情况下的行、列和数据并行列表。因此,我们必须逐个检查推文(推文编号=行编号),然后检查推文中的标记;我们必须在索引中查找标记(标记索引=列编号),确定我们想要为该标记使用的值(要么是 1,要么是其idf),然后将这些添加到rowscolumnsdata中。一旦我们有了这三个列表,我们就可以直接调用稀疏矩阵的构造函数。稀疏矩阵有几种形式:csc_matrix创建一个当每行只包含少量条目时适合的表示。我们必须排除出现次数不超过wthreshold次的单词,因为包含非常罕见的单词会使矩阵变得不那么稀疏,从而减慢速度,而不会提高算法的性能:

from scipy import sparsedef tweets2sparse(train, wthreshold=1):
    rows = []
    data = []
    columns = []
    for i, tweet in enumerate(train.tweets):
        t = sum(train.idf[token] for token in tweet.tokens)
        for token in tweet.tokens:
            if train.df[token] > wthreshold:
                rows.append(i)
                columns.append(train.index[token])
                if useDF:
                    s = train.idf[token]/t
                else:
                    s = 1
                data.append(s)
    return sparse.csc_matrix((data, (rows, columns)),
                             (len(train.tweets[:N]),
                              len(train.index)))

一旦我们将分配给推文的标签表示转换为独热格式,并且我们可以将一组带有黄金标准标签的推文转换为稀疏矩阵,我们就拥有了构建分类器所需的一切。我们如何使用这些结构将完全取决于分类器的类型和数据点到单类标识符的独热值。我们所有的sklearn分类器都将是一个名为SKLEARNCLASSIFIER的通用类的子类:SKLEARNCLASSIFIER的定义不包括构造函数。我们只会创建这个类的子类的实例,所以在某种程度上它就像一个抽象类——它提供了一些将被多个子类共享的方法,例如用于创建朴素贝叶斯分类器、支持向量机分类器或深度神经网络分类器。

分类器,但我们永远不会实际创建一个SKLEARNCLASSIFIER类。在SKLEARNCLASSIFIER中,我们首先需要的是读取训练数据并将其转换为稀疏矩阵的功能。readTrainingData通过使用第五章情感词典和向量空间模型中的makeDATASET来实现这一点,然后将训练数据转换为稀疏矩阵,并将与训练数据相关的标签转换为独热格式:

class SKLEARNCLASSIFIER(classifiers.BASECLASSIFIER):    def readTrainingData(self, train, N=sys.maxsize,
                         useDF=False):
        if isinstance(train, str):
            train = tweets.makeDATASET(train)
        self.train = train
        self.matrix = tweets.tweets2sparse(self.train, N=N,
                                           useDF=useDF)
        # Convert the one-hot representation of the Gold
        # Standard for each tweet to a class identifier
        emotions = self.train.emotions
        self.values = [tweets.onehot2value(tweet.GS,
                                           emotions)
                       for tweet in train.tweets[:N]]

我们需要一个函数来将分类器应用于推文。这个默认值被定义为SKLEARNCLASSIFIER的方法,它封装了底层sklearn类的predict方法,并返回结果,具体格式取决于需要什么:

    def applyToTweet(self, tweet, resultAsOneHot):        p = self.clsf.predict(tweets.tweet2sparse(tweet,
                                                  self))[0]
        if resultAsOneHot:
            k = [0 for i in self.train.emotions]+[0]
            k[p] = 1
            return k
        else:
            return p

我们所有使用sklearn的分类器都将是这个通用类型的子类。它们都将使用readTrainingData——即,将一组推文转换为稀疏矩阵的机制——并且它们都将需要applyToTweet的某个版本。SKLEARNCLASSIFIER提供了这些的默认版本,尽管一些分类器可能会覆盖它们。我们将使用SKLEARNCLASSIFIER作为基类开发的第一个分类器将涉及使用贝叶斯定理来分配事件的概率。首先,我们将研究贝叶斯定理背后的理论及其在分类中的应用,然后再转向如何实现这些细节的具体方法。

高级贝叶斯作为机器学习算法

Naïve Bayes 算法背后的关键思想是,你可以通过使用条件概率并将单个观察结果与结果联系起来,来估计给定一组观察结果的一些结果的似然性。定义什么是条件概率出人意料地困难,因为概率本身的概念非常模糊。概率通常被定义为与比例类似的东西,但当你观察独特或无界的集合时,这种观点就变得难以维持,这通常是你想要使用它们的时候。

假设,例如,我正在试图计算法国赢得 2022 年 FIFA 世界杯的可能性(本文是在决赛前两天写的,法国和阿根廷之间的决赛即将进行)。在某种意义上,询问这个概率是合理的——如果博彩公司提供 3 赔 1 的赔率,他们赢得的概率是 0.75,那么我应该在这个结果上下注。但是,这个概率不能定义为*#(法国赢得 2022 年世界杯的次数)/#(法国参加 2022 年世界杯决赛的次数)*。现在,这两个数字都是 0,所以概率看起来是 0/0,这是未定义的。等你读到这篇文章的时候,第一个数字将是 0 或 1,第二个数字将是 1,所以法国赢得世界杯的概率将是 0 或 1。博彩公司和赌徒会估计这个可能性,但他们不能通过实际计算尚未进行的比赛结果以法国为胜的比例来做到这一点。

因此,我们不能通过观察事件发生给定结果的比例来定义未来一次性事件的概率,因为我们还没有观察到结果,同样地,我们也不能以这种方式合理地定义过去一次性事件的概率,因为一旦事件发生,它必然是 0 或 1。

但我们也不能将一个结果的可能性,例如一系列看似相似事件的似然性定义为比例。我一生中每天早上都看到它变亮的事实——即,25,488 次中有 25,488 次——并不意味着明天早上它会变亮的可能性是 1。明天早上可能会有所不同。太阳可能变成了黑洞并停止了辐射。可能发生了一次巨大的火山爆发,天空可能被完全遮蔽。明天可能不会和今天一样

此外,我们也不能用有限子集成员满足该属性的频率来定义一个无界集成员满足该属性的似然性。考虑一个随机选择的整数是质数的可能性。如果我们绘制前几个整数中质数出现的次数,我们得到的图类似于以下:

图 6.1 – 前 N 个整数中的质数数量

图 6.1 – 前 N 个整数中的质数数量

看起来,前 10,000 个整数中的质数数量呈线性增长,大约有 10%的数字是质数。如果我们查看前 10 亿个整数,那么大约有 6%是质数。真正的概率是什么?不能将其定义为质数数量与整数数量的比率,因为这两个都是无限的,∞/∞是未定义的。看起来,当我们查看更多案例时,比例会下降,所以它可能趋于 0,但它不是 0。结果发现,定义或估计涉及无界集的概率非常困难。

我们可以估计前两种事件的可能性。我们可以查看所有我们认为与当前法国和阿根廷队相似的球队之间的足球比赛,并使用类似当前法国队的球队击败类似当前阿根廷队的球队次数。我可以回顾我生命中的所有日子,并说如果明天在所有相关方面都像所有其他日子一样,那么我对它明天早上会变轻的可能性估计是 1。但这些只是估计,并且它们取决于下一个事件在所有相关方面都与前一个事件相同。

这一直是自 19 世纪以来概率论和统计学中的一个棘手问题。由于这个原因,托马斯·贝叶斯(Thomas Bayes)等人将概率定义为,本质上,某人可能合理地分配给某个结果的几率(Bayes, T, 1958)。计算以往的经验可能是一个重要部分,这样的人可能会在提出他们的合理分配时使用这些信息,但由于无法知道下一个事件在所有相关方面都会与过去的事件相似,因此不能将其用作定义。

因此,我们无法说出给定结果的概率是什么。然而,我们可以定义如果我们有这样一个概率,它应该如何表现。如果你的合理估计不遵守这些约束,那么你应该修改它!

概率分布应该是什么样的?假设我们有一个有限集,{O1, ... On},的可能结果,任何概率分布都应该满足以下约束:

  • 对于所有结果Oip(Oi) >= 0

  • p(O1) + ... +p(On) = 1

  • p(Oi or Oj) = p(Oi)+p(Oj) 对于i ≠ j

前两个约束共同意味着对于所有Oip(Oi) <= 1,第二个和第三个意味着p(not(Oi)) = 1-p(Oi)(因为not(Oi)O1O2或...或Oi-1Oi+1或..或On)。

这些约束对OiOj同时发生的可能性没有任何说明。考虑到初始条件,这是不可能的,因为O1,... On被指定为不同的可能结果。关于多个结果,我们最多只能说,如果我们有两个完全不同且不相连的事件集,每个集都有可能的结果,O1,... OnQ1,... Qm,那么OiQj发生的概率必须是p(Oi) X p(Qj)。就像我们无法确定我们关心的事件是否在所有相关方面都像集合中的其他事件一样,我们也无法确定两组事件是否真的不相连,因此,这又是一个关于概率度量应该如何表现而不是定义的约束。

考虑到所有这些,我们可以定义在已知某个事件B已经发生(或者实际上我们知道B将会发生)的情况下,某个事件A的条件概率:

  • p(A | B) = p(A & B)/p(B)

已知我们了解BA的可能性有多大?嗯,这就是它们一起发生的可能性除以B本身的可能性(所以,如果它们一起发生 5%的时间,而B本身发生 95%的时间,那么看到B不会让我们更有可能期望A,因为A只在B发生的 19 次中出现 1 次;然而,如果它们一起发生 5%的时间,但B本身只发生 6%,那么看到B将是一个强有力的线索,表明A将会发生,因为AB发生的 6 次中有 5 次发生)。

这个定义直接导致贝叶斯定理

  • p(A | B) = p(A & B)/p(B) 定义

  • p(B | A) = p(B & A)/p(A) 定义

  • p(A & B)= p(B &A)AB的约束

  • p(B & A) = p(B | A)×p(A) 重新排列(2)

  • p(A | B) = p(B | A)×p(A)/p(B) 将(4)代入(1)

如果我们有一个事件集,B1, ... Bn,那么我们可以使用贝叶斯定理来说明p(A | B1 & ... Bn) = p(B1 & ...Bn | A)×p(A)/p(B1 & ...& Bn)。如果B**i 是完全不相连的,我们可以说p(A | B1 & ... Bn) = p(B1 | A) ×p(Bn | A)×p(A)/(p(B1) ×p(Bn))

这可以非常方便。假设 A 是“这条推文被标记为愤怒”,而 B1、...、Bn 是“这条推文包含单词 furious”、“这条推文包含单词 cross”、“...”、“这条推文包含单词 irritated”。我们可能从未见过包含这三个单词的推文,因此我们无法通过计数来估计 A 的似然。然而,我们将看到包含这些单词的推文,我们可以计算被标记为 angry 的推文中包含 furious(或 crossirritated)的数量,以及总共被标记为 angry 的数量,忽略它们包含的单词,以及包含 furious(或 crossirritated)的数量,忽略它们的标签。因此,我们可以对这些做出合理的估计,然后我们可以使用贝叶斯定理来估计 p(A | B1 & .. Bn)

这种应用贝叶斯定理的方法假设事件 B1、...、Bn 完全不相关。这很少是真实的:包含单词 cross 的推文更有可能也包含 irritated,而不是不包含。因此,虽然我们可以确实地以这种方式 天真地 错误地使用贝叶斯定理来获得给定一组观察结果的一些结果的可用估计,但我们永远不应该忽视这些估计本质上是不可靠的这一事实。在下一节中,我们将探讨如何实现这种天真地应用贝叶斯定理作为分类器的方法,并研究它在我们的各种数据集上的表现如何。这种方法成功的关键在于,尽管某些结果的似然估计并不可靠,但不同结果的排名通常是有意义的——如果某些推文是 angrysad 的概率估计分别是 0.6 和 0.3,那么它确实更有可能是 angry 而不是 sad,即使实际的数字不能被信赖。

Naively applying Bayes’ theorem as a classifier

sklearn.naive_bayes.MultinomialNB 为我们计算这些总和(这些总和并不困难,但有一个快速计算它们的包是非常方便的)。鉴于这一点,NBCLASSIFIER 类的定义非常简单:

class NBCLASSIFIER(sklearnclassifier.SKLEARNCLASSIFIER):    def __init__(self, train, N=sys.maxsize, args={}):
        # Convert the training data to sklearn format
        self.readTrainingData(train, N=N, args=args)
        # Make a naive bayes classifier
        self.clsf = naive_bayes.MultinomialNB()
        # Train it on the dataset
        self.clsf.fit(self.matrix, self.values)

这就是制作朴素贝叶斯分类器所需的所有内容:使用 sklearn.naive_bayes.MultinomialNB 创建 SKLEARNCLASSIFIER

这效果如何?我们将尝试在我们的数据集上使用这种方法,对于非英语数据集使用词干提取,但英语数据集则不使用(从现在开始我们将这样做,因为这似乎是 第五章 中提到的正确选择,情感词典和向量空间模型):

PrecisionRecallMicro F1Macro F1Jaccard
SEM4-EN0.8730.8730.8730.8730.775
SEM11-EN0.6250.2620.3690.3730.227
WASSA-EN0.8300.8300.8300.8300.709
CARER-EN0.8740.8740.8740.8740.776
IMDB-EN0.8490.8490.8490.8490.738
SEM4-AR0.6940.6940.6940.6940.531
SEM11-AR0.6280.2740.3810.3930.236
KWT.M-AR0.6670.6550.6610.6640.494
SEM4-ES0.5250.5350.5300.4620.360
SEM11-ES0.5080.2960.3740.3800.230

图 6.2 – 朴素贝叶斯,每条推文一个情绪

首先要注意的是,构建和应用朴素贝叶斯分类器都非常快——每秒可以分类 10K 条推文,甚至在包含 40K 条推文的训练集上训练也只需不到 10 秒。但是,正如之前所说,重要的是分类器是否擅长我们希望它执行的任务。前表显示,对于大多数英语数据集,分数优于第五章中情感词典和向量空间模型的分数,特别是 CARER 数据集的改进尤为明显,而 SEM11-EN 的分数在第五章情感词典和向量空间模型中明显较低。

回顾 CARER 与其他数据集的主要区别:CARER 比其他数据集大得多,并且与 SEM11 相反,每条推文都与一个标签精确关联。为了查看问题是否与训练集的大小有关,我们将绘制该数据集的准确率与不断增加的训练大小之间的关系:

图 6.3 – Jaccard 分数与训练大小对比,朴素贝叶斯,使用 CARER 数据集

Jaccard 分数从相当低的基数稳步上升,尽管当我们达到大约 40K 条训练推文时它开始趋于平坦,但很明显朴素贝叶斯确实需要相当多的数据。这可能是它对其他数据集效果较差的部分原因:它们简单地没有足够的数据。

值得详细研究一下这个算法的内部工作原理。就像基于词典的分类器一样,朴素贝叶斯构建了一个词典,其中每个词都与各种情绪相关联的分数:

愤怒恐惧喜悦爱情悲伤惊讶
a0.01870.01940.02030.02010.01900.0172
and0.02910.02840.03110.02860.03080.0247
the0.02410.02380.02840.02750.02450.0230
angry0.00200.00030.00010.00010.00030.0001
happy0.00050.00030.00140.00040.00050.0004
hate0.00070.00050.00020.00030.00070.0002
irritated0.00130.00000.00000.00000.00000.0000
joy0.00010.00010.00020.00020.00010.0001
love0.00090.00090.00190.00300.00110.0011
sad0.00050.00030.00020.00020.00100.0003
scared0.00010.00190.00010.00010.00020.0001
terrified0.00000.00140.00000.00000.00000.0000

图 6.4 – 单个单词、朴素贝叶斯与 CARER 数据集的得分

与基于词典的模型一样,aandthe的得分相当高,反映了这些词在大多数推文中出现的事实,因此它们在表达各种情感的推文中出现的条件概率也相当高。当我们通过它们的总体频率来划分它们所做出的贡献时,这些词将被大量抵消。其他所有词的得分都非常小,但总体上,它们与预期的情感相匹配 – angryirritated愤怒联系最为紧密,joy(几乎)与快乐联系最为紧密,依此类推。与不同情感关联程度的差异远不如简单的基于词典的算法明显,因此改进的性能必须归因于贝叶斯定理结合得分的方式的改进。很明显,这些词不是独立分布的:在 CARER 数据集中包含angryirritated以及两者都包含的推文比例分别为 0.008、0.003 和 0.0001。如果我们把这些作为相应概率的估计,我们会发现 p(angry + irritated)/p(angry) X p(irritated) = 3.6,如果这些词是独立分布的,那么这个值应该是 1。这并不令人惊讶 – 你在单个推文中使用表达相同情感的词比使用表达不同情感或彼此无关的词的可能性要大得多。尽管如此,贝叶斯定理足够稳健,即使在应用它的条件不完全适用的情况下,只要我们有足够的数据,它也能给出有用的结果。

多标签数据集

SEM11 与其他数据集的关键区别在于 SEM11 集中的推文可以被分配任意数量的情感 – 它们是多标签数据集,如第五章中定义的,情感词典和向量空间模型。实际的分布如下:

012345678910
SEM11-EN205997282721516621001100000
SEM11-AR17544100576921033300000
SEM11-ES17914991605479521100000

图 6.5 – 每个 SEM11 数据集中带有 0,1,2,...情感标签的推文数量

在每种情况下,大多数推文都有两个或更多的标签。这使得任何为每条推文分配一个标签的算法很难获得高分——对于每个没有标签的推文,必须有假阳性,对于每个有 K 个标签的推文,必须有 K-1 个假阴性(因为,最多只有一个,K,被选中,因此 K-1 没有被选中)。假设我们有N条推文,其中Z没有标签,O恰好有一个标签,而M有多个标签。所以,即使我们假设我们的分类器在推文至少有一个标签时总是正确地得到一个标签,所能获得的最佳 Jaccard 分数是*(O+M)/(O+2M+Z)——将有O+M个真阳性(所有应该分配一个标签的情况,加上根据假设应该有多个标签的情况),至少Z个假阳性(每个应该没有标签的推文一个),至少M个假阴性。

因此,对于 SEM11-EN 数据集,通过算法为每条推文分配一个标签所能获得的最佳 Jaccard 分数是 0.41(如果分配给任何具有一个或多个标签的推文的标签都是正确的,那么我们将有 6,748 个真阳性,205 个假阳性,和 9,570 个假阴性)。如果这是算法可能达到的最大 Jaccard 分数,那么我们之前获得的约 0.2 的分数并不算太差。

但它们并不如我们在第五章“情感词典和向量空间模型”中为这些数据集获得的分数好。我们需要 somehow 让朴素贝叶斯返回多个标签。

这实际上相当简单。我们可以使用贝叶斯定理来估计每个可能结果的概率。sklearn.naive_bayes.MultinomialNB通常选择概率最高的结果,但它有一个方法,predict_log_proba,它返回每个可能结果的概率的对数(由于使用对数可以替换乘法为加法,这通常很方便,因为加法运算比乘法运算快得多)。我们可以使用这个来选择,例如,每个概率超过某个阈值的输出,或者选择最好的两个而不是仅仅最好的一个。我们将依次查看这两个选项。对于第一个,我们将使用与NBCLASSIFIER相同的构造函数,我们只需将applyToTweet更改为使用predict_log_proba而不是predict

class NBCLASSIFIER1(NBCLASSIFIER):    def applyToTweet(self, tweet, resultAsOneHot=True):
        tweet = tweets.tweet2sparse(tweet, self)
        # use predict_log_proba
        p = self.clsf.predict_log_proba(tweet)[0]
        # compare to previously defined threshold
        threshold = numpy.log(self.threshold)
        return [1 if i > threshold else 0 for i in p]

下表只是为了方便比较而复制了之前用于处理朴素贝叶斯多标签情况的表格:

精确度召回率微观 F1宏观 F1Jaccard
SEM11-EN0.6250.2620.3690.3730.227
SEM11-AR0.6280.2740.3810.3930.236
KWT.M-AR0.6670.6550.6610.6640.494
SEM11-ES0.5080.2960.3740.3800.230

图 6.6 – 朴素贝叶斯,每条推文一个情感,多类情况

以下表格显示了当我们允许分类器为推文分配多个情感时会发生什么:

精确率召回率微观 F1宏观 F1Jaccard
SEM11-EN0.5150.3560.4210.4240.267
SEM11-AR0.4940.3810.4300.4440.274
KWT.M-AR0.6450.7040.6730.6770.507
SEM11-ES0.4190.3940.4060.4150.255

图 6.7 – 朴素贝叶斯,具有最佳阈值的多个结果,SEM11 数据集

在每种情况下,我们显著提高了召回率(因为我们现在允许每条推文选择多个标签),但代价是精确率下降。Jaccard 分数略有上升,但并未达到比第第五章中获得的分数更好的程度,即情感词典和向量空间模型

我们也可以简单地要求每条推文有两个标签。同样,这将提高召回率,因为我们为所有应该有两个标签的情况都提供了两个标签,为所有应该有三个标签的情况提供了两个,为所有应该有四个标签的情况提供了两个——也就是说,我们可能会减少错误标签的数量

负面标签的数量也会不可避免地增加,因为我们会在本应没有或只有一个标签的地方有两个标签。这是一个极其简单的算法,因为它没有注意到何时应该允许两个标签——我们只是假设在每种情况下这都是正确的做法:

class NBCLASSIFIER2(NBCLASSIFIER):    def applyToTweet(self, tweet, resultAsOneHot=True):
        tweet = tweets.tweet2sparse(tweet, self)
        p = self.clsf.predict_log_proba(tweet)[0]
        # pick the second highest score in p
        threshold = list(reversed(sorted(list(p))))[2]
        return [1 if i > threshold else 0 for i in p]

这进一步略微提高了 SEM11 案例的精确率,但仍然不足以超过第第五章情感词典和向量空间模型的结果,对 KWT.M-AR 来说更是灾难性的,其中有一些案例有多个分配,而大多数案例则完全没有分配——迫使分类器在应该没有标签的情况下选择两个标签将对精确率产生重大影响!

精确率召回率微观 F1宏观 F1Jaccard
SEM11-EN0.4770.4040.4370.4290.280
SEM11-AR0.4740.4130.4410.4400.283
KWT.M-AR0.4610.9060.6110.6120.440
SEM11-ES0.3700.4310.3980.3950.249

图 6.8 – 朴素贝叶斯,最佳两个结果,多标签数据集

因此,我们有两种非常简单的方法将朴素贝叶斯转换为具有多个(或零个)结果的分类器。在两种情况下,与标准版本相比的改进都是微小的但很有用。在两种情况下,都需要我们了解一些关于训练集的信息——第一种需要我们选择一个阈值来比较单个分数,第二种需要我们知道每条推文的输出数量分布。这意味着在这两种情况下,我们都要使用训练数据做两件事——像标准情况一样找到条件概率,然后选择最佳可能的阈值或查看输出数量的分布;为此,我们必须将训练数据分成两部分,一个用于找到基本概率的训练部分,然后是一个开发部分用于找到额外信息。这在需要调整基本模型的情况下很常见。没有规则说我们必须像必须保持训练集和测试集分离一样保持训练集和开发集分离,但实践中,这样做通常会产生比使用训练集作为开发集更好的结果。

然而,多标签数据集的分数仍然比在第五章“情感词典和向量空间模型”中的分数要差。我们可以尝试这两种策略的组合,例如,只要两者都满足某些条件,就可以要求最好的两个结果。

阈值,但无论如何调整都不会将朴素贝叶斯转变为多标签问题的良好分类器。我们将在第十章“多分类器”中回到这个问题。

我们还需要尝试找出为什么朴素贝叶斯在 SEM4、CARER 和 IMDB 数据集上相对于基于词典的方法有相当大的改进,但在 WASSA 上表现较差。我们已经看到,随着训练数据的增加,朴素贝叶斯在 CARER 上的性能显著提高。这三个数据集的大小分别是 SEM4-EN 6812,WASSA-EN 3564,和 CARER-EN 411809。如果我们将这三个案例的训练数据限制为与 WASSA 相同,会发生什么?以下表格是原始表格的相关部分的副本,每个案例都使用完整数据集:

精确率召回率微观 F1宏观 F1Jaccard
SEM4-EN0.8730.8730.8730.8730.775
WASSA-EN0.8300.8300.8300.8300.709
CARER-EN0.8740.8740.8740.8740.776
IMDB-EN0.8490.8490.8490.8490.738

图 6.9 – 基于朴素贝叶斯,英文单类数据集 – 完整训练集

当我们将可用的数据量减少到与 WASSA 相同,结果如预期的那样变得更差:

精确率召回率微观 F1宏观 F1Jaccard
SEM4-EN0.8370.8370.8370.8370.719
WASSA-EN0.8300.8300.8300.8300.709
CARER-EN0.7320.7320.7320.7320.577
IMDB-EN0.8250.8250.8250.8250.703

图 6.10 – 朴素贝叶斯,英语单类别数据集 – 限制训练集

我们对 SEM4-EN、CARER-EN 和 IMDB-EN 数据集的改进相对于第五章中*“情感词典和向量空间模型”*的结果现在不那么明显,尤其是对于 CARER-EN:当我们限制数据集大小时,信息丢失是显著的。

是否还有其他可能解释这些差异的因素?拥有更多的类别会使问题更加困难。例如,如果你有 10 个类别,那么随机选择正确的概率将是 10%

在 5 个类别的情况下,随机选择正确率仅为 20%。然而,SEM4-EN 和 WASSA-EN 具有相同的标签集,即愤怒恐惧快乐悲伤,而 CARER-EN 除了这四个标签外还有惊讶,所以如果这是关键因素,我们预计 SEM4-EN 和 WASSA 的版本会产生相似的结果,而 CARER 会略差一些,但这并不是我们观察到的结果。此外,可能存在一个类别分布非常不均的集合,这可能会产生影响。然而,SEM4-EN 和 WASSA-EN 之间各种情绪的分布相当相似:

SEM4-EN: 愤怒:834,恐惧:466,快乐:821,悲伤:1443

WASSA-EN: 愤怒:857,恐惧:1098,快乐:823,悲伤:786

SEM4-EN 有更多表达悲伤的推文,而 WASSA-EN 有更多表达恐惧的推文,但差异并不足以导致你期望分类器的性能有差异。这两个数据集的词汇量几乎相同(75723 与 75795),平均每条推文的标记数也几乎相同(均为 21.2)。有时,似乎一个分类器非常适合一个任务,而另一个分类器更适合另一个任务。

摘要

在本章中,我们看到了朴素贝叶斯可以作为寻找推文中情感的分类器工作得非常好。它在大训练集上尤其有效(由于它只是计算单词及其出现在其中的推文所关联的情感的出现次数,因此训练时间非常短)。它可以相当直接地适应与可能具有任何数量标签(包括零)的单条推文一起工作的数据集,但在具有此属性的测试集上,它被第五章中*“情感词典和向量空间模型”*的基于词典的方法所超越。图 6.11显示了迄今为止各种数据集的最佳分类器:

LEXCPNB (single)NB (multi)
SEM4-EN0.5030.5930.775****0.778
SEM11-EN0.347****0.3530.2270.267
WASSA-EN0.4450.505****0.7090.707
CARER-EN0.3500.395****0.7760.774
IMDB-EN0.7220.7220.738****0.740
SEM4-AR0.5060.5130.531****0.532
SEM11-AR0.378****0.3820.2360.274
KWT.M-AR****0.6870.6660.4940.507
SEM4-ES****0.4250.1770.3600.331
SEM11-ES0.269****0.2780.2300.255

图 6.11 – 目前最佳分类器

一般来说,朴素贝叶斯是针对每个推文只有一个标签的数据集的最佳分类器,在这些数据集中,朴素贝叶斯假设每个推文只有一个标签的版本与允许多个标签的版本之间的边际差异不大。对于多标签数据集,允许多个标签的版本总是优于不允许的版本,但在所有这些情况下,来自 wwwwwwwwwww, Sentiment Lexicons and Vector Space Models 的基于词典的分类器表现最佳。到目前为止,本章最大的教训是,在尝试解决分类问题时,你应该尝试各种方法,并选择效果最好的方法。在接下来的章节中,我们将看到当我们查看更复杂的机器学习算法时会发生什么。

参考文献

要了解更多关于本章所涉及的主题的信息,请查看以下资源:

第七章:支持向量机

第六章中,朴素贝叶斯,我们探讨了使用贝叶斯定理来查找与单个推文相关的情绪。那里的结论是,标准的朴素贝叶斯算法对某些数据集效果良好,而对其他数据集效果较差。在接下来的章节中,我们将探讨几种其他算法,看看我们是否能取得任何改进,本章将从众所周知的支持向量机(SVM)(Boser 等人,1992)方法开始。

我们将本章开始于对 SVMs 的简要介绍。这种介绍将采用几何方法,这可能比标准的介绍更容易理解。Bennett 和 Bredensteiner(见参考文献部分)给出了两个方法等效的详细形式证明——本章的讨论旨在简单地提供对问题的直观理解。然后,我们将向您展示如何使用sklearn.svm.LinearSVC实现来处理我们的当前任务。与先前的方法一样,我们将从一个简单应用该方法开始,该方法对某些示例效果良好,但对其他示例效果较差;然后,我们将介绍两种改进方法,以便与多标签数据集一起使用,最后,我们将反思我们获得的结果。

在本章中,我们将涵盖以下主题:

  • SVM 背后的基本思想

  • 简单 SVMs 在标准数据集上的应用

  • 将 SVM 扩展到覆盖多标签数据集的方法

SVMs 的几何介绍

假设我们有两个实体组,称为 B 和 R,其中每个实体都由一对数值坐标描述。B 包括坐标为(6.38,-10.62)、(4.29,-8.99)、(8.68,-4.54)等的对象,而 R 包含坐标为(6.50,-3.82)、(7.39,-3.13)、(7.64,-10.02)等的对象(本讨论中使用的示例已从scikit-learn.org/stable/modules/svm.xhtml#classification中选取)。将这些点绘制在图上,我们得到以下结果:

图 7.1 – R 点和 B 点的绘图

图 7.1 – R 点和 B 点的绘图

看起来你应该能够画一条直线来分隔两组,如果你能这样做,那么你可以用它来决定某个新点是否是 R 或 B 的实例。

对于如此简单的情况,有许多找到这样一条线的方法。一种方法是为两组找到凸包(Graham,1972)——即包括它们的 polygons。可视化这种方法的简单方式是,从集合中的最左端点作为起点。然后,你应该从那里选择最顺时针的点,并将其设置为列表中的下一个点,然后再次用那个点做同样的事情,直到你回到起点。

要了解如何从一个给定的起点选择最顺时针的点,请考虑这里显示的两个图表:

图 7.2 – 逆时针和顺时针转向

图 7.2 – 逆时针和顺时针转向

在左侧图中,从 A 到 B 的斜率比从 B 到 C 的斜率平缓,这意味着如果你想从 A 到 B 到 C,你到达 B 时必须逆时针转向,这反过来又意味着 C 比 B 更远离 A。在右侧图中,从 A' 到 B' 的斜率比从 B' 到 C' 的斜率更陡,这意味着 C' 比 B' 更远离 A'。因此,为了判断 C 比 B 更远离 A 还是更近,我们需要计算连接它们的线的斜率,并看哪条更陡:从 A 到 C 的线的斜率是 (C[1]-A[1])/(C[0]-A[0]),同样对于连接 A 和 B 的线也是如此,所以如果 (C[1]-A[1])/(C[0]-A[0]) > (B[1]-A[1])/(B[0]-A[0]),则 C 比 B 更远离 A。重新排列这个公式,我们得到 ccw,如下所示:

def ccw(a, b, c):    return (b[1]-a[1])*(c[0]-b[0]) < (b[0]-a[0])*(c[1]-b[1])

然后,我们可以使用这个算法来找到凸包。我们按点的 YX 坐标对点进行排序,这使我们能够找到最低点 p(如果有平局,则选择最左边的点)。这个点必须位于凸包上,因此我们将其添加到凸包中。然后,我们从点的列表中选择下一个项目 q(如果 p 是最后一个点,则回到列表的开始 – 如果 pn,则 (p+1)%n 将为 0,否则为 p+1)。现在,我们从 q 开始遍历整个点的列表,使用 ccw 来判断从 piq 是否满足之前给出的约束:如果满足,则 iq 更远离 p,因此我们用 i 替换 q。在这个过程中,我们知道 q 是从 p 最远离的点,因此我们将其添加到凸包中并继续。

该算法的复杂度为 o(HN)*,其中 H 是凸包的大小,N 是点的总数 – H 因为主循环在添加每个迭代一个项目后终止,构建了凸包,N 因为在主循环的每次遍历中,我们必须查看每个点以找到最远离的点。在某些情况下,存在更复杂的算法,其效率高于此,但这里给出的算法对于我们的目的来说已经足够高效:

def naiveCH(points):    points.sort()
    p = 0
    hull = []
    while True:
        # Add current point to result
        hull.append(points[p])
        # Pick the next point (or go back to the beginning
        # if p was the last point)
        q = (p + 1) % n
        for i in range(len(points)):
    # If i is more counterclockwise
    # than current q, then update q
           if(ccw(points[p], points[i], points[q])):
                q = i;
    # Now q is the most counterclockwise with respect to p
    # Set p as q for next iteration, so that q is added to 'hull'
          p = q
    # Terminate when you get back to that start and close the loop
          if(p == 0):
              hull.append(points[0])
              break
    return hull

下图说明了该算法在我们示例中围绕 B 点集的进展情况:

图 7.3 – 为 B 增长凸包

图 7.3 – 为 B 增长凸包

存在更多用于生长船体的有效算法(参见 scipy.spatial.ConvexHulldocs.scipy.org/doc/scipy/reference/generated/scipy.spatial.ConvexHull.xhtml),但这个算法易于理解。我们用它来计算 R 和 B 的凸包:

图 7.4 – B 和 R 的凸包

图 7.4 – B 和 R 的凸包

如果有任何线可以分隔 R 和 B(如果它们是线性可分的),那么凸包的至少一个段必须是。如果我们选择 B 的凸包上离 R 的凸包上某条边最近的边,我们可以看到它是一个分隔符——所有的 B 项目都在虚线橙色线以上或在其上,同样,所有的 R 项目都在虚线绿色线以下或在其以下:

图 7.5 – 作为 R 和 B 候选分隔符的边界段

图 7.5 – 作为 R 和 B 候选分隔符的边界段

但它们并不是很好的分隔符。所有刚好低于虚线橙色线的项目都会被分类为 R,即使它们只是刚好低于这条线,因此它们比 R 更接近 B;并且所有刚好高于虚线绿色线的项目都会被分类为 B,即使它们只是刚好高于它。

因此,我们希望找到一种方法来找到一个分隔符,可以适当地处理介于这两条极端线之间的案例。我们可以通过找到一条线(虚线灰色线)从一条段的最接近点到另一条(另一条段)来做到这一点,然后在这条线的中间和垂直于这条线的地方画我们的分隔符(实线黑色线):

图 7.6 – R 和 B 的最佳分隔符

图 7.6 – R 和 B 的最佳分隔符

实线黑色线是一个最佳分隔符,因为它使得两组之间的分离尽可能大:每个组中离线最近的点的距离尽可能大,因此任何落在它上面的未观察到的点将被分配到 B,这是它最好的去处,任何落在它下面的点将被分配到 R。

这正是我们想要的。不幸的是,如果有些点是异常值——也就是说,如果有些 R 点落在 B 点的主体内或有些 B 点落在 R 点的主体内,那么一切都会出错。在以下示例中,我们切换了两个点,使得有一个 B 点位于 R 点的左上角附近,有一个 R 点位于 B 点的底部附近:

图 7.7 – 一个 R 点和一个 B 点被切换

图 7.7 – 一个 R 点和一个 B 点被切换

两组的凸包现在重叠,不能合理地用于寻找分隔符:

图 7.8 – 带有异常值的凸包

图 7.8 – 带有异常值的凸包

我们可以尝试识别异常值并将它们从其组中排除,例如,通过找到整个组的质量中心,标记为黑色椭圆,如果它离另一个组的质量中心更近,则从其中移除一个点:

图 7.9 – 两组的重心

图 7.9 – 两组的重心

很明显,异常点更接近“错误”组的质心,因此可以在尝试找到分隔符时识别并移除它们。如果我们都移除它们,我们就会得到非重叠的凸包。分隔符没有将异常点放在“正确”的一侧,但那时任何直线都无法做到这一点——任何包含异常 R 和其他 R 的直线都必须包含异常 B,反之亦然:

图 7.10 – 忽略两个异常点(棕色 X 被忽略)的非重叠凸包和分隔符

图 7.10 – 忽略两个异常点(棕色 X 被忽略)的非重叠凸包和分隔符

然而,我们也可以通过只忽略其中一个来得到非重叠的凸包:

图 7.11 – 忽略 R(左)和 B(右)异常点的非重叠凸包和分隔符

图 7.11 – 忽略 R(左)和 B(右)异常点的非重叠凸包和分隔符

这次,我们在计算凸包时只忽略一个异常点。在每种情况下,我们都得到两个非重叠的凸包和一个分隔符,但这次,我们使用了更多的原始数据点,只有一个点落在线的错误一侧。

哪个更好?在尝试找到分隔符时使用更多的数据点,让更多的数据点落在线的右侧,还是最小化线与两个集合中点之间的总距离?如果我们决定忽略一个点,我们应该选择哪一个(图 7.11顶部的分隔符比底部的更好吗)?左下角的 B 点是不是 B 的正常成员,还是它是异常点?如果我们没有 R 的异常点,那么就永远不会有任何理由怀疑这个确实是 B。

我们需要了解每个问题的重要性,然后我们必须优化我们的点选择以获得最佳结果。这使它成为一个优化算法,其中我们通过连续改变点的集合来尝试优化先前给出的标准——包含多少个点,分隔线与最近点有多接近,以及在最一般的情况下,定义分隔线的方程应该是什么(有一些技巧可以允许圆形或曲线分隔线,或者允许分隔线稍微弯曲)。如果分隔线是直的——也就是说,类别是线性可分的——那么对于二维情况,线将有一个如<mml:math  >mml:miA</mml:mi><mml:mi mathvariant="normal"></mml:mi>mml:mix</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi><mml:mi mathvariant="normal"></mml:mi>mml:miy</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:math>的方程。当我们移动到三维时,分隔线变成一个如<mml:math  >mml:miA</mml:mi><mml:mi mathvariant="normal"></mml:mi>mml:mix</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi><mml:mi mathvariant="normal"></mml:mi>mml:miy</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi><mml:mi mathvariant="normal">*</mml:mi>mml:miz</mml:mi>mml:mo+</mml:mo>mml:miD</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:math>的平面。当我们移动到更高的维度时,它变成一个超平面,其方程如<mml:math  >mml:miA</mml:mi>mml:mn1</mml:mn><mml:mi mathvariant="normal"></mml:mi>mml:mix</mml:mi>mml:mn1</mml:mn>mml:mo+</mml:mo>mml:miA</mml:mi>mml:mn2</mml:mn><mml:mi mathvariant="normal"></mml:mi>mml:mix</mml:mi>mml:mn2</mml:mn>mml:mo+</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo+</mml:mo>mml:miA</mml:mi>mml:min</mml:mi><mml:mi mathvariant="normal">*</mml:mi>mml:mix</mml:mi>mml:min</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn>mml:mo.</mml:mo></mml:math>

我们用于说明目的的简单程序不足以处理所有这些问题。我们将希望与非常高维的空间一起工作,在大多数情况下,大多数维度都是零。幸运的是,有许多高效的实现可供我们使用。我们将使用来自sklearn.svm的 Python LinearSVC实现——Python 中还有许多其他实现,但sklearn包通常很稳定,并且与其他sklearn部分很好地集成,而且LinearSVC已知特别适用于大型稀疏线性任务。

使用支持向量机进行情感挖掘

我们现在已经看到 SVM 如何通过找到将数据分离成类别的超平面来提供分类器,并看到了即使数据不是线性可分时如何找到这样的超平面的图形解释。现在,我们将探讨如何将 SVM 应用于我们的数据集以找到情感之间的边界,分析它们在单标签和多标签数据集上的行为,并对如何提高多标签数据集上的性能进行初步研究。

应用我们的 SVM

与之前的分类器一样,我们可以通过以下初始化代码定义SVMCLASSIFIERs类作为SKLEARNCLASSIFIER的子类(useDF是一个标志,用于决定在构建训练集时是否使用来自第五章**,情感词典和向量空间模型的 TF-IDF 算法;max_iter设置 SVM 算法应执行的迭代次数的上限——在我们的例子中,分数通常在 2,000 步时收敛,所以我们通常将其作为限制):

    def __init__(self, train, args={"useDF":True}):        self.readTrainingData(train, args=args)
        # Make an sklearn SVM
        self.clsf = sklearn.svm.LinearSVC(max_iter=2000)
        # Get it to learn from the data
        self.clsf.fit(self.matrix, self.values)

这与NBCLASSIFIERS的构造函数非常相似——只需使用readTrainingData将数据格式化,然后使用sklearn实现来构建 SVM。

如同往常,我们首先将此方法应用于我们的标准数据集:

精确率召回率微平均 F1宏平均 F1Jaccard
SEM4-EN0.9160.9160.9160.9160.845
SEM11-EN0.6200.2600.3660.3720.224
WASSA-EN0.8700.8700.8700.8700.770
CARER-EN0.8700.8700.8700.8700.770
IMDB-EN0.8480.8480.8480.8480.736
SEM4-AR0.6790.6790.6790.6790.514
SEM11-AR0.5860.2550.3560.3670.216
KWT.M-AR0.7810.7670.7740.7780.631
SEM4-ES0.5920.5740.5830.4940.412
SEM11-ES0.4930.2950.3690.3720.226

图 7.12 – SVM 应用于标准数据集

基本 SVM 为我们带来了迄今为止 WASSA 和两个 SEM4 数据集的最佳分数,对于大多数其他数据集的分数也接近我们迄今为止获得的最佳分数。与之前的算法一样,如果我们使用标准设置,它在多标签问题上的表现非常差,仅仅是因为返回确切一个标签的算法无法处理具有零个或多个标签的项目集。

训练 SVM 比我们迄今为止看到的任何分类器都要长得多。因此,简要地看一下随着训练数据大小的变化,CARER 的准确性和训练时间如何变化是值得的——如果我们发现更多的数据对准确性影响不大,但会使训练时间大大增加,我们可能会决定获取更多数据不值得麻烦。

当我们将准确率(因为没有任何空类,召回率和宏平均及微平均 F 度量值都相同)和 Jaccard 分数与训练集的大小进行对比时,我们发现我们不需要整个数据集——这两个度量值相当快地趋于一致,而且如果有什么的话,性能在一段时间后开始下降:

图 7.13 – SVM 的准确率与训练数据大小的关系

图 7.13 – SVM 的准确率与训练数据大小的关系

在大约 30K 条推文之后性能的下降可能只是噪声,也可能是过拟合的结果——随着机器学习算法看到越来越多的数据,它们可以开始注意到训练数据中特有的东西,而这些东西在测试数据中并不存在。无论如何,性能显著优于我们在第五章第六章中看到的所有内容,并且似乎在约 0.89 的准确率和 0.80 的 Jaccard 分数上趋于平稳。

我们还绘制了训练时间与数据大小的关系图,以查看如果我们能获得更多数据,是否可行运行它,时间大致与数据大小呈线性增长(这类问题通常有这种报道)。然而,由于准确率已经在约 40K 时趋于平稳,因此增加更多数据似乎不会产生任何影响:

图 7.14 – SVM 的训练时间与数据大小的关系

图 7.14 – SVM 的训练时间与数据大小的关系

注意

任何分类器在拥有任何数量的数据后,其准确率都必须在达到 1 之前趋于平稳。大多数可以轻易拟合到如图所示(例如,多项式曲线)的图表中的曲线,随着 X 值的增加而持续增加,因此它们只能是对真实曲线的近似,但它们确实让你对增加训练数据量是否值得有一个印象。

对无标签推文和有多个标签的推文数据集进行的实验

为什么标准支持向量机(SVM)在多标签数据集上的表现比大多数其他情况都要差得多?

SVMs(支持向量机)和其他任何标准分类器一样,旨在将每个项目分配到单个类别。训练集中的每个点都有一组特征和标签,学习算法会找出特征和标签之间的联系。将这种方法适应包括某些点没有标签的数据相当直接,只需说有一个额外的标签,比如中性、以上皆非,或者类似的东西,当某个点没有给出标签时使用。这是一种稍微有些人为的方法,因为它意味着分类器找到与没有情绪相关的单词,而实际情况是,这样的点根本没有任何带有情感重量的单词。然而,这通常效果不错,并且可以被纳入标准的 SVM 训练算法(第六章朴素贝叶斯中显示的onehot2value的定义允许这种情况)。

SEM11 和 KWT.M-AR 示例属于更难、也许更现实的类别问题,其中一条推文可能表达零个或多个情绪。SEM11-EN 测试集中的第二条推文,“我正在做这一切以确保你对我微笑,兄弟”,表达了快乐、爱和乐观这三种情绪,而倒数第二条,“# ThingsIveLearned 聪明的牧羊人永远不会把羊群托付给一个微笑的狼。# TeamFollowBack # fact # wisewords”,与之没有关联的情绪。

由于可能的情绪集合[‘愤怒’,‘期待’,‘厌恶’,‘恐惧’,‘快乐’,‘爱’,‘乐观’,‘悲观’,‘悲伤’,‘惊讶’,‘信任’],将这些情绪编码为向量是足够的:我们用[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0]表示快乐、爱和乐观,用[0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0]表示没有情绪。但是这些不是独热编码onehot2value无法正确处理它们。特别是,它将[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0]解释为快乐,因为这是它遇到的第一个非零项。

没有简单的解决办法——SEM11 数据具有多种情绪,而 SVMs 期望单一的情绪。这有两个后果。在训练期间,只会使用与推文相关联的情绪中的一个——如果前面的例子发生在训练期间,它会导致单词smilingbro(这是这条推文中唯一的低文档频率单词)与快乐相关联,而不是与爱和乐观相关联,这可能导致较低的精确度和较低的召回率;如果在测试期间发生,那么不可避免地会导致召回率的损失,因为只能返回三个中的一个。这在前面的结果中得到了证实,特别是 SEM11-EN 在第六章朴素贝叶斯中,它的召回率很好,但精确度较差。

本章前面关于 SVM 工作原理的插图显示了一个双分类问题——Rs 和 Bs。有两种明显的方法可以处理扩展到多分类问题(如 CARER 和 WASSA 数据集)。假设我们有 Rs、Bs 和 Gs。我们可以训练三个双分类器,一个用于 R 对 B,一个用于 R 对 G,一个用于 B 对 G(一对多),然后组合结果;或者,我们可以训练另一组双分类器,一个用于 R 对(B 或 G),一个用于 B 对(R 或 G),一个用于 G 对(R 或 B)(一对多)。两者都给出相似的结果,但当有大量类别时,你必须训练 N*(N+1)/2 个分类器(N 为第一个类别与剩余每个类别的组合 + N-1 为第二个类别与剩余每个类别的组合 + ...)用于一对多,但只有 N 用于一对多。例如,对于有六个类别的 CARER,我们不得不训练 21 个分类器并将它们的结果组合起来用于一对多,而对于一对多,我们只需要训练六个分类器并将它们的结果组合起来。

由于所有数据集都有几种可能的输出,我们需要为所有数据集遵循以下策略之一。幸运的是,sklearn.svm.LinearSVC会自动(使用一对多)为存在一系列可能标签的问题执行此操作。然而,仅此并不能解决多标签数据集的问题——从几个选项中抽取一个标签的输出与从几个选项中抽取未知数量标签的输出是有区别的。标准的多分类器,它们被组合为一对一或一对多,将解决第一个问题但不会解决第二个问题。

我们有两种方法可以将我们的 SVM 分类器适应这个问题:

  • 我们可以遵循与朴素贝叶斯分类器相同的策略,即对每个情感的真实值得分进行训练,并使用阈值来确定推文是否满足每个情感。

  • 我们可以训练多个分类器,采用一对多的风格,并简单地接受它们中的每一个的结果。在前面的例子中,如果 R 对(B 或 G)分类器说 R,那么我们接受 R 作为测试案例的一个标签;如果 B 对(R 或 G)说 B,那么我们也接受它同样

我们将在接下来的两个部分中依次探讨这些问题。

使用带有阈值的标准 SVM

要使用朴素贝叶斯处理多标签数据集,我们像这样修改了applyToTweet

    def applyToTweet(self, tweet):        tweet = tweets.tweet2sparse(tweet, self)
        # use predict_log_proba
        p = self.clsf.predict_log_proba(tweet)[0]
        # compare to previously defined threshold
        threshold = numpy.log(self.threshold)
        return [1 if i > threshold else 0 for i in p]

之前的代码使用了predict_log_proba为每个标签返回一个值的事实。在朴素贝叶斯的常规版本中,我们只为每个案例选择得分最高的标签,但使用阈值允许我们选择任意数量的标签,从 0 开始。

这对于 SVM 来说不太适用,因为它们没有名为predict_log_proba的方法。它们所拥有的方法是名为decision_function的方法,它为每个标签生成一个分数。我们不是将applyToTweet的定义改为使用decision_function而不是predict_log_proba,而是在 SVM 的构造函数中简单地将predict_log_proba的值设置为decision_function,然后使用applyToTweet,就像我们之前做的那样。因此,我们必须调整 SVM 的构造函数,如下所示:

    def __init__(self, train, args={}):        self.readTrainingData(train, args=args)
        # Make an sklearn SVM
        self.clsf = sklearn.svm.LinearSVC(max_iter=2000)
        # Get it to learn from the data
        self.clsf.fit(self.matrix, self.values)
        # Set its version of predict_proba to be its decision_function
        self.clsf.predict_proba = self.clsf.decision_function
        # and set its version of weights to be its coefficients
        self.weights = self.clsf.coef_

换句话说,一旦我们制作了底层的 SVM,我们必须设置一些我们将发现有用的标准属性,这些属性在所有sklearn分类器中名称并不相同。对于多标签情况的结果如下:

精确度召回率微观 F1宏观 F1Jaccard
SEM11-EN0.5110.3280.3990.3870.249
SEM11-AR0.5210.2900.3730.3610.229
KWT.M-AR0.1350.6940.2270.1310.128
SEM11-ES0.4340.3380.3800.3610.235

图 7.15 – 使用阈值处理多标签问题的 SVM

SEM11 案例比我们之前查看的简单 SVM 更好,但并不比我们使用早期算法获得的分数更好,而 KWT.M-AR 的分数比简单 SVM 更差。仅仅使用 SVM 为每个标签分配的决策函数值并不能解决多标签数据集的问题。我们将把使用每个标签的值集加上阈值的 SVM 称为 SVM(多)分类器。

制作多个 SVM

第二种选择是制作一系列一对多分类器,并接受所有相关分类器成功的标签。关键是依次处理每个标签,并将训练数据中的N个标签压缩成两个——一个用于目标标签,一个用于所有其他标签。考虑一个被标记为快乐的推文。这个推文作为向量的表示将是[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]——也就是说,在快乐这一列中有一个 1。如果我们将其压缩为快乐与剩余标签的对立,那么它将输出为[1, 0]——也就是说,在新列中快乐这一列有一个 1,而在非快乐这一列中有一个 0。如果我们将其压缩为愤怒与非愤怒的对立,那么它将是[0, 1],在新列中愤怒这一列有一个 0,而在非愤怒这一列中有一个 1。如果它被标记为快乐和爱,那么向量将是[0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0],压缩后的版本将是[1, 1]:第一列中的 1 因为它确实表达了快乐,第二列中的 1 因为它表达了其他东西。

假设我们有一个向量,gs,它代表了一个多标签推文的情感,并且我们想要在I列上压缩它。第一列很简单——我们只需将其设置为gs[i]。要得到第二列,它代表除了I列之外的某些列是否非零,我们使用numpy.sign(sum(gs[:i]+gs[i+1:]))gs[:i]gs[i+1:]是其他列。它们的和如果至少有一个非零,则将大于 0,而取其符号,如果和为 0,则符号为 0,如果大于零,则符号为 1。请注意,gs[i]numpy.sign(sum(gs[:i]+gs[i+1:]))都可能是0,同时它们也可能是1

def squeeze(train, i):# Collapse the Gold Standard for each tweet so that we just
# have two columns, one for emotion[i] in the original and
# one for the rest.
    l = []
    for tweet in train.tweets:
        # now squeeze the Gold Standard value
        gs = tweet.GS
        scores=[gs[i], numpy.sign(sum(gs[:i]+gs[i+1:]))]
        tweet = tweets.TWEET(id=tweet.id, src=tweet.src,
                             text=tweet.text, tf=tweet.tf,
                             scores=scores,
                             tokens=tweet.tokens,
                             args=tweet.ARGS)
        l.append(tweet)
    emotion = train.emotions[i]
    emotions = [emotion, "not %s"%(emotion)]
    return tweets.DATASET(emotions, l, train.df,
                          train.idf, train.ARGS)

MULTISVMCLASSIFIER的构造函数很简单——只需为每个情感创建一个标准的SVMCLASSIFIER。要将其中一个应用于推文,我们必须应用每个标准的,并收集正面的结果。所以,如果训练在快乐与不快乐之间的分类器说某个推文表达了快乐,那么我们就标记这个推文为满足快乐,但我们忽略它关于不快乐的描述,因为不快乐的正分只告诉我们推文也表达了其他某种情感,而我们允许推文表达多种情感:

    def applyToTweet(self, tweet):        k = [0 for i in self.train.emotions]
        for i in self.classifiers:
            c = self.classifiers[i]
            p = c.clsf.predict(tweets.tweet2sparse(tweet, self))[0]
            """
            if classifier i says that this tweet expresses
            the classifier's emotion (i.e. if the underlying
            SVM returns 0) then set the ith column of the
            main classifier to 1
            """
            if p == 0:
                k[i] = 1
        return k

这基本上是标准的一个对多个方法来训练具有多个标签的 SVM。关键的区别在于如何组合单个X与不-X分类器的结果——我们接受所有正面的结果,而标准方法只接受一个。

下表是使用 SVM(多)的多标签问题的表的重复,用于比较:

精确度召回率微观 F1宏观 F1Jaccard
SEM11-EN0.5110.3280.3990.3870.249
SEM11-AR0.5210.2900.3730.3610.229
KWT.M-AR0.1350.6940.2270.1310.128
SEM11-ES0.4340.3380.3800.3610.235

图 7.16 – 多标签数据集,SVM(多)

当我们使用多个 SVM 时,每个标签一个,我们会在每个情况下得到改进,其中 SEM11-EN 的得分目前是最好的:

精确度召回率微观 F1宏观 F1Jaccard
SEM11-EN0.5800.5350.5560.5290.385
SEM11-AR0.5310.4850.5070.4780.340
KWT.M-AR0.6480.4190.5090.3400.341
SEM11-ES0.4980.3680.4230.3780.268

图 7.17 – 多标签数据集,多个 SVM

这比 SVM(多)案例的结果要好,并且是 SEM11-EN 迄今为止最好的结果。对于 SEM11 数据集的 SVM 改进来自于召回率的巨大提升。记住,标准的 SVM 只能对每个数据点返回一个结果,所以当黄金标准包含多个情感时,它的召回率必须很差——如果一个推文有三个与之相关的情感,而分类器只报告一个,那么该推文的召回率是 1/3。KWT.M-AR 的改进来自于精度的提升——如果一个推文没有与之相关的情感,这在数据集中很常见,那么标准的 SVM 必须为它产生一个假阳性。

可以对sklearn.svm.LinearSVC进行多种调整,我们还可以尝试第五章**,情感词典和向量空间模型中的调整方法——例如,使用 IDF 获取特征值,这在整体上产生了一些小的改进。一旦你使用默认值获得了合理的结果,这些方法都值得一试,但尝试各种变体以在给定数据集上获得几个百分点的提升很容易让人沉迷。目前,我们只需注意,即使默认值在数据集中每条推文恰好有一个情感的情况下也能提供良好的结果,对于一些更困难的情况,多 SVM 提供了迄今为止最好的结果。

使用 SVM 可以很容易地被视为从语料库中提取带权重的词典的另一种方式。本章中使用的 SVM 的维度仅仅是词典中的单词,我们可以像第五章**,情感词典和向量空间模型中那样进行相同的操作——使用不同的分词器、词干提取和消除不常见的单词。我们在这里不会重复这些变体:从第五章**,情感词典和向量空间模型我们知道不同的组合适合不同的数据集,简单地运行所有变体不会告诉我们任何新的东西。然而,值得思考的是 SVM 如何使用它们分配给单个单词的权重。

例如,CARER 数据集的 SVM 具有一个 6 行 74,902 列的系数数组:6 行是因为这个数据集中有六个情感,75K 列是因为有 75K 个不同的单词。如果我们随机挑选一些单词,其中一些与某些情感相关,而一些几乎没有情感意义,我们会看到它们对各种情感的权重反映了我们的直觉:

angerfearjoylovesadnesssurprise
sorrow-0.033-0.2330.0140.0260.1190.068
scared-0.5081.392-1.039-0.474-0.701-0.290
disgust1.115-0.293-0.973-0.185-0.855-0.121
happy-0.239-0.2670.546-0.210-0.432-0.080
喜爱0.0000.0000.412-0.060-0.059-0.000
-0.027-0.0080.001-0.008-0.020-0.004
0.001-0.012-0.0040.001-0.002-0.002

图 7.18 – 单词与情感之间的关联,使用 SVM 作为分类器,基于 CARER 数据集

悲伤悲伤紧密相连,害怕恐惧紧密相连,厌恶愤怒紧密相连,快乐喜悦紧密相连,而喜爱喜悦紧密相连(但有趣的是,并不与相连:单词总是带来惊喜);中性词并不与任何特定的情感强烈相连。与第六章**,朴素贝叶斯中的词典不同之处在于,一些单词也强烈反对某些情感——如果你是害怕的,那么你就不是喜悦的,如果你是快乐的,那么你就不是愤怒的、恐惧的或悲伤的。

SVMs 使用这些权重进行分类的方式与第六章**,朴素贝叶斯相同,即如果你给定一个值向量 V = [v0, v1, ..., vn]和一组系数 C = [w0, w1, ..., wn],那么检查V.dot(C)是否大于某个阈值,这正是我们在第六章**,朴素贝叶斯中处理权重时所做的事情(假设 V 和 C 在sklearn.svm.LinearSVC中是稀疏数组,这可能是一种相当快速的方式来计算这个和,但它仍然是同一个和)。唯一的区别在于 SVMs 获取权重的方式以及 SVM 可以为单词分配负权重。我们将在第十章多分类器中返回处理多标签数据集的方法。现在,我们只需注意,SVMs 和简单的基于词典的方法最终会在相同的特征上使用相同的决策函数,但 SVMs 到达这些特征的权重的方式通常更好,在某些情况下要好得多。

摘要

图 7.17显示了迄今为止我们看到的最佳分类器,以及每个数据集的 Jaccard 分数:

LEXCP (未分词)CP (分词)NB (单)NB (多)SVM (单)SVM (多)MULTI-SVM
SEM4-EN0.4970.5930.5930.7750.778****0.8450.836
SEM11-EN0.3480.3520.3530.2270.2670.2240.249****0.385
WASSA-EN0.4370.5120.5050.7090.707****0.7700.749
CARER-EN0.3500.4140.3950.7760.7740.770****0.796
IMDB-EN0.6670.7210.7220.738****0.7400.7360.736
SEM4-AR0.5090.4930.5130.531****0.5320.5140.494
SEM11-AR****0.3860.3700.3820.2360.2740.2160.2290.340
KWT.M-AR0.663****0.6840.6660.4940.5070.6310.1280.341
SEM4-ES****0.4200.1910.1770.3600.3310.4120.336
SEM11-ES0.2710.276****0.2780.2300.2550.2260.2350.268

图 7.19 – 每个数据集的最佳分类器

如我们所见,不同的分类器与不同的数据集配合得很好。这里的主要教训是,你不应该仅仅接受存在一个最佳的分类算法:进行实验,尝试不同的变体,并亲自看看哪种算法对你的数据效果最好。还值得注意的是,多标签数据集(SEM11-EN、SEM11-AR、SEM11-ES 和 KWT.M-AR)在使用简单的 SVM 时得分非常低,唯一一个多 SVM 获胜的是 SEM11-EN,来自第五章的简单算法,情感词典和向量空间模型在其他情况下仍然产生最佳得分。

参考文献

要了解更多关于本章所涉及的主题,请查看以下资源:

  • Bennett, K. P., & Bredensteiner, E. J. (2000). Duality and Geometry in SVM Classifiers. Proceedings of the Seventeenth International Conference on Machine Learning, 57–64.

  • Boser, B. E., Guyon, I. M., & Vapnik, V. N. (1992). A Training Algorithm for Optimal Margin Classifiers. Proceedings of the Fifth Annual Workshop on Computational Learning Theory, 144–152. doi.org/10.1145/130385.130401.

  • Graham, R. L. (1972). An efficient algorithm for determining the convex hull of a finite planar set. Information Processing Letters, 1(4), 132–133. doi.org/10.1016/0020-0190(72)90045-2.

第八章:神经网络和深度神经网络

第七章中,支持向量机,我们了解到支持向量机SVMs)可以根据包含的单词对推文进行情感分类。我们还能使用什么仅仅查看现有单词的方法吗?在本章中,我们将考虑使用神经网络来实现这一目的。神经网络是一种通过为节点网络分配权重并传播一组初始值直到达到输出节点来执行计算的方法。输出节点的值将代表计算的结果。当神经网络在 20 世纪 40 年代被引入时,它们被设想为人类大脑执行计算的方式的模型(Hebb,1949 年)(McCulloch & Pitts,1943 年)。这种网络不再被认真视为人类大脑的模型,但有时通过这种方式可以实现的结果可以非常令人印象深刻,尤其是在输入和输出之间的关系难以确定时。一个典型的神经网络有一个输入层的节点、一组隐藏层和一个输出层,通常通过连接同一层或下一层的节点来实现。

我们将首先探讨没有隐藏层的简单神经网络的用途,并研究几个相关参数变化的影响。与第六章和第七章中的算法一样,神经网络的标准应用旨在为每个输入特征集产生一个单一值;然而,正如第六章中提到的朴素贝叶斯算法第六章朴素贝叶斯通过为每个潜在的输出标签分配一个分数来实现这一点,因此我们可以轻松地将其适应于一条推文可以具有任意数量标签的情况。到本章结束时,你将清楚地了解神经网络如何执行计算,以及添加隐藏层如何使网络能够计算单隐藏层无法计算的功能。你还将了解它们如何用于为我们数据集中的推文分配标签。

在本章中,我们将涵盖以下主题:

  • 单层神经网络及其作为分类器的应用

  • 多层神经网络及其作为分类器的应用

单层神经网络

通常,一个神经网络由一组节点组成,这些节点按层组织,它们之间有连接。一个简单神经网络SNN)仅仅有一个与分类所依据的特征相对应的输入层,以及一个与可能结果相对应的输出层。在最简单的情况下,如果我们只想知道某物是否属于指定的类别,将只有一个输出节点,但在我们的情况下,由于有多个可能的结果,我们将有多个输出节点。一个 SNN 看起来可能如下所示:

图 8.1 – 单层神经网络,其中输入是单词,输出是情感

图 8.1 – 单层神经网络,其中输入是单词,输出是情感

节点之间的链接每个都有一个权重,并且不在输入层的每个节点都有一个偏差。权重和偏差本质上与<mml:math  >mml:miA</mml:mi>mml:mn1</mml:mn>mml:mi</mml:mi>mml:mix</mml:mi>mml:mn1</mml:mn>mml:mo+</mml:mo>mml:miA</mml:mi>mml:mn2</mml:mn>mml:mi</mml:mi>mml:mix</mml:mi>mml:mn2</mml:mn>mml:mo+</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo+</mml:mo>mml:miA</mml:mi>mml:min</mml:mi>mml:mi*</mml:mi>mml:mix</mml:mi>mml:min</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:math>方程中的权重和常数项相同,该方程我们用来定义第七章中的支持向量机的分隔超平面。

将此类网络应用于需要分类的推文非常简单:只需将推文中每个单词(每个活动输入节点)相关的权重相乘,计算这些权重的总和,并将其加到连接节点的偏差上:如果是正的,则将其设置为连接节点的激活;否则,将激活设置为 0。训练此类网络更具挑战性。基本思想是查看输出节点。如果一个输出节点正在执行训练数据所说的操作,那么就没有什么需要做的(毕竟,如果所有输出节点在所有训练数据上都按预期执行,那么分类器就会训练得尽可能好)。如果不是这样,那么进入它的连接中肯定有问题。有两种可能性:节点在应该关闭时打开,或者在应该打开时关闭。假设它在应该关闭时打开。它打开的唯一原因可能是从导致它的活动节点到它的链接上的权重总和大于其阈值,因此为了阻止它打开,这些链接上的权重都应该稍微减小。

同样,如果一个节点在应该开启时关闭,那么进入该节点的活动节点的权重应该略微增加。请注意,在这两种情况下,调整的是来自活动节点的链接——非活动节点不能帮助它们连接的节点开启,因此改变它们链接上的权重没有任何效果。权重应该增加或减少多少,以及何时进行这种变化,对结果的准确性和获得结果所需的时间有重大影响。如果你改变权重太多,这个过程可能无法收敛,或者可能收敛到一个次优配置;如果你改变得太少,那么收敛可能需要非常长的时间。这个过程通过逐渐改变权重来驱动网络重现训练数据,被称为梯度下降,反映了目标是在权重和阈值的空间中将网络向下移动以获得最小的总体误差。

在神经网络最初的演示中,这个过程是通过网络反向传播的,以便在输出层之前的层的连接上的权重也根据它们对输出的总体贡献进行调整,然后是之前的层,以此类推,直到达到输入层(Rumelhart 等人,1986 年)。对于具有许多隐藏层的网络,这样做可能会非常慢,早期层的变化非常小——有时几乎可以忽略不计。因此,神经网络的用途因此被限制在相当浅的网络,直到人们意识到可以通过训练一个具有 N-1 层的网络,然后添加另一个层并微调得到的网络来训练一个具有 N 个隐藏层的网络(Hinton 等人,2006 年)。这意味着你可以通过训练一个没有隐藏层的网络,然后在其输出层之前添加一个新层并重新训练这个网络(它有一个隐藏层),然后在其输出层之前添加另一个新层并重新训练这个网络(它有两个隐藏层),然后添加另一个新层,使用这个新层重新训练(它有三个隐藏层)。这种策略使得基于早期层捕获的泛化假设稳健,从而后来的错误不会对早期层产生重大影响,从而能够训练复杂的网络成为可能。

实现训练算法(一旦网络训练完成,实际应用只有一种方式)的策略有很多,同样,训练算法和将训练好的网络应用于任务的机制也有很多种。对于非常大的数据集,使用可以并行运行的实现可能是个好主意,但我们的数据稀疏性意味着sklearn.neural_network.MLPClassifier的实现可以在合理的时间内运行。我们不会尝试每个数据集上所有可能的特征组合。与 SVMs(尤其是)一样,有无数个设置和参数可以调整,很容易陷入尝试各种变体以期望获得几个百分点的改进的希望中。我们将研究一些更显著选择的影响,但我们将主要关注考虑特征的使用方式,而不是训练过程的细节。我们将从考虑一个 SNN(即只有一个输入层和一个输出层的神经网络,如图 8**.1所示)开始。

SNN 只是一个作为SVMCLASSIFIER子类的DNNCLASSIFIER(它们有几个共享属性,我们可以通过这样做来利用)。如果我们指定默认情况下DNNCLASSIFIER应该没有隐藏层,那么我们就有了 SNN 的构造函数。初始化代码遵循与其他SKLEARNCLASSIFIER(包括SVMCLASSIFIER)相同的模式——使用readTrainingData读取训练数据并将其放入标准格式,然后调用sklearn的分类算法实现并将其拟合到训练数据:

class DNNCLASSIFIER(svmclassifier.SVMCLASSIFIER):    def __init__(self, train=None,
                 args={"hiddenlayers":()}):
       args0 = ARGS({"N":500, "wthreshold":5,
                      "useDF": False,
                      "max_iter":sys.maxsize,
                      "solver":"sgd", "alpha": 1e-5})
       for k in args:
            args0[k] = args[k]
       self.readTrainingData(train, N=args0["N"])
       # making a multi-layer classifier requires a
       # lot of parameters to be set
       self.clsf = MLPClassifier(solver=args["solver"],
                   alpha=args["alpha"],
                   max_iter=args["max_iter"],
                   hidden_layer_sizes=args["hiddenlayers"],
                   random_state=1)
       self.clsf.fit(self.matrix, self.values)

我们正在使用来自sklearnsklearn.neural_network.MLPClassifier包。这个包接受大量参数,这些参数控制网络的形状以及权重计算和使用的方式。一如既往,我们不会进行实验来观察这些参数的变化如何影响我们任务上的性能。我们的目标是看看基本算法对我们来说效果如何,所以我们将主要使用这些参数的默认值。一旦我们确定了算法在一般情况下效果如何,可能值得调整参数,但由于所有这些算法的性能在很大程度上取决于数据集的性质,这可以留到以后再考虑。

与迄今为止的所有分类器一样,构造函数训练模型:基于 sklearn 的这些分类器,这总是涉及使用readTrainingData将数据转换为标准形式,创建指定类型的模型,并调用self.clsf.fit(self.matrix, self.values)来训练它。应用训练好的模型涉及应用从抽象BASECLASSIFIER类继承的applyToTweets方法,如第五章中所述,情感词典和 向量空间模型

在我们的数据集上尝试,我们得到以下结果。(CARER 的结果是通过在 440K 中的 70K 上训练获得的。训练神经网络比其他算法慢得多。我们将在稍后研究训练大小、准确性和时间之间的关系,但到目前为止,只需注意 CARER 数据集上的准确性似乎在 70K 左右开始趋于平稳,因此我们可以用它与其他算法进行比较):

精确度召回率微观 F1宏观 F1Jaccard
SEM4-EN0.9020.9020.9020.9020.822
SEM11-EN0.6480.2750.3860.3880.239
WASSA-EN0.8370.8370.8370.8370.720
CARER-EN0.9010.9010.9010.9010.820
IMDB-EN0.8850.8850.8850.8850.793
SEM4-AR0.6700.6700.6700.6700.504
SEM11-AR0.5960.2600.3620.3700.221
KWT.M-AR0.0350.1260.0550.0340.028
SEM4-ES0.5410.4720.5040.4090.337
SEM11-ES0.4840.2900.3620.3610.221

图 8.2 – 应用于标准数据集的简单神经网络

在前表中,对于两个大型数据集的分数是目前为止最好的,但其他都略差于我们使用朴素贝叶斯和 SVMs 所达到的效果。提高此算法性能的明显方法是使用深度神经网络。深度神经网络已经在许多任务上显示出比简单神经网络(SNNs)更好的性能,因此有理由期待它们在这里也会有所帮助。然而,当你开始使用具有隐藏层的网络时,有大量的选项可以选择,在尝试添加隐藏层之前,查看非隐藏层版本如何处理所提供的数据是值得的。我们是否想要一个大小是输入层一半的隐藏层?我们是否想要 50 个大小为 15 的隐藏层?鉴于训练具有隐藏层的神经网络可能非常缓慢,在我们开始进行任何实验之前,考虑隐藏层想要做什么是一个好主意。

我们将首先研究参数变化的影响,例如训练数据的大小、输入特征的数量和迭代次数。即使是没有任何隐藏层的神经网络训练也可能相当缓慢(参见图 8.3),研究训练数据的变化如何影响训练时间和准确性是值得的:如果我们发现有一种方法可以在保持合理性能水平的同时减少训练时间,那么使用这种方法而不是完整的无限制训练集可能是有价值的。

我们可以观察三个明显的事情:

  • 准确性和训练时间如何随着训练集的大小变化?

  • 准确性和训练时间如何随着输入特征数量(即单词)的变化而变化?

  • 准确度和训练时间是如何随着迭代次数变化的?

我们将从观察准确度(以 Jaccard 分数报告)和训练时间如何随着训练集大小变化开始。以下图表展示了 CARER 数据集(我们数据集中最大的一个)的这些数据,其他因素保持不变(仅使用最频繁的 10K 个单词,最多进行 1K 次迭代):

图 8.3 – Jaccard 分数和训练时间(以秒为单位)与 CARER-EN 数据集的训练大小对比

图 8.3 – Jaccard 分数和训练时间(以秒为单位)与 CARER-EN 数据集的训练大小对比

很明显,Jaccard 分数在大约 40K 条推文后趋于平稳,而训练时间似乎呈上升趋势。将曲线拟合到 Jaccard 图上并不容易——多项式曲线不可避免地会开始下降趋势,而对数曲线则不可避免地会在某个点增加到 1 以上——然而,简单的检查应该能给你一个合理的想法,即添加额外数据将停止产生性能的有用提升。

接下来要变化的是字典的大小。由于输入层由推文中出现的单词组成,移除不频繁出现的单词可能会加快速度,而对准确度的影响不大:

图 8.4 – Jaccard 分数和训练时间(以秒为单位)与 CARER-EN 数据集的字典大小对比

图 8.4 – Jaccard 分数和训练时间(以秒为单位)与 CARER-EN 数据集的字典大小对比

CARER-EN 数据集包含 16.7K 个单词,但 Jaccard 分数在大约 1K 到 2K 之间趋于平稳。由于训练时间随着输入特征数量的增加而或多或少呈线性增加,因此检查添加新单词对准确度影响甚微的点是有意义的。

我们可以变化的第三件事是迭代次数。神经网络训练涉及对权重和阈值进行一系列调整,直到无法实现进一步的改进。我们进行的迭代越多,训练时间越长,但准确度往往会在找到最佳结果之前开始趋于平稳。以下图表显示了 SEM4-EN 数据集随着迭代次数增加,训练时间和 Jaccard 分数的变化。在 1,800 次迭代后,该数据集没有进一步的改进,所以我们在这个点停止了绘图。不出所料,训练时间与迭代次数呈线性增长,而 Jaccard 分数在大约 1,400 次迭代时开始趋于平稳:

图 8.5 – Jaccard 分数和训练时间与迭代次数对比(SEM4-EN 数据集)

图 8.5 – Jaccard 分数和训练时间与迭代次数对比(SEM4-EN 数据集)

调整训练数据的大小、输入特征的数量和迭代次数会影响分数和训练时间。当你在开发模型并尝试不同的参数组合、设置和预处理步骤时,进行一些初步调查以找到使 Jaccard 分数似乎趋于平稳的这些因素的值无疑是值得的。但最终,你只能咬紧牙关,使用大量的训练数据、大型字典和大量的迭代来训练模型。

多层神经网络

我们已经看到,如果我们愿意等待,使用 SNN 至少在某些情况下可以产生比之前任何算法更好的结果。对于许多问题,添加额外的隐藏层可以比只有输入层和输出层的网络产生更好的结果。这能帮助我们当前的任务吗?

SNNs 计算的信息与朴素贝叶斯和 SVMs 计算的信息非常相似。输入节点和输出节点之间的链接携带了关于输入节点(即词汇)与输出节点(即情感)之间相关性有多强的信息,以及偏差大致携带了关于给定输出可能性有多大的信息。以下表格显示了在 CARER 数据集上训练后,几个常见词汇与情感之间的链接:

愤怒恐惧快乐悲伤惊讶
-0.036-0.0650.0310.046-0.0150.036
悲伤0.002-0.028-0.0980.0980.0630.020
害怕-0.3561.792-0.683-0.283-0.5620.057
快乐-0.090-0.1610.936-0.332-0.191-0.156
讨厌0.048-0.014-0.031-0.0450.020-0.000
-0.001-0.0330.0140.015-0.0310.022
爱慕-0.054-0.034-0.1100.218-0.0850.007
激怒1.727-0.249-0.558-0.183-0.621-0.124
-0.004-0.041-0.0410.120-0.038-0.001

图 8.6 – CARER-EN 数据集中词汇与情感之间的联系

以下表格显示了与情感联系最强和最弱的词汇:

愤怒愤怒、贪婪、匆忙、怨恨、自私 ... 热情、支持、奇怪、惊人、怪异
恐惧不确定、犹豫、颤抖、不安全、脆弱 ... 惊吓、支持、甜蜜、紧张
快乐自满、真诚、振奋、快乐、积极 ... 无助、焦虑、怪异、奇怪、压倒
淫荡、同情、温柔、淘气、喜欢 ... 惊人、压倒、讨厌、奇怪、怪异
悲伤负担、思乡、不安、腐烂、内疚 ... 甜蜜、焦虑、怪异、奇怪
惊讶印象深刻、震惊、惊讶、好奇、好奇 ... 感觉、做、非常、存在、或

图 8.7 – CARER-EN 数据集中每种情感的强度和最弱词汇

给定一组输入词(一条推文!),神经网络计算这些词到每个情绪的链接总和,并将其与阈值进行比较(不同神经网络实现执行的计算略有不同:这里使用的,修正线性激活(Fukushima,1969)计算输入和偏差的加权和,但如果是负数则将其设置为零)。这与所有其他算法所做的是非常相似的——SVMs 也计算输入的加权和,但不会将负结果重置为零;基于词典的算法也只计算加权和,但由于没有负权重,总和不可能小于零,因此没有必要重置它们。朴素贝叶斯结合各种观察事件的条件概率来产生一个整体概率。它们共同的特点是单个词 总是 对总和做出相同的贡献。这并不总是正确的:

  • 有些词的唯一任务就是改变其他词的含义。考虑一下 happy 这个词。这个词与 喜悦 相关联,而不是与其他任何情绪相关:
愤怒恐惧喜悦悲伤
happy-0.077-0.1590.320-0.048

图 8.8 – SEM4-EN 中“喜悦”与四种情绪之间的链接

然而,当 happynot 同时出现在推文中时,并不表达喜悦:

这有点糟糕,但我的兄弟即将加入警察学院……而我并不高兴。而且我并不是 唯一一个.*

Yay bmth canceled Melbourne show fanpoxytastic just lost a days pay and hotel fees not happy atm # sad # angry

我刚才被电话保持通话 20 分钟直到我挂断。# 不高兴 # 糟糕的服务 # 不高兴 @ virginmedia 我应该留下来……

not 的存在并不改变这些推文的含义,以至于它们表达的不是喜悦。

并非所有影响其他词含义的词都像 not 那样容易识别,尤其是在非正式文本中,缩写词如 don’tcan’t 非常常见,但肯定还有其他词做类似的事情。还应注意,被修饰词的含义可能不会与修饰词相邻。

  • 一些单词可以形成复合词,这些复合词表达的意义与单个单词孤立时的意义并不直接相关。我们之前在中文复合词中看到了这一点,但英语单词也可以这样做。使用点互信息来寻找复合词(如第五章**,情感词典和向量空间模型),我们发现 supporting castsweet potatoes 在 CARER-EN 数据集中比根据单个单词的分布预期出现的频率要高得多——也就是说,这些术语可以被视为复合词。以下表格给出了单个单词的权重,其中 supportingsweet 都与 有强烈的联系,与 喜悦 的联系则稍微弱一些。复合词本身并不预期会有这些联系——土豆本身并没有什么特别可爱或喜悦的地方!使用 SNN 或任何早期算法都无法捕捉到这些单词与 castpotatoes 共现时对包含它们的文本整体情感电荷的不同贡献:
愤怒恐惧喜悦悲伤惊讶
支持性-0.183-0.1540.2200.515-0.319-0.043
cast-0.0150.017-0.0120.0030.006-0.009
甜味-0.177-0.1870.2070.553-0.371-0.079
土豆-0.0090.0030.0040.003-0.020-0.019

图 8.8 – 可以作为复合词出现的单个单词的权重

  • 一些单词纯粹是模糊的,一个解释带有一种情感电荷,另一个解释则带有不同的情感电荷(或没有)。仅通过查看包含该单词的文本,很难检测到一个单词是否模糊,更不用说检测它有多少种解释了,即使你知道一个单词有多少种解释,你仍然必须决定在给定的文本中哪种解释是意图的。因此,推断每种解释的情感电荷并决定哪种解释是意图的几乎是不可行的。然而,在某些情况下,我们可以看到,就像之前的复合词案例一样,两个单词意外地经常共现,在这种情况下,我们可以合理地确信在每个情况下都意图了相同的解释。"Feel like" 和 "looks like" 例如,在 SEM4-EN 数据中比预期的出现频率要高:这两个词都可能具有歧义,不同的含义带有不同的情感电荷。但似乎非常可能,在每个 "feel like" 的出现中,"feel" 和 "like" 的相同解释被意图——实际上,这些短语中 "like" 的解释并不是与 紧密相关的那个。

我们迄今为止看到的所有算法,包括 SNN,都是原子性地处理单个单词的贡献的——它们都为每个单词和每种情绪计算一个分数,然后使用一些相当简单的算术计算来组合这些分数。因此,它们不能对这里提出的问题敏感。

在我们的神经网络中添加额外的层将使我们能够处理这些现象。添加层如何使神经网络能够计算 SNN 无法处理的内容的最简单示例是 XOR 函数,其中我们有两个输入,我们希望如果其中一个输入而不是两个都处于激活状态时,得到一个响应。

这不能用 SNN 来完成。我们将通过考虑一组由喜欢仇恨震惊以及愤怒快乐惊讶组成的虚构推文来探讨这一原因,以及 DNN 如何通过考虑这些推文来克服这一限制,如图 8.9 所示:

IDtweetjoyangersurprise
1love100
2like100
3love like100
4hate010
5shock001
6love100
7like100
8love like110
9hate010
10shock001

图 8.9 – 直接的训练数据

如果我们在这些数据上训练一个 SNN,我们将得到以下网络:

图 8.10 – 用于惊讶、愤怒和快乐的 SNN,以及直接的训练数据

图 8.10 – 用于惊讶、愤怒和快乐的 SNN,以及直接的训练数据

仇恨愤怒的关联最强,从震惊惊讶的关联最强,而从喜欢快乐的关联最强。因此,如果一条推文包含这些词中的任何一个,它将触发相应的情绪。如果一条推文同时包含喜欢,它也会触发快乐,但训练数据并没有说明如果一条推文包含例如震惊喜欢震惊仇恨,会发生什么。观察网络,我们可以看到仇恨愤怒的投票相当强烈,而震惊惊讶的投票大约相同,但震惊愤怒的投票远比对惊讶的投票要强。因此,总的来说,震惊仇恨投票给惊讶。这里并没有什么有意义的事情发生:网络是用随机值初始化的,这些随机值溢出到训练数据中未见过的特征配置的随机决策中。

如前所述,我们的 SNN 执行的操作本质上与 SVM 相同:如果一组输入节点<mml:math  >mml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:mo…</mml:mo>mml:mo,</mml:mo>mml:mo,</mml:mo>mml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub></mml:math>与输出节点<mml:math  >mml:miO</mml:mi></mml:math>之间的权重<mml:math  >mml:miw</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:miO</mml:mi></mml:mrow></mml:mfenced>mml:mo,</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo,</mml:mo>mml:miw</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:miO</mml:mi></mml:mrow></mml:mfenced></mml:math><mml:math  >mml:miw</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:miO</mml:mi></mml:mrow></mml:mfenced>mml:mo,</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo,</mml:mo>mml:miw</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:miO</mml:mi></mml:mrow></mml:mfenced></mml:math>以及输出节点的偏置<mml:math  >mml:mib</mml:mi><mml:mfenced separators="|">mml:mrowmml:miO</mml:mi></mml:mrow></mml:mfenced></mml:math>,那么如果<mml:math  >mml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo,</mml:mo>mml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub></mml:math>的输入值为 v(N 1), ..., v(N k),那么输出节点的激发由<mml:math  >mml:miv</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub></mml:mrow></mml:mfenced>mml:mo×</mml:mo>mml:miw</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:miO</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo+</mml:mo>mml:miv</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub></mml:mrow></mml:mfenced>mml:mo×</mml:mo>mml:miw</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:miO</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mib</mml:mi><mml:mfenced separators="|">mml:mrowmml:miO</mml:mi></mml:mrow></mml:mfenced></mml:math><mml:math  >mml:miv</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub></mml:mrow></mml:mfenced>mml:mo×</mml:mo>mml:miw</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:miO</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo.</mml:mo>mml:mo+</mml:mo>mml:miv</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub></mml:mrow></mml:mfenced>mml:mo×</mml:mo>mml:miw</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:miO</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mib</mml:mi><mml:mfenced separators="|">mml:mrowmml:miO</mml:mi></mml:mrow></mml:mfenced></mml:math>决定,如果这个和是负的,输出节点的激发将是<mml:math  >mml:mn0</mml:mn></mml:math>,如果是正的,则在一定程度上是正的。<mml:math  >mml:miv</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub></mml:mrow></mml:mfenced>mml:mo×</mml:mo>mml:miw</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:miO</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mo…</mml:mo>mml:mo+</mml:mo>mml:miv</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub></mml:mrow></mml:mfenced>mml:mo×</mml:mo>mml:miw</mml:mi><mml:mfenced separators="|">mml:mrowmml:msubmml:mrowmml:miN</mml:mi></mml:mrow>mml:mrowmml:mik</mml:mi></mml:mrow></mml:msub>mml:mo,</mml:mo>mml:miO</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo>mml:mib</mml:mi><mml:mfenced separators="|">mml:mrowmml:miO</mml:mi></mml:mrow></mml:mfenced></mml:math>确定了一个将点分为两类<mml:math  >mml:miO</mml:mi></mml:math><mml:math  >mml:min</mml:mi>mml:mio</mml:mi>mml:mit</mml:mi><mml:mfenced separators="|">mml:mrowmml:miO</mml:mi></mml:mrow></mml:mfenced></mml:math>的超平面,就像 SVM 中的系数一样。

但这意味着,如果类别不是线性可分的,SNN 就无法进行分类。这里的经典例子是 XOR 函数——也就是说,每个特征单独表示一个特定的类别,但两个特征组合在一起则不表示——即 XOR(0, 0)=0XOR(0, 1)=1XOR(1, 0)=1,和 XOR(1, 1)=0。画出这个函数并展示它看起来似乎没有线能分开 0 和 1 的情况是很容易的。在 图 8*.11 中,红色圆圈(位于 (0, 0) 和 (1, 1))代表 XOR 为 0 的情况,而蓝色钻石(位于 (1, 0) 和 (0, 1))代表 XOR 为 1 的情况:

图 8.11 – XOR – 蓝色钻石和红色圆圈无法用直线分开

图 8.11 – XOR – 蓝色钻石和红色圆圈无法用直线分开

看起来似乎不可能画出一条线来将蓝色钻石和红色圆圈分开——也就是说,这两个类别看起来似乎不是线性可分的。

对于这个结论的正式证明,假设存在这样一条线。它将有一个类似于 <mml:math  >mml:miA</mml:mi>mml:mo×</mml:mo>mml:mix</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo×</mml:mo>mml:miy</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo=</mml:mo>mml:mn0</mml:mn></mml:math> 的方程,其中,对于任何位于线上的点,<mml:math  >mml:miA</mml:mi>mml:mo×</mml:mo>mml:mix</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo×</mml:mo>mml:miy</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math> ,而对于任何位于线下的点,<mml:math  >mml:miA</mml:mi>mml:mo×</mml:mo>mml:mix</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo×</mml:mo>mml:miy</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo<</mml:mo>mml:mn0</mml:mn></mml:math> (如果 B 是正数,反之亦然)。

根据我们的四个点,假设红色圆圈都在线以下,蓝色菱形都在线以上,且 B 为正值。那么,对于位于(0, 0)的红色圆圈,我们会有<mml:math  >mml:miC</mml:mi>mml:mo<</mml:mo>mml:mn0</mml:mn></mml:math>,因为将xy都设为 0 会得到<mml:math  >mml:miA</mml:mi>mml:mo×</mml:mo>mml:mn0</mml:mn>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo×</mml:mo>mml:mn0</mml:mn>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo<</mml:mo>mml:mn0</mml:mn></mml:math>),这使得 C<0。同样地,对于位于(1, 1)的红色圆圈,将xy都替换为 1 会得到<mml:math  >mml:miA</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo<</mml:mo>mml:mn0</mml:mn></mml:math>,对于位于(1, 0)的蓝色菱形,将x替换为 1,将y替换为 0 会得到<mml:math  >mml:miA</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math>,而对于位于(1, 0)的蓝色菱形,我们会得到<mml:math  >mml:miB</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math>

<mml:math  >mml:miC</mml:mi>mml:mo<</mml:mo>mml:mn0</mml:mn></mml:mn></mml:math><mml:math  >mml:miA</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math>,我们得到 <mml:math  >mml:miA</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math>,同样从 <mml:math  >mml:miC</mml:mi>mml:mo<</mml:mo>mml:mn0</mml:mn></mml:mn></mml:math><mml:math  >mml:miB</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math>,我们得到 <mml:math  >mml:miB</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math>。但随后 <mml:math  >mml:miA</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo></mml:mo>mml:miA</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo+</mml:mo>mml:mn2</mml:mn>mml:mo×</mml:mo>mml:miC</mml:mi></mml:math>,因此由于 <mml:math  ><mml:mfenced separators="|">mml:mrowmml:miA</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi></mml:mrow></mml:mfenced>mml:mo+</mml:mo><mml:mfenced separators="|">mml:mrowmml:miB</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi></mml:mrow></mml:mfenced>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math>,那么 <mml:math  >mml:miA</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo></mml:mo>mml:mn0</mml:mn></mml:math>,这与观察到的点 (1, 1) 处 <mml:math  >mml:miA</mml:mi>mml:mo+</mml:mo>mml:miB</mml:mi>mml:mo+</mml:mo>mml:miC</mml:mi>mml:mo<</mml:mo>mml:mn0</mml:mn></mml:math> 相矛盾。我们确实必须考虑蓝色菱形都在线上方,而红色圆圈都在下方的可能性,或者 B 是负数的可能性,但一个完全相同的论证以同样的方式排除了这些可能性——无法画出一条直线来区分蓝色菱形和红色圆圈。

这对我们任务意味着什么?假设我们调整我们的虚构数据如下:

IDtweetjoyangerSurprise
1love100
2like100
3love like010
4hate010
5shock001
6love100
7like100
8love like010
9hate010

图 8.12 – 难以训练的数据

我们唯一做的改变是,我们让包含喜欢的推文表达愤怒而不是快乐。这与之前 XOR 的情况非常相似,其中两个特征在单独出现时表达一种情绪,而在一起出现时表达另一种情绪。没有与 XOR 的(0, 0)点完全相同的情况,但在两种特征都不存在的情况下,目标要么是愤怒(如果推文只是单词),要么是惊讶(如果推文只是单词震惊)——也就是说,当喜欢都不存在时,推文不表达快乐

当我们在这种数据上训练时,我们会发现 SNN 不能被依赖来找到分配正确情绪的权重。有时它做到了,有时没有。问题不在于没有一组权重可以分配正确的标签。在 90%训练/10%测试的 10 次折叠运行中,在两种情况下找到了正确分割数据的权重,但在另外八次中,分类器将喜欢都包含的推文分配了错误的情绪。

在这里显示的错误训练的网络中,包含这两个单词的推文的快乐、愤怒和惊讶的分数分别为 0.35、-0.25 和-3.99,快乐是明显的赢家。数据线性可分的,因为正确训练的分类器通过使用由连接权重和偏差定义的超平面将数据正确地分为正确的类别;然而,梯度下降过程很容易陷入局部最小值,在单词推文上做到最好,但无法找到复合词的正确权重:

图 8.13 – 正确训练的网络

图 8.13 – 正确训练的网络

图 8.14 – 错误训练的 SNN

图 8.14 – 错误训练的 SNN

因此,我们有两种类型的问题:

  • 如果数据不可线性分离,那么没有任何 SNN 可以正确分类它

  • 即使它可以被一组超平面分割,SNN 也容易陷入局部最小值,在大多数数据上做到最好,但无法找到当单词在一起时与单独出现时不同的效果的权重

我们可以通过添加额外的层来解决第一个问题。例如,为了计算异或(XOR),网络中需要一个在两个输入节点都开启时开启,并且对输出节点有负链接的节点。一个简单的前馈网络应该将输入层的节点连接到第一隐藏层的节点,然后第一隐藏层的节点连接到第二隐藏层的节点,依此类推,直到最后一隐藏层的节点连接到输出层的节点。你需要至少一个包含至少三个节点的隐藏层。然而,正如我们所看到的,网络很容易陷入局部最小值,而可以可靠训练以识别异或的最小配置只有一个隐藏层,包含五个节点:

图 8.15 – 训练用于将只包含快乐或喜欢但不包含两者的推文分类为喜悦的深度神经网络

图 8.15 – 训练用于将只包含快乐或喜欢但不包含两者的推文分类为喜悦的深度神经网络

如果喜欢是开启的但快乐是关闭的,那么第二个隐藏节点将开启,得分为 1.22(即-0.766+1.988),这将传播到输出节点,得分为-2.96(1.22*-2.46)。然后,这将添加到输出节点的偏差中,产生-2.15。如果快乐是开启的但喜欢不是,那么第二个和第四个隐藏节点将开启,得分分别为 0.76 和 1.23,这将传播到输出节点,第二个隐藏节点为-0.7660.76(对于第二个隐藏节点)加上-1.3091.23,当加上输出节点的偏差时,变为-2.66。如果两个输入节点都开启,那么没有隐藏节点会开启,因此输出节点的得分只是其自身的偏差,即 0.81。对于只有一个输出的网络,用于解释最终得分的标准逻辑函数将负数视为开启,正数视为关闭,因此这个网络将只包含喜欢快乐的推文分类为表达喜悦,而包含两者的推文则不表达喜悦。

添加隐藏单元将使网络能够识别输入特征的显著组合为非组合性,即组合的效果不仅仅是特征本身的累积效果。我们还可以看到,如果你没有足够的隐藏特征,那么训练过程可能会陷入局部最小值——尽管你可以仅使用隐藏层中的三个特征来计算 XOR,但训练这样一个网络来完成这项任务是非常困难的(参见(Minsky & Papert, 1969)对该问题的进一步讨论)。这不仅仅是没有足够的数据,或者没有允许网络训练足够长的时间的问题。具有三个节点的隐藏层的网络会非常快地收敛(大约在 10 或 12 个 epoch 之后),而具有四个节点的网络只需要几百个 epoch。我们还可以添加额外的层——具有两个隐藏层,每个层有四个和三个节点的网络也可以解决这个问题,并且通常比具有一个隐藏层和五个节点的网络收敛得更快:

图 8.16 – 解决 XOR 问题的具有两个隐藏层的网络

图 8.16 – 解决 XOR 问题的具有两个隐藏层的网络

问题在于,在小网络中随机设置初始权重和偏差几乎总是让你处于搜索空间的一个区域,最终会陷入局部最小值。另一方面,使用较大的网络,几乎总是会产生能够解决问题的新网络,因为将会有节点位于搜索空间的正确部分,可以赋予它们越来越重要的意义,但它们需要更长的时间来训练。因此,关键任务是找到合适的隐藏单元数量。

隐藏单元的作用是在孤立状态下和与其他单词组合时,找到向输出节点输入不同单词的单词。这里的关键参数似乎很可能是输入特征的数量(即数据中的不同单词数量)和输出类别的数量(即情绪的数量)。如果词典中有更多的单词,那么就有更多的单词组合可能,这意味着可能需要更多的隐藏节点。如果输出类别更多,那么就有更多的地方可能需要单词组合来发挥作用。

考虑到我们的数据集的词典中有成千上万的单词,但只有四到十一个情绪,似乎有道理首先研究将隐藏节点数与情绪数相关联的影响。图 8.17显示了当我们有一个隐藏层,其节点数是情绪数的 0.5 倍、1 倍或 1.5 倍时会发生什么:

图 8.17 – Jaccard 分数与隐藏节点数的关系 = F*情绪数,F 从 0.5 到 5

图 8.17 – Jaccard 分数与隐藏节点数的关系 = F*情绪数,F 从 0.5 到 5

对于我们使用 SNN 获得相当好结果的三个数据集,添加一个具有适度节点数量的隐藏层的效果是显著的。为了便于参考,这里重复了原始分数:

精确率召回率微观 F1宏观 F1Jaccard
SEM4-EN0.9020.9020.9020.9020.822
SEM11-EN0.6480.2750.3860.3880.239
WASSA-EN0.8370.8370.8370.8370.720
CARER-EN0.9010.9010.9010.9010.820

图 8.18 – 应用于标准英语数据集的简单神经网络

CARER-EN 的原始 Jaccard 分数为 0.77,相当于大约 0.87 的准确率;当我们添加一个节点数量是 CARER 中情感数量一半的隐藏层(即,由于 CARER 有六个情感,隐藏层中只有三个节点)时,我们得到了比原始分数更好的分数(Jaccard 0.79,准确率 0.89),然后当我们增加隐藏节点的数量到 6、9、12 时,我们得到了非常渐进的改进,直到分数似乎已经趋于平稳,甚至可能开始过拟合。

与 SEM4-EN 和 WASSA-EN 类似,但更为明显的是,当隐藏层中的节点数量只有情感数量的一半时(即,这两个都是两个节点),分数开始相当低,但一旦隐藏层中的节点数量与情感数量相同,分数就会显著提高,并且对于 SEM4-EN 在 Jaccard 0.875(准确率 0.93)左右趋于平稳,对于 WASSA-EN 在 Jaccard 0.81(准确率 0.9)左右趋于平稳。总的来说,看起来添加一个具有适度节点数量的隐藏层可以在没有隐藏单元的基本神经网络之上产生一些改进,但更多隐藏层或单个具有更多节点的隐藏层的实验表明,这些改进相当有限。这很可能是由于隐藏层寻找非组合性的单词组合。这种有限效果可能有两个原因:

  • 简单来说,并不是所有单词的情感权重在它们与特定伙伴共现时都会发生变化

  • 在这些组合存在的情况下,它们在数据中的频率不足以改变它们的正常解释

可能是使用更多的训练数据使得使用具有多个或大型隐藏层的网络更加有效,但在数据集规模较小的情况下,这样做的影响相对较小。

摘要

在本章中,我们探讨了使用神经网络来识别非正式通讯(如推文)中表达的情感的任务。我们研究了数据集的词汇表如何作为输入层的节点,并考察了与单个单词相关的权重如何反映这些单词的情感意义。我们考虑了没有隐藏层的简单神经网络,以及具有一个隐藏层且节点数量略多于输出节点集的稍微深一点的神经网络——一旦隐藏层包含的节点数量是输出层的 1.5 到 2 倍,神经网络的性能就会趋于平稳,因此似乎没有必要做得更复杂。

对于各种数据集,得分最高的算法如下:

**LEX (**未分词)**LEX (**分词)**CP (**分词)**NB (**多标签)**SVM (**单标签)MULTI-SVM**SNN (**单标签)DNN
SEM4-EN0.5030.4970.5930.7780.8450.829****0.847
SEM11-EN0.3470.3480.3530.2670.224****0.3850.2420.246
WASSA-EN0.4450.4370.5050.707****0.7700.7370.752
CARER-EN0.3500.3500.3950.7740.770****0.8200.804
IMDB-EN0.7220.6670.7220.7400.736****0.793****0.793
SEM4-AR0.5060.5090.513****0.5320.5140.5040.444
SEM11-AR0.378****0.3860.3820.2740.2160.3400.2210.207
KWT.M-AR****0.6870.6630.6660.5070.6310.3410.0280.026
SEM4-ES****0.4250.4200.1770.3310.4120.3370.343
SEM11-ES0.2690.271****0.2780.2550.2260.2680.2210.222

图 8.19 – 目前最佳算法的得分

在 10 个数据集中,神经网络在 4 个数据集上产生了最佳结果,但对于多标签数据集,简单的词汇算法仍然是最好的。一般的教训与第七章末尾的“支持向量机”相同,即你不应该仅仅接受存在一个最佳分类算法:进行实验,尝试不同的变体,并亲自看看什么最适合你的数据。

参考文献

要了解更多关于本章涉及的主题,请参阅以下资源:

  • 冈山,K. (1969). 通过多层模拟阈值元件网络进行视觉特征提取. IEEE 系统科学与控制论杂志,5(4),322–333. doi.org/10.1109/TSSC.1969.300225.

  • 赫布,D. O. (1949). 行为组织:一种神经心理理论. 威廉姆斯出版社。

  • 辛顿,G. E.,奥斯因德罗,S.,与 Teh,Y.-W. (2006). 深度信念网的快速学习算法. 神经计算,18(7),1527–1554. doi.org/10.1162/neco.2006.18.7.1527.

  • McCulloch, W. S., & Pitts, W. (1943). 神经活动中内在思想的逻辑演算. 数学生物物理学通报, 5(4), 115–133. doi.org/10.1007/BF02478259.

  • Minsky, M., & Papert, S. (1969). 感知器. 麻省理工学院出版社.

  • Rumelhart, D. E., Hinton, G. E., & Williams, R. J. (1986). 通过反向传播错误学习表示. 自然, 323(6088), 533–536. doi.org/10.1038/323533a0.

第九章:探索 Transformer

Transformer 是一种全新的机器学习模型,它彻底改变了人类语言的处理和理解方式。这些模型可以分析大量数据,以前所未有的准确性发现和理解复杂模式,并在诸如翻译、文本摘要和文本生成等任务上产生人类难以获得的见解。

Transformer 强大之处在于它们可以处理大量数据,并从先前示例中学习以做出更好的预测。它们已经“彻底改变”了(有意为之)NLP,并在许多 NLP 任务中优于传统方法,迅速成为行业最佳实践。

在本章中,我们将介绍 Transformer,讨论它们的工作原理,并查看一些关键组件。然后,我们将介绍 Hugging Face,并展示它是如何帮助我们完成任务,然后再介绍一些有用的现有 Transformer 模型。我们还将展示如何使用 Hugging Face 实现 two 模型,并使用 Transformer。

本章将演示如何构建 Transformer 模型,同时引导您完成重要步骤,例如将 Google Colab 连接到 Google Drive 以持久保存文件,准备数据,使用自动类,并最终构建可用于分类的模型。

我们将涵盖以下主题:

  • Transformer 简介

  • 数据如何在 Transformer 中流动

  • Hugging Face

  • 现有模型

  • 用于分类的 Transformer

  • 实现 Transformer

让我们首先更深入地了解 Transformer,它们的发明者以及它们是如何工作的。

Transformer 简介

在本章中,我们将简要介绍 Transformer。在语言和 NLP 任务中,上下文起着至关重要的作用——也就是说,要知道一个词的含义,必须考虑关于情况(即上下文)的知识。在 Transformer 出现之前,序列到序列模型被用于许多 NLP 任务。这些模型通过一次预测一个单词来生成输出序列,并将源文本编码以获取上下文知识。然而,语言的问题在于它们是复杂、流动且难以转化为严格的基于规则的结构的。上下文本身也很难追踪,因为它通常位于所需位置(即许多单词、句子或甚至段落)很远的地方。为了解决这个问题,序列到序列模型通过使用具有某种有限形式的记忆的神经网络来工作:

图 9.1 – 序列到序列模型与 Transformer 对比

图 9.1 – 序列到序列模型与 Transformer 对比

关于变压器的普遍论文《Attention Is All You Need》由 Vaswani 等人于 2017 年发表。他们提出了一种新的神经网络架构,称为变压器,可用于 NLP 任务。从图 9.2 中可以看到,变压器有几个组件,包括“编码器”(在左侧),“解码器”(在右侧),以及重复N次的注意力和前馈组件块:

图 9.2 – 来自 Vaswani 等人撰写的《Attention Is All You Need》论文

图 9.2 – 来自 Vaswani 等人撰写的《Attention Is All You Need》论文。

变压器由编码器和解码器层组成;每个通常有多个相同的实例(例如,原始研究论文中的六个),并且每个都有自己的权重集。在左侧,编码器的任务是转换输入序列为一组连续表示。在右侧,解码器使用编码器的输出以及前一时间步的输出来生成输出序列。架构中的每个堆叠的第一个编码器和解码器都有嵌入层和位置编码作为输入。每个编码器包含一个自注意力层,该层计算不同单词之间的关系,以及一个前馈层。每个解码器也包含一个前馈层,但它有两个自注意力层。最后一个编码器的输出被用作第一个解码器的输入。这些组件结合使变压器架构更快、更高效,并允许它处理更长的序列,使得单词之间的分隔变得无关紧要。因此,该架构可以优于其他更传统的其他方法。

Vaswani 等人描述的变压器架构是为了翻译而创建的。在训练期间,编码器接收一种语言的输入(即句子,例如英语),而解码器接收相同输入(即句子)的预期目标语言(例如法语)。编码器中的注意力层利用输入句子中的每个单词,但编码器是顺序操作的,并且只能关注翻译文本中的单词(即当前正在生成的单词之前的单词)。例如,如果已经预测了翻译目标的前N个单词,这些单词将被输入到解码器中,解码器使用编码器的所有输入来预测N+1位置的单词。

解码器提供了整个目标句子,但被限制不能使用即将到来的单词。因此,在预测一个单词时,解码器不能参考目标句子中它之后的任何单词。例如,在预测Nth 个单词时,只有位置1N-1的单词可以被注意力层考虑。这个限制对于确保任务对模型来说足够具有挑战性以获得知识至关重要。

变换器模型中的数据流动

在本节中,我们将更深入地探讨数据是如何在变换器模型中流动的。理解数据在变换器中的流动方式,以及将原始输入转换为有意义的输出的步骤,对于理解其功能和潜力至关重要。变换器能够高效且有效地对数据中的长距离依赖关系进行建模,使其能够高度捕捉上下文和语义。通过探索变换器内部的数据流动机制,我们将更深入地理解其处理和理解语言的能力。我们首先将查看输入嵌入。

输入嵌入

从左侧开始,编码器的输入是源文本中的单词标记。这种文本数据必须使用 GloVe 或 Word2Vec 等方法(以及其他方法)转换为数值表示(根据作者的说法,大小为 512),以便进行转换。

位置编码

然后将一个位置元素添加到这些嵌入中。这很重要,因为它允许变换器发现关于单词之间距离和单词顺序的信息。然后,这些信息被传递到第一个编码器块的自注意力层。

注意

位置编码不会改变向量维度。

编码器

每个编码器内部都有几个子层:

  • 多头注意力:这允许变换器同时关注输入序列的不同部分,从而提高其输入处理能力,并使其能够获得更多上下文并做出更明智的决策。这是架构中最重要的部分,也是计算成本最高的部分。当处理输入中的单词时,自注意力将输入中的每个单词与每个其他单词相关联。考虑变换器如何决定哪组权重将产生最佳结果是非常有趣的。目标是使与句子中某些方式相关的单词的注意力值很大,反之亦然。例如,让我们看看这个句子:“天气非常晴朗。”

单词weathersunny是相关的,因此应该生成较高的关注值;相反,Thewas的关注值应该较小。如前所述,变压器是在嵌入上进行训练的。因此,变压器将从它们中学习,并能够产生所需的向量,以便单词产生与单词相关性的关注值。此外,除了考虑单个含义之外,自注意力机制根据单词的重要性及其与其他单词的关系对输入单词进行不同的加权。这使得它能够处理上述长距离上下文问题,从而在 NLP 任务上实现更好的性能。简而言之,关注值是使用三个矩阵计算的,每个矩阵中的每一行代表一个输入单词。重要的是要注意,这些行中的值是由模型学习的,以便生成所需的输出。让我们依次查看这些重要的矩阵:

  • 查询:每一行对应于输入单词的嵌入。换句话说,查询词是正在计算关注值的特定单词。

  • :模型正在将其与查询比较的输入文本中的每个单词——即被关注的单词——以计算其对查询词的重要性。

  • :模型试图根据查询和键矩阵之间的比较来生成的信息。键和值矩阵可以是相同的。

给定这些矩阵,通过计算查询和键矩阵的点积来获得关注值。然后使用这些值来加权值矩阵,从而有效地允许模型“学习”输入文本中应该关注的单词。

  • 加和归一化:这些层由一个残差连接层后面跟一个归一化层组成。就我们的目的而言,重要的是要知道它们有助于解决梯度消失问题并提高模型性能。

  • 前馈神经网络:这是一个处理注意力向量输入并将它们转换为与输入相同维度的形式的神经网络,可以输入到下一层。这些注意力向量彼此独立;因此,在这个阶段可以使用并行化,而不是像在序列到序列架构中那样按顺序处理它们。

现在让我们继续讨论解码器。

解码器

编码器的输出被用作解码器堆栈中每个解码器的第二层的输入。

与编码器类似,掩码多头注意力接受一个输出嵌入和一个位置嵌入。变换器的目标是学习如何根据输入和所需的输出生成输出。

注意

在训练过程中,所需的输出(例如,翻译)被提供给解码器。

一些单词被遮蔽,以便模型可以学习如何预测它们。这些单词在每次迭代中都会改变。解码器会处理这些单词,以及来自编码器的编码表示,以生成目标序列。

然而,在预测过程中,使用一个空序列(带有特殊的句子开始)标记)。这被转换为一个嵌入;添加位置编码后用作解码器的输入。解码器和其他层的工作方式与之前相同,但输出序列的最后一个单词被用来填充输入序列的第一个空白,因此输入现在是和第一个预测的单词。这再次被送入解码器,并重复这个过程,直到句子的结尾。

线性层

解码器的输出被用作这个线性层的输入,这是一个简单的全连接神经网络,它生成下一层的向量。

Softmax 层

Softmax 层将输入转换为概率分布——也就是说,它将一组数字转换为总和为 1 的正面数字,对较高值赋予更高的权重,对较小值赋予较低的权重。

输出概率

最后,输出概率是目标单词标记。变压器将这个输出与来自训练数据的目标序列进行比较,并使用它通过反向传播来改进结果。

现在我们已经了解了变压器的工作原理,在下一节中,我们将简要地看看一个组织是如何使基于变压器的模型的实施和实验变得简单,使其对所有用户都易于访问。

Hugging Face

变压器需要大量的数据才能有效并产生良好的结果。此外,还需要巨大的计算能力和时间;最好的模型通常使用多个 GPU 进行训练,可能需要几天(甚至更长)的时间来完成训练。因此,并不是每个人都能负担得起训练这样的模型,通常这由像谷歌、Facebook 和 OpenAI 这样的大型企业来完成。幸运的是,已经有预训练的模型可供使用。

Hugging Face(以微笑脸和展开双手的 emoji 命名)与变换器、模型和 NLP 同义。Hugging Face (huggingface.co)提供了一个用于发布预训练变换器(和其他)模型的存储库。这些模型可以免费下载并用于广泛的 NLP 任务。此外,如果任务涉及具有独特命名法、术语和特定领域语言的领域,则可以对模型进行“微调”以提高模型在该特定领域的性能。微调是一个使用预训练模型的权重作为起点,并使用新的特定领域数据来更新它们的过程,从而使模型在特定领域任务上表现得更好。除了发布模型外,Hugging Face 还提供了一项服务,允许模型进行微调、训练以及更多操作。

Hugging Face 还提供了一个 Python 库,为 NLP 任务提供了一个高级接口。该库提供了一系列最先进的预训练模型,包括 BERT、GPT、RoBERTa、T5 等(见下一节)。可以使用以下命令进行安装:

pip install transformers

除了下载预训练模型外,该库还可以用于下载标记化器。这两者都可以与您的数据集一起使用,以微调分类等任务,以创建最先进的 NLP 系统。

总结来说,Hugging Face 的transformers库是一个强大的工具,用于处理 NLP 模型,使得与变换器模型的工作变得简单。它具有直观的设计和广泛的模型选择,非常值得一看。

现有模型

驱动于计算、存储和数据容量的提升,变换器在全球范围内掀起了一场风暴。一些更著名的预训练模型包括以下内容:

  • 来自变换器的双向编码器表示 (BERT): 由谷歌 AI 团队创建,并在大量的文本数据语料库上进行训练,BERT 考虑了每个单词左右两边的上下文。

  • 高效学习准确分类标记的编码器 (ELECTRA): ELECTRA 使用生成器-判别器模型来区分生成的文本和真实文本。生成器被训练生成与真实文本相似的文本,而判别器被训练区分真实文本和生成文本。

  • 生成式预训练变换器 3 (GPT-3): 由 OpenAI 开发,并在广泛的互联网文本上进行预训练,GPT-3 拥有 1750 亿个参数,是目前可用的最大模型之一。

  • NVIDIA 训练的大型变换器模型 (Megatron): 由 NVIDIA 开发,Megatron 可扩展,可以在数百个 GPU 上训练,因此可以使用更大的模型。

  • 鲁棒优化 BERT (RoBERTa): 基于 BERT,RoBERTa 通过使用更大的训练语料库和更多的训练步骤来学习更鲁棒的文字表示,旨在改进 BERT。

  • 文本到文本迁移转换器T5):由 Google 开发,T5 将 NLP 问题视为“文本到文本”问题。它在未标记和标记数据上进行了训练,然后针对各种任务进行单独微调。

  • 具有额外长上下文的转换器Transformer-XL):该模型引入了一个记忆模块,使得模型能够更好地处理和理解长期依赖关系。

  • XLNet(广义自回归预训练):由 Google 开发,XLNet 从 Transformer-XL 和 BERT 中吸取了最佳元素,并建立了所有输入词之间的依赖关系。

在下一节中,我们将更详细地探讨转换器是如何为我们感兴趣的分类任务进行训练的:灵感来源于 Hugging Face 页面。

用于分类的转换器

转换器模型被训练为语言模型。这是一种通过分析人类语言模式来理解和生成人类语言的算法类型。

它们了解语法、句法和语义,并能辨别单词和短语之间的模式和联系。此外,它们可以检测命名实体,如个人、地点和机构,并解释它们被引用的上下文。本质上,转换器模型是一个使用统计模型来分析和生成语言的计算机程序。

语言模型是在大量文本数据上以自监督的方式进行训练的,例如书籍、文章和在线内容,以学习单词和短语之间的模式和关系。用于预训练转换器的流行数据集包括 Common Crawl、Wikipedia 和 BooksCorpus。例如,BERT 总共使用了大约 35 亿个单词进行训练,其中大约 25 亿来自 Wikipedia,大约 10 亿来自 BooksCorpus。这使得模型能够预测在给定单词序列之后出现某个单词或短语的可能性。预训练的大型语言模型的输出通常涉及基于输入文本的预测。模型可能会输出某些单词或短语在句子中接下来使用的概率,预测给定输入词后最可能跟随的词,或者根据输入文本生成整个句子或段落。输出可用于各种目的,如文本生成、翻译、情感分析等。

自监督学习是一种机器学习方法,模型学习从未标记的数据中提取有用的信息,而不需要任何明确的标签或监督。相反,模型是在预测图像缺失部分或重建损坏句子等任务上训练的。因此,这类模型对其训练过的语言有了理解——但仅从统计的角度来看。然而,这种方法在日常任务中缺乏实用性,因此,必须通过使用针对当前任务的人类标注标签的监督微调来定制通用的预训练模型。

预训练(即从头开始训练一个模型)需要大量的数据,因此这个过程可能需要数周或数月。然后,在预训练模型上进行微调,因此需要预训练的语言模型来进行微调。本质上,微调是一个进一步训练的步骤,使用适合任务的数据库集。

微调模型通常调整模型预训练层的权重,以更好地适应新的数据集或任务。微调的过程包括初始化预训练层的权重,然后在新的数据集或任务上对整个模型进行训练。在训练过程中,预训练层的权重会随着新添加的层权重一起更新,使模型能够从新的数据集中学习更细微的特征,同时保留从预训练模型中学到的知识。预训练层权重在微调过程中更新的程度取决于新任务的特定情况和可用数据量。在某些情况下,只有新添加层的权重被更新,而在其他情况下,预训练层的权重可能被显著更新。另一种选择是除了最后一层之外的所有层都保持固定,其权重在训练过程中被修改。因此,在微调过程中使用这些技术,并使用较小的学习率,通常会产生性能提升。有时,这伴随着在旧架构之上添加新层,从而保持旧固定权重,只允许新层的权重被改变。

但微调时究竟发生了什么?有各种技术,但一般的想法是,早期层学习与实际任务(例如,分类)无关的通用模式,而后期层学习与任务相关的模式。这种直觉已经被各个研究团队所验证。

注意

在梯度下降计算过程中,每次迭代的步长大小由学习率决定,整体目标是找到损失函数的最小值。

在实际应用中,对于分类任务,我们会下载一个如BertForSequenceClassification的模型——这是一个用于句子分类的 BERT 模型,它包含一个线性层。因此,最后一层生成一个概率向量,表示输入序列每个潜在类标签的概率。

简而言之,微调模型使其能够将学习到的特征适应新的任务或数据集,这可能导致性能提升。

在本章前面,我们探讨了模型的各个部分,并看到了模型具有编码器和解码器块。根据任务,这些部分中的每一个都可以单独使用。对于分类任务,建议使用仅编码器模型。更多细节,有一些很好的 Packt 书籍可供参考,例如 Denis Rothman 的《自然语言处理中的变压器》。

在下一节中,我们将使用训练数据集微调模型,在测试数据集上进行一些预测,并评估结果。

实现变压器

在本节中,我们将通过代码实现针对单情感和多情感数据集的变压器的实现。我们将使用Google ColaboratoryColab),因为它通过提供一个强大的基于云的环境,预装了库和资源,从而简化了变压器的实现。因此,让我们先看看这一点。

Google Colab

Google Colab 是一个免费的笔记本环境服务,它在云端运行(colab.research.google.com)。使用 Colab 有许多好处;例如,它允许开发者快速开始编程,无需担心设置,它允许与没有正确软件安装的本地用户共享代码,并且它与 GitHub 很好地集成。然而,最大的优势之一是 Google 提供了免费的 GPU 访问。机器学习的核心涉及大量的数学运算——这是 GPU 擅长的。从实际应用的角度来看,即使是具有小型训练数据集的简单模型,GPU 和非 GPU 系统之间的节省时间也可以是数小时(例如,10 分钟与 10 小时相比)。

尽管如此,还有一些注意事项。Colab 是短暂的——换句话说,上传到会话或由会话生成(例如,结果)的文件(例如,数据文件)最终会消失。解决这个问题的方法是上传文件到 Google Drive,并允许 Colab 访问它们。

在 Colab 上的调试也比通过 VS Code 等工具要繁琐一些。它涉及到安装和导入ipdb(IPython 启用 Python 调试器)包:

!pip install -Uqq ipdbimport ipdb

断点对开发者很有用,这些可以通过代码设置,以使调试器停止:

ipdb.set_trace()

我们可以使用命令行参数来控制调试器,如下所示:

  • c:继续执行

  • n:移动到下一行

  • r:继续执行,直到当前函数返回

可以使用以下命令全局关闭调试:

%pdb off

也可以使用以下命令全局打开调试:

%pdb on

现在我们已经知道了什么是转换器,它是如何工作的,以及如何实现它,让我们使用 Colab 在 Python 中实现一个转换器来分类前几章中介绍的数据集。

单一情感数据集

我们将实现两个转换器来满足两种不同类型的数据集。让我们从单一情感任务开始。总的来说,我们将遵循以下步骤:

  1. 安装必要的库。

  2. 导入必要的库。

  3. 提供对 Google Drive 的访问权限。

  4. 创建数据集和模型变量。

  5. 加载数据集并准备。

  6. 标记数据集。

  7. 加载用于分类的模型。

  8. 设置训练器参数。

  9. 训练模型。

  10. 使用训练好的模型进行预测。

  11. 评估。

对单一情感推文进行分类是一个相对容易的任务,所以让我们从这里开始。

让我们从安装一些库开始:

!pip install datasets!pip install evaluate
!pip install transformers

这些库用于轻松访问数据集、评估模型的输出结果以及访问 Hugging Face 上可用的预训练模型。我们现在可以将它们导入到我们的代码中:

import datasetsfrom datasets import load_dataset
from enum import Enum
import evaluate
from evaluate import evaluator
import numpy as np
from sklearn.metrics import jaccard_score
from transformers import (
    AutoModelForSequenceClassification,
    AutoTokenizer,
    Pipeline,
    Trainer,
    TrainingArguments
)
import pandas as pd
from pathlib import Path
from google.colab import drive

如前所述,我们希望一次性上传我们的训练和测试文件,并在需要时按需访问它们,因此让我们让 Colab 获取我们的 Google Drive 访问权限,这是文件上传的地方。实际上,其中一些文件已经通过 datasets 库可用,但为了现在,我们假设我们想从我们的存储库中访问它们:

drive.mount("/content/gdrive/", force_remount=True)BASE_PATH = "/content/gdrive/MyDrive/PacktBook/Data/C9"

注意

您应将 BASE_PATH 替换为您自己的路径。

现在,我们应该设置一些事情来使我们的任务更容易。不同的数据集需要不同的参数,因此可以使用 enum 来控制代码的执行流程。我们必须命名我们的文件,使文件名包含文件内推文的语言代码(即 ARESEN),然后使用 enum 和文件名来设置在代码中有用的变量:

class Dataset(Enum):  SEM4_EN=1
  WASSA_EN=2
  CARER_EN=3
  SEM4_AR=4
  SEM4_ES=5
  IMDB_EN=6

现在,我们还将设置一个变量,例如 NUM_LABELS,以告诉模型有多少个标签。稍后,我们将看到我们不需要这样做:

# set the required dataset hereds = Dataset.SEM4_EN
NUM_LABELS = 4
COLS = 'ID', 'tweet', 'label'

现在,我们可以使用 enum 来设置一些特定于数据集的变量。这样,当我们想尝试其他数据集时,我们只需要修改 ds 变量:

if (ds == Dataset.SEM4_EN):  training_file = "SEM4_EN_train.csv"
  test_file = "SEM4_EN_dev.csv"
elif (ds == Dataset.WASSA_EN):
  training_file = "WASSA_train.csv"
  test_file = "WASSA_dev.csv"
elif(ds == Dataset.CARER_EN):
  training_file = "CARER_EN_train.csv"
  test_file = "CARER_EN_dev.csv"
  NUM_LABELS = 6
elif(ds == Dataset.SEM4_ES):
  training_file = "SEM4_ES_train.csv"
  test_file = "SEM4_ES_dev.csv"
  NUM_LABELS = 5
elif(ds == Dataset.SEM4_AR):
  training_file = "SEM4_AR_train.csv"
  test_file = "SEM4_AR_dev.csv"
elif(ds == Dataset.IMDB_EN):
  NUM_LABELS = 2
  training_file = "IMDB_EN_train.csv"
  test_file = "IMDB_EN_dev.csv"

我们还必须设置 model_name 以告诉程序使用哪种特定语言的模型:

# select a modelif "_AR_" in training_file:
  model_name = "asafaya/bert-base-arabic"
elif "_EN_" in training_file:
  model_name = "bert-base-cased"
elif "_ES_" in training_file:
  model_name = "dccuchile/bert-base-spanish-wwm-cased"

然后,我们可以设置各种文件路径变量:

# add the base pathtraining_file = f"{BASE_PATH}/{training_file}"
test_file = f"{BASE_PATH}/{test_file}"

最后,我们必须设置一个名为 stub 的变量,我们将用它来保存我们的模型:

# get file name for savingstub = (Path(training_file).stem)

Hugging Face 的 transformers 库与 datasets 库配合得很好。因此,接下来,我们将加载数据文件,删除任何不需要的列,并创建一个 DatasetDict 对象,该对象将在管道的后续部分中使用:

def get_tweets_dataset():  data_files = {"train": training_file, "test": test_file}
  ds = datasets.load_dataset("csv", data_files=data_files,
                             delimiter=",",
                             encoding='utf-8')
  ds_columns = ds['train'].column_names
  drop_columns = [x for x in ds_columns if x not in COLS]
  ds = ds.remove_columns(drop_columns)
  dd = datasets.DatasetDict({"train":ds["train"],
                             "test":ds["test"]})
  return dd
dataset = get_tweets_dataset()

接下来,我们必须创建一个函数,该函数将存储在dataset变量中的训练集和测试集中的推文进行分词。简单来说,分词器的工作是准备数据,使其准备好输入到模型中。它是通过将句子拆分成单词(标记)然后将单词拆分成片段(例如,flyfishing会被拆分成flyfishing)来完成的。然后这些标记通过查找表拆分成 ID(数字)。通常,你会使用与你使用的模型关联的分词器。例如,对于bert-base-cased模型,你会使用BertTokenizer。然而,在下面的代码中,我们使用了名为AutoTokenizer的东西。AutoTokenizer是一个通用的分词器自动类,它自动从 Hugging Face 分词器库中获取正确的分词器类以及与模型分词器相关的数据。一个自动类是一个通用类,通过自动根据其名称找到预训练模型的架构来简化编码过程。我们只需要为我们的任务选择合适的AutoModel。本质上,它们更加灵活,使编程变得稍微简单一些:

tokenizer = AutoTokenizer.from_pretrained(model_name)def tokenise_function(tweets):
    return tokenizer(tweets["tweet"],
                     padding="max_length",
                     truncation=True,
                     max_length = 512)
tokenised_datasets = dataset.map(tokenise_function, batched=True)

现在是有趣的部分!我们需要加载一个用于分类的模型。和之前一样,我们可以使用一个专门为句子分类训练的特定 BERT 模型,比如BertForSequenceClassification。然而,我们选择使用一个自动类来获取文本分类模型。在这种情况下,由于我们正在对文本进行分类,我们使用了AutoModelForSequenceClassification作为AutoModel。我们只需提供模型的名称和我们正在处理的标签数量——库会处理其余部分:

model = AutoModelForSequenceClassification.from_pretrained(    model_name,
    num_labels=NUM_LABELS)
training_args = TrainingArguments(output_dir=f"{stub}")

我们现在准备训练模型,但首先,我们需要设置一些参数来指定我们希望训练器执行的操作。我们可以通过简单地创建一个TrainingArguments实例来实现,告诉它将模型保存在哪里,以及我们希望在每个 epoch 结束时进行评估。这些参数通过Trainer传递,包括模型和训练集以及测试集。现在,调用训练并等待结果就变得简单了。注意我们如何保存生成的模型:

training_args = TrainingArguments(    output_dir=f"{stub}",
    evaluation_strategy="epoch")
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenised_datasets["train"],
    eval_dataset=tokenised_datasets["test"],
)
trainer.train()
trainer.save_model(stub)

如果一切顺利,你应该会看到类似以下的内容(截断):

The following columns in the training set don't have a corresponding argument in `BertForSequenceClassification.forward` and have been ignored: ID, tweet. If ID, tweet are not expected by `BertForSequenceClassification.forward`,  you can safely ignore this message./usr/local/lib/python3.8/dist-packages/transformers/optimization.py:306: FutureWarning: This implementation of AdamW is deprecated and will be removed in a future version. Use the PyTorch implementation torch.optim.AdamW instead, or set `no_deprecation_warning=True` to disable this warning
  warnings.warn(
***** Running training *****
  Num examples = 3860
  Num Epochs = 3
  Instantaneous batch size per device = 8
  Total train batch size (w. parallel, distributed & accumulation) = 8
  Gradient Accumulation steps = 1
  Total optimization steps = 1449
  Number of trainable parameters = 108313348
 [1449/1449 19:47, Epoch 3/3]
Epoch  Training Loss  Validation Loss
1  No log  0.240059
2  0.555900  0.210987
3  0.208900  0.179072
Training completed. Do not forget to share your model on huggingface.co/models =)
Saving model checkpoint to SEM4_EN_train
Configuration saved in SEM4_EN_train/config.json
Model weights saved in SEM4_EN_train/pytorch_model.bin

注意

本章中使用的算法和程序都会随机化数据的一些方面,尤其是网络内部节点的权重初始分配,因此,你在相同数据上运行相同脚本获得的结果可能与文本中的结果略有不同。

现在我们已经对我们的数据集进行了微调的模型,我们可以看到它在我们测试数据集上的表现如何:

predictions = trainer.predict(tokenized_datasets["test"])

最后,我们可以设置一个度量字典并遍历它们,边计算边打印:

model_predictions = np.argmax(predictions.predictions,    axis=1)
model_predictions = model_predictions.tolist()
model_references = tokenised_datasets["test"]["label"]
measures = [
              ["precision" , "macro"],
              ["recall" , "macro"],
              ["f1" , "micro"],
              ["f1" , "macro"],
              ["jaccard" , "macro"],
              ["accuracy" , None],
            ]
for measure in measures:
  measure_name = measure[0]
  average = measure[1]
  if measure_name = = "jaccard":
    results = get_jaccard_score(references = model_references,
    predictions = model_predictions,average = average)
  else:
    metric = evaluate.load(measure_name)
    if measure_name=="accuracy":
      results = metric.compute(references = model_references,
      predictions = model_predictions)
    else:
      results = metric.compute(references = model_references,
        predictions = model_predictions, average = average)
  print(measure_name, average, results[measure_name])

这将生成类似以下的内容:

precision macro 0.9577305808563304recall macro 0.9592563645499727
f1 micro 0.9576446280991735
f1 macro 0.9576513771741846
jaccard macro 0.9192365565992706
accuracy None 0.9576446280991735

模型的结果总结在以下表格中:

数据集精确度召回率micro F1macro F1Jaccard
SEM4-EN0.9620.9640.9620.9620.927
WASSA-EN0.8550.8610.8550.8560.753
CARER-EN0.8810.9210.9270.8960.816
SEM4-AR0.8170.8370.8430.8250.710
SEM4-ES0.7910.7860.8070.7870.663
IMDB-EN0.9050.9050.9050.9050.826

表 9.1 – 单标签数据集基于 transformer 模型的分数

这里的大部分分数都优于我们在这本书早期使用分类器获得的分数,尽管 WASSA-EN 和 CARER-EN 的最佳分类器仍然是单类 SVM。SEM4-AR 和 SEM4-ES 的分数都显著优于之前的分数,这可能是由于预训练模型在寻找根和可能进行消歧方面做得比我们在早期章节中使用的简单词干提取器要好。从复杂的 DNN(如 transformer)中提取中间结果非常困难,因此分析为什么这种类型的某个分类器比另一个分类器表现更好比之前章节中更困难,但似乎这在这些情况下是一个关键因素。

多情绪数据集

现在,让我们构建一个用于分类多标签推文的 transformer 模型。大部分代码是相似的,所以我们将不会重新展示,而是专注于多分类问题的有趣部分。我们将遵循以下步骤:

  1. 安装必要的库。

  2. 导入必要的库。

  3. 提供对 Google Drive 的访问权限。

  4. 创建数据集变量。

  5. 将数据集转换为DatasetDict

  6. 加载并准备数据集。

  7. 对数据集进行分词。

  8. 加载用于分类的模型。

  9. 定义度量函数。

  10. 设置训练器参数。

  11. 训练模型。

  12. 评估。

让我们开始吧!

我们必须像以前一样安装和导入库,并且像以前一样允许访问 Google Drive。现在,让我们从在线存储库中获取数据文件。然而,KWT 文件在 Google Drive 中,因此我们需要一些代码来加载并将这些转换为DatasetDict对象:

def get_kwt_tweets_dataset(code):  if code == "KWTM":
    training_file = "train-KWT-M.csv"
    test_file = "test-KWT-M.csv"
  else:
    training_file = "train-KWT-U.csv"
    test_file = "test-KWT-U.csv"
  # add the base path
  training_file = f"{BASE_PATH}/{training_file}"
  test_file = f"{BASE_PATH}/{test_file}"
  data_files = {"train": training_file, "validation": test_file}
  ds = datasets.load_dataset("csv", data_files=data_files,
        delimiter=",",encoding='utf-8')
  dd = datasets.DatasetDict(
                            {"train":ds["train"],
                             "validation":ds["validation"]
                            })
  return dd

我们的Dataset枚举现在也反映了我们正在处理不同的文件,因此让我们使用enum来获取正确的数据文件并设置模型:

class Dataset(Enum):  SEM11_AR=1
  SEM11_EN=2
  SEM11_ES=3
  KWT_M_AR=4
  KWT_U_AR=5
ds = Dataset.SEM11_EN
if (ds == Dataset.SEM11_AR):
  dataset = load_dataset("sem_eval_2018_task_1",
    "subtask5.arabic")
  model_name = "asafaya/bert-base-arabic"
elif (ds == Dataset.SEM11_EN):
  dataset = load_dataset("sem_eval_2018_task_1",
    "subtask5.english")
  model_name = "bert-base-cased"
elif(ds == Dataset.SEM11_ES):
  dataset = load_dataset("sem_eval_2018_task_1",
    "subtask5.spanish")
  model_name = "dccuchile/bert-base-spanish-wwm-cased"
elif(ds == Dataset.KWT_M_AR):
  dataset = get_tweets_dataset("KWTM")
  model_name = "asafaya/bert-base-arabic"
elif(ds == Dataset.KWT_U_AR):
  dataset = get_tweets_dataset("KWTU")
  model_name = "asafaya/bert-base-arabic"

数据集有三种类型:训练集、测试集和验证集。我们将使用训练集和验证集:

DatasetDict({    train: Dataset({
        features: ['ID', 'Tweet', 'anger', 'anticipation',
        'disgust', 'fear', 'joy', 'love', 'optimism',
        'pessimism', 'sadness', 'surprise', 'trust'],
        num_rows: 6838
    })
    test: Dataset({
        features: ['ID', 'Tweet', 'anger', 'anticipation',
        'disgust', 'fear', 'joy', 'love', 'optimism',
        'pessimism', 'sadness', 'surprise', 'trust'],
        num_rows: 3259
    })
    validation: Dataset({
        features: ['ID', 'Tweet', 'anger', 'anticipation',
        'disgust', 'fear', 'joy', 'love', 'optimism',
        'pessimism', 'sadness', 'surprise', 'trust'],
        num_rows: 886
    })
})

注意在第一个例子中,我们必须设置NUM_LABELS,而我们不知道实际的标签是什么。在这里,我们将动态确定标签,并创建一些查找表,使我们能够轻松地从情绪到标签以及反之:

Labels = [label for label in dataset[ 'train'].features.keys() if label not in ['ID', 'Tweet']]id2label = {idx:label for idx, label in enumerate(labels)}
label2id = {label:idx for idx, label in enumerate(labels)}

以下输出解释了这些看起来是什么样子:

['anger', 'anticipation', 'disgust', 'fear', 'joy', 'love', 'optimism', 'pessimism', 'sadness', 'surprise', 'trust']{0: 'anger', 1: 'anticipation', 2: 'disgust', 3: 'fear', 4: 'joy', 5: 'love', 6: 'optimism', 7: 'pessimism', 8: 'sadness', 9: 'surprise', 10: 'trust'}
{'anger': 0, 'anticipation': 1, 'disgust': 2, 'fear': 3, 'joy': 4, 'love': 5, 'optimism': 6, 'pessimism': 7, 'sadness': 8, 'surprise': 9, 'trust': 10}

现在,我们需要对数据集进行分词,就像我们之前做的那样。这里的任务稍微复杂一些,因为我们每个推文都有多个标签,标签被加载为TrueFalse,而我们需要01来供我们的模型使用。tokenize_function一次处理 1000 条推文,像之前一样对推文文本进行分词,并将标签转换为 1s 和 0s 的数组:

tokenizer = AutoTokenizer.from_pretrained(model_name)def tokenise_function(tweets):
  text = tweets["Tweet"]
 encoding = tokenizer(text,
                      padding="max_length",
                      truncation=True,
                      max_length=512)
  labels_batch = {k: tweets[k] for k in tweets.keys() if k in labels}
  labels_matrix = np.zeros((len(text), len(labels)))
  for idx, label in enumerate(labels):
    labels_matrix[:, idx] = labels_batch[label]
  encoding["labels"] = labels_matrix.tolist()
  return encoding
encoded_dataset = dataset.map(tokenise_function,
        batched=True,
        remove_columns = dataset['train'].column_names)

在这个例子中,我们使用pytorch,因此我们需要设置数据集的格式,使其兼容:

encoded_dataset.set_format("torch")

现在,我们可以实例化一个自动类,就像我们之前做的那样。注意我们如何设置problem_type以及传递id-labellabel-id映射对象:

model = AutoModelForSequenceClassification.from_pretrained(    model_name,
    problem_type="multi_label_classification",
    num_labels=len(labels),
    id2label=id2label,
    label2id=label2id
    )

接下来,我们需要定义一些函数来为我们计算一些度量。因为我们有多个标签,所以我们处理的是概率。因此,我们需要一个阈值来区分情感中的 0 和 1 – 我们现在任意地将这个值设置为0.5。在实践中,这需要仔细确定。这些概率通过阈值转换为 0 和 1,并且,就像之前一样,我们利用scikit-learn函数为我们做繁重的工作:

def compute_multi_label_metrics(predictions,        labels, threshold=0.5):
    sigmoid = torch.nn.Sigmoid()
    probs = sigmoid(torch.Tensor(predictions))
    y_pred = np.zeros(probs.shape)
    y_pred[np.where(probs >= threshold)] = 1
    y_true = labels
    f1_macro_average = f1_score(y_true=y_true,
                                y_pred=y_pred,
                                average='macro')
    f1_micro_average = f1_score(y_true=y_true,
                                y_pred=y_pred,
                                average='micro')
    accuracy = accuracy_score(y_true, y_pred)
    precision = precision_score(y_true, y_pred,
        average = 'macro')
    recall = recall_score(y_true, y_pred,
        average = 'macro')
    jaccard = jaccard_score(y_true, y_pred,
        average='macro')
    metrics = {
                'precision': precision,
                'recall': recall,
                'f1_micro_average': f1_micro_average,
                'f1_macro_average': f1_macro_average,
                'jaccard': jaccard,
                'accuracy': accuracy
              }
    return metrics
def compute_metrics(p: EvalPrediction):
    if isinstance(p.predictions, tuple):
      preds = p.predictions[0]
    else:
      preds = p.predictions
    result = compute_multi_label_metrics(predictions=preds,
                                         labels=p.label_ids)
    return result

我们现在可以设置一些TrainingArguments并训练模型:

metric_name = "jaccard"training_args = TrainingArguments(
    model_name,
    evaluation_strategy = "epoch",
    save_strategy = "epoch",
    num_train_epochs = 3,
    load_best_model_at_end = True,
    metric_for_best_model = metric_name,
)
trainer = Trainer(
    model,
    training_args,
    train_dataset=encoded_dataset["train"],
    eval_dataset=encoded_dataset["validation"],
    tokenizer=tokenizer,
    compute_metrics=compute_metrics
)
trainer.train()

最后一步是使用我们的度量函数评估结果。注意我们如何将compute_metrics函数的名称作为参数传递。这个函数反过来调用compute_multi_label_metrics来计算各种度量:

trainer.evaluate()

最终结果应该看起来像这样:

{'eval_loss': 0.3063639998435974, 'eval_precision': 0.6944130688122799,
 'eval_recall': 0.4961206747689895,
 'eval_f1_micro_average': 0.7107381546134663,
 'eval_f1_macro_average': 0.539464842236441,
 'eval_jaccard': 0.4181996269238169,
 'eval_accuracy': 0.30242437923250563,
 'eval_runtime': 26.6373,
 'eval_samples_per_second': 33.262,
 'eval_steps_per_second': 4.167,
 'epoch': 3.0}

模型的结果总结在下表中:

数据集精确度召回率微观 F1宏观 F1Jaccard
SEM11-EN0.6940.4960.7100.5390.418
SEM11-AR0.5520.4410.6580.4620.359
KWT.M-AR0.1320.0740.2240.0920.053
SEM11-ES0.5940.3990.5970.4630.340

表 9.2 – 基于转换器的模型在多类数据集上的得分

再次,对于某些数据集,基于转换器的模型表现更好,但不是所有。值得注意的是,SEM11-AR 之前最好的分类器是来自第五章的简单词法模型的词干版本,其 Jaccard 得分为 0.386。对于 SEM11-ES,它是条件概率模型的词干版本,也来自第五章,其 Jaccard 得分为 0.278。与单类数据集一样,使用预训练模型可能有助于我们识别和消除歧义,但这次,基础模型在处理多类情况时表现较差。KWT.M-AR 数据集的得分尤其糟糕:使用这里描述的转换器似乎不是处理没有情感归属的大量推文数据集的好方法。

在几个案例中,使用 transformers 产生的结果比之前章节中的分类器更好。以下表格显示了我们在数据集上对一系列分类器的评分(考虑到我们现在已经查看的分类器数量,这个表格只包括至少在一个数据集上表现最好的那些):

LEX (unstemmed)LEX (stemmed)SVM (single)SNN (single)Transformers
SEM4-EN0.5030.4970.8450.8290.927
SEM11-EN0.3470.3480.2240.2420.418
WASSA-EN0.4450.4370.7700.7370.753
CARER-EN0.3500.3500.7700.8200.816
IMDB-EN0.7220.6670.7360.7930.826
SEM4-AR0.5060.5090.5140.5040.710
SEM11-AR0.3780.3860.2160.2210.359
KWT.M-AR0.6870.6630.6310.0280.053
SEM4-ES0.4250.4200.4120.3370.663
SEM11-ES0.2690.2710.2260.2210.340

表 9.3 – 标准数据集至今的最佳分数 – Jaccard 分数

使用 transformers 的结果比我们 10 个数据集中任何先前分类器的结果都要好,尽管令人惊讶的是,来自第五章“情感词典和向量空间模型”的非常简单的基于词典的分类器对于多类阿拉伯语数据集仍然产生了最佳结果!

仍然存在单情感数据集和多情感数据集之间性能下降的情况。正如之前所说,这可能是由于多种因素的综合作用。多类数据集比其他数据集拥有更多的标签,这也使得任务更加困难,因为错误的空间更大。然而,我们知道多情感分类比单情感分类要困难得多,因为它需要确定文本表达了多少种情感,从零开始向上,而不是仅仅选择得分最高的那个。我们将在第十章“多分类器”中更详细地探讨处理这类数据的方法。

一个有趣且自然的问题在这里是,为什么变换器比其他方法表现更好?我们已经看到,自注意力机制如何允许变换器在预测时关注输入序列的不同部分,从而使其能够捕捉重要的长距离依赖关系和上下文信息。这对于稳健的分类非常重要。此外,我们还看到了变换器如何使用多头注意力,这允许它们同时关注输入序列的不同部分,从而使其在捕捉对稳健分类可能重要的不同类型信息方面更加有效。变换器还能处理长输入序列而不会丢失重要信息,这在分类任务中可能比其他任务更有用。最后,正如我们所看到的,变换器是在大量数据集上预训练的。因此,即使在微调之前,它们也已经知道语言的通用表示。这些概念可以以高度有效的方式结合,以创建一个能够产生良好结果的机制。

现在,让我们总结一下本章所学的内容。

摘要

变换器在一系列自然语言任务中已被证明非常成功,最近发布的许多聊天机器人已经超越了现有模型,在理解和操纵人类语言的能力上表现出色。在本章中,我们探讨了如何使用变换器为非正式文本分配情感,并调查了它们在各种数据集上执行此任务的效果。我们首先简要地了解了变换器,重点关注变换器的各个组成部分以及数据是如何通过它们的。变换器需要大量的数据才能有效并产生良好的结果,同时还需要大量的计算能力和时间。然后,我们介绍了 Hugging Face,讨论了它的有用之处,并介绍了 Hugging Face 平台上一些更常见的预训练模型,之后转向讨论变换器在分类中的应用。最后,我们展示了如何使用变换器为单情感数据集和多情感数据集编写分类器代码,在讨论结果后结束本章。在下一章中,我们将探讨多分类器。

参考文献

若想了解更多本章涉及的主题,请参阅以下资源:

  • Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł. and Polosukhin, I., 2017. Attention Is All You Need. Advances in neural information processing systems, 30.

  • Rothman, D., 2021. Transformers for Natural Language Processing: Build innovative deep neural network architectures for NLP with Python, PyTorch, TensorFlow, BERT, RoBERTa, and more. Packt Publishing Ltd.

  • 德夫林,J.,张,M. W.,李,K.,和图托诺瓦,K.,2018. Bert:用于语言理解的深度双向转换器的预训练. arXiv 预印本 arXiv:1810.04805.

  • 克拉克,K.,卢昂,M. T.,黎,Q. V.,和曼宁,C. D.,2020. Electra:将文本编码器预训练为判别器而不是生成器. arXiv 预印本 arXiv:2003.10555.

  • 布朗,T.,曼,B.,莱德,N.,苏比亚哈,M.,卡普兰,J. D.,达里瓦尔,P.,尼拉卡坦,A.,希亚姆,P.,萨斯特里,G.,阿斯凯尔,A.,和阿加瓦尔,S.,2020. 语言模型是少样本学习者. 神经信息处理系统进展,33,pp.1877-1901.

  • 肖伊比,M.,帕特瓦里,M.,普里,R.,莱格雷斯利,P.,卡斯珀,J.,和卡坦扎罗,B.,2019. Megatron-lm:使用模型并行训练数十亿参数的语言模型. arXiv 预印本 arXiv:1909.08053.

  • 刘,Y.,奥特,M.,高亚尔,N.,杜,J.,乔希,M.,陈,D.,利维,O.,刘易斯,M.,泽特莱莫伊,L.,和斯托扬诺夫,V.,2019. RoBERTa:一种鲁棒优化的 BERT 预训练方法. arXiv 预印本 arXiv:1907.11692.

  • 拉费尔,C.,沙泽尔,N.,罗伯茨,A.,李,K.,纳兰,S.,马特纳,M.,周,Y.,李,W.,和刘,P. J.,2020. 探索统一文本到文本转换器在迁移学习中的极限. J. Mach. Learn. Res.,21(140),pp.1-67.

  • 戴,Z.,杨,Z.,杨,Y.,卡本内尔,J.,黎,Q. V.,和沙拉胡丁诺夫,R.,2019. Transformer-xl:超越固定长度上下文的注意力语言模型. arXiv 预印本 arXiv:1901.02860.

  • 杨,Z.,戴,Z.,杨,Y.,卡本内尔,J.,沙拉胡丁诺夫,R. R.,和黎,Q. V.,2019. XLNet:用于语言理解的泛化自回归预训练. 神经信息处理系统进展,32.

第十章:多分类器

在前面的章节中,我们看到了多标签数据集,其中一条推文可能有零个、一个或多个标签,与每个推文恰好有一个标签的简单多类数据集相比,处理起来要困难得多,尽管这些标签来自一个包含多个选项的集合。在本章中,我们将探讨处理这些情况的方法,特别是探讨使用中性标签来处理允许推文有零个标签的情况;使用不同的阈值来使标准分类器返回可变数量的标签;以及训练多个分类器,每个标签一个,并允许它们各自对其训练的标签做出决定。结论,一如既往,将是没有单一的“银弹”可以在每种情况下提供最佳解决方案,但总的来说,使用多个分类器往往比其他方法更好。

本章我们将涵盖以下主题:

  • 使用混淆矩阵来分析复杂数据上分类器的行为

  • 使用中性标签来处理未分配标签的推文

  • 使用不同的阈值来处理多标签数据集

  • 训练多个分类器来处理多标签数据集

在本章结束时,您将了解如何实施几种处理多标签数据集的策略,并会对这些策略对不同类型数据的有效性有所认识。

多标签数据集难以处理

我们将从查看前几章中选择的几个分类器在主要数据集上的性能开始。我们曾多次提到多标签数据集特别具有挑战性,但将表现最好的算法的结果汇集在一起,可以看到它们究竟有多具挑战性。图 10.1包括了迄今为止我们查看的所有主要分类器。多标签数据集以灰色突出显示,每行的最佳性能分类器以粗体/星号标记:

LEXCPNBSVMSNNDNNTransformers
SEM4-EN0.4970.5930.7750.8450.8290.847***** **0.927 ***
SEM11-EN0.3480.3530.2270.2240.2420.246***** **0.418 ***
WASSA-EN0.4370.5050.709***** **0.770 ***0.7370.7520.753
CARER-EN0.3500.3950.7760.770***** 0.820*0.8040.816
IMDB-EN0.6670.7220.7380.7360.7930.793***** **0.826 ***
SEM4-AR0.5090.5130.5310.5140.5040.444***** **0.710 ***
SEM11-AR***** **0.386 ***0.3820.2360.2160.2210.2070.359
KWT.M-AR0.663***** **0.666 ***0.4940.6310.0280.0260.053
SEM4-ES0.4200.1770.3600.4120.3370.343***** **0.663 ***
SEM11-ES0.2710.2780.2300.2260.2210.222***** **0.340 ***

图 10.1 – 标准数据集选择的 Jaccard 分数(多标签数据集以灰色显示)

从这张表中,有两点特别突出:

  • 在这个表格的大部分条目中,LEX 是最差的分类器,其次是 NB,然后其他分类器的得分通常相当相似。然而,对于多标签情况,LEX 或 CP 总是优于除变压器以外的任何其他分类器,而且在几个情况下,它们甚至优于变压器。鉴于这些数据集似乎是最现实的,因为许多推文没有表达情感,相当一部分推文表达了多个情感,因此值得更详细地研究这些情况中发生的事情。

  • 多标签情况的整体得分也显著更差 – 虽然 LEX 和 CP 在这些情况下的表现优于大多数其他分类器,但它们通常在这些情况下的得分低于其他情况,而对于所有其他分类器,这些情况与单一情感/推文情况之间的差距是显著的。

这些情况在实践中似乎最有用,因为大多数推文都没有表达任何情感,相当一部分推文表达了多个情感,所以处理这些情况不佳的算法可能不适合这项任务。

混淆矩阵部分,我们将查看各种算法对这两种数据集的处理方式。一旦我们更清楚地了解为什么多标签数据集比单标签数据集更难处理,并且我们已经看到它们对特定算法造成的具体问题,我们将探讨处理这类数据集的方法。我们不会使用基于变压器的模型进行这些实验,部分原因是训练变压器的耗时使得这不可行,但更重要的是,我们需要深入了解模型以了解其工作原理 – 这在基于变压器的模型中几乎是不可能的。

混淆矩阵

仅通过查看原始输出,很难看出分类器犯了什么错误。混淆矩阵使我们能够可视化分类器的行为,使我们能够看到两个类别是否被系统地混淆,或者某个类别是否被分配了太多或太少的项目。考虑以下数据集,其中每个项目由黄金标准(G)分类为 A、B 或 C,并且还有一个预测值(P):

GCCABCBCBBBAABBCCBCBBCABAACCCAAACBCAABA
PCBBBCAABBAAACBABBCBCCABBBCBBBABCBBAABA

图 10.2 – 示例数据的黄金标准和预测值

在这个表格中很难看出任何模式。简单地计算 G 和 P 值相同的案例数量,我们得到 22 个案例中的 38 个 – 即,准确率为 0.58 – 但很难看出它做对了什么,做错了什么。将此转换为混淆表可以帮助解决这个问题。我们通过计算应该分配 C1 作为其值的项被预测为具有 C2 的次数来实现这一点,从而产生一个正确与预测分配的表格。例如,图 10.3 中的混淆矩阵显示,七个本应被分配标签 A 的项确实被分配了该标签,但五个被分配了 B,而六个本应被分配 C 的项被分配了 C 但五个被分配了 B。这表明 B 的某些属性使得在它们应该被分配到 A 或 C 时容易将它们分配到这个类别,这可能导致对导致此问题的 B 的哪些属性进行调查的探究:

ABC
A750
B292
C256

图 10.3 – 图 10.2 中数据的混淆矩阵

如果 gsp 是一组点的黄金标准值,那么 confusion 将计算混淆矩阵:c 是一个表格,其中为 gs 中的每个标签都有一个条目,该标签的值是预测为该标签的次数集合:

def confusion(gs, p):    c = {}
    for x, y in zip(gs, p):
        if not x in c:
            c[x] = counter()
        c[x].add(y)

混淆矩阵可以提供大量关于分类器所做事情的信息。然而,当黄金标准和预测可以包含不同数量的情绪时,构建混淆矩阵会有一些小问题。例如,假设某些推文的黄金标准是爱+喜悦,而预测是爱+悲伤+愤怒。我们想要承认当分类器预测时是正确的,但关于它遗漏了喜悦(即存在一个假阴性)以及预测了悲伤愤怒(两个假阳性)的事实我们该如何处理?

对于这个问题没有正确答案。我们按照以下方式调整构建混淆矩阵的标准方法,其中 C[e1][e2] 是黄金标准中 e1 和预测中 e2 的得分。我们需要为“未分配情绪”添加一行和一列(我们将使用 -- 表示此类类别):

  • 对于黄金标准和预测包含给定情绪 e 的每个情况,将 1 添加到 C[e][e],并从黄金标准和预测中删除 e

  • 如果黄金标准现在为空,那么预测中剩余的每个 e 必须是一个假阳性,因此对于每个剩余的 e,将 1 添加到 C[--][e]

  • 如果预测为空,那么黄金标准中剩余的每个 e 必须是一个假阴性,因此将 1 添加到 C[e][--]

  • 如果在移除共享案例后两者都不为空,很难看出要做什么。考虑前面的例子。在移除之后,我们在金标准中剩下快乐,在预测中剩下悲伤+愤怒快乐悲伤的错误,而愤怒是假阳性吗?快乐愤怒的错误,而悲伤是假阳性吗?快乐是假阴性,而悲伤愤怒都是假阳性吗?最后一个建议似乎不正确。假设我们有一个案例,其中快乐悲伤+愤怒相匹配,另一个案例中它与悲伤+恐惧相匹配,还有一个案例中它与悲伤相匹配。如果我们将这些情况都标记为快乐是假阴性而悲伤是假阳性的案例,我们就会错过快乐悲伤之间似乎存在联系的事实。

我们是这样处理的。假设在移除了两者都出现的标签之后,金标准中剩下G个项目,预测中剩下P个项目。在这里,对于金标准中的每个g和预测中的每个p,我们向C[p][g]中添加1/P。这样做总共向混淆矩阵中添加了G,从而承认金标准中的情绪数量尚未匹配,预测中的每个项目被视为有同等可能性是应该替换g的那个。

计算修改后的混淆矩阵的机制相当复杂,将其包含在这里对前面的解释增加的很少。这本书的 GitHub 仓库中有这个代码——目前,最好只是注意一下,当一个项目可以分配多个标签时,混淆矩阵必须考虑到金标准和预测都分配了多个标签的情况,分配的集合大小不同,并且有些标签两者都有,有些只出现在金标准中,有些只出现在预测中。

我们这样做的方式在金标准与预测之间并不对称,但它确实提供了混淆矩阵,这些矩阵告诉我们关于给定分类器正在做什么的一些有用的信息。对于金标准和预测中恰好有一个项目的情况,它将退化为标准版本,而对于每个中都有不同数量项目的情况,它确实提供了一幅正在发生的事情的图景。

我们将首先查看使用 SVM 作为分类器(SVM 和 DNN 的分数非常相似,混淆矩阵也非常相似,因此为了方便,我们将在这里使用 SVM)的 CARER-EN 混淆矩阵。以下矩阵是使用一个简单的 SVM 版本获得的,该版本只是为每条推文选择最可能的情绪,而不是使用阈值来尝试确定是否有任何情绪足够可能被计算,如果是这样,是否有几个可以计算:

愤怒恐惧快乐悲伤惊讶
愤怒12401010
恐惧11280001
快乐00337100
0017300
悲伤01102930
惊讶0000037

图 10.4 – 使用 SVM 作为分类器的 CARER-EN,每条推文一个情感,混淆矩阵

这是你期望的混淆矩阵看起来像什么——对角线上的最大分数和一些其他分配的散点,最大的混淆发生在快乐之间。当我们对 SEM11-EN 使用相同的算法时,我们得到了一个非常不同的图像:

愤怒厌恶恐惧快乐乐观悲观悲伤惊讶信任--
愤怒31120120000011
期待86512100000012
厌恶1033613000001182
恐惧90046000000034
快乐1121118600000139
0100040000040
乐观710220201000119
悲观01000001900026
悲伤931130011600119
惊讶4100000001016
信任2101000000115
--2000000000210

图 10.5 – 使用 SVM 作为分类器的 SEM11-EN,每条推文一个情感,混淆矩阵

我们得到了几个错误阳性(预期没有但预测到了的地方——即以**--为首的行:其中两个被分配了愤怒**,21 个信任)。这是因为我们迫使分类器在黄金标准不期望任何东西的情况下也要选择一些东西。我们还有许多地方存在错误阴性(以**--为首的列),预期有东西但什么也没有找到,通常是因为黄金标准有多个标签,而只有一个预测。还有许多情况,分配是错误的,大量的事情被错误地标记为愤怒**,而它们应该是其他东西。

问题在于,如果分类器被强制为每条推文分配恰好一种情绪,那么它无法避免产生误报(如果黄金标准说没有任何东西应该被分配)和漏报(如果黄金标准说应该分配多种情绪)。如果我们仔细查看测试集,我们会看到有 23 条没有分配情绪的推文,这些推文显示为误报,还有 645 条分配了多种情绪的推文,这些推文显示为 1,065 条漏报(因为其中一些被分配了三种或更多情绪)。如果我们的分类器假设每条推文只有一个情绪,那么对此就无能为力了

假设我们共有N条推文,其中X条没有分配情绪,Y条分配了多种情绪。在这种情况下,至少会有X条误报(每条应该没有标签但分类器分配了一个标签的推文)和至少Y条漏报(每条应该有多个标签但分类器只分配了一个标签的推文),这意味着最佳可能的 Jaccard 分数是*(N-X)/((N-X)+X+Y)。对于 SEM11-EN 中的 772 条推文集合,这个分数是(772-23)/(772-23+(1065+23)) = 0.41*(由于应该分配两个以上标签的推文占多数,漏报的数量非常高——这个方程假设推文被分配了零、一或两个标签)。这是一个严格的上限。没有任何分类器能够在这个数据集上实现高于 0.41 的 Jaccard 分数。

位置比这还要糟糕。仔细检查对角线显示,一些情绪在对角线上有很好的分数(愤怒快乐),而其他情绪在对角线上分数非常低,并且有很多漏报(厌恶爱情乐观),其中几种情绪与愤怒混淆。

当我们查看 KWT.M-AR 数据集时,我们会看到一些方面相似但并不令人鼓舞的输出:

愤怒厌恶恐惧快乐爱情乐观悲观拒绝信任--
愤怒5000000070
不满021000000311
恐惧0020000010
快乐000110000120
爱情000050000443
乐观000002200170
悲观0000001022
拒绝0000000010
信任00000000140
--010083007510

图 10.6 – 使用 SVM 作为分类器的 KWT.M-AR 每条推文一个情绪的混淆矩阵

这次,有大量的假阳性,这反映了这些数据集在黄金标准中未分配情感的比例非常高(在本测试集中,有 763 个,占总数的 76.3%,最大可达到的 F1 分数约为 0.42)。同样,这是不可避免的——如果一条推文应该没有任何情感分配,而分类器被迫分配一个,那么我们将得到一个假阳性。还值得注意的是,尽管对角线上的非平凡条目很多,但令人惊讶的是,有大量情况中正确的分配被替换为 angry: 2.38fuming: 2.32annoying: 2.31revenge: 2.26,……对于 positivity: 1.82,💕: 1.75rejoice: 1.74gift: 1.72laughing: 1.70 对于 flat: 1.25com: 1.19cup: 1.06need: 1.05major: 1.05。这些不是显然与信任相关的词,而且它们与这种情感之间的联系并不强。因此,当分类器被迫为不包含任何与特定情感相关联的词汇的推文选择情感时,它很可能会选择那些本就不期望有此类词汇的情感。

如果,如前所述,大量推文表达的是没有情感或多种情感,那么我们必须处理这些问题。我们可以尝试以下几种方法:

  • 我们可以包含一个明确的“以上皆非”或“中性”类别来表示一条推文没有任何情感权重。对于“零情感”的情况,这是最容易做到的,尽管在推文被分配了多种情感的情况下,这并不理想。

  • 我们可以利用某些分类器为每种情绪计算分数的事实。我们将在稍后更详细地探讨这一点。

  • 我们可以训练一组二元分类器——快乐非快乐愤怒非愤怒,等等。这可能会处理两种情况:如果这些分类器中的每一个都返回负版本,我们将得到一个整体零分配,如果有多个返回正版本,我们将得到多个分配。

在本章剩余部分,我们将专注于 SEM-11 和 KWT 数据集,因为这些是唯一具有可变标签数量的数据集。如果你的训练数据为每条推文分配了恰好一种情绪,并且你希望在运行分类器对实时数据进行分类时也恰好分配一种情绪,那么其他方法通常能提供最佳解决方案——LEXCLASSIFIER 通常在极短的训练时间内提供相当准确的结果,Transformer 通常提供最佳结果但需要大量训练,而 SVM 和 DNN 在准确性和训练时间之间处于中等水平。

使用“中性”作为标签

我们可以通过查看 Gold Standard 分配的标签来简单地引入中性作为标签。这不会影响 CARER-EN 集:训练数据中没有分配中性,因此没有找到与该标签相关的单词,因此,反过来,分类器没有分配任何内容。对 SEM11-EN 数据的影响更有趣:

| | 愤怒 | 反义 | 厌恶 | 恐惧 | 喜悦 | 爱情 | 乐观 | 悲观 | 悲伤 | 惊讶 | 信任 | 中性 | -- | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 愤怒 | 311 | 2 | 0 | 1 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | | 期待 | 8 | 65 | 1 | 2 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 12 | | 厌恶 | 10 | 3 | 36 | 1 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 182 | | 恐惧 | 9 | 0 | 0 | 46 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 34 | | 喜悦 | 11 | 2 | 1 | 1 | 186 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 39 | | 爱情 | 0 | 1 | 0 | 0 | 0 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 40 | | 乐观 | 7 | 1 | 0 | 2 | 2 | 0 | 20 | 1 | 0 | 0 | 0 | 1 | 118 | | 悲观 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 19 | 0 | 0 | 0 | 0 | 26 | | 悲伤 | 9 | 3 | 1 | 1 | 3 | 0 | 0 | 1 | 16 | 0 | 0 | 0 | 119 | | 惊讶 | 4 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 16 | | 信任 | 2 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 15 | | 中性 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 21 | 0 |

图 10.7 - SEM11-EN 的混淆矩阵,每条推文一个情绪,使用 SVM 作为分类器,中性作为标签

在对角线以下的变化非常小——也就是说,分类器在有或没有中性标签的情况下,对相同的实际情绪的判断是相同的;大多数本应被归类为中性的事物确实被标记为中性,有少数被错误地标记为愤怒;有几件事物被错误地标记为中性,而它们不应该被这样标记;由于有很多推文本应被赋予多个标签,因此仍然有很多假阴性。这些推文不能被分类器标记为中性,因为分类器只能为每条推文分配一个标签,所以任何本应被赋予多个标签的推文都将导致假阴性集的增加。

KWT 示例的情况很吸引人。这些示例中有大量没有分配情绪的推文,因此我们预计如果分类器设置为每条推文分配一个情绪,将会出现很多假阳性。这里给出了 KWT.M-AR 在有和没有中性标签时的混淆矩阵:

愤怒厌恶恐惧喜悦爱情乐观悲观拒绝信任--
愤怒7000000050
不满019000000174
恐惧0030000011
喜悦000110000232
000082000471
优化000003700182
悲观0000001020
拒绝0000000120
信任00000000121
--0203132006970

图 10.8 - KWT.M-AR 的混淆矩阵,每条推文一个情感,使用 SVM 作为分类器,不包括中性

如前所述,对角线上的大多数分数都相当好——也就是说,大多数时候,分类器在需要分配标签的地方分配了正确的标签。不可避免的是,存在大量错误阳性,几乎所有这些都被分配给了信任。如前所述,在几乎每个金标准说应该没有标签的情况下,分类器都选择了信任,而不是将错误阳性均匀分配。再次,似乎发生的情况是分类器没有特别强烈地将任何单词与信任关联起来,因此当它被给出一个没有任何非常显著单词的推文时,它决定它不可能是其他任何类别,因为这些类别有更强的线索,所以它选择了信任

当我们允许中性作为标签时,情况发生了相当大的变化。现在,几乎所有错误阳性都被分配给了中性,这是最合理的结果。由于这个数据集包含具有多个标签的推文,因此存在一些错误阴性,但对角线变得更加清晰——大多数情感都被正确分配,大多数没有情感的情况都被分配给了中性

愤怒不满恐惧快乐优化悲观拒绝信任中性--
愤怒70000000050
不满意0190000000174
恐惧00300000011
快乐0001100000241
0000810000481
优化0000037000182
悲观00000010020
拒绝00000001020
信任00000000751
中性02021420006970

图 10.9 - KWT.M-AR 的混淆矩阵,每条推文一个情感,使用 SVM 作为分类器,包括中性

因此,使用中性作为标签为无标签和多个标签的问题提供了一个部分解决方案,但它不能提供一个完整的解决方案。即使分类器在为应该恰好有一个标签的推文分配标签时是 100%准确的,并且当它为应该没有标签的推文分配中性时,它也必须为应该有多个标签的情况引入错误阴性。

现在是引入一个新的性能度量指标的好时机。大多数既没有被分类为中性也没有被赋予任何标签的案例都位于对角线上,这表明一组推文被分配的总体分类可能有助于衡量观点,即使对个别推文的分配是不可靠的。特别是,当你试图发现一般趋势时,假阴性可能不是很重要,只要分类器分配标签的案例与基本现实一致。

当然,不可能确定某件事是假阴性还是真正的中性分配实例,然后询问假阴性的分配应该是什么。如果我们能这样做,那么我们最初就会训练分类器来完成这项任务,同样对于假阳性也是如此。我们能做的最好的事情就是评估如果我们接受分类器做出的所有分配,我们可能会被误导到何种程度。因此,我们将分类器的比例性定义为金标准中分配给每种情感的推文比例与预测(即忽略分配为中性或完全没有标签的推文)之间的余弦距离。这个值越接近 1,我们越可以期待我们的分类器给出一个可靠的总体情况,即使一些个别分配是错误的。

以一个简单的例子来说明,假设我们有一个包含 SEM11 数据中 11 种情感的语料库,悲观和悲伤的推文数量相同,并且它几乎完全正确,除了恰好将一半本应被标记为悲观的推文标记为悲伤,以及恰好将一半本应被标记为悲伤的推文标记为悲观。在这种情况下,比例将是完美的,你可以安全地使用这个分类器来对整体情况做出判断,即使你不能依赖它来告诉你某个推文是否悲伤或悲观。同样,如果每个类别中一半的推文都没有被分配情感,那么比例将是完美的,而如果最常见的类别中一半的推文被分配为中性,但没有其他推文被分配,那么这将是相当差的。

从现在起,我们将对我们在训练过程中生成的所有分类器都这样做,因为与单个折叠相关的测试集相对较小(这就是我们最初进行交叉折叠验证的原因),并且通过忽略中性和未分配的推文,我们失去了很多实例。为了计算比例性,我们只需计算预测/金标准中包含每种情感的推文数量,忽略中性和未分配的推文,并将结果归一化:

def proportions(clsfs, emotions, which=lambda x: x.predicted, ignore=["neutral", "--"]):    conf = numpy.zeros(len(emotions))
    for clsf in clsfs:
        for t in clsf.test.tweets:
            for i, k in enumerate(which(t)):
                if not emotions[i] in ignore:
                    if k == 1:
                        conf[i] += 1
    return conf/sum(conf)

现在,我们可以对预测和金标准进行这样的操作,并使用余弦相似度来计算两者之间的相似度:

def scoreproportions(clsfs):    ignore = ["neutral", "--"]
    emotions = clsfs[0].train.emotions
    predictedproportions = proportions(clsfs, emotions, ignore)
    gsproportions = proportions(clsfs, emotions, ignore,
                                which=lambda x: x.GS)
    return cosine_similarity(predictedproportions.reshape(1, -1),
                             gsproportions.reshape(1, -1))[0][0]

对于 SEM11-EN,允许每条推文有任意数量的情感,并使用 LEX 作为分类器以及neutral作为标签,例如,预测和黄金标准中分配给每个标签的推文比例分别为愤怒:0.30期待:0.00厌恶:0.31恐惧:0.02快乐:0.25爱情:0.00乐观:0.04悲观:0.00悲伤:0.06惊讶:0.00信任:0.00愤怒:0.18期待:0.06厌恶:0.17恐惧:0.06快乐:0.15爱情:0.04乐观:0.12悲观:0.04悲伤:0.12惊讶:0.02信任:0.02,相应的比例分数为 0.89。如果我们使用相同的分类器,以neutral作为标签,但允许每条推文恰好有一个标签,比例分数将降至 0.87。

如果我们将此应用于 KWT.M-AR 数据集,我们得到预测为愤怒:0.03不满:0.07恐惧:0.00快乐:0.07爱情:0.69乐观:0.10悲观:0.00拒绝:0.02信任:0.02,对于黄金标准为愤怒:0.04不满:0.16恐惧:0.02快乐:0.10爱情:0.43乐观:0.18悲观:0.01拒绝:0.01信任:0.05,比例分数为 0.94。如果我们没有忽略中性/未分配的案例,分数将高得多,达到 0.99,因为在这个数据集中中性案例的巨大优势。因此,我们有一个有用的单一数字,它让我们能够了解分类器在提供整体图景方面的可靠性,即使它未能为每条推文分配具体标签(也就是说,有些可能什么都没有分配,或者被分配了中性)。

这个分数通常相当高,因为在大多数情况下,大多数具体分数都位于对角线上。重要的是中性/未分配案例的分布是否遵循具体案例的一般分布——如果遵循,那么即使有时在应该分配具体标签时未能分配,分类器也会对评估总体趋势有用。因此,我们将在此章的剩余部分使用此度量标准,除了 Jaccard 之外来评估分类器。图 10.1010.12中的表格显示了当我们添加中性作为标签,并坚持为每条推文分配一个确切标签时,各种分类器的比例发生了什么变化。作为一个参考点,我们首先看看如果我们指定每个分类器在未使用中性的情况下返回会发生什么。与之前一样,具有最佳 Jaccard 分数的分类器用粗体标出:

LEXNBSVMDNN
SEM11-EN0.224 (0.813)0.229 (0.690)0.223 (0.771)*** 0.242 (****0.677) ***
SEM11-AR*** 0.247 (****0.824) ***0.216 (0.667)0.204 (0.736)0.207 (0.613)
SEM11-ES0.225 (0.799)*** 0.226 (****0.788) ***0.215 (0.888)0.222 (0.774)
KWT.M-AR*** 0.208 (****0.973) ***0.108 (0.352)0.078 (0.207)0.026 (0.148)

图 10.10 – Jaccard 和比例(括号内),每条推文一个标签,不包括中性

图 10.10 表明,如果我们简单地使用原始分类器不变——也就是说,每条推文一个情感,并且没有中性作为标签——我们得到的 Jaccard 分数相当低,但 LEX 的比例分数从合理到相当好,其他分类器在这个指标上通常表现更差。特别是 LEX 在 KWT.M-AR 数据集上的比例分数比这个数据集上其他任何分类器的相同分数都要好得多。关键在于 NB、SVM 和 DNN 将几乎所有应该被标记为中性的案例分配给了信任,因为这些案例缺乏在更明显标记的情感中常见的区分性词汇,而 LEX 则更接近地分配了它们。值得注意的是,对于给定的数据集,得分最高的分类器并不总是产生该集合的最佳比例:

angedissfearjoyloveoptipessrejetrus--
anger9190011200014
dissat4133027110000
fear03201220002
joy1605350800118
love0708548120000
optimi0501441800012
pessim0400722011
reject0200300302
信任190228800133
--308804159200857721610

图 10.11 – KWT.M-AR 的混淆矩阵,每条推文一个标签,使用 LEX 作为分类器,不包括中性

当我们允许中性作为一个标签时,NB 和 SVM 可以选择这个标签作为具有最少独特术语的类别,因此将应该被分配为中性的案例分配给它,这导致这些分类器的 Jaccard 和比例都得到了大幅提升:

LEXNBSVMDNN
SEM11-EN0.222 (0.813)0.227 (0.690)0.222 (0.768)*** 0.239 (****0.677) ***
SEM11-AR*** 0.246 (****0.824) ***0.216 (0.666)0.204 (0.736)0.207 (0.615)
SEM11-ES0.221 (0.800)*** 0.222 (****0.787) ***0.211 (0.885)0.216 (0.774)
KWT.M-AR0.608 (0.984)0.510 (0.986)*** 0.632 (****0.992) ***0.595 (0.905)

图 10.12 – Jaccard 和比例,每条推文一个标签,包括中性

因此,我们可以看到,使用比例作为指标使我们能够发现一般趋势。如果我们允许中性作为标签,我们的大部分分类器在多标签数据集上表现更好,尤其是在查看比例时,但 LEX 即使没有中性作为标签也能表现得相当好。

阈值和局部阈值

接下来要探索的选项是使用阈值。正如我们所见,我们的大多数分类器为每条推文的每个选项提供分数,默认设置是选择分数最高的选项。在第六章中,朴素贝叶斯,我们了解到假设我们的分类器将为每条推文分配一个精确的标签,这给其性能设定了一个相当紧的上限,而我们可以设置一个阈值,并说超过该阈值的任何内容都应被视为标签。

考虑以下推文:“嗨,大家好!我现在通过 Skype 上课!联系我获取更多信息。# skype # lesson # basslessons # teacher # free lesson # music # groove # rock # blues。”

金标准将此推文的分数分配为(‘愤怒’,0),(‘期待’,1),(‘厌恶’,0),(‘恐惧’,0),(‘快乐’,1),(‘爱情’,0),(‘乐观’,0),(‘悲观’,0),(‘悲伤’,0),(‘惊讶’,0),(‘信任’,0),因此它应该被标记为期待+快乐

朴素贝叶斯将此推文分配的分数为(‘愤怒’,‘0.00’),(‘期待’,‘0.88’),(‘厌恶’,‘0.00’),(‘恐惧’,‘0.00’),(‘快乐’,‘0.11’),(‘爱情’,‘0.00’),(‘乐观’,‘0.00’),(‘悲观’,‘0.00’),(‘悲伤’,‘0.00’),(‘惊讶’,‘0.00’),(‘信任’,‘0.00’),因此如果我们把阈值设为 0.1,我们会得到期待+快乐;如果我们把阈值设为 0.2,我们只会得到期待;如果我们把阈值设为 0.9,我们则一无所获。

对于同一推文,SVM 分配的分数为(‘愤怒’,‘-0.77’),(‘期待’,‘0.65’),(‘厌恶’,‘-2.64’),(‘恐惧’,‘-1.67’),(‘快乐’,‘-0.99’),(‘爱情’,‘-1.93’),(‘乐观’,‘-3.52’),(‘悲观’,‘-1.61’),(‘悲伤’,‘-2.58’),(‘惊讶’,‘-1.47’),(‘信任’,‘-3.86’)。因此,这次,如果我们把阈值设为-1,我们会得到愤怒+期待+快乐;如果我们把阈值设为 0,我们只会得到期待;如果我们把阈值设为 1,我们则一无所获。

因此,使用阈值将使我们能够生成零个或多个标签。我们必须优化阈值,但我们可以通过找到任何推文中任何标签分配的最小和最大值,并在这些值之间均匀递增来实现这一点。《bestThreshold》函数,如第五章中所述,情感词典和向量空间模型,将像在那里一样与朴素贝叶斯、SVM 和 DNN 产生的原始分数一样工作。

如果我们将之前通过要求单个标签获得的分数与我们使用阈值允许零个或多个标签在关键数据集上获得的分数进行对比,我们将看到,总的来说,后者产生了更好的结果:

LEXNBSVMDNN
SEM11-EN* 0.347 (0.898) *0.270 (0.764)0.250 (0.828)0.273 (0.729)
SEM11-AR* 0.377 (0.940) *0.257 (0.761)0.224 (0.798)0.246 (0.731)
SEM11-ES* 0.266 (0.890) *0.250 (0.837)0.228 (0.924)0.238 (0.791)
KWT.M-AR* 0.691 (0.990) *0.522 (0.988)0.631 (0.998)0.604 (0.935)

图 10.13 – 每条推文中零个或多个情感,带有最佳全局阈值

这里的分数比使用简单分类器时的分数要好得多,有些情况下比例分数几乎完美。然而,如果我们想要正确地为单个推文标签分配标签,而不仅仅是得到一个良好的整体印象,我们还有很长的路要走。下一步是为每个标签设置一个阈值,而不是为整个数据集设置。我们将从第五章**,情感词典和向量空间模型中调整bestThreshold,以便我们可以为标签分配单独的阈值。我们将对原始定义进行两项更改:

  • 我们将把它分成两种情况 – 一种用于计算全局阈值(适用于所有情况的单个阈值)和另一种用于为每个标签计算局部阈值。

  • 在原始版本中,我们查看数据中的每一列,找到任何地方出现的最小值和最大值,然后查看每一列的预测值来计算每个潜在阈值的 Jaccard 分数。为了计算局部阈值,我们只需要一次查看一列。如果我们指定一个列的范围,从startend,我们可以处理这两种情况。对于全局情况,我们必须设置start=0end=sys.maxsize;对于我们要为i列选择最佳阈值的情况,我们必须设置start=iend=i+1。这使得我们可以使用相同的机制来计算这两种类型的阈值。以下更新版本中的主要更改已突出显示:

        def bestThreshold(self, bestthreshold, start=0, end=sys.maxsize):
    
            train = self.train.tweets[:len(self.test.tweets)]
    
            self.applyToTweets(train, threshold=0, probs=True)
    
            if bestthreshold == "global":
    
                predicted = [t.predicted for t in train]
    
                # select the required columns from the prediction
    
                predicted = numpy.array(predicted)[start:end, :]
    
                lowest = threshold = numpy.min(predicted)
    
                highest = numpy.max(predicted)
    
                step = (highest-lowest)/20
    
                best = []
    
                GS = numpy.array([t.GS for t in train])[:, start:end]
    
                for i in range(20):
    
                    l = self.applyToTweets(train, threshold=threshold)
    
                    l = numpy.array(l)[:, start:end]
    
                    m = metrics.getmetrics(GS, l, show=False)
    
                    (macroF, tp, tn, fp, fn) = m
    
                    j = tp/(tp+fp+fn)
    
                    best = max(best, [j, threshold])
    
                    if show:
    
                        print("%.2f %.3f"%(threshold, j))
    
                    threshold += step
    
                return best[1]
    
            elif bestthreshold == "local":
    
                # do the global version, but just for each column in turn
    
                localthresholds = []
    
                for i in range(len(self.train.emotions)):
    
                    localthreshold = self.bestThreshold("global",
    
                                                        start=i, end=i+1)
    
                    localthresholds.append(localthreshold)
    
                return localthresholds
    
            else:
    
                raise Exception("%s unexpected value for bestthreshold"%(bestthreshold))
    

允许分类器为不同的标签选择不同的阈值的结果如下所示:

LEXNBSVMDNN
SEM11-EN* 0.371 (0.987) *0.271 (0.827)0.270 (0.809)0.277 (0.811)
SEM11-AR0.371 (0.965)0.255 (0.854)0.236 (0.809)0.238 (0.795)
SEM11-ES* 0.267 (0.962) *0.192 (0.674)0.222 (0.983)0.202 (0.852)
KWT.M-AR0.681 (0.989)0.217 (0.163)0.615 (0.987)0.226 (0.167)

图 10.14 – 每条推文中零个或多个情感,带有最佳局部阈值

LEX 的比例分数都有所提高,现在 LEX 很容易给出 SEM11-EN 的最佳比例分数,朴素贝叶斯现在几乎对所有 KWT.U-AR 和其他大多数分数都回归到选择中性/未分配,尽管 Jaccard 分数只有 SEM11-EN 和 SEM11-ES 有所提高。再次强调,不同的分类器更适合不同的数据集和不同的任务。

多个独立的分类器

使用 LEX 与最优局部阈值或朴素贝叶斯或 SVM 与最优全局阈值,通过第七章中的支持向量机MULTICLASSIFIER类,允许在较低级别使用不同类型的分类器。这里与原始版本的关键变化在于,我们在可选参数集中指定要使用的分类器,而不是假设我们将使用SVMCLASSIFIER

    def __init__(self, train, showprogress=True, args={}):        self.train = train
        T = time.time()
        self.datasets = {}
        self.classifiers = {}
        self.args = args
        # Find what kind of classifier to use for the individual emotions
        subclassifier = args["subclassifiers"]
        for i in range(len(self.train.emotions)):
            squeezed = self.squeeze(i)
            if squeezed:
                self.datasets[i] = squeezed
                self.classifiers[i] = subclassifier(self.datasets[i], args=args)

这将使用指定的子分类器类型创建双向分类器,用于愤怒非愤怒爱情非爱情等。对于单个分类器,由于一条推文可以同时满足爱情喜悦,或者愤怒恐惧,但让一条推文同时满足愤怒非愤怒是没有意义的。如果,例如,同时满足爱情非爱情喜悦非喜悦,我们仍然可以得到多个标签,如果选择了所有负面标签,我们仍然可以得到零标签,但允许单个分类器分配零个或多个标签是没有意义的。

如同以往,各种子分类器有广泛的设置。主要的多分类器只是结合了单个子分类器的结果,因此除了选择作为子分类器的选项之外,没有显著的超参数,但单个子分类器有通常的选项范围。以下表格报告了使用每个子分类器一个标签的分数,但允许或不允许中性作为标签:

多分类器多 NB多 SVM多 DNN
SEM11-EN0.348 (0.868)*0.441 (0.996) *0.385 (1.000)0.422 (0.991)
SEM11-AR0.363 (0.878)0.376 (0.996)0.314 (0.997)0.333 (0.956)
SEM11-ES0.260 (0.852)* 0.296 (0.993) *0.256 (0.995)0.236 (0.936)
KWT.M-AR0.304 (0.979)0.236 (0.989)0.294 (0.996)0.182 (0.938)

图 10.15(a)- 每条推文 0 个或多个情绪,多个分类器,-中性

多分类器多 NB多 SVM多 DNN
SEM11-EN0.342 (0.861)0.438 (0.996)0.381 (1.000)0.419 (0.991)
SEM11-AR0.363 (0.879)0.376 (0.996)0.313 (0.997)0.333 (0.956)
SEM11-ES0.256 (0.836)0.290 (0.993)0.250 (0.995)0.234 (0.938)
KWT.M-AR0.665 (0.984)0.546 (0.989)0.617 (0.996)0.599 (0.950)

图 10.15 (b) – 每条推文 0 或更多情绪,多个分类器,+中性

在这里,整体情况是使用多个独立的分类器来决定一条推文是否应该有特定的标签,对于零到多个数据集产生了最佳的比例结果。尽管 Jaccard 分数只对 SEM11-EN 和 SEM11-ES 有所提高,但在这种制度下,不同分类器的性能之间存在相当大的差异。当我们不允许中性作为标签时,所有四个分类器在 SEM11 案例上的表现都略有改善,但当我们允许中性时,它们在 KWT.M-AR 数据集上的表现都显著提高。这有点令人惊讶,因为允许个别分类器选择不分配它们的标签,因此对于特定的推文,即使不允许中性,也有可能得到“未分配标签”的结果。图 10*.16*显示了当我们查看 KWT.M-AR 的+/-中性分类器时分数的变化:

精确度召回率微观 F1宏观 F1Jaccard比例
**多词法,-**中性0.4000.5590.4670.3190.3040.979
**多 LEX,+**中性0.7310.8810.7990.8170.6650.984
**多 NB,-**中性0.3380.4410.3830.2470.2360.989
**多 NB,+**中性0.6450.7810.7070.7140.5460.989
**多 SVM,-**中性0.5980.3670.4550.2940.2940.996
**多 SVM,+**中性0.7640.7630.7630.7470.6170.996
**多 DNN,-**中性0.2550.3890.3080.1940.1820.938
**多 DNN,+**中性0.7250.7760.7500.7580.5990.950

图 10.16 – KWT.M-AR,多个分类器,有无中性

在所有情况下,当我们允许中性作为标签时,召回率和精确度都会上升。

观察朴素贝叶斯(其他方法非常相似,但朴素贝叶斯给出了最佳的整体结果,因此是最有趣的)的混淆矩阵可以揭示一些信息:

愤怒厌恶恐惧快乐乐观悲观拒绝信任--
愤怒317001000023
不满01110015000094
恐惧02301000015
快乐0605612600066
02043274000155
乐观0203012300070
悲观12001020015
拒绝0100100006
信任040120002650
--65329518252731863670

图 10.17(a)- 混淆矩阵,多分类器,NB 作为子分类器,KWT.M-AR,-中性

愤怒不满意恐惧快乐爱情乐观悲观拒绝信任中性--
愤怒3530010000234
不满意011800120000924
恐惧013010000133
快乐03058123000678
爱情0202343200113811
乐观01010126000673
悲观020000200180
拒绝00001000070
信任0200100027540
中性2140151626400535210
--60333516539827363633780

图 10.17(b)- 混淆矩阵,多分类器,NB 作为子分类器,KWT.M-AR,-中性

两个表格的主要部分分数之间有一些小的差异——对角线上的分数略好,其他标签之间的混淆略低——但关键的区别是,正如之前一样,当我们没有中性作为标签时,我们会得到大量的误报。使用中性作为标签可以减少误报的数量,即使我们有多个独立的分类器,每个分类器都根据自己的推荐结果而不看其他分类器的结果。

摘要

在过去的几章中,我们研究了许多不同的分类器,并比较了它们在各种数据集上的性能。现在,是时候反思我们所学的知识了。我们针对所研究数据集的最佳分类器的最终表格如下:

SVMSNNTransformersMULTI-NBLEX, MULTI
SEM4-EN0.8450.829* 0.927
SEM11-EN0.2240.2420.418* 0.4380.347
WASSA-EN* 0.7700.7370.753
CARER-EN0.770* 0.8200.816
IMDB-EN0.7360.793* 0.826
SEM4-AR0.5140.504* 0.710
SEM11-AR0.2160.2210.359* 0.4120.377
KWT.M-AR0.6310.0280.0530.537* 0.691
SEM4-ES0.4120.337* 0.663
SEM11-ES0.2260.221* 0.3400.2940.266

图 10.18 – 综合最佳分类器

根据我们所看到的,我们可以得出几个一般性的观察:

  • 各种算法的性能差异并不大。对于每个数据集和每个设置配置,最佳分类器的性能非常相似,因此在选择分类器时,考虑训练时间和各种指标上的得分可能是明智的。特别是,没有哪个分类器在所有情况下都是最佳的,有时,非常简单的算法(LEX 和朴素贝叶斯)与更复杂的算法一样好,甚至更好。

  • 对于可以分配零个、一个或多个标签的推文数据集来说,比每个推文只分配一个标签的数据集更具挑战性。实际上,对于这些数据集,为每个推文分配恰好一个标签的分类器的性能有一个明显的上限,并且通过重新考虑分类器使用的方式可以获得最佳结果。有些分类器比其他分类器更适合这类任务,在选择这类数据集的分类器时必须考虑这一点。再次强调,在选择时考虑训练时间是有价值的:训练单个分类器然后设置N个单独的阈值比训练N个分类器要快得多,并且在至少某些情况下,性能差异很小。

  • 我们还研究了各种预处理步骤,包括使用不同的标记化器和词干提取器,并考虑使用可以建议“相似”词语来替换目标推文中训练数据中未出现的词语的算法。所有这些调整在某些情况下是有效的,而在其他情况下则不是。

我们不能过分强调这一点:没有万能的解决方案。不同的任务需要不同的分类器,在决定使用哪个分类器之前,你应该调查一系列的分类器。特别是,如果你正在处理多标签数据集,你应该考虑本章中提到的算法之一。

在训练分类器时,查看它分配的标签的混淆矩阵是一个好主意。特别是对于有大量零分配的数据集,一些分类器只需在每个情况下选择最常见类别(即中立!)就能产生相当好的 F1 和 Jaccard 分数。在选择分类器时,考虑分类器所需完成的具体任务也是一个好主意。如果你想要的只是对某个话题的意见有一个感觉,而不太关心个别推文对此说了什么,那么使用比例作为指标可以是一个有用的工具。我们将在下一章中使用这个工具,我们将探讨推文中表达的情感与一定时期内真实生活中的事件之间的联系。

第四部分:案例研究

第三部分 讨论了执行算法(EA)的多种方法,并在一组标准数据集上比较了它们的有效性。在本部分的最后,我们调查了这些方法在未与标准集相连的真实世界数据上的表现,观察了推文中表达的情感变化如何反映关键的真实世界事件。我们还检查了各种方法在应用于新数据时的鲁棒性,展示了当测试数据来自与训练数据相同的人群时表现良好的方法,在应用于新数据时可能会变得脆弱。

本部分包含以下章节:

  • 第十一章案例研究 – 卡塔尔封锁