WebSocket에 대해 알아보자

WebSocket - 실시간 양방향 통신 프로토콜

 

simple, half-duplex, full-duplex

HTTP 기반 통신 한계

HTTP는 기본적으로 client의 request에 대해 server가 response를 주는 request-response 모델임

HTTP/1.1 부터는 keep-alive로 하나의 TCP connection을 재사용할 수 있지만, 여전히 *half-duplex 방식이라 server가 client에게 먼저 데이터를 push할 수 없음

따라서 실시간 양방향 통신을 흉내내려면 아래와 같은 기법을 사용해야함

 

 

 

 

 

 

1. Polling

Client가 주기적으로 server에게 request를 보내 새로운 데이터가 있는지 확인

데이터가 없어도 request를 계속 보내기 때문에 빈 response가 발생함 -> 불필요한 http overhead가 커짐

 

2. Long Polling

Server가 응답 가능한 데이터가 없으면 해당 request를 holding한 채로 wait

데이터가 준비되면 그때 reponse를 보냄 -> server가 어느정도 push 하는 효과 보여줌

client는 응답 받자마자 바로 다음 request 보냄

각 request에는 timeout이 존재하므로 client는 주기적으로 reconnect 해야 함 -> 여전히 resource-intensive

 

WebSocket

WebSocket은 위와 같은 http 기반 방식의 한계를 해결하기 위해 등장한 프로토콜임

하나의 unique connection을 열린 상태로 계속 유지할 수 있어 latency 문제를 제거할 수 있음

또한 full-duplex asynchronous messaging을 지원함 -> client와 server가 동일한 TCP connection 위에서 동시에 message를 stream할 수 있음

 

HTTP처럼 매 요청마다 큰 header를 붙이는 대신 작은 frame header(2~14bytes)만으로 데이터를 주고받기 때문에 overhead가 훨씬 적음

워래는 browser와 server간 통신 위해 설계 되었지만 WhatsApp이나 Facebook Messenger 같은 messagin app에서도 널리 사용됨

 

WebSocket 방식

1. Handshake 요청

Client가 HTTP GET 요청에 {Upgrade: websocket, Connection: Upgrade} 헤더를 담아 보내 WebSocket connection 시도함

2. Protocol upgrade

Server가 Websocket을 지원하면 101 Switching Protocols 응답을 보내 handshake를 완료함

이때 새로운 TCP connection이 만들어지는 게 아니라 기존 HTTP connection을 그대로 유지한 채로 protocol만 WebSocket으로 전환됨

3. 양방향 통신 시작

이 시점부터 client와 server 양쪽 모두 자유롭게 데이터 주고받기 가능

 

WebSocket Handler

WebSocket이 사용될 때는 보통 user별로 WebSocket handler가 연결됨

WebSocket handler는 lightweight server machine으로 모든 active user에 대한 open connection을 유지하는 역할을 함

 

물론 WebSocket도 한계가 존재함

Stateful connection이라 server scaling이 까다로움 (load balance, sticky session 이슈)

Connection이 끊겼을 때 reconnection 로직을 직접 구현해야 함

일부 corporate proxy/firewall에서 차단될 수 있어 wss://사용이 권장됨 -> 실제 증권사 API들이 대부분 wss://만 지원함

 

 

주피터노트북 매직 커맨드 %%time을 사용하자

%%time
print('hello')

 

 

전체 학습 내용
1. 초기화 - 활성화 함수, 전처리, 가중치 초기화, regularization, gradient checking
2. Training dynamics - babysitting the learning process, 파라미터 업데이트, 하이퍼파라미터 최적화
3. 평가 - 모델 앙상블

 

 

오늘 다룰 내용

활성화 함수

데이터 전처리

가중치 초기화

배치정규화

babysitting the learning process

하이퍼파라미터 최적화 


Activation Functions

Sigmoid

 

 

$$ \sigma(x) = 1/(1+e^{-x}) $$

- 각 입력을 받아 그 입력을 [0,1] 사이의 값이 되도록 해줍니다.

- 입력의 값이 크면 출력은 1에 가깡루 것이고 값이 작으면 0에 가까울 것입니다.

- 0 근처 구간을 보면 선형함수처럼 보입니다. 

 

문제점 1. Saturate된 뉴런은 gradient를 없앱니다.

만약 한 뉴런에서의 입력 x가 큰 음수 값이라면 역전파 과정에서 dL/dσ 는 0이 됩니다. gradient가 죽어버리게 되고 밑으로 0이 계속 전달되게 됩니다.

 

문제점 2.  Sigmoid의 출력이 zero-centered 하지 않습니다. 

뉴런의 입력 x가 항상 양수일 경우를 생각해봅시다. 이 x는 어떤 가중치랑 곱해져 활성함수를 통과하게 됩니다.

이때 뉴런의 Local Gradient는 그냥 x 이고 모든 x가 양수란는 것은 gradient가 전부 양수 또는 전부 음수가 됩니다. 

 

위에서 음 또는 양의 값을 가지는 dL/df(활성함수) 가 넘어와 local gradient인 df/dW와 곱해지게 됩니다.  이때 df/dW는 x이고 x는

항상 양수이기 때문에 gradient의 부호는 그저 위에서 내려온 gradient의 부호와 같아집니다.

이로 인해 모든 W가 같은 방향으로만 움직일 것입니다. 파라미터를 업데이트 할 때 다 같이 증가하거나 다 같이 감소하거나 할 수 밖에 없습니다. 이런 gradient 업데이트 방식은 아주 비효율적입니다. 

 

위 그래프의 축은 각각의 w를 나타냄 (2차원 W)

 

W가 이차원일 경우에 전부 양수 또는 음수로 업데이트된다면 gradient가 이동할 수 있는 방향은 4분명 중 두 영역뿐입니다.

만약 이론상 최적의 해가 파란색 벡터라면 gradient는 파란색 방향으로 내려갈 수 없습니다 (오른쪽-아래). 위의 빨간색 화살표 방향과 같이 gradient가 이동 가능한 방향(오른쪽-위, 왼쪽-아래)으로만 이동할 수 있게 됩니다. 지그재그로 내려가야해서 굉장히 비효율적이어집니다. 

 

이런 이유로 일반적으로 zero-mean data를 원합니다. 입력 x가 양수/음수 모두 가지고 있으면 전부 같은 방향으로 움직이는 일은 발생하지 않을 것입니다. 

 

문제점 3. exp()로 인해 계산 비용이 큽니다.

그렇게 큰 문제는 아닙니다. 오히려 내적의 계산 비용이 더 큽니다. 근데 굳이 문제를 뽑자면 얘도 문제가 될 수 있겠습니다.

 

tanh

- sigmoid와 유사하지만 범위가 [1,-1]입니다.

- zero-centered 합니다. 

- 여전히 saturation때문에 gradient가 죽습니다. 

- sigmoid보다 조금 좋지만 여전히 문제점이 있습니다.

ReLU (Rectified Linear Unit)

$$ f(x) = \max(0,x) $$

- ReLU는 element-wise 연산을 수행합니다.

- 입력이 음수면 0을 출력하고 양수면 입력 값 그대로 출력합니다.

-  양의 범위에서 Saturate하지 않습니다. 적어도 입력 스페이스의 절반은 saturation 되지 않습니다.

-  계산이 아주 효율적입니다. 시그모이드는 exp 연산을 해야했던 반면에 ReLU는 max 연산이므로 계산이 매우 빠릅니다.

- Sigmoid나 tanh보다 수렴속도가 거의 6배정도 빠릅니다. 

 

 

문제점 1. zero-centered 하지 않습니다.

문제점 2. 양수에서 saturation 되지 않지만 음의 경우에선 그렇지 않습니다. 

 x가 큰 음수값이거나 정의되진 않지만 0이라면 gradient가 0이 됩니다. 이로 인해 ReLU는 gradient의 절반을 죽여버립니다. 

이로인해 dead ReLU라는 현상을 겪을 수 있습니다.

 

이런일은 몇가지 상황에서 발생할 수 있습니다.

DATA CLOUD = Traning data

ReLU에서는 평면의 절반만 activate 됩니다.  ReLU가 data cloud에서 떨어져있는 경우에 dead ReLU 발생할 수 있는데 해당 ReLU에서는 activate가 일어나지 않고 update되지 않습니다. 반면 active ReLU에서는 일부는 active 되고 일부는 active 되지 않을 것입니다. 

 

이런일이 발생하는 이유는 몇가지 존재합니다. 

 

첫번째, 초기화를 잘못한 경우

위 그림의 dead ReLU처럼 생긴 경운데 data cloud에서 멀리 떨어져 있는 경우입니다. 이런 경우 어떤 데이터 입력에서도 activate되는 경우가 존재하지 않을 것이고 backprop이 일어나지 않습니다. 이런 경우 update도 activate도 되지 않습니다.

 

두번째, learning rate가 지나치게 높은 경우

더 흔한 경우입니다. 처음에 적절한 ReLU로 시작할 수 있다고 해도 만약 update를 지나치게 크게 해 버려 가중치가 날뛰면  ReLU가 데이터의 manifold를 벗어나게 됩니다. 그래서 처음에는 학습이 잘되다가 갑자기 죽어버리는 경우가 생깁니다.

 

그리고 실제로 학습을 다 시켜놓은 네트워크를 살펴보면 10~20% 가량은 dead ReLU가 됩니다. 이게 문제긴 하지만 ReLU를 사용하고 있다면 대부분의 네트워크가 이 문제를 격을 수 있음. 하지만 그 정도로는 네트워크 학습에 큰 지장을 줄 수 없습니다.

그래서 실제로 ReLU를 초기화할 때 update시에 active ReLU가 될 가능성을 조금이라도 더 높혀주기 위해 postivie biases를 추가해 주는 경우가 있습니다. 효과가 있다는 사람들도 있고 없다는 사람들도 있습니다. 이 방법보다 대부분은 zero-bias로 초기화합니다.  

Leaky ReLU

$$ f(x) = \max(0.01x, x) $$

- ReLU와 거의 유사하지만 음수 구간에서 0이 아닙니다.

- 음의 구간에서 saturation되지 않습니다.

- 계산이 효율적이고 수렴이 빠릅니다.

- dead ReLU 현상이 발생하지 않습니다.

PReLU (Parametric Rectifier)

$$ f(x) = \max(\alpha x, x) $$

- 음의 구간에서 기울기가 있다는 점에서 Leaky ReLU와 유사하지만 기울기가 alpha라는 파라미터로 결정됩니다.

- alpha를 딱 정해놓는 것이 아닌 backprop으로 학습시키는 파라미터로 만든 것입니다.

 

ELU (Exponential Linear Units) 

-  ReLU의 이점을 가집니다.

-  zero mean에 가까운 출력을 가집니다.

- 음의 구간에서 saturation 됩니다.

- ELU는 이런 saturation(deactivation)이 noise에 더 robust할 수 있다고 주장합니다. 

- ReLU와 Leaky ReLU의 중간 정도로 보면 좋을 것 같습니다.

 

 

Maxout "Neuron"

$$  \max(w_1^Tx+b_1, w_2^Tx + b_2) $$

- 지금까지 본 활성함수들과 다르게 입력을 받아드리는 특정한 기본형식을 미리 정의하지 않습니다.

- 두 함수의 결과중 최적값을 출력합니다. 

- Maxout은 ReLU와 Leaky ReLU의 좀 더 일반화된 형태입니다.

- Maxout 또한 선형이기 때문에 saturation되지 않으면 gradient가 죽지 않을 것입니다.

