부트캠프 파이널 프로젝트를 시작하며
4월 11일부터 나는 프로그래머스 인공지능 부트캠프의 마무리를 장식하는 파이널 프로젝트를 준비하고 있다. 사진을 초해상화(super-resolution)할 수 있는 GAN 모델인 ESRGAN을 웹 서비스 형태로 서빙하려고 한다. 파이널 프로젝트에 사용할 모델을 GAN 모델 중에서 ESRGAN을 선택한 이유는 단순히 GAN 모델이 창작물을 만들어내는 신기한 경험보다 일상생활에서 실용적으로 느낄 수 있는 서비스를 만들어보고 싶었기 때문이다. 다른 창작물들을 생성해내는 GAN 모델들의 경우 신기하긴 했지만 결과물이 썩 좋아보이지도 않았고 실제로 필요하다는 느낌이 오지 않았다. 하지만 ESRGAN의 경우 성능이 눈으로 바로 보이기도 하고, 일상생활에서도 인터넷에서 압축되어 화질이 떨어진 이미지들을 개선할 때 유용하게 사용할 수 있을 것 같았다. 그 동시에 내가 관심있는 GAN 프레임워크의 모델도 다뤄볼 수 있으니 이득이라고 생각했다.
처음엔 뮤직비디오 같이 저화질로 공개된 인터넷 영상들을 초해상화하는 서비스를 만들고 싶었지만, 동영상의 경우에는 inference에 걸리는 시간이 매우 오래걸리기 때문에 웹 서비스를 하는 것은 어려울 것 같다는 조언을 들어 이미지 쪽으로 방향을 바꿨다. 내가 구상한 구체적인 웹 서비스는 이미지를 업로드하면 모델의 연산을 거쳐 초해상화 된 이미지를 다운받을 수 있는 형태이다. 인터넷에 이미지 초해상화 관련 웹사이트들을 뒤져보니 역시나 비슷한 웹페이지들이 좀 있었다. 하지만 프로젝트를 할 때는 아이디어가 겹치는 다른 서비스들을 생각하지말고 우선 만들어보는 것이 중요하다고 어디선가 들었다(...) 실제로 계속 열어두고 서비스를 할 것도 아니니 괜찮겠지... 라는 생각으로 프로젝트 마무리까지 열심히 달려보기로 했다.
내가 생각한 프로젝트에 사용할 ESRGAN 모델은 사전학습되어 깃허브에 공개된 모델을 파인튜닝하여 사용하기로 했다. 아마추어 프로젝트이지만 그래도 성능이 좋은 모델을 만들고 싶었다. 성능이 안좋은 모델이 나오면 뭔가를 만들었다는 성취감보다는 아쉬운 마음이 남을 것 같았다. 사전학습 모델에 FFHQ 데이터 셋을 추가로 파인튜닝하여 인물 사진에 특화된 super-resolution 모델을 만들 계획이다. 고화질 인물 사진 데이터셋은 찾기 힘들 줄 알았는데 유명한 데이터셋이 있어서 의외로 금방 찾았다.
모델을 개선해야 해!
하지만 문제는 여기서 시작되었다. 명색이 파이널 프로젝트인데 그냥 사전 학습 모델을 파인튜닝하는 것만으로 괜찮을까? 라는 생각을 했다. 대체 내 능력으로 이 사전학습 모델을 그대로 사용하지 않고 개선하는 방법에는 무엇이 있는지 찾는 일은 너무 어려웠다. 고민고민하다가 모델의 inference time을 개선해보는 것은 어떨까하는 생각이 들었다. 사전학습 모델의 공개된 데모를 돌려보니 400 * 700 픽셀정도의 이미지를 넣으면 초해상화를 하는데 20초 정도의 시간이 걸렸다. 이 시간을 줄일 수 있다면 더 큰 이미지를 초해상화하는 데도 유용하게 쓰일 수 있겠다고 생각했다. 초해상화라는 것이 원래 적은 픽셀을 가진 저화질 이미지를 고화질 이미지로 바꾸는 것이긴 하지만 인터넷에서는 픽셀 수가 많아도 화질이 좋다고 할 수 없는 이미지들이 많다. 여러 웹사이트를 거치며 이미지가 압축되었거나 확대되었기 때문이다.
나는 솔직히 모델의 속도 개선에 대해서는 아직 시도해본 적이 없어 먼저 모델의 inference time을 줄이는 방법들을 찾아봐야 했다. 사전학습 모델을 사용할 것이므로 모델의 설계 단계에서 적용할 수 있는 방법들은 제외했다. 열심히 인터넷을 뒤져보니 모델의 inference time을 개선하는 방법은 크게 두가지 정도 있는 것 같다. Quantization(양자화)와 pruning(가지치기?)이다.
Quantizaton(양자화)
Quantization은 모델의 가중치 행렬의 값들을 더 낮은 바이트의 데이터 타입으로 변경하는 것이다. 모델의 가중치 행렬 속 값들의 데이터 타입을 보면 보통 float32 데이터 타입을 가질 것이다. Quantizaiton을 해주게되면 이 데이터 타입을 float16(half)나 int8 데이터 타입으로 변경한다. 이 과정을 거치면 모델의 성능이 떨어질 가능성도 있으나 전체적으로 메모리 사용량이 줄어들면서 모델의 inference time이 개선되게 된다. pytorch의 경우에 이 Quantization을 api 형태로 제공한다. 이 api를 사용할 경우 dynamic quantization이라고 해서 모든 가중치의 데이터타입을 바꾸는 것이 아닌 일부 가중치의 데이터 타입만 int8로 바꾸어 연산할 수 있다. 하지만 문제가 pytorch의 api를 사용하면 GPU가 아니라 CPU로 밖에 연산하지 못한다는 것이다. 속도를 빠르게 할 생각으로 GPU가 장착된 AWS EC2 P2 인스턴스를 제공받았는데 GPU를 못쓴다면 아무 의미가 없지않은가! 하긴 모델의 inference time을 줄이려는 시도는 원래 속도가 빠르던 GPU보단 CPU 쪽에서 더 수요가 많을테니...

