
def __init__() → 연산(예측)에 사용할 함수들을 정의하는 공간

__init__에서 정의한 내용과 forward 함수 사이의 관계 이해하기
# 학습 횟수 및 출력 횟수, 학습률 지정
n_epochs = 4000
print_interval = 200
learning_rate = 0.001
# 최적화 함수 정의
optimizer = optim.Adam(model1.parameters(), lr=learning_rate)
# 학습 반복문 작성
for i in tqdm(range(n_epochs)):
# 예측 결과
y_pred = model1(X)
# 손실 함수를 통한 loss
loss = F.mse_loss(y_pred, y)
# 최적화 함수 초기화
optimizer.zero_grad()
# 오차역전파
loss.backward()
# 결과 담기
optimizer.step()
# 결과 출력
if (i+1)%print_interval==0:
print(f"epoch: {i+1}, loss: {loss:.4e}")

딥러닝에서는 검증과정에서 accuracy보다 loss(Error)를 훨씬 더 많이 사용합니다.

Q. 왜 학습과 평가로 과대적합/과소적합을 평가하지 않나요?
A. 하이퍼파라미터를 계속 조절하면서 다시 한번 평가 데이터에 과대적합될 위험이 있기 때문에 평가 데이터는 최종 데이터로 남겨 놓고 학습과 검증 데이터를 통해 하이퍼파라미터 조절 & 과대적합, 과소적합을 확인합니다.
# 라이브러리 불러오기
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# 데이터 불러오기
data = fetch_california_housing()
# 데이터를 데이터프레임으로 변경(bunch → DataFrame): 문제와 정답 데이터 합치기
df = pd.DataFrame(data.data, columns=data.feature_names)
df["price"] = data.target
# 데이터를 텐서 형태로 변환 후 분리
data = torch.from_numpy(df.values).float()
# 문제와 정답으로 분리
X = data[:, :-1]
y = data[:, -1:] # 2차원으로 출력하기 위해 슬라이싱 사용(-1만 쓰면 vector 형태라 크기가 안 맞음)
# train, test, valid 분리 (1. 훈련, 평가 분리 → 2. 훈련 데이터 내에서 훈련, 검증 분리)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=21)
X_train, X_valid, y_train, y_valid = train_test_split(X_train, y_train, test_size=0.25, random_state=21)
print(X_train.shape, X_test.shape, X_valid.shape)
print(y_train.shape, y_test.shape, y_valid.shape)
torch.Size([12384, 8]) torch.Size([4128, 8]) torch.Size([4128, 8])
torch.Size([12384, 1]) torch.Size([4128, 1]) torch.Size([4128, 1])
std_scaler = StandardScaler()
# 스케일링을 진행하려면 numpy 배열 형태여야 함
std_scaler.fit(X_train.numpy())
# train, test, valid 변환
X_train = torch.from_numpy(std_scaler.transform(X_train.numpy())).float()
X_test = torch.from_numpy(std_scaler.transform(X_test.numpy())).float()
X_valid = torch.from_numpy(std_scaler.transform(X_valid.numpy())).float()
# Sequentual() 활용하여 모델 설계 → 간단하게 확인할 때 많이 사용
model = nn.Sequential(
nn.Linear(X_train.size(-1), 3)
, nn.ReLU()
, nn.Linear(3, 3)
, nn.ReLU()
, nn.Linear(3, 3)
, nn.ReLU()
, nn.Linear(3, y_train.size(-1))
)
n_epochs = 4000
print_interval = 100
# 배치 사이즈 설정 → 한 번의 학습에 사용하는 데이터 수
batch_size = 256
# 최적화 함수
optimizer = optim.Adam(model.parameters()) # lr은 기본값(0.001) 쓸 거라 따로 기재하지 않음
# 모델 학습 전 학습 상태 추적을 위한 변수 초기화
train_history, valid_history = [], []
# best 모델 저장할 변수
best_model = None
# 비교를 위한 최소 오차 → 가장 처음 들어온 오차보다 무조건 커야 함 (∴무한대로 설정)
lowest_loss = np.inf
# 학습 중단에 필요한 대기 epoch 수
early_stop = 200
# 학습 중단 기준이 되는 최소 손실값을 갖는 epoch 저장
lowest_epoch = np.inf
# 학습 반복 시작!
# 데이터를 복사하는 라이브러리
from copy import deepcopy
# 저장할 모델을 복사하여 best_model에 저장할 때 사용
for i in tqdm(range(n_epochs)):
indices = torch.randperm(X_train.size(0)) # 데이터 셔플용 인덱스 생성
# 훈련데이터를 랜덤 셔플링된 인덱스 번호를 통해서 재지정 → ※1
X_ = torch.index_select(X_train, index = indices, dim = 0)
y_ = torch.index_select(y_train, index = indices, dim = 0)
# 셔플링 된 데이터를 batch_size로 분리 → 미니 배치를 생성
X_ = X_.split(batch_size, dim=0)
y_ = y_.split(batch_size, dim=0)
# 훈련 손실, 검증 손실 결과를 저장하기 위해 변수 초기화
train_loss, valid_loss = 0, 0
# 예측 결과 저장을 위한 빈 리스트 생성 (검증 데이터의 예측 결과 누적)
y_pred_list = []
# 학습 ★
# batch_size로 분리된 X_, y_ 텐서들을 batch_size 개씩 훈련
for X_i, y_i in zip(X_,y_):
y_i_pred = model(X_i)
loss = F.mse_loss(y_i_pred, y_i)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 현재 batch_size의 데이터 loss를 더함 → epoch의 loss 결과는 모든 배치마다 loss의 평균
train_loss += float(loss)
# 현재 epoch의 loss를 계산 → train_loss 평균
train_loss = train_loss / len(X_)
# 검증 ★
# 학습된 모델을 통해 검증 데이터로 검증(손실 계산) → 역전파 x, 셔플링 x
with torch.no_grad():
X_ = X_valid.split(batch_size, dim=0) # 검증 데이터는 셔플링 안 함
y_ = y_valid.split(batch_size, dim=0)
# 반복문 (배치): 메모리 문제가 발생하기 때문에 검증도 동일하게 배치를 쪼개서 진행
for X_i, y_i in zip(X_,y_):
y_i_pred = model(X_i)
loss = F.mse_loss(y_i_pred, y_i)
valid_loss += float(loss)
# 검증 데이터 배치별 예측값을 리스트에 저장
y_pred_list.append(y_i_pred)
# 평균 검증 손실값 계산
valid_loss = valid_loss / len(X_)
# 훈련 손실값, 검증 손실값 list에 저장
train_history.append(train_loss)
valid_history.append(valid_loss)
# 학습 과정 출력하기
if (i+1) % print_interval == 0:
print(f"epoch: {i+1}, valid_loss: {valid_loss:.4e}, train_loss: {train_loss:.4e}, lowest_loss: {lowest_loss:.4e}")
# 모델 저장
# 현재 검증손실값이 지정된 최소손실값보다 작다면? 저장!
if valid_loss <= lowest_loss:
lowest_loss = valid_loss
lowest_epoch = i+1
# 현재 모델의 가중치를 저장
best_model = deepcopy(model.state_dict()) # 가중치를 딕셔너리 형태로 가져옴
# 모델 조기 학습 중단
else:
if (early_stop > 0) and (lowest_epoch+early_stop) < i+1:
print(f"{early_stop} epochs 동안 모델이 개선되지 않음")
break
print(f"{lowest_epoch + 1} epochs에서 가장 낮은 검증손실값: {lowest_loss}")
# 저장한 best_model 의 가중치를 model에게 로드해주기
model.load_state_dict(best_model)

