第7章 嵌入与语义空间:让机器比较含义

本章技术主题: 嵌入表示与语义检索。
本章技术实验: 把问题和资料片段变成向量,比较关键词与语义近邻。
家庭项目: “建立家庭维修与说明书助手”是主项目;循环利用规则检索是限定拓展。
跨章位置: 来源编号和检索证据可交给第9章故事盒与第12章综合系统继续使用。
技术主线: 问题和资料片段 → 嵌入模型 → 向量 → 相似度 → 候选证据 → 人工核对。

学习目标

学完本章,你将能够:

  1. 说明嵌入向量、维度、距离和语义空间之间的关系;
  2. 运行一个“编码—比较—排序—回到原文”的语义检索实验;
  3. 比较关键词基线与嵌入检索,解释各自容易漏掉和误选什么;
  4. 用相似但无关、相似型号、资料缺失和高风险维修等样例判断检索结果能否交给生成模型。

项目导入

关键词检索要求问题与资料使用相同词语。用户问“设备为什么一直发出短促提示音”,说明书却可能写“蜂鸣器间歇响起”;两段文字含义接近,但共同词很少。嵌入模型把问题和资料片段都转换为数值向量,使系统可以比较语义,而不只比较字面重合。

本章使用教师提供的模拟说明书,建立家庭维修与说明书助手。输出不是维修结论,而是若干候选段落及其文件、产品型号、资料版本、页码、章节、片段编号和相似度。学生必须打开对应页回到原文确认。资料没有命中,或者涉及带电拆装、高温、高压、燃气、结构安全等高风险维修时,系统停止生成操作步骤并提示转交专业人员。

本章成果

考点提示

嵌入向量不是文字摘要,单个维度通常没有可直接命名的含义;相似度高表示模型认为表示接近,不保证事实相关。关键词检索和嵌入检索都是候选召回方法。检索增强生成只是把命中材料继续交给生成模型,不能把错误材料变成正确证据。

第一节 把问题和资料片段变成向量

一、先建立可核对的资料单元

检索前先把说明书划分成能够独立核对的片段。片段不能只剩半句话,也不宜把几十页合成一段。每个片段至少保存文件名、产品型号、资料版本、页码、章节、片段编号、正文。问题也保留原句,并单独填写用户正在查询的产品型号;不先让生成模型替用户改写成唯一版本。

片段切分会直接改变检索对象。按固定字数硬切,可能把“故障现象”和下一页的“处理限制”分开;整章作为一片,又会让许多无关句子共享同一向量。课堂先按自然段切分,再把标题附加到每段开头,使短句保留所属主题。若采用相邻重叠,要在评价时去除重复候选,避免同一段落占满前三名。

例如资料包含三段:

编号 型号 版本 页码 正文
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可协助写“没有证据便停止”的测试,但学生要亲自用不存在的故障代码验证。

三、形成检索使用判断

判断卡应说明:资料集合、型号、版本和页码;片段切分方法;关键词与嵌入各自的命中情况;候选数量和核对成本;相似但无关、相似型号材料的错误;资料不足、型号冲突和高风险内容的停止条件。

适合采用的范围是:资料封闭、来源明确、问题以同义表达为主、输出只做候选证据。若资料很少且术语固定,关键词检索可能更透明;若必须精确匹配编号,普通字符串检索更可靠;若材料不完整,增加生成模型不会补回缺失事实。

拓展任务可以把相同方法用于“家庭物品整理与循环利用”中的规则检索:只从所在地区、注明发布日期和适用范围的官方分类指引中查找候选条款,并返回来源页码或网页锚点。电池、药品、化学品、带电设备等高风险物品不得根据语义相似度自行决定处理方式;规则缺失、地区不符或已过期时停止并转交当地专业渠道。这个拓展学习的是资料检索,不把嵌入模型写成物品识别或政策判断模型。

本章小结

嵌入把文字映射为向量,使语义关系能够通过距离比较。最近邻负责找候选,余弦相似度只是排序数值,不是事实证明。片段来源、型号、版本和页码仍由普通数据字段和人工核对保证。

关键词和嵌入各有适用范围。控制候选数量、测试相似但无关文本,并让“无证据”成为合法输出,才能把语义检索从演示变成可判断的方法。检索后可以连接生成模型,但生成不能修复错误或缺失的证据。

关键术语

目标测试

一、单项选择题

  1. 嵌入模型的直接输出通常是( )。A.维修结论 B.固定长度向量 C.文件权限 D.人工标签
  2. 余弦相似度主要比较两个向量的( )。A.文件大小 B.方向 C.来源日期 D.安全等级
  3. 候选数量从1增至5通常会( )。A.一定更正确 B.候选和核对量增加 C.重新训练模型 D.消除资料缺失
  4. 检索结果交给生成模型后,最重要的操作是( )。A.删除来源 B.逐句核对证据 C.提高字数 D.隐藏低分结果

二、判断并改错

  1. “相似度最高的片段一定能正确回答问题。”请改正。
  2. “检索增强生成会自动把新资料训练进语言模型参数。”请改正。

三、简答与操作题

  1. 用“问题—向量—相似度—候选—型号/版本/页码—原文”说明一次语义检索。
  2. 设计一条“字面不同但含义相近”和一条“相似型号但不适用”的测试。
  3. 比较关键词检索与嵌入检索各自适合的资料和问题。
  4. 候选数量增加后命中改善但无关片段明显增多,应如何形成判断?

答案编号:A1-7-01—A1-7-10。完整答案和评价要点统一放入书后“目标测试参考答案”。