Skip to main content

用子词信息充实词向量

词嵌入的下一步。
Created on March 3|Last edited on January 27
本报告是作者Devjyoti Chakraborty, Aritra Roy Gosthipaty所写的"Enriching Word Vectors with Sub-word Information"的翻译

引言

 查看仓库| Check out the Repository Repository查看代码

来源:
智能有时被定义为能够推理、能够理解逻辑。大多数情况下这没错,直到“教”电脑智能这个问题成为争论点。
在我看来,用知识库教电脑要被归为伪智能。电脑一直都被我们的知识数据库设定的规则和范式所束缚。电脑永远不能超出自己的方式去推理、去理解逻辑。从本质上来说,那近似于马戏团的狮子被教导去跳过火焰圈以换取食物,如果看起来有难度,它周围就满是食物。
那么我们如何解答这样一个问题?就是在不明确说明一切的情况下教电脑智能。对此,我们不妨改一下过程。如果不在知识库中定义好一切,我们可不可以通过n维空间的数字(向量)让电脑理解对象的意思?
我们举个例子。假设我们想让电脑懂得灯泡💡这个概念。我们用一个二维空间,其中一维描述光照强度,另一维描述对象大小。用这样一个简单配置我们就可以把灯泡表示为二维空间中的一个点。这种空间让电脑不但可以理解灯泡,现在还会理解筒灯。利用对象的向量表征,电脑现在可以轻微调整这个点并利用它去理解别的对象。这是不是电脑理解事物的更简单方法?如果你赞同,那么你就和人工智能教父Geoffrey Hinton站在了同一边。那么你对“智能就是向量”这种观点有什么看法?
用向量表示词语是个酝酿已久的概念。我们以前有关Word2VecGloVe的文章探讨了这一概念的本质。其中我们不仅讨论了这一概念背后的直觉,还在嵌入层一路编程。这篇文章作为后续报告。我们尝试解读一篇论文 用子词信息充实词向量(Enriching Word Vectors with Subword Information),该论文由Piotr Bojanowski等人所著。这被认为是Word2Vec的直接继承者。其中作者们考虑到了单个词的词态学,就是为了得到更好的向量表征。

直觉

我们来讨论一下Word2Vec。针对Word2Vec的那个提议简单但很强大。
一个词的意思取决于其上下文。
这导致被构思出来两种模式,即Skip-Gram和CBOW,它们利用那个概念把词语编译成向量。关于词向量最有趣的领悟就是,训练完备的词向量的线性代数直接转化为逻辑。一个非常有名的例子就是“国王-男人+女人=女王”。感兴趣的读者可转至有关Word2VecGloVe 的文章,可以更好地了解这一主题。
借助于向量表征和联想(上下文),就推出了word2vec这一独创概念,我们能够通过N维空间中的向量把词语的意思教给机器。但由于我们把每个词当做向量来教授,我们完全忽略了词语背后的词态学和词源学。这导致信息的关键部分被忽略,正是这些关键信息告诉我们一个词是如何产生的、一个词的子词是如何被关联到其它词的。
当写一篇渊博的论文,或者要表达一个复杂的思想或情感,就会使用生僻、复杂的词语。如果有人尝试通过词语联想路由(Word2Vec、GloVe系列)训练模型,在这种情况下就可能会失败。另一方面,这些生僻词语或许能通过词态学进行译解,因为它们是通过别的词的一部分构造而来,或者通过在词源学上可译解的字符构造而来。某些语言,如梵文或者其现代衍生品印度语,非常适合这种训练方式,因为这些语言是从路由音节派生而来。
理解词态学
摘自那篇论文:
比如说,在法语和西班牙语中,大部分动词有超过40种词形,而芬兰语的名词有15种形式。这些语言包含很多词形,这些词形极少(或者根本不会)在训练语料库中出现,造成难以学习较好的词语表征。因为很多词形遵循规律,通过利用字符级的信息有可能提高词态丰富的语言的向量表征。
该论文作者建议对Skip-Gram模型进行扩展。Skip-Gram模型背后的关键思想就是,给定单个中心词/目标词之后,预测多个上下文词。
比如,看这些句子:
  1. “红色突然出现”
  2. “什么是绿色的?”
  3. “黄色色调适合你”
如果我们在上面这些句子上训练模型,我们最终会看到“色”字与“红”“绿”“黄”这些字有关。通过词语联想找到“色”字的意思这个基本目标就真的实现了。
另一方面,语言作为一种实体,有更加深的、明确的意思。估计世界上有7000种人类语言,每一种语言都有与众不同的方言和明确的词汇。为了搞明白某种语言中的某些词语是如何产生的,意味着要把词分解为明确的子词。不过,把这个任务交给word2vec模型意味着我们选择忽略语言的含蓄之美。看下面的例子:
一个词态学例子
“unreadable”(不可读)这个词可以轻松地分解为其构成子词,那么它的意思就明确了。该论文想把这种重要信息加入到skip-gram模型,其方法就是通过把目标词/中心词替换为其构成子词。这样一来,我们还能把“unreadable”的子词关联到别的词,如“unstoppable”。尽管这两个词意思不同,但都有同样的子词。这就意味着它们的信息曾经有着相似的思想。这种方法的另一个帮助就是,词语的意思现在如何取决于两个因素:词语联想和词态学。这就是说,生僻词语现在也可以被译解,即使很少出现。