- 뉴런 당 파라미터의 수가 두배가 된다는 단점이 있습니다. 

 

실제로는 어떻게 사용할까

 

ReLU가 표준으로 많이 사용되며 보통 잘 동작합니다. 다만 Learning Rate를 아주 조심히 결정해야합니다.

Leaky ReLU / Maxout / ELU도 써볼 수 있겠지만 아직 실험적입니다.

tanh도 써볼 수 있겠지만 다른 활성함수들이 더 좋을 것 입니다.

sigmoid는 사용하지 않는 것이 좋을 것 같습니다.


DATA PREPROCESSING

 

가장 대표적인 전처리 과정은 'zero-mean'으로 만들고 'normalize'하는 것입니다. 보통 표준편차로 normalization 해줍니다. 

앞서 활성함수를 다룰 때 본 것처럼 입력이 전부 양수이면 모든 뉴런이 양수인 기울기를 얻게 되고 이는 최적하지 못한(suboptimal) 최적화가 됩니다. 이런 일은 데이터가 전부 양수일 때 뿐만 아니라 0이거나 전부 음수일 경우에도 발생합니다. 

normalization을 해주는 이유는 모든 차원이 동일한 범위 안에 있게 해줘 전부 동등하게 contribute 할 수 있게 합니다. 

실제로 이미지의 경우 전처리로 zero-centering 정도만 해줍니다.

여러 ML 문제와는 다르게 이미지는 이미 각 차원 간에 스케일이 어느정도 맞춰져 있기 때문에 normalization은 하지 않습니다. 

 

ML에는 PCA나 whitening같은 더 복잡한 전처리 과정이 있지만 이미지에서는 단순히 zero-mean 정도만 사용하고 그 밖의 여러 복잡한 방법은 잘 다루지 않습니다. 일반적으로 이미지를 다룰 때는 굳이 입력을 더 낮은 차원으로 projection 시키지 않습니다. 

 

 기본적으로 이미지를 다룰 때는 zero-mean을 전처리 해줍니다. 평균값은 전체 training data에서 계산하고 test data에도 동일한 값으로 계산하게됩니다. 보통 입력이미지의 사이즈를 서로 맞춰주고 네트워크에 들어가기 전에 평균값을 빼주게 됩니다. 

실제로 일부 네트워크는 채널 전체의 평균을 구하지 않고 채널(RGB)마다 평균을 독립적으로 계산하는 경우도 있습니다.

 채널별로 평균이 비슷할지 아닐지는 본인이 판단하기 나름입니다.

 

zero-mean을 사용해야하는 이유를 추가적으로 설명드리겠습니다.

 

위의 이진분류 문제를 푸는 상황이고 왼쪽은 normalization/zero-centered 되지 않은 경우이고 오른쪽은 적용된 경우입니다. 

물론 단순하게 생각해서 왼쪽의 경우에도 classification이 가능하지만 선이 조금만 움직여도 classification이 잘 되지 않습니다. 이는 손실함수가 아주 약간의 가중치 변화에도 엄청 예민하다는 것을 의미합니다. 왼쪽의 경우에 Loss가 파라미터에 너무 민감하기 때문에 동일한 함수를 쓰더라도 오른쪽에 비해 학습 시키기 아주 어렵습니다. 

 

반면 오른쪽에서 선의 기울기가 살짝식 왔다갔다 하는 경우를 생각해보면 손실함수가 가중치의 변동에 왼쪽 경우보다 덜 민감하다는 것을 할 수 있습니다. 이 경우가 최적화가 더 쉽습니다.

 

이는 Linear Classification의 경우에만 국한되는 것이 아닙니다. Neural Network 내부에도 다수의 linear classifier가 있다고 생각할 수 있기 때문에 입력이 zero-centered/Unit variance가 아닌 경우라면 레이어의 Weight Matrix가 아주 조금만 변해도 출력은 엄청 심하게 변하게 돼 학습이 어려워집니다. 

 


Weight Initialization

Q. 만약 모든 가중치를 0으로 초기화하면 어떻게 될까

A. 모든 뉴런이 똑같은 일을 할 것입니다. 가중치가 0이기 때문에 모든 뉴런은 동일한 연산을 수행합니다. 

출력, gradient 전부 같습니다. 결국 모든 가중치가 똑같은 값으로 업데이트 됩니다. 

가중치를 동일한 값으로 초기화 시켰을 경우 이런 일이 발생합니다. 

 

해결책 1. 임의의 작은 값으로 초기화 하기

W = 0.01 * np.random.randn(D,H)

 

이 경우 초기W를 표준정규분포에서 샘플링합니다. 좀 더 작은 값을 위해 0.01을 곱해 표준편차를 1e-2로 만들어줍니다.

네트워크의 크기가 작다면 symmetry breaking하기에 충분하지만 더 깊은 네트워크에서 문제가 생길 수 있습니다.

 

문제점을 살펴봅시다.

D = np.random.randn(1000, 500)
hidden_layer_sizes = [500] * 10
nonlinearities = ['tanh'] * len(hidden_layer_sizes)

act = {'relu':lambda x: np.maximum(0,x), 'tanh':lambda x:np.tanh(x)}
Hs = {}
for i in range(len(hidden_layer_sizes)):
    X = D if i==0 else Hs[i-1]
    fan_in = X.shape[1]
    fan_out = hidden_layer_sizes[i]
    W = np.random.randn(fan_in, fan_out) * 0.01

    H = np.dot(X,W)
    H = act[nonlinearities[i]](H)
    Hs[i] = H

 

10개의 레이어로 이루어진 네트워크고 레이어당 500개의 뉴런이 있습니다.  nonlinearities로는 tanh를 사용하고 가중치는 임의의 작은 값으로 초기화시킵니다. 데이터를 랜덤으로 만들어주고 forward pass 시켜보겠습니다. 그리고 각 레이어별 activations 수치를 통계화 시켜보겠습니다.

print('input layer had mean %f and std %f'%(np.mean(D), np.std(D)))
layer_means = [np.mean(H) for i,H in Hs.items()]
layer_stds = [np.std(H) for i,H in Hs.items()]
for i,H in Hs.items():
    print('hidden layer %d had mean %f and std %f'%(i+1, layer_means[i], layer_stds[i]))

plt.figure()
plt.subplot(121)
plt.plot(Hs.keys(), layer_means, 'ob-')
plt.title('layer mean')
plt.subplot(122)
plt.plot(Hs.keys(), layer_stds, 'or-')
plt.title('layer std')

plt.figure()
for i,H in Hs.items():
    plt.subplot(1, len(Hs), i+1)
    plt.hist(H.ravel(), 30, range=(-1,1))

 

좌상단에 보이는 값들은 각 레이어의 출력의 평균과 표준편차를 계산한 것입니다. 레이어들의 평균은 tanh의 특성상 항상 0 근처에 있습니다. 하지만 표준편차를 보게되면 아주 가파르게 줄어 0에 수렴하게 됩니다. 

첫번째 레이어에서는 가우시안스럽게 생긴 좋은 분포를 가지지만 W를 곱하면 곱할수록 W가 너무 작은 값들이라 출력 값이 급격히 줄어듭니다. 그리고 결국 0이 됩니다.

backward pass 할때 upstream gradient를 local gradient와 곱하게 됩니다. 그때 local gradient를 구해보면 WX를 W에 대해 미분해 결국 X가 될텐데 X가 엄청 작은 값이기 때문에 gradient도 작을 것이고 결국 업데이트가 잘 일어나지 않을 것입니다. 

 

이번엔 가중치를 큰 값으로 초기화하면 어떻게 될지 알아보겠습니다.

 

가중치의 편차를 0.01이 아니라 더 큰 값인 1로 했을 경우에 레이어의 출력이 항상 -1 이거나 +1 일 것입니다. 그렇기 때문에 tanh의 출력은 saturation될 것이고 gradient는 0이 될 것입니다. 따라서 가중치의 업데이트가 일어나지 않게 됩니다. 

 

가중치가 너무 작으면 사라져버리고 너무 크면 saturation되어 버립니다. 그래서 사람들이 초기화하기 좋은 방법을 찾으려했습니다.

그 중 하나가 Xavier Initialization입니다.

 

Xavier Initialization

W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in)

 

Standard Gaussian으로 뽑은 값을 입력의 수로 스케일링 해줍니다. 기본적으로 Xavier Initilization가 하는 일은 입/출력의 분산을 맞춰주는 것입니다. 

 

이 수식을 통해 직관적으로 이해할 수 있는 것은 입력의 수가 작으면 더 작은 값으로 나눠 더 큰 값을 얻게됩니다. 이는 입력의 수가 작을 경우 분산이 작아지는 것을 막기 위해 더 큰 가중치로 초기화해 더 큰 값을 얻을 수 있기 때문입니다. 반대로 입력의 수가 많을 경우에는 더 작은 가중치가 필요합니다.

 

각 레이어의 입력이 Unit gaussian이길 원한다면 이런 류의 초기화 기법을 사용해 볼 수 있습니다.

 

여기서 가정하는 것은 현재 linear activation이 있다는 것입니다. 가령 tanh의 경우 tanh의 active region 안에 있다고 가정하는 것입니다. 

 

 

 

Xavier Initilization + ReLU

 

Xavier Initilization과 ReLU를 함께 사용하면 잘 동작하지 않습니다. ReLU는 출력의 절반을 죽입니다. 그 절반은 매번 0이됩니다. 따라서 점점 더 많은 값들이 0이 되고 비활성 됩니다. 결국 분포가 계속 줄어 결국 0이 돼버리고 맙니다.

W = np.random.randn(fan_in, fan_out) / np.sqrt(fan_in/2)

 

이 문제를 해결하기 위한 일부 논문에서는 뉴런 들 중 절반이 없어진다는 사실을 고려해 추가적으로 2를 더 나눠줍니다.

 

실제로 잘 동작합니다. 결과를 보시면 전체적으로 좋은 분포를 형성하고 있는 것을 볼 수 있습니다.

이런 작은 변화는 트레이닝에 있어서 엄청난 차이를 보입니다. 가령 일부 논문을 보면 그런 차이가 트레이닝이 정말 잘되거나 하나도 안되거나를 결정하는 결과를 보이기도 합니다.

 

초기화는 우선 Xavier Initilization을 해보고 나서 그 밖에 다른 방법을 시도해 봅시다.


Batch Normalization

$$ \hat{x}^{(k)} ={x^{(k)} - E[x^{(k)}]\over{\sqrt{Var[x^{(k)}]}}} $$

레이어의 출력을 강제로 unit gaussian으로 만듭니다.

어떤 레이어로부터 나온 Batch 단위 만큼의 activation이 있다고 했을 때 이 값들을 현재 Batch에서 계산한 평균과 분산으로 normalization해 Unit Gaussian으로 만듭니다.

가중치를 잘 초기화 시키는 것 대신에 학습할 때마다각 레이어에 이런 일을 해줘서 모든 레이어가 unit gaussian이 되게 합니다. 

그래서 이제부터할일은 네트워크의 forward pass 동안에 그렇게 되도록 명시적으로 만들어 주는 것입니다.

또한 평균과 분산을 상수로 가지고만 있으면 언제든지 미분이 가능해 backprop이 가능하게 됩니다. 

 

 

Batch당 N개의 학습 데이터가 있고 각 데이터가 D차원이라고 해봅시다. 

각 차원(feature element)별로 평균을 각각 구해줍니다. 한 Batch 내에 이걸 전부 계산해서 Normalize합니다. 

 

 

