본문 바로가기

AI

LSTM 예측 - BEMS 시계열 데이터

RNN, LSTM, GRU, Transformer Model에 대해 살펴보고, 

LSTM과 GRU를 통해 주가 데이터 예측을 진행해 보았다.

이제 BEMS(Building Energy Management System)의 실제 Data set에 적용해보자.

 

시드 고정 및 GPU장비 설정

import os
import random

# 시드 값 고정
seed = 42

# Python 내장 해시 함수의 시드를 고정하여 재현성을 확보 (Python 3.3 이상에서만 적용)
os.environ['PYTHONHASHSEED'] = str(seed)

random.seed(seed) # Python의 random 모듈
np.random.seed(seed )# NumPy의 난수 생성기
torch.manual_seed(seed) # PyTorch의 난수 생성기

torch.cuda.manual_seed(seed) #GPU의 난수 생성기
torch.backends.cudnn.deterministic = True # CuDNN을 사용할 때, 비결정론적 알고리즘을 사용하지 않도록 설정
torch.backends.cudnn.benchmark = False # CuDNN의 벤치마크 기능을 비활성화하여 매번 동일한 연산 경로가 사용되도록 설정

# GPU가 사용 가능하면 CUDA 장치로 설정, 그렇지 않으면 CPU로 설정
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 설정된 장치(device) 출력
device

 

 

데이터 준비 

BEMS관련 논문에서 제공해주는 dataset을 사용해보겠다.

3년간의 사무실 건물에서의 에너지 소비 정보를 제공해주고 있으며, 

그 중 zone_022의 co2 column의 값들을 사용할 예정이다. (약 1년반간의 데이터)

 

EDA 및 이상치 처리

데이터를 확인해본 결과 date가 끊겨있는 부분을 발견할 수 있었다. 

 

끊겨 있는 부분의 수: 1 date 2020-04-27 01:50:00 116 days 09:51:00 Name: date, dtype: timedelta64[ns]

결측치 수 확인 zone_022_co2 0 dtype: int64

 

date 자체가 건너뛰어져있기에 결측치로는 잡히지 않았다. 

이대로 분석을 진행해보기도 하고, 2020년 5월1일 이후의 (끊기지 않은) 데이터로도 진행하였다.

 

이상치의 값들이 꽤 많은 것을 확인 할 수 있었으며, 400중반에 머물러져있는 데이터와 달리

72, 853 정도의 값들을 확인해 볼 수 있었다.

 

Z-score > 3 을 이상치로 판단  : 이상치 수: 21155 이상치 비율: 5.81%

제거 / 이전 값으로 대체 시도  => 이상치 수: 9895 이상치 비율: 2.72%

 

정규화 - MinMaxScaler()

정규화와 표준화

정규화 : 0~1 사이의 값으로 데이터 범주를 정해주는 작업.

표준화 : 평균을 0으로, 표준편차를 1로 변환해주는 작업.

  • 표준화가 아닌 정규화를 해주는 이유
    시그모이드 활성화 함수와 함께 사용하기 용이
    시계열 데이터 예측에서는 데이터의 상대적 크기를 유지할 수 있어 정규화를 보다 선호
from sklearn.preprocessing import MinMaxScaler

# 데이터 로드 및 전처리 (데이터셋 나누기 전에 수행하는 것이 좋음!)
def load_data_from_csv(df, column_name):
    data = df
    data = data[column_name].values.reshape(-1, 1)

    scaler = MinMaxScaler()
    scaled_data = scaler.fit_transform(data)

    return scaled_data, scaler

data, scaler = load_data_from_csv(df, column_name)

MinMaxScaler( ) 인자

  • feature_range: 변환할 범위를 설정. Default (0, 1)
  • copy: 원본 데이터를 변경하지 않고 사본을 반환할지 설정. Default : True
  • clip: 정규화된 데이터가 설정된 범위를 벗어날 경우, 그 값을 범위 경계값으로 대체할지 설정. Default : False

그 외 함수 : MaxAbsScaler, StandardScaler, RobustScaler

  • 정규화 이전에 이상치 처리를 해주어야 하는 이유
    이상치가 존재하면 최소,최댓값을 잘못 설정할 확률이 높음. ⇒ 이상치에 민감
  • scaling을 미리 해주는 이유
    훈련/테스트 데이터 셋에 대해 각각 fit_transform( )을 진행할 경우,
    fit ( )의 정도가 달라질 수 있음 ! ⇒ 테스트셋에는 transform( )만 적용
    이러한 과정이 복잡하기에, 가능하다면 데이터 셋 분리 전 미리 스케일링 적용
  • 이러한 전처리과정이 ML에서 중요한 이유
    모델이 잘못된 데이터로 학습 하지 않도록 해주며, 학습 속도 및 예측 성능의 향상이 가능

⚠️ 트리 기반 모델에서는 피쳐 스케일링이 필요하지 않음.

데이터의 크기보다는 대소 관계에 영향을 받기 때문

 

Pytorch - DataSet(), DataLoader()

