#!/usr/bin/env python # coding: utf-8 # **도구 - 넘파이(NumPy)** # # *넘파이(NumPy)는 파이썬의 과학 컴퓨팅을 위한 기본 라이브러리입니다. 넘파이의 핵심은 강력한 N-차원 배열 객체입니다. 또한 선형 대수, 푸리에(Fourier) 변환, 유사 난수 생성과 같은 유용한 함수들도 제공합니다." # # # 배열 생성 # # 먼저 파이썬 2와 3에 모두 호환되도록 필요한 모듈을 임포트합니다: # In[1]: from __future__ import division, print_function, unicode_literals # `numpy`를 임포트해 보죠. 대부분의 사람들이 `np`로 알리아싱하여 임포트합니다: # In[2]: import numpy as np # ## `np.zeros` # `zeros` 함수는 0으로 채워진 배열을 만듭니다: # In[3]: np.zeros(5) # 2D 배열(즉, 행렬)을 만들려면 원하는 행과 열의 크기를 튜플로 전달합니다. 예를 들어 다음은 $3 \times 4$ 크기의 행렬입니다: # In[4]: np.zeros((3,4)) # ## 용어 # # * 넘파이에서 각 차원을 **축**(axis) 이라고 합니다 # * 축의 개수를 **랭크**(rank) 라고 합니다. # * 예를 들어, 위의 $3 \times 4$ 행렬은 랭크 2인 배열입니다(즉 2차원입니다). # * 첫 번째 축의 길이는 3이고 두 번째 축의 길이는 4입니다. # * 배열의 축 길이를 배열의 **크기**(shape)라고 합니다. # * 예를 들어, 위 행렬의 크기는 `(3, 4)`입니다. # * 랭크는 크기의 길이와 같습니다. # * 배열의 **사이즈**(size)는 전체 원소의 개수입니다. 축의 길이를 모두 곱해서 구할 수 있습니다(가령, $3 \times 4=12$). # In[5]: a = np.zeros((3,4)) a # In[6]: a.shape # In[7]: a.ndim # len(a.shape)와 같습니다 # In[8]: a.size # ## N-차원 배열 # 임의의 랭크 수를 가진 N-차원 배열을 만들 수 있습니다. 예를 들어, 다음은 크기가 `(2,3,4)`인 3D 배열(랭크=3)입니다: # In[9]: np.zeros((2,3,4)) # ## 배열 타입 # 넘파이 배열의 타입은 `ndarray`입니다: # In[10]: type(np.zeros((3,4))) # ## `np.ones` # `ndarray`를 만들 수 있는 넘파이 함수가 많습니다. # # 다음은 1로 채워진 $3 \times 4$ 크기의 행렬입니다: # In[11]: np.ones((3,4)) # ## `np.full` # 주어진 값으로 지정된 크기의 배열을 초기화합니다. 다음은 `π`로 채워진 $3 \times 4$ 크기의 행렬입니다. # In[12]: np.full((3,4), np.pi) # ## `np.empty` # 초기화되지 않은 $2 \times 3$ 크기의 배열을 만듭니다(배열의 내용은 예측이 불가능하며 메모리 상황에 따라 달라집니다): # In[13]: np.empty((2,3)) # ## np.array # `array` 함수는 파이썬 리스트를 사용하여 `ndarray`를 초기화합니다: # In[14]: np.array([[1,2,3,4], [10, 20, 30, 40]]) # ## `np.arange` # 파이썬의 기본 `range` 함수와 비슷한 넘파이 `range` 함수를 사용하여 `ndarray`를 만들 수 있습니다: # In[15]: np.arange(1, 5) # 부동 소수도 가능합니다: # In[16]: np.arange(1.0, 5.0) # 파이썬의 기본 `range` 함수처럼 건너 뛰는 정도를 지정할 수 있습니다: # In[17]: np.arange(1, 5, 0.5) # 부동 소수를 사용하면 원소의 개수가 일정하지 않을 수 있습니다. 예를 들면 다음과 같습니다: # In[18]: 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`와는 다르게 최댓값이 **포함**됩니다): # In[19]: print(np.linspace(0, 5/3, 6)) # ## `np.rand`와 `np.randn` # 넘파이의 `random` 모듈에는 `ndarray`를 랜덤한 값으로 초기화할 수 있는 함수들이 많이 있습니다. # 예를 들어, 다음은 (균등 분포인) 0과 1사이의 랜덤한 부동 소수로 $3 \times 4$ 행렬을 초기화합니다: # In[20]: np.random.rand(3,4) # 다음은 평균이 0이고 분산이 1인 일변량 [정규 분포](https://ko.wikipedia.org/wiki/%EC%A0%95%EA%B7%9C_%EB%B6%84%ED%8F%AC)(가우시안 분포)에서 샘플링한 랜덤한 부동 소수를 담은 $3 \times 4$ 행렬입니다: # In[21]: np.random.randn(3,4) # 이 분포의 모양을 알려면 맷플롯립을 사용해 그려보는 것이 좋습니다(더 자세한 것은 [맷플롯립 튜토리얼](tools_matplotlib.ipynb)을 참고하세요): # In[22]: get_ipython().run_line_magic('matplotlib', 'inline') import matplotlib.pyplot as plt # In[23]: plt.hist(np.random.rand(100000), normed=True, bins=100, histtype="step", color="blue", label="rand") plt.hist(np.random.randn(100000), normed=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() # ## np.fromfunction # 함수를 사용하여 `ndarray`를 초기화할 수도 있습니다: # In[24]: 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`이 원소마다 호출되는 것이 아니고 딱 **한 번** 호출된다는 점입니다. 그래서 매우 효율적으로 초기화할 수 있습니다. # # 배열 데이터 # ## `dtype` # 넘파이의 `ndarray`는 모든 원소가 동일한 타입(보통 숫자)을 가지기 때문에 효율적입니다. `dtype` 속성으로 쉽게 데이터 타입을 확인할 수 있습니다: # In[25]: c = np.arange(1, 5) print(c.dtype, c) # In[26]: c = np.arange(1.0, 5.0) print(c.dtype, c) # 넘파이가 데이터 타입을 결정하도록 내버려 두는 대신 `dtype` 매개변수를 사용해서 배열을 만들 때 명시적으로 지정할 수 있습니다: # In[27]: d = np.arange(1, 5, dtype=np.complex64) print(d.dtype, d) # 가능한 데이터 타입은 `int8`, `int16`, `int32`, `int64`, `uint8`|`16`|`32`|`64`, `float16`|`32`|`64`, `complex64`|`128`가 있습니다. 전체 리스트는 [온라인 문서](http://docs.scipy.org/doc/numpy/user/basics.types.html)를 참고하세요. # # ## `itemsize` # `itemsize` 속성은 각 아이템의 크기(바이트)를 반환합니다: # In[28]: e = np.arange(1, 5, dtype=np.complex64) e.itemsize # ## `data` 버퍼 # 배열의 데이터는 1차원 바이트 버퍼로 메모리에 저장됩니다. `data` 속성을 사용해 참조할 수 있습니다(사용할 일은 거의 없겠지만요). # In[29]: f = np.array([[1,2],[1000, 2000]], dtype=np.int32) f.data # 파이썬 2에서는 `f.data`가 버퍼이고 파이썬 3에서는 memoryview입니다. # In[30]: if (hasattr(f.data, "tobytes")): data_bytes = f.data.tobytes() # python 3 else: data_bytes = memoryview(f.data).tobytes() # python 2 data_bytes # 여러 개의 `ndarray`가 데이터 버퍼를 공유할 수 있습니다. 하나를 수정하면 다른 것도 바뀝니다. 잠시 후에 예를 살펴 보겠습니다. # # 배열 크기 변경 # # ## 자신을 변경 # # `ndarray`의 `shape` 속성을 지정하면 간단히 크기를 바꿀 수 있습니다. 배열의 원소 개수는 동일하게 유지됩니다. # In[31]: g = np.arange(24) print(g) print("랭크:", g.ndim) # In[32]: g.shape = (6, 4) print(g) print("랭크:", g.ndim) # In[33]: g.shape = (2, 3, 4) print(g) print("랭크:", g.ndim) # ## `reshape` # # `reshape` 함수는 동일한 데이터를 가리키는 새로운 `ndarray` 객체를 반환합니다. 한 배열을 수정하면 다른 것도 함께 바뀝니다. # In[34]: g2 = g.reshape(4,6) print(g2) print("랭크:", g2.ndim) # 행 1, 열 2의 원소를 999로 설정합니다(인덱싱 방식은 아래를 참고하세요). # In[35]: g2[1, 2] = 999 g2 # 이에 상응하는 `g`의 원소도 수정됩니다. # In[36]: g # ## `ravel` # # 마지막으로 `ravel` 함수는 동일한 데이터를 가리키는 새로운 1차원 `ndarray`를 반환합니다: # In[37]: g.ravel() # # 산술 연산 # # 일반적인 산술 연산자(`+`, `-`, `*`, `/`, `//`, `**` 등)는 모두 `ndarray`와 사용할 수 있습니다. 이 연산자는 원소별로 적용됩니다: # In[38]: 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 # # 배열의 랭크가 동일하지 않으면 랭크가 맞을 때까지 랭크가 작은 배열 앞에 1을 추가합니다. # In[39]: h = np.arange(5).reshape(1, 1, 5) h # 여기에 `(1,1,5)` 크기의 3D 배열에 `(5,)` 크기의 1D 배열을 더해 보죠. 브로드캐스팅의 규칙 1이 적용됩니다! # In[40]: h + [10, 20, 30, 40, 50] # 다음과 동일합니다: h + [[[10, 20, 30, 40, 50]]] # ## 규칙 2 # # 특정 차원이 1인 배열은 그 차원에서 크기가 가장 큰 배열의 크기에 맞춰 동작합니다. 배열의 원소가 차원을 따라 반복됩니다. # In[41]: k = np.arange(6).reshape(2, 3) k # `(2,3)` 크기의 2D `ndarray`에 `(2,1)` 크기의 2D 배열을 더해 보죠. 넘파이는 브로드캐스팅 규칙 2를 적용합니다: # In[42]: k + [[100], [200]] # 다음과 같습니다: k + [[100, 100, 100], [200, 200, 200]] # 규칙 1과 2를 합치면 다음과 같이 동작합니다: # In[43]: k + [100, 200, 300] # 규칙 1 적용: [[100, 200, 300]], 규칙 2 적용: [[100, 200, 300], [100, 200, 300]] # 또 매우 간단히 다음 처럼 해도 됩니다: # In[44]: k + 1000 # 다음과 같습니다: k + [[1000, 1000, 1000], [1000, 1000, 1000]] # ## 규칙 3 # # 규칙 1 & 2을 적용했을 때 모든 배열의 크기가 맞아야 합니다. # In[45]: try: k + [33, 44] except ValueError as e: print(e) # 브로드캐스팅 규칙은 산술 연산 뿐만 아니라 넘파이 연산에서 많이 사용됩니다. 아래에서 더 보도록 하죠. 브로드캐스팅에 관한 더 자세한 정보는 [온라인 문서](https://docs.scipy.org/doc/numpy-dev/user/basics.broadcasting.html)를 참고하세요. # ## 업캐스팅 # # `dtype`이 다른 배열을 합칠 때 넘파이는 (실제 값에 상관없이) 모든 값을 다룰 수 있는 타입으로 업캐스팅합니다. # In[46]: k1 = np.arange(0, 5, dtype=np.uint8) print(k1.dtype, k1) # In[47]: k2 = k1 + np.array([5, 6, 7, 8, 9], dtype=np.int8) print(k2.dtype, k2) # 모든 `int8`과 `uint8` 값(-128에서 255까지)을 표현하기 위해 `int16`이 필요합니다. 이 코드에서는 `uint8`이면 충분하지만 업캐스팅되었습니다. # In[48]: k3 = k1 + 1.5 print(k3.dtype, k3) # # 조건 연산자 # 조건 연산자도 원소별로 적용됩니다: # In[49]: m = np.array([20, -5, 30, 40]) m < [15, 16, 35, 36] # 브로드캐스팅을 사용합니다: # In[50]: m < 25 # m < [25, 25, 25, 25] 와 동일 # 불리언 인덱싱과 함께 사용하면 아주 유용합니다(아래에서 설명하겠습니다). # In[51]: m[m < 25] # # 수학 함수와 통계 함수 # `ndarray`에서 사용할 수 있는 수학 함수와 통계 함수가 많습니다. # # ## `ndarray` 메서드 # # 일부 함수는 `ndarray` 메서드로 제공됩니다. 예를 들면: # In[52]: a = np.array([[-2.5, 3.1, 7], [10, 11, 12]]) print(a) print("평균 =", a.mean()) # 이 명령은 크기에 상관없이 `ndarray`에 있는 모든 원소의 평균을 계산합니다. # # 다음은 유용한 `ndarray` 메서드입니다: # In[53]: for func in (a.min, a.max, a.sum, a.prod, a.std, a.var): print(func.__name__, "=", func()) # 이 함수들은 선택적으로 매개변수 `axis`를 사용합니다. 지정된 축을 따라 원소에 연산을 적용하는데 사용합니다. 예를 들면: # In[54]: c=np.arange(24).reshape(2,3,4) c # In[55]: c.sum(axis=0) # 첫 번째 축을 따라 더함, 결과는 3x4 배열 # In[56]: c.sum(axis=1) # 두 번째 축을 따라 더함, 결과는 2x4 배열 # 여러 축에 대해서 더할 수도 있습니다: # In[57]: c.sum(axis=(0,2)) # 첫 번째 축과 세 번째 축을 따라 더함, 결과는 (3,) 배열 # In[58]: 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` 객체를 반환합니다: # In[59]: a = np.array([[-2.5, 3.1, 7], [10, 11, 12]]) np.square(a) # 다음은 유용한 단항 일반 함수들입니다: # In[60]: 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`에 원소별로 적용되는 이항 함수도 많습니다. 두 배열이 동일한 크기가 아니면 브로드캐스팅 규칙이 적용됩니다: # In[61]: a = np.array([1, -2, 3, 4]) b = np.array([2, 8, -1, 7]) np.add(a, b) # a + b 와 동일 # In[62]: np.greater(a, b) # a > b 와 동일 # In[63]: np.maximum(a, b) # In[64]: np.copysign(a, b) # # 배열 인덱싱 # # ## 1차원 배열 # # 1차원 넘파이 배열은 보통의 파이썬 배열과 비슷하게 사용할 수 있습니다: # In[65]: a = np.array([1, 5, 3, 19, 13, 7, 3]) a[3] # In[66]: a[2:5] # In[67]: a[2:-1] # In[68]: a[:2] # In[69]: a[2::2] # In[70]: a[::-1] # 물론 원소를 수정할 수 있죠: # In[71]: a[3]=999 a # 슬라이싱을 사용해 `ndarray`를 수정할 수 있습니다: # In[72]: a[2:5] = [997, 998, 999] a # ## 보통의 파이썬 배열과 차이점 # # 보통의 파이썬 배열과 대조적으로 `ndarray` 슬라이싱에 하나의 값을 할당하면 슬라이싱 전체에 복사됩니다. 위에서 언급한 브로드캐스팅 덕택입니다. # In[73]: a[2:5] = -1 a # 또한 이런 식으로 `ndarray` 크기를 늘리거나 줄일 수 없습니다: # In[74]: try: a[2:5] = [1,2,3,4,5,6] # 너무 길어요 except ValueError as e: print(e) # 원소를 삭제할 수도 없습니다: # In[75]: try: del a[2:5] except ValueError as e: print(e) # 중요한 점은 `ndarray`의 슬라이싱은 같은 데이터 버퍼를 바라보는 뷰(view)입니다. 슬라이싱된 객체를 수정하면 실제 원본 `ndarray`가 수정됩니다! # In[76]: a_slice = a[2:6] a_slice[1] = 1000 a # 원본 배열이 수정됩니다! # In[77]: a[3] = 2000 a_slice # 비슷하게 원본 배열을 수정하면 슬라이싱 객체에도 반영됩니다! # 데이터를 복사하려면 `copy` 메서드를 사용해야 합니다: # In[78]: another_slice = a[2:6].copy() another_slice[1] = 3000 a # 원본 배열이 수정되지 않습니다 # In[79]: a[3] = 4000 another_slice # 마찬가지로 원본 배열을 수정해도 복사된 배열은 바뀌지 않습니다 # ## 다차원 배열 # # 다차원 배열은 비슷한 방식으로 각 축을 따라 인덱싱 또는 슬라이싱해서 사용합니다. 콤마로 구분합니다: # In[80]: b = np.arange(48).reshape(4, 12) b # In[81]: b[1, 2] # 행 1, 열 2 # In[82]: b[1, :] # 행 1, 모든 열 # In[83]: b[:, 1] # 모든 행, 열 1 # **주의**: 다음 두 표현에는 미묘한 차이가 있습니다: # In[84]: b[1, :] # In[85]: b[1:2, :] # 첫 번째 표현식은 `(12,)` 크기인 1D 배열로 행이 하나입니다. 두 번째는 `(1, 12)` 크기인 2D 배열로 같은 행을 반환합니다. # ## 팬시 인덱싱(Fancy indexing) # # 관심 대상의 인덱스 리스트를 지정할 수도 있습니다. 이를 팬시 인덱싱이라고 부릅니다. # In[86]: b[(0,2), 2:5] # 행 0과 2, 열 2에서 4(5-1)까지 # In[87]: b[:, (-1, 2, -1)] # 모든 행, 열 -1 (마지막), 2와 -1 (다시 반대 방향으로) # 여러 개의 인덱스 리스트를 지정하면 인덱스에 맞는 값이 포함된 1D `ndarray`를 반환됩니다. # In[88]: 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) # ## 고차원 # # 고차원에서도 동일한 방식이 적용됩니다. 몇 가지 예를 살펴 보겠습니다: # In[89]: c = b.reshape(4,2,6) c # In[90]: c[2, 1, 4] # 행렬 2, 행 1, 열 4 # In[91]: c[2, :, 3] # 행렬 2, 모든 행, 열 3 # 어떤 축에 대한 인덱스를 지정하지 않으면 이 축의 모든 원소가 반환됩니다: # In[92]: c[2, 1] # 행렬 2, 행 1, 모든 열이 반환됩니다. c[2, 1, :]와 동일합니다. # ## 생략 부호 (`...`) # # 생략 부호(`...`)를 쓰면 모든 지정하지 않은 축의 원소를 포함합니다. # In[93]: c[2, ...] # 행렬 2, 모든 행, 모든 열. c[2, :, :]와 동일 # In[94]: c[2, 1, ...] # 행렬 2, 행 1, 모든 열. c[2, 1, :]와 동일 # In[95]: c[2, ..., 3] # 행렬 2, 모든 행, 열 3. c[2, :, 3]와 동일 # In[96]: c[..., 3] # 모든 행렬, 모든 행, 열 3. c[:, :, 3]와 동일 # ## 불리언 인덱싱 # # 불리언 값을 가진 `ndarray`를 사용해 축의 인덱스를 지정할 수 있습니다. # In[97]: b = np.arange(48).reshape(4, 12) b # In[98]: rows_on = np.array([True, False, True, False]) b[rows_on, :] # 행 0과 2, 모든 열. b[(0, 2), :]와 동일 # In[99]: cols_on = np.array([False, True, False] * 4) b[:, cols_on] # 모든 행, 열 1, 4, 7, 10 # ## `np.ix_` # # 여러 축에 걸쳐서는 불리언 인덱싱을 사용할 수 없고 `ix_` 함수를 사용합니다: # In[100]: b[np.ix_(rows_on, cols_on)] # In[101]: np.ix_(rows_on, cols_on) # `ndarray`와 같은 크기의 불리언 배열을 사용하면 해당 위치가 `True`인 모든 원소를 담은 1D 배열이 반환됩니다. 일반적으로 조건 연산자와 함께 사용합니다: # In[102]: b[b % 3 == 1] # # 반복 # # `ndarray`를 반복하는 것은 일반적인 파이썬 배열을 반복한는 것과 매우 유사합니다. 다차원 배열을 반복하면 첫 번째 축에 대해서 수행됩니다. # In[103]: c = np.arange(24).reshape(2, 3, 4) # 3D 배열 (두 개의 3x4 행렬로 구성됨) c # In[104]: for m in c: print("아이템:") print(m) # In[105]: for i in range(len(c)): # len(c) == c.shape[0] print("아이템:") print(c[i]) # `ndarray`에 있는 모든 원소를 반복하려면 `flat` 속성을 사용합니다: # In[106]: for i in c.flat: print("아이템:", i) # # 배열 쌓기 # # 종종 다른 배열을 쌓아야 할 때가 있습니다. 넘파이는 이를 위해 몇 개의 함수를 제공합니다. 먼저 배열 몇 개를 만들어 보죠. # In[107]: q1 = np.full((3,4), 1.0) q1 # In[108]: q2 = np.full((4,4), 2.0) q2 # In[109]: q3 = np.full((3,4), 3.0) q3 # ## `vstack` # # `vstack` 함수를 사용하여 수직으로 쌓아보죠: # In[110]: q4 = np.vstack((q1, q2, q3)) q4 # In[111]: q4.shape # q1, q2, q3가 모두 같은 크기이므로 가능합니다(수직으로 쌓기 때문에 수직 축은 크기가 달라도 됩니다). # # ## `hstack` # # `hstack`을 사용해 수평으로도 쌓을 수 있습니다: # In[112]: q5 = np.hstack((q1, q3)) q5 # In[113]: q5.shape # q1과 q3가 모두 3개의 행을 가지고 있기 때문에 가능합니다. q2는 4개의 행을 가지고 있기 때문에 q1, q3와 수평으로 쌓을 수 없습니다: # In[114]: try: q5 = np.hstack((q1, q2, q3)) except ValueError as e: print(e) # ## `concatenate` # # `concatenate` 함수는 지정한 축으로도 배열을 쌓습니다. # In[115]: q7 = np.concatenate((q1, q2, q3), axis=0) # vstack과 동일 q7 # In[116]: q7.shape # 예상했겠지만 `hstack`은 `axis=1`으로 `concatenate`를 호출하는 것과 같습니다. # ## `stack` # # `stack` 함수는 새로운 축을 따라 배열을 쌓습니다. 모든 배열은 같은 크기를 가져야 합니다. # In[117]: q8 = np.stack((q1, q3)) q8 # In[118]: q8.shape # # 배열 분할 # # 분할은 쌓기의 반대입니다. 예를 들어 `vsplit` 함수는 행렬을 수직으로 분할합니다. # # 먼저 6x4 행렬을 만들어 보죠: # In[119]: r = np.arange(24).reshape(6,4) r # 수직으로 동일한 크기로 나누어 보겠습니다: # In[120]: r1, r2, r3 = np.vsplit(r, 3) r1 # In[121]: r2 # In[122]: r3 # `split` 함수는 주어진 축을 따라 배열을 분할합니다. `vsplit`는 `axis=0`으로 `split`를 호출하는 것과 같습니다. `hsplit` 함수는 `axis=1`로 `split`를 호출하는 것과 같습니다: # In[123]: r4, r5 = np.hsplit(r, 2) r4 # In[124]: r5 # # 배열 전치 # # `transpose` 메서드는 주어진 순서대로 축을 뒤바꾸어 `ndarray` 데이터에 대한 새로운 뷰를 만듭니다. # # 예를 위해 3D 배열을 만들어 보죠: # In[125]: t = np.arange(24).reshape(4,2,3) t # `0, 1, 2`(깊이, 높이, 너비) 축을 `1, 2, 0` (깊이→너비, 높이→깊이, 너비→높이) 순서로 바꾼 `ndarray`를 만들어 보겠습니다: # In[126]: t1 = t.transpose((1,2,0)) t1 # In[127]: t1.shape # `transpose` 기본값은 차원의 순서를 역전시킵니다: # In[128]: t2 = t.transpose() # t.transpose((2, 1, 0))와 동일 t2 # In[129]: t2.shape # 넘파이는 두 축을 바꾸는 `swapaxes` 함수를 제공합니다. 예를 들어 깊이와 높이를 뒤바꾸어 `t`의 새로운 뷰를 만들어 보죠: # In[130]: t3 = t.swapaxes(0,1) # t.transpose((1, 0, 2))와 동일 t3 # In[131]: t3.shape # # 선형 대수학 # # 넘파이 2D 배열을 사용하면 파이썬에서 행렬을 효율적으로 표현할 수 있습니다. 주요 행렬 연산을 간단히 둘러 보겠습니다. 선형 대수학, 벡터와 행렬에 관한 자세한 내용은 [Linear Algebra tutorial](math_linear_algebra.ipynb)를 참고하세요. # # ## 행렬 전치 # # `T` 속성은 랭크가 2보다 크거나 같을 때 `transpose()`를 호출하는 것과 같습니다: # In[132]: m1 = np.arange(10).reshape(2,5) m1 # In[133]: m1.T # `T` 속성은 랭크가 0이거나 1인 배열에는 아무런 영향을 미치지 않습니다: # In[134]: m2 = np.arange(5) m2 # In[135]: m2.T # 먼저 1D 배열을 하나의 행이 있는 행렬(2D)로 바꾼다음 전치를 수행할 수 있습니다: # In[136]: m2r = m2.reshape(1,5) m2r # In[137]: m2r.T # ## 행렬 점곱 # # 두 개의 행렬을 만들어 `dot` 메서드로 행렬 [점곱](https://en.wikipedia.org/wiki/Dot_product)을 실행해 보죠. # In[138]: n1 = np.arange(10).reshape(2, 5) n1 # In[139]: n2 = np.arange(15).reshape(5,3) n2 # In[140]: n1.dot(n2) # **주의**: 앞서 언급한 것처럼 `n1*n2`는 점곱이 아니라 원소별 곱셈입니다. # ## 역행렬과 유사 역행렬 # # `numpy.linalg` 모듈 안에 많은 선형 대수 함수들이 있습니다. 특히 `inv` 함수는 정방 행렬의 역행렬을 계산합니다: # In[141]: import numpy.linalg as linalg m3 = np.array([[1,2,3],[5,7,11],[21,29,31]]) m3 # In[142]: linalg.inv(m3) # `pinv` 함수를 사용하여 [유사 역행렬](https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_pseudoinverse)을 계산할 수도 있습니다: # In[143]: linalg.pinv(m3) # ## 단위 행렬 # # 행렬과 그 행렬의 역행렬을 곱하면 단위 행렬이 됩니다(작은 소숫점 오차가 있습니다): # In[144]: m3.dot(linalg.inv(m3)) # `eye` 함수는 NxN 크기의 단위 행렬을 만듭니다: # In[145]: np.eye(3) # ## QR 분해 # # `qr` 함수는 행렬을 [QR 분해](https://en.wikipedia.org/wiki/QR_decomposition)합니다: # In[146]: q, r = linalg.qr(m3) q # In[147]: r # In[148]: q.dot(r) # q.r는 m3와 같습니다 # ## 행렬식 # # `det` 함수는 [행렬식](https://en.wikipedia.org/wiki/Determinant)을 계산합니다: # In[149]: linalg.det(m3) # 행렬식 계산 # ## 고윳값과 고유벡터 # # `eig` 함수는 정방 행렬의 [고윳값과 고유벡터](https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors)를 계산합니다: # In[150]: eigenvalues, eigenvectors = linalg.eig(m3) eigenvalues # λ # In[151]: eigenvectors # v # In[152]: m3.dot(eigenvectors) - eigenvalues * eigenvectors # m3.v - λ*v = 0 # ## 특잇값 분해 # # `svd` 함수는 행렬을 입력으로 받아 그 행렬의 [특잇값 분해](https://en.wikipedia.org/wiki/Singular_value_decomposition)를 반환합니다: # In[153]: m4 = np.array([[1,0,0,0,2], [0,0,3,0,0], [0,0,0,0,0], [0,2,0,0,0]]) m4 # In[154]: U, S_diag, V = linalg.svd(m4) U # In[155]: S_diag # `svd` 함수는 Σ의 대각 원소 값만 반환합니다. 전체 Σ 행렬은 다음과 같이 만듭니다: # In[156]: S = np.zeros((4, 5)) S[np.diag_indices(4)] = S_diag S # Σ # In[157]: V # In[158]: U.dot(S).dot(V) # U.Σ.V == m4 # ## 대각원소와 대각합 # In[159]: np.diag(m3) # m3의 대각 원소입니다(왼쪽 위에서 오른쪽 아래) # In[160]: np.trace(m3) # np.diag(m3).sum()와 같습니다 # ## 선형 방정식 풀기 # `solve` 함수는 다음과 같은 선형 방정식을 풉니다: # # * $2x + 6y = 6$ # * $5x + 3y = -9$ # In[161]: coeffs = np.array([[2, 6], [5, 3]]) depvars = np.array([6, -9]) solution = linalg.solve(coeffs, depvars) solution # solution을 확인해 보죠: # In[162]: coeffs.dot(solution), depvars # 네 같네요 # 좋습니다! 다른 방식으로도 solution을 확인해 보죠: # In[163]: np.allclose(coeffs.dot(solution), depvars) # # 벡터화 # # 한 번에 하나씩 개별 배열 원소에 대해 연산을 실행하는 대신 배열 연산을 사용하면 훨씬 효율적인 코드를 만들 수 있습니다. 이를 벡터화라고 합니다. 이를 사용하여 넘파이의 최적화된 성능을 활용할 수 있습니다. # # 예를 들어, $sin(xy/40.5)$ 식을 기반으로 768x1024 크기 배열을 생성하려고 합니다. 중첩 반복문 안에 파이썬의 math 함수를 사용하는 것은 **나쁜** 방법입니다: # In[164]: 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` 함수로 좌표 벡터를 사용해 행렬을 만듭니다. # In[165]: 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 # In[166]: Y # 여기서 볼 수 있듯이 `X`와 `Y` 모두 768x1024 배열입니다. `X`에 있는 모든 값은 수평 좌표에 해당합니다. `Y`에 있는 모든 값은 수직 좌표에 해당합니다. # # 이제 간단히 배열 연산을 사용해 계산할 수 있습니다: # In[167]: data = np.sin(X*Y/40.5) # 맷플롯립의 `imshow` 함수를 사용해 이 데이터를 그려보죠([matplotlib tutorial](tools_matplotlib.ipynb)을 참조하세요). # In[168]: 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() # # 저장과 로딩 # # 넘파이는 `ndarray`를 바이너리 또는 텍스트 포맷으로 손쉽게 저장하고 로드할 수 있습니다. # # ## 바이너리 `.npy` 포맷 # # 랜덤 배열을 만들고 저장해 보죠. # In[169]: a = np.random.rand(2,3) a # In[170]: np.save("my_array", a) # 끝입니다! 파일 이름의 확장자를 지정하지 않았기 때문에 넘파이는 자동으로 `.npy`를 붙입니다. 파일 내용을 확인해 보겠습니다: # In[171]: with open("my_array.npy", "rb") as f: content = f.read() content # 이 파일을 넘파이 배열로 로드하려면 `load` 함수를 사용합니다: # In[172]: a_loaded = np.load("my_array.npy") a_loaded # ## 텍스트 포맷 # # 배열을 텍스트 포맷으로 저장해 보죠: # In[173]: np.savetxt("my_array.csv", a) # 파일 내용을 확인해 보겠습니다: # In[174]: with open("my_array.csv", "rt") as f: print(f.read()) # 이 파일은 탭으로 구분된 CSV 파일입니다. 다른 구분자를 지정할 수도 있습니다: # In[175]: np.savetxt("my_array.csv", a, delimiter=",") # 이 파일을 로드하려면 `loadtxt` 함수를 사용합니다: # In[176]: a_loaded = np.loadtxt("my_array.csv", delimiter=",") a_loaded # ## 압축된 `.npz` 포맷 # # 여러 개의 배열을 압축된 한 파일로 저장하는 것도 가능합니다: # In[177]: b = np.arange(24, dtype=np.uint8).reshape(2, 3, 4) b # In[178]: np.savez("my_arrays", my_a=a, my_b=b) # 파일 내용을 확인해 보죠. `.npz` 파일 확장자가 자동으로 추가되었습니다. # In[179]: with open("my_arrays.npz", "rb") as f: content = f.read() repr(content)[:180] + "[...]" # 다음과 같이 이 파일을 로드할 수 있습니다: # In[180]: my_arrays = np.load("my_arrays.npz") my_arrays # 게으른 로딩을 수행하는 딕셔너리와 유사한 객체입니다: # In[181]: my_arrays.keys() # In[182]: my_arrays["my_a"] # # 그 다음은? # # 넘파이 기본 요소를 모두 배웠지만 훨씬 더 많은 기능이 있습니다. 이를 배우는 가장 좋은 방법은 넘파이를 직접 실습해 보고 훌륭한 [넘파이 문서](http://docs.scipy.org/doc/numpy/reference/index.html)에서 필요한 함수와 기능을 찾아 보세요.