파이토치 딥러닝 마스터_3장

코넬·2023년 3월 20일
0

ComputerVision_Pytorch

목록 보기
3/10
post-thumbnail

🤠 3장 - 텐서 구조체 이해하기

이번 시간에는 ...

  • 파이토치 기본 자료구조인 텐서를 이해한다.
  • 텐서를 인덱스로 접근해서 연산한다.
  • 다차원 배열 넘파이와 연계해서 다루기
  • 성능 개선을 위해 GPU로 연산 처리하기

딥러닝 프로세스는 입력을 부동소수점 수로 변환하는 것부터 시작한다. 3장에서는 응용을 들어가기 전에 ! 텐서 를 사용하여 파이토치가 부동소수점 수를 어떻게 다루는지를 알아보자 !

여기서 잠깐 !
부동 소수점 방식(floating point)이란?
실수는 보통 정수부와 소수부로 나누지만, 가수부와 지수부로 나누어 표현할 수 있다. 부동 소수점 방식은 이렇게 하나의 실수를 가수부와 지수부로 나누어 표현하는 방식이다. 앞서 살펴본 고정 소수점 방식은 제한된 자릿수로 인해 표현할 수 있는 범위가 매우 작다. 하지만 부동 소수점 방식은 다음 수식을 이용하여 매우 큰 실수까지도 표현할 수 있게 된다.

🌏 부동소수점 수의 세계

부동소수점이 왜 중요할까? 신경망 안에서의 정보 처리는 모두 부동소수점 수 로 나타내기 때문에, 실제 데이터를 신경망이 연산 가능하도록 인코딩 해야하며, 처리 후 나온 결괏값은 해석 가능하거나 사용할 수 있도록 디코딩해야한다.

심층 신경망(deep neural network)은 보통 여러 단계를 거쳐 데이터 변환을 학습한다.
따라서 각 단계 사이의 일부 변환된 데이터들은 중간 단계를 표현하는 연속적인 흐름으로 생각할 수 있다.

여기서 중요한 점은, 중간 단계는 입력값의 특징을 잡아내는 부동소수점 수의 모음인 동시에 신경망에서 입력이 최종적으로 출력으로 표현되는 방법을 기술하기 위한 수단으로 데이터 구조를 잡는다. 중간 표현값은 부동소수점 수로 이루어져 있고 입력을 특징짓는 동시에 데이터 구조를 잡아내어 신경망 출력과 어떻게 매핑되는지를 설명하는데 중요한 역할을 한다는 것 !

즉, 중간 표현값은 입력과 이전 층의 뉴런이 가진 가중치를 조합한 결과라는 점이다. 중간 단계의 개별 표현은 자신만의 방식으로 앞 단계에서 넘어온 입력을 반환한다.

이러한 데이터 처리와 저장을 위해 파이토치에서는 텐서(tensor) 라는 기본 자료구조를 제공한다. 텐서는 다차원 배열(multidimensional array) 라고 부르며, 텐서의 차원 수는 텐서 안의 스칼라 값을 참조하기 위해 사용하는 인덱스 수와 동일하다.

이제부터 텐서에 차근차근 살펴보자 !

❄️ 텐서 : 다차원 배열

텐서는 일종의 배열이다. 즉, 한개나 여러개의 인덱스를 사용하여 개별적으로 값에 접근할 수 있는 형태의 숫자 모음을 저장하는 자료구조이다.

텐서의 핵심

텐서는 자료구조를 사용해 이미지와 시계열 데이터 혹은 문장들을 나타내는 것이 일반 파이썬 리스트보다 더 효율적이다.

텐서와 넘파이 배열의 경우에는, 파이썬 객체가 아닌 언박싱 된 C 언어의 숫자 타입을 포함한 연속적인 메모리가 할당되고, 이에 대한 뷰를 제공한다.

t = torch.FloatTensor([0., 1., 2., 3., 4., 5., 6.])
print(t) #	1차원 텐서인 벡터를 만든다.

print(t.dim())  # rank. 즉, 차원
print(t.shape)  # shape
print(t.size()) # shape

------------------------------------------------------
1
torch.Size([7])
torch.Size([7])

넘파이 실습과 동일하게 슬라이싱, 즉 고급 인덱싱 이 가능한 것을 확인할 수 있다.

print(t[0], t[1], t[-1])  # 인덱스로 접근
print(t[2:5], t[4:-1])    # 슬라이싱
print(t[:2], t[3:])       # 슬라이싱

------------------------------------------------------

tensor(0.) tensor(1.) tensor(6.)
tensor([2., 3., 4.]) tensor([4., 5.])
tensor([0., 1.]) tensor([3., 4., 5., 6.])

추가적인 내용들은 파이썬 리스트와 넘파이 배열을 다루는 방식과 비슷하기 때문에, 따로 각자 공부하는 걸로 우리끼리 약속하자 😽!

📒 이름이 있는 텐서

