Link

실습: 텐서 자르기 & 붙이기

이번에는 텐서를 원하는 크기로 자르거나 붙이는 방법을 소개하겠습니다. 앞서 텐서의 요소 갯수는 유지하는 채로 형태를 변화하는 방법에 대해서 다루었는데, 이번에는 하나의 텐서를 둘 이상으로 자르거나, 둘 이상의 텐서를 하나로 합치는 방법에 대해 배우겠습니다.

인덱싱과 슬라이싱

이번에도 $3\times2\times2$ 크기의 텐서 x를 선언합니다.

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

이 텐서를 그림으로 나타내면 다음과 같을 것입니다.

그럼 이 텐서의 첫 번째 차원의 0번 인덱스만 잘라내고 싶다면 다음과 같이 코드로 구현할 수 있습니다.

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

여기서 주의할 점은 첫 번째 차원은 잘라내는 과정에서 사라졌다는 점 입니다. 즉, $3\times2\times2$ 크기의 텐서를 잘라내어 $2\times2$ 크기의 행렬을 얻었습니다. 이것을 그림으로 나타내면 다음과 같이 밝은색으로 색칠된 부분만 잘라내서 반환하게 될 것입니다.

여기서도 마찬가지로 음수를 넣으면 뒤에서부터 접근하는 것도 가능합니다.

>>> print(x[-1])
tensor([[ 9., 10.],
        [11., 12.]])

이것은 그림에서 다음과 같이 나타내어집니다.

그리고 첫 번째 차원이 아닌, 중간 차원에 대해서 비슷한 작업을 수행하고 싶을 경우에는 콜론(:) 기호를 사용하면 됩니다. 콜론을 사용하면 해당 차원에서는 모든 값을 가져오라는 의미가 됩니다.

>>> print(x[:, 0])
tensor([[ 1.,  2.],
        [ 5.,  6.],
        [ 9., 10.]])

이 작업을 그림으로 나타내면 다음과 같습니다. 두 번째 차원은 가로축으로 표현하기 때문에, 텐서에서 왼쪽에 있는 요소들이 색칠된 것을 볼 수 있습니다.

앞서는 인덱스를 통해 텐서의 특정 부분에 접근했다면, 범위를 지정하여 텐서의 부분 값을 가져올 수 있습니다. 다음의 코드는 첫 번째 차원에서는 인덱스 1 이상부터 2 이전까지의 부분을, 두 번째 차원에서는 1 이상부터의 부분을, 마지막 차원에서는 전부를 가져왔을 때, 크기를 반환하는 코드입니다.

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

이것을 그림으로 나타내면 다음의 부분을 잘라냈을 것입니다.

여기서 주의할 점은 범위를 통해 텐서의 부분을 얻어낼 경우, 차원의 갯수가 줄어들지 않는다는 점 입니다.

Split 함수

split 함수는 텐서를 특정 차원에 대해서 원하는 크기로 잘라줍니다. 다음의 코드는 split 함수를 통해 첫 번째 차원의 크기가 4가 되도록 텐서를 등분하도록 구현한 후, 등분된 텐서들의 각각의 크기를 출력하는 코드입니다.

x = torch.FloatTensor(10, 4)

splits = x.split(4, dim=0)

for s in splits:
    print(s.size())

이 코드의 출력 결과는 다음과 같습니다.

torch.Size([4, 4])
torch.Size([4, 4])
torch.Size([2, 4])

주목할 점은 원래 주어진 텐서의 첫 번째 차원의 크기가 10이었기 때문에, 크기 4로 등분할 경우 마지막에 크기 2의 텐서가 남게 된다는 점 입니다. 따라서 마지막 텐서의 크기는 다른 텐서들과 달리 $2\times4$ 의 크기가 된 것을 볼 수 있습니다.

Chunk 함수

