NumPy 연산

벡터화 연산

NumPy는 코드를 간단하게 만들고 계산 속도를 빠르게 하기 위한 벡터화 연산(vectorized operation)을 지원한다. 벡터화 연산이란 반복문(loop)을 사용하지 않고 선형 대수의 벡터 혹은 행렬 연산과 유사한 코드를 사용하는 것을 말한다.

예를 들어 다음과 같은 연산을 해야 한다고 하자.

$$ x = \begin{bmatrix}1 \\ 2 \\ 3 \\ \vdots \\ 100 \end{bmatrix}, \;\;\;\; y = \begin{bmatrix}101 \\ 102 \\ 103 \\ \vdots \\ 200 \end{bmatrix}, $$$$z = x + y = \begin{bmatrix}1+101 \\ 2+102 \\ 3+103 \\ \vdots \\ 100+200 \end{bmatrix}= \begin{bmatrix}102 \\ 104 \\ 106 \\ \vdots \\ 300 \end{bmatrix} $$

만약 NumPy의 벡터화 연산을 사용하지 않는다면 이 연산은 루프를 활용하여 다음과 같이 코딩해야 한다.


In [7]:
x = np.arange(1, 101)
x


Out[7]:
array([  1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,
        14,  15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,  26,
        27,  28,  29,  30,  31,  32,  33,  34,  35,  36,  37,  38,  39,
        40,  41,  42,  43,  44,  45,  46,  47,  48,  49,  50,  51,  52,
        53,  54,  55,  56,  57,  58,  59,  60,  61,  62,  63,  64,  65,
        66,  67,  68,  69,  70,  71,  72,  73,  74,  75,  76,  77,  78,
        79,  80,  81,  82,  83,  84,  85,  86,  87,  88,  89,  90,  91,
        92,  93,  94,  95,  96,  97,  98,  99, 100])

In [8]:
y = np.arange(101, 201)
y


Out[8]:
array([101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113,
       114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126,
       127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139,
       140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152,
       153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165,
       166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178,
       179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191,
       192, 193, 194, 195, 196, 197, 198, 199, 200])

In [17]:
%%time
z = np.zeros_like(x)
for i, (xi, yi) in enumerate(zip(x, y)):
    z[i] = xi + yi


CPU times: user 202 µs, sys: 129 µs, total: 331 µs
Wall time: 179 µs

In [18]:
z


Out[18]:
array([102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126,
       128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152,
       154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178,
       180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204,
       206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, 228, 230,
       232, 234, 236, 238, 240, 242, 244, 246, 248, 250, 252, 254, 256,
       258, 260, 262, 264, 266, 268, 270, 272, 274, 276, 278, 280, 282,
       284, 286, 288, 290, 292, 294, 296, 298, 300])

그러나 NumPy는 벡터화 연산을 지원하므로 다음과 같이 덧셈 연산 하나로 끝난다. 위에서 보인 선형 대수의 벡터 기호를 사용한 연산과 코드가 완전히 동일하다.


In [16]:
%%time
z = x + y


CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 28.1 µs

In [19]:
z


Out[19]:
array([102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126,
       128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152,
       154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178,
       180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204,
       206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, 228, 230,
       232, 234, 236, 238, 240, 242, 244, 246, 248, 250, 252, 254, 256,
       258, 260, 262, 264, 266, 268, 270, 272, 274, 276, 278, 280, 282,
       284, 286, 288, 290, 292, 294, 296, 298, 300])

연산 속도도 벡터화 연산이 훨씬 빠른 것을 볼 수 있다.

Element-Wise 연산

NumPy의 벡터화 연산은 같은 위치의 원소끼리 연산하는 element-wise 연산이다. NumPy의 ndarray를 선형 대수의 벡터나 행렬이라고 했을 때 덧셈, 뺄셈은 NumPy 연산과 일치한다

스칼라와 벡터의 곱도 마찬가지로 선형 대수에서 사용하는 식과 NumPy 코드가 일치한다.


In [24]:
x = np.arange(10)
x


Out[24]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [26]:
a = 100
a * x


Out[26]:
array([  0, 100, 200, 300, 400, 500, 600, 700, 800, 900])

NumPy 곱셉의 경우에는 행렬의 곱, 즉 내적(inner product, dot product)의 정의와 다르다. 따라서 이 경우에는 별도로 dot이라는 명령 혹은 메서드를 사용해야 한다.


In [20]:
x = np.arange(10)
y = np.arange(10)
x * y


Out[20]:
array([ 0,  1,  4,  9, 16, 25, 36, 49, 64, 81])

In [21]:
np.dot(x, y)