이 부분이 중요하기 때문에 짚고 넘어가도록 한다. 우리가 다루는 텐서는 차원이나 축이 있으며, 각 차원은 픽셀 위치나 컬러 채널에 해당된다. 따라서 ! 텐서에 접근을 하기 위해서는 차원의 순서를 기억해서 인덱싱해야하는데, 데이터가 여러 텐서 형태를 거치며 다양하게 반환되면, 어느 차원에 어떤 데이터가 들어있는지 헷갈릴 수 있다.

따라서 코드를 일반화 하는 과정이 중요한데, 이렇게 말하면 어려우니 예를 들어보자.
우리가 해결해야할 미션은 이미지 데이터를 흑백으로 변환해야한다면, 높이와 너비를 가진 2차원 텐서로 이뤄진 흑백 이미지로부터 RGB 값을 담을 세 번째 채널 차원을 더하는 코드로 제작을 한다. (정보를 따로 규칙적으로 추가하는 것 !)

예를 한번 들어보자 !

import torch
_ = torch.tensor([0.2126, 0.7152, 0.0722], names=['c'])
img_t = torch.randn(3, 5, 5) # shape [channels, rows, columns]
weights = torch.tensor([0.2126, 0.7152, 0.0722])

batch_t = torch.randn(2, 3, 5, 5) # shape [batch, channels, rows, columns]
img_gray_naive = img_t.mean(-3)
batch_gray_naive = batch_t.mean(-3)
img_gray_naive.shape, batch_gray_naive.shape

#RGB 채널은 0번 혹은 1번 차원에 존재 - 뒤에서 세 번째 차원이므로 RGB 채널은 0번 혹은 1번 차원에 존재
#두 경우 모두 뒤에서부터 세 번째 차원이므로 RGB 채널은 -3번 차원에 있는 것으로 일반화 가능

지금은 가중치가 존재한다. 파이토치는 동일한 차원 정보의 텐서끼리 연산 가능하며, 각 차원의 길이가 1인 텐서 또한 가능하다 ! 아니면 길이가 1인 차원을 알아서 늘려주기도 하는데, 이러한 방식을 브로드캐스팅(broadcasting) 이라고 한다.
unsqueezed_weight로 곱하면 차원이 어떻게 변화하는지 확인해보자 !

unsqueezed_weights = weights.unsqueeze(-1).unsqueeze_(-1)
img_weights = (img_t * unsqueezed_weights)
batch_weights = (batch_t * unsqueezed_weights)
img_gray_weighted = img_weights.sum(-3)
batch_gray_weighted = batch_weights.sum(-3)
batch_weights.shape, batch_t.shape, unsqueezed_weights.shape

## 결과는
(torch.Size([2, 3, 5, 5]), torch.Size([2, 3, 5, 5]), torch.Size([3, 1, 1]))

여기서 잠깐 !
(2,3,5,5) 뒤에서 세 번째 차원 크기 3은 세개의 값을 의미하며, 각각은 R,G,B 라는 것 !(이미지 데이터니까 ~)

위의 코드를 확인해보면, 확연히 이름을 할당해서 그런지 복잡한 코드로 변신해버렸다. (파이토치는 넘파이에서 차용한 einsum함수 를 제공하여 길이를 줄인다) 파이썬에서는 점 세개(...) 로 변수명 없이 합을 구하는 브로드캐스팅을 사용한다.

img_gray_weighted_fancy = torch.einsum('...chw,c->...hw', img_t, weights)
batch_gray_weighted_fancy = torch.einsum('...chw,c->...hw', batch_t, weights)
batch_gray_weighted_fancy.shape

##결과
torch.Size([2,5,5])

더 간단하게 작성해볼까? tensor 나 rand 같은 텐서 팩토리 함수에 이름으로 사용할 문자열 리스트를 names 인자로 전달할 수 있다.

weights_named = torch.tensor([0.2126, 0.7152, 0.0722], names=['channels'])
weights_named

##결과
tensor([0.2126, 0.7152, 0.0722], names=('channels',))

텐서를 먼저 만들고 나중에 이름을 수정하거나 지정하려면 redefine_names 함수 를 사용한다.

img_named =  img_t.refine_names(..., 'channels', 'rows', 'columns')
batch_named = batch_t.refine_names(..., 'channels', 'rows', 'columns')
print("img named:", img_named.shape, img_named.names)
print("batch named:", batch_named.shape, batch_named.names)

##결과
img named: torch.Size([3, 5, 5]) ('channels', 'rows', 'columns')
batch named: torch.Size([2, 3, 5, 5]) (None, 'channels', 'rows', 'columns')

브로드캐스팅에 대해 알아야 할점 !

  • 텐서끼리의 연산은 먼저 각 차원의 크기가 같은지 혹은 한쪽이 1이고 다른쪽으로 브로드캐스팅 될 수 있는지 확인하기
  • 이름이 지정되어있다면 파이토치가 알아서 체크해준다.
  • 하지만 파이토치가 자동으로 정렬은 안해주기에 명시적으로 브로드캐스팅 작업 수행해야한다.
  • align_as 함수 는 빠진 차원을 채우고, 존재하는 차원은 올바른 순서로 바꿔준다.

📔 텐서의 요소 타입

