Word2vec用CBOW模型的keras代码详解

Word2vec的原理和code在这篇文本处理算法汇总文章里有总结
代码来源: Word2vec用CBOW模型的keras代码

分析目的:
用于理解word2vec模型的输入、输出,及具体用法。

总结:这个模型的原理在于训练2个矩阵,及一个偏置,将一个word输入第一个矩阵emedding_1,这个矩阵将经过one-hot编码的一个及多个中心word,从训练集中出现的总词数(nb_word)维度转换为预先设好的word_size维度的词向量。将第一层的输出与第二个矩阵emedding_2相乘,由于第二个矩阵的第一列为当前word对应的word2id, 其余列为当前word的负采样wordid。 因此,两个矩阵点乘后,矩阵1与矩阵2的第一列相关性最大。

1.首先创建一个简单的文本。

save following words in a './text8_small.txt' file
This is the first document.
This is the second second document.
And the third one.
Is this the first document?

2.导入需要的库文件

import os
import jieba
import random
import numpy as np
from keras.layers import Input, Embedding, Lambda
from keras.models import Model
import keras.backend as K
from collections import Counter

3.定义各种参数 ,word_size = 12是预先设置要转换的词向量维度。window是COBW要取的上下文的词。

word_size = 12 #词向量维度
window = 3 #窗口大小
nb_negative = 2 #随机负采样的样本数
min_count = 1#频数少于min_count的词将会被抛弃,低频词类似于噪声,可以抛弃掉
nb_epoch = 2 #迭代次数

4.用jieba拆分文本,形成句子和word。

def get_corpus(file):
    words = []     #词库,不去重
    corpus = []
    try:
        with open(file, encoding='gbk') as fr:
            for line in fr:
                words+=jieba.lcut(line)     #jieba分词,将句子切分为一个个词,并添加到词库中
                corpus.append(jieba.lcut(line))
    except:
                pass
    return words, corpus

我们能够看到拆分后打印出来的语料库,和word字典

words, corpus = get_corpus('./text8_small.txt')
words = dict(Counter(words))
print('corpus',corpus)
print('words',words)
corpus [['This', ' ', 'is', ' ', 'the', ' ', 'first', ' ', 'document', '.', '\n'], ['This', ' ', 'is', ' ', 'the', ' ', 'second', ' ', 'second', ' ', 'document', '.', '\n'], ['And', ' ', 'the', ' ', 'third', ' ', 'one', '.', '\n'], ['Is', ' ', 'this', ' ', 'the', ' ', 'first', ' ', 'document', '?']]
words {'This': 2, ' ': 16, 'is': 2, 'the': 4, 'first': 2, 'document': 3, '.': 3, '\n': 3, 'second': 2, 'And': 1, 'third': 1, 'one': 1, 'Is': 1, 'this': 1, '?': 1}
  1. 形成word2id ,计算词袋中总词数赋值nb_word。
total = sum(words.values()) #总词频
words = {i:j for i,j in words.items() if j >= min_count} #去掉低频词
id2word = {i+2:j for i,j in enumerate(words)} #id到词语的映射,习惯将0设置为PAD,1设置为UNK
id2word[0] = 'PAD'
id2word[1] = 'UNK'
word2id = {j:i for i,j in id2word.items()} #词语到id的映射
nb_word = len(id2word) #总词数
print('id2word',id2word,'word2id',word2id,'nb_word',nb_word)

可以看出sentence通过jieba分词后将每个单词,包括空格PAD, 标点,无法识别UNK。

id2word {2: 'This', 3: ' ', 4: 'is', 5: 'the', 6: 'first', 7: 'document', 8: '.', 9: '\n', 10: 'second', 11: 'And', 12: 'third', 13: 'one', 14: 'Is', 15: 'this', 16: '?', 0: 'PAD', 1: 'UNK'} 
word2id {'This': 2, ' ': 3, 'is': 4, 'the': 5, 'first': 6, 'document': 7, '.': 8, '\n': 9, 'second': 10, 'And': 11, 'third': 12, 'one': 13, 'Is': 14, 'this': 15, '?': 16, 'PAD': 0, 'UNK': 1} 
nb_word 17
  1. 生成数据,保存每个word作为中心词时,COBW在窗口采样的上下文word, 赋值x为,长度为2*window。保存中心词、负采样词,赋值y, 长度为nb_negative+1。 可以想到上下文的词向量求和平均后与中心词形成的词向量相关性最大。
