-
LSTM을 이용한 저수율 예측딥러닝 2023. 9. 7. 14:21
몇달 전 간단한 저수율 예측 모델을 만들어 본 적 있었다. 물론 결과는 좋지 않았지만 LSTM 맛보기 하는 용도로 아주 괜찮았던 경험이었다. ^^
사용한 데이터는 저수율, 강수량, 평균기온 등 기상 정보이다.
특정 저수지에 대해 대략 10개년 간의 저수율 데이터가 필요했는데, 표 형태로 정리된 공공데이터가 존재하지 않아서 농촌용수종합정보시스템 (ekr.or.kr) 에서 하나하나 크롤링해서 가져왔다. 처음해보는 크롤링이라 코드가 비효율적이고 중구난방이었지만 어쨌든 성공했다. ㅎㅎ~ 크롤링 코드는 나중에 올리겠다.
기상정보는 기상청에 잘 정리돼 있어서 쉽게 얻을 수 있다.
라이브러리, 데이터 불러오기
import numpy as np import pandas as pd import random import torch import torch.nn as nn from sklearn.metrics import mean_squared_error from sklearn.preprocessing import MinMaxScaler from pandas import datetime from math import sqrt import math, time import itertools import datetime
df_seomjin = pd.read_csv('data.csv', encoding='CP949') df_seomjin.set_index('일시', inplace=True) df_seomjin=df_seomjin[['강수량(mm)', '평균기온(℃)', '일교차', '평균습도(%rh)', '평균풍속(m/s)', '섬진 저수율']]
나는 섬진 저수지를 특정하여 총 3811일에 해당하는 데이터를 수집했다.
scaler = MinMaxScaler() for i in range(0, len(df_seomjin.columns)): df_seomjin[df_seomjin.columns[i]] = scaler.fit_transform(df_seomjin[df_seomjin.columns[i]].values.reshape(-1,1))
데이터의 각 열의 scale이 굉장히 상이하므로 MinMaxScaler를 통해 scale을 맞춰주었다.
def load_data(df, window): data_raw = df.values data = [] for index in range(len(data_raw) - window): data.append(data_raw[index: index + window+1]) data = np.array(data) test_set_size = int(np.round(0.002*data.shape[0])) train_set_size = data.shape[0] - (test_set_size) x_train = data[:train_set_size,:-1,:] y_train = data[:train_set_size,-1,5:6] x_test = data[train_set_size:,:-1] y_test = data[train_set_size:,-1,5:6] return [x_train, y_train, x_test, y_test] window = 7 x_train, y_train, x_test, y_test = load_data(df_seomjin, window) print('x_train.shape = ',x_train.shape) print('y_train.shape = ',y_train.shape) print('x_test.shape = ',x_test.shape) print('y_test.shape = ',y_test.shape)
데이터를 전처리하는 과정이 생각보다 복잡하고 중요하다. 여기서 window(Look-back)라는 개념이 사용되는데, 미래 시점의 값을 예측하기 위해 얼마나 먼 과거까지 look back 할 것인지를 나타낸다. 위 코드에서는 window=7으로 설정하였으므로, 지난 일주일 간의 데이터를 참조하여 내일의 저수율을 예측하는 것이다.
하이퍼파라미터가 늘어나서 귀찮긴 하지만 window size를 적절히 조절해가며 성능을 비교해야 한다. 또한 window size 값을 크게 설정할수록 연산량이 늘어난다는 것 역시 고려해야 한다.
Data shape을 살펴보면 → (행, window size, 열) 이다.
모델 설정하기
x_train = torch.from_numpy(x_train).type(torch.Tensor) x_test = torch.from_numpy(x_test).type(torch.Tensor) y_train = torch.from_numpy(y_train).type(torch.Tensor) y_test = torch.from_numpy(y_test).type(torch.Tensor)
# LSTM Model input_dim = 6 hidden_dim = 32 num_layers = 2 output_dim = 1 class LSTM(nn.Module): def __init__(self, input_dim, hidden_dim, num_layers, output_dim): super(LSTM, self).__init__() self.hidden_dim = hidden_dim self.num_layers = num_layers self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True) self.fc = nn.Linear(hidden_dim, output_dim) def forward(self, x): h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim).requires_grad_() c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim).requires_grad_() out, (hn, cn) = self.lstm(x, (h0.detach(), c0.detach())) out = self.fc(out[:, -1, :]) return out model = LSTM(input_dim=input_dim, hidden_dim=hidden_dim, output_dim=output_dim, num_layers=num_layers) loss_fn = torch.nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.01) print(model) print(len(list(model.parameters()))) for i in range(len(list(model.parameters()))): print(list(model.parameters())[i].size())
기본적으로 설정해주어야 하는 파라미터가 제법 된다. input_dim, hidden_dim, num_layers를 설정해주어야 하는데, 나처럼 LSTM 별로 안 써본 사람들은 어느정도로 설정해야 하는지 감이 안 온다. 그래서 초반에는 캐글 노트북 참고해서 쓰다가 나중에는 그냥 Optuna에 파라미터 범위 넣고 돌려버렸다.
# Train model num_epochs = 100 hist = np.zeros(num_epochs) for t in range(num_epochs): # Forward pass y_train_pred = model(x_train) loss = loss_fn(y_train_pred, y_train) if t % 10 == 0 and t !=0: print("Epoch ", t, "MSE: ", loss.item()) hist[t] = loss.item() # Zero out gradient, else they will accumulate between epochs optimizer.zero_grad() # Backward pass loss.backward() # Update parameters optimizer.step()
데이터가 3800개라 학습속도가 굉장히 빠르다.
Training loss를 찍어보면 다음과 같다.
plt.plot(hist, label="Training loss") plt.xlabel("Epoch") plt.ylabel("Loss") plt.title("Loss") plt.legend() plt.grid(True) plt.show()
이제 실제 값과 예측 값을 그려 비교해본다.
y_test_pred = model(x_test) y_train_pred = scaler.inverse_transform(y_pred_train.detach().numpy()) y_train = scaler.inverse_transform(y_train.detach().numpy()) y_test_pred = scaler.inverse_transform(y_test_pred.detach().numpy()) y_test = scaler.inverse_transform(y_test.detach().numpy()) trainScore = math.sqrt(mean_squared_error(y_train[:,0], y_train_pred[:,0])) print('Train Score: %.2f RMSE' % (trainScore)) testScore = math.sqrt(mean_squared_error(y_test[:,0], y_test_pred[:,0])) print('Test Score: %.2f RMSE' % (testScore))
100 epoch만 학습했음에도 나쁘지 않은 예측 성능을 보여준다. 하지만 이는 학습에 사용되는 data에 이미 y열을 포함했기 때문인 것 같다. (여러 논문들을 참조했는데, 미래의 저수율 예측을 위해 과거 저수율을 사용했기 때문에 문제 없으리라 판단)
# 결과 시각화 figure, axes = plt.subplots(figsize=(15, 6)) axes.xaxis_date() axes.plot(df_seomjin[:len(df_seomjin)-len(y_test)-7].index, train_data.iloc[:len(train_data)-7, 5].values, color = 'red', label = 'Real Water rate') axes.plot(df_seomjin[:len(df_seomjin)-len(y_test)-7].index, model(x_train).detach().numpy(), color = 'blue', label = 'Predicted Water rate') plt.title('Seomjin') plt.xlabel('Time') plt.ylabel('Somejin water rate') plt.legend() plt.xlim(0, 3800) plt.show()
아주 잘 예측하는 것을 볼 수 있다. 하지만 여기서 문제점이 한 가지 있는데, 내 task는 과거 저수율을 관측한 것을 토대로 미래 7일간의 저수율을 예측하는 것이었다. 이때 모델이 3800개의 과거 관측치를 학습했다면 3800+1번째의 저수율을 예측한다. 다시 말하면 내 task는 3800+n (n=1~7)을 예측하는 것이었고, 3800+k를 예측하기 위해서는 3800+(k-1) 시점의 저수율을 알고있어야 했다. n=1 (바로 다음 날)인 경우에는 위 그래프에서 보다시피 예측 값과 실제 값이 큰 차이를 보이지 않는다. 하지만 n=2,3,4 ... 로 넘어갈수록 실제 값과 멀어지는 것은 물론, 예측을 위해 미래의 기상정보 (강수량, 기온, 습도 등)를 수집해야 하는데 이 역시 기상청에서 "예측"한 값이지 실제 값이 아니므로 내 모델의 성능은 더욱 나락열차다.
위의 이유로 너무 복잡해져서 분석을 멈췄었다. 하지만 저수율 예측을 위해 LSTM 개념 강의를 수강했고 (사실 복습 안해서 기억 잘 안난다 ㅋ) 평생 안쓸 것 같던 RNN 툴을 한 번 써봤다는 것에 큰 의미를 둔다. 나중에 비슷한 프로젝트를 해 볼 날이 오겠지
'딥러닝' 카테고리의 다른 글
2. 분류와 회귀 (0) 2023.09.13 1. 순방향 신경망 (0) 2023.09.13 K means, 계층 군집화를 이용한 팀원 짜기 (0) 2023.09.12 PyTorch CNN 모델 Fast API로 배포하기 (0) 2023.09.07 🤗Roberta를 이용한 리뷰 감정분석 (0) 2023.09.07