PyTorch에서 가장 기초적인 텐서를 생성하고, 조작하는 방법에 대해서 다루어 보겠습니다.
참고로 텐서의 개념은 수학적, 물리적으로 따로 볼 수 있으나, PyTorch에서의 텐서는 스칼라, 벡터, 행렬 등을 수학적으로 나타내는 구조로 생각하시면 됩니다. 참고로 랭크 0(다른 말로는 0차원)는 스칼라, 랭크 1는 벡터, 랭크 2는 행렬이라고 볼 수 있습니다.
그러면 이제 코드를 보면서 직접 알아보도록 하겠습니다. PyTorch를 사용하기 위해서는 먼저 torch를 불러와야 합니다. numpy와 연관된 작업을 수행할 것이기 때문에 이도 같이 불러오도록 합니다.
import torch
import numpy as np
먼저 텐서를 만드는 방법에 대해 알아보겠습니다. 이 텐서는 리스트나 numpy 배열 등로부터 만들 수 있습니다. 리스트, numpy 배열, 텐서를 만들고 출력해보겠습니다.
x_list = [[1., 2.],[3., 4.]]
x_numpy = np.array(x_list)
x = torch.tensor(x_list)
print("list \n", x_list)
print("numpy \n", x_numpy)
print("tensor \n", x)
list
[[1.0, 2.0], [3.0, 4.0]]
numpy
[[1. 2.]
[3. 4.]]
tensor
tensor([[1., 2.],
[3., 4.]])
이 코드의 출력 결과 리스트, numpy 배열, 텐서가 표현하고자 하는 값들은 같으나, type가 다른 것을 확인할 수 있습니다.
numpy와 텐서 사이의 변환은 전처리나 후처리 과정에서 자주 수행되어야 하기 때문에 이 둘 사이의 변환이 필요합니다. 이를 수행하기 위한 numpy 배열과 텐서 사이의 변환 연산은 다음과 같습니다.
x_tensor_from_numpy = torch.from_numpy(x_numpy)
x_numpy_from_tensor = x.numpy()
print("tensor_from_numpy \n", x_tensor_from_numpy)
print("numpy_from_tensor \n", x_numpy_from_tensor)
tensor_from_numpy
tensor([[1., 2.],
[3., 4.]], dtype=torch.float64)
numpy_from_tensor
[[1. 2.]
[3. 4.]]
이제는 텐서를 원하는 대로 생성하는 방법에 대해 알아보겠습니다. 보통 텐서를 생성할 때 많이 사용하는 형식은 세 가지가 있습니다. 이는 0 또는 1로 채워진 텐서, 그리고 랜덤한 값으로 채워진 텐서입니다. 해당되는 값들을 특정 shape에 따라 생성해보겠습니다.
x23_zeros = torch.zeros(2, 3)
x23_ones = torch.ones(2, 3)
x23_rand = torch.rand(2, 3)
print("zeros_2by3 \n", x23_zeros)
print("ones_2by3 \n", x23_ones)
print("rand_2by3 \n", x23_rand)
zeros_2by3
tensor([[0., 0., 0.],
[0., 0., 0.]])
ones_2by3
tensor([[1., 1., 1.],
[1., 1., 1.]])
rand_2by3
tensor([[0.5518, 0.7528, 0.8064],
[0.5173, 0.6691, 0.4061]])
zeros
, ones
, rand
함수들의 파라메터에 원하는 shape를 넣으면 되는데, 여기서는 2 by 3인 2차원 텐서를 생성하였습니다.
이렇게 shape를 지정하는 방법도 존재하지만, 특정 텐서의 shape를 통해 생성할 수도 있는 방법을 사용해 보겠습니다.
x_zeros = torch.zeros_like(x)
x_ones = torch.ones_like(x)
print("zeros_like \n", x_zeros)
print("ones_like \n", x_ones)
zeros_like
tensor([[0., 0.],
[0., 0.]])
ones_like
tensor([[1., 1.],
[1., 1.]])
위 함수들에 like
를 붙이면 파라메터로 주어진 텐서의 shape에 따라 생성됩니다. 위 코드에서 x의 shape는 (2,2)
이기 때문에 다음과 같이 생성되었습니다. 잠시 뒤에 알아볼 shape
를 사용해서, torch.zeros(x.shape)
라는 코드로도 텐서를 생성할 수 있는데, 위의 코드가 보기에 더 편해 보입니다.
다음은 for문에서 사용하는 range
처럼 0부터 인수의 값까지 차례로 값이 저장되는 텐서를 만들어 보겠습니다.
x5 = torch.arange(5)
print("x5 \n", x5)
x5
tensor([0, 1, 2, 3, 4])
arange
함수를 통해 0부터 4(5 미만)까지의 텐서를 생성하였습니다. 물론 range
처럼 start, end, step
순서로 인수를 넣어 생성할 수도 있습니다.
다음으로 이미 생성된 텐서에서 shape를 출력해보겠습니다.
print("x23_shape:", x23_rand.shape)
print("x5_shape:", x5.shape)
x23_shape: torch.Size([2, 3])
x5_shape: torch.Size([5])
텐서에서 shape를 반환할 때에는 Size
라는 객체를 사용함을 알 수 있습니다.
다음은 텐서 간의 연산에 대해 알아보겠습니다. 일반적으로 더하기(+)나 빼기(-)는 같은 텐서의 shape를 가질 때만 수행됨을 직관적으로 알 수 있을 것입니다. 행렬곱과 아다마르 곱(원소간 곱)이 헷갈릴 수 있는데 다음 코드로 알아보겠습니다.
print("matmul \n", x @ x)
print("mul \n", x * x)
matmul
tensor([[ 7., 10.],
[15., 22.]])
mul
tensor([[ 1., 4.],
[ 9., 16.]])
2차원 텐서를 기준으로 두 텐서의 shape가 각각 (a,b)
, (c,d)
일 때 행렬곱의 경우 b==c
를 만족해야 하고, 아마다르 곱은 a==c and b==d
를 만족해야 합니다. 연산자도 각각 @
와 *
를 사용함을 참고하시기 바랍니다.
텐서의 연산이 잘 수행되었거나 평가에 활용하는 등 상태를 확인하기 위해 합과 평균을 사용할 수 있습니다. 텐서의 합과 평균을 출력해보겠습니다.
print("sum:", x.sum())
print("mean: ", x.mean())
sum: tensor(10.)
mean: tensor(2.5000)
다음과 같이 텐서의 합과 평균을 모든 요소에 대해 계산했지만, numpy와 마찬가지로 인수에 axis
를 통해 각 연산을 수행할 축을 지정할 수도 있습니다.
다음은 전처리나 후처리에 사용할 수 꽤 사용할 수 있는 squeeze
와 unsqueeze
함수에 대해 알아보겠습니다. 이들은 텐서의 차원을 줄이거나, 늘릴 수 있는데 다음 코드로 어떻게 작동하는지 알아보겠습니다.
y = torch.rand(2, 1, 3, 2, 1, 5)
y_squeeze = torch.squeeze(y)
y_squeeze_0 = torch.squeeze(y, 1)
y_unsqueeze_0 = torch.unsqueeze(y, 0)
y_unsqueeze_3 = torch.unsqueeze(y, 3)
print("y_squeeze_shape:", y_squeeze.shape)
print("y_squeeze_0_shape:", y_squeeze_0.shape)
print("y_unsqueeze_0_shape:", y_unsqueeze_0.shape)
print("y_unsqueeze_3_shape:", y_unsqueeze_3.shape)
y_squeeze_shape: torch.Size([2, 3, 2, 5])
y_squeeze_0_shape: torch.Size([2, 3, 2, 1, 5])
y_unsqueeze_0_shape: torch.Size([1, 2, 1, 3, 2, 1, 5])
y_unsqueeze_3_shape: torch.Size([2, 1, 3, 1, 2, 1, 5])
이 코드에는 터무니없는(실질적으로는 잘 안쓰일 것 같은) shape를 가진 y
를 활용합니다. squeeze
함수는 크기가 1인 차원을 삭제한 것을 알 수 있는데, 삭제할 차원을 두번째 인수에 명시할 수 있습니다. unsqueeze
함수는 크기가 1인 차원을 삽입하는데, 이 때에는 차원 인덱스를 꼭 명시해 주어야 합니다.
다음은 텐서의 shape를 바꾸는 함수를 알아보겠습니다. 이 작업에 자주 사용되는 두 함수를 사용해보겠습니다.
z = torch.rand(2, 3, 4)
z_6_view = z.view(6, -1)
z_6_reshape = z.reshape(6, -1)
print("z_6_view_shape:", z_6_view.shape)
print("z_6_reshape_shape:", z_6_reshape.shape)
z_6_view_shape: torch.Size([6, 4])
z_6_reshape_shape: torch.Size([6, 4])
두 함수 view
와 reshape
의 차이는 반환된 텐서와 원래의 텐서 사이의 데이터 공유 유무입니다. 특정 상황 외에는 크게 중요하지 않기 때문에 크게 상관하지 않고 사용하셔도 됩니다.
마지막으로 차원의 순서를 바꾸어 주는 함수를 알아보겠습니다. 이 함수들은 보통 연산을 수행하기 전에 차원을 맞춰줘야 하는 경우에 사용될 수 있습니다. 두 함수들을 사용해 차원의 순서를 바꾸어보겠습니다.
z_transpose = z.transpose(0, 2)
z_permute = z.permute(1, 2, 0)
print("z_transpose_shape:", z_transpose.shape)
print("z_permute_shape:", z_permute.shape)
z_transpose_shape: torch.Size([4, 3, 2])
z_permute_shape: torch.Size([3, 4, 2])
transpose
함수는 주어지는 두 인수의 차원을 바꿉니다. 그래서 인수의 개수도 2개로 고정됩니다. 그러나 permute
함수는 차원의 전체 순서를 바꾸는 기능을 수행합니다. 그래서 인수의 개수도 차원의 랭크와 같아야 합니다.