电子版docx怎么弄(「PyTorch自然语言处理系列」5. 词嵌入和类型(上))

wufei123 发布于 2023-12-11 阅读(380)

数据与智能 出版了专著「构建企业级推荐系统:算法、工程实现与案例分析」每周输出7篇推荐系统、数据分析、大数据、AI原创文章「数据与智能」(同名视频号、知乎、头条、B站、快手、抖音、小红书等自媒体平台号) 社区,聚焦数据、智能领域的知识分享与传播。

来源 | Natural Language Processing with PyTorch作者 | Rao,McMahan译者 | Liangchu校对 | gongyouliu编辑 | auroral-L

在实现自然语言处理任务时,我们需要处理不同类型的离散类型,典型例子是单词(word)单词来自一个有限的集合(也即词汇表)其他离散类型的示例包括字符(character)、词性标签(part-of-speech tag)、命名实体(named entity)、命名实体类型(named entity type)、解析特性(parse feature)、产品目录中的条目等等。

本质上,当任何输入特征来自一个有限(或可数无限)集合时,它都是离散类型(discrete type)将离散类型(如单词)表示为密集向量是NLP中深度学习成功的核心术语“表示学习(representation learning)”和“嵌入(embedding)”是指学习从一种离散类型到向量空间中的某点的映射。

当离散类型是单词时,这个密集向量表示称为词嵌入(word embedding)我们在第二章中看到了基于计数的嵌入方法的例子,比如TF-IDF在本章中,我们主要研究基于学习或基于预测(Baroni等人,2014)的嵌入方法,这种嵌入方法通过最大化特定学习任务的目标来学习表征,例如,根据上下文预测一个单词。

基于学习的嵌入方法由于其广泛的适用性和性能而得到了承认事实上,单词嵌入在 NLP 任务中的普遍性为它们赢得了“NLP 的 Sriracha”的称号,因为你可以在所有 NLP 任务中使用词嵌入,并期望任务的性能得到改进。

然而,我们认为这种绰号是有误导性的——与 Sriracha 不同,嵌入(embedding)通常不是事后添加到模型中的,而是模型本身的基本组成部分在本章中,我们讨论与嵌入词相关的向量表示:嵌入词的方法、优化用于监督和非监督语言任务词嵌入的方法、可视化嵌入词的方法、以及结合句子和文档的词嵌入方法。

然而请务必记住一点:我们在这里描述的方法适用于任何离散类型5.1 为什么要学习嵌入?在前几章中,你看到了创建单词向量表示的传统方法具体来讲,你了解到可以使用独热表示——向量的长度与词汇表的大小相同,除了表示特定单词的值为1的单个位置外,其他位置都有0填充。

此外,你还学习了计数表示——向量的长度也与模型中唯一单词的数量相同,但在向量中与句子中单词的频率相对应的位置上有计数基于计数的表示也称为分布表示(distributional representation),因为它们的重要内容或意义是由向量中的多个维度表示的。

分布表示历史悠久(Firth, 1935),并且可以很好地适用于许多机器学习和神经网络模型这些表示不是从数据中学习的,而是启发式构建的分布表示的名称来源于:由于单词现在由维度低得多的密集向量表示(假设d = 100,而不是整个词汇的大小,词汇表的大小大概是到或更高), 并且一个单词的含义和其他属性都分布在这个密集向量的不同维度上。

低维学习到的密集表示与我们在前几章中所学的基于独热和计数的向量相比有几个优点:首先,降低维度使得在计算上变得高效;第二,基于计数的表示会导致高维向量在多个维度上冗余地编码相似的信息,而且不共享统计强度;第三,输入中维数过高会导致机器学习和优化中的问题——一种叫做“维数灾难(curse of dimensionality)”的现象。

传统上,为了处理这个维度问题,我们会应用到像奇异值分解(singular value decomposition,SVD)和主成分分析(principal component analysis ,PCA)这样的降维方法,然而有些讽刺的是,当维度以百万计的顺序排列时(NLP的典型情况),这些方法并不能很好地进行缩放;第四,从特定任务的数据中学习(或微调)的表示形式最适合手头的任务。