보통 배치정규화는 FC나 Conv Layer 직후에 넣어줍니다. 깊은 네트워크에서 각 레이어의 W가 지속적으로 곱해져서 Bad scling effct가 발생했지만 Normalizationd는 그 bad effect를 상쇄시켜줍니다. batch normalization은 입력의 스케일만 살짝 조정해주는 역할이기 때문에 FC와 Conv 어디에든 적용할 수 있습니다.

 

Conv Layer에서 차이점이 있다면 Normalization을 차원(feature)마다 독립적으로 수행하는 것이 아니라 Activation Map의 같은 채널에 있는 요소들을 같이 Normalize 해줍니다. 왜냐하면 Convolutional Property 때문에 같은 방식으로 normalize 시켜줘야 하기 때문입니다. 따라서 Conv Layer의 경우엔 Activation map(채널, Depth)마다 평균과 분산을 하나만 구합니다.

 

제대로된 흐름도 살펴보고 gradient 어떻게 구하는지 궁금하면 논문을 한번 봅시다.

http://proceedings.mlr.press/v37/ioffe15.html

 

Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift

Training Deep Neural Networks is complicated by the fact that the distribution of each layer’s inputs changes during training, as the parameters of the previous layers change. This slows down the t...

proceedings.mlr.press

 

Normalization이 하는 일은 입력이 tanh의 linear한 영역에만 존재하도록 강제하는 것입니다. 그렇게 되면 saturation이 전혀 일어나지 않지만 그것보단 '얼마나' saturation이 일어날지를 우리가 조절하면 더 좋을 것 같습니다.

 

왼쪽 아래 수식에서 볼 수 있듯이 Batch Normalization에서는 Scaling 연산을 추가합니다. 이를 통해 Unit Gaussian으로 normalize된 값들을 감마로는 스케일링의 효과를, 베타로는 이동의 효과를 줍니다. 매우 극단적인 상황에서 네트워크가 감마는 분산을 베타는 평균을 학습한다면 네트워크가 값들을 원상복구 시킬 수 있습니다. 하지만 실제로는 잘 일어나지 않습니다.

이를 통해 네트워크가 데이터를 tanh에 얼마나 saturation 시킬지를 학습하기 때문에 유연성을 얻을 수 있습니다. 

BN을 다시 모든 mini-batch마다 각각 평균과 분산을 계산해줍니다. 그리고 평균과 분산으로 Normalize한 이후에 다시 추가적인 scaling, shifting factor를 사용합니다. BN은 gradient의 흐름을 보다 원활하게 해주며 결국 학습이 더 잘되게(robust) 해줍니다.

BN을 쓰면 learning rates를 더 키울 수도 있고 다양한 초기화 기법들도 사용해 볼 수 있습니다. 

 

또한 BN은 Regularization의 역할도 합니다. 각 레이어의 출력은 batch안에 존재하는 모든 데이터들의 평균과 분산으로 normalize되기 때문에 batch안에 존재하는 모든 데이터들에 영향을 받습니다.

 

bn은 평균과 분산을 학습데이터에서 구하고 test time에서 추가적인 계산을 하지 않습니다. trainig time에 running averages 같은 방법으로 평균, 분산을 계산하고 test time에 사용합니다. 


Babysitting the Learning Process

학습 과정을 다루는 방법에 대해 알아봅시다. 지금까지는 네트워크 설계를 배웠고 이제는 학습과정을 어떻게 모니터링하고 하이퍼파라미터를 조절할 것인지 배워보겠습니다. 

 

 

1단계 : 데이터전처리

앞서 배운 것처럼 zero-mean을 사용합니다.

 

2단계 : 아키텍처 선택

 

우선 하나의 Hidden Layer와 50개의 뉴런을 가진 모델로 해보겠습니다. 그 밖에 어떤 모델을 선택해도 상관 없습니다.

 

 

이제 Loss가 잘 동작하는지 알아봅시다. Forward pass를 하고난 후에 Loss가 그럴듯 해야 합니다. 가령 우리가 softmax를 사용하고자 한다면 우리는 가중치가 작은 값일 때 Loss가 대강 어떻게 분포해야하는지를 이미 알고 있습니다. Softmax classifier의 Loss는 negative log likelihood 가 되어야합니다. 가령 10개의 클래스라면 Loss는 -log(1/10)이 될 것입니다. 위에서 loss가 약 2.3 정도 되는 것을 볼 수 있는데 이를 통해 Loss가 원하는 대로 동작한다는 것을 알 수 있습니다. 이는 꽤 유용한 sanity check입니다.

 

 

이번엔 Regularization을 추가해보겠습니다. 손실함수에 Regularization term이 추가되기 때문에 Loss가 증가했습니다.

이 또한 유용한 sanity check입니다.

 

이제 학습할 준비가 끝났고 학습을 시작해봅시다.

20 examples from CIFAR-10 / reg = 0.0 / vanilla 'SGD'

 

처음 시작할 때 좋은 방법은 데이터의 일부만 우선 학습시켜 보는 것입니다. 데이터가 적으면 당연히 overfit이 생길 것이고 loss가 많이 줄어들 것입니다. 이때는 regularization을 사용하지 않고 Epoch마다 Loss가 0을 향해 잘 내려가는지 확인합니다. 

 

지금까지의 sanity check가 잘 끝났다면 이제부터 진짜 학습을 시작해보겠습니다. 

 

이젠 전체 데이터셋을 사용할 것이고 regularization을 약간만 주면서 적절한 learning rate를 찾아야 합니다.

 

learning rate는 가장 중요한 하이퍼파라미터 중 하나이고 가장 먼저 정해야만 하는 하이퍼파라미터입니다. 

learning rate를 몇가지로 정하고 실험해봅시다. 

 

우선 위에서 learning rate를 1e-6으로 정해봤습니다. loss가 좀처럼 변하지 않았습니다. loss가 잘 줄어들지 않는 가장 큰 요인은 learning rate가 지나치게 작은 경우입니다. learning rate가 지나치게 작으면 gradient 업데이트가 충분히 일어나지 않게되고 cost가 안 변하게 됩니다. 

여기서 유심히 살펴봐야할점은 loss가 잘변하지 않음에도 training/validation accuracy가 20%까지 급상승합니다. 

이는 현재 확률 값들이 아직까지 멀리 퍼져있기 때문에 loss가 여전히 비슷비슷한 것입니다. 하지만 이 확률은 조금씩 옳은 방향으로 바뀌고잇습니다. 우리가 지금 학습을 하고 있기 때문입니다.

가중치와 loss는 서서히 변하고 있지만 accuracy는 갑자기 뛰어버릴 수 있습니다. Loss는 각 클래스에 대한 확률값들을 비교하지만 accuracy는 단지 예측한 클래스가 정답이면 1 아니면 0이기 때문에 갑자기 뛸 수 있는 것입니다.

 

이번엔 더 큰 값인 1e6으로 learning rate를 바꿔보겠습니다. 결과를 보면 cost가 NaN임을 알 수 있습니다. 이는 cost가 발산했음을 의미합니다. 주된 이유는 learning rate가 지나치게 높기 때문입니다. 이 경우에는 learning rate를 낮춰야 합니다.

 

보통 learning rate는 1e-3 에서 1e-5 사이의 값을 사용합니다. 이 범위 사이의 값들을 이용해서 cross-validation을 수행해 줍니다. 이 사이의 값들을 이요해서 learning rate가 지나치게 작은지 아니면 큰지 정할 수 있습니다.


Hyperparameter Optimization

그렇다면 하이퍼 파라미터를 최적화시키고 그중 가장 좋은 것을 선택하려면 어떻게 해야할까요?

 

취할 수 있는 한가지 전략은 바로 cross-validation 입니다.

두가지 스테이지를 통해 확인해볼 수 있습니다.

 

1. Coarse Stage

넓은 범위에서 값을 골라냅니다. 적은 Epoch 수만으로도 현재 값이 잘 작동하는지 알 수 있습니다. NaN이 뜨거나 Loss가 줄지 않거나 하는 것을 보면서 적절히 잘 조절할 수 있을 것입니다. 이 Stage가 끝나면 어느 범위에서 잘 동작하겠다를 대충 알게 될 것입니다.

2. Fine Stage

Coarse Stage 이후에 좀 더 좁은 범위를 설정해 학습을 더 길게 시켜보면서 최적의 값을 찾습니다.

 

NaNs로 발산하는 징조를 미리 감지할 수도 있습니다. Train 동안에 Cost가 어떻게 변하는 지를 살펴보는 것입니다. 이전의 cost 보다 매우 커졌다면 잘못 되고 있는 것입니다. Cost가 엄청 크고 빠르게 오르고 있다면 loop를 멈춰버리고 다른 하이퍼파라미터를 선택하면 됩니다. 

 

예시를 한번 보겠습니다.

 

Coarse Search

위 예시는 5 epochs를 돌며 coarse search를 하는 과정입니다. 네트워크 구조는 앞서 만든 것과 유사합니다. 여기에서 확인해야할것은 validation accuracy 입니다. 높은 val_acc에는 빨간색으로 표시해놨습니다. 빨간색으로 표시해둔 지역이 바로 fine-stage를 시작할 만한 범위가 될 것입니다. 한 가지 주목할 점은 하이퍼파라미터 최적화 시에는 Log scale로 값을 주는 것이 좋습니다. 파라미터 값을 샘플링할때 10^-3 ~ 10^-6을 샘플링하지 말고 10의 차수 값만 샘플링하는 것이 좋습니다 (-3 ~ -6) 왜냐하면 learning rate는 gradient와 곱해지기 때문에 learning rate의 선택 범위를 log scale을 사용하는 편이 좋습니다. 따라서 차수(orders of magnitude)를 사용하는 것이 좋습니다.

 

Fine Search



범위를 다시한번 조절해보겠습니다. reg는 범위를 10^-4 에서 10 ^0 정도로 좁히면 좋을 것 같습니다. 

하지만 여기엔 문제가 있습니다. 우리는 여기에서 가장 좋은 Accuracy를 찾았습니다(빨간 화살표). 잘 보면 good learning rates는 전부 10e-4 사이에 존재합니다. learning-rate의 최적값들이 우리가 다시 좁혀 설정한 범위의 경계부분에 집중되어 있다는 것을 알 수 있습니다. 이렇게 되면 최적의 learning rate를 효율적으로 탐색할 수 없습니다. 실제로 최적의 값이 1E-5나 1E-6 쯤에 존재할 수도 있습니다. 탐색 범위를 조금 이동시킨다면 더 좋은 범위를 찾을 수 있을지도 모릅니다. 

 

당연히 최적의 값이 내가 정한 범위의 중앙 쯤에 위치하도록 범위를 잘 설정해 주는 것이 중요합니다. 


하이퍼파라미터를 찾는 또 다른 방법은 grid search를 이용하는 것입니다. 하이퍼 파라미터를 고정된 값과 간격으로 샘플링하는 것입니다. 하지만 실제로는 grid search 보다는 random search를 하는 것이 더 좋습니다.  random이 더 좋은 이유는 바로 내 모델이 어떤 특정 파라미터의 변화에 더 민감하게 반응을 하고 있다고 생각해보면 이 함수가 더 비효율적인 dimentionality를 보인다고 할 수 있고 (노랑에는 별로 영향을 받지 않음) Random Search는 중요한 파라미터에게도 더 많은 샘플링이 가능하므로 위에 그려놓은 초록색 함수를더 잘 찾을 수 있을 것입니다.  grid layout에서는 정해진 샘플링 밖에 할 수 없으므로 good region을 제대로 찾을 수 없습니다.

 

