본문 바로가기

카테고리 없음

[밑바닥 딥러닝] Ctypes로 짠 C함수의 합성곱과, im2col 방식 4차원 합성곱 계산 성능 비교...

 

<밑바닥부터 시작하는 딥러닝>이라는 책을 공부하고 있는데, 얇은 책에 방대한 내용을 담고 있어서 책만으로 제대로 공부하기 힘들다. 마침 수원대학교의 데이터과학부 한경훈 교수님이 이 책을 교재로 삼아 아주 친절하고 상세하게, 그리고 전문적으로 유튜브 영상에서 학교 강의를 하고 계시길래, 아주 열심히 강의를 듣고 있다. 심봤다,라는 표현이 딱 어울린다.

 

한 교수님이 CNN 합성곱 설명을 하면서 합성곱을 공식대로 구현하면 im2col방식보다 수백배 느리다고 말한 부분에 의구심이 들었다. 물론 전제가 깔려 있다. 파이썬에서 파이썬 코드로 합성곱을 짠 코드와, C언어로 짜여지고 장기간 최적화 과정을 밟은 Numpy의 핵심 기능인 numpy.dot과 transpose, 그외 행렬 계산 기능을 사용한 코드의 성능을 비교하면 그렇다는 것이다.

 

전제부터 공정하지 못하다. A라는 펑션을 어셈블리어로 짜고, B라는 펑션을 C언어로 짜서, 펑션 A가 펑션C보다 속도가 10배 빠르니 A가 B보다 10배 나은 알고리즘이라고 말하는 것과 같다.

 

따라서 합리적으로 비교하자면, C언어로 짠 합성곱 함수를 파이썬에서 임포트해서 합성곱 공식대로 계산해보는 것이다. 그리고 im2col 방식과 비교해보는 것이다.

 

Ctypes 합성곱 테스트 코드

https://madrabbit7.tistory.com/46

 

im2col 방식 합성곱 테스트 코드

https://madrabbit7.tistory.com/47

 

Ctypes 합성곱 테스트 코드는 최종 말단에 directConvolution3D1FCtypes() 라는 함수가 있고, 거기서 _directConvolve2d()라는 C언어 함수를 호출해서 계산한다. _directConvolve2d()는 파이썬에서 2차원 배열(행렬)을 넘겨 받아 합성곱을 계산해서 되넘겨준다. 여기서 Ctypes에 국한된 얘기일 수도 있고, 필자가 깊숙하게 몰라서 그런 것일 수도 있지만, 파이썬에서는 행렬을 벡터로 C에게 넘겨주기 때문에 C에서는 벡터로 행렬 인덱싱을 해야 하는 곤란한 상황에 처하게 된다. (C, H, W) 이라는 (채널, height, width)라는 3차원 데이터를 파이썬으로 directConvolution3D1F라는 함수를 작성해봤는데 인덱싱 계산 부분에서 그렇게 어렵진 않았다. 행렬 합성곱이라고 하더라도 2차원 배열 내에서 위치를 찾는 과정에서 for문 2개, 합성곱을 구하는 과정에 for문 2개가 들어가기 때문에 총 4개의 for문을 사용하게 된다.

 

directConvolve2d.c : https://madrabbit7.tistory.com/45

 

int start = i*stride * xw + j*stride;

 

start는 필터의 좌측 최상단이 데이터 x에 위치한 인덱스인데, 이것만 잘 잡아도 계산의 절반은 해결된다. 

나머지 하나는 제일 마지막에 구체적인 수치를 누적하는 부분이다.

 

dst[i * ow + j] += src[start + l + xw * k] * f[k * fw + l];

 

보면, 배열(벡터) 계산인데, 인덱스가 i, j, k, l에 변수 ow, start, xw, fw이 사용되고 있다.

 

그리고 결과를 저장할 변수를 0으로 초기화하는 것도 중요하다.

    dst[i * ow + j] = 0;

 

이 초기화를 안 하고 파이썬에 맡겨놨더니 파이썬에서 1번 초기화하고 두 번째 사용하면서 초기화를 하지 않아서 값이 누진 누적되는 결과가 나타나 원인을 찾느라 좀 헤맸다.

 

어쨌던 2차원 배열을 Ctypes을 이용해 작성한 것이라, 4차원에 적용할 때 약간의 아쉬움이 있을 수 있지만, scipy signal 함수인 correlate2d 함수와 성능 비교를 했을 때 동일한 처리 속도가 나와서 나름 만족스러웠다.

 

 

madrabbit7.tistory.com/44

 

그렇다면 4차원 im2col 방식과 비교하면 어떨까?

 

    x4 = np.random.randint(0, 256, 100*3*256*256)

    x4 = x4.reshape((100, 3, 256, 256))

 

먼저 난수를 100*3*256*256개(19,660,800개)를 만들어 파일로 저장해서 그 데이터를 양측이 함께 사용한다.

    f4 = np.random.randint(-2, 3, 81)

    f4 = f4.reshape(3, 3, 3, 3)

    

필터 데이터도 난수로 발생시켰고, 채널은 둘 다 3개로 동일하게 만들었다.

난수를 파일로 저장하지 않고 처음에 각기 실행을 시켰는데, 합성곱 결과가 서로 다르게 나와서 좀 의아했는데, 그런 실수를 하는 사람은 미친토끼 혼자만은 아니겠지.

stride를 2로 주면 stride = 1일 때보다 데이터 찾는 속도가 3~4배 빨라서 금방 끝나버린다. 몇 초쯤 걸리면 좋으므로 stride는 1로 했다. 

    

리눅스에서 time 명령으로 실행시켜서 시간을 쟀다.

(리눅스가 좋은 점이 C의 데이터 타입 중 long double이 16바이트라서 엄청나게 큰 수를 처리할 수 있다. 윈도우는 8바이트고.... 또한, 코맨드 라인에서 다양한 명령을 사용해 여러 작업을 처리할 수 있다.)

 

먼저 Ctypes 선수 출전이다.

---------------------------

[Don@localhost PYC]$ time python directConvolutionCtypes.py 

[[[ -584.  -203.  1064. ...  -421.   667.   114.]

  [ 2241.   406.   485. ...   520.   276.   823.]

  [   70.   764.   310. ...  -556.  -135.   510.]

  ...

  [ 1737.   486.   575. ...  -838.   989.   272.]

  [  171.    76.   208. ...   698.  -820.   481.]

  [  520.   183.   406. ...   306.   686.   190.]]

 

 [[-2317.  -928. -1664. ... -1125. -1073. -1095.]

  [  396.  -646.  -912. ... -1630.   -87.  -110.]

  [ -693.  -634. -1274. ... -1257.  -204.  -618.]

  ...

  [ -665.  -624.  -981. ...  -280. -1088.  -437.]

  [-1040.  -633.  -113. ...  -232. -1603.  -173.]

  [-1130. -1009. -1571. ...  -519.    37. -1247.]]]

 

real 0m6.056s

user 0m5.220s

sys 0m0.811s

--------------------------

 

대략 6초 정도 걸린다. 시스템 상황에 따라 대략 6초~8초 사이를 오락가락한다.

 

다음은 im2col 선수다. 든든한 넘파이를 우군으로 거느린 녀석이다.

--------------------------

[Don@localhost PYC]$ time python  benchim2col.py

[[[[ -584.  -203.  1064. ...  -421.   667.   114.]

   [ 2241.   406.   485. ...   520.   276.   823.]

   [   70.   764.   310. ...  -556.  -135.   510.]

   ...

   [ 1737.   486.   575. ...  -838.   989.   272.]

   [  171.    76.   208. ...   698.  -820.   481.]

   [  520.   183.   406. ...   306.   686.   190.]]

 

  [[-2317.  -928. -1664. ... -1125. -1073. -1095.]

   [  396.  -646.  -912. ... -1630.   -87.  -110.]

   [ -693.  -634. -1274. ... -1257.  -204.  -618.]

   ...

real 0m2.987s

user 0m2.160s

sys 0m1.718s

----------------------------

비슷한 속도가 나올줄 알았는데 좀 아쉽다. 대략 Ctypes가 im2col 방식보다 2~3배 느리다.

너무 슬퍼하지는 말자. 2차원 배열까지 C로 작성해서 그렇지 3차원까지 C로 정의한다면 속도가 더 빨라지지 않을까?

물론 그렇게까지 하고 싶지 않다. 빨리 딥러닝의 자연어 처리 등등을 배워 인공지능 대본 쓰기, 인공지능 작곡, 인공지능 채팅 프로그램을 짜야 하지 여기서 이미 시간을 충분히 쓴 것 같다.

 

Ctypes는 가끔씩 특정 부분의 성능 향상을 위해 건드려보면 재미도 있고 성취감도 있을 것 같다.

 

* 추가: C 동적 라이브러리의 경우, gcc 컴파일 옵션에 따라 성능이 달라짐을 뒤늦게 새삼 깨달았다. 

* 20년의 세월이 흐르다 보니, -O2 최적화 컴파일에 따라 성능이 상당히 달라짐을 까먹고 있었는데, 수치 계산에 있어서는 성능 차이가 더욱 큰 듯하다.

컴파일 최적화 옵션을 None, -O1, -O2, -O3, -Os로 줬을 경우, 실행 속도 차이가 상당한 차이가 있다. im2col도 매번 실행할 때마다 조금씩 속도가 달라진다. 모두 5번 연속 실행해서 평균을 냈다. C 동적 라이브러리 최고 속도인 -O3 최적화 옵션 평균을 im2col 평균으로 나누면 약 45% C 동적 라이브러리 쪽이 느리다는 이야기다. 코드 최적화하기에 따라 numpy를 뒤집을 수도 있음을 확인했으니 안심하고 잠을 자야겠다...

  gcc 컴파일 옵션          
             
None -O1 -O2 -Os -O3 im2col  
5.964 3.87 3.764 4.33 3.625 3.908  
6.028 3.917 3.71 4.011 3.634 2.739  
5.955 3.882 3.714 4.202 3.683 2.277  
5.949 3.92 3.746 4.063 3.717 1.893  
5.987 3.875 3.795 4.053 3.668 1.849  
             
5.9766 3.8928 3.7458 4.1318 3.6654 2.5332 average