🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

LSTM을 이용한 시계열 예측 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 12. 3. · 14 Views

LSTM을 이용한 시계열 예측 완벽 가이드

시간에 따라 변하는 데이터를 예측하는 LSTM 신경망의 핵심 개념과 실전 구현 방법을 다룹니다. 주가 예측, 날씨 예보, 수요 예측 등 실무에서 바로 활용할 수 있는 코드와 함께 단계별로 학습합니다.


목차

  1. 시계열_데이터의_이해
  2. LSTM이란_무엇인가
  3. 데이터_전처리_윈도우_생성
  4. LSTM_모델_구축과_학습
  5. 모델_학습과_시각화
  6. 예측과_성능_평가
  7. 다중_스텝_예측
  8. 다변량_시계열_예측
  9. 실전_프로젝트_구조
  10. 하이퍼파라미터_튜닝

1. 시계열 데이터의 이해

어느 날 김개발 씨는 회사에서 특별한 미션을 받았습니다. "다음 달 상품 판매량을 예측해서 재고를 미리 준비해 주세요." 과거 3년치 판매 데이터는 있는데, 이걸 어떻게 활용해야 할지 막막했습니다.

시계열 데이터란 시간 순서에 따라 측정된 데이터를 말합니다. 마치 일기장에 매일매일 기록을 남기는 것처럼, 시간의 흐름에 따라 값이 변화하는 패턴을 담고 있습니다.

이 패턴을 잘 파악하면 미래를 예측할 수 있게 됩니다.

다음 코드를 살펴봅시다.

import numpy as np
import pandas as pd

# 시계열 데이터 예시: 일별 판매량
dates = pd.date_range('2024-01-01', periods=100, freq='D')
sales = np.random.randn(100).cumsum() + 100  # 누적 합으로 트렌드 생성

# 시계열 데이터프레임 생성
df = pd.DataFrame({'date': dates, 'sales': sales})
df.set_index('date', inplace=True)

# 시계열의 핵심: 시간 순서가 중요합니다
print(df.head(10))
print(f"데이터 기간: {df.index.min()} ~ {df.index.max()}")

김개발 씨는 입사 6개월 차 데이터 분석가입니다. 어느 날 팀장님이 다가와 이렇게 말했습니다.

"다음 분기 매출을 예측해서 경영진에게 보고해야 해요. 가능하겠어요?" 김개발 씨는 엑셀 파일을 열어보았습니다.

3년간의 일별 매출 데이터가 빼곡히 적혀 있었습니다. 숫자들이 오르락내리락하는 게 마치 심전도 그래프처럼 보였습니다.

그렇다면 시계열 데이터란 정확히 무엇일까요? 쉽게 비유하자면, 시계열 데이터는 마치 매일 쓰는 일기장과 같습니다.

1월 1일에는 기분이 좋았고, 1월 2일에는 비가 와서 우울했고, 1월 3일에는 친구를 만나 즐거웠습니다. 이렇게 날짜별로 기록된 감정 변화도 일종의 시계열 데이터입니다.

핵심은 시간의 순서가 중요하다는 점입니다. 일반적인 데이터와 무엇이 다를까요?

일반 데이터는 순서를 섞어도 상관없습니다. 학생들의 키를 분석할 때, 1번 학생과 5번 학생의 순서가 바뀌어도 평균 키는 똑같습니다.

하지만 시계열은 다릅니다. 월요일 다음에 화요일이 와야 하고, 1월 다음에 2월이 와야 합니다.

순서가 곧 의미입니다. 시계열 데이터에는 크게 세 가지 요소가 숨어 있습니다.

첫째는 추세입니다. 장기적으로 데이터가 증가하는지, 감소하는지를 나타냅니다.

회사가 성장하면 매출이 꾸준히 오르는 것이 추세입니다. 둘째는 계절성입니다.

일정한 주기로 반복되는 패턴입니다. 아이스크림은 여름에 많이 팔리고, 핫초코는 겨울에 많이 팔립니다.

이런 규칙적인 패턴이 계절성입니다. 셋째는 노이즈입니다.

예측할 수 없는 무작위 변동입니다. 갑자기 연예인이 특정 음식을 먹방에서 소개해서 판매량이 폭증하는 경우처럼, 예상치 못한 변화입니다.

위 코드를 살펴보면, 먼저 날짜 인덱스를 생성합니다. pd.date_range 함수로 2024년 1월 1일부터 100일간의 날짜를 만듭니다.

그 다음 cumsum 함수로 누적 합을 구해 자연스러운 추세를 만들어냅니다. set_index로 날짜를 인덱스로 설정하는 것이 중요합니다.

이렇게 하면 "2024년 3월의 데이터만 보여줘"라고 쉽게 조회할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

그는 데이터를 자세히 들여다보니, 매달 초에 판매가 급증하고 월말에는 감소하는 패턴을 발견했습니다. "아, 이게 계절성이구나!" 시계열 데이터의 특성을 이해하는 것이 예측의 첫걸음입니다.

이제 본격적으로 이 패턴을 학습하는 방법을 알아보겠습니다.

실전 팁

💡 - 시계열 분석 전에 항상 데이터를 시각화해서 추세와 계절성을 먼저 파악하세요

  • 결측치가 있으면 시간 순서를 고려한 보간법으로 채워야 합니다

2. LSTM이란 무엇인가

김개발 씨는 시계열 예측을 검색하다가 LSTM이라는 단어를 자주 마주쳤습니다. Long Short-Term Memory라는 긴 이름이 어렵게 느껴졌습니다.

선배 박시니어 씨에게 물어보았습니다. "LSTM이 뭔가요?

왜 시계열에 좋다는 건가요?"

