LSTM 학습과 추론, 모델 저장과 복원
1. 데이터 로드
import numpy as np
import pandas as pd
import random
df_weather = pd.read_csv('sample_weather_data.csv')
features = ['humidity','rainfall','wspeed','temperature']
df_weather = df_weather[features]
print(df_weather.head())
print(f"데이터 갯수 : {len(df_weather)}")
2. 난수 시드 설정
import torch
# 난수 시드 설정
def set_seed(seed_value):
random.seed(seed_value) # 파이썬 난수 생성기
np.random.seed(seed_value) # Numpy 난수 생성기
torch.manual_seed(seed_value) # PyTorch 난수 생성기
# CUDA 환경에 대한 시드 설정 (GPU 사용 시)
if torch.cuda.is_available():
torch.cuda.manual_seed(seed_value)
torch.cuda.manual_seed_all(seed_value)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
seed_value = 42
set_seed(seed_value) # 위에서 정의한 함수 호출로 모든 시드 설정
※ 코드 해석
torch.manual_seed(seed_value)를 포함하여 여러 라이브러리에 걸쳐 난수 시드를 설정하는 코드입니다. 이렇게 하면 모델의 가중치 초기화나 데이터셋의 분할 등 모델 학습과 관련된 모든 난수 생성 과정이 일정하게 유지됩니다. 이는 실험의 재현 가능성을 높이고 다른 설정(하이퍼파라미터, 모델 파라미터)이 변함에 따라 모델 성능에 얼마나 영향을 미치는지 분석할 수 있으므로, 모델을 개발하는 과정에 있어서 반드시 설정해야 하는 코드입니다.
- random.seed(seed_value): 파이썬 내장 난수 생성기의 시드를 설정합니다.
- np.random.seed(seed_value): 넘파이 난수 생성기의 시드를 설정합니다.
- torch.manual_seed(seed_value): 토치 난수 생성기의 시드를 설정합니다. GPU를 사용하는 경우에는 torch.cuda.manual_seed_all(seed_value)를 호출하여 모든 GPU에 대한 시드도 설정합니다.
- torch.backends.cudnn.deterministic = True: CUDA의 일부 연산을 결정적(비확률적)으로 만들어, 동일한 입력과 모델 파라미터에서 항상 동일한 출력이 나오도록 합니다. 이는 디버깅과 실험의 재현을 용이하게 해 줍니다.
3. LSTM 모델 클래스 정의 : 클래스 생성 및 초기화
import torch
import torch.nn as nn
class LSTMModel(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, output_size):
super(LSTMModel, self).__init__()
self.hidden_size = hidden_size
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size) # 출력 크기 조정을 위한 선형 레이어
# 모델 인스턴스 생성 및 사용
model_lstm = LSTMModel(input_size=4, hidden_size=10, num_layers=1,output_size=1)
model_lstm
LSTMModel(
(lstm): LSTM(4, 10, batch_first=True)
(fc): Linear(in_features=10, out_features=1, bias=True)
)
class LSTMModel(nn.Module): LSTMModel 클래스는 nn.Module을 상속받아, 파이토치의 기본 모듈 구조를 활용할 수 있게 합니다.
※ nn.Module을 상속받아 클래스를 정의하는 이유
파이토치의 모듈화된 구조를 활용하여 네트워크를 보다 체계적으로 구성하고, 재사용 및 확장이 용이하게 만들기 위함입니다. 이 구조를 통해 개발자는 다음과 같은 여러 이점을 얻을 수 있습니다.
1) 매개변수 관리 용이성
- nn.Module은 모든 하위 nn.Module 객체의 매개변수를 자동으로 추적합니다.
- 이로 인해 모델의 모든 가중치와 편향을 쉽게 접근하고, 반복할 수 있으며, 이는 학습 과정에서 매개변수를 업데이트할 때 매우 유용합니다.
2) 모듈화 및 코드 재사용
- 모델의 다양한 부분을 모듈로 분리함으로써, 동일한 레이어나 구성을 다른 모델에 쉽게 재사용할 수 있습니다.
- 예를 들어, 여러 모델에 공통적으로 사용되는 특정 종류의 블록(예: LSTM 블록)을 정의하고, 필요에 따라 반복적으로 사용할 수 있습니다.
3) 후크 기능
- nn.Module은 forward 및 backward 통과 시 특정 동작을 실행할 수 있는 후크(hooks)를 등록하는 기능을 제공합니다.
- 이를 통해 모델의 입력 또는 출력 데이터에 대한 로깅, 중간 계산의 저장, 기타 사용자 정의 작업을 쉽게 수행할 수 있습니다.
4) GPU 지원
- nn.Module을 사용하면 .to(device) 메서드를 통해 모델의 모든 매개변수를 쉽게 다른 장치(예: CPU에서 GPU)로 이동할 수 있습니다.
- 이는 대규모 데이터셋과 복잡한 모델을 효과적으로 학습시키는 데 필수적입니다.
※ super()함수
super()는 부모 클래스의 객체를 참조하는 함수입니다. 여기서 부모 클래스는 nn.Module이며, 이는 LSTMModel 클래스가 상속받는 클래스입니다.
super(LSTMModel, self).init() 호출은 nn.Module의 생성자를 호출합니다. 이는 LSTMModel 객체가 생성될 때 필요한 모든 초기화 작업을 nn.Module에서도 수행하도록 합니다.
※ 생성자 매개변수가 설정하는 것
생성자에서는 입력 크기(input_size), 은닉 상태의 크기(hidden_size), LSTM 층의 개수(num_layers), 출력 크기(output_size)를 매개변수로 받습니다.
※ LSTM 레이어 설정과 batch_first 옵션
self.lstm은 LSTM 네트워크를 정의합니다. batch_first=True는 입력 텐서의 첫 번째 차원이 배치 크기임을 나타냅니다. 만일 이 옵션을 설정하지 않으면 default값은 batch_first = False입니다. batch_first 옵션이 False로 설정 되어 있을때 LSTM 인스턴스가 기대하는 input data의 차원은 (시퀀스수, 배치수, 특성수) 였습니다.
만약, batch_first=True로 설정하면, LSTM 인스턴스가 기대하는 input data의 차원은 (배치수, 시퀀스수, 특성수) 입니다.
self.fc는 은닉 상태에서 최종 출력까지의 매핑을 담당하는 선형 레이어입니다. 이 레이어는 nn.LSTM을 통해 생성된 은닉 상태의 출력을 입력으로 받아, 특정 문제에 대한 예측 결과를 생성합니다. 예를 들어, '다음날의 기온'을 예측하는 경우, LSTM 네트워크는 시계열 데이터를 학습하여 은닉 상태를 생성하고, self.fc는 은닉 상태를 사용하여 최종적으로 다음날의 기온을 예측하는 값을 출력합니다.
self.fc()의 output_size는 LSTM 클래스는 통해 예측하고자 하는 결과의 유형에 따라 결정됩니다. 이 경우 '다음날의 기온'을 예측하기 위해 output_size는 1로 설정되며, 이는 기온 값을 하나를 출력한다는 의미입니다. 이렇게 설정함으로써, 모델은 시계열 데이터를 기반으로 하루 뒤 기온을 하나의 숫자 값으로 예측할 수 있습니다.
※ 결과 해석
실행 결과를 통해, 모델이 정상적으로 초기화되었음을 확인할 수 있습니다.
여기서 LSTM(4, 10, batch_first=True)는 입력 특성이 4개, 은닉 상태의 크기가 10인 LSTM 레이어를 나타냅니다.
Linear(in_features=10, out_features=1, bias=True)는 LSTM 레이어의 출력을 받아 1차원의 최종 출력으로 매핑하는 선형 레이어를 나타냅니다.
4. LSTM 모델을 클래스 정의 : init_hidden 함수
LSTM 모델을 효과적으로 사용하기 위해 초기 은닉 상태와 셀 상태를 초기화 하는 init_hidden 함수를 정의합니다.
import torch
import torch.nn as nn
class LSTMModel(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, output_size):
super(LSTMModel, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size) # 출력 크기 조정을 위한 선형 레이어
def init_hidden(self, batch_size):
h0 = torch.zeros(self.num_layers, batch_size, self.hidden_size)
c0 = torch.zeros(self.num_layers, batch_size, self.hidden_size)
return h0, c0
# 모델 인스턴스 생성 및 사용
model_lstm = LSTMModel(input_size=4, hidden_size=10, num_layers=1,output_size=1)
model_lstm
LSTMModel(
(lstm): LSTM(4, 10, batch_first=True)
(fc): Linear(in_features=10, out_features=1, bias=True)
)
init_hidden 함수는 batch_size와 device를 입력으로 받아, 해당 배치 크기와 디바이스에 맞는 초기 은닉 상태(h0)와 셀 상태(c0)를 생성합니다.
h0와 c0의 형태는 (num_layers * num_directions, batch, hidden_size) 이어야 합니다. 이는 LSTM이 시퀀스 데이터를 처리하기 전에 필요한 초기 상태를 설정하는 표준 방식입니다.
이 함수는 모든 LSTM 층(num_layers)에 대해 hidden_size 크기의 영벡터로 은닉 상태와 셀 상태를 초기화합니다. 이 벡터들은 계산시에 사용되는 디바이스(device)로 전송됩니다.
LSTM의 각 층과 각 배치 아이템에 대해 독립적인 상태를 유지하기 위해, h0와 c0는 각각의 요소가 0인 텐서로 초기화됩니다.
반환된 h0와 c0는 LSTM 네트워크의 초기 상태로 사용되며, 시퀀스 처리 과정이 시작될 때 각 시퀀스의 독립성을 보장합니다.
5. LSTM 모델을 클래스 정의 : forward 함수
forward 함수는 모델의 데이터 처리 과정을 직접적으로 관리하며, 이를 통해 다양한 시계열 예측 문제를 해결할 수 있습니다. 특히, 이 함수는 LSTM레이어를 활용하여 시퀀스의 각 요소에서 정보를 추출하고, 최종적으로는 regression 문제를 해결하기 위해 결과를 예측하는 선형 레이어로 데이터를 전달합니다.
import torch
import torch.nn as nn
class LSTMModel(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, output_size):
super(LSTMModel, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size) # 출력 크기 조정을 위한 선형 레이어
def forward(self, x):
batch_size = x.size(0)
h0, c0 = self.init_hidden(batch_size)
out, _ = self.lstm(x, (h0, c0))
out = self.fc(out[:, -1, :]) # 마지막 타임 스텝의 출력만 사용
return out
def init_hidden(self, batch_size):
h0 = torch.zeros(self.num_layers, batch_size, self.hidden_size)
c0 = torch.zeros(self.num_layers, batch_size, self.hidden_size)
return h0, c0
# 모델 인스턴스 생성 및 사용
num_features = len(features)
num_hidden = 10
num_output = 1
model_lstm = LSTMModel(input_size=num_features, hidden_size=num_hidden, num_layers = 1,output_size=num_output)
model_lstm
LSTMModel(
(lstm): LSTM(4, 10, batch_first=True)
(fc): Linear(in_features=10, out_features=1, bias=True)
)
1) 입력 데이터 x의 차원확인 및 init함수 통한 은닉상태, 셀상태 초기화
- batch_size = x.size(0)
- h0, c0 = self.init_hidden(batch_size, x.device)
forward 함수는 입력 데이터 x를 받아 처리합니다. 여기서 입력 데이터 x의 차원을 확인하고, LSTM에서 기대하는 차원을 맞추는 일은 매우 중요하면서도 어려운 일입니다.
INIT함수에서 정의된 self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) 와 같이 LSTM 네트워크 레이어를 정의하였기 때문에 입력 데이터 x의 차원은 (배치 수, 시퀀스 수, 특성 수)을 기대할 것이고, 이 차원에 맞춰 forward 함수를 호출하였을 것입니다.
따라서 forward 함수에서 x에 대한 차원(x.shape)을 반드시 확인하는 작업이 선행되어야 합니다.
여기서는 batch_first=True로 설정하였기 때문에 x.size(0)가 배치 수가 됩니다. 따라서 batch_size = x.size(0)와 같이 배치 수를 얻어온 후, 위에서 정의한 init_hidden 함수를 호출할 때마다 배치 크기에 맞게 초기 은닉 상태를 설정하여야 합니다.
h0와 c0는 init_hidden 함수를 통해 초기 은닉 상태와 셀 상태를 설정합니다. 이 상태들은 각 배치 시 LSTM 셀에 전달되어 시퀀스 처리의 기반을 형성합니다.
2) LSTM레이어 통한 데이터처리 및 결과 추출
- out, _ = self.lstm(x, (h0, c0))
- out = self.fc(out[:, -1, :])
self.lstm(x, (h0, c0)) 호출은 입력 데이터 x와 초기 상태 (h0, c0)를 LSTM 레이어에 전달합니다. 이 때, x는 (batch_size, sequence_length, input_size) 형태의 텐서이며, h0와 c0는 각각 LSTM의 은닉 상태 셀 상태를 초기화하는 데 사용됩니다.
self.lstm(x, (h0, c0))은 두 개의 출력을 반환합니다.
out: 모든 타임 스텝에 대한 출력을 포함하는 텐서입니다. 이 텐서의 차원은 (batch_size, sequence_length, hidden_size)로, 각 타임 스텝에서의 LSTM 은닉 상태의 출력을 나타냅니다.
_: LSTM 셀의 상태를 저장할 때 반환되는 튜플 (hn, cn)이며, 이는 주로 시퀀스의 계속 처리나 상태 검사할 때 필요할 때 사용됩니다. 여기서는 특별한 용도가 없을 때 종종 사용되는 관행처럼 _로 처리됩니다.
out[:, -1, :]은 out 텐서에서 각 배치의 마지막 타임 스텝의 출력을 선택합니다. out의 차원이 (batch_size, sequence_length, hidden_size)이므로 두번째 차원인 sequence_length의 마지막 요소 -1을 선택하여, 각 배치의 최근의 은닉 상태값을 가져오는 것을 의미합니다.
out[:, -1, :]의 결과는 (batch_size, hidden_size) 차원을 가지게 됩니다. 이는 각 배치의 마지막 타임 스텝에서의 은닉 상태 벡터를 의미하며, LSTM 네트워크가 시퀀스의 마지막 부분에서 추출한 정보를 나타냅니다.
선택된 마지막 타임 스텝의 출력 out[:, -1, :]은 선형 레이어 self.fc 통해 최종 출력 크기로 매핑됩니다. 이는 예를 들어, 회귀 문제에 예측 값이나 분류 문제의 로짓 값으로 변환되는 과정을 포함합니다.
3) LSTM 인스턴스 생성 및 사용
- num_features = len(features)
- num_hidden = 10
- num_output = 1
- model_lstm = LSTMModel(input_size=num_features, hidden_size=num_hidden, num_layers=1,output_size=num_output)
여기서는 이후의 과정에서 실제로 학습할 LSTM 모델의 인스턴스를 생성합니다. 모델을 초기화하기 위해 필요한 매개변수를 정의하는 과정을 포함합니다.
특성수로 매개변수 input_size 설정 df_weather 데이터프레임에서 선택된 특징은 다음과 같습니다:
features = ['humidity', 'rainfall', 'wspeed', 'temperature'] 이 특징들은 모델 입력에 사용될 것이므로, input_size 매개변수는 len(features)로 설정하여, 총 4개의 입력 특성을 반영합니다.
은닉 상태 크기 설정: 은닉 상태의 크기(num_hidden)는 10으로 설정하여, 이는 모델의 내부 레이어에서 처리할 특징의 수를 나타냅니다. 이 크기는 hidden_size 매개변수를 통해 모델에 전달됩니다.
출력 크기 설정: 이 모델은 다음날의 기온을 예측하는 회귀 작업을 수행하기 때문에, 출력 크기(num_output)는 1로 설정됩니다. 이 값은 예측해야 할 값의 수를 의미하며, output_size 매개변수로 모델에 전달됩니다.
6. 데이터 정규화
앞에서 정의한 데이터 'df_weather'를 StandardScaler 를 이용하여 표준화 합니다.
LSTM과 같은 신경망은 입력 데이터의 스케일에 민감하므로, 표준화 과정을 통해 모든 입력 특성이 동등한 중요도를 가지고 네트워크에 의해 처리될 수 있도록 조정해야 합니다.
from sklearn.preprocessing import StandardScaler
# 정규화를 위한 스케일러 초기화 및 적용
scaler = StandardScaler()
weather_scaled_arr = scaler.fit_transform(df_weather)
# DataFrame으로 변환 (스케일러는 numpy array를 반환하기 때문)
df_weather_scaled = pd.DataFrame(weather_scaled_arr, columns=features)
df_weather_scaled.head()
※ 스케일러 초기화 및 적용
StandardScaler 인스턴스를 생성하고 fit_transform 메서드를 사용하여 df_weather 데이터프레임의 모든 특성을 정규화합니다. 이 메서드는 각 특성의 평균을 0으로, 표준편차를 1로 조정합니다.
DataFrame 변환: 스케일러는 처리된 데이터를 NumPy 배열로 반환합니다. 이 배열을 다시 pandas DataFrame으로 변환하여, 원래의 컬럼명(features)을 유지합니다.
7. 시퀀스 데이터 생성
시퀀스 데이터 생성 함수 build_sequence_dataset를 활용하여, 필요한 시퀀스 데이터 및 해당 시퀀스의 정답레이블을 생성하겠습니다.
# 시퀀스 길이 정의
seq_length = 6 # 과거 6일의 데이터를 기반으로 다음날의 기온을 예측
# 시퀀스 데이터 생성 함수
def build_sequence_dataset(df, seq_length):
dataX = []
dataY = []
for i in range(0, len(df) - seq_length):
_x = df.iloc[i:i+seq_length].values # 시퀀스 데이터
_y = df.iloc[i+seq_length]['temperature'] # 다음 포인트의 기온을 레이블로 사용
dataX.append(_x)
dataY.append(_y)
return np.array(dataX), np.array(dataY)
# 시퀀스 길이 정의
seq_length = 6 # 과거 6일의 데이터를 기반으로 다음날의 기온을 예측
# 데이터셋 생성
sequence_dataX, sequence_dataY = build_sequence_dataset(df_weather_scaled, seq_length)
print(f"sequence_dataX : {sequence_dataX.shape}")
print(f"sequence_dataY : {sequence_dataY.shape}")
sequence_dataX : (724, 6, 4)
sequence_dataY : (724,)
※ 결과 해석
sequence_dataX : (724, 6, 4) sequence_dataY : (724,)
생성된 sequence_dataX는 724개의 시퀀스를 포함하며, 각 시퀀스는 6일 동안의 데이터를 나타내는 4개의 특성을 갖습니다.
sequence_dataY는 이에 대응하는 724개의 레이블(다음날의 기온)을 포함합니다.
8. 학습 세트와 테스트 세트로 분할
시계열 데이터를 처리할 때는, 테스트 데이터가 학습 데이터의 시간적 범위를 넘어서는 미래의 데이터를 포함하도록 분할 합니다. 이는 실제 환경에서 모델이 과거 데이터를 바탕으로 미래를 예측하는 시나리오를 반영합니다.
시계열 데이터의 특성상 데이터의 시간적 순서가 중요하기 때문에 데이터를 무작위로 섞지 않고 순차적으로 분할합니다.(shuffle = False)
from sklearn.model_selection import train_test_split
# sequence_dataX와 sequence_dataY를 사용하여 데이터를 학습 세트와 테스트 세트로 분할
train_X, test_X, train_Y, test_Y = train_test_split(
sequence_dataX, sequence_dataY, test_size=0.2, shuffle = False)
# 분할된 데이터의 크기 확인
print("학습 데이터 크기:", train_X.shape)
print("테스트 데이터 크기:", test_X.shape)
print(type(train_X))
학습 데이터 크기: (579, 6, 4)
테스트 데이터 크기: (145, 6, 4)
<class 'numpy.ndarray'>
※ 결과 해석
학습 데이터의 시퀀스 데이터 수는 579개이고 검증 데이터의 시퀀스 데이터 수는 145개입니다. 분할한 뒤의 train_X와 test_X의 형태를 관찰해 보겠습니다. 아직 텐서화를 시키기 이전이므로 train_X와 test_X의 타입은 numpy의 array이며,
학습 데이터 크기: (579, 6, 4)
테스트 데이터 크기: (145, 6, 4)
array가 3개의 차원을 가지며, 두 번째 차원의 시퀀스 수임을 확인할 수 있습니다. 이는 각 시퀀스가 6일 동안의 데이터를 포함하고 있으며, 각 일에 대해 4개의 특성 데이터가 존재함을 나타냅니다.
우리가 데이터를 로드한 직후의 전체 데이터 수는 730개였습니다. ‘지난 6일동안의 시퀀스를 기반으로 예측하는 문제’이기 때문에, 앞의 6개 시퀀스를 만드는 과정에서 첫 6개의 데이터를 학습 데이터 첫 번째 시퀀스로 변환할 때 사용되지 않으며, 데이터 수가 724개로 줄어듭니다. 이 중에서 579개를 학습 데이터로, 145개를 검증 데이터로 사용합니다.
일반적으로 시계열 예측에서는 데이터를 특정 날짜를 기준으로 분할하거나, 슬라이딩 윈도우 방식으로 점진적으로 테스트 세트를 확장하는 방법을 사용합니다.
9. 데이터 텐서화 및 DataLoader 설정
분할하여 얻은 시퀀스 데이터 train_X,train_Y,test_X,test_Y를 텐서화 시키고 TensorDataset, DataLoader를 이용하여 데이터 로딩 및 배치 처리를 합니다.
import torch
from torch.utils.data import TensorDataset, DataLoader
# 텐서로 데이터 변환
train_X_tensor = torch.tensor(train_X, dtype=torch.float32)
train_Y_tensor = torch.tensor(train_Y.reshape(-1, 1), dtype=torch.float32)
test_X_tensor = torch.tensor(test_X, dtype=torch.float32)
test_Y_tensor = torch.tensor(test_Y.reshape(-1, 1), dtype=torch.float32)
print("train_X_tensor 형태:", train_X_tensor.shape)
print("train_Y_tensor 형태:", train_Y_tensor.shape)
print("test_X_tensor 형태:", test_X_tensor.shape)
print("test_Y_tensor 형태:", test_Y_tensor.shape)
# TensorDataset 생성
train_dataset = TensorDataset(train_X_tensor, train_Y_tensor)
test_dataset = TensorDataset(test_X_tensor, test_Y_tensor)
# DataLoader 설정
batch_size = 32
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
print("학습 데이터셋 배치 크기:", train_loader.batch_size)
print("학습 데이터셋 배치 총 개수 :", len(train_loader))
print("검증 데이터셋 배치 갯수:", test_loader.batch_size)
print("검증 데이터셋 배치 총 개수:", len(test_loader))
# 첫 번째 배치를 불러와서 데이터 형태 확인
first_batch = next(iter(train_loader))
data, target = first_batch
print(f"첫번째 배치 데이터 'data'의 형태 : {data.shape}")
print(f"첫번째 배치 데이터 'target' 형태 : {target.shape}")
train_X_tensor 형태: torch.Size([579, 6, 4])
train_Y_tensor 형태: torch.Size([579, 1])
test_X_tensor 형태: torch.Size([145, 6, 4])
test_Y_tensor 형태: torch.Size([145, 1])
학습 데이터셋 배치 크기: 32
학습 데이터셋 배치 총 개수 : 19
검증 데이터셋 배치 갯수: 32
검증 데이터셋 배치 총 개수: 5
첫번째 배치 데이터 'data'의 형태 : torch.Size([32, 6, 4])
첫번째 배치 데이터 'target' 형태 : torch.Size([32, 1])
※ 텐서화
텐서를 데이터로 변환하고 구성하는 과정은 PyTorch에서 모델 학습을 수행하기 위한 필수적인 단계입니다. 이 과정은 데이터를 모델이 처리할 수 있는 형태로 표준화하여, 계산 효율성을 높이고 GPU 가속을 활용할 수 있게 합니다.
※ 텐서 생성시 dtype=torch.float32 설정하는 이유
dtype=torch.float32를 지정하는 이유는 대부분의 딥러닝 모델이 32비트 부동소수점 연산을 사용하기 때문입니다.
PyTorch에서 텐서를 생성할 때 dtype을 지정하지 않으면 기본적으로 입력 데이터의 자료형을 따릅니다. 예를 들어 numpy 배열에서 텐서를 생성하면 변수의 자료형을 그대로 가져옵니다.
하지만 딥러닝 모델에서는 대부분 32비트 부동소수점 연산이 필요합니다. 이는 계산 성능과 메모리 효율성 측면에서 가장 적절한 자료형이기 때문입니다. 만약 dtype을 지정하지 않고 64비트 부동소수점 자료형(float64)인 입력값을 사용하면, 모델 내부에서 32비트로 변환하는 과정이 추가로 필요합니다.
따라서 dtype=torch.float32를 사용하면 불필요한 변환을 줄일 수 있고, 모델 내부 계산이 32비트 부동소수점 연산으로 직접 이루어질 수 있습니다.
만약 dtype을 지정하지 않으면 입력 데이터의 자료형에 따라 다른 결과를 얻게 됩니다. 예를 들어 int64 자료형이 입력되면 모델 내부에서 int64 연산이 이루어지게 되는데,
이는 부동소수점 연산에 비해 정보량도 낮고 오버플로우 문제가 발생할 수 있습니다.
따라서 대부분의 딥러닝 코드에서는 torch.float32를 명시적으로 지정하여 안정적이고 효율적인 계산이 이루어지도록 합니다.
※ train_Y, test_Y에서 reshape을 하는 이유
train_Y와 test_Y 레이블 데이터는 기본적으로 1차원 배열로, 각 샘플에 단일 값만 존재합니다.
PyTorch의 일부 함수와 모델 구조는 입력 데이터의 차원을 명확히 요구하기 때문에, reshape(-1, 1) 을 사용해 명시적으로 레이블을 2차원 배열(각 샘플을 행으로 가지는 형태)로 변환합니다.
※ 학습 및 검증 데이터셋 배치 크기 및 총 개수
DataLoader를 생성할 때 'batch_size' 매개 변수를 통하여 설정한 배치 크기는 DataLoader 객체의 batch_size 속성을 통해 확인할 수 있습니다. 이는 각 배치에 포함된 데이터 샘플의 수를 의미하며, 이 경우 학습 및 검증 데이터셋 모두 배치 크기가 32입니다.
이전 단계에서 생성된 시퀀스 데이터의 차원은 학습 데이터셋 (579, 6, 4)이고, 테스트 데이터셋 (145, 6, 4)였습니다.
이 데이터를 배치 크기 32로 나누면 학습 데이터셋의 경우 579개의 데이터를 32개씩 묶어 처리할 수 있는데, 총 19개의 완전한 배치(32개씩)와 1개의 불완전한 배치(여분 3개)가 생성됩니다.
테스트 데이터셋도 동일하게 145개의 데이터를 32개씩 나누면 4개의 완전한 배치가 나오며, 남은 데이터(여분 17개)는 추가로 남은 데이터를 포함한 5번째 배치에 포함되게 됩니다.
배치 사이즈를 32로 설정했기 때문에, 학습 데이터셋에서는 모델이 한 번의 에포크(epoch) 동안 579개의 데이터를 나눠 학습하고, 검증 데이터셋에서는 보유된 평가용 데이터를 사용하여 모델을 평가하게 됩니다.
이렇게 데이터를 배치로 나눠서 로드함으로써, 모델 학습 시 계산 효율성을 높이고, 메모리 사용을 최적화할 수 있습니다.
※ DataLoader 의 shuffle 옵션
모델 학습 중에는 데이터의 다양성을 확보하고, 각 배치가 데이터 전체의 특성을 잘 대표할 수 있도록 하는 것이 중요합니다. shuffle=True로 설정하면 매 에포크마다 데이터셋이 무작위로 섞여, 모델이 특정 순서 내 패턴에 의존하지 않고 일반화할 수 있도록 돕습니다. 일반적으로 시계열 데이터의 경우 시간 순서가 중요하지만, LSTM과 같은 모델을 학습할 때는 초기 상태의 시간 편향을 방지하기 위해 데이터를 섞는 것이 유리할 수 있습니다. 그러나 이는 시퀀스 내부 데이터 순서를 변경하지 않고, 시퀀스 간의 순서만 랜덤으로 섞는 것을 의미합니다. 테스트 단계에서는 모델의 성능을 실제 운영 환경과 유사한 조건에서 평가하기 위해 데이터의 순서를 유지하는 것이 일반적입니다. 따라서 shuffle=False를 설정하여 테스트 데이터를 순차적으로 로드함으로써, 시계열 데이터의 시간적 연속성을 보존하고, 모델이 실제 시간대에서 얼마나 잘 예측하는지를 정확히 평가할 수 있습니다.
※ 배치 데이터 형태
(data.shape): 출력된 데이터 형태 torch.Size([32, 6, 4])는 각 배치에 32개의 시퀀스 데이터가 포함되어 있으며, 각 시퀀스는 6개의 시간 단계와 각 시간 단계에 4개의 특성이 있다는 것을 나타냅니다.
※ 배치 타겟 형태
(target.shape): 타겟의 형태 torch.Size([32, 1])는 각 배치에 32개의 타겟 값이 포함되어 있으며, 각 타겟 값은 하나의 출력 값을 가지고 있음을 보여줍니다.
10. 모델 설정 및 옵티마이저, 손실 함수 정의
이번 단계에서는 모델의 학습 과정을 관리할 핵심 구성 요소인 옵티마이저와 손실 함수를 설정합니다. 옵티마이저는 모델의 파라미터를 업데이트하는 방식을 정의하며, 이는 학습 과정에서 모델이 데이터로부터 얼마나 효과적으로 학습할 수 있는지를 결정합니다. 손실 함수는 모델의 예측값과 실제값 사이의 차이를 수치화하며, 이 값이 최소화되도록 모델을 조정하는 데 중요한 역할을 합니다. 이러한 설정은 모델의 학습 효율성과 최종 성능에 직접적인 영향을 미치므로, 적절한 옵티마이저와 손실 함수의 선택은 성공적인 모델 훈련을 위해 필수적입니다.
import torch.optim as optim
learning_rate = 0.01
optimizer_lstm = torch.optim.Adam(model_lstm.parameters(), lr=learning_rate)
criterion_lstm = nn.MSELoss()
print("\n옵티마이저 정보:")
print(optimizer_lstm)
print("\n손실 함수:")
print(criterion_lstm)
옵티마이저 정보:
Adam (
Parameter Group 0
amsgrad: False
betas: (0.9, 0.999)
capturable: False
differentiable: False
eps: 1e-08
foreach: None
fused: None
lr: 0.01
maximize: False
weight_decay: 0
)
손실 함수:
MSELoss()
※ 손실 함수 설정
옵티마이저는 신경망 학습 과정에서 모델의 가중치와 편향을 조정하여 손실 함수의 값을 최소화하는 방법을 정의합니다.
이 과정을 통해 모델은 주어진 데이터에 대해 최적의 예측을 수행할 수 있도록 학습됩니다.
옵티마이저는 손실 함수의 그래디언트(미분값)를 계산하고, 이를 사용하여 모델 파라미터를 업데이트합니다.
이때, 파라미터 업데이트 속도와 방향은 옵티마이저의 종류에 따라 다양하게 조절될 수 있습니다.
※ Adam 옵티마이저
Adam은 Adaptive Moment Estimation의 약자로, 각 매개변수의 학습률을 개별적으로 조정하면서도, 이전 그래디언트의 지수적 감소 평균을 계산하여 조정합니다.
이는 Adam이 두 가지 주요 기법을 합친 것으로 볼 수 있습니다.
- RMSprop 방식에서 영감을 받아 그래디언트의 제곱근 평균을 사용합니다.
- Momentum 업데이트에서 사용하는 이동 평균을 사용하여 파라미터를 조정합니다.
Adam은 다양한 조건에서 좋은 성능을 발휘하며, 특히 복잡한 데이터셋과 비선형 최적화 문제에서 자주 사용됩니다.
그의 효율성과 일반적인 우수한 성능으로 인해, 딥러닝 커뮤니티에서 널리 채택되고 있습니다.
※ 손실 함수
손실 함수는 회귀 모델에서 예측값과 실제값 간의 오차를 측정하는 데 사용되며, PyTorch에서는 다양한 손실 함수를 지원합니다.
회귀 문제에서 널리 사용되는 세 가지 주요 손실 함수평균 절대 오차(MAE), 평균 제곱 오차(MSE), Huber 손실의 사용 방법과 코드 예시는 다음과 같습니다.
평균 제곱 오차 (Mean Squared Error, MSE)
mse_loss = nn.MSELoss()
평균 절대 오차 (Mean Absolute Error, MAE)
MAE는 예측값과 실제값 절대 차이를 평균낸 것입니다. 이는 모든 오차에 동일한 가중치를 부여하며, 이상치에 대해 덜 민감합니다.
PyTorch에서 MAE는 L1Loss라고도 불리며, 다음과 같이 생성할 수 있습니다.
mae_loss = nn.L1Loss()
Huber 손실
Huber 손실은 MSE와 MAE의 조합으로, 오차가 작을 때는 제곱을 사용하여 민감하게 반응하고, 오차가 클 때는 절댓값을 사용하여 이상치의 영향을 줄이는 방식입니다.
이는 MSE의 민감성과 MAE의 견고함을 결합한 것으로, 실제 사용에서는 오차의 임계값(delta)을 설정하여 사용됩니다.
PyTorch에서는 SmoothL1Loss로 구현되어 있으며, 다음과 같이 객체를 생성할 수 있습니다.
huber_loss = nn.SmoothL1Loss()
이러한 손실 함수들은 모델의 학습 과정에서 오차를 최소화하기 위한 기준으로 사용됩니다.
각 함수는 특정 상황에 적합할 수 있으므로, 문제의 성격을 고려하여 적절한 손실 함수를 선택하는 것이 중요합니다.
11. 간단한 모델 학습 프로세스의 이해: 옵티마이저와 손실 함수를 통한 가중치 업데이트
# 간단한 모델 정의
model_ex = nn.Linear(10, 1)
optimizer_ex = torch.optim.Adam(model_ex.parameters(), lr=learning_rate)
criterion_ex = nn.MSELoss()
# 가상의 데이터와 타겟 생성
data = torch.randn(1, 10)
target = torch.randn(1, 1)
# 모델 예측 전 가중치 출력
print("가중치 업데이트 전:", model_ex.weight.data.numpy())
# 모델 예측
output = model_ex(data)
# 손실 계산
loss_ex = criterion_ex(output, target)
# 가중치 업데이트 전후 비교
optimizer_ex.zero_grad() # 그래디언트 버퍼 초기화
loss_ex.backward() # 그래디언트 계산
print("계산된 그래디언트:", model_ex.weight.grad.numpy())
optimizer_ex.step() # 가중치 업데이트
# 모델 예측 후 가중치 출력
print("가중치 업데이트 후:", model_ex.weight.data.numpy())
가중치 업데이트 전: [[ 0.04625276 -0.09263909 -0.06024083 -0.13335113 -0.21362087 -0.16742873
0.3126103 -0.05292277 0.11077332 0.21868859]]
계산된 그래디언트: [[ 0.17020436 -0.24023376 -0.19833042 -0.2148661 0.11305677 0.377794
-0.2580071 0.34026036 -0.04740559 0.30494863]]
가중치 업데이트 후: [[ 0.03625276 -0.08263909 -0.05024083 -0.12335113 -0.22362088 -0.17742874
0.3226103 -0.06292277 0.12077332 0.20868859]]
※ optimizer_ex.zero_grad()
이 함수는 모델의 가중치에 대한 그래디언트를 0으로 초기화합니다. 그래디언트는 손실 함수를 기반으로 계산된 값으로, 모델의 파라미터를 조정하는 데 사용됩니다. 그러나 PyTorch는 기본적으로 그래디언트를 누적하는 방식으로 작동하기 때문에, 각 학습 반복(iteration) 시작 전에 전 반복에서 계산된 그래디언트를 명시적으로 제거해야 합니다. 그렇지 않으면 그래디언트가 계속 누적되어 올바른 학습이 이루어지지 않습니다.
PyTorch의 자동 미분 엔진은 연산 그래프를 통해 파라미터에 대한 손실의 그래디언트를 계산합니다. backward() 함수를 호출하면, 이 그래디언트는 각 파라미터(가중치)에 누적됩니다. 따라서, 새로운 그래디언트를 계산하기 전에 이전의 그래디언트를 0으로 초기화하는 것이 중요합니다.
옵티마이저가 관리하는 모든 파라미터의 그래디언트를 0으로 설정함으로써, 모델이 새로운 입력 데이터로부터 그래디언트를 깨끗하게 계산할 수 있으며, 각 학습 스텝에서 정확한 파라미터 업데이트가 가능해집니다.
※ loss_ex.backward()
loss_ex.backward() 함수는 PyTorch에서 모델 학습 과정의 핵심적인 부분입니다. 이 함수는 손실 함수의 값에 대해 파라미터들의 그래디언트를 자동으로 계산하는 역할을 합니다. 이 과정을 역전파(backpropagation)라고 하며, 모델의 가중치를 적절히 조정하기 위한 필수적인 단계입니다.
loss_ex.backward()가 호출되면, 다음의 과정이 순차적으로 일어납니다.
1) 손실 값 계산: 모델의 출력과 실제 타겟 간의 오차를 기반으로 손실 함수에서 손실 값이 계산됩니다.
2) 그래디언트 계산: 계산된 손실 값에 대해, 손실 함수가 각 파라미터에 대한 편미분(그래디언트)을 수행합니다. 이 편미분은 파라미터 값이 손실에 얼마나 영향을 미치는지를 나타내며, 옵티마이저는 이 값을 활용해 손실을 감소시킬 수 있는 방향을 압력합니다.
3) 연산 그래프를 통한 역전파 : PyTorch는 모델의 각 연산을 연산 그래프로 구성합니다. backward() 함수는 이 그래프를 거슬러 올라가며, 연쇄 법칙을 사용하여 각 파라미터의 그래디언트를 계산합니다. 이는 입력층에 도달할 때까지 계속됩니다. 이 그래디언트는 이후 옵티마이저에 의해 사용되어 모델의 가중치를 업데이트합니다. 즉, loss.backward()는 모든 학습 파라미터의 오차를 줄이는 방향으로 그래디언트를 계산하고, 이를 옵티마이저가 사용할 수 있도록 준비합니다. 이 과정을 통해 모델은 데이터로부터 학습을 최적화하고, 예측 성능을 점차 향상시킵니다.
loss_ex.backward()는 계산된 손실에서 파생된 정보를 사용하여 모델이 어떻게 개선되어야 할지를 결정합니다. 이는 딥러닝에서 "학습"이라는 과정의 본질을 구현하는 핵심 역할을 하며, 모든 학습 반복에서 실행되어야 합니다.
※ optimizer_ex.step()
optimizer_ex.step() 함수는 PyTorch에서 모델의 파라미터를 업데이트하는 데 사용됩니다.
이 함수는 계산된 그래디언트를 사용하여 모델의 가중치와 편향을 조정하고, 모델의 학습 과정을 한 단계 진척시킵니다.
loss.backward()를 통해 계산된 그래디언트는 각 파라미터에 누적됩니다.
optimizer_ex.step()은 이 누적된 그래디언트를 각 파라미터에 적용하여 새로운 값으로 조정합니다.
업데이트 과정에서 옵티마이저에 설정된 학습률(lr)이 중요한 역할을 합니다.
학습률은 그래디언트에 곱해져 파라미터 업데이트의 크기를 결정합니다.
즉, 학습률이 크면 더 큰 폭으로 파라미터가 조정되고, 작으면 서서히 조정됩니다.
최종적으로, 계산된 그래디언트와 학습률을 곱하여 각 파라미터의 값을 업데이트합니다.
이는 모델이 손실을 줄이는 방향으로 점진적으로 개선될 수 있도록 합니다.
12. 단일 epoch 학습
이번 단계에서는 PyTorch를 사용하여 모델의 학습 과정을 관리하는 함수를 구현하고, 한 번의 에폭(epoch) 동안 모델을 훈련하는 과정을 진행해 보도록 하겠습니다. 이 함수는 모델을 학습 모드로 설정하고, 학습 데이터를 반복 처리하여 가중치를 업데이트하는 기능을 포함합니다. 함수의 목적은 모델의 가중치를 최적화하여 손실을 최소화하는 것이며, 이 과정을 통해 모델의 예측 정확도를 향상시키는 것입니다.
def train(model, train_loader, optimizer, criterion):
model.train() # 모델을 학습 모드로 설정
total_loss = 0
i = 0
for data, target in train_loader:
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
total_loss += loss.item()
i+=1
return total_loss / len(train_loader)
epoch_loss = train(model_lstm, train_loader, optimizer_lstm, criterion_lstm)
print(f"epoch loss(mse) = {epoch_loss:.4f}")
epoch loss(mse) = 0.4031
※ model.train()
model.train()은 PyTorch에서 모델을 학습 모드로 설정하는 명령입니다.
이 메서드를 호출하면 모델 내부 특정 레이어들이 학습을 위한 동작 모드로 전환됩니다.
학습 모드에서는 드롭아웃(Dropout) 레이어와 배치 정규화(Batch Normalization) 레이어가 활성화되어, 모델 학습에 필수적인 역할을 수행하게 됩니다.
드롭아웃 레이어:
학습 과정에서 일부 뉴런의 활성화를 임의로 0으로 설정합니다.
이는 모델이 특정 뉴런이나 뉴런 연결에 과도하게 의존하는 것을 방지하고 과적합을 줄이는 데 도움을 줍니다.
학습 모드에서는 드롭아웃이 활성화되어 작동하며, 평가 모드에서는 이 기능이 비활성화되어 모든 뉴런이 활성화 상태를 유지합니다.
만약, 모델에 드롭아웃 레이어가 포함된 경우, 학습 시 model.train()을 호출하지 않으면 드롭아웃이 비활성화된 상태로 학습이 진행될 수 있습니다. 따라서 학습을 진행할 때 반드시 model.train()을 호출해야 합니다.
반대로, 모델 평가를 진행할 때는 model.eval()을 사용하여 드롭아웃을 비활성화하고, 전체 뉴런이 활성화된 상태로 입력을 전달하여 일관된 출력을 얻어야 합니다.
배치 정규화 레이어:
일반적으로 학습 중 미니 배치의 평균과 분산을 사용하여 입력 데이터를 정규화합니다.
이는 입력 데이터가 학습 중 균일한 분포를 유지하도록 하고 학습 안정성과 속도를 향상시킵니다.
그러나 평가 시에는 모든 데이터의 통계를 기반으로 정규화가 수행되어야 합니다.
따라서, 학습 모드에서는 현재 배치에서 계산된 평균과 분산을 사용하지만, 평가 모드에서는 이미 계산된 전체 데이터의 평균과 분산을 사용합니다.
※ optimizer.zero_grad()
옵티마이저에 등록된 모든 파라미터의 그래디언트를 0으로 초기화합니다.
이는 새로운 가중치 업데이트를 위해 이전 반복에서 계산된 그래디언트를 제거하는 것입니다.
그래디언트가 누적되지 않도록 하여 각 학습 스텝에서 정확한 파라미터 업데이트가 가능하게 합니다.
※ 모델을 통한 예측 수행
output = model(data)
현재 배치의 입력 데이터(data)를 모델에 전달하여 예측 결과(output)을 얻습니다.
이 과정은 모델의 순전파 단계로, 입력 데이터가 모델의 여러 레이어를 통과하면서 최종 출력까지 계산됩니다.
※ 손실 계산
loss = criterion(output, target.view(-1, 1))
모델의 출력(output)과 실제 타겟(target) 간의 손실을 계산합니다.
target.view(-1, 1)은 타겟 데이터의 형태를 조정하여 손실 함수의 입력 요구 사항에 맞추는 작업입니다.
※ 역전파(back propagation)를 통한 그래디언트 계산
loss.backward()
손실 함수의 결과를 바탕으로 모델의 각 파라미터에 대한 손실의 그래디언트를 계산합니다.
이 과정은 역전파(backpropagation)라고 하며, 계산된 그래디언트는 각 파라미터에 영향을 주어 손실을 줄일 수 있는지를 알려줍니다.
※ 옵티마이저를 통한 가중치 업데이트
optimizer.step()
optimizer.step()은 계산된 그래디언트를 사용하여 모델의 파라미터(가중치와 편향)를 업데이트합니다.
이 과정은 손실을 최소화하는 방향으로 파라미터를 조정함으로써, 모델의 학습을 진행시킵니다.
13. 여러 epoch에 걸친 학습
이번 단계에서는 모델을 여러 에폭(epoch)에 걸쳐 반복적으로 학습시키는 과정을 구현합니다. 이 과정을 통해 모델의 성능이 시간이 지남에 따라 어떻게 개선되는지 관찰할 수 있으며, 손실의 감소 추이를 통해 학습의 진행 상황을 모니터링할 수 있습니다. 함수는 각 에포크마다 학습을 수행하고, 결과적으로 각 에포크의 평균 손실을 기록하여 학습의 효과를 시각적으로 확인할 수 있게 합니다.
# 각 에포크의 평균 손실을 저장할 리스트 초기화
loss_history = []
max_epochs = 200
for epoch in range(max_epochs):
epoch_loss = train(model_lstm, train_loader, optimizer_lstm, criterion_lstm)
loss_history.append(epoch_loss) # 손실 기록
if (epoch+1) % 10 == 0:
print(f"epoch {epoch+1}: loss(mse) = {epoch_loss:.4f}")
print(f"학습 완료 : 총 {epoch+1} epoch")
epoch 10: loss(mse) = 0.0466
epoch 20: loss(mse) = 0.0402
epoch 30: loss(mse) = 0.0321
epoch 40: loss(mse) = 0.0326
epoch 50: loss(mse) = 0.0291
epoch 60: loss(mse) = 0.0293
epoch 70: loss(mse) = 0.0266
epoch 80: loss(mse) = 0.0246
epoch 90: loss(mse) = 0.0213
epoch 100: loss(mse) = 0.0204
epoch 110: loss(mse) = 0.0182
epoch 120: loss(mse) = 0.0172
epoch 130: loss(mse) = 0.0197
epoch 140: loss(mse) = 0.0155
epoch 150: loss(mse) = 0.0122
epoch 160: loss(mse) = 0.0133
epoch 170: loss(mse) = 0.0118
epoch 180: loss(mse) = 0.0119
epoch 190: loss(mse) = 0.0124
epoch 200: loss(mse) = 0.0098
학습 완료 : 총 200 epoch
※ 코드해석
각 에포크마다, train 함수는 모델 model_lstm, 학습 데이터 로더 train_loader, 옵티마이저 optimizer_lstm, 그리고 손실함수 criterion_lstm을 사용하여 한번의 전체 학습 사이클을 수행합니다.
※ 에포크(epoch)의 역할과 설정
에포크는 전체 데이터셋을 한 번 통과하는 사이클을 의미합니다. 여기서 한 번의 에포크 동안 모든 학습 데이터가 모델에 의해 처리되며, 이 과정에서 데이터의 특성을 학습하고, 가중치를 조정합니다.
※ 최대 epoch 설정
에포크 수를 설정하는 것은 학습 알고리즘에 있어 중대한 전략적 결정입니다. 너무 적은 수의 에포크는 모델이 데이터의 학습을 충분히 완료하지 못해 성능이 낮게 나올 수 있습니다. (underfitting). 반면, 너무 많은 에포크는 학습 시간이 길어지고, 모델이 훈련 데이터에 과적합(overfitting)할 가능성이 있습니다.
일반적으로 모델의 에포크를 조절할 때 검증 데이터셋의 성능을 주기적으로 체크하며 최적점을 찾습니다. 검증 손실이 더 이상 개선되지 않거나 성능이 저하되기 시작하면 학습을 중단하는 것이 일반적입니다.
또 다른 방법으로는 조기 종료(Early Stopping) 기법이 있습니다. 이 방법은 검증 손실이 일정 기간 동안 개선되지 않을 때 학습을 자동으로 중단시키는 것입니다.
※ 손실 추적
각 에포크 결과로 반환되는 epoch_loss는 그 에포크에서의 평균 손실을 나타냅니다.
이 값은 학습의 진행 상태를 모니터링하고, 모델의 성능을 평가하는 데 중요한 자료로 활용될 수 있습니다.
※ 결과 해석
각 에포크별로 손실이 점차 감소하는 추세를 볼 수 있습니다.
이는 모델이 점점 더 정확한 예측을 하게 되며, 학습 데이터에 대해 더 잘 일반화하고 있음을 나타냅니다.
※ 학습 목표 및 모델의 학습 거동 평가
최종적으로, 손실이 지속적으로 감소하여 매우 낮은 수준에 도달했다는 것은 모델의 학습이 성공적으로 이루어졌음을 나타냅니다. 이는 모델이 학습 데이터셋의 패턴을 효과적으로 포착했으며, 주어진 문제에 대해 적절한 예측을 수행할 수 있음을 의미합니다. 하지만 손실이 너무 빠르게 감소하거나 너무 늦게 감소하는 경우, 모델의 과적합 또는 학습 부족의 가능성을 고려하여 추가적인 조정을 시도해 봐야 합니다.
※ 입력 데이터와 모델 구조의 호환성 문제
1) 차원 불일치:모델의 입력 레이어와 제공된 데이터의 차원이 서로 일치하지 않을 경우, 효과적인 학습이 이루어지지 않을 수 있습니다. 예를 들어, 모델이 요구하는 입력 차원과 실제 데이터의 특성 수가 다르면 오류가 발생하며, 잘못된 결과를 초래할 수 있습니다.
2) 형태 불일치: 텐서의 형태(shape)가 예상과 다르게 구성되어 있어도 문제가 될 수 있습니다. 예를 들어, 배치 크기나 시퀀스 길이 등이 모델이 기대하는 형태와 다를 경우, 이는 학습과정에서 에러를 발생시키거나 성능 저하의 원인이 될 수 있습니다.
15. LSTM 모델 검증
LSTM 모델을 평가 모드로 전환하여 테스트 데이터셋에 대한 성능을 평가합니다. 이 과정은 모델이 새로운 데이터에 대해 얼마나 잘 일반화하는지를 평가하며,실제 운영 환경에서의 성능 예측에 중요한 단계입니다.
def validate_model(model, test_loader, criterion):
model.eval() # 모델을 평가 모드로 설정
total_loss = 0
actuals = []
predictions = []
with torch.no_grad(): # 그라디언트 계산을 비활성화
for data, target in test_loader:
output = model(data)
loss = criterion(output, target)
total_loss += loss.item()
# 예측값과 실제값을 리스트에 저장
actuals.extend(target.squeeze(1).tolist())
predictions.extend(output.squeeze(1).tolist())
# 손실 계산
avg_loss = total_loss / len(test_loader)
return avg_loss, actuals, predictions
# 모델 검증 및 실제값, 예측값 가져오기
test_loss, actuals, predictions = validate_model(model_lstm, test_loader, criterion_lstm)
print(f"테스트 손실: {test_loss:.4f}")
print("Actuals Sample:", actuals[:10])
print("Predictions Sample:", predictions[:10])
테스트 손실: 0.0960
Actuals Sample: [1.079803228378296, 1.04249107837677, 1.0704751014709473, 1.2570358514785767, 1.145099401473999, 1.3130040168762207, 1.3596442937850952, 1.2290517091751099, 1.1637555360794067, 1.1264433860778809]
Predictions Sample: [1.0471099615097046, 1.4057235717773438, 1.1768426895141602, 1.2876627445220947, 1.4329736232757568, 1.1708322763442993, 1.3993799686431885, 0.9542418718338013, 1.4003021717071533, 1.2177982330322266]
※ 모델을 평가 모드 설정
model.eval()
이 메서드는 모델을 평가(evaluation) 모드로 설정합니다.
평가 모드에서는 모델의 학습을 위해 활성화되었던 일부 기능들이 변경되거나 비활성화되며,
모델의 일반된 성능 평가를 위해 필요합니다.
예를 들어, 드롭아웃 레이어나 배치 정규화 레이어가 평가 시에는 다른 동작을 하거나 비활성화되어,
입력 데이터의 각 요소에 대한 모델의 반응을 정확히 평가할 수 있습니다.
※ 그래디언트 계산 비활성화
torch.no_grad()
평가 시에는 모델의 파라미터를 업데이트하지 않으므로,
그래디언트 계산을 비활성화하여 리소스 사용을 최적화하고 계산 속도를 높입니다.
이는 메모리 사용량을 줄이고, 평가 과정의 효율성을 증가시킵니다.
※ 모델을 통한 예측 수행 및 손실 계산
테스트 데이터 로더(test_loader)를 통해 모델에 데이터를 제공하고,
모델이 예측을 수행한 후, 손실 함수를 사용하여 모델의 출력(output)과 실제 타겟(target) 간의 손실을 계산합니다.
※ 데이터 차원과 test_loader의 역할
test_loader는 테스트 데이터셋에서 데이터를 배치로 모델에 전달하는 역할을 합니다.
각 배치 내 data의 형태는 (32, 6, 4)로, 이는 각 배치가 32개의 샘플을 포함하며, 각 샘플은 6개의 시퀀스 길이와 4개의 특성을 가짐을 뜻합니다.
target 텐서의 형태는 torch.Size([32, 1])로, 각 배치에 32개의 타겟 값이 있으며, 각 타겟은 1개의 값을 가짐을 나타냅니다.
이는 LSTM 모델이 예측한 각 샘플의 출력 형태와 일치해야 합니다.
※ view(-1, 1) 함수 사용
target.view(-1,1) 함수는 target 텐서의 차원을 조정합니다.
여기서 -1은 해당 차원의 크기를 유연하게 조정하여 전체 요소 수가 일정하게 유지되도록 합니다.
1은 새로운 차원의 크기를 1로 설정함을 의미합니다. 이는 target 텐서가 원래 [32]의 형태였다면, [32,1]로 변경하여 각 타겟 값이 별도의 차원에서 개별적으로 취급될 수 있도록 합니다. 이러한 조정은 손실 함수가 예측값과 실제값을 올바르게 비교하고, 적절한 그래디언트를 계산할 수 있도록 하기 위해 필요합니다.
※ 예측값과 실제값 수집
actuals와 predictions list는 각각 모델의 실제 타겟 값과 모델이 예측한 값을 저장하기 위해 초기화됩니다.
각 반복에서 model(data)를 호출하여 data에 대한 예측 output을 생성합니다. criterion 함수는 모델 출력과 실제 타겟 값 사이의 손실을 계산합니다. 계산된 손실은 total_loss에 누적됩니다.
모델의 최종 출력 output과 실제 target 형태는 torch.Size([n, 1])이므로 squeeze(1).tolist()를 사용하여 불필요한 두 번째 차원을 제거하고 리스트로 변환(tolist())하여 actuals에 추가합니다.
※ epoch에 따른 학습 및 검증 동시 진행 방법
해당 코드는 여러 에포크에 걸친 학습 과정을 모두 마친 후테스트 데이터에서 별도로 검증을 수행합니다.
일반적으로, 학습을 진행하면서 동시에 테스트 데이터를 활용하여 모델의 학습이 과적합 없이 지속적으로 검증을 수행하고, 학습 데이터에 대한 손실을 동시에 비교하고 평가합니다. 이러한 접근 방식은 다음과 같은 이유로 중요합니다.
1) 과적합 방지
학습 데이터에 대한 손실이 지속적으로 감소하는 동안 검증 데이터에 대한 손실이 증가하기 시작하면,
이는 모델이 과적합되기 시작했음을 나타냅니다. 이러한 현상을 조기에 발견하여 학습을 조기에 중단하면 모델을 학습하여 과적합을 방지할 수 있습니다.
2) 최적의 에포크 도출
학습과 검증 손실을 비교함으로써 모델의 학습 과정에서 최적의 에포크를 결정할 수 있습니다. 최적의 에포크는 검증 손실이 최소화되고 이후에 성능이 개선되지 않거나 감소하는 지점입니다.
3) 모델 성능의 일반화
검증 데이터셋에 대한 성능을 주기적으로 확인함으로써, 모델이 학습 데이터에만 잘 작동하는 것이 아니라 일반적인 데이터에 대해 얼마나 잘 작동하는지 평가할 수 있습니다. 이는 모델의 일반화 능력을 확인하는 데 중요합니다.
16. 실제값과 예측값 시각화
# 실제값과 예측값을 시각화
plt.figure(figsize=(8, 4))
plt.plot(actuals, label='Actual Values', color='blue')
plt.plot(predictions, label='Predicted Values', color='red', alpha=0.5)
plt.title('Actual vs Predicted Values')
plt.xlabel('Sample Number')
plt.ylabel('Temperature')
plt.legend()
plt.show()
※ 결과 해석
이 그래프는 실제 온도값(Actual Values)과 LSTM 모델에 의해 예측된 온도값(Predicted Values)을
시간의 흐름에 따라 나타낸 것입니다. 이를 통해 모델의 예측 성능과 그 거동의 특성을 평가할 수 있습니다.
1) 일반적인 추세 파악
실제값과 예측값은 대체로 유사한 추세를 보이고 있습니다.
이는 모델이 데이터의 기본적인 패턴과 변동성을 잘 파악하고 있음을 나타냅니다.
두 선이 비슷할수록 모델의 예측 성능이 우수하며, 주요 변동 포인트에서 실제값을 어느 정도 반영하고 있는지 확인할 수 있습니다.
2) 지연 효과 및 적응도
그래프에서 볼 수 있듯이, 예측값이 실제값과 잘 일치하고 있지만, 일부 구간에서는 약간의 지연 효과나 오차가 발생하고 있습니다.
이는 모델이 특정 상황에서의 신속한 변화에 빠르게 적응하지 못하는 한계를 가질 수 있음을 의미합니다. 특히, 급격한 온도 변화가 있는 지점에서 예측이 실제값을 따라가지 못하는 경우가 관찰됩니다.
이 데이터를 바탕으로, 모델의 예측력을 향상시키기 위해
더 많은 훈련 데이터셋을 사용하거나, 온도 관련 파라미터를 실험해 보는 것이 좋습니다.
또한, 시계열 데이터의 특성을 더 잘 포착할 수 있는 고급 알고리즘을 적용하여 더 복잡한 패턴을 학습할 수 있는 딥러닝 모델의 길을 탐색하거나, 장기적 메모리 네트워크(LSTM) 외의 다른 유형의 순환 신경망을 테스트해 볼 수도 있습니다.
17. 모델 및 모델 파라미터, 실험환경 저장
모델의 상태, 파라미터 설정 및 학습 환경을 파일로 저장합니다. 이러한 정보를 저장하는 것은 추후 모델의 재현성을 보장하고, 이전 학습된 상태에서 추가적인 학습을 계속하거나 다른 환경에서 모델을 평가는데 사용 될 수 있습니다.
import torch
model_params = {
'input_size' :num_features,
'hidden_size' : num_hidden,
'num_layers': 1,
'output_size': num_output
}
training_params = {
'batch_size' : batch_size,
'sequence_length':seq_length,
'learning_rate': learning_rate,
'num_epochs': max_epochs,
'seed':seed_value,
}
filepath = 'my_lstm_checkpoin_001.pth'
torch.save({
'model_state_dict': model_lstm.state_dict(),
'optimizer_state_dict': optimizer_lstm.state_dict(),
'model_params': model_params, # 모델 생성 매개변수
'training_params': training_params # 학습 환경 매개변수
}, filepath)
이제 저장된 체크포인트 파일에서 모델의 상태 및 파라미터를 로드하고, 로드한 내부 파라미터를 출력하여 내용을 검증합니다.
이는 모델과 학습 파라미터가 올바르게 저장되었는지 확인하는 과정입니다.
checkpoint = torch.load(filepath)
# LSTM 관련 파라미터의 형태 출력
print("lstm.weight_ih_l0 shape:", checkpoint['model_state_dict'].get('lstm.weight_ih_l0', None).shape)
print("lstm.weight_hh_l0 shape:", checkpoint['model_state_dict'].get('lstm.weight_hh_l0', None).shape)
print("lstm.bias_ih_l0 shape:", checkpoint['model_state_dict'].get('lstm.bias_ih_l0', None).shape)
print("lstm.bias_hh_l0 shape:", checkpoint['model_state_dict'].get('lstm.bias_hh_l0', None).shape)
# 모델과 학습 매개변수 출력
print("Model parameters:", checkpoint.get('model_params', 'No model_params in checkpoint'))
print("Training parameters:", checkpoint.get('training_params', 'No training_params in checkpoint'))
# 옵티마이저 상태 딕셔너리의 키 출력
if 'optimizer_state_dict' in checkpoint:
print("Optimizer state keys:", list(checkpoint['optimizer_state_dict'].keys()))
else:
print("No optimizer_state_dict found in the checkpoint.")
18. 저장된 체크포인트로부터 모델과 옵티마이저 복원
이 단계에서는 저장된 체크포인트 파일로부터 모델과 옵티마이저의 상태를 복원합니다. 복원 과정을 통해 이전에 중단된 학습을 재개하거나 모델을 다른 데이터셋에 적용하는 등의 작업을 수행할 수 있습니다.
model_params = checkpoint['model_params']
training_params = checkpoint['training_params']
model_lstm = LSTMModel(**model_params)
model_lstm.load_state_dict(checkpoint['model_state_dict'])
optimizer_lstm = torch.optim.Adam(model_lstm.parameters(), lr=training_params['learning_rate'])
optimizer_lstm.load_state_dict(checkpoint['optimizer_state_dict'])
print("Model parameters:", model_params)
print("Training parameters:", training_params)
Model parameters: {'input_size': 4, 'hidden_size': 10, 'num_layers': 1, 'output_size': 1}
Training parameters: {'batch_size': 32, 'sequence_length': 6, 'learning_rate': 0.01, 'num_epochs': 200, 'seed': 42}
19. 복원한 모델과 옵티마이저로 검증 수행하기
# 검증 수행 및 결과 출력
test_loss, actuals, predictions = validate_model(model_lstm, test_loader, criterion_lstm)
print(f"테스트 손실: {test_loss:.4f}")
print("Actuals Sample:", actuals[:10])
print("Predictions Sample:", predictions[:10])
테스트 손실: 0.0960
Actuals Sample: [1.079803228378296, 1.04249107837677, 1.0704751014709473, 1.2570358514785767, 1.145099401473999, 1.3130040168762207, 1.3596442937850952, 1.2290517091751099, 1.1637555360794067, 1.1264433860778809]
Predictions Sample: [1.0471099615097046, 1.4057235717773438, 1.1768426895141602, 1.2876627445220947, 1.4329736232757568, 1.1708322763442993, 1.3993799686431885, 0.9542418718338013, 1.4003021717071533, 1.2177982330322266]