파이썬 라이브러리 넘파이 (numpy) 학습 두번째 시간, 지난 시간에는 numpy란 무엇인지와 기본적인 어레이(array)를 생성하는 방법에 대해 알아보았다.
이전글 : 넘파이 어레이 (numpy array) 생성과 dtype, 차원 변경
오늘은 numpy array의 연산에 대해 알아본다.
numpy 배열의 연산은 다음과 같은 특징을 가진다.
- 반복문을 회피하여 수식이 간편하고 속도가 빠르다
- broadcast (element-wise) 연산방식 : 같은 위치 요소들끼리의 사칙연산 가능
- 기본적으론 shape이 동일해야 하나, 확장 가능한 경우 shape이 달라도 연산을 지원
이 특징들을 한가지씩 실습을 통해 익혀본다.
numpy 배열과 list 연산 비교
왜 넘파이 배열로 연산하는 게 좋은지, 파이썬 리스트를 만들어서 연산하는 경우와 비교해 보자.
test1 = list(range(10))
test1
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 결과
🔺 test1 이라는 변수에 range함수를 이용하여 0에서 9까지의 값을 갖는 리스트를 만들었다. 이것을 각각 2배씩 곱해주고 싶다고 하자.
test1 * 2
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 결과
🔺 리스트 변수명에 다짜고짜 곱하기 2를 하면 위와 같이 리스트 두개를 이어붙인 형태가 되어버린다. 원소들의 값 자체를 두배씩으로 바꿔주고 싶다면 반복문을 쓰는 수밖에 없다.
result1 = []
for i in test1:
result1.append(i*2)
result1
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18] # 결과
🔺 for 반복문을 이용해서 리스트 내의 원소들을 하나씩 순차적으로 불러오고 그것들을 2배해서 새로 생성해놓은 빈 리스트에 추가해주는 작업을 하도록 만들었다.
만약 원소의 개수가 10000개라면 반복문을 10000번 수행해야 한다. 얼핏 생각해도 속도가 매우 느릴 것 같다.
반면에 넘파이 어레이를 통해 이 연산을 하면 어떤 식이 될까.
import numpy as np
test2 = np.arange(10)
test2
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) # 결과
🔺 import로 numpy를 불러온 뒤, arange 함수를 이용해 0에서 9까지의 숫자를 갖는 1차원 어레이를 생성하였다. 각각을 두배씩 해주고 싶다면,
test2 *2
array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18]) # 결과
🔺 그냥 이렇게 곱하기 2를 해주는 것으로 원하는 결과를 얻을 수 있다. 아까 리스트에서와 달리 array 배열은 사칙연산자 기호에 의해 각 요소별 연산이 된다. 따라서 반복문을 거치지 않고 보다 빠르게 작업을 수행할 수 있다.
그리고 매번 반복문 코드 짜는 것보다 numpy array를 써주는 것이 코드도 훨씬 간단해진다. 넘파이 배열의 연산 방법들을 좀 더 구체적으로 살펴보자.
broadcast 연산
broadcast 연산이라는 것은 element-wise 연산 방식이라고도 하는데, 배열의 각 요소별로 연산과정을 수행한다는 의미이다.
a = np.arange(15).reshape(3,5)
b = np.arange(20,35).reshape(3,5)
a # a array 내용출력
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
b # b array 내용출력
array([[20, 21, 22, 23, 24],
[25, 26, 27, 28, 29],
[30, 31, 32, 33, 34]])
🔺 위와 같이 3행 5열 shape에 다른 원소값을 갖는 a, b 두개의 배열을 생성하였다. 이 두개로 사칙연산을 시켜보자.
더하기
a+b
array([[20, 22, 24, 26, 28],
[30, 32, 34, 36, 38],
[40, 42, 44, 46, 48]])
np.add(a,b)
array([[20, 22, 24, 26, 28],
[30, 32, 34, 36, 38],
[40, 42, 44, 46, 48]])
🔺 연산자 기호 + 또는 연산 함수 np.add(배열1, 배열2) 형태로 각각의 합을 구한다.
빼기
a-b
array([[-20, -20, -20, -20, -20],
[-20, -20, -20, -20, -20],
[-20, -20, -20, -20, -20]])
np.subtract(a,b)
array([[-20, -20, -20, -20, -20],
[-20, -20, -20, -20, -20],
[-20, -20, -20, -20, -20]])
🔺 연산자 기호 – 또는 연산 함수 np.subtract(배열1, 배열2) 형태로 각각의 합을 구한다.
곱하기
a*b
array([[ 0, 21, 44, 69, 96],
[125, 156, 189, 224, 261],
[300, 341, 384, 429, 476]])
np.multiply(a,b)
array([[ 0, 21, 44, 69, 96],
[125, 156, 189, 224, 261],
[300, 341, 384, 429, 476]])
🔺 연산자 기호 * 또는 연산 함수 np.multiply(배열1, 배열2) 형태로 각각의 합을 구한다.
나누기
a/b
array([[0. , 0.04761905, 0.09090909, 0.13043478, 0.16666667],
[0.2 , 0.23076923, 0.25925926, 0.28571429, 0.31034483],
[0.33333333, 0.35483871, 0.375 , 0.39393939, 0.41176471]])
np.divide(a,b)
array([[0. , 0.04761905, 0.09090909, 0.13043478, 0.16666667],
[0.2 , 0.23076923, 0.25925926, 0.28571429, 0.31034483],
[0.33333333, 0.35483871, 0.375 , 0.39393939, 0.41176471]])
🔺 연산자 기호 / 또는 연산 함수 np.divide(배열1, 배열2) 형태로 각각의 합을 구한다.
이것은 연산한 결과값만 보여주는 것이고 배열 a와 b의 원래 내용을 바꾸는 것은 아니다. 연산 결과를 새로운 array로 생성하고 싶다면,
c = np.divide(a, b) 와 같이 새로운 변수명에 받아주면 된다.
벡터의 내적, 행렬곱
행렬곱으로 벡터의 내적을 구할 수 있는데, 벡터와 내적의 의미 같은 것은 수학 공부시간에 따로 다루기로 하고 여기서는 numpy array를 통해 연산하는 방법만 공부한다.
🔺 행렬곱은 이렇게 첫번째 행렬의 m 행과 두번째 행렬의 n 열 요소들간에 곱의 합을 구해서 결과값을 m행 n열의 원소로 넣는 것이다.
내적 : 곱끼리의 합 이라고만 일단 알아둔다.
딱 저렇게 m x n 하나의 연산만 존재하도록 1차원 배열끼리 먼저 해보자.
a = np.array([10, 20, 30])
b = np.array([5, 10, 15])
a, a.shape, b, b.shape
(array([10, 20, 30]), (3,), array([ 5, 10, 15]), (3,))
이렇게 1행3열짜리 어레이 두개를 만든 뒤 벡터의 내적을 구할 때, 방법은 4가지이다.
np.sum(a * b)
np.dot(a, b)
a.dot(b)
a @ b
700 # 결과
🔺 4가지 중 어떤 방법으로 해도 결과는 모두 같게 나온다.
10*5 + 20*10 + 30*15 = 50 + 200 + 450 = 700
이제 2차원 배열끼리 연산을 해보자.
c = np.arange(12).reshape(3,4)
d = np.arange(10,22).reshape(3,4)
c, c.shape
(array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]]), (3, 4))
d, d.shape
(array([[10, 11, 12, 13],
[14, 15, 16, 17],
[18, 19, 20, 21]]), (3, 4))
🔺 이렇게 3행 4열짜리 어레이 두개를 생성하고 벡터 내적 연산을 실행하면
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 3 is different from 4)
ValueError가 발생한다. 벡터의 내적은 곱의 합이라고 했는데, 이걸 하려면 첫번째 행렬의 열 개수 (가로 원소 수)와 두번째 행렬의 행 개수 (세로 원소 수)가 일치해야 한다. 즉 shape이 (3, 4) 하고 (4, 3) 두개를 행렬곱 시켜야 정상적인 결과가 출력된다.
c @ d.T
array([[ 74, 98, 122],
[258, 346, 434],
[442, 594, 746]])
🔺 이때는 이렇게 d.T 를 이용해서 두번째 행렬의 shape을 변환해주고 내적 연산을 하면 결과를 얻을 수 있다.
np.matmul(c, d.T)
array([[ 74, 98, 122],
[258, 346, 434],
[442, 594, 746]])
🔺 dot 함수들은 내적을 구하는 것이고, 행렬곱은 matmul 함수를 이용하여 계산하는데 이렇게 2차원 배열까지는 결과값이 동일하다. 3차원부터는 결과가 달라진다.
크기다른 배열간의 연산
numpy array 연산은 element wise 방식이라 두 배열의 shape이 같아야 한다고 했는데, 크기가 다르더라도 연산이 가능한 경우가 있다.
🔺 이렇게 차원이 다른 배열간에 연산을 수행하는 경우, 작은 차원의 배열을 큰 차원의 배열만큼 확장해서 계산을 수행해 준다. 빈칸에 같은 내용이 반복되어 있다고 가정하고 확장해서 연산한다는 뜻이다.
a = np.arange(12).reshape(3,4)
a, a.shape
(array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]]), (3, 4))
b = np.array([1,2,3,4])
b, b.shape
(array([1, 2, 3, 4]), (4,))
a + b
array([[ 1, 3, 5, 7],
[ 5, 7, 9, 11],
[ 9, 11, 13, 15]])
🔺 shape(3, 4) 배열 a에 shape(4, )인 배열 b를 더하면 서로 형태가 다르지만 마치 b의 한 행 데이터가 세 줄 있는 것처럼 확장해서 a의 각각의 행에 더하기를 해준다.
이렇게 numpy array 간의 간단한 연산 방법에 대해 알아보았다. 다음은 배열 내에서 특정 데이터를 찾고 추출하는 인덱싱과 슬라이싱에 대해 알아본다.