PLSA模型的再理解以及源码分析

PLSA模型的再理解以及源码分析

之前写过一篇PLSA的博文,其中的收获就是知道PLSA是LSA在概率层面的扩展,知道了PLSA是一种主题模型,知道了PLSA中的参数估计使用的是EM算法。当时我就认为,这样子经典好用的算法,我是会回头再来理解它的,这样子才会有更加深刻的心得。所以有了这篇PLSA模型的再理解。

1. 两种思路解PLSA模型

参考了很多资料,发现大体上有两种解决PLSA模型的思路。下面我们大致说一下它们的思路。

思路一:EM算法中需要更新两个概率

PLSA模型的示意图如下:

其中包括的概率有:

也就是说我们知道的是P(d,w),需要求解的是P(z,d)和P(w,z)。

我们根据P(d,w)构建似然函数,而且我们需要最大化这个似然函数,如下:

需要明确的是,这个似然函数中的自变量是P(z,d)和P(w,z),不巧的是,这两个变量在Log函数的内部是相加的关系,我们知道这为对似然函数的求导带来了很多麻烦。所以我们使用的是EM算法。

EM算法的基本实现思想是:

(1)E步骤:求隐含变量Given当前估计的参数条件下的后验概率。

(2)M步骤:最大化Complete data对数似然函数的期望,此时我们使用E步骤里计算的隐含变量的后验概率,得到新的参数值。

两步迭代进行直到收敛。

具体而言,

在PLSA模型中的E步骤,我们直接用贝叶斯公式计算隐含变量zk在当前参数取值条件下的后验概率,记住是后验概率,如下:

这里的都是已知的,因为我们一开始会对它们设置一个初始值,这样后面的迭代就可以依据于此。使用上面的公式我们计算出了隐含变量的后验概率,然后我们进入M步骤。

M步骤中,我们需要计算complete data的对数似然函数的期望,而且要最大化这个期望。那么首先我们要先计算出这个对数似然期望,如下:

(整篇算法就是这个无法理解,为什么期望是这样子的公式)

注意,这个时候的P(z|d,w)是已知的了,因为在E步骤中已经计算过了,这时候我们需要最大化这个期望,其中包含的自变量有P(z,d)和P(w,z),所以我们需要分别对它们求偏导,令偏导为0:

解方程可得如下的结果:

这个就是我们在M步骤中最大化complete data对似然函数的期望,得到的两个参数的更新公式。

不断重复EM两个步骤,可以得到最终的两个参数P(z,d)和P(w,z)的值。

思路二:EM算法中需要更新3个参数

这个思路和上面思路的区别就在于对P(d,w)的展开公式使用的不同,思路二使用的是3个概率来展开的,如下:

这样子我们后面的EM算法的大致思路都是相同的,就是表达形式变化了,最后得到的EM步骤的更新公式也变化了。当然,思路二得到的是3个参数的更新公式。如下:

你会发现,其实多了一个参数是P(z),参数P(d|z)变化了(之前是P(z|d)),然后P(w|z)是不变的,计算公式也相同。

2. PLSA算法的应用效果

对于数据X,如下:

我们使用思路二的PLSA算法,更新许多次之后的概率为:

iteration 27:

p(d/z)=

0.0000 0.0769 0.2931 0.2934 0.0000 0.0733 0.1467 0.1166 0.0000

0.1952 0.3223 0.0002 0.0000 0.1952 0.0000 0.0000 0.0918 0.1952

p(w/z)=

0.0733 0.0733 0.0000 0.0823 0.2878 0.0000 0.0000 0.1467 0.0000 0.2200 0.1166 0.0000

0.0651 0.0652 0.1302 0.1222 0.0049 0.1302 0.1302 0.0000 0.1302 0.0000 0.0918 0.1302

p(z)=

0.4702 0.5298

迭代了27次,得到了这三个概率。那么这三个概率有什么用呢?

我们知道LSA模型是利用SVD把原来的数据矩阵X降维,使用两个词之间的语义关系被挖掘了出来。两个原始空间中不相关的却语义上相关的词,在LSA模型中是比较相关的,这个是LSA直接的作用。那么PLSA有什么作用呢?

给定一个文档d,我们可以将其分类到一些主题词类别下。