Out[21]:
285

In [23]:
x.dot(y)


Out[23]:
285

비교 연산도 마찬가지로 element-wise 연산이다. 따라서 벡터 혹은 행렬 전체의 원소가 모두 같아야 하는 선형 대수의 비교 연산과는 다르다.


In [44]:
a = np.array([1, 2, 3, 4])
b = np.array([4, 2, 2, 4])

In [45]:
a == b


Out[45]:
array([False,  True, False,  True], dtype=bool)

In [46]:
a >= b


Out[46]:
array([False,  True,  True,  True], dtype=bool)

만약 배열 전체를 비교하고 싶다면 array_equal 명령을 사용한다.


In [47]:
a = np.array([1, 2, 3, 4])
b = np.array([4, 2, 2, 4])
c = np.array([1, 2, 3, 4])

In [48]:
np.array_equal(a, b)


Out[48]:
False

In [49]:
np.array_equal(a, c)


Out[49]:
True

만약 NumPy 에서 제공하는 지수 함수, 로그 함수 등의 수학 함수를 사용하면 element-wise 벡터화 연산을 지원한다.


In [55]:
a = np.arange(5)
a


Out[55]:
array([0, 1, 2, 3, 4])

In [51]:
np.exp(a)


Out[51]:
array([  1.        ,   2.71828183,   7.3890561 ,  20.08553692,  54.59815003])

In [54]:
10**a


Out[54]:
array([    1,    10,   100,  1000, 10000])

In [52]:
np.log(a)


Out[52]:
array([       -inf,  0.        ,  0.69314718,  1.09861229,  1.38629436])

In [53]:
np.log10(a)


Out[53]:
array([       -inf,  0.        ,  0.30103   ,  0.47712125,  0.60205999])

만약 NumPy에서 제공하는 함수를 사용하지 않으면 벡터화 연산은 불가능하다.


In [58]:
import math
a = [1, 2, 3]
math.exp(a)



TypeErrorTraceback (most recent call last)
<ipython-input-58-5b4545ac8959> in <module>()
      1 import math
      2 a = [1, 2, 3]
----> 3 math.exp(a)

TypeError: a float is required

브로드캐스팅

선형 대수의 행렬 덧셈 혹은 뺄셈을 하려면 두 행렬의 크기가 같아야 한다. 그러나 NumPy에서는 서로 다른 크기를 가진 두 ndarray 배열의 사칙 연산도 지원한다. 이 기능을 브로드캐스팅(broadcasting)이라고 하는데 크기가 작은 배열을 자동으로 반복 확장하여 크기가 큰 배열에 맞추는 방벙이다.

예를 들어 다음과 같이 벡터와 스칼라를 더하는 경우를 생각하자. 선형 대수에서는 이러한 연산이 불가능하다.

$$ x = \begin{bmatrix}0 \\ 1 \\ 2 \\ 3 \\ 4 \end{bmatrix}, \;\;\;\; x + 1 = \begin{bmatrix}0 \\ 1 \\ 2 \\ 3 \\ 4 \end{bmatrix} + 1 = ? $$

그러나 NumPy는 브로드캐스팅 기능을 사용하여 스칼라를 벡터와 같은 크기로 확장시켜서 덧셈 계산을 한다.

$$ \begin{bmatrix}0 \\ 1 \\ 2 \\ 3 \\ 4 \end{bmatrix} \overset{\text{numpy}}+ 1 = \begin{bmatrix}0 \\ 1 \\ 2 \\ 3 \\ 4 \end{bmatrix} + \begin{bmatrix}1 \\ 1 \\ 1 \\ 1 \\ 1 \end{bmatrix} = \begin{bmatrix}1 \\ 2 \\ 3 \\ 4 \\ 5 \end{bmatrix} $$

In [27]:
x = np.arange(5)
y = np.ones_like(x)
x + y


Out[27]:
array([1, 2, 3, 4, 5])

In [30]:
x + 1


Out[30]:
array([1, 2, 3, 4, 5])

브로드캐스팅은 더 차원이 높은 경우에도 적용된다. 다음 그림을 참조하라.


In [38]:
a = np.tile(np.arange(0, 40, 10), (3, 1)).T
a


Out[38]:
array([[ 0,  0,  0],
       [10, 10, 10],
       [20, 20, 20],
       [30, 30, 30]])

In [32]:
b = np.array([0, 1, 2])
b


Out[32]:
array([0, 1, 2])

In [33]:
a + b


Out[33]:
array([[ 0,  1,  2],
       [10, 11, 12],
       [20, 21, 22],
       [30, 31, 32]])

