Post
Ch2 실습환경 설정과 파이토치 기초 | Gihun Son

Ch2 실습환경 설정과 파이토치 기초

2.1 파이토치 개요

Pytorch를 사용하는 이유 • 넘파이를 대체하면서 GPU를 이용한 연산이 필요한 경우

• 최대한의 유연성과 속도를 제공하는 딥러닝 연구 플랫폼이 필요한 경우

하지만 무엇보다 주목받는 이유 중 하나는 간결하고 빠른 구현성에 있습니다.

2.1.1 파이토치 특징 및 장점

파이토치 특징: GPU에서 텐서 조작 및 동적 신경망 구축이 가능한 프레임워크

• GPU(Graphics Processing Unit): 연산 속도를 빠르게 하는 역할

  • 딥러닝에서는 기울기를 계산할 때 미분을 쓰는데, GPU를 사용하면 빠른 계산이 가능

  • 내부적으로 CUDA, cuDNN이라는 API를 통해 GPU를 연산에 사용 가능

  • 병렬 연산에서 GPU의 속도는 CPU의 속도보다 훨씬 빠르므로 딥러닝 학습에서 GPU 사용은 필수

• 텐서(Tensor)

  • 텐서는 파이토치의 데이터 형태

  • 텐서는 단일 데이터 형식으로 된 자료들의 다차원 행렬

  • 텐서는 간단한 명령어(변수 뒤에 .cuda()를 추가)를 사용해서 GPU로 연산을 수행 가능

• 동적 신경망: 훈련을 반복할 때마다 네트워크 변경이 가능한 신경망을 의미. 예를 들어 학습 중에 은닉층을 추가하거나 제거하는 등 모델의 네트워크 조작이 가능

  • 연산 그래프를 정의하는 것과 동시에 값도 초기화되는 ‘Define by Run’ 방식을 사용. 따라서 연산 그래프와 연산을 분리해서 생각할 필요가 없기 때문에 코드를 이해하기 쉽다. image.png

+)

image.png

• 1차원 축(행) = axis 0 = 벡터

• 2차원 축(열) = axis 1 = 행렬

• 3차원 축(채널) = axis 2 = 텐서

2.1.2 파이토치의 아키텍처

image.png

Pytorch API

torch: GPU를 지원하는 텐서 패키지

  • 다차원 텐서를 기반으로 다양한 수학적 연산이 가능. 특히 CPU뿐만 아니라 GPU에서 연산이 가능하므로 빠른 속도로 많은 양의 계산.

torch.autograd: 자동 미분 패키지

  • Autograd는 텐서플로(TensorFlow), 카페(Caffe), CNTK 같은 다른 딥러닝 프레임워크와 가장 차별되는 패키지. 일반적으로 신경망에 사소한 변경(ex. 은닉층 노드 수 변경)이 있다면 신경망 구축을 처음부터 다시 시작해야 한다. 하지만 파이토치는 ‘자동 미분(auto-differentiation)’이라고 하는 기술을 채택하여 미분 계산을 효율적으로 처리한다. 즉, ‘연산 그래프’가 즉시 계산(실시간으로 네트워크 수정이 반영된 계산)되기 때문에 사용자는 다양한 신경망을 적용 가능

torch.nn: 신경망 구축 및 훈련 패키지

  • torch.nn을 사용할 경우 신경망을 쉽게 구축하고 사용할 수 있다. 특히 합성곱 신경망, 순환 신경망, 정규화 등이 포함되어 손쉽게 신경망을 구축하고 학습 가능

torch.multiprocessing: 파이썬 멀티프로세싱 패키지

  • 파이토치에서 사용하는 프로세스 전반에 걸쳐 텐서의 메모리 공유가 가능. 따라서 서로 다른 프로세스에서 동일한 데이터(텐서)에 대한 접근 및 사용이 가능

torch.utils: DataLoader 및 기타 유틸리티를 제공하는 패키지

  • 모델에 데이터를 제공하기 위한 torch.utils.data.DataLoader 모듈을 주로 사용 또한, 병목 현상을 디버깅하기 위한 torch.utils.bottleneck, 모델 또는 모델의 일부를 검사하기 위한 torch.utils.checkpoint 등의 모듈도 있음.

Pytorch 연산처리

• 오프셋(offset): 텐서에서 첫 번째 요소가 스토리지에 저장된 인덱스

