본문 바로가기

명사 美 비격식 (무리 중에서) 아주 뛰어난[눈에 띄는] 사람[것]

Personal/Book

파이토치 딥러닝 마스터 - 1부 - 5장 학습기법

독일의 수학 천문학자인 요한네스 케플러는 1600년대 초 행성의 운동에 대한 세가지 법칙을 발견한다. 그의 멘토 튀코 브라헤가 맨눈으로 밤하늘을 관찰한 결과를 종이에 적은 데이터에 기반했다. 뉴튼의 중력법칙이없던 시절 케플러는 데이터에 부합하면서도 최대한 단순한 기하 모델을 추론하려 노력했지만 뭔가 이치에 잘 맞지않는 데이터를 6년간 관찰하며 깨달음을 얻어낸 뒤에 3가지 법칙을 만들어냈다. 

 - 모든 행성의 궤도는 타원이며 두 초점중 하나에 태양이 위치한다. 

 - 행성과 태양을 이은 선분이 같은 시간동안 지나면서 만드는 면적은 일정하다. ...

케플러는 궤도가 왜 타원인지 모른채 행성이나 위성을 관찰해 타원의 궤도 모양 크기를 측정하고 데이터를 기반으로 천체 이동경로와 곤측을 통해 행성이 언제 어떤 지점에 머무를것인지도 예츨 할 수 있었다. 케플러의 회고는 신천문학 저서나 증명의기원들이라는 책에서 볼 수 있다. 

6년간 케플러가 이뤄낸 업적을 요약해보자. 동료 브라헤로부터 힘들이지않고 좋은 데이터를 많이 얻었고 수상한점에 가시화해보려고 시도했으며 데이터에 가장 잘 부합할만한 단순한 모델 타원을 골라 데이터를 쪼개서 일부만 사용하고 나머지는 검증을 위해 남겨뒀다. 잠정적으로 타원에 대한 이심률과 크기를 정하고 이 모델이 관찰결고와 맞을때까지 반복했으며 별도의 관찰을 통해 자신의 모델을 검증한 후 의심스러운 부분을 되돌아보았다. 수세기 동안 이 단계를 벗어나는것은 재앙에 이르는 지름길임을 우리는 배워왔다. 이 과정은 우리가 데이터로부터 무언가를 배울때 정의하는 순서와 정확하게 일치하며 fitting해 학습하는 알고리즘을 만드는 과정도 이와 같다. 항상 데이터로부터 평가되는 알 수 없는 파라미터를 가진 함수가 있음을 전제로 이 함수를 모델이라 일컫는다. 

 

학습은 파리미터 추정에 불과하다. 

데이터 수집 - 데이터 시각화 - 선형모델을 골라 시도 - 손실을 줄이기 위한 방안의 손실함수 - 경사하강 알고리즘을 사용한 파라미터 관점에서 손실함수 최적화 - 손실줄이기 손실이 줄어드는 방향으로 파라미터 값을 바꿔나간다. 손잡이를 만지작거리는 것처럼 w와 b릐 값을 조금 늘리거나 줄여서 그 사이 손실값이 얼마나 변하는지를 확인 이를 머신러닝에서는 주로 learning_rate라는 변수명을 사용한다. - 분석 각 파라미터에 대한 손실함수의 편미분을 구하고 미분벡터에 넣어 기울기 계산하기 - 모델 적합을 위한 반복. 여러 조건이있으나 우선 정한 횟수만큼 반복하며 이를 epoch라 부른다. - 재시각화 

import torch
import torch.nn as nn
import torch.optim as optim

# -----------------------------
# 1. 가짜 데이터 (y = 2x + 1 + noise)
x = torch.unsqueeze(torch.linspace(-1, 1, 100), dim=1)
y = 2 * x + 1 + 0.1 * torch.randn(x.size())

# -----------------------------
# 2. 선형 모델
model = nn.Linear(1, 1)

# -----------------------------
# 3. 손실함수
criterion = nn.MSELoss()

# -----------------------------
# 4. optimizer (learning rate 포함)
optimizer = optim.SGD(model.parameters(), lr=0.1)

# -----------------------------
# 5. 학습 (epoch 반복)
epochs = 50

for epoch in range(epochs):
    # forward
    y_pred = model(x)
    loss = criterion(y_pred, y)

    # backward (gradient 계산)
    optimizer.zero_grad()
    loss.backward()

    # parameter update (경사하강)
    optimizer.step()

    if epoch % 10 == 0:
        w = model.weight.item()
        b = model.bias.item()
        print(f"epoch {epoch} | loss: {loss.item():.4f} | w: {w:.3f} | b: {b:.3f}")

