NLP--Word2vec

word2vec是一种词表示的方法,于2013被Google团队发表,他包含两个主要的模型一个是skip-gram一个是CBOW。
word2vec的思想是通过一个词在句子中周围的词来理解这个词。
skip-gram模型的输入是指定的词(中心词),CBOW则是指定词周围的词,这里我主要写skip-gram

原理

给出一个句子比如"I love this cute dog",假设他的中心词汇是love(这个中心词汇就是我们想要了解的词)那么我们此时选择一个窗口w,我们把love左边w个词和右边的w个词称为love的周围词汇。
比如w=2,那么周围词汇就有[“I”,“this”,“cute”],我们发现love左边只有一个词,所有他左边的周围词汇只有一个,那么我们就可以尝试使用love这个词来预测周围的词。
那么我们需要做的就是使这个概率尽可能的大
P ( w 1 , w 2 , w 3 , w 4 ∣ c ) P(w_1,w_2,w_3,w_4|c) P(w1,w2,w3,w4c)
其中c表示中心词, w 1 , w 2 w_1, w_2 w1,w2表示左边的词, w 3 , w 4 w_3, w_4 w3,w4表示右边的词,这个概率的意思就是在中心词出现的情况下,指定周围词出现的概率。
那么这个式子可以写成 ∏ i = 1 4 P ( w i ∣ c ) \prod_{i= 1}^4\limits P(w_i|c) i=14P(wic)
这是一个中心词我们需要最大化的概率,但是可能有多个中心词,所以需要对他们进行求和,我们对上述概率取一个log使其变成求和的方式
C ( θ ) = − ∑ i = 1 n ∑ j = 1 4 l o g ( p ( w i j ∣ c i ) ) C(\theta)=-\sum_{i=1}^n\sum_{j=1}^4\limits log(p(w_{ij}|c_i)) C(θ)=i=1nj=14log(p(wijci))
我们可以对其加上一个负号这样它就变成了一个最小化的问题,我们就可以把它当做代价函数
那么此时一个问题就出现了,那就是如何去计算这个后验概率 p ( w i j ∣ c i ) p(w_{ij}|c_i) p(wijci)
想要回答这个问题,那么首先就要先表示出词,我们使用一个长度为m的向量来表示词,并且使用向量之间内积的方式表示两个词之间的相似度。
那么两个词之间的相似度就是 s ( x i , x j ) = x i ⋅ x j s(x_i, x_j)=x_i\cdot x_j s(xi,xj)=xixj,如果两个词的相似度越高,说明当一个词出现时另一个词出现的概率也越大。
对于 p ( w i j ∣ c i ) p(w_{ij}|c_i) p(wijci) w i , j w_{i,j} wi,j的取值是字典种所有可能的词,我们让所有的词都和中心词 x c i x_{c_i} xci相乘,这样就可以得出所有词和 c i c_i ci的相似度,然后我们做一个softmax,就可以得到这个后验概率了,这样既满足的概率的和为1,同时相似度越大的词其后验概率越大。
那么此代价函数就变成了
C ( θ ) = − ∑ i = 1 n ∑ j = 1 4 l o g ( x w i j ⋅ x w i ∑ k = 1 v x w k ⋅ x w i ) C(\theta)=-\sum_{i=1}^n\sum_{j=1}^4\limits log(\frac{x_{w_{ij}}\cdot x_{w_i}}{\sum\limits_{k = 1}^v x_{w_k}\cdot x_{w_i}}) C(θ)=i=1nj=14log(k=1vxwkxwixwijxwi)
其中,v表示词典的大小。
那么怎么训练呢?
我们可以使用一个浅层神经网络来解决这个问题。
在这里插入图片描述
它大概长这样。
首先,我们需要把词先变成one-hot形式,这个词就是我们的中心词。
那么此时我们的输入向量就是(batch x V)的形状。
第一层我们有m个神经元,所以第一层的权重矩阵就是(V x m)记为 W 1 W_1 W1
第二层也是输出层,我们有v个神经元,我们第二层的权重形状就是(m x V)记为 W 2 W_2 W2
我们注意一下one-hot向量的特点,它的特点是只有对应词的位置才是1,否则都是0.
如果我们使用one-hot向量对 W 1 W_1 W1作乘积,那么我们就可以得到 W 1 W_1 W1的一行。
因为只有为1的地方才会被取到,其它对方都是0。就像下面这个一样
在这里插入图片描述
所以我们使用不同的one-hot向量就可以得到不同的 W 1 W_1 W1 的行,然后我们再让他乘上 W 2 W_2 W2,其实就是 W 1 W_1 W1中取出的那一行和 W 2 W_2 W2中的每一列进行相乘,最终得出结果。
在这整个过程中,我们 W 1 W_1 W1中的那一行当做成中心词的向量,那么着整个过程实际上就是完成了一次计算中心词和所有其他词之间的相似度计算。
对于 W 1 W_1 W1它的每一行都代表着一个对应的中心词的词向量,对于 W 2 W_2 W2它的每一列都代表着一个背景词的词向量。
所以在最后一层的输出就是每个词和中心词之间的相似度。