使用 TF-IDF 等启发式方法或 SVD 等低维方法,并不清楚嵌入方法的优化目标是否与任务有关5.1.1 嵌入的有效性为了理解嵌入是如何工作的,下面看一个用独热向量乘以线性层中的权重矩阵的例子,如下图(5-1)所示。

在第三章和第四章中,独热向量的大小与词汇表的大小相同,这个向量被称为“独热”,因为它在索引中有一个1,表示存在一个特定的单词

根据定义,接受这个独热向量作为输入的线性层的权值矩阵必须与这个独热向量的大小具有相同的行数当执行矩阵乘法时,如上图(5-1)所示,结果向量实际上只是选择了由非零项指示的行基于该观察结果,我们可以跳过乘法步骤,直接使用整数作为索引来检索所选行。

关于嵌入效率,最后要注意的一点是:尽管上图(5-1)中的示例显示的权重矩阵与传入的独热向量具有相同的维度,但情况并非总是如此实际上,嵌入通常用于在低维空间中表示单词,而不是使用一个独热向量或基于计数的表示。

在研究文献中,嵌入的典型大小从25维到500维不等,维度的准确选择可以节省GPU内存5.1.2 学习词嵌入的方式本章的目标不是学习一些指定的单词嵌入技术,而是要理解什么是嵌入、如何使用它们以及在哪里使用它们、如何在模型中可靠地使用它们、以及理解它们的局限性。

之所以如此,是因为实践者一般很少遇到需要编写新的词嵌入训练算法的情况在本节中,我们会概述目前训练词嵌入的一些方法所有的词嵌入方法只使用单词(也就是一种无标签数据)来训练,却是以一种有监督的方式进行的,这可以通过构造辅助监督任务来实现。

在这些任务中,数据被隐式地标记,直觉告诉我们,为解决辅助任务而优化的表示将捕获文本语料库的许多统计和语言特性,以便普遍使用下面是一些辅助任务的示例:• 给定一个单词序列,预测下一个单词这也称为语言建模任务(language modeling task)。

• 前后给定一系列单词,预测缺失的单词• 给定一个单词,预测出现在窗口中的单词,与位置无关当然,上面列出的辅助任务并不完整,辅助任务的选择取决于算法设计者的直觉和计算开销像Glove、连续词袋模型(Continuous Bag-of-Words,CBOW)、Skipgrams 等等都是些典型例子。

我们推荐你参考Goldberg, 2017, Chapter 10以获得更多细节,但我们也会简要介绍CBOW模型然而,对于大多数目的来说,使用预先训练好的单词嵌入并对它们进行微调就足够了5.1.3 预训练词嵌入的实践

本章的主体以及本书的后续部分,都涉及到使用预训练词嵌入在大型语料库(比如Google News、Wikipedia或Common Crawl)上使用前面描述的多种方法之一训练的预训练词嵌入,都是可以免费下载和使用的。

本章的剩余部分将展示如何有效地加载和查找这些嵌入、研究词嵌入的一些属性、并演示在NLP任务中使用预训练嵌入的示例5.1.3.1 加载嵌入词嵌入现在已经变得足够流行和普及,因此你可以从最初的Word2Vec到Stanford 的 GLoVE、Facebook 的 FastText 和许多其他来源下载许多不同的版本。

通常,嵌入会以这种格式出现:每行以嵌入的单词(word)/类型(type)开始,然后是一系列数字(也即向量表示)这个序列的长度就是表示的维度(也即嵌入维度(embedding dimension)),嵌入维度通常数以百计。

标记(token)类型的数量通常是词汇表的大小,通常数以百万计例如,下面是来自 GloVe 的狗和猫向量的前七个维度:

为了有效地加载和处理嵌入,我们使用一个叫PreTrainedEmbeddings的实用工具类,如下例(5-1)所示这个类构建了所有单词向量的内存索引,以便使用近似最近邻包annoy进行快速查找和最近邻查询。

示例 5-1:使用预训练词嵌入Input[0] import numpy as np from annoy import AnnoyIndex classPreTrainedEmbeddings(object)