LSTM은 Long Short-Term Memory의 약자로, 순차적인 데이터에서 장기 패턴을 기억하고 학습하는 특별한 신경망입니다. 마치 오랜 친구의 이름은 기억하면서 어제 점심 메뉴는 잊어버리는 것처럼, 중요한 정보는 오래 기억하고 불필요한 정보는 잊는 능력을 가지고 있습니다.

다음 코드를 살펴봅시다.

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense

# LSTM 모델의 기본 구조
model = Sequential()

# LSTM 레이어: 과거를 기억하는 핵심 부분
# units=50: 50개의 메모리 셀 사용
# input_shape: (시간 단계, 특성 수)
model.add(LSTM(units=50, input_shape=(10, 1)))

# 출력 레이어: 최종 예측값 생성
model.add(Dense(units=1))

# 모델 컴파일
model.compile(optimizer='adam', loss='mse')
model.summary()

박시니어 씨가 화이트보드 앞으로 김개발 씨를 데려갔습니다. "LSTM을 이해하려면 먼저 기억의 문제를 알아야 해요." 일반적인 신경망은 금붕어 같습니다.

방금 본 것만 기억하고, 조금 전에 본 것은 까맣게 잊어버립니다. 그래서 "오늘 비가 왔고, 어제도 비가 왔고, 그저께도 비가 왔으니 내일도 비가 올 것이다"라는 추론을 하지 못합니다.

RNN(순환 신경망)이 이 문제를 해결하려고 등장했습니다. RNN은 이전 정보를 다음 단계로 전달합니다.

하지만 치명적인 약점이 있었습니다. 멀리 있는 과거는 점점 희미해져서 결국 잊어버리는 것입니다.

예를 들어 보겠습니다. "나는 프랑스에서 태어났습니다.

여러 나라를 여행하며 살았고, 다양한 언어를 배웠습니다. 하지만 모국어는 여전히 ___입니다." 빈칸에 들어갈 말은 프랑스어입니다.

하지만 RNN은 문장이 길어지면 맨 앞의 "프랑스"를 까먹어버립니다. 바로 이 문제를 해결하기 위해 LSTM이 탄생했습니다.

LSTM의 비밀은 세 개의 게이트에 있습니다. 마치 댐의 수문처럼, 어떤 정보를 흘려보내고 어떤 정보를 가둬둘지 조절합니다.

첫째, 잊기 게이트입니다. "이 정보는 이제 필요 없으니 버려야겠다"고 판단합니다.

어제 점심으로 뭘 먹었는지는 오늘 저녁 메뉴 결정에 크게 중요하지 않으니 잊어버립니다. 둘째, 입력 게이트입니다.

"이 새로운 정보는 중요하니 기억해야겠다"고 판단합니다. 오늘 갑자기 물가가 급등했다면, 이 정보는 내일 가격 예측에 중요하므로 기억합니다.

셋째, 출력 게이트입니다. 지금 필요한 정보만 꺼내서 사용합니다.

마치 도서관에서 필요한 책만 꺼내 읽는 것과 같습니다. 위 코드에서 units=50은 50개의 메모리 셀을 사용한다는 뜻입니다.

메모리 셀이 많을수록 더 복잡한 패턴을 기억할 수 있지만, 학습 시간도 오래 걸립니다. **input_shape=(10, 1)**은 과거 10일의 데이터를 보고 예측한다는 의미입니다.

10은 시간 단계, 1은 특성의 수입니다. 만약 온도, 습도, 기압을 모두 사용한다면 특성 수가 3이 됩니다.

실제 현업에서는 어떻게 활용할까요? 주식 시장에서 과거 30일의 주가 흐름을 보고 내일 주가를 예측하거나, 공장에서 과거 일주일의 센서 데이터를 보고 장비 고장을 예측하는 데 사용됩니다.

김개발 씨가 고개를 끄덕였습니다. "그러니까 LSTM은 중요한 건 기억하고, 안 중요한 건 잊어버리는 똑똑한 기억력을 가진 신경망이군요!"

실전 팁

💡 - LSTM의 units 수는 데이터의 복잡도에 따라 32~256 사이에서 실험해 보세요

  • 시계열이 길면 LSTM 레이어를 여러 개 쌓아서 더 깊은 패턴을 학습할 수 있습니다

3. 데이터 전처리 윈도우 생성

김개발 씨가 LSTM 모델을 만들려고 하니, 선배가 멈춰 세웠습니다. "잠깐, 데이터 그대로 넣으면 안 돼요.

LSTM이 이해할 수 있는 형태로 바꿔줘야 해요." 데이터 전처리, 그중에서도 슬라이딩 윈도우 기법을 배울 시간입니다.

LSTM에 데이터를 입력하려면 시퀀스 형태로 변환해야 합니다. 마치 책을 읽을 때 한 페이지씩 넘기는 것처럼, 데이터를 일정한 길이의 창문으로 잘라서 순차적으로 학습시킵니다.

이것을 슬라이딩 윈도우 기법이라고 합니다.

다음 코드를 살펴봅시다.

import numpy as np
from sklearn.preprocessing import MinMaxScaler

# 샘플 데이터 생성
data = np.sin(np.linspace(0, 20, 200)) + np.random.randn(200) * 0.1

# 정규화: 0~1 사이로 스케일링
scaler = MinMaxScaler()
data_scaled = scaler.fit_transform(data.reshape(-1, 1))

# 슬라이딩 윈도우로 시퀀스 생성
def create_sequences(data, window_size):
    X, y = [], []
    for i in range(len(data) - window_size):
        X.append(data[i:i+window_size])  # 과거 window_size개 데이터
        y.append(data[i+window_size])     # 다음 값 (예측 대상)
    return np.array(X), np.array(y)

