모델보다 데이터가 어려웠다 — 학부생이 제조 AI 프로젝트를 하며 배운 것

모델보다 데이터가 어려웠다 — 학부생이 제조 AI 프로젝트를 하며 배운 것

SECOM 데이터로 불량을 예측하고, 그 판단 근거를 사람이 읽을 수 있는 형태로 연결하기까지.

안녕하세요. AI를 공부하고 있는 학부생입니다. GreenFab 프로젝트에서 반도체 제조 데이터를 활용한 LightGBM 기반 불량 예측과 SHAP 설명을 대시보드로 연결하는 작업을 했습니다.

처음에는 어떤 모델을 선택할지가 가장 중요한 문제라고 생각했습니다. 하지만 작업을 이어갈수록 다른 질문들이 더 오래 남았습니다. 이름을 알 수 없는 변수를 어떻게 해석해야 할까? 불량이 적은 데이터에서 무엇을 잘 맞혀야 할까? 예측 옆에 설명 그래프를 붙이면 사용자가 판단하기에 충분할까?

이 글은 그 질문들을 데이터 → 모델 → 설명 → 서비스의 순서로 정리한 회고입니다. PyTorch 구현을 다루지는 않지만, 모델을 실제 사용 맥락에 연결하려는 분들과 나누고 싶은 경험입니다. 성능 순위나 현장 도입 효과를 주장하기보다 설계의 근거와 한계에 집중했습니다. 아래 코드는 프로젝트 원본을 그대로 옮긴 것이 아니라, 핵심 구조를 설명하기 위해 작성한 예시입니다.

1. 문제 정의: 예측 다음에 무엇을 보여줄 것인가

GreenFab의 출발점은 제조 공정 데이터에서 불량을 예측하는 것이었습니다. 여기에 한 가지 목표를 더했습니다. 예측 결과를 본 사람이 모델의 판단 근거를 살펴볼 수 있게 하자.

불량 위험이 높다는 결과는 검토의 출발점이 될 수 있습니다. 하지만 그 결과만으로 어떤 측정값을 먼저 확인해야 할지는 알기 어렵습니다. 그래서 개별 샘플의 예측과 변수별 기여도를 함께 제공하고, 전체 데이터에서는 모델이 어떤 변수에 주로 의존하는지 볼 수 있는 구성을 생각했습니다.

여기서 ‘근거’의 범위는 분명히 해야 합니다. 모델이 특정 변수에 의존했다는 사실과 그 변수가 실제 불량을 일으켰다는 사실은 다릅니다. GreenFab에서 목표로 한 것은 모델의 판단을 검토할 단서를 제공하는 것입니다. 실제 공정의 원인을 규명하거나 자동으로 조치하는 단계까지 검증한 것은 아닙니다.

공정 측정 데이터
    ↓ 입력 열·타입·결측 상태 확인
LightGBM 불량 예측
    ↓ 같은 모델과 같은 입력에 대한 설명
SHAP: 개별 판단 근거 / 여러 샘플의 기여도 요약
    ↓ 설명의 단위와 한계를 함께 표시
대시보드 → 사람이 검토할 단서

그림 1. GreenFab에서 연결하려 한 데이터·예측·설명의 흐름을 정리한 개념도.

2. SECOM: 1,567×591이라는 숫자 뒤의 문제

SECOM은 반도체 제조 공정의 측정값과 검사 결과로 구성된 공개 데이터입니다. UCI 설명 기준으로 1,567개 샘플과 591개 특성이 있으며, 불량은 104개입니다. 원본 라벨은 정상(pass)이 -1, 불량(fail)이 1이고, 라벨 파일에는 검사 시각도 포함됩니다. 결측값은 NaN으로 표현됩니다. UCI SECOM

결측치는 채우기 전에 이해해야 했다

결측치를 보면 먼저 평균이나 중앙값으로 채우고 싶어집니다. 하지만 제조 데이터에서는 측정 실패인지, 해당 공정에서 측정하지 않은 것인지에 따라 해석이 달라질 수 있습니다. 공개 데이터만으로 그 이유를 확정할 수는 없었습니다.

따라서 결측치를 처리한다는 것과 결측의 의미를 이해한다는 것은 별개의 문제였습니다. 다음과 같은 점검이 먼저 필요합니다.

  • 어떤 열과 샘플에 결측이 집중되어 있는가?
  • 값이 전혀 없거나 사실상 일정한 열이 있는가?
  • 학습 때와 서비스 입력에서 결측 패턴이 달라질 가능성이 있는가?