:def__init__(self, word_to_index, word_vectors):""" Args: word_to_index (dict): mapping from word to integers word_vectors (list of numpy arrays) """

self.word_to_index = word_to_index self.word_vectors = word_vectors self.index_to_word = \ {v: k

for k, v in self.word_to_index.items()} self.index = AnnoyIndex(len(word_vectors[0]), metric=

euclidean) for _, i in self.word_to_index.items(): self.index.add_item(i, self.word_vectors[i]) self.index.build(

50) @classmethoddeffrom_embeddings_file(cls, embedding_file):"""Instantiate from pre-trained vector file. Vector file should be of the format: word0 x0_0 x0_1 x0_2 x0_3 ... x0_N word1 x1_0 x1_1 x1_2 x1_3 ... x1_N Args: embedding_file (str): location of the file Returns: instance of PretrainedEmbeddings """

word_to_index = {} word_vectors = [] with open(embedding_file) as fp:

for line in fp.readlines(): line = line.split(" ") word = line[0] vec = np.array([float(x)

for x in line[1:]]) word_to_index[word] = len(word_to_index) word_vectors.append(vec)

return cls(word_to_index, word_vectors) Input[1] embeddings = \ PreTrainedEmbeddings.from_embeddings_file(

glove.6B.100d.txt)在这些例子中,我们使用Glove词嵌入下载之后,正如上例(5-1)中的第二个输入所示,你可以使用PretrainedEmbeddings类进行实例化5.1.3.2 词嵌入之间的关系。

词嵌入的核心特征是对句法和语义关系进行编码,这些句法和语义关系表现为词的使用规律例如,人们谈论猫和狗的方式非常相似(讨论宠物、喂食等),因此它们的嵌入之间彼此的距离比它们与其他动物(如鸭子和大象)相比的距离要近得多。

我们可以从几个方面探讨嵌入词编码的语义关系,最流行的一种方法是类比任务(像SAT 等考试中常见的推理任务):Word1 : Word2 :: Word3 : ______在这个任务中提供了前三个单词,我们需要确定第四个单词与前两个单词之间的关系是一致的。

使用词嵌入,我们可以对其进行空间编码首先,我们从Word1中减去Word2,这个差异向量编码了Word1和Word2之间的关系然后可以将这个差异加入Word3中,从而生成一个接近第四个单词的向量,也就是要填的空。

使用这个结果向量对索引执行最近邻查询就可以解决类比问题了计算上面示例的函数如下例(5-2)所示,它完成了刚刚描述的工作:使用向量算法和近似的最近邻索引来完成类比任务示例 5-2:使用词嵌入的类比任务Input[

0] import numpy as np from annoy import AnnoyIndex classPreTrainedEmbeddings(object):""" implementation continued from previous code box"""

defget_embedding(self, word):""" Args: word (str) Returns an embedding (numpy.ndarray) """

return self.word_vectors[self.word_to_index[word]] defget_closest_to_vector(self, vector, n=1):

"""Given a vector, return its n nearest neighbors Args: vector (np.ndarray): should match the size of the vectors in the Annoy index n (int): the number of neighbors to return Returns: [str, str, ...]: words nearest to the given vector. The words are not ordered by distance """

nn_indices = self.index.get_nns_by_vector(vector, n) return [self.index_to_word[neighbor]

for neighbor in nn_indices] defcompute_and_print_analogy(self, word1, word2, word3):"""Prints the solutions to analogies using word embeddings Analogies are word1 is to word2 as word3 is to __ This method will print: word1 : word2 :: word3 : word4 Args: word1 (str) word2 (str) word3 (str) """

vec1 = self.get_embedding(word1) vec2 = self.get_embedding(word2) vec3 = self.get_embedding(word3)

# Simple hypothesis: Analogy is a spatial relationship spatial_relationship = vec2 - vec1 vec4 = vec3 + spatial_relationship closest_words = self.get_closest_to_vector(vec4, n=

