机器学习 23 、BM25 Word2Vec

你挡我一时,挡不了我一世

发布日期: 2019-06-23 14:57:53 浏览量: 85
评分:
star star star star star star star star star star
*转载请注明来自write-bug.com

前文链接:https://write-bug.com/article/2689.html

word embedding

什么是word embedding? 简单的来说就是将一个特征转换为一个向量。在推荐系统当中,我们经常会遇到离散特征,如userid、itemid。对于离散特征,我们一般的做法是将其转换为one-hot,但对于itemid这种离散特征,转换成one-hot之后维度非常高,但里面只有一个是1,其余都为0。这种情况下,我们的通常做法就是将其转换为embedding。

word embedding为什么翻译成词嵌入模型?一个向量可以想象成多维空间,那么在多维空间里面找到一个点来代表这个词,相当于把这个词嵌入到了多维空间里,所以可以称为词嵌入模型。

one-hot编码形式

输入:

  1. apple on a apple tree Bag of Words ["apple", "on", "a", "tree"]

– Word Embedding的输出就是每个word的向量表示

最简单直接的是one-hot编码形式:那么每个word都对应了一种数值表示

例如,apple对应的vector就是[1, 0, 0, 0],a对应的vector就是[0, 0, 1, 0]

两类表达方式:

基于频率:(本质:one-hot)

Count vector:需要穷举所有单词,形成非常庞大、稀疏的矩阵,穷举时,计算每词的词频TF

每个文档用词向量的组合来表示,每个词的权重用其出现的次数来表示

假设语料库内容如下:

  1. D1: He is a boy.
  2. D2: She is a girl, good girl.

那么可以构建如下2 × 7维的矩阵

tfidf vector:同时考虑了TF与IDF

IDF=log(N/n)。 – N代表语料库中文档的总数 – n代表某个word在几个文档中出现过;

Co-Occurrence vector:只考虑滑动窗口内单词的共现,等于考虑了上下文之间的关系

前面两类方法,每个字都是独立的,缺少了语义的学习

例如: He is not lazy. He is intelligent. He is smart.

如果Context Window大小为2,那么可以得到如下的共现矩阵:

he和is计算方法(窗口共现):

基于模型

CBOW

Skip-Gram

技术升级

TF-IDF-》BM25

Co-Occurrence-》WordtoVector

BM25:

定位:搜索引擎相关性评分

通常我们在搜索信息时,搜索的句子为query,关联出的排序列表为item,即:

query -》item

那么可分词query-》token1 token2 token3

同时可把item -》doc-》token token token

词在文章中的评分:token1 -》doc :score1(tfidf)

所以相关性评分query-》item:score1+score2+…

公式可表示为:

  1. Sqd)=∑Wi*Rqid

Wi用idf表示:

N为索引中的全部文档数

n(qi)为包含了qi的文档数

相关性评分R:

  • k1,k2,b为调节因子,一般k1=2,b=0.75

  • b是调整文档长度对相关性影响的大小 • b越大,文档长度影响R越大,反之越小

  • fi为qi在d中的出现频率 =TF

  • qfi为qi在Query中的出现频率 =query中的TF

在之前的tfidf中TF越大最后的分数也越大,而通常情况下,文章越长,TF大的可能性越大,没有考虑到文章长度的问题,这里BM25考虑到了这个问题,如果文章过长就要给定一定的打压,那么如何界定文章长度呢?除以avgdl,即所有文章平均长度

  • dl为文档d的长度

  • avgdl为所有文档的平均长度

公式简化:

绝大部分情况下,qi在Query中只会出现一次,即qfi=1

BM25实践:

1.gensim word2vec

语料库-》每个词的50维向量即word embedding

  1. from gensim.models import word2vec
  2. model=word2vec.Word2Vec(sentences,size=50,window=5,min_count=1,workers=6)
  3. model.wv.save_word2vec_format(file_voc, binary=False)

2.coumpute_idf

