大数据舆情情感分析,如何提取情感并使用什么样的工具?(贴情感标签)

1、我将数据筛选预处理好,然后分好词。 2、是不是接下来应该与与情感词汇本库对照,生成结合词频和情感词库的情感关键词库。 3、将信息与情感关键词库进行…
关注者
814
被浏览
121,893

47 个回答

情感分析说白了,就是一个文本(多)分类问题,我看一般的情感分析都是2类(正负面)或者3类(正面、中性和负面)。其实,这种粒度是远远不够的。本着“Talk is cheap, show you my code”的原则,我不扯咸淡,直接上代码给出解决方案(而且是经过真实文本数据验证了的:我用一个14个分类的例子来讲讲各类文本分类模型---从传统的机器学习文本分类模型到现今流行的基于深度学习的文本分类模型,最后给出一个超牛逼的模型集成,效果最优。

*********************************************************************************************前方高能 **********前方高能************前方高能******************************************************************************************************************************

在这篇文章中,笔者将讨论自然语言处理中文本分类的相关问题。笔者将使用一个复旦大学开源的文本分类语料库,对文本分类的一般流程和常用模型进行探讨。首先,笔者会创建一个非常基础的初始模型,然后使用不同的特征进行改进。 接下来,笔者还将讨论如何使用深度神经网络来解决NLP问题,并在文章末尾以一般关于集成的一些想法结束这篇文章。

本文覆盖的NLP方法有:

  • TF-IDF
  • Count Features
  • Logistic Regression
  • Naive Bayes
  • SVM
  • Xgboost
  • Grid Search
  • Word Vectors
  • Dense Network
  • LSTM
  • GRU
  • Ensembling

NOTE: 笔者并不能保证你学习了本notebook之后就能在NLP相关比赛中获得非常高的分数。 但是,如果你正确地“吃透”它,并根据实际情况适时作出一些调整,你可以获得非常高的分数。 废话不多说,让我们开始导入一些我将要使用的重要python模块。

import pandas as pd
import numpy as np
import xgboost as xgb
from tqdm import tqdm
from sklearn.svm import SVC
from keras.models import Sequential
from keras.layers.recurrent import LSTM, GRU
from keras.layers.core import Dense, Activation, Dropout
from keras.layers.embeddings import Embedding
from keras.layers.normalization import BatchNormalization
from keras.utils import np_utils
from sklearn import preprocessing, decomposition, model_selection, metrics, pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import TruncatedSVD
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from keras.layers import GlobalMaxPooling1D, Conv1D, MaxPooling1D, Flatten, Bidirectional, SpatialDropout1D
from keras.preprocessing import sequence, text
from keras.callbacks import EarlyStopping
from nltk import word_tokenize

接下来是加载并检视数据集。

data=pd.read_excel('/home/kesci/input/Chinese_NLP6474/复旦大学中文文本分类语料.xlsx','sheet1') 
data.head()
data.info()

对文本数据的正文字段进行分词,这里是在Linux上运行的,可以开启jieba的并行分词模式,分词速度是平常的好多倍,具体看你的CPU核心数。

import jieba
jieba.enable_parallel(16) #并行分词开启
data['文本分词'] = data['正文'].apply(lambda i:jieba.cut(i) )
data['文本分词'] =[' '.join(i) for i in data['文本分词']]

值得注意的是,分词是任何中文文本分类的起点,分词的质量会直接影响到后面的模型效果。在这里,作为演示,笔者有点偷懒,其实你还可以:

  • 设置可靠的自定义词典,以便分词更精准;
  • 采用分词效果更好的分词器,如pyltp、THULAC、Hanlp等;
  • 编写预处理类,就像下面要谈到的数字特征归一化,去掉文本中的#@¥%……&等等。
data.分类.unique()
data.head()

这是一个典型的文本多分类问题,需要将文本划分到给定的14个主题上。

针对该问题,笔者采用了kaggle上通用的 Multi-Class Log-Loss 作为评测指标(Evaluation Metric).

def multiclass_logloss(actual, predicted, eps=1e-15):
    """对数损失度量(Logarithmic Loss  Metric)的多分类版本。
    :param actual: 包含actual target classes的数组
    :param predicted: 分类预测结果矩阵, 每个类别都有一个概率
    """
    # Convert 'actual' to a binary array if it's not already:
    if len(actual.shape) == 1:
        actual2 = np.zeros((actual.shape[0], predicted.shape[1]))
        for i, val in enumerate(actual):
            actual2[i, val] = 1
        actual = actual2

    clip = np.clip(predicted, eps, 1 - eps)
    rows = actual.shape[0]
    vsota = np.sum(actual * np.log(clip))
    return -1.0 / rows * vsota

接下来用scikit-learn中的Label Encoder将文本标签(Text Label)转化为数字(Integer)

lbl_enc = preprocessing.LabelEncoder()
y = lbl_enc.fit_transform(data.分类.values)

在进一步研究之前,我们必须将数据分成训练和验证集。 我们可以使用scikit-learn的model_selection模块中的train_test_split来完成它。

xtrain, xvalid, ytrain, yvalid = train_test_split(data.文本分词.values, y, 
                                                  stratify=y, 
                                                  random_state=42, 
                                                  test_size=0.1, shuffle=True)

print (xtrain.shape)
print (xvalid.shape)

(8324,)
(925,)

构建基础模型(Basic Models)

让我们先创建一个非常基础的模型。

我们非常基础的模型(very first model) 是一个简单的 TF-IDF (Term Frequency - Inverse Document Frequency)+逻辑斯底回归(Logistic Regression).

def number_normalizer(tokens):
    """ 将所有数字标记映射为一个占位符(Placeholder)。
    对于许多实际应用场景来说,以数字开头的tokens不是很有用,
    但这样tokens的存在也有一定相关性。 通过将所有数字都表示成同一个符号,可以达到降维的目的。
    """
    return ("#NUMBER" if token[0].isdigit() else token for token in tokens)


class NumberNormalizingVectorizer(TfidfVectorizer):
    def build_tokenizer(self):
        tokenize = super(NumberNormalizingVectorizer, self).build_tokenizer()
        return lambda doc: list(number_normalizer(tokenize(doc)))

利用刚才创建的NumberNormalizingVectorizer类来提取文本特征,注意里面各类参数的含义,自己去sklearn官方网站找教程看

stwlist=[line.strip() for line in open('/home/gaochangkuan/input/stopwords7085/停用词汇总.txt',
'r',encoding='utf-8').readlines()]
tfv = NumberNormalizingVectorizer(min_df=3,  
                                  max_df=0.5,
                                  max_features=None,                 
                                  ngram_range=(1, 2), 
                                  use_idf=True,
                                  smooth_idf=True,
                                  stop_words = stwlist)

使用TF-IDF来fit训练集和测试集(半监督学习)

tfv.fit(list(xtrain) + list(xvalid))
xtrain_tfv =  tfv.transform(xtrain) 
xvalid_tfv = tfv.transform(xvalid)

利用提取的TFIDF特征来fit一个简单的Logistic Regression :

clf = LogisticRegression(C=1.0,solver='lbfgs',multi_class='multinomial')
clf.fit(xtrain_tfv, ytrain)
predictions = clf.predict_proba(xvalid_tfv)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.627

做完第一个基础模型后,得出的 multiclass logloss 是0.627.

但笔者“贪婪”,想要获得更好的分数。 基于相同模型采用不同的特征,再看看结果如何。

我们也可以使用词汇计数(Word Counts)作为功能,而不是使用TF-IDF。 这可以使用scikit-learn中的CountVectorizer轻松完成。

ctv = CountVectorizer(min_df=3,
                      max_df=0.5,
                      ngram_range=(1,2),
                      stop_words = stwlist)

使用Count Vectorizer来fit训练集和测试集(半监督学习)

ctv.fit(list(xtrain) + list(xvalid))
xtrain_ctv =  ctv.transform(xtrain) 
xvalid_ctv = ctv.transform(xvalid)

利用提取的word counts特征来fit一个简单的Logistic Regression .

clf = LogisticRegression(C=1.0,solver='lbfgs',multi_class='multinomial')
clf.fit(xtrain_ctv, ytrain)
predictions = clf.predict_proba(xvalid_ctv)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.732

比之前者,效果略差。。。

接下来,让我们尝试一个非常简单的模型- 朴素贝叶斯,它在以前是非常有名的。
让我们看看当我们在这个数据集上使用朴素贝叶时会发生什么:

利用提取的TFIDF特征来拟合Naive Bayes

clf = MultinomialNB()
clf.fit(xtrain_tfv, ytrain)

predictions = clf.predict_proba(xvalid_tfv)
print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.982

效果更差。。。。。。

朴素贝叶斯模型的表现不怎么样! 但基于词汇计数的逻辑回归的效果仍然很棒! 当我们在基于词汇计数的基础上使用朴素贝叶斯模型时会发生什么?

利用提取的word counts特征来fitNaive Bayes

clf = MultinomialNB()
clf.fit(xtrain_ctv, ytrain)
predictions = clf.predict_proba(xvalid_ctv)
print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 3.780
。。。。。。
这次效果感觉不咋样。 传统文本分类算法里还有一个名叫支持向量机(SVM)。 SVM曾是很多机器学习爱好者的“最爱”。 因此,我们必须在此数据集上尝试SVM。
由于SVM需要花费大量时间,因此在应用SVM之前,我们将使用奇异值分解(Singular Value Decomposition )来减少TF-IDF中的特征数量。
同时,在使用SVM之前,我们还需要将数据标准化(Standardize Data )

使用SVD进行降维,components设为120,对于SVM来说,SVD的components的合适调整区间一般为120~200 。

svd = decomposition.TruncatedSVD(n_components=120)
svd.fit(xtrain_tfv)
xtrain_svd = svd.transform(xtrain_tfv)
xvalid_svd = svd.transform(xvalid_tfv)

对从SVD获得的数据进行缩放:

scl = preprocessing.StandardScaler()
scl.fit(xtrain_svd)
xtrain_svd_scl = scl.transform(xtrain_svd)
xvalid_svd_scl = scl.transform(xvalid_svd)

现在是时候应用SVM模型进行文本分类了。 在运行以下单元格后,你可以去喝杯茶了---因为这将耗费大量的时间...

调用下SVM模型:

clf = SVC(C=1.0, probability=True) # since we need probabilities
clf.fit(xtrain_svd_scl, ytrain)
predictions = clf.predict_proba(xvalid_svd_scl)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.347

看起来,SVM在这些数据上表现还行!
在采用更高级的算法前,让我们再试试Kaggle上应用最流行的算法:xgboost!

基于tf-idf特征,使用xgboost

clf = xgb.XGBClassifier(max_depth=7, n_estimators=200, colsample_bytree=0.8, 
                        subsample=0.8, nthread=10, learning_rate=0.1)
clf.fit(xtrain_tfv.tocsc(), ytrain)
predictions = clf.predict_proba(xvalid_tfv.tocsc())

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.182

效果不错,比SVM还牛呢!

基于word counts特征,使用xgboost:

clf = xgb.XGBClassifier(max_depth=7, n_estimators=200, colsample_bytree=0.8, 
                        subsample=0.8, nthread=10, learning_rate=0.1)
clf.fit(xtrain_ctv.tocsc(), ytrain)
predictions = clf.predict_proba(xvalid_ctv.tocsc())

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.154

基于tf-idf的svd特征,使用xgboost

clf = xgb.XGBClassifier(max_depth=7, n_estimators=200, colsample_bytree=0.8, 
                        subsample=0.8, nthread=10, learning_rate=0.1)
clf.fit(xtrain_svd, ytrain)
predictions = clf.predict_proba(xvalid_svd)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.394

再对经过数据标准化(Scaling)的tf-idf-svd特征使用xgboost

clf = xgb.XGBClassifier(nthread=10)
clf.fit(xtrain_svd_scl, ytrain)
predictions = clf.predict_proba(xvalid_svd_scl)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.373

XGBoost的效果似乎挺棒的! 但我觉得还可以进一步优化,因为我还没有做过任何超参数优化。 我很懒,所以我会告诉你该怎么做,你可以自己做!)。 这将在下一节中讨论:

网格搜索(Grid Search)

网格搜索是一种超参数优化的技巧。 如果知道这个技巧,你可以通过获取最优的参数组合来产生良好的文本分类效果。

在本节中,我将讨论使用基于逻辑回归模型的网格搜索。

在开始网格搜索之前,我们需要创建一个评分函数,这可以通过scikit-learn的make_scorer函数完成的。

mll_scorer = metrics.make_scorer(multiclass_logloss, 
                       greater_is_better=False, needs_proba=True)

接下来,我们需要一个pipeline。 为了演示,我将使用由SVD(进行特征缩放)和逻辑回归模型组成的pipeline。


#SVD初始化
svd = TruncatedSVD()

# Standard Scaler初始化
scl = preprocessing.StandardScaler()

# 再一次使用Logistic Regression
lr_model = LogisticRegression()

# 创建pipeline 
clf = pipeline.Pipeline([('svd', svd),
                         ('scl', scl),
                         ('lr', lr_model)])

接下来我们需要一个参数网格(A Grid of Parameters):

param_grid = {'svd__n_components' : [120, 180],
              'lr__C': [0.1, 1.0, 10], 
              'lr__penalty': ['l1', 'l2']}

因此,对于SVD,我们评估120和180个分量(Components),对于逻辑回归,我们评估三个不同的学习率C值,其中惩罚函数为l1和l2。 现在,我们可以开始对这些参数进行网格搜索咯。

