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’ 방식을 사용. 따라서 연산 그래프와 연산을 분리해서 생각할 필요가 없기 때문에 코드를 이해하기 쉽다.
+)
• 1차원 축(행) = axis 0 = 벡터
• 2차원 축(열) = axis 1 = 행렬
• 3차원 축(채널) = axis 2 = 텐서
2.1.2 파이토치의 아키텍처
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이다.
파이토치 기초문법
텐서(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
데이터로더(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())
+) 참고
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( )
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)를 계속 수정한다.
가장 먼저 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()를 사용하여 기울기 값을 저장하지 않도록 한다. 이와 같은 과정을 통해 기울기 값을 저장하고 기록하는 데 필요한 메모리와 연산 시간을 줄일 수 있다.