def data_generator(): #训练数据生成器
    x,y = [],[]
    for sentence in corpus:
        sentence = [0]*window + [word2id[w] for w in sentence if w in word2id] + [0]*window
        #上面这句代码的意思是,因为我们是通过滑窗的方式来获取训练数据的,那么每一句语料的第一个词和最后一个词
        #如何出现在中心位置呢?答案就是给它padding一下,例如“我/喜欢/足球”,两边分别补窗口大小个pad,得到“pad pad 我 喜欢 足球 pad pad”
        #那么第一条训练数据的背景词就是['pad', 'pad','喜欢', '足球'],中心词就是'我'
        for i in range(window, len(sentence)-window):
            x.append(sentence[i-window: i]+sentence[i+1: window+i+1])
            y.append([sentence[i]]+get_negtive_sample(sentence[i], nb_word, nb_negative))
    x,y = np.array(x),np.array(y)
    z = np.zeros((len(x), nb_negative+1))
    z[:,0]=1
    return x,y,z

通过打印可以看出sentence=[0]*window + [word2id[w] for w in sentence if w in word2id] + [0]*window 是将 sentence中每个word进行了word2id的映射,并在句子前后加入window个padding。

sentence ['This', ' ', 'is', ' ', 'the', ' ', 'first', ' ', 'document', '.', '\n']
sentence pad [0, 0, 0, 2, 3, 4, 3, 5, 3, 6, 3, 7, 8, 9, 0, 0, 0]

7.形成训练数据

x,y,z = data_generator() #获取训练数据
print('x,y,z',x,y,z)

现在研究下输入的x,y,z是什么样子的。
当data_generator()循环
i=3的时候,
x = [[0, 0, 0, 3, 4, 3]],
i=4的时候x.append(sentence[i-window: i]+sentence[i+1: window+i+1]),
x =[[0, 0, 0, 3, 4, 3], [0, 0, 2, 4, 3, 5]],
可见当x的长度是corpus中所有的字符43. 转换成array(x).shape=(43, 6),array(y).shape=(43,3)

  1. 创建模型
#苏神对多维向量或者叫张量的操作简直信手拈来,苏神经常使用这个K(keras.backend)对张量进行维度变换、维度提取和张量加减乘除。
#我这个小白看的是晕头转向,得琢磨半天。但是后来我也没有找到合适的方式来替换这个K,只能跟着用。
#第一个输入是周围词
input_words = Input(shape=(window*2,), dtype='int32')
#建立周围词的Embedding层
input_vecs = Embedding(nb_word, word_size, name='word2vec')(input_words)
#CBOW模型,直接将上下文词向量求和
input_vecs_sum = Lambda(lambda x: K.sum(x, axis=1))(input_vecs)
#第二个输入,中心词以及负样本词
samples = Input(shape=(nb_negative+1,), dtype='int32')
#同样的,中心词和负样本词也有一个Emebdding层,其shape为 (?, nb_word, word_size)
softmax_weights = Embedding(nb_word, word_size, name='W')(samples)
softmax_biases = Embedding(nb_word, 1, name='b')(samples)
#将加和得到的词向量与中心词和负样本的词向量分别进行点乘
#注意到使用了K.expand_dims,这是为了将input_vecs_sum的向量推展一维,才能和softmax_weights进行dot
input_vecs_sum_dot_ = Lambda(lambda x: K.batch_dot(x[0], K.expand_dims(x[1],2)))([softmax_weights,input_vecs_sum])
#然后再将input_vecs_sum_dot_与softmax_biases进行相加,相当于 y = wx+b中的b项
#这里又用到了K.reshape,在将向量加和之后,得到的shape是(?, nb_negative+1, 1),需要将其转换为(?, nb_negative+1),才能进行softmax计算nb_negative+1个概率值
add_biases = Lambda(lambda x: K.reshape(x[0]+x[1], shape=(-1, nb_negative+1)))([input_vecs_sum_dot_,softmax_biases])
#这里苏神用了K.softmax,而不是dense(nb_negative+1, activate='softmax')
#这是为什么呢?因为dense是先将上一层的张量先进行全联接,再使用softmax,而向下面这样使用K.softmax,就没有了全联接的过程。
#实验下来,小编尝试使用dense(activate='softmax')训练出来的效果很差。
softmax = Lambda(lambda x: K.softmax(x))(add_biases)
#编译模型
model = Model(inputs=[input_words,samples], outputs=softmax)
#使用categorical_crossentropy多分类交叉熵作损失函数
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.summary()
model.fit([x,y],z, epochs=nb_epoch, batch_size=512)

a. Input
详解:Keras.Layer. Input https://keras.io/zh/layers/core/#input
Input() 用于实例化 Keras 张量。
shape: 一个尺寸元组(整数),不包含批量大小。
例如,shape=(32,) 表明期望的输入是按批次的 32 维向量,这个案例中Input(shape=(window2,), dtype=‘int32’)中的输入array(x)是43 * 6的数组,即2 * windows=6.*
如果 a, b 和 c 都是 Keras 张量,那么以下操作是可行的:
model = Model(input=[a, b], output=c)

