多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

超越向量相似度:源条件描述长度增益如何检测生成式抄袭

超越向量相似度:源条件描述长度增益如何检测生成式抄袭 生成式抄袭检测Generative Plagiarism Detection最近是 NLP 领域里非常现实的一个问题。以前查抄袭靠的是字符串重叠和向量相似度现在一个人只要把某篇来源文档丢给大模型让它换一种说法重新写一遍传统检测手段基本就失效了。这篇标题为 Beyond Representational Similarity: Source-Conditioned Description-Length Gain for Generative Plagiarism Detection and Candidate Source Reranking 的工作核心思路是把判断依据从“表示相似度Representational Similarity”换成“来源条件下的描述长度增益Source-Conditioned Description-Length Gain”并且把候选来源重排序Candidate Source Reranking当作流程里的关键环节。如果你在做学术诚信工具、内容风控、AIGC 溯源或者单纯在研究文本相似度之外还应该看什么信号这条方法路线很值得仔细拆一遍。1. 先理解为什么“向量相似”不足以判断生成式抄袭1.1 生成式抄袭和传统抄袭的差别传统抄袭很好理解整段复制或者改几个词。检测工具只需要做两件事一个是看字符串重叠一个是看 n-gram 重合度。只要重合度超过阈值基本就能判定。生成式抄袭不一样。抄袭者把来源文档作为输入交给大模型让它“换个说法”“扩充一下”“概括一下”模型输出出来的文本在词汇层面已经和原文差得很远。你拿原文和输出做字符串匹配匹配率可能很低拿嵌入向量算余弦相似度如果改写足够激进相似度也会掉下去。但本质上这段文本确实是“从来源文档衍生出来的”版权和学术诚信问题依然存在。所以我理解的“生成式抄袭”核心不是“有没有复制粘贴”而是“这段可疑文本能不能由某个来源文档生成出来”。检测任务因此从“找相同”变成了“找生成关系”。1.2 表示相似度擅长什么不擅长什么表示相似度的做法是把文本映射到向量空间然后用余弦相似度、欧氏距离这类指标做比较。它擅长捕捉语义接近两句话用词不同意思一样向量会很近。这是它好用的地方也是它容易误判的地方。问题主要有三个。第一语义接近不等于衍生关系。两篇新闻都在写同一个热点事件用词不同但向量相似度可能很高。你拿阈值去卡很容易把独立写作误判成抄袭这是假阳性。第二改写攻击可以压低相似度。大模型改写时只要调整句式、替换同义词、打乱信息顺序嵌入向量的余弦相似度就会明显下降。检测方需要不断下调阈值但阈值越低误判越多。第三相似度是对称的没有方向。A 和 B 的余弦相似度是同一个值但“A 从 B 生成”和“B 从 A 生成”是两种完全不同的情况有时候两个判断都是错的。对称的相似度根本分不清因果方向。这三点合在一起就是标题里说的“Beyond Representational Similarity”。要继续做生成式抄袭检测就不能只盯着表示相似度这一个信号。1.3 真正需要判断的是“生成关系”如果从生成视角看检测任务可以重新表述为给定可疑文本 T 和候选来源 ST 是否可能是以 S 为条件生成的产物生成关系是有方向的而且和“是否相似”是两件事。这也是 Source-Conditioned Description-Length Gain 这个命名的由来。它不是拿 T 和 S 各自的向量做全局比较而是把 S 作为条件去看 T 在给定 S 之后是否变得更容易描述、更容易生成。这个思路比“像不像”更接近抄袭发生的真实过程。2. 核心方法描述长度增益怎么替代“像不像”2.1 描述长度的信息论直觉描述长度Description Length来自信息论和最小描述长度原则。一组数据越是有规律越容易被压缩描述它所需要的位数就越短。反过来随机文本几乎压缩不动描述长度很长。把这条思路搬到文本关系上如果一段文本 T 确实是从 S 衍生的那么在你已经知道 S 的前提下再去描述 T会比完全不知道 S 时省下很多“描述成本”。换句话说S 提供了关于 T 的额外信息降低了 T 的不确定性。这个“省下来的成本”就是“描述长度增益”。增益越高说明 S 对解释 T 的贡献越大T 从 S 生成出来的可能性也就越高。Kolmogorov 复杂度是这种思路的理论形式实际无法直接计算。到了语言模型时代我们有了很好的近似工具用模型输出的交叉熵来估算一段文本的描述长度。2.2 Source-Conditioned 条件增益怎么算把问题落到语言模型上。假设我们有一个强大的因果语言模型它对一段文本的输出概率是 P(T)。给定来源 S 之后模型对 T 的输出概率是 P(T | S)。那么条件增益可以写作增益 log P(T | S) - log P(T)因为 log 概率越大说明模型越“不意外”对应的描述长度越短。如果 S 是 T 的真实来源模型看到 S 之后再看 T会是相对平滑的续写P(T | S) 明显高于 P(T)增益为正且数值较大。如果 S 和 T 无关模型不能从 S 中得到任何帮助增益会接近 0。这里有一点要说清楚论文的具体公式、参数设置和实验结论我没有办法只凭标题替作者确认上面是按方法名称给出的标准信息论理解。真正落地时要以正式发表版本为准。但从直觉上Source-Conditioned 强调的是“针对该来源的条件概率”而不是全局的向量相似度。2.3 为什么这个信号更贴合生成式抄袭原因在于生成式抄袭本身就是“以 S 为条件生成 T”的过程。抄袭者把来源 S 喂给大模型模型在 S 的条件下采样出 T。所以从统计角度看T 在 S 条件下的概率天然会高。这带来一个关键优势检测和伪造不对等。改写方很难在保持内容相关的前提下同时把 P(T | S) 压到接近 P(T) 的水平。因为只要内容真的来自 S语言模型在 S 条件下预测下一个词就会更准确这个信号很难被表面的词汇替换抹掉。表示相似度可以被改写攻击“骗过”但描述长度增益对应的是生成过程中的真实依赖。另外这个信号是带方向的。S 到 T 的增益高不代表 T 到 S 的增益也高。这个方向性正好符合抄袭检测的需求我们关心的是“可疑文本是否源自某个候选来源”而不是“两个文本是否差不多”。3. 完整检测流程召回、打分、重排序3.1 候选源召回先缩小范围如果候选来源全集很大比如一个论文库、一个网页快照库逐个两两计算描述长度增益是不现实的。先要用低成本的检索器把范围缩到几十到几百个候选。召回阶段通常用 BM25 和稠密检索的混合结果。BM25 对词汇重合敏感稠密检索对语义近义敏感两者取并集或者加权融合可以保证真正来源大概率落在候选集里。这一阶段不追求精确只追求召回率。宁可多带回一些无关来源也不能漏掉真正的来源。因为后面的 SDLG 打分再准确如果真正的来源没有进入候选集重排序也无从谈起。3.2 对每个候选源计算 SDLG候选源拿到之后对每个候选 S 计算可疑文本 T 的描述长度增益。这一步是整条链路里最重的计算。具体来说需要跑两遍模型一遍是 T 单独的概率 P(T)另一遍是拼接 S 和 T 之后T 部分的条件概率 P(T | S)。两边的负对数似然差值就是增益。有几个细节会影响结果质量拼接方式要稳定。S 和 T 之间用什么分隔符需要固定下来不能每次实验都不一样。T 的长度不能过大。太长的文本可以分块每块单独算增益再聚合。只统计 T 部分的 token不要把 S 的 token 也算进去。3.3 候选源重排序与最终判断召回阶段给出的排序通常偏向词汇重叠真正的来源可能排在第 5 位甚至第 20 位。这时候用 SDLG 分数对候选源重新排序就是标题里说的 Candidate Source Reranking。重排序之后排名最高的候选源大概率就是真实来源。如果这个来源对应的 SDLG 增益超过阈值就判定 T 存在抄袭如果增益都很低说明候选集里没有明显来源结论应该是“未检出”而不是“一定没有抄袭”。这里很容易踩的坑是不要只看 Top-1 的结果。真正生产环境里应该同时输出 Top-3 来源、对应的增益分数和原始检索分数让审核人员有一个可解释的判断依据。4. 实现中容易被忽略的四个工程细节4.1 用因果语言模型算 NLL 的基本写法一个常见的实现方式是用 HuggingFace 生态里的因果语言模型对每个 token 的交叉熵求和得到负对数似然。下面是示意逻辑实际落地时要处理 tokenizer 的 BOS、截断和 batch 等问题。# 示意代码用因果语言模型近似计算 SDLG # 实际实现需要处理 BOS、截断、padding 和 batch import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_name your-causal-lm # 换成实际可用的模型 tok AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained(model_name) def seq_nll(ids): logits model(ids.unsqueeze(0)).logits[0] shift_logits logits[:-1, :] shift_labels ids[1:] nll torch.nn.functional.cross_entropy( shift_logits, shift_labels, reductionsum ).item() return nll, len(ids) - 1 def sdlg(query, source): query_ids tok(query, return_tensorspt).input_ids[0] source_ids tok(source, return_tensorspt).input_ids[0] nll_uncond, _ seq_nll(query_ids) full_ids torch.cat([source_ids, query_ids]) nll_full, _ seq_nll(full_ids) nll_source, _ seq_nll(source_ids) # 有条件分支只统计 query 部分的 token nll_cond nll_full - nll_source q_len len(query_ids) return (nll_uncond - nll_cond) / q_len上面这个示意没有处理无条件分支的第一个 token 对齐问题。实际使用中我建议用统一的起始符把两边拉齐或者有条件分支也跳过 query 的第一个 token否则增益会带轻微偏差。这个小偏差在单条样本上不明显积累到批量评估时会影响排序稳定性。4.2 上下文拼接、长度归一化和截断无条件分支里模型对 T 的第一个 token 通常没有上文所以不会被计入损失。有条件分支里T 的第一个 token 有 S 的最后一个 token 作为上文会被计入。这样两边统计的 token 数量不一致需要对齐。长度归一化也很关键。增益是累加的文本越长数值越大。如果要在不同长度的可疑文本之间比较建议归一化到每个 token 的平均增益
返回列表