TF使用例子-情感分類
作者: Slyne_D
原文鏈接:https://www.jianshu.com/p/1659ce108f55查看更多的專業文章,請移步至「人工智慧LeadAI」公眾號,查看更多的課程信息及產品信息,請移步至全新打造的官網:www.leadai.org.
正文共10052個字,4張圖,預計閱讀時間26分鐘。
這次改寫一下,做一個簡單的分類模型和探討一下hidden layer在聚類的應用場景下會有什麼效果。為了能寫的儘可能讓讀者理解,本文也會寫一下keras來實現(就幾行代碼)。
01
爬取數據
網上有很多的爬蟲教程,這裡不具體講了,不過強烈建議爬別人網站的時候先找找有沒有現成的api(比如你想爬網易雲音樂的歌詞評論數據什麼的o( ̄▽ ̄)d)。
我這裡爬了bangumi上一些作品的評論,附上代碼(crawler.py):
#!/usr/bin/env pythonn# -*- coding: utf-8 -*-nimport requestsnimport renimport timenfrom bs4 import BeautifulSoupnreq_header = { n Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8, n Accept-Language:zh - CN, zh;q = 0.8, en;q = 0.6, nConnection:keep - alive, nCookie:_hc.v = ""4e10f82f-cdb5-4fa1-ba89-262a394be3d1.1490604667""; PHOENIX_ID = 0a01084a - 15b1299fd35 - 163c9ff; s_ViewType = 10; JSESSIONID = F255BB7A08A17AFC8F2E3701599B3193; aburl = 1; cy = 6; cye = suzhou, n Upgrade-Insecure-Requests:1, n User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36}n history = {} #記錄鏈接是否已經爬取過,防止重定向nfor subjectid in range(1,220000): npageid = 1 ncur_url = "http://bgm.tv/subject/" + str(subjectid)+ "/comments" nwhile(True): nmark = 0 ncur_url = cur_url + "?page=" + str(pageid) nif cur_url in history: # 是否爬取過 nbreak nelse: nhistory[cur_urln ![@H95LRLI39{5M`FA3OIY{QD.png](http://upload-images.jianshu.io/upload_images/77013-6993ddc74df9c560.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)] = 1 n try: nr = requests.get(cur_url, headers=req_header, timeout=10) nexcept: nprint(subjectid) ntime.sleep(5) n try: nr = requests.get(cur_url, headers=req_header, timeout=10) nexcept: nbreak n#爬兩次還爬不到就放棄換下一個 ncontent = BeautifulSoup(r.content, lxml) nbase_url_index = r.url.find("?") n cur_url = r.url[0:base_url_index] ntry: ntitle = content.find("a",href=re.compile(r"/subject*")).text nexcept: nbreak n if title == "": #爬取到的頁面有問題,比如404 nbreak n # 因為把每個詞條都作為文件名,所以有些特殊字元不能作為文件名 n title = title.replace("/","-").replace(":","-").replace("""," ").replace("<", "(").replace(">", ")").replace("?","-").replace("*","-").replace("","-").replace("|","-").replace("n","-") n # 把爬取到的條目下的評論都拿出來放到條目文件里 n with open("directory/"+title, "a", encoding="utf-8")as f: n for item in content.find("div", {"id": "comment_box"}).find_all("div", {"class":"item clearit"}): item = item.find("div",{"class": re.compile(r"text_main_*")}) n try: nprint(item.find("span")["class"]) nexcept: nbreak nf.write(item.find("span")["class"][0] + n) nf.write(item.text.strip()) nmark = 1 n f.write("n===========================n") n pageid += 1 nif mark == 0: nbreakn time.sleep(0.3) # 睡一會,別爬太快n
1.1 處理數據
爬下來的數據是這樣的:
每個詞條裡面是這樣的:
sstarsX表示的是打分,半個星表示1分,所以如果是sstars8就是4個星。每一行用「=====」分割。我們先把數據抽取出來。
#!/usr/bin/env pythonn# -*- coding: utf-8 -*-nimport osimport renout = open("bangumi_sentiment", "w", encoding="utf-8")for cur_file in os.listdir("directory"): nwith open("directory/"+cur_file, "r", encoding="utf-8") as f:n comments = f.read().split("===========================") nfor comment in comments: n if comment == "": ncontinuen start_index = comment.find("sstars")n score = comment[start_index+6:8].strip() ntry: n #找到日期末尾位置n content_index = re.search("d{4}-d{1,2}-d{1,2} d{1,2}:d{1,2} ",comment).end() nexcept: ncontinuenout.write(score+" ")nout.write(" ".join(comment[content_index:].split("n"))+"n")n
(因為又爬了日期又爬了用戶名以及打分,所以還可以做一些數據分析比如看看那些人容易給差評,興趣相投什麼的,再按一定規則給詞條打個平均分什麼的..反正有很多東西可以做,不過又不是PM操什麼心..)
抽取出來的數據如下,第一列是打分:
8 Tr1對唱和列車音好評,Tr2+1星
6 「Terminal Station」6.87 Tr.1的對唱真心帶感,可惜量少是硬傷8 嗯!很棒!一二曲很好聽的!4 「Forgotten Paradise」57 合唱都很贊。個別幾軌有點抽風,不過不影響整體的水平7 感覺還行吧……6 一般吧,感覺沒有3好6 聽不出原曲系列……Tr.1這個5合1略虎(可惜不好聽2336 GCHM良心大大滴壞了
9 這張很魔性
用類似的方法也可以爬爬其它網站,因為現在依然有很多網站沒有做防爬蟲的措施。(下篇博客寫一下驗證碼識別哈~)
label 部分,我把情感分成low, middle, high三個部分,比如打分在[1,4]為low, (4,7]為middle, (7,10]為high。只是我自己拍腦袋這麼設置的,因為我想弄成分類模型而不是回歸,如果你打算直接預測得分,可以把label作為數值,後面建模的時候loss選用mean_squared_error,讓預測的得分和實際得分儘可能的相近。
02
用TensorFlow建簡單的文本分類模型
首先要把訓練語料里的字和事先訓練的word2vector里的字對應起來,再構建一個統一的embedding層。
2.1 訓練詞向量
這裡我爬取了一些萌娘百科的文章加上上面bangumi的語料加起來差不多200M,因為是基於字,所以要把字按照空格分開,然後繁簡大小寫轉換之後作為word2vector的輸入,類似於這樣:
#!/usr/bin/env python# n-*- coding: utf-8 -*-nimport loggingnimport multiprocessingnimport sysfrom gensim.models nimport Word2Vecnfrom gensim.models.word2vec nimport LineSentencedef train(inp, model_name, size=100, word_frequency_threshold=5):n logging.basicConfig(format = %(asctime)s : %(levelname)s : %(message)s, level = logging.INFO) n print("Begin to train model ...")n model = Word2Vec(LineSentence(inp),n size = size,n window = 10,n hs = 1,n sg = 1, # skip-gramn min_count = word_frequency_threshold,n workers = multiprocessing.cpu_count()/2) #CPU數量n model.save_word2vec_format(model_name, binary=False) # save a model in order to be checkedif __name__ == "__main__": if len(sys.argv) <3:n print("please input input file and model file")n inp = sys.argv[1]n model = sys.argv[2]n word_size = 100n word_threshold = 3n if len(sys.argv) > 3:n word_size = int(sys.argv[3]) #詞向量維度n word_threshold = int(sys.argv[4]) #每個字最少出現的次數n train(inp, model, word_size, word_threshold)n
運行python inp out.model 200 3訓練一段時間後就可以得到一個名為out.model的模型,為了方便查看所以binary設置成False,輸出的模型文件如下:
第一行是模型的維度,這裡表示的含義是公有37064個字,每個字的詞向量為200。每一行的第一個列是字。
這裡提一下,我們有的時候訓練了兩個詞向量的模型,那怎麼把一個詞的詞向量映射到另一個詞向量的空間呢?你可以利用兩個詞向量模型都有的詞來學習權重w(有點像auto encoder),從而可以把一個模型中的詞向量映射到另一個空間。
(另外,詞向量的模型可以載入一個模型後,繼續加入句子來訓練。不過不可以加入新詞)
我們這裡比較簡單的,如果不出現在我們訓練好的詞(字)向量中的字,直接用<unk>(unknow)來代替。
2.2 embedding層
如果要使用我們訓練好的詞向量來代替embedding層(你也可以不用,效果可能會稍微差點),你要確保的是你的輸入(句子)中的每個字的id正好是詞向量矩陣的第id個。比如有一個句子:
除 了 劇 情 外 啥 都 好 的 片 子
每個字在詞向量矩陣中的行號分別是:
[1 2 3 4 5 6 7 8 0 10 11]
那你的模型這句話的輸入就是[1 2 3 4 5 6 7 8 0 10 11]。
請一定要確保這一點,而且如果你用keras,你的padding的值就是embedding中對應的行號,比如如果你的padding是-1,對應的就是embedding[-1] 也就是embedding的最後一個字。
2.3 建模型
這部分其實跟上次的序列標註差不多,區別就是上次是多輸出,這次是一個輸出判斷屬於哪個類。核心代碼如下:
# 設置placeholderword_ids = tf.placeholder(tf.int32, shape=[None, None],name="word_ids") n # batch size, max length of sentence in batchsequence_lengths = tf.placeholder(tf.int32, shape=[None], name="sequence_length") n# shape = batch sizelabels = tf.placeholder(tf.int32, shape=[None], name="labels") n# only one dimensiondropout = tf.placeholder(dtype=tf.float32, shape=[], name="dropout")nlr = tf.placeholder(dtype=tf.float32, shape=[], name="lr")n# 設置embedding層with tf.variable_scope("words"):n _word_embeddings = tf.Variable(embeddings, name="_word_embeddings", dtype=tf.float32,n trainable=trainable)n word_embeddings = tf.nn.embedding_lookup(_word_embeddings, word_ids,n name="word_embeddings")n word_embeddings = tf.nn.dropout(word_embeddings, dropout)# 設置模型with tf.variable_scope("bi-lstm"):n lstm_cell = tf.contrib.rnn.LSTMCell(hidden_size)n _, (output_state_fw, output_state_bw) = tf.nn.bidirectional_dynamic_rnn(lstm_cell,n lstm_cell, word_embeddings,n sequence_length=sequence_lengths,n dtype=tf.float32)n output = tf.concat((output_state_fw[-1], output_state_bw[-1]), axis=-1)n output = tf.nn.dropout(output, dropout)n# 輸出部分在雙向lstm的最後一層合併後加一個全連接層,全連接層後接一個softmax層with tf.variable_scope("proj"):n W = tf.get_variable("W", shape=[2 * hidden_size, nlabels],n dtype=tf.float32)n b = tf.get_variable("b", shape=[nlabels], dtype=tf.float32,n initializer=tf.zeros_initializer())n output = tf.reshape(output, [-1, 2 * hidden_size])n pred = tf.matmul(output, W) + bn logits = prednlabels_pred = tf.cast(tf.argmax(logits, axis=-1), tf.int32)nlosses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=labels)nloss = tf.reduce_mean(losses) # batch的平均losswith tf.variable_scope("train_step"):n optimizer = tf.train.AdamOptimizer(lr)n train_op = optimizer.minimize(loss)n
03
用Keras建簡單的文本分類模型
keras這部分的代碼比較簡潔,需要注意的是如果要用variable_length的句子(不同長度句子),需要多設置一個參數mask_zero=True,這個參數在embedding層設置後,所有word_id=0的字都會在後面的lstm層會忽略掉,所以我們在設置embedding的時候要在第一行插入一個全0的row,相應的word_id也都要加1,這點要注意注意。
這裡我給了兩個可以做這個模型的模型,區別只是在輸出的時候是要預測一個分類還是一個數值。如果是分類就把label處理成categorical的,如果是預測打分值就直接用數值就行(比如半顆星是1分,5星是10分)。
# 分類模型def create_model_classify(max_features, nlabels, embeddings=None, embedding_dim=200, hidden_dim=300):n model = Sequential() if embeddings is not None:n model.add(Embedding(input_dim=max_features,output_dim=embedding_dim, dropout=0.2, weights=[embeddings], mask_zero=True)) else:n model.add(Embedding(max_features, embedding_dim=embedding_dim, dropout=0.2))n model.add(Bidirectional(LSTM(hidden_dim, dropout_W=0.2, dropout_U=0.2, return_sequences=False))) # try using a GRU instead, for funn model.add(Dense(nlabels))n model.add(Activation(softmax))nn model.compile(loss=categorical_crossentropy,n optimizer=adam,n metrics=[accuracy,]) # availabel metrics https://keras.io/metrics/n return model# 回歸模型def create_model_regress(max_features, embeddings=None, embedding_dim=200, hidden_dim=300):n model = Sequential() if embeddings is not None:n model.add(Embedding(input_dim=max_features,output_dim=embedding_dim, dropout=0.2, weights=[embeddings], mask_zero=True)) else:n model.add(Embedding(max_features, embedding_dim=embedding_dim, dropout=0.2))n model.add(n Bidirectional(LSTM(hidden_dim, dropout_W=0.2, dropout_U=0.2),n merge_mode=concat)) nn model.compile(loss=mean_squared_error,n optimizer=sgd,n metrics=[mae,acc ]) # availabel metrics https://keras.io/metrics/idden_dim=nreturn modeln
04
情感模型的隱藏層聚類
利用上面訓練出來的模型,抽取每一條訓練數據的隱藏層,然後對其進行聚類。(saraba1st數據集,訓練集準確率90%)
#keras實現from keras.models import Modelnorg_model = load_model("result/model.weights/acc_XXX.model")nlayer_name = bidirectional_1intermediate_layer_model = Model(input=org_model.input, n# keras 2.0 inputs outputsnoutput=org_model.get_layer(layer_name).output)nintermediate_output = intermediate_layer_model.predict(np.asarray(word_ids))n
4.1 聚類結果
我們先對原始數據用PCA降維到2維方便顯示在平面上,不同的顏色表示的是其原始的label。
從圖中可以看到原始的數據分布就是很不均勻的,黑色部分的數據量非常大。
那麼來看一下其隱藏層的實際聚類情況:
Kmeans k=3:
我們可以發現原本很大的一部分黑色被納入了灰色,很有可能就是這部分的數據很難判斷是歸於哪個類。檢查之後發現是level_middle和level_high之間存在了混淆(看起來也是可以理解的)。比如有些句子是這樣的:
level_high 因 為 天 元 , 所 以 期 望 越 大 失 望 越 大 吧 . . . 看 完 後 還 有 印 象 的 就 第 1 第 3 第 7 集 了 . . . 後 半 部 分 慢 慢 變 得 很 乏 力 , 能 記 住 的 只 有 幾 個 小 點 . . . 神 展 開 也 讓 人 感 覺 不 到 太 大 的 驚 喜 。
level_high 前 第 三 話 真 是 驚 艷 , 往 後 越 來 越 爛 。
這兩句句子就算是人眼去看的話也真的很難區分到底應不應該標level_high。
來看一下Kmeans k=4:
可以看到在middle和high之間確實還夾雜了一層難以判斷是好還是中立的語句。
可以慶幸的是負類都很明顯的區分開來了。
05
分成正負兩類的結果
把三個分類的結果轉換成二分類之後,驗證集上的acc從0.8提升到了0.85。
訓練集上hidden layer的結果如下:
Kmeans聚類之後的結果
可以看到中間還是有一部分被遮蓋了。
由於從原始的PCA降維後的結果其實是很容易看到兩個"簇"的,所以考慮換一個聚類方法。
Agglomerative聚類:
linkage = complete看起來好多了。。。
同樣的出現上面的問題,很大程度是因為我們用了現成的"標註"(用戶的評分),但是這種標註有的時候是非常不準確的。比如:
(明明打了5星,硬是在評價上做出了看起來不那麼positive的結果,sigh~)
所以在實際應用訓練集的時候往往是需要多個"專業"人員對其共同進行打分和標註,才能儘可能的準確。(然而這樣的數據非常缺乏)。
話說旅遊網站的評論數據通常都十分準確。。
06
文本代碼
請戳這裡(https://github.com/Slyne/tf_classification_sentiment)
07
總結
本文用tensorflow和keras實現了一下文本情感分類,窺探了一下隱藏層的表述能力(還是不錯的)。
推薦閱讀:
※tf.set_random_seed
※資源|100精品開源項目助你成為TensorFlow專家(一)
※CNN中卷積層的計算細節
※tensorflow讀取數據-tfrecord格式
※如何使用最流行框架Tensorflow進行時序預測和時間序列分析
TAG:TensorFlow | 情感 |