연속된 숫자로 Shape 변경과 차원 교환 쉽게 배우기
PyTorch를 공부하다 보면 Tensor의 모양을 바꾸는 view()와 차원의 순서를 바꾸는 transpose()를 자주 만나게 됩니다.
특히 Transformer의 Multi-Head Attention 코드에서는 다음과 같은 형태가 거의 항상 등장합니다.
x = x.view(batch_size, seq_len, num_heads, head_dim)
x = x.transpose(1, 2)
두 함수 모두 Tensor의 Shape를 변경하기 때문에 처음에는 비슷해 보일 수 있습니다.
하지만 두 함수의 역할은 완전히 다릅니다.
view()는 데이터의 순서를 유지하면서 Shape만 변경합니다.transpose()는 두 차원의 위치를 바꿉니다.
이번 글에서는 랜덤 숫자 대신 1부터 시작하는 연속된 숫자를 사용하여 두 함수의 차이를 쉽게 알아보겠습니다.
전체 예제 코드
import torch
# =====================================
# view() 예제
# =====================================
# 1부터 16까지의 숫자로 Tensor 생성
x = torch.arange(1, 17).view(4, 4)
print("x:")
print(x)
print("x size():", x.size())
print("x shape:", x.shape)
# 1차원으로 변경
y = x.view(16)
print("\ny:")
print(y)
print("y size():", y.size())
print("y shape:", y.shape)
# 2행 8열로 변경
z = x.view(2, 8)
print("\nz:")
print(z)
print("z size():", z.size())
print("z shape:", z.shape)
# =====================================
# transpose() 예제
# =====================================
# 1부터 24까지의 숫자를 4차원 Tensor로 변경
a = torch.arange(1, 25).view(1, 2, 3, 4)
print("\na(original):")
print(a)
print("a size():", a.size())
# 두 번째 차원과 세 번째 차원 교환
b = a.transpose(1, 2)
print("\nb(transpose):")
print(b)
print("b size():", b.size())
# transpose 없이 같은 Shape만 만들기
c = a.view(1, 3, 2, 4)
print("\nc(view):")
print(c)
print("c size():", c.size())
# 두 Tensor가 완전히 같은지 확인
print("\nWhether equal:", torch.equal(b, c))
실행결과:


1. 원본 Tensor 만들기
먼저 1부터 16까지의 숫자를 가진 Tensor를 생성합니다.
x = torch.arange(1, 17).view(4, 4)
torch.arange(1, 17)은 다음과 같은 1차원 Tensor를 만듭니다.
tensor([ 1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16])
여기에 view(4, 4)를 적용하면 4행 4열의 Tensor가 됩니다.
tensor([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]])
Shape는 다음과 같습니다.
torch.Size([4, 4])
즉, Tensor에는 총 16개의 원소가 있으며 이를 4행 4열로 보고 있는 것입니다.
2. view(16): 1차원 Tensor로 변경하기
다음 코드는 4×4 Tensor를 다시 1차원 Tensor로 변경합니다.
y = x.view(16)
출력 결과는 다음과 같습니다.
tensor([ 1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16])
Shape는 다음과 같습니다.
torch.Size([16])
여기서 중요한 점은 숫자의 순서가 바뀌지 않았다는 것입니다.
원래 저장된 순서인
1, 2, 3, 4, 5, 6, ..., 16
을 그대로 유지하면서 Tensor의 모양만 1차원으로 변경한 것입니다.
3. view(2, 8): 2행 8열로 변경하기
이번에는 같은 Tensor를 2행 8열로 변경해 보겠습니다.
z = x.view(2, 8)
출력 결과는 다음과 같습니다.
tensor([[ 1, 2, 3, 4, 5, 6, 7, 8],
[ 9, 10, 11, 12, 13, 14, 15, 16]])
Shape는 다음과 같습니다.
torch.Size([2, 8])
이번에도 숫자의 순서는 그대로입니다.
view()는 데이터를 새로운 순서로 섞지 않습니다. 메모리에 저장된 원소를 처음부터 순서대로 읽으면서 새로운 Shape에 맞게 나누어 보여줍니다.
정리하면 다음과 같습니다.
기존 모양
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
이를 메모리 순서대로 펼치면 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
이 값을 8개씩 나누면 다음과 같이 됩니다.
1 2 3 4 5 6 7 8
9 10 11 12 13 14 15 16
즉, view()는 원소의 순서를 유지한 채 모양만 다시 해석하는 함수입니다.
4. view(-1, 8)에서 -1의 의미
PyTorch 코드에서는 다음과 같은 표현도 자주 볼 수 있습니다.
x.view(-1, 8)
여기서 -1은 다음과 같은 의미입니다.
해당 차원의 크기는 전체 원소 개수에 맞게 PyTorch가 자동으로 계산한다.
현재 Tensor의 전체 원소 개수는 16개입니다.
두 번째 차원의 크기를 8로 지정했으므로 첫 번째 차원의 크기는 다음과 같이 계산됩니다.
16 ÷ 8 = 2
따라서 다음 두 코드는 같은 결과를 반환합니다.
x.view(-1, 8)
x.view(2, 8)
결과 Shape는 모두 다음과 같습니다.
torch.Size([2, 8])
단, -1은 한 번에 하나의 차원에만 사용할 수 있습니다.
다음 코드는 사용할 수 없습니다.
x.view(-1, -1)
PyTorch가 두 차원의 크기를 동시에 추론할 수 없기 때문입니다.
5. 4차원 Tensor 이해하기
이번에는 transpose()를 알아보기 위해 4차원 Tensor를 만들어 보겠습니다.
a = torch.arange(1, 25).view(1, 2, 3, 4)
Shape는 다음과 같습니다.
torch.Size([1, 2, 3, 4])
각 차원을 다음과 같이 이해할 수 있습니다.
첫 번째 차원: 1개
두 번째 차원: Block 2개
세 번째 차원: 각 Block에 Row 3개
네 번째 차원: 각 Row에 숫자 4개

