도구 - 넘파이(NumPy)
*넘파이(NumPy)는 파이썬의 과학 컴퓨팅을 위한 기본 라이브러리입니다. 넘파이의 핵심은 강력한 N-차원 배열 객체입니다. 또한 선형 대수, 푸리에(Fourier) 변환, 유사 난수 생성과 같은 유용한 함수들도 제공합니다."
numpy
를 임포트해 보죠. 대부분의 사람들이 np
로 알리아싱하여 임포트합니다:
import numpy as np
np.zeros
¶zeros
함수는 0으로 채워진 배열을 만듭니다:
np.zeros(5)
array([0., 0., 0., 0., 0.])
2D 배열(즉, 행렬)을 만들려면 원하는 행과 열의 크기를 튜플로 전달합니다. 예를 들어 다음은 $3 \times 4$ 크기의 행렬입니다:
np.zeros((3,4))
array([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]])
(3, 4)
입니다.a = np.zeros((3,4))
a
array([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]])
a.shape
(3, 4)
a.ndim # len(a.shape)와 같습니다
2
a.size
12
임의의 랭크 수를 가진 N-차원 배열을 만들 수 있습니다. 예를 들어, 다음은 크기가 (2,3,4)
인 3D 배열(랭크=3)입니다:
np.zeros((2,3,4))
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.]]])
넘파이 배열의 타입은 ndarray
입니다:
type(np.zeros((3,4)))
numpy.ndarray
np.ones((3,4))
array([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]])
np.full
¶주어진 값으로 지정된 크기의 배열을 초기화합니다. 다음은 π
로 채워진 $3 \times 4$ 크기의 행렬입니다.
np.full((3,4), np.pi)
array([[3.14159265, 3.14159265, 3.14159265, 3.14159265], [3.14159265, 3.14159265, 3.14159265, 3.14159265], [3.14159265, 3.14159265, 3.14159265, 3.14159265]])
np.empty
¶초기화되지 않은 $2 \times 3$ 크기의 배열을 만듭니다(배열의 내용은 예측이 불가능하며 메모리 상황에 따라 달라집니다):
np.empty((2,3))
array([[4.6624083e-310, 0.0000000e+000, 0.0000000e+000], [0.0000000e+000, 0.0000000e+000, 0.0000000e+000]])
array
함수는 파이썬 리스트를 사용하여 ndarray
를 초기화합니다:
np.array([[1,2,3,4], [10, 20, 30, 40]])
array([[ 1, 2, 3, 4], [10, 20, 30, 40]])
np.arange
¶파이썬의 기본 range
함수와 비슷한 넘파이 arange
함수를 사용하여 ndarray
를 만들 수 있습니다:
np.arange(1, 5)
array([1, 2, 3, 4])
부동 소수도 가능합니다:
np.arange(1.0, 5.0)
array([1., 2., 3., 4.])
파이썬의 기본 range
함수처럼 건너 뛰는 정도를 지정할 수 있습니다:
np.arange(1, 5, 0.5)
array([1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5])
부동 소수를 사용하면 원소의 개수가 일정하지 않을 수 있습니다. 예를 들면 다음과 같습니다:
print(np.arange(0, 5/3, 1/3)) # 부동 소수 오차 때문에, 최댓값은 4/3 또는 5/3이 됩니다.
print(np.arange(0, 5/3, 0.333333333))
print(np.arange(0, 5/3, 0.333333334))
[0. 0.33333333 0.66666667 1. 1.33333333 1.66666667] [0. 0.33333333 0.66666667 1. 1.33333333 1.66666667] [0. 0.33333333 0.66666667 1. 1.33333334]
np.linspace
¶이런 이유로 부동 소수를 사용할 땐 arange
대신에 linspace
함수를 사용하는 것이 좋습니다. linspace
함수는 지정된 개수만큼 두 값 사이를 나눈 배열을 반환합니다(arange
와는 다르게 최댓값이 포함됩니다):
print(np.linspace(0, 5/3, 6))
[0. 0.33333333 0.66666667 1. 1.33333333 1.66666667]
np.rand
와 np.randn
¶넘파이의 random
모듈에는 ndarray
를 랜덤한 값으로 초기화할 수 있는 함수들이 많이 있습니다.
예를 들어, 다음은 (균등 분포인) 0과 1사이의 랜덤한 부동 소수로 $3 \times 4$ 행렬을 초기화합니다:
np.random.rand(3,4)
array([[0.03180573, 0.04345732, 0.98142132, 0.36467342], [0.10237531, 0.7970142 , 0.80017681, 0.85613689], [0.51845547, 0.86306651, 0.46900123, 0.93905324]])
다음은 평균이 0이고 분산이 1인 일변량 정규 분포(가우시안 분포)에서 샘플링한 랜덤한 부동 소수를 담은 $3 \times 4$ 행렬입니다:
np.random.randn(3,4)
array([[ 0.47997624, 0.18871329, -1.22975117, 0.60303237], [ 0.03966342, 0.26993552, 1.0156384 , -1.03525213], [ 1.30157063, -2.21755203, -1.85066939, 0.22902771]])
이 분포의 모양을 알려면 맷플롯립을 사용해 그려보는 것이 좋습니다(더 자세한 것은 맷플롯립 튜토리얼을 참고하세요):
%matplotlib inline
import matplotlib.pyplot as plt
plt.hist(np.random.rand(100000), density=True, bins=100, histtype="step", color="blue", label="rand")
plt.hist(np.random.randn(100000), density=True, bins=100, histtype="step", color="red", label="randn")
plt.axis([-2.5, 2.5, 0, 1.1])
plt.legend(loc = "upper left")
plt.title("Random distributions")
plt.xlabel("Value")
plt.ylabel("Density")
plt.show()
함수를 사용하여 ndarray
를 초기화할 수도 있습니다:
def my_function(z, y, x):
return x * y + z
np.fromfunction(my_function, (3, 2, 10))
array([[[ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]], [[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], [ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10.]], [[ 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.], [ 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.]]])
넘파이는 먼저 크기가 (3, 2, 10)
인 세 개의 ndarray
(차원마다 하나씩)를 만듭니다. 각 배열은 축을 따라 좌표 값과 같은 값을 가집니다. 예를 들어, z
축에 있는 배열의 모든 원소는 z-축의 값과 같습니다:
[[[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
[[ 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[ 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]]
[[ 2. 2. 2. 2. 2. 2. 2. 2. 2. 2.]
[ 2. 2. 2. 2. 2. 2. 2. 2. 2. 2.]]]
위의 식 x * y + z
에서 x, y, z는 사실 ndarray
입니다(배열의 산술 연산에 대해서는 아래에서 설명합니다). 중요한 점은 함수 my_function
이 원소마다 호출되는 것이 아니고 딱 한 번 호출된다는 점입니다. 그래서 매우 효율적으로 초기화할 수 있습니다.
c = np.arange(1, 5)
print(c.dtype, c)
int64 [1 2 3 4]
c = np.arange(1.0, 5.0)
print(c.dtype, c)
float64 [1. 2. 3. 4.]
넘파이가 데이터 타입을 결정하도록 내버려 두는 대신 dtype
매개변수를 사용해서 배열을 만들 때 명시적으로 지정할 수 있습니다:
d = np.arange(1, 5, dtype=np.complex64)
print(d.dtype, d)
complex64 [1.+0.j 2.+0.j 3.+0.j 4.+0.j]
e = np.arange(1, 5, dtype=np.complex64)
e.itemsize
8
data
버퍼¶배열의 데이터는 1차원 바이트 버퍼로 메모리에 저장됩니다. data
속성을 사용해 참조할 수 있습니다(사용할 일은 거의 없겠지만요).
f = np.array([[1,2],[1000, 2000]], dtype=np.int32)
f.data
<memory at 0x7f14d4455750>
파이썬 2에서는 f.data
가 버퍼이고 파이썬 3에서는 memoryview입니다.
if (hasattr(f.data, "tobytes")):
data_bytes = f.data.tobytes() # python 3
else:
data_bytes = memoryview(f.data).tobytes() # python 2
data_bytes
b'\x01\x00\x00\x00\x02\x00\x00\x00\xe8\x03\x00\x00\xd0\x07\x00\x00'
여러 개의 ndarray
가 데이터 버퍼를 공유할 수 있습니다. 하나를 수정하면 다른 것도 바뀝니다. 잠시 후에 예를 살펴 보겠습니다.
g = np.arange(24)
print(g)
print("랭크:", g.ndim)
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23] 랭크: 1
g.shape = (6, 4)
print(g)
print("랭크:", g.ndim)
[[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11] [12 13 14 15] [16 17 18 19] [20 21 22 23]] 랭크: 2
g.shape = (2, 3, 4)
print(g)
print("랭크:", g.ndim)
[[[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]] [[12 13 14 15] [16 17 18 19] [20 21 22 23]]] 랭크: 3
reshape
¶reshape
함수는 동일한 데이터를 가리키는 새로운 ndarray
객체를 반환합니다. 한 배열을 수정하면 다른 것도 함께 바뀝니다.
g2 = g.reshape(4,6)
print(g2)
print("랭크:", g2.ndim)
[[ 0 1 2 3 4 5] [ 6 7 8 9 10 11] [12 13 14 15 16 17] [18 19 20 21 22 23]] 랭크: 2
행 1, 열 2의 원소를 999로 설정합니다(인덱싱 방식은 아래를 참고하세요).
g2[1, 2] = 999
g2
array([[ 0, 1, 2, 3, 4, 5], [ 6, 7, 999, 9, 10, 11], [ 12, 13, 14, 15, 16, 17], [ 18, 19, 20, 21, 22, 23]])
이에 상응하는 g
의 원소도 수정됩니다.
g
array([[[ 0, 1, 2, 3], [ 4, 5, 6, 7], [999, 9, 10, 11]], [[ 12, 13, 14, 15], [ 16, 17, 18, 19], [ 20, 21, 22, 23]]])
ravel
¶마지막으로 ravel
함수는 동일한 데이터를 가리키는 새로운 1차원 ndarray
를 반환합니다:
g.ravel()
array([ 0, 1, 2, 3, 4, 5, 6, 7, 999, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
일반적인 산술 연산자(+
, -
, *
, /
, //
, **
등)는 모두 ndarray
와 사용할 수 있습니다. 이 연산자는 원소별로 적용됩니다:
a = np.array([14, 23, 32, 41])
b = np.array([5, 4, 3, 2])
print("a + b =", a + b)
print("a - b =", a - b)
print("a * b =", a * b)
print("a / b =", a / b)
print("a // b =", a // b)
print("a % b =", a % b)
print("a ** b =", a ** b)
a + b = [19 27 35 43] a - b = [ 9 19 29 39] a * b = [70 92 96 82] a / b = [ 2.8 5.75 10.66666667 20.5 ] a // b = [ 2 5 10 20] a % b = [4 3 2 1] a ** b = [537824 279841 32768 1681]
여기 곱셈은 행렬 곱셈이 아닙니다. 행렬 연산은 아래에서 설명합니다.
배열의 크기는 같아야 합니다. 그렇지 않으면 넘파이가 브로드캐스팅 규칙을 적용합니다.
일반적으로 넘파이는 동일한 크기의 배열을 기대합니다. 그렇지 않은 상황에는 브로드캐시틍 규칙을 적용합니다:
배열의 랭크가 동일하지 않으면 랭크가 맞을 때까지 랭크가 작은 배열 앞에 1을 추가합니다.
h = np.arange(5).reshape(1, 1, 5)
h
array([[[0, 1, 2, 3, 4]]])
여기에 (1,1,5)
크기의 3D 배열에 (5,)
크기의 1D 배열을 더해 보죠. 브로드캐스팅의 규칙 1이 적용됩니다!
h + [10, 20, 30, 40, 50] # 다음과 동일합니다: h + [[[10, 20, 30, 40, 50]]]
array([[[10, 21, 32, 43, 54]]])
특정 차원이 1인 배열은 그 차원에서 크기가 가장 큰 배열의 크기에 맞춰 동작합니다. 배열의 원소가 차원을 따라 반복됩니다.
k = np.arange(6).reshape(2, 3)
k
array([[0, 1, 2], [3, 4, 5]])
(2,3)
크기의 2D ndarray
에 (2,1)
크기의 2D 배열을 더해 보죠. 넘파이는 브로드캐스팅 규칙 2를 적용합니다:
k + [[100], [200]] # 다음과 같습니다: k + [[100, 100, 100], [200, 200, 200]]
array([[100, 101, 102], [203, 204, 205]])
규칙 1과 2를 합치면 다음과 같이 동작합니다:
k + [100, 200, 300] # 규칙 1 적용: [[100, 200, 300]], 규칙 2 적용: [[100, 200, 300], [100, 200, 300]]
array([[100, 201, 302], [103, 204, 305]])
또 매우 간단히 다음 처럼 해도 됩니다:
k + 1000 # 다음과 같습니다: k + [[1000, 1000, 1000], [1000, 1000, 1000]]
array([[1000, 1001, 1002], [1003, 1004, 1005]])
규칙 1 & 2을 적용했을 때 모든 배열의 크기가 맞아야 합니다.
try:
k + [33, 44]
except ValueError as e:
print(e)
operands could not be broadcast together with shapes (2,3) (2,)
브로드캐스팅 규칙은 산술 연산 뿐만 아니라 넘파이 연산에서 많이 사용됩니다. 아래에서 더 보도록 하죠. 브로드캐스팅에 관한 더 자세한 정보는 온라인 문서를 참고하세요.
dtype
이 다른 배열을 합칠 때 넘파이는 (실제 값에 상관없이) 모든 값을 다룰 수 있는 타입으로 업캐스팅합니다.
k1 = np.arange(0, 5, dtype=np.uint8)
print(k1.dtype, k1)
uint8 [0 1 2 3 4]
k2 = k1 + np.array([5, 6, 7, 8, 9], dtype=np.int8)
print(k2.dtype, k2)
int16 [ 5 7 9 11 13]
모든 int8
과 uint8
값(-128에서 255까지)을 표현하기 위해 int16
이 필요합니다. 이 코드에서는 uint8
이면 충분하지만 업캐스팅되었습니다.
k3 = k1 + 1.5
print(k3.dtype, k3)
float64 [1.5 2.5 3.5 4.5 5.5]
조건 연산자도 원소별로 적용됩니다:
m = np.array([20, -5, 30, 40])
m < [15, 16, 35, 36]
array([False, True, True, False])
브로드캐스팅을 사용합니다:
m < 25 # m < [25, 25, 25, 25] 와 동일
array([ True, True, False, False])
불리언 인덱싱과 함께 사용하면 아주 유용합니다(아래에서 설명하겠습니다).
m[m < 25]
array([20, -5])
a = np.array([[-2.5, 3.1, 7], [10, 11, 12]])
print(a)
print("평균 =", a.mean())
[[-2.5 3.1 7. ] [10. 11. 12. ]] 평균 = 6.766666666666667
이 명령은 크기에 상관없이 ndarray
에 있는 모든 원소의 평균을 계산합니다.
다음은 유용한 ndarray
메서드입니다:
for func in (a.min, a.max, a.sum, a.prod, a.std, a.var):
print(func.__name__, "=", func())
min = -2.5 max = 12.0 sum = 40.6 prod = -71610.0 std = 5.084835843520964 var = 25.855555555555554
이 함수들은 선택적으로 매개변수 axis
를 사용합니다. 지정된 축을 따라 원소에 연산을 적용하는데 사용합니다. 예를 들면:
c=np.arange(24).reshape(2,3,4)
c
array([[[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]], [[12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]]])
c.sum(axis=0) # 첫 번째 축을 따라 더함, 결과는 3x4 배열
array([[12, 14, 16, 18], [20, 22, 24, 26], [28, 30, 32, 34]])
c.sum(axis=1) # 두 번째 축을 따라 더함, 결과는 2x4 배열
array([[12, 15, 18, 21], [48, 51, 54, 57]])
여러 축에 대해서 더할 수도 있습니다:
c.sum(axis=(0,2)) # 첫 번째 축과 세 번째 축을 따라 더함, 결과는 (3,) 배열
array([ 60, 92, 124])
0+1+2+3 + 12+13+14+15, 4+5+6+7 + 16+17+18+19, 8+9+10+11 + 20+21+22+23
(60, 92, 124)
넘파이는 일반 함수(universal function) 또는 ufunc라고 부르는 원소별 함수를 제공합니다. 예를 들면 square
함수는 원본 ndarray
를 복사하여 각 원소를 제곱한 새로운 ndarray
객체를 반환합니다:
a = np.array([[-2.5, 3.1, 7], [10, 11, 12]])
np.square(a)
array([[ 6.25, 9.61, 49. ], [100. , 121. , 144. ]])
다음은 유용한 단항 일반 함수들입니다:
print("원본 ndarray")
print(a)
for func in (np.abs, np.sqrt, np.exp, np.log, np.sign, np.ceil, np.modf, np.isnan, np.cos):
print("\n", func.__name__)
print(func(a))
원본 ndarray [[-2.5 3.1 7. ] [10. 11. 12. ]] absolute [[ 2.5 3.1 7. ] [10. 11. 12. ]] sqrt [[ nan 1.76068169 2.64575131] [3.16227766 3.31662479 3.46410162]] exp [[8.20849986e-02 2.21979513e+01 1.09663316e+03] [2.20264658e+04 5.98741417e+04 1.62754791e+05]] log [[ nan 1.13140211 1.94591015] [2.30258509 2.39789527 2.48490665]] sign [[-1. 1. 1.] [ 1. 1. 1.]] ceil [[-2. 4. 7.] [10. 11. 12.]] modf (array([[-0.5, 0.1, 0. ], [ 0. , 0. , 0. ]]), array([[-2., 3., 7.], [10., 11., 12.]])) isnan [[False False False] [False False False]] cos [[-0.80114362 -0.99913515 0.75390225] [-0.83907153 0.0044257 0.84385396]]
/home/haesun/anaconda3/envs/homl2/lib/python3.7/site-packages/ipykernel_launcher.py:5: RuntimeWarning: invalid value encountered in sqrt """ /home/haesun/anaconda3/envs/homl2/lib/python3.7/site-packages/ipykernel_launcher.py:5: RuntimeWarning: invalid value encountered in log """
두 개의 ndarray
에 원소별로 적용되는 이항 함수도 많습니다. 두 배열이 동일한 크기가 아니면 브로드캐스팅 규칙이 적용됩니다:
a = np.array([1, -2, 3, 4])
b = np.array([2, 8, -1, 7])
np.add(a, b) # a + b 와 동일
array([ 3, 6, 2, 11])
np.greater(a, b) # a > b 와 동일
array([False, False, True, False])
np.maximum(a, b)
array([2, 8, 3, 7])
np.copysign(a, b)
array([ 1., 2., -3., 4.])
a = np.array([1, 5, 3, 19, 13, 7, 3])
a[3]
19
a[2:5]
array([ 3, 19, 13])
a[2:-1]
array([ 3, 19, 13, 7])
a[:2]
array([1, 5])
a[2::2]
array([ 3, 13, 3])
a[::-1]
array([ 3, 7, 13, 19, 3, 5, 1])
물론 원소를 수정할 수 있죠:
a[3]=999
a
array([ 1, 5, 3, 999, 13, 7, 3])
슬라이싱을 사용해 ndarray
를 수정할 수 있습니다:
a[2:5] = [997, 998, 999]
a
array([ 1, 5, 997, 998, 999, 7, 3])
보통의 파이썬 배열과 대조적으로 ndarray
슬라이싱에 하나의 값을 할당하면 슬라이싱 전체에 복사됩니다. 위에서 언급한 브로드캐스팅 덕택입니다.
a[2:5] = -1
a
array([ 1, 5, -1, -1, -1, 7, 3])
또한 이런 식으로 ndarray
크기를 늘리거나 줄일 수 없습니다:
try:
a[2:5] = [1,2,3,4,5,6] # 너무 길어요
except ValueError as e:
print(e)
cannot copy sequence with size 6 to array axis with dimension 3
원소를 삭제할 수도 없습니다:
try:
del a[2:5]
except ValueError as e:
print(e)
cannot delete array elements
중요한 점은 ndarray
의 슬라이싱은 같은 데이터 버퍼를 바라보는 뷰(view)입니다. 슬라이싱된 객체를 수정하면 실제 원본 ndarray
가 수정됩니다!
a_slice = a[2:6]
a_slice[1] = 1000
a # 원본 배열이 수정됩니다!
array([ 1, 5, -1, 1000, -1, 7, 3])
a[3] = 2000
a_slice # 비슷하게 원본 배열을 수정하면 슬라이싱 객체에도 반영됩니다!
array([ -1, 2000, -1, 7])
데이터를 복사하려면 copy
메서드를 사용해야 합니다:
another_slice = a[2:6].copy()
another_slice[1] = 3000
a # 원본 배열이 수정되지 않습니다
array([ 1, 5, -1, 2000, -1, 7, 3])
a[3] = 4000
another_slice # 마찬가지로 원본 배열을 수정해도 복사된 배열은 바뀌지 않습니다
array([ -1, 3000, -1, 7])
다차원 배열은 비슷한 방식으로 각 축을 따라 인덱싱 또는 슬라이싱해서 사용합니다. 콤마로 구분합니다:
b = np.arange(48).reshape(4, 12)
b
array([[ 0, 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]])
b[1, 2] # 행 1, 열 2
14
b[1, :] # 행 1, 모든 열
array([12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
b[:, 1] # 모든 행, 열 1
array([ 1, 13, 25, 37])
주의: 다음 두 표현에는 미묘한 차이가 있습니다:
b[1, :]
array([12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
b[1:2, :]
array([[12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]])
첫 번째 표현식은 (12,)
크기인 1D 배열로 행이 하나입니다. 두 번째는 (1, 12)
크기인 2D 배열로 같은 행을 반환합니다.
관심 대상의 인덱스 리스트를 지정할 수도 있습니다. 이를 팬시 인덱싱이라고 부릅니다.
b[(0,2), 2:5] # 행 0과 2, 열 2에서 4(5-1)까지
array([[ 2, 3, 4], [26, 27, 28]])
b[:, (-1, 2, -1)] # 모든 행, 열 -1 (마지막), 2와 -1 (다시 반대 방향으로)
array([[11, 2, 11], [23, 14, 23], [35, 26, 35], [47, 38, 47]])
여러 개의 인덱스 리스트를 지정하면 인덱스에 맞는 값이 포함된 1D ndarray
를 반환됩니다.
b[(-1, 2, -1, 2), (5, 9, 1, 9)] # returns a 1D array with b[-1, 5], b[2, 9], b[-1, 1] and b[2, 9] (again)
array([41, 33, 37, 33])
고차원에서도 동일한 방식이 적용됩니다. 몇 가지 예를 살펴 보겠습니다:
c = b.reshape(4,2,6)
c
array([[[ 0, 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]]])
c[2, 1, 4] # 행렬 2, 행 1, 열 4
34
c[2, :, 3] # 행렬 2, 모든 행, 열 3
array([27, 33])
어떤 축에 대한 인덱스를 지정하지 않으면 이 축의 모든 원소가 반환됩니다:
c[2, 1] # 행렬 2, 행 1, 모든 열이 반환됩니다. c[2, 1, :]와 동일합니다.
array([30, 31, 32, 33, 34, 35])
...
)¶생략 부호(...
)를 쓰면 모든 지정하지 않은 축의 원소를 포함합니다.
c[2, ...] # 행렬 2, 모든 행, 모든 열. c[2, :, :]와 동일
array([[24, 25, 26, 27, 28, 29], [30, 31, 32, 33, 34, 35]])
c[2, 1, ...] # 행렬 2, 행 1, 모든 열. c[2, 1, :]와 동일
array([30, 31, 32, 33, 34, 35])
c[2, ..., 3] # 행렬 2, 모든 행, 열 3. c[2, :, 3]와 동일
array([27, 33])
c[..., 3] # 모든 행렬, 모든 행, 열 3. c[:, :, 3]와 동일
array([[ 3, 9], [15, 21], [27, 33], [39, 45]])
불리언 값을 가진 ndarray
를 사용해 축의 인덱스를 지정할 수 있습니다.
b = np.arange(48).reshape(4, 12)
b
array([[ 0, 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]])
rows_on = np.array([True, False, True, False])
b[rows_on, :] # 행 0과 2, 모든 열. b[(0, 2), :]와 동일
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]])
cols_on = np.array([False, True, False] * 4)
b[:, cols_on] # 모든 행, 열 1, 4, 7, 10
array([[ 1, 4, 7, 10], [13, 16, 19, 22], [25, 28, 31, 34], [37, 40, 43, 46]])
np.ix_
¶여러 축에 걸쳐서는 불리언 인덱싱을 사용할 수 없고 ix_
함수를 사용합니다:
b[np.ix_(rows_on, cols_on)]
array([[ 1, 4, 7, 10], [25, 28, 31, 34]])
np.ix_(rows_on, cols_on)
(array([[0], [2]]), array([[ 1, 4, 7, 10]]))
ndarray
와 같은 크기의 불리언 배열을 사용하면 해당 위치가 True
인 모든 원소를 담은 1D 배열이 반환됩니다. 일반적으로 조건 연산자와 함께 사용합니다:
b[b % 3 == 1]
array([ 1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46])
ndarray
를 반복하는 것은 일반적인 파이썬 배열을 반복한는 것과 매우 유사합니다. 다차원 배열을 반복하면 첫 번째 축에 대해서 수행됩니다.
c = np.arange(24).reshape(2, 3, 4) # 3D 배열 (두 개의 3x4 행렬로 구성됨)
c
array([[[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]], [[12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]]])
for m in c:
print("아이템:")
print(m)
아이템: [[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]] 아이템: [[12 13 14 15] [16 17 18 19] [20 21 22 23]]
for i in range(len(c)): # len(c) == c.shape[0]
print("아이템:")
print(c[i])
아이템: [[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]] 아이템: [[12 13 14 15] [16 17 18 19] [20 21 22 23]]
ndarray
에 있는 모든 원소를 반복하려면 flat
속성을 사용합니다:
for i in c.flat:
print("아이템:", i)
아이템: 0 아이템: 1 아이템: 2 아이템: 3 아이템: 4 아이템: 5 아이템: 6 아이템: 7 아이템: 8 아이템: 9 아이템: 10 아이템: 11 아이템: 12 아이템: 13 아이템: 14 아이템: 15 아이템: 16 아이템: 17 아이템: 18 아이템: 19 아이템: 20 아이템: 21 아이템: 22 아이템: 23
종종 다른 배열을 쌓아야 할 때가 있습니다. 넘파이는 이를 위해 몇 개의 함수를 제공합니다. 먼저 배열 몇 개를 만들어 보죠.
q1 = np.full((3,4), 1.0)
q1
array([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]])
q2 = np.full((4,4), 2.0)
q2
array([[2., 2., 2., 2.], [2., 2., 2., 2.], [2., 2., 2., 2.], [2., 2., 2., 2.]])
q3 = np.full((3,4), 3.0)
q3
array([[3., 3., 3., 3.], [3., 3., 3., 3.], [3., 3., 3., 3.]])
vstack
¶vstack
함수를 사용하여 수직으로 쌓아보죠:
q4 = np.vstack((q1, q2, q3))
q4
array([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.], [2., 2., 2., 2.], [2., 2., 2., 2.], [2., 2., 2., 2.], [2., 2., 2., 2.], [3., 3., 3., 3.], [3., 3., 3., 3.], [3., 3., 3., 3.]])
q4.shape
(10, 4)
q5 = np.hstack((q1, q3))
q5
array([[1., 1., 1., 1., 3., 3., 3., 3.], [1., 1., 1., 1., 3., 3., 3., 3.], [1., 1., 1., 1., 3., 3., 3., 3.]])
q5.shape
(3, 8)
q1과 q3가 모두 3개의 행을 가지고 있기 때문에 가능합니다. q2는 4개의 행을 가지고 있기 때문에 q1, q3와 수평으로 쌓을 수 없습니다:
try:
q5 = np.hstack((q1, q2, q3))
except ValueError as e:
print(e)
all the input array dimensions for the concatenation axis must match exactly, but along dimension 0, the array at index 0 has size 3 and the array at index 1 has size 4
concatenate
¶concatenate
함수는 지정한 축으로도 배열을 쌓습니다.
q7 = np.concatenate((q1, q2, q3), axis=0) # vstack과 동일
q7
array([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.], [2., 2., 2., 2.], [2., 2., 2., 2.], [2., 2., 2., 2.], [2., 2., 2., 2.], [3., 3., 3., 3.], [3., 3., 3., 3.], [3., 3., 3., 3.]])
q7.shape
(10, 4)
예상했겠지만 hstack
은 axis=1
으로 concatenate
를 호출하는 것과 같습니다.
stack
¶stack
함수는 새로운 축을 따라 배열을 쌓습니다. 모든 배열은 같은 크기를 가져야 합니다.
q8 = np.stack((q1, q3))
q8
array([[[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]], [[3., 3., 3., 3.], [3., 3., 3., 3.], [3., 3., 3., 3.]]])
q8.shape
(2, 3, 4)
r = np.arange(24).reshape(6,4)
r
array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11], [12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]])
수직으로 동일한 크기로 나누어 보겠습니다:
r1, r2, r3 = np.vsplit(r, 3)
r1
array([[0, 1, 2, 3], [4, 5, 6, 7]])
r2
array([[ 8, 9, 10, 11], [12, 13, 14, 15]])
r3
array([[16, 17, 18, 19], [20, 21, 22, 23]])
split
함수는 주어진 축을 따라 배열을 분할합니다. vsplit
는 axis=0
으로 split
를 호출하는 것과 같습니다. hsplit
함수는 axis=1
로 split
를 호출하는 것과 같습니다:
r4, r5 = np.hsplit(r, 2)
r4
array([[ 0, 1], [ 4, 5], [ 8, 9], [12, 13], [16, 17], [20, 21]])
r5
array([[ 2, 3], [ 6, 7], [10, 11], [14, 15], [18, 19], [22, 23]])
t = np.arange(24).reshape(4,2,3)
t
array([[[ 0, 1, 2], [ 3, 4, 5]], [[ 6, 7, 8], [ 9, 10, 11]], [[12, 13, 14], [15, 16, 17]], [[18, 19, 20], [21, 22, 23]]])
0, 1, 2
(깊이, 높이, 너비) 축을 1, 2, 0
(깊이→너비, 높이→깊이, 너비→높이) 순서로 바꾼 ndarray
를 만들어 보겠습니다:
t1 = t.transpose((1,2,0))
t1
array([[[ 0, 6, 12, 18], [ 1, 7, 13, 19], [ 2, 8, 14, 20]], [[ 3, 9, 15, 21], [ 4, 10, 16, 22], [ 5, 11, 17, 23]]])
t1.shape
(2, 3, 4)
transpose
기본값은 차원의 순서를 역전시킵니다:
t2 = t.transpose() # t.transpose((2, 1, 0))와 동일
t2
array([[[ 0, 6, 12, 18], [ 3, 9, 15, 21]], [[ 1, 7, 13, 19], [ 4, 10, 16, 22]], [[ 2, 8, 14, 20], [ 5, 11, 17, 23]]])
t2.shape
(3, 2, 4)
넘파이는 두 축을 바꾸는 swapaxes
함수를 제공합니다. 예를 들어 깊이와 높이를 뒤바꾸어 t
의 새로운 뷰를 만들어 보죠:
t3 = t.swapaxes(0,1) # t.transpose((1, 0, 2))와 동일
t3
array([[[ 0, 1, 2], [ 6, 7, 8], [12, 13, 14], [18, 19, 20]], [[ 3, 4, 5], [ 9, 10, 11], [15, 16, 17], [21, 22, 23]]])
t3.shape
(2, 4, 3)
넘파이 2D 배열을 사용하면 파이썬에서 행렬을 효율적으로 표현할 수 있습니다. 주요 행렬 연산을 간단히 둘러 보겠습니다. 선형 대수학, 벡터와 행렬에 관한 자세한 내용은 Linear Algebra tutorial를 참고하세요.
T
속성은 랭크가 2보다 크거나 같을 때 transpose()
를 호출하는 것과 같습니다:
m1 = np.arange(10).reshape(2,5)
m1
array([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]])
m1.T
array([[0, 5], [1, 6], [2, 7], [3, 8], [4, 9]])
T
속성은 랭크가 0이거나 1인 배열에는 아무런 영향을 미치지 않습니다:
m2 = np.arange(5)
m2
array([0, 1, 2, 3, 4])
m2.T
array([0, 1, 2, 3, 4])
먼저 1D 배열을 하나의 행이 있는 행렬(2D)로 바꾼다음 전치를 수행할 수 있습니다:
m2r = m2.reshape(1,5)
m2r
array([[0, 1, 2, 3, 4]])
m2r.T
array([[0], [1], [2], [3], [4]])
n1 = np.arange(10).reshape(2, 5)
n1
array([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]])
n2 = np.arange(15).reshape(5,3)
n2
array([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11], [12, 13, 14]])
n1.dot(n2)
array([[ 90, 100, 110], [240, 275, 310]])
주의: 앞서 언급한 것처럼 n1*n2
는 행렬 곱셈이 아니라 원소별 곱셈(또는 아다마르 곱이라 부릅니다)입니다.
numpy.linalg
모듈 안에 많은 선형 대수 함수들이 있습니다. 특히 inv
함수는 정방 행렬의 역행렬을 계산합니다:
import numpy.linalg as linalg
m3 = np.array([[1,2,3],[5,7,11],[21,29,31]])
m3
array([[ 1, 2, 3], [ 5, 7, 11], [21, 29, 31]])
linalg.inv(m3)
array([[-2.31818182, 0.56818182, 0.02272727], [ 1.72727273, -0.72727273, 0.09090909], [-0.04545455, 0.29545455, -0.06818182]])
pinv
함수를 사용하여 유사 역행렬을 계산할 수도 있습니다:
linalg.pinv(m3)
array([[-2.31818182, 0.56818182, 0.02272727], [ 1.72727273, -0.72727273, 0.09090909], [-0.04545455, 0.29545455, -0.06818182]])
행렬과 그 행렬의 역행렬을 곱하면 단위 행렬이 됩니다(작은 소숫점 오차가 있습니다):
m3.dot(linalg.inv(m3))
array([[ 1.00000000e+00, -1.66533454e-16, 0.00000000e+00], [ 6.31439345e-16, 1.00000000e+00, -1.38777878e-16], [ 5.21110932e-15, -2.38697950e-15, 1.00000000e+00]])
eye
함수는 NxN 크기의 단위 행렬을 만듭니다:
np.eye(3)
array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])
q, r = linalg.qr(m3)
q
array([[-0.04627448, 0.98786672, 0.14824986], [-0.23137241, 0.13377362, -0.96362411], [-0.97176411, -0.07889213, 0.22237479]])
r
array([[-21.61018278, -29.89331494, -32.80860727], [ 0. , 0.62427688, 1.9894538 ], [ 0. , 0. , -3.26149699]])
q.dot(r) # q.r는 m3와 같습니다
array([[ 1., 2., 3.], [ 5., 7., 11.], [21., 29., 31.]])
linalg.det(m3) # 행렬식 계산
43.99999999999997
eigenvalues, eigenvectors = linalg.eig(m3)
eigenvalues # λ
array([42.26600592, -0.35798416, -2.90802176])
eigenvectors # v
array([[-0.08381182, -0.76283526, -0.18913107], [-0.3075286 , 0.64133975, -0.6853186 ], [-0.94784057, -0.08225377, 0.70325518]])
m3.dot(eigenvectors) - eigenvalues * eigenvectors # m3.v - λ*v = 0
array([[ 6.66133815e-15, 1.66533454e-15, -3.10862447e-15], [ 7.10542736e-15, 5.16253706e-15, -5.32907052e-15], [ 3.55271368e-14, 4.94743135e-15, -9.76996262e-15]])
m4 = np.array([[1,0,0,0,2], [0,0,3,0,0], [0,0,0,0,0], [0,2,0,0,0]])
m4
array([[1, 0, 0, 0, 2], [0, 0, 3, 0, 0], [0, 0, 0, 0, 0], [0, 2, 0, 0, 0]])
U, S_diag, V = linalg.svd(m4)
U
array([[ 0., 1., 0., 0.], [ 1., 0., 0., 0.], [ 0., 0., 0., -1.], [ 0., 0., 1., 0.]])
S_diag
array([3. , 2.23606798, 2. , 0. ])
svd
함수는 Σ의 대각 원소 값만 반환합니다. 전체 Σ 행렬은 다음과 같이 만듭니다:
S = np.zeros((4, 5))
S[np.diag_indices(4)] = S_diag
S # Σ
array([[3. , 0. , 0. , 0. , 0. ], [0. , 2.23606798, 0. , 0. , 0. ], [0. , 0. , 2. , 0. , 0. ], [0. , 0. , 0. , 0. , 0. ]])
V
array([[-0. , 0. , 1. , -0. , 0. ], [ 0.4472136 , 0. , 0. , 0. , 0.89442719], [-0. , 1. , 0. , -0. , 0. ], [ 0. , 0. , 0. , 1. , 0. ], [-0.89442719, 0. , 0. , 0. , 0.4472136 ]])
U.dot(S).dot(V) # U.Σ.V == m4
array([[1., 0., 0., 0., 2.], [0., 0., 3., 0., 0.], [0., 0., 0., 0., 0.], [0., 2., 0., 0., 0.]])
np.diag(m3) # m3의 대각 원소입니다(왼쪽 위에서 오른쪽 아래)
array([ 1, 7, 31])
np.trace(m3) # np.diag(m3).sum()와 같습니다
39
solve
함수는 다음과 같은 선형 방정식을 풉니다:
coeffs = np.array([[2, 6], [5, 3]])
depvars = np.array([6, -9])
solution = linalg.solve(coeffs, depvars)
solution
array([-3., 2.])
solution을 확인해 보죠:
coeffs.dot(solution), depvars # 네 같네요
(array([ 6., -9.]), array([ 6, -9]))
좋습니다! 다른 방식으로도 solution을 확인해 보죠:
np.allclose(coeffs.dot(solution), depvars)
True
한 번에 하나씩 개별 배열 원소에 대해 연산을 실행하는 대신 배열 연산을 사용하면 훨씬 효율적인 코드를 만들 수 있습니다. 이를 벡터화라고 합니다. 이를 사용하여 넘파이의 최적화된 성능을 활용할 수 있습니다.
예를 들어, $sin(xy/40.5)$ 식을 기반으로 768x1024 크기 배열을 생성하려고 합니다. 중첩 반복문 안에 파이썬의 math 함수를 사용하는 것은 나쁜 방법입니다:
import math
data = np.empty((768, 1024))
for y in range(768):
for x in range(1024):
data[y, x] = math.sin(x*y/40.5) # 매우 비효율적입니다!
작동은 하지만 순수한 파이썬 코드로 반복문이 진행되기 때문에 아주 비효율적입니다. 이 알고리즘을 벡터화해 보죠. 먼저 넘파이 meshgrid
함수로 좌표 벡터를 사용해 행렬을 만듭니다.
x_coords = np.arange(0, 1024) # [0, 1, 2, ..., 1023]
y_coords = np.arange(0, 768) # [0, 1, 2, ..., 767]
X, Y = np.meshgrid(x_coords, y_coords)
X
array([[ 0, 1, 2, ..., 1021, 1022, 1023], [ 0, 1, 2, ..., 1021, 1022, 1023], [ 0, 1, 2, ..., 1021, 1022, 1023], ..., [ 0, 1, 2, ..., 1021, 1022, 1023], [ 0, 1, 2, ..., 1021, 1022, 1023], [ 0, 1, 2, ..., 1021, 1022, 1023]])
Y
array([[ 0, 0, 0, ..., 0, 0, 0], [ 1, 1, 1, ..., 1, 1, 1], [ 2, 2, 2, ..., 2, 2, 2], ..., [765, 765, 765, ..., 765, 765, 765], [766, 766, 766, ..., 766, 766, 766], [767, 767, 767, ..., 767, 767, 767]])
여기서 볼 수 있듯이 X
와 Y
모두 768x1024 배열입니다. X
에 있는 모든 값은 수평 좌표에 해당합니다. Y
에 있는 모든 값은 수직 좌표에 해당합니다.
이제 간단히 배열 연산을 사용해 계산할 수 있습니다:
data = np.sin(X*Y/40.5)
맷플롯립의 imshow
함수를 사용해 이 데이터를 그려보죠(matplotlib tutorial을 참조하세요).
import matplotlib.pyplot as plt
import matplotlib.cm as cm
fig = plt.figure(1, figsize=(7, 6))
plt.imshow(data, cmap=cm.hot)
plt.show()
a = np.random.rand(2,3)
a
array([[0.99686149, 0.00408438, 0.90786167], [0.94890767, 0.43262855, 0.8701696 ]])
np.save("my_array", a)
끝입니다! 파일 이름의 확장자를 지정하지 않았기 때문에 넘파이는 자동으로 .npy
를 붙입니다. 파일 내용을 확인해 보겠습니다:
with open("my_array.npy", "rb") as f:
content = f.read()
content
b"\x93NUMPY\x01\x00v\x00{'descr': '<f8', 'fortran_order': False, 'shape': (2, 3), } \n\x86\xd5\x81\x12J\xe6\xef?\x00\xde\xb3\xd1\xc7\xbap?\xe7\xb1\n\xea3\r\xed?\xf7,\xe8\x9es]\xee?\xb2\xa3M\xa7/\xb0\xdb?\xbc\x00\x04\xe9m\xd8\xeb?"
이 파일을 넘파이 배열로 로드하려면 load
함수를 사용합니다:
a_loaded = np.load("my_array.npy")
a_loaded
array([[0.99686149, 0.00408438, 0.90786167], [0.94890767, 0.43262855, 0.8701696 ]])
배열을 텍스트 포맷으로 저장해 보죠:
np.savetxt("my_array.csv", a)
파일 내용을 확인해 보겠습니다:
with open("my_array.csv", "rt") as f:
print(f.read())
9.968614922969869507e-01 4.084377807195505028e-03 9.078616686943491443e-01 9.489076713229270821e-01 4.326285489865383527e-01 8.701695967756886141e-01
이 파일은 탭으로 구분된 CSV 파일입니다. 다른 구분자를 지정할 수도 있습니다:
np.savetxt("my_array.csv", a, delimiter=",")
이 파일을 로드하려면 loadtxt
함수를 사용합니다:
a_loaded = np.loadtxt("my_array.csv", delimiter=",")
a_loaded
array([[0.99686149, 0.00408438, 0.90786167], [0.94890767, 0.43262855, 0.8701696 ]])
.npz
포맷¶여러 개의 배열을 압축된 한 파일로 저장하는 것도 가능합니다:
b = np.arange(24, dtype=np.uint8).reshape(2, 3, 4)
b
array([[[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]], [[12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]]], dtype=uint8)
np.savez("my_arrays", my_a=a, my_b=b)
파일 내용을 확인해 보죠. .npz
파일 확장자가 자동으로 추가되었습니다.
with open("my_arrays.npz", "rb") as f:
content = f.read()
repr(content)[:180] + "[...]"
'b"PK\\x03\\x04\\x14\\x00\\x00\\x00\\x00\\x00\\x00\\x00!\\x00\\xb7UG\\xdc\\xb0\\x00\\x00\\x00\\xb0\\x00\\x00\\x00\\x08\\x00\\x14\\x00my_a.npy\\x01\\x00\\x10\\x00\\xb0\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xb0\\x00\\x00\\x00\\[...]'
다음과 같이 이 파일을 로드할 수 있습니다:
my_arrays = np.load("my_arrays.npz")
my_arrays
<numpy.lib.npyio.NpzFile at 0x7f14f03dce90>
게으른 로딩을 수행하는 딕셔너리와 유사한 객체입니다:
my_arrays.keys()
KeysView(<numpy.lib.npyio.NpzFile object at 0x7f14f03dce90>)
my_arrays["my_a"]
array([[0.99686149, 0.00408438, 0.90786167], [0.94890767, 0.43262855, 0.8701696 ]])