精华内容
下载资源
问答
  • 情感极性:关于中文情感分类的知识
    千次阅读
    2017-12-06 16:55:36

    一、文本分类

    1、什么是文本分类?

    文本分类,就是在预定义的分类体系下,根据文本的特征(内容或属性),将给定文本与一个或多个类别相关联的过程。

    2、文本分类的具体步骤?

    (1)构建分类类别体系

    (2)获取带有类别标签的文本

    (3)文本的特征选择及权重计算

    (4)分类器的选择与训练

    (5)文本的分类应用

    3、类别体系

    一般的文本分类基于文本内容,将文本自动划分为政治、经济、军事、体育等等类别。按目前互联网网页内容分类,还可以覆盖更多类别,比如旅游资讯、游戏、人物访谈、音乐、影视、软件、文学、健康、美食、财经、教育、广告、犯罪、自然灾害等,每一个分类还可以拓展子分类,具体情况还需要以实际项目需求为准。

    对应每一个类别,都可以训练出对应的词特征文件。对应到类别的细分或者合并,只需要简单的将词特征文件按需要进行分拆或者合并,具有比较好的扩展性。

    4、文本获取

    互联网信息资讯非常庞大,除了各大厂商自身拥有的海量数据,还可以基于爬虫技术进行数据获取,这里有一个简单的例子:京东商城评论数据获取

    但实际上互联网上存在非常多没有标注的数据,这时候可以考虑无监督或者弱监督的方法,不过效果相比监督方法较差。这时候可以选择人工进行标注,但这部分工作异常耗费人力。

    5、文本特征选择

    (1)什么是文本?

    一个文本表现为一个由文字和标点符号组成的字符串,由字或字符组成词,由词组成短语,进而形成句、段、节、章、篇的结构。

    (2)为什么要选择特征?

    要进行文本分类,必须将文本转化为计算机可以理解的形式,这种形式要能够真实反映文本的内容并且具有将不同文档区分开的能力。

    (3)什么是特征?

    目前比较流行的方法是将文本表示为向量空间模型(VSM),如one-hot向量等。而特征是VSM中最小的不可分的语言单元,可以使字、词、词组或者短语等。一个文本可以看成是它含有的特征项所组成的集合,如进行分本分词后产生的词列表,关于分词可以参考:浅谈中文分词与jieba源码

    (4)怎么选择特征?

    在进行文本分词后,会产生大量的词,而很多词如:“”我“”,“”的“”等对于类别不具有可分性,同时如果特征数目太大,膨胀起来也会造成位数灾难等困难,所以需要对特征进行一定的选择,下面列举几种特征选择方法,不做详细介绍:

    1)基于文档频率的特征提取

    2)信息增益法

    3)卡方统计量

    4)互信息法

    6、特征权重计算

    关于文本的表示,实际上不只是特征的集合,而应该是特征与该特征的权重的某种组合。特征的权重衡量了某个特征在文本表示中的重要程度或区分能力的强弱。

    下面同样列举几种方法:

    1)布尔权重:这种方法形式成的文本表示一般称为one-hot向量

    2)TF、IDF、TF-IDF:基本文本频率或文本频率倒排的计算,TF-IDF效果相对较好

    3)基于错误驱动的方法

    这里说一说目前比较流行的word2vec,笔者不太确定这个方法是否归于这个分类底下,但从字面意义理解上很相似。word2vec严格来说并不能算是一种权重计算方法,该方法的作者实际上是提出几种训练语言模型的方法,在训练完毕后得到的副产品才是所谓的词向量,但总体来说同样起到了权重计算的方法。

    7、分类器的选择、训练与应用

    前面将文本表示构建完毕,接下来就是分类器部分了。关于分类的内容比较多,这里只是简单列举几种方法,不做介绍:

    传统的统计学习方法:支持向量机、朴素贝叶斯、knn,或者深度学习方法:CNN、RNN等等

    二、情感分类

    1、什么是情感分类?

    与上文的文本分类不同,情感分类是对带有感情色彩的主观性文本进行分析,将文本分为积极、中性、消极等类型的过程。按照处理文本的粒度不同,情感分类可以分为词语级、短语级、句子级、篇章级以及多篇章级等层次;按处理的文本类别不同,可分为基于新闻的情感分类和基于产品评论的情感分类等等。

    大部分处理过程与上述文本分类很相似,这里不多介绍。

    2、情感分类的主要方法

    (1)基于词典的方法

    基于词库的方法的关键点在于极性词库的搭建,这里可以寻找网站的开源的极性词典或者手工抽取标注,是一个比较耗费人力的过程。

    具体的方法可以简化为,对文本进行分词,之后对比极性词库,通过计算句子的正面得分,比如有多少词是正面的,与负面得分,及两者相加的综合得分,设定阈值进行比较,得到该文本的情感极性。

    (2)基于机器学习的方法

    基本上的方法和文本分类很相似,但关于情感分类在特征选择从而进行分类上有一些不同的地方

    1)特征

    情感分类中,有一种方法称为全词表法,即是将初步分词去燥之后的所有词都作为特征,结合已标注文本进行模型训练。这样的好处是单词能够全面保留,但特征维度较大。

    另一种方法称为极性词表法,同文本特征选择,将用某种方法选择出来的特征保存下来,经过人工审核后,纳入极性词表,作为文本特征进行训练,这里和基于的词典的方法有一部分工作是相同的。

    2)分类

    情感极性判断中,如果进行积极、消极、中性的判断,这是一个三分类问题。但如果是评论的极性判断,则可以进行简化。

    首先进行主客观判断,将客观语料分为中性,之后再进行正负极性的判断。这样,就可以把一个复杂的三分类问题,简化成两个二分类问题了。

    三、参考

    1、《统计自然语言处理》  宗成庆

    2、《情感分类研究进展》  陈龙

    3、https://www.qcloud.com/community/article/164816001481011844  腾讯文智

    4、https://www.qcloud.com/community/article/164816001481011804  腾讯文智

    更多相关内容
  • 微博评论数据集7962条,其中包含积极和消极情感倾向。主要做法如下: 实现语言:python、tensorflow==1.12、keras==2.2.4 一是基于传统文本特征表示的稀疏性,结合当前成熟技术,设计并实现了基于Word2vec的词向量...
  • Datawhale干货作者:太子长琴,算法工程师,Datawhale成员文本分类是自然语言处理(NLP)最基础核心的任务,或者换句话说,几乎所有NLP任务都是「分类」任务,或者涉及到「...

     Datawhale干货 

    作者:太子长琴,算法工程师,Datawhale成员

    文本分类是自然语言处理(NLP)最基础核心的任务,或者换句话说,几乎所有NLP任务都是「分类」任务,或者涉及到「分类」概念。比如分词、词性标注、命名实体识别等序列标注任务其实就是Token粒度的分类;再比如文本生成其实也可以理解为Token粒度在整个词表上的分类任务。

    本文侧重于从宏观角度(历史演变和基本流程)对文本情感分类任务进行介绍,目的是给读者提供一个整体视角,从高远处审视情感分析、文本分类、甚至NLP,期望能抛砖引玉,引发读者更多的思考。

    本文同样适合于非算法岗位的工程师,以及没有技术背景但对NLP感兴趣的伙伴。

    本文篇幅较长,主要分为四个部分。

    背景介绍:简单介绍情感分析相关的概念、类型,并和文本分类任务对应。

    基本流程:文本分类(或常见的NLP任务)基本处理流程。

    模型简史:NLP处理任务中模型变迁的简单历史。

    情感未来:探讨未来情感分析可能的发展方向。

    01 背景介绍

    情感分析是根据输入的文本、语音或视频,自动识别其中的观点倾向、态度、情绪、评价等,广泛应用于消费决策、舆情分析、个性化推荐等商业领域。包括篇章级、句子级和对象或属性级。可以分为以下三类:

    d2f06acc3eaf9e8659a440ed5eb1e011.png

    工业界最常见的往往是这种情况。比如大众点评某家餐饮店下的评论:“服务非常赞,但味道很一般”,这句话其实表达了两个意思,或者说两个对象(属性)的评价,我们需要输出类似:<服务,正向>和<口味,负向>这样的结果(<属性-倾向>二元组),或者再细一点加入用户的观点信息(<属性-观点-倾向>三元组):<服务,赞,正向>和<口味,一般,负向>。这样的结果才更有实际意义。

    从 NLP 处理的角度看,情感分析包括两种基础任务:文本分类和实体对象或属性抽取,而这也正好涵盖了 NLP 的两类基本任务:序列分类(Sequence Classification)和 Token 分类(Token Classification)。两者的区别是,前者输出一个标签,后者每个 Token 输出一个标签。

    02 基本流程

    一般来说,NLP 任务的基本流程主要包括以下五个:

    c3983d5c6eac7c99341784969e758bee.png

    2.1 文本预处理

    文本预处理主要是对输入文本「根据任务需要」进行的一系列处理。

    文本清理:去除文本中无效的字符,比如网址、图片地址,无效的字符、空白、乱码等。

    标准化:主要是将不同的「形式」统一化。比如英文大小写标准化,数字标准化,英文缩写标准化,日期格式标准化,时间格式标准化,计量单位标准化,标点符号标准化等。

    纠错:识别文本中的错误,包括拼写错误、词法错误、句法错误、语义错误等。

    改写:包括转换和扩展。转换是将输入的文本或 Query 转换为同等语义的另一种形式,比如拼音(或简拼)转为对应的中文。扩展主要是讲和输入文本相关的内容一并作为输入。常用在搜索领域。

    需要注意的是,这个处理过程并不一定是按照上面的顺序从头到尾执行的,可以根据需要灵活调整,比如先纠错再标准化或将标准化放到改写里面。咱们不能被这些眼花缭乱的操作迷惑,始终谨记,文本预处理的目的是将输入的文本变为已有系统「喜欢且接受」的形式。举个例子,比如系统在训练时都使用「单车」作为自行车的称呼,那预处理时就应该把自行车、Bike、Bicycle 等都转为单车。或者甚至系统用了某个错别字,那输入也要变成对应的错别字。

    2.2 Tokenizing(分词)

    主要目的是将输入的文本 Token 化,它涉及到后续将文本转为向量。一般主要从三个粒度进行切分:

    字级别:英文就是字母级别,操作起来比较简单。

    词级别:英文不需要,中文可能会需要。关于中文分词,之前写过一点思考,简单来说,分词主要是「分割语义」,降低不确定性,要不要分词一般要看任务和模型。

    子词:包括BPE(Byte Pair Encoding),WordPieces,ULM(Unigram Language Model)等。在英文中很常见,当然中文也可以做,是介于字级别和词级别中间的一种粒度。主要目的是将一些「统一高频」的形式单独拎出来。比如英文中 de 开头的前缀,或者最高级 est 等等。子词一般是在大规模语料上通过统计「频率」自动学习到的。

    我们需要知道的是,字和词并非哪个一定比另一个好,都要需要根据具体情况具体分析的。它们的特点如下:

    粒度
    词表大小固定无法穷尽
    未识别词(OOV)没有典型问题,长尾、稀疏性
    参数/计算量Token 变少,参数少,计算快
    语义/建模复杂度有不确定性携带语义,能够降低不确定性

    注:OOV=Out Of Vocabulary

    2.3 构造数据

    文本经过上一步后会变成一个个 Token,接下来就是根据后续需要将 Token 转为一定形式的输入,可能就是 Token 序列本身,也可能是将 Token 转为数字,或者再加入新的信息(如特殊信息、Token 类型、位置等)。我们主要考虑后两种情况。

    Token 转数字:就是将每个「文本」的 Token 转为一个整数,一般就是它在词表中的位置。根据后续模型的不同,可能会有一些特殊的 Token 加入,主要用于「分割输入」,其实就是个「标记」。不过有两个常用的特殊 Token 需要稍加说明:<UNK><PAD>,前者表示「未识别 Token」,UNK=Unknown,后者表示「填充 Token」。因为我们在实际使用时往往会批量输入文本,而文本长度一般是不相等的,这就需要将它们都变成统一的长度,也就是把短的用 <PAD> 填充上。<PAD> 一般都放在词表第一个位置,index 为 0,这样有个好处就是我们在计算时,0 的部分可以方便地处理掉(它们是「填充」上去的,本来就算不得数)。

    加入新的信息:又可以进一步分为「在文本序列上加入新的信息」和「加入和文本序列平行的信息」。

    • 序列上新增信息:输入的文本序列有时候不一定『只』是一句话,还可能会加入其他信息,比如:「公司旁边的螺蛳粉真的太好吃了。<某种特殊分隔符>螺蛳粉<可能又一个分隔符>好吃」。所以,准确来说,应该叫「输入序列」。

    • 新增平行信息:有时候除了输入的文本序列,还需要其他信息,比如位置、Token 类型。这时候就会有和 Token 序列 Token 数一样的其他序列加入,比如绝对位置信息,如果输入的句子是「今天吃了螺蛳粉很开心」,对应的位置编码是「1 2 3 4 5 6 7 8 9 10」。

    需要再次强调的是,这一步和后续使用的模型直接相关,要根据具体情况进行相应处理。

    2.4 文本特征

    根据上面构造的数据,文本特征(也可以看作对文本的表征)从整体来看可以分为两个方面:Token 直接作为特征和 Token(或其他信息)编码成数字,然后转成向量作为特征。这一小节咱们主要介绍一下从 Token 到特征形态发生了哪些变化,至于怎么去做这种转换,为什么,下一节《模型发展》中会做进一步分析。

    a. OneHot

    首先,要先明确下输入的文本最终要变成什么样子,也就是特征的外形长啥样。注意,之前得到的整数并不能直接放到模型里去计算,因为数字的大小并没有语义信息。

    那么,最简单的表示方式就是某个 Token 是否出现在给定的输入中。我们假设实现已经有一个做好的很大的词表,里面有 10w 个 Token。当我们给定 Token 序列时,词表中的每个 Token 是否出现在给定的序列中就可以构造出一个 01 向量:

    [0, 0, 1, 0, ..., 0, 1] # 10w 个

    其中 1 表示那个位置的 Token 在出现在了给定的序列中,0 则表示未出现。可以想象,对于几乎所有的输入,对应的 01 向量绝大多数的位置都是 0。假设有 m 个句子,那么会得到一个矩阵:

    # m * 10w
    [[0, 0, 1, 0, ..., 0, 1],
     [0, 1, 0, 0, ..., 0, 0],
     [0, 0, 1, 1, ..., 0, 1],
     [0, 1, 1, 0, ..., 0, 0],
     ...
     [1, 0, 0, 0, ..., 0, 1]]

    其中的每一列就表示该位置 Token 的向量表示,换句话说,在表示句子的同时,Token 也被成功地表示成了一个向量。

    上面的这种编码方式就叫 OneHot 编码,也就是把 Token 编成一个只有一个 1 其余全是 0 的向量,然后通过在语料上计算(训练)得到最后的表示。

    b. TF-IDF

    聪明的您一定会想到,既然可以用「出现」或「不出现」来表示,那为啥不用频率呢?没错,我们可以将上面的 1 都换成该词在每个句子中出现的频率,归一化后就是概率。由于自然语言的「齐夫定律」,高频词会很自然地占据了主导地位,这对向量表示大有影响,想象一下,那些概率较高的词几乎在所有句子中出现,它们的值也会更大,从而导致最终的向量表示向这些词倾斜。

    齐夫定律:在自然语言语料库中,一个单词出现的频率与它在频率表里的排名成反比。即第 n 个常见的频率是最常见频率的 1/n。举个例子来说,中文的「的」是最常见的词,排在第 1 位,比如第 10 位是「我」,那「我」的频率就是「的」频率的 1/10。

    于是,很自然地就会想到使用某种方式去平和这种现象,让真正的高频词凸显出来,而降一些类似的、是这种常见词的影响下降。咱们很自然就会想到,能不能把这些常见词从词表给剔除掉。哎,是的。这种类似的常见词有个专业术语叫「停用词」,一般呢,停用词主要是虚词居多,包括助词、连词、介词、语气词等,但也可能会包括一些「没用」的实词,需要根据实际情况处理。

    除了停用词,还有另一种更巧妙的手段处理这个问题——TF-IDF,TF=Term Frequency,IDF=Inverse Document Frequency。TF 就是刚刚提到的词频,这不是有些常用词吗,这时候 IDF 来了,它表示「有多少个文档包含那个词」,具体等于文档总数/包含该词的文档数。比如「的」在一句话(或一段文档)中概率很高,但几乎所有句子(或文档)都有「的」,IDF 接近 1;相反如果一个词在句子中概率高,但包含该词的文档比较少,IDF 就比较大,最后结果也大。而这是满足我们预期的——词在单个文档或句子中是高概率的,但在所有文档或句子中是低概率的,这不正说明这个词对所在文档或句子比较重要吗。实际运算时,一般会取对数,并且防止 0 除:

    这时候的向量虽然看着不是 OneHot,但其实本质还是,只是在原来是 1 的位置,变成了一个小数。

    c. Embedding

    刚刚的得到的矩阵最大的问题是维度太大,数据稀疏(就是绝大部分位置是 0),而且词和词之间是孤立的。最后这个问题不用多解释,这样构建的特征肯定「不全面」。但是维度太大和数据稀疏又有什么影响呢?首先说前者,在《文献资料:文本特征》第一篇文章提到了在超高维度下的反直觉现象——数据不会变的更均匀,反而会聚集在高维空间的角落,这会让模型训练特别困难。再说后者,直观来看,数据稀疏最大的问题是使得向量之间难以交互,比如「出差住酒店」和「出差住旅店」,酒店和旅店在这里意思差不多,但模型却无法学习到。

    既然如此,先辈们自然而然就想能不能用一个连续稠密、且维度固定的向量来表示。然后,大名鼎鼎的「词向量」就登场了——它将一个词表示为一个固定维度大小的稠密向量。具体来说,就是首先随机初始化固定维度大小的稠密向量,然后用某种策略通过词的上下文自动学习到表征向量。也就是说,当模型训练结束了,词向量也同时到手了。具体的过程可参考《文献资料:文本特征》第二篇文章。

    「词向量」是一个划时代的成果,为啥这么说呢?因为它真正把自然语言词汇表征成一个可计算、可训练的表示,带来的直接效果就是自然语言一步跨入了深度学习时代——Embedding 后可以接各种各样的模型架构,完成复杂的计算。

    2.5 结果输出

    当用户的输入是一句话(或一段文档)时,往往需要拿到整体的向量表示。在 Embedding 之前虽然也可以通过频率统计得到,但难以进行后续的计算。Embedding 出现之后,方法就很多了,其中最简单的当然是将每个词的向量求和然后平均,复杂的话就是 Embedding 后接各种模型了。

    那么在得到整个句子(或文档)的向量表示后该如何得到最终的分类呢?很简单,通过一个矩阵乘法,将向量转为一个类别维度大小的向量。我们以二分类为例,就是将一个固定维度的句子或文档向量变为一个二维向量,然后将该二维向量通过一个非线性函数映射成概率分布。

    举个例子,假设最终的句子向量是一个 8 维的向量,w 是权重参数,计算过程如下:

    import numpy as np
    
    rng = np.random.default_rng(seed=42)
    embed = rng.normal(size=(1, 8)).round(2)
    # array([[ 0.3 , -1.04,  0.75,  0.94, -1.95, -1.3 ,  0.13, -0.32]])  维度=1*8
    w = rng.normal(size=(8, 2)).round(2)
    """ 维度=8*2
    array([[-0.02, -0.85],
           [ 0.88,  0.78],
           [ 0.07,  1.13],
           [ 0.47, -0.86],
           [ 0.37, -0.96],
           [ 0.88, -0.05],
           [-0.18, -0.68],
           [ 1.22, -0.15]])
    """
    z = embed @ b # 矩阵乘法
    # array([[-2.7062,  0.8695]]) 维度=1*2
    
    def softmax(x): 
        return np.exp(x) / np.sum(tf.exp(x), axis=1)
    
    softmax(z)
    # array([[0.0272334, 0.9727666]])

    这个最后的概率分布是啥意思呢?它分别表示结果为 0 和 1 的概率,两者的和为 1.0。

    您可能会有疑问或好奇:参数都是随机的,最后输出的分类不对怎么办?这个其实就是模型的训练过程了。简单来说,最后输出的概率分布会和实际的标签做一个比对,然后这个差的部分会通过「反向传播算法」不断沿着模型网络往回传,从而可以更新随机初始化的参数,直到最终的输出和标签相同或非常接近为止。此时,我们再用训练好的网络参数计算,就会得到正确的标签。

    03 模型简史

    这部分我们主要简单回顾一下 NLP 处理情感分类任务模型发展的历史,探讨使用了什么方法,为什么使用该方法,有什么问题等。虽然您看到的是情感分类,其实也适用于其他类似的任务。

    fe1c1c1470d6870988c6d0d73d08c9d3.png

    3.1 词典/规则

    在 NLP 发展的初级阶段,词典和规则的方法是主流。算法步骤也非常简单:

    • 事先收集好分别代表正向和负向的词表。比如正向的「开心、努力、积极」,负向的「难过、垃圾、郁闷」

    • 对给定的文本分词

    • 分别判断包含正向和负向词的数量

    • 包含哪类词多,结果就是哪一类

    可以看出这个非常简单粗暴,模型就是这两个词表,整个算法的核心就是「基于匹配」,实际匹配时,考虑到性能一般会使用 Trie 或自动机进行匹配。这种方法的主要特点包括:

    简单:意味着成本低廉,非常容易实施。

    歧义:因为一个个词是独立的,没有考虑上下文,导致在否定、多重否定、反问等情况下会失败,比如「不开心、不得不努力、他很难过?不!」

    无法泛化:词表是固定的,没有出现在词表中的词,或稍微有些变化(比如单个字变了)就会导致识别失败。

    虽然有不少问题,但词典/规则方法直到现在依然是常用的方法之一,这背后根本的原因就在于我们在自然语言理解时往往会特别关注到其中的「关键词」,而语序、助词等往往没啥别特影响^_^

    3.2 机器学习

    随着统计在 NLP 领域的使用,「基于频率」的方法开始风靡,最简单常用的模型就是 Ngram,以及基于 Ngram 构建特征并将之运用在机器学习模型上。这一阶段的主要特点是对数据进行「有监督」地训练,效果自然比上一种方法要好上很多。

    Ngram 其实很简单。比如给定一句话「人世间的成见就像一座大山」,如果以字为粒度,Bigram 结果是「人世 世间 间的……」,Trigram 自然就是「人世间 世间的 间的成……」,4-Gram,5-Gram 以此类推。不过实际一般使用 Bigram 和 Trigram 就够了。

    Ngram 本质是对句子进行语义分割(回想前面提到的「分词的意义」),也可以看成是一种「分词」。所以之前构建文本特征的 Token 也都可以换成 Ngram。现在从概率角度考虑情感分类问题,其实就是解决下面这个式子:

    情感倾向给定序列)给定序列情感倾向情感倾向给定序列给定序列情感倾向

    这里给定的 Token 可以是字、词或任意的 Ngram,甚至是彼此的结合。由于分母在不同类型下是一样的,所以可以不考虑,主要考虑分子部分。又由于类别的概率一般是先验的,因此最终就成了解决给定情感倾向得到给定 Token 序列的概率。上面的式子也叫贝叶斯公式,如果不考虑给定 Token 之间的相关性,就得到了朴素贝叶斯(Naive Bayes):

    给定序列情感倾向情感倾向情感倾向情感倾向

    此时,我们只需要在正向和负向语料上分别计算 Token 的概率即可,这个过程也叫训练,得到的模型其实是两个 Token 概率表。有了这个模型,再有新的句子过来时,Token 化后,利用(2)式分别计算正向和负向的概率,哪个高,这个句子就是哪种类别。深度学习之前,Google 的垃圾邮件分类器就是用该算法实现的。

    可以发现,这种方法其实是考虑了两种不同类型下,可能的 Token 序列概率,相比上一种方法容错率得到了提高,泛化能力增加。需要注意的是,歧义本质是使用 Ngram 解决的,这也同样适用于上一种方法。由于类似于直接查表,所以这种方法本质上是 OneHot 特征,只不过是直接用了 Token 本身(和它的概率)作为特征。另外,无论是哪种「文本特征」,都是可以直接运用在机器学习模型上进行计算训练的。

    这里的核心其实是「基于频率」建模,实际会使用 Ngram,通过 OneHot、TF-IDF 等来构建特征。这种方法的主要问题是:维度灾难、数据稀疏、词孤立等,在『文本特征』一节已做相应介绍,这里不再赘述。

    3.3 深度学习

    深度学习时代最大的不同是使用了稠密的 Embedding,以及后面可接各式各样的神经网络,实际上是一种「基于上下文」的建模方式。我们以 NLP 领域经典的 TextCNN 架构来进行说明。

    069e4bb0a24c8e6daa09edc7b7541cb2.png

    假设每个词是 6 维的(如图所示),每个词旁边的格子里都对应着该词的 Embedding,整个那一片可以叫输入句子的 Embedding 矩阵。我们现在假设有一个 3×6 的矩阵,里面的值是要学习的参数,一开始是随机初始化的。这个 3×6 的矩阵叫 Kernel,它会沿着句子 Embedding 矩阵从上往下移动,图例中的步幅是 1,每移动一步,Kernel 和 3 个词的 Embedding 点乘后求和后得到一个值,最后就会得到一个一维向量(卷积层 Convolutional layer),然后取最大值或平均值(池化层 Pooling),就会得到一个值。如果我们每次使用一个值不一样的 Kernel,就会得到一组不同的值,这个就构成了输入句子的表征,通过类似前面『结果输出』中的计算,就会得到最终的概率分布。

    这里有几点需要说明一下:

    第一,我们可以使用多个不同大小的 Kernel,刚刚用了 3,还可以用 2、4 或 5,最后得到的向量会拼接在一起,共同作为句子的标准。

    第二,Embedding 矩阵也是可以作为参数在训练时初始化的,这样模型训练完了,Embedding 顺便也就有了(此时因为最终结果都已经有了,往往该矩阵也没啥意义,不需要单独拿出来考虑),或者也可以直接使用已经训练好的 Embedding 矩阵。

    第三,除了 TextCNN 还有其他很多模型也可以做类似的事,大同小异。

    Embedding 有效地解决了上一种方法的问题,但它本身也是有一些问题的,比如没考虑外部知识,这就进入了我们下一个时代——预训练模型。

    3.4 预训练+微调

    预训练模型「基于大规模语料训练」,其本质是一种迁移学习——将大规模语料中的知识学习到模型中,然后用在各个实际的任务中,是对上一种方法的改进。我们以百度的 ERNIE 为例说明。

    cc8aa3603598938d69fe1b8b9e1afced.png

    首先是它的输入比上一种方法多了新的信息,但最终每个 Token 依然会得到一个 Embedding,然后经过一个复杂的预训练模型 ERNIE 就会得到最终的输出向量(类似上个方法的中间步骤),进而就可以得到标签的概率分布。预训练模型已经在很多 NLP 任务上达到了最好的效果。

    当然,要说预训练模型有什么不好,那就是太大太贵了,大是指模型大以及参数规模大(起码上亿),贵则是训练过程需要消耗的资源很多。不过大多数情况下,我们并不需要自己训练一个,只要使用开源的即可,因为预训练的语料足够大,几乎涵盖了所有领域,大部分时候会包含您任务所需要的信息。

    以上涉及的代码可以参考:http://nbviewer.org/github/hscspring/All4NLP/blob/master/Senta/senta.ipynb

    04 探讨展望

    4.1 实际应用

    上面介绍了那么多的方法和模型,这里主要探讨一下在实际应用过程中的一些取舍和选择。

    规则 VS 模型:纯规则、纯模型和两者结合的方法都有。规则可控,但维护起来不容易,尤其当规模变大时,规则重复、规则冲突等问题就会冒出来;模型维护简单,有 bad case 重新训练一下就行,但可能需要增加语料,另外过程也不能干预。实际中,简单任务可以使用纯模型,复杂可控任务可以使用模型+规则组合,模型负责通用大多数,规则覆盖长尾个案。

    深度 VS 传统:这个选择其实比较简单,当业务需要可解释时,可以选择传统的机器学习模型,没有这个限制时,应优先考虑深度学习。

    简单 VS 复杂:当任务简单,数据量不大时,可以用简单模型(如 TextCNN),此时用复杂模型未必效果更好;但是当任务复杂,数据量比较多时,复杂模型基本上是碾压简单模型的。

    总而言之,使用什么方案要综合考虑具体的任务、数据、业务需要、产品规划、资源等多种因素后确定。

    4.2 情感的未来

    主要简单畅想一下未来,首先可以肯定的是未来一定是围绕着更深刻的语义理解发展的。从目前的发展看,主要有以下几个方向。

    1、多模态:多种不同形态的输入结合。包括:文本、图像、声音等,或者文本、视频。这个也是目前比较前沿的研究方向,其实也是很容易理解的。因为我们人类往往都会察言观色,听话听音,其实就是从多个渠道接收到「信息」。换成机器,自然也可以做类似的事情,预期来看,效果必然是有提升的。举个例子,比如就是简单的「哈哈哈哈」几个字,如果单纯从文本看就是「大笑」,但如果配上图像和声音,那可能就变成「狂暴」、「淫邪」、「假笑」等可能了。

    2、深度语义:综合考虑率多种影响因素。包括:环境、上下文、背景知识。这点和第一点类似,也是尽量将场景「真实化」。

    环境指的是对话双方当前所处的环境,比如现在是冬天,但用户说「房间怎么这么热」,其实可能是因为房间空调或暖气开太高。如果不考虑环境,可能就会难以理解(这对人来说也是一样的)。

    上下文指的是对话中的历史信息,比如开始对话时用户说「今天有点感冒」,后面如果再说「感觉有点冷」,那可能是生病导致的。如果没有这样的上下文记忆,对话可能看起来就有点傻,对情感的理解和判断也会不准确。其实上下文在多轮对话中已有部分应用,但还远远不够,主要是难以将和用户所有的历史对话都结构化地「连接」在一起。目前常用的也是根据用户的「行为」数据对其「画像」。

    背景知识是指关于世界认知的知识。比如用户说「年轻人,耗子尾汁」,如果机器人没有关于马保国相关的背景知识就很难理解这句话是啥意思。这块目前在实践的是知识图谱,其实就是在做出判断时,多考虑一部分背景知识的信息。

    3、多方法:综合使用多种方法。包括:知识图谱、强化学习和深度学习。这是从方法论的角度进行思考,知识图谱主要是世界万物及其基本关系进行建模;强化学习则是对事物运行的规则建模;而深度学习主要考虑实例的表征。多种方法组合成一个立体完整的系统,这里有篇不成熟的胡思乱想对此进行了比较详细的阐述。

    文献资料

    这部分作为扩展补充,对相应领域有兴趣的伙伴可以顺着链接了解一下:

    • 业界应用

     情感分析技术在美团的探索与应用:https://mp.weixin.qq.com/s/gXyH4JrhZI2HHd5CsNSvTQ

     情感计算在UGC应用进展:https://mp.weixin.qq.com/s/FYjOlksOxb255CvNLqFjAg

    • 预处理

    NLP 中的预处理:使用 Python 进行文本归一化:https://cloud.tencent.com/developer/article/1625962)

    当你搜索时,发生了什么?(中) | 人人都是产品经理:http://www.woshipm.com/pd/4680562.html

    全面理解搜索 Query:当你在搜索引擎中敲下回车后,发生了什么?:https://zhuanlan.zhihu.com/p/112719984

    • Tokenize

    深入理解NLP Subword算法:BPE、WordPiece、ULM:https://zhuanlan.zhihu.com/p/86965595

    Byte Pair Encoding — The Dark Horse of Modern NLP:https://towardsdatascience.com/byte-pair-encoding-the-dark-horse-of-modern-nlp-eb36c7df4f10

    • 文本特征

    The Curse of Dimensionality in Classification:https://www.visiondummy.com/2014/04/curse-dimensionality-affect-classification/

    word2vec 前世今生:https://www.cnblogs.com/iloveai/p/word2vec.html

    • 情感未来

    多模态情感分析简述:https://www.jiqizhixin.com/articles/2019-12-16-7

    千言数据集:情感分析:https://aistudio.baidu.com/aistudio/competition/detail/50/0/task-definition

    86c4e04c4dfd6935681c9285712bca95.png

    太子长琴

    算法工程师

    个人博客:https://yam.gift/

    作品推荐:我转行AI的成长心得

    436517efb97435d262e811d8df164442.png

    整理不易,三连

    展开全文
  • 准备数据 电影评论数据下载地址 将文本数据处理成torch,我们希望可以得到的target是他的评论态度是积极还是消极,将数据分为2500训练,2500测试,这里网址下载的数据数量已经分好了,利用pytorch进行文本处理 ...

    准备数据

    电影评论数据下载地址
    将文本数据处理成torch,我们希望可以得到的target是他的评论态度是积极还是消极,将数据分为2500训练,2500测试,这里网址下载的数据数量已经分好了,利用pytorch进行文本处理

    处理数据

    遇到这个问题
    RuntimeError: each element in list of batch should be of equal size
    这里重写一下Dataloader中的collate_fn方法

    import os
    import re
    from lib import ws,max_len
    import torch
    from torch.utils.data import Dataset,DataLoader
    
    # 将一个句子分为由单词组成的列表
    def tokenize(str):
        # 这个是re库里的函数,其原型为re.sub(pattern, repl, string, count)
        # 第一个参数为正则表达式需要被替换的参数,第二个参数是替换后的字符串,
        # 第三个参数为输入的字符串,第四个参数指替换个数。默认为0,表示每个匹配项都替换。
        filter = ['!','"','#','$','%','&','\(','\)','\*','\+',',','-','\.','/',':',';','<','=','>','\?','@'
            ,'\[','\\','\]','^','_','`','\{','\|','\}','~','\t','\n','\x97','\x96','”','“']
        token = re.sub('|'.join(filter),'',str)
        token = token.strip()
        return token.split(' ')
    
    
    class Datas(Dataset):
        def __init__(self,train=True):
            self.train_path = r'data/aclImdb/train'
            self.test_path = r'data/aclImdb/test'
            temp_path = self.train_path if train else self.test_path
            data_path = [temp_path+'/pos', temp_path+'/neg']
            # 将含评论的所有文件放在列表中
            self.files = []
            for i in data_path:
                listdirs = os.listdir(i)
                data_path2 = [i+'/'+j for j in listdirs]
                self.files.extend(data_path2)
    
        def __len__(self):
        # 有多少评论数据
            return len(self.files)
    
        def __getitem__(self, index):
            file_path = self.files[index]
            label = file_path.split('/')[-2]
            label = 1 if label == 'pos' else 0
            # 这里label就是我们想预测的
            # 从文件中读到的是真实值
            content = open(file_path,encoding='utf-8').read()
            # 返回经过分词的句子列表
            return label,tokenize(content)
    
    # 需要重写collate_fn方法
    def collate_fn(batch):
        #  batch是一个列表,其中是一个一个的元组,每个元组是dataset中_getitem__的结果
        batch = list(zip(*batch))
        labels = torch.tensor(batch[0], dtype=torch.int64)
        texts = batch[1]
        texts = [ws.transform(text,max_len=max_len) for text in texts]
        texts = torch.LongTensor(texts)
        # labels = torch.LongTensor(labels)
        del batch
        # 返回真实值,评论(特征值)
        return labels, texts
    
    
    def get_dataloader():
        data = Datas(True)
        dataloader = DataLoader(dataset=data, batch_size=64, shuffle=True, collate_fn=collate_fn)
        # 传入经过分词的64个评论一个batch进入到数据加载器中
        return dataloader
    
    
    if __name__ == '__main__':
    
        # print(tokenize("e D:/test/deep_learing/test/text_test.py data/aclImdb/train/pos/0_9.txt pos1"))
        for idx,(input,out) in enumerate(get_dataloader()):
            print(idx)
            print(input)
            print(out)
            break
    
    

    在这里插入图片描述
    结果是这样,第二行的tensor代表的是两个评论的label都是0因为,第三行是两个评论的文本分词
    得到了评论的数据之后,我们需要对词语进行序列化
    即把词语转换为数字

    序列化

    class word2sequense():
    # 定义当我们训练出来的词典的两个关键词:UNK就是字典里没有出现的,APP就是由于句子长度不够,填充的符号
        unknown_tag = 'UNK'
        append_tag = 'APP'
        UNK = 0
        APP = 1
    
        def __init__(self):
        # 定义词典
            self.dict = {
                self.unknown_tag:self.UNK,
                self.append_tag:self.APP
    
            }
        # 统计词频
        count = {}
    
        def fit(self,sentence):
            for word in sentence:
                self.count[word] = self.count.get(word,0) + 1
    
        def filter_count(self,min=5,max=None,max_features=None):
            """
            :param min:最小词频
            :param max: 最大词频
            :param max_features:一共保存的词语个数
            :return:
            """
            # 去掉词频小于min的
            self.count = {word:value for word,value in self.count.items() if value > min}
            # 去掉大于max的
            if max is not None:
                self.count = {word: value for word, value in self.count.items() if value < max}
            if max_features is not None:
                # 把count进行排序,取出前max_features个
                temp = sorted(self.count.items(),key=lambda x:x[-1],reverse=True)[:max_features]
                # 转回字典
                self.count = dict(temp)
            # 将单词用数字表示,数字就是第几个加到词典的
            for word in self.count:
                self.dict[word] = len(self.dict)
            # 将数字用单词表示
            self.dict2 = dict(zip(self.dict.values(),self.dict.keys()))
    
        def transform(self,sentence,max_len=None):
            if max_len is not None:
                # 将句子长度统一
                if max_len>len(sentence):
                # 当长度不够的时候,在句子后面加上对应数量的app符号
                    sentence += [self.APP]*(max_len-len(sentence))
                if max_len<len(sentence):
                    # 裁剪后面的
                    sentence = sentence[:max_len]
            # 将句子转换成序列
            # for word in sentence:
            #     self.dict.get(word,self.UNK)
            return [self.dict.get(word,self.UNK) for word in sentence]
            # 从dict里找单词的编号,找不到为0
    
        def inverse_trans(self,indics):
            # 将序列转换成句子
            return [self.dict2.get(indx) for indx in indics]
     
        def __len__(self):
            return len(self.dict)
    
    
    if __name__ == '__main__':
        word2 = word2sequense()
        word2.fit(['日','你','妈'])
        word2.fit(['退','钱'])
        word2.fit(['中','果','足','球','冲','进','世界','杯'])
        word2.filter_count(min=0)
        print(word2.dict)
        print(word2.dict2)
    
    

    结果是这样
    在这里插入图片描述
    在这里将使用pickle将以上保存起来

    import pickle
    from tqdm import tqdm
    # 显示进度
    import os
    from text_test import tokenize
    from word2 import word2sequense
    ws = word2sequense()
    # ws就是我们的序列化器
    # 传入要训练的句子,从train文件中将所有的评论文件放入
    train_path = r'data/aclImdb/train'
    data_path = [train_path + '/pos', train_path + '/neg']
    files = []
    for i in data_path:
        listdirs = os.listdir(i)
        datas_path = [i + '/' + j for j in listdirs]
        for data in tqdm(datas_path):
            sentence = tokenize(open(data,encoding='UTF-8').read())
            ws.fit(sentence)
    ws.filter_count()
    # 将训练好的词典保存起来
    pickle.dump(ws, open(r'model/ws.okl','wb'))
    print(len(ws))
    

    在这里如果出现了open error找不到文件,创建就好了不知道为什么wb模式自动创建没有成功,不过我们手动创建的小效果一样
    如果出现了编码的问题,open的时候加入encoding='utf-8’即可,具体情况具体分析.
    在这里保存之后,后续使用pickle.load(model/ws.okl)进行载入
    去之前的最开始的地方进行检测
    对前面的collate_fn进行部分修改

    def collate_fn(batch):
        #  batch是一个列表,其中是一个一个的元组,每个元组是dataset中_getitem__的结果
        batch = list(zip(*batch))
        labels = torch.tensor(batch[0], dtype=torch.int32)
        texts = batch[1]
        texts = [ws.transform(text) for text in texts]
        del batch
        return labels, texts
    

    修改后的运行结果
    在这里插入图片描述
    从之前的词语成功的变为数字,达到了目的
    ,至此数据处理完成

    模型建立

    在这里插入图片描述
    模型建立之后遇到了问题,注意以下label和text的顺序,就好了

    import torch.nn as nn
    from lib import ws,max_len
    import torch.nn.functional as f
    from torch.optim import Adam
    
    
    class mymodel(nn.Module):
        def __init__(self):
            super(mymodel, self).__init__()
            self.embedding = nn.Embedding(len(ws),100)
            # 传入参数:词的数量,多少个词形成一个向量
            self.fc = nn.Linear(max_len*100,2)
    
        def forward(self,input):
            """
            :param input:[batch_size,句子长度(max_size)]
            :return:
            """
            x = self.embedding(input)  # x的形状为[batch_size,句子长度(max_size),100]
            # 变换形状
            x = x.view([-1,max_len*100])
            out = self.fc(x)
            return f.log_softmax(out)
    
    
    from text_test import get_dataloader
    
    model = mymodel()
    # 优化器,传入参数和学习率
    optimizer= Adam(model.parameters(),0.001)
    
    
    def train():
        for idx,(out,input) in enumerate(get_dataloader()):
            optimizer.zero_grad()
            pre = model(input)
            # 计算损失
            loss = f.nll_loss(pre,out)
            loss.backward()
            optimizer.step()
            print(loss.item())
    
    
    if __name__ == '__main__':
        train()
    
    
    

    运行结果:
    在这里插入图片描述

    RNN循环神经网络的使用进行优化

    这里只是将几个单词看成一个向量,训练的效果并不好
    循环神经网络的使用,可以让数据具有时序性,即考虑的不仅仅是当前的输入,也考虑之前的输入来调节参数的概率从而使训练效果更好
    具有短期记忆–rnn循环神经网络

    RNN分类

    在这里插入图片描述

    使用LSTM解决RNN中的长期依赖问题(long-Term Dependencies)

    rnn存在间隔长的词语,导致前面的词语对后面的影响小的问题,即长期依赖问题
    LSTM的三个输入:
    上一次的隐藏状态+上一次的细胞状态(记忆)+本次的输入
    三个输出:
    隐藏状态+细胞状态+输出,输出和隐藏状态是一样的
    输入的形状:
    input : [seq_len,batch,embedding size]
    输出的形状:
    output:[seq_len, batch, num_directions * hidden_size]—>batch_first=False
    ,/其中hidden_size为一层神经系统里有多少lstm,num_direction是由单向lstm还是双向决定的
    单向为1,双向为2
    h_n(隐藏状态):(num_layers * num_directions, batch, hidden_size)
    c_n(细胞状态): (num_layers * num_directions, batch, hidden_size)
    num_layers为有多少层神经系统

    实例化LSTM

    torch.nn.LSTM(input_size,hidden_size,num_layers,batch_first,dropout,bidirectional)

    input_size:输入数据的形状,即embedding_dim
    hidden_size:隐藏层神经元的数量,即每一层有多少个LSTM单元
    num_layer :即RNN的中LSTM单元的层数
    batch_first:默认值为False,输入的数据需要[seq_len,batch,feature],如果为True,则为[batch,seq_len,feature]
    dropout:dropout的比例,默认值为0。dropout是一种训练过程中让部分参数随机失活的一种方式,能够提高训练速度,同时能够解决过拟合的问题。这里是在LSTM的最后一层,对每个输出进行dropout
    bidirectional:是否使用双向LSTM,默认是False

    import os
    
    import torch
    import torch.nn as nn
    from lib import ws,max_len
    import torch.nn.functional as f
    from torch.optim import Adam
    import lib
    
    
    class mymodel(nn.Module):
        def __init__(self):
            super(mymodel, self).__init__()
            self.embedding = nn.Embedding(len(ws),100)
            # 传入参数:词的数量,多少个词形成一个向量
            self.fc = nn.Linear(lib.hidden_size*2,2)
            # LSTM优化,实例化
            self.LSTM= nn.LSTM(input_size=100,hidden_size=lib.hidden_size,
                               num_layers=lib.layer_num,bidirectional=lib.biderational,
                               batch_first=True,dropout=lib.dropout)
    
        def forward(self,input):
            """
            :param input:[batch_size,句子长度(max_size)]
            :return:
            """
            x = self.embedding(input)  # x的形状为[batch_size,句子长度(max_size),100]
            # 变换形状
            # lstm处理
            x,(h_n,c_n) = self.LSTM(x)
            # x的形状[batch_size,句子长度(max_size),batch_size*2(方向)],h_n的形状[方向*层数(4),batch_size,hidden_size,]
            # c_n的形状[方向*层数(4),batch_size,hidden_size]
            # 获取两个方向的output进行拼接,第一维是层数,第一层正向的最后一个,即层数-2,第二层就是-1
            output_fw = h_n[-2,:,:]
            output_bw = h_n[-1,:,:]
            output = torch.cat((output_fw,output_bw),dim=-1)
            # output形状为[batch_size,hidden_size*2]
            # x = x.view([-1,max_len*100])
            out = self.fc(output)
            return f.log_softmax(out)
    
    
    from text_test import get_dataloader
    
    model = mymodel()
    # 优化器,传入参数和学习率
    optimizer= Adam(model.parameters(),0.001)
    if os.path.exists('model/model1.okl'):
        model.load_state_dict(torch.load('model/model1.kl'))
        optimizer.load_state_dict(torch.load('model/opt1.kl'))
    
    
    def train():
        for idx,(out,input) in enumerate(get_dataloader()):
            optimizer.zero_grad()
            pre = model(input)
            # 计算损失
            loss = f.nll_loss(pre,out)
            loss.backward()
            optimizer.step()
            print(loss.item())
            # 保存模型
            if idx % 100 == 0:
                torch.save(model.state_dict(),'model/model1.okl')
                torch.save(optimizer.state_dict(),'model/opt1.okl')
    
    
    if __name__ == '__main__':
        train()
    
    
    

    进行训练的时候.使用cpu训练的时间较长采用gpu计算

    校验模型

    模型的检验使用训练好的模型,将test数据放入,获取到误差的平均值和准确值作为模型评判

    展开全文
  • 带有表情符号的文本情感分类实验

    热门讨论 2021-08-11 12:46:00
    表情的情感分类已经完成 文本处理 下面是文字处理的部分,也就是specialize.py部分,复现了论文中的一些关于文字权重的操作,并且进行了一些改进。首先由于给的数据集是句子,而且其中有很多诸如@、//之类的杂质对...

    一点感想

    这是本人的内容安全课堂作业,参考一些网上代码与文章,汇报时排在前面的大佬都是各种深度学习曲线分析,参数调整,获得了教辅博士的大力表扬,本人啥也不会,只能选择一篇简单的论文进行复现,并且进行若干改进,在此记录以避免踩坑。

    涉及知识点

    中文分词(词性分析),TF-IDF,朴素贝叶斯,人工神经网络

    论文内容

    进入正文,首先进行论文复现,本论文是基于表情符分析的情感关键句提取方法,里面用到了基于表情符分析的句子情感极性计算、关键词计算、位置信息计算
    其中基于表情符分析的句子情感极性计算是对现有方法的改进,通过融入表情符分析提升情感关键句提取的正确率,论文如下
    在这里插入图片描述
    这篇文章主要是介绍表情符分类的若干新方式与新模型,并未提出实现的具体方式,故代码实现时做了一些变化。

    论文复现

    表情符处理

    首先是进行表情符的提取,因为表情符都有[]包裹,所以根据此特征进行匹配提取,所有数据集里的表情符都存至一个数组中,供后面进行处理,首先是第一个函数pre_processer.py介绍
    在这里插入图片描述
    然后将这些表情符进行存储,避免后面进行重复读取,浪费时间
    在这里插入图片描述

    下一个函数进行表情的分类,这里参考论文中的共现率判断方式来评判未知表情的情感,但需要说明的是这里和论文中的复现基础不同,论文中的数据集是采用词向量的训练集,而这里给出的是句向量的标签信息,所以方式做了一个调整,判断该表情在积极句子中出现的次数与消极句子中出现的次数,同时也有中性的句子
    在这里插入图片描述

    当积极占比大于0.8时就是积极表情,消极同理,其余的由于情感不明显,归入中立的表情当中,分别存入三个数组,至此,表情的情感分类已经完成

    文本处理

    在这里插入图片描述
    下面是文字处理的部分,也就是specialize.py部分,复现了论文中的一些关于文字权重的操作,并且进行了一些改进。首先由于给的数据集是句子,而且其中有很多诸如@、//之类的杂质对分析不利
    在这里插入图片描述
    所以首先应该做的就是数据清洗的工作,首先进行匹配过滤与正则过滤,把一些肉眼可见出现频率很高的无意义符号给去除
    在这里插入图片描述
    随后使用停词过滤
    在这里插入图片描述
    这里发现之前的停用词表存在一些不足, 具体停用词表可自行百度,因为其中存在一些诸如“很”、“特别”之类代表感情的词语,而在这里过滤掉之后影响后文分析,所以手动删除了所有的情感词,只留下一部分过滤无用词与符号,随后进行jieba分词,用作词性的标注
    在这里插入图片描述
    之后是常规程序TF-IDF选择每个句子的重要词成分,这里每个句子选择5个关键词,以避免后面的训练太慢
    在这里插入图片描述
    这里面也用到了论文中所说的副词重要性与位置的重要性进行权重的调整,比如说下面的权重数组
    在这里插入图片描述
    并且进行权重的修正,例如写一个简单的权重返回函数
    在这里插入图片描述

    还有尾词语比中部词语权重增加一倍,生成一个speciallist的特征向量,在后面进行训练的时候加入。这部分是对文字的训练。

    构建模型训练

    随后就是进行训练的部分,首先是读取数据集,定义初始权重
    在这里插入图片描述
    正面负面与中性的词语和表情权重分别为1、0、-1,然后先直接进行朴素贝叶斯训练,方便与后文实验效果做对比,值得一提的是文中是对词语进行分别打分计算权重,但这里复现时数据集不具备条件,于是赋予基础权重,也就是该词语所在句子超过80%情感的权重。
    在这里插入图片描述
    这里的六万条数据集进行交叉选取数据集与训练集,并且调库进行打分
    在这里插入图片描述
    而后的分类是对普通朴素贝叶斯的一个权重修正
    在这里插入图片描述
    可以看到加入了前面进行处理的特征向量,并且与正确结果进行对比,直接算比例,这里结果中可以看见提升非常明显,从百分之五十多直接提升到百分之八十多,说明论文中提出的方法明显有效(结果写在后文)。
    在这里插入图片描述
    前面都是定义方法,主体函数如下
    在这里插入图片描述
    三个类型分别进行训练并且叠加,因为实验二使用人工神经网络效果还不错,所以这里在后面也加了一个人工神经网络进行处理在这里插入图片描述

    也没有用工具进行调参,自己写了两个循环跑了一晚上,写入文件自己去判断参数优化情况,得出得分较高的参数组合,得出的结果也有了比较明显的优化。

    结果

    运行代码,得运行结果:
    在这里插入图片描述
    可以看出,用表情辅助对文本数据进行分类情感分析的效果,比单独的朴素贝叶斯分类效果要好一些。
    在这里插入图片描述在这里插入图片描述
    而深度学习又比单纯朴素贝叶斯效果要更好

    代码

    表情处理

    import csv
    # 获取某一评论中存在的表情 参数:path:文件路径 emotion_set:用来存储表情列表
    def get_emotion(path, emotion_set):
        with open(path, encoding= 'utf-8-sig') as f:
            reader = csv.reader(f)
            rows=[row for row in  reader]
            for each in rows:
                for i in range(len(each[0])):
                    if each[0][i] == '[' : # 判断是否为表情符号
                        temp = ''
                        for k in range(100):
                            if i+k > len(each[0]) - 1:
                                break
                            temp = temp + each[0][i+k]
                            # print(each[i+k])
                            if each[0][i+k] == ']':
                                if temp not in emotion_set:
                                    emotion_set.append(temp) # 若为表情符号则存储在该列表中,使每个符号只出现一次
                                # print(temp)
                                break
        return emotion_set
    
    
    # 整合三个文件获取的表情存储于emotion_result_sat,存储于csv
    def Save_as_File(save_path, emotion_result_set):  
        emotion_set = []
        Star_emotion_set = get_emotion('明星.csv', emotion_set)
        Hotspot_emotion_set = get_emotion('热点.csv', emotion_set)
        Epidemic_emotion_set = get_emotion('疫情.csv', emotion_set)
        for each in Star_emotion_set:
            if each not in emotion_result_set:
                emotion_result_set.append(each)
        for each in Hotspot_emotion_set:
            if each not in emotion_result_set:
                emotion_result_set.append(each)
        for each in Epidemic_emotion_set:
            if each not in emotion_result_set:
                emotion_result_set.append(each)
                
        with open(save_path, 'w', encoding='utf-8-sig') as f:
            for each in emotion_result_set:
                write=csv.writer(f)
                write.writerow(each)
        return emotion_result_set
    
    
    # 根据评论标签1或-1的多少判断表情是积极还是负面,积极的存储于PE_set,负面的存储于NE_set
    def creat_Respiratory(emotion_result_set):
        PE_set = []
        NE_set = []
        Neu_set = []
        with open('明星.csv',encoding= 'utf-8-sig') as f1:
            reader1 = csv.reader(f1)
            rows1=[row for row in  reader1]      
            with open('热点.csv',encoding= 'utf-8-sig') as f2:
                reader2 = csv.reader(f2)
                rows2=[row for row in  reader2]  
                with open('疫情.csv',encoding= 'utf-8-sig') as f3:
                    reader3 = csv.reader(f3)
                    rows3=[row for row in  reader3]
            
                    temp_set = []
                    for a in rows1:
                        temp_set.append(a)
                    for b in rows2:
                        temp_set.append(b)
                    for c in rows3:
                        temp_set.append(c) #将所有评论存储在一个列表temp_set内
        # print(temp_set)
        for emotion in emotion_result_set:
            positive = 0
            negtive = 0
            for critic in temp_set:
                # print(emotion)
                if emotion in critic[0]:
                    if critic[1] == '1':
                        positive = positive + 1
                    if critic[1] == '-1':
                        negtive = negtive + 1
            if positive + negtive == 0:
                Neu_set.append(emotion)
            else:
                if (positive/(positive + negtive)) > 0.8:
                    PE_set.append(emotion)
                elif (negtive/(positive + negtive)) > 0.8:
                    NE_set.append(emotion)
                else:
                    Neu_set.append(emotion)
        with open('Positive_emotion.csv', 'w', encoding='utf-8-sig') as f:
            for each in PE_set:
                write=csv.writer(f)
                write.writerow(each)
            
        with open('Negtive_emotion.csv', 'w', encoding='utf-8-sig') as f:
            for each in NE_set:
                write=csv.writer(f)
                write.writerow(each)
            
        with open('Neu_emotion.csv', 'w', encoding='utf-8-sig') as f:
            for each in Neu_set:
                write=csv.writer(f)
                write.writerow(each)
    
        return PE_set, NE_set, Neu_set
        
        
    
    # emotion_result_set = []
    # emotion_result_set = Save_as_File('.//emotion//All_emotion.csv', emotion_result_set)
    # PE_set, NE_set = creat_Respiratory(emotion_result_set)
    
    

    文本处理

    import pre_professer
    import jieba
    import csv
    from sklearn.feature_extraction.text import CountVectorizer
    from sklearn.feature_extraction.text import TfidfTransformer
    
    list3=['最','最为','极','极其','极为','极度']
    list2_5=['太','至','至为','顶','过','过于','过份','分外','万分']
    list2=['很','挺','怪','非常','特别','相当','十分','甚为','够','多','多么']
    list1_5=['不甚','不胜','好','好不','颇','颇为','大','大为']
    list1_1=['稍','比较','较为','还']
    list0_8=['稍稍','稍微','稍许','略微','多少']
    list0_5=['有点','有些']
    list_1=['甭', '别', '不', '不曾', '不必', '非', '没', '没有', '莫', '未必', '未尝', '无从', '无须', '不要', '不用', '不再', '不很', '不太', '绝非', '决非', '并非', '不能', '不常', '不会', '不可能', '何曾', '何尝', '勿']
    
    def PLfenci(emotion_result_set, _class): #遍历每一条评论分词处理并去杂(去除表情、去除@内容、去除停用词)
        label_list = [] # 一维列表,存储评论标签
        with open('明星.csv',encoding= 'utf-8-sig') as f1:
            reader1 = csv.reader(f1)
            rows1=[row for row in  reader1]      
            with open('热点.csv',encoding= 'utf-8-sig') as f2:
                reader2 = csv.reader(f2)
                rows2=[row for row in  reader2]  
                with open('疫情.csv',encoding= 'utf-8-sig') as f3:
                    reader3 = csv.reader(f3)
                    rows3=[row for row in  reader3]
            
                    temp_set = []
                    for a in rows1:
                        if _class == 'Star' or _class == 'All':
                            temp_set.append(a)
                        label_list.append(a[1])
                    for b in rows2:
                        if _class == 'Hotspot' or _class == 'All':
                            temp_set.append(b)
                        label_list.append(b[1])
                    for c in rows3:
                        if _class == 'Epidemic' or _class == 'All':
                            temp_set.append(c) #将评论存储在一个列表temp_set内
                        label_list.append(c[1])
                    if _class == 'All':
                        return temp_set, label_list
    # 去除表情,去除@信息和停用表词
        for i in range(len(temp_set)):
            for emotion in emotion_result_set:
                if emotion in temp_set[i][0]:
                    temp_set[i][0] = temp_set[i][0].replace(emotion, '')
    
        for each in temp_set:
            for m in range(len(each[0])):
                if (each[0][m] == '/') and (each[0][m+1] == '/') and (each[0][m+2] == '@'):
                    while m < len(each[0]):
                        each[0] = each[0].replace(each[0][m], '')
                        m+=1
                    break    
                
        for each in temp_set:
            for i in range(len(each[0])):
                if i < len(each[0]) and each[0][i] == '@':
                    temp = ''
                    while i < len(each[0]) and (each[0][i] != ':' or each[0][i] == ' '): # 注意要使用中文':'
                        temp = temp + each[0][i]
                        i = i + 1
                    each[0] = each[0].replace(temp, '')
               
        stop_list = []
        f_stop = open('.\\停用词表.txt', 'r', encoding = 'UTF-8')
        #获取停用词列表
        for each in f_stop:
            each = each.strip()#去除尾字符中的换行符
            stop_list.append(each)
        for each in temp_set:
            for k in stop_list:
                if k in each[0]:
                    each[0] = each[0].replace(k, '')
        
    # 进行jieba分词
        speciallist=[]
        data_list = [] # 二维列表,每个元素为一个评论预处理后的结果,每个结果也是一个列表,用','分割。
        for k in range(len(temp_set)):
            data_list.append([])
        k = 0
        for each in temp_set:
            a=checkpoint(each)
            speciallist.append(a)
            generator = jieba.cut(each[0])
            for i in generator:
                data_list[k].append(i)
            k = k + 1
        
    
    # 预处理结果写入segment文件
        with open('.\\segment.csv', 'w', newline = '',encoding = 'utf-8-sig') as f:
            write = csv.writer(f)
            for each in data_list:
                write.writerow(each)
    
        return data_list, label_list,speciallist
    
    
    def tfidf_get(data_list, _class): # 实现3,4功能 data_list即为前面获取的数据列表, 类名(和前一函数一致)
        transfer_data_list = [] #将data_list转化为tfidf可以看懂的格式
        for each in data_list:
            x = ' '.join(each)
            transfer_data_list.append(x)
        vectorizer = CountVectorizer() # 将文本中的词语转换为词频矩阵
        X = vectorizer.fit_transform(transfer_data_list) # 计算个词语出现的次数
        word_list = vectorizer.get_feature_names()  # 获取词袋中所有文本关键词
        transformer = TfidfTransformer() #类调用
        tfidf = transformer.fit_transform(X) #将词频矩阵X统计成TF-IDF值
        feature = [] # 存储所有评论的feature, 每个元素为一条评论的feature,每条评论5个feature。
        length = len(word_list)
        s = 0
        for each in tfidf.toarray():#进入二维列表tfidf_list的每一项中,遍历每一句的tfidf
            feature_list = []    #对于每一评论,初始特征置空
            item = 0#特征值计数变量,满5个为止
            while item < 5:
                max_tfidf = 0#最大值置零
                for i in range(length):#每一次循环找出一个最大tfidf值对应的word并存入feature_list列表中,并置该tfidf = 0
                    if each[i] >= max_tfidf:
                        max_tfidf = each[i] 
                        m = i#记录相应索引号
                    if i == (length - 1):# i = length - 1说明句子已经遍历到末尾了,此时最大的tfidf值就可以确定了
                        feature_list.append(word_list[m])
                        each[m] = 0
                        item = item + 1
            v = ' '.join(feature_list)
            feature.append(v)
            s = s + 1
        with open('.\\特征数据\\' + _class + '.csv', 'w', newline = '',encoding = 'utf-8-sig') as f:
            write = csv.writer(f)
            for each in feature:
                write.writerow(each)
    
    def checkpoint(str):
        if str in list3:
            return 3
        elif str in list2_5:
            return 2.5
        elif str in list2:
            return 2
        elif str in list1_5:
            return 1.5
        elif str in list1_1:
            return 1.1
        elif str in list0_8:
            return 0.8
        elif str in list0_5:
            return 0.5
        elif str in list_1:
            return -1
        else:
            return 0
    

    建模训练

    import pre_professer
    import specialize
    from sklearn.model_selection import  train_test_split #数据集划分
    from sklearn.naive_bayes import MultinomialNB
    from sklearn.feature_extraction.text import CountVectorizer  # 从sklearn.feature_extraction.text里导入文本特征向量化模块
    from sklearn.metrics import classification_report
    from sklearn.neural_network import MLPClassifier
    import time
    
    data = []#数据集
    def Contribute_data(data, _class): # 获取class类别的数据
        with open('\\特征数据\\' + _class + '.csv', 'r', encoding = 'utf-8-sig') as fr:
            for tem in fr:
                tem = tem.replace(',', '')
                tem = tem.replace('\n', '')
                data.append(tem)
        return ''  
    def get_target(lebal_list):
        target = []
        for each in lebal_list:
            if each == '1':
                target.append('积极')
            elif each == '0':
                target.append('中性')
            elif each == '-1':
                target.append('消极')
            else:
                target.append('中性')
        return target
    
    def Bayesian(data, target):
        #数据预处理:训练集和测试集分割,文本特征向量化
        #X_train,X_test,y_train,y_test = train_test_split(data, target, test_size=30000 ,random_state=4) # 随机采样数据样本作为测试集
        X_train = data[10000:20000] + data[30000:40000] + data[60000: 80000]
        X_test = data[0:10000] + data[20000:30000] + data[50000: 60000]
        y_train = target[10000:20000] + target[30000:40000] + target[60000: 80000]
        y_test = target[0:10000] + target[20000:30000] + target[50000: 60000]
        k = X_test
        #文本特征向量化
        vec = CountVectorizer()
        X_train = vec.fit_transform(X_train)
        X_test = vec.transform(X_test)
        #print(y_test)
        #print(y_train)
        #使用朴素贝叶斯进行训练
        mnb = MultinomialNB()   # 使用默认配置初始化朴素贝叶斯
        mnb.fit(X_train,y_train)    # 利用训练数据对模型参数进行估计
        y_predict = mnb.predict(X_test)     # 对参数进行预测
        #print(y_predict)
        #获取结果报告
        print ('不带表情的朴素贝叶斯:', mnb.score(X_test,y_test))
        print ('其它指标:\n',classification_report(y_test, y_predict, target_names = ['积极', '中性', '消极']))
        
        return k, y_test, y_predict
    
    def Fenlei(emotion_X_test, emotion_y_test, emotion_y_predict, origin_list, PE_set, NE_set, Neu_set,specialist): # 运用表情训练
        o_list = origin_list[0:10000] + origin_list[20000:30000] + origin_list[50000: 60000]
        length = len(emotion_y_predict)
        for i in range(length):
            if emotion_y_test[i] != emotion_y_predict[i]:
                key = 0
                if '' in emotion_X_test[i]:
    
                    for each in PE_set:
                        if each in o_list[i]:
                            key = key + 1
                    for each in NE_set:
                        if each in o_list[i]:
                            key = key - 1
                    key+=specialist[i]
                    if key > 0:
                        emotion_y_predict[i] = '积极'
                    if key < 0:
                        emotion_y_predict[i] = '消极'
                    if key == 0:
                       emotion_y_predict[i] = '中性'
                else:
                    key = 0.5
                    for each in PE_set:
                        if each in o_list[i]:
                            key = key + 1
                    for each in NE_set:
                        if each in o_list[i]:
                            key = key - 1
                    for each in Neu_set:
                        if each in o_list[i]:
                            key = key - 0.5
                    if key > 0:
                        emotion_y_predict[i] = '积极'
                    if key < 0:
                        emotion_y_predict[i] = '消极'
                    if key == 0:
                        emotion_y_predict[i] = '中性'
    # 呈现分类结果
        rate = 0
        for p in range(length):
            if emotion_y_predict[p] == emotion_y_test[p]:
                rate += 1
        print('带表情的贝叶斯:%.8f'%(rate/30000))
                    
        return ''
    start=time.time()
    print('开始预处理')
    emotion_result_set = []
    emotion_result_set = pre_professer.Save_as_File('All_emotion.csv', emotion_result_set)
    PE_set, NE_set, Neu_set = pre_professer.creat_Respiratory(emotion_result_set)
    print('预处理完毕')
    
    Star_data_list,lebal_list ,specialkist= specialize.PLfenci(emotion_result_set, 'Star')
    print('开始star处理')
    feature = specialize.tfidf_get(Star_data_list, 'Star')
    print('Star类别特征构建成功')
    print('开始hotpot处理')
    Hotspot_data_list,lebal_list ,specialkist= specialize.PLfenci(emotion_result_set, 'Hotspot')
    feature = specialize.tfidf_get(Hotspot_data_list, 'Hotspot')
    print('Hotspot特征构建成功')
    Epidemic_data_list,lebal_list ,specialkist= specialize.PLfenci(emotion_result_set, 'Epidemic')
    feature = specialize.tfidf_get(Epidemic_data_list, 'Epidemic')
    print('Epidemic特征构建成功')
    
    Contribute_data(data, 'Star')
    Contribute_data(data, 'Hotspot')
    Contribute_data(data, 'Epidemic')
    
    origin_list, lebal_list = specialize.PLfenci(emotion_result_set, 'All')
    target = get_target(lebal_list)
    emotion_X_test, emotion_y_test, emotion_y_predict = Bayesian(data, target)
    #print(len(emotion_y_test))
    Fenlei(emotion_X_test, emotion_y_test, emotion_y_predict, origin_list, PE_set, NE_set, Neu_set,specialkist)
    size = (10,10000)
    iters = 10000
    clf = MLPClassifier(activation='relu', alpha=1e-05, batch_size='auto', beta_1=0.9,
           beta_2=0.999, early_stopping=False, epsilon=1e-08,
           hidden_layer_sizes=size, learning_rate='constant',
           learning_rate_init=0.001, max_iter=iters, momentum=0.9,
           nesterovs_momentum=True, power_t=0.5, random_state=1, shuffle=True,
           solver='adam', tol=0.0001, validation_fraction=0.1, verbose=False,
           warm_start=False)#多层感知机
    tv=specialize.tfidf_get()
    print('深度学习:',clf.score(tv.transform(emotion_X_test),emotion_y_test))
    from sklearn.metrics import classification_report
    print('其它指标:\n',classification_report(emotion_y_predict,clf.predict(tv.transform(emotion_X_test))))
    end=time.time()
    
    
    

    参考论文:基于表情符分析的情感关键句提取方法
    (提取码:3882)

    展开全文
  • 朴素贝叶斯和情感分类

    千次阅读 2019-08-01 17:05:26
    朴素贝叶斯和情感分类 1 朴素贝叶斯分类器 2 训练朴素贝叶斯分类器 3 例子 4 情感分析优化 5 朴素贝叶斯作为一种语言模型 6 评估指标:精确度,召回率,F-measure 7 测试集交叉验证 8 特征选择 9 小结 ...
  • 简介 本文来讲述BERT应用的一个例子,采用预训练好的BERT模型来进行演示。BERT的库来源于Transformers,这是一个由PyTorch编写的库,其集成了多个NLP领域SOTA的模型,比如bert、gpt-2、transformer xl...1、任务与数...
  • 背景 数据预处理 文本分类 情感分析
  • 文章目录1 文章简介2 文本情感分类概述 1 文章简介 文本分类与情感分类是自然语言处理中基础的领域,在机器学习的基础上,分析了线性逻辑回归算法、朴素贝叶斯模型在文本情感分类项目中的应用,并针对数据处理、模型...
  • 作者:太子长琴,算法工程师,Datawhale成员文本分类是自然语言处理(NLP)最基础核心的任务,或者换句话说,几乎所有NLP任务都是「分类」任务,或者涉及到「分类」概念。比如分词、词性...
  • 向AI转型的程序员都关注了这个号????????????大数据挖掘DT机器学习 公众号:datayx基于情感词典的文本情感分类传统的基于情感词典的文本情感分类,是对人的记忆判断思维的...
  • #积极评价:25*200的矩阵,有效词的个数,标签,如果是分3类,标签就为[1,0,0] randIt.append([posArray[i], posStep[i], [1,0]]) for i in range(len(negArray)):#消极评价:25*200的矩阵,有效词的个数,标签 ...
  • 文本情感分析学习篇(三) 题目:Word2Vec+LSTM多类情感...三分类的情感分类模型:消极 积极 中性 相对于Word2Vec+SVM准确率提高了10% Word2Vec之前是one-hot编码方式 Jieba库 lcut 进行句子切分,多为向量表...
  • 基于LSTM分类文本情感分析

    千次阅读 2020-08-19 17:36:08
    文本情感分析作为NLP的常见任务,具有很高的实际应用价值。本文将采用LSTM模型,训练一个能够识别文本postive, neutral, negative三种情感的分类器。 本文的目的是快速熟悉LSTM做情感分析任务,所以本文提到的只是...
  • 基于CNN的文本情感分析

    千次阅读 2020-05-03 20:13:16
    情感分析是自然语言处理中很常见的任务,它的目的是识别出一段文本潜在的情感,是表扬还是批评,是支持还是反对。比如我们可以使用情感分析去分析社媒的评论,从而得到网友对某件事的看法,进一步分析可以得到舆论的...
  • poscount2 = 0 #积极反转后的分值 poscount3 = 0 #积极词的最后分值(包括叹号的分值) negcount =0 negcount2=0 negcount3=0for word insegtmp:if word in posdict: #判断词语是否是情感词 poscount +=1c=0for w in...
  • 本文由来为了赚足学分丰富假期生活,...序幕既然题目是“基于情感词典的文本情感分析”,那么情感词典就是必不可少的了。对于情感词典的要求:要包含积极的词语消极的词语、每一种类的数量要足够多、包含足够广...
  • Python文本情感分析实战【源码】

    千次阅读 多人点赞 2020-10-03 17:52:43
    Python文本情感分析 引言: 情感分析:又称为倾向性分析...比如我们标注数据集,标签为1表示积极情感,0位中立情感,-1为消极情感。 一、实验前的准备: 其中数据集如下所示: 二、数据分析 首先读取csv文件数据,
  • 末尾时间步处的隐藏状态向量被馈送到二进制softmax分类器中,在其中与另一个权重矩阵相乘,并经过softmax函数(输出0和1之间的值)的处理,有效地给出情绪偏向正面或负面的概率。 长短期记忆单元(LSTM) 长短期...
  • 文本分类数据评价指标

    万次阅读 多人点赞 2019-04-07 23:19:45
    1 中文文本分类数据集THUCNews 1.1 数据说明   THUCNews是根据新浪新闻RSS订阅频道2005~2011年间的历史数据筛选过滤生成,包含74万篇新闻文档(2.19 GB),均为UTF-8纯文本格式。我们在原始新浪新闻分类体系的基础...
  • 基于lstm的情感分类

    2021-04-20 15:55:00
    基于lstm的文本分类 简介 本文是有关于lstm的文本分类的实现,并不会介绍很多相关理论内容。 相关包和超参 以下是我实现这个程序所使用的相关包 import pandas as pd import re import torch.nn as nn from torch ...
  • 本文介绍神经网络的入门案例,通过搭建训练一个模型,来对对电影评论进行文本分类;将影评分为积极(positive)或消极(nagetive)两类。这是一个二分类(binary)问题。 使用到网络电影数据库(Internet Movie ...
  • 传统的基于情感词典的文本情感分类,是对人的记忆判断思维的最简单的模拟,如上图。我们首先通过学习来记忆一些基本词汇,如否定词语有“不”,积极词语有“喜欢”、“爱”,消极词语有“讨厌”、“恨”等,从而在...
  • 九、(1情感分类——基于词典。评论。 ————数据集留言邮箱发送 # -*- coding: utf-8 -*- """ Created on Thu Jun 13 23:32:14 2019 @author: sun """ import jieba import numpy as np #打开词典文件,返回...
  • 情感分类 就能得到以下结果: {'text': '你长得真好看', 'sentiment_label': 1, 'sentiment_key': 'positive', 'positive_probs': 0.9866, 'negative_probs': 0.0134} {'text': '《黑色四叶草》是部不错的番', '...
  • 写在前面本文将带来CCF BDCI新闻情感分类的题解报告,该方案在初赛A榜获得了4/2735,复赛成绩1%。希望可以给大家提供一些思路,互相交流学习。比赛代码已经开源在https://g...
  • Python SVM分类器 XGBOOST分类文本情绪分析 疫情期间网民情绪识别比赛 目录 Python SVM分类器 XGBOOST分类文本情绪分析 疫情期间网民情绪识别比赛 一:比赛相关事项 二:使用工具PyCharm配合Anaconda3 三...
  • 注:本文转载自知乎专栏情感分析就是分析一句话说得是很主观还是客观描述,分析这句话表达的是积极的情绪还是消极的情绪。原理比如这么一句话:"这手机的画面极好,操作也比较流畅。不过拍照真的太烂了!系统也...
  • #0代表消极,1代表积极 test=pd.DataFrame({'text':['我想用CNN做个情感分析,这个语句是我喜欢的', '哈哈哈,万年刮痧王李白终于加强了', '这个游戏好极了,个别英雄强度超标,游戏里面英雄种类丰富,我太菜,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,072
精华内容 828
关键字:

文本情感分类1和0代表积极还是

友情链接: ad-blackoil.zip