본문 바로가기

딥러닝

LSTM을 이용한 저수율 예측

몇달 전 간단한 저수율 예측 모델을 만들어 본 적 있었다. 물론 결과는 좋지 않았지만 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