从『词袋』到『词序列』
我们之前对于文档的数字化表示,采用的是向量空间模型(例如TF-IDF),又被形象地称为『词袋』模型(Bag-of-words model)。就像是把文档里的词汇,放入到以词典作为标签的袋子里。
我们可以看到,基于词袋模型的文档表示方法,虽然考虑了词的重要程度,但它只是根据词的统计特性表示一个文档,而没有考虑到词在文中的次序。
比方说有这样两句话:
•“熊二/的/哥哥/是/熊大。”
•“熊二/是/熊大/的/哥哥。”
这两句话的TF-IDF数据表示形式是一样的,但它们的语义是截然相反的。
究其原因,是因为词袋模型丢失了文档中的上下文信息。
这样就有了一个新的思路:将文档表示成词编码的一个序列,这样词在文档上下文关系信息就能够保留下来。
接下来我们尝试将文档转换成一个数字序列,方便电脑处理。
首先我们要根据语料库中出现的词建立一个词典,建立数字索引(Index),分别对应到特定的词汇。实际上,这个建立词典的步骤在之前的TF-IDF模型训练函数TfidfVectorizer中已经隐含地做过。但现在我们不再使用TF-IDF,因此需要把这一过程单独拿出来显性实现一下。
在李孟博士的原博客里,后续演示使用的是TensorFlow.Keras相关包,我们改为全部使用Pytorch演示相关功能的实现。
首先,把Jieba分词结果和语料库调入内存。
import pandas as pd
import pickle
pkl_file_rb = open(r'./save_file', 'rb')
train =pickle.load(pkl_file_rb)
corpus = pd.concat([train . title1_tokenized, train . title2_tokenized])
corpus = [c for c in corpus]
type(train[0:1])
pandas.core.frame.DataFrame
corpus[16].strip().split()
['男人', '在', '机舱', '口', '跪下', '原来', '一切', '都', '只', '因为', '爱']
定义一个迭代器对语料库中的分词数据进行预处理:
def yield_tokens(corpus):
for i in range(len(corpus)):
yield corpus[i].strip().split()
使用 torchtext 的 build_vocab_from_iterator 函数建立词典。
from torchtext.vocab import build_vocab_from_iterator
vocab = build_vocab_from_iterator(yield_tokens(corpus), min_freq=2, specials=["" ])
vocab.set_default_index(vocab['' ]) #在词典里设置一个默认词,用来对于OOV的生词做默认编码
vocab['熊二'], vocab['爱'],vocab['' ] #'熊二'在语料库里没有出现过
(0, 326, 0)
vocab.__len__()
61841
经过这一步骤,我们就可以将一个新闻标题文档表示为一个词典序编码的数字序列,如下所示:
vocab(['男人', '在', '机舱', '口', '跪下', '原来', '一切', '都', '只', '因为', '爱'])
[306, 16, 5905, 1724, 5474, 181, 1843, 14, 99, 487, 326]
在后续建立深度学习模型之前,我们将采用PyTorch建议使用的 torch.utils.data.DataLoader 方法对数据进行预处理。
首先我们定义2个匿名函数,用来对训练数据中的label和titleX_tokenized字段做个简单的处理。
label_to_index = { 'unrelated' : 0 , 'agreed' : 1 , 'disagreed' : 2 }
label_pipeline = lambda x : label_to_index [ x ]
text_pipeline = lambda x: vocab(x.strip().split())
import torch
import numpy as np
from torch.utils.data import Dataset
from torch.utils.data import DataLoader,TensorDataset
MAX_LEN = 30 #定义一个最大长度,用于对过长的标题进行截断
下面我们定义一个myDataSet 类,用于对之前的DataFrame train 进行预处理。
这里稍微有点复杂,对于程序比较生疏的我来说有些许困难,花了半天时间学习和试验才调通。
主要的逻辑是:
从train中提取title1_tokenized和title2_tokenized字段,转为词典编码序列,并根据最大长度MAX_LEN进行截断,作为将来模型输入的tensor;
提取label字段转换为0、1、2作为预测目标,作为将来模型输出tensor。
# 自定义一个myDataSet类 对输入数据进行预处理
class myDataset(Dataset):
def __init__(self, train_data, max_len=20, transform=None):
super().__init__()
x1_train = train_data.title1_tokenized.apply(text_pipeline)
x2_train = train_data.title2_tokenized.apply(text_pipeline)
y_train = train.label.apply(label_pipeline)
# 以下根据max_len参数,对长短不一的数字化标题序列进行截断和补齐
tmp = []
for x in (x1_train.values,x2_train.values):
tmp_x = []
for i in range(len(x)):
temp_x1 = []
for j in range(max_len):
temp_x1.append(x[i][j] if j < len(x[i]) else 0)
tmp_x.append(temp_x1)
tmp.append(tmp_x)
self.x = torch.from_numpy(np.transpose(np.asarray(tmp),[1,0,2]))
self.y = torch.from_numpy(np.asarray(y_train.values))
self.transform = transform
def __len__(self):
return len(self.y) # 数据集长度
def __getitem__(self, index):
x = self.x[index] # tensor类型
y = self.y[index]
if self.transform is not None:
x = self.transform(x) # 对输入进行某些变换
return x, y
实例化一个MyDataset 类的对象train_data,作为参数传入DataLoader 进行数据预处理。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
train_data = myDataset(train,max_len=MAX_LEN)
dataloader = DataLoader(train_data, batch_size=8, shuffle=False)
看一下处理后的数据样例和维度形式是否如我们所愿:
for x,y in dataloader:
print(x,y)
print("*"*80)
print(x.shape,y.shape)
break
tensor([[[ 218, 1269, 32, 1178, 5971, 24, 488, 2875, 116, 5568,
4, 1847, 2, 13, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 150, 8, 12895, 6168, 6345, 529, 44, 64, 740, 12,
488, 286, 13213, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[[ 4, 10, 47, 677, 2561, 4, 165, 34, 17, 47,
5153, 62, 15, 677, 4509, 3208, 23, 284, 1185, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 677, 3208, 1141, 284, 1185, 677, 23975, 8, 784, 4515,
16, 12858, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[[ 4, 10, 47, 677, 2561, 4, 165, 34, 17, 47,
5153, 62, 15, 677, 4509, 3208, 23, 284, 1185, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 3208, 1141, 284, 1185, 677, 390, 22, 953, 9816, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[[ 4, 10, 47, 677, 2561, 4, 165, 34, 17, 47,
5153, 62, 15, 677, 4509, 3208, 23, 284, 1185, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 3299, 677, 3208, 1141, 284, 1185, 677, 23975, 8, 22,
953, 45212, 120, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[[ 31, 319, 3373, 3057, 1, 94, 97, 3373, 3057, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 7, 2, 208, 15, 14291, 174, 50, 1627, 319, 122,
3373, 3057, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[[ 4, 10, 47, 677, 2561, 4, 165, 34, 17, 47,
5153, 62, 15, 677, 4509, 3208, 23, 284, 1185, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 677, 3208, 1141, 284, 1185, 23975, 8, 381, 284, 242,
4515, 1670, 12858, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[[ 7, 2220, 1, 2071, 7, 18165, 2188, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 2220, 78, 20, 101, 125, 7, 46, 1552, 641, 7,
3055, 4705, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[[ 4, 10, 47, 677, 2561, 4, 165, 34, 17, 47,
5153, 62, 15, 677, 4509, 3208, 23, 284, 1185, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 677, 3208, 1141, 284, 1185, 8, 381, 284, 242, 4515,
1670, 12858, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]],
dtype=torch.int32) tensor([0, 0, 0, 0, 1, 0, 0, 0])
********************************************************************************
torch.Size([8, 2, 30]) torch.Size([8])
好了,就到这儿吧。
本系列共12期,将分6天发布。相关代码已全部分享在我的github项目(moronism189/chinese-nlp-stepbystep)下,急的可以先去那里下载。