Merge pull request 'Create version 1.2.1' (#40) from ldi into main

Reviewed-on: #40
This commit is contained in:
2025-07-19 20:42:25 +00:00
24 changed files with 748 additions and 140 deletions

View File

@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [1.2.0] - 2025-06-30 ## [1.2.0] - 2025-07-08
### Internal ### Internal
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- *ld_proposed_cuts*: number of cut points to return. - *ld_proposed_cuts*: number of cut points to return.
- *mdlp_min_length*: minimum length of a partition in MDLP algorithm to be evaluated for partition. - *mdlp_min_length*: minimum length of a partition in MDLP algorithm to be evaluated for partition.
- *mdlp_max_depth*: maximum level of recursion in MDLP algorithm. - *mdlp_max_depth*: maximum level of recursion in MDLP algorithm.
- *max_iterations*: maximum number of iterations of discretization-build model loop.
- *verbose_convergence*: display status messages during the convergence process.
- Remove vcpkg as a dependency manager, now the library is built with Conan package manager and CMake. - Remove vcpkg as a dependency manager, now the library is built with Conan package manager and CMake.
- Add `build_type` option to the sample target in the Makefile to allow building in *Debug* or *Release* mode. Default is *Debug*. - Add `build_type` option to the sample target in the Makefile to allow building in *Debug* or *Release* mode. Default is *Debug*.

View File

@@ -17,6 +17,14 @@ mansrcdir = docs/man3
mandestdir = /usr/local/share/man mandestdir = /usr/local/share/man
sed_command_link = 's/e">LCOV -/e"><a href="https:\/\/rmontanana.github.io\/bayesnet">Back to manual<\/a> LCOV -/g' sed_command_link = 's/e">LCOV -/e"><a href="https:\/\/rmontanana.github.io\/bayesnet">Back to manual<\/a> LCOV -/g'
sed_command_diagram = 's/Diagram"/Diagram" width="100%" height="100%" /g' sed_command_diagram = 's/Diagram"/Diagram" width="100%" height="100%" /g'
# Set the number of parallel jobs to the number of available processors minus 7
CPUS := $(shell getconf _NPROCESSORS_ONLN 2>/dev/null \
|| nproc --all 2>/dev/null \
|| sysctl -n hw.ncpu)
# --- Your desired job count: CPUs 7, but never less than 1 --------------
JOBS := $(shell n=$(CPUS); [ $${n} -gt 7 ] && echo $$((n-7)) || echo 1)
define ClearTests define ClearTests
@for t in $(test_targets); do \ @for t in $(test_targets); do \
@@ -36,6 +44,7 @@ define setup_target
@if [ -d $(2) ]; then rm -fr $(2); fi @if [ -d $(2) ]; then rm -fr $(2); fi
@conan install . --build=missing -of $(2) -s build_type=$(1) @conan install . --build=missing -of $(2) -s build_type=$(1)
@cmake -S . -B $(2) -DCMAKE_TOOLCHAIN_FILE=$(2)/build/$(1)/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=$(1) -D$(3) @cmake -S . -B $(2) -DCMAKE_TOOLCHAIN_FILE=$(2)/build/$(1)/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=$(1) -D$(3)
@echo ">>> Will build using $(JOBS) parallel jobs"
@echo ">>> Done" @echo ">>> Done"
endef endef
@@ -72,10 +81,10 @@ release: ## Setup release version using Conan
@$(call setup_target,"Release","$(f_release)","ENABLE_TESTING=OFF") @$(call setup_target,"Release","$(f_release)","ENABLE_TESTING=OFF")
buildd: ## Build the debug targets buildd: ## Build the debug targets
cmake --build $(f_debug) --config Debug -t $(app_targets) --parallel $(CMAKE_BUILD_PARALLEL_LEVEL) cmake --build $(f_debug) --config Debug -t $(app_targets) --parallel $(JOBS)
buildr: ## Build the release targets buildr: ## Build the release targets
cmake --build $(f_release) --config Release -t $(app_targets) --parallel $(CMAKE_BUILD_PARALLEL_LEVEL) cmake --build $(f_release) --config Release -t $(app_targets) --parallel $(JOBS)
# Install targets # Install targets
@@ -105,7 +114,7 @@ opt = ""
test: ## Run tests (opt="-s") to verbose output the tests, (opt="-c='Test Maximum Spanning Tree'") to run only that section test: ## Run tests (opt="-s") to verbose output the tests, (opt="-c='Test Maximum Spanning Tree'") to run only that section
@echo ">>> Running BayesNet tests..."; @echo ">>> Running BayesNet tests...";
@$(MAKE) clean-test @$(MAKE) clean-test
@cmake --build $(f_debug) -t $(test_targets) --parallel $(CMAKE_BUILD_PARALLEL_LEVEL) @cmake --build $(f_debug) -t $(test_targets) --parallel $(JOBS)
@for t in $(test_targets); do \ @for t in $(test_targets); do \
echo ">>> Running $$t...";\ echo ">>> Running $$t...";\
if [ -f $(f_debug)/tests/$$t ]; then \ if [ -f $(f_debug)/tests/$$t ]; then \
@@ -228,7 +237,7 @@ sample: ## Build sample with Conan
@if [ -d ./sample/build ]; then rm -rf ./sample/build; fi @if [ -d ./sample/build ]; then rm -rf ./sample/build; fi
@cd sample && conan install . --output-folder=build --build=missing -s build_type=$(build_type) -o "&:enable_coverage=False" -o "&:enable_testing=False" @cd sample && conan install . --output-folder=build --build=missing -s build_type=$(build_type) -o "&:enable_coverage=False" -o "&:enable_testing=False"
@cd sample && cmake -B build -S . -DCMAKE_BUILD_TYPE=$(build_type) -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake && \ @cd sample && cmake -B build -S . -DCMAKE_BUILD_TYPE=$(build_type) -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake && \
cmake --build build -t bayesnet_sample cmake --build build -t bayesnet_sample --parallel $(JOBS)
sample/build/bayesnet_sample $(fname) $(model) sample/build/bayesnet_sample $(fname) $(model)
@echo ">>> Done"; @echo ">>> Done";

View File

@@ -8,7 +8,7 @@
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=rmontanana_BayesNet&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=rmontanana_BayesNet) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=rmontanana_BayesNet&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=rmontanana_BayesNet)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Doctorado-ML/BayesNet) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Doctorado-ML/BayesNet)
![Gitea Last Commit](https://img.shields.io/gitea/last-commit/rmontanana/bayesnet?gitea_url=https://gitea.rmontanana.es&logo=gitea) ![Gitea Last Commit](https://img.shields.io/gitea/last-commit/rmontanana/bayesnet?gitea_url=https://gitea.rmontanana.es&logo=gitea)
[![Coverage Badge](https://img.shields.io/badge/Coverage-98,0%25-green)](https://gitea.rmontanana.es/rmontanana/BayesNet) [![Coverage Badge](https://img.shields.io/badge/Coverage-99,1%25-green)](https://gitea.rmontanana.es/rmontanana/BayesNet)
[![DOI](https://zenodo.org/badge/667782806.svg)](https://doi.org/10.5281/zenodo.14210344) [![DOI](https://zenodo.org/badge/667782806.svg)](https://doi.org/10.5281/zenodo.14210344)
Bayesian Network Classifiers library Bayesian Network Classifiers library

View File

@@ -37,6 +37,7 @@ namespace bayesnet {
std::vector<std::string> getNotes() const override { return notes; } std::vector<std::string> getNotes() const override { return notes; }
std::string dump_cpt() const override; std::string dump_cpt() const override;
void setHyperparameters(const nlohmann::json& hyperparameters) override; //For classifiers that don't have hyperparameters void setHyperparameters(const nlohmann::json& hyperparameters) override; //For classifiers that don't have hyperparameters
Network& getModel() { return model; }
protected: protected:
bool fitted; bool fitted;
unsigned int m, n; // m: number of samples, n: number of features unsigned int m, n; // m: number of samples, n: number of features

View File

@@ -5,40 +5,38 @@
// *************************************************************** // ***************************************************************
#include "KDBLd.h" #include "KDBLd.h"
#include <memory>
namespace bayesnet { namespace bayesnet {
KDBLd::KDBLd(int k) : KDB(k), Proposal(dataset, features, className) KDBLd::KDBLd(int k) : KDB(k), Proposal(dataset, features, className, KDB::notes)
{ {
validHyperparameters = validHyperparameters_ld; validHyperparameters = validHyperparameters_ld;
validHyperparameters.push_back("k"); validHyperparameters.push_back("k");
validHyperparameters.push_back("theta"); validHyperparameters.push_back("theta");
} }
void KDBLd::setHyperparameters(const nlohmann::json& hyperparameters_)
{
auto hyperparameters = hyperparameters_;
if (hyperparameters.contains("k")) {
k = hyperparameters["k"];
hyperparameters.erase("k");
}
if (hyperparameters.contains("theta")) {
theta = hyperparameters["theta"];
hyperparameters.erase("theta");
}
Proposal::setHyperparameters(hyperparameters);
}
KDBLd& KDBLd::fit(torch::Tensor& X_, torch::Tensor& y_, const std::vector<std::string>& features_, const std::string& className_, map<std::string, std::vector<int>>& states_, const Smoothing_t smoothing) KDBLd& KDBLd::fit(torch::Tensor& X_, torch::Tensor& y_, const std::vector<std::string>& features_, const std::string& className_, map<std::string, std::vector<int>>& states_, const Smoothing_t smoothing)
{ {
checkInput(X_, y_); checkInput(X_, y_);
features = features_;
className = className_;
Xf = X_; Xf = X_;
y = y_; y = y_;
// Fills std::vectors Xv & yv with the data from tensors X_ (discretized) & y return commonFit(features_, className_, states_, smoothing);
states = fit_local_discretization(y); }
// We have discretized the input data KDBLd& KDBLd::fit(torch::Tensor& dataset, const std::vector<std::string>& features_, const std::string& className_, map<std::string, std::vector<int>>& states_, const Smoothing_t smoothing)
// 1st we need to fit the model to build the normal KDB structure, KDB::fit initializes the base Bayesian network {
if (!torch::is_floating_point(dataset)) {
throw std::runtime_error("Dataset must be a floating point tensor");
}
Xf = dataset.index({ torch::indexing::Slice(0, dataset.size(0) - 1), "..." }).clone();
y = dataset.index({ -1, "..." }).clone().to(torch::kInt32);
return commonFit(features_, className_, states_, smoothing);
}
KDBLd& KDBLd::commonFit(const std::vector<std::string>& features_, const std::string& className_, map<std::string, std::vector<int>>& states_, const Smoothing_t smoothing)
{
features = features_;
className = className_;
states = iterativeLocalDiscretization(y, static_cast<KDB*>(this), dataset, features, className, states_, smoothing);
KDB::fit(dataset, features, className, states, smoothing); KDB::fit(dataset, features, className, states, smoothing);
states = localDiscretizationProposal(states, model);
return *this; return *this;
} }
torch::Tensor KDBLd::predict(torch::Tensor& X) torch::Tensor KDBLd::predict(torch::Tensor& X)

View File

@@ -15,8 +15,15 @@ namespace bayesnet {
explicit KDBLd(int k); explicit KDBLd(int k);
virtual ~KDBLd() = default; virtual ~KDBLd() = default;
KDBLd& fit(torch::Tensor& X, torch::Tensor& y, const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing) override; KDBLd& fit(torch::Tensor& X, torch::Tensor& y, const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing) override;
KDBLd& fit(torch::Tensor& dataset, const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing) override;
KDBLd& commonFit(const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing);
std::vector<std::string> graph(const std::string& name = "KDB") const override; std::vector<std::string> graph(const std::string& name = "KDB") const override;
void setHyperparameters(const nlohmann::json& hyperparameters_) override; void setHyperparameters(const nlohmann::json& hyperparameters_) override
{
auto hyperparameters = hyperparameters_;
Proposal::setHyperparameters(hyperparameters);
KDB::setHyperparameters(hyperparameters);
}
torch::Tensor predict(torch::Tensor& X) override; torch::Tensor predict(torch::Tensor& X) override;
torch::Tensor predict_proba(torch::Tensor& X) override; torch::Tensor predict_proba(torch::Tensor& X) override;
static inline std::string version() { return "0.0.1"; }; static inline std::string version() { return "0.0.1"; };

View File

@@ -5,14 +5,22 @@
// *************************************************************** // ***************************************************************
#include "Proposal.h" #include "Proposal.h"
#include <iostream>
#include <cmath>
#include <limits>
#include "Classifier.h"
#include "KDB.h"
#include "TAN.h"
#include "SPODE.h"
#include "KDBLd.h"
#include "TANLd.h"
namespace bayesnet { namespace bayesnet {
Proposal::Proposal(torch::Tensor& dataset_, std::vector<std::string>& features_, std::string& className_) : pDataset(dataset_), pFeatures(features_), pClassName(className_) Proposal::Proposal(torch::Tensor& dataset_, std::vector<std::string>& features_, std::string& className_, std::vector<std::string>& notes_) : pDataset(dataset_), pFeatures(features_), pClassName(className_), notes(notes_)
{ {
} }
void Proposal::setHyperparameters(const nlohmann::json& hyperparameters_) void Proposal::setHyperparameters(nlohmann::json& hyperparameters)
{ {
auto hyperparameters = hyperparameters_;
if (hyperparameters.contains("ld_proposed_cuts")) { if (hyperparameters.contains("ld_proposed_cuts")) {
ld_params.proposed_cuts = hyperparameters["ld_proposed_cuts"]; ld_params.proposed_cuts = hyperparameters["ld_proposed_cuts"];
hyperparameters.erase("ld_proposed_cuts"); hyperparameters.erase("ld_proposed_cuts");
@@ -38,8 +46,14 @@ namespace bayesnet {
throw std::invalid_argument("Invalid discretization algorithm: " + algorithm.get<std::string>()); throw std::invalid_argument("Invalid discretization algorithm: " + algorithm.get<std::string>());
} }
} }
if (!hyperparameters.empty()) { // Convergence parameters
throw std::invalid_argument("Invalid hyperparameters for Proposal: " + hyperparameters.dump()); if (hyperparameters.contains("max_iterations")) {
convergence_params.maxIterations = hyperparameters["max_iterations"];
hyperparameters.erase("max_iterations");
}
if (hyperparameters.contains("verbose_convergence")) {
convergence_params.verbose = hyperparameters["verbose_convergence"];
hyperparameters.erase("verbose_convergence");
} }
} }
@@ -163,4 +177,65 @@ namespace bayesnet {
} }
return yy; return yy;
} }
template<typename Classifier>
map<std::string, std::vector<int>> Proposal::iterativeLocalDiscretization(
const torch::Tensor& y,
Classifier* classifier,
torch::Tensor& dataset,
const std::vector<std::string>& features,
const std::string& className,
const map<std::string, std::vector<int>>& initialStates,
Smoothing_t smoothing
)
{
// Phase 1: Initial discretization (same as original)
auto currentStates = fit_local_discretization(y);
auto previousModel = Network();
if (convergence_params.verbose) {
std::cout << "Starting iterative local discretization with "
<< convergence_params.maxIterations << " max iterations" << std::endl;
}
const torch::Tensor weights = torch::full({ pDataset.size(1) }, 1.0 / pDataset.size(1), torch::kDouble);
for (int iteration = 0; iteration < convergence_params.maxIterations; ++iteration) {
if (convergence_params.verbose) {
std::cout << "Iteration " << (iteration + 1) << "/" << convergence_params.maxIterations << std::endl;
}
// Phase 2: Build model with current discretization
classifier->fit(dataset, features, className, currentStates, weights, smoothing);
// Phase 3: Network-aware discretization refinement
currentStates = localDiscretizationProposal(currentStates, classifier->getModel());
// Check convergence
if (iteration > 0 && previousModel == classifier->getModel()) {
if (convergence_params.verbose) {
std::cout << "Converged after " << (iteration + 1) << " iterations" << std::endl;
}
notes.push_back("Converged after " + std::to_string(iteration + 1) + " of "
+ std::to_string(convergence_params.maxIterations) + " iterations");
break;
}
// Update for next iteration
previousModel = classifier->getModel();
}
return currentStates;
}
// Explicit template instantiation for common classifier types
template map<std::string, std::vector<int>> Proposal::iterativeLocalDiscretization<KDB>(
const torch::Tensor&, KDB*, torch::Tensor&, const std::vector<std::string>&,
const std::string&, const map<std::string, std::vector<int>>&, Smoothing_t);
template map<std::string, std::vector<int>> Proposal::iterativeLocalDiscretization<TAN>(
const torch::Tensor&, TAN*, torch::Tensor&, const std::vector<std::string>&,
const std::string&, const map<std::string, std::vector<int>>&, Smoothing_t);
template map<std::string, std::vector<int>> Proposal::iterativeLocalDiscretization<SPODE>(
const torch::Tensor&, SPODE*, torch::Tensor&, const std::vector<std::string>&,
const std::string&, const map<std::string, std::vector<int>>&, Smoothing_t);
} }

View File

@@ -18,25 +18,50 @@
namespace bayesnet { namespace bayesnet {
class Proposal { class Proposal {
public: public:
Proposal(torch::Tensor& pDataset, std::vector<std::string>& features_, std::string& className_); Proposal(torch::Tensor& pDataset, std::vector<std::string>& features_, std::string& className_, std::vector<std::string>& notes);
void setHyperparameters(const nlohmann::json& hyperparameters_); void setHyperparameters(nlohmann::json& hyperparameters_);
protected: protected:
void checkInput(const torch::Tensor& X, const torch::Tensor& y); void checkInput(const torch::Tensor& X, const torch::Tensor& y);
torch::Tensor prepareX(torch::Tensor& X); torch::Tensor prepareX(torch::Tensor& X);
map<std::string, std::vector<int>> localDiscretizationProposal(const map<std::string, std::vector<int>>& states, Network& model); map<std::string, std::vector<int>> localDiscretizationProposal(const map<std::string, std::vector<int>>& states, Network& model);
map<std::string, std::vector<int>> fit_local_discretization(const torch::Tensor& y); map<std::string, std::vector<int>> fit_local_discretization(const torch::Tensor& y);
// Iterative discretization method
template<typename Classifier>
map<std::string, std::vector<int>> iterativeLocalDiscretization(
const torch::Tensor& y,
Classifier* classifier,
torch::Tensor& dataset,
const std::vector<std::string>& features,
const std::string& className,
const map<std::string, std::vector<int>>& initialStates,
const Smoothing_t smoothing
);
torch::Tensor Xf; // X continuous nxm tensor torch::Tensor Xf; // X continuous nxm tensor
torch::Tensor y; // y discrete nx1 tensor torch::Tensor y; // y discrete nx1 tensor
map<std::string, std::unique_ptr<mdlp::Discretizer>> discretizers; map<std::string, std::unique_ptr<mdlp::Discretizer>> discretizers;
// MDLP parameters // MDLP parameters
struct { struct {
size_t min_length = 3; // Minimum length of the interval to consider it in mdlp size_t min_length = 3; // Minimum length of the interval to consider it in mdlp
float proposed_cuts = 0.0; // Proposed cuts for the Discretization algorithm float proposed_cuts = 0.0; // Proposed cuts for the Discretization algorithm
int max_depth = std::numeric_limits<int>::max(); // Maximum depth of the MDLP tree int max_depth = std::numeric_limits<int>::max(); // Maximum depth of the MDLP tree
} ld_params; } ld_params;
nlohmann::json validHyperparameters_ld = { "ld_algorithm", "ld_proposed_cuts", "mdlp_min_length", "mdlp_max_depth" };
// Convergence parameters
struct {
int maxIterations = 10;
bool verbose = false;
} convergence_params;
nlohmann::json validHyperparameters_ld = {
"ld_algorithm", "ld_proposed_cuts", "mdlp_min_length", "mdlp_max_depth",
"max_iterations", "verbose_convergence"
};
private: private:
std::vector<int> factorize(const std::vector<std::string>& labels_t); std::vector<int> factorize(const std::vector<std::string>& labels_t);
std::vector<std::string>& notes; // Notes during fit from BaseClassifier
torch::Tensor& pDataset; // (n+1)xm tensor torch::Tensor& pDataset; // (n+1)xm tensor
std::vector<std::string>& pFeatures; std::vector<std::string>& pFeatures;
std::string& pClassName; std::string& pClassName;

View File

@@ -7,7 +7,7 @@
#include "SPODELd.h" #include "SPODELd.h"
namespace bayesnet { namespace bayesnet {
SPODELd::SPODELd(int root) : SPODE(root), Proposal(dataset, features, className) SPODELd::SPODELd(int root) : SPODE(root), Proposal(dataset, features, className, SPODE::notes)
{ {
validHyperparameters = validHyperparameters_ld; // Inherits the valid hyperparameters from Proposal validHyperparameters = validHyperparameters_ld; // Inherits the valid hyperparameters from Proposal
} }
@@ -34,12 +34,8 @@ namespace bayesnet {
{ {
features = features_; features = features_;
className = className_; className = className_;
// Fills std::vectors Xv & yv with the data from tensors X_ (discretized) & y states = iterativeLocalDiscretization(y, static_cast<SPODE*>(this), dataset, features, className, states_, smoothing);
states = fit_local_discretization(y);
// We have discretized the input data
// 1st we need to fit the model to build the normal SPODE structure, SPODE::fit initializes the base Bayesian network
SPODE::fit(dataset, features, className, states, smoothing); SPODE::fit(dataset, features, className, states, smoothing);
states = localDiscretizationProposal(states, model);
return *this; return *this;
} }
torch::Tensor SPODELd::predict(torch::Tensor& X) torch::Tensor SPODELd::predict(torch::Tensor& X)

View File

@@ -18,6 +18,12 @@ namespace bayesnet {
SPODELd& fit(torch::Tensor& dataset, const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing) override; SPODELd& fit(torch::Tensor& dataset, const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing) override;
SPODELd& commonFit(const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing); SPODELd& commonFit(const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing);
std::vector<std::string> graph(const std::string& name = "SPODELd") const override; std::vector<std::string> graph(const std::string& name = "SPODELd") const override;
void setHyperparameters(const nlohmann::json& hyperparameters_) override
{
auto hyperparameters = hyperparameters_;
Proposal::setHyperparameters(hyperparameters);
SPODE::setHyperparameters(hyperparameters);
}
torch::Tensor predict(torch::Tensor& X) override; torch::Tensor predict(torch::Tensor& X) override;
torch::Tensor predict_proba(torch::Tensor& X) override; torch::Tensor predict_proba(torch::Tensor& X) override;
static inline std::string version() { return "0.0.1"; }; static inline std::string version() { return "0.0.1"; };

View File

@@ -5,24 +5,37 @@
// *************************************************************** // ***************************************************************
#include "TANLd.h" #include "TANLd.h"
#include <memory>
namespace bayesnet { namespace bayesnet {
TANLd::TANLd() : TAN(), Proposal(dataset, features, className) {} TANLd::TANLd() : TAN(), Proposal(dataset, features, className, TAN::notes)
{
validHyperparameters = validHyperparameters_ld; // Inherits the valid hyperparameters from Proposal
}
TANLd& TANLd::fit(torch::Tensor& X_, torch::Tensor& y_, const std::vector<std::string>& features_, const std::string& className_, map<std::string, std::vector<int>>& states_, const Smoothing_t smoothing) TANLd& TANLd::fit(torch::Tensor& X_, torch::Tensor& y_, const std::vector<std::string>& features_, const std::string& className_, map<std::string, std::vector<int>>& states_, const Smoothing_t smoothing)
{ {
checkInput(X_, y_); checkInput(X_, y_);
features = features_;
className = className_;
Xf = X_; Xf = X_;
y = y_; y = y_;
// Fills std::vectors Xv & yv with the data from tensors X_ (discretized) & y return commonFit(features_, className_, states_, smoothing);
states = fit_local_discretization(y); }
// We have discretized the input data TANLd& TANLd::fit(torch::Tensor& dataset, const std::vector<std::string>& features_, const std::string& className_, map<std::string, std::vector<int>>& states_, const Smoothing_t smoothing)
// 1st we need to fit the model to build the normal TAN structure, TAN::fit initializes the base Bayesian network {
TAN::fit(dataset, features, className, states, smoothing); if (!torch::is_floating_point(dataset)) {
states = localDiscretizationProposal(states, model); throw std::runtime_error("Dataset must be a floating point tensor");
return *this; }
Xf = dataset.index({ torch::indexing::Slice(0, dataset.size(0) - 1), "..." }).clone();
y = dataset.index({ -1, "..." }).clone().to(torch::kInt32);
return commonFit(features_, className_, states_, smoothing);
}
TANLd& TANLd::commonFit(const std::vector<std::string>& features_, const std::string& className_, map<std::string, std::vector<int>>& states_, const Smoothing_t smoothing)
{
features = features_;
className = className_;
states = iterativeLocalDiscretization(y, static_cast<TAN*>(this), dataset, features, className, states_, smoothing);
TAN::fit(dataset, features, className, states, smoothing);
return *this;
} }
torch::Tensor TANLd::predict(torch::Tensor& X) torch::Tensor TANLd::predict(torch::Tensor& X)
{ {

View File

@@ -16,7 +16,15 @@ namespace bayesnet {
TANLd(); TANLd();
virtual ~TANLd() = default; virtual ~TANLd() = default;
TANLd& fit(torch::Tensor& X, torch::Tensor& y, const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing) override; TANLd& fit(torch::Tensor& X, torch::Tensor& y, const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing) override;
TANLd& fit(torch::Tensor& dataset, const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing) override;
TANLd& commonFit(const std::vector<std::string>& features, const std::string& className, map<std::string, std::vector<int>>& states, const Smoothing_t smoothing);
std::vector<std::string> graph(const std::string& name = "TANLd") const override; std::vector<std::string> graph(const std::string& name = "TANLd") const override;
void setHyperparameters(const nlohmann::json& hyperparameters_) override
{
auto hyperparameters = hyperparameters_;
Proposal::setHyperparameters(hyperparameters);
TAN::setHyperparameters(hyperparameters);
}
torch::Tensor predict(torch::Tensor& X) override; torch::Tensor predict(torch::Tensor& X) override;
torch::Tensor predict_proba(torch::Tensor& X) override; torch::Tensor predict_proba(torch::Tensor& X) override;
}; };

View File

@@ -7,7 +7,7 @@
#include "AODELd.h" #include "AODELd.h"
namespace bayesnet { namespace bayesnet {
AODELd::AODELd(bool predict_voting) : Ensemble(predict_voting), Proposal(dataset, features, className) AODELd::AODELd(bool predict_voting) : Ensemble(predict_voting), Proposal(dataset, features, className, Ensemble::notes)
{ {
validHyperparameters = validHyperparameters_ld; // Inherits the valid hyperparameters from Proposal validHyperparameters = validHyperparameters_ld; // Inherits the valid hyperparameters from Proposal
} }

View File

@@ -17,6 +17,10 @@ namespace bayesnet {
virtual ~AODELd() = default; virtual ~AODELd() = default;
AODELd& fit(torch::Tensor& X_, torch::Tensor& y_, const std::vector<std::string>& features_, const std::string& className_, map<std::string, std::vector<int>>& states_, const Smoothing_t smoothing) override; AODELd& fit(torch::Tensor& X_, torch::Tensor& y_, const std::vector<std::string>& features_, const std::string& className_, map<std::string, std::vector<int>>& states_, const Smoothing_t smoothing) override;
std::vector<std::string> graph(const std::string& name = "AODELd") const override; std::vector<std::string> graph(const std::string& name = "AODELd") const override;
void setHyperparameters(const nlohmann::json& hyperparameters_) override
{
hyperparameters = hyperparameters_;
}
protected: protected:
void trainModel(const torch::Tensor& weights, const Smoothing_t smoothing) override; void trainModel(const torch::Tensor& weights, const Smoothing_t smoothing) override;
void buildModel(const torch::Tensor& weights) override; void buildModel(const torch::Tensor& weights) override;

View File

@@ -17,14 +17,90 @@ namespace bayesnet {
Network::Network() : fitted{ false }, classNumStates{ 0 } Network::Network() : fitted{ false }, classNumStates{ 0 }
{ {
} }
Network::Network(const Network& other) : features(other.features), className(other.className), classNumStates(other.getClassNumStates()), Network::Network(const Network& other)
fitted(other.fitted), samples(other.samples) : features(other.features), className(other.className), classNumStates(other.classNumStates),
fitted(other.fitted)
{ {
if (samples.defined()) // Deep copy the samples tensor
samples = samples.clone(); if (other.samples.defined()) {
samples = other.samples.clone();
}
// First, create all nodes (without relationships)
for (const auto& node : other.nodes) { for (const auto& node : other.nodes) {
nodes[node.first] = std::make_unique<Node>(*node.second); nodes[node.first] = std::make_unique<Node>(*node.second);
} }
// Second, reconstruct the relationships between nodes
for (const auto& node : other.nodes) {
const std::string& nodeName = node.first;
Node* originalNode = node.second.get();
Node* newNode = nodes[nodeName].get();
// Reconstruct parent relationships
for (Node* parent : originalNode->getParents()) {
const std::string& parentName = parent->getName();
if (nodes.find(parentName) != nodes.end()) {
newNode->addParent(nodes[parentName].get());
}
}
// Reconstruct child relationships
for (Node* child : originalNode->getChildren()) {
const std::string& childName = child->getName();
if (nodes.find(childName) != nodes.end()) {
newNode->addChild(nodes[childName].get());
}
}
}
}
Network& Network::operator=(const Network& other)
{
if (this != &other) {
// Clear existing state
nodes.clear();
features = other.features;
className = other.className;
classNumStates = other.classNumStates;
fitted = other.fitted;
// Deep copy the samples tensor
if (other.samples.defined()) {
samples = other.samples.clone();
} else {
samples = torch::Tensor();
}
// First, create all nodes (without relationships)
for (const auto& node : other.nodes) {
nodes[node.first] = std::make_unique<Node>(*node.second);
}
// Second, reconstruct the relationships between nodes
for (const auto& node : other.nodes) {
const std::string& nodeName = node.first;
Node* originalNode = node.second.get();
Node* newNode = nodes[nodeName].get();
// Reconstruct parent relationships
for (Node* parent : originalNode->getParents()) {
const std::string& parentName = parent->getName();
if (nodes.find(parentName) != nodes.end()) {
newNode->addParent(nodes[parentName].get());
}
}
// Reconstruct child relationships
for (Node* child : originalNode->getChildren()) {
const std::string& childName = child->getName();
if (nodes.find(childName) != nodes.end()) {
newNode->addChild(nodes[childName].get());
}
}
}
}
return *this;
} }
void Network::initialize() void Network::initialize()
{ {
@@ -503,4 +579,41 @@ namespace bayesnet {
} }
return oss.str(); return oss.str();
} }
bool Network::operator==(const Network& other) const
{
// Compare number of nodes
if (nodes.size() != other.nodes.size()) {
return false;
}
// Compare if all node names exist in both networks
for (const auto& node : nodes) {
if (other.nodes.find(node.first) == other.nodes.end()) {
return false;
}
}
// Compare edges (topology)
auto thisEdges = getEdges();
auto otherEdges = other.getEdges();
// Compare number of edges
if (thisEdges.size() != otherEdges.size()) {
return false;
}
// Sort both edge lists for comparison
std::sort(thisEdges.begin(), thisEdges.end());
std::sort(otherEdges.begin(), otherEdges.end());
// Compare each edge
for (size_t i = 0; i < thisEdges.size(); ++i) {
if (thisEdges[i] != otherEdges[i]) {
return false;
}
}
return true;
}
} }

View File

@@ -17,7 +17,8 @@ namespace bayesnet {
class Network { class Network {
public: public:
Network(); Network();
explicit Network(const Network&); Network(const Network& other);
Network& operator=(const Network& other);
~Network() = default; ~Network() = default;
torch::Tensor& getSamples(); torch::Tensor& getSamples();
void addNode(const std::string&); void addNode(const std::string&);
@@ -47,6 +48,7 @@ namespace bayesnet {
void initialize(); void initialize();
std::string dump_cpt() const; std::string dump_cpt() const;
inline std::string version() { return { project_version.begin(), project_version.end() }; } inline std::string version() { return { project_version.begin(), project_version.end() }; }
bool operator==(const Network& other) const;
private: private:
std::map<std::string, std::unique_ptr<Node>> nodes; std::map<std::string, std::unique_ptr<Node>> nodes;
bool fitted; bool fitted;

View File

@@ -13,6 +13,41 @@ namespace bayesnet {
: name(name) : name(name)
{ {
} }
Node::Node(const Node& other)
: name(other.name), numStates(other.numStates), dimensions(other.dimensions)
{
// Deep copy the CPT tensor
if (other.cpTable.defined()) {
cpTable = other.cpTable.clone();
}
// Note: parent and children pointers are NOT copied here
// They will be reconstructed by the Network copy constructor
// to maintain proper object relationships
}
Node& Node::operator=(const Node& other)
{
if (this != &other) {
name = other.name;
numStates = other.numStates;
dimensions = other.dimensions;
// Deep copy the CPT tensor
if (other.cpTable.defined()) {
cpTable = other.cpTable.clone();
} else {
cpTable = torch::Tensor();
}
// Clear existing relationships
parents.clear();
children.clear();
// Note: parent and children pointers are NOT copied here
// They must be reconstructed to maintain proper object relationships
}
return *this;
}
void Node::clear() void Node::clear()
{ {
parents.clear(); parents.clear();

View File

@@ -14,6 +14,9 @@ namespace bayesnet {
class Node { class Node {
public: public:
explicit Node(const std::string&); explicit Node(const std::string&);
Node(const Node& other);
Node& operator=(const Node& other);
~Node() = default;
void clear(); void clear();
void addParent(Node*); void addParent(Node*);
void addChild(Node*); void addChild(Node*);

View File

@@ -3,6 +3,7 @@ from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps
from conan.tools.files import copy from conan.tools.files import copy
class BayesNetConan(ConanFile): class BayesNetConan(ConanFile):
name = "bayesnet" name = "bayesnet"
settings = "os", "compiler", "build_type", "arch" settings = "os", "compiler", "build_type", "arch"
@@ -10,26 +11,35 @@ class BayesNetConan(ConanFile):
"shared": [True, False], "shared": [True, False],
"fPIC": [True, False], "fPIC": [True, False],
"enable_testing": [True, False], "enable_testing": [True, False],
"enable_coverage": [True, False] "enable_coverage": [True, False],
} }
default_options = { default_options = {
"shared": False, "shared": False,
"fPIC": True, "fPIC": True,
"enable_testing": False, "enable_testing": False,
"enable_coverage": False "enable_coverage": False,
} }
# Sources are located in the same place as this recipe, copy them to the recipe # Sources are located in the same place as this recipe, copy them to the recipe
exports_sources = "CMakeLists.txt", "bayesnet/*", "config/*", "cmake/*", "docs/*", "tests/*", "bayesnetConfig.cmake.in" exports_sources = (
"CMakeLists.txt",
"bayesnet/*",
"config/*",
"cmake/*",
"docs/*",
"tests/*",
"bayesnetConfig.cmake.in",
)
def set_version(self) -> None: def set_version(self) -> None:
cmake = pathlib.Path(self.recipe_folder) / "CMakeLists.txt" cmake = pathlib.Path(self.recipe_folder) / "CMakeLists.txt"
text = cmake.read_text(encoding="utf-8") text = cmake.read_text(encoding="utf-8")
# Accept either: project(foo VERSION 1.2.3) or set(foo_VERSION 1.2.3) # Accept either: project(foo VERSION 1.2.3) or set(foo_VERSION 1.2.3)
match = re.search( match = re.search(
r"""project\s*\([^\)]*VERSION\s+([0-9]+\.[0-9]+\.[0-9]+)""", r"""project\s*\([^\)]*VERSION\s+([0-9]+\.[0-9]+\.[0-9]+)""",
text, re.IGNORECASE | re.VERBOSE text,
re.IGNORECASE | re.VERBOSE,
) )
if match: if match:
self.version = match.group(1) self.version = match.group(1)
@@ -47,14 +57,14 @@ class BayesNetConan(ConanFile):
def requirements(self): def requirements(self):
# Core dependencies # Core dependencies
self.requires("libtorch/2.7.0") self.requires("libtorch/2.7.1")
self.requires("nlohmann_json/3.11.3") self.requires("nlohmann_json/3.11.3")
self.requires("folding/1.1.1") # Custom package self.requires("folding/1.1.2") # Custom package
self.requires("fimdlp/2.1.0") # Custom package self.requires("fimdlp/2.1.1") # Custom package
def build_requirements(self): def build_requirements(self):
self.build_requires("cmake/[>=3.27]") self.build_requires("cmake/[>=3.27]")
self.test_requires("arff-files/1.2.0") # Custom package self.test_requires("arff-files/1.2.1") # Custom package
self.test_requires("catch2/3.8.1") self.test_requires("catch2/3.8.1")
def layout(self): def layout(self):
@@ -78,7 +88,12 @@ class BayesNetConan(ConanFile):
self.run("ctest --output-on-failure", cwd=self.build_folder) self.run("ctest --output-on-failure", cwd=self.build_folder)
def package(self): def package(self):
copy(self, "LICENSE", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses")) copy(
self,
"LICENSE",
src=self.source_folder,
dst=os.path.join(self.package_folder, "licenses"),
)
cmake = CMake(self) cmake = CMake(self)
cmake.install() cmake.install()

View File

@@ -8,7 +8,7 @@ if(ENABLE_TESTING)
add_executable(TestBayesNet TestBayesNetwork.cc TestBayesNode.cc TestBayesClassifier.cc TestXSPnDE.cc TestXBA2DE.cc add_executable(TestBayesNet TestBayesNetwork.cc TestBayesNode.cc TestBayesClassifier.cc TestXSPnDE.cc TestXBA2DE.cc
TestBayesModels.cc TestBayesMetrics.cc TestFeatureSelection.cc TestBoostAODE.cc TestXBAODE.cc TestA2DE.cc TestBayesModels.cc TestBayesMetrics.cc TestFeatureSelection.cc TestBoostAODE.cc TestXBAODE.cc TestA2DE.cc
TestUtils.cc TestBayesEnsemble.cc TestModulesVersions.cc TestBoostA2DE.cc TestMST.cc TestXSPODE.cc ${BayesNet_SOURCES}) TestUtils.cc TestBayesEnsemble.cc TestModulesVersions.cc TestBoostA2DE.cc TestMST.cc TestXSPODE.cc ${BayesNet_SOURCES})
target_link_libraries(TestBayesNet PUBLIC "${TORCH_LIBRARIES}" fimdlp::fimdlp PRIVATE Catch2::Catch2WithMain folding::folding) target_link_libraries(TestBayesNet PRIVATE torch::torch fimdlp::fimdlp Catch2::Catch2WithMain folding::folding)
add_test(NAME BayesNetworkTest COMMAND TestBayesNet) add_test(NAME BayesNetworkTest COMMAND TestBayesNet)
add_test(NAME A2DE COMMAND TestBayesNet "[A2DE]") add_test(NAME A2DE COMMAND TestBayesNet "[A2DE]")
add_test(NAME BoostA2DE COMMAND TestBayesNet "[BoostA2DE]") add_test(NAME BoostA2DE COMMAND TestBayesNet "[BoostA2DE]")

View File

@@ -31,9 +31,9 @@ TEST_CASE("Test Bayesian Classifiers score & version", "[Models]")
{{"diabetes", "SPODE"}, 0.802083}, {{"diabetes", "SPODE"}, 0.802083},
{{"diabetes", "TAN"}, 0.821615}, {{"diabetes", "TAN"}, 0.821615},
{{"diabetes", "AODELd"}, 0.8125f}, {{"diabetes", "AODELd"}, 0.8125f},
{{"diabetes", "KDBLd"}, 0.80208f}, {{"diabetes", "KDBLd"}, 0.804688f},
{{"diabetes", "SPODELd"}, 0.7890625f}, {{"diabetes", "SPODELd"}, 0.7890625f},
{{"diabetes", "TANLd"}, 0.803385437f}, {{"diabetes", "TANLd"}, 0.8125f},
{{"diabetes", "BoostAODE"}, 0.83984f}, {{"diabetes", "BoostAODE"}, 0.83984f},
// Ecoli // Ecoli
{{"ecoli", "AODE"}, 0.889881}, {{"ecoli", "AODE"}, 0.889881},
@@ -42,9 +42,9 @@ TEST_CASE("Test Bayesian Classifiers score & version", "[Models]")
{{"ecoli", "SPODE"}, 0.880952}, {{"ecoli", "SPODE"}, 0.880952},
{{"ecoli", "TAN"}, 0.892857}, {{"ecoli", "TAN"}, 0.892857},
{{"ecoli", "AODELd"}, 0.875f}, {{"ecoli", "AODELd"}, 0.875f},
{{"ecoli", "KDBLd"}, 0.880952358f}, {{"ecoli", "KDBLd"}, 0.872024f},
{{"ecoli", "SPODELd"}, 0.839285731f}, {{"ecoli", "SPODELd"}, 0.839285731f},
{{"ecoli", "TANLd"}, 0.848214269f}, {{"ecoli", "TANLd"}, 0.869047642f},
{{"ecoli", "BoostAODE"}, 0.89583f}, {{"ecoli", "BoostAODE"}, 0.89583f},
// Glass // Glass
{{"glass", "AODE"}, 0.79439}, {{"glass", "AODE"}, 0.79439},
@@ -53,9 +53,9 @@ TEST_CASE("Test Bayesian Classifiers score & version", "[Models]")
{{"glass", "SPODE"}, 0.775701}, {{"glass", "SPODE"}, 0.775701},
{{"glass", "TAN"}, 0.827103}, {{"glass", "TAN"}, 0.827103},
{{"glass", "AODELd"}, 0.799065411f}, {{"glass", "AODELd"}, 0.799065411f},
{{"glass", "KDBLd"}, 0.82710278f}, {{"glass", "KDBLd"}, 0.864485979f},
{{"glass", "SPODELd"}, 0.780373812f}, {{"glass", "SPODELd"}, 0.780373812f},
{{"glass", "TANLd"}, 0.869158864f}, {{"glass", "TANLd"}, 0.831775725f},
{{"glass", "BoostAODE"}, 0.84579f}, {{"glass", "BoostAODE"}, 0.84579f},
// Iris // Iris
{{"iris", "AODE"}, 0.973333}, {{"iris", "AODE"}, 0.973333},
@@ -68,29 +68,29 @@ TEST_CASE("Test Bayesian Classifiers score & version", "[Models]")
{{"iris", "SPODELd"}, 0.96f}, {{"iris", "SPODELd"}, 0.96f},
{{"iris", "TANLd"}, 0.97333f}, {{"iris", "TANLd"}, 0.97333f},
{{"iris", "BoostAODE"}, 0.98f} }; {{"iris", "BoostAODE"}, 0.98f} };
std::map<std::string, bayesnet::BaseClassifier*> models{ {"AODE", new bayesnet::AODE()}, std::map<std::string, std::unique_ptr<bayesnet::BaseClassifier>> models;
{"AODELd", new bayesnet::AODELd()}, models["AODE"] = std::make_unique<bayesnet::AODE>();
{"BoostAODE", new bayesnet::BoostAODE()}, models["AODELd"] = std::make_unique<bayesnet::AODELd>();
{"KDB", new bayesnet::KDB(2)}, models["BoostAODE"] = std::make_unique<bayesnet::BoostAODE>();
{"KDBLd", new bayesnet::KDBLd(2)}, models["KDB"] = std::make_unique<bayesnet::KDB>(2);
{"XSPODE", new bayesnet::XSpode(1)}, models["KDBLd"] = std::make_unique<bayesnet::KDBLd>(2);
{"SPODE", new bayesnet::SPODE(1)}, models["XSPODE"] = std::make_unique<bayesnet::XSpode>(1);
{"SPODELd", new bayesnet::SPODELd(1)}, models["SPODE"] = std::make_unique<bayesnet::SPODE>(1);
{"TAN", new bayesnet::TAN()}, models["SPODELd"] = std::make_unique<bayesnet::SPODELd>(1);
{"TANLd", new bayesnet::TANLd()} }; models["TAN"] = std::make_unique<bayesnet::TAN>();
models["TANLd"] = std::make_unique<bayesnet::TANLd>();
std::string name = GENERATE("AODE", "AODELd", "KDB", "KDBLd", "SPODE", "XSPODE", "SPODELd", "TAN", "TANLd"); std::string name = GENERATE("AODE", "AODELd", "KDB", "KDBLd", "SPODE", "XSPODE", "SPODELd", "TAN", "TANLd");
auto clf = models[name]; auto clf = std::move(models[name]);
SECTION("Test " + name + " classifier") SECTION("Test " + name + " classifier")
{ {
for (const std::string& file_name : { "glass", "iris", "ecoli", "diabetes" }) { for (const std::string& file_name : { "glass", "iris", "ecoli", "diabetes" }) {
auto clf = models[name];
auto discretize = name.substr(name.length() - 2) != "Ld"; auto discretize = name.substr(name.length() - 2) != "Ld";
auto raw = RawDatasets(file_name, discretize); auto raw = RawDatasets(file_name, discretize);
clf->fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing); clf->fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing);
auto score = clf->score(raw.Xt, raw.yt); auto score = clf->score(raw.Xt, raw.yt);
// std::cout << "Classifier: " << name << " File: " << file_name << " Score: " << score << " expected = " << // std::cout << "Classifier: " << name << " File: " << file_name << " Score: " << score << " expected = " <<
// scores[{file_name, name}] << std::endl; // scores[{file_name, name}] << std::endl;
INFO("Classifier: " << name << " File: " << file_name); INFO("Classifier: " << name << " File: " << file_name);
REQUIRE(score == Catch::Approx(scores[{file_name, name}]).epsilon(raw.epsilon)); REQUIRE(score == Catch::Approx(scores[{file_name, name}]).epsilon(raw.epsilon));
REQUIRE(clf->getStatus() == bayesnet::NORMAL); REQUIRE(clf->getStatus() == bayesnet::NORMAL);
@@ -101,7 +101,6 @@ TEST_CASE("Test Bayesian Classifiers score & version", "[Models]")
INFO("Checking version of " << name << " classifier"); INFO("Checking version of " << name << " classifier");
REQUIRE(clf->getVersion() == ACTUAL_VERSION); REQUIRE(clf->getVersion() == ACTUAL_VERSION);
} }
delete clf;
} }
TEST_CASE("Models features & Graph", "[Models]") TEST_CASE("Models features & Graph", "[Models]")
{ {
@@ -133,7 +132,7 @@ TEST_CASE("Models features & Graph", "[Models]")
clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing); clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing);
REQUIRE(clf.getNumberOfNodes() == 5); REQUIRE(clf.getNumberOfNodes() == 5);
REQUIRE(clf.getNumberOfEdges() == 7); REQUIRE(clf.getNumberOfEdges() == 7);
REQUIRE(clf.getNumberOfStates() == 27); REQUIRE(clf.getNumberOfStates() == 26);
REQUIRE(clf.getClassNumStates() == 3); REQUIRE(clf.getClassNumStates() == 3);
REQUIRE(clf.show() == std::vector<std::string>{"class -> sepallength, sepalwidth, petallength, petalwidth, ", REQUIRE(clf.show() == std::vector<std::string>{"class -> sepallength, sepalwidth, petallength, petalwidth, ",
"petallength -> sepallength, ", "petalwidth -> ", "petallength -> sepallength, ", "petalwidth -> ",
@@ -149,7 +148,6 @@ TEST_CASE("Get num features & num edges", "[Models]")
REQUIRE(clf.getNumberOfNodes() == 5); REQUIRE(clf.getNumberOfNodes() == 5);
REQUIRE(clf.getNumberOfEdges() == 8); REQUIRE(clf.getNumberOfEdges() == 8);
} }
TEST_CASE("Model predict_proba", "[Models]") TEST_CASE("Model predict_proba", "[Models]")
{ {
std::string model = GENERATE("TAN", "SPODE", "BoostAODEproba", "BoostAODEvoting", "TANLd", "SPODELd", "KDBLd"); std::string model = GENERATE("TAN", "SPODE", "BoostAODEproba", "BoostAODEvoting", "TANLd", "SPODELd", "KDBLd");
@@ -180,15 +178,15 @@ TEST_CASE("Model predict_proba", "[Models]")
{0.0284828, 0.770524, 0.200993}, {0.0284828, 0.770524, 0.200993},
{0.0213182, 0.857189, 0.121493}, {0.0213182, 0.857189, 0.121493},
{0.00868436, 0.949494, 0.0418215} }); {0.00868436, 0.949494, 0.0418215} });
auto res_prob_tanld = std::vector<std::vector<double>>({ {0.000544493, 0.995796, 0.00365992 }, auto res_prob_tanld = std::vector<std::vector<double>>({ {0.000597557, 0.9957, 0.00370254},
{0.000908092, 0.997268, 0.00182429 }, {0.000731377, 0.997914, 0.0013544},
{0.000908092, 0.997268, 0.00182429 }, {0.000731377, 0.997914, 0.0013544},
{0.000908092, 0.997268, 0.00182429 }, {0.000731377, 0.997914, 0.0013544},
{0.00228423, 0.994645, 0.00307078 }, {0.000838614, 0.998122, 0.00103923},
{0.00120539, 0.0666788, 0.932116 }, {0.00130852, 0.0659492, 0.932742},
{0.00361847, 0.979203, 0.017179 }, {0.00365946, 0.979412, 0.0169281},
{0.00483293, 0.985326, 0.00984064 }, {0.00435035, 0.986248, 0.00940212},
{0.000595606, 0.9977, 0.00170441 } }); {0.000583815, 0.997746, 0.00167066} });
auto res_prob_spodeld = std::vector<std::vector<double>>({ {0.000908024, 0.993742, 0.00535024 }, auto res_prob_spodeld = std::vector<std::vector<double>>({ {0.000908024, 0.993742, 0.00535024 },
{0.00187726, 0.99167, 0.00645308 }, {0.00187726, 0.99167, 0.00645308 },
{0.00187726, 0.99167, 0.00645308 }, {0.00187726, 0.99167, 0.00645308 },
@@ -216,29 +214,33 @@ TEST_CASE("Model predict_proba", "[Models]")
{"TANLd", res_prob_tanld}, {"TANLd", res_prob_tanld},
{"SPODELd", res_prob_spodeld}, {"SPODELd", res_prob_spodeld},
{"KDBLd", res_prob_kdbld} }; {"KDBLd", res_prob_kdbld} };
std::map<std::string, bayesnet::BaseClassifier*> models{ {"TAN", new bayesnet::TAN()},
{"SPODE", new bayesnet::SPODE(0)}, std::map<std::string, std::unique_ptr<bayesnet::BaseClassifier>> models;
{"BoostAODEproba", new bayesnet::BoostAODE(false)}, models["TAN"] = std::make_unique<bayesnet::TAN>();
{"BoostAODEvoting", new bayesnet::BoostAODE(true)}, models["SPODE"] = std::make_unique<bayesnet::SPODE>(0);
{"TANLd", new bayesnet::TANLd()}, models["BoostAODEproba"] = std::make_unique<bayesnet::BoostAODE>(false);
{"SPODELd", new bayesnet::SPODELd(0)}, models["BoostAODEvoting"] = std::make_unique<bayesnet::BoostAODE>(true);
{"KDBLd", new bayesnet::KDBLd(2)} }; models["TANLd"] = std::make_unique<bayesnet::TANLd>();
models["SPODELd"] = std::make_unique<bayesnet::SPODELd>(0);
models["KDBLd"] = std::make_unique<bayesnet::KDBLd>(2);
int init_index = 78; int init_index = 78;
SECTION("Test " + model + " predict_proba") SECTION("Test " + model + " predict_proba")
{ {
INFO("Testing " << model << " predict_proba");
auto ld_model = model.substr(model.length() - 2) == "Ld"; auto ld_model = model.substr(model.length() - 2) == "Ld";
auto discretize = !ld_model; auto discretize = !ld_model;
auto raw = RawDatasets("iris", discretize); auto raw = RawDatasets("iris", discretize);
auto clf = models[model]; auto& clf = *models[model];
clf->fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing); clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing);
auto yt_pred_proba = clf->predict_proba(raw.Xt); auto yt_pred_proba = clf.predict_proba(raw.Xt);
auto yt_pred = clf->predict(raw.Xt); auto yt_pred = clf.predict(raw.Xt);
std::vector<int> y_pred; std::vector<int> y_pred;
std::vector<std::vector<double>> y_pred_proba; std::vector<std::vector<double>> y_pred_proba;
if (!ld_model) { if (!ld_model) {
y_pred = clf->predict(raw.Xv); y_pred = clf.predict(raw.Xv);
y_pred_proba = clf->predict_proba(raw.Xv); y_pred_proba = clf.predict_proba(raw.Xv);
REQUIRE(y_pred.size() == y_pred_proba.size()); REQUIRE(y_pred.size() == y_pred_proba.size());
REQUIRE(y_pred.size() == yt_pred.size(0)); REQUIRE(y_pred.size() == yt_pred.size(0));
REQUIRE(y_pred.size() == yt_pred_proba.size(0)); REQUIRE(y_pred.size() == yt_pred_proba.size(0));
@@ -267,18 +269,20 @@ TEST_CASE("Model predict_proba", "[Models]")
} else { } else {
// Check predict_proba values for vectors and tensors // Check predict_proba values for vectors and tensors
auto predictedClasses = yt_pred_proba.argmax(1); auto predictedClasses = yt_pred_proba.argmax(1);
// std::cout << model << std::endl;
for (int i = 0; i < 9; i++) { for (int i = 0; i < 9; i++) {
REQUIRE(predictedClasses[i].item<int>() == yt_pred[i].item<int>()); REQUIRE(predictedClasses[i].item<int>() == yt_pred[i].item<int>());
// std::cout << "{";
for (int j = 0; j < 3; j++) { for (int j = 0; j < 3; j++) {
// std::cout << yt_pred_proba[i + init_index][j].item<double>() << ", ";
REQUIRE(res_prob[model][i][j] == REQUIRE(res_prob[model][i][j] ==
Catch::Approx(yt_pred_proba[i + init_index][j].item<double>()).epsilon(raw.epsilon)); Catch::Approx(yt_pred_proba[i + init_index][j].item<double>()).epsilon(raw.epsilon));
} }
// std::cout << "\b\b}," << std::endl;
} }
} }
delete clf;
} }
} }
TEST_CASE("AODE voting-proba", "[Models]") TEST_CASE("AODE voting-proba", "[Models]")
{ {
auto raw = RawDatasets("glass", true); auto raw = RawDatasets("glass", true);
@@ -297,17 +301,30 @@ TEST_CASE("AODE voting-proba", "[Models]")
REQUIRE(pred_proba[67][0] == Catch::Approx(0.702184).epsilon(raw.epsilon)); REQUIRE(pred_proba[67][0] == Catch::Approx(0.702184).epsilon(raw.epsilon));
REQUIRE(clf.topological_order() == std::vector<std::string>()); REQUIRE(clf.topological_order() == std::vector<std::string>());
} }
TEST_CASE("SPODELd dataset", "[Models]") TEST_CASE("Ld models with dataset", "[Models]")
{ {
auto raw = RawDatasets("iris", false); auto raw = RawDatasets("iris", false);
auto clf = bayesnet::SPODELd(0); auto clf = bayesnet::SPODELd(0);
// raw.dataset.to(torch::kFloat32);
clf.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing); clf.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing);
auto score = clf.score(raw.Xt, raw.yt); auto score = clf.score(raw.Xt, raw.yt);
clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing); clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing);
auto scoret = clf.score(raw.Xt, raw.yt); auto scoret = clf.score(raw.Xt, raw.yt);
REQUIRE(score == Catch::Approx(0.97333f).epsilon(raw.epsilon)); REQUIRE(score == Catch::Approx(0.97333f).epsilon(raw.epsilon));
REQUIRE(scoret == Catch::Approx(0.97333f).epsilon(raw.epsilon)); REQUIRE(scoret == Catch::Approx(0.97333f).epsilon(raw.epsilon));
auto clf2 = bayesnet::TANLd();
clf2.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing);
auto score2 = clf2.score(raw.Xt, raw.yt);
clf2.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing);
auto score2t = clf2.score(raw.Xt, raw.yt);
REQUIRE(score2 == Catch::Approx(0.97333f).epsilon(raw.epsilon));
REQUIRE(score2t == Catch::Approx(0.97333f).epsilon(raw.epsilon));
auto clf3 = bayesnet::KDBLd(2);
clf3.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing);
auto score3 = clf3.score(raw.Xt, raw.yt);
clf3.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing);
auto score3t = clf3.score(raw.Xt, raw.yt);
REQUIRE(score3 == Catch::Approx(0.97333f).epsilon(raw.epsilon));
REQUIRE(score3t == Catch::Approx(0.97333f).epsilon(raw.epsilon));
} }
TEST_CASE("KDB with hyperparameters", "[Models]") TEST_CASE("KDB with hyperparameters", "[Models]")
{ {
@@ -324,11 +341,15 @@ TEST_CASE("KDB with hyperparameters", "[Models]")
REQUIRE(score == Catch::Approx(0.827103).epsilon(raw.epsilon)); REQUIRE(score == Catch::Approx(0.827103).epsilon(raw.epsilon));
REQUIRE(scoret == Catch::Approx(0.761682).epsilon(raw.epsilon)); REQUIRE(scoret == Catch::Approx(0.761682).epsilon(raw.epsilon));
} }
TEST_CASE("Incorrect type of data for SPODELd", "[Models]") TEST_CASE("Incorrect type of data for Ld models", "[Models]")
{ {
auto raw = RawDatasets("iris", true); auto raw = RawDatasets("iris", true);
auto clf = bayesnet::SPODELd(0); auto clfs = bayesnet::SPODELd(0);
REQUIRE_THROWS_AS(clf.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing), std::runtime_error); REQUIRE_THROWS_AS(clfs.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing), std::runtime_error);
auto clft = bayesnet::TANLd();
REQUIRE_THROWS_AS(clft.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing), std::runtime_error);
auto clfk = bayesnet::KDBLd(0);
REQUIRE_THROWS_AS(clfk.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing), std::runtime_error);
} }
TEST_CASE("Predict, predict_proba & score without fitting", "[Models]") TEST_CASE("Predict, predict_proba & score without fitting", "[Models]")
{ {
@@ -386,14 +407,15 @@ TEST_CASE("Check proposal checkInput", "[Models]")
{ {
class testProposal : public bayesnet::Proposal { class testProposal : public bayesnet::Proposal {
public: public:
testProposal(torch::Tensor& dataset_, std::vector<std::string>& features_, std::string& className_) testProposal(torch::Tensor& dataset_, std::vector<std::string>& features_, std::string& className_, std::vector<std::string>& notes_)
: Proposal(dataset_, features_, className_) : Proposal(dataset_, features_, className_, notes_)
{ {
} }
void test_X_y(const torch::Tensor& X, const torch::Tensor& y) { checkInput(X, y); } void test_X_y(const torch::Tensor& X, const torch::Tensor& y) { checkInput(X, y); }
}; };
auto raw = RawDatasets("iris", true); auto raw = RawDatasets("iris", true);
auto clf = testProposal(raw.dataset, raw.features, raw.className); std::vector<std::string> notes;
auto clf = testProposal(raw.dataset, raw.features, raw.className, notes);
torch::Tensor X = torch::randint(0, 3, { 10, 4 }); torch::Tensor X = torch::randint(0, 3, { 10, 4 });
torch::Tensor y = torch::rand({ 10 }); torch::Tensor y = torch::rand({ 10 });
INFO("Check X is not float"); INFO("Check X is not float");
@@ -428,3 +450,49 @@ TEST_CASE("Check KDB loop detection", "[Models]")
REQUIRE_NOTHROW(clf.test_add_m_edges(features, 0, S, weights)); REQUIRE_NOTHROW(clf.test_add_m_edges(features, 0, S, weights));
REQUIRE_NOTHROW(clf.test_add_m_edges(features, 1, S, weights)); REQUIRE_NOTHROW(clf.test_add_m_edges(features, 1, S, weights));
} }
TEST_CASE("Local discretization hyperparameters", "[Models]")
{
auto raw = RawDatasets("iris", false);
auto clfs = bayesnet::SPODELd(0);
clfs.setHyperparameters({
{"max_iterations", 7},
{"verbose_convergence", true},
});
REQUIRE_NOTHROW(clfs.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing));
REQUIRE(clfs.getStatus() == bayesnet::NORMAL);
auto clfk = bayesnet::KDBLd(0);
clfk.setHyperparameters({
{"k", 3},
{"theta", 1e-4},
});
REQUIRE_NOTHROW(clfk.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing));
REQUIRE(clfk.getStatus() == bayesnet::NORMAL);
auto clfa = bayesnet::AODELd();
clfa.setHyperparameters({
{"ld_proposed_cuts", 9},
{"ld_algorithm", "BINQ"},
});
REQUIRE_NOTHROW(clfa.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing));
REQUIRE(clfa.getStatus() == bayesnet::NORMAL);
auto clft = bayesnet::TANLd();
clft.setHyperparameters({
{"ld_proposed_cuts", 7},
{"mdlp_max_depth", 5},
{"mdlp_min_length", 3},
{"ld_algorithm", "MDLP"},
});
REQUIRE_NOTHROW(clft.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing));
REQUIRE(clft.getStatus() == bayesnet::NORMAL);
clft.setHyperparameters({
{"ld_proposed_cuts", 9},
{"ld_algorithm", "BINQ"},
});
REQUIRE_NOTHROW(clft.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing));
REQUIRE(clft.getStatus() == bayesnet::NORMAL);
clft.setHyperparameters({
{"ld_proposed_cuts", 5},
{"ld_algorithm", "BINU"},
});
REQUIRE_NOTHROW(clft.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing));
REQUIRE(clft.getStatus() == bayesnet::NORMAL);
}

View File

@@ -338,6 +338,190 @@ TEST_CASE("Test Bayesian Network", "[Network]")
REQUIRE_THROWS_AS(net5.addEdge("A", "B"), std::logic_error); REQUIRE_THROWS_AS(net5.addEdge("A", "B"), std::logic_error);
REQUIRE_THROWS_WITH(net5.addEdge("A", "B"), "Cannot add edge to a fitted network. Initialize first."); REQUIRE_THROWS_WITH(net5.addEdge("A", "B"), "Cannot add edge to a fitted network. Initialize first.");
} }
SECTION("Test assignment operator")
{
INFO("Test assignment operator");
// Create original network
auto net1 = bayesnet::Network();
buildModel(net1, raw.features, raw.className);
net1.fit(raw.Xv, raw.yv, raw.weightsv, raw.features, raw.className, raw.states, raw.smoothing);
// Create empty network and assign
auto net2 = bayesnet::Network();
net2.addNode("TempNode"); // Add something to make sure it gets cleared
net2 = net1;
// Verify they are equal
REQUIRE(net1.getFeatures() == net2.getFeatures());
REQUIRE(net1.getEdges() == net2.getEdges());
REQUIRE(net1.getNumEdges() == net2.getNumEdges());
REQUIRE(net1.getStates() == net2.getStates());
REQUIRE(net1.getClassName() == net2.getClassName());
REQUIRE(net1.getClassNumStates() == net2.getClassNumStates());
REQUIRE(net1.getSamples().size(0) == net2.getSamples().size(0));
REQUIRE(net1.getSamples().size(1) == net2.getSamples().size(1));
REQUIRE(net1.getNodes().size() == net2.getNodes().size());
// Verify topology equality
REQUIRE(net1 == net2);
// Verify they are separate objects by modifying one
net2.initialize();
net2.addNode("OnlyInNet2");
REQUIRE(net1.getNodes().size() != net2.getNodes().size());
REQUIRE_FALSE(net1 == net2);
}
SECTION("Test self assignment")
{
INFO("Test self assignment");
buildModel(net, raw.features, raw.className);
net.fit(raw.Xv, raw.yv, raw.weightsv, raw.features, raw.className, raw.states, raw.smoothing);
int original_edges = net.getNumEdges();
int original_nodes = net.getNodes().size();
// Self assignment should not corrupt the network
net = net;
auto all_features = raw.features;
all_features.push_back(raw.className);
REQUIRE(net.getNumEdges() == original_edges);
REQUIRE(net.getNodes().size() == original_nodes);
REQUIRE(net.getFeatures() == all_features);
REQUIRE(net.getClassName() == raw.className);
}
SECTION("Test operator== topology comparison")
{
INFO("Test operator== topology comparison");
// Test 1: Two identical networks
auto net1 = bayesnet::Network();
auto net2 = bayesnet::Network();
net1.addNode("A");
net1.addNode("B");
net1.addNode("C");
net1.addEdge("A", "B");
net1.addEdge("B", "C");
net2.addNode("A");
net2.addNode("B");
net2.addNode("C");
net2.addEdge("A", "B");
net2.addEdge("B", "C");
REQUIRE(net1 == net2);
// Test 2: Different nodes
auto net3 = bayesnet::Network();
net3.addNode("A");
net3.addNode("D"); // Different node
REQUIRE_FALSE(net1 == net3);
// Test 3: Same nodes, different edges
auto net4 = bayesnet::Network();
net4.addNode("A");
net4.addNode("B");
net4.addNode("C");
net4.addEdge("A", "C"); // Different topology
net4.addEdge("B", "C");
REQUIRE_FALSE(net1 == net4);
// Test 4: Empty networks
auto net5 = bayesnet::Network();
auto net6 = bayesnet::Network();
REQUIRE(net5 == net6);
// Test 5: Same topology, different edge order
auto net7 = bayesnet::Network();
net7.addNode("A");
net7.addNode("B");
net7.addNode("C");
net7.addEdge("B", "C"); // Add edges in different order
net7.addEdge("A", "B");
REQUIRE(net1 == net7); // Should still be equal
}
SECTION("Test RAII compliance with smart pointers")
{
INFO("Test RAII compliance with smart pointers");
std::unique_ptr<bayesnet::Network> net1 = std::make_unique<bayesnet::Network>();
buildModel(*net1, raw.features, raw.className);
net1->fit(raw.Xv, raw.yv, raw.weightsv, raw.features, raw.className, raw.states, raw.smoothing);
// Test that copy constructor works with smart pointers
std::unique_ptr<bayesnet::Network> net2 = std::make_unique<bayesnet::Network>(*net1);
REQUIRE(*net1 == *net2);
REQUIRE(net1->getNumEdges() == net2->getNumEdges());
REQUIRE(net1->getNodes().size() == net2->getNodes().size());
// Destroy original
net1.reset();
// Test predictions still work
std::vector<std::vector<int>> test = { {1}, {2}, {0}, {1} };
REQUIRE_NOTHROW(net2->predict(test));
// net2 should still be valid and functional
net2->initialize();
REQUIRE_NOTHROW(net2->addNode("NewNode"));
REQUIRE(net2->getNodes().count("NewNode") == 1);
}
SECTION("Test complex topology copy")
{
INFO("Test complex topology copy");
auto original = bayesnet::Network();
// Create a more complex network
original.addNode("Root");
original.addNode("Child1");
original.addNode("Child2");
original.addNode("Grandchild1");
original.addNode("Grandchild2");
original.addNode("Grandchild3");
original.addEdge("Root", "Child1");
original.addEdge("Root", "Child2");
original.addEdge("Child1", "Grandchild1");
original.addEdge("Child1", "Grandchild2");
original.addEdge("Child2", "Grandchild3");
// Copy it
auto copy = original;
// Verify topology is identical
REQUIRE(original == copy);
REQUIRE(original.getNodes().size() == copy.getNodes().size());
REQUIRE(original.getNumEdges() == copy.getNumEdges());
// Verify edges are properly reconstructed
auto originalEdges = original.getEdges();
auto copyEdges = copy.getEdges();
REQUIRE(originalEdges.size() == copyEdges.size());
// Verify node relationships are properly copied
for (const auto& nodePair : original.getNodes()) {
const std::string& nodeName = nodePair.first;
auto* originalNode = nodePair.second.get();
auto* copyNode = copy.getNodes().at(nodeName).get();
REQUIRE(originalNode->getParents().size() == copyNode->getParents().size());
REQUIRE(originalNode->getChildren().size() == copyNode->getChildren().size());
// Verify parent names match
for (size_t i = 0; i < originalNode->getParents().size(); ++i) {
REQUIRE(originalNode->getParents()[i]->getName() ==
copyNode->getParents()[i]->getName());
}
// Verify child names match
for (size_t i = 0; i < originalNode->getChildren().size(); ++i) {
REQUIRE(originalNode->getChildren()[i]->getName() ==
copyNode->getChildren()[i]->getName());
}
}
}
} }
TEST_CASE("Test and empty Node", "[Network]") TEST_CASE("Test and empty Node", "[Network]")

View File

@@ -159,3 +159,47 @@ TEST_CASE("TEST MinFill method", "[Node]")
REQUIRE(node_3.minFill() == 3); REQUIRE(node_3.minFill() == 3);
REQUIRE(node_4.minFill() == 1); REQUIRE(node_4.minFill() == 1);
} }
TEST_CASE("Test operator =", "[Node]")
{
// Generate a test to test the operator = of the Node class
// Create a node with 3 parents and 2 children
auto node = bayesnet::Node("N1");
auto parent_1 = bayesnet::Node("P1");
parent_1.setNumStates(3);
auto child_1 = bayesnet::Node("H1");
child_1.setNumStates(2);
node.addParent(&parent_1);
node.addChild(&child_1);
// Create a cpt in the node using computeCPT
auto dataset = torch::tensor({ {1, 0, 0, 1}, {0, 1, 2, 1}, {0, 1, 1, 0} });
auto states = std::vector<int>({ 2, 3, 3 });
auto features = std::vector<std::string>{ "N1", "P1", "H1" };
auto className = std::string("Class");
auto weights = torch::tensor({ 1.0, 1.0, 1.0, 1.0 }, torch::kDouble);
node.setNumStates(2);
node.computeCPT(dataset, features, 0.0, weights);
// Get the cpt of the node
auto cpt = node.getCPT();
// Check that the cpt is not empty
REQUIRE(cpt.numel() > 0);
// Check that the cpt has the correct dimensions
auto dimensions = cpt.sizes();
REQUIRE(dimensions.size() == 2);
REQUIRE(dimensions[0] == 2); // Number of states of the node
REQUIRE(dimensions[1] == 3); // Number of states of the first parent
// Create a copy of the node
bayesnet::Node node_copy("XX");
node_copy = node;
// Check that the copy has not any parents or children
auto parents = node_copy.getParents();
auto children = node_copy.getChildren();
REQUIRE(parents.size() == 0);
REQUIRE(children.size() == 0);
// Check that the copy has the same name
REQUIRE(node_copy.getName() == "N1");
// Check that the copy has the same cpt
auto cpt_copy = node_copy.getCPT();
REQUIRE(cpt_copy.equal(cpt));
// Check that the copy has the same number of states
REQUIRE(node_copy.getNumStates() == node.getNumStates());
}

View File

@@ -16,10 +16,10 @@
#include "TestUtils.h" #include "TestUtils.h"
std::map<std::string, std::string> modules = { std::map<std::string, std::string> modules = {
{ "mdlp", "2.1.0" }, { "mdlp", "2.1.1" },
{ "Folding", "1.1.1" }, { "Folding", "1.1.2" },
{ "json", "3.11" }, { "json", "3.11" },
{ "ArffFiles", "1.2.0" } { "ArffFiles", "1.2.1" }
}; };
TEST_CASE("MDLP", "[Modules]") TEST_CASE("MDLP", "[Modules]")