From 4939a5b6738e844a5abd032000f4382a6a6348ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana?= Date: Thu, 8 Dec 2022 22:28:21 +0100 Subject: [PATCH] Refactor base algorithm --- fimdlp/CPPFImdlp.cpp | 312 ++++------------ fimdlp/CPPFImdlp.h | 23 +- fimdlp/Metrics.cpp | 67 ++-- fimdlp/Metrics.h | 19 +- fimdlp/ccFImdlp.cc | 110 ------ fimdlp/ccFImdlp.h | 32 -- fimdlp/ccMetrics.cc | 74 ---- fimdlp/ccMetrics.h | 21 -- fimdlp/cfimdlp.pyx | 21 +- fimdlp/cppfimdlp.cpython-310-darwin.so | Bin 122688 -> 122456 bytes fimdlp/m2.cpp | 36 -- fimdlp/main | Bin 239361 -> 0 bytes fimdlp/main.cpp | 52 --- fimdlp/mdlp.py | 1 - fimdlp/pyfimdlp.py | 479 ------------------------- fimdlp/testcpp/FImdlp_unittest.cc | 4 +- fimdlp/testcpp/Metrics_unittest.cc | 2 +- fimdlp/tests/bak/CPPFImdlp.cpp | 286 +++++++++++++++ fimdlp/tests/bak/CPPFImdlp.h | 39 ++ fimdlp/tests/bak/Metrics.cpp | 47 +++ fimdlp/tests/bak/Metrics.h | 14 + fimdlp/typesFImdlp.h | 19 +- prueba/FImdlp.cpp | 2 +- prueba/cfimdlp.pyx | 2 +- setup.py | 6 +- 25 files changed, 538 insertions(+), 1130 deletions(-) delete mode 100644 fimdlp/ccFImdlp.cc delete mode 100644 fimdlp/ccFImdlp.h delete mode 100644 fimdlp/ccMetrics.cc delete mode 100644 fimdlp/ccMetrics.h delete mode 100644 fimdlp/m2.cpp delete mode 100755 fimdlp/main delete mode 100644 fimdlp/main.cpp delete mode 100644 fimdlp/pyfimdlp.py create mode 100644 fimdlp/tests/bak/CPPFImdlp.cpp create mode 100644 fimdlp/tests/bak/CPPFImdlp.h create mode 100644 fimdlp/tests/bak/Metrics.cpp create mode 100644 fimdlp/tests/bak/Metrics.h diff --git a/fimdlp/CPPFImdlp.cpp b/fimdlp/CPPFImdlp.cpp index cdafb99..8113c55 100644 --- a/fimdlp/CPPFImdlp.cpp +++ b/fimdlp/CPPFImdlp.cpp @@ -1,41 +1,20 @@ -#include "CPPFImdlp.h" #include #include #include +#include +#include "CPPFImdlp.h" #include "Metrics.h" namespace mdlp { - ostream& operator << (ostream& os, const cutPoint_t& cut) + CPPFImdlp::CPPFImdlp(): proposal(true), debug(false), indices(indices_t()), y(labels()), metrics(Metrics(y, indices)) { - os << cut.classNumber << " -> (" << cut.start << ", " << cut.end << - ") - (" << cut.fromValue << ", " << cut.toValue << ") " - << endl; - return os; - } - CPPFImdlp::CPPFImdlp(): proposal(true), precision(6), debug(false) + CPPFImdlp::CPPFImdlp(bool proposal, bool debug): proposal(proposal), debug(debug), indices(indices_t()), y(labels()), metrics(Metrics(y, indices)) { - divider = pow(10, precision); - numClasses = 0; - } - CPPFImdlp::CPPFImdlp(bool proposal, int precision, bool debug): proposal(proposal), precision(precision), debug(debug) - { - divider = pow(10, precision); - numClasses = 0; } CPPFImdlp::~CPPFImdlp() = default; - samples CPPFImdlp::getCutPoints() - { - samples output(cutPoints.size()); - ::transform(cutPoints.begin(), cutPoints.end(), output.begin(), - [](cutPoint_t cut) { return cut.toValue; }); - return output; - } - labels CPPFImdlp::getDiscretizedValues() - { - return xDiscretized; - } + CPPFImdlp& CPPFImdlp::fit(samples& X_, labels& y_) { X = X_; @@ -47,227 +26,78 @@ namespace mdlp { throw invalid_argument("X and y must have at least one element"); } indices = sortIndices(X_); - xDiscretized = labels(X.size(), -1); - numClasses = Metrics::numClasses(y, indices, 0, X.size()); - - if (proposal) { - computeCutPointsProposal(); - } else { - computeCutPointsOriginal(); - } - filterCutPoints(); - // Apply cut points to the input vector - for (auto cut : cutPoints) { - for (size_t i = cut.start; i < cut.end; i++) { - xDiscretized[indices[i]] = cut.classNumber; - } - } + metrics.setData(y, indices); + computeCutPoints(0, X.size()); return *this; } - bool CPPFImdlp::evaluateCutPoint(cutPoint_t rest, cutPoint_t candidate) + void CPPFImdlp::computeCutPoints(size_t start, size_t end) + { + int cut; + if (end - start < 2) + return; + cut = getCandidate(start, end); + if (cut == -1 || !mdlp(start, cut, end)) { + // cut.value == -1 means that there is no candidate in the interval + // No boundary found, so we add both ends of the interval as cutpoints + // because they were selected by the algorithm before + if (start != 0) + cutPoints.push_back((X[indices[start]] + X[indices[start - 1]]) / 2); + if (end != X.size()) + cutPoints.push_back((X[indices[end]] + X[indices[end - 1]]) / 2); + return; + } + computeCutPoints(start, cut); + computeCutPoints(cut, end); + } + long int CPPFImdlp::getCandidate(size_t start, size_t end) + { + long int candidate = -1, elements = end - start; + precision_t entropy_left, entropy_right, minEntropy = numeric_limits::max(); + for (auto idx = start + 1; idx < end; idx++) { + // Cutpoints are always on boudndaries + if (y[indices[idx]] == y[indices[idx - 1]]) + continue; + entropy_left = precision_t(idx - start) / elements * metrics.entropy(start, idx); + entropy_right = precision_t(end - idx) / elements * metrics.entropy(idx, end); + if (entropy_left + entropy_right < minEntropy) { + minEntropy = entropy_left + entropy_right; + candidate = idx; + } + } + return candidate; + } + bool CPPFImdlp::mdlp(size_t start, size_t cut, size_t end) { int k, k1, k2; - float ig, delta; - float ent, ent1, ent2; - auto N = float(rest.end - rest.start); + precision_t ig, delta; + precision_t ent, ent1, ent2; + auto N = precision_t(end - start); if (N < 2) { return false; } - k = Metrics::numClasses(y, indices, rest.start, rest.end); - k1 = Metrics::numClasses(y, indices, rest.start, candidate.end); - k2 = Metrics::numClasses(y, indices, candidate.end, rest.end); - ent = Metrics::entropy(y, indices, rest.start, rest.end, numClasses); - ent1 = Metrics::entropy(y, indices, rest.start, candidate.end, numClasses); - ent2 = Metrics::entropy(y, indices, candidate.end, rest.end, numClasses); - ig = Metrics::informationGain(y, indices, rest.start, rest.end, candidate.end, numClasses); - delta = log2(pow(3, float(k)) - 2) - (float(k) * ent - float(k1) * ent1 - float(k2) * ent2); - float term = 1 / N * (log2(N - 1) + delta); - if (debug) { - cout << "Rest: " << rest; - cout << "Candidate: " << candidate; - cout << "k=" << k << " k1=" << k1 << " k2=" << k2 << " ent=" << ent << " ent1=" << ent1 << " ent2=" << ent2 << endl; - cout << "ig=" << ig << " delta=" << delta << " N " << N << " term " << term << endl; - } - return (ig > term); + k = metrics.computeNumClasses(start, end); + k1 = metrics.computeNumClasses(start, cut); + k2 = metrics.computeNumClasses(cut, end); + ent = metrics.entropy(start, end); + ent1 = metrics.entropy(start, cut); + ent2 = metrics.entropy(cut, end); + ig = metrics.informationGain(start, cut, end); + delta = log2(pow(3, precision_t(k)) - 2) - + (precision_t(k) * ent - precision_t(k1) * ent1 - precision_t(k2) * ent2); + precision_t term = 1 / N * (log2(N - 1) + delta); + return ig > term; } - void CPPFImdlp::filterCutPoints() + cutPoints_t CPPFImdlp::getCutPoints() { - cutPoints_t filtered; - cutPoint_t rest, item; - int classNumber = 0; - - rest.start = 0; - rest.end = X.size(); - rest.fromValue = numeric_limits::lowest(); - rest.toValue = numeric_limits::max(); - rest.classNumber = classNumber; - bool first = true; - for (size_t index = 0; index < size_t(cutPoints.size()); index++) { - item = cutPoints[index]; - if (evaluateCutPoint(rest, item)) { - if (debug) - cout << "Accepted: " << item << endl; - //Assign class number to the interval (cutpoint) - item.classNumber = classNumber++; - filtered.push_back(item); - first = false; - rest.start = item.end; - } else { - if (debug) - cout << "Rejected: " << item << endl; - if (index != size_t(cutPoints.size()) - 1) { - // Try to merge the rejected cutpoint with the next one - if (first) { - cutPoints[index + 1].fromValue = numeric_limits::lowest(); - cutPoints[index + 1].start = indices[0]; - } else { - cutPoints[index + 1].fromValue = item.fromValue; - cutPoints[index + 1].start = item.start; - } - } - } - } - if (!first) { - filtered.back().toValue = numeric_limits::max(); - filtered.back().end = X.size() - 1; - } else { - filtered.push_back(rest); - } - cutPoints = filtered; - } - void CPPFImdlp::computeCutPointsProposal() - { - cutPoints_t cutPts; - cutPoint_t cutPoint; - float xPrev, xCur, xPivot; - int yPrev, yCur, yPivot; - size_t idx, numElements, start; - - xCur = xPrev = X[indices[0]]; - yCur = yPrev = y[indices[0]]; - numElements = indices.size() - 1; - idx = start = 0; - bool firstCutPoint = true; - if (debug) - printf("*idx=%lu -> (-1, -1) Prev(%3.1f, %d) Elementos: %lu\n", idx, xCur, yCur, numElements); - while (idx < numElements) { - xPivot = xCur; - yPivot = yCur; - if (debug) - printf(" Prev(%3.1f, %d) Pivot(%3.1f, %d) Cur(%3.1f, %d) \n", idx, xPrev, yPrev, xPivot, yPivot, xCur, yCur); - // Read the same values and check class changes - do { - idx++; - xCur = X[indices[idx]]; - yCur = y[indices[idx]]; - if (yCur != yPivot && xCur == xPivot) { - yPivot = -1; - } - if (debug) - printf(">idx=%lu -> Prev(%3.1f, %d) Pivot(%3.1f, %d) Cur(%3.1f, %d) \n", idx, xPrev, yPrev, xPivot, yPivot, xCur, yCur); - } - while (idx < numElements && xCur == xPivot); - // Check if the class changed and there are more than 1 element - if ((idx - start > 1) && (yPivot == -1 || yPrev != yCur) && goodCut(start, idx, numElements + 1)) { - // Must we add the entropy criteria here? - // if (totalEntropy - (entropyLeft + entropyRight) > 0) { Accept cut point } - cutPoint.start = start; - cutPoint.end = idx; - start = idx; - cutPoint.fromValue = firstCutPoint ? numeric_limits::lowest() : cutPts.back().toValue; - cutPoint.toValue = (xPrev + xCur) / 2; - cutPoint.classNumber = -1; - firstCutPoint = false; - if (debug) { - printf("Cutpoint idx=%lu Cur(%3.1f, %d) Prev(%3.1f, %d) Pivot(%3.1f, %d) = (%3.1g, %3.1g] \n", idx, xCur, yCur, xPrev, yPrev, xPivot, yPivot, cutPoint.fromValue, cutPoint.toValue); - } - cutPts.push_back(cutPoint); - } - yPrev = yPivot; - xPrev = xPivot; - } - if (idx == numElements) { - cutPoint.start = start; - cutPoint.end = numElements + 1; - cutPoint.fromValue = firstCutPoint ? numeric_limits::lowest() : cutPts.back().toValue; - cutPoint.toValue = numeric_limits::max(); - cutPoint.classNumber = -1; - if (debug) - printf("Final Cutpoint idx=%lu Cur(%3.1f, %d) Prev(%3.1f, %d) Pivot(%3.1f, %d) = (%3.1g, %3.1g] \n", idx, xCur, yCur, xPrev, yPrev, xPivot, yPivot, cutPoint.fromValue, cutPoint.toValue); - cutPts.push_back(cutPoint); - } - if (debug) { - cout << "Entropy of the dataset: " << Metrics::entropy(y, indices, 0, numElements + 1, numClasses) << endl; - for (auto cutPt : cutPts) - cout << "Entropy: " << Metrics::entropy(y, indices, cutPt.start, cutPt.end, numClasses) << " :Proposal: Cut point: " << cutPt; - } - cutPoints = cutPts; - } - void CPPFImdlp::computeCutPointsOriginal() - { - cutPoints_t cutPts; - cutPoint_t cutPoint; - float xPrev; - int yPrev; - bool first = true; - // idxPrev is the index of the init instance of the cutPoint - size_t index, idxPrev = 0, last, idx = indices[0]; - xPrev = X[idx]; - yPrev = y[idx]; - last = indices.size() - 1; - for (index = 0; index < last; index++) { - idx = indices[index]; - // Definition 2 Cut points are always on class boundaries && - // there are more than 1 items in the interval - // if (entropy of interval) > (entropyLeft + entropyRight)) { Accept cut point } (goodCut) - if (y[idx] != yPrev && xPrev < X[idx] && idxPrev != index - 1 && goodCut(idxPrev, idx, last + 1)) { - // Must we add the entropy criteria here? - if (first) { - first = false; - cutPoint.fromValue = numeric_limits::lowest(); - } else { - cutPoint.fromValue = cutPts.back().toValue; - } - cutPoint.start = idxPrev; - cutPoint.end = index; - cutPoint.classNumber = -1; - cutPoint.toValue = round(divider * (X[idx] + xPrev) / 2) / divider; - idxPrev = index; - cutPts.push_back(cutPoint); - } - xPrev = X[idx]; - yPrev = y[idx]; - } - if (first) { - cutPoint.start = 0; - cutPoint.classNumber = -1; - cutPoint.fromValue = numeric_limits::lowest(); - cutPoint.toValue = numeric_limits::max(); - cutPts.push_back(cutPoint); - } else - cutPts.back().toValue = numeric_limits::max(); - cutPts.back().end = X.size(); - if (debug) { - cout << "Entropy of the dataset: " << Metrics::entropy(y, indices, 0, indices.size(), numClasses) << endl; - for (auto cutPt : cutPts) - cout << "Entropy: " << Metrics::entropy(y, indices, cutPt.start, cutPt.end, numClasses) << ": Original: Cut point: " << cutPt; - } - cutPoints = cutPts; - } - bool CPPFImdlp::goodCut(size_t start, size_t cut, size_t end) - { - /* - Meter las entropías en una matríz cuadrada dispersa (samples, samples) M[start, end] iniciada a -1 y si no se ha calculado calcularla y almacenarla - - - */ - float entropyLeft = Metrics::entropy(y, indices, start, cut, numClasses); - float entropyRight = Metrics::entropy(y, indices, cut, end, numClasses); - float entropyInterval = Metrics::entropy(y, indices, start, end, numClasses); - if (debug) - printf("Entropy L, R, T: L(%5.3g) + R(%5.3g) - T(%5.3g) \t", entropyLeft, entropyRight, entropyInterval); - //return (entropyInterval - (entropyLeft + entropyRight) > 0); - return true; + // Remove duplicates and sort + cutPoints_t output(cutPoints.size()); + set s; + unsigned size = cutPoints.size(); + for (unsigned i = 0; i < size; i++) + s.insert(cutPoints[i]); + output.assign(s.begin(), s.end()); + sort(output.begin(), output.end()); + return output; } // Argsort from https://stackoverflow.com/questions/1577475/c-sorting-and-keeping-track-of-indexes indices_t CPPFImdlp::sortIndices(samples& X_) @@ -275,12 +105,8 @@ namespace mdlp { indices_t idx(X_.size()); iota(idx.begin(), idx.end(), 0); for (size_t i = 0; i < X_.size(); i++) - stable_sort(idx.begin(), idx.end(), [&X_](size_t i1, size_t i2) + sort(idx.begin(), idx.end(), [&X_](size_t i1, size_t i2) { return X_[i1] < X_[i2]; }); return idx; } - void CPPFImdlp::setCutPoints(cutPoints_t cutPoints_) - { - cutPoints = cutPoints_; - } } diff --git a/fimdlp/CPPFImdlp.h b/fimdlp/CPPFImdlp.h index 926b6e6..7d467cc 100644 --- a/fimdlp/CPPFImdlp.h +++ b/fimdlp/CPPFImdlp.h @@ -1,39 +1,30 @@ #ifndef CPPFIMDLP_H #define CPPFIMDLP_H #include "typesFImdlp.h" +#include "Metrics.h" #include namespace mdlp { class CPPFImdlp { protected: bool proposal; // proposed algorithm or original algorithm - int precision; bool debug; - float divider; indices_t indices; // sorted indices to use with X and y samples X; labels y; - labels xDiscretized; - int numClasses; + Metrics metrics; cutPoints_t cutPoints; - void setCutPoints(cutPoints_t); static indices_t sortIndices(samples&); - void computeCutPointsOriginal(); - void computeCutPointsProposal(); - bool evaluateCutPoint(cutPoint_t, cutPoint_t); - void filterCutPoints(); - bool goodCut(size_t, size_t, size_t); // if the cut candidate reduces entropy + void computeCutPoints(size_t, size_t); + long int getCandidate(size_t, size_t); + bool mdlp(size_t, size_t, size_t); public: CPPFImdlp(); - CPPFImdlp(bool, int, bool debug = false); + CPPFImdlp(bool, bool debug = false); ~CPPFImdlp(); - samples getCutPoints(); - indices_t getIndices(); - labels getDiscretizedValues(); - void debugPoints(samples&, labels&); CPPFImdlp& fit(samples&, labels&); - labels transform(samples&); + samples getCutPoints(); }; } #endif \ No newline at end of file diff --git a/fimdlp/Metrics.cpp b/fimdlp/Metrics.cpp index ffc1806..041ecf4 100644 --- a/fimdlp/Metrics.cpp +++ b/fimdlp/Metrics.cpp @@ -1,46 +1,63 @@ #include "Metrics.h" #include +#include +using namespace std; namespace mdlp { - Metrics::Metrics() - = default; - int Metrics::numClasses(labels& y, indices_t indices, size_t start, size_t end) + Metrics::Metrics(labels& y_, indices_t& indices_): y(y_), indices(indices_), numClasses(computeNumClasses(0, indices.size())), entropyCache(cacheEnt_t()), igCache(cacheIg_t()) { - std::set numClasses; - for (auto i = start; i < end; ++i) { - numClasses.insert(y[indices[i]]); - } - return numClasses.size(); } - float Metrics::entropy(labels& y, indices_t& indices, size_t start, size_t end, int nClasses) + int Metrics::computeNumClasses(size_t start, size_t end) { - float entropy = 0; + set nClasses; + for (auto i = start; i < end; ++i) { + nClasses.insert(y[indices[i]]); + } + return nClasses.size(); + } + void Metrics::setData(labels& y_, indices_t& indices_) + { + indices = indices_; + y = y_; + numClasses = computeNumClasses(0, indices.size()); + } + precision_t Metrics::entropy(size_t start, size_t end) + { + precision_t p, ventropy = 0; int nElements = 0; - labels counts(nClasses + 1, 0); + labels counts(numClasses + 1, 0); + if (end - start < 2) + return 0; + if (entropyCache.find(make_tuple(start, end)) != entropyCache.end()) { + return entropyCache[make_tuple(start, end)]; + } for (auto i = &indices[start]; i != &indices[end]; ++i) { counts[y[*i]]++; nElements++; } for (auto count : counts) { if (count > 0) { - float p = (float)count / nElements; - entropy -= p * log2(p); + p = (precision_t)count / nElements; + ventropy -= p * log2(p); } } - return entropy < 0 ? 0 : entropy; + entropyCache[make_tuple(start, end)] = ventropy; + return ventropy; } - float Metrics::informationGain(labels& y, indices_t& indices, size_t start, size_t end, size_t cutPoint, int nClasses) + precision_t Metrics::informationGain(size_t start, size_t cut, size_t end) { - float iGain; - float entropy, entropyLeft, entropyRight; - int nClassesLeft, nClassesRight; - int nElementsLeft = cutPoint - start, nElementsRight = end - cutPoint; + precision_t iGain; + precision_t entropyInterval, entropyLeft, entropyRight; + int nElementsLeft = cut - start, nElementsRight = end - cut; int nElements = end - start; - nClassesLeft = Metrics::numClasses(y, indices, start, cutPoint); - nClassesRight = Metrics::numClasses(y, indices, cutPoint, end); - entropy = Metrics::entropy(y, indices, start, end, nClasses); - entropyLeft = Metrics::entropy(y, indices, start, cutPoint, nClassesLeft); - entropyRight = Metrics::entropy(y, indices, cutPoint, end, nClassesRight); - iGain = entropy - ((float)nElementsLeft * entropyLeft + (float)nElementsRight * entropyRight) / nElements; + if (igCache.find(make_tuple(start, cut, end)) != igCache.end()) { + cout << "**********Cache IG hit for " << start << " " << end << endl; + return igCache[make_tuple(start, cut, end)]; + } + entropyInterval = entropy(start, end); + entropyLeft = entropy(start, cut); + entropyRight = entropy(cut, end); + iGain = entropyInterval - ((precision_t)nElementsLeft * entropyLeft + (precision_t)nElementsRight * entropyRight) / nElements; + igCache[make_tuple(start, cut, end)] = iGain; return iGain; } diff --git a/fimdlp/Metrics.h b/fimdlp/Metrics.h index 41b9b2c..79bc286 100644 --- a/fimdlp/Metrics.h +++ b/fimdlp/Metrics.h @@ -1,14 +1,21 @@ -#ifndef METRICS_H -#define METRICS_H +#ifndef CCMETRICS_H +#define CCMETRICS_H #include "typesFImdlp.h" #include namespace mdlp { class Metrics { + protected: + labels& y; + indices_t& indices; + int numClasses; + cacheEnt_t entropyCache; + cacheIg_t igCache; public: - Metrics(); - static int numClasses(labels&, indices_t, size_t, size_t); - static float entropy(labels&, indices_t&, size_t, size_t, int); - static float informationGain(labels&, indices_t&, size_t, size_t, size_t, int); + Metrics(labels&, indices_t&); + void setData(labels&, indices_t&); + int computeNumClasses(size_t, size_t); + precision_t entropy(size_t, size_t); + precision_t informationGain(size_t, size_t, size_t); }; } #endif \ No newline at end of file diff --git a/fimdlp/ccFImdlp.cc b/fimdlp/ccFImdlp.cc deleted file mode 100644 index 629b762..0000000 --- a/fimdlp/ccFImdlp.cc +++ /dev/null @@ -1,110 +0,0 @@ -#include "ccFImdlp.h" -#include -#include -#include -#include -#include "ccMetrics.h" - -namespace mdlp { - CPPFImdlp::CPPFImdlp(): proposal(true), precision(6), debug(false), divider(pow(10, precision)), indices(indices_t()), y(labels()), metrics(Metrics(y, indices)) - { - } - CPPFImdlp::CPPFImdlp(bool proposal, int precision, bool debug): proposal(proposal), precision(precision), debug(debug), divider(pow(10, precision)), indices(indices_t()), y(labels()), metrics(Metrics(y, indices)) - { - } - CPPFImdlp::~CPPFImdlp() - = default; - - CPPFImdlp& CPPFImdlp::fitx(samples& X_, labels& y_) - { - X = X_; - y = y_; - if (X.size() != y.size()) { - throw invalid_argument("X and y must have the same size"); - } - if (X.size() == 0 || y.size() == 0) { - throw invalid_argument("X and y must have at least one element"); - } - indices = sortIndices(X_); - metrics.setData(y, indices); - computeCutPoints(0, X.size()); - return *this; - } - void CPPFImdlp::computeCutPoints(size_t start, size_t end) - { - int cut; - if (end - start < 2) - return; - cut = getCandidate(start, end); - if (cut == -1 || !mdlp(start, cut, end)) { - // cut.value == -1 means that there is no candidate in the interval - // that enhances the information gain - if (start != 0) - xCutPoints.push_back(xcutPoint_t({ start, (X[indices[start]] + X[indices[start - 1]]) / 2 })); - if (end != X.size()) - xCutPoints.push_back(xcutPoint_t({ end, (X[indices[end]] + X[indices[end - 1]]) / 2 })); - return; - } - computeCutPoints(start, cut); - computeCutPoints(cut, end); - } - long int CPPFImdlp::getCandidate(size_t start, size_t end) - { - long int candidate = -1, elements = end - start; - float entropy_left, entropy_right, minEntropy = numeric_limits::max(); - for (auto idx = start + 1; idx < end; idx++) { - // Cutpoints are always on boudndaries - if (y[indices[idx]] == y[indices[idx - 1]]) - continue; - entropy_left = float(idx - start) / elements * metrics.entropy(start, idx); - entropy_right = float(end - idx) / elements * metrics.entropy(idx, end); - if (entropy_left + entropy_right < minEntropy) { - minEntropy = entropy_left + entropy_right; - candidate = idx; - } - } - return candidate; - } - bool CPPFImdlp::mdlp(size_t start, size_t cut, size_t end) - { - int k, k1, k2; - float ig, delta; - float ent, ent1, ent2; - auto N = float(end - start); - if (N < 2) { - return false; - } - k = metrics.computeNumClasses(start, end); - k1 = metrics.computeNumClasses(start, cut); - k2 = metrics.computeNumClasses(cut, end); - ent = metrics.entropy(start, end); - ent1 = metrics.entropy(start, cut); - ent2 = metrics.entropy(cut, end); - ig = metrics.informationGain(start, cut, end); - delta = log2(pow(3, float(k)) - 2) - (float(k) * ent - float(k1) * ent1 - float(k2) * ent2); - float term = 1 / N * (log2(N - 1) + delta); - return ig > term; - } - samples CPPFImdlp::getCutPointsx() - { - // Remove duplicates and sort - samples output(xCutPoints.size()); - set s; - unsigned size = xCutPoints.size(); - for (unsigned i = 0; i < size; i++) - s.insert(xCutPoints[i].value); - output.assign(s.begin(), s.end()); - sort(output.begin(), output.end()); - return output; - } - // Argsort from https://stackoverflow.com/questions/1577475/c-sorting-and-keeping-track-of-indexes - indices_t CPPFImdlp::sortIndices(samples& X_) - { - indices_t idx(X_.size()); - iota(idx.begin(), idx.end(), 0); - for (size_t i = 0; i < X_.size(); i++) - sort(idx.begin(), idx.end(), [&X_](size_t i1, size_t i2) - { return X_[i1] < X_[i2]; }); - return idx; - } -} diff --git a/fimdlp/ccFImdlp.h b/fimdlp/ccFImdlp.h deleted file mode 100644 index 00f3f9f..0000000 --- a/fimdlp/ccFImdlp.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef CCFIMDLP_H -#define CCFIMDLP_H -#include "typesFImdlp.h" -#include "ccMetrics.h" -#include -namespace mdlp { - class CPPFImdlp { - protected: - bool proposal; // proposed algorithm or original algorithm - int precision; - bool debug; - float divider; - indices_t indices; // sorted indices to use with X and y - samples X; - labels y; - Metrics metrics; - xcutPoints_t xCutPoints; - - static indices_t sortIndices(samples&); - void computeCutPoints(size_t, size_t); - long int getCandidate(size_t, size_t); - bool mdlp(size_t, size_t, size_t); - - public: - CPPFImdlp(); - CPPFImdlp(bool, int, bool debug = false); - ~CPPFImdlp(); - CPPFImdlp& fitx(samples&, labels&); - samples getCutPointsx(); - }; -} -#endif \ No newline at end of file diff --git a/fimdlp/ccMetrics.cc b/fimdlp/ccMetrics.cc deleted file mode 100644 index 06ddb9a..0000000 --- a/fimdlp/ccMetrics.cc +++ /dev/null @@ -1,74 +0,0 @@ -#include "ccMetrics.h" -#include -#include -using namespace std; -namespace mdlp { - Metrics::Metrics(labels& y_, indices_t& indices_): y(y_), indices(indices_), numClasses(computeNumClasses(0, indices.size())), entropyCache(cacheEnt_t()), igCache(cacheIg_t()) - { - } - int Metrics::computeNumClasses(size_t start, size_t end) - { - set nClasses; - for (auto i = start; i < end; ++i) { - nClasses.insert(y[indices[i]]); - } - return nClasses.size(); - } - void Metrics::setData(labels& y_, indices_t& indices_) - { - indices = indices_; - y = y_; - numClasses = computeNumClasses(0, indices.size()); - } - float Metrics::entropy(size_t start, size_t end) - { - float p, ventropy = 0; - int nElements = 0; - labels counts(numClasses + 1, 0); - if (end - start < 2) - return 0; - if (entropyCache.find(make_tuple(start, end)) != entropyCache.end()) { - return entropyCache[make_tuple(start, end)]; - } - for (auto i = &indices[start]; i != &indices[end]; ++i) { - counts[y[*i]]++; - nElements++; - } - for (auto count : counts) { - if (count > 0) { - p = (float)count / nElements; - ventropy -= p * log2(p); - } - } - entropyCache[make_tuple(start, end)] = ventropy; - return ventropy; - } - float Metrics::informationGain(size_t start, size_t cut, size_t end) - { - float iGain; - float entropyInterval, entropyLeft, entropyRight; - int nElementsLeft = cut - start, nElementsRight = end - cut; - int nElements = end - start; - if (igCache.find(make_tuple(start, cut, end)) != igCache.end()) { - cout << "**********Cache IG hit for " << start << " " << end << endl; - return igCache[make_tuple(start, cut, end)]; - } - entropyInterval = entropy(start, end); - entropyLeft = entropy(start, cut); - entropyRight = entropy(cut, end); - iGain = entropyInterval - ((float)nElementsLeft * entropyLeft + (float)nElementsRight * entropyRight) / nElements; - igCache[make_tuple(start, cut, end)] = iGain; - return iGain; - } - -} -/* - cache_t entropyCache; - std::map, double> c; - - // Set the value at index (3, 5) to 7.8. - c[std::make_tuple(3, 5)] = 7.8; - - // Print the value at index (3, 5). - std::cout << c[std::make_tuple(3, 5)] << std::endl; -*/ \ No newline at end of file diff --git a/fimdlp/ccMetrics.h b/fimdlp/ccMetrics.h deleted file mode 100644 index b4c5752..0000000 --- a/fimdlp/ccMetrics.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef CCMETRICS_H -#define CCMETRICS_H -#include "typesFImdlp.h" -#include -namespace mdlp { - class Metrics { - protected: - labels& y; - indices_t& indices; - int numClasses; - cacheEnt_t entropyCache; - cacheIg_t igCache; - public: - Metrics(labels&, indices_t&); - void setData(labels&, indices_t&); - int computeNumClasses(size_t, size_t); - float entropy(size_t, size_t); - float informationGain(size_t, size_t, size_t); - }; -} -#endif \ No newline at end of file diff --git a/fimdlp/cfimdlp.pyx b/fimdlp/cfimdlp.pyx index 1a04922..db87af1 100644 --- a/fimdlp/cfimdlp.pyx +++ b/fimdlp/cfimdlp.pyx @@ -3,16 +3,13 @@ from libcpp.vector cimport vector from libcpp cimport bool -cdef extern from "ccFImdlp.h" namespace "mdlp": - cdef struct CutPointBody: - size_t start, end; - int classNumber; - float fromValue, toValue; +cdef extern from "CPPFImdlp.h" namespace "mdlp": + ctypedef float precision_t cdef cppclass CPPFImdlp: CPPFImdlp() except + - CPPFImdlp(bool, int, bool) except + - CPPFImdlp& fitx(vector[float]&, vector[int]&) - vector[float] getCutPointsx() + CPPFImdlp(bool, bool) except + + CPPFImdlp& fit(vector[precision_t]&, vector[int]&) + vector[precision_t] getCutPoints() class PcutPoint_t: @@ -24,14 +21,14 @@ class PcutPoint_t: cdef class CFImdlp: cdef CPPFImdlp *thisptr - def __cinit__(self, precision=6, debug=False, proposal=True): + def __cinit__(self, debug=False, proposal=True): # Proposal or original algorithm - self.thisptr = new CPPFImdlp(proposal, precision, debug) + self.thisptr = new CPPFImdlp(proposal, debug) def __dealloc__(self): del self.thisptr def fit(self, X, y): - self.thisptr.fitx(X, y) + self.thisptr.fit(X, y) return self def get_cut_points(self): - return self.thisptr.getCutPointsx() + return self.thisptr.getCutPoints() \ No newline at end of file diff --git a/fimdlp/cppfimdlp.cpython-310-darwin.so b/fimdlp/cppfimdlp.cpython-310-darwin.so index 681ead17e268209d6f7e105a6c214bac2814572b..29955782f898db47ea4aaf4e183492b94b2079d9 100755 GIT binary patch delta 29813 zcmbV#30#!r+V}koGcbTSU?R8z1`6f^D()yFpo4U7GolFCyL(ZZ>YOQwAP>s}D%?f3n@_j~MK{?~Qg*L~gl@;pPn zV?pPS29WOGAIXjhDwCQCi?qoBW6f3` ztSB6!6=OA4jXWvaXt=})8q0+J6&{Tp8qZiLXnAn-=nx{;8u8~5K0#$mUi6U0!Za?> z%xe!s8W*+O@$KcQgTCC-DsuauwhPDgJWEAn!FY|8gP8q%r(GK=E}28@!29WtZ>nAiEslPXEPj>Fvoo}$Nj4@xGQ5^s(1&8jwqDpU8Kl3mA$?Q-O;fc0UD@0HUyaaHd$fWLy>HpD zOxkc79sVz>;s3{khZFu^>fsq=DBGf=v>~;r4HNRv3J%Y4-h`zM&K3^$kqX9`v6vE; z9YGP8!*gI0k^YPz(z-~OD3H~Lcw&$?JeNi^Cf2$GdZHVB0~ ziceW*%&ZO5f9PRv6YCgw#n~d;(?8oY#cC9HL^GAG9H85Kwhc>k+>sIM@QhA(+%bB9 zepkF%KOJozHHI2+BS?lJ{p@VixbR4o_CK!<`WgQ}fUVbKHLzgX77x z3D7zrR)4Q&yekUH9qyBkvedRE6gz8Vql9%Gj*}+i8fr>#+eR7h>41rI0^KRUV6n5*GWy9qs_C0==Nu3ay>~ZLi*@pYGSA zl1t)0yq~d@j~3dNhkc3|!pMsv?D)M16Mng0Kc4K=vh}_xZqxpC`tg)bvA=XeEri0< zY|l_$?b+@PRwH1YFz@NL`i+!WS8p?ejh|+_Z#c@vqPcU=WxGGgzVel$Y*Yi#jvRMk zxx+oFa$!$f42p}V+)*}5%NcmxR=feT466Azhpn6MH(NxOFUPZfM|RQ`$Jwlk20Zqo zG!6Y`3%3kJrX+E+uHC|NNAXE5yZCHFcJV1q-`(40v1WU62k4c(^Rzp4txtyQC5LDI zIfwfzhi6iy5gJQLr)9kaJ-HwMW}A8rk*uS(o+qgJJcZ@92cwaf!#%SCIpiL#4FS4k zE!w-H_-X?zt8@-^%sZds{w3S}yTko?j{BN18->qte^=sj-VQp5M%vAW23L;zsw3$< z8BlyxvpuvHwA$5}Umc~FZ6%2)Q?@6IY>st!Ce~kdc+B-M$-EJ^nYY1IOVt|2ay@TU zpzztAC-zY=@T--Pzo9<{W*46^=ejws_DkZFRq@eeHocMN>jVH^IL<1N8#h;a;j$6?Dj?_PR7 zS}gQA2A;G%^d8K1lnr?TSu$hVYGmv_26VRjuITQ=k< zTF9JRdU-<>W@v}|H_Yki%Enxz%yHMFZgSnvk>R=SstN?z?ouizYKYgg)#1Kk{JaWu zPgX2yD$$X2!g1o7DQDm{TS+fogSpmR&vTS?lxO0#tD`(Eu2Cg!LM6J)wA#5bLl0jKJ-mg-#4gmBLN9>+%>%Vc)QE4aI5;9Z5(&FuV5N* z(26w^Yu4F@RHVEXnxy{=zMAUbAF{D1?U`w!H> zxO2en+b%gFIS<~iwyBnj1P17#12avfPJP_K)A*#8sYTIcb~rpwRv?;_ zbUoYs6;-MJ)4-_kLNuIh6Afyb(*9VXM-S>{`e22gJt%Hm45*pZF|sPMQhv%xxs0j^ zHy@-3A$WW3nDTIMr3u znhKtE2=m1mEoX3Hg>y9q^o|_QA+j{bv#hdtHqHlf+;z}a*P-bQ5P&Qlr9NB9IE)j_ zhv;Y-9pL4j!Ys1tqOIq-`@(X{uz+|nu!#5yzu?A{WHhvBz$7IyF5ILq9UR-K6xljF zgR#h29c7cw8TNY#7gjpT*6%Z}>Msm#H+((TK3gcdM_n{Lvat>ZoF$1I_s_;LZ07hP zKhHl69xLXDIN5VLw0KE`oSTyP2Vj)5S{j>NlQHN!LYOzrXTY4 zmO1&RE&2Mr(_8B$Ic-dP^7SXCchV2%bTTc?*Dp*@G!4nuEi;nz9=Xx3nX_R|OcCrB zyQ|3)#4`3G=(LPsb?eIum6}Su%oF)!Au%o*`ye1!uWg9tU_^%^yj8K~jSCr8%KZz_ zXaS_tW9iPjt60zUQ@K-3pU>hZZt1I@i8IVPt=4Ljce2NH^!G~yY!3+uGmr2q^ViuIc4!!vldjK&pE?j zGgW!Yfo~@C(zO11t4U92Gxf?zD@>Or>+zFa+G2gr9iGU>0Kvw-*EldyPnjAOejiDBQX`>=wzqTisZ%@j zTnnP@SYag=8emfCvM1j^^4 z!4T#dOW_;Y`k`qPO@EBnTTY*2dSX0kwVUa{c+}_+ZJqx4^dxPxZk}O}ST?R%(;GAO z$uqiZ6ZPU5gIa0e!N!Wa5R|PyXVm5CAIyl>f^=iXc$Y7aQ!9;~6mg%&+>>}~Iyza` zlKKUu@_feb~&-rfXyMc{BTI%k*6{Cxo;eD^eXCqhFc%gtl6Lbk+kwk5(`?vv#@O zX?DEp{T#}4b+{45BVm7l=P16`;KXk7gtI&PX6zi2z#%5J3YBjhAI(^8KU!hkmiM^u z)dpKh0th*tgfFn9<(6J@hGZ9;07E-Lv1^QO%57 zYDW93`C&90D(v~3T}LAjv`X~BX(tAwv)_P1k-LjMa(ud(R(a6X2B580ArhA z#v2b1GhHxiB-1IFLB;|^>RPs<#zx~&TU{NcSI!xfy8_laJm?}gI(RJ3y+E)dDQDnS zTZv9(^;n(~%21+}IHVhGpo(wVYX2Don1#CHgCz2c@p}Kcov=@zJa?2fO@DfBt+fmz zF{jLOxBkey+g-IIG3%fiSPI(wE5iwhF1G0nPBA7qkeS=EYY%$DF_PjPDG^yX%F=$s zP)fOMd@zE`Y=8{sMhfoxf}1TgEK*#P;AX-GBVi%8Gan?dA=&AiLu)QiRRM}Mf0||I6|HK4Jm46ouqG7TRWMd*(SC)4Cam0&c z;kI)5)RUk&$~x{rz_*;A8%a{&9ug?FxLsgjCqT^;S9=#vg>&Fw%OjxMN;`nSnYNPA z#B+Ci=UD`$7i=XHfOFhw!-2DlgEU%Y?-bQEmW#l;A1#yz%3PMV8MBPR1i!!He+qt{ z6euKtnk>r05~BqEt`vCEE<6L4C-P4U%2FqjbiSyBO$7l&>sX8KVy{}RA70R|eK}Zl zBgEYUKgN1i;v{^g!T2mw|7Af3m!0@FBL~5)wb$Xn!43_C`IWX2C^8fqm1Gl6JGPP` zluz-xL}WH2(N?+)UA{Q&yQk4!&dv@`S{S!5b*M0O7y@H6(o$D@jud^FiR9F1@0y1r z#?Qh_&>GYgG6k=&6N-2S8d1ctoORK#gGD-Sr@HWTOy=s--qyccm}oo4LC1Rq?Ge4> zU5T!p9Hh2GaGZB~pHWZR-Ij+yvR7?Hr{ZQLnn+>le~_ILJSzkjk_xiaB1Bpe_C&_; zAgmk-ay*uuJP2zA;&GV9QhdfHByW$5dXXTdKLZ1DJYmNxD1FCD{o-AnT;UC=KLaxn1zah^J@#T2lvBNa_lrTKxRIrRgf#mTxih~*oyBeRNnd)MV9I^;I`Zh3k4~k6YmZe5RqmjXl zO)Eq?t*y`D3A3^QhfReGc~%=dtkEN^Uoa)!UQW zkxN;I()8s~HI_(-`o>Rat%hnItw$8QivJ=iqdzHF6BawyNcv%X-&6R*~!d&V0+bYicaFu42q88(Yq)*j7Jw!v@eg| zP0=JCT}{zWh#L2%%ZxUnF;G!SD6hjq7rm*=a}jlaj`31946(D8ZnB!~RW3?@5!*JS zw@SZ8r2hdMBvH}HjmgI#xlHpS(xmuGe74f1f1*5v@a$D6izuPTvJq$5TRrD)J#$&SZ49y8 zrPVM8IXLuH%LZz5^;ee-a9NN8tAjL<8F2tFc3^LY3bEf+$=85H#eO3YCdW$#-E&0Mk9=_ zze?Z6)L6{;PWr5Ik(i~f3q(`9>wkboJ9Jxy# zjyWC;b`2wD5ijmbsz4)>XqkBlROL^o@!>X(p+A7ln|8`2(tT4eGp z0y5>(fn>_+(cBE@c&dBnbmI+BQKingd|^r(Bhq*&z+ib+q{$OQZ~Ik#_1a)$Ag9`g zfb3PTz()Hq8!WHeK+8uWSiD*HQnR)LGA4*D3ZJxB9i&O*!WByR7EcIEspxgXsmD0) z7Mg0`;t~~*08dtSU0YtI3EvpqP;k_#dp}yyUiA#u6AnGMdea`gokM3wjn($)^9 zIqHx8;GsuicWs&D7@Dv`(?OUm7lUa_)=nKNk`X_h)iy z)cDl+fsq3$-2{~MpsMs(cHvaYEq@t}ndwPbt3Q5UQK|-NVT!%@RC@Nj-yJ8uGt+Lq zpsy5N#V1|MC0Lv3PWTrEo`f@x=y|J3lZFA+enb;|<8<&9rK8t0*sq1k>HD3&`ps3t z?|2Zb;|!~Xk=I8mpGj%mmUp%>cGhwTembh?tYxo$_v$XgQXya44idNCzlzsmnUtIk zF;0G!5=9WzdN*&5i`a#Ww}sWKI|UEK44>kgS$n^JW%Yp8w~2h6GpI@27jY`S-doRD zlc3Gg7q1x*n?P)jWeA;GJOi>l6XPA&PCSTNGvxxNN9TbPYhKi>(^Js7TN)E|=eohx zC;`ItgX;zdf1J!Y?kr;PD)@3Q4u0vSe|CRHn-_rW zo(GFi^14rRJsVc@l}YctJ|6eI`hPu-tH6zVGRC_Oe_w>JC*{_8k~XD{!TOM|D-WeFLI@t zKIpC=bd8BU0gL$UAsPzni{%3}yC|CvnHoLygrdA55fIy@)r}8{4QC zU0dkA1Cxhwx~smmnCmT0)o#;&E$-9hd*tj)xn)u;x94ws7eKrCT^L}7RUci_6YaaU zWYn-FT`*gD0<%4{aMR$^vfY6+ecnccEB9|=G9H%5MZj(#wrBl$OpG;nBWoC$dO&Hs z>j-q988*?yrB}qy*RDc4HD2RRqOUlLe@w*dWxN4H4Rz-Ak1?E8efgmmP#590(!B^$ zE;o3SY1wg~0Phf=x$Xwe@$^`Q!sob``Eop|_w#7sxg5{D@Z3@t7LLb|mM?WrK{&hk zPmT6`f0}HiZ-SBISs9z-xjQ;%aB2op;@pi`3dQ(&9iTf03n-((sWuMiyiNrQMuF_^ zGjtDGnhRR(WOyjStF-=lQdc~{TzDbhP(Fajx2!JBLHTQ6Q)a!52$^Lop~8}Bg)j+b zxv3N|jS7Z+%uDg4y zl|V_T@@s3rZfab(Fs`mYSsFl~)VHCJ!^N))%ny(mlW9L{EP@W+bT_E5xTtn#`S7YA z+nNiut@WT)YMHh->rto9md3@~g>dOk%5HFv$FY@jc1C0uMcN{=ks{&rmc)fj-6u;g z6t=2shjZ}Y9`}LevTX;+8DcypG!@P`UI|skN?3teKjkCj`sm+NE+e~i+oSjf121Zx zqcUe5%5ks8#A(^d#|QS-S=F{u8p*`-d((03OVnST<=9uE8sjklQH?hA4%gv!lwIKu zx9ZVWdK7DD?Q4Ad-{T~oR88^q7KMpM4k)+>InrO>_;6(Oa3rk#06nt! zdMKDNf_Y3ZBZ>J8XNG|JE46_qY@1*jabVKB;^OPUV4fDtrGj~cm=4agg1JL5hjC`q z7Gk#N%pfrD7R+LYLB!k6dFH|d z;}X?}C+v3Owf;O7)wQJfdH@)Q1>>5Y;%U>mk{Id6qo5UE$EZb$C`#dakQj8EbcMPC zUtU&WxEc3=Zp@%s`HF9KP8wqX>eiqsj7*9xr`RBhEudI3#b!{f3&qA!tOLbHQmhrl z(kN!7SRabrL^9(EqXrX2x5cHrkpl@1!3riwXS(B`geI z+tP^Zq<>v@zqVDMwWWvltiE|mqN#U`{^pjPekGldzN|{TUn#_k{a+1lRYLtAH7vsX~dl3^wI*F+45!`8Lc1DAJum2y zmt(FVtPdAzw`ov3*fI`^Z6)vE=qhriNxD`)zx|%5bYz02-Djk=;0vf|v_1OFM@vm1 z(fXxFZ#Q*~)`K7Gkz5%?#$Bs@7wYjrTUPl(hi${jV@1dawf82C;Igo7PB-(Zx;%%el_tj+jPD+N>z6_!Xb>8d&VBErxW>HJ`bKY5mqk*v3qLX;tsnDtjimm7$$C?54r)j>w*H5=&+Jt^A+D?OIvJ(H%Kb+i1(C1vAnXzXEZ}h% z^!7t0NMIS)Z^N{mhiqzhQ*S$lQv~|l3gjUwA9oD3C(#M)$DR7-9nr2!%{cE9r|P?A zv`x^E>$yLk7Qv4asm`#E7()AqX~LRtVNHgxrU*f!HNUokHFHEG{TWNPhkc3$IE=B% zjI_@i8iHVzC#-dMz1z-CQ6Hny>{S{%`Vlg_wk@}Ft3G>YJA8()c4tr1(AN43I~{Sq zqmT~I{~U5(5FPz`t$y3Cgy8uV=y45(t(9)yHA;I*-@I#JhZ8U;ci{KV0<7NW=!?R- zeQ1Arce*%2|9V%u@%hL-yEu@)(Y<1A8(g99ct?iAYG_PS4hot1{HG{7?-`D;=uB@{rFsc>*EP`_NFXQ zjA~3}joagi6L1Ui{yo%J(mq9y?ZK4m@a!l;dNOf2l-~wr##EXBk7;N_?6>=15}>Pu zj~cLWpEMj1did^0mjz7my}=X+BjMgy3c><06J)wCAEv%!w1Q13KJg*+?3vgv2t`I` znsLN!%ilykbybGjMppO=51^B@rJWsL>mGqxqX?|J$JOVcRGu99%{B`3^DjDiY+Zzi+`Jh05 zYk8|c1Nml#jz+Ix$ulazYF;3{M;G` zC+{&?!}LF%OiTMEjI$0Ee&F!rR-?Y|#P}8U^XG`mWK3bfRFSNPeiS zmpr|~G%G~E`gFhW$$aBL^_hzHG5)0weJ0hl6U0J0X}4jj##hKR2&)KR3qe^97)xnh z0E;d@3->^M1K${snoF4Yjaf9rHj7iXR+xxs=Prrr9BS(zD%7X(9TQX9u_<8#BI4I_Y9b ze0aG8eFt+%6Uul{Fb9o9RB-D1NWpuv{#PWpyD z?X*wyoqH16UZyEobosB1{lX5^=&BC-r+a24j-z5UzaSk75}Ae*vzi<4ui6c(0 zK)u)V9bCGFFN7vdOjYynx0HIQaxA8jDY!lt5RwMRiZ5(VZVn#bxoABmtiQsQiJB$VP% z%<4jpSSXG{!~C#Muw$Y=?Kaa=JrmQS3w@hTfKW?6a=^}^gl5)5$bxnbmyxh8J=-&r zpLTib0SnEoV;b$@jjQp_;Djj6Y*4`zuO zE7&nOx4O`Vfiktphl0j#l<7!&{lOOo&55PGG*-}ciAH-+an`FAF?;GiYna(hWpXn zf0+B5x&IjVcW{3f_jhyuN$yv0{{=ntrPPE|Jbs4z=eYk4_y58D_qkuKFL-Hi%8xw$ zGxzJc|10-@=l*5xU**234Hd?$fB4c!S3Hk*;eI0byK%oe_j__bnfs%-pTqqz+|T3w zc<#^V{zC3A;{FouFXjFQ?iX?Y0q&P@|G_ytxXS%Mxc?{jZ*rfZt2ZQ=xNqiuAos1@ zZ^iu%-0#HwSnhY`emwWPMB$fo9ky#N^w?J>>p#ET?m*Eiw`=;#`)BIml^NZvgF@%A ztSscxpf!sR4xP_NNIH+}_f}?f9Akad%qH)`fU4I*o;9<71jOtzvxm*WAnW>vm1kYQ zVPySg4!Itvm0PSo2WmSkKSTP#Aj{sMC{GZ&(o;OTCn$ljw}V0}TCk6UIx}`TDD<%w zY$M$fJZlZz+=9JA(Q0ex11;DEir%n>UJYfNf)RZoIP~XG_69{i4Gz5&%D$m!Nl57D zq0F^41o2lxLaRgB(GV=#7eYeMgtG4_S{fR9G?eWQZSh(t+ZSqitp$4{bma>zv^QE< z{@y}6+yb+K#!Tx2)|=N`)(5T+T5nw+ygp=o==v7xF;CEs!s_8(f;$BF1>6p}qi{aB z-8{s||Kh;T*ZNx9EStZyV0NE5v&j{#STJw#%Eim?nSJ2H!)`6=uX#@W)wgatu;S=D znkypIJT8uzv^iRUhPa0KxQbKw!4lpKT!Jxo!l_@85+*wRBs8`=V;vAyB1}Y>-h;8e z2n%{5j<5!2mwbfbxY4^GVP!63dk~h7W9)T=z6p$-s>F?OS}N9gk~w)8b5+zI*t{NS?!zs5v}^0O8!gGFm-btEl^i`AI95-#1uH2OVgjR`a} z)AGzLpgNG5idwROns8<+Xw58@t(mE!y^Cqd9a&&`EHlx5GPX0b7IbD>MQ3J;jfYVQ zOk-VGKv7pH?8+?3i7Wsm4?rn{i+VB>OGcVx;1m|%>&;Bneb97$m?=G%X+8`=PSBRC)BK3&VQ4tO)@h?dY(dV3w3BD*1e!+V>O0PI=4X&4z z+c>2jl)T0iubz(40`#~uF~N(ZHaGH%(bLaZwP5{T&{s=(zP=NvNTM)(Bl6lLQIuYL zrmKeE{GRQq#p~nF#ppFi-$S2sHYRw7l-Z!W!QH06dNxKcI%^G1-!ID2M}PWERKz|h zwNg@BkgdU$lIr3*W6xQGcS*~`^ptZk+9tAGF93a|q=)Jo&&32+OD(N2Ytz1lLQ{THkgy+A4)#;nkBY(d%SN2n!L7Q?*|YIUQq_ z==6b|l*Alh7N0YR%t%_z-G0{;ez!N+7(TBB8!DfsdYTfilU)6eP_D8?g zqO@vnoThaR3SUF4JWR`RLCHK`fOzNVbRsu-)>1s2<$K+lHdZV0ex_*;MTmL`XR$8g z7BCG-dubcIPn)!{5ep8AB*~l^&WgO4KB6Mryzau8ANd|B?UT~X`!q>!lhS@<(J(Nl zNaRt#rGr;XCDq;&0a{GNMhWYsRI>L!q|@`4WNo|mL$lUZdzuHqFDrYzbIe*y@Zsk0 zjD$6UTipysd?q1#Kv-AgO|c-~@Bf5Y(%3OFPnD-mHj4=Yi_1Gj6@+BB~g zs6|7&O4#?8r-y;$^EOoM(;zT}I6_doe+09}=G1u|6A0z0_Ts(Jcb zzCd?>;$TF&)a9-9fqrcGl#O9UEoH+&Buy=vj~yhaL3@V1-SV; zTyzU|b12+gxGivp;eLnf9>#7?hg%J|7w!wVRxR1hesF8y_QKV`F&n$t4sHnCoi=8= z-^R4va2>*#wghf3+~;sDBCuACh+x|5a9vt4Z4X?B+nDws+&;MTa06O1Z9d#5aM&no z$#C~XvK!b0-|)d<<9Z{y4ZD#E=Y-qa2Fl^E6V$My(=y=-qS%c+aIZ%()8BzVhO|n%JZEnE9DI>J&R_~0l+QLNa zd}gyyGpnJy1ykOd?0XhFXU{7rSh$$BD$FvQ?X;L#<$mw?W3@i5%b$f*KnhFh&k8cw zyPBE#p7-|5(ezMA=rSyI^dralW7JH0<1)w+0lZP8jSJ7Mli z86)8`cE+5+YRsAJK}!IuzY~e-3*h;tBZIkf*}LZZ*adR{tC&i~$zs*!-dXHFAXH8h z0zX3FPvG(?lpi)_kh+g;wDg9*9V)A5c=N|=cl1Av$}8U{2YmUIa!vgU_nDH5EcMok zfQo<`b9qpDeOP%wO<++#cwTZ*fcM0Bt-V(1{c^mP3D@JC8qy+Ytc?U;*~sCu;^R7e4_mtgE)5{vx=WArMF3}$PE`~->f*9q*F zxQI?Rc;1q@LgJq!j^%p;#(H!>gcOQB1Y>t~kR^};FGwtQ4UGL;VphcUv!sqfK40QR z5*J8ZDRH&LbrOq>1Y=!XxYy(cY!nKYNL(rLK8bx2eC|n|O>{9}(VN*~cFdsIMFOpb|nrexkliLSA)h-!lUH zB)^(t7Y?41;CogGMBOe7N`Fq^sS?N1Cqj5uNt`b+ecnO^sF0Z6j1sPq_!Ghbm^b(i zl(DN4KTjC>vzGBfV80Ydlvo^_aFHglI506bM`CegVvN2Mp$x=+`M1m}l#OV@?QxId5C3Z>vGKn`yyiwvU`$hh2 zrzGr_0xwA{jya5-l=v0N|5W1FCH_I;Dv5E5<~4F!V)`7Yr5VQ#etcmpPT~%gy!`0J zl0cUW^khj)mkjhwk(e$U=vgc=T{_USPGY)zpl6H3dn8sF(q#laPfPwOmn0mJ1iGA{ z=d{H9k^=a=#B^CfPo2bcX+h6TiRto!o`|lZ#B_;4PrSr*nL$r~!q{rN)L+3GLj=4G zq01tA=1W{4F)k%Ize3_FiT6qTABoRN9D&Uj>B~0>eYngJ=_MX3u#448!aY*JpkL47 zc|_tO{9!A4-jX=hDzG_8C`^}llEnEEzd$$?!&2g768j{6mt(X)j@dkeZ>2!B#MdQ0 zFL7iyp|D2c&Jr6E_ma3?;!zS`m3X$qj2}Mm=^SDDjg~mEV;PnL;ZooQiK8WcPvTgK zFG-vz@imE)C2o0#FnEB(NfM_^YzKBx6*(kfh7`z?c$vggC8p~*lFyfTuf&TaJ}j{~ zUNQE*#H%I$SBZ-xZqXg%pA>GAgaq905iXZ_n8Z6Io-A>N#G54ECvkn|Pk8Dt2717!FKZMgK2{f24o45+ zF@AhXPdC7Ly0K@a0!iQr{z^ftH*U5T)t0_$q5SCPm>$y4#l7QZ<6?&{lIi(ailuwC zd@UwUkx80zua;szNwI1nrpQVxAC*IoC0S&e@3oUQMOI>TYfVo_DOTX!NMeet#MD7% zsT8a9o**$rR$_GbO^-*4)p&8e9y3Rgl~{gnA@-57O=99xg%p?tpD;Cml2_@g`w9Ma znZDY)5!u8kvJxv8AjD!*L=_ZxPmnc=ti$5>rRa%)TNUg^5qqSws;$6i_xz z`IL=1lA2OQ`U>v}WaG`BuZ2gb(`mNkReH7gsA@$fm83!)T9->Pg^5Kbpxf|=i#Cy` zD5Y12+=pd)g^B+cMd;j^Kq;skD>87Pqw*2&Jpnu86j_O>!}3-sh8kT!H7ZjpF?EK% zAjK3`=7fvP8f&yEpNbRvf)ME3Usx0CJ%McE6j^1HFU9OqEMJHzvJ$J1V);@G?L*oW zS&7w1u_7sksbC?RN|7r-?qa^R3w}*vdb38)DT&o4_!EiMCfJZzZG!1HfTT}tf?Hth z6IPqx7=cY}k9Xt3zgRnZgFjzn&HbUkw01n`w|1nD6GD%IPc5r36+0d-Pp})?qA8!+ zqFkg8VKq`gxi{`EEha_Mc{-8{pCA-j2a0?YZp_E~Dw0LyOELOcE18q;eH!FAMJ8#| z=96N3q?ph9HHj&*5~DYg^qi1lmEO2TG`u8PiK#mrLy9R(e5wQg{@9ey%Ql@d!GnpH zj8=C*Dt!?}@K7*DW1X7`kOFl_bWAEJ_kImK;}n_Fk(jzVvJ4jaR0uIeR$}T#DMyM` zdgm<0C{$!6Mz3e-d0C1nOnlz*6v0CQtmdzC6((!c#Z=r7VNIU*YsyBE?P=(+O-|uCN^QD-=#HTu}lwF4~)nQXUWuq?Z zw#f8Y_>oPVB2zLhwi*#U`y0hhkTvRNPl>6UJ^Dc$?}uwdGUAgpu>v->sFGLd)lFcB zVIqC9*S-|lD6$e$7ldP_n8GTXe2UrtXhg)kz&=0WIGj8qos9H(R?CNIZX?-8Op!o3?IHb;?_n7T4wCB+mbemq>V2_7E8WKB~(Wuvax zUzO=&y_yrXuE>;(#0n}9!Sin^rZDj-8~=H!DWB38RX_+&_mQGB3aj*ag00M)+n9g^ zNI`XlkQgr&D6AADH&@UouhOem0n22146s6!Q<0UJdPA^PiYctJ@h@jnJ~#6jVaF%R zOo>(c`sQZNW#Sb?lkWMfkoZFxAhA+VL=ij`jDe@#SP&)y)EkQ)4pGqvJKQO;dhf7YV)fqPA&JE|Sd6_Xv3l?D4~f-#hi^H?I;LJcT#*8J@xUM3C}FXB z@6cId_17A&*YqAKpswj(lvrKUzoQh$Tlrdv z)h)d>M`WOG>FH0bl74kdKSAPHdFlOR4s&sVbV>L{3gkzutqBQLJFvB`&xI9cLz5)Y90TZz*pzAmvt;z(RwPyzEK?ke$A ziCv>4Azu>mC0-=4Q{n=NH%Yu&;+G^YlK70on4zwWmMIMOfY1KOr%{wWodbK}k^8_U}lnuI(>Mtgh{Umsnlf_Z%-W zP}la$Bv!Zf4@#_V?Vpl(DsJ(;nfGfQTC)e3CI#xkITo81!sW=<7iPZ~%Dv8yN|0f)y5!5S(pQV7hmZzV!P=@L@zZ2mQ$jb?1C@|PJMfQx1^TSL1 z@M=HoCXD&t_Vfr0d(*ln0!>)Wk!g4>n&JS<$!(KnU z#}B{ehfg%eF7`ol0{hwz)8A`oT7saU=D3X?PVmF{KDSx={ARchW6PTnXv@+(!A?K? zlGnWvyXVjS$UpjFGqz_et4}PF89MP`{61-{Ei>Ka0~PN@k7(*7G4(^E17sg zvH3s=@Sk`_`{5OSxZDrF;D=8(Vv#?qX(WgYqFOZ1FwYNf^~3M_VHVareS{xQQds8C zrYV9f;c`E`&kvvS!~gQbzx!cpOERdau?9Oz?5!x(tgb$O+-yHQ+Yhhu!{vVX1&(jE z*AYMbUUTeX7n>8yn&0>Ie2V~_C}N6EzCPu_qg_0;K7<_w5u(&6z}2vTAVi5dvw3nAs{7d z?!hniYpXSH04{W`^#A|> delta 29920 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 diff --git a/fimdlp/m2.cpp b/fimdlp/m2.cpp deleted file mode 100644 index 73dadda..0000000 --- a/fimdlp/m2.cpp +++ /dev/null @@ -1,36 +0,0 @@ - -#include - -using namespace std; -struct CutPointBody { - size_t start, end; // indices of the sorted vector - int classNumber; // class assigned to the cut point - float fromValue, toValue; -}; -typedef CutPointBody cutPoint_t; -typedef vector samples; -typedef vector labels; -typedef vector indices_t; -typedef vector cutPoints_t; -//typedef std::map, float> cache_t; -struct cutPointStruct { - size_t index; - float value; -}; -typedef cutPointStruct xcutPoint_t; -typedef vector xcutPoints_t; -class Metrics { -private: - labels& y; - indices_t& indices; - int numClasses; -public: - Metrics(labels&, indices_t&); - int computeNumClasses(size_t, size_t); - float entropy(size_t, size_t); - float informationGain(size_t, size_t, size_t); -}; -Metrics::Metrics(labels& y_, indices_t& indices_) : y(y_), indices(indices_) -{ - numClasses = computeNumClasses(0, indices.size()); -} \ No newline at end of file diff --git a/fimdlp/main b/fimdlp/main deleted file mode 100755 index 5e9e630cced5788d2004e1c2f25be7dd724b98ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 239361 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 -#include -#include -#include -#include -using namespace std; -using namespace mdlp; - -int main() -{ - ifstream fin("kdd_JapaneseVowels.arff"); - if (!fin.is_open()) { - cout << "Error opening file" << endl; - return 1; - } - - int count = 0; - - // Read the Data from the file - // as String Vector - size_t col; - vector row; - string line, word; - vector> dataset = vector>(15, vector()); - while (getline(fin, line)) { - if (count++ > 215) { - stringstream ss(line); - col = 0; - while (getline(ss, word, ',')) { - col = col % 15; - dataset[col].push_back(stof(word)); - cout << col << "-" << word << " "; - col++; - } - cout << endl; - } - } - labels y = labels(dataset[0].begin(), dataset[0].end()); - cout << "Column 0 (y): " << y.size() << endl; - for (auto item : y) { - cout << item << " "; - } - CPPFImdlp test = CPPFImdlp(false, 6, true); - test.fit(dataset[3], y); - cout << "Cut points: " << test.getCutPoints().size() << endl; - for (auto item : test.getCutPoints()) { - cout << item << " "; - } - fin.close(); - return 0; -} \ No newline at end of file diff --git a/fimdlp/mdlp.py b/fimdlp/mdlp.py index 2d3f610..50e5ca7 100644 --- a/fimdlp/mdlp.py +++ b/fimdlp/mdlp.py @@ -1,6 +1,5 @@ import numpy as np from .cppfimdlp import CFImdlp -from .pyfimdlp import PyFImdlp from sklearn.base import BaseEstimator, TransformerMixin from sklearn.utils.multiclass import unique_labels from sklearn.utils.validation import check_X_y, check_array, check_is_fitted diff --git a/fimdlp/pyfimdlp.py b/fimdlp/pyfimdlp.py deleted file mode 100644 index 91c01c7..0000000 --- a/fimdlp/pyfimdlp.py +++ /dev/null @@ -1,479 +0,0 @@ -import numpy as np -from math import log2 -from types import SimpleNamespace - - -class PyFImdlp: - def __init__(self, proposal=True, debug=False): - self.proposal = proposal - self.n_features_ = None - self.X_ = None - self.y_ = None - self.debug = debug - self.features_ = None - self.cut_points_ = [] - self.entropy_cache = {} - self.information_gain_cache = {} - - def fit(self, X, y): - self.n_features_ = len(X) - self.indices_ = np.argsort(X) - self.use_indices = False - X = [ - 4.3, - 4.4, - 4.4, - 4.4, - 4.5, - 4.6, - 4.6, - 4.6, - 4.6, - 4.7, - 4.7, - 4.8, - 4.8, - 4.8, - 4.8, - 4.8, - 4.9, - 4.9, - 4.9, - 4.9, - 4.9, - 4.9, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5.1, - 5.1, - 5.1, - 5.1, - 5.1, - 5.1, - 5.1, - 5.1, - 5.1, - 5.2, - 5.2, - 5.2, - 5.2, - 5.3, - 5.4, - 5.4, - 5.4, - 5.4, - 5.4, - 5.4, - 5.5, - 5.5, - 5.5, - 5.5, - 5.5, - 5.5, - 5.5, - 5.6, - 5.6, - 5.6, - 5.6, - 5.6, - 5.6, - 5.7, - 5.7, - 5.7, - 5.7, - 5.7, - 5.7, - 5.7, - 5.7, - 5.8, - 5.8, - 5.8, - 5.8, - 5.8, - 5.8, - 5.8, - 5.9, - 5.9, - 5.9, - 6, - 6, - 6, - 6, - 6, - 6, - 6.1, - 6.1, - 6.1, - 6.1, - 6.1, - 6.1, - 6.2, - 6.2, - 6.2, - 6.2, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.4, - 6.4, - 6.4, - 6.4, - 6.4, - 6.4, - 6.4, - 6.5, - 6.5, - 6.5, - 6.5, - 6.5, - 6.6, - 6.6, - 6.7, - 6.7, - 6.7, - 6.7, - 6.7, - 6.7, - 6.7, - 6.7, - 6.8, - 6.8, - 6.8, - 6.9, - 6.9, - 6.9, - 6.9, - 7, - 7.1, - 7.2, - 7.2, - 7.2, - 7.3, - 7.4, - 7.6, - 7.7, - 7.7, - 7.7, - 7.7, - 7.9, - ] - y = [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 2, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 1, - 1, - 1, - 1, - 1, - 0, - 1, - 1, - 2, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 1, - 2, - 0, - 1, - 1, - 2, - 0, - 1, - 2, - 1, - 2, - 2, - 1, - 1, - 2, - 1, - 1, - 1, - 2, - 1, - 2, - 2, - 1, - 1, - 1, - 1, - 2, - 2, - 1, - 1, - 2, - 2, - 1, - 2, - 2, - 1, - 2, - 1, - 2, - 2, - 1, - 2, - 2, - 2, - 1, - 2, - 2, - 2, - 1, - 2, - 2, - 1, - 1, - 2, - 2, - 2, - 2, - 2, - 1, - 1, - 1, - 2, - 2, - 1, - 2, - 1, - 2, - 2, - 1, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - ] - # self.X_ = X[self.indices_] if not self.use_indices else X - # self.y_ = y[self.indices_] if not self.use_indices else y - self.X_ = X - self.y_ = y - self.compute_cut_points(0, len(y)) - return self - - def get_cut_points(self): - return sorted(list(set([cut.value for cut in self.cut_points_]))) - - def compute_cut_points(self, start, end): - # print((start, end)) - cut = self.get_candidate(start, end) - if cut.value is None: - return - print("cut: ", cut.value, " index: ", cut.index) - if self.mdlp(cut, start, end): - print("¡Ding!", cut.value, cut.index) - self.cut_points_.append(cut) - self.compute_cut_points(start, cut.index) - self.compute_cut_points(cut.index, end) - - def mdlp(self, cut, start, end): - N = end - start - k = self.num_classes(start, end) - k1 = self.num_classes(start, cut.index) - k2 = self.num_classes(cut.index, end) - ent = self.entropy(start, end) - ent1 = self.entropy(start, cut.index) - ent2 = self.entropy(cut.index, end) - ig = self.information_gain(start, cut.index, end) - delta = log2(pow(3, k) - 2, 2) - ( - float(k) * ent - float(k1) * ent1 - float(k2) * ent2 - ) - term = 1 / N * (log2(N - 1, 2) + delta) - print("start: ", start, " cut: ", cut.index, " end: ", end) - print( - "k=", - k, - " k1=", - k1, - " k2=", - k2, - " ent=", - ent, - " ent1=", - ent1, - " ent2=", - ent2, - ) - print("ig=", ig, " delta=", delta, " N ", N, " term ", term) - return ig > term - - def num_classes(self, start, end): - n_classes = set() - for i in range(start, end): - n_classes.add( - self.y_[self.indices_[i]] if self.use_indices else self.y_[i] - ) - return len(n_classes) - - def get_candidate(self, start, end): - """Return the best cutpoint candidate for the given range. - - Parameters - ---------- - start : int - Start of the range. - end : int - End of the range. - - Returns - ------- - candidate : SimpleNamespace with attributes index and value - value == None if no candidate is found. - """ - candidate = SimpleNamespace() - candidate.value = None - minEntropy = float("inf") - for idx in range(start + 1, end): - condition = ( - self.y_[self.indices_[idx]] == self.y_[self.indices_[idx - 1]] - if self.use_indices - else self.y_[idx] == self.y_[idx - 1] - ) - if condition: - continue - entropy_left = self.entropy(start, idx) - entropy_right = self.entropy(idx, end) - entropy_cut = entropy_left + entropy_right - print( - "idx: ", - idx, - " entropy_left: ", - entropy_left, - " entropy_right : ", - entropy_right, - " -> ", - start, - " ", - end, - ) - if entropy_cut < minEntropy: - minEntropy = entropy_cut - candidate.index = idx - if self.use_indices: - candidate.value = ( - self.X_[self.indices_[idx]] - + self.X_[self.indices_[idx - 1]] - ) / 2 - else: - candidate.value = (self.X_[idx] + self.X_[idx - 1]) / 2 - return candidate - - def entropy(self, start, end) -> float: - n_labels = end - start - if n_labels <= 1: - return 0 - if (start, end) in self.entropy_cache: - return self.entropy_cache[(start, end)] - if self.use_indices: - counts = np.bincount(self.y_[self.indices_[start:end]]) - else: - counts = np.bincount(self.y_[start:end]) - proportions = counts / n_labels - n_classes = np.count_nonzero(proportions) - if n_classes <= 1: - return 0 - entropy = 0.0 - # Compute standard entropy. - for prop in proportions: - if prop != 0.0: - entropy -= prop * log2(prop, 2) - self.entropy_cache[(start, end)] = entropy - return entropy - - def information_gain(self, start, cut, end): - if (start, cut, end) in self.information_gain_cache: - return self.information_gain_cache[(start, cut, end)] - labels = end - start - if labels == 0: - return 0.0 - entropy = self.entropy(start, end) - card_left = cut - start - entropy_left = self.entropy(start, cut) - card_right = end - cut - entropy_right = self.entropy(cut, end) - result = ( - entropy - - (card_left / labels) * entropy_left - - (card_right / labels) * entropy_right - ) - self.information_gain_cache[(start, cut, end)] = result - return result diff --git a/fimdlp/testcpp/FImdlp_unittest.cc b/fimdlp/testcpp/FImdlp_unittest.cc index df90f23..3bdc69d 100644 --- a/fimdlp/testcpp/FImdlp_unittest.cc +++ b/fimdlp/testcpp/FImdlp_unittest.cc @@ -34,7 +34,7 @@ namespace mdlp { X = X_; indices = indices_; indices_t testSortedIndices = sortIndices(X); - float prev = X[testSortedIndices[0]]; + precision_t prev = X[testSortedIndices[0]]; for (auto i = 0; i < X.size(); ++i) { EXPECT_EQ(testSortedIndices[i], indices[i]); EXPECT_LE(prev, X[testSortedIndices[i]]); @@ -162,7 +162,7 @@ namespace mdlp { fit(X, y); computeCutPointsOriginal(); cutPoints_t expected; - vector computed = getCutPoints(); + vector computed = getCutPoints(); expected = { { 0, 4, -1, -3.4028234663852886e+38, 5.15 }, { 4, 6, -1, 5.15, 5.45 }, { 6, 10, -1, 5.45, 3.4028234663852886e+38 } diff --git a/fimdlp/testcpp/Metrics_unittest.cc b/fimdlp/testcpp/Metrics_unittest.cc index c04ec0f..0bea1c1 100644 --- a/fimdlp/testcpp/Metrics_unittest.cc +++ b/fimdlp/testcpp/Metrics_unittest.cc @@ -2,7 +2,7 @@ #include "../Metrics.h" namespace mdlp { - float precision = 0.000001; + precision_t precision = 0.000001; TEST(MetricTest, NumClasses) { labels y = { 1, 1, 1, 1, 1, 1, 1, 1, 2, 1 }; diff --git a/fimdlp/tests/bak/CPPFImdlp.cpp b/fimdlp/tests/bak/CPPFImdlp.cpp new file mode 100644 index 0000000..7f35562 --- /dev/null +++ b/fimdlp/tests/bak/CPPFImdlp.cpp @@ -0,0 +1,286 @@ +#include "CPPFImdlp.h" +#include +#include +#include +#include "Metrics.h" + +namespace mdlp { + ostream& operator << (ostream& os, const cutPoint_t& cut) + { + os << cut.classNumber << " -> (" << cut.start << ", " << cut.end << + ") - (" << cut.fromValue << ", " << cut.toValue << ") " + << endl; + return os; + + } + CPPFImdlp::CPPFImdlp(): proposal(true), precision(6), debug(false) + { + divider = pow(10, precision); + numClasses = 0; + } + CPPFImdlp::CPPFImdlp(bool proposal, int precision, bool debug): proposal(proposal), precision(precision), debug(debug) + { + divider = pow(10, precision); + numClasses = 0; + } + CPPFImdlp::~CPPFImdlp() + = default; + samples CPPFImdlp::getCutPoints() + { + samples output(cutPoints.size()); + ::transform(cutPoints.begin(), cutPoints.end(), output.begin(), + [](cutPoint_t cut) { return cut.toValue; }); + return output; + } + labels CPPFImdlp::getDiscretizedValues() + { + return xDiscretized; + } + CPPFImdlp& CPPFImdlp::fit(samples& X_, labels& y_) + { + X = X_; + y = y_; + if (X.size() != y.size()) { + throw invalid_argument("X and y must have the same size"); + } + if (X.size() == 0 || y.size() == 0) { + throw invalid_argument("X and y must have at least one element"); + } + indices = sortIndices(X_); + xDiscretized = labels(X.size(), -1); + numClasses = Metrics::numClasses(y, indices, 0, X.size()); + + if (proposal) { + computeCutPointsProposal(); + } else { + computeCutPointsOriginal(); + } + filterCutPoints(); + // Apply cut points to the input vector + for (auto cut : cutPoints) { + for (size_t i = cut.start; i < cut.end; i++) { + xDiscretized[indices[i]] = cut.classNumber; + } + } + return *this; + } + bool CPPFImdlp::evaluateCutPoint(cutPoint_t rest, cutPoint_t candidate) + { + int k, k1, k2; + precision_t ig, delta; + precision_t ent, ent1, ent2; + auto N = precision_t(rest.end - rest.start); + if (N < 2) { + return false; + } + k = Metrics::numClasses(y, indices, rest.start, rest.end); + k1 = Metrics::numClasses(y, indices, rest.start, candidate.end); + k2 = Metrics::numClasses(y, indices, candidate.end, rest.end); + ent = Metrics::entropy(y, indices, rest.start, rest.end, numClasses); + ent1 = Metrics::entropy(y, indices, rest.start, candidate.end, numClasses); + ent2 = Metrics::entropy(y, indices, candidate.end, rest.end, numClasses); + ig = Metrics::informationGain(y, indices, rest.start, rest.end, candidate.end, numClasses); + delta = log2(pow(3, precision_t(k)) - 2) - (precision_t(k) * ent - precision_t(k1) * ent1 - precision_t(k2) * ent2); + precision_t term = 1 / N * (log2(N - 1) + delta); + if (debug) { + cout << "Rest: " << rest; + cout << "Candidate: " << candidate; + cout << "k=" << k << " k1=" << k1 << " k2=" << k2 << " ent=" << ent << " ent1=" << ent1 << " ent2=" << ent2 << endl; + cout << "ig=" << ig << " delta=" << delta << " N " << N << " term " << term << endl; + } + return (ig > term); + } + void CPPFImdlp::filterCutPoints() + { + cutPoints_t filtered; + cutPoint_t rest, item; + int classNumber = 0; + + rest.start = 0; + rest.end = X.size(); + rest.fromValue = numeric_limits::lowest(); + rest.toValue = numeric_limits::max(); + rest.classNumber = classNumber; + bool first = true; + for (size_t index = 0; index < size_t(cutPoints.size()); index++) { + item = cutPoints[index]; + if (evaluateCutPoint(rest, item)) { + if (debug) + cout << "Accepted: " << item << endl; + //Assign class number to the interval (cutpoint) + item.classNumber = classNumber++; + filtered.push_back(item); + first = false; + rest.start = item.end; + } else { + if (debug) + cout << "Rejected: " << item << endl; + if (index != size_t(cutPoints.size()) - 1) { + // Try to merge the rejected cutpoint with the next one + if (first) { + cutPoints[index + 1].fromValue = numeric_limits::lowest(); + cutPoints[index + 1].start = indices[0]; + } else { + cutPoints[index + 1].fromValue = item.fromValue; + cutPoints[index + 1].start = item.start; + } + } + } + } + if (!first) { + filtered.back().toValue = numeric_limits::max(); + filtered.back().end = X.size() - 1; + } else { + filtered.push_back(rest); + } + cutPoints = filtered; + } + void CPPFImdlp::computeCutPointsProposal() + { + cutPoints_t cutPts; + cutPoint_t cutPoint; + precision_t xPrev, xCur, xPivot; + int yPrev, yCur, yPivot; + size_t idx, numElements, start; + + xCur = xPrev = X[indices[0]]; + yCur = yPrev = y[indices[0]]; + numElements = indices.size() - 1; + idx = start = 0; + bool firstCutPoint = true; + if (debug) + printf("*idx=%lu -> (-1, -1) Prev(%3.1f, %d) Elementos: %lu\n", idx, xCur, yCur, numElements); + while (idx < numElements) { + xPivot = xCur; + yPivot = yCur; + if (debug) + printf(" Prev(%3.1f, %d) Pivot(%3.1f, %d) Cur(%3.1f, %d) \n", idx, xPrev, yPrev, xPivot, yPivot, xCur, yCur); + // Read the same values and check class changes + do { + idx++; + xCur = X[indices[idx]]; + yCur = y[indices[idx]]; + if (yCur != yPivot && xCur == xPivot) { + yPivot = -1; + } + if (debug) + printf(">idx=%lu -> Prev(%3.1f, %d) Pivot(%3.1f, %d) Cur(%3.1f, %d) \n", idx, xPrev, yPrev, xPivot, yPivot, xCur, yCur); + } + while (idx < numElements && xCur == xPivot); + // Check if the class changed and there are more than 1 element + if ((idx - start > 1) && (yPivot == -1 || yPrev != yCur) && goodCut(start, idx, numElements + 1)) { + // Must we add the entropy criteria here? + // if (totalEntropy - (entropyLeft + entropyRight) > 0) { Accept cut point } + cutPoint.start = start; + cutPoint.end = idx; + start = idx; + cutPoint.fromValue = firstCutPoint ? numeric_limits::lowest() : cutPts.back().toValue; + cutPoint.toValue = (xPrev + xCur) / 2; + cutPoint.classNumber = -1; + firstCutPoint = false; + if (debug) { + printf("Cutpoint idx=%lu Cur(%3.1f, %d) Prev(%3.1f, %d) Pivot(%3.1f, %d) = (%3.1g, %3.1g] \n", idx, xCur, yCur, xPrev, yPrev, xPivot, yPivot, cutPoint.fromValue, cutPoint.toValue); + } + cutPts.push_back(cutPoint); + } + yPrev = yPivot; + xPrev = xPivot; + } + if (idx == numElements) { + cutPoint.start = start; + cutPoint.end = numElements + 1; + cutPoint.fromValue = firstCutPoint ? numeric_limits::lowest() : cutPts.back().toValue; + cutPoint.toValue = numeric_limits::max(); + cutPoint.classNumber = -1; + if (debug) + printf("Final Cutpoint idx=%lu Cur(%3.1f, %d) Prev(%3.1f, %d) Pivot(%3.1f, %d) = (%3.1g, %3.1g] \n", idx, xCur, yCur, xPrev, yPrev, xPivot, yPivot, cutPoint.fromValue, cutPoint.toValue); + cutPts.push_back(cutPoint); + } + if (debug) { + cout << "Entropy of the dataset: " << Metrics::entropy(y, indices, 0, numElements + 1, numClasses) << endl; + for (auto cutPt : cutPts) + cout << "Entropy: " << Metrics::entropy(y, indices, cutPt.start, cutPt.end, numClasses) << " :Proposal: Cut point: " << cutPt; + } + cutPoints = cutPts; + } + void CPPFImdlp::computeCutPointsOriginal() + { + cutPoints_t cutPts; + cutPoint_t cutPoint; + precision_t xPrev; + int yPrev; + bool first = true; + // idxPrev is the index of the init instance of the cutPoint + size_t index, idxPrev = 0, last, idx = indices[0]; + xPrev = X[idx]; + yPrev = y[idx]; + last = indices.size() - 1; + for (index = 0; index < last; index++) { + idx = indices[index]; + // Definition 2 Cut points are always on class boundaries && + // there are more than 1 items in the interval + // if (entropy of interval) > (entropyLeft + entropyRight)) { Accept cut point } (goodCut) + if (y[idx] != yPrev && xPrev < X[idx] && idxPrev != index - 1 && goodCut(idxPrev, idx, last + 1)) { + // Must we add the entropy criteria here? + if (first) { + first = false; + cutPoint.fromValue = numeric_limits::lowest(); + } else { + cutPoint.fromValue = cutPts.back().toValue; + } + cutPoint.start = idxPrev; + cutPoint.end = index; + cutPoint.classNumber = -1; + cutPoint.toValue = round(divider * (X[idx] + xPrev) / 2) / divider; + idxPrev = index; + cutPts.push_back(cutPoint); + } + xPrev = X[idx]; + yPrev = y[idx]; + } + if (first) { + cutPoint.start = 0; + cutPoint.classNumber = -1; + cutPoint.fromValue = numeric_limits::lowest(); + cutPoint.toValue = numeric_limits::max(); + cutPts.push_back(cutPoint); + } else + cutPts.back().toValue = numeric_limits::max(); + cutPts.back().end = X.size(); + if (debug) { + cout << "Entropy of the dataset: " << Metrics::entropy(y, indices, 0, indices.size(), numClasses) << endl; + for (auto cutPt : cutPts) + cout << "Entropy: " << Metrics::entropy(y, indices, cutPt.start, cutPt.end, numClasses) << ": Original: Cut point: " << cutPt; + } + cutPoints = cutPts; + } + bool CPPFImdlp::goodCut(size_t start, size_t cut, size_t end) + { + /* + Meter las entropías en una matríz cuadrada dispersa (samples, samples) M[start, end] iniciada a -1 y si no se ha calculado calcularla y almacenarla + + + */ + precision_t entropyLeft = Metrics::entropy(y, indices, start, cut, numClasses); + precision_t entropyRight = Metrics::entropy(y, indices, cut, end, numClasses); + precision_t entropyInterval = Metrics::entropy(y, indices, start, end, numClasses); + if (debug) + printf("Entropy L, R, T: L(%5.3g) + R(%5.3g) - T(%5.3g) \t", entropyLeft, entropyRight, entropyInterval); + //return (entropyInterval - (entropyLeft + entropyRight) > 0); + return true; + } + // Argsort from https://stackoverflow.com/questions/1577475/c-sorting-and-keeping-track-of-indexes + indices_t CPPFImdlp::sortIndices(samples& X_) + { + indices_t idx(X_.size()); + iota(idx.begin(), idx.end(), 0); + for (size_t i = 0; i < X_.size(); i++) + stable_sort(idx.begin(), idx.end(), [&X_](size_t i1, size_t i2) + { return X_[i1] < X_[i2]; }); + return idx; + } + void CPPFImdlp::setCutPoints(cutPoints_t cutPoints_) + { + cutPoints = cutPoints_; + } +} diff --git a/fimdlp/tests/bak/CPPFImdlp.h b/fimdlp/tests/bak/CPPFImdlp.h new file mode 100644 index 0000000..608b817 --- /dev/null +++ b/fimdlp/tests/bak/CPPFImdlp.h @@ -0,0 +1,39 @@ +#ifndef CPPFIMDLP_H +#define CPPFIMDLP_H +#include "typesFImdlp.h" +#include +namespace mdlp { + class CPPFImdlp { + protected: + bool proposal; // proposed algorithm or original algorithm + int precision; + bool debug; + precision_t divider; + indices_t indices; // sorted indices to use with X and y + samples X; + labels y; + labels xDiscretized; + int numClasses; + cutPoints_t cutPoints; + + void setCutPoints(cutPoints_t); + static indices_t sortIndices(samples&); + void computeCutPointsOriginal(); + void computeCutPointsProposal(); + bool evaluateCutPoint(cutPoint_t, cutPoint_t); + void filterCutPoints(); + bool goodCut(size_t, size_t, size_t); // if the cut candidate reduces entropy + + public: + CPPFImdlp(); + CPPFImdlp(bool, int, bool debug = false); + ~CPPFImdlp(); + samples getCutPoints(); + indices_t getIndices(); + labels getDiscretizedValues(); + void debugPoints(samples&, labels&); + CPPFImdlp& fit(samples&, labels&); + labels transform(samples&); + }; +} +#endif \ No newline at end of file diff --git a/fimdlp/tests/bak/Metrics.cpp b/fimdlp/tests/bak/Metrics.cpp new file mode 100644 index 0000000..d43d314 --- /dev/null +++ b/fimdlp/tests/bak/Metrics.cpp @@ -0,0 +1,47 @@ +#include "Metrics.h" +#include +namespace mdlp { + Metrics::Metrics() + = default; + int Metrics::numClasses(labels& y, indices_t indices, size_t start, size_t end) + { + std::set numClasses; + for (auto i = start; i < end; ++i) { + numClasses.insert(y[indices[i]]); + } + return numClasses.size(); + } + precision_t Metrics::entropy(labels& y, indices_t& indices, size_t start, size_t end, int nClasses) + { + precision_t entropy = 0; + int nElements = 0; + labels counts(nClasses + 1, 0); + for (auto i = &indices[start]; i != &indices[end]; ++i) { + counts[y[*i]]++; + nElements++; + } + for (auto count : counts) { + if (count > 0) { + precision_t p = (precision_t)count / nElements; + entropy -= p * log2(p); + } + } + return entropy < 0 ? 0 : entropy; + } + precision_t Metrics::informationGain(labels& y, indices_t& indices, size_t start, size_t end, size_t cutPoint, int nClasses) + { + precision_t iGain; + precision_t entropy, entropyLeft, entropyRight; + int nClassesLeft, nClassesRight; + int nElementsLeft = cutPoint - start, nElementsRight = end - cutPoint; + int nElements = end - start; + nClassesLeft = Metrics::numClasses(y, indices, start, cutPoint); + nClassesRight = Metrics::numClasses(y, indices, cutPoint, end); + entropy = Metrics::entropy(y, indices, start, end, nClasses); + entropyLeft = Metrics::entropy(y, indices, start, cutPoint, nClassesLeft); + entropyRight = Metrics::entropy(y, indices, cutPoint, end, nClassesRight); + iGain = entropy - ((precision_t)nElementsLeft * entropyLeft + (precision_t)nElementsRight * entropyRight) / nElements; + return iGain; + } + +} \ No newline at end of file diff --git a/fimdlp/tests/bak/Metrics.h b/fimdlp/tests/bak/Metrics.h new file mode 100644 index 0000000..5054998 --- /dev/null +++ b/fimdlp/tests/bak/Metrics.h @@ -0,0 +1,14 @@ +#ifndef METRICS_H +#define METRICS_H +#include "typesFImdlp.h" +#include +namespace mdlp { + class Metrics { + public: + Metrics(); + static int numClasses(labels&, indices_t, size_t, size_t); + static precision_t entropy(labels&, indices_t&, size_t, size_t, int); + static precision_t informationGain(labels&, indices_t&, size_t, size_t, size_t, int); + }; +} +#endif \ No newline at end of file diff --git a/fimdlp/typesFImdlp.h b/fimdlp/typesFImdlp.h index f23b78e..b94b943 100644 --- a/fimdlp/typesFImdlp.h +++ b/fimdlp/typesFImdlp.h @@ -5,21 +5,12 @@ using namespace std; namespace mdlp { - struct CutPointBody { - size_t start, end; // indices of the sorted vector - }; - typedef CutPointBody cutPoint_t; - typedef vector samples; + typedef float precision_t; + typedef vector samples; typedef vector labels; typedef vector indices_t; - typedef vector cutPoints_t; - typedef map, float> cacheEnt_t; - typedef map, float> cacheIg_t; - struct cutPointStruct { - size_t index; - float value; - }; - typedef cutPointStruct xcutPoint_t; - typedef vector xcutPoints_t; + typedef vector cutPoints_t; + typedef map, precision_t> cacheEnt_t; + typedef map, precision_t> cacheIg_t; } #endif \ No newline at end of file diff --git a/prueba/FImdlp.cpp b/prueba/FImdlp.cpp index 68c2f69..0e18c7a 100644 --- a/prueba/FImdlp.cpp +++ b/prueba/FImdlp.cpp @@ -13,7 +13,7 @@ namespace FImdlp { int n = X.size(); for (i = 1; i < n; i++) { if (X.at(i) != ant) { - cutPts.push_back(float(X.at(i) + ant) / 2); + cutPts.push_back(precision_t(X.at(i) + ant) / 2); ant = X.at(i); } } diff --git a/prueba/cfimdlp.pyx b/prueba/cfimdlp.pyx index cfa00b2..f7ba7f0 100644 --- a/prueba/cfimdlp.pyx +++ b/prueba/cfimdlp.pyx @@ -5,7 +5,7 @@ from libcpp.vector cimport vector cdef extern from "FImdlp.h" namespace "FImdlp": cdef cppclass FImdlp: FImdlp() except + - vector[float] cutPoints(vector[int]&, vector[int]&) + vector[precision_t] cutPoints(vector[int]&, vector[int]&) cdef class CFImdlp: cdef FImdlp *thisptr diff --git a/setup.py b/setup.py index e62a83a..b1ce695 100644 --- a/setup.py +++ b/setup.py @@ -12,10 +12,8 @@ setup( name="cppfimdlp", sources=[ "fimdlp/cfimdlp.pyx", - # "fimdlp/CPPFImdlp.cpp", - # "fimdlp/Metrics.cpp", - "fimdlp/ccMetrics.cc", - "fimdlp/ccFImdlp.cc", + "fimdlp/CPPFImdlp.cpp", + "fimdlp/Metrics.cpp", ], language="c++", include_dirs=["fimdlp"],