# -----------------------------
# 6. 최종 파라미터 확인
print("\n최종 w, b")
print(model.weight.item(), model.bias.item())

 

 

 

chainn rule 연쇄 규칙을 사용해 미분을 역방향으로 전파하는 방법을 통해 모델의 손실에 대한 합성함수의 기울기를 계산했다. 매우 복잡한 선형과 비선형 합성 함수의 미분에 대한 해석 가능한 표현식을 작성하는 작업은 느리고 즐겁지않다. 이러한 이유로 파이토치에 자동미분기능과 함께 텐서가 등장했다. 텐서는 자신이 어디서 왔는지 어떤 연산을 수행해서 만들어진것인지를 기억해 자연스럽게 미분을 수동으로 도출할 필요가없다. 자동미분. requires_grad= T인자는 params에 가해지는 연산결과로 만들어지는 모든 텐서를 이은 전체 트리를 기록하라는 의미. params를  조상으로 두는 모든 텐서는 그 사이 모든 함수에 접근할 권한을 가져 미분값은 params텐서의 grad 속성으로 자동 기록된다. 일반적으로 모든 파이토치 텐서는 grad 속성을 가지고 주로 값은 none이다 .이 값을 얻기 위해 require_grad를 true로 지정하고 손실값을 구해 loss텐서에 대해 backward를 호출하는 것 뿐. 연쇄적으로 연결된 함수들의 grad 속성에 누적한다. 파라미터 조정을 위해 사용한 후에는 기울기를 명시적으로 다시 0으로 초기화할 필요가 있다. 텐서 자체를 바꿔치기하는 zero_ 메소드를 사용해서 쉽게 초기화할 수 있다. 우리는 수작업으로 미분 계산이 가능하지만 굳이 그렇게 할 필요는 없다.

import torch

# -----------------------------
# 파라미터 (자동미분 활성화)
w = torch.tensor(1.0, requires_grad=True)
b = torch.tensor(0.0, requires_grad=True)

# -----------------------------
# 데이터
x = torch.tensor([1.0, 2.0, 3.0])
y = torch.tensor([3.0, 5.0, 7.0])

# -----------------------------
# forward (합성함수 생성)
y_pred = w * x + b
loss = ((y_pred - y) ** 2).mean()

print("loss:", loss.item())

# -----------------------------
# backward (chain rule 자동 적용)
loss.backward()

print("w grad:", w.grad)
print("b grad:", b.grad)

# -----------------------------
# 파라미터 업데이트 (경사하강)
lr = 0.1
with torch.no_grad():
    w -= lr * w.grad
    b -= lr * b.grad

# -----------------------------
# gradient 초기화 (zero_ 사용)
w.grad.zero_()
b.grad.zero_()

print("\nafter update")
print("w:", w.item(), "b:", b.item())
print("w grad:", w.grad, "b grad:", b.grad)

 

 

최적화알고리즘 리스트는 간단하게 추렸을때 다음과 같다. 

import torch.optim as optim

ASGD, Adadelta, Adagrad, Adam, Adamx, LBFGS, Optimizer, RMSprop, Rprop, SGD, SparseAdam..


훈련루프에서 다른 옵티마이저를 테스트하려면 인스턴스 이름만 바꾸면된다. 동일하게 단순히 model함수만을 바꿀수도있다. 근사하려는 함수가 어떤 형태인지 알필요가없다.우리는 머신러닝 내부 동작을 알고 필수적인 개념을 배워 복잡한 형태의 딥러닝 모델을 훈련시킬수있고 기울기를 추정하기 위한 역전파, 자동미분, 모델 가중치의 최적화 경사하강이나 다른 옵티마이저를 통해 수행하는 것을 배웠다. 별거없다. 나머지는 범위가 넓긴하나 대부분 빈칸 채워넣기다. 

import torch
import torch.nn as nn
import torch.optim as optim

# -----------------------------
# 더미 데이터 (y = 3x + noise)
x = torch.linspace(-1, 1, 50).unsqueeze(1)
y = 3 * x + 0.1 * torch.randn_like(x)

# -----------------------------
# 간단한 선형 모델
class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(1, 1)

    def forward(self, x):
        return self.fc(x)

# -----------------------------
# optimizer 리스트
optimizers = [
    optim.SGD,
    optim.ASGD,
    optim.Adagrad,
    optim.RMSprop,
    optim.Adadelta,
    optim.Adam,
    optim.Adamax
]

# -----------------------------
# optimizer 하나씩 바꿔가며 학습
for opt_class in optimizers:

    model = Model()
    criterion = nn.MSELoss()

    optimizer = opt_class(model.parameters(), lr=0.1)

    print(f"\n===== {opt_class.__name__} =====")

    for epoch in range(50):
        # forward
        pred = model(x)
        loss = criterion(pred, y)

        # backward
        optimizer.zero_grad()
        loss.backward()

        # update
        optimizer.step()

    # 결과 확인
    w = model.fc.weight.item()
    b = model.fc.bias.item()

    print("final loss:", loss.item())
    print("w:", w, "b:", b)

 

 

 

모든 옵티마이저 생성자는 필미트 리스트를 받는다. 옵티마이저 객체 내부에 유지되며 값을 조정하고 grad 속성에 접근할 때 사용된다.

모든 옵티마이저는 zero_grad와 step이라는 두가지 메소드를 제공하며 grad 속성값을 0으로 만들고 옵티마이저별로 구현된 최적화 전략에 따라 ㅍ라미터 값을 조정한다.  SGD는 확률적 강사하강으로 순정 버전의 경사 하강과 완전히 동일하다. 여러 샘플 중 임의로 뽑은 일부에 대해 평균을 계산해 얻기 때문에 확률적이라는 말을 사용한다.

import torch
import torch.nn as nn
import torch.optim as optim

# -----------------------------
# 간단한 모델
model = nn.Linear(1, 1)

# -----------------------------
# 옵티마이저 생성 (파라미터 리스트 전달)
optimizer = optim.SGD(model.parameters(), lr=0.1)

# -----------------------------
# 더미 데이터
x = torch.tensor([[1.0], [2.0], [3.0]])
y = torch.tensor([[2.0], [4.0], [6.0]])

# -----------------------------
# forward
y_pred = model(x)
loss = nn.MSELoss()(y_pred, y)

# -----------------------------
# backward (grad 계산)
loss.backward()

print("before step")
for name, p in model.named_parameters():
    print(name, p.data, p.grad)

# -----------------------------
# optimizer step (SGD 업데이트)
optimizer.step()

# -----------------------------
# gradient 초기화
optimizer.zero_grad()

print("\nafter step")
for name, p in model.named_parameters():
    print(name, p.data, p.grad)

 

 

 

 

사용하던 데이터와는 다른 별개의 데이터를 사용하면 기대했던 것보다 높은 손실값을 얻으며 이 현상을 과적합 overfitting이라고 한다. 주로 모델이 일반화된 학습을 수행하는 대신 훈련셋의 출력을 암기하는 식으로 전개되기 때문에 발생한다. 

결국 우리 삶도 적합과 과적합 사이를 오갈뿐이다. 

reanperm 함수는 색인 순열을 찾기 위해 텐서 요소를 섞었을때 인덱스 리스트를 반환한다. 인덱스 텐서를 얻어 훈련셋과 검증셋을 만들어 훈련한다. 부가적으로 각 에포크맏 부가적으로 검증셋에 대한 손실을 계산해서 과적합하고 있는지를 파악하는 부분을 추가한다. 검증데이터는 학습하면 안됨으로 val_loss.backward()가 없다. 

import torch
import torch.nn as nn
import torch.optim as optim

# -----------------------------
# 데이터 생성
x = torch.linspace(-1, 1, 100).unsqueeze(1)
y = x ** 3 + 0.2 * torch.randn_like(x)

# -----------------------------
# 데이터 섞기 (permutation)
perm = torch.randperm(len(x))

train_size = int(0.8 * len(x))
train_idx = perm[:train_size]
val_idx = perm[train_size:]

x_train, y_train = x[train_idx], y[train_idx]
x_val, y_val = x[val_idx], y[val_idx]

# -----------------------------
# 모델
model = nn.Sequential(
    nn.Linear(1, 32),
    nn.ReLU(),
    nn.Linear(32, 1)
)

criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)

# -----------------------------
# 학습 + 검증 루프
epochs = 100

for epoch in range(epochs):

    # ---- train ----
    model.train()
    pred = model(x_train)
    train_loss = criterion(pred, y_train)

    optimizer.zero_grad()
    train_loss.backward()
    optimizer.step()

    # ---- validation ----
    model.eval()
    with torch.no_grad():
        val_pred = model(x_val)
        val_loss = criterion(val_pred, y_val)

    if epoch % 10 == 0:
        print(f"epoch {epoch} | train_loss: {train_loss.item():.4f} | val_loss: {val_loss.item():.4f}")