• 深度学习进阶自然语言处理


    title: 深度学习进阶自然语言处理
    date: 2022-09-18 17:04:31
    mathjax:true
    tags:
    - python
    - 深度学习
    - NLP
    

    2. 自然语言和单词的分布式表示

    单词含义的表示方法有3种

    • 基于同义词词典的方法
    • 基于计数的方法
    • 基于推理的方法

    2.2 同义词词典

    p61

    难以顺应时代变化,人力成本高,无法表示单词的微妙差异

    2.3 基于计数的方法

    p63

    这里需要使用语料库(corpus)

    2.3.1 基于python的语料库的预处理

    p63

    text  = 'You say goodbye and I say hello.'
    text = text.lower()
    text = text.replace('.',' .')
    text # 'you say goodbye and i say hello .'
    
    words = text.split(' ')
    words # ['you', 'say', 'goodbye', 'and', 'i', 'say', 'hello', '.']
    

    现在单词拿到了,但是直接拿文本操作,还是有些不方便,我们需要给单词标上ID

    word_to_id = {}
    id_to_word = {}
    
    for word in words:
        if word not in word_to_id:
            new_id = len(word_to_id)
            word_to_id[word] = new_id
            id_to_word[new_id] = word
            
    print(word_to_id)
    print(id_to_word)
    
    #{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
    #{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
    

    将单词列表转换我单词ID列表

    import numpy as np
    corpus = [word_to_id[w] for w in words]
    corpus = np.array(corpus)
    corpus
    # array([0, 1, 2, 3, 4, 1, 5, 6])
    

    封装成一个函数

    text  = 'You say goodbye and I say hello.'
    def preprocess(text):
        text = text.lower()
        text = text.replace('.', ' .')
        words = text.split(' ')
        
        word_to_id = {}
        id_to_word = {}
        for word in words:
            if word not in word_to_id:
                new_id = len(word_to_id)
                word_to_id[word] = new_id
                id_to_word[new_id] = word
        
        corpus = np.array([word_to_id[w] for w in words])
        
        return corpus,word_to_id,id_to_word
    
    corpus,word_to_id,id_to_word = preprocess(text)
    print(corpus) # [0 1 2 3 4 1 5 6]
    print(word_to_id) # {'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
    print(id_to_word) # {0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
    

    2.3.4 共现矩阵

    p69

    基于分布式假设(用向量表示单词p67)表示单词,最直接的实现方法是对其周围的单词的数量进行计数,然后汇总。这里设窗口为1,you旁边为1的窗口,只有say这个单词,设其为1,其余的词为0,就有you的one-hot向量[0,1,0,0,0,0]

    image-20220918180005378

    同样把say旁边的单词表示出来

    image-20220918184818115

    把剩下的单词表示出来,这个就叫做共现矩阵

    image-20220918185038839

    我们创建一个可以根据语料库生成(p72)

    def create_co_matrix(corpus,vocab_size,window_size=1):
        corpus_size = len(corpus)
        co_matrix = np.zeros((vocab_size,vocab_size),dtype=np.int32)
        
        for idx,word_id in enumerate(corpus):
            for i in range(1,window_size+1):
                left_idx = idx - i
                right_idx = idx + i 
                if left_idx >= 0:
                    left_word_id = corpus[left_idx]
                    co_matrix[word_id,left_word_id] += 1
                    
                if right_idx < corpus_size:
                    right_word_id = corpus[right_idx]
                    co_matrix[word_id,right_word_id] += 1
        return matrix
    

    2.3.5 向量间的相似度

    p72

    余弦相似度是最常用的,表示了“两个向量在多大程度上指向同一方向”,当两个向量完全指向相同的方向时,余弦相似度为1,完全指向相反的方向,余弦相似度指向-1

    \[similarity(x,y)=\frac{x\cdot y}{\left \| x \right \| \left \| y\right \| } =\frac{x_{1}y_{1}+\cdots +x_{n}y_{n} }{\sqrt{x_{1}^{2}+\cdots+x_{n}^{2} }\sqrt{y_{1}^{2}+\cdots+y_{n}^{2} } } \]

    其中||x||表示范数,这里求的是L2范数,首先正则化,然后求它们的内积

    代码实现(p73)

    def cos_similarity(x,y):
        nx = x / np.sqrt(np.sum(x**2))
        ny = y / np.sqrt(np.sum(y**2))
        return np.dot(nx,ny)
    
    # 改进版,防止除0
    
    def cos_similarity(x,y,eps=1e-8):
        nx = x / (np.sqrt(np.sum(x**2)) + eps)
        ny = y / (np.sqrt(np.sum(y**2)) + eps)
        return np.dot(nx,ny)
    

    利用这个函数,可以求得单词向量间的相似度,这里我们求you和i的相似度

    text  = 'You say goodbye and I say hello.'
    corpus,word_to_id,id_to_word = preprocess(text)
    vocab_size = len(word_to_id)
    C = create_co_matrix(corpus,vocab_size)
    
    c0 = C[word_to_id['you']]
    c1 = C[word_to_id['i']]
    print(cos_similarity(c0,c1))
    
    # 0.7071067691154799
    

    2.3.6 相似单词的排序

    p74

    def most_similiar(query,word_to_id,id_to_word,word_matrix,top=5):
        # 取出查询词
        if query not in word_to_id:
            print('%s is not found' % query)
            return
        print('\n[query] ' + query)
        query_id = word_to_id[query]
        query_vec = word_matrix[query_id]
        
        # 计算余弦相似度
        vocab_size = len(id_to_word)
        similarity = np.zeros(vocab_size)
        for i in range(vocab_size):
            similarity[i] = cos_similarity(word_matrix[i],query_vec)
            
        
        # 基于余弦相似度,按降序输出值
        count = 0
        for i in (-1 * similarity).argsort():
            # 与自己做余弦相似度时,跳过
            if id_to_word[i] == query:
                continue
            print(' %s: %s' % (id_to_word[i],similarity[i]))
            
            count += 1
            if count >= top:
                return
    
    • argsort()
    x = np.array([100,-20,2])
    x.argsort()
    # array([1, 2, 0], dtype=int64)
    (-x).argsort() # 负号表示反着来,就是降序
    # array([0, 2, 1], dtype=int64)
    

    显示和you最为相近的单词p76

    import sys
    sys.path.append('..')
    from common.util import preprocess,create_co_matrix,most_similar
    
    text = "you say goodbye and i say hello."
    corpus,word_to_id,id_to_word = preprocess(text)
    
    corpus
    # array([0, 1, 2, 3, 4, 1, 5, 6])
    word_to_id
    #{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
    id_to_word
    #{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
    
    vocab_size = len(word_to_id)
    C = create_co_matrix(corpus,vocab_size)
    
    most_similar('you',word_to_id,id_to_word,C,top=5)
    

    image-20220919144646773

    可以看出you和hello和goodbye和i最为接近

    2.4 基于计数的方法的改进

    2.4.1 点互信息

    p77

    通常我们会看到the car这样的短语,thecar共现的次数很大。但是并不意味着thecar的关联度就很高,drive相比the具有更高的关联度。但是如果只看短语出现的次数来认定它们之间的关联度就不太恰当,为了解决这个问题,可以用点互信息(PMI)。定义如下:

    \[PMI(x,y) = log_{2}\frac{P(x,y)}{P(x)P(y)} \]

    P(x,y)是联合概率,也可以写作P(x∩y)或者P(xy)

    image-20220908214612454

    P(x),P(y)分别表示x和y发生的概率

    PMI的值越高,表示相关性越高,这里举个例子:

    语料库中一共有10000个词,the出现的次数是100,那么p("the")=\(\frac{100}{10000}=0.01\),假设thecar出现的次数是10,那么p("the","car") = \(\frac{10}{10000}=0.001\)

    现在,使用共现矩阵C,来表示PMI,单词x和y共现次数表示为C(x,y),将x和y出现的次数分别表示为C(x),C(y)。带入PMI式子中得:

    \[PMI(x,y) = log_{2}\frac{P(x,y)}{P(x)P(y)}=log_{2}\frac{\frac{C(x,y)}{N}}{\frac{C(x)}{N}\frac{C(y)}{N}}=log_{2}\frac{C(x,y) \cdot N}{C(x)C(y)} \]

    这里假设语料库(corpus)的数量(N)为10000,the出现1000次,car出现20次,drive出现10次,the和car共现10次car和drive共现5次。如果只从共现的次数来看,the和car的相关性更高。如果从PMI的角度来看,就不一样

    \[PMI("the","car") = log_{2}\frac{10 \cdot 10000}{1000 \cdot 20} \approx 2.32 \]

    \[PMI("car","drive") = log_{2}\frac{5 \cdot 10000}{20 \cdot 10} \approx 7.97 \]

    car和drive的相关性就比the高,但是有个问题是如果两个单词共现次数是0时,log20 = -∞。为了解决这个问题,我们会使用正的点互信息(PPMI)

    \[PPMI(x,y) = max(0,PMI(x,y)) \]

    代码实现

    p79

    def ppmi(C,verbose=False,eps=1e-8):
        # 创建一个和共现矩阵形状一样的全0矩阵
        M = np.zeros_like(C,dtype=np.float32)
        N = np.sum(C)
    	print(N) # 14 N是共现矩阵的值
        S = np.sum(C,axis=0)
        total = C.shape[0] * C.shape[1]
        cnt = 0
        
        for i in range(C.shape[0]):
            for j in range(C.shape[1]):
                pmi = np.log2(C[i,j] * N / (S[j]*S[i]) + eps)
                M[i,j] = max(0,pmi)
                
                if verbose:
                    cnt += 1
                    if cnt % (total//100+1) == 0:
                        print('%.1f%% done' % (100*cnt/total))
        
        return M
    
    • C

    C是共现矩阵

    • verbose

    决定是否输出运行情况的标志,处理大语料库时,设置verbose=True

    • C(x),C(y),C(x,y)

    C(x) = \(\sum_{i}C(i,x)\),C(y) = \(\sum_{i}C(i,y)\),N = \(\sum_{i}\sum_{j}C(i,j)\)


    import numpy as np
    from common.util import preprocess,create_co_matrix,cos_similarity
    
    text = "you say goodbye and i say hello."
    corpus,word_to_id,id_to_word = preprocess(text)
    vocab_size = len(word_to_id)
    C = create_co_matrix(corpus,vocab_size)
    W = ppmi(C)
    
    np.set_printoptions(precision=3) # 有效位数为3位
    print('covariance matrix')
    print(C)
    print('-'*50)
    print('PPMI')
    print(W)
    

    image-20220919165838146

    但是,PPMI还有问题是,PPMI矩阵会随着语料库的词汇量增加,单词向量的维数同样增加

    2.4.2 降维

    p81

    减少维度,不是简单的减少,而是尽量保留“重要信息"的基础上减少。我们要观察数据的分布,从而找到重要的”轴“image-20220919170557733

    选择新轴是,需要考虑数据的广度,这样就可以用一维的值也能捕获数据的本质差异,在多维数据中,也可以进行同样的处理。

    稀疏矩阵中,要找出重要的轴,用更少的维度对其进行重新表示。结果,稀疏矩阵就会被转化成大多数元素均不为0的密集矩阵。这个密集矩阵就是单词的分布式表示。

    降维的方法有很多,这里使用SVD(奇异值分解),将任何矩阵分解为3个矩阵的乘积。

    \[X = USV^{T} \]

    SVD将任意的矩阵X分解为U、S、V这个3个矩阵的乘积,其中U和V是列向量相互正交的正交矩阵,S是除了对角线元素以外均为0的对角矩阵

    image-20220919182225486

    U是正交矩阵。这个正交矩阵构成了一些空间中的基轴(基向量),把U称为“单词空间”。S是对角矩阵,奇异值在对角线上降序排列。简单说,可以将奇异值视为“对应的基轴”

    • 基向量

    基向量是这个向量空间里最最基本的组成部分【因为基向量必定线性无关】,由这部分可以表示该向量空间的每一个向量,不就是母体嘛

    • 奇异值

    可以理解奇异值为矩阵中重要信息,其重要程度与奇异值大小息息相关

    奇异值的物理意义是什么? - 知乎 (zhihu.com)

    image-20220919184830105

    矩阵的S的奇异值越小,对应的基轴的重要性越低,因此,可以通过去出矩阵U中多余的列向量来近似原始矩阵(如果饶话,可以看这个链接奇异值的物理意义是什么? - 知乎 (zhihu.com),看为什么图片越来越清晰)

    2.4.3 基于SVD的降维

    p84

    import numpy as np
    import matplotlib.pyplot as plt
    from common.util import preprocess,create_co_matrix,ppmi
    
    text  = 'You say goodbye and I say hello.'
    corpus,word_to_id,id_to_word = preprocess(text)
    vocab_size = len(id_to_word)
    # 共现矩阵
    C = create_co_matrix(corpus,vocab_size,window_size=1)
    W = ppmi(C)
    
    # SVD
    U,S,V = np.linalg.svd(W)
    

    共现矩阵

    print(C[0])
    # [0 1 0 0 0 0 0]
    

    ppmi矩阵

    print(W[0]) 
    # [0.        1.8073549 0.        0.        0.        0.        0.       ]
    

    SVD

    print(U[0])
    # [-1.1102230e-16  3.4094876e-01 -1.2051624e-01 -3.8857806e-16
    #  0.0000000e+00 -9.3232495e-01  8.7683712e-17]
    

    稀疏向量W[0]经过SVD被转换成了密集向量U[0],如果要对这个密集向量降维,比如把它降维到二维向量,取出前两个元素即可

    print(U[0,:2])
    # [-1.1102230e-16  3.4094876e-01]
    

    这样我们完成了降维,现在,我们用二维向量表示各个单词

    for word,word_id in word_to_id.items():
        plt.annotate(word,(U[word_id,0],U[word_id,1]))
        
    plt.scatter(U[:,0],U[:,1],alpha=0.5)
    plt.show()
    

    image-20220919193046666

    • plt.annotate(word,x,y)

    在坐标(x,y)处,绘制单词的文本

    • plt.scatter()

    散点图

    2.4.4 PTB数据集

    PTB语料库中,多了若干预处理,包括将稀有单词替换成特殊字符(unkown),将具体的数字替换成“N”。在PTB中一行保存一个句子。每个句子结尾处插入一个特殊字符(end of sentence)

    from dataset import ptb
    
    corpus,word_to_id,id_to_word = ptb.load_data('train')
    
    print('corpus size:',len(corpus))
    print('corpus[:30]:',corpus[:30])
    print()
    print('id_to_word[0]:',id_to_word[0])
    print('id_to_word[1]:',id_to_word[1])
    print('id_to_word[2]:',id_to_word[2])
    print()
    print("word_to_id['car']:",word_to_id['car'])
    print("word_to_id['happy']:",word_to_id['happy'])
    print("word_to_id['lexus']:",word_to_id['lexus'])
    

    image-20220920141738414

    • plt.load_data()

    使用plt.load_data()加载数据时,需要指定参数'train','test','valid'

    2.4.5 基于PTB数据集的评价

    p88

    corpus,word_to_id,id_to_word = ptb.load_data('train')
    vocab_size = len(word_to_id)
    
    print(corpus.shape) # (929589,)
    print(len(word_to_id)) # 10000
    
    print('counting co-occurrence .....')
    C = create_co_matrix(corpus,vocab_size,window_size)
    print(C.shape) # (10000, 10000)
    
    print('calculating PPMI .....')
    W = ppmi(C,verbose=True)
    
    print('calculating SVD ...')
    try:
        # 使用truncated SVD更快
        from sklearn.utils.extmath import randomized_svd
        U,S,V = randomized_svd(W,n_components=wordvec_size,n_iter=5,random_state=None)
    
    except ImportError:
        # SVD比上面的慢些
        U,S,V = np.linalg.svd(W)
    
    # 取出U中较大的基轴
    word_vecs = U[:,:wordvec_size]
    print('-----word_vecs-------------')
    print(word_vecs.shape) # (10000, 100)
    
    querys = [ta']
    for query in querys:
       	# most_similar的作用是,将query转换为词向量和PPMI矩阵(奇异值分解后)做余弦相似度
        most_similar(query,word_to_id,id_to_word,word_vecs,top=5)
    

    image-20220920145836349

    将单词含义编码成向量,基于语料库,计算单词出现次数,然后转成PPMI矩阵,再基于SVD降维得到的单词向量,这就是单词的分布式表示。

    总结

    • 基于同义词词典的方法,费时费力,且不能表示单词之间细微的差别
    • 基于计数的方法,从语料库中自动提取单词含义,并将其表示为向量,首先先创建共现矩阵,将其转换成PPMI矩阵,再基于SVD降维得到单词的分布式表示

    3. word2vec

    p93

    这里使用的word2vec是基于计数方法的改进版,是基于推理的方法,这里的推理机制使用的是神经网络

    3.1 基于推理的方法和神经网络

    p93

    3.1.1 基于计数的方法的问题

    p94

    现实中,语料库处理的单词数量非常大,比如英文单词可能在100万,基于计数的方法要创建一个100万×100万的庞大矩阵,这么庞大的矩阵对执行SVD显然是不现实的。

    image-20220920153736599

    基于计数的方法一次性处理全部学习数据,基于推理的方法通常在mini-batch数据上进行学习。这意味着神经网络一次只需要看一部分学习数据(mini-batch),并反复更新权重,逐步学习。神经网络的学习可以使用多台机器,多个GPU并行执行,加速学习过程

    3.1.2 基于推理的方法的概要

    p95

    基于推理的方法的主要操作是“推理”。例如,给出上下文,预测处出现什么单词。

    1663660009008

    基于推理的方法,引入某种模型,这个模型接收上下文信息作为输入,并输出可能的单词的出现概率

    3.1.3 神经网络中单词的处理方法

    p96

    神经网络无法直接这些单词,需要先将单词转换成固定长度的向量

    image-20220920161508848

    输入层由7个神经元表示,分别对应7个单词(第一个神经元对应于you,第二个对应于say)

    3.2 简单的word2vec

    p101

    3.2.1 CBOW模型的推理

    p101

    CBOW模型是根据上下文预测目标词的神经网络(目标词是指中间的单词,它周围的单词是“上下文”)。CBOW模型的输入是上下文。这个上下文用['you','goodbye'](处上下文单词是you和goodbye)这样的单词列表表示。我们将其表示为one-hot表示。CBOW模型的网络可以画成下图

    image-20220920164432786

    因为,这里上下文仅考虑两个单词,所以输入层有两个。如果对上下文考虑N个单词,则输入层由N个。

    这个中间层神经元是各个输入层经全连接层变换后得到的值的“平均”。输出层就是各个单词的得分。值越大,出现概率越高。

    从输入层到中间层的变换由全连接层(权重是Win)完成。此时,全连接层的权重Win是一个7×3的矩阵。这个矩阵就是单词的分布式表示

    image-20220920170300955

    如上图所示,权重Win的各行保存着各个单词的分布式表示。通过不断学习,不断更新单词的分布式表示。就是word2vec的全貌。

    中间层的神经元数量比如输入层少,这一点很重要,中间层需要将预测单词所需的信息压缩保存,从而产生密集的向量表示。


    image-20220920171607524

    如上图所示,CBOW模型有两个MatMul层,这两个MatMul的输出被加在一起。然后乘以0.5求平均。得到中间层的神经元。最后,将另一个MatMul层应用于中间层的神经元,得到输出得分。

    代码实现

    import numpy as np
    from common.layers import MatMul
    
    # 样本的上下文数据
    c0 = np.array([[1,0,0,0,0,0,0]])
    c1 = np.array([[0,0,1,0,0,0,0]])
    
    # 权重的初始化
    W_in = np.random.randn(7,3)
    W_out = np.random.randn(3,7)
    
    # 生成层
    in_layer0 = MatMul(W_in)
    in_layer1 = MatMul(W_in)
    out_layer = MatMul(W_out)
    
    # 正向传播
    h0 = in_layer0.forward(c0)
    h1 = in_layer1.forward(c1)
    h = 0.5 * (h0 + h1)
    s = out_layer.forward(h)
    
    print(s.shape) # (1, 7)
    print(s) 
    # [[ 0.33901709  0.73718094  2.10042435 -3.27163436 -0.5974134   1.48779824
    #   1.44842419]]
    

    将权重初始化后,生成与上下文单词数量等量(这里是2个)的处理输入层的MatMul层。输出那边只生成一个MatMul层,输入层的MatMul层共享权重W_in

    3.3 学习数据的准备

    p110

    3.3.1 上下文和目标词

    p110

    我们从语料库中生成上下文和目标词image-20220920193443575

    然后,生成contexts和targetimage-20220920193917636

    代码实现

    from common.util import preprocess
    
    text = "You say googbye and I say hello."
    corpus,word_to_id,id_to_word = preprocess(text)
    print('-----------------corpus-------------------')
    print(corpus)
    print('-----------------id_to_word-------------------')
    print(id_to_word)
    
    def create_contexts_target(corpus,window_size=1):
        target = corpus[window_size:-window_size]
        contexts = []
        
        for idx in range(window_size,len(corpus) - window_size):
            cs = []
            # 每次取target值的前一个和后一个值
            for t in range(-window_size,window_size + 1):
                if t == 0:
                    continue
                cs.append(corpus[idx + t])
            contexts.append(cs)
        return np.array(contexts),np.array(target)
    
    contexts,target = create_contexts_target(corpus)
    print('------------contexts----------------')
    print(contexts)
    print('-------------target-----------')
    print(target)
    

    image-20220920195522325

    3.3.2 转换为one-hot表示

    p113

    image-20220921080609651

    from common.util import preprocess,create_contexts_target,convert_one_hot
    
    text = "You say googbye and I say hello."
    corpus,word_to_id,id_to_word = preprocess(text)
    
    contexts,target = create_contexts_target(corpus,window_size=1)
    
    vocab_size = len(word_to_id)
    target = convert_one_hot(target,vocab_size)
    contexts = convert_one_hot(contexts,vocab_size)
    print('--------------contexts-----------------')
    print(contexts)
    print(contexts.shape)
    print('--------------targets-------------')
    print(target)
    print(target.shape)
    

    image-20220921084407508

    3.4 CBOW模型的实现

    p114

    image-20220921085303990

    • 反向传播是输入乘以局部导数

    代码实现

    import numpy as np
    from common.layers import MatMul,SoftmaxWithLoss
    
    class SimpleCBOW:
        def __init__(self,vocab_size,hidden_size):
            # vocab_size:神经元个数
            # hidden_size:神经元个数
            V,H = vocab_size,hidden_size
            
            # 初始化权重
            W_in = 0.01 * np.random.randn(V,H).astype('f') # 32位浮点型
            W_out = 0.01 * np.random.randn(H,V).astype('f')
            
            # 生成层
            self.in_layer0 = MatMul(W_in)
            self.in_layer1 = MatMul(W_in)
            self.out_layer = MatMul(W_out)
            self.loss_layer = SoftmaxWithLoss()
            
            # 将所有的权重和梯度整理到列表中
            layers = [self.in_layer0,self.in_layer1,self.out_layer]
            self.params,self.grads = [],[]
            for layer in layers:
                self.params += layer.params
                self.grads += layer.grads
            
            # 将单词的分布式表示设置为成员变量
            self.word_vecs = W_in
        
        def forward(self,contexts,target):
            h0 = self.in_layer0.forward(contexts[:,0])
            h1 = self.in_layer1.forward(contexts[:,1])
            h = (h0+h1)*0.5
            score = self.out_layer.forward(h)
            loss = self.loss_layer.forward(score,target)
            return loss
        
        def backward(self,dout=1):
            ds = self.loss_layer.backward(dout)
            da = self.out_layer.backward(ds)
            da *= 0.5
            self.in_layer0.backward(da)
            self.in_layer1.backward(da)
            return None
        
    
    window_size = 1
    hidden_size = 5
    batch_size = 3
    max_epoch = 1000
    
    text = "You say googbye and I say hello."
    corpus,word_to_id,id_to_word = preprocess(text)
    
    vocab_size = len(word_to_id)
    contexts,target = create_contexts_target(corpus,window_size)
    target = convert_one_hot(target,vocab_size)
    contexts = convert_one_hot(contexts,vocab_size)
    
    model = SimpleCBOW(vocab_size,hidden_size)
    optimizer = Adam()
    trainer = Trainer(model,optimizer)
    trainer.fit(contexts,target,max_epoch,batch_size)
    trainer.plot()
    

    image-20220921091905754

    通过权重查看每个单词的分布式表示

    word_vecs = model.word_vecs
    for word_id,word in id_to_word.items():
        print(word,word_vecs[word_id])
    

    image-20220921100430552

    3.5 word2vec的补充说明

    3.5.1 CBOW模型和概率

    p121

    对第t个单词,考虑窗大小为1的上下文

    \[w_{1}w_{2}\dots w_{t-1}w_{t}w_{t+1} \dots w_{T-1}w_{T} \]

    表示当前给定上下文wt-1和wt+1时目标词为wt的概率,使用后验概率,有

    \[P(w_{t}|w_{t-1},w_{t+1})\qquad (3,1) \]

    CBOW模型可以上面的式子建模为式。CBOW模型的损失函数为交叉熵损失函数。式子如下

    \[L = -\sum_{k}t_{k}log_{y_{k}} \]

    其中yk表示第k个事件发生的概率。tk是监督标签,它是one-hot向量的元素。wt发生,这一事件是正确解,它对应的one-hot向量的元素是1,其他元素都是0(wt之外的事件发生时,对应的one-hot向量的元素均为0)。根据这一点,可以推导出下式

    \[L = -logP(w_{t}|w_{t-1},w_{t+1}) \quad(3.2) \]

    CBOW模型的损失函数只是对\(P(w_{t}|w_{t-1},w_{t+1})\)取log,并加上负号,这也成为负对数似然。式(3.2)是一笔样本数据的损失函数。如果将其扩展到整个语料库,则损失函数是

    \[L = -\frac{1}{T}\sum_{t=1}^{T}logP(w_{t}|w_{t-1},w_{t+1}) \quad(3.3) \]

    3.5.2 skip-gram模型

    p122

    word2vec有两个模型,一个是CBOW,一个是skip-gram。skip-gram是反转了CBOW的。

    image-20220922171444566

    CBOW是根据上下文预测中间的词,而skip-gram是通过中间的词去预测上下文。skip-gram结构如下

    image-20220922171633357

    skip-gram输入层只有一个,输出层的数量与上下文单词数量相等。可以建立skip-gram的式子

    \[P(w_{t-1},w_{t+1}|w_{t}) \quad (3.4) \]

    对上面的式子进行分解得

    \[P(w_{t-1},w_{t+1}|w_{t}) = P(w_{t-1}|w_{t})P(w_{t+1}|w_{t}) \quad (3.5) \]

    将(3.5)式子带入交叉熵误差函数,可以推出skip-gram模型的损失函数

    \[\begin{aligned} L &= -logP(w_{t-1},w_{t+1}|w_{t}) \\ &= -logP(w_{t-1}|w_{t})P(w_{t+1}|w_{t}) \\ &= -(logP(w_{t-1}|w_{t})+logP(w_{t+1}|w_{t})) \end{aligned} \]

    扩展到整个语料库 上就是

    \[L = -\frac{1}{T}\sum_{t=1}^{T}(logP(w_{t-1}|w_{t})+logP(w_{t+1}|w_{t})) \]

    skip-gram模型需要求对上下文对应各个单词的损失总和,而CBOW模型只需要求目标单词的损失。但是,我们还是要用skip-gram模型。

    因为,大多数情况下,skip-gram模型的效果更好。

    总结

    word2vec是基于推理的方法,由简单的2层神经网络构成

    word2vec有skip-gram和cbow模型

    CBOW模型从多个单词(上下文)预测一个单词

    skip-gram从一个单词预测多个单词(上下文)

    4. word2vec的高速化

    4.1 word2vec的改进

    p129

    前面的输入层只有7个神经元,这好弄,但是如果词汇量有100万个,CBOW模型的中间的神经元只有100个,输入层和输出层存在100万个神经元。这么多神经元的情况下,中间的计算过程需要很长时间。也会出现两个问题

    • 输入层的one-hot表示和权重矩阵win的乘积
    • 中间层和权重矩阵wout的乘积及softmax层的计算

    第一个问题与输入层的one-hot表示有关,随着单词的增加,one-hot表示的向量大小也会增加。如果有100万个单词,那么one-hot就需要100万个,计算这就很麻烦。需要大量的计算资源,解决这个问题可以使用Embedding层,解决第二个问题可以使用Negative Sampling损失函数解决。

    4.1.1 Embedding层

    p132

    这里考虑单词量是100万个情况下,中间层有100个神经元。MatMul的结构如下

    image-20220923142710533

    我们发现,如果语料库词汇量有100万个,则单词的one-hot表示的维数也会是100万,我们需要计算这个巨大向量和权重矩阵的乘积。但是上图中,无非是从Win中取一行而已,而我却需要花费这么大计算量,很不必要。我们创建一个从参数权重中抽取“单词ID对应的向量”的层,叫做Embedding层。

    从权重矩阵中取一行很简单,例如:W[索引]就可以取到某一行

    import numpy as np
    
    class Embedding:
        def __init__(self,W):
            self.params = [W]
            self.grads = [np.zeros_like[W]]
            self.idx = None
        
        def forward(self,idx):
            W, = self.params
            self.idx = idx
            out = W[idx]
            return out
    

    Embedding的正向传播只是从权重矩阵W中提取特定的行,并将该行的神经元原样传给下一层。

    在反向传播时,输出侧的层传过来的梯度将原样传给输入层。不过,从输出层传过来的梯度会被应用到权重梯度的特定行(idx),

    我们可以一次取多行的索引(前提是mini-batch,mini-batch可以用GPU进行并行计算)

    image-20220923155555229

    反向传播的实现,如果我们一次取多行的索引的话,比如取索引[0,1,0,2]的话第0行会被写入两次,这里的解决方法是将那行的值进行累加

    def backward(self,dout):
        dW, = self.grads
        dW[...] = 0
    
        for i,word_id in enumerate(self.idx):
            dW[word_id] += dout[i]
            
    	# np.add.at(dW,self.idx,dout) 比for循环更快
        
        return None
    

    我们可以将MatMul层换成Embedding层。这样一来,既能减少内存开销,又能避免不必要的计算

    4.2 word2vec的改进

    p137

    4.2.1 中间层之后的计算问题

    word2vec的第二个改进是中间层之后的处理,即矩阵乘积和softmax层的计算。这里采用的方法是负采样。使用Negative Sampling替代Softmax,无论词汇多么大,都可以使计算量保持较低或恒定。

    image-20220923171654528

    这里有两个问题

    • 中间层的神经元和权重矩阵(Wout)的乘积
    • Softmax层的计算

    第一个问题是巨大的矩阵计算,需要对矩阵进行轻量化。其次softmax随着单词数的增加,计算会变得慢起来。

    4.2.2 从多分类到二分类

    p139

    负采样的关键思想在于二分类问题,更准确的说,是用二分类问题拟合多分类问题。(二分类问题就是回答yes或no的问题)

    我们可以把从100万个单词中选择1个正确单词的任务,当做二分类问题。

    假如,现在上下文是you和goodbye,我想知道say的得分的话,其实,输出层只要一个神经元就可以了。如下图所示

    image-20220924184658166

    注意,这里CBOW模型规定you和goodbye乘以Win后会加在一块,然后取平均,也就是最后shape是(1,100)

    如上图所示,输出层的神经元只有一个。要想计算中间层和输出层的权重,只需要提取say对应的列,然后与中间层的神经元计算内积就可以了。计算过程如下

    image-20220924190332587

    4.2.3 sigmoid函数和交叉熵误差

    p141

    要使用神经网络解决二分类问题,需要使用sigmoid函数将得分转换为概率。交叉熵作为损失函数。通过sigmoid函数得到概率y后,可以由概率y计算损失,用于sigmoid的损失函数也是交叉熵误差,其数学式如下

    \[L = -(tlogy \ + \ (1-t)log(1-y)) \]

    y是sigmoid的输出,t是正确解标签,取值为0或者1。1为Yes,0为No。sigmoid函数和交叉熵误差的组合产生了y-t这样漂亮的结果,softmax函数和交叉熵误差的组合,或者恒等函数和均方误差的组合也会在反向传播时传播y-t。

    4.2.4 多分类到二分类的实现

    p144

    二分类的网络结构图,dot:在原来的位置上乘,还有就是之前是多分类问题,target可以是0和1以外的其它的值,现在变为二分类问题,target(正确解标签)变为1,这点注意

    image-20220924193807461

    后半部分可以画成下图

    image-20220924194656751

    EmbeddingDot实现

    import numpy as np
    
    class EmbeddingDot:
        def __init__(self,W):
            self.embed = Embedding(W)
            self.params = self.embed.params
            self.grads = self.embed.grads
            self.cache = None
            
        def forward(self,h,idx):
            target_W = self.embed.forward(idx)
            # 因为算的是内积,target_w * h 和 h * target_w 没有区别
            out = np.sum(target_W * h ,axis=1)
            
            # 保存正向传播的结果
            self.cache = (h,target_W)
            return out
        
        def backward(self,dout):
            h,target_W = self.cache
            dout = dout.reshape(dout.shape[0],1)
            
            dtarget_W  = dout * h
            self.embed.backward(dtarget_W)l
            dh = dout * target_W
            return dh
    

    内积的先后顺序不影响

    image-20220925130655317

    可以通过下图理解

    image-20220924202601040

    4.2.5 负采样

    p148

    当前的神经网络只学习了正例say,但是对say之外的负例一无所知。我们要做的是让正例的sigmoid输出无限接近1,负例的sigmoid输出无限接近0。为了把多分类问题转换为二分类问题,能够正确的处理正例和负例,需要同时考虑正例和负例。当然这里只考虑少量负例,然后将正例和负例的损失累加起来。

    image-20220925145403131

    4.2.6 负采样的采样方法(没弄懂)

    p151

    如何抽取负例,基于语料库的统计数据进行采样的方法比随机抽样要好,具体就是,让高频词容易被抽到,低频词不容易被抽到,有必要将负例限定在较小范围内(5个或10个)。

    在采样之前,我们先用下np.random.choice()方法

    # 从0到9中随机挑选一个数字
    np.random.choice(10) # 9
    
    # 随机选一个元素
    words = ['you','say','goodbye','I','hello','.']
    np.random.choice(words) # hello
    
    # 有放回采样5次
    np.random.choice(words,size=5) # array(['goodbye', '.', 'you', 'I', '.'], dtype='<U7')
    
    # 无放回采样5次
    np.random.choice(words,size=5,replace=False) # array(['say', 'hello', 'goodbye', 'you', '.'], dtype='<U7')
    
    # 基于概率分布进行采样
    p = [0.5,0.1,0.05,0.2,0.05,0.1]
    np.random.choice(words,p=p) # 'I'
    

    word2vec中提出的负采样对刚才的概率分布增加了一个步骤,对原来的概率分布取0.75次方

    \[P'(w_{i})=\frac{P(w_{i})^{0.75}}{\sum_{j}^{n}P(w_{j})^{0.75}} \]

    通过这个0.75次方可以让低频单词的概率稍微变高

    p = [0.7,0.29,0.01]
    new_p = np.power(p,0.75)
    new_p /= np.sum(new_p)
    print(new_p) # [0.64196878 0.33150408 0.02652714]
    

    至于为啥取0.75,这个随缘取的哈

    corpus = np.array([0,1,2,3,4,1,2,3])
    power = 0.75
    sample_size = 2 
    
    sampler = UnigramSampler(corpus,power,sample_size)
    target = np.array([1,3,0])# 这里的target的作用是在embedding层,现在的正确解是1,不是其它
    negative_sample = sampler.get_negative_sample(target)
    print(negative_sample)
    # [[0 3]
    #  [1 2]
    #  [2 3]]
    

    这个UnigramSampler()作用生成负例样本

    class NegativeSamplingLoss:
        def __init__(self, W, corpus, power=0.75, sample_size=5):
            self.sample_size = sample_size
            self.sampler = UnigramSampler(corpus, power, sample_size)
            self.loss_layers = [SigmoidWithLoss() for _ in range(sample_size + 1)]  # 5个负例样本,一个正例,所以+1
            self.embed_dot_layers = [EmbeddingDot(W) for _ in range(sample_size + 1)]
    
            self.params, self.grads = [], []
            for layer in self.embed_dot_layers:
                self.params += layer.params 
                self.grads += layer.grads
    
        def forward(self, h, target):
            # target是一维的,batch_size就是target的个数
            batch_size = target.shape[0]
            # 根据target个数,生成相应的负样本
            negative_sample = self.sampler.get_negative_sample(target)
    
            # 正例的正向传播,target是向量,h计算完后,h分别与正例和负例进行dot运算,正例和负例都是从w_out来的,不同h对应不同的target,不然为什么h和target是dot运算,不懂看上面的图4-14,那个batch_size是3,h是3×3
            score = self.embed_dot_layers[0].forward(h, target)
            # 正确标签是1
            correct_label = np.ones(batch_size, dtype=np.int32)
            # target是向量,正确标签是1,计算它们之间的loss,算完后还是向量
            loss = self.loss_layers[0].forward(score, correct_label)
    
            # 负例的正向传播,h与w_out的embedding中负样本(这里的负样本是随机选的)进行dot运算
            negative_label = np.zeros(batch_size, dtype=np.int32)
            for i in range(self.sample_size):
                negative_target = negative_sample[:, i]
                score = self.embed_dot_layers[1 + i].forward(h, negative_target)
                loss += self.loss_layers[1 + i].forward(score, negative_label)
    
            return loss
    
        def backward(self, dout=1):
            dh = 0
            for l0, l1 in zip(self.loss_layers, self.embed_dot_layers):
                dscore = l0.backward(dout)
                dh += l1.backward(dscore)
    
            return dh
    

    4.3 改进版word2vec的学习

    p156

    class CBOW:
        def __init__(self,vocab_size,hidden_size,window_size,corpus):
            V,H = vocab_size,hidden_size
            
            # 初始化权重
            W_in = 0.01 * np.random.randn(V,H).astype('f')
            W_out = 0.01 * np.random.randn(H,V).astype('f')
            
            # 生成层
            self.in_layers = []
            for i in range(2 * window_size):
                layer = Embedding(W_in)
                self.in_layers.append(layer)
            self.ns_loss = NegativeSamplingLoss(W_out,corpus,power=0.75,sample_size=5)
            
            # 将所有的权重和梯度整理到列表中
            layers = self.in_layers + [self.ns_loss]
            self.params,self.grads = [],[]
            for layer in layers:
                self.params += layer.params
                self.grads += layer.grads
            
            # 将单词的分布式表示设置为成员变量
            self.word_vecs = W_in
           
        def forward(self,context,target):
            h = 0
            for i,layer in enumerate(self.in_layers):
                h += layer.forward(contexts[0][i])
            h *= 1 / len(self.in_layers) # 对应计算图的中乘以0.5
            loss = self.ns_loss.forward(h,target)
            return loss
        
        def backward(self,dout=1):
            dout = self.ns_loss.backward(dout)
            dout *= 1 / len(self.in_layers)
            for layer in self.in_layers:
                layer.backward(dout)
            return None
    

    vocab_size:词汇量

    hidden_size:中间层的神经元个数

    corpus:单词ID列表

    window_size:上下文的大小

  • 相关阅读:
    软件设计师经验分享
    数据库设计基本规范
    UEditor上传文件的默认地址修改
    mongoDB简单介绍及安装
    链表中倒数第k个结点
    一入python深似海--对象的属性
    stl--vector 操作实现
    android5.x加入sim1,sim2标识
    leetCode 27.Remove Element (删除元素) 解题思路和方法
    java8新增特性(一)---Lambda表达式
  • 原文地址:https://www.cnblogs.com/bzwww/p/16812234.html
Copyright © 2020-2023  润新知