任务说明:NLP-Beginner:自然语言处理入门练习 任务二   数据下载:Sentiment Analysis on Movie Reviews   参考资料:  
  1. Convolutional Neural Networks for Sentence Classificatio
  2. PyTorch官方文档
  3. 关于深度学习与自然语言处理的一些基础知识:【深度学习实战】从零开始深度学习(四):RNN与自然语言处理
  4. TorchText文本数据集读取操作
  代码:NLP-Beginner-Exercise 该任务主要利用torchtext这个包来进行。  

目录

 
    • 导入需要的包
    • 创建Field对象
    • 从文件中读取数据——TabularDataset
    • 构建词表
    • 生成迭代器
    • 创建网络模型
    • 模型训练
    • 模型结果(一)
    • 模型改进——使用预训练好的词向量
    • 模型结果(二)
    • 模型再改进——使用LSTM
 

导入需要的包

 
import torch
from torch import nn, optim
import torch.nn.functional as F
from torchtext import data
from torchtext.data import Field, TabularDataset, BucketIterator
import time
  这里主要用到torchtext的四个模块:  
  1. Field——用来定义文本预处理的模式
  2. Example——数据样本,“文本+标签”
  3. TabularDataset——用于从文件中读取数据
  4. BucketIterator——迭代器,生成Batch
  整体思路如下图所示(摘自参考资料4)   微信图片_20210112153107  

创建Field对象

 
fix_length = 40
# 创建Field对象
TEXT = Field(sequential = True, lower=True, fix_length = fix_length)
LABEL = Field(sequential = False, use_vocab = False)
  Field参数说明(摘自参考资料4):  
  • 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——分词函数. 默认值: string.split().
  • include_lengths——是否返回一个已经补全的最小batch的元组和和一个包含每条数据长度的列表 . 默认值: False.
  • batch_first——Whether to produce tensors with the batch dimension
  • first. 默认值: False. pad_token——用于补全的字符. 默认值: “<pad>”.
  • unk_token——不存在词典里的字符. 默认值: “<unk>”.
  • pad_first——是否补全第一个字符. 默认值:False.
  Field几个重要的方法(摘自参考资料4):  
  • pad(minibatch)——在一个batch对齐每条数据
  • build_vocab()—— 建立词典
  • numericalize()——把文本数据数值化,返回tensor
 

从文件中读取数据——TabularDataset

  torchtext的TabularDataset可以用于从CSV、TSV或者JSON文件中读取数据。   TabularDataset参数说明  
  • path——文件存储路径
  • format——读取文件的格式(CSV/TSV/JSON)
  • fields——tuple(str, Field)。如果使用列表,格式必须是CSV或TSV,列表的值应该是(name, Field)的元组。字段的顺序应该与CSV或TSV文件中的列相同,而(name, None)的元组表示将被忽略的列。如果使用dict,键应该是JSON键或CSV/TSV列的子集,值应该是(名称、字段)的元组。不存在于输入字典中的键将被忽略。这允许用户从JSON/CSV/TSV键名重命名列,还允许选择要加载的列子集。
  • skip_header——是否要跳过表的第一行(表头)
  • csv_reader_params——读取csv或者tsv所需要的一些参数。具体可以参考csv.reader
  我们可以先看一下数据的样子   微信图片_20210112153135   实际上我们只需要知道文本内容(即Phrase列)和标签(即Sentiment列)就可以了,设置fields读取器如下:  
fields = [('PhraseId', None), ('SentenceId', None), ('Phrase', TEXT), ('Sentiment', LABEL)]
  设置好读取器后,可以利用TabularDataset读取数据:  
# 从文件中读取数据
fields = [('PhraseId', None), ('SentenceId', None), ('Phrase', TEXT), ('Sentiment', LABEL)]

dataset, test = TabularDataset.splits(path = './', format = 'tsv',
                                      train = 'train.tsv', test = 'test.tsv',
                                      skip_header = True, fields = fields)
train, vali = dataset.split(0.7)
  这里需要注意的是,我们读进来的train.tsv文件其实是包含训练集和测试集的,利用dataset.split(0.7)将其分成训练集和测试集,数据样本数的比例的是7:3。  

构建词表

  torchtext.data.Field对象提供了build_vocab方法来帮助构建词表。build_vocab()方法的参数参考Vocab   从训练集中构建词表的代码如下。文本数据的词表最多包含10000个词,并删除了出现频次不超过10的词。  
这里需要注意的是,我们读进来的train.tsv文件其实是包含训练集和测试集的,利用dataset.split(0.7)将其分成训练集和测试集,数据样本数的比例的是7:3。

构建词表

torchtext.data.Field对象提供了build_vocab方法来帮助构建词表。build_vocab()方法的参数参考Vocab

从训练集中构建词表的代码如下。文本数据的词表最多包含10000个词,并删除了出现频次不超过10的词。
 

生成迭代器

  trochtext提供了BucketIterator,可以帮助我们批处理所有文本并将词替换成词的索引。如果序列的长度差异很大,则填充将消耗大量浪费的内存和时间。BucketIterator可以将每个批次的相似长度的序列组合在一起,以最小化填充。  
