来源 | Natural Language Processing with PyTorch
作者 | Rao,McMahan
译者 | Liangchu
校对 | gongyouliu
编辑 | auroral-L
全文共5190字,预计阅读时间40分钟。
上下拉动翻看这个目录
5.1 为什么要学习嵌入?
5.1.1 嵌入的有效性
5.1.2 学习词嵌入的方式
5.1.3 预训练词嵌入的实践
5.1.3.1 加载嵌入
5.1.3.2 词嵌入之间的关系
5.2 示例:学习词嵌入的连续词袋
5.2.1 Frankenstein数据集
5.2.2 Vocabulary,Vectorizer和DataLoader
5.2.3. CBOWClassifier模型
5.2.4 训练例程
5.2.5 模型评估和预测
5.3 示例:使用预训练嵌入用于文档分类的迁移学习
5.3.1 AG News数据集
5.3.2 Vocabulary,Vectorizer和DataLoader
5.3.3 NewsClassifier模型
5.3.4 训练例程
5.3.5 模型评估和分类
5.3.5.1 在测试集上评估
5.3.5.2 预测新的新闻头条的类别
5.4 总结
5.3 示例:使用预训练嵌入用于文档分类的迁移学习
前面的示例使用了一个嵌入层(embedding layer)做简单分类,这个例子的构建基于三个方面:首先加载预训练的词嵌入,然后通过对整个新闻文章进行分类来微调这些预训练的嵌入,最后使用卷积神经网络来捕获单词之间的空间关系。
在本例中,我们使用 AG News 数据集。为了对 AG News 中的单词序列进行建模,我们引入了Vocabulary类的一个变体SequenceVocabulary,以绑定一些对建模序列至关重要的token。Vectorizer将演示如何使用这个类。
在描述数据集以及向量化的minibatch是如何构建的之后,我们将逐步将预先训练好的单词向量加载到一个Embedding层中,并演示如何自定义它们。然后,该模型将预训练嵌入层与“示例:使用 CNN 对姓氏进行分类”一节中使用的CNN相结合使用。为了将模型的复杂性扩展到更真实的结构,我们还确定了使用正则化技术dropout的地方。接下来我们讨论训练例程。与第四章和本章中的前两个示例相比,训练例程几乎没什么变化,对此你并不会感到奇怪。最后,我们通过在测试集上对模型进行评价并讨论结果来总结这个例子。
5.3.1 AG News数据集
AG News 数据集是在2005年学术界为实验数据挖掘和信息提取方法而收集的100多万篇新闻文章的集合。这个例子的目的是说明预训练词嵌入在文本分类中的有效性。在本例中,我们使用精简版的120000篇新闻文章,它们平均分为四类:体育(Sports)、科学(Science)/技术(Technology)、世界(World)和商业(Business)。除了精简数据集之外,我们还将文章标题作为我们的观察重点,并创建多元分类任务来预测给定标题的类别。
和以前一样,我们通过删除标点符号、在标点符号周围添加空格(如逗号、撇号和句点)来预处理文本,并将文本转换为小写。此外,我们将数据集拆分为训练集、验证集和测试集,这是通过按类标签聚合数据点,然后将每个数据点分配给三个拆分集中的一个完成的。通过这种方式,保证了跨数据集的类分布是相同的。
如下例(5-11)所示,NewsDataset.__getitem__()方法遵循一个你很熟悉的基本公式:表示模型输入的字符串由数据集中的特定行检索,由Vectorizer进行向量化,并与表示新闻类别(类标签)的整数配对。
示例 5-11:NewsDataset.__getitem__()方法
class NewsDataset(Dataset):
@classmethod
def load_dataset_and_make_vectorizer(cls, news_csv):
"""Load dataset and make a new vectorizer from scratch
Args:
surname_csv (str): location of the dataset
Returns:
an instance of SurnameDataset
"""
news_df = pd.read_csv(news_csv)
train_news_df = news_df[news_df.split=='train']
return cls(news_df, NewsVectorizer.from_dataframe(train_news_df))
def __getitem__(self, index):
"""the primary entry point method for PyTorch datasets
Args:
index (int): the index to the data point
Returns:
a dict holding the data point's features (x_data) and label (y_target)
"""
row = self._target_df.iloc[index]
title_vector = \
self._vectorizer.vectorize(row.title, self._max_seq_length)
category_index = \
self._vectorizer.category_vocab.lookup_token(row.category)
return {'x_data': title_vector,
'y_target': category_index}
5.3.2 Vocabulary,Vectorizer和DataLoader
在本例中,我们引入了SequenceVocabulary,它是标准Vocabulary类的子类,它绑定了用于序列数据的四个特殊token:UNK token,MASK token,BEGIN-SEQUENCE token和END-SEQUENCE token。我们会在第六章中详细介绍这些token,但简而言之,它们的用途有三:我们在第四章中看到的UNK token(unknown 的缩写)允许模型学习罕见单词的表示,以便它可以在测试时接受从未见过的单词。当我们有可变长度的序列时,MASK token充当Embedding层和损失计算的标记。最后,BEGIN-SEQUENCE和END-SEQUENCE给出了神经网络关于序列边界的提示。下图(5-3)展示了在更广泛的向量化管道中使用这些特殊标记的结果:
文本到向量化minibatch的管道中的第二个部分是Vectorizer,它实例化并封装了SequenceVocabulary。在本例中,Vectorizer遵循我们在前面“Vectorizer”一节中演示的模式,即通过对特定频率进行计数和阈值化来限制词汇表中允许的词的总数。此操作的核心目的是改善模型的信号质量,并通过消除噪声低频词来限制内存模型的内存使用。
实例化之后,Vectorizer的vectorize()方法将新闻标题作为输入,并返回与数据集中最长标题一样长的向量,它有两个关键行为:第一,它在本地存储最大序列长度,通常,数据集会跟踪最大序列长度,并在推断时,测试序列的长度会被视为向量的长度,但由于我们有 CNN 模型,所以即使在推理时也要保持大小不变;第二,正如下例(5-11)中的代码片段所示,它输出一个由零填充的整数向量,它表示序列中的单词。此外,这个整数向量将BEGIN-SEQUENCE的整数添加到开头,并将END-SEQUENCE的整数添加到结尾。对分类器来说,这些特殊标记提示了序列边界,使其能够对边界附近的单词作出反应,而不是对靠近中心的单词作出反应。
示例 5-12:为 AG News数据集实现Vectorizer
class NewsVectorizer(object):
def vectorize(self, title, vector_length=-1):
"""
Args:
title (str): the string of words separated by a space
vector_length (int): forces the length of index vector
Returns:
the vectorized title (numpy.array)
"""
indices = [self.title_vocab.begin_seq_index]
indices.extend(self.title_vocab.lookup_token(token)
for token in title.split(" "))
indices.append(self.title_vocab.end_seq_index)
if vector_length < 0:
vector_length = len(indices)
out_vector = np.zeros(vector_length, dtype=np.int64)
out_vector[:len(indices)] = indices
out_vector[len(indices):] = self.title_vocab.mask_index
return out_vector
@classmethod
def from_dataframe(cls, news_df, cutoff=25):
"""Instantiate the vectorizer from the dataset dataframe
Args:
news_df (pandas.DataFrame): the target dataset
cutoff (int): frequency threshold for including in Vocabulary
Returns:
an instance of the NewsVectorizer
"""
category_vocab = Vocabulary()
for category in sorted(set(news_df.category)):
category_vocab.add_token(category)
word_counts = Counter()
for title in news_df.title:
for token in title.split(" "):
if token not in string.punctuation:
word_counts[token] += 1
title_vocab = SequenceVocabulary()
for word, word_count in word_counts.items():
if word_count >= cutoff:
title_vocab.add_token(word)
return cls(title_vocab, category_vocab)
5.3.3 NewsClassifier模型
我们在本章前面介绍了如何从磁盘中加载预训练嵌入,并使用Spotify’s的annoy库中的近似最近邻数据结构有效地使用它们。在前面的示例中,我们比较向量以获得有趣的语言学见解。然而,预训练的单词向量具有更有效的用途:我们可以使用它们来初始化Embedding层的嵌入矩阵。
使用词嵌入(word embedding)作为初始嵌入矩阵的过程包括:首先从磁盘加载嵌入,然后为数据中实际存在的单词选择正确的嵌入子集,最后将嵌入层的权重矩阵设置为加载的子集。在下例(5-13)中演示了选择子集的前两步。通常出现的一个问题是:数据集中存在的单词并没有包含在预训练的GloVe嵌入中,处理该问题的一种常用方法是使用PyTorch库中的初始化方法,例如Xavier Uniform方法,如下例(5-13)所示(Glorot 和 Bengio,2010):
示例 5-13:基于词汇表选择词嵌入的子集
def load_glove_from_file(glove_filepath):
"""Load the GloVe embeddings
Args:
glove_filepath (str): path to the glove embeddings file
Returns:
word_to_index (dict), embeddings (numpy.ndarray)
"""
word_to_index = {}
embeddings = []
with open(glove_filepath, "r") as fp:
for index, line in enumerate(fp):
line = line.split(" ") # each line: word num1 num2 ...
word_to_index[line[0]] = index # word = line[0]
embedding_i = np.array([float(val) for val in line[1:]])
embeddings.append(embedding_i)
return word_to_index, np.stack(embeddings)
def make_embedding_matrix(glove_filepath, words):
"""Create embedding matrix for a specific set of words.
Args:
glove_filepath (str): file path to the glove embeddings
words (list): list of words in the dataset
Returns:
final_embeddings (numpy.ndarray): embedding matrix
"""
word_to_idx, glove_embeddings = load_glove_from_file(glove_filepath)
embedding_size = glove_embeddings.shape[1]
final_embeddings = np.zeros((len(words), embedding_size))
for i, word in enumerate(words):
if word in word_to_idx:
final_embeddings[i, :] = glove_embeddings[word_to_idx[word]]
else:
embedding_i = torch.ones(1, embedding_size)
torch.nn.init.xavier_uniform_(embedding_i)
final_embeddings[i, :] = embedding_i
return final_embeddings
本例中的NewsClassifier是基于4-4节的ConvNet 分类器(使用 CNN 对字符的独热嵌入来对姓氏进行分类)而建立的。具体而言,我们使用Embedding层,它将输入token索引映射到向量表示。我们通过替换Embedding层的权重矩阵来使用预训练嵌入子集,如下例(5-14)所示。然后在forward()中使用嵌入以从索引映射到向量。除了嵌入层,一切都与第4-4节中的示例完全相同。
示例 5-14:实现NewsClassifier
class NewsClassifier(nn.Module):
def __init__(self, embedding_size, num_embeddings, num_channels,
hidden_dim, num_classes, dropout_p,
pretrained_embeddings=None, padding_idx=0):
"""
Args:
embedding_size (int): size of the embedding vectors
num_embeddings (int): number of embedding vectors
filter_width (int): width of the convolutional kernels
num_channels (int): number of convolutional kernels per layer
hidden_dim (int): the size of the hidden dimension
num_classes (int): the number of classes in classification
dropout_p (float): a dropout parameter
pretrained_embeddings (numpy.array): previously trained word embeddings
default is None. If provided,
padding_idx (int): an index representing a null position
"""
super(NewsClassifier, self).__init__()
if pretrained_embeddings is None:
self.emb = nn.Embedding(embedding_dim=embedding_size,
num_embeddings=num_embeddings,
padding_idx=padding_idx)
else:
pretrained_embeddings = torch.from_numpy(pretrained_embeddings).float()
self.emb = nn.Embedding(embedding_dim=embedding_size,
num_embeddings=num_embeddings,
padding_idx=padding_idx,
_weight=pretrained_embeddings)
self.convnet = nn.Sequential(
nn.Conv1d(in_channels=embedding_size,
out_channels=num_channels, kernel_size=3),
nn.ELU(),
nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
kernel_size=3, stride=2),
nn.ELU(),
nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
kernel_size=3, stride=2),
nn.ELU(),
nn.Conv1d(in_channels=num_channels, out_channels=num_channels,
kernel_size=3),
nn.ELU()
)
self._dropout_p = dropout_p
self.fc1 = nn.Linear(num_channels, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, num_classes)
def forward(self, x_in, apply_softmax=False):
"""The forward pass of the classifier
Args:
x_in (torch.Tensor): an input data tensor.
x_in.shape should be (batch, dataset._max_seq_length)
apply_softmax (bool): a flag for the softmax activation
should be false if used with the Cross Entropy losses
Returns:
the resulting tensor. tensor.shape should be (batch, num_classes)
"""
# embed and permute so features are channels
x_embedded = self.emb(x_in).permute(0, 2, 1)
features = self.convnet(x_embedded)
# average and remove the extra dimension
remaining_size = features.size(dim=2)
features = F.avg_pool1d(features, remaining_size).squeeze(dim=2)
features = F.dropout(features, p=self._dropout_p)
# final linear layer to produce classification outputs
intermediate_vector = F.relu(F.dropout(self.fc1(features),
p=self._dropout_p))
prediction_vector = self.fc2(intermediate_vector)
if apply_softmax:
prediction_vector = F.softmax(prediction_vector, dim=1)
return prediction_vector
5.3.4 训练例程
训练例程包括以下步骤:实例化数据集、实例化模型、实例化损失函数、实例化优化器、迭代数据集的训练部分并更新模型参数、迭代数据集的验证部分并测量性能、然后重复数据集迭代一定次数。此时你应该已经非常熟悉这一系列步骤了。下例(5-15)中展示了此示例的超参数和其他训练参数:
示例 5-15:使用与训练嵌入的 CNN NewsClassifier的参数
args = Namespace(
# Data and path hyper parameters
news_csv="data/ag_news/news_with_splits.csv",
vectorizer_file="vectorizer.json",
model_state_file="model.pth",
save_dir="model_storage/ch5/document_classification",
# Model hyper parameters
glove_filepath='data/glove/glove.6B.100d.txt',
use_glove=False,
embedding_size=100,
hidden_dim=100,
num_channels=100,
# Training hyper parameter
seed=1337,
learning_rate=0.001,
dropout_p=0.1,
batch_size=128,
num_epochs=100,
early_stopping_criteria=5,
# ... runtime options not shown for space
)
5.3.5 模型评估和分类
在本例中,任务是根据新闻标题分类。如前所述,有两种方法可以理解模型执行任务的效果:使用测试集的定量评估,以及亲自检查分类结果的定性评估。
5.3.5.1 在测试集上评估
虽然这是你第一次分类新闻标题,但是定量评估例程和前面的示例是一样的:设置模型为eval模式以关掉dropout和反向传播(使用classifier.eval()),然后和遍历训练集和验证集一样遍历测试集。典型情况是,你应该尝试不同选项直到符合预期,然后执行模型评估,我们会把这个留作练习。在这个测试集中你能得到的最终准确度是多少?请记住,在整个实验过程中,测试集只能用一次。
5.3.5.2 预测新的新闻头条的类别
训练分类器的目标是将其部署到生产环境中,以便对不知道的新闻标题执行推理或预测。要预测数据集中尚未处理的新闻标题的类别,有几个步骤:首先是对文本进行预处理,其方式类似于对训练中的数据进行预处理。对于推理,我们对输入使用与训练中相同的预处理函数。这个预处理后的字符串使用训练期间使用的Vectorizer向量化,并转换为 PyTorch 张量。接下来,对它应用分类器。计算预测向量的最大值以查找类别名称。下例(5-16)给出了代码:
示例 5-16:使用训练模型作预测
def predict_category(title, classifier, vectorizer, max_length):
"""Predict a News category for a new title
Args:
title (str): a raw title string
classifier (NewsClassifier): an instance of the trained classifier
vectorizer (NewsVectorizer): the corresponding vectorizer
max_length (int): the max sequence length
Note: CNNs are sensitive to the input data tensor size.
This ensures to keep it the same size as the training data
"""
title = preprocess_text(title)
vectorized_title = \
torch.tensor(vectorizer.vectorize(title, vector_length=max_length))
result = classifier(vectorized_title.unsqueeze(0), apply_softmax=True)
probability_values, indices = result.max(dim=1)
predicted_category = vectorizer.category_vocab.lookup_index(indices.item())
return {'category': predicted_category,
'probability': probability_values.item()}
5.4 总结
在本章中,我们研究了词嵌入,这是一种将离散项(如单词)表示为空间中的固定维向量的方法,使得向量之间的距离编码了各种语言属性。请记住,本章介绍的技术适用于任何离散单元,如句子、段落、文档、数据库记录等, 这使得嵌入技术对于深度学习(特别是在 NLP 中)不可或缺。我们展示了如何以黑盒方式使用预训练嵌入。我们简要讨论了直接从数据中学习这些嵌入的几种方法,包括连续词袋(Continuous Bag-of-Words,CBOW)方法。我们接着展示了如何在语言建模任务的上下文中训练 CBOW 模型。最后,我们学习了一个使用预训练嵌入的示例,并探索了在像文档分类的任务中的微调嵌入。
不幸的是,由于篇幅问题,本章略过了许多重要的话题,比如词嵌入除偏(debiasing word embeddings)、建模上下文(modeling context)和一词多义(polysemy)。语言数据是现实世界的反映,社会偏见可以通过有偏见的训练语料库编码成模型。在一项研究中,最接近代词“she”的词是家庭主妇、护士、接待员、图书管理员、理发师等,然而最接近“he”的词则是外科医生、保护者、哲学家、建筑师、金融家等。在这种有偏见的嵌入上训练的模型会继续做出可能产生不公平结果的决策。词嵌入除偏仍然是一个新兴领域,我们建议你阅读提到了这一点的Bolukbasi等人(2016 年)和最近的论文。此外,我们使用的词嵌入不考虑上下文。例如,根据上下文,单词play可能有两个不同的含义,但这里讨论的所有嵌入(embedding)方法都会破坏这两个含义。最近的研究如 Peters(2018)探索了以上下文为条件提供嵌入的方法。