利用Python Numpy从零开始步步为营计算Word2Vec词向量

利用Python Numpy从零开始步步为营计算Word2Vec词向量

@牛伯雨

词向量建模是自然语言处理当中的重要基础步骤。有了用向量表示的词汇,计算机就可以更好地处理文本数据了。
2013年,Mikolov et al. (2013)提出的Word2Vec是一个里程碑式的词向量建模方法。
最近看到一篇Derek Chia的关于徒手计算Word2Vec的博文An implementation guide to Word2Vec using NumPy and Google Sheets,作者利用表格软件表现这种模型训练过程中的矩阵和向量的变化,这对理解这种模型的原理大有裨益。另一篇GeeksforGeeks上题为Implement your own word2vec(skip-gram) model in Python的文章对这一模型也有比较详细的说明。本文是受此启发的产物,表格设计风格和部分代码有参考,旨在动手利用Python里的Numpy一步步地从零构建Word2Vec词向量。

1. CBOW模型和Skip-gram模型

Mikolov等人2013年的论文提出了两种模型,它们分别叫Continuous Bag of Words(CBOW)和Continuous Skip-gram,图示如下:
CBOW和Skip-gram示意图这两种模型都是预测模型,不过CBOW是已知某个词(中心词)周围的上下文,来预测这个词本身最有可能是什么,而Skip-gram则是已知一个词(中心词),来预测这个词周围最有可能是哪些词作为它的上下文。

这两种模型看上去相似,有相互对称的感觉,在具体的操作中也有一些区别。Mikolov 2013年的原文第4.3节对这两种模型进行了比较:

  1. 在模型训练时间方面,Skip-gram所需的时间基本上是CBOW的3倍。这一件比较符合我们的直观感受:已知上下文去预测其中的某一个词,是比只知道一个词去预测上下文要简单很多的。
  2. 在模型表现方面,Skip-gram在语义上的表现要优于CBOW,而CBOW则在句法上稍微更胜一筹。具体说来,如果我们用英文文本举例子,在单词相似性的任务里,Skip-gram更倾向于将"dog"和"cat"这类语义相近的单词视为相似词,而CBOW则可能会认为"dog"和其复数形式"dogs"更接近。

除了前面这两点,它们还有在对低频词的识别度方面的区别:Skip-gram一般比CBOW更敏感。其原因在于,在Skip-gram的训练过程中,虽然高频词比低频词出现的次数更多,但是高频词仍然是一个一个单独地出现的,而在CBOW中,低频词通常被裹在高频词之间作为上下文里不怎么有能见度的那一部分,而高频词经常会在上下文里连着一起出现,总是具有高能见度。

2. 动手编写Python代码

为了方便起见,我们就以句子“利用Python Numpy从零开始步步为营计算Word2Vec词向量”作为我们的(迷你)语料库,来计算词向量。

2.1. 文本预处理

处理中文需要进行分词。我们可以利用Python Jieba进行这一步。

import jieba

class Corpus(object):
    def __init__(self, texts):
        self.texts = texts 
        #texts: ["sentence1", "sentence2", ...]
        
        self.tokenizedCorpus = [] 
        #tokenizedCorpus: [["word1", "word2", ...], ["wordn", ...]]
        
    def makeCorpus(self):
        for sentence in self.texts:
            self.tokenizedCorpus.append([])
            for x in jieba.tokenize(sentence):
                if x[0] == ' ': 
                #我们句子中的"Python"和"Numpy"之间有空格,而jieba认为这个空格也是一个词,但我们不需要空格作为单独的词
                    continue
                self.tokenizedCorpus[-1].append(x[0])
                
    def getTokenizedCorpus(self):
        return self.tokenizedCorpus

具体用我们的迷你语料库作为实验:

s = ["利用Python Numpy从零开始步步为营计算Word2Vec词向量"]
corpusTest = Corpus(s)
corpusTest.makeCorpus()
corpusTest.getTokenizedCorpus()

我们得到的文本corpus是由词组成的列表的列表:

[['利用', 'Python', 'Numpy', '从零开始', '步步为营', '计算', 'Word2Vec', '词', '向量']]

2.2. 滑动窗口(Sliding windows)

上面我们说到,无论是CBOW还是Skip-gram,都会用到“中心词”和它周围的“上下文”的概念。
和文学意义的上下文不同的是,这里的上下文不是指真正的一个包含一定语义的段落整体,而是提前设置好长度的“窗口”。一个“窗口”所包含的词数总是一定的(除非在开头和结尾处会出现长度不够的情况),因此当它的中心词向前移动时,整个“窗口”都会随之移动,我们称这种窗口为“滑动窗口”。我们可以用表格软件来把这一过程变得成具体化:
滑动窗口示意

2.3. One-hot编码向量化

