natural language toolkit decision tree classifiers c 20012023 nltk project edward loper edlopergmail com url https www nltk org for license information see license txt a classifier model that decides which label to assign to a token on the basis of a tree structure where branches correspond to conditions on feature values and leaves correspond to label assignments param label the most likely label for tokens that reach this node in the decision tree if this decision tree has no children then this label will be assigned to any token that reaches this decision tree param featurename the name of the feature that this decision tree selects for param decisions a dictionary mapping from feature values for the feature identified by featurename to child decision trees param default the child that will be used if the value of feature featurename does not match any of the keys in decisions this is used when constructing binary decision trees decision leaf decision tree return a string containing a prettyprinted version of this decision tree each line in this string corresponds to a single decision tree node or leaf and indentation is used to display the structure of the decision tree xx display default return a string representation of this decision tree that expresses the decisions it makes as a nested set of pseudocode if statements param binary if true then treat all featurevalue pairs as individual binary features rather than using a single nway branch for each feature collect a list of all feature names collect a list of the values each feature can take start with a stump refine the stump return it find the best label for each value find the best label for each value but hopefully we have observations demo natural language toolkit decision tree classifiers c 2001 2023 nltk project edward loper edloper gmail com url https www nltk org for license information see license txt a classifier model that decides which label to assign to a token on the basis of a tree structure where branches correspond to conditions on feature values and leaves correspond to label assignments param label the most likely label for tokens that reach this node in the decision tree if this decision tree has no children then this label will be assigned to any token that reaches this decision tree param feature_name the name of the feature that this decision tree selects for param decisions a dictionary mapping from feature values for the feature identified by feature_name to child decision trees param default the child that will be used if the value of feature feature_name does not match any of the keys in decisions this is used when constructing binary decision trees decision leaf decision tree return a string containing a pretty printed version of this decision tree each line in this string corresponds to a single decision tree node or leaf and indentation is used to display the structure of the decision tree xx display default return a string representation of this decision tree that expresses the decisions it makes as a nested set of pseudocode if statements param binary if true then treat all feature value pairs as individual binary features rather than using a single n way branch for each feature collect a list of all feature names collect a list of the values each feature can take start with a stump refine the stump return it find the best label for each value freq label value find the best label for each value but hopefully we have observations demo decisiontreeclassifier train
from collections import defaultdict

from nltk.classify.api import ClassifierI
from nltk.probability import FreqDist, MLEProbDist, entropy


class DecisionTreeClassifier(ClassifierI):
    def __init__(self, label, feature_name=None, decisions=None, default=None):
        
        self._label = label
        self._fname = feature_name
        self._decisions = decisions
        self._default = default

    def labels(self):
        labels = [self._label]
        if self._decisions is not None:
            for dt in self._decisions.values():
                labels.extend(dt.labels())
        if self._default is not None:
            labels.extend(self._default.labels())
        return list(set(labels))

    def classify(self, featureset):
        
        if self._fname is None:
            return self._label

        
        fval = featureset.get(self._fname)
        if fval in self._decisions:
            return self._decisions[fval].classify(featureset)
        elif self._default is not None:
            return self._default.classify(featureset)
        else:
            return self._label

    def error(self, labeled_featuresets):
        errors = 0
        for featureset, label in labeled_featuresets:
            if self.classify(featureset) != label:
                errors += 1
        return errors / len(labeled_featuresets)

    def pretty_format(self, width=70, prefix="", depth=4):
        
        
        if self._fname is None:
            n = width - len(prefix) - 15
            return "{}{} {}\n".format(prefix, "." * n, self._label)
        s = ""
        for i, (fval, result) in enumerate(
            sorted(
                self._decisions.items(),
                key=lambda item: (item[0] in [None, False, True], str(item[0]).lower()),
            )
        ):
            hdr = f"{prefix}{self._fname}={fval}? "
            n = width - 15 - len(hdr)
            s += "{}{} {}\n".format(hdr, "." * (n), result._label)
            if result._fname is not None and depth > 1:
                s += result.pretty_format(width, prefix + "  ", depth - 1)
        if self._default is not None:
            n = width - len(prefix) - 21
            s += "{}else: {} {}\n".format(prefix, "." * n, self._default._label)
            if self._default._fname is not None and depth > 1:
                s += self._default.pretty_format(width, prefix + "  ", depth - 1)
        return s

    def pseudocode(self, prefix="", depth=4):
        
        if self._fname is None:
            return f"{prefix}return {self._label!r}\n"
        s = ""
        for (fval, result) in sorted(
            self._decisions.items(),
            key=lambda item: (item[0] in [None, False, True], str(item[0]).lower()),
        ):
            s += f"{prefix}if {self._fname} == {fval!r}: "
            if result._fname is not None and depth > 1:
                s += "\n" + result.pseudocode(prefix + "  ", depth - 1)
            else:
                s += f"return {result._label!r}\n"
        if self._default is not None:
            if len(self._decisions) == 1:
                s += "{}if {} != {!r}: ".format(
                    prefix, self._fname, list(self._decisions.keys())[0]
                )
            else:
                s += f"{prefix}else: "
            if self._default._fname is not None and depth > 1:
                s += "\n" + self._default.pseudocode(prefix + "  ", depth - 1)
            else:
                s += f"return {self._default._label!r}\n"
        return s

    def __str__(self):
        return self.pretty_format()

    @staticmethod
    def train(
        labeled_featuresets,
        entropy_cutoff=0.05,
        depth_cutoff=100,
        support_cutoff=10,
        binary=False,
        feature_values=None,
        verbose=False,
    ):
        
        
        feature_names = set()
        for featureset, label in labeled_featuresets:
            for fname in featureset:
                feature_names.add(fname)

        
        if feature_values is None and binary:
            feature_values = defaultdict(set)
            for featureset, label in labeled_featuresets:
                for fname, fval in featureset.items():
                    feature_values[fname].add(fval)

        
        if not binary:
            tree = DecisionTreeClassifier.best_stump(
                feature_names, labeled_featuresets, verbose
            )
        else:
            tree = DecisionTreeClassifier.best_binary_stump(
                feature_names, labeled_featuresets, feature_values, verbose
            )

        
        tree.refine(
            labeled_featuresets,
            entropy_cutoff,
            depth_cutoff - 1,
            support_cutoff,
            binary,
            feature_values,
            verbose,
        )

        
        return tree

    @staticmethod
    def leaf(labeled_featuresets):
        label = FreqDist(label for (featureset, label) in labeled_featuresets).max()
        return DecisionTreeClassifier(label)

    @staticmethod
    def stump(feature_name, labeled_featuresets):
        label = FreqDist(label for (featureset, label) in labeled_featuresets).max()

        
        freqs = defaultdict(FreqDist)  
        for featureset, label in labeled_featuresets:
            feature_value = featureset.get(feature_name)
            freqs[feature_value][label] += 1

        decisions = {val: DecisionTreeClassifier(freqs[val].max()) for val in freqs}
        return DecisionTreeClassifier(label, feature_name, decisions)

    def refine(
        self,
        labeled_featuresets,
        entropy_cutoff,
        depth_cutoff,
        support_cutoff,
        binary=False,
        feature_values=None,
        verbose=False,
    ):
        if len(labeled_featuresets) <= support_cutoff:
            return
        if self._fname is None:
            return
        if depth_cutoff <= 0:
            return
        for fval in self._decisions:
            fval_featuresets = [
                (featureset, label)
                for (featureset, label) in labeled_featuresets
                if featureset.get(self._fname) == fval
            ]

            label_freqs = FreqDist(label for (featureset, label) in fval_featuresets)
            if entropy(MLEProbDist(label_freqs)) > entropy_cutoff:
                self._decisions[fval] = DecisionTreeClassifier.train(
                    fval_featuresets,
                    entropy_cutoff,
                    depth_cutoff,
                    support_cutoff,
                    binary,
                    feature_values,
                    verbose,
                )
        if self._default is not None:
            default_featuresets = [
                (featureset, label)
                for (featureset, label) in labeled_featuresets
                if featureset.get(self._fname) not in self._decisions
            ]
            label_freqs = FreqDist(label for (featureset, label) in default_featuresets)
            if entropy(MLEProbDist(label_freqs)) > entropy_cutoff:
                self._default = DecisionTreeClassifier.train(
                    default_featuresets,
                    entropy_cutoff,
                    depth_cutoff,
                    support_cutoff,
                    binary,
                    feature_values,
                    verbose,
                )

    @staticmethod
    def best_stump(feature_names, labeled_featuresets, verbose=False):
        best_stump = DecisionTreeClassifier.leaf(labeled_featuresets)
        best_error = best_stump.error(labeled_featuresets)
        for fname in feature_names:
            stump = DecisionTreeClassifier.stump(fname, labeled_featuresets)
            stump_error = stump.error(labeled_featuresets)
            if stump_error < best_error:
                best_error = stump_error
                best_stump = stump
        if verbose:
            print(
                "best stump for {:6d} toks uses {:20} err={:6.4f}".format(
                    len(labeled_featuresets), best_stump._fname, best_error
                )
            )
        return best_stump

    @staticmethod
    def binary_stump(feature_name, feature_value, labeled_featuresets):
        label = FreqDist(label for (featureset, label) in labeled_featuresets).max()

        
        pos_fdist = FreqDist()
        neg_fdist = FreqDist()
        for featureset, label in labeled_featuresets:
            if featureset.get(feature_name) == feature_value:
                pos_fdist[label] += 1
            else:
                neg_fdist[label] += 1

        decisions = {}
        default = label
        
        if pos_fdist.N() > 0:
            decisions = {feature_value: DecisionTreeClassifier(pos_fdist.max())}
        if neg_fdist.N() > 0:
            default = DecisionTreeClassifier(neg_fdist.max())

        return DecisionTreeClassifier(label, feature_name, decisions, default)

    @staticmethod
    def best_binary_stump(
        feature_names, labeled_featuresets, feature_values, verbose=False
    ):
        best_stump = DecisionTreeClassifier.leaf(labeled_featuresets)
        best_error = best_stump.error(labeled_featuresets)
        for fname in feature_names:
            for fval in feature_values[fname]:
                stump = DecisionTreeClassifier.binary_stump(
                    fname, fval, labeled_featuresets
                )
                stump_error = stump.error(labeled_featuresets)
                if stump_error < best_error:
                    best_error = stump_error
                    best_stump = stump
        if verbose:
            if best_stump._decisions:
                descr = "{}={}".format(
                    best_stump._fname, list(best_stump._decisions.keys())[0]
                )
            else:
                descr = "(default)"
            print(
                "best stump for {:6d} toks uses {:20} err={:6.4f}".format(
                    len(labeled_featuresets), descr, best_error
                )
            )
        return best_stump







def f(x):
    return DecisionTreeClassifier.train(x, binary=True, verbose=True)


def demo():
    from nltk.classify.util import binary_names_demo_features, names_demo

    classifier = names_demo(
        f, binary_names_demo_features  
    )
    print(classifier.pretty_format(depth=7))
    print(classifier.pseudocode(depth=7))


if __name__ == "__main__":
    demo()