Processing math: 100%
본문 바로가기
  • Deep dive into Learning
  • Deep dive into Optimization
  • Deep dive into Deep Learning
Deep dive into Pytorch

Pytorch 7 : Tensor 심화

by Sapiens_Nam 2023. 7. 23.

 

여러 번 이야기하였듯이 딥러닝에서 데이터와 모델 파라미터, 모델의 결괏값 모든 것은 텐서로 이뤄져 있다.

텐서는 결국 다차원 배열로 각 성분들은 부동소수점 수로 이뤄져 있다. 

이번 글에서는 텐서에 대해 조금 더 깊게 들어가보고자 한다.

제목에 '심화'라고 써있지만 파이토치를 잘 다루기 위해서는 이 글에서 다루는 내용은 알고 있어야 한다.

 

파이토치는 상당히 파이썬스러운 프레임워크이지만 텐서는 파이썬 객체가 아니다. 오히려 C언어의 배열에 더 가깝다.

즉, 텐서의 각 성분은 메모리 할당이 "연속적으로" 이뤄지고 이에 대한 view를 제공하는 것이다.

여기서 각 성분들은 float32가 default이다. float 32는 메모리를 32bit (4 byte) 차지하므로 100만 개의 float32 타입의 1차원 텐서를 생성한다면 메모리에는 400만 byte의 연속적인 공간과 메타데이터 공간을 조금 더 차지하게 된다.

하지만 파이썬 리스트는 이와 다르게 메모리에 따로따로 할당된다.

 

실제 코드를 통해 살펴보자. 예를 들어 배치 사이즈가 2인 이미지 텐서를 생각해보자.

import torch
batch_t = torch.randn(2, 3, 5, 5) 

batch_gray_naive = batch_t.mean(-3)
batch_gray_naive.shape # torch.Size([2, 5, 5])

 

batch_t는 3차원 텐서 이미지가 2장 있는 것으로 볼 수 있다.

RGB 채널은 1번 차원에 있는데 ((2,3,5,5)에서 두 번째 인덱스이므로) 뒤에서부터 세 번째 차원(인덱스)에 있으므로 -3번 차원 (인덱스)로 접근할 수 있다.

이를 활용해 평균을 구할 수 있다. 또한 파이토치는 넘파이처럼 broadcasting 기능이 있는데 텐서끼리 서로 연산을 할 때 길이가 1인 차원을 늘려주는 것을 의미한다.

즉, (2,3,5,5) 차원의 텐서와 (3,1,1) 차원의 텐서 사이의 연산이 가능하다는 것이다. 즉, (3,1,1)차원을 (2,3,5,5) 차원으로 늘려준다. 단, 만약 (3,2,2) 차원이었다면 이는 연산이 불가능하다. 1이 아니기 떄문에 broadcasting이 적용되지 않는다.

즉, 텐서끼리 연산할 때는 차원이 서로 같거나 한쪽이 1이고 다른 쪽으로 broadcasting이 가능한 경우에 적용할 수 있다.

 

또한 텐서의 연산에 대한 다양한 문법은 파이토치 공식 자습서 (튜토리얼)을 참고하면 잘 정리되어져 있다.

