본문 바로가기

Pytorch

[Pytorch] View, reshape, permute, transpose의 차이와 Contiguous

# Intro

Pytorch에서 Tensor의 모양을 바꾸는 방법들이 다양하다. 제목에 명시된 것처럼 View, reshape, permute, transpose가 있다. 행동은 비슷하지만, 결과는 살짝씩 다르다. 이 함수들의 차이들을 간단하게 설명해 보려고 한다. 

 

# Contiguous

먼저, Contiguous 메소드를 살펴보아야 한다. Pytorch Tensor들은 1d 이상의 배열로 구현되어 있을 경우, 안에 들어있는 value들이 연속적으로 메모리에 배정이 된 게 된다. 예를 들어서,

 

temp_tensor = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
print(temp_tensor.dtype)
print(temp_tensor[0][0].data_ptr())
print(temp_tensor[0][1].data_ptr())

 

위의 코드를 돌려보면 

 

int64타입으로 뜰 것이고, 두 개의 할당된 메모리 주소가 나올 것이다. 

 

제대로 한 경우에는 두 메모리 주소의 차이는 8 Bytes 차이가 날 것이다 (int64 이기 때문에)

임의로 적어보자면

93969595054208 93969595054216 93969595054224
93969595054232 93969595054240 93969595054248
93969595054256 93969595054264 93969595054272

 

이런식으로 끝에 두 자리만 보면 연속적으로 8 Bytes씩 할당된 것을 확인해 볼 수 있다. 이렇게 바르게 메모리에 연속적으로 정렬된 상태를 Contiguous상태라고 한다. 확인 방법은

temp_tensor.is_contiguous()

 

라는 메소드를 호출하면 True/False가 나올 것이다. Pytorch에서는 연산 전에 무조건 텐서들이 Contiguous 상태여야 하기 때문에 Contiguous 상태가 깨지면 에러를 출력한다. 나중에 더 자세하게 설명하겠지만, transpose 메서드로 shape를 변형을 하게 되면 Contiguous상태가 깨지게 되고 저 메서드들로 변형을 시켰다면 

temp_tensor.contiguous()

 위의 메소드를 실행시켜서 다시 메모리에 연속적으로 배치를 해줘야 한다. 연산을 할 때뿐만이 아니라 특정 메서드들은 contiguous 해야 실행이 되는 메서드들이 존재한다. 

 

# view vs reshape

둘다 비슷한 행동을 하는 메서드들인데 약간의 차이가 존재한다. 

view는 텐서의 shape를 바꾸지만, Copy값을 넘겨주는 게 아니라 원래 기존의 tensor의 메모리를 그대로 셰어 한다. view 넘겨받은 텐서를 변경하면 기존의 텐서도 변경이 된다. 그리고 기존의 텐서가 Contiguous 해야 사용가능하다. 

temp_tensor = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]])
view_temp_tensor = temp_tensor.view((3,4))
print(view_temp_tensor.is_contiguous()) # True
view_temp_tensor[0][0] = 12
print(view_temp_tensor) 
print(temp_tensor) # 기존의 텐서도 값이 바뀜을 확인

 

temp_tensor = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]])
transpose_tensor = temp_tensor.transpose(1, 0)
print(transpose_tensor.is_contiguous()) # False
view_transpose_tensor = transpose_tensor.view((4,3)) # 에러 발생
view_transpose_tensor = transpose_tensor.contiguous().view((4,3)) # 변경 성공

 

반면에 reshape는 기존의 텐서가 Contiguous 하지 않아도 사용할 수 있다는 장점이 있다. 그리고 조금은 애매한게, Contiguous 한 텐서를 받아오면 view처럼 작동을 하고, Contiguous 하지 않으면 copy값을 넘겨준다고 한다. 헷갈리기 쉬우니 기존 데이터값을 유지해야 한다면 clone 메서드를 활용해서 카피값을 확실하게 만들어주는 게 낫다. 보통 numpy나 pandas가 익숙한 사람들이 관성적으로 view 보다는 reshape을 많이 사용하는 것 같다. 

 

텐서를 copy하는 방법들도 여러 가지가 있는데, 그것들의 차이도 다음에 한번 글을 적어볼 것이다. 

 

