news 2026/10/18 12:21:58

Transformer编码解码与token化:语义向量提取与相似度计算实操

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Transformer编码解码与token化:语义向量提取与相似度计算实操

我印象里最直观的一次,是某次给一个跨语言检索项目做语义召回优化。我用“某预训练模型”跑完一个句子的编码,把它的CLS向量取出来做相似度计算,结果发现它跟一个看起来完全不相关的中文句子得分极高,排在前面的结果也莫名相似。后来把中间层的token向量全部拉出来做了个可视化和方差分析,才发现问题出在模型顶层输出分布过于各向异性,很多维度其实都是死维度,根本不参与语义区分。那次之后我对“编码解码、token化、语义向量”这三个词背后的机制就格外敏感——它们不是三个独立概念,而是同一条流水线上依次衔接的三道工序。

这篇文章我想站在实操角度,把transformer里编码和解码两侧到底各自在做什么、token化流程怎么影响最终的语义向量质量、向量又是如何一步步被“榨”出来的,掰开来写清楚。适合刚接触NLP的初学者用来建立整体认知,也适合已经上手但对内部细节还没完全吃透的人快速补齐盲区。

1. 编码解码架构:不是所有模型都长一个样

在真正上手抓向量之前,最好先对transformer的架构分岔有个清醒认识。因为并不是所有transformer模型都同时拥有编码器和解码器,它们的分工和产出物差异极大,直接决定了你取到的“语义向量”到底代表什么。

1.1 三种变体与各自的产出物

第一类是编码器专属架构,典型代表是某基础预训练模型家族。它的职责是把输入token序列映射成一串上下文相关的向量序列,输出层每个token对应一个向量。这类模型适合做语义理解、文本分类、检索召回等任务,因为它的双向注意力机制让每个token都能同时看到左右两侧的上下文,编码出的向量携带的信息密度较高。

第二类是解码器专属架构,典型代表就是GPT这类模型。它使用因果注意力,训练目标是从左到右预测下一个token。解码器同样会产出每个token的向量,但每个位置只能看到它之前的内容,所以向量携带的是一种“基于前文条件”的语义。这类模型适合做生成任务,但如果直接用它的隐状态做语义相似度,效果可能不如编码器,因为它的建模目标是预测下一个词,而非完整理解全句语义。

第三类是完整的编码器-解码器架构,像原始的sequence-to-sequence模型系列。编码器负责把源序列压缩成上下文表示,解码器每个位置通过交叉注意力从编码器侧拉取信息,逐步生成目标序列。这类架构在翻译、摘要任务上仍然有优势。

从实操角度看,抓语义向量首选编码器架构;如果非要拿解码器模型做嵌入,至少要做到两点:一是把提示词和待编码文本当作一个整体丢进去,二是取最后一个token的向量。因为只有最后一个token能看到全部前文信息,其他位置的向量都“近视”。

1.2 掩码机制:编码与解码最大的行为分水岭

双向注意力和因果注意力在实现上的区别,说白了就是一个注意力掩码矩阵。编码器在标准自注意力下,每个token对序列中所有其他token的注意力权重都允许非零;解码器则强加一个上三角掩码,把未来位置全部置为负无穷,经过Softmax归一化后注意力权重趋近于0。

这个机制对语义向量的影响是深层次的。我在做某些长文本分类任务时做过一组对照实验:同一个句子,分别用编码器模型和因果解码器模型取CLS(或最后一个token)向量做分类,前者在样本量不大时效果明显更稳。原因在于编码器在计算第3个token的表示时,已经提前“偷看”了第10个token的信息,使得这个位置的向量在一开始就融入了全局语境;而解码器第3个token的向量则完全不知道第10个token会说什么。

差异还体现在序列级聚合策略上。编码器模型里,[CLS]位或均值池化都能用;解码器模型里,最好使用最后一个token,或者对位置靠后的token加权平均,因为靠前的token向量严重“目盲”,拉进均值里反而稀释语义。

