情感分析簡(jiǎn)介
??文本情感分析(Sentiment Analysis)是自然語言處理(NLP)方法中常見的應(yīng)用,也是一個(gè)有趣的基本任務(wù),尤其是以提煉文本情緒內(nèi)容為目的的分類。它是對(duì)帶有情感色彩的主觀性文本進(jìn)行分析、處理、歸納和推理的過程。
??本文將介紹情感分析中的情感極性(傾向)分析。所謂情感極性分析,指的是對(duì)文本進(jìn)行褒義、貶義、中性的判斷。在大多應(yīng)用場(chǎng)景下,只分為兩類。例如對(duì)于“喜愛”和“厭惡”這兩個(gè)詞,就屬于不同的情感傾向。
??本文將詳細(xì)介紹如何使用深度學(xué)習(xí)模型中的LSTM模型來實(shí)現(xiàn)文本的情感分析。
文本介紹及語料分析
??我們以某電商網(wǎng)站中某個(gè)商品的評(píng)論作為語料(corpus.csv),該數(shù)據(jù)集的下載網(wǎng)址為:https://github.com/renjunxiang/Text-Classification/blob/master/TextClassification/data/data_single.csv ,該數(shù)據(jù)集一共有4310條評(píng)論數(shù)據(jù),文本的情感分為兩類:“正面”和“反面”,該數(shù)據(jù)集的前幾行如下:
evaluation,label
用了一段時(shí)間,感覺還不錯(cuò),可以,正面
電視非常好,已經(jīng)是家里的第二臺(tái)了。第一天下單,第二天就到本地了,可是物流的人說車壞了,一直催,客服也幫著催,到第三天下午5點(diǎn)才送過來。父母年紀(jì)大了,買個(gè)大電視畫面清晰,趁著耳朵還好使,享受幾年。,正面
電視比想象中的大好多,畫面也很清晰,系統(tǒng)很智能,更多功能還在摸索中,正面
不錯(cuò),正面
用了這么多天了,感覺還不錯(cuò)。夏普的牌子還是比較可靠。希望以后比較耐用,現(xiàn)在是考量質(zhì)量的時(shí)候。,正面
物流速度很快,非常棒,今天就看了電視,非常清晰,非常流暢,一次非常完美的購物體驗(yàn),正面
非常好,客服還特意打電話做回訪,正面
物流小哥不錯(cuò),辛苦了,東西還沒用,正面
送貨速度快,質(zhì)量有保障,活動(dòng)價(jià)格挺好的。希望用的久,不出問題。,正面
??接著我們需要對(duì)語料做一個(gè)簡(jiǎn)單的分析:
- 數(shù)據(jù)集中的情感分布;
- 數(shù)據(jù)集中的評(píng)論句子長(zhǎng)度分布。
??使用以下Python腳本,我們可以統(tǒng)計(jì)出數(shù)據(jù)集中的情感分布以及評(píng)論句子長(zhǎng)度分布。
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from itertools import accumulate
# 設(shè)置matplotlib繪圖時(shí)的字體
my_font = font_manager.FontProperties(fname="/Library/Fonts/Songti.ttc")
# 統(tǒng)計(jì)句子長(zhǎng)度及長(zhǎng)度出現(xiàn)的頻數(shù)
df = pd.read_csv('./corpus.csv')
print(df.groupby('label')['label'].count())
df['length'] = df['evaluation'].apply(lambda x: len(x))
len_df = df.groupby('length').count()
sent_length = len_df.index.tolist()
sent_freq = len_df['evaluation'].tolist()
# 繪制句子長(zhǎng)度及出現(xiàn)頻數(shù)統(tǒng)計(jì)圖
plt.bar(sent_length, sent_freq)
plt.title("句子長(zhǎng)度及出現(xiàn)頻數(shù)統(tǒng)計(jì)圖", fontproperties=my_font)
plt.xlabel("句子長(zhǎng)度", fontproperties=my_font)
plt.ylabel("句子長(zhǎng)度出現(xiàn)的頻數(shù)", fontproperties=my_font)
plt.savefig("./句子長(zhǎng)度及出現(xiàn)頻數(shù)統(tǒng)計(jì)圖.png")
plt.close()
# 繪制句子長(zhǎng)度累積分布函數(shù)(CDF)
sent_pentage_list = [(count/sum(sent_freq)) for count in accumulate(sent_freq)]
# 繪制CDF
plt.plot(sent_length, sent_pentage_list)
# 尋找分位點(diǎn)為quantile的句子長(zhǎng)度
quantile = 0.91
#print(list(sent_pentage_list))
for length, per in zip(sent_length, sent_pentage_list):
if round(per, 2) == quantile:
index = length
break
print("\n分位點(diǎn)為%s的句子長(zhǎng)度:%d." % (quantile, index))
# 繪制句子長(zhǎng)度累積分布函數(shù)圖
plt.plot(sent_length, sent_pentage_list)
plt.hlines(quantile, 0, index, colors="c", linestyles="dashed")
plt.vlines(index, 0, quantile, colors="c", linestyles="dashed")
plt.text(0, quantile, str(quantile))
plt.text(index, 0, str(index))
plt.title("句子長(zhǎng)度累積分布函數(shù)圖", fontproperties=my_font)
plt.xlabel("句子長(zhǎng)度", fontproperties=my_font)
plt.ylabel("句子長(zhǎng)度累積頻率", fontproperties=my_font)
plt.savefig("./句子長(zhǎng)度累積分布函數(shù)圖.png")
plt.close()
輸出的結(jié)果如下:
label
正面 1908
負(fù)面 2375
Name: label, dtype: int64
分位點(diǎn)為0.91的句子長(zhǎng)度:183.
可以看到,正反面兩類情感的比例差不多。句子長(zhǎng)度及出現(xiàn)頻數(shù)統(tǒng)計(jì)圖如下:

句子長(zhǎng)度累積分布函數(shù)圖如下:

可以看到,大多數(shù)樣本的句子長(zhǎng)度集中在1-200之間,句子長(zhǎng)度累計(jì)頻率取0.91分位點(diǎn),則長(zhǎng)度為183左右。
使用LSTM模型
??接著我們使用深度學(xué)習(xí)中的LSTM模型來對(duì)上述數(shù)據(jù)集做情感分析,筆者實(shí)現(xiàn)的模型框架如下:

完整的Python代碼如下:
# -*- coding: utf-8 -*-
import pickle
import numpy as np
import pandas as pd
from keras.utils import np_utils, plot_model
from keras.models import Sequential
from keras.preprocessing.sequence import pad_sequences
from keras.layers import LSTM, Dense, Embedding, Dropout
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 導(dǎo)入數(shù)據(jù)
# 文件的數(shù)據(jù)中,特征為evaluation, 類別為label.
def load_data(filepath, input_shape=20):
df = pd.read_csv(filepath)
# 標(biāo)簽及詞匯表
labels, vocabulary = list(df['label'].unique()), list(df['evaluation'].unique())
# 構(gòu)造字符級(jí)別的特征
string = ''
for word in vocabulary:
string += word
vocabulary = set(string)
# 字典列表
word_dictionary = {word: i+1 for i, word in enumerate(vocabulary)}
with open('word_dict.pk', 'wb') as f:
pickle.dump(word_dictionary, f)
inverse_word_dictionary = {i+1: word for i, word in enumerate(vocabulary)}
label_dictionary = {label: i for i, label in enumerate(labels)}
with open('label_dict.pk', 'wb') as f:
pickle.dump(label_dictionary, f)
output_dictionary = {i: labels for i, labels in enumerate(labels)}
vocab_size = len(word_dictionary.keys()) # 詞匯表大小
label_size = len(label_dictionary.keys()) # 標(biāo)簽類別數(shù)量
# 序列填充,按input_shape填充,長(zhǎng)度不足的按0補(bǔ)充
x = [[word_dictionary[word] for word in sent] for sent in df['evaluation']]
x = pad_sequences(maxlen=input_shape, sequences=x, padding='post', value=0)
y = [[label_dictionary[sent]] for sent in df['label']]
y = [np_utils.to_categorical(label, num_classes=label_size) for label in y]
y = np.array([list(_[0]) for _ in y])
return x, y, output_dictionary, vocab_size, label_size, inverse_word_dictionary
# 創(chuàng)建深度學(xué)習(xí)模型, Embedding + LSTM + Softmax.
def create_LSTM(n_units, input_shape, output_dim, filepath):
x, y, output_dictionary, vocab_size, label_size, inverse_word_dictionary = load_data(filepath)
model = Sequential()
model.add(Embedding(input_dim=vocab_size + 1, output_dim=output_dim,
input_length=input_shape, mask_zero=True))
model.add(LSTM(n_units, input_shape=(x.shape[0], x.shape[1])))
model.add(Dropout(0.2))
model.add(Dense(label_size, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
plot_model(model, to_file='./model_lstm.png', show_shapes=True)
model.summary()
return model
# 模型訓(xùn)練
def model_train(input_shape, filepath, model_save_path):
# 將數(shù)據(jù)集分為訓(xùn)練集和測(cè)試集,占比為9:1
# input_shape = 100
x, y, output_dictionary, vocab_size, label_size, inverse_word_dictionary = load_data(filepath, input_shape)
train_x, test_x, train_y, test_y = train_test_split(x, y, test_size = 0.1, random_state = 42)
# 模型輸入?yún)?shù),需要自己根據(jù)需要調(diào)整
n_units = 100
batch_size = 32
epochs = 5
output_dim = 20
# 模型訓(xùn)練
lstm_model = create_LSTM(n_units, input_shape, output_dim, filepath)
lstm_model.fit(train_x, train_y, epochs=epochs, batch_size=batch_size, verbose=1)
# 模型保存
lstm_model.save(model_save_path)
N = test_x.shape[0] # 測(cè)試的條數(shù)
predict = []
label = []
for start, end in zip(range(0, N, 1), range(1, N+1, 1)):
sentence = [inverse_word_dictionary[i] for i in test_x[start] if i != 0]
y_predict = lstm_model.predict(test_x[start:end])
label_predict = output_dictionary[np.argmax(y_predict[0])]
label_true = output_dictionary[np.argmax(test_y[start:end])]
print(''.join(sentence), label_true, label_predict) # 輸出預(yù)測(cè)結(jié)果
predict.append(label_predict)
label.append(label_true)
acc = accuracy_score(predict, label) # 預(yù)測(cè)準(zhǔn)確率
print('模型在測(cè)試集上的準(zhǔn)確率為: %s.' % acc)
if __name__ == '__main__':
filepath = './corpus.csv'
input_shape = 180
model_save_path = './corpus_model.h5'
model_train(input_shape, filepath, model_save_path)
對(duì)上述模型,共訓(xùn)練5次,訓(xùn)練集和測(cè)試集比例為9:1,輸出的結(jié)果為:
......
Epoch 5/5
......
3424/3854 [=========================>....] - ETA: 2s - loss: 0.1280 - acc: 0.9565
3456/3854 [=========================>....] - ETA: 1s - loss: 0.1274 - acc: 0.9569
3488/3854 [==========================>...] - ETA: 1s - loss: 0.1274 - acc: 0.9570
3520/3854 [==========================>...] - ETA: 1s - loss: 0.1287 - acc: 0.9568
3552/3854 [==========================>...] - ETA: 1s - loss: 0.1290 - acc: 0.9564
3584/3854 [==========================>...] - ETA: 1s - loss: 0.1284 - acc: 0.9568
3616/3854 [===========================>..] - ETA: 1s - loss: 0.1284 - acc: 0.9569
3648/3854 [===========================>..] - ETA: 0s - loss: 0.1278 - acc: 0.9572
3680/3854 [===========================>..] - ETA: 0s - loss: 0.1271 - acc: 0.9576
3712/3854 [===========================>..] - ETA: 0s - loss: 0.1268 - acc: 0.9580
3744/3854 [============================>.] - ETA: 0s - loss: 0.1279 - acc: 0.9575
3776/3854 [============================>.] - ETA: 0s - loss: 0.1272 - acc: 0.9579
3808/3854 [============================>.] - ETA: 0s - loss: 0.1279 - acc: 0.9580
3840/3854 [============================>.] - ETA: 0s - loss: 0.1281 - acc: 0.9581
3854/3854 [==============================] - 18s 5ms/step - loss: 0.1298 - acc: 0.9577
......
給父母買的,特意用了一段時(shí)間再來評(píng)價(jià),電視非常好,沒有壞點(diǎn)和損壞,界面也很簡(jiǎn)潔,便于操作,稍微不足就是開機(jī)會(huì)比普通電視慢一些,這應(yīng)該是智能電視的通病吧,如果可以希望微鯨大大可以更新系統(tǒng)優(yōu)化下開機(jī)時(shí)間~電視真的很棒,性價(jià)比爆棚,值得大家考慮購買。 客服很細(xì)心,快遞小哥很耐心的等我通電驗(yàn)貨,態(tài)度非常好。 負(fù)面 正面
長(zhǎng)須鯨和海獅回答都很及時(shí),雖然物流不夠快但是服務(wù)不錯(cuò)電視不錯(cuò),對(duì)比了樂視小米和微鯨論性價(jià)比還是微鯨好點(diǎn) 負(fù)面 負(fù)面
所以看不到4k效果,但是應(yīng)該可以。 自帶音響,中規(guī)中矩吧,好像沒有別人說的好。而且,到現(xiàn)在沒連接上我的漫步者,這個(gè)非常不滿意,因?yàn)榭吹骄W(wǎng)上說好像普通3.5mm的連不上或者連上了聲音小。希望廠家接下來開發(fā)的電視有改進(jìn)。不知道我要不要換個(gè)音響。其他的用用再說。 放在地上的是跟我混了兩年的tcl,天氣受潮,修了一次,下崗了。 最后,我也覺得底座不算太穩(wěn),湊合著用。 負(fù)面 負(fù)面
電視機(jī)一般,低端機(jī)不要求那么高咯。 負(fù)面 負(fù)面
很好,兩點(diǎn)下單上午就到了,服務(wù)很好。 正面 正面
幫朋友買的,好好好好好好好好 正面 正面
......
模型在測(cè)試集上的準(zhǔn)確率為: 0.9020979020979021.
可以看到,該模型在訓(xùn)練集上的準(zhǔn)確率為95%以上,在測(cè)試集上的準(zhǔn)確率為90%以上,效果還是相當(dāng)不錯(cuò)的。
模型預(yù)測(cè)
??接著,我們利用剛剛訓(xùn)練好的模型,對(duì)新的數(shù)據(jù)進(jìn)行測(cè)試。筆者隨機(jī)改造上述樣本的評(píng)論,然后預(yù)測(cè)其情感傾向。情感預(yù)測(cè)的Python代碼如下:
# -*- coding: utf-8 -*-
# Import the necessary modules
import pickle
import numpy as np
from keras.models import load_model
from keras.preprocessing.sequence import pad_sequences
# 導(dǎo)入字典
with open('word_dict.pk', 'rb') as f:
word_dictionary = pickle.load(f)
with open('label_dict.pk', 'rb') as f:
output_dictionary = pickle.load(f)
try:
# 數(shù)據(jù)預(yù)處理
input_shape = 180
sent = "電視剛安裝好,說實(shí)話,畫質(zhì)不怎么樣,很差!"
x = [[word_dictionary[word] for word in sent]]
x = pad_sequences(maxlen=input_shape, sequences=x, padding='post', value=0)
# 載入模型
model_save_path = './sentiment_analysis.h5'
lstm_model = load_model(model_save_path)
# 模型預(yù)測(cè)
y_predict = lstm_model.predict(x)
label_dict = {v:k for k,v in output_dictionary.items()}
print('輸入語句: %s' % sent)
print('情感預(yù)測(cè)結(jié)果: %s' % label_dict[np.argmax(y_predict)])
except KeyError as err:
print("您輸入的句子有漢字不在詞匯表中,請(qǐng)重新輸入!")
print("不在詞匯表中的單詞為:%s." % err)
輸出結(jié)果如下:
輸入語句: 電視剛安裝好,說實(shí)話,畫質(zhì)不怎么樣,很差!
情感預(yù)測(cè)結(jié)果: 負(fù)面
??讓我們?cè)賴L試著測(cè)試一些其他的評(píng)論:
輸入語句: 物超所值,真心不錯(cuò)
情感預(yù)測(cè)結(jié)果: 正面
輸入語句: 很大很好,方便安裝!
情感預(yù)測(cè)結(jié)果: 正面
輸入語句: 卡,慢,死機(jī),閃退。
情感預(yù)測(cè)結(jié)果: 負(fù)面
輸入語句: 這種貨色就這樣吧,別期待怎樣。
情感預(yù)測(cè)結(jié)果: 負(fù)面
輸入語句: 啥服務(wù)態(tài)度碼,出了事情一個(gè)推一個(gè),送貨安裝還收我50
情感預(yù)測(cè)結(jié)果: 負(fù)面
輸入語句: 京東服務(wù)很好!但我買的這款電視兩天后就出現(xiàn)這樣的問題,很后悔買了這樣的電視
情感預(yù)測(cè)結(jié)果: 負(fù)面
輸入語句: 產(chǎn)品質(zhì)量不錯(cuò),就是這位客服的態(tài)度十分惡劣,對(duì)相關(guān)服務(wù)不予解釋說明,缺乏耐心,
情感預(yù)測(cè)結(jié)果: 負(fù)面
輸入語句: 很滿意,電視非常好。護(hù)眼模式,很好,也很清晰。
情感預(yù)測(cè)結(jié)果: 負(fù)面
總結(jié)
??當(dāng)然,該模型并不是對(duì)一切該商品的評(píng)論都會(huì)有好的效果,還是應(yīng)該針對(duì)特定的語料去訓(xùn)練,去預(yù)測(cè)。
??本文主要介紹了LSTM模型在文本情感分析方面的應(yīng)用,該項(xiàng)目已上傳Github,地址為: https://github.com/percent4/Sentiment_Analysis 。
注意:不妨了解下筆者的微信公眾號(hào): Python爬蟲與算法(微信號(hào)為:easy_web_scrape), 歡迎大家關(guān)注~
參考文獻(xiàn)
- Python機(jī)器學(xué)習(xí) -- NLP情感分析:https://blog.csdn.net/qq_38328378/article/details/81198322
- 數(shù)據(jù)集來源:https://github.com/renjunxiang/Text-Classification/blob/master/TextClassification/data/data_single.csv