- word2vec中有5个程序,其中demo-word.sh中涉及两个:word2vec、distance。考虑到distance比较简单,所以我从这个入手,希望通过简单代码理解如何在一个高维数据空间计算距离(查找)。
- 一维数据的查找,一般是通过二分法进行比较,找到完全相等的元素。完全相等本质是距离为0.
- 推论,高维词向量空间中,找到完全相等的结果,是不可能的。于是,我们用距离较近的N个单词作为结果集。后续,可以在此结果集基础上做进一步的运算。
- 一维坐标轴上,两个点的距离是|x1-x2| 也可以用sqrt((x1-x2)*(x1-x2))。
- 为什么不直接用(x1-x2) ,而要用平方后开方?因为距离这个一维概念只能大于等于0,等于0就是同一个点。
- 二维坐标轴上,两个点的距离是sqrt((x1-x2)(x1-x2)+(y1-y2)(y1-y2)),这是勾股定理,计算二维点的一维距离值。
- 3维到N维坐标轴上,两个点的距离是sqrt((x1-x2)(x1-x2)+(y1-y2)(y1-y2)+…+(z1-z2)*(z1-z2))
- 归一化,相当于把所有词向量都映射到了R==1的单位圆单位球上了,只是这是一个N维球体,无法想象,但三维的计算方式可以推理应用到任意维度。很明显,高维数学知识至少是需要了解的。
for (a = 0; a < dim_size; a++) len += f_mat[a + b * dim_size] * f_mat[a + b * dim_size];//向量平方和,向量模的平方len = (float)sqrt(len);//向量模长度if (len == 0.0f) {fprintf(stderr, "Warning: Vector length is zero for word: %s\n", &vocab[b * max_word_len]);continue; // 跳过当前单词的归一化}for (a = 0; a < dim_size; a++) f_mat[a + b * dim_size] /= len;//归一化向量,归一化后的平方和等于1
- 句子的向量计算
for (a = 0; a < dim_size; a++) vec[a] = 0;//初始化多个输入单词对应的向量累计值for (b = 0; b < input_word_count; b++) {if (input_word_index[b] == -1) continue;//把多个输入单词的向量相加,可以理解为句子的向量for (a = 0; a < dim_size; a++) vec[a] += f_mat[a + input_word_index[b] * dim_size];}len = 0;for (a = 0; a < dim_size; a++) len += vec[a] * vec[a];//句子的向量模平方len = (float)sqrt(len);//句子的向量模长for (a = 0; a < dim_size; a++) vec[a] /= len;//句子的向量归一化
- 输入单词/句子的向量归一化
for (a = 0; a < dim_size; a++) vec[a] = 0;//初始化多个输入单词对应的向量累计值for (b = 0; b < input_word_count; b++) {if (input_word_index[b] == -1) continue;//把多个输入单词的向量相加,可以理解为句子的向量for (a = 0; a < dim_size; a++) vec[a] += f_mat[a + input_word_index[b] * dim_size];}len = 0;for (a = 0; a < dim_size; a++) len += vec[a] * vec[a];//句子的向量模平方len = (float)sqrt(len);//句子的向量模长for (a = 0; a < dim_size; a++) vec[a] /= len;//句子的向量归一化
- 输入单词/句子与所有单词之间的距离
此处距离计算直接是x1x2+y1y2+…+z1*z2,实际是点积计算、余弦相似度计算
for (a = 0; a < dim_size; a++)
dist += vec[a] * f_mat[a + c * dim_size];
这个原理见下图
distance.c源码注释
//https://github.com/swordll80/word2vec/blob/master/distance.c
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>#ifdef _WIN32
#define max_size ((long long)2000) // max length of strings
#define closest_word_count ((long long)40) // number of closest words that will be shown,输出时展示与输入单词余弦相似度最高的前 N 个单词
#define max_word_len ((long long)50) // max length of vocabulary entries,词汇表中单词的最大长度
#else
const long long max_size = 2000; // max length of strings
const long long closest_word_count = 40; // number of closest words that will be shown
const long long max_w = 50; // max length of vocabulary entries
#endifvoid print_word_vec(char* word_str, float* f_mat, long long size); //打印单个单词的向量值int main(int argc, char** argv) {FILE* word_vec_bin_file; //word2vec程序训练出来的二进制数据文件。C:/code/word2vec/vectors.binchar input_str_line[max_size]; //存储用户输入的字符串(单词或句子)char* best_word_str[closest_word_count];//存储与输入单词最接近的前 N 个单词。char file_name[max_size]; //存储词向量文件的路径。char str_array[100][max_size]; //将用户输入的字符串拆分成单个单词存储float dist; //用于计算输入向量与其他向量之间的余弦相似度。float len; //向量的长度(模),用于归一化。float best_dist[closest_word_count];//存储与输入单词最接近的前 N 个单词的余弦相似度值。float vec[max_size]; //用户输入单词向量的累积值。long long words_count; //文件中包含的单词总数。long long dim_size; //每个单词向量的维度。long long a, b, c, d; //循环变量。long long input_word_count; //输入的单词数long long input_word_index[100];//输入单词在词汇表中的索引float* f_mat; //存储所有词向量的矩阵(按行存储)char* vocab; //存储词汇表中所有单词。if (argc < 2) {printf("Usage: ./distance <FILE>\nwhere FILE contains word projections in the BINARY FORMAT\n");return 0;}strcpy_s(file_name, sizeof(file_name), argv[1]);errno_t err = fopen_s(&word_vec_bin_file, file_name, "rb");if (word_vec_bin_file == NULL || 0 != err) {printf("Input file not found\n");return -1;}fscanf_s(word_vec_bin_file, "%lld", &words_count); //71291 (用字符串存储,后面是一个空格0x20)fscanf_s(word_vec_bin_file, "%lld", &dim_size); //200 (用字符串存储,后面是一个0x0A==\n==换行)vocab = (char*)malloc((long long)words_count * max_word_len * sizeof(char));//单词表,每个单词最长50for (a = 0; a < closest_word_count; a++) best_word_str[a] = (char*)malloc(max_size * sizeof(char));//相似度最高的词条或句子f_mat = (float*)malloc((long long)words_count * (long long)dim_size * sizeof(float));//每个单词用200个32位浮点数存储if (f_mat == NULL) {printf("Cannot allocate memory: %lld MB %lld %lld\n", (long long)words_count * dim_size * sizeof(float) / 1048576, words_count, dim_size);return -1;}for (b = 0; b < words_count; b++) {//71291个单词循环a = 0;while (1) {//读一个单词,以空格结束vocab[b * max_word_len + a] = (char)fgetc(word_vec_bin_file);//读一个字符if (feof(word_vec_bin_file) || (vocab[b * max_word_len + a] == ' ')) break;//空格或文件结束if ((a < max_word_len) && (vocab[b * max_word_len + a] != '\n')) a++;//忽略换行符\n}vocab[b * max_word_len + a] = 0;//单词结束符for (a = 0; a < dim_size; a++) fread(&f_mat[a + b * dim_size], sizeof(float), 1, word_vec_bin_file);//读取一个单词的200个float,后面有一个\nlen = 0;//if (0 == b)print_word_vec(&vocab[b * max_w], &M[b * size], size);//输出测试for (a = 0; a < dim_size; a++) len += f_mat[a + b * dim_size] * f_mat[a + b * dim_size];//向量平方和,向量模的平方len = (float)sqrt(len);//向量模长度if (len == 0.0f) {fprintf(stderr, "Warning: Vector length is zero for word: %s\n", &vocab[b * max_word_len]);continue; // 跳过当前单词的归一化}for (a = 0; a < dim_size; a++) f_mat[a + b * dim_size] /= len;//归一化向量,归一化后的平方和等于1//if (0 == b)print_word_vec(&vocab[b * max_w], &M[b * size], size);//输出测试}fclose(word_vec_bin_file);while (1) {for (a = 0; a < closest_word_count; a++) best_dist[a] = 0;//初始化for (a = 0; a < closest_word_count; a++) best_word_str[a][0] = 0;//初始化printf("Enter word or sentence (EXIT to break): ");a = 0;while (1) {//读取用户输入的单词或句子,例如 franceinput_str_line[a] = (char)fgetc(stdin);if ((input_str_line[a] == '\n') || (a >= max_size - 1)) {input_str_line[a] = 0;break;}a++;}if (!strcmp(input_str_line, "EXIT")) break;//退出程序input_word_count = 0;b = 0;c = 0;while (1) {str_array[input_word_count][b] = input_str_line[c];b++;c++;str_array[input_word_count][b] = 0;if (input_str_line[c] == 0) break;if (input_str_line[c] == ' ') {//输入的是句子,拆为多个单词存放input_word_count++;b = 0;c++;}}input_word_count++;for (a = 0; a < input_word_count; a++) {for (b = 0; b < words_count; b++) if (!strcmp(&vocab[b * max_word_len], str_array[a])) break;if (b == words_count) b = -1;//没找到用户输入的单词input_word_index[a] = b;//找到每个单词的序号,例如 france 对应 vocab[303]printf("\nWord: %s Position in vocabulary: %lld\n", str_array[a], input_word_index[a]);if (b == -1) {printf("Out of dictionary word!\n");break;}}if (b == -1) continue;printf("\n Word Cosine distance\n------------------------------------------------------------------------\n");for (a = 0; a < dim_size; a++) vec[a] = 0;//初始化多个输入单词对应的向量累计值for (b = 0; b < input_word_count; b++) {if (input_word_index[b] == -1) continue;//把多个输入单词的向量相加,可以理解为句子的向量for (a = 0; a < dim_size; a++) vec[a] += f_mat[a + input_word_index[b] * dim_size];}len = 0;for (a = 0; a < dim_size; a++) len += vec[a] * vec[a];//句子的向量模平方len = (float)sqrt(len);//句子的向量模长for (a = 0; a < dim_size; a++) vec[a] /= len;//句子的向量归一化for (a = 0; a < closest_word_count; a++) best_dist[a] = -1;//初始化距离for (a = 0; a < closest_word_count; a++) best_word_str[a][0] = 0;//初始化字符串for (c = 0; c < words_count; c++) {//与所有单词计算距离a = 0;for (b = 0; b < input_word_count; b++) if (input_word_index[b] == c) a = 1;if (a == 1) continue;//跳过句子中出现过的单词dist = 0;for (a = 0; a < dim_size; a++) dist += vec[a] * f_mat[a + c * dim_size];//输入句子与单词的距离计算for (a = 0; a < closest_word_count; a++) {if (dist > best_dist[a]) {//插入找到的距离更大的单词(dist值越大,相关度越大)for (d = closest_word_count - 1; d > a; d--) {//移动后面的单词best_dist[d] = best_dist[d - 1];strcpy_s(best_word_str[d], max_size, best_word_str[d - 1]);}best_dist[a] = dist;strcpy_s(best_word_str[a], max_size, &vocab[c * max_word_len]);break;}}}for (a = 0; a < closest_word_count; a++) printf("%50s\t\t%f\n", best_word_str[a], best_dist[a]);}return 0;
}void print_word_vec(char* word_str, float* f_mat, long long size) {//打印一个单词的向量,典型值就是100个浮点数const int count_per_line = 10;//每行的浮点数const int line_count = (int)(size / count_per_line);//完整的行数const int less_count = size % count_per_line;//最后一行的数目printf("%s\n", NULL == word_str ? "NULL" : word_str);for (int line_index = 0; line_index < line_count; ++line_index) {for (int data_index = 0; data_index < count_per_line; ++data_index) {printf("%10.6f ", f_mat[line_index * count_per_line + data_index]);}printf("\n");}if (less_count > 0) {for (int data_index = 0; data_index < less_count; ++data_index) {printf("%10.6f ", f_mat[line_count * count_per_line + data_index]);}printf("\n");}
}
word2vec的输入文件text8的格式
解压后97657KB,将近100MB,全部是英文单词组成的文本,甚至找不到逗号句号。
有个特殊单词是代码中固定加入的:AddWordToVocab((char*)“”);
可能是作为某种分界符。
text8第一行(实际是文本软件强行的分行,源文件并没有换行符)及其翻译。
据说text8的内容来自维基百科。
word2vec的输出文件vectors.bin的格式
vectors.bin是一个二进制数据文件,56353KB,但实际上最前面两个整数参数就是字符串,中间的词条结束符就是空格,所以,严格来说,这个文件是个混合文件,里面的float是二进制存储方式。
vectors.txt形式附在后面。
distance程序只接受vectors.bin二进制数据形式。
上图中,
- 71291是当前文件词条总数,后面一个空格0x20分隔。
- 200是词向量维度,这个是运行word2vec的参数-size 200决定的,后面一个换行符0xA分隔。
- 第一个词条是代码强行加进去的,后面紧跟着一个空格0x20分隔。
- 第一个词条对应的200个float,紧挨着前面的空格存储,连4字节对齐都没有,0xF6就是第一个浮点数0xF628033B的第一个字节(实际是小尾的最后一个字节)。
- 第一个词条对应的200个float中的最后一个float在0x32B那个位置,0xF6A813BA,后面有一个换行符0xA分隔。
- 第二个词条是the,位置就是0x330,后面是空格分隔。
- 后面就是重复以上格式存储完71291个词条。
- 最后一个词条的最后一个float结束后,也有一个换行符0xA分隔。
本地win11下运行word2vec的完整参数说明
./word2vec.exe -train text8 -output vectors.bin-cbow 1 使用连续词袋模型cbow;默认值为 1(对于 skip-gram 模型使用 0)-size 200 词向量大小(维度)-window 8 词之间的最大跳过长度-negative 25 负面示例的数量-hs 0 使用分层 Softmax;默认值为 0(未使用)-sample 1e-4 词出现的阈值-threads 20 线程数-binary 1 以二进制模式保存结果向量;默认值为 0(关闭)-iter 15 训练迭代