• 스트라이드(stride): 각 차원에 따라 다음 요소를 얻기 위해 건너뛰기(skip)가 필요한 스토리지의 요소 개수이다. 즉, 스트라이드는 메모리에서의 텐서 레이아웃을 표현하는 것으로 이해하면 된다. 요소가 연속적으로 저장되기 때문에 행 중심으로 스트라이드는 항상 1이다.

image.png

파이토치 기초문법

텐서(Tensor) 다루기

[Tensor 생성 및 변환]

Tensor는 pytorch의 가장 기본이 되는 구조이다. Numpy의 ndarray와 유사하고, GPU에서도 연산 가능.

1
2
3
4
5
6
import torch
print(torch.tensor([[1,2],[3,4]])) #2차원 텐서 생성
print('------------------------')
#print(torch.tensor([[1,2],[3,4]], device="cuda:0")) #GPU에 텐서 생성(GPU가 없다면 오류가 발생하므로 주석 처리)
print('------------------------')
print(torch.tensor([[1,2],[3,4]], dtype=torch.float64))#dtype을 사용하여 텐서 생성

위 Tensor를 ndarray로 변환하는 방법은 아래와 같다.

1
2
3
4
5
temp=torch.tensor([[1,2],[3,4]])
print(temp.numpy()) #tensor를 ndarray로 변환
#temp = torch.tensor([[1,2],[3,4]], device="cuda:0") #GPU가 없다면 오류가 발생하므로 주석 처리
temp = torch.tensor([[1,2],[3,4]], device="cpu:0")
print(temp.to("cpu").numpy()) #GPU상의 tensor를 cpu의 tensor로 변환 후 ndarray로 변환

[Tensor index 조작]

Tensor의 자료형

  • torch.FloatTensor: 32비트의 부동 소수점

  • torch.DoubleTensor: 64비트의 부동 소수점

  • torch.LongTensor: 64비트의 부호가 있는 정수

1
2
3
4
temp = torch.FloatTensor([1, 2, 3, 4, 5, 6, 7]) #pytorch로 1차원 vector생성
print(temp[0], temp[1], temp[-1]) #index로 접근 가능
print('------------------------')
print(temp[2:5], temp[4:-1]) #slicing가능

[Tensor 연산 및 Dimension 조작]

Tensor간의 type이 다르면 연산 불가능

Ex) FloatTensor, DoubleTensor간의 연산 불가능

1
2
3
v=torch.tensor([1,2,3]) #길이가 3인 vector
w=torch.tensor([3,4,6])
print(w-v) #Vector간의 뺄셈 연산 가능

Dimenstion조작은 주로 view를 사용한다. view는 numpy의 reshape과 유사하다.

+) stack, cat(Tensor결합), t, transpose(Dimension 교환)

1
2
3
4
5
6
7
8
9
10
11
12
temp=torch.tensor([[1,2],[3,4]])

print(temp.shape)
print('-------------------------')
print(temp.view(4,1)) # 2x2 matrix를 4x1로 변형
print('-------------------------')
print(temp.view(-1)) # 2x2 matrix를 1d Vector로 변형
print('-------------------------')
print(temp.view(1,-1)) #-1은 (1, ?)와 같은 의미로 다른 차원으로부터 해당 값을 유추, 행이 1이므로 (1,4)
print('-------------------------')
print(temp.view(-1,1))
print('-------------------------')

데이터 준비

[단순하게 파일을 불러오는 방법]

pandas를 통해 JSON, PDF, CSV등의 파일을 불러올 수 있다.

1
2
3
import pandas as pd #------ pandas 라이브러리 호출
import torch #------ torch 라이브러리 호출
data = pd.read_csv('../class2.csv') #------ csv 파일을 불러온다.
1
2
x = torch.from_numpy(data['x'].values).unsqueeze(dim=1).float() #------ CSV 파일의 x 칼럼의 값을 넘파이 배열로 받아 Tensor(dtype)으로 바꾸어 준다.
y = torch.from_numpy(data['y'].values).unsqueeze(dim=1).float() #------ CSV 파일의 y 칼럼의 값을 넘파이 배열로 받아 Tensor(dtype)으로 바꾸어 준다.

[Custom Dataset 만들어서 사용하는 방법]

