tensor에 대해서 공부하다가 각각의 함수가 비슷비슷하고 -1, 1의 쓰임이 조금씩 다르고 텐서를 공부하면서 슬라이싱([:])에 대해서도 새롭게 알게된 내용이 있어서 정리해보려고 한다.
텐서 shape 바꾸기 (1) reshape 함수
reshape은 텐서의 모양을 다시 배열하는 함수로 텐서의 원소 수는 유지하되 차원 배열을 바꿀 수 있다. 메모리를 공유하지 않는다.
# 모양 변경
a = torch.randn(2, 3, 5) # (2,3,5) 크기를 가지는 텐서 생성
print(a)
print("Shape : ", a.size()) # 텐서 모양 반환
print('\n')
reshape_a = a.reshape(5, 6) # 3차원 텐서를 2차원 텐서로 크기 변경 (2,3,5) -> (5,6)
print(reshape_a)
print("Shape : ", reshape_a.size()) # 변경한 텐서 모양 반환
reshape은 텐서의 모양을 변경하지만, 원소의 총 개수는 그대로 유지된다.
-1을 사용하면 PyTorch가 자동으로 남은 차원의 크기를 계산해준다.
# (2,3,5) 크기를 가지는 Tensor를 (3,n)의 모양으로 변경, "-1" 로 크기 자동 계산
reshape_auto_a = a.reshape(3, -1)
# 2x3x5 = 3 x n 의 방정식을 푸는 문제로 n 이 자동설정
print(reshape_auto_a.size())
텐서 shape 바꾸기 (2) view 함수
view 함수도 텐서의 모양을 변경해준다.
print(a)
print("Shape : ", a.size()) # 텐서 모양 반환
print('\n')
# reshape 과 동일하게 (2,3,5) 크기를 (5,6) 크기로 변경
view_a = a.view(5, 6)
print(view_a)
print("Shape : ", view_a.size())
reshape과 동일하게 -1로 크기를 자동으로 계산해준다.
# (3,n)의 모양으로 변경. "-1" 로 크기 자동 계산
view_auto_a = a.view(3, -1)
print(view_auto_a.size())
텐서 차원 추가 - unsqueeze
unsqueeze는 텐서에 특정 차원에 크기가 1인 차원을 추가한다.
# 0부터 9까지의 숫자들을 (5,2) 크기로 변경
tensor_a = torch.tensor([i for i in range(10)]).reshape(5, 2)
print(tensor_a)
print('Shape : ', tensor_a.size())
print('\n')
unsqu_a = tensor_a.unsqueeze(0)
print(unsqu_a)
print('Shape : ', unsqu_a.size())
-1로 마지막번째에 차원을 추가해줄 수 있다.
# 마지막번째에 차원 하나 추가 (5,2) => (5,2,1)
unsqu_a2 = tensor_a.unsqueeze(-1)
print(unsqu_a2)
print('Shape : ', unsqu_a2.size())
텐서 차원 제거 - squeeze
squeeze는 텐서에 차원의 크기가 1인 차원을 제거한다.
print(unsqu_a)
print("Shape : ", unsqu_a.size())
print('\n')
squ = unsqu_a.squeeze() # 차원이 1인 차원을 제거
print(squ)
print("Shape : ", squ.size())
# 모든 원소가 0인 (2,1,2,1,2) 크기를 가지는 텐서
x = torch.zeros(2, 1, 2, 1, 2)
print("Shape (original) : ", x.size()) # 원래 텐서 크기
print('\n')
# 차원이 1인 차원이 여러개일 때, 모든 차원이 1인 차원 제거
print("Shape (squeeze()) :", x.squeeze().size())
print('\n')
# 0번째 차원은 차원의 크기가 1이 아니므로, 변화 없음
print("Shape (squeeze(0)) :", x.squeeze(0).size())
print('\n')
# 1번째 차원은 차원의 크기가 1이므로 제거
print("Shape (squeeze(1)) :", x.squeeze(1).size())
print('\n')
# 여러 차원 제거 가능 (0번째 차원은 차원의 크기가 1이 아니기 때문에 무시)
print("Shape (squeeze(0,1,3)) :", x.squeeze((0, 1, 3)).size())
unsqueeze와 squeeze를 함께 사용하는 경우
squeeze와 unsqueeze는 자주 함께 사용되어 차원을 제거했다가 다시 추가하는 작업을 한다. 예를 들어, 1차원을 가진 텐서를 한 차원 줄였다가 다시 특정 위치에 차원을 추가할 수 있다.
특히, 모델 입력으로 차원이 특정 형태여야 할 때, 자주 사용하는 패턴이다.
tensor_d = torch.randn(1, 3, 4, 1) # (1, 3, 4, 1)
print(tensor_d)
print('\n')
squeezed_tensor = tensor_d.squeeze() # (3, 4)
print(squeezed_tensor)
print('\n')
unsqueezed_tensor = squeezed_tensor.unsqueeze(0) # (1, 3, 4)
print(unsqueezed_tensor)
텐서 shape 변경 함수 view vs reshape vs unsqueeze
view와 reshape의 가장 큰 차이는 contiguous 여부이다.
※ contiguous 란?
- 텐서의 메모리 상에 연속적인 데이터 배치를 갖는 것
- 텐서를 처음 생성 후 정의하면 기본적으로 contiguous 하지만, 이에 대해 차원의 순서를 변경하는 과정을 거치면 contiguous 하지 않다.
- 텐서의 contiguous 함을 확인하기 위해선 is_contiguous() 를 사용한다.
view는 contiguous 하지 않은 텐서에 대해서 동작하지 않는다. 텐서가 메모리 상에서 연속적으로 배치되어 있어야 (contiguous해야) 동작한다.
reshape은 contiguous 하지 않은 텐서를 내부적으로 contiguous 하게 만들어주고, 크기를 변경한다.
💡 추가적으로 transpose, permute와 같이 텐서의 차원 순서를 변경하는 연산을 할 경우, 텐서가 더 이상 contiguous하지 않게 되므로 view 대신 reshape을 사용하는 것이 안전하다.
# view vs reshape
tmp = torch.tensor([[[0, 1], [2, 3], [4, 5]], \
[[6, 7], [8, 9], [10, 11]], \
[[12, 13], [14, 15], [16, 17]], \
[[18, 19], [20, 21], [22, 23]]])
tmp_t = tmp.transpose(0,1) # contiguous 를 False 로 만들기 위한 작업
print(tmp_t.is_contiguous()) # contiguous 한지 검사
print(tmp_t.view(-1)) # view는 contiguous 하지 않은 텐서에 대해선 동작이 되지 않음
reshape_tmp = tmp_t.reshape(-1) # reshape은 contiguous 하지 않아도 동작이 됨
print(reshape_tmp)
print(reshape_tmp.is_contiguous()) # contiguous 하지 않았던 Tensor를 contiguous 하게 변경해 줌
unsqueeze 는 차원의 크기가 1인 차원을 추가하지만, 차원의 크기가 1이 아니면 차원의 모양을 변경할 수 없다.
# (view , reshape) vs unsqueeze
tensor_a = torch.randn(2, 3)
# (2, 3) 의 텐서를 (2, 3, 1)의 크기로 변경
view_tensor = tensor_a.view(2, 3, 1) # view 를 이용하여 (2,3,1) 의 크기로 변경
reshape_tensor = tensor_a.reshape(2, 3, 1) # reshape 를 이용하여 (2,3,1) 의 크기로 변경
unsqueeze_tensor = tensor_a.unsqueeze(-1) # unsqueeze 를 이용하여 (2,3,1) 의 크기로 변경
print("View output size : ",view_tensor.size())
print("Reshape output size : ",reshape_tensor.size())
print("Unsqueeze output size : ",unsqueeze_tensor.size())
참고할만한 자료:
- [what is contiguous?] : https://titania7777.tistory.com/3
- [view vs reshape] : https://inmoonlight.github.io/2021/03/03/PyTorch-view-transpose-reshape/
- [view, reshape, transpose, permute 비교] : https://sanghyu.tistory.com/3
텐서 차원 확장 (1) expand 함수
expand는 텐서의 특정 차원을 반복하여 크기를 확장할 때 사용된다. 중요한 점은 메모리 복사 없이 새로운 모양을 만들어 브로드캐스팅 방식으로 동작한다.
- A 텐서가 1차원일 경우 : A 텐서의 크기가 (m,) 이면 m은 고정하고 (x,m)의 크기로만 확장 가능
- A 텐서가 2차원 이상일 경우 : 크기가 1인 차원에 대해서만 적용 가능. A 텐서의 크기가 (1,m) 이면 (x,m) , (m,1) 이면 (m,y) 로만 확장 가능.
# 텐서가 1차원일 경우
t1 = torch.randint(1, 10, (1, 3))
print(t1.shape)
print(t1)
print('\n')
t2 = t1.expand(3, 3)
print(t2.shape)
print(t2)
# 텐서가 2차원일 경우
t1 = torch.randint(1, 10, (4, 1))
print(t1.shape)
print(t1)
print('\n')
t2 = t1.expand(4, 5)
print(t2.shape)
print(t2)
# 텐서가 1차원일 경우
tensor_1dim = torch.tensor([1, 2, 3, 4])
print(tensor_1dim)
print("Shape : ", tensor_1dim.size())
print('\n')
expand_tensor = tensor_1dim.expand(3, 4) # (,4) 를 (3,4) 의 크기로 확장 (값을 반복)
print(expand_tensor)
print("Shape : ", expand_tensor.size())
# 텐서가 2차원일 경우
tensor_2dim = torch.tensor([[1, 2, 3, 4], [1, 2, 3, 4]]) # (2,4) 크기를 가진 Tensor
print(tensor_2dim)
print("Shape : ", tensor_2dim.size())
print('\n')
expand_tensor = tensor_2dim.expand(4,4) # (2,4) 를 (4,4) 의 크기로 확장 (값을 반복)
print(expand_tensor) # 에러 발생
print("Shape : ", expand_tensor.size()) # 에러 발생
-1을 사용해서 그 차원의 크기를 유지하면서 다른 차원을 확장할 수 있다.
t1 = torch.randn(1, 1, 3)
print(t1.shape)
print(t1)
print('\n')
# 4: 첫 번째 차원을 크기 4로 확장
# 1: 두 번째 차원을 크기 1로 확장
# -1: 세 번째 차원은 그대로 유지한다. 즉, 원래의 크기를 변경하지 않는다.
t2 = t1.expand(4, 1, -1)
print(t2.shape)
print(t2)
unsqueeze와 expand의 차이
unsqueeze는 새로운 차원을 추가(디멘션 증가)할 때 사용하고, 메모리 구조를 변경하지 않으며 원소의 복사도 일어나지 않는다. expand는 unsqueeze로 추가된 차원을 특정 크기로 확장할 때 자주 사용되며, 메모리 복사 없이 브로드캐스팅 방식으로 데이터를 확장한다.
예를 들어, unsqueeze로 추가된 차원을 expand로 확장하면 데이터는 반복되지 않으며 메모리 효율성을 유지한 채 새로운 차원에서 크기만 늘어난다. 즉, unsqueeze로 새로운 차원을 추가하고 그 차원의 크기가 1이기 때문에 그 차원만 expand를 통해 확장한다는 뜻이다. 이렇게 해서 메모리 복사 없이 텐서의 크기를 확잘할 수 있다.
tensor_b = torch.randn(3, 2)
print(tensor_b.size()) # (3, 2)
# unsqueeze로 차원 추가
unsqueezed_tensor = tensor_b.unsqueeze(1)
print(unsqueezed_tensor.size()) # (3, 1, 2)
# expand로 차원 확장
expanded_tensor = unsqueezed_tensor.expand(3, 4, 2)
print(expanded_tensor.size()) # (3, 4, 2)
텐서 차원 확장 (2) repeat 함수
repeat는 텐서의 특정 차원을 복사하여 반복하는 함수이다. 이는 실제로 메모리에 복사본을 생성해 크기를 늘린다.
- ex) A 텐서가 (m,n) 크기를 가진다하고, A 텐서를 repeat(i,j) 를 하면 결과값으로 (m x i, n x j)의 크기의 텐서가 생성된다.
tensor_1dim = torch.tensor([1, 2, 3, 4])
print(tensor_1dim)
print("Shape : ", tensor_1dim.size())
print('\n')
repeat_tensor = tensor_1dim.repeat(3, 4) # tensor_1dim 자체를 행으로 3번 반복, 열로 4번 반복
print(repeat_tensor)
print("Shape : ", repeat_tensor.size())
repeat에 1을 넣으면 그 차원을 그대로 유지하겠다는 의미다. 해당 차원의 크기를 변화시키지 않고 그대로 유지한다.
# 원본 텐서
tensor = torch.tensor([[1, 2], [3, 4]])
print(tensor.size())
print(tensor)
print('\n')
# repeat(2, 1)
repeated_tensor = tensor.repeat(2, 1)
print(repeated_tensor.size())
print(repeated_tensor)
참고할만한 자료: https://pytorch.org/docs/stable/generated/torch.Tensor.repeat.html
텐서 차원 확장 함수 비교 expand vs repeat
반복을 통한 텐서 크기 확장 함수로는 expand와 repeat이 있다. 이들의 차이점은 메모리 공유에 있다.
- expand
- 원본 텐서와 메모리를 공유한다.
- repeat
- 원본 텐서와 메모리를 공유하지 않는다.
import torch
# 원본 텐서 생성
tensor_a = torch.rand(1, 1, 3)
print('Original Tensor Size')
print(tensor_a.size())
print(tensor_a)
print('\n')
# expand 사용하여 (1,1,3) => (4, 1, 3)
expand_tensor = tensor_a.expand(4, 1, -1)
print("Shape of expanded tensor:", expand_tensor.size())
print('\n')
# repeat 사용하여 (1,1,3) => (4, 1, 3)
repeat_tensor = tensor_a.repeat(4, 1, 1)
print("Shape of repeated tensor:", repeat_tensor.size())
print('\n')
# 평면화된 뷰 수정 후 원본 텐서 확인
tensor_a[:] = 0
print("Expanded Tensor")
print(expand_tensor) # 값 변경이 됨
print('\n')
print("Repeated Tensor")
print(repeat_tensor) # 깂 변경 안됨
메모리와 차원 관리
메모리와 차원 관리가 중요한 경우, 특히 대규모 연산에서 expand와 repeat의 차이를 신경 써야한다.
메모리 절약이 중요한 경우에는 가급적 expand를 사용해 메모리 복사를 피하는 것이 좋다. 하지만, 만약 메모리 절약보다는 데이터를 복사해 완전한 복제본을 만들고자 할 때는 repeat를 사용하는게 좋다.
참고할만한 자료:
[expand vs repeat] https://seducinghyeok.tistory.com/9
슬라이싱 [:], [:, :]
코드를 작성하다가 여러 차원일때도 모든 텐서를 지칭할때 단순한 슬라이싱 표현을 사용하는걸 봤다. 찾아보니 PyTorch에서 [:]와 같은 슬라이싱 표현은 몇 차원이든지 상관없이 텐서 전체에 적용될 수 있다고 한다. 텐서의 차원 수가 여러 개여도, [:]는 모든 차원의 요소를 선택하겠다는 의미로 작동한다. 그래서 1차원이든 3차원이든 상관없이 [:] = 0으로 모든 값을 0으로 설정할 수 있다.
import torch
tensor_tmp = torch.rand(1, 1, 3) # 3차원 텐서 생성
print(tensor_tmp)
# 모든 값을 0으로 바꾸기
tensor_tmp[:] = 0 # 텐서의 모든 값이 0으로 변경
print(tensor_tmp)
그러나!! 텐서 전체값을 특정 텐서값으로 바꾸고 싶을때는 명시적으로 슬라이싱을 해줘야한다.
torch_randn(3,2) 같은 텐서가 있을때 모든 값을 tensor[0, 1]로 바꾸고 싶을때는 tensor_a[:,:] = torch.tensor([0,1]) 이렇게 해줘야한다. 왜냐하면 특정 값을 사용해 다차원 텐서를 바꾸려 할 때는 차원 맞춤이 필요하기 때문에, 명확하게 차원을 지정해 주는 것이 중요하다.
tensor_a = torch.randn(3, 2) # 크기 (3, 2)인 2차원 텐서
print(tensor_a)
# 모든 값을 [0, 1]로 바꾸기
tensor_a[:, :] = torch.tensor([0, 1])
print(tensor_a)
왜 tensor_a[:]만으로는 안 되는걸까?
tensor_a[:]는 텐서 전체를 선택하지만, 할당할 값의 차원이 맞지 않을 때 오류가 발생할 수 있다. 예를 들어, torch.tensor([0, 1])은 1차원 텐서이므로, 2차원 텐서인 tensor_a의 각 행에 맞춰 값을 할당하려면 차원을 명시적으로 지정해 주어야 한다.
💡 요약:
expand에서 -1: 해당 차원의 크기를 그대로 유지하면서 다른 차원을 확장한다.
reshape에서 -1: 남은 차원의 크기를 자동으로 계산해 맞춘다.
view에서 -1: 남은 차원의 크기를 자동으로 계산해 맞춘다.
repeat에서 1: 그 차원을 그대로 유지
unsqueeze에서 -1: 마지막 번째에 차원을 추가
'Deep Learning' 카테고리의 다른 글
텍스트 데이터 전처리 방법 (토큰화, 정제, 정규화, Stemming, Lemmatization 등) (2) | 2024.11.12 |
---|---|
딥러닝 모델 구현에서 PyTorch의 쓰임 (2) | 2024.10.28 |
손실 함수, 활성화 함수, 최적화 함수 등 순서, 쓰임, 역할 (1) | 2024.10.23 |
가중치 초기화의 중요성과 Xavier 초기화, He 초기화 소개 (1) | 2024.10.17 |
딥러닝에서 손실 함수의 선택: Maximum Likelihood와 확률적 해석 (MSE, Cross-Entropy) (1) | 2024.10.16 |