近期一直在看关于文本处理的东西,怎奈一点基础都没有,看得着实费劲。特别是自己对于word2vec的理解一直不到位,因为总是介绍含糊不清,刚没说两句呢,就转到它的两个常用模型了。前段时间搜索的相关信息并不能减少word2vec给我的神秘感。不过一直好奇会催使你不断搜索新的信息,从而减少自己对其的持续的违和感。抱怨一句,在自认为一天就能搞定的事情上浪费了一个星期还没有彻底搞明白,真的是很难受,内心在不断的排斥。言归正传,介绍本篇主角:word2vec.
给出两篇博文地址,个人感觉还不错(本篇也只是这两篇博文的搬运工):
源码:https://www.cnblogs.com/KingKou/p/4567280.html
使用说明:http://jacoxu.com/word2vector/
首先给出本人对word2vec的理解:
(抱怨两句,可跳过)先抛开大家常说的两个模型Continuous Bag of Words Model(CBOW)和Skip-Gram Model,这两是应用级别的。个人感觉这俩对于理解word2vec的本质是有干扰的。他俩了解的再多有时对word2vec还是不理解。小编就是对这俩模型都熟透了,可我一直在问自己一个问题,这俩模型和word2vec是一个东西吗?对其关系有困扰就是还没理解透。
很简单的几句话:word2vec就是根据语料(也就是你的文本)来生成单词向量的一个工具。它的实质就是一个模型,通过训练这个模型来预测文档中出现的单词概率。可以将其比作是一个CNN 模型,只不过CNN 通常时给出各个类别的概率,而word2vec则时给出可能出现的单词的概率。CNN 训练数据是图片数据,而word2vec的训练数据时文本数据。个人认为,不同专业应用场景的文本有时需要不同的word2vec,需要用专业的文本训练生成一个word2vec.
ok, 那CBOW和Skip-Gram Model这两个模型就可以看作是CNN 的不同模型,如ALexNet, ResNet等。这两个模型是word2vec的两个不同结构的模型,属于word2vec。
想要具体了解上面这两个模型可以看https://blog.csdn.net/u010417185/article/details/80647714
接下来的内容就是对上面两篇博文的搬运啦,只是为了方便自己以后查找而已:
word2vec源码解读
Skip-gram模型和CBOW模型,两者的区别除了在于:
CBOW模型是用上下文预测中间的词,并且参数是用的上下文词向量的和
skip-gram模型是中间的词预测上下文,并且参数是中间词的词向量
下面对word2vec的源码做一些解析,主要包含一些辅助函数的作用,参数的设置和训练的过程:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <pthread.h>
// 一个word的最大长度
#define MAX_STRING 100
// 对f的运算结果进行缓存,存储1000个,需要用的时候查表
#define EXP_TABLE_SIZE 1000
// 最大计算到6 (exp^6 / (exp^6 + 1)),最小计算到-6 (exp^-6 / (exp^-6 + 1))
#define MAX_EXP 6
// 定义最大的句子长度,句子以</s>结束,如果没有结束符,则长度最长为1000
#define MAX_SENTENCE_LENGTH 1000
// 定义最长的霍夫曼编码长度
#define MAX_CODE_LENGTH 40
// 哈希,线性探测,开放定址法,装填系数0.7
const int vocab_hash_size = 30000000; // 词库中最多有30 * 0.7 = 21M个单词
typedef float real; // 浮点数精度
struct vocab_word {
long long cn; // 单词词频
int *point; // 霍夫曼树中从根节点到该词的路径,存放路径上每个非叶结点的索引
char *word, *code, codelen; // 分别是词的字面,霍夫曼编码,编码长度
};
// 训练文件、输出文件名称定义
char train_file[MAX_STRING], output_file[MAX_STRING];
// 词汇表输出文件和词汇表读入文件名称定义
char save_vocab_file[MAX_STRING], read_vocab_file[MAX_STRING];
// 声明词汇表结构体
struct vocab_word *vocab;
// binary 0则vectors.bin输出为二进制(默认),1则为文本形式
// cbow 1使用cbow框架,0使用skip-gram框架
// debug_mode 大于0,加载完毕后输出汇总信息,大于1,加载训练词汇的时候输出信息,训练过程中输出信息
// window 窗口大小,在cbow中表示了word vector的最大的sum范围,在skip-gram中表示了max space between words(w1,w2,p(w1 | w2))
// min_count 删除长尾词的词频标准
// num_threads 线程数
// min_reduce ReduceVocab删除词频小于这个值的词,因为哈希表总共可以装填的词汇数是有限的
int binary = 0, cbow = 0, debug_mode = 2, window = 5, min_count = 5, num_threads = 1, min_reduce = 1;
int *vocab_hash; // 词汇表的hash存储,下标是词的hash,内容是词在vocab中的位置,a[word_hash] = word index in vocab
// vocab_max_size 词汇表的最大长度,动态扩增,每次扩1000
// vocab_size 词汇表的现有长度,接近vocab_max_size的时候会扩容
// layer1_size 隐层的节点数/词向量大小
long long vocab_max_size = 1000, vocab_size = 0, layer1_size = 100;
// train_words 训练的单词总数(词频累加)
// word_count_actual 已经训练完的word个数
// file_size 训练文件大小,ftell得到
// classes 输出word clusters的类别数
long long train_words = 0, word_count_actual = 0, file_size = 0, classes = 0;
// alpha BP算法的学习速率,过程中自动调整
// starting_alpha 初始alpha值
// sample 亚采样概率的参数,亚采样的目的是以一定概率拒绝高频词,使得低频词有更多出镜率,默认为0,即不进行亚采样
real alpha = 0.025, starting_alpha, sample = 0;
// syn0 单词的向量输入 concatenate word vectors
// syn1 hs(hierarchical softmax)算法中隐层节点到霍夫曼编码树非叶结点的映射权重
// syn1neg ns(negative sampling)中隐层节点到分类问题的映射权重
// expTable 预先存储f函数结果,算法执行中查表
real *syn0, *syn1, *syn1neg, *expTable;
// start 算法运行的起始时间,会用于计算平均每秒钟处理多少词
clock_t start;
// hs 采用hs还是ns的标志位,默认采用hs
int hs = 1, negative = 0;
// table_size 静态采样表的规模
// table 采样表
const int table_size = 1e8;
int *table;
// 根据词频生成采样表,词频越高,占据在表中的数目越大,用于表示词频分布
void InitUnigramTable() {
int a, i;
long long train_words_pow = 0;
real d1, power = 0.75; // 概率与词频的power次方成正比
table = (int *)malloc(table_size * sizeof(int));
for (a = 0; a < vocab_size; a++) train_words_pow += pow(vocab[a].cn, power);
i = 0;
d1 = pow(vocab[i].cn, power) / (real)train_words_pow; // 第一个词出现的概率
for (a = 0; a < table_size; a++) {
table[a] = i;
if (a / (real)table_size > d1) {
i++;
d1 += pow(vocab[i].cn, power) / (real)train_words_pow;
}
if (i >= vocab_size) i = vocab_size - 1; // 处理最后一段概率,防止越界
}
}
// Reads a single word from a file, assuming space + tab + EOL to be word boundaries
// 每次从fin中读取一个单词
void ReadWord(char *word, FILE *fin) {
int a = 0, ch;
while (!feof(fin)) {
ch = fgetc(fin);
if (ch == 13) continue;
// ASCII值为8、9、10 和13 分别转换为退格、制表、换行和回车字符
if ((ch == ' ') || (ch == '\t') || (ch == '\n')) { // 词的分隔符
if (a > 0) {
if (ch == '\n') <br> ungetc(ch, fin); // 把一个字符回退到输入流中
break;
}
if (ch == '\n') {
strcpy(word, (char *)"</s>");
return;
} else continue;
}
word[a] = ch;
a++;
if (a >= MAX_STRING - 1)<br> a--; // 如果单词过长
}
word[a] = 0;
}
// 获取单词的哈希值
int GetWordHash(char *word) {
unsigned long long a, hash = 0;
for (a = 0; a < strlen(word); a++) <br> hash = hash * 257 + word[a]; // hash计算方法
hash = hash % vocab_hash_size;
return hash;
}
// Returns position of a word in the vocabulary; if the word is not found, returns -1
// 线性探索,开放定址法
int SearchVocab(char *word) {
unsigned int hash = GetWordHash(word);
while (1) {
if (vocab_hash[hash] == -1) return -1; // 没有这个词
if (!strcmp(word, vocab[vocab_hash[hash]].word)) return vocab_hash[hash]; // 返回单词在词汇表中的索引
hash = (hash + 1) % vocab_hash_size;
}
return -1; // 应该到不了这里吧……
}
// 获取单词的索引值
int ReadWordIndex(FILE *fin) {
char word[MAX_STRING];
ReadWord(word, fin);
if (feof(fin)) return -1;
return SearchVocab(word);
}
// 将单词添加到词汇表中
int AddWordToVocab(char *word) {
unsigned int hash, length = strlen(word) + 1;
if (length > MAX_STRING) length = MAX_STRING;
vocab[vocab_size].word = (char *)calloc(length, sizeof(char)); // 单词存储
strcpy(vocab[vocab_size].word, word);
vocab[vocab_size].cn = 0; // 在调用函数之外赋值1
vocab_size++; // 词汇表现有单词数
// Reallocate memory if needed
if (vocab_size + 2 >= vocab_max_size) {
vocab_max_size += 1000; // 每次增加1000个词位
vocab = (struct vocab_word *)realloc(vocab, vocab_max_size * sizeof(struct vocab_word));
}
hash = GetWordHash(word); // 获得hash表示
while (vocab_hash[hash] != -1)
hash = (hash + 1) % vocab_hash_size; // 线性探索hash
vocab_hash[hash] = vocab_size - 1; // 记录在词汇表中的存储位置
return vocab_size - 1; // 返回添加的单词在词汇表中的存储位置
}
// 比较函数,词汇表需使用词频进行排序(qsort)
int VocabCompare(const void *a, const void *b) {
return ((struct vocab_word *)b)->cn - ((struct vocab_word *)a)->cn;
}
// 对单词按照词频排序
void SortVocab() {
int a, size;
unsigned int hash;
// Sort the vocabulary and keep </s> at the first position
// 保留回车在首位
qsort(&vocab[1], vocab_size - 1, sizeof(struct vocab_word), VocabCompare); // 对词汇表进行快速排序
for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1; // 词汇重排了,哈希记录的index也乱了,所有的hash记录清除,下面会重建
size = vocab_size;
train_words = 0; // 用于训练的词汇总数(词频累加)
for (a = 0; a < size; a++) {
// Words occuring less than min_count times will be discarded from the vocab
if (vocab[a].cn < min_count) { // 清除长尾词
vocab_size--;
free(vocab[vocab_size].word);
} else {
// Hash will be re-computed, as after the sorting it is not actual
hash=GetWordHash(vocab[a].word);
while (vocab_hash[hash] != -1)
hash = (hash + 1) % vocab_hash_size;
vocab_hash[hash] = a;
train_words += vocab[a].cn; // 词频累加
}
}
vocab = (struct vocab_word *)realloc(vocab, (vocab_size + 1) * sizeof(struct vocab_word)); // 分配的多余空间收回
// 给霍夫曼编码和路径的词汇表索引分配空间
for (a = 0; a < vocab_size; a++) {
vocab[a].code = (char *)calloc(MAX_CODE_LENGTH, sizeof(char));
vocab[a].point = (int *)calloc(MAX_CODE_LENGTH, sizeof(int));
}
}
此处,有些人可能不理解为什么要重排。这里我就列一下单词和哈希值在代码里面的数据关系,就比较好理解了。所以一旦单词表重排,单词的位置相应改变,哈希记录表vocab_hash就要重新构造。
// 删除词频不大于min_reduce的词
void ReduceVocab() {
int a, b = 0;
unsigned int hash;
for (a = 0; a < vocab_size; a++){
if (vocab[a].cn > min_reduce) {
vocab[b].cn = vocab[a].cn;
vocab[b].word = vocab[a].word;
b++;
} else free(vocab[a].word);
vocab_size = b; // 最后剩下b个词,词频均大于min_reduce
// 重新分配hash索引
for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1;
for (a = 0; a < vocab_size; a++) {
// Hash will be re-computed, as it is not actual
hash = GetWordHash(vocab[a].word);
while (vocab_hash[hash] != -1)
hash = (hash + 1) % vocab_hash_size;
vocab_hash[hash] = a;
}
fflush(stdout);
min_reduce++;
}
// 根据词频生成霍夫曼树
void CreateBinaryTree() {
long long a, b, i, min1i, min2i, pos1, pos2, point[MAX_CODE_LENGTH]; // 最长的编码值
char code[MAX_CODE_LENGTH];
//词频记录,前一半是叶子节点,即文本中单词的词频,后一半是非叶子节点的词频
long long *count = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long));
//记录哈夫曼编码的逆序,因为是从底向上构建的
long long *binary = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long));
//记录节点的父节点
long long *parent_node = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long));
for (a = 0; a < vocab_size; a++)
count[a] = vocab[a].cn;
for (a = vocab_size; a < vocab_size * 2; a++)
count[a] = 1e15;
pos1 = vocab_size - 1;
pos2 = vocab_size;
// Following algorithm constructs the Huffman tree by adding one node at a time
for (a = 0; a < vocab_size - 1; a++) {
// 每次寻找两个最小的点做合并,最小的点的分支为0,词小的点的分支为1
//由于是已经按照词频高低排好序的,最后的两个单词就是词频最低的
//pos1控制叶子节点,pos2控制非叶子节点
//min1i是左分支,min2i是右分支
if (pos1 >= 0) {
if (count[pos1] < count[pos2]) {
min1i = pos1;
pos1--;
} else {
min1i = pos2;
pos2++;
}
} else {
min1i = pos2;
pos2++;
}
if (pos1 >= 0) {
if (count[pos1] < count[pos2]) {
min2i = pos1;
pos1--;
} else {
min2i = pos2;
pos2++;
}
} else {
min2i = pos2;
pos2++;
}
count[vocab_size + a] = count[min1i] + count[min2i];
parent_node[min1i] = vocab_size + a;
parent_node[min2i] = vocab_size + a;
binary[min2i] = 1;
}
// Now assign binary code to each vocabulary word
// 顺着父子关系找回编码
for (a = 0; a < vocab_size; a++) {
b = a;
i = 0;
while (1) {
code[i] = binary[b]; // 编码赋值
point[i] = b; // 路径赋值,第一个是自己
i++; // 码个数
b = parent_node[b];
if (b == vocab_size * 2 - 2) break;
}
// 以下要注意的是,同样的位置,point总比code深一层
vocab[a].codelen = i; // 编码长度赋值,少1,没有算根节点
vocab[a].point[0] = vocab_size - 2; // 逆序,把第一个赋值为root(即2*vocab_size - 2 - vocab_size)
for (b = 0; b < i; b++) { // 逆序处理
vocab[a].code[i - b - 1] = code[b]; // 编码逆序,没有根节点,左子树0,右子树1
vocab[a].point[i - b] = point[b] - vocab_size; // 其实point数组最后一个是负的,用不到,point的长度是编码的真正长度,比code长1
}
}
free(count);
free(binary);
free(parent_node);
}
// 装载训练文件到词汇表数据结构
void LearnVocabFromTrainFile() {
char word[MAX_STRING];
FILE *fin;
long long a, i;
for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1;
fin = fopen(train_file, "rb");
if (fin == NULL) {
printf("ERROR: training data file not found!\n");
exit(1);
}
vocab_size = 0;
AddWordToVocab((char *)"</s>"); // 首先添加的是回车
while (1) {
ReadWord(word, fin);
if (feof(fin)) break;
train_words++;
if ((debug_mode > 1) && (train_words % 100000 == 0)) {
printf("%lldK%c", train_words / 1000, 13);
fflush(stdout);
}
i = SearchVocab(word);
if (i == -1) { // 如果这个单词不存在,我们将其加入hash表
a = AddWordToVocab(word);
vocab[a].cn = 1;
} else vocab[i].cn++; // 否则词频加一
if (vocab_size > vocab_hash_size * 0.7) ReduceVocab(); // 如果超出装填系数,将词汇表扩容
}
SortVocab(); // 所有词汇加载完毕后进行排序,词频高的靠前
if (debug_mode > 0) {
printf("Vocab size: %lld\n", vocab_size);
printf("Words in train file: %lld\n", train_words);
}
file_size = ftell(fin); // 文件大小
fclose(fin);
}
// 输出单词和词频到文件
void SaveVocab() {
long long i;
FILE *fo = fopen(save_vocab_file, "wb");
for (i = 0; i < vocab_size; i++) fprintf(fo, "%s %lld\n", vocab[i].word, vocab[i].cn);
fclose(fo);
}
// 读入词汇表文件到词汇表数据结构
void ReadVocab() {
long long a, i = 0;
char c;
char word[MAX_STRING];
FILE *fin = fopen(read_vocab_file, "rb");
if (fin == NULL) {
printf("Vocabulary file not found\n");
exit(1);
}
for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1;
vocab_size = 0;
while (1) {
ReadWord(word, fin);
if (feof(fin)) break;
a = AddWordToVocab(word);
fscanf(fin, "%lld%c", &vocab[a].cn, &c);
i++;
}
SortVocab();
if (debug_mode > 0) {
printf("Vocab size: %lld\n", vocab_size);
printf("Words in train file: %lld\n", train_words);
}
fin = fopen(train_file, "rb"); // 还得打开以下训练文件好知道文件大小是多少
if (fin == NULL) {
printf("ERROR: training data file not found!\n");
exit(1);
}
fseek(fin, 0, SEEK_END);
file_size = ftell(fin);
fclose(fin);
}
// 网络结构初始化,就是把所有参数初始化
void InitNet() {
long long a, b;
// posix_memalign() 成功时会返回size字节的动态内存,并且这块内存的地址是alignment(这里是128)的倍数
// syn0 存储的是就是单词向量
a = posix_memalign((void **)&syn0, 128, (long long)vocab_size * layer1_size * sizeof(real));
if (syn0 == NULL) {printf("Memory allocation failed\n"); exit(1);}
if (hs) { // hierarchical softmax
// hs中,用syn1
a = posix_memalign((void **)&syn1, 128, (long long)vocab_size * layer1_size * sizeof(real));
if (syn1 == NULL) {printf("Memory allocation failed\n"); exit(1);}
for (b = 0; b < layer1_size; b++)
for (a = 0; a < vocab_size; a++)
syn1[a * layer1_size + b] = 0;
}
if (negative>0) { // negative sampling
// ns中,用syn1neg
a = posix_memalign((void **)&syn1neg, 128, (long long)vocab_size * layer1_size * sizeof(real));
if (syn1neg == NULL) {printf("Memory allocation failed\n"); exit(1);}
for (b = 0; b < layer1_size; b++) <br> for (a = 0; a < vocab_size; a++)
syn1neg[a * layer1_size + b] = 0;
}
for (b = 0; b < layer1_size; b++) <br> for (a = 0; a < vocab_size; a++)
syn0[a * layer1_size + b] = (rand() / (real)RAND_MAX - 0.5) / layer1_size; // 随机初始化word vectors
CreateBinaryTree(); // 创建霍夫曼树
}
void *TrainModelThread(void *id) {
// word 向sen中添加单词用,句子完成后表示句子中的当前单词
// last_word 上一个单词,辅助扫描窗口
// sentence_length 当前句子的长度(单词数)
// sentence_position 当前单词在当前句子中的index
long long a, b, d, word, last_word, sentence_length = 0, sentence_position = 0;
// word_count 已训练语料总长度
// last_word_count 保存值,以便在新训练语料长度超过某个值时输出信息
// sen 单词数组,表示句子
long long word_count = 0, last_word_count = 0, sen[MAX_SENTENCE_LENGTH + 1];
// l1 ns中表示word在concatenated word vectors中的起始位置,之后layer1_size是对应的word vector,因为把矩阵拉成长向量了
// l2 cbow或ns中权重向量的起始位置,之后layer1_size是对应的syn1或syn1neg,因为把矩阵拉成长向量了
// c 循环中的计数作用
// target ns中当前的sample
// label ns中当前sample的label
long long l1, l2, c, target, label;
// id 线程创建的时候传入,辅助随机数生成
unsigned long long next_random = (long long)id;
// f e^x / (1/e^x),fs中指当前编码为是0(父亲的左子节点为0,右为1)的概率,ns中指label是1的概率
// g 误差(f与真实值的偏离)与学习速率的乘积
real f, g;
// 当前时间,和start比较计算算法效率
clock_t now;
real *neu1 = (real *)calloc(layer1_size, sizeof(real)); // 隐层节点
real *neu1e = (real *)calloc(layer1_size, sizeof(real)); // 误差累计项,其实对应的是Gneu1
FILE *fi = fopen(train_file, "rb");
fseek(fi, file_size / (long long)num_threads * (long long)id, SEEK_SET); // 将文件内容分配给各个线程
while (1) {
if (word_count - last_word_count > 10000) {
word_count_actual += word_count - last_word_count;
last_word_count = word_count;
if ((debug_mode > 1)) {
now=clock();
printf("%cAlpha: %f Progress: %.2f%% Words/thread/sec: %.2fk ", 13, alpha,
word_count_actual / (real)(train_words + 1) * 100,
word_count_actual / ((real)(now - start + 1) / (real)CLOCKS_PER_SEC * 1000));
fflush(stdout);
}
alpha = starting_alpha * (1 - word_count_actual / (real)(train_words + 1)); // 自动调整学习速率
if (alpha < starting_alpha * 0.0001) alpha = starting_alpha * 0.0001; // 学习速率有下限
}
if (sentence_length == 0) { // 如果当前句子长度为0
while (1) {
word = ReadWordIndex(fi);
if (feof(fi)) break; // 读到文件末尾
if (word == -1) continue; // 没有这个单词
word_count++; // 单词计数增加
if (word == 0) break; // 是个回车
// 这里的亚采样是指 Sub-Sampling,Mikolov 在论文指出这种亚采样能够带来 2 到 10 倍的性能提升,并能够提升低频词的表示精度。
// 低频词被丢弃概率低,高频词被丢弃概率高
if (sample > 0) {
real ran = (sqrt(vocab[word].cn / (sample * train_words)) + 1) * (sample * train_words) / vocab[word].cn;
next_random = next_random * (unsigned long long)25214903917 + 11;
if (ran < (next_random & 0xFFFF) / (real)65536) continue;
}
sen[sentence_length] = word;
sentence_length++;
if (sentence_length >= MAX_SENTENCE_LENGTH) break;
}
sentence_position = 0; // 当前单词在当前句中的index,起始值为0
}
if (feof(fi)) break; // 照应while中的break,如果读到末尾,退出
if (word_count > train_words / num_threads) break; // 已经做到了一个thread应尽的工作量,就退出
word = sen[sentence_position]; // 取句子中的第一个单词,开始运行BP算法
if (word == -1) continue; // 如果没有这个单词,则继续
// 隐层节点值和隐层节点误差累计项清零
for (c = 0; c < layer1_size; c++) neu1[c] = 0;
for (c = 0; c < layer1_size; c++) neu1e[c] = 0;
next_random = next_random * (unsigned long long)25214903917 + 11;
b = next_random % window; // b是个随机数,0到window-1,指定了本次算法操作实际的窗口大小
// cbow 框架
if (cbow) { //train the cbow architecture
// in -> hidden
// 从输入层到隐层所进行的操作实际就是窗口内上下文向量的加和
for (a = b; a < window * 2 + 1 - b; a++) if (a != window) {
c = sentence_position - window + a;
if (c < 0) continue;
if (c >= sentence_length) continue;
last_word = sen[c];
if (last_word == -1) continue; // 这个单词没有
for (c = 0; c < layer1_size; c++) neu1[c] += syn0[c + last_word * layer1_size];
}
// hs
if (hs) for (d = 0; d < vocab[word].codelen; d++) { // 这里的codelen其实是少一个的,所以不会触及point里面最后一个负数
f = 0;
l2 = vocab[word].point[d] * layer1_size; // 路径上的点
// Propagate hidden -> output
// 准备计算f
for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1[c + l2];
// 不在expTable内的舍弃掉,作者说计算精度有限,怕有不好印象,但这里改成太小的都是0,太大的都是1,运行结果还是有差别的
// if (f <= -MAX_EXP) continue;
// else if (f >= MAX_EXP) continue;
if (f <= -MAX_EXP) f = 0;
else if (f >= MAX_EXP) f = 1;
// 从expTable中查找,快速计算
else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];
// g 实际为负梯度中公共的部分与 Learning rate alpha 的乘积
g = (1 - vocab[word].code[d] - f) * alpha;
// Propagate errors output -> hidden
// 记录累积误差项
for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];
// Learn weights hidden -> output
// 更新隐层到霍夫曼树非叶节点的权重
for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * neu1[c];
}
// NEGATIVE SAMPLING
if (negative > 0) for (d = 0; d < negative + 1; d++) {
if (d == 0) { // 当前词的分类器应当输出1
target = word;
label = 1;
} else { // 采样使得与target不同,不然continue,label为0,也即最多采样negative个negative sample
next_random = next_random * (unsigned long long)25214903917 + 11;
target = table[(next_random >> 16) % table_size];
if (target == 0) target = next_random % (vocab_size - 1) + 1;
if (target == word) continue;
label = 0;
}
l2 = target * layer1_size;
f = 0;
for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1neg[c + l2];
// 这里直接上0、1,没有考虑计算精度问题
if (f > MAX_EXP) g = (label - 1) * alpha;
else if (f < -MAX_EXP) g = (label - 0) * alpha;
// g 并非梯度,可以看做是一个乘了学习率的 error(label与输出f的差)。损失函数Loss=-log Likehood = -label•logf-(1-lable)•log(1-f),推导同上。
else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];
for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * neu1[c];
}
// hidden -> in
// 根据隐层节点累积误差项,更新word vectors
for (a = b; a < window * 2 + 1 - b; a++) if (a != window) {
c = sentence_position - window + a;
if (c < 0) continue;
if (c >= sentence_length) continue;
last_word = sen[c];
if (last_word == -1) continue;
for (c = 0; c < layer1_size; c++) syn0[c + last_word * layer1_size] += neu1e[c];
}
} else { //train skip-gram
for (a = b; a < window * 2 + 1 - b; a++) if (a != window) { // 预测非中心的单词(邻域内的单词)
c = sentence_position - window + a;
if (c < 0) continue;
if (c >= sentence_length) continue;
last_word = sen[c];
if (last_word == -1) continue;
l1 = last_word * layer1_size;
// 每次循环neu1e都被置零了
for (c = 0; c < layer1_size; c++) neu1e[c] = 0;
// HIERARCHICAL SOFTMAX
if (hs) for (d = 0; d < vocab[word].codelen; d++) {
f = 0;
l2 = vocab[word].point[d] * layer1_size;
// Propagate hidden -> output
// 待预测单词的 word vecotr 和 隐层-霍夫曼树非叶节点权重 的内积
for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1[c + l2];
// 同cbow中hs的讨论
// if (f <= -MAX_EXP) continue;
// else if (f >= MAX_EXP) continue;
if (f <= -MAX_EXP) f = 0;
else if (f >= MAX_EXP) f = 1;
// 以下内容同之前的cbow
else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];
// g 就是梯度中的公共部分与学习率的乘积,此处损失函数的计算方式有别于CBOW模型,具体参考文献[6]。
g = (1 - vocab[word].code[d] - f) * alpha; // 这里的code[d]其实是下一层的,code错位了,point和code是错位的!
// Propagate errors output -> hidden
for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];
// Learn weights hidden -> output
for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * syn0[c + l1];
}
// NEGATIVE SAMPLING
if (negative > 0) for (d = 0; d < negative + 1; d++) {
if (d == 0) {
target = word;
label = 1;
} else {
next_random = next_random * (unsigned long long)25214903917 + 11;
target = table[(next_random >> 16) % table_size];
if (target == 0) target = next_random % (vocab_size - 1) + 1;
if (target == word) continue;
label = 0;
}
l2 = target * layer1_size;
f = 0;
for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1neg[c + l2];
// 以下内容同之前的cbow
if (f > MAX_EXP) g = (label - 1) * alpha;
else if (f < -MAX_EXP) g = (label - 0) * alpha;
else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];
for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * syn0[c + l1];
}
// Learn weights input -> hidden
for (c = 0; c < layer1_size; c++) syn0[c + l1] += neu1e[c];
}
}
sentence_position++;
if (sentence_position >= sentence_length) {
sentence_length = 0;
continue;
}
}
fclose(fi);
free(neu1);
free(neu1e);
pthread_exit(NULL);
}
void TrainModel() {
long a, b, c, d;
FILE *fo;
// 创建多线程
pthread_t *pt = (pthread_t *)malloc(num_threads * sizeof(pthread_t));
printf("Starting training using file %s\n", train_file);
starting_alpha = alpha;
// 优先从词汇表文件中加载,否则从训练文件中加载
if (read_vocab_file[0] != 0) ReadVocab(); else LearnVocabFromTrainFile();
// 输出词汇表文件,词+词频
if (save_vocab_file[0] != 0) SaveVocab();
if (output_file[0] == 0) return;
InitNet(); // 网络结构初始化
if (negative > 0) InitUnigramTable(); // 根据词频生成采样映射
start = clock(); // 开始计时
for (a = 0; a < num_threads; a++) pthread_create(&pt[a], NULL, TrainModelThread, (void *)a);
for (a = 0; a < num_threads; a++) pthread_join(pt[a], NULL);
// 训练结束,准备输出
fo = fopen(output_file, "wb");
if (classes == 0) { // 保存 word vectors
// Save the word vectors
fprintf(fo, "%lld %lld\n", vocab_size, layer1_size); // 词汇量,vector维数
for (a = 0; a < vocab_size; a++) {
fprintf(fo, "%s ", vocab[a].word);
if (binary) for (b = 0; b < layer1_size; b++) fwrite(&syn0[a * layer1_size + b], sizeof(real), 1, fo);
else for (b = 0; b < layer1_size; b++) fprintf(fo, "%lf ", syn0[a * layer1_size + b]);
fprintf(fo, "\n");
}
} else {
// Run K-means on the word vectors
// 运行K-means算法
int clcn = classes, iter = 10, closeid;
int *centcn = (int *)malloc(classes * sizeof(int));
int *cl = (int *)calloc(vocab_size, sizeof(int));
real closev, x;
real *cent = (real *)calloc(classes * layer1_size, sizeof(real));
for (a = 0; a < vocab_size; a++) cl[a] = a % clcn;
for (a = 0; a < iter; a++) {
for (b = 0; b < clcn * layer1_size; b++) cent[b] = 0;
for (b = 0; b < clcn; b++) centcn[b] = 1;
for (c = 0; c < vocab_size; c++) {
for (d = 0; d < layer1_size; d++) cent[layer1_size * cl[c] + d] += syn0[c * layer1_size + d];
centcn[cl[c]]++;
}
for (b = 0; b < clcn; b++) {
closev = 0;
for (c = 0; c < layer1_size; c++) {
cent[layer1_size * b + c] /= centcn[b];
closev += cent[layer1_size * b + c] * cent[layer1_size * b + c];
}
closev = sqrt(closev);
for (c = 0; c < layer1_size; c++) cent[layer1_size * b + c] /= closev;
}
for (c = 0; c < vocab_size; c++) {
closev = -10;
closeid = 0;
for (d = 0; d < clcn; d++) {
x = 0;
for (b = 0; b < layer1_size; b++) x += cent[layer1_size * d + b] * syn0[c * layer1_size + b];
if (x > closev) {
closev = x;
closeid = d;
}
}
cl[c] = closeid;
}
}
// Save the K-means classes
for (a = 0; a < vocab_size; a++) fprintf(fo, "%s %d\n", vocab[a].word, cl[a]);
free(centcn);
free(cent);
free(cl);
}
fclose(fo);
}
int ArgPos(char *str, int argc, char **argv) {
int a;
for (a = 1; a < argc; a++) if (!strcmp(str, argv[a])) {
if (a == argc - 1) {
printf("Argument missing for %s\n", str);
exit(1);
}
return a;
}
return -1;
}
int main(int argc, char **argv) {
int i;
if (argc == 1) {
printf("WORD VECTOR estimation toolkit v 0.1b\n\n");
printf("Options:\n");
printf("Parameters for training:\n");
printf("\t-train <file>\n"); // 指定训练文件
printf("\t\tUse text data from <file> to train the model\n");
printf("\t-output <file>\n"); // 指定输出文件,以存储word vectors,或者单词类
printf("\t\tUse <file> to save the resulting word vectors / word clusters\n");
printf("\t-size <int>\n"); // word vector的维数,对应 layer1_size,默认是100
printf("\t\tSet size of word vectors; default is 100\n");
// 窗口大小,在cbow中表示了word vector的最大的叠加范围,在skip-gram中表示了max space between words(w1,w2,p(w1 | w2))
printf("\t-window <int>\n");
printf("\t\tSet max skip length between words; default is 5\n");
printf("\t-sample <float>\n"); // 亚采样拒绝概率的参数
printf("\t\tSet threshold for occurrence of words. Those that appear with higher frequency");
printf(" in the training data will be randomly down-sampled; default is 0 (off), useful value is 1e-5\n");
printf("\t-hs <int>\n"); // 使用hs求解,默认为1
printf("\t\tUse Hierarchical Softmax; default is 1 (0 = not used)\n");
printf("\t-negative <int>\n"); // 使用ns的时候采样的样本数
printf("\t\tNumber of negative examples; default is 0, common values are 5 - 10 (0 = not used)\n");
printf("\t-threads <int>\n"); // 指定线程数
printf("\t\tUse <int> threads (default 1)\n");
printf("\t-min-count <int>\n"); // 长尾词的词频阈值
printf("\t\tThis will discard words that appear less than <int> times; default is 5\n");
printf("\t-alpha <float>\n"); // 初始的学习速率,默认为0.025
printf("\t\tSet the starting learning rate; default is 0.025\n");
printf("\t-classes <int>\n"); // 输出单词类别数,默认为0,也即不输出单词类
printf("\t\tOutput word classes rather than word vectors; default number of classes is 0 (vectors are written)\n");
printf("\t-debug <int>\n"); // 调试等级,默认为2
printf("\t\tSet the debug mode (default = 2 = more info during training)\n");
printf("\t-binary <int>\n"); // 是否将结果输出为二进制文件,默认为0,即不输出为二进制
printf("\t\tSave the resulting vectors in binary moded; default is 0 (off)\n");
printf("\t-save-vocab <file>\n"); // 词汇表存储文件
printf("\t\tThe vocabulary will be saved to <file>\n");
printf("\t-read-vocab <file>\n"); // 词汇表加载文件,则可以不指定trainfile
printf("\t\tThe vocabulary will be read from <file>, not constructed from the training data\n");
printf("\t-cbow <int>\n"); // 使用cbow框架
printf("\t\tUse the continuous bag of words model; default is 0 (skip-gram model)\n");
printf("\nExamples:\n"); // 使用示例
printf("./word2vec -train data.txt -output vec.txt -debug 2 -size 200 -window 5 -sample 1e-4 -negative 5 -hs 0 -binary 0 -cbow 1\n\n");
return 0;
}
// 文件名均空
output_file[0] = 0;
save_vocab_file[0] = 0;
read_vocab_file[0] = 0;
// 参数与变量的对应关系
if ((i = ArgPos((char *)"-size", argc, argv)) > 0) layer1_size = atoi(argv[i + 1]);
if ((i = ArgPos((char *)"-train", argc, argv)) > 0) strcpy(train_file, argv[i + 1]);
if ((i = ArgPos((char *)"-save-vocab", argc, argv)) > 0) strcpy(save_vocab_file, argv[i + 1]);
if ((i = ArgPos((char *)"-read-vocab", argc, argv)) > 0) strcpy(read_vocab_file, argv[i + 1]);
if ((i = ArgPos((char *)"-debug", argc, argv)) > 0) debug_mode = atoi(argv[i + 1]);
if ((i = ArgPos((char *)"-binary", argc, argv)) > 0) binary = atoi(argv[i + 1]);
if ((i = ArgPos((char *)"-cbow", argc, argv)) > 0) cbow = atoi(argv[i + 1]);
if ((i = ArgPos((char *)"-alpha", argc, argv)) > 0) alpha = atof(argv[i + 1]);
if ((i = ArgPos((char *)"-output", argc, argv)) > 0) strcpy(output_file, argv[i + 1]);
if ((i = ArgPos((char *)"-window", argc, argv)) > 0) window = atoi(argv[i + 1]);
if ((i = ArgPos((char *)"-sample", argc, argv)) > 0) sample = atof(argv[i + 1]);
if ((i = ArgPos((char *)"-hs", argc, argv)) > 0) hs = atoi(argv[i + 1]);
if ((i = ArgPos((char *)"-negative", argc, argv)) > 0) negative = atoi(argv[i + 1]);
if ((i = ArgPos((char *)"-threads", argc, argv)) > 0) num_threads = atoi(argv[i + 1]);
if ((i = ArgPos((char *)"-min-count", argc, argv)) > 0) min_count = atoi(argv[i + 1]);
if ((i = ArgPos((char *)"-classes", argc, argv)) > 0) classes = atoi(argv[i + 1]);
vocab = (struct vocab_word *)calloc(vocab_max_size, sizeof(struct vocab_word));
vocab_hash = (int *)calloc(vocab_hash_size, sizeof(int));
expTable = (real *)malloc((EXP_TABLE_SIZE + 1) * sizeof(real));
// 提前产生e^-6 到 e^6 之间的f值 ,便于提高运算效率
for (i = 0; i < EXP_TABLE_SIZE; i++) {
expTable[i] = exp((i / (real)EXP_TABLE_SIZE * 2 - 1) * MAX_EXP); // Precompute the exp() table
expTable[i] = expTable[i] / (expTable[i] + 1); // Precompute f(x) = x / (x + 1)
}
TrainModel();
return 0;
}
word2vec使用说明:
Google的word2vec官网:https://code.google.com/p/word2vec/
下载下来的Demo源码文件共有如下几个:
word2vec – Revision 41: /trunk
…
LICENSE //Apache LICENSE
README.txt //工具使用说明
compute-accuracy.c
demo-analogy.sh //
demo-classes.sh //词聚类的示例脚本
demo-phrase-accuracy.sh
demo-phrases.sh //
demo-train-big-model-v1.sh
demo-word-accuracy.sh
demo-word.sh
distance.c
makefile
questions-phrases.txt
questions-words.txt
word-analogy.c
word2phrase.c
word2vec.c
【快速入门】:
1. 从http://word2vec.googlecode.com/svn/trunk/ 下载所有相关代码(所有的文件如上);
2. 运行make编译word2vec工具:Makefile的编译代码在makefile.txt文件中,先改名makefile.txt 为Makefile,然后在当前目录下执行make进行编译,生成可执行文件(编译过程中报出很出Warning,暂且不管);
3. 运行示例脚本:./demo-word.sh 和 ./demo-phrases.sh:看一下./demo-word.sh的内容,大致执行了3步操作
a). 从http://mattmahoney.net/dc/text8.zip 下载了一个文件text8 ( 一个解压后不到100M的txt文件,可自己下载并解压放到同级目录下),
b). 执行word2vec生成词向量到 vectors.bin文件中,(速度比较快,几分钟的事情)
2.将分好词的训练语料进行训练,假定我语料名称为test.txt且在word2vec目录中。输入命令:
./word2vec -train text8 -output vectors.bin -cbow 0 -size 48 -window 5 -negative 0 -hs 1 -sample 1e-4 -threads 20 -binary 1 -iter 100
以上命令 -train text8 表示的是输入文件是text8,-output vectors.bin 输出文件是vectors.bin,-cbow 0表示不使用cbow模型,默认为Skip-Gram模型。-size 48 每个单词的向量维度是48,-window 5 训练的窗口大小为5就是考虑一个词前五个和后五个词语(实际代码中还有一个随机选窗口的过程,窗口大小小于等于5)。-negative 0 -hs 1不使用NEG方法,使用HS方法。-sampe指的是采样的阈值,如果一个词语在训练样本中出现的频率越大,那么就越会被采样。-binary为1指的是结果二进制存储,为0是普通存储(普通存储的时候是可以打开看到词语和对应的向量的)除了以上命令中的参数,word2vec还有几个参数对我们比较有用比如-alpha设置学习速率,默认的为0.025. –min-count设置最低频率,默认是5,如果一个词语在文档中出现的次数小于5,那么就会丢弃。-classes设置聚类个数,看了一下源码用的是k-means聚类的方法。要注意-threads 20 线程数也会对结果产生影响。
注意:–min-count设置最低频率,默认是5,进行参数传递无效,我们可能是因为参数名中有-,唉我们只好在程序word2vec.c中将min-count初始化为1了。
· 架构:skip-gram(慢、对罕见字有利)vs CBOW(快)
· 训练算法:分层softmax(对罕见字有利)vs 负采样(对常见词和低纬向量有利)
· 欠采样频繁词:可以提高结果的准确性和速度(适用范围1e-3到1e-5)
· 文本(window)大小:skip-gram通常在10附近,CBOW通常在5附近
[root@localhost /home/jacoxu/word2vec]$./demo-word.sh
make: Nothing to be done for `all’.
Starting training using file text8
Vocab size: 71291
Words in train file: 16718843
Alpha: 0.000560 Progress: 98.88% Words/thread/sec: -117.11k
c). 执行./distance vectors.bin可以进入一个计算word相似度的demo中去,如下:
========中断========
52 if (ch == ‘\n’) {
53 strcpy(word, (char *)”");
148 // Sort the vocabulary and keep at the first position
149 qsort(&vocab[1], vocab_size – 1, sizeof(struct vocab_word), VocabCompare);
—-
看代码,应该是\n这个词,而且特意排在第一位了
====================
对于训练出来的模型进行操作,我推荐大家使用http://blog.csdn.net/zhaoxinfan/article/details/11640573这个java版本的模型读取类,比较方便。可以参照:http://blog.csdn.net/jj12345jj198999/article/details/11069485
Enter word or sentence (EXIT to break): china
Word: china Position in vocabulary: 486
Word Cosine distance (找到了很多和china共现度很高的词汇,可以得到认可)
————————————————————————
taiwan 0.656181
japan 0.633499
tibet 0.607813
manchuria 0.581230
hainan 0.561931
xiamen 0.555860
chongqing 0.550099
jiang 0.549195
chinese 0.548320
liao 0.548220
…
Enter word or sentence (EXIT to break): hongkong
Word: hongkong Position in vocabulary: 24623
Word Cosine distance(感觉效果很糟糕,有些不知所云,认可度很低,主要还是因为语料太少,覆盖面窄)
————————————————————————
jardines 0.708792
matheson 0.659444
jardine 0.611295
shareholding 0.489012
company 0.431918
kowloon 0.417640
firm 0.415780
venture 0.415077
plc 0.409350
shanghai 0.404716
…
然后看一下./demo-phrases.sh的内容,也大致执行了3步操作:
a). 从http://www.statmt.org/wmt14/training-monolingual-news-crawl/news.2012.en.shuffled.gz 下载了一个文件news.2012.en.shuffled.gz ( 一个解压到1.7G的txt文件,可自己下载并解压放到同级目录下);
b). 将文件中的内容拆分成 phrases,然后执行./word2vec生成短语向量到 vectors-phrase.bin文件中(数据量大,速度慢,将近半个小时),如下:
[root@localhost /home/jacoxu/word2vec]$ bash demo-phrases.sh (总耗时75分钟)
make: Nothing to be done for `all’.
Starting training using file news.2012.en.shuffled-norm0
Words processed: 296900K Vocab size: 33198K
Vocab size (unigrams + bigrams): 18838711
Words in train file: 296901342
Words written: 296900K
real 11m1.465s
user 10m47.888s
sys 0m8.317s
Starting training using file news.2012.en.shuffled-norm0-phrase0
Words processed: 280500K Vocab size: 38761K
Vocab size (unigrams + bigrams): 21728781
Words in train file: 280513979
Words written: 280500K
real 10m43.277s
user 10m29.983s
sys 0m8.137s
Starting training using file news.2012.en.shuffled-norm1-phrase1
Vocab size: 681320
Words in train file: 283545447
Alpha: 0.000005 Progress: 100.00% Words/thread/sec: 83.67k
real 49m3.925s
user 847m21.369s
sys 1m11.140s
c). 执行./distance vectors-phrase.bin 可以进入一个计算word相似度的demo中去,如下:
Enter word or sentence (EXIT to break): great_wall
Word: great_wall Position in vocabulary: 36052
Word Cosine distance (确实找到一些极为相关的词汇)
————————————————————————
mutianyu 0.588232
terra_cotta_warriors 0.541900
forbidden_city 0.527797
changsha 0.520975
based_koryo_tours 0.510810
three_gorges 0.509765
world’s_tallest 0.501899
dajie 0.500128
ming_dynasty 0.496112
qin_shihuang 0.486954
…
Enter word or sentence (EXIT to break): hong_kong
Word: hong_kong Position in vocabulary: 2322
Word Cosine distance (并没有任何体现Hong Kong本土的词汇,仍是语料的问题)
————————————————————————
singapore 0.755051
mainland_china 0.739481
shanghai 0.732764
hong_kong’s 0.727834
mainland_chinese 0.652696
singapore’s 0.636082
keith_bradsher_contributed_reporting 0.626221
hang_seng_index_fell 0.621854
chinese 0.617985
taipei 0.609921
…
【其他】:Java版本Word2Vector
词向量的训练最经典的有 3 个工作,C&W 2008、M&H 2008、Mikolov 2010,看看其他几个版本的Word Embedding:
(以下大部分内容摘自Zhao老板的学生licstar)
1. 【C&W 的 SENNA – 2008】
■ 训练语料及规模:English Wikipedia + Reuters RCV1 共 631M + 221M 词;
■ 词向量:130000 词,50 维;
■ 特点:不区分大小写,经过有监督修正,训练了7周;
■ 资源:测试代码、词向量 [链接]
Ronan Collobert 和 Jason Weston 在 2008 年的 ICML 上发表的《A Unified Architecture for Natural Language Processing: Deep Neural Networks with Multitask Learning》里面首次介绍了他们提出的词向量的计算方法,他们还把论文所写的系统开源了,叫做 SENNA,3500 多行纯 C 代码也是写得非常清晰。C&W 这篇论文主要目的并不是在于生成一份好的词向量,甚至不想训练语言模型,而是要用这份词向量去完成 NLP 里面的各种任务,比如词性标注、命名实体识别、短语识别、语义角色标注等等。
2. 【M&H 的 HLBL – 2007】
Andriy Mnih 和 Geoffrey Hinton 在 2007 年和 2008 年各发表了一篇关于训练语言模型和词向量的文章。2007 年发表在 ICML 上的《Three new graphical models for statistical language modelling》表明了 Hinton 将 Deep Learning 战场扩展到 NLP 领域的决心。2008 年发表在 NIPS 上的《A scalable hierarchical distributed language model》则提出了一种层级的思想替换了 Bengio 2003 方法中最后隐藏层到输出层最花时间的矩阵乘法,在保证效果的基础上,同时也提升了速度。2008 年 NIPS 的这篇论文,介绍的是“hierarchical log-bilinear”模型,很多论文中都把它称作简称“HLBL”。
3. 【Mikolov 的 RNNLM – 2010】
■ 训练语料及规模:Broadcast news;
■ 词向量:82390 词,80、640、1600 维;
■ 特点:不区分大小写;训练了若干天;
■ 资源:训练、测试代码、词向量 [链接]
Bengio 2003 在论文里提到一句,可以使用一些方法降低参数个数,比如用循环神经网络。Mikolov 就抓住了这个坑,从此与循环神经网络结下了不解之缘。他最早用循环神经网络做语言模型是在 INTERSPEECH 2010 上发表的《Recurrent neural network based language model》里。Recurrent neural network 是循环神经网络,简称 RNN,还有个 Recursive neural networks 是递归神经网络(Richard Socher 借此发了一大堆论文),也简称 RNN。看到的时候需要注意区分一下。不过到目前为止,RNNLM 只表示循环神经网络做的语言模型,还没有歧义。
在之后的几年中,Mikolov 在一直在RNNLM 上做各种改进,有速度上的,也有准确率上的。现在想了解 RNNLM,看他的博士论文《Statistical Language Models based on Neural Networks》肯定是最好的选择。
4. 【Huang 的语义强化 – 2012】
■ 训练语料及规模:English Wikipedia;
■ 词向量:100232 词,50 维;
■ 特点:不区分大小写,最高频的6000词,每词有10种表示;
■ 资源:训练、测试代码、词向量 [链接]
与前几位大牛的工作不同,Eric H. Huang 的工作是在 C&W 的基础上改进而成的,并非自成一派从头做起。他这篇发表在 ACL 2012 上的《Improving Word Representations via Global Context and Multiple Word Prototypes》试图通过对模型的改进,使得词向量富含更丰富的语义信息。他在文中提出了两个主要创新来完成这一目标:(其实从论文标题就能看出来)第一个创新是使用全文信息辅助已有的局部信息,第二个创新是使用多个词向量来表示多义词。
Huang 认为 C&W 的工作只利用了“局部上下文(Local Context)”。C&W 在训练词向量的时候,只使用了上下文各 5 个词,算上自己总共有 11 个词的信息,这些局部的信息还不能充分挖掘出中间词的语义信息。Huang 直接使用 C&W 的网络结构计算出一个得分,作为“局部得分”。
然后 Huang 提出了一个“全局信息”,这有点类似传统的词袋子模型。词袋子模型是把文章中所有词的 One-hot Representation 加起来,形成一个向量(就像把词全都扔进一个袋子里),用来表示文章。Huang 的全局模型是将文章中所有词的词向量求个加权平均(权重是词的 idf),作为文章的语义。他把文章的语义向量和当前词的词向量拼接起来,形成一个两倍长度的向量作为输入,之后还是用 C&W 的网络结构算出一个打分。
有了 C&W 方法的得到的“局部得分”,再加上在 C&W 方法基础上改造得到的“全局得分”,Huang 直接把两个得分相加,作为最终得分。最终得分使用 C&W 提出的 pair-wise 目标函数来优化。
加了这个全局信息有什么用处呢?Huang 在实验中发现,他的模型能更好地捕捉词的语义信息。比如 C&W 的模型中,与 markets 最相近的词为 firms、industries;而 Huang 的模型得到的结果是 market、firms。很明显,C&W 的方法由于只考虑了临近词的信息,最后的结果是词法特征最相近的词排在了前面(都是复数形式)。不过我觉得这个可能是英语才有的现象,中文没有词形变化,如果在中文中做同样的实验还不知道会有什么效果。
Huang 论文的第二个贡献是将多义词用多个词向量来表示。Bengio 2003 在最后提过这是一个重要的问题,不过当时他还在想办法解决,现在 Huang 给出了一种思路。
将每个词的上下文各 5 个词拿出来,对这 10 个词的词向量做加权平均(同样使用 idf 作为权重)。对所有得到的上下文向量做 k-means 聚类,根据聚类结果给每个词打上标签(不同类中的同一个词,当作不同的词处理),最后重新训练词向量。
当然这个实验的效果也是很不错的,最后 star 的某一个表示最接近的词是 movie、film;另一个表示最接近的词是 galaxy、planet。
5. 【Turian 对比试验 – 2010】
优化C&W代码并重跑试验
■ 训练语料及规模:Reuters RCV1 63M 词;
■ 词向量:268810 词 25、50、100、200 维;
■ 特点:区分大小写,训练了若干周;
■ 资源:训练代码、词向量 [链接]
由M帮忙重跑M&H
■ 训练语料及规模:Reuters RCV1;
■ 词向量:246122 词 50、100 维;
■ 特点:区分大小写,用GPU训练了7天;
■ 资源:训练代码、词向量 [链接]
问题?主页上提供了 两份 词向量,一份是 scaled,一份是unscaled,scaled的作用是什么?这个Scale貌似是全局的,而非单个向量的。
README.text中是这么解释的:
embeddings-scaled.*.txt.gz
Embeddings scaled by 0.1/stddev(embeddings), as described in the ACL 2010 paper.
These are the embeddings you should use by default, if you just want word features.
The first column is the word, the rest of the columns are the dimensions of the embedding.
也就是说通过stddev进行了一个全局的样本标准偏差进行缩放;
Turian 的工作前面只是提了一下,他在做 C&W 向量与 H&M 向量的对比实验时,自己按照论文重新实现了一遍他们的方法,并公布了词向量。后来 C&W 在主页上强调了一下:尽管很多论文把 Turian 实现的结果叫做 C&W 向量,但是与我发布的词向量是不同的,我这个在更大的语料上训练,还花了两个月时间呢!
Turian 公布的 M&H 向量是直接请 Andriy Mnih 在 Turian 做好的语料上运行了一下 HLBL,所以没有代码公布。同时 Turian 自己实现了一份 LBL模型,但是没有公布训练出来的词向量。(这是根据他主页上描述推测的结果,从 Turian 的论文中看,他应该是实现了 HLBL 算法并且算出词向量的。)
RCV1 的词数两篇文章中所写的数据差距较大,还不知道是什么原因。
Turian 发表在 ACL 2010 上的实验对比了 C&W 向量与 M&H 向量用作辅助特征时的效果。在短语识别和命名实体识别两个任务中,C&W 向量的效果都有略微的优势。同时他也发现,如果将这两种向量融合起来,会有更好的效果。除了这两种词向量,Turian 还使用 Brown Cluster 作为辅助特征做了对比,效果最好的其实是 Brown Cluster,不过这个已经超出本文的范围了。
【值得一读】:
1. What are the continuous bag of words and skip-gram architectures
2. Hierarchical Softmax & Negative Sampling
3. word2vec 中的数学原理详解
4. Deep Learning in NLP (一)词向量和语言模型:http://licstar.net/archives/328
【问题语料训练实例】
1. 搜集语料
StackOverflow语料下载train.zip https://www.kaggle.com/c/predict-closed-questions-on-stack-overflow/ 进行处理,得到content内容和title标题作为训练语料
2. 利用word2vec进行训练,采用和官网提供的300维Vector同样的训练模型:
-cbow 1 -size 300 -window 5 -negative 3 -hs 0 -sample 1e-5 -threads 12 -binary 1 -iter 15
(注:默认的低频下限是5,由于当前版本(word2vec42)程序有bug,原因不太清楚,如果初始化失败,则到word2vec.c文件中修改此值)
475630606个words, vocab 大小为1598580,大概花费25分钟。
运行结果并测试效果如下:
[root@linux-237 word2vec42]# bash demo-word.sh
make: Nothing to be done for `all’.
Starting training using file stackText.txt
Vocab size: 1598580
Words in train file: 475630606
Alpha: 0.000263 Progress: 99.47% Words/thread/sec: 541.23k
real 25m39.774s
user 221m57.108s
sys 0m49.199s
Enter word or sentence (EXIT to break): java
Word: java Position in vocabulary: 454
Word Cosine distance
————————————————————————
Java 0.804742
java, 0.780371
java. 0.775374
JAVA 0.743753
Java. 0.683836
swing 0.674023
android 0.672503
c++ 0.668149
c# 0.644594
python 0.643520
eclipse 0.643359
Java, 0.637573
java? 0.630209
jar 0.628616
groovy 0.627132
scala 0.611233
servlet 0.610064
applet 0.608627
php 0.608549
i 0.600868
my 0.599664
jsp 0.598296
ruby 0.597236
netbeans 0.595057
a 0.595011
write 0.594350
program 0.593679
jdk 0.593030
j2me 0.587537
which 0.585925
to 0.585683
using 0.580617
that 0.579790
written 0.579572
some 0.579534
android, 0.577219
and 0.575742
.net 0.575061
web 0.574278
linux 0.573493
Enter word or sentence (EXIT to break): html
Word: html Position in vocabulary: 394
Word Cosine distance
————————————————————————
HTML 0.853783
html. 0.836931
html, 0.795768
javascript 0.763503
HTML. 0.721012
js 0.715835
webpage 0.711215
HTML, 0.700154
content 0.694945
markup 0.691843
Html 0.686390
css 0.683201
tags 0.676316
javascript, 0.676179
xhtml 0.674844
javascript. 0.674052
php 0.673596
text 0.673089
page 0.663661
xml 0.662498
iframe 0.661740
jquery 0.652209
tag 0.651575
webpage, 0.647792
content, 0.644945
.html 0.641437
aspx 0.638099
ajax 0.637875
html? 0.633876
jsp 0.631024
tags, 0.624686
textarea 0.624658
webpage. 0.624080
pdf 0.622019
page, 0.620723
contents 0.617092
JS 0.615214
text, 0.613630
form 0.613238
css. 0.612103
Enter word or sentence (EXIT to break): android
Word: android Position in vocabulary: 495
Word Cosine distance
————————————————————————
Android 0.857668
android. 0.814769
android, 0.810972
iphone 0.744005
blackberry 0.719089
Android. 0.718010
androids 0.693840
Android, 0.687129
emulator 0.680624
java 0.672503
android? 0.664975
phone. 0.661444
Blackberry 0.658390
andriod 0.653750
android’s 0.648015
phonegap 0.641884
emulator. 0.640713
iPhone 0.639118
ios 0.639108
tablet 0.636023
Iphone 0.635636
j2me 0.632937
phone 0.630827
mobile 0.624277
device 0.621280
apk 0.619061
phone, 0.611067
ICS 0.610646
adroid 0.606088
emulator, 0.604733
activity 0.603200
2.2 0.602148
eclipse 0.599507
Honeycomb 0.597581
device. 0.597449
IOS 0.597157
honeycomb 0.596399
iphone. 0.595373
sdk 0.592870
iOS 0.591931
参考:https://groups.google.com/forum/#!topic/word2vec-toolkit/lxbl_MB29Ic Tomas Mikolov 用了9个小时进行训练WIKI数据。
./word2vec -train train100B.txt -read-vocab voc -output vectors.bin -cbow 1 -size 300 -window 5 -negative 3 -hs 0 -sample 1e-5 -threads 12 -binary 1 -min-count 10
除了上面的语料,Tomas Mikolov,在他的word2vec主页上还提供了WIKI语料的链接,以及XML文件预处理的批文件。
使用方式为:perl wikifil.pl enwik9 > text (下载地址:http://mattmahoney.net/dc/textdata.html)
有人问道:embedding作为将词映射为一个向量作为输入层,词向量应该要有更新才对,但是作为第一层的输入层本应该是不变的。这样分析是否矛盾?
我的回答:确实大家都说采用词向量做为DNN的输入,但是并非第一层输入。可以换个角度看,第一层应该是one-hot词特征,通过乘以一个权重矩阵W(即词向量矩阵)得到词向量化表示。这样看的话,词向量矩阵只是一个权重参数,在模型训练阶段是可以更新的。从记忆的角度来看,神经网络连接的权值更新都是长时记忆,因而词向量的学习可以认为是一种长时学习。