1
2
3
4
5
6
class CustomDataset(torch.utils.data.Dataset):
    def __init__(self): #------ 필요한 변수를 선언하고, 데이터셋의 전처리를 해 주는 함수

    def __len__(self):# ------ 데이터셋의 길이. 즉, 총 샘플의 수를 가져오는 함수

    def __getitem__(self, index): #------ 데이터셋에서 특정 데이터를 가져오는 함수(index번째 데이터를 반환하는 함수이며, 이때 반환되는 값은 텐서의 형태를 취해야 합니다)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pandas as pd
import torch
from torch.utils.data import Dataset
from torch.utils.data import DataLoader

class CustomDataset(Dataset):
    def __init__(self, csv_file): #------ csv_file 파라미터를 통해 데이터셋을 불러옵니다.
        self.label = pd.read_csv(csv_file)

    def __len__(self): #------ 전체 데이터셋의 크기(size)를 반환합니다.
        return len(self.label)

    def __getitem__(self, idx): #------ 전체 x와 y 데이터 중에 해당 idx번째의 데이터를 가져옵니다.
        sample = torch.tensor(self.label.iloc[idx,0:3]).int()
        label = torch.tensor(self.label.iloc[idx,3]).int()
        return sample, label

tensor_dataset = CustomDataset('../covtype.csv') #------ 데이터셋으로 covtype.csv를 사용합니다.
dataset = DataLoader(tensor_dataset, batch_size=4, shuffle=True) #------  데이터셋을 torch.utils.data.DataLoader에 파라미터로 전달합니다.

torch.utils.data.DataLoader

image.png

데이터로더(DataLoader) 객체는 학습에 사용될 데이터 전체를 보관했다가 모델 학습을 할 때 배치 크기만큼 데이터를 꺼내서 사용한다. 이때 주의할 것은 데이터를 미리 잘라 놓는 것이 아니라 내부적으로 반복자(iterator)에 포함된 인덱스(index)를 이용하여 배치 크기만큼 데이터를 반환한다는 것

1
2
3
4
5
6
#따라서 데이터로더는 다음과 같이 for 문을 이용하여 구문을 반복 실행하는 것과 같다.

for i, data in enumerate(dataset,0):
    print(i, end='')
    batch=data[0]
    print(batch.size())

+) 참고

image.png

image.png

image.png

2.2.3 모델 정의

파이토치에서 모델을 정의하기 위해 module을 상속한 클래스를 사용한다.

• 계층(layer): 모듈 또는 모듈을 구성하는 한 개의 계층으로 합성곱층(convolutional layer), 선형 계층(linear layer) 등이 있습니다.

• 모듈(module): 한 개 이상의 계층이 모여서 구성된 것으로, 모듈이 모여 새로운 모듈을 만들 수도 있습니다.

• 모델(model): 최종적으로 원하는 네트워크로, 한 개의 모듈이 모델이 될 수도 있습니다.

[단순 신경망을 정의하는 방법]

nn.Module을 상속받지 않는 단순한 모델을 만들 때, 사용

1
model=nn.Linear(in_features=1,out_features=1,bias=True)

[nn.Module()을 상속하여 정의하는 방법]

nn.Module을 상속받는 모델은 __init()__forward()함수를 기본적으로 포함한다.

  • __init()__: 모델에서 사용될 모듈(nn.Linear, nn.Conv2d), 활성화 함수(activation funtion) 등을 정의
  • forward(): 모델에서 실행되어야 하는 연산을 정의

Ex)

1
2
3
4
5
6
7
8
9
10
class MLP(Module):
    def __init__(self, inputs):
        super(MLP, self).__init__() #super().__init()__과 동일 (부모 클래스의 __init()__을 실행->부모클래스의 attribute를 사용가능)
        self.layer = Linear(inputs, 1) #------ Layer 정의
        self.activation = Sigmoid() #------ Activation funtion 정의

    def forward(self, X):
        X = self.layer(X)
        X = self.activation(X)
        return X

[Sequential 신경망 정의]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import torch.nn as nn
class MLP(nn.Module):
    def __init__(self):
        super(MLP, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=64, kernel_size=5),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2))

        self.layer2 = nn.Sequential(
            nn.Conv2d(in_channels=64, out_channels=30, kernel_size=5),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2))

        self.layer3 = nn.Sequential(
            nn.Linear(in_features=30*5*5, out_features=10, bias=True),
            nn.ReLU(inplace=True))


        def forward(self, x):
            x = self.layer1(x)
            x = self.layer2(x)
            x = x.view(x.shape[0], -1)
            x = self.layer3(x)
            return x
model = MLP() ##------ 모델에 대한 객체 생성