2. token语义向量:从离散符号到稠密空间

讲清楚了架构差异,接下来要把token本身的语义向量是怎么形成的拆明白。一个token从一个离散的ID变成一个能参与相似度计算的稠密浮点数向量,中间其实经过了嵌入层、位置编码注入、多层注意力加工三个主要阶段。

2.1 token化:粒度选择如何影响向量质量

token化是整条流水线的入口。它的本质是把文本切成模型词汇表里能对应的最小单位。中文场景下最容易踩的坑就是“按字切”还是“按词切”的纠结。现在主流模型普遍采用子词分词算法,比如BPE或WordPiece,它会根据语料统计频率自动找出一批最常见的子词单元。

举个例子,“嵌入表示”在某种分词器下可能被切成“嵌”、“入”、“表示”三个token;另一种词表则可能把它切成“嵌入”、“表示”。这两种切法的差异会直接影响语义向量的质量:切得越碎,每个token的单独语义越模糊,模型必须靠上下文慢慢“修补”;切得越整,token的语义本体越强,但遇到生僻复合词时词汇表会急剧膨胀。

从实操角度讲,在做中文任务时,我会先跑一遍tokenizer看看句子被切成什么样,再决定后续特征怎么处理。比如对“自然语言处理”这个短语,如果tokenizer把它切成了“自然”、“语言”、“处理”,那么取第2个token的向量做短语级表示通常比取第1个强,因为“语言”在中间位置能同时吸收左右语境。这种细节只有实际打开tokenizer看输出才知道。

2.2 嵌入层到上下文向量:每个step在干什么

token进入模型后,先通过嵌入矩阵查表得到一个初始向量,这个向量只包含该token本身的语义,完全没有上下文信息。然后会加上一个位置编码向量,因为注意力机制本身是不关心顺序的,如果不加位置信息,“我爱你”和“你爱我”在初始输入阶段会得到完全相同的向量序列。

接下来进入多层transformer模块。每一层内部大致做四件事:自注意力(或掩码注意力)、残差连接、层归一化、前馈网络。自注意力负责让每个token去“询问”其他token,并按照相关性权重聚合信息;残差连接保证梯度可以顺畅回传;层归一化稳定数值分布;前馈网络则对每个位置独立做非线性变换,相当于在注意力汇总结果之上再做一层特征精炼。

经过多层这样迭代加工,每个token的向量逐渐从“孤立的词表向量”变成“融合了全文语境的上下文向量”。这也是为什么说transformer堆叠层数越多,向量语义越细腻——但也不是越多越好,考虑到算力与过拟合,一般实践里取中间层或倒数第二层的小节位置往往反而比最后一层好用。

3. 如何提取和检验语义向量的质量

3.1 提取位置的选择逻辑

实际操作中,最常见的需求是把一整句话变成一个向量,再利用向量做相似度检索或分类。提取方式通常有三种,每种背后的逻辑完全不同,得分效果差异也大。

第一种是CLS池化。某些大型预训练模型在序列开头放置一个特殊的“分类用”token,它本身不承载具体词义,经过多层编码后变成整个序列的“摘要向量”。CLS向量优势是省事,劣势是当文本过长时容易丢失细节信息。

第二种是均值池化。把所有token的隐状态在序列维度上取平均,得到一个固定维度向量。这种策略在短文本上表现不错,因为它把每个位置的语义都纳入了考量,缺点是无法自动区分哪些token重要。

第三种是加权池化。根据每个token与某个目标概念的相关性给它分配权重,再加权求和。在检索场景里,我常用注意力权重或TF-IDF来加权,效果比普通均值更可控。比如做某个关键词的相似搜索时,先把关键词对应的token权重拉高,再做池化,得到的向量会明显朝该方向偏移。

经验上,短文本(几十个token以内)用均值池化或CLS都行,中等长度文本建议取CLS加均值拼接,长文本则要考虑分段编码再加总。我踩过最典型的坑:一段含300个token的法律条款文本,直接取CLS向量做检索,结果召回的是“法条标题”相似但“具体罚则”无关的条目,换成分段均值池化后才恢复正常。原因就是CLS向量被标题与开头部分的强语义带跑了,尾部细节信息被稀释殆尽。

