伯禹公益AI《动手学深度学习PyTorch版》Task 07 学习笔记

伯禹公益AI《动手学深度学习PyTorch版》Task 07 学习笔记

Task 07:优化算法进阶;word2vec;词嵌入进阶

微信昵称:WarmIce

优化算法进阶

emmmm,讲实话,关于所谓的病态问题是什么,条件数在复杂优化目标函数下的计算,讲者也没说,咱也没法问,只知道要计算一个Hessian矩阵,然后计算得到特征值,但是具体怎么操作实属未知,还得去参看别的材料。总而言之,很多机器学习的优化目标函数是个病态的函数,整体就是个病态问题。其实在《数值分析》里面也有提到过条件数的计算,不过那个是放在“线性代数方程组数值解法”中提及的,如果对应的矩阵条件数过大,那么也构成病态问题,使用迭代求解的方法很难得到优良的近似解。

那么针对这个病态问题呢,两个方向可以解决:

  • Preconditioning gradient vector: applied in Adam, RMSProp, AdaGrad, Adelta, KFC, Natural gradient and other secord-order optimization algorithms. 说白了就是根据Hessian矩阵,对迭代过程中的优化量进行二阶修正。
  • Averaging history gradient: like momentum, which allows larger learning rates to accelerate convergence; applied in Adam, RMSProp, SGD momentum. 这个就更直白了,即根据历史梯度来决定接下来怎么走,也就是说历史梯度为优化提供了一些意见。

后面先介绍了动量算法,也就是利用历史梯度信息进行优化的算法。然后搞了花里胡哨的指数近似函数,害,都是骗人的,就是个加权平均。说好听点,指数移动平均。说难听点,这公式咱们在滤波里面也用过。

后面就介绍了一些花里胡哨,但是用下来都不咋地的优化算法,我就不想提了。

总之,这些算法都是梯度下降算法,也就是说,他们都无可避免地要遇到之前说的梯度下降必然有的问题,虽然能够在一定程度上缓解,但是同时也是在限定情形下的缓解,终究都不是普适的方法,说来说去,还是SGD够硬。例如AdaGrad的梯度消失,然后RMSProp和AdaDelta给AdaGrad擦屁股,Adam在不少算法中有用到,但是一些特定情形下,这个Adam也就只能悻悻然灰溜溜逃走。

word2vec

之前的笔记里,我就有用删除线删除了一些语句,那是因为,当时我不太清楚nn.Embedding()究竟是在干啥的。今天,在这位优秀的讲者(一个声音贼好听的小妹妹,人家还是本科生,上交的)的引领下,我略懂那么一些了。

不过呢,有一说一,讲得字正腔圆是没错,但是很多时候还是在陈述过程,但是我更想知道这么做的目的,或者为什么要这么做,这些我更加关心。

上来就甩出来一些概念,什么背景词、中心词,然后Skip-Gram跳字模型、CBOW (continuous bag-of-words) 连续词袋模型,然后又是条件概率公式,又是本节以Skip-Gram跳字模型实现为主云云,整得人晕晕。

但是我们要赶紧猜测或者说捋一捋这个word2vec究竟要干啥,简单来说,one-hot那种编码方法是离散的,每个词之间都是完全正交的,没有互相之间的关系,但是我们希望表征每个单词的向量能够包含语义上的信息,尤其是在语义相似度这一点上。所以,我们要如何获得这样的向量表达呢?

首先,整理数据集,建立词典(或者说是词语索引)。

接下来这一步就很重要了,二次采样,当当当当。

文本数据中一般会出现一些高频词,如英文中的“the”“a”和“in”。通常来说,在一个背景窗口中,一个词(如“chip”)和较低频词(如“microprocessor”)同时出现比和较高频词(如“the”)同时出现对训练词嵌入模型更有益。因此,训练词嵌入模型时可以对词进行二次采样。 具体来说,数据集中每个被索引词 w i w_i wi 将有一定概率被丢弃,该丢弃概率为

P ( w i ) = max ⁡ ( 1 − t f ( w i ) , 0 ) P(w_i)=\max(1-\sqrt{\frac{t}{f(w_i)}},0) P(wi)=max(1f(wi)t ,0)