网格搜索模型(Grid Search Model)初始化:

model = GridSearchCV(estimator=clf, param_grid=param_grid, scoring=mll_scorer,
                                 verbose=10, n_jobs=-1, iid=True, refit=True, cv=2)

fit网格搜索模型:

model.fit(xtrain_tfv, ytrain)  #为了减少计算量,这里我们仅使用xtrain
print("Best score: %0.3f" % model.best_score_)
print("Best parameters set:")
best_parameters = model.best_estimator_.get_params()
for param_name in sorted(param_grid.keys()):
    print("\t%s: %r" % (param_name, best_parameters[param_name]))

logloss: 0.377

nb_model = MultinomialNB()

创建pipeline :

clf = pipeline.Pipeline([('nb', nb_model)])

搜索参数设置:

param_grid = {'nb__alpha': [0.001, 0.01, 0.1, 1, 10, 100]}

网格搜索模型(Grid Search Model)初始化:

model = GridSearchCV(estimator=clf, param_grid=param_grid, scoring=mll_scorer,
                                 verbose=10, n_jobs=-1, iid=True, refit=True, cv=2)

fit网格搜索模型:

model.fit(xtrain_tfv, ytrain)  # 为了减少计算量,这里我们仅使用xtrain
print("Best score: %0.3f" % model.best_score_)
print("Best parameters set:")
best_parameters = model.best_estimator_.get_params()
for param_name in sorted(param_grid.keys()):
    print("\t%s: %r" % (param_name, best_parameters[param_name]))

在NLP问题中,我们习惯于检索词向量(Word Vectors)。 Word向量提供了很多关于数据的见解。 接下来,让我们来深入研究一下。

