pytorch tensor 다루기 재활치료 1편 -tensor, broadcasting, max, argmax -

1. 벡터, 행렬, 텐서

 

 

차원이 없는 값을 스칼라

 

1차원으로 구성된 값을 벡터(vector)

 

2차원으로 구성된 값을 행렬(matrix)

 

3차원 이상으로 구성된 값은 텐서(tensor)

 

인간은 3차원 세상에 살고 있다보니 4차원 이상부터는 머리로 상상하기 어렵다.

 

데이터사이언스에서는 3차원 이상의 텐서는 다차원 행렬이나 배열로 간주할 수 있다.

 

또한 주로 3차원 이상을 텐서라고 하며, 1차원 벡터나 2차원 행렬도 텐서라고 표현하기도 한다.

 

1차원 벡터 = 1차원 텐서, 2차원 행렬 = 2차원 텐서, ......

 

 

2. tensor의 shape를 표현하는 방법?

 

2-1) 2차원 tensor

 

 

2차원 텐서의 크기를 (batch size * dim)으로 표현할 수 있다

 

훈련 데이터 하나의 크기를 256이라고 하자. [3,1,2,5,...] 이런 숫자들의 나열이 256의 길이로 있다고 상상해보자.

 

훈련데이터 하나 = 벡터 차원 256이다.

 

이런 훈련데이터가 3000개 있다고 한다면, 전체 훈련 데이터의 크기는 3000*256개이다.

 

이는 행렬이니까 2d tensor가 된다.

 

3000개를 1개씩 꺼내 처리하는 것도 가능하지만, 훈련데이터를 하나씩 처리하는 것보다 덩어리로 처리하는 것이 보통이다.

 

3000개에서 64개씩 꺼내 처리한다고 하면 이때 batch size를 64라고 한다.

 

따라서 컴퓨터가 한번에 처리하는 2d tensor의 크기는 (batch size * dim) = 64 * 256이다.

 

 

2-2) 컴퓨터 비전에서 3차원 tensor

 

 

이미지라는 것은 가로, 세로가 존재한다.

 

여러 장의 이미지, batch size로 구성한다면 위와 같은 3차원 tensor가 된다.

 

가로,세로 이미지를 위에서부터 아래로 batch size개만큼 쌓은 그런 형태인듯

 

 

2-3) NLP에서 3차원 tensor

 

 

NLP에서 tensor는 보통 (batch size, 문장의 길이(length), 단어 벡터의 차원(dim))이라는 3차원 tensor를 사용

 

 

3.예시로 이해하는 NLP 데이터 전처리

 

다음과 같이 4개의 문장으로 구성된 전체 훈련 데이터가 있다

 

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

 

 

컴퓨터는 이 상태로는 "나는 사과를 좋아해"가 단어가 1개인가 3개인가 이해하지 못한다.

 

 

컴퓨터의 입력으로 사용하기 위해 단어별로 나눠줘야 한다.

 

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

 

이러면 훈련데이터의 크기는 4*3크기의 2차원 tensor이다.

 

 

컴퓨터는 텍스트보다는 숫자를 더 잘 처리할 수 있다.

 

어떻게 잘 변환하여 다음과 같이 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]

 

이를 위 4*3 tensor에 그대로 대입한다면...

 

[[[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*3*3크기의 3차원 tensor이다.

 

만약 batch size를 2로 해본다면...?

 

#첫번째 batch
[[[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]]]
 
#두번째 batch
[[[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]]]

 

 

컴퓨터는 batch 단위로 가져가 연산을 수행하며, 각 배치의 텐서의 크기는 2*3*3이다.

 

이는 (batch size, 문장의 길이(3), 단어 벡터의 차원(3))이다.

 

 

4. numpy로 텐서 만들기

 

먼저 numpy의 import는 다음과 같이 한다

 

import numpy as np

 

 

4-1) 1차원 tensor

 

numpy로 텐서를 만드는 방법은 리스트 [1,2,3,...]을 np.array로 감싸주면 된다.

 

#list를 생성해 np.array로 1차원 array로 변환
t = np.array([0.,1.,2.,3.,4.,5.,6.])

print(t)
[0. 1. 2. 3. 4. 5. 6.]

 

차원은 ndim으로 크기는 shape로 구할 수 있다

 

print('Rank of t: ', t.ndim)
print('Shape of t: ', t.shape)

Rank of t:  1
Shape of t:  (7,)

 

여기서 rank, 차원은 1차원, 2차원, 3차원 할때 그 차원이고 벡터는 1차원이라 1이 나온다

 

shape는 tensor의 크기로 (7,)는 (1,7)을 의미한다. 즉 1*7 벡터이다.

 

 

4-2) numpy의 기초

 

1) numpy에서 인덱스는 0부터 시작하며 인덱싱으로 tensor의 원소에 접근이 가능

 

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

 

