Commit ece3483c authored by ehebrard's avatar ehebrard
Browse files

neurips

parent 7c0791b4
......@@ -85,13 +85,13 @@ int main(int argc, char *argv[]) {
WeightedDataset<int> input;
////// READING
try {
read_binary(input, opt);
} catch (const std::exception &e) {
// try {
// read_binary(input, opt);
// } catch (const std::exception &e) {
if (opt.verbosity >= DTOptions::NORMAL)
cout << "c binarizing...\n";
read_non_binary(input, opt);
}
// }
if (opt.sample < 1)
input.sample(opt.sample);
......
......@@ -65,6 +65,9 @@ int run_algorithm(DTOptions &opt) {
cout << "d examples=" << A.numExample() << " features=" << A.numFeature()
<< endl;
// A.perfectTree();
// A.minimize_error();
////// SOLVING
if (opt.mindepth) {
if (opt.minsize)
......@@ -72,10 +75,10 @@ int run_algorithm(DTOptions &opt) {
else
A.minimize_error_depth();
} else {
if (opt.minsize)
A.set_size_objective();
if (opt.minsize)
A.set_size_objective();
A.minimize_error();
}
}
Tree<E_t> sol = A.getSolution();
......
......@@ -5,7 +5,7 @@
#include <iostream>
#include <random>
#include <vector>
#include <cmath>
// #include <cmath>
#include "CmdLine.hpp"
#include "Partition.hpp"
......@@ -30,31 +30,31 @@ namespace blossom {
/// this needs to be templated with "T" and should be 0 for integral types
/// (e.g., static_cast<T>(1.e-9) should work)
// #define FLOAT_PRECISION std::numeric_limits<T>::epsilon()
#define FLOAT_PRECISION static_cast<T>(1.e-6)
template <typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
equal(const T &a, const T &b) {
return a == b;
}
template <typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
lt(const T &a, const T &b) {
return a < b;
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, bool>::type
equal(const T &a, const T &b) {
return std::fabs(a - b) < FLOAT_PRECISION;
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, bool>::type
lt(const T &a, const T &b) {
return a + FLOAT_PRECISION < b;
}
// #define FLOAT_PRECISION static_cast<T>(1.e-6)
//
// template <typename T>
// typename std::enable_if<std::is_integral<T>::value, bool>::type
// equal(const T &a, const T &b) {
// return a == b;
// }
//
// template <typename T>
// typename std::enable_if<std::is_integral<T>::value, bool>::type
// lt(const T &a, const T &b) {
// return a < b;
// }
//
// template <typename T>
// typename std::enable_if<std::is_floating_point<T>::value, bool>::type
// equal(const T &a, const T &b) {
// return std::fabs(a - b) < FLOAT_PRECISION;
// }
//
// template <typename T>
// typename std::enable_if<std::is_floating_point<T>::value, bool>::type
// lt(const T &a, const T &b) {
// return a + FLOAT_PRECISION < b;
// }
template <typename E_t> class CardinalityError;
......@@ -100,6 +100,8 @@ public:
E_t getUbError() const;
void perfectTree();
size_t getUbDepth() const;
size_t getUbSize() const;
......
......@@ -47,6 +47,9 @@ public:
template <typename boolean_function>
void split(Part &l1, Part &l2, boolean_function condition);
void addFalse(vector<int>::iterator elt_ptr);
void addTrue(vector<int>::iterator elt_ptr);
/*!@name Miscellaneous*/
//@{
std::ostream &display(std::ostream &os) const;
......@@ -82,6 +85,8 @@ public:
template <typename boolean_function>
void branch(const int node, const int x, const int y,
boolean_function condition);
void branch(const int node, const int x, const int y);
/*!@name Miscellaneous*/
//@{
......
......@@ -263,7 +263,7 @@ public:
// cout << ClassicEncoding<T>::value_set.size() << " " << (num_examples) ;
if (ClassicEncoding<T>::value_set.size() < sqrt(num_examples)) {
if (ClassicEncoding<T>::value_set.size() < sqrt(num_examples) or ClassicEncoding<T>::value_set.size() < 10) {
// cout << " full\n";
full_encoding();
......
......@@ -154,7 +154,9 @@ inline void WeightedDataset<E_t>::addExample(rIter beg_row, rIter end_row,
int f{0};
for (auto x{beg_row}; x != end_row; ++x) {
assert(*x == 0 or *x == 1);
// assert(*x == 0 or *x == 1);
if (*x != 0 and *x != 1)
throw 0;
if (x - beg_row != column) {
if (*x)
data[y].back().set(f);
......
......@@ -9,6 +9,7 @@
#include <iomanip>
#include <iostream>
#include <math.h>
#include <cmath>
// #define DEBUG_MODE
......@@ -29,6 +30,32 @@ floating_point fixedwidthfloat(const floating_point f, const int width) {
return (static_cast<floating_point>(i) / m);
}
#define FLOAT_PRECISION static_cast<T>(1.e-6)
template <typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
equal(const T &a, const T &b) {
return a == b;
}
template <typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
lt(const T &a, const T &b) {
return a < b;
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, bool>::type
equal(const T &a, const T &b) {
return std::fabs(a - b) < FLOAT_PRECISION;
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, bool>::type
lt(const T &a, const T &b) {
return a + FLOAT_PRECISION < b;
}
}
#endif // _BLOSSOM_UTILS_HPP
......@@ -334,6 +334,7 @@ void BacktrackingAlgorithm<ErrorPolicy, E_t>::print_new_best() {
<< search_size << " mem=" << setw(3) << wood.size()
<< " time=" << setprecision(max(4, static_cast<int>(log10(t))))
<< fixedwidthfloat(t, 3) << right << endl;
}
template <template <typename> class ErrorPolicy, typename E_t>
......@@ -601,7 +602,8 @@ void BacktrackingAlgorithm<ErrorPolicy, E_t>::sort_features(const int node) {
for (auto f{feature[node]}; f != end_feature[node]; ++f)
f_error[*f] = get_feature_error(node, *f);
sort(feature[node], end_feature[node],
[&](const int a, const int b) { return f_error[a] < f_error[b]; });
// [&](const int a, const int b) { return f_error[a] < f_error[b]; });
[&](const int a, const int b) { return (f_error[a] < f_error[b] or (f_error[a] == f_error[b] and a < b)); });
break;
case DTOptions::ENTROPY:
for (auto f{feature[node]}; f != end_feature[node]; ++f)
......@@ -830,12 +832,12 @@ bool BacktrackingAlgorithm<ErrorPolicy, E_t>::update_upperbound(const int node)
max_error[node] = err;
max_size[node] = sz;
#ifdef PRINTTRACE
if (PRINTTRACE)
cout << "new best for node " << node << ": feat=" << *feature[node]
<< ", error=" << max_error[node] << ", size=" << max_size[node]
<< endl;
#endif
// #ifdef PRINTTRACE
// if (PRINTTRACE)
// cout << "new best for node " << node << ": feat=" << *feature[node]
// << ", error=" << max_error[node] << ", size=" << max_size[node]
// << endl;
// #endif
if (node > 0) {
assert(parent[node] >= 0);
......@@ -863,6 +865,7 @@ bool BacktrackingAlgorithm<ErrorPolicy, E_t>::backtrack() {
#ifdef PRINTTRACE
if (PRINTTRACE) {
cout << setw(3) << decision.size();
for (auto i{0}; i < decision.size(); ++i)
cout << " ";
cout << "backtrack to " << backtrack_node << endl;
......@@ -874,13 +877,13 @@ bool BacktrackingAlgorithm<ErrorPolicy, E_t>::backtrack() {
(equal<E_t>(tree_error[backtrack_node],max_error[backtrack_node]) and
tree_size[backtrack_node] > max_size[backtrack_node])) {
#ifdef PRINTTRACE
if (PRINTTRACE) // and not updt)
cout << "new best for node " << backtrack_node
<< ": feat=" << *feature[backtrack_node]
<< ", error=" << max_error[backtrack_node]
<< ", size=" << max_size[backtrack_node] << endl;
#endif
// #ifdef PRINTTRACE
// if (PRINTTRACE) // and not updt)
// cout << "new best for node " << backtrack_node
// << ": feat=" << *feature[backtrack_node]
// << ", error=" << max_error[backtrack_node]
// << ", size=" << max_size[backtrack_node] << endl;
// #endif
tree_error[backtrack_node] = max_error[backtrack_node];
tree_size[backtrack_node] = max_size[backtrack_node];
......@@ -888,12 +891,12 @@ bool BacktrackingAlgorithm<ErrorPolicy, E_t>::backtrack() {
} else {
#ifdef PRINTTRACE
if (PRINTTRACE)
cout << "no improvement for node " << backtrack_node
<< ": feat=" << *feature[backtrack_node] << "("
<< ") -> free the best subtrees" << endl;
#endif
// #ifdef PRINTTRACE
// if (PRINTTRACE)
// cout << "no improvement for node " << backtrack_node
// << ": feat=" << *feature[backtrack_node] << "("
// << ") -> free the best subtrees" << endl;
// #endif
for (auto i{0}; i < 2; ++i)
if (child[i][backtrack_node] >= 0 and
......@@ -914,12 +917,13 @@ bool BacktrackingAlgorithm<ErrorPolicy, E_t>::backtrack() {
(not max_entropy(backtrack_node, *feature[backtrack_node])));
dead_end = (
// (depth[backtrack_node] == ub_depth - 1 and
// (node_error(backtrack_node) >= ub_error)) or
// current_error >= ub_error or
(lt<E_t>(0,ub_error) and equal<E_t>(max_error[backtrack_node], 0)) or
(lt<E_t>(0, ub_error) and equal<E_t>(max_error[backtrack_node], 0)) or
no_feature(backtrack_node) or
max_entropy(backtrack_node, *feature[backtrack_node])
or ( options.bounding and fail(backtrack_node) )
);
max_entropy(backtrack_node, *feature[backtrack_node]) or
(options.bounding and fail(backtrack_node)));
// backtrack again
if (dead_end) {
......@@ -1013,12 +1017,8 @@ void BacktrackingAlgorithm<ErrorPolicy, E_t>::branch(const int node, const int f
cout << setw(3) << decision.size();
for (auto i{0}; i < decision.size(); ++i)
cout << " ";
cout << "branch on " << node << " with " << f << " children: " << c[0]
// << " (" << P[0][c[0]].count() << "/" << P[1][c[0]].count() << ") and
// "
// << c[1] << "(" << P[0][c[1]].count() << "/" << P[1][c[1]].count()
// << ")"
<< endl;
cout << "branch on " << node << " with " << f << " ("
<< (end_feature[node] - feature[node]) << ")";
}
#endif
......@@ -1065,6 +1065,36 @@ void BacktrackingAlgorithm<ErrorPolicy, E_t>::branch(const int node, const int f
++current_size;
}
}
#ifdef PRINTTRACE
if (PRINTTRACE) {
// cout << setw(3) << decision.size()-1;
// for (auto i{0}; i < decision.size()-1; ++i)
// cout << " ";
// cout << "branch on " << node << " (" << P[node].count() << "/"
// << (usize(node) - P[node].count()) << ") with " << f
// cout << " children: " << child[0][node] << " (" << P[0][c[0]].count() << "/" << P[1][c[0]].count() << ") and " << c[1] << " ("
// << P[0][c[1]].count() << "/" << P[1][c[1]].count()
// << ")" << endl;
cout << " children: " << c[0] << " (" << P[0][c[0]].count() << "/" << P[1][c[0]].count() << ") and " << c[1] << " ("
<< P[0][c[1]].count() << "/" << P[1][c[1]].count()
<< ")" << endl;
// cout << " children: " << c[0] << " ("
// << (child[0][node] >= 0 ? error_policy.get_total(0, child[0][node]) :
// (child[0][node] == -1 ? error_policy.get_total(0, node) : 0))
// << "/"
// << (child[0][node] >= 0 ? error_policy.get_total(1, child[0][node]) :
// (child[0][node] == -2 ? error_policy.get_total(1, node) : 0))
// << ") and "
// << c[1] << " ("
// << (child[1][node] >= 0 ? error_policy.get_total(0, child[1][node]) :
// (child[1][node] == -1 ? error_policy.get_total(0, node) : 0))
// << "/"
// << (child[1][node] >= 0 ? error_policy.get_total(1, child[1][node]) :
// (child[1][node] == -2 ? error_policy.get_total(1, node) : 0))
// << ")" << endl;
}
#endif
update_upperbound(node);
}
......@@ -1172,16 +1202,16 @@ void BacktrackingAlgorithm<ErrorPolicy, E_t>::expend() {
break;
}
if (max_entropy(selected_node, *feature[selected_node])) {
cout << selected_node << " " << *feature[selected_node] << " "
<< get_feature_frequency(0, selected_node, *feature[selected_node])
<< " / " << error_policy.get_total(0, selected_node) << " || "
<< get_feature_frequency(1, selected_node, *feature[selected_node])
<< " / " << error_policy.get_total(1, selected_node) << endl;
}
assert(not max_entropy(selected_node, *feature[selected_node]));
// if (max_entropy(selected_node, *feature[selected_node])) {
//
// cout << selected_node << " " << *feature[selected_node] << " "
// << get_feature_frequency(0, selected_node, *feature[selected_node])
// << " / " << error_policy.get_total(0, selected_node) << " || "
// << get_feature_frequency(1, selected_node, *feature[selected_node])
// << " / " << error_policy.get_total(1, selected_node) << endl;
// }
//
// assert(not max_entropy(selected_node, *feature[selected_node]));
}
if (options.width > 1)
......@@ -1190,6 +1220,11 @@ void BacktrackingAlgorithm<ErrorPolicy, E_t>::expend() {
assert(feature[selected_node] >= ranked_feature[selected_node].begin() and
feature[selected_node] < end_feature[selected_node]);
// cout << "(" << (end_feature[selected_node] - feature[selected_node]) << ")";
// for(auto f{feature[selected_node]}; f!=end_feature[selected_node]; ++f)
// cout << setw(3) << *f << " [" << get_feature_error(selected_node, *f) << "]";
// cout << endl;
branch(selected_node, *(feature[selected_node]));
......@@ -1203,7 +1238,8 @@ void BacktrackingAlgorithm<ErrorPolicy, E_t>::initialise_search() {
num_level_zero_feature = num_feature;
setReverse();
setReverse();
start_time = cpu_time();
// search_size = 0;
......@@ -1374,6 +1410,11 @@ void BacktrackingAlgorithm<ErrorPolicy, E_t>::minimize_error() {
print_new_best();
}
template <template <typename> class ErrorPolicy, typename E_t>
void BacktrackingAlgorithm<ErrorPolicy, E_t>::perfectTree() {
ub_error = min_positive<E_t>();
}
template <template<typename> class ErrorPolicy, typename E_t>
void BacktrackingAlgorithm<ErrorPolicy, E_t>::minimize_error_depth() {
......@@ -1520,10 +1561,10 @@ void BacktrackingAlgorithm<ErrorPolicy, E_t>::clearExamples() {
template <template <typename> class ErrorPolicy, typename E_t>
bool BacktrackingAlgorithm<ErrorPolicy, E_t>::fail(const int b) const {
#ifdef PRINTTRACE
if (PRINTTRACE)
cout << "bound from " << b << endl;
#endif
// #ifdef PRINTTRACE
// if (PRINTTRACE)
// cout << "bound from " << b << endl;
// #endif
E_t lbe{0};
auto lbs{0};
......@@ -1544,15 +1585,15 @@ bool BacktrackingAlgorithm<ErrorPolicy, E_t>::fail(const int b) const {
lbs += min_size[child[i][p]];
++lbs;
}
#ifdef PRINTTRACE
if (PRINTTRACE)
cout << "parent " << p << " (ub=" << ube << "/" << ubs << ", lb=" << lbe
<< "/" << lbs << ") ["
<< (child[0][p] >= 0 ? min_error[child[0][p]] : 0) << "/"
<< (child[1][p] >= 0 ? min_error[child[1][p]] : 0) << " | "
<< (child[0][p] >= 0 ? min_size[child[0][p]] : 1) << "/"
<< (child[1][p] >= 0 ? min_size[child[1][p]] : 1) << "]\n";
#endif
// #ifdef PRINTTRACE
// if (PRINTTRACE)
// cout << "parent " << p << " (ub=" << ube << "/" << ubs << ", lb=" << lbe
// << "/" << lbs << ") ["
// << (child[0][p] >= 0 ? min_error[child[0][p]] : 0) << "/"
// << (child[1][p] >= 0 ? min_error[child[1][p]] : 0) << " | "
// << (child[0][p] >= 0 ? min_size[child[0][p]] : 1) << "/"
// << (child[1][p] >= 0 ? min_size[child[1][p]] : 1) << "]\n";
// #endif
if (lt<E_t>(ube, lbe) or
(equal<E_t>(lbe, ube) and (lbs >= ubs or not size_matters))) {
......
......@@ -31,6 +31,11 @@ size_t TreePartition::addNode() {
void TreePartition::remNode() { part.pop_back(); }
void TreePartition::branch(const int node, const int x, const int y) {
part[x].begin_idx = part[x].end_idx = part[node].begin_idx;
part[y].begin_idx = part[y].end_idx = part[node].end_idx;
}
Part &TreePartition::operator[](const int i) { return part[i]; }
const Part &TreePartition::operator[](const int i) const { return part[i]; }
......@@ -93,6 +98,14 @@ vector<int>::const_iterator Part::end() const {
return element.begin() + end_idx;
}
void Part::addTrue(vector<int>::iterator elt_ptr) {
std::swap(*elt_ptr, element[end_idx++]);
}
void Part::addFalse(vector<int>::iterator elt_ptr) {
std::swap(*elt_ptr, element[--begin_idx]);
}
std::ostream& Part::display(std::ostream& os) const {
assert(begin() <= end());
......
......@@ -15,6 +15,12 @@ void SparseSet::reserve(const size_t n) {
list_.push_back(list_.size());
}
}
void SparseSet::resize(const size_t n) {
reserve(n);
fill();
}
//
// void SparseSet::save(size_t &stamp1, size_t &stamp2) { stamp1 = size_; stamp2
// = start_; }
......
\documentclass{llncs}
\documentclass{article}
\usepackage{neurips_2021}
\usepackage[usenames,dvipsnames,svgnames,table]{xcolor}%% http://ctan.org/pkg/xcolor
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{hyperref} % hyperlinks
\usepackage{url} % simple URL typesetting
\usepackage{booktabs} % professional-quality tables
\usepackage{amsfonts} % blackboard math symbols
\usepackage{nicefrac} % compact symbols for 1/2, etc.
\usepackage{microtype} % microtypography
\usepackage{xcolor} % colors
\usepackage{xspace}
\usepackage{array}
%\usepackage{amsthm}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage[ruled,vlined]{algorithm2e}
\usepackage{booktabs}
\usepackage{multirow}
\usepackage{url}
\usepackage{tikz}
\usetikzlibrary{arrows,shadows,fit,calc,positioning,decorations.pathreplacing,matrix,shapes,petri,topaths,fadings,mindmap,backgrounds,shapes.geometric}
......@@ -23,6 +33,10 @@
\usepackage{forest}
\usepackage{relsize}
\newtheorem{theorem}{Theorem}
\newtheorem{example}{Example}
\newenvironment{proof}{\paragraph{Proof:}}{\hfill$\square$}
\input{src/macros.tex}
......@@ -31,9 +45,26 @@
\title{A Simple and Efficient Anytime Algorithm for Computing Optimal Decision Trees}
\author{Emir Demirovi\'c\inst{1} \and Emmanuel Hebrard\inst{2} \and Louis Jean\inst{2}}
\institute{TU DELFT, The Netherlands, email: e.demirovic@tudelft.nl
\and LAAS-CNRS, Universit\'e de Toulouse, CNRS, France, email: \{hebrard,ljean\}@laas.fr}
\author{%
Emir Demirovi\'c \\
TU DELFT \\
The Netherlands \\
\texttt{e.demirovic@tudelft.nl} \\
\And
Emmanuel Hebrard \\
LAAS-CNRS \\
Universit\'e de Toulouse, CNRS \\
France \\
\texttt{hebrard@laas.fr} \\
\And
Louis Jean \\
LAAS-CNRS \\
Universit\'e de Toulouse, CNRS \\
France \\
\texttt{ljean@laas.fr} \\
}
\begin{document}
......@@ -46,11 +77,13 @@
\begin{abstract}
In this paper we introduce a relatively {simple} algorithm to learn optimal decision trees of bounded depth. This algorithm, \budalg, is as memory and time efficient as heuristics, and yet more efficient than most exact methods on most data sets. State-of-the-art exact methods often have poor anytime behavior, and hardly scale to deep trees. In our experiments, we found that they are typically orders of magnitude slower than the proposed algorithm to compute optimally accurate classifiers of a given depth.
In this paper we introduce a relatively {simple} algorithm to learn optimal decision trees of bounded depth. This algorithm, \budalg, is as memory and time efficient as heuristics, and yet more efficient than most exact methods on most data sets. State-of-the-art exact methods often have poor anytime behavior, and hardly scale to deep trees. Experiments show that they are typically orders of magnitude slower than the proposed algorithm to compute optimally accurate classifiers of a given depth.
On the other hand, \budalg\ finds, without significant computational overhead, solutions comparable to those returned by standard greedy heuristics, and can quickly improve their accuracy when given more computation time.
% the first solution found by \budalg\ is comparable to those found by standard greedy heuristics and that significantly improve upon greedy heuristics. On the
\end{abstract}
\section{Introduction}
In conclusion of their short paper showing that computing decision tree classifiers of maximum accuracy is NP-complete, Hyafil and Rivest stated: ``Accordingly, it is to be expected that that good heuristics for constructing near-optimal binary decision trees will be the best solution to this problem in the near future.''~\cite{NPhardTrees}. Indeed, heuristic approaches such as \cart\cite{breiman1984classification}, \idthree~\cite{10.1023/A:1022643204877} or \cfour~\cite{c4-5} have been prevalent long afterward, and are still vastly more commonly used in practice than exact approaches.
......@@ -161,21 +194,21 @@ To a \emph{branch} of a decision tree we associate the ordered set of labels on
%Moreover, if we also consider data points as conjunctions of features (where every feature appears either positively or negatively),
%given a branch $\abranch \subseteq \features$
Given a data set $\langle \negex,\posex \rangle$, we can associate a data set $\langle \negex(\abranch),\posex(\abranch) \rangle$ to a branch $\abranch$ where:
\begin{eqnarray*}
\negex(\abranch) = \{\ex \mid \ex \in \negex, \abranch \subseteq \ex\}\\
\posex(\abranch) = \{\ex \mid \ex \in \posex, \abranch \subseteq \ex\}
\end{eqnarray*}
Given a data set $\langle \negex,\posex \rangle$, we can associate a data set $\langle \negex(\abranch),\posex(\abranch) \rangle$ to a branch $\abranch$ where $\negex(\abranch) = \{\ex \mid \ex \in \negex, \abranch \subseteq \ex\}$ and $\posex(\abranch) = \{\ex \mid \ex \in \posex, \abranch \subseteq \ex\}$.
% \begin{eqnarray*}
% \negex(\abranch) = \{\ex \mid \ex \in \negex, \abranch \subseteq \ex\}\\
% \posex(\abranch) = \{\ex \mid \ex \in \posex, \abranch \subseteq \ex\}
% \end{eqnarray*}
%
% For instance, the branch $\abranch = \{\afeat_i, \bar{\afeat_j}, \bar{\afeat_k}, \afeat_l\}$ has length 4 and
%
We write $\grow{\abranch}{\afeat}$ as a shortcut for $\abranch \cup \{\afeat\}$.
The classification error for branch $\abranch$ is $\error[\abranch]=\min(|\negex(\abranch)|, |\posex(\abranch)|)$.
% %Let $\error[\abranch]$ be $\min(|\negex(\abranch)|, |\posex(\abranch)|)$,
% and we write $\error[\abranch,\afeat]$ for $\error[\grow{\abranch}{\afeat}] + \error[\grow{\abranch}{\bar{\afeat}}]$.
% %A branch \abranch\ is said \emph{pure} iff $\error[\abranch]=0$.
\medskip
% \medskip
Given a binary data set $\langle \negex,\posex \rangle$ on the features \features, %with label function $\classlabel$,
the \emph{minimum error bounded depth decision tree problem} consists in finding a binary decision tree with vertex labels in \features\ whose branches have cardinality at most $\mdepth$ and the sum of the classification error %($\error[\abranch]$)
......@@ -183,6 +216,8 @@ of its maximal branches is minimum.
%of depth at most $\maxd$ whose sum of error $\error[\abranch]$ for all branches $\abranch$ from the root to the leaves is equal to $\epsilon$.
\subsection{Dynamic Programming Algorithm}