temp_tensor = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9,10, 11]])
print(temp_tensor.is_contiguous()) # True
reshape_tensor = temp_tensor.reshape((4,3))
reshape_tensor[0][0] = 12 
print(reshape_tensor) 
print(temp_tensor) # 기존 데이터도 값이 변경됨을 확인
temp_tensor = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]])
transpose_tensor = temp_tensor.transpose(1, 0)
print(transpose_tensor.is_contiguous()) # False
reshape_transpose_tensor = transpose_tensor.reshape((4,3))
reshape_transpose_tensor.is_contiguous() # True -> 카피값을 받아오면서 contiguous하게 만든다

reshape_transpose_tensor[0][0] = 12
print(reshape_transpose_tensor)
print(transpose_tensor) # 값이 변경이 안됨
print(temp_tensor) # 값이 변경이 안됨

 

# Permute vs Transpose

Permute와 Transpose는 역할이 똑같다고 보면 되는데 차이점은 축을 여러개 바꾸고 싶다면(3개 이상) permute를 사용하면 되고, transpose는 딱 두 개만 바꿀 때 사용이 된다. 

 

그리고, 두개의 메서드 전부 Contiguous를 깨버리고, 기존 메모리를 셰어 한다.

 

new_tensor = torch.tensor([[1, 2, 3, 4],[1, 2, 3, 4],[1, 2, 3, 4]])
print(new_tensor)
#tensor([[1, 2, 3, 4],
#        [1, 2, 3, 4],
#        [1, 2, 3, 4]]) 

permute_tensor = new_tensor.permute(1, 0)
print(permute_tensor.is_contiguous()) # False
print(permute_tensor)
#tensor([[1, 1, 1],
#        [2, 2, 2],
#        [3, 3, 3],
#        [4, 4, 4]])

permute_tensor[0][0] = 12
print(permute_tensor)
print(new_tensor) # 값이 변경된다.
new_tensor = torch.tensor([[1, 2, 3, 4],[1, 2, 3, 4],[1, 2, 3, 4]])
print(new_tensor)
#tensor([[1, 2, 3, 4],
#        [1, 2, 3, 4],
#        [1, 2, 3, 4]])
transpose_tensor = new_tensor.transpose(1,0)
print(transpose_tensor.is_contiguous()) # False
print(transpose_tensor)
#tensor([[1, 1, 1],
#        [2, 2, 2],
#        [3, 3, 3],
#        [4, 4, 4]])
transpose_tensor[0][0] = 12
print(transpose_tensor)
print(new_tensor) # 값 변경확인

 

보통은 Permute가 transpose의 역할도 할 수 있기 때문에 보통은 Permute를 많이 사용한다. 

 

# Permute vs Reshape?

 

처음 입문하시는 분들이 가장 많이 헷갈리시는 게 Reshape (혹은 view)를 써야 할지 Permute(혹은 Transpose)를 써야할지 이다. 

간단히 생각하면 Permute는 axis 순서를 바꿀 때 사용한다. 보통 많이 사용하는 예는 보통의 이미지는 (height, width, channels) 순인데 파이토치에서는 (channels, height width) 순으로 많이 사용된다. 이때 channels의 위치를 바꿔줄 때 permute를 활용하여 바꿔준다.

 

Axis의 위치를 교정해야 할 때 Reshape나 View로 맞춰 준다면 데이터가 엉키고 무너지기 때문에 학습이 아예 안된다. 그 외에는 reshape를 활용해 주면 된다. 

 

예를 통해서 보면,

 

new_tensor = torch.tensor([[1, 2, 3, 4],[1, 2, 3, 4],[1, 2, 3, 4]])
print(new_tensor)
#tensor([[1, 2, 3, 4],
#        [1, 2, 3, 4],
#        [1, 2, 3, 4]])
permute_tensor = new_tensor.permute(1, 0)
print(permute_tensor) # 축만 변경이되고 축에 할당된 데이터들 순서는 변하지 않음
#tensor([[1, 1, 1],
#        [2, 2, 2],
#        [3, 3, 3],
#        [4, 4, 4]])
reshape_tensor = new_tensor.reshape((4,3))
print(reshape_tensor) # 모양에 맞춰서 데이터가 변경이 됨.
#tensor([[1, 2, 3],
#        [4, 1, 2],
#        [3, 4, 1],
#        [2, 3, 4]])

 

예를 보면 정확하게 차이를 느낄 수 있을 것이다.