본문 바로가기
시계열분석

[시계열분석 18] 스케일링과 데이터 누수 — 시계열에서만 생기는 사고

by 카이스토 2026. 9. 6.

CH 18 시계열분석 · Part 6 예측을 제대로 하기

스케일링과 데이터 누수 — 시계열에서만 생기는 사고

이 장은 실무에서 가장 값비싼 실수를 다룬다. 데이터 누수(data leakage)예측 시점에 알 수 없는 정보가 학습에 들어가는 것이고, 시계열에서 그 정보는 언제나 미래다. 같은 모형에 정규화 방식만 바꿔 넣으면 테스트 MAE 가 없음 1.9592 / 전역(누수) 1.5432 / 학습구간만 1.3506 / 인스턴스 0.8769 로 갈린다. 그런데 이 장의 핵심은 그 표가 아니다. 초고는 "전역 스케일링이 성능을 부풀린다"고 썼는데 측정은 정확히 반대였다 — 추세 0.50 에서 전역 1.9902, 학습구간만 1.6540 으로 전역이 0.3362 나쁘다. 새어 든 정보가 '도움'이 아니라 '왜곡'으로 작용한 것이다. 그래도 전역 스케일링은 쓰면 안 된다. 이유가 성능이 아니라 타당성이기 때문이다. 누수는 '점수가 올라가는가'로 판별하지 않는다. '예측 시점에 알 수 있는 정보만 썼는가'로 판별한다.

  • 01 개념
  • 02 수식
  • 03 코드
  • 04 실험
  • 05 시각화

용어 및 기호 정의

데이터 누수 (data leakage)
예측 시점에는 알 수 없는 정보가 학습 과정에 들어가는 것. 시계열에서 그 정보는 거의 언제나 미래다. 시험 문제를 풀기 전에 답안지의 일부를 본 것과 같아서, 실험실 성적과 실전 성적이 갈라진다. 이 장은 누수가 생기는 네 자리를 하나씩 열어 본다
지식 경계 (knowledge boundary)
예측 시점 t 를 기준으로 "이때까지 실제로 손에 들어와 있던 것"과 "아직 없는 것"을 가르는 선. 누수 판별은 언제나 이 선 하나로 한다 — 이 선 오른쪽의 어떤 숫자도 왼쪽 계산에 들어가면 안 된다. 평균 같은 요약통계량도 예외가 아니다
정규화 · 스케일링
입력의 크기를 모형이 다루기 좋은 범위로 옮기는 것. 신경망은 입력이 800 이면 학습이 되지 않는다(활성함수가 포화되고 기울기가 죽는다). 이 장은 무엇으로 나누는가가 아니라 그 값을 어디서 재는가를 문제 삼는다
전역 스케일링 (global scaling)
계열 전체의 평균·표준편차로 표준화하는 것. 여기서는 μ_all = 506.7366, σ_all = 288.7322 다. 전체에는 테스트 구간이 포함되어 있으므로 이 두 수는 미래를 보고 잰 값이다 — 곧 누수다
학습구간만 스케일링
같은 표준화를 학습 구간에서 잰 통계량으로만 하는 것. μ_tr = 406.7170, σ_tr = 230.9803. 전역과의 차이가 평균 +100.0196, 표준편차 +57.7520 이다. 이것이 정직한 파이프라인의 최소선이다
인스턴스 정규화 (instance normalization)
창마다 그 창의 마지막 값을 빼서 수준(level)을 지우는 것. 11장부터 계속 써 온 처방이고, 이 장에서 완결한다. 통계량을 창 하나 안에서만 재므로 구조적으로 누수가 불가능하다. 테스트 MAE 0.8769 로 네 방식 중 가장 좋다
미래 특징 (future feature)
예측 시점 이후에야 확정되는 값을 특징으로 넣는 실수. 극단적으로 정답 자체를 특징에 넣으면 MAE 가 0.000000 이 되고 그 항의 가중치가 정확히 1.000000 이 된다. 실무에서는 '당일 집계값', '월말 확정 매출'처럼 늦게 확정되는 변수가 이 사고를 낸다
겹치는 창 (overlapping windows)
창 길이가 L = 16 이므로 인접한 두 창은 15개 시점을 공유한다. 그래서 분할 경계 부근에서는 한쪽 표본의 정답 시점이 다른 쪽 표본의 입력에 들어간다. 무작위 분할이 시계열에서 치명적인 이유가 이것이다
purge · embargo
분할 경계에 창 길이만큼 간격을 두어 겹침을 잘라내는 관행. 여기서는 간격 16 을 두어 학습 창이 1584개 → 1568개 로 줄었고 테스트 MAE 는 0.8769 → 0.8752 였다. 성능을 위해서가 아니라 절차의 타당성을 위해 한다
하이퍼파라미터 누수
테스트 점수를 보고 은닉 크기·학습률·epoch 을 고르는 것. 모형에는 아무 정보도 안 들어간 것 같지만 사람이 통로가 되어 테스트 정보가 새어 든다. 이것을 막으려고 검증 구간을 따로 둔다 — 이 시리즈가 학습 1584 / 검증 200 / 테스트 200 으로 셋을 나눠 온 이유다
검증손실 vs 테스트 MAE
검증손실은 각 파이프라인이 자기 좌표계에서 잰 값이라 방식이 다르면 단위가 다르다. 전역(누수)의 0.00473 과 인스턴스의 0.21433 을 나란히 놓고 "전역이 45배 좋다"고 말하면 완전한 오독이다. 비교는 반드시 원래 단위로 되돌린 MAE 로 한다
STEP 01

개념: 답안지를 본 채로 치른 시험

11장부터 17장까지 우리는 모형을 바꿔 왔다. RNN, LSTM, TCN, 어텐션, 다단계 전략, 분위수 회귀. 이 장은 모형을 하나도 바꾸지 않는다. 모형은 그대로 두고 자료를 준비하는 절차만 바꾼다. 그런데 그 결과가 지금까지 본 어떤 모형 교체보다 크다.

먼저 이 장이 다루는 사고의 이름을 정확히 붙여 두자.

데이터 누수 = 예측 시점에 알 수 없는 정보가 학습에 들어가는 것 정의

일반적인 지도학습에서도 누수는 생긴다. 하지만 시계열에는 다른 데서 없는 특수한 형태가 있다. 여기서 "알 수 없는 정보"는 거의 언제나 미래이기 때문이다. 그리고 미래는 아주 조용히 새어 든다 — 값 하나를 통째로 베끼는 방식이 아니라, 평균 하나, 표준편차 하나 같은 요약통계량의 형태로 스며든다.

1-1. 시험 비유 — 그런데 이 비유의 절반은 틀렸다

보통 누수를 이렇게 설명한다. "시험 보기 전에 답안지를 본 것과 같다. 그래서 성적이 부풀려진다." 앞의 절반은 맞다. 뒤의 절반은 이 장에서 무너진다.

정확히 말하면 이렇다. 답안지를 훔쳐본 것이 아니라, 남의 답안지에 적힌 평균 점수를 보고 그 값을 기준으로 자기 답을 조정한 것에 가깝다. 그 평균이 자기 시험 범위와 잘 맞으면 도움이 되지만, 잘 안 맞으면 오히려 방해가 된다. 실제로 이 장의 측정이 그랬다.

그러니 이 장의 진짜 물음은 "누수가 점수를 얼마나 올리는가"가 아니다. "이 절차를 실전에서 그대로 재현할 수 있는가" 다.

1-2. 왜 재현할 수 없는가 — 지식 경계

지금 t = 1800 시점에서 다음 값을 예측한다고 하자. 손에 있는 것은 y₀ … y₁₇₉₉ 뿐이다. y₁₈₀₀ 이후는 아직 세상에 존재하지 않는다. 이 선을 지식 경계라 부르자.

여기서 계열 전체의 평균 506.7366 을 계산했다면, 그 계산에는 y₁₈₀₀ … y₁₉₉₉ 가 들어가 있다. 존재하지 않는 값으로 계산한 수다. 실전에서는 그 평균을 계산할 방법이 없다. 즉 이 파이프라인은 배포하는 순간 재현할 수 없는 절차가 된다.

이것이 결정적이다. 실험실에서 성적이 좋든 나쁘든 상관없이, 실전에 옮길 수 없는 절차는 실험 자체가 무의미하다. 우리가 실험을 하는 이유는 "이 방법을 쓰면 앞으로 이만큼 맞힐 것이다"라고 말하기 위해서인데, 그 방법을 앞으로 쓸 수가 없다면 아무 말도 한 것이 없다.

1-3. 시계열이 유난히 위험한 이유 — 추세

이 시리즈의 계열은 y_t = 7 + 0.5t + s(t mod 4) + e_t 다. 기울기 0.5 의 추세가 있어서 뒤로 갈수록 값이 커진다. 그래서:

구간 값의 크기
학습 구간(t < 1600) 최댓값 807.79
테스트 구간(t ≥ 1800) 범위 [904.76, 1008.93]

테스트 구간의 값은 학습 구간에서 한 번도 나온 적 없는 크기다. 겹치는 부분이 아예 없다. 그러니 전역 표준화를 쓰면 모형은 시작하기 전부터 "테스트 값이 이 정도 크기구나"를 이미 알고 들어간다. 정상 계열(추세가 없는 계열)이라면 학습 구간 평균과 전체 평균이 거의 같아서 문제가 눈에 띄지 않는데, 추세가 있으면 두 수가 100 넘게 벌어진다. 실험 3 이 정확히 이것을 잰다.

이 장을 관통하는 한 문장

초보자가 가장 자주 하는 판별법이 있다. "성적이 이상하게 좋으면 누수를 의심하라." 이 조언은 절반만 옳다. 좋은 휴리스틱이긴 하지만 정의가 아니다.

이 장의 실험 3 이 그 반례다. 전역 스케일링은 명백한 누수인데도 성능이 나빠졌다(추세 0.50 에서 0.3362 나쁘다). 만약 "점수가 올라가는가"로 판별했다면 우리는 이 누수를 발견하지 못했을 것이다. 오히려 "전역 스케일링은 손해니까 안 쓴다"는 엉뚱한 이유를 만들어 냈을 것이고, 손해가 아닌 상황(정상 계열, 추세 0.00 에서는 차이가 −0.0002)에서는 아무 거리낌 없이 썼을 것이다.

그래서 판별 기준을 바꿔야 한다. 누수는 결과가 아니라 절차로 판별한다. 물어야 할 질문은 하나뿐이다 — "이 계산에 쓴 모든 숫자를, 예측 시점에 실제로 손에 넣을 수 있었는가?"

이 질문에는 실험을 돌리지 않고도 답할 수 있다. 코드를 읽으면 된다. 그것이 이 판별법의 가장 큰 장점이다.

ch18-d1
도해 1. 이 장 전체가 이 그림의 주황 점선 하나로 정리된다. 왼쪽이 정직한 절차다. 예측 시점 t 를 경계로 왼쪽만 "이미 관측된 값"이고 오른쪽은 아직 존재하지 않는다. 평균·표준편차·특징·하이퍼파라미터를 전부 왼쪽에서만 재고, 그렇게 만든 예측이라야 배포한 뒤에도 똑같이 재현된다. 오른쪽이 누수다. 화살표가 경계를 거꾸로 넘어 미래에서 과거로 들어온다 — 계열 전체의 평균 506.7366 이 학습에 쓰이는 순간이 그것이다. 학습 구간만으로 재면 406.7170 이니 차이가 100.0196, 결코 무시할 크기가 아니다. 그리고 이 그림에서 눈여겨볼 것은 화살표가 값이 아니라 요약통계량 하나라는 점이다. 누수는 보통 정답을 통째로 베끼는 형태가 아니라 평균 하나처럼 눈에 잘 안 띄는 모양으로 들어온다. 아래 두 줄이 이 장의 결론이다 — 판별 기준은 점수가 아니라 "예측 시점에 알 수 있는 정보만 썼는가" 다.
STEP 02

수식: 네 가지 정규화가 각각 무엇을 계산하는가

수식 자체는 전부 뺄셈 하나와 나눗셈 하나다. 어려운 것은 그 두 개의 숫자를 어디서 가져오는가 뿐이다. 네 방식을 한 줄씩 끊어서 본다.

x̃ = x ① 정규화 없음

읽는 법 — 아무것도 하지 않는다. 입력이 그대로 들어간다. 문제는 그 입력이 최대 807.79 라는 것이다. 첫 층 Wx + b 의 결과가 수백 단위가 되고, tanh 나 ReLU 를 지나면서 기울기가 사실상 죽는다. 학습이 안 되는 것이지 모형이 나쁜 것이 아니다. 로그의 1.9592 는 "이 신경망이 이 문제를 못 푼다"가 아니라 "이 신경망은 시작조차 못 했다"로 읽어야 한다.

x̃ = (x − μ_all) / σ_all, μ_all = 506.7366, σ_all = 288.7322 ② 전역 — 누수

읽는 법 — 식은 교과서의 표준화 그대로다. 문제는 μ_all 의 아래첨자다. allt = 0 … 1999 전부를 뜻하고, 거기에는 테스트 구간 t = 1800 … 1999 가 들어 있다. 이 한 글자가 지식 경계를 넘는다. 다시 말해 수식이 틀린 것이 아니라 수식에 넣은 자료의 범위가 틀렸다. 코드에서 이런 실수를 잡아내기 어려운 이유도 여기 있다 — 식은 완벽하게 정상으로 보인다.

x̃ = (x − μ_tr) / σ_tr, μ_tr = 406.7170, σ_tr = 230.9803 ③ 학습구간만 — 정직한 최소선

읽는 법 — 아래첨자가 tr 로 바뀐 것이 전부다. t < 1600 에서만 잰다. 두 방식의 차이를 숫자로 보면 평균 +100.0196, 표준편차 +57.7520 이다. 그리고 검증·테스트 구간도 이 μ_tr, σ_tr 로 변환한다 — 이 부분에서 초보자가 자주 헷갈린다. 테스트 구간을 테스트 구간의 평균으로 표준화하면 그것도 누수다. 재는 것은 학습 구간에서, 적용은 전 구간에가 규칙이다.

x̃ = (x − x_L) / SCALE, x_L = 창의 마지막 값, SCALE = 4.165256 ④ 인스턴스

읽는 법 — 여기서 빼는 값 x_L창마다 다르다. 창 하나에 하나씩, 그 창의 마지막 값이다. 창 하나는 통째로 예측 시점 이전의 관측이므로 지식 경계를 넘을 방법이 구조적으로 없다. 나눗셈에 쓰는 SCALE 만 전체 표본에서 재는 값인데, 이것도 학습 구간에서만 쟀다(tsdl.pyXW[TR]). 11장부터 이 한 줄을 지켜 온 것이 이 장에서 회수된다.

2-1. 인스턴스 정규화를 여기서 완결한다

11장에서 인스턴스 정규화를 쓰면서 "추세가 있으면 필요하다"고만 말하고 넘어갔다. 이제 그 이유를 끝까지 쓴다.

학습 창의 수준 ≈ 50, 테스트 창의 수준 ≈ 900 추세 0.5 가 만든 격차

읽는 법 — 전역이든 학습구간만이든, 계열 전체에 하나의 μ, σ 를 쓰는 방식은 이 격차를 없애지 못한다. μ_tr = 406.7170 을 빼면 학습 구간 초반의 창은 −400 근처가 되고 테스트 창은 +500 근처가 된다. 부호까지 뒤집힌다. 같은 상수를 빼는 한, 서로 다른 시점의 창은 영영 같은 자리에 오지 않는다.

x̃ = x − x_L → 창의 마지막 값이 항상 정확히 0 앵커

읽는 법 — 창마다 다른 값을 빼기 때문에 비로소 모든 창이 같은 자리로 온다. 학습 창도 테스트 창도 오른쪽 끝이 0 이고, 남는 것은 모양뿐이다. 그래서 신경망이 배우는 것은 "값이 얼마인가"가 아니라 "최근 흐름이 어떤 모양이면 다음이 얼마나 움직이는가"가 된다. 예측한 뒤에는 뺐던 x_L 을 다시 더해 원래 눈금으로 돌린다.

ŷ = 0 (정규화 좌표) ŷ = y_{t−1} (원래 눈금) 나이브가 원점이 된다

읽는 법 — 이 등가가 인스턴스 정규화의 숨은 이점이다. 정규화 좌표에서 0 을 예측하는 것이 곧 나이브 예측이므로, 신경망은 나이브로부터의 보정량을 배우게 된다. 모형이 아무것도 못 배워도 최소한 나이브(MAE 3.0577)는 나온다는 뜻이고, 실제로 0.8769 까지 내려갔으니 보정을 제대로 배운 것이다.

2-2. 검증손실을 서로 비교하면 안 되는 이유

여기가 초보자가 가장 자주 넘어지는 자리다. 실험 2 의 검증손실 네 개를 보자.

없음 0.00829 · 전역 0.00473 · 학습구간만 0.00580 · 인스턴스 0.21433 서로 다른 자로 잰 길이

읽는 법 — 전역이 가장 낮고 인스턴스가 45배 크다. 여기서 "전역이 최고"라고 결론 내면 완전한 오독이다. 손실은 각 파이프라인이 자기 좌표계에서 잰 값이기 때문이다. 전역은 오차를 σ_all = 288.7322 로 나눈 세계에서, 인스턴스는 SCALE = 4.165256 로 나눈 세계에서 잰다. 288 로 나눈 오차가 4.16 으로 나눈 오차보다 작은 것은 당연하다. 자가 다르면 길이를 비교할 수 없다.

비교는 반드시 원래 단위로 되돌린 MAE 로 이 장의 실무 규칙

읽는 법 — 예측값을 역변환해서(전역·학습구간만은 ×σ + μ, 인스턴스는 + x_L) 원래 눈금으로 되돌린 뒤 mean|ŷ − y| 를 잰다. 그러면 순서가 완전히 뒤집힌다 — 1.9592 / 1.5432 / 1.3506 / 0.8769 로 인스턴스가 1등이다. 검증손실로 매긴 순위(전역 1등)와 정반대다. 서로 다른 전처리를 비교할 때 손실값을 나란히 놓는 표는 그 자체로 버그다.
ch18-d2
도해 2. 네 방식을 한 표로 세운 것이다. 가운데 열의 식들을 보면 수학적으로는 전부 같은 종류의 뺄셈과 나눗셈이다 — 다른 것은 오른쪽 열, 곧 통계량을 어디서 재는가 하나다. 주황 테두리가 쳐진 칸이 문제의 자리로, "계열 전체 ← 미래 포함"이라고 적혀 있다. 이 칸 하나 때문에 그 줄 전체가 무효가 된다. 아래로 갈수록 재는 범위가 좁아지는 것에 주목하라 — 전체 → 학습 구간 → 창 하나. 범위가 좁아질수록 누수의 여지가 줄고, 맨 아래 인스턴스는 창 하나 안에서만 재기 때문에 구조적으로 누수가 불가능하다. 오른쪽 끝 MAE 열은 그 결과다. 그런데 이 표를 "아래로 갈수록 성능이 좋아진다"로만 읽으면 안 된다. 성능과 타당성은 다른 축이고, 실험 3 에서 그 둘이 갈라지는 것을 보게 된다.
STEP 03

코드: 한 줄의 위치가 실험 전체를 결정한다

먼저 tsdl.py 에서 11장부터 지켜 온 두 줄을 다시 본다. 이 장은 이 두 줄이 왜 그 자리에 있어야 하는지를 증명하는 장이다.

tsdl.py① 통계량을 재는 자리 — 이 장의 전부
def inorm(X, t=None):
    a = X[:, -1:]                       # 각 창의 마지막 값
    Xn = X - a
    return (Xn, (t - a[:, 0]) if t is not None else None, a[:, 0])
SCALE = float(np.std(inorm(XW[TR], TW[TR])[0]))   # 학습 구간에서만 잰다
#                                 ^^^^^^^^^^^ 여기가 XW 였다면 누수다

코드 읽기 — XW[TR] 의 대괄호 하나

np.std(inorm(XW, TW)[0]) 로 쓰면 계열 전체에서 표준편차를 잰다. XW[TR] 로 쓰면 학습 창 1584개에서만 잰다. 눈으로 보면 대괄호 하나 차이고, 실행하면 둘 다 오류 없이 돌아가며, 결과도 소수점 몇 자리만 다르다. 그런데 하나는 정직한 실험이고 하나는 무효다.

이 종류의 버그가 위험한 이유는 아무것도 고장 나지 않기 때문이다. 예외도 안 나고, 경고도 없고, 그래프도 멀쩡하다. 심지어 이 장의 실험 3 처럼 성능이 나빠질 수도 있어서 "이상하게 성적이 좋다"는 신호조차 없다. 유일한 방어는 코드를 읽고 통계량을 재는 자리를 확인하는 것뿐이다.

그래서 이 시리즈는 11~20장 내내 분할과 정규화를 tsdl.py 한 파일에 묶어 두었다. 장마다 다시 짜면 어느 장에서 어느 대괄호가 빠졌는지 아무도 확인할 수 없다. 공유 파이프라인은 편의가 아니라 감사(audit) 장치다.

ch18_code.py② 네 방식을 함수 하나로 갈아 끼운다 (실험 2)
def run(mode, epochs=60, seed=0):
    X, T_ = XW.copy(), TW.copy()
    if mode=="없음":
        f=lambda X,T: (X, T, np.zeros(len(X),np.float32)); sc=float(np.std(XW[TR]))
    elif mode=="전역(누수)":
        f=lambda X,T: ((X-mu_all)/sd_all, (T-mu_all)/sd_all, ...); sc=1.0
    elif mode=="학습구간만":
        f=lambda X,T: ((X-mu_tr)/sd_tr, (T-mu_tr)/sd_tr, ...);   sc=1.0
    else:   # 인스턴스
        f=lambda X,T: (X-X[:,-1:], (T-X[:,-1]), X[:,-1]);        sc=SCALE
    Xtr,ttr,_ = f(X[TR],T_[TR]); Xva,tva,_ = f(X[VA],T_[VA]); Xte,_,ate = f(X[TE],None)

코드 읽기 — 왜 f 를 함수로 만들어 세 구간에 똑같이 적용하는가

f 를 정의해 두고 X[TR], X[VA], X[TE]같은 함수를 적용하는 것이 이 코드의 요점이다. 구간마다 다른 변환을 쓰면 그 자체가 사고다 — 특히 테스트 구간을 테스트 구간의 통계량으로 변환하는 실수가 흔하다. 그것은 배포 시점에 불가능한 계산이다.

mu_all/sd_allmu_tr/sd_tr 은 함수 바깥에서 이미 계산되어 있다. 이름만 봐도 어느 쪽이 위험한지 알 수 있게 지은 이름이다. 실무 코드에서도 scaler_global, scaler_train_only 처럼 범위를 이름에 박아 두는 습관이 큰 도움이 된다.

sc 는 손실을 잴 때 쓰는 나눗셈 상수다. "없음"에서만 np.std(XW[TR]) 로 따로 두는 것은, 정규화를 안 하면 목표값도 수백 단위라 손실이 폭주하기 때문이다. 이 배려까지 해 줘도 "없음"은 1.9592 로 꼴찌다.

ch18_code.py③ 예측을 원래 눈금으로 되돌린다 — 비교의 전제
    with torch.no_grad():
        p = m(torch.tensor(np.asarray(Xte,np.float32)/sc)).squeeze(-1).numpy()*sc
    if mode in ("전역(누수)",): p = p*sd_all + mu_all
    elif mode=="학습구간만":     p = p*sd_tr  + mu_tr
    elif mode=="인스턴스":       p = p + ate
    return best, p

코드 읽기 — 역변환이 없으면 표를 만들 수 없다

세 줄의 if 가 하는 일은 각자의 좌표계에서 원래 눈금으로 되돌리는 것이다. 전역·학습구간만은 ×σ 하고 μ 를 더하고, 인스턴스는 앵커 ate(창마다 다른 값)를 더한다.

이 역변환을 빠뜨리면 STEP 02 에서 말한 사고가 그대로 난다 — 검증손실 네 개를 나란히 놓고 순위를 매기게 된다. 그 순위는 전역 1등, 인스턴스 꼴찌로 나오고, 진실과 정반대다. 비교 가능한 표를 만드는 일 자체가 이 코드의 절반이다.

atef(X[TE], None) 에서 나온 세 번째 반환값이라는 점도 확인해 두자. 인스턴스 정규화만 앵커를 돌려주고, 나머지 셋은 영벡터를 돌려준다. 인터페이스를 통일해 두면 역변환을 한 줄로 쓸 수 있다.

ch18_code.py④ 미래 특징 누수를 일부러 만들어 본다 (실험 4-가)
Xn,tn,an = inorm(XW[TE],TW[TE])
Xcheat = np.column_stack([Xn, (TW[TE]-an)])   # 정답을 특징에 넣음
Xtr_cheat = np.column_stack([Xtr_n, ttr_n])
A  = np.column_stack([Xtr_cheat, np.ones(len(Xtr_cheat))])
bb = np.linalg.lstsq(A, ttr_n, rcond=None)[0]
pc = np.column_stack([Xcheat, np.ones(len(Xcheat))]) @ bb + an

코드 읽기 — column_stack 한 줄이 만드는 완벽한 성적

정답 ttr_n 을 특징 행렬의 마지막 열로 붙였다. 그리고 그 행렬로 정답을 예측하는 최소제곱을 푼다. 답이 무엇일지는 풀기 전에도 알 수 있다 — 마지막 열의 계수를 1 로, 나머지를 0 으로 두면 오차가 정확히 0 이다. 로그가 그대로 확인해 준다: 가중치 1.000000, 나머지 최대 0.000000, MAE 0.000000.

이것을 일부러 해 보는 이유는, 실무의 미래 특징 누수가 정확히 이 구조이기 때문이다. 다만 실무에서는 열 이름이 ttr_n 이 아니라 daily_total, 월말_확정매출, 당일_방문자수 처럼 무해해 보이는 이름이다. 그 변수가 예측 시점에 이미 확정되어 있었는지를 아무도 확인하지 않으면 이 코드와 같은 일이 벌어진다.

선형모형이라 가중치를 열어 볼 수 있었다는 점도 중요하다. 신경망이었다면 "MAE 가 0 이다"까지만 알고 원인을 찾느라 한참 헤맸을 것이다. 의심스러울 때는 같은 특징으로 선형회귀를 한 번 돌려 계수를 보는 것이 가장 빠른 진단법이다.

ch18_code.py⑤ 분할 경계에 간격을 둔다 (실험 4-나)
gap = L                      # 창 길이만큼
tr2 = IW < NTR - gap         # 정답 시점이 1584 미만인 창만 학습에 쓴다
Xg, tg, _ = inorm(XW[tr2], TW[tr2])
# 창 1584개 -> 1568개

코드 읽기 — NTR 이 아니라 NTR - gap

원래 학습 창의 조건은 IW < 1600(정답 시점이 1600 미만)이었다. 여기에 - gap 을 붙여 1584 미만으로 좁혔다. 잘려 나간 16개 창의 정답 시점은 1584 … 1599 다.

왜 하필 그 16개인가. 검증 구간의 첫 창(정답 시점 1600)의 입력1584 … 1599 이기 때문이다. 그 시점들을 학습의 정답으로 쓰면, 검증 표본의 입력에 들어 있는 값을 모형이 이미 맞혀 본 셈이 된다. 창 길이가 L = 16 이므로 간격도 16 이면 충분하다.

결과는 MAE 0.8769 → 0.8752. 차이 0.0017 로 거의 없다. 그래도 하는 이유는 성능이 아니다 — 이 자료는 잡음이 독립이라 겹침의 해가 작지만, 잡음에 자기상관이 있는 실제 자료에서는 이 겹침이 검증 점수를 크게 부풀린다. 9장의 롤링 원점 검증에서 원점을 한 칸씩 밀 때도 같은 문제가 생기고, 금융 쪽에서 purge/embargo 를 표준 절차로 쓰는 이유가 그것이다.

ch18-d3
도해 3. 코드 순서 하나가 실험 전체를 가른다. 왼쪽은 흔한 사고 순서다 — ① 계열 전체를 표준화하고 ② 그다음에 자른다. 이 순서로 쓰면 ①을 실행하는 순간 테스트 구간의 μ, σ 가 이미 손에 들어와 있고, 그 뒤 ②③④ 를 아무리 정확하게 해도 실험은 무효다. 무서운 것은 이 코드가 오류 없이 돌아간다는 점이다. 오른쪽이 옳은 순서다. ① 먼저 자르고 ② 학습 구간에서만 재고 ③ 그 값으로 세 구간을 모두 변환한다. ③의 색이 다른 이유는 여기가 헷갈리는 자리이기 때문이다 — 검증·테스트도 변환은 하되 통계량은 학습 구간 것을 쓴다. 사이킷런의 fit / transform 이 굳이 나뉘어 있는 이유가 이 그림이고, fit_transform 을 테스트 자료에 쓰면 정확히 왼쪽이 된다. 맨 아래 줄의 XW[TR] 이 이 시리즈가 11장부터 지켜 온 대괄호다.
STEP 04

실험과 검증


==================================================================
실험 1 — 전역 스케일링이 새는 정보
==================================================================
계열 전체로 표준화하면 테스트 구간의 평균·표준편차가 학습에 섞여 든다.
  전체로 계산: 평균 506.7366  표준편차 288.7322
  학습만 계산: 평균 406.7170  표준편차 230.9803
  차이       : 평균 +100.0196  표준편차 +57.7520
추세가 있으니 뒤로 갈수록 값이 크다. 전체 평균은 테스트 구간을 '미리 본' 값이다.
테스트 구간 값의 범위 [904.76, 1008.93] — 학습 구간 최대 807.79 를 훌쩍 넘는다.
전역 표준화를 쓰면 모형은 '테스트 값이 이 정도 크기구나' 를 이미 알고 시작한다.
==================================================================
실험 2 — 네 가지 정규화를 같은 모형으로 비교
==================================================================
정규화 방식      검증손실(자기 척도)   테스트 MAE
없음                      0.00829         1.9592
전역(누수)                  0.00473         1.5432
학습구간만                   0.00580         1.3506
인스턴스                    0.21433         0.8769
검증손실은 척도가 달라 서로 비교할 수 없다. 반드시 원래 단위의 MAE 로 비교해야 한다.
정규화 없음: 입력이 808 까지 커서 학습 자체가 잘 안 된다.
전역 vs 학습구간만: MAE 1.5432 vs 1.3506  (차이 0.1927)
인스턴스 정규화가 0.8769 로 가장 낫다 — 추세가 있는 계열의 표준 처방이다.
==================================================================
실험 3 — 누수의 크기를 재는 실험
==================================================================
추세가 없는 계열(정상)에서는 전역 스케일링이 얼마나 해로운가?
추세 기울기    전역 스케일링 MAE   학습구간만 MAE    차이
      0.00             0.8773            0.8776  -0.0002
      0.05             0.9293            0.9181   0.0112
      0.20             1.1428            0.9923   0.1505
      0.50             1.9902            1.6540   0.3362
초고에서는 '전역 스케일링이 성능을 부풀린다' 고 쓰려 했으나 측정은 반대였다:
  추세 0.00 : 차이 -0.0002 (사실상 없음)
  추세 0.50 : 전역 1.9902  vs  학습구간만 1.6540  ->  전역이 0.3362 나쁘다
이유: 전역 평균은 학습 구간의 실제 수준보다 훨씬 크다. 그 값으로 학습 입력을 중심화하면
모형이 보는 입력이 통째로 치우쳐 학습이 나빠진다. 새어 든 정보가 '도움' 이 아니라
'왜곡' 으로 작용한 것이다.
그래도 전역 스케일링은 쓰면 안 된다. 이유가 성능이 아니라 타당성이기 때문이다:
  - 실전에서는 테스트 구간이 아직 존재하지 않으므로 그 평균을 알 수가 없다.
  - 즉 이 파이프라인은 배포하는 순간 재현할 수 없는 절차가 된다.
교훈: 누수는 '점수가 올라가는가' 로 판별하는 것이 아니라
      '예측 시점에 알 수 있는 정보만 썼는가' 로 판별한다.
==================================================================
실험 4 — 다른 종류의 누수
==================================================================
(가) 미래 특징: 예측 시점 t 의 값을 특징으로 넣는 실수
   정답을 특징 하나로 넣으면 MAE = 0.000000  (당연히 0에 가깝다)
   계수: 마지막(정답) 항의 가중치 = 1.000000, 나머지 최대 0.000000
   실무에서는 '당일 집계값' 처럼 예측 시점 이후에야 확정되는 변수를 넣을 때 생긴다.
(나) 겹치는 창: 학습·검증 창이 시점을 공유하면
   창 길이 L=16 이라 인접한 창은 15개 시점을 공유한다.
   시간 분할에서도 경계에서 15개 창이 검증 시점을 일부 포함한다.
   해법: 분할 경계에 L 만큼 간격(purge/embargo)을 둔다.
   간격 16 을 두고 학습: 창 1584개 -> 1568개, 테스트 MAE 0.8752 (간격 없이 0.8769)

4-1. 결과 읽기

실험 1 — 100.0196 이라는 숫자의 의미

전체 평균 506.7366, 학습 구간 평균 406.7170. 차이가 100.0196 이다. 이 값이 큰지 작은지는 자료의 크기와 비교해야 한다. 학습 구간 표준편차가 230.9803 이므로 차이는 0.43 표준편차다. 작지 않다.

이 차이가 생기는 이유는 순전히 추세 때문이다. 기울기 0.5 로 2000 시점을 가면 전체가 1000 만큼 오른다. 계열의 앞쪽 1600 개 평균과 전체 2000 개 평균이 다를 수밖에 없다. 대략 0.5 × (1000 − 800) = 100 인데, 실제 차이 100.0196 이 이 어림과 거의 정확히 일치한다. 우연이 아니라 추세 계산 그대로다.

더 중요한 줄은 그다음이다. 테스트 구간 값의 범위가 [904.76, 1008.93] 인데 학습 구간 최댓값이 807.79 다. 겹치는 구간이 아예 없다. 학습에서 900 이라는 값을 한 번도 본 적이 없다는 뜻이고, 이것이 11장에서 인스턴스 정규화를 도입한 이유였다.

실험 2 — 순위가 두 번 뒤집힌다

검증손실 순위는 전역(0.00473) < 학습구간만(0.00580) < 없음(0.00829) < 인스턴스(0.21433) 다. 테스트 MAE 순위는 인스턴스(0.8769) < 학습구간만(1.3506) < 전역(1.5432) < 없음(1.9592) 다. 1등과 꼴찌가 통째로 뒤바뀌었다.

이유는 STEP 02 에서 이미 봤다. 전역은 오차를 288.7322 로 나눈 세계에서 손실을 재고, 인스턴스는 4.165256 으로 나눈 세계에서 잰다. 나누는 수가 69배 다르니 손실이 45배 차이 나는 것은 모형 성능과 아무 관계가 없다. 이 표를 그대로 보고서에 실으면 정확히 반대 결론을 내게 된다.

"없음"의 1.9592 는 따로 볼 값이다. 이 모형은 나쁜 것이 아니라 학습이 되지 않았다. 입력이 최대 808 이라 첫 층을 지나는 순간 값이 폭주하고, ReLU 와 L1 손실의 조합에서 기울기가 제 역할을 못 한다. 그래도 나이브(3.0577)보다는 나은데, 그것은 목표를 평균 근처로 찍는 것만으로도 나이브를 이길 수 있기 때문이다.

전역과 학습구간만의 차이는 0.1927. 같은 식, 같은 모형, 같은 난수 씨앗에서 대괄호 하나 때문에 생긴 차이다. 그리고 방향에 주의하라 — 누수가 있는 쪽이 더 나쁘다.

실험 3 — 초고가 틀렸다. 그대로 적는다

이 장의 초고는 이렇게 쓰려고 했다. "전역 스케일링은 테스트 정보를 미리 보므로 성능을 부풀린다. 그러니 성적이 좋아 보여도 믿으면 안 된다." 자연스럽고, 교과서에서도 흔히 그렇게 설명한다.

측정은 정반대였다. 추세 0.00 에서 차이 −0.0002 — 사실상 없다. 추세가 커질수록 차이가 벌어지는데, 벌어지는 방향이 전역이 나쁜 쪽이다: 0.05 에서 +0.0112, 0.20 에서 +0.1505, 0.50 에서 +0.3362. 추세 0.50 은 전역 1.9902, 학습구간만 1.6540 이다.

왜 그런가. 전역 평균 506.7366 은 학습 구간의 실제 수준(406.7170)보다 100 이나 크다. 그 값으로 학습 입력을 중심화하면 학습 입력이 통째로 음수 쪽으로 0.35 표준편차만큼 치우친다(그림 1 오른쪽에서 눈으로 확인할 수 있다). 모형은 그렇게 치우친 자료로 학습하고, 그 상태로 테스트를 만난다. 새어 든 정보가 '정답의 힌트'가 아니라 '좌표계의 왜곡'으로 들어온 것이다.

여기서 이 장의 결론이 나온다. 누수가 성능을 올릴 수도, 내릴 수도 있다면 성능은 판별 기준이 될 수 없다. 남는 기준은 하나뿐이다 — 예측 시점에 알 수 있는 정보만 썼는가.

실험 3 — 그런데 왜 여전히 쓰면 안 되는가

"전역이 어차피 나쁘다면, 실용적으로는 안 쓰는 게 이득이니 결론이 같지 않은가?" 이 반문이 위험하다. 추세 0.00 에서는 차이가 −0.0002 였다는 것을 기억하라. 정상 계열에서는 전역 스케일링이 손해도 이득도 아니다. 성능으로 판별하는 사람은 그 상황에서 거리낌 없이 전역을 쓸 것이다.

그런데 그 파이프라인은 배포할 수 없다. 실전에서 예측하려는 시점에는 테스트 구간이 아직 존재하지 않으므로 μ_all 을 계산할 방법이 없다. 학습할 때 쓴 절차를 운영할 때 재현할 수 없다면, 실험 결과는 앞으로의 성능에 대해 아무 말도 해 주지 않는다.

정리하면 두 축이다. 성능은 상황에 따라 오르내리고, 타당성은 상황과 무관하게 깨져 있다. 우리가 지켜야 하는 것은 두 번째다.

한 가지 더. 실무에서 전역 스케일링이 실제로 성적을 올리는 경우도 흔하다(테스트 구간의 분산이 학습 구간보다 크고 그것을 미리 반영하는 경우). 이 장의 자료에서 반대로 나왔다고 해서 "누수는 원래 손해다"라고 일반화하면 안 된다. 방향은 자료마다 다르고, 그래서 더더욱 방향으로 판별할 수 없다.

실험 4-가 — MAE 0.000000 은 축하할 일이 아니다

정답을 특징 하나로 넣었더니 MAE 가 0.000000 이 나왔다. 그리고 그 항의 가중치가 정확히 1.000000, 나머지 16개 지연의 가중치는 절댓값 최대 0.000000 이다. 모형은 지연을 보는 일을 완전히 포기하고 새어 든 열 하나를 그대로 베꼈다.

이 장의 이론 하한을 떠올려 보자. 잡음이 N(0,1) 이므로 완벽한 예측기라도 MAE E|N(0,1)| = 0.7979 아래로 내려갈 수 없다. 그런데 0.000000 이 나왔다. 이론상 불가능한 값이 나오면 모형이 훌륭한 것이 아니라 파이프라인이 고장 난 것이다. 이것이 이 장에서 가장 실용적인 자기 점검법이다 — "이 성적이 이론적으로 가능한가" 를 먼저 묻는다.

실무의 미래 특징 누수는 이렇게 극단적이지 않다. '당일 집계값'처럼 예측 시점 이후에 확정되는 변수가 섞여 들면 MAE 가 0 이 되지는 않고 그럴듯하게 좋아진다. 그래서 더 위험하다. 특징 하나하나에 대해 "이 값이 예측 시점에 이미 확정되어 있었는가" 를 묻는 것 말고 다른 방어가 없다.

실험 4-나 — 0.0017 을 위해 창 16개를 버린다

창 길이가 L = 16 이므로 인접한 두 창은 15개 시점을 공유한다. 시간 순서로 잘라도 경계 부근에서는 검증 창의 입력과 학습 창의 정답이 겹친다. 간격 16 을 두면 학습 창이 1584개 → 1568개 로 줄고, 테스트 MAE 는 0.8769 → 0.8752 가 됐다.

차이 0.0017. 앞 장들에서 확인한 대로 테스트 200개에서 MAE 의 표준오차가 0.01 안팎이니, 이 차이는 잡음보다 작다. 성능만 보면 할 이유가 없는 일이다.

그런데 이 자료가 특별히 관대하다는 점을 알아야 한다. 잡음 e_t독립이라 겹친 시점을 공유해도 정보가 거의 새지 않는다. 잡음에 자기상관이 있는 실제 자료에서는 겹친 15개 시점이 검증 점수를 상당히 부풀린다. 금융 시계열에서 purge/embargo 를 표준으로 쓰는 이유가 그것이다.

그리고 무작위 분할(shuffle)을 쓰면 이 문제가 경계 16개가 아니라 전체로 번진다. 학습 창과 테스트 창이 시점을 공유하는 짝이 수천 개 생긴다. 9장의 롤링 원점 검증과 이 장의 purge 는 같은 원칙의 두 얼굴이다 — 평가는 언제나 지식 경계 오른쪽에서만 한다.

기준선 표에 놓으면

이 파트의 규칙대로 마지막에 기준선 표로 돌아온다. 나이브 3.0577, 계절나이브 2.0704, 지연16 선형 0.8806, 추세+계절더미 회귀(모수 5) 0.7532, 오라클 0.7555, 이론 하한 0.7979.

이 장의 네 방식 중 셋(1.9592 / 1.5432 / 1.3506)은 지연16 선형 0.8806 에도 못 미친다. 정규화를 잘못 고르면 모수 17개짜리 선형모형에도 지는 것이다. 인스턴스 정규화의 0.8769 만이 겨우 그 선을 넘는다.

그리고 그 0.8769 조차 모수 5개짜리 회귀 0.7532 에 0.1237 진다. 11장부터 반복해 온 결론이 여기서도 그대로다. 이 장이 보탠 것은 한 줄이다 — 모형을 고르기 전에 파이프라인을 고쳐야 하고, 파이프라인의 차이(1.9592 → 0.8769, 1.0823)가 이 파트에서 본 어떤 모형 차이보다 크다.

ch18-d4
도해 4. 왼쪽이 실무에서 만나는 누수의 목록이다. 전역 스케일링은 이 장의 주인공이고, 미래 특징은 예측 시점 이후에 확정되는 변수를 넣는 사고, 겹치는 창은 시계열에만 있는 형태, 하이퍼파라미터 누수는 사람이 통로가 되는 형태다. 넷은 겉모습이 아주 다르지만 증상이 똑같다 — 실험실에서는 그럴듯하고 배포하면 무너진다. 그래서 점수가 아니라 절차를 감사해야 한다는 결론이 나온다. 오른쪽은 겹치는 창을 확대한 것이다. 청록 막대가 입력 16개, 오른쪽 끝의 주황 조각이 정답 한 개다. 창을 한 칸 밀면 15개 시점이 그대로 겹치므로 인접한 두 표본은 사실상 같은 자료를 두 번 쓴 것에 가깝다. 오른쪽 짙은 상자가 처방으로, 분할 경계에서 학습 정답 시점을 L = 16 만큼 빼면 창이 1584에서 1568로 줄어든다. 아래 두 상자가 그 결과인데 차이가 0.0017 밖에 안 된다. 그래도 하는 이유가 이 장의 주제다 — 성능이 아니라 절차의 타당성.
STEP 05

시각화

ch18-leak
그림 1. 이 장의 얼굴이다. 왼쪽부터 본다. 파란 선이 계열 전체이고 배경색이 학습(청록)·검증(노랑)·테스트(주황) 구간이다. 가로로 그은 두 선이 이 장의 전부다 — 주황 파선이 전역 평균 506.7366, 청록 실선이 학습구간 평균 406.7170. 주황 파선을 눈으로 따라가 보라. 이 선의 높이를 정하는 데 오른쪽 주황 구간(테스트)의 값들이 참여했다. 그래서 청록 선보다 100.0196 만큼 위에 있다. 회색 점선이 학습 구간 최댓값 807.79 인데, 테스트 구간의 값 [904.76, 1008.93]이 이 점선을 통째로 넘어간다 — 겹치는 구간이 아예 없다는 뜻이다. 오른쪽은 그 결과를 모형이 보는 좌표로 옮긴 것이다. 두 곡선은 같은 계열을 다른 기준으로 표준화한 것인데, 청록(학습구간만)은 학습 구간에서 평균이 정확히 0 인 반면 주황(전역)은 −0.3464 로 통째로 아래로 내려앉아 있다. 모형이 학습하는 입력이 이만큼 치우친다는 뜻이고, 실험 3 에서 전역이 더 나쁘게 나온 이유가 바로 이 치우침이다. 누수가 성능을 올린 것이 아니라 좌표계를 비틀었다.
ch18-four
그림 2. 같은 모형에 정규화만 갈아 끼운 결과다. 왼쪽이 정답 표 — 원래 단위로 되돌린 테스트 MAE 다. 회색(없음) 1.9592 는 학습이 되지 않은 결과이고, 주황(전역) 1.5432 와 파랑(학습구간만) 1.3506 의 차이 0.1927 이 대괄호 하나가 만든 값이다. 방향에 주의하라 — 누수가 있는 주황이 더 높다(나쁘다). 청록(인스턴스) 0.8769 만이 아래 두 기준선 근처까지 내려온다. 파선이 추세+계절더미 회귀 0.7532, 점선이 이론 하한 0.7979 로, 이 두 선은 11~20장 모든 그림에 같은 자리에 그어진다. 오른쪽은 절대 이렇게 비교하면 안 된다는 것을 보여 주는 그림이다. 세로축이 검증손실인데 로그 축인 것에 주목하라 — 값이 0.00473 부터 0.21433 까지 45배 차이 난다. 이 막대만 보면 전역이 압도적으로 좋아 보이지만, 각 막대는 서로 다른 자로 잰 길이다. 왼쪽과 오른쪽의 순위가 정반대라는 사실 하나만 기억해도 이 장의 실무적 가치는 충분하다.
ch18-trend
그림 3. 이 장의 반증이 담긴 그림이다. 왼쪽에서 가로축이 추세 기울기, 세로축이 테스트 MAE 다. 추세 0.00 에서 두 선(주황=전역, 청록=학습구간만)이 0.8773 과 0.8776 으로 사실상 붙어 있다. 추세가 없으면 전체 평균과 학습 구간 평균이 거의 같으니 당연하다. 그런데 추세가 커질수록 주황이 위로 벌어진다 — 0.9293 대 0.9181, 1.1428 대 0.9923, 그리고 0.50 에서 1.9902 대 1.6540. 색칠된 영역이 그 격차이고, 주황이 위에 있다는 것은 누수가 있는 쪽이 더 나쁘다는 뜻이다. 오른쪽은 그 차이만 막대로 뽑은 것이다. −0.0002 → +0.0112 → +0.1505 → +0.3362. 초고는 이 막대들이 아래로 자랄 것이라 예상했다("누수는 점수를 부풀린다"). 측정은 반대였고, 그래서 이 장의 결론이 바뀌었다 — 누수는 결과의 방향으로 판별할 수 없다. 성능이 오르든 내리든, 예측 시점에 알 수 없는 정보를 썼다면 그것은 누수다.
ch18-future
그림 4. 다른 종류의 누수 — 미래 특징이다. 왼쪽 막대에서 맨 오른쪽이 정답을 특징 하나로 넣은 모형이고 MAE 가 0.000000 이다. 주황 점선이 이론 하한 0.7979 인데, 이 자료의 잡음이 N(0,1) 이므로 완벽한 예측기라도 이 선 아래로 갈 수 없다. 그런데 막대가 바닥에 붙어 있다. 화살표가 가리키는 대로 이론상 불가능한 값이고, 이런 성적을 보면 축하할 것이 아니라 파이프라인을 뒤져야 한다. 오른쪽이 그 모형의 내부다. 지연 16개의 가중치가 전부 0(절댓값 최대 0.000000)이고, 새어 든 정답 항 하나만 정확히 1.000000 이다. 모형은 시계열을 보는 일을 완전히 포기하고 답을 그대로 베꼈다. 실무에서는 이 열의 이름이 '정답'이 아니라 '당일 집계값'처럼 무해해 보이고, MAE 도 0 이 아니라 그럴듯하게 좋은 값으로 나온다. 그래서 특징 하나하나에 "예측 시점에 이미 확정되어 있었는가" 를 물어야 한다.
ch18-purge
그림 5. 시계열에만 있는 누수 — 겹치는 창이다. 왼쪽에서 가로 막대 하나가 창 하나의 입력 16개이고 별이 정답이다. 위 네 개(청록)는 정답이 학습 구간(t < 1600)에 있고, 아래 세 개(노랑)는 검증 구간에 있다. 노랑 막대들의 왼쪽 끝이 경계선을 넘어 학습 구간으로 들어가 있는 것을 보라 — 검증 표본의 입력이 학습에 쓰인 시점들을 포함한다. 창 길이가 16 이니 인접한 창은 15개 시점을 공유한다. 주황으로 칠한 띠가 처방인 간격(purge/embargo) 16 으로, 이 구간에 정답이 있는 학습 창을 빼면 겹침이 사라진다. 오른쪽이 그 결과다. 창 1584개에서 1568개로 줄이고 얻은 것이 0.8769 → 0.8752, 겨우 0.0017 이다. 잡음이 독립인 이 자료에서는 겹침의 해가 이 정도로 작다. 그래도 관행으로 굳어진 이유는, 잡음에 자기상관이 있는 실제 자료에서는 이 겹침이 검증 점수를 크게 부풀리기 때문이다. 두 기준선(회귀 0.7532, 하한 0.7979)이 여기서도 같은 자리에 그어져 있다.

이것만 기억하자

  1. 누수는 결과가 아니라 절차로 판별한다. 정의는 하나다 — 예측 시점에 알 수 없는 정보가 학습에 들어갔는가. 이 장의 측정이 그 이유를 보여 준다: 전역 스케일링은 명백한 누수인데도 추세 0.50 에서 1.99021.6540 으로 0.3362 나빴다. 성능이 오를 수도 내릴 수도 있으니 "성적이 좋으면 의심하라"는 판별법으로는 잡을 수 없다. 물어야 할 것은 "이 계산에 쓴 숫자를 예측 시점에 손에 넣을 수 있었는가" 하나다.
  2. 통계량은 학습 구간에서만 재고, 그 값으로 전 구간을 변환한다. 네 방식의 테스트 MAE 가 없음 1.9592 / 전역 1.5432 / 학습구간만 1.3506 / 인스턴스 0.8769 다. 전역과 학습구간만의 차이 0.1927XWXW[TR], 대괄호 하나가 만든 값이다. 추세가 있으면 창의 수준이 학습(≈50)과 테스트(≈900)에서 완전히 다르므로 인스턴스 정규화가 표준 처방이고, 그것이 11장부터 창의 마지막 값을 빼 온 이유다.
  3. 비교는 반드시 원래 단위의 MAE 로 한다. 검증손실은 파이프라인마다 척도가 달라(전역 0.00473 vs 인스턴스 0.21433, 45배) 서로 비교할 수 없다. 실제로 검증손실 순위와 테스트 MAE 순위가 정반대다. 누수는 스케일링 말고도 미래 특징(정답을 넣으면 MAE 0.000000, 가중치 1.000000), 겹치는 창(L=16 이면 15개 시점 공유 → 간격 16 을 두어 창 1584→1568, MAE 0.8752), 하이퍼파라미터 누수로 들어온다.

흔한 오해

  1. "성적이 이상하게 좋으면 누수, 그렇지 않으면 괜찮다" — 이 장이 반증한 문장이다. 전역 스케일링은 추세 0.50 에서 0.3362 나빴고, 추세 0.00 에서는 차이가 −0.0002아무 흔적도 없었다. 성능을 신호로 삼으면 두 경우 모두 놓친다. 반대로 실무에서는 누수가 성적을 크게 올리는 경우도 흔하다 — 방향이 자료마다 다르다는 것이 요점이고, 그래서 방향으로는 판별할 수 없다. 판별은 코드를 읽어서 한다.
  2. "검증손실이 가장 낮은 방식을 고르면 된다" — 정규화 방식이 다르면 손실의 단위가 다르다. 전역은 오차를 288.7322 로, 인스턴스는 4.165256 으로 나눈 세계에서 잰다. 그래서 검증손실은 전역이 1등(0.00473)인데 테스트 MAE 는 인스턴스가 1등(0.8769)이다. 같은 전처리 안에서 모형을 고를 때만 검증손실을 쓸 수 있고, 전처리끼리 비교할 때는 반드시 역변환한 뒤 원래 단위로 잰다.
  3. "시간 순서로 잘랐으니 누수는 없다" — 시간 분할은 필요조건이지 충분조건이 아니다. 정규화 통계량을 전체에서 재면 분할과 무관하게 미래가 새고, 창이 겹치면 경계에서 시점을 공유하며(L=16 이면 15개), '당일 집계값' 같은 미래 특징은 분할을 아무리 잘해도 그대로 들어온다. 게다가 테스트 점수를 보고 하이퍼파라미터를 고르면 사람이 누수의 통로가 된다. 시간 분할은 네 가지 중 하나만 막는다.
  4. "인스턴스 정규화는 성능을 위한 기법이다" — 성능도 좋지만(0.8769) 더 중요한 것은 구조다. 창 하나 안에서만 통계량을 재므로 지식 경계를 넘을 방법이 아예 없다. 반면 전역·학습구간만은 사람이 범위를 매번 올바르게 지정해야 안전하다. 사람이 실수할 여지 자체를 없애는 설계가 실수하지 않으려고 애쓰는 것보다 낫고, 이것이 이 시리즈가 tsdl.py 한 파일에 분할과 정규화를 묶어 둔 이유다.

댓글