# 과거 10일로 다음 날 예측
X, y = create_sequences(data_scaled, window_size=10)
print(f"입력 형태: {X.shape}")  # (샘플 수, 시간 단계, 특성)
print(f"출력 형태: {y.shape}")  # (샘플 수, 1)

박시니어 씨가 종이 위에 숫자를 쭉 적었습니다. 1, 3, 5, 7, 9, 11, 13, 15...

"자, 다음 숫자는 뭘까요?" 김개발 씨가 바로 대답했습니다. "17이요.

2씩 증가하는 패턴이니까요." "맞아요. 그런데 컴퓨터는 어떻게 이 패턴을 알까요?

우리처럼 한눈에 보고 이해하지 못해요." LSTM에게 시계열을 학습시키려면 질문과 답 형태로 바꿔줘야 합니다. "1, 3, 5가 주어졌을 때, 다음은?" 답은 7.

"3, 5, 7이 주어졌을 때, 다음은?" 답은 9. 이렇게 창문을 한 칸씩 밀면서 데이터를 만들어냅니다.

이것이 바로 슬라이딩 윈도우 기법입니다. 창문의 크기를 window_size라고 합니다.

window_size가 10이면, 과거 10개의 데이터를 보고 11번째를 예측합니다. 이 크기를 어떻게 정할까요?

정해진 규칙은 없지만, 데이터의 주기를 고려합니다. 주간 패턴이 있다면 7 이상, 월간 패턴이 있다면 30 이상으로 설정합니다.

하지만 그 전에 해야 할 일이 있습니다. 바로 정규화입니다.

주가가 10,000원에서 50,000원 사이를 오간다고 합시다. 신경망은 이렇게 큰 숫자를 다루기 어려워합니다.

학습이 불안정해지고, 시간도 오래 걸립니다. 그래서 모든 값을 0과 1 사이로 압축합니다.

MinMaxScaler가 이 일을 해줍니다. 가장 작은 값은 0, 가장 큰 값은 1로 변환합니다.

나머지 값들은 그 사이에 비례해서 배치됩니다. 10,000원은 0, 50,000원은 1, 30,000원은 0.5가 됩니다.

위 코드의 create_sequences 함수를 자세히 살펴봅시다. for 문이 데이터의 처음부터 끝까지 돌면서, window_size만큼의 구간을 잘라 X에 넣습니다.

그리고 바로 다음 값을 y에 넣습니다. 마치 책의 앞 10페이지를 읽고 11페이지 내용을 맞히는 퀴즈를 만드는 것과 같습니다.

출력된 X.shape을 보면 (샘플 수, 시간 단계, 특성) 형태입니다. LSTM은 이 3차원 형태의 입력을 받습니다.

200개의 데이터에서 window_size 10을 빼면 190개의 샘플이 만들어집니다. 주의할 점이 있습니다.

정규화에 사용한 scaler를 꼭 저장해야 합니다. 나중에 예측 결과를 원래 단위로 되돌릴 때 필요합니다.

예측값이 0.7이 나왔는데, 이게 원래 가격으로 얼마인지 알려면 scaler.inverse_transform을 사용해야 합니다. 김개발 씨가 물었습니다.

"window_size를 너무 크게 하면 어떻게 되나요?" "샘플 수가 줄어들어서 학습 데이터가 부족해질 수 있어요. 또 계산량도 늘어나고요.

적절한 균형이 중요해요."

실전 팁

💡 - 정규화 후 scaler 객체를 pickle로 저장해두면 나중에 예측값을 원래 단위로 복원할 수 있습니다

  • window_size는 데이터의 주기성을 고려해서 설정하세요 (일별 데이터면 7의 배수 추천)

4. LSTM 모델 구축과 학습

드디어 모델을 만들 시간입니다. 김개발 씨는 설레는 마음으로 코드를 작성하기 시작했습니다.

하지만 막상 시작하니 레이어는 몇 개를 쌓아야 하는지, 뉴런은 몇 개로 해야 하는지 고민이 됩니다. 박시니어 씨가 옆에서 가이드를 시작합니다.

LSTM 모델은 Sequential 방식으로 레이어를 차례대로 쌓아 만듭니다. 핵심은 LSTM 레이어에서 시퀀스의 패턴을 학습하고, Dense 레이어에서 최종 예측값을 출력하는 것입니다.

과적합을 방지하기 위해 Dropout을 추가하는 것이 중요합니다.

다음 코드를 살펴봅시다.

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping

# 모델 구축
model = Sequential([
    # 첫 번째 LSTM: return_sequences=True로 다음 LSTM에 시퀀스 전달
    LSTM(64, return_sequences=True, input_shape=(10, 1)),
    Dropout(0.2),  # 과적합 방지: 20% 뉴런 무작위 비활성화

    # 두 번째 LSTM: 더 깊은 패턴 학습
    LSTM(32, return_sequences=False),
    Dropout(0.2),

    # 출력층: 단일 값 예측
    Dense(1)
])

# 컴파일: 옵티마이저와 손실 함수 설정
model.compile(optimizer='adam', loss='mse', metrics=['mae'])

# 조기 종료: 성능 개선 없으면 학습 중단
early_stop = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)

박시니어 씨가 화면을 가리키며 설명을 시작했습니다. "LSTM 모델은 레고 블록 쌓듯이 만들어요.

한 층 한 층 목적에 맞게 쌓아 올리는 거죠." 첫 번째 블록은 LSTM(64) 레이어입니다. 64는 메모리 셀의 개수입니다.

메모리 셀이 많을수록 복잡한 패턴을 기억할 수 있지만, 너무 많으면 학습이 느려지고 과적합 위험이 커집니다. 여기서 중요한 것이 return_sequences=True 옵션입니다.

