cs224n作业_Assignment 2: word2vec Part 2(Coding)

本次作业与代码见上篇翻译

word2vec.py

实现sigmoid方法

我们可以注意到,sigmoid函数形似\sigma = \frac{1}{1+e^x}

因此对于参数: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,我们定义v_cu_o为对应词向量,N为外部单词数量,因此定义损失函数J为以下式子:

J=-log(\frac{e^{v_c*u_o}}{\sum_N e^{v_c*u_j}})

其中v_c*u_o = \hat{y}\hat{y}我们定义为模型两个向量经过点积后经过softmax函数后的概率分布。

对于梯度而言,

对于中心词向量 ​v_c的梯度,使用链式法则,我们首先对Softmax输出求导:

\frac{\delta y_{hat,o}}{\delta v_c}=(y_{hat}-1)\odot U

其中,⊙ 表示Hadamard积(元素乘积),1 是一个全1向量,U 是外部词向量矩阵。然后,将这个导数乘以中心词向量v_c 来得到梯度:

\bigtriangledown _{v_c}J=\frac{\delta J}{\delta v_c}=-\frac{1}{y_{hat,o}}(y_{hat}-1) \odot U*v_c

因为我们可以定义y为独热编码,因此表达式简化为:

\bigtriangledown _{v_c}J=(y_{hat}-y)*u_o

这里的 u_o是正确的外部词向量。

对于外部词向量u_j的梯度,我们同样使用链式法则:

\frac{\delta y_{hat,o}}{\delta u_j}=(y_{hat}-1)\odot v_c

然后,对这个导数求和,得到外部词向量矩阵 U 的梯度:

\bigtriangledown _{U}J=\sum^N_{j=1} (y_{hat}-1)\odot v_c

对于朴素Softmax,这个求和实际上只影响正确的外部词向量 u_o

代码如下:

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方法中实现负采样损失和梯度

与上题相同,这题同样要求返回损失和梯度,但这题采用了负采样措施。

我们同样回忆一下负采样的定义,我们为了避免模型被错误句子的错误中心词影响,因此在外部词多次抽样,定义损失函数为:

J=-log\sigma (u_o*v_c)-\sum_{k\in K} log\sigma (-u_k*v_c)

这样定义损失函数,我们既最小化正样本的影响,也最大化了负样本,使得错误句子不被错误信息影响。

梯度同理,唯一不同就是一个外部词可能被多次抽样,因此要考虑多次抽样情况下的梯度累加。

代码如下:

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这一预测损失和梯度函数,因此我们只需处理v_c向量和外部词即可,代码如下:

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

  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值