3.2 标准化、维度与相似度计算细节

拿到向量后,第一步习惯性要做L2标准化。原因有两个:一是很多相似度计算(如余弦相似度)本身对模长敏感度不一,标准化后统一在单位球面上比较方向,物理含义更干净;二是很多向量空间里存在“高频方向”干扰,标准化能从一定程度上压低模长异常向量带来的干扰。

维度选择方面,常规预训练模型输出的隐状态维度一般在768或1024左右。工程上我做过一个对比:把768维向量用PCA压到128维再做检索,速度提升了近3倍但召回率几乎没掉,因为原始维度里存在大量冗余相关维度。如果你做的是超大规模向量检索,强烈建议先降维再做索引,不必执着于原始维度。

相似度计算时,还有一个细节常被忽略:某些模型输出的向量空间呈现“各向异性”,表现为几乎所有向量都聚集在少数几个方向上,此时直接用余弦相似度会让不相关的句子得分虚高。解决办法可以是:对向量做均值中心化,减去全体语料的平均向量后再做标准化;或者直接改用负欧氏距离(即向量越近得分越高)来替代余弦。我实测下来,在检索类任务中,中心化后的余弦相似度比原始余弦稳定不少。

4. 实操环节:用现成工具跑通一条完整编码链路

理论说再多,不如实际跑一遍。下面我用PyTorch生态里最常用的库来演示,如何快速加载一个预训练模型、两行代码完成token化、拿到token级隐状态,并组装成可用的句子级语义向量。

4.1 环境准备与基础调用

首先需要安装必要的库。如果你在本地跑,建议用虚拟环境隔离依赖,避免不同项目包版本互相冲突。

pip install transformers torch

这里推荐用某预训练模型底座,因为它的权重相对稳定、文档齐全、中文生态也友好。代码姿态上,第一件事是加载分词器和模型:

from transformers import AutoTokenizer, AutoModel tokenizer = AutoTokenizer.from_pretrained("某预训练模型底座") model = AutoModel.from_pretrained("某预训练模型底座") model.eval()

加载后先别急着喂数据,我一般会先检查一下tokenizer的vocab大小和模型配置的hidden_size,确保两个组件的版本匹配。曾经遇到过本地缓存的tokenizer版本与模型权重版本不一致,导致token id错位,生成的向量全部无效。这种问题最大特点是:代码不报错,但结果诡异——排查起来极耗时间,检查版本一致性永远是第一步。

4.2 完整编码流程与中间量提取

下面这段代码演示了从原始文本到token级向量、再到句子级向量的完整过程:

import torch # 1. 文本结构化 text = "transformer的编码器与解码器各自承担不同职责" # 2. 分词 + 拼接特殊标记 encoded = tokenizer( text, max_length=128, truncation=True, padding="max_length", return_tensors="pt" ) # 3. 关闭梯度,节省显存 with torch.no_grad(): outputs = model(**encoded, output_hidden_states=True) # 4. 提取最后一层隐状态,形状: (batch, seq_len, hidden_dim) last_hidden = outputs.last_hidden_state # 5. 提取CLS向量(开头位置) cls_vec = last_hidden[:, 0, :] # 6. 提取均值池化向量(建议忽略padding部分) attention_mask = encoded["attention_mask"] expanded_mask = attention_mask.unsqueeze(-1).float() sum_vec = (last_hidden * expanded_mask).sum(dim=1) token_counts = expanded_mask.sum(dim=1) mean_vec = sum_vec / token_counts # 7. L2标准化 cls_vec = torch.nn.functional.normalize(cls_vec, p=2, dim=-1) mean_vec = torch.nn.functional.normalize(mean_vec, p=2, dim=-1)

这段代码里值得展开讲的有三个细节。