基于word2vec的词嵌入

在不深入细节的情况下,笔者将解释如何创建语句向量(Sentence Vectors),以及如何基于它们在其上创建机器学习模型。鄙人是GloVe向量,word2vec和fasttext的粉丝(但平时还是用word2vec较多)。在这篇文章中,笔者使用的文本分类模型是基于Word2vec词向量模型(100维)。

训练word2vec词向量:

import gensim
model = gensim.models.Word2Vec(X, size=100)  

X是经分词后的文本构成的list,也就是tokens的列表的列表.

注意,Word2Vec还有3个值得关注的参数,iter是模型训练时迭代的次数,假如参与训练的文本量较少,就需要把这个参数调大一些;sg是模型训练算法的类别,1 代表 skip-gram,;0代表 CBOW;window控制窗口,它指当前词和预测词之间的最大距离,如果设得较小,那么模型学习到的是词汇间的功能性特征(词性相异),如果设置得较大,会学习到词汇之间的相似性特征(词性相同)的大小,假如语料够多,笔者一般会设置得大一些,8~10。
embeddings_index = dict(zip(model.wv.index2word, model.wv.syn0))

print('Found %s word vectors.' % len(embeddings_index))

Found 56,000word vectors

该函数会将语句转化为一个标准化的向量(Normalized Vector)

def sent2vec(s):
    words = str(s).lower()
    words = word_tokenize(words)
    words = [w for w in words if not w in stop_words]
    words = [w for w in words if w.isalpha()]
    M = []
    for w in words:
        try:
            M.append(embeddings_index[w])
        except:
            continue
    M = np.array(M)
    v = M.sum(axis=0)
    if type(v) != np.ndarray:
        return np.zeros(300)
    return v / np.sqrt((v ** 2).sum())

对训练集和验证集使用上述函数,进行文本向量化处理

xtrain_w2v  = [sent2vec(x) for x in tqdm(xtrain)]
xvalid_w2v  = [sent2vec(x) for x in tqdm(xvalid)]

xtrain_w2v  = np.array(xtrain_w2v)
xvalid_w2v  = np.array(xvalid_w2v)

让我们看看xgboost在Glove词向量特征的表现如何:

基于word2vec特征使用XGB文本分类器:

clf = xgb.XGBClassifier(nthread=10, silent=False)
clf.fit(xtrain_glove, ytrain)
predictions = clf.predict_proba(xvalid_w2v)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.389

clf = xgb.XGBClassifier(max_depth=7, n_estimators=200, colsample_bytree=0.8, 
                        subsample=0.8, nthread=10, learning_rate=0.1, silent=False)
clf.fit(xtrain_w2v, ytrain)
predictions = clf.predict_proba(xvalid_w2v)

print ("logloss: %0.3f " % multiclass_logloss(yvalid, predictions))

logloss: 0.122

我们可以看到,简单的对参数进行微调,就提高基于word2vec词向量特征的xgboost得分! 相信我,你还可以从中继续“压榨”出更优秀的表现!

深度学习(Deep Learning)

这是一个深度学习大行其道的时代! 文本分类问题在它的指引下得到了突飞猛进的发展! 在这里,我们将在word2vec功能上训练LSTM和简单的全连接网络(Dense Network)。
让我们先从全连接网络开始:

在使用神经网络前,对数据进行缩放:

scl = preprocessing.StandardScaler()
xtrain_w2v_scl = scl.fit_transform(xtrain_w2v)
xvalid_w2v_scl = scl.transform(xvalid_w2v)

对标签进行binarize处理

ytrain_enc = np_utils.to_categorical(ytrain)
yvalid_enc = np_utils.to_categorical(yvalid)

创建1个3层的序列神经网络(Sequential Neural Net)

model = Sequential()

model.add(Dense(300, input_dim=300, activation='relu'))
model.add(Dropout(0.2))
model.add(BatchNormalization())

model.add(Dense(300, activation='relu'))
model.add(Dropout(0.3))
model.add(BatchNormalization())

model.add(Dense(14))
model.add(Activation('softmax'))

对模型进行编译和拟合:

model.compile(loss='categorical_crossentropy', optimizer='adam')

model.fit(xtrain_w2v_scl, y=ytrain_enc, batch_size=64, 
          epochs=5, verbose=1, 
          validation_data=(xvalid_v_scl, yvalid_enc))

logloss: 0.422
你需要不断的对神经网络的参数进行调优,添加更多层,增加Dropout以获得更好的结果。 在这里,笔者只是简单的实现下,追求速度而不是最终效果,并且它比没有任何优化的xgboost取得了更好的结果:)

为了更进一步,笔者使用LSTM,我们需要对文本数据进行Tokenize:

token = text.Tokenizer(num_words=None)
max_len = 70

token.fit_on_texts(list(xtrain) + list(xvalid))
xtrain_seq = token.texts_to_sequences(xtrain)
xvalid_seq = token.texts_to_sequences(xvalid)

对文本序列进行zero填充:

xtrain_pad = sequence.pad_sequences(xtrain_seq, maxlen=max_len)
xvalid_pad = sequence.pad_sequences(xvalid_seq, maxlen=max_len)

word_index = token.word_index

