NLP 补充内容
AI 学习笔记系列的第三篇,介绍 NLP 的一些补充内容,包括指代消解、上下文化表示、编码器-解码器、分词、注意力、自注意力等概念。
指代消解(Coreference Resolution)
提及(Mention)是文本中对某个实体的提法,如 The man is Tom. He is a student.(这个男人是 Tom,他是一个学生。)里 The man、Tom、He 都是同一个实体的提及。
指代消解要做的是判断哪些提及其实指向同一个实体。
相关任务还有实体链接(Entity Linking / Wikification):把文本中的实体链接到外部知识库里的具体条目。
这类问题的挑战是搜索空间巨大:可能引用对数 |words| * |words|,可能的集合是所有提及的所有聚类方式。
有几种常见的简化思路:
- 先做提及检测(Mention Detection)并过滤不可能的提及
- 只链接提及对:对每个新提及,只找最可能共指的前一个提及
- 做传递闭包:如果 A 和 B 共指、B 和 C 共指,就把 A / B / C 放到同一簇
上下文化表示(Contextual Representations)
为了解释上下文化表示,我们先从**静态嵌入(Static Embeddings)**说起。
备注:静态嵌入不是上下文化的。嵌入(Embedding)是一个更广义的概念,指把离散对象(如词、句子、图等)映射到连续向量空间的过程。
早期的词向量通常是从表里查出来的,同一个词在任何上下文中都是同一个向量,这被称为静态词向量(Static Word Vectors),更一般的说法是静态嵌入。 常见例子包括:
- word2vec(CBOW):输入上下文词,输出一个词
- GloVe:统计共现;给每个词随机向量;更新向量使两向量点积近似其共现值
- FastText:将词表示为字符 n-gram 集合,学习 n-gram 向量,词向量为其 n-gram 向量和,用 word2vec skip-gram 训练
备注:词向量(Word Vector)是嵌入的一种特例,专门指词的连续表示。CBOW(Continuous Bag of Words)是 word2vec 的一种训练方法,输入是上下文词的向量平均,输出是中心词的概率分布。与之相对的是 Skip-Gram,输入是中心词的向量,输出是上下文词的概率分布。
如果数据与预训练数据差异很大(例如医疗记录、公司内部文档、邮件、即时通讯、短信),常见做法是微调(Fine-Tuning),即在任务数据上继续更新 embeddings。
面对一词多义问题(可以参考词汇关系数据库 WordNet),可以有两类思路:
- 为每个词义训练一个向量(例如使用语义标注语料 SemCor)
- 训练上下文化表示模型:同一个词在不同上下文输出不同向量
- 可以用 RNN 构建上下文化表示
- 可以同时在多个词上训练
- 可以用 LSTM
静态嵌入有两种常见的表示方式:
- 稠密表示(Dense Representation)指向量中大多数维度是非零值,且维度数相对较小。
- 稀疏表示(Sparse Representation)指向量中大多数维度是零值,且维度数相对较大。
word2vec 向量是稠密表示(Dense Representation),原因是:
- 大多数维度是非零值
- 向量维度远小于词表维度
WordNet 与向量都能表达词间关系,但它们表达关系的方式不同:
- WordNet:显式标注关系的数据库,关系可解释
- 向量:用空间位置关系表示(例如距离近表示相似),成本低且能表达“软关系”或关系强弱
静态嵌入的主要缺点是不能处理同一个词在不同上下文中意义不同的问题,这也是上下文化表示要解决的核心动机之一。
与静态嵌入相对,**动态嵌入(Dynamic Embeddings)**指同一个词在不同上下文中有不同的向量表示。
编码器-解码器(Encoder-Decoder)
编码器(Encoder)把输入映射到一个表示向量,解码器(Decoder)利用该表示逐步生成输出。
RNN 可以作为 Acceptor / Encoder。 把 Encoder 和 Decoder 组合起来可以得到编码器-解码器架构,最早成功应用是在机器翻译,后来也用于摘要、对话、代码生成等任务。
关于解码何时停止,常见做法有两种:
- 固定输出 token 数
- 生成到特殊
stoptoken 为止
实际系统往往更复杂,例如:
- 编码得到的上下文在每一步都传入 Decoder
- 多层模型
- 双向模型(Encoder 易做,Decoder 也可做,但算力代价更高)
Encoder 输出要传给 Decoder 的每一步,这是为了帮助模型生成时不至于过度偏离原输入。 Decoder 的第一个输入来自我们提供的固定值,后续每步输入 token 来自 Decoder 上一步输出。
我们可以利用教师强制(Teacher Forcing)来训练 Decoder。 Teacher Forcing 只在训练时使用:把 Decoder 每一步的输入从上一步模型输出改成上一步的真值输出。 它的目的可以理解为:避免模型在训练时过早偏离参考答案,否则长序列会越走越偏,训练会变慢。
编码器-解码器的常见推断方法包括:
- Top-1(贪心)
- 采样
- Beam Search(也可以在不同长度处停止)
编码器-解码器的典型问题包括:
- 瓶颈:所有信息都要通过一个向量
- 难并行:每步计算依赖前一步
Decoder 可以看成模型加推断方法的组合,用于生成序列输出。 例如翻译时,输入一句话,Decoder 按 token 逐步生成译文。
Encoder 和 Decoder 的结构不必相同,可以使用不同大小的权重矩阵、不同单元结构,甚至完全不同的模型(如 CNN 作 Encoder、RNN 作 Decoder)。 通用思想是一个网络处理输入并产出表示向量,另一个网络利用该表示逐步生成输出。
分词(Tokenisation)
分词即把文本切分成更小单位的过程。
我们先来思考一下机器翻译如何评估?
- 人工方式:流畅性(Fluency)、充分性(Adequacy)
- 自动方式:chrF(比较字符 n-gram)、BLEU(比较词 n-gram)
备注:n-gram 是指连续 n 个词的序列。
如果我们一直按空白分词,会遇到缩写和标点的处理问题。 一个好的分词方式至关重要,因为它直接影响模型的输入表示和性能。 分词也可能帮助处理低频词,此前我们可以用 FastText 的字符 n-gram 缓解,但还有另一种思路是拆成子词(Sub-tokens)。
子词词表的构建
子词词表的一个通用构建流程如下(做加法,不断把新子词加入词表):
- 词表初始化为全部字符
- 按某方法找两个词表项
- 为该配对(Pair)新建词表项并更新数据
- 若词表大小未达 $K$(如 100,000),回到第 2 步
上面第 2 步的“某方法”可以是:
- BPE(Byte-Pair Encoding):选相邻且最频繁的 pair
- WordPiece:训练 n-gram 语言模型;考虑所有可能词表 pair;选加入后最能降低困惑度的 pair
- HuggingFace WordPiece:选择最大化 $ \frac{|\text{combined}|}{|\text{first symbol}|\cdot|\text{second symbol}|} $ 的 pair
子词词表的另一个通用构建流程如下(做减法,不断删掉不重要的词表项):
- 词表初始化为所有字符和高频字符序列(直到整词)
- 用复杂方法找要删除的词表项
- 重复,直到词表降到目标大小
Unigram 算法就是用的这种思路。
SentencePiece 分词器是 Google 的一个工具,支持 BPE 和 Unigram 两种算法。
BPE
BPE 是现代 NLP 中最常用的子词分词方法之一,我们来以一个例子说明它的核心思想。
BPE 的目标是构建词表,也就是把所有词切分成哪些单位。
- 收集数据,先分词,再把词拆成字符,例如
Hi there!变为[["H", "i"], ["t", "h", "e", "r", "e", "!"]] - 建立仅含所有字符的初始词表
- 循环执行:
- 查看所有词表项 pair
- 统计该 pair 在数据中相邻出现频次(如
e和r的er出现多少次) - 把频次最高 pair 加入词表,并在数据中把该 pair 替换为合并后的新项
- 词表到达目标大小则停止
如此可逐步把词表项扩展为更长片段。
注意力(Attention)
当输入/输出很长时,信息沿 RNN 长链路传播会很难,这也是 Encoder-Decoder 架构的一个瓶颈问题。
这时候我们把目光回到人是怎么理解一句话的:我们会关注其中某些词,忽略其他词。 即我们可以给每个输出步提供一个对该步决策最有用的输入表示。
于是就引入了注意力机制,它允许模型在生成每个输出时动态地关注输入的不同部分。
它在不引入大量额外参数的情况下缓解了编码器-解码器的瓶颈问题。 注意力建立了从输入到输出的捷径,并沿该路径传递当前最相关信息。 它通过加权平均提供对当前输出步更相关的输入视图,不重要部分权重会更低。
备注:注意力权重有时候看起来像在做词对齐(Word Alignment),但它本质上只是为了当前输出步选取有用信息的一个加权分配,不是专门学出来的对齐标注。词对齐是一个更具体的概念,指的是在翻译等任务中标注哪些输入词与哪些输出词对应。
常见注意力形式包括:
- 点积注意力(Dot Product Attention)
- 缩放点积注意力(Scaled Dot-Product Attention)
- 乘性/双线性注意力(Multiplicative / Bilinear Attention)
- 低秩乘性注意力(Reduced-Rank Multiplicative Attention)
- 加性/前馈注意力(Additive / Feedforward Attention)
这些公式的细节就不展开了,但本质上都在计算加权平均所需的权重分数。
自注意力(Self-Attention)
注意力最早常出现在 Encoder-Decoder 里,我们可以用查值来理解:
- 键(Keys):编码器隐藏状态
- 值(Values):编码器隐藏状态
- 查询(Query):解码器隐藏状态
- 输出(Output):加权平均
既然它本质上是查表,那就不必限定 query 一定来自解码器、key / value 一定来自编码器。 我们完全可以让 query、key、value 都来自同一组输入表示。
这一步推广就是自注意力:对输入序列中的每个 token,都用它自己的表示当 query,同时把整段序列的表示当作 keys / values,于是每个 token 都能根据需要从其它 token 取信息,得到上下文化表示。
自注意力的一个基本流程如下:
- 初始词向量
- 变换得到 query、keys、values
- 点积得到注意力分数
- Softmax 得到分布
- 加权平均
- 前馈层
只有一组输入向量时,keys 和 values 相同,但 query 对每个词是不同的。
那么,我们还需要 RNN 吗? 很多场景下可以把 RNN 的循环依赖去掉,直接用注意力来建模序列中各位置之间的关系。 自注意力的核心能力是:让每个 token 在生成自己的表示时,都能看见同一句子里其他 token,从而得到上下文化表示。 不过,单靠自注意力并不自动等价于 RNN 的全部特性。
要补回 RNN 擅长的部分,通常还需要加入非线性变换(例如前馈网络)和显式的位置信息(位置编码),让模型知道顺序与距离,同时在生成式任务里还要加上因果约束(Mask),保证第 $t$ 步只能使用已知的历史信息,而不会偷看未来。 从这一步开始,我们就进入了 Transformer 和 LLM 的世界。
