본문 바로가기
Dev/Deep Learning

Pytorch 스터디

by wenect 2025. 1. 3.

PyTorch 학습 목차

  1. PyTorch 기본 개념
    • 1.1 Tensor
    • 1.2 Autograd
    • 1.3 Dataset and DataLoader
  2. 신경망 모델 구축 및 훈련
    • 2.1 신경망 모델 정의 (nn.Module)
    • 2.2 손실 함수 (Loss Function)
    • 2.3 옵티마이저 (Optimizer)
    • 2.4 훈련 루프 (Training Loop)
  3. 이미지 분류 모델 훈련 실습
    • 3.1 CIFAR-10 데이터셋 로드
    • 3.2 CNN 모델 정의
    • 3.3 모델 훈련
    • 3.4 모델 평가

이제 위 목차에 따라 "1. PyTorch 기본 개념"부터 진행하겠습니다.


1. PyTorch 기본 개념

1.1 Tensor

  • 정의: Tensor는 PyTorch의 가장 핵심적인 데이터 구조로, 다차원 배열을 나타냅니다. NumPy의 ndarray와 유사하지만, GPU를 사용하여 연산을 가속할 수 있다는 장점이 있습니다.
  • 생성:실행 결과:
  • tensor([[0.0000e+00, 0.0000e+00, 0.0000e+00],
            [0.0000e+00, 0.0000e+00, 0.0000e+00],
            [0.0000e+00, 0.0000e+00, 0.0000e+00],
            [0.0000e+00, 0.0000e+00, 0.0000e+00],
            [0.0000e+00, 0.0000e+00, 0.0000e+00]])
    tensor([[0.7399, 0.0384, 0.7724],
            [0.2793, 0.3475, 0.9867],
            [0.1476, 0.5694, 0.2669],
            [0.2467, 0.7483, 0.1399],
            [0.3536, 0.7453, 0.3473]])
    tensor([[0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0]])
    tensor([5.5000, 3.0000])
    tensor([[1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.],
            [1., 1., 1.]], dtype=torch.float64)
    tensor([[-0.4941, -0.9492, -1.4278],
            [ 0.8486, -0.5345,  0.8756],
            [-1.2466, -0.4889,  0.0953],
            [ 1.1298, -0.2803,  0.5022],
            [ 0.6089,  0.2535, -0.5662]])
    torch.Size([5, 3])
    
  • Python
     
    import torch
    
    # 빈 5x3 텐서 생성 (초기화되지 않음)
    x = torch.empty(5, 3)
    print(x)
    
    # 무작위로 초기화된 5x3 텐서 생성
    x = torch.rand(5, 3)
    print(x)
    
    # 0으로 채워진 long 타입의 5x3 텐서 생성
    x = torch.zeros(5, 3, dtype=torch.long)
    print(x)
    
    # 데이터로부터 직접 텐서 생성
    x = torch.tensor([5.5, 3])
    print(x)
    
    # 기존 텐서의 속성(크기, 자료형)을 사용하여 새로운 텐서 생성
    x = x.new_ones(5, 3, dtype=torch.double)  # x와 같은 크기, double 타입, 1로 채워진 텐서
    print(x)
    
    x = torch.randn_like(x, dtype=torch.float)  # x와 같은 크기, float 타입, 무작위 값으로 채워진 텐서
    print(x)
    
    # 텐서 크기 확인
    print(x.size())
    
  • 연산:실행 결과:
  • tensor([[ 0.1988, -0.2748, -1.1798],
            [ 1.6027, -0.3240,  1.8295],
            [-0.7497,  0.3719,  0.2704],
            [ 1.3412,  0.6433,  0.7886],
            [ 0.6956,  0.5735, -0.4309]])
    tensor([[ 0.1988, -0.2748, -1.1798],
            [ 1.6027, -0.3240,  1.8295],
            [-0.7497,  0.3719,  0.2704],
            [ 1.3412,  0.6433,  0.7886],
            [ 0.6956,  0.5735, -0.4309]])
    tensor([[ 0.1988, -0.2748, -1.1798],
            [ 1.6027, -0.3240,  1.8295],
            [-0.7497,  0.3719,  0.2704],
            [ 1.3412,  0.6433,  0.7886],
            [ 0.6956,  0.5735, -0.4309]])
    tensor([[ 0.1988, -0.2748, -1.1798],
            [ 1.6027, -0.3240,  1.8295],
            [-0.7497,  0.3719,  0.2704],
            [ 1.3412,  0.6433,  0.7886],
            [ 0.6956,  0.5735, -0.4309]])
    tensor([-0.9492, -0.5345, -0.4889, -0.2803,  0.2535])
    torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])
    
  • Python
     
    # 덧셈
    y = torch.rand(5, 3)
    print(x + y)
    print(torch.add(x, y))
    
    # 덧셈 결과를 저장할 텐서를 미리 지정
    result = torch.empty(5, 3)
    torch.add(x, y, out=result)
    print(result)
    
    # inplace 덧셈 (y에 x를 더한 후 y에 저장)
    y.add_(x)
    print(y)
    
    # 인덱싱 (NumPy와 유사)
    print(x[:, 1])  # 두 번째 열
    
    # 크기 변경
    x = torch.randn(4, 4)
    y = x.view(16)
    z = x.view(-1, 8)  # -1은 다른 차원으로부터 유추
    print(x.size(), y.size(), z.size())
    
  • NumPy 변환:실행 결과:
  • tensor([1., 1., 1., 1., 1.]) [1. 1. 1. 1. 1.]
    [1. 1. 1. 1. 1.] tensor([1., 1., 1., 1., 1.], dtype=torch.float64)
    
  • Python
     
    # Tensor -> NumPy
    a = torch.ones(5)
    b = a.numpy()
    print(a, b)
    
    # NumPy -> Tensor
    import numpy as np
    a = np.ones(5)
    b = torch.from_numpy(a)
    print(a, b)
    
  • CUDA Tensors:실행 결과:
    tensor([[...]], device='cuda:0') # GPU 텐서임을 나타내는 device='cuda:0'
    tensor([[...]], dtype=torch.float64) # CPU로 이동하고 자료형이 double로 변경됨
    
  • (GPU 사용 환경에 따라 결과가 다를 수 있습니다. GPU를 사용할 수 없는 환경이라면 이 코드는 에러를 발생시킵니다.)
  • Python
     
    # GPU 사용 가능 여부 확인
    if torch.cuda.is_available():
        device = torch.device("cuda")          # CUDA device 객체
        y = torch.ones_like(x, device=device)  # GPU에 직접 텐서 생성
        x = x.to(device)                       # 텐서를 GPU로 이동
        z = x + y
        print(z)
        print(z.to("cpu", torch.double))       # 텐서를 CPU로 이동 (자료형 변경)
    

