来源:analyticsvidhya
编译:X程序媛
来自:磐创AI公众号
了解如何使用PyTorch进行文本分类
掌握包装填充特征(Pack Padding feature)的重要性
了解解决文本分类问题涉及的关键点
我总是选择最先进的体系结构来进行数据科学编程马拉松的首次提交。由于PyTorch,Keras和TensorFlow等深度学习框架,实现最先进的体系结构已变得非常容易。这些框架提供了一种简单的方法,以最少的概念和编码技能知识即可实现复杂的模型架构和算法。简而言之,它是数据科学界的金矿!
pytorch-11在本文中,我们将使用PyTorch,它以其快速的计算能力而闻名。因此,在本文中,我们将逐步讲解解决文本分类问题的要点。然后,我们将在PyTorch中实现我们的第一个文本分类器!
注意:我强烈建议您先阅读以下文章,然后再继续阅读本文章。
PyTorch入门指南-从零开始
1.为什么选择使用PyTorch进行文本分类?
处理超出词汇表的单词
处理可变长度序列
包装器和预训练模型
2.理解问题陈述
3.用PyTorch实现文本分类
在深入探讨技术概念之前,让我们快速熟悉将要使用的框架–PyTorch。PyTorch的基本单位是张量(Tensor),类似于python中的“numpy”数组。使用PyTorch有很多好处,但其最重要的两个是:
动态网络–在运行期间可以改变网络架构
跨GPU的分布式训练
我敢肯定,您想知道–我们为什么要选择使用PyTorch来处理文本数据?让我们讨论一下PyTorch有哪些令人难以置信的功能,这些功能使其与其他框架不同,尤其是在处理文本数据时。
在固定的词汇表上训练文本分类模型。但是在训练过程中,我们可能会遇到一些词汇表中不存在的单词。这些单词被称为词汇表外单词。超出词汇表单词的出现可能是一个关键问题,因为这会导致信息丢失。
为了处理超出词汇表的单词,PyTorch支持一项很酷的功能,即用未知令牌(token)来替换训练数据中的出现次数少的单词。反过来,这将有助于我们解决单词超出词汇表的问题。
除了处理超出词汇表的单词之外,PyTorch还可以处理可变长度的序列!
您是否听说过递归神经网络需要处理可变长度的序列?有没有想过要如何实现?PyTorch带有一个有用的功能,即“填充序列(Packed Padding sequence)”,这个功能可以实现动态循环神经网络。
填充是指在句子的开头或结尾添加一个填充令牌的额外令牌的过程。随着每个句子中单词数量的变化,我们通过添加padding标记将变长度的输入句子转换为具有相同长度的句子。
由于大多数框架都支持静态网络,即在整个模型训练过程中,神经网络体系结构保持不变,因此需要进行填充。尽管填充解决了可变长度序列的问题,但是这个想法还有另一个问题–神经网络体系结构像处理其他信息/数据一样处理这些填充令牌。让我通过一个简单的图来解释这一点。
如您在下图中所见,在生成输出时,还需要使用最后一个元素(即填充令牌)。这可以通过PyTorch中的填充序列来解决。
Untitled-Diagram填充会忽略带有填充令牌的输入时间步。这些值从未显示给递归神经网络,这有助于我们构建动态循环神经网络。
Untitled-Diagram1目前正在为PyTorch框架推出最先进的架构。Hugging Face发布了transformer,为自然语言理解提供了超过32种最先进的体系结构!
不仅如此,PyTorch还为一些任务提供了预训练的模型,例如文本转语音,对象检测等,用几行代码就可以实现。
不可思议,难道不是吗?这些功能只是PyTorch的许多有用的功能中的一部分。现在让我们使用PyTorch来解决文本分类问题。
作为本文的一部分,我们将致力于解决一个非常有趣的问题。
Quora希望在其平台上跟踪用户不真诚的问题,以使用户在共享知识时感到安全。在这种情况下,一个不真诚的问题被定义为一个旨在陈述而不是在寻求有用答案的问题。为了进一步说明这一点,这里有一些特征可以表明一个特定的问题是不真诚的:
具有中立的语气
贬低或煽动
不扎根于现实
使用性内容(乱伦,兽交,恋童癖)来让人感到震惊,而不是寻求真正的答案
训练数据包括所提出的问题以及一个标志,该标志表示该问题是否被识别为不真诚(目标= 1)。真实标签包含一些噪音,即不能保证它们是完美的。我们的任务是在给定的问题下,判断该问题是否具有“诚意”。您可以从此处下载数据集。
现在该使用PyTorch来编写我们自己的文本分类模型了。
让我们首先导入构建模型所需的库。这是我们要使用的软件包/库的简要概述-
Torch包用于定义张量(Tensor)和数学运算
TorchText是PyTorch中的自然语言处理(NLP)库。该库包含一些用于预处理文本的脚本和流行的NLP数据集。
#处理张量(tensors)
import torch
#处理文本数据
from torchtext import data
为了使结果可以重复,我指定了随机种子的数值。由于深度学习模型的随机性,在执行时可能会产生不同的结果,因此指定随机种子的数值很重要。
#产生相同的结果
SEED = 2019
#Torch
torch.manual_seed(SEED)
#Cuda算法
torch.backends.cudnn.deterministic = True
数据预处理:
现在,让我们看看如何使用字段对象来预处理文本。有两种不同类型的字段对象–Field和LabelField。让我们快速了解两者之间的区别-
Field:数据模块中的Field对象用于为数据集中的每一列指定预处理步骤。
LabelField:LabelField对象是Field对象的特例,仅用于分类任务。它的唯一用途是将unk_token按顺序设置为默认值(None)。
在使用Field之前,让我们看一下Field的不同参数以及它们的用途。Field的参数:
Tokenize:指定句子的令牌化(tokenize)方式,即转换将句子转换为单词的方式。我正在使用spacy令牌生成器,因为它使用了新颖的令牌生成算法
Lower: 将文本转换为小写
batch_first: 输入和输出的第一维始终是批处理大小(batch size)
TEXT = data.Field(tokenize='spacy',batch_first=True,include_lengths=True)
LABEL = data.LabelField(dtype = torch.float,batch_first=True)
接下来,我们将创建一个元组列表,其中每个元组中的第一个值包含一个列名,第二个值是上面定义的字段对象。此外,我们将按照csv的列的排列顺序,来排列每个元组,并指定为(None,None)以忽略csv文件中的列。
让我们只读取需要的列-问题和标签
fields = [(None, None), ('text',TEXT),('label', LABEL)]
在下面的代码块中,我通过定义字段对象加载自定义数据集。
#加载自定义数据集
training_data=data.TabularDataset(path = 'quora.csv',format = 'csv',fields = fields,skip_header = True)
#打印预处理文本
print(vars(training_data.examples[0]))
现在让我们将数据集分为训练数据和验证数据
import random
train_data, valid_data = training_data.split(split_ratio=0.7, random_state = random.seed(SEED))
准备输入和输出序列:
下一步是构建词汇表并将文本转换为整数序列。词汇表包含文本数据中的单词。每个单词都分配有一个索引。以下是参数
参数:
min_freq:忽略频率小于指定频率的单词,并将其映射到未知令牌。
两个特殊的令牌,unknown和padding,它们也将添加到词汇表中
Unknown令牌用于处理超出词汇表的单词
Padding令牌用于制作相同长度的输入序列
让我们建立词汇表并使用预训练的词嵌入来初始化单词。如果您希望随机初始化嵌入,请忽略vectors参数。
#初始化glove词嵌入
TEXT.build_vocab(train_data,min_freq=3,vectors = "glove.6B.100d")
LABEL.build_vocab(train_data)
#文本中的唯一标记
print("Size of TEXT vocabulary:",len(TEXT.vocab))
#标签中唯一令牌的集合
print("Size of LABEL vocabulary:",len(LABEL.vocab))
#常用单词
print(TEXT.vocab.freqs.most_common(10))
#单词词典
print(TEXT.vocab.stoi)
现在,我们将准备用于训练模型的批数据(batch)。BucketIterator以需要最少的填充量的方式来形成批数据(batch)。
#检查cuda是否可用
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
#设置批数据大小
BATCH_SIZE = 64
#加载迭代器
train_iterator, valid_iterator = data.BucketIterator.splits(
(train_data, valid_data),
batch_size = BATCH_SIZE,
sort_key = lambda x: len(x.text),
sort_within_batch=True,
device = device)
模型架构
现在是时候定义解决二分类问题的模型结构了。torch的nn模块是所有模型的基本模型。这意味着每个模型都必须是nn模块的子类。
我在这里定义了2个函数:init和forward。让我解释一下这两个函数的用法:
Init:每当创建类的实例时,都会自动调用init函数。因此,它被称为构造函数。传递给类的参数由构造函数初始化。我们在该方法中定义模型将使用的所有层。
Forward:定义输入数据的正向传递。
最后,让我们详细了解用于构建模型架构的不同层及其参数–
嵌入层(Embedding layer):嵌入对于任何与NLP相关的任务都非常重要,因为它以数字格式表示单词。嵌入层创建一个查找表,其中每一行代表一个单词的嵌入。嵌入层将整数序列转换为密集的矢量表示形式。这是嵌入层的两个最重要的参数–
num_embeddings:词汇表单词的数量
embedding_dim:一个单词表示的维数
LSTM:LSTM是RNN的变体,能够捕获长期依赖关系。您应该熟悉的LSTM一些重要的参数。以下是该层的参数:
input_size:输入的维数
hidden_size:隐藏节点数
num_layers:要堆叠的层数
batch_first:如果为True,则输入和输出张量按(batch,seq,feature)提供
dropout:如果不为零,则在除最后一层以外的每个LSTM层的输出上都加入一个Dropout层,其丢弃概率等于dropout的值。默认值:0
bidirection:如果为True,则使用双向的LSTM
线性层(Linear Layer):线性层是指全连接层(dense层)。此处描述了两个重要参数:
in_features:输入特征的数量
out_features:输出特征的数量Pack Padding:像之前所讨论的那样,pack padding用于定义动态循环神经网络。如果没有pack padding,填充输入的令牌也将由rnn处理,并返回填充令牌的隐藏状态。这是一个很棒的包装器,不显示输入的填充。它只是忽略这些值并返回未填充令牌的隐藏状态。
我将从定义架构的所有层开始:
import torch.nn as nn
class classifier(nn.Module):
#定义模型中使用的所有层
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers,
bidirectional, dropout):
#构造函数
super().__init__()
#embeddding层
self.embedding = nn.Embedding(vocab_size, embedding_dim)
#lstm层
self.lstm = nn.LSTM(embedding_dim,
hidden_dim,
num_layers=n_layers,
bidirectional=bidirectional,
dropout=dropout,
batch_first=True)
#dense层
self.fc = nn.Linear(hidden_dim * 2, output_dim)
#激活函数
self.act = nn.Sigmoid()
def forward(self, text, text_lengths):
#text = [batch size,sent_length]
embedded = self.embedding(text)
#embedded = [batch size, sent_len, emb dim]
#填充句子
packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, text_lengths,batch_first=True)
packed_output, (hidden, cell) = self.lstm(packed_embedded)
#hidden = [batch size, num layers * num directions,hid dim]
#cell = [batch size, num layers * num directions,hid dim]
#合并(concat)前向传播和后向传播的最终隐藏状态
hidden = torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1)
#hidden = [batch size, hid dim * num directions]
dense_outputs=self.fc(hidden)
#最终激活函数
outputs=self.act(dense_outputs)
return outputs
下一步是定义超参数并实例化模型。这是相同的代码块:
#定义超参数
size_of_vocab = len(TEXT.vocab)
embedding_dim = 100
num_hidden_nodes = 32
num_output_nodes = 1
num_layers = 2
bidirection = True
dropout = 0.2
#实例化模型
model = classifier(size_of_vocab, embedding_dim, num_hidden_nodes,num_output_nodes, num_layers,
bidirectional = True, dropout = dropout)
让我们看一下模型摘要(summary),并使用预训练的词嵌入来初始化嵌入层
#模型框架
print(model)
#可训练参数的数量
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'The model has {count_parameters(model):,} trainable parameters')
#初始化预训练的词嵌入
pretrained_embeddings = TEXT.vocab.vectors
model.embedding.weight.data.copy_(pretrained_embeddings)
print(pretrained_embeddings.shape)
在这里,我为模型定义了优化器,损失(loss)和度量指标:
import torch.optim as optim
#定义优化器和损失
optimizer = optim.Adam(model.parameters())
criterion = nn.BCELoss()
#定义度量指标
def binary_accuracy(preds, y):
#round预测到最接近的整数
rounded_preds = torch.round(preds)
correct = (rounded_preds == y).float()
acc = correct.sum() / len(correct)
return acc
#转化为cuda(如果可用)
model = model.to(device)
criterion = criterion.to(device)
构建模型有两个阶段:
训练阶段:model.train()将模型设置为训练阶段并激活dropout层。
测试阶段:model.eval()将模型设置为评估阶段并停用dropout层。
这是代码块,用于定义训练模型的函数
def train(model, iterator, optimizer, criterion):
#每个epoch进行初始化
epoch_loss = 0
epoch_acc = 0
#将模型设置为训练阶段
model.train()
for batch in iterator:
#重设梯度
optimizer.zero_grad()
#获取文本和单词数量
text, text_lengths = batch.text
#转换为一维张量
predictions = model(text, text_lengths).squeeze()
#计算loss
loss = criterion(predictions, batch.label)
#计算二分类准确度
acc = binary_accuracy(predictions, batch.label)
#后向传播损失并计算梯度
loss.backward()
#更新权重
optimizer.step()
#损失和准确度
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
因此,我们有一个训练模型的函数,但是我们还需要一个函数来评估模型。让我们来写吧!
def evaluate(model, iterator, criterion):
#每个epoch进行初始化
epoch_loss = 0
epoch_acc = 0
#停用dropout层
model.eval()
#停用自动求导
with torch.no_grad():
for batch in iterator:
#获取文本和单词数量
text, text_lengths = batch.text
#转换为一维张量
predictions = model(text, text_lengths).squeeze()
#计算损失和准确度
loss = criterion(predictions, batch.label)
acc = binary_accuracy(predictions, batch.label)
#跟踪损失和准确度
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
最后,我们在一定时间内训练模型,并在每个时间段保存最佳模型。
N_EPOCHS = 5
best_valid_loss = float('inf')
for epoch in range(N_EPOCHS):
#训练模型
train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
#评估模型
valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)
#保存模型
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'saved_weights.pt')
print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc*100:.2f}%')
让我们加载最佳模型并定义推断函数,该函数接受用户定义的输入并进行预测
#加载权重
path='/content/saved_weights.pt'
model.load_state_dict(torch.load(path));
model.eval();
#推断
import spacy
nlp = spacy.load('en')
def predict(model, sentence):
tokenized = [tok.text for tok in nlp.tokenizer(sentence)] #令牌化(tokenize)句子
indexed = [TEXT.vocab.stoi[t] for t in tokenized] #转换为整数序列
length = [len(indexed)] #计算单词个数
tensor = torch.LongTensor(indexed).to(device) #转换为张量
tensor = tensor.unsqueeze(1).T #reshape成[batch, 单词个数]
length_tensor = torch.LongTensor(length) #转换为张量
prediction = model(tensor, length_tensor) #预测
return prediction.item()
惊人!让我们使用此模型对几个问题进行预测:
#进行预测
predict(model, "Are there any sports that you don't like?")
#不真诚的问题
predict(model, "Why Indian girls go crazy about marrying Shri. Rahul Gandhi ji?")
我们已经了解了如何利用PyTorch来构建自己的文本分类模型,并了解填充的重要性。您可以调节LSTM的超参数,例如隐藏层节点数,隐藏层数等,来进一步提高性能。
如果您有任何疑问/反馈,请在评论部分留言。我会尽快回复您。
原文链接 :https://www.analyticsvidhya.com/blog/2020/01/first-text-classification-in-pytorch/
添加个人微信,备注:昵称-学校(公司)-方向,即可获得
1. 快速学习深度学习五件套资料
2. 进入高手如云DL&NLP交流群
记得备注呦