目标

Piotr Bojanowski等人在该论文中提出的方法是Skip-Gram模型的直接扩展。在深入探究子词空间以前,我们来回顾一下Skip-Gram。
给定一个大小为 WW,的词汇表,每个词根据索引值 w{1,...,W}w\in \{1,...,W\} 来分辨,目标就是为每个词w学习一个向量表征。给定一个大型的训练语料库并表示为一个词序列 w1,...,wT w_{1},...,w_{T} ,skip-gram模型的目标就是使下列对数似然函数最大化:
J(θ)=t=1TcCtlogp(wcwt)J(\theta)=\sum ^{T}_{t=1}\sum _{c\in C_{t}}\log p( w_{c} |w_{t})

其中上下文CtC_t是目标词 wtw_t周围词语的索引集合。
问题就是对数似然函数的参数化,具体就是“我们调节什么能使那个对数似然函数最大化?”其答案就在这个概率函数。
p(wcwt)=exp(s(wt,wc))j=1Wexp(s(wt,j))p( w_{c} |w_{t}) =\frac{\exp( s( w_{t} ,w_{c}))}{\sum ^{W}_{j=1}\exp( s( w_{t} ,j))}

这个概率函数其实就是个柔性最大化函数。其中 s(x,y)s(x,y)被认为是评分函数,用来计算向量 xx 与向量 yy之间的相似度。在最大化那个对数似然函数过程中所调节的参数就是词语的向量表征。目标函数是这样的,更好的词向量表征会使损失减小。
通过运用柔性最大化函数,我们得到一个更高的概率分布,使我们能够仅专注于一个上下文词
然而,这样一个模型不适合我们这种情况,因为它意味着,给定一个词 wtw_t ​,我们只能预测一个上下文词wcw_c
这导致了要有差别地构造概率函数。现在预测上下文词的问题被认定是一个二元分类任务。它变成这样一个任务,就是独立地预测上下文词的出现或不出现。当使用图中的负采样,这个二元分类任务产生两种上下文词——正和负。正上下文词就是与目标词位于同一窗口的词语,而负上下文词是窗口之外的所有词。

子词空间

通过为每个词使用单独的向量表征,skip-gram模型忽略词的内部结构。
为了攻克这一问题,作者们提出了一个不同的评分函数。若要深入理解那个评分函数,还需要理解他们提出的配置。他们把每个词视为一包n元字符。他们考虑到前缀和后缀与其它字符序列之间的界限,还在每个词的首尾添加边界特殊字符<<和>>。他们把词语w本身加入到该词的n元集合。我们通过一段代码加以理解。
>>>word = "where"
>>>word = f"<{word}>"
>>>n_grams = [word[i:i+3] for i in range(len(word)-2)]+[word]
n_grams
>>>n_grams
['<wh', 'whe', 'her', 'ere', 're>', '<where>']
现在,我们的评分函数 s(wt,wc)s( w_{t} , w_c)取两个向量作为参数,即目标向量和上下文向量。现在框架中有了子词,评分函数更改为只包含目标词语的子词。这就是说,分数计算结果是上下文向量和全部n元目标向量的标积。
假设给你一个大小为G的n元字典。给定一个词 ww,我们把 ww中的n元集合表示为 Gw{1,...,G}G_w \sub \{1,...,G\} t。我们把一个向量表征 zgz_g 关联到每个n元 gg。我们把词语表示为其n元向量表征的总和。我们就得出评分函数:
s(w, c)=gGwzgTvcs( w,\ c) = \sum\limits _{g\in G_{w}} z^{T}_{g} v_{c}

一定要注意的是,我们把目标词语的向量表征视为其n元向量表征的总和。
其中 zgz_g  表示对应目标词语的n元中的每一元。例如,如果目标词语为'<where> ';就是'<wh', 'whe', 'her', 'ere', 're>'和'<where>'组成的向量。一定要记住,某个词的子词也会出现在别的词中。共有信息共享就是这样进行的。这个简单模型使框架能够在词语之间共享子词信息。

代码

 查看代码|  查看仓库

在这部分,我们来探究论文的TensorFlow实现。代码大大受益于TensorFlow Word2Vec官方指南
代码的最重要部分就是准备数据。我们用的数据是一个文本文件,其中有很多句子并用回车符分开。
# Create a `tf.data` with all the non-negative sentences
>>> text_ds = tf.data.TextLineDataset(path_to_file).filter(lambda x: tf.cast(tf.strings.length(x), bool))

>>> for text in text_ds.take(5):
print(text)

tf.Tensor(b'First Citizen:', shape=(), dtype=string)
tf.Tensor(b'Before we proceed any further, hear me speak.', shape=(), dtype=string)
tf.Tensor(b'All:', shape=(), dtype=string)
tf.Tensor(b'Speak, speak.', shape=(), dtype=string)
tf.Tensor(b'First Citizen:', shape=(), dtype=string)

