本次作业与代码见上篇翻译
word2vec.py
实现sigmoid方法
我们可以注意到,sigmoid函数形似
因此对于参数:x -- 一个标量或NumPy数组,我们使用np方法,代码如下:
def sigmoid(x):
"""
在此处计算输入的Sigmoid函数。
参数:
x -- 一个标量或NumPy数组。
返回:
s -- sigmoid(x) 的值
"""
### YOUR CODE HERE (~1 Line)
s = 1 / (1 + np.exp(-x))
### END YOUR CODE
return s
在naiveSoftmaxLossAndGradient方法中实现softmax损失和梯度
我们可以回忆一下朴素Softmax损失与梯度函数怎么定义的:
给定中心词c与外部词o,我们定义与为对应词向量,N为外部单词数量,因此定义损失函数J为以下式子:
其中,我们定义为模型两个向量经过点积后经过softmax函数后的概率分布。
对于梯度而言,
对于中心词向量 的梯度,使用链式法则,我们首先对Softmax输出求导:
其中,⊙ 表示Hadamard积(元素乘积),1 是一个全1向量,U 是外部词向量矩阵。然后,将这个导数乘以中心词向量 来得到梯度:
因为我们可以定义y为独热编码,因此表达式简化为:
这里的 是正确的外部词向量。
对于外部词向量的梯度,我们同样使用链式法则:
然后,对这个导数求和,得到外部词向量矩阵 U 的梯度:
对于朴素Softmax,这个求和实际上只影响正确的外部词向量
代码如下:
def naiveSoftmaxLossAndGradient(
centerWordVec,
outsideWordIdx,
outsideVectors,
dataset
):
"""朴素Softmax损失与梯度函数,用于word2vec模型
实现中心词嵌入与外部词嵌入之间的朴素Softmax损失及其梯度计算。这将是构建我们的word2vec模型的基础模块。
对于不熟悉numpy表示法的读者,请注意,具有形状(x,)的numpy数组是一维数组,你可以将其视为长度为x的向量。
参数:
centerWordVec -- numpy数组,中心词的嵌入,形状为(词向量长度, )(pdf手册中的v_c)
outsideWordIdx -- 整型,外部词的索引(pdf手册中u_o的o)
outsideVectors -- 所有词汇表中词的外部向量,形状为(词汇表中词的数量, 词向量长度) (pdf手册中U的转置)
dataset -- 用于负采样,但在本函数中未使用
返回:
loss -- 朴素Softmax损失
gradCenterVec -- 关于中心词向量的梯度,形状为(词向量长度, )(pdf手册中的dJ/dv_c)
gradOutsideVecs -- 关于所有外部词向量的梯度,形状为(词汇表中词的数量, 词向量长度)(pdf手册中的dJ/dU)
"""
### YOUR CODE HERE (~6-8 Lines)
### 请使用本文件前面导入的softmax函数
### 这个数值稳定的实现帮助你避免与整数溢出相关的问题。
y_hat = softmax(np.dot(outsideVectors, centerWordVec))
y = np.zeros(outsideVectors.shape[0])
y[outsideWordIdx] = 1
loss = -np.log(y_hat[outsideWordIdx])
gradCenterVec = np.dot(y_hat - y, outsideVectors, )
gradOutsideVecs = np.outer((y_hat - y), centerWordVec)
### END YOUR CODE
return loss, gradCenterVec, gradOutsideVecs
在negSamplingLossAndGradient方法中实现负采样损失和梯度
与上题相同,这题同样要求返回损失和梯度,但这题采用了负采样措施。
我们同样回忆一下负采样的定义,我们为了避免模型被错误句子的错误中心词影响,因此在外部词多次抽样,定义损失函数为:
这样定义损失函数,我们既最小化正样本的影响,也最大化了负样本,使得错误句子不被错误信息影响。
梯度同理,唯一不同就是一个外部词可能被多次抽样,因此要考虑多次抽样情况下的梯度累加。
代码如下:
def negSamplingLossAndGradient(
centerWordVec,
outsideWordIdx,
outsideVectors,
dataset,
K=10
):
""" 负采样损失函数,用于word2vec模型
实现中心词向量与外部词索引的负采样损失及梯度计算,作为word2vec模型的构建模块。K是负样本的数量。
注意:同一个词可能被多次负采样。例如,如果一个外部词被采样两次,你必须将该词的梯度加倍。如果采样三次,则需三倍计算,以此类推。
参数/返回规格:与naiveSoftmaxLossAndGradient相同
"""
# Negative sampling of words is done for you. Do not modify this if you
# wish to match the autograder and receive points!
negSampleWordIndices = getNegativeSamples(outsideWordIdx, dataset, K)
indices = [outsideWordIdx] + negSampleWordIndices
### YOUR CODE HERE (~10 Lines)
### Please use your implementation of sigmoid in here.
uo = outsideVectors[outsideWordIdx]
uk = outsideVectors[negSampleWordIndices]
y_hat = np.dot(outsideVectors, centerWordVec)
y_hat_k = y_hat[negSampleWordIndices]
y_hat_o = y_hat[outsideWordIdx]
loss = np.sum(-np.log(sigmoid(-y_hat_k))) - np.log(sigmoid(y_hat_o))
gradCenterVec = np.dot(sigmoid(y_hat_k), uk)
gradCenterVec += -sigmoid(-y_hat_o) * uo
gradOutsideVecs = np.zeros_like(outsideVectors)
tmp = np.zeros_like(outsideVectors)
z = sigmoid(np.dot(uk, centerWordVec))
tmp[negSampleWordIndices] += np.outer(z, centerWordVec)
for idx in negSampleWordIndices:
gradOutsideVecs[idx] += tmp[idx]
# +=, accumulate repeated words
gradOutsideVecs[outsideWordIdx] = -sigmoid(-y_hat[outsideWordIdx]) * centerWordVec
### END YOUR CODE
return loss, gradCenterVec, gradOutsideVecs
实现skip-gram模型
在这个函数参数中,已经提供了word2vecLossAndGradient这一预测损失和梯度函数,因此我们只需处理向量和外部词即可,代码如下:
def skipgram(currentCenterWord, windowSize, outsideWords, word2Ind,
centerWordVectors, outsideVectors, dataset,
word2vecLossAndGradient=naiveSoftmaxLossAndGradient):
""" word2vec模型中的skip-gram算法
在此函数中实现skip-gram算法。
参数:
currentCenterWord -- 当前中心词的字符串形式
windowSize -- 整数,上下文窗口大小
outsideWords -- 不超过2*windowSize的列表,包含上下文中的外部词
word2Ind -- 字典,将词映射到它们在词向量列表中的索引
centerWordVectors -- 中心词向量(作为行)的形状为(num words in vocab, word vector length),包括词汇表中所有词的向量(V在pdf手册中)
outsideVectors -- 外部向量的形状为(num words in vocab, word vector length),包括词汇表中所有词的向量(U的转置在pdf手册中)
word2vecLossAndGradient -- 预测向量给定外部词索引词向量的损失和梯度函数,可以是上面实现的两种损失函数之一。
返回:
loss -- skip-gram模型的损失函数值(J在pdf手册中)
gradCenterVec -- 关于中心词向量的梯度,形状为(word vector length, )
gradOutsideVecs -- 关于所有外部词向量的梯度,形状为(num words in vocab, word vector length)
"""
loss = 0.0
gradCenterVecs = np.zeros(centerWordVectors.shape)
gradOutsideVectors = np.zeros(outsideVectors.shape)
### YOUR CODE HERE (~8 Lines)
c = word2Ind[currentCenterWord]
vc = centerWordVectors[c]
for word in outsideWords:
o = word2Ind[word]
loss_, gradc, grado = word2vecLossAndGradient(
vc,
o,
outsideVectors,
dataset
)
loss += loss_
gradCenterVecs[c] += gradc
gradOutsideVectors += grado
### END YOUR CODE
return loss, gradCenterVecs, gradOutsideVectors
SGD .py
我们可以分析sgd方法如下:
首先定义ANNEAL_EVERY,其是学习率衰减的周期,即每进行固定次数的迭代后,步长会减少到原来的一半。
然后判断useSaved,如果为True,则尝试从先前保存的状态加载迭代次数、参数和随机状态。
如果否,则x初始化为x0,exploss用于存储指数加权平均损失。
如果没有提供postprocessing函数,则使用一个默认的函数,该函数不对参数做任何改变
接着使用for循环进行迭代,主要操作便在该循环内完成,包括计算当前参数 x
的损失和梯度,使用梯度和步长更新参数x,并把x应用到postprocessing函数上。
因为sgd方法中已经提供了f()参数给我们优化函数,返回损失和梯度,因此最终代码如下:
def sgd(f, x0, step, iterations, postprocessing=None, useSaved=False,
PRINT_EVERY=10):
""" 随机梯度下降
在这个函数中实现随机梯度下降方法。
参数:
f -- 需要优化的函数,它应该只接受一个参数并返回两个输出,
即损失和相对于参数的梯度
x0 -- 开始进行SGD的初始点
step -- SGD的步长
iterations -- 运行SGD的总迭代次数
postprocessing -- 如果必要的话,参数的后处理函数。在word2vec的情况下,
我们需要将词向量归一化到单位长度。
PRINT_EVERY -- 指定多少次迭代输出一次损失
返回:
x -- SGD完成后参数的值
"""
# Anneal learning rate every several iterations
ANNEAL_EVERY = 20000
if useSaved:
start_iter, oldx, state = load_saved_params()
if start_iter > 0:
x0 = oldx
step *= 0.5 ** (start_iter / ANNEAL_EVERY)
if state:
random.setstate(state)
else:
start_iter = 0
x = x0
if not postprocessing:
postprocessing = lambda x: x
exploss = None
for iter in range(start_iter + 1, iterations + 1):
# You might want to print the progress every few iterations.
loss = None
### YOUR CODE HERE (~2 lines)
loss, gradient = f(x)
x += - step * gradient
### END YOUR CODE
x = postprocessing(x)
if iter % PRINT_EVERY == 0:
if not exploss:
exploss = loss
else:
exploss = .95 * exploss + .05 * loss
print("iter %d: %f" % (iter, exploss))
if iter % SAVE_PARAMS_EVERY == 0 and useSaved:
save_params(iter, x)
if iter % ANNEAL_EVERY == 0:
step *= 0.5
return x