4) existing_words = set([word1, word2, word3]) closest_words = [word for word in closest_words

if word notin existing_words] if len(closest_words) == 0: print("Could not find nearest neighbors for the vector!"

) returnfor word4 in closest_words: print("{} : {} :: {} : {}".format(word1, word2, word3, word4))

有趣的是,简单的单词类比任务可以证明单词嵌入捕获了各种语义和语法关系,如下例(5-3)所示:示例 5-3:单词嵌入编码了许多语言学关系,如SAT类比任务所示Input[0] # Relationship

1: therelationshipbetweengenderednounsandpronounsembeddings.compute_and_print_analogy(man, he, woman)

Output[0]man : he :: woman : sheInput[1] # Relationship2: Verb-Nounrelationshipsembeddings.compute_and_print_analogy

(fly, plane, sail) Output[1]fly : plane :: sail : shipInput[2] # Relationship3: Noun-Nounrelationships

embeddings.compute_and_print_analogy(cat, kitten, dog) Output[2]cat : kitten :: dog : puppyInput[3]

# Relationship4: Hypernymy (broader category) embeddings.compute_and_print_analogy(blue, color, dog)

Output[3]blue : color :: dog : animalInput[4] # Relationship5: Meronymy (part-to-whole) embeddings.compute_and_print_analogy

(toe, foot, finger) Output[4]toe : foot :: finger : handInput[5] # Relationship6: Troponymy (difference in manner)

embeddings.compute_and_print_analogy(talk, communicate, read) Output[5]talk : communicate :: read :

interpretInput[6] # Relationship7: Metonymy (convention / figures of speech) embeddings.compute_and_print_analogy

(blue, democrat, red) Output[6]blue : democrat :: red : republicanInput[7] # Relationship8: Adjectival

Scalesembeddings.compute_and_print_analogy(fast, fastest, young) Output[7]fast : fastest :: young :

youngest虽然语言的功能似乎是系统化的,但事情可能会变得棘手如下例(5-4)所示,由于单词向量只是基于共现,所以关系可能是错误的:示例 5-4:一个例子说明了使用共现性来编码语义的危险性,有时它们并没有这种关系!。

Input[0]embeddings.compute_and_print_analogy(fast, fastest, small) Output[0]fast : fastest :: small

: largest下例(5-5)说明了最常见的类比对之一是如何编码性别角色的:示例 5-5:注意受保护的属性,例如单词嵌入中编码的性别这可能会在下游模型中引入不必要的偏差Input[0]embeddings。

.compute_and_print_analogy(man, king, woman) Output[0]man : king :: woman : queen事实证明,很难区分语言规律和成文的文化偏见。

例如,医生实际上并不只有男性,护士实际上也不只有女性,但这些长期存在的文化偏见被视为语言中的规律,并被编入单词向量,如下例(5-6)所示:示例 5-6:向量类比中的文化性别偏见Input[0]embeddings

.compute_and_print_analogy(man, doctor, woman) Output[0]man : doctor :: woman : nurse考虑到嵌入中的偏差在 NLP 应用中变得越来越多,你需要特别注意这一点。

去偏现有词嵌入如今是一个新的令人兴奋的研究领域(Bolukbasi等人, 2016)此外,我们建议你访问ethicsinnlp.org,以获取伦理学与 NLP 交叉的最新研究结果5.2 示例:学习词嵌入的连续词袋

在本例中,我们将介绍构建和学习通用词嵌入最著名的模型之一,即Word2Vec 连续词袋(Continuous Bag-of-Words ,CBOW)模型在本节中,当我们提到“CBOW 任务”或“CBOW 分类任务”时,隐含的意思是我们构建分类任务的目的是要学习 CBOW 嵌入。

CBOW 模型是一个多元分类任务,它表现为对单词文本进行扫描、创建单词的上下文窗口、从上下文窗口中删除中心词、并将上下文窗口分类为丢失的单词直觉上,你可以把它想成完形填空,有一个句子缺了一个单词,模型的工作就是找出这个缺失的单词应该是什么。

这个例子的目的是介绍nn.Embedding层,它是一个用于封装嵌入矩阵(embedding matrix)的 PyTorch 模块使用Embedding层,我们可以将token的整数ID映射到用于神经网络计算的向量上。