앞서 corase search ->fine search 순으로 진행해야한다고 했습니다. coarse search 단계에서는 하이퍼 파라미터를 조금 더 넓게 설정하고 iteration도 작게 설정해  학습시켜봅니다. 그리고 fine search 단계에서는 결과가 좋은 범위로 좁혀 iteration을 조금 더 돌면서 다시 탐색합니다. 적절한 하이퍼파라미터를 찾을 때까지 이 과정을 반복합니다. 

 

가장 중요한 점은 coarse range를 설정할 때 가능한 최대로 범위를 설정해 줘야 한다는 것입니다. 그 범위가 하이퍼파라미터의 전범위를 살펴볼 수 있도록 충분히 넓어야합니다. 

Q. 보통 하이퍼파라미터를 몇개 선택하나요?
A. 모델에 따라 다릅니다. 선택한 하이퍼파라미터의 수가 많을수록 기하급수적으로 경우의 수가 늘어납니다. 따라서 한번에 너무 많이 테스트 할 수 없습니다. 또 이는 얼마나 많은 자원을 학습에 사용할 수 있는지도 중요합니다. 이는 사람마다 다르고 실험마다 다릅니다. 보통 두세가지 정도를 고르는 편이고 많아도 네가지 정도만 선택하고 그 이상은 통제하기 어려워집니다.
일반적으로 learning rate가 가장 중요해 learning rate를 가장 먼저 선택해 놔야만 합니다. regularization, learning rate decay, model size 같은 것들은 lr보단 덜 중요합니다. 

Q. 어떤 하이퍼파라미터의 값을 변경할 시에 다른 하이퍼파라미터의 최적 값이 변해버리는 경우가 빈번하나요?
A. 가끔 발생합니다. learning rates가 이런 문제에 덜 민감함에도 실제로 발생하곤 합니다. learning rates가 좋은 범위 내에 속하길 원하지만 보통은 optimal 보다는 작은 값이고 학습 속도가 길어지곤 합니다. 이를 해결할 더 좋은 최적화 방법은 다음 강의에서 알아봅시다.

다양한 하이퍼파라미터가 존재합니다.

- 네트워크 아키텍처

- learning rate, decay scehdule, update type, regularization

- hidden unit, depth

등이 있는데 일부는 이번 시간에 다뤘고 나머지는 다음 강의에서 다뤄봅시다.


Cross Validation

 

실제로 하이퍼파라미터 최적화와 Cross-validation을 정말 많이 해야 할 것입니다.  cross validation으로 엄청나게 많은 하이퍼파라미터를 직접 돌려보고 모니터해서 어떤 값이 좋고 나쁜지를 확인해야합니다. 위에 보이는 loss curve를 보면서 좋은 것을 찾아서 시도해보는 일을 계속 반복해야합니다.  

 

Loss Curve로 Learning rate를 알아보자

loss curve를 모니터링 하는데 있어서 learning rate이 정말 중요합니다. 그리고 loss curve를 보고 어떤 learning rate가 좋고 나쁜지를 알 수 있습니다. loss가 발산하면 learning rate가 너무 높은 것이고 너무 평평하다 싶으면 너무 낮은 것입니다. 또 가파르게 내려가다가 어느 순간 정체기가 생기면 이 또한 여전히 너무 높다는 의미입니다.  learning step이 너무 크게 점프해서 적절한 local optimum에 도달하지 못하는 경우입니다.  최적의 learning rate는 보통 파란색의 Loss curve 형태를 보입니다. 비교적 가파르게 내려가면서도 지속적으로 잘 내려간다면 현재 learning rate를 유지해도 좋습니다. 

 

Loss curve

 

 

Loss가 평평하다가 갑자기 가파르게 내려가는 것을 보게 된다면 이는 초기화의 문제일 수 있습니다. gradient의 backprop이 초기에는 잘 되지 않다가 학습이 진행되면서 회복되는 경우입니다. 

 

Accuracy 모니터링하기

 

accuracy를 모니터링 하다보면 겪을 수 있는 일인데 만일 train_acc와 va_acc가 큰 차이를 보인다면 overfit일지도 모릅니다 따라서 regularization의 강도를 높혀야 할지도 모릅니다. gap이 없다면 아직 overfit 하지 않은 것이고 capacity를 높힐 수 있는 충분한 여유가 있다는 것을 의미합니다. 

 

가중치 크기 대비 가중치 업데이트 추적하기

# assume parameter vector W and its gradient vector dW
param_scale = np.linalg.norm(W.ravel())
update = -learning_rate * dW
update_scale = np.linalg.norm(update.ravel())
W += update
print(update_scale / param_scale)

 

가중치의 크기 대비 가중치 업데이트 비율을 지켜볼 필요가 있습니다. 우선 파라미터의 norm을 구해서 가중치의 규모를 계산합니다. 그리고 업데이트 사이즈 또한 norm을 통해 구할 수 있고 이를 통해 얼마나 크게 업데이트 되는지를 알 수 있습니다. 대충 이 비율이 0.001 정도 되길 원합니다. 이 값은 변동이 커서 정확하지 않을 수 있지만 업데이트가 지나치게 크거나 작은지에 대한 감을 어느정도 가질 수는 있습니다.  업데이트가 너무 지나치게 커서도 너무 없어도 안됩니다. 문제가 뭔지 디버깅할때 유용하게 사용할 수 있습니다.

 


요약

활성화함수는 ReLU 사용하기
이미지 데이터 전처리는 평균 빼기
가중치 초기화는 xavier initialization 사용하기
배치 정규화 사용하기
하이퍼파라미터 최적화는 random search 사용하기

 

강의 초반에 역사에 대해서 다루는데 직접 영상 보는 걸 추천드립니다. (~14:00)

https://www.youtube.com/watch?v=bNb2fEVKeEo&list=PL3FW7Lu3i5JvHM8ljYj-zLfQRF3EO8sYv&index=5

 


 

 

오늘은 CNN에 대해 살펴볼건데 우선 저번 인공신경망 때처럼 뇌 관련 유사성은 배제하고 함수적인 측면에서 다뤄보겠습니다.

 

FC Layer

 

 

 저번 시간에 fc layer에 대해 다뤘는데 what we're doing is we operate on top of these vectors, 32 x 32 x 3의 이미지를 예를 들었을 때 모든 픽셀들을 쭉 길게 늘려서 3072차원의 벡터를 얻을 수 있습니다. 이후에 10 x 3072 가중치 행렬과 곱한 후 해당 레이어의 출력값인 activations를 얻습니다.  

 

이후에 하나의 숫자를 얻게 되는데 이 숫자는 해당 뉴런의 값입니다. 위에서 말한 경우에는 이런 숫자들을 10개 얻게됩니다.

 

Convolution Layer

 

 

Convolution Layer와 FC layer의 주요한 차이점은 Convolution Layer는 공간적 특성을 보존한다는 것입니다. 이전에 했던 것처럼 픽셀을 길게 늘려 하나의 벡터를 얻는 대신에 이미지의 구조를 유지합니다. 

 

 

가중치는 하나의 작은 필터가 됩니다. 위의 경우에는 5x5x3 짜리 필터입니다. 이 필터를 가지고 이미지를 슬라이드 하게 됩니다. 그리고 모든 공간적 위치에서 dot product를 계산합니다.  좀 더 디테일하게 다뤄보겠습니다.

 

 

첫번째로 필터는 항상 input 크기의 전체 깊이 만큼 확장합니다. 필터는 단순히 입력 이미지 보다 작은 공간입니다. 하지만 항상 입력 이미지와 동일한 깊이를 가집니다. 위 경우에서는 3 만큼의 깊이를 가집니다. 

 

 

이 필터를 가지고 주어진 공간적 위치에서 dot product를 합니다. 그 결과로 하나의 이미지 청크를 얻습니다. 

Q. 5 x 5 x 3 크기의 필터를 하나의 벡터로 변환하는 겁니까?
A. 맞습니다. 입력 이미지 위에 필터를 올리고 해당 위치에서 element-wise multiplication을 수행한 후 그 결과를 모두 더하는 방식으로 이해할 수 있는데 이 연산은 결국 필터를 일렬로 펼쳐 벡터 형태로 만들고 입력 볼륨의 해당 부분도 같은 방식으로 벡터로 펼친 다음 두 벡터 간의 dot product를 수행하는 것과 동일한 결과를 제공합니다.

 

 

 

가장 최상단 좌측 코너에서부터 시작해 입력 이미지를 돌며 각 위치에서 dot product를 수행합니다. 출력값은 activation map의 하나의 값이 됩니다. 가장 단순한 버전은 단순히 모든 픽셀에서 위의 작업을 수행한 후 activation map에 대응하는 위치를 채워넣는 것입니다. 위의 그림에서 볼 수 있듯이 우리가 예상한 것과는 다르게 나옵니다. 입력이 32 by 32 였는데 28 by 28의 출력이 나왔습니다. 이에 대해서는 이후에 수학적으로 실제로 어떻게 동작하는지 알아보도록 하겠습니다. 하지만 기본적으로 원하는 출력 사이즈를 얻기 위해 어떻게 슬라이드할 것인지, 모든 픽셀에 대해 계산할 것인지, 두 픽셀을 동시에 계산할 것인지에 대한 선택을 할 수 있습니다. 

 

 

방금까지는 하나의 필터를 가지고 이미지의 모든 위치를 돌아서 activation map을 얻었습니다. Convolutional Layer에서는 각 필터가 입력 데이터의 특정한 template이나 concept을 추출하도록 설계되기 때문에, 다양한 정보를 효과적으로 포착하기 위해 여러 개의 필터를 사용합니다. 따라서 우리는 여러개의 필터셋을 갖게됩니다. 위의 예에서는 두번째 필터(5 x 5 x 3 사이즈의 초록색 필터)를 가지고 모든 위치를 돌아 동일한 크기의 초록색 activation map을 얻습니다.

 

이와 동일하게 한 레이어에서 여러개의 필터를 가지고도 할 수 있습니다. 예를들어 만약 6개의 5 x 5 필터를 가지고 있다면 총 6개의 activation map을 얻을 수 있습니다. 최종적으로 해당 레이어의 출력 크기는 6 x 28 x 28이 됩니다. 

 

 

ConvNet은 기본적으로 이 Convolutional Layer들의 시퀀스입니다. 그리고 이 레이어들을 사이에 활성화함수(예를 들어 ReLU 함수)들을 끼워넣습니다. 

 



각각의 레이어들은 이전에 말했던 것처럼 많은 필터를 가집니다. 그리고 각 필터들은 activation map을 만들어냅니다. ConvNet에서는 이러한 레이더들이 여러 개 쌓이면서 필터들의 계층적인 구조를 학습하게 됩니다.

 

초기의 layer들에 있는 필터들은 보통 엣지 같은 low-lever feature들을 표현합니다. mid-level 에서는 코너나 blob 같은 좀 더 복잡한 특성들을 얻게됩니다. higher-lever에서는 단순한 형태가 아니라 점점 더 개념적인 것들을 포착하게됩니다. 이후 수업에서는 어떻게 이런 feature들을 시각화하고 네트워크가 어떤 feature들을 학습하는지 해석할것입니다. 일단 여기서 가장 중요한 부분은 이런 레이어들을 쌓을 수록 간단한 특성에서 복잡한 특성을 얻을 수 있다는 것입니다.

 

위 사진은 각 필터들이 만들어낸 activation map의 예들입니다. 빨간색 필터를 보면 oriented edge같은 template을 기대하는 것으로 보입니다. 따라서 이 필터를 이미지 위로 슬라이드하면, 이 특정 방향의 에지가 있는 영역에서 더 높은 점수 즉 더 밝은(하얀) 값을 가지게 됩니다. 그래서 각각의 activation map은 이러한 필터 중 하나를 이미지 위로 슬라이딩하면서 해당 템플릿이 이미지에 더 뚜렷하게 나타나는 위치에서 높은 출력을 가지는 결과라고 볼 수 있습니다. 

 

위 연산을 합성곱이라고 부르는 이유는 두 신호 간의 합성곱 연산과 수학적으로 연관이 있기 때문입니다. 위에 제시된 식은 합성곱 연산에 익숙한 분들에게는 익숙한 형태로 전형적인 합성곱 정의에 해당합니다.

 

실제 구현에서는 필터를 뒤집지 않고 사용하는 경우가 많아 수학적으로는 교차상관에 더 가깝지만, 이러한 차이는 비교적 미묘하며 이 강의에서는 중요한 부분이 아닙니다.

 

결국 이 연산은 하나의 필터를 이미지 위에 공간적으로 이동시키면서 각 위치에서 필터와 해당 이미지 영역 간의 dot product를 계산하는 방식으로 이루어집니다.

 

CNN의 전체적인 모습은 입력 이미지가 있고 레이어들을 순차적으로 통과합니다. CONV 레이어가 있고 이후에 비선형성 레이어가 있고, CONV RELU CONV RELU 하다가 중간중간 pooling 레이어가 있습니다. 마지막으로 나온 값을 FC 레이어를 통과시켜 score function을 얻게됩니다.


이제 공간적 차원이 어떻게 계산되는지 몇가지 예를 통해 알아보겠습니다. 

 

아까 전의 32 x 32 x 3 이미지와 5 x 5 x 3 필터를 가지고 어떻게 28 x 28 사이즈의 활성화 맵을 만들어내는지 알아보겠습니다. 

 

 

좀 더 단순하게 알아보기 위해 7 x 7 의 예를 보겠습니다.

 

위의 사진처럼 3 x 3의 필터를 픽셀 하나 단위로 왼쪽 위부터 슬라이딩 합니다. 수평으로 5 수직으로 5만큼 필터가 딱 들어맞기 때문에 최종적으로 5 x 5의 activation map을 얻게됩니다. 

 

방금 전의 예시에서는 한 픽셀 단위로 슬라이딩 했는데 이를 설계함에 있어서 여러 선택을 할 수 있습니다. 

그렇게 정한 슬라이딩 간격을 stride라고 부릅니다. 아래 예시에서 stride를 2로 했을 경우를 확인해보겠습니다.

위의 예시와 다르게 3 x 3의 activation map을 얻을 수 있습니다. 

 

이번엔 stride를 3으로 해보겠습니다.

위의 두 경우와 다르게 stride를 3으로 했을 경우에는 딱 맞지 않습니다. 이런식으로 Convolution을 할 경우에는 비대칭적인 output을 낼 것이기 때문에 이런식으로는 계산하지 않습니다. 


 

위의 식처럼 입력 이미지의 크기, 필터의 크기 그리고 stride를 통해 출력 사이즈를 계산할 수 있습니다. 위에서 했던 값들을 넣어보면 N=7, F=3, stride=3 일때 딱 맞아 떨어지지 않는 것을 확인할 수 있습니다. 

 

 

실제로는 zero-padding 기법을 사용합니다. 입력 이미지룰 0으로 pad하여 코너나 엣지에도 필터의 중심을 위치 시킬 수 있습니다. zero-padding을 통해 입력 이미지의 사이즈를 유지시킬 수 있습니다. 주로 사용하는 필터 크기인 3, 5, 7에는 1, 2, 3의 zero-pad를 사용하면 기존 입력 이미지의 사이즈를 보존할 수 있습니다. 

 

zero-padding을 사용하지 않으면  CONV RELU 레이어를 지날 때 마다 이미지의 사이즈가 빠르게 줄어들고 이는 정보 손실을 야기시킬 수 있으며 또한 코너나 엣지에 대한 정보들을 잃을 수 있습니다.

 

참고로 보통 필터의 개수는 2의 거듭제곱으로 설정함.

 

계산 예시

 

 

 


프레임워크들

 


이제 Convolutional Layer을 뇌의 뉴런 관점에서 살펴보겠습니다. Convolutional Layer에서는 이미지의 각 공간적 위치마다 필터와 해당 이미지 영역 간의 dot product를 수행하고 그 결과로 하나의 숫자를 출력하게 됩니다.

 

이는 입력값에 필터의 가중치를 곱해 결과를 얻는 방식으로 마치 필터의 가중치가 뉴런의 시냅스 역할을 하는 것과 유사합니다. 하지만 일반적인 뉴런과의 주요 차이점은 바로 Local Connectivity에 있습니다. 기존의 뉴런은 전체 입력에 연결되어 있는 반면 합성곱 계층의 뉴런은 입력 이미지의 일부분만을 참조합니다.

 

즉  뉴런은 이미지의 국소적인 영역만을 바라보고 반응하며 이 반응 정도를 각 위치마다 계산함으로써 뉴런의 활성화 정도를 공간적으로 유지한 출력, 즉 activation map을 얻게됩니다.

이를 통해 공간적 구조를 그대로 유지하면서, 이후 계층에서는 이러한 activation map을 기반으로 더 높은 수준의 추론을 수행할 수 있게 됩니다.

 

 

추가적으로 5 x 5 filter를 "5 x 5 receptive filed for each neuron" 이라고도 부릅니다.


Pooling Layer

풀링 레이어의 역할은 feature map의 공간적 크기를 줄여 연산량을 감소시키고 표현을 보다 간결하고 효율적으로 만들기 위한 목적으로 사용됩니다.

 

Pooling Layer는 그냥 공간적으로 downsample하는 겁니다. 중요한 건 depth는 전혀 안 건드리고 downsample 한다는 점입니다. 따라서 input depth와 output depth는 동일합니다.

 

흔한 방법 중 하나는 max pooling 입니다. 위의 예시에서 pooling layer 또한 filter size와 stride를 가집니다. convolution에서 했던 것과 동일하게 필터가 input volume을 돕니다. 하지만 convolution에서 dot product를 했던 것과 다르게 단순히 input volume의 해당 영역에서 최대값을 뽑습니다. 

 

Q. 보통 stride를 설정할 때  overlap(겹침)이 없도록 설정하나요?
A. 그렇습니다. 일반적으로 겹치지 않도록 stride를 설정하는 경우가 더 흔합니다. 단순하게 쉽게 이해해보면, 기본적으로 풀링의 목적이 다운샘플링이기 때문에 특정 영역을 하나의 값으로 요약하고 그 다음 겹치지 않는 영역을 또 요약하는 식으로 처리하는 것이 자연스럽고 효율적이라고 생각할 수 있습니다.

Q. 왜 max pooling이 average pooling 같은 다른 방식보다 더 좋은가요?
A. Average pooling 또한 사용할 수 있는 방법 중 하나이지만 Max pooling이 갖는 직관적인 해석 덕분에 더 자주 사용됩니다. activation map의 각 값은 해당 뉴런 또는 필터가 해당 위치에서 얼마나 강하게 반응했는지를 나타냅니다 이러한 관점에서 max pooling은 이 필터가 해당 영역에서 가장 강하게 반응한 정도를 추출하는 방식으로 해석할 수 있습니다. 
이런 방식은 recognition과 같은 작업에서 특히 직관적으로 이해될 수 있습니다. 즉, 어떤 특징(예: 밝은 점, 특정 형태 등)이 이미지의 특정 위치가 아니라 어디에서든 나타났다면, 그 전체 영역에서 강하게 반응하도록 하는 것이 의미가 있습니다. 따라서 Max pooling은 해당 특징이 존재하기만 하면 강한 신호를 전달하는 방식으로 기능하게 됩니다.

Q. pooling과 stride 둘 다 다운샘플링 효과를 가지는데 그렇다면 그냥 pooling 대신 stride를 사용하면 되는 거 아닌가요?
A. 실제로 최근의(*2017년 기준) 신경망 아키텍처들을 살펴보면, 풀링보다 스트라이드를 활용하여 다운샘플링을 수행하는 경우가 점점 더 많아지고 있습니다. 특히 스트라이드를 조절함으로써 더 유연한 구조 설계각 가능하고, fractional stride와 같은 다양한 변형도 사용할 수 있습니다. 실제로 이러한 방식이 더 나은 성능을 보이는 경우가 있으며, 따라서 스트라이드를 이용한 다운샘플링은 충분히 유효한 대안이고 실제로 많은 모델들이 이를 채택하고 있습니다. 

https://stats.stackexchange.com/questions/387482/pooling-vs-stride-for-downsampling

 

Pooling vs. stride for downsampling

Pooling and stride both can be used to downsample the image. Let's say we have an image of 4x4, like below and a filter of 2x2. Then how do we decide whether to use (2x2 pooling) vs. (stride of 2)?

stats.stackexchange.com

위 질문에 대한 답변으로 이 글을 참조해보아도 좋을 듯합니다.


 

 

보통 pooling layer에서는 직접적인 downsample을 하길 원하기 때문에 zero-padding을 사용하지 않습니다.

pooling layer에서 흔히 설정하는 값은 필터 사이즈를 2 x 2로 하고 스트라이드를 2로 하는 것입니다.

필터 사이즈 3 x 3에 스트라이드를 2로 할 수도 있지만 2 x 2의 필터 사이즈가 더 흔히 사용됩니다.


FC Layer

 

 

FC layer는 지금까지 봐왔던 Fully Connected Layer와 동일하게 작동합니다. 이 단계에서는 CNN의 마지막 계층에서 출력된 볼륨을 처리하게 됩니다. 이 볼륨은 보통 가로 x 세로 x 채널 수의 형태를 가지며, 우리는 이 출력을 1차원 벡터 형태로 펼치는 과정을 수행합니다. 이 과정을 통해 얻어진 1차원 벡터는 일반적인 fully connected layer의 입력 형식과 동일해지며, 이후에는 이 벡터를 FC layer에 입력하여 각 뉴런이 모든 합성곱 출력에 연결되도록 합니다. Convolutional Layer에서는 이미지의 공간적 구조를 유지하며 처리했지만 마지막 단계에서는 이러한 정보를 하나로 집약하여 최종적인 판단 또는 예측을 내리는 구조로 전환됩니다. 이러한 과정을 통해 얻어지는 출력은 우리가 잉전에 봤던 것과 같은 최종 점수 또는 분류 결과입니다.

 

끝!

 

오늘은 computational graphs라고 불리는 프레임워크를 사용해 무작위의 복잡한 함수에 대한 analytic gradient를 어떻게 계산하는지 얘기해봅시다.

Computational graphs

 

모든 함수들을 표현하기 위해 Computational Graphs를 사용할 수 있습니다. 여기서 노드는 각 계산과정들을 의미합니다. 위 그림에 보이는 예시는 Linear Classifier입니다. 입력 x와 W가 있고 곱하기 노드는 파라미터 W와 데이터 X에대한 행렬곱을 의미하며 출력으로 score 벡터를 내놓습니다. hinge loss 노드에서 data loss L_i를 게산합니다. R노드에서 Regularization을 계산하고 덧셈 노드에서 둘을 합해 최종 Loss 값을 계산합니다.

 

Backpropagation

역전파가 어떻게 동작하는지 아래의 쉬운 예제를 통해 알아봅시다.

모든 함수의 출력에대한 gradient를 계산해봅시다. 첫번째 단계는 항상 함수를 가지고 computational graph로 나타내는 것입니다.

위 슬라이드에서 오른쪽 사진이 그 computational graph 입니다. 이후 이 네트워크에 대한 순전파(forward pass)를 진행합니다.

 

계산이 끝난 값들에 대해 이름을 지어줍니다. 덧셈 노드의 출력에 대한 변수의 이름을 q(=x+y), 곱셈 노드의 출력에 대한 변수의 이름을 f(=qz)로 지어주겠습니다. 또 q와 f에대한 gradient를 계산해주겠습니다. 우리가 찾고자 하는 것은 x,y,z 에대한 f의 gradient입니다. 역전파는 chain rule를 재귀적으로 적용하는 것입니다. 그래서 맨끝에서의 gradient를 구하고 뒤로 돌아오면서 모든 gradient를 계산합니다.

 

 

 

f에대한 f의 gradient를 구하면 당연하게도 1입니다. 

 

 

z에 대한 gradient를 구하게되면 q와 같고 q는 3이기 때문에 z에 대한 gradient는 3입니다.

 

df/dq는 z와 같고 z는 -4이기 때문에 q에 대한 gradient는 -4입니다.

 

df/dy를 구하려고 하는데 y는 직접적으로 f와 연결되어있지 않습니다. y는 중간노드인 q를 통해 f와 연결되어있습니다. 여기서 Chain rule을 사용하여 y에 대한 gradient를 구할 수 있습니다. dq/dy 는 1이고 df/dq 는 z 즉 -4입니다. 따라서 df/dy는 -4입니다.

df/dx도 마찬가지로 계산하여 -2라는 값을 얻을 수 있습니다. 


역전파에서 기본적으로 computational graph의 노드들은 모두 각각 주변 환경만을 인식하고있습니다. 그래서 각 노드는 노드에 연결된 local input들과 직접적으로 출력되는 local ouput을 가집니다.

 

위 사진에서 입력은 x와 y, 출력은 z입니다.

 

또한 각 노드는 local gradient를 가집니다. x에대한 z의 gradient와 y에대한 z의 gradient를 계산해낼 수 있습니다. 위의 예에서 각 노드는 덧셈 혹은 곱셈 노드이기 때문에 복잡한 연산을 요구하지 않습니다. 

 

 

역전파 과정에서는 맨 마지막 노드에서 시작해서 맨 첫번째 노드까지 돌아오게됩니다. 돌아오는 과정에서 각 노드에 도달할 때마다 현재 노드의 출력에대한 이전 노드에서의  gradient를 얻게됩니다. 그래서 역전파 과정에서 각 노드에 도달할 때에 이미 z 에대한 최종 Loss L의 gradient가 계산되어있습니다. 이후 우리가 원하는 것은 이전 노드에 대한 gradient입니다.

 

이전에서 본 것처럼 chain rule을 사용하게 됩니다. 

 

이후 gradient들을 현재 노드에 연결된 바로 이전 노드로 보내게됩니다.

여기서 가장 중요한 점은 각 노드에서 계산하는 Local Gradient들을 추적하고 역전파 과정에서 뒤노드 (upstream 노드) 에서 오는 gradient 값을 받아 local gradient와 곱해 연결된 노드로 보내고, 다음 노드에서는 이러한 주변환경만을 고려하여 또 다시 뒤로 이동한다는 것입니다. 

 


 

위의 예시에 대해선 직접 계산해보시기 바랍니다.

 

 

 

한가지 짚고 넘어갈 부분은 computational graph를 만들 때 우리가 원하는 세분화된 계산 노드를 정의할 수 있다는 것입니다. 위의 예에서는 덧셈과 곱셈을 사용해 가장 간단한 방법으로 표현했습니다. 하지만 실제로는 여전히 local gradient 적어낼 수 있을 정도라면 이러한 노드들을 좀더 복잡한 노드로 묶을 수 있습니다. 하나의 예로 위의 sigmoid function이 있습니다. 이 함수는 이후 강의에서도 볼 수 있는 매우 흔한 함수입니다. 이 함수에 대한 gradient를 계산하면 꽤 괜찮은 expression을 얻을 수 있습니다.

computational graph에서 sigmoid 함수에 대한 local gradient 값을 알기 때문에 sigmoid 함수를 구성하는 노드들을 묶어 하나의 큰 노드로 대치할 수 있습니다. 여기서 중요한 점은 local gradient 값만 알 수 있다면 원하는 대로 노드를 합쳐도 된다는 것입니다. 그래서 이 모든 것은 기본적으로 더 간결하고 간단한 그래프를 얻기 위해 얼마나 많은 수학적인 계산을 하고 싶은지와 각 gradient들을 얼마나 단순하게 하고 싶은지 간의 균형을 맞추는 것입니다. 


 

 

역전파에서 add gate는 gradient 를 분배해줍니다.

max gate는 하나의 브랜치로만 gradient를 보내줍니다. 

mul gate는 다른 브랜치의 값으로 gradient를 scaling 해줍니다.


 

$$ \frac{\partial f}{\partial x} = \sum \frac{\partial f}{\partial q_i} \frac{\partial q_i}{\partial x}  $$

노드가 여러개의 브랜치와 연결되어있을 경우에는 모든 upstream gradient를 더해 계산합니다.

 


벡터의 경우엔 어떻게 해야할까??

전체적인 흐름은 동일하지만 가장 큰 하나의 차이점은 gradient가 야코비 행렬이 된다는 것입니다.

 

위와 같은 예시가 있다고 하였을 때 야코비 행렬의 사이즈는 4096 x 4096이 됩니다.

이 값은 매우 커보이지만 실제로는 미니배치를 사용하며 더 효율적이게 하기 위해 노드에 통째로 집어넣기 때문에 야코히 행렬의 사이즈는 (예를 들어 배치사이즈를 100으로 했을 경우에) 409600 x 409600 이됩니다.

사실 실제로는 대부분의 경우에 이렇게 큰 야코비 행렬을 계산할 필요는 없습니다. 

야코비 행렬은 element-wise하게 이루어지기 때문에 input의 각 요소는 대응하는 해당 ouput 요소에만 영향을 미칩니다. 따라서 야코비 행렬은 대각행렬이 될 것입니다. 실제로 야코비 행렬의 전체를 작성하고 공식화할 필요는 없으며, 출력에 대한 x의 영향을 알아내 gradient를 계산할 때 이 값을 입력하기만 하면 됩니다.


실습

아래 정리한 내용을 더 잘 이해하기 위해 직접 손으로 계산해보는 것을 추천드립니다. 

x는 n차원 벡터이고, W는 n x n 행렬이라고 해보겠습니다.

우선 첫번째로 위의 예시를 computational graph로 나타내보겠습니다.

 

W와 x에 임의의 값을 넣어주고 주어진 수식에 맞게 순전파를 진행하여 값들을 얻을 수 있습니다. 

 

이제 역전파를 진행해보겠습니다.

 

 

첫번째로 당연하게도 f에 대한 f의 gradient는 1입니다. 

 

$ f(q) = q_1^2 + \dots q_n^i$ 이므로 q_i에 대한 f의 gradient는 다음과 같습니다.

$$ \frac{\partial f}{\partial q_i} = 2q_i $$

$$\nabla_q f = 2q$$

 

 

 

 

이제 q에 대한 f의 gradient를 얻었습니다. 바로 다음으로 넘어가 W에 대한 f의 gradient를 구해보겠습니다.

구하는 과정에 아래와같이 앞서 배운 chain rule을 사용할 것입니다. 

$$ \frac{\partial f}{\partial W} = \frac{\partial f}{\partial q} \frac{\partial q}{\partial W}  $$

 

좌변의 $\frac{\partial f}{\partial q}$는 이미 위에서 구했기 때문에 $\frac{\partial q}{\partial W}$만 구하면 됩니다. 

우선 $q_k = W_{k,1}x_1 + \dots + W_{k,n}x_n$ 이므로 

$\frac{\partial q_k}{\partial W_{i,j}} = 1_{k=i}x_j$ 입니다.

 

따라서 

 

$$ \frac{\partial f}{\partial W_{i,j}} = \sum_{k} \frac{\partial f}{\partial q_k} \frac{\partial q_k}{\partial W_{i,j}} = \sum_{k} (2q_k)(1_{k=i}x_j) = 2q_ix_j $$

 

$$ \nabla_w f =2q \cdot x^t $$

 

위 수식을 바탕으로 값을 계산해보겠습니다. 

 

$$ 2q \cdot x^T = 2\begin{pmatrix}0.22 \\ 0.26\end{pmatrix} \cdot \begin{pmatrix}0.2 &  0.4\\\end{pmatrix} = \begin{pmatrix}0.088 & 0.176 \\0.104 & 0.208 \\\end{pmatrix} $$

 

 

다음으로 넘어가 x에 대한 f의 gradient를 계산해보겠습니다. 

위에서 한 방식과 동일하게 chain rule을 이용합니다. 따라서 먼저 $\frac{\partial q_k}{\partial x_i}$를 구해보겠습니다. 

$q_k = W_{k,1}x_1 + \dots + W_{k,n}x_n$ 이므로

$\frac{\partial q_k}{\partial x_i} = W_{i,x}$입니다.

 

따라서

$$ \frac{\partial f}{\partial x_i} = \sum_{k} \frac{\partial f}{\partial q_k} \frac{\partial q_k}{\partial x_i} = \sum_{k} 2q_kW_{k,i}$$

 

$$ \nabla_xf = 2W^T \cdot q $$

 

이어서 위 수식을 바탕으로 값을 계산해보겠습니다.

$$2W^T \cdot q = 2 \begin{pmatrix}0.1 & -0.3 \\0.5 &  0.8\\\end{pmatrix} \cdot \begin{pmatrix}0.22 \\0.26\end{pmatrix} = \begin{pmatrix}-0.112 \\ 0.636\end{pmatrix}$$

 

 

짜잔


Modularized implementation : forward / backward API

우리가 위에서 한 방식들은 모듈화된 구현입니다. computational graph에서 각 노드들 보고 local gradient를 계산하고 upstream gradient와 chain합니다. 그래서 이걸 forward API와 backward API로 생각할 수 있습니다. forward pass에서는 그 노드의 출력값을 계산하고 backward pass에서는 gradient를 계산하도록 구현하면됩니다. 그래서 실제로 우리가 코드로 구현할 때 동일한 방식으로 구현하게됩니다.  

class ComputationalGraph(object):
    #...
    def forward(inputs):
        # 1. [pass inputs to input gates...]
        # 2. forward the computational graph:
        for gate in self.graph.nodes_topologically_sorted():
            gate.forward()
        return loss
    def backward():
        for gate in reversed(self.graph.nodes_topologically_sorted()):
            gate.backward() # little piece of backprop (chain rule applied)
        return inputs_gradients

 

만약 전체 그래프를 가지고 있을 경우 forward pass를 위상정렬된 그래프 내의 노드를 iterate함으로써 구현할 수 있습니다.

backward pass를 구현할때는 역위상정렬된 그래프의 노드를 iterate하며 backward 함수를 call하면됩니다. 

 

 예시

https://github.com/BVLC/caffe/tree/master/src/caffe/layers

 

caffe/src/caffe/layers at master · BVLC/caffe

Caffe: a fast open framework for deep learning. Contribute to BVLC/caffe development by creating an account on GitHub.

github.com

 

caffe라고 유명한 딥러닝 프레임워크인데 layer들을 보면 위와 비슷한 모듈화 방법을 따르는 것을 볼 수 있습니다. 

 

sigmoid gate

 

sigmoid layer를 한번 살펴보겠습니다. forward pass는 동일하게 sigmoid를 계산하고 backward 에서는 top_diff(upstream gradient)를 input을 local gradient와 계산합니다.


Neural Networks

이제 Nerual Netowrk(인공신경망)에 대해 알아봅시다. 보통 사람들이 인공신경망에 대해 말할 때 뇌나 생물학적인 여러 내용들의 유사점을 끌고오는 경우가 많은데 이번에는 그런 것들 제외하고 단순히 함수로서 봐봅시다.

 

 

지금까지는 위에 보이는 것처럼 linear score function들을 다뤘습니다.
인공신경망의 가장 단순한 형태는 두 개의 함수를 이어서 구성하는 것입니다.

 

 

 

먼저 W1과 x의 행렬곱을 계산한 후, 그 결과를 비선형 함수인 maxx(0, W1x)에 통과시켜 최종 출력값을 얻습니다.

비선형성은 매우 중요합니다. 선형 레이어만 계속 쌓다보면 결국 전체 모델이 하나의 선형함수로 수렴하게 됩니다.

 

신경망은 여러개의 간단한 함수들이 서로 겹쳐 쌓여져 있는 함수의 한 종류로, 이러한 함수들이 계층적으로 배열되어 더 복잡한 비선형 함수를 형성합니다. 즉, 여러 단계의 계층적 계산을 통해 복잡한 문제를 해결할 수 있는 아이디어를 담고있습니다.

 

인공신경망을 만드는 주요한 방법은 행렬곱 같은 선형 레이어들을 반복해서 쌓고, 그 사이에 비선형 함수를 포함시키는 것입니다.

 


 

이전에 다뤘던 linear score function을 생각해보면, weight matrix W의 각 행이 input에 대한 특정 클래스에 대해 기대하는 부분을 표현하는 template 역할을 한다고 말했습니다. 그때 얘기했던 한가지 문제점은 하나의 클래스가 오직 하나의 템플릿을 가진다는 것이었습니다.

 

다층 네트워크에서는 각 중간 변수인 h나 W1이 여전히 이러한 템플릿 역할을 할 수 있습니다. 그러나 이제 우리는 h에 있는 이 템플릿들에 대한 모든 점수를 가지고 있고, 그 위에 다른 레이어를 두어 이들을 결합할 수 있습니다. 따라서 이제 자동차 클래스는 빨간색 차와 노란색 차와도 연관지을 수 있습니다. 그 이유는 W2 행렬이 h에서 얻은 벡터들을 가중합하여 이를 조합하는 역할을 하기 때문입니다.

 

Q. W1처럼 W2도 이미지로 표현가능합니까?
A. W1은 직접적으로 input image와 연결되어있기 때문에 해석이 가능하지만 h는 각 템플릿에 대한 이미지의 점수이기 때문에 표현되지 않습니다.

Q. 그럼 이제 W1은 10개가 아니라 더 많은 템플릿을 가지는 겁니까?
A. 그렇습니다. 예를 들어 왼쪽을 바라보는 말, 오른쪽을 바라보는 말 모두 W1에 포함될 수 있습니다. W2는 이 모든 템플릿들에 대한 가중합을 해 특정 클래스에 대한 최종 점수를 얻습니다. 

Q. 만약 입력 이미지가 왼쪽을 바라보는 말이고 W1에는 왼쪽을 바라보는 말, 오른쪽을 바라보는 말 둘다 포함되어있으면 어떤 일이 생깁니까?
A. 우선 h에서 왼쪽을 바라보는 말에 대한 점수가 굉장히 높을 것입니다. 반면에 오른쪽을 바라보는 말에 대한 점수는 낮을 것입니다. W2는 가중합이기 때문에 특정 템플릿에서 매우 높은 점수를 받거나, 두 개의 템플릿에서 각각 낮은 점수와 중간 점수를 받는 경우라도, 최종적으로 높은 점수가 나올 수 있습니다. 결국, 특정 유형의 말이 존재하면 전반적으로 높은 점수를 받는 경향이 생기게 됩니다.

Q. 어디가 비선형 함수입니까??
A. 보통 비선형함수는 h 바로 직전에 있습니다. 따라서 h는 비선형 함수로 생긴 값입니다. (위 그림 예시에서는 max 함수)

방금 전까지 2-layer짜리 신경망을 다뤘는데 우리는 임의의 깊이의 더 깊은 신경망을 얻기 위해 레이어들을 더 쌓을 수 있습니다. 


2-layer 신경망은 20줄 정도만으로 구현 가능합니다.

import numpy as np
from numpy.random import randn

N, D_in, H, D_out = 64, 1000, 100, 10
x, y = rand(N, D_in), randn(N,D_out)
w1, w2 = randn(D_in, H), randn(H, D_out)

for t in range(2000):
    h = 1 / (1 + np.exp(-x.dot(w1)))
    y_pred = h.dot(w2)
    loss = np.square(y_pred - y).sum()
    print(t, loss)
    
    grad_y_pred = 2.0 * (y_pred - y)
    grad_w2 = h.T.dot(grad_y_pred)
    grad_h = grad_y_pred.dot(w2.T)
    grad_w1 = x.T.dot(grad_h * h * (1 - h))
    
    w1 -= 1e-4 * grad_w1
    w2 -= 1e-4 * grad_w2

 


Biological Inspiration

신경망에 대하 얘기할 때 주로 생물학적 연관성이 언급되곤 합니다. 생물학적인 유사도가 그렇게 높진 않지만, 이러한 연관성과 영감 중 일부가 어디에서 오는지 이해하는 것도 꽤 흥미로울 수 있습니다.

 

 

 

단순하게 뉴런에 대해 생각해보겠습니다. 뉴런에는 자극이란 게 존재하고 이 자극들은 각 뉴런들을 향해 전달됩니다. 연결된 수많은 뉴런들이 존재하고 각 뉴런들은 뉴런에 들어오는 자극을 받는 dendrite(가지돌기)를 가지고 있습니다. 또한 cell body(신경세포체)는 가지돌기로 들어온 자극들을 통합하고 이후에 axon(축삭돌기)를 통해 연결된 다음 뉴런으로 자극을 보냅니다. 

 

 

지금까지 공부했던 내용들을 살펴보면, 각각의 computational node에서 뉴런과의 유사성을 볼 수 있습니다. 노드들은 서로 연결되어있고 input은 뉴런으로 들어오는 자극과 동일합니다. 각 x0, x1, x2는 예를 들어 가중치 W를 통해 통합됩니다. 이후 activation function을 통해 얻은 값을 출력으로 내보내게 됩니다. 

 

위의 활성화 함수를 보면 기본적으로 모든 input 값들을 취해서 하나의 숫자를 출력합니다. 이전에 활성화 함수의 예 중 하나로 sigmoid activation function과 여러 비선형성을 다뤘습니다. 

 

생각해낼 수 있는 조금 느슨한 비유 중 하나는 이런 비선형성이 뉴런의 firing이나 spiking rate를 표현할 수 있다는 것입니다. 뉴런의 연결된 뉴런으로의 신경전달은 이런 discrete spikies를 사용합니다. 만약 spiking이 매우 빠르다면 이후에 전달되는 강한 신호를 갖습니다. 

 

실제로 이런 내용들을 연구하는 신경학자들은 뉴런들의 동작과 가장 비슷한 비선형성의 한 종류가 ReLU 함수라고 말합니다. 이후에 다룰 함수고 이 함수는 모든 음의 값 입력에 대해 0이고 양의 값에 대해서는 선형 함수입니다.

 

지금까지 다룬 생물학적인 내용을 만들어낼 때 굉장히 조심해야합니다. 실제 생물학적 뉴런은 훨씬 더 복잡합니다. 다양한 종류의 뉴런들이 존재하고 가지돌기들은 정말 복잡한 비선형성 계산을 수행할 수 있습니다. 또한 시냅스들은 단순히 하나의 가중치가 아니라 복잡한 비선형 동적 시스템입니다. 또 활성화 함수를 rate code 나 firing rate로 해석하는 것은 적합하지 않습니다. 


Activation Functions

 

 

많은 활성화 함수들이 존재하고 이 함수들에 대해선 이후에 자세하게 다뤄보겠습니다.


 

또 여러 종류의 신경망 아키텍처에 대해 다룰것입니다.

왼쪽 위의 예시를 우리는 2-layer Neural Net라고 불렀는데 1-hidden-layer Nueral Net이라고도 부를 수 있습니다. 행렬곱 횟수를 세는 것 대신에 hidden layer의 개수를 셉니다. 두 용어 다 사용해도 되고 보통은 2-layer Neural Net을 흔히 사용합니다. 오른쪽 예도 마찬가지로 3-layer Neural Net 이나 2-hidden0layer Neural Net이라고 부를 수 있습니다. 


Example feed-forward computation of a neural network

 

신경망에서 순전파를 할 때, 네트워크 안의 각 노드는 이전에 보여줬던 뉴런과 같은 연산을 수행합니다.

은닉층은 여러 개의 뉴런들로 이루어진 하나의 벡터(또는 배열) 처럼 생각할 수 있고, 이 뉴런들의 출력을 계산할 때는 행렬곱셉을 사용해 효율적으로 한번에 전체 층의 출력을 계산할 수 있습니다. 예를 들어, 은닉층에 10개나 50개 혹은 100개의 뉴런이 있더라도 한번의 행렬곱 연산으로 이 모든 뉴런의 출력을 구할 수 있다는 뜻입니다.

 

 

다시 정리해 보면, 신경망을 행렬-벡터 형태로 표현하면 위 그림처럼 구성됩니다. 먼저 입력 데이터 x (입력 벡터)에 첫번째 가중치 행렬 W1을 곱합니다. 그리고 그 결과에 비선형 함수 f (sigmoid 함수)를 적용합니다. 그다음 두 번째 가중치 행렬을 곱해서 두번째 은닉층 h2를 계싼하고 마지막에는 출력층에 도달하게 됩니다. 이런식으로 신경망의 순전파를 구성할 수 있고 역전파를 사용해서 가중치에 대한 기울기들으 계산하면 신경망을 학습시킬 수 있습니다. 이것이 기본적으로 신경망의 핵심 개념입니다. 

 

끝!

 

 

단순하게 한번에 minima를 찾을 수 있으면 좋겠지만. loss function에 regluarizer까지 엄청 커지고 복잡해지고 거기에 neural networks까지 사용한다면 minima까지 직접적으로 접근해주는 명시적으로 계산할 수 있는 수학적 해를 찾아내기는 어려울 것입니다. 그래서 실제로는 iterative한 방법을 사용합니다. 


첫번째로 생각할만한 가장 멍청한 방법은 random search입니다.

bestloss = float("inf") #파이썬이 가능한 가장 큰 float value를 대입해줌
for num in xrange(1000):
	W = np.random.randn(10, 3073) * 0.0001
    loss = L(X_train, Y_train, W)
    if loss < bestloss:
    	bestloss = loss
        bestW = W
    print `in attempt %d the loss was %f, best %f` % (num, loss, bestloss)

 

보시다시피 굉장히 쓰레기 알고리즘이고 절대 사용하면 안됩니다.

 

두번째로 경사를 따라가는 방법입니다.

산에서 가장 낮은 지점을 찾아가려고 할때 내가 서있는 위치에서 낮은 경사쪽으로 가다보면 도달할 수 있을 것 같습니다. 이를 적용해보았을 때 상대적으로 간단한 알고리즘 같아보이지만 실제로 꽤 잘 작동합니다.

1차원에서의 미분은 꽤 자연스럽게 다변수함수에서도 일반화됨.
실제로 우리는 스칼라가아닌 벡터를 다룹니다. 따라서 저 공식을 일반화할 필요가 있습니다. 다변수 환경에서 미분의 일반화는 gradient입니다, gradient는 편미분들의 vector입니다, 따라서 gradient는 x와 같은 모양을 가집니다. 그리고 gradient의 각요소들은 그 좌표 방향으로 움직였을때 우리의 함수f들의 기울기가 무엇인지 말해줍니다. gradient의 장점은, gradient가 편미분들의 vector이지만 함수가 가장 크게 증가하는 방향을 가리키고, 이에 따라서 함수가 가장 크게 감소하는 방향은 음의 gradient를 보면 알 수 있습니다.그리고 더 일반적으로 만약 어떠한 방향에서는 내 landscape의 기울기를 알고 싶다면 그건 gradient와 unit vector의 dot product와 동일합니다. gradient는 현재 위치에서의 함수에 대한 선형 일차 근사값을 주기 때문에 gradient는 매우 중요합니다. 그래서 실제로 많은 딥러닝은 함수의 gradient를 계산하고 그 gradient를 사용해 계속해서 파라미터 벡터를 업데이트하는 것에 관한 것입니다.


컴퓨터에서 gradient를 naive하게 구하려면 유한차분을 사용하면 됩니다.

gradien dW를 계산해보겠습니다. 위 슬라이드에서 W의 첫번째 원소를 h라는 작은 값만큼 증가시키고 다시 loss 값을 계산합니다. 이제 극한 공식에 따라 첫번쨰 원소에 대한 gradient를 계산합니다. 이제 나머지 모든 원소들도 반복적으로 동일하게 계산하면 됩니다.

하지만 이런 방식은 매우 느리기 때문에 좋지 않습니다. 만약 엄청 큰 cnn모델이라고 생각한다면 이 함수 f를 계산하는 데에 오랜 시간이 걸리게됩니다. 그리고 파라미터 벡터 W는 위처럼 고작 10개의 원소만을 가지지 않고 천만 혹은 억 단위의 원소를 가질 것입니다.  유한차분 방식은 단 하나의 gradient를 얻기 위해 매우 오랜 시간을 기다려야하기 때문에 실제로는 사용하지 않습니다.


 

실제로는 W의 모든 차원을 돌기보다는 먼저 gradient에 대한 분석적인 표현(analytic gradient)를 찾고 W로부터 직접 dW 혹은 gradient를 계산합니다. 

위의 numeric gradient도 좋아보이긴 하지만 실제로는 analytic gradient를 거의 항상 사용합니다.

analytic expression이 맞는지 확인하고 싶으면 numeric gradient를 사용해 디버깅 해볼 수 있습니다.


Gradient Descent

while True:
    weights_grad = evaluate_gradient(loss_fun, data, weights)
    weights += - step_size * weights_grad

 

step_size는 hyperparameter입니다. gradient를 계산할때 그 방향으로 얼마나 멀리 갈지 정해줍니다, 가끔 learning_rate라고도 부릅니다. 가장 중요한 hyper parameter로 항상 첫번째로 체크해야할 하이퍼파라미터입니다. model size나 regularization strength는 나중에 체크하고 learning rate를 올바르게 하는 것이 첫번째입니다. 

 

 

2차원의 간단한 예시를 살펴보겠습니다. 이 bowl은 우리의 loss function을 보여줍니다. 빨간 영역은 우리가 원하는 낮은 loss 값 영역, 파란색 초록색으로 갈 수록 우리가 피해야하는 높은 loss 영역입니다. W를 랜덤한 영역에서 시작해보겠습니다. 우선 negative gradient direction을 계산할거임, 그리고 그렇게 계산한게 결국 minima로 방향을 가리키길 기대하며 이걸 계속 반복하다보면 실제 minima에 도달하게됩니다.


SGD

 

 

우리는 loss가 우리의 classifier가 각각의 single training example에 대해 얼마나 나쁘게 동작하는지 계산하는 것이라고 정의했고 전체 loss는 전체 training set의 loss 값의 평균이라고 얘기했습니다. 하지만 실제로 N은 매우매우 큽니다. 만약 ImageNet dataset을 예로 들면 N은 130만입니다. 그래서 이런 loss를 계산하는 것은 비용이 매우 크고 이 함수에 대해 수백만번의 계산을 요구할것입니다. 따라서 gradient를 계산하는 건 전체 training set을 iterate해야하기 때문에 매우매우 오래 걸립니다. 그래서 W의 update를 매우매우 오래 기다려야합니다. 그래서 실제로는 stochastic gradient descent라고 불리는 걸 사용합니다. 전체 training set의 loss와 gradient를 계산하기보다는 각각의 iteration에서 우리는 training set의 작은 set을 sample하고 이를 minibatch라고 부릅니다. 보통 배치사이즈는 2의 제곱 (32/64/128/...)로 정합니다. 그리고 우리는 이 작은 minibatch를 전체 합의 추정값과 실제 gradient의 추정값을 계산하기 위해 사용합니다. 이게 왜 stochastic이냐면 실제 값의 기대치에 대한 monte carlo 추정으로 볼 수 있기 때문입니다.

while True:
    data_batch = sample_training_data(data, 256)
    weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
    weights += - step_size * weight_grad

 

아까 알고리즘보다 더 좋아진 것 같은데 아직도 4줄밖에 안됩니다.
minibatch에 대한 loss를 계산하고 이 loss와 gradient의 추정치에 기반해서 파라미터 W를 업데이트합니다.


 

http://vision.stanford.edu/teaching/cs231n-demos/linear-classify/

 

Multiclass SVM optimization demo

Parameters \(W,b\) are shown below. The value is in bold and its gradient (computed with backprop) is in red, italic below. Click the triangles to control the parameters. Multiclass SVM loss formulation:

vision.stanford.edu

위 사이트에서 하이퍼파라미터를 바꿔가며 시각적으로 모델의 학습과정을 확인할 수 있습니다.


Image Feature

지금까지 linear classifier에 대해 얘기해보았는데, 지금까지는 단순히 raw image pixel을 가지고 그 이미지 픽셀 자체를 linear classifier에 넣었습니다. 하지만 이건 multi-modality 같은 것들 때문에 별로 좋지 않습니다. 실제로 raw pixel 값을 linear classifier에게 주는(feeding) 건 잘 동작하지 않습니다. deep neural networks 지배 이전에는 꽤 흔했던 두 단계의 approcch가 있었습니다. 이미지를 가져와서 우선 다양한 feature representations을 계산합니다.(아마도 다양한 종류의 appearance에 관한 quantity들입니다.) 그리고 각각의 feature vector들을 이어붙입니다.(concatenate) 그리고 그 이미지의 feature representation을 linear classifier에게 줍니다.

왼쪽과 같은 training data set이 있다고 해보겠습니다. 빨간색 점들과 파란색 점들을 구분할 수 있는 선형 결정 경계(linear decision boundary)를 그리는 방법이 존재하지 않습니다. 하지만 극좌표계로 바꾸는 feature transform을 한 이후에는 이 복잡한 데이터셋이 실제로 선형적을 구분가능하게되었습니다. 그리고 linear classifier에 의해 정확하게 classify될 수 있게 됐습니다. 가장 중요한 요령은 관심 있는 문제에 대한 올바른 quantity를 계산하는 올바른 feature transform이 뭔지 알아내는 것입니다. 예를 들어 이미지에서 각 픽셀을 극좌표계로 변환하는 건 말이 안되지만 말이될것같은 이미지의 feature representation들을 적어내는 건 가능합니다.

 

이런 feature representation 중의 한 예시로는 color histogram이 있습니다.


Color Histogram

큰 색깔 스펙트럼을 가지고 각각의 bucket을 나눠서 각 픽셀들을 해당하는 색깔 bucket중 하나로 mapping합니다. 그리고 얼마나 많은 pixel들이 각각의 버켓에 들어갔는지 셉니다. 이것은 전반적으로 이미지 안에 어떤 색깔들이 있는지 알려줍니다.

 

 

Histogram of Oriented Gradients (HoG)

neural networks 이전에 볼 수 있는 또다른 흔한 종류의 feature vector는 HoG (Histogram of Oriented Gradients)입니다.

예전 강의에서 hubbel과 Wiesel이 oriented edge들이 사람의 시각 시스템에서 중요하다는 것을 발견해냈다고 하였습니다. 그리고 HoG feature representations은 같은 방법을 capture하려고 했고 이미지에서 edge의 local orientation 측정하려고했습니다. 이미지를 작은 8x8 픽셀 크기의 작은 영역으로 나눈 뒤, 각각의 영역에서 지배적인 edge의 방향을 계산하고 이를 bucket으로 양자화하였습니다. 각각의 픽셀 영역에서 다양한 edge orientation으로 히스토그램을 계산합니다. 이렇게 계산된 모든 8 x 8 영역의 히스토그램을 결합하면 이미지 전체에 대한 full-feature vecotr가 됩니다. 예를 들어 나뭇잎은 지배적으로 대각선 방향의 edge를 많이 포함하고 있습니다. 따라서 나뭇잎 이미지의 oriented gradient features 히스토그램을 시각화하면 다양한 대각선 edge 패턴이 두드러지게 나타납니다. 꽤 최근까지도(2017년 기준) object recognition에서 많이 사용된 기법 중 하나입니다.

 

 

Bag of Words

또 다른 아이디어는 Bag of Words입니다.

 

 

이 방법은 자연어 처리에서 영향을 받았습니다. 문장을 feature vector로 표현하는 한가지 방법은 문장 속 단어들의 출현빈도를 세는 것입니다. 이러한 아이디어를 이미지 데이터에도 적용하고 싶었지만, 단순히 동일한 방식으로는 구현하기 어려웠습니다. 따라서, 우리만의 visual words vocabulary를 정의할 필요가 있었습니다. 이를 위해 주요한 두 가지 단계가 있습니다.

 

1. 대규모 이미지 데이터셋을 수집하고, 매우 작은 크기의 랜덤 패치를 대량으로 샘플링합니다.

2, 이 샘플들을 K-means와 같은 군집화 기법을 사용하여 여러 개의 클러스터로 그룹화합니다.

 

오른쪽 샘플들을 보면 클러스터링 이후, visual words가 다양한 색깔(red, blue, yellow...)과 다양한 종류의 oriented edges를 효과적으로 포착하고있음을 확인할 수 있습니다. 

 

흥미로운 점은, 이렇게 하면 oriented edges를 완전히 data-driven 방식으로 학습할 수 있다는 것입니다. 즉, 사람이 직접 정의하는 것이 아니라 데이터로부터 시각적 특징을 자동으로 추출할 수 있습니다.

 

이제 visual words의 집합(code book)을 얻은 후에는, 이미지 내에서 각 visual word가 얼마나 자주 등장하는지를 기반으로 이미지를 인코딩할 수 있습니다. 이를 통해, 이미지의 visual appearance에 대한 새로운 정보를 효과적으로 제공할 수 있습니다.


다음 시간에는 neural network와 backpropagation을 배워봅시다.!

+ Recent posts