본문 바로가기

AI

PyTorch 스터디 2주차 (PyTorch 공식 튜토리얼 공부하기)

 

참고 자료

 

Quickstart — PyTorch Tutorials 2.6.0+cu124 documentation

Note Click here to download the full example code Learn the Basics || Quickstart || Tensors || Datasets & DataLoaders || Transforms || Build Model || Autograd || Optimization || Save & Load Model Quickstart Created On: Feb 09, 2021 | Last Updated: Jan 24,

pytorch.org

노션으로 보기 (더 깔끔함)

샘플 데이터셋 다운받기

torch.utils.data.Dataset
torch.utils.data.DataLoader
  • Dataset: 데이터셋의 형태, 데이터셋을 불러오는 방법이 정의되어있음
  • DataLoader: 정의된 데이터셋을 학습에 사용하기에 편리하도록 함. batch 단위로 묶어주고, shuffle하거나, 병렬처리 parallel 해주는 wrapper 클래스.
DataLoader(  # [DataLoader의 주요 properties]
    dataset,  # 필수 인자: Dataset 객체
    batch_size=1,  # 배치 크기
    shuffle=False,  # 데이터 섞기 여부
    num_workers=0,  # 병렬 처리를 위한 프로세스 수
    drop_last=False,  # 마지막 배치가 batch_size보다 작을 때 버릴지 여부
    pin_memory=False,  # GPU 메모리에 고정할지 여부
    collate_fn=None,  # 배치를 어떻게 구성할지 커스텀하는 함수
    sampler=None,  # 데이터 샘플링 방식 지정
    batch_sampler=None,  # 배치 단위 샘플링 방식 지정
    timeout=0,  # 데이터 로딩 시 타임아웃 설정
    prefetch_factor=2,  # 데이터 미리 가져오기 비율
)

 

📌 Wrapper class 래퍼 클래스
- 개념: 다른 클래스를 감싸서 새로운 기능을 추가하거나, 인터페이스를 변경하는 기능을 하는 클래스
- 다른 클래스를 감싸서 새로운 기능 추가

 

# 원본 클래스
class Coffee:
    def get_cost(self):
        return 3000

# 래퍼 클래스
class CoffeeWithMilk:
    def __init__(self, coffee):
        self.coffee = coffee  # 원본 객체를 내부에 저장 -> 기존 기능 사용 가능
        
    def get_cost(self):
        return self.coffee.get_cost() + 500  # 새로운 기능 추가할 수 있음
  • 인터페이스 변경
# 레거시(기존) 클래스
class OldCalculator:
    def add(self, x, y):
        return x + y
    
    def subtract(self, x, y):
        return x - y

# 새로운 인터페이스를 원하는 상황
# 예: 문자열로 계산식을 받아서 처리하고 싶음
class CalculatorWrapper:
    def __init__(self):
        self.calculator = OldCalculator()
    
    def calculate(self, expression):  # "1 + 2" 같은 문자열을 받음
        # 문자열을 파싱
        x, op, y = expression.split()
        x, y = int(x), int(y)
        
        # 기존 메서드를 새로운 방식으로 사용
        if op == '+':
            return self.calculator.add(x, y)
        elif op == '-':
            return self.calculator.subtract(x, y)

# 사용 예시
old_calc = OldCalculator()
print(old_calc.add(1, 2))  # 기존 방식: 유저는 add(), subtract() 2개의
print(old_calc.subtract(4, 2))  # 메소드를 알고 있어야 함.

new_calc = CalculatorWrapper()
print(new_calc.calculate("1 + 2"))  # 새로운 방식: 유저는 new_calc()
print(new_calc.calculate("4 - 2"))  # 1개의 메소드만 알면 됨.

# 유저는 코드의 작동 원리를 굳이 몰라도 됨. 유저가 마주하는 건 단순한 사용법이면 됨!

 

 

📌 Parrallel 병렬 처리
- 장점
   - 처리 속도가 빨라짐, CPU 자원을 효율적으로 사용, 대용량 데이터 처리시 유용
- 단점
   - 메모리 사용량 증가, 프로세스 간 통신 오버헤드 발생, 디버깅이 더 어려워짐
      - 통신 오버헤드: 프로세스들이 데이터를 주고받을 때 발생하는 추가 시간&자원 소모
  • TorchText, TorchVision, TorchAudio: 각각 텍스트 데이터셋, 시각 데이터셋, 음성 데이터셋을 갖고 있음
from torchvision import datasets
from torchvision.transforms import ToTensor

 

Creating Models

class NeuralNetwork(nn.Module): # nn.Module 상속
    def __init__(self): # 클래스 초기화
        super().__init__() # 부모 클래스 초기화
        self.flatten = nn.Flatten() # 평탄화
        self.linear_relu_stack = nn.Sequential( 
		        # nn.Linear(input data size, output data size)
		        # == y = ax + b 선형 변환 (직선만 생성)
            nn.Linear(784, 512), # input size == flattened data size
            nn.ReLU(), # activatation 함수: 비선형 변환 추가 (곡선 추가)
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10) # 아웃풋 클래스(y 라벨)는 총 10개
        )
  • PyTorch 패키지의 nn.Module 클래스 상속 -> nn.Module에서 정의된 메소드, 프로퍼티 그대로 사용 가능
  • super().__init__() : 부모 클래스 nn.Module 초기화 (그냥 정해진 문법임)
    • 클래스 초기화 개념이 명확히 이해는 안 됐는데 클래스에게 사용할거라고 미리 신호 줘서 기능들 준비시키는 느낌인듯?
    • 초기화를 하지 않으면 정의한 메소드, 프로퍼티가 제대로 작동 안 하고 여러 문제 발생 가능
  • 평탄화 Flatten
    • 변환 방법: torch.Size([64, 1, 28, 28]) → torch.Size([64, (12828)]) = torch.Size([64, 784])
      • 64는 Batch size, 784가 평탄화된 데이터. 기존 3차원 데이터를 1차원 데이터로 바꿈.
    • 목적: nn.Linear() 은 1차원 입력만 받을 수 있어서 여기에 데이터 넣으려면 1차원으로 바꿔야 함.
  • nn.Linear(784, 512) 계산 방법
# 입력 x: 784개의 값
x = [x1, x2, ..., x784]

# 출력 y: 512개의 값
y = [y1, y2, ..., y512]

# 각각의 출력 y는 모든 입력 x와 연결됨
y1 = w11*x1 + w12*x2 + ... + w1_784*x784 + b1
y2 = w21*x1 + w22*x2 + ... + w2_784*x784 + b2
...
y512 = w512_1*x1 + w512_2*x2 + ... + w512_784*x784 + b512
  • w(가중치), b(편향)은 처음에는 랜덤하게 정해지고, 학습하면서 최적화됨
    • 첫번째 예측 → 정답 값과 비교했을 때 loss값 계산 → 오차를 줄이는 방향으로 w, b 바꿔감.
    • Loss function : x축은 (w, b), y축은 loss값. local minimum이나 global minimum을 찾으면, 그게 loss값이 가장 작을 때(== 정답에 가장 가까울 때)
    • 오차 줄이는 방향은 경사하강법 기법 사용! 주로 미니 배치 경사하강법을 사용한다고 함.
# 위 NeuralNetwork 클래스에 이어서...
    def forward(self, x): # 실제로 외부에서 호출하는 함수
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits

model = NeuralNetwork().to("cpu")
  • model.forward(input_data) : 그냥 model() 하면 forward()가 호출된 것으로 간주됨. 굳이 model.forward() 할 필요 없음.
  • to(device) : 연산을 어떤 하드웨어에서 할 건지 정함. 모델과 데이터가 같은 하드웨어에 있어야 함!

Optimizing the Model Parameters

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
  • Optimizer
    • 정의: 파라미터(가중치 w, 편향 b)를 학습하는 방법을 구현한 도구
    • 역할: loss 기반으로 파라미터 업데이트, 학습률(Learning rate) 관리, 경사하강법 구현
    • 종류: SGD, Adam, …
    • lr: Learning rate, 경사하강법에서 한 번에 얼마나 움직일지 정함. 여기서는 0.001 사용
def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    model.train() # 모델을 학습 모드로 사용 
    for batch, (X, y) in enumerate(dataloader):
        X, y = X.to(device), y.to(device)

        # Compute prediction error
        pred = model(X)
        loss = loss_fn(pred, y)

        # Backpropagation
        loss.backward() # 역전파 수행. 각 파라미터(w, b)에 대해 gradient(기울기) 계산
        # 연쇄법칙 활용해서 미분하는 거 같은데 잘 모르겠음.
        optimizer.step() # 계산된 gradient를 활용해서 모델 파라미터 업데이트, 경사하강법 수행
        # parameter = parameter - (learning_rate * gradient)
        optimizer.zero_grad() # 다음 역전파를 위해 기울기 0으로 초기화.
        # 초기화 설정 안 하면 기울기가 계속 누적됨. PyTorch 기본 설정은 기울기 누적임.

        if batch % 100 == 0:
            loss, current = loss.item(), (batch + 1) * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")
# 순전파 과정
x = 2
w1 = 3  # 첫 번째 가중치
w2 = 4  # 두 번째 가중치
y_target = 10

# 계산 그래프
a = w1 * x      # 6
b = w2 * a      # 24
loss = (b - y_target)**2  # (24-10)² = 196
loss = ((w2 * (w1 * x)) - y_target) ** 2
= ((w2 * w1 * x) - y_target) ** 2

# 역전파 과정 (연쇄 법칙 사용)

 

def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    model.eval() # 모델을 평가 모드로 사용 
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= num_batches
    correct /= size
    print(f"Test Error: \\n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \\n")
epochs = 5
for t in range(epochs):
    print(f"Epoch {t+1}\\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model, loss_fn)
print("Done!")