04. SMOTE는 독이었다

클래스 불균형이라는 유혹

모델 v2.1이 60.74%의 정확도를 찍었을 때, 나는 꽤 고무되어 있었다. LSTM + XGBoost + LightGBM 앙상블이 제법 잘 돌아가고 있었고, 백테스트에서 32,687%라는 수익률도 나쁘지 않았다. 하지만 한 가지가 거슬렸다. 상승 예측의 Recall이 55%에 불과했다는 점이다. 하락은 66%로 잘 잡는데, 상승은 놓치고 있었다.

원인은 명확했다. 학습 데이터에서 하락 4,883개 vs 상승 4,668개. 미미한 차이지만, 모델은 이 불균형에 민감하게 반응하고 있었다. 클래스 불균형의 정도를 정량적으로 표현하면, Imbalance Ratio(IR)라는 지표가 있다.

$$IR = \frac{N_{majority}}{N_{minority}} = \frac{4883}{4668} \approx 1.046$$

IR이 1.0이면 완벽 균형이고, 보통 1.5 이상이면 불균형 문제를 논한다. 1.046이면 사실상 균형에 가까운 수치다. 하지만 나는 이 미미한 불균형마저도 해결하고 싶었고, "클래스 불균형 해결"이라는 키워드가 머릿속을 맴돌았다. 자연스럽게 SMOTE(Synthetic Minority Over-sampling Technique)가 떠올랐다.

SMOTE 알고리즘: 수학적으로 들여다보기

SMOTE(Chawla et al., 2002)의 핵심 아이디어는 소수 클래스 샘플 사이를 선형 보간해서 새로운 합성 샘플을 만드는 것이다. 수학적 정의는 다음과 같다.

소수 클래스의 한 샘플 $x_i$와 그 K-nearest neighbor 중 하나인 $x_{nn}$이 있을 때, 합성 샘플 $x_{new}$는 다음과 같이 생성된다:

$$x_{new} = x_i + \lambda \cdot (x_{nn} - x_i), \quad \lambda \sim U(0, 1)$$

SMOTE를 요리에 비유하면, 된장찌개 레시피 A와 레시피 B 사이의 임의 비율로 재료를 섞어서 새 레시피를 만드는 것이다. 둘 다 된장찌개니까 중간 레시피도 된장찌개일 거라는 가정인데, 이미지 같은 데이터에서는 꽤 잘 통한다. 문제는 시계열에서는 "어제의 된장찌개"와 "한 달 전의 된장찌개"를 섞으면 시간 맥락이 엉망이 된다는 것이다.

여기서 $\lambda$는 0과 1 사이의 균일분포에서 뽑은 난수다. 기하학적으로 보면, $x_i$와 $x_{nn}$을 잇는 선분 위의 임의의 점을 하나 찍는 것이다. 이 과정을 소수 클래스 전체에 대해 반복하면, 다수 클래스와 같은 수의 샘플이 될 때까지 합성 샘플이 늘어난다.

이미지 분류나 텍스트 분류처럼 각 샘플이 독립(i.i.d.)인 도메인에서는 매우 효과적인 기법이다. 하지만 내 데이터는 그런 도메인이 아니었다.

v2.2: 네 가지 개선을 한꺼번에

나는 v2.2에서 욕심을 부렸다. 한 번에 네 가지를 동시에 바꿨다.

  1. 앙상블 가중치 최적화 -- LSTM 35%를 20%로 대폭 감소
  2. LSTM 정규화 강화 -- Dropout 0.3에서 0.4로, L2 정규화 추가
  3. Learning Rate 최적화 -- CosineDecay 스케줄링 도입
  4. SMOTE 클래스 균형 -- 소수 클래스 오버샘플링
from imblearn.over_sampling import SMOTE

# LSTM 시퀀스를 2D로 펴서 XGB 피처와 결합
X_combined = np.concatenate([X_train_seq_flat, X_train_xgb], axis=1)

smote = SMOTE(random_state=42, k_neighbors=5)
X_combined_balanced, y_train_seq = smote.fit_resample(X_combined, y_train_seq)

# 다시 분리하고 LSTM은 3D로 복원
X_train_seq = X_train_seq_flat.reshape(-1, 90, 21)

