서론
이 장에서는 pytorch의 View, Reshape 함수의 차이점과 이를 결정하는 텐서가 contiguous 속성을 갖는가에 대해 설명한다.
목차
View, Reshape 차이
View
view함수가 반환하는 텐서를 변경하면 원본 텐서도 변경된다.view함수는 메모리에 연속적으로 저장된 텐서에만 작동한다.- 만약 텐서가 메모리에 연속적으로 저장되어 있지 않다면,
view함수는 에러를 반환한다.
reshape
reshape함수는view함수와 비슷하게 작동하지만,reshape함수는 원본 텐서가 메모리에 연속적으로 저장되어 있지 않아도 작동한다.- 만약 텐서가 메모리에 연속적으로 저장되어 있다면,
reshape함수는view와 같이 원본 데이터를 공유하는 텐서를 반환한다. - 그러나 만약 텐서가 메모리에 연속적으로 저장되어 있지 않다면,
reshape함수는 텐서의 복사본을 반환한다.
Transpose vs Transpose_
본론에 들어가기 앞서 둘의 차이를 먼저 알아보자
transpose와 transpose_ 차이는 inplace적용 여부의 차이다.
먼저 transpose를 아래와 같이 사용할 경우 적용되지 않는 것을 볼 수 있다.
y = torch.ones(3, 4) # (3, 4)
y.transpose(1, 0) # 적용되지 않음
print(y.shape)
# 출력결과
torch.Size([3, 4]) # 기존 (3, 4)에서 (4, 3)으로 변경되지 않음
그런데 아래와 같이 적용하면 변경이 가능하다.
y = torch.ones(3, 4)
y = y.transpose(1, 0) # 적용됨
print(y.shape)
# 출력결과
torch.Size([4, 3])# 기존 (3, 4)에서 (4, 3)으로 변경됨
이를 =을 사용하지 않고 _를 사용하면 바로 적용이 된다.
a = torch.randn(3, 4)
a.transpose_(1, 0) # (4, 3)
print(a.shape)
# 출력결과
torch.Size([4, 3])
Contiguous 속성
contiguous 속성을 알아보기 위해 각각 a와 b에 torch.size[(4, 3)]을 갖도록 만들어 보겠다.
데이터의 타입인 torch.float32 자료형은 4바이트이므로, 메모리 1칸 당 주소 값이 4씩 증가해야 한다.
import torch
a = torch.randn([3, 4], dtype=torch.float32)
a.transpose_(0, 1) # (4, 3)
b = torch.randn([4, 3], dtype=torch.float32)
print(f"{a}{a.dtype}\n{b}{b.dtype}")
# 출력결과
tensor([[ 0.3883, -0.7349, -1.5846],
[-0.8760, 0.0967, -0.5309],
[ 2.5855, 0.6644, -0.0572],
[-0.2135, -1.6887, -1.7495]])torch.float32
tensor([[-1.1466, 0.2903, 0.6962],
[ 2.3420, 0.7467, 0.1814],
[-0.1162, 0.2598, 0.3594],
[-0.2046, -0.2923, 2.0186]])torch.float32
각각의 값들이 메모리 주소에 할당되는 주소를 찍어보겠다.
for i in range(4) :
for j in range(3) :
print(a[i][j].data_ptr())
print(a)
for i in range(4) :
for j in range(3) :
print(b[i][j].data_ptr())
print(b)
a의 출력결과는 메모리가 16씩 커지는 것을 알 수 있고, b의 경우는 4씩 증가한다.
즉 b의 경우 axis=0의 방향으로 메모리가 증가되는 반면 a의 경우는 axis=1인 방향으로 메모리가 증가된다.
# a 출력결과
94074058297024
94074058297040
94074058297056
94074058297028
94074058297044
94074058297060
94074058297032
94074058297048
94074058297064
94074058297036
94074058297052
94074058297068# b 출력결과
94074058079808
94074058079812
94074058079816
94074058079820
94074058079824
94074058079828
94074058079832
94074058079836
94074058079840
94074058079844
94074058079848
94074058079852a를 axis=1의 방향으로 메모리값을 출력해보니 4씩 증가하는 것을 볼 수 있다.
for j in range(3) :
for i in range(4) :
print(a[i][j].data_ptr())
# 출력결과
94074058881664
94074058881668
94074058881672
94074058881676
94074058881680
94074058881684
94074058881688
94074058881692
94074058881696
94074058881700
94074058881704
94074058881708
a같이 자료 저장 순서가 원래 방향과 어긋난 경우를 contiguous = False 상태라고 하며 b처럼 axis 순서대로 자료가 저장된 상태를 contiguous = True 상태라고 부른다.
Contiguous 속성 확인하기
이는 각 텐서에 stride() 메소드를 활용하면 데이터의 저장 방향을 알 수 있다.
a.stride() 결과가 (1, 4)라는 것은 a[0][0] -> a[1][0]으로 증가할 때는 자료 1개 만큼의 메모리 주소가 이동되고(우리의 다룬 예시의 경우 자료 1개당 메모리 이동수는 4이다.)
a[0][0] -> a[0][1]로 증가할 때는 자료 4개 만큼의 메모리 주소가 바뀐다는 의미다.(위 예시는 16씩 이동한다)
print(a.stride()) # (1, 4)
print(b.stride()) # (3, 1)
print(a.is_contiguous()) # False
print(b.is_contiguous()) # True
Contiguous하게 변경하기
import torch
import numpy as np
a = torch.randn([3, 4], dtype=torch.float32)
a.transpose_(0, 1) # (4, 3)
for i in range(4) :
for j in range(3) :
print(a[i][j].data_ptr())
print("----------------------------------")
a = np.ascontiguousarray(a)
c = torch.Tensor(a)
for i in range(4) :
for j in range(3) :
print(c[i][j].data_ptr())
결과를 보면 16씩 메모리 공간을 뛰는 반면 np.ascontiguousarray 를 사용하니 4씩 연속적으로 할당되는 것을 볼 수 있다.
# 출력결과
94505564124864
94505564124880
94505564124896
94505564124868
94505564124884
94505564124900
94505564124872
94505564124888
94505564124904
94505564124876
94505564124892
94505564124908
----------------------------------
94505569910288
94505569910292
94505569910296
94505569910300
94505569910304
94505569910308
94505569910312
94505569910316
94505569910320
94505569910324
94505569910328
94505569910332
View, Reshape 알아보기
torch.view와 torch.reshape의 가장 큰 차이는 contiguous 속성을 만족하지 않는 텐서에
적용이 가능하느냐 여부다. view는 contiguous 속성이 만족되지 않는 경우 일부 사용이 제한될 수 있다.
view와 Reshape은 차원을 변경해주는 함수이다. 어떻게 사용하는지 먼저 알아보자
import torch
x = torch.arange(12)
print(x) # tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
두 함수를 적용해보면 차원을 변경해준다. 아래처럼 사용하면 문제가 되지 않는다.
print(x.reshape(3, 4))
# 출력결과
'''
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])'''
print(x.view(3, 4))
# 출력결과
'''
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])'''
두 함수의 차이에 대한 이해를 돕기 위해 transpose를 거쳐 contiguous 속성을 만족하지 않는 텐서를 만들어 적용시켜 보겠다.
import torch
y = torch.ones(3, 4) # (3, 4)
y.transpose_(1, 0) # (4, 3)
y.is_contiguous() # False
reshape에서는 잘 동작한다.
print(y.reshape(3, 2, 2)) # 실행 가능
# 출력결과
tensor([[[1., 1.],
[1., 1.]],
[[1., 1.],
[1., 1.]],
[[1., 1.],
[1., 1.]]])
그러나 view를 사용하는 경우 RuntimeError가 발생한다.
print(y.view(3, 2, 2)) # 실행 불가
# 출력결과
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.
결론
차원 변환을 적용하려는 텐서의 상태에 대하여 정확하게 파악하기가 모호한 경우에는 view 대신 reshape를 사용하시는 것을 권장한다.
[reference]
Comment