※1: 순서를 셔플링한 결과를
X_train[indices]로 가져오면 편한데 왜 이렇게 가져오지 않는 걸까?
→X_train[indices]는 CUDA 오류가 발생할 수 있기 때문
: 텐서 내에서는 GPU 연산을 하다 보니 위와 같은 형식으로 인덱싱을 해서 데이터를 가져오다 보면 오류가 날 위험이 높습니다.
인프런 QnA 답변
PyTorch의torch.index_select와 인덱싱 연산자인[]를 통한 인덱싱 방법은 매우 비슷한 기능을 제공합니다. 둘 다 텐서에서 특정 인덱스를 기반으로 원하는 요소를 선택하도록 합니다.
그럼에도 불구하고, 코드에서 torch.index_select를 사용하는 특별한 이유가 있다면, 다음과 같은 이유들이 있을 수 있습니다:
1. 명시성:torch.index_select함수는 선택하려는 차원을 명시적으로 지정해야 합니다. 이는 코드를 읽는 사람에게 해당 연산이 어떤 차원에서 수행되는지 명확히 보여주는 장점이 있습니다.x[indices]표현식은 암시적으로 첫 번째 차원에서 인덱싱을 수행한다는 것을 가정하므로, 이 점이 명시성에서 약간의 차이를 만들 수 있습니다.
2. 성능: 일부 상황에서torch.index_select는[]연산자보다 약간 더 빠를 수 있습니다. 하지만 대부분의 일반적인 사용 사례에서는 성능 차이는 무시할 수 있을 정도로 작습니다.
3. 호환성: 일부 라이브러리 또는 환경에서는torch.index_select를 선호할 수 있습니다. 예를 들어, autograd의 역전파 계산이torch.index_select와 더 잘 작동할 수 있습니다.
그럼에도 불구하고, 주어진 상황에서torch.index_select(x, 0, indices)와x[indices]는 동일한 결과를 제공하므로, 어떤 방법을 선택할지는 주로 개발자의 선호도와 스타일에 따라 편하게 사용하시면 됩니다.
해당 내용에 대해 Perplexity가 답변한 내용:
torch.index_select(x, 0, indices)vs.x[indices]1. 기능적 차이
torch.index_select(x, dim, indices)
- 지정한 차원(
dim)에서 1D 인덱스 텐서(indices)에 따라 요소를 선택합니다.- 반환되는 텐서는 항상 입력 텐서와 동일한 차원을 가지며, 선택한 축만 인덱스의 길이로 바뀝니다.
- 인덱싱이 한 축에만 국한되므로 다차원 인덱싱을 원할 땐 반복적으로 사용하거나 추가 구현이 필요합니다.
- 고급 인덱싱 (
x[indices])
- 파이썬 스타일의 인덱싱을 사용하여 복수 차원, 불리언 마스크, 다차원 인덱싱 등 더 다양한 인덱스 방법을 지원합니다.
- 인덱스 형태에 따라 결과 차원이 달라질 수 있습니다.
- 내부적으로 더 많은 연산이 수행될 수 있어 상황에 따라 성능이 다를 수 있습니다.
2. 성능 차이
- 작은 텐서에서는
torch.index_select가 더 빠르고, 텐서 크기가 커질수록 고급 인덱싱(x[indices])이 더 나은 성능을 보이기도 합니다. 실제로 성능은 인덱스 모양, 반복성 여부, 연산량에 따라 달라집니다.index_select는 대체로 간단한 1D 인덱싱에 적합하며, 메모리 사용량 측면에서는 고급 인덱싱이 더 효과적일 수 있습니다.3. PyTorch에서
torch.index_select와 고급 인덱싱(X_train[indices])의 차이와 CUDA 관련 문제
- 두 인덱싱 방식의 차이
torch.index_select
- 연산 자체가 PyTorch 내부적으로 최적화되어 있어, 지정한
dim에 대해 효율적으로 인덱싱을 수행합니다.- 인덱스 텐서는 반드시 1-D
LongTensor여야 하며,input,index둘 다 GPU상(CUDA tensor)이면 완전히 GPU에서 연산됩니다.- 파이썬 스타일 고급 인덱싱 (
X_train[indices])
- 파이썬의 NumPy 스타일 고급 텐서 인덱싱(Advanced/Integer Indexing) 기능을 활용하는 방법.
- 내부적으로 더 많은 복잡성과 부가적인 자료구조 생성, 그리고 일부 경우 추가 메모리 사용을 수반합니다.
- 때로는 CPU와 GPU 간의 예기치 않은 데이터 이동이 발생할 수 있습니다.
- CUDA 문제(메모리/오류/성능) 이유
- 파이썬 스타일 고급 인덱싱은 GPU 메모리를 훨씬 더 많이 사용할 수 있습니다. (10배까지 메모리 사용량이 늘어나는 사례도 보고됨 → github)
- 고급 인덱싱은 새로운 텐서를 만드는데, 이 과정에서 임시 버퍼와 중간 객체가 대량으로 할당되므로, 대규모 데이터셋, 특히 대용량 model에선 CUDA out-of-memory 에러 위험이 높아집니다.
torch.index_select는 지정된 차원에서 직접 인덱싱하기 때문에 메모리 효율이 높으며, CUDA에서 추가적인 메모리 복사나 불필요한 장치 간 이동 없이 빠르게 작동합니다.- 장치 간 이동/호환성 문제
- 일부 고급 인덱싱 연산 과정에서 인덱스 텐서나 입력 텐서가 GPU와 CPU에 각각 있을 경우 PyTorch가 자동으로 데이터를 CPU에서 GPU로 혹은 그 반대로 이동시킬 수 있으며, 이 과정에서 오류 또는 예기치 않은 동작이 발생할 수 있습니다.
- 인덱싱에서 텐서가 GPU(CUDA)에 있을 때, PyTorch는 가능한 한 연산을 GPU에서 실행하지만, 고급 인덱싱 구현에 따라 일부 요소가 CPU에서 실행되어 속도가 급격히 저하되거나 오류가 발생하는 케이스가 보고됨.
- 또, CUDA에서 고급 인덱싱을 사용할 때 인덱스 범위 오류 등은 device-side assert로 매우 불명확한 에러 메시지를 내뱉거나, 정상적으로 예외처리되지 않습니다.
- 결정성 및 일관성
- 고급 인덱싱은 CPU와 GPU에서 결과가 다르거나 비결정적(non-deterministic)일 수 있습니다. 특히, 동일한 코드가 CPU에서는 매번 같은 결과이지만, GPU에서는 다를 수 있어 디버깅을 어렵게 만듭니다.
- 실전 코드 적용 권장 사항
- 덜 복잡한 설정에서는
index_select사용: 메모리 효율, 속도, 에러 처리 등 모든 면에서 안전하게 동작합니다. 입력/인덱스 텐서를 꼭 동일한 장치(GPU) 위에 올려놓으세요.- 고급 인덱싱은 작은 텐서, CPU 처리, 특수 목적에만: 대형 텐서나 반복적인 인덱싱이 필요한 상황에서 GPU에서는 피하고, 성능/메모리/디버깅 이슈에 유의해야 합니다.
- 요약:
- PyTorch에서
X_train[indices]와 같은 고급 텐서 인덱싱은 CUDA 환경에서 비효율적이거나 예기치 않은 메모리 과다 사용, 장치간 데이터 이동, 불명확한 에러 메시지, 결과의 비결정성 등 여러 문제가 발생할 수 있습니다. 반면,torch.index_select는 이런 이슈 없이 GPU에서 안전하고 효율적으로 동작하므로, 대규모 데이터 처리와 DNN 학습 루프에서는index_select사용을 권장합니다.- CUDA 환경에서
x[indices]를 사용할 때 가장 흔한 오류는 device-side assert입니다. 예를 들어, 인덱스가 범위를 벗어나면 CPU에서는IndexError가 발생하지만, CUDA에서는 불명확한 "device-side assert"나 "CUDA error: device-side assert triggered"라는 메시지가 나올 수 있습니다. 이 경우 실제 오류 원인이 바로 드러나지 않아 디버깅이 어려워질 수 있습니다.- 또한 인덱싱에 사용하는 텐서와 대상 텐서가 서로 다른 디바이스(CPU vs CUDA)에 있을 때도 오류가 발생할 수 있습니다. 모든 텐서가 동일한 디바이스에 있어야 합니다.
예시: CUDA "device-side assert" 오류 발생 상황
import torch x = torch.rand(10).cuda() indices = torch.tensor([11]).cuda() # 10개 요소밖에 없는데 11번 인덱스 참조 x[indices] # CUDA error: device-side assert triggered이 오류는 CPU에서는
IndexError로 명확하게 나오지만, CUDA에서는 애매한 에러 메시지를 뱉습니다. 따라서 디버깅 시CUDA_LAUNCH_BLOCKING=1를 사용해 동기적으로 에러를 확인하는 것이 좋습니다.정리
torch.index_select(x, dim, indices)는 간단하면서도 안전한 인덱싱을 제공하며, 1D 인덱스 및 한 축에만 사용할 때 적합합니다.x[indices]는 더 유연하지만(다차원, 불리언, 마스킹 등), 성능 및 직관성 측면에서 상황에 따라 다를 수 있고, 특히 CUDA 환경에서 인덱스가 잘못되거나 device 불일치 시 애매한 CUDA 오류가 발생할 수 있습니다.- 실수로 인한 범위 초과 인덱싱은 CPU에서는 명확한 에러가 드러나지만, CUDA에서는 디버깅이 까다로워질 수 있으니 주의가 필요합니다.
참고:- 인덱싱 변수, 대상 텐서 모두 같은 디바이스에 있어야 하며, 인덱스 범위 초과가 없도록 항상 관리하는 습관을 들이세요.
# 결과 시각화
# 추세 확인을 위해 로그로 스케일링
plt.subplots(figsize=(8,4))
plt.grid()
plt.plot(range(1, len(train_history)+1), train_history, label="train_loss")
plt.plot(range(1, len(valid_history)+1), valid_history, label="valid_loss")
plt.legend()
plt.yscale("log") # 줄어드는 값의 차이가 큰 경우 미세한 변화는 그래프 상 표현이 어려움
plt.show()

