【新闻文本分类】(task4)使用gensim训练word2vec

简介: Word2vec即“word to vector”,是一个生成对“词”的向量表达的模型。想要训练 Word2vec 模型,我们需要准备由一组句子组成的语料库。假设其中一个长度为 T 的句子包含的词有 w1,w2……wt,并且我们假定每个词都跟其相邻词的关系最密切。

一、Word2vec算法

自从 2013 年谷歌提出 Word2vec 以来,Embedding 技术从自然语言处理领域推广到广告、搜索、图像、推荐等几乎所有深度学习的领域,成了深度学习知识框架中不可或缺的技术点。Word2vec是经典的 Embedding 方法。

1.1 CBOW 和 Skip-gram

Word2vec即“word to vector”,是一个生成对“词”的向量表达的模型。

想要训练 Word2vec 模型,我们需要准备由一组句子组成的语料库。假设其中一个长度为 T 的句子包含的词有 w1,w2……wt,并且我们假定每个词都跟其相邻词的关系最密切。

根据模型假设的不同,Word2vec 模型分为两种形式,CBOW 模型(图 3 左)和 Skip-gram 模型(图 3 右)。

image.png

图3 Word2vec的两种模型结构CBOW和Skip-gram

(1)CBOW 模型假设句子中每个词的选取都由相邻的词决定,因此我们就看到 CBOW 模型的输入是 wt周边的词,预测的输出是 wt。

CBOW是用周围词预测中心词,训练过程中其实是在从output的loss学习周围词的信息也就是embedding,但是在中间层是average的,一共预测V次;

(2)Skip-gram 模型则正好相反,它假设句子中的每个词都决定了相邻词的选取,所以你可以看到 Skip-gram 模型的输入是 wt,预测的输出是 wt周边的词。按照一般的经验,Skip-gram 模型的效果会更好一些,所以下面会以 Skip-gram 作为框架,来讲Word2vec 的模型细节。

Skip-gram是用中心词预测周围词,对每一个中心词都有K个词作为output,对一个词的预测有K次,所以能够更有效的从context中学习信息,共预测K*V次,因此,skip-gram的训练时间更长。

总结:鉴于skip-gram学习的词向量更细致。当数据量较少或者语料库中有大量低频词时,使用skip-gram学习比较合适。

1.2 Word2vec的样本怎样生成

作为一个自然语言处理的模型,训练 Word2vec 的样本当然来自于语料库,比如我们想训练一个电商网站中关键词的 Embedding 模型,那么电商网站中所有物品的描述文字就是很好的语料库。

我们从语料库中抽取一个句子,选取一个长度为 2c+1(目标词前后各选 c 个词)的滑动窗口,将滑动窗口由左至右滑动,每移动一次,窗口中的词组就形成了一个训练样本。根据 Skip-gram 模型(中心词决定了它的相邻词),就可以根据这个训练样本定义出 Word2vec 模型的输入和输出,输入是样本的中心词,输出是所有的相邻词。

【example】:这里选取了“Embedding 技术对深度学习推荐系统的重要性”作为句子样本。

(1)我们对它进行分词、去除停用词的过程,生成词序列;

(2)再选取大小为 3 的滑动窗口从头到尾依次滑动生成训练样本;

(3)然后我们把中心词当输入,边缘词做输出,就得到了训练 Word2vec 模型可用的训练样本。

image.png

图4 生成Word2vec训练样本的例子

1.3 Word2vec模型结构

它的结构本质上就是一个三层的神经网络(如图 5)

image.png

图5 Word2vec模型的结构

它的输入层和输出层的维度都是 V,这个 V 其实就是语料库词典的大小。假设语料库一共使用了 10000 个词,那么 V 就等于 10000。根据图 4 生成的训练样本:

(1)输入向量自然就是由输入词转换而来的 One-hot 编码向量;

(2)输出向量则是由多个输出词转换而来的 Multi-hot 编码向量。

