데이터가 누락되는 종류

  • 무작위로 누락: 변수의 종류와 상관없는 누락

  • 다른 변수와 관련있는 누락: 해당 변수가 아닌 다른 변수의 영향을 받아서 누락된 경우 (ex. 설문조사의 뒷면이 있는 지 모르고 응답하지 않아서 누락된 경우)

  • 해당 변수와 관련있는 누락: 누락된 해당 변수에 관련해서 누락된 경우(어떤 설문조사에서 일부 질문에 정치적인 성향 등의 이유로 채우지 않았을 경우)

  • 첫번째, 두번째는 제거하는 것이 좋지만, 세번째 경우 단순 제거하면 모델이 편향 될 가능성이 있다.

누락값 해결책

  • 삭제: 데이터 버리기(row) or 변수 버리기(col)

    • 단점: 모델이 편향될 수 있으며, 중요한 변수가 버려질 수 있음
  • 대표값(mean, median, mode)으로 대체

    • 단점: 대체한 변수가 다른 변수와 상관관계가 있을 때, 상관관계를 고려하지 않음, 데이터 전체의 분산이 줄어들게 됨(평균), 데이터 전체에 편향이 생김(최빈값)
  • 다중 대체법(multiple imputation, MICE)

    • 과정

      1. 대체하고자 하는 누락값을 비우고, 다른 누락값은 대표값으로 채운다.
      2. 다른 변수들을 X로하고, 대체하고자 하는 누락값이 속하는 변수를 Y로하는 선형회귀모델을 이용해 누락값을 채운다.
      3. 다른 누락값에 대해서도 똑같이 적용한다.
      4. 대체 전 값과 대체 후의 값의 차이가 0이 되도록 이 과정을 반복한다.
  • KNN 사용, 변수를 통해 가까운 데이터 K개를 골라 이 데이터들의 평균을 사용함

    • 단점: 오래걸림, outlier에 민감함, 변수의 scale을 조절해줘야 함

출처: https://subinium.github.io/missing-data-handling/

'Data Science' 카테고리의 다른 글

[ML] 의사결정나무(Decision Tree) 뿌시기  (0) 2022.05.21
[논문 리뷰] ESRGAN  (0) 2022.03.25
[논문 리뷰] Attention is all you Need (transformer)  (0) 2022.03.04
[논문 리뷰] SRGAN  (0) 2022.02.18
빈도주의 vs 베이지안  (0) 2022.02.06

위키백과의 정의에 따르면 자료형(data type)이란 "컴퓨터 과학과 프로그래밍 언어에서 실수치, 정수, 불린 자료형 따위의 여러 종류를 데이터로 식별하는 분류로서, 더 나아가 해당 자료형에 대한 가능한 값, 해당 자료형에서 수행을 마칠 수 있는 명령들, 데이터의 의미, 해당 자료형의 값을 저장하는 방식을 결정하는 것"이다.

파이썬을 조금이라도 배운 사람이라면 적어도 자료형이 무엇인지는 익숙할 것이다. int, float, list와 같은 예시들이 떠오를 수도 있을 것이다. 하지만 이런 자료형들은 어떻게 만들어진 것이고 무엇을 위해 존재하는 것일까? 그저 유용하게 사용하기만 했지 나는 이 자료형들의 존재 이유에 관해서는 별로 생각해본 적이 없었다. 이 자료형들의 의미에 대해서 알기위해선 과거 컴퓨터 언어들은 어떤 자료형을 가지고 있었는지 알아보면 좋을 것 같다. 예를 들면 여러 컴퓨터 언어 중 가장 할아버지라고 볼 수 있는 C언어다.

출처: 코딩도장

 구글에서 C언어의 자료형에 대해서 서치해보면 파이썬과는 사뭇 다른 분위기를 풍긴다. 파이썬은 흔히 수치형으로 퉁치고 넘어가는 자료형이 short, int, long, float, double... 등 바이트 수에 따라 굉장히 세분화하여 분류하고 있다.  char의 경우 파이썬의 문자열과는 달리 보통 글자 "하나"(character)를 뜻한다.  C언어의 경우 파이썬과는 달리 정적 언어이므로 변수를 선언할 때마다 int a같이 특정 자료형을 지정해주어야 하며 위의 자료형들은 변수를 선언할 때 사용하는 "기본 자료형"들이다.

 물론 위의 자료형들은 기본 자료형이고 이 기본 자료형들이 응용되어 더 복잡한 자료형을 만들기도 한다. 하지만 내가 느낀 것은 다른 자료형들도 결국은 저 "숫자"로 귀결된다는 것이다. 문자도 결국 아스키 코드 규칙에 따라 변형된 "숫자"이며, 포인터는 자료가 담긴 메모리의 주소를 "숫자"로 표현한다. C언어에서 배열은 메모리의 연속적으로 저장된 자료 중 맨 앞 자료가 담긴 메모리의 "포인터"이므로 결국 숫자다.

 나는 이런 개념들이 상당히 복잡하다고 느꼈고 숫자를 극한으로 이용하면 이렇게 까지 되는구나라고 생각했다. 하지만 한편으로는 간결해보이기도 한다. 하드웨어에는 0과 1로 이루어진 비트들이 존재하고, 이를 이용해 숫자를 표현하고, 이 숫자를 아스키 코드로 해석하면 문자가 되고, 메모리의 위치를 표시하면 포인터가 된다. 그리고 이 자료들을 메모리에 인접하게 저장하면 배열이 된다. 파이썬에선 잘 느껴지지 않던 자료형들 간의 관계가 너무 잘 보인다.

 파이썬도 결국 런타임이 C언어로 작성된 프로그래밍 언어다. 파이썬은 C언어와는 매우 다르지만 인터프리터가 작동하고 있는 내부에서는 C언어를 이용해 해석되고 있을 것이다. 굳이 그냥도 사용할 수 있는 C언어를 이용해 파이썬이라는 새로운 언어를 만든 것은 분명한 이유가 있을 것이다. 파이썬의 철학에서 이 이유를 추측할 수 있다. 파이썬은 가독성과 단순성, 효율성을 극도로 추구하고 있다. C언어는 한 번 읽고 이해하기가 어려운 것이 사실이며 낮은 가독성은 개발의 효율성을 떨어트리고 커뮤니케이션을 어렵게 한다. 파이썬은 C언어의 이 단점을 해결하기 위해 등장했다고 볼 수 있다.

여기서 파이썬의 자료형을 어떻게 봐야 좋을지에 대한 해답이 나온다. 파이썬의 자료형은 결국엔 C언어의 자료형을 응용해 더 다양한 기능을 수행할 수 있도록 만들어진 것이다. 그렇다면 이 파이썬의 자료형이 C언어의 어떤 번거로움을 해결하기 위해 만들어진 것인지 알게 된다면 그 자료형의 존재 이유가 밝혀지게 되는 것이다. 파이썬의 각 자료형들을 C언어의 관점에서 본다면 어떤 느낌인지 살펴보겠다. 밑의 자료형들은 파이썬의 공식문서 Built-in Types에 등장하는 분류대로 분류한 것이다. 

수치형(numerics)

파이썬의 수치형 자료형은 integer, float, complex 등 수를 나타내는 자료형들을 포괄적으로 부른다. C언어는 수치 자료형을 shor, int, long, float, double등 사용되는 메모리의 바이트를 기준으로 세세하게 나눠놓은 반면 왜 파이썬에서는 수치형을 하나로 뭉뚱그려 묶어서 부르는 경우가 많은 것일까? 파이썬 공식문서에 따르면 파이썬의 integer는 기본적으로 무한한 숫자를 표현할 수 있으며 float는 C언어의 double로 표현된다고 한다. 파이썬은 C언어와 달리 동적 언어이기 때문에 메모리를 할당하고 자료를 집어넣는 C언어와 달리 넣는 자료에 따라 메모리가 자동으로 할당되는 성격이 있기 때문에 굳이 바이트에 따라 수치 자료형을 나누지 않는 것으로 보인다. 또한 수치형에서 파이썬과 C언어가 근본적으로 다른 점은, 파이썬에서는 숫자가 웬만하면 숫자의 용도로 사용된다는 것이다. C언어는 숫자가 문자, 포인터 등의 형태로도 존재하는 것과 달리 파이썬에서는 수치형 자료형의 숫자들이 말 그대로 "숫자"그 자체의 목적으로 존재한다. 즉, 파이썬의 수치형 자료형은 C언어와 달리 상당한 유연성을 가졌으면서 용도 면에서는 더 명확한 자료형이라고 할 수 있다.

시퀀스(Sequence)

파이썬의 시퀀스 자료형은 리스트, 튜플, 문자열, 바이트 등을 포함하는 자료형이다.(다만 문자열과 바이트는 리스트, 튜플과는 조금 다른점이 있다) C언어의 배열과 비슷해보이는데 C언어의 배열과 파이썬의 시퀀스 자료형은 굉장히 큰 괴리감이 느껴졌다. 파이썬이 C언어의 바이트별로 다양한 수치 자료형들을 하나로 통합했다면, 시퀀스 자료형은 C언어의 배열을 세분화, 특수화해서 만들었다는 느낌이 강하다.

파이썬의 시퀀스 자료형은 공통되는 한가지 특징이 있고, 그 특징이 C언어의 배열과의 큰 차이점이다. 그것은 C언어가 데이터를 배열로 저장하는 것과 달리 파이썬은 포인터를 배열로 저장한다는 것이다. 포인터라는 것은 앞에서도 언급했다시피 메모리의 위치를 나타내는 것이다. C언어의 배열은 데이터가 일렬로 저장된 메모리의 맨 앞을 가리키는 포인터이다. C언어가 배열을 다룰땐 포인터를 이용해 배열의 맨 앞부분으로 접근하고, 배열의 끝을 알려주는 메모리가 나올때까지 옆으로 읽어 나간다. 때문에 옆으로 계속 읽어 나가기 위해선 배열이 한가지 자료형만으로 이루어져야 해석할 수 있고, 어디까지 읽어야 하는지 정해줘야 하기에 배열의 길이도 미리 선언해줘야 한다. 반면에 파이썬은 데이터 자체가 아닌 데이터가 담긴 포인터를 배열로 저장하기에 "데이터"의 자료형이 같을 이유가 없다. 어차피 배열 안은 똑같은 포인터 자료형이고, 포인터가 가리키는 데이터의 자료형은 마음대로 바꿀 수 있기 때문이다. 또한 파이썬은 포인터의 배열을 저장할 메모리를 일정 값으로 알아서 선언해주므로 시퀀스의 길이를 직접 선언해 줄 필요가 없다.

그런데 여기서 리스트와 튜플의 차이점이 드러난다. 파이썬에는 객체를 mutable 객체와 immutable 객체로 나눌 수 있는데 mutable 객체는 '뮤턴트' 처럼 값을 바꾸는게 가능하고 immutable 객체는 값을 바꾸는 것이 불가능하다. 리스트는 mutable이고 튜플은 immutable은 이유는, 리스트는 포인터의 배열의 크기가 데이터의 크기에 따라 동적으로 변하는 반면, 튜플은 튜플 생성시의 크기로 고정되기 때문이다. 그래서 리스트는 내부의 데이터가 바뀌더라도 바뀐 크기에 따라 메모리를 알아서 할당하지만 튜플은 그렇지 못하기때문에 오류가 생기는 것이다. 튜플에 + 연산으로 데이터를 추가할 수 있기 때문에 의아하게 생각할 수도 있으나 그것은 튜플에 데이터를 더 붙이는 것이 아니라, 튜플의 데이터와 + 연산해준 데이터를 순서대로 포함한 새로운 튜플을 생성하는 것뿐이다.

C언어의 배열은 튜플과 달리 배열 안의 값을 바꾸는 것에 대해서는 제한이 없다. 어차피 배열 안의 모든 자료형이 동일하기 때문에 다른 값으로 바꾼다고 해서 메모리 사용량이 변하지 않으므로 메모리 크기를 추가로 할당할 필요도 없긴하다. 근데 왜 파이썬은 mutable/immutable 객체를 구분하고 있는 것일까? 대부분의 파이썬 학습서에서 이 부분에 대한 설명은 없이 단순히 리스트는 가변객체이고 튜플은 불변객체라는 차이점이 있다는 식으로 넘어가기 때문에 따로 찾아보는 수밖에 없었다. immutable 객체가 필요한 이유는 크게 두가지로, '스레드 안전(thread-safety)'과 '시간과 메모리의 절약'이다.

스레드 안전은 어느 함수나 변수가 여러 스레드에 의해 호출되더라도 실행에 문제가 없는 것을 말한다. mutable 객체의 경우 값이 변할 수 있기 때문에 어떤 스레드에서 값을 변화시키면 다른 스레드에서의 작업이 잘못될 수 있다. 반면 immutable 객체는 그럴 걱정이 없다. 또한 mutable 객체가 값을 변화시키기 위해 실제로 필요한 메모리보다 더 많은 메모리를 할당하고 있는것과 달리, immutable 객체는 필요한 메모리만 할당시킨다. 때문에 mutable 객체보다 메모리 사용량이 적고 속도도 더 빠르다.

문자열 역시 시퀀스 타입에 속한다. 다만 다른 시퀀스와는 달리 문자 자료형으로 원소들의 자료형이 고정되고, immutable 객체이다. C언어도 문자열을 char의 배열로 다루지만 파이썬 문자열의 편의성이 훨씬 크다. 개인적인 느낌을 말하자면 C언어의 문자열은 문자의 탈을 쓴 배열이라는 느낌이지만 파이썬의 문자열은 더 직관적으로 문자열이라고 느껴진다. C언어에서 문자열을 다루는 것은 매우 어려운 반면 파이썬에서는 쉬운 이유다. 또한 파이썬의 문자열은 문자열끼리의 연산도 제공한다.

매핑(mapping)

매핑 자료형에는 딕셔너리가 속한다. 파이썬의 딕셔너리는 key와 value 구조를 가져서 시퀀스 자료형이 인덱스로 데이터에 접근하는 것처럼 key를 이용해 value에 접근할 수 있다. 딕셔너리의 key가 주어지면 해시함수를 통해 key를 인덱스로 매핑하고, 그 인덱스를 이용해 value를 찾는 방식이다. 이런 자료구조를 해시테이블(hash table)이라고도 한다. 

파이썬의 딕셔너리가 특이한 점은 배열과 해시테이블의 특징을 모두 가졌다는 것이다. 3.6버전부터 파이썬의 딕셔너리는 일정한 순서를 가지게 되었다. 그로 인해 for loop에서 더 유용하게 사용할 수 있게 되었다. 이는 파이썬 딕셔너리 자료형의 내부구조에 indicis 를 저장한 배열이 있기 때문이다. 다만 추가적인 메모리를 요구하기 때문에 시퀀스 자료형보다 메모리 낭비가 크다는 것은 단점이다. 파이썬의 딕셔너리는 JSON 형식의 파일을 다루는 데도 매우 유용하다. JSON 형식 자체가 딕셔너리와 완벽히 매칭되기 때문이다.

C언어에서는 딕셔너리와 같은 해시테이블을 사용하려면 직접 구현해야 한다. 이렇게 다른 언어에서는 직접 구현해서 사용해야하는 자료구조를 기본 자료형으로 제공하고 있다는 것은 파이썬의 철학을 잘 보여주는 사례라고 할 수 있다.

집합(Set)

집합 자료형에는 set과 frozenset이 있다. 두 자료형의 차이는 set은 mutable이고 frozenset은 immutable이라는 점이다. 집합 자료형은 데이터의 탐색이 O(1)로 빠르고 데이터들의 유일성을 보장한다. 사실 집합 자료형은 C언어 뿐만 아니라 다른 프로그래밍 언어에서도 잘 볼 수 없는 자료형이다. C언어에서 집합을 구현하는 것은 번거로운 일이라고 하는데 이런 자료형을 파이썬에선 built-in으로 사용할 수 있다는 것이 매우 편리하다.

마치며

글을 쓰다보니 다른 자료형에 비해 시퀀스 자료형에서 글이 너무 길어졌다. 사실 내가 이 글을 쓰게 된 계기가 시퀀스 자료형에 있기 때문이다. 파이썬을 배웠다고 하면서도 나는 이제까지 자료형들이 왜 존재하는지에 대해서 궁금해본적이 없는데, 리스트와 튜플에 대해 생각하다가 "리스트도 튜플이 할 수 있는 작업들을 모두 할 수 있는데 튜플이 왜 필요하지?" 라는 생각이 문득 들게 된 것이다. 나는 평소에도 배열을 쓸 일이 있으면 리스트를 사용하지 튜플을 사용한 적은 거의 없었고 딕셔너리의 items나 함수의 리턴값 정도로나 봐왔기 때문이다. 튜플에 대해 생각하다보니 다른 자료형들에 대해서도 생각하게 되었다. 그래서 결국 이번 기회에 한번 제대로 조사해보자는 생각으로 이 글을 작성하게 된 것이다. 좀 더 C언어랑 잘 비교하면서 본질을 꿰뚫 수 있는 글이 되었으면 좋았을텐데 C언어에 대한 사전 지식이 부족해서 아쉽다. 하지만 이번 기회에 자료형에 대해 깊게 이해하게 되면서 좀 더 파이썬과 가까워진 느낌이다. 앞으로도 종종 내가 가볍게 넘겨버려서 놓쳐버린 인사이트들을 다시 깊게 배우면서 되찾아야겠다.

부트캠프 파이널 프로젝트를 시작하며

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 쪽에서 더 수요가 많을테니...

future work라는데 언제 GPU에서 사용할 수 있게 될까요?

그래서 다른 방법으로 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 모듈을 사용하면 되며, 여기에 자세한 공식 튜토리얼이 소개되어 있다.

대충 이런모습, 근데 0으로 바꿔도 어차피 똑같이 텐서에 담겨 연산되는데 정말 속도가 빨라질까 의심되긴 했다.

환장하겠는건 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가 있듯이 모델을 개발하는 과정에는 왠지 모르게 뭔가 노력으로 커버 되지 않는 영역이 있는 것 같다. 다른 사람들은 대체 이런 부분들을 어떻게 헤쳐나가는지 궁금하다 흑흑...

+ Recent posts