Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add optional argument to predict/score functions to avoid covering #59

Merged
merged 6 commits into from
Apr 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

Changes:
* Add an optional argument to the Python predict function that specifies the value to return for a sample if the match set is empty instead of invoking covering.

## Version 1.2.5 (Oct 3, 2022)

Changes:
Expand Down
8 changes: 4 additions & 4 deletions xcsf/clset.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* @file clset.c
* @author Richard Preen <rpreen@gmail.com>
* @copyright The Authors.
* @date 2015--2021.
* @date 2015--2023.
* @brief Functions operating on sets of classifiers.
*/

Expand Down Expand Up @@ -322,12 +322,12 @@ clset_pset_enforce_limit(struct XCSF *xcsf)
* @brief Constructs the match set - forward propagates conditions and actions.
* @details Processes the matching conditions and actions for each classifier
* in the population. If a classifier matches, it is added to the match set.
* Covering is performed if any actions are unrepresented.
* @param [in] xcsf The XCSF data structure.
* @param [in] x The input state.
* @param [in] cover Whether to check action set coverage.
*/
void
clset_match(struct XCSF *xcsf, const double *x)
clset_match(struct XCSF *xcsf, const double *x, const bool cover)
{
#ifdef PARALLEL_MATCH
// prepare for parallel processing of matching conditions
Expand Down Expand Up @@ -361,7 +361,7 @@ clset_match(struct XCSF *xcsf, const double *x)
}
#endif
// perform covering if all actions are not represented
if (xcsf->n_actions > 1 || xcsf->mset.size < 1) {
if (cover && (xcsf->n_actions > 1 || xcsf->mset.size < 1)) {
clset_cover(xcsf, x);
}
// update statistics
Expand Down
4 changes: 2 additions & 2 deletions xcsf/clset.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* @file clset.h
* @author Richard Preen <rpreen@gmail.com>
* @copyright The Authors.
* @date 2015--2020.
* @date 2015--2023.
* @brief Functions operating on sets of classifiers.
*/

Expand Down Expand Up @@ -62,7 +62,7 @@ void
clset_kill(const struct XCSF *xcsf, struct Set *set);

void
clset_match(struct XCSF *xcsf, const double *x);
clset_match(struct XCSF *xcsf, const double *x, const bool cover);

void
clset_pset_enforce_limit(struct XCSF *xcsf);
Expand Down
146 changes: 118 additions & 28 deletions xcsf/pybind_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* @author Richard Preen <rpreen@gmail.com>
* @author David Pätzel
* @copyright The Authors.
* @date 2020--2022.
* @date 2020--2023.
* @brief Python library wrapper functions.
*/

Expand Down Expand Up @@ -393,41 +393,101 @@ class XCS
return xcs_supervised_fit(&xcs, train_data, test_data, shuffle);
}

/**
* @brief Returns the values specified in the cover array.
* @param [in] cover The values to return for covering.
* @return The cover array values.
*/
double *
get_cover(const py::array_t<double> cover)
{
const py::buffer_info buf_c = cover.request();
if (buf_c.ndim != 1) {
std::ostringstream err;
err << "cover must be an array of shape (1, " << xcs.y_dim << ")"
<< std::endl;
throw std::invalid_argument(err.str());
}
if (buf_c.shape[0] != xcs.y_dim || buf_c.shape[1] != 0) {
std::ostringstream err;
err << "cover shape (" << buf_c.shape[1] << ", " << buf_c.shape[0]
<< ") but expected (0, " << xcs.y_dim << ")" << std::endl;
throw std::invalid_argument(err.str());
}
return reinterpret_cast<double *>(buf_c.ptr);
}

/**
* @brief Returns the XCSF prediction array for the provided input.
* @param [in] X The input variables.
* @param [in] cover If cover is not NULL and the match set is empty, the
* prediction array will be set to this value instead of covering.
* @return The prediction array values.
*/
py::array_t<double>
predict(const py::array_t<double> X)
get_predictions(const py::array_t<double> X, const double *cover)
{
const py::buffer_info buf_x = X.request();
const int n_samples = buf_x.shape[0];
if (buf_x.shape[1] != xcs.x_dim) {
std::ostringstream error;
error << "predict(): x_dim is not equal to: " << xcs.x_dim
<< std::endl;
error << "2-D arrays are required. Perhaps reshape your data.";
throw std::invalid_argument(error.str());
std::ostringstream err;
err << "predict(): x_dim (" << buf_x.shape[1]
<< ") is not equal to: " << xcs.x_dim << std::endl;
err << "2-D arrays are required. Perhaps reshape your data.";
throw std::invalid_argument(err.str());
}
const double *input = (double *) buf_x.ptr;
const double *input = reinterpret_cast<double *>(buf_x.ptr);
double *output =
(double *) malloc(sizeof(double) * n_samples * xcs.pa_size);
xcs_supervised_predict(&xcs, input, output, n_samples);
xcs_supervised_predict(&xcs, input, output, n_samples, cover);
return py::array_t<double>(
std::vector<ptrdiff_t>{ n_samples, xcs.pa_size }, output);
}

/**
* @brief Returns the error over one sequential pass of the provided data.
* @brief Returns the XCSF prediction array for the provided input.
* @param [in] X The input variables.
* @param [in] cover If the match set is empty, the prediction array will
* be set to this value instead of covering.
* @return The prediction array values.
*/
py::array_t<double>
predict(const py::array_t<double> X, const py::array_t<double> cover)
{
const double *cov = get_cover(cover);
return get_predictions(X, cov);
}

/**
* @brief Returns the XCSF prediction array for the provided input,
* and executes covering for samples where the match set is empty.
* @param [in] X The input variables.
* @return The prediction array values.
*/
py::array_t<double>
predict(const py::array_t<double> X)
{
return get_predictions(X, NULL);
}

/**
* @brief Returns the error using N random samples from the provided data.
* @param [in] X The input values to use for scoring.
* @param [in] Y The true output values to use for scoring.
* @param [in] N The maximum number of samples to draw randomly for scoring.
* @param [in] cover If the match set is empty, the prediction array will
* be set to this value instead of covering.
* @return The average XCSF error using the loss function.
*/
double
score(const py::array_t<double> X, const py::array_t<double> Y)
get_score(const py::array_t<double> X, const py::array_t<double> Y,
const int N, const double *cover)
{
return score(X, Y, 0);
load_input(test_data, X, Y);
if (N > 1) {
return xcs_supervised_score_n(&xcs, test_data, N, cover);
}
return xcs_supervised_score(&xcs, test_data, cover);
}

/**
Expand All @@ -440,11 +500,24 @@ class XCS
double
score(const py::array_t<double> X, const py::array_t<double> Y, const int N)
{
load_input(test_data, X, Y);
if (N > 1) {
return xcs_supervised_score_n(&xcs, test_data, N);
}
return xcs_supervised_score(&xcs, test_data);
return get_score(X, Y, N, NULL);
}

/**
* @brief Returns the error using N random samples from the provided data.
* @param [in] X The input values to use for scoring.
* @param [in] Y The true output values to use for scoring.
* @param [in] N The maximum number of samples to draw randomly for scoring.
* @param [in] cover If the match set is empty, the prediction array will
* be set to this value instead of covering.
* @return The average XCSF error using the loss function.
*/
double
score(const py::array_t<double> X, const py::array_t<double> Y, const int N,
const py::array_t<double> cover)
{
const double *cov = get_cover(cover);
return get_score(X, Y, N, cov);
}

/* GETTERS */
Expand Down Expand Up @@ -1228,12 +1301,20 @@ PYBIND11_MODULE(xcsf, m)
const py::array_t<double>, const py::array_t<double>,
const bool) = &XCS::fit;

double (XCS::*score1)(const py::array_t<double> test_X,
const py::array_t<double> test_Y) = &XCS::score;
double (XCS::*score2)(const py::array_t<double> test_X,
const py::array_t<double> test_Y, const int N) =
py::array_t<double> (XCS::*predict1)(const py::array_t<double> test_X) =
&XCS::predict;
py::array_t<double> (XCS::*predict2)(const py::array_t<double> test_X,
const py::array_t<double> cover) =
&XCS::predict;

double (XCS::*score1)(const py::array_t<double> X,
const py::array_t<double> Y, const int N) =
&XCS::score;

double (XCS::*score2)(const py::array_t<double> X,
const py::array_t<double> Y, const int N,
const py::array_t<double> cover) = &XCS::score;

double (XCS::*error1)(void) = &XCS::error;
double (XCS::*error2)(const double, const bool, const double) = &XCS::error;

Expand Down Expand Up @@ -1291,26 +1372,35 @@ PYBIND11_MODULE(xcsf, m)
py::arg("X_train"), py::arg("y_train"), py::arg("X_test"),
py::arg("y_test"), py::arg("shuffle"))
.def("score", score1,
"Returns the error over one sequential pass of the provided data. "
"X_val shape must be: (n_samples, x_dim). y_val shape must be: "
"(n_samples, y_dim).",
py::arg("X_val"), py::arg("y_val"))
"Returns the error using at most N random samples from the "
"provided data. X_val shape must be: (n_samples, x_dim). y_val "
"shape must be: (n_samples, y_dim).",
py::arg("X_val"), py::arg("y_val"), py::arg("N") = 0)
.def("score", score2,
"Returns the error using at most N random samples from the "
"provided data. X_val shape must be: (n_samples, x_dim). y_val "
"shape must be: (n_samples, y_dim).",
py::arg("X_val"), py::arg("y_val"), py::arg("N"))
py::arg("X_val"), py::arg("y_val"), py::arg("N") = 0,
py::arg("cover"))
.def("error", error1,
"Returns a moving average of the system error, updated with step "
"size BETA.")
.def("error", error2,
"Returns the reinforcement learning system prediction error.",
py::arg("reward"), py::arg("done"), py::arg("max_p"))
.def("predict", &XCS::predict,
.def("predict", predict1,
"Returns the XCSF prediction array for the provided input. X_test "
"shape must be: (n_samples, x_dim). Returns an array of shape: "
"(n_samples, y_dim).",
"(n_samples, y_dim). Covering will be invoked for samples where "
"the match set is empty.",
py::arg("X_test"))
.def("predict", predict2,
"Returns the XCSF prediction array for the provided input. X_test "
"shape must be: (n_samples, x_dim). Returns an array of shape: "
"(n_samples, y_dim). If the match set is empty for a sample, the "
"value of the cover array will be used instead of covering. "
"cover must be an array of shape: y_dim.",
py::arg("X_test"), py::arg("cover"))
.def("save", &XCS::save,
"Saves the current state of XCSF to persistent storage.",
py::arg("filename"))
Expand Down
6 changes: 3 additions & 3 deletions xcsf/xcs_rl.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* @file xcs_rl.c
* @author Richard Preen <rpreen@gmail.com>
* @copyright The Authors.
* @date 2015--2020.
* @date 2015--2023.
* @brief Reinforcement learning functions.
* @details A trial consists of one or more steps.
*/
Expand Down Expand Up @@ -106,7 +106,7 @@ xcs_rl_fit(struct XCSF *xcsf, const double *state, const int action,
{
xcs_rl_init_trial(xcsf);
xcs_rl_init_step(xcsf);
clset_match(xcsf, state);
clset_match(xcsf, state, true);
pa_build(xcsf, state);
const double prediction = pa_val(xcsf, action);
const double error = (xcsf->loss_ptr)(xcsf, &prediction, &reward);
Expand Down Expand Up @@ -249,7 +249,7 @@ xcs_rl_error(struct XCSF *xcsf, const int action, const double reward,
int
xcs_rl_decision(struct XCSF *xcsf, const double *state)
{
clset_match(xcsf, state);
clset_match(xcsf, state, true);
pa_build(xcsf, state);
if (xcsf->explore && rand_uniform(0, 1) < xcsf->P_EXPLORE) {
return pa_rand_action(xcsf);
Expand Down
Loading