每个词的idf

  1. file_corpus='../data/file_corpus.txt'
  2. file_voc='../data/voc.txt'
  3. file_idf='../data/idf.txt'
  4. class ComIdf(object):
  5. def __init__(self,file_corpus,file_voc,file_idf):
  6. self.file_corpus=file_corpus
  7. self.file_voc=file_voc
  8. self.file_idf=file_idf
  9. self.voc=load_voc(self.file_voc)
  10. self.corpus_data=self.load_corpus()
  11. self.N=len(self.corpus_data)
  12. def load_corpus(self):
  13. input_data = codecs.open(self.file_corpus, 'r', encoding='utf-8')
  14. return input_data.readlines()
  15. def com_idf(self,word):
  16. n = 0
  17. for _,line in enumerate(self.corpus_data):
  18. n+=line.count(word)
  19. idf=math.log(1.0*self.N/n+1)
  20. return {word:idf}
  21. def parts(self):
  22. words=set(self.voc.keys())
  23. multiprocessing.freeze_support()
  24. cores=multiprocessing.cpu_count()
  25. pool=multiprocessing.Pool(processes=cores-2)
  26. reuslt=pool.map(self.com_idf,words)
  27. idf_dict=dict()
  28. for r in reuslt:
  29. k=list(r.keys())[0]
  30. v=list(r.values())[0]
  31. idf_dict[k]=idf_dict.get(k,0)+v
  32. with codecs.open(self.file_idf,'w',encoding='utf-8') as f:
  33. f.write(json.dumps(idf_dict,ensure_ascii=False,indent=2,sort_keys=False))
  34. if __name__ == '__main__':
  35. t1 = time.time()
  36. IDF=ComIdf(file_corpus,file_voc,file_idf)
  37. IDF.parts()

3.get_sentence

分割句子,取出最常见句子1w个

  1. def get_sentence():
  2. file_corpus=codecs.open('../data/file_corpus.txt','r',encoding='utf-8')
  3. file_sentence=codecs.open('../data/file_sentence.txt','w',encoding='utf-8')
  4. st=dict()
  5. for _,line in enumerate(file_corpus):
  6. line=line.strip()
  7. blocks=re_han.split(line)
  8. for blk in blocks:
  9. if re_han.match(blk) and len(blk)>10:
  10. st[blk]=st.get(blk,0)+1
  11. st=sorted(st.items(),key=lambda x:x[1],reverse=True)
  12. for s in st[:10000]:
  13. file_sentence.write(s[0]+'\n')
  14. file_corpus.close()
  15. file_sentence.close()
  16. get_sentence()

4.test