SMOTE 적용 후 클래스 분포가 16,550:16,229에서 16,550:16,550으로 완벽하게 균형이 맞춰졌다. 겉보기에는 깔끔했다. 하지만 결과는 참담했다.

숫자가 말해주는 실패

앙상블 정확도: 60.74% -> 60.48% (-0.26%) 백테스트 수익률: 32,687% -> 24,806% (-24%)

정확도는 고작 0.26% 떨어졌을 뿐인데, 수익률은 24%나 폭락했다. 약 7억 8천만 원의 차이. SMOTE가 약속했던 상승 Recall 개선은? 55%에서 56%로 겨우 1% 올랐을 뿐이다. 목표는 7%였는데.

더 심각한 건 정확도 하락폭 대비 수익률 하락폭이 57배나 크다는 점이었다. 단순히 "조금 틀리는" 수준이 아니라, "중요한 순간에 크게 틀리는" 모델이 되어버린 것이다. 이 현상은 확률 보정(calibration)의 관점에서 이해할 수 있다. 정확도가 비슷해도, 모델의 확신도(confidence)가 높은 시점에서의 오류는 저확신 시점의 오류보다 수익에 미치는 영향이 훨씬 크다. SMOTE가 노이즈를 주입하면서 모델의 고확신 영역을 교란한 셈이다.

v2.2.1: 반창고로 골절을 치료하려 했다

나는 SMOTE가 문제라는 걸 인정하기 싫었다. "앙상블 가중치를 잘못 조정한 게 문제일 거야." 그래서 v2.2.1을 만들었다. LSTM 가중치를 20%에서 25%로 올리고 LightGBM을 35%에서 30%로 줄였다. 5분이면 되는 실험이었다.

# v2.2.1: 가중치 재조정 시도
self.lstm_weight = 0.25  # 0.20에서 복원 시도
self.xgb_weight = 0.45   # 유지
self.lgb_weight = 0.30   # 0.35에서 감소

계산상으로는 60.67%가 나와야 했다. 실제 결과는 60.25%. 오히려 더 떨어졌다. 단순 가중 평균 계산과 실제 앙상블 예측 사이에는 확률 임계값(0.5)에 의한 비선형 효과가 있었고, 내 직관이 그걸 간과한 것이다.

앙상블 예측은 단순히 $\hat{y} = w_1 \cdot p_1 + w_2 \cdot p_2 + w_3 \cdot p_3$의 연속 값이 아니다. 이 값이 임계값 0.5를 넘는지에 따라 이진 분류가 결정되기 때문에, 가중치의 미세한 변화가 임계값 근처의 샘플들을 뒤집어버릴 수 있다. 이 비선형성 때문에 "계산상 예상 정확도"와 "실제 정확도"가 크게 벌어진 것이다.

이 실패가 오히려 확신을 줬다.

v2.2의 문제는 가중치가 아니라 SMOTE다. 근본 원인을 제거해야 한다.

SMOTE가 시계열에서 독이 되는 이유

SMOTE의 합성 공식 $x_{new} = x_i + \lambda \cdot (x_{nn} - x_i)$를 다시 보자. 이 공식이 암묵적으로 가정하는 것은 "$x_i$와 $x_{nn}$ 사이의 공간이 같은 클래스에 속한다"는 것이다. 이미지에서는 고양이 사진 두 장의 중간이 여전히 고양이일 가능성이 높다. 하지만 금융 시계열에서 이 가정은 세 가지 이유로 무너진다.

1. 자기상관(Autocorrelation) 구조의 파괴

금융 시계열의 핵심 특성 중 하나는 자기상관이다. 시계열 $\{x_t\}$의 lag-$k$ 자기상관은 다음과 같이 정의된다:

$$\rho_k = \frac{\text{Cov}(x_t, x_{t-k})}{\text{Var}(x_t)} = \frac{E[(x_t - \mu)(x_{t-k} - \mu)]}{\sigma^2}$$

자기상관은 드라마의 "이전 줄거리"와 같다. 오늘의 에피소드(현재 값)는 어제 에피소드(과거 값)의 맥락 위에 성립한다. SMOTE가 100화 장면과 500화 장면을 보간해서 합성 장면을 만들면, 중간 줄거리가 통째로 빠진 앞뒤가 안 맞는 장면이 나오는 것이다.

즉, 현재 값과 $k$ 시점 전 값 사이에 통계적 의존성이 존재한다. SMOTE가 $x_i$(t=100 시점)와 $x_{nn}$(t=500 시점)을 보간하면, 이 두 시점 사이 400개 봉에 걸친 자기상관 구조가 완전히 무시된다. 합성 샘플은 어떤 시점에서도 존재하지 않았던, 자기상관 관계가 깨진 데이터 포인트가 된다.

2. 미래 데이터 오염 (Look-Ahead Bias)

SMOTE가 K-nearest neighbors를 찾을 때, 유클리드 거리를 기준으로 가장 가까운 이웃을 선택한다:

$$d(x_i, x_j) = \sqrt{\sum_{f=1}^{F} (x_i^{(f)} - x_j^{(f)})^2}$$

이 거리 계산에서 시간 축은 고려되지 않는다. t=100 시점의 상승 샘플을 보강하기 위해 t=300 시점의 상승 샘플이 최근접 이웃으로 선택될 수 있다. 이렇게 보간된 합성 샘플에는 미래 정보가 암묵적으로 녹아들어 간다. 결과적으로 학습 데이터에 미래 편향이 스며들고, 모델은 백테스트에서 실전보다 과도하게 좋은 성능을 보이게 된다.

3. 피처 공간의 비볼록성(Non-Convexity)

금융 시장의 패턴은 맥락에 극도로 의존적이다. RSI=30이고 거래량이 높은 상승 샘플 $x_i$와, RSI=70이고 거래량이 낮은 상승 샘플 $x_{nn}$의 중간 -- RSI=50에 거래량 중간인 합성 샘플 -- 이 반드시 유의미한 상승 패턴을 나타내지는 않는다. 오히려 어느 쪽에도 속하지 않는 애매한 노이즈가 되어버린다.

수학적으로 표현하면, 클래스 영역이 볼록 집합(convex set)이 아니라는 뜻이다. 집합 $S$가 볼록하려면 임의의 두 점 $x, y \in S$에 대해 $\lambda x + (1-\lambda)y \in S$여야 한다. 금융 데이터의 "상승" 클래스는 이 조건을 만족하지 않는 경우가 많다.

v2.2에서 성공한 것: 정규화의 수학

SMOTE는 실패였지만, v2.2에서 동시에 적용한 정규화 강화는 성공적이었다. LSTM 정확도가 56.47%에서 57.77%로 1.30% 올랐다. 이 성공을 분리해서 이해하려면, 각 정규화 기법의 수학적 원리를 알아야 한다.

L2 정규화(Ridge Regularization)

원래의 Cross-Entropy 손실함수에 가중치의 L2 노름을 페널티로 추가한다:

$$\mathcal{L}_{total} = \mathcal{L}_{CE} + \lambda \sum_{i} w_i^2$$

여기서 $\mathcal{L}_{CE} = -\sum_{c} y_c \log(\hat{y}_c)$는 표준 Cross-Entropy 손실이고, $\lambda$는 정규화 강도(우리 코드에서 0.01), $w_i$는 네트워크의 모든 가중치다.

L2 정규화는 짐 싸기에 비유할 수 있다. 배낭 무게에 비례해서 입장료를 내야 하는 캠핑장이라고 생각하면 된다. 짐(가중치)을 많이 가져갈수록 비용(페널티)이 올라가니까, 정말 필요한 것만 골라서 가볍게 싸게 된다. 모델이 소수의 무거운 짐에 의존하는 대신, 꼭 필요한 것들을 고르게 챙기도록 유도하는 원리다.

이 페널티가 하는 일은 직관적이다. 가중치가 커지면 페널티가 커지니까, 모델이 소수의 큰 가중치에 의존하는 대신 많은 작은 가중치로 일반적인 패턴을 학습하도록 유도한다. 베이지안 관점에서 L2 정규화는 가중치에 대한 가우시안 사전분포(Gaussian prior) $w_i \sim \mathcal{N}(0, 1/\lambda)$를 부여하는 것과 동치다. $\lambda$가 클수록 가중치가 0 근처에 집중되어야 한다는 강한 사전 믿음을 표현한다.

from tensorflow.keras.regularizers import l2
x = Dense(64, activation='relu', kernel_regularizer=l2(0.01))(x)
x = Dense(32, activation='relu', kernel_regularizer=l2(0.01))(x)

Dropout의 확률적 해석

Dropout은 학습 시 매 단계마다 각 뉴런을 독립적으로 "끈다." 수학적으로는 베르누이 마스킹이다:

$$\tilde{h}_i = m_i \cdot h_i, \quad m_i \sim \text{Bernoulli}(1 - p)$$

Dropout의 베르누이 마스킹은 조별 과제에서 매번 랜덤으로 조원을 결석시키는 것과 같다. 매 수업(학습 스텝)마다 40% 확률로 각 조원(뉴런)이 빠지니까, 특정 에이스 한 명에게 의존하는 조는 살아남지 못한다. 결국 모든 조원이 골고루 실력을 키워야 하고, 이게 과적합을 막는 원리다.

여기서 $h_i$는 뉴런 $i$의 활성값, $p$는 드롭아웃 확률(v2.2에서 0.4), $m_i$는 0 또는 1인 마스크 변수다. 기대값으로 보면:

$$E[\tilde{h}_i] = (1 - p) \cdot h_i$$

이것이 의미하는 바는 무엇인가? 매 학습 스텝마다 네트워크의 다른 서브네트워크가 활성화된다. 사실상 $2^n$개의 서브네트워크를 동시에 학습하는 효과가 있다(Srivastava et al., 2014). 추론 시에는 모든 뉴런을 활성화하되, 출력을 $(1-p)$로 스케일링하면 학습 시 기대값과 일치하게 된다.

v2.1에서 v2.2로 Dropout을 0.3에서 0.4로 올린 것은, 더 강한 정규화를 통해 LSTM의 과적합을 억제하겠다는 의도였다. 결과적으로 학습-검증 정확도 격차가 줄어들고 검증 정확도가 1.30% 개선되었다.

# v2.1: 약한 Dropout
x = Dropout(0.3)(x)  # 30% 드롭

# v2.2: 강화된 Dropout
x = Dropout(0.4)(x)  # 40% 드롭 → 과적합 억제 효과

Cosine Annealing Learning Rate

학습률 스케줄링은 최적화 궤적에 직접 영향을 미친다. v2.2에서 도입한 CosineDecay는 다음 공식을 따른다:

$$\eta_t = \eta_{min} + \frac{1}{2}(\eta_{max} - \eta_{min})\left(1 + \cos\left(\frac{t}{T}\pi\right)\right)$$

여기서 $\eta_{max} = 0.0002$ (초기 학습률), $\eta_{min} = 0.00001$ (최소 학습률), $T = 10000$ (총 감쇠 스텝), $t$는 현재 스텝이다.

Cosine Annealing은 낯선 동네에서 맛집을 찾는 과정과 비슷하다. 처음에는 보폭을 크게 해서 동네 전체를 훑고(높은 학습률), 괜찮은 골목을 발견하면 점점 보폭을 줄여가며 정확한 위치를 찾는다(낮은 학습률). 코사인 곡선처럼 부드럽게 줄이는 게 핵심인데, 계단식으로 갑자기 보폭을 줄이면 좋은 골목을 지나칠 수 있기 때문이다.

이 스케줄이 고정 학습률이나 계단식(StepDecay) 감쇠보다 좋은 이유는 무엇인가? Loshchilov & Hutter(2017)의 SGDR 논문에 따르면, 코사인 커브의 부드러운 감소가 손실 지형(loss landscape)의 날카로운 극소점(sharp minima)을 피하고 넓고 평탄한 극소점(flat minima)으로 수렴하는 데 유리하다. 넓은 극소점은 일반화 성능이 더 좋은 것으로 알려져 있다 -- 파라미터 공간에서 조금 벗어나도 손실이 크게 증가하지 않기 때문이다.

from tensorflow.keras.optimizers.schedules import CosineDecay

lr_schedule = CosineDecay(
    initial_learning_rate=0.0002,   # η_max
    decay_steps=10000,              # T
    alpha=0.00001                   # η_min
)
optimizer = Adam(learning_rate=lr_schedule, clipnorm=1.0)

v2.3: SMOTE 제거, 그리고 부활

v2.3의 전략은 단순했다. "좋은 것은 유지하고, 나쁜 것은 제거한다."

  • SMOTE 제거 (근본 원인)
  • v2.1의 앙상블 가중치 복원 (LSTM 0.35 / XGB 0.40 / LGB 0.25)
  • v2.2의 정규화 개선은 유지 (Dropout + L2 + CosineDecay)
# v2.3: SMOTE 제거, 나머지 유지
use_smote = False
self.lstm_weight = 0.35  # v2.1 가중치 복원
self.xgb_weight = 0.40
self.lgb_weight = 0.25
# Dropout, L2, CosineDecay는 v2.2 그대로 유지

2시간의 학습과 10분의 백테스트 뒤에 나온 결과는 기대 이상이었다.

메트릭 v2.1 v2.2 (SMOTE) v2.3 (SMOTE 제거)
정확도 60.74% 60.48% 60.67%
수익률 32,687% 24,806% 37,970%
승률 54.63% 54.46% 55.10%
샤프 비율 31.23 29.78 32.00
MDD 41.2% 40.88% 40.41%

정확도는 v2.1보다 아주 약간 낮지만, 수익률은 16.2%나 더 높았다. 역대 최고 성능이었다. 정확도가 0.07% 낮은데 수익률은 16% 높다는 이 역설은, 정규화가 모델의 예측 품질 분포를 개선했기 때문이다.

v2.2의 정규화 강화가 LSTM 과적합을 줄여서(56.47% -> 57.75%) "수익성 높은 거래를 더 잘 포착"하는 모델이 된 것이다. 여기에 SMOTE라는 노이즈를 제거하니 그 효과가 온전히 발현된 것이다.

실험 설계의 교훈: 변인 통제와 소거법

이 경험에서 가장 뼈아팠던 건 한 번에 여러 가지를 바꾼 것이었다. v2.2에서 네 가지를 동시에 수정하는 바람에, 정규화 강화는 성공이었고 SMOTE는 실패였다는 걸 분리해서 파악하기가 어려웠다. v2.2.1이라는 실패한 가설 검증을 거쳐서야 SMOTE가 범인이라는 확신을 얻었다.

과학 실험에서는 이걸 통제 변인(controlled variable)이라고 부른다. 독립 변인 하나만 바꾸고 나머지는 고정해야 인과관계를 파악할 수 있다. v2.2는 독립 변인 네 개를 동시에 바꾼, 설계가 잘못된 실험이었다. 다행히 v2.2.1(가중치만 변경)이라는 부분 소거 실험과, v2.3(SMOTE만 제거)이라는 결정적 소거 실험을 통해 원인을 분리할 수 있었다.

결과적으로 세 버전이 만들어낸 자연스러운 소거법(ablation study)은 이렇게 정리된다:

구성 요소 v2.1 v2.2 v2.3 효과
가중치 0.35/0.40/0.25 0.20/0.45/0.35 0.35/0.40/0.25 v2.1 복원
정규화 기본 강화 강화 긍정적
SMOTE X O X 부정적
수익률 32,687% 24,806% 37,970%

시계열 데이터에서는 통계적 트릭보다 데이터의 본질을 존중해야 한다. 클래스 불균형이 있다고 해서 합성 데이터로 메꾸는 건, 빈 곳에 가짜 벽돌을 쌓는 것과 같다. 멀리서 보면 그럴듯하지만, 힘이 가해지면 무너진다. 특히 IR이 1.046 정도로 거의 균형 잡힌 데이터에 SMOTE를 적용한 것은, 해결할 문제가 없는 곳에 해결책을 들이댄 격이었다.

v2.3은 내게 중요한 원칙을 새겨줬다.

실패한 실험도 가치가 있다. 단, 실패의 원인을 정확히 분리해낼 수 있어야 한다.

5분짜리 실험(v2.2.1)이 2시간짜리 학습(v2.3)의 방향을 결정했다. 빠른 실패, 빠른 학습. 이건 모델 개발뿐 아니라 모든 엔지니어링에 통하는 원칙이 아닐까.