Complete implementation with tests

This commit is contained in:
2025-07-08 11:42:20 +02:00
parent 2c7352ac38
commit ed380b1494
13 changed files with 255 additions and 170 deletions

View File

@@ -14,33 +14,29 @@ namespace bayesnet {
validHyperparameters.push_back("k");
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)
{
checkInput(X_, y_);
features = features_;
className = className_;
Xf = X_;
y = y_;
// Use iterative local discretization instead of the two-phase approach
return commonFit(features_, className_, states_, smoothing);
}
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)
{
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);
// Final fit with converged discretization
KDB::fit(dataset, features, className, states, smoothing);
return *this;
}
torch::Tensor KDBLd::predict(torch::Tensor& X)

View File

@@ -15,8 +15,15 @@ namespace bayesnet {
explicit KDBLd(int k);
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& 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;
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_proba(torch::Tensor& X) override;
static inline std::string version() { return "0.0.1"; };

View File

@@ -11,6 +11,7 @@
#include "Classifier.h"
#include "KDB.h"
#include "TAN.h"
#include "SPODE.h"
#include "KDBLd.h"
#include "TANLd.h"
@@ -18,9 +19,8 @@ namespace bayesnet {
Proposal::Proposal(torch::Tensor& dataset_, std::vector<std::string>& features_, std::string& className_) : pDataset(dataset_), pFeatures(features_), pClassName(className_)
{
}
void Proposal::setHyperparameters(const nlohmann::json& hyperparameters_)
void Proposal::setHyperparameters(nlohmann::json& hyperparameters)
{
auto hyperparameters = hyperparameters_;
if (hyperparameters.contains("ld_proposed_cuts")) {
ld_params.proposed_cuts = hyperparameters["ld_proposed_cuts"];
hyperparameters.erase("ld_proposed_cuts");
@@ -55,9 +55,6 @@ namespace bayesnet {
convergence_params.verbose = hyperparameters["verbose_convergence"];
hyperparameters.erase("verbose_convergence");
}
if (!hyperparameters.empty()) {
throw std::invalid_argument("Invalid hyperparameters for Proposal: " + hyperparameters.dump());
}
}
void Proposal::checkInput(const torch::Tensor& X, const torch::Tensor& y)
@@ -209,7 +206,7 @@ namespace bayesnet {
// 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());
@@ -228,51 +225,15 @@ namespace bayesnet {
return currentStates;
}
double Proposal::computeLogLikelihood(Network& model, const torch::Tensor& dataset)
{
double logLikelihood = 0.0;
int n_samples = dataset.size(0);
int n_features = dataset.size(1);
for (int i = 0; i < n_samples; ++i) {
double sampleLogLikelihood = 0.0;
// Get class value for this sample
int classValue = dataset[i][n_features - 1].item<int>();
// Compute log-likelihood for each feature given its parents and class
for (const auto& node : model.getNodes()) {
if (node.first == model.getClassName()) {
// For class node, add log P(class)
auto classCounts = node.second->getCPT();
double classProb = classCounts[classValue].item<double>() / dataset.size(0);
sampleLogLikelihood += std::log(std::max(classProb, 1e-10));
} else {
// For feature nodes, add log P(feature | parents, class)
int featureIdx = std::distance(model.getFeatures().begin(),
std::find(model.getFeatures().begin(),
model.getFeatures().end(),
node.first));
int featureValue = dataset[i][featureIdx].item<int>();
// Simplified probability computation - in practice would need full CPT lookup
double featureProb = 0.1; // Placeholder - would compute from CPT
sampleLogLikelihood += std::log(std::max(featureProb, 1e-10));
}
}
logLikelihood += sampleLogLikelihood;
}
return logLikelihood;
}
// 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

@@ -19,7 +19,7 @@ namespace bayesnet {
class Proposal {
public:
Proposal(torch::Tensor& pDataset, std::vector<std::string>& features_, std::string& className_);
void setHyperparameters(const nlohmann::json& hyperparameters_);
void setHyperparameters(nlohmann::json& hyperparameters_);
protected:
void checkInput(const torch::Tensor& X, const torch::Tensor& y);
torch::Tensor prepareX(torch::Tensor& X);
@@ -61,7 +61,6 @@ namespace bayesnet {
};
private:
std::vector<int> factorize(const std::vector<std::string>& labels_t);
double computeLogLikelihood(Network& model, const torch::Tensor& dataset);
torch::Tensor& pDataset; // (n+1)xm tensor
std::vector<std::string>& pFeatures;
std::string& pClassName;

View File

@@ -34,12 +34,8 @@ namespace bayesnet {
{
features = features_;
className = className_;
// Fills std::vectors Xv & yv with the data from tensors X_ (discretized) & y
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
states = iterativeLocalDiscretization(y, static_cast<SPODE*>(this), dataset, features, className, states_, smoothing);
SPODE::fit(dataset, features, className, states, smoothing);
states = localDiscretizationProposal(states, model);
return *this;
}
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& 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;
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_proba(torch::Tensor& X) override;
static inline std::string version() { return "0.0.1"; };

View File

@@ -12,17 +12,26 @@ namespace bayesnet {
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_);
features = features_;
className = className_;
Xf = X_;
y = y_;
// Use iterative local discretization instead of the two-phase approach
return commonFit(features_, className_, states_, smoothing);
}
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)
{
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);
}
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);
// Final fit with converged discretization
TAN::fit(dataset, features, className, states, smoothing);
return *this;
}
torch::Tensor TANLd::predict(torch::Tensor& X)

View File

@@ -16,7 +16,15 @@ namespace bayesnet {
TANLd();
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& 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;
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_proba(torch::Tensor& X) override;
};

View File

@@ -17,6 +17,10 @@ namespace bayesnet {
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;
std::vector<std::string> graph(const std::string& name = "AODELd") const override;
void setHyperparameters(const nlohmann::json& hyperparameters_) override
{
hyperparameters = hyperparameters_;
}
protected:
void trainModel(const torch::Tensor& weights, const Smoothing_t smoothing) override;
void buildModel(const torch::Tensor& weights) override;