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()