PLSA算法可以通过训练样本的学习得到三个概率,而对于一个测试样本,其中P(w|z)概率是不变的,但是P(z)和P(d|z)都是需要重新更新的,我们也可以使用上面的EM算法,假如测试样本d的数据,我们可以得到新学习的P(z)和P(d|z)参数。这样我们就可以计算:

为什么要计算P(z|d)呢?因为给定了一个测试样本d,要判断它是属于那些主题的,我们就需要计算P(z|d),就是给定d,其在主题z下成立的概率是多少,不就是要计算吗。这样我们就可以计算文档d在所有主题下的概率了。

这样既可以把一个测试文档划归到一个主题下,也可以给训练文档打上主题的标记,因为我们也是可以计算训练文档它们的的。如果从这个应用思路来说,思路一说似乎更加直接,因为其直接计算出来了

如果从上面这个应用的角度来说,LSA和PLSA的目的就是从一群文档集中找到潜在的语义因子latent factors。由于提取到的主题词比文档中的词的数量要少很多,而且我们在学习的过程中不需要知道文档的类型信息,所以说LSA和PLSA是无监督的特征降维方法。

3. PLSA源码分析

知道PLSA算法的理论和作用,我们还要会用,下面提供一个MATLAB源码,人家写的挺简单的,我加了一些分析,帮助理解。

main函数:

% function demo
%
close all; clear all;
load data.mat;
k=2;
[pz pdz pwz pzdw]=plsa(X,k);  % 返回的是4种概率
imagesc(pz); colormap(gray); colorbar;
title('Probability of topics pz');
xlabel('topic z_i'); ylabel('probability');

figure;
imagesc(pdz); colormap(gray); colorbar;
title('Category probability of document pdz');
xlabel('document d_i (pdz_{(i,j)} is the prob. that d_i belongs to z_j)'); ylabel('topic z_j');

figure;
imagesc(pwz); colormap(gray); colorbar;
title('Category probability of word pwz');
xlabel('word w_i (pwz_{(i,j)} is the prob. that w_i belongs to z_j)'); ylabel('topic z_j');

PLSA子函数:

function [pz pdz pwz pzdw]=plsa(X,k)
% function [pz pdz pwz pzdw]=plsa(X,k)
% Return pLSA probability matrix p of m*n matrix X
% X is the document-word co-occurence matrix
% k is the number of the topics--z
% document--collums,word--rows

err=0.0001;
x = X;
[m n]=size(x);  % m个词,n个文档
pz=rand(1,k);  % 随机初始化pz, k个主题词
pz2=pz;
pz2(1)=pz2(1)+2*err;

% 初始化的都是随机值
pdz=rand(k,n);  % 二维
pwz=rand(k,m);  % 二维
pzdw=rand(m,n,k);     % 三维   %initialize
h=0;

deno=zeros(1,k);        %denominator of p(d/z) and p(w/z)
denopzdw=zeros(m,n);       %denominator of p(z/d,w)
numepdz=zeros(k,n);        %numerator of p(d/z)
numepwz=zeros(k,m);        % numerator of p(w/z)
R=sum(sum(x));  %  所有的文档的词数总和

for ki=1:k
    for i=1:m
        for j=1:n
            % P(w|z)和P(d|z)的分母,是每个zk一个值,所以是个向量
            deno(ki)=deno(ki)+x(i,j)*pzdw(i,j,ki); % 叠加,计算的是P(w|z)和P(d|z)的分母
        end
    end
end
% p(d/z)
for ki=1:k
    for j=1:n
        for i=1:m
            % P(d|z)的分子,每个zk和dj一个值,所以是一个矩阵
            numepdz(ki,j)=numepdz(ki,j)+x(i,j)*pzdw(i,j,ki); % 叠加,计算的是P(d|z)的分子
        end
        pdz(ki,j)=numepdz(ki,j)/deno(ki);  % 分子除以分母,一个矩阵
    end
end
% disp(pdz);

% p(w/z)
for ki=1:k
    for i=1:m
        for j=1:n
            % 计算的是P(w|z)的分子,每个zk和wi一个值,一个矩阵
            numepwz(ki,i)=numepwz(ki,i)+x(i,j)*pzdw(i,j,ki);
        end
        % 分子除以分母,矩阵中的每个元素
        pwz(ki,i)=numepwz(ki,i)/deno(ki);
    end
end
% disp(pwz);

