句子文本数据如何作为机器学习(深度学习)模型的输入(pytorch)

在我们做机器学习/深度学习时,如何表示一个文本数据并让计算机理解呢,很多深度学习框架,如pytorch,在接受文本数据时,我们都会采用Embedding层作为第一层,那它的作用是啥呢?

以中文为例,在这之前,我们都会根据数据集中的所有文本数据构建出一个高频字/词典,中文一般都是构建字典,也就是将句子进行字符级切分,构成字典。

比如,给定句子:“我我我我是是是你你爸爸,爸爸”,构建出来的字典文件如下:

<PAD> 0
我 1
是 2
你 3
爸 4
,5
<UNK> 6

说明:

  1. 每个字后面都对应一个索引,表示当前字的排列位置,在字典文件中写不写这个索引值随你自己,不过我们在具体代码实现时,在python中会有两个dict类型变量来加载所生成的字典文件中的内容:char2id(或word2id)(根据字获取此字的索引值)、id2char(或id2word)(根据索引值获取具体字)
  2. 在上述字典文件中,多了几个在句子中没有出现的字,<PAD>表示填充(Padding)字符,因为在进行文本数据输入时,如RNN网络,每次输入都是一个句子序列,但每个句子长度不一定都是一样的,为了统一长度(这个长度根据情况你自己定),长句进行截断,短句进行填充,填充的字符就是<PAD>。虽然我们构建了字典,但是舍弃了一些低频词,所以明显是有可能遇到字典中没有的字,那么这个词就用<UNK>表示。在实际编码过程中,根据情况添加这些类似的字符是很常见的。
  3. 我们通常不会将数据集中的出现的所有字加进字典,只会采用出现频率最高的字。具体做法就是,统计这些所有字出现的频率次数,按照次数进行降序排列,取前n(根据情况定义)个字。
  4. 其实也不一定自己构建字典,也可以下载一份字典,如开源bert模型就提供了一份字典。

有了字典我们如何表示文本呢?

one-hot向量表示法

表示一个字(我),以上面讲解的句子和字典为例。

  1. 令字典长度为len_dict = 7,初始化一个维度为len_dict = 7的全零向量(数组)vec = [0,0,0,0,0,0,0]
  2. 获取要表示的字()在字典中的索引,为index = 1
  3. 将向量vec中对应索引index位置的值置为1,vec[index] = 1。
  4. 最终字“”的one-hot表示为[0,1,0,0,0,0,0]

如果用此种方法表示句子的话,就将句子中的每个字的one-hot表示出来就行了。如果句子长度为len_sen,那么就会得到一个大小为len_sen * len_dict 的矩阵,每一行就代表句子中对应字的one-hot表示。

很明显,one-hot方法只是单纯的表示了一个字/词,无法体现一个字词的其它隐藏特征,如字词间的相关性。并且如果模型整个后续过程都采用one-hot表示的数据进行计算,那会带来很大的计算压力。

所以为了获取更低纬度的文本特征,出现word2vec方法。

word2vec

本文不讲解具体的word2vec实现原理及方法,相关论文可参考word2vec 相关论文。总的来说,就是采用更低纬度的向量来表示一个字/单词。如下图:
在这里插入图片描述
上图表明了经过word2vec之后,每个字/词的向量维度降低了(这个维度又称为word_embedding_dim,这个数值是自己定义的,我一般设置成300)。总的来说,我们可以用更低维的向量表示无穷多的字/词。
为什么呢?假设字/词典大小为len_dict,那么one-hot向量维度也为len_dict,但one-hot向量中每次只能有一个为1,其余为0,来是一个字/词,最多只能表示len_dict个字/词。但经过word2vec后,其中每个向量中的数值可以为任意实数,所以可以表示为无穷个字/词。

那么有哪些方法获得这份word2vec后向量呢?
1、训练gensim模块的Word2Vec模型获取。
前提: 首先你需要一份文本数据集,如果是一篇篇文章,要进行分句处理。然后对句子进行字符级划分。最后将数据集构建成如下形式送入模型即可。
在这里插入图片描述

from gensim.models import Word2Vec
import numpy as np
import os
word_embedding_dim = 250
x = []  #这个你要按照上面图片的例子构建数据
# size 表示经过word2vec之后的每个字/词向量的大小。sg表示具体实现训练的模型的是哪个(1 for skip-gram; otherwise CBOW.),如果你有了解过我上面给出的论文,你会知道我在说什么。
model = Word2Vec(x, size = word_embedding_dim , window = 5, min_count = 5, iter = 10, sg = 1)  

#模型训练完之后(需要一些时间,具体根据数据量和设置参数而定)。可对其进行保存和加载。
#model.save(os.path.join('./', 'w2v_all.model'))
#model =  Word2Vec.load(os.path.join('./', 'w2v_all.model'))

#我们在训练时并没设置字典这些,它会根据我们输入的数据X自动生成字典。下面演示如何获取字典,和word2vec后的字/词向量。
word2idx = {}
idx2word = []
embedding_matrix = []  #word2vec后的字/词向量,最后大小为len(word2idx) * word_embedding_dim 
for i ,word in enumerate(model.wv.vocab):
	word2idx[word] = len(word2idx)
	idx2word.append(word)
	embedding_matrix.append(model[word])

#在这里你可能会要问,像<PAD>、<UNK>这些字符并没在里面啊。在此我们给他加进去,但不要忘了同时还要加上word2vec后的对应的向量,这个随机初始化一个就行了。
def add_embedding(word):
    # 把 word 加进embedding,并赋予他一个随机生成的 representation vector
    # word 只会是 "<PAD>" 或 "<UNK>"
    #np.random.uniform(0, 1, word_embedding_dim) 产生word_embedding_dim个在区间[0,1)中的服从均匀分布的数据。
    vector = np.random.uniform(0, 1, word_embedding_dim)
    word2idx[word] = len(word2idx)
    idx2word.append(word)
    embedding_matrix.append(vector)
    
