Fork me on GitHub

NLP学习3-基于计数方法的改进

基于计数方法的改进

本文记录的是鱼书第3章:如何对原有的计数方法进行改进。

基于统计方法函数

下面介绍的是传统的基于统计的方法。

预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 常用函数

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

共现矩阵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

相似度计算

1
2
3
4
5
6
7
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)

相似单词的降序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

调用函数

1
2
3
4
5
6
7
8
9
10
11
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)
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值越高表示相关性越强

定义为:

$$PMI(x,y)=log_2 \frac{P(x,y)}{P{(x)}{P(y)}}$$

  • $P(x)$:表示x发生的概率
  • $P(x,y)$:表示x和y同时发生的概率

使用共现矩阵来重写上面的式子:

$$PMI(x,y)=log_2 \frac{P(x,y)}{P{(x)}{P(y)}} = log_2\frac{\frac{C{(x,y)}}{N}}{\frac{C{(x)}}{N} \frac{C{(y)}}{N}} = log_2 \frac{C{(x,y)} * N}{C{(x)}C{(y)}}$$

其中:N表示语料库的单词数量N

优化方案PPMI

上面基于点的互信息的方法有个缺点:当两个单词的共现次数为0时,会出现$log_2{0}= \infty$

使用正的点互信息Positive Pointwise Mutual Information,PPMI

$$PPMI(x,y) = max(0, PMI(x,y))$$

代码实现PPMI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
1
2
3
4
5
6
7
8
9
10
11
12
13
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)
1
M
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矩阵存在的问题:

  1. 维度爆炸:随着语料库词汇量的增加,各个单词向量的维度也会随着增加

  2. 矩阵稀疏:在PPMI矩阵中存在很多的元素都是0,这表明向量中的很多元素是不重要的

  3. 向量中的大多数元素为0的矩阵(向量)称为稀疏矩阵(稀疏向量)

  4. 从稀疏向量中找出重要的轴,用更少的维度对其重新表示;稀疏矩阵转化为密集矩阵

奇异值分解SVD-Singular Value Decomposition

SVD基本原理:

SVD可以将任意矩阵分解为3个矩阵的乘积:

$$X = USV^T$$

  • UV是列向量彼此正交的正交矩阵;U矩阵构成了一些空间的基轴(基向量),看做是"单词空间"。

  • S是除了对角线元素外其他元素均为0的对角矩阵;奇异值在对角线上降序排列

  • S中奇异值越小,对应的基轴的重要性越低;因此通过去除U中多余的列向量来近似原始矩阵

基于SVD的降维

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
C[0]  # 共现矩阵
array([0, 1, 0, 0, 0, 0, 0], dtype=int32)
1
M[0]  # PPMI矩阵
array([0.   , 1.807, 0.   , 0.   , 0.   , 0.   , 0.   ], dtype=float32)
1
U[0] # PPMI矩阵的稀疏向量转成了密集向量U
array([ 3.409e-01, -1.110e-16, -1.205e-01, -4.163e-16, -9.323e-01,
       -1.110e-16, -2.426e-17], dtype=float32)

比如现在我们将这个密集向量降到二维,只要取出前面两个元素:

1
print(U)
[[ 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]]
1
U[0, :2]
array([ 3.409e-01, -1.110e-16], dtype=float32)

降维可视化

1
2
3
4
5
6
7
8
9
10
11
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语料库是以文件文本的形式提供,一行保存一个句子。实战案例略。

本文标题:NLP学习3-基于计数方法的改进

发布时间:2023年01月28日 - 22:01

原始链接:http://www.renpeter.cn/2023/01/28/%E9%B1%BC%E4%B9%A6%E7%AC%94%E8%AE%B03-%E5%9F%BA%E4%BA%8E%E8%AE%A1%E6%95%B0%E6%96%B9%E6%B3%95%E7%9A%84%E6%94%B9%E8%BF%9B.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Coffee or Tea