% p(z)
for ki=1:k
    pz(ki)=deno(ki)./R;  % 即为前面的分母
end
%denominator of p(z/d,w)
for i=1:m
    for j=1:n
        for ki=1:k
            % 计算的是P(z|d,w)的分母,对zk叠加,每个wi和dj一个值,一个矩阵
            denopzdw(i,j)=denopzdw(i,j)+pz(ki)*pdz(ki,j)*pwz(ki,i);
        end
    end
end
% p(z/d,w)
for i=1:m
     for j=1:n
         for ki=1:k
             % 计算的是P(z|d,w),每个zk和wi和dj一个值,一个三维矩阵
             pzdw(i,j,ki)=pz(ki)*pdz(ki,j)*pwz(ki,i)/denopzdw(i,j);
         end
     end
end
% fprintf('p(z/d,w)=\n');
% disp(pzdw)

%  iteration
iteration=0;
fprintf('iteration:\n');
while abs(pz2(1)-pz(1))>err || abs(pz2(2)-pz(2))>err
    iteration=iteration+1;  % 迭代次数
    deno=zeros(1,k);        %denominator of p(d/z) and p(w/z)
    denopzdw=zeros(m,n);       %denominator of p(z/d,w)
    numepdz=zeros(k,n);        %numerator of p(d/z)
    numepwz=zeros(k,m);        % numerator of p(w/z)
    fprintf('iteration %d:\n',iteration);
    for ki=1:k
        for i=1:m
            for j=1:n
                deno(ki)=deno(ki)+x(i,j)*pzdw(i,j,ki); % 继续迭代,deno回到0,因为需要叠加,不过pzdw就是上次的新值
            end
        end
    end
% p(d/z)
    for ki=1:k
        for j=1:n
            for i=1:m
                numepdz(ki,j)=numepdz(ki,j)+x(i,j)*pzdw(i,j,ki);  % 计算公式同上,只不过此时的pzdw是更新过后的值
            end
            pdz(ki,j)=numepdz(ki,j)/deno(ki);
        end
    end
    fprintf('p(d/z)=\n');
    disp(pdz)
    % p(w/z)
    for ki=1:k
        for i=1:m
            for j=1:n
                numepwz(ki,i)=numepwz(ki,i)+x(i,j)*pzdw(i,j,ki);
            end
            pwz(ki,i)=numepwz(ki,i)/deno(ki);
        end
    end
    fprintf('p(w/z)=\n');
    disp(pwz)

    % p(z)
    pz=pz2;
    for ki=1:k
        pz2(ki)=deno(ki)./R;
    end
    fprintf('p(z)=\n');
    disp(pz2)
    %denominator of p(z/d,w)
    for i=1:m
        for j=1:n
            for ki=1:k
                denopzdw(i,j)=denopzdw(i,j)+pz2(ki)*pdz(ki,j)*pwz(ki,i);
            end
        end
    end
    % p(z/d,w)
    for i=1:m
         for j=1:n
             for ki=1:k
                 pzdw(i,j,ki)=pz2(ki)*pdz(ki,j)*pwz(ki,i)/(denopzdw(i,j)+eps);
             end
         end
    end
end  %end while

return;

参考:

http://www.cnblogs.com/ywl925/p/3552815.html

http://blog.csdn.net/huangxy10/article/details/8091478

http://blog.csdn.net/huangxy10/article/details/8091558

LSA和PLSA 唐克坦

http://blog.csdn.net/puqutogether/article/details/41720073

时间: 2024-09-29 03:15:19

PLSA模型的再理解以及源码分析的相关文章

三元表达式之理解/jquery源码分析之$.inArray实现

每次看到三元表达式就会惶惶然分不清如何读,正如语文中的断句一样,jquery源码中的三元表达式更是不知如何断句. 附jquery中的inArray实现. 大家熟悉jquery的应该都不陌生inArray的用法.在源码实现中,先判断了arr是否存在,如果不存在,则可直接返回-1,否则进行后续判断,当存在indexOf方法时,则直接使用call来调用indexOf方法. 否则继续. 在jquery代码中处处体现代码构建者对于javascript的深刻理解,譬如这里对arr.length的缓存,接下来

《深入理解SPARK:核心思想与源码分析》——SparkContext的初始化(中)