1.2 Autograd

  • 정의: Autograd는 PyTorch의 자동 미분 엔진으로, 텐서 연산에 대한 *그래디언트(gradient)*를 자동으로 계산해줍니다. 이를 통해 복잡한 딥러닝 모델의 학습을 쉽게 구현할 수 있습니다.
  • requires_grad: requires_grad=True로 설정된 텐서에 대한 연산은 계산 그래프에 기록됩니다. 이를 통해 나중에 .backward()를 호출하여 해당 텐서까지의 모든 연산에 대한 그래디언트를 자동으로 계산할 수 있습니다.
  • grad_fn: 연산의 결과로 생성된 텐서는 grad_fn 속성을 갖게 됩니다. grad_fn은 해당 텐서를 생성한 함수를 참조합니다. 이를 통해 연산 그래프를 역추적하며 그래디언트를 계산할 수 있습니다.
  • backward(): backward() 함수는 스칼라 텐서 (즉, 하나의 요소만 가진 텐서)에서만 호출 가능합니다. y.backward()가 호출되면, y의 grad 속성에 y까지의 모든 연산에 대한 그래디언트가 누적됩니다.실행 결과:
  • tensor([[1., 1.],
            [1., 1.]], requires_grad=True)
    tensor([[3., 3.],
            [3., 3.]], grad_fn=<AddBackward0>)
    <AddBackward0 object at 0x...>
    tensor([[27., 27.],
            [27., 27.]], grad_fn=<MulBackward0>) tensor(13.5000, grad_fn=<MeanBackward0>)
    tensor([[4.5000, 4.5000],
            [4.5000, 4.5000]])
    
  • Python
     
    import torch
    
    # requires_grad=True로 텐서 생성
    x = torch.ones(2, 2, requires_grad=True)
    print(x)
    
    # 연산 수행
    y = x + 2
    print(y)
    print(y.grad_fn)  # y는 연산의 결과로 생성되었으므로 grad_fn을 가짐
    
    z = y * y * 3
    out = z.mean()
    print(z, out)
    
    # 그래디언트 계산
    out.backward()
    print(x.grad)  # d(out)/dx
    
  • 더 복잡한 예제:실행 결과:
  • tensor([-749.7367,  915.7348,   83.5167], grad_fn=<MulBackward0>)
    tensor([1.0240e+02, 1.0240e+03, 1.0240e-01])
    
  • Python
     
    x = torch.randn(3, requires_grad=True)
    
    y = x * 2
    while y.data.norm() < 1000:
        y = y * 2
    
    print(y)
    
    v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float) # Jacobian-vector product 계산을 위한 벡터
    y.backward(v)
    
    print(x.grad)
    
  • requires_grad_() 와 detach():
    • requires_grad_()는 기존 텐서의 requires_grad 속성을 in-place로 변경합니다.
    • detach()는 텐서를 계산 그래프에서 분리하여 requires_grad=False인 새로운 텐서를 생성합니다.
    Python
     
    print(x.requires_grad)
    print((x ** 2).requires_grad)
    
    with torch.no_grad():
        print((x ** 2).requires_grad)
    
    # detach() 예제
    print(x.requires_grad)
    y = x.detach()
    print(y.requires_grad)
    print(x.eq(y).all())
    
    실행 결과:
  • True
    True
    False
    True
    False
    tensor(True)
    