이때 중앙값 대치나 결측률 기준의 열 제거를 적용한다면, 그 기준은 각 학습 fold에서만 정해야 합니다. 전체 데이터로 기준을 만든 뒤 교차검증하면 검증 데이터의 정보가 전처리에 들어갑니다. 아래 예제는 이 문제를 단순화하기 위해 별도 대치나 열 선택 없이 NaN을 유지합니다.

익명 변수는 설명의 범위를 제한했다

변수의 물리적 의미를 알 수 없다는 점도 어려웠습니다. feature_42가 중요한 변수라는 결과가 나와도 그것이 온도인지, 압력인지, 다른 측정값인지 알 수 없습니다.

그래서 “이 변수가 이번 예측에 크게 기여했다”까지는 말할 수 있어도, “이 공정 조건을 조정하면 불량이 줄어든다”로 넘어갈 수는 없습니다. 설명 가능한 모델을 만드는 일에는 데이터 사전과 도메인 지식도 필요하다는 것을 느꼈습니다.

정확도만 보면 놓치는 것

공개된 클래스 구성으로 계산하면 모든 샘플을 정상으로 분류해도 정확도는 약 93.4%입니다. 이는 GreenFab의 성능이 아니라, 불량이 적은 데이터에서 정확도만 읽을 때 생기는 문제를 보여주는 단순 계산입니다.

이런 문제에서 결과를 보고한다면 적어도 다음 질문에 답해야 합니다.

관점 확인할 내용
불량을 얼마나 발견하는가 불량 클래스 recall, 놓친 불량 수(FN)
경보가 얼마나 유효한가 불량 클래스 precision, 정상 오경보 수(FP)
임계값을 바꾸면 어떻게 되는가 precision–recall 곡선과 운영 가능한 경보량
분할에 따라 결과가 달라지는가 fold별 지표와 불량 샘플 수

불량 누락을 줄이려고 임계값을 낮추면 정상 제품의 추가 검사 부담이 늘 수 있습니다. 어느 쪽이 더 중요한지는 공정 비용과 검토 역량에 따라 달라집니다. 따라서 이 글에서는 확인되지 않은 성능 수치나 ‘최적 임계값’을 제시하지 않습니다.

3. LightGBM을 선택한 이유와 그 선택의 한계

GreenFab에서는 LightGBM을 사용했습니다. 수치형 표 데이터에서 예측 파이프라인을 구성하고, 트리 모델의 예측을 SHAP으로 설명하는 흐름이 프로젝트 목적에 잘 맞았습니다.

결측값을 모델 내부에서 처리할 수 있다는 점도 실용적이었습니다. LightGBM은 기본적으로 NaN을 결측으로 처리합니다. 다만 이 기능이 결측 원인을 밝혀주거나 데이터 품질 문제를 해결해주는 것은 아닙니다. LightGBM 결측값 처리 문서

“특성이 많으니 LightGBM이 무조건 유리하다”는 설명은 정확하지 않습니다. 샘플 수에 비해 변수가 많고 불량 사례가 적으면 트리 모델도 우연한 패턴을 학습할 수 있습니다. LightGBM 공식 문서 역시 leaf-wise 성장에서 과적합을 주의하고, num_leaves, min_data_in_leaf, max_depth 등의 제어를 설명합니다. scikit-learn 인터페이스에서는 min_child_samplesmin_data_in_leaf에 대응합니다. LightGBM 튜닝 가이드

이 선택을 다른 모델보다 우수하다는 결론으로 확대할 수도 없습니다. 그런 주장을 하려면 같은 분할과 평가 기준에서 비교한 실험이 필요합니다. 제게 중요한 배움은 모델 이름만으로 선택을 정당화하기보다, 데이터 특성·구현 비용·검증 방법·설명 방식까지 함께 판단해야 한다는 것이었습니다.

4. 5-fold와 OOF SHAP: 어떤 모델의 설명인가

GreenFab의 설명 구조에서 중요한 부분은 5-fold와 OOF(out-of-fold) SHAP입니다. OOF는 각 샘플에 대해 그 샘플을 학습에 사용하지 않은 모델의 출력을 얻는 방식입니다.

구조는 다음과 같습니다.

  1. 데이터를 다섯 fold로 나눕니다.
  2. 네 fold로 모델을 학습합니다.
  3. 남은 한 fold에 대해 예측과 SHAP 값을 계산합니다.
  4. 이를 다섯 번 반복하고 원래 샘플 순서대로 결과를 모읍니다.
반복 1: [검증] [학습] [학습] [학습] [학습] → 1번 fold 예측·SHAP
반복 2: [학습] [검증] [학습] [학습] [학습] → 2번 fold 예측·SHAP
  ...
