Entity Dedup · 实体判重与合并命名
同一实体在不同对话中可能具有不同名称,导致信息重复或分散。基于BERT 二分类 + Qwen3.5-2B LoRA 命名的推理项目。输入两个候选实体,判断是否合并;合并时生成名称并合并描述。本项目包含推理代码及选定的 BERT、LoRA 适配器权重,不包含Qwen 基座权重,需要单独准备Qwen3.5-2B基座。
模型流程
flowchart TD
IN["输入 JSON:A / B"] --> VALID["字段校验与白名单"]
VALID --> RULE["现有推理规则:号码拦截优先,其次强制合并"]
RULE -->|未命中| BERT["BERT:名称、类型、别名"]
BERT --> SEM["CLS 768 → MLP 128 → 64 维"]
VALID --> FEAT["Python:32 维硬特征"]
FEAT --> SCALE["长度及计数 log1p + 标准化;其余保持原值"]
SEM --> CONCAT["拼接 96 维 → MLP 128 → 1 → Sigmoid"]
SCALE --> CONCAT
CONCAT --> AVG["A/B 与 B/A 概率平均"]
AVG --> DECISION["阈值判断,默认 0.5"]
RULE -->|命中| DECISION
DECISION -->|不合并| DROP["should_merge=false;合并字段为 null"]
DECISION -->|合并| PROMPT["名称 + 可选描述;清洗描述日志时间戳"]
PROMPT --> LORA["Qwen3.5-2B + LoRA:仅生成名称"]
LORA --> PY["Python:别名并集、描述直接拼接"]
PY --> OUT["输出 JSON"]
目录
.
├── config.json # 相对路径以配置文件所在目录为基准
├── predict.py # JSON / JSONL 推理入口
├── verify_artifacts.py # 模型文件 SHA-256 校验
├── entity_merge/ # 独立推理实现、硬特征、词表和 prompt
├── models/
│ ├── bert/ # 完整分类器、tokenizer、特征元数据
│ ├── lora/ # LoRA 适配器及配套 tokenizer
│ └── manifest.json # 模型文件摘要
├── examples/ # 虚构示例
└── tests/ # 无须权重的接口与规则测试
安装与配置
已验证环境为 Python 3.13.5、NVIDIA GPU、torch 2.9.0+cu128。先按设备环境安装 CUDA 版 PyTorch,再安装依赖。项目默认离线加载,不自动下载基座。
python -m pip install -r requirements.txt
python verify_artifacts.py
编辑 config.json 的 lora.base_model_path,指向完整的 Qwen3.5-2B 模型目录,目录中须包含 config、权重及其索引。可使用绝对路径,也可使用相对于配置文件的路径。例如 Linux 使用 /opt/models/Qwen3.5-2B,Windows 使用 C:/models/Qwen3.5-2B。
{
"bert": {
"checkpoint_dir": "models/bert",
"device": "cuda:0",
"threshold": 0.5,
"batch_size": 16
},
"lora": {
"base_model_path": "models/Qwen3.5-2B",
"adapter_dir": "models/lora",
"device": "cuda:0",
"dtype": "bfloat16",
"attn_implementation": "sdpa",
"max_new_tokens": 96
}
}
threshold 允许 0–1,概率大于等于阈值预测合并。LoRA 按需加载,全部不合并时不会加载基座。两个模型常驻同一进程;显存不足时可以让 BERT 使用 cpu,LoRA 仍用 GPU。纯 CPU 使用时将两处 device 改为 cpu、LoRA dtype 改为 float32,速度较慢。batch_size 用于 BERT 两种顺序的计算,当前 CLI 按候选对顺序推理。
输入格式
单条 JSON 对象、JSON 数组均支持;输入文件扩展名为 .jsonl 时逐行解析。可选 sample_id 原样回传,不进入模型。
{
"sample_id": "demo-merge",
"A": {
"name": "蓝色收纳箱",
"entity_type": "物品设备",
"aliases": ["储物箱"],
"description": "蓝色的收纳箱"
},
"B": {
"name": "带盖收纳箱",
"entity_type": "物品设备",
"aliases": ["储物箱", "带盖箱"],
"description": "有盖子的收纳箱"
}
}
| 字段 | 类型 | 约束 |
|---|---|---|
| A / B | object | 必填 |
| name | string | 必填,非空 |
| entity_type | string | 必填,非空,保持业务类别名称 |
| aliases | string[] | 可省略,默认 [] |
| description | string / null | 可省略,只进入 LoRA |
| sample_id | string | 可省略,调用方请求标识 |
其他字段不传入模型,尤其不使用 description_summary、标签、审核答案或召回分数。
输出格式
合并示例:
{
"sample_id": "demo-merge",
"should_merge": true,
"merge_score": 0.9,
"bert_probability": 0.9,
"decision_source": "bert",
"name_containment": 0,
"new_name": "蓝色带盖收纳箱",
"merged_aliases": ["储物箱", "带盖箱"],
"description_merge": "蓝色的收纳箱;有盖子的收纳箱"
}
| 字段 | 说明 |
|---|---|
| should_merge | 最终是否合并 |
| merge_score | BERT 合并概率;规则拦截为 0,规则合并为 1。规则值不是模型置信度 |
| bert_probability | 实际 BERT 概率;规则跳过模型时为 null |
| decision_source | bert、number_guard、action_wrapper 或 lead_customer |
| name_containment | 原始名称相等或互为子字符串为 1,否则为 0;仅诊断标签 |
| new_name | LoRA 生成的名称;不合并时 null |
| merged_aliases | Python 计算的别名并集;不合并时 null |
| description_merge | Python 拼接原始描述;不合并时 null |
执行推理
在项目根目录运行:
python predict.py --config config.json --input examples/input.json --output outputs/result.json
JSONL 示例:
python predict.py --config config.local.json --input requests.jsonl --output outputs/results.jsonl
输出形状随输入:对象对应对象、数组对应数组、JSONL 对应 JSONL。已有结果文件会被覆盖;只有整批成功后才原子替换文件,失败时保留原有文件并返回非零退出码。不会将错误生成结果当成“不合并”。日志写入终端,不进入结果 JSON。
Python 调用:
from entity_merge.runtime import load_pipeline
pipeline = load_pipeline("config.json") # 进程内初始化一次并复用
result = pipeline.predict({
"A": {"name": "蓝色收纳箱", "entity_type": "物品设备", "aliases": []},
"B": {"name": "带盖收纳箱", "entity_type": "物品设备", "aliases": []}
})
输出名称可能存在遗漏,单行格式校验不代表语义完整性。此包提供推理接口,不将历史实验指标视为当前端到端链路的上线评估。
验证
python -m unittest discover -s tests -v
python verify_artifacts.py
单元测试覆盖不合并跳过 LoRA、合并字段、别名去重、描述保留、日志时间戳清洗、规则优先级、包含关系仍走 LoRA、输入白名单及生成失败行为。真实权重加载和端到端测试需要安装依赖并提供基座。
交付前已在真实 GPU 上通过随包示例完成一次端到端推理,实际结果见 examples/output.actual.json。