LSTM 레이어를 여러 개 쌓을 때, 중간 레이어는 이 옵션을 True로 설정해야 합니다. 왜냐하면 다음 LSTM에게도 시퀀스 전체를 전달해야 하기 때문입니다.

마지막 LSTM은 False로 설정해서 최종 결과만 출력합니다. 두 번째 블록은 **Dropout(0.2)**입니다.

이것은 과적합을 방지하는 기술입니다. 학습할 때 무작위로 20%의 뉴런을 꺼버립니다.

왜 이렇게 할까요? 쉽게 비유하자면, 학생이 시험 공부를 할 때 오답노트만 달달 외우면 같은 문제는 잘 풀지만 새로운 문제는 못 푸는 것과 같습니다.

일부 뉴런을 끄면 모델이 특정 패턴에만 의존하지 않고, 더 일반적인 규칙을 학습하게 됩니다. 세 번째는 또 다른 LSTM(32) 레이어입니다.

점점 작아지는 것을 볼 수 있습니다. 이것은 일종의 깔때기 구조로, 정보를 점차 압축해가며 핵심만 남기는 효과가 있습니다.

마지막은 Dense(1) 레이어입니다. Dense는 완전 연결층으로, LSTM이 추출한 특성을 받아서 최종 예측값 하나를 출력합니다.

compile 부분을 봅시다. adam은 가장 널리 쓰이는 옵티마이저로, 학습률을 자동으로 조절해줍니다.

mse는 평균 제곱 오차로, 예측값과 실제값의 차이를 측정합니다. 회귀 문제에서 가장 기본적인 손실 함수입니다.

EarlyStopping은 매우 중요한 콜백입니다. 모델이 학습하다 보면 어느 순간부터 훈련 데이터에만 과하게 적응해서, 새로운 데이터에 대한 성능이 오히려 떨어집니다.

EarlyStopping은 검증 손실이 10번(patience=10) 연속 개선되지 않으면 학습을 중단합니다. restore_best_weights=True 옵션은 학습 중 가장 성능이 좋았던 시점의 가중치를 복원합니다.

학습이 끝났을 때 항상 최고 성능의 모델을 갖게 되는 것입니다. 김개발 씨가 궁금해했습니다.

"레이어를 더 많이 쌓으면 더 좋은 건가요?" "꼭 그렇지만은 않아요. 데이터가 적으면 깊은 모델이 오히려 과적합되기 쉬워요.

보통 2~3개의 LSTM 레이어로 시작해서 성능을 보고 조절하는 게 좋아요."

실전 팁

💡 - 처음에는 작은 모델로 시작해서 점진적으로 복잡도를 높여가세요

  • validation_split을 사용해서 항상 검증 데이터로 과적합 여부를 확인하세요

5. 모델 학습과 시각화

모델 구조가 완성되었습니다. 이제 실제로 데이터를 넣고 학습시킬 차례입니다.

김개발 씨는 긴장되는 마음으로 model.fit 버튼을 눌렀습니다. 과연 모델이 시계열의 패턴을 제대로 학습할 수 있을까요?

모델 학습은 fit 메서드로 수행합니다. 학습 데이터와 검증 데이터를 분리하고, epochs와 batch_size를 설정하여 반복 학습합니다.

학습 과정에서 손실 값의 변화를 시각화하면 모델이 잘 학습되고 있는지 확인할 수 있습니다.

다음 코드를 살펴봅시다.

import matplotlib.pyplot as plt

# 학습/검증 데이터 분리 (80:20)
split = int(len(X) * 0.8)
X_train, X_val = X[:split], X[split:]
y_train, y_val = y[:split], y[split:]

# 모델 학습
history = model.fit(
    X_train, y_train,
    epochs=100,              # 전체 데이터 100번 반복
    batch_size=32,           # 32개씩 묶어서 학습
    validation_data=(X_val, y_val),
    callbacks=[early_stop],
    verbose=1
)

# 학습 과정 시각화
plt.figure(figsize=(10, 4))
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.title('Learning Curve')
plt.savefig('learning_curve.png')

드디어 학습을 시작할 시간입니다. 김개발 씨가 코드를 실행하자 화면에 숫자들이 빠르게 스크롤됩니다.

Epoch 1, Epoch 2... 손실 값이 점점 줄어드는 것이 보입니다.

먼저 데이터를 학습용검증용으로 나눠야 합니다. 왜 굳이 나눌까요?

시험 문제로 비유하면 쉽습니다. 학습 데이터는 교과서입니다.

교과서로 공부합니다. 검증 데이터는 모의고사입니다.

교과서에 없는 문제로 실력을 테스트합니다. 만약 모의고사 점수가 점점 떨어진다면?

교과서만 달달 외우고 있다는 뜻입니다. 이것이 과적합입니다.

코드에서 80%는 학습에, 20%는 검증에 사용합니다. 시계열에서 중요한 점은 시간 순서를 유지해야 한다는 것입니다.

랜덤하게 섞으면 안 됩니다. 미래 데이터로 과거를 예측하는 부정행위가 되어버립니다.

fit 메서드의 파라미터를 살펴봅시다. epochs=100은 전체 데이터를 100번 반복 학습한다는 뜻입니다.

책을 100번 정독하는 것과 같습니다. 처음에는 내용이 낯설지만, 반복할수록 점점 이해가 깊어집니다.

batch_size=32는 한 번에 32개의 샘플을 묶어서 학습한다는 뜻입니다. 왜 하나씩 학습하지 않을까요?

하나씩 하면 학습이 불안정해지고, 전체를 한꺼번에 하면 메모리가 부족합니다. 32개씩 하면 적당한 균형을 찾을 수 있습니다.

validation_data에 검증 데이터를 넣어두면, 매 에폭이 끝날 때마다 이 데이터로 모델을 평가합니다. 학습 데이터에서의 성능(train loss)과 검증 데이터에서의 성능(val loss)을 비교할 수 있습니다.

