본문 바로가기

파이토치

torch.nn (1)

torch.nn — PyTorch 2.5 documentation

 

torch.nn — PyTorch 2.5 documentation

Shortcuts

pytorch.org

 

1. Linear Layers

https://pytorch.org/docs/stable/generated/torch.nn.Linear.html

 

Linear — PyTorch 2.5 documentation

Shortcuts

pytorch.org

■ torch.nn.Linear는 어파인(Affine) 계층으로 \( y = w \cdot x + b \)를 계산한다.   x @ weights + bias의 계산을 nn.Linear가 수행하는 것이다.

m = nn.Linear(20, 30)
m
```#결과#```
Linear(in_features=20, out_features=30, bias=True)
````````````

inputs = torch.randn(128, 20)
outputs = m(inputs)

- bias의 디폴트 값은 True로 설정되어 있으며, False로 지정하면 이 계층은 편향에 대해 학습하지 않는다.

- 첫 번째 인수 in_features와 두 번째 인수 out_features는 입력 데이터의 크기와 출력 데이터의 크기를 받는다.

■ 따라서 nn.Linear를 통해 완전연결 층을 쌓으려면 입력과 출력의 크기를 맞춰줘야 한다.

■ 예를 들어 입력 데이터의 차원이 784 크기의 1차원 벡터이고, 두 번째 층의 입력의 크기는 512, 세 번째 층의 입력 크기는 256이고 세 번째 층의 출력 크기가 10이라면, 다음과 같이 세 개의 층을 쌓을 수 있다.

nn.Linear(28 * 28, 512)
nn.Linear(512, 256)
nn.Linear(256, 10)

 

2. Sequential Container

https://pytorch.org/docs/stable/generated/torch.nn.Sequential.html#torch.nn.Sequential

 

Sequential — PyTorch 2.5 documentation

Shortcuts

pytorch.org

■ 케라스에 Sequential API가 있듯이, 파이토치 nn에도 동일한 기능을 수행하는 Sequential Container가 있다.

■ 예를 들어 첫 번째 층의 출력 값에 ReLU 함수를 적용한 값을 두 번째 층의 입력으로 사용하고, 두 번째 층의 출력 값에도 ReLU 함수를 적용한 값을 세 번째 층의 입력으로 사용하는 모델을 다음과 같이 정의할 수 있다.

- 활성화 함수도 torch.nn을 이용해 지정할 수 있다.

model = nn.Sequential(
    nn.Linear(28 * 28, 512),
    nn.ReLU(),
    nn.Linear(512, 256),
    nn.ReLU(),
    nn.Linear(256, 10)
)

model
```#결과#```
Sequential(
  (0): Linear(in_features=784, out_features=512, bias=True)
  (1): ReLU()
  (2): Linear(in_features=512, out_features=256, bias=True)
  (3): ReLU()
  (4): Linear(in_features=256, out_features=10, bias=True)
)
````````````

■ 정의한 모델을 출력하면 각 계층에 순번이 명시되어 있는 것을 확인할 수 있다. 이 순번을 따라 각 계층이 실행된다.

■ 순서가 있는 딕셔너리를 이용해서 Sequential Container로 모델을 정의할 수도 있다.

from collections import OrderedDict

model2 = nn.Sequential(OrderedDict([
          ('affine1', nn.Linear(28 * 28, 512)),
          ('relu1', nn.ReLU()),
          ('affine2', nn.Linear(512,256)),
          ('relu2', nn.ReLU()),
          ('affine3', nn.Linear(256, 10))
]))

model2
```#결과#```
Sequential(
  (affine1): Linear(in_features=784, out_features=512, bias=True)
  (relu1): ReLU()
  (affine2): Linear(in_features=512, out_features=256, bias=True)
  (relu2): ReLU()
  (affine3): Linear(in_features=256, out_features=10, bias=True)
)
````````````

- OrderedDict을 사용했기 때문에 affine1 \( \rightarrow \) relu1 \( \rightarrow \) affine2 \( \rightarrow \) relu2 \( \rightarrow \) affine3 계층 순대로 실행된다.

■ 텐서플로 케라스에서는 .add( ) 메서드를 이용해 순서대로 계층을 쌓는 것이 가능하였다. 파이토치도 동일한 기능을 add_module( ) 메서드를 통해 수행할 수 있다.