各种算法计算打分:cos\idf\bm25\jaccard

  1. file_voc='./data/voc.txt'
  2. file_idf='./data/idf.txt'
  3. file_userdict='./data/medfw.txt'
  4. class SSIM(object):
  5. def __init__(self):
  6. t1 = time.time()
  7. self.voc=load_voc(file_voc)
  8. print("Loading word2vec vector cost %.3f seconds...\n" % (time.time() - t1))
  9. t1 = time.time()
  10. self.idf=load_idf(file_idf)
  11. print("Loading idf data cost %.3f seconds...\n" % (time.time() - t1))
  12. jieba.load_userdict(file_userdict)
  13. def M_cosine(self,s1,s2):
  14. s1_list=jieba.lcut(s1)
  15. s2_list=jieba.lcut(s2)
  16. v1=np.array([self.voc[s] for s in s1_list if s in self.voc])
  17. v2=np.array([self.voc[s] for s in s2_list if s in self.voc])
  18. v1=v1.sum(axis=0)
  19. v2=v2.sum(axis=0)
  20. sim=1-spatial.distance.cosine(v1,v2)
  21. return sim
  22. def M_idf(self,s1, s2):
  23. v1, v2 = [], []
  24. s1_list = jieba.lcut(s1)
  25. s2_list = jieba.lcut(s2)
  26. for s in s1_list:
  27. idf_v = self.idf.get(s, 1)
  28. if s in self.voc:
  29. v1.append(1.0 * idf_v * self.voc[s])
  30. for s in s2_list:
  31. idf_v = self.idf.get(s, 1)
  32. if s in self.voc:
  33. v2.append(1.0 * idf_v * self.voc[s])
  34. v1 = np.array(v1).sum(axis=0)
  35. v2 = np.array(v2).sum(axis=0)
  36. sim = 1 - spatial.distance.cosine(v1, v2)
  37. return sim
  38. def M_bm25(self,s1, s2, s_avg=10, k1=2.0, b=0.75):
  39. bm25 = 0
  40. s1_list = jieba.lcut(s1)
  41. for w in s1_list:
  42. idf_s = self.idf.get(w, 1)
  43. bm25_ra = s2.count(w) * (k1 + 1)
  44. bm25_rb = s2.count(w) + k1 * (1 - b + b * len(s2) / s_avg)
  45. bm25 += idf_s * (bm25_ra / bm25_rb)
  46. return bm25
  47. def M_jaccard(self,s1, s2):
  48. s1 = set(s1)
  49. s2 = set(s2)
  50. ret1 = s1.intersection(s2)
  51. ret2 = s1.union(s2)
  52. jaccard = 1.0 * len(ret1)/ len(ret2)
  53. return jaccard
  54. def ssim(self,s1,s2,model='cosine'):
  55. if model=='idf':
  56. f_ssim=self.M_idf
  57. elif model=='bm25':
  58. f_ssim=self.M_bm25
  59. elif model=='jaccard':
  60. f_ssim=self.M_jaccard
  61. else:
  62. f_ssim = self.M_cosine
  63. sim=f_ssim(s1,s2)
  64. return sim
  65. sm=SSIM()
  66. ssim=sm.ssim
  1. def test():
  2. test_data=[u'临床表现及实验室检查即可做出诊断',
  3. u'面条汤等容易消化吸收的食物为佳',
  4. u'每天应该摄入足够的维生素A',
  5. u'视患者情况逐渐恢复日常活动',
  6. u'术前1天开始预防性运用广谱抗生素']
  7. model_list=['cosine','idf','bm25','jaccard']
  8. file_sentence=codecs.open('./data/file_sentence.txt','r',encoding='utf-8')
  9. train_data=file_sentence.readlines()
  10. for model in model_list:
  11. t1 = time.time()
  12. dataset=dict()
  13. result=dict()
  14. for s1 in test_data:
  15. dataset[s1]=dict()
  16. for s2 in train_data:
  17. s2=s2.strip()
  18. if s1!=s2:
  19. sim=similarity.ssim(s1,s2,model=model)
  20. dataset[s1][s2]=dataset[s1].get(s2,0)+sim
  21. for r in dataset:
  22. top=sorted(dataset[r].items(),key=lambda x:x[1],reverse=True)
  23. result[r]=top[0]
  24. with codecs.open('./data/test_result.txt','a') as f:
  25. f.write('--------------The result of %s method------------------\n '%model)
  26. f.write('\tThe computing cost %.3f seconds\n'% (time.time() - t1))
  27. f.write(json.dumps(result, ensure_ascii=True, indent=2, sort_keys=False))
  28. f.write('\n\n')
  29. file_sentence.close()
  30. test()

word2vec

继承了之前的窗口滑动思想,得到更好的语义理解

涉及的技术点:

– Hierarchical softmax分层softmax

– Negative sampling负采样

两种方式:

  • CBOW:根据前后词,预测中间词

  • Skip-Gram :根据中间词预测前后词,工业用的更多

CBOW

一句话包含了很多单词,时间窗口选择到了一个中间词,w3,假设左边右边各学习两个,中间词假设不知道,那么根据w1245猜测中间这个w3词,故意装作w3不知道做预测,那么w3就是中心词label,w1245就是周围词训练数据,随着时间窗口滑动,同理w4又继续成为了中间词,那么由此类推形成了很多训练样本

  • Input layer:由one-hot编码的输入上下文{x1,…,xC}组成, 其中窗口大小为C,词汇表大小为V

  • Hidden layer:隐藏层是N维的向量

  • Output layer:被one-hot编码的输出单词y

被one-hot编码的输入向量通过一个V×N维的权重矩阵W连接到隐藏层; 隐藏层通过一个N×V的权重矩阵W′连接到输出层

不管输入多少词,相加并都映射为中间得隐层向量上,再做一个softmax输出

Skip-Gram:CBOW的网络翻转

样本怎么得到的?

input word和output word都是one-hot编码的向量。最终模型的输出是一个概率分布

