Проблемы с реализацией недвоичного решения ID3 на основе классов - PullRequest
0 голосов
/ 15 декабря 2018

РЕДАКТИРОВАТЬ: я добавил свои собственные методы для энтропийного / inf-усиления / лучшего разделения внизу на случай, если кто-нибудь захочет помочь мне отладить, чтобы им не пришлось писать свои собственные!

Каквызов, после просмотра этого видео я хотел создать решение на Python (класс, основанный на практике ООП).Поскольку данные здесь категоричны, я хотел создать дерево, чтобы оно отображалось визуально так же, как видео (т. Е. Возможно,> 2 дочерних элемента, метки на разделении и значения и т. Д.).

Я сделал все возможное, чтобы отладить код, чтобы он работал.В своем текущем состоянии у него две огромные проблемы:

(1) Список дочерних элементов экземпляра класса показывает некоторые значения None и некоторые объекты Tree.Поскольку значения None нарушают функцию .predict, я поместил временный оператор if, чтобы игнорировать их

(2) Атрибуты значения показывают «Да» и «Нет», которые являются целевыми значениями, а не значениями функций.например,

In: dt.tree_.children[0].value
Out: "Yes"

Разделение объектов вообще не входит в атрибут label.Поле метки равно None независимо от узла / дочернего элемента, и прогноз также возвращает None.Я часами просматривал код, но не могу понять почему.Я включил данные (дублированные из видео) и настройку DataFrame для простоты доступа и чтобы показать, как я пытаюсь запустить программу.

Заранее извиняюсь за очень длинный пост!Я пытаюсь объяснить свою логику (и не хочу, чтобы люди думали, что я просто прошу других написать мой код), хотя это, вероятно, удержит большинство людей от помощи!

Примечание: дерево решенийкласс использует вложенные функции, так что .fit всегда начинается с нового экземпляра Tree (), и поэтому .predict будет использовать атрибут dt.tree_, который должен быть заполнен после .fit

Tree () class:

class Tree():
    def __init__(self, children = [], label = None, value = None):
        self.children = children    #used in place of left and right for binary solutions
        self.label = label          #to label which feature this node's children are split on
        self.value = value          #the values of the above node's split feature. This should always be None for the head node

Псевдокод для dt.fit:

def fit(self, data, target, features)
    def run_id3(data, target, features, tree):

        (base case)
        Check if target column has only one unique value. 
            If so, set current tree label to target column, add one child below current tree with target value
            return (end recursion)

        find the best feature to split data on
        set current node label to feature
        for each unique value in splitting feature:
            create a node and set value equal to unique value
            append new node to the children list of the current tree
            recur with data filtered for the current unique feature value (split) and with the child tree as the head
    run_id3(data, target, features, self.tree_)

Код для dt.fit:

class DecisionTree():
    tree_: Tree

    def __init__(self):
        self.tree_ = Tree()
        pass

    def fit(self, data, target, features):
        def run_id3(data, target, features, tree):
            unique_targets = pd.unique(data[target])
            if len(unique_targets) == 1:
                tree.label = target
                tree.children.append(Tree(value=unique_targets[0]))
                return
            best_split = find_best(data, target, features)
            tree.label = best_split
            for unique_val in np.unique(data[best_split]):
                new_tree = Tree()
                new_tree.value = unique_val
                tree.children.append(run_id3(data[data[best_split] == unique_val], target, features, new_tree))

        run_id3(data, target, features, self.tree_)

Псевдокод для dt.predict:

def predict(self, row):
    def get_prediction(tree, row):
        check if current node has no children
            return node label (should be target prediction)
        set current column (feature split) to current node label
        for each child of current node
            if child is not null (THIS IS NOT GOOD, EXISTS TO STOP PROGRAM HALTING)
                if child’s value is equal to the value in that column in our test row
                    recur (go down tree), set current child tree to head in parameter

    tree = self.tree_ (so tree starts at the head of the instantiated tree, should be populated after dt.fit)
    return get_prediction(tree, row)

Код для dt.predict:

    def predict(self, row):
        def get_prediction(tree, row):
            if len(tree.children) == 0:
                return tree.label
            column = tree.label
            for child in tree.children:
# the below conditional is purely to stop the program halting since I haven't worked out why the children attribute keeps adding NoneType objects
                if child is not None:
                    if child.value == row[column]:
                        return get_prediction(child, row)

        tree = self.tree_
        return get_prediction(tree, row)

Настройка данных:

outlook = ['Sunny', 'Sunny', 'Overcast', 'Rain', 'Rain', 'Rain', 'Overcast', 'Sunny', 'Sunny', 'Rain', 'Sunny', 'Overcast', 'Overcast', 'Rain', 'Rain']
humidity = ['High', 'High', 'High', 'High', 'Normal', 'Normal', 'Normal', 'High', 'Normal', 'Normal', 'Normal', 'High', 'Normal', 'High', 'High']
wind = ['Weak', 'Strong', 'Weak', 'Weak', 'Weak', 'Strong', 'Strong', 'Weak', 'Weak', 'Weak', 'Strong', 'Strong', 'Weak', 'Strong', 'Weak']
play = ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', '?']

columns = ["Outlook", "Humidity", "Wind", "Play"]
data = pd.DataFrame([outlook, humidity, wind, play]).T
data.columns = columns
train = data.iloc[:-1, :]
test = data.iloc[-1, :3]
features = columns.copy()
features.remove("Play")
target = "Play"

dt = DecisionTree()
dt.fit(train, target, features)
pred = dt.predict(test)

Методы получения информации:

import numpy as np
import pandas as pd



def entropy(column):
    elements, counts = np.unique(column, return_counts=True)
    # if statement in comprehension stops nan result since 0*log2(x) is undefined, returns 0. in this case,
    # 1*log2(1) + 0*log2(0) = 0. zero entropy result, zero uncertainty is consistent with theory
    entropy = np.sum(
        [-(counts[i] / np.sum(counts)) * np.log2(counts[i] / np.sum(counts)) if counts[i] > 0 else 0 for i in
         range(len(counts))])
    return entropy


def information_gain(data, split_name, target_name):
    target_entropy = entropy(data[target_name])
    vals, counts = np.unique(data[split_name], return_counts=True)
    weighted_entropy = np.sum(
        [counts[i] / np.sum(counts) * entropy(data.loc[data[split_name] == vals[i], target_name]) for i in
         range(len(counts))])
    return target_entropy - weighted_entropy


def find_best(data, target_name, features):
    max_gain = 0
    best_col = ""
    for col in features:
        gain = information_gain(data, col, target_name)
        if gain > max_gain:
            max_gain = gain
            best_col = col
    return best_col

1 Ответ

0 голосов
/ 16 декабря 2018

Я не могу дать полный ответ, но сделаю следующие наблюдения:

В DecisionTree.fit, внутри функции run_id3, вы добавляете к tree.children дважды, к одному из этих добавленийвызовы должны быть причиной значений None в дочерних узлах.

Это выглядит нормально, вы добавляете дерево:

tree.children.append(Tree(value=unique_targets[0]))

Это выглядит более подозрительно:

        for unique_val in np.unique(data[best_split]):
            new_tree = Tree()
            new_tree.value = unique_val
            tree.children.append(run_id3(data[data[best_split] == unique_val], target, features, new_tree))

Вы добавляете возвращаемое значение run_id3 к tree.children, но run_id3 не возвращает значение, а в функциях Python, которые не возвращают значение, возвращается None.run_id3 добавляет к списку дочерних элементов дерева, которое было передано ему, поэтому ваш код должен , вероятно, быть таким:

        for unique_val in np.unique(data[best_split]):
            new_tree = Tree()
            new_tree.value = unique_val
            run_id3(data[data[best_split] == unique_val], target, features, new_tree)

Пара незначительных стилевых точек:

class Tree():
    def __init__(self, children = [], label = None, value = None):
        self.children = children

Вам не нужны скобки после Tree, если только вы не хотите наследовать от другого класса, в этом случае у вас будет class Tree(Ancestor):...

Предоставление изменяемых аргументов по умолчанию в таких параметрах функции, как children=[], может иметь неожиданные эффекты, поэтому лучше избегать этой практики.Используйте эту идиому вместо:

class Tree: 

    def __init__(self, children=None, label=None, value=None):
        self.children = children if children is not None else []
        ...
...