[PyTorch Basic] 텐서 조작하기(Tensor Manipulation) 1
1. Vector, Matrix and Tensor
1) 벡터, 행렬, 텐서 그림으로 이해하기
딥러닝을 하게 되면 다루게 되는 가장 기본적인 단위는 벡터, 행렬, 텐서다. 차원이 없는 값을 스칼라, 1차원으로 구성된 값을 우리는 벡터라고 한다.
2차원으로 구성된 값을 행렬(Matrix)라고 하며 3차원이 되면 텐서(tensor)라고 부른다. 4차원 이상부터는 3차원의 텐서를 위로 쌓아 올린 모습, 5차원은 그 4차원을 다시 옆으로 확장한 모습, 6차원은 5차원을 뒤로 확장한 모습으로 볼 수 있다.
2) PyTorch Tensor Shape Convention
딥러닝을 할 때 다루고 있는 행렬 또는 텐서의 크기를 고려하는 것은 항상 중요하다. 행렬과 텐서의 크기를 표현할 때 다음과 같은 방법으로 표기한다.
* 2D Tensor(Typical Simple Setting)
|t| = (Batch size, dim)
아래의 그림과 같이 행렬에서 행의 크기가 batch size, 열의 크기가 dim이라는 의미다.
* 3D Tensor(Typical Computer Vision) - 비전 분야에서의 3차원 텐서
|t| = (batch size, width, height)
일반적으로 자연어처리보다 비전 분야(이미지, 영상 처리)를 할 경우 좀 더 복잡한 텐서를 다루게 된다. 이미지는 가로, 세로가 존재한다. 그리고 여러 장의 이미지, 즉 batch size로 구성하게 되면 아래와 같이 3차원 텐서가 된다.
* 3D Tensor(Typical Natural Language Processing) - NLP 분야에서의 3차원 텐서
|t| = (batch size, length, dim)
자연어 처리는 보통 (batch size, 문장 길이, 단어 벡터의 차원)이라는 3차원 텐서를 사용한다.
* NLP 분야의 3D 텐서 예제로 이해하기
아래와 같이 4개의 문장으로 구성된 전체 훈련 데이터가 있다.
[[나는 사과를 좋아해], [나는 바나나를 좋아해], [나는 사과를 싫어해], [나는 바나나를 싫어해]]
컴퓨터는 아직 이 상태로는 '나는 사과를 좋아해'가 단어가 1개인지 3개인지 이해하지 못한다. 컴퓨터의 입력으로 사용하기 위해서는 우선 단어별로 나눠줘야한다.
[['나는', '사과를', '좋아해'], ['나는', '바나나를', '좋아해'], ['나는', '사과를', '싫어해'], ['나는', '바나나를', '싫어해']]
[['나는', '사과를', '좋아해'],
['나는', '바나나를', '좋아해'],
['나는', '사과를', '싫어해'],
['나는', '바나나를', '싫어해']]
이제 훈련 데이터의 크기는 4 x 3의 크기를 가지는 2D 텐서다. 컴퓨터는 텍스트보다 숫자를 더 잘 처리할 수 있다. 이제 각 단어를 아래와 같이 3차원 벡터로 변환했다고 가정한다.
'나는' = [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]]]
[[[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]]]
이제 훈련 데이터는 4 x 3 x 3의 크기를 가지는 3D 텐서다. 이제 batch size를 2로 해본다.
첫번째 배치 #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]]]
컴퓨터는 배치 단위로 가져가서 연산을 수행한다. 그리고 현재 각 배치의 텐서 크기는 (2 x 3 x 3)이다. 이는 (batch size, 문장의 길이, 벡터의 차원)의 크기다
2. 넘파이로 텐서 만들기(벡터와 행렬 만들기)
PyTorch로 텐서를 만들어보기 전에 우선 Numpy로 텐서를 만들어본다. 우선 numpy를 import한다.
import numpy as np
Numpy로 텐서를 만드는 방법은 간단하다. list형식으로 만들고 이를 np.array()
로 감싸주면 된다.
1) 1D with Numpy
Numpy로 1차원 텐서인 벡터를 만들어본다.
t = np.array([0., 1., 2., 3., 4., 5., 6.])
print(t)
[0. 1. 2. 3. 4. 5. 6.]
이제 1차원 텐서인 벡터의 차원과 크기를 출력해본다.
print('Rank of t: ', t.ndim)
print('Shape of t: ', t.shape)
Rank of t: 1
Shape of t: (7,)
.ndim
: 몇 차원인지 출력한다..shape
: 크기를 출력한다. (7,)는 (1,7)을 의미한다. 다시 말해 (1 x 7)의 크기를 가지는 벡터다.
1-1) Numpy 기초 이해하기
이제 Numpy 에서 각 벡터의 원소에 접근하는 방법을 알아본다. Numpy에서 인덱스는 0부터 시작한다.
print('t[0] t[1] t[-1] = ', t[0], t[1], t[-1])
t[0] t[1] t[-1] = 0.0 1.0 6.0
위의 결과는 0번 인덱스를 가진 원소인 0.0, 1번 인덱스를 가진 원소인 1.0, -1번 인덱스를 가진 원소인 6.0이 출력되는 것을 보여준다. -1번 인덱스는 맨 뒤에서부터 시작하는 인덱스다.
범위 지정으로 원소를 불러올 수 있는데 이를 슬라이싱(Slicing)이라고 한다. 사용 방법은 [시작 번호 : 끝 번호]를 통해 사용한다. 주의할 점은 끝 번호에 해당하는 것은 포함되지 않는다. 또한, 시작 번호 또는 끝 번호를 생략해서 슬라이싱 하기도 한다.
print('t[2:5] t[4:-1] = ', t[2:5], t[4:-1])
t[2:5] t[4:-1] = [2. 3. 4.] [4. 5.]
print('t[:2] t[3:] = ', t[:2], t[3:])
t[:2] t[3:] = [0. 1.] [3. 4. 5. 6.]
2) 2D with Numpy
Numpy로 2차원 행렬을 만들어 본다.
t = np.array([[1., 2., 3.], [4., 5., 6.], [7., 8., 9.], [10., 11., 12.]])
print(t)
[[ 1. 2. 3.]
[ 4. 5. 6.]
[ 7. 8. 9.]
[10. 11. 12.]]
print('Rank of t: ', t.ndim)
print('Shape of t: ', t.shape)
Rank of t: 2
Shape of t: (4, 3)
3. 파이토치 텐서 선언하기 ( PyTorch Tensor Allocation)
파이토치는 Numpy와 매우 유사하지만 장점이 더 많다. 우선 torch를 import한다.
import torch
Numpy를 사용하여 진행했던 실습을 파이토치로 똑같이 해본다.
1) 1D with PyTorch
파이토치로 1차원 텐서인 벡터를 만들어본다.
t = torch.FloatTensor([0., 1., 2., 3., 4., 5., 6.])
print(t)
tensor([0., 1., 2., 3., 4., 5., 6.])
dim()
을 사용하면 현재 텐서의 차원을 보여준다. shape
나 size()
를 사용하면 크기를 확인할 수 있다.
print(t.dim())
print(t.shape)
print(t.size())
1
torch.Size([7])
torch.Size([7])
현재 1차원 텐서이며, 원소는 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) 2D with PyTorch
파이토치로 2차원 텐서인 행렬을 만들어본다.
t = torch.FloatTensor([[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.],
[10., 11., 12.]
])
print(t)
tensor([[ 1., 2., 3.],
[ 4., 5., 6.],
[ 7., 8., 9.],
[10., 11., 12.]])
print(t.dim())
print(t.size())
2
torch.Size([4, 3])
print(t[:,1])
print(t[:,1].size())
tensor([ 2., 5., 8., 11.])
torch.Size([4])
print(t[:,:-1])
tensor([[ 1., 2.],
[ 4., 5.],
[ 7., 8.],
[10., 11.]])
3) 브로드캐스팅(Broadcasting)
두 행렬 A, B가 있다고 할 때 덧셈과 뺄셈을 하려면 두 행렬 A, B의 크기가 같아야 한다. 그리고 두 행렬이 곱셈을 할 때는 A의 마지막 차원과 B의 첫번째 차원이 일치해야한다.
하지만 딥러닝을 하게 되면 불가피하게 크기가 다른 행렬 또는 텐서에 대해서 사칙 연산을 수행해야될 경우가 생긴다. 이를 위해 PyTorch에서는 자동으로 크기를 맞춰서 연산을 수행하게 만드는 브로드캐스팅(Broadcasting)이라는 기능을 제공한다.
우선 같은 크기일 때 연산을 하는 경우를 본다.
m1 = torch.FloatTensor([[3, 3]])
m2 = torch.FloatTensor([[2, 2]])
print(m1 + m2)
tensor([[5., 5.]])
m1, m2의 크기가 둘 다 (1, 2)라 문제 없이 덧셈 연산이 가능했다. 이번에는 크기가 다른 텐서들 간의 연산을 해본다. 아래는 벡터와 스칼라가 덧셈을 수행하는 것을 보여준다. 물론, 수학적으로는 원래 연산이 안되는게 맞지만 파이토치에서는 브로드캐스팅을 통해 이를 연산한다.
m1 = torch.FloatTensor([[1, 2]])
m2 = torch.FloatTensor([3]) # [3] -> [3, 3]
print(m1 + m2)
tensor([[4., 5.]])
이번에는 벡터 간 연산에서 브로드캐스팅이 적용되는 경우를 본다.
# 2 x 1 Vector + 1 x 2 Vector
m1 = torch.FloatTensor([[1, 2]])
m2 = torch.FloatTensor([[3],[4]])
print(m1 + m2)
tensor([[4., 5.],
[5., 6.]])
# 브로드캐스팅 과정에서 실제로 두 텐서가 어떻게 변경되는지
[1, 2]
==> [[1, 2],
[1, 2]]
[3]
[4]
==> [[3, 3],
[4, 4]]
브로드캐스팅은 편리하지만, 자동으로 실행되는 기능이므로 사용자 입장에서 주의해서 사용해야된다.
4) 자주 사용되는 기능들
1) 행렬 곱셈과 곱셈의 차이 (Matrix Multiplication vs. Multiplication)
행렬로 곱셈을 하는 방법은 크게 두 가지가 있다. 행렬 곱셈과 원소 별 곱셈이다.
파이토치 텐서의 행렬 곱셈은 matmul()
을 통해 수행한다.
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
Shape of Matrix 1: torch.Size([2, 2])
Shape of Matrix 2: torch.Size([2, 1])
tensor([[ 5.],
[11.]])
위의 결과는 2 x 2 행렬과 2 x 1의 행렬(벡터) 곱셈의 결과를 보여준다.
행렬 곱셈이 아니라 element-wise 곱셈이라는 것이 존재한다. 이는 동일한 위치에 있는 원소끼리 곱하는 것을 말한다. 아래는 서로 다른 크기의 행렬이 브로드캐스팅이 된 후에 element-wise 곱셈이 수행되는 것을 보여준다. 이는 *
또는 mul()
을 통해 수행한다.
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 x2
print(m1.mul(m2))
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.]])
m1 행렬의 크기는 (2,2)고 m2 행렬의 크기는 (2, 1)이다. 이때 element-wise 곱셈을 수행하면, 두 행렬의 크기는 브로드캐스팅이 된 후에 곱셈이 수행된다.
[1]
[2]
==> [[1, 1],
[2, 2]]
2) 평균(Mean)
평균을 구하는 방법도 제공하고 있다. 이는 Numpy에서의 사용법과 매우 유사하다.
t = torch.FloatTensor([1, 2])
print(t.mean())
tensor(1.5000)
t = torch.FloatTensor([[1, 2], [3, 4]])
print(t)
tensor([[1., 2.],
[3., 4.]])
print(t.mean())
tensor(2.5000)
4개 원소의 평균인 2.5가 나왔다. 이번에는 dim. 즉, 차원(dimension)을 인자를 주는 경우를 본다.
print(t.mean(dim=0))
tensor([2., 3.])
dim=0이라는 것은 첫번째 차원을 의미한다. 행렬에서 첫번째 차원은 '행'을 의미한다. 그리고 인자로 dim을 준다면 해당 차원을 제거한다는 의미가 된다. 다시 말해 행렬에서 '열'만 남기겠다는 의미가 된다. 기존 행렬의 크기는 (2, 2)였지만 이를 수행하면 열의 차원만 보존되면서 (1, 2)가 된다. 이는 (2, )와 같은 벡터다.
# 실제 연산 과정
t.mean(dim=0)
[[1., 2.],
[3., 4.]]
1과 3의 평균을 구하고, 2와 4의 평균을 구한다
결과 ==> [2., 3.]
이번에는 인자로 dim=1을 준다. 이번에는 두번째 차원을 제거한다.
print(t.mean(dim=1))
tensor([1.5000, 3.5000])
열의 차원이 제거되어야 하므로 (2,2)의 크기에서 (2,1)의 크기가 된다. 이번에는 1과 3의 평균을 구하고 3과 4의 평균을 구하게 된다. 하지만 (2 x 1)은 결국 1차원이므로 (1 x 2)와 같이 표현되면서 위와 같이 [1.5, 3.5]로 출력된다. dim=-1로 주는 경우 마지막 차원을 제거한다는 의미고, 결국 열의 차원을 제거한다는 의미와 같다.
print(t.mean(dim=-1))
tensor([1.5000, 3.5000])
3) 덧셈(Sum)
덧셈(Sum)은 평균(Mean)과 연산 방법이나 인자가 의미하는 바는 정확히 동일하다. 다만, 평균이 아니라 덧셈을 할 뿐이다.
t = torch.FloatTensor([[1, 2], [3, 4]])
print(t)
tensor([[1., 2.],
[3., 4.]])
print(t.sum())
print(t.sum(dim=0))
print(t.sum(dim=1))
print(t.sum(dim=-1))
tensor(10.)
tensor([4., 6.])
tensor([3., 7.])
tensor([3., 7.])
4) 최대(Max)와 아그맥스(ArgMax)
Max는 원소의 최대값을 리턴하고, argmax는 최대값을 가진 인덱스를 리턴한다.
(2, 2) 크기의 행렬을 선언하고 Max를 사용해본다.
t = torch.FloatTensor([[1, 2], [3, 4]])
print(t)
tensor([[1., 2.],
[3., 4.]])
우선 (2, 2) 행렬에서 .max()
를 사용해본다.
print(t.max())
tensor(4.)
이번에는 인자로 dim=0을 준다. 첫번째 차원을 제거한다는 의미다.
print(t.max(dim=0))
torch.return_types.max(
values=tensor([3., 4.]),
indices=tensor([1, 1]))
행의 차원을 제거한다는 의미로 (1,2) 텐서를 만든다. 결과는 [3, 4]이다.
그런데 [1, 1]이라는 값도 함께 리턴되었는데 max에 dim 인자를 주면 argmax도 함께 리턴하는 특징 때문이다. 첫번째 열에서 3의 인덱스는 1이었다. 두번째 열에서 4의 인덱스는 1이었다. 그러므로 [1, 1]이 리턴된다.
만약 두 개를 함께 리턴받는 것이 아니라 max또는 argmax만 리턴받고 싶다면 다음과 같이 리턴값에 인덱스를 부여하면된다.
print('Max: ', t.max(dim=0)[0])
print('Argmax: ', t.max(dim=0)[1])
Max: tensor([3., 4.])
Argmax: tensor([1, 1])
print(t.max(dim=1))
print(t.max(dim=-1))
torch.return_types.max(
values=tensor([2., 4.]),
indices=tensor([1, 1]))
torch.return_types.max(
values=tensor([2., 4.]),
indices=tensor([1, 1]))