我正在尝试使用minFunc训练单层自动编码器,虽然成本函数似乎减少,但启用时,DerivativeCheck失败。我正在使用的代码尽可能接近教科书值,但非常简化。
我正在使用的损失函数是平方误差:
$ J(W; x)= \ frac {1} {2} || a ^ {l} - x || ^ 2 $
$ a ^ {l} $等于$ \ sigma(W ^ {T} x)$,其中$ \ sigma $是sigmoid函数。因此,梯度应为:
$ \ delta =(a ^ {l} - x)* a ^ {l}(1 - a ^ {l})$
$ \ nabla_ {W} = \ delta(a ^ {l-1})^ T $
请注意,为了简化事情,我完全放弃了偏见。虽然这会导致性能不佳,但它不应该影响梯度检查,因为我只关注权重矩阵。另外,我已经绑定了编码器和解码器矩阵,因此实际上只有一个权重矩阵。
我用于丢失函数的代码是(编辑:我已经将我的循环矢量化并清理了一些代码):
% loss function passed to minFunc
function [ loss, grad ] = calcLoss(theta, X, nHidden)
[nInstances, nVars] = size(X);
% we get the variables a single vector, so need to roll it into a weight matrix
W = reshape(theta(1:nVars*nHidden), nVars, nHidden);
Wp = W; % tied weight matrix
% encode each example (nInstances)
hidden = sigmoid(X*W);
% decode each sample (nInstances)
output = sigmoid(hidden*Wp);
% loss function: sum(-0.5.*(x - output).^2)
% derivative of loss: -(x - output)*f'(o)
% if f is sigmoid, then f'(o) = output.*(1-output)
diff = X - output;
error = -diff .* output .* (1 - output);
dW = hidden*error';
loss = 0.5*sum(diff(:).^2, 2) ./ nInstances;
% need to unroll gradient matrix back into a single vector
grad = dW(:) ./ nInstances;
end
下面是我用来运行优化器的代码(一次,因为所有训练样本的运行时相当长):
examples = 5000;
fprintf('loading data..\n');
images = readMNIST('train-images-idx3-ubyte', examples) / 255.0;
data = images(:, :, 1:examples);
% each row is a different training sample
X = reshape(data, examples, 784);
% initialize weight matrix with random values
% W: (R^{784} -> R^{10}), W': (R^{10} -> R^{784})
numHidden = 10; % NOTE: this is extremely small to speed up DerivativeCheck
numVisible = 784;
low = -4*sqrt(6./(numHidden + numVisible));
high = 4*sqrt(6./(numHidden + numVisible));
W = low + (high-low)*rand(numVisible, numHidden);
% run optimization
options = {};
options.Display = 'iter';
options.GradObj = 'on';
options.MaxIter = 10;
mfopts.MaxFunEvals = ceil(options.MaxIter * 2.5);
options.DerivativeCheck = 'on';
options.Method = 'lbfgs';
[ x, f, exitFlag, output] = minFunc(@calcLoss, W(:), options, X, numHidden);
我在DerivitiveCheck上得到的结果通常小于0,但大于0.1。我使用批量梯度下降尝试了类似的代码,并获得了稍微好一点的结果(有些是<0.0001,但肯定不是全部)。
我不确定我的数学或代码是否犯了错误。任何帮助将不胜感激!
更新
我在代码中发现了一个小错字(下面的代码中没有出现),导致性能异常糟糕。不幸的是,我的成绩仍然不尽如人意。例如,两个渐变之间的比较:
calculate check
0.0379 0.0383
0.0413 0.0409
0.0339 0.0342
0.0281 0.0282
0.0322 0.0320
差异高达0.04,我假设仍然失败。
答案 0 :(得分:2)
好的,我想我可能已经解决了这个问题。通常,梯度的差异<1。 1e-4,虽然我确实至少有一个是6e-4。有谁知道这是否仍然可以接受?
为了得到这个结果,我重写了代码而没有绑定权重矩阵(我不确定这样做是否会导致派生检查失败)。我也包含了偏见,因为它们并没有使事情变得太复杂。
我在调试时意识到的是,真的很容易在代码中出错。例如,我花了一段时间来抓住:
grad_W1 = error_h*X';
而不是:
grad_W1 = X*error_h';
虽然这两行之间的差异只是grad_W1的转置,但由于需要将参数打包/解包到单个向量中,因此Matlab无法抱怨grad_W1是错误的维度。
我还包括了我自己的衍生检查,它给出了与minFunc相比略有不同的答案(我的衍生检查给出的差异均低于1e-4)。
<强> fwdprop.m:强>
function [ hidden, output ] = fwdprop(W1, bias1, W2, bias2, X)
hidden = sigmoid(bsxfun(@plus, W1'*X, bias1));
output = sigmoid(bsxfun(@plus, W2'*hidden, bias2));
end
<强> calcLoss.m:强>
function [ loss, grad ] = calcLoss(theta, X, nHidden)
[nVars, nInstances] = size(X);
[W1, bias1, W2, bias2] = unpackParams(theta, nVars, nHidden);
[hidden, output] = fwdprop(W1, bias1, W2, bias2, X);
err = output - X;
delta_o = err .* output .* (1.0 - output);
delta_h = W2*delta_o .* hidden .* (1.0 - hidden);
grad_W1 = X*delta_h';
grad_bias1 = sum(delta_h, 2);
grad_W2 = hidden*delta_o';
grad_bias2 = sum(delta_o, 2);
loss = 0.5*sum(err(:).^2);
grad = packParams(grad_W1, grad_bias1, grad_W2, grad_bias2);
end
<强> unpackParams.m:强>
function [ W1, bias1, W2, bias2 ] = unpackParams(params, nVisible, nHidden)
mSize = nVisible*nHidden;
W1 = reshape(params(1:mSize), nVisible, nHidden);
offset = mSize;
bias1 = params(offset+1:offset+nHidden);
offset = offset + nHidden;
W2 = reshape(params(offset+1:offset+mSize), nHidden, nVisible);
offset = offset + mSize;
bias2 = params(offset+1:end);
end
<强> packParams.m 强>
function [ params ] = packParams(W1, bias1, W2, bias2)
params = [W1(:); bias1; W2(:); bias2(:)];
end
<强> checkDeriv.m:强>
function [check] = checkDeriv(X, theta, nHidden, epsilon)
[nVars, nInstances] = size(X);
[W1, bias1, W2, bias2] = unpackParams(theta, nVars, nHidden);
[hidden, output] = fwdprop(W1, bias1, W2, bias2, X);
err = output - X;
delta_o = err .* output .* (1.0 - output);
delta_h = W2*delta_o .* hidden .* (1.0 - hidden);
grad_W1 = X*delta_h';
grad_bias1 = sum(delta_h, 2);
grad_W2 = hidden*delta_o';
grad_bias2 = sum(delta_o, 2);
check = zeros(size(theta, 1), 2);
grad = packParams(grad_W1, grad_bias1, grad_W2, grad_bias2);
for i = 1:size(theta, 1)
Jplus = calcHalfDeriv(X, theta(:), i, nHidden, epsilon);
Jminus = calcHalfDeriv(X, theta(:), i, nHidden, -epsilon);
calcGrad = (Jplus - Jminus)/(2*epsilon);
check(i, :) = [calcGrad grad(i)];
end
end
<强> checkHalfDeriv.m:强>
function [ loss ] = calcHalfDeriv(X, theta, i, nHidden, epsilon)
theta(i) = theta(i) + epsilon;
[nVisible, nInstances] = size(X);
[W1, bias1, W2, bias2] = unpackParams(theta, nVisible, nHidden);
[hidden, output] = fwdprop(W1, bias1, W2, bias2, X);
err = output - X;
loss = 0.5*sum(err(:).^2);
end
<强>更新强>
好的,我也想出了为什么绑重物会导致问题。我想从[ W1; bias1; bias2 ]
开始追溯到W2 = W1'
。这样我就可以通过查看W1来重新创建W2。但是,由于epsilon更改了$ \ theta $的值,因此实际上同时更改了两个矩阵。正确的解决方案是简单地将W1
作为单独的参数传递,同时减少$ \ theta $。
更新2
好的,这是我晚上发布的内容。虽然第一次更新确实导致事情正确通过,但这不是正确的解决方案。
我认为正确的做法是实际计算W1和W2的梯度,然后将W1的最终梯度设置为grad_W1到grad_W2。挥手的论点是,由于权重矩阵对编码和解码起作用,其权重必须受两个梯度的影响。然而,我还没有想到这个的实际理论分支。
如果我使用自己的衍生检查来运行它,它会通过10e-4阈值。使用minFunc的衍生检查确实比以前好多了,但是如果我没有加权,那就更糟了。