eval.m
函数定义:
function eval(obj, inputs, derOutputs, stopLayer)
用法一:执行前向传播
eval(obj, inputs)
,使用指定的inputs
执行DagNN
,在整个网络上执行前向传播,计算所有变量的值,计算结果可以通过obj.vars(outputIndex)
获得。
用法二:执行前向传播+反向传播
eval(obj, inputs, derOutputs)
,从inputs
开始执行前向传播,完成后再从derOutputs
执行反向传播。
obj
是DagNN对象
inputs
是一个元胞数组:{'inputName', inputValue, ...}
derOutputs
是一个元胞数组:{'outputName', outputDerValue, ...}
准备工作
参数检查
确保inputs和derOutputs一定是cell数组类型
obj.computingDerivative = nargin > 2 && ~isempty(derOutputs) ;
if ~iscell(inputs), error('INPUTS is not a cell array.') ; end
if obj.computingDerivative && ~iscell(derOutputs), error('DEROUTPUTS is not a cell array.') ; end
if nargin < 4
stopLayer = [];
end
前向传播
输入赋值
将输入inputs的值赋给对应的变量。
for i = 1:2:numel(inputs)
v = obj.getVarIndex(inputs{i}) ;
if ~isnan(v)
switch obj.device
case 'cpu', obj.vars(v).value = gather(inputs{i+1}) ;
case 'gpu', obj.vars(v).value = gpuArray(inputs{i+1}) ;
end
end
end
前向传播
整个卷积神经网络可以用一个有向无环图(DagNN)表示,每个节点代表一层,每一层的inputs代表入边,outputs代表出边。obj.executionOrder则返回该有向无环图的拓扑序,按照这个顺序执行前向传播,可以确保在计算每一层的时候,输入都已经计算过。
forwardAdvanced执行该层的具体运算。
forwardTime记录该层执行时间。
前向传播到stopLayer则退出执行,不再继续传播。
inputs = [] ;
obj.numPendingVarRefs = [obj.vars.fanout] ;
for l = obj.executionOrder
time = tic ;
obj.layers(l).block.forwardAdvanced(obj.layers(l)) ;
obj.layers(l).forwardTime = toc(time) ;
if stopLayer == l
break;
end
end
类比Java的垃圾回收机制,Java内部维护着每个对象的引用次数,当对象的引用次数为0时,对象占用的内存就可以回收了。matconvnet使用了类似的机制来节约内存,当变量的引用次数为0时,将变量占用的内存回收。
numPendingVarRefs 记录每个变量的引用数,每一层执行完之后,对numPendingVarRefs维护,将该层引用的变量的计数器减一。当引用次数为0时,就可以将改变了占用的内存清空了。
参考Layer.m代码:
function forwardAdvanced(obj, layer)
in = layer.inputIndexes ;
out = layer.outputIndexes ;
par = layer.paramIndexes ;
net = obj.net ;
inputs = {net.vars(in).value} ;
% clear inputs if not needed anymore
for v = in
net.numPendingVarRefs(v) = net.numPendingVarRefs(v) - 1 ;
if net.numPendingVarRefs(v) == 0
if ~net.vars(v).precious & ~net.computingDerivative & net.conserveMemory
net.vars(v).value = [] ;
end
end
end
...
end
反向传播
清空变量,节约内存
backpropDepth设置反向传播的终止层,当反向传播执行到这一层时就不再继续。
如果设置了backpropDepth,那该层前面的层在反向传播的时候就用不到。为了节约内存,把前面的层的变量都先置空。
if obj.backpropDepth
for l = obj.executionOrder
[obj.vars(obj.layers(l).inputIndexes).value] = deal([]) ;
if strcmp(obj.layers(l).name, obj.backpropDepth)
break;
end
end
end
设置derOutputs权重
derOutputs代表反向传播的起点,并且可以有多个起点(多个优化目标),每个优化目标有一个权重。例如:
derOutputs = {'loss_spacial', 1, 'loss_temporal', 3}
有两个优化目标,并且权重比为1:3。derOutputs(1:2:end)
代表变量名,derOutputs{2:2:end}
代表对应权重。
v = obj.getVarIndex(derOutputs(1:2:end)) ;
[obj.vars(v).der] = deal(derOutputs{2:2:end}) ;
derOutputs = [] ;
反向传播
fliplr(obj.executionOrder)代表按前向传播相反的顺序执行反向传播。
backwardAdvanced 执行该层具体的反向传播。
backwardTime 记录反向传播时间。
执行到backprobDepth之后就不再继续执行反向传播。
obj.numPendingVarRefs = zeros(1, numel(obj.vars)) ;
obj.numPendingParamRefs = zeros(1, numel(obj.params)) ;
for l = fliplr(obj.executionOrder)
time = tic ;
if strcmp(obj.layers(l).name, obj.backpropDepth)
[obj.vars.value] = deal([]) ;
[obj.vars.der] = deal([]) ;
break;
end
obj.layers(l).block.backwardAdvanced(obj.layers(l)) ;
obj.layers(l).backwardTime = toc(time) ;
end
总结
- 使用
obj.executionOrder
获得DagNN的拓扑序,按序执行前向传播和反向传播。 stopLayer
和backprobDepth
决定了前向传播和反向传播的终点,可以更加细微地控制训练过程,在预训练的模型上进行二次训练时可能会用到,可以将参数改变限定在少数层上。forwardAdvanced
和backwardAdvanced
具体地实现了每一层的前向传播和反向传播的过程,这要看其他部分的源码。- 借助
numPendingVarRefs
维护变量的引用数,将无引用的变量清空,尽可能地节约内存。