1.3 Dataset and DataLoader

  • 정의: PyTorch는 데이터를 효율적으로 로드하고 관리하기 위해 torch.utils.data.Dataset과 torch.utils.data.DataLoader 클래스를 제공합니다.
  • Dataset:
    • Dataset은 추상 클래스로, 사용자 정의 데이터셋을 만들 때 상속해야 합니다.
    • __len__과 __getitem__ 메소드를 구현해야 합니다.
      • __len__: 데이터셋의 총 샘플 개수를 반환합니다.
      • __getitem__: 주어진 인덱스에 해당하는 샘플을 반환합니다.
  • DataLoader:
    • DataLoader는 Dataset을 감싸서 미니 배치 생성, 데이터 셔플링, 멀티프로세싱을 통한 데이터 로딩 가속화 등의 기능을 제공합니다.
    • 주요 인자:
      • dataset: 사용할 Dataset 객체
      • batch_size: 미니 배치 크기
      • shuffle: 에폭마다 데이터를 섞을지 여부
      • num_workers: 데이터를 로드하는 데 사용할 서브 프로세스 개수
    Python
     
    import torch
    from torch.utils.data import Dataset, DataLoader
    
    # 예제 Dataset 클래스
    class MyDataset(Dataset):
        def __init__(self, data, targets, transform=None):
            self.data = data
            self.targets = targets
            self.transform = transform
    
        def __getitem__(self, index):
            x = self.data[index]
            y = self.targets[index]
    
            if self.transform:
                x = self.transform(x)
    
            return x, y
    
        def __len__(self):
            return len(self.data)
    
    # 데이터와 타겟 생성 (예시)
    data = torch.randn(100, 3, 32, 32)
    targets = torch.randint(0, 10, (100,))
    
    # Dataset 객체 생성
    dataset = MyDataset(data, targets)
    
    # DataLoader 객체 생성
    dataloader = DataLoader(dataset, batch_size=4, shuffle=True, num_workers=2)
    
    # DataLoader를 이용한 데이터 순회
    for batch_idx, (data, targets) in enumerate(dataloader):
        print(f"Batch {batch_idx}: Data shape={data.size()}, Targets shape={targets.size()}")
    
    실행 결과: (실행할 때마다 결과가 달라질 수 있습니다.)
  • Batch 0: Data shape=torch.Size([4, 3, 32, 32]), Targets shape=torch.Size([4])
    Batch 1: Data shape=torch.Size([4, 3, 32, 32]), Targets shape=torch.Size([4])
    ...
    Batch 24: Data shape=torch.Size([4, 3, 32, 32]), Targets shape=torch.Size([4])
    

2. 신경망 모델 구축 및 훈련

이 섹션에서는 PyTorch를 사용하여 신경망 모델을 정의하고, 손실 함수와 옵티마이저를 설정하여 모델을 훈련하는 과정을 살펴보겠습니다.