当优化器更新模型权重以最小化损失时,它还更新向量的值通过这个过程,模型将学习如何以最有效的方式嵌入单词下面,我们将遵循标准的示例格式:在第一部分,我们介绍数据集,即Mary Shelley的小说Frankenstein;然后,我们将讨论从token到向量化minibatch的向量化管道;接着,我们会概述 CBOW 分类模型以及如何使用Embedding层;接下来,我们将介绍训练例程(尽管按顺序阅读本书的你已经十分熟悉训练过程了);最后,我们会讨论模型评估、模型推理以及如何检查模型。

5.2.1 Frankenstein数据集在本例中,我们将从Mary Shelley的小说Frankenstein的电子版(可以通过Gutenberg项目获得)构建一个文本数据集本节介绍预处理过程、为这个文本数据集构建一个 PyTorch Dataset类、最后将数据集分解为训练集、验证集和测试集。

从Gutenberg项目发布的原始文本文件着手,预处理很简单:我们NLTK的Punkt tokenizer将文本分割成不同的句子然后,将每个句子转换为小写字母,并完全去掉标点符号这种预处理允许我们稍后根据空格拆分字符串,以便检索token列表。

这一预处理函数是从“示例:分类餐馆评论的情感”一节的示例中重用的接下来是将数据集枚举为一系列窗口,以便优化CBOW 模型为此,我们迭代每个句子中的token列表,并将它们分组到指定窗口大小的窗口中,如下图(5-2)所示:。

构建数据集的最后一步是将数据分割为三个集合:训练集、验证集和测试集回想一下,训练集和验证集都是在模型训练期间使用的:训练集用于更新参数,验证集用于度量模型的性能测试集顶多使用一次,它用来提供一个较少偏差的测量。

在本例(以及本书的大多数示例)中,我们使用70%的训练集、15%的验证集和15%的测试集窗口和目标的结果数据集由Pandas DataFrame加载,并在CBOWDataset类中建立索引下例(5-7)展示了__getitem__()代码片段,它利用Vectorizer将上下文——左右窗口——转换为向量。

目标(窗口中心的单词)使用Vocabulary转换为整数示例 5-7:为 CBOW 任务构造数据集类classCBOWDataset(Dataset):# ... existing implementation from Section 3.5

@classmethoddefload_dataset_and_make_vectorizer(cls, cbow_csv):"""Load dataset and make a new vectorizer from scratch Args: cbow_csv (str): location of the dataset Returns: an instance of CBOWDataset """

cbow_df = pd.read_csv(cbow_csv) train_cbow_df = cbow_df[cbow_df.split==train]

