机器学习 Word2vec---2 python实现过程

获取语料库

# define the corpus
# 处理方法1
import re
text = 'natural language processing and machine learning \
is fun and exciting'
corpus = re.split('[\n]|\s+', text)

# 处理方法2
from gensim.utils import simple_preprocess
corpus = simple_preprocess(text)

# 定义超参数
params = {
    'window_size': 10,
    'n': 5,
    'epochs': 2,
    'learning_rate': 0.01   
}

定义超参数

  1. window_size:上下文单词是与目标单词相邻的单词,但是如何量化相邻程度呢?有一个滑动窗口slide_windwo,如果将滑动窗的窗口定义2,那么表示目标单词左边和右边最近2个单词被认为是上下文单词。当窗口滑动时,其中的每一个词都有可能称为目标词
  2. n: 词嵌入(word embedding)的维度,通常其的大小从100到300不等,取决于词汇库的大小。超过300会导致效益递减。同时n也是隐藏层的大小;
  3. epochs: 表示遍历整个样本的次数,在每个epochs中我们循环通过一边训练集的样本;
    learning_rate: 学习率控制着损失梯度对权重进行调整的量。
n_word = len(corpus)
for iter_num in range(n_word):
    
    left_index_2 = iter_num - 2
    left_index_1 = iter_num - 1
    
    right_index_1 = iter_num + 1
    right_index_2 = iter_num + 2
    
    left_2 =  '' if left_index_2 < 0 else corpus[left_index_2]
    left_1 =  '' if left_index_1 < 0 else corpus[left_index_1]
    target = corpus[iter_num]
    right_1 =  '' if right_index_1 > n_word - 1 else corpus[right_index_1]
    right_2 =  '' if right_index_2 > n_word - 1 else corpus[right_index_2]
    
    
    print(f'{iter_num}'.rjust(2, ' '), end='  ')
    print(f'{left_2}'.ljust(12, ' '), end='')
    print(f'{left_1}'.ljust(12, ' '), end='')
    print(f'{target}'.ljust(12, ' '), end='')
    print(f'{right_1}'.ljust(12, ' '), end='')
    print(f'{right_2}'.ljust(12, ' '))
    print('    ----------------------------------------------------------')

在这里插入图片描述

生成训练数据

我们的主要目标是 将语料库转换成one-hot编码表示,以方便Word2Vec模型用来训练。如果有10词迭代,每次迭代中每个窗口都由目标词以及其上下文组成。

one_hot_dict = dict()
for pos, word in enumerate(corpus):
    vec = [0] * n_word
    vec[pos] = 1
    one_hot_dict[word] = vec

train_data = []

n_word = len(corpus)
for iter_num in range(n_word):
    window_ = []
    
    left_index_2 = iter_num - 2
    left_index_1 = iter_num - 1
    
    right_index_1 = iter_num + 1
    right_index_2 = iter_num + 2
    
    left_2 =  '' if left_index_2 < 0 else corpus[left_index_2]
    left_1 =  '' if left_index_1 < 0 else corpus[left_index_1]
    target = corpus[iter_num]
    right_1 =  '' if right_index_1 > n_word - 1 else corpus[right_index_1]
    right_2 =  '' if right_index_2 > n_word - 1 else corpus[right_index_2]
        
    tar_ = one_hot_dict[target]
    neigh = []
    for wd in [left_2, left_1, right_1, right_2]:
        if wd in one_hot_dict.keys():
            neigh.append(one_hot_dict[wd])
    
    train_data.append([tar_, neigh])   

模型训练

在这里插入图片描述
数据准备好之后,我们就可以开始训练模型了。

  1. word2vec有两个权重矩阵( W 1 , W 2 W_1, W_2 W1,W2),我们把权重矩阵初始化到形状分别为(9×10)和(10×9)的矩阵中,这个是便于反向传播误差的计算。在实际训练过程中,应该要随机初始化这些权重。
# initialize weight matrix
# 中间隐层的数量为5
rng = np.random.RandomState(seed=10)
W_i = rng.randn(n_word, params['n'])
W_o = rng.randn(params['n'], n_word)

前向传递