print("Printing children\n------------------------------")
print(list(model.children()))
print("\n\nPrinting Modules\n------------------------------")
print(list(model.modules()))

+) model.modules( ) & model.children( )

image.png

model.modules()는 모델의 네트워크에 대한 모든 노드를 반환하며, model.children()은 같은 수준(level)의 하위 노드를 반환한다.

[함수로 신경망 정의]

Sequential을 이용하는 것과 동일하지만, 함수로 선언할 경우 변수에 저장해 놓은 layer들을 재사용할 수 있지만, 모델이 복잡해지는 단점이 있다. (참고로 복잡한 모델의 경우에는 함수를 이용하는 것보다는 nn.Module()을 상속받아 사용하는 것이 편리)

1
2
3
4
5
6
def MLP(in_features=1, hidden_features=20, out_features=1):
    hidden = nn.Linear(in_features=in_features, out_features=hidden_features, bias=True)
    activation = nn.ReLU()
    output = nn.Linear(in_features=hidden_features, out_features=out_features, bias=True)
    net = nn.Sequential(hidden, activation, output)
    return net

2.2.4 모델 파라미터 정의

손실 함수(loss function): 학습하는 동안 출력과 실제 값(정답) 사이의 오차를 측정. 즉, wx + b를 계산한 값과 실제 값인 y의 오차를 구해서 모델의 정확성을 측정한다. 손실 함수로 많이 사용되는 것은 다음과 같다.

  • BCELoss: 이진 분류를 위해 사용

  • CrossEntropyLoss: 다중 클래스 분류를 위해 사용

  • MSELoss: 회귀 모델에서 사용

옵티마이저(optimizer): 데이터와 손실 함수를 바탕으로 모델의 업데이트 방법을 결정합니다. 다음은 옵티마이저의 주요 특성

  • optimizer는 step() 메서드를 통해 전달받은 파라미터를 업데이트

  • 모델의 파라미터별로 다른 기준(예 학습률)을 적용

  • torch.optim.Optimizer(params, defaults)는 모든 옵티마이저의 기본이 되는 클래스

  • zero_grad() 메서드는 옵티마이저에 사용된 파라미터들의 기울기(gradient)를 0으로 만듦

  • torch.optim.lr_scheduler는 에포크에 따라 학습률을 조절

  • optim.Adadelta, optim.Adagrad, optim.Adam, optim.SparseAdam, optim.Adamax

  • optim.ASGD, optim.LBFGS

  • optim.RMSProp, optim.Rprop, optim.SGD

학습률 스케줄러(learning rate scheduler): 미리 지정한 횟수의 에포크를 지날 때마다 학습률을 감소(decay)시켜 준다. 학습률 스케줄러를 이용하면 학습 초기에는 빠른 학습을 진행하다가 전역 최소점(global minimum) 근처에 다다르면 학습률을 줄여서 최적점을 찾아갈 수 있도록 해 준다. 학습률 스케줄러의 종류는 다음과 같다.

  • optim.lr_scheduler.LambdaLR: 람다(lambda) 함수를 이용하여 그 함수의 결과를 학습률로 설정합니다.

  • optim.lr_scheduler.StepLR: 특정 단계(step)마다 학습률을 감마(gamma) 비율만큼 감소시킵니다.

  • optim.lr_scheduler.MultiStepLR: StepLR과 비슷하지만 특정 단계가 아닌 지정된 에포크에만 감마 비율로 감소시킵니다.

  • optim.lr_scheduler.ExponentialLR: 에포크마다 이전 학습률에 감마만큼 곱합니다.

  • optim.lr_scheduler.CosineAnnealingLR: 학습률을 코사인(cosine) 함수의 형태처럼 변화시킵니다. 따라서 학습률이 커지기도 작아지기도 합니다.

  • optim.lr_scheduler.ReduceLROnPlateau: 학습이 잘되고 있는지 아닌지에 따라 동적으로 학습률을 변화시킬 수 있습니다.

지표(metrics): 훈련과 테스트 단계를 모니터링합니다.

Ex)

1
2
3
4
5
6
7
8
9
10
from torch.optim import optimizer
criterion = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer=optimizer, lr_lambda=lambda epoch: 0.95 ** epoch)
for epoch in range(1, 100+1):# ------ 에포크 수만큼 데이터를 반복하여 처리
    for x, y in dataloader:# ------ 배치 크기만큼 데이터를 가져와서 학습 진행
        optimizer.zero_grad()
loss_fn(model(x), y).backward()
optimizer.step()
scheduler.step()

2.2.5Model Training

앞서 만들어 둔 데이터로 모델을 학습시킨다. 이때 학습을 시킨다는 것은 y = wx + b라는 함수에서 w와 b의 적절한 값을 찾는다는 의미. w와 b에 임의의 값을 적용하여 시작하며 오차가 줄어들어 전역 최소점에 이를 때까지 파라미터(w, b)를 계속 수정한다.

image.png

가장 먼저 optimizer.zero_grad() method를 이용하여 기울기를 초기화한다.

pytorch는 gradient를 계산하기 위해 loss.backward() method를 사용(새로운 gradient값이 이전 gradient값에 누적하여 계산된다)

-> Recurrent Neural Network 모델을 구현할 때는 효과적이지만, 누적 계산이 필요하지 않은 모델에는 불필요(optimizer.zero_grad()를 호출)

Ex)

loss.backward() method를 통해 gradient를 계산하는데, batch가 반복될때마다 오차가 누적됨으로 zero_grad()를 사용하여 0으로 초기화

1
2
3
4
5
6
for epoch in range(100):
    yhat = model(x_train)
    loss = criterion(yhat, y_train)
    optimizer.zero_grad() #------ 오차가 중첩적으로 쌓이지 않도록 초기화
    loss.backward()
    optimizer.step()

2.2.6 Model Evaluation

Ex) 함수를 사용한 모델 평가 코드

1
2
3
4
5
6
7
import torch
import torchmetrics

preds = torch.randn(10, 5).softmax(dim=-1)
target = torch.randint(5, (10,))

acc = torchmetrics.functional.accuracy(preds, target) #------ 모델을 평가하기 위해 torchmetrics.functional.accuracy 이용

Ex) 모듈을 이용한 모델 평가 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
import torchmetrics
metric = torchmetrics.Accuracy() #------ 모델 평가(정확도) 초기화

n_batches = 10
for i in range(n_batches):
    preds = torch.randn(10, 5).softmax(dim=-1)
    target = torch.randint(5, (10,))

    acc = metric(preds, target)
    print(f"Accuracy on batch {i}: {acc}") #------ 현재 배치에서 모델 평가(정확도)

acc = metric.compute()
print(f"Accuracy on all data: {acc}") #------ 모든 배치에서 모델 평가(정확도)

[model.train() & model.eval()]

  • model.train(): 훈련 데이터셋에 사용하며 모델 훈련이 진행될 것임을 알린다. 이때 드롭아웃(dropout)이 활성화됨.

  • model.eval(): 모델을 평가할 때는 모든 노드를 사용하겠다는 의미로 검증과 테스트 데이터셋에 사용

model.train()과 model.eval()을 선언해야 모델의 정확도를 높일 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import torch
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("../chap02/tensorboard") # ------ 모니터링에 필요한 값들이 저장될 위치

for epoch in range(num_epochs):
    model.train()# ------ 학습 모드로 전환(dropout=True)
    batch_loss = 0.0

    for i, (x, y) in enumerate(dataloader):
        x, y = x.to(device).float(), y.to(device).float()
        outputs = model(x)
        loss = criterion(outputs, y)
        writer.add_scalar("Loss", loss, epoch) #------ 스칼라 값(오차)을 기록
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

writer.close() #------ SummaryWriter가 더 이상 필요하지 않으면 close( ) 메서드 호출
1
2
3
4
5
6
7
8
9
10
11
model.eval() #------ 검증 모드로 전환(dropout=False)
with torch.no_grad(): #------ ①
    valid_loss = 0

    for x, y in valid_dataloader:
        outputs = model(x)
        loss = F.cross_entropy(outputs, y.long().squeeze())
        valid_loss += float(loss)
        y_hat += [outputs]

valid_loss = valid_loss / len(valid_loader)

① model.eval()에서 with torch.no_grad()를 사용하는 이유

파이토치는 모든 연산과 기울기 값을 저장한다. 하지만 검증(혹은 테스트) 과정에서는 역전파가 필요하지 않기 때문에 with torch.no_grad()를 사용하여 기울기 값을 저장하지 않도록 한다. 이와 같은 과정을 통해 기울기 값을 저장하고 기록하는 데 필요한 메모리와 연산 시간을 줄일 수 있다.

This post is licensed under CC BY 4.0 by the author.