PyTorch系列之优化算法进阶|word2vec|词嵌入进阶

优化算法进阶|word2vec|词嵌入进阶

Posted by Young on February 18, 2020

Task07:优化算法进阶|word2vec|词嵌入进阶

优化算法进阶

Momentum

  • 目标函数有关自变量的梯度代表了目标函数在自变量当前位置下降最快的方向。因此,梯度下降也叫作最陡下降(steepest descent)。在每次迭代中,梯度下降根据自变量当前位置,沿着当前位置的梯度更新自变量。然而,如果自变量的迭代方向仅仅取决于自变量当前位置,这可能会带来一些问题。对于noisy gradient,我们需要谨慎的选取学习率和batch size, 来控制梯度方差和收敛的结果。

  • An ill-conditioned Problem

    • Condition Number of Hessian Matrix:

      \[cond_{H} = \frac{\lambda_{max}}{\lambda_{min}}\]

      where $\lambda_{max}, \lambda_{min}$ is the maximum amd minimum eignvalue of Hessian matrix.

      让我们考虑一个输入和输出分别为二维向量$x = [x_1, x_2]^\top$和标量的目标函数:

      \[f(x)=0.1x_1^2+2x_2^2\] \[cond_{H} = \frac{4}{0.2} = 20 \quad \rightarrow \quad \text{ill-conditioned}\]

      • This function is very flat in the direction of $𝑥_1$
      • By construction, the gradient in the $x_2$ direction is much higher and changes much more rapidly than in the horizontal $x_1$ direction. Thus we are stuck between two undesirable choices: if we pick a small learning rate we ensure that the solution does not diverge in the $x_2$ direction but we’re saddled with slow convergence in the $x_1$ direction. Conversely, with a large learning rate we progress rapidly in the $x_1$ direction but diverge in $x_2$.
    learning_rate show_trace_2d
    0.01
    0.1
    0.2
    0.3
    0.5
    0.6

The Momentum Method

动量法的提出是为了解决梯度下降的上述问题。设时间步 $t$ 的自变量为 $\x_t$,学习率为 $\eta_t$。 在时间步 $t=0$,动量法创建速度变量 $m_0$,并将其元素初始化成 0。在时间步 $t>0$,动量法对每次迭代的步骤做如下修改:

\[\begin{aligned} m_t &\leftarrow \beta m_{t-1} + \eta_t g_t, \\ x_t &\leftarrow x_{t-1} - m_t, \end{aligned}\]

Another version:

\[\begin{aligned} m_t &\leftarrow \beta m_{t-1} + (1-\beta) g_t, \\ x_t &\leftarrow x_{t-1} - \alpha_t m_t, \end{aligned}\] \[\alpha_t = \frac{\eta_t}{1-\beta}\]

其中,动量超参数 $\beta$满足 $0 \leq \beta < 1$。当 $\beta=0$ 时,动量法等价于小批量随机梯度下降。

def momentum_2d(x1, x2, v1, v2):
		"""
		f(x)=0.1x_1^2+2x_2^2
		"""
    v1 = beta * v1 + eta * 0.2 * x1
    v2 = beta * v2 + eta * 4 * x2
    return x1 - v1, x2 - v2, v1, v2

eta, beta = 0.4, 0.5
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))

  • 可以看到使用较小的学习率 $\eta=0.4$ 和动量超参数 $\beta=0.5$ 时,动量法在竖直方向上的移动更加平滑,且在水平方向上更快逼近最优解。下面使用较大的学习率 $\eta=0.6$,此时自变量也不再发散。
  • 换句话说,相比于小批量随机梯度下降,动量法在每个时间步的自变量更新量近似于将前者对应的最近 $1/(1-\beta)$ 个时间步的更新量做了指数加权移动平均后再除以 $1-\beta$。所以,在动量法中,自变量在各个方向上的移动幅度不仅取决当前梯度,还取决于过去的各个梯度在各个方向上是否一致。在本节之前示例的优化问题中,所有梯度在水平方向上为正(向右),而在竖直方向上时正(向上)时负(向下)。这样,我们就可以使用较大的学习率,从而使自变量向最优解更快移动。

  • 在Pytorch中,torch.optim.SGD已实现了Momentum

    def init_momentum_states():
        v_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
        v_b = torch.zeros(1, dtype=torch.float32)
        return (v_w, v_b)
      
    def sgd_momentum(params, states, hyperparams):
      for p, v in zip(params, states):
            v.data = hyperparams['momentum'] * v.data + hyperparams['lr'] * p.grad.data
            p.data -= v.data
    