第一,max_length和truncation的选择。如果文本实际长度远超128,截断会让尾部语义丢失;如果远小于128,padding后会浪费大量计算在无效位置上。实操上,我会先统计语料长度分布,取85%分位作为max_length,既减少截断损失又不至于过分拉长训练时间。

第二,均值池化时必须用attention_mask配合。直接用last_hidden.mean(dim=1)会把padding位置(内容全是0或特殊填充向量)也平均进去,严重拉低向量质量。我见过不止一个人在这个地方踩坑,最后相似度分数一片飘低,排查半天才发现是padding未过滤。

第三,output_hidden_states=True是必须的开关。如果不打开这个参数,outputs里只有last_hidden_state,拿不到每个transformer层的隐状态,也就无法做中层向量提取或层间对比实验。很多进阶玩法(如层间加权、早停解码策略)都依赖这个参数。

4.3 层间加权与语义向量的进阶玩法

既然能拿到所有层的隐状态,就可以做一些更精细的加工。比如在文本相似度任务上,不同层捕获的信息粒度不同——浅层更关注词法与局部搭配,中层更关注句法结构与短语语义,深层更关注全局语义与逻辑关系。

一种常用的做法是层间加权求和,而不是只用最后一层。权重可以来自经验值,也可以通过学习得到。经验值上,我试过给倒数第2层、倒数第3层各分配0.3的权重,中间层分配0.2,其余层均分剩余0.2,效果比只用最后一层稳定不少。原因在于最后一层隐状态在某些情况下过度适配预训练任务的损失函数,作为通用特征反而有点“偏科”。

代码上只需多一个循环:

hidden_states = outputs.hidden_states # 包含embedding层输出,共13个元素(12层+embedding) weighted_vec = torch.zeros(hidden_states[0].shape[-1]) for i, h in enumerate(hidden_states[1:], start=1): layer_weight = 0.2 if i in [len(hidden_states)-2, len(hidden_states)-3] else 0.1 weighted_vec += layer_weight * h[:, 0, :].squeeze()

需要提醒的是,层间加权并非对所有任务都是“越复杂越好”。在新闻分类这种任务上,最后一层表现往往最直接;在语义相似度、文本聚类这类任务上,层间加权才明显占优。所以我习惯的做法是先跑一个快速基线(用最后一层),再对比层间加权的提升幅度,最后再决定上线方案。

5. 常见问题与排查技巧实录

章节 | 问题现象 | 排查思路与方法 | 避坑心得

第一,维度对不上或形状报错。常见于多卡推理或批量处理时没有保持tensor形状一致。解决方法是打印模型的config.hidden_size,再结合batch中的seq_len、mask形状逐一核对。遇到维度报错,不要盲目调代码,先打印所有相关tensor的shape做一次全面体检。

第二,相似度结果虚高。所有文本得分都在0.95以上,完全区分不开。这种情况通常意味着向量没有经过中心化或标准化,且模型输出层分布高度各向异性。解决方法是先对所有向量做batch内中心化,再L2标准化,最后用余弦相似度,虚高问题会缓解很多。

第三,序列长度超过模型上限。比如某模型的最大位置编码只有512,但文本有800个token,直接输入会报错或截断。方案有三种:一是用滑动窗口分段编码,每段分别出向量,再加权累加;二是使用支持长文本的模型或其他结构;三是先用句子分割把长文本切块,再对块级向量做平均。实际生产环境中,第三种方案最常用且稳定。

第四,tokenize后token数与模型期望不一致。典型表现是中文标点、全角空格处理方式不同导致序列长度或ID错位。排查时先print出tokenizer输出的ids和对应文本,逐条对照。尽量避免手动改tokenizer配置,用官方分词器内置参数达到需求是最省事的做法。

第五,CPU推理慢到怀疑人生。如果你只是少量文本需要编码,或只是做实验验证,直接在CPU上跑也可以,但务必设置torch.no_grad()并对模型调用eval()。这两个操作能省下大量内存与时间。若要处理大规模语料,建议用GPU并开启批量推理,配合半精度加载模型可显著提速。

