본문 바로가기
Book Review/[파이토치 딥러닝 마스터] 리뷰

파이토치: 텐서(Tensor) 구조체

by 3n952 2023. 3. 24.


 

이미지나 텍스트 같은 비정형의 데이터를 처리하기 위해서는 여러 가지 형태의 데이터로 만드는 것이 필요합니다.

부동소수점 수로 변환하는 것이 그 예시입니다.

입력데이터를 부동소수점 수로 변환하면 딥러닝 프로세스에서도 비슷한 표현을 가질 것입니다.

입력 데이터가 딥러닝 프로세스에 들어가기 전에 어떻게 인코딩 되어야 하고, 출력으로 나온 결괏값을 우리가 해석할 수 있도록 디코딩하는 것이 필요합니다.

 

입력 데이터를 부동소수점으로 바꾸기 전에 입력과 중간 표현, 출력에서 어떻게 데이터를 다루는지 알 필요가 있습니다.

이때 등장하는 것이 텐서(Tensor)입니다.

텐서란 다차원 배열을 의미합니다. 즉,  임의의 차원을 가지는 벡터나, 행렬의 일반화된 개념이라고 생각하면 됩니다.

이를 정리한 포스트가 있으니 참고해 주세요!

https://sanmldl.tistory.com/7

 

신경망 이해를 위한 Tensor(텐서), Gradient(그레이디언트)

딥러닝을 이해하려면 여러 가지 수학 개념과 친숙해질 필요가 있습니다. 텐서, 미분, 경사 하강법 등이 있는데요. 이번 포스트의 목표는 비전공자도 이해할 수 있도록 최대한 간결하게 이 수학

sanmldl.tistory.com

 

- 파이토치에서 텐서 활용해 보기

텐서는 파이토치의 기본 자료구조로 한 개나 여러 개의 인덱스를 사용해 개별적으로 값에 접근할 수 있습니다.

마치 파이썬의 리스트를 인덱스를 사용해서 호출하는 것과 같습니다.

#파이썬 리스트
a = [1.0, 2.0, 1.0]

#첫 번째 요소 호출
a[0]

텐서를 사용하면 파이썬 리스트 인덱싱처럼 개별 값에 접근할 수 있습니다.

하지만 다차원의 이미지와 시계열 데이터, 문장 데이터는 다차원의 텐서를 다룹니다.

따라서 텐서 연산을 정의해 두면 훨씬 효율적이고 알아보기 쉽게 데이터를 조작할 수 있습니다.

 

-텐서 만들기

파이토치로 텐서를 만들어 보겠습니다.

크기가 3인 1차원 텐서의 값을 1로 채우고 마지막 요소를 2.0으로 바꿔보고,

.zero를 사용하여 텐서 숫자 값을 채워보겠습니다.

코드는 다음과 같습니다.

import torch 

a = torch.ones(3)
'''  =>tensor([1., 1., 1.]) '''

a[2] = 2.0
''' => tensor([1., 1., 2.]) '''

#방법 1: 일일히 덮어쓰기
point = torch.zeros(4)
point[0] = 4.0
point[1] = 1.0


#방법 2: 파이썬 리스트로 넘겨주기
point = torch.tensor([4.0, 1.0, 5.0, ....])

#점의 좌표 읽기
float(point[2])

겉으로 보기에는 파이썬 프로그램의 리스트 숫자 객체와 같지만 내부적으로 이뤄지는 동작은 완전히 다릅니다.

파이썬 리스트는 메모리에 따로따로 할당되어 박싱 된 파이썬 객체 숫자값에 할당되지만

파이토치의 텐서나 넘파이 배열은 파이썬 객체가 아닌 언박싱된 C언어로 연속적인 메모리에 할당되고 이에 대한 뷰를 제공하는 식입니다.

여기에서 각 요소는 32비트의 float타입입니다.

 

2차원텐서도 비슷하게 만들어줄 수 있습니다.

