두 번째 예제는 Caffe의 기본 구조체인 Blob에 대해서 좀 더 알아보겠습니다.
Edit me

By koosy on 2015.09.24

Blob은 Caffe의 기본 데이터구조 입니다. 예제파일은 요기있습니다. 파일 준비단계는 앞에서 설명했으므로 생략하구요, 바로 소스파일로 들어가 봅시다.

소스파일

이번 예제에서는 두 개의 소스파일을 준비했습니다. ex_blob.cppex_math.cpp 인데요. CMake에 아직 익숙하지 않으신 분들을 위해서 한 프로젝트에서 두 개의 실행파일을 어떻게 만드는지 간단히 알아볼까요? CMakeLists.txt파일을 열어보면 다음과 같습니다. (프로젝트 파일을 여는 방법은 여기을 보세요.)

cmake_minimum_required(VERSION 2.8.8)

project (ex_blob)

find_package(Caffe)
include_directories(${Caffe_INCLUDE_DIRS})
add_definitions(${Caffe_DEFINITIONS})

add_executable(ex_blob ex_blob.cpp)
target_link_libraries(ex_blob ${Caffe_LIBRARIES})

add_executable(ex_math ex_math.cpp)
target_link_libraries(ex_math ${Caffe_LIBRARIES})

척 보시면 아시겠죠? add_executable()target_link_libraries()를 그냥 두 번 써주면 됩니다. 참 쉽죠?

먼저 첫 번째 ex_blob.cpp 파일에서는 blob의 초기화, 재형상화, 난수값 할당, 연산 등을 소개하고요. 특히 CPU에서 또는 GPU에서 선택하여 연산하는 방법과, 실행할 때의 계산 속도 차이를 한번 살펴보도록 하겠습니다.

두 번째 ex_math.cpp 파일에서는 Caffe가 지원하는 blob에서의 연산 종류가 무엇이 있는지 알아보고, 그 중에 몇 가지는 한번 테스트 해보도록 하죠..

blob 초기화 및 데이터 할당

여러분들의 ex_blob.cpp파일을 한번 열어보세요. 헤더파일 및 선언부는 특별한건 없지만, 나중에 계산 속도를 측정해 보기 위해서 COMPTIME() 하나 만들어 봤어요.

clock_t tStart, tEnd;
#define COMPTIME(X)          \
cout << "CompTime of "<< (X) <<": " << (double)(tEnd-tStart)/CLOCKS_PER_SEC<<endl;

그럼 main()함수 안에 있는 초기화 부분을 먼저 보도록 합시다.

Blob<Dtype>* const blob = new Blob<Dtype>(20, 30, 40, 50);

이 부분은 첫 번째 예제와 같이 n=20, c=30, h=40, w=50 인 blob을 생성했습니다. 한번 생성된 blob은 다음 보시는 것처럼 크기를 재형상화(reshaping)할 수 있습니다.

blob->Reshape(50, 40, 30, 20);

이것은 나중에 기존에 만들어진 네트워크 모델에다가 새로운 데이터를 넣어서 prediction을 하거나, fine tuning 할 때 유용하게 사용할 수 있으니 잘 봐두세요.

그 다음에는 UniformFiller로 균일분포에서 셈플링한 난수를 blob에 채워봅시다.

FillerParameter filler_param;
filler_param.set_min(-3);
filler_param.set_max(3);
UniformFiller<Dtype> filler(filler_param);
filler.Fill(blob);

UniformFiller외에도 Filler에는 ConstantFiller, GaussianFiller, PositiveUnitballFiller, XavierFiller 등이 있습니다. 이름만 보셔도 감이 오시죠? 자세한 정보는 Filler.hpp를 보세요. (Filler만 다루는 페이지를 하나 만들어야겠습니다.)

데이터로 채워진 blob 값을 보고 싶으시죠? 아니면 Filler를 쓰지 않고 각 셀단위로 데이터를 저장하고 싶으세요? 그건 dataset을 다루는 다음 예제에서 같이 알아보도록 하겠습니다.

CPU/GPU blob 연산

이번에는 blob 안에 있는 데이터의 연산이 어떻게 이루어지는지 알아보도록 하겠습니다. blob은 여러가지 데이터 연산함수들을 제공하는데요. 자세한건 ex_math.cpp에서 다루고, 여기서는 sumsq_data() 함수 (sum of squares, 제곱합)를 예로 들어서 blob의 연산 메커니즘을 설명하도록 하겠습니다.

먼저 sumsq_data()함수가 무얼 하는 함수인지 아래 코드를 볼까요?

Dtype expected_sumsq = 0;
const Dtype* data = blob->cpu_data();
for (int i = 0; i < blob->count(); ++i) {
   expected_sumsq += data[i] * data[i];
}
cout<<endl;
cout<<"expected sumsq of blob: "<<expected_sumsq<<endl;
tStart = clock();
cout<<"sumsq of blob on cpu: "<<blob->sumsq_data()<<endl;
tEnd = clock();
COMPTIME("sumsq of blob on cpu");

수동으로 제곱합을 계산한 값과 sumsq_data() 함수를 이용한 값을 비교해 보았습니다. 같아야겠지요? 나중에 실행결과를 직접 확인해 보시구요. 여기서 눈여겨 봐야할 것은 특별한것(?) 없이 blob->sumsq_data()함수를 한번 콜 하면 이 연산은 CPU 위에서 이루어진다는 점입니다. 계산 시간은 제 컴퓨터로 0.001164초 걸리는군요.

그럼 이번에는 이 연산을 GPU 위에서 해 볼까요? 그 아래 코드를 한번 봅시다.

tStart = clock();
blob->gpu_data(); // memcopy host to device (to_gpu() in syncedmem.cpp)
tEnd = clock();
COMPTIME("cpu->gpu time");

tStart = clock();
cout<<"sumsq of blob on gpu: "<<blob->sumsq_data()<<endl;
tEnd = clock();
COMPTIME("sumsq on gpu time");

계산시간을 측정하는 부분을 빼면 코드가 딱 두 줄 인데요. 첫 번째로 blob->gpu_data(); 이 명령은 host (CPU)에 있는 메모리를 GPU 메모리로 카피하라는 명령입니다. 옆에 주석에 적어놓은것 처럼, syncedmem.cpp에서 이 명령을 수행하는데요. 한번 따라가 볼까요?

const void* SyncedMemory::gpu_data() {
#ifndef CPU_ONLY
to_gpu();
return (const void*)gpu_ptr_;
#else
NO_GPU;
#endif
}

gpu_data()를 콜 하면, 여기에 to_gpu()' 명령이 있고, 이 명령 안에는caffe_gpu_memset(size_, 0, gpu_ptr_);caffe_gpu_memcpy(size_, cpu_ptr_, gpu_ptr_);`가 있습니다. CUDA가 익숙하신 분들은 GPU에 메모리를 할당하고 host->device 데이터 복사하는 작업임을 알 수 있습니다.

자, 그런데 문제는 데이터양이 크다면, 이 복사하는 작업도 만만치 않게 계산시간이 걸리다는 점이죠. 얼마나 걸리는지 cpu->gpu time 결과를 한번 볼까요?

제 컴퓨터에서는 0.004674초가 걸렸습니다. 앗, 이거슨 위에서 CPU위에서 제곱합을 연산한 시간보다 자그마치 4배나 더 걸리는 시간이군요? 그럼 그 다음에 GPU위에서 제곱합을 연산한 시간은 0.000229초이네요. 확실히 연산 시간만 보면 GPU에서 연산한 것이 CPU보다는 5배정도 빠르지만 데이터 복사 시간에 훨씬 많은 리소스가 필요하군요.

CPU/GPU 동기화

눈치채셨겠지만, blob연산의 특징은 CPU에서든 GPU에서든 명령어는 같다는 점입니다. 위에서 sumsq_data()는 동일하게 사용되는데, 단지 gpu_data()를 하고 나면 CPU데이터가 GPU로 복사가 되고, GPU에 데이터가 있으면 연산 명령은 자동으로 GPU에서 수행하게 되는 것이죠.

그럼, 실수로 GPU에 데이터가 있는데도 gpu_data()를 여러번 호출하면 이 무거운 명령을 여러번 수행하게 될까요? blob은 똑똑하게도(당연하게도) GPU 데이터와 CPU 데이터의 갱신여부를 동기화해서 최신의 데이터를 사용하게 됩니다. 즉, GPU의 데이터가 최신이라면 gpu_data()를 호출해도, CPU에서 데이터를 가져오지 않고, 반대로 CPU 데이터가 최신이라면 복사를 수행하게 됩니다. 반대로 cpu_data()를 호출해도 마찬가지구요. 아래 코드로 한번 확인해 볼까요?

cout<<endl;
tStart = clock();
blob->gpu_data();   // no data copy since both have up-to-date contents.
tEnd = clock();
COMPTIME("cpu->gpu time");

자, 위에 이어서 첫 번째 호출된 gpu_data()는 아무 동작을 하지 않습니다. 즉 계산 시간을 확인해보면 1e-06초 의미 없죠.

그럼 이번에는 GPU위의 데이터에 변화를 주어 봅시다.

// gpu data manipulation
const Dtype kDataScaleFactor = 2;
blob->scale_data(kDataScaleFactor); // change data on gpu

scale_data()는 blob안에 각 데이터에다가 곱하기를 하는 것이겠죠? 그러면 현재 GPU상의 데이터는 두 배씩 커졌는데, CPU상의 데이터는 예전 그대로 입니다. 여기서 GPU데이터를 CPU로 복사해봅시다.

tStart = clock();
blob->cpu_data();   // memcopy device to host (to_cpu() in syncedmem.cpp)
tEnd = clock();
COMPTIME("gpu->cpu time");

cpu_data() 역시 현재 GPU 메모리 값이 CPU 메모리보다 최신이면 복사를 수행하고, 아니면 수행하지 않습니다. 여기서는 복사가 되었겠죠? 계산 시간을 살펴보면 0.001744초로 아까 CPU에서 GPU로 복사할 때 보다는 2.5배정도 빠르군요. (이유는 묻지 마세요. 그냥 사실 확인만 ㅎㅎ)

그럼 마지막으로, 여기서 sumsq_data()를 호출하면 어디서 계산이 될까요? 현재 CPU로 데이터를 복사한 상태로 GPU값과 CPU값이 동일하니, 이 작업은 GPU에서 이루어집니다. 계산 시간을 확인해보면 알 수 있죠.

tStart = clock();
cout<<"sumsq of blob on gpu: "<<blob->sumsq_data()<<endl;   // this is done on gpu
tEnd = clock();
COMPTIME("sumsq on gpu time");

결과는 0.000169초로 아까 CPU에서 계산한 시간 0.001164과 GPU에서 계산한 시간 0.000229 중에 후자와 비슷하다는 것을 확인할 수 있습니다.

Blob 연산

이번에는 앞에서 알아본 sumsq_data() 제곱합 외에 다른 blob에서 가능한 연산을 알아보도록 하겠습니다. 여기서는 mutable_gpu_data() 라는 개념이 나오니 잘 봐주세요.

ex_math.cpp 파일을 한번 열어보세요. 이번에는 blob을 생성한 후에 다른 방법으로 난수값을 할당해 보겠습니다.

Blob<Dtype>* blob_in = new Blob<Dtype>(20, 30, 40, 50);
Blob<Dtype>* blob_out = new Blob<Dtype>(20, 30, 40, 50);
int n = blob_in->count();

// random number generation
caffe_gpu_rng_uniform<Dtype>(n, -3, 3, blob_in->mutable_gpu_data());

여기서 caffe_gpu_rng_uniform<type>() 함수가 사용되었습니다. 그리고 특이한 점은 사용된 데이터의 주소값으로 blob_in->mutable_gpu_data()을 사용했는데요. CPU든 GPU든 blob의 데이터를 접근할 때, 데이터를 쓸 수 있게 하려면 앞에 mutable을 붙여야 합니다. 안붙이면 읽기전용이죠. 아마 Caffe에서 주소를 통한 데이터 직접 접근은 허용하되, 혹시 잘못 쓸까봐 보호 차원에서 쓰는 데이터는 더 귀찮게 해놓은것 같습니다.

뒤에 보실 연산함수들도 모두 caffe_gpu_로 시작하는데요. 이 함수들은 util 안에 있는 math_functions.hpp안에 정의되어 있습니다. 함수 구현은 CPU 연산은 math_functions.cpp, GPU 연산은 math_functions.cu 여기서 자세히 보실 수 있습니다.

여기서는 GPU 연산만 해보도록 하겠습니다. 첫 번째로 절대갑합을 구해보죠.

Dtype asum;
caffe_gpu_asum<Dtype>(n, blob_in->gpu_data(), &asum);
cout<<"asum: "<<asum<<endl;

결과는 1.79959e+06으로 뭐 의미가 있는 수 같네요. ㅎㅎ

그리고 아래처럼 다른 함수들도 한번 테스트 해보세요.

caffe_gpu_sign<Dtype>(n, blob_in->gpu_data(), blob_out->mutable_gpu_data());
caffe_gpu_sgnbit<Dtype>(n, blob_in->gpu_data(), blob_out->mutable_gpu_data());
caffe_gpu_abs<Dtype>(n, blob_in->gpu_data(), blob_out->mutable_gpu_data());
caffe_gpu_scale<Dtype>(n, 10, blob_in->gpu_data(), blob_out->mutable_gpu_data());

여기서는 blob_in은 읽기, blob_out은 쓰기 전용입니다.

다음 예제에서는 난수말고, 의미있는 데이터셋에서 데이터를 읽어서 blob에 저장하고, 이 값들을 접근해서 시각화까지 한번 해보도록 하겠습니다.

Tags: caffe