# 데이터셋 클래스 정의
class Co2Dataset(Dataset):
    def __init__(self, data, window_size):
        self.data = data
        self.window_size = window_size

    def __len__(self):
        return len(self.data) - self.window_size

    def __getitem__(self, idx):
        x = self.data[idx:idx+self.window_size]
        y = self.data[idx+self.window_size]
        return torch.tensor(x, dtype=torch.float32), torch.tensor(y, dtype=torch.float32)

Dataset : PyTorch에서 데이터셋을 표현하기 위해 사용되는 추상 클래스, 여러 데이터셋 제공

  • init( ) : 데이터셋의 전처리 작업이나 데이터를 읽어오는 등의 초기화를 수행
  • len( ) : 데이터셋의 크기(샘플 수)를 반환
  • getitem( ) : 주어진 인덱스에 해당하는 데이터를 반환

❔ Tensor 란?

Pytorch 에서 모델의 입출력에 사용하는 NumPy의 ndarray와 유사한 자료구조

❔ window_size 란?

시계열 데이터 처리 시, 슬라이딩 윈도우(sliding window) 방식으로 분할하여 학습하기 위한 변수

 

# 데이터셋 생성
window_size = 10
dataset = Co2Dataset(data, window_size)

# 훈련/검증 데이터 분리 (75% 훈련 데이터, 25% 테스트 데이터)
train_size = int(len(dataset) * 0.75)

# 학습과 테스트 데이터셋을 슬라이싱으로 나누기
train_dataset = Subset(dataset, list(range(train_size)))
test_dataset = Subset(dataset, list(range(train_size, len(dataset))))

# 데이터 로더 생성
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)

 

  • DataLoader( )데이터셋의 순서를 섞거나(shuffle), 병렬 처리를 통해 데이터를 로드하는 등의 기능도 제공
  • batch_size : 2^n 권장
  • 데이터셋을 배치(batch) 단위로 나누어 모델에 제공할 수 있도록 도와주는 클래스

LSTM 모델 설계

torch.nn.Module ⇒ RNNBase ⇒ torch.nn.LSTM

Class LSTM - Init()

class LSTMModel(nn.Module):
    def __init__(self, input_size=1, hidden_size=50, num_layers=2, output_size=1, dropout=0.2): # LSTM 전체 모델 정의
        super(LSTMModel, self).__init__() # Moudle(부모 클래스의 생성자 호출
        self.hidden_size = hidden_size
        self.num_layers = num_layers

        # nn.LSTM을 통해 LSTM 레이어 정의
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout)

        # 출력 크기 조정 (FC레이어 = 선형 변환 레이어 정의)
        self.fc = nn.Linear(hidden_size, output_size)

        # 가중치와 편향을 초기화
        self._initialize_weights()

    def _initialize_weights(self):
        for name, param in self.lstm.named_parameters():
            if 'weight' in name:
                nn.init.xavier_uniform_(param.data)
            elif 'bias' in name:
                nn.init.zeros_(param.data)
        nn.init.xavier_uniform_(self.fc.weight)
        nn.init.zeros_(self.fc.bias)

    def forward(self, x): # x는 입력 데이터 (하나의 batch)
        # 초기 은닉 상태와 셀 상태 정의
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)

        # LSTM 통과
        out, _ = self.lstm(x, (h0, c0))

        # 마지막 시퀀스의 출력만 선택하여 FC 레이어에 통과
        out = self.fc(out[:, -1, :])
        out = torch.sigmoid(out)  # 시그모이드 활성화 함수 추가

        return out

 

  • input_size: 입력 x에서 기대되는 특징의 수
  • hidden_size: 숨겨진 상태 h에서의 특징 수 (50 단위)
  • num_layers: 순환 레이어의 수. 기본값: 1 (1~2)
  • dropout: 마지막 레이어를 제외한 각 LSTM 레이어의 출력에 Dropout 레이어를 도입하며, 드롭아웃 확률 값 설정. 기본값: 0 (0.2)
  • 그 외
    • bias: False로 설정하면, 해당 레이어는 b_ih와 b_hh라는 편향 가중치를 사용하지 않습니다. 기본값: True (4차원의 input_hidden, hidden_hidden간의 편향성)
    • batch_first: True로 설정하면, 입력과 출력 텐서가 (batch, seq, feature) 형식으로 제공되며,기본 형식인 (seq, batch,feature) 대신 사용됩니다. 이는 숨겨진 상태나 셀 상태에는 적용되지 않습니다. 기본값: False
    • bidirectional: True로 설정하면, 양방향 LSTM이 됩니다. 기본값: False
    • proj_size: 0보다 크면, 해당 크기의 프로젝션이 있는 LSTM을 사용하게 됩니다. (차원축소) 기본값: 0

가중치 초기화 함수와 활성화 함수 간 궁합이 잘 맞는 쌍이 존재

He - ReLu, xavier - sigmoid... 

 

모델 학습