其中 f ( w i ) f(w_i) f(wi) 是数据集中词 w i w_i wi 的个数与总词数之比,常数 t t t 是一个超参数(实验中设为 1 0 − 4 10^{−4} 104)。可见,只有当 f ( w i ) > t f(w_i)>t f(wi)>t 时,我们才有可能在二次采样中丢弃词 w i w_i wi,并且越高频的词被丢弃的概率越大。

这一段说的很清楚了,其实这个视觉SLAM里面也有用到,不过那个里面就是把单词换成了图像特征而已,不多比比了。

再接下来,就是提取中心词和背景词。除了这两个概念,还有个视窗的概念。其实吧,都说到这儿了,这个意思已经很明确了,就是说我在一个足够长的句子里面(起码要有2个单词),从左到右,每个单词都有机会作为中心词,然后根据所开的视窗大小,中心词左边和右边的视窗大小个的单词就都是属于这个中心词的背景词。说白了,这个中心词此时就和这几个背景词挂上钩了,因为它们一起出现了

提取中心词和背景词的函数get_centers_and_contexts返回的是一个元组,第一个元素是中心词单层列表,第二个元素是一个双层列表,依序为对应中心词的背景词,视窗大小随机。

这里还顺带重新提到了torch自带的Embedding层,其实这个层乍一看有点懵逼,其实很好理解,初始化时候的num_embeddings是指给多少个字符进行词嵌入,embedding_dim是指最后形成的词向量的维度。因此,我们输入这个Embedding层的tensor其实是指定了idx,而这个idx必须在range(num_embeddings)范围内,否则就没有与之对应的词向量。

刚刚说这一节主要就是要将跳字模型。

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

P ( w o ∣ w c ) = exp ⁡ ( u o ⊤ v c ) ∑ i ∈ V exp ⁡ ( u i ⊤ v c ) 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(wowc)=iVexp(uivc)exp(uovc)

可以看到,式子中“中心词”是不变的,而在中心词确定的情况下,所有与之一起出现过的背景词,每个背景词就都有了一个softmax概率,exp函数计算的值隐式地表达了中心词和每个背景词的相似度。但是各位想一想,一个中心词可能有很多很多单词都是其背景词,一旦如此,这个计算开销可是顶不住。这该怎么好呢?还有一个问题,我不能只关注中心词和谁接近啊,我还得知道中心词和谁疏远,这个该咋整呢?

对于第一个问题,我们可以用数学的方法解决,对于第二个问题,我们引入噪声词。至于为啥有第二个问题,你想想啊,你是个刚刚生下来的孩子,我只告诉你什么样的好的,不告诉你什么是坏的,你就会倾向于给所有人发好人卡,因为只要做出是好的这个判断,在之前的训练过程中都有很低的损失。这也就是负样本的作用。

所以,解决上面这个问题的整个方法就叫做负采样近似

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

负采样方法用以下公式来近似条件概率 P ( w o ∣ w c ) = exp ⁡ ( u o ⊤ v c ) ∑ i ∈ V exp ⁡ ( u i ⊤ v c ) 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(wowc)=iVexp(uivc)exp(uovc)

P ( w o ∣ w c ) = P ( D = 1 ∣ w c , w o ) ∏ k = 1 , w k ∼ P ( w ) K P ( D = 0 ∣ w c , w k ) 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(wowc)=P(D=1wc,wo)k=1,wkP(w)KP(D=0wc,wk)

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

给出的代码里面的get_negatives函数实现的还是比较巧妙的,通过random.choices函数,其中的相对权重可以事先计算好传进去,最后循环,就得到了与中心词顺序对应的噪声词。

**这样一来,我们就有了中心词、背景词与噪声词了,且他们的次序是对应好的。**然后就可以用torch.utils.data.Dataset来getitem了。

但是还是不够,因为我们训练的时候是要按照batch这样的批量送进去的,每个getitem得到的数据,其背景词和噪声词的长度很大可能是不一样的,此时就需要对批量拉取出来的数据做进一步处理,箭头指向batchify函数。然后在Data.DataLoader进行批量化拉取数据时指定collect_fn为batchify,就会很美好,pytorch会在拉取好原始数据后自动调用collect_fn对数据进行处理。经处理后得到 (centers, contexts_negatives, masks, labels) 元组:

centers: 中心词下标,形状为 (n, 1) 的整数张量
contexts_negatives: 背景词和噪声词的下标,形状为 (n, m) 的整数张量
masks: 与补齐相对应的掩码,形状为 (n, m) 的0/1整数张量
labels: 指示中心词的标签,形状为 (n, m) 的0/1整数张量

最后就是训练模型了,但是还有一步咱们没做,损失函数是什么?

应用负采样方法后,我们可利用最大似然估计的对数等价形式将损失函数定义为如下

∑ t = 1 T ∑ − m ≤ j ≤ m , j ≠ 0 [ − log ⁡ P ( D = 1 ∣ w ( t ) , w ( t + j ) ) − ∑ k = 1 , w k ∼ P ( w ) K log ⁡ P ( D = 0 ∣ w ( t ) , w k ) ] \sum_{t=1}^T\sum_{-m\le j\le m,j\ne 0} [-\log P(D=1\mid w^{(t)},w^{(t+j)})-\sum_{k=1,w_k\sim P(w)^K}\log P(D=0\mid w^{(t)},w_k)] t=1Tmjm,j=0[logP(D=1w(t),w(t+j))k=1,wkP(w)KlogP(D=0w(t),wk)]

根据这个损失函数的定义,我们可以直接使用二元交叉熵损失函数进行计算。

我理一下,说白了,中心词会生成一个对应的词向量,背景词和噪声词也会生成各自对应的词向量,我们的目标就是让中心词生成的词向量与背景词生成的词向量尽可能接近,同时要与噪声词生成的词向量尽可能疏远

使用余弦相似度获取相似语义的单词(近义词)时,要注意取topk时,跟自己肯定是最相近的,所以要取k+1个,然后去掉自己看看近义词是哪些。

词嵌入进阶

回顾刚才的word2vec,其实你可以发现,我们是回避了上面提到的全局运算量过大的问题,转而利用一个近似函数作为目标函数进行优化。那么这一节的Glove全局向量的词嵌入模型就要和这个问题正面刚,是男人就跟他刚,看谁够硬。

GloVe通过等价转换 Word2Vec 模型的条件概率公式,得到一个全局的损失函数表达,并在此基础上进一步优化模型。

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

哦哦哦,我突然有个想法,就是说,上面的负采样,其实是一种对于不能进行全局采样的弥补措施,对不对!!!

想想看,要是我知道了全局的信息,我还考虑个屁的负样本啊,因为正样本都被我考虑进来啦,剩下来的不就都是负样本吗,这个时候上面那种偏差认知就不存在啦!!!

介绍这个GloVe模型的优化函数的变形那边我尚且还可以看得懂,贴一下内容:

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

− ∑ t = 1 T ∑ − m ≤ j ≤ m , j ≠ 0 log ⁡ P ( w ( t + j ) ∣ w ( t ) ) -\sum_{t=1}^T\sum_{-m\le j\le m,j\ne 0} \log P(w^{(t+j)}\mid w^{(t)}) t=1Tmjm,j=0logP(w(t+j)w(t))

其中

P ( w j ∣ w i ) = exp ⁡ ( u j ⊤ v i ) ∑ k ∈ V exp ⁡ ( u k ⊤ v i ) 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)} P(wjwi)=kVexp(ukvi)exp(ujvi)

w i w_i wi 为中心词, w j w_j wj 为背景词时 Skip-Gram 模型所假设的条件概率计算公式,我们将其简写为 q i j q_{ij} qij

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

− ∑ i ∈ V ∑ j ∈ V x i j log ⁡ q i j -\sum_{i\in\mathcal{V}}\sum_{j\in\mathcal{V}} x_{ij}\log q_{ij} iVjVxijlogqij

其中 x i j x_{ij} xij 表示整个数据集中 w j w_j wj 作为 w i w_i wi 的背景词的次数总和。

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

