1. 程式人生 > 實用技巧 >基於情感詞典的文字情感分析 (snownlp)

基於情感詞典的文字情感分析 (snownlp)

目前情感分析在中文自然語言處理中比較火熱,很多場景下,我們都需要用到情感分析。比如,做金融產品量化交易,需要根據爬取的輿論資料來分析政策和輿論對股市或者基金期貨的態度;電商交易,根據買家的評論資料,來分析商品的預售率等等。

下面我們通過以下幾點來介紹中文自然語言處理情感分析:

  1. 中文情感分析方法簡介;
  2. SnowNLP 快速進行評論資料情感分析;
  3. 基於標註好的情感詞典來計算情感值;
  4. pytreebank 繪製情感樹;
  5. 股吧資料情感分類。

中文情感分析方法簡介

情感傾向可認為是主體對某一客體主觀存在的內心喜惡,內在評價的一種傾向。它由兩個方面來衡量:一個情感傾向方向,一個是情感傾向度。

目前,情感傾向分析的方法主要分為兩類:一種是基於情感詞典的方法;一種是基於機器學習的方法,如基於大規模語料庫的機器學習。前者需要用到標註好的情感詞典;後者則需要大量的人工標註的語料作為訓練集,通過提取文字特徵,構建分類器來實現情感的分類。

文字情感分析的分析粒度可以是詞語、句子、段落或篇章。

段落篇章級情感分析主要是針對某個主題或事件進行情感傾向判斷,一般需要構建對應事件的情感詞典,如電影評論的分析,需要構建電影行業自己的情感詞典,這樣效果會比通用情感詞典更好;也可以通過人工標註大量電影評論來構建分類器。句子級的情感分析大多通過計算句子裡包含的所有情感詞的值來得到。

篇章級的情感分析,也可以通過聚合篇章中所有的句子的情感傾向來計算得出。因此,針對句子級的情感傾向分析,既能解決短文字的情感分析,同時也是篇章級文字情感分析的基礎。

中文情感分析的一些難點,比如句子是由詞語根據一定的語言規則構成的,應該把句子中詞語的依存關係納入到句子情感的計算過程中去,不同的依存關係,進行情感傾向計算是不一樣的。文件的情感,根據句子對文件的重要程度賦予不同權重,調整其對文件情感的貢獻程度等。

SnowNLP 快速進行評論資料情感分析

如果有人問,有沒有比較快速簡單的方法能判斷一句話的情感傾向,那麼 SnowNLP 庫就是答案。

SnowNLP 主要可以進行中文分詞、詞性標註、情感分析、文字分類、轉換拼音、繁體轉簡體、提取文字關鍵詞、提取摘要、分割句子、文字相似等。

需要注意的是,用 SnowNLP 進行情感分析,官網指出進行電商評論的準確率較高,其實是因為它的語料庫主要是電商評論資料,但是可以自己構建相關領域語料庫,替換單一的電商評論語料,準確率也挺不錯的。

1. SnowNLP 安裝。

(1) 使用 pip 安裝:

pip install snownlp==0.11.1

(2)使用 Github 原始碼安裝。

首先,下載 SnowNLP 的 Github 原始碼並解壓,在解壓目錄,通過下面命令安裝:

python  setup.py install

以上方式,二選一安裝完成之後,就可以引入 SnowNLP 庫使用了。

from snownlp import SnowNLP

(1) 測試一條京東的好評資料:

SnowNLP(u'本本已收到,體驗還是很好,功能方面我不瞭解,只看外觀還是很不錯很薄,很輕,也有質感。').sentiments

得到的情感值很高,說明買家對商品比較認可,情感值為:

0.999950702449061 (2)測試一條京東的中評資料:

SnowNLP(u'螢幕解析度一般,送了個極醜的滑鼠。').sentiments 得到的情感值一般,說明買家對商品看法一般,甚至不喜歡,情感值為:

0.03251402883400323 (3)測試一條京東的差評資料:

SnowNLP(u'很差的一次購物體驗,細節做得極差了,還有發熱有點嚴重啊,散熱不行,用起來就是燙得厲害,很垃圾!!!').sentiments 得到的情感值一般,說明買家對商品不認可,存在退貨嫌疑,情感值為:

0.0036849517156107847 以上就完成了簡單快速的情感值計算,對評論資料是不是很好用呀!!!

使用 SnowNLP 來計算情感值,官方推薦的是電商評論資料計算準確度比較高,難道非評論資料就不能使用 SnowNLP 來計算情感值了嗎?當然不是,雖然 SnowNLP 預設提供的模型是用評論資料訓練的,但是它還支援我們根據現有資料訓練自己的模型。

首先我們來看看自定義訓練模型的原始碼 Sentiment 類,程式碼定義如下:

class Sentiment(object):

    def __init__(self):
        self.classifier = Bayes()

    def save(self, fname, iszip=True):
        self.classifier.save(fname, iszip)

    def load(self, fname=data_path, iszip=True):
        self.classifier.load(fname, iszip)

    def handle(self, doc):
        words = seg.seg(doc)
        words = normal.filter_stop(words)
        return words

    def train(self, neg_docs, pos_docs):
        data = []
        for sent in neg_docs:
            data.append([self.handle(sent), 'neg'])
        for sent in pos_docs:
            data.append([self.handle(sent), 'pos'])
        self.classifier.train(data)

    def classify(self, sent):
        ret, prob = self.classifier.classify(self.handle(sent))
        if ret == 'pos':
            return prob
        return 1-prob

通過原始碼,我們可以看到,可以使用 train方法訓練資料,並使用 save 方法和 load 方法儲存與載入模型。下面訓練自己的模型,訓練集 pos.txt 和 neg.txt 分別表示積極和消極情感語句,兩個 TXT 文字中每行表示一句語料。

下面程式碼進行自定義模型訓練和儲存:

from snownlp import sentiment
sentiment.train('neg.txt', 'pos.txt')
sentiment.save('sentiment.marshal')

基於標註好的情感詞典來計算情感值

這裡我們使用一個行業標準的情感詞典——玻森情感詞典,來自定義計算一句話、或者一段文字的情感值。

整個過程如下:

載入玻森情感詞典; jieba 分詞; 獲取句子得分。 首先引入包:

import pandas as pd
import jieba

接下來載入情感詞典:

df = pd.read_table("bosonnlp//BosonNLP_sentiment_score.txt",sep= " ",names=['key','score'])

檢視一下情感詞典前5行:

將詞 key 和對應得分 score 轉成2個 list 列表,目的是找到詞 key 的時候,能對應獲取到 score 值:

key = df['key'].values.tolist()
score = df['score'].values.tolist()

定義分詞和統計得分函式:

def getscore(line):
    segs = jieba.lcut(line)  #分詞
    score_list  = [score[key.index(x)] for x in segs if(x in key)]
    return  sum(score_list)  #計算得分

最後來進行結果測試:

line = "今天天氣很好,我很開心"
print(round(getscore(line),2))

line = "今天下雨,心情也受到影響。"
print(round(getscore(line),2))

獲得的情感得分保留2位小數:

5.26
-0.96

pytreebank 繪製情感樹

1. 安裝 pytreebank。

在 Github 上下載 pytreebank 原始碼,解壓之後,進入解壓目錄命令列,執行命令:

python setup.py install

最後通過引入命令,判斷是否安裝成功:

import pytreebank

提示,如果在 Windows 下安裝之後,報錯誤:

UnicodeDecodeError: 'gbk' codec can't decode byte 0x92 in position 24783: illegal multibyte sequence

這是由於編碼問題引起的,可以在安裝目錄下報錯的檔案中報錯的程式碼地方加個 encoding='utf-8' 編碼:

import_tag( "script", contents=format_replacements(open(scriptname,encoding='utf-8').read(), replacements), type="text/javascript" )
  1. 繪製情感樹。

首先引入 pytreebank 包:

import pytreebank

然後,載入用來視覺化的 JavaScript 和 CSS 指令碼:

pytreebank.LabeledTree.inject_visualization_javascript()

繪製情感樹,把句子首先進行組合再繪製圖形:

line = '(4 (0 你) (3 (2 是) (3 (3 (3 誰) (2 的)) (2 誰))))'
pytreebank.create_tree_from_string(line).display()

得到的情感樹如下:

股吧資料情感分類

歷經89天的煎熬之後,7月15日中興終於盼來了解禁,在此首先恭喜中興,解禁了,希望再踏征程。

但在7月15日之前,隨著中美貿易戰不斷升級,中興股價又上演了一場“跌跌不休”的慘狀,我以中美貿易戰背景下中興通訊在股吧解禁前一段時間的評論資料,來進行情感資料人工打標籤和分類。其中,把消極 、中性 、積極分別用0、1、2來表示。

整個文字分類流程主要包括以下6個步驟:

  • 中文語料;
  • 分詞;
  • 複雜規則;
  • 特徵向量;
  • 演算法建模;
  • 情感分析。

本次分類演算法採用 CNN,首先引入需要的包:

import pandas as pd
import numpy as np
import jieba
import random
import keras
from keras.preprocessing import sequence
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation
from keras.layers import Embedding
from keras.layers import Conv1D, GlobalMaxPooling1D
from keras.datasets import imdb
from keras.models import model_from_json
from keras.utils import np_utils
import matplotlib.pyplot as plt

繼續引入停用詞和語料檔案:

dir = "D://ProgramData//PythonWorkSpace//chat//chat8//"
stopwords=pd.read_csv(dir +"stopwords.txt",index_col=False,quoting=3,sep="\t",names=['stopword'], encoding='utf-8')
stopwords=stopwords['stopword'].values
df_data1 = pd.read_csv(dir+"data1.csv",encoding='utf-8')
df_data1.head()

下圖展示資料的前5行:

接著進行資料預處理,把消極、中性、積極分別為0、1、2的預料分別拿出來:

#把內容有缺失值的刪除
df_data1.dropna(inplace=True)
#抽取文字資料和標籤
data_1 = df_data1.loc[:,['content','label']]
#把消極  中性  積極分別為0、1、2的預料分別拿出來
data_label_0 = data_1.loc[data_1['label'] ==0,:]
data_label_1 = data_1.loc[data_1['label'] ==1,:]
data_label_2 = data_1.loc[data_1['label'] ==2,:]

接下來,定義中文分詞函式:

#定義分詞函式
def preprocess_text(content_lines, sentences, category):
    for line in content_lines:
        try:
            segs=jieba.lcut(line)
            segs = filter(lambda x:len(x)>1, segs)
            segs = [v for v in segs if not str(v).isdigit()]#去數字
            segs = list(filter(lambda x:x.strip(), segs)) #去左右空格
            segs = filter(lambda x:x not in stopwords, segs)
            temp = " ".join(segs)
            if(len(temp)>1):
                sentences.append((temp, category))
        except Exception:
            print(line)
            continue

生成訓練的分詞資料,並進行打散,使其分佈均勻:

#獲取資料
data_label_0_content = data_label_0['content'].values.tolist()
data_label_1_content = data_label_1['content'].values.tolist()
data_label_2_content = data_label_2['content'].values.tolist()
#生成訓練資料
sentences = []
preprocess_text(data_label_0_content, sentences, 0)
preprocess_text(data_label_1_content, sentences, 1)
preprocess_text(data_label_2_content, sentences,2)
#我們打亂一下順序,生成更可靠的訓練集
random.shuffle(sentences)

對資料集進行切分,按照訓練集合測試集7:3的比例:

#所以把原資料集分成訓練集的測試集,咱們用sklearn自帶的分割函式。
from sklearn.model_selection import train_test_split
x, y = zip(*sentences)
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3,random_state=1234)#所以把原資料集分成訓練集的測試集,咱們用sklearn自帶的分割函式。
from sklearn.model_selection import train_test_split
x, y = zip(*sentences)
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3,random_state=1234)

然後,對特徵構造詞向量:

#抽取特徵,我們對文字抽取詞袋模型特徵
from sklearn.feature_extraction.text import CountVectorizer
vec = CountVectorizer(
    analyzer='word', #tokenise by character ngrams
    max_features=4000,  #keep the most common 1000 ngrams
)
vec.fit(x_train)

