공부/AIFFEL

Going Deeper(CV)_DJ 20 : 행동 스티커 만들기

dong_dong_2 2021. 5. 7. 19:58

1. 데이터셋을 어디에서 구할까?
    - MPII 데이터셋 다운로드 받기


       1) 오늘은 MPII Human Pose Dataset을 사용해서 Human Pose Estimation task를 위한 모델을 훈련시켜 보겠다.
       2) (주의) 해당 파일은 12GB가 넘는 용량을 가지므로 다운로드할 때 수 시간이 소요될 수 있다.
       3) zip 파일을 풀어보면 확장자가 mat 파일이 나와서 열기 불편한 파일이 있어서 코드를 통해 json 파일로 변환했다.
       4) 마지막으로 오늘 실습할 코드의 파일(코드가 정리된 파일)이 있는 압축폴더를 다운 받고, 압축을 풀어서 디렉토리에 옮겼다.
2. 데이터 전처리하기
    - 필요한 패키지(loguru, ray)를 다운로드 한다.
    - 그리고 다운 받은 파일 중 tfrecords_mpii라는 파일의 코드를 살펴본다. (2 ~ 4까지 이 파일에 대해 설명한다)
    - json 파싱하기
       1) 이전 스텝에서 train과 validation의 json 파일을 다운로드 받았다.
       2) 이 파일들은 이미지에 담겨 있는 사람들의 pose keypoint 정보들을 가지고 있어서 Pose Estimation을 위한 label로 삼을 수 있다.
       3) 우선 json이 어떻게 구성되어 있는지 파악해 보기 위해 json 파일을 열어 샘플로 annotation 정보를 1개만 출력해봤다.
       4) 코드를 통해 실행했다.
       5) joints가 우리가 label로 사용할 keypoint의 label이다.
       6) 이미지 형상과 사람의 포즈에 따라 모든 label이 이미지에 나타나지 않기 때문에 joints_vis를 이용해서 실제로 사용할 수 있는 keypoint 인지 나타낸다.
       7) MPII의 경우 1 (visible) / 0 (non) 으로만 나누어지기 때문에 조금 더 쉽게 사용할 수 있다.
       8) COCO의 경우 2 / 1 / 0 으로 표현해서 occlusion 상황까지 label화 되어 있다.
       9) joints 순서는 아래와 같은 순서로 배치되어 저장해뒀다.
          (1) 0 : 오른쪽 발목
          (2) 1 : 오른쪽 무릎
          (3) 2 : 오른쪽 엉덩이
          (4) 3 : 왼쪽 엉덩이
          (5) 4 : 왼쪽 무릎
          (6) 5 : 왼쪽 발목
          (7) 6 : 골반
          (8) 7 : 가슴(흉부)
          (9) 8 : 목
          (10) 9 : 머리 위
          (11) 10 : 오른쪽 손목
          (12) 11 : 오른쪽 팔꿈치
          (13) 12 : 오른쪽 어깨
          (14) 13 : 왼쪽 어깨
          (15) 14 : 왼쪽 팔꿈치
          (16) 15 : 왼쪽 손목
      10) index 값은 우리가 언제든지 바꿔서 사용할 수 있다.
      11) 가장 어렵게 느껴지는 값은 scale과 center일 것이다.
          (1) 높이 = scale * 200px
          (2) center는 사람의 중심점
      12) 위와 같은데 왜 200px의 상수값으로 고정되어 있을까?
      13) 이는 정확한 근거는 없다. 단순한 매직넘버인 듯 하다. 검색을 해서 찾아보면 "사람의 키를 200px로 가정한다" 정도의 정보만 있다.
      14) https://github.com/bearpaw/pytorch-pose/issues/31을 
      15) 적절한 근거가 없어서 어렵게 느껴지는 부분이지만 편의상 사용한다 정도로만 이해하고 넘어갔다.
      16) 특이한 점은 scale 정보가 coco dataset에는 scale 값 또는 2차원으로 주어져 bbox scale이 나오지만 mpii는 높이만 나온다는 점이다.
3. tfrecord 파일 만들기
    - 이전까지 keras의 imagedatagenerator를 이용해서 주로 학습데이터를 읽었다.
    - 하지만 실제 프로젝트에서는 튜토리얼 데이터셋보다 훨씬 큰 크기의 데이터를 다뤄야 한다.
    - 학습을 많이 해볼 수록 학습속도에 관심을 가지게 된다.
    - 일반적으로 학습 과정에서 gpu의 연산 속도보다 HDD I/O가 느리기 때문에 병목 현상이 발생하고 대단위 프로젝트 실험에서 효율성이 떨어지는 것을 볼 수 있다.
    - 따라서 학습 데이터를 어떻게 빠르게 읽는가 에 대한 고민을 반드시 수행해야 더 많은 실험을 할 수 있다.
    - 학습 속도를 향상시키기 위해서 데이터 관점에서 고려해야 하는 단계는 어떤 단계인가? 속도 향상을 위한 처리 방법은 어떤 것이 있을까?
       1) data read(또는 prefetch) 또는 데이터 변환 단계에서 고려해야 한다.
       2) 속도 향상을 위해서 gpu 학습과 병렬적으로 수행되도록 prefetch를 적용해야 한다.
       3) 수행 방법은 tf.data의 map함수를 이용하고 cache에 저장해두는 방법을 사용하면 된다.
    - 내용이 어렵지만 tensorflow에서는 위 변환을 자동화해주는 도구를 제공하고 있다.
    - 바로 데이터셋을 tfrecord 형태로 표현하는 것이다. tfrecord는 binary record sequence를 저장하기 위한 형식이다.
    - 내부적으로 protocol buffer라는 것을 이용하는데 옆에 참고 링크를 써놓겠다. (https://developers.google.com/protocol-buffers/?hl=ko)
    - protobuf는 크로스플랫폼에서 사용할 수 있는 직렬화 데이터 라이브러리라고 생각하면 된다.
    - 데이터셋 크기가 크기 때문에 빠른 학습을 위해서 이 정보를 tfrecord 파일로 변환해 본다.
4. Ray
    - Ray는 파이썬을 위한 간단한 분산 어플리케이션 api이다.
    - 쉽게 multiprocessing을 생각하면 되지만 차이점은 있다.
    - multiprocessing과 ray의 사용상 차이점은 무엇일까?
       1) multiprocessing은 병렬화를 위해 추상적 구조를 새로 설계해야 하지만 ray는 사용하던 코드에서 거의 수정 없이 병렬화 할 수 있는 장점이 있다.
    - 이제 파일에 대한 설명은 끝이 났으니 파일을 실행시켜 tfrecord들을 만든다.
    - 코드에 대한 자세한 설명은 블로그에서는 생략했다.
5. data label로 만들기
    - tfrecords 파일을 읽고 전처리를 할 수 있는 dataloader를 만든다.
    - 해당 스텝은 preprocess라는 파일에 대한 설명이다.
    - 이 코드에서 Preprocessor 클래스 코드를 만들고 그 안에 __call__() 메소드가 있는데 내부에서 진해오디는 주요 과정을 정리하면 아래와 같다.
       1) tfrecord 파일이기 때문에 병렬로 읽는 것은 tf가 지원해준다. parse_tfexample()에 구현되어 있고 이 함수를 통해 tensor로 이루어진 dictionary 형태의 features를 얻을 수 있다.
       2) 즉 image는 features["image/encoded"] 형태로 사용할 수 있고 tfrecord를 저장할 때 jpeg encoding된 값을 넣었으므로 tensorflow의 io의 decode_jpeg()로 decoding하여 tensor 형태의 이미지를 얻는다.
       3) crop_roi() 메소드를 이용해 해당 이미지를 학습하기 편하도록 몇 가지 트릭을 적용한다.
       4) make_heatmaps() 메소드를 이용해 label을 heatmap으로 나타낸다.
    - def parse_tfexample
       1) tfrecord 파일 형식을 우리가 저장한 data type feature에 맞게 parsing한다.
       2) tf가 자동으로 parsing 해주는 점은 아주 편하지만 feature description을 정확하게 알고 있어야 하는 단점이 있다.
       3) 즉, tfrecord에서 사용할 key 값들과 data type을 모르면 tfrecord 파일을 사용하기 굉장히 어렵다. 이는 serializa 되어있기 때문이다.
    - def crop_roi
       1) 우리가 알고 있는 것은 joints의 위치, center의 좌표, body height 값이다.
       2) 균일하게 학습하기 위해 body width를 적절히 정하는 것도 중요하다.
       3) 여기서는 높이 정보와 keypoint 위치를 이용해서 정사각형 박스를 사용하는 것을 기본으로 디자인 했다.
       4) 이와 관련해서는 여러 방법이 있을 수 있겠지만 배우는 단계에서 더 중요하게 봐야할 부분은 우리가 임의로 조정한 crop box가 이미지 바깥으로 나가지 않는지 예외 처리를 잘 해줘야 한다는 점이다.
       5) (x, y) 좌표로 되어있는 keypoint를 heatmap으로 변경시킨다.


    - def make_heatmaps
       1) 16개의 점을 generate_2d_gaussian()함수를 이용해서 64x64의 map에 표현한다.
       2) 2D 가우스 분포 수식을 적용해서 만들 수 있다.


    - def generate_2d_gaussian
       1) sigma 값이 1이고 window size 7 인 필터를 이용해서 만든다.
       2) 이런 특수 함수들은 공개되어 있는 구현이 많기 때문에 참고해서 사용하는 것을 권장한다.
    - 이 파일은 실행하는 파일이 아닌 train할 때 사용할 파일이다.
6. 모델을 학습해보자
    - Hourglass 모델 만들기
    - 이번에는 hourglass104라는 파일에 대한 설명이다.
       1) 이전에 배운 hourglass 모델이 잘 기억나는가? 아래와 같은 모습이였다.


       2) 직육면체 박스는 residual block 이었는데 이를 하나씩 구현했다.
       3) resnet 구현과 비슷하기 때문에 조금은 쉽게 느껴질 수 있다.
       4) residual block에는 2가지 타입이 있다. 어떤 것일까?
          (1) 3x3-3x3 (basic block)
          (2) 1x1-3x3-1x1 (bottleneck block)
       5) hourglass 모델을 보고 잘 생각해보면 마치 양파처럼 가장 바깥의 layer를 제거하면 똑같은 구조가 나타나는 것을 알 수 있다.
       6) 이 점을 이용해서 간단하게 모델을 표현할 수 있다.
    - Hourglass module
       1) 바로 재귀함수를 이용하는 것이다.
       2) 바깥부터 5개의 양파껍칠(층)을 만들고 싶으면 order를 이용해서 5, 4, 3, 2, 1이 될 때까지 HourglassModule을 반복하면 order가 1이 되면 BottleneckBlock으로 대체해주면 아주 간결하게 만들 수 있다.
       3) 이 hourglass 모듈을 여러 층으로 쌓은 것이 stacked hourglass network인데, 모델이 깊어지는 만큼 학습이 어려워 intermediate loss (auxilary loss)를 추가해야 하는 것을 논문에서 언급했다.


       4) 따라서 stacked되는 hourglass 층 사이사이에 LinearLayer를 삽입하고 중간 loss를 계산해준다.
       5) 지금까지 만든 hourglass를 여러층으로 쌓으면 stacked hourglass가 된다.


    - Stacked Hourglass
       1) 위에서 만든 것을 정리하면 해당 파일의 코드가 된다.
       2) 이 파일도 trian을 위한 파일이다.
7. 학습 엔진 만들기
    - 학습 코드인 train 파일을 설명한다.
    - 위 2개의 파일을 여기서 import 해서 사용될 것이다.
    - 먼저 model로 만들어 둔 hourglass와 데이터 전처리용 preprocess를 import 한다.
    - 그리고 gpu memory grouth 옵션을 조정하는 코드를 만든다.
    - Trainer class
       1) 이 코드에서 정의한 학습에 사용할 옵션들 중 몇가지를 살펴본다.
          (1) loss : MSE(heatmap을 pixel 단위 MSE로 계산) -> 실제 계산은 약간 다르다. compute_loss()에서 새롭게 구현한다.
          (2) strategy : 분산학습용 이다. 사용 가능한 GPU가 1개뿐이라면 사용하지 않는다.
          (3) optimizer : Adam
       2) learning rate는 decay step에 따라 1 / 10씩 작아지도록 설정했다.
       3) loss function
          (1) 이론대로라면 loss_object를 사용해서 MSE로 구현하는 것이 맞지만 사실 동일 weight MSE는 잘 수렴이 되지 않는다.
          (2) 예측해야하는 positive (joint들) 의 비율이 negative (배경) 에 비해 상당히 적은 비율로 등장하기 때문이다.
          (3) 이 때문에 실제 구현에서는 약간의 테크닉을 추가해줄 필요가 있다
          (4) label이 배경이 아닌 경우 (heatmap 값이 0보다 큰 경우) 에 추가적인 weight를 주면 보다 나아지는 경향을 볼 수 있었다.
          (5) weight가 82인 이유는 heatmap 전체 크기인 64x64에서 gaussian point 등장 비율이 7x7 패치이기 때문에 64 / 7 = 9.1 -> 9x9로 계산해 봤다.
          (6) tf의 gradienttape을 이용해 loss를 업데이트 하면 된다.
    - tf의 dataset만들기
       1) trainer의 모델 학습 부분은 제작이 완료되었고 tfrecord 파일을 dataset으로 만든다.
       2) preprocessor 구현에서 tfrecord 규칙을 모두 정의했기 때문에 단순히 tfrecord list를 읽어와서 tf의 data API에 입력한 후, preprocessor를 map으로 적용하면 된다.
    - train 함수 구현
       1) 이제 학습을 해본다.
       2) 1epoch 당 30분 정도 소요되니 적절한 epochs를 설정하는 것이 좋다.
       3) 학습이 완료되면 weight 파일까지 만들어 진 것을 확인할 수 있다.
8. 둠칫둠칫 댄스타임
    - 예측 엔진 만들기
    - test파일을 실행시키면 결과 이미지를 확인할 수 있다.
9. 프로젝트 : 모델 바꿔보기
    - 이전 시간에 simplebaseline를 배웠다.
    - 이번에 이 모델을 만들어서 학습을 시켜보고 2개의 모델의 성능을 비교해보자.