我们开始用第一组训练样本来训练第一个epoch,方法是:将目标词的one-hot向量 w t w_t wt与权重矩阵进行相乘(进行点积运算),得到一个向量 h t h_t ht,然后再将 h t h_t ht w 2 w_2 w2做点积运算。最后,在返回预测向量 y p r e d y_pred ypred和隐藏层h和输出层u前,我们使用softmax把每个元素的值映射到0和1之间来得到用来预测的概率。

反向传递

接下来我们进行反向传播,反向传播的公式就是梯度下降: w k + 1 = w k − α ∗ ∂ l o s s ∂ w ∣ w = w k w^{k+1}=w^{k} - \alpha * \frac{\partial loss}{\partial w}|_{w=w_k} wk+1=wkαwlossw=wk,通过传入误差El、隐藏层h和目标字 w t w_t wt的向量,来计算我们所需要的权重调整量。为了更新权重,我们将权重的调整量( d w 1 , d w 1 dw_1, dw_1 dw1,dw1)与学习率相乘,然后从当前权重 ( w 1 和 w 2 ) (w_1和w_2) (w1w2)中减去它。

# the proprocess of forward propogation

def softmax(x):
    
    e1 = np.exp(x - np.mean(x))
    s = np.sum(e1)
    return e1 / s
        
def forward_flow(x, W_i, W_o):
    
    for i, v in enumerate(x):
        
        assert len(v) == W_i.shape[0]
        ret = np.dot(W_i.T, np.array(v).reshape(-1, 1))
        if i == 0:
            hid = ret
        else:
            hid += ret
    
    # 窗口中的上下文向量求平均值
    hid = hid / (i + 1)        
    
    out = np.dot(W_o.T, hid)
    y_pred = softmax(out)
    return y_pred, hid, out

def back_flow(err_vec, h, vc, W_i, W_o):
    
    d_dw_o = np.outer(h, err_vec)
    d_dw_i = np.outer(vc, np.dot(W_o, err_vec))
    
    W_i = W_i - params['learning_rate'] * d_dw_i
    W_o = W_o - params['learning_rate'] * d_dw_o
    return W_i, W_o

epoch = 0
loss_list = []
while epoch <= params['epochs']:
    
    for vc, vn in train_data:
        vc = np.array(vc).reshape(-1, 1)
        pred_v, h_v, o_v = forward_flow(vn, W_i, W_o)
        
        error = pred_v - vc
        
        W_i, W_o = back_flow(error, h_v, vc, W_i, W_o)
        
    p1 =  -1 * np.sum(o_v)
    p2 = 2 * params['window_size'] * np.log(np.sum(np.exp(pred_v)))
    loss = p1 + p2
    loss_list.append(loss)
    epoch += 1

plt.figure(figsize=(6, 8))
plt.plot(range(epoch), loss_list)
plt.show()

在这里插入图片描述

求总损失

最后,根据损失函数计算出每个样本完成训练后的总损失。注意,损失函数包括两部分。第一部分是输出层(在softmax之前)中所有元素的和的负数。第二部分是上下文单词的数量乘以输出层中所有元素(在exp之后)之和的对数。

E = − l o g ( p ( W o , 1 , W o , 1 . . . . W o , v ∣ W I ) ) = − l o g ∏ c = 1 C e x p ( u c ) ∑ j = 1 V e x p ( u j ) = − ∑ c = 1 C u j + C ∗ l o g ( ∑ j = 1 V e x p ( u j ) ) E = -log(p(W_{o,1}, W_{o,1}....W_{o,v}|W_I)) = -log\prod_{c=1}^C \frac{exp(u_c)}{\sum_{j=1}^V exp(u_j)} = -\sum_{c=1}^C u_j + C*log(\sum_{j=1}^V exp(u_j)) E=log(p(Wo,1,Wo,1....Wo,vWI))=logc=1Cj=1Vexp(uj)exp(uc)=c=1Cuj+Clog(j=1Vexp(uj))

p1 =  -1 * np.sum(o_v)
p2 = 2 * params['window_size'] * np.log(np.sum(np.exp(pred_v)))
loss = p1 + p2