model3 = nn.Sequential()
model3.add_module('affine1', nn.Linear(28*28, 512))
model3.add_module('relu1', nn.ReLU())
model3.add_module('affine2', nn.Linear(512, 256))
model3.add_module('relu2', nn.ReLU())
model3.add_module('affine3', nn.Linear(256, 10))

model3
```#결과#```
Sequential(
  (affine1): Linear(in_features=784, out_features=512, bias=True)
  (relu1): ReLU()
  (affine2): Linear(in_features=512, out_features=256, bias=True)
  (relu2): ReLU()
  (affine3): Linear(in_features=256, out_features=10, bias=True)
)
````````````

 

3. Module

https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module

 

Module — PyTorch 2.5 documentation

Shortcuts

pytorch.org

■ 케라스의 모델 서브클래싱처럼 파이토치도 파이토치 모듈 내에 딥러닝 모델 관련 기본 함수를 포함하고 있는 nn.Module 클래스를 상속받아 '나만의 모델'을 정의할 수 있다.

■ 케라스의 모델 서브클래싱에서 학습을 수행하는 메서드를 구현할 때, 이 과정을 함수형 API로 구현할 수 있었다. 파이토치도 동일하게 구현할 수 있다.

import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(28 * 28, 512)
        self.fc2 = nn.Linear(512, 256)
        self.fc3 = nn.Linear(256, 10)
        
    def forward(self, x):
        x = x.view(-1, 28*28)
        x = self.fc1(x)
        x = F.sigmoid(x)
        x = self.fc2(x)
        x = F.sigmoid(x)
        x = self.fc3(x)
        x = F.log_softmax(x, dim = 1)
        return x

torch.nn은 클래스로 정의되어 있고 torch.nn.functional은 함수이다. 즉, torch.nn은 인스턴스화가 필요하고 torch.nn.functional로 구현한 함수의 경우 인스턴스화 필요 없이 사용 가능하다.

https://pytorch.org/docs/stable/nn.functional.html#non-linear-activation-functions

 

torch.nn.functional — PyTorch 2.5 documentation

Shortcuts

pytorch.org

- forward 함수는 입력 x를 받으면 1차원 데이터(벡터)로 평탄화시킨다.

- 그다음, 평탄화된 입력값이 첫 번째 affine1 \( \rightarrow \) sigmoid \( \rightarrow \) affine2 \( \rightarrow \) sigmoid \( \rightarrow \) affine3 \( \rightarrow \) log softmax 순서대로 통과하여 forward의 출력값이 반환된다.

cf) 일반적인 softmax 함수가 아닌 log softmax 함수를 사용하는 이유는 MLP 모델이 역전파를 진행할 때 Loss 값에 대한 그래디언트 값을 좀 더 원활하게 게산할 수 있기 때문이다.

model4 = Net().to(DEVICE)
model4
```#결과#```
Net(
  (fc1): Linear(in_features=784, out_features=512, bias=True)
  (fc2): Linear(in_features=512, out_features=256, bias=True)
  (fc3): Linear(in_features=256, out_features=10, bias=True)
)
````````````

- nn.Moudle을 상속받았기 때문에 위와 같이 .to( ) 메서드로 장비(cpu/cuda)를 지정할 수 있다.

 

4. 옵티마이저, 손실 함수 정의하기

텐서플로 케라스에서는 모델의 컴파일 단계에서 옵티마이저와 손실 함수, 평가지표를 한 번에 묶어 정의하였다.

## 텐서플로 
model.compile(
    optimizer = tf.keras.optimizers.Adam(learning_rate=0.01, beta_1 = 0.9, beta_2 = 0.99),
    loss = tf.keras.losses.CategoricalCrossentropy(),
    metrics = [tf.keras.metrics.CategoricalAccuracy()]
)

그러나 파이토치는 다음과 같이 한줄 한줄 옵티마이저와 손실 함수를 정의한다. 텐서플로는 옵티마이저와 손실 함수가 타이트하게 결합되어 있고, 파이토치는 느슨하게 결합되어 있는 것이 두 딥러닝 프레임워크의 차이점 중 하나이다.

## 파이토치
model = Net().to(DEVICE)
optimizer = torch.optim.SGD(model.parameters(), lr = 0.01, momentum = 0.5)
criterion = nn.CrossEntropyLoss().to(DEVICE)

