Keras는 wrapper로, interface는 사용자가 사용하기 쉽지만 내부는 TensorFlow나 PyTorch로 구현되어 있다.
Keras의 속도는 완전 느리지 않다. 창시자가 구글에 있다보니 TensorFlow와 결합하게 되어 유지/보수가 되기 때문이다.
인기도(popularity)는 분야에 따라 달라진다.
PyTorch와 TensorFlow의 가장 큰 차이
는 마지막 부분인 Computational graphs used
이다.
Computational graphs
는 연산의 과정을 그래프로 표현하는 것이다. 두 프레임워크는 역전파 과정에서 자동미분을 할 때, 연산과정을 그래프로 그리는 시점에서 차이가 발생한다.TensorFlow
는 Define and Run
방법으로 그래프를 위와 같이 먼저 코드로 정의한 후, 실행 시점에 데이터를 feed한다.
PyTorch
는 Define by Run
방법(DCG: Dynamic Computational Graph)으로 실행을 하면서 그래프를 생성하는 방식이다.
DCG 방식이 중간에 미분 값을 확인할 수 있어, 디버깅할 때 더 편하다.
TensorFlow는 production, scalability, Multi GPU 등 에서 강점을 가진다.
PyTorch는 논문 구현, 아이디어 구현, 사용의 편리성
등에서 강점을 가진다.
Numpy
, AutoGrad
(자동미분), Function
이다.
- Numpy 구조를 가지는 Tensor 객체로 array를 표현한다.
자동미분
을 지원하여 DL 연산을 지원한다.- 다양한 형태의 DL을 지원하는 함수와 모델을 지원한다.
자동미분이 딥러닝 프레임워크의 핵심부분이다.
Dataset, Multi-GPU 등의 다양한 함수와 모델을 지원한다.
이런 특징들로, PyTorch는 비교적 쉽게 사용할 수 있다는 장점을 가진다.
PyTorch는 Numpy 기반으로 만들었기 때문에 문법은 자동미분
을 제외하고 Numpy와 유사하다.
다차원 Arrays
를 표현하는 PyTorch 클래스이다.
시살상 numpy의 ndarray와 동일
하고, TensorFlow의 Tensor와도 동일하다.
Tensor를 생성하는 함수도 거의 동일하다.
import numpy as np
n_array = np.arange(10).reshape(2,5)
print(n_array)
print("ndim :", n_array.ndim, "shape :", n_array.shape)
# 결과
#
# [[0 1 2 3 4]
# [5 6 7 8 9]]
# ndim : 2 shape : (2, 5)
import torch
t_array = torch.FloatTensor(n_array)
print(t_array)
print("ndim :", t_array.ndim, "shape :", t_array.shape)
# 결과
#
# tensor([[0., 1., 2., 3., 4.],
# [5., 6., 7., 8., 9.]])
# ndim : 2 shape : torch.Size([2, 5])
# data to tensor
data = [[3, 5],[10, 5],]
x_data = torch.tensor(data)
# ndarray to tensor
nd_array_ex = np.array(data)
tensor_array = torch.from_numpy(nd_array_ex)
Tensor의 data type은 numpy와 유사하지만, GPU tensor
를 추가적으로 가진다.
GPU tensor는 GPU를 쓸 수 있게 해준다.
자세한 내용은 아래 reference를 참고하자.
import numpy as np
import torch
data = [[3, 5, 20],[10, 5, 50], [1, 5, 10]]
x_data = torch.tensor(data)
print(x_data[1:])
# 결과
#
# tensor([[10, 5, 50],
# [ 1, 5, 10]])
print(x_data[:2, 1:])
# 결과
#
# tensor([[ 5, 20],
# [ 5, 50]])
print(x_data.flatten())
# 결과
#
# tensor([ 3, 5, 20, 10, 5, 50, 1, 5, 10])
print(torch.ones_like(x_data))
# 결과
#
# tensor([[1, 1, 1],
# [1, 1, 1],
# [1, 1, 1]])
print(x_data.numpy())
# 결과
#
# array([[ 3, 5, 20],
# [10, 5, 50],
# [ 1, 5, 10]])
print(x_data.shape)
print(x_data.dtype)
# 결과
#
# torch.Size([3, 3])
# torch.int64
GPU에
올려서 사용가능하다.print(x_data.device)
# 결과
#
# device(type='cpu')
if torch.cuda.is_available():
x_data_cuda = x_data.to('cuda')
print(x_data_cuda.device)
# 결과
#
# device(type='cuda', index=0)
덧셈, 슬라이스, 뺄셈 등은 numpy와 동일하지만, 행렬곱셈 연산에서 차이가 난다.
행렬곱셈 연산의 함수는 dot이 아닌 mm을 사용한다.
PyTorch에서는 내적의 연산(dot)과 행렬곱셈 연산(mm)을 구분한다.
matmul은 mm과 동일한 기능을 하지만, matmul은 mm과 다르게 broadcasting을 지원
하여 처리한다.
a = torch.rand(5, 2, 3)
b = torch.rand(5)
a.mm(b)
# 결과
#
# error
# 5는 batch size, 2 x 3 행렬과 3 x 1 행렬의 곱셈
# 따라서 결과는 5 x 2 x 1 행렬이 된다.
a = torch.rand(5, 2, 3)
b = torch.rand(3)
a.matmul(b)
# 결과
#
# tensor([[0.2700, 0.3807],
# [0.5270, 0.4182],
# [0.3577, 0.4374],
# [0.1954, 0.2551],
# [0.2503, 0.2015]])
# 아래의 결과는 matmul 결과와 같다.
# unsqueeze, saqueeze는 바로 아래에서 알아보자.
a[0].mm(torch.unsqueeze(b, 1)).squeeze()
a[1].mm(torch.unsqueeze(b, 1)).squeeze()
a[2].mm(torch.unsqueeze(b, 1)).squeeze()
a[3].mm(torch.unsqueeze(b, 1)).squeeze()
a[4].mm(torch.unsqueeze(b, 1)).squeeze()
mm 쓰는 것을 권장
한다.view
는 reshape과 동일하게 tensor의 shape을 변환한다.
view와 reshape의 차이
는 contiguity 보장이다.
view는 contiguity를 보장하고, reshape은 contiguity를 보장하지 않는다.
view 쓰는 것을 권장한다.
import tensor
# view vs reshape
a = torch.zeros(3, 2)
b = a.view(2, 3)
a.fill_(1)
print(f"a: {a}")
print(f"b: {b}")
# 결과
#
# a: tensor([[1., 1.],
# [1., 1.],
# [1., 1.]])
# b: tensor([[1., 1., 1.],
# [1., 1., 1.]])
a = torch.zeros(3, 2)
b = a.t().reshape(6)
a.fill_(1)
print(f"a: {a}")
print(f"b: {b}")
# 결과
#
# a: tensor([[1., 1.],
# [1., 1.],
# [1., 1.]])
# b: tensor([0., 0., 0., 0., 0., 0.])
squeeze
는 차원의 개수가 1인 차원을 삭제(압축)하고, unsqueeze
는 차원의 개수가 1인 차원을 추가한다.
squeeze와 unsqueeze는 BERT 모델에서 자주 사용된다.
tensor_ex = torch.rand(size=(2, 1, 2))
print(tensor_ex.shape)
print(tensor_ex)
print(tensor_ex.squeeze())
print(tensor_ex.squeeze().shape)
# 결과
#
# torch.Size([2, 1, 2])
# tensor([[[0.0248, 0.1450]],
#
# [[0.1607, 0.0856]]])
#
# torch.Size([2, 2])
# tensor([[0.0248, 0.1450],
# [0.1607, 0.0856]])
tensor_ex = torch.rand(size=(2, 2))
print(tensor_ex.unsqueeze(0).shape)
# 결과
#
# torch.Size([1, 2, 2])
print(tensor_ex.unsqueeze(1).shape)
# 결과
#
# torch.Size([2, 1, 2])
print(tensor_ex.unsqueeze(2).shape)
# 결과
#
# torch.Size([2, 2, 1])
nn.functional 모듈
을 통해 다양한 수식 변환을 사용할 수 있다.
필요한 것은 찾아서 사용하자.
import torch
import torch.nn.functional as F
tensor = torch.FloatTensor([0.5, 0.7, 0.1])
h_tensor = F.softmax(tensor, dim=0)
h_tensor
# 결과
#
# tensor([0.3458, 0.4224, 0.2318])
import itertools
a = [1, 2, 3]
b = [4, 5]
print(list(itertools.product(a, b)))
# 결과
# [(1, 4), (1, 5), (2, 4), (2, 5), (3, 4), (3, 5)]
tensor_a = torch.tensor(a)
tensor_b = torch.tensor(b)
torch.cartesian_prod(tensor_a, tensor_b)
# 결과
#
# tensor([[1, 4],
# [1, 5],
# [2, 4],
# [2, 5],
# [3, 4],
# [3, 5]])
PyTorch의 핵심
은 자동 미분의 지원이다.
requires_grad = True
로 설정해야 한다.
'requires_grad = True'는 autograd에 모든 연산(operation)들을 추적해야 한다고 알려준다.
w = torch.tensor(2.0, requires_grad=True)
y = w**2
z = 10*y + 25
z.backward() # 자동 미분 수행
print(w.grad)
# 결과
#
# tensor(40.)
벡터 편미분
requires_grad=True
를 갖는 2개의 tensor 를 만든다.로부터 새로운 텐서 를 만든다.
가 모두 신경망(NN)의 매개변수이고, 가 오차(error)라고 가정한다.
신경망을 학습할 때, 아래와 같이 매개변수들에 대한 오차의 변화도(gradient)를 구해야 한다.
에 대해서 .backward() 를 호출할 때, autograd는 이러한 변화도들을 계산하고 이를 각 텐서()의 .grad 속성(attribute)에 저장한다.
는 벡터(vector)이므로
Q.backward()
에 gradient 인자(argument)를 명시적으로 전달해야 한다.gradient 는 와 같은 모양(shape)의 텐서로, 자기 자신에 대한 변화도(gradient)를 나타낸다. 즉, 이다.
이제 변화도는
a.grad
와b.grad
에 저장된다.
a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)
Q = 3*a**3 - b**2
# a벡터 2개의 원소에 대해 미분을 해야하므로
# external_grad를 다음과 같이 잡아줬다.
external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)
print(a.grad)
# 결과
#
# tensor([36., 81.])
print(b.grad)
# 결과
#
# tensor([-12., -8.])