-1번 인덱스는 맨 뒤에서부터 시작하는 인덱스

 

 

2) 슬라이싱으로 범위 지정하여 원소를 불러올 수 있다

 

[시작번호: 끝번호]

 

여기서 끝 번호에 해당하는 원소는 포함하지 않는다

 

print('t[2:5] t[4:-1] = ', t[2:5], t[4:-1]) #[시작번호: 끝번호]로 범위 지정
t[2:5] t[4:-1]  =  [2. 3. 4.] [4. 5.]

 

여기서 t[4:-1]은 4번 인덱스부터 -1번 앞인 -2번 인덱스까지 원소를 가져온다는 말이다.

 

현재 t가 [0,1,2,3,4,5,6]이므로 4번인 4부터 -2번인 5까지 가져온다

 

만약 시작번호를 생략한다면 처음부터 끝 번호 앞까지 뽑아내고

 

끝 번호를 생략하면 시작번호부터 tensor의 끝까지 뽑아낸다

 

print('t[:2] t[3:]     = ', t[:2], t[3:]) # 시작 번호를 생략한 경우와 끝 번호를 생략한 경우
t[:2] t[3:]     =  [0. 1.] [3. 4. 5. 6.]

 

 

4-3) 2차원 텐서

 

4*3행렬 생성

 

#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)

 

 

5. pytorch로 tensor 만들기

 

numpy와 유사하지만, pytorch로 tensor를 만드는 것이 더 낫다

 

import torch

 

5-1) 1차원 tensor

 

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

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

 

pytorch에서 dim()은 tensor의 rank를 구해주고, shape와 size()는 tensor의 크기를 보여준다

 

#dim은 rank(차원)

#shape, size()는 tensor의 크기

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.])

 

5-2) 2차원 tensor

 

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.]])

 

 

dim()과 size()로 차원과 shape를 구한다

 

#dim()으로 차원, size()로 shape(크기)

print(t.dim()) #rank
print(t.size()) #shape

2
torch.Size([4, 3])

 

슬라이싱이 가능하다

 

#슬라이싱 가능
print(t[:,1]) #첫번째 차원을 전체 선택한 상황에서 두번째 차원의 첫번째 것만 가져온다
print(t[:,1].size())

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

 

첫번째 차원인 모든 행을 선택하고, 거기서 두번째 차원의 1번 인덱스만 가져온다.

 

즉, 행렬에서 첫번째 열을 모두 가져온다

 

t[:, :-1]을 한다면, 0열과 1열을 모두 가져올 것이다.

 

print(t[:,:-1]) 
#첫번째 차원을 전체 선택한 상황에서, 두번째 차원에서는 맨 마지막에서 첫번째를 제외하고 다 가져온다

tensor([[ 1.,  2.],
        [ 4.,  5.],
        [ 7.,  8.],
        [10., 11.]])

 

6. broadcasting

 

두 행렬 A,B가 있을 때 행렬의 덧셈과 뺄셈은 A,B의 크기가 같아야 한다.

 

두 행렬의 곱셈을 위해서는 A의 열(마지막 차원)과 B의 행(첫번째 차원)이 일치해야한다.

 

하지만 딥러닝에서 불가피하게 크기가 다른 행렬이나 텐서끼리 사칙연산을 수행할 필요가 있다.

 

이를 위해 pytorch에서는 자동으로 크기를 맞춰 연산을 수행하는 broadcasting이 있다.

 

 

6-1) 같은 크기일때 연산

 

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

print(m1+m2)
tensor([[5., 5.]])

 

여기서 m1, m2는 둘다 (1,2)이다. 그래서 문제없이 덧셈 연산이 가능하다.

 

 

6-2) 크기가 다른 경우

 

이번에는 크기가 다른 텐서들 간 연산을 보자.

 

#vector + scalar

m1 = torch.FloatTensor([[1,2]])
m2 = torch.FloatTensor([3]) #[3] -> [3,3]

print(m1+m2)
tensor([[4., 5.]])

 

 

원래 m1의 크기는 (1,2)이고, m2의 크기는 (1,)이다.

 

그런데 pytorch는 m2의 크기를 (1,2)로 변경하여 연산을 수행한다. 

 

 

6-3) 벡터 간 연산에서 broadcasting

 

#2*1 vector + 1*2 vector

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

tensor([4., 5.],
       [5., 6.]])

 

여기서 m1의 크기는 (1,2)이고 m2의 크기는 (2,1)이다.

 

이 두 행렬은 수학적으로 덧셈을 할 수 없지만, pytorch는 (2,2)로 모두 변경시켜서 덧셈을 수행한다

 

# 브로드캐스팅 과정에서 실제로 두 텐서가 어떻게 변경되는지 보겠습니다.
[1, 2]
==> [[1, 2],
     [1, 2]]
