AI/딥러닝

[Pytorch] 텐서(Tensor) 다루기

daeunnniii 2023. 3. 26. 18:32
728x90
반응형

1. Pytorch 패키지의 기본 구성

1) torch: 메인 네임스페이스. 텐서 등 다양한 수학 함수가 포함되어있으며 numpy와 유사한 구조를 가진다.

2) torch.autograd: 자동 미분을 위한 함수들이 포함. 자동 미분자동 미분의 on/off를 제어하는 콘텍스트 매니저(enable_grad/no_grad)나 자체 미분 가능 함수를 정의할 때 사용하는 기반 클래스인 'Function' 등이 포함되어 있다.

3) torch.nn: 신경망을 구축하기 위한 다양한 데이터 구조나 레이어 등이 정의되어있다. 예를 들어 RNN, LSTM과 같은 레이어, ReLU와 같은 활성화 함수, MSELoss와 같은 손실 함수들이 있다.

4) torch.optim: 확률적 경사 하강법(Stochastic Gradient Descent, SGD)를 중심으로 한 파라미터 최적화 알고리즘이 구현되어있다.

5) torch.utils.data: SGD의 반복 연산을 실행할 때 사용하는 미니 배치용 유틸리티 함수가 포함되어있다.

6) torch.onnx: ONNX(Open Neural Network Exchange)의 포맷으로 모델을 익스포트(export)할 때 사용한다. ONNX는 서로 다른 딥러닝 프레임워크 간에 모델을 공유할 때 사용하는 포맷이다.

 

2. 벡터, 행렬 그리고 텐서(Vector, Matrix and Tensor)

딥러닝에서 다루는 가장 기본적인 단위는 벡터, 행렬, 텐서이다.

- 차원이 없는 값을 스칼라, 1차원으로 구성된 값을 "벡터"라고 한다.

- 2차원으로 구성된 값은 행렬(Matrix)라고 한다.

- 3차원이 되면 텐서(Tensor)라고 부른다.

 

사실 우리는 3차원의 세상에 살고 있으므로, 4차원 이상부터는 머리로 생각하기는 어렵다. 4차원은 3차원의 텐서를 위로 쌓아 올린 모습으로 상상해보자. 그리고 5차원은 그 4차원을 다시 옆으로 확장한 모습으로 생각해볼 수 있다.

 

데이터사이언스 분야 한정으로 3차원 이상의 텐서는 그냥 다차원 행렬 또는 배열로 간주할 수 있다. 또한 주로 3차원 이상을 텐서라고 하긴 하지만, 1차원 벡터나 2차원인 행렬도 텐서라고 표현하기도 한다. 벡터 = 1차원 텐서, 2차원 행렬 = 2차원 텐서. 그리고 3차원 텐서, 4차원 텐서, 5차원 텐서 등...

 

3. Pytorch Tensor Shape Convention

딥러닝을 할 때 다루고 있는 행렬 또는 텐서의 크기를 고려하는 것은 항상 중요하다.

1) 2차원 Tensor

|t| = (Batch size, dim)

훈련 데이터 하나의 크기를 256이라고 하자. 즉, [1, 2, 5, 3, ...] 숫자들의 나열이 256의 길이로 있고 벡터의 차원은 256이다. 만약 이러한 훈련 데이터의 개수가 3000개라고 한다면 전체 훈련 데이터의 크기는 3000 x 256이다. 훈련 데이터를 3000개에서 64개씩 꺼내서 처리한다고 한다면 이때 batch size를 64라고 한다. 그렇다면 컴퓨터가 한번에 처리하는 2D 텐서의 크기는 (batch size x dim) = 64 x 256 이다.

2) 3차원 Tensor

|t| = (batch size, width, height)

일반적으로 자연어 처리보다 이미지, 영상 처리 등 비전 분야에서는 좀 더 복잡한 텐서를 다루게 된다.

이미지는 가로, 세로라는 것이 존재한다. 그리고 여러 장의 이미지 즉, batch size로 구성하게 되면 다음과 같이 3차원의 텐서가 된다.

아래 4개 문장으로 구성된 전체 훈련 데이터가 있다. 컴퓨터는 아직 이 상태로 '나는 사과를 좋아해'가 단어 1개인지 3개인지 이해하지 못한다. 컴퓨터의 입력으로 사용하기 위해서는 단어별로 나누어주어야한다.