학습이 끝나면 history 객체에 모든 기록이 담깁니다. 이것을 그래프로 그려봅시다.

이상적인 학습 곡선은 어떤 모양일까요? 처음에는 train loss와 val loss가 모두 빠르게 감소합니다.

그러다가 점점 감소 속도가 느려지며 수렴합니다. 두 선이 비슷한 수준에서 안정되면 좋은 학습입니다.

하지만 train loss는 계속 떨어지는데 val loss가 올라가기 시작한다면? 과적합의 신호입니다.

EarlyStopping이 이 시점에서 학습을 중단시켜줍니다. 김개발 씨가 그래프를 보며 물었습니다.

"val loss가 들쑥날쑥한데 괜찮은 건가요?" "어느 정도의 변동은 자연스러운 거예요. 전체적인 추세가 하향이면 괜찮아요.

너무 들쑥날쑥하면 batch_size를 늘리거나 learning rate를 낮춰볼 수 있어요."

실전 팁

💡 - 학습 곡선에서 val loss가 상승하기 시작하면 과적합이므로 Dropout을 늘리거나 모델을 단순화하세요

  • 시계열 데이터는 절대 shuffle=True로 섞지 마세요

6. 예측과 성능 평가

학습이 완료되었습니다. 이제 진짜 실력을 확인할 시간입니다.

김개발 씨는 테스트 데이터로 모델의 예측 성능을 평가해보기로 했습니다. 과연 미래를 얼마나 정확하게 예측할 수 있을까요?

학습된 모델로 predict 메서드를 사용해 예측을 수행합니다. 예측 결과를 원래 스케일로 복원한 뒤, 실제값과 비교하여 RMSE, MAE 등의 지표로 성능을 평가합니다.

그래프로 시각화하면 예측의 정확도를 직관적으로 파악할 수 있습니다.

다음 코드를 살펴봅시다.

from sklearn.metrics import mean_squared_error, mean_absolute_error
import numpy as np

# 예측 수행
y_pred = model.predict(X_val)

# 역정규화: 예측값을 원래 스케일로 복원
y_pred_original = scaler.inverse_transform(y_pred)
y_val_original = scaler.inverse_transform(y_val)

# 성능 지표 계산
rmse = np.sqrt(mean_squared_error(y_val_original, y_pred_original))
mae = mean_absolute_error(y_val_original, y_pred_original)
print(f"RMSE: {rmse:.4f}")
print(f"MAE: {mae:.4f}")

# 예측 결과 시각화
plt.figure(figsize=(12, 4))
plt.plot(y_val_original, label='Actual', alpha=0.7)
plt.plot(y_pred_original, label='Predicted', alpha=0.7)
plt.legend()
plt.title('Prediction vs Actual')
plt.savefig('prediction_result.png')

학습된 모델의 진짜 실력을 테스트해볼 시간입니다. 박시니어 씨가 말했습니다.

"학교에서 공부만 잘하는 학생이 있고, 실제 시험도 잘 보는 학생이 있어요. 우리 모델은 어느 쪽인지 확인해봐야 해요." predict 메서드에 검증 데이터를 넣으면 예측값이 나옵니다.

하지만 이 값은 아직 0에서 1 사이로 정규화된 상태입니다. 실제 의미 있는 값으로 바꾸려면 역정규화가 필요합니다.

inverse_transform을 사용하면 정규화를 되돌릴 수 있습니다. 0.7이라는 예측값이 실제로 35,000원의 주가였다는 것을 알 수 있게 됩니다.

이제 성능을 측정할 차례입니다. 가장 많이 쓰이는 두 가지 지표가 있습니다.

첫째, RMSE(Root Mean Squared Error)입니다. 예측과 실제의 차이를 제곱해서 평균 낸 뒤 루트를 씌운 값입니다.

큰 오차에 더 민감합니다. 만약 한 번 크게 빗나가면 RMSE가 확 올라갑니다.

둘째, MAE(Mean Absolute Error)입니다. 예측과 실제의 차이에 절댓값을 씌워서 평균 낸 값입니다.

모든 오차를 동등하게 취급합니다. 이해하기 쉽고 직관적입니다.

MAE가 1000이면, 평균적으로 1000원 정도 빗나간다는 뜻입니다. 어떤 지표가 더 좋을까요?

상황에 따라 다릅니다. 큰 오차가 치명적인 경우(예: 의료 분야)에는 RMSE가 적합합니다.

전반적인 정확도를 보고 싶을 때는 MAE가 좋습니다. 그래프를 그려보면 더 직관적으로 이해할 수 있습니다.

파란 선이 실제값, 주황 선이 예측값입니다. 두 선이 거의 겹친다면 훌륭한 모델입니다.

차이가 크다면 모델 개선이 필요합니다. 김개발 씨가 그래프를 보며 미소를 지었습니다.

"오, 생각보다 잘 따라가네요!" 하지만 박시니어 씨는 신중했습니다. "잠깐, 추세는 잘 잡는데 급격한 변화에서 좀 늦게 반응하네요.

LSTM의 특성이에요. 개선할 방법도 있어요." 실무에서는 단순히 수치만 보지 않습니다.

그래프를 보고 어떤 부분에서 예측이 잘 되고, 어떤 부분에서 실패하는지 분석합니다. 급등락 구간에서 예측이 늦다면, 더 많은 특성을 추가하거나 윈도우 크기를 조절해볼 수 있습니다.

실전 팁

💡 - RMSE와 MAE를 함께 보면 오차의 분포를 파악할 수 있습니다 (RMSE가 MAE보다 훨씬 크면 큰 오차가 가끔 있다는 뜻)

  • 예측 그래프에서 지연이 보이면 window_size를 줄이거나 더 민감한 특성을 추가해보세요

