# PyTorch?
PyTorch(파이토치)는 파이썬 기반의 딥러닝 라이브러리입니다. 현재 페이스북의 AI Research lab(Facebook's AI Research lab, FAIR)에 의해 주도적으로 개발되고 있습니다.
최근 PyTorch의 인기는 구글의 Tensorflow에 비해 급격히 많아지고 있습니다. 많은 논문과 모델들이 PyTorch를 사용하고 있습니다. HuggingFace의 모델들의 대부분이 PyTorch를 사용하고, TOP30 모델들은 모두 PyTorch를 사용했다고 합니다.
# PyTorch 설치
conda install pytorch torchvision -c pytorch
pip3 install torch torchvision
PyTorch
An open source machine learning framework that accelerates the path from research prototyping to production deployment.
pytorch.org
파이토치 홈페이지에서 os와 패키지, 버전에 따라 설치할 수 있는 command를 알려주니, 들어가보셔서 복붙만 해도 쉽게 설치할 수 있을 것 같습니다.
# tensor 만들기
tensor란 "삼차원 공간에서 아홉 개의 성분을 가지며, 좌표 변환에 의하여 좌표 성분의 곱과 같은 형의 변환을 하는 양"이라고 국어사전에서 정의하고 있습니다. 하지만 우리는 물리를 배우는 것이 아니기 때문에 간단하게 그냥 '행렬(matrix)'과 비슷한 것이라고만 생각해도 전혀 무리가 없을 듯 합니다.
tensor를 만들어봅시다.
- numpy array와 리스트로 텐서 만들기 (torch.from_numpy(), torch.tensor())
x = np.random.rand(2, 3)
y = torch.tensor(x) # cf. y = torch.from_numpy(x)
print(y.ndim, y.dim()) # 2, 2
print(y.nelement()) # 6
print(y.shape, y.size()) # torch.Size([2, 3])
print(y.dtype) # torch.float64
print(y.type()) # torch.DoubleTensor
x = [[1, 2, 3], [4, 5, 6]]
y = torch.tensor(x)
print(y.ndim, y.dim()) # 2, 2
print(y.nelement()) # 6
print(y.shape) # torch.Size([2, 3])
print(y.dtype) # torch.int64
print(y.type()) # torch.LongTensor
x = [[1., 2., 3.], [4., 5., 6.]]
y = torch.tensor(x)
print(y.ndim, y.dim()) # 2, 2
print(y.nelement()) # 6
print(y.shape) # torch.Size([2, 3])
print(y.dtype) # torch.float32
print(y.type()) # torch.FloatTensor
리스트와 넘파이 배열로 텐서를 만드는 건 거의 똑같지만, 텐서의 자료형이 조금 달라집니다. 넘파이 배열은 기본적으로 float64로 데이터가 만들어지기 때문에 tensor 타입이 DoubleTensor가 됩니다. 리스트는 int자료형에 대해서는 LongTensor가, float자료형에 대해서는 FloatTensor가 만들어지는 것을 확인할 수 있습니다.
- initializer로 텐서 만들기
p = torch.rand(3, 2)
q = torch.zeros_like(p) # tensor([[0., 0.], [0., 0.], [0., 0.]])
print(p.dtype) # torch.float32
print(q.shape) # torch.Size([3, 2])
q = torch.ones_like(p) # tensor([[1., 1.], [1., 1.], [1., 1.]])
q = torch.eye(3) # tensor([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])
q = torch.empty(3) # tensor([0.0000e+00, -0.0000e+00, 5.8377e-26])
다양한 initializer들이 있습니다.
이때, _(언더바)를 포함하여 메서드를 입력하면(zeros_(), eye_like_()) in-place 메서드로 적용되어 파라미터로 들어간 텐서값 자체를 바꿔줍니다.
+)
- as_tensor(): tensor로 보는 view를 생성해주는 함수입니다.
z = torch.as_tensor(x) # Or torch.from_numpy(x) cf.np.asarray()
x[-1, -1] = 86
print(z[-1]) # tensor([10, 18, 86])
- 텐서 원소 접근하기
print(y[:, 1]) # tensor([29, 18])
print(y[0, 0]) # tensor(3)
print(y[0, 0].item()) # 3
텐서 element에 접근하는 건, 넘파이나 리스트 자료형에서의 인덱싱과 거의 다를 것이 없으니 간단하게만 보고 넘어가도 좋을 듯 합니다.
# reshaping tensor
tensor를 reshaping 해보겠습니다. 별겻도 아닌데 엄청 헷갈립니다. 저는 이게 왜 필요한지 궁금합니다. 실제로 reshaping하는 것이 많이 쓰일까요?
- view()와 reshape()을 이용해서 reshape
x = np.array([[[29, 3], [18, 10]], [[27, 10], [12, 5]]])
y = torch.tensor(x)
print(y.dim) # 3
print(y.shape) # torch.Size([2, 2, 2])
p = y.view(-1)
print(p.shape, p) # torch.Size([8]) tensor([29, 3, 18, 10, 27, 10, 12, 5])
q = y.view(1, -1) # The size -1 is inferred from other dimensions
print(q.shape, q) # torch.Size([1, 8]) tensor([[29, 3, 18, 10, 27, 10, 12, 5]])
r = y.view(2, -1)
print(r.shape, r) # torch.Size([2, 4]) tensor([[29, 3, 18, 10], [27, 10, 12, 5]])
s = y.reshape(2, -1, 1)
print(s.shape, s) # torch.Size([2, 4, 1]) tensor([[[29], [3], [18], [10]], [[27], [10], [12], [5]]])
view의 인자로 넘겨준 -1은 다른 차원의 값에 따라 알아서 채워지는 값입니다. view는 원소의 개수는 유지한채 텐서의 모양을 바꿔주기때문입니다.
예를 들면, 위의 코드에서 y의 shape이 (2, 2, 2)였는데 y.view(1, -1)을 하면 -1에는 자동으로 8이 계산되어 들어가고 1개의 채널(즉, 2차원)에 1*8짜리 배열처럼 reshape이 되겠죠?
reshape() 또한 마찬가지로 view에서의 reshape 기능을 지원합니다.
- squeeze()와 unsqueeze()로 차원 축소하고 확대하기
ss = s.squeeze(2) # cf. s.squeeze(0) and s.squeeze(1) have no effect
print(ss.shape, ss) # torch.Size([2, 4]) tensor([[29, 3, 18, 10], [27, 10, 12, 5]])
u0 = ss.unsqueeze(0)
print(u0.size, u0) # torch.Size([1, 2, 4]) tensor([[[29, 3, 18, 10], [27, 10, 12, 5]]])
u1 = ss.unsqueeze(1)
print(u1.size, u1) # torch.Size([2, 1, 4]) tensor([[[29, 3, 18, 10]], [[27, 10, 12, 5]]])
u2 = ss.unsqueeze(2)
print(u2.size, u2) # torch.Size([2, 4, 1]) tensor([[[29], [3], [18], [10]], [[27], [10], [12], [5]]])
squeeze(2)는 (channel, row, column)에서 2번째 인덱스인 column을 날린다고 생각하면 됩니다. 따라서 ss의 shape은 (2, 4)가 될 것입니다.
unsqueeze는 인자로 들어온 값의 인덱스 자리에 차원을 하나 더 끼워넣어 줍니다. 예를 들어, unsqueeze(1)은 shpae이 (2, 4)인 텐서에 index 1자리에 차원을 하나 더 끼워서 shape을 (2, 1, 4)로 만들어주는 것입니다.
- transpose()로 행/열 인덱스 바꾸기
t_021 = y.transpose(1, 2)
print(t_021.shape, t_021, t_021.is_contiguous()) # torch.Size([2, 2, 2]) tensor([[[29, 18], [3, 10]], [[27, 12], [10, 5]]]) False
c_021 = t_021.contiguous()
print(c_021, c_021.is_contiguous()) # tensor([[[29, 18], [3, 10]], [[27, 12], [10, 5]]]) True
t_012 = y.transpose(0, 1)
print(t_012.shape, t_012, t_012.is_contiguous()) # torch.Size([2, 2, 2]) tensor([[[29, 3], [27, 10]], [[18, 10], [12, 5]]]) False
transpose는 말그대로 전치시켜서 행과 열을 바꿔주는 함수입니다. 중요한건, transpose를 하면 copy해서 tensor를 새로 만드는 것이 아니고 내부 index의 배열만 바뀝니다. 따라서 is_contiguous함수로 연결되어있는지 확인을 하면 False 값이 나오는 것입니다.
transpose() 후, contigious() 메소드까지 적용시켜주면 복사해서 tensor를 새로 만들기 때문에 연결되어있는(contiguous) 텐서가 생성되는 것입니다.
# line fitting from two points
파이토치로 line fitting을 해보도록 하겠습니다. (1, 4)와 (4, 1)을 지나가는 직선을 구해보도록 할게요.
\begin{pmatrix}
A+b=4 \\
4A+b=2 \\
\end{pmatrix}
import numpy as np
import torch
# Find a line which passes two points, (1, 4) and (4, 2)
A = torch.tensor([[1., 1.], [4., 1.]])
b = torch.tensor([[4.], [2.]])
A_inv = A.inverse()
print(A_inv.mm(b)) # tensor([[-0.6667], [4.6667]])
# cf. Line fitting with NumPy
A = np.array([[1., 1.], [4., 1.]])
b = np.array([[4.], [2.]])
A_inv = np.linalg.inv(A)
print(np.matmul(A_inv, b)) # [[-0.66666667], [4.66666667]]
넘파이랑 거어의 똑같습니다. 교수님께서 왜 파이토치가 넘파이의 extension이라고 하셨는지 알 수 있는 부분입니다.
A의 역행렬을 구해주고, 그 역행렬과 b를 곱해줍니다. 끝- 너무 간단하죠?
# Automatic differentiation (Autograd)
그다음은 자동 미분계산입니다. automatic differentiation을 지원해주는 것이 pytorch와 numpy의 차이점이라고 할 수 있습니다.(numpy는 automatic differentiation을 지원하지 않지만 pytorch는 지원해줍니다.)
딥러닝을 위해서도 중요한 부분이니 자세히 살펴보도록 하겠습니다.
$y = 0.1*x**3 - 0.8*x**2 - 1.5*x + 5.4$에서 x가 2일 때의 미분값을 계산해보도록 하겠습니다.
import torch
x = torch.tensor([2.], requires_grad=True)
y = 0.1*x**3 - 0.8*x**2 - 1.5*x + 5.4
y.backward()
print(x.grad) # tensor([-3.5000])
코드는 너무너무너무 간단합니다. gradient를 계산할 수 있도록 tensor를 만들때 requires_grad 옵션을 true로 지정해주고, backward()함수를 사용하면 자동으로 미분값이 계산됩니다.
requires_grad=True로 설정한다는 것은 tensor를 계산할 때 gradient 계산이 필요하다는 것을 의미합니다.
y = 0.1*x**3 - 0.8*x**2 - 1.5*x + 5.4에서 foward propagation이 계산되고 backward()에서 backward propagation이 계산됩니다.
chain rule이 적용되는 예시를 살펴볼까요?
$y(x)=x^3, z(y)=log{y}$에서 x값이 5일 때의 $\frac{\partial{z}}{\partial{x}}$의 값을 구해보겠습니다.
(참고: https://teamdable.github.io/techblog/PyTorch-Autograd )
import torch
def get_tensor_info(tensor):
info = []
for name in ['requires_grad', 'is_leaf', 'retains_grad', 'grad']:
info.append(f'{name}({getattr(tensor, name, None)})')
info.append(f'tensor({str(tensor)})')
return ' '.join(info)
x = torch.tensor(5., requires_grad=True)
y = x ** 3
z = torch.log(y)
print('Before `backward()`')
print('* x: ', get_tensor_info(x))
print('* y: ', get_tensor_info(y))
print('* z: ', get_tensor_info(z))
y.retain_grad()
z.retain_grad()
z.backward()
print('After `backward()`')
print('* x: ', get_tensor_info(x))
print('* y: ', get_tensor_info(y))
print('* z: ', get_tensor_info(z))
# 출력 결과
# Before `backward()`
# * x: requires_grad(True) is_leaf(True) retains_grad(False) grad(None) tensor(tensor(5., requires_grad=True))
# * y: requires_grad(True) is_leaf(False) retains_grad(False) grad(None) tensor(tensor(125., grad_fn=<PowBackward0>))
# * z: requires_grad(True) is_leaf(False) retains_grad(False) grad(None) tensor(tensor(4.8283, grad_fn=<LogBackward0>))
# After `backward()`
# * x: requires_grad(True) is_leaf(True) retains_grad(False) grad(0.6000000238418579) tensor(tensor(5., requires_grad=True))
# * y: requires_grad(True) is_leaf(False) retains_grad(True) grad(0.00800000037997961) tensor(tensor(125., grad_fn=<PowBackward0>))
# * z: requires_grad(True) is_leaf(False) retains_grad(True) grad(1.0) tensor(tensor(4.8283, grad_fn=<LogBackward0>))
x.requires_grad를 True로 설정했기 때문에 x로부터
# Gradient descent by torch.optim
위에서 본 것처럼 pytorch에서는 backward()함수를 이용해서 자동으로 미분값을 계산할 수 있기 때문에 간단하게 gradient descent를 구현할 수 있습니다.
import torch
import matplotlib.pyplot as plt
f = lambda x: 0.1*x**3 - 0.8*x**2 - 1.5*x + 5.4
viz_range = torch.FloatTensor([-6, 12])
learn_rate = 0.1
max_iter = 100
min_tol = 1e-6
x_init = 12.
# Prepare visualization
xs = torch.linspace(*viz_range, 100)
plt.plot(xs, f(xs), 'r-', label='f(x)', linewidth=2)
plt.plot(x_init, f(x_init), 'b.', label='Each step', markersize=12)
plt.axis((*viz_range, *f(viz_range)))
plt.legend()
x = torch.tensor(x_init, requires_grad=True)
optimizer = torch.optim.SGD([x], lr=learn_rate)
for i in range(max_iter):
# Run the gradient descent with the optimizer
optimizer.zero_grad() # Reset gradient tracking
y = f(x) # Calculate the function (forward)
y.backward() # Calculate the gradient (backward)
xp = x.clone().detach() # cf. xp = x
optimizer.step() # Update 'x'
# Update visualization for each iteration
print(f'Iter: {i}, x = {xp:.3f} to {x:.3f}, f(x) = {f(xp):.3f} to {f(x):.3f} (f\'(x) = {x.grad:.3f})')
lcolor = torch.rand(3).tolist()
approx = x.grad*(xs-xp) + f(xp)
plt.plot(xs, approx, '-', linewidth=1, color=lcolor, alpha=0.5)
xc = x.clone().detach() # Copy 'x' for plotting Or use x.detach().numpy()
plt.plot(xc, f(xc), '.', color=lcolor, markersize=12)
# Check the terminal condition
if abs(x - xp) < min_tol:
break
plt.show()
optimizer는 SGD(Stochastic Gradient Descent)를 사용해보겠습니다. learning rate는 0.1, 최대 iteration은 100, x의 초깃값은 12로 설정해주었습니다. 시각화를 위한 코드를 제외하면 gradient descent를 하는 코드는 4줄이면 끝납니다. gradient tracking reset, forward 계산, backward 계산, step, 끝!
출력 결과는 다음과 같습니다.
# Iter: 0, x = 12.000 to 9.750, f(x) = 45.000 to 7.411 (f'(x) = 22.500)
# Iter: 1, x = 9.750 to 8.608, f(x) = 7.411 to -3.006 (f'(x) = 11.419)
# Iter: 2, x = 8.608 to 7.912, f(x) = -3.006 to -7.017 (f'(x) = 6.957)
# Iter: 3, x = 7.912 to 7.450, f(x) = -7.017 to -8.827 (f'(x) = 4.622)
# Iter: 4, x = 7.450 to 7.127, f(x) = -8.827 to -9.725 (f'(x) = 3.231)
# Iter: 5, x = 7.127 to 6.894, f(x) = -9.725 to -10.198 (f'(x) = 2.335)
# ...
# Iter: 45, x = 6.147 to 6.147, f(x) = -10.822 to -10.822 (f'(x) = 0.000)
# Iter: 46, x = 6.147 to 6.147, f(x) = -10.822 to -10.822 (f'(x) = 0.000)
# Iter: 47, x = 6.147 to 6.147, f(x) = -10.822 to -10.822 (f'(x) = 0.000)
# Iter: 48, x = 6.147 to 6.147, f(x) = -10.822 to -10.822 (f'(x) = 0.000)
# Iter: 49, x = 6.147 to 6.147, f(x) = -10.822 to -10.822 (f'(x) = 0.000)
# Iter: 50, x = 6.147 to 6.147, f(x) = -10.822 to -10.822 (f'(x) = 0.000)
잘 찾아가고 있는 것을 확인할 수 있습니다.
'Deep Learning' 카테고리의 다른 글
[NLP] Word Embedding (0) | 2022.05.07 |
---|---|
[NLP] Vectorization - Bag of Word (0) | 2022.05.06 |
댓글