softmax分类需要所有单词:这里是50002个单词,需要遍历一遍,那么每次计算都需要一定时间,不管是多次还是一次时间成本都很高,所以引出了分层softmax:

Hierarchical softmax哈夫曼树

  • 是二叉树的一种特殊形式,又称为最优二叉树,其主要作用在于数据压缩和编码长度的优化

  • 定义:给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度(WPL)达到最小,称这样的二叉 树为最优二叉树,也称为霍夫曼树(Huffman Tree)

  • WPL:树的带权路径长度,所有叶子结点的带权路径长度之和

让wpl最小,压缩数据信息

主要用于编码:从根节点出发,向左为0,向右为1

等长编码和哈夫曼编码

生成:

每个节点按权重大小从小到大排序

两个最小的为叶子节点相加成为父节点并和其他最小的成两个叶子节点,最终形成哈夫曼树

之前做softmax,50002个单词需要从头到尾计算一遍,把词表做成树,每个叶子节点都是一个单词,树的每个结点都是一个sigmoid,来了一个单词做一个lr二分类向左还是向右分类直至最后叶子节点为所要的词

Negative sampling负采样

思想:每次让一个训练样本仅更新一小部分权重

需要离错误的样本更远

不对所有输出神经元对应的权值进行更新,只是随机选取几个“negative”词,更新他们对应的权重。当然, 我们也会更新”positive”的对应权值

随机取的样本只要不共现就是负样本,概率大的会多选,即出现多的单词自然被取出的概率越大

实际论文实现:

  • CBOW:input是context(周围词)而output是中心词,训练过程中其实是在从output的loss学习周围词的信息 也就是embedding,但是在中间层是average的,一共预测V(vocab size)次就够了

一次softmax

  • Skipgram:用中心词预测周围词,预测的时候是一对word pair,等于对每一个中心词都有K个词作为output, 对于一个词的预测有K次,所以能够更有效的从context中学习信息,但是总共预测K*V词

多次softmax

  • Skip-Gram的训练时间更长,更准确

  • Cbow训练速度快,Skip-Gram更好处理生僻字

