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
184 lines
4.1 KiB
C++
184 lines
4.1 KiB
C++
#include <iostream>
|
|
#include <fstream>
|
|
#include <streambuf>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <cassert>
|
|
#include <armadillo>
|
|
#include <jsoncpp/json/json.h>
|
|
#include "Rbm.hpp"
|
|
#include "Layer.hpp"
|
|
#include "RnnStack.hpp"
|
|
#include "StackCreator.hpp"
|
|
#include "RnnTextHelper.hpp"
|
|
|
|
using namespace std;
|
|
using namespace arma;
|
|
|
|
static const char *DEFAULT_START_WORD = "CHAP";
|
|
|
|
class RbmListener : public Rbm::IListener
|
|
{
|
|
public:
|
|
RbmListener(AStack &stack)
|
|
: m_stack(stack)
|
|
, m_last_progress(0)
|
|
{
|
|
}
|
|
virtual ~RbmListener() {}
|
|
|
|
bool onProgress(Rbm *pRbm, const Rbm::Status &status)
|
|
{
|
|
if (status.progress == 0)
|
|
{
|
|
m_last_progress = -10;
|
|
}
|
|
if ((status.progress - m_last_progress) == 10)
|
|
{
|
|
std::cout << "Progress : " << status.progress << " %" << std::endl;
|
|
std::cout << "error (per mini batch) = " << status.err << std::endl;
|
|
std::cout << "error (total) = " << status.err_total << std::endl;
|
|
std::cout << "L1 = " << status.L1 << std::endl;
|
|
std::cout << "L2 = " << status.L2 << std::endl;
|
|
m_last_progress = status.progress;
|
|
forward(DEFAULT_START_WORD, 100);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 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);
|
|
pickNext(next);
|
|
}
|
|
cout << "Start: " << curr_str << std::endl;
|
|
|
|
for (int i=stack->getSeqLen(); i < len; i++)
|
|
{
|
|
curr = next;
|
|
curr_str.append(1, Matutils::vec2char(curr));
|
|
arma::mat r = stack->step_forward(state, curr);
|
|
next = stack->to_next(r);
|
|
pickNext(next);
|
|
curr = stack->to_curr(r);
|
|
}
|
|
cout << "Curr: " << curr_str << std::endl;
|
|
|
|
}
|
|
AStack &m_stack;
|
|
int m_last_progress;
|
|
};
|
|
|
|
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;
|
|
|
|
if (argc > 1)
|
|
{
|
|
char cmd_token = *argv[1];
|
|
if (cmd_token == 'c')
|
|
{
|
|
command = Command::Create;
|
|
}
|
|
else if (cmd_token == 'r')
|
|
{
|
|
command = Command::Reset;
|
|
}
|
|
else if (cmd_token == 't')
|
|
{
|
|
command = Command::Train;
|
|
}
|
|
else if (cmd_token == 'f')
|
|
{
|
|
command = Command::Forward;
|
|
}
|
|
}
|
|
|
|
// Load project
|
|
RnnStack *stack = reinterpret_cast<RnnStack*>(StackCreator::fromFile(".", "poet_2v_5s"));
|
|
|
|
// Load weights
|
|
stack->loadWeights(".");
|
|
|
|
RbmListener listener(*stack);
|
|
|
|
if (command == Command::Reset)
|
|
{
|
|
stack->weightsInit(0.01);
|
|
stack->saveWeights(".");
|
|
}
|
|
|
|
if (command == Command::Train)
|
|
{
|
|
arma::mat batch;
|
|
if (argv[2] != nullptr)
|
|
{
|
|
batch = RnnTextHelper::createTraining(argv[2], RnnTextHelper::SEQ_LENGTH);
|
|
}
|
|
else
|
|
{
|
|
batch = RnnTextHelper::createTraining("batch1.txt", RnnTextHelper::SEQ_LENGTH);
|
|
}
|
|
|
|
// Phase 10
|
|
stack->train(batch, &listener);
|
|
|
|
stack->saveWeights(".");
|
|
#if DO_SAVE_PROJECT_AFTER_TRAINING
|
|
stack->save(".");
|
|
#endif
|
|
}
|
|
|
|
if (command == Command::Forward)
|
|
{
|
|
std::string start(DEFAULT_START_WORD);
|
|
double temperature = 0.0;
|
|
|
|
if (argv[2] != nullptr)
|
|
{
|
|
start = std::string(argv[2]);
|
|
}
|
|
if (argv[3] != nullptr)
|
|
{
|
|
temperature = std::atof(argv[3]);
|
|
}
|
|
listener.forward(start, 100, temperature);
|
|
}
|
|
printf("\nEnd of program\n");
|
|
return 0;
|
|
}
|