Link

실습: 텐서 형태 변환

이제까지 파이토치 텐서를 선언하고, 크기를 구하고, 기본 연산을 수행하는 방법을 알아보았습니다. 이번에는 텐서의 전체 요소element 갯수는 유지한 채, 모양을 바꾸는 방법을 알아보겠습니다.

View 함수

>>> x = torch.FloatTensor([[[1, 2],
...                         [3, 4]],
...                        [[5, 6],
...                         [7, 8]],
...                        [[9, 10],
...                         [11, 12]]])
>>> print(x.size())
torch.Size([3, 2, 2])

다음의 코드와 같이 앞서 선언된 $3\times2\times2$ 텐서에 대해, view 함수를 통해 형태 변환을 수행합니다. view 함수의 인자로는 원하는 텐서의 크기를 넣어주면 됩니다. 여기서 중요한 점은 텐서의 요소 갯수는 유지되어야 한다는 점 입니다.

>>> print(x.view(12)) # 12 = 3 * 2 * 2
tensor([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12.])

>>> print(x.view(3, 4)) # 3 * 4 = 3 * 2 * 2
tensor([[ 1.,  2.,  3.,  4.],
        [ 5.,  6.,  7.,  8.],
        [ 9., 10., 11., 12.]])

>>> print(x.view(3, 1, 4))
tensor([[[ 1.,  2.,  3.,  4.]],

        [[ 5.,  6.,  7.,  8.]],

        [[ 9., 10., 11., 12.]]])

만약 새로운 크기가 기존 텐서의 요소 갯수와 맞지 않으면 오류가 발생하게 됩니다. 하지만 view 함수에 인자를 넣어줄 때 -1을 활용하면, 일일히 요소 갯수를 맞추기 위해 노력할 필요가 없습니다. -1이 들어간 차원의 크기는 다른 차원의 값들을 곱하고 남은 필요한 값이 자동으로 채워지게 됩니다. 다음 코드를 통해 -1의 활용을 확인하세요. 앞서와 같은 결과를 만드는 코드입니다.

>>> print(x.view(-1))
tensor([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12.])

>>> print(x.view(3, -1))
tensor([[ 1.,  2.,  3.,  4.],
        [ 5.,  6.,  7.,  8.],
        [ 9., 10., 11., 12.]])

>>> print(x.view(-1, 1, 4))
tensor([[[ 1.,  2.,  3.,  4.]],

        [[ 5.,  6.,  7.,  8.]],

        [[ 9., 10., 11., 12.]]])

이때 중요한 점은 view 함수의 결과 텐서의 주소는 바뀌지 않는다는 것입니다. 따라서 다음 코드에서 y의 값을 바꾼다면, x의 값도 함께 바뀌게 됩니다.

>>> y = x.view(3, 4)
>>> x.storage().data_ptr() == y.storage().data_ptr()
True

파이토치 텐서의 형태를 변환하는 함수는 view 이외에도 여러 함수가 있습니다. 사실 view 함수는 메모리에 순차대로 선언된contiguous 텐서에 대해서만 동작합니다. 만약 해당 조건에 만족하지 않는다면 오류를 발생시키게 됩니다.

RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.

만약 이러한 상황에서 텐서 형태 변환을 진행하고 싶다면, contiguous 함수를 호출한 후 view 함수를 호출하면 됩니다. contiguous 함수는 텐서를 새로운 메모리상의 인접한 주소에 인접한 값을 순서대로 할당해주는 함수입니다. 만약 이미 메모리상에 원하는 형태로 존재한다면, 새롭게 할당하지 않고 해당 텐서를 contiguous 함수의 결과값으로 그대로 반환합니다. 또는 오류 메시지에 써져 있는대로 reshape 함수를 활용할 수 있습니다. reshape 함수는 view 함수와 동일하게 동작하되, contiguous 함수와 view 함수를 순차적으로 호출한 것과 같습니다.

>>> print(x.reshape(3, 4))
tensor([[ 1.,  2.,  3.,  4.],
        [ 5.,  6.,  7.,  8.],
        [ 9., 10., 11., 12.]])

다만 reshape 함수는 contiguous 함수도 함께 호출한 것이기 때문에, 결과 텐서의 주소가 이전 텐서와 다를 수 있음에 유의하세요.

Squeeze 함수

이번에는 squeeze 함수에 대해서 다뤄보도록 하겠습니다. 다음과 같이 $1\times2\times2$ 형태의 텐서를 선언하고 출력합니다.

>>> x = torch.FloatTensor([[[1, 2],
...                         [3, 4]]])
>>> print(x.size())
torch.Size([1, 2, 2])

이때 squeeze 함수는 차원의 크기가 1인 차원을 없애주는 역할을 합니다. 다음의 코드를 통해 squeeze의 동작을 확인하세요.

>>> print(x.squeeze())
tensor([[1., 2.],
        [3., 4.]])

>>> print(x.squeeze().size())
torch.Size([2, 2])

텐서 x의 첫 번째 차원의 크기가 1이었기 때문에, squeeze 함수를 통해 텐서 x의 형태는 $2\times2$ 로 바뀌었습니다. 앞서 다른 함수들과 마찬가지로 squeeze의 경우에도 원하는 차원의 인덱스를 지정할 수 있습니다. 만약 해당 차원의 크기가 1이 아닌 경우, 같은 텐서가 반환됩니다.

>>> print(x.squeeze(0).size())
torch.Size([2, 2])

>>> print(x.squeeze(1).size())
torch.Size([1, 2, 2])

Unsqueeze 함수

squeeze의 반대 역할을 수행하는 함수도 존재합니다. 바로 unsqueeze 입니다. unsqueeze는 지정된 차원의 인덱스에 차원의 크기가 1인 차원을 삽입합니다. 다음의 코드를 통해 unsqueeze의 동작을 살펴보도록 합니다.

>>> x = torch.FloatTensor([[1, 2],
...                        [3, 4]])
>>> print(x.size())
torch.Size([2, 2])

다음의 코드는 $2\times2$ 행렬에 unsqueeze를 수행하여 형태를 변환합니다. 다른 함수들과 마찬가지로 차원의 인덱스에 음수(e.g. -1)을 주어 뒤에서부터 접근할 수도 있음을 주목하세요.

>>> print(x.unsqueeze(1).size())
torch.Size([2, 1, 2])

>>> print(x.unsqueeze(-1).size())
torch.Size([2, 2, 1])

>>> print(x.unsqueeze(2).size())
torch.Size([2, 2, 1])

이것은 reshape 함수를 통해서도 똑같이 구현할 수 있습니다.

>>> print(x.reshape(2, 2, -1).size())
torch.Size([2, 2, 1])

이처럼 다양한 함수들을 통해 텐서의 형태를 변환할 수 있습니다. 실제로 딥러닝 연산을 수행하는 과정에서는 텐서의 형태를 변환할 일이 많기 때문에, 텐서를 조작하는 방법에 익숙해지는 것이 좋습니다.