Word2vec中的Huffman树

  • 最近因为某些原因,要实现一下word2vec,但是想试一下C实现(然后开始怀疑人生,真的看得懂C,但是python写多了,写不出来C了,很别扭的感觉…),正好再次体验一下它源码,这篇是单独将其中Huffman算法提出来讨论一番。
  • 分三部分来说一下:(1)算法流程 (2)自问自答的方式拿些问题出来提一下 (3)自己注释好的的代码贴出来

算法流程

  1. 数据的数据结构:三个数组,count数组初始化。
  2. 构建树结构。
  3. 构建完毕后编码。
  4. 释放树结构。

自问自答

  1. 问:如果让你用C构建一棵树?
    答:我的第一想法是,来吧,链表走起…然后源码放出了三个数组最为基本结构????

  2. 注意一个细节:
    代码在申请完那三个数组的地址后,最后都free了,所以整个结构信息的存储另有玄机。

  3. 为什么源码要用数组?
    我最开始这样傻傻的想?现在偏向于“简便”这个解释,当然也是因为Huffuman树编码本身就不需要维护复杂的树结构,只需要根据树结构编完码就够了,为什么?因为他有这样一个性质,对于某个词来说,从根节点到它的路径是唯一的,意味着我只需要关心这条路径就够了。

  4. 三个数组分别是:count,binary,parent_code,各自有什么用?
    (细节,词是按从大到小的顺序排列好的)

  • count:前半部分vocab_size大小的部分=下坐标下,对应词库中词的频率,剩下的空间全部初始化为:1e15。但是在遍历过程中,两个节点合并出的新结点的count也会按顺序往后存入。总之:这个是用来存储频率的。
  • binary : 用来编码的,两个节点,负类编码为1,分到了左边,且约定两个节点权值为大的为1。(你也可以理解最重要的作用是用来区分左右节点的,保存左右信息)
  • parent_node : 结构信息,用来存储层次(上下)之间的关系,即两个点对应一个父节点,这个对应关系被这个数组保存。见下:
parent_node[min1i] = vocab_size + a;
parent_node[min2i] = vocab_size + a;
  • 注释:min1i/min2i表示前面找到的两个最小的下坐标的值,然后这两个下坐标对应的节点,存储位置是vocab_size往后顺移动a大小的位置。
  1. 所以虽然不是常见的链表那种更形象的方式,三个数组也保存下了整个树的结构。

  2. 我怎样从数组中恢复上面的树的层次关系?其实这个问题不重要,重要的是,我怎么从上面的数组中找到每个叶子节点到根节点的路径(词编码的本质)。两个循环搞定:外层for循环保证每个词都循环一遍。内层循环保证(while),找到当前词一直到根节点的路径(通过parent_node),这个过程中的信息会被暂时保存在point和code数组中。

  3. 其实到这里我基本就没啥问题了,因为整体思路都有了,但是…我看到了两个不解的操作…

  4. 第一个:为什么要初始化一个char类型的数组来存储code编码??

  • long long 称为超长整型:binary
  • char 字符类性:code

神奇的一幕来了:

code[i] = binary[b];

我暂时不理解…

  1. 第二个:最后的压缩存储…虽然不清楚这样会有啥好处,但是我在这一行代码里捕捉到一个我…的信息:从根节点到叶子节点的路径中,除了叶子节点,都是非叶子节点,这意味着什么?我们映射到数据存储结构中:非叶子节点都是在vocab_size * 2 + 1 的后(vocab_size + 1)个,能这么做的原因找到了,但是为什么这么做?应该我可以在后面的代码中找到答案。但是这个和数据结构呼应还是让我觉得,大佬是怎么想到这种存储方式的???刚刚好的那种感觉,太经典了。
// 路径被压缩了????
 // 注意这个索引对应的是huffman树中的非叶子节点,
// 对应syn1中的索引, 因为非叶子节点都是在vocab_size * 2 + 1 的后(vocab_size + 1)个

vocab[a].point[0] = vocab_size - 2;// 根结点

        for (b = 0; b < i; b++) {
            vocab[a].code[i - b - 1] = code[b];// 编码的反转
            vocab[a].point[i - b] = point[b] - vocab_size;// 记录的是从根结点到叶子节点的路径
        }
    }
  • 总结:我认为对我影响最大的有四点:
    1. 数组存储树结构的方式。尤其是一个考虑左右层次,一个考虑上下层次,一个存储节点count信息…,
    1. 非叶子节点个数为N-1。(这个小知识点贯穿全代码,突然觉得这种理论很重要)。
    1. 从根节点到叶子节点路径唯一,意味着每一个词的编码我可以独立考虑,整个树结构在编码后不再重要。
    1. 所有的词都在叶子节点上,非叶子节点都是中间新生成的节点,且存储在vocab_size后面,这个呼应我真的是有点惊艳。

