Torch7入門續集補充(2)--- 每一層設定不同的學習率(finetuning有用)
總說
咋說來著,有時候你真的很想finetuning一個網路,想讓網路前面固定或是學習率很小,但是你會發現,完全弄不來啊!雖然你可能發現了有一個叫nn.ZeroGrad
的layer,看看程式碼:
local ZeroGrad, parent = torch.class('nn.ZeroGrad', 'nn.Module')
function ZeroGrad:updateOutput(input)
self.output:set(input)
return self.output
end
-- the gradient is simply zeroed.
-- useful when you don't want to backpropgate through certain paths.
function ZeroGrad:updateGradInput(input, gradOutput)
self.gradInput = nn.utils.recursiveResizeAs(self.gradInput, input)
self.gradInput = nn.utils.recursiveFill(self.gradInput, 0)
return self.gradInput
end
就是直接將gradInput填0就行。這樣,在 nn.ZeroGrad前面的層的梯度都是0,從而使得前面固定。
但是,你如果想要讓前面的網路學習率不是0,這就難辦了。
兩種方法
第一種:針對使用 SGD的方法
lrs是learningRates, 其實在SGD中,可以為每個引數設定不同的學習率的。見下面程式碼:
function optim.sgd(opfunc, x, config, state)
-- (0) get/update state
local config = config or {}
local state = state or config
local lr = config.learningRate or 1e-3
local lrd = config.learningRateDecay or 0
local wd = config.weightDecay or 0
local mom = config.momentum or 0
local damp = config.dampening or mom
local nesterov = config.nesterov or false
local lrs = config.learningRates
local wds = config.weightDecays
state.evalCounter = state.evalCounter or 0
local nevals = state.evalCounter
assert(not nesterov or (mom > 0 and damp == 0), "Nesterov momentum requires a momentum and zero dampening")
-- (1) evaluate f(x) and df/dx
local fx,dfdx = opfunc(x)
-- (2) weight decay with single or individual parameters
if wd ~= 0 then
dfdx:add(wd, x)
elseif wds then
if not state.decayParameters then
state.decayParameters = torch.Tensor():typeAs(x):resizeAs(dfdx)
end
state.decayParameters:copy(wds):cmul(x)
dfdx:add(state.decayParameters)
end
-- (3) apply momentum
if mom ~= 0 then
if not state.dfdx then
state.dfdx = torch.Tensor():typeAs(dfdx):resizeAs(dfdx):copy(dfdx)
else
state.dfdx:mul(mom):add(1-damp, dfdx)
end
if nesterov then
dfdx:add(mom, state.dfdx)
else
dfdx = state.dfdx
end
end
-- (4) learning rate decay (annealing)
local clr = lr / (1 + nevals*lrd)
-- (5) parameter update with single or individual learning rates
if lrs then
if not state.deltaParameters then
state.deltaParameters = torch.Tensor():typeAs(x):resizeAs(dfdx)
end
-- deltaParameters是 梯度*每個引數的學習率
state.deltaParameters:copy(lrs):cmul(dfdx)
-- 然後進行更新引數x
x:add(-clr, state.deltaParameters)
else
x:add(-clr, dfdx)
end
-- (6) update evaluation counter
state.evalCounter = state.evalCounter + 1
-- return x*, f(x) before optimization
return x,{fx}
end
關鍵看這個:
-- (5) parameter update with single or individual learning rates
if lrs then
if not state.deltaParameters then
state.deltaParameters = torch.Tensor():typeAs(x):resizeAs(dfdx)
end
-- deltaParameters是 梯度*每個引數的學習率
state.deltaParameters:copy(lrs):cmul(dfdx)
-- 然後進行更新引數x
x:add(-clr, state.deltaParameters)
else
x:add(-clr, dfdx)
end
可以看到,如果設定了lrs(我們在外面一般是傳入optmState.learningRates, 注意是”learningRates”不是”learningRate”),那麼就先將lrs乘以梯度,然後在將這個已經對每個引數的梯度乘了一個特定係數(這個係數是每個引數的學習率),再乘以一個總體的學習率 -clr,然後加上x,x引數就這樣更新了。
-- deltaParameters是 梯度*每個引數的學習率
state.deltaParameters:copy(lrs):cmul(dfdx)
因此:我們可以通過改變optimState.learningRates的值來逐層設定學習率(weight和bias都可以單獨設定),看例子:
-- suppose you have a model called model
lrs_model = model:clone()
lrs = lrs_model:getParameters() -- 為了讓lrs的長度是所有引數的長度。並且是嚴格按照逐層逐引數展開的。因此lrs嚴格對應每個引數。
lrs:fill(1) -- setting the base learning rate to 1
-- 這裡設定第5層的bias的學習率是2(都是相對值)
lrs_model:get(5).bias:fill(2)
-- 設定第2層的weight的相對學習率是3
lrs_model:get(2).weight:fill(3)
-- now pass lrs_model to optimState, which was created previously
optimState.learningRates = lrs
由於getParameters是對於每層的引數進行flaten, 所以一層的引數先展開weight, 再展開bias。因此這裡
lrs_model:get(5).bias:fill(2)
就是將lrs的某些值由1變成2,同理解釋下面一行。最後我們將
lrs(按照上面的程式碼來講是,大部分為1,有些變成了2,有些變成了3)作為 optimState.learningRates即可。
通用演算法的學習率設定
核心:利用parameters而不是getParameters來獲得引數!。
這就有意思了,因為幾乎所有的程式碼都是通過 getParameters來將網路的引數拉成一個向量,然後進行優化,這個你已經知道了的。但是如果你看原始碼:
function Module:getParameters()
-- get parameters
local parameters,gradParameters = self:parameters()
local p, g = Module.flatten(parameters), Module.flatten(gradParameters)
assert(p:nElement() == g:nElement(),
'check that you are sharing parameters and gradParameters')
if parameters then
for i=1,#parameters do
assert(parameters[i]:storageOffset() == gradParameters[i]:storageOffset(),
'misaligned parameter at ' .. tostring(i))
end
end
return p, g
end
這裡可以看到呼叫了 parameters
函式,然後再進行“壓平”flatten。再看parameters
:
function Module:parameters()
if self.weight and self.bias then
return {self.weight, self.bias}, {self.gradWeight, self.gradBias}
elseif self.weight then
return {self.weight}, {self.gradWeight}
elseif self.bias then
return {self.bias}, {self.gradBias}
else
return
end
end
可以看到這裡直接返回 table 型別。因此如果我們使用 parameters
函式,則返回一個table,table中的每個tensor對應每層的weight/bias。因此我們需要對於每層都弄一個optimState就可以了。
local params, gradParams = model:parameters()
-- 設定每層的學習率為0.01
local learningRates = torch.Tensor(#params):fill(0.01)
-- 將第2層的設定為0.01
learningRates[2] = 0.001
optimState = {}
for i = 1, #params do
table.insert(optimState, {
learningRate = learningRates[i],
learningRateDecay = 0.0001,
momentum = 0.9,
dampening = 0.0,
weightDecay = 5e-4
})
end
for e = 1, epochs do
-- Get MNIST batch
X, Y = get_mnist_batch(batch_size)
-- forward -> backward (outside of feval)
model:zeroGradParameters()
out = model:forward(X)
err = criterion:forward(out, Y)
gradOutputs = criterion:backward(out, Y)
model:backward(X, gradOutputs)
-- 對每一層的引數 params
for i = 1, #params do
local feval = function(x)
return err, gradParams[i]
end
optim.sgd(feval, params[i], optimState[i])
end
end