1 前世今生:NGRAM
NGRAM:將詞當成一個離散的單元(因此存在一定的局限性,沒有考慮到詞與詞之間的關系)
neural network language model:只能處理定長序列,訓練慢。使用RNN之后有所改善
2 兩種訓練方式:CBOW,Skip-gram
CBOW(通過附近詞預測中心詞)、
Skip-gram(通過中心詞預測附近的詞)
2.1 CBOW步驟
變量:詞表大小V, 維度N,窗口大小C;N一般取50~300
步驟:
- 輸入層:上下文窗口內的C個詞的onehot表示
- 隱藏層1: 權重矩陣𝑊 + 激活函數
(V維表示降到N維表示)(所有的權重矩陣相同,即共享權重矩陣)
𝑊𝑉×𝑁 ,將 𝑉×𝐶映射為 𝑁×𝐶
word2vec中激活函數,用了簡單取平均;- 隱藏層2: 權重矩陣W‘ + 激活函數
(N維升維到V維)
𝑊′𝑁×𝑉 ,將 𝑁×1映射為 𝑉×1- 輸出層:softmax 激活函數,將值歸一化到0~1之間 y
- 用BP+梯度下降優化cost function y和真實y之間的距離,迭代優化參數 𝑊、𝑊′
- 收斂result:y是V維向量,每個元素取值0~1, 將最大元素值,還原為onehot編碼,就是最終結果了。
實際使用:得到第一個矩陣W之后,我們就能得到每個單詞的詞向量了。
訓練示例:I drink coffee everyday
1、第一個batch:I為中心詞,drink coffee為上下文,即使用單詞drink coffee來預測單詞I,即輸入為X_drink和X_coffee,輸出為X_I,然后訓練上述網絡;
2、第二個batch:drink為中心詞,I和 coffee everyday為上下文,即使用單詞I和coffee everyday,即輸入為X_I和X_coffee、X_everyday,輸出為X_drink,然后訓練上述網絡;
3、第三個batch:coffee為中心詞,I coffee 和 everyday為上下文,同理訓練網絡;
4、第四個batch:everyday為中心詞,drink coffee為上下文,同理訓練網絡。
然后重復上述過程(迭代)3-5次(epoehs)左右即得到最后的結果。
具體的第三個batch的過程可見word2vec是如何得到詞向量的?。得到的結果為[0.23,0.03,0.62,0.12],此時結果是以coffee為中心詞的詞向量,每個位置表示的是對應單詞的概率,例如該詞向量coffee的概率為0.62。
需要注意的是每個batch中使用的權重矩陣都是一模一樣的
2.2 skip-gram 步驟
思路同CBOW, 只是輸入是1個詞,輸出是C個詞;
一般用BP訓練得到參數,預測用一次前向傳播;
- 輸入層:中心詞的embedding表示,
looktable查表【Vd 表,每個詞作為中心詞的表示】得到中心詞的embedding 表示[1300]
例如:你 真 漂亮。根據 真,預測其上下文的情況
- 隱藏層: [W 大小為30010000], Vd 大小,是每個詞作為背景詞的表示
- 輸出層:經過softmax,得到每個詞的概率。
(0.3, 0.5, 0.7,), (0.1,0.9,0.1)
你出現在真之前的概率是0.3,之后是0.1;
真出現在真之前的概率是0.5,之后是0.9
漂亮出現在真之前的概率是0.7,之后是0.1
用蒙特卡洛模擬的方法根據哪些概率值去采樣,就能得到一個具體的上下文。
然后就是優化了,使得輸入的詞之間“真漂亮”之間的概率足夠大。
寫出目標函數:
- T是語料庫單詞的總個數,p(wt+j|wt)是已知當前詞wt,預測周圍詞的總概率對數值
> - 訓練數據: ( input word, output word ) 單詞對input word和output word都是one-hot編碼的向量。最終模型的輸出是一個概率分布。
3 問題優化
word2vec結合了CBOW, skip-gram的方法 訓練得到參數𝑊,
針對CBOW和SKIP-gram,樣本大以及不均衡問題,在計算中做了很多優化;
可以看到,NNLM計算中,兩個問題導致計算量大;
詞表維度大;
softmax計算量大
sofmax歸一化需要遍歷整個詞匯表,
解決方案
將常見的單詞組合(word pairs)或者詞組作為單個“words”來處理。
對高頻次單詞進行抽樣來減少訓練樣本的個數。
層次softmax 實質上生成一顆帶權路徑最小的哈夫曼樹,讓高頻詞搜索路徑變小;
負采樣更為直接,實質上對每一個樣本中每一個詞都進行負例采樣;這樣每個訓練樣本的訓練只會更新一小部分的模型權重,從而降低計算負擔。
事實證明,對常用詞抽樣并且對優化目標采用“negative sampling”不僅降低了訓練過程中的計算負擔,還提高了訓練的詞向量的質量。
3.1 層次softmax
問題:
計算量瓶頸:
詞表大
softmax計算大
每次更新更新參數多,百萬數量級的權重矩陣和億萬數量級的訓練樣本
1)用huffman編碼做詞表示
2)把N分類變成了log(N)個2分類。 如要預測的term(足球)的編碼長度為4,則可以把預測為’足球’,轉換為4次二分類問題,在每個二分類上用二元邏輯回歸的方法(sigmoid);
3)邏輯回歸的二分類中,sigmoid函數導數有很好的性質,𝜎′(𝑥)=𝜎(𝑥)(1?𝜎(𝑥))
4)采用隨機梯度上升求解二分類,每計算一個樣本更新一次誤差函數
從根節點到葉子節點的唯一路徑編碼了這個葉子節點所屬的類別
代價:增強了詞與詞之間的耦合性
Word2Vec中從輸入到隱層的過程就是Embedding的過程。
在word2vec中,約定左子樹編碼為1,右子樹編碼為0,同時約定左子樹的權重不小于右子樹的權重。
在霍夫曼樹中,隱藏層到輸出層的softmax映射不是一下子完成的,而是沿著霍夫曼樹一步步完成的,因此這種softmax取名為"Hierarchical Softmax"。
二元邏輯回歸的方法,即規定沿著左子樹走,那么就是負類(霍夫曼樹編碼1),沿著右子樹走,那么就是正類(霍夫曼樹編碼0)。判別正類和負類的方法是使用sigmoid函數,即:
基于層次softmax的CBOW和SKIP_GRAM梯度推導以及訓練過程
3.1.1 Hierarchical Softmax的缺點與改進
如果我們的訓練樣本里的中心詞𝑤是一個很生僻的詞,那么就得在霍夫曼樹中辛苦的向下走很久.
負采樣就是結局這個問題的
Negative Sampling就是這么一種求解word2vec模型的方法,它摒棄了霍夫曼樹,采用了Negative Sampling(負采樣)的方法來求解,下面我們就來看看Negative Sampling的求解思路
3.2 負采樣
gensim的word2vec 默認已經不采用分層softmax了, 因為𝑙𝑜𝑔21000=10也挺大的;如果huffman的根是生僻字,則分類次數更多;
所以這時候負采樣就派上了用場
Negative Sampling由于沒有采用霍夫曼樹,每次只是通過采樣neg個不同的中心詞做負例,就可以訓練模型,因此整個過程要比Hierarchical Softmax簡單。
不過有兩個問題還需要弄明白:
1)如果通過一個正例和neg個負例進行二元邏輯回歸呢?
2) 如何進行負采樣呢?
基于負采樣的CBOW和SKIP_GRAM梯度推導以及訓練過程
3.2.1負采樣怎么做的
所有的參數都在訓練中調整的話,巨大計算量
負采樣每次讓一個訓練樣本僅僅更新一小部分的權重
現在只更新預測詞以及neg個負例詞的權重
隨機選擇一小部分的negative words(比如選5個negative words)來更新對應的權重。
我們也會對我們的“positive” word進行權重更新(在我們上面的例子中,這個單詞指的是”quick“)。
在論文中,作者指出指出對于小規模數據集,選擇5-20個negative words會比較好,對于大規模數據集可以僅選擇2-5個negative words。
3.2.2 如何選擇negative words
“一元模型分布(unigram distribution)”來選擇“negative words”。
出現頻次越高的單詞越容易被選作negative words。
每個單詞被賦予一個權重,即f(wi), 它代表著單詞出現的頻次。
公式中開3/4的根號完全是基于經驗的,論文中提到這個公式的效果要比其它公式更加出色。
你可以在google的搜索欄中輸入“plot y = x^(3/4) and y = x”,然后看到這兩幅圖(如下圖),仔細觀察x在[0,1]區間內時y的取值,有一小段弧形,取值在y=x函數之上。
負采樣的C語言實現非常的有趣。unigram table有一個包含了一億個元素的數組,這個數組是由詞匯表中每個單詞的索引號填充的,并且這個數組中有重復,也就是說有些單詞會出現多次。那么每個單詞的索引在這個數組中出現的次數該如何決定呢,有公式P(wi)V,也就是說計算出的負采樣概率1億=單詞在表中出現的次數。
有了這張表以后,每次去我們進行負采樣時,只需要在0-1億范圍內生成一個隨機數,然后選擇表中索引號為這個隨機數的那個單詞作為我們的negative word即可。一個單詞的負采樣概率越大,那么它在這個表中出現的次數就越多,它被選中的概率就越大。
3.2.3 為什么需要做采樣:
skip_gram 解決高頻詞訓練樣本太大的問題:
原始訓練樣本:
- 1.對于高頻詞。會產生大量的訓練樣本。
-2.(“fox”, “the”) 這樣的訓練樣本并不會給我們提供關于“fox”更多的語義信息,因為“the”在每個單詞的上下文中幾乎都會出現。
常用詞出現概率很大,樣本數量遠遠超過了我們學習“the”這個詞向量所需的訓練樣本數。
Word2Vec通過“抽樣”模式來解決這種高頻詞問題。
它的基本思想如下:對于我們在訓練原始文本中遇到的每一個單詞,它們都有一定概率被我們從文本中刪掉,而這個被刪除的概率與單詞的頻率有關。
Z(wi)即單詞wi在語料中出現頻率
當Z(wi)<=0.0026時,P(wi)=1.0。當單詞在語料中出現的頻率小于0.0026時,它是100%被保留的,這意味著只有那些在語料中出現頻率超過0.26%的單詞才會被采樣。
當Z(wi)<=0.00746時,P(wi)=0.5,意味著這一部分的單詞有50%的概率被保留。
當Z(wi)<=1.0時,P(wi)=0.033,意味著這部分單詞以3.3%的概率被保留。
4 問題
4.1 詞袋模型到word2vec改進了什么?
詞袋模型(Bag-of-words model)是將一段文本(比如一個句子或是一個文檔)用一個“裝著這些詞的袋子”來表示,這種表示方式不考慮文法以及詞的順序。而
在用詞袋模型時,文檔的向量表示直接將各詞的詞頻向量表示加和。通過上述描述,可以得出詞袋模型的兩個缺點:
詞向量化后,詞與詞之間是有權重大小關系的,不一定詞出現的越多,權重越大。 詞與詞之間是沒有順序關系的。
而word2vec是考慮詞語位置關系的一種模型。通過大量語料的訓練,將每一個詞語映射成一個低維稠密向量,通過求余弦的方式,可以判斷兩個詞語之間的關系,word2vec其底層主要采用基于CBOW和Skip-Gram算法的神經網絡模型。
因此,綜上所述,詞袋模型到word2vec的改進主要集中于以下兩點:
考慮了詞與詞之間的順序,引入了上下文的信息
得到了詞更加準確的表示,其表達的信息更為豐富
5 代碼
import jieba
import os
import re
import pandas as pd
from gensim.models.word2vec import Word2Vec
import gensimclass TrainWord2Vec(object):"""訓練得到一個Word2Vec模型"""def __init__(self, data, stopword, new_path, num_features=100, min_word_count=1, context=4, incremental=False):"""定義變量:param data: 用于訓練胡語料:param stopword: 停用詞表:param num_features: 返回的向量長度:param min_word_count: 最低詞頻:param context: 滑動窗口大小:param incremental: 是否進行增量訓練:param old_path: 若進行增量訓練,原始模型路徑"""self.data = dataself.stopword = stopwordself.num_features = num_featuresself.min_word_count = min_word_countself.context = contextself.incremental = incremental#self.old_path = old_pathself.new_path = new_pathdef clean_text(self):"""采用結巴分詞函數分詞:param corpus: 待分詞的Series序列:return: 分詞結果,list"""# 去除無用字符pattern = re.compile(r'[\sA-Za-z~()()【】%*#+-\.\\\/:=:__,,。、;;“”""''’‘??!!<《》>^&{}|=……]')corpus_ = self.data.apply(lambda s: re.sub(pattern, '', str(s)))# 分詞text = corpus_.apply(jieba.lcut)# 過濾通用詞text = text.apply(lambda cut_words: [word for word in cut_words if word not in self.stopword])return textdef get_model(self, text):"""從頭訓練word2vec模型:param text: 經過清洗之后的語料數據:return: word2vec模型"""model = Word2Vec(text, vector_size=self.num_features, min_count=self.min_word_count, window=self.context)return modeldef update_model(self, text):"""增量訓練word2vec模型:param text: 經過清洗之后的新的語料數據:return: word2vec模型"""model = Word2Vec.load(self.old_path) # 加載舊模型model.build_vocab(text, update=True) # 更新詞匯表model.train(text, total_examples=model.corpus_count, epochs=20) # epoch=iter語料庫的迭代次數;(默認為5) total_examples:句子數。return modeldef train(self):"""主函數,保存模型"""# 加入自定義分析詞庫#jieba.load_userdict("add_word.txt")text = self.clean_text()if self.incremental:model = self.update_model(text)else:model = self.get_model(text)model.train(text, total_examples=model.corpus_count, epochs=20)# 保存模型model.wv.save_word2vec_format(self.new_path, binary=True)if __name__ == '__main__':corpus = []infile = "/dataset/sentence_sim/V1_6/data/qiwei/single"stop_file = "qiwei_vocab_no_in.txt"w2v_file = 'w2v_qiwei.bin'for file_i in ["train.csv", "test.csv"]:corpus.append(pd.read_csv(os.path.join(infile, file_i))["text"])corpus = corpus[0].append(corpus[1])print(len(corpus))stopword = []with open(stop_file, "r") as f:for info in f.readlines():stopword.append(info.strip())new_model_path = w2v_filemodel = TrainWord2Vec(corpus, stopword, new_model_path)model.train()