Guide · 使用教程
SemHash Python 教程:重复检测、阈值校准与误删复核
固定版本、模型与精确后端,比较七条记录在三个阈值下的分组,保留实际误判,输出可追溯候选并完成十项检查。
先生成复核报告,不直接删除
本教程用 SemHash 比较七条合成记录,在三个阈值下输出保留与过滤候选,并保存候选为何被归并。目标是发现完全副本、观察业务误判并检查来源完整性;程序不修改或删除输入,也不把过滤数量当成质量得分。
固定环境为 Python 3.11、SemHash 0.5.0、Model2Vec 0.10.0、Vicinity 0.4.6。模型为英文 potion-base-8M,搜索采用 basic 精确后端。中文样本只是语言错配对照,不是中文模型推荐,也没有大规模近似索引或图片实验。
第一步:安装并准备固定模型
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install 'semhash==0.5.0' 'model2vec==0.10.0' 'vicinity==0.4.6'
模型准备需要联网,保存为 download_model.py 后执行。使用固定官方快照,避免模型更新与代码改动同时影响结果。权重和库许可分别核对,本次模型卡标注 MIT。
from huggingface_hub import snapshot_download
snapshot_download(
'minishlab/potion-base-8M',
revision='bf8b056651a2c21b8d2565580b8569da283cab23',
local_dir='./potion-base-8m',
allow_patterns=['*.json', '*.safetensors', 'README.md'],
)
python download_model.py
若已有上一条 Model2Vec 教程的同版本完整快照,可以复用,先核对模型文件摘要。严格离线环境应提前准备依赖与权重;HF_HUB_OFFLINE 只约束 Hub 行为,不是操作系统网络隔离。不要用来源不明镜像替换而不记录变化。
第二步:保存完整去重程序
将以下代码保存为 deduplicate.py。只让 text 参与比较,id 用于追踪来源;完全一致指比较字段一致,不代表整行所有元数据相同。本例保留两个不同 ID 的相同文本,检验精确副本能否回溯。
当前固定版本的过滤项提供 record、exact、duplicate_of、score。官方一些旧示例还使用 duplicates,复制代码前应核对实际包接口。这里每个阈值重新调用 self_deduplicate,便于解释,不依赖缓存阈值调整功能。
import argparse,json,hashlib
from dataclasses import asdict
from importlib.metadata import version
from pathlib import Path
from semhash import SemHash
from model2vec import StaticModel
RECORDS=[
{'id':'refund-7','text':'Customers can request a refund within seven days.'},
{'id':'refund-copy','text':'Customers can request a refund within seven days.'},
{'id':'refund-30','text':'Customers can request a refund within thirty days.'},
{'id':'no-refund','text':'Customers cannot request a refund.'},
{'id':'password','text':'Reset your password from the account security page.'},
{'id':'zh-7','text':'购买后七天内可以申请退款。'},
{'id':'zh-30','text':'购买后三十天内可以申请退款。'},
]
def validate(records,threshold):
if not records:raise ValueError('empty input')
if not 0<=threshold<=1:raise ValueError('threshold outside [0,1]')
if len({r['id'] for r in records})!=len(records):raise ValueError('duplicate source id')
if any(not r['text'].strip() for r in records):raise ValueError('empty text')
def report(result):
return {'selected':[dict(x) for x in result.selected],
'filtered':[{'record':dict(x.record),'exact':x.exact,
'duplicate_of':dict(x.duplicate_of),'score':float(x.score)} for x in result.filtered]}
def main():
p=argparse.ArgumentParser();p.add_argument('model_directory',type=Path);args=p.parse_args()
validate(RECORDS,0.9)
model=StaticModel.from_pretrained(str(args.model_directory))
engine=SemHash.from_records(RECORDS,columns=['text'],model=model,ann_backend='basic')
results={str(t):report(engine.self_deduplicate(threshold=t)) for t in [0.8,0.9,0.98]}
output={'versions':{n:version(n) for n in ['semhash','model2vec','vicinity']},
'model_sha256':hashlib.sha256((args.model_directory/'model.safetensors').read_bytes()).hexdigest(),
'input':RECORDS,'results':results,
'note':'Review candidates only; no source files changed or deleted.'}
print(json.dumps(output,ensure_ascii=False,indent=2))
if __name__=='__main__':main()
第三步:本地运行并读报告
HF_HUB_OFFLINE=1 python deduplicate.py ./potion-base-8m > deduplication-output.json
报告保存包版本、权重 SHA-256、完整合成输入和三个阈值结果。selected 是本次算法保留记录,filtered 是候选过滤项,并不是已经删除的数据。检查每个过滤项匹配的来源、是否 exact,以及业务事实是否真的可互换。
只把 text 放入比较列,允许相同文本不同 ID 被识别;真实任务还要保留来源和权限。若同一个问题对应不同答案,需要比较正确字段或增加业务规则,不能照抄单列示例合并整个问答库。代表记录应按权威性、有效期与质量决定,不能默认首次出现最可靠。
第四步:核对实际误判
本次结果如下:
| 阈值 | 保留数 | 过滤候选 | 解释 |
|---|---|---|---|
| 0.8 | 4 | 完全副本、英文三十天、中文三十天 | 两组期限差异被归并 |
| 0.9 | 5 | 完全副本、英文三十天 | 英文期限差异仍被误判 |
| 0.98 | 6 | 完全副本 | 仅对本样例未归并期限差异 |
英文三十天退款匹配七天退款,分数约 0.9239;中文期限差异分数约 0.8441。它们主题相同但业务事实不同,不能因为超过阈值就删除。在 0.98 下保留冲突,不等于这个阈值适用于其他数据。
“不能退款”的记录在这组实验中被保留,也不能因此证明所有否定表达都会正确区分。七条样本无法估计生产误判率,本文没有多语言模型、近似索引或三家准确率排行榜。提高阈值还可能漏掉真正改写,需同时检查误判与漏判。
第五步:验证每条输入都有去向
将下列代码保存为 check_report.py,与 deduplicate.py 放在同一目录。它检查各阈值的记录分区与来源,拒绝不合理输入。这里不把 observed false positive 当成测试失败丢掉,而是明确记录为需要人工处理的结果。
import json
from pathlib import Path
from deduplicate import RECORDS, validate
out = json.loads(Path('deduplication-output.json').read_text())
expected = sorted(row['id'] for row in RECORDS)
for threshold, result in out['results'].items():
actual = [r['id'] for r in result['selected']]
actual += [r['record']['id'] for r in result['filtered']]
assert sorted(actual) == expected, 'lost or duplicated source'
for item in result['filtered']:
assert item['duplicate_of']['id'] in expected
assert item['exact'] or item['score'] >= float(threshold) - 1e-6
assert any(r['record']['id'] == 'refund-30'
for r in out['results']['0.9']['filtered'])
assert 'refund-30' in {r['id'] for r in out['results']['0.98']['selected']}
for records, threshold in [([], 0.9), ([RECORDS[0], RECORDS[0]], 0.9),
([{'id':'x', 'text':' '}], 0.9), (RECORDS, 1.1)]:
try:
validate(records, threshold)
except ValueError:
continue
raise AssertionError('invalid input accepted')
print('Report consistency passed; factual merges still require review.')
python check_report.py
本次本地共完成十项检查:三个阈值的记录分区与来源、完全副本报告、实际误判保留、较高阈值对照,以及空输入、重复 ID、空文本和非法阈值拒绝。它验证示例路径和可追溯性,不证明模型没有误判。
阈值校准应从业务标签开始
先用人工样本标记哪些可以合并、哪些必须保留,覆盖数字、否定、时间、实体、同义改写和不相关内容。调参查询与最终验收集分开,不能反复优化同一批样本后把成绩当成泛化能力。复核边界候选,明确错误合并的成本。
换用适合中文的模型后,重新编码并重建索引,再用固定样本验收。保持模型快照、比较列、预处理、后端和阈值可追踪。近似索引还要检查候选召回;本例 basic 不提供其生产规模结论,也不能用小样本运行时间估算百万条性能。
跨训练测试去重不等于没有泄漏,仍应按来源、实体与时间分组。不同期限条款可能代表历史版本,应保留有效期与来源而非直接去掉。知识库需要把内容合并、来源关系和用户访问范围分开管理。
报告确认后再设计写回
当前程序只输出 JSON,写回是另外一个需复核的步骤。保留原始数据、过滤候选和最终审批记录,定义可恢复的更新方式。不要让文件名“filtered”误导后续人员认为它已获得删除授权。
真实输入与匹配报告可能包含内部文本,限制日志与共享权限。软件采用 MIT,不代表所有编码器和模型权重都使用相同条件。计算、索引、存储、下载、标注和人工复核仍是实际成本;本次没有调用付费 API。
继续阅读:SemHash 介绍、去重工具推荐、RapidFuzz 对比、datasketch 对比。官方资料:SemHash、去重接口、固定模型卡。
常见问题
- 高相似度可以直接删除吗?
- 不可以。期限、金额、否定和版本变化可能是必须保留的事实。
- 0.98 是通用安全阈值吗?
- 不是。它只在本次七条样本中保留期限差异,仍需按模型和业务校准。
- 默认英文模型能证明中文质量吗?
- 不能。中文示例仅为错配对照,换多语言模型仍需独立验收。
- 去重后就没有训练测试泄漏吗?
- 不能保证。还需按来源、实体、时间及任务定义划分和核查。
- 开源免费包含全部成本吗?
- 不包含模型、计算、存储、索引、标注、人工复核和维护。
- 教程会删除文件吗?
- 不会。只输出合成记录的保留和过滤候选报告,写回需另外复核。