NLP-Assignment2

Assignment2

解答:理解词向量(23分)

我们先快速回顾一下word2vec算法,它的核心思想是“一个词的含义可以由它周围的词来表示”。具体来说,我们有一个中心词(center word) c,和这个词 c 周围上下文构成的窗口,这个窗口内的除了 c 之外的词叫做外围词(outside words)。比如下图中,中心词是“banking”,窗口大小为2,所以上下文窗口是:“turning”、”into“、”crises“和”as“。

Skip-gram模型(word2vec比较常用的一种实现,另一种是cbow)目的是学习概率分布 。这样一来,就能计算给定的一个词 和词 的概率 (即,在已知词 出现的情况下,词 出现的概率), 其中 是中心词, 是窗口中非中心的外围词。

在word2vec中,这个条件概率分布是通过计算向量点积(dot-products),再应用naive-softmax函数得到的:

这里,向量代表外围词,向量代表中心词。为了包含这些向量,我们有两个矩阵 的列是外围词, 的列是中心词,这两矩阵都有所有词的表示 。

对于词 和词 ,损失函数为对数几率:

可以从交叉熵的角度看这个损失函数。真实值为 ,是一个独热向量,预测值 计算得到。具体来说, 如果是第k个单词,那么它的第k维为1,其余维都是0,而 的第k维表示这是第k个词的概率大小。

问题(a) (3分)

证明公式(2)给出的naive-softmax的损失函数,和 的交叉熵损失函数是一样的,均如下所示(答案控制在一行)

答:因为除了 之外的词都不在窗口内,所以只有词 对损失函数有贡献

问题(b) (5分)

计算损失函数 对中心词 的偏导数,用 来表示。

答:

问题(c) (5分)

计算损失函数 对上下文窗口内的词 的偏导数,考虑两种情况,即 是外围词 ,和 不是 ,用 来表示。

答:

问题(d) (3分)

sigmoid函数如公式所示 请计算出它对于 的导数, 是一个向量

答:

计算雅克比矩阵可得 所以有

问题(e) (4分)

现在我们考虑负采样的损失函数。假设有K个负样本,表示为,它们对应的向量为 ,外围词 ,则外围词 在中心词是 时产生的损失函数如公式所示。 根据该损失函数,重新计算问题(b)、问题(c)的偏导数,用 来表示。

完成计算后,简要解释为什么这个损失函数比naive-softmax效率更高。

注意:你可以用问题(d)的答案来帮助你计算导数

答:

  • 原始的损失函数中需要求指数和,很容易溢出,负采样的损失函数能很好地规避这个问题。

  • 词库从 𝑉𝑜𝑐𝑎𝑏 变成了K+1个词

  • 在求内层导数的时候用了sigmoid函数

问题(f) (3分)

假设中心词是 ,上下文窗口是 是窗口大小,回顾skip-gram的word2vec实现,在该窗口下的总损失函数是: 这里,是外围词是外围词在中心词在中心词 下产生的损失,损失函数可以是naive-softmax或者是neg-sample(负采样),这取决于具体实现。

计算:

  1. 损失函数对 的偏导数

  2. 损失函数对 的偏导数

  3. 损失函数对 的偏导数

答: $$

$$

代码:实现word2vec(20分)

点击 此处 下载代码,python版本 >= 3.5,需要安装numpy,你利用conda来配置环境:

1
2
conda env create -f env.yml
conda activate a2

写完代码后,运行:

1
conda deactivate

问题(a) (12分)

首先,实现 word2vec.py 里的 sigmoid函数,要支持向量输入。接着实现同一个文件里的 softmax 、负采样损失和导数。然后实现skip-gram的损失函数和导数。全部做完之后,运行python word2vec.py来检查是否正确。

答:

sigmoid

numpy具备广播特性,最终得到向量输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def sigmoid(x):
"""
Compute the sigmoid function for the input here.
Arguments:
x -- A scalar or numpy array.
Return:
s -- sigmoid(x)
"""

