LITTLE BY LITTLE

[7] 실무로 통하는 인과추론 with 파이썬 - 7장. 메타 러너 본문

데이터 분석/인과추론

[7] 실무로 통하는 인과추론 with 파이썬 - 7장. 메타 러너

위나 2024. 6. 2. 15:59

목차

<7장 메타러너>

7.1 이산형 처치 메타러너
7.1.1  T 러너
7.1.2  X 러너
7.2 연속형 처치 메타러너
7.2.1 S 러너
7.2.2 이중/편향 제거 머신러닝
7.3 요약


7장. 메타러너

위의 그림의 메타몽이 피카츄, 파이리, 이상해씨가 되듯이 메타 러너의 "meta"는 CATE를 추정하기 위한 base 알고리즘이 랜덤포레스트나 BART (Bayesian Additive Regression Tree)처럼 여러 형태를 가질 수 있음을 의미한다.

 
이질적 처치효과를 식별하기 위해 CATE를 추정했었다. (도함수의 정의 활용)

  • 이는 모든 대상에게 처치할 수 없고, 우선순위를 정해야 하는 경우에 매우 유용하다.
  • 이번 장에서는 몇 가지 머신러닝 알고리즘을 섞어 CATE 추정을 해보자.
  • 머신러닝 알고리즘은 고차원 데이터를 잘 처리하기 때문에 주로 ATE보다는 CATE 추정에 사용된다.
  • 선형회귀, 부스트 의사결정 트리, 신경망, 가우스 과정 등 예측 모델을 인과추론에 맞게 재활용한다.

7.1 이산형 처치 메타러너

  • 이메일(T)고객의 미래 구매량(Y)에 미치는 조건부 평균 처치효과를 추정하려고 할 때,
  • 이 CATE 추정값을 바탕으로 누구에게 메일을 발송하면 효율적일지에 관한 전략을 세울 수 있다.

 

  • mkt_email : 이메일 발송 여부
  • next_mnth_pv : 이메일을 받고 한 달 후의 구매금액(결과변수)
  • 그 외 공변량 : 고객의 나이, 웹사이트에서 첫 구매 이후의 기간(↔고객 유지 기간), 카테고리별 구매 금액

✅ 메타러너 인과추론 라이브러리

  • 대부분의 인과추론 패키지(마이크로소프트의 EconML, 우버의 CausalML) 에 메타러너가 구현되어 있으나, 여기에서는 처음부터 만드는 방법을 알려준다.
  • S-learner, T-learner, X-learner 알고리즘은 CausalML 및 EconML패키지에서,
  • R-learner는 CausalML에서 사용 가능하며, EconML에서는 비모수적 DML CATE Estimator과 동일하다.
    • EconML의DML CATE Estimator는 R-learner의 특별한 인스턴스

7.1.1 T (Two model) 러너

: 잠재적 결과 Yi를 추정하기 위해 모든 처치에 대해 하나의 결과 모델 μ t(x)를 적합시킨다. 추정해야 할 모델이 두 개뿐이므로, T 러너라고 불린다.

러너가 T=1, T=0에서 ml 모델을 학습하고, 예측 시점에 두 모델을 모두 사용해 그 차이를 추정하는 구조

< T 러너 STEPS >
1. μ₀(x) 및 μ₁(x) 추정 (ml 모델 사용)

2. CATE 추정값 정의


T, Y, X, 훈련/테스트 셋을 저장하는 변수 생성

y = "next_mnth_pv"
T = "mkt_email"
X = list(data_rnd.drop(columns=[y, T]).columns)

train, test = data_biased, data_rnd

 
결과 모델로 부스트 회귀 트리(boosted regression tree)인 LGBM Regressor 적용

from lightgbm import LGBMRegressor

np.random.seed(123)

m0 = LGBMRegressor()
m1 = LGBMRegressor()

m0.fit(train.query(f"{T}==0")[X], train.query(f"{T}==0")[y])
m1.fit(train.query(f"{T}==1")[X], train.query(f"{T}==1")[y]);

 
두 개의 모델로 테스트 셋에서 CATE 예측하기

t_learner_cate_test = test.assign(
    cate=m1.predict(test[X]) - m0.predict(test[X])
)

 
6장에서 배운 상대 누적 이득 곡선과 AUC를 사용해서 모델이 처치효과를 올바르게 정렬했는지 평가하기

gain_curve_test = relative_cumulative_gain_curve(t_learner_cate_test, T, y, prediction="cate")
auc = area_under_the_relative_cumulative_gain_curve(t_learner_cate_test, T, y, prediction="cate")

plt.figure(figsize=(10,4))
plt.plot(gain_curve_test, color="C0", label=f"AUC: {auc:.2f}")
plt.hlines(0, 0, 100, linestyle="--", color="black", label="Baseline")

plt.legend();
plt.title("T-Learner")

