本文翻譯自做者在medium發佈的一篇推文,這裏是原文連接html
本文是 Word Embedding 系列的第一篇。本文適合中級以上的讀者或者訓練過word2vec/doc2vec/Paragraph Vectors的讀者閱讀,但別擔憂,我將在接下來的推文中介紹理論以及背景知識,並聯繫論文講解代碼是如何實現的。java
我會盡力不把各位讀者引導到一大堆冗長而又沒法讓人真正理解的教程中,最後以放棄了結(相信我,我也是網上諸多教程的受害者)。我想咱們能夠一塊兒從代碼層面來了解word2vec,這樣咱們能夠知道如何設計並實現咱們本身的word embedding 和language model.git
若是您曾經本身訓練過word vectors,會發現儘管使用相同的數據進行訓練,但每次訓練獲得的模型和詞向量表示都不同。這是由於在訓練過程當中引入了隨機性所致。讓咱們一塊兒來從代碼中找到這些隨機性是如何引入的,以及如何消除這種隨機性。我將用DL4j的Paragraph Vectors的實現來展現代碼。若是您想看其餘包的實現,能夠看gensim的doc2vec,它有相同的實現方法。github
咱們知道在訓練最初,模型各參數和詞向量表示會隨機初始化,這裏的隨機性是由seed控制實現的。所以,當咱們把seed設爲0,咱們在每次訓練中會獲得徹底相同的初始化。這裏來看seed是如何影響初始化的,syn0是模型權重。算法
// Nd4j 設置有關生成隨機數的seed
Nd4j.getRandom().setSeed(configuration.getSeed());
// Nd4j 爲 syn0 初始化一個隨機矩陣
syn0 = Nd4j.rand(new int[] {vocab.numWords(), vectorLength}, rng).subi(0.5).divi(vectorLength);複製代碼
若是咱們使用PV-DBOW算法訓練Paragraph Vectors,在訓練迭代中,單詞會從窗口中隨機取得並計算、更新模型。可是這裏的隨機在代碼實現中並非真正的隨機。api
// nextRandom 是一個 AtomicLong,並被threadId初始化
this.nextRandom = new AtomicLong(this.threadId);複製代碼
nextRandom在trainSequence(sequence, nextRandom, alpha);
被用到,在trainSequence
中,nextRandom.set(nextRandom.get() * 25214903917L + 11);
若是咱們更加深刻到每一個訓練的步驟,咱們會發現nextRandom產生於相同的步驟及方法,即進行固定的數學運算(到這裏和這裏瞭解爲何這樣作),因此nextRandom
是依賴於threadId
的數字,而threadId
是0,1,2,3,...因此這裏咱們實際上再也不有隨機性。bash
由於對文本的處理是一項耗時的工做,因此進行並行tokenization能夠提升性能,但訓練的一致性將不能獲得保證。並行處理下,提供給每一個thread進行訓練的數據將出現隨機性。從代碼中能夠看到,若是咱們將allowParallelBuilder
設爲false
,進行tokenization的runnable
將阻塞其餘thread直到tokenization結束,從而保持輸入訓練數據的一致性。oracle
if (!allowParallelBuilder) {
try {
runnable.awaitDone();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}複製代碼
該隊列是一個LinkedBlockingQueue
,這個隊列從迭代器中取出訓練文本,而後提供給各個線程進行訓練。由於各個線程請求數據的時間能夠是任意的,因此在每次訓練中,每一個線程獲得的數據也是不同的。請看這裏的代碼具體實現。dom
// 初始化一個 sequencer 來提供數據給每一個線程
val sequencer = new AsyncSequencer(this.iterator, this.stopWords);
// 每一個線程使用同一個 sequencer
// worker是咱們設置的進行訓練的線程數
for (int x = 0; x < workers; x++) {
threads.add(x, new VectorCalculationsThread(x, ..., sequencer);
threads.get(x).start();
}
// 在sequencer中 初始化一個 LinkedBlockingQueue buffer
// 同時保持該buffer的size在[limitLower, limitUpper]
private final LinkedBlockingQueue<Sequence<T>> buffer;
limitLower = workers * batchSize;
limitUpper = workers * batchSize * 2;
// 線程從buffer中讀取數據
buffer.poll(3L, TimeUnit.SECONDS);複製代碼
因此,若是咱們將worker
設爲1,即採用單線程進行訓練,那麼每次訓練咱們將獲得相同順序的數據。這裏須要注意的是,若是採用單線程,訓練的速度將會大幅下降。性能
爲了將隨機性排除,咱們須要作如下:
seed
設爲0;allowParallelTokenization
設爲false
;worker
設爲1。最終,咱們的訓練代碼將會像:
ParagraphVectors vec = new ParagraphVectors.Builder()
.minWordFrequency(1)
.labels(labelsArray)
.layerSize(100)
.stopWords(new ArrayList<String>())
.windowSize(5)
.iterate(iter)
.allowParallelTokenization(false)
.workers(1)
.seed(0)
.tokenizerFactory(t)
.build();
vec.fit();複製代碼
若是您以爲對上述內容不理解,那麼別擔憂,我將在以後的推文中聯繫代碼和論文,詳細解釋word embedding以及language model的技術。