词向量源码解析:(3.2)GloVe源码解析之vocab_count

vocab_count的功能就是生成词典。它的输入是整个语料,它的输出是词典。词典的形式是单词以及单词在语料中出现的次数(频数)。词典是按照频数从高到低排好序的。这部分代码和word2vec中建立词典的代码很像。由于C语言中没有dict这个的现成的数据结构,需要用C语言自己写一个dict。首先看一下GloVe是如何存储单词的

typedef struct vocabulary {
    char *word;
    long long count;
} VOCAB;

typedef struct hashrec {
    char *word;
    long long count;
    struct hashrec *next;
} HASHREC;

这里存了单词的字符串和频数。和word2vec类似,在这份儿代码中一样需要解决哈希冲突的问题。GloVe中采取的策略是使用链表。相同哈希的单词会通过链表串联起来。所以有了HASHREC这个类型。下面是几个比较函数,用于词典中单词排序用的。

/* Efficient string comparison */
int scmp( char *s1, char *s2 ) {//根据字符串比较单词的大小
    while (*s1 != '\0' && *s1 == *s2) {s1++; s2++;}
    return(*s1 - *s2);
}


/* Vocab frequency comparison; break ties alphabetically */
int CompareVocabTie(const void *a, const void *b) {//先根据频数返回单词的大小,频数一样再根据字符串返回大小
    long long c;
    if ( (c = ((VOCAB *) b)->count - ((VOCAB *) a)->count) != 0) return ( c > 0 ? 1 : -1 );
    else return (scmp(((VOCAB *) a)->word,((VOCAB *) b)->word));
    
}


/* Vocab frequency comparison; no tie-breaker */
int CompareVocab(const void *a, const void *b) {//根据频数返回单词大小,如果频数一样返回0
    long long c;
    if ( (c = ((VOCAB *) b)->count - ((VOCAB *) a)->count) != 0) return ( c > 0 ? 1 : -1 );
    else return 0;
}

返回单词的哈希值

/* Simple bitwise hash function */
unsigned int bitwisehash(char *word, int tsize, unsigned int seed) {
    char c;
    unsigned int h;
    h = seed;
    for (; (c =* word) != '\0'; word++) h ^= ((h << 5) + c + (h >> 2));
    return((unsigned int)((h&0x7fffffff) % tsize));
}

HASHREC **ht实现了dict的功能,能通过单词的哈希值快速的检索到单词。它是一个链表的数组,数组的大小是TSIZE,是哈希值的上限。要在这个链表数组中查找到想要的单词,首先要计算单词的哈希值,比如是15,然后再ht[15]这个链表中找到单词。

HASHREC ** inithashtable() {
    int i;
    HASHREC **ht;
    ht = (HASHREC **) malloc( sizeof(HASHREC *) * TSIZE );
    for (i = 0; i < TSIZE; i++) ht[i] = (HASHREC *) NULL;
    return(ht);
}

我们要往ht中插入单词。

/* Search hash table for given string, insert if not found */
void hashinsert(HASHREC **ht, char *w) {
    HASHREC *htmp, *hprv;
    unsigned int hval = HASHFN(w, TSIZE, SEED);//首先计算单词的哈希值
    
    for (hprv = NULL, htmp = ht[hval]; htmp != NULL && scmp(htmp->word, w) != 0; hprv = htmp, htmp = htmp->next);//随着链表往下走,直到遇到了要找的单词,否则一直走到底

    if (htmp == NULL) {//如果没有找到就插入单词,维护链表
        htmp = (HASHREC *) malloc( sizeof(HASHREC) );
        htmp->word = (char *) malloc( strlen(w) + 1 );
        strcpy(htmp->word, w);
        htmp->count = 1;
        htmp->next = NULL;
        if ( hprv==NULL )
            ht[hval] = htmp;
        else
            hprv->next = htmp;
    }
    else {//如果找到单词就在频数上加1,把找到的单词移动到链表最前面。一个提高效率的小技巧
        /* new records are not moved to front */
        htmp->count++;
        if (hprv != NULL) {
            /* move to front on access */
            hprv->next = htmp->next;
            htmp->next = ht[hval];
            ht[hval] = htmp;
        }
    }
    return;
}