x_values = [1, 2, 3, 4, 5]
y_values = [10, 1000, 2000, 3000, 1000000000]
plt.figure(figsize=(5, 3))
plt.scatter(x_values, y_values)

x_values = [1, 2, 3, 4]
y_values = [10, 1000, 2000, 3000]
plt.figure(figsize=(5, 3))
plt.scatter(x_values, y_values)

# 로그 스케일 적용을 하면 좀 나아짐: 증가 비율을 그래프로 출력
# 상대적인 비율로 그래프에 출력
x_values = [1, 2, 3, 4, 5]
y_values = [10, 1000, 2000, 3000, 1000000000]
plt.figure(figsize=(5, 3))
plt.yscale("log") # 로그 스케일 적용 → 비율로 볼 수 있음
plt.scatter(x_values, y_values);

test_loss = 0
y_pred = []
with torch.no_grad():
X_ = X_test.split(batch_size, dim=0)
y_ = y_test.split(batch_size, dim=0)
for X_i, y_i in zip(X_,y_):
y_i_pred = model(X_i)
loss = F.mse_loss(y_i_pred, y_i)
test_loss += float(loss)
# 테스트 데이터 배치별 예측값을 리스트에 저장
y_pred.append(y_i_pred)
test_loss = test_loss / len(X_)
# 평가 후 예측값들을 하나의 텐서로 병합화여 전체 결과를 확인하기 위해 텐서 병합
y_pred = torch.cat(y_pred, dim=0)
s_his = sorted(zip(train_history, valid_history), key = lambda x: x[1])
print(f"train 데이터의 loss: {s_his[0][0]:.4e}")
print(f"valid 데이터의 loss: {s_his[0][1]:.4e}")
train 데이터의 loss: 3.4322e-01
valid 데이터의 loss: 3.6157e-01
from sklearn.metrics import r2_score
# 예측값: numpy 변환
y_pred_np = y_pred.detach().cpu().numpy()
# 실제값: numpy 변환
y_test_np = y_test.detach().cpu().numpy()
# r2 score 계산
r2 = r2_score(y_test_np, y_pred_np)
print(f"r2 score: {r2:.4f}")
r2 score: 0.7354
# 라이브러리 불러오기
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from copy import deepcopy # 데이터를 복사
from tqdm import tqdm
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
# 데이터 불러오기
data_org = load_breast_cancer()
# DataFrame으로 변경
df = pd.DataFrame(data_org.data, columns=data_org.feature_names)
# 정답 데이터 추가
df['target'] = data_org.target
# 데이터 분리(학습, 검증, 평가용 3개로 분리)
data = torch.from_numpy(df.values).float()
X = data[:,:-1]
y = data[:,-1:]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=21)
X_train, X_valid, y_train, y_valid = train_test_split(X_train, y_train, test_size=0.25, random_state=21)
print(X_train.shape, X_test.shape, X_valid.shape)
print(y_train.shape, y_test.shape, y_valid.shape)
torch.Size([341, 30]) torch.Size([114, 30]) torch.Size([114, 30])
torch.Size([341, 1]) torch.Size([114, 1]) torch.Size([114, 1])
# 히스토그램 확인
df.hist(bins=50, figsize=(15, 15))
plt.show()