定義模型引數:

# 設定引數
max_features = 5001
maxlen = 100
batch_size = 32
embedding_dims = 50
filters = 250
kernel_size = 3
hidden_dims = 250
epochs = 10
nclasses = 3

輸入特徵轉成 Array 和標籤處理,列印訓練集和測試集的 shape:

x_train = vec.transform(x_train)
x_test = vec.transform(x_test)
x_train = x_train.toarray()
x_test = x_test.toarray()
y_train = np_utils.to_categorical(y_train,nclasses)
y_test = np_utils.to_categorical(y_test,nclasses)
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)

定義一個繪製 Loss 曲線的類:

class LossHistory(keras.callbacks.Callback):
    def on_train_begin(self, logs={}):
        self.losses = {'batch':[], 'epoch':[]}
        self.accuracy = {'batch':[], 'epoch':[]}
        self.val_loss = {'batch':[], 'epoch':[]}
        self.val_acc = {'batch':[], 'epoch':[]}

    def on_batch_end(self, batch, logs={}):
        self.losses['batch'].append(logs.get('loss'))
        self.accuracy['batch'].append(logs.get('acc'))
        self.val_loss['batch'].append(logs.get('val_loss'))
        self.val_acc['batch'].append(logs.get('val_acc'))

    def on_epoch_end(self, batch, logs={}):
        self.losses['epoch'].append(logs.get('loss'))
        self.accuracy['epoch'].append(logs.get('acc'))
        self.val_loss['epoch'].append(logs.get('val_loss'))
        self.val_acc['epoch'].append(logs.get('val_acc'))

    def loss_plot(self, loss_type):
        iters = range(len(self.losses[loss_type]))
        plt.figure()
        # acc
        plt.plot(iters, self.accuracy[loss_type], 'r', label='train acc')
        # loss
        plt.plot(iters, self.losses[loss_type], 'g', label='train loss')
        if loss_type == 'epoch':
            # val_acc
            plt.plot(iters, self.val_acc[loss_type], 'b', label='val acc')
            # val_loss
            plt.plot(iters, self.val_loss[loss_type], 'k', label='val loss')
        plt.grid(True)
        plt.xlabel(loss_type)
        plt.ylabel('acc-loss')
        plt.legend(loc="upper right")
        plt.show()

然後,初始化上面類的物件,並作為模型的回撥函式輸入,訓練模型:

history = LossHistory()
print('Build model...')
model = Sequential()

model.add(Embedding(max_features,
                        embedding_dims,
                        input_length=maxlen))
model.add(Dropout(0.5))
model.add(Conv1D(filters,
                     kernel_size,
                     padding='valid',
                     activation='relu',
                     strides=1))
model.add(GlobalMaxPooling1D())
model.add(Dense(hidden_dims))
model.add(Dropout(0.5))
model.add(Activation('relu'))
model.add(Dense(nclasses))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
model.fit(x_train, y_train,
              batch_size=batch_size,
              epochs=epochs,
              validation_data=(x_test, y_test),callbacks=[history])

得到的模型迭代次數為10輪的訓練過程:

最後繪製 Loss 影象:

關於本次分類,這裡重點討論的一個知識點就是資料分佈不均勻的情況,我們都知道,本次貿易戰中興公司受影響很大,導致整個股票價格處於下跌趨勢,所以整個輿論上,大多數評論都是消極的態度,導致資料分佈極不均勻。

那資料分佈不均勻一般怎麼處理呢?從以下幾個方面考慮:

  • 資料取樣,包括上取樣、下采樣和綜合取樣;
  • 改變分類演算法,在傳統分類演算法的基礎上對不同類別採取不同的加權方式,使得模型更看重少數類;
  • 採用合理的效能評價指標;
  • 代價敏感。

總結,本文通過第三方、基於詞典等方式計算中文文字情感值,以及通過情感樹來進行視覺化,然而這些內容只是情感分析的入門知識,情感分析還涉及句法依存等,最後通過一個 CNN 分類模型,提供一種有監督的情感分類思路。