TCN과 Transformer로 설계하는 시계열 예측 전략
TCN과 시계열 Transformer의 구조, 감수장과 어텐션, 다변량 예측, 운영 환경별 선택 기준을 실무 관점에서 정리한다.
2026-08-14 · 최초 발행 2025-10-14
예측 문제는 시간 의존성과 운영 제약을 함께 가진다
시계열 데이터가 길어지고 변수와 엔티티가 늘어나면, 단순히 모델의 정확도만 비교해서는 선택하기 어렵다. Temporal Convolutional Networks(TCN)와 시계열 Transformer는 모두 긴 시간축을 다룰 수 있지만, 의존성을 포착하는 방식과 추론·배포 특성이 다르다.
TCN은 인과 패딩과 팽창 계수(dilation)를 적용한 1D CNN, 그리고 잔차 블록을 중심으로 수열을 처리한다. 시퀀스 길이에 선형적 또는 준선형으로 확장할 수 있고, 합성곱 연산을 병렬화할 수 있어 학습과 추론의 지연이 낮은 편이다.
Transformer는 자기 주의(attention)로 각 시점의 정보를 가중 집계한다. 위치 인코딩과 다중 헤드 구조를 통해 시간 순서와 서로 떨어진 구간의 관계를 학습한다. 다만 표준 Self-Attention의 시간·메모리 복잡도는 O(L^2)이므로, 긴 시퀀스에서는 Informer, Performer, Nyström, PatchTST 같은 효율화 기법을 함께 검토한다.
두 모델 모두 다음 파이프라인 위에서 운영된다.
- 결측치와 이상치를 처리한 뒤 표준화 또는 Robust 스케일링을 적용하고, window와 horizon을 정한다.
- 단일 시계열 또는 다중 시계열을 병합해 학습하며 MSE, MAE, Quantile loss 중 목적에 맞는 손실을 고른다.
- 롤링 오리진(backtesting)으로 검증하고 MAPE, MAE, sMAPE, RMSE를 함께 보고한다.
감수장과 어텐션이 읽는 시간 범위
TCN에서 시간축을 얼마나 볼 수 있는지는 dilated causal convolution의 누적 감수장에 의해 결정된다.
R = 1 + (k-1) Σ d_l
여기서 k는 커널 크기이고 d_l은 각 층의 dilation이다. 잔차 연결은 깊은 네트워크의 학습을 안정화한다. 필요한 lookback을 감수장이 덮도록 설계하는 일이 TCN 구성의 출발점이다.
Transformer는 사인 방식 또는 학습형 위치 인코딩으로 시간 순서를 제공하고, multi-head attention으로 시점 사이의 관계를 계산한다. temporal patching은 입력 길이 L을 줄이면서 지역 패턴을 보존하는 방법이다. 긴 입력에서는 이 패치화와 다운샘플링, 효율 어텐션을 우선 검토할 수 있다.
예측 horizon은 학습 방식에도 영향을 준다. Direct 방식은 여러 시점을 한 번에 출력하고, Recursive 방식은 1-step 예측을 반복한다. Multi-Decoder는 두 방식을 섞는다. 누적 오차를 줄여야 하는 환경에서는 Direct 방식이 적합하며, 긴 구간에는 Hybrid 방식을 고려할 수 있다.
τ=0.1/0.5/0.9의 multi-horizon quantile loss를 적용하면 점 예측뿐 아니라 예측 구간도 제공할 수 있다. 과잉과 결품 비용처럼 의사결정 비용이 비대칭인 경우에 활용할 수 있다.
다변량 데이터와 교차 시계열을 다루는 방식
매장, 설비, 종목처럼 엔티티가 여럿인 데이터에서는 ID 임베딩과 정적·동적 공변량을 함께 결합한다. 이는 희소한 개별 시계열에서도 다른 엔티티의 정보를 활용할 수 있게 한다.
Transformer는 직교 주의 변형을 통해 변수 사이의 상호작용을 학습하는 데 강점을 보인다. 반면 TCN은 채널별 합성곱과 그룹노름을 이용해 경량화하기 좋다. 따라서 변수 간 장거리 상호작용이 모델의 핵심인지, 낮은 지연과 단순한 운영이 더 중요한지를 먼저 구분해야 한다.
수요·재고 예측에서는 수천~수만 규모의 크로스 시계열에 엔티티 임베딩을 결합한 Transformer가 희소 시계열 성능 개선에 활용될 수 있다. 엣지 매장에서 재주문 지연 요구가 크다면 TCN으로 빠르게 추론하고, 본사 집계와 계층 예측에는 Transformer로 긴 패턴을 학습하는 구성이 가능하다.
멀티센서 설비에서는 주기와 변동 주파수가 섞여 있을 때 TCN의 다중 팽창 스택이 현지 패턴 검출에 맞는다. 장기 열화 신호나 운전 모드 전환은 Transformer의 장거리 의존 학습이 유리하다.
금융·거시 지표에서는 금리, 원자재, 섹터 ETF 같은 다중 공변량과 캘린더·세션 시간 특징을 Transformer에 통합하고, quantile loss로 불확실성을 추정할 수 있다. 초저지연 트레이딩 서브시스템에는 경량 TCN을 실시간으로 적용할 수 있다.
이상 탐지와 시계열 분류에서는 TCN Autoencoder의 reconstruction 방식 또는 forecasting error 기반 감지를 사용할 수 있다. Transformer encoder는 클래스 토큰(pooling)을 통해 시계열 분류 정확도를 높일 수 있다.
비용과 지연을 고려한 운영 선택
TCN은 엣지나 저사양 환경처럼 지연과 메모리가 제한된 조건에 적합하다. Transformer는 GPU 기반 배치 추론 환경에서 장점을 발휘하지만 메모리 부담을 고려해야 한다.
동일한 길이 L에서 TCN 추론 지연은 3070% 절감될 수 있다. Transformer는 롱시퀀스에서 효율화 기법을 적용하면 메모리를 4080% 절감할 수 있다. 예측 정확도는 전통 ARIMA/Prophet 대비 MAPE 5~20%p 개선 사례가 보고되지만, 도메인과 데이터 품질에 의존한다.
Direct multi-horizon과 backtesting 자동화를 결합하면 주/월 단위 재학습 주기를 최적화하고 다운타임을 최소화할 수 있다. 운영 중에는 KS/PSI 기반 데이터 분포 드리프트, 개체별 성능 편차, 롤링 리트레이닝 정책을 함께 관리한다.
| 모델 | 롱 의존·다변량 성능 | 시퀀스 길이 확장성 | 재현성·수렴 | 훈련 난이도 | 지연·메모리 운영 특성 |
|---|---|---|---|---|---|
| TCN | 중~상, 지역 패턴 강점 | O(L), 병렬 합성곱 | 높음, 잔차·정규화로 안정 | 비교적 용이 | 낮은 지연, 메모리 효율 우수 |
| Transformer | 상, 장거리·교차 변수 상호작용 | O(L^2) → 효율화로 O(L log L) 수준 | 초기화·스케일 민감 | 중~상, 튜닝 요구 | 배치 추론 적합, 메모리 부담 존재 |
효율화 기법을 적용하면 Transformer의 확장성과 운영 편의는 개선된다. 최신 구현과 라이브러리 성능은 최신 정보 확인이 필요하다.
데이터 준비부터 재학습까지의 흐름
PyTorch로 TCN과 Transformer를 비교하는 최소 예제
전제조건은 Python 3.10+, PyTorch 2.2+, CUDA 선택사항이다. 아래 코드는 합성 데이터인 사인파와 노이즈를 이용해 TCN과 Transformer를 비교 학습한다.
# pip install torch==2.2.* numpy==1.26.*
import math, numpy as np, torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
# 데이터셋: 윈도잉
class WindowDataset(Dataset):
def __init__(self, series, lookback=64, horizon=16):
self.x, self.y = [], []
for i in range(len(series) - lookback - horizon + 1):
self.x.append(series[i:i+lookback])
self.y.append(series[i+lookback:i+lookback+horizon])
self.x = torch.tensor(np.array(self.x), dtype=torch.float32).unsqueeze(-1) # (N, L, C=1)
self.y = torch.tensor(np.array(self.y), dtype=torch.float32) # (N, H)
def __len__(self): return len(self.x)
def __getitem__(self, i): return self.x[i], self.y[i]
def make_series(T=5000, noise=0.1, seed=42):
rng = np.random.default_rng(seed)
t = np.arange(T)
s = np.sin(2*math.pi*t/50) + 0.5*np.sin(2*math.pi*t/200 + 0.5)
trend = 0.0005*t
series = s + trend + noise * rng.standard_normal(T)
series = (series - series.mean()) / series.std()
return series.astype(np.float32)
# TCN 블록
class TemporalBlock(nn.Module):
def __init__(self, in_ch, out_ch, k=3, d=1, dropout=0.1):
super().__init__()
pad = (k-1)*d
self.conv1 = nn.Conv1d(in_ch, out_ch, kernel_size=k, dilation=d, padding=pad)
self.conv2 = nn.Conv1d(out_ch, out_ch, kernel_size=k, dilation=d, padding=pad)
self.dropout = nn.Dropout(dropout)
self.relu = nn.ReLU()
self.downsample = nn.Conv1d(in_ch, out_ch, 1) if in_ch != out_ch else None
self.init_weights()
def init_weights(self):
for m in [self.conv1, self.conv2]:
nn.init.kaiming_normal_(m.weight); nn.init.zeros_(m.bias)
if self.downsample is not None:
nn.init.kaiming_normal_(self.downsample.weight); nn.init.zeros_(self.downsample.bias)
def forward(self, x):
# x: (B, C, L), causal slice
out = self.conv1(x); out = out[..., :x.size(-1)]
out = self.relu(out); out = self.dropout(out)
out = self.conv2(out); out = out[..., :x.size(-1)]
out = self.relu(out); out = self.dropout(out)
res = x if self.downsample is None else self.downsample(x)
res = res[..., :out.size(-1)]
return self.relu(out + res)
class TCNRegressor(nn.Module):
def __init__(self, in_ch=1, channels=[32, 32, 64], k=3, dropout=0.1, horizon=16):
super().__init__()
layers, c_in, d = [], in_ch, 1
for c_out in channels:
layers.append(TemporalBlock(c_in, c_out, k=k, d=d, dropout=dropout))
c_in, d = c_out, d*2
self.tcn = nn.Sequential(*layers)
self.head = nn.Sequential(nn.AdaptiveAvgPool1d(1), nn.Flatten(), nn.Linear(c_in, horizon))
def forward(self, x):
# x: (B, L, C)
x = x.transpose(1, 2) # (B, C, L)
z = self.tcn(x) # (B, C, L)
return self.head(z) # (B, H)
# Transformer 기반
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=10000):
super().__init__()
pe = torch.zeros(max_len, d_model)
pos = torch.arange(0, max_len).unsqueeze(1)
div = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(pos * div)
pe[:, 1::2] = torch.cos(pos * div)
self.register_buffer('pe', pe.unsqueeze(1)) # (L, 1, D)
def forward(self, x): # x: (L, B, D)
return x + self.pe[:x.size(0)]
class TransformerRegressor(nn.Module):
def __init__(self, in_ch=1, d_model=64, nhead=4, num_layers=3, dim_ff=128, dropout=0.1, horizon=16):
super().__init__()
self.proj = nn.Linear(in_ch, d_model)
enc_layer = nn.TransformerEncoderLayer(d_model, nhead, dim_ff, dropout, batch_first=False)
self.encoder = nn.TransformerEncoder(enc_layer, num_layers)
self.pos = PositionalEncoding(d_model)
self.pool = nn.AdaptiveAvgPool1d(1)
self.head = nn.Sequential(nn.Flatten(), nn.Linear(d_model, horizon))
def forward(self, x):
# x: (B, L, C)
x = self.proj(x) # (B, L, D)
x = x.transpose(0, 1) # (L, B, D)
x = self.pos(x)
z = self.encoder(x).transpose(0, 1) # (B, L, D)
z = z.transpose(1, 2) # (B, D, L)
z = self.pool(z) # (B, D, 1)
return self.head(z) # (B, H)
def train(model, dl, epochs=5, lr=1e-3, device="cpu"):
model.to(device)
opt = torch.optim.AdamW(model.parameters(), lr=lr)
crit = nn.MSELoss()
for ep in range(1, epochs+1):
model.train(); loss_sum = 0.0
for xb, yb in dl:
xb, yb = xb.to(device), yb.to(device)
pred = model(xb)
loss = crit(pred, yb)
opt.zero_grad(); loss.backward(); opt.step()
loss_sum += loss.item() * xb.size(0)
print(f"epoch {ep} | train_mse: {loss_sum/len(dl.dataset):.4f}")
def main():
series = make_series(T=6000, noise=0.15)
lookback, horizon = 96, 24
ds = WindowDataset(series, lookback, horizon)
n = len(ds); dl = DataLoader(ds[:int(0.8*n)], batch_size=64, shuffle=True)
print("TCN")
tcn = TCNRegressor(in_ch=1, channels=[32, 32, 64], k=3, dropout=0.1, horizon=horizon)
train(tcn, dl, epochs=5)
print("Transformer")
tr = TransformerRegressor(in_ch=1, d_model=64, nhead=4, num_layers=2, dim_ff=128, dropout=0.1, horizon=horizon)
train(tr, dl, epochs=5)
if __name__ == "__main__":
main()
실제 환경에서는 롤링 백테스트, 다중 지표, 조기 종료와 학습률 스케줄러를 적용한다. 다변량 입력 C>1에서는 x 텐서의 마지막 축으로 변수를 확장한다. 엔티티 임베딩과 공변량 결합에는 별도 임베딩 또는 프로젝션 레이어를 추가한다.
모델 성능을 운영으로 연결하는 기준
표준화는 엔티티별·특징별로 분리하고, 누수를 막기 위해 훈련 구간의 통계만 사용한다. window 크기 L과 horizon H는 감수장뿐 아니라 실제 업무 의사결정 창을 함께 고려해 정한다.
TCN은 감수장을 계산해 필요한 L을 덮는지 확인한다. 과도한 팽창은 그리딩 현상을 만들 수 있으므로 커널 크기를 섞어 완화할 수 있다. Transformer는 시퀀스가 길어질수록 패치화와 다운샘플링을 먼저 적용하고, 이어서 효율 어텐션 대안을 검토한다.
불확실성은 Quantile loss로 관리할 수 있으며 비용 민감도에 맞춘 커스텀 손실도 적용할 수 있다. Mixup/CutMix(시간축), 인코더 드롭, 레이어드롭은 과적합을 억제하는 수단이다.
배포 단계에서는 SLA 기준으로 지연과 메모리를 프로파일링한다. TCN은 CPU 엣지 환경을 우선하고, Transformer는 GPU 배치 파이프라인을 선호한다. 데이터·성능 드리프트를 감시하고 성능 임계를 기준으로 자동 재학습을 설정한다. 최신 라이브러리와 커널 최적화 적용 여부는 최신 정보 확인이 필요하다.
짧은 지연과 경량 운영, 지역 패턴이 중요한 과제에는 TCN을 우선 채택할 수 있다. 장거리 의존, 다변량 상호작용, 불확실성 모델링이 중심이라면 Transformer와 효율화 기법을 검토한다. Direct multi-horizon, 롤링 백테스트, 엔티티 임베딩 기반 크로스 시계열 학습을 표준화하고, 지연·메모리 예산에 맞춰 아키텍처를 선택한다. TCN 특징추출과 Transformer 통합을 결합한 하이브리드 접근도 고려할 수 있다.