In [42]:
a = np.arange(0, 40, 10)[:, np.newaxis]
a


Out[42]:
array([[ 0],
       [10],
       [20],
       [30]])

In [43]:
a + b


Out[43]:
array([[ 0,  1,  2],
       [10, 11, 12],
       [20, 21, 22],
       [30, 31, 32]])

차원 축소 연산

ndarray의 하나의 행에 있는 원소를 하나의 데이터 집합으로 보고 평균을 구하면 각 행에 대해 하나의 숫자가 나오게 된다. 예를 들어 10x5 크기의 2차원 배열에 대해 행-평균을 구하면 10개의 숫자를 가진 1차원 벡터가 나오게 된다. 이러한 연산을 차원 축소(dimension reduction) 연산이라고 한다.

ndarray 는 다음과 같은 차원 축소 연산 명령 혹은 메서드를 지원한다.

  • 최대/최소: min, max, argmin, argmax
  • 통계: sum, mean, median, std, var
  • 불리언: all, any

In [59]:
x = np.array([1, 2, 3, 4])
x


Out[59]:
array([1, 2, 3, 4])

In [60]:
np.sum(x)


Out[60]:
10

In [61]:
x.sum()


Out[61]:
10

In [69]:
x = np.array([1, 3, 2])

In [70]:
x.min()


Out[70]:
1

In [71]:
x.max()


Out[71]:
3

In [72]:
x.argmin()  # index of minimum


Out[72]:
0

In [73]:
x.argmax()  # index of maximum


Out[73]:
1

In [82]:
x = np.array([1, 2, 3, 1])

In [83]:
x.mean()


Out[83]:
1.75

In [84]:
np.median(x)


Out[84]:
1.5

In [74]:
np.all([True, True, False])


Out[74]:
False

In [75]:
np.any([True, True, False])


Out[75]:
True

In [76]:
a = np.zeros((100, 100), dtype=np.int)
a


Out[76]:
array([[0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       ..., 
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0]])

In [77]:
np.any(a != 0)


Out[77]:
False

In [78]:
np.all(a == a)


Out[78]:
True

In [79]:
a = np.array([1, 2, 3, 2])
b = np.array([2, 2, 3, 2])
c = np.array([6, 4, 4, 5])

In [80]:
((a <= b) & (b <= c)).all()


Out[80]:
True

연산의 대상이 2차원 이상인 경우에는 어느 차원으로 계산을 할 지를 axis 인수를 사용하여 지시한다. axis=0인 경우는 행 연산, axis=1인 경우는 열 연산 등으로 사용한다. 디폴트 값은 0이다.

<img src="https://datascienceschool.net/upfiles/edfaf93a7f124f359343d1dcfe7f29fc.png", style="margin: 0 auto 0 auto;">


In [65]:
x = np.array([[1, 1], [2, 2]])
x


Out[65]:
array([[1, 1],
       [2, 2]])

In [66]:
x.sum()


Out[66]:
6

In [67]:
x.sum(axis=0)   # columns (first dimension)


Out[67]:
array([3, 3])

In [68]:
x.sum(axis=1)   # rows (second dimension)


Out[68]:
array([2, 4])

In [85]:
y = np.array([[1, 2, 3], [5, 6, 1]])
np.median(y, axis=-1) # last axis


Out[85]:
array([ 2.,  5.])

정렬

sort 명령이나 메서드를 사용하여 배열 안의 원소를 크기에 따라 정렬하여 새로운 배열을 만들 수도 있다. 2차원 이상인 경우에는 마찬가지로 axis 인수를 사용하여 방향을 결정한다.


In [90]:
a = np.array([[4, 3, 5], [1, 2, 1]])
a


Out[90]:
array([[4, 3, 5],
       [1, 2, 1]])

In [91]:
np.sort(a)


Out[91]:
array([[3, 4, 5],
       [1, 1, 2]])

In [92]:
np.sort(a, axis=1)


Out[92]:
array([[3, 4, 5],
       [1, 1, 2]])

sort 메서드는 해당 객체의 자료 자체가 변화하는 in-place 메서드이므로 사용할 때 주의를 기울여야 한다.


In [93]:
a.sort(axis=1)
a


Out[93]:
array([[3, 4, 5],
       [1, 1, 2]])

만약 자료를 정렬하는 것이 아니라 순서만 알고 싶다면 argsort 명령을 사용한다.


In [95]:
a = np.array([4, 3, 1, 2])
j = np.argsort(a)
j


Out[95]:
array([2, 3, 1, 0])

In [96]:
a[j]


Out[96]:
array([1, 2, 3, 4])