显然,基于 Skip-gram 框架的 Word2vec 模型解决的是一个多分类问题。

隐层的维度是 N,N 的选择就需要一定的调参能力了,需要对模型的效果和模型的复杂度进行权衡,来决定最后 N 的取值,并且最终每个词的 Embedding 向量维度也由 N 来决定。

激活函数:注意隐层神经元是没有激活函数的,或者说采用了输入即输出的恒等函数作为激活函数,而输出层神经元采用了 softmax 作为激活函数。为什么要这样设置 Word2vec 的神经网络,为什么要这样选择激活函数呢?因为这个神经网络其实是为了表达从输入向量到输出向量的这样的一个条件概率关系:


image.png

image.png

这个由输入词 WI 预测输出词 WO 的条件概率,其实就是 Word2vec 神经网络要表达的东西。通过极大似然的方法去最大化这个条件概率,就能够让相似的词的内积距离更接近,这就是我们希望 Word2vec 神经网络学到的。

如果你对数学和机器学习的底层理论没那么感兴趣的话,也不用太深入了解这个公式的由来,因为现在大多数深度学习平台都把它们封装好了,你不需要去实现损失函数、梯度下降的细节,你只要大概清楚他们的概念就可以了。

【注意】Word2vec 还有很多值得挖掘的东西,比如

为了节约训练时间,Word2vec 经常会采用负采样(Negative Sampling)或者分层 softmax(Hierarchical Softmax)的训练方法。

关于这一点,推荐阅读《Word2vec Parameter Learning Explained》这篇文章,有最详细和准确的解释。

二、把词向量从 Word2vec 模型中提取出来

在训练完 Word2vec 的神经网络之后,我们想得到每个词对应的 Embedding 向量,这个 Embedding 在哪呢?其实它就藏在输入层到隐层的权重矩阵 W V × N W_{V\times N}W

V×N

 中。

图中橙色的部分,Embedding Matrix。

image.png

图6 词向量藏在Word2vec的权重矩阵中

这个W V × N W_{V\times N}W

V×N

的意思是W权重矩阵,下角标是(VxN),也就是输入层和隐藏层的权重矩阵吧! 然后我们一直在说embedding,那么embedding在哪儿呢? 如果看一下维度的话,我们输入是一个10000维的词的one-hot编码,那么这里的V就是10000,我们的输入应该是VxV的,那么我们的隐藏层有N个神经元,那么我们的权重矩阵不就是VxN的咯?而我们在python代码里运行torch.nn.Embedding()时候,第一个参数是输入维度,第二个参数是隐藏层维度,所以也就是说 我们习惯取这样的输入和隐藏层之间的权重矩阵为我们的Embedding矩阵。

输入向量矩阵 W V × N W_{V\times N}W

V×N

 的每一个行向量对应的就是我们要找的“词向量”(即上图中橙色矩阵中的每行深色橙色向量)。

比如我们要找词典里第 i 个词对应的 Embedding,因为输入向量是采用 One-hot 编码的,所以输入向量的第 i 维就应该是 1,那么输入向量矩阵 W V × N W_{V\times N}W

V×N

 中第 i 行的行向量自然就是该词的 Embedding 。

输出向量矩阵 W′ 也遵循这个道理,确实是这样的,但一般来说,我们还是习惯于使用输入向量矩阵(即这里的W V × N W_{V\times N}W

V×N

)作为词向量矩阵。

2.1 提取词向量

在实际的使用过程中,我们往往会把输入向量矩阵转换成词向量查找表(Lookup table,如图 7 所示)。例如,输入向量是 10000 个词组成的 One-hot 向量,隐层维度是 300 维,那么输入层到隐层的权重矩阵为 10000x300 维。在转换为词向量 Lookup table 后,每行的权重即成了对应词的 Embedding 向量。如果我们把这个查找表存储到线上的数据库中,就可以轻松地在推荐物品的过程中使用 Embedding 去计算相似性等重要的特征了。