*fklearn의 relative_cumulative_gain_curve()

 

  • T 러너는 CATE에 따른 고객을 잘 정렬할 수 있는 것으로 보인다. 
    • X축은 누적 단계의 비율
    • Y축은 상대적 누적 이득(=ATE - 누적 효과 * 상대 sample size)을 나타냄
    • AUC는 약 105.52로, 클수록 모델 예측 성능이 좋음

❗T 러너는 단순한 모델이기에, 처치가 이산형으로 주어진 상황일 때 처음 시도하기 좋다. 하지만, 상황에 따라 정규화 편향(regularization bias)이 발생하기 쉽다.
 
만약, 대조군이 아주 많고, 실험군 데이터가 적은 상황이라면,

(이는 처치에 큰 비용이 들어가는 사례에서 매우 흔한 상황이다.)

  • 결과 Y에 약간의 비선형성이 있지만, 처치효과는 일정하다고 가정해보자.
  • 데이터가 이처럼 처치 받은 관측값이 훨씬 적은 경우, 과적합을 피하기 위해 μ1(실험군)의 모델은 단순해질 가능성이 크다. (일자로 예측된 점선)
  • 반대로 μ0(대조군)의 모델은 더 복잡할 수 있지만, 데이터가 풍부하므로 과적합을 방지할 수 있다. (실선) 

자기 정규화(self-normalization)
: 모델이 스스로 복잡성을 조절하여 과적합을 방지하는 과정

  • 트리 기반 모델에서는 리프 노드의 최소 샘플 수(min_child_samples)를 높게 설정할 경우, 모델이 데이터의 일반적인 패턴을 학습하도록 유도한다.

① 더 많은 샘플을 포함해야 하므로
② 트리의 깊이가 줄어들고, 더 이상 분할을 진행할 수 없기 때문에 트리의 성장이 자연스럽게 제한되며,
③ 모델이 너무 특정한 데이터 패턴에만 집중하는 것(과적합)을 방지한다. 
 

자기 정규화는 데이터셋의 크기가 작을 때, 과적합을 방지하고 모델의 일반화 능력을 향상시켜줌

(↔모델이 훈련 데이터의 노이즈에 덜 민감하게 만들어줌)

 
두 모델에 동일한 하이퍼파라미터를 사용하더라도, 위와 같은 상황이 발생한다.

  • min_child_sample=25로 설정하여, 이전과 같은 값을 생성하고, 다른 매개변수는 기본값으로 설정
  • 많은 ml 알고리즘은 min_child_samples처럼 데이터가 적을 때에는 자체적으로 정규화한다. 
  • 위의 설정값은 LGBM의 트리가 각 리프 노드에 최소 25개의 표본을 갖도록 강제하므로 표본 크기가 작다면 트리는 더 작아진다.

❗따라서, 데이터가 적으면 더 단순한 모델을 사용해야 한다. 

 

  • 두 모델 각각 표본의 크기에 최적화되어 예측 성능이 상당히 좋지만,
  • 이 모델을 사용하여 CATE(= μ1(X) - μ0(X))를 계산하면, μ0(X)의 비선형성과 μ1(X)의 선형성의 차이 때문에, 비선형CATE(점선에서 실선을 뺀 값)이 된다.
  • 실제 CATE는 1로 일정하기 때문에 잘못된 결과 
두 모델의 선형성 차이 때문에 잘못 계산된 CATE (점선-실선)

 

❗표본이 많은 대조군(T=0) 모델에서 비선형성을 포착할 수 있지만,
실험군(T=1) 모델에서는 작은 표본때문에 자기 정규화되어 비선형성을 포착할 수 없는 것

(그러면 실험군(T=1) 모델에 정규화를 덜 할 수도 있지만, 그러면 과적합의 위험이 있다.)

 
이 문제를 해결하기 위해 X러너가 등장한다.


7.1.2 X(Cross) 러너

: 실험군과 대조군의 표본 크기 차이에서 오는 T러너의 CATE의 부정확한 추정을 보완하는 방법

X러너는 2가지 단계에서 ml모델과 성향점수 모델을 학습하며, 예측 시 2번째 단계의 모델과 성향점수 모델만 활용

 

 
<X 러너 STEPS>

[T러너 - 성향 점수 모델] 2 stage

1. μ₀(x) 및 μ₁(x) 추정 (ml 모델 사용)

: 첫 번째 단계는 T러너와 동일하다. 표본을 실험-대조군으로 나누고 각 그룹에 대한 모델 적합시키기

 

2. μ₀(x)를 기반으로 한 실험의 잠재적 결과 Dᵢ와 μ₁(x)를 기반으로 한 대조군의 잠재적 결과 Dⱼ ⁰를 대체

3. μ₁(x) 및 μ₀(x)의 가중 평균(성향 점수 활용)으로 CATE 추정값 정의하기

 

 

  • 여기에서 매우 작은 표본에 적합된 μ1으로 계산된  τ(X,T=0), 즉 대조군의 처치효과는 잘못되었을 것
  • 반면, 표본이 충분한 모델로 적합된 μ0으로 계산된 τ(X,T=1), 즉 실험군의 처치효과는 아마도 정확할 것
  • 요약하면 하나의 모델은 처치효과를 잘못 대체하여 부정확하고, 다른 하나의 모델은 올바르게 대체했기에 정확하다.

따라서 이 모델을 결합하기 전, 정확한 모델에 더 많은 가중치를 부여하는 과정이 필요
3. μ₁(x) 및 μ₀(x)의 가중 평균으로 CATE 추정값 정의하는 과정에서, 성향점수 모델을 사용할 수 있다. 

  • 위의 예제에서는 실험군(T=1)이 매우 적으므로, 성향점수 e(x)가 매우 작아, 잘못된 CATE 모델 μ(X) τ0에 매우 작은 가중치 부여
  • 반대로 1-e(x)는 1에 가까우므로, 올바른 CATE모델 μ(X) τ1에 더 많은 가중치 부여

❗일반적으로 성향점수를 사용한 이 가중평균더 많은 데이터를 사용하여 학습된 μt 모델에서 얻은 처치효과 추정값에
더 많은 가중치를 준다.

X러너가 잘못 추정된 데이터(&amp;amp;tau;0, 동그라미)를 배제하는 것을 알 수 있다.

 

→ T러너와 달리 X러너는 비선형성에서 잘못 추정된 CATE를 보정하는 데 훨씬 더 나은 성능을 보인다.
일반적으로, 한 실험 대상의 집단이 다른 집단보다 훨씬 클 때, X러너의 성능이 더 좋다.


도메인 적응 러너(domain adaptation learner)
: X러너이지만, 성향 점수 모델을 사용하여 μ1(X)를 추정하되 가중치는 1/P(T=t)로 설정한다.

 
X러너를 코드로 확인해보자.

from sklearn.linear_model import LogisticRegression
from lightgbm import LGBMRegressor

# 성향점수 모델
ps_model = LogisticRegression(penalty='none')
ps_model.fit(train[X], train[T])


# 첫 번째 단계 모델
train_t0 = train.query(f"{T}==0")
train_t1 = train.query(f"{T}==1")

m0 = LGBMRegressor()
m1 = LGBMRegressor()

np.random.seed(123)

m0.fit(train_t0[X], train_t0[y],
       sample_weight=1/ps_model.predict_proba(train_t0[X])[:, 0])

m1.fit(train_t1[X], train_t1[y],
       sample_weight=1/ps_model.predict_proba(train_t1[X])[:, 1]);

 
처치 효과를 예측하고, 예측된 효과에 대해 두 번째 단계 모델을 적합시키기

# 두 번째 단계 모델
tau_hat_0 = m1.predict(train_t0[X]) - train_t0[y]
tau_hat_1 = train_t1[y] - m0.predict(train_t1[X])

m_tau_0 = LGBMRegressor()
m_tau_1 = LGBMRegressor()

np.random.seed(123)

m_tau_0.fit(train_t0[X], tau_hat_0)
m_tau_1.fit(train_t1[X], tau_hat_1);

 
마지막으로 성향 점수 모델으로 두 번째 단계 모델들의 예측 값을 결합하여 CATE 얻기 (test set에서 추정)

# CATE 추정 단계
ps_test = ps_model.predict_proba(test[X])[:, 1]

x_cate_test = test.assign(
    cate=(ps_test*m_tau_0.predict(test[X]) +
          (1-ps_test)*m_tau_1.predict(test[X])
         )
)

 
누적 이득 곡선 관점에서 X러너의 성능 평가하기

gain_curve_test = relative_cumulative_gain_curve(x_cate_test, T, y, prediction="cate")
auc = area_under_the_relative_cumulative_gain_curve(x_cate_test, T, y, prediction="cate")

plt.figure(figsize=(10, 4))
plt.plot(gain_curve_test, color="C0", label=f"AUC: {auc:.2f}")
plt.hlines(0, 0, 100, linestyle="--", color="black", label="Baseline")

plt.legend();
plt.title("X-Learner")

 
여기에서는 실험군-대조군의 크기가 거의 동일하고, 표본 크기가 충분히 크기 때문에, T러너의 성능과 큰 차이가 없는 것을 볼 수 있다. (오히려 곡선 아래 영역에서 약간 성능이 떨어진다.)


7.2 연속형 처치 메타러너

6장에서 사용한 레스토랑 체인의 3년치 데이터를 사용해보자. CATE를 추정하여 레스토랑에서 고객에게 할인을 제공해야 하는 적절한 시기를 파악하고자 하는 상황

 
T = discounts / Y = sales
시간 차원을 활용하여 train/test 분할하기

train = data_cont.query("day<'2018-01-01'")
test = data_cont.query("day>='2018-01-01'")

7.2.1  S(Single model) 러너

: S러너는 가장 기본적인 방식으로, 단순히 특성 중 하나로 처치를 포함하는 단일 ml 모델로 CATE를 추정

 
<S 러너 STEPS>
1. 공변량 X와 처치 T에 대한 표시 변수를 사용하여 μ(x) 추정

2. 실험군과 대조군의 예측값 차이를 CATE 추정값으로 계산

 

X = ["month", "weekday", "is_holiday", "competitors_price"]
T = "discounts"
y = "sales"

np.random.seed(123)
s_learner = LGBMRegressor()
s_learner.fit(train[X+[T]], train[y]);

처치 효과를 직접 출력하지 않고 반사실 예측값을 구하기에, 다양한 처치에서의 예측이 가능하다.
 
처치가 연속형일 때에는 약간의 추가 작업이 필요하다. 먼저 처치의 그리드를 정의해야 함

  • 예제에서는 처치(dicounts)가 0%부터 40%까지 변하므로, [0, 10, 20, 30, 40] 그리드를 사용할 수 있음
  • 예측하고자 하는 데이터를 확장하여 각 행이 그리드의 각 처칫값을 한 번씩 복사
  • 그리드의 값이 있는 데이터프레임을 예측하려는 데이터(test set)에 교차 조인하는 방법
t_grid = pd.DataFrame(dict(key=1,
                           discounts=np.array([0, 10, 20, 30, 40])))

test_cf = (test
           .drop(columns=["discounts"])
           .assign(key=1)
           .merge(t_grid)
           # 확장된 데이터에서 에측하기
           .assign(sales_hat = lambda d: s_learner.predict(d[X+[T]])))

test_cf.head(8)
날짜가 크리스마스인 12-15에 추정된 반응함수가 06-18보다 더 가파른 것으로, 고객이 6월보다 크리스마스 할인에 더 민감함을 모델이 학습했음을 의미

그러나 이러한 반사실 예측이 정확한지는 전혀 다른 문제이다.

  • CATE 예측을 하지 않았으니 6장에서 배운 평가 방법을 사용할 수는 없다.
  • CATE 예측을 얻으려면, 실험 대상 수준의 곡선을 처치효과를 나타내는 단일 숫자로 요약하는 방법이 필요하다.

→ 각 실험 대상에 대해 회귀분석을 하고, 그 결과에서 처치의 기울기 매개변수를 추출하여 CATE 추정값으로 활용해보자.
→ 기울기 매개변수만 신경 쓰면 되므로, 단순선형회귀 계수 공식을 사용하여 이 작업을 훨씬 효율적으로 수행할 수 있다.

 코드로 확인해보자

  1. 개별 곡선을 기울기 매개변수로 요약하는 함수 정의하기
  2. 확장된 test set을 rest_id와 day별로 그룹화하고, 각 그룹에 기울기 함수를 적용
  3. 인덱스가 rest_id와 day인 시리즈가 생기고, 이 시리즈를 원래 test set(expanded말고)에 조인하여 day별 rest_id별 CATE 예측값 얻기
from toolz import curry

@curry
# 기울기 함수 정의
def linear_effect(df, y, t):
    return np.cov(df[y], df[t])[0, 1]/df[t].var()

# rest_id, day별 그룹화하여 기울기 함수 적용
cate = (test_cf
        .groupby(["rest_id", "day"])
        .apply(linear_effect(t="discounts", y="sales_hat"))
        .rename("cate"))

# 원래 test set과 조인
test_s_learner_pred = test.set_index(["rest_id", "day"]).join(cate)

test_s_learner_pred.head()

 
이제 CATE 예측값이 있으니, relative_cumulative_gain_curve() 로 모델 평가하기

from fklearn.causal.validation.auc import area_under_the_relative_cumulative_gain_curve
from fklearn.causal.validation.curves import relative_cumulative_gain_curve

gain_curve_test = relative_cumulative_gain_curve(test_s_learner_pred, T, y, prediction="cate")
auc = area_under_the_relative_cumulative_gain_curve(test_s_learner_pred, T, y, prediction="cate")

plt.figure(figsize=(10, 4))
plt.plot(gain_curve_test, color="C0", label=f"AUC: {auc:.2f}")
plt.hlines(0, 0, 100, linestyle="--", color="black", label="Baseline")

plt.legend();
plt.title("S-Learner")

누적 이득 곡선에서 볼 수 있듯이, S러너는 간단하지만 예제 데이터셋에서 괜찮은 성능을 보여준다.

  • S러너는 랜덤화된 데이터가 많고, 상대적으로 쉬운 데이터셋에 특화된 성능
  • 단순함 때문에 어떤 인과 문제에도 처음 시도하기 좋은 선택
    1. 랜덤화된 데이터가 없더라도 괜찮은 성능을 보이는 경향이 있으며,
    2. 이진 및 연속형 처치 모두에 활용가능하기 때문
  • S 러너의 가장 큰 단점은 처치효과를 0으로 편향시키려는 경향이 있다는 것
  • 일반적으로 정규화 ml모델을 사용하므로 추정된 처치효과를 제한할 수 있다. 

S러너의 한계점 - 처치효과를 0으로 편향시키려는 경향이 존재함

빅터 체르노주코프 논문 결과 재현

  • 20개 공변량과 실제 ATE가 1인 이진 처치로 만든 데이터를 시뮬레이션해서 만들고,
  • ATE를 S러너를 사용해서 추정
  • 추정된 ATE(시뮬레이션)의 분포가 실제 ATE(실선)보다 왼쪽에 편향되어 있어 0으로 향하는 모습을 볼 수 있다.
  • 실제 인과효과는 추정된 효과보다 더 크다.

더 큰 문제점은, T가 다른 X보다 결과를 설명하는 데 영향력이 매우 작다면, S러너는 T를 완전히 버릴 수 있다는 점

  • 이는 선택한 ml모델종류와 밀접한 관련이 있음
  • 그리고 정규화가 클수록 이 문제는 더 커진다.

이러한 ATE 편향 문제를 해결하는 방법은, 체르노주코프등이 제안한 이중/편향 제거 ml 또는 R러너이다.


7.2.2 R(Residual)러너 (이중/편향 제거 머신러닝)

: R러너라고도 불리는 이중/편향 제거 ml은 FWL(프리슈-워-로벨) 정리의 정제된 버전으로 볼 수 있다.

 
*FWL 정리
: 편향제거단계 → 잡음제거단계  → 결과모델단계로 구성되어있으며, 결과 잔차를 처치 잔차에 회귀하여 T가 Y에 미치는 인과효과 추정값을 구함

  • 지금까지 살펴본 T 러너, X 학습기는 이산형 처치에만 가능하다는 단점 존재
  • R러너는 연속형 처치에도 가능하며, 매우 직관적이면서도 엄격하게 적용할 수 있는 일반적인 프레임워크가 있다.

결과와 처치의 잔차를 구성할 때 ml 모델을 사용하는 매우 간단한 방법으로, 

  • μy(Xi)는 E[Y|X]를 추정하고, μt(Xi)는 E[T|X]를 추정
  • ml모델은 FWL 스타일의 직교화를 유지한채로, Y와 T의 잔차를 추정하면서 상호작용과 비선형성을 더 잘 포착할 수 있다.
    • 즉, 정확한 처치효과를 얻으려고 공변량 X와 결과 Y 사이의 관계나 공변량 X와 처치 T 사이의 관계에 관한 모수적 가정을 할 필요가 없다.

관측되지 않은 교란 요인이 없다면, 다음과 같은 직교화 과정으로 ATE를 구할 수 있다.

Y(~) = α+τT(~) 에서 τ인 인과 매개변수 ATE 추정 과정

  1. ml 회귀 모델 μy를 사용하여 특성 X로 결과 Y 추정
  2. ml 회귀 모델 μt를 사용하여 특성 X로 처치 T 추정
  3. 잔차 Y(~) = Y - μy(X)와 T(~) = T - μt(X)를 구함
  4. 결과 잔차 Y(~)를 처치 잔차 T(~)에 회귀한다.

FWL 정리의 한계점을 극복하기 위해 기계학습을 적용
: FWL 정리는 인과 매개변수의 추정 절차와 성가신 매개변수의 추정 절차를 분리할 수 있다는 점에서 매우 훌륭하다.
하지만 여전히, 어떻게 성가신 매개변수에 올바른 함수 형태를 지정해야하는 번거로움을 피할 수 있을지에 대한 질문이 생기고, 여기에 기계학습이 등장

  • 기계학습 모델은 매우 유연하기 때문에, Y와 T의 잔차를 추정할 때 교호작용과 비선형성을 모델링할 수 있고, 동시에 FWL 스타일의 직교화도 유지할 수 있다.
  • X와 T, X와 Y간의 관계에대한 매개변수 가정이 필요 없음
  • 그러나, 기계학습의 유연함은 곧 과적합의 가능성을 고려해야 한다는 것을 의미함 (trade-off)

✅ R러너의 한계점 - 과적합 가능성

과적합에 대한 직관적인 이해

μy 모델이 과적합되면,

  1. 잔차 Y(~)가 실제보다 작아짐
  2. 이는, μy가 단지 X와 Y 사이의 관계만 포착하는 것이 아님을 의미함 (↔모델이 과적합되면 실제관계이외에 잡음이나 불필요한 패턴까지 학습하게된다)   
  3. 같이 포착된 것들중 일부는 T와 Y 사이의 관계도 있을 것이고,
  4. T와 Y 사이의 관계가 이미 μy 모델에 반영되었기 때문에, 결과 잔차 Y(~)를 처치 잔차 T(~)에 회귀했을 때 그 결과는 0에 가까워질것 
  5. 즉, μy가 T와 Y의 인과관계를 포착하면서, 최종 잔차 회귀식에 남지 않는다. 

과적합된 μy 모델은 T의 분산(변동성)을 더 많이 설명함

  • 그 결과, 처치 잔차 T(~)는 본래보다 작은 분산(변동성)을 갖게 됨
  • 처치 분산이 작으면, 최종 추정량의 분산이 커짐
  • 이는 곧 거의 모든 대상에게 처치가 동일하거나, 양수성 가정을 위배한 경우와 같음 (↔ 모든 실험 대상이 동일한 처치를 받는다면, 다른 처치를 받았을 때 어떤 일이 일어날지 추정하기 매우 어려움)

이러한 문제점들은 ml모델 시 겪는 문제들이다. 해결책은 교차 예측(cross prediction)과 아웃 오브 폴드 잔차에 있다.

  1. 데이터를 K(=4)개의 폴드로 분할하고,
  2. 그 중 K-1(=3)개의 폴드에서 모델을 추정한 후, 남겨진 폴드에서 잔차를 얻음(↔ 'Predict' 폴드에서 잔차를 만듦)
  3. 전체 데이터셋에 대한 잔차를 얻으려면, 동일한 과정을 K번 반복 (↔ 모든 K부분에 대한 예측을 결합하여 최종 인과 모델 Y(~) = α+τT(~)를 추정)

이 접근 방식을 통해서 모델이 과적합되더라도, 잔차를 의도적으로 0으로 만들지 않게 된다.
sklearn의 cross_val_predict 함수를 사용해서  ml 모델에서 아웃 오브  폴드 예측값을 구해보자

from sklearn.model_selection import cross_val_predict

X = ["month", "weekday", "is_holiday", "competitors_price"]
T = "discounts"
y = "sales"

debias_m = LGBMRegressor()
denoise_m = LGBMRegressor()

t_res =  train[T] - cross_val_predict(debias_m,train[X],train[T],cv=5)
y_res =  train[y] - cross_val_predict(denoise_m,train[X],train[y],cv=5)

 
ATE에만 관심이 있다면, 단순히 Y(~)를 T(~)에 대해 회귀하면 된다. 

*단, 잔차 추정 시 발생하는 분산을 설명하지 않으므로 표준오차는 신뢰하면 안 됨 

import statsmodels.api as sm

sm.OLS(y_res, t_res).fit().summary().tables[1]

 
하지만 ATE가 아닌 CATE를 어떻게 이중 머신러닝에서 얻을 수 있을까?


이중 머신러닝으로 CATE  추정하기

비모수적 인과 관계 손실을 얻기 위한 가중치 트릭

CATE를 추정하기 위해서 몇 가지 조정이 필요하다.

인과 매개변수 &amp;amp;tau;가 실험 대상의 공변량에 따라 바뀜

1. 잔차 (ϵ^t)
μy와 μt는 각각 특성 X에서 결과와 처치를 예측한다. 식을 재조정해서 오차에 관한 식으로 구성
손실 함수로 변환하기 위해서 제곱 평균을 계산하면, 

잔차

2. 손실 함수 (L^n(τ(x))
인과 손실 함수로, R러너가 최소화하려는 손실이므로 R손실이라고도 불린다.
즉, 이 손실의 제곱을 최소화하면 원하는 CATE인 τ(Xi)의 기댓값을 추정할 수 있다.  

손실함수의 정의
인과 손실 함수(R손실)

위 식의 표기를 간소화하면,

위의 손실 함수를 최소화하기 위해서 식을 변형해보자.
대수적 변형으로 T(~)를 괄호 밖으로 빼내고,

손실함수의 제곱 부분에서 τ(Xi)를 분리하면, 

최종 식이 아래와 같이 도출된다.

 
위의 손실을 최소화하는 것은 괄호 안에 있는 것을 최소화하는 것과 같지만, 각 항에  T^2(~) 만큼의 가중치가 있는 것

  • 괄호 안의 값을 최소화하는 것 = Yi(~) / Ti(~) 를 예측하는 것
  • 앞서 살펴본 목표 변환 아이디어와 유사하나, 가중치 트릭이 추가되었다.

 < R 러너 STEPS>
1. 손실함수를 생성하기 위한 가중치와 목표를 추정
1-1. 가중치 𝑇 𝑖 (~)^2 생성
1-2. 목표 𝑌 𝑖 (~) / 𝑇 𝑖 (~) 생성
2. 데이터에 적합한 손실함수 최적화
: 가중치를 사용하면서 어떤 예측 방법을 사용하여 목표를 예측
 

w = t_res**2 # 1-1. 가중치 생성
y_star = y_res/t_res # 1-2. 목표 생성

cate_model = LGBMRegressor().fit(train[X], y_star, sample_weight=w) # 2. 가중치로 목표 예측

test_r_learner_pred = test.assign(cate = cate_model.predict(test[X]))

R러너는 CATE 추정값을 직접 출력하기 때문에, S러너에서 처치의 그리드를 정의하는 등 필요했던 모든 추가단계가 필요 없다. 
누적 이득 곡선으로 CATE 순서 관련 성능을 확인해보자.

gain_curve_test = relative_cumulative_gain_curve(test_r_learner_pred, T, y, prediction="cate")
auc = area_under_the_relative_cumulative_gain_curve(test_r_learner_pred, T, y, prediction="cate")

plt.figure(figsize=(10, 4))
plt.plot(gain_curve_test, color="C0", label=f"AUC: {auc:.2f}")
plt.hlines(0, 0, 100, linestyle="--", color="black", label="Baseline")

plt.legend();
plt.title("R 러너")

이 예제에서는 S러너와 비슷한 성능을 보인다.


이중 머신러닝에 대한 시각적인 직관

시뮬레이션 데이터 생성

  • 2개의 공변량
    • x_c는 교란 요인으로, 처치와 결과 모두에 비선형적 영향을 미침
    • x_h는 교란 요인이 아니며, 이질적 효과를 만들고, 세 가지 값(1,2,3)만 존재
  • 처치 효과는 t + t * x_h 이므로 x_h의 세 가지 값 1,2,3에 대해서 CATE는 각각 2,3,4
  • 또한, x_h는 균등하게 분포되므로, ATE는 CATE의 단순 평균인 3
시뮬레이션 데이터

위의 시뮬레이션 데이터를 교란 요인 x_c에 따라(hue=x_c) 그래프로 그려보면,

import matplotlib

plt.figure(figsize=(10, 5))
cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["0.1","0.5","0.9"])