负采样

也叫negative sample
在上述所说的skip-gram中,我们每次更新其实需要花费很多的时间,首先对于一个中心词,它的背景词一般远小于字典的词,而我们每次梯度下降更新时都会更新非背景词,而我们词典一般非常大这就造成了训练十分耗时,所以我们必须想办法改进。而改进方法其中之一就是使用负采样,同时也还有一种改进方法叫做hierarchical。

负采样的思想就是对于背景词,我们每次都更新,而对于非背景词,我们每次只随机的选择一部分更新,这样就降低了我们更新参数的数目。

但是这样就有了一个问题,有些词出现的次数很少,有的出现的很多,如果使用随机选择那么就就有一定的偏差,有可能多的并没有受到足够多的训练。
所以我门可以使用所有词出现的频率当做概率分布,按照这个概率分布来取词,但是有些词可能就出现了几次那么按照概率,这些词可能永远也不会被选到,所以我们还需要对这个概率动一些手脚。
word2vec作者通过实验得到,把所有词的概率都开 0.75次方得到的效果最好,这样可以让概率小的词变得概率稍微大一点,而大的变得稍微小一点。

代码实现

代码实现参考了这位博主的这篇文章https://blog.csdn.net/Delusional/article/details/114477987,使用的是pytorch
首先数据及太大了,我切出来的一小部分放到了项目目录下

import numpy as np
import torch
from torch.utils.data import DataLoader, Dataset
import re
from collections import Counter

def toLocal(rate=0.2): # 用于切分出一段文本到项目路径下,其中rate表示百分比,这里corpus是一个英文语料库,在上面那个博主的文章里有下载链接
    res, stopwords = [], None
    with open(corpus, encoding='utf-8', mode='r') as rs:
        res = rs.read().split()
        
    with open('./corpus.txt', encoding='utf-8', mode='w') as ws:
        ws.write(' '.join(res[:int(rate * len(res))]))

然后是处理数据

def makePosAndNegDatasets(VocSize):
    content, allwords = None, None # 分别表示句子,和所有词
    with open(r'./corpus.txt', encoding='utf-8', mode='r') as rs:
        content = rs.read().lower()
    allwords = content.split()

    Voc_dict = dict(Counter(allwords).most_common(VocSize - 1)) # 选取出现次数最大的前VocSize - 1个词做词典
    Voc_dict['<UNK>'] = len(allwords) - sum(list(Voc_dict.values())) # 把剩下所有的词都记为UNK
    words = sorted(list(Voc_dict.keys()))
    with open('./voc.txt', encoding='utf-8', mode='w') as ws: # 顺便把词典给保存了
        ws.write('\n'.join(words))

    word2idx = {word: i for i, word in enumerate(words)} # 建立 词-序号映射
    idx2word = {i: word for i, word in enumerate(words)}# 建立 序号-词 映射

    word_count = np.array([Voc_dict[word] for word in words])
    word_freq = (word_count / np.sum(word_count)) ** (3 / 4) # 计算频率,并且开0.75次方

    content = [[word2idx.get(word, word2idx['<UNK>']) for word in sentence.strip().split(' ')] for sentence in content.split('\n')] # 把content中的每个句子都以空格切分
    return word_freq, content, word2idx, idx2word

