基于计数方法的改进
本文记录的是鱼书第3章:如何对原有的计数方法进行改进。
基于统计方法函数
下面介绍的是传统的基于统计的方法。
预处理
代码语言:javascript复制# 常用函数
def preprocess(text):
text = text.lower() # 转成小写
text = text.replace('.', ' .') # 增加空格
words = text.split(' ') # 切割
# 单词和单词ID的对应关系
word_to_id = {}
id_to_word = {}
for word in words:
if word not in word_to_id.keys(): # 原文 if word not in word_to_id:
new_id = len(word_to_id)
word_to_id[word] = new_id
id_to_word[new_id] = word
# 单词列表-----> 单词ID列表
corpus = np.array([word_to_id[w] for w in words])
return corpus, word_to_id, id_to_word
共现矩阵
代码语言:javascript复制def create_to_matrix(corpus, vocab_size,window_size=1):
"""
corpus:单词ID列表
vocab_size:词汇个数
window_size:窗口大小
"""
corpus_size = len(corpus)
# 全0矩阵初始化
co_matrix = np.zeros((vocab_size, vocab_size), dtype=np.int32)
for idx, word_id in enumerate(corpus): # 遍历语料库中的每个单词
for i in range(1, window_size 1): # 遍历窗口中的数据;是否超出语料库的左右端
left_idx = idx - i # 左右索引值
right_idx = idx i
if left_idx >= 0: # 判断左索引大于0的时候
left_word_id = corpus[left_idx] # 取出索引对应的word_id
co_matrix[word_id, left_word_id] = 1 # 对应的位置赋值1
if right_idx < corpus_size: # 右索引小于整体的语料库长度
right_word_id = corpus[right_idx]
co_matrix[word_id, right_word_id] = 1
return co_matrix
相似度计算
代码语言:javascript复制def cos_similarity(x, y, eps=1e-8):
"""
优化版本
"""
nx = x / (np.sqrt(np.sum(x ** 2)) eps) # x的正规化
ny = y / (np.sqrt(np.sum(y ** 2)) eps) # y的正规化
return np.dot(nx,ny)
相似单词的降序
代码语言:javascript复制def most_similar(query, word_to_id, id_to_word,word_matrix, top=5):
"""
query:查询单词
word_to_id:单词到单词ID
id_to_word:单词ID到单词
word_matrix:汇总了单词向量的矩阵,假定保存了与各行对应的单词向量(共现矩阵)
top:显示到前几位
"""
if query not in word_to_id: # 不存在查询词的处理
print(f"{query} is not found")
return
print(f'{query}')
query_id = word_to_id[query] # 先找到查询词的id
query_vec = word_matrix[query_id] # 从共现矩阵中找出对应id的向量
# 计算相似度
vocab_size = len(id_to_word) # 词汇总长度
similarity = np.zeros(vocab_size) # 相似度初始值;全0
for i in range(vocab_size): # 循环计算余弦相似度;
similarity[i] = cos_similarity(word_matrix[i], query_vec) # 赋值给对应的similarity的位置
# 基于余弦相似度降序输出值
count = 0
for i in (-1 * similarity).argsort(): # argsort是返回索引值
if id_to_word[i] == query:
continue
print(f'{id_to_word[i]}: {similarity[i]}')
count = 1
if count >= top:
return
调用函数
代码语言:javascript复制import numpy as np
import sys
sys.path.append('..')
text = "you say goodbye and I say hello."
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
C = create_to_matrix(corpus, vocab_size) # 共现矩阵
most_similar("you", word_to_id, id_to_word, C, top=5)
代码语言:javascript复制you
goodbye: 0.7071067691154799
i: 0.7071067691154799
hello: 0.7071067691154799
say: 0.0
and: 0.0
基于【计数】存在问题
比如,我们来考虑某个语料库中the和car共现的情况:
在这种情况下,我们会看到很多...the car...
这样的短语。
因此,它们的共现次数将会很大。另外,car和drive也明显有很强的相关性。
但是,如果只看单词的出现次数,那么与drive相比,the和car的相关性更强。
这意味着,仅仅因为the是个常用词,它就被认为与car有很强的相关性
解决方法
点互信息PMI
使用点互信息Pointwise Mutual Information,PMI
;PMI值越高表示相关性越强
定义为:
- P(x):表示x发生的概率
- P(x,y):表示x和y同时发生的概率
使用共现矩阵来重写上面的式子:
其中:N表示语料库的单词数量N
优化方案PPMI
上面基于点的互信息的方法有个缺点:当两个单词的共现次数为0时,会出现log_2{0}= infty
使用正的点互信息Positive Pointwise Mutual Information,PPMI
代码实现PPMI
代码语言:javascript复制def ppmi(C, verbose=False, eps=1e-8):
"""
基于共现矩阵C求解PPMI
"""
M = np.zeros_like(C, dtype=np.float32) # 类比C共现矩阵的全0矩阵;后面进行更新
N = np.sum(C) # 全部数据求和:共现单词总个数
S = np.sum(C,axis=0) # 行方向求和
#print("C: n", C) # 共现矩阵
#print("初始化M: n", M) # 和共现矩阵行列数相同的全0矩阵(方阵)
#print("N: n", N) # 共现矩阵中所有数之和
#print("S: n", S) # 共现矩阵在每行上的求和
total = C.shape[0] * C.shape[1]
cnt = 0
for i in range(C.shape[0]): # 行
for j in range(C.shape[1]): # 列
pmi = np.log2(C[i,j] * N / (S[j] * S[i]) eps) # 计算pmi和ppmi
#print("pmi: ",pmi)
#print(max(0, pmi))
M[i,j] = max(0, pmi)
if verbose:
cnt = 1
if cnt % (total // 100 1) == 0:
print('%.1f%% done' % (100 * cnt / total))
#print("PPMI矩阵: n", M)
return M
代码语言:javascript复制import numpy as np
# numpy输出有效位数
np.set_printoptions(precision=3)
import sys
sys.path.append('..')
text = "you say goodbye and I say hello."
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
C = create_to_matrix(corpus, vocab_size) # 共现矩阵
M = ppmi(C)
代码语言:javascript复制M
代码语言:javascript复制array([[0. , 1.807, 0. , 0. , 0. , 0. , 0. ],
[1.807, 0. , 0.807, 0. , 0.807, 0.807, 0. ],
[0. , 0.807, 0. , 1.807, 0. , 0. , 0. ],
[0. , 0. , 1.807, 0. , 1.807, 0. , 0. ],
[0. , 0.807, 0. , 1.807, 0. , 0. , 0. ],
[0. , 0.807, 0. , 0. , 0. , 0. , 2.807],
[0. , 0. , 0. , 0. , 0. , 2.807, 0. ]], dtype=float32)
降维-dimensionality reduction
PPMI存在问题
PPMI矩阵存在的问题:
- 维度爆炸:随着语料库词汇量的增加,各个单词向量的维度也会随着增加
- 矩阵稀疏:在PPMI矩阵中存在很多的元素都是0,这表明向量中的很多元素是不重要的
- 向量中的大多数元素为0的矩阵(向量)称为稀疏矩阵(稀疏向量)
- 从稀疏向量中找出重要的轴,用更少的维度对其重新表示;稀疏矩阵转化为密集矩阵
奇异值分解SVD-Singular Value Decomposition
SVD基本原理:
SVD可以将任意矩阵分解为3个矩阵的乘积:
- UV是列向量彼此正交的正交矩阵;U矩阵构成了一些空间的基轴(基向量),看做是"单词空间"。
- S是除了对角线元素外其他元素均为0的对角矩阵;奇异值在对角线上降序排列
- S中奇异值越小,对应的基轴的重要性越低;因此通过去除U中多余的列向量来近似原始矩阵
基于SVD的降维
代码语言:javascript复制import numpy as np
# numpy输出有效位数
np.set_printoptions(precision=3)
import sys
sys.path.append('..')
text = "you say goodbye and I say hello."
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
C = create_to_matrix(corpus, vocab_size) # 共现矩阵
M = ppmi(C)
# 降维
U,S,V = np.linalg.svd(M)
对比3大矩阵
对比原共现矩阵、PPMI矩阵、经过SVD降维后的密集U
代码语言:javascript复制C[0] # 共现矩阵
代码语言:javascript复制array([0, 1, 0, 0, 0, 0, 0], dtype=int32)
代码语言:javascript复制M[0] # PPMI矩阵
代码语言:javascript复制array([0. , 1.807, 0. , 0. , 0. , 0. , 0. ], dtype=float32)
代码语言:javascript复制U[0] # PPMI矩阵的稀疏向量转成了密集向量U
代码语言:javascript复制array([ 3.409e-01, -1.110e-16, -1.205e-01, -4.163e-16, -9.323e-01,
-1.110e-16, -2.426e-17], dtype=float32)
比如现在我们将这个密集向量降到二维,只要取出前面两个元素:
代码语言:javascript复制print(U)
代码语言:javascript复制[[ 3.409e-01 -1.110e-16 -1.205e-01 -4.163e-16 -9.323e-01 -1.110e-16
-2.426e-17]
[ 0.000e 00 -5.976e-01 0.000e 00 1.802e-01 0.000e 00 -7.812e-01
0.000e 00]
[ 4.363e-01 -5.551e-17 -5.088e-01 -2.220e-16 2.253e-01 -1.388e-17
-7.071e-01]
[ 1.665e-16 -4.978e-01 2.776e-17 6.804e-01 -1.110e-16 5.378e-01
7.467e-17]
[ 4.363e-01 -3.124e-17 -5.088e-01 -1.600e-16 2.253e-01 -1.302e-17
7.071e-01]
[ 7.092e-01 -3.124e-17 6.839e-01 -1.600e-16 1.710e-01 -1.302e-17
2.314e-17]
[-1.665e-16 -6.285e-01 -4.163e-17 -7.103e-01 2.220e-16 3.169e-01
-9.614e-17]]
代码语言:javascript复制U[0, :2]
代码语言:javascript复制array([ 3.409e-01, -1.110e-16], dtype=float32)
降维可视化
代码语言:javascript复制import matplotlib.pyplot as plt
%matplotlib inline
# 和书中降维后的二维系数是反的;
# 在绘图的时候数据也是先取第1个,再取第0个
for word, word_id in word_to_id.items():
plt.annotate(word, (U[word_id, 1], U[word_id, 0]))
plt.scatter(U[:,1], U[:, 0], alpha=0.5)
plt.show()
降维进阶-Truncated SVD
如果矩阵大小是N,SVD的计算复杂度将达到N^3;计算量大大增加,现实中无法达到。通常使用Truncated SVD等方法。Truncated SVD通过截去奇异值较小的部分,从而实现高速化。
PTB数据集(略)
PTB语料库是以文件文本的形式提供,一行保存一个句子。实战案例略。