注释好的代码

// 构建Huffuman树
void CreateBinaryTree(){

    // pos_vocab表示这个在原有节点中索引(叶子节点)  pos_new表示在新节点里索引
    // 这个min1i默认小于min2i(下面第一个节点给了min1i)
    long long a,b,i,min1i, min2i,pos_vocab,pos_new;
    long long point[MAX_CODE_LENGTH];
    char code[MAX_CODE_LENGTH];

    //申请空间
    long long * count = (long long *)calloc(2*vocab_size+1, sizeof(long long));
    long long * binary = (long long *)calloc(2*vocab_size+1,sizeof(long long));
    long long * parent_node = (long long *)calloc(2*vocab_size+1,sizeof(long long));

    //循环存入每一个词的count信息,剩下的空间进行初始化
    for(a=0;a < vocab_size; a++ ) count[a] = vocab[a].cn;
    for(a = vocab_size;a < 2*vocab_size;a++) count[a] = 1e15;


    // 初始化筛选节点下坐标的值

    pos_vocab = vocab_size - 1;   // 指向词库最后那个最小的那值
    pos_new = vocab_size;         // 指向了初始构成节点的存储位置的第一个,用于存储生成节点(非叶子节点)的count,默认值为1e15

    // 开始构建,这个外部循环有意思噢,循环多少次?
    // 答案:我要构建 n-1个非节点,才会形成一个树,每次一个节点,那不就是循环 n-1次!!

    for(a = 0; a < vocab_size - 1; a++){

        // 筛选第一个节点,这个大于0好细节,但是我感觉是多余的。
        if(count[pos_vocab] >=0){
            if(count[pos_vocab] < count[pos_new]){
                min1i = pos_vocab;
                pos_vocab--;
            }else{
                min1i = pos_new;
                pos_new++;
            }
        }else{
            min1i = pos_new;
            pos_new++;
        }

        // 筛选第二个节点

        if(count[pos_vocab] >= 0){
            if(count[pos_vocab] < count[[pos_new]]){
                min2i = pos_vocab;
                pos_vocab--;
            }else{
                min2i = pos_new;
                pos_new++;
            }
        } else{
            min2i = pos_new;
            pos_new++;
        }

        // 提取后干什么??
        // 当然是合并求出新节点啊
        count[vocab_size+a] = count[min1i] + count[min2i];

        // 把上面的结构信息记录下来
        parent_node[min1i] = vocab_size + a;
        parent_node[min2i] = vocab_size + a;

        // 上面只有上下的信息,左右信息靠什么标志?
        // 这个节点标为1表示其在左边(约定频率大)
        // SO?

        binary[min1i] = 0;     //其实不用写,但是写上容易理解
        binary[min2i] = 1;

    }

    // 树结构有了,编码开始!
    // 第一步是什么?
    // 答案:我是对每个词编码的(叶子节点),非叶子节点不是我的目标,我先写个对每个词的外循环没毛病吧。
    // 然后呢,里面我要干什么?
    // 恩.....反过来想??最后把我所有的信息(三个数组)都删掉了
    // 那我到底应该保留词的什么信息对应到每一个词的结构体里面。
    // 好了,其实最最重要的,就是辣个性质,从根节点到每个词(叶子节点)的路径唯一。
    // 对于每个词,其他我不用管,只要我有这个词从根节点到该词叶子节点的路径就好了。

    for(a = 0; a < vocab_size;a++){

        b = a;
        i = 0;

        // 提取出编码和路径指向信息
        while(1){

            code[i] = binary[b];     // 记录编码,左右信息
            point[i] = b;           // 记录路径,上下信息
            i++;

            // 记住这三句话,很重要!!
            // count[vocab_size+a] = count[mi1l] + count[mi2l]
            // parent_node[mi1l] = vocab_size + a;
            // parent_node[mi2l] = vocab_size + a;

            b = parent_node[b];  // b的范围在 0 - vocab_size*2 - 2 里

            // (vocab_size + vocab_size -1) - 1
            if(b == vocab_size*2 - 2) break;
        }

        // 将提取的信息存到词的结构体里
        vocab[a].codelen = i;

        // 然后最不正常的代码出来了??????
        // 路径被压缩了????
        // 注意这个索引对应的是huffman树中的非叶子节点,
        // 对应syn1中的索引, 因为非叶子节点都是在vocab_size * 2 + 1 的后(vocab_size + 1)个
        vocab[a].point[0] = vocab_size - 2;// 根结点

        for (b = 0; b < i; b++) {
            vocab[a].code[i - b - 1] = code[b];// 编码的反转
            vocab[a].point[i - b] = point[b] - vocab_size;// 记录的是从根结点到叶子节点的路径
        }
    }

    free(count);
    free(binary);
    free(parent_node);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值