Add SPnDE n=2
This commit is contained in:
575
bayesnet/classifiers/XSPnDE.cc
Normal file
575
bayesnet/classifiers/XSPnDE.cc
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
// ***************************************************************
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez
|
||||||
|
// SPDX-FileType: SOURCE
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// ***************************************************************
|
||||||
|
|
||||||
|
#include "XSPnDE.h"
|
||||||
|
#include <pthread.h> // for pthread_setname_np on linux
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <iostream>
|
||||||
|
#include "bayesnet/utils/TensorUtils.h"
|
||||||
|
|
||||||
|
namespace bayesnet {
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// Constructor
|
||||||
|
// --------------------------------------
|
||||||
|
XSpnde::XSpnde(int spIndex1, int spIndex2)
|
||||||
|
: superParent1_{ spIndex1 }
|
||||||
|
, superParent2_{ spIndex2 }
|
||||||
|
, nFeatures_{0}
|
||||||
|
, statesClass_{0}
|
||||||
|
, alpha_{1.0}
|
||||||
|
, initializer_{1.0}
|
||||||
|
, semaphore_{ CountingSemaphore::getInstance() }
|
||||||
|
, Classifier(Network())
|
||||||
|
{
|
||||||
|
validHyperparameters = { "parent1", "parent2" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// setHyperparameters
|
||||||
|
// --------------------------------------
|
||||||
|
void XSpnde::setHyperparameters(const nlohmann::json &hyperparameters_)
|
||||||
|
{
|
||||||
|
auto hyperparameters = hyperparameters_;
|
||||||
|
if (hyperparameters.contains("parent1")) {
|
||||||
|
superParent1_ = hyperparameters["parent1"];
|
||||||
|
hyperparameters.erase("parent1");
|
||||||
|
}
|
||||||
|
if (hyperparameters.contains("parent2")) {
|
||||||
|
superParent2_ = hyperparameters["parent2"];
|
||||||
|
hyperparameters.erase("parent2");
|
||||||
|
}
|
||||||
|
// Hand off anything else to base Classifier
|
||||||
|
Classifier::setHyperparameters(hyperparameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// fitx
|
||||||
|
// --------------------------------------
|
||||||
|
void XSpnde::fitx(torch::Tensor & X, torch::Tensor & y,
|
||||||
|
torch::Tensor & weights_, const Smoothing_t smoothing)
|
||||||
|
{
|
||||||
|
m = X.size(1); // number of samples
|
||||||
|
n = X.size(0); // number of features
|
||||||
|
dataset = X;
|
||||||
|
|
||||||
|
// Build the dataset in your environment if needed:
|
||||||
|
buildDataset(y);
|
||||||
|
|
||||||
|
// Construct the data structures needed for counting
|
||||||
|
buildModel(weights_);
|
||||||
|
|
||||||
|
// Accumulate counts & convert to probabilities
|
||||||
|
trainModel(weights_, smoothing);
|
||||||
|
fitted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// buildModel
|
||||||
|
// --------------------------------------
|
||||||
|
void XSpnde::buildModel(const torch::Tensor &weights)
|
||||||
|
{
|
||||||
|
nFeatures_ = n;
|
||||||
|
|
||||||
|
// Derive the number of states for each feature from the dataset
|
||||||
|
// states_[f] = max value in dataset[f] + 1.
|
||||||
|
states_.resize(nFeatures_);
|
||||||
|
for (int f = 0; f < nFeatures_; f++) {
|
||||||
|
// This is naive: we take max in feature f. You might adapt for real data.
|
||||||
|
states_[f] = dataset[f].max().item<int>() + 1;
|
||||||
|
}
|
||||||
|
// Class states:
|
||||||
|
statesClass_ = dataset[-1].max().item<int>() + 1;
|
||||||
|
|
||||||
|
// Initialize the class counts
|
||||||
|
classCounts_.resize(statesClass_, 0.0);
|
||||||
|
|
||||||
|
// For sp1 -> p(sp1Val| c)
|
||||||
|
sp1FeatureCounts_.resize(states_[superParent1_] * statesClass_, 0.0);
|
||||||
|
|
||||||
|
// For sp2 -> p(sp2Val| c)
|
||||||
|
sp2FeatureCounts_.resize(states_[superParent2_] * statesClass_, 0.0);
|
||||||
|
|
||||||
|
// For child features, we store p(childVal | c, sp1Val, sp2Val).
|
||||||
|
// childCounts_ will hold raw counts. We’ll gather them in one big vector.
|
||||||
|
// We need an offset for each feature.
|
||||||
|
childOffsets_.resize(nFeatures_, -1);
|
||||||
|
|
||||||
|
int totalSize = 0;
|
||||||
|
for (int f = 0; f < nFeatures_; f++) {
|
||||||
|
if (f == superParent1_ || f == superParent2_) {
|
||||||
|
// skip the superparents
|
||||||
|
childOffsets_[f] = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
childOffsets_[f] = totalSize;
|
||||||
|
// block size for a single child f: states_[f] * statesClass_
|
||||||
|
// * states_[superParent1_]
|
||||||
|
// * states_[superParent2_].
|
||||||
|
totalSize += (states_[f] * statesClass_
|
||||||
|
* states_[superParent1_]
|
||||||
|
* states_[superParent2_]);
|
||||||
|
}
|
||||||
|
childCounts_.resize(totalSize, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// trainModel
|
||||||
|
// --------------------------------------
|
||||||
|
void XSpnde::trainModel(const torch::Tensor &weights,
|
||||||
|
const bayesnet::Smoothing_t smoothing)
|
||||||
|
{
|
||||||
|
// Accumulate raw counts
|
||||||
|
for (int i = 0; i < m; i++) {
|
||||||
|
std::vector<int> instance(nFeatures_ + 1);
|
||||||
|
for (int f = 0; f < nFeatures_; f++) {
|
||||||
|
instance[f] = dataset[f][i].item<int>();
|
||||||
|
}
|
||||||
|
instance[nFeatures_] = dataset[-1][i].item<int>(); // class
|
||||||
|
double w = weights[i].item<double>();
|
||||||
|
addSample(instance, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose alpha based on smoothing:
|
||||||
|
switch (smoothing) {
|
||||||
|
case bayesnet::Smoothing_t::ORIGINAL:
|
||||||
|
alpha_ = 1.0 / m;
|
||||||
|
break;
|
||||||
|
case bayesnet::Smoothing_t::LAPLACE:
|
||||||
|
alpha_ = 1.0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
alpha_ = 0.0; // no smoothing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large initializer factor for numerical stability
|
||||||
|
initializer_ = std::numeric_limits<double>::max() / (nFeatures_ * nFeatures_);
|
||||||
|
|
||||||
|
// Convert raw counts to probabilities
|
||||||
|
computeProbabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// addSample
|
||||||
|
// --------------------------------------
|
||||||
|
void XSpnde::addSample(const std::vector<int> &instance, double weight)
|
||||||
|
{
|
||||||
|
if (weight <= 0.0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int c = instance.back();
|
||||||
|
// increment classCounts
|
||||||
|
classCounts_[c] += weight;
|
||||||
|
|
||||||
|
int sp1Val = instance[superParent1_];
|
||||||
|
int sp2Val = instance[superParent2_];
|
||||||
|
|
||||||
|
// p(sp1|c)
|
||||||
|
sp1FeatureCounts_[sp1Val * statesClass_ + c] += weight;
|
||||||
|
|
||||||
|
// p(sp2|c)
|
||||||
|
sp2FeatureCounts_[sp2Val * statesClass_ + c] += weight;
|
||||||
|
|
||||||
|
// p(childVal| c, sp1Val, sp2Val)
|
||||||
|
for (int f = 0; f < nFeatures_; f++) {
|
||||||
|
if (f == superParent1_ || f == superParent2_)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int childVal = instance[f];
|
||||||
|
int offset = childOffsets_[f];
|
||||||
|
// block layout:
|
||||||
|
// offset + (sp1Val*(states_[sp2_]* states_[f]* statesClass_))
|
||||||
|
// + (sp2Val*(states_[f]* statesClass_))
|
||||||
|
// + childVal*(statesClass_)
|
||||||
|
// + c
|
||||||
|
int blockSizeSp2 = states_[superParent2_]
|
||||||
|
* states_[f]
|
||||||
|
* statesClass_;
|
||||||
|
int blockSizeChild = states_[f] * statesClass_;
|
||||||
|
|
||||||
|
int idx = offset
|
||||||
|
+ sp1Val*blockSizeSp2
|
||||||
|
+ sp2Val*blockSizeChild
|
||||||
|
+ childVal*statesClass_
|
||||||
|
+ c;
|
||||||
|
childCounts_[idx] += weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// computeProbabilities
|
||||||
|
// --------------------------------------
|
||||||
|
void XSpnde::computeProbabilities()
|
||||||
|
{
|
||||||
|
double totalCount = std::accumulate(classCounts_.begin(),
|
||||||
|
classCounts_.end(), 0.0);
|
||||||
|
|
||||||
|
// classPriors_
|
||||||
|
classPriors_.resize(statesClass_, 0.0);
|
||||||
|
if (totalCount <= 0.0) {
|
||||||
|
// fallback => uniform
|
||||||
|
double unif = 1.0 / static_cast<double>(statesClass_);
|
||||||
|
for (int c = 0; c < statesClass_; c++) {
|
||||||
|
classPriors_[c] = unif;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int c = 0; c < statesClass_; c++) {
|
||||||
|
classPriors_[c] =
|
||||||
|
(classCounts_[c] + alpha_)
|
||||||
|
/ (totalCount + alpha_ * statesClass_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// p(sp1Val| c)
|
||||||
|
sp1FeatureProbs_.resize(sp1FeatureCounts_.size());
|
||||||
|
int sp1Card = states_[superParent1_];
|
||||||
|
for (int spVal = 0; spVal < sp1Card; spVal++) {
|
||||||
|
for (int c = 0; c < statesClass_; c++) {
|
||||||
|
double denom = classCounts_[c] + alpha_ * sp1Card;
|
||||||
|
double num = sp1FeatureCounts_[spVal * statesClass_ + c] + alpha_;
|
||||||
|
sp1FeatureProbs_[spVal * statesClass_ + c] =
|
||||||
|
(denom <= 0.0 ? 0.0 : num / denom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// p(sp2Val| c)
|
||||||
|
sp2FeatureProbs_.resize(sp2FeatureCounts_.size());
|
||||||
|
int sp2Card = states_[superParent2_];
|
||||||
|
for (int spVal = 0; spVal < sp2Card; spVal++) {
|
||||||
|
for (int c = 0; c < statesClass_; c++) {
|
||||||
|
double denom = classCounts_[c] + alpha_ * sp2Card;
|
||||||
|
double num = sp2FeatureCounts_[spVal * statesClass_ + c] + alpha_;
|
||||||
|
sp2FeatureProbs_[spVal * statesClass_ + c] =
|
||||||
|
(denom <= 0.0 ? 0.0 : num / denom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// p(childVal| c, sp1Val, sp2Val)
|
||||||
|
childProbs_.resize(childCounts_.size());
|
||||||
|
int offset = 0;
|
||||||
|
for (int f = 0; f < nFeatures_; f++) {
|
||||||
|
if (f == superParent1_ || f == superParent2_)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int fCard = states_[f];
|
||||||
|
int sp1Card_ = states_[superParent1_];
|
||||||
|
int sp2Card_ = states_[superParent2_];
|
||||||
|
int childBlockSizeSp2 = sp2Card_ * fCard * statesClass_;
|
||||||
|
int childBlockSizeF = fCard * statesClass_;
|
||||||
|
|
||||||
|
int blockSize = fCard * sp1Card_ * sp2Card_ * statesClass_;
|
||||||
|
for (int sp1Val = 0; sp1Val < sp1Card_; sp1Val++) {
|
||||||
|
for (int sp2Val = 0; sp2Val < sp2Card_; sp2Val++) {
|
||||||
|
for (int childVal = 0; childVal < fCard; childVal++) {
|
||||||
|
for (int c = 0; c < statesClass_; c++) {
|
||||||
|
// index in childCounts_
|
||||||
|
int idx = offset
|
||||||
|
+ sp1Val*childBlockSizeSp2
|
||||||
|
+ sp2Val*childBlockSizeF
|
||||||
|
+ childVal*statesClass_
|
||||||
|
+ c;
|
||||||
|
double num = childCounts_[idx] + alpha_;
|
||||||
|
// denominator is the count of (sp1Val,sp2Val,c) plus alpha * fCard
|
||||||
|
// We can find that by summing childVal dimension, but we already
|
||||||
|
// have it in childCounts_[...] or we can re-check the superparent
|
||||||
|
// counts if your approach is purely hierarchical.
|
||||||
|
// Here we'll do it like the XSpode approach: sp1&sp2 are
|
||||||
|
// conditionally independent given c, so denominators come from
|
||||||
|
// summing the relevant block or we treat sp1,sp2 as "parents."
|
||||||
|
// A simpler approach:
|
||||||
|
double sumSp1Sp2C = 0.0;
|
||||||
|
// sum over all childVal:
|
||||||
|
for (int cv = 0; cv < fCard; cv++) {
|
||||||
|
int idx2 = offset
|
||||||
|
+ sp1Val*childBlockSizeSp2
|
||||||
|
+ sp2Val*childBlockSizeF
|
||||||
|
+ cv*statesClass_ + c;
|
||||||
|
sumSp1Sp2C += childCounts_[idx2];
|
||||||
|
}
|
||||||
|
double denom = sumSp1Sp2C + alpha_ * fCard;
|
||||||
|
childProbs_[idx] = (denom <= 0.0 ? 0.0 : num / denom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset += blockSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// predict_proba (single instance)
|
||||||
|
// --------------------------------------
|
||||||
|
std::vector<double> XSpnde::predict_proba(const std::vector<int> &instance) const
|
||||||
|
{
|
||||||
|
if (!fitted) {
|
||||||
|
throw std::logic_error(CLASSIFIER_NOT_FITTED);
|
||||||
|
}
|
||||||
|
std::vector<double> probs(statesClass_, 0.0);
|
||||||
|
|
||||||
|
int sp1Val = instance[superParent1_];
|
||||||
|
int sp2Val = instance[superParent2_];
|
||||||
|
|
||||||
|
// Start with p(c) * p(sp1Val| c) * p(sp2Val| c)
|
||||||
|
for (int c = 0; c < statesClass_; c++) {
|
||||||
|
double pC = classPriors_[c];
|
||||||
|
double pSp1C = sp1FeatureProbs_[sp1Val * statesClass_ + c];
|
||||||
|
double pSp2C = sp2FeatureProbs_[sp2Val * statesClass_ + c];
|
||||||
|
probs[c] = pC * pSp1C * pSp2C * initializer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply by each child feature f
|
||||||
|
int offset = 0;
|
||||||
|
for (int f = 0; f < nFeatures_; f++) {
|
||||||
|
if (f == superParent1_ || f == superParent2_)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int valF = instance[f];
|
||||||
|
int fCard = states_[f];
|
||||||
|
int sp1Card = states_[superParent1_];
|
||||||
|
int sp2Card = states_[superParent2_];
|
||||||
|
int blockSizeSp2 = sp2Card * fCard * statesClass_;
|
||||||
|
int blockSizeF = fCard * statesClass_;
|
||||||
|
|
||||||
|
// base index for childProbs_ for this child and sp1Val, sp2Val
|
||||||
|
int base = offset
|
||||||
|
+ sp1Val*blockSizeSp2
|
||||||
|
+ sp2Val*blockSizeF
|
||||||
|
+ valF*statesClass_;
|
||||||
|
for (int c = 0; c < statesClass_; c++) {
|
||||||
|
probs[c] *= childProbs_[base + c];
|
||||||
|
}
|
||||||
|
offset += (fCard * sp1Card * sp2Card * statesClass_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
normalize(probs);
|
||||||
|
return probs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// predict_proba (batch)
|
||||||
|
// --------------------------------------
|
||||||
|
std::vector<std::vector<double>> XSpnde::predict_proba(std::vector<std::vector<int>> &test_data)
|
||||||
|
{
|
||||||
|
int test_size = test_data[0].size(); // each feature is test_data[f], size = #samples
|
||||||
|
int sample_size = test_data.size(); // = nFeatures_
|
||||||
|
std::vector<std::vector<double>> probabilities(
|
||||||
|
test_size, std::vector<double>(statesClass_, 0.0));
|
||||||
|
|
||||||
|
// same concurrency approach
|
||||||
|
int chunk_size = std::min(150, int(test_size / semaphore_.getMaxCount()) + 1);
|
||||||
|
std::vector<std::thread> threads;
|
||||||
|
|
||||||
|
auto worker = [&](const std::vector<std::vector<int>> &samples,
|
||||||
|
int begin,
|
||||||
|
int chunk,
|
||||||
|
int sample_size,
|
||||||
|
std::vector<std::vector<double>> &predictions) {
|
||||||
|
std::string threadName =
|
||||||
|
"XSpnde-" + std::to_string(begin) + "-" + std::to_string(chunk);
|
||||||
|
#if defined(__linux__)
|
||||||
|
pthread_setname_np(pthread_self(), threadName.c_str());
|
||||||
|
#else
|
||||||
|
pthread_setname_np(threadName.c_str());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::vector<int> instance(sample_size);
|
||||||
|
for (int sample = begin; sample < begin + chunk; ++sample) {
|
||||||
|
for (int feature = 0; feature < sample_size; ++feature) {
|
||||||
|
instance[feature] = samples[feature][sample];
|
||||||
|
}
|
||||||
|
predictions[sample] = predict_proba(instance);
|
||||||
|
}
|
||||||
|
semaphore_.release();
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int begin = 0; begin < test_size; begin += chunk_size) {
|
||||||
|
int chunk = std::min(chunk_size, test_size - begin);
|
||||||
|
semaphore_.acquire();
|
||||||
|
threads.emplace_back(worker, test_data, begin, chunk, sample_size,
|
||||||
|
std::ref(probabilities));
|
||||||
|
}
|
||||||
|
for (auto &th : threads) {
|
||||||
|
th.join();
|
||||||
|
}
|
||||||
|
return probabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// predict (single instance)
|
||||||
|
// --------------------------------------
|
||||||
|
int XSpnde::predict(const std::vector<int> &instance) const
|
||||||
|
{
|
||||||
|
auto p = predict_proba(instance);
|
||||||
|
return static_cast<int>(
|
||||||
|
std::distance(p.begin(), std::max_element(p.begin(), p.end()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// predict (batch of data)
|
||||||
|
// --------------------------------------
|
||||||
|
std::vector<int> XSpnde::predict(std::vector<std::vector<int>> &test_data)
|
||||||
|
{
|
||||||
|
auto probabilities = predict_proba(test_data);
|
||||||
|
std::vector<int> predictions(probabilities.size(), 0);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < probabilities.size(); i++) {
|
||||||
|
predictions[i] = static_cast<int>(
|
||||||
|
std::distance(probabilities[i].begin(),
|
||||||
|
std::max_element(probabilities[i].begin(),
|
||||||
|
probabilities[i].end()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return predictions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// predict (torch::Tensor version)
|
||||||
|
// --------------------------------------
|
||||||
|
torch::Tensor XSpnde::predict(torch::Tensor &X)
|
||||||
|
{
|
||||||
|
auto X_ = TensorUtils::to_matrix(X);
|
||||||
|
auto result_v = predict(X_);
|
||||||
|
return torch::tensor(result_v, torch::kInt32);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// predict_proba (torch::Tensor version)
|
||||||
|
// --------------------------------------
|
||||||
|
torch::Tensor XSpnde::predict_proba(torch::Tensor &X)
|
||||||
|
{
|
||||||
|
auto X_ = TensorUtils::to_matrix(X);
|
||||||
|
auto result_v = predict_proba(X_);
|
||||||
|
int n_samples = X.size(1);
|
||||||
|
torch::Tensor result =
|
||||||
|
torch::zeros({ n_samples, statesClass_ }, torch::kDouble);
|
||||||
|
for (int i = 0; i < (int)result_v.size(); ++i) {
|
||||||
|
result.index_put_({ i, "..." }, torch::tensor(result_v[i]));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// score (torch::Tensor version)
|
||||||
|
// --------------------------------------
|
||||||
|
float XSpnde::score(torch::Tensor &X, torch::Tensor &y)
|
||||||
|
{
|
||||||
|
torch::Tensor y_pred = predict(X);
|
||||||
|
return (y_pred == y).sum().item<float>() / y.size(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// score (vector version)
|
||||||
|
// --------------------------------------
|
||||||
|
float XSpnde::score(std::vector<std::vector<int>> &X, std::vector<int> &y)
|
||||||
|
{
|
||||||
|
auto y_pred = predict(X);
|
||||||
|
int correct = 0;
|
||||||
|
for (size_t i = 0; i < y_pred.size(); ++i) {
|
||||||
|
if (y_pred[i] == y[i]) {
|
||||||
|
correct++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<float>(correct) / static_cast<float>(y_pred.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// Utility: normalize
|
||||||
|
// --------------------------------------
|
||||||
|
void XSpnde::normalize(std::vector<double> &v) const
|
||||||
|
{
|
||||||
|
double sum = 0.0;
|
||||||
|
for (auto &val : v) {
|
||||||
|
sum += val;
|
||||||
|
}
|
||||||
|
if (sum > 0.0) {
|
||||||
|
for (auto &val : v) {
|
||||||
|
val /= sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// to_string
|
||||||
|
// --------------------------------------
|
||||||
|
std::string XSpnde::to_string() const
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "----- XSpnde Model -----\n"
|
||||||
|
<< "nFeatures_ = " << nFeatures_ << "\n"
|
||||||
|
<< "superParent1_ = " << superParent1_ << "\n"
|
||||||
|
<< "superParent2_ = " << superParent2_ << "\n"
|
||||||
|
<< "statesClass_ = " << statesClass_ << "\n\n";
|
||||||
|
|
||||||
|
oss << "States: [";
|
||||||
|
for (auto s : states_) oss << s << " ";
|
||||||
|
oss << "]\n";
|
||||||
|
|
||||||
|
oss << "classCounts_:\n";
|
||||||
|
for (auto v : classCounts_) oss << v << " ";
|
||||||
|
oss << "\nclassPriors_:\n";
|
||||||
|
for (auto v : classPriors_) oss << v << " ";
|
||||||
|
oss << "\nsp1FeatureCounts_ (size=" << sp1FeatureCounts_.size() << ")\n";
|
||||||
|
for (auto v : sp1FeatureCounts_) oss << v << " ";
|
||||||
|
oss << "\nsp2FeatureCounts_ (size=" << sp2FeatureCounts_.size() << ")\n";
|
||||||
|
for (auto v : sp2FeatureCounts_) oss << v << " ";
|
||||||
|
oss << "\nchildCounts_ (size=" << childCounts_.size() << ")\n";
|
||||||
|
for (auto v : childCounts_) oss << v << " ";
|
||||||
|
|
||||||
|
oss << "\nchildOffsets_:\n";
|
||||||
|
for (auto c : childOffsets_) oss << c << " ";
|
||||||
|
|
||||||
|
oss << "\n----------------------------------------\n";
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// Some introspection about the graph
|
||||||
|
// --------------------------------------
|
||||||
|
int XSpnde::getNumberOfNodes() const
|
||||||
|
{
|
||||||
|
// nFeatures + 1 class node
|
||||||
|
return nFeatures_ + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int XSpnde::getClassNumStates() const
|
||||||
|
{
|
||||||
|
return statesClass_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int XSpnde::getNFeatures() const
|
||||||
|
{
|
||||||
|
return nFeatures_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int XSpnde::getNumberOfStates() const
|
||||||
|
{
|
||||||
|
// purely an example. Possibly you want to sum up actual
|
||||||
|
// cardinalities or something else.
|
||||||
|
return std::accumulate(states_.begin(), states_.end(), 0) * nFeatures_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int XSpnde::getNumberOfEdges() const
|
||||||
|
{
|
||||||
|
// In an SPNDE with n=2, for each feature we have edges from class, sp1, sp2.
|
||||||
|
// So that’s 3*(nFeatures_) edges, minus the ones for the superparents themselves,
|
||||||
|
// plus the edges from class->superparent1, class->superparent2.
|
||||||
|
// For a quick approximation:
|
||||||
|
// - class->sp1, class->sp2 => 2 edges
|
||||||
|
// - class->child => (nFeatures -2) edges
|
||||||
|
// - sp1->child, sp2->child => 2*(nFeatures -2) edges
|
||||||
|
// total = 2 + (nFeatures-2) + 2*(nFeatures-2) = 2 + 3*(nFeatures-2)
|
||||||
|
// = 3nFeatures - 4 (just an example).
|
||||||
|
// You can adapt to your liking:
|
||||||
|
return 3 * nFeatures_ - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace bayesnet
|
||||||
|
|
75
bayesnet/classifiers/XSPnDE.h
Normal file
75
bayesnet/classifiers/XSPnDE.h
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// ***************************************************************
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez
|
||||||
|
// SPDX-FileType: SOURCE
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// ***************************************************************
|
||||||
|
|
||||||
|
#ifndef XSPNDE_H
|
||||||
|
#define XSPNDE_H
|
||||||
|
|
||||||
|
#include "Classifier.h"
|
||||||
|
#include "bayesnet/utils/CountingSemaphore.h"
|
||||||
|
#include <torch/torch.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace bayesnet {
|
||||||
|
|
||||||
|
class XSpnde : public Classifier {
|
||||||
|
public:
|
||||||
|
XSpnde(int spIndex1, int spIndex2);
|
||||||
|
void setHyperparameters(const nlohmann::json &hyperparameters_) override;
|
||||||
|
void fitx(torch::Tensor &X, torch::Tensor &y, torch::Tensor &weights_, const Smoothing_t smoothing);
|
||||||
|
std::vector<double> predict_proba(const std::vector<int> &instance) const;
|
||||||
|
std::vector<std::vector<double>> predict_proba(std::vector<std::vector<int>> &test_data) override;
|
||||||
|
int predict(const std::vector<int> &instance) const;
|
||||||
|
std::vector<int> predict(std::vector<std::vector<int>> &test_data) override;
|
||||||
|
torch::Tensor predict(torch::Tensor &X) override;
|
||||||
|
torch::Tensor predict_proba(torch::Tensor &X) override;
|
||||||
|
|
||||||
|
float score(torch::Tensor &X, torch::Tensor &y) override;
|
||||||
|
float score(std::vector<std::vector<int>> &X, std::vector<int> &y) override;
|
||||||
|
std::string to_string() const;
|
||||||
|
std::vector<std::string> graph(const std::string &title) const override {
|
||||||
|
return std::vector<std::string>({title});
|
||||||
|
}
|
||||||
|
|
||||||
|
int getNumberOfNodes() const override;
|
||||||
|
int getNumberOfEdges() const override;
|
||||||
|
int getNFeatures() const;
|
||||||
|
int getClassNumStates() const override;
|
||||||
|
int getNumberOfStates() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void buildModel(const torch::Tensor &weights) override;
|
||||||
|
void trainModel(const torch::Tensor &weights, const bayesnet::Smoothing_t smoothing) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void addSample(const std::vector<int> &instance, double weight);
|
||||||
|
void normalize(std::vector<double> &v) const;
|
||||||
|
void computeProbabilities();
|
||||||
|
|
||||||
|
int superParent1_;
|
||||||
|
int superParent2_;
|
||||||
|
int nFeatures_;
|
||||||
|
int statesClass_;
|
||||||
|
double alpha_;
|
||||||
|
double initializer_;
|
||||||
|
|
||||||
|
std::vector<int> states_;
|
||||||
|
std::vector<double> classCounts_;
|
||||||
|
std::vector<double> classPriors_;
|
||||||
|
std::vector<double> sp1FeatureCounts_, sp1FeatureProbs_;
|
||||||
|
std::vector<double> sp2FeatureCounts_, sp2FeatureProbs_;
|
||||||
|
// childOffsets_[f] will be the offset into childCounts_ for feature f.
|
||||||
|
// If f is either superParent1 or superParent2, childOffsets_[f] = -1
|
||||||
|
std::vector<int> childOffsets_;
|
||||||
|
// For each child f, we store p(x_f | c, sp1Val, sp2Val). We'll store the raw
|
||||||
|
// counts in childCounts_, and the probabilities in childProbs_, with a
|
||||||
|
// dimension block of size: states_[f]* statesClass_* states_[sp1]* states_[sp2].
|
||||||
|
std::vector<double> childCounts_;
|
||||||
|
std::vector<double> childProbs_;
|
||||||
|
CountingSemaphore &semaphore_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bayesnet
|
||||||
|
#endif // XSPNDE_H
|
@@ -4,14 +4,9 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// ***************************************************************
|
// ***************************************************************
|
||||||
|
|
||||||
#include <set>
|
|
||||||
#include <functional>
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <folding.hpp>
|
#include <folding.hpp>
|
||||||
#include "bayesnet/feature_selection/CFS.h"
|
|
||||||
#include "bayesnet/feature_selection/FCBF.h"
|
|
||||||
#include "bayesnet/feature_selection/IWSS.h"
|
|
||||||
#include "BoostA2DE.h"
|
#include "BoostA2DE.h"
|
||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
|
@@ -8,17 +8,10 @@
|
|||||||
#define XBAODE_H
|
#define XBAODE_H
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include <limits>
|
|
||||||
#include "bayesnet/classifiers/XSPODE.h"
|
|
||||||
#include "Boost.h"
|
#include "Boost.h"
|
||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
class XBAODE : public Boost {
|
class XBAODE : public Boost {
|
||||||
|
|
||||||
// Hay que hacer un vector de modelos entrenados y hacer un predict ensemble con todos ellos
|
|
||||||
// Probar XA1DE con smooth original y laplace y comprobar diferencias si se pasan pesos a 1 o a 1/m
|
|
||||||
public:
|
public:
|
||||||
XBAODE();
|
XBAODE();
|
||||||
std::string getVersion() override { return version; };
|
std::string getVersion() override { return version; };
|
||||||
|
@@ -9,7 +9,7 @@ if(ENABLE_TESTING)
|
|||||||
${CMAKE_BINARY_DIR}/configured_files/include
|
${CMAKE_BINARY_DIR}/configured_files/include
|
||||||
)
|
)
|
||||||
file(GLOB_RECURSE BayesNet_SOURCES "${BayesNet_SOURCE_DIR}/bayesnet/*.cc")
|
file(GLOB_RECURSE BayesNet_SOURCES "${BayesNet_SOURCE_DIR}/bayesnet/*.cc")
|
||||||
add_executable(TestBayesNet TestBayesNetwork.cc TestBayesNode.cc TestBayesClassifier.cc
|
add_executable(TestBayesNet TestBayesNetwork.cc TestBayesNode.cc TestBayesClassifier.cc TestXSPnDE.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 PRIVATE Catch2::Catch2WithMain)
|
target_link_libraries(TestBayesNet PUBLIC "${TORCH_LIBRARIES}" fimdlp PRIVATE Catch2::Catch2WithMain)
|
||||||
@@ -18,6 +18,7 @@ if(ENABLE_TESTING)
|
|||||||
add_test(NAME BoostA2DE COMMAND TestBayesNet "[BoostA2DE]")
|
add_test(NAME BoostA2DE COMMAND TestBayesNet "[BoostA2DE]")
|
||||||
add_test(NAME BoostAODE COMMAND TestBayesNet "[BoostAODE]")
|
add_test(NAME BoostAODE COMMAND TestBayesNet "[BoostAODE]")
|
||||||
add_test(NAME XSPODE COMMAND TestBayesNet "[XSPODE]")
|
add_test(NAME XSPODE COMMAND TestBayesNet "[XSPODE]")
|
||||||
|
add_test(NAME XSPnDE COMMAND TestBayesNet "[XSPnDE]")
|
||||||
add_test(NAME XBAODE COMMAND TestBayesNet "[XBAODE]")
|
add_test(NAME XBAODE COMMAND TestBayesNet "[XBAODE]")
|
||||||
add_test(NAME Classifier COMMAND TestBayesNet "[Classifier]")
|
add_test(NAME Classifier COMMAND TestBayesNet "[Classifier]")
|
||||||
add_test(NAME Ensemble COMMAND TestBayesNet "[Ensemble]")
|
add_test(NAME Ensemble COMMAND TestBayesNet "[Ensemble]")
|
||||||
|
120
tests/TestXSPnDE.cc
Normal file
120
tests/TestXSPnDE.cc
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// ***************************************************************
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez
|
||||||
|
// SPDX-FileType: SOURCE
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// ***************************************************************
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/catch_approx.hpp>
|
||||||
|
#include <catch2/matchers/catch_matchers.hpp>
|
||||||
|
#include "bayesnet/classifiers/XSPnDE.h" // <-- your new 2-superparent classifier
|
||||||
|
#include "TestUtils.h" // for RawDatasets, etc.
|
||||||
|
|
||||||
|
// Helper function to handle each (sp1, sp2) pair in tests
|
||||||
|
static void check_spnde_pair(
|
||||||
|
int sp1,
|
||||||
|
int sp2,
|
||||||
|
RawDatasets &raw,
|
||||||
|
bool fitVector,
|
||||||
|
bool fitTensor)
|
||||||
|
{
|
||||||
|
// Create our classifier
|
||||||
|
bayesnet::XSpnde clf(sp1, sp2);
|
||||||
|
|
||||||
|
// Option A: fit with vector-based data
|
||||||
|
if (fitVector) {
|
||||||
|
clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing);
|
||||||
|
}
|
||||||
|
// Option B: fit with the whole dataset in torch::Tensor form
|
||||||
|
else if (fitTensor) {
|
||||||
|
// your “tensor” version of fit
|
||||||
|
clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing);
|
||||||
|
}
|
||||||
|
// Option C: or you might do the “dataset” version:
|
||||||
|
else {
|
||||||
|
clf.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic checks
|
||||||
|
REQUIRE(clf.getNumberOfNodes() == 5); // for iris: 4 features + 1 class
|
||||||
|
// For XSpnde, edges are often computed as 3*nFeatures - 4. For iris nFeatures=4 => 3*4 -4 = 8
|
||||||
|
REQUIRE(clf.getNumberOfEdges() == 8);
|
||||||
|
REQUIRE(clf.getNotes().size() == 0);
|
||||||
|
|
||||||
|
// Evaluate on test set
|
||||||
|
float sc = clf.score(raw.X_test, raw.y_test);
|
||||||
|
// If you know the exact expected accuracy for each pair, use:
|
||||||
|
// REQUIRE(sc == Catch::Approx(someValue));
|
||||||
|
// Otherwise, just check it's > some threshold:
|
||||||
|
REQUIRE(sc >= 0.90f); // placeholder; you can pick your own threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// 1) Fit vector test
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
TEST_CASE("fit vector test (XSPNDE)", "[XSPNDE]") {
|
||||||
|
auto raw = RawDatasets("iris", true);
|
||||||
|
|
||||||
|
// We’ll test a couple of two-superparent pairs, e.g. (0,1) and (2,3).
|
||||||
|
// You can add more if you like, e.g. (0,2), (1,3), etc.
|
||||||
|
std::vector<std::pair<int,int>> parentPairs = {
|
||||||
|
{0,1}, {2,3}
|
||||||
|
};
|
||||||
|
for (auto &p : parentPairs) {
|
||||||
|
// We’re doing the “vector” version
|
||||||
|
check_spnde_pair(p.first, p.second, raw, /*fitVector=*/true, /*fitTensor=*/false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// 2) Fit dataset test
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
TEST_CASE("fit dataset test (XSPNDE)", "[XSPNDE]") {
|
||||||
|
auto raw = RawDatasets("iris", true);
|
||||||
|
|
||||||
|
// Again test multiple pairs:
|
||||||
|
std::vector<std::pair<int,int>> parentPairs = {
|
||||||
|
{0,2}, {1,3}
|
||||||
|
};
|
||||||
|
for (auto &p : parentPairs) {
|
||||||
|
// Now do the “dataset” version
|
||||||
|
check_spnde_pair(p.first, p.second, raw, /*fitVector=*/false, /*fitTensor=*/false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// 3) Tensors dataset predict & predict_proba
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
TEST_CASE("tensors dataset predict & predict_proba (XSPNDE)", "[XSPNDE]") {
|
||||||
|
auto raw = RawDatasets("iris", true);
|
||||||
|
|
||||||
|
// Let’s test a single pair or multiple pairs. For brevity:
|
||||||
|
std::vector<std::pair<int,int>> parentPairs = {
|
||||||
|
{0,3}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto &p : parentPairs) {
|
||||||
|
bayesnet::XSpnde clf(p.first, p.second);
|
||||||
|
// Fit using the “tensor” approach
|
||||||
|
clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing);
|
||||||
|
|
||||||
|
REQUIRE(clf.getNumberOfNodes() == 5);
|
||||||
|
REQUIRE(clf.getNumberOfEdges() == 8);
|
||||||
|
REQUIRE(clf.getNotes().size() == 0);
|
||||||
|
|
||||||
|
// Check the score
|
||||||
|
float sc = clf.score(raw.X_test, raw.y_test);
|
||||||
|
REQUIRE(sc >= 0.90f);
|
||||||
|
|
||||||
|
// You can also test predict_proba on a small slice:
|
||||||
|
// e.g. the first 3 samples in X_test
|
||||||
|
auto X_reduced = raw.X_test.slice(1, 0, 3);
|
||||||
|
auto proba = clf.predict_proba(X_reduced);
|
||||||
|
|
||||||
|
// If you know exact probabilities, compare them with Catch::Approx.
|
||||||
|
// For example:
|
||||||
|
// REQUIRE(proba[0][0].item<double>() == Catch::Approx(0.98));
|
||||||
|
// etc.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user