策略算法工程师之路-文本情感分类(Sentiment Analysis)

策略算法工程师之路-文本情感分类(Sentiment Analysis)

目录

1.情感分析简述

2.情感字典匹配方法

3.依存句法方法

4.传统模型方法

5.深度模型方法

5.1) TextCNN

5.2) LSTM(BiLSTM)

5.3) Attention机制

5.4) HAN

5.5) SelfAttention (暂无)

5.6) DeepMoji (暂无)

5.7) 多层级多标签分类网络(HMCN)

1.情感分析简述

情感分析( Sentiment Analysis )又称倾向性分析,或意见挖掘,它是对带有情感色彩的主观性文本进行分析、处理、归纳和推理的过程。利用情感分析能力,可以针对带有主观描述的自然语言文本,自动判断该文本的情感正负倾向并给出相应的结果。在 评论分析与决策 电商评论分类 以及 舆情监控 中有非常广泛的应用。

如下是 百度大脑 提供的 情感分析 服务,感受一下。

通常情感识别依据所处理文本的级别分为以下三类:

  • 文本级别:通过完整文档或段落来获取情绪;
  • 句子级别:获得单句的情绪。
  • 子句级别:获得句子中,子表达的情感。

本文主要讲解 句子级别 的情感分析。


参考资料:

1.清华大学:网络社交媒体的情感认知与计算

360doc.com/content/16/0

2.情感字典方法

基于词典的情感分析是最简单的策略,即根据文本中出现的 情感词 判断一句话的情感极性。该方法的核心是维护 情感词典 (可以是通用情感词典,也可以是 领域专用 的情感词典)。英文的词典有很多,中文主要有知网整理的情感词典Hownet和台湾大学整理发布的NTUSD两个情感词典,还有哈工大信息检索研究室开源的《同义词词林》。

1) 情感词典

下面是词典中的部分情感词,词后面的数字代表其 情感正负程度。 比如" fuck "是负向情感词 其情感值是 " -6.70400012637"。再比如" 好赞 "是正向情感词,其情感值是"4.03663526"。情感词及其极性可以由人工搜集标注,也可以由算法挖掘。

词典下载: bosonnlp.com/resources/

...
最尼玛 -6.70400012637
扰民 -6.49756445867
fuck... -6.32963390433
Fxxk -5.87247473641
MLP -5.87247473641
吃哑巴亏 -5.77120419579
IAQI -5.77107837123
太好 1.51096421722
好赞 4.03663526827
...

2).文本分词

原句: 真是太差劲了!

for word in jieba.cut("真是太差劲了!"):
    print(word)

分词: 真是太 / 差劲 / / !

3).情感计算

查询情感词典, 累加 每个词的 情感值

0 [词典中没有'真是太',默认为0] -2.76814873241 ["差劲"]+ 0 ["了"] = -2.76814873241

因此" 真是太差劲了! "的情感极性为 负向。

4). 完整代码

import re
import jieba
# 基于词典的情感识别
class DictBasedSentAnal:
    # 加载情感词典
    def __init__(self):
        self.__root_dir = 'dict/'
        self.__sent_dict__ = self.__read_dict(self.__root_dir+'BosonNLP_sentiment_score.txt')
    # 情感极性分析
    def analyse(self, sentence):
        score = 0.0
        for word in jieba.cut(sentence):
            score += self.__sent_dict__.get(word, 0)
        return score
    # 读取词典
    @staticmethod
    def __read_dict(path, encoding='utf-8'):
        sent_dict = {}
        return sent_dict

以上代码主要参考(复制)自:

jianshu.com/p/a7b76beea

5).考虑程度副词

原句: 这个游戏不是太好玩。

分词: 这个/游戏/不是/太好玩/。

按照上面的算法,如果仅仅考虑" 太好玩 ",原句的情感是正向的,显然这是不对的,主要是由于忽略了 情感词 ( 太好玩 )前面的 程度副词 ( 不是 )。由于 程度副词 是对情感词 情感极性 加强 ,所以最终的情感值可以通过程度副词的 程度值(正值表示加强,负值表示否定) 乘以情感词的情感值。

