学习随记:word2vec的distance程序源码注释、输入输出文件格式说明

  1. word2vec中有5个程序,其中demo-word.sh中涉及两个:word2vec、distance。考虑到distance比较简单,所以我从这个入手,希望通过简单代码理解如何在一个高维数据空间计算距离(查找)。
  2. 一维数据的查找,一般是通过二分法进行比较,找到完全相等的元素。完全相等本质是距离为0.
  3. 推论,高维词向量空间中,找到完全相等的结果,是不可能的。于是,我们用距离较近的N个单词作为结果集。后续,可以在此结果集基础上做进一步的运算。
  4. 一维坐标轴上,两个点的距离是|x1-x2| 也可以用sqrt((x1-x2)*(x1-x2))。
  5. 为什么不直接用(x1-x2) ,而要用平方后开方?因为距离这个一维概念只能大于等于0,等于0就是同一个点。
  6. 二维坐标轴上,两个点的距离是sqrt((x1-x2)(x1-x2)+(y1-y2)(y1-y2)),这是勾股定理,计算二维点的一维距离值。
  7. 3维到N维坐标轴上,两个点的距离是sqrt((x1-x2)(x1-x2)+(y1-y2)(y1-y2)+...+(z1-z2)*(z1-z2))
  8. 归一化,相当于把所有词向量都映射到了R==1的单位圆单位球上了,只是这是一个N维球体,无法想象,但三维的计算方式可以推理应用到任意维度。很明显,高维数学知识至少是需要了解的。
c 复制代码
		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
  1. 句子的向量计算
c 复制代码
		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;//句子的向量归一化
  1. 输入单词/句子的向量归一化
c 复制代码
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;//句子的向量归一化
  1. 输入单词/句子与所有单词之间的距离
    此处距离计算直接是x1x2+y1y2+...+z1*z2,实际是点积计算、余弦相似度计算
c 复制代码
for (a = 0; a < dim_size; a++) 
dist += vec[a] * f_mat[a + c * dim_size];

这个原理见下图

distance.c源码注释

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
#endif

void 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.bin
	char 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));//单词表,每个单词最长50
	for (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,后面有一个\n
		len = 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) {//读取用户输入的单词或句子,例如 france
			input_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二进制数据形式。

上图中,

  1. 71291是当前文件词条总数,后面一个空格0x20分隔。
  2. 200是词向量维度,这个是运行word2vec的参数-size 200决定的,后面一个换行符0xA分隔。
  3. 第一个词条是代码强行加进去的,后面紧跟着一个空格0x20分隔。
  4. 第一个词条对应的200个float,紧挨着前面的空格存储,连4字节对齐都没有,0xF6就是第一个浮点数0xF628033B的第一个字节(实际是小尾的最后一个字节)。
  5. 第一个词条对应的200个float中的最后一个float在0x32B那个位置,0xF6A813BA,后面有一个换行符0xA分隔。
  6. 第二个词条是the,位置就是0x330,后面是空格分隔。
  7. 后面就是重复以上格式存储完71291个词条。
  8. 最后一个词条的最后一个float结束后,也有一个换行符0xA分隔。

本地win11下运行word2vec的完整参数说明

c 复制代码
./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     训练迭代
相关推荐
MichaelIp2 天前
LLM大模型RAG内容安全合规检查
人工智能·python·安全·语言模型·自然语言处理·chatgpt·word2vec
御风@户外4 天前
学习随笔:word2vec在win11 vs2022下编译、测试运行
人工智能·word2vec
MarkHD5 天前
第二十六天 自然语言处理(NLP)词嵌入(Word2Vec、GloVe)
人工智能·自然语言处理·word2vec
后端研发Marion6 天前
【大模型】wiki中文语料的word2vec模型构建
人工智能·自然语言处理·word2vec
计算机真好丸6 天前
第N8周:使用Word2vec实现文本分类
人工智能·分类·word2vec
AIWhispers21 天前
Word2Vec:将词汇转化为向量的技术
人工智能·自然语言处理·word2vec
Illusionna.22 天前
Word2Vec 模型 PyTorch 实现并复现论文中的数据集
人工智能·pytorch·算法·自然语言处理·nlp·matplotlib·word2vec
volcanical23 天前
Word2Vec
人工智能·自然语言处理·word2vec
Point__Nemo1 个月前
深度学习Day-42:seq2seq翻译实战-Pytorch复现-小白版
深度学习·分类·word2vec