文本情感分類:分詞 OR 不分詞(3)


為什么要用深度學習模型?除了它更高精度等原因之外,還有一個重要原因,那就是它是目前唯一的能夠實現“端到端”的模型。所謂“端到端”,就是能夠直接將原始數據和標簽輸入,然后讓模型自己完成一切過程——包括特征的提取、模型的學習。而回顧我們做中文情感分類的過程,一般都是“分詞——詞向量——句向量(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]

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM