[AIAS] Lect5. Deep Learning Programming

11 minutes to read

AIAS-Lect5_DL Programming

Deep Learning Platform

소소한 팁

데이터의 양보다는 데이터의 퀄리티가 더 중요하다. 예를 들어 얼굴인식 모델을 만들 때, 너무 고정적인 환경에서의 데이터만 주어진다면 데이터가 10만장이어도 중복성때문에 사실상 유용성은 떨어질 수 있다. 반면, 30개의 데이터를 사용하더라도 성별, 옷차림, 헤어스타일, 악세사리, 안경 등의 practical 환경에서 가능한 여러가지 변화를 준 데이터를 사용한다면 훨씬 성능이 나아질 수 있다.

model을 만드는 것보다 더 중요한 것은 goal을 정의하는 것이다.

AI의 역사

초기 인공지능 분야는 비주류였다. 하지만 빅데이터, HW의 발전, SW의 발전 등의 요인으로 짧은 기간동안 큰 발전을 이룰 수 있었다.

  • Big Data : Datasets이 더욱 커졌으며 수집 및 저장이 용이해졌다.
  • Hardware : Graphics Procesisng Units(GPUs)기반 딥러닝 등장과 대규모 병렬 컴퓨팅(Massively Parallel Computer, MPP)
  • Software : 향상된 기술, 새로운 models과 toolboxs

ML Pipeline

ML Pipeline이란?

ML pipeline은 아래 diagram과 같이 몇 가지 components로 구성되어 있다.1 ex_screenshot

이처럼 model은 일부이고 다양한 components를 거쳐 최종적인 ML이 완성된다. 예를 들어, 일반적인 ML은 다음의 과정을 거친다.

  1. Data & Streaming : 분산 데이터 저장 및 스트리밍. 심하게 noisy한 data는 삭제된다. Google Cloud Storage, 아파치 카산드라, 아파치 하둡 등.

  2. Users : 데이터 준비 및 분석의 단계. Jupyterhub, 아파치 제플린, 아파치 스파크 등.

  3. Frameworks & Cluster : Deep Learning Tools와 분산 호스팅 사용의 단계. 엔지니어링 영역. Tensorflow, PyTorch 등.

  4. Models : Machine learning model을 빌딩. 사이언티스트의 영역.

  5. Model Serving : Model을 clients에게 전송

Deep Learning Hardware

CPU와 GPU의 차이점

재밌게 봤던 영상: Mythbusturs Demo GPU versus CPU - NVIDIA. CPU와 GPU의 차이점을 시각적으로 재미있게 설명해준다.

  • CPU(Central Processing Unit)2
    개수는 적지만 각 코어가 빠르고 훨씬 capable하다. 순차적인 tasks 처리에 유용하다.
    • number of cores = 4 ⇒ 직렬처리에 최적화된 몇 개의 코어로 구성됨
    • Memory = System RAM ⇒ cache가 매우 작으므로 대부분의 메모리를 RAM에서 가져다 사용한다. RAM은 보통 8, 12, 16, 32GB의 크기를 갖는다. 이는 GPU의 메모리 크기와 비교했을 때 매우 적은 양.
  • GPU(Graphics Processing Unit)3
    개수는 많지만 각 코어가 비교적 느리고 단순한 작업만을 수행한다. 병렬처리에 유용한다.
    • number of cores = 4,352 ⇒ 다수의 소형 코어로 구성됨. 이를 통해 병렬처리를 효율적으로 처리 가능.
    • Memory = 11GB GDDR6 ⇒ 칩 안에 RAM이 내장되어있는 내장 메모리를 사용한다.

CPU와 GPU의 Communication

하드웨어가 균형적으로 구성되지 않는 경우, PC의 성능이 저하되는 병목현상이 나타날 수 있다.

Bottle Neck이란? bottle neck은 이름 그대로 해석할 때 “병(bottle)의 목(neck) 현상”이다. 병에 들어있는 물체가 병의 주둥아리(bottle neck)으로 인해 한 번에 나오지 못하고 지연이 발생하는 현상과 관련한 용어이다.

CPU Bottle Neck CPU가 여러 복잡한 프로세스를 처리할 때, CPU의 처리 속도가 데이터의 전송속도를 따라가지 못하는 경우 발생한다.

실제 딥러닝 모델을 학습시킬 때 구간별로 소요시간을 측정해볼 것! 병목현상이 어디에서 발생하는지를 확인할 수 있다.

해결방법

  1. CPU와 GPU의 사양을 일치시킨다.
  2. CPU와 GPU에 걸리는 처리 부하의 균형을 맞춘다.
  3. HDD보다는 SSD를 사용한다.

Deep Learning Software

언어는 주로 PyTorch(Facebook), TensorFlow(Google)를 사용한다.

Microsoft Azure, amazon web services, Google Cloud Platform, Alibaba Cloud 등에서 좋은 GPU를 대여해주기도 한다.

Deep Learning Frameworks에서의 주요 포인트

  1. 실행력 : 새로운 idea가 있을 때 빠르게 실행하는 것이 중요하다. ppt 100슬라이드보담 빠르게 데모 보여주는게 더 임팩트가 있다. 그런 측면에서 DL framework를 활용하면 실제와 모델을 맵핑하기 쉬워진다.
  2. autograd : pytorch등은 기본적인 함수의 gradient는 자동으로 구해준다. 하지만 ReLU같은 간단한 module에 대해서 forward와 backward정도는 구해본다면 이해도가 높아질 것이다.
  3. GPU : 머신러닝 모델은 GPU 내에서 효율적으로 돌아간다. (cuDNN, cuBLAS, OpenGL 등)

Numpy vs. PyTorch

PyTorch를 사용하면 보다 간단하고 빠르게 머신러닝 코드를 구현할 수 있다. Numpy와 PyTorch로 각각 모델을 구현해본 뒤 차이점을 살펴보자.

Numpy

import numpy as np
np.random.seed(0)

N,D = 3, 4 # N=batch개수, D=차원

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)

# 각 node별로 gradient 계산하기<sup>[4](#mFoot)</sup>
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_q * x
  • Numpy로 구현한 코드는 API가 깔끔하고 숫자를 다루기 쉽다는 장점이 있으나, gradients 수식을 직접 계산해야므로 번거롭고, GPU 위에서 돌릴 수 없다는 단점이 있다.

PyTorch

import torch

N, D = 3, 4
x = torch.randn(N,D, required_grad=True)
y = torch.randn(N,D)
z = torch.randn(N,D)

a = x * y
b = a + z
c = torch.sum(b)

c.backward() # 위에서 required_grad값을 True해주었으므로 backward()를 통해 바로 gradient를 계산할 수 있다.
print(x.grad) 

1: Overview of ML Pipelines (https://developers.google.com/machine-learning/testing-debugging/pipeline/overview?hl=ko)
2: Intel Core i7-7700k 기준
3: (NVIDIA RTX 2080 Ti 기준)
4: gradient 계산 관련 포스팅 참고


PyTorch: 기본 개념

  • Tensor : numpy array와 유사한 개념으로, numpy와 달리 GPU위에서도 돌아간다.
  • Autograd : tensors로 구성된 computational graphs를 만드는 package로, 자동으로 gradients를 계산해준다.
  • Module : 하나의 neural network layer로, state와 learnable weigths를 저장할 수 있다.

위의 내용을 코드와 함께 하나씩 살펴보자.

해당 내용은 PyTorch 공식 홈페이지의 Learning PyTorch with Examples를 참고했습니다.

PyTorch 문법과 관련된 내용은 PyTorch 공식 홈페이지의 Docs에서 확인할 수 있습니다.

PyTorch: Running Example

Running example:

  1. A fully-connected ReLU network with one hidden layer and no biases
  2. trained to predict y from x by minimizing squared Euclidean distance.

img1

위와 같은 그림을 많이 봤을 것이다. 2개의 layer로 구성되었다는 것을 알 수 있다. 위 그림에서 생략된 부분을 자세히 나타내면 아래 그림과 같다.

img2

layer별로 weigth parameter가 존재하며, 첫 번째 layer에는 ReLU activation function도 존재함을 확인할 수 있다. 이와 같이 모델을 구성하고자 할때 PyTorch를 이용하여 코드를 작성해보자.

PyTorch: Tensors

PyTorch는 Tensors를 지원한다. 이를 통해 모델을 디자인할 수 있다.

Tensor란?

  • 전체 코드 보기 [참고]

    import torch
      
    device = torch.device('cpu')
      
    N, D_in, H, D_out = 64, 1000, 100, 10
    x = torch.randn(N, D_in, device=device)
    y = torch.randn(N, D_out, device=device)
    w1 = torch.randn(D_in, H, device=device)
    w2 = torch.randn(H, D_out, device=device)
      
    learning_rate = 1e-6
    for t in range(500):
        h = x.mm(w1)
        h_relu = h.clamp(min=0)
        y_pred = h_relu.mm(w2)
        loss = (y_pred - y).pow(2).sum()
      
        grad_y_pred = 2.0 * (y_pred - y)
    		grad_w2 = h_relu.t().mm(grad_y_pred)
    		grad_h_relu = grad_y_pred.mm(we.t())
    		grad_h = grad_h_relu.clone()
    		grad_h[h<0] = 0
    		grad_w1 = x.t().mm(grad_h)
      
    		w1 -= learning_rate * grad_w1
    		w2 -= learning_rate * grad_w2
    
  1. Create random tensors for data and weights

    img3

    device = torch.device('cpu')
       
    N, D_in, H, D_out = 64, 1000, 100, 10
    x = torch.randn(N, D_in, device=device)
    y = torch.randn(N, D_out, device=device)
    w1 = torch.randn(D_in, H, device=device)
    w2 = torch.randn(H, D_out, device=device)
       
    learning_rate = 1e-6
    

    먼저 N, D_in, H, D_out과 같은 hyperparameters를 정의한다. 여기서 N은 batch size를 의미하며, D_in, H, D_out은 각각 input layer, hidden layer, output layer의 크기를 의미한다.

    hyperparameters(N, D_in, H, D_out)을 결정했다면 자동적으로 x, y, w1, w2의 사이즈가 결정된다. 결정된 사이즈를 갖는 x, y, w1, w2에 대해 random tensors를 생성하여 초기화 해 준다.

    learning_rate도 마찬가지로 초기화해준다.

    한편, device는 코드가 돌아갈 장치를 지정해준다. 값을 'cpu'가 아닌 'cuda:0'와 같이 GPU로 지정하면 학습 속도가 향상된다.

  2. Forward pass: compute predictions and loss

    img4

    for t in range(500):
        h = x.mm(w1)
        h_relu = h.clamp(min=0)
        y_pred = h_relu.mm(w2)
        loss = (y_pred - y).pow(2).sum()
    

    이 예제에서는 activation function으로 ReLU함수를 사용한다. 구성된 텐서 수식을 통해 예측값(y_pred)와 예측값에 따른 loss를 계산한다.

  3. Backward pass: manually compute gradients

    img5

        grad_y_pred = 2.0 * (y_pred - y)
    		grad_w2 = h_relu.t().mm(grad_y_pred)
    		grad_h_relu = grad_y_pred.mm(we.t())
    		grad_h = grad_h_relu.clone()
    		grad_h[h<0] = 0
    		grad_w1 = x.t().mm(grad_h)
    

    gradient는 loss가 줄어드는 방향을 알려준다. gradients를 구하는 수식을 계산해준다.

  4. Gradient descent step on weights

    img6

    계산한 gradients값을 w1, w2에 적용해준다. 이를 통해 다음 반복부터는 loss를 줄일 수 있게 될 것이다.

PyTorch: Autograd

PyTorch는 autograd 기능을 지원한다. 이를 통해 gradients를 손수 계산하는 대신 코드상에서 자동으로 계산되게 만들 수 있다.

  • 전체 코드 보기

    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):
        h = x.mm(w1)
        h_relu = h.clamp(min=0)
        y_pred = h_relu.mm(w2)
        loss = (y_pred - y).pow(2).sum()
      
        loss.backward()
      
    		with torch.no_grad():
    			w1 -= learning_rate * grad_w1
    			w2 -= learning_rate * grad_w2
    			w1.grad.zero_()
    			w2.grad.zero_()
    

img7

왼쪽은 Autograd를 사용하기 이전 코드이고 오른쪽은 PyTorch에서 지원하는 Autograd를 사용한 코드이다. 주황색 박스를 보면, 기존의 수식을 직접 대입하는 방식에서 loss.backward()와 같이 자동으로 계산하게 하는 코드로 바뀐 것을 확인할 수 있다. 이때 autograd를 수행하는 parameter는 requires_grad=True와 같이 명시해주어야 한다(빨간 박스부분). 해당 인자를 통해 PyTorch가 computational graph를 만들기 때문이다.

x와 y는 parameters가 아니라 data다. 따라서 학습가능하지 않고 불변하므로 gradients를 계산할 수 없다.

torch.no_grad()는 이 부분에 대해서는 computational graph를 생성하지 말라는 것을 의미한다. 한편, 다음 iteration에서는 새로운 data가 들어오므로 메모리를 들고있을 필요가 없다. 따라서 w.grad.zero_()를 통해 gradient를 초기화해준다.

TIP! gradient초기화해주는 부분(w.grad.zero_())에서 함수 끝에 언더바(_)가 있는 걸 확인할 수 있다. 이것은 결과값이 return으로 반환되는 게 아니라, 그 자체를 변형시킨다는 것을 의미한다.

Autograd함수 직접 정의하는 방법

forward와 backward함수 작성을 통해 자유롭게 autograd함수를 만들 수 있다.

  • 직접정의한 Autograd 함수: MyReLU()

    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
    
  • MyReLU()를 사용한 전체 코드

    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):
    		y_pred = my_relu(x.mm(w1)).mm(w2)
        loss = (y_pred - y).pow(2).sum()
      
        loss.backward()
      
    		with torch.no_grad():
    			w1 -= learning_rate * grad_w1
    			w2 -= learning_rate * grad_w2
    			w1.grad.zero_()
    			w2.grad.zero_()
    

Forward pass: compute predictions and loss Backward pass: compute gradients

forward()는 predictions와 loss를 계산하는 역할을 한다. 위 예시에서는 x.clamp(min=0)을 통해 ReLU결과를 반환한다. 한편 여기서 ctx는 일종의 cache memory역할을 한다.

backward()는 gradients를 계산하는 역할을 한다.

일반적인 경우에는 backward()함수를 정의할 필요가 없다. backward가 필요없는 경우에는 ctx도 필요없으므로 autograd를 하지 않는 경우는 해당 부분을 삭제함으로써 memory증가를 막아줘야 한다.

PyTorch: nn

Computational graph와 autograd는 복잡한 연산을 정의하거나 자동으로 derivatives를 구하는 데에 효과적이다. 하지만 매우 큰 neural networks에 대해서는 autograd가 너무 low-level일 수 있다. 따라서 PyTorch는 nn package를 통해 이러한 문제를 해결한다.

nn package는 neural network layers와 거의 동등한 modules 세트를 정의한다. Module은 input Tensors를 받고, output Tensors를 계산한다 (물론 learnable parameters를 포함하는 tensors같은 internal state도 보유할 수 있다.). nn package는 또한 유용한 loss functions 세트도 정의하고 있다.

img8

주요 코드 비교

  • 전체 코드보기

    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):
    	y_pred = model(x)
    	loss = torch.nn.functional.mse_loss(y_pred, y)
      
    	loss.backward()
      
    	with torch.no_grad():
    		for param in model.parameters():
    			param -= learning_rate * param.grad
    	model.zero_grad()
    

PyTorch:Autograd 코드(일부)

...

learning_rate = 1e-6
for t in range(500):
    h = x.mm(w1)
    h_relu = h.clamp(min=0)
    y_pred = h_relu.mm(w2)
    loss = (y_pred - y).pow(2).sum()

...

PyTorch:nn 코드(일부)

...

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):
	y_pred = model(x)
	loss = torch.nn.functional.mse_loss(y_pred, y)

...
  • PyTorch 문법 helper

    torch.nn.Sequential(*args) : A sequential container. Modules will be added to it in the order they are passed in the constructor.

    torch.nn.Linear(in_features, out_features, bias=True) : Applies a linear transformation to the incoming data: $y=xA^T+b$

    parameters

    • in_features – size of each input
    • sampleout_features – size of each output
    • samplebias – If set to False, the layer will not learn an additive bias. Default: True

    torch.nn.ReLU(inplace=False) : Applies the rectified linear unit function element-wise: $ReLU(x)=(x)^+=max(0,x)$

    parameters

    • inplace – can optionally do the operation in-place. Default: False

위와 같이 model을 정의하면, for문에서 위와 같이 사용할 수 있다. 이처럼 PyTorch.nn class의 장점을 활용하면 코드를 좀 더 간결하고 유연하게 만들 수 있다.

