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배 빠른 것을 볼 수 있다.



댓글

이 블로그의 인기 게시물

쉽게 설명한 파티클 필터(particle filter) 동작 원리와 예제

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

간단한 cfar 알고리즘에 대해

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

base64 인코딩 디코딩 예제 c 소스