优化过程

  1. huffman树
    1. 基本概念
      1. 路径和路径长度:树中一个结点到另一个结点之间的分支构成这两个结点之间的路径;路径上的分枝数目称作路径长度,它等于路径上的结点数减1.
      2. 结点的权和带权路径长度:在许多应用中,常常将树中的结点赋予一个有着某种意义的实数,我们称此实数为该结点的权;结点的带权路径长度规定为从树根结点到该结点之间的路径长度与该结点上权的乘积.
      3. 树的带权路径长度:为树中所有叶子结点的带权路径长度之和,
    2. 构建过程
      1. 假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:

      2. 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);

      3. 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;

      4. 从森林中删除选取的两棵树,并将新树加入森林;

      5. 重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。

    3. 在建立好huffman树之后,就可以得到每一个叶子节点的huffman编码,这种编码是不等长编码,并且每个字符的编码都不是另外字符的前缀。
      1. 利用词库中,每个词出现的频率作为该词的权重,即{v1: w1, v2: w2, …, vn: wn}
      2. 从根节点开始,然后到每个叶子节点路径上的左子树分支赋予0,右子树分支赋予1,并从根到叶子节点的方向形成叶子节点的编码。
## huffman树的构建过程

# 构建一个节点类
class Node(object):
    def __init__(self, name=None, value=None):
        self._name = name
        self._value = value
        self._left = None
        self._right = None

class HuffmanTree(object):
#根据10Huffman树的思想:以叶子节点为基础,反向建立Huffman树
    """构建huffman树:
    输入:以(nm, wei) 为元素的嵌套列表,tuple, 或者字典
    """
    
    def __init__(self, data):
        
        # 使用辅助节点列表
        self.a = [Node(_[0], _[1]) for _ in data]
        
        # 由底向上建树,直到根节点
        while len(self.a) != 1:
            
            # 两个数据加了之后,可能会发生变化,所以要重新排列
            self.a.sort(key=lambda x: x._value, reverse=True)
            min_1 = self.a[-1]
            min_2 = self.a[-2]
            
            tmp_node = Node(name=min_1._name+min_2._name, 
                           value = min_1._value+min_2._value)
            tmp_node._left = min_1
            tmp_node._right = min_2
            
            self.a.remove(min_1)
            self.a.remove(min_2)
            
            self.a.append(tmp_node)
        self.root = self.a[0]
        self.encode = list(range(10))

        
    # 只要初始化类,树就已经建立完了
    
    # 递归建立树
    def prev(self, node, length):
        
        if not node:
            return None
        elif node._name:
            print(node._name + '   encoder:')
            for i in range(length):
                print(self.encode[i], end=' ')
            
        self.encode[length] = 0
        self.prev(node._left, length+1)
        self.encode[length] = 1
        self.prev(node._right, length+1)
        
   # 生成huffman编码
    def get_code(self):
        self.prev(self.root, 0)
        
test =  [('a',5),('b',4),('c',10),('d',8),('f',15),('g',2)]   
tree = HuffmanTree(test)
print(tree.get_code())

2. 负采样

  1. 是对高频词进行抽样的过程,一个词出现频率Z(w)越高,越有可能会被抽到(抽到的概率P(wi)越大)
  2. 如何选择negative words
    1. 使用一元模型分布(unigram distribution) 来选择“negative words”。一个单词被选择为negative sample的概率与它出现的频率有关。出现的频率越高,越容易被选择为negative words。
    2. 在代码中,每个单词被选择为negative words的概率表达公式为:
      1. P ( w i ) = f ( w i ) 3 4 ∑ j = 0 n f ( w j ) 3 4 P(w_i)=\frac{f(w_i)^{\frac{3}{4}}}{\sum_{j=0}^nf(w_j)^{\frac{3}{4}}} P(wi)=j=0nf(wj)43f(wi)43
      2. 可以看到每个单词被选择为negative words 的概率与其出现的频率有关。
      3. C语言实现负采样:有个一unigram table 包含了一亿个元素的数组,这个数组由词汇表中每个单词的索引来填充,并且这个数组中有重复,也就是说有些单词会出现多次,那么每个单词的索引在这数组中出现的次数如何决定的呢?有公式: P ( w i ) ∗ t a b l e S i z e P(w_i)*tableSize P(wi)tableSize,也就是计算出的负样本的概率*1亿=单词在表中出现的次数。
      4. 有了这张表以后,每一次进行负采样的时候,只需要在1~1亿范围内生成一个随机数,然后选择表中索引号为这个随机数的那个单词作为我们的negative word即可。一个单词负采样的概率越大,那么他在这个表中出现的概率越大,被选作负样本的概率越大。
