딥러닝 뿌셔먹기 1 - 에피타이저 Numpy로 백터 살펴보기

2021. 3. 30. 11:16개발노트/ML

안녕하세요.!

 

Mysterico에서 스크래핑 시스템과 빅데이터, 딥러닝 부분을 맡고 있는 Khan(채용공고 가면라이더..!!) 입니다.

 

미스테리코에서는 매주 개발자 컨퍼런스가 진행됩니다. >_<

 

 저희는 팀원들끼리 돌아가면서 개발자로서 능력을 키우기 위해 학습한 내용을 공유하거나 프로젝트를 진행하면서 느낀 점들을 정리하고 블로그에 포스팅하고 있는데요. 

 

벌써 시간이 야속하게도,,, 저의 차례는 돌아왔고, 맡은 분야 중 빅데이터, 딥러닝 부분에 대해 다루어볼까 했습니다.

하지만 주제가 초큼 특수해서,,, 발표를 하더라도 딥러닝에 대한 기초 지식이 없는 팀원들에게 발표가 명확히 전달될 수 있을까?라는 의문이 들었습니다.(나도 이해를 못하지만... 나랑 놀아줘.....ㅠㅠ)

 

그래서!!

당분간 딥러닝과 관련된 시리즈 물로 딥러닝에 대한 기초부터 적용까지 정리하여 포스팅하고자 합니다.

 

이를 통해

딥러닝을 학습한 경험이 전무한 사람들도 딥러닝을 이해할 수 있도록 하고, 저 스스로도 이론과 적용에 대해 하나하나 짚어가면서 딥러닝에 대해 단단해지는 기회로 삼고자 합니다.

 

그럼... 시작합니다!!

첫 번째 주제는 Numpy입니다!

갑자기 Numpy???라는 생각이 드실 수 있지만, 딥러닝과 Numpy는 땔레야 땔 수가 없는 아이(절대 찰떡!!)입니다.

(Numpy는 python 라이브러리 중 하나로 행렬과 리스트의 표현과 연산에 대한 표준으로 널리 쓰입니다.)

 

다음 편에 예정된 딥러닝 개요를 다룰 때 설명드리겠지만,

딥러닝은 인간의 뇌를 구성하는 수많은 뉴런들의 상호작용을 행렬의 묶음과 연산을 통해 묘사하고 흉내 낸 것입니다.

(행렬을 통해 어떻게 뉴런을 흉내 낼 수 있는지도 다음 장에...)

 

그렇기 때문에, 딥러닝 뿌셔먹기 위한 첫걸음으로 딥러닝에서 써먹기 위한 Numpy를 살짝 훑고 가겠습니다.

 

앞으로 딥러닝을 공부하시면서 사용하게 될 프레임워크인 pytorch와 tensorflow에서는 행렬 관련 자료형과 연산을 Tensor라는 이름으로 각각 구현하였는데, 각 Tensor들은 Numpy의 속성과 연산의 이름을 대부분 가지고 있으며, 이름이 같은 연산은 Numpy의 연산과 같은 동작과 결과를 기대할 수 있습니다.!!

