[CS231n] 강의6. Hardware and Software with Pytorch 리뷰
본 글은 2021년 4월에 강의한 스탠포드 대학의 “Convolutional Neural Networks for Visual Recognition” 2021년 강의를 듣고 정리한 내용입니다.
개인 공부 목적으로 작성되었으며, 설명이 맞지 않거나 글 오타가 있으면 알려주시길 바랍니다.
원본 링크 : cs231n.stanford.edu
한글 번역 링크 : aikorea.org - cs231n
강의 링크 : youtube - 2017 Spring (English)
- 이 강의에서는 CPU와 GPU같은 하드웨어 내용이 자세히 나오지만, 여기서는 생략하도록 하겠다.
- 또한 파이토치와 텐서플로 등 다양한 딥러닝 프레임워크들이 나오지만, 여기서는 파이토치만 집중적으로 다루겠다.
Computational Graphs
-
먼저 계산 그래프를 numpy로 작성해본다.
import numpy as np np.random.seed(0) N, D = 3, 4 x = np.random.randn(N, D) y = np.random.randn(N, D) z = np.random.randn(N, D) a = x * y b = a + z c = np.sum(b) grad_c = 1.0 grad_b = grad_c * np.ones((N, D)) grad_a = grad_b.copy() grad_z = grad_b.copy() grad_x = grad_a * y grad_y = grad_a * x
- Good
- API가 깔끔하다.
- 수치적 코드를 쓰기 편하다.
- Bad
- 고유의 그레이디언트를 계산하지 못한다.
- GPU에서 실행할 수 없다.
PyTorch
- torch.Tensor : 넘파이(numpy) 어레이(array)와 비슷하지만, GPU에서 실행할 수 있다.
- torch.autograd : 텐서로 계산 그래프(computational graph)를 만들고, 그레이디언트(gradient)를 자동으로 계산하는 패키지이다.
- torch.nn.Module : 신경망(neural network) 레이어; 상태(state) 또는 학습 가능한 가중치를 저장할 수 있다.
Tensors
- 다음은 torch.Tensor에 관한 예제이다.
- torch.randn : 평균이 0이고, 표준편차가 1인 가우시안 정규분포를 이용해 생성한다.
- torch.mm : 행렬의 행렬 곱셈을 수행한다.
- torch.clamp(min, max) : 모든 요소를 [min, max] 범위로 고정한다. 1
- torch.t(input) : input이 2차원 텐서보다 작을 것으로 예상하고 차원 0과 1을 전치(transpose)한다.
import torch
# device 정의 = gpu가 가능하면 cuda, 아니면 cpu
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 데이터와 가중치를 위해 랜덤 텐서 생성
# H : hidden의 약어
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in, device=device)
x = torch.randn(N, D_out, device=device)
x = torch.randn(D_in, H, device=device)
x = torch.randn(H, D_out, device=device)
learning_rate = 1e-6
for t in range(500):
# 정방향 전달(forward pass) : 예측(prediction)과 손실(loss)을 계산한다.
h = x.mm(w1)
h_relu = h.clamp(min=0)
y_pred = h_relu.mm(w2)
loss = (y_pred - y).pow(2).sum()
# 역방향 전달(backward pass) : 수동으로 그레이디언트를 계산한다.
grad_y_pred = 2.0 * (y_pred - y)
grad_w2 = h_relu.t().mm(grad_y_pred)
grad_h_relu = grad_y_pred.mm(w2.t())
grad_h = grad_h_relu.clone()
grad_h[h < 0] = 0
grad_w1 = x.t().mm(grad_h)
# 가중치에 대한 경사 하강(gradient descent) 단계(step)
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2
Autograd
requires_grad=True
로 텐서를 생성하면 autograd가 활성화된다.- 모든 연산 추적
requires_grad=True
가 있는 텐서에 대한 연산(operation)은 pytorch가 계산 그래프(computational graph)를 빌드한다.- torch.randn(requires_grad=True) : autograd가 반환된 텐서의 연산을 기록해야 하는 경우 쓰인다.
- torch.Tensor.backward : 현재 w,r,t 그래프가 남아있는 텐서의 그레이디언트를 계산한다.
- 텐서에 대한 그레이디언트는
.grad
속성에 누적된다.
- 텐서에 대한 그레이디언트는
- torch.autograd.grad : 입력에 대한 출력의 그레이디언트 합계를 계산하고 반환한다.
- torch.no_grad : 이 부분에서는 계산 그래프를 만들지 말라는 의미이다.
- grad.zero_ : _로 끝나는 파이토치 메서드는 텐서를 제자리에서 수정하고, 새로운 텐서를 반환하지 않는 메서드이다.
import torch
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
w1 = torch.randn(D_in, H, requires_grad=True)
w2 = torch.randn(H, D_out, requires_grad=True)
learning_rate = 1e-6
for t in range(500):
# forward pass이지만, pytorch가 그래프에서 자동으로 추적해준다.
y_pred = x.mm(w1).clamp(min=0).mm(w2)
loss = (y_pred - y).pow(2).sum()
# w1 및 w2에 대한 손실 그레이디언트 계산
loss.backward()
# 가중치에 gradient step을 만든 다음 0으로 만든다.
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w1.grad.zero_()
w2.grad.zero_()
New Autograd Functions
- 텐서에 대한 정방향(forward) 및 역방향(backward) 함수를 만들어 자체 autograd 함수를 정의해본다.
- torch.autograd.Function : 사용자 지정 autograd.Function를 만들 기본 클래스이다.
- 서브클래스 함수 및 forward 와 backward 메서드 구현
- forward : 연산 수행
- backward : 역방향 모드 자동 미분를 하는 연산을 하는 공식을 정의
- ctx(=context method) 인수에 대한 적절한 메서드 호출한다.
- function.FunctionCtx.save_for_backward : 나중에 backward()에 호출할 때 사용할 텐서를 저장한다.
import torch
class MyReLU(torch.autograd.Function):
@staticmethod
def forward(ctx, x):
ctx.save_for_backward(x)
return x.clamp(min=0)
@staticmethod
def backward(ctx, grad_y):
x, = ctx.saved_tensors
grad_input = grad_y.clone()
grad_input[x < 0] = 0
return grad_input
def my_relu(x):
return MyReLU.apply(x)
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
w1 = torch.randn(D_in, H, requires_grad=True)
w2 = torch.randn(H, D_out, requires_grad=True)
learning_rate = 1e-6
for t in range(500):
# 정방향에 새로운 autograd function 사용
y_pred = my_relu(x.mm(w1)).mm(w2)
loss = (y_pred - y).pow(2).sum()
# w1 및 w2에 대한 손실 그레이디언트 계산
loss.backward()
# 가중치에 gradient step을 만든 다음 0으로 만든다.
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w1.grad.zero_()
w2.grad.zero_()
nn
- 신경망 작업을 위한 상위 레벨 래퍼(wrapper)이다.
- torch.nn.Linear(in_features, out_features) : 들어오는 데이터에 선형 변환 적용
- $y=A^T+b$
- torch.nn.ReLU : rectfied linear unit function을 요소별로 적용
- $ReLU(x)=max(0,x)$
- torch.nn.functional.mse_loss(input, target) : 요소별 평균 제곱 오차를 측정한다.
- torch.no_grad : 그레이디언트 계산을 사용하지 않도록 설정한 컨텍스트 관리자(context-manager)
- model.parameters : 파라미터 반환(weight, bias 등 torch.tensor로 이루어진 것들을 말함) 2
import torch
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# 모델을 일련의 데이터로 정의
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out)
)
learning_rate = 1e-2
for t in range(500):
# forward pass
y_pred = model(x)
loss = torch.nn.functional.mse_loss(y_pred, y)
# 모든 모델 가중치에 대한 그레이디언트 계산
# requires_grad = True 를 가지고 있는 것들을
loss.backward()
# 각 모델 매개변수에 gradient step 만들기
# 이때는 그레이디언트 사용 안함
with torch.no_grad():
for param in model.parameters():
param -= learning_rate * param.grad
model.zero_grad()
optim
- optimizer.step : 파라미터 값을 업데이트하는 과정이다.
- optimizer.zero_grad : 역전파 단계 전에, optimizer 객체를 사용하여 (모델의 학습 가능한 가중치인) 갱신할 변수들에 대한 모든 그레이디언트(gradient)를 0으로 만든다. 이렇게 하는 이유는 기본적으로 .backward()를 호출할 때마다 그레이디언트가 버퍼(buffer)에 (덮어쓰지 않고) 누적되기 때문이다. 3
import torch
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# 모델을 일련의 데이터로 정의
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out)
)
learning_rate = 1e-4
# 다른 업데이트 규칙으로 옵티마이저(optimizer)를 적용
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(500):
# forward pass
y_pred = model(x)
loss = torch.nn.functional.mse_loss(y_pred, y)
# 모든 모델 가중치에 대한 그레이디언트 계산
# requires_grad = True 를 가지고 있는 것들을
loss.backward()
optimizer.step()
optimizer.zero_grad()
nn - Define new Modules
- PyTorch Module : 신경망 계층으로 텐서를 입력 및 출력한다.
- autograd를 사용하여 자체 모듈을 정의할 수 있다.
import torch
# 전체 모듈을 단일 모듈로 정의
class TwoLayerNet(torch.nn.Modules):
def __init__(self, D_in, H, D_out):
super(TwoLayerNet, self).__init__()
self.linear1 = torch.nn.Linear(D_in, H)
self.linear2 = torch.nn.Linear(H, D_out)
# child module을 사용하여 forward pass 정의
# autograd가 알아서 처리하기 때문에 backward 정의할 필요 없음
def forward(self, x):
h_relu = self.linear1(x).clamp(min=0)
y_pred = self.linear2(h_relu)
return y_pred
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
model = TwoLayerNet(D_in, H, D_out)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4)
for t in range(500):
y_pred = model(x)
loss = torch.nn.functional.mse_loss(y_pred, y)
loss.backward()
optimizer.step()
optimizer.zero_grad()
- 사용자 지정 모듈 하위 클래스와 sequential container를 혼합하여 쓸 수도 있다.
import torch
class ParallelBlock(torch.nn.Module):
def __init__(self, D_in, D_out):
self.linear1 = torch.nn.Linear(D_in, D_out)
self.linear2 = torch.nn.Linear(D_in, D_out)
def forward(self, x):
h1 = self.linear1(x)
h2 = self.linear2(x)
return (h1 * h2).clamp(min=0)
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
model = torch.nn.Sequential(
ParallelBlock(D_in, H),
ParallelBlock(H, H),
torch.nn.Linear(H, D_out)
)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
for t in range(500):
y_pred = model(x)
loss = torch.nn.functional.mse_loss(y_pred, y)
loss.backward()
optimizer.step()
optimizer.zero_grad()
Pretrained Models
- torchvision을 통해 사전 학습된(pre-trained) 모델을 매우 쉽게 사용할 수 있다.
import torch
import torchvision
alexnet = torchvision.models.alexnet(pretrained=True)
vgg16 = torchvision.models.vgg16(pretrained=True)
resnet101 = torchvision.models.resnet101(pretrained=True)
Dynamic Computation Graphs
- 다음은 코드에 따른 계산 그래프를 나타낸 것이다. 설명없이 ppt만 봐도 쉽게 이해될 것이다.
- 이 과정을 매번 반복할 때마다 그래프 삭제, backprop path 등 처음부터 다시 시작한다.
댓글남기기