### 负采样的实现过程
from collections import Counter
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns

np.random.seed(10)
corpus = list('abcdefghijklmn')
rand = np.random.randint(low=50, high=600, size=len(corpus))
cnt = [int(6000/_) for _ in rand]

word_couter_ = {k: v for k, v in zip(corpus, cnt)}

# 计算每个单词出现的频率
word_couter = {k: v/sum(cnt) for k, v in word_couter_.items()}

# posistive and negative sample
pos_sam = 'k'
neg_sams = [_ for _ in corpus if _ != pos_sam]

sum_ = sum([np.power(_, 0.75) for _ in word_couter.values()])
word_proba = {k: round(np.power(v, 0.75)/sum_, 7) for k, v in word_couter.items()}

table_size = 1e+7
uni_table = []
for index, word in enumerate(corpus):
    uni_table.extend([index] * int(word_proba[word] * table_size))


# 进行负采样
neg_num = 5
neg_word = []
for neg in range(neg_num):
    rand_index = np.random.randint(0, table_size-1, 1)
    word_index = uni_table[rand_index[0]]
    neg_word.append(corpus[word_index])
    
print('选择的负样本为:', neg_word)

Word2Vec 的API使用

参数说明
ID参数说明
1sentences可以是一个list,对于大语料集,建议使用BrownCorpus,Text8Corpus或lineSentence构建。
2size是指特征向量的维度,默认为100。
3alpha是初始的学习速率,在训练过程中会线性地递减到min_alpha。
4window窗口大小,表示当前词与预测词在一个句子中的最大距离是多少。
5min_count可以对字典做截 断. 词频少于min_count次数的单词会被丢弃掉, 默认值5。
6max_vocab_size设置词向量构建期间的RAM限制,设置成None则没有限制。
7sample高频词汇的随机降采样的配置阈值,默认为1e-3,范围是(0,1e-5)。
8seed用于随机数发生器。与初始化词向量有关。
9workers用于控制训练的并行数。
10min_alpha学习率的最小值。
12sg用于设置训练算法,默认为0,对应CBOW算法;sg=1则采用skip-gram算法。
12hs如果为1则会采用hierarchica·softmax技巧。如果设置为0(默认),则使用negative sampling。
13negative如果>0,则会采用negativesampling,用于设置多少个noise words(一般是5-20)。
14cbow_mean如果为0,则采用上下文词向量的和,如果为1(default)则采用均值,只有使用CBOW的时候才起作用。
15hashfxnhash函数来初始化权重,默认使用python的hash函数。
16ite迭代次数,默认为5。
17trim_rule用于设置词汇表的整理规则,指定那些单词要留下,哪些要被删除。可以设置为None(min_count会被使用)。
18sorted_voca如果为1(默认),则在分配word index 的时候会先对单词基于频率降序排序。
19batch_words每一批的传递给线程的单词的数量,默认为10000。

模型训练过程

# 训练模型
params = {
    'sentences': train_list,
    'iter': 50,
    'min_count': 1, 
    'size': 128, 
    'workers': 10,
    'sg': 1,
    'hs': 1,
    'negative': 20,
    'window': 3}

model = Word2Vec(sentences=params['sentences'], size=params['size'],
                 alpha=0.025, window=params['window'], 
                 min_count=params['min_count'], workers=params['workers'],
                negative=params['negative'])
model.save('./model/w2v.pkl')

rec_dict = dict()
for goods in tqdm(all_goods):
    
    ret = model.most_similar(positive=goods, topn=20)
    tmp_dict = {k: v for k, v in ret}
    rec_dict[goods] = tmp_dict
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值