Word2Vec实践:

  1. import sys
  2. import torch
  3. import torch.nn as nn
  4. import torch.nn.functional as F
  5. import torch.utils.data as tud
  6. from torch.nn.parameter import Parameter
  7. from collections import Counter
  8. import numpy as np
  9. import random
  10. import math
  11. import pandas as pd
  12. import scipy
  13. import sklearn
  14. from sklearn.metrics.pairwise import cosine_similarity
  1. # 超参数
  2. K = 100 # number of negative samples
  3. C = 3 # nearby words threshold
  4. NUM_EPOCHS = 2 # The number of epochs of training
  5. MAX_VOCAB_SIZE = 30000 # the vocabulary size
  6. BATCH_SIZE = 128 # the batch size
  7. LEARNING_RATE = 0.2 # the initial learning rate
  8. EMBEDDING_SIZE = 100
  1. # 分词
  2. def word_tokenize(text):
  3. return text.split()
  4. with open("./data/text8", "r") as fin:
  5. text = fin.read()
  6. text = [w for w in word_tokenize(text.lower())]
  7. print(text[1])
  8. vocab = dict(Counter(text).most_common(MAX_VOCAB_SIZE - 1))#计数器,取前30000个高频元素
  9. vocab["<unk>"] = len(text) - np.sum(list(vocab.values()))
  10. print (np.sum(list(vocab.values())))
  11. print (len(text))
  12. print (vocab["<unk>"])
  13. print (len(vocab))
  1. #output:
  2. originated
  3. 17005207
  4. 17005207
  5. 690103
  6. 30000
  1. idx_to_word = [word for word in vocab.keys()]
  2. word_to_idx = {word: i for i, word in enumerate(idx_to_word)}
  3. word_counts = np.array([count for count in vocab.values()], dtype=np.float32)
  4. word_freqs = word_counts / np.sum(word_counts)
  5. word_freqs = word_freqs ** (3. / 4.)
  6. word_freqs = word_freqs / np.sum(word_freqs) # 用来做 negative sampling
  7. VOCAB_SIZE = len(idx_to_word)
  8. print(VOCAB_SIZE)
  9. #text 每个单词
  10. #word_to_idx 单词id化:dict
  11. #idx_to_word 每个单词
  12. #word_freqs 负采样概率
  13. #word_counts 单词计数列表
  1. #数据样本列举:
  2. class WordEmbeddingDataset(tud.Dataset):
  3. def __init__(self, text, word_to_idx, idx_to_word, word_freqs, word_counts):
  4. ''' text: a list of words, all text from the training dataset
  5. word_to_idx: the dictionary from word to idx
  6. idx_to_word: idx to word mapping
  7. word_freq: the frequency of each word
  8. word_counts: the word counts
  9. '''
  10. super(WordEmbeddingDataset, self).__init__()
  11. self.text_encoded = [word_to_idx.get(t, VOCAB_SIZE - 1) for t in text]
  12. self.text_encoded = torch.Tensor(self.text_encoded).long()
  13. self.word_to_idx = word_to_idx
  14. self.idx_to_word = idx_to_word
  15. self.word_freqs = torch.Tensor(word_freqs)
  16. self.word_counts = torch.Tensor(word_counts)
  17. def __len__(self):
  18. ''' 返回整个数据集(所有单词)的长度
  19. '''
  20. return len(self.text_encoded)
  21. def __getitem__(self, idx):
  22. ''' 这个function返回以下数据用于训练
  23. - 中心词
  24. - 这个单词附近的(positive)单词
  25. - 随机采样的K个单词作为negative sample
  26. '''
  27. center_word = self.text_encoded[idx]
  28. pos_indices = list(range(idx - C, idx)) + list(range(idx + 1, idx + C + 1)#窗口左33
  29. pos_indices = [i % len(self.text_encoded) for i in pos_indices]#去除类似第一次左边无词这种情况
  30. pos_words = self.text_encoded[pos_indices]
  31. neg_words = torch.multinomial(self.word_freqs, K * pos_words.shape[0], True)
  32. return center_word, pos_words, neg_words
  33. dataset = WordEmbeddingDataset(text, word_to_idx, idx_to_word, word_freqs, word_counts)
  34. dataloader = tud.DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4)

  1. class EmbeddingModel(nn.Module):
  2. def __init__(self, vocab_size, embed_size):
  3. ''' 初始化输出和输出embedding
  4. '''
  5. super(EmbeddingModel, self).__init__()
  6. self.vocab_size = vocab_size
  7. self.embed_size = embed_size
  8. initrange = 0.5 / self.embed_size
  9. self.out_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False)
  10. self.out_embed.weight.data.uniform_(-initrange, initrange)
  11. self.in_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False)
  12. self.in_embed.weight.data.uniform_(-initrange, initrange)
  13. def forward(self, input_labels, pos_labels, neg_labels):
  14. '''
  15. input_labels: 中心词, [batch_size]
  16. pos_labels: 中心词周围 context window 出现过的单词 [batch_size * (window_size * 2)]
  17. neg_labelss: 中心词周围没有出现过的单词,从 negative sampling 得到 [batch_size, (window_size * 2 * K)]
  18. return: loss
  19. '''
  20. input_embedding = self.in_embed(input_labels) # B * embed_size
  21. pos_embedding = self.out_embed(pos_labels) # B * (2*C) * embed_size
  22. neg_embedding = self.out_embed(neg_labels) # B * (2*C * K) * embed_size
  23. print("input_embedding size:", input_embedding.size())
  24. print("pos_embedding size:", pos_embedding.size())
  25. print("neg_embedding size:", neg_embedding.size())
  26. log_pos = torch.bmm(pos_embedding, input_embedding.unsqueeze(2)).squeeze() # B * (2*C)
  27. log_neg = torch.bmm(neg_embedding, -input_embedding.unsqueeze(2)).squeeze() # B * (2*C*K)先拓展再压缩
  28. print("log_pos size:", log_pos.size())
  29. print("log_neg size:", log_neg.size())
  30. log_pos = F.logsigmoid(log_pos).sum(1)#1为横向压缩,2为竖向压缩,可以试试玩
  31. log_neg = F.logsigmoid(log_neg).sum(1)
  32. loss = log_pos + log_neg
  33. print("log_pos size:", log_pos.size())
  34. print("log_neg size:", log_neg.size())
  35. print("loss size:", loss.size())
  36. return -loss
  37. def input_embeddings(self):
  38. return self.in_embed.weight.data.cpu().numpy()
  39. model = EmbeddingModel(VOCAB_SIZE, EMBEDDING_SIZE)
  40. if USE_CUDA:
  41. model = model.cuda()
  1. if __name__ == '__main__':
  2. optimizer = torch.optim.SGD(model.parameters(), lr=LEARNING_RATE)
  3. for e in range(NUM_EPOCHS):
  4. for i, (input_labels, pos_labels, neg_labels) in enumerate(dataloader):
  5. print(input_labels.size())
  6. print(pos_labels.size())
  7. print(neg_labels.size())
  8. input_labels = input_labels.long()
  9. pos_labels = pos_labels.long()
  10. neg_labels = neg_labels.long()
  11. if USE_CUDA:
  12. input_labels = input_labels.cuda()
  13. pos_labels = pos_labels.cuda()
  14. neg_labels = neg_labels.cuda()
  15. optimizer.zero_grad()
  16. loss = model(input_labels, pos_labels, neg_labels).mean()
  17. loss.backward()
  18. optimizer.step()
  19. if i % 100 == 0:
  20. with open(LOG_FILE, "a") as fout:
  21. fout.write("epoch: {}, iter: {}, loss: {}\n".format(e, i, loss.item()))
  22. print("epoch: {}, iter: {}, loss: {}".format(e, i, loss.item()))
  23. embedding_weights = model.input_embeddings()
  24. torch.save(model.state_dict(), "embedding-{}.th".format(EMBEDDING_SIZE))