基于已有的数据集中的词汇创建一个词嵌入矩阵(Embedding Matrix):

embedding_matrix = np.zeros((len(word_index) + 1, 100))
for word, i in tqdm(word_index.items()):
    embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None:
        embedding_matrix[i] = embedding_vector

基于前面训练的Word2vec词向量,使用1个两层的LSTM模型:

model = Sequential()
model.add(Embedding(len(word_index) + 1,
                     100,
                     weights=[embedding_matrix],
                     input_length=max_len,
                     trainable=False))
model.add(SpatialDropout1D(0.3))
model.add(LSTM(100, dropout=0.3, recurrent_dropout=0.3))

model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.8))

model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.8))

model.add(Dense(14))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')

model.fit(xtrain_pad, y=ytrain_enc, batch_size=512, epochs=100, verbose=1, validation_data=(xvalid_pad, yvalid_enc))

logloss: 0.312

现在,我们看到分数小于0.5。 我跑了很多个epochs都没有获得最优的结果,但我们可以使用early stopping来停止在最佳的迭代节点。

那我们该如何使用early stopping?

好吧,其实很简单的。 让我们再次compile模型:

#基于前面训练的Word2vec词向量,使用1个两层的LSTM模型
model = Sequential()
model.add(Embedding(len(word_index) + 1,
                     100,
                     weights=[embedding_matrix],
                     input_length=max_len,
                     trainable=False))
model.add(SpatialDropout1D(0.3))
model.add(LSTM(100, dropout=0.3, recurrent_dropout=0.3))

model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.8))

model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.8))

model.add(Dense(14))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')

在模型拟合时,使用early stopping这个回调函数(Callback Function)

earlystop = EarlyStopping(monitor='val_loss', min_delta=0, patience=3, verbose=0, mode='auto')
model.fit(xtrain_pad, y=ytrain_enc, batch_size=512, epochs=100, 
          verbose=1, validation_data=(xvalid_pad, yvalid_enc), callbacks=[earlystop])

logloss: 0.487
一个可能的问题是:为什么我会使用这么多的dropout? 嗯,fit模型时,没有或很少的dropout,你会出现过拟合(Overfit):)

让我们看看双向长短时记忆(Bi-Directional LSTM)是否可以给我们带来更好的结果。 对于Keras来说,使用Bilstm小菜一碟:)

基于前面训练的Word2vec词向量,构建1个2层的Bidirectional LSTM :

model = Sequential()
model.add(Embedding(len(word_index) + 1,
                     100,
                     weights=[embedding_matrix],
                     input_length=max_len,
                     trainable=False))
model.add(SpatialDropout1D(0.3))
model.add(Bidirectional(LSTM(100, dropout=0.3, recurrent_dropout=0.3)))

model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.8))

model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.8))

model.add(Dense(14))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')

在模型拟合时,使用early stopping这个回调函数(Callback Function)

earlystop = EarlyStopping(monitor='val_loss', min_delta=0, patience=3, verbose=0, mode='auto')
model.fit(xtrain_pad, y=ytrain_enc, batch_size=512, epochs=100, 
          verbose=1, validation_data=(xvalid_pad, yvalid_enc), callbacks=[earlystop])

logloss: 0.119

很接近最优结果了! 让我们尝试两层的GRU:

# 基于前面训练的Word2vec词向量,构建1个2层的GRU模型
model = Sequential()
model.add(Embedding(len(word_index) + 1,
                     100,
                     weights=[embedding_matrix],
                     input_length=max_len,
                     trainable=False))
model.add(SpatialDropout1D(0.3))
model.add(GRU(100, dropout=0.3, recurrent_dropout=0.3, return_sequences=True))
model.add(GRU(100, dropout=0.3, recurrent_dropout=0.3))

model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.8))

model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.8))

model.add(Dense(14))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')

模型拟合时,使用early stopping这个回调函数(Callback Function)

earlystop = EarlyStopping(monitor='val_loss', min_delta=0, patience=3, verbose=0, mode='auto')
model.fit(xtrain_pad, y=ytrain_enc, batch_size=512, epochs=100, 
          verbose=1, validation_data=(xvalid_pad, yvalid_enc), callbacks=[earlystop])

logloss: 0.107

太好了! 比我们以前的模型好多了! 持续优化,模型的性能将不断提高。

在文本分类的比赛中,想要获得最高分,你应该拥有1个合成的模型。 让我们来看看吧!

模型集成(Model Ensembling)

集多个文本分类模型之长,合成一个很棒的分类融合模型。

#创建一个Ensembling主类,具体使用方法见下一个cell
import numpy as np
from sklearn.metrics import roc_auc_score
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import StratifiedKFold, KFold
import pandas as pd
import os
import sys
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="[%(asctime)s] %(levelname)s %(message)s",
    datefmt="%H:%M:%S", stream=sys.stdout)
logger = logging.getLogger(__name__)

再来编写主类:

class Ensembler(object):
    def __init__(self, model_dict, num_folds=3, task_type='classification', optimize=roc_auc_score,
                 lower_is_better=False, save_path=None):
          """
        Ensembler init function
        :param model_dict: 模型字典 
        :param num_folds: ensembling所用的fold数量
        :param task_type: 分类(classification) 还是回归(regression)
        :param optimize: 优化函数,比如 AUC, logloss, F1等,必须有2个函数,即y_test 和 y_pred
        :param lower_is_better: 优化函数(Optimization Function)的值越低越好还是越高越好
        :param save_path: 模型保存路径
        """

        self.model_dict = model_dict
        self.levels = len(self.model_dict)
        self.num_folds = num_folds
        self.task_type = task_type
        self.optimize = optimize
        self.lower_is_better = lower_is_better
        self.save_path = save_path

        self.training_data = None
        self.test_data = None
        self.y = None
        self.lbl_enc = None
        self.y_enc = None
        self.train_prediction_dict = None
        self.test_prediction_dict = None
        self.num_classes = None

    def fit(self, training_data, y, lentrain):
        """
        :param training_data: 二维表格形式的训练数据
        :param y: 二进制的, 多分类或回归
        :return: 用于预测的模型链(Chain of Models)

        """

        self.training_data = training_data
        self.y = y

        if self.task_type == 'classification':
            self.num_classes = len(np.unique(self.y))
logger.info("Found %d classes", self.num_classes)
            self.lbl_enc = LabelEncoder()
            self.y_enc = self.lbl_enc.fit_transform(self.y)
            kf = StratifiedKFold(n_splits=self.num_folds)
            train_prediction_shape = (lentrain, self.num_classes)
        else:
            self.num_classes = -1
            self.y_enc = self.y
            kf = KFold(n_splits=self.num_folds)
            train_prediction_shape = (lentrain, 1)

        self.train_prediction_dict = {}
        for level in range(self.levels):
            self.train_prediction_dict[level] = np.zeros((train_prediction_shape[0],
                                                          train_prediction_shape[1] * len(self.model_dict[level])))

        for level in range(self.levels):

            if level == 0:
                temp_train = self.training_data
            else:
                temp_train = self.train_prediction_dict[level - 1]

            for model_num, model in enumerate(self.model_dict[level]):
                validation_scores = []
                foldnum = 1
                for train_index, valid_index in kf.split(self.train_prediction_dict[0], self.y_enc):
logger.info("Training Level %d Fold # %d. Model # %d", level, foldnum, model_num)

                    if level != 0:
                        l_training_data = temp_train[train_index]
                        l_validation_data = temp_train[valid_index]
model.fit(l_training_data, self.y_enc[train_index])
                    else:
                        l0_training_data = temp_train[0][model_num]
                        if type(l0_training_data) == list:
                            l_training_data = [x[train_index] for x in l0_training_data]
                            l_validation_data = [x[valid_index] for x in l0_training_data]
                        else:
                            l_training_data = l0_training_data[train_index]
                            l_validation_data = l0_training_data[valid_index]
model.fit(l_training_data, self.y_enc[train_index])

logger.info("Predicting Level %d. Fold # %d. Model # %d", level, foldnum, model_num)

                    if self.task_type == 'classification':
                        temp_train_predictions = model.predict_proba(l_validation_data)
                        self.train_prediction_dict[level][valid_index,
                        (model_num * self.num_classes):(model_num * self.num_classes) +
                                                       self.num_classes] = temp_train_predictions

                    else:
                        temp_train_predictions = model.predict(l_validation_data)
                        self.train_prediction_dict[level][valid_index, model_num] = temp_train_predictions
                    validation_score = self.optimize(self.y_enc[valid_index], temp_train_predictions)
                    validation_scores.append(validation_score)
logger.info("Level %d. Fold # %d. Model # %d. Validation Score = %f", level, foldnum, model_num,
                                validation_score)
                    foldnum += 1
                avg_score = np.mean(validation_scores)
                std_score = np.std(validation_scores)
logger.info("Level %d. Model # %d. Mean Score = %f. Std Dev = %f", level, model_num,
                            avg_score, std_score)

logger.info("Saving predictions for level # %d", level)
            train_predictions_df = pd.DataFrame(self.train_prediction_dict[level])
            train_predictions_df.to_csv(os.path.join(self.save_path, "train_predictions_level_" + str(level) + ".csv"),
                                        index=False, header=None)

        return self.train_prediction_dict

    def predict(self, test_data, lentest):
        self.test_data = test_data
        if self.task_type == 'classification':
            test_prediction_shape = (lentest, self.num_classes)
        else:
            test_prediction_shape = (lentest, 1)

        self.test_prediction_dict = {}
        for level in range(self.levels):
            self.test_prediction_dict[level] = np.zeros((test_prediction_shape[0],
                                                         test_prediction_shape[1] * len(self.model_dict[level])))
        self.test_data = test_data
        for level in range(self.levels):
            if level == 0:
                temp_train = self.training_data
                temp_test = self.test_data
            else:
                temp_train = self.train_prediction_dict[level - 1]
                temp_test = self.test_prediction_dict[level - 1]

            for model_num, model in enumerate(self.model_dict[level]):

logger.info("Training Fulldata Level %d. Model # %d", level, model_num)
                if level == 0:
model.fit(temp_train[0][model_num], self.y_enc)
                else:
model.fit(temp_train, self.y_enc)

logger.info("Predicting Test Level %d. Model # %d", level, model_num)

                if self.task_type == 'classification':
                    if level == 0:
                        temp_test_predictions = model.predict_proba(temp_test[0][model_num])
                    else:
                        temp_test_predictions = model.predict_proba(temp_test)
                    self.test_prediction_dict[level][:, (model_num * self.num_classes): (model_num * self.num_classes) +
                                                                                        self.num_classes] = temp_test_predictions

                else:
                    if level == 0:
                        temp_test_predictions = model.predict(temp_test[0][model_num])
                    else:
                        temp_test_predictions = model.predict(temp_test)
                    self.test_prediction_dict[level][:, model_num] = temp_test_predictions

            test_predictions_df = pd.DataFrame(self.test_prediction_dict[level])
            test_predictions_df.to_csv(os.path.join(self.save_path, "test_predictions_level_" + str(level) + ".csv"),
                                       index=False, header=None)

        return self.test_prediction_dict

# specify the data to be used for every level of ensembling:
train_data_dict = {0: [xtrain_tfv, xtrain_ctv, xtrain_tfv, xtrain_ctv], 1: [xtrain_glove]}
test_data_dict = {0: [xvalid_tfv, xvalid_ctv, xvalid_tfv, xvalid_ctv], 1: [xvalid_glove]}

model_dict = {0: [LogisticRegression(), LogisticRegression(), MultinomialNB(alpha=0.1), MultinomialNB()],

              1: [xgb.XGBClassifier(silent=True, n_estimators=120, max_depth=7)]}

为每个level的集成指定使用数据:

ens = Ensembler(model_dict=model_dict, num_folds=3, task_type='classification',
                optimize=multiclass_logloss, lower_is_better=True, save_path='')
ens.fit(train_data_dict, ytrain, lentrain=xtrain_w2v.shape[0])
preds = ens.predict(test_data_dict, lentest=xvalid_w2v.shape[0])

检视损失率

multiclass_logloss(yvalid, preds[1])

logloss: 0.09

因此,我们看到集成模型在很大程度上提高了分数!但要注意,集成模型只有在参与集成的模型势均力敌 - 表现都不差的情况下才能取得良好的效果,不然会出现拖后腿的情况,导致模型的整体性能还不如单个模型的要好~


由于本文只是一个教程,更多的技术细节还没有深入下去,对此,你可以利用空余时间多多优化下,也可以尝试其他方法,比如:

  • 基于CNN的文本分类,达到的效果类似于N-gram,效率奇高
  • 基于Attention机制的BiLSTM、Hierarchical LSTM等
  • 基于ELMO、BERT等预训练模型来提取高质量的文本特征,再喂给分类器

...


以上就是笔者的分享,希望大家喜欢,也希望大家踊跃留言,发表看法和意见,我会持续更新的。

Note:需要训练语料的朋友请关注我的公众号【Social Listening与文本挖掘】,在后台回复 “语料”即可得到训练语料的下载链接。


笔者在和鲸(科赛)上的notebook附加资料 :

  1. 基于attention的情感分析,kesci.com/home/project/
  2. 【NLP文本表示】如何科学的在Tensorflow里使用词嵌入 ,kesci.com/home/project/
  3. 基于Position_Embedding和 Attention机制进行文本分类,kesci.com/home/project/
  4. 【BERT-至今最强大的NLP大杀器!】基于BERT的文本分类,kesci.com/home/project/
  5. NLP分析利器】利用Foolnltk进行自然语言处理,kesci.com/home/project/
  6. 文本挖掘】基于DBSCAN的文本聚类,kesci.com/home/project/

情感分析是学术领域研究多年的课题,用google学术搜索可以找到很多paper,基本的方法上有基于词典规则的方法、语言文法的方法,此外还有分类器以及近几年比较火的深度学习的方法(稍后有详细介绍)。

各类paper是有一定的借鉴意义的,不过这主要是学术界在单个问题上的细化,要真正从研究领域落地到大数据的处理还有很多工作要做。

一、工程上的处理流程

工程上的处理流程具体包括以下几个方面:

1、情感分析任务的界定

在进行情感分析任务的界定时,要弄清楚工程的需求到底是什么;要分析文本的哪个层面上的情感,比如篇章、段落、句子、短语、词等粒度;是不是要分析所有的文本还是分析其中的部分文本;准许的错误误差是在个什么范围内等。

2、情感分析标准的制定

在实际的企业应用中往往要根据行业的特点来制定一些情感分析的标准,甚至要从客户的立场中去建立标准。根据国双实际接触客户的经验,在行业上建立标准后,还需要再具体跟客户做一些适度调整。

3、 语料数据加工、词典加工

有了上一步的工作, 接下来进行加工语料或者字典的总结。这一步中不同的方法要做的工作不同,基本上是铺人力的工作,难点是让各个语料加工人员能协调一致,执行统一的标准 (通常会在这个过程中还会反作用到第二步情感分析标准的制定,因为看到实际数据后会发现标准总会有一些模糊地带) 。

4、根据数据特征、规模等选择合适的方法,并评测方法的优劣

