Fix RBM training: invert epoch/mini-batch loop nesting, drop duplicate CD computation

Rbm::train had mini-batch chunks as the outer loop and epochs as the
inner loop: each fixed, never-reshuffled slice of the data got all
numEpochs gradient steps back-to-back before ever being revisited, so
"numEpochs" didn't mean "passes over the whole dataset" and training
was biased toward whatever data came last. Invert the nesting (epochs
outer, mini-batches inner, batch reshuffled via arma::shuffle at the
start of each epoch) so every epoch is an actual full pass over the
data in a fresh random order.

Also drop a redundant toHiddenProbs(v_states) call in cd_jens: the
positive-phase hidden probabilities were computed once unconditionally
and then discarded, recomputed a second time with identical input in
two of the three sampling branches. Same result, half the cost, on
every mini-batch of every epoch of every layer.

Both only affect training; inference (step_forward/generation) is
unchanged, confirmed by identical poet.elf f output before and after.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016K8Gu7Qejd11JbdiHZqYAs
This commit is contained in:
2026-07-27 13:39:58 +02:00
co-authored by Claude Sonnet 5
parent 656a0252d3
commit aabf7385ce
+36 -34
View File
@@ -252,23 +252,20 @@ void Rbm::cd_hinton(arma::mat const &v_data, arma::mat &dw, arma::mat &dbh, arma
void Rbm::cd_jens(arma::mat const &v_states, arma::mat &dw, arma::mat &dbh, arma::mat &dbv)
{
arma::mat v_probs(v_states);
arma::mat h_states = toHiddenProbs(v_states);
arma::mat h_probs;
arma::mat h_probs = toHiddenProbs(v_states);
arma::mat h_states;
// Sample hidden
if (m_params.doGaussianHidden)
{
h_probs = h_states;
h_states = h_probs + arma::randn(h_probs.n_rows, h_probs.n_cols);
}
else if (m_params.doRaoBlackwell)
{
h_probs = toHiddenProbs(v_states);
h_states = h_probs;
}
else
{
h_probs = toHiddenProbs(v_states);
h_states = sample(h_probs);
}
@@ -313,8 +310,7 @@ void Rbm::train(arma::mat const &batch, IListener* pListener)
double dProgress = 100.0/(batch.n_rows*m_params.numEpochs);
double progress = 0;
int lastProgress = -100;
int batchRowIndex = 0;
arma::mat dbv(arma::zeros(1, m_bv.n_cols));
arma::mat dbh(arma::zeros(1, m_bh.n_cols));
arma::mat dwhv(arma::zeros(m_whv.n_rows, m_whv.n_cols));
@@ -322,40 +318,46 @@ void Rbm::train(arma::mat const &batch, IListener* pListener)
arma::mat inc_bv(arma::zeros(1, m_bv.n_cols));
arma::mat inc_bh(arma::zeros(1, m_bh.n_cols));
int trainingSizeRemain = batch.n_rows;
bool shouldAbort = false;
while (trainingSizeRemain && !shouldAbort)
// One epoch = one full pass over the (freshly reshuffled) data, mini-batch
// by mini-batch. Epochs must be the outer loop: nesting mini-batches on
// the outside would run all numEpochs steps on one fixed chunk before ever
// moving to the next, biasing training toward whatever data comes last.
for (int epoch=0; epoch < m_params.numEpochs && !shouldAbort; epoch++)
{
int miniBatchSizeActual = std::min(m_params.miniBatchSize, trainingSizeRemain);
arma::mat miniBatch = batch.rows(batchRowIndex, batchRowIndex+miniBatchSizeActual-1);
trainingSizeRemain -= miniBatchSizeActual;
batchRowIndex += miniBatchSizeActual;
int numcases = std::min(m_params.miniBatchSize, (int)batch.n_rows);
arma::mat shuffledBatch = arma::shuffle(batch);
int trainingSizeRemain = shuffledBatch.n_rows;
int batchRowIndex = 0;
arma::mat v_states(miniBatch);
while (trainingSizeRemain && !shouldAbort)
{
int miniBatchSizeActual = std::min(m_params.miniBatchSize, trainingSizeRemain);
arma::mat miniBatch = shuffledBatch.rows(batchRowIndex, batchRowIndex+miniBatchSizeActual-1);
trainingSizeRemain -= miniBatchSizeActual;
batchRowIndex += miniBatchSizeActual;
int numcases = std::min(m_params.miniBatchSize, (int)batch.n_rows);
arma::mat v_states(miniBatch);
// Create hidden layer base on training data
if (m_params.doSampleBatch)
{
// When the hidden units are being driven by data, always use stochastic binary states
v_states = sample(miniBatch);
}
// Create hidden layer base on training data
if (m_params.doSampleBatch)
{
// When the hidden units are being driven by data, always use stochastic binary states
v_states = sample(miniBatch);
}
for (int epoch=0; epoch < m_params.numEpochs; epoch++)
{
// Contrastive divergence learning: calculate gradients
cd(v_states, dwhv, dbh, dbv);
// Adjust weight and biases
inc_bv = m_params.momentum*inc_bv + m_params.learningRate/numcases*dbv;
inc_bh = m_params.momentum*inc_bh + m_params.learningRate/numcases*dbh;
inc_whv = m_params.momentum*inc_whv + m_params.learningRate*(dwhv/numcases - m_params.weightDecay*m_whv);
m_bv += inc_bv;
m_bh += inc_bh;
m_whv += inc_whv;
progress += dProgress*miniBatchSizeActual;
status.progress = (int)(progress + 0.5);
@@ -363,7 +365,7 @@ void Rbm::train(arma::mat const &batch, IListener* pListener)
if (status.progress != lastProgress)
{
lastProgress = status.progress;
// Calculate error
status.err = rms_error_accu(miniBatch - toVisibleProbs(toHiddenProbs(v_states)));
if (pListener)
@@ -375,10 +377,10 @@ void Rbm::train(arma::mat const &batch, IListener* pListener)
}
}
}
} // Number of epochs
} // number of mini batches
} // mini batches
} // epochs
// Update final status
status.err_total = rms_error_accu(batch - toVisibleProbs(toHiddenProbs(batch)));