image.png

图7 Word2vec的Lookup table

2.2 Word2vec 对 Embedding 技术的意义

Word2vec 是由谷歌于 2013 年正式提出的,其实它并不完全是原创性的,学术界对词向量的研究可以追溯到 2003 年,甚至更早的时期。但正是谷歌对 Word2vec 的成功应用,让词向量的技术得以在业界迅速推广,进而使 Embedding 这一研究话题成为热点。Word2vec 对深度学习时代 Embedding 方向的研究具有奠基性的意义。

从另一个角度来看,Word2vec 的研究中提出的模型结构、目标函数、负采样方法、负采样中的目标函数在后续的研究中被重复使用并被屡次优化。掌握 Word2vec 中的每一个细节成了研究 Embedding 的基础。

三、代码实践

这里继续天池的入门赛题——新闻分类。

用10折交叉验证,将数据分为10份,9份训练1份验证。

(1)用到了训练好的词向量文件:词向量下载链接: https://pan.baidu.com/s/1ewlck3zwXVQuAzraZ26Euw 提取码: qbpr

(2)all_data2fold函数的作用:针对类别不均衡问题【task1中看到的有的类别数据多,有的类别数据少】,所有类别进行平均分为10份再放在一起,这样其中任意9份做训练集,另外一份做测试集,的到的模型鲁棒性会更好。

(3)注意要将Word2vec函数的参数size改为vector_size。

