더불어서, 굳이 하나만 더 언급하자면, 우리가 만드는 모델이 분류(classification) 모델인지 예측(regression) 모델인지 따져볼 필요도 있겠다. (regression은 선형회귀라고 부르지만... 예측이라고 일단 하고 넘어가겠다.) 분류는 카테고리가 여러개인 것 중에 어떤 카테고리 하나가 선택되어야 하는지에 대한 문제이고, 예측은 어떤 값이 나와야 하는지에 대한 문제이다. 예를 들어서, 이미지를 바탕으로 고양이와 개라는 두 개의 카테고리를 두고 어느 것이 선택되어야 하는가에 대한 것이라면 분류 문제이고, 나이와 몸무게를 바탕으로 키가 얼마나 될지에 대한 값을 예측하는 것이라면 예측 문제다. (참고로, 예측할 값을 카테고리로 나누어서 분류 문제로 접근할 수도 있지는 하다.)
여튼 가장 처음 만들어볼 모델은 예측 모델이다. 앞서 들었던 나이와 몸무게를 바탕으로 키가 얼마나 될지를 예측하는 인공신경망을 만들어보자.
우선, 데이터를 만들어야 하는데, 어디선가 데이터를 불러올 수도 있겠지만, 여기서는 가상의 데이터를 만들도록 하겠다.
torch.randn(100, 2)함수로 shape이 (100, 2) (또는 torch.Size([100,2])인 torch tensor를 만들었다. 각각의 값은 0부터 1 사이의 임의의 값이고, 여기에 10씩 곱한 다음, 첫번째 열에는 25씩을, 두번째 열에는 70씩을 더했으니, X_features는 첫번째 열에는 평균이 25이고 편차가 10인 100개의 값이 들어있고, 두번째 열에는 평균이 70이고 편차가 10인 100개의 값이 들어있게 된다.
X_features는 모델에 입력으로 사용될 값들이고, 이제 출력으로 사용될 키에 대한 값들도 만들어 보자.
모양을 굳이 한 차원 더 만들어서 표시하는 데에는 앞으로 만들 인공신경망이 그렇게 원하기 때문이겠지. 그리고 확장성을 생각하더라도 출력이 1개만 있는 경우보다는 n개가 있는 경우들도 많을텐데, 그 모양을 (샘플개수, 출력개수)로 만들어 두는 것이 일반적일 것임.
아, 참고로 데이터의 모양을 표시하는 방법으로 numpy에서 사용하는 튜플 형태 (M, N)이 표시하는데 더 좋아서 그걸 선호해서 쓰겠음... pytorch에서는 위에서 보는 것과 같이 shape을 출력시켜 보면, torch.Siez([ ]) 형태로 출력해준다. 그게 더 명확하긴 한데.. 여튼 설명에는 좀 귀찮다;; ㅎㅎ;
from torch import nn
뉴럴네트워크 사용하기
설명은 Gemini의 주석달기 기능으로 달아두었다. ㅎㅎ
# ----------------- 1. 모델 클래스 정의 -----------------
class HeightPredictor(nn.Module):
# 이 모델 클래스는 nn.Module의 모든 기능을 상속받습니다.
# 모델의 '뼈대'와 '부품'을 준비하는 단계입니다.
def __init__(self, input_size=2, hidden_size=64, output_size=1):
# nn.Module의 초기화 기능을 먼저 실행합니다. (필수!)
super().__init__()
# nn.Sequential: 계층(Layer)들을 일렬로 순서대로 쌓아주는 컨테이너입니다.
self.regressor_stack = nn.Sequential(
# nn.Linear (선형 계층): Wx + b 연산을 수행합니다.
# input_size=2 (나이, 몸무게)를 64개의 은닉 뉴런으로 변환합니다.
nn.Linear(input_size, hidden_size),
# nn.ReLU (활성화 함수): 비선형성을 추가하여 모델의 표현력을 높입니다.
# ReLu는 x가 0보다 크면 x를 그대로, 0보다 작으면 0을 출력합니다.
nn.ReLU(),
# 은닉 계층 2: 64개의 뉴런 -> 64개의 뉴런으로 다시 변환합니다. (깊이를 추가)
nn.Linear(hidden_size, hidden_size),
nn.ReLU(),
# 최종 출력 계층: 64개의 뉴런 -> output_size=1 (키 예측값)으로 출력합니다.
# **회귀(Regression) 문제이므로, 마지막에 활성화 함수는 사용하지 않습니다.**
nn.Linear(hidden_size, output_size)
)
# 2. 데이터의 흐름 정의 (모델을 통해 데이터가 흘러가는 계산 과정)
def forward(self, x):
# 입력 데이터 텐서(x)를 위에서 정의한 계층 스택에 통과시킵니다.
predicted_height = self.regressor_stack(x)
# 계산된 최종 예측값(키) 텐서를 반환합니다.
return predicted_height
Keras로 먼저 공부를 했던 터라, Sequential이라는 용어가 뭔가 친숙하다. 물론 방식은 다르지만, 뭐 층층히 layer와 활성함수가 추가되는 구조가 직관적이다.
여기서 특이? 특별?한 점은 forward 함수를 따로 만들었다는 점이다. keras에서는 모델명.predict(입력)으로 결과를 얻었는데, 여기서는 클래스명.forward(입력)으로 결과를 얻겠네. 물론 클래스를 정의하기 나름이겠지만, 일단 Gemini가 가르쳐 주는 대로 따라가 보자.
# ----------------- 2. 모델 사용 예시 -----------------
# 모델 인스턴스 생성: __init__ 함수가 실행됩니다.
model = HeightPredictor()
# 가짜 입력 데이터 (Batch size: 5, Features: 2)
# torch.randn(5, 2)는 (5, 2) 모양의 표준 정규 분포 난수 텐서를 만듭니다.
dummy_batch = torch.randn(5, 2)
# forward() 실행: model(입력)을 호출하면 자동으로 forward 메서드가 실행됩니다.
predictions = model(dummy_batch)
# 결과 출력
print("### 모델 구조 (nn.Module이 자동으로 관리하는 계층): ###")
print(model)
print("-" * 30)
print(f"입력 데이터 모양: {dummy_batch.shape}") # (5, 2)
print(f"출력 예측값 모양: {predictions.shape}") # (5, 1) - 5개 샘플의 키 예측값
print(prediction)
모델이 어떻게 생겼는지, 입력과 출력이 어떤 모양을 갖는지 볼 수 있다. 참고로 여기서 dummy_batch는 정말 아무런 값을 만들어 (실제 입력 범위랑도 다름) 넣었고, 모델도 학습되지 않은 상태이기 때문에 아무런 출력을 내뱉었다. ㅎㅎ 일단 모양만 눈여겨 보자.
import torch.optim as optim
# 1. 모델 인스턴스 (Model Instance)
model = HeightPredictor() # Step 4에서 정의한 모델
# 2. 손실 함수 (Loss Function)
loss_fn = nn.MSELoss()
# 3. 옵티마이저 (Optimizer)
optimizer = optim.Adam(model.parameters(), lr=0.01)
print("모델 학습을 위한 준비 완료: Model, Loss Function, Optimizer 설정 완료!")
이제 손실함수와 옵티마이저 설정을 마쳤다. 자꾸 Keras랑 비교하게 되긴 하지만... (당분간은 어쩔 수 없다 나는 그게 더 익숙한 몸이니...) 손실함수랑 옵티마이저를 바깥에다가 따로 준비시켜 두네.
# 필요한 모듈 및 설정 불러오기
import torch
from torch import nn
import torch.optim as optim
import matplotlib.pyplot as plt # 시각화를 위한 Matplotlib 라이브러리
# (이전 Step 4 & 5에서 정의했던 모델, 데이터, 손실 함수, 옵티마이저 설정은 동일)
# ... (model, X_features, y_labels, loss_fn, optimizer 정의 부분 생략)
# ----------------------------------------------------
# 학습할 에포크(Epoch) 횟수 설정
num_epochs = 1000
# 손실 값을 기록할 리스트 준비
loss_history = []
print(f"모델 학습 시작: {num_epochs} 에포크")
for epoch in range(num_epochs):
optimizer.zero_grad()
predictions = model(X_features)
loss = loss_fn(predictions, y_labels)
# 현재 손실 값을 리스트에 기록
loss_history.append(loss.item())
loss.backward()
optimizer.step()
if (epoch + 1) % 100 == 0:
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}")
print("모델 학습 완료!")
1000번 학습 완료.
import matplotlib.pyplot as plt # 시각화를 위한 Matplotlib 라이브러리
# 손실 그래프 그리기 (Plotting the Loss)
plt.figure(figsize=(10, 6)) # 그래프 크기 설정
plt.plot(loss_history, label='Training Loss', color='blue') # 기록된 손실 값으로 그래프 생성
plt.title('Training Loss over Epochs') # 그래프 제목
plt.xlabel('Epoch') # X축 라벨
plt.ylabel('Mean Squared Error (MSE) Loss') # Y축 라벨
plt.grid(True) # 그리드 표시
plt.legend() # 범례 표시
plt.show() # 그래프 출력
손실값 그래프도 출력
뭔가 더 돌리면 더 줄어들 수도 있을 것 같은데, 1000번 더 돌려본다.
1000번 더?
이제 더 학습시키는 건 의미가 없어 보인다.
이제 모델을 이용해 예측을 해보자.
# 새로운 가상 데이터 (나이 30세, 몸무게 80kg)로 예측
new_input = torch.tensor([[30.0, 80.0]], dtype=torch.float32)
# **평가 모드 설정 (Evaluation Mode)**
# 드롭아웃(Dropout) 등 학습 때만 필요한 기능들을 비활성화합니다.
model.eval()
# 미분 추적 비활성화 (Disable gradient tracking)
# 예측할 때는 미분 계산이 필요 없으므로 메모리 절약 및 속도 향상을 위해 사용합니다.
with torch.no_grad():
predicted_height = model(new_input)
# 원래 데이터 생성 공식에 따르면 예상 키는 (30 * 0.5) + (80 * 1.5) + 50 = 15 + 120 + 50 = 185cm 근처
print(f"\n입력 (나이, 몸무게): (30, 80)")
print(f"예측된 키: {predicted_height.item():.2f} cm")
185cm 근처로 값이 나왔다.
다음 질문,
우리는 현재 모델이 일반화된 특징을 학습했다고 할 수 있을까?
1. 3000회를 학습시키면서 사실상 오버피팅을 의도했다.
2. 검증 데이터가 없었다.
그래서 Gemini한테 시켜서 다음의 코드로 그래프를 찍어보았다.
import torch
import matplotlib.pyplot as plt
import numpy as np
# -------------------- Step 4, 6에서 사용된 변수 가정 --------------------
# model: 학습이 완료된 HeightPredictor 모델 인스턴스
# X_features: (100, 2) 모양의 입력 데이터 텐서 (나이, 몸무게)
# y_labels: (100, 1) 모양의 실제 키 레이블 텐서
# ----------------------------------------------------------------------
# 모델을 평가 모드로 전환 (Dropout 등 비활성화)
model.eval()
# 미분 추적 비활성화 (메모리 및 속도 최적화)
with torch.no_grad():
# 1. 모델의 예측값 얻기
# 모든 학습 데이터 X_features에 대한 모델의 예측을 수행합니다.
predicted_y = model(X_features)
# 2. 텐서를 NumPy 배열로 변환 (Matplotlib 사용을 위함)
# CPU 텐서로 변환 (.cpu()) 후, NumPy 배열로 변환 (.numpy())
X_numpy = X_features.cpu().numpy()
y_numpy = y_labels.cpu().numpy()
predicted_numpy = predicted_y.cpu().numpy()
# 3. 모델 수식 그래프를 위한 가상 데이터 생성 (몸무게만 변화)
# 몸무게 범위를 설정하고, 나이는 평균 나이(25.0)로 고정하여 예측 라인을 그립니다.
weight_range = np.linspace(X_numpy[:, 1].min(), X_numpy[:, 1].max(), 100).reshape(-1, 1)
# 나이는 평균값(25.0)으로 고정하여 (100, 2) 모양의 입력 텐서 생성
mean_age = torch.full((100, 1), 25.0)
test_weights = torch.tensor(weight_range, dtype=torch.float32)
# 모델 입력 텐서: (100, 2) 모양의 [고정된 평균 나이, 변화하는 몸무게]
input_for_line = torch.cat((mean_age, test_weights), dim=1)
# 모델 예측
line_predictions = model(input_for_line)
line_numpy = line_predictions.cpu().numpy()
# 4. 실제 수식 라인 (Ground Truth Formula) 계산
# y = (나이 * 0.5) + (몸무게 * 1.5) + 50
true_line = (25.0 * 0.5) + (weight_range * 1.5) + 50
# -------------------- 5. 그래프 시각화 --------------------
plt.figure(figsize=(12, 7))
# 1. 실제 데이터 포인트 (Actual Data Points) - 산점도 (Scatter Plot)
# X_numpy[:, 1]는 몸무게(두 번째 특징)를 의미합니다.
plt.scatter(X_numpy[:, 1], y_numpy, color='skyblue', alpha=0.6, label='Actual Data Points (Weight vs. Height)')
# 2. 모델이 예측한 라인 (Learned Model Prediction Line)
# 모델이 학습한, 나이를 25세로 고정했을 때의 키 예측 라인
plt.plot(weight_range, line_numpy, color='red', linewidth=3, label='Learned Model Prediction Line (Age=25)')
# 3. 실제 데이터 생성 수식 라인 (Ground Truth Formula Line)
# 데이터가 생성된 실제 수식의 라인
plt.plot(weight_range, true_line, color='green', linestyle='--', linewidth=2, label='Ground Truth Formula (Age=25)')
plt.title('Height Prediction Visualization: Model vs. True Formula')
plt.xlabel('Weight (kg) [Input Feature]')
plt.ylabel('Height (cm) [Predicted/Actual Value]')
plt.legend()
plt.grid(True)
plt.show()
25세 기준으로 몸무게와 키 그래프를 확인해보았다. 생각보다 오버피팅이 덜하다. ㅎㅎ 아마도 우리 모델 자체가 파라메터 수가 많지 않아서였거나, 데이터 자체에 랜덤 값을 충분히 넣어둬서 였을지도.
그나저나 위 시각화 코드도 한 번 공부해봐야 할텐데...
모델을 평가 모드로 전환하는 부분과, with torch.no_grad():를 통해 미분 추적 비활성화를 하는 부분은 눈여겨볼만하다. 그리고 torch tensor에서 numpy 배열로 바꿔줘야 matplotlib이 사용가능한데, 이거 은근 번거롭겠네. (물론 이젠 인공지능이 다 알아서 해주겠지만..;; ㅎㅎ;;) 아, 그리고 numpy는 cpu 기반이라고 하네. 그래서 torch tensor에서 cpu()를 호출한 뒤에 바꾸네. 이것도 익숙해져야 할듯.
그러고보니 앞서 모델 학습시킬 때 특별히 GPU 사용 여부를 설정하지 않았는데, GPU를 사용하고 싶다면 아래와 같이 해야 한다고 Gemini가 알려준다. (맞겠지 ㅎㅎ. 나중에 무거운 모델 학습시킬 때 확실히 비교해볼 수 있겠다.)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"사용할 장치: {device}")
model = HeightPredictor()
X_features = ... # 데이터 텐서
y_labels = ... # 레이블 텐서
model.to(device)
X_features = X_features.to(device)
y_labels = y_labels.to(device)
이런 식으로 GPU 장치 확인 후 모델과 입출력 데이터를 to(device)로 보내면 된다고 한다. 순전파, 손실 계산, 역전파 코드는 그대로 사용해도 됨.
다음 글은 MINIST 데이터와 CNN 모델로 손글씨 데이터 분류하는 예제를 만들어봐야겠다.