FastText是facebook開源的一個詞向量與文本分類工具 ,其最大的優(yōu)點就是快崭孤,同時不失精度售睹。 此算法有兩個主要應用場景:
- 文本分類
- 詞向量訓練
工業(yè)界碰到一些簡單分類問題時腿短,經(jīng)常采用這種簡單鸠补,快速的模型解決問題。
FastText原理簡介
FastText原理部分有3個突出的特點:
-
模型簡單藐翎,其結構有點類似word2vector中的CBOW架構材蹬,如下圖所示。FastText將句子特征通過一層全連接層映射到向量空間后吝镣,直接將詞向量平均處理一下堤器,就去做預測。
- 使用了n-gram的特征末贾,使得句子的表達更充分吼旧。筆者會在實戰(zhàn)中詳細介紹這部分的操作。
- 使用 Huffman算法建立用于表征類別的樹形結構未舟。這部分可以加速運算,同時減緩一些樣本不均衡的問題掂为。
其中比較有意思的是裕膀,做完分類任務后,模型全連接層的權重可以用來做詞向量勇哗。而且由于使用了n-gram的特征昼扛,fasttext的詞向量可以很好的緩解Out of Vocabulary的問題。接下來筆者就用keras構建一個fasttext模型做一下情感分析的任務欲诺,同時拿出它的詞向量看一看抄谐。
FastText情感分析實戰(zhàn)
import numpy as np
np.random.seed(1335) # for reproducibility
from keras.preprocessing import sequence
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Embedding
from keras.layers import GlobalAveragePooling1D
這里定義了兩個函數(shù)用于n-gram特征增廣,這里筆者是直接將這篇參考文章的代碼拷貝過來扰法,作者的注釋極其詳細蛹含。這里需要講解一下n-gram特征的含義:
如果原句是:今天的雪下個不停。
- unigram(1-gram)的特征:["今天"塞颁,"的"浦箱,"雪"吸耿,"下","個"酷窥,"不停"]
- bigram(2-gram) 的特征: ["今天的"咽安,"的雪","雪下"蓬推,"下個"妆棒,"個不停"]
所以大家發(fā)現(xiàn)沒,n-gram的意思將句子中連續(xù)的n個詞連起來組成一個單獨的詞沸伏。
如果使用unigram和bigram的特征糕珊,句子特征就會變成:
["今天","的"馋评,"雪"放接,"下","個"留特,"不停"纠脾,"今天的","的雪"蜕青,"雪下"苟蹈,"下個","個不停"]這么一長串右核。
這樣做可以豐富句子的特征慧脱,能夠更好的表示句子的語義。
def create_ngram_set(input_list, ngram_value=2):
"""
Extract a set of n-grams from a list of integers.
從一個整數(shù)列表中提取 n-gram 集合贺喝。
>>> create_ngram_set([1, 4, 9, 4, 1, 4], ngram_value=2)
{(4, 9), (4, 1), (1, 4), (9, 4)}
>>> create_ngram_set([1, 4, 9, 4, 1, 4], ngram_value=3)
[(1, 4, 9), (4, 9, 4), (9, 4, 1), (4, 1, 4)]
"""
return set(zip(*[input_list[i:] for i in range(ngram_value)]))
def add_ngram(sequences, token_indice, ngram_range=2):
"""
Augment the input list of list (sequences) by appending n-grams values.
增廣輸入列表中的每個序列菱鸥,添加 n-gram 值
Example: adding bi-gram
>>> sequences = [[1, 3, 4, 5], [1, 3, 7, 9, 2]]
>>> token_indice = {(1, 3): 1337, (9, 2): 42, (4, 5): 2017}
>>> add_ngram(sequences, token_indice, ngram_range=2)
[[1, 3, 4, 5, 1337, 2017], [1, 3, 7, 9, 2, 1337, 42]]
Example: adding tri-gram
>>> sequences = [[1, 3, 4, 5], [1, 3, 7, 9, 2]]
>>> token_indice = {(1, 3): 1337, (9, 2): 42, (4, 5): 2017, (7, 9, 2): 2018}
>>> add_ngram(sequences, token_indice, ngram_range=3)
[[1, 3, 4, 5, 1337], [1, 3, 7, 9, 2, 1337, 2018]]
"""
new_sequences = []
for input_list in sequences:
new_list = input_list[:]
for i in range(len(new_list) - ngram_range + 1):
for ngram_value in range(2, ngram_range + 1):
ngram = tuple(new_list[i:i + ngram_value])
if ngram in token_indice:
new_list.append(token_indice[ngram])
new_sequences.append(new_list)
return new_sequences
數(shù)據(jù)載入
筆者在之前的情感分析文章中介紹了這個數(shù)據(jù)集的數(shù)據(jù)格式,想詳細了解的同學可以去這篇文章查看數(shù)據(jù)詳情躏鱼。
def read_data(data_path):
senlist = []
labellist = []
with open(data_path, "r",encoding='gb2312',errors='ignore') as f:
for data in f.readlines():
data = data.strip()
sen = data.split("\t")[2]
label = data.split("\t")[3]
if sen != "" and (label =="0" or label=="1" or label=="2" ) :
senlist.append(sen)
labellist.append(label)
else:
pass
assert(len(senlist) == len(labellist))
return senlist ,labellist
sentences,labels = read_data("data_train.csv")
char_set = set(word for sen in sentences for word in sen)
char_dic = {j:i+1 for i,j in enumerate(char_set)}
char_dic["unk"] = 0
n-gram特征增廣
這里筆者只使用了unigram和bigram的特征氮采,如果使用trigram的特征,特征數(shù)以及計算量將會猛增染苛,所以沒有好的硬件不要輕易嘗試3鹊漠,4-gram以上的特征。
max_features = len(char_dic)
sentences2id = [[char_dic.get(word) for word in sen] for sen in sentences]
ngram_range = 2
if ngram_range > 1:
print('Adding {}-gram features'.format(ngram_range))
# Create set of unique n-gram from the training set.
ngram_set = set()
for input_list in sentences2id:
for i in range(2, ngram_range + 1):
set_of_ngram = create_ngram_set(input_list, ngram_value=i)
ngram_set.update(set_of_ngram)
# Dictionary mapping n-gram token to a unique integer. 將 ngram token 映射到獨立整數(shù)的詞典
# Integer values are greater than max_features in order
# to avoid collision with existing features.
# 整數(shù)大小比 max_features 要大茶行,按順序排列躯概,以避免與已存在的特征沖突
start_index = max_features
token_indice = {v: k + start_index for k, v in enumerate(ngram_set)}
fea_dict = {**token_indice,**char_dic}
# 使用 n-gram 特征增廣 X_train
sentences2id= add_ngram(sentences2id,fea_dict, ngram_range)
print('Average train sequence length: {}'.format(
np.mean(list(map(len, sentences2id)), dtype=int)))
數(shù)據(jù)預處理
將句子特征padding成300維的向量,同時對label進行onehot編碼畔师。
import numpy as np
from keras.utils import np_utils
print('Pad sequences (samples x time)')
X_train = sequence.pad_sequences(sentences2id, maxlen=300)
labels = np_utils.to_categorical(labels)
定義模型
這里我們我們可以看到fasttext的一些影子了:
- 使用了一個簡單的Embedding層(其實本質(zhì)上就是一個Dense層)娶靡,
- 然后接一個GlobalAveragePooling1D層對句子中每個詞的輸出向量求平均得到句子向量,
- 之后句子向量通過全連接層后看锉,得到的輸出和label計算損失值固蛾。
此模型的最后一部沒有嚴格的遵循fasttext结执。
print('Build model...')
model = Sequential()
#我們從一個有效的嵌入層(embedding layer)開始,它將我們的詞匯索引(vocab indices )映射到詞向量的維度上.
model.add(Embedding(len(fea_dict),
200,
input_length=300))
# 我們增加 GlobalAveragePooling1D, 這將平均計算文檔中所有詞匯的的詞嵌入
model.add(GlobalAveragePooling1D())
#我們投射到單個單位的輸出層上
model.add(Dense(3, activation='softmax'))
model.compile(loss='categorical_crossentropy',
optimizer="adam",
metrics=['accuracy'])
model.summary()
這下面是模型結構的的可視化輸出艾凯,我們可以看到献幔,只用了unigram和bigram的特征詞典的維度已經(jīng)到了5千多萬,如果用到trigram了特征趾诗,特征詞典的維度肯定過億蜡感。
模型訓練
從訓練速度上來看,2分多鐘一個epoch恃泪,同樣的數(shù)據(jù)郑兴,比之前筆者使用的BiLSTM的速度快了不少。
訓練副產(chǎn)物——詞向量
embedding_layer = model.get_layer("embedding_1")
emb_wight = embedding_layer.get_weights()[0]
我們可以通過上方兩行代碼就拿到fasttext的訓練副產(chǎn)物——詞向量贝乎。
其中Embedding層的weight的形式和下圖中間的 W矩陣一樣情连,每行對應著一個詞的詞向量。通過簡單的index索引就可以得到訓練好的詞向量览效。
下面是筆者索引"媽媽"這個詞的詞向量的代碼却舀。
def word2fea(word,char_dic):
wordtuple = tuple(char_dic.get(i) for i in word)
return wordtuple
mather = word2fea("媽媽",char_dic)
index = fea_dict.get(mather)
mama = emb_wight[index]
打印出來如下圖所示,"媽媽"被映射成了一個200維的詞向量锤灿。
結語
fasttext一個如此簡單的模型卻極其好用挽拔,這也是工業(yè)界特別喜歡它的原因。所以在面對問題的時候不要一上來就構建一個特別復雜的模型但校,有時候簡單的模型也能很好解決的問題螃诅,一定要記住大道至簡。
參考:
https://kexue.fm/archives/4122
http://www.voidcn.com/article/p-alhbnusv-bon.html
https://github.com/facebookresearch/fastText