numpy.einsum을 이용한 빠른 2d convolution 연산 (conv2d)예제 코드

 본 포스트는 numpy.einsum을 사용하여 2차원 합성곱 연산을 수행하는 예제 코드를 싣고 있다.


단순 연산을 사용한 2차원 합성곱

단순히 for loop를 사용하여 아래처럼 2차원 합성곱 연산을 수행할 수 있다.

import numpy as np

def conv2d(image, kernel):
    kernel = np.flipud(np.fliplr(kernel)) #XCorrel
    output = np.zeros(np.subtract(image.shape, kernel.shape))
    
    for y in range(output.shape[1]):
        for x in range(output.shape[0]):
            output[x, y] = (kernel * image[x: x + kernel.shape[0], y: y + kernel.shape[1]]).sum()

    return output


하지만, 위 경우 image의 크기가 커지면 연산양이 많아져 수행 속도가 느려지게 된다. 


numpy.einsum을 사용한 2차원 합성곱

 빠른 속도의 합성곱 연산을 원한다면, numpy.lib.stride_tricks.as_strided와 numpy.einsum을 사용하면 빠른 속도로 2차원 함성곱 연산을 수행할 수 있다.

import numpy as np

def conv2d_np(image, kernel):
    kernel = np.flipud(np.fliplr(kernel)) #XCorrel
    
    sub_matrices = np.lib.stride_tricks.as_strided(image,
                                                   shape = tuple(np.subtract(image.shape, kernel.shape))+kernel.shape, 
                                                   strides = image.strides * 2)

    return np.einsum('ij,klij->kl', kernel, sub_matrices)


numpy.lib.stride_tricks.as_strided는 배열을 주어진 shape과 stride에 맞게 새로운 배열로 만들어준다. 

https://numpy.org/doc/stable/reference/generated/numpy.lib.stride_tricks.as_strided.html

 

예를 들어 위 conv2d_np 코드에서는 입력 image가 아래와 같을 때,

ipdb> image
array([[11, 12, 13, 14, 15, 16],
       [21, 22, 23, 24, 25, 26],
       [31, 32, 33, 24, 25, 26],
       [41, 42, 43, 44, 45, 46],
       [51, 52, 53, 54, 55, 56]])

numpy.lib.stride_tricks.as_strided를 사용해 새로 만들어진 sub_matrices는 다음과 같다. 

ipdb> sub_matrices
array([[[[11, 12, 13],
         [21, 22, 23],
         [31, 32, 33]],

        [[12, 13, 14],
         [22, 23, 24],
         [32, 33, 24]],

        [[13, 14, 15],
         [23, 24, 25],
         [33, 24, 25]]],


       [[[21, 22, 23],
         [31, 32, 33],
         [41, 42, 43]],

        [[22, 23, 24],
         [32, 33, 24],
         [42, 43, 44]],

        [[23, 24, 25],
         [33, 24, 25],
         [43, 44, 45]]]])


즉 합성곱을 위해 image 배열을 kernel 배열의 크기에 맞게 분리해 새로운 배열로 만들어주는 일을 수행한다. 


위에서 만들어진 배열을 numpy.einsum을 사용하면 쉽게 스칼라곱을 취할 수 있다. 이는 다음의 코드를 numpy.einsum만으로 쉽고 빠르게 수행할 수 있다는 의미이다.

for y in range(output.shape[1]):
    for x in range(output.shape[0]):
        output[x, y] = (kernel * image[x: x + kernel.shape[0], y: y + kernel.shape[1]]).sum()


numpy.einsum은 아래 stackoverflow 사이트에 잘 설명되어 있다.

https://stackoverflow.com/questions/26089893/understanding-numpys-einsum



성능 비교

위 예제 코드 conv2d의 성능을 비교하면, 동일한 이미지에서 for loop를 사용한 conv2d는 약1.33초의 시간이 걸리고, numpy.einsum을 사용한 conv2d_np는 0.01초의 시간이 걸렸다.

numpy.einsum을 사용한 2차원 합성곱 연산이 약 133배 빠른 것을 볼 수 있다.



댓글

이 블로그의 인기 게시물

간단한 cfar 알고리즘에 대해

바로 프로젝트 적용 가능한 FIR Filter (low/high/band pass filter )를 c나 python으로 만들기

소음 측정 원리 개요

python winsound를 이용한 윈도우 환경에서 소리 재생 예제

아두이노(arduino) 심박센서 (heart rate sensor) 심박수 측정 example code