실습: 텐서 자르기 & 붙이기
이번에는 텐서를 원하는 크기로 자르거나 붙이는 방법을 소개하겠습니다. 앞서 텐서의 요소 갯수는 유지하는 채로 형태를 변화하는 방법에 대해서 다루었는데, 이번에는 하나의 텐서를 둘 이상으로 자르거나, 둘 이상의 텐서를 하나로 합치는 방법에 대해 배우겠습니다.
인덱싱과 슬라이싱
이번에도 $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 함수를 통해 하나의 텐서로 만드는 작업입니다.