Topic Model 的复杂度计算(时间和空间)

算法复杂度包含两个方面,时间复杂度和空间复杂度,也就是常称的计算复杂度和内存空间占用两方面。下面先说主题模型的计算复杂度,再说内存占用。

时间复杂度

    本来这个问题挺简单的,但是有搞统计的同学不知道CS里面的复杂度是怎么计算,就写到这里吧。本文比较三个topic model时间复杂度的计算方法,分别是LDA,Biterm,和hdLDA。

    时间复杂度的计算很好掌握,就是把算法中的最基本操作作为单元操作,设定其时间复杂度为O(1)。最简单的可以是赋值、加减乘除等操作;复杂的也可以将之前文章中讲到的一次Gibbs sampling抽样(一个变量t+1时刻的值,由其它变量t时刻的值抽得),作为一个单元操作。最后看该操作总共执行了多少次,若执行了M次,则算法的时间复杂度就是O(M)。

    在Topic Model中,单元操作可看作是一次Gibbs抽样(其实可以更细粒度到加减乘除运算,但是本次所对比的LDA和Biterm模型的单次抽样复杂度相同,所以不需要比较更细粒度的运算)。那么计算时间复杂度就是计算该抽样所执行的次数。所以接下来就是看一下套在单元计算外面的计算步骤。

int last_iter = liter;
for (liter = last_iter + 1; liter <= niters + last_iter; liter++) {
    printf("Iteration %d ...\n", liter);
    for (int m = 0; m < M; m++) {
        for (int n = 0; n < ptrndata->docs[m]->length; n++) {
            int topic = sampling(m, n);
            z[m][n] = topic;
        }
    }
}

    由上面代码可以看出,最外层是一个迭代次数 Niter , 内层是文档个数 M ,也即 ND ,再内层是相应文档的长度(不重复单词的个数)。由于每一篇文档的长度不同,因此取平均值 l¯ 。然后再看一下sampling()代码:

int model::sampling(int m, int n) {
    // remove z_i from the count variables
    int topic = z[m][n];
    int w = ptrndata->docs[m]->words[n];
    nw[w][topic] -= 1;
    nd[m][topic] -= 1;
    nwsum[topic] -= 1;
    ndsum[m] -= 1;

    double Vbeta = V * beta;
    double Kalpha = K * alpha;    
    // do multinomial sampling via cumulative method
    for (int k = 0; k < K; k++) {
        p[k] = (nw[w][k] + beta) / (nwsum[k] + Vbeta) *
            (nd[m][k] + alpha) / (ndsum[m] + Kalpha);
    }
    // cumulate multinomial parameters
    for (int k = 1; k < K; k++) {
    p[k] += p[k - 1];
    }
    double u = ((double)random() / RAND_MAX) * p[K - 1];
    for (topic = 0; topic < K; topic++) {
        if (p[topic] > u) {
            break;
        }
    }
    nw[w][topic] += 1;
    nd[m][topic] += 1;
    nwsum[topic] += 1;
    ndsum[m] += 1;     
    return topic;
}

    可以看到核心计算过程也就是在为p[k]赋值的过程,而k一共有K个。所以最终的复杂度也就是: O(NiterNDKl¯)

    那么计算Biterm topic model也就很简单了。由于该模型的主题分布不在文档上,而是在biterm上,而biterm的计算是 l¯(l¯1)/2 。因此时间复杂度就是 O(NiterKl¯(l¯1)/2)

    对于hdLDA模型来说,其抽样步骤比较繁琐。具体代码为:

printf("Sampling %d iterations!\n", niters);
int last_iter = liter;
for (liter = last_iter + 1; liter <= niters + last_iter; liter++) {
     for (int m = 0; m < M; m++) {
         for (int n = 0; n < ptrndata->contents[m]->length; n++) {
            int topic = sampling_step_one(m, n);
            z[m][n] = topic;
         }
     }
     for (int m = 0; m < M; m++) {
         for (int c = 0; c < ptrndata->comments[m]->C; c++) {
             xdc[m][c] = sampling_step_two_x(m, c);
             ydc[m][c] = sampling_step_two_y(m, c);
         }
     }
     for (int m = 0; m < M; m++) {
         for (int c = 0; c < ptrndata->comments[m]->C; c++) {
             sampling_step_three(m, c);
         }
     }
}

    可以看到每一个sampling step在外层都共享了 NiterND 次循环(其中用 NDM )。而sampling_step_one()接下来的循环次数为:

ptrndata->contents[m]->length

这与LDA代码部分相似。

    而sampling_step_two()和sampling_step_three()则各自循环下面次数,也就是每篇正文的跟帖条数:

ptrndata->comments[m]->C

由于不是每篇正文的跟帖数目都是相同的,因此这里也取平均值,记作 C¯

    接下来再看看每一个抽样步骤中的时间复杂度。sampling_step_one()的代码是这样的:

int model::sampling_step_one(int m, int n) {
    int topic = z[m][n];
    int w = ptrndata->contents[m]->words[n];
    ndk[m][topic] -= 1;
    nkv[topic][w] -= 1;
    ndksum[m] -= 1;
    nkvsum[topic] -= 1;
    for (int k = 0; k < K; k++) {
    p[k] = exp(log(ndk[m][k] + gdk[m][k] + alpha) - log((ndksum[m] + gdksum[m] + Kalpha)) + log((nkv[k][w] + gkv[k][w] + betaFM[k][w])) - log((nkvsum[k] + gkvsum[k] + VbetaF[k])));
    }
    // cumulate multinomial parameters
    for (int k = 1; k < K; k++) {
        p[k] += p[k - 1];
    }
    // scaled sample because of unnormalized p[]
    double u = ((double)rand() / RAND_MAX) * p[K - 1];
    for (topic = 0; topic < K; topic++) {
        if (p[topic] >= u) {
            break;
        }
    }
    if (topic == K){
        topic = K - 1;
    }
    ndk[m][topic] += 1;
    nkv[topic][w] += 1;
    ndksum[m] += 1;
    nkvsum[topic] += 1;
    return topic;
}

可以看到其中核心计算步骤迭代了K次,与LDA和Biterm相同。因此对于sampling_step_one()步来说,其具有复杂度 NiterNDl¯

    对于sampling_step_two_x()和sampling_step_two_y()来说,其代码非常相似,节省篇幅起见,下面只给出step_two_x()的代码:

int model::sampling_step_two_x(int m, int c){
    int topic = 0;
    for (int i = 0; i < ptrndata->comments[m]->commlines[c]->length; i++) {
        if(I[m][c][i]) {
            topic = xdc[m][c];
            gdk[m][topic] -= 1;
            gdksum[m] -= 1;
            gkv[topic][ptrndata->comments[m]->commlines[c]->words[i]] -= 1;
            gkvsum[topic] -= 1;
        }
    }
    for (int k = 0; k < K; k++) {
        p[k] = log(1.0);
        double left = log(ndk[m][k] + gdk[m][k] + alpha) - log(ndksum[m] + gdksum[m] + Kalpha);
        for (int i = 0; i < ptrndata->comments[m]->commlines[c]->length; i++) {
            if (I[m][c][i]){
            int w = ptrndata->comments[m]->commlines[c]->words[i];
            p[k] += (log(nkv[k][w] + gkv[k][w] + betaFM[k][w]) - log(nkvsum[k] + gkvsum[k] + VbetaF[k]));
        }
    }
     p[k] += left;
     p[k] = exp(p[k]);
    }
    // cumulate multinomial parameters
    for (int k = 1; k < K; k++) {
        p[k] += p[k - 1];
    }
    double u = ((double)rand() / RAND_MAX) * p[K - 1];
    for (topic = 0; topic < K; topic++) {
        if (p[topic] >= u) {
            break;
        }
    }
    if (topic == K){
        topic = K - 1;
    }
    for (int i = 0; i < ptrndata->comments[m]->commlines[c]->length; i++) {
        if(I[m][c][i]) {
            gkv[topic][ptrndata->comments[m]->commlines[c]->words[i]] += 1;
            gkvsum[topic] += 1;
            gdk[m][topic] += 1;
            gdksum[m] += 1;
        }
    }
    return topic;
}

可以看出这一步中,计算时间复杂度为 C¯lc¯ ,其中 lc¯ 为跟帖的平均长度。同理sampling_step_two_y()也具有相同的复杂度。

    而sampling_step_three()的复杂度为 lc¯ 。因此hdLDA的时间复杂度为: NiterND(Kl¯+2KC¯lc¯+C¯lc¯)

空间复杂度

    空间复杂度就是程序在计算时,需要放在内存中用于存储中间结果的矩阵、向量等数据类型所占的空间。一般省略掉无关重要的变量,如标记变量等。而只关注与算法核心相关的内存空间占用(好像更贴近伪代码)。

    在LDA中,算法需要维护三个矩阵: θ,ϕ 和字典映射矩阵。它们分别为文档在主题上的分布矩阵,规模为 NDK ;主题在词上的分布,规模为 WK 和文档次的编号映射矩阵,规模为 NDl¯ 。因此其空间复杂度就是 NDK+WK+NDl¯ 了。

    Biterm因为没有了文档的概念,没有了矩阵 θ , 而 ϕ 又变成了主题在biterm上的分布,因而复杂度相对于LDA有一点调整,是 K+WK+NDl¯(l¯1)/2

    hdLDA所需要的不重要变量特别多,但是如果不计入核心算法的话,有 ϕ 矩阵,规模为 WK θ 矩阵,规模为 NDK ψ 矩阵,规模为 JW ,其中 K 表示formal topic 的个数, J 表示leisure topic 的个数;以及文档保存矩阵ptrndata(结构体),规模为 NDC¯lc¯ 因此空间复杂度为 WK+NDK+JW+NDC¯lc¯

发现好简单啊。

你可能感兴趣的:(复杂度,主题模型)