工程中的方法并不是单一的方法,想用一个方法或者模型来解决各类数据源上的问题是不可能的。想要做出好的效果一定是采用分而治之的思想,比如,能用规则精准过的就不需要用分类器。

当应用在实际产品时,最好能结合产品的垂直特点,充分利用垂直行业的特性,比如在金融行业、汽车行业,它们一定有自己的行话,这些行话具有非常明显的规则或者特征。

二、情感分析方法及工具

情感分析对象的粒度最小是词汇,但是表达一个情感的最基本的单位则是句子,词汇虽然能描述情感的基本信息,但是单一的词汇缺少对象,缺少关联程度,并且不同的词汇组合在一起所得到的情感程度不同甚至情感倾向都相反。所以以句子为最基本的情感分析粒度是较为合理的。篇章或者段落的情感也可以通过句子的情感来计算。

现阶段关于情感分析方法主要有两类:

(一)、基于词典的方法:

基于词典的方法主要通过制定一系列的情感词典和规则,对文本进行拆句、分析及匹配词典(一般有词性分析,句法依存分析),计算情感值,最后通过情感值来作为文本的情感倾向判断的依据。

做法:

基于词典的情感分析大致步骤如下:

  • 对大于句子力度的文本进行拆解句子操作,以句子为最小分析单元;
  • 分析句子中出现的词语并按照情感词典匹配;
  • 处理否定逻辑及转折逻辑;
  • 计算整句情感词得分(根据词语不同,极性不同,程度不同等因素进行加权求和);
  • 根据情感得分输出句子情感倾向性。

如果是对篇章或者段落级别的情感分析任务,按照具体的情况,可以以对每个句子进行单一情感分析并融合的形式进行,也可以先抽取情感主题句后进行句子情感分析,得到最终情感分析结果。

参考及工具:

1. 常见英文情感词库:GI(The General Inquirer)、sentiWordNet等;

2. 常见中文情感词库:知网、台湾大学的情感极性词典;

3. 几种情感词典构建方法:基于bootstrapping方法的Predicting the semantic orientation of adjectives及Determining the sentiment of opinions两种最为经典的词典构建方法。

(二)、 基于机器学习的方法:

情感词典准确率高,但存在召回率比较低的情况。对于不同的领域,构建情感词典的难度是不一样的,精准构建成本较高。另外一种解决情感分析的思路是使用机器学习的方法,将情感分析作为一个有监督的分类问题。对于情感极性的判断,将目标情感分为三类:正、中、负。对训练文本进行人工标注,然后进行有监督的机器学习过程,并对测试数据用模型来预测结果。

处理过程:

基于机器学习的情感分析思路是将情感分析作为一个分类问题来处理,具体的流程如下:

1、 文本预处理

文本的预处理过程是使用机器学习作用于文本分类的基础操作。由于文本是非结构化数据及其特殊性,计算机并不能直接理解,所以需要一系列的预处理操作后,转换为计算机可以处理的结构化数据。在实际分析中,文本更为复杂,书写规范也更为随意,且很有可能掺杂部分噪声数据。整体上来说,文本预处理模块包括去噪、特征提取、文本结构化表示等。

特征抽取:中文最小语素是字,但是往往词语才具有更明确的语义信息,但是随着分词,可能出现词语关系丢失的情况。n-元文法正好解决了这个问题,它也是传统机器学习分类任务中最常用的方法。

文本向量化:对抽取出来的特征,向量化是一个很重要的过程,是实现由人可以理解的文本转换为计算机可以处理数据的重要一步。这一步最常用到的就是词袋模型(bag-of-words )以及最近新出的连续分布词向量模型(word Embedding)。词袋模型长度为整个词表的长度,词语对应维度置为词频,文档的表示往往比较稀疏且维度较高。Embedding的表示方式,能够有效的解决数据稀疏且降维到固定维度,更好的表示语义信息。对于文档表示,词袋模型可以直接叠加,而Embedding的方法可以使用深度学习的方法,通过pooling得到最终表示。

特征选择:在机器学习分类算法的使用过程中,特征好坏直接影响机器的准确率及召回率。选择有利于分类的特征,可以有效的减少训练开支及防止模型过拟合,尤其是数据量较大的情况下,这一部分工作的重要性更加明显。其选择方法为,将所有的训练语料输入,通过一定的方法,选择最有效的特征,主要的方法有卡方,信息熵,dp深层感知器等等。

目前也有一些方法,从比句子粒度更细的层次去识别情感,如基于方面的情感分析(Aspect based Sentiment Analysis),他们从产品的评价属性等更细粒度的方面对评价主体进行情感倾向性分析。

2、分类算法选择

文本转换为机器可处理的结构后,接下来便要选择进行机器学习的分类算法。目前,使用率比较高的是深度学习(CNN,RNN)和支持向量机(SVM)。深度学习的方法,运算量大,准确率有一定的提高,所以都在做这方面的尝试。而支持向量机则是比较传统的方法,其准确率及数据处理能力也比较出色,很多人都在用它来做分类任务。

参考及工具:

1. svm分类 libsvm

2. python 机器学习工具scikit-learn

3. 深度学习框架:Tensorflow、Theano

by 崔维福