hee_!J commited on
Commit
77d44e1
·
1 Parent(s): 8a48888

feat: Tier 1 이상 탐지 SECOM 기반 구현

Browse files
agents/detection.py CHANGED
@@ -1,11 +1,63 @@
1
- """Tier 1 이상 탐지 에이전트 (M2에서 구현)
2
 
3
- 입력: 알람 컨텍스트 + SECOM 센서 (data.secom.loader.load_secom)
4
- 출력: core.schema.Tier1 (이상 점수 + 기여 피처 Top-N + 영향 lot)
5
- 모델: GPT-5 mini + tool use, IsolationForest 등으이상 점수 계산
 
6
  """
 
 
 
 
 
7
  from core.schema import Tier1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
 
10
  def run_detection(alarm: dict) -> Tier1:
11
- raise NotImplementedError("M2: Tier 1 이상 탐지 에이전트 구현 예정")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tier 1 이상 탐지 에이전트
2
 
3
+ 알람 SECOM 웨이퍼 row에 매핑해 점수와 기여 피처를 계산
4
+ 모델: IsolationForest (experiments/tier1_detection 벤치마크에서 PR-AUC 가장 우수)
5
+ 이상 점수는 학습 분포 대비 백분위0~1 정규화
6
+ 기여 피처는 표준화 값의 절대크기 Top-N (정상 분포에서 가장 벗어난 센서)
7
  """
8
+ from functools import lru_cache
9
+
10
+ import numpy as np
11
+ from sklearn.ensemble import IsolationForest
12
+
13
  from core.schema import Tier1
14
+ from data.secom.loader import load_secom
15
+ from data.secom.preprocess import SecomPreprocessor
16
+
17
+ RANDOM_STATE = 42
18
+ TOP_N_FEATURES = 3
19
+
20
+ # SECOM은 익명화 데이터라 lot/step이 없음, MVP에선 알람을 특정 웨이퍼 row에 수동 매핑
21
+ # secom_row: fail 라벨 row 인덱스, wafers: 데모 컨텍스트상 영향 웨이퍼 수
22
+ ALARM_WAFER = {
23
+ "A1": {"secom_row": 2, "wafers": 25},
24
+ }
25
+
26
+
27
+ @lru_cache(maxsize=1)
28
+ def _fit_model():
29
+ """SECOM 전체로 전처리기와 IsolationForest를 학습, 첫 호출 시 1회만"""
30
+ X, _ = load_secom()
31
+ pre = SecomPreprocessor().fit(X)
32
+ Xz = pre.transform(X)
33
+ model = IsolationForest(n_estimators=200, random_state=RANDOM_STATE)
34
+ model.fit(Xz)
35
+ train_scores = -model.score_samples(Xz)
36
+ return X, pre, model, train_scores
37
 
38
 
39
  def run_detection(alarm: dict) -> Tier1:
40
+ mapping = ALARM_WAFER.get(alarm["id"])
41
+ if mapping is None:
42
+ raise ValueError(f"SECOM 매핑이 없는 알람: {alarm['id']}")
43
+
44
+ X, pre, model, train_scores = _fit_model()
45
+ Xz = pre.transform(X.iloc[[mapping["secom_row"]]])
46
+
47
+ raw_score = float(-model.score_samples(Xz)[0])
48
+ # 학습 분포 대비 백분위, 높을수록 이상
49
+ score = float((train_scores < raw_score).mean())
50
+
51
+ # 표준화 값의 절대크기 = 정상 분포에서 벗어난 정도, Top-N
52
+ deviations = np.abs(Xz[0])
53
+ top_idx = np.argsort(deviations)[::-1][:TOP_N_FEATURES]
54
+ features = [
55
+ {"name": pre.keep_cols[i], "value": round(float(deviations[i]), 2)}
56
+ for i in top_idx
57
+ ]
58
+
59
+ return {
60
+ "score": round(score, 2),
61
+ "features": features,
62
+ "lot": {"id": alarm["lot_id"], "wafers": mapping["wafers"]},
63
+ }
data/secom/preprocess.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SECOM 전처리
2
+
3
+ 이상 탐지 모델에 넣기 전 공통 전처리
4
+ - 전부 결측이거나 분산이 0(상수)인 컬럼 제거
5
+ - 남은 결측치는 중앙값으로 임퓨테이션
6
+ - StandardScaler로 스케일링
7
+
8
+ agents/detection.py와 experiments/ 양쪽에서 공용으로 사용
9
+ fit은 train 데이터에만, transform은 train/test 공통으로 적용
10
+ """
11
+ import numpy as np
12
+ import pandas as pd
13
+ from sklearn.impute import SimpleImputer
14
+ from sklearn.preprocessing import StandardScaler
15
+
16
+
17
+ class SecomPreprocessor:
18
+ """SECOM 센서 데이터 전처리, sklearn 스타일 fit/transform"""
19
+
20
+ def __init__(self, var_threshold: float = 0.0):
21
+ self.var_threshold = var_threshold
22
+ self.keep_cols: list[str] = []
23
+ self.imputer = SimpleImputer(strategy="median")
24
+ self.scaler = StandardScaler()
25
+
26
+ def fit(self, X: pd.DataFrame) -> "SecomPreprocessor":
27
+ # 전부 결측인 컬럼 제거 후, 분산이 임계 이하(거의 상수)인 컬럼도 제거
28
+ non_empty = X.columns[X.notna().any()]
29
+ variances = X[non_empty].var()
30
+ self.keep_cols = list(variances[variances > self.var_threshold].index)
31
+
32
+ kept = X[self.keep_cols]
33
+ self.imputer.fit(kept)
34
+ self.scaler.fit(self.imputer.transform(kept))
35
+ return self
36
+
37
+ def transform(self, X: pd.DataFrame) -> np.ndarray:
38
+ kept = X[self.keep_cols]
39
+ return self.scaler.transform(self.imputer.transform(kept))
40
+
41
+ def fit_transform(self, X: pd.DataFrame) -> np.ndarray:
42
+ return self.fit(X).transform(X)
experiments/__init__.py ADDED
File without changes
experiments/tier1_detection/__init__.py ADDED
File without changes
experiments/tier1_detection/benchmark.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tier 1 이상 탐지 모델 벤치마크
2
+
3
+ SECOM pass/fail 라벨을 정답으로 보고 비지도 이상 탐지 모델을 비교
4
+ - IsolationForest
5
+ - LocalOutlierFactor
6
+ - OneClassSVM
7
+ - baseline: 표준화 피처 절대값 평균 (단순 통계)
8
+
9
+ 평가: ROC-AUC, PR-AUC (fail이 6.6%뿐인 불균형 데이터라 PR-AUC를 주지표로 봄)
10
+ train에만 fit, test에서 평가 (70/30 stratified split)
11
+
12
+ 실행: python -m experiments.tier1_detection.benchmark
13
+ 결과: results.md 표 + plots/ 그래프
14
+ """
15
+ from pathlib import Path
16
+
17
+ import matplotlib
18
+
19
+ matplotlib.use("Agg")
20
+ import matplotlib.pyplot as plt
21
+ import numpy as np
22
+ from sklearn.ensemble import IsolationForest
23
+ from sklearn.metrics import (
24
+ average_precision_score,
25
+ precision_recall_curve,
26
+ roc_auc_score,
27
+ roc_curve,
28
+ )
29
+ from sklearn.model_selection import train_test_split
30
+ from sklearn.neighbors import LocalOutlierFactor
31
+ from sklearn.svm import OneClassSVM
32
+
33
+ from data.secom.loader import load_secom
34
+ from data.secom.preprocess import SecomPreprocessor
35
+
36
+ RANDOM_STATE = 42
37
+ OUT_DIR = Path(__file__).parent
38
+ PLOTS_DIR = OUT_DIR / "plots"
39
+
40
+ MODELS = {
41
+ "IsolationForest": IsolationForest(n_estimators=200, random_state=RANDOM_STATE),
42
+ "LocalOutlierFactor": LocalOutlierFactor(n_neighbors=20, novelty=True),
43
+ "OneClassSVM": OneClassSVM(nu=0.1, kernel="rbf", gamma="scale"),
44
+ "baseline": None,
45
+ }
46
+
47
+
48
+ def anomaly_scores(name, model, X_train, X_test):
49
+ """모델별 이상 점수 반환, 높을수록 이상"""
50
+ if name == "baseline":
51
+ # 표준화된 피처의 절대값 평균, 정상에서 멀수록 큼
52
+ return np.abs(X_test).mean(axis=1)
53
+ model.fit(X_train)
54
+ # score_samples는 높을수록 정상이므로 부호 반전
55
+ return -model.score_samples(X_test)
56
+
57
+
58
+ def main():
59
+ X, y = load_secom()
60
+ y_true = (y == 1).astype(int).to_numpy() # 1 = fail(이상)
61
+
62
+ X_train_df, X_test_df, y_train, y_test = train_test_split(
63
+ X, y_true, test_size=0.3, stratify=y_true, random_state=RANDOM_STATE
64
+ )
65
+
66
+ pre = SecomPreprocessor().fit(X_train_df)
67
+ X_train = pre.transform(X_train_df)
68
+ X_test = pre.transform(X_test_df)
69
+
70
+ PLOTS_DIR.mkdir(exist_ok=True)
71
+ fig_roc, ax_roc = plt.subplots(figsize=(6, 5))
72
+ fig_pr, ax_pr = plt.subplots(figsize=(6, 5))
73
+
74
+ results = []
75
+ for name, model in MODELS.items():
76
+ scores = anomaly_scores(name, model, X_train, X_test)
77
+ roc = roc_auc_score(y_test, scores)
78
+ ap = average_precision_score(y_test, scores)
79
+ results.append((name, roc, ap))
80
+
81
+ fpr, tpr, _ = roc_curve(y_test, scores)
82
+ ax_roc.plot(fpr, tpr, label=f"{name} (AUC={roc:.3f})")
83
+ prec, rec, _ = precision_recall_curve(y_test, scores)
84
+ ax_pr.plot(rec, prec, label=f"{name} (AP={ap:.3f})")
85
+
86
+ # 참조선, plot 텍스트는 폰트 의존 피하려고 영문 사용
87
+ ax_roc.plot([0, 1], [0, 1], "k--", alpha=0.4, label="random")
88
+ ax_roc.set_xlabel("False Positive Rate")
89
+ ax_roc.set_ylabel("True Positive Rate")
90
+ ax_roc.set_title("Tier 1 Anomaly Detection - ROC")
91
+ ax_roc.legend(loc="lower right", fontsize=8)
92
+
93
+ ax_pr.axhline(y_test.mean(), color="k", ls="--", alpha=0.4, label="random")
94
+ ax_pr.set_xlabel("Recall")
95
+ ax_pr.set_ylabel("Precision")
96
+ ax_pr.set_title("Tier 1 Anomaly Detection - Precision-Recall")
97
+ ax_pr.legend(loc="upper right", fontsize=8)
98
+
99
+ fig_roc.tight_layout()
100
+ fig_roc.savefig(PLOTS_DIR / "roc.png", dpi=120)
101
+ fig_pr.tight_layout()
102
+ fig_pr.savefig(PLOTS_DIR / "pr.png", dpi=120)
103
+
104
+ write_results(results, len(X_train_df), len(X_test_df), int(y_test.sum()))
105
+ for name, roc, ap in sorted(results, key=lambda r: -r[2]):
106
+ print(f"{name:24s} ROC-AUC={roc:.3f} PR-AUC={ap:.3f}")
107
+
108
+
109
+ def write_results(results, n_train, n_test, n_test_fail):
110
+ """results.md에 평가 표 기록, PR-AUC 내림차순 정렬"""
111
+ ranked = sorted(results, key=lambda r: -r[2])
112
+ lines = [
113
+ "# Tier 1 이상 탐지 - 모델 벤치마크",
114
+ "",
115
+ "SECOM 데이터셋(반도체 제조 공정 센서)으로 비지도 이상 탐지 모델을 비교합니다.",
116
+ "",
117
+ "## 설정",
118
+ "",
119
+ f"- train {n_train} / test {n_test} (70/30 stratified split)",
120
+ f"- test의 fail(이상) 샘플: {n_test_fail}건",
121
+ "- 전처리: 전결측/상수 컬럼 제거 -> 중앙값 임퓨테이션 -> 표준화",
122
+ "- 평가 지표: ROC-AUC, PR-AUC (불균형 데이터라 PR-AUC가 주지표)",
123
+ "",
124
+ "## 결과 (PR-AUC 내림차순)",
125
+ "",
126
+ "| 모델 | ROC-AUC | PR-AUC |",
127
+ "|---|---|---|",
128
+ ]
129
+ for name, roc, ap in ranked:
130
+ lines.append(f"| {name} | {roc:.3f} | {ap:.3f} |")
131
+ lines += [
132
+ "",
133
+ "![ROC](plots/roc.png)",
134
+ "",
135
+ "![PR](plots/pr.png)",
136
+ "",
137
+ f"## 채택",
138
+ "",
139
+ f"PR-AUC 기준 최고 모델은 **{ranked[0][0]}** "
140
+ f"(PR-AUC {ranked[0][2]:.3f}), agents/detection.py의 baseline으로 사용합니다.",
141
+ "",
142
+ ]
143
+ (OUT_DIR / "results.md").write_text("\n".join(lines), encoding="utf-8")
144
+
145
+
146
+ if __name__ == "__main__":
147
+ main()
experiments/tier1_detection/plots/pr.png ADDED
experiments/tier1_detection/plots/roc.png ADDED
experiments/tier1_detection/results.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tier 1 이상 탐지 - 모델 벤치마크
2
+
3
+ SECOM 데이터셋(반도체 제조 공정 센서)으로 비지도 이상 탐지 모델을 비교합니다.
4
+
5
+ ## 설정
6
+
7
+ - train 1096 / test 471 (70/30 stratified split)
8
+ - test의 fail(이상) 샘플: 31건
9
+ - 전처리: 전결측/상수 컬럼 제거 -> 중앙값 임퓨테이션 -> 표준화
10
+ - 평가 지표: ROC-AUC, PR-AUC (불균형 데이터라 PR-AUC가 주지표)
11
+
12
+ ## 결과 (PR-AUC 내림차순)
13
+
14
+ | 모델 | ROC-AUC | PR-AUC |
15
+ |---|---|---|
16
+ | IsolationForest | 0.600 | 0.129 |
17
+ | baseline | 0.565 | 0.119 |
18
+ | OneClassSVM | 0.563 | 0.098 |
19
+ | LocalOutlierFactor | 0.544 | 0.089 |
20
+
21
+ ![ROC](plots/roc.png)
22
+
23
+ ![PR](plots/pr.png)
24
+
25
+ ## 채택
26
+
27
+ PR-AUC 기준 최고 모델은 **IsolationForest** (PR-AUC 0.129), agents/detection.py의 baseline으로 사용합니다.
requirements.txt CHANGED
@@ -2,3 +2,5 @@ streamlit>=1.36.0
2
  openai>=1.0.0
3
  python-dotenv>=1.0.0
4
  pandas>=2.0.0
 
 
 
2
  openai>=1.0.0
3
  python-dotenv>=1.0.0
4
  pandas>=2.0.0
5
+ scikit-learn>=1.4.0
6
+ matplotlib>=3.8.0