Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

예제코드들은 code에서 다운로드 할 수 있다. 테스트에 사용한 python 버전은 아래와 같다.
(my_env) yundream@yundream:~/workspace/python/numpy$ python
Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.

예제

NumPy는 모든 것을 벡터(vectorization)로 다룬다. 만약 파이썬에 익숙하다면, 사고방식의 변화 때문에 어려움을 겪을 수 있다. 앞으로 당신은 vector, array, views, ufuncs 와 같은 것들을 다루게 될 것이다.

객체지향적 접근

간단한 랜덤워크(random walk)예제를 만들었다. 객체지향적인 접근에서는 RandomWalk 클래스를 정의하고, 랜덤한 현재 위치를 반환하는 walk 메서드를 작성한다. 아래 코드를 보자. 멋지다. 하지만 느릴 것이다.
import random
import timeit

class RandomWalker:
    def __init__(self):
        self.position=0

    def walk(self, n):
        self.position = 0
        for i in range(n):
            yield self.position
            self.position += 2*random.randint(0,1) -1
walker = RandomWalker()

timeit패키지를 이용 벤치마크를 돌려봤다.
if __name__ == "__main__":
    from tools import timeit
    walker = RandomWalker()
    timeit("[position for position in walker.walk(n=10000)]", globals())

$ python randomwalk.py 
10 loops, best of 3: 11 msec per loop

절차지향적 접근

이런 간단한 문제를 푸는데 굳이 클래스를 만들 필요는 없을 것이다. 그래서 그냥 walk 메서드만 구현을 했다.
def random_walk(n):
    position = 0
    walk = [position]
    for i in range(n):
        position += 2*random.randint(0, 1)-1
        walk.append(position)
    return walk

if __name__ == "__main__":
    from tools import timeit
    timeit("random_walk(n=10000)", globals())
벤치마크 결과다.
10 loops, best of 3: 9.24 msec per loop
클래스를 포기하는 것으로 약간의 CPU 시간을 아꼈다. 생각만큼 크게 아낀 것은 아니다. 아마도 내부적으로 객체지향을 구현하면서 약간의 사이클을 소비했기 때문일 것이다.

Vectorized 접근

그러나 효과적인 루프작업을 위한 iterators(반복자)를 제공하는 itertools 모듈을 이용해서 실행시간을 줄일 수 있다.
def random_walk_faster(n=1000):
    from itertools import accumulate
    # Only available from Python 3.6
    steps = random.choices([-1,+1], k=n)
    return [0]+list(accumulate(steps))

 walk = random_walk_faster(1000)
순차적으로 루프를 도는 대신에, 데이터를 백터에 저장해서 연산하도록 코드를 수정했다. 결과적으로 루프를 제거했고 더 빠른 작업 수행이 가능해졌다.

>>> from tools import timeit
>>> timeit("random_walk_faster(n=10000)", globals())
10 loops, best of 3: 2.21 msec per loop
이전 코드에 비해서 85% 정도의 성능을 개선했다. 이제 NumPy 벡터화를 이용해서, 이 과정을 더욱 효율적으로 그리고 더 간단하게 만들어보자.

import numpy as np
import random

def random_walk_fatest(n=1000):
    steps = np.random.choice([-1,+1],n)
    return np.cumsum(steps)

walk = random_walk_fatest(1000)
실행해보자. 500배 이상 빨라진 것을 확인 할 수 있다.
>>> from tools import timeit
>>> timeit("random_walk_fastest(n=10000)", globals())
1000 loops, best of 3: 14 usec per loop

가독성과 성능

다음 장으로 넘어가기 전에 NumPy에 익숙해지면 발생할 수 있는 잠재적인 문제점에 대해서 경고할게 있다. NumPy는 매우 강력한 라이브러리로 이것을 이용해서 놀라운 애플리케이션들을 개발 할 수 있다. 하지만 주석을 제대로 달지 않을 경우 가독성의 댓가를 치룰 수 있다. 작성 시점에 주석을 달지 않으면 몇 주(혹은 며칠)후에 무엇을 하려고 만든 코드인지 알 수 없게 될 것이다. 예를들어 아래의 두 코드가 무슨 일을 하는지 설명 할 수 있을까 ? 아마도 첫번째 코드는 말 할 수 있지만, 두 번째 코드를 쉽게 설명 할 수 없을 것이다.
def function_1(seq, sub):
    return [i for i in range(len(seq) - len(sub) +1) if seq[i:i+len(sub)] == sub]

def function_2(seq, sub):
    target = np.dot(sub, sub)
    candidates = np.where(np.correlate(seq, sub, mode='valid') == target)[0]
    check = candidates[:, np.newaxis] + np.arange(len(sub))
    mask = np.all((np.take(seq, check) == sub), axis=-1)
    return candidates[mask]
두번째 함수는 첫번째 함수를 numpy에 맞게 최적화한 함수다. 두번째 함수는 첫번째 함수에 비해서 10배 정도 더 빠르지만 10배 이상으로 이해하기 힘들다.