最后,get_count函数把前面的函数串起来,构建一个词典

int get_counts() {
    long long i = 0, j = 0, vocab_size = 12500;
    char format[20];
    char str[MAX_STRING_LENGTH + 1];
    HASHREC **vocab_hash = inithashtable();//哈希表,维护dict数据结构
    HASHREC *htmp;
    VOCAB *vocab;
    FILE *fid = stdin;//这里输入是通过脚本重定向给的程序,fid就是语料文件的指针
    
    fprintf(stderr, "BUILDING VOCABULARY\n");//通过错误流打印信息
    if (verbose > 1) fprintf(stderr, "Processed %lld tokens.", i);
    sprintf(format,"%%%ds",MAX_STRING_LENGTH);
    while (fscanf(fid, format, str) != EOF) { // Insert all tokens into hashtable//word2vec中从文件读取一个单词逻辑比较复杂,这里就通过fscanf就很简单的解决了,format指定了要读取字符串,默认可能是按照空格分,这里我没有仔细研究。
        if (strcmp(str, "") == 0) {
            fprintf(stderr, "\nError, vector found in corpus.\nPlease remove s from your corpus (e.g. cat text8 | sed -e 's///g' > text8.new)");
            return 1;
        }
        hashinsert(vocab_hash, str);//插入到哈希表中(就是ht,这里可能用词不严格)
        if (((++i)%100000) == 0) if (verbose > 1) fprintf(stderr,"\033[11G%lld tokens.", i);
    }
    if (verbose > 1) fprintf(stderr, "\033[0GProcessed %lld tokens.\n", i);//现在已经把所有单词都存在了哈希表中
    vocab = malloc(sizeof(VOCAB) * vocab_size);//我们要把哈希表中的单词挪到VOCAB数组中,排好序输出
    for (i = 0; i < TSIZE; i++) { // Migrate vocab to array
        htmp = vocab_hash[i];
        while (htmp != NULL) {
            vocab[j].word = htmp->word;
            vocab[j].count = htmp->count;
            j++;
            if (j>=vocab_size) {
                vocab_size += 2500;
                vocab = (VOCAB *)realloc(vocab, sizeof(VOCAB) * vocab_size);
            }
            htmp = htmp->next;
        }
    }
    if (verbose > 1) fprintf(stderr, "Counted %lld unique words.\n", j);
    if (max_vocab > 0 && max_vocab < j)//单词排序的逻辑是如果词典大小超了预设值,那么就先用CompareVocab这样只通过频数比较大小的来排序,然后再根据预设值截断,丢掉低频的,再对已有的词典用CompareVocabTie排序,先按照频数排序,频数一样用字符串大小排序。这么做是为了在丢掉单词的过程中,仅仅是根据频数丢掉,不考虑单词的拼写。比如根据max_vocab的值,要丢到频数为5的一部分单词,我们就这样做就能随机的丢掉。
        // If the vocabulary exceeds limit, first sort full vocab by frequency without alphabetical tie-breaks.
        // This results in pseudo-random ordering for words with same frequency, so that when truncated, the words span whole alphabet
        qsort(vocab, j, sizeof(VOCAB), CompareVocab);//仅仅通过频数
    else max_vocab = j;
    qsort(vocab, max_vocab, sizeof(VOCAB), CompareVocabTie); //After (possibly) truncating, sort (possibly again), breaking ties alphabeticallyCompareVocabTie//对于剩下的部分的排序先根据频数来再根据字符串来
    
    for (i = 0; i < max_vocab; i++) {//写出词典,注意是通过printf写出,脚本中重定向到一个文件
        if (vocab[i].count < min_count) { // If a minimum frequency cutoff exists, truncate vocabulary
            if (verbose > 0) fprintf(stderr, "Truncating vocabulary at min count %lld.\n",min_count);
            break;
        }
        printf("%s %lld\n",vocab[i].word,vocab[i].count);
    }
    
    if (i == max_vocab && max_vocab < j) if (verbose > 0) fprintf(stderr, "Truncating vocabulary at size %lld.\n", max_vocab);
    fprintf(stderr, "Using vocabulary of size %lld.\n\n", i);
    return 0;
}

你可能感兴趣的:(词向量)