2.1 신경망 모델 정의 (nn.Module)

  • nn.Module: PyTorch에서 신경망 모델은 nn.Module을 상속하여 정의합니다.
  • __init__: 신경망의 레이어들을 정의하고 초기화합니다.
  • forward: 입력 데이터가 신경망을 통과하는 순전파(forward pass) 과정을 정의합니다.실행 결과:
  • Net(
      (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
      (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
      (fc1): Linear(in_features=400, out_features=120, bias=True)
      (fc2): Linear(in_features=120, out_features=84, bias=True)
      (fc3): Linear(in_features=84, out_features=10, bias=True)
    )
    
  • Python
     
    import torch.nn as nn
    import torch.nn.functional as F
    
    class Net(nn.Module):
        def __init__(self):
            super(Net, self).__init__()
            # 1개의 입력 이미지 채널, 6개의 출력 채널, 5x5 합성곱 커널
            self.conv1 = nn.Conv2d(1, 6, 5)
            self.conv2 = nn.Conv2d(6, 16, 5)
            # 아핀(affine) 연산: y = Wx + b
            self.fc1 = nn.Linear(16 * 5 * 5, 120)  # 5*5는 이미지 차원에 해당
            self.fc2 = nn.Linear(120, 84)
            self.fc3 = nn.Linear(84, 10)
    
        def forward(self, x):
            # (2, 2) 크기 윈도우에 대해 맥스 풀링(max pooling)
            x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
            # 크기가 제곱수라면 하나의 숫자만을 특정
            x = F.max_pool2d(F.relu(self.conv2(x)), 2)
            x = x.view(-1, self.num_flat_features(x))
            x = F.relu(self.fc1(x))
            x = F.relu(self.fc2(x))
            x = self.fc3(x)
            return x
    
        def num_flat_features(self, x):
            size = x.size()[1:]  # 배치 차원을 제외한 모든 차원
            num_features = 1
            for s in size:
                num_features *= s
            return num_features
    
    net = Net()
    print(net)
    

2 1 .2 손실 함수 (Loss Function)  

  • 정의: 손실 함수는 모델의 예측값과 실제값 사이의 차이를 측정하는 함수입니다. 모델 훈련의 목표는 손실 함수의 값을 최소화하는 것입니다.
  • 종류: PyTorch는 nn 패키지에서 다양한 손실 함수를 제공합니다.
    • nn.MSELoss: 평균 제곱 오차 (Mean Squared Error)
    • nn.CrossEntropyLoss: 크로스 엔트로피 손실
    • nn.L1Loss: L1 손실
    • ...
  • 예제:실행 결과:
  • tensor(0.5559, grad_fn=<MseLossBackward>)
    
  • Python
     
    output = net(torch.randn(1, 1, 32, 32)) # 임의의 입력값
    target = torch.randn(10)  # 임의의 타겟
    target = target.view(1, -1)  # 출력과 같은 shape로 만듦
    criterion = nn.MSELoss()
    
    loss = criterion(output, target)
    print(loss)
    

2.3 옵티마이저 (Optimizer)

  • 정의: 옵티마이저는 손실 함수의 값을 최소화하기 위해 모델의 가중치를 업데이트하는 알고리즘을 구현합니다.
  • 종류: PyTorch는 torch.optim 패키지에서 다양한 옵티마이저를 제공합니다.
    • optim.SGD: 확률적 경사 하강법 (Stochastic Gradient Descent)
    • optim.Adam: Adam 옵티마이저
    • optim.RMSprop: RMSprop 옵티마이저
    • ...
  • 예제:
  • Python
     
    import torch.optim as optim
    
    # 옵티마이저 생성 (SGD 사용)
    optimizer = optim.SGD(net.parameters(), lr=0.01)
    

2.4 훈련 루프 (Training Loop)

  • 훈련 루프: 모델 훈련은 일반적으로 다음과 같은 단계를 반복적으로 수행합니다.
    1. 입력 데이터를 모델에 전달하여 예측값을 계산합니다. (순전파)
    2. 손실 함수를 사용하여 예측값과 실제값 사이의 손실을 계산합니다.
    3. 역전파를 수행하여 손실에 대한 각 가중치의 그래디언트를 계산합니다.
    4. 옵티마이저를 사용하여 가중치를 업데이트합니다.
  • 예제:실행 결과: (이 코드는 가중치를 업데이트하지만, 그 결과를 직접 출력하지는 않습니다.)
  • Python
     
    # 훈련 루프
    optimizer.zero_grad()   # 모든 변화도(gradient) 버퍼를 0으로 만듦
    output = net(torch.randn(1, 1, 32, 32))
    loss = criterion(output, target)
    loss.backward()
    optimizer.step()    # 업데이트 진행
    

댓글