还是上面的例子, "太好玩(+2)" * "不是(-1)" = -1 ,原句的情感极性为" -2 "。


Tips:

情感词:是主体对某一客体表示内在评价的词语,带有强烈的感情色彩。

程度副词:本身没有情感倾向,但能够增强或减弱情感强度

否定词:本身也没有情感倾向,但能改变情感的极性

停用词:完全没有用或者没有意义的词


参考资料:

1.基于情感词典的文本情感分析

jianshu.com/p/92584d21f

2.基于词典的中文情感倾向分析算法设计

基于词典的中文情感倾向分析算法设计

3.基于情感词典进行情感态度分析

blog.csdn.net/qq_391876

4.基于词典的中文情感倾向分析算法设计

jianshu.com/p/f2ba15868

5.文本情感分类

jianshu.com/p/258b6f09b

6.情感分析:几乎包括你需要知道的所有(二)

jianshu.com/p/e424061ac

7.深入理解NLP中的文本情感分析(华为)

biaodianfu.com/sentimen

8.文本情感分类(一):传统模型

spaces.ac.cn/archives/3


3.基于依存句法的规则方法

前面所讲的基于情感词典的方法只是简单的匹配,而实际的语言是复杂的非线性系统。一种改进的思路是利用 句法分析技术 将文本解析成 结构化的语法树 ,然后依据规则从中提取感兴趣的内容。句法分析是根据给定的语法体系,自动推导出句子的句子结构,分析句子包含的句法单元及其之间关系,并转化为结构化的句法分析树。如下图( ltp.ai/demo.html ):

句法分析

其中,ADV->状中结构,ATT->定中关系,HED->核心关系,RAD->右附加关系。除此之外,在NLP中 依存句法分析 更加常见 的句法分析技术。

依存句法分析

上图中边的含义如下:

\begin{align*} &Exp->当事关系 \\ &mDegr->程度标记 \\ &mPars->插入语标记 \\ &mTone->语气标记 \\ &Nmod->名字修饰角色 \\ &Root->根节点 \end{align*}

Root 根结点指向"棒",表明"棒"是整个句子的 核心 。以"棒"为起点的各边指向的句子成分,分别以某种" 依赖关系" 依赖于核心词"棒"。因此以 核心词为线索 可以解析出句子的 语法结构 ,进一步依据" 预定义的规则" 抽取出评论文本中的 依存情感单元, 包括 评论对象 情感词 否定词 程度副词 等。

下面是几条简单的规则:

1).否定词修饰情感词

否定词修饰情感词会改变情感倾向 ,如"不喜欢"这个短语 中,"不"作为否定词修饰"欢"。

2).程度词修饰情感词

程度词修饰情感词会改变情感强度 ,如"太棒" 短语中,"太"作为程度词修饰"喜欢"。

3).否定词修饰否定词

否定词修饰否定词即为双重否定 ,一般情况下 不影响原来的情感倾向 ,对于情感程度的影响需要具体分析依存关系。如"不是不喜欢"中,第一个"不"作为否定词修饰了"是",后面的"不喜欢"和"是"是动宾关系。

4).否定词修饰程度词

否定词修饰程度词相当于对程度的否定 ,而 非情感的否定 ,因此否定词修饰后,最终 情感倾向未变 ,但是 程度有所降低 。如"不很喜欢"中,"不"修饰了"很",然后才是"很"修饰"喜欢"。"很"作为程度词表现了一种情感等级的加强,但是“不”作 为否定词对“很”这个程度词的否定使得最终的强度有一定的削减。"不很喜欢"这个情感短语仍然表现了"喜欢"这种情感类 别,但是在强度上 强于"喜欢"这个情感词 弱于"很喜欢"这个情感短语

5).程度词修饰否定词

程度词修饰否定词相当于给否定加个强度 ,因为否定本身无强度可言,最终的 强度会随否定词一起携带到情感词上 。如 "很不喜欢"中,"很"修饰"不",然后才是"不"修饰"喜欢"。因此,"很不喜欢"在情感类别上是"厌恶",在强度 上比"不喜欢"更强,和"很喜欢"强度一致。

