도구 - 넘파이(NumPy)
*넘파이(NumPy)는 파이썬의 과학 컴퓨팅을 위한 기본 라이브러리입니다. 넘파이의 핵심은 강력한 N-차원 배열 객체입니다. 또한 선형 대수, 푸리에(Fourier) 변환, 유사 난수 생성과 같은 유용한 함수들도 제공합니다."
numpy
를 임포트해 보죠. 대부분의 사람들이 np
로 알리아싱하여 임포트합니다:
import numpy as np
np.zeros
¶zeros
함수는 0으로 채워진 배열을 만듭니다:
np.zeros(5)
2D 배열(즉, 행렬)을 만들려면 원하는 행과 열의 크기를 튜플로 전달합니다. 예를 들어 다음은 $3 \times 4$ 크기의 행렬입니다:
np.zeros((3,4))
(3, 4)
입니다.a = np.zeros((3,4))
a
a.shape
a.ndim # len(a.shape)와 같습니다
a.size
임의의 랭크 수를 가진 N-차원 배열을 만들 수 있습니다. 예를 들어, 다음은 크기가 (2,3,4)
인 3D 배열(랭크=3)입니다:
np.zeros((2,3,4))
넘파이 배열의 타입은 ndarray
입니다:
type(np.zeros((3,4)))
np.ones((3,4))
np.full
¶주어진 값으로 지정된 크기의 배열을 초기화합니다. 다음은 π
로 채워진 $3 \times 4$ 크기의 행렬입니다.
np.full((3,4), np.pi)
np.empty
¶초기화되지 않은 $2 \times 3$ 크기의 배열을 만듭니다(배열의 내용은 예측이 불가능하며 메모리 상황에 따라 달라집니다):
np.empty((2,3))
array
함수는 파이썬 리스트를 사용하여 ndarray
를 초기화합니다:
np.array([[1,2,3,4], [10, 20, 30, 40]])
np.arange
¶파이썬의 기본 range
함수와 비슷한 넘파이 arange
함수를 사용하여 ndarray
를 만들 수 있습니다:
np.arange(1, 5)
부동 소수도 가능합니다:
np.arange(1.0, 5.0)
파이썬의 기본 range
함수처럼 건너 뛰는 정도를 지정할 수 있습니다:
np.arange(1, 5, 0.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))
np.linspace
¶이런 이유로 부동 소수를 사용할 땐 arange
대신에 linspace
함수를 사용하는 것이 좋습니다. linspace
함수는 지정된 개수만큼 두 값 사이를 나눈 배열을 반환합니다(arange
와는 다르게 최댓값이 포함됩니다):
print(np.linspace(0, 5/3, 6))
np.rand
와 np.randn
¶넘파이의 random
모듈에는 ndarray
를 랜덤한 값으로 초기화할 수 있는 함수들이 많이 있습니다.
예를 들어, 다음은 (균등 분포인) 0과 1사이의 랜덤한 부동 소수로 $3 \times 4$ 행렬을 초기화합니다:
np.random.rand(3,4)
다음은 평균이 0이고 분산이 1인 일변량 정규 분포(가우시안 분포)에서 샘플링한 랜덤한 부동 소수를 담은 $3 \times 4$ 행렬입니다:
np.random.randn(3,4)
이 분포의 모양을 알려면 맷플롯립을 사용해 그려보는 것이 좋습니다(더 자세한 것은 맷플롯립 튜토리얼을 참고하세요):
%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))
넘파이는 먼저 크기가 (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)
c = np.arange(1.0, 5.0)
print(c.dtype, c)
넘파이가 데이터 타입을 결정하도록 내버려 두는 대신 dtype
매개변수를 사용해서 배열을 만들 때 명시적으로 지정할 수 있습니다:
d = np.arange(1, 5, dtype=np.complex64)
print(d.dtype, d)
e = np.arange(1, 5, dtype=np.complex64)
e.itemsize
data
버퍼¶배열의 데이터는 1차원 바이트 버퍼로 메모리에 저장됩니다. data
속성을 사용해 참조할 수 있습니다(사용할 일은 거의 없겠지만요).
f = np.array([[1,2],[1000, 2000]], dtype=np.int32)
f.data
파이썬 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
여러 개의 ndarray
가 데이터 버퍼를 공유할 수 있습니다. 하나를 수정하면 다른 것도 바뀝니다. 잠시 후에 예를 살펴 보겠습니다.
g = np.arange(24)
print(g)
print("랭크:", g.ndim)
g.shape = (6, 4)
print(g)
print("랭크:", g.ndim)
g.shape = (2, 3, 4)
print(g)
print("랭크:", g.ndim)
reshape
¶reshape
함수는 동일한 데이터를 가리키는 새로운 ndarray
객체를 반환합니다. 한 배열을 수정하면 다른 것도 함께 바뀝니다.
g2 = g.reshape(4,6)
print(g2)
print("랭크:", g2.ndim)
행 1, 열 2의 원소를 999로 설정합니다(인덱싱 방식은 아래를 참고하세요).
g2[1, 2] = 999
g2
이에 상응하는 g
의 원소도 수정됩니다.
g
ravel
¶마지막으로 ravel
함수는 동일한 데이터를 가리키는 새로운 1차원 ndarray
를 반환합니다:
g.ravel()
일반적인 산술 연산자(+
, -
, *
, /
, //
, **
등)는 모두 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)
여기 곱셈은 행렬 곱셈이 아닙니다. 행렬 연산은 아래에서 설명합니다.
배열의 크기는 같아야 합니다. 그렇지 않으면 넘파이가 브로드캐스팅 규칙을 적용합니다.
일반적으로 넘파이는 동일한 크기의 배열을 기대합니다. 그렇지 않은 상황에는 브로드캐시틍 규칙을 적용합니다:
배열의 랭크가 동일하지 않으면 랭크가 맞을 때까지 랭크가 작은 배열 앞에 1을 추가합니다.
h = np.arange(5).reshape(1, 1, 5)
h
여기에 (1,1,5)
크기의 3D 배열에 (5,)
크기의 1D 배열을 더해 보죠. 브로드캐스팅의 규칙 1이 적용됩니다!
h + [10, 20, 30, 40, 50] # 다음과 동일합니다: h + [[[10, 20, 30, 40, 50]]]
특정 차원이 1인 배열은 그 차원에서 크기가 가장 큰 배열의 크기에 맞춰 동작합니다. 배열의 원소가 차원을 따라 반복됩니다.
k = np.arange(6).reshape(2, 3)
k
(2,3)
크기의 2D ndarray
에 (2,1)
크기의 2D 배열을 더해 보죠. 넘파이는 브로드캐스팅 규칙 2를 적용합니다:
k + [[100], [200]] # 다음과 같습니다: k + [[100, 100, 100], [200, 200, 200]]
규칙 1과 2를 합치면 다음과 같이 동작합니다:
k + [100, 200, 300] # 규칙 1 적용: [[100, 200, 300]], 규칙 2 적용: [[100, 200, 300], [100, 200, 300]]
또 매우 간단히 다음 처럼 해도 됩니다:
k + 1000 # 다음과 같습니다: k + [[1000, 1000, 1000], [1000, 1000, 1000]]
규칙 1 & 2을 적용했을 때 모든 배열의 크기가 맞아야 합니다.
try:
k + [33, 44]
except ValueError as e:
print(e)
브로드캐스팅 규칙은 산술 연산 뿐만 아니라 넘파이 연산에서 많이 사용됩니다. 아래에서 더 보도록 하죠. 브로드캐스팅에 관한 더 자세한 정보는 온라인 문서를 참고하세요.
dtype
이 다른 배열을 합칠 때 넘파이는 (실제 값에 상관없이) 모든 값을 다룰 수 있는 타입으로 업캐스팅합니다.
k1 = np.arange(0, 5, dtype=np.uint8)
print(k1.dtype, k1)
k2 = k1 + np.array([5, 6, 7, 8, 9], dtype=np.int8)
print(k2.dtype, k2)
모든 int8
과 uint8
값(-128에서 255까지)을 표현하기 위해 int16
이 필요합니다. 이 코드에서는 uint8
이면 충분하지만 업캐스팅되었습니다.
k3 = k1 + 1.5
print(k3.dtype, k3)
조건 연산자도 원소별로 적용됩니다:
m = np.array([20, -5, 30, 40])
m < [15, 16, 35, 36]
브로드캐스팅을 사용합니다:
m < 25 # m < [25, 25, 25, 25] 와 동일
불리언 인덱싱과 함께 사용하면 아주 유용합니다(아래에서 설명하겠습니다).
m[m < 25]
a = np.array([[-2.5, 3.1, 7], [10, 11, 12]])
print(a)
print("평균 =", a.mean())
이 명령은 크기에 상관없이 ndarray
에 있는 모든 원소의 평균을 계산합니다.
다음은 유용한 ndarray
메서드입니다:
for func in (a.min, a.max, a.sum, a.prod, a.std, a.var):
print(func.__name__, "=", func())
이 함수들은 선택적으로 매개변수 axis
를 사용합니다. 지정된 축을 따라 원소에 연산을 적용하는데 사용합니다. 예를 들면:
c=np.arange(24).reshape(2,3,4)
c
c.sum(axis=0) # 첫 번째 축을 따라 더함, 결과는 3x4 배열
c.sum(axis=1) # 두 번째 축을 따라 더함, 결과는 2x4 배열
여러 축에 대해서 더할 수도 있습니다:
c.sum(axis=(0,2)) # 첫 번째 축과 세 번째 축을 따라 더함, 결과는 (3,) 배열
0+1+2+3 + 12+13+14+15, 4+5+6+7 + 16+17+18+19, 8+9+10+11 + 20+21+22+23
넘파이는 일반 함수(universal function) 또는 ufunc라고 부르는 원소별 함수를 제공합니다. 예를 들면 square
함수는 원본 ndarray
를 복사하여 각 원소를 제곱한 새로운 ndarray
객체를 반환합니다:
a = np.array([[-2.5, 3.1, 7], [10, 11, 12]])
np.square(a)
다음은 유용한 단항 일반 함수들입니다:
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
에 원소별로 적용되는 이항 함수도 많습니다. 두 배열이 동일한 크기가 아니면 브로드캐스팅 규칙이 적용됩니다:
a = np.array([1, -2, 3, 4])
b = np.array([2, 8, -1, 7])
np.add(a, b) # a + b 와 동일
np.greater(a, b) # a > b 와 동일
np.maximum(a, b)
np.copysign(a, b)
a = np.array([1, 5, 3, 19, 13, 7, 3])
a[3]
a[2:5]
a[2:-1]
a[:2]
a[2::2]
a[::-1]
물론 원소를 수정할 수 있죠:
a[3]=999
a
슬라이싱을 사용해 ndarray
를 수정할 수 있습니다:
a[2:5] = [997, 998, 999]
a
보통의 파이썬 배열과 대조적으로 ndarray
슬라이싱에 하나의 값을 할당하면 슬라이싱 전체에 복사됩니다. 위에서 언급한 브로드캐스팅 덕택입니다.
a[2:5] = -1
a
또한 이런 식으로 ndarray
크기를 늘리거나 줄일 수 없습니다:
try:
a[2:5] = [1,2,3,4,5,6] # 너무 길어요
except ValueError as e:
print(e)
원소를 삭제할 수도 없습니다:
try:
del a[2:5]
except ValueError as e:
print(e)
중요한 점은 ndarray
의 슬라이싱은 같은 데이터 버퍼를 바라보는 뷰(view)입니다. 슬라이싱된 객체를 수정하면 실제 원본 ndarray
가 수정됩니다!
a_slice = a[2:6]
a_slice[1] = 1000
a # 원본 배열이 수정됩니다!
a[3] = 2000
a_slice # 비슷하게 원본 배열을 수정하면 슬라이싱 객체에도 반영됩니다!
데이터를 복사하려면 copy
메서드를 사용해야 합니다:
another_slice = a[2:6].copy()
another_slice[1] = 3000
a # 원본 배열이 수정되지 않습니다
a[3] = 4000
another_slice # 마찬가지로 원본 배열을 수정해도 복사된 배열은 바뀌지 않습니다
다차원 배열은 비슷한 방식으로 각 축을 따라 인덱싱 또는 슬라이싱해서 사용합니다. 콤마로 구분합니다:
b = np.arange(48).reshape(4, 12)
b
b[1, 2] # 행 1, 열 2
b[1, :] # 행 1, 모든 열
b[:, 1] # 모든 행, 열 1
주의: 다음 두 표현에는 미묘한 차이가 있습니다:
b[1, :]
b[1:2, :]
첫 번째 표현식은 (12,)
크기인 1D 배열로 행이 하나입니다. 두 번째는 (1, 12)
크기인 2D 배열로 같은 행을 반환합니다.
관심 대상의 인덱스 리스트를 지정할 수도 있습니다. 이를 팬시 인덱싱이라고 부릅니다.
b[(0,2), 2:5] # 행 0과 2, 열 2에서 4(5-1)까지
b[:, (-1, 2, -1)] # 모든 행, 열 -1 (마지막), 2와 -1 (다시 반대 방향으로)
여러 개의 인덱스 리스트를 지정하면 인덱스에 맞는 값이 포함된 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)
고차원에서도 동일한 방식이 적용됩니다. 몇 가지 예를 살펴 보겠습니다:
c = b.reshape(4,2,6)
c
c[2, 1, 4] # 행렬 2, 행 1, 열 4
c[2, :, 3] # 행렬 2, 모든 행, 열 3
어떤 축에 대한 인덱스를 지정하지 않으면 이 축의 모든 원소가 반환됩니다:
c[2, 1] # 행렬 2, 행 1, 모든 열이 반환됩니다. c[2, 1, :]와 동일합니다.
...
)¶생략 부호(...
)를 쓰면 모든 지정하지 않은 축의 원소를 포함합니다.
c[2, ...] # 행렬 2, 모든 행, 모든 열. c[2, :, :]와 동일
c[2, 1, ...] # 행렬 2, 행 1, 모든 열. c[2, 1, :]와 동일
c[2, ..., 3] # 행렬 2, 모든 행, 열 3. c[2, :, 3]와 동일
c[..., 3] # 모든 행렬, 모든 행, 열 3. c[:, :, 3]와 동일
불리언 값을 가진 ndarray
를 사용해 축의 인덱스를 지정할 수 있습니다.
b = np.arange(48).reshape(4, 12)
b
rows_on = np.array([True, False, True, False])
b[rows_on, :] # 행 0과 2, 모든 열. b[(0, 2), :]와 동일
cols_on = np.array([False, True, False] * 4)
b[:, cols_on] # 모든 행, 열 1, 4, 7, 10
np.ix_
¶여러 축에 걸쳐서는 불리언 인덱싱을 사용할 수 없고 ix_
함수를 사용합니다:
b[np.ix_(rows_on, cols_on)]
np.ix_(rows_on, cols_on)
ndarray
와 같은 크기의 불리언 배열을 사용하면 해당 위치가 True
인 모든 원소를 담은 1D 배열이 반환됩니다. 일반적으로 조건 연산자와 함께 사용합니다:
b[b % 3 == 1]
ndarray
를 반복하는 것은 일반적인 파이썬 배열을 반복한는 것과 매우 유사합니다. 다차원 배열을 반복하면 첫 번째 축에 대해서 수행됩니다.
c = np.arange(24).reshape(2, 3, 4) # 3D 배열 (두 개의 3x4 행렬로 구성됨)
c
for m in c:
print("아이템:")
print(m)
for i in range(len(c)): # len(c) == c.shape[0]
print("아이템:")
print(c[i])
ndarray
에 있는 모든 원소를 반복하려면 flat
속성을 사용합니다:
for i in c.flat:
print("아이템:", i)
종종 다른 배열을 쌓아야 할 때가 있습니다. 넘파이는 이를 위해 몇 개의 함수를 제공합니다. 먼저 배열 몇 개를 만들어 보죠.
q1 = np.full((3,4), 1.0)
q1
q2 = np.full((4,4), 2.0)
q2
q3 = np.full((3,4), 3.0)
q3
vstack
¶vstack
함수를 사용하여 수직으로 쌓아보죠:
q4 = np.vstack((q1, q2, q3))
q4
q4.shape
q5 = np.hstack((q1, q3))
q5
q5.shape
q1과 q3가 모두 3개의 행을 가지고 있기 때문에 가능합니다. q2는 4개의 행을 가지고 있기 때문에 q1, q3와 수평으로 쌓을 수 없습니다:
try:
q5 = np.hstack((q1, q2, q3))
except ValueError as e:
print(e)
concatenate
¶concatenate
함수는 지정한 축으로도 배열을 쌓습니다.
q7 = np.concatenate((q1, q2, q3), axis=0) # vstack과 동일
q7
q7.shape
예상했겠지만 hstack
은 axis=1
으로 concatenate
를 호출하는 것과 같습니다.
stack
¶stack
함수는 새로운 축을 따라 배열을 쌓습니다. 모든 배열은 같은 크기를 가져야 합니다.
q8 = np.stack((q1, q3))
q8
q8.shape
r = np.arange(24).reshape(6,4)
r
수직으로 동일한 크기로 나누어 보겠습니다:
r1, r2, r3 = np.vsplit(r, 3)
r1
r2
r3
split
함수는 주어진 축을 따라 배열을 분할합니다. vsplit
는 axis=0
으로 split
를 호출하는 것과 같습니다. hsplit
함수는 axis=1
로 split
를 호출하는 것과 같습니다:
r4, r5 = np.hsplit(r, 2)
r4
r5
t = np.arange(24).reshape(4,2,3)
t
0, 1, 2
(깊이, 높이, 너비) 축을 1, 2, 0
(깊이→너비, 높이→깊이, 너비→높이) 순서로 바꾼 ndarray
를 만들어 보겠습니다:
t1 = t.transpose((1,2,0))
t1
t1.shape
transpose
기본값은 차원의 순서를 역전시킵니다:
t2 = t.transpose() # t.transpose((2, 1, 0))와 동일
t2
t2.shape
넘파이는 두 축을 바꾸는 swapaxes
함수를 제공합니다. 예를 들어 깊이와 높이를 뒤바꾸어 t
의 새로운 뷰를 만들어 보죠:
t3 = t.swapaxes(0,1) # t.transpose((1, 0, 2))와 동일
t3
t3.shape
넘파이 2D 배열을 사용하면 파이썬에서 행렬을 효율적으로 표현할 수 있습니다. 주요 행렬 연산을 간단히 둘러 보겠습니다. 선형 대수학, 벡터와 행렬에 관한 자세한 내용은 Linear Algebra tutorial를 참고하세요.
T
속성은 랭크가 2보다 크거나 같을 때 transpose()
를 호출하는 것과 같습니다:
m1 = np.arange(10).reshape(2,5)
m1
m1.T
T
속성은 랭크가 0이거나 1인 배열에는 아무런 영향을 미치지 않습니다:
m2 = np.arange(5)
m2
m2.T
먼저 1D 배열을 하나의 행이 있는 행렬(2D)로 바꾼다음 전치를 수행할 수 있습니다:
m2r = m2.reshape(1,5)
m2r
m2r.T
n1 = np.arange(10).reshape(2, 5)
n1
n2 = np.arange(15).reshape(5,3)
n2
n1.dot(n2)
주의: 앞서 언급한 것처럼 n1*n2
는 행렬 곱셈이 아니라 원소별 곱셈(또는 아다마르 곱이라 부릅니다)입니다.
numpy.linalg
모듈 안에 많은 선형 대수 함수들이 있습니다. 특히 inv
함수는 정방 행렬의 역행렬을 계산합니다:
import numpy.linalg as linalg
m3 = np.array([[1,2,3],[5,7,11],[21,29,31]])
m3
linalg.inv(m3)
pinv
함수를 사용하여 유사 역행렬을 계산할 수도 있습니다:
linalg.pinv(m3)
행렬과 그 행렬의 역행렬을 곱하면 단위 행렬이 됩니다(작은 소숫점 오차가 있습니다):
m3.dot(linalg.inv(m3))
eye
함수는 NxN 크기의 단위 행렬을 만듭니다:
np.eye(3)
q, r = linalg.qr(m3)
q
r
q.dot(r) # q.r는 m3와 같습니다
linalg.det(m3) # 행렬식 계산
eigenvalues, eigenvectors = linalg.eig(m3)
eigenvalues # λ
eigenvectors # v
m3.dot(eigenvectors) - eigenvalues * eigenvectors # m3.v - λ*v = 0
m4 = np.array([[1,0,0,0,2], [0,0,3,0,0], [0,0,0,0,0], [0,2,0,0,0]])
m4
U, S_diag, V = linalg.svd(m4)
U
S_diag
svd
함수는 Σ의 대각 원소 값만 반환합니다. 전체 Σ 행렬은 다음과 같이 만듭니다:
S = np.zeros((4, 5))
S[np.diag_indices(4)] = S_diag
S # Σ
V
U.dot(S).dot(V) # U.Σ.V == m4
np.diag(m3) # m3의 대각 원소입니다(왼쪽 위에서 오른쪽 아래)
np.trace(m3) # np.diag(m3).sum()와 같습니다
solve
함수는 다음과 같은 선형 방정식을 풉니다:
coeffs = np.array([[2, 6], [5, 3]])
depvars = np.array([6, -9])
solution = linalg.solve(coeffs, depvars)
solution
solution을 확인해 보죠:
coeffs.dot(solution), depvars # 네 같네요
좋습니다! 다른 방식으로도 solution을 확인해 보죠:
np.allclose(coeffs.dot(solution), depvars)
한 번에 하나씩 개별 배열 원소에 대해 연산을 실행하는 대신 배열 연산을 사용하면 훨씬 효율적인 코드를 만들 수 있습니다. 이를 벡터화라고 합니다. 이를 사용하여 넘파이의 최적화된 성능을 활용할 수 있습니다.
예를 들어, $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
Y
여기서 볼 수 있듯이 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
np.save("my_array", a)
끝입니다! 파일 이름의 확장자를 지정하지 않았기 때문에 넘파이는 자동으로 .npy
를 붙입니다. 파일 내용을 확인해 보겠습니다:
with open("my_array.npy", "rb") as f:
content = f.read()
content
이 파일을 넘파이 배열로 로드하려면 load
함수를 사용합니다:
a_loaded = np.load("my_array.npy")
a_loaded
배열을 텍스트 포맷으로 저장해 보죠:
np.savetxt("my_array.csv", a)
파일 내용을 확인해 보겠습니다:
with open("my_array.csv", "rt") as f:
print(f.read())
이 파일은 탭으로 구분된 CSV 파일입니다. 다른 구분자를 지정할 수도 있습니다:
np.savetxt("my_array.csv", a, delimiter=",")
이 파일을 로드하려면 loadtxt
함수를 사용합니다:
a_loaded = np.loadtxt("my_array.csv", delimiter=",")
a_loaded
.npz
포맷¶여러 개의 배열을 압축된 한 파일로 저장하는 것도 가능합니다:
b = np.arange(24, dtype=np.uint8).reshape(2, 3, 4)
b
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] + "[...]"
다음과 같이 이 파일을 로드할 수 있습니다:
my_arrays = np.load("my_arrays.npz")
my_arrays
게으른 로딩을 수행하는 딕셔너리와 유사한 객체입니다:
my_arrays.keys()
my_arrays["my_a"]