반복 5: [학습] [학습] [학습] [학습] [검증] → 5번 fold 예측·SHAP
                                        ↓
                         원래 행 순서로 OOF 결과 결합

그림 2. 한 샘플의 OOF 예측과 SHAP은 동일한 fold 모델에서 계산한다.

이렇게 하면 학습에 포함된 샘플만 설명할 때와 다른 관점에서 모델을 살펴볼 수 있습니다. 다만 OOF라는 이름만으로 누수가 사라지거나 설명의 안정성이 보장되지는 않습니다. 전처리, 특성 선택, 튜닝 역시 검증 경계를 지켜야 합니다.

또 하나 주의할 점은 모아 놓은 OOF SHAP이 다섯 모델의 설명이라는 것입니다. 전체 데이터로 다시 학습한 최종 모델 하나의 설명과 같지 않습니다. fold마다 기준값과 기여도가 달라질 수 있으므로 local 설명에는 해당 fold의 기준값을 사용하고, global 요약과 함께 fold별 중요도 변화도 살펴볼 필요가 있습니다.

무작위 분할과 현장 검증은 다르다

아래 예제는 클래스 비율을 유지하는 StratifiedKFold를 사용합니다. 그러나 미래 생산품을 예측하는 것이 목적이라면 과거로 학습하고 미래로 검증하는 시간 분할이 더 적절할 수 있습니다. 로트나 장비 단위로 관측이 묶여 있다면 그룹 분할도 검토해야 합니다. 무작위 분할 성능을 그대로 미래 공정 성능으로 해석해서는 안 됩니다. scikit-learn 교차검증 가이드

5. Local과 global: 서로 다른 질문에 답하기

SHAP은 개별 예측에 대한 변수별 기여도를 제공하는 방법입니다. 여기서 출발하면 두 수준의 질문을 구분할 수 있습니다. Lundberg & Lee, 2017

Local 설명의 질문은 “이번 샘플에서 어떤 변수가 모델 출력을 높이거나 낮췄는가?”입니다. 예측 하나를 검토할 때 유용합니다.

Global 설명의 질문은 “여러 샘플에 걸쳐 어떤 변수의 기여 크기가 컸는가?”입니다. 예를 들어 변수별 SHAP 절댓값의 평균을 계산할 수 있습니다. 다만 절댓값을 취한 순위만으로는 방향을 알 수 없습니다. 어떤 변수의 평균 절대 기여도가 크다고 해서 그 값이 커질수록 항상 불량 예측이 증가하는 것은 아닙니다.

기여도의 단위를 먼저 확인하기

아래 예제처럼 기본 binary 목적함수의 LightGBM을 model_output="raw"로 설명하면, 기준값과 SHAP 기여도의 합은 확률이 아니라 raw margin, 즉 log-odds에 대응합니다.

해당 fold의 기준값 + 변수별 SHAP 값의 합 = raw margin
불량 예측 확률 = sigmoid(raw margin)

따라서 SHAP 값 +0.3을 “불량 확률을 30%p 높였다”라고 읽으면 안 됩니다. 개별 SHAP 값에 sigmoid를 적용한 뒤 더하는 것도 맞지 않습니다. 설명 방식과 출력 단위를 명시하고 실제 모델 출력과 합이 맞는지 확인해야 합니다. TreeExplainer API

SHAP은 변수 간 의존성을 다루는 방식에 따라서도 달라질 수 있습니다. 상관된 변수의 중요도를 유일한 원인 순위처럼 읽기보다는, 선택한 설명 설정에서 모델이 보인 의존성으로 해석하는 편이 적절합니다.

6. 구조를 재현하기 위한 최소 코드

다음은 UCI에서 받은 secom.datasecom_labels.data를 같은 폴더에 둔 상황의 예시입니다. Python 환경에 NumPy, pandas, scikit-learn, LightGBM, SHAP이 필요합니다. 프로젝트의 확정 하이퍼파라미터나 성능 재현 코드가 아니며, 이 원고 작성 과정에서 학습 실행을 검증한 코드는 아닙니다.

fold 밖에서 학습되는 전처리를 피하고, 각 행의 예측·설명·기준값을 같은 모델에 연결하는 부분에 초점을 맞췄습니다. SHAP 반환 형태가 다른 환경에서는 조용히 잘못된 축을 선택하지 않도록 중단하게 했습니다.

import numpy as np
import pandas as pd
import shap
from lightgbm import LGBMClassifier
from sklearn.model_selection import StratifiedKFold