然后就是模型和数据集

class EmbeddingDatasets(Dataset):
    def __init__(self, content: list, word_freq, w=3, K=12):
        super(EmbeddingDatasets, self).__init__()
        self.content = content
        self.word_freq = torch.tensor(word_freq)
        self.idx2idx = {}
        self.w = w
        self.K = K

        idx = 0 # 把整个语料库的每个词都给标上号
        for i in range(len(self.content)):
            for j in range(len(self.content[i])):
                self.idx2idx.update({idx: (i, j)}) # i表示第i个句子,j表示第i个句子中的第j个词。
                idx += 1
    
    def __getitem__(self, idx):
        sen, idx = self.idx2idx[idx]
        pos = self.getNeighbours(self.content[sen], idx)
        
        neg = torch.multinomial(self.word_freq, len(pos) * self.K, True)
        while len(set(pos) & set(neg)): # 如果出现重复的
            neg = torch.multinomial(self.word_freq, len(pos) * self.K, True)
        
        return torch.tensor(self.content[sen][idx]), torch.tensor(pos), neg

    def getNeighbours(self, sentence, idx): # 给出指定位置的词后依据窗口大小获取周围词
        l, r = max(idx - self.w, 0), min(idx + self.w + 1, len(sentence))
        res = []
        for i in range(l, r):
            if i != idx:
                res.append(sentence[i])
        now_len = len(res)
        res = ((self.w * 2 - now_len) // now_len + 1) * res # 这里由于tensor的行和列是固定的,所以当遇到左右词加起来不等于2 * w时,就在当前已经取得的词中进行重复取词,使其满足为2 * w
        res += res[: (self.w * 2 - now_len) % now_len]
        return res
    
    def __len__(self):
        return len(self.idx2idx)

class ComplexWord2Vec(nn.Module):
    def __init__(self, voc_size, embe_size):
        super(ComplexWord2Vec, self).__init__()
        self.embe1 = nn.Embedding(voc_size, embe_size) # 中心词嵌入层
        self.embe2 = nn.Embedding(voc_size, embe_size) # 背景词嵌入层
        self.voc_size = voc_size
        self.embe_size = embe_size
        self.sig = nn.LogSigmoid()
    
    def forward(self, X, pos, neg):
        X = self.embe1(X).unsqueeze(2)
        pos = self.embe2(pos)
        neg = self.embe2(neg)

        pos_loss = self.sig(torch.bmm(pos, X).squeeze(2)).sum(1) # 求出和背景词的损失
        neg_loss = self.sig(torch.bmm(neg, -X).squeeze(2)).sum(1) # 求出和选出的非背景词的损失
        # 这里由于我们采用了负采样,所以不用softmax,而且我们希望负采样的向量尽可能的小,所以我们取负号。
        loss = (-(pos_loss + neg_loss)).mean() # 由于是最小化,所以取负号
        return loss

    def getEmbedding(self):
        return self.embe1.weight

最后就是训练了

device = torch.device('cuda:0')
voc_size = 10000
embeding_size = 300
batch_size = 90

def train():
    model = ComplexWord2Vec(voc_size, embeding_size).to(device)
    optm = torch.optim.Adam(params=model.parameters(), lr=0.01) 
    word_freq, content, word2idx, idx2word = Processing.makePosAndNegDatasets(voc_size)
    dataset = EmbeddingDatasets(content, word_freq, w=3, K=25)
    print(len(dataset))
    loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size)

    for epoch in range(1):
        tmp = 0
        for i, batch in enumerate(loader):
            X, pos, neg = batch
            X = X.to(device)
            pos = pos.to(device)
            neg = neg.to(device)

            loss = model(X, pos, neg)
            tmp += loss.item()
            optm.zero_grad()
            loss.backward()
            optm.step()
            if i % 500 == 0:
                print(loss.item())

    torch.save(model.getEmbedding().to(torch.device('cpu')), './embeding.pth')

