google PLDA + 实现原理及源代码分析
LDA背景
LDA(隐含狄利克雷分布)是一个主题聚类模型,是当前主题聚类领域最火、最有力的模型之中的一个,它能通过多轮迭代把特征向量集合按主题分类。
眼下,广泛运用在文本主题聚类中。
LDA的开源实现有非常多。眼下广泛使用、可以分布式并行处理大规模语料库的有微软的LightLDA,谷歌plda、plda+,sparkLDA等等。以下介绍这3种LDA:
LightLDA依赖于微软自己实现的multiverso參数server,server底层使用mpi或zeromq发送消息。LDA模型(word-topic矩阵)由參数server保存,它为文档训练进程提供參数查询、更新服务。
plda、plda+使用mpi消息通信。将mpi进程分为word、doc俩部分。doc进程训练文档。word进程为doc进程提供模型的查询、更新功能。
spark LDA有两种实现:1.基于gibbs sampling原理和使用GraphX实现的版本号(即spark文档上所说的EMLDAOptimizer and DistributedLDAModel)。2.基于变分判断原理的实现的版本号(即spark文档上的OnlineLDAOptimizer and LocalLDAModel)。Spark LDA的介绍请參见这里。
LightLDA,plda、plda+,spark LDA比較
论可以处理预料库的规模大小,LihgtLDA要远远好于plda和spark LDA
经过測试,在10个server(8核40GB)集群规模下:
LihgtLDA可以处理上亿文档、百万词汇的语料库,可以训练上百万主题数。
这种处理能力使得LihgtLDA可以轻松训练绝大多数语料库。微软号称使用几十机器的集群便能训练Bing搜索引擎爬下数据的十分之中的一个。
相对于LihgtLDA ,plda+可以处理规模小的多。上限是:词汇数目*主题数(模型大小) < 5亿。当语料库规模达到上限后,mpi集群会因内存不够而终止。或由于内存数据频繁切换,迭代速度十分缓慢。尽管plda+对语料库的词汇数目和训练的主题数目非常敏感,但对文档的规模并非非常敏感,在词汇数目和主题数目较小的情况下,1000万级别的文档也可以轻松解决。
spark LDA的GraphX版处理规模衡量标准是图的顶点数据,即(文档数 + 词汇数目)*主题数目,上限是 文档数*主题数 < 50亿(由于词汇数目相对于文档数目往往较小,近似等于 文档数*主题数)。
当超过这个规模后,spark集群进入假死状态。不停有节点出现OOM,直至任务以失败告终。
变分判断实现的spark LDA瓶颈是 词汇数目*主题数目。这个值也就是我们所说的模型大小,上限约1亿。为什么存在这个瓶颈呢?是由于变分判断的实现过程中。模型使用矩阵本地存储,各个分区计算模型的部分值。然后在driver上将矩阵reduce叠加。当模型过大,driver节点的内存就无法承受各个分区发过来的模型。
收敛速度上,LightLDA要远快于plda、plda+和spark LDA。小规模语料库(30万文档。10万词,1000主题)測试。LightLDA : plda+ : spark LDA(graphx) = 1:4:50
为什么各种LDA的可以处理语料库规模的衡量标准不一样呢?这与它们的实现方式有关。不同的LDA有不同的瓶颈,我们这里单讲plda+的源代码解读,其它lda兴许介绍
plda+介绍
plda+是LDA的并行C++实现。由谷歌公司开发。它分布式基础是MPI,使用高度优化的Gibbs sampling算法训练文档。
如图所看到的,plda+将mpi进程组分为2部分——word进程和doc进程。
word进程
word进程存储plda+的模型,使用分布是存储方式。每一个进程仅仅负责模型的一部分。LDA的模型指的是word-topic矩阵(矩阵大小=词汇数目x主题数目)。矩阵每行表示语料库中一word在各个topic中出现的次数。实现上。每行word-topic由向量或数组表示。
word进程负责为doc进程提供word-topic模型參数(即矩阵中的一行,word的各topic出现次数),响应doc进程发送过来的模型更新消息。
它的角色就相当于一个參数server。
doc进程
doc进程是plda+存储文档的地方,也是训练文档的地方。
也採用分布式存储方式,每一个进程仅仅持有语料库的一部分文档。
另外,doc进程还分布是存储doc-topic矩阵,doc-topic矩阵(矩阵大小=文档数目x主题数目)描写叙述语料库各文档doc中的全部词在各个topic下的数目。
doc进程从word进程获取word-topic參数和global_topic參数(每一个主题拥有的词的数目,由word-topic矩阵按行叠加),根据gibbs sampling算法为每一个词的又一次选取主题。将词的主题选取情况发送消息给word进程,通知其更新模型。
doc进程主要由3部分组成:
- 文档集合
文档由分布到该doc进程的全部文档组成,各文档记录了自己的词频信息和自己的各个词主题选取信息。 - local word
local word表示doc进程中所拥有的文档的词汇集合。进程建立了词到文档的反转索引word_inverted_index数据结构,可以使用word来遍历全部拥有该词的文档。 - route(路由表)
route为每一个local word记录了一个mpi进程号,这个进程号即word进程的编号,表示该local word相应的word-topic模型由这个word进程负责。有了route,doc进程发送word-topic请求和更新消息时便知道往哪个word进程发送了.
MPI消息
plda+中总是doc进程向word进程主动发送请消息,word进程响应doc进程的请求消息,不存在其它的消息通信方式,如doc进程和doc进程之间、word进程和word进程之间,就不存在消息通信。
消息通信的类型有以下几种:
- PLDAPLUS_TAG_FETCH
doc进程向word进程发起word-topic參数请求。消息通信的方式是:请求-应答机制,word进程收到请求后向doc进程发送数据。全部的消息通信採用异步发送方式。doc进程与word进程发送消息后无需等待,继续做其它的事。 - PLDAPLUS_TAG_FETCH_GLOBAL
doc进程向word进程请求global_topic參数 - PLDAPLUS_TAG_UPDATE
doc进程向word进程发送模型更新消息,消息中包括doc进程local word中某个词的主题变化情况。 - PLDAPLUS_TAG_DONE
doc进程通知word进程训练结束,word进程退出消息等待的主循环。这类消息在最后一轮迭代完成后,doc进程才发送。
plda+初始化
plda+要求在集群各个节点放置一份完整的语料库文档,各个进程从完整语料库中抽取文档和词来初始化一些重要的数据结构。
由前面介绍可知。word进程和doc进程所需的数据并不同样,因而他们的初始化行为也不一样。
好在mpi可以根据进程号来判断,有差别的让不同进程运行不同的代码。以下是基本的初始化步骤:
全部进程
- 建立word_index_map
word_index_map是语料库词汇到索引的映射结构(c++ map),实现上是先将词汇按字符串顺序排序,把词汇映射到序号。之后,使用索引来代表词汇。由于处理int比string效率要高的多,这个做法可以提升效率。
- 建立word_pw_map (路由表)
word_pw_map是local word到word进程的映射,就是我们上述的route结构。
doc进程
- 将语料库中的文档进行按doc进程轮流分配,分配完成后,便确定了各doc进程拥有的文档集合和local word
- 为文档中的词随机选择主题,形成doc-topic
- 将local word的主题初始主题情况发送给word进程,通知其更新模型。
Word进程
- 按照route路由表为word进程分配word。分配完成后各进程便拥有各自的local word。进行编号,形成本地索引。
建立global_local_word_index_map_,实现语料库中词全局索引到进程中本地索引之间的映射
- 为本地的词建立空的word-topic模型。其初始值为0
- 进行listen,listen是word进程接下来一直进行的事,它在不停地循环等待doc进程的消息。直到接收到全部doc进程的PLDAPLUS_TAG_DONE消息后才退出
- listen在初始化阶段。word进程主要接收的是doc进程发送过来的模型更新消息。形成初始模型。
在兴许迭代阶段。便响应doc进程的各种消息。
word进程listen实现
就像大多数server程序逻辑一样,listen不断运行循环。等待消息,响应消息…
do {
MPI_Recv(recv_buf, num_topics_t, MPI_LONG_LONG,
MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
int tag = status.MPI_TAG;
int source = status.MPI_SOURCE;
switch(tag & 3) { // get the last two bits
case PLDAPLUS_TAG_FETCH : {
MPI_Wait(&req, &status);
map<int, int>::iterator iter =
global_local_word_index_map_.find(tag >> PLDAPLUS_TAG_LENGTH);
if(iter != global_local_word_index_map_.end()) {
const TopicCountDistribution& topic_word = //请求词的topic參数
GetWordTopicDistribution(iter->second); //将请求的词转为本地索引
topic_word.replicate(send_buf);
}
MPI_Isend(send_buf, num_topics_t, MPI_LONG_LONG,
source, tag, MPI_COMM_WORLD, &req); //异步发送消息
break;
}
case PLDAPLUS_TAG_FETCH_GLOBAL : {
ComputeLocalWordLlh(); //tlz
if(first_flag) {
first_flag = false;
} else {
MPI_Wait(&req, &status);
}
const TopicCountDistribution& global_topic =
GetGlobalTopicDistribution();
global_topic.replicate(send_buf);
MPI_Isend(send_buf, num_topics_t, MPI_LONG_LONG,
source, tag, MPI_COMM_WORLD, &req);
break;
}
case PLDAPLUS_TAG_UPDATE : {
int word_index = global_local_word_index_map_[tag >> PLDAPLUS_TAG_LENGTH];
for(int k = 0; k < num_topics_t; ++k) {
IncrementTopic(word_index, k, recv_buf[k]); //更新模型
}
break;
}
case PLDAPLUS_TAG_DONE : {
++count_done; //累加doc进程发来的PLDAPLUS_TAG_DONE消息
break;
}
default : {
// tag error
}
}
} while(count_done < pdnum); //收到全部doc进程的PLDAPLUS_TAG_DONE退出listen
word进程就像參数server。不停地为doc进程提供word-topic和global-topic模型參数
doc进程的词优先顺序训练文档
plda+仍然使用传统的gibbs sampling算法。但它在训练顺序上进行了大胆的创新。
原始的文档训练採用文档优先顺序,即为语料库中每篇文档里的每一个词使用gibbs sampling确定新的主题。
plda+为doc进程中的文档建立了词-文档反转索引(word_inverted_index),local word中的每一个词可以索引一系列含有该词的文档。plda+将训练过程该为local word中的每一个词相应的每篇文档进行gibbs sampling,为该文档中该词选取新的主题。
以下是词(本地索引是local_word_index)训练代码:
// Sample for word local_word_index
for(list<InvertedIndex*>::iterator iter = pldaplus_corpus->word_inverted_index[local_word_index].begin();
iter != pldaplus_corpus->word_inverted_index[local_word_index].end(); ++iter) {
SampleNewTopicForWordInDocumentWithDistributions(
(*iter)->word_index_in_document,
(*iter)->document_ptr, train_model,
topic_word, global_topic, delta_topic);
}
迭代器iter是InvertedIndex结构的指针。它会遍历指向local_word_index相应的全部InvertedIndex结构,InvertedIndex结构会记录local_word_index相应的文档(document_ptr)。以及该词在文档中的编号(word_index_in_document)。
gibbs sampling过程会为该文档该词选择新的主题。它事实上并不复杂,仅仅是根据採用公式通过word-topic(代码中是topic_word)。global_topic,doc-topic參数,该词新的主题,并把主题更新信息记录在delta_topic数组中。
plda+按词优先训练文档中的词有以下几个优势:
1. 降低了local word主题更新信息的存储,当local_word_index相应的全部文档处理完成,doc进程为该词发送模型更新消息。训练该词相应文档的过程仅仅须要一个delta_topic数组的空间存储就可以。若按文档优先顺序来sampling,要将取样结果更新到模型必定要经历下列方式之中的一个:每取样一个词,就发送模型更新消息。这会导致大量的通信。一篇文档训练完成或全部文档训练完成才发送更新模型消息,这须要记录全部词的主题更新信息。因而会带来大量存储开销。
2. 模型更新速度适宜,文档优先顺序中全部文档处理完成再发送消息,尽管发送的更新消息量非常小,但对模型更新来说,这是一个大同步,会导致模型的收敛速度便慢,甚至会出现抖动。
3. 词优先训练文档每处理一个word发送更新消息。使得消息发送(异步发送)与文档训练交叉运行,使得通信与计算重叠。提高了系统的吞吐率。
global_topic參数获取
void PLDAPLUSModelForPd::GetGlobalTopic(int64* global_topic) {
int num_topics_t = num_topics();
MPI_Status status;
MPI_Send(buf_, 0, MPI_LONG_LONG, 0, PLDAPLUS_TAG_FETCH_GLOBAL, MPI_COMM_WORLD);
MPI_Recv(global_topic, num_topics_t, MPI_LONG_LONG,
0, PLDAPLUS_TAG_FETCH_GLOBAL, MPI_COMM_WORLD, &status);
for(int dest = 1; dest < pwnum_; ++dest) {
MPI_Send(buf_, 0, MPI_LONG_LONG,
dest, PLDAPLUS_TAG_FETCH_GLOBAL, MPI_COMM_WORLD);
MPI_Recv(buf_, num_topics_t, MPI_LONG_LONG,
dest, PLDAPLUS_TAG_FETCH_GLOBAL, MPI_COMM_WORLD, &status);
for(int k = 0; k < num_topics_t; ++k) {
global_topic[k] += buf_[k];
}
}
}
doc进程依次向各个word进程发送PLDAPLUS_TAG_FETCH_GLOBAL消息,将word进程响应的局部global_topic累加,形成global_topic。为什么说是局部呢?由于global_topic是每一个主题拥全部词的总数目,每一个word进程仅仅能统计它自己拥有的那一部分模型。
由于进程数目本来较小,plda+为了实现简单,doc进程使用同步方式进行通信。
word-topic參数获取和消息的异步机制
// Init fetching pool
for(int i = 0; i < num_words_t && pool_size < PLDAPLUS_MAX_POOL_SIZE; ++i) {
model_pd_->GetTopicWordNonblocking(i, recv_buf + pool_size * num_topics_t,
request_pool + pool_size);
word_index_pool[pool_size] = i;
++pool_size;
}
for(int i = pool_size; i < num_words_t; ++i) {
// Wait for fetching any topic word distribution
MPI_Waitany(PLDAPLUS_MAX_POOL_SIZE, request_pool, &request_index, &status);
// Redirect topic word pointer
topic_word = recv_buf + request_index * num_topics_t;
memset(delta_topic, 0, sizeof(*delta_topic) * num_topics_t);
int local_word_index = word_index_pool[request_index];
model_pd_->UpdateWordCoverTopic(local_word_index, topic_word);
// Sample for word local_word_index
for(list<InvertedIndex*>::iterator iter = pldaplus_corpus->word_inverted_index[local_word_index].begin();
iter != pldaplus_corpus->word_inverted_index[local_word_index].end(); ++iter) {
SampleNewTopicForWordInDocumentWithDistributions(
(*iter)->word_index_in_document,
(*iter)->document_ptr, train_model,
topic_word, global_topic, delta_topic);
}
// Update for word local_word_index
model_pd_->UpdateTopicWord(local_word_index, delta_topic);
for(int k = 0; k < num_topics_t; ++k) {
global_topic[k] += delta_topic[k];
}
// Fetch next topic word distribution
model_pd_->GetTopicWordNonblocking(i, topic_word, request_pool + request_index);
word_index_pool[request_index] = i;
}
doc进程使用异步的MPI消息请求word_topic參数,异步方式即请求后不原地等待word进程的响应。plda+的实现例如以下:
1. plda+在为local word请求word-topic參数时,最開始发出一池子(100个)的word_topic请求,将他们放到消息池中,监控池子中响应的到来。
2. 每到来一个响应,便根据响应消息带来的word_topic參数训练该词相应的一系列文档。训练完成后,发送该词的模型更新消息。
该词处理完成,在消息池中占的位置也可被占用。
3. 发送下一个local word的word-topic參数请求,用前一个词在消息池中的位置来存放请求的消息。进行监控。直到local word全部训练完成。
plda+ loglikelihood计算问题
使用过plda+的同学可能发现。plda+的loglikelihood的值居然是随迭代次数添加而递减的,这严重不符合likelihood的定义。随着迭代加深。似然函数的值应该不断逼近最大值。
笔者按照LihgtLda的计算方式又一次实现了plda+的loglikelihood计算。
參见:https://github.com/tanglizhe1105/plda
我们把loglikelihood的计算拆成2部分:doc-topic矩阵和word-topic模型矩阵。当中word-topic为了方便计算又分为了word loglikelihood和normalized loglikelihood。拆分的理由请见:https://github.com/Microsoft/lightlda/issues/9
因而,word-topic的loglikelihood为 word +normalized loglikelihood。
总的loglikelihood = doc + word + normalized loglikelihood。
作者介绍
唐黎哲。国防科学技术大学 并行与分布式计算国家重点实验室(PDL)硕士,从事spark、图计算、LDA(主题分类)研究。欢迎交流,请多不吝赐教。
邮箱:tanglizhe1105@qq.com
google PLDA + 实现原理及源代码分析的更多相关文章
- 深入理解Spark 2.1 Core (十一):Shuffle Reduce 端的原理与源代码分析
http://blog.csdn.net/u011239443/article/details/56843264 在<深入理解Spark 2.1 Core (九):迭代计算和Shuffle的原理 ...
- Spark MLlib LDA 基于GraphX实现原理及源代码分析
LDA背景 LDA(隐含狄利克雷分布)是一个主题聚类模型,是当前主题聚类领域最火.最有力的模型之中的一个,它能通过多轮迭代把特征向量集合按主题分类.眼下,广泛运用在文本主题聚类中. LDA的开源实现有 ...
- spawn-fcgi原理及源代码分析
spawn-fcgi是一个小程序,作用是管理fast-cgi进程,功能和php-fpm类似,简单小巧,原先是属于lighttpd的一部分.后来因为使用比較广泛.所以就迁移出来作为独立项目了.本文介绍的 ...
- Tomcat7.0源代码分析——启动与停止服务原理
前言 熟悉Tomcat的project师们.肯定都知道Tomcat是怎样启动与停止的. 对于startup.sh.startup.bat.shutdown.sh.shutdown.bat等脚本或者批处 ...
- Parrot源代码分析之海贼王
我们的目的是找到speedup-example在使用Parrot加速的原因,假设仅仅说它源于Context Switch的降低,有点简单了,它究竟为什么降低了?除了Context Switch外是否还 ...
- Android应用Activity、Dialog、PopWindow、Toast窗体加入机制及源代码分析
[工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处.尊重劳动成果] 1 背景 之所以写这一篇博客的原因是由于之前有写过一篇<Android应用setCont ...
- MonkeyRunner源代码分析之启动
在工作中由于要追求完毕目标的效率,所以很多其它是强调实战.注重招式.关注怎么去用各种框架来实现目的.可是假设一味仅仅是注重招式.缺少对原理这个内功的了解,相信自己非常难对各种框架有更深入的理解. 从几 ...
- android-plugmgr源代码分析
android-plugmgr是一个Android插件加载框架,它最大的特点就是对插件不需要进行任何约束.关于这个类库的介绍见作者博客,市面上也有一些插件加载框架,但是感觉没有这个好.在这篇文章中,我 ...
- UiAutomator源代码分析之UiAutomatorBridge框架
上一篇文章<UIAutomator源代码分析之启动和执行>我们描写叙述了uitautomator从命令行执行到载入測试用例执行測试的整个流程.过程中我们也描写叙述了UiAutomatorB ...
随机推荐
- 第二篇:python基础_2
本篇内容 数字 字符串 元祖 字典 列表 集合 for循环 二进制 字符编码 文件处理 一.数字 1.int(整型) 在32位机器上,整数的位数为32位,取值范围为-2**31-2**31-1,即-2 ...
- BZOJ 1051:[HAOI2006]受欢迎的牛(强连通分量)
受欢迎的牛Description每一头牛的愿望就是变成一头最受欢迎的牛.现在有N头牛,给你M对整数(A,B),表示牛A认为牛B受欢迎. 这种关系是具有传递性的,如果A认为B受欢迎,B认为C受欢迎,那么 ...
- 【bzoj3295】[Cqoi2011]动态逆序对 线段树套SBT
题目描述 对于序列A,它的逆序对数定义为满足i<j,且Ai>Aj的数对(i,j)的个数.给1到n的一个排列,按照某种顺序依次删除m个元素,你的任务是在每次删除一个元素之前统计整个序列的逆序 ...
- 12小时制时间&&24小时制时间
今天在获取时间的时候发现,插入到数据库中的时间,其中下午的时间直接显示01,02的样子...查了下资料发现了端倪, java.text.SimpleDateFormat f=new java.text ...
- Codeforces #1063C Dwarves, Hats and Extrasensory Abilities
题目大意 交互题. 输出平面上的一个点的坐标,交互程序给这个点染色(白或黑). 如此重复 $n$ 次($ 1\le n \le 30$). 要求输出的 $n$ 个点各不相同,并且不论交互程序怎样给它们 ...
- linux系统初始化——启动脚本是如何工作的
启动脚本是如何工作的 Linux 使用的是基于 运行级(run-levels) 概念的称为 SysVinit 的专用启动工具.它在不同的系统上可能是完全不一样的,所以不能认为一个脚本在某个 Linux ...
- Linux SCRT本地免秘钥登录远程机器
一.生成本地公钥和私钥 1.1.创建公钥 步骤:工具->创建公钥 然后下一步: 秘钥类型选择RSA: 然后下一步: 密钥位长度:默认是1024,我这边是2048 然后下一步: 密钥格式: 然后点 ...
- Long.ValueOf("String") Long.parseLong("String") 区别 看JAVA包装类的封箱与拆箱
IP地址类型转换原理: 将一个点分十进制IP地址字符串转换成32位数字表示的IP地址(网络字节顺序). 将一个32位数字表示的IP地址转换成点分十进制IP地址字符串. 1.Long.ParseLong ...
- 【CF1015F】Bracket Substring(字符串DP)
题意:给定一个只由左右括号组成的字符串s,问长度为2*n的包含它的合法括号序列方案数,答案对1e9+7取模 1≤n≤100,1≤|s|≤200 思路:暴力预处理出s的每个前缀[0..i]后加左右括号分 ...
- linux2.4内核调度
进程调度需要兼顾3种进程:交互进程,批处理进程,实时进程,在设计一个进程调度机制时需要考虑具体问题 (1)调度时机? 答:进程在用户空间可以pause()或者让内核设置进程为睡眠状态,以此调度,调度还 ...