### YOUR CODE HERE
s = 1 / (1 + np.exp(-x))

### END YOUR CODE

return s

naiveSoftmaxLossAndGradient

这个模型其实就是一个三层的前馈神经网络,只需要注意维度即可,注释里已经标记出了维度。

需要注意的是,单词表示是在行,而不是列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
### YOUR CODE HERE

### Please use the provided softmax function (imported earlier in this file)
### This numerically stable implementation helps you avoid issues pertaining
### to integer overflow.
'''
centerWordVec: 1 * d
outsideVectors: n * d
'''
#1 * n
vec = centerWordVec.dot(outsideVectors.T)
#1 * n
prob = softmax(vec)
loss = -np.log(prob[outsideWordIdx])
#1 * d
gradCenterVec = -outsideVectors[outsideWordIdx] + prob.dot(outsideVectors)
#n * d
gradOutsideVecs = prob.reshape(-1, 1).dot(centerWordVec.reshape(1, -1))
#n * d
gradOutsideVecs[outsideWordIdx] -= centerWordVec
### END YOUR CODE

negSamplingLossAndGradient

与native-softmax不同的是:

  • 只选取K个非外围词作为负样本,加上1个正确的外围词,共K+1个输出
  • 最后一层使用sigmoid输出,而不是softmax

注意,反向传播得到的是这K+1个词的梯度,所以需要挨个更新到 梯度矩阵 中去

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
### Please use your implementation of sigmoid in here.

# indices might have same index
# extract W
W = np.zeros((len(indices), outsideVectors.shape[1]))
for i in range(len(indices)):
W[i] = outsideVectors[indices[i]]

# forward
a = centerWordVec
a = a.reshape((a.shape[0], 1))

z = np.dot(W, a) # (K+1, 1)
preds = sigmoid(z)

# backprop
y = np.zeros((preds.shape[0], 1))
y[0] = 1 # index 0 is target

loss = -(y*np.log(preds) + (1 - y)*np.log(1 - preds)).sum()

delta = preds - y
gradCenterVec = np.dot(W.T, delta) # (V, 1)
gradW = np.dot(delta, a.T) # (K+1, V)
gradCenterVec = gradCenterVec.flatten()

# apply gradW into gradOutsideVecs
gradOutsideVecs = np.zeros_like(outsideVectors)
for i in range(len(indices)):
oi = indices[i]
gradOutsideVecs[oi] += gradW[i]

skipgram

遍历所有的外围词,求和损失函数

1
2
3
4
5
6
7
8
9
ci = word2Ind[currentCenterWord]
vc = centerWordVectors[ci]

for o in outsideWords:
oi = word2Ind[o]
loss_, gradVc, gradUo = word2vecLossAndGradient(vc, oi, outsideVectors, dataset)
gradCenterVecs[ci] += gradVc
gradOutsideVectors += gradUo
loss += loss_

问题(b) (4分)

完成sgd.py文件的SGD优化器,运行python sgd.py来检查是否正确。

sgd

调用函数得到损失值和梯度,更新即可

1
2
3
4
5
### YOUR CODE HERE
loss, grad = f(x)
x -= step * grad

### END YOUR CODE

问题(c) (4分)

至此所有的代码都写完了,接下来是下载数据集,这里我们使用Stanform Sentiment Treebank(SST)数据集,它可以用在简单的语义分析任务中去。通过运行 sh get_datasets.sh 可以获得该数据集,下载完成后运行 python run.py 即可。

注意:训练的时间取决于代码是否高效(即便是高效的实现,也要跑接近一个小时)

经过40,000次迭代后,最终结果会保存到 word_vectors.png 里。

答:

  • 在上图中可以观察到,male->famale 和 king -> queen 这两条向量是平行的
  • (women, famale),(enjoyable,annoying) 这些含义接近的词距离很近

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!