요약


CenterNet은 기존의 Two stage detection방법(=한 객체에 대해서 여러 bounding box을 찾고, bounding box이 겹치는 영역을 줄이는 방법)이 비효율적인 것에 Motivaiton이 있습니다. 따라서, CenterNet은 효율적은 예측을 위해서 한 객체에는 하나의 Anchor(=center point, keypoint)만 있다고 가정하고, 이를 예측하는 문제로 바꿉니다. 일단 Anchor 을 찾고난 다음, 문제가 3D size, pose estimation이면 Anchor로부터 사이즈가 얼마인지 등을 추가로 예측하는 문제를 추가로 도입합니다. 모델 구조는 1) Heatmap: 각 객체의  Centerpoint 예측하는 출력층, 2) offset: Heatmap을 만드는 단계에서 Anchor와 원래사이즈에서의 Anchor을 보정하기위한 출력층, 3) size 예측: 각 객체의 Centerpoint으로부터, 객체의 사이즈를 예측하는 출력층으로 구성되어있습니다. [1] 

 

 

Motivation (introduction): Two stage detection방법에 제한이 있어,  One stage dection으로


현재 컴퓨터 비전에서 쓰는 대부분의 개체인식(Object detection)은 각 개체에 bounding box로 표현하는 것입니다. 보통 하나의 bounding box로 표현하기보다는 잠재적으로 될만한 bounding box을 다 찾습니다. 이 과정을 거치고나면, 하나의 객체에는 여러 bounding box가 생깁니다. 따라서, 최적의 bounding box(개체와 가장 근접하도록 작은 사이즈의 bounding box만 남김)을 만드는 작업을 합니다. 이러한 방법은 "Two stage dectecor"라고 합니다. 계산을 여러 번 하게되는 작업들을 아래와 같이 진행합니다 (Figure 3).

  • recompute image feature for each potential box: 잠재적으로 개체가 될만한 박스들을 다 계산하여 그립니다.
  • then, classify those feature: 이 박스들이 background일지 object일지 분류를합니다.
  • Post-processing (Non maximal supression): 최적의 박스만 남기는 작업을 진행합니다. bounding box의 IoU값을 계산하여 한 개체에 겹치는 부분이 있으면 해당 내용만 씁니다. 특히 이러한 후처리는 미분도, 학습도 어려워서 end-to-ent 모델에 담기가 어렵습니다. 

그래서 나온것이 "One stage detection"입니다. 말 그대로 위의 단계를 하지않고, 하나의 단계로만 진행합니다. 이 단계는 가능한 하나의 bounding box을 만드는 것입니다. 이 하나의 bounding box의 중심을 anchor라고도 부릅니다. 일단 각 개체에 대해서 Anchor을 찾고나면, bounding box을 어디까지 그려야할지(size), 3D extent, orientation, post등은 중심점으로 부터 다 예측하겠다는 것입니다. 각 개체에 Anchor 만 찾고나면, 나머지는 부수적으로 예측하여 얻어낼 수 있다는 것을 상정하고 만든 모델입니다.

 

 

 

 

 

CenterNet은 어떤 로직? (method) : Heatmap, Offeset, Sizes 을 예측하기위해서 end-to-end로모델을 결합


CenterNet은 단순히 CNN으로만 이뤄져 있습니다. 구성은 인코더-디코더로 되어있는데, 디코더가 3개로 되어있습니다. 우선, 문제의 정의를 아래와 같이합니다.

  • 입력 이미지: $ I \in \mathbb{R}^{W\times H\times 3}$. 가로의 크기가 W, 세로의 크기가 H인 이미지가 입력으로 주어진다고 가정합니다. 
  • 결과물 (heatmap): $\hat{Y} \in [0, 1]^{W/R \times H/R \times C}$. R은 stride, C은 키포인트 타입(클레스 타입)을 의미합니다. 즉 이미지가 STRIDE 떄문에 좀 작아지더라고 각 개체가 C클레스에 중심점이 될만한지 heamap을 찍는 것을 의미합니다. 예를 들어, C은 human pose 라면 관절 개수인 17개을 씁니다. stride인 R은 기본적으로 4을 씁니다. 그렇게되면 이미지가 작아지는(=downsampling)됩니다.  만일 예측치 $\hat{Y}_{x,y,c}=1$라면 keypoint을 의미합니다(=예측된 x좌표, y좌표에서 c클레스일 확률이 1이라면 그 부분이 중심점일 것이다) 반대로 0이라면 배경을 의미합니다(Figure 1).

Figure 1. Keypoint estimation 과 heatmap의 관계. STRIDE $R$ 때문에 downsampling된 heatmap의 예시. C가 80이라면 heatmap은 약 80개가 만들어지고, 각 C(클레스)가 될만한 heamtpa을 다 찍는다. 그중에 확률이 높은 클레스가 해당클레스일 것이다. 예를들어 dog class가 80번에 있다면, C=80인 heatmap에 $\hat{y}=1$인 지점이 2개일 것이다. 그리고 그 주변부에는 1에 가까운 수치들이 많을 것이다.

 

 

디코더의 역할을 아래와 같이 기술합니다.

Figure. CenterNet 아키텍처, 반환값 및 Ground truth, 및 loss의 관계

1. Heatmap 예측을 위한 decoder: CNN은 heatmap이라는 것을만드는데, heatmap에서의 중앙부가 개체의 중심부가 되도록 예측합니다. 즉 개체가 3개라면 가장 높은 점수가 3개(=본문 내 피크)가 있어야하는 것입니다. 그리고 각 이미지의 피크를 이용하여 bounding box의 가로, 세로도 예측합니다. 그리고 keypoint을 예측하는 과정에서 Stacked hourglass, up-convolutional residula network(ResNet0, deep layer aggregation (DLA)을 dencoder-decoder로 묶어서 사용합니다.

 

C 클레스가 될만한, heatmap의 정답은 [x, y]좌표로 2차원의 데이터로 표현할 수 있을 것입니다. 원래 이미지 사이즈를 heatmap에 맞추어야합니다. 이 과정을 위해서 원좌표 [x.y]을 아래와 같이 R로 나누어 floor만 씁니다. 예를들어 중심점의 좌표가 512, 512을 4로 나눴다면 딱 나눠떨어져서 128, 128이되겠지만, 중심점의 좌표가 총 512,512이미지의 중심점의 좌표가 111,111, R=4이라면 [27, 27] (27.75에서 0.75버림)으로 만든다는 것입니다. 

$\tilde{p}\ = \left \lfloor \frac{p}{R} \right \rfloor$ 

이렇게 각 클레스 C에 맞춰 라벨을 heatmap에 맞춰 만듭니다. $Y \in [0, 1]^{\frac{W}{R}\times\frac{H}{R}\times{C}}$

그리고, 히트맵에 가우시안 커널을 적용하는데, 이는 keypoint가 단일의 점 [x,y]인 것에 비해서 예측치점이 $\hat{x}, \hat{y}$이기 때문에, 딱 하나의 점으로는 표현하자면 너무 예측이 어렵기에, ground truth keypoint주변으로 예측치가 어느정도 맞으면 가만할 수 있게끔 뿌려주는 역할을합니다. 가우시안 커널의 공식은 다음과 같습니다. 아래의 가우시안 커널에서 $\sigma^{2}_{p}$은 오브젝트 사이즈에 따라서 달라질 수 있다고합니다.

$Y_{x,y,z}=exp(-\frac{(x-\tilde{p_{x}})^{2} + (y-\tilde{p_{y}})^{2}}{2\sigma^{2}_{p}})$

 

Figure 2. 가우시안 커널을적용한 ground truth heatmap 예시

정리하면, 예측할 heatmap은 STRIDE R이 적용된 리사이즈된 heatmap($\hat{Y} \in [0, 1]^{W/R \times H/R \times C}$), 예측치도 같은 사이즈의 가우시안 커널이 적용된 이미지($Y \in [0, 1]^{\frac{W}{R}\times\frac{H}{R}\times{C}}$)입니다. 그럼 로스만 적용하면됩니다.

로스는 아래의 (식 1)과 같이 작성합니다. Focal loss을 적용합니다. 흔히 focal loss은 클레스불균형이 심할때 사용합니다. 예를 들어, 보험사기자, 신용사기자처럼 다수의 정상적인 사용자가 있고, 가끔 발생할 수 있는 사기의심자를 한 둘을 맞춰야할 때, 이 사기의심자를 틀린 경우에 많은 패널티를 주기위해서 제곱항을주는 방식입니다 [2].

 

 

2. Discretization error 보정: Local offset

- 위의 예시와 같이 keypoint가 STRIDE (R)로 나눴을 때 나눠떨어지지 않는 경우, 약간의 오차가 발생했습니다(예, 111,111, R=4이라면 [27, 27] (27.75에서 0.75버림)). 이 에러를 보정하기위해서  Local offset이라는 개념을 만듭니다. 오프셋의 정의는 아래와 같습니다.

$\hat{O} \in \mathbb{R}^{\frac{W}{R}\times \frac{H}{R}\times 2}$

O은 예측해야할 좌표를 의미하며, 원본이미지에서 STRIDE을 주어 리사이즈된(=downsampling)이미지에서 단 2개의  채널의 값만 가집니다. 그리고 원래 중심좌표 p와 최대한 떨어지지 않게 만들기위해서 아래와 같이 L1 loss을 주어 학습합니다.

3. 오브젝트의 사이즈 예측: size

오브젝트의 사이즈는 2개의좌표만 알면됩니다. 좌상단, 우하단만 알면, bounding box을 그릴 수 있기 때문입니다 (Figure 3.). 따라서, 좌표2개를 아래와 같이 정의합니다. k은 카테고리를 의미합니다. 

그리고 더 중요한 것은 keypoint 을 예측하고, 오브젝트의 사이즈를 구해야하기 때문에, kepoint와 오브젝트의 관계식을 얻을 수 있습니다.

$p_{k} = (\frac{x_{1}^{k} + x_{1}^{k}}{2}, \frac{y_{1}^{k} + y_{1}^{k}}{2})$

그리고, 오브젝트 사이즈는 각 x좌표, y좌표의차이만 구하면됩니다. 

$s_{k} = (x_{2}^{k} - x_{1}^{k}, y_{2}^{k} - y_{1}^{k})$

각 오브젝트가 어떤 클레스가 될지에 따라서 k가 달라질 수 있다는 것입니다. 하지만, 이 예측치를 계산하는데는 연산이 오래걸려서 그냥 단일의 개체의 사이즈로만 판단하도록 변경합니다. 그래서 사이즈을 의미하는 값의 차원이 다음과 같이 바뀝니다. $\hat{S} \in \mathbb{R}^{\frac{W}{R}\times\frac{H}{R}\times2}$. 

Figure 3. https://arxiv.org/pdf/1904.07850.pdf

 

그리고 아래와 같은 L1 로스를 줍니다. 실제 bounding box의 x,y, 가 중심점일때의 가로,세로의 길이를 예측하는 문제로 정의하는 것입니다.

그리고, 이를 joint learning하기위해서 모든 로스를 다음과 같이 합칩니다.

 

 

Anchor로부터 Bounding box예측하기


bounding box예측은 heatmap과 offset, size prediciton을 다 이용하면됩니다.

1. 일단 Anchor을 찾습니다. Anchor가 될만한것은 Heatmap에서 비교적 높은 값들을 찾으면 되거나, 일단 Anchor포인트로부터 8개의 좌표값들을 비교했을때 anchor보다 크거나 같은 경우 그부분을 중심점이라고 둡니다. 그리고 기본적으로 CenterNet은 100개의 피크값을 뽑아냅니다. 최대 오브젝트가 100개라고 보는것이죠. 몇 개인지까지는 예측을 못하니...

2. 그리고 각 예측치를 아래와 같이 구합니다. $\delta\hat{x}_{i}, \delta\hat{y}_{i} $은 offset prediction의 결과입니다. 예측한 heatmap의 좌표가 stride R때문에 실제 이미지에서 약간 이동될 수 있으니, 이를 보정하해줍니다. $\hat{w}_{i}, \hat{h}_{i}$은  사이즈 예측치입니다. 사이즈 예측을 위해서 중심점으로부터 좌측하단, 우측상단을 구하는 공식입니다. 총 4개의 좌표가 나옵니다. 이렇게 진행하면 딱히 non-maxima suppresion 또는 후처리가 필요가 없습니다.  그렇기에 one-stage detection입니다.

 

 

CenterNet 성능(Result)


Table 1은 backbone 모델을 어떤것을 쓰냐에따라서, 성능, 계산시간을 보여줍니다. AP(average precision)은 hourglass-104가 제일 성능이 좋았으나, 추론시간은 좀 오래걸린다. 초당 detection, 추론성능은 ResNet-18이 가장 빠릅니다. augmentation , flpi, multi-scale등을 추가로해볼 수 있는데 각각에 대한 성능표입니다.

 

Table 2: coco dataset에서 대부분 알려진 모델들에서의 성능입니다. 위의 5개의 모델은 two-stage, 아래 모델들은 one-stage입니다. two-stage중에서는 MaskRCNN이 빠르내요. AP성능이 가장좋은것은 TridentNet입니다. 반면 one-stage에서는 Hourglass을 쓴 CenterNet이내요.

 

 

정리


엄청 월등한 성능의 예측력 또는 엄청난 계산효율은 둘다 보여준 것은 아니지만, one-stage 모델중에서는 준수한 성능과 빠른 계산량을 보여주는 모델이라고 할 수 있을 것 같습니다. key point estimation을 3가지 방식으로 loss을 주어 최적화하는 모델이어서 꽤 좋은 모델링인 것 같습니다.

 

Reference:

[1] https://arxiv.org/pdf/1904.07850.pdf

[2] T.-Y. Lin, P. Goyal, R. Girshick, K. He, and P. Dollar. ´ Focal loss for dense object detection. ICCV, 2017.

반응형

요약


임상시험은 대부분 무작위 선정(Randomization)으로 시험군(case), 대조군(Control)을 나눈다. 이 시험군과 대조군에 중재를 할 때, 시험군 또는 대조군에 속해있는 대상자들이 모종의 이유로 중재를 못받게 되는 상황이 된다(중도 탈락, 중재 전 사망, 치료 불이행). 이 때, 어쩔수없는 이유로 중재를 못받은 사람을 분석에 포함해서 의학적 성과(outcome)을 측정해야하냐 말아야하나 고민되는 시점이 생긴다. IIT은 모종의 이유가 있다 하더라도, 일단 무작위 배정되었으면, 분석에 포함하는 것을 일컫는다(“once randomized, always analyzed”). 반대의 개념으로는 "Per-Protocol"이라고 하며, 중재를 못받은 샘플을 분석에 포함하지 않는 것을 의미한다.

 

 

Reference

[1] https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5654877/

반응형

딥러닝을 이용한 서비스를 배포할 목적이 있으면, 운영체제 등에서 독립적으로 S/W가 돌아갈 수 있게끔 배포하는 것이 중요하다. 이 때 주로쓰이는 것이 컨테이너 기술인데, 컨테이너 기술을 이용해서, 딥러닝의 GPU가속을 사용해보자.

 

단계는 아래와 같다


  1. NVIDA driver (그레픽 드라이버) 와 docker은 설치
  2. CUDA 이미지 풀하기
  3. GPU 지원 컨테이너 만들기
    1. 방법 1: Native GPU support
    2. 방법 2: Nivida container Runtime을 이용하기
    3. 방법 3: Nivida GPus 을 위한 도커엔진 유틸리티 이용하기
  4. 확인: GPU 지원 컨테이너 내 접속 후 cuda확인

 

0. NVIDIA driver 확인


이미 설치된 경우, 다음의 코드로 CUDA Version을 확인한다.아래와 같이 CUDA Version이 11.7임을 확인할 수 있다. 

$nvidia-smi

 

 

2. 도커허브에서 CUDA 이미지 가져오기


docker hub에 cuda을 검색하면, 아래의 그림과 같이 여러 이미지들이 검색되어 나온다.

그 중에, nvidia/cuda을 설치해본다. (링크: https://hub.docker.com/r/nvidia/cuda)

 

nvidia/cuda 리포지토리에 들어가면 Tags가 나오는데, CUDA 버전에 맞는 tag을 찾아본다.

 

아래와 같이 CUDA 버전에 맞는 이미지를 찾는다.

nvidia-smi 명령어에서 cuda 버전이 11.7이었으므로 11.7을 검색해서 runtime, base, devel, 버전 및 원하는 컨테이너 OS환경에 맞춰 설치한다.

 

필자는 nvidia-11.7.1용 런타임-ubunut22.04을 pull했다.

 

제대로 잘 pulling되었는지 아래와 같이 확인해본다.

 

 

GPU 지원 컨테이너 사용: 방법1: Native GPU 지원


아래와 같이 docker 및 docker환경에서 GPU을 돌리기위한 데몬(nvidia-container-toolkit)을 설치한다

// docker, nvidia-conatiner-toolkit 설치
$ sudo apt-get install -y docker nvidia-container-toolkit

 

모든그레픽카드을 사용할 수 있도록 GPU세팅을 하는 것은 아래와 같다.

docker run --gpus all [이미지명:테그명]

 

필자는 background option으로 (-d), ssh로 접속해서 인터렉션할것이기 떄문에(-it)아래와 같이 옵션을 주어 컨테이너를 실행한다.

컨테이너명이 "2c2e80a61f240a689d1d6d13a94c886fdab9508fc6226e1889ac5cbb623a5528"로 반환되어 컨테이너가 실행중임을 알려준다.

 

컨테이너가 잘 생성되었는지, docker ps명령어로 확인해본다. 33초전에 실행됨을 확인할 수 있다.

이 컨테이너로 직접 들어가보자

 

대화창이 나온다.

 

nvidia-smi명령어로 host graphic card을 사용할수 있는지 확인해본다.

 

반응형

0. PUSH할 원격저장소를 만든다. docker hub (https://hub.docker.com/) 에서 로그인 후, 원격저장소를 생성한다. github저장소처럼 "ID/원격저장소명"으로 주소가 만들어진다.

 

 

1. 도커 로그인

$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: [YourID]
Password: ******

Login Succeeded

 

 

2. Docker hub에 올릴 이미지를 찾는다. 여기서 필자는 REPOSITOY에 simple-nlp-app, TAG은 v1을 올릴 것이다. 그렇지만, 도커허브에 올릴 때는 ID/repository명  = docker image ls에서의 REPOSITORY 명이 동일해야한다. 따라서, 한번 변경이 필요하다. 

$ docker image ls
REPOSITORY                                                TAG                                                                          IMAGE ID       CREATED         SIZE  
simple-nlp-app                                            v1                                                                           7108c8dbe574   6 minutes ago   2.73GB
simple-nlp-app                                            v0                                                                           c82b9d4786fe   9 minutes ago   2.73GB
simple-nlp-app                                            latest                                                                       798c7cf60156   2 days ago      2.72GB
nginx                                                     latest                                                                       ac8efec875ce   6 days ago      142MB 
alpine/git                                                latest                                                                       22d84a66cda4   3 weeks ago     43.6MB
hubproxy.docker.internal:5000/docker/desktop-kubernetes   kubernetes-v1.25.2-cni-v1.1.1-critools-v1.24.2-cri-dockerd-v0.2.5-1-debian   09d7e1dbc2c4   2 months ago    363MB 
k8s.gcr.io/kube-apiserver                                 v1.25.2                                                                      97801f839490   2 months ago    128MB 
k8s.gcr.io/kube-controller-manager                        v1.25.2                                                                      dbfceb93c69b   2 months ago    117MB 
k8s.gcr.io/kube-scheduler                                 v1.25.2                                                                      ca0ea1ee3cfd   2 months ago    50.6MB
k8s.gcr.io/kube-proxy                                     v1.25.2                                                                      1c7d8c51823b   2 months ago    61.7MB
k8s.gcr.io/pause                                          3.8                                                                          4873874c08ef   5 months ago    711kB 
k8s.gcr.io/etcd                                           3.5.4-0                                                                      a8a176a5d5d6   6 months ago    300MB 
k8s.gcr.io/coredns                                        v1.9.3                                                                       5185b96f0bec   6 months ago    48.8MB
docker/desktop-vpnkit-controller                          v2.0                                                                         8c2c38aa676e   19 months ago   21MB  
docker/desktop-storage-provisioner                        v2.0                                                                         99f89471f470   19 months ago   41.9MB
nginx                                                     1.14.2                                                                       295c7be07902   3 years ago     109MB 
dattarajrao/simple-app                                    latest                                                                       3f4a466b587c   4 years ago     132M

 

3. 두 가지 방법이 있는데, 첫 번째는 docker image tag을 이용하는 방법.

  • docker image tag SOURCE_IMAGE[:TAG] TARGET[:TAG]  . 이 명령어는 원본 이미지에서 타겟이이지로 리포지토리 명을 다시 테그를 달 수 있게한다.
  • docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] : 이 명령어는 실행중인 컨테이너를 다시 이미지화하는 방법이다.

아래의 예시는 docker image tag을 이용한 방법이다. tag을 이용할 때, 미리 생성한 원격저장소의 이름과 동일하게 해준다. 그렇지 않으면 아래와 같이 에러가 뜬다.

$ docker push 4pymgalion/simple-nlp-app:v1
The push refers to repository [docker.io/4pymgalion/simple-nlp-app]
An image does not exist locally with the tag

 

따라서, 아래와 같이 원격저장소의 ID/저장소명과 같이 도커 이미지를 테그를 다시 달아준다. 그러면 추가로 "4pygmalion/simple-nlp-app"라는 리포지토리 이름을 가진 이미지가 생성된다.

$ docker tag simple-nlp-app:v1 4pygmalion/simple-nlp-app:latest
$ docker images
REPOSITORY                                                TAG                                                                          IMAGE ID       CREATED          SIZE
4pygmalion/simple-nlp-app                                 latest                                                                       7108c8dbe574   14 minutes ago   2.73GB
simple-nlp-app                                            v1                                                                           7108c8dbe574   14 minutes ago   2.73GB
nginx                                                     latest                                                                       ac8efec875ce   6 days ago       142MB 
alpine/git                                                latest                                                                       22d84a66cda4   3 weeks ago      43.6MB
hubproxy.docker.internal:5000/docker/desktop-kubernetes   kubernetes-v1.25.2-cni-v1.1.1-critools-v1.24.2-cri-dockerd-v0.2.5-1-debian   09d7e1dbc2c4   2 months ago     363MB 
k8s.gcr.io/kube-apiserver                                 v1.25.2                                                                      97801f839490   2 months ago     128MB 
k8s.gcr.io/kube-scheduler                                 v1.25.2                                                                      ca0ea1ee3cfd   2 months ago     50.6MB

 

4. 푸시: Docker push 도커이미지/리포지토리명

$ docker push 4pygmalion/simple-nlp-app:latest
The push refers to repository [docker.io/4pygmalion/simple-nlp-app]
e56c84262ba7: Pushing [>                                                  ]  35.33kB/2.22MB
cec010fb56f8: Pushing [>                                                  ]  544.8kB/2.262GB
5af9244c08c5: Pushing [==================================================>]  5.632kB
fac360d91a98: Preparing
c43e4ddc5992: Preparing
e5896e4ce6c3: Waiting
7c40600bc52d: Waiting
d326469892d9: Waiting
b5ebffba54d3: Waiting

 

반응형

 

요약


쿠버네티스를 이용하면,파드를 통해 클라이언트들이 어플리케이션을 호출할 수 있다. 그러나, 클라이언트 입장에서는 각각 파드의 주소가 존재하는하고, 클라이언트는 어느 파드로 갈지 명시하지 않았는데도, 쿠버네티스가 알아서 각각의 파드가 처리하게 만들어준다. 즉, 네트워킹이 쿠버네티스에 의해서, 알아서 처리되는데, 이 네트워크 서비스를 제공할 수 있는 자원이 "서비스(Service)"이다. 즉, 파드로 실행중인 앱을 네트워크서비스로 노출하게 해주는 자원이다.

 

 

서비스를 이용하기위해 각 파드, 노드가 만족해야할 사항[1]


  • 각 파드는 노드상의 모든 파드와 통신할 수 있어야함
  • 노드상의 에이전트(데몬, kubelet)은 해당 노드의 모든 파드와 통신할 수 있어야함.

 

 

어플리케이션을 네트워크 서비스로 외부에 노출하기


아래와 같이, 디플로이먼트(deployment)와 서비스(Service)를 명시한 yaml파일을 각각 만든다. 

// nginx-svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx
# run-my-nginx.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80

 

그리고, 아래와 같이 클러스터 내부에서는  서비스가 가능하고, 각 클러스터의 IP을 확인할 수 있다.

$ kubectl get services
NAME              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
kubernetes        ClusterIP   10.96.0.1        <none>        443/TCP        28h
my-nginx          ClusterIP   10.108.171.235   <none>        80/TCP         26h

해당 클러스터의 내부

 

 

Trouble shooting: Cluster IP에 접속이 안될 때 -> Node Port로 외부 클라이언트가 접속할 수 있도록 변경 [2]


서비스하는 클러스터의 IP는 가상의 IP이기 때문에, 클러스터 내부에서 내부로만 서비스할 수 있다. 즉 위의 예시에서처럼 클러스터 IP가 10.108.171.235면, 이 클러스터로 들어가야만 "10.108.171.235:80"의 서비스를 요청하고 받을 수 있는 것이다. 이것을 해결하기, "Cluster IP"을 "Node Port"로 변경해주어야 한다.

Node Port은 외부 클라이언트가 노드로 접속하기 위한 포트를 의미한다. 이 노드 포트을 변경하려면 아래와 같이 kubectl expose을 이용한다.

$ kubectl expose deployment hello-world --type=LoadBalancer --name=example-service

 

expose을 하면 TYPE이 NodePort인 서비스가 하나 생성이된다.  example-serivce라는 이름을 가진 서비스의 TYPE이 NodePort인 것을 잘보자. 이 클러스터의 가상 IP은 10.101.151.252이다. 이것은 여전히 가상IP이기 때문에 서비스를 요청할 수 없다. 다만, 이 노드의 원래주소(localhost, 또는 privateIP, publicIP)로 들어갈때, 30384포트로 요청을하면 알아서 80번으로 바꿔준다. 즉 포트포워딩으로 30384을 80으로 바꿔서 10.101.151.252로 요청해볼 수 있다.

$ kubectl get services
NAME              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
example-service   NodePort    10.101.151.252   <none>        80:30384/TCP   26h
kubernetes        ClusterIP   10.96.0.1        <none>        443/TCP        28h

$ curl localhost:30384
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

 

 

[1] https://kubernetes.io/ko/docs/concepts/services-networking/

 

서비스, 로드밸런싱, 네트워킹

쿠버네티스의 네트워킹에 대한 개념과 리소스에 대해 설명한다.

kubernetes.io

[2] https://stackoverflow.com/questions/49297226/kubernetes-cluster-ip-service-not-working-as-expected

반응형

본 포스팅은 naver d2 이활석님의 발표자로를 글과 이해가 쉽도록 만든 Figure들로 재구성했음을 알립니다.

 

요약


  1. MSE로 최적화하는 것과 MLE로 최적화 하는 것은 동치이다. 
  2. sigmoid을 출력값으로 하는 경우 MSE로 최적화하는 것보다 CE로 하는 경우, 학습이 더 빨리 된다. 이는 activation function(sigmoid)의 도함수를 한 번 계산하지 않아도 되기 때문이다.

1. Backpropgation (역전파) 과정에 대한 가정(Assumption)


역전파알고리즘을 이용하기 위해서는 2가지 가정이 일단 필요하다 [1].

  1. Average $E = 1/n \sum_{x}E_{x}$: $n$개의 training 데이터에 해당하는 각각의 $x$에 대한 손실함수의 합의 평균은 손실함수 전체와 같다. 이는 각 훈련데이터를 각각 구한것의 그레디언트를 전체 오차함수에 대해서 일반화을 위하기 때문이다.
  2. 신경망의 출력값($\hat{y}$)에 대해서 손실함수를 표현할 수 있어야 한다는 것이다.

 

직관적으로 생각해보면 아래와 같다.

첫 번째로, MSE로 종류의 $E$(error)을 설정했다고 하자. 그리고, 이 오차함수는 예측값($y'$)과 실측값($y$)의 이를 구하게 된다. 그러면 다음과 같이 표기할 수 있다. 가령, $E(y, y')= (y-y')^{2}$가 라고 하자. 이는 일단 두번째 조건을 만족한다. 왜냐하면 $y'$은 신경망의 두번째 출력값으로 표현되었기 떄문이다. 그리고, 이러한 $y'$을 출력하기위한 입력값 $x$가 있을 것이다. 이 $x$가 $n$개가 있다고하면 각각의 $n$개의 오차의 합은 전체 $E$가 될 수 있다는 것이다. 즉, 다음과 같은 식으로 표현할 수 있다. $E=1/n \sum_{x} (y-y')^{2}$. 이렇게되면 1번 가정도 만족한다. 

 

위의 가정을 만족할 수 있으면, 이제 손실함수를 최소화시키는 최적의 파라미터($\theta$)을 찾으면된다. 보통 우리는 아래와 같이 표현한다.

$\theta^{*} = argmin_{\theta \in \Theta} L(f_{\theta}(x), y)$

위 식을 친절하게 해석해보면 다음과 같다.

  1. $\theta^{*}$: 우리가 원하는 최적의 파라미터를 의미한다. 
  2. $L(f_{\theta}(x), y)$: $\theta$가 어떤 것이 지정되냐에 따르는 손실함수($y$와 예측치 $f_{\theta}(x)$의 차이)를 의미한다.
  3. $argmin_{\theta \in \Theta} L$ : $\theta$가 될수 있는 모든 수 중에 $L$을 가장 작게만드는 $\theta$을 의미한다.

 

 

2. Gradient descent (경사하강법): 경사하강법으로 최적의 파라미터를 찾아간다.


위의 $\theta^{*} = argmin_{\theta \in \Theta} L(f_{\theta}(x), y)$식의 최적의 해를 찾는 방법으로 경사하강법(Gradient descent)가 이용된다. 이는 Iteration으로, 같은 로직을 여러번 반복하는 것을 의미한다. 식이 회귀방정식이라면 close form solution으로 직접 전개해서 풀면되지만, 활성화 함수랑 이것저것 엮여있는 DNN은 open form solution이어서 직접 추정해서 구해야한다.

 

경사하강법으로 최적의 파라미터($\theta$)을 구할 때, 고려사항과 각각의 방법은 아래의 표처럼 정리할 수 있다.

고려사항 방법
어느정도만큼 파라미터를 업데이트 할 것인가?
(= $\theta -> \theta + \Delta\theta$)
새로운 $\theta + \Delta\theta$로 손실을 측정할 때가 더 작을 때
(= $L(\theta + \Delta\theta) < L(\theta)$)
언제 멈출 것인가? 더 이상 업데이트가 변화하지 않을 때
$L(\theta + \Delta\theta) < L(\theta)$
어떻게 업데이트할 만큼($\Delta\theta$)을 찾을 것인가? $\Delta\theta = -\mu \nabla\theta$ where $\mu > 0 $

 

그림1. 이미지 출처: https://builtin.com/data-science/gradient-descent

 

위의 전략대로 아래와 같이, 일단 새로운 파라미터에 대해서 손실함수를 구하고자한다. 헷갈리지 말아야할 것은 $x$에 대해서 구하는게 아니라, 최적의 $\theta$을 찾는 것이다. 위의 그림과 같이 x1, x2축에 해당하는 w, b에따라서 손실함수의 크기 $J(w, b)$가 달라지는 것이다. 즉, $\theta$가 우리의 목표이다.

전체 데이터 포인트 X가 있으면, 아래와 같이 풀이가 가능하다.

  • $L(\theta_k, X) = \sum_{i}{L(\theta_{k}, x_{i}})$: 각 데이터포인트($x_{i}$)의 합은 전체와 같다.
  • $\nabla L(\theta_k, X) = \sum_{i}{\nabla L(\theta_{k}, x_{i}})$: 위의 식을 양변에 $\theta$에 대해서 미분한다. 
  • $\nabla L(\theta_k, X) \triangleq \sum_{i}{\nabla L(\theta_{k}, x_{i}})/ N$: 최적화만 구하면되니까, 이 미분값이 N으로 나눠도 된다. 
  • $\nabla L(\theta_k, X) \triangleq \sum_{j}{\nabla L(\theta_{k}, x_{j}})/M, ~ where ~M< N$: 전체 훈련데이터에 해당하는 데이터포인트를 다넣어서 N개의 연산을 해도되지만,너무 연산이 많이드니, 적당히 작은 M으로 나눠서 해를 찾을 수 있다. 이 때 랜덤으로 M개의 데이터 j개를 뽑는다.

즉, 결국에 X개 계산한다음에 업데이트하고싶지만, 계산이 많이 드니 M개만큼 쪼개서 업데이해볼 수 있다는 것이다.수식으로는 $\nabla L(\theta_k, X)$ 만큼 움직이는 것 대신에 $\sum_{j}{\nabla L(\theta_{k}, x_{j}})/M$ 만큼 움직여서 업데이트할 수 있다는 것이다.

그리고, 아래와 같이 다음(k+1)의 파라미터틀 업데이트한다. 우리는 이 k를 iteration, M개를 넣는 작업을 step이라고한다. tensorflow에는 iteration은 epoch, M개의 미니배치로 계산하는 것은 step이라는 표현으로 사용한다.

$\theta_{k+1} = \theta_{k} - \mu \nabla L(\theta_{k},X)$

 

 

 

3. Backpropgation에 따라서 각 레이어의 파라미터를 업데이트


위에서 구한 것과 같이 손실함수(loss function)은 $L(\theta_{k},X)$라고 했다. 헷갈리지 말아야할 것이, 파라미터($\theta$)에 해당하는 것은 각각의 레이어의 $w, b$을 의미한다. 2개의 표현식을 통틀어 크게는 $\theta$라고 표현한다. 

파라미터 업데이트는 $\theta_{k+1} = \theta_{k}-\mu\nabla L(\theta_{k}, X)$ 가 목표다. 이를 각각 w와 b에 대해서 풀어쓰면 다음과 같다.

  • $w_{k+1}^{l} = w_{k}^{l} - \mu\nabla L_{w_{k}^l}(\theta_{k}, X)$ : $k$번째 업데이트 할 때, $l$번째 레이어의 $w$에 대해서 미분이 필요하다
  • $b_{k+1}^{l} = w_{k}^{l} - \mu\nabla L_{b_{k}^l}(\theta_{k}, X)$: $k$번째 업데이트 할 때, $l$번째 레이어의 $b$에 대해서 미분이 필요하다

위의 두 식을 보면 각 레이어 $l$에 대해서 매 업데이트시(k)마다 미분이 필요하다. 이건 너무 계산이 많이들어 딥러닝이 못했던 허들이기도 하다.

 

그렇기에 이를 해결하고자 했던, 역전파로 해결한다 [2]. 이 계산을 알기위해서는 forward방향에서의 오차를 먼저 알아야한다. 그림4처럼 L번쨰 레이어의 i번째에 원래 뉴런이 하는 작업을 $\sigma$ (활성화함수아님)라 하자. 그러면, $z_{j}^{l}$이 입력이 오던걸, $\nabla z_{j}^{l}$만큼 변경하면, 출력값은 $\sigma(z_{j}^{l} + \nabla z_{j}^{l})$로 바뀐다. 그러면 최종적으로 전체 손실함수값은 $\frac{\partial C}{\partial z^l_j} \Delta z^l_j$만큼 바뀐다고 알려져있다.

그림 4.

이제 역전파로 바꿔생각해보자. 이를 구하기위한 각 notation은 아래 와 같다. 그리고, 레이어의 개수가 $l$개라고하자(=$l$ 레이어가 마지막레이어).

  • $\delta_{j}^{l}$: $j$번째 뉴런(유닛)의 $l$번째의 레이어의 변화량. 쉽게 표현하면, j번째 뉴런이 조금변경되면, 오차가 얼마나 변화하는지를 의미한다. 상세히는 w, b로 표현하면 $\partial C / \partial w^l_{jk}$, $\partial C / \partial b^l_j$도 된다. $\delta_{j}^{l}$을 활성화 함수가 포함된 합성함수라고 생각하면 다음과 같이 미분된다 [3]. $\delta^L_j = \frac{\partial C}{\partial a^L_j} \sigma'(z^L_j).$.
  • $a_{j}^{L}$: $l$레이어에 존재하는  $j$번재 아웃풋
  • $C$: cost function을 의미. 예를 들어 $C=1/2 \sum _{j}(y_{i}-a_{j}^{L})^{2}$
  • $z^{L}$: L번째 레이어에서 출력한 결과

 

다음과 같이 역전파를 진행한다.

순서 수식 의미
1 $\delta^L = \nabla_a C \odot \sigma'(z^L)$ l번째 레이어의 출력값이 변하면, 손실함수가 얼마나 변할까?
그리고 그 계수에 해당하는 activation()만큼 변하겠지?

$\nabla_a C$은 예측값과 출값의 차이니까 이를 대입한다. (단순히 MSE라고 생각)
가령, $\delta^L = (a^L-y) \odot \sigma'(z^L).$ 을 구할 수 있다.
2 $\delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma'(z^l)$ $l$번째 레이어와 $l+1$번째 레이어에서의 관계식을 의미한다. 

일단 다음 레이어인 $l+1$번째의 오차를 먼저알기 때문에, 이때 계산에 쓰였던 파라미터 $w^{l+1}$을 전치한다음에 곱하고, 활성화함수만 곱해주면 이전 연산의 에러를 구할 수 있다는 것이다.

3 $\frac{\partial C}{\partial b^l_j} = \delta^l_j.$ 1,2을 이용하여 각 b에 대해서 편미분을 구할 수 있다.
4 $\frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k \delta^l_j.$ 1,2을 이용하여 각 w에 대해서 편미분을 구할 수 있다.

 

 

 

 

MSE와 CrossEntorpy 비교: 왜 굳이 이진분류를 할 때 Xentopry을 사용할까?


 

TLTR: CrossEntropy을 이용할 수 있으면, gradient vanishing에서 조금이나마 MSE보다는 이득이기 떄문이다.

 

MSE와 CE을 이용하는 각각의 모델에서, 우리 모델의 마지막 레이어가 sigmoid로 활성화 함수를 썼다고 가정하자. MSE은 출력레이어에서의 오차를 $\delta^L = \nabla_a C \odot \sigma'(z^L)$처럼 계산한다고 했다. 자세히보면 오차를 계산할 때, 오차를 계산하자마자 $\sigma'(z^L)$을 한 번 한다. C은 간단히 y-a(마지막 아웃풋과의 차이)이라고하자. 그러면 MSE을 계산하려면 $\delta^L = (a-y) \sigma'(z^L)$.가 된다.

반대로 CE을 이용하자고하자 CE의 loss은 $C=-[ylna+(1-y)ln(1-a)]$이다. 이걸 a에 대해서 미분하면 다음과 같다.

  1. $\nabla_{a}C = \frac {1-y} {(1-a)a}$.
  2. $\sigma'(z^L)$도 미분하면, $\sigma'(x)=\frac{d}{dx}\sigma(x)=\sigma(x)(1-\sigma(x))$을 따르므로, $a(1-a)$가 된다.
  3. $\delta^L = \nabla_a C \odot \sigma'(z^L)$ 라고 했기 때문에,
  4. $\delta^L = \frac {1-y} {(1-a)a} a(1-a) = (a-y)$ 이므로,  $\sigma'(z^L)$가 상쇄된다. 

정리하면, MSE을 이용할경우 backpropagation에서 gradient decay가 한 번 발생하고 역전파하기 때문에, 학습이 늦게되는 반면, CE을 이용한 경우 gradient decay가 한번 없이 역전파해서 상대적으로 학습이 빠르게 된다

그림3. sigmoid function의 원함수와 미분된 함수의 값

 

 

 

 

MSE의 최적화와 Likelihood을 이용한 최적화의 비교


MLE(Maximum likelihood estimation)은 관측한 데이터(x)을 가장 잘 설명하도록 파라미터($\theta$)을 찾는 과정이라고 생각하면 쉽다. 조금 바꾸어 말하면, 모델(f)내에 파라미터가 출력(y)을 가장 잘 설하도록 모델의 파라미터($\theta$)을 찾는 과정이랑 같다. 우리가 모델에서부터 얻는 예측값($f_{\theta}(x))$( =$\hat(y)$ 이기에...)가 정규분포를 따른다면, 평균적으로 실측값(y)을 얻게끔 파라미터를 얻는 과정이다. 따라서 아래와 같이 수식으로 표현할 수 있다.

$\theta^{*} = argmin_{\theta}[-log(p(y|f_{\theta}(x))]$

위의 식이 (-)Negative와 log가 붙은 negative log likelihood 이다. 의미는 모델의 반환값이 어떤 분포를 따른다면, 따른다면, 이 분포의 파라미터를 잘조절에서 y가 잘 나오게해주는 $\theta$을 찾아주세요. 라는 의미이다. 

 

위를 풀기위해 함수를 최적화하기위해 IID 을 가정한다

  • Independent : $p(y|f_{\theta}(x) = \Pi_{i}P_{D_{i}}(y|f_{\theta}(x_{i}))$: 트레이닝 데이터 DB ($D_{i}$)을 최적화하는 것에 대한 모든 곱은 전체와 같다. (독립사건이기에 서로 곱할 수 있다)
  • Identical distribution: $p(y|f_{\theta}(x) = \Pi_{i}p(y|f_{\theta}(x_{i}))$: training데이터는 매번 다른 분포를 같는 것이 아니라, $x1$이 정규분포라면 $x_{2},...x_{i}$가 모두 정규분포이다.

이를 이용하여 다음과 같이 negative loglikelhood을 정리할 수 있다

=> $-log(p(y|f_{\theta}(x))) = -\sum_{i}log(p(y_{i}|f_{\theta}(x_{i})))$: (IID을 이용). 각각의 확률값의 negative log likelhood을 다 더하면 전체의 training DB의 negative loglikelihood와 같다

위의 분포가 정규분포를 따른다면, 정규분포의 Likelihood을 이용해서 파라미터를 찾을 수 있다. 정규분포의 log-likelihood값은 위의 그림2와 같다. x은 관찰값 데이터포인트, $\mu$은 우리가 구하고자하는 파라미터 중에 평균에 해당한다. 이 식을 우리의 문제로 바꿔보자. x가 관찰값이었다면, 우리문제에서는 모델의 매번반환하는 $f(x)$의 평균값이 $\mu$가 되고자하는 것이다. 즉, $f_{\theta}(x_{i}) =\mu_{i}, ~\sigma_{i}=1$ 인 것을 원한다.

그림 2. log-likelihood function .&nbsp;https://www.statlect.com/fundamentals-of-statistics/normal-distribution-maximum-likelihood

$= -\frac{n}{2}ln(2\pi)-\frac{n}{2}(\sigma^{2}) - \frac{1}{2\sigma^{2}}\sum_{j=1}(x_{j}-\mu)^{2}$ 을 

$= -\frac{n}{2}ln(2\pi)-\frac{n}{2}(\sigma^{2}) - \frac{1}{2\sigma^{2}}\sum_{j=1}(f_{\theta}(x) - y_{i})^{2}$: 이렇게 바꿀 수 있다.

위의 식을보면 $y_{i}$와 $f_{\theta}(x_{i})$의 차이의 제곱. 즉 MSE와 같다.

 

반면의 베르누이 분포(=확률(p)에 따라, 결과가 0,1로 나오는 분포)를 따른다면 아래와 같다.

$p(y_{i}|p_{i})=p_{i}^{y_{i}}(1-p_{i})^{1-y_{i}}$

<=> $log(p(y_{i}|p_{i})) = y_{i}logp_{i} + (1-y_{i})log(1-p_{i})$
<=> $-log(p(y_{i}|p_{i})) = -[y_{i}logp_{i} + (1-y_{i})log(1-p_{i})]$

 

 

Reference


[1]  Nielsen (2015), "[W]hat assumptions do we need to make about our cost function ... in order that backpropagation can be applied? The first assumption we need is that the cost function can be written as an average ... over cost functions ... for individual training examples ... The second assumption we make about the cost is that it can be written as a function of the outputs from the neural network ..."

[2] http://neuralnetworksanddeeplearning.com/chap2.html

 

Neural networks and deep learning

In the last chapter we saw how neural networks can learn their weights and biases using the gradient descent algorithm. There was, however, a gap in our explanation: we didn't discuss how to compute the gradient of the cost function. That's quite a gap! In

neuralnetworksanddeeplearning.com

[3] https://j1w2k3.tistory.com/1100

반응형

+ Recent posts