infer_and_combine.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. #!/usr/bin/env python3
  2. """Generate retrospective model-assistance outputs without changing rule CSVs."""
  3. from __future__ import annotations
  4. import argparse
  5. import csv
  6. import importlib
  7. import json
  8. from pathlib import Path
  9. import joblib
  10. import numpy as np
  11. from sklearn.neighbors import NearestNeighbors
  12. def feature_module(model_type: str):
  13. """Pressure and vibration use independent feature modules."""
  14. return importlib.import_module(
  15. "generalized_features" if model_type == "pressure" else "generalized_features_vibration"
  16. )
  17. def load_samples(directory: Path) -> list[dict]:
  18. return [json.loads(path.read_text(encoding="utf-8")) for path in sorted(directory.glob("*.json"))]
  19. def matrix(feature_dicts: list[dict], names: list[str]) -> np.ndarray:
  20. return np.asarray([[item.get(name) for name in names] for item in feature_dicts], dtype=float)
  21. def combined_result(rule_label: int, probability: float, anomaly_similarity: float, threshold: float) -> str:
  22. model_support = probability >= threshold or anomaly_similarity >= 0.70
  23. if rule_label == 1 and model_support:
  24. return "模型辅助确认异常"
  25. if rule_label == 1:
  26. return "保留规则异常,模型意见不一致,人工复核"
  27. if model_support:
  28. return "规则正常但模型疑似异常,继续观察"
  29. return "正常"
  30. def main() -> None:
  31. parser = argparse.ArgumentParser(description="生成PKS规则与模型的历史联合辅助确认结果")
  32. parser.add_argument("model_type", choices=("pressure", "vibration"))
  33. parser.add_argument("--datasets-dir", default=str(Path(__file__).resolve().parent / "datasets"))
  34. parser.add_argument("--models-dir", default=str(Path(__file__).resolve().parent / "models"))
  35. parser.add_argument("--output-dir", default=str(Path(__file__).resolve().parents[2] / "cache" / "pks_scanner" / "model_assistant_generalized"))
  36. args = parser.parse_args()
  37. gf = feature_module(args.model_type)
  38. dataset = Path(args.datasets_dir) / args.model_type
  39. bundle = joblib.load(Path(args.models_dir) / f"{args.model_type}_model.joblib")
  40. metadata = json.loads((Path(args.models_dir) / f"{args.model_type}_metadata.json").read_text(encoding="utf-8"))
  41. samples = load_samples(dataset / "candidates")
  42. training = load_samples(dataset / "train")
  43. baselines = bundle["unit_baselines"]
  44. x = bundle["imputer"].transform(matrix(gf.feature_matrix(samples, baselines), bundle["feature_names"]))
  45. x_train = bundle["imputer"].transform(matrix(gf.feature_matrix(training, baselines), bundle["feature_names"]))
  46. probabilities = bundle["model"].predict_proba(x)[:, 1]
  47. scaled = bundle["knn"].named_steps["scale"].transform(x)
  48. train_scaled = bundle["knn"].named_steps["scale"].transform(x_train)
  49. anomaly_indexes = [index for index, sample in enumerate(training) if sample["label"] == 1]
  50. if not anomaly_indexes:
  51. parser.error("训练集没有异常样本,无法建立异常相似性索引")
  52. anomaly_neighbors = NearestNeighbors(n_neighbors=1).fit(train_scaled[anomaly_indexes])
  53. distances, nearest_indexes = anomaly_neighbors.kneighbors(scaled)
  54. rows = []
  55. for index, sample in enumerate(samples):
  56. nearest = training[anomaly_indexes[int(nearest_indexes[index][0])]]
  57. anomaly_similarity = float(1 / (1 + distances[index][0]))
  58. rule_label = int(sample["label"])
  59. rows.append({
  60. "机组": sample["unit"], "批次": sample["batch"], "窗口开始小时": sample["start_hour"], "窗口结束小时": sample["end_hour"],
  61. "样本编号": sample["sample_id"], "现有规则结果": "异常" if rule_label else "正常",
  62. "规则阶段编号": sample.get("rule_stage_id") or "", "模型异常概率": round(float(probabilities[index]), 6),
  63. "最相似历史异常样本": nearest["sample_id"], "历史异常相似度": round(anomaly_similarity, 6),
  64. "联合判断": combined_result(rule_label, probabilities[index], anomaly_similarity, metadata["threshold"]),
  65. })
  66. rows.sort(key=lambda row: (int(row["机组"][0]), row["窗口结束小时"], row["批次"]))
  67. output = Path(args.output_dir)
  68. output.mkdir(parents=True, exist_ok=True)
  69. name = "压力模型辅助确认.csv" if args.model_type == "pressure" else "振动模型辅助确认.csv"
  70. with (output / name).open("w", encoding="utf-8-sig", newline="") as handle:
  71. writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
  72. writer.writeheader()
  73. writer.writerows(rows)
  74. print(f"{args.model_type} 联合辅助结果:{len(rows):,} 行,输出:{output / name}")
  75. if __name__ == "__main__":
  76. main()