데이터의 종류가 뭐냐에 따라서 정의되는 텐서의 타입은 달라진다. 이에 대해 어떻게 명시하는지를 확인해볼 필요가 있다.

dtype 으로 숫자 타입 지정하기

tensor 나 zeros,ones 와 같은 텐서 생성자 실행 시 넘겨주는 dtype 인자 로 텐서 내부에 들어갈 데이터 타입을 지정할 수 있다.

이를 통해 텐서가 가질 값이 정수 혹은 부동소수점 수 같은 타입을 지정하고 각 값이 차지하는 바이트 수도 표현할 수 있다.

dtype 인자 타입을 보면 !

이렇게 있으나 텐서의 기본 데이터 타입32비트 부동소수점 이다.

dtype 속성 관리의 경우, 다음 예시 코드와 같이 생성자에 dtype 인자를 정확하게 전달하면된다.

double_points = torch.ones(10, 2, dtype=torch.double)
short_points = torch.tensor([[1, 2], [3, 4]], dtype=torch.short)

short_points.dtype

##결과
torch.int16

추가로, 텐서 생성 함수가 반환하는 텐서의 타입을 대응하는 캐스팅 메소드를 사용하여 올바른 타입으로 변환하는 것도 가능하다.

double_points = torch.zeros(10, 2).double()
short_points = torch.ones(10, 2).short()

double_points = torch.zeros(10, 2).to(torch.double)
short_points = torch.ones(10, 2).to(dtype=torch.short)

#혹은 to 메소드 사용은 더 쉬움 !

텐서 API

텐서끼리의 연산 대부분은 torch모듈 에 존재하며 대부분이 텐서 객체에 대해 메소드처럼 호출할 수 있다.

나머지 범용 텐서 API 는 파이토치 온라인 문서 에 존재하니 한번씩 써보길 바란다!

텐서를 저장소 관점에서 생각해볼까?

텐서의 내부 값은 실제로는 torch.Storage인스턴스 로 관리하며 연속적인 메모리 조각으로 할당된 상태이다. 저장 공간은 숫자 데이터를 가진 1차원 배열이다.

파이토치의 Tensor 객체는 이런 저장 공간을 나타내는 Storage 객체에 대한 뷰 역할을 담당하고, offset을 사용해서 공간의 임의 위치에 접근하거나 특정 차원의 크기를 단위로 해서 접근할 수 있다.

여기서 중요한 점은 서로 다른 방식으로 구성된 텐서가 동일한 메모리 공간을 가리키고 있을 수 있으나, 동일한 데이터에 대해 다른 텐서 뷰를 만드는 작업은 Storage 객체가 관리하는 데이터 크기에 상관없이 빠르게 수행된다는 점이다.

또한 저장된 값을 수정하여 텐서 내부 연산까지 가능하다.

코드는 간단하게 텐서의 저장공간 접근이 어떻게 진행되는지만 살펴보자 !

#텐서 저장 공간 접근

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

#결과

 4.0
 1.0
 5.0
 3.0
 2.0
 1.0
[torch.storage.TypedStorage(dtype=torch.float32, device=cpu) of size 6]
#텐서를 거치지 않고 저장 공간을 직접 접근하는 방법 

points_storage = points.storage()
points_storage[0]

#결과
4.0

👻 텐서 메타데이터 : 사이즈, 오프셋, 스트라이드

저장 공간을 인덱스로 접근하기 위해 텐서는 저장 공간에 포함된 몇 가지 명확한 정보, 즉 사이즈(size), 오프셋(offset), 스트라이드(stride) 에 의존한다.

  • 사이즈(size) : 텐서의 각 차원 별로 들어가는 요소의 수를 표시한 튜플
  • 오프셋(offset) : 텐서의 첫 번째 요소를 가리키는 색인 값과 동일
  • 스트라이드(stride) : 각 차원에서 다음 요소를 가리키고 싶을 때 실제 저장 공간상에서 몇 개의 요소를 건너뛰어야하는지를 알려주는 요소

이 세가지, 정말정말 중요하니까 개념을 확실히 알고 가는 것이 좋다.

세가지를 활용한 텐서 이리저리 만져보는 학습을 진행해볼까?

학습 시 집중해야할 방식들

  • 복사 없이 텐서 전치하는 방법
  • 다른 텐서의 저장 공간에 대한 뷰 만드는 방법
  • 스트라이드, 사이즈, 오프셋이 어떻게 코딩으로 작성될까?
  • 높은 차원에서의 전치 연산 방법?
  • 인접한 텐서는 어떻게 정의될까?

텐서 이리저리 만져보는 코드 둘러보기 !

텐서 직렬화

파이토치는 텐서 객체를 직렬화하기 위하여 내부적으로 pickle 을 사용하며, 저장 공간을 위한 전용 직렬화 코드를 가지고 있다.

마무리

3장을 학습하면서 텐서를 어떻게 코드로 다루는지, 어떻게 바꿔가면서 사용하는지를 전반적으로 학습할 수 있었다. 4장에서 이렇게 배운 기본 텐서를 가지고 이미지 처리 방식에 대해서 들어가보자 !

profile
어서오세요.

0개의 댓글