ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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
Designed by Tistory.