347 lines
15 KiB
Python
347 lines
15 KiB
Python
import os
|
|
import re
|
|
import json
|
|
import time
|
|
import argparse
|
|
from collections import defaultdict
|
|
import torch
|
|
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
|
|
from peft import PeftModel
|
|
from modelscope import snapshot_download
|
|
|
|
# ==================== 1. 3GPP 协议核心规范与术语词表 ====================
|
|
SPEC_REGEX = r'\bTS\s*38\.\d{3}\b|\bTR\s*38\.\d{3}\b|\bTS\s*23\.\d{3}\b'
|
|
CORE_RAN_TERMS = [
|
|
# 物理层与信道
|
|
'PDCCH', 'PUSCH', 'PUCCH', 'PRACH', 'DMRS', 'CSI-RS', 'SSB', 'PBCH', 'SRS', 'BWP', 'PT-RS',
|
|
# 控制信令与单元
|
|
'DCI', 'UCI', 'MAC CE', 'RRC', 'RLC', 'PDCP', 'SDAP', 'NAS', 'SIB1', 'SIB',
|
|
# 移动性与度量
|
|
'LTM', 'RSRP', 'RSRQ', 'SINR', 'TA', 'TCI', 'QCL', 'C-RNTI', 'PCI', 'RLF', 'HO',
|
|
# 前沿特性
|
|
'NES', 'RedCap', 'eRedCap', 'XR', 'SBFD', 'CLI', 'Ambient IoT', 'NTN', 'MUSIM', 'SDT'
|
|
]
|
|
|
|
# 6 类核心任务的标准结构要素检测模式
|
|
TASK_STRUCTURE_PATTERNS = {
|
|
'proposal_understanding': [
|
|
(r'解决.*问题|问题根因', '解决什么问题'),
|
|
(r'核心方案|总体设计', '核心方案'),
|
|
(r'Trigger|触发条件|触发机制', 'Trigger'),
|
|
(r'Configuration|配置', 'Configuration'),
|
|
(r'UE\s*行为|终端行为', 'UE 行为'),
|
|
(r'Network\s*行为|网络行为|基站行为', 'Network 行为'),
|
|
(r'新增.*修改.*信息|信元|协议修改', '新增或修改的信息'),
|
|
(r'协议流程|信令流程|交互流程', '协议流程'),
|
|
(r'优点|优势|收益', '优点'),
|
|
(r'潜在限制|局限|约束', '潜在限制')
|
|
],
|
|
'proposal_comparison': [
|
|
(r'核心思路|方案思路', '各方案核心思路'),
|
|
(r'共同点|相同点', '共同点'),
|
|
(r'差异点|不同点', '差异点'),
|
|
(r'冲突点|分歧点', '冲突点'),
|
|
(r'优缺点|各自优缺点', '各自优缺点'),
|
|
(r'已经覆盖|已覆盖', '哪些问题已经覆盖'),
|
|
(r'仍未解决|未决问题|遗留', '哪些问题仍未解决')
|
|
],
|
|
'gap_to_solution': [
|
|
(r'问题根因|根因分析', '问题根因'),
|
|
(r'总体设计|架构设计', '总体设计'),
|
|
(r'新增[/、]修改.*机制|协议机制', '新增/修改的协议机制'),
|
|
(r'UE\s*行为|终端行为', 'UE 行为'),
|
|
(r'Network\s*行为|网络行为|基站行为', 'Network 行为'),
|
|
(r'Trigger|触发条件', 'Trigger'),
|
|
(r'Configuration|配置信元', 'Configuration'),
|
|
(r'IE|Timer|Procedure|信元|定时器', 'IE / Timer / Procedure'),
|
|
(r'协议流程|交互时序', '协议流程'),
|
|
(r'解决.*Gap|解决.*问题', '解决了哪些 Gap'),
|
|
(r'Trade-off|权衡|代价', 'Trade-off'),
|
|
(r'潜在风险|边界风险', '潜在风险')
|
|
],
|
|
'gap_analysis': [
|
|
(r'根因|背景|协议限制', '根因与背景'),
|
|
(r'缺陷|瓶颈|Gap', '瓶颈与缺陷'),
|
|
(r'开销|时延|复杂度', '开销与时延影响'),
|
|
(r'兼容|互操作|向后兼容', '兼容性与互操作分析')
|
|
],
|
|
'candidate_critic': [
|
|
(r'解决.*目标\s*Gap|解决.*问题', '是否真正解决目标Gap'),
|
|
(r'新问题|副作用', '是否引入新问题/副作用'),
|
|
(r'冗余设计|复用', '冗余设计与复用已有机制'),
|
|
(r'信令开销|开销评估', '信令开销评估'),
|
|
(r'复杂度|实现复杂度', '实现复杂度分析'),
|
|
(r'向后兼容|兼容性风险', '向后兼容性风险'),
|
|
(r'极端场景|Corner\s*Case', '极端场景与边界'),
|
|
(r'替代方案|更简洁', '是否存在更简洁替代方案'),
|
|
(r'评审结论|修改建议', '评审结论与修改建议')
|
|
],
|
|
'remaining_gap': [
|
|
(r'已解决|基线成果|当前阶段', '当前已解决的核心部分'),
|
|
(r'未覆盖|场景盲区', '仍未覆盖的典型场景'),
|
|
(r'极端边界|Corner\s*Case', '极端边界与 Corner Cases'),
|
|
(r'开销|代价|代价分析', '开销存储与时延代价'),
|
|
(r'演进建议|未来工作|后续路线', '演进建议与未来工作方向')
|
|
]
|
|
}
|
|
|
|
# ==================== 2. 评测打分函数 ====================
|
|
def compute_structure_score(task_type, text):
|
|
patterns = TASK_STRUCTURE_PATTERNS.get(task_type, [])
|
|
if not patterns:
|
|
return 100.0, []
|
|
hits = []
|
|
for pat, name in patterns:
|
|
if re.search(pat, text, re.I):
|
|
hits.append(name)
|
|
score = (len(hits) / len(patterns)) * 100.0
|
|
return score, hits
|
|
|
|
def compute_spec_compliance(text):
|
|
specs = set(re.findall(SPEC_REGEX, text, re.I))
|
|
terms = set()
|
|
for t in CORE_RAN_TERMS:
|
|
if re.search(r'\b' + re.escape(t) + r'\b', text, re.I):
|
|
terms.add(t.upper())
|
|
total_unique_hits = len(specs) + len(terms)
|
|
# 达到 8 个不同标准规范与关键术语即得满分 100
|
|
density_score = min(100.0, total_unique_hits * 12.5)
|
|
return density_score, list(specs), list(terms)
|
|
|
|
def compute_char_lcs(s1, s2):
|
|
"""计算两段文本的最长公共子序列(用于评估专家语义相似度),优化空间复杂度"""
|
|
s1_clean = re.sub(r'[^\w\u4e00-\u9fa5]', '', s1)
|
|
s2_clean = re.sub(r'[^\w\u4e00-\u9fa5]', '', s2)
|
|
if not s1_clean or not s2_clean:
|
|
return 0.0
|
|
|
|
# 截取前 800 字符进行快速精确动态规划
|
|
s1_sub = s1_clean[:800]
|
|
s2_sub = s2_clean[:800]
|
|
|
|
dp = [0] * (len(s2_sub) + 1)
|
|
for c1 in s1_sub:
|
|
new_dp = [0] * (len(s2_sub) + 1)
|
|
for j, c2 in enumerate(s2_sub):
|
|
if c1 == c2:
|
|
new_dp[j + 1] = dp[j] + 1
|
|
else:
|
|
new_dp[j + 1] = max(new_dp[j], dp[j + 1])
|
|
dp = new_dp
|
|
|
|
lcs_len = dp[-1]
|
|
f1 = (2.0 * lcs_len) / (len(s1_sub) + len(s2_sub)) * 100.0
|
|
return f1
|
|
|
|
# ==================== 3. 主评测执行流程 ====================
|
|
def run_benchmark(model, tokenizer, test_samples, device, desc="模型", batch_size=5, max_new_tokens=1024):
|
|
print(f"\n=======================================================", flush=True)
|
|
print(f" 开始 3GPP-Agent 50 题全量盲测基准评测 (并行提速版, Batch={batch_size}): {desc}", flush=True)
|
|
print(f"=======================================================", flush=True)
|
|
|
|
results = []
|
|
task_scores = defaultdict(lambda: {'struct': [], 'spec': [], 'semantic': [], 'composite': []})
|
|
|
|
total = len(test_samples)
|
|
start_time = time.time()
|
|
|
|
# 设置左侧 padding 用于 decoder-only 批量生成
|
|
tokenizer.padding_side = "left"
|
|
if tokenizer.pad_token is None:
|
|
tokenizer.pad_token = tokenizer.eos_token
|
|
|
|
for b_start in range(0, total, batch_size):
|
|
b_samples = test_samples[b_start : b_start + batch_size]
|
|
prompts = [
|
|
tokenizer.apply_chat_template(
|
|
s['messages'][:-1],
|
|
tokenize=False,
|
|
add_generation_prompt=True
|
|
)
|
|
for s in b_samples
|
|
]
|
|
|
|
inputs = tokenizer(prompts, return_tensors="pt", padding=True).to(device)
|
|
input_len = inputs.input_ids.shape[1]
|
|
|
|
with torch.no_grad():
|
|
outputs = model.generate(
|
|
**inputs,
|
|
max_new_tokens=max_new_tokens,
|
|
do_sample=False,
|
|
repetition_penalty=1.05,
|
|
pad_token_id=tokenizer.pad_token_id,
|
|
eos_token_id=tokenizer.eos_token_id
|
|
)
|
|
|
|
for i, sample in enumerate(b_samples):
|
|
sid = sample['sample_id']
|
|
task_type = sample['task_type']
|
|
topic = sample['topic']
|
|
ref_assistant = sample['messages'][-1]['content']
|
|
|
|
gen_ids = outputs[i][input_len:]
|
|
pred_text = tokenizer.decode(gen_ids, skip_special_tokens=True).strip()
|
|
|
|
# 计算三维指标
|
|
struct_score, struct_hits = compute_structure_score(task_type, pred_text)
|
|
spec_score, spec_hits, term_hits = compute_spec_compliance(pred_text)
|
|
semantic_f1 = compute_char_lcs(pred_text, ref_assistant)
|
|
|
|
# 综合得分加权 (40% 结构遵从 + 35% 协议规范 + 25% 专家语义对齐)
|
|
composite = 0.40 * struct_score + 0.35 * spec_score + 0.25 * semantic_f1
|
|
|
|
task_scores[task_type]['struct'].append(struct_score)
|
|
task_scores[task_type]['spec'].append(spec_score)
|
|
task_scores[task_type]['semantic'].append(semantic_f1)
|
|
task_scores[task_type]['composite'].append(composite)
|
|
|
|
results.append({
|
|
'sample_id': sid,
|
|
'task_type': task_type,
|
|
'topic': topic,
|
|
'struct_score': round(struct_score, 2),
|
|
'spec_score': round(spec_score, 2),
|
|
'semantic_f1': round(semantic_f1, 2),
|
|
'composite_score': round(composite, 2),
|
|
'struct_hits': struct_hits,
|
|
'spec_hits': spec_hits,
|
|
'term_hits': term_hits,
|
|
'pred_len': len(pred_text),
|
|
'generated_response': pred_text
|
|
})
|
|
|
|
processed = min(b_start + batch_size, total)
|
|
elapsed = time.time() - start_time
|
|
last_sid = b_samples[-1]['sample_id']
|
|
last_tt = b_samples[-1]['task_type']
|
|
last_comp = results[-1]['composite_score']
|
|
print(f"进度: [{processed:2d}/{total:2d}] | 耗时: {elapsed:.1f}s ({elapsed/processed:.1f}s/题) | 最新: {last_sid} ({last_tt}) -> 综合得分: {last_comp:.1f}", flush=True)
|
|
|
|
# 4. 汇总统计报告
|
|
print("\n" + "="*70, flush=True)
|
|
print(f" 3GPP-Agent 50 题盲测基准评测报告 - {desc}", flush=True)
|
|
print("="*70, flush=True)
|
|
print(f"{'任务类型 (Task Type)':<25} | {'样本数':<5} | {'结构遵从':<8} | {'规范术语':<8} | {'语义对齐':<8} | {'综合得分':<8}")
|
|
print("-" * 70)
|
|
|
|
all_struct = []
|
|
all_spec = []
|
|
all_semantic = []
|
|
all_composite = []
|
|
|
|
summary_by_task = {}
|
|
for tt, scores in task_scores.items():
|
|
avg_st = sum(scores['struct']) / len(scores['struct'])
|
|
avg_sp = sum(scores['spec']) / len(scores['spec'])
|
|
avg_se = sum(scores['semantic']) / len(scores['semantic'])
|
|
avg_cp = sum(scores['composite']) / len(scores['composite'])
|
|
|
|
all_struct.extend(scores['struct'])
|
|
all_spec.extend(scores['spec'])
|
|
all_semantic.extend(scores['semantic'])
|
|
all_composite.extend(scores['composite'])
|
|
|
|
summary_by_task[tt] = {
|
|
'count': len(scores['struct']),
|
|
'struct_avg': round(avg_st, 2),
|
|
'spec_avg': round(avg_sp, 2),
|
|
'semantic_avg': round(avg_se, 2),
|
|
'composite_avg': round(avg_cp, 2)
|
|
}
|
|
print(f"{tt:<25} | {len(scores['struct']):<5} | {avg_st:6.1f}% | {avg_sp:6.1f}% | {avg_se:6.1f}% | {avg_cp:6.1f}%")
|
|
|
|
print("-" * 70)
|
|
overall_st = sum(all_struct) / len(all_struct)
|
|
overall_sp = sum(all_spec) / len(all_spec)
|
|
overall_se = sum(all_semantic) / len(all_semantic)
|
|
overall_cp = sum(all_composite) / len(all_composite)
|
|
print(f"{'全量总评 (OVERALL)':<25} | {total:<5} | {overall_st:6.1f}% | {overall_sp:6.1f}% | {overall_se:6.1f}% | {overall_cp:6.1f}%")
|
|
print("="*70 + "\n", flush=True)
|
|
|
|
report_payload = {
|
|
'model_description': desc,
|
|
'timestamp': time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
'total_samples': total,
|
|
'overall_summary': {
|
|
'structure_completeness_avg': round(overall_st, 2),
|
|
'spec_compliance_avg': round(overall_sp, 2),
|
|
'semantic_alignment_avg': round(overall_se, 2),
|
|
'composite_score_avg': round(overall_cp, 2)
|
|
},
|
|
'task_summary': summary_by_task,
|
|
'details': results
|
|
}
|
|
return report_payload
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="3GPP-Agent 50-sample Benchmark Evaluation")
|
|
parser.add_argument("--model_id", type=str, default="Qwen/Qwen2.5-3B-Instruct", help="Base model identifier")
|
|
parser.add_argument("--lora_dir", type=str, default=None, help="Path to LoRA weights directory")
|
|
parser.add_argument("--test_file", type=str, default="dataset/ran2_benchmark_test_50.jsonl", help="Path to 50 test jsonl")
|
|
parser.add_argument("--output_json", type=str, default="output/benchmark_baseline_base_3b.json", help="Path to output json")
|
|
parser.add_argument("--desc", type=str, default="Qwen2.5-3B-Instruct (Base 未微调)", help="Model description")
|
|
parser.add_argument("--limit", type=int, default=None, help="Limit number of test samples (for dry run)")
|
|
parser.add_argument("--batch_size", type=int, default=5, help="Batch size for parallel generation")
|
|
parser.add_argument("--max_new_tokens", type=int, default=1024, help="Max new tokens to generate")
|
|
args = parser.parse_args()
|
|
|
|
device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
print(f"[Device] 评测设备: {device} ({torch.cuda.get_device_name(0) if 'cuda' in device else 'CPU'})", flush=True)
|
|
|
|
if not os.path.exists(args.test_file):
|
|
raise FileNotFoundError(f"未找到测试集: {args.test_file}")
|
|
|
|
with open(args.test_file, 'r', encoding='utf-8') as f:
|
|
samples = [json.loads(line) for line in f if line.strip()]
|
|
|
|
if args.limit:
|
|
samples = samples[:args.limit]
|
|
print(f"[Dataset] 已加载测试集: {len(samples)} 道题目 ({args.test_file})", flush=True)
|
|
|
|
print(f"[Model] 正在加载模型权重: {args.model_id} ...", flush=True)
|
|
local_model_dir = r"C:\Users\ZoOoM\.cache\modelscope\hub\models\Qwen\Qwen2___5-3B-Instruct"
|
|
if os.path.exists(local_model_dir):
|
|
print(f"[Model] 使用本地权重: {local_model_dir}", flush=True)
|
|
model_dir = local_model_dir
|
|
else:
|
|
model_dir = snapshot_download(args.model_id)
|
|
|
|
tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
|
|
if tokenizer.pad_token is None:
|
|
tokenizer.pad_token = tokenizer.eos_token
|
|
|
|
# 4-bit 量化加载以保护 8GB 显存并加速推断
|
|
bnb_config = BitsAndBytesConfig(
|
|
load_in_4bit=True,
|
|
bnb_4bit_quant_type="nf4",
|
|
bnb_4bit_use_double_quant=True,
|
|
bnb_4bit_compute_dtype=torch.bfloat16
|
|
)
|
|
|
|
model = AutoModelForCausalLM.from_pretrained(
|
|
model_dir,
|
|
quantization_config=bnb_config,
|
|
device_map="auto",
|
|
attn_implementation="sdpa",
|
|
trust_remote_code=True
|
|
)
|
|
|
|
if args.lora_dir:
|
|
print(f"[LoRA] 正在挂载 LoRA 权重: {args.lora_dir} ...", flush=True)
|
|
model = PeftModel.from_pretrained(model, args.lora_dir)
|
|
|
|
model.eval()
|
|
|
|
# 运行 Benchmark
|
|
report = run_benchmark(model, tokenizer, samples, device, desc=args.desc, batch_size=args.batch_size, max_new_tokens=args.max_new_tokens)
|
|
|
|
os.makedirs(os.path.dirname(args.output_json), exist_ok=True)
|
|
with open(args.output_json, 'w', encoding='utf-8') as f:
|
|
json.dump(report, f, ensure_ascii=False, indent=2)
|
|
|
|
print(f"[Saved] 评测完整 JSON 数据已保存至: {args.output_json}", flush=True)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|