새로운 modules 직접 정의하는 방법

__init__과 forward함수 작성을 통해 자유롭게 모듈을 정의할 수 있다.

  • 직접 정의한 module: TwoLayerNet()

    import torch
      
    class TwoLayerNet(torch.nn.Module):
    	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)
      
    	def forward(self, x):
    		h_relu = self.linear1(x).clamp(min=0)
    		y_pred = self.linear2(h_relu)
    		return y_pred
    

    autograd가 backward를 핸들링하므로 여기서는 backward()함수를 정의할 필요가 없다.

  • TwoLayerNet을 사용한 전체 코드

    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 = 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()
    

새로운 module을 정의할 때 nn의 module을 사용할 수 있다.

PyTorch: optim

[PyTorch: Tutorials > 예제로 배우는 파이토치(PyTorch) > PyTorch: optim]

이전에 PyTorch:nn package를 이용해서 신경망을 구현한 코드에서 더 나아가 모델의 가중치(loss)를 직접 갱신하는 대신 optim package를 이용하여 가중치를 갱신할 optimizer를 정의해보자. optim package는 일반적으로 딥러닝에 사용되는 SGD+momentum, RMSProp, Adam과 같은 다양한 최적화(Optimization) 알고리즘을 정의한다.

주요 코드 비교

  • 전체 코드보기

    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 = torch.optim.Adam(model.parameters(),
    															lr = learning_rate)
      
    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()
    

PyTorch:nn 코드(일부)

...

for t in range(500):
	y_pred = model(x)
	loss = torch.nn.functional.mse_loss(y_pred, y)

	loss.backward()

	with torch.no_grad():
		for param in model.parameters():
			param -= learning_rate * param.grad
	model.zero_grad()

PyTorch:optim 코드(일부)

...
optimizer = torch.optim.Adam(model.parameters(),
															lr = learning_rate)

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()
  • PyTorch 문법 helper

    torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0, amsgrad=False) : Implements Adam algorithm.

    parameters

    • params (iterable) – iterable of parameters to optimize or dicts defining parameter groups
    • lr (float, optional) – learning rate (default: 1e-3)
    • betas (Tuple[floatfloat], optional) – coefficients used for computing running averages of gradient and its square (default: (0.9, 0.999))
    • eps (float, optional) – term added to the denominator to improve numerical stability (default: 1e-8)
    • weight_decay (float, optional) – weight decay (L2 penalty) (default: 0)
    • amsgrad (boolean, optional) – whether to use the AMSGrad variant of this algorithm from the paper On the Convergence of Adam and Beyond (default: False)

    torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0, amsgrad=False) : Implements Adam algorithm.

    parameters

    • params (iterable) – iterable of parameters to optimize or dicts defining parameter groups
    • lr (float, optional) – learning rate (default: 1e-3)
    • betas (Tuple[floatfloat], optional) – coefficients used for computing running averages of gradient and its square (default: (0.9, 0.999))
    • eps (float, optional) – term added to the denominator to improve numerical stability (default: 1e-8)
    • weight_decay (float, optional) – weight decay (L2 penalty) (default: 0)
    • amsgrad (boolean, optional) – whether to use the AMSGrad variant of this algorithm from the paper On the Convergence of Adam and Beyond (default: False)

위 코드는 optimizer를 Adam으로 정의하고, 이를 이용하여 for문에서 parameters와 zero gradients를 업데이트해주었다.

PyTorch: Pretrained Models

torchvision을 이용하면 pretrained models를 쉽게 사용할 수 있다. pytorch/vision 깃허브에 들어가면 자세한 내용을 확인할 수 있다.

PyTorch: torch.utils.tensorboard

TensorBoard utility를 이용하여 TensorBoard UI내에서 PyTorch 결과를 시각화 할 수 있다. TensorBoard에 대한 자세한 내용은 TensorFlow-TensorBoard 페이지에서 확인 가능하다.

PyTorch: Dynamic Computational Graphs

코드의 실행 단계에 따라 computational graph가 동적으로 생성된다.

⇒ 비효율적

PyTorch: Static Computational Graphs

먼저 computational graph를 생성한 뒤, 해당 graph를 매 iteration마다 재사용한다.

graph = build_graph()

for x_batch, y_batch in loader:
	run_graph(graph, x=x_batch, y=y_batch)