그래서 다른 방법으로 Quantization을 진행했다. 꼭 dynamic Quantization을 할 필요는 없다. pytorch 모델의 경우 model = model.half()
와 같이 메소드를 사용하면 모델의 가중치를 float16으로 바꿔줄 수 있다. 이 경우에는 CPU에서 연산은 불가능해지고 GPU로만 연산이 가능했다. 또한 input tensor의 데이터 타입도 float16으로 바꿔줘야 한다.
하지만 나는 최악의 결과인 "효과 없음"을 보고 말았다. 정확히는 float32 모델로 연산시 21초 정도가 걸렸다면 float16을 적용한 후에는 20.xx초가 걸렸다. 사실상 큰 효과가 없었던 것이다. 정확한 이유는 알 수 없으나 내 추측에는 GPU의 문제인게 아니었을까 싶다. EC2 인스턴스에 장착되어 있던 GPU는 K80이었는데 엔비디아 홈페이지를 뒤져보니 K80은 float16 데이터 타입의 연산을 지원하지 않았다. 그럼에도 불구하고 나는 0.xx초가 줄어드는 효과를 봤기에 어떻게 잘하면 되는게 아닐까 어떤 부분의 코드를 잘못쓴게 아닐까하고 계속 시도해봤지만 역시나 똑같은 결과였다.
Pruning(가지치기)
Pruning의 경우는 모델의 가중치의 일부 값을 0으로 바꿔주는 방법이다. 무작위로 가중치를 골라 바꿔줄 수도 있고 모델의 영향을 별로 주지 않는 가중치만 0으로 바꿔줄 수도 있다. pruning을 하고나면 모델의 용량과 연산시 메모리 사용량이 작아지게 된다. pytorch는 pruning도 api로 제공한다. torch.nn.utils.prune
모듈을 사용하면 되며, 여기에 자세한 공식 튜토리얼이 소개되어 있다.

환장하겠는건 pruning 조차 아무 효과가 없었다. 처음에는 pruning을 하면 모델의 성능이 많이 떨어지지 않을까 싶어서 소심하게 가중치 20% 정도를 pruning 해보았다. 그런데 inference time이 전혀 변화가 없었을 뿐더러 결과물에도 아무런 변화가 없어보였다. 나는 내가 너무 적게 pruning 했나 싶어 계속 pruning하는 가중치의 비율을 늘려보았지만 inference time은 변함없었고 결과물도 전혀 변화가 없었다. 시간이 변화가 없는건 그렇다쳐도 결과물까지 변화가 없는게 너무 이상해서 내가 어디에서 코드를 잘못친건가 계속 확인했는데 도저히 문제점을 찾을 수가 없었다. 서치해보니 pruning이 inference time에 별로 도움이 되지 않을수도 있다는 글을 보긴했지만 결과물까지 변함이 없는건 납득할 수 없었다. 아무리 찾아봐도 답이 나오지 않아 일단은 이 부분은 포기하기로 했다.
TensorRT
Quantization과 Pruning이 모델의 가중치를 변형시키는 방법이라고 하면, GPU 연산을 효율화하여 inference time을 줄이는 방법도 있다. 엔비디아의 경우 tensorRT 엔진이라고 하는 엔비디아 GPU 환경에서 사용할 수 있는 GPU 연산 최적화 SDK를 제공한다. tensorRT는 Quantization을 하거나 레이어의 연산같은 부분을 최적화시켜서 inference time을 줄일 수 있는데 pytorch와 tensorflow 등 다양한 프레임워크의 딥러닝 모델에서 사용할 수 있다. tensorRT 엔진은 엔비디아 도커르 사용해서 매우 쉽게 사용할 수 있었다. 다만 도커 이미지의 용량이 꽤 나갔다. tensorRT 엔진의 베이스 이미지 자체만 4GB 정도였다. 여기에 여러 필요한 라이브러리들을 설치하다보면 용량이 너무 쉽게 커졌다. 그래서 EC2 인스턴스의 스토리지를 20GB에서 50GB로 업그레이드하기도 했다.
그러나 이번에도 또 실패했다! 처음에는 tensorRT도 호환되는 GPU가 따로있어서 K80에선 돌려보지도 못하는 건가 싶었는데 구버전 tensorRT의 경우 K80도 지원한다고 쓰여있는 것을 발견해서 구버전 tensorRT로 도커 이미지를 만들고 torch2rt 라이브러리를 이용해서 pytorch 모델을 tensorRT 모델로 변경하려 했는데 이번에는 CUDA out of memory 에러와 마주하고 말았다;; K80은 이렇게 나약하단 말인가? 에러 난 부분을 보니 모델의 레이어를 전부 풀어서 input을 한번 통과시키는 과정이 있는 모양이었는데 그 부분에서 메모리가 터진 것으로 보였다. torch2rt의 깃허브 이슈에서 GPU 메모리가 터지면 torch.cuda.empty_cache()를 스크립트에 추가해서 GPU 캐시를 삭제해보라길래 vim으로 열어서 라이브러리 스크립트를 수정까지 해봤지만 GPU 사용량은 그대로였고 역시나 에러가 났다. 모델이 큰 것도 맞긴했는데 패러미터가 1천만개 정도로 BERT나 GPT 급으로 큰 모델도 아니었다. 대체 안에서 어떤 연산이 이뤄지길래 10GB 정도의 GPU 메모리를 전부 쓴단 말인가? 세번째로 시도한 방법을 실패하자 이쯤되니 좌절감이 장난아니었다.
다른 방법으로는 엔비디아의 trition inference server를 사용하는 방법도 있었다. 이 역시 엔비디아 도커를 통해 이용할 수 있었지만 GPU 호환이 맞지 않아 가볍게 컷됐다. triton inference server가 요구하는 GPU는 엔비디아 기준으로 6점 이상의 GPU인데 K80은 3점이다. 억지로 실행시킨다고 해도 오류를 뿜을 것이다.
그 결말
이렇게 모델의 inference time을 줄이려는 내 노력은 실패로 돌아갔다. 이 방법들 말고는 input으로 주어지는 이미지의 크기를 제한해서 모델의 inference time을 줄이는 방법밖에는 떠오르지 않았다. 이 부분도 실험을 해보긴 해야겠지만 나름 큰 맘 먹고 다양한 방법들을 적용해서 모델의 inference time을 줄여보고 싶었는데 모두 실패해서 노력이 수포로 돌아가니 마음이 안좋았다. 최근 본 회사 면접때, 면전에 이제까지 한 프로젝트들이 시키는 것만 잘한 것 같다는 소리까지 들어서 조금 우울했는데 마음 다 잡고 이번에는 내가 스스로 생각해서 프로젝트에 기여해보리라는 생각에 다시 잘 해보려는데 이런 결과가 발생하다니. 사실 이 글을 남기는 것도 한 일들을 기록해서 내가 노력했다는 것을 알아주기라도 했으면 하는 마음에 적게 되었다.
나는 결론적으로는 모델의 inference time을 줄여보려는 시도를 멈추고 다른 모델 개선방향을 찾아보고 있는 중이다. mlops 프레임워크를 도입해서 모델의 학습과 배포과정을 최적화해보면 어떨까 생각하고 있지만 내가 아직 사용해보지 않은 툴들이 많아서 잘 할 수 있을지는 모르겠다. 그래도 이왕 시작한 프로젝트 마무리 짓지 않으면 의미가 없다. 어떻게든 되게 하리라. 프로젝트가 마무리 될 쯤에는 꼭 내 노력으로 이뤄냈다는 성취감을 느껴보고 싶다.
ps. 여담이지만 머신러닝/딥러닝을 배우기로 결심한 이후로 나는 컴퓨터 앞에서 기도를 하는 일이 많아진 것 같다. 제발 한 번에 되게 해주세요, 제발 제 노력이 효과있게 해주세요 하고. 하지만 번번히 꼭 한 번씩은 실패한다. 딥러닝 모델에 blackbox가 있듯이 모델을 개발하는 과정에는 왠지 모르게 뭔가 노력으로 커버 되지 않는 영역이 있는 것 같다. 다른 사람들은 대체 이런 부분들을 어떻게 헤쳐나가는지 궁금하다 흑흑...
'Project' 카테고리의 다른 글
SRGAN 논문 코드로 구현해보기 (0) | 2022.03.10 |
---|---|
KoGPT2를 활용해 K-유튜브 제목을 생성해보자 - 2편 파인튜닝 (0) | 2022.03.06 |
KoGPT2를 활용해 K-유튜브 제목을 생성해보자 - 1편 EDA (0) | 2022.02.24 |