sns.scatterplot(data=df_sim, y="y", x="t", hue="x_c", style="x_h", palette=cmap);
plt.legend(fontsize=14)
교란 요인 x_c는 T와 Y에 비선형적 영향을 미침

 
이제 이중 머신러닝(R 러너)가 이 데이터를 어떻게 다루는지 살펴보자.

  1. 잔차 T(~)와 Y(~)를 구하고,
  2. 데이터가 많지 않으므로 모델 트리의 max_depth=3이도록 제한
  3. t_res를 구하기 위한 편향 제거 모델(debiase_m)에는 유일한 교란 요인인 x_c만 포함시키기
  4. y_res를 구하기 위한 잡음 감소 모델(denoise_m)에는 두 가지 공변량 x_c, x_h 모두 포함시키기
debias_m = LGBMRegressor(max_depth=3)
denoise_m = LGBMRegressor(max_depth=3)

t_res = cross_val_predict(debias_m, df_sim[["x_c"]], df_sim["t"],
                          cv=10)

y_res = cross_val_predict(denoise_m, df_sim[["x_c", "x_h"]],df_sim["y"],
                          cv=10)

df_res = df_sim.assign(
    t_res =  df_sim["t"] - t_res,
    y_res =  df_sim["y"] - y_res
)

이 잔차를 얻은 후에는 x_c에서 오는 교란 편향이 제거될 것

  • 비록 교란 요인 x_c은 비선형적이지만, ml모델은 이 비선형성을 포착하고 편향을 제거할 수 있어야 한다. 
  • 그러면 Y(~)를 T(~)에 단순 회귀했을 때 정확한 ATE를 추정할 수 있다.

잔차 회귀로 ATE 추정하기

import statsmodels.formula.api as smf

smf.ols("y_res~t_res", data=df_res).fit().params["t_res"]

 
CATE 추정하기
먼저 앞서 구한 잔차로 그려보았던 산점도를 다시 그려보자 (교란이 제거되었는지 확인)

  • 첫 번째로 교란 요인 x_c에 따라 잔차간의 관계를 그려보고,
  • 두 번째로 이질적 처치의원인이 되는 변수(공변량)인 x_h에 따라 잔차간의 관계를 그려보자.
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))

ax1 = sns.scatterplot(data=df_res, y="y_res", x="t_res", hue="x_c", style="x_h", alpha=0.5, ax=ax1, palette=cmap)

h,l = ax1.get_legend_handles_labels()
ax1.legend(fontsize=14)

sns.scatterplot(data=df_res, y="y_res", x="t_res", hue="x_h", ax=ax2, alpha=0.5, palette=cmap)
ax2.legend(fontsize=14)

 

왼쪽 그래프

  • 교란 요인에 따라 그린 첫 번째 그래프에서 색상에 패턴이 없기에, 교란 요인이 제거되었음을 보여준다.
  • 교란 요인이 제거되어 데이터가 마치 처치가 무작위 배정된 것처럼 보인다. 

오른쪽 그래프

  • 이질척 처치의 원인이 되는 공변량 x_h에 따라 잔차의 관계를 그려본 결과, x_h=1일 때(어두운 점) 기울기가 낮아 y가 t에 덜 민감한 것처럼 보인다. 
  • 반대로x_h=3일 때(밝은 점)은 기울기가 높아 t에 더 민감한 것처럼 보인다.

이러한 처치 민감도를 어떻게 추출할 수 있을까?

 

1. 이중 머신러닝(R 러너)의 목표(Y*)를 원점을 통과하고, 절편이 0인 선의 기울기로 설정

그 이유는,

    • Y*=목표가 예측값 τ(Xi)에 가까워져야 손실이 최소화될 것
  • 두 잔차가 모두 0 주변으로 분포된 것에 주목해야 한다.
    • Y* = Yi(~) / Ti(~) 이 0에 가깝기 때문에, x_h별 그룹의 모든 τ(Xi), 즉 기울기선도 0을 지나야 한다.
    • 기울기 선이 0을 지나면, 즉 절편이 0이면 기울기를  y△/t△로 추정할 수 있다. 
    • 따라서 목표(Y*)를 월점을 통과하고, 절편이 0인 선의 기울기로 설정하는 것

