【译】如何在每次训练中都获得相同的word2vec/doc2vec/Paragraph Vectors

本文翻译自做者在medium发布的一篇推文,这里是原文连接html

本文是 Word Embedding 系列的第一篇。本文适合中级以上的读者或者训练过word2vec/doc2vec/Paragraph Vectors的读者阅读,但别担忧,我将在接下来的推文中介绍理论以及背景知识,并联系论文讲解代码是如何实现的。java

我会尽力不把各位读者引导到一大堆冗长而又没法让人真正理解的教程中,最后以放弃了结(相信我,我也是网上诸多教程的受害者)。我想咱们能够一块儿从代码层面来了解word2vec,这样咱们能够知道如何设计并实现咱们本身的word embedding 和language model.git

若是您曾经本身训练过word vectors,会发现尽管使用相同的数据进行训练,但每次训练获得的模型和词向量表示都不同。这是由于在训练过程当中引入了随机性所致。让咱们一块儿来从代码中找到这些随机性是如何引入的,以及如何消除这种随机性。我将用DL4jParagraph 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 算法

若是咱们使用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

由于对文本的处理是一项耗时的工做,因此进行并行tokenization能够提升性能,但训练的一致性将不能获得保证。并行处理下,提供给每一个thread进行训练的数据将出现随机性。从代码中能够看到,若是咱们将allowParallelBuilder设为false,进行tokenization的runnable将阻塞其余thread直到tokenization结束,从而保持输入训练数据的一致性。oracle

if (!allowParallelBuilder) {
    try {
        runnable.awaitDone();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException(e);
    }
}复制代码

为各个thread提供训练数据的队列

该队列是一个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,即采用单线程进行训练,那么每次训练咱们将获得相同顺序的数据。这里须要注意的是,若是采用单线程,训练的速度将会大幅下降。性能

总结

为了将随机性排除,咱们须要作如下:

  1. seed设为0;
  2. allowParallelTokenization设为false;
  3. 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的技术。

参考

  1. Deeplearning4j, ND4J, DataVec and more - deep learning & linear algebra for Java/Scala with GPUs + Spark - From Skymind http://deeplearning4j.org https://github.com/deeplearning4j/deeplearning4j
  2. Java™ Platform, Standard Edition 8 API Specification https://docs.oracle.com/javase/8/docs/api/
相关文章
相关标签/搜索