- 개요
이제는 sin(x) 같은 단순한 함수가 아니라 하루 뒤 서울 평균기온을 예측해봅시다.
애초에 DNN 모델 자체를 설명하려고 작성하는 포스트가 아니라서 기온 예측 DNN 모델 코드를 만들다보니 '아 이거 설명 안했었네' 싶은 내용도 꽤 있네요. ㅠㅠ
D기온 예측 DNN 모델을 만들면서 하나씩 풀어가봅시다.
- 자료 분류하는 함수(split_data_scale) 설명
서울 ASOS 자료를 읽는 함수 read_ASOS108(), 필요한 열만 남기는 함수 process_cols()은 그대로 쓸 수 있지만 입력자료를 분류하는 split_data 함수는 쓸 수 없습니다.
그리고 DNN의 입력자료를 분리하는 함수는 좀 더 많은 전처리 작업을 해야합니다.
1. 학습, 검증, 테스트기간 분류
원래는 학습, 테스트기간으로 나눴지만 DNN은 학습, 검증, 테스트기간으로 나눠야합니다.
2. 스케일링
입력변수의 범위라든가 크기는 당연히 입력변수마다 다릅니다.
예를 들어 기온은 영하 15도~영상35도 범위를, 현지기압(해발고도 0 m라면)은 대충 990~1050 hPa 범위를 갖습니다.
현지기압의 평균값은 기온의 평균값보다 수십배 더 크죠.
이렇게 자료 간 범위, 크기의 차이가 크면 학습이 잘되지 않아 스케일링 작업을 해줘야합니다.
저는 최솟값을 0, 최댓값을 1로 만드는 스케일링 방법을 사용합니다.
3. tensor 타입으로 변경
입력 자료를 tensor 타입으로 변환해서 모델에 넣어야 합니다.
4. DataLoader 사용
저번 포스트에서 학습시 학습기간의 자료 전부 사용하지 않고 batch로 쪼개서 사용한다는 말을 했습니다.
batch 크기만큼 순서대로 자료를 넣는 코드를 짜도 되겠지만 pytorch에서는 이 작업을 편하게 만들어주는 DataLoader라는 함수를 제공합니다.
import torch
from torch.utils.data import DataLoader, TensorDataset
from sklearn.preprocessing import MinMaxScaler
def split_data_scale(df, date1, date2, batch_size=64):
datetime = df['datetime'][df['datetime'] >= date2]
"""
date1: 학습(train)과 검증(valid)을 나누는 기준 날짜
date2: 검증(valid)과 테스트(test)를 나누는 기준 날짜
"""
# 데이터 분할
train_df = df[df['datetime'] < date1].drop('datetime', axis=1)
valid_df = df[(df['datetime'] >= date1) & (df['datetime'] < date2)].drop('datetime', axis=1)
test_df = df[df['datetime'] >= date2].drop('datetime', axis=1)
# X, y 분리
train_X, train_y = train_df.drop('Y', axis=1), train_df['Y']
valid_X, valid_y = valid_df.drop('Y', axis=1), valid_df['Y']
test_X, test_y = test_df.drop('Y', axis=1), test_df['Y']
# MinMaxScaler 적용 (train_X 기준으로 fit)
scaler = MinMaxScaler()
train_X_scaled = scaler.fit_transform(train_X) # train에 fit 후 변환
valid_X_scaled = scaler.transform(valid_X) # train 기준으로 transform만 수행
test_X_scaled = scaler.transform(test_X)
# PyTorch Tensor 변환
train_X_tensor = torch.tensor(train_X_scaled, dtype=torch.float32)
valid_X_tensor = torch.tensor(valid_X_scaled, dtype=torch.float32)
test_X_tensor = torch.tensor(test_X_scaled, dtype=torch.float32)
train_y_tensor = torch.tensor(train_y.values, dtype=torch.float32).view(-1, 1)
valid_y_tensor = torch.tensor(valid_y.values, dtype=torch.float32).view(-1, 1)
test_y_tensor = torch.tensor(test_y.values, dtype=torch.float32).view(-1, 1)
# DataLoader 생성 (train/valid만 사용)
train_loader = DataLoader(TensorDataset(train_X_tensor, train_y_tensor), batch_size=batch_size, shuffle=True)
valid_loader = DataLoader(TensorDataset(valid_X_tensor, valid_y_tensor), batch_size=batch_size, shuffle=False)
return train_loader, valid_loader, test_X_tensor, test_y_tensor, datetime
- 자료 전처리 코드
이전에 작성한 함수 코드는 이전 포스트를 확인해주세요.
df_raw = read_ASOS108()
use_cols = ['일시', '평균기온(°C)', '평균 풍속(m/s)', '평균 이슬점온도(°C)']
# use_cols = ['일시', '평균기온(°C)', '일강수량(mm)', '평균 풍속(m/s)',
# '평균 이슬점온도(°C)', '평균 상대습도(%)',
# '평균 현지기압(hPa)', '평균 전운량(1/10)', '합계 일사량(MJ/m2)']
df_raw1 = process_cols(df_raw, use_cols)
"""
raw
"""
target = '평균기온(°C)'
df_raw1['Y'] = df_raw1[target].shift(-1)
df_raw1.drop(['일시'], axis=1, inplace=True)
df_raw1 = df_raw1.dropna()
# 계절성 없애고 예측할거면 deseasonalized 주석을 풀면됨
# """
# deseasonalized
# """
# df_raw1 = remove_seasonal_cycle(df_raw1, use_cols)
# target = '평균기온(°C)_detrended'
# df_raw1['Y'] = df_raw1[target].shift(-1)
# df_raw1 = df_raw1.iloc[:-1]
# use_cols_detrend = df_raw1.columns
# use_cols_detrend = [ col for col in use_cols_detrend if col not in use_cols]
# df_raw1 = df_raw1[use_cols_detrend]
# df_raw1 = df_raw1.dropna()
# 자료 전처리
train_loader, valid_loader, test_X, test_y, datetime = split_data_scale(df_raw1, '2019-01-01','2022-01-01')
- DNN 모델 코드
활성화함수를 tanh가 아닌 ReLU를 씁니다. 층이 적어보이긴 해도 학습은 잘됩니다.
맨 처음 nn.Linear에 "len(use_cols)+2"는 입력 자료의 차원입니다.
use_cols 리스트에서 '일시'가 빠지고 'year', 'month', 'day'의 3개 열이 추가되므로 +2개가 됩니다.
import torch.nn as nn
class DNN(nn.Module):
def __init__(self):
super(DNN, self).__init__()
self.network = nn.Sequential(
nn.Linear(len(use_cols)+2, 8),
nn.ReLU(),
nn.Linear(8, 4),
nn.ReLU(),
nn.Linear(4, 1),
)
def forward(self, x):
return self.network(x)
- DNN 모델 학습 코드
저번 DNN 모델 코드에서는 전체 학습기간 자료를 batch로 나눠서 사용하지 않았습니다.
batch마다 오차를 계산하고 가중치를 업데이트하게 바꿔야합니다.
최고로 성능이 좋은 모델을 best_model.pth라는 이름으로 저장합니다.
import torch.optim as optim
model = DNN()
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=50, factor=0.5)
# 모델 학습 함수 (DataLoader 사용)
def train_model(model, train_loader, valid_loader, epochs=300):
train_losses = []
valid_losses = []
best_valid_loss = float('inf')
best_model_path = 'best_model.pth'
for epoch in range(epochs):
model.train()
train_loss = 0.0
for batch_X, batch_y in train_loader:
optimizer.zero_grad()
outputs = model(batch_X)
loss = criterion(outputs, batch_y) # y의 차원 맞추기
loss.backward()
optimizer.step()
train_loss += loss.item()
train_loss /= len(train_loader)
model.eval()
valid_loss = 0.0
with torch.no_grad():
for batch_X, batch_y in valid_loader:
valid_outputs = model(batch_X)
loss = criterion(valid_outputs, batch_y) # y의 차원 맞추기
valid_loss += loss.item()
valid_loss /= len(valid_loader)
train_losses.append(train_loss)
valid_losses.append(valid_loss)
scheduler.step(valid_loss)
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), best_model_path)
print(f'Epoch {epoch}: New best model saved with valid_loss: {best_valid_loss:.4f}')
if epoch % 100 == 0:
print(f'Epoch {epoch}, Train Loss: {train_loss:.4f}, Valid Loss: {valid_loss:.4f}, LR: {optimizer.param_groups[0]["lr"]:.6f}')
return train_losses, valid_losses, best_model_path
# 모델 학습 실행
set_random_seed(42)
train_losses, valid_losses, best_model_path = train_model(model, train_loader, valid_loader)
# 모델 로드 및 테스트 예측
best_model = DNN()
best_model.load_state_dict(torch.load(best_model_path))
best_model.eval()
with torch.no_grad():
y_pred = model(test_X)
- 결과 확인
지금까지 작성한 코드를 차례대로 돌려서 먼저 학습오차, 검증오차를 확인합니다(train_losses, valid_losses를 그리면 됨).
epoch가 50도 되기 전에 학습오차, 검증오차가 크게 감소합니다.
최적의 모델은 검증오차가 가장 적은 모델이니 epoch 100 정도에서 학습을 멈춰도 큰 문제는 없겠네요.
예측 결과를 실제값과 비교해보니 상관계수는 0.978, RMSE는 2.25가 나옵니다.
선형회귀 모델, XGboost 모델과 비슷한 성능이 나오네요.
이렇게 보면 예측을 잘하고 있지만 확대를 해보면 여전히 오늘 기온을 다음날 기온으로 예측하고 있습니다.
선형회귀 모델, XGboost 모델을 쓸 때도 계속 이랬는데 DNN을 써도 어쩔수 없나봅니다.
- 랜덤시드 설정
DNN 모델의 예측 결과를 확인했지만 끝이 아닙니다.
다시 한 번 DNN 모델을 돌리면 상관계수와 RMSE값이 바뀝니다.
DNN 모델의 첫 가중치는 랜덤하게 정해지기 때문에 코드를 돌릴 때마다 달라집니다.
같은 자료를 쓰는데 학습할 때마다 예측 결과가 다르면 과제를 하거나 연구를 할 때 곤란하죠.
항상 같은 결과가 재현될 수 있도록 랜덤시드(random seed)를 설정해야합니다.
import random
def set_random_seed(seed):
random.seed(seed) # Python의 기본 랜덤 시드 설정
np.random.seed(seed) # NumPy의 랜덤 시드 설정
torch.manual_seed(seed) # PyTorch의 랜덤 시드 설정, CPU 사용
# CuDNN에서 사용하는 비결정적 알고리즘을 비활성화 (결정적 실행을 위해)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False # 성능 최적화를 위한 자동 튜닝 비활성화 (결정적 실행을 보장)
이 함수를 모델 선언 이전에 "set_random_seed(숫자)"라고 적으면 항상 같은 결과를 얻을 수 있습니다.
- DNN과 XGboost와 비교
XGboost 포스트이 마지막에 계절성 유무, 입력 변수의 변화에 따라 XGboost 모델의 상관계수, RMSE가 어떤지 확인했었습니다. DNN 모델의 상관계수, RMSE와도 비교해보죠
DNN 모델은 랜덤시드에 따라 성능이 바뀌므로 여러 랜덤시드로 모델을 만들어서 비교해야 합니다.
전 랜덤시드 0부터 9까지 10개의 모델에 대해 제일 큰 상관계수와 작은 RMSE를 표에 적겠습니다(같은 모델이 아닐 수 있음, 상관계수가 높다고 반드시 RMSE가 낮은 건 아님).
epoch는 100까지 돌립니다.
참고로 랜덤시드에 따라 학습이 아예 안되거나 epoch를 더 길게 줘야 검증오차가 충분히 작아지는 경우도 있습니다.
(공정한 비교를 위해선 XGboost도 랜덤시드를 바꿔주는 것처럼 파라미터값에 따라 상관계수, RMSE를 확인해야됩니다만 그냥 넘어가죠.)
DNN | XGboost | |||
실험 입력 자료 | 상관계수 | RMSE | 상관계수 | RMSE |
평균 기온 | 0.977 | 2.27 | 0.968 | 2.70 |
평균 기온, 평균 풍속, 평균 이슬점 온도 | 0.980 | 2.15 | 0.973 | 2.46 |
평균기온, 일강수량, 평균 풍속, 평균 이슬점 온도, 평균 상대습도, 평균 현지기압, 평균 전운량, 합계 일사량 | 0.980 (제일 큼) |
2.14 | 0.977 | 2.30 |
[계절성 제거] 평균 기온 | 0.743 | 2.08 | 0.616 | 2.62 |
[계절성 제거] 평균 기온, 평균 풍속, 평균 이슬점 온도 | 0.771 | 2.08 | 0.672 | 2.42 |
[계절성 제거] 평균기온, 일강수량, 평균 풍속, 평균 이슬점 온도, 평균 상대습도, 평균 현지기압, 평균 전운량, 합계 일사량 | 0.783 | 2.02 | 0.754 | 2.13 |
전반적으로 DNN 모델 성능이 XGboost 모델 성능보단 좋습니다.
소수점 넷째자리에서 반올림해서 0.980이 두 번 나왔지만 입력 자료를 가장 많이 쓴 모델의 상관계수가 제일 큽니다.
- 결론 및 다음 방향
DNN 모델이 XGboost보단 좋다.
DNN 모델에 입력 자료를 많이 쓰면 성능이 좋아진다.
이 다음 내용으로 삼을 만한 후보는
1. 시계열 처리에 적합한 LSTM을 사용
2. 서울 ASOS 자료 이외의 다른 자료를 입력 자료로 추가
이 정도가 있는 것 같네요.
'프로젝트 > 기계학습 기반 서울 기온 예측' 카테고리의 다른 글
[서울기온예측][pytorch][DNN 2] DNN으로 y=sin(x) 예측해보기, DNN 코드 이해하기 (0) | 2025.03.05 |
---|---|
[서울기온예측][pytorch][DNN 1] DNN이란? 학습하는 원리 (0) | 2025.02.28 |
[서울기온예측][XGboost 2] 서울 ASOS 자료 기반 기온 예측 (0) | 2025.02.25 |
[서울기온예측][XGboost 1] 의사결정나무, XGboost란? (0) | 2025.02.20 |
[서울기온예측][다중선형회귀모델 3] 예측 변수와 입력 변수 간 선형상관계수 확인 (0) | 2025.02.17 |