【譯】如何在每次訓練中都得到相同的word2vec/doc2vec/Paragraph Vectors
本文翻譯自作者在medium釋出的一篇推文,這裡是原文連結
本文是Word Embedding 系列的第一篇。本文適合中級以上的讀者或者訓練過word2vec" target="_blank" rel="nofollow,noindex">word2vec/doc2vec/Paragraph Vectors 的讀者閱讀,但別擔心,我將在接下來的推文中介紹理論以及背景知識,並聯絡論文講解程式碼是如何實現 的。
我會盡力不把各位讀者引導到一大堆冗長而又無法讓人真正理解的教程中,最後以放棄告終(相信我,我也是網上諸多教程的受害者)。我想我們可以一起從程式碼層面來了解word2vec, 這樣我們可以知道如何設計並實現我們自己的word embedding 和language model.
如果您曾經自己訓練過word vectors,會發現儘管使用相同的資料進行訓練,但每次訓練得到的模型和詞向量表示都不一樣。這是因為在訓練過程中引入了隨機性所致。讓我們一起來從程式碼中找到這些隨機性是如何引入的,以及如何消除這種隨機性。我將用DL4j的Paragraph Vectors 的實現 來展示程式碼。如果您想看其他包的實現,可以看gensim的doc2vec,它有相同的實現方法。
隨機性從哪裡來
模型權重和詞向量的初始化
我們知道在訓練最初,模型各引數和詞向量表示會隨機初始化,這裡的隨機性是由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 演算法
如果我們使用PV-DBOW演算法訓練Paragraph Vectors,在訓練迭代中,單詞會從視窗中隨機取得並計算、更新模型。但是這裡的隨機在程式碼實現 中並不是真正的隨機。
// 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,...所以這裡我們實際上不再有隨機性。
並行tokenization
因為對文字的處理是一項耗時的工作,所以進行並行tokenization可以提高效能,但訓練的一致性將不能得到保證。並行處理下,提供給每個thread進行訓練的資料將出現隨機性。從程式碼
中可以看到,如果我們將allowParallelBuilder
設為false
,進行tokenization的runnable
將阻塞其他thread直到tokenization結束,從而保持輸入訓練資料的一致性。
if (!allowParallelBuilder) { try { runnable.awaitDone(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } }複製程式碼
為各個thread提供訓練資料的佇列
該佇列是一個LinkedBlockingQueue
,這個佇列從迭代器中取出訓練文字,然後提供給各個執行緒進行訓練。因為各個執行緒請求資料的時間可以是任意的,所以在每次訓練中,每個執行緒得到的資料也是不一樣的。請看這裡的程式碼具體實現
。
// 初始化一個 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的技術。