<深入理解Spark:核心思想与源码分析>一书前言的内容请看链接<深入理解SPARK:核心思想与源码分析>一书正式出版上市 <深入理解Spark:核心思想与源码分析>一书第一章的内容请看链接<第1章 环境准备> <深入理解Spark:核心思想与源码分析>一书第二章的内容请看链接<第2章 SPARK设计理念与基本架构> 由于本书的第3章内容较多,所以打算分别开辟三篇随笔分别展现. <深入理解Spark:核心思想与源码分析>一

Nginx源码分析 - Nginx启动以及IOCP模型

Nginx 源码分析 - Nginx启动以及IOCP模型 版本及平台信息 本文档针对Nginx1.11.7版本,分析Windows下的相关代码,虽然服务器可能用linux更多,但是windows平台下的代码也基本相似 ,另外windows的IOCP完成端口,异步IO模型非常优秀,很值得一看. Nginx启动 曾经有朋友问我,面对一个大项目的源代码,应该从何读起呢?我给他举了一个例子,我们学校大一大二是在紫金港校区,到了 大三搬到玉泉校区,但是大一的时候也会有时候有事情要去玉泉办.偶尔会去玉泉,但

Memcached源码分析之线程模型

作者:Calix 一)模型分析 memcached到底是如何处理我们的网络连接的? memcached通过epoll(使用libevent,下面具体再讲)实现异步的服务器,但仍然使用多线程,主要有两种线程,分别是“主线程”和“worker线程”,一个主线程,多个worker线程. 主线程负责监听网络连接,并且accept连接.当监听到连接时,accept后,连接成功,把相应的client fd丢给其中一个worker线程.worker线程接收主线程丢过来的client fd,加入到自己的epol

Netty源码分析--内存模型(上)(十一)

前两节我们分别看了FastThreadLocal和ThreadLocal的源码分析,并且在第八节的时候讲到了处理一个客户端的接入请求,一个客户端是接入进来的,是怎么注册到多路复用器上的.那么这一节我们来一起看下客户端接入完成之后,是怎么实现读写操作的?我们自己想一下,应该就是为刚刚读取的数据分配一块缓冲区,然后把channel中的信息写入到缓冲区中,然后传入到各个handler链上,分别进行处理.那Netty是怎么去分配一块缓冲区的呢?这个就涉及到了Netty的内存模型. 当然,我们在第一节的时

别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】

目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.ClassLoader源码分析 11.自定义类加载器 12.加载类的三种方式 13.总结 14.特别注意 @ 前言 你是否真的理解java的类加载机制?点进文章的盆友不如先来做一道非常常见的面试题,如果你能做出来,可能你早已掌握并理解了java的类加载机制,若结果出乎你的意料,那就很有必要来了解了解j

深入理解OkHttp源码(一)——提交请求

本篇文章主要介绍OkHttp执行同步和异步请求的大体流程.主要流程如下图: 主要分析到getResponseWidthInterceptorChain方法,该方法为具体的根据请求获取响应部分,留着后面的博客再介绍. Dispatcher类 Dispatcher类负责异步任务的请求策略.首先看它的部分定义: public final class Dispatcher { private int maxRequests = 64; private int maxRequestsPerHost = 5

深入理解OkHttp源码(三)——网络操作

这篇博客侧重于了解OkHttp的网络部分,包括Socket的创建.连接,连接池等要点.OkHttp对Socket的流操作使用了Okio进行了封装,本篇博客不做介绍,想了解的朋友可以参考拆轮子系列:拆Okio. OkHttp中关于网络的几个概念 下面的主要翻译自OkHttp的官方文档,查看原文. URL URLs(比如https://github.com/square/okhttp)是HTTP和网络的基础,不止指定了Web上的资源,还指定了如何获取该资源. Address Address(比如gi

快速理解RxJava源码的设计理念

前言 我在看过几篇关于RxJava源码分析的博客后,不知是我的水平有限还是源码过于博大精深,导致花了很长的时间才搞清楚其运行原理.我个人觉得应该有更好的办法来快速剖析理解,于是决定写下本文. 本文适合已经看过一些RxJava源码分析资料的同学,不过没看过也没关系.在看本文时可参考这篇博客:RxJava基本流程和lift源码分析,它说得比较全,在此感谢博主大头鬼Bruce. 一.初探RxJava [以下摘录了RxJava基本流程和lift源码分析] 我们先来看一段最基本的代码,分析这段代码在RxJ