앞서 split 함수는 갯수에 상관 없이 원하는 크기로 나누었다면, 이번에는 크기에 상관 없이 원하는 갯수로 나누는 chunk 함수를 다뤄보겠습니다. 다음 코드와 같이 임의의 값으로 채워진 $8\times4$ 텐서를 만들어봅니다.

x = torch.FloatTensor(8, 4)

그럼 여기서 첫 번째 차원의 크기 8을 최대한 같은 크기로 3개로 나누고자 합니다. 그럼 $3+3+2$ 를 생각해볼 수 있을 것입니다. 실제로 chunk 함수를 실행한 결과를 살펴보겠습니다.

chunks = x.chunk(3, dim=0)

for c in chunks:
    print(c.size())

다음은 앞선 코드의 실행 결과입니다.

torch.Size([3, 4])
torch.Size([3, 4])
torch.Size([2, 4])

Index Select 함수

이번에는 index_select 함수를 이야기해보겠습니다. 이 함수는 특정 차원에서 원하는 인덱스의 값들만 취해오는 함수입니다. 다음 코드와 같이 $3\times2\times2$ 크기의 텐서를 만들어보겠습니다.

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

여기서 indice 텐서의 값을 활용하여 index_select 함수를 수행하면 다음과 같이 진행 될 것입니다.

>>> y = x.index_select(dim=0, index=indice)
>>> print(y)
tensor([[[5., 5.],
         [6., 6.]],

        [[3., 3.],
         [4., 4.]]])

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

이 과정을 그림으로 나타내면 다음과 같습니다.

왼쪽 그림에서 맨 아래 $2\times2$ 행렬이 오른쪽 그림에서 맨 위가 된 것에 주목하세요.

Concatenate 함수

이전까지 주로 하나의 텐서에서 원하는 부분을 잘라내는 방법에 대해서 이야기하였다면, 이제는 여러 텐서를 합쳐서 하나의 텐서로 만드는 방법에 대해서 살펴보겠습니다. cat 함수의 이름은 Concatenate 를 줄여부르는 이름입니다. 배열(리스트list) 내의 두 개 이상의 텐서를 순서대로 합쳐서 하나의 텐서로 반환합니다. 물론 합쳐지기 위해서는 다른 차원들의 크기가 같아야 할 것입니다. 자세한 내용은 다음 예제를 통해 확인하겠습니다.

>>> x = torch.FloatTensor([[1, 2, 3],
...                        [4, 5, 6],
...                        [7, 8, 9]])
>>> y = torch.FloatTensor([[10, 11, 12],
...                        [13, 14, 15],
...                        [16, 17, 18]])

>>> print(x.size(), y.size())
torch.Size([3, 3]) torch.Size([3, 3])

앞서와 같이 두 개의 $3\times3$ 텐서 x, y 가 있을 때, 두 텐서를 원하는 차원으로 이어붙여보도록 하겠습니다. 다음의 코드는 첫 번째 차원으로 이어붙이는 코드입니다.

>>> z = torch.cat([x, y], dim=0)
>>> print(z)
tensor([[ 1.,  2.,  3.],
        [ 4.,  5.,  6.],
        [ 7.,  8.,  9.],
        [10., 11., 12.],
        [13., 14., 15.],
        [16., 17., 18.]])

>>> print(z.size())
torch.Size([6, 3])

이 과정을 그림으로 나타내면 다음과 같습니다. 첫 번째 차원(dim=0)은 텐서의 세로 축을 의미하므로, 세로로 두 텐서를 이어붙이게 됩니다.

다음의 코드는 마지막 차원(dim=-1)으로 텐서를 이어붙이는 코드입니다.

>>> z = torch.cat([x, y], dim=-1)
>>> print(z)
tensor([[ 1.,  2.,  3., 10., 11., 12.],
        [ 4.,  5.,  6., 13., 14., 15.],
        [ 7.,  8.,  9., 16., 17., 18.]])

>>> print(z.size())
torch.Size([3, 6])