(사실 프레임워크 없이 numpy만으로도 딥러닝을 구현할 수 있습니다. but... Don't reinvent the wheel)

 

그 덕분에 pytorch와 tensorflow의 Tensor를 Numpy와 서로 치환하는 것이 매우 간편하고, 또 이렇기에 각 프레임워크의 내부에서는 Tensor로 연산이 이루어지더라도 입력을 Numpy로도 받는 가능한 경우가 굉장히 많습니다.

 

용어 정리부터 하겠습니다.

 

중고등학교 때 배웠던 행렬은 차원 수에 대해 2차원 행렬, 3차원 행렬 이라고 불렀지만, 딥러닝을 학습하다보면 행렬에 대해 스칼라, 벡터, 텐서라고 표현하므로 이 용어들과 친숙해지셔야 합니다.

 

하나의 수에 대해 스칼라, 1차원 행렬(List, Array)는 'Vector', 2차원 행렬은 원래의 '행렬'(Matrix) 그대로, 그리고 3차원 이상은 'Tensor'라고 부릅니다.

딥러닝에서는 다양한 차원을 다루므로 표현의 편의를 위해 다양한 차원에 대해 Tensor라고 표현하기도 합니다.

(pytorch와 tensorflow가 행렬을 Tensor라는 이름으로 다루는 이유죠!) 

 

그럼 시작합니다.!!

일단 numpy 코드를 예제로 첨부할 텐데요.

(Tensor, Vector라는 단어와 친숙하지 않을 수 있으니 일단 모든 차원에 대해 이번 글에서만 '행렬'이라고 표현하겠습니다.)

 

1. Numpy 선언 및 Numpy 만들기

Numpy부터 선언, 간단한 예제용 list를 만들고 Numpy로 변환한 후 print 하여 확인해보겠습니다.

 

관용적으로 Numpy는 아래와 같이 선언하고 코드에서는 np로 사용합니다.

import numpy as np
	
sample_list = [[2, 4, 8], [3, 9, 27]]
sample_np = np.array(sample_list)
print(sample_np)

notebook out : 

 

오호! list와 흡사한 모양으로 출력되네요!!

 

2. Numpy의 데이터 타입

Numpy에 들어가는 원소들의 자료형은 dtype이라는 이름으로 표현되며, 설정해주지 않으면 입력된 원소들을 바탕으로 추론하여 결정됩니다.

Numpy 사이트의 자료형 설명

위에 만든 sample_np의 자료형은 무엇으로 추론되었는지 볼까요?

sample_np.dtype

notebook out :

 

자동으로 int로 dtype이 결정되어있네요.

만약 dtype을 선언해주면 어떻게 될까요? sample_np를 다시 만들며 확인해보겠습니다.

sample_list = [[2, 4, 8], [3, 9, 27]]
sample_np = np.array(sample_list, dtype=np.float32)
print(sample_np)
print(f'dtype = {sample_np.dtype}')

notebook out : 

아까는 원소가 2, 4, 8로 print 되었는데 자료형이 float이라 2. , 4., 8., 으로 표현되었네요!!

데이터 타입을 변경하는 것은 astype 메서드로 이루어집니다.

sample_np.astype(np.int64)

 

3. Numpy 형태 및 구조 파악

행렬 간의 연산에서는 각 행렬의 모양에 따라 연산의 과정이 다르거나 연산이 불가능할 수 있으므로 Numpy가 어떤 모양의 행렬인지 확인해보겠습니다.

 

- 차원의 개수 확인

numpy의 차원은 ndim으로 확인합니다.

앞에서 선언한 sample_np의 차원을 확인해보죠!

sample_np 확인

2차원 배열인 것을 확인해 볼 수 있습니다.

sample_np.ndim

notebook out :

 

- 각 차원의 구조 확인

numpy의 차원의 개수와 각 차원별 크기에 대한 확인은 shape로 확인합니다.

sample_np 확인

이번엔 sample_np의 구조를 보겠습니다.

sample_np.shape

notebook out :

첫 번째 행렬의 크기는 2, 두 번째 행렬의 크기는 3입니다!!

 

- 행렬의 총 원수 개수 확인

행렬에 원소가 총 몇 개 있는지는 size로 확인합니다. sample_np의 크기는 6이겠죠?

sample_np.size

notebook out :

 

3차원 행렬도 만들어보고 ndim, shape, size를 확인해보죠!

sample_list_3d = [[[2, 4, 8, 16, 32], [3, 9, 27, 81, 243]], [[1, 2, 3, 4, 5], [5, 25, 125, 625, 3125]], [[5, 4, 3, 2, 1], [10, 20, 30, 40, 50]]]
sample_np_3d = np.array(sample_list_3d)
print(sample_np_3d)

notebook out:

numpy 배열을 출력하니 차원에 대해 보기 좋게 표현했네요!

print(f'sample_np_3d.ndim = {sample_np_3d.ndim}')
print(f'sample_np_3d.shape = {sample_np_3d.shape}')
print(f'sample_np_3d.size = {sample_np_3d.size}')

notebook out :

 

4. Numpy 구조에 맞춘 초기 원소 값 세팅

행렬의 연산과 결과 표현은 크기와 구조가 중요하므로 지정한 크기에 대해 일괄적으로 원소 값을 세팅한 행렬들이 필요한 경우가 종종 있습니다.

 

- 원소 0으로 이루어진 3 X 5 행렬 만들기

zeros 함수를 사용하며 원하는 형태를 넘겨줍니다.

print(np.zeros((3,5)))

 

- 원소 1로 이루어진 2 x 4 행렬 만들기

ones 함수를 사용하여 원하는 형태를 넘겨줍니다.

print(np.ones((2,4)))

 

-  랜덤 한 값의 원소로 이루어진 3 x 5 행렬 만들기

np.random.random : 0~1 사이의 무작위 값

np.random.randint : 지정한 범위의 int의 무작위 값

print(np.random.random((3,5)))

print(np.random.randint(1, 10, (3,5))) # 1 ~ 10까지의 랜덤한 int로 3x5 행렬을 만들어라.!

 

5. Numpy 구조 변환

원소 크기가 16인 1차원 배열을 원하는 형태로 변환하며 Numpy의 구조 변환에 대해 알아보겠습니다.

 

sample_list_size16 = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
sample_list_size16_np = np.array(sample_list_size16)
sample_list_size16_np.shape

원소 크기 확인

 

colab으로 샘플 numpy를 찍어서 확인해볼게요.

sample_list_size16_np

 

- reshape() 사용하여 구조 변환(reshape는 메모리를 공유하므로 원소를 수정하면 reshape로 생성한 np들이 수정내용을 공유합니다.)

 

이제 원소가 16이므로 2x8, 4x4, 8x2, 16x1로 바꿔볼게요!

 

- 2x8

sample_list_size16_np.reshape(2,8)

 

- 4x4

sample_list_size16_np.reshape(4,4)

 

- 8x2

sample_list_size16_np.reshape(8,2)

 

- 16x1

sample_list_size16_np.reshape(16,1)

 

- 2x2x2x2

sample_list_size16_np.reshape(2,2,2,2)

 

- 차원 추론하여 구조 변환

'-1'을 입력하면 numpy가 크기를 지정해서 만들어줍니다.(wow 매우 편리 but -1을 여러번 사용하면 오류가 납니다!)

sample_list_size16_np.reshape(4, -1)

원소 크기가 16인데 1차원을 4로 입력하고 2차원을 추론하라면  4x4가 나오겠죠!

 

7. Numpy 행렬 연산

- 같은 형태의 행렬 원소 간 연산

1~15까지 연속된 숫자의 행렬과 그 행렬의 역순인 행렬을 만들어보겠습니다.

sample_a = np.arange(1,17)
print(f'sample - a : {sample_a}')
sample_b = np.arange(16, 0 , -1)
print(f'sample - b : {sample_b}')

같은 형태의 행렬 간 연산에 대해 사칙연산을 적용하면 같은 위치에 있는 원소간에 연산이 일어납니다.

print(f'sample_a + sample_b : {sample_a + sample_b}')
print(f'sample_a - sample_b : {sample_a - sample_b}')
print(f'sample_a * sample_b : {sample_a * sample_b}')
print(f'sample_a / sample_b : {sample_a / sample_b}')

- 다른 형태의 행렬 원소간 연산

Numpy는 다른 형태의 행렬에 대해 '브로드캐스팅'을 이용하여 연산을 합니다.

'브로드캐스팅'이란 형태가 다른 두 행렬 중 형태가 조그만 행렬을 확장시켜서 큰 형태의 행렬과 맞출 수 있다면 이에 대해 Numpy가 내부적으로 차원을 늘려서 형태를 맞춘 후 연산을 진행하는 것입니다.

(행렬을 확장시켜도 계산이 불가능한 2x3, 2x2과 같은 행렬들은 브로드케스트 불가합니다.)

 

예로 3x3 행렬과 1x3을 계산해보죠

 

1번 행렬

 

2번 행렬

 

두개의 행렬을 더한다면 2번 행렬을 아래와 같이 변환한 후 계산합니다.

 

연산 결과는 아래와 같습니다.

matrix_1 = np.array([[1,2,3],[4,5,6],[7,8,9]])
matrix_2 = np.array([1,2,3])
print(matrix_1 + matrix_2)

 

만약 원소 1개와 행렬을 곱한다면?

(위에서 언급했듯 원소 1개를 스칼라라고 하므로, 이와 같은 곱을 스칼라 곱이라고 합니다.)

 

위의 10과 matrix_1을 연산해보겠습니다.

matrix_1 = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(10 * matrix_1)

 

- 행렬의 곱셈

위에서는 각 행열의 위치가 대응되는 원소끼리 사칙연산을 하였지만 행렬의 곱셈은 행과 열이 대응되어 진행됩니다.

Numpy에서는 행렬의 곱을 dot, matmul 함수를 통해 진행합니다.

 

행렬의 곱은

아래 그림과 같이 진행됩니다.

중고등학교 때 배웠던 행렬의 곱셈입니다!

앞 행렬의 행과 뒤 행렬의 열간에 곱하고 더해주며, 그 결과로 나오는 행렬은 앞 행렬의 행과 뒤 행렬의 열의 크기를 가집니다.

그리고 행렬의 곱셈이 이루어지는 두행렬은 앞 행렬의 열과 뒤 행렬의 행의 크기가 같아야 가능합니다.

matrix_front = np.array([[1,2],[3,4]])
matrix_back = np.array([[5,6,7],[8,9,10]])
print(np.matmul(matrix_front, matrix_back))
print(np.dot(matrix_front, matrix_back))

2차원 행렬끼리의 연산으로 matmul과 dot의 결과가 같음을 확인하였습니다.

하지만, 3차원 이상의 행렬간 연산은 각각 다른 결과를 나타내므로 대략적인 개념을 확인해볼 필요가 있습니다.

(기본적인 2차원 행렬 간의 곱에서 크게 벗어나지 않습니다.)

 

설명을 잘해놓으신 블로그가 있어서 블로그 링크 참조합니다.

m.blog.naver.com/PostView.nhn?blogId=cjh226&logNo=221356884894&proxyReferer=https:%2F%2Fwww.google.com%2F

 

numpy에서 dot과 matmul의 차이

이전 포스트에서 python의 여러가지 행렬곱(matrix multiplication)을 살펴보았다[1]. 신가하게도 행렬곱을...

blog.naver.com

numpy 공식 사이트의 dot과 mamul에 대한 설명입니다.

numpy api - dot

 

numpy.dot — NumPy v1.20 Manual

Output argument. This must have the exact kind that would be returned if it was not used. In particular, it must have the right type, must be C-contiguous, and its dtype must be the dtype that would be returned for dot(a,b). This is a performance feature.

numpy.org

numpy api - matmul

 

numpy.matmul — NumPy v1.20 Manual

A location into which the result is stored. If provided, it must have a shape that matches the signature (n,k),(k,m)->(n,m). If not provided or None, a freshly-allocated array is returned.

numpy.org

7. 딥러닝에서 자주 쓰이는 Numpy 함수

딥러닝에서 자주쓰이는 함수들에 대해 몇가지 소개하겠습니다.

 

- numpy 합치기 concatenate

딥러닝에서는 텐서에 대해 배치로 연산을 진행하고 그 결과값들을 모아서 정확도를 계산하거나 평균 손실을 계산하는 등의 처리가 필요한 데, 배치 크기 단위로 연산이 이루어지고 그 결과가 추가되므로 전체 결과를 확인하려면 배치 크기 단위로 나누어진 결과들을 합쳐주어야합니다.

이때 concatenate를 사용합니다.

(배치(batch)에 대해 : gpu에서는 여러개의 데이터를 한번에 처리하는 것이 효율적이므로 한번에 여러 데이터를 넣어서 처리하는 것을 배치처리라고 하며, 한번에 넣는 데이터의 양을 배치 크기(batch size)라고 부릅니다.)

 

예시와 결과를 확인해보죠!

 

3가지 항목에 대해 분류하는 딥러닝 모델이 배치사이즈 2로 결과를 처리를 한다면 아래와 같은 모습의 결과를 내보낼 것입니다.

sample_batch_1_result = np.array([[0.2, 0.3, 0.5], [0.35, 0.2, 0.45]])
sample_batch_2_result = np.array([[0.1, 0.8, 0.1], [0.7, 0.1, 0.2]])

예시 데이터 'sample_batch_1_result'에 대해 간략하게 설명하면, 2개의 데이터에 대한 3가지 항목에 대한 예측 값이 들어있습니다.

첫번째 데이터에 대한 예측은 0번일 확률 0.2, 1번일 확률 0.3, 2번일 확률 0.5입니다.

 

concatenate를 이용해 배치로 된 예측 값을 모아보겠습니다.

np.concatenate([sample_batch_1_result, sample_batch_2_result])

concatenate에 어떤 축으로 합칠 것인지 지정해줄 수도 있습니다.(위의 결과는 default인 axis=0의 결과입니다.)

 

axis=1로 지정하면 그 결과는 아래와 같습니다.

np.concatenate([sample_batch_1_result, sample_batch_2_result], axis=1)

 

- 결과값 중 가장 큰 값은 어떤 위치일까? argmax

위의 예시를 보면, 입력 데이터에 대한 예측 결과가 항목에 대한 배열로 나오는 것을 확인할 수 있습니다.

딥러닝 모델이 결과를 [0.2, 0.3, 0.5] 형태로 나타내기 때문에 예측 결과를 사용하거나 정확도를 계산하려면 가장 큰 확률 값의 위치를 계산하여 사용해야합니다.

np.argmax(np.array([0.2, 0.3, 0.5]))

결과 : 0.5의 인덱스인 2

만약 concatenate를 사용하여 배치 데이터를 모은 결과들로 하나의 예측값들만 만든다면?

sample_batch_1_result = np.array([[0.2, 0.3, 0.5], [0.35, 0.2, 0.45]])
sample_batch_2_result = np.array([[0.1, 0.8, 0.1], [0.7, 0.1, 0.2]])
total_batch_result = np.concatenate([sample_batch_1_result, sample_batch_2_result], axis=0)
print(f'total_batch_result : \n{total_batch_result}')
print()
#차원이 하나 더 늘어났으므로 축을 지정해주어야 합니다!
print(f'predict_result : \n {np.argmax(total_batch_result, axis=1)}') 

이제 모델의 결과를 모았으니 예측값을 사용하거나 정답과 예측값을 비교하여 정확도를 산출할 수 있습니다.!!

 

그럼 Numpy에 대한 간단한 소개는 여기서 마치고 본격적인 딥러닝으로 넘어가겠습니다.

감사합니다!