return cls(cbow_df, CBOWVectorizer.from_dataframe(train_cbow_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 with features (x_data) and label (y_target) """

row = self._target_df.iloc[index] context_vector = \ self._vectorizer.vectorize(row.context, self._max_seq_length) target_index = self._vectorizer.cbow_vocab.lookup_token(row.target)

return {x_data: context_vector, y_target: target_index}5.2.2 Vocabulary,Vectorizer和DataLoader

在 CBOW 分类任务中,从文本到向量化minibatch的管道一般是标准的:Vocabulary和DataLoader函数都与“示例:分类餐馆评论的情感”一节中的示例完全一样然而,与我们在第三章和第四章中看到的Vectorizer不同,这里的Vectorizer不构造独热向量,相反,它构造并返回一个表示上下文索引的整数向量。

下例(5-8)给出了vectorize()函数的代码:示例 5-8:用于 CBOW 数据的VectorizerclassCBOWVectorizer(object):""" The Vectorizer which coordinates the Vocabularies and puts them to use"""

defvectorize(self, context, vector_length=-1):""" Args: context (str): the string of words separated by a space vector_length (int): an argument for forcing the length of index vector """

indices = \ [self.cbow_vocab.lookup_token(token) for token in context.split()]

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.cbow_vocab.mask_index

return out_vector请注意,如果上下文中的token数量小于最大长度,那么其他项填充为零实际上,这叫做用零填充(padding with zeros)5.2.3. CBOWClassifier模型。

下例(5-9)中展示的CBOWClassifier包括三个基本步骤:第一,将表示上下文单词的索引与Embedding层一起使用,为上下文中的每个单词创建向量;第二,目标是以某种方式组合这些向量,使其能够捕获整个上下文。

在这个例子中,我们对向量求和,然而还有其他方法可供选择,比如求最大值、平均值,甚至在顶部使用多层感知器(MLP);第三,上下文向量与Linear层一起用来计算预测向量,这个预测向量是整个词汇表的概率分布。

预测向量中最大(最可能)的值表示目标词的可能预测——上下文中缺少的中心词这里使用的Embedding层主要由两个数字参数化:嵌入的数量(词汇表的大小)和嵌入的大小(嵌入维度)下例(5-9)的代码里使用了第三个参数:padding_idx。

对于像我们这样数据点长度可能不相同的情况,这个参数被用作嵌入层的标记值该层强制对应于该索引的向量及其梯度均为0示例 5-9:CBOWClassifier模型classCBOWClassifier(nn.Module)

:def__init__(self, vocabulary_size, embedding_size, padding_idx=0):""" Args: vocabulary_size (int): number of vocabulary items, controls the number of embeddings and prediction vector size embedding_size (int): size of the embeddings padding_idx (int): default 0; Embedding will not use this index """

super(CBOWClassifier, self).__init__() self.embedding = nn.Embedding(num_embeddings=vocabulary_size, embedding_dim=embedding_size, padding_idx=padding_idx) self.fc1 = nn.Linear(in_features=embedding_size, out_features=vocabulary_size)

defforward(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, input_dim) 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, output_dim) """

x_embedded_sum = self.embedding(x_in).sum(dim=1) y_out = self.fc1(x_embedded_sum)

if apply_softmax: y_out = F.softmax(y_out, dim=1) return y_out5.2.4 训练例程在这个例子中,训练例程遵循我们在整本书中使用的标准:首先初始化数据集、向量化器、模型、损失函数和优化器;然后对数据集的训练和验证部分进行一定次数的迭代,优化训练部分的损失最小化,并衡量验证部分的进度。

关于训练例程的更多细节,我们建议你参考“示例:分类餐馆评论的情感”一节获取详细内容下例(5-10)展示了我们用于训练的参数:示例 5-10:CBOW 训练脚本的参数Input[0]args = Namespace(

# Data and Path informationcbow_csv="data/books/frankenstein_with_splits.csv",vectorizer_file="vectorizer.json",

model_state_file="model.pth",save_dir="model_storage/ch5/cbow", # Model hyper parametersembedding_size

=300, # Training hyper parametersseed=1337,num_epochs=100,learning_rate=0.001,batch_size=128,early_stopping_criteria

=5, # Runtime options omitted for space)5.2.5 模型评估和预测本例中的评估基于从测试集中每个目标和上下文对提供的单词上下文预测目标单词,正确分类的单词意味着模型正在学习从上下文预测单词。

在本例中,该模型在测试集上达到了 15% 的目标词分类准确率首先,本例中 CBOW 的构建旨在说明如何构建通用嵌入,因此初始实现的许多特性被忽略了,因为它们增加了学习所不需要的复杂性(但对于优化性能却是必要的);第二,我们使用的数据集是微不足道的——当从头开始训练时,一本大约70000字的书不足以识别出许多规律,相比之下,最先进的嵌入技术通常是在TB规模的文本数据集上训练的。

在这个例子中,我们会展示如何使用 PyTorch nn.Embedding层通过设置一个名为CBOW分类的人工监督任务来从头开始训练嵌入在下一个示例中,我们将研究如何在一个语料库上预先训练一个嵌入,并将其微调使适用于另一个任务。

在机器学习中,使用在一个任务上训练的模型作为另一个任务的初始化器称为迁移学习(transfer learning)

亲爱的读者们,感谢您花时间阅读本文。如果您对本文有任何疑问或建议,请随时联系我。我非常乐意与您交流。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。