#!/usr/bin/env python3 import json from datetime import datetime from pathlib import Path ROOT = Path(__file__).resolve().parents[1] REPORT_DIR = ROOT / "reports" OUTPUT = REPORT_DIR / "index.html" def read_json(path, default=None): try: return json.loads(Path(path).read_text(encoding="utf-8")) except Exception: return default def count_lines(path): try: with Path(path).open(encoding="utf-8") as f: return sum(1 for _ in f) except Exception: return 0 def parse_log_status(): monitor = ROOT / "outputs/logs/training_monitor.log" final_files = [ ROOT / "outputs/metrics/finetuned_struct_metrics.json", ROOT / "outputs/metrics/finetuned_qa_metrics.json", ROOT / "outputs/figures/metric_comparison.csv", ] if monitor.exists(): log = monitor else: logs = sorted((ROOT / "outputs/logs").glob("*.log"), key=lambda p: p.stat().st_mtime, reverse=True) if not logs: return {"log_file": None, "tail": [], "stage": "not_started"} log = logs[0] tail = log.read_text(encoding="utf-8", errors="ignore").splitlines()[-120:] joined = "\n".join(tail) if all(p.exists() for p in final_files): stage = "completed" elif "scripts/train_qlora.py" in joined: stage = "training" elif "--run-name finetuned" in joined: stage = "finetuned_eval" elif "Traceback" in joined or "Error" in joined: stage = "needs_attention" else: stage = "running" return {"log_file": str(log.relative_to(ROOT)), "tail": tail[-20:], "stage": stage} def metrics_from_prediction_file(path, limit=None): required = [ "current_behavior", "is_transition", "elapsed_seconds_in_current_behavior", "estimated_remaining_seconds", "full_remaining_seconds", "expected_end_time", "next_possible_behavior", "stage_index", "total_stages", "sequence_so_far", ] rows = [] try: with Path(path).open(encoding="utf-8") as f: for line in f: if line.strip(): rows.append(json.loads(line)) if limit and len(rows) >= limit: break except Exception: return {} if not rows: return {} parsed = [r for r in rows if isinstance(r.get("prediction"), dict)] def acc(field): pairs = [(r["target"].get(field), r["prediction"].get(field)) for r in parsed if field in r["prediction"]] return sum(a == b for a, b in pairs) / len(pairs) if pairs else 0 def mae(field): pairs = [] for r in parsed: p = r.get("prediction", {}) t = r.get("target", {}) if isinstance(t.get(field), (int, float)) and isinstance(p.get(field), (int, float)): pairs.append(abs(float(t[field]) - float(p[field]))) return sum(pairs) / len(pairs) if pairs else None return { "num_examples": len(rows), "json_parse_rate": len(parsed) / len(rows), "required_field_complete_rate": sum(all(f in r["prediction"] for f in required) for r in parsed) / len(rows), "current_behavior_accuracy": acc("current_behavior"), "next_possible_behavior_accuracy": acc("next_possible_behavior"), "is_transition_accuracy": acc("is_transition"), "stage_index_accuracy": acc("stage_index"), "full_remaining_seconds_mae": mae("full_remaining_seconds"), } def load_metrics(): metric_dir = ROOT / "outputs/metrics" metrics = {} for path in sorted(metric_dir.glob("*.json")): payload = read_json(path) if not payload: continue key = f"{payload.get('run_name')}_{payload.get('task_type')}" metrics[key] = payload partial = metrics_from_prediction_file(ROOT / "outputs/predictions/base_struct_predictions.jsonl") if partial: metrics["base_struct_partial"] = { "run_name": "base_partial", "task_type": "struct", "input_file": "outputs/predictions/base_struct_predictions.jsonl", "metrics": partial, } return metrics def build_data(): summary = read_json(ROOT / "data/processed/summary.json", {}) progress = { "base_struct_done": count_lines(ROOT / "outputs/predictions/base_struct_predictions.jsonl"), "base_qa_done": count_lines(ROOT / "outputs/predictions/base_qa_predictions.jsonl"), "finetuned_struct_done": count_lines(ROOT / "outputs/predictions/finetuned_struct_predictions.jsonl"), "finetuned_qa_done": count_lines(ROOT / "outputs/predictions/finetuned_qa_predictions.jsonl"), "val_total": summary.get("val_struct", {}).get("num_examples", 4030), } return { "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "summary": summary, "metrics": load_metrics(), "progress": progress, "log_status": parse_log_status(), "files": { "processed_summary": "data/processed/summary.json", "base_predictions": "outputs/predictions/base_struct_predictions.jsonl", "adapter_dir": "outputs/qwen35_9b_lora", "figures_dir": "outputs/figures", }, } HTML_TEMPLATE = r""" MWave Aircraft Lavatory Radar LLM Workflow
MWave Radar LLM · Aircraft Lavatory Intelligence

