이미지나 텍스트 같은 비정형의 데이터를 처리하기 위해서는 여러 가지 형태의 데이터로 만드는 것이 필요합니다.
부동소수점 수로 변환하는 것이 그 예시입니다.
입력데이터를 부동소수점 수로 변환하면 딥러닝 프로세스에서도 비슷한 표현을 가질 것입니다.
입력 데이터가 딥러닝 프로세스에 들어가기 전에 어떻게 인코딩 되어야 하고, 출력으로 나온 결괏값을 우리가 해석할 수 있도록 디코딩하는 것이 필요합니다.
입력 데이터를 부동소수점으로 바꾸기 전에 입력과 중간 표현, 출력에서 어떻게 데이터를 다루는지 알 필요가 있습니다.
이때 등장하는 것이 텐서(Tensor)입니다.
텐서란 다차원 배열을 의미합니다. 즉, 임의의 차원을 가지는 벡터나, 행렬의 일반화된 개념이라고 생각하면 됩니다.
이를 정리한 포스트가 있으니 참고해 주세요!
신경망 이해를 위한 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을 살펴보면 위의 행렬이 텐서이며 밑의 리스트가 저장 공간이라고 생각하면 됩니다.
우선, 텐서는 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
이번 포스팅에서는 파이토치에서 다루는 텐서의 개념과 어떻게 효율적으로 텐서가 작동하는지에 대해 알아보았습니다.
'Book Review > [파이토치 딥러닝 마스터] 리뷰' 카테고리의 다른 글
파이토치: 데이터 적합(훈련하기) (1) | 2023.05.12 |
---|---|
Data to Tensor: 이미지, 테이블, 시계열 , 텍스트 데이터를 텐서로 (0) | 2023.04.05 |
모델 학습 기법 기초 (0) | 2023.04.05 |
딥러닝과 파이토치 (0) | 2023.03.21 |