[PyTorch로 시작하는 딥러닝 입문] 2장 파이토치 기초 (2)_텐서 조작하기 II
안녕하세요. 지난 포스트에 이어서 파이토치 기초 2장의 두 번째 내용을 포스팅하려 해요! 텐서를 조작하는 함수 위주로 설명드리겠습니다.
2-3. 텐서 조작하기 2
8. 뷰 (View)
numpy에는 reshape를 이용하여 원소의 수를 유지하면서 텐서의 크기를 변경했습니다. Pytorch에는View를 통해 변경합니다!
import numpy as np
import torch
t1 = np.array([[[0, 1, 2, 3],
[4, 5, 6, 7]],
[[8, 9, 10, 11],
[12, 13, 14, 15]],
[[16, 17, 18, 19],
[20, 21, 22, 23]]])
print('t1')
print(t1)
t2 = torch.FloatTensor(t1)
print('')
print('t2')
print(t2)
--------------------------------------------------------------------------------------------------------------------------------
t1
[[[ 0 1 2 3]
[ 4 5 6 7]]
[[ 8 9 10 11]
[12 13 14 15]]
[[16 17 18 19]
[20 21 22 23]]]
t2
tensor([[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.]],
[[ 8., 9., 10., 11.],
[12., 13., 14., 15.]],
[[16., 17., 18., 19.],
[20., 21., 22., 23.]]])
t2의 텐서 크기는 [3, 2, 4]입니다. 그렇다면 View를 이용하여 3차원에서 2차원으로 텐서를 변경해보겠습니다.
print(t2.shape)
--------------------------------------------------------------------------------------------------------------------------------
torch.Size([3, 2, 4])
view에서 -1이 의미하는 것은 차원의 길이가 정해지지 않았다는 것입니다. 아래의 결과를 보면, 텐서 전체 원소의 수는 24입니다. 여기서 [-1, 3]으로 텐서 크기를 변경하려 합니다. 그런데 텐서의 전체 원소 수는 크기가 변경되더라도 변하지 않아야 합니다. 따라서, $24 / 3 = 8$이 첫 번째 차원의 길이가 됩니다. 즉, $[-1, 3] = [8, 3]$이 됩니다!
# 3차원 텐서를 2차원으로 변경
print(t2.view([-1, 3])) # (?, 3)의 크기로 변경
print(t2.view([-1, 3]).shape)
print('')
print(t2.view([-1, 2])) # (?, 2)의 크기로 변경
print(t2.view([-1, 2]).shape)
--------------------------------------------------------------------------------------------------------------------------------
tensor([[ 0., 1., 2.],
[ 3., 4., 5.],
[ 6., 7., 8.],
[ 9., 10., 11.],
[12., 13., 14.],
[15., 16., 17.],
[18., 19., 20.],
[21., 22., 23.]])
torch.Size([8, 3])
tensor([[ 0., 1.],
[ 2., 3.],
[ 4., 5.],
[ 6., 7.],
[ 8., 9.],
[10., 11.],
[12., 13.],
[14., 15.],
[16., 17.],
[18., 19.],
[20., 21.],
[22., 23.]])
torch.Size([12, 2])
이번에는 3차원에서 3차원으로 차원은 그대로 하되, 크기만 변경을 해보겠습니다. 여기서는 [2, -1, 3]으로 크기 변경을 하려고 합니다. 전체 원소 수가 24이므로 -1이 의미하는 차원의 크기는 $24 / (2 \times 3) = 4$가 됩니다. 따라서 [2, 4, 3]으로 변경됩니다!
# 3차원 텐서를 3차원으로 변경
print(t2.view([2, -1, 3]))
print('')
print(t2.view([2, -1, 3]).shape)
--------------------------------------------------------------------------------------------------------------------------------
tensor([[[ 0., 1., 2.],
[ 3., 4., 5.],
[ 6., 7., 8.],
[ 9., 10., 11.]],
[[12., 13., 14.],
[15., 16., 17.],
[18., 19., 20.],
[21., 22., 23.]]])
torch.Size([2, 4, 3])
9. 스퀴즈 (Squeeze)
스퀴즈는 텐서의 차원 중 크기가 1인 차원이 있을 경우 제거해주는 함수입니다. 예를 들어, [4, 1] 크기의 텐서를 정의하겠습니다.
t1 = torch.FloatTensor([[0], [1], [2], [3]])
print(t1)
print('')
print(t1.shape)
--------------------------------------------------------------------------------------------------------------------------------
tensor([[0.],
[1.],
[2.],
[3.]])
torch.Size([4, 1])
스퀴즈는 크기가 1인 차원을 제거해주므로, [4, 1] 크기의 텐서가 [4,] 크기로 변경된 것을 알 수 있습니다.
print(t1.squeeze())
print(t1.squeeze().shape)
--------------------------------------------------------------------------------------------------------------------------------
tensor([0., 1., 2., 3.])
torch.Size([4])
[1, 3] 크기와 [3, 1, 4] 크기의 텐서의 경우도 아래와 같이 1인 차원이 제거되어, 각각 [3,], [3, 4] 크기로 변경됩니다.
t2 = torch.FloatTensor([[3., 4., 5.]])
print(t2.shape)
print(t2.squeeze())
print(t2.squeeze().shape)
--------------------------------------------------------------------------------------------------------------------------------
torch.Size([1, 3])
tensor([3., 4., 5.])
torch.Size([3])
t3 = torch.FloatTensor([[[0, 1, 2, 3]],
[[8, 9, 10, 11]],
[[16, 17, 18, 19]]])
print(t3.shape)
print(t3.squeeze())
print(t3.squeeze().shape)
--------------------------------------------------------------------------------------------------------------------------------
torch.Size([3, 1, 4])
tensor([[ 0., 1., 2., 3.],
[ 8., 9., 10., 11.],
[16., 17., 18., 19.]])
torch.Size([3, 4])
10. 언스퀴즈 (Unsqueeze)
언스퀴즈는 스퀴즈와 정반대로 특정 위치에 크기가 1인 차원을 추가해주는 함수입니다. [4,] 크기의 1차원 텐서를 정의하겠습니다.
t1 = torch.FloatTensor([0., 1., 2., 3.])
print(t1)
print('')
print(t1.shape)
--------------------------------------------------------------------------------------------------------------------------------
tensor([0., 1., 2., 3.])
torch.Size([4])
언스퀴즈의 인자에 어떤 값을 대입시키냐에 따라 텐서 크기가 달라집니다. 인자로 0을 넣었을 경우, 첫 번째 차원이 1이 되어 [4,]에서 [1, 4] 크기로 변경됩니다. 인자로 1을 넣었을 경우, 두 번째 차원이 1이 되어 [4, ]에서 [4, 1] 크기로 변경됩니다. 만약, -1을 넣게 된다면 가장 마지막 차원이 1이 됩니다. 아래의 예에서는 두 번째 차원이 마지막 차원이 되므로, 인자에 -1을 넣은 결과가 인자에 1을 넣은 결과와 동일하게 나옵니다.
print(t1.unsqueeze(0))
print(t1.unsqueeze(0).shape)
print('')
print(t1.unsqueeze(1))
print(t1.unsqueeze(1).shape)
print('')
print(t1.unsqueeze(-1))
print(t1.unsqueeze(-1).shape)
--------------------------------------------------------------------------------------------------------------------------------
tensor([[0., 1., 2., 3.]])
torch.Size([1, 4])
tensor([[0.],
[1.],
[2.],
[3.]])
torch.Size([4, 1])
tensor([[0.],
[1.],
[2.],
[3.]])
torch.Size([4, 1])
이렇게 View, Squeeze, Unsqueeze는 텐서의 전체 원소 수는 그대로 유지하며, 모양과 차원을 변경할 수 있습니다.
11. 타입 캐스팅 (Type Casting)
위와 같이 텐서에는 여러가지 타입이 있습니다. 예를 들어, 32비트의 부동 소수점은 torch.FloatTensor, 64비트의 부호 있는 정수는 torch.LongTensor, 만약 GPU 연산이 필요한 32비트 부동 소수점 텐서가 필요하다면 torch.cuda.FloatTensor가 있습니다. 이러한 자료형들을 변환하는 것을 타입 캐스팅이라고 합니다.
아래와 같이 LongTensor를 만들어 보겠습니다. 뒤에 .float()를 붙이면 float로 타입이 변경됩니다.
t1 = torch.LongTensor([1, 2, 3])
print(t1)
print(t1.float())
--------------------------------------------------------------------------------------------------------------------------------
tensor([1, 2, 3])
tensor([1., 2., 3.])
이번엔 ByteTensor를 이용하여 8비트의 부호 있는 정수 타입을 만들어 봤습니다. 뒤에 .long()을 붙이면 long 타입으로 변경되는 것을 볼 수 있습니다.
t2 = torch.ByteTensor([True, False, False, True, False])
print(t2)
print(t2.long())
print(t2.float())
--------------------------------------------------------------------------------------------------------------------------------
tensor([1, 0, 0, 1, 0], dtype=torch.uint8)
tensor([1, 0, 0, 1, 0])
tensor([1., 0., 0., 1., 0.])
12. 연결하기 (Concatenate)
두 텐서를 연결하는 방법에 대해 알아보겠습니다. 아래와 같이 [2, 3] 크기의 텐서 두 개를 만들었습니다.
t1 = torch.FloatTensor([[1, 2, 3], [4, 5, 6]])
t2 = torch.FloatTensor([[7, 8, 9], [10, 11, 12]])
print(t1)
print('')
print(t2)
print('')
print(t1.shape)
print(t2.shape)
--------------------------------------------------------------------------------------------------------------------------------
tensor([[1., 2., 3.],
[4., 5., 6.]])
tensor([[ 7., 8., 9.],
[10., 11., 12.]])
torch.Size([2, 3])
torch.Size([2, 3])
여기에서 cat을 이용하면 두 텐서를 붙일 수 있습니다. dim 인자가 필요한데 0은 행 차원으로, 1은 열 차원으로 붙이게 됩니다! 만약 행 차원으로 붙인다면, [2, 3] 크기의 텐서 두 개가 붙어 [4, 3] 크기가 됩니다. 여러 텐서들을 한꺼번에 연결하는 것도 가능합니다.
print(torch.cat([t1, t2], dim = 0)) # dim = 0 -> 행 차원을 늘려라
print('')
print(torch.cat([t1, t2], dim = 1)) # dim = 1 -> 열 차원을 늘려라
print('')
print(torch.cat([t1, t2, t1], dim = 0))
--------------------------------------------------------------------------------------------------------------------------------
tensor([[ 1., 2., 3.],
[ 4., 5., 6.],
[ 7., 8., 9.],
[10., 11., 12.]])
tensor([[ 1., 2., 3., 7., 8., 9.],
[ 4., 5., 6., 10., 11., 12.]])
tensor([[ 1., 2., 3.],
[ 4., 5., 6.],
[ 7., 8., 9.],
[10., 11., 12.],
[ 1., 2., 3.],
[ 4., 5., 6.]])
13. 스택킹 (Stacking)
cat 이외에도 텐서를 연결하는 방법으로 스택킹이 있습니다. 스택킹이 cat보다 더 많은 연산을 포함하는 것으로 알려져 있습니다. 예를 들어 [2, ] 크기의 텐서 3개가 아래와 같이 주어져 있습니다. stack을 이용하여 행 차원으로 붙였고, [3, 2] 크기의 텐서가 만들어진 것을 볼 수 있습니다.
t1 = torch.FloatTensor([1, 4])
t2 = torch.FloatTensor([2, 5])
t3 = torch.FloatTensor([3, 6])
print(t1.shape)
print(torch.stack([t1, t2, t3], dim = 0))
print(torch.stack([t1, t2, t3], dim = 0).shape)
--------------------------------------------------------------------------------------------------------------------------------
torch.Size([2])
tensor([[1., 4.],
[2., 5.],
[3., 6.]])
torch.Size([3, 2])
그렇다면 위의 작업에서 함수 cat만 바꿔서 적용한다면 어떻게 될까요? 인자를 동일하게 0으로 줬을 때는, 행이 아닌 열로 붙여지게 되고 1로 줬을 때는 에러가 발생하게 됩니다. 이 부분이 바로 stack과 다른 점이라고 할 수 있습니다. 지금 하고 싶은 것은 [2, ] 크기의 텐서 3개를 행으로 붙여 [3, 2] 크기로 만드는 것입니다. stack 함수는 차원과 관계없이 텐서를 스택킹해주는 연산이 내재되어 있어 1차원인 [2, ]에서 2차원인 [3, 2]로 텐서를 쉽게 바꿔주었지만, cat은 1차원에서 2차원으로 바꿔주는 연산이 따로 존재하지 않습니다. 즉, cat으로 작업을 실행하려면 그 다음의 코드와 같이 작성해야 합니다.
print(torch.cat([t1, t2, t3], dim = 0))
print('')
print(torch.cat([t1, t2, t3], dim = 1))
--------------------------------------------------------------------------------------------------------------------------------
tensor([1., 4., 2., 5., 3., 6.])
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[63], line 3
1 print(torch.cat([t1, t2, t3], dim = 0))
2 print('')
----> 3 print(torch.cat([t1, t2, t3], dim = 1))
IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)
이전 단계에서 배웠었던 언스퀴즈 함수를 통해 [2, ] 크기의 텐서를 [1, 2]로 바꿔준 후에 cat을 사용해야 오류가 나지 않습니다. 즉, cat은 텐서를 연결해주는 함수지만, 단독으로 차원을 변경시킬 수는 없습니다. 이러한 측면에 있어서는 스택킹이 훨씬 편리하게 사용될 수 있음을 보여줍니다.
print(torch.cat([t1.unsqueeze(0), t2.unsqueeze(0), t3.unsqueeze(0)], dim = 0)) # (2,) -> (1, 2) -> (3, 2)로 변경
--------------------------------------------------------------------------------------------------------------------------------
tensor([[1., 4.],
[2., 5.],
[3., 6.]])
14. ones_like, zeros_like
ones_like는 1로 채워진 텐서, zeros_like는 0으로 채워진 텐서입니다. 아래와 같이 만들 수 있습니다.
t1 = torch.FloatTensor([[10, 20, 30], [40, 50, 60]])
print(t1)
print('')
print(torch.ones_like(t1))
print('')
print(torch.zeros_like(t1))
--------------------------------------------------------------------------------------------------------------------------------
tensor([[10., 20., 30.],
[40., 50., 60.]])
tensor([[1., 1., 1.],
[1., 1., 1.]])
tensor([[0., 0., 0.],
[0., 0., 0.]])
15. In-place Operation
In-place Operation은 덮어쓰기 연산을 의미합니다. 개인적으로 이 기능은 정말 편리한 것 같아요! 어떤 것인지 아래와 같이 예시를 들어보겠습니다.
t1 = torch.FloatTensor([[10, 20], [30, 40], [50, 60]])
print(t1)
--------------------------------------------------------------------------------------------------------------------------------
tensor([[10., 20.],
[30., 40.],
[50., 60.]])
지난 포스트에 설명드렸던 아마다르 곱셈 함수인 mul을 이용하여 각 원소에 2씩 곱셈을 하고 싶습니다. 그런데 아래와 같이 프린트를 하면 2씩 곱셈을 한 결과가 저장되지 않고 기존의 값이 그래도 도출되는 것을 알 수 있습니다.
print(t1.mul(2.))
print('')
print(t1)
--------------------------------------------------------------------------------------------------------------------------------
tensor([[ 20., 40.],
[ 60., 80.],
[100., 120.]])
tensor([[10., 20.],
[30., 40.],
[50., 60.]])
따라서 덮어쓰기 연산인 _를 함수 뒤에 적용하면 결과를 바로 저장할 수 있습니다. 추가적으로 변수를 정의하지 않아도 되기 때문에 아주 편리한 기능이라고 생각합니다.
print(t1.mul_(2.))
print('')
print(t1)
--------------------------------------------------------------------------------------------------------------------------------
tensor([[ 20., 40.],
[ 60., 80.],
[100., 120.]])
tensor([[ 20., 40.],
[ 60., 80.],
[100., 120.]])
댓글남기기