7. 다중 스텝 예측

김개발 씨가 결과를 팀장님께 보고했습니다. "좋아요, 그런데 하루만 예측하지 말고 일주일 치를 한꺼번에 예측할 수는 없나요?" 단일 스텝 예측에서 다중 스텝 예측으로 확장하는 방법을 배워보겠습니다.

다중 스텝 예측은 한 번에 여러 시점의 미래를 예측하는 것입니다. 재귀적 방법은 예측값을 입력에 다시 넣어 반복하고, 직접 방법은 출력층에서 여러 값을 한꺼번에 예측합니다.

각각 장단점이 있어서 상황에 맞게 선택해야 합니다.

다음 코드를 살펴봅시다.

# 방법 1: 재귀적 예측 (Recursive)
def recursive_predict(model, last_sequence, n_steps, scaler):
    predictions = []
    current_seq = last_sequence.copy()

    for _ in range(n_steps):
        # 다음 값 예측
        pred = model.predict(current_seq.reshape(1, -1, 1), verbose=0)
        predictions.append(pred[0, 0])

        # 시퀀스 업데이트: 가장 오래된 값 제거, 예측값 추가
        current_seq = np.roll(current_seq, -1)
        current_seq[-1] = pred

    return scaler.inverse_transform(np.array(predictions).reshape(-1, 1))

# 방법 2: 직접 예측 (Direct) - 모델 구조 변경
model_direct = Sequential([
    LSTM(64, input_shape=(10, 1)),
    Dense(7)  # 7일 치를 한 번에 예측
])
model_direct.compile(optimizer='adam', loss='mse')

팀장님의 요청은 합리적입니다. 비즈니스에서는 내일 하루만 아는 것보다 다음 주 전체를 아는 것이 훨씬 유용합니다.

재고 계획, 인력 배치, 마케팅 전략 모두 며칠 앞을 내다봐야 합니다. 다중 스텝 예측에는 크게 두 가지 방법이 있습니다.

첫째, 재귀적 방법입니다. 한 칸 예측하고, 그 예측값을 입력에 넣어서 다음 한 칸을 예측합니다.

이것을 원하는 만큼 반복합니다. 마치 도미노처럼 앞의 예측이 뒤의 예측에 영향을 줍니다.

장점은 기존 모델을 그대로 사용할 수 있다는 것입니다. 단점은 오차가 누적된다는 것입니다.

첫 예측이 조금 틀리면, 두 번째는 더 틀리고, 세 번째는 더더욱 틀립니다. 멀리 예측할수록 신뢰도가 급격히 떨어집니다.

둘째, 직접 방법입니다. 출력층을 **Dense(7)**로 바꿔서 한 번에 7일 치를 예측합니다.

각 날짜에 대해 독립적으로 예측하므로 오차가 누적되지 않습니다. 단점은 모델을 새로 학습해야 하고, 날짜 간의 연관성을 덜 활용한다는 것입니다.

또한 학습 데이터의 형태도 바꿔야 합니다. y가 단일 값이 아니라 7개의 값을 가진 배열이 됩니다.

위 코드에서 재귀적 방법의 핵심은 np.roll 함수입니다. 시퀀스를 왼쪽으로 한 칸씩 밀고, 맨 끝에 새 예측값을 넣습니다.

슬라이딩 윈도우가 한 칸 이동하는 것입니다. 어떤 방법을 선택해야 할까요?

짧은 기간(3~5 스텝)을 예측한다면 재귀적 방법이 간편합니다. 긴 기간(20 스텝 이상)을 예측한다면 직접 방법이 더 안정적입니다.

또 다른 선택지로 Encoder-Decoder 구조가 있는데, 이는 두 방법의 장점을 결합한 고급 기법입니다. 김개발 씨가 일주일 예측 결과를 그래프로 그려보았습니다.

처음 2~3일은 꽤 정확했지만, 뒤로 갈수록 오차가 커지는 것이 보였습니다. "이게 재귀적 방법의 한계예요.

업무에서는 예측 기간에 따른 신뢰 구간을 함께 보여주는 게 좋아요. '3일까지는 90% 신뢰, 7일은 70% 신뢰' 이런 식으로요."

실전 팁

💡 - 재귀적 예측은 5 스텝 이내에서 가장 효과적입니다

  • 직접 예측은 학습 데이터에서 y를 여러 시점으로 구성해야 합니다

8. 다변량 시계열 예측

팀장님이 추가 요청을 했습니다. "판매량만 보지 말고, 광고비, 할인율, 경쟁사 가격도 같이 분석하면 더 정확하지 않을까요?" 맞는 말입니다.

여러 변수를 함께 고려하는 다변량 시계열 예측을 배워보겠습니다.

다변량 시계열은 여러 개의 변수를 동시에 입력으로 사용하여 예측하는 방법입니다. 판매량뿐만 아니라 기온, 광고비, 이벤트 여부 등 관련 있는 모든 정보를 모델에 제공하면 더 정확한 예측이 가능합니다.

입력 형태가 3차원으로 확장됩니다.

다음 코드를 살펴봅시다.

import numpy as np

# 다변량 데이터 예시: 판매량, 기온, 광고비
n_samples = 200
sales = np.random.randn(n_samples).cumsum() + 100
temperature = 20 + 10 * np.sin(np.linspace(0, 4*np.pi, n_samples))
ad_spend = np.random.uniform(1000, 5000, n_samples)

# 다변량 데이터 결합 (n_samples, n_features)
multivariate_data = np.column_stack([sales, temperature, ad_spend])