# 3 x 2 크기의 제로텐서
point = torch.zero(3,2)

#2차원 텐서 예시
point = torch.tensor([4.0, 1.0], [5.0, 3.0], [2.0, 1.0])

#좌표 접근
point[0,1]
''' => tensor(1.) '''

 

-텐서의 인덱싱

표준 파이썬의 범위 인덱싱이 가능합니다.

더욱이 파이토치 텐서는 넘파이를 비롯한 파이썬 과학 라이브러리와 동일한 표기법을 가집니다.

가령 첫 번째~모든 행에 대해 모든 열이 포함되게 하려면 다음코드와 같을 것입니다.

point[1:, :]

이러한 범위 인덱싱뿐만 아니라 파이토치는 고급 인덱싱이라 부르는 더욱 강력한 방식을 사용할 수 있게 합니다.

이는 다음 포스팅에서 예제를 통해 알려드리도록 하겠습니다.

 

-텐서에 이름 부여하기

텐서는 차원 혹은 축이 있기 때문에 각 차원이 어떠한 정보를 담고 있는지 아는 것이 중요합니다.

텐서끼리의 연산을 위해 텐서에 접근해야 하는데,

텐서에 접근하기 위해서는 차원의 순서를 기억해야 하기 때문입니다.

 

파이토치에서는 각 차원에 이름을 부여할 수 있는 기능이 있습니다.

이름을 부여하는 방법에는 크게 3가지 방식 정도가 있습니다.

1. tensor나 randn와 같은 텐서 팩토리 함수에 named인자로 이름 문자열 전달하기

2. refine_names 함수 사용하기

3. rename함수 사용하기

 

이를 코드로 한번 살펴보겠습니다.

#해당 차원 channels로 이름 짓기
tensor_named = torch.tensor([0.124,0.345,0,363], names = ['channels'])

#각 차원별 이름 짓기(img 텐서가 있다고 가정)
img_named = img.refine_names(..., 'channels', 'row', 'columns')

2번째 줄에서 ...의 의미는 해당 차원의 이름은 건들지 않겠다는 뜻입니다.

 

이름이 지정되어 있다면 파이토치가 자동적으로 각 차원의 크기가 같은지 혹은 브로드캐스팅될 수 있는지 확인해 줍니다.

이때 명시적으로 차원을 정렬해줘야 하기 때문에 align_as함수를 사용해 줍니다.

이를 통해 빠진 차원은 채우고 존재하는 차원은 올바른 순서로 바꿔주게 됩니다.

바로 위의 코드와 이어서 코드를 작성해 보겠습니다.

#해당 차원 channels로 이름 짓기
tensor_named = torch.tensor([0.124,0.345,0,363], names = ['channels'])

#각 차원별 이름 짓기(img 텐서가 있다고 가정)
img_named = img.refine_names(..., 'channels', 'row', 'columns')

weight_tensor = tensor_named.align_as(img_named)
weight_tensor.shape, weight_tensor.names

''' 출력값 => torch.Size([4, 1, 1]), ('channels', 'row', 'columns')) '''

이렇게 지정한 이름 있는 텐서를 만들어 함수 내 연산에서 활용할 수 있습니다.

 

-텐서를 내부적으로 살펴보기

텐서 객체는 파이썬 리스트의 원소가 새로운 메모리에 영역이 할당되고 값이 복사된 후 새 메모리에 대해 새 원소가 래핑 되어 반환되는 형식과는 다릅니다. 텐서 내부 값은 실제로 torch.Storage 인스턴스로 관리하며 연속적인 메모리 조각으로 할당됩니다.

심지어 서로 다른 방식으로 구성된 텐서가 동일한 메모리 공간을 가리키고 있을 수 있습니다.

메모리는 한 번만 할당되었지만 동일한 데이터에 대해 다른 텐서 뷰를 만드는 것은 데이터 크기에 상관없이 빠르게 수행됩니다.

 

실제로 2D포인트로 저장 공간 인덱싱이 어떻게 동작하는지 코드로 살펴보겠습니다.

point = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])
point.storage()

출력: 4.0 1.0 5.0 3.0 2.0 1.0 [torch.storage.TypedStorage(dtype=torch.float32, device=cpu) of size 6]

텐서는 3 x 2 크기지만 실제로는 크기가 6인 배열 공간에 저장되어 있습니다.

텐서는 그저 주어진 차원 쌍이 실제로 어느 공간에 해당하는지 알 뿐입니다.

즉, 텐서는 차원에 무관하게 실제 저장 공간 레이아웃은 1차원이라는 것을 의미합니다.

 

따라서 저장 공간에 접근해서 값을 바꾸면 참조하는 텐서의 내용도 바꿀 수 있습니다.

point_storage = point.storage()
point_storage[0] = 2
point

출력: tensor([[2., 1.], [5., 3.], [2., 1.]])

 

-텐서의 메타데이터

앞서 살펴본 것처럼 저장 공간을 인덱스로 접근하기 위해서는 텐서는 저장 공간에 대한 정보를 알아야 합니다.

이때 등장하는 개념이 사이즈(size), 오프셋(offset), 스트라이드(stride)입니다.

텐서의 사이즈는 넘파이 용어의 shape와 같습니다. 각 차원 별 요소 수를 튜플로 나타냅니다.

저장 공간의 오프셋은 텐서의 첫 번째 요소를 가리키는 index입니다.

스트라이드는 각 차원에서 다음 요소를 가리킬 때 저장 공간 상에서 얼마나 떨어져 있는지를 나타냅니다.

 

그림1 텐서의 메타데이터

그림 1을 살펴보면 위의 행렬이 텐서이며 밑의 리스트가 저장 공간이라고 생각하면 됩니다.

우선, 텐서는 3 * 2의 shape를 가집니다. 즉 그림 1의 텐서의 사이즈는 (3,2)으로 표현됩니다.

 

오프셋은 첫 번째 요소인 '1'이 저장 공간에서 인덱스가 무엇인지 보면 됩니다.

즉 빨간 선을 따라가면 저장 공간에서 인덱스가 1( 저장공간[1] = 1 ) 이므로 오프셋은 1입니다.

 

마지막으로 스트라이드는 합성곱 신경망에서 합성곱 연산에서 쓰이는 그 stride와 비슷한 개념으로

행 차원에서 다음 요소는 저장 공간 상에서 2칸 뒤에 위치합니다. 

(2차원 텐서에서 axis = 0을 기준으로 1 -> 2는 저장 공간에서 2칸 차이가 난다)

열 차원에서 다음 요소는 저장 공간 상에서 1칸 뒤에 위치합니다.

(2차원 텐서에서 axis = 1을 기준으로 1 -> 4은 저장 공간에서 1칸 차이가 난다)

즉, stride[0] = 2, stride[1] = 1 인 것입니다.

 

텐서는 차원을 추가하거나 줄이거나, 전치 연산을 하거나 서브 텐서를 만들거나 등등의 계산을 할 때

따로 저장 공간을 만들어내는 것이 아닌 사이즈, 오프셋, 스트라이드를 바꾸면서 텐서를 변경할 수 있습니다.

동일한 저장 공간을 가리키고 있기 때문에 효율적입니다.

 

 

-텐서 API

파이토치 텐서에 대해 파악했으면 이제 활용할 줄 알아야 합니다.

파이토치가 제공하는 텐서 연산을 다 외우진 못해도 무엇이 있는지는 알 필요가 있습니다.

이에 대한 API가 많으므로 온라인 문서를 찾아보며 연습해 보세요!

https://pytorch.org/docs/stable/index.html

 

PyTorch documentation — PyTorch 2.0 documentation

Shortcuts

pytorch.org

 

 

이번 포스팅에서는 파이토치에서 다루는 텐서의 개념과 어떻게 효율적으로 텐서가 작동하는지에 대해 알아보았습니다.