毫米波雷达时序行为模型工作流报告

从飞机厕所内毫米波雷达轨迹窗口和中间层表征出发,微调 Qwen3.5-9B,完成结构化行为预测、序列重建、剩余时间估计和 QA 状态问答,并支持部署到航空场景的边缘推理方案。

-
训练结构样本
-
验证结构样本
-
QA 训练样本
-
整体评估进度
RUNNING

报告生成时间:
模型:Qwen/Qwen3.5-9B + 4-bit QLoRA
输出:JSON schema + QA schema + charts

端到端 Workflow

Train · Evaluate · Deploy
1
数据解析读取 train/val JSONL,解析 chat 格式、轨迹窗口、背景知识和 assistant JSON。
2
标签规范统一 `反复折返 -> 折返`,固定结构化输出字段,生成 QA 目标。
3
基线评估Qwen3.5-9B base 先跑 val,保存微调前预测和指标。
4
QLoRA 微调结构化预测与 QA 混合 SFT,4-bit NF4,LoRA 训练约 29M 参数。
5
微调后评估同一套 val、同一套指标,输出 finetuned 预测、指标和图表。
6
机载部署毫米波雷达边缘预处理 + 本地 LLM 推理 + 客舱系统状态输出。

数据画像

行为标签分布

任务样本构成

可视化结果

Base vs Finetuned

结构化指标

QA 指标

总结与发现

Current Findings
1
base 模型不等于任务模型

未微调 Qwen3.5-9B 能理解中文指令,但结构化 schema 稳定性不足,尤其容易输出解释性文本或缺字段。

2
QA 必须独立评估

QA 不是简单复述,需要从 `full_remaining_seconds`、`sequence_so_far` 和异常规则推导,占用、空出时间、区域使用和异常应单独打分。

3
短行为是关键风险

进入、门锁、坐下、起身、折返、犹豫等短时行为占比较低,但对流程阶段和剩余时间预测影响很大。

数据风险

训练集中坐用马桶占比最高,类别不平衡明显;若最终少数类 F1 低,需要重采样或 loss 权重。

工程风险

长 prompt 和 9B 推理导致全量评估耗时较长;部署时应缓存背景知识、压缩轨迹窗口并使用约束解码。

安全边界

系统只输出状态和行为,不输出身份识别,不存储原始可逆人体点云,降低隐私风险。

上线策略

先 shadow mode 与人工规则并行,确认误报/漏报边界,再进入客舱状态提示闭环。

飞机厕所部署技术方案

Edge-first · Privacy-preserving

1. 传感与预处理

毫米波雷达采集点云/轨迹;本地 MCU/边缘 SoC 做去噪、目标跟踪、窗口化、速度和区域特征提取。

2. 行为 LLM 推理

Qwen3.5-9B LoRA/量化模型接收结构化窗口,输出严格 JSON:当前行为、阶段、剩余时间、序列。

3. 客舱系统集成

输出占用、预计空出、异常、已使用区域。对接乘务终端、维护日志、客舱状态总线。

边缘硬件

训练在地面 GPU;机载推理建议使用小型边缘 GPU/NPU 或将 LLM 部署在客舱边缘计算单元,厕所侧只传结构化特征。

实时策略

每 0.5-1 秒更新窗口;稳定行为可降频推理,过渡态或异常候选升频推理。

失效保护

JSON 校验失败时回退规则模型;连续异常时只提示“需关注”,不直接做强制控制决策。

运行状态与文件

Live
0
base struct 已生成
0
base QA 已生成
0
finetuned struct 已生成
0
finetuned QA 已生成

最近日志


  
""" def main(): REPORT_DIR.mkdir(parents=True, exist_ok=True) data = build_data() html = HTML_TEMPLATE.replace("__DATA__", json.dumps(data, ensure_ascii=False)) OUTPUT.write_text(html, encoding="utf-8") print(f"wrote {OUTPUT}") if __name__ == "__main__": main()