在我们正式用Mikolov的思想计算词向量之前,我们先把每个词用One-hot的方式表达出来。
One-hot是最简单的词向量:向量的共有V个维度,V为文本中所出现的不同的单词数,每个词都对应有一个维度为1,其余维度均为0。比如对于我们的例子迷你文本来说,V=9。这种方法可以简单有效地区分不同的单词,但它的缺点也是显而易见的:所有词之间的距离均相等,不含任何语义信息,而且维度数过大。所以这种One-hot向量只是我们的一个参照物,重要的是根据文本和这些参照物将Word2Vec模型里面的参数不断更新,以得到维度数更少且反映语义信息的Word2Vec向量。
接下来,我们就用这种One-hot编码的方法把刚才我们制造出来的不同“窗口”表示出来:
窗口1

窗口2

窗口3

![窗口4](https://img-blog.csdnimg.cn/img_convert/6b52e32e21d728a6bc5d804eb4c00184.png
等等,以此类推。图中的每一列即表示一个词的One-hot向量。
比如,在窗口3中,中心词是“Numpy”,One-hot向量为 ( 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 ) T (0, 0, 1, 0, 0, 0, 0, 0, 0)^T (0,0,1,0,0,0,0,0,0)T,而它的上下文分别为:
“利用”: ( 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ) T (1, 0, 0, 0, 0, 0, 0, 0, 0)^T (1,0,0,0,0,0,0,0,0)T
“Python”: ( 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ) T (0, 1, 0, 0, 0, 0, 0, 0, 0)^T (0,1,0,0,0,0,0,0,0)T
“从零开始”: ( 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 ) T (0, 0, 0, 1, 0, 0, 0, 0, 0)^T (0,0,0,1,0,0,0,0,0)T
“步步为营”: ( 0 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 ) T (0, 0, 0, 0, 1, 0, 0, 0, 0)^T (0,0,0,0,1,0,0,0,0)T

下面我们编写实现One-hot编码和滑动窗口的代码。

import numpy as np

class TrainingData(object):
    def __init__(self, tokenizedCorpus):
        self.tokenizedCorpus 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是利用LDA主题模型、Word2Vec词向量模型与TextRank相融合的关键词抽取算法Python代码: ```python import jieba import gensim from gensim.models import Word2Vec from gensim import corpora, models import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity # 加载停用词表 stopwords = [line.strip() for line in open('stopwords.txt', encoding='UTF-8').readlines()] # 加载语料库 corpus = [] with open('data.txt', 'r', encoding='UTF-8') as f: for line in f.readlines(): corpus.append(line.strip()) # 分词 texts = [[word for word in jieba.cut(text) if word not in stopwords] for text in corpus] # 训练Word2Vec模型 model = Word2Vec(texts, size=100, window=5, min_count=1, workers=4) # 训练LDA主题模型 dictionary = corpora.Dictionary(texts) corpus_bow = [dictionary.doc2bow(text) for text in texts] lda_model = models.ldamodel.LdaModel(corpus_bow, num_topics=10, id2word=dictionary) # 获取关键词列表 keywords_list = [] for i in range(len(texts)): text = texts[i] bow = dictionary.doc2bow(text) # 获取LDA主题分布 lda_dist = lda_model[bow] lda_dist = sorted(lda_dist, key=lambda x: x[1], reverse=True) # 获取Word2Vec词向量 word_vectors = [] for word in text: try: word_vectors.append(model[word]) except: pass word_vectors = np.array(word_vectors) word_vectors_mean = np.mean(word_vectors, axis=0) # 获取TextRank分数 vectorizer = TfidfVectorizer() tfidf = vectorizer.fit_transform(text) sim_matrix = cosine_similarity(tfidf) scores = np.sum(sim_matrix, axis=1) scores = scores / np.sum(scores) # 综合三种方法获取关键词 keywords = [] for j in range(10): topic = lda_dist[j][0] word_dist = lda_model.show_topic(topic, topn=20) for word, dist in word_dist: if word in text: score = dist * 0.5 + model.similarity(word, '主题') * 0.3 + scores[text.index(word)] * 0.2 keywords.append((word, score)) keywords = sorted(keywords, key=lambda x: x[1], reverse=True)[:5] # 将关键词加入列表 keywords_list.append([keyword[0] for keyword in keywords]) # 输出关键词列表 print(keywords_list) ``` 在上面的代码中,我们首先加载了停用词表和语料库。然后对每一篇文本进行分词,并使用Word2Vec模型和LDA主题模型训练。接着,我们使用LDA主题模型获取主题分布,使用Word2Vec模型获取词向量,使用TextRank算法获取每个词的权重得分。最后,我们将三种方法得出的关键词综合起来,得到每篇文本的关键词列表。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值