6).程度词修饰程度词

程度词修饰程度相当于程度的叠加 。多用于 微博等非正式语料 中,此类表达方式多用于 表现强烈情感。 如"非常极其喜欢"中,"非常"修饰"极其",然后是"极其"修饰"喜欢"。对于此类的表述,其情感强度相比于"非常喜欢"和"极其喜欢"都要强一些。

以上只是列举了几个简单的规则示例,实际应用中需要依据所处理的领域做 Domain Special 的规则抽取。


示例Demo参考:

github.com/ruizhang1993


参考资料:

1.基于改进依存句法的微博情感分析研究_李雪红

2.基于关键词和关键句抽取的用户评论情感分析_喻影

3.基于依存句法的评论情感极性分析_邹海林

4.基于依存句法的情感分析

jianshu.com/p/66947e2f4

5.基于句法依存关系的微博情感分析方法_文坤梅

6.基于依存关系的旅游景点评论的特征-观点对抽取

7.基于依存句法树方法的微博文本的情感分析研究_王彬菁

8.基于依存关系的旅游景点评论的特征-观点对抽取

9.意图识别(规则模板解析、深度学习意图识别)

blog.csdn.net/qq_165551


4.传统模型方法

语言系统是相当复杂的,前面介绍的 基于情感词典 基于依存句法 的文本情感分类只是一个规则系统,其性能是有限的,依赖于领域专家的总结,不同领域间的适用性较差。基于机器学习(朴素贝叶斯、最大熵、SVM、LR、树模型等)的方法可以从数据中 自动学习Pattern 。本小节 暂时 只总结下文本分类问题常用的特征。

常用特征:

1).统计特征

比如 句子长度 词长度 标点数量 标点种类 词性序列 等。

2).句子表示

BOW(bag of word ,n-gram)、tf-idf、LDA等。

3).编辑距离

4).位置特征

关键词位置等

5).句法特征

6).预训练向量

Word2vec、Doc2vec


这里有一个基于SVM的简单示例:

github.com/BUPTLdy/Sent


参考资料:

1.竞赛专题(四)特征工程-竞赛中的必杀技

cloud.tencent.com/devel

2.从Kaggle赛题: Quora Question Pairs 看文本相似性/相关性

zhuanlan.zhihu.com/p/35

3.蚂蚁金融NLP竞赛——文本语义相似度赛题总结

blog.csdn.net/u01473253

4.通过kaggle比赛学习机器学习文本分类方法

zhuanlan.zhihu.com/p/34

5.Kaggle 恶意评论(toxic comment classification)分类 top 1%方案

zhuanlan.zhihu.com/p/34

6. NLP-SentimentAnalysisForChineseText

github.com/yirui-wang-0


5.深度模型方法

传统机器学习做文本分类需要做大量的 特征工程 (feature engineering),限制了预测性能的提升。近些年深度学习在NLP得到了广泛的应用,首先深度学习将算法工程师从繁杂的特征工程中解放出来,其次赋予算法工程师 根据领域问题先验定制化网络结构 的能力。接下来分别介绍情感分类或者文本分类中常见的网络结构。

5.1 TextCNN网络

TextCNN是一个通用的文本分类框架,在这里用于情感识别。

Paper: Convolutional Neural Networks for Sentence Classification

TextCNN的网络架构如下:

TextCNN

一张更清晰的:

1).输入层

# 输入占位符
self.input_x = tf.placeholder(tf.int32, [None, sequence_length], name="input_x")
self.input_y = tf.placeholder(tf.float32, [None, num_classes], name="input_y")

sequence_length :输入文本的长度。通常采用 截断 补"0" 的方式将句子归一化到统一长度,比如可以设为 80

num_classes: 分类数量。根据需要识别的 情感级别数量 定义。比如有三种情感等级,正向、负向、中性,此时num_classes应设为 3。

2).Embedding层

