參考 http://caffe.berkeleyvision.org/tutorial/
- 表達:models和optimizations使用純文本文檔形式定義,不是用代碼定義;
- 速度:適用於工業和科研中的模型和大數據
- 模塊性:新任務和設置可以靈活擴展
- 開源、社區
開始學習!
Blobs, Layers, and Nets: 一個Caffe模型的基本組成
Blobs: 標准數組和統一內存接口,用於存儲、通信和操作信息(數據和偏導流)
Layers:模型和計算
Nets:連接層
Blobs:
blob實際上是caffe中處理和傳輸的數據的包裝器,並同時兼具CPU和GPU之間的數據同步的能力。
數學上看,一個blob實際上是一個類似C中的N維數組。
caffe使用blob存儲和通信數據。blob提供一個統一的內存接口保存數據。caffe中常用的數據有image batches, model parameters and derivatives for optimization。
由於我們通常對於blobs的數值和差分感興趣,所以一個blob存儲兩塊內存:data and diff。前者是我們傳輸的正常數據,后者是網絡計算的梯度。
實際數據可以被存儲在CPU或GPU上,我們有兩種方式獲得它們:const方式(不改變數值),mutable方式(改變數值)。兩種方式如下定義:
const Dtype* cpu_data() const; Dtype* mutable_cpu_data();
在GPU上和對差分的操作是類似的。
這樣設計的原因在於:blob使用SyncedMem類同步CPU和GPU的數據,為了隱藏同步的細節和最小化數據傳輸使用兩種調用方式。
經驗方法是如果不想改變數值就一直使用const方式調用,不在自己定義的object上存儲指針。
Layers:
layer是模型的關鍵,計算的基礎單元。layer的操作有卷積、pool、內積、應用非線性elementwise變換(ReLU,sigmoid等),normalize,加載數據,計算損失(softmax和hinge)等。
每個layer從底層連接獲得輸入,輸出到頂層連接。
每個layer定義三種關鍵計算:setup,forward和backward。
setup:模型初始化時,初始化這個layer和它的連接;
forward:從底層取出輸入,計算輸出,傳遞到頂層;
backward:從頂層獲得輸入,計算梯度,發送到底層。有參數的層計算關於它的參數的梯度,並在內部存儲。
特別地,每個layer有兩種forward和backward實現,分別是基於CPU和GPU。如果沒有實現GPU版本,layer默認使用CPU函數,這給快速實驗帶來了便利。
Nets:
net聯合定義一個函數和它的梯度通過合成和自動差分。每個layer的輸出的合成計算一個給定任務的函數,每個layer的backward的合成計算學習任務的loss的梯度。
caffe model是端到端的機器學習引擎。
net是layers的集合,layers之間使用有向無環圖(Directed acyclic graph, DAG)連接。
net使用純文本文檔定義layers和他們指尖的連接。一個簡單的logistic regression分類器如下定義:
name: "LogReg" layer { name: "mnist" type: "Data" top: "data" top: "label" data_param { source: "input_leveldb" batch_size: 64 } } layer { name: "ip" type: "InnerProduct" bottom: "data" top: "ip" inner_product_param { num_output: 2 } } layer { name: "loss" type: "SoftmaxWithLoss" bottom: "ip" bottom: "label" top: "loss" }
模型初始化使用Net::Init().這個初始化主要做兩件事:創建blobs和layers搭建整個有向無環圖(DAG),調用layers的Setup()函數。它也做一些統計工作,例如校驗整個網絡架構的正確性。
注意:網絡的構建是設備無關的。構建之后,網絡是運行在CPU或GPU上是通過一個單獨的定義實現的Caffe::mode(),設置Caffe::set_mode()。
模型是在純文本protocol buffer模式.prototxt中定義的,學習好的模型被序列化為binary protocol buffer,存儲在 .caffemodel文件中。
caffe使用Google Protocol Buffer出於以下幾個優點:
序列化時最小化binary string的size,有效序列化,文本格式兼容binary version,在多種語言中都有接口實現,例如C++和Python。這些優點使得在caffe建模靈活可拓展。
Forward and Backward: 前線傳播和后向傳播
前向傳播和后向傳播是一個網絡的關鍵計算。
以簡單的logistic regression為例:
前向傳播計算給定輸入的輸出,caffe依次排列每個layer定義的計算操作,計算模型表達的函數。前向傳播方向自底向上:
輸入數據x通過一個內積層輸出g(x),接着再通過一個softmax計算獲得h(g(x)),得到輸出softmax loss。
后向傳播計算給定的loss的梯度。后向傳播時,caffe逆向排列每層定義的梯度,通過自動差分計算整個模型的梯度。后向傳播方向自頂向下:
后向傳播從loss開始,計算loss關於輸出的梯度∂fw/∂h。關於模型剩余部分的梯度被一層層的根據鏈式法則求解。有參數的layer,像INNER_PRODUCT layer,計算關於layer參數的梯度∂fw/∂Wip,用於更新layer參數。
這些計算從定義model開始立即生效,caffe自動計划和執行前向和后向傳播:
Net::Forward()和Net::Backward()方法執行相關的傳播,而Layer::Forward()和Layer::Backward()在每個步驟中負責具體計算;
每個layer的計算類型有cpu和gpu兩種(forward_{cpu, gpu}(), backward_{cpu, gpu}())。處於便利或限制因素考慮,一個layer允許只實現CPU或GPU版本。
Solver最優化一個model通過首先調用前向傳播獲得output和loss,然后調用后向傳播生成model的梯度,更新weight和參數,以最小化loss。
Solver,Net,和Layer之間的分工使得caffe模塊化,易於開發使用。
Loss:損失函數
caffe中,像眾多機器學習技術一樣,learning過程有loss function驅動。loss function也常被稱為error,cost或objective function。損失函數指定了學習的目標,將參數設置(也就是當前網絡的權重)映射為一個標量數值,來表達這些參數設置的“badness”程度。因此,學習的目標就是找出一組參數設置最小化損失函數。
caffe里的loss通過網絡的前向傳播計算。每個layer取一組輸入(bottom)blobs,產生一組輸出(top)blobs。這些layer的輸出可能用於計算loss function。在one-verse-all分類任務中,loss function的一個典型選擇是SoftmaxWithLoss函數,網絡定義中如下定義:
layer { name: "loss" type: "SoftmaxWithLoss" bottom: "pred" bottom: "label" top: "loss" }
在一個SoftmaxWithLoss函數中,top blobs是一個標量,取整個mini-batch的loss的平均值(mini-batch中每個樣本的預測label pred 與實際label label的誤差的平均值)。
Loss Weights:
對於有多個layer的net來說,每個layer產生一個loss,loss weights用來指定這些loss的相對重要性。
按照約定,caffe中類型type中帶有后綴Loss的layer對loss function有貢獻,而其他的layers被默認為純粹用於中間計算。然而,任何layer都可以被用作loss,通過在layer定義中添加一個field: loss_weight: <float>。每個帶有后綴Loss的layer默認loss_weight:1對於第一個top blob,其他的top blob的loss_weight:0;其它layer默認對所有top blob的loss_weight:0。所以上面的SoftmaxWithLosslayer等價於如下定義
layer { name: "loss" type: "SoftmaxWithLoss" bottom: "pred" bottom: "label" top: "loss" loss_weight: 1 }
然而,任何能夠后向傳播的layer都可能被給一個非零的loss_weight,例如允許正則化網絡中間層的激勵。對於非單個輸出的layer,如果被賦予一個非零loss_weight,loss通過對blob的所有元素求和簡單計算。
caffe中最終loss通過網絡的總體加權loss的求和被計算,如下偽碼所示:
loss := 0 for layer in layers: for top, loss_weight in layer.tops, layer.loss_weights: loss += loss_weight * sum(top)
Solver:
solver精心排列網絡的前向推斷和后向梯度構成參數更新過程,以保證loss最小化。學習過程被分為Solver監督管理最優化和生成參數更新過程,Net計算loss和梯度。
caffe的solver包括:
- Stochastic Gradient Descent (
type: "SGD"
), - AdaDelta (
type: "AdaDelta"
), - Adaptive Gradient (
type: "AdaGrad"
), - Adam (
type: "Adam"
), - Nesterov’s Accelerated Gradient (
type: "Nesterov"
) and - RMSprop (
type: "RMSProp"
)
Solver構建最優化過程,創建學習的訓練網絡和評估的測試網絡;迭代地調用forward/backward函數和更新參數;周期性地評估測試網絡;在最優化過程中給model和solver狀態拍快照(緩存?)
在每個迭代步驟中,調用網絡前向傳播計算輸出和loss;調用網絡后向傳播計算梯度;根據solver方法,將所得梯度用於參數更新;根據學習率,history,和方法更新solver狀態。
像caffe的model一樣,caffe solver有CPU和GPU兩種模式。
solver方法針對於解決loss最小化的一般最優化問題。對數據集D,最優化目標是所有數據的平均loss,如下式所示:
其中fW(X(i))是數據項X(i)的loss,r(W)是正則化項。|D|在實際中可能非常大,所以在每個solver迭代過程中,我們使用目標的stochastic近似,使用一個mini-batch的數據N<<|D|,loss如下所示:
model計算前向傳播時計算fW,后向傳播時計算梯度。
參數更新被solver構建,來源於error梯度
,正則化梯度和其他特定項。
SGD:Stochastic gradient descent (type: "SGD")
更新權重W通過一個線性聯合負梯度和先前的權重更新
。學習率α是負梯度的加權系數,momentum μ是前面權重更新的加權系數。
一般情況下,我們使用下面公式根據以前權重更新Vt和當前權重Wt,計算迭代次數為t+1時的更新值Vt+1和更新權重Wt+1:
學習過程的超參數("hyperparameters" α和μ)可能需要一些調整以獲得最后的結果。如果不確定從哪里開始調整,參考下面的經驗法則("Rules of thumb"),更多的信息可以參見Leon Bottou's Stochastic Gradient Descent Tricks(L. Bottou. Stochastic Gradient Descent Tricks. Neural Networks: Tricks of the Trade: Springer, 2012.)。
設置學習率α和momentum μ的經驗法則:
使用SGD方法的deep learning,初始化學習率的一個好的策略是設置一個值約,然后在訓練過程中,每當loss達到一個明顯的谷點時,使它降低一個常數因子(例如10),這個過程重復幾次。通常情況下momentum μ = 0.9或相似的值。通過在迭代中平滑權重更新,momentum使得基於SGD的deep learning過程更平穩更快。
這是Krizhevsky et al.[1]中使用的策略。caffe使得這個策略在SolverParameter中很容易實現,見./example/imagenet/alexnet_solver.prototxt。
如要使用這樣的超參數調整策略,你可以將下面幾行放入你的solver的prototxt中的某位置:
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
上面的設置中,momentum μ始終等於0.9。訓練開始時的前100,000次迭代,學習率base_lr ,然后乘以gamma,以學習率
訓練第100K-200K次迭代,然后第200K至300K迭代使用學習率
,最后直到350K次迭代,學習率為
。
注意到在很多次迭代后,momentum μ有效地使用一個因子乘以更新的size,所以如果增加μ,也應該相應地降低學習率α。反之亦然。
例如μ = 0.9時,我們有一個有效地更新size乘子;如果我們增加momentum到μ = 0.99時,我們已經增加更新size乘子到100,所以應該降低學習率以一個因子10(乘以0.1)。
注意上面的設置僅僅是指導作用,並不保證在每種情況下最優,甚至某些情況下根本就無效。如果學習過程中,例如初夏非常大的或者NaN或者inf的loss值或輸出,嘗試降低初始學習率base_lr,重新訓練,直到你發現base_lr值有效。
[1] A. Krizhevsky, I. Sutskever, and G. Hinton. ImageNet Classification with Deep Convolutional Neural Networks. Advances in Neural Information Processing Systems, 2012.
AdaDelta: (type: "AdaDelta")
這個方法是一個"穩定學習率的方法“,它像SGD一樣基於梯度進行最優化,更新形式如下:
[1] M. Zeiler ADADELTA: AN ADAPTIVE LEARNING RATE METHOD. arXiv preprint, 2012.
RMS:均方根值,Root-Mean-Square
AdaGrad: adaptive gradient (type: "AdaGrad")
是一個類似於SGD的基於梯度的優化算法,企圖以非常的predictive但幾乎看不見的特征的形式進行大海撈針("find needles in haystacks in the form of very predictive but rarely seen features", in Duchi et al.'s words)。給定所有前面迭代時的更新信息,更新公式如下所示:
實際上,對於權重W ,AdaGrad實現使用僅僅O(d)的額外存儲空間為歷史gradient信息。
[1] J. Duchi, E. Hazan, and Y. Singer. Adaptive Subgradient Methods for Online Learning and Stochastic Optimization. The Journal of Machine Learning Research, 2011.
Adam:(type: "Adam")
也是基於梯度的最優化方法,包含一個自適應矩估計(mt,vt),能夠被視為AdaGrad的推廣,更新形式如下:
Kingma等人提出使用作為默認值,caffe使用momentum, momentum2, delta分別表示
。
[1] D. Kingma, J. Ba. Adam: A Method for Stochastic Optimization. International Conference for Learning Representations, 2015.
NAG: Nesterov's accelerated gradient (type: "Nesterov")
是一種最優的凸優化方法,實現了收斂率,而不是
。盡管實現
收斂率所要求的假設典型地不支持使用caffe訓練的神經網絡,由於非平滑性和非凸性。實際中NAG是一種非常有效的方法,去最優化特定形式的deep learning架構,例如Sutskever等人生命的deep MNIST autoencoders。
權重更新看起來與SGD非常相似:
與SGD的區別在於SGD中使用梯度中的W不同,SGD中權重設置W基於誤差梯度,NAG中我們采用權重加上momentum
。
[1] Y. Nesterov. A Method of Solving a Convex Programming Problem with Convergence Rate
O(1/k)
. Soviet Mathematics Doklady, 1983.
[2] I. Sutskever, J. Martens, G. Dahl, and G. Hinton. On the Importance of Initialization and Momentum in Deep Learning. Proceedings of the 30th International Conference on Machine Learning, 2013
RMSprop: (type: "RMSprop")
是一個基於梯度的最優化方法,更新形式如下:
如果梯度更新導致震盪,梯度降低為(1 - δ)倍,否則增加δ。默認的δ值(rms_decay)為0.02。
[1] T. Tieleman, and G. Hinton. RMSProp: Divide the gradient by a running average of its recent magnitude. COURSERA: Neural Networks for Machine Learning.Technical report, 2012.
Scaffolding:拍快照
solver的scanffolding准備最優化方法和崔思華模型在Solver::Presolve()方法中。
> 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 initialization
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
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
Completion
I0902 13:35:56.806970 16020 solver.cpp:46] Solver scaffolding done.
I0902 13:35:56.806984 16020 solver.cpp:165] Solving LeNet
# 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
∇