第7章 嵌入与语义空间:让机器比较含义
本章技术主题: 嵌入表示与语义检索。
本章技术实验: 把问题和资料片段变成向量,比较关键词与语义近邻。
家庭项目: “建立家庭维修与说明书助手”是主项目;循环利用规则检索是限定拓展。
跨章位置: 来源编号和检索证据可交给第9章故事盒与第12章综合系统继续使用。
技术主线: 问题和资料片段 → 嵌入模型 → 向量 → 相似度 → 候选证据 → 人工核对。
学习目标
学完本章,你将能够:
- 说明嵌入向量、维度、距离和语义空间之间的关系;
- 运行一个“编码—比较—排序—回到原文”的语义检索实验;
- 比较关键词基线与嵌入检索,解释各自容易漏掉和误选什么;
- 用相似但无关、相似型号、资料缺失和高风险维修等样例判断检索结果能否交给生成模型。
项目导入
关键词检索要求问题与资料使用相同词语。用户问“设备为什么一直发出短促提示音”,说明书却可能写“蜂鸣器间歇响起”;两段文字含义接近,但共同词很少。嵌入模型把问题和资料片段都转换为数值向量,使系统可以比较语义,而不只比较字面重合。
本章使用教师提供的模拟说明书,建立家庭维修与说明书助手。输出不是维修结论,而是若干候选段落及其文件、产品型号、资料版本、页码、章节、片段编号和相似度。学生必须打开对应页回到原文确认。资料没有命中,或者涉及带电拆装、高温、高压、燃气、结构安全等高风险维修时,系统停止生成操作步骤并提示转交专业人员。
本章成果
- 一个可运行、能显示型号、版本和页码的家庭说明书语义检索Notebook;
- 一张“文字—向量—空间距离—最近邻—原文”的递进机制图;
- 一份关键词与嵌入检索的对照结果;
- 一张说明检索、生成与人工核对边界的判断卡。
考点提示
嵌入向量不是文字摘要,单个维度通常没有可直接命名的含义;相似度高表示模型认为表示接近,不保证事实相关。关键词检索和嵌入检索都是候选召回方法。检索增强生成只是把命中材料继续交给生成模型,不能把错误材料变成正确证据。
第一节 把问题和资料片段变成向量
一、先建立可核对的资料单元
检索前先把说明书划分成能够独立核对的片段。片段不能只剩半句话,也不宜把几十页合成一段。每个片段至少保存文件名、产品型号、资料版本、页码、章节、片段编号、正文。问题也保留原句,并单独填写用户正在查询的产品型号;不先让生成模型替用户改写成唯一版本。
片段切分会直接改变检索对象。按固定字数硬切,可能把“故障现象”和下一页的“处理限制”分开;整章作为一片,又会让许多无关句子共享同一向量。课堂先按自然段切分,再把标题附加到每段开头,使短句保留所属主题。若采用相邻重叠,要在评价时去除重复候选,避免同一段落占满前三名。
例如资料包含三段:
| 编号 | 型号 | 版本 | 页码 | 正文 |
|---|---|---|---|---|
| M-01 | HX-100 | 2026.1 | 12 | 指示灯连续闪烁时,请检查上盖是否完全闭合。 |
| M-02 | HX-100 | 2026.1 | 18 | 蜂鸣器间歇响起表示水箱未安装到位。 |
| M-03 | HX-100 | 2026.1 | 26 | 清洁外壳前应断开电源,不得将主机浸入水中。 |
问题“为什么一直发出短促提示音”与M-02没有相同的核心名词,却可能在语义空间中接近。首轮实验同时运行关键词基线和嵌入检索,避免把新方法的结果孤立展示。
二、与开发与学习AI共同准备实验
应用中的AI是把文字编码成向量的嵌入模型。开发与学习AI协助检查切分规则、生成Notebook和解释向量形状。学生负责资料版本、片段边界、标准答案与停止条件。可使用下面的任务说明:
任务:对同一批说明书片段比较关键词检索和嵌入检索。
输入:segments.csv与questions.csv,均为模拟资料并带来源编号。
输出:每个问题的前三个候选、相似度、型号、版本、页码、来源和是否命中标准片段。
限制:不生成维修动作;找不到证据时明确返回“资料不足”。
实验:只改变候选数量,比较命中和无关材料增加情况。
请解释:文本怎样变成向量,向量形状是什么,相似度在哪里计算。
开发与学习AI可以提出片段重叠等方案,却不能一边改片段,一边用同一批问题证明检索改善。标准问题和期望片段应在调整前封存一部分作为测试。
实际Notebook还应建立资料索引表。模型向量只保存数值,索引表保存向量行号与片段编号、产品型号、资料版本和页码之间的对应关系。更新说明书后要重新编码受影响片段,并记录所用嵌入模型版本;不能把旧向量与新正文错位。文件读取、编号检查和版本筛选是普通程序,不是嵌入模型学到的能力。
三、观察第一组检索结果
先查询“上盖没扣好会怎样”。关键词基线与嵌入检索都能找到M-01。再查询“短促提示音是什么原因”,关键词基线可能没有命中,嵌入检索把M-02排在第一。第三个问题“机器声音很大正常吗”与M-02共享“声音”含义,却未必询问蜂鸣提示;若M-02仍排第一,这就是“相似但无关”的首个失败样例。
记录时同时保存前三名,不只保存第一名。学生要核对:候选是否真的回答问题;来源是否属于正确型号和版本;页码能否打开并定位原句;相似度接近时是否应继续追问;资料不存在时系统能否停止。
第二节 用距离与近邻完成语义检索
一、从坐标位置理解嵌入
可以先把“蜂鸣”“提示音”“上盖”“水箱”想象成一维轴上的点;一维只能表达一种关系。增加第二个维度后,模型可以同时表达更多关系,例如“声音事件”和“设备部件”。真实嵌入通常有数百个维度,图上只能投影其中一小部分。
嵌入模型接收文字,输出固定长度向量。语义相近的文字通常方向更接近。余弦相似度比较两个向量夹角,取值越大通常表示方向越接近:
[ \operatorname{cos}(q,d)=\frac{q\cdot d}{\lVert q\rVert\lVert d\rVert} ]
式中的(q)是问题向量,(d)是资料向量。公式负责比较数值,不理解型号、日期或安全后果。最近邻检索是在所有资料向量中找距离最近的若干个。聚类则把彼此接近的资料先分组,适合观察资料主题,但聚类标签仍需人解释。
模型内部先把词元转换为向量,再综合词序和上下文,最后形成句子或片段表示。两个嵌入模型即使维数相同,坐标含义也不相同,问题向量和资料向量必须来自同一个兼容模型。不能用模型A编码资料、模型B编码问题后直接计算距离。向量维数更高也不自动更准确,评价仍要回到本书的实际问题集。
二、运行透明的向量排序代码
下面的教学数值已经由同一个嵌入模型生成并缩减到三维,只用于看清排序计算。规划中的数字资源Notebook应调用实际嵌入模型得到完整向量,再执行相同的比较步骤。
import numpy as np
ids = ["M-01", "M-02", "M-03"]
vectors = np.array([
[0.10, 0.91, 0.22], # 上盖与指示灯
[0.92, 0.18, 0.25], # 蜂鸣提示音
[0.08, 0.24, 0.95], # 断电与清洁
], dtype="float32")
query = np.array([0.88, 0.20, 0.18], dtype="float32")
def normalize(x):
return x / np.maximum(np.linalg.norm(x, axis=-1, keepdims=True), 1e-9)
scores = normalize(vectors) @ normalize(query.reshape(1, -1)).T
ranking = np.argsort(-scores[:, 0])
for index in ranking:
print(ids[index], round(float(scores[index, 0]), 3))
矩阵vectors的形状是3×3,代表三个片段、每个三个教学维度;查询形状为1×3。归一化后做点积,结果就是余弦相似度。实际向量维数更高,但“同一模型编码—归一化—比较—排序”的主线不变。
规划中的完整Notebook应在这段排序代码前增加真实编码,在后面增加来源回查。运行顺序应清楚显示:读取片段;批量调用嵌入模型;检查输出形状和有限数值;归一化并保存向量;编码问题;计算全部相似度;按型号等硬条件过滤;返回片段正文与来源。硬条件过滤不应交给相似度猜测,例如型号完全不符时即使分数高也要排除。
下面给出一个可以从文字训练到向量检索的微型版本。六组正负样例让共享编码器学习“提示音—蜂鸣器”等关系。它只用于展示参数怎样由配对样本调整,不能替代在大规模语料上训练的通用嵌入模型。课堂先运行代码,再让开发与学习AI逐行说明encoder为什么在问题和资料两边共享。
import numpy as np
import tensorflow as tf
from tensorflow import keras
tf.keras.utils.set_random_seed(42)
segments = np.array([
"指示灯连续闪烁时检查上盖是否完全闭合",
"蜂鸣器间歇响起表示水箱未安装到位",
"清洁外壳前应断开电源不得浸入水中",
])
train_query = np.array([
"短促提示音是什么原因", "提示音一直响",
"盖子没有扣好会怎样", "上盖打开",
"怎样清洗主机", "可以把主机泡水吗",
"短促提示音是什么原因", "上盖打开", "怎样清洗主机",
])
train_doc = np.array([
segments[1], segments[1], segments[0], segments[0],
segments[2], segments[2], segments[0], segments[2], segments[1],
])
labels = np.array([1, 1, 1, 1, 1, 1, 0, 0, 0], dtype="float32")
vectorizer = keras.layers.TextVectorization(
standardize="lower_and_strip_punctuation",
split="character",
output_mode="int",
output_sequence_length=24,
)
vectorizer.adapt(np.concatenate([segments, train_query]))
vocab_size = len(vectorizer.get_vocabulary())
text = keras.Input(shape=(), dtype=tf.string)
x = vectorizer(text)
x = keras.layers.Embedding(vocab_size, 24, mask_zero=True)(x)
x = keras.layers.GlobalAveragePooling1D()(x)
embedding = keras.layers.Dense(16)(x)
encoder = keras.Model(text, embedding)
query_input = keras.Input(shape=(), dtype=tf.string)
doc_input = keras.Input(shape=(), dtype=tf.string)
similarity = keras.layers.Dot(axes=1, normalize=True)(
[encoder(query_input), encoder(doc_input)])
probability = keras.layers.Rescaling(0.5, offset=0.5)(similarity)
pair_model = keras.Model([query_input, doc_input], probability)
pair_model.compile(optimizer="adam", loss="binary_crossentropy")
pair_model.fit([train_query, train_doc], labels, epochs=80, verbose=0)
doc_vectors = encoder(segments, training=False).numpy()
doc_vectors /= np.maximum(np.linalg.norm(doc_vectors, axis=1, keepdims=True), 1e-9)
# 这些封存改写不参与词表适配和配对训练。
test_questions = np.array([
"设备为何发出短促声音",
"盖子没关严会出现什么现象",
"主机可以直接用水冲洗吗",
])
expected = np.array([1, 0, 2])
encoded = vectorizer(test_questions).numpy()
unknown_id = vectorizer.get_vocabulary().index("[UNK]")
for question, token_ids, expected_index in zip(test_questions, encoded, expected):
used = token_ids[token_ids != 0]
unknown_ratio = np.mean(used == unknown_id) if len(used) else 1.0
query_vector = encoder(np.array([question]), training=False).numpy()[0]
query_vector /= max(np.linalg.norm(query_vector), 1e-9)
semantic_scores = doc_vectors @ query_vector
literal_scores = np.array([
len(set(question) & set(doc)) / max(len(set(question) | set(doc)), 1)
for doc in segments
])
top_index = int(np.argmax(semantic_scores))
print("问题:", question)
print("非零词元数/OOV比例:", len(used), round(float(unknown_ratio), 3))
print("期望/实际Top-1:", int(expected_index), top_index)
for index in np.argsort(-semantic_scores):
print(index, round(float(semantic_scores[index]), 3),
round(float(literal_scores[index]), 3), segments[index])
TextVectorization在这里显式采用split="character",把没有空格的中文句子按字符切分,再转换为词元编号;若沿用默认的空格切分,整句中文很可能只得到一个词元,这个微型实验就只会记忆整句。Embedding和全局平均池化形成初始表示,Dense层输出16维向量。正样例推动配对向量接近,负样例推动它们分开。最后一段对三条未参与训练的改写同时报告OOV比例、语义分数与字面字符重合基线。这仍只是字符级配对表示教学模型,不是通用中文句向量模型;封存题答错时应记录失败并扩充训练配对,不能把期望答案改成模型的输出。
运行后还要观察同一句资料在重新训练后是否移动。训练随机性和样本选择可能改变坐标,因而向量文件应与模型版本一起保存。只保存向量而丢失编码模型,后续查询就无法在同一空间中比较。
三、只改变候选数量
把候选数量从1改为3,其他条件不变。候选更多可能提高“正确片段至少出现一次”的机会,也会增加无关片段和人工核对量。实验用十个封存问题记录:前1名命中数、前3名命中数、每题无关片段数和核对时间。
若前1名命中6题,前3名命中8题,同时无关片段从4条增加到18条,就不能只宣称“召回提高”。候选数量不是越多越好,需要结合材料长度、错误后果和生成模型能处理的上下文决定。
可以用“前k名命中数”评价召回,用“第一个正确片段平均排在第几位”观察排序,也要逐条看错误。十个问题中命中八个,比只写80%更能提醒读者样本很少。若两个问题失败原因都是片段切分,应调整切分;若失败来自型号字段缺失,应修数据;不能把所有错误都归咎于嵌入模型。
第三节 测试相似但无关的文本并限定检索后生成
一、建立语义检索失败集
正常样例使用同义表达询问资料中明确存在的内容;边界样例含“它”“那个声音”等指代不清表达;陌生样例询问另一型号或资料中没有的功能;故障样例包括空文件、向量维度不一致和嵌入模型不可用。还要专门加入“词语很像但任务不同”的干扰段落,以及相似型号反例:问题明确询问HX-100,资料库中的HX-100S段落虽然文字更相似,也不得越过型号字段进入有效候选。
相似度阈值只能减少低分候选,不能判断高分候选一定正确。正确通过标准应同时要求:命中期望片段;型号和版本匹配;页码与来源可打开;资料不足时停止。若正确型号的资料没有任何片段回答问题,系统应输出“资料不足”,不得借用相似型号或语言模型常识补写。对于高风险维修,即使检索正确,也只能展示来源、提示查阅专业资料或转交专业人员。
还应测试问题改写的影响。同一含义分别写成口语、书面语和含错别字的版本,观察正确片段排序是否稳定。若系统只能接受一种写法,可让模型产生多个查询候选,但必须保留原问题,并把“查询扩展”作为独立变量测试。查询扩展可能找回同义表达,也可能把原问题带向错误主题。
二、理解检索后生成的边界
把检索到的片段连同来源交给生成模型,请它在材料范围内整理答案,常被称为检索增强生成。这里增加的是生成环节,不会重新训练嵌入模型。生成模型可能忽略证据、混合不同片段或补写材料中不存在的步骤,因此输出仍要逐句回查来源。
本章只要求实现“检索结果+限定生成”的最小连接,不要求搭建复杂平台。若固定模板已经能把命中片段展示清楚,就可以不用生成模型。开发与学习AI可协助写“没有证据便停止”的测试,但学生要亲自用不存在的故障代码验证。
三、形成检索使用判断
判断卡应说明:资料集合、型号、版本和页码;片段切分方法;关键词与嵌入各自的命中情况;候选数量和核对成本;相似但无关、相似型号材料的错误;资料不足、型号冲突和高风险内容的停止条件。
适合采用的范围是:资料封闭、来源明确、问题以同义表达为主、输出只做候选证据。若资料很少且术语固定,关键词检索可能更透明;若必须精确匹配编号,普通字符串检索更可靠;若材料不完整,增加生成模型不会补回缺失事实。
拓展任务可以把相同方法用于“家庭物品整理与循环利用”中的规则检索:只从所在地区、注明发布日期和适用范围的官方分类指引中查找候选条款,并返回来源页码或网页锚点。电池、药品、化学品、带电设备等高风险物品不得根据语义相似度自行决定处理方式;规则缺失、地区不符或已过期时停止并转交当地专业渠道。这个拓展学习的是资料检索,不把嵌入模型写成物品识别或政策判断模型。
本章小结
嵌入把文字映射为向量,使语义关系能够通过距离比较。最近邻负责找候选,余弦相似度只是排序数值,不是事实证明。片段来源、型号、版本和页码仍由普通数据字段和人工核对保证。
关键词和嵌入各有适用范围。控制候选数量、测试相似但无关文本,并让“无证据”成为合法输出,才能把语义检索从演示变成可判断的方法。检索后可以连接生成模型,但生成不能修复错误或缺失的证据。
关键术语
- 嵌入向量:模型把文字等对象转换成的固定长度数值表示。
- 语义空间:对象以向量位置组织、相近位置通常表达相近模式的空间。
- 维度:向量中数值的位置数量,单个维度通常不能直接命名。
- 余弦相似度:通过向量夹角比较方向接近程度的指标。
- 最近邻:离查询向量最近的一个或多个候选。
- 聚类:依据向量距离把相近对象分组的方法。
- 候选数量:一次检索返回的前若干个结果,常写作Top-k。
- 检索增强生成:先检索外部资料,再让生成模型依据候选资料作答的连接方式。
目标测试
一、单项选择题
- 嵌入模型的直接输出通常是( )。A.维修结论 B.固定长度向量 C.文件权限 D.人工标签
- 余弦相似度主要比较两个向量的( )。A.文件大小 B.方向 C.来源日期 D.安全等级
- 候选数量从1增至5通常会( )。A.一定更正确 B.候选和核对量增加 C.重新训练模型 D.消除资料缺失
- 检索结果交给生成模型后,最重要的操作是( )。A.删除来源 B.逐句核对证据 C.提高字数 D.隐藏低分结果
二、判断并改错
- “相似度最高的片段一定能正确回答问题。”请改正。
- “检索增强生成会自动把新资料训练进语言模型参数。”请改正。
三、简答与操作题
- 用“问题—向量—相似度—候选—型号/版本/页码—原文”说明一次语义检索。
- 设计一条“字面不同但含义相近”和一条“相似型号但不适用”的测试。
- 比较关键词检索与嵌入检索各自适合的资料和问题。
- 候选数量增加后命中改善但无关片段明显增多,应如何形成判断?
答案编号:A1-7-01—A1-7-10。完整答案和评价要点统一放入书后“目标测试参考答案”。