在文本分类任务中,通常将Word映射到Embeddding之后再输入到后续网络。给定一个文档单词序列,"A B A C B F G", 文档中每个不同的单词都对应一个低维向量表示。比如,A对应的向量为[0.1 0.6 -0.5],B对应的向量为[-0.2 0.9 0.7] 。很多词语的意思是各个方向发散开的(多义性),而多维向量解决了词语的多方向发散问题,因此可以表达更丰富的语义。

# Embedding layer
with tf.device('/cpu:0'), tf.name_scope("embedding"):
     self.W = tf.Variable(
          tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0),
          name="W")
     self.embedded_chars = tf.nn.embedding_lookup(self.W, self.input_x)
     self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)

vocab_size: 字典大小。

embedding_size: Embedding向量维度。

参考资料:

1.什么是 word embedding?

zhihu.com/question/3227

3).卷积层

# 卷积与池化层
pooled_outputs = []
for i, filter_size in enumerate(filter_sizes):
     with tf.name_scope("conv-maxpool-%s" % filter_size):
         # 卷积层(Convolution Layer)
         filter_shape = [filter_size, embedding_size, 1, num_filters]
         W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name="W")
         b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name="b")
         conv = tf.nn.conv2d(
                    self.embedded_chars_expanded,
                    strides=[1, 1, 1, 1],
                    padding="VALID",
                    name="conv")
         # 非线性变换
         h = tf.nn.relu(tf.nn.bias_add(conv, b), name="relu")
         # 对卷积层的输出做Maxpooling操作
         pooled = tf.nn.max_pool(
             ksize=[1, sequence_length - filter_size + 1, 1, 1],
             strides=[1, 1, 1, 1],
             padding='VALID',
             name="pool")
         pooled_outputs.append(pooled)

filter_sizes: 不同类型(这里主要指 尺寸 )卷积核的数量。

num_filters: 同一类型卷积核的数量。

所以, 总的卷积核的数量 filter\_sizes\times num\_filters

不同尺寸的卷积核可以捕获 不同范围内Word间的关系 。理论上尺寸越大越能捕获 全局的语义信息

4).输出层

num_filters_total = num_filters * len(filter_sizes)
self.h_pool = tf.concat(pooled_outputs, 3)
self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])
# 最终输出
with tf.name_scope("output"):
     W = tf.get_variable(
                shape=[num_filters_total, num_classes],
                initializer=tf.contrib.layers.xavier_initializer())
     b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name="b")
     l2_loss += tf.nn.l2_loss(W)
     l2_loss += tf.nn.l2_loss(b)
     self.scores = tf.nn.xw_plus_b(self.h_drop, W, b, name="scores")
     self.predictions = tf.argmax(self.scores, 1, name="predictions")

完整代码如下:

github.com/diaosj/cnn-t

cloud.tencent.com/devel

可以试着运行下,在tensorboard中的可视化结果如下:

5.2 LSTM模型

前面讲过基于CNN的文本分类尽管在实践中取得了较好的效果,但由于CNN本身的特性导致其忽略了 Word 的序列依赖关系。设想,当我们理解一句话时通常" 从前往后 "依次阅读每个 Word ,同时结合单词间的 前后依赖关系, 理解整句话的含义。本小节讲解的 LSTM (Long Short-term Memory Neural Networks)模型可以捕获 Word 的依赖关系,通常用于 序列建模 LSTM 是对传统 RNN 的改进,缓解了 梯度爆炸 问题。

LSTM的内部结构:

本小节的 网络结构定义 如下( 基于Keras ):