[3]
[4]
==> [[3, 3],
     [4, 4]]

 

broadcasting은 편리하지만, 자동으로 실행되기 때문에 주의해서 사용해야한다.

 

두 텐서의 크기가 같다고 생각하여 연산을 수행했지만, 실제로 달랐고 broadcasting에 의해 아무런 문제 없이 수행하기 때문에

 

생각하는 결과와 달라도 어디서 에러가 났는지 찾기 어려울 수 있기 때문이다.

 

 

7. 자주 사용되는 기능

 

7-1) 행렬 곱셈(matrix multiplication)과 곱셈(multiplication)

 

행렬로 곱셈을 하는 방법으로 matmul과 mul이 있다.

 

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.]])

 

행렬 곱셈이 아닌, element-wise product도 존재한다.

 

동일한 위치에 있는 원소끼리 곱하는 연산이다.

 

다음은 서로 다른 크기의 행렬이 broadcasting되어 element wise product를 수행하는 것이다.

 

* 연산 또는 mul()로 수행가능하다

 

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

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

print(m1 * m2) #2*2
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.]])

 

다음과 같이 2번째 행렬의 크기가 변한다

 

# 브로드캐스팅 과정에서 m2 텐서가 어떻게 변경되는지 보겠습니다.
[1]
[2]
==> [[1, 1],
     [2, 2]]

 

2) 평균 구하기

 

.mean()을 사용하면 가능

 

#1차원
t = torch.FloatTensor([1, 2])
print(t.mean())

tensor(1.5000)

#2차원
t = torch.FloatTensor([[1, 2], [3, 4]])
print(t)
tensor([[1., 2.],
        [3., 4.]])
        
print(t.mean())
tensor(2.5000)

 

2차원 이상에서 dim인자로 차원별 평균을 구할수도 있다

 

print(t.mean(dim=0))
tensor([2., 3.])

 

dim = 0은 첫번째 차원을 의미한다. 행렬에서 첫번째 차원은 '행'을 의미

 

인자로 dim을 준다면 해당 차원을 제거한다는 의미이다.

 

다시 말해 행렬에서 '열'만을 남긴다.

 

즉 열의 차원을 보존하면서 평균을 구한다.

 

즉 열방향으로 원소끼리의 평균을 구한다

 

# 실제 연산 과정
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])

 

 

만약 dim = -1을 주면 어떻게 될까?

 

마지막 차원을 제거한다는 의미로 두번째 차원인 열의 차원을 제거한다는 의미이다.

 

그러므로 행방향으로 평균을 구해주어 위의 결과와 같다

 

print(t.mean(dim=-1))
tensor([1.5000, 3.5000])

 

 3) 덧셈(sum)

 

평균과 동일하게 dim 연산이 가능하다

 

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는 원소들의 최댓값을 return

 

argmax는 최댓값을 가지는 원소의 인덱스를 return

 

t = torch.FloatTensor([[1, 2], [3, 4]])
print(t)
tensor([[1., 2.],
        [3., 4.]])
        
print(t.max()) # Returns one value: max
tensor(4.)

 

dim연산이 가능하다.

 

dim = 0을 주면 첫번째 차원인 행을 제거하는 것으로 열방향으로 최댓값을 구해준다

 

print(t.max(dim=0)) # Returns two values: max and argmax

(tensor([3., 4.]), tensor([1, 1]))

 

여기서 [1,1]은 각 열에서 최댓값을 나타내는 원소의 인덱스로 argmax를 의미한다.

 

첫번째 열은 [1,3]인데 3이 최댓값이고 1번 인덱스

 

두번째 열은 [2,4]인데 4가 최댓값이고 1번 인덱스이다.

 

# [1, 1]가 무슨 의미인지 봅시다. 기존 행렬을 다시 상기해봅시다.
[[1, 2],
 [3, 4]]
첫번째 열에서 0번 인덱스는 1, 1번 인덱스는 3입니다.
두번째 열에서 0번 인덱스는 2, 1번 인덱스는 4입니다.
다시 말해 3과 4의 인덱스는 [1, 1]입니다.

 

argmax만 가지고 싶다면, 혹은 max만 가지고 싶다면 인덱싱을 통해 가져올 수 있다

 

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

 

혹은 t.argmax(dim=0)으로 argmax를 얻을 수 있다

 

#argmax

print(t.argmax(dim=0))

tensor([1,1])

 

 

https://wikidocs.net/52460

 

02-02 텐서 조작하기(Tensor Manipulation) 1

이번 챕터에서 배울 내용에 대해서 리뷰해보겠습니다. 벡터, 행렬, 텐서의 개념에 대해서 이해하고, Numpy와 파이토치로 벡터, 행렬, 텐서를 다루는 방법에 대해서 이해합니…

wikidocs.net

 

TAGS.

Comments