#对于其它和上下文有意义的字符不建议这么做
add_embedding("<PAD>")
add_embedding("<UNK>")
print(word2idx)
print(idx2word)
print(np.array(embedding_matrix).shape)

注意: 在模型初始化过程中,我们设置了一个参数min_count = 5,这个5其实也是默认值。这个参数代表它自动生成的字/词典中的字/词在你输入文本中的出现频率如果小于min_count,它将忽略这个字/词。也就是说,如果你仅仅是为了练习练习代码,用几个句子数据测试,很可能报下列错误:

RuntimeError: you must first build vocabulary before training the model

也就是说,你的字/词频率都小于min_count,全都丢弃了,字/词典为空。练练代码的话将min_count设置小一点就行了。做实验的话,数据量一般够多,不会出现这种情况。

总的来说,word2vec不仅降低了字/词向量表示维度,且提炼出了这些字/词之间的相关性。如果你的训练数据够多,你会发现一些同义词在向量空间中挨得比较近(利用余弦相似度计算字/词的相似性)。你也可以对输出的所有字/词的word2vec向量,每个词向量取其中的任意对应两维数(比如都取1,3列),画在x,y轴座标空间里面,你会发现相关字/词是挨着的。更准确的,你也可以采用PCA、Tsne对高维数据进行可视化。

Embedding

在很多深度学习框架中,如pytorch,都会采用Embedding层作为文本数据输入第一层。如果已经看完我上面所讲的Word2vec,其实,embedding表面上和word2vec一样,将一个字/词变成更低纬度的向量了。只不过他们具体实现的方式不一样,word2vec采用的无监督学习(我们只给它输入了大量文本而已),embedding是有监督的更新权重向量。比如文本情感分类,首先随机初始化一个embedding字/词向量,然后根据预测结果和真实结果的损失函数求导结果反向传播更新embedding字/词向量。

pytorch中的embedding层使用方式:
前提: 根据所有数据集生成一个字典,<PAD>这些字符根据情况都要考虑进去(也可以自己去下一个)。
一般,embedding层接收的数据集格式是这样的:
在这里插入图片描述
如果上述矩阵为A,那么A(i,j)就代表第i行句子中的第j个字/词在字典中的索引值。

我们进行数据训练时,都是一批一批数据进行训练的,这个batch_size大小是我们自定义的,也就是我们每次将batch_size个句子一次性送入模型训练。 句子长度也是我们统一自定义的。

输出数据格式是这样的:
在这里插入图片描述

import torch
import torch.nn as nn

#字典
char2id = {'是':0,'我':1,'你':2,'<PAD>':3,'<UNK>':4}
# num_embeddings = 字典长度 , embedding_dim = 生成的每个字/词向量维度
num_embeddings = 5
embedding_dim = 4
#我们只有一句文本,所以batch_size为1,句子"我是你"长度sentence_len = 3,所以inputs的shape = (1,3)
#句子"我是你"的字索引表示为[1,0,2]
inputs = torch.tensor([[1,0,2]])  #inputs.shape = (batch_size, sentence_len)
embedding = torch.nn.Embedding(num_embeddings, embedding_dim)
outputs = embedding(inputs)
print(outputs, outputs.shape)   #outputs.shape = (batch_size, sentence_len, embedding_dim)
tensor([[[-0.3039, -0.7565, -0.2139, -1.3951],
         [ 1.0783,  0.1729, -1.0732,  0.9884],
         [ 0.0366,  0.4481, -1.8975,  2.4956]]], grad_fn=<EmbeddingBackward>) torch.Size([1, 3, 4])

embedding层之后呢,一般都接RNN等类型网络啦。上面这个outputs输出会根据后续网络反向传播进行迭代训练更新,第一次只是程序自动随机初始化的一个权重向量。

在Embedding 中使用预训练好了Word2vec字/词向量

那么怎么在embedding中使用通过word2vec训练好了的字/词向量呢。过程很简单,通过以下代码即可实现。
前提: 我们已经通过前面的Word2vec获取到了word2idx字典,
idx2word列表、embedding_matrix字/词向量矩阵。这个矩阵的行索引是和列表idx2word是对应的,对于一个字索引值id(id ∈ [0,len(word2idx) ) 且为整数)。那么可以通过 idx2word[id] 获取到此索引值的具体字/词,可通过 embedding_matrix[id] 可以获取到此字/词的对应的word2vec后的向量。

embedding = torch.nn.Embedding(len(embedding_matrix), len(embedding_matrix[0]))  
embedding.weight = torch.nn.Parameter(torch.tensor(embedding_matrix))
embedding.weight.requires_grad = False #设置为False表明在模型训练过程中,不更新此权重向量,直接使用我们自己设置的Word2vec预训练权重向量。当然你也可以设置成True从而进行微调。

总结

在深度学习中,文本数据的最开始的输入还是字索引形式,也就是one-hot表示,word2vec、embedding这些就是一个文本隐藏特征抽取过程,一般放在整个网络的第一层,我们说的这些字词向量就是这个模型训练完成之后的权重(参数)向量。当然不同的方式还是差异的,如word2vec之后,这些向量能学习到字词之间的相关性。以及没有提到的elmo、bert、gpt等方式,能学习到字词的上下文语义特征,比如“苹果”一词在不同的文本上下中所代表的的意义不同,公司?or 水果?,但word2vec或许只能获取其中的一种含义,因为它是静态的,它的结果是一个固定了的向量矩阵。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章