from sklearn.cross_validation import train_test_split
from keras.models import Sequential
from keras.layers.embeddings import Embedding
from keras.layers.recurrent import LSTM
from keras.layers.core import Dense, Dropout,Activation
from keras.models import model_from_yaml
np.random.seed(1337)  # For Reproducibility
import sys
sys.setrecursionlimit(1000000)
import yaml
import keras
#定义网络结构
# n_symbols:
# embedding_weights:Embedding矩阵
# x_train:训练集X
# y_train:训练集y
# x_test:测试集x
# y_test:测试集y
def train_lstm(n_symbols,embedding_weights,x_train,y_train,x_test,y_test):
    print('开始定义网络...')
    model = Sequential()
    # 将Word转换成Embedding向量
    model.add(Embedding(output_dim=vocab_dim,
                        input_dim=n_symbols,
                        mask_zero=True,
                        weights=[embedding_weights],
                        input_length=input_length))  # Adding Input Length
    # LSTM
    model.add(LSTM(output_dim=50, activation='tanh', inner_activation='hard_sigmoid'))    
    model.add(Dropout(0.5))
    # 全链接层
    model.add(Dense(3, activation='softmax')) # Dense=>全连接层,输出维度=1
    model.add(Activation('softmax'))
    print('Compiling the Model...')
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',metrics=['accuracy'])
    print("开始训练...")
    model.fit(x_train , y_train , batch_size=batch_size , epochs=n_epoch , verbose=1)
    print("模型评估...")
    score = model.evaluate(x_test, y_test,batch_size=batch_size)
    print('ACC:'+str(score))

模型训练:

train_lstm(n_symbols,embedding_weights,x_train,y_train,x_test,y_test)
开始训练...
Epoch 1/10
16870/16870 [==============================] - 68s 4ms/step - loss: 0.9180 - acc: 0.6228
Epoch 2/10
16870/16870 [==============================] - 55s 3ms/step - loss: 0.6898 - acc: 0.8638
Epoch 3/10
16870/16870 [==============================] - 61s 4ms/step - loss: 0.6756 - acc: 0.8767
Epoch 4/10
16870/16870 [==============================] - 55s 3ms/step - loss: 0.6712 - acc: 0.8811
Epoch 10/10
16870/16870 [==============================] - 62s 4ms/step - loss: 0.6226 - acc: 0.9289
Evaluate...
4218/4218 [==============================] - 5s 1ms/step
Test score:[0.6559340375585543, 0.8944997629773397]

在模型训练前需要对文本做处理:

1).Embedding词向量训练

可以用 gensim 工具包中的 word2vec 训练词向量 以词语" 不错 "为例:

word_vectors["不错"]
array([-0.21249017, -0.7439336 ,  1.5525742 ,  2.523707  , -1.26884   ,
        0.94706655, -1.4531343 ,  0.3719145 , -1.798436  , -2.8654885 ,
       -0.98588014,  2.590885  , -1.296376  , -1.5622058 , -1.3199229 ,
       -0.5774353 , -0.9784841 , -0.08609965,  1.6380359 , -0.71032125,
        0.8648587 ,  2.1153228 ,  0.34322342, -1.0344014 , -1.1715566 ,
       -1.1469606 , -1.0197061 ,  2.4668577 , -1.4312339 ,  0.5667959 ,
       -0.64566624, -1.432156  , -0.712759  , -0.17578368,  0.33848813,
       -1.9471674 , -1.3971925 , -6.4188747 , -1.7790052 ,  0.03861272,
       -0.35280612,  1.0356431 , -1.1207013 ,  1.240891  , -1.5177649 ,
        1.3788583 ,  2.014057  , -0.9945446 ,  1.1588986 ,  0.21868615,
        1.1321787 , -0.98895115, -1.8484693 , -1.7223359 ,  1.3790622 ,
        2.2856283 ,  0.45740244,  0.39467898, -0.38629273,  0.8045153 ,
        1.4975424 , -0.43285686, -1.0815718 , -0.55428714, -1.1573505 ,
       -0.14508727,  3.0183074 ,  0.9997595 ,  3.1770122 ,  3.4902725 ,
       -0.1341271 , -0.3940994 ,  0.35976872, -0.8396342 , -0.14406282,
        0.04484363,  0.45852518, -2.6293077 , -1.2270257 , -0.2648226 ,
        3.6768708 ,  0.06674827,  1.4748832 ,  1.652305  , -1.1041977 ,
       -0.0180153 ,  0.41195512,  1.0792816 , -2.2666452 ,  1.0985105 ,
        0.5582074 , -0.78833973,  0.7233552 ,  0.28352848,  1.1825304 ,
       -0.6322761 ,  1.9660307 ,  1.0732826 , -3.7055943 , -1.2365547 ],
      dtype=float32)