이를 구조로 표현하면 다음과 같습니다.
a.shape = (1, 2, 3, 4)
전체 그룹 1개
├─ Block 0
│ ├─ Row 0: 1 2 3 4
│ ├─ Row 1: 5 6 7 8
│ └─ Row 2: 9 10 11 12
│
└─ Block 1
├─ Row 0: 13 14 15 16
├─ Row 1: 17 18 19 20
└─ Row 2: 21 22 23 24
실제 출력 결과는 다음과 같습니다.
tensor([[[[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]],
[[13, 14, 15, 16],
[17, 18, 19, 20],
[21, 22, 23, 24]]]])
6. transpose(1, 2)는 무엇을 할까?
다음 코드를 실행해 보겠습니다.
b = a.transpose(1, 2)
transpose(1, 2)는 1번 차원과 2번 차원의 위치를 교환합니다.
PyTorch의 차원 번호는 0부터 시작합니다.
원래 Shape
차원 번호: 0 1 2 3
Shape: (1, 2, 3, 4)
1번 차원과 2번 차원을 교환하면 다음과 같습니다.
변경 후 Shape
차원 번호: 0 1 2 3
Shape: (1, 3, 2, 4)
즉, 기존의 Block 차원과 Row 차원이 서로 바뀝니다.
변경 전
Block
├─ Row 0
├─ Row 1
└─ Row 2
변경 후
Row
├─ Block 0
└─ Block 1
따라서 같은 위치의 Row가 서로 모이게 됩니다.
7. transpose() 결과를 숫자로 이해하기
원래 Row 0은 각 Block에 다음과 같이 존재합니다.
Block 0의 Row 0: 1 2 3 4
Block 1의 Row 0: 13 14 15 16
두 Row가 하나의 그룹으로 모이면 다음과 같습니다.
[[ 1, 2, 3, 4],
[13, 14, 15, 16]]
원래 Row 1은 다음과 같습니다.
Block 0의 Row 1: 5 6 7 8
Block 1의 Row 1: 17 18 19 20
따라서 두 번째 그룹은 다음과 같습니다.
[[ 5, 6, 7, 8],
[17, 18, 19, 20]]
원래 Row 2는 다음과 같습니다.
Block 0의 Row 2: 9 10 11 12
Block 1의 Row 2: 21 22 23 24
따라서 세 번째 그룹은 다음과 같습니다.
[[ 9, 10, 11, 12],
[21, 22, 23, 24]]
최종 결과는 다음과 같습니다.
tensor([[[[ 1, 2, 3, 4],
[13, 14, 15, 16]],
[[ 5, 6, 7, 8],
[17, 18, 19, 20]],
[[ 9, 10, 11, 12],
[21, 22, 23, 24]]]])
Shape는 다음과 같습니다.
torch.Size([1, 3, 2, 4])
transpose()를 사용하면 단순히 숫자를 순서대로 자르는 것이 아니라, 지정한 두 차원의 역할이 서로 바뀝니다.
8. 같은 Shape를 view()로 만들면 어떻게 될까?
이번에는 transpose()를 사용하지 않고 view()만 사용하여 동일한 Shape를 만들어 보겠습니다.
c = a.view(1, 3, 2, 4)
Shape는 b와 동일합니다.
torch.Size([1, 3, 2, 4])
하지만 출력 결과는 다릅니다.
tensor([[[[ 1, 2, 3, 4],
[ 5, 6, 7, 8]],
[[ 9, 10, 11, 12],
[13, 14, 15, 16]],
[[17, 18, 19, 20],
[21, 22, 23, 24]]]])
왜 이런 결과가 나올까요?
view()는 차원의 의미를 교환하지 않습니다.
메모리에 저장된 숫자를 다음 순서 그대로 읽습니다.
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20
21 22 23 24
그리고 (1, 3, 2, 4)라는 Shape에 맞게 8개씩 나눕니다.
첫 번째 그룹은 다음과 같습니다.
[[1, 2, 3, 4],
[5, 6, 7, 8]]
두 번째 그룹은 다음과 같습니다.
[[ 9, 10, 11, 12],
[13, 14, 15, 16]]
세 번째 그룹은 다음과 같습니다.
[[17, 18, 19, 20],
[21, 22, 23, 24]]
즉, view()는 원래 데이터가 어떤 Block이나 Row에 속했는지 고려하지 않습니다.
단순히 저장된 순서를 유지한 채 새로운 Shape로 나누어 보여줍니다.
9. Shape는 같은데 왜 값은 다를까?
b와 c의 Shape를 비교하면 둘 다 같습니다.
print(b.shape)
print(c.shape)
출력 결과는 다음과 같습니다.
torch.Size([1, 3, 2, 4])
torch.Size([1, 3, 2, 4])
그러나 두 Tensor를 비교하면 결과는 False입니다.
torch.equal(b, c)
False
두 Tensor의 차이를 다시 정리하면 다음과 같습니다.
transpose() 결과
[[ 1, 2, 3, 4],
[13, 14, 15, 16]]
[[ 5, 6, 7, 8],
[17, 18, 19, 20]]
[[ 9, 10, 11, 12],
[21, 22, 23, 24]]
view() 결과
[[ 1, 2, 3, 4],
[ 5, 6, 7, 8]]
[[ 9, 10, 11, 12],
[13, 14, 15, 16]]
[[17, 18, 19, 20],
[21, 22, 23, 24]]
Shape는 같지만 각 위치에 들어 있는 숫자가 다르기 때문에 두 Tensor는 같지 않습니다.
Tensor에서는 Shape가 같다고 해서 반드시 같은 Tensor인 것은 아닙니다.
각 위치의 값까지 모두 같아야 합니다.
10. view()와 transpose()의 핵심 차이
view()
view()는 Tensor의 Shape를 변경합니다.
x.view(2, 8)
특징은 다음과 같습니다.
- 전체 원소 개수는 유지됩니다.
- 원소가 저장된 순서를 유지합니다.
- 메모리에 있는 값을 순서대로 읽어 새로운 Shape로 해석합니다.
- 차원의 의미를 서로 교환하지 않습니다.
쉽게 말하면 다음과 같습니다.
같은 책의 페이지 순서는 유지한 채 책을 묶는 방식만 바꾸는 것
transpose()
transpose()는 지정한 두 차원의 위치를 교환합니다.
a.transpose(1, 2)
특징은 다음과 같습니다.
- 두 차원의 역할이 서로 바뀝니다.
- Tensor를 인덱싱했을 때 보이는 값의 배치가 달라집니다.
- Shape뿐만 아니라 각 값이 논리적으로 위치하는 방식도 달라집니다.
쉽게 말하면 다음과 같습니다.
표에서 행과 열의 역할을 바꾸는 것
11. 정확히 말하면 transpose()는 메모리를 바로 재배치하지 않는다
초보자 관점에서는 transpose()가 데이터를 재배치한다고 이해해도 결과를 파악하는 데 큰 문제는 없습니다.
하지만 내부 동작을 정확히 설명하면 조금 다릅니다.
transpose()는 일반적으로 메모리에 저장된 데이터를 실제로 복사하거나 이동하지 않습니다.
대신 Tensor가 데이터를 읽는 규칙인 Stride를 변경합니다.
즉,
b = a.transpose(1, 2)
를 실행하면 메모리 안의 숫자들이 실제로 새로운 순서로 복사되는 것이 아니라, 같은 메모리를 다른 차원 순서로 읽도록 설정됩니다.
따라서 다음과 같이 이해하는 것이 가장 정확합니다.
transpose()는 실제 저장 순서를 즉시 바꾸기보다는, 차원의 순서와 데이터를 읽는 방식을 변경한다.
사용자 입장에서는 값의 배치가 달라진 것처럼 보이지만, 내부적으로는 같은 데이터를 새로운 규칙으로 바라보고 있는 경우가 많습니다.
12. transpose() 후 view()를 바로 사용하면 오류가 날 수 있다
transpose()를 실행한 Tensor는 메모리가 연속적이지 않은 Non-contiguous Tensor가 될 수 있습니다.
예를 들어 다음 코드는 오류가 발생할 수 있습니다.
b = a.transpose(1, 2)
result = b.view(1, -1)
view()는 기본적으로 메모리가 연속적으로 배치된 Tensor를 대상으로 동작하기 때문입니다.
이럴 때는 contiguous()를 먼저 사용할 수 있습니다.
result = b.contiguous().view(1, -1)
contiguous()는 현재 보이는 데이터 순서에 맞게 새로운 연속 메모리를 만들어 줍니다.
또는 reshape()를 사용할 수도 있습니다.
result = b.reshape(1, -1)
reshape()는 가능한 경우 view()처럼 기존 메모리를 공유하고, 불가능한 경우에는 필요한 데이터를 복사하여 새로운 Tensor를 만듭니다.
따라서 다음과 같이 기억하면 좋습니다.
view()
→ 메모리가 연속적인 경우에 주로 사용
reshape()
→ 연속 여부를 자동으로 처리하여 더 편리하게 사용 가능
transpose()
→ 차원의 순서를 교환하며 Non-contiguous Tensor가 될 수 있음
13. Transformer에서는 왜 두 함수를 함께 사용할까?
Transformer의 Multi-Head Attention에서는 하나의 임베딩 차원을 여러 개의 Head로 나눕니다.
예를 들어 다음과 같은 Tensor가 있다고 가정하겠습니다.
(batch_size, seq_len, embedding_dim)
여기서
embedding_dim = num_heads × head_dim
이라면 view()를 사용하여 임베딩 차원을 Head와 Head 차원으로 분리할 수 있습니다.
x = x.view(batch_size, seq_len, num_heads, head_dim)
Shape는 다음과 같이 변합니다.
(batch_size, seq_len, embedding_dim)
↓
(batch_size, seq_len, num_heads, head_dim)
하지만 Attention 계산에서는 보통 num_heads를 앞쪽으로 이동시켜야 합니다.
그래서 다음과 같이 transpose()를 사용합니다.
x = x.transpose(1, 2)
Shape는 다음과 같이 변합니다.
(batch_size, seq_len, num_heads, head_dim)
↓
(batch_size, num_heads, seq_len, head_dim)
여기서 두 함수의 역할은 분명하게 나뉩니다.
view()
→ embedding_dim을 num_heads와 head_dim으로 분리
transpose()
→ seq_len과 num_heads의 차원 순서를 교환
따라서 Transformer 코드를 이해하려면 view()와 transpose()의 차이를 정확하게 알아두는 것이 중요합니다.
한눈에 정리
| 함수 | 역할 | 원소를 읽는 순서 | 주요 특징 |
|---|---|---|---|
view() | Tensor의 Shape 변경 | 기존 메모리 순서 유지 | 연속된 메모리가 필요할 수 있음 |
view(-1, n) | 한 차원의 크기를 자동 계산 | 기존 메모리 순서 유지 | -1은 한 번만 사용 가능 |
transpose(dim1, dim2) | 두 차원의 위치 교환 | 차원에 따라 읽는 방식 변경 | Non-contiguous Tensor가 될 수 있음 |
contiguous() | 데이터를 연속된 메모리로 정리 | 현재 보이는 순서 기준 | transpose() 후 view() 전에 자주 사용 |
reshape() | Tensor의 Shape 변경 | 가능한 경우 기존 메모리 활용 | 필요하면 자동으로 복사 |
torch.equal() | 두 Tensor가 완전히 같은지 비교 | 값과 위치를 모두 비교 | Shape와 모든 값이 같아야 True |
핵심 요약
view()와 transpose()의 차이는 다음 두 문장으로 정리할 수 있습니다.
view()는 원소의 저장 순서를 유지하면서 Tensor의 Shape만 새롭게 해석합니다.
transpose()는 두 차원의 위치를 교환하여 각 차원의 역할과 값을 읽는 방식을 변경합니다.
따라서 두 Tensor의 Shape가 같더라도 view()를 사용했는지 transpose()를 사용했는지에 따라 각 위치에 들어 있는 값은 완전히 달라질 수 있습니다.
특히 Transformer에서는 다음과 같이 두 함수가 서로 다른 역할을 수행합니다.
x = x.view(batch_size, seq_len, num_heads, head_dim)
x = x.transpose(1, 2)
첫 번째 줄은 하나의 임베딩 차원을 여러 Head로 나누고, 두 번째 줄은 Attention 계산에 맞게 Head 차원을 앞으로 이동시킵니다.
이 차이를 이해하면 이후 Multi-Head Attention의 Tensor Shape 변화도 훨씬 쉽게 따라갈 수 있습니다.