# 다변량 시퀀스 생성
def create_multivariate_sequences(data, target_col, window_size):
    X, y = [], []
    for i in range(len(data) - window_size):
        X.append(data[i:i+window_size])        # 모든 특성 사용
        y.append(data[i+window_size, target_col])  # 예측 대상만
    return np.array(X), np.array(y)

# 판매량(0번 컬럼) 예측, 입력은 3개 특성 모두 사용
X, y = create_multivariate_sequences(multivariate_data, target_col=0, window_size=10)
print(f"입력 형태: {X.shape}")  # (샘플, 시간, 특성 3개)

지금까지는 판매량 하나만 보고 판매량을 예측했습니다. 하지만 현실에서 판매량에 영향을 미치는 요소는 무수히 많습니다.

날씨, 요일, 공휴일, 경쟁사 할인, 우리 광고비, 경기 상황... 이 모든 것을 고려하면 더 정확해지지 않을까요?

다변량 시계열이 바로 이 아이디어입니다. 여러 개의 시계열을 동시에 입력으로 사용합니다.

쉽게 비유하자면, 의사가 환자를 진단할 때와 비슷합니다. 열이 39도라는 정보 하나만으로 병을 진단하기는 어렵습니다.

하지만 열이 39도이고, 기침을 하고, 목이 붓고, 코가 막혔다면? 감기라고 진단하기 훨씬 쉽습니다.

정보가 많을수록 판단이 정확해집니다. 위 코드에서 np.column_stack으로 세 개의 시계열을 합쳤습니다.

이제 데이터의 형태가 (200, 3)이 됩니다. 200개의 시점, 각 시점에 3개의 값(판매량, 기온, 광고비)이 있습니다.

create_multivariate_sequences 함수가 핵심입니다. X에는 모든 특성을 넣고, y에는 예측하고 싶은 특성(판매량)만 넣습니다.

결과적으로 X의 형태는 (샘플 수, 시간 단계, 특성 수)가 됩니다. 모델은 어떻게 바뀔까요?

거의 동일합니다. input_shape만 바꿔주면 됩니다.

기존에는 **input_shape=(10, 1)**이었습니다. 과거 10일, 특성 1개.

이제는 **input_shape=(10, 3)**이 됩니다. 과거 10일, 특성 3개.

주의할 점이 있습니다. 모든 특성을 같은 스케일로 정규화해야 합니다.

판매량은 100 단위, 기온은 20 단위, 광고비는 1000 단위라면, LSTM이 광고비에만 집중하게 됩니다. 숫자가 크면 영향력이 커 보이기 때문입니다.

StandardScalerMinMaxScaler로 각 특성을 정규화합니다. 이때 각 특성별로 따로 정규화하는 것이 좋습니다.

그래야 나중에 역정규화할 때도 깔끔합니다. 김개발 씨가 궁금해했습니다.

"어떤 특성을 넣어야 할지 어떻게 알죠?" "도메인 지식이 중요해요. 판매량에 영향을 줄 것 같은 변수를 넣어보고, 성능이 개선되는지 확인하는 거예요.

때로는 직관과 다른 결과가 나오기도 해요. 실험이 필요해요."

실전 팁

💡 - 상관관계 분석으로 어떤 특성이 예측 대상과 관련 있는지 먼저 확인하세요

  • 특성이 너무 많으면 과적합 위험이 있으므로 중요한 것만 선별하세요

9. 실전 프로젝트 구조

이론을 충분히 배웠습니다. 이제 실제 프로젝트에서 어떻게 코드를 구조화하는지 배워보겠습니다.

김개발 씨는 지금까지 주피터 노트북에서 작업했지만, 실제 서비스에 적용하려면 체계적인 구조가 필요합니다.

실전 프로젝트에서는 데이터 로드, 전처리, 모델 학습, 예측, 평가를 각각의 함수나 클래스로 분리합니다. 재사용성과 유지보수를 위해 파이프라인 형태로 구성하는 것이 좋습니다.

모델과 스케일러를 저장해두면 나중에 다시 로드하여 예측할 수 있습니다.

다음 코드를 살펴봅시다.

import joblib
from tensorflow.keras.models import load_model

class TimeSeriesPredictor:
    def __init__(self, window_size=10):
        self.window_size = window_size
        self.model = None
        self.scaler = MinMaxScaler()

    def preprocess(self, data):
        """데이터 정규화 및 시퀀스 생성"""
        scaled = self.scaler.fit_transform(data.reshape(-1, 1))
        X, y = create_sequences(scaled, self.window_size)
        return X, y

    def train(self, X, y, epochs=100):
        """모델 학습"""
        self.model = self._build_model()
        self.model.fit(X, y, epochs=epochs, validation_split=0.2, verbose=0)

    def save(self, path):
        """모델 및 스케일러 저장"""
        self.model.save(f"{path}/model.h5")
        joblib.dump(self.scaler, f"{path}/scaler.pkl")

    def load(self, path):
        """저장된 모델 로드"""
        self.model = load_model(f"{path}/model.h5")
        self.scaler = joblib.load(f"{path}/scaler.pkl")

주피터 노트북에서 실험하는 것과 실제 서비스에 배포하는 것은 완전히 다른 일입니다. 박시니어 씨가 말했습니다.

"노트북 코드를 그대로 서버에 올리면 안 돼요. 구조화가 필요해요." 클래스로 감싸면 여러 장점이 있습니다.

첫째, 재사용성입니다. 다른 데이터셋에도 같은 클래스를 사용할 수 있습니다.

predictor = TimeSeriesPredictor(window_size=14)처럼 파라미터만 바꿔서 새 인스턴스를 만들면 됩니다. 둘째, 상태 관리입니다.

모델과 스케일러가 하나의 객체 안에 함께 있으므로, 둘이 어긋날 일이 없습니다. 스케일러 A로 학습한 모델에 스케일러 B를 적용하는 실수를 방지합니다.