[[나는 사과를 좋아해], [나는 바나나를 좋아해], [나는 사과를 싫어해], [나는 바나나를 싫어해]]

이제 훈련 데이터의 크기는 4x3 크기를 가지는 2D 텐서이다.

각 단어를 벡터로 만들어 3차원의 벡터로 변환하여 훈련 데이터를 재구성하면 4x3x3의 크기를 가지는 3D 텐서가 된다.

'나는' = [0.1, 0.2, 0.9]
'사과를' = [0.3, 0.5, 0.1]
'바나나를' = [0.3, 0.5, 0.2]
'좋아해' = [0.7, 0.6, 0.5]
'싫어해' = [0.5, 0.6, 0.7]

[[[0.1, 0.2, 0.9], [0.3, 0.5, 0.1], [0.7, 0.6, 0.5]],
 [[0.1, 0.2, 0.9], [0.3, 0.5, 0.2], [0.7, 0.6, 0.5]],
 [[0.1, 0.2, 0.9], [0.3, 0.5, 0.1], [0.5, 0.6, 0.7]],
 [[0.1, 0.2, 0.9], [0.3, 0.5, 0.2], [0.5, 0.6, 0.7]]]

batch size를 2로 설정하면 컴퓨터는 배치 단위로 가져가 연산을 수행한다. 현재 각 배치의 텐서 크기는 (2 x 3 x 3) 이다. 이는 (batch size, 문장 길이, 단어 벡터의 차원) 이다.

첫번째 배치 #1
[[[0.1, 0.2, 0.9], [0.3, 0.5, 0.1], [0.7, 0.6, 0.5]],
 [[0.1, 0.2, 0.9], [0.3, 0.5, 0.2], [0.7, 0.6, 0.5]]]

두번째 배치 #2
[[[0.1, 0.2, 0.9], [0.3, 0.5, 0.1], [0.5, 0.6, 0.7]],
 [[0.1, 0.2, 0.9], [0.3, 0.5, 0.2], [0.5, 0.6, 0.7]]]

 

4. 텐서 만들기

Pytorch는 Numpy와 매우 유사하다. Numpy로도 3차원 텐서를 만들 수는 있지만 Pytorch의 장점이 더 많다.

t = np.array([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.], [10., 11., 12.]])
print('Rank  of t: ', t.ndim)
print('Shape of t: ', t.shape)

>> Result
Rank  of t:  2
Shape of t:  (4, 3)

 

5. Pytorch Tensor 다루기

1) Pytorch Tensor 다루기

import torch
import numpy as np

torch.empty(5, 4) # 초기화되지 않은 5x4 빈 행렬 생성
torch.ones(3, 3) # 3x3 일행렬 생성
torch.zeros(2)  # 2행 영벡터 생성
torch.rand(5, 6)  # 5x6 랜덤 행렬 생성

# 2차원 텐서인 행렬 생성
t = torch.FloatTensor([[1., 2., 3.],
                       [4., 5., 6.],
                       [7., 8., 9.],
                       [10., 11., 12.]
                      ]) 
print(t.dim())  # 텐서 t의 차원
print(t.shape)  # 텐서 t의 크기: (4, 3)
print(t.size()) # 텐서 t의 크기: (4, 3)
type(t)  # t의 type 출력

print()
print(t[:, 1]) # 슬라이싱: [첫번째 차원의 범위, 두번째 차원의 범위]
print(t[:, 1].size())

>> Result:
2
torch.Size([4, 3])
torch.Size([4, 3])
torch.Tensor

tensor([ 2.,  5.,  8., 11.])
torch.Size([4])

2) 리스트, 넘파이 배열을 텐서로 만들기

torch.tensor([1, 2]) # 리스트를 텐서로 변환
torch.tensor(np.array([1, 2])) # 넘파이 배열을 텐서로 변환
x = torch.rand(2, 2)
x.numpy() # 텐서를 넘파이 배열로 변환

3) 텐서의 덧셈

x = torch.rand(2, 2)
y = torch.rand(2, 2)
torch.add(x, y) # y.add(x) 또는 x + y
y.add_(x) # y에 x+y값이 새로 대체된다

4) 텐서의 크기 변환하기