使用场景:

1.item title检索 +ANN+word avg,word average: – 对所有输入词向量求和并取平均 – 例如:输入三个4维词向量:(1,2,3,4),(9,6,11,8),(5,10,7,12) – 那么我们word2vec映射后的词向量就是(5,6,7,8)

2.模型初始化:处理特征等

3.近义词(语义理解):可通过计算词向量的加减,与相似度可进行语义理解

实践例:

  1. def evaluate(filename, embedding_weights):
  2. if filename.endswith(".csv"):
  3. data = pd.read_csv(filename, sep=",")
  4. else:
  5. data = pd.read_csv(filename, sep="\t")
  6. human_similarity = []
  7. model_similarity = []
  8. for i in data.iloc[:, 0:2].index:
  9. word1, word2 = data.iloc[i, 0], data.iloc[i, 1]
  10. if word1 not in word_to_idx or word2 not in word_to_idx:
  11. continue
  12. else:
  13. word1_idx, word2_idx = word_to_idx[word1], word_to_idx[word2]
  14. word1_embed, word2_embed = embedding_weights[[word1_idx]], embedding_weights[[word2_idx]]
  15. model_similarity.append(float(sklearn.metrics.pairwise.cosine_similarity(word1_embed, word2_embed)))
  16. human_similarity.append(float(data.iloc[i, 2]))
  17. return scipy.stats.spearmanr(human_similarity, model_similarity)# , model_similarity
  18. def find_nearest(word):
  19. index = word_to_idx[word]
  20. embedding = embedding_weights[index]
  21. cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])#1-cosine
  22. return [idx_to_word[i] for i in cos_dis.argsort()[:10]]
  23. model.load_state_dict(torch.load("model/embedding-{}.th".format(EMBEDDING_SIZE), map_location='cpu'))
  24. embedding_weights = model.input_embeddings()
  25. # 找相似的单词
  26. for word in ["good", "computer", "china", "mobile", "study"]:
  27. print(word, find_nearest(word))
  28. # 单词之间的关系,寻找nearest neighbors
  29. man_idx = word_to_idx["man"]
  30. king_idx = word_to_idx["king"]
  31. woman_idx = word_to_idx["woman"]
  32. embedding = embedding_weights[woman_idx] - embedding_weights[man_idx] + embedding_weights[king_idx]
  33. cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])
  34. for i in cos_dis.argsort()[:20]:
  35. print(idx_to_word[i])
上传的附件
最近文章
eject