1. 코드의 목적과 역할 분석
과거 30일치 서울의 일평균 기온 시계열 데이터를 입력 (2015.01.01 ~ 2025.06.27)
SimpleRNN, LSTM, GRU 세 가지 순환 신경망 셀을 각각 학습
테스트 구간에 대한 다음 날 기온을 예측
RMSE(Root Mean Squared Error)로 모델 성능을 비교 후 시각화
2. 필수 라이브러리
sklearn.metrics.mean_squared_error : MSE 계산, RMSE 산출
tensorflow.keras.models.Sequential : 순차 모델 템플릿
tensorflow.keras.layers.SimpleRNN : 기본 순환 셀 구현
tensorflow.keras.layers.LSTM : 게이트 구조로 장기 의존성 학습 개선
tensorflow.keras.layers.GRU : LSTM 보다 경량화된 게이트 구조
tensorflow.keras.layers.Dense : 출력층 (회귀 예측값 1개)
3. 주요 기능 및 이론적 배경
1. 결측치 전처리
- fillna(method='ffill')로 앞선 값으로 결측 보간 → 시계열 누락 방지
- ffill : forward fill의 줄임말. 결측치가 있을 때 그 바로 이전 값을 그대로 채워넣는 방식 (시계열에서 주로 사용)
2. 슬라이딩 윈도우(make_dataset)
- 과거 30일(window) 데이터 → 다음 날 값 매핑
- 시계열 예측의 표준 기법
3. 훈련/테스트 분리
- 80%:20% 비율로 분할 → 과적합 방지, 일반화 성능 평가
4. RNN vs LSTM vs GRU
- SimpleRNN: 단순 순환 구조, 장기 기억 어려움(소실 기울기)
- LSTM: 입력/포겟/출력 게이트로 정보 흐름 제어 → 장기 의존성 보존
- GRU: 업데이트/리셋 게이트, 파라미터 절반 수준으로 경량화
5. 모델 학습/검증
- validation_split=0.2 → 내부 검증으로 과적합 모니터링
- loss='mse' → 회귀 특성에 맞는 손실함수
6. 성능 평가
RMSE = sqrt(MSE) → 원 단위 온도 오차 직관적 해석
- 오차를 제곱한 후 평균을 낸 MSE에 루트를 씌운 값이므로 RMSE의 단위는 실제 예측하려는 값과 동일한 단위를 갖는다.
- 주로 기온, 에너지 소비량, 주가 등 단위를 명확히 알고 있어 직관적 이해가 필요할 때 사용
( + MSE는 상 Gradient Descent 같은 최적화 알고리즘과 호환이 좋아, 주로 모델 내부 학습에 쓰임, 평가지표로는 직관성이 떨어져 잘 안씀 )
4. 코드 해석
# recurrent_ex.py (완료)
# 순환신경망 실습
# 주제 : 기상청 데이터를 활용한 일일 평균기온 예측
# 데이터 : 기상자료개방포털에서 서울의 일최고기온/일평균기온 CSV파일 다운로드
# 예측목표 : 과거 30일간의 기온데이터를 이용해 다음 날의 기온을 예측
# 날짜, 평균기온 CSV파일을 사용
# 라이브러리
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import mean_squared_error
from tensorflow.keras.models import Sequential # 순차 모델 템플릿
from tensorflow.keras.layers import SimpleRNN, LSTM, GRU, Dense # RNN/LSTM/GRU 셀, 출력층(Dense)
# 1. 기상자료개방포털에 있는 서울_일평균기온.csv 로딩
df = pd.read_csv('assets/seoul_temp.csv', encoding='utf-8')
df['date'] = df['date'].astype(str).str.strip() # 문자열 앞뒤 공백 제거
df = df[df['date'] != ''] # 빈 문자열 제거
df = df.dropna(subset=['date']) # 결측값 제거
df['date'] = pd.to_datetime(df['date'], errors='coerce') # 에러가 나면 NaT로 처리
df = df.dropna(subset=['date']) # NaT 제거
df = df.sort_values('date') # 날짜 순으로 정렬
# 2. 기온 데이터 전처리
temp = df['avg_temp'].fillna(method='ffill').values # 결측치 앞 값으로 채움
# 3. 시계열 입력/출력 만들기
def make_dataset(data, window=30):
X, y = [], []
for i in range(len(data) - window):
X.append(data[i:i+window]) # 과거 window 일치 입력
y.append(data[i+window]) # 바로 다음 날 출력
return np.array(X), np.array(y)
X, y = make_dataset(temp, window=30)
X = X.reshape((X.shape[0], X.shape[1], 1)) # (샘플 수, 30, 1)
# 4. 훈련/테스트 나누기
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
# 5. 모델 빌딩 (구성) 함수
# 아무런 하이퍼파라미터 튜닝 없이 베이스라인 모델로 구성 (모델 성능 비교, 검증할 때 기준점으로 삼음)
# 튜닝 가능한 하이퍼파라미터 : 유닛수, 레이어 깊이, 드롭아웃/정규화, 학습률, 배치 사이즈/에포크 수, 할성화 함수 변경, 조기종료, 모델 체크포인트
def build_model(cell_type='RNN'):
model = Sequential()
if cell_type == 'RNN':
model.add(SimpleRNN(32, input_shape=(30, 1))) # 셀 유닛수 32, 레이어 1개
elif cell_type == 'LSTM':
model.add(LSTM(32, input_shape=(30, 1)))
elif cell_type == 'GRU':
model.add(GRU(32, input_shape=(30, 1)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')
return model
# 6. 세 가지 모델 학습 및 예측
models, preds, rmses = {}, {}, {} #변수 3개를 빈 딕셔너리로 초기화
# 왜 딕셔너리? :각각의 모델·예측·RMSE 값을
# models['LSTM'], preds['GRU'], rmses['RNN'] 처럼
# 문자열 키로 바로 꺼내 쓰기 위해서.
# 리스트로 받으면 숫자 인덱스를 써야 해서 가독성이 떨어지기 때문
for cell in ['RNN', 'LSTM', 'GRU']:
print(f"▶ {cell} 모델 학습 시작...")
m = build_model(cell)
m.fit(X_train, y_train, epochs=20, batch_size=32, validation_split=0.2, verbose=0)
pred = m.predict(X_test).flatten()
models[cell] = m
preds[cell] = pred
rmses[cell] = np.sqrt(mean_squared_error(y_test, pred))
print(f"{cell} 모델 RMSE: {rmses[cell]:.3f}")
# 7. 결과 시각화 (x축을 날짜로 지정)
plt.rc("font", family="Malgun Gothic")
plt.figure(figsize=(14, 6))
# 테스트 구간에 해당하는 날짜 생성
test_dates = df['date'].values[-len(y_test):]
plt.plot(test_dates, y_test, label='실제 기온', color='black') # x축에 날짜 사용
for cell in preds:
plt.plot(test_dates, preds[cell], label=f'{cell} 예측')
plt.title('기상청 기온 예측: RNN vs LSTM vs GRU')
plt.xlabel('날짜')
plt.ylabel('평균기온 (℃)')
plt.xticks(rotation=45) # 날짜 라벨 회전
plt.legend()
plt.grid()
plt.tight_layout()
plt.show()
# 8. RMSE 출력
print("\n 모델별 RMSE (Root Mean Squared Error)")
for cell in rmses:
print(f"{cell}: {rmses[cell]:.3f}")

24/24 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step
RNN 모델 RMSE: 2.972
24/24 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step
LSTM 모델 RMSE: 2.493
24/24 ━━━━━━━━━━━━━━━━━━━━ 0s 7ms/step
GRU 모델 RMSE: 2.513
5. 결과 해석
1. RMSE 비교
최저 RMSE: LSTM (2.493℃)
그 다음이 GRU (2.513℃), SimpleRNN이 가장 높음 (2.972℃)
=> LSTM이 세 모델 중 테스트 구간에서 오차가 가장 작아 다음날 기온 예측 과제에선 LSTM이 최적 선택임.
2. 예측 곡선 패턴 분석
(1) 추세 추종력
- 세 모델 모두 장기적인 계절 변화(봄→여름→가을→겨울) 패턴은 잘 따라가지만,
- SimpleRNN(파란선)은 급격한 기온 등락 구간에서 반응이 느리고 평탄화 되는 경향이 강해,
- LSTM(주황선)과 GRU(녹색선)이 훨씬 빠르게 변동 폭을 좁히며 실제 곡선(검은선)에 더 밀착
(2) 극단값 처리
- 겨울철 영하권 일변동이나, 한여름 고온(30℃ 이상) 구간에서
- SimpleRNN은 과소/과대 예측 폭이 크고
- LSTM이 가장 정확하게 피크(고온)와 트로프(저온)를 재현
- GRU는 LSTM보다 약간 과소평가되는 경향이 있다,
Q. 7월 ~ 10월까지 실제 기온을 모든 모델이 거의 추종하지 못한 이유?
원인분석 1. MSE 손실함수의 평탄화 경향
- MSE를 최소화할 때 모델은 극단값(30℃ 이상)보다 평범한 값(20~25℃) 쪽으로 손실을 줄이는 방향으로 “안전하게” 예측하려고 한다.
- 여름 피크 구간에서는 고온을 과소 예측하고, 가을 초입의 서서히 떨어지는 패턴도 잘 따라가지 못하는 “평균 회귀(regression to the mean)” 현상이 나타남.
원인분석 2. 외부 입력 부재
- 단순히 온도만 사용하면, 예컨대 장마철 습도 급등·폭염·찬 대륙 고기압 등 기상 환경 변화를 반영하기 어려움
- 습도·바람·일조량·강수량 같은 외부 피처를 넣거나, 달력 변수(월, 요일)·Fourier 계절성 항을 추가하면 7~10월 변화가 더 잘 잡힐 것으로 예상됨.
(3) 시계열 의존성 학습
- SimpleRNN은 긴 시퀀스(30일) 정보를 학습할수록 소실 기울기(vanishing gradient) 문제로 과거 정보를 충분히 활용하지 못해,
- LSTM/GRU의 게이트 구조는 과거 30일치 히스토리를 더 잘 보존해 “갑작스러운 일교차”를 포착하는 데 유리하다.
3. 실무적 시사점
1. 모델 선택
- 비교적 짧은 윈도우라도 장기 의존성을 요구하는 기온 예측에는 LSTM이 최적
- GRU는 파라미터가 더 적으므로 딥러닝 리소스가 제한적일 때 대안으로 활용해보기
2. 더 긴 윈도우 (년 주기 히스토리) 월, 일과 같은 계절성 인덱스를 추가 입력해보기
3. MSE 대신 극단값에 강한 Huber Loss 사용해보기
6. 코드 개선 및 발전 아아디어
< 주요 개선 포인트 >
1. 윈도우 연장 : 30일 -> 365일
2. 계절성 인코딩 : dayofyear → sin/cos 피처 추가
3. Loss 함수: MSE 대신 극단값에 강한 Huber Loss
- Huber Loss : MSE와 MAE의 장점을 동시에 취하면서 극단값에 강건하게 설계된 손실함수
- 작은 오차 영역에선 이차함수로 부드러운 기울기 제공, 큰 오차 영역에선 일차함수로 완만한 기울기 제공
4. 모델 깊이: 2개 LSTM 레이어 + Dropout
5. EarlyStopping: 검증 손실 기반 자동 학습 중단
* Google Colaboratory의 T4 GPU 가속 환경을 활용해 실행함.
gpus = tf.config.list_physical_devices('GPU')
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
# 1) 나눔폰트 설치
!sudo apt-get update -qq
!sudo apt-get install -y fonts-nanum
# 필요한 라이브러리를 불러옵니다.
import pandas as pd # CSV 로딩 및 데이터 전처리
import numpy as np # 수치 연산 라이브러리
import matplotlib.pyplot as plt # 데이터 시각화 라이브러리
from sklearn.preprocessing import MinMaxScaler # 데이터를 0~1 사이로 정규화
from sklearn.metrics import mean_squared_error # 모델 성능 평가 (RMSE 계산)
from tensorflow.keras.models import Sequential # 신경망 모델을 순차적으로 구성
from tensorflow.keras.layers import LSTM, Dropout, Dense # LSTM 레이어, 과적합 방지 Dropout, 최종 출력층
from tensorflow.keras.callbacks import EarlyStopping # 과적합 방지 콜백 함수
# 2. CSV파일 데이터 로드 및 정제
df = pd.read_csv('assets/seoul_temp.csv', encoding='utf-8') # CSV 데이터 불러오기
df['date'] = df['date'].astype(str).str.strip() # 날짜 열의 문자열 앞뒤 공백 제거
df['date'] = pd.to_datetime(df['date'], errors='coerce') # 문자열을 날짜 형식으로 변환 (실패하면 NaT)
df = df.dropna(subset=['date', 'avg_temp']) # 날짜나 평균기온이 없는 행 제거
df = df.sort_values('date').reset_index(drop=True) # 날짜순으로 오름차순 정렬
# 3. 날짜의 계절적 주기를 인코딩 (sin, cos 변환)
df['dayofyear'] = df['date'].dt.dayofyear # 연중 날짜(1~365)를 얻음
df['sin_doy'] = np.sin(2 * np.pi * df['dayofyear'] / 365) # 주기적 특성 표현을 위해 사인 변환
df['cos_doy'] = np.cos(2 * np.pi * df['dayofyear'] / 365) # 주기적 특성 표현을 위해 코사인 변환
# 4. 결측된 기온 데이터를 보간하고 정규화
df['avg_temp'] = df['avg_temp'].interpolate() # 결측된 기온 데이터를 선형 보간하여 채움
scaler = MinMaxScaler() # 데이터 정규화 도구 생성
df['temp_scaled'] = scaler.fit_transform(df[['avg_temp']]) # 평균기온을 0~1 사이로 정규화
# 5. 시계열 데이터 생성을 위한 슬라이딩 윈도우 (길이=365, 특징=3개)
window = 365 # 입력으로 쓸 기간을 365일(1년)로 설정
features = ['temp_scaled', 'sin_doy', 'cos_doy'] # 사용할 특징(피처)
data = df[features].values # 지정된 피처를 numpy 배열로 변환
X, y = [], []
for i in range(window, len(data)): # 슬라이딩 윈도우 방식으로 데이터 생성
X.append(data[i-window:i]) # 과거 365일 데이터를 입력으로 추가
y.append(data[i, 0]) # 366일째 날의 정규화된 기온을 예측 타깃으로 추가
X = np.array(X) # 리스트를 numpy 배열로 변환
y = np.array(y) # 리스트를 numpy 배열로 변환
# 6. 데이터를 훈련용과 테스트용으로 분할 (8:2 비율)
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:] # 입력 데이터를 훈련/테스트로 분리
y_train, y_test = y[:split], y[split:] # 출력 데이터를 훈련/테스트로 분리
# 7. LSTM 모델 구성 (LSTM 2개층 + Dropout + Huber Loss + EarlyStopping)
model = Sequential()
model.add(LSTM(64, return_sequences=True, input_shape=(window, len(features)))) # 첫번째 LSTM층 (시퀀스 출력)
model.add(Dropout(0.2)) # 20% 노드를 무작위로 제거하여 과적합 방지
model.add(LSTM(32)) # 두번째 LSTM층 (단일 출력)
model.add(Dropout(0.2)) # 추가적 과적합 방지
model.add(Dense(1)) # 최종 출력층 (기온 예측)
model.compile(optimizer='adam', loss='huber') # 옵티마이저 adam, 손실함수 huber 설정
early_stop = EarlyStopping(
monitor='val_loss', # 검증 손실이 더 이상 개선되지 않으면 멈춤
patience=10, # 10회까지 기다림
restore_best_weights=True # 최적 가중치로 복원
)
history = model.fit(
X_train, y_train, epochs=100, batch_size=32,
validation_split=0.2, callbacks=[early_stop], verbose=1 # 훈련 및 과적합 방지 조건 설정
)
# 8. 테스트 데이터 예측 후 역정규화
y_pred_scaled = model.predict(X_test).flatten() # 모델이 예측한 스케일링된 기온값
y_pred = scaler.inverse_transform(y_pred_scaled.reshape(-1,1)).flatten() # 실제 기온 단위로 역정규화
y_true = scaler.inverse_transform(y_test.reshape(-1,1)).flatten() # 실제 기온값도 역정규화
# 9. 모델 평가(RMSE) 및 결과 시각화
rmse = np.sqrt(mean_squared_error(y_true, y_pred)) # 모델 성능 RMSE 계산
print(f"개선 모델 RMSE: {rmse:.3f} ℃") # RMSE 출력
# 예측값과 실제값을 그래프로 시각화
plt.figure(figsize=(12,5))
test_dates = df['date'].values[-len(y_true):] # 테스트 기간의 날짜 얻기
plt.plot(test_dates, y_true, label='실제 기온', color='black') # 실제 기온 그리기
plt.plot(test_dates, y_pred, label='개선 모델 예측', color='tab:orange') # 예측 기온 그리기
plt.title('계절성 인코딩 + 긴 윈도우 LSTM 예측 결과')
plt.xlabel('날짜'); plt.ylabel('평균기온 (℃)')
plt.xticks(rotation=45); plt.legend(); plt.grid(); plt.tight_layout()
plt.show()

개선 LSTM 모델 RMSE: 2.194℃
1. 성능 개선 확인
기존 2.493 -> 개선 2.194
약 12% 수준의 오차 감소
2. 그래프 패턴 분석
(1) 여름철 고온(7~8월) 피크
기존 모델은 고온 구간에서 ±2℃ 이상 과소 예측 경향이 있었으나, 개선 모델은 실제 곡선(검은선)에 더 밀착되어 30℃를 상회하는 고온 피크가 뭉개지지 않고 재현됨
(2) 가을(9~10월) 완만한 하강
과거 30일만 본 모델은 갑작스러운 하강에 민감하지 못했으나, 연간 주기(365일) 윈도우+사인·코사인 계절성 피처 덕분에 9~10월 온도 변동의 완급 조절을 더 자연스럽게 포착
3. 배운 점 및 핵심 포인트 정리
계절성 처리: 연간 주기를 명시적으로 인코딩하면 기온 예측력 대폭 개선
윈도우 크기: 시계열 창 길이가 짧으면 장기 패턴을 놓치고, 너무 길면 노이즈가 많아지므로 도메인에 맞춰 조정
손실함수 선택: MSE vs Huber 등 목적에 맞는 손실 선택이 결과에 큰 영향
모델 구조: 단일 층보다 다층·정규화 조합이 복합 패턴 학습에 유리
자동 중단: 검증 성능 기반 EarlyStopping으로 과적합 방지 및 학습 효율화
+ ) Q. Epoch 수 차이 때문에 모델 성능이 좋아진 건 아닐까?
베이스라인 모델은 20, 개선 모델은 100
개선 모델은 모델 복잡도 증가로 20 Epoch만으로는 충분히 수렴하지 못하고 과소적합 될 수 있으므로, 베이스라인 모델을 100 Epoch로 수정 후 재실행
모델별 RMSE (Root Mean Squared Error)
RNN: 2.691
LSTM: 2.362
GRU: 2.351
진짜 구조 개선이 성능을 끌어올리는 것을 확인함.
'Data Analysis Study' 카테고리의 다른 글
| TensorFlow로 구현한 Self-Attention 미니 트랜스포머 실습 (9) | 2025.08.11 |
|---|---|
| Transformer 자연어 처리 모델 개념 및 원리 (3) | 2025.08.03 |
| LSTM모델을 이용한 삼성전자 주가 예측 (시계열 예측) (3) | 2025.07.30 |
| RNN, LSTM, GRU 세 가지 순환 신경망 성능 비교 (IMDB 리뷰 데이터 셋) (3) | 2025.07.30 |
| IMDB 영화 리뷰 데이터셋 EDA (3) | 2025.07.29 |