■ 모델 학습도 텐서플로는 model 내부에서 fit( ) 메서드를 통해 파이토치의 optimizer.zero_grad( ), loss.backward( ), optimizer.step( ) 과정을 수행한다.

## 텐서플로
model.fit(train_set, train_labels, batch_size = 32, validation_data=(valid_set, valid_labels),
           epochs=100)
## 파이토치
def train(model, train_loader, optimizer, log_interval):
    model.train()
    for batch_idx, (image, label) in enumerate(train_loader):
        image = image.to(DEVICE)
        label = label.to(DEVICE)
        
        optimizer.zero_grad()
        output = model(image)
        loss = criterion(output, label) ## nn.CrossEntropyLoss(output, label)
        loss.backward()
        optimizer.step()
        
        if batch_idx % log_interval == 0:
            print("Train Epoch: {} [{}/{} ({:.0f}%)]\tTrain Loss: {:.6f}".format(
                epoch, batch_idx * len(image), 
                len(train_loader.dataset), 100. * batch_idx / len(train_loader), 
                loss.item()))
            
def evaluate(model, test_loader):
    model.eval()
    test_loss = 0
    correct = 0

    with torch.no_grad():
        for image, label in test_loader:
            image = image.to(DEVICE)
            label = label.to(DEVICE)
            
            output = model(image)
            
            test_loss += criterion(output, label).item()
            prediction = output.max(1, keepdim = True)[1]
            correct += prediction.eq(label.view_as(prediction)).sum().item()
    
    test_loss /= (len(test_loader.dataset) / BATCH_SIZE)
    test_accuracy = 100. * correct / len(test_loader.dataset)
    return test_loss, test_accuracy

- model.train( )으로 모델을 학습 상태로 지정한다.

- train_loader나 test_loader는 이미지 데이터와 레이블 데이터가 미니배치 단위로 묶여 저장돼 있다. 또한 제네레이터이기 때문에 반복문을 통해 순서대로 미니배치를 하나씩 꺼내서 모델 학습 또는 평가를 진행한다.

- 미니배치 내에 있는 이미지 데이터와, 이 이미지 데이터와 매칭된 레이블 데이터를 지정한 장비(이 예에서는 cuda)에 할당해 학습시킨다.

- train 과정에서는

   (1) 옵티마이저 그래디언트 값 초기화 optimizer.zero_grad( ),

   (2) 모델 계산(순전파) output = model(image),

   (3) 손실 계산 loss = criterion(output, label),

   (4) 역전파 loss.backward( ),

   (5) 최적화 optimizer.step( ) 순서대로 모델을 학습한다. 

cf) optimizer.zero_grad( )의 배치는 optimizer.step( ) 바로 다음에 배치해도 된다.

- 기존에 정의한 장비에 이미지 데이터와 레이블 데이터를 할당할 경우, 과거에 이용한 미니배치 내에 있는 이미지 데이터와 레이블 데이터를 바탕으로 계산된 Loss의 그래디언트 값이 옵티마이저에 할당돼 있으므로, 옵티마이저의 그래디언트를 초기화해서 사용해야 한다.

- 그다음, 장비에 할당한 이미지 데이터를 정의한 모델의 입력으로 이용해 output을 계산한다.

- 손실 함수를 이용해 계산된 output과 레이블 데이터를 비교해 Loss 값을 계산한다.

- Loss 값을 계산했으니 이제, Loss 값을 계산한 결과를 바탕으로 역전파를 통해 계산된 그래디언트 값을 각 파라미터에 할당한다.

- 마지막으로 각 파라미터에 할당된 그래디언트 값을 이용해 파라미터 값을 업데이트한다. 여기까지가 model.train( )부터 optimizer.step() 까지의 과정이다.  

- 모델 학습이 완료된 상태에서 모델의 성능을 평가하기 위해 evaluate 함수를 정의한다.

- model.eval( )은 모델을 학습 상태가 아닌 평가 상태로 지정한다.

- 평가하거나 예측하는 과정에서는 그래디언트 값을 업데이트할 이유가 없다. 따라서 그래디언트 값이 업데이트되지 않도록 설정해야 한다.

 

■ 예를 들어 파이토치를 이용해 FashionMNIST 흑백 이미지 데이터에 대해 다중 분류 문제를 풀어보자. 

from torchvision import transforms, datasets

train_dataset = datasets.FashionMNIST(root = "../data/FashionMNIST",
                               train = True,
                               download = True,
                               transform = transforms.ToTensor())

test_dataset = datasets.FashionMNIST(root = "../data/FashionMNIST",
                              train = False,
                              download = True,
                              transform = transforms.ToTensor())

- torchvision의 datasets 함수를 이용해 파이토치에서 제공하는 데이터를 불러올 수 있다.

- train 속성은 불러올 데이터가 학습용 데이터인지, 학습된 이후 모델 성능을 검증하기 위한 검증용 데이터인지 불리언 값으로 설정할 수 있다.

- download는 데이터를 인터넷상에서 다운로드해 이용할 것인지를 설정한다.

- transform 속성에 ToTensor( ) 메서드를 지정해 데이터를 텐서 형태로 변경하고 [0, 1] 범위로 값을 정규화한다.

BATCH_SIZE = 32
train_loader = torch.utils.data.DataLoader(dataset = train_dataset,
                                           batch_size = BATCH_SIZE,
                                           shuffle = True)

test_loader = torch.utils.data.DataLoader(dataset = test_dataset,
                                          batch_size = BATCH_SIZE,
                                          shuffle = False)

- 하나의 미니배치에 들어갈 데이터의 개수, 배치 사이즈를 지정한다.

- shuffle = True로 설정해 데이터의 순서를 섞을 수 있다.

- 학습 데이터의 순서를 섞는 이유는, 모델이 학습을 진행할 때 이미지 데이터의 특징을 보고 학습하는 것이 아니라 레이블의 순서를 암기해 학습할 수 있다. 이를 방지하고자 셔플을 사용한다.

 class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(28 * 28, 512)
        self.fc2 = nn.Linear(512, 256)
        self.fc3 = nn.Linear(256, 10)
        
    def forward(self, x):
        x = x.view(-1, 28*28)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.fc2(x)
        x = F.relu(x)
        x = self.fc3(x)
        x = F.log_softmax(x, dim = 1)
        return x
        
model = Net().to(DEVICE)
optimizer = torch.optim.SGD(model.parameters(), lr = 0.01, momentum = 0.5)
criterion = nn.CrossEntropyLoss().to(DEVICE)

- 위에서 정의한 Net 클래스로 모델 구조를 정의하고 모델을 생성한다.

- 그리고 가중치 업데이트에 사용할 옵티마이저와 손실 함수를 정의한다.

image, label = next(iter(train_loader))
image.shape, label.shape
```#결과#```
(torch.Size([32, 1, 28, 28]), torch.Size([32]))
````````````

for (x_train, y_train) in train_loader:
    print(f'x_train size: {x_train.size()}, type: {x_train.type()}')
    print(f'y_train size: {y_train.size()}, type: {y_train.type()}')
    break
```#결과#```
x_train size: torch.Size([32, 1, 28, 28]), type: torch.FloatTensor
y_train size: torch.Size([32]), type: torch.LongTensor
````````````

- torch.utils.data.DataLoader로 불러온 것은 제네레이터이므로 next, iter을 사용하거나 for 문을 통해 미니배치를 하나씩 불러올 수 있다.

- 텐서플로는 (N, H, W, C), 파이토치는 (N, C, H, W) 형상으로 이미지 데이터를 사용한다. 

- 불러온 미니배치의 형상을 보면 x_train은 가로 28개, 세로 28개의 픽셀로 구성된 32개의 이미지 데이터 1개가 미니배치를 구성하고 있다. 이때, 채널 수가 1이므로 그레이 스케일(gray scale)로 이뤄진, 다시 말해 흑백으로 이뤄진 이미지 데이터라는 것을 확인할 수 있다.

- y_train의 형상에서는 32개 이미지의 데이터 각각에 레이블 값이 1개씩 존재하기 때문에 32개의 값을 갖고 있다는 것을 확인할 수 있다.

 

 

torch.nn (2)

 

'파이토치' 카테고리의 다른 글

파이토치 합성곱 신경망(CNN) (2)  (2) 2024.12.06
파이토치 합성곱 신경망(CNN) (1)  (2) 2024.12.05
torch.nn (2)  (0) 2024.12.04