셋째, 저장과 복원입니다. save 메서드로 학습 결과를 저장하고, 나중에 load로 불러와서 바로 예측에 사용할 수 있습니다.

매번 학습할 필요가 없습니다. joblib은 파이썬 객체를 파일로 저장하는 라이브러리입니다.

스케일러처럼 Keras 모델이 아닌 객체를 저장할 때 사용합니다. Keras 모델은 자체 save 메서드로 저장합니다.

실제 서비스 흐름을 생각해 봅시다. 개발 단계에서는 데이터를 모으고, 전처리하고, 모델을 학습하고, 성능을 평가합니다.

만족스러우면 모델과 스케일러를 저장합니다. 서비스 단계에서는 저장된 모델을 로드합니다.

새 데이터가 들어오면 스케일러로 정규화하고, 모델로 예측하고, 결과를 역정규화하여 반환합니다. 학습 코드는 실행되지 않습니다.

김개발 씨가 물었습니다. "모델 업데이트는 어떻게 하나요?

새 데이터가 계속 쌓이잖아요." "보통 주기적으로 재학습해요. 일주일에 한 번, 또는 한 달에 한 번.

자동화된 파이프라인을 만들어두면 편해요. 새 데이터로 학습하고, 성능을 검증하고, 괜찮으면 기존 모델을 교체하는 거죠."

실전 팁

💡 - 모델 버전 관리를 위해 저장 경로에 날짜나 버전 번호를 포함하세요

  • 예측 서비스에서는 학습 코드를 아예 분리해서 보안과 성능을 모두 챙기세요

10. 하이퍼파라미터 튜닝

모델이 어느 정도 작동하지만, 더 좋은 성능을 원합니다. 김개발 씨는 문득 궁금해졌습니다.

"LSTM 뉴런을 32로 할까 64로 할까? 윈도우 사이즈는 7이 좋을까 14가 좋을까?" 최적의 설정을 찾는 하이퍼파라미터 튜닝을 배워보겠습니다.

하이퍼파라미터는 모델 학습 전에 미리 정해야 하는 값들입니다. LSTM의 뉴런 수, 레이어 수, 윈도우 크기, 학습률 등이 해당됩니다.

Grid SearchRandom Search로 다양한 조합을 시험해보고 최적의 설정을 찾습니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import TimeSeriesSplit
import itertools

# 탐색할 하이퍼파라미터 공간
param_grid = {
    'units': [32, 64, 128],
    'window_size': [7, 14, 21],
    'dropout': [0.1, 0.2, 0.3]
}

# 시계열 교차 검증
tscv = TimeSeriesSplit(n_splits=3)

def evaluate_params(units, window_size, dropout, data):
    """특정 파라미터 조합의 성능 평가"""
    X, y = create_sequences(data, window_size)
    scores = []

    for train_idx, val_idx in tscv.split(X):
        model = Sequential([
            LSTM(units, input_shape=(window_size, 1)),
            Dropout(dropout),
            Dense(1)
        ])
        model.compile(optimizer='adam', loss='mse')
        model.fit(X[train_idx], y[train_idx], epochs=20, verbose=0)
        score = model.evaluate(X[val_idx], y[val_idx], verbose=0)
        scores.append(score)

    return np.mean(scores)

하이퍼파라미터 튜닝은 요리의 간 맞추기와 비슷합니다. 소금을 얼마나 넣을지, 불은 얼마나 세게 할지, 몇 분 동안 조리할지.

모두 경험과 실험으로 찾아가는 값들입니다. Grid Search는 가장 단순한 방법입니다.

모든 가능한 조합을 시험합니다. 위 코드에서는 units 3가지, window_size 3가지, dropout 3가지로 총 27가지 조합을 시험합니다.

시간이 오래 걸리지만 확실합니다. Random Search는 무작위로 조합을 뽑아 시험합니다.

더 빠르지만, 운이 없으면 좋은 조합을 놓칠 수 있습니다. 탐색 공간이 넓을 때 유용합니다.

시계열에서 특히 중요한 것이 TimeSeriesSplit입니다. 일반적인 교차 검증은 데이터를 무작위로 섞어서 나눕니다.

하지만 시계열은 시간 순서가 중요하므로 섞으면 안 됩니다. TimeSeriesSplit은 시간 순서를 유지합니다.

첫 번째 폴드에서는 130일로 학습, 3140일로 검증. 두 번째 폴드에서는 140일로 학습, 4150일로 검증.

이런 식으로 점점 학습 데이터가 늘어납니다. 튜닝할 때 주의할 점이 있습니다.

첫째, 시간입니다. 조합이 많으면 며칠이 걸릴 수도 있습니다.

먼저 적은 epoch로 빠르게 탐색하고, 좋은 후보를 찾으면 그때 더 오래 학습합니다. 둘째, 과적합입니다.

튜닝도 일종의 학습입니다. 검증 데이터에 맞춰서 파라미터를 고르면, 정작 새 데이터에서는 성능이 떨어질 수 있습니다.

최종 테스트 데이터는 튜닝 과정에서 절대 보지 않아야 합니다. 김개발 씨가 하루 동안 튜닝을 돌렸습니다.

결과를 보니 units=64, window_size=14, dropout=0.2가 가장 좋았습니다. "튜닝 결과를 기록해두는 게 좋아요.

나중에 왜 이 파라미터를 선택했는지 근거가 되니까요."

실전 팁

💡 - 처음에는 넓은 범위를 탐색하고, 좋은 영역을 찾으면 그 주변을 더 세밀하게 탐색하세요

  • Keras Tuner나 Optuna 같은 라이브러리를 사용하면 더 효율적인 탐색이 가능합니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#LSTM#TimeSeries#DeepLearning#TensorFlow#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.