# 모델 학습 함수
def train_model(train_loader, num_epochs=5, learning_rate=0.001, device=device):
    model = LSTMModel().to(device)
    criterion = nn.MSELoss() #손실 함수
    optimizer = optim.Adam(model.parameters(), lr=learning_rate) #최적 가중치를 찾아주는 알고리즘

    for epoch in range(num_epochs):
        model.train()
        epoch_loss = 0

        for seqs, targets in train_loader:
            seqs, targets = seqs.to(device), targets.to(device)
            optimizer.zero_grad()
            outputs = model(seqs)
            loss = criterion(outputs, targets)
            loss.backward()
            optimizer.step()
            epoch_loss += loss.item()

        avg_loss = epoch_loss / len(train_loader)
        print(f'Epoch {epoch+1}/{num_epochs}, Loss: {avg_loss:.4f}')

    return model
print(device)

 

➕ 학습에서 사용되는 파라미터

ephocs : 10~100 (데이터 셋의 크기가 클수록 적게 설정)

learning_rate : 0.001

 

이상치 이전 값으로 대체, 2020년 5월 이후의 데이터셋 사용

ephoc 찾기 (early stopping?)

예측 및 성능 평가

 

# 예측 함수
def predict(model, test_loader, scaler, device=device):
    model.eval()
    predictions = []
    actuals = []

    with torch.no_grad():
        for seqs, targets in test_loader:
            seqs, targets = seqs.to(device), targets.to(device)
            outputs = model(seqs)
            predictions.extend(outputs.cpu().numpy())
            actuals.extend(targets.cpu().numpy())

    predictions = scaler.inverse_transform(np.array(predictions).reshape(-1, 1))
    actuals = scaler.inverse_transform(np.array(actuals).reshape(-1, 1))

    return predictions, actuals

위에서 scaler를 return해주었던 이유. inverse_transform을 사용할 때 재사용하기 위함!

# 데이터를 720개씩 묶어서 평균을 계산하는 함수
def average_data(data, interval):
    # 데이터 길이를 interval로 나누어 묶음의 수를 결정
    data = np.array(data)
    averaged_data = [np.mean(data[i:i+interval]) for i in range(0, len(data), interval)]
    return averaged_data

# 720개마다 평균을 계산
interval = 720
averaged_actuals = average_data(actuals, interval)
averaged_predictions = average_data(predictions, interval)

# 시각화
plt.figure(figsize=(12, 6))
plt.plot(averaged_actuals, label='Averaged Actual Data')
plt.plot(averaged_predictions, label='Averaged Predicted Data')

# 데이터셋 분할점을 나타내는 빨간 점선 추가 (80:20 비율로 가정)
split_point = int(0.8 * len(averaged_actuals))  # 평균을 낸 데이터에서의 분할점
plt.axvline(x=split_point, color='red', linestyle='--', label='Train/Test Split')

# 범례 추가 및 시각화
plt.legend()
plt.show()

75%,25%로 비율을 바꾸었는데.. 빨간 점선표시에는 반영이 안되어있다.. ㅠ

MSE : 1102

RMSE : 33

MAE : 17

- 이상치에 따른 예측 결과 비교

MSE (Mean Squared Error) : 평균 제곱 오차. 예측값과 실제 값 간의 차이가 얼마나 되는가

RMSE : MSE에 Root를 씌워 원래의 단위로 다시 변환.

MAE : Mean Absolute Error : 평균 절대 오차, 모든 오차를 동일하게 취급하며, 이상치에 덜 민감

이상치 제거 - MSE : 150591232 RMSE : 12271 MAE : 8850 MSE : 162 RMSE : 12 MAE : 7

⚠️ 이상 탐지 시스템을 구축한다고 했을 때, 이상치들이 모두 과연 쓸모 없는 값일까…? 판단 필요
      어느 정도의 값까지를 이상치로 판단할 것인가...!

 

 

 

  1. ephoc:10, hidden_size = 50, num_layers = 2 , 학습 시간 : 15분 최종 loss: 0.0016
    MSE : 329 MAE :15 RMSE : 18
  2. ephoc : 20, hidden_size = 25, num_layers = 1 , 학습 시간 :: 33분 최종 loss: 0.0015
    MSE : 139 MAE : 8 RMSE : 11
  3. sigmoid활성화 함수 적용, ephoc: 5, hidden_size:50, num_layers ; 2 학습 시간: 5분 최종 loss: 0.0016 
    MSE : 193 MAE : 10 RMSE : 13

 


 

BEMS/MY_LSTM.ipynb at main · codrae/BEMS

Contribute to codrae/BEMS development by creating an account on GitHub.

github.com

 

 

Dryad | Data -- A three-year building operational performance dataset for informing energy efficiency

This dataset was curated from an office building constructed in 2015 in Berkeley, California, which includes whole-building and end-use energy consumption, HVAC system operating conditions, indoor and outdoor environmental parameters, and occupant counts.

datadryad.org

참고 논문 : https://www.nature.com/articles/s41597-022-01257-x#auth-Na-Luo-Aff1

 

'AI' 카테고리의 다른 글

CNN 이란?  (2) 2024.09.30
Local 원격저장소를 통해 DVC 사용하기  (0) 2024.08.21
Pytorch를 통한 주가 분석 (LSTM, GRU)  (0) 2024.08.20