− ∑ i ∈ V x i ∑ j ∈ V p i j log ⁡ q i j -\sum_{i\in\mathcal{V}}x_i\sum_{j\in\mathcal{V}}p_{ij} \log q_{ij} iVxijVpijlogqij

其中 x i x_i xi w i w_i wi 的背景词窗大小总和, p i j = x i j / x i p_{ij}=x_{ij}/x_i pij=xij/xi w j w_j wj w i w_i wi 的背景词窗中所占的比例。

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

但是后面那个改进的4点,为什么那么做,讲者是只字未提,不过是念了一遍。。。。。。我特么咋知道为什么这么做,我怀疑她也不知道。。。。贴一下内容吧:

而在 Word2Vec 之后提出的 GloVe 模型,则是在之前的基础上做出了以下几点改动:

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

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

∑ i ∈ V ∑ j ∈ V h ( x i j ) ( u j ⊤ v i + b i + c j − log ⁡ x i j ) 2 \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 iVjVh(xij)(ujvi+bi+cjlogxij)2

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

那么实际上呢,这个训练咱们也没得办法进行,那么大的语料库。所以我们就只能用人家放出来给咱们用的,即载入与训练的GloVe向量,然后在此基础上做一些应用。怎么载入呢?

import torch
import torchtext.vocab as vocab

print([key for key in vocab.pretrained_aliases.keys() if "glove" in key])
cache_dir = "/home/kesci/input/GloVe6B5429"
glove = vocab.GloVe(name='6B', dim=50, cache=cache_dir)
print("一共包含%d个词。" % len(glove.stoi))
print(glove.stoi['beautiful'], glove.itos[3366])

能做些什么应用呢,这里官方给了两个例子,求近义词和类比词,近义词我就不想说了,跟上面的一毛一样。

啥叫类比词呢?

除了求近义词以外,我们还可以使用预训练词向量求词与词之间的类比关系,例如“man”之于“woman”相当于“son”之于“daughter”。求类比词问题可以定义为:对于类比关系中的4个词“ a a a 之于 b b b 相当于 c c c 之于 d d d”,给定前3个词 a , b , c a,b,c a,b,c d d d。求类比词的思路是,搜索与 vec ( c ) + vec ( b ) − vec ( a ) \text{vec}(c)+\text{vec}(b)−\text{vec}(a) vec(c)+vec(b)vec(a) 的结果向量最相似的词向量,其中 vec ( w ) \text{vec}(w) vec(w) w w w 的词向量。

代码在下面,也没啥意思,只能说这个语义信息学得确实挺不错的:

def knn(W, x, k):
    '''
    @params:
        W: 所有向量的集合
        x: 给定向量
        k: 查询的数量
    @outputs:
        topk: 余弦相似性最大k个的下标
        [...]: 余弦相似度
    '''
    cos = torch.matmul(W, x.view((-1,))) / (
        (torch.sum(W * W, dim=1) + 1e-9).sqrt() * torch.sum(x * x).sqrt())
    _, topk = torch.topk(cos, k=k)
    topk = topk.cpu().numpy()
    return topk, [cos[i].item() for i in topk]

def get_similar_tokens(query_token, k, embed):
    '''
    @params:
        query_token: 给定的单词
        k: 所需近义词的个数
        embed: 预训练词向量
    '''
    topk, cos = knn(embed.vectors,
                    embed.vectors[embed.stoi[query_token]], k+1)
    for i, c in zip(topk[1:], cos[1:]):  # 除去输入词
        print('cosine sim=%.3f: %s' % (c, (embed.itos[i])))

get_similar_tokens('chip', 3, glove)

def get_analogy(token_a, token_b, token_c, embed):
    '''
    @params:
        token_a: 词a
        token_b: 词b
        token_c: 词c
        embed: 预训练词向量
    @outputs:
        res: 类比词d
    '''
    vecs = [embed.vectors[embed.stoi[t]] 
                for t in [token_a, token_b, token_c]]
    x = vecs[1] - vecs[0] + vecs[2]
    topk, cos = knn(embed.vectors, x, 1)
    res = embed.itos[topk[0]]
    return res

get_analogy('man', 'woman', 'son', glove)

以上。

你可能感兴趣的:(机器学习)