x = torch.rand(8, 8)
a = x.view(-1, ) # 2차원 -> 1차원 / 크기: 8x8 -> ? = 64
b = x.view(-1, 4, 4) # 2차원 -> 3차원 / 크기: 8x8 -> ?x4x4 = 4x4x4

5) 스퀴즈(Squeeze)와 언스퀴즈(Unsqueeze)

스퀴즈(Squeeze)는 차원이 1인 경우에는 해당 차원을 제거한다. 아래 예시에서 두번째 차원이 1이므로 squeeze를 사용하면 (3,)의 크기를 가지는 텐서로 변경된다.

ft = torch.FloatTensor([[0], [1], [2]])
print(ft.shape)
print(ft.squeeze())
print(ft.squeeze().shape)

>> Result:
torch.Size([3, 1])
tensor([0., 1., 2.])
torch.Size([3])

언스퀴즈(Unsqueeze)는 스퀴즈(Squeeze)와 정반대로, 특정 위치에 1인 차원을 추가한다.

ft = torch.Tensor([0, 1, 2])
print(ft.shape)
print(ft.unsqueeze(0)) # 인덱스가 0부터 시작하므로 0은 첫번째 차원을 의미한다.
print(ft.unsqueeze(0).shape)

print()
print(ft.unsqueeze(1))
print(ft.unsqueeze(1).shape)

>> Result:
torch.Size([3])
tensor([[0., 1., 2.]])
torch.Size([1, 3])

tensor([[0.],
        [1.],
        [2.]])
torch.Size([3, 1])

6) 연결하기(concatenate)와 스택킹(Stacking)

x = torch.FloatTensor([[1, 2], [3, 4]])
y = torch.FloatTensor([[5, 6], [7, 8]])
print(torch.cat([x, y], dim=0))

>> Result:
tensor([[1., 2.],
        [3., 4.],
        [5., 6.],
        [7., 8.]])
        
x = torch.FloatTensor([1, 4])
y = torch.FloatTensor([2, 5])
z = torch.FloatTensor([3, 6])        
print(torch.stack([x, y, z]))

>> Result:
tensor([[1., 4.],
        [2., 5.],
        [3., 6.]])

7) zeros_like와 ones_like: 0으로 채워진 텐서와 1로 채워진 텐서

ones_like를 하면 동일한 크기(shape)지만 1으로만 값이 채워진 텐서를 생성한다.

마찬가지로 zeros_like를 하면 동일한 크기(shape)지만 0으로만 값이 채워진 텐서를 생성한다.

x = torch.FloatTensor([[0, 1, 2], [2, 1, 0]])
print(torch.ones_like(x))
print(torch.zeros_like(x))

>> Result:
tensor([[1., 1., 1.],
        [1., 1., 1.]])
tensor([[0., 0., 0.],
        [0., 0., 0.]])

8) 단일 텐서에서 값으로 뽑아내기

x = torch.ones(1)
print(x.item()) # 텐서가 아닌 값 자체를 출력

>> Result:
1.0

 

추가. 브로드캐스팅(Broadcasting)

두 행렬 A, B를 덧셈 혹은 뺄셈할 때 두 행렬의 크기가 같아야한다. 그리고 곱셈을 할 때는 A의 마지막 차원과 B의 첫번째 차원이 일치해야한다.

하지만 딥러닝을 하게 되면 불가피하게 크기가 다른 행렬 또는 텐서에 대해서 사칙 연산을 수행할 필요가 있는 경우가 생긴다. 이를 위해 Pytorch에서는 자동으로 크기를 맞춰서 연산을 수행하게 만드는 브로드캐스팅 기능을 제공한다.

# Example 1. Vector + scalar
m1 = torch.FloatTensor([[1, 2]])
m2 = torch.FloatTensor([3]) # [3] -> [3, 3]
print(m1 + m2)

>> Result:
tensor([[4., 5.]])

# Example 2. 2 x 1 Vector + 1 x 2 Vector
m1 = torch.FloatTensor([[1, 2]])
m2 = torch.FloatTensor([[3], [4]])
print(m1 + m2)

>> Result:
tensor([4., 5.],
       [5., 6.]])
       
#### Example 2에서 브로드캐스팅 과정 ####
[1, 2]
==> [[1, 2],
     [1, 2]]
[3]
[4]
==> [[3, 3],
     [4, 4]]

 

 