# -*- coding: utf-8 -*-
"""
Created on Wed Nov 10 20:08:40 2021
@author: 86493
"""
import logging
import random
import numpy as np
import torch
logging.basicConfig(level=logging.INFO, format='%(asctime)-15s %(levelname)s: %(message)s')
# set seed 
seed = 666
random.seed(seed)
np.random.seed(seed)
torch.cuda.manual_seed(seed)
torch.manual_seed(seed)
# split data to 10 fold
fold_num = 10
data_file = 'train_set.csv'
import pandas as pd
def all_data2fold(fold_num, num=10000):
    fold_data = []
    f = pd.read_csv(data_file, sep='\t', encoding='UTF-8')
    # 只取前10000条数据 原来是的shape(200000,2)
    texts = f['text'].tolist()[:num]    
    # texts,lables都变成了list,里面有10000个元素
    labels = f['label'].tolist()[:num]  
    # 总的数据量 与num相等
    total = len(labels)  
    index = list(range(total))
    #打乱这个包含索引的list
    np.random.shuffle(index)  
    all_texts = []
    all_labels = []
    # 在这个索引list里 
    for i in index: 
        all_texts.append(texts[i])
        # 用这些打乱的索引,去取原来的texts 和 labels 里的值,
        # 此时他们也相应变成了all_texts ,all_labels
        all_labels.append(labels[i]) 
    # 给这个label—id建立一个字典 ,key是label,value是一个列表,
    # 元素是label在 all_labels中的位置索引
    label2id = {} 
    for i in range(total):
        label = str(all_labels[i])
        if label not in label2id:
            label2id[label] = [i]
        else:
            # 同一个label会出现多条,所以他在all_labels中有多个索引位置
            label2id[label].append(i)  
    # 根据fold_num,建立相等长度的列表,目前列表里的元素为空。
    # 即all_index是一个长度为fold_num的list,元素目前为空。
    all_index = [[] for _ in range(fold_num)]
    # 在label-id这个字典里,data存的是label在all_labels中的位置索引,
    # 然后,每一个label每一个label的去循环
    for label, data in label2id.items(): 
        # print(label, len(data)) 
        # len(data)某个label出现的次数。将每个label出现的总次数 分成fold_num份,
        # 每一份batch_size个数据
        batch_size = int(len(data) / fold_num)  
        # 不能整除的剩下的数据,此处的other一定小于fold_num
        other = len(data) - batch_size * fold_num  
        # 某一折中,我们记为i
        for i in range(fold_num): 
            # 如果i小于剩下的数据个数,batch_size就增加1,把剩下的数据依次加进每一份里
            cur_batch_size = batch_size + 1 if i < other else batch_size 
            # print(cur_batch_size) #每个label 分成fold_num份后,每一份目前的数据量
            batch_data = [data[i * batch_size + b] for b in range(cur_batch_size)]
            #取出当前的数据,data里是label的索引位置。 i * batch_size用来跳过以前的batch_size,跳到现在这一折的位置
            all_index[i].extend(batch_data)#添加到all_index的相应位置中  
            #all_index数据形式:[[],[],...[]]-->[[label_1_1折 expand label_2_1折 expand label_3_1折...expand label_19_1折 ],[label_1_2折 expand label_2_1折...],...[label_1_n折 expand ...]]
        #这样在每一折中都保证了,label的分布同原始数据一致
    # 总的数据量分成fold_num份,每一份大小为batch_size
    batch_size = int(total / fold_num) 
    other_texts = []
    other_labels = []
    other_num = 0
    start = 0
    for fold in range(fold_num):
        # num每一折的数据量
        num = len(all_index[fold]) 
        # all_texts是打乱后的text数据, all_index[fold]代表第fold折的数据
        texts = [all_texts[i] for i in all_index[fold]]  
        labels = [all_labels[i] for i in all_index[fold]] 
        '''
          以fold=0为例,   all_index[0]=[label_1_1折 expand label_2_1折 expand label_3_1折...expand label_19_1折 ]
          其中,label_1_1折 代表label——1划分到1折中的数据,数据的内容是label-1在原始数据中的索引位置 data[i * batch_size + b]
          所以,all_index[0] 代表是是,各个label划分到1折中的数据,数据内容是每个label在原始数据中的索引位置
          然后,利用这些索引位置,在all_texts和all_labels中取到真正的数据
        '''
        #如果这一折的数量>batch_size
        if num > batch_size: 
            # 取前batch_size个数据
            fold_texts = texts[:batch_size] 
            # 之后的数据放到 other_texts中
            other_texts.extend(texts[batch_size:]) 
            fold_labels = labels[:batch_size]
            # label也这样操作
            other_labels.extend(labels[batch_size:]) 
            # 统计每一折中在batch_size中放不开的数据总量
            other_num += num - batch_size 
        #如果这一折的数量<batch_size
        elif num < batch_size:
            # batch_size比num多的数据量
            end = start + batch_size - num 
            # 从others_texts里补上
            fold_texts = texts + other_texts[start: end] 
            fold_labels = labels + other_labels[start: end]
            # 移动在others_texts中的start位置
            start = end 
        else:
            fold_texts = texts
            fold_labels = labels
        # assert函数主要是用来声明某个函数是真的,
        # 当assert()语句失败的时候,就会引发assertError
        assert batch_size == len(fold_labels) 
        # 这里将batch_size的大小与每一折的大小弄成一样的  
        # 数据存到fold_texts和fold_labels里
        # shuffle
        index = list(range(batch_size))
        # 打乱batch_size
        np.random.shuffle(index)  
        shuffle_fold_texts = []
        shuffle_fold_labels = []
        for i in index:
            shuffle_fold_texts.append(fold_texts[i])
            # 打乱索引再去取值   fold_texts和fold_labels 变成了  
            # shuffle_fold_texts和shuffle_fold_labels
            shuffle_fold_labels.append(fold_labels[i]) 
        # 一折中的数据
        data = {'label': shuffle_fold_labels, 'text': shuffle_fold_texts} 
        # 每一折的数据现在变成了上面的data字典,然后加入到fold_data列表中。
        fold_data.append(data) 
    #打印每一折数据的大小
    logging.info("Fold lens %s", str([len(data['label']) for data in fold_data]))  
    return fold_data
