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