파이썬 원소 곱 - paisseon wonso gob

안녕하세요. 지난 포스팅의 넘파이 알고 쓰자 - 쌍곡선 함수(Hyperbolic functions)에서는 넘파이에서 제공하는 쌍곡함수에 대해서 소개하였습니다. 오늘은 더 간단한 넘파이에서 제공하는 덧셈, 곱셈, 뺄셈를 할 수 있는 다양한 함수들을 소개해드리도록 하겠습니다. 여기서는 기존파이썬에서 제공하는 함수와 얼마나 많은 시간 차이가 나는지도 알아보도록 하겠습니다.

1. numpy.prod(a, axis=None)

가장 먼저 볼 함수는 곱셈 함수입니다. 기존의 파이썬과는 다르게 axis가 존재하여 원하는 축을 중심으로 계산을 수행하여 활용성이 좀 더 높은 것을 볼 수 있습니다. axis=None이라면 입력되는 넘파이 배열의 원소를 전부 곱하는 함수입니다. 

a = np.arange(1, 6)
np.prod(a) # 120

위의 코드에서는 1~5까지의 넘파이 배열을 생성한 뒤 전부 곱하기 때문에 120을 반환하는 것을 볼 수 있습니다. 그렇다면 넘파이 배열이 엄청커진다면 얼마나 시간이 소요될까요? 간단한 실험을 위해서 아래의 함수를 구현합니다.

def product_time_checker(num) :
    a = [i for i in range(1, num+1)]
    result = 1
    start_time = time()
    for i in a :
        result *= i
    end_time = time() - start_time
    print("for loop time = ", end_time)
    start_time = time()
    result = prod(a)
    end_time = time() - start_time
    print("math module time = ", end_time)
    a_np = np.array(a)
    start_time = time()
    result = np.prod(a)
    end_time = time() - start_time
    print("numpy module time = ", end_time)

이제 배열의 개수를 늘려가면서 시간이 얼마나 소비되는 지 비교해보겠습니다.

product_time_checker(100)
# for loop time =  9.059906005859375e-06
# math module time =  8.106231689453125e-06
# numpy module time =  2.8848648071289062e-05

product_time_checker(1000)
# for loop time =  0.0003142356872558594
# math module time =  0.00031304359436035156
# numpy module time =  3.600120544433594e-05

product_time_checker(10000)
# for loop time =  0.02707982063293457
# math module time =  0.026133060455322266
# numpy module time =  5.2928924560546875e-05

product_time_checker(100000)
# for loop time =  3.1611640453338623
# math module time =  3.1249091625213623
# numpy module time =  0.0001819133758544922

보시면 for loop > math module > numpy module 순으로 시간이 오래걸리는 것을 관찰할 수 있습니다. 재밌는 점은 데이터가 100개 정도 있을 때는 넘파이 모듈이 for loop에 비해 0.23배 정도 더 느린것을 볼 수 있습니다. 하지만 데이터의 개수를 점점 많이 늘려감에 따라서 for loop와 math module의 시간은 비약적으로 증가하지만 numpy module의 경우 그리 많이 증가하지 않는 것도 관찰할 수 있습니다. 곱해야하는 데이터가 십만개인 경우 for loop에 비해 508.65배 정도 빠르고, math module에 비해 508.76배 정도 더 느립니다. 이를 통해 알 수 있는 것은 빅데이터와 같이 처리해야하는 데이터가 너무 많은 경우에는 넘파이 모듈을 이용하는 것이 시간적인 측면이나 코드의 간결성 측면에서나 더 좋다는 것입니다.

2. numpy.sum(a, axis=None)

이 함수 역시 간단하기 때문에 걸리는 시간만 비교해보도록 하겠습니다. 

def sum_time_checker(num) :
    a = [i for i in range(1, num+1)]
    
    result = 0
    start_time = time()
    for i in a :
        result += i
    end_time = time() - start_time
    print("for loop time = ", end_time)
    
    start_time = time()
    result = sum(a)
    end_time = time() - start_time
    print("math module time = ", end_time)
    
    a_np = np.array(a)
    start_time = time()
    result = np.sum(a)
    end_time = time() - start_time
    print("numpy module time = ", end_time)
sum_time_checker(100)
# for loop time =  5.0067901611328125e-06
# math module time =  8.106231689453125e-06
# numpy module time =  3.361701965332031e-05

sum_time_checker(1000)
# for loop time =  4.100799560546875e-05
# math module time =  7.152557373046875e-06
# numpy module time =  2.5033950805664062e-05

sum_time_checker(10000)
# for loop time =  0.00043392181396484375
# math module time =  6.914138793945312e-05
# numpy module time =  6.723403930664062e-05

sum_time_checker(100000)
# for loop time =  0.005200862884521484
# math module time =  0.0006539821624755859
# numpy module time =  0.00021219253540039062

sum_time_checker(1000000)
# for loop time =  0.045256853103637695
# math module time =  0.004628419876098633
# numpy module time =  0.0007779598236083984

sum_time_checker(10000000)
# for loop time =  0.4517228603363037
# math module time =  0.04965090751647949
# numpy module time =  0.007107734680175781

이번에도 마찬가지로 데이터가 적을 때는 오히려 넘파이 모듈의 사용이 좋은 효율을 보이기는 어렵습니다. 하지만 데이터가 점점 많아짐에 따라서 계산하는 속도의 차이가 크지 않은 것을 볼 수 있습니다. 특히, 마지막의 경우에는 for loop에 비해 약 63배, math module에 비해 약 7배 정도 빠른 성능을 보인것을 관찰할 수 있습니다. 

3.numpy.nanprod(a, axis=None), numpy.nansum(a, axis=None)

이번에도 위에서 언급한 함수들과 동일한 기능을 하는 함수들입니다. 차이점은 np.prod, np.sum 같은 경우에는 np.nan이라는 값을 만나게 되면 np.nan 값을 반환하지만 이 함수들은 해당 값들은 제외하고 곱셈이나 덧셈을 하게 됩니다. 다루는 데이터에 np.nan 값을 처리하기 애매한 경우에 유용하게 사용할 수 있는 함수입니다. 

a = np.array([1, 2, 3, 4, np.nan])

np.prod(a) # nan
np.sum(a) # nan
np.nanprod(a) # 24.0
np.nansum(a) # 10.0

4. numpy.cumprod(a, axis=None), numpy.cumsum(a, axis=None)

이번에는 기능이 살짝 다른 함수들입니다. a 라는 넘파이 배열이 입력되면 이 함수는 누적곱과 누적합을 반환합니다. 예를 들어 배열이 [1, 2, 3]이 입력되는 경우 np.cumprod에서는 [1, 1 * 2, 1 * 2 * 3]을 반환하고, np.cumsum에서는 [1, 1 + 2, 1 + 2 + 3]을 반환하게 됩니다. 물론, axis를 지정해주면 해당 axis만 cumprod, cumsum을 수행하게 되겠죠. 바로 간단한 예제를 통해서 확인해보도록 하겠습니다.

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

np.cumprod(a) # array([  1,   2,   6,  24, 120])
np.cumsum(a) # array([ 1,  3,  6, 10, 15])

5. numpy.nancumprod(a, axis=None), numpy.nancumsum(a, axis=None)

그렇다면 넘파이 배열 a에 np.nan 값이 포함되어 있는 경우에는 어떻게 해야할까요? 물론 이를 위한 함수도 존재합니다.

a = np.array([1, 2, 3, 4, np.nan])

np.cumprod(a) # array([ 1.,  2.,  6., 24., nan])
np.cumsum(a) # array([ 1.,  3.,  6., 10., nan])

np.nancumprod(a) # array([ 1.,  2.,  6., 24., 24.])
np.nancumsum(a) # array([ 1.,  3.,  6., 10., 10.])

그냥 cumprod, cumsum에서는 np.nan이 껴있기 때문에 해당 인덱스 이후로는 전부 nan을 반환합니다. 그에 반에 nancumprod, nancumsum 함수의 경우에는 해당 인덱스가 np.nan이라면 이전 인덱스의 결과를 그대로 복사해주게 됩니다.

6. numpy.diff(a, n=1, axis=-1)

이 함수는 이산미분을 수행하는 함수입니다. 수학적으로 1차 이산미분은 out[i] = a[i + 1] - a[i]로 정의됩니다. 만약 1차보다 큰 고차 이산미분을 수행하게 되면 diff 함수를 recursive 하게 사용하여 계산하게 됩니다. 이는 쉽게 n을 1보다 큰 값으로 지정해주면 됩니다. 아래의 예제를 통해서 보도록 하겠습니다. 

a = np.array([-2, -1, 0, 1, 2, 3, 3, 3, 3, 3, -3, -3, -3, -3])

diff_1 = np.diff(a) # array([ 1,  1,  1,  1,  1,  0,  0,  0,  0, -6,  0,  0,  0])
diff_2 = np.diff(a, n=2) # array([ 0,  0,  0,  0, -1,  0,  0,  0, -6,  6,  0,  0])

plt.plot(np.arange(len(a)), a, 'bs')
plt.plot(np.arange(len(a)), a, 'b--')
plt.plot(np.arange(len(diff_1)), diff_1, 'ro')
plt.plot(np.arange(len(diff_1)), diff_1, 'r--')
plt.plot(np.arange(len(diff_2)), diff_2, 'g+')
plt.plot(np.arange(len(diff_2)), diff_2, 'g--')
plt.plot([0, len(a)], [0, 0], 'k')
plt.savefig('./finite_derivate.png', dpi=300)
파이썬 원소 곱 - paisseon wonso gob

상단 그림에서 빨간색 원은 1차 미분입니다. 파란색 사각형이 0~5까지 1의 크기로 늘어나고 있기 때문에 1~5까지의 1차 미분값은 1이 됩니다. 초록색 십자형은 2차 미분입니다. 즉, 1차 미분을 한번 더 미분한것이라고 볼 수 있습니다. 1차 미분은 1~5까지는 값이 동일하기 때문에 1~4까지의 2차 미분값은 0입니다. 이때, 여기서 가장 눈여겨 볼 부분은 2차 미분이 음수에서 양수로 변화하는 x = 10인 부분입니다. 이 부분은 이미지 처리에서 zero-crossing이라고 해서 이미지의 가장자리(edge)를 추출해낼 수 있는 부분으로 간주하고 있기 때문에 이미지 처리를 공부하신다면 반드시 알아두어야할 개념입니다.