一切过往,皆为序章,一切未知,皆为终章。

NLP系列2:Word2Vec理论及实战

Word2Vec

写在前面:最近在学习word2vec,所以记录一下这方面的东西,主要包括skip-gram,cbow以及公式推导及实现

提出

word2vec是Google2013年开源推出的工具包,它简单高效,迅速吸引了大量学者投身其中。对于其中的细节内容却不甚了解。据此,本文也就呼之欲出,就是为了搞定这些内容。

基础知识

sigmoid

sigmoid是神经网络激活函数,大部分人都知道这个函数,其值区间在(0~1)之间,定义如下:

\[\sigma(x) = \frac{1}{1+e^-x} \]

sigmoid的图像就不给出了,网上一堆。我们对\(\sigma(x)\)求导可得:

\[\sigma'(x)=\sigma(x)[1-\sigma(x)] \]

我们对\(log\sigma(x)\)\(log(1-\sigma(x))\)求导,可得到

\[[log\sigma(x)]' = 1-\sigma(x) \]

\[[log(1-\sigma(x))]' = -\sigma(x) \]

logistics

以二分类问题说明,对于样本\(X\),我们可以定义其对应的线性问题\(h_\theta(x)=sum_{i = 0}^{n}\theta_ix_i=\theta^Tx\),利用sigmoid,我们可以得到

\[h_\theta(x) = \sigma(\theta^Tx) = \frac{1}{1+e^{-\theta^Tx}} \]

对二分类问题而言,我们可以取阈值T = 0.5,这里也可以取其他值,但是由于sigmoid纵轴的值域在[0~1],因此我们取0.5,可得判别公式为:

\[y(x) = 1,h_\theta(x) \geqslant 0.5 \]

\[y(x) = 0,h_\theta(x)\lt0.5 \]

\(\theta\)怎么求呢?先定义损失函数:

\[J(\theta) = \frac{1}{m} sum_{i = 1}^{m}cost(x_i,y_i) \]

对损失函数进行优化,得到最佳\(\theta*\)

实际我们对损失函数\(cost(x_i,y_u)\)取其对数似然函数:

\[cost(x_i,y_i) = -log(h_\theta(x_i)),y_i = 1 \]

\[cost(x_i,y_i) = -log(1-h_\theta(x_i)),y_i=0 \]

对上面的分段函数整理得到

\[cost(x_i,y_i) = -y_ilog(h_\theta(x_i)) - (1-y_i)log(1-h_\theta(x_i)) \]

贝叶斯公式

由称为信念网络、有向无环图模型、概率图模型,贝叶斯属于监督学习生成式模型,参数模型,实现简单,再大量数据量下会有较好的效果,但是其前提假设太强,要求特征之间条件独立,再输入特征有关联的情况下并不适用。

贝叶斯公式是用来描述两个条件概率之间的关系,\(P(A),P(B)\)表示A事件,B事件发生的概率,\(P(A|B)\)表示B发生的情况下A发生的概率,\(P(A,B)\)表示A,B同时发生的概率。

\[P(A|B) = \frac{P(A,B)}{P(B)},P(B|A)=\frac{P(A,B)}{P(A)} \]

两公式捣腾捣腾就可以得到==》

\[P(A|B) = P(A)\frac{P(B|A)}{P(B)} \]

上面就是Bayes公式。

Huffman编码

Huffman树

数据结构中曾介绍过Huffman树,是一种非线性数据结构、是一种特殊的二叉树结构。

  • 路径和路径长度

    在树中,从一个节点往下可以达到的孩子或孙子节点之间的通路,称为路径;

    根节点层号为1,第L层节点层号为L,路径长度就为L-1.

  • 节点的权和带权路径长度

    节点到连接的节点边上的值称为权,代表某种含义;

    带权路径长度就是从一个节点到另一个可达节点所有边和对应节点乘积的和。

  • 树的带权路径长度

    所有叶子节点的带权路径的长度的和。

Huffman树的构造

词频越大(权值越大)离根节点越近;

叶子节点为n,则构造Huffman树新增节点个数为n-1;

具体过程可参考https://blog.csdn.net/qq_33990383/article/details/53073825

Huffman编码

lqMdDf.png

本文介绍word2vec中用到的Huffman编码,如图所示

a--->0

b--->101

c--->100

d--->111

e--->1101

f--->1100

背景知识

word2vec是词向量工具,词向量与语言模型有着密切的关系,所以了解一下语言模型。

统计语言模型

由于互联网的发展,每天充斥这大量的文本和图片,要想从大规模语料中提取出有用信息,这就出现了自然语言处理方法,而统计模型可以在海量的数据中提取出有用的信息,从而可以抛弃业务,直接通过统计模型来进行学习提取。统计语言模型是NLP的基础,应用在语音识别、机器翻译、分词、词性标注和信息检索等任务。

假设一个句子S有n个字符构成\(S={w_1,w_2,...,w_n}\),那么这个句子是否合理就可以用概率来衡量:

\[P(S) = P(w_1,w_2,...,w_n)= P(w_1)P(w_2|w_1)P(w_3|w_1^2)...P(w_n|w_1^{n-1})=P(w_1)P(w_2|w_1)P(w_3|w_1,w_2)...P(w_n|w_1,w_2,...,w_{n-1}) \]

\[其中:P(w_n|w_1^{m})=P(w_n|w_1,w_2,...,w_m),以下同 \]

其中条件概率\(P(w_1),P(w_2|w_1),P(w_3|w_1^2)...P(w_n|w_1^{n-1})\)就是语言模型的参数,若给定一个句子S,则可以计算出对应的概率\(P(s)\).

我们计算一下时间复杂度,假设我们一句话有n个字,我们的词库vocab有N个,那么我们就用\(N^n\)种情况,每句话我们需要计算n个概率,那么总的计算量就是\(nN^n\),这就很大了。下面我们就针对计算概率介绍几种方法进行优化。

n-gram模型

上式可以看出,一个词出现的概率和他前面所有的词都相关。如果假设一个词出现的概率之和前面的n个词相关,这就提出了n-gram模型的思想。那么:

\[P(w_k|w_1^{k-1})\thickapprox P(w_k|w_{k-n+1}^{k-1}) \]

根据大数定律可得:

\[P(w_K|w_1^{k-1})\thickapprox \frac{count(w_{k-n+1}^k)}{count(w_{k-n+1}^{k-1})} \]

假设\(n=2\)可得:

\[P(w_K|w_1^{k-1})\thickapprox \frac{count(w_k,w_{k-1})}{count(w_{k-1})} \]

这样就使得参数总数变少,更容易统计。

关于n的选取,理论上n越大越好,但随着n的增大,计算及内存需求就会增加很多;n-gram模型训练word2vec,加入我们的语料较少,n取小一点即可,语料越大可适当增大n,小语料一般建议取3即可。另外随着n的增大,效果增幅会减弱,比如从2增加到3和从3增加到4,后者提升效果就不显著了。

n 模型参数
1 \(2*10^5\)
2 \(4*10^{10}\)
3 \(8*10^{15}\)
4 \(16*10^{20}\)

平滑化

假设\(count(w_{k-n+1}^k)=0\),能否认为\(p(w_k|w_1^{k-1})\)等于0呢?

假设\(count(w_{k-n+1}^k) = count(w_{k-n+1}^{k-1})\),能否认为\(p(w_K|w_1^{k-1}\)就等于1呢?

显然不能这么认为,但是不论我们的语料大与小,都会存在这样的问题,平滑化就是为了解决这个问题。

平滑方式以后看看能能补充

Add-one平滑
Add-delta平滑
留存估计
删除估计
Good-Turing平滑
组合估计
简单线形插值
Jelinek-Mercer平滑
回退模型
Katz平滑

总结:n-gram模型就是通过语料库统计各个词串出现的次数和平滑化处理,计算好的概率存储起来,以后碰到的句子的概率直接取出来乘起来即可。

对机器学习问题我们都要定义一个目标函数,从而求的最优参数,最后利用最优模型进行预测

对于统计模型,利用最大似然,可把目标函数设为:

\[\prod_{w\in C}p(w|Context(w)) \]

其中C表示语料,\(Context(w)\)表示词\(w\)的上下文,即\(w\)的上下文。\(Context(w)\)为空,就取\(p(w|Context(w))=p(w)\)。特别的,对于n-gram模型,\(Context(w_i)=w_{i-n+1}^{i-1}\)

==》最大对数似然,可得

\[\iota = \sum_{w\in C}logP(w|Context(w)) \]

word2vec基于Hierarchical Softmax,注意条件概率\(\iota = \sum_{w\in C}logP(w|Context(w))\)是CBOW的目标函数,\(\iota = \sum_{w\in C}logP(Context(w)|w)\)则是SKip-gram的目标函数,因此对两个方法的分析主要注意再这点上的区别就好。

CBOW模型

网络结构-->Hierarchical Softmax

输入、投影、输出,对样本($(Context(w),w) \()为例,\)Context(w)\(由\)w\(前后个\)c$个词构成

输入层:

\(w\)前后各\(c\)个词,总共\(2c\)个词的词向量\(v(Context(w)_1),...\in R^m\),\(m\)为词向量的长度

投影层:

​ 对\(2c\)个向量求和累加,即\(x = \sum_{i=1}^{2c}v(Context(w)_i)\in R^m\)

输出层:

​ 对应一个二叉树,它是以语料中出现过的词当叶子节点,以各个词再语料中出现的频数当作权值构造Huffman树,其中叶子节点共\(N=|D|\)个,对应词典\(D\)中的词,非叶子节点\(N-1\)个。

1SS9VH.png

与神经网络不同

1、神经网络通过拼接,CBOW通过累加求和-->从输入到投影层的操作

2、神经网络由隐藏层,CBOW没有-->隐藏层

3、神经网络是线性结构,CBOW是树形结构-->输出层

我们知道神经网络的时间消耗主要再隐藏层和输出层的矩阵运算以及输出层的softmax计算,而CBOW没有隐藏层,输出改为Huffman树,大大减少了时间消耗。

网络结构-->Negative Sampling

为了提高训练速度并改善词向量的质量。

在CBOW模型中,已知词\(w\)的上下文\(Context(w)\),需要预测\(w\),因此给定\(Context(w)\),词\(w\)就是一个正样本,其他词就是负样本了,负采样就是让一个训练样本仅仅更新一小部分权重。

** 负采样算法 **

在我们的语料\(C\)中词典\(D\)中的词出现的次数有高有底,对于高频词,被选为负采样的概率就应该比较大,反之,对低频词,其被选中的概率就应该比较小,本质就是一个带权采样问题。

\(D\)中的每个词\(w\)对应一个线段\(l(w)\),长度为\(len(w) = \frac{counter(w)}{\sum_{u\in D}counter(u)}\),其实就是对词频进行归一化,词频越高,值越大,被负采样选中的概率就越大。

总结

高频词效果更好

向量维度较低时效果更好

维度高时近似误差比较大

Word2Vec实现

#_Author_:Monkey
#!/usr/bin/env python
#-*- coding:unicode_escape -*-
# 为了解决read_csv乱码的情况,加上面这句,读取的时候加encoding='gbk'
import time
import numpy as np
import tensorflow.compat.v1 as tf
import random
import pandas as pd
from collections import Counter
cut_text_path = '../data/cut_text.csv'
# load data set
def loader_data(path):
	df = pd.read_csv(path,encoding='gbk',header = None).rename(columns = {0:'text'})
	## cat sentence
	text = '	'.join(df['text'])
	return text
text = loader_data(cut_text_path)
print(len(text.split('	')))
# data pre-processing
def processing(text,freq = 5):
	'''
	:param text: 文本数据
	:param freq: 词频阈值
	:return:
	'''
	text = text.lower().replace('。', ' <PERIOD> ').replace(',', ' <COMMA> ').replace(',', ' <COMMA> ').replace('"', ' <QUOTATION_MARK> ').replace(';', ' <SEMICOLON> ').replace('!', ' <EXCLAMATION_MARK> ').replace('?', ' <QUESTION_MARK> ').replace('(', ' <LEFT_PAREN> ').replace(')', ' <RIGHT_PAREN> ').replace('--', ' <HYPHENS> ').replace('?', ' <QUESTION_MARK> ').replace(':', ' <COLON> ')

	words = text.split('	')
	words_count = Counter(words)
	print(words_count)
	trimmed_words = [word for word in words if words_count[word] > freq]
	return trimmed_words
words = processing(text)
print(words[:20])
print(len(words))
# 构建隐射表
vocab = set(words)
print(len(vocab))
vocab2int = {word:index for index,word in enumerate(vocab)}
int2vocab = {index:word for index,word in enumerate(vocab)}
print('total words:{}'.format(len(words)))
print('unique words:{}'.format(len(set(words))))
# 原始文本进行vocab到int的转换
int_words = [vocab2int[w] for w in words]
int_word_counts = Counter(int_words)
# $P(w_i)=1-\sqrt{\frac{t}{f(w_i)}}$
t = 1e-3 # t值
threshold = 0.7 #剔除阈值概率
#统计单词出现的频次
int_word_counts = Counter(int_words)
total_count = len(int_words)
#计算单词频率
word_freqs = {w:c/total_count for w,c in int_word_counts.items()}
# 计算被删除的概率
prob_drop = {w:1-np.sqrt(t/word_freqs[w]) for w in int_word_counts}
#对单词进行采样
train_words = [w for w in int_words if prob_drop[w] < threshold]
drop_words = [int2vocab[w] for w in int_words if prob_drop[w] > threshold]
# print(set(drop_words)) 
# print(int_words)
# print(train_words)
# print(int_words)

def get_targets(words,idx,window_size = 5):
	'''
	:param words:单词列表
	:param idx: input word的索引号
	:param window_size: 窗口大小
	:return:
	'''
	target_window = np.random.randint(1,window_size+1)
	#这里考虑input word前面单词不够的情况
	start_point = idx - target_window if (idx-target_window)>0 else 0
	end_point = idx + target_window
	#output words(窗口上下文单词)
	targets = set(words[start_point:idx] + words[idx+1:end_point+1])
	return list(targets)

def get_batches(words, batch_size, window_size=5):
	'''
	构造一个获取batch的生成器
	'''
	n_batches = len(words) // batch_size
	# 仅取full batches
	words = words[:n_batches * batch_size]
	for idx in range(0, len(words), batch_size):
		x, y = [], []
		batch = words[idx: idx + batch_size]
		for i in range(len(batch)):
			batch_x = batch[i]
			batch_y = get_targets(batch, i, window_size)
			# 由于一个input word会对应多个output word,因此需要长度统一
			x.extend([batch_x] * len(batch_y))
			y.extend(batch_y)
		yield x, y

# 构建网络
train_graph = tf.Graph()
with train_graph.as_default():
	inputs = tf.placeholder(tf.int32,shape=[None],name = 'inputs')
	labels = tf.placeholder(tf.int32,shape=[None,None],name = 'labels')
vocab_size = len(int2vocab)
embedding_size = 100
print(vocab_size)
with train_graph.as_default():
	# 嵌入层权重矩阵
	embedding = tf.Variable(tf.random.uniform([vocab_size,embedding_size],-1,1) )
	# 实现lookup
	embed = tf.nn.embedding_lookup(embedding,inputs)
	print(embed)
n_sampled = 1000
with train_graph.as_default():
	softmax_w = tf.Variable(tf.truncated_normal([vocab_size,embedding_size],stddev=0.1))
	softmax_b = tf.Variable(tf.zeros(vocab_size))
	# 计算negative sampling下的损失nec
	loss = tf.nn.sampled_softmax_loss(weights=softmax_w,biases=softmax_b,inputs=embed,labels=labels,num_sampled=n_sampled,num_classes=vocab_size)
	cost = tf.reduce_mean(loss)
	optimizer = tf.train.AdamOptimizer().minimize(cost)
#验证
with train_graph.as_default():
# # 随机挑选一些单词
#     valid_size = 16
#     valid_window = 10
#     # 从不同位置各选8个单词
#     valid_examples = np.array(random.sample(range(valid_window), valid_size//2))
#     valid_examples = np.append(valid_examples,
#                                random.sample(range(1000,1000+valid_window), valid_size//2))

	valid_examples = [vocab2int['丰田'],
					  vocab2int['发动机'],
					  vocab2int['助力'],
					  vocab2int['方向机'],
					  vocab2int['雨刮器']]
	valid_size = len(valid_examples)
	# 验证单词集
	valid_dataset = tf.constant(valid_examples,dtype=tf.int32)
	#计算每个词向量的摸并进行单位化
	norm = tf.sqrt(tf.reduce_mean(tf.square(embedding),1,keepdims=True))
	normalized_embedding = embedding / norm
	# 查找验证单词的词向量
	valid_embedding = tf.nn.embedding_lookup(normalized_embedding,valid_dataset)
	# 计算余弦相似度
	similarity = tf.matmul(valid_embedding,tf.transpose(normalized_embedding))
print(len(train_words))

# 训练模型
epochs = 2
batch_size = 2000
window_size = 3
with train_graph.as_default():
	saver = tf.train.Saver()
with tf.Session(graph = train_graph) as sess:
	iteration = 1
	loss = 0
	#添加节点用于初始化所有变量
	sess.run(tf.global_variables_initializer())
	for e in range(1,epochs+1):
		# 获得batch数据
		batches = get_batches(train_words,batch_size,window_size)
		start = time.time()
		for x,y in batches:
			feed = {inputs: x,
					labels: np.array(y)[:, None]}
			train_loss, _ = sess.run([cost, optimizer], feed_dict=feed)
			loss += train_loss
			if iteration % 1000 == 0:
				end = time.time()
				print("Epoch {}/{}".format(e, epochs),
					  "Iteration: {}".format(iteration),
					  "Avg. Training loss: {:.4f}".format(loss / 1000),
					  "{:.4f} sec/batch".format((end - start) / 1000))
				loss = 0
				start = time.time()
			# 计算相似的词
			if iteration % 1000 == 0:
				print('*' * 100)
				# 计算similarity
				sim = similarity.eval()
				for i in range(valid_size):
					valid_word = int2vocab[valid_examples[i]]
					top_k = 8  # 取最相似单词的前8个
					nearest = (-sim[i, :]).argsort()[1:top_k + 1]
					log = 'Nearest to [%s]:' % valid_word
					for k in range(top_k):
						close_word = int2vocab[nearest[k]]
						log = '%s %s,' % (log, close_word)
					print(log)
				print('*' * 100)
			iteration += 1

		save_path = saver.save(sess, "checkpoints/text8.ckpt")
		embed_mat = sess.run(normalized_embedding)

参考:《word2vc中的数学》

posted @ 2020-01-20 12:39  爱吃帮帮糖  阅读(378)  评论(0编辑  收藏  举报