在上一节中,我们简要地了解了词向量,但并没有去实现它。在本节中,我们将下载一个名为IMDB的数据集(其中包含了评论),然后构建一个用于计算评论的情感是正面、负面还是未知的情感分类器。在构建过程中,还将为 IMDB 数据集中存在的词进行词向量的训练。我们将使用一个名为 torchtext 的库,这个库使下载、向量化文本和批处理等许多过程变得更加容易。训练情感分类器将包括以下步骤。
- 下载 IMDB 数据并对文本分词;
- 建立词表;
- 生成向量的批数据;
- 使用词向量创建网络模型;
- 训练模型。
6.2.1 下载 IMDB 数据并对文本分词
对于与计算机视觉相关的应用,我们使用过 torchvision 库。它提供了许多实用功能,并帮助我们构建计算机视觉应用程序。同样,有一个名为 torchtext 的库,它也是 PyTorch 的一部分,它与 PyTorch 一起工作,通过为文本提供不同的数据加载器和抽象,简化了许多与自然语言处理相关的活动。在本书写作时,torchtext 没有包含在 PyTorch 包内,需要独立安装。可以在计算机的命令行中运行以下代码来安装torchtext:
pip install torchtext
安装完成后就可以使用它了。torchtext 提供了两个重要的模块:torchtext.data和torchtext.datasets。
1. torchtext.data
torchtext.data 实例定义了一个名为 Field 的类,它可以用来定义数据如何读取和分词。让我们看一下使用它来准备 IMDB 数据集的示例:
from torchtext import data
TEXT = data.Field(lower=True, batch_first=True, fix_length=20)
LABEL = data.Field(sequential=False)
在上述代码中,我们定义了两个 Field 对象,一个用于实际的文本,另一个用于标签数据。对于实际的文本,我们期望 torchtext 将所有文本都小写并对文本分词,同时将其修整为最大长度为20。如果我们正在为生产环境构建应用程序,则可以将长度修正为更大的数字。当然对于当前练习的例子,20的长度够用了。Field 的构造函数还接受另一个名为 tokenize 的参数,该参数默认使用str.split 函数。还可以指定spaCy作为参数或任何其他分词器。我们的例子将使用 str.split。
2. torchtext.datasets
torchtext.datasets 实例提供了使用不同数据集的封装,如IMDB、TREC(问题分类)、语言建模(WikiText-2)和一些其他数据集。我们将使用 torch.datasets 下载 IMDB 数据集并将其拆分为 train 和 test 数据集。以下代码执行此操作,当第一次运行它时,可能需要几分钟,具体取决于网络连接速度,因为它是从 Internet 上下载 IMDB 数据集的:
train,test=datasets.IMDB.splits(TEXT,LABEL)
之前的数据集的 IMDB 类抽象出了下载、分词和将数据库拆分为 train 和 test 数据集涉及的所有复杂度。train.fields 包含一个字典,其中 TEXT 是键,值是 LABEL。让我们看看 train.fields 和 train 集合的每个元素:
print('train.fields',train.fields)
从这些结果中可以看到,单个元素包含了一个字段 text 和表示 text 的所有 token,以及包含了文本标签的字段 label。现在已准备好对 IMDB 数据集进行批处理了。
6.2.2 构建词表
当为 thor_review 创建独热编码时,同时创建了一个作为词表的 word2idx 字典,它包含文档中唯一词的所有细节。torchtext 实例使处理更加容易。在加载数据后,可以调用 build_vocab 并传入负责为数据构建词表的必要参数。以下代码说明了如何构建词表:
TEXT.build_vocab(train,vectors=GloVe(name=,6B,dim=300),max_size=10000,min_freq=10)
LABEL.build_vocab(train)
在上述代码中,传入了需要构建词表的 train 对象,并让它使用维度为 300 的预训练词向量来初始化向量。当使用预训练权重训练情感分类器时,build_vocab 对象只是下载并创建稍后将使用的维度。max_size 实例限制了词表中词的数量,而min_freg删除了出现不超过10 次的词,其中 10是可配置的。
当词汇表构建完成后,我们就可以获得例如词频、词索引和每个词的向量表示等不同的值。下面的代码演示了如何访问这些值:
print(TEXT.vocab.freqs)
以下代码演示了如何访问结果:
print(TEXT.vocab.vectors)
使用 stoi 访问包含词及其索引的字典。
6.2.3 生成向量的批数据
torchtext 提供了 BucketIterator,它有助于批处理所有文本并将词替换成词的索引。BucketIterator 实例带有许多有用的参数,如batch_size、device(GPU或CPU)和 shuffle (是否必须对数据进行混洗)。下面的代码演示了如何为 train 和 test 数据集创建生成批处理的迭代器:
train_iter, test_iter = data.BucketIterator.splits((train, test),
batch_size=128,device=-1,shuffle=True)
#device = -1 表示使用 cpu,设置为 None 时使用 gpu.
上述代码为 train 和 test 数据集提供了一个 BucketIterator 对象。以下代码将说明如何创建 batch 并显示 batch 的结果:
batch = next(iter(train_iter))
batch.text
从上面代码段的结果中,可以看到文本数据如何转换为 batch_size * fix_len (即128x20) 大小的矩阵。
6.2.4 使用词向量创建网络模型
我们之前简要地讨论过词向量。在本节中,我们将创建作为网络架构的一部分的词向量,并训练整个模型用以预测每个评论的情感。在训练结束时,将得到一个情感分类器模型,以及 IMDB 数据集的词向量。以下代码演示了如何使用词向量创建用于情感预测的网络架构:
class EmbNet(nn.Module):
def _init_(self,emb_size,hidden_sizel,hidden_size2 = 400):
super()._init_()
self.embedding = nn.Embedding(emb_size,hidden_sizel)
self.fc = nn.Linear(hidden_size2,3)
def forward(self,x):
embeds = self.embedding(x).view(x.size(0),-1)
out = self.fc(embeds)
return F.log_softmax(out, dim = -1)
在上述代码中,EmbNet 创建了情感分类模型。在_init_函数中,我们使用两个参数初始化了 nn.Embedding 类的一个对象,它接收两个参数,即词表的大小和希望为每个单词创建的维度。由于限制了唯一单词的数量,因此词表的大小将为10,000,并且我们可以从一个小的向量尺寸(比如10)开始。为了快速运行程序,有必要使用个小尺寸的向量值,但是当试图为生产系统构建应用程序时,请使用大尺寸的词向量。我们还有一个线性层,将词向量映射到情感的类别(如正面、负面或未知)。
forward 函数确定了输入数据的处理方式。对于批量大小为 32 以及最大长度为 20 个词的句子,输入形状为 32x20。第一个 embedding 层充当查找表,用相应的词向量替换掉每个词。对于向量维度 10,当每个词被其相应的词向量替换时,输出形状变成了 32x20x10。view 函数将使 embedding 层的结果变得扁平。传递给 view 函数的第一个参数将保持维数不变。在我们的例子中,我们不希望组合来自不同批次的数据,因此保留第一个维数并将张量中的其余值扁平化。在应用 view 函数后,张量形状变为 32x200。全连接层将扁平化的词向量映射到类别的编号。定义了网络后就可以像往常一样训练它了。
6.2.5 训练模型
训练模型与在构建图像分类器时看到的非常类似,因此将使用相同的函数。我们把批数据传入模型并计算输出和损失,然后优化包括词向量权重在内的模型权重。以下代码执行此操作:
def fit(epoch,model,data_loader,phase=,training,,volatile=False):
if phase == 'training':
model.train()
if phase == 'validation':
model.eval()
volatile = True
running_loss = 0.0
running_correct = 0
for batch_idx r batch in enumerate(data_loader):
text, target = batch.text r batch.label
if is_cuda:
text,target = text.cuda(), target.cuda()
if phase =='training':
optimizer.zero_grad()
output = model(text)
loss = F.nll_loss(output,target)
running loss += F.nll loss(output,target,size_average=False).data[0]
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}.{2}} and {phase} accuracy is
{running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
return loss,accuracy
train_losses,train_accuracy = [],[]
val_losses,val_accuracy = [],[]
train_iter.repeat = False
test_iter.repeat = False
for epoch in range(1,10):
epoch_loss,epoch_accuracy = fit(epoch,model,train_iter,phase='training')
val_epoch_loss,val_epoch_accuracy =
fit(epoch,model,test_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)
在上述代码中,通过传入为批处理数据创建的 BucketIterator 对象来调用 fit 方法。默认情况下,迭代器不会停止生成批数据,因此必须将 BucketIterator 对象的 repeat 变量设置为 False。如果不将 repeat 变量设置为 False,那么 fit 函数将无限地运行。模型训练10轮后得到的验证准确率约为70%。