6. 자주 사용되는 기능 정리

1) 행렬 곱셈

2x2 행렬과 2x1 행렬(벡터)의 행렬 곱셈의 결과이다.

m1 = torch.FloatTensor([[1, 2], [3, 4]])
m2 = torch.FloatTensor([[1], [2]])

print('Shape of Matrix 1: ', m1.shape) # 2 x 2
print('Shape of Matrix 2: ', m2.shape) # 2 x 1
print(m1.matmul(m2)) # 2 x 1

>> Result:
Shape of Matrix 1:  torch.Size([2, 2])
Shape of Matrix 2:  torch.Size([2, 1])
tensor([[ 5.],
        [11.]])

추가로, 행렬 곱셈이 아니라 동일한 크기의 행렬이 동일한 위치에 있는 원소끼리 곱하는 element-wise 곱셈을 할 때는 * 또는 mul()을 사용한다. 아래 예시는 두 행렬의 크기가 브로드캐스팅 된 후 element-wise 곱셈을 수행하는 과정이다.

m1 = torch.FloatTensor([[1, 2], [3, 4]])
m2 = torch.FloatTensor([[1], [2]])
print('Shape of Matrix 1: ', m1.shape) # 2 x 2
print('Shape of Matrix 2: ', m2.shape) # 2 x 1
print(m1 * m2) # 2 x 2
print(m1.mul(m2))

>> Result:
Shape of Matrix 1:  torch.Size([2, 2])
Shape of Matrix 2:  torch.Size([2, 1])
tensor([[1., 2.],
        [6., 8.]])
tensor([[1., 2.],
        [6., 8.]])
        
        
# 브로드캐스팅 과정에서 m2 텐서의 변환
[1]
[2]
==> [[1, 1],
     [2, 2]]

 

2) 평균(Mean)

차원 dim을 인자로 주는 경우, dim=0은 첫번째 차원을 의미한다. 즉, 행렬에서 첫번째 차원은 '행'을 의미한다.

기존 행렬의 크기는 (2, 2)였지만 t.mean(dim=0)을 수행하면 열의 차원만 보존되면서 (1, 2)가 된다. 마찬가지로  t.mean(dim=1)을 수행하면 열의 차원만 보존되면서 (2, 1)의 크기가 된다.

dim=-1을 주는 경우에는 마지막 차원을 제거한다는 의미이므로 결국 행렬에서 열의 차원을 제거한다는 의미이다. 그러므로 dim=1과 출력 결과가 같다.

t = torch.FloatTensor([[1, 2], [3, 4]])
print(t.mean())
print(t.mean(dim=0))
print(t.mean(dim=1))
print(t.mean(dim=-1))

>> Result:
tensor(2.5000)
tensor([2., 3.])
tensor([1.5000, 3.5000])
tensor([1.5000, 3.5000])

 

3) 덧셈(Sum)

t = torch.FloatTensor([[1, 2], [3, 4]])
print(t.sum())    # 단순히 원소 전체의 덧셈을 수행
print(t.sum(dim=0)) # 행을 제거
print(t.sum(dim=1)) # 열을 제거
print(t.sum(dim=-1)) # 열을 제거

>> Result:
tensor(10.)
tensor([4., 6.])
tensor([3., 7.])
tensor([3., 7.])

 

4) 최대(Max)와 아그맥스(ArgMax)

max는 원소의 최댓값을 리턴하고, argmax는 최댓값을 가진 인덱스를 리턴한다.

max에 dim 인자를 주면 argmax도 함께 리턴한다.

t = torch.FloatTensor([[1, 2], [3, 4]])
print(t.max())
print(t.max(dim=0))

>> Result:
tensor(4.)
(tensor([3., 4.]), tensor([1, 1]))

만약 두 개를 함께 리턴받는 것이 아니라 max 또는 argmax만 리턴받고 싶다면 다음과 같이 리턴값에 인덱스를 부여하면 된다.

t = torch.FloatTensor([[1, 2], [3, 4]])
print('Max: ', t.max(dim=0)[0])
print('Argmax: ', t.max(dim=0)[1])

>> Result:
Max:  tensor([3., 4.])
Argmax:  tensor([1, 1])

 

  • 참고: 위키독스 Pytorch로 시작하는 딥러닝 입문
728x90
반응형