Adagrad

在之前介绍过的优化算法中,目标函数自变量的每一个元素在相同时间步都使用同一个学习率来自我迭代。举个例子,假设目标函数为$f$,自变量为一个二维向量$[x_1, x_2]^\top$,该向量中每一个元素在迭代时都使用相同的学习率。例如,在学习率为$\eta$的梯度下降中,元素$x_1$和$x_2$都使用相同的学习率$\eta$来自我迭代:

\[x_1 \leftarrow x_1 - \eta \frac{\partial{f}}{\partial{x_1}}, \quad x_2 \leftarrow x_2 - \eta \frac{\partial{f}}{\partial{x_2}}.\]
  • 在“动量法”一节里我们看到当$x_1$和$x_2$的梯度值有较大差别时,需要选择足够小的学习率使得自变量在梯度值较大的维度上不发散但这样会导致自变量在梯度值较小的维度上迭代过慢。动量法依赖指数加权移动平均使得自变量的更新方向更加一致,从而降低发散的可能。本节我们介绍AdaGrad算法,它根据自变量在每个维度的梯度值的大小来调整各个维度上的学习率,从而避免统一的学习率难以适应所有维度的问题

  • AdaGrad算法会使用一个小批量随机梯度$g_t$按元素平方的累加变量$s_t$。在时间步0,AdaGrad将$s_0$中每个元素初始化为0。在时间步$t$,首先将小批量随机梯度$g_t$按元素平方后累加到变量$s_t$:

    \[s_t \leftarrow s_{t-1} + g_t \odot g_t,\]

    其中$\odot$是按元素相乘。接着,我们将目标函数自变量中每个元素的学习率通过按元素运算重新调整一下:

    \[x_t \leftarrow x_{t-1} - \frac{\eta}{\sqrt{s_t + \epsilon}} \odot g_t,\]

    其中$\eta$是学习率,$\epsilon$是为了维持数值稳定性而添加的常数,如$10^{-6}$。这里开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。

  • 需要强调的是,小批量随机梯度按元素平方的累加变量$s_t$出现在学习率的分母项中。因此,如果目标函数有关自变量中某个元素的偏导数一直都较大,那么该元素的学习率将下降较快;反之,如果目标函数有关自变量中某个元素的偏导数一直都较小,那么该元素的学习率将下降较慢。然而,由于$s_t$一直在累加按元素平方的梯度,自变量中每个元素的学习率在迭代过程中一直在降低(或不变)。所以,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。

  • 
    ```python
    def init_adagrad_states():
        s_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
        s_b = torch.zeros(1, dtype=torch.float32)
        return (s_w, s_b)
      
    def adagrad(params, states, hyperparams):
        eps = 1e-6
        for p, s in zip(params, states):
            s.data += (p.grad.data**2)
            p.data -= hyperparams['lr'] * p.grad.data / torch.sqrt(s + eps)
    

RMSProp

我们在“动量法”一节里介绍过指数加权移动平均。不同于AdaGrad算法里状态变量$s_t$是截至时间步$t$所有小批量随机梯度$g_t$按元素平方和,RMSProp算法将这些梯度按元素平方做指数加权移动平均

  • 具体来说,给定超参数$0 \leq \gamma 0$计算

    \[v_t \leftarrow \beta v_{t-1} + (1 - \beta) g_t \odot g_t.\]

    和AdaGrad算法一样,RMSProp算法将目标函数自变量中每个元素的学习率通过按元素运算重新调整,然后更新自变量

\[x_t \leftarrow x_{t-1} - \frac{\eta}{\sqrt{v_t + \epsilon}} \odot g_t,\]

其中$\eta$是学习率,$\epsilon$是为了维持数值稳定性而添加的常数,如$10^{-6}$。因为RMSProp算法的状态变量$s_t$是对平方项$g_t \odot g_t$的指数加权移动平均,所以可以看作是最近$1/(1-\beta)$个时间步的小批量随机梯度平方项的加权平均。如此一来,自变量每个元素的学习率在迭代过程中就不再一直降低(或不变)

  • torch.optim.RMSprop

    def init_rmsprop_states():
        s_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
        s_b = torch.zeros(1, dtype=torch.float32)
        return (s_w, s_b)
      
    def rmsprop(params, states, hyperparams):
        gamma, eps = hyperparams['beta'], 1e-6
        for p, s in zip(params, states):
            s.data = gamma * s.data + (1 - gamma) * (p.grad.data)**2
            p.data -= hyperparams['lr'] * p.grad.data / torch.sqrt(s + eps)
    

AdaDelta

除了RMSProp算法以外,另一个常用优化算法AdaDelta算法也针对AdaGrad算法在迭代后期可能较难找到有用解的问题做了改进有意思的是,AdaDelta算法没有学习率这一超参数

  • AdaDelta算法也像RMSProp算法一样,使用了小批量随机梯度$g_t$按元素平方的指数加权移动平均变量$s_t$。在时间步0,它的所有元素被初始化为0。给定超参数$0 \leq \rho 0$,同RMSProp算法一样计算
\[s_t \leftarrow \rho s_{t-1} + (1 - \rho) g_t \odot g_t.\]

与RMSProp算法不同的是,AdaDelta算法还维护一个额外的状态变量$\Delta x_t$,其元素同样在时间步0时被初始化为0。我们使用$\Delta x_{t-1}$来计算自变量的变化量:

\[g_t' \leftarrow \sqrt{\frac{\Delta x_{t-1} + \epsilon}{s_t + \epsilon}} \odot g_t,\]

其中$\epsilon$是为了维持数值稳定性而添加的常数,如$10^{-5}$。接着更新自变量:

\[x_t \leftarrow x_{t-1} - g'_t.\]

最后,我们使用$\Delta x_t$来记录自变量变化量$g’_t$按元素平方的指数加权移动平均:

\[\Delta x_t \leftarrow \rho \Delta x_{t-1} + (1 - \rho) g'_t \odot g'_t.\]

可以看到,如不考虑$\epsilon$的影响,AdaDelta算法与RMSProp算法的不同之处在于使用$\sqrt{\Delta\boldsymbol{x}_{t-1}}$来替代超参数$\eta$

  • torch.optim.Adadelta

    def init_adadelta_states():
        s_w, s_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
        delta_w, delta_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
        return ((s_w, delta_w), (s_b, delta_b))
      
    def adadelta(params, states, hyperparams):
        rho, eps = hyperparams['rho'], 1e-5
        for p, (s, delta) in zip(params, states):
            s[:] = rho * s + (1 - rho) * (p.grad.data**2)
            g =  p.grad.data * torch.sqrt((delta + eps) / (s + eps))
            p.data -= g
            delta[:] = rho * delta + (1 - rho) * g * g
    

Adam

Adam算法在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均。

  • Adam算法使用了动量变量$\boldsymbol{m}_t$和RMSProp算法中小批量随机梯度按元素平方的指数加权移动平均变量$\boldsymbol{v}_t$,并在时间步0将它们中每个元素初始化为0。给定超参数$0 \leq \beta_1 < 1$(算法作者建议设为0.9),时间步$t$的动量变量$\boldsymbol{m}_t$即小批量随机梯度$\boldsymbol{g}_t$的指数加权移动平均:
\[\boldsymbol{m}_t \leftarrow \beta_1 \boldsymbol{m}_{t-1} + (1 - \beta_1) \boldsymbol{g}_t.\]

和RMSProp算法中一样,给定超参数$0 \leq \beta_2 < 1$(算法作者建议设为0.999), 将小批量随机梯度按元素平方后的项$\boldsymbol{g}_t \odot \boldsymbol{g}_t$做指数加权移动平均得到$\boldsymbol{v}_t$:

\[\boldsymbol{v}_t \leftarrow \beta_2 \boldsymbol{v}_{t-1} + (1 - \beta_2) \boldsymbol{g}_t \odot \boldsymbol{g}_t.\]

由于我们将$\boldsymbol{m}0$和$\boldsymbol{s}_0$中的元素都初始化为0, 在时间步$t$我们得到$\boldsymbol{m}_t = (1-\beta_1) \sum{i=1}^t \beta_1^{t-i} \boldsymbol{g}i$。将过去各时间步小批量随机梯度的权值相加,得到 $(1-\beta_1) \sum{i=1}^t \beta_1^{t-i} = 1 - \beta_1^t$。需要注意的是,当$t$较小时,过去各时间步小批量随机梯度权值之和会较小。例如,当$\beta_1 = 0.9$时,$\boldsymbol{m}_1 = 0.1\boldsymbol{g}_1$。为了消除这样的影响,对于任意时间步$t$,我们可以将$\boldsymbol{m}_t$再除以$1 - \beta_1^t$,从而使过去各时间步小批量随机梯度权值之和为1。这也叫作偏差修正。在Adam算法中,我们对变量$\boldsymbol{m}_t$和$\boldsymbol{v}_t$均作偏差修正:

\[\hat{\boldsymbol{m}}_t \leftarrow \frac{\boldsymbol{m}_t}{1 - \beta_1^t},\] \[\hat{\boldsymbol{v}}_t \leftarrow \frac{\boldsymbol{v}_t}{1 - \beta_2^t}.\]

接下来,Adam算法使用以上偏差修正后的变量$\hat{\boldsymbol{m}}_t$和$\hat{\boldsymbol{m}}_t$,将模型参数中每个元素的学习率通过按元素运算重新调整:

\[\boldsymbol{g}_t' \leftarrow \frac{\eta \hat{\boldsymbol{m}}_t}{\sqrt{\hat{\boldsymbol{v}}_t} + \epsilon},\]

其中$\eta$是学习率,$\epsilon$是为了维持数值稳定性而添加的常数,如$10^{-8}$。和AdaGrad算法、RMSProp算法以及AdaDelta算法一样,目标函数自变量中每个元素都分别拥有自己的学习率。最后,使用$\boldsymbol{g}_t’$迭代自变量:

\[\boldsymbol{x}_t \leftarrow \boldsymbol{x}_{t-1} - \boldsymbol{g}_t'.\]
  • torch.optim.Adam

    def init_adam_states():
        v_w, v_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
        s_w, s_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
        return ((v_w, s_w), (v_b, s_b))
      
    def adam(params, states, hyperparams):
        beta1, beta2, eps = 0.9, 0.999, 1e-6
        for p, (v, s) in zip(params, states):
            v[:] = beta1 * v + (1 - beta1) * p.grad.data
            s[:] = beta2 * s + (1 - beta2) * p.grad.data**2
            v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
            s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
            p.data -= hyperparams['lr'] * v_bias_corr / (torch.sqrt(s_bias_corr) + eps)
        hyperparams['t'] += 1
    

word2vec

我们在“循环神经网络的从零开始实现”一节中使用 one-hot 向量表示单词,虽然它们构造起来很容易,但通常并不是一个好选择。一个主要的原因是,one-hot 词向量无法准确表达不同词之间的相似度,如我们常常使用的余弦相似度。

  • Word2Vec 词嵌入工具的提出正是为了解决上面这个问题,它将每个词表示成一个定长的向量,并通过在语料库上的预训练使得这些向量能较好地表达不同词之间的相似和类比关系,以引入一定的语义信息。基于两种概率模型的假设,我们可以定义两种 Word2Vec 模型:

    • Skip-Gram 跳字模型:假设背景词由中心词生成,即建模 $P(w_o\mid w_c)$,其中 $w_c$ 为中心词,$w_o$ 为任一背景词;

      • The skip-gram model assumes that a word can be used to generate the words that surround it in a text sequence. For example, we assume that the text sequence is “the”, “man”, “loves”, “his”, and “son”. We use “loves” as the central target word and set the context window size to 2. As shown in , given the central target word “loves”, the skip-gram model is concerned with the conditional probability for generating the context words.
    • CBOW (continuous bag-of-words) 连续词袋模型:假设背景词由中心词生成,即建模 $P(w_o\mid w_c)$,其中 $w_c$ 为中心词,$w_o$ 为任一背景词;

  • 数据集

    • PTB 数据集PTB (Penn Tree Bank) 是一个常用的小型语料库,它采样自《华尔街日报》的文章,包括训练集、验证集和测试集。我们将在PTB训练集上训练词嵌入模型。
  • 二次采样

    • 文本数据中一般会出现一些高频词,如英文中的“the”“a”和“in”。通常来说,在一个背景窗口中,一个词(如“chip”)和较低频词(如“microprocessor”)同时出现比和较高频词(如“the”)同时出现对训练词嵌入模型更有益。因此,训练词嵌入模型时可以对词进行二次采样。 具体来说,数据集中每个被索引词 $w_i$ 将有一定概率被丢弃,该丢弃概率为 $P(w_i)=\max(1-\sqrt{\frac{t}{f(w_i)}},0)$ 。其中 $f(w_i)$ 是数据集中词 $w_i$ 的个数与总词数之比,常数 $t$ 是一个超参数(实验中设为 $10^{−4}$)。可见,只有当 $f(w_i)>t$ 时,我们才有可能在二次采样中丢弃词 $w_i$,并且越高频的词被丢弃的概率越大。

      def discard(idx):
          '''
          @params:
              idx: 单词的下标
          @return: True/False 表示是否丢弃该单词
          '''
          return random.uniform(0, 1) < 1 - math.sqrt(
              1e-4 / counter[idx_to_token[idx]] * num_tokens)
      
  • 提取中心词和背景词

    def get_centers_and_contexts(dataset, max_window_size):
        '''
        @params:
            dataset: 数据集为句子的集合,每个句子则为单词的集合,此时单词已经被转换为相应数字下标
            max_window_size: 背景词的词窗大小的最大值
        @return:
            centers: 中心词的集合
            contexts: 背景词窗的集合,与中心词对应,每个背景词窗则为背景词的集合
        '''
        centers, contexts = [], []
        for st in dataset:
            if len(st) < 2:  # 每个句子至少要有2个词才可能组成一对“中心词-背景词”
                continue
            centers += st
            for center_i in range(len(st)):
                window_size = random.randint(1, max_window_size) # 随机选取背景词窗大小
                indices = list(range(max(0, center_i - window_size),
                                     min(len(st), center_i + 1 + window_size)))
                indices.remove(center_i)  # 将中心词排除在背景词之外
                contexts.append([st[idx] for idx in indices])
        return centers, contexts
          
          
    >>> all_centers, all_contexts = get_centers_and_contexts(subsampled_dataset, 5)
        tiny_dataset = [list(range(7)), list(range(7, 10))]
        print('dataset', tiny_dataset)
        for center, context in zip(*get_centers_and_contexts(tiny_dataset, 2)):
            print('center', center, 'has contexts', context)
              
    >>> dataset [[0, 1, 2, 3, 4, 5, 6], [7, 8, 9]]
        center 0 has contexts [1, 2]
        center 1 has contexts [0, 2, 3]
        center 2 has contexts [0, 1, 3, 4]
        center 3 has contexts [2, 4]
        center 4 has contexts [3, 5]
        center 5 has contexts [4, 6]
        center 6 has contexts [5]
        center 7 has contexts [8]
        center 8 has contexts [7, 9]
        center 9 has contexts [7, 8]
    

Skip-Gram 跳字模型

  • 在跳字模型中,每个词被表示成两个 $d$ 维向量,用来计算条件概率。假设这个词在词典中索引为 $i$ ,当它为中心词时向量表示为 $\boldsymbol{v}_i\in\mathbb{R}^d$,而为背景词时向量表示为 $\boldsymbol{u}_i\in\mathbb{R}^d$ 。设中心词 $w_c$ 在词典中索引为 $c$,背景词 $w_o$ 在词典中索引为 $o$,我们假设给定中心词生成背景词的条件概率满足下式:

    \[P(w_o\mid w_c)=\frac{\exp(\boldsymbol{u}_o^\top \boldsymbol{v}_c)}{\sum_{i\in\mathcal{V}}\exp(\boldsymbol{u}_i^\top \boldsymbol{v}_c)}\]
  • 负采样近似

    • 由于 softmax 运算考虑了背景词可能是词典 $\mathcal{V}$ 中的任一词,对于含几十万或上百万词的较大词典,就可能导致计算的开销过大。我们将以 skip-gram 模型为例,介绍负采样 (negative sampling) 的实现来尝试解决这个问题。

      负采样方法用以下公式来近似条件概率 $P(w_o\mid w_c)=\frac{\exp(\boldsymbol{u}o^\top \boldsymbol{v}_c)}{\sum{i\in\mathcal{V}}\exp(\boldsymbol{u}_i^\top \boldsymbol{v}_c)}$:

      \[P(w_o\mid w_c)=P(D=1\mid w_c,w_o)\prod_{k=1,w_k\sim P(w)}^K P(D=0\mid w_c,w_k)\]

      其中 $P(D=1\mid w_c,w_o)=\sigma(\boldsymbol{u}_o^\top\boldsymbol{v}_c)$,$\sigma(\cdot)$ 为 sigmoid 函数。对于一对中心词和背景词,我们从词典中随机采样 $K$ 个噪声词(实验中设 $K=5$)。根据 Word2Vec 论文的建议,噪声词采样概率 $P(w)$ 设为 $w$ 词频与总词频之比的 $0.75$ 次方。

词嵌入进阶

GloVe

虽然 Word2Vec 已经能够成功地将离散的单词转换为连续的词向量,并能一定程度上地保存词与词之间的近似关系,但 Word2Vec 模型仍不是完美的,它还可以被进一步地改进:

  1. 子词嵌入(subword embedding):FastText 以固定大小的 n-gram 形式将单词更细致地表示为了子词的集合,而 BPE (byte pair encoding) 算法则能根据语料库的统计信息,自动且动态地生成高频子词的集合;
  2. GloVe 全局向量的词嵌入: 通过等价转换 Word2Vec 模型的条件概率公式,我们可以得到一个全局的损失函数表达,并在此基础上进一步优化模型。

实际中,我们常常在大规模的语料上训练这些词嵌入模型,并将预训练得到的词向量应用到下游的自然语言处理任务中。本节就将以 GloVe 模型为例,演示如何用预训练好的词向量来求近义词和类比词。

  • 先简单回顾以下 Word2Vec 的损失函数(以 Skip-Gram 模型为例,不考虑负采样近似):

    \[-\sum_{t=1}^T\sum_{-m\le j\le m,j\ne 0} \log P(w^{(t+j)}\mid w^{(t)})\]

    其中

    \[P(w_j\mid w_i) = \frac{\exp(\boldsymbol{u}_j^\top\boldsymbol{v}_i)}{\sum_{k\in\mathcal{V}}\exp(\boldsymbol{u}_k^\top\boldsymbol{v}_i)}\]

    是 $w_i$ 为中心词,$w_j$ 为背景词时 Skip-Gram 模型所假设的条件概率计算公式,我们将其简写为 $q_{ij}$。

    注意到此时我们的损失函数中包含两个求和符号,它们分别枚举了语料库中的每个中心词和其对应的每个背景词。实际上我们还可以采用另一种计数方式,那就是直接枚举每个词分别作为中心词和背景词的情况:

    \[-\sum_{i\in\mathcal{V}}\sum_{j\in\mathcal{V}} x_{ij}\log q_{ij}\]

    其中 $x_{ij}$ 表示整个数据集中 $w_j$ 作为 $w_i$ 的背景词的次数总和。

    我们还可以将该式进一步地改写为交叉熵 (cross-entropy) 的形式如下:

    \[-\sum_{i\in\mathcal{V}}x_i\sum_{j\in\mathcal{V}}p_{ij} \log q_{ij}\]

    其中 $x_i$ 是 $w_i$ 的背景词窗大小总和,$p_{ij}=x_{ij}/x_i$ 是 $w_j$ 在 $w_i$ 的背景词窗中所占的比例。

    从这里可以看出,我们的词嵌入方法实际上就是想让模型学出 $w_j$ 有多大概率是 $w_i$ 的背景词,而真实的标签则是语料库上的统计数据。同时,语料库中的每个词根据 $x_i$ 的不同,在损失函数中所占的比重也不同。

    注意到目前为止,我们只是改写了 Skip-Gram 模型损失函数的表面形式,还没有对模型做任何实质上的改动。而在 Word2Vec 之后提出的 GloVe 模型,则是在之前的基础上做出了以下几点改动

    1. 使用非概率分布的变量 $p’{ij}=x{ij}$ 和 $q′_{ij}=\exp(\boldsymbol{u}^\top_j\boldsymbol{v}_i)$,并对它们取对数;
    2. 为每个词 $w_i$ 增加两个标量模型参数:中心词偏差项 $b_i$ 和背景词偏差项 $c_i$,松弛了概率定义中的规范性
    3. 将每个损失项的权重 $x_i$ 替换成函数 $h(x_{ij})$,权重函数 $h(x)$ 是值域在 $[0,1]$ 上的单调递增函数,松弛了中心词重要性与 $x_i$ 线性相关的隐含假设
    4. 平方损失函数替代了交叉熵损失函数。

    综上,我们获得了 GloVe 模型的损失函数表达式:

    \[\sum_{i\in\mathcal{V}}\sum_{j\in\mathcal{V}} h(x_{ij}) (\boldsymbol{u}^\top_j\boldsymbol{v}_i+b_i+c_j-\log x_{ij})^2\]

    由于这些非零 $x_{ij}$ 是预先基于整个数据集计算得到的,包含了数据集的全局统计信息,因此 GloVe 模型的命名取“全局向量”(Global Vectors)之意。

题目

  • 对于 Skip-Gram, CBOW, GloVe 等词嵌入方法的理解
    • 词嵌入模型的训练本质上是在优化模型预测各词语同时出现的概率
    • 词嵌入模型的设计和训练语料库的选取都很重要
      • 抽象地说,词嵌入方法都是通过在大规模的语料库上进行训练,来让模型更好地“理解”词义,而好的模型设计则能提高训练的效率及模型的上界
    • ❌ 不管是什么任务,在使用他人已经训练好的词向量时,直接复制词向量的权重就行了,不需要再进行任何额外的操作
      • 由于他人训练词向量时用到的语料库和当前任务上的语料库通常都不相同,所以词典中包含的词语以及词语的顺序都可能有很大差别,此时应当根据当前数据集上词典的顺序,来依次读入词向量,同时,为了避免训练好的词向量在训练的最初被破坏,还可以适当调整嵌入层的学习速率甚至设定其不参与梯度下降
    • GloVe 模型用到了语料库上全局的统计信息,而 Skip-Gram 和 CBOW 模型则只用到了局部的统计信息
  • 关于 GloVe 方法基于 Skip-Gram 的改动
    • GloVe 使用了非概率分布的变量,并添加了中心词和背景词的偏差项,这样做是在松弛概率的规范性,即各个概率事件的概率和加起来等于1
    • GloVe 使用了一个单调递增的权重函数来加权各个损失项
    • GloVe 的损失函数计算公式中用到了语料库上的全局统计信息
  • 关于利用词向量求近义词和类比词
    • 我们可以直接使用他人预训练好的词向量,而不必从头开始训练
      • 由于我们的计算资源和时间都很有限,所以我们通常都会加载他人预训练好的词向量,而非在大规模语料库上从头开始训练
    • 词语含义上的相似性和词向量空间中的余弦相似性是可以对应的
    • ❌ 载入预训练词向量时,语料库和词向量维度的选取并不会对任务的表现有所影响
      • 在进行预训练词向量的载入时,我们需要根据任务的特性来选定语料库的大小和词向量的维度,以均衡模型的表达能力和泛化能力,同时还要兼顾计算的时间复杂度
    • 求类比词时可以复用求近义词的代码
      • 求类比词时我们先会对给定的三个词的词向量进行加减运算,以得到一个虚拟的词向量,再去求这个虚拟词向量的近义词,就可以找到类比词