word2vec源码详解2

震惊!!!metapath2vec竟然是这样的直接改了这个代码!下面是看到的一篇解释word2vec代码的博客,转过来记记(第二部)

 

(原文链接为:https://blog.csdn.net/EnochX/article/details/52852271)

Word2Vec源码最详细解析(下)


        在这一部分中,重点分析的是Word2Vec源码中算法部分的实现,需要一定得算法理论基础,如果对CBOW和skip-gram算法没有了解的话,请参考csdn著名博客《word2vec中的数学原理详解》。其他词表构建、Haffman树生成等辅助函数参见《Word2Vec源码最详细解析(上)》。

该部分代码分析如下:

//初始化神经网络结构
void InitNet() {
  long long a, b;
  unsigned long long next_random = 1;
  //syn0存储的是词表中每个词的词向量
  //这里为syn0分配内存空间
  //调用posiz_memalign来获取一块数量为vocab_size * layer1_size,128byte页对齐的内存
  //其中layer1_size是词向量的长度
  a = posix_memalign((void **)&syn0, 128, (long long)vocab_size * layer1_size * sizeof(real));
  if (syn0 == NULL) {printf("Memory allocation failed\n"); exit(1);}
  
  //多层Softmax回归
  if (hs) {
	//syn1存储的是Haffman树中每个非叶节点的向量
	//这里为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);}
    //初始化syn1为0
	for (a = 0; a < vocab_size; a++) for (b = 0; b < layer1_size; b++)
     syn1[a * layer1_size + b] = 0;
  }
  
  //如果要使用负采样,则需要为syn1neg分配内存空间
  //syn1neg是负采样时每个词的辅助向量
  if (negative>0) {
    a = posix_memalign((void **)&syn1neg, 128, (long long)vocab_size * layer1_size * sizeof(real));
    if (syn1neg == NULL) {printf("Memory allocation failed\n"); exit(1);}
	//初始化syn1neg为0
    for (a = 0; a < vocab_size; a++) for (b = 0; b < layer1_size; b++)
     syn1neg[a * layer1_size + b] = 0;
  }
  for (a = 0; a < vocab_size; a++) for (b = 0; b < layer1_size; b++) {
    next_random = next_random * (unsigned long long)25214903917 + 11;
    //初始化词向量syn0,每一维的值为[-0.5, 0.5]/layer1_size范围内的随机数
	syn0[a * layer1_size + b] = (((next_random & 0xFFFF) / (real)65536) - 0.5) / layer1_size;
  }
  //创建Haffman二叉树
  CreateBinaryTree();
}
 
 
//该函数为线程函数,是训练算法代码实现的主要部分
//默认在执行该线程函数前,已经完成词表排序、Haffman树的生成以及每个词的Haffman编码计算
void *TrainModelThread(void *id) {
  long long a, b, d;
  //cw:窗口长度(中心词除外)
  long long cw;
  //word: 在提取句子时用来表示当前词在词表中的索引                                                          
  //last_word: 用于在窗口扫描辅助,记录当前扫描到的上下文单词
  //setence_length: 当前处理的句子长度
  //setence_position: 当前处理的单词在当前句子中的位置
  long long word, last_word, sentence_length = 0, sentence_position = 0;
  //word_count: 当前线程当前时刻已训练的语料的长度
  //last_word_count: 当前线程上一次记录时已训练的语料长度
  long long word_count = 0, last_word_count = 0;
  //sen:当前从文件中读取的待处理句子,存放的是每个词在词表中的索引
  long long sen[MAX_SENTENCE_LENGTH + 1];
  //l1:在skip-gram模型中,在syn0中定位当前词词向量的起始位置
  //l2:在syn1或syn1neg中定位中间节点向量或负采样向量的起始位置
  //target:在负采样中存储当前样本
  //label:在负采样中存储当前样本的标记
  long long l1, l2, c, target, label, local_iter = iter;
  //next_random:用来辅助生成随机数
  unsigned long long next_random = (long long)id;
  real f, g;
  clock_t now;
  //neu1:输入词向量,在CBOW模型中是Context(x)中各个词的向量和,在skip-gram模型中是中心词的词向量
  real *neu1 = (real *)calloc(layer1_size, sizeof(real));
  //neuele:累计误差项
  real *neu1e = (real *)calloc(layer1_size, sizeof(real));
  
  FILE *fi = fopen(train_file, "rb");
  
  //每个进程对应一段文本,根据当前线程的id找到该线程对应文本的初始位置
  //file_size就是之前LearnVocabFromTrainFile和ReadVocab函数中获取的训练文件的大小
  fseek(fi, file_size / (long long)num_threads * (long long)id, SEEK_SET);
  
  //开始主循环
  while (1) {
	//每训练约10000词输出一次训练进度
    if (word_count - last_word_count > 10000) {
      //word_count_actual是所有线程总共当前处理的词数
	  word_count_actual += word_count - last_word_count;
      last_word_count = word_count;
      if ((debug_mode > 1)) {
        now=clock();
		//输出信息包括:
		//当前的学习率alpha;
		//训练总进度(当前训练的总词数/(迭代次数*训练样本总词数)+1);
		//每个线程每秒处理的词数
        printf("%cAlpha: %f  Progress: %.2f%%  Words/thread/sec: %.2fk  ", 13, alpha,
         word_count_actual / (real)(iter * 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)(iter * train_words + 1));
	  //调整的过程中保证学习率不低于starting_alpha * 0.0001
      if (alpha < starting_alpha * 0.0001) alpha = starting_alpha * 0.0001;
    }
	
	//从训练样本中取出一个句子,句子间以回车分割
    if (sentence_length == 0) {
      while (1) {
		//从文件中读入一个词,将该词在词表中的索引赋给word
        word = ReadWordIndex(fi);
        if (feof(fi)) break;
        if (word == -1) continue;
        word_count++;
		//如果读到的时回车,表示句子结束
        if (word == 0) break;
		//对高频词进行随机下采样,丢弃掉一些高频词,能够使低频词向量更加准确,同时加快训练速度
		//可以看作是一种平滑方法
        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;
		  //以1-ran的概率舍弃高频词
          if (ran < (next_random & 0xFFFF) / (real)65536) continue;
        }
        sen[sentence_length] = word;
        sentence_length++;
		//如果句子长度超出最大长度则截断
        if (sentence_length >= MAX_SENTENCE_LENGTH) break;
      }
	  //定位到句子头
      sentence_position = 0;
    }
	
	//如果当前线程处理的词数超过了它应该处理的最大值,那么开始新一轮迭代
	//如果迭代数超过上限,则停止迭代
    if (feof(fi) || (word_count > train_words / num_threads)) {
      word_count_actual += word_count - last_word_count;
      local_iter--;
      if (local_iter == 0) break;
      word_count = 0;
      last_word_count = 0;
      sentence_length = 0;
      fseek(fi, file_size / (long long)num_threads * (long long)id, SEEK_SET);
      continue;
    }
	
	//取出当前单词
    word = sen[sentence_position];
    if (word == -1) continue;
	//初始化输入词向量
    for (c = 0; c < layer1_size; c++) neu1[c] = 0;
	//初始化累计误差项
    for (c = 0; c < layer1_size; c++) neu1e[c] = 0;
	//生成一个[0, window-1]的随机数,用来确定|context(w)|窗口的实际宽度(提高训练速率?)
    next_random = next_random * (unsigned long long)25214903917 + 11;
	b = next_random % window;
	
	
	/********如果使用的是CBOW模型:输入是某单词周围窗口单词的词向量,来预测该中心单词本身*********/
    if (cbow) {
      cw = 0;
	  //一个词的窗口为[setence_position - window + b, sentence_position + window - b]
	  //因此窗口总长度为 2*window - 2*b + 1
      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;
		  //sen数组中存放的是句子中的每个词在词表中的索引
          last_word = sen[c];
          if (last_word == -1) continue;
		  //计算窗口中词向量的和
          for (c = 0; c < layer1_size; c++) neu1[c] += syn0[c + last_word * layer1_size];
		  //统计实际窗口中的有效词数
          cw++;
        }
		
      if (cw) {
		//求平均向量和
        for (c = 0; c < layer1_size; c++) neu1[c] /= cw;
		
		
		//如果采用分层softmax优化
		//根据Haffman树上从根节点到当前词的叶节点的路径,遍历所有经过的中间节点
        if (hs) for (d = 0; d < vocab[word].codelen; d++) {
          f = 0;
		  //l2为当前遍历到的中间节点的向量在syn1中的起始位置
          l2 = vocab[word].point[d] * layer1_size;
          
		  //f为输入向量neu1与中间结点向量的内积
          for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1[c + l2];
		  
		  //检测f有没有超出Sigmoid函数表的范围
          if (f <= -MAX_EXP) continue;
          else if (f >= MAX_EXP) continue;
		  //如果没有超出范围则对f进行Sigmoid变换
          else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];
		  
          //g是梯度和学习率的乘积
		  //学习率越大,则错误分类的惩罚也越大,对中间向量的修正量也越大
		  //注意!word2vec中将Haffman编码为1的节点定义为负类,而将编码为0的节点定义为正类
		  //即一个节点的label = 1 - d
          g = (1 - vocab[word].code[d] - f) * alpha;
          //根据计算得到的修正量g和中间节点的向量更新累计误差
          for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];
          //根据计算得到的修正量g和输入向量更新中间节点的向量值
		  //很好理解,假设vocab[word].code[d]编码为1,即负类,其节点label为1-1=0
		  //sigmoid函数得到的值为(0,1)范围内的数,大于label,很自然的,我们需要把这个中间节点的向量调小
		  //而此时的g = (label - f)*alpha是一个负值,作用在中间节点向量上时,刚好起到调小效果
		  //调小的幅度与sigmoid函数的计算值偏离label的幅度成正比
          for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * neu1[c];
        }
		
		
        //如果采用负采样优化
		//遍历所有正负样本(1个正样本+negative个负样本)
        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;
          }
		  //在负采样优化中,每个词在syn1neg数组中对应一个辅助向量
		  //此时的l2为syn1neg中目标单词向量的起始位置
          l2 = target * layer1_size;
          f = 0;
		  //f为输入向量neu1与辅助向量的内积
          for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1neg[c + l2];
          if (f > MAX_EXP) g = (label - 1) * alpha;
          else if (f < -MAX_EXP) g = (label - 0) * alpha;
		  //g = (label - f)*alpha
          else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
          //用辅助向量和g更新累计误差
		  for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];
		  //用输入向量和g更新辅助向量
          for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * neu1[c];
        }
		
        //根据获得的的累计误差,更新context(w)中每个词的词向量
        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];
        }
      }
    }
	
	
	/********如果使用的是skip-gram模型:输入是中心单词,来预测该单词的上下文*********/
	else {
		
	  //因为需要预测Context(w)中的每个词,因此需要循环2window - 2b + 1次遍历整个窗口
	  //遍历时跳过中心单词
      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为当前待预测的上下文单词
		last_word = sen[c];
        if (last_word == -1) continue;
		//l1为当前单词的词向量在syn0中的起始位置
        l1 = last_word * layer1_size;
		//初始化累计误差
        for (c = 0; c < layer1_size; c++) neu1e[c] = 0;
		
		
        //如果采用分层softmax优化
		//根据Haffman树上从根节点到当前词的叶节点的路径,遍历所有经过的中间节点
        if (hs) for (d = 0; d < vocab[word].codelen; d++) {
          f = 0;
          l2 = vocab[word].point[d] * layer1_size;
          //注意!这里用到了模型对称:p(u|w) = p(w|u),其中w为中心词,u为context(w)中每个词
		  //也就是skip-gram虽然是给中心词预测上下文,真正训练的时候还是用上下文预测中心词
		  //与CBOW不同的是这里的u是单个词的词向量,而不是窗口向量之和
		  //算法流程基本和CBOW的hs一样,这里不再赘述
          for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1[c + l2];
          if (f <= -MAX_EXP) continue;
          else if (f >= MAX_EXP) continue;
          else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];
          g = (1 - vocab[word].code[d] - f) * alpha;
          for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];
          for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * syn0[c + l1];
        }
        
		
		//如果采用负采样优化
		//遍历所有正负样本(1个正样本+negative个负样本)
		//算法流程基本和CBOW的ns一样,也采用的是模型对称
        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];
          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];
        }
        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;
  //创建多线程,线程数为num_threads
  pthread_t *pt = (pthread_t *)malloc(num_threads * sizeof(pthread_t));
  printf("Starting training using file %s\n", train_file);
  //设置初始学习率
  starting_alpha = alpha;
  //如果有词汇表文件,则从中加载生成词表和hash表,否则从训练文件中获得
  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");
  
  //如果classes参数为0,则输出所有词向量到文件中
  if (classes == 0) {
    fprintf(fo, "%lld %lld\n", vocab_size, layer1_size);
    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");
    }
  }
  
  //如果classes参数不为0,则需要对词向量进行K-means聚类,输出词类
  //classes为最后要分成的类的个数
  else {
	//clcn:总类数
	//iter:总迭代次数
	//closeid:用来存储计算过程中离某个词最近的类编号
    int clcn = classes, iter = 10, closeid;
	//centcn:属于每个类的单词数
    int *centcn = (int *)malloc(classes * sizeof(int));
	//cl:每个单词所属的类编号
    int *cl = (int *)calloc(vocab_size, sizeof(int));
	//x:用来存储每次计算得到的词向量和类中心的内积,值越大说明距离越近
	//closev:用来最大的内积,即距离最近
	real closev, x;
	//cent:每个类的中心向量
    real *cent = (real *)calloc(classes * layer1_size, sizeof(real));
	
    //先给所有单词随机指派类
	for (a = 0; a < vocab_size; a++) cl[a] = a % clcn;
	
	//一共迭代iter次
    for (a = 0; a < iter; a++) {
	  //初始化类中心向量数组为0
      for (b = 0; b < clcn * layer1_size; b++) cent[b] = 0;
	  //初始化每个类含有的单词数为1
      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为类平均中心向量的二范数的平方
          closev += cent[layer1_size * b + c] * cent[layer1_size * b + c];
        }
		//对closev开方,此时的closev即为类平均中心向量的二范数
        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;
      }
    }
	
    //经过多次迭代后,逐渐会将词向量向正确的类靠拢
	//输出K-means聚类结果到文件中
    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;
}


---------------------
作者:EnochX
来源:CSDN
原文:https://blog.csdn.net/EnochX/article/details/52852271
版权声明:本文为博主原创文章,转载请附上博文链接!

你可能感兴趣的:(word2vec)