(https://pytorch.org/docs)

여기서 그 많은 연산들을 하나하나 나열하는 건 큰 의미가 없어 보인다.

 

그 대신 좀 더 메모리적인 관점에서 텐서에 대해 접근을 계속해보자.

텐서의 내부 값들은 실제로는 torch.Storage 인스턴스로 관리하며 '연속적인' 메모리에 할당된 상태이다. 

우선, 실제로 값이 존재하는 '저장 공간'과 저장 공간을 '참조'한다를 구분하도록 하자.

파이토치의 Tensor 객체는 이러한 저장공간을 나타내는 Storage 객체에 대한 view 역할을 담당한다.

또한 offset을 사용하여 저장 공간의 위치에 접근하거나 특정 차원의 크기를 단위로 하여 접근할 수 있다.

즉, 한 문장으로 정리하면 Tensor는 Storage 객체 (인스턴스)에 대한 view다.

 

points = torch.tensor([[4, 1], [5, 3], [2, 1]])
points_storage = points.storage()
points_storage[0] # 4.0
points_storage[0] = 2.0
points # tensor([[2, 1], [5, 3], [2, 1]])

 

 

위 코드를 보면 points라고 하는 tensor 변수는 2차원으로 3개의 행과 2개의 열로 이뤄져 있다.

하지만 실제로 이 텐서는 어느 저장공간에 이 텐서가 있는지 가리키고 있을 뿐이다.

우리는 텐서를 거치지 않고 직접 이 저장 공간에 접근하였고, 이곳의 값을 바꾸면 실제 텐서에서의 값도 당연히 바뀜을 알 수 있다. 물론 이렇게 storage()로 직접 접근하는 방법보다 텐서의 값을 바꿀 때는 다음과 같은 방법을 많이 사용한다.

 

a = torch.ones(3, 2)

a.zero_()

 

'_'로 끝나는 연산을 활용하면 기존 텐서의 값을 바꿔준다. 이는 새로운 저장공간에 텐서를 생성하는 것이 아닌, 기존의 저장공간에서 텐서를 바꾸는 것임을 기억하자.

 

텐서의 Size는 텐서의 각 차원 별로 들어가는 요소의 수를 표시한 '튜플'이다.

넘파이에서는 이를 Shape이라 한다.

예를 들어 (3,3) 크기를 가진 텐서라면 첫 번째 차원에 3개의 요소, 두 번째 차원에 3개의 요소를 가진 텐서임을 알 수 있다.

 

다음은 전치 연산이다. 

전치 연산은 .t(), .transpose()를 사용하면 된다.

당연히 전치된 텐서는 Size가 바뀌게 된다.

이때 전치 연산된 텐서는 동일한 저장 공간을 view하고 있다.

 

points = torch.tensor([[4, 1], [5, 3], [2, 1]])
points_t = points.t()

id(points.storage()) == id(points_t.storage()) # True

 

그렇다면 points의 원소를 바꾸게 되면 points_t의 원소도 바뀌게 되는 것일까? 그렇지는 않다.

이를 이해하기 위해서는 텐서 메타데이터에 대한 이해가 필요한데 텐서 메타데이터로는 Size, offset, stride가 있다.

사이즈는 앞서 설명하였고, 오프셋 (offset)은 텐서의 첫 번째 요소를 가리키는 색인 값이다.

또한 stride는 각 차원에서 다음 요소를 가리키고 싶을 때 '실제 저장 공간에서' 몇 개의 요소를 건너뛰어야 하는지 알려준다.

 

transpose 연산은 메모리 재할당을 하는 것이 아닌 사이즈, offset, stride 등을 변경한 새로운 Tensor 객체를 할당하기 때문에 메모리 소모 관점에서 좀 더 저렴한 연산이다.

 

위 코드를 보면 두 텐서 (points, points_t)가 같은 메모리 공간을 가리키고 있다. id가 True로 나온다는 점에서 알 수 있다.

즉, 새로운 메모리를 할당하는 것이 아닌 원래 것과 다른 Stride 순서를 가진 새로운 Tensor 인스턴스를 생성하는 것이다.

 

실제로 points의 stride는 (2, 1)이고 points_t의 stride는 (1, 2)이다.

이 말은 points[0,0]에서 points[1, 0]로 이동하는 것은 '저장 공간 상에서' 요소 두 개를 건너뛰는 것과 같고 points[0, 0]에서 points[0, 1]로 이동은 한 칸만 이동하는 것과 같다는 의미이다.

 

이 모든 연산은 현재 CPU상에서 이뤄진다.

이를 GPU로 이동하고 싶다면 다음과 같이 device인자를 활용해주면 된다.

 

points_gpu = torch.tensor([[4, 1], [5, 3], [2, 1]], device='cuda')

 

이렇게 되면 gpu 메모리 상에 텐서가 생성되며 위 텐서를 활용한 연산은 gpu를 활용해서 수행된다.

 

 

728x90

'Deep dive into Pytorch' 카테고리의 다른 글

Pytorch 9 : Distributed training  (0) 2023.08.05
Pytorch 8 : Train과 Test  (0) 2023.07.26
Pytorch 6 : Implement CNN  (0) 2023.07.20
Pytorch 5 : Save and Load  (0) 2023.07.17
Pytorch 4 : Training  (1) 2023.07.16

댓글