# 스케일링 (X_train, X_test, X_valid)
std_scaler = StandardScaler()
std_scaler.fit(X_train.numpy())
X_train = torch.from_numpy(std_scaler.transform(X_train.numpy())).float()
X_test = torch.from_numpy(std_scaler.transform(X_test.numpy())).float()
X_valid = torch.from_numpy(std_scaler.transform(X_valid.numpy())).float()
# class 설계 MyModel → nn.Module 부모 클래스 상속
class MyModel(nn.Module):
# 함수 설계(self, 입력, 출력)
def __init__(self, input_dim, output_dim):
super().__init__()
self.input_dim = input_dim
self.output_dim = output_dim
# 신경망 레이어 정의: 5개층(25,20,15,10,5)+출력층
self.linear1 = nn.Linear(input_dim,25)
self.linear2 = nn.Linear(25,20)
self.linear3 = nn.Linear(20,15)
self.linear4 = nn.Linear(15,10)
self.linear5 = nn.Linear(10,5)
self.linear6 = nn.Linear(5,output_dim)
# 활성화 함수(ReLU, Sigmoid)
self.act_relu = nn.ReLU()
self.act_sigmoid = nn.Sigmoid()
# 연산과정 정의(함수→forward)
def forward(self, X):
# 모든 연산층 ReLU, 출력층의 활성화 함수 Sigmoid
h = self.act_relu(self.linear1(X))
h = self.act_relu(self.linear2(h))
h = self.act_relu(self.linear3(h))
h = self.act_relu(self.linear4(h))
h = self.act_relu(self.linear5(h))
y = self.act_sigmoid(self.linear6(h))
return y
# 모델 객체 생성 → model
model = MyModel(X_train.size(-1), y_train.size(-1))
# 최적화 함수 선택
optimizer = optim.Adam(model.parameters())
# 학습 횟수 및 출력 횟수 지정
n_epochs = 10000
batch_size = 32
print_interval = 10
early_stop = 100
best_model = None
lowest_loss = np.inf
lowest_epoch = np.inf
F.binary_cross_entropy()를 사용# 학습 코드 구현: 학습 반복문
train_history, valid_history = [], []
# 1. 반복 학습 (epoch 루프)
for i in tqdm(range(n_epochs)):
# 2. 훈련 데이터 셔플
indices = torch.randperm(X_train.size(0))
# 3. 미니배치로 분할
X_ = torch.index_select(X_train, dim=0, index=indices)
y_ = torch.index_select(y_train, dim=0, index=indices)
X_ = X_.split(batch_size, dim=0)
y_ = y_.split(batch_size, dim=0)
# 4. 훈련
train_loss = 0
y_pred = []
for X_i, y_i in zip(X_, y_):
y_i_pred = model(X_i)
loss = F.binary_cross_entropy(y_i_pred, y_i)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += float(loss)
y_pred.append(y_i_pred)
# 5. 평균 훈련 손실 계산
train_loss = train_loss / len(X_)
# 6. 검증
with torch.no_grad():
X_ = X_valid.split(batch_size, dim=0)
y_ = y_valid.split(batch_size, dim=0)
valid_loss = 0
y_pred = []
for X_i, y_i in zip(X_, y_):
y_i_pred = model(X_i)
loss = F.binary_cross_entropy(y_i_pred, y_i)
valid_loss += float(loss)
y_pred.append(y_i_pred)
valid_loss = valid_loss / len(X_)
# 7. 손실 기록
train_history.append(train_loss)
valid_history.append(valid_loss)
# 8. 진행 상황 출력
if (i+1)%print_interval == 0:
print(f"epoch: {i+1}, valid_loss: {valid_loss:.4e}, train_loss: {train_loss:.4e}, lowest_loss: {lowest_loss:.4e}")
# 9. 베스트 모델 저장
if valid_loss < lowest_loss:
lowest_loss = valid_loss
lowest_epoch = i+1
best_model = deepcopy(model.state_dict())
# 10. 조기 종료 (Early Stopping)
if (early_stop > 0) and (lowest_epoch + early_stop < i+1):
print(f"{early_stop} epochs 동안 모델이 개선되지 않음")
break
# 11. 베스트 모델 복원
model.load_state_dict(best_model)
# 12. 최종 성능 출력
print(f"{lowest_epoch + 1} epochs에서 가장 낮은 검증손실값: {lowest_loss}")

