Wire up temperature-based sampling for poet text generation

Greedy argmax decoding in RbmListener::forward always produces the
exact same character sequence and quickly falls into short repeating
loops once the 5-character lookback state revisits a prior cycle.

Add an optional temperature argument (poet f <seed> [temperature]) that
switches decoding to RnnStack::sample_one_hot, which now does proper
categorical sampling (temperature-scaled, renormalized draw) instead of
the old per-code Bernoulli approach that could leave the result as a
non-one-hot probability vector. Also seed Armadillo's RNG in main(),
since it otherwise defaults to a fixed seed and every run would sample
identically.

Add docs/RNN_ARCHITECTURE.md documenting how the RnnStack/Layer stack
implements the RNN (context chaining across layers, training/generation
data flow, and the decoding behavior above).

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 12:34:44 +02:00
co-authored by Claude Sonnet 5
parent 33a0647a51
commit 656a0252d3
4 changed files with 247 additions and 20 deletions
+32 -6
View File
@@ -2,6 +2,7 @@
#include <fstream>
#include <streambuf>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <string>
#include <cassert>
@@ -47,21 +48,37 @@ class RbmListener : public Rbm::IListener
return true;
}
void forward(std::string const &start, size_t len)
// temperature <= 0 means greedy argmax decoding (the original behavior);
// temperature > 0 samples the next character from the (temperature-scaled)
// output distribution instead, which avoids the short repeating loops that
// greedy decoding tends to fall into.
void forward(std::string const &start, size_t len, double temperature=0.0)
{
RnnStack *stack = reinterpret_cast<RnnStack*>(&m_stack);
arma::mat state;
arma::mat curr;
arma::mat next;
std::string curr_str;
auto pickNext = [stack, temperature](arma::mat &next)
{
if (temperature > 0.0)
{
stack->sample_one_hot(next, temperature);
}
else
{
stack->clamp_one_hot(next);
}
};
for (int i=0; i < start.size(); i++)
{
curr = Matutils::char2vec(start.at(i), RnnTextHelper::NUM_CODES);
curr_str.append(1, Matutils::vec2char(curr));
arma::mat r = stack->step_forward(state, curr);
next = stack->to_next(r);
stack->clamp_one_hot(next);
pickNext(next);
}
cout << "Start: " << curr_str << std::endl;
@@ -71,7 +88,7 @@ class RbmListener : public Rbm::IListener
curr_str.append(1, Matutils::vec2char(curr));
arma::mat r = stack->step_forward(state, curr);
next = stack->to_next(r);
stack->clamp_one_hot(next);
pickNext(next);
curr = stack->to_curr(r);
}
cout << "Curr: " << curr_str << std::endl;
@@ -83,6 +100,10 @@ class RbmListener : public Rbm::IListener
int main(int argc, char *argv[])
{
// Armadillo's RNG otherwise defaults to a fixed seed, which would make
// sample_one_hot() produce the exact same "random" text on every run.
arma::arma_rng::set_seed_random();
enum Command {Nop, Create, Reset, Train, Forward};
Command command = Nop;
@@ -145,12 +166,17 @@ int main(int argc, char *argv[])
if (command == Command::Forward)
{
std::string start(DEFAULT_START_WORD);
double temperature = 0.0;
if (argv[2] != nullptr)
{
start = std::string(argv[2]);
}
listener.forward(start, 100);
if (argv[3] != nullptr)
{
temperature = std::atof(argv[3]);
}
listener.forward(start, 100, temperature);
}
printf("\nEnd of program\n");
return 0;