赞
踩
自然语言定义:它是一种能够让计算机理解人类语言的技术。换言之,自然语言处理的目标就是让计算机理解人说的话,进而完成对我们有帮助的事情。
单词定义:我们的语言是由文字构成的,而语言的含义是由单词构成的,单词是含义的最小单位。
为了让计算机理解自然语言,让它理解 单词含义可以说是最重要的事情了。
让计算机理解单词含义的方法,或者说单词含义的表示方法:
基于同义词词典的方法
基于计数的方法
基于推理的方法(word2vec)
要表示单词含义,首先可以考虑通过人工方式来定义单词含义。有两种方法:
一种方法是像《新华字典》那样,一个词一个词地说明单词含义。
一种被称为同义词词典(thesaurus)的词典。在同义词词 典中,具有相同含义的单词(同义词)或含义类似的单词(近义词)被归 类到同一个组中。
两种方法都是为了是计算机理解单词含义,但是第二种方法使用更加广泛。此外,在自然语言处理中用到的同义词词典有时会定义单词之间的粒度更细的关系,比如 “上位 - 下位” 关系、“整体 - 部分” 关系。
通过对所有单词创建近义词集合,并用图表示各个单词的关系,可以定义单词之间的联系,计算单词之间的相似度等。利用这个“单词网络”,可以教会计算机单词之间的相关性。也就是说,我们可以将单词含义(间接地)教给计算机, 然后利用这一知识,就能让计算机做一些对我们有用的事情。
在自然语言处理领域,最著名的同义词词典是 WordNet(感兴趣参考附录B)。
WordNet 等同义词词典中对大量单词定义了同义词和层级结构关系等。 利用这些知识,可以(间接地)让计算机理解单词含义。不过,人工标记也存在一些较大的缺陷。
难以顺应时代变化
人力成本高
无法表示单词的微妙差异
语料库(corpus):语料库就是大量用于自然语言处理研究和应用的文本数据。其实语料库只是一些文本数据而已。
基于计数的方法的目标就是从这些富有实践知识的语料库中,自动且高效地提取本质。
自然语言处理领域存在各种各样的语料库。说到有名的语料库,有 Wikipedia 和 Google News 等。
这里,先使用仅包含一个句子的简单文本作为语料库,然后再处理更实用的语料库。
使用的语料库:You say goodbye and I say hello.
分词操作:
text = 'You say goodbye and I say hello.'
text = text.lower()
text = text.replace('.', ' .')
words = text.split(' ')
words 的结果为:['you', 'say', 'goodbye', 'and', 'i', 'say', 'hello', '.']
上述操作也可以使用正则表达式进行分词,在python中导入re模块,使用 re.split('(\W+)?', text)
也可以进行分词。
下面,为方便后续处理,进一步给单词标上 ID,以便使用单词 ID 列表。
word_to_id = {}
id_to_word = {}
for word in words:
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_to_word 内容:{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6:'.'}
字典 word_to_id 内容:{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
最后,将单词列表转化为单词 ID 列表。
corpus = [word_to_id[w] for w in words]
corpus = np.array(corpus)
corpus 中内容:array([0, 1, 2, 3, 4, 1, 5, 6])
到此,就完成了利用语料库的准备工作。可以将上述操作封装为一个函数 preprocess():
def preprocess(text):
text = text.lower()
text = text.replace('.', ' .')
words = text.split(' ')
word_to_id = {}
id_to_word = {}
for word in words:
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
corpus = np.array([word_to_id[w] for w in words])
return corpus, word_to_id, id_to_word
在自然语言处理领域,单词含义的向量表示,被称为分布式表示。
单词的分布式表示将单词表示为固定长度的向量。这种向量的特征在于它是用密集向量表示的。密集向量的意思是,向量的各个元 素(大多数)是由非 0 实数表示的。
在自然语言处理的历史中,用向量表示单词的研究有很多。如果仔细看一下这些研究,就会发现几乎所有的重要方法都基于一个简单的想法,这个想法就是“某个单词的含义由它周围的单词形成”,称为分布式假设(distributional hypothesis)。单词本身没有含义,单词含义由它所在的上下文(语境)形成。的确,含义相同的单词经常出现在相同的语境中。
如图上图所示,上下文是指某个居中单词的周围词汇。这里,我们将上下文的大小(即周围的单词有多少个)称为窗口大小(window size)。窗口 大小为 1,上下文包含左右各 1 个单词;窗口大小为 2,上下文包含左右各 2 个单词,以此类推。
注意,我们将左右两边相同数量的单词作为上下文。但是,根据具体情况,也可以仅将左边的单词或者右边的单词作为上下文。 此外,也可以使用考虑了句子分隔符的上下文。
下面,考虑如何基于分布式假设使用向量表示单词,最直截了当的实现方法是对周围单词的数量进行计数。具体来说,在关注某个单词的情况 下,对它的周围出现了多少次什么单词进行计数,然后再汇总。这里,我们将这种做法称为“基于计数的方法”,在有的文献中也称为“基于统计的方法”。
现在,对基于技术的方法,如何实现。先使用函数 preprocess() 对语料进行预处理:
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
print(corpus)
# [0 1 2 3 4 1 5 6]
print(id_to_word)
# {0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
从上面的结果可以看出,词汇总数为 7 个。下面,我们计算每个单词的上下文所包含的单词的频数。在这个例子中,我们将窗口大小设为 1,从单词 ID 为 0 的 you 开始。
从下图可以清楚地看到,单词 you 的上下文仅有 say 这个单词。用表格表示如图。
图 2-5 表示的是作为单词 you 的上下文共现的单词的频数。同时,这也意味着可以用向量 [0, 1, 0, 0, 0, 0, 0] 表示单词 you。
接着对单词 ID 为 1 的 say 进行同样的处理,结果如图 2-6 所示。
从上面的结果可知,单词 say 可以表示为向量 [1, 0, 1, 0, 1, 1, 0]。 对所有的 7 个单词进行上述操作,会得到如图 2-7 所示的结果。
图 2-7 是汇总了所有单词的共现单词的表格。这个表格的各行对应相应单词的向量。因为图 2-7 的表格呈矩阵状,所以称为共现矩阵(co-occurence matrix)。
至此,我们通过共现矩阵成功地用向量表示了单词。
下面是实现从语料库直接生成共现矩阵的代码,分装在函数 create_co_matrix(corpus, vocab_size, window_size=1),其中参数 corpus 是单词 ID 列表,参数 vocab_ size 是词汇个数,window_size 是窗口大小。
def create_co_matrix(corpus, vocab_size, window_size=1):
corpus_size = len(corpus) # 语料库长度
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:
left_word_id = corpus[left_idx]
co_matrix[word_id, left_word_id] += 1
if right_idx < corpus_size:
right_word_id = corpus[right_idx]
co_matrix[word_id, right_word_id] += 1
return co_matrix
测量向量间的相似度有很多方法,其中具有代表性的方法有向量内积或欧式距离等。虽然除此之外还有很多方法,但是在测量单词的向量表示的相似度方面,余弦相似度(cosine similarity)是很常用的。
假设
x
=
(
x
1
,
x
2
,
x
3
,
…
,
x
n
)
x = (x_1, x_2, x_3, \dots, x_n)
x=(x1,x2,x3,…,xn) 和
y
=
(
y
1
,
y
2
,
y
3
,
…
,
y
n
)
y = (y_1, y_2, y_3, \dots, y_n)
y=(y1,y2,y3,…,yn) 两个向量,它们之间的余弦相似度定义如下:
s
i
m
i
l
a
r
i
t
y
(
x
,
y
)
=
x
⋅
y
∣
∣
x
∣
∣
∣
∣
y
∣
∣
=
x
1
y
1
+
⋯
+
x
n
y
n
x
1
2
+
⋯
+
x
n
2
y
1
2
+
⋯
+
y
n
2
similarity(x, y) = \frac{x \cdot y}{||x|| \ ||y||} = \frac{x_1y_1 + \dots + x_ny_n}{\sqrt{x_1^2 + \dots + x_n^2} \sqrt{y_1^2 + \dots + y_n^2}}
similarity(x,y)=∣∣x∣∣ ∣∣y∣∣x⋅y=x12+⋯+xn2
y12+⋯+yn2
x1y1+⋯+xnyn
分子是向量内积,分母是各个向量的范数。范数表示向量的大小,这里计算的是 L2 范数(即向量各个元素的平方和的平方根)。
注意上式中的内积,要先对向量进行正规化,再求它们的内积。
余弦相似度直观地表示了“两个向量在多大程度上指向同一方向”。 两个向量完全指向相同的方向时,余弦相似度为 1;完全指向相反 的方向时,余弦相似度为 −1。
余弦相似度代码实现:
def cos_similarity(x, y):
nx = x / np.sqrt(np.sum(x**2)) # x的正规化
ny = y / np.sqrt(np.sum(y**2)) # y的正规化
return np.dot(nx, ny)
这里余弦相似度的实现虽然完成了,但是还有一个问题。那就是当零向量(元素全部为 0 的向量)被赋值给参数时,会出现 “除数为 0”(zero division)的错误。
解决此类问题的一个常用方法是,在执行除法时加上一个微小值。这里,通过参数指定一个微小值 eps(eps 是 epsilon 的缩写),并默认 eps=1e-8 (= 0.000 000 01)。这样修改后的余弦相似度的实现如下所示:
def cos_similarity(x, y, eps=1e-8):
nx = x / (np.sqrt(np.sum(x ** 2)) + eps)
ny = y / (np.sqrt(np.sum(y ** 2)) + eps)
return np.dot(nx, ny)
下面是求 you 和 i(= I)的相似度:
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_co_matrix(corpus, vocab_size) # 创建共现矩阵
c0 = C[word_to_id['you']] # you的单词向量
c1 = C[word_to_id['i']] # i的单词向量
print(cos_similarity(c0, c1))
# 0.7071067691154799
实现函数 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 | 显示到最相似的前几位 |
代码实现:
def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):
# 取出查询词
if query not in word_to_id:
print('%s is not found' % query)
return
print('\n[query] ' + query)
query_id = word_to_id[query]
query_vec = word_matrix[query_id] # 查询词的词向量
# 计算余弦相似度
vocab_size = len(id_to_word)
similarity = np.zeros(vocab_size)
for i in range(vocab_size):
similarity[i] = cos_similarity(word_matrix[i], query_vec)
# 基于余弦相似度,按降序输出值
count = 0
for i in (-1 * similarity).argsort():
if id_to_word[i] == query:
continue
print(' %s: %s' % (id_to_word[i], similarity[i]))
count += 1
if count >= top:
return
这里使用 argsort() 方法对数组的索引进行了重排。这个 argsort() 方法可以按升序对 NumPy 数组的元素进行排序(不过,返回值是数组的索引)。
将 you 作为查询词, 显示与其相似的单词,代码如下所示:
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_co_matrix(corpus, vocab_size)
most_similar('you', word_to_id, id_to_word, C, top=5)
执行代码后,会得到如下结果。
[query] you
goodbye: 0.7071067691154799
i: 0.7071067691154799
hello: 0.7071067691154799
say: 0.0
and: 0.0
这个结果只按降序显示了 you 这个查询词的前 5 个相似单词,各个单 词旁边的值是余弦相似度。
上一节的共现矩阵的元素表示两个单词同时出现的次数。但是,这种 “原始”的次数并不具备好的性质。
比如,我们来考虑某个语料库中 the 和 car 共现的情况。在这种情况下, 我们会看到很多“…the car…”这样的短语。因此,它们的共现次数将会很大。另外,car 和 drive 也明显有很强的相关性。但是,如果只看单词的出现次数,那么与 drive 相比,the 和 car 的相关性更强。这意味着,仅仅因为 the 是个常用词,它就被认为与 car 有很强的相关性。
为了解决这一问题,可以使用点互信息(Pointwise Mutual Information, PMI)这一指标。
对于随机变量
x
x
x 和
y
y
y,它们的
P
M
I
PMI
PMI 定义如下:
P
M
I
(
x
,
y
)
=
l
o
g
2
P
(
x
,
y
)
P
(
x
)
P
(
y
)
PMI(x,y) = log_2{\frac{P(x,y)}{P(x)P(y)}}
PMI(x,y)=log2P(x)P(y)P(x,y)
其中,
P
(
x
)
P(x)
P(x) 表示
x
x
x 发生的概率,
P
(
y
)
P(y)
P(y) 表示
y
y
y 发生的概率,
P
(
x
,
y
)
P(x, y)
P(x,y) 表示
x
x
x 和
y
y
y 同时发生的概率。在自然语言的例子中,
P
(
x
)
P(x)
P(x) 就是指单词
x
x
x 在语料库中出现的概率。
P
M
I
PMI
PMI 的值越高,表明相关性越强。
现在,我们使用共现矩阵(其元素表示单词共现的次数)来重写上式。 这里,将共现矩阵表示为
C
C
C,将单词
x
x
x 和
y
y
y 的共现次数表示为
C
(
x
,
y
)
C(x, y)
C(x,y),将 单词
x
x
x 和
y
y
y 的出现次数分别表示为
C
(
x
)
C(x)
C(x)、
C
(
y
)
C(y)
C(y),将语料库的单词数量记为
N
N
N,则上式可以重写为:
P
M
I
(
x
,
y
)
=
l
o
g
2
P
(
x
,
y
)
P
(
x
)
P
(
y
)
=
l
o
g
2
C
(
x
,
y
)
N
C
(
x
)
N
C
(
y
)
N
=
l
o
g
2
C
(
x
,
y
)
⋅
N
C
(
x
)
C
(
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) \cdot N}{C(x)C(y)}
PMI(x,y)=log2P(x)P(y)P(x,y)=log2NC(x)NC(y)NC(x,y)=log2C(x)C(y)C(x,y)⋅N
虽然已经获得了
P
M
I
PMI
PMI 这样一个好的指标,但是
P
M
I
PMI
PMI 也有一个问题。那就是当两个单词的共现次数为 0 时,
l
o
g
2
0
=
−
∞
log_20 = −∞
log20=−∞。为了解决这个问题, 实践上我们会使用下述正的点互信息(Positive PMI,PPMI)。
P
P
M
I
(
x
,
y
)
=
m
a
x
(
0
,
P
M
I
(
x
,
y
)
)
PPMI(x,y) = max(0, PMI(x,y))
PPMI(x,y)=max(0,PMI(x,y))
下面实现从共现矩阵转换为
P
P
M
I
PPMI
PPMI 矩阵的函数 ppmi(C, verbose=False, eps=1e-8),其中 C 表示共现矩阵,verbose 是决定是否输出运行情况的标志。
def ppmi(C, verbose=False, eps=1e-8):
M = np.zeros_like(C, dtype=np.float32)
N = np.sum(C)
S = np.sum(C, axis=0)
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) # 为了防止log2(0)=-inf添加了eps
M[i, j] = max(0, pmi)
if verbose:
cnt += 1
if cnt % (total//100+1) == 0:
print('%.1f%% done' % (100*cnt/total))
return M
现在对语料库转换为 P P M I PPMI PPMI 矩阵的实现如下:
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_co_matrix(corpus, vocab_size)
W = ppmi(C)
np.set_printoptions(precision=3) # 有效位数为3位
print('covariance matrix')
print(C)
print('-'*50)
print('PPMI')
print(W)
运行该文件,可以得到下述结果:
covariance matrix
[[0 1 0 0 0 0 0]
[1 0 1 0 1 1 0]
[0 1 0 1 0 0 0]
[0 0 1 0 1 0 0]
[0 1 0 1 0 0 0]
[0 1 0 0 0 0 1]
[0 0 0 0 0 1 0]]
--------------------------------------------------
PPMI
[[ 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. ]]
这样一来,就将共现矩阵转化为了 PPMI 矩阵。此时,PPMI 矩 阵的各个元素均为大于等于 0 的实数。我们得到了一个由更好的指标形成的 矩阵,这相当于获取了一个更好的单词向量。
通过这样的处理可见,这样的分布式表示具有在含义或语法上相似的单词在向量空间上位置相近的性质。
P P M I PPMI PPMI 矩阵存在的问题:
降维(dimensionality reduction),就是在尽量保留“重要信息”的基础上减少向量维度。
如上图所示,考虑到数据的广度,导入了一根新轴,以将原来用二维坐标表示的点表示在一个坐标轴上。此时,用新轴上的投影值来表示各个数据点的值。这里非常重要的一点是,选择新轴时要考虑数据的广度。
向量中的大多数元素为 0 的矩阵(或向量)称为稀疏矩阵(或稀疏向量)。这里的重点是,从稀疏向量中找出重要的轴,用更少的维度对其进行重新表示。结果,稀疏矩阵就会被转化为大多数元素均不为 0 的密集矩阵。这个密集矩阵就是我们想要的单词的分布式表示。
降维的方法有很多,这里我们使用奇异值分解(Singular Value Decomposition,SVD)。
SVD 将任意矩阵分解为 3 个矩阵的乘积,如下式所示:
X
=
U
S
T
T
X=UST^T
X=USTT
SVD 将任意的矩阵
X
X
X 分解为
U
、
S
、
V
U、S、V
U、S、V 这 3 个矩阵的乘积,其中
U
U
U 和
V
V
V 是列向量彼此正交的正交矩阵,
S
S
S 是除了对角线元素以外其余元素均为 0 的对角矩阵。
U U U 是正交矩阵。这个正交矩阵构成了一些空间中的基轴 (基向量),我们可以将矩阵 U U U 作为“单词空间”。 S S S 是对角矩阵,奇异值在对角线上降序排列。简单地说,我们可以将奇异值视为“对应的基轴”的重要性。这样一来,减少非重要元素就成为可能。
矩阵 S S S 的奇异值小,对应的基轴的重要性低,因此,可以通过去除矩阵 U U U 中的多余的列向量来近似原始矩阵。用我们正在处理 的“单词的 PPMI 矩阵”来说明的话,矩阵 X X X 的各行包含对应的单词 ID 的单词向量,这些单词向量使用降维后的矩阵 U ′ U' U′ 表示。
接下来,使用 Python 来实现 SVD,这里可以使用 NumPy 的 linalg 模块中的 svd 方法。linalg 是 linear algebra(线性代数)的简称。下面,我们创建一个共现矩阵,将其转化为 PPMI 矩阵,然后对其进行 SVD降维。
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(id_to_word)
C = create_co_matrix(corpus, vocab_size, window_size=1)
W = ppmi(C)
U, S, V = np.linalg.svd(W) # SVD
SVD 执行完毕。上面的变量 U 包含经过 SVD 转化的密集向量表示。现在,来看一下它的内容。单词 ID 为 0 的单词向量如下:
print(C[0]) # 共现矩阵
# [0 1 0 0 0 0 0]
print(W[0]) # PPMI矩阵
# [ 0. 1.807 0. 0. 0. 0. 0. ]
print(U[0]) # SVD
# [ 3.409e-01 -1.110e-16 -1.205e-01 -4.441e-16 0.000e+00 -9.323e-01 2.226e-16]
如上所示,原先的稀疏向量 W[0] 经过 SVD 被转化成了密集向量 U[0]。如果要对这个密集向量降维,比如把它降维到二维向量,取出前两个元素即可。
print(U[0, :2])
# [ 3.409e-01 -1.110e-16]
如果矩阵大小是 N,SVD 的计算的复杂度将达到 O ( N 3 ) O(N^3) O(N3)。这意味着 SVD 需要与 N 的立方成比例的计算量。因为现实中这样的计算量是做不到的,所以往往会使用 Truncated SVD[21] 等更快的方法。 Truncated SVD 通过截去(truncated)奇异值较小的部分,从而实现高速化。作为另一个选择,可以使用 sklearn 库的 Truncated SVD。
全称 Penn Treebank 语料库。这个 PTB 语料库是以文本文件的形式提供的,与原始的 PTB 的文章相比,多了若干预处理,包括将稀有单词替换成特殊字符 (unk 是 unknown 的简称),将具体的数字替换成 “N” 等。下面,我们将经过这些预处理之后的文本数据作为 PTB 语料库使用。
在 PTB 语料库中,一行保存一个句子。在本书中,我 们将所有句子连接起来,并将其视为一个大的时序数据。此时,在每个句子的结尾处插入一个特殊字符 (eos 是 end of sentence 的简称)。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。