plt.subplots(figsize=(8,4))
plt.grid()
plt.plot(range(1, len(train_history)+1), train_history, label="train_loss")
plt.plot(range(1, len(valid_history)+1), valid_history, label="valid_loss")
plt.legend()
# plt.yscale("log")
plt.show()

test_loss = 0
y_pred = []
with torch.no_grad():
X_ = X_test.split(batch_size, dim=0)
y_ = y_test.split(batch_size, dim=0)
for X_i, y_i in zip(X_,y_):
y_i_pred = model(X_i)
loss = F.binary_cross_entropy(y_i_pred, y_i)
test_loss += float(loss)
# 테스트 데이터 배치별 예측값을 리스트에 저장
y_pred.append(y_i_pred)
test_loss = test_loss / len(X_)
# 평가 후 예측값들을 하나의 텐서로 병합하여 전체 결과를 확인하기 위해 텐서 병합
y_pred = torch.cat(y_pred, dim=0)
s_his = sorted(zip(train_history, valid_history), key = lambda x: x[1])
print(f"train 데이터의 loss: {s_his[0][0]:.4e}")
print(f"valid 데이터의 loss: {s_his[0][1]:.4e}")
train 데이터의 loss: 6.7226e-02
valid 데이터의 loss: 8.0191e-02
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix
# 예측값: numpy 변환
y_pred_np = y_pred.detach().cpu().numpy()
# 실제값: numpy 변환
y_test_np = y_test.detach().cpu().numpy()
y_pred_np = np.where(y_pred_np > 0.5, 1. , 0.) #0.5 보다 크면 1., 작으면 0.
# 정확도 계산
accuracy = accuracy_score(y_test_np, y_pred_np)
print(f"accuracy: {accuracy}")
# 정밀도 계산
precision = precision_score(y_test_np, y_pred_np)
print(f"precision: {precision}")
# 재현율 계산
recall = recall_score(y_test_np, y_pred_np)
print(f"recall: {recall}")
# F1-점수 계산
f1 = f1_score(y_test_np, y_pred_np)
print(f"F1-score: {f1}")
# 혼동 행렬 계산 및 시각화
conf_matrix = confusion_matrix(y_test_np, y_pred_np)
print("confusion matrix:\n", conf_matrix)
plt.figure(figsize=(8, 6))
sns.heatmap(conf_matrix, annot=True, fmt="d", cmap="Blues", xticklabels=["negative", "positive"], yticklabels=["negative", "positive"])
plt.xlabel("y_pred")
plt.ylabel("y_test")
plt.title("confusion matrix")
plt.show()

accuracy: 0.9912280701754386
precision: 0.9868421052631579
recall: 1.0
F1-score: 0.9933774834437086
confusion matrix:
[[38 1]
[ 0 75]]
