Caffe Tutorial 5. Solver
Solver
Solver는 model의 최적화를 조율합니다. 이 과정에서 network의 loss를 향상시키기 위해서 전방향 추론(forward inference)와 파라미터의 업데이트를 위한 역방향(backward) 그래디언트 계산을 조정합니다. 학습(learning)을 좌우하는 것은 최적화와 파라미터 갱신을 생성하는 Solver와 loss와 그래디언트를 생성하는 Net 두가지입니다.
Caffe의 solver는 다음의 것들이 준비되어있습니다.
- Stochastic Gradient Descent (
type: "SGD"
) - AdaDelta (
type: "AdaDelta"
) - Adaptive Gradient (
type: "AdaGrad"
) - Adam (
type: "Adam"
) - Nesterov's Accelerated Gradient (
type: "Nesterov"
) - RMSprop (
type: "RMSProp"
)
Solver는 다음의 역할들을 수행합니다.
- 최적화 기록을 위한 뼈대를 준비하고 학습을 위한 training network와 평가를 위한 test network를 생성합니다.
- 전방향 / 역방향 진행과 파라미터 갱신을 통하여 반복적으로 최적화 작업을 수행합니다.
- (주기적으로) test network를 평가합니다.
- 최적화 중에 model과 solver의 상태를 저장해둡니다.
이러한 작업들은 아래에 열거된 시점의 각 반복에서 수행됩니다.
- 출력과 loss를 계산하기 위한 network 전방향 루틴 호출
- 그래디언트 계산을 위한 network 역방향 루틴 호출
- 그래디언트를 solver method에 적용하여 파라미터를 업데이트 할 때
- 학습률(learning rate), 학습추세, 방법에 따라 solver의 상태를 갱신할 때
이러한 부분은 model의 초기화부터 학습이 완료될 때까지 모든 가중치를 취할때마다 반복됩니다.
Caffe의 model과 마찬가지로, solver또한 CPU / GPU 모드에서 실행됩니다.
Methods
Solver method는 loss의 최소화를 위한 일반적인 최적화 방법을 제시합니다. 데이터셋 $D$가 있다 가정할 때, 목적 함수의 최적화는 모든 $|D|$개 데이터의 평균 loss가 됩니다. 이는 다음과 같습니다.
$$ L(W) = \frac{1}{|D|} \sum_i^{|D|} f_W\left(X^{(i)}\right) + \lambda r(W) $$
여기서 $f_W\left(X^{(i)}\right)$는 데이터 $X^{(i)}$의 loss를 나타내고, $r(W)$는 가중치 $\lambda$와 곱해지는 정규화 항입니다. $|D|$는 매우 처질 수 있으므로, 실제로는 각 solver의 반복마다 목적함수의 stochastic approximation을 하기 위해 $N \lt\lt |D| $개의 미니배치를 뽑아서 사용합니다.
$$ L(W) \approx \frac{1}{N} \sum_i^N f_W\left(X^{(i)}\right) + \lambda r(W) $$
Model은 전방향 진행 시 $f_W$를 계산하고, 역방향 진행에서 그래디언트 $\nabla f_W$를 계산합니다.
파라미터 업데이트 $\nabla W$는 에러 그래디언트 $\nabla f_W$, 정규화 그래디언트 $\nabla r(W)$와 다른 기타 항들로부터 solver에 의해 생성됩니다.
SGD
Stochastic gradient descent(type: "SGD"
) 는 그래디언트의 반대 방향 $\nabla L(W)$과 이전에 가중치가 갱신량 $V_t$의 선형 조합으로 가중치 $W$를 업데이트합니다. 학습률 $\alpha$는 역 그래디언트에 주는 가중치가 됩니다. 모멘텀 $\mu$는 이전 업데이트의 가중치를 주는 역할을 합니다.
다시 말하자면, $t+1$번째 반복에서 이전에 가중치 갱신량 $V_t$와 현재 가중치 $W_t$가 주어졌을 경우, 갱신될 가중치 량 $V_{t+1}$과 갱신 이후 가중치 결과 $W_{t+1}$를 다음의 수식으로 계산합니다.
$$ V_{t+1} = \mu V_t - \alpha \nabla L(W_t)$$
$$ W_{t+1} = W_t + V_{t+1} $$
최상의 결과를 위해서는 학습의 "하이퍼파라미터(hyperparameters)" ($\alpha$, $\mu$)의 튜닝이 필요합니다. 어디서부터 시작해야할지 모르겠다면 아래의 "Rules of thumb"를 살펴보기 바랍니다. 더 많은 정보는 Leon Bottou의 Stochastic Gradient Descent Trick을 참조하면 됩니다.
Rules of thumb for setting the learning rate $\alpha$ and momentum $\mu$
SGD를 이용하여 딥러닝을 할 때 좋은 전략은 학습률 $\alpha$를 $0.01 = 10^{-2}$부근에서 초기화한 다음, 훈련시키는 동안 loss가 명백하네 plateau에 다다랐을 때 상수 부분 10을 감소시키는 작업을 몇번 수행하는 것입니다. 또한 일반적으로 모멘텀 $\mu = 0.9$ 혹은 비슷한 값으로 사용합니다. 모멘텀은 반가중치 갱신을 부드럽게 만들어주어 SGD를 이용한 딥러닝이 안정적이고 빠르게 동작하도록 만들어 줍니다.
이것은 Krizhevsky가 ILSVRC-2012 대회에서 CNN으로 우승한 유명한 전략입니다. Caffe는 이 전략을 SolverParameter
에서 구현하기 쉽도록 ./examples/imagenet/alexnet_solver.prototxt
에 구현해 두었습니다.
학습률 정책을 위와 같이 사용하려면 아래의 몇줄을 Solver prototxt에 아무데나 적어두면 됩니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
base_lr: 0.01 # begin training at a learning rate of 0.01 = 1e-2 lr_policy: "step" # learning rate policy: drop the learning rate in "steps" # by a factor of gamma every stepsize iterations gamma: 0.1 # drop the learning rate by a factor of 10 # (i.e., multiply it by a factor of gamma = 0.1) stepsize: 100000 # drop the learning rate every 100K iterations max_iter: 350000 # train for 350K iterations total momentum: 0.9 |
위와 같은 세팅으로 모멘텀 $\mu=0.9$을 고정하여 사용해보겠습니다. $\alpha=0.01=10^{-2}$의 base_lr
로부터 훈련을 시작하여 처음 100,000번 반복을 한 후에 학습률에 gamma
($\gamma$)를 곱하여 $\alpha' = \alpha \gamma = (0.01) (0.1) = 0.001 = 10^{-3}$으로 다음 반복 100,000-200,000번째을 훈련할 것입니다. 다음 $\alpha'' = 10^{-4}$을 이용하여 200,000-300,000번에서 사용하고 마지막으로 $\alpha''' = 10^{-5}$
을 이용하여 350,000번까지 훈련시킬 것입니다. 이는 max_iter: 350000
로 설정하였기 때문입니다.
모멘텀 $\mu$는 여러 반복 이후에 업데이트의 크기를 $ \frac{1}{1 - \mu}$ 로 하여 곱하는 것이 효과적입니다. 따라서 $\mu$를 증가시키면 $\alpha$를 같이 줄여주는 것이 좋습니다. 그리고 반대의 경우도 마찬가지 입니다.
예를 들면, $\mu=0.9$를 사용할 때에는 업데이트 크기를 $\frac{1}{1 - 0.9} = 10$로 하는 것이 효과적입니다. 모멘텀은 $\mu=0.99$로 사용할 경우 업데이트 크기를 100으로 하는 것이 좋으며, 이에 따라 $\alpha$(base_lr
)을 10 단위로 줄여주는 것이 좋습니다.
위의 세팅들은 단지 가이드라인일 뿐이며, 모든 경우에 대해서 최적이라고 결코 이야기할 수 없습니다. 심지어 잘 동작하지 않을 수도 있습니다. 학습이 발산해버리면, 그러니까 출력이나 loss 값이 매우 커지거나 NaN
, inf
가 되어버릴 경우, base_lr
을 줄이고 (base_lr: 0.001
) 다시 시도해보아야 합니다. 적절한 base_lr
값을 찾을 때 까지 이를 반복해보세요.
AdaDelta
AdaDelta(type: "AdaDelta"
)는 "강인한 학습률 방법" 입니다. 이는 SGD와 마찬가지로 그래디언트 기반의 최적화 방법입니다. 업데이트 수식은 아래와 같습니다.
$$ \begin{align} (v_t)_i &= \frac{\operatorname{RMS}((v_{t-1})_i)}{\operatorname{RMS}\left( \nabla L(W_t) \right)_{i}} \left( \nabla L(W_{t'}) \right)_i \\ \operatorname{RMS}\left( \nabla L(W_t) \right)_{i} &= \sqrt{E[g^2] + \varepsilon} \\ E[g^2]_t &= \delta{E[g^2]_{t-1} } + (1-\delta)g_{t}^2 \end{align} $$
$$ (W_{t+1})_i = (W_t)_i - \alpha (v_t)_i $$
AdaGrad
Adaptive Gradient (type: "AdaGrad"
)는 SGD와 같은 그래디언트 기반의 최적화 방법으로 Duchi의 말에 따르면 "예측할 수는 있으나 거의 특징이 보이지 않는 상황의 모래 속에서 바늘을 찾는 것"과 같은 시도를 하는 방법입니다. 이전까지의 모든 반복 $t' \in \{1, 2, ..., t\}$에서 비롯된 업데이트 정보($\left( \nabla L(W) \right)_{t'}$)를 이용하여 아래 수식을 이용하여 가중치 $W$의 각 컴포넌트 $i$를 업데이트하게 됩니다.
$$ (W_{t+1})_i = (W_t)_i - \alpha \frac{\left( \nabla L(W_t) \right)_{i}}{ \sqrt{\sum_{t'=1}^{t} \left( \nabla L(W_{t'}) \right)_i^2} } $$
AdaGrad는 가중치 $W \in \mathcal{R}^d$의 계층적 그래디언트 정보를 저장하여야 합니다. 각각의 정보를 그대로 저장한다면 $\mathcal{O}(dt)$의 공간이 필요하지만 실제로 Caffe에서는 $\mathcal{O}(d)$수준의 추가적인 공간만이 필요합니다.
Adam
Kingma가 제안한 Adam (type: "Adam"
)은 SGD와 같은 그래디언트 기반의 방법으로, adaptive moment estimation($m_t, v_t$)를 포함하며 일반화된 AdaGrad로 생각할 수 있습니다. 갱신 수식은 아래와 같습니다.
$$ (m_t)_i = \beta_1 (m_{t-1})_i + (1-\beta_1)(\nabla L(W_t))_i,\\ (v_t)_i = \beta_2 (v_{t-1})_i + (1-\beta_2)(\nabla L(W_t))_i^2 $$
$$ (W_{t+1})_i = (W_t)_i - \alpha \frac{\sqrt{1-(\beta_2)_i^t}}{1-(\beta_1)_i^t}\frac{(m_t)_i}{\sqrt{(v_t)_i}+\varepsilon} $$
Kingma는 $\beta_1 = 0.9, \beta_2 = 0.999, \epsilon = 10^{-8}$를 기본 값으로 사용할 것을 제안하였습니다. Caffe는 이들 값을 각각 momentum, momentum2, delta
의 이름으로 사용합니다.
NAG
Nesterov의 accelerated gradient (type: "Nesterov"
)는 컨백스 최적화를 위한 최선의 방법으로 Nesterov가 제안한 것으로 $\mathcal{O}(1/t)$보다 더 빠른 $\mathcal{O}(1/t^2)$의 수렴률을 달성한 방법입니다. 이를 달성하기 위해 필요한 가정, 즉 스무스하지 않고, 컨벡스하지 않기 때문에 Caffe를 이용한 심층 네트워크에 맞지 않더라도 실제로 NAG를 사용해보면 심층 학습 구조의 최적화에도 아주 효과적인 방법입니다. 이는 심층 Sutskever의 MNIST autoencoder에 잘 나타나 있습니다.
가중치 업데이트 수식은 SGD의 그것과 매우 비슷합니다.
$$ V_{t+1} = \mu V_t - \alpha \nabla L(W_t + \mu V_t) $$
$$ W_{t+1} = W_t + V_{t+1} $$
SGD와의 차이점은 에러의 그래디언트 $\nabla L(W)$를 계산할 때의 가중치 세팅 $W$입니다. NAG에서는 모멘텀이 더해진 가중치 $\nabla L(W_t + \mu V_t)$의 그래디언트를 사용합니다. SGD에서는 단순히 현재 가중치의 그래디언트 $\nabla L(W_t)$를 사용하고 있었습니다.
RMSprop
RMSprop (type: "RMSProp"
)는 Coursera 강의에서 Tieleman가 제안한 것으로 SGD와 같이 그래디언트 기반의 방법입니다. 업데이트 수식은 아래와 같습니다.
$$ \operatorname{MS}((W_t)_i)= \delta\operatorname{MS}((W_{t-1})_i)+ (1-\delta)(\nabla L(W_t))_i^2 \\ (W_{t+1})_i= (W_{t})_i -\alpha\frac{(\nabla L(W_t))_i}{\sqrt{\operatorname{MS}((W_t)_i)}} $$
$\delta$(rms_decay
)의 기반 값은 $\delta=0.99$입니다.
Scaffolding
Solver scaffolding은 Solver::Presolve()
에서 최적화 알고리즘을 준비하고 model이 학습할 수 있도록 초기화합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
> caffe train -solver examples/mnist/lenet_solver.prototxt I0902 13:35:56.474978 16020 caffe.cpp:90] Starting Optimization I0902 13:35:56.475190 16020 solver.cpp:32] Initializing solver from parameters: test_iter: 100 test_interval: 500 base_lr: 0.01 display: 100 max_iter: 10000 lr_policy: "inv" gamma: 0.0001 power: 0.75 momentum: 0.9 weight_decay: 0.0005 snapshot: 5000 snapshot_prefix: "examples/mnist/lenet" solver_mode: GPU net: "examples/mnist/lenet_train_test.prototxt" |
Net 초기화
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
I0902 13:35:56.655681 16020 solver.cpp:72] Creating training net from net file: examples/mnist/lenet_train_test.prototxt [...] I0902 13:35:56.656740 16020 net.cpp:56] Memory required for data: 0 I0902 13:35:56.656791 16020 net.cpp:67] Creating Layer mnist I0902 13:35:56.656811 16020 net.cpp:356] mnist -> data I0902 13:35:56.656846 16020 net.cpp:356] mnist -> label I0902 13:35:56.656874 16020 net.cpp:96] Setting up mnist I0902 13:35:56.694052 16020 data_layer.cpp:135] Opening lmdb examples/mnist/mnist_train_lmdb I0902 13:35:56.701062 16020 data_layer.cpp:195] output data size: 64,1,28,28 I0902 13:35:56.701146 16020 data_layer.cpp:236] Initializing prefetch I0902 13:35:56.701196 16020 data_layer.cpp:238] Prefetch initialized. I0902 13:35:56.701212 16020 net.cpp:103] Top shape: 64 1 28 28 (50176) I0902 13:35:56.701230 16020 net.cpp:103] Top shape: 64 1 1 1 (64) [...] I0902 13:35:56.703737 16020 net.cpp:67] Creating Layer ip1 I0902 13:35:56.703753 16020 net.cpp:394] ip1 <- pool2 I0902 13:35:56.703778 16020 net.cpp:356] ip1 -> ip1 I0902 13:35:56.703797 16020 net.cpp:96] Setting up ip1 I0902 13:35:56.728127 16020 net.cpp:103] Top shape: 64 500 1 1 (32000) I0902 13:35:56.728142 16020 net.cpp:113] Memory required for data: 5039360 I0902 13:35:56.728175 16020 net.cpp:67] Creating Layer relu1 I0902 13:35:56.728194 16020 net.cpp:394] relu1 <- ip1 I0902 13:35:56.728219 16020 net.cpp:345] relu1 -> ip1 (in-place) I0902 13:35:56.728240 16020 net.cpp:96] Setting up relu1 I0902 13:35:56.728256 16020 net.cpp:103] Top shape: 64 500 1 1 (32000) I0902 13:35:56.728270 16020 net.cpp:113] Memory required for data: 5167360 I0902 13:35:56.728287 16020 net.cpp:67] Creating Layer ip2 I0902 13:35:56.728304 16020 net.cpp:394] ip2 <- ip1 I0902 13:35:56.728333 16020 net.cpp:356] ip2 -> ip2 I0902 13:35:56.728356 16020 net.cpp:96] Setting up ip2 I0902 13:35:56.728690 16020 net.cpp:103] Top shape: 64 10 1 1 (640) I0902 13:35:56.728705 16020 net.cpp:113] Memory required for data: 5169920 I0902 13:35:56.728734 16020 net.cpp:67] Creating Layer loss I0902 13:35:56.728747 16020 net.cpp:394] loss <- ip2 I0902 13:35:56.728767 16020 net.cpp:394] loss <- label I0902 13:35:56.728786 16020 net.cpp:356] loss -> loss I0902 13:35:56.728811 16020 net.cpp:96] Setting up loss I0902 13:35:56.728837 16020 net.cpp:103] Top shape: 1 1 1 1 (1) I0902 13:35:56.728849 16020 net.cpp:109] with loss weight 1 I0902 13:35:56.728878 16020 net.cpp:113] Memory required for data: 5169924 |
Loss
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
I0902 13:35:56.728893 16020 net.cpp:170] loss needs backward computation. I0902 13:35:56.728909 16020 net.cpp:170] ip2 needs backward computation. I0902 13:35:56.728924 16020 net.cpp:170] relu1 needs backward computation. I0902 13:35:56.728938 16020 net.cpp:170] ip1 needs backward computation. I0902 13:35:56.728953 16020 net.cpp:170] pool2 needs backward computation. I0902 13:35:56.728970 16020 net.cpp:170] conv2 needs backward computation. I0902 13:35:56.728984 16020 net.cpp:170] pool1 needs backward computation. I0902 13:35:56.728998 16020 net.cpp:170] conv1 needs backward computation. I0902 13:35:56.729014 16020 net.cpp:172] mnist does not need backward computation. I0902 13:35:56.729027 16020 net.cpp:208] This network produces output loss I0902 13:35:56.729053 16020 net.cpp:467] Collecting Learning Rate and Weight Decay. I0902 13:35:56.729071 16020 net.cpp:219] Network initialization done. I0902 13:35:56.729085 16020 net.cpp:220] Memory required for data: 5169924 I0902 13:35:56.729277 16020 solver.cpp:156] Creating test net (#0) specified by net file: examples/mnist/lenet_train_test.prototxt |
완료
1 2 3 4 |
I0902 13:35:56.806970 16020 solver.cpp:46] Solver scaffolding done. I0902 13:35:56.806984 16020 solver.cpp:165] Solving LeNet |
Updating Parameters
실제 가중치 업데이트는 solver에 의해 만들어진 다음 Solver::ComputeUpdateValue()
에서 net 파라미터로 적용됩니다. ComputeUpdateValue
메서드는 weight decay($r(W)$)와 통합되어 각 네트워크의 가중치에 대하여 최종 그래디언트를 얻기 위한 가중치 그래디언트(현재는 에러의 그래디언트)로 변환됩니다. 다음 그 그래디언트는 학습률 $\alpha$로 스케일링되어 뺄셈을 위해 Blob의 diff
필드의 각 파라미터에 저장됩니다. 마지막으로 각 파라미터 blob의 Blob::Update
메서드를 실행하여 최종 업데이트(data
에서 diff
를 빼는 작업)를 수행합니다.
Snapshotting and Resuming
Solver는 훈련 중 가중치와 그것의 상태를 Solver::Snapshot()
과 Solver::SnapshotSOlverState()
를 사용하여 스냅샷을 저장해놓습니다. 가중치 스냅샷은 정해진 학습된 model을 저장하는 반면, solver 스냅샷은 주어진 시점부터 이어서 다시 학습할 수 있습니다. 훈련은 Solver::Restore()
와 Solver::RestoreSolverState()
에 의해 재개될 수 있습니다.
가중치는 확장자 없이 저장되지만 solver 상태는 .solverstate
확장자가 붙어 저장됩니다. 두 파일일 반복의 숫자에 따라 모두 _iter_N
라는 이름이 붙을 것입니다.
스냅샷은 solver를 정의하는 prototxt에 다음과 같이 설정합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# The snapshot interval in iterations. snapshot: 5000 # File path prefix for snapshotting model weights and solver state. # Note: this is relative to the invocation of the `caffe` utility, not the # solver definition file. snapshot_prefix: "/path/to/model" # Snapshot the diff along with the weights. This can help debugging training # but takes more storage. snapshot_diff: false # A final snapshot is saved at the end of training unless # this flag is set to false. The default is true. snapshot_after_train: true |