→ 근데 문제는, T(~)와 Y(~) 모두 평균이 0에 가까워서, 나눴을 때 결과가 매우 불안정해져서 엄청난 잡음이 발생
 

2. (잡음 문제를 해결하기 위해서) x_h의 각 값에 대해 Y*의 평균을 T^2(~)의 가중치로 주기

  • 여기서 가중치 T^2(~)이 중요한 역할을 한다. 큰 점들에 더 많은 중요도를 부여해서, 분산이 낮은 영역에 집중할 수 있게 된다.
  • x_h의 각 값에 대해 Y(*)의 평균을 T^2(~)로 가중치를 줘서 계산한 값이 각각 2,3,4인 실제 CATE에 근사한지 확인해보자.
df_star = df_res.assign(
    y_star = df_res["y_res"]/df_res["t_res"],
    weight = df_res["t_res"]**2,
)

for x in range(1, 4):
    cate = np.average(df_star.query(f"x_h=={x}")["y_star"],
                      weights=df_star.query(f"x_h=={x}")["weight"])
    
    print(f"CATE x_h={x}", cate)
CATE x_h=1 2.019759619990067
CATE x_h=2 2.974967932350952
CATE x_h=3 3.9962382855476957

→ 각각 2,3,4였던 CATE에 가중치가 더해진 모습
 
교란 요인 x_h별로 색상을 표시하되, T^2(~)에 해당하는 가중치를 추가해서 그려보자.

plt.figure(figsize=(10, 6))

sns.scatterplot(data=df_star, palette=cmap,
                y="y_star", x="t_res", hue="x_h", size="weight", sizes=(1, 100)), 

plt.hlines(np.average(df_star.query("x_h==1")["y_star"], weights=df_star.query("x_h==1")["weight"]),
           -1, 1, label="x_h=1", color="0.1")

plt.hlines(np.average(df_star.query("x_h==2")["y_star"], weights=df_star.query("x_h==2")["weight"]),
           -1, 1, label="x_h=2", color="0.5")

plt.hlines(np.average(df_star.query("x_h==3")["y_star"], weights=df_star.query("x_h==3")["weight"]),
           -1, 1, label="x_h=3", color="0.9")

plt.ylim(-1, 8)

plt.legend(fontsize=12)
  • 그래프의 중앙에 가까워질수록 Y*(~)의 분산이 크게 증가한다.(양 옆으로 넓게 데이터가 분포한 모습)
  • Y축 범위를 제한해서 보이지는 않지만, 실제로는 -2,000부터 2,000까지의 데이터 포인트가 있다.
  • 이 점들은 모두 T(~)=0 근처에 있으므로 가중치가 매우 작다. 

그 외 메타러너 - 트리 기반 및 신경망 러너

  1. 트리 기반 CATE 러너 - econml과 causalml 라이브러리에 포함되어 있다.
  2. 신경망 기반 알고리즘 - 초기 단계이고, 아직 가져오는 복잡성이 잠재적 이득에 비해 가치가 낮다고 여겨진다.

7.3 요약

7장에서는 그룹 수준의 처치효과 τ(xi)를 학습하는 아이디어를 확장해보았다.
 

회귀 모델에서 T를 X와 상호작용하는 대신, 일반 ml모델의 용도를 변경해서 CATE를 추정하는 메타러너에 대해 배움

  • 범주형 처치에서만 작동하는 메타러너 2개 X러너와 T러너
    • T(Two) 러너 - 각 처치 t에 대한 y를 예측하기 위해 ml모델을 적합시킨 후, 결과 모델로 처치효과를 추정
    • X(Cross) 러너 - T러너에서 실험군의 데이터셋이 작을때 발생할 수 있는 정규화 편향을 해결하기 해, 성향점수 모델을 사용하여 작은 표본에서 학습된 μt의 중요도를 낮춤 
  • 모든 유형의 처치에서 작동하는 메타러너 S러너와 R러너
    • S(Single) 러너 - 연속형 처치 t에 대한 그리드를 정의하여, Yt에 대한 반사실 예측에 사용하고, 실험 대상별 처치 반응 함수를 생성하여 단일 기울기 매개변수로 요약하는방법
    • R러너(이중 머신러닝) - 일반 ml모델과 아웃 오브 폴드 예측을 사용하여 처치 잔차 T(~)와 결과 잔차 Y(~)를 얻고, CATE인 τ(xi)에 근사한 목표 Y*=Y(~)/T(~)를 구성하는법

마지막으로, 이러한 모든 방법은 비교란성 가정에 의존한다는 점을 기억해야 한다.

  • 비교란성 가정이란, CATE 추정에서 편향을 제거하려면 데이터에 관련된 모든 교란 요인이 포함되어야 한다는 것
  • 이 가정을 통해서 조건부 기댓값의 변화율을 마치 처치 반응 함수의 기울기처럼 해석할 수있게 된다.

 

Comments