6. 实操路线建议与后续扩展思路

6.1 如果向量质量不够好,先自己怀疑三步

如果相似度结果总是不尽如人意,先别急着换模型,按顺序排查三个环节:分词粒度是否适合语料、提取层是否有偏差、池化策略是否掩盖了关键token语义。这三步走完,绝大多数向量质量问题都能定位到具体环节。

就分词粒度而言,也不一定“切得越细越好”。某些医疗领域的术语必须整词切分才能保留意义,但默认词表未必覆盖。如果目标语料高度垂直,可以额外做一次自定义词典增强,让分词器优先把术语切成整词,后者编码得到的向量语义显著更准。

池化策略则是很多初学者的盲区。CLS、均值、加权三种策略,在不同任务上差距可以很大。我建议准备一套小型的验证集,每个策略都跑一遍,用任务指标选优,不要凭感觉猜测。

6.2 语义向量的后续工程化扩展

向量拿到手,紧接着就是存储和检索。工程上,百万级向量用向量数据库或普通文件配FAISS都能应对。低于十万级,用NumPy文件加暴力检索即可;十万到千万级,建议上FAISS或专用向量数据库;更大规模再考虑分布式方案。

另一个值得尝试的方向是融合多向量表达。短文本用句子级向量,长文本用分段向量求平均,再拼上一个加权向量,多路召回后做分数融合。这种方式在检索精度上通常会比单路强一大截。

最后,关于模型选择和个人经验:如果任务强调语义相似度,优先选编码器架构且训练目标含对比学习的模型或相关底座;如果任务偏生成或对话,那么解码器或完整编解码结构才是你的选择;若条件允许,针对垂直语料做一次小规模微调,能让token向量的分布贴近你的数据领域,相似度质量的提升往往超出预期。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 12:20:34

artcraft工作流:从创意到成品的半自动引导式创作方法

1. 从“artcraft”这个名字说起:它到底想解决什么问题第一次看到“artcraft”这个项目标题,我脑子里蹦出来的第一反应是:这大概率是一个把“艺术创作”和“手工实现”捏在一起的东西。art 是艺术,craft 是手艺、工艺,两…

作者头像 李华
网站建设 2026/10/18 12:19:05

数字图像处理大作业实战:OTSU分割+Canny边缘+OpenCV人脸检测全流程拆解

简介:这是一份数字图像处理课程大作业的完整工程代码包,围绕Canny边缘检测、OTSU图像分割与OpenCV人脸检测三大任务展开,适合正在学习OpenCV与Python图像处理的学生和开发者,既能用于课程项目实践,也能作为入门综合案例…

作者头像 李华
网站建设 2026/10/18 12:17:16

MySQL日期时间类型选型与避坑指南:从DATE到TIMESTAMP全解析

做开发这些年,被日期时间类型折磨的次数,恐怕比被我吐槽过的需求还多。不是时间本身多复杂,而是很多人在设计表结构时,根本没把日期时间当成一种“有自己脾气”的数据类型,而是随手存个字符串、存个时间戳完事。等到排…

作者头像 李华
网站建设 2026/10/18 12:14:46

AI原生应用安全实战:提示注入、工具滥用与三段式防护架构

做了几年应用开发,以前聊安全基本绕不开登录鉴权、参数校验、权限控制这些东西。但最近一年多,我连续参与了好几个大模型落地的项目,最大的感触是:传统那套安全思路,放到 AI 原生应用上真的不够用了。比如同样是“输入…

作者头像 李华
网站建设 2026/10/18 12:13:58

MATLAB机组组合优化:热备用率与直流潮流实战

简介:这份资源面向电力系统优化方向的学生、科研人员与工程技术人员,围绕机组组合这一电力行业核心运营问题,提供MATLAB环境下的建模与求解实践素材。压缩包共10个文件,约312KB,以xls数据表为主,包含不同热…

作者头像 李华