評論上的情感分析:主題與情感詞抽取
問題描述
- 針對評論網站上的用戶評論進行細粒度的情感分析,區別於傳統的粗粒度的情感分類(判斷一句話的表達情感的正/負性),評論者在一句話中往往會提到多個角度,並在每個角度都抱有不同的觀點內容與正/負極性
- 舉例:金拱門快餐的食品質量一般,但是服務很周到
- 抽取結果:食品質量 → 一般;服務 → 周到,這裡 「食品質量」 與 「服務」 是兩個不同的角度(aspect,也叫opinion target),前一個角度對應的情感詞(opinion word)是 「一般」,極性為負(negative),後一個角度對應的情感詞為 「周到」,極性為正(positive)
- 問題抽象:其實可以看作一個類似於分詞問題的 「序列標註」 問題,如下圖所示,給出分詞後的輸入序列,輸出兩個同等長度的 BIO 序列,一個作為角度詞抽取的輸出序列結果,一個作為情感詞抽取的輸出序列結果,這裡 BIO 標記為序列標註問題的慣用標記法,「B」 即為欲標記的目標內容的開始詞,「I」 為欲標記內容的中間詞(或結尾詞),「O」 為不標記的內容
- (另外,將這個問題抽象成序列標註問題還有一個很大的缺點,就是角度詞和情感詞的抽取是單獨的,不是成對匹配的,即就算抽取出兩個角度詞和兩個情感詞,也不能將每個情感詞對應到每個角度詞上,萬一兩個情感詞說的都是同一個角度呢,比如 「美國記者我不知道,但是香港記者啊最快且最吼」,這個問題暫時不知道其他的解決辦法)
目標
- 剛從 keras 轉到 tensorflow(極其智障的做法,一定要先學 tf 再學 keras),實踐一下
- 實踐一下序列標註這類 seq2seq 類問題的操作方式
- 探索一下文本上的細粒度情感分析
源代碼與數據下載
還是百度網盤 https://pan.baidu.com/s/1gfnJs8j
數據集
SemEval 2014 ( Restaurant ) :(標註好的數據的下載也在上面那個百度網盤裡)這是一個標註數據集,看到很多論文都在用,實驗使用了Restaurant 的那一部分數據,數據內容是用戶在網上對餐廳的評價
從數據示例看,數據集只提供了角度詞(aspectTerm)的抽取結果,沒有情感詞的抽取結果,訓練加測試數據總共3841條,需要人工標註情感詞結果(WTF...)
不過還好我拿到了別人標註過的一個結果,提供者是南陽理工大學的 Wang Wenya (感激不盡),也是下面要說的這個參考文獻的作者
參考文獻
Coupled Multi-Layer Attentions for Co-Extraction of Aspect and Opinion Terms(AIII 2017)
因為這個論文是目前這個數據集上成績最好的,準確度達到了 0.85+ ,本來想實現一下然後改進一下投個論文啥的,後來我想多了。。。論文中提出的模型不知道高到哪裡去了,根本看不懂,也請教過世外高人神秘高手都沒什麼結果。然後就直接照著寫了一遍,結果跑出來準確度只能到 0.7,因為不理解模型原理也沒法去調試,所以就放棄了。。。
有興趣的可以去研究下這個論文,目前作者已經提供了源代碼下載 https://github.com/happywwy/Coupled-Multi-layer-Attentions,在這裡放一下模型的架構圖,簡單來說就是 tensor+GRU+attention 三個內容的組合,反正我也不是什麼搞研究的,一貫原則,看不懂就撤
還是先上結論
- 像主題詞與情感詞提取這種細粒度情感分析問題並不是一個簡單的問題,目前是當成一個序列標註問題來處理,可是無法滿足成對提取的要求,目前我還不知道什麼更好的辦法,分開提取的話,反正據論文里說能達到 86%+
- tensorflow 比 keras 強大,不能只學 keras 這種傻瓜式的高層介面,還是要學習下底層的具體的東西,能實現的東西要更靈活,對模型的理解也更深刻
- 但是 tensorflow 真的是一個特別麻煩且腦迴路特彆扭曲的一個框架,所以以後可能轉戰據說很優雅的 pytorch 了
正文
因為對上面那個論文的實踐探索無果,就不放出來丟人了,下面就只說用簡單的 LSTM 實現的過程吧,可能有不對的地方,反正你也不能打我
1 數據預處理
標註數據集下載下來的數據示例上面貼出來過,是 xml 格式的,提供了抽取出來的單詞,需要自己把原句子序列處理成 BIO 序列的形式,數據量比較小,不到 4000 條,並且是英文的,涉及不到什麼萬惡的編碼問題,所以沒啥可說的,放一下處理好的數據結果
test_docs.txt
test_labels_a.txt(角度詞標註結果)
(這裡把 BIO 序列換成了 012 序列,B 對應 0,I 對應 1,因為數字標籤方便之後操作)
test_labels_p.txt(情感詞標註結果)
詞向量模型
訓練一個 word2vec 詞向量模型,這個是獨立在模型外面提前做的,因為數據集提供的數據只有 4000 條很少,不使用預訓練的 word embedding 模型效果就會不好。訓練數據用的是 yelp 的數據,數據內容就是很多用戶在它們網站上留下的對酒店的評論文本,下載地址 https://www.yelp.com/dataset/challenge 文件太大 3 個多 G 就不放網盤了,需要的自己下吧,然後用 gensim.word2vec 訓練一個詞向量模型出來,維度我設的 200 維
2 模型搭建
tensorflow 啊,有趣。首先總共兩個文件,一個是 lstm.py,負責搭建模型,包括輸入,輸出,loss,參數更新方式等等一切細節,另一個是 train_lstm.py,負責讀入數據,數據預處理,以及調用前一個 py 進行訓練等操作,一個一個說
lstm.py
先放一個模型框架,包括兩個並列的 LSTM 層,兩個全連接層(dense_out),最後是損失函數(loss)與優化器(optimizer),evaluate 是用來在訓練過程中定期計算準確度的,方便自己看結果
首先定義輸入輸出,在 tf 的模型搭建過程中,輸入輸出用 tf.placeholder(佔位符)表示,而參數用 tf.Variable 表示,這個區別很重要,一開始我反應了很久。。。上代碼
def build_input(self): config = self.config x = tf.placeholder(tf.float32, shape=(None, config.embedding_dim), name=x) y1 = tf.placeholder(tf.int32, shape=(None,), name=y1) y2 = tf.placeholder(tf.int32, shape=(None,), name=y2) return x, y1, y2
這裡輸入格式是(None,config.embedding_dim),None 是指序列的長度,因為每個句子長度不一樣,無法提前確定有多長,我又不想做 padding 把它們切割到同樣的長度,所以就用 None 佔位,而 config.embedding_dim 是每個單詞的詞向量的維度,也就是詞向量訓練的維度,即 200
然後 y1,y2 就分別是角度詞與情感詞的結果序列,格式是(None,),這個 None 跟 x 的 None 相等,然後第二維為空就相當於第二維為 1,因為輸出的只是一個數字(0,1 或 2,對應B,I 與 O),只有 1 維
接下來是模型,首先輸入的 x 分別進入兩個 LSTM 中,得到結果分別為 r_a 與 r_p,然後再分別進入兩個全連接層,得到結果 logits_a 與 logits_p,最後 softmax 一下,得到最終的結果 out_a 與 out_p ,放代碼:
def __init__(self, config): self.config = config self.init_state = [] self.final_state = [] self.x, self.y1, self.y2 = self.build_input() for i in [a,p]: with tf.variable_scope("rnn_"+i): with tf.variable_scope("gru_cell"): cell = tf.nn.rnn_cell.BasicLSTMCell(config.gru_hidden_size) cell = tf.nn.rnn_cell.DropoutWrapper(cell, output_keep_prob=config.drop_rate) init_state = cell.zero_state(1, tf.float32) self.init_state.append(init_state) r, final_state = tf.nn.dynamic_rnn(cell, tf.reshape(self.x, [1, -1, config.embedding_dim]), initial_state=init_state) self.final_state.append(final_state) r = tf.reshape(r, [-1, config.gru_hidden_size]) if i == a: r_a = r else: r_p = r with tf.variable_scope("dense_out_a"): C_a = tf.Variable(tf.random_normal([config.gru_hidden_size, 3]), name=C) logits_a = tf.matmul(r_a, C_a) out_a = tf.nn.softmax(logits_a) with tf.variable_scope("dense_out_p"): C_p = tf.Variable(tf.random_normal([config.gru_hidden_size, 3]), name=C) logits_p = tf.matmul(r_p, C_p) out_p = tf.nn.softmax(logits_p)
這裡 tensorflow 和 keras 的區別就出來了,keras 做到以上幾點只需要無腦一層一層往上堆就可以了,但是 tensorflow 就要很具體的寫了,比如 keras 里全連接是這麼寫
model.add(Dense(labels.shape[1], activation=softmax))
反正就是堆一層,我具體怎麼個全連接法你也不用管,但是 tensorflow 里就得明白寫出來,是先新建一個參數張量 C_a ,然後再去和輸入 r_a 去做矩陣乘法,讓你有一種 「哦,原來如此」 的感覺
這裡 LSTM 是個比較難寫的地方,因為需要保存 init_state 之類的中間輸出,又不是很懂是幹什麼用的,還好有 dynamic_rnn 這個函數,要不寫起來更麻煩
另外 with tf.variable_scope 這句話屬於沒有作用但是很有意義的語句,保持良好的為變數建立命名空間的習慣,既能避免重名參數產生衝突的尷尬,又能讓人在使用 tensorboard 對模型進行檢查的時候看上去很整齊,而不是亂七八糟一大團
接下來是 loss 的定義和參數更新函數的定義,loss 直接在上面得到的輸出結果 logits_a 與 logits_p (注意不是 softmax 之後的 out_a 與 out_p)上加一個交叉熵損失函數 sparse_softmax_cross_entropy_with_logits ,然後將得到的 loss 輸入 optimizer 中,用 Adam 優化器對參數進行反向傳播更新,就打完收工了。上代碼:
def build_loss(self, logits_a, logits_p): logits = tf.concat([logits_a, logits_p], 0) y = tf.concat([self.y1, self.y2], 0) loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=y) loss = tf.reduce_sum(loss) return lossdef build_optimizer(self, loss): config = self.config tvars = tf.trainable_variables() grads, _ = tf.clip_by_global_norm(tf.gradients(loss, tvars), config.max_grad_norm) optimizer = tf.train.AdamOptimizer(config.learning_rate) train_op = optimizer.apply_gradients(zip(grads, tvars), global_step=tf.contrib.framework.get_or_create_global_step()) return train_op
3 訓練與測試
train_lstm.py
首先隨便設置一些參數
class Config(object): embedding_dim = 200 # 詞向量維度 gru_hidden_size = 80 # lstm 隱層個數 batch_size = 1 # 數據量小,沒有用 batch learning_rate = 0.007 # 學習率 drop_rate = 0.5 # LSTM 層drop率
學習率很小,才 0.007,因為假如稍微大點,一次參數更新後輸出的結果就全是 「2」,即把所有的序列都標成 「O」,原因不知,可能是因為數據中 「O」 標籤太多了?
然後就是把數據讀入,然後處理成模型輸入所需的向量形式,代碼就不放了。直接放訓練代碼:
epochs = 100with tf.Session() as sess: tf.summary.FileWriter(graph, sess.graph) sess.run(tf.global_variables_initializer()) start = time.time() new_state = sess.run(model.init_state) statistic_step = 200 total_loss = 0 for e in range(epochs): for i in range(len(x_train)): feed_dict = {model.x: x_train[i], model.y1: y_train_a[i], model.y2: y_train_p[i]} for ii, dd in zip(model.init_state, new_state): feed_dict[ii] = dd loss, new_state, _ = sess.run([model.loss, model.final_state, model.optimizer], feed_dict=feed_dict) total_loss += loss end = time.time() if i % statistic_step == 0: print ******************************************** print epoch: +str(e)+ / +str(epochs) print steps: +str(i) print cost_time: +str(end-start) if i == 0: print loss: +str(total_loss) else: print loss: +str(total_loss/statistic_step) total_loss = 0 if i % statistic_step == 0: correct_a_num = 0 correct_p_num = 0 test_batch_size = 128 for j in range(test_batch_size): index = random.randint(0, len(x_train)-1) feed_dict[model.x] = x_train[index] feed_dict[model.y1] = y_train_a[index] feed_dict[model.y2] = y_train_p[index] correct_a, correct_p, out_a = sess.run([model.correct_a, model.correct_p, model.out_a], feed_dict=feed_dict) if correct_a: correct_a_num += 1 if correct_p: correct_p_num += 1 score1 = float(correct_a_num)*100/test_batch_size score2 = float(correct_p_num)*100/test_batch_size print precision: +str(score1)+ +str(score2)
這裡 epochs=100 是指跑 100 輪,每輪把所有數據跑一遍。statistic_step=200 是指每輸入200條句子就測試一下目前的準確度
tensorflow 的訓練很有意思,這個 sess.run 這個函數,你放模型里哪個位置的變數進去,他就運行到哪個位置。比如這裡如果只放 model.loss,他就跑一遍模型到 loss 函數那個位置,然後輸出,但是如果只放 model.r_a 進去(就是 LSTM 層的輸出結果),他就只運行到 LSTM 層然後輸出,後面的就不管了,自然也運行不到優化器那塊,也不能進行參數的更新,很清奇的腦迴路
結果
具體運行結果就不放了,因為忘截圖了。。。反正準確度大概就在 60%-70%之間這樣吧,之前看到一個使用 LSTM 的論文里的結果能達到 75%,不知道是用了什麼黑科技,科研圈的事嘛,反正他就說他達到了 75%,你也看不著,又不能拿他怎麼樣
總結
- 像主題詞與情感詞提取這種細粒度情感分析問題並不是一個簡單的問題,目前是當成一個序列標註問題來處理,可是無法達到成對提取的要求,目前我還不知道什麼更好的辦法,分開提取的話,反正據論文里說能達到 80%+
- tensorflow 比 keras 強大,不能只學 keras 這種傻瓜式的高層介面,還是要學習下底層的具體的東西,能實現的東西要更靈活,對模型的理解也更深刻
- 但是 tensorflow 真的是一個特別麻煩且腦迴路特彆扭曲的一個框架,所以以後可能轉戰據說很優雅的 pytorch 了
就醬
推薦閱讀:
※TensorFlow(1)-AlexNet實現
※FaceRank-人臉打分基於 TensorFlow 的 CNN 模型,這個妹子顏值幾分? FaceRank 告訴你!
※Fully-Convolutional Siamese Networksfor Object Tracking 翻譯筆記
※乾貨 | TensorFlow的55個經典案例
TAG:TensorFlow | 自然语言处理 | 文本情感分析 |