for i in range(1):
    train()

然后我们可以使用scipy.spatial.distance.cosine方法计算余弦相似度。

def load(path):
    res = []
    with open(path, encoding='utf-8', mode='r') as rs:
        for i in rs:
            res.append(i.strip())
    return res

def loadVoc(): # 加载字典
    Vchr = load(r'./voc.txt')
    return Vchr, {s: i for i, s in enumerate(Vchr)}

Vchr, Vdic = loadVoc()
x = torch.load('./embeding.pth').detach().numpy()

def similar_by_word(word, embedding_weights):
    index = Vdic[word]
    embedding = embedding_weights[index]
    cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])
    return [Vchr[i] for i in cos_dis.argsort()[:10]]

print(similar_by_word('one', x))
print(similar_by_word('china', x))
print(similar_by_word('songS', x))
"""
输出:
['one', 'six', 'four', 'two', 'nine', 'eight', 'three', 'seven', 'five', 'zero']
['china', 'asian', 'taiwan', 'southeast', 'central', 'northeastern', 'berlin', 'southwest', 'chad', 'mexico']
['song', 'dracula', 'disco', 'holidays', 'rhythm', 'lyrics', 'folk', 'county', 'drink', 'pete']
"""

训练时间比较长,需要很大的数据才能看到比较有效的结果。

gensim的word2vec

gensim也提供了word2vec类,以此来进行训练,并且训练速度远快于我们自己写的。

from gensim.models import word2vec
x = word2vec.Word2Vec()
"""
def __init__(
    self, sentences=None, corpus_file=None, vector_size=100, alpha=0.025, window=5, min_count=5,
    max_vocab_size=None, sample=1e-3, seed=1, workers=3, min_alpha=0.0001,
    sg=0, hs=0, negative=5, ns_exponent=0.75, cbow_mean=1, hashfxn=hash, epochs=5, null_word=0,
    trim_rule=None, sorted_vocab=1, batch_words=MAX_WORDS_IN_BATCH, compute_loss=False, callbacks=(),
    comment=None, max_final_vocab=None,
):
"""

可以看到它有很多参数,我们说一下比较重要的几个
sentences: 就是句子,可以传列表
corpus_file: 就是语料库路径,可以是txt格式,其中sentences和corpus_file只能使用一个
vector_size: 词向量的大小
alpha: 初始的学习率,训练过程中会不断的降低
window: w的大小
min_count: 一个整数,如果某些词在语料库中的出现次数小于min_count那么这个词就会被过滤掉不会出现在词典内。
seed: 随机种子号
worker: 训练时的线程个数
min_alpha: 降低到的最小学习率
sg: 0表示cbow,1表示skip-gram
hs: 1表示hierarchical, 0表示negative sample
negative: 负采样词的数目
epochs: 训练轮数
batch_words: 就相当于是batch_size
例子:
这里我用了另一个处理过后的中文数据集,大家用别的都行

x = word2vec.Word2Vec(corpus_file=r'./corpus.txt', window=3, min_count=3, sg=1, vector_size=300, negative=40)
for i in x.wv.similar_by_word('妈', topn=5):
    print(i)
"""
输出:
('爸', 0.9473974704742432)
('娘', 0.9101603627204895)
('大伙', 0.8989053964614868)
('阿姨', 0.8972049951553345)
('奶奶', 0.8941314220428467)
"""
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值