# 生成向量的批数据
bs = 64
train_iter, vali_iter = BucketIterator.splits((train, vali), batch_size = bs, 
                                              device= torch.device('cpu'), 
                                              sort_key=lambda x: len(x.Phrase),
                                              sort_within_batch=False,
                                              shuffle = True,
                                              repeat = False)
 

创建网络模型

 
class EmbNet(nn.Module):
    def __init__(self,emb_size,hidden_size1,hidden_size2=400):
        super().__init__()
        self.embedding = nn.Embedding(emb_size,hidden_size1)
        self.fc = nn.Linear(hidden_size2,3)
        self.log_softmax = nn.LogSoftmax(dim = -1)
        
    def forward(self,x):
        embeds = self.embedding(x).view(x.size(0),-1)
        out = self.fc(embeds)
        out = self.log_softmax(out)
        return out

model = EmbNet(len(TEXT.vocab.stoi), 20)
model = model.cuda()
optimizer = optim.Adam(model.parameters(),lr=0.001)
  模型包括三层,首先是一个embedding层,它接收两个参数,即词表的大小和希望为每个单词创建的word embedding的维度。对于一个句子来说,所有的词的word embedding向量收尾相接(扁平化),通过一个线性层和一个log_softmax层得到最后的分类。   这里的优化器选择Adam,在实践中的效果比SGD要好很多。  

模型训练

 
# 训练模型
def fit(epoch, model, data_loader, phase = 'training'):
    if phase == 'training':
        model.train()
    if phase == 'validation':
        model.eval()
    running_loss = 0.0
    running_correct = 0.0
            
    for batch_idx, batch in enumerate(data_loader):
        text, target = batch.Phrase, batch.Sentiment
#        if torch.cuda.is_available():
#            text, target = text.cuda(), target.cuda()
        if phase == 'training':
            optimizer.zero_grad()
        output = model(text)
        
        loss = F.cross_entropy(output, target)
        
        running_loss += F.cross_entropy(output, target, reduction='sum').item()
        preds = output.data.max(dim=1, keepdim = True)[1]
        running_correct += preds.eq(target.data.view_as(preds)).cpu().sum()
        if phase == 'training':
            loss.backward()
            optimizer.step()
    
    loss = running_loss/len(data_loader.dataset)
    accuracy = 100. * running_correct/len(data_loader.dataset)
    
    print(f'{phase} loss is {loss:{5}.{4}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
    return loss, accuracy

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]

t0 = time.time()

for epoch in range(1, 100):
    print('epoch no. {} :'.format(epoch) + '-'* 15)
    epoch_loss, epoch_accuracy = fit(epoch, model, train_iter,phase='training')
    val_epoch_loss, val_epoch_accuracy = fit(epoch, model, vali_iter,phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

tend = time.time()
print('总共用时:{} s'.format(tend-t0))
 

模型结果(一)

  微信图片_20210112153245   模型一共训练了100轮,在val的准确率大致在60%左右。  

模型改进——使用预训练好的词向量

  很多时候,在处理特定领域的NLP任务时,使用预训练好的词向量会非常有用。通常使用预训练的词向量包括下面三个步骤。   下载词向量  
TEXT.build_vocab(train, vectors=GloVe(name='6B', dim=300), max_size=10000, min_freq=20)
  在模型中加载词向量  
model = EmbNet(len(TEXT.vocab.stoi),300, 300*fix_length)
# 利用预训练好的词向量
model.embedding.weight.data = TEXT.vocab.vectors
  冻结embedding层  
# 冻结embedding层的权重
model.embedding.weight.requires_grad = False
optimizer = optim.Adam([ param for param in model.parameters() if param.requires_grad == True],lr=0.001)
 

模型结果(二)

  微信图片_20210112153328  

模型再改进——使用LSTM

 
## 创建模型
class LSTM(nn.Module):
    def __init__(self, vocab, hidden_size, n_cat, bs = 1, nl = 2):
        super().__init__()
        self.hidden_size = hidden_size
        self.bs = bs
        self.nl = nl
        self.n_vocab = len(vocab)
        self.n_cat = n_cat
        self.e = nn.Embedding(self.n_vocab, self.hidden_size)
        self.rnn = nn.LSTM(self.hidden_size, self.hidden_size, self.nl)
        self.fc2 = nn.Linear(self.hidden_size, self.n_cat)
        self.sofmax = nn.LogSoftmax(dim = -1)
    
    def forward(self, x):
        bs = x.size()[1]
        if bs != self.bs:
            self.bs = bs
        e_out = self.e(x)
        h0, c0 = self.init_paras()
        rnn_o, _ = self.rnn(e_out, (h0, c0))
        rnn_o = rnn_o[-1]
        fc = self.fc2(rnn_o)
        out = self.sofmax(fc)
        return out
    
    def init_paras(self):
        h0 = Variable(torch.zeros(self.nl, self.bs, self.hidden_size))
        c0 = Variable(torch.zeros(self.nl, self.bs, self.hidden_size))
        return h0, c0
        

model = LSTM(TEXT.vocab, hidden_size = 300, n_cat = 5, bs = bs)
# 利用预训练好的词向量
model.e.weight.data = TEXT.vocab.vectors

# 冻结embedding层的权重
model.e.weight.requires_grad = False
optimizer = optim.Adam([ param for param in model.parameters() if param.requires_grad == True],lr=0.001)