# We create a custom standardization function to lowercase the text and
# remove punctuation.
def custom_standardization(input_data):
lowercase = tf.strings.lower(input_data)
return tf.strings.regex_replace(lowercase,
'[%s]' % re.escape(string.punctuation), '')

# Define the vocabulary size and number of words in a sequence.
vocab_size = 4096
sequence_length = 10

# Use the text vectorization layer to normalize, split, and map strings to
# integers. Set output_sequence_length length to pad all samples to same length.
vectorize_layer = TextVectorization(
standardize=custom_standardization,
max_tokens=vocab_size,
output_mode='int',
output_sequence_length=sequence_length)

# build the vocab
vectorize_layer.adapt(text_ds.batch(1024))
我们得到单词(token)之后,需要创建一个配置,用来协助学习过程的有监督配置。
Source: Word2Vec
. 对于我们的训练过程,我们不使用tf.random.log_uniform_candidate_sampler,而是自定义该过程,从而使加入到训练过程的负例会更好。查看该StackOverFlow话题并关注讨论。

对于我们的子词,配置稍有改动。我们的初始位置放的不是目标词索引(如图所示),而是目标词语的子词的索引。
<PrefetchDataset shapes: (((1000, None), (1000, 5, 1)), (1000, 5)),数据集形如:<预取数据集形式: (((1000, None), (1000, 5, 1)), (1000, 5)),类型:((tf.int32, tf.int64), tf.int64)>
  • (1000, None) - 1000打n元;
  • (1000, 5, 1) - 1000打5条上下文词。
  • (1000, 5) - 1000打5条标签。
模型相当简单
class Word2Vec(Model):
def __init__(self, subword_vocab_size, vocab_size, embedding_dim):
super(Word2Vec, self).__init__()
self.target_embedding = Embedding(subword_vocab_size+1,
embedding_dim,
input_length=None,
name="w2v_embedding",)
self.context_embedding = Embedding(vocab_size+1,
embedding_dim,
input_length=num_ns+1)
self.dots = Dot(axes=(3,1))
self.flatten = Flatten()

def call(self, pair):
target, context = pair
we = tf.math.reduce_sum(self.target_embedding(target),axis=1)
ce = self.context_embedding(context)
dots = self.dots([ce, we])
return self.flatten(dots)
我们定义两个嵌入层。然后通过点积对上下文嵌入和目标词嵌入进行评分。接下来,评估该分数,损失被反向传播以微调嵌入。

结果

模型的损失和准确率如下所示。看起来两个指标的结果都不错。

Run set
0


嵌入投影

\寻找嵌入的另一个方法就是在投影仪(projector)上观察。TensorFlow对此有一个强大的可视化工具。你可以为嵌入创建vector.tsvmetadat.tsv,然后载入投影仪。投影仪运用了和PCA一样的降维法,用于把数据合并到一个视觉向量空间,但仍然能保留重要信息。为了大家方便,我们把tsv文件上传到了wandb项目的制品(artifact)。可随意下载该制品并使用之。我们还上传文件至GitHub仓库

上下文

看到损失非常稳定之后,我们决定看一下上下文嵌入成什么样了。我们选择“for”作为检索词。注意一下像“the”“a”“of”“with”这些单词是如何显示在最近的。这就是说我们的模型学会了如何把带有主要语法意义的词语归到一起。
查找的词:“for”

接下来,我们选择一个生僻点儿的词“secrets”,我们看到模型找到了一些词语,如“safeguard”“Signal”“strangely”。要知道我们的数据是莎士比亚的一些文章,相对来说模型挺出色的!
查找的词:“secrets”

目标

目标嵌入包含了从给定数据构造出的全部子词。我们首先查找短语“<th”。回顾一下我们的主要目的是利用子词来关联不同的词。我们非常成功地实现了我们的目的,因为我们看到像“the”“they”“them”“this”“thou”“thy”这些词和短语“<th”离得最近。
查找的词:“<th”

当搜索“the”这个词,我们看到了类似的结果。有相似子词的词语出现为最近的邻居。我们可以成功地下结论:我们已经通过我们的模型获得了词语的词态学。大家可以尝试用不同大小的n元进行实验,以获取更多语义意义。
查找的词:“<th”

结论

本论文所描述的作品是word2vec的广受欢迎继承者,继而成为构建 fastText的三个先决条件之一(fastText被认为是自然语言处理工作的基准,因为它使用很多概念来生成高效的词语表征)。
我们的实验表明,该论文利用极简方法来学习词语表征,在其过程中同时也学习子词信息。作为skip-gram方法的完美继承者,该模型训练速度比其它传统模型快得多,胜过那些没有子词信息和词态学分析的基准。
总之,利用子词信息让我们更接近真正使电脑最大限度地利用语言之美和语言的力量。

作者:
姓名TwitterGitHub
德夫乔蒂·查克拉博蒂(Devjyoti Chakraborty)@Cr0wley_zz@cr0wley-zz
阿里特拉·罗伊·戈斯蒂帕蒂(Aritra Roy Gosthipaty)@ariG23498@ariG23498

Iterate on AI agents and models faster. Try Weights & Biases today.