4 분 소요

본 글은 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를 만들 기본 클래스이다.
    • 서브클래스 함수 및 forwardbackward 메서드 구현
    • 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 등 처음부터 다시 시작한다.





References

댓글남기기