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, "
fprintf(stderr, "\nError,
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;
}