3. Purification — 트리 모형의 사후 정제¶
작성 중
이 페이지는 현재 작성 중입니다.
왜 Purification이 필요한가¶
2절에서 fANOVA의 직교 제약(Zero Means, Orthogonality, Integrate-to-Zero)이 분해를 유일하게 결정한다는 것을 확인했다. 문제는, XGBoost 같은 트리 기반 모형이 학습 과정에서 이 제약을 전혀 고려하지 않는다는 것이다. 트리는 단지 예측 오차를 줄이는 방향으로 분할할 뿐이고, "이 효과가 main에 담겨야 하는가, interaction에 담겨야 하는가"를 구분하지 않는다. (EBM은 학습 파이프라인에서 분리를 시도하지만, 그것만으로는 완전하지 않다 — 뒤에서 자세히 다룬다.)
그 결과, 학습된 shape function에서 main effect와 교호작용이 뒤섞인다. 본래 \(f_1(X_1)\)에 있어야 할 효과가 \(f_{12}(X_1, X_2)\)에 누출되거나, 반대로 교호작용의 일부가 main effect에 흡수된다.
1절에서 확인한 것처럼, 이 누출은 예측값을 전혀 바꾸지 않는다. 예측 성능은 동일하지만 해석이 완전히 달라진다. 이것을 사후적으로(post-hoc) 바로잡아서 fANOVA의 직교 제약을 만족시키는 것이 purification(정제)이다.
이 절에서는 purification이 구체적으로 무엇을 하는지 숫자 예시로 먼저 확인한 뒤, 그것을 일반화한 Mass-Moving 알고리즘을 소개한다.
핵심 아이디어 — 숫자로 먼저 이해하기¶
알고리즘을 보기 전에, 신용평가에서 가져온 숫자 예시로 직관을 잡자. 결론부터 말하면, main effect가 전부 0이고 교호작용이 지배적인 것처럼 보이는 모형이, purification 후에는 main effect가 핵심이고 교호작용은 미미한 것으로 바뀐다.
설정¶
DTI(부채비율)와 소득, 두 변수가 각각 2구간(Low, High)인 XGBoost depth-2 모형이다. 학습된 결과가 다음과 같다고 하자:
| component | 값 |
|---|---|
| \(f_0\) (intercept) | 0 |
| \(f_{\text{DTI}}(\text{Low})\) | 0 |
| \(f_{\text{DTI}}(\text{High})\) | 0 |
| \(f_{\text{소득}}(\text{Low})\) | 0 |
| \(f_{\text{소득}}(\text{High})\) | 0 |
| \(f_{12}\) 교호작용 | 소득=Low | 소득=High |
|---|---|---|
| DTI=Low | +0.1 | -0.7 |
| DTI=High | +0.5 | +0.1 |
main effect가 전부 0이고, 모든 효과가 교호작용에 담겨 있다. 이 모형의 해석은 "DTI도 소득도 개별 효과는 없고, 오직 두 변수의 조합만 중요하다"가 된다.
정말 그런가? 교호작용 테이블을 자세히 보면, 뭔가 이상하다. 소득=Low인 열의 값들(+0.1, +0.5)이 소득=High인 열의 값들(-0.7, +0.1)보다 전반적으로 높다. 이것은 "소득이 낮으면 위험하다"는 패턴인데, 이 패턴은 소득 구간에만 의존하고 DTI와 무관하다. 즉, 소득의 main effect가 교호작용에 누출되어 있다는 신호다. 2절에서 다룬 Integrate-to-Zero 조건으로 말하면, 소득 방향의 열 평균이 0이 아니라는 것이다.
Purification 실행¶
Purification은 이 누출을 찾아서 원래 있어야 할 곳으로 되돌린다. 2절의 Integrate-to-Zero 조건이 "교호작용 테이블의 모든 행/열 평균이 0이어야 한다"는 것이었으므로, 행/열 평균이 0이 될 때까지 평균값을 main effect로 옮기면 된다.
1단계 — 소득의 누출을 찾아서 되돌린다
교호작용 테이블에서 각 소득 구간별로, DTI 방향의 가중평균을 구한다 (균등 가중):
이 평균값(+0.3, -0.3)은 소득 구간에만 의존하고 DTI와 무관하다. 따라서 교호작용이 아니라 소득의 main effect다. 이것을 교호작용에서 빼고, main effect로 옮긴다:
\(f_{\text{소득}}\) 업데이트:
| 이전 | 이동량 | 이후 | |
|---|---|---|---|
| 소득=Low | 0 | +0.3 | +0.3 |
| 소득=High | 0 | -0.3 | -0.3 |
\(f_{12}\) 업데이트 (각 열에서 해당 평균을 뺀다):
| \(f_{12}\) | 소득=Low | 소득=High |
|---|---|---|
| DTI=Low | \(0.1 - 0.3 = \mathbf{-0.2}\) | \(-0.7 - (-0.3) = \mathbf{-0.4}\) |
| DTI=High | \(0.5 - 0.3 = \mathbf{+0.2}\) | \(0.1 - (-0.3) = \mathbf{+0.4}\) |
검산 — 소득 방향의 누출이 제거되었는지 확인:
- 소득=Low 열의 평균: \((-0.2 + 0.2)/2 = \mathbf{0}\) ✓
- 소득=High 열의 평균: \((-0.4 + 0.4)/2 = \mathbf{0}\) ✓
소득 방향은 깨끗해졌다. 그런데 아직 DTI 방향(행 평균)이 남아 있다.
2단계 — DTI의 누출을 찾아서 되돌린다
이번에는 각 DTI 구간별로, 소득 방향의 가중평균을 구한다:
이것은 DTI 구간에만 의존하는 효과 → DTI의 main effect다. 교호작용에서 빼고, main effect로 옮긴다:
\(f_{\text{DTI}}\) 업데이트:
| 이전 | 이동량 | 이후 | |
|---|---|---|---|
| DTI=Low | 0 | -0.3 | -0.3 |
| DTI=High | 0 | +0.3 | +0.3 |
\(f_{12}\) 업데이트 (각 행에서 해당 평균을 뺀다):
| \(f_{12}\) | 소득=Low | 소득=High |
|---|---|---|
| DTI=Low | \(-0.2 - (-0.3) = \mathbf{+0.1}\) | \(-0.4 - (-0.3) = \mathbf{-0.1}\) |
| DTI=High | \(0.2 - 0.3 = \mathbf{-0.1}\) | \(0.4 - 0.3 = \mathbf{+0.1}\) |
검산 — 모든 행/열 평균이 0인지 확인:
- DTI=Low 행의 평균: \((0.1 + (-0.1))/2 = 0\) ✓
- DTI=High 행의 평균: \((-0.1 + 0.1)/2 = 0\) ✓
- 소득=Low 열의 평균: \((0.1 + (-0.1))/2 = 0\) ✓
- 소득=High 열의 평균: \((-0.1 + 0.1)/2 = 0\) ✓
모든 행/열 평균이 0이다. Integrate-to-Zero 조건을 만족하므로, 수렴 완료.
결과 비교¶
Purification 전:
| component | 값 | 해석 |
|---|---|---|
| \(f_{\text{DTI}}\) | Low=0, High=0 | "DTI는 개별 효과 없음" |
| \(f_{\text{소득}}\) | Low=0, High=0 | "소득도 개별 효과 없음" |
| \(f_{12}\) | -0.7 ~ +0.5 (넓은 범위) | "교호작용이 지배적" |
Purification 후:
| component | 값 | 해석 |
|---|---|---|
| \(f_{\text{DTI}}\) | Low=-0.3, High=+0.3 | "DTI 높으면 위험 ↑" |
| \(f_{\text{소득}}\) | Low=+0.3, High=-0.3 | "소득 높으면 위험 ↓" |
| \(f_{12}\) | ±0.1 (작은 범위) | "순수 교호작용은 미미" |
같은 모형, 같은 예측값인데 해석이 완전히 달라졌다. 교호작용이 지배적이었던 모형이 실은 main effect가 핵심이었고, 교호작용은 미미했다. 이것이 purification의 효과다.
예측값이 보존되는지 검산¶
Purification은 효과의 귀속(attribution)만 바꾸고, 예측은 건드리지 않는다. 4개 입력 모두에서 확인하자:
| 입력 | Purification 전 | Purification 후 |
|---|---|---|
| DTI=Low, 소득=Low | \(0 + 0 + 0 + 0.1 = \mathbf{0.1}\) | \(0 + (-0.3) + 0.3 + 0.1 = \mathbf{0.1}\) ✓ |
| DTI=Low, 소득=High | \(0 + 0 + 0 + (-0.7) = \mathbf{-0.7}\) | \(0 + (-0.3) + (-0.3) + (-0.1) = \mathbf{-0.7}\) ✓ |
| DTI=High, 소득=Low | \(0 + 0 + 0 + 0.5 = \mathbf{0.5}\) | \(0 + 0.3 + 0.3 + (-0.1) = \mathbf{0.5}\) ✓ |
| DTI=High, 소득=High | \(0 + 0 + 0 + 0.1 = \mathbf{0.1}\) | \(0 + 0.3 + (-0.3) + 0.1 = \mathbf{0.1}\) ✓ |
4개 입력 모두 예측값이 정확히 일치한다.
Mass-Moving 알고리즘 (Lengerich et al., 2020)¶
위 예시에서 한 것을 일반화한 것이 Mass-Moving 알고리즘이다. piecewise-constant 함수(트리 모형의 출력)에 대해 적용 가능하다.
단일 텐서 정제 (Purify-Matrix)¶
위 예시의 1단계와 2단계를 다시 보면, 패턴이 보인다: (1) 한 방향으로 가중평균을 구하고, (2) 그 평균을 교호작용에서 빼고, (3) main effect에 더한다. 이것을 모든 변수에 대해 반복하면 된다. 하나의 교호작용 텐서 \(T_u\)를 정제하는 절차를 일반화하면:
입력: 교호작용 텐서 T_u, 가중치 w
반복 (모든 slice 가중평균이 0이 될 때까지):
for each 변수 i ∈ u:
for each (나머지 변수 고정값) x_{u\i}:
① m ← 변수 i 방향으로 slice 가중평균 계산
② T_u 에서 m을 뺀다 ← 고차항에서 빼기
③ T_{u\i} 에 m을 더한다 ← 저차항에 더하기
위 예시에서 ①②③이 정확히 어떻게 대응되는지 되짚어보자:
| 단계 | 알고리즘 | 예시에서 한 것 |
|---|---|---|
| ① | slice 가중평균 계산 | "소득=Low 열의 DTI 방향 평균 = +0.3" |
| ② | 고차항에서 빼기 | "소득=Low 열의 모든 값에서 0.3을 뺀다" |
| ③ | 저차항에 더하기 | "\(f_{\text{소득}}(\text{Low})\)에 0.3을 더한다" |
전체 모형 정제 (Purify)¶
실제 모형에는 2차뿐 아니라 3차 이상의 교호작용도 있을 수 있다(depth-3 이상의 트리). 이때는 가장 고차부터 내려온다:
Purify (전체 모형):
3차 interaction → 2차 interaction + main effect로 mass 이동
2차 interaction → main effect로 mass 이동
main effect → intercept(f₀)로 mass 이동
각 단계에서 "이 텐서에 담긴 효과 중, 더 적은 수의 변수로 설명 가능한 부분"을 아래로 내려보낸다. 최종적으로 각 component에는 해당 변수 조합에서만 나타나는 순수한 효과만 남는다.
수렴 보장¶
위 예시에서는 2단계(소득 → DTI 순으로 한 번씩)만에 모든 행/열 평균이 0이 되었다. 항상 이렇게 빨리 끝나는 것인가?
Theorem 1: 균등 가중이면 1회 반복¶
모든 bin의 가중치(샘플 비율)가 균등하면, 각 변수를 한 번씩만 순회하면 수렴한다.
위 예시가 정확히 이 경우다. DTI의 Low/High가 각각 50%, 소득의 Low/High도 각각 50%의 균등 가중 → 변수 2개를 한 바퀴 돌면 끝이었다.
Theorem 2: 일반 가중이면 \(O(\log(1/\varepsilon))\) 반복¶
실제 데이터에서는 구간별 샘플 수가 다르다. 예를 들어 DTI=Low에 80%, DTI=High에 20%의 샘플이 몰려 있을 수 있다. 이 경우 한 번의 순회로는 행/열 평균이 정확히 0이 되지 않는다 — 한 방향을 정리하면 다른 방향이 살짝 틀어지기 때문이다.
그러나 매 반복마다 정제되지 않은 잔여 mass가 최소 절반으로 줄어든다:
따라서:
- 5회 반복 → 잔여 mass가 원래의 \(1/2^5 \approx 3\%\) 이하
- 10회 반복 → 잔여 mass가 원래의 \(1/2^{10} \approx 0.1\%\) 이하
실무적으로 5~10회 반복이면 충분하다.
비균등 가중에서의 수렴 과정 — 2×2 예시 (클릭하여 펼치기)
본문의 DTI/소득 예시는 균등 가중이라 2단계만에 수렴했다. 비균등 가중(데이터가 특정 조합에 몰린 경우)에서는 어떻게 반복 수렴하는지, 스텝별로 추적하자.
설정¶
Interaction tensor (purify 대상 — 효과 크기):
Cell weight (데이터 비율 — 대각선에 몰린 비균등 가중):
100명 기준이면: \((x_1\!=\!0, x_2\!=\!0)\)에 30명, \((0,1)\)에 20명, \((1,0)\)에 20명, \((1,1)\)에 30명. 두 변수가 양의 상관관계다.
Main effects (초기): \(f_1 = [0,\; 0]\), \(f_2 = [0,\; 0]\)
균등 가중이면 행 centering 후 열 centering을 해도 행의 zero-mean이 깨지지 않아 1회에 수렴한다 (Theorem 1). 비균등 가중에서는 한 축을 centering하면 다른 축의 zero-mean이 살짝 깨진다. 다시 고치면 또 깨지지만, 깨지는 양이 매번 급격히 줄어들어 수렴한다 (Theorem 2).
Iteration 1¶
Step 1A — 행(Row) centering
각 행의 가중평균을 계산해서 \(f_1\)으로 이동한다:
각 행에서 가중평균을 빼면:
검산:
- 행0 가중평균: \((0.3 \times 2.4 + 0.2 \times (-3.6)) / 0.5 = 0\) ✓
- 행1 가중평균: \((0.2 \times (-2.4) + 0.3 \times 1.6) / 0.5 = 0\) ✓
- 열0 가중평균: \((0.3 \times 2.4 + 0.2 \times (-2.4)) / 0.5 = \mathbf{+0.48}\) ✗
- 열1 가중평균: \((0.2 \times (-3.6) + 0.3 \times 1.6) / 0.5 = \mathbf{-0.48}\) ✗
→ 행은 깨끗하지만, 열은 아직 zero-mean이 아니다.
Step 1B — 열(Column) centering
검산:
- 열0, 열1 가중평균 = 0 ✓
- 행0 가중평균: \((0.3 \times 1.92 + 0.2 \times (-3.12)) / 0.5 = \mathbf{-0.096}\) ✗
→ 열 centering이 행의 zero-mean을 깨뜨렸다! 비균등 가중이기 때문이다.
Iteration 2¶
Step 2A — 행 centering
- 행 가중평균 = 0 ✓
- 열0 가중평균 = +0.0192 ✗ (남아있지만 이전보다 훨씬 작다)
Step 2B — 열 centering
- 열 가중평균 = 0 ✓
- 행0 가중평균 = -0.00384 ✗ (더 작아짐)
Iteration 3¶
같은 과정을 반복하면:
행/열 가중평균 ≈ 0. 수렴 완료.
잔차 수렴 추적¶
| 반복 | 단계 | 잔여 행 평균 | 잔여 열 평균 | 축소율 |
|---|---|---|---|---|
| 1 | A (행) | 0 | ±0.48 | — |
| 1 | B (열) | ±0.096 | 0 | ×0.2 |
| 2 | A (행) | 0 | ±0.0192 | ×0.2 |
| 2 | B (열) | ±0.00384 | 0 | ×0.2 |
| 3 | A (행) | 0 | ±0.000768 | ×0.2 |
| 3 | B (열) | ≈0 | 0 | ×0.2 |
매 half-step마다 잔차가 ×0.2로 줄어든다. 3회 반복(6 half-steps) 후 잔차 < 0.001. 이 축소율은 가중치의 "비균등 정도"에 따라 달라지지만, Theorem 2에 의해 최악의 경우에도 매 반복 최소 절반으로 줄어든다.
예측값 보존 검증¶
| \((x_1, x_2)\) | Before | After: \(f_1 + f_2 + T_{12}^*\) | 일치 |
|---|---|---|---|
| (0, 0) | 6 | 3.5 + 0.5 + 2.0 = 6.0 | ✓ |
| (0, 1) | 0 | 3.5 + (−0.5) + (−3.0) = 0.0 | ✓ |
| (1, 0) | 0 | 2.5 + 0.5 + (−3.0) = 0.0 | ✓ |
| (1, 1) | 4 | 2.5 + (−0.5) + 2.0 = 4.0 | ✓ |
균등 가중과의 비교¶
같은 tensor에 균등 가중 \(w_{i,j} = 0.25\)를 적용하면:
- Step 1A (행 centering): \(\bar{r}_0 = 3,\; \bar{r}_1 = 2\)
- Step 1B (열 centering): \(\bar{c}_0 = 0.5,\; \bar{c}_1 = -0.5\)
행/열 가중평균 모두 0 → 1회 반복만에 수렴. 이것이 Theorem 1이다.
| 균등 가중 (Theorem 1) | 비균등 가중 (Theorem 2) | |
|---|---|---|
| 수렴 | 1회 반복 | \(O(\log(1/\varepsilon))\) 반복 |
| 원리 | 한 축 centering이 다른 축을 안 깨뜨림 | 한 축 centering이 다른 축을 깨뜨리지만, 깨지는 양이 급감 |
| 실무 | Uniform 분포 가정 시 | 실제 데이터 분포 사용 시 (일반적) |
구현
이 알고리즘은 Python 100줄 이내로 구현 가능하며, InterpretML에 내장되어 있다. GAM Purification 별도 라이브러리도 있다.
Purification이 실제로 무엇을 바꾸는가 — 실증 사례¶
위의 숫자 예시는 원리를 보여주기 위한 것이었다. 실제 데이터에서도 이 정도의 차이가 발생할까? Lengerich et al. (2020)은 두 가지 사례에서 purification 전후로 main effect의 부호가 뒤바뀌거나, 존재하지 않는 패턴이 나타나는 현상을 보고했다. 핵심은, 같은 모형을 purification 없이 해석하면 정반대의 결론에 도달할 수 있다는 것이다.
사례 1: California Housing — 바다 위가 가장 비싼 지역? (클릭하여 펼치기)
캘리포니아 주택 가격 데이터셋(scikit-learn 내장)으로 XGBoost depth-2 모형을 학습한다. 변수 중에 위도(\(X_1\))와 경도(\(X_2\))가 있다. depth-2 트리는 두 변수의 교호작용을 자동으로 학습한다.
Purification 전 — 무엇이 잘못되었나:
캘리포니아에는 "해안에 가까울수록 비싸다"는 강한 패턴이 있다. 이것은 위도와 경도 각각의 main effect에 해당하는 효과다:
- 경도가 서쪽(해안 쪽)일수록 비싸다 → \(f_{\text{경도}}\)에 담겨야 함
- 위도가 SF·LA 위치일수록 비싸다 → \(f_{\text{위도}}\)에 담겨야 함
그런데 XGBoost는 학습 과정에서 이 구분을 신경 쓰지 않는다. depth-2 트리가 "경도가 서쪽 AND 위도가 SF 근처"를 하나의 leaf에서 같이 학습하면, 해안가 효과가 interaction 항 \(f_{12}\)에 누출된다.
결과: interaction heatmap에서 가장 높은 값이 태평양 바다 위 좌표에 나타난다. 실제 주택이 없는 바다 한가운데가 "가장 비싼 지역"으로 표시되는 것이다 — main effect(해안 = 비쌈)가 interaction에 섞여 들어간 전형적 증상이다.
Purification 후 — 무엇이 바뀌는가:
Purification은 \(f_{12}\)의 각 행/열 가중평균을 계산해서 main effect로 되돌려보낸다. 위 숫자 예시에서 한 것과 정확히 같은 절차다:
- "경도를 고정하고 위도에 대해 평균"을 내면 → 그 평균값은 경도의 main effect → \(f_{\text{경도}}\)로 이동
- "위도를 고정하고 경도에 대해 평균"을 내면 → 그 평균값은 위도의 main effect → \(f_{\text{위도}}\)로 이동
이 과정을 반복하면 \(f_{12}\)에는 두 변수가 동시에 특정 조합일 때만 나타나는 순수한 지역 효과(예: "SF 도심의 특정 블록")만 남고, 해안가 효과는 main effect로 돌아간다. 바다 위의 비정상적 패턴이 사라진다.
사례 2: COMPAS — main effect 부호가 뒤집힌다 (클릭하여 펼치기)
COMPAS는 미국 법원에서 사용하는 재범 위험 예측 시스템이다. Lengerich et al. (2020)은 이 데이터로 XGBoost depth-2를 학습한 뒤 shape function을 확인했다.
무슨 일이 벌어졌나:
특정 변수의 main effect가 purification 전후로 부호가 뒤바뀌었다:
- Purification 전: main effect가 양(+) → "이 특성이 높을수록 재범 위험 증가"
- Purification 후: main effect가 음(-) → "이 특성이 높을수록 재범 위험 감소"
왜 이런 일이 가능한가:
위의 숫자 예시에서 본 것과 같은 원리다. DTI의 main effect가 0이었는데 purification 후 -0.3/+0.3이 된 것처럼, 교호작용에 누출된 main effect의 크기가 원래 main effect보다 클 수 있다. 극단적으로는, 누출된 양이 원래 main effect의 부호를 뒤집을 만큼 커서 최종적으로 반대 부호가 되는 것이다.
왜 위험한가
같은 모형, 같은 예측값인데 purification 여부에 따라 "이 변수는 위험을 높인다" vs "이 변수는 위험을 낮춘다"로 해석이 갈린다. 규제 심사역에게 shape function을 보여줄 때, purification을 안 한 결과를 보여주면 틀린 답을 하게 될 수 있다. 알고리즘 공정성 감사에서도 특정 변수의 영향 방향에 대한 판단이 뒤집힐 수 있다.
모형별 Purification 영향¶
| 모형 | Interaction 처리 | Purification 영향 |
|---|---|---|
| XGB depth-1 (= GAM) | 없음 | 없음 — 교호작용 자체가 구조적으로 불가능 |
| XGB depth-2 이상 | 자동 생성, 분리 설계 없음 | 매우 큼 — main effect 부호까지 변할 수 있음 |
| GA\(^2\)M (EBM) | 2-stage 분리 | 2-stage로 대부분 분리, 잔여 누출은 purification으로 보정 가능 (아래 참조) |
EBM(GA²M)에서의 Purification¶
EBM은 2-stage 학습(round-robin → FAST)으로 main effect와 interaction을 분리하지만, 잔여 누출이 발생할 수 있다. EBM의 학습 구조, 효과 누출 메커니즘, 그리고 interpret.utils.purify()를 통한 보정에 대해서는 EBM (GA²M)에서 상세히 다룬다.
참고문헌¶
| 문헌 | 내용 |
|---|---|
| Lengerich, Tan, Chang, Hooker, Caruana (2020) | Purification 알고리즘 (AISTATS) |