b. Embeding https://keras.io/zh/layers/embeddings/

input_vecs = Embedding(nb_word, word_size, name='word2vec')(input_words)

nb_word: input_dim: int > 0。词汇表大小,即,最大整数 index + 1。
word_size: output_dim: int >= 0。词向量的维度。
输入尺寸为 (batch_size, sequence_length) 的 2D 张量。
输出尺寸为 (batch_size, sequence_length, output_dim) 的 3D 张量。

在函数中Embeding 包含了将一维的word进行one-hot转换,转换为nb-word=17维,输出为word_size=12维.
比如输入Input1 (None, 6) 输出word2vec (Embedding)形状为(None, 6, 12), 经过Lambda_1对6个surrounding的词求和后平均,输出形状为(None, 12),

同理Embeding W 输入Input_2 (InputLayer) 形状为 (None, 3) ,输出形状(None, 3, 12).

Lambda_2 是将W与Lambd1相乘,
比如Lambda(lambda x: K.batch_dot(x[0], K.expand_dims(x[1],2)))([softmax_weights,input_vecs_sum])
其中x[0]代表softmax_weights, shape为(None,3,12) , 其中K.expand_dims(x[1],2)代表将input_vecs_sum,从维度(None,12)拓展维度为(None,12,1),然后进行dot相乘, 成为(None,3,1)

K.expand_dims https://keras.io/zh/backend/#expand_dims
keras.backend.expand_dims(x, axis=-1)
x: 张量或变量。
axis: 需要添加新的轴的位置。

Lambda3是加上偏置,Lambda4是softmax, 输入(None,3) 输出(None,3) 。
在这里插入图片描述
9.验证模型

model.save_weights('word2vec.model')
#embedding层的权重永远在模型的第一层
embeddings = model.get_weights()[0]
def most_similar(w):
    v = embeddings[word2id[w]]
    sims = np.dot(embeddings, v)
    sort = sims.argsort()[::-1]
    sort = sort[sort > 0]
    return [(id2word[i],sims[i]) for i in sort[:10]]

可以看出用这个训练好的模型,求得输入得相近词,只用到了embeding Weight的第一层get_weights()[0]。

print(model.get_weights()[0].shape)
print(model.get_weights()[1].shape)
print(model.get_weights()[2].shape)
(17, 12)
(17, 12)
(17, 1)

word2id[w]]中的this 的id为15,找到embeddings的第15个元素,经过第一层矩阵,得到对应的长度为12的向量。

print(model.get_weights()[0][15])
[ 0.0499722   0.00947531 -0.04782331 -0.03567474 -0.00890872 -0.02059875
  0.04561229 -0.02593856 -0.01512181  0.01678449 -0.03727285  0.00724424]
word2id {'This': 2, ' ': 3, 'is': 4, 'the': 5, 'first': 6, 'document': 7, '.': 8, '\n': 9, 'second': 10, 'And': 11, 'third': 12, 'one': 13, 'Is': 14, 'this': 15, '?': 16, 'PAD': 0, 'UNK': 1} 

通过让 embeding 矩阵与目标词向量进行点乘得到相似值,sims = np.dot(embeddings, v),然后使用 numpy 的内置函数 argsort 进行排序,从而得到索引,这样就可以得到与词相似度有高到低的一个排序。

import pandas as pd
pd.Series(most_similar(u'this'))
0      (this, 0.011237454)
1         (., 0.005504311)
2      (This, 0.004702386)
3      (UNK, 0.0033156318)
4      (And, 0.0031191725)
5       (Is, 0.0026839643)
6    (first, 0.0013143882)
7       (\n, 0.0011139032)
8       (?, 0.00050036504)
9     (one, -0.0008003423)
dtype: object

对比gensim用法
有相同的功能most_similar 方法

Gensim 官方手册
https://radimrehurek.com/gensim/models/word2vec.html

from gensim.models import word2vec
import logging
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s',level=logging.INFO)
sentences=word2vec.Text8Corpus("./text8")#加载分词语料
model=word2vec.Word2Vec(sentences,size=200)#训练skip-gram模型,默认window=5
print("输出模型",model)
y1=model.similarity("china","japan")
y2=model.most_similar("market",topn=20)#20个最相关的
for word in y2:
    print(word[0],word[1])
print("*********\n")
y1 := 0.73442537
y2 :
markets 0.7433197498321533
commodity 0.6431580781936646
economy 0.6383275985717773
price 0.6141645908355713
...
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值