#fold_data = all_data2fold(10)
fold_data = all_data2fold(10)
# build train data for word2vec
fold_id = 9
train_texts = []
for i in range(0, fold_id):
    data = fold_data[i]
    train_texts.extend(data['text'])
logging.info('Total %d docs.' % len(train_texts))
logging.info('Start training...')
from gensim.models.word2vec import Word2Vec
# Word vector dimensionality
num_features = 100    
# Number of threads to run in parallel 
num_workers = 1       
train_texts = list(map(lambda x: list(x.split()), train_texts))
model = Word2Vec(train_texts, workers=num_workers, vector_size=num_features)
model.init_sims(replace=True)
# save model
model.save("./word2vec.bin")
# load model
model = Word2Vec.load("./word2vec.bin")
# convert format
model.wv.save_word2vec_format('./word2vec.txt', binary=False)

生成的word2vec文件:

image.png

相关文章
|
1月前
|
机器学习/深度学习 数据采集 自然语言处理
如何使用 Word2Vec 模型进行情感分析?
【10月更文挑战第5天】如何使用 Word2Vec 模型进行情感分析?
|
3月前
|
自然语言处理
【NLP】如何实现快速加载gensim word2vec的预训练的词向量模型
本文探讨了如何提高使用gensim库加载word2vec预训练词向量模型的效率,提出了三种解决方案:保存模型以便快速重新加载、仅保存和加载所需词向量、以及使用Embedding工具库代替word2vec原训练权重。
211 2
|
6月前
|
机器学习/深度学习 自然语言处理 算法
Coggle 30 Days of ML(23年7月)任务六:训练FastText、Word2Vec词向量
Coggle 30 Days of ML(23年7月)任务六:训练FastText、Word2Vec词向量
|
机器学习/深度学习 自然语言处理 算法
深度学习基础入门篇10:序列模型-词表示{One-Hot编码、Word Embedding、Word2Vec、词向量的一些有趣应用}
深度学习基础入门篇10:序列模型-词表示{One-Hot编码、Word Embedding、Word2Vec、词向量的一些有趣应用}
深度学习基础入门篇10:序列模型-词表示{One-Hot编码、Word Embedding、Word2Vec、词向量的一些有趣应用}
|
自然语言处理 数据可视化 数据处理
基于gensim实现word2vec模型(附案例实战)
基于gensim实现word2vec模型(附案例实战)
912 1
基于gensim实现word2vec模型(附案例实战)
|
机器学习/深度学习 存储 自然语言处理
基于 word2vec TextCNN 的新闻文本分类
基于 word2vec TextCNN 的新闻文本分类
345 0
基于 word2vec TextCNN 的新闻文本分类
|
机器学习/深度学习 自然语言处理 算法
GPT-3 vs Bert vs GloVe vs Word2vec 文本嵌入技术的性能对比测试
本文将GPT3与三种传统文本嵌入技术GloVe、Word2vec(Mikolov ,2013 年)和 BERT生成的嵌入进行性能的简单对比。
641 0
GPT-3 vs Bert vs GloVe vs Word2vec 文本嵌入技术的性能对比测试
|
自然语言处理 搜索推荐 算法
word2vec模型原理及实现词向量训练案例(二)
word2vec模型原理及实现词向量训练案例
489 0
word2vec模型原理及实现词向量训练案例(二)
|
机器学习/深度学习 Serverless Windows
word2vec模型原理及实现词向量训练案例(一)
word2vec模型原理及实现词向量训练案例
240 0
word2vec模型原理及实现词向量训练案例(一)
【新闻文本分类】(task2)文本表示(CBOW和TF-IDF)
Bag of words词袋表示,又称为CountVectors或者CBOW,用它表示成对应的文本向量时,每个向量的元素对应该该维对应的词在文本中出现的次数。显然这种表示方法木有考虑词的顺序信息,没有融入上下文的信息。 下面来看下它的实现吧:
230 0
【新闻文本分类】(task2)文本表示(CBOW和TF-IDF)