想象你养了一只变色龙和一本色卡手册。色卡手册里的每个词只有一种固定颜色:“bank“这张色卡永远是同一种蓝,不管它出现在”river bank(河岸)“旁边还是”money bank(银行)“旁边。变色龙则完全不同:它先看看左边的单词,再看看右边的单词,然后才决定自己今天要”显“什么颜色。ELMo(Embeddings from Language Models,来自语言模型的嵌入)就是这样一只变色龙——它用双向 LSTM 先从左到右、再从右到左读完整个句子,为每个词现场调制一个”随上下文而变”的动态词向量。而 word2vec 就是那本色卡手册,训练完就定格成一张静态查找表,永远无法区分多义词。本文从 word2vec 的局限出发,拆解 ELMo 的双向架构、字符级编码、预训练流程和下游应用,看看它是如何开创上下文词嵌入这个新纪元的。
理解 ELMo 是理解 BERT、GPT 等现代预训练模型的起点。掌握“上下文相关词向量”的思想,你才能解释为什么同一个词在不同语境下需要不同的向量——这是当前 NLP 工程师的基本功。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
静态词向量的局限
word2vec 训练完成后,就像一本厚重的色卡手册——每个词对应一张固定色卡。“bank“这张色卡印刷着唯一的颜色,无论你把它贴在”river bank(河岸)“旁边,还是”money bank(银行)”旁边,它都倔强地保持原色。这正是静态词向量的第一性局限:词与句子无关。
word2vec 通过 skip-gram 或 CBOW 在大规模语料上学习“词 → 向量”的映射,训练目标是让出现在相似上下文中的词获得相近的向量。但要注意,这里的“上下文”是海量语料统计出来的平均结果,训练一结束就冻结成了静态查找表。推理时刻,无论句子是什么,模型只是机械地查表——这种“上下文无关”的特性,让词向量无法感知当下语境。
多义词困境由此而来:“bank“在语料中既经常与”river、water“共现,又经常与”money、loan“共现,最后学到的向量是两种语境的”平均色“——像把红色和蓝色颜料倒在一起,得到浑浊的灰紫色,既不像河岸,也不像银行。”苹果”这个词同样尴尬,它既出现在水果语境,又出现在手机品牌语境。形式化地看,word2vec 的推理就是一次查表:
1. 查表操作 Lookup:把单词 w 当作行号,从参数矩阵中直接取出一行向量,整个过程不关心句子里的其他词。
2. 嵌入矩阵 E:训练得到的参数表,每行对应词表中的一个词,维度通常取 100 或 300。
3. 词向量 v_w:最终输出的固定向量,w 无论出现在哪个句子里,取出的 v_w 都一模一样。
色卡手册的局限一目了然:它印刷的是“词的平均长相”,而不是“词的当下长相”。要让"bank"在河岸边变成青色、在银行门口变成金色,必须引入上下文——这就是 ELMo 登场的理由。
ELMo 的核心思想
现在轮到变色龙登场。ELMo 不查色卡,而是像变色龙一样先环顾四周:看看左边是什么词,再看看右边是什么词,最后才决定自己此刻“显什么色”。同一个“bank“,在”river bank“里变成河岸的翠绿,在”money bank”里变成金库的金黄——向量随句子不同而不同。
ELMo 全称 Embeddings from Language Models(来自语言模型的嵌入),由 AllenNLP 团队于 2018 年提出。它的核心主张只有一句话:词表示应当是“词在句子中的函数”,而不是“词类型的查表结果”。于是每个词在每句话里的向量都是现场计算出来的——这正是“动态词向量”与“静态词向量”的分水岭。
具体做法是:用一个在大语料上预训练好的多层双向 LSTM 语言模型读取整个句子,每一层都会为第 k 个词产出一个隐藏状态向量。ELMo 的最终表示不是只看最后一层,而是把所有层的向量按权重混合——低层偏向词法与句法特征,高层偏向语义与上下文特征,权重交给下游任务自己学:
1. 第 j 层隐藏状态:h_{k,j} 是第 j 层 LSTM 对第 k 个词输出的隐藏向量,其中 j=0 表示最底层的字符编码向量。
2. 层权重 s_j:每个任务重新学习一组权重,经 softmax 归一化后总和为 1,表示任务对各层信息的侧重。
3. 缩放因子 γ:任务相关的整体缩放,把 ELMo 向量的数值范围调整到适合下游模型的量级。
4. 加权求和:把各层向量按权重相加,得到第 k 个词的动态表示。
变色龙与色卡的本质区别:色卡的颜色在印刷时就死了,变色龙的颜色在观察时才诞生。ELMo 想做的,就是让每个词都变成一只时刻观察环境的变色龙。
双向 LSTM 架构
想象一句“bank is near the river“被两个读书人同时朗读:正着读的人 A 从左往右念,读到”bank“时他只知道前面有”the“;倒着读的人 B 从右往左念,读到”bank“时他已经把”river、the、near、is”都读完了——于是 B 能立刻断言:这里的 bank 是河岸。
单向语言模型只能做 A 做的事:给定前文预测下一个词。但判断"bank"的词义,答案常常藏在后文里(比如 river 或 money 出现在句尾)。因此 ELMo 训练两个方向的 LSTM 语言模型:前向 LSTM 根据 t₁ 到 t₍ₖ₋₁₎ 预测 tₖ;后向 LSTM 反过来根据 t₍ₖ₊₁₎ 到 tₙ 预测 tₖ。两者共享词嵌入和输出 softmax 层,联合目标函数是两个方向对数似然之和:
1. 对全部词累加:从第 1 个词到第 N 个词,把每个词的前向与后向对数概率相加,构成整个句子的分数。
2. 前向项:给定 t₁ 到 t₍ₖ₋₁₎,模型预测第 k 个词,取对数概率。
3. 后向项:给定 t₍ₖ₊₁₎ 到 tₙ,模型反向预测第 k 个词,取对数概率。
4. 参数共享:两个方向共享输入词嵌入与输出 softmax 参数,但前向、后向 LSTM 各自拥有独立权重。
训练完成后,第 k 个词在每一层的隐藏状态就是两个方向的拼接:hₖ = [hₖᵗᵒ ; hₖᵗᵃ]。两个读书人互为对方的“环境”,变色龙这才看得清完整的四周。
字符级编码
变色龙识别环境靠的是皮肤上细小的感光颗粒,而不是整块颜色。ELMo 同样不把单词当作不可拆分的整体,而是从构成它的字母颗粒开始感知——这是它与 word2vec 的另一个本质区别。
word2vec 需要一张包含全词表的色卡手册,遇到手册外的词(比如"banking"少见)就束手无策,只能报错或给零向量。ELMo 则把每个词拆成字符序列,用字符级 CNN 生成该词的输入向量:
卷积核在字符序列上滑动,捕捉 n-gram 级别的词形特征:前缀(un-、re-)、后缀(-ing、-ed)、词根(bank-)。这些特征经过最大池化和 highway network 后,得到固定维度的词向量 xₖ,作为 LSTM 的输入。这样做有三大好处:一是形态相近的词(bank、banker、banking)向量天然接近;二是未登录词(OOV)也能凭借字母形态获得合理向量;三是词表规模从几十万词降到几千字符,模型更轻。
1. 字符矩阵 C_k:第 k 个词的每个字母映射为一个向量,按顺序排成矩阵。
2. 卷积与池化:多个卷积核在字符序列上提取 n-gram 特征,再池化成定长向量。
3. 词输入向量 x_k:得到的向量作为双向 LSTM 的输入,完全替代了 word2vec 式的查表操作。
变色龙靠颗粒质感识别从没见过的昆虫:不用翻色卡,看花纹就知道该显什么色。ELMo 用同样的逻辑面对未登录词。
预训练与微调
变色龙不是天生就会在钢筋水泥里伪装——它先在很多环境里被“训练”过,换个新家只需稍作适应。ELMo 就走这样一条两阶段路线:预训练 + 微调,这也是后来 BERT、GPT 一直沿用的范式。
预训练阶段,双向 LSTM 语言模型在超大语料(如 30 亿词的 1 Billion Word Benchmark)上学习“猜词”。它不关心具体任务,只学会一件事:根据上下文猜出当前位置的词。这个阶段产出的是整个双向语言模型——各层 LSTM 权重、字符编码器、softmax 输出层,而不是一张向量表。
微调阶段,下游任务(比如 NER 或情感分析)拿到预训练好的 biLM,在自己的数据上学习层权重 s 和缩放因子 γ,通常还会把 ELMo 向量与任务原有的词向量拼接后送入任务模型。实践中,微调时保留语言模型损失作为正则效果更好:
1. 任务损失 L_down:下游任务带标注的有监督损失,例如 NER 的交叉熵损失。
2. 语言模型损失 L_LM:双向语言模型的“猜词”目标,微调时保留它,可防止模型遗忘通用语言知识。
3. 平衡系数 λ:控制任务损失与语言模型损失的比例,通常取 0.001 到 0.1 的小值。
变色龙换新家后,会保留野外学到的本能,再针对新环境微调配色——ELMo 的“预训练 + 微调”正是这种“保留常识、快速适应”的智慧。
下游应用
变色龙进了森林。森林里(NER 任务)它把自己伪装成树皮,沙漠里(情感分析)它融入黄沙——同一只变色龙,不同场景展现不同本领。ELMo 的动态向量,让下游任务第一次享受到“按语境取义”的红利。
命名实体识别(NER)要求给每个词标注人名、地名、机构名等实体类型。“Apple 发布新款 iPhone”中的 Apple 是机构,“他啃了一口苹果”中的苹果是水果——word2vec 的静态向量无法区分,ELMo 读完整个句子后能给出不同向量,实体分类器自然更容易判对。情感分析同样受益:“这部电影很无聊”里的“无聊”是负面,“他无聊地翻了翻杂志”里的“无聊”近乎中性,上下文向量让模型不再误杀。
下游模型的用法很直接:把第 k 个词的 ELMo 向量当作特征,接到任务自己的分类器上:
在 ELMo 原论文中,仅此一个特征就让多个任务刷新纪录:SQuAD 阅读理解提升 4.7 个百分点,CoNLL-2003 NER 提升 2.1 个 F1,SNLI 蕴含推断提升 1.4 个百分点——而且完全不需要改动任务模型的结构。
1. ELMo 特征向量:第 k 个词的动态表示,来自双向语言模型各层的加权融合。
2. 线性变换:权重矩阵 W 与偏置 b 把特征向量映射到各类别的分数。
3. softmax 归一化:将各类别分数转换为概率,取最大概率对应的类别作为预测标签。
变色龙在森林里是树皮色,在沙漠里是沙黄色——同一个词在不同语境中有不同的“伪装”,这正是上下文词嵌入的精髓,也是 ELMo 留给 NLP 世界最重要的遗产。
AI 应用场景
医疗文本命名实体识别
阶段:特征表示(模型输入层)
解决的问题:病历中“患者否认服用阿司匹林”里的“阿司匹林”是药物实体,而“阿司匹林耐受性试验”里它是文献概念。传统 word2vec 给“阿司匹林”一个固定向量,模型频繁误标或漏标。ELMo 的上下文向量让同一词在不同句子里携带不同语义,实体边界和类型判断同时受益。
具体案例:某三甲医院临床科研文本结构化项目,将 ELMo 向量拼接到 BiLSTM-CRF 的输入层,药物、疾病、症状三类实体 F1 提升约 2 个百分点;否定句(“否认/无/未见”)场景下的漏报明显减少。
电商评论情感分析
阶段:特征表示 / 分类模型输入
解决的问题:“苹果手机拍照好看”和“这个苹果很甜”里都有“苹果”,静态向量无法区分品牌与水果,导致“苹果很甜”被误判为夸手机。ELMo 先借助上下文消歧,再判断情感,粒度细得多。
具体案例:某电商平台评论情感分类管线中,用 ELMo 替换 word2vec 特征后,手机类目评论的情感准确率提升 1.8%;对“新品价格真香”“这瓜不保熟”这类需要语境的评论,错误率显著下降。
搜索查询词义消歧
阶段:查询理解(Query Understanding)
解决的问题:用户搜"bank",搜索引擎需要判断他是想找河岸、银行还是篮球动作。ELMo 结合用户当前会话上下文,为查询词生成动态向量,再与文档向量匹配,召回更精准。
具体案例:某搜索团队把 ELMo 用于查询建议和召回排序的辅助特征,多义词查询的点击率提升约 3%,“苹果”类查询的结果相关性明显改善。
Python 演示
# -*- coding: utf-8 -*-"""迷你 ELMo 演示: 静态词向量 vs 上下文相关词向量"""import numpy as np sentence_river = "the bank is near the river".split()sentence_money = "the bank is open for money".split() rng = np.random.RandomState(42)vocab = sorted(set(sentence_river + sentence_money)) # 静态词向量: 每个词一张色卡, 与上下文无关static_vec = {}for w in vocab: v = rng.randn(8) static_vec[w] = v / np.linalg.norm(v) def lookup(words, i): return static_vec[words[i]] def avg_pool(ws): if not ws: return np.zeros(8) return np.mean([static_vec[w] for w in ws], axis=0) def context_embedding(words, i): fwd = avg_pool(words[:i]) bwd = avg_pool(words[i + 1:]) return np.concatenate([fwd, bwd]) def cosine(a, b): return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9)) def char_embedding(word): feats = np.zeros(16) for ch in word: feats[hash(ch) % 16] += 1.0 norm = np.linalg.norm(feats) return feats / norm if norm > 0 else feats def main(): i_river = sentence_river.index("bank") i_money = sentence_money.index("bank") s_sim = cosine(lookup(sentence_river, i_river), lookup(sentence_money, i_money)) c_sim = cosine(context_embedding(sentence_river, i_river), context_embedding(sentence_money, i_money)) print("word2vec 静态向量: 两个 bank 的余弦相似度 = %.4f" % s_sim) print("ELMo 式上下文向量: 两个 bank 的余弦相似度 = %.4f" % c_sim) print("未登录词 banker 的字符向量长度 = %.4f" % np.linalg.norm(char_embedding("banker"))) if __name__ == "__main__": main()
static_vec[w] = v / np.linalg.norm(v)给每个词做一张“归一化色卡”,向量长度固定为 1,方便计算余弦相似度。def lookup(words, i):word2vec 式查表函数:只看词本身,完全不看前后文。return static_vec[words[i]]同一个词永远返回同一个向量,与句子无关。fwd = avg_pool(words[:i])模拟前向 LSTM 的输入:只用第 i 个词左侧的上下文。bwd = avg_pool(words[i + 1:])模拟后向 LSTM 的输入:只用第 i 个词右侧的上下文。return np.concatenate([fwd, bwd])把左右两个方向的信息拼成一个向量,类似 ELMo 拼接双向隐藏状态。feats[hash(ch) % 16] += 1.0字符级特征:每个字母投票进哈希桶,未登录词也能得到向量。s_sim = cosine(lookup(sentence_river, i_river), lookup(sentence_money, i_money))静态向量下两个 bank 的相似度。c_sim = cosine(context_embedding(sentence_river, i_river), context_embedding(sentence_money, i_money))上下文向量下两个 bank 的相似度。
常见误区
误区我以为 ELMo 和 word2vec 一样,训练完存一张“词 → 向量”的大表,只是多了个 LSTM 帮忙查表,同一个词在任何句子里的向量还是一样的。
正解ELMo 根本不存“词 → 向量”表。它保存的是整个预训练好的双向语言模型(各层 LSTM 权重、字符编码器、softmax 层)。推理时用当前句子现场计算每个词的向量,同一个词在不同上下文得到的向量必然不同。所谓“动态”,是计算方式的动态,而不只是把表变大。
为何常见:word2vec 的查表心智太强,初学者容易把“更强大的词向量”想象成“更大的词向量表”。
误区我理解的“双向”就是两个 LSTM 各算各的,最后把输出向量拼起来完事,像把两根绳子拧成一根。
正解拼接只是表象。前向和后向 LSTM 在数学上是两个独立的语言模型,各自拥有独立的隐藏层参数;它们通过共享输入词嵌入、共享输出 softmax 参数,以及联合目标函数间接“对齐”。更关键的是,联合训练让每个位置既能被前文预测、也能被后文预测,信息融合发生在整个模型的训练目标里,而不仅仅在最后一步拼接。
为何常见:各种教程的架构图都画着两个箭头指向一个 concat,容易让人误以为双向只是“最后拼一下”。
误区反正 BERT 效果更好,ELMo 是淘汰技术,面试和工作里都不用管它。
正解ELMo 是第一个证明“上下文相关词向量 + 预训练微调”有效的工作,BERT 的掩码语言模型直接继承并升级了它的思路。理解 ELMo 才能理解 NLP 预训练范式的演进脉络;面试中它常作为“静态词向量 → 上下文词向量”的转折点被考察。
为何常见:只看排行榜和最新模型,忽略思想史;但面试考的是理解深度,而不是模型新旧。
误区我以为有现成的 ELMo 词向量文件,像 word2vec 一样下载后按 key 就能拿到所有词的向量。
正解ELMo 发布的是预训练 biLM 模型文件(几百 MB),而不是向量表。使用时要先让模型“读”你的句子,逐词算出向量;并且在下游任务里还需要训练各层权重 s 和缩放因子 γ,才能得到任务最优的 ELMo 表示。它是“特征提取器”,不是“查表字典”。
为何常见:word2vec 以 .vec 文件形式分发得太成功,以至于“词向量 = 文件查表”成了默认联想。
面试题
Q1. word2vec 静态词向量的根本局限是什么?
- A.训练速度比较慢
- B.一词一义难分多义词← 正确答案
- C.无法处理英文语料
- D.词向量维度偏高
答案:B。word2vec 训练完就把每个词固化成一张“色卡”,无论多义词出现在什么语境,都取同一个向量,这正是多义词困境的根源。A、C、D 都是工程问题而非本质局限。
Q2. ELMo 中“上下文相关的词向量”指的是什么?
- A.每个句子中同词向量相同
- B.同词在不同句子向量不同← 正确答案
- C.词向量由字符个数决定
- D.词向量完全随机生成
答案:B。ELMo 的核心理念就是词向量随句子“现场计算”,同一个词在不同的句子里向量不同,像变色龙随环境变色。A 是 word2vec 的行为,C、D 与上下文无关。
Q3. ELMo 中后向语言模型的预测方向是什么?
- A.根据前文从左往右预测
- B.根据后文从右往左预测← 正确答案
- C.同时利用前后文进行预测
- D.随机选取一个方向预测
答案:B。后向语言模型是“根据后文猜前文”:给定第 k+1 到第 N 个词预测第 k 个词。A 是前向模型的行为,C 是双向合起来的效果,D 是干扰项。
Q4. ELMo 的最终向量由哪些成分组合而成?
- A.各层 LSTM 隐藏状态加权← 正确答案
- B.任务可学习的缩放标量← 正确答案
- C.词表查询得到的唯一向量
- D.字符级编码生成的基础向量← 正确答案
答案:ABD。ELMo 最终表示 = 各层隐藏状态加权和 × 任务缩放 γ,其中第 0 层就是字符编码向量,所以 A、B、D 都对。C 错在 ELMo 并没有“词到向量”的查表环节——它根本不存这样的表。
Q5. 在 ELMo 表示公式中,符号 s_j 的含义是什么?
- A.softmax 归一化层权重← 正确答案
- B.训练阶段的学习率
- C.语料中的单词词频
- D.句子中的位置编码
答案:A。公式里 s_j 是 softmax 归一化后的层权重,总和为 1,表示任务倾向使用哪一层的信息。B 是训练超参,C 是词频,D 是位置编码,都与层级融合无关。
Q6. 字符级编码(CharCNN)最主要的好处是什么?
- A.能处理未登录词← 正确答案
- B.训练速度大幅提升
- C.参数数量显著减少
- D.完全不需要预训练
答案:A。字符级 CNN 从字母序列生成词向量,遇到没见过的词也能靠词形特征给出合理向量,直接解决 OOV 问题。B、C、D 都不是字符级编码的核心收益。
Q7. ELMo 的动态词向量能直接提升以下哪些任务?
- A.命名实体识别任务← 正确答案
- B.文本情感分析任务← 正确答案
- C.词语词义消歧任务← 正确答案
- D.数据库索引的优化
答案:ABC。ELMo 的上下文向量对 NER、情感分析、词义消歧这类依赖语境的任务都有显著提升,原论文在 SQuAD、CoNLL-2003 等任务上刷新了纪录。D 是数据库领域问题,与 NLP 无关。
Q8. 预训练完成后,ELMo 保存下来的核心产物是什么?
- A.每个词一条固定向量
- B.一个双向语言模型← 正确答案
- C.一棵决策树分类器
- D.一份词频统计表
答案:B。ELMo 的产物是预训练好的双向语言模型,推理时用句子现场“算”向量。A 是 word2vec 的产物,C、D 与 ELMo 无关。
Q9. ELMo 中双向联合训练的目标函数是什么?
- A.前向与后向对数似然之和← 正确答案
- B.仅最大化前向概率似然
- C.仅最大化后向概率似然
- D.最大化词频的交叉熵
答案:A。ELMo 的训练目标 = 前向语言模型对数似然 + 后向语言模型对数似然,两个方向一起优化。B、C 只取了一半,D 与语言模型目标无关。
Q10. 关于 ELMo 的层级加权表示,下列说法正确的有哪些?
- A.不同层捕捉不同粒度信息← 正确答案
- B.各层权重由下游任务学习← 正确答案
- C.可以只取最后一层特征← 正确答案
- D.所有层权重必须完全相等
答案:ABC。论文发现低层偏向词法句法、高层偏向语义,任务学习的权重可以灵活侧重;只用最后一层效果也接近最优,所以 A、B、C 都对。D 与“softmax 加权学习”的设计矛盾。