本文翻译自做者在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的技术。