為什么要用深度學習模型?除了它更高精度等原因之外,還有一個重要原因,那就是它是目前唯一的能夠實現“端到端”的模型。所謂“端到端”,就是能夠直接將原始數據和標簽輸入,然后讓模型自己完成一切過程——包括特征的提取、模型的學習。而回顧我們做中文情感分類的過程,一般都是“分詞——詞向量——句向量(LSTM)——分類”這么幾個步驟。雖然很多時候這種模型已經達到了state of art的效果,但是有些疑問還是需要進一步測試解決的。對於中文來說,字才是最低粒度的文字單位,因此從“端到端”的角度來看,應該將直接將句子以字的方式進行輸入,而不是先將句子分好詞。那到底有沒有分詞的必要性呢?本文測試比較了字one hot、字向量、詞向量三者之間的效果。
模型測試
本文測試了三個模型,或者說,是三套框架,具體代碼在文末給出。這三套框架分別是:
1、one hot:以字為單位,不分詞,將每個句子截斷為200字(不夠則補空字符串),然后將句子以“字-one hot”的矩陣形式輸入到LSTM模型中進行學習分類;
2、one embedding:以字為單位,不分詞,,將每個句子截斷為200字(不夠則補空字符串),然后將句子以“字-字向量(embedding)“的矩陣形式輸入到LSTM模型中進行學習分類;
3、word embedding:以詞為單位,分詞,,將每個句子截斷為100詞(不夠則補空字符串),然后將句子以“詞-詞向量(embedding)”的矩陣形式輸入到LSTM模型中進行學習分類。
其中所用的LSTM模型結構是類似的。所用的語料還是《文本情感分類:深度學習模型(2)》中的語料,以15000條進行訓練,剩下的6000條左右做測試。意外的是,三個模型都取得了相近的結果。
可見,在准確率方面,三者是類似的,區分度不大。不管是用one hot、字向量還是詞向量,結果都差不多。也許用《文本情感分類:深度學習模型(2)》的方法來為每個模型選取適當的閾值,會使得測試准確率更高一些,但模型之間的相對准確率應該不會變化很大。
當然,測試本身可能存在一些不公平的情況,也許會導致測試結果公平,而我也沒有反復去測試。比如one hot的模型迭代了90次,其它兩個模型是30次,因為one hot模型所構造的樣本維度太大,需要經過更長時間才出現收斂現象,而且訓練過程中,准確率是波動上升的,並非像其它兩個模型那樣穩定上升。事實上這是所有one hot模型的共同特點。
多扯一點
看上去,one hot模型的確存在維度災難的問題,而且訓練時間又長,效果又沒有明顯提升,那是否就說明沒有研究one hot表示的必要了呢?
我覺得不是這樣的。當初大家詬病one hot模型的原因,除了維度災難之外,還有一個就是“語義鴻溝”,也就說任意兩個詞之間沒有任何相關性(不管用歐式距離還是余弦相似度,任意兩個詞的計算結果是一樣的)。可是,這一點假設用在詞語中不成立,可是用在中文的“字”上面,不是很合理嗎?漢字單獨成詞的例子不多,大多數是二字詞,也就是說,任意兩個字之間沒有任何相關性,這個假設在漢字的“字”的層面上,是近似成立的!而后面我們用了LSTM,LSTM本身具有整合鄰近數據的功能,因此,它暗含了將字整合為詞的過程。
此外,one hot模型還有一個非常重要的特點——它沒有任何信息損失——從one hot的編碼結果中,我們反過來解碼出原來那句話是哪些字詞組成的,然而,我無法從一個詞向量中確定原來的詞是什么。這些觀點都表明,在很多情況下,one hot模型都是很有價值的。
而我們為什么用詞向量呢?詞向量相當於做了一個假設:每個詞具有比較確定的意思。這個假設在詞語層面也是近似成立的,畢竟一詞多義的詞語相對來說也不多。正因為如此,我們才可以將詞放到一個較低維度的實數空間里,用一個實數向量來表示一個詞語,並且用它們之間的距離或者余弦相似度來表示詞語之間的相似度。這也是詞向量能夠解決“一義多詞”而沒法解決“一詞多義”的原因。
從這樣看來,上面三個模型中,只有one hot和word embedding才是理論上說得過去的,而one embedding則看上去變得不倫不類了,因為字似乎不能說具有比較確定的意思。但為什么one embedding效果也還不錯?我估計,這可能是因為二元分類問題本身是一個很粗糙的分類(0或1),如果更多元的分類,可能one embedding的方式效果就降下來了。不過,我也沒有進行更多的測試了,因為太耗時間了。
當然,這只能算是我的主觀臆測,還望大家指正。尤其是one embedding部分的評價,是值得商榷的。
代碼來了
可能大家並不想看我胡扯一通,是直接來看代碼的,現奉上三個模型的代碼。最好有GPU加速,尤其是試驗one hot模型,不然慢到哭了。
模型1:one hot
# -*- coding:utf-8 -*- ''' one hot測試 在GTX960上,約100s一輪 經過90輪迭代,訓練集准確率為96.60%,測試集准確率為89.21% Dropout不能用太多,否則信息損失太嚴重 ''' import numpy as np import pandas as pd pos = pd.read_excel('pos.xls', header=None) pos['label'] = 1 neg = pd.read_excel('neg.xls', header=None) neg['label'] = 0 all_ = pos.append(neg, ignore_index=True) maxlen = 200 #截斷字數 min_count = 20 #出現次數少於該值的字扔掉。這是最簡單的降維方法 content = ''.join(all_[0]) abc = pd.Series(list(content)).value_counts() abc = abc[abc >= min_count] abc[:] = range(len(abc)) def doc2num(s, maxlen): s = [i for i in s if i in abc.index] s = s[:maxlen] return list(abc[s]) all_['doc2num'] = all_[0].apply(lambda s: doc2num(s, maxlen)) #手動打亂數據 #當然也可以把這部分加入到生成器中 idx = range(len(all_)) np.random.shuffle(idx) all_ = all_.loc[idx] #按keras的輸入要求來生成數據 x = np.array(list(all_['doc2num'])) y = np.array(list(all_['label'])) y = y.reshape((-1,1)) #調整標簽形狀 from keras.utils import np_utils from keras.models import Sequential from keras.layers import Dense, Activation, Dropout from keras.layers import LSTM import sys sys.setrecursionlimit(10000) #增大堆棧最大深度(遞歸深度),據說默認為1000,報錯 #建立模型 model = Sequential() model.add(LSTM(128, input_shape=(maxlen,len(abc)))) model.add(Dropout(0.5)) model.add(Dense(1)) model.add(Activation('sigmoid')) model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy']) #單個one hot矩陣的大小是maxlen*len(abc)的,非常消耗內存 #為了方便低內存的PC進行測試,這里使用了生成器的方式來生成one hot矩陣 #僅在調用時才生成one hot矩陣 #可以通過減少batch_size來降低內存使用,但會相應地增加一定的訓練時間 batch_size = 128 train_num = 15000 #不足則補全0行 gen_matrix = lambda z: np.vstack((np_utils.to_categorical(z, len(abc)), np.zeros((maxlen-len(z), len(abc))))) def data_generator(data, labels, batch_size): batches = [range(batch_size*i, min(len(data), batch_size*(i+1))) for i in range(len(data)/batch_size+1)] while True: for i in batches: xx = np.zeros((maxlen, len(abc))) xx, yy = np.array(map(gen_matrix, data[i])), labels[i] yield (xx, yy) model.fit_generator(data_generator(x[:train_num], y[:train_num], batch_size), samples_per_epoch=train_num, nb_epoch=30) model.evaluate_generator(data_generator(x[train_num:], y[train_num:], batch_size), val_samples=len(x[train_num:])) def predict_one(s): #單個句子的預測函數 s = gen_matrix(doc2num(s, maxlen)) s = s.reshape((1, s.shape[0], s.shape[1])) return model.predict_classes(s, verbose=0)[0][0]
模型2:one embedding
# -*- coding:utf-8 -*- ''' one embedding測試 在GTX960上,36s一輪 經過30輪迭代,訓練集准確率為95.95%,測試集准確率為89.55% Dropout不能用太多,否則信息損失太嚴重 ''' import numpy as np import pandas as pd pos = pd.read_excel('pos.xls', header=None) pos['label'] = 1 neg = pd.read_excel('neg.xls', header=None) neg['label'] = 0 all_ = pos.append(neg, ignore_index=True) maxlen = 200 #截斷字數 min_count = 20 #出現次數少於該值的字扔掉。這是最簡單的降維方法 content = ''.join(all_[0]) abc = pd.Series(list(content)).value_counts() abc = abc[abc >= min_count] abc[:] = range(1, len(abc)+1) abc[''] = 0 #添加空字符串用來補全 def doc2num(s, maxlen): s = [i for i in s if i in abc.index] s = s[:maxlen] + ['']*max(0, maxlen-len(s)) return list(abc[s]) all_['doc2num'] = all_[0].apply(lambda s: doc2num(s, maxlen)) #手動打亂數據 idx = range(len(all_)) np.random.shuffle(idx) all_ = all_.loc[idx] #按keras的輸入要求來生成數據 x = np.array(list(all_['doc2num'])) y = np.array(list(all_['label'])) y = y.reshape((-1,1)) #調整標簽形狀 from keras.models import Sequential from keras.layers import Dense, Activation, Dropout, Embedding from keras.layers import LSTM #建立模型 model = Sequential() model.add(Embedding(len(abc), 256, input_length=maxlen)) model.add(LSTM(128)) model.add(Dropout(0.5)) model.add(Dense(1)) model.add(Activation('sigmoid')) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) batch_size = 128 train_num = 15000 model.fit(x[:train_num], y[:train_num], batch_size = batch_size, nb_epoch=30) model.evaluate(x[train_num:], y[train_num:], batch_size = batch_size) def predict_one(s): #單個句子的預測函數 s = np.array(doc2num(s, maxlen)) s = s.reshape((1, s.shape[0])) return model.predict_classes(s, verbose=0)[0][0]
模型3:word embedding
# -*- coding:utf-8 -*- ''' word embedding測試 在GTX960上,18s一輪 經過30輪迭代,訓練集准確率為98.41%,測試集准確率為89.03% Dropout不能用太多,否則信息損失太嚴重 ''' import numpy as np import pandas as pd import jieba pos = pd.read_excel('pos.xls', header=None) pos['label'] = 1 neg = pd.read_excel('neg.xls', header=None) neg['label'] = 0 all_ = pos.append(neg, ignore_index=True) all_['words'] = all_[0].apply(lambda s: list(jieba.cut(s))) #調用結巴分詞 maxlen = 100 #截斷詞數 min_count = 5 #出現次數少於該值的詞扔掉。這是最簡單的降維方法 content = [] for i in all_['words']: content.extend(i) abc = pd.Series(content).value_counts() abc = abc[abc >= min_count] abc[:] = range(1, len(abc)+1) abc[''] = 0 #添加空字符串用來補全 def doc2num(s, maxlen): s = [i for i in s if i in abc.index] s = s[:maxlen] + ['']*max(0, maxlen-len(s)) return list(abc[s]) all_['doc2num'] = all_['words'].apply(lambda s: doc2num(s, maxlen)) #手動打亂數據 idx = range(len(all_)) np.random.shuffle(idx) all_ = all_.loc[idx] #按keras的輸入要求來生成數據 x = np.array(list(all_['doc2num'])) y = np.array(list(all_['label'])) y = y.reshape((-1,1)) #調整標簽形狀 from keras.models import Sequential from keras.layers import Dense, Activation, Dropout, Embedding from keras.layers import LSTM #建立模型 model = Sequential() model.add(Embedding(len(abc), 256, input_length=maxlen)) model.add(LSTM(128)) model.add(Dropout(0.5)) model.add(Dense(1)) model.add(Activation('sigmoid')) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) batch_size = 128 train_num = 15000 model.fit(x[:train_num], y[:train_num], batch_size = batch_size, nb_epoch=30) model.evaluate(x[train_num:], y[train_num:], batch_size = batch_size) def predict_one(s): #單個句子的預測函數 s = np.array(doc2num(list(jieba.cut(s)), maxlen)) s = s.reshape((1, s.shape[0])) return model.predict_classes(s, verbose=0)[0][0]