2).原始文本

0  做为一本声名在外的流行书,说的还是广州的外企,按道理应该和我的生存环境差不多啊。但是一看之下...
1  作者完全是以一个过来的自认为是成功者的角度去写这个问题,感觉很不客观。虽然不是很喜欢,但是,...
2  作者提倡内调,不信任化妆品,这点赞同。但是所列举的方法太麻烦,配料也不好找。不是太实用。
3  作者的文笔还行,但通篇感觉太琐碎,有点文人的无病呻吟。自由主义者。作者的品性不敢苟同,无民族...
...

3).文本统计

对分词后的语料建立 Word->ID 的映射。

{
'一样': 482,
 '一根': 483,
 '一楼': 484,
 '一模一样': 485,
 '一次': 486,
 '一次性': 487,
 '一次次': 488,
}

4).文本转换成ID序列

原句:做为一本声名在外的流行书,说的还是广州的外企,按道理应该和我的生存环境差不多啊。但是一看之下...
分词:['做','为','一本','声名在外','的','流行','书',',','说','的','还是','广州','的','外企',...]
ID :[ 1489, 947, 478, ...]

上面的模型取得的ACC(准确率)为:0.8944

下面是几个预测的例子感受下:

1). 酒店的环境非常好,价格也便宜,值得推荐

Label : positive(正向)

Predict: positive(正向)

2).手机质量太差了,傻逼店家,赚黑心钱,以后再也不会买了

Label : negative(负向)

Predict: negative(负向)

3).太好了

Label : positive(正向)

Predict: positive(正向)

4).太强了

Label : positive(正向)

Predict: negative(负向) 预测错误!

...

预测效果还是不错的,不过显然“ 太强了 ”预测错了。接下来对上面的网络定义做一下改进,用BiLSTM替换成LSTM。相比于LSTM只能捕获单向的依赖,BiLSTM的改进之处在于可以捕获双向的依赖,如下图:

借助 Keras 只需要对网络定义稍作改动:

...
model.add(Embedding(output_dim=vocab_dim,
                        input_dim=n_symbols,
                        mask_zero=True,
                        weights=[embedding_weights],
                        input_length=input_length))  # Adding Input Length
#model.add(LSTM(output_dim=50, activation='tanh', inner_activation='hard_sigmoid'))
model.add(Bidirectional(LSTM(output_dim=50, activation='tanh', inner_activation='hard_sigmoid'),merge_mode='concat'))
...

重新训练模型,ACC为 :0.9011。

重新预测"太强了":

Label : positive(正向)

Predict: positive(正向)

除此之外还可以尝试 多层LSTM ,感兴趣的可以试下。

代码参考:

github.com/ble55ing/LST

另外还可以考虑 深度LSTM ( BiLSTM ),如下图:


5.3 Attention机制

Attention机制通俗的说,对于某个时刻的输出y,它在输入x上各个部分上的 注意力 ,这里的注意力也就是 权重 ,即 输入x的各个部分对某时刻输入y贡献的权重 。具体的在情感识别任务中,原句中 每个Word对情感类别的影响是不同的 。如下图所示网络结构中 a(h_{t}) 部分的作用是用于计算原句中每个Word对应的权重。 a(h_{t}) 部分的参数与原有网络(这里为LSTM)同时训练,其输入为每个Word对应的隐向量。

Attention机制

在前面LSTM的基础上加入Attention机制,网络定义结构如下:

from sklearn.cross_validation import train_test_split
from keras.models import Sequential
from keras.layers.embeddings import Embedding
from keras.layers.recurrent import LSTM
from keras.layers.core import Dense, Dropout,Activation
from keras.models import model_from_yaml
np.random.seed(1337)  # For Reproducibility
import sys
sys.setrecursionlimit(1000000)
import yaml
import keras
# 定义网络结构
# n_symbols:
# embedding_weights:Embedding矩阵
# x_train:训练集X
# y_train:训练集y
# x_test:测试集x
# y_test:测试集y
def train_lstm(n_symbols,embedding_weights,x_train,y_train,x_test,y_test):
    print('开始定义网络...')
    model = Sequential()
    # 将Word转换成Embedding向量
    model.add(Embedding(output_dim=vocab_dim,
                        input_dim=n_symbols,
                        mask_zero=True,
                        weights=[embedding_weights],
                        input_length=input_length))  # Adding Input Length
    # LSTM
    # 注意这里加了return_sequences=True参数,表示每个Word都会输出一个"隐向量",而不是仅返回最后一个Word的向量。
    model.add(Bidirectional(LSTM(output_dim=50, activation='tanh', inner_activation='hard_sigmoid',return_sequences=True)))    
    model.add(Attention())
    model.add(Dropout(0.5))
    # 全链接层
    model.add(Dense(3, activation='softmax')) # Dense=>全连接层,输出维度=1
    model.add(Activation('softmax'))
    print('Compiling the Model...')
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',metrics=['accuracy'])
    print("开始训练...")
    model.fit(x_train , y_train , batch_size=batch_size , epochs=n_epoch , verbose=1)
    print("模型评估...")
    score = model.evaluate(x_test, y_test,batch_size=batch_size)
    print('ACC:'+str(score))

其中, Attention() 是我们基于Keras的Layer扩展机制自定义的层,计算原理如下。

代码如下:

from tensorflow.keras import backend as K
from tensorflow.keras import initializers, regularizers, constraints
from tensorflow.keras.layers import Layer
class Attention(Layer):
    def __init__(self, step_dim,
                 W_regularizer=None, b_regularizer=None,
                 W_constraint=None, b_constraint=None,
                 bias=True, **kwargs):
        # Input shape(输入尺寸)
            3D tensor with shape: `(samples, steps, features)`.
        # Output shape(输出尺寸)
            2D tensor with shape: `(samples, features)`.
        使用方法非常简单,只需要把Attention层加在RNN(GRU/LSTM/SimpleRNN)后面即可,并把设置return_sequences=True.
        Example:
            model.add(LSTM(64, return_sequences=True))
            model.add(Attention())
            # next add a Dense layer (for classification/regression)
            hidden = LSTM(64, return_sequences=True)(words)
            sentence = Attention()(hidden)
            # next add a Dense layer (for classification/regression)
        self.supports_masking = True
        self.init = initializers.get('glorot_uniform')
        # 模型超参数
        self.W_regularizer = regularizers.get(W_regularizer)
        self.b_regularizer = regularizers.get(b_regularizer)
        self.W_constraint = constraints.get(W_constraint)
        self.b_constraint = constraints.get(b_constraint)
        self.bias = bias
        self.step_dim = step_dim
        self.features_dim = 0
        super(Attention, self).__init__(**kwargs)
    # Attention层的参数定义
    def build(self, input_shape):
        # (samples, steps, features)
        assert len(input_shape) == 3
        self.W = self.add_weight(shape=(input_shape[-1],),
                                 initializer=self.init,
                                 name='{}_W'.format(self.name),
                                 regularizer=self.W_regularizer,
                                 constraint=self.W_constraint)
        self.features_dim = input_shape[-1]
        if self.bias:
            self.b = self.add_weight(shape=(input_shape[1],),
                                     initializer='zero',
                                     name='{}_b'.format(self.name),
                                     regularizer=self.b_regularizer,
                                     constraint=self.b_constraint)
        else:
            self.b = None
        self.built = True
    def compute_mask(self, input, input_mask=None):
        return None
    # Attention层的计算逻辑
    # x的尺寸:(samples, steps, features)
    def call(self, x, mask=None):
        features_dim = self.features_dim
        step_dim = self.step_dim
        e = K.reshape(K.dot(K.reshape(x, (-1, features_dim)), K.reshape(self.W, (features_dim, 1))), (-1, step_dim))  # e = K.dot(x, self.W)
        if self.bias:
            e += self.b
        e = K.tanh(e)
        a = K.exp(e)
        # apply mask after the exp. will be re-normalized next
        if mask is not None:
            # cast the mask to floatX to avoid float64 upcasting in theano
            a *= K.cast(mask, K.floatx())
        # 归一化        
        a /= K.cast(K.sum(a, axis=1, keepdims=True) + K.epsilon(), K.floatx())
        a = K.expand_dims(a)
        c = K.sum(a * x, axis=1)
        return c
    # Attention层的输出尺寸
    # 2D tensor with shape: `(samples, features)`.
    def compute_output_shape(self, input_shape):
        return input_shape[0], self.features_dim

用上节的数据训练网络准确率为 0.8924,性能没有得到提升,还需要进一步深入调参。 感兴趣的可以试一下。

最期兴起的Transformer、Bert,其 核心思量也是Attention ,都可以尝试一下。


参考资料:

1. 达观数据曾彦能:如何用深度学习做好长文本分类与法律文书智能化处理

2.tensorflow文本分类实战(五)——HAN模型

zhuanlan.zhihu.com/p/97

3.keras实现Attention机制

jianshu.com/p/31c0acf94

4.tensorflow文本分类实战(四)——Bi-LSTM+Attention

zhuanlan.zhihu.com/p/97

5.python - Keras attention layer over LSTM

itkeyword.com/doc/34175

6.tensorflow文本分类实战(五)——HAN模型

zhuanlan.zhihu.com/p/97

7. keras-attention-mechanism

github.com/philipperemy

8.[深度应用]·Keras实现Self-Attention文本分类(机器如何读懂人心)

cloud.tencent.com/devel

9.基于Keras实现双向LSTM,可视化其注意力机制

elecfans.com/d/786485.h

10. 文本分类实战(五)—— Bi-LSTM + Attention模型

cnblogs.com/jiangxinyan

11.[深度应用]·Keras实现Self-Attention文本分类

cloud.tencent.com/devel

12.基于改进注意力机制的短文本情感分析研究_杨帆

13.干货 | Attention注意力机制超全综述

cloud.tencent.com/devel

14.什么是注意力机制?

cnblogs.com/ydcode/p/11

15. Sentiment_analysis

github.com/mpk001/Senti


5.4 HAN模型

HAN(Hierarchical Attention Networks), 层次注意力网络 。对 文档/较长文本 进行分类的时候,仅仅对word粒度进行Attention是不够的,还需要对各个句子(短句)进行Attention的学习, 不同句子也需要分配不同的权重 每个句子里的词语也分配不同的权重 网络结构如下图所示:

HAN网络

基于Keras的网络结构定义如下( 参考自: zhuanlan.zhihu.com/p/97 ):

from tensorflow.keras import Input, Model
from tensorflow.keras.layers import Embedding, Dense, Dropout, Bidirectional, LSTM, TimeDistributed
# 定义网络结构
# n_symbols:
# maxlen_sentence:最大子句数量
# maxlen_word    :子句最大数量
# max_features   :Word数量
# class_num      :情感类别数量
# embedding_dims :Word Embedding向量维度
class HAN(object):
    def __init__(self, maxlen_sentence, maxlen_word, max_features, embedding_dims,
                 class_num=3,
                 last_activation='softmax'):
        self.maxlen_sentence = maxlen_sentence
        self.maxlen_word = maxlen_word
        self.max_features = max_features
        self.embedding_dims = embedding_dims
        self.class_num = class_num
        self.last_activation = last_activation
    # 模型定义
    def get_model(self):
        # 单词(Word)部分
        input_word = Input(shape=(self.maxlen_word,))
        x_word = Embedding(self.max_features, self.embedding_dims, input_length=self.maxlen_word)(input_word)
        x_word = Bidirectional(LSTM(128, return_sequences=True))(x_word)
        x_word = Attention(self.maxlen_word)(x_word)
        model_word = Model(input_word, x_word)
        # 句子部分
        input = Input(shape=(self.maxlen_sentence, self.maxlen_word))
        x_sentence = TimeDistributed(model_word)(input)