DerivativeCheck не работает с minFunc - PullRequest
3 голосов
/ 06 июля 2011

Я пытаюсь обучить один слой автокодера с помощью minFunc, и хотя функция стоимости, по-видимому, уменьшается, при включении DerivativeCheck завершается ошибкой. Код, который я использую, максимально приближен к значениям учебника, хотя и чрезвычайно упрощен.

Я использую функцию потерь в квадрате:

$ J (W; x) = \ frac {1} {2} || a ^ {l} - x || ^ 2 $

с $ a ^ {l} $, равным $ \ sigma (W ^ {T} x) $, где $ \ sigma $ - сигмоидальная функция. Следовательно, градиент должен быть:

$ \ delta = (a ^ {l} - x) * a ^ {l} (1 - a ^ {l}) $

$ \ nabla_ {W} = \ delta (a ^ {l-1}) ^ T $

Заметьте, что для упрощения вещей я полностью прекратил предвзятость. Хотя это приведет к снижению производительности, это не должно повлиять на проверку градиента, так как я смотрю только на матрицу весов. Кроме того, я связал матрицы кодировщика и декодера, так что фактически существует единая матрица весов.

Код, который я использую для функции потерь: ( edit: Я векторизовал цикл, который у меня был, и немного очистил код):

% 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, но, конечно, не все). </p>

Я не уверен, допустил ли я ошибку с моей математикой или кодом. Любая помощь будет принята с благодарностью!

обновление

Я обнаружил небольшую опечатку в своем коде (которая отсутствует в коде ниже), вызывающую исключительно плохую производительность. К сожалению, я все еще получаю плохие результаты. Например, сравнение между двумя градиентами:

calculate     check
0.0379        0.0383
0.0413        0.0409
0.0339        0.0342
0.0281        0.0282
0.0322        0.0320

с разницей до 0,04, которая, как я предполагаю, все еще не работает.

1 Ответ

2 голосов
/ 12 июля 2011

Хорошо, думаю, я мог бы решить проблему.Обычно различия в градиентах составляют <1e-4, хотя у меня есть по крайней мере один, который равен 6e-4.Кто-нибудь знает, если это все еще приемлемо? </p>

Чтобы получить этот результат, я переписал код и не связывая матрицы весов (я не уверен, что это всегда приведет к сбою проверки производной).Я также добавил ошибки, поскольку они не слишком усложняли ситуацию.

Что-то еще, что я понял при отладке, это то, что действительно легко допустить ошибку в коде.Например, мне потребовалось некоторое время, чтобы поймать:

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'.Таким образом, я мог бы просто воссоздать W2, посмотрев на W1.Однако, поскольку значения $ \ theta $ изменяются эпсилоном, это фактически изменяет обе матрицы одновременно.Правильное решение - просто передать W1 в качестве отдельного параметра, одновременно уменьшая $ \ theta $.

Обновление 2

Хорошо, вот чтоЯ получаю пост слишком поздно ночью.Хотя первое обновление действительно приводит к тому, что все проходит правильно, это не правильное решение.

Я думаю, что правильное решение - это фактически рассчитать градиенты для W1 и W2, а затем установить окончательный градиент для W1в grad_W1 в grad_W2.Аргумент махания рукой состоит в том, что, поскольку весовая матрица действует как для кодирования, так и для декодирования, на ее веса должны влиять оба градиента.Однако я еще не обдумал фактические теоретические последствия этого.

Если я выполню это, используя свою собственную проверку производной, она преодолеет порог 10e-4.Это намного лучше, чем раньше, с производной проверкой minFunc, но все же хуже, чем если бы я не связывал веса.

...