众所周知,Pytorch是现今非常流形的深度学习框架。而Torchtext是一个非官方的、为Pytorch提供文本数据处理的库。在自然语言处理尤其是RNN、LSTM等模型的应用方面具有重要意义。虽然torchtext主要是为Pytorch提供服务的,但是也可以用于其他框架比如Tensorflow、Keras等。 本文主要内容有:
## 导入相关包
import pandas as pd # pandas包,适合导入csv,tsv等数据
import torch
from torchtext import data, datasets # 需要从torchtext导入data,datasets
from torchtext.vocab import Vectors
from torch.nn import init
from sklearn.model_selection import train_test_split
import jieba # 用来预处理文本(分词等)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 选择Gpu或Cpu
# 查看文件 (./data是存放文件的路径)
train = pd.read_csv('./data/train.tsv', sep='\t')
test = pd.read_csv('./data/test.tsv', sep='\t')
print(train.head(5)) # 查看前5行
# 从训练集划分出测试集
train, val = train_test_split(train, test_size=0.2)
print(len(train))
train.to_csv("./data/train.csv", index=False)
val.to_csv("./data/val.csv", index=False)
Torchtext采用了一种声明式的方法来加载数据:你来告诉Torchtext你希望的数据是什么样子的,剩下的由torchtext来处理。下面是Field包含的一些参数:
sequential:是否把数据表示成序列,如果是False, 不能使用分词 默认值: True.
use_vocab: 是否使用词典对象. 如果是False 数据的类型必须已经是数值类型. 默认值: True.
init_token: 每一条数据的起始字符 默认值: None.
eos_token: 每条数据的结尾字符 默认值: None.
fix_length: 修改每条数据的长度为该值,不够的用pad_token补全. 默认值: None.
tensor_type: 把数据转换成的tensor类型 默认值: torch.LongTensor.
preprocessing:在分词之后和数值化之前使用的管道 默认值: None.
postprocessing: 数值化之后和转化成tensor之前使用的管道默认值: None.
lower: 是否把数据转化为小写 默认值: False.
tokenize: 分词函数. 默认值: str.split.
include_lengths: 是否返回一个已经补全的最小batch的元组和和一个包含每条数据长度的列表 . 默认值: False.
batch_first: Whether to produce tensors with the batch dimension first. 默认值: False.
pad_token: 用于补全的字符. 默认值: “”.
unk_token: 不存在词典里的字符. 默认值: “”.
pad_first: 是否补全第一个字符. 默认值: False.
stop_words: Tokens to discard during the preprocessing step. Default: None
重要的几个方法:
pad(minibatch): 在一个batch对齐每条数据
build_vocab(): 建立词典
numericalize(): 把文本数据数值化,返回tensor
# 定义Field
import jieba
def tokenizer(text): # 可以自己定义分词器,比如jieba分词。也可以在里面添加数据清洗工作
"分词操作,可以用jieba"
return [wd for wd in jieba.cut(text, cut_all=False)]
"""
field在默认的情况下都期望一个输入是一组单词的序列,并且将单词映射成整数。
这个映射被称为vocab。如果一个field已经被数字化了并且不需要被序列化,
可以将参数设置为use_vocab=False以及sequential=False。
"""
# 定义停用词
stopwords = open('./data/stopwords.txt', encoding='utf-8').read().strip().split('\n')
LABEL = data.Field(sequential=False, use_vocab=False)
TEXT = data.Field(sequential=True, tokenize=tokenizer, lower=True, stop_words=stop_words)
Dataset可以处理很多格式(tsv,csv,json…)的数据,具体参考datasets。当给定原始数据时,Field知道要做什么。Dataset要告诉Field它需要处理哪些数据。
# 定义Dataset
"""
我们不需要 'PhraseId' 和 'SentenceId'这两列, 所以我们给他们的field传递 None
如果你的数据有列名,如我们这里的'Phrase','Sentiment',...
设置skip_header=True,不然它会把列名也当一个数据处理。
我们需要把 ‘Phrase’,'Sentiment'列按照Field进行处理,即('Phrase', TEXT), ('Sentiment', LABEL)
注意:fields设置列的顺序要与原数据列的顺序一样。
"""
train, val = data.TabularDataset.splits(
path='./data', train='train.csv', validation='val.csv', format='csv', skip_header=True,
fields=[('PhraseId', None), ('SentenceId', None), ('Phrase', TEXT), ('Sentiment', LABEL)]
)
test = data.TabularDataset('./data/test.tsv', format='tsv', skip_header=True,
fields=[('PhraseId', None), ('SentenceId', None), ('Phrase', TEXT)])
# 查看生成的dataset
print(len(train),train[2].Phrase, train[2].Sentiment)
# (一行是一个example类型)
#['remains', ' ', 'oddly', ' ', 'detached'] 1
我们可以看到第6行的输入,它是一个Example对象。Example对象绑定了一行中的所有属性,可以看到,句子已经被分词了,但是没有转化为数字。这是因为我们还没有建立vocab,我们将在下一步建立vocab。
# 建立vocab(不需要加载预训练的词向量)
TEXT.build_vocab(train, val)
LABEL.build_vocab(train, val)
# 建立vocab(加载预训练的词向量,如果路径没有该词向量,会自动下载)
TEXT.build_vocab(train, vectors='./data/glove.6B.100d')#, max_size=30000)
# 当 corpus 中有的 token 在 vectors 中不存在时 的初始化方式.
TEXT.vocab.vectors.unk_init = init.xavier_uniform
现在每个词都对应一个索引值。如果你加载了词向量,你的每个词还会对应一个词向量。在后续RNN或LSTM模型中,可以直接获取该词嵌入向量。如果没加载,现在只有词索引而没有词向量,后续RNN或LSTM会随机生成一个词嵌入矩阵(self.embedding = nn.Embedding(vocab_size, embedding_dim))。这部分后续会介绍。
我们日常使用pytorch训练网络时,每次训练都是输入一个batch。torchtext可以构造一个batch的迭代器为模型提供输入。
# 构造迭代器
'''
sort_key指在一个batch内根据文本长度进行排序。
'''
train_iter = data.BucketIterator(train, batch_size=128, sort_key=lambda x: len(x.Phrase),
shuffle=True,device=DEVICE)
val_iter = data.BucketIterator(val, batch_size=128, sort_key=lambda x: len(x.Phrase),
shuffle=True,device=DEVICE)
# 在 test_iter , sort一定要设置成 False, 要不然会被 torchtext 搞乱样本顺序
test_iter = data.Iterator(dataset=test, batch_size=128, train=False,
sort=False, device=DEVICE)
# 查看trainiter一个batch
batch = next(iter(train_iter))
data = batch.Phrase
label = batch.Sentiment
print(data.shape)
print(batch.Phrase)
结果为:
torch.Size([95, 128])
'''
batch大小128,文本长度95(每个batch内的样本长度一样,但是每个batch的文本长度不一样).
如果在Field设置参数fix_length=100。那么所有数据的文本长度
都会成100(少的pad,多的截断)。
'''
tensor([[ 18, 494, 19, ..., 12363, 71, 20],
[ 2, 2, 2, ..., 2, 2, 2],
[ 15, 28, 11483, ..., 285, 1273, 796],
...,
[ 1, 1, 1, ..., 1, 1, 1],
[ 1, 1, 1, ..., 1, 1, 1],
[ 1, 1, 1, ..., 1, 1, 1]], device='cuda:0')
import jieba
import torch
from torchtext import data, datasets
from torchtext.vocab import Vectors
from torch.nn import init
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
from sklearn.model_selection import train_test_split
import pandas as pd
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
data = pd.read_csv('train.tsv', sep='\t')
test = pd.read_csv('test.tsv', sep='\t')
# create train and validation set
train, val = train_test_split(data, test_size=0.2)
train.to_csv("train.csv", index=False)
val.to_csv("val.csv", index=False)
spacy_en = spacy.load('en')
def tokenizer(text): # create a tokenizer function
return [wd for wd in jieba.cut(text, cut_all=False)]
# Field
TEXT = data.Field(sequential=True, tokenize=tokenizer, lower=True)
LABEL = data.Field(sequential=False, use_vocab=False)
# Dataset
train,val = data.TabularDataset.splits(
path='.', train='train.csv',validation='val.csv', format='csv',skip_header=True,
fields=[('PhraseId',None),('SentenceId',None),('Phrase', TEXT), ('Sentiment', LABEL)])
test = data.TabularDataset('test.tsv', format='tsv',skip_header=True,
fields=[('PhraseId',None),('SentenceId',None),('Phrase', TEXT)])
# build vocab
TEXT.build_vocab(train, vectors='glove.6B.100d')#, max_size=30000)
TEXT.vocab.vectors.unk_init = init.xavier_uniform
# Iterator
train_iter = data.BucketIterator(train, batch_size=128, sort_key=lambda x: len(x.Phrase),
shuffle=True,device=DEVICE)
val_iter = data.BucketIterator(val, batch_size=128, sort_key=lambda x: len(x.Phrase),
shuffle=True,device=DEVICE)
# 在 test_iter , sort一定要设置成 False, 要不然会被 torchtext 搞乱样本顺序
test_iter = data.Iterator(dataset=test, batch_size=128, train=False,
sort=False, device=DEVICE)
"""
由于目的是学习torchtext的使用,所以只定义了一个简单模型
"""
len_vocab = len(TEXT.vocab)
class Enet(nn.Module):
def __init__(self):
super(Enet, self).__init__()
self.embedding = nn.Embedding(len_vocab,100)
self.lstm = nn.LSTM(100,128,3,batch_first=True)#,bidirectional=True)
self.linear = nn.Linear(128,5)
def forward(self, x):
batch_size,seq_num = x.shape
vec = self.embedding(x)
out, (hn, cn) = self.lstm(vec)
out = self.linear(out[:,-1,:])
out = F.softmax(out,-1)
return out
model = Enet()
"""
将前面生成的词向量矩阵拷贝到模型的embedding层
这样就自动的可以将输入的word index转为词向量
如果没有使用预训练词向量,name就用随机生成的,会跟着模型进行更新
vocab_size是所用词的总数,embedding_dim是预设的词向量维度。
model.embedding = nn.Embedding(vocab_size, embedding_dim)
"""
model.embedding.weight.data.copy_(TEXT.vocab.vectors)
model.to(DEVICE)
# 训练
optimizer = optim.Adam(model.parameters())#,lr=0.000001)
n_epoch = 20
best_val_acc = 0
for epoch in range(n_epoch):
for batch_idx, batch in enumerate(train_iter):
data = batch.Phrase
target = batch.Sentiment
target = torch.sparse.torch.eye(5).index_select(dim=0, index=target.cpu().data)
target = target.to(DEVICE)
data = data.permute(1,0)
optimizer.zero_grad()
out = model(data)
loss = -target*torch.log(out)-(1-target)*torch.log(1-out)
loss = loss.sum(-1).mean()
loss.backward()
optimizer.step()
if (batch_idx+1) %200 == 0:
_,y_pre = torch.max(out,-1)
acc = torch.mean((torch.tensor(y_pre == batch.Sentiment,dtype=torch.float)))
print('epoch: %d \t batch_idx : %d \t loss: %.4f \t train acc: %.4f'
%(epoch,batch_idx,loss,acc))
val_accs = []
for batch_idx, batch in enumerate(val_iter):
data = batch.Phrase
target = batch.Sentiment
target = torch.sparse.torch.eye(5).index_select(dim=0, index=target.cpu().data)
target = target.to(DEVICE)
data = data.permute(1,0)
out = model(data)
_,y_pre = torch.max(out,-1)
acc = torch.mean((torch.tensor(y_pre == batch.Sentiment,dtype=torch.float)))
val_accs.append(acc)
acc = np.array(val_accs).mean()
if acc > best_val_acc:
print('val acc : %.4f > %.4f saving model'%(acc,best_val_acc))
torch.save(model.state_dict(), 'params.pkl')
best_val_acc = acc
print('val acc: %.4f'%(acc))