이 과정을 그림으로 나타내면 다음과 같습니다. 마지막(두 번째) 차원은 가로축을 의미하므로, 두 텐서를 가로로 이어붙이게 됩니다.

여기서 이어붙이고자 하는 차원 이외의 차원의 크기가 맞지 않으면 다음 그림과 같이 cat 함수를 수행할 수 없습니다. 다음 그림에서는 두 번째 차원에 대해서 이어붙이기 작업을 수행하려고 하는데요. 그럼 나머지 차원인 첫 번째 차원의 크기가 같아야 합니다. 하지만 텐서 x의 첫 번째 차원의 크기는 3이고, 텐서 y의 첫 번째 차원의 크기는 2이기 때문에, 두 크기가 다르므로 cat 함수를 실행할 수 없습니다.

Stack 함수

이번에는 cat 함수와 비슷한 역할을 수행하는 stack 함수에 대해서 이야기해보려 합니다. stack 함수가 다른 점은, 함수의 이름에서 알 수 있듯이, 이어붙이기 작업을 수행하는 것이 아니라, 쌓기 작업을 수행한다는 것입니다. 코드와 그림을 통해 이해해보도록 하겠습니다. 앞서 선언한 x와 y를 활용하여 그대로 stack을 수행해보도록 하지요.

>>> z = torch.stack([x, y])
>>> print(z)
tensor([[[ 1.,  2.,  3.],
         [ 4.,  5.,  6.],
         [ 7.,  8.,  9.]],

        [[10., 11., 12.],
         [13., 14., 15.],
         [16., 17., 18.]]])

>>> print(z.size())
torch.Size([2, 3, 3])

텐서 z의 크기를 출력하여 볼 수 있듯이, 맨 앞에 새로운 차원이 생겨서 배열 내의 텐서 갯수만큼의 크기가 된 것을 볼 수 있습니다. 즉, 새로운 차원을 만든 뒤, 이어붙이기(cat 함수)를 수행한 것과 같습니다. 이것을 그림으로 나타내면 다음과 같습니다.

다음의 코드와 같이 새롭게 생겨날 차원의 인덱스를 직접 지정해줄 수도 있습니다.

>>> z = torch.stack([x, y], dim=-1)
>>> print(z)
tensor([[[ 1., 10.],
         [ 2., 11.],
         [ 3., 12.]],

        [[ 4., 13.],
         [ 5., 14.],
         [ 6., 15.]],

        [[ 7., 16.],
         [ 8., 17.],
         [ 9., 18.]]])

>>> print(z.size())
torch.Size([3, 3, 2])

아까 stack 함수는 새로운 차원을 만든 뒤, cat 함수를 수행한 것과 같다고 하였습니다. 그럼 unsqueeze 함수와 cat 함수를 써서 똑같이 구현해볼까요?

>>> d = 0
>>> # z = torch.stack([x, y], dim=d)
>>> z = torch.cat([x.unsqueeze(d), y.unsqueeze(d)], dim=d)
>>> print(z)
tensor([[[ 1.,  2.,  3.],
         [ 4.,  5.,  6.],
         [ 7.,  8.,  9.]],

        [[10., 11., 12.],
         [13., 14., 15.],
         [16., 17., 18.]]])

>>> print(z.size())
torch.Size([2, 3, 3])

유용한 팁

cat 함수나 stack 함수는 실전에서 매우 유용하게 활용될 때가 많습니다. 특히, 여러 이터레이션iteration을 돌며 반복되는 작업을 수행한 후, 반복 작업의 결과물을 하나로 합치는데 사용됩니다. 이 경우에는 주로 다음의 코드와 같은 형태를 띄게 됩니다.

result = []
for i in range(5):
    x = torch.FloatTensor(2, 2)
    result += [x]

result = torch.stack(result)

result라는 빈 배열(리스트)을 만든 후, 결과물(텐서 x)을 result에 차례대로 추가(append)한 후, stack 또는 cat 함수를 통해 하나의 텐서로 만드는 작업입니다.