X = pd.read_csv("secom.data", sep=r"\s+", header=None)
labels = pd.read_csv("secom_labels.data", sep=r"\s+", header=None)
assert len(X) == len(labels)
assert set(labels[0].unique()) == {-1, 1}
X.columns = [f"feature_{j}" for j in range(X.shape[1])]
y = labels[0].eq(1).astype(int).to_numpy()  # fail=1

n, p = X.shape
oof_prob = np.full(n, np.nan)
oof_phi = np.full((n, p), np.nan)
oof_base = np.full(n, np.nan)
fold_id = np.full(n, -1)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

for k, (tr, va) in enumerate(cv.split(X, y)):
    # 설명을 위한 고정값이며, 최적화한 값은 아닙니다.
    model = LGBMClassifier(
        objective="binary", n_estimators=100,
        num_leaves=15, min_child_samples=30,
        random_state=42 + k, verbosity=-1,
    )
    model.fit(X.iloc[tr], y[tr])
    assert list(model.classes_) == [0, 1]
    oof_prob[va] = model.predict_proba(X.iloc[va])[:, 1]

    explainer = shap.TreeExplainer(
        model, model_output="raw",
        feature_perturbation="tree_path_dependent",
    )
    exp = explainer(X.iloc[va])
    phi = np.asarray(exp.values)
    base = np.asarray(exp.base_values)
    if phi.shape != (len(va), p):
        raise ValueError("SHAP 버전과 출력 클래스 축을 확인하세요.")
    if base.ndim == 0:
        base = np.full(len(va), base.item())
    if base.shape != (len(va),):
        raise ValueError("SHAP 기준값의 형태를 확인하세요.")

    raw = model.predict(X.iloc[va], raw_score=True)
    np.testing.assert_allclose(
        base + phi.sum(axis=1), raw, rtol=1e-5, atol=1e-5
    )
    oof_phi[va], oof_base[va], fold_id[va] = phi, base, k

assert np.all(fold_id >= 0)
assert np.isfinite(oof_prob).all() and np.isfinite(oof_phi).all()

# Global: 여러 fold 모델의 OOF 기여 크기 요약
importance = pd.Series(
    np.abs(oof_phi).mean(axis=0), index=X.columns
).sort_values(ascending=False)

# Local: 해당 샘플을 검증한 모델의 기준값을 사용
i = 0
local = shap.Explanation(
    values=oof_phi[i], base_values=oof_base[i],
    data=X.iloc[i].to_numpy(), feature_names=X.columns.tolist(),
)
shap.plots.waterfall(local, max_display=10)

이 예제에는 튜닝이나 early stopping을 넣지 않았습니다. 이를 추가한다면 바깥 검증 fold를 반복해서 최적화에 쓰지 않고, 학습 fold 안에 별도의 검증 절차를 두는 것이 좋습니다. OOF 결과를 보고 특성이나 임계값을 계속 바꾼 뒤 같은 OOF 결과를 최종 성능으로 제시하는 것도 낙관적인 평가로 이어질 수 있습니다.

7. 설명을 서비스로 옮길 때 생기는 질문

노트북에서 그래프를 보는 것과 대시보드에서 판단 근거를 전달하는 것은 달랐습니다. 예측과 설명을 함께 제공하려면 적어도 무엇을 예측했는지, 어떤 기준으로 표시했는지, 기여도를 어느 단위로 읽어야 하는지가 연결되어야 합니다.

GreenFab의 설계 의도를 실제 운영으로 발전시킨다면 다음 항목들을 함께 다루고 싶습니다.

화면에 제공할 정보 함께 명확히 할 내용
불량 예측 확률과 판정 적용한 임계값, 확률 보정 여부
주요 변수와 기여 방향 모델 출력에 대한 기여이며 공정 원인 확정이 아님
입력값과 결측 상태 값이 없는 경우를 정상값처럼 표시하지 않기
모델 정보 예측과 설명이 동일한 모델 버전에 속하는지

특히 OOF 분석 결과를 서비스용 최종 모델의 설명으로 재사용하면 안 됩니다. 전체 데이터로 재학습한 모델을 배포한다면 그 모델로 설명도 다시 계산해야 합니다. 여러 모델의 확률을 평균하는 서비스라면 설명 대상도 그 평균 출력과 맞아야 합니다. raw SHAP을 평균한 뒤 sigmoid를 취하는 것이 각 모델의 확률 평균과 일반적으로 같지는 않습니다.

결국 설명은 그래프 하나보다 넓은 문제였습니다. 예측값, 입력 데이터, 모델 버전, 설명 방식이 서로 맞아야 사용자가 결과를 검토할 수 있습니다.

8. 예상 밖의 난관: 개발 환경도 결과의 일부였다

프로젝트에서는 라이브러리 설치와 버전 호환성도 시행착오였습니다. SHAP을 사용하려고 해도 NumPy·Numba 등 의존성의 조합과 실행 환경에 따라 문제가 생길 수 있었습니다.

이 경험을 특정 버전 하나의 문제나 “모두 최신으로 설치하면 된다”는 교훈으로 정리하기는 어렵습니다. Numba 공식 문서도 릴리스별 Python·NumPy·llvmlite 등의 지원 범위를 따로 안내합니다. Numba 버전 호환성 표

다시 같은 작업을 한다면 환경을 다음 순서로 확인하고 기록하려고 합니다.

  1. 별도 환경에서 라이브러리 import가 되는지 확인합니다.
  2. 작은 입력으로 학습 → 예측 → SHAP 계산까지 실행합니다.
  3. 기준값과 기여도의 합이 모델 출력과 일치하는지 확인합니다.
  4. Python·라이브러리 버전과 운영체제를 기록합니다.
  5. 데이터 버전, 열 순서, 라벨 매핑, 분할 인덱스도 함께 보관합니다.

설치 성공만으로 분석이 재현되는 것은 아닙니다. 같은 데이터와 모델을 사용하더라도 설명의 출력 형태나 설정이 다르면 해석이 달라질 수 있습니다. 환경과 입력 규칙을 함께 남기는 일도 모델 개발의 일부였습니다.

9. 남은 한계와 다음에 검증하고 싶은 것

GreenFab은 공개 데이터에서 불량 예측과 설명을 연결해 본 프로젝트입니다. 여기서 얻은 경험이 곧 현장 유효성을 의미하지는 않습니다.

첫째, 익명 변수만으로는 공정 조치까지 연결하기 어렵습니다. 실제 적용에는 센서 의미와 측정 시점, 공정 맥락을 아는 사람의 검토가 필요합니다.

둘째, 불량 샘플이 적어 분할에 따른 변동을 주의해야 합니다. 평균 지표 하나뿐 아니라 어떤 불량을 놓쳤는지, fold별 결과가 얼마나 달라지는지 보고 싶습니다.

셋째, 설명이 그럴듯해 보인다는 것과 유용하다는 것은 다릅니다. fold에 따라 상위 변수가 얼마나 바뀌는지, 잘못된 예측에서 어떤 기여 패턴이 나타나는지, 사용자에게 실제로 검토 시간을 줄여주는지 따로 확인해야 합니다.

마지막으로, 미래 데이터에 대한 검증이 필요합니다. 시간 분할, 공정 변화에 따른 입력 분포 변화, 운영 비용을 반영한 임계값 검토는 후속 과제입니다. 이 글에서는 이를 완료한 실험이나 확인된 개선 효과로 제시하지 않습니다.

마치며: 모델의 앞과 뒤를 보게 되었다

GreenFab을 하며 가장 크게 달라진 것은 모델을 바라보는 범위였습니다.

데이터를 읽는 단계에서 이미 설명의 한계가 정해졌습니다. 모델을 학습하는 단계에서는 무엇을 검증 데이터로 남길지 결정해야 했습니다. SHAP을 붙인 뒤에는 어떤 출력의 기여도를 보여주는지 확인해야 했고, 서비스로 옮길 때는 사람이 그 결과를 어떻게 읽을지 생각해야 했습니다.

높은 성능은 여전히 중요합니다. 여기에 데이터의 맥락, 검증의 경계, 설명의 의미, 재현 가능한 실행 환경이 함께 갖춰져야 한다는 것이 이번 프로젝트에서 배운 점입니다.

제조 데이터나 설명 가능한 AI를 다뤄보신 분들은 모델의 설명을 실제 검토나 조치로 연결할 때 어떤 정보가 가장 부족했나요? 익명 변수나 적은 불량 샘플을 다룰 때의 경험도 듣고 싶습니다.

참고문헌

  1. McCann, M. & Johnston, A. (2008). SECOM — UCI Machine Learning Repository. DOI: 10.24432/C54305.
  2. Lundberg, S. M. & Lee, S.-I. (2017). A Unified Approach to Interpreting Model Predictions. NeurIPS.
  3. SHAP. TreeExplainer API.
  4. LightGBM. Advanced Topics: Missing Value Handle.
  5. LightGBM. Parameters Tuning.
  6. scikit-learn. Cross-validation: evaluating estimator performance.
  7. Numba. Installation: Version support information.

태그: 제조AI LightGBM SHAP XAI 교차검증 프로젝트회고

1개의 좋아요