Машинное обучение — практическая реализация случайных лесов из деревьев решений

машинное обучение

Эта статья возникла из личного публичного аккаунта:TechFlow, оригинальность это не просто, прошу внимания


СегодняТемы машинного обученияВ 26-й статье поговорим о другой модели обучения ансамбля, которой является знаменитый случайный лес.

Случайный лес хорошо известен и широко используется в отрасли, а также занял первые места во многих конкурсах алгоритмов. Кроме того, это также классическая модель построения сильных классификаторов путем объединения нескольких слабых классификаторов, поэтому она широко используется в отрасли.популярный.

Эта статья основана на статьях, связанных с деревьями решений. Студенты, которые не читали ее, могут прочитать ее излучший альбомОзнакомьтесь с прошлыми статьями о деревьях решений.

Принцип алгоритма

В предыдущей статье, когда я представил AdaBoost, я кратко представил несколько идей ансамблевого обучения, в основном существует три метода ансамблевого обучения.Бэгинг, бустинг и укладка. AdaBoost относится к Boosting, а Random Forest — к Bagging.

Самая большая особенность Бэгинга — это «демократия», идею которой легко понять Для одних и тех же обучающих данных мы будем обучать несколько слабых классификаторов. Столкнувшись с новой выборкой, эти классификаторы объединяются, чтобы демократически проголосовать,Все классификаторы равны, они имеют одинаковый вес. Наконец, окончательный результат интегрируется на основе результатов всех этих слабых классификаторов. Например, всего мы обучили слабых классификаторов 50. При столкновении с выборкой 35 из них дают категорию 1 и 15 дают категорию 0, тогда результатом всей модели является категория 1.

Другой момент заключается в том, что данные, используемые Бэггингом для обучения каждого классификатора, не фиксированы, авыборка с заменойвыбрано. Поэтому распределение выборок в обучающей выборке также различно: некоторые выборки могут встречаться в одном и том же классификаторе несколько раз, а есть и не попавшие. Одной только случайности выборок недостаточно, потому что распределение обучающих выборок в целом похожее, хотя мы и делаем случайную выборку, это не сделает каждую обучающую выборку большим разрывом. Поэтому, чтобы гарантировать, что каждый классификатор имеет разную направленность и более сильную случайность, мы также можем начать с функций и ограничитьКаждый классификатор может использовать только часть признаков случайным образом.. Характеристики каждого классификатора, полученного таким образом, разные, и фокус тоже разный, что может максимально усилить случайный эффект и позволить модели сфокусироваться на данных.

Название модели случайного леса скрывает два ключевых момента, а именно случайный и лесной. Случайность, как мы уже объясняли, — это, с одной стороны, случайность каждой выборки классификатора, а с другой — случайность признаков, которые классификатор может использовать. Лес также хорошо изучен, потому что классификатор, который мы используем, представляет собой дерево решений, поэтому модель, состоящая из нескольких «деревьев» решений, естественно, является лесом.

Благодаря этим двум функциям случайный лес легко понять и легко реализовать, ведь мы уже несколько раз реализовывали модель дерева решений.

Код

Мы выбираем самый классический алгоритм CART среди деревьев решений для реализации деревьев решений, и мы по-прежнему используем данные для прогнозирования рака молочной железы в последней модели AdaBoost.

Прежде всего, мы все те же, сначала читаем данные:

import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer

breast = load_breast_cancer()
X, y = breast.data, breast.target
# reshape,将一维向量转成二维
y = y.reshape((-1, 1))

Затем мы используем инструмент train_test_split в библиотеке sklearn, чтобы разделить данные на обучающие и тестовые данные.

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=23)

Основной структурой случайного леса по-прежнему является дерево решений, мы можем напрямуюСледуйте коду предыдущего дерева решений CART., с небольшими изменениями. Первая — это функция для расчета индекса GIni и разделения данных по выбранным признакам, эти две функции не нуждаются в каких-либо изменениях, мы скопировали их из предыдущей статьи.

from collections import Counter

def gini_index(dataset):
    dataset = np.array(dataset)
    n = dataset.shape[0]
    if n == 0:
        return 0
    
    counter = Counter(dataset[:, -1])
    ret = 1.0
    for k, v in counter.items():
        ret -= (v / n) ** 2
    return ret

def split_gini(dataset, idx, threshold):
    left, right = [], []
    n = dataset.shape[0]
    # 根据阈值拆分,拆分之后计算新的Gini指数
    for data in dataset:
        if data[idx] <= threshold:
            left.append(data)
        else:
            right.append(data)
    left, right = np.array(left), np.array(right)
    # 拆分成两半之后,乘上所占的比例
    return left.shape[0] / n * gini_index(left) + right.shape[0] / n * gini_index(right)

Затем идет разделенный набор данных, где мы разделяем набор данных на две части на основе функций и пороговых значений. Подобна функции split_gini выше, за исключением того, что функция split_gini вычисляет индекс Джини после разделения, и то, что мы сейчас разрабатываем,разделить набор данныхфункция.

from collections import defaultdict

def split_dataset(dataset, idx, thread):
    splitData = defaultdict(list)
    # 否则根据阈值划分,分成两类大于和小于
    for data in dataset:
        splitData[data[idx] <= thread].append(np.delete(data, idx))
    return list(splitData.values()), list(splitData.keys())

Затем есть функции для получения всех порогов и выбора лучших признаков и разделения порогов, Логика этих двух функций точно такая же, как и раньше, и мы их также скопировали.

def get_thresholds(X, idx):

    # numpy多维索引用法
    new_data = X[:, [idx, -1]].tolist()
    # 根据特征值排序
    new_data = sorted(new_data, key=lambda x: x[0], reverse=True)
    base = new_data[0][1]
    threads = []

    for i in range(1, len(new_data)):
        f, l = new_data[i]
        # 如果label变化则记录
        if l != base:
            base = l
            threads.append(f)

    return threads


def choose_feature_to_split(dataset):
    n = len(dataset[0])-1
    m = len(dataset)
    # 记录最佳Gini,特征和阈值
    bestGini = float('inf')
    feature = -1
    thred = None
    for i in range(n):
        threds = get_thresholds(dataset, i)

        for t in threds:
            # 遍历所有的阈值,计算每个阈值的Gini
            gini = split_gini(dataset, i, t)
            if gini < bestGini:
                bestGini, feature, thred = gini, i, t
    return feature, thred

Затем идет строительная часть, здесь мы вносим небольшие коррективы. из-за СШАИспользуется только часть обучающих данных и часть признаковОбученное дерево решений, поэтому, как правило, нет переоснащения, поэтому я удалил логику предварительной обрезки, чтобы предотвратить переоснащение. Еще одно изменение — добавление настроек резервного копирования из-за разреженности функций и данных,Могут быть случаи, когда узел имеет только одну ветвь. Чтобы предотвратить это, я храню резервную копию в каждом узле, которая представляет наиболее часто встречающуюся категорию в данных текущего узла.

def create_decision_tree(dataset):
    dataset = np.array(dataset)
    # 如果都是一类,那么直接返回类别
    counter = Counter(dataset[:, -1])
    if len(counter) == 1:
        return dataset[0, -1]
    
    # 如果已经用完了所有特征
    if dataset.shape[1] == 1:
        return counter.most_common(1)[0][0]
    
    # 记录最佳拆分的特征和阈值
    fidx, th = choose_feature_to_split(dataset)
    
    node = {'threshold': th, 'feature': fidx, 'backup': counter.most_common(1)[0][0]}
    
    split_data, vals = split_dataset(dataset, fidx, th)
    for data, val in zip(split_data, vals):
        node[val <= th] = create_decision_tree(data)
    return node

Затем есть две функции полезности: первая — это функция прогнозирования одного поддерева для одной выборки, а вторая — функция прогнозирования одного поддерева для группы выборок. Общая логика аналогична предыдущей, мы можем просто посмотреть код напрямую:

def subtree_classify(node, data):
    key = node['feature']
    pred = None
    thred = node['threshold']
    
    # 如果当前的分支没有出现过,那么返回backup
    if (data[key] < thred) not in node:
        return node['backup']

    if isinstance(node[data[key] < thred], dict):
        pred = subtree_classify(node[data[key] < thred], data)
    else:
        pred = node[data[key] < thred]
            
    # 防止pred为空,挑选一个叶子节点作为替补
    if pred is None:
        for key in node:
            if not isinstance(node[key], dict):
                pred = node[key]
                break
    return pred


def subtree_predict(node, X):
    y_pred = []
    # 遍历数据,批量预测
    for x in X:
        y = subtree_classify(node, x)
        y_pred.append(y)
    return np.array(y_pred)

На самом деле это часть дерева решений.После того, как дерево решений реализовано, часть построения леса очень проста. только одно,Случайные выборки и признаки, затем создайте новое дерево решений со случайными выборками и признакамии запись.

Сначала мы рассмотрим функцию случайных выборок и функций:

def get_random_sample(X, y):
    rnd_idx = np.random.choice(range(X.shape[0]), 350, replace=True)
    ft_idx = np.random.choice(range(X.shape[1]), 15, replace=False)
    x_ret = X[rnd_idx][:, ft_idx]
    # 将类别拼接到X当中
    x_ret = np.hstack((x_ret, y[rnd_idx]))
    # 对特征的下标进行排序
    ft_idx.sort()
    return x_ret, np.array(ft_idx)

В этой функции мы устанавливаем количество выборок равным 350 и количество признаков равным 15 каждый раз, когда обучается новое дерево решений. Таким образом гарантируется случайность построенного дерева решений.

После того, как случайная выборка сделана, код для построения леса очень прост, просто создайте дерево и запишите его в цикле:

trees_num = 20
subtrees = []

for i in range(trees_num):
    X_rnd, features = get_random_sample(X_train, y_train)
    node = create_decision_tree(X_rnd)
    # 记录下创建出来的决策树与用到的特征
    subtrees.append((node, features))

Мы случайным образом используем некоторые функции при создании дерева, мы находимся вВ последующих прогнозах необходимо использовать те же функции для прогнозирования. Поэтому нам нужно записать, какие значения функций используются в каждом дереве, а в дереве решений записываются только индексы функций, поэтому нам нужно отсортировать эти функции перед записью.

После того, как эти функции реализованы, используется метод прогнозирования, Согласно определению Бэгинга, для каждой выборки нам нужно получить классификацию каждого дерева решений. Затем выберите тот, у которого наибольшее число, как окончательный результат прогноза.Эта логика не сложна.Я думаю, каждый может понять это, посмотрев код.

def predict(X, trees):
    y_pred = []
    for x in X:
        ret = []
        for tree, features in trees:
            ret.append(subtree_classify(tree, x[features]))
        ret = Counter(ret)
        y_pred.append(ret.most_common(1)[0][0])
    return np.array(y_pred)

Наконец, мы проверяем влияние модели на тестовый набор:

Показатель точности составляет 65,7%, что не кажется высоким и несколько отличается от того, что мы ожидали.Давайте посмотрим на эффект одиночного дерева в лесу:

Будет обнаружено, что точность некоторых деревьев составляет только более 30%, и ни одно из них не достигло теоретического значения 50%. Этому есть много причин: с одной стороны, случайность нашего выбора признаков приводит к тому, что мы выбираем какие-то менее эффективные признаки. С другой стороны, это также связано с количеством образцов, которые мы обучаем.В этом примере мыСлишком мало доступных образцов, поэтому случайность очень большая, а отклонение, естественно, не малое.

Кроме того, мы можем видеть эффект вызова случайного леса в sklearn, Мы также устанавливаем количество деревьев решений в лесу равным 40 и выбираем индекс Джини в качестве основы для разделения выборок.

Мы видим, что его точность даже ниже нашей собственной разработки, что, в конце концов, нормально.Модели не универсальны., будут некоторые ситуации, когда это неприменимо или эффект будет плохим, что очень нормально.

Суммировать

Самой большой особенностью модели случайного леса является случайность, На самом деле, принцип очень близок к принципу AdaBoost, Мы используем случайные выборки и функции, чтобы обеспечить случайность модели и убедиться, что модель, обученная таким образом, является слабый классификатор. Помимо слабого эффекта, у слабого классификатора есть и отличная особенность:Не может быть переобучен, то есть все они недообучающие модели, а переподгонка, естественно, невозможна. Мы получаем сильные результаты классификации, интегрируя большое количество слабых классификаторов.

По сравнению с AdaBoost,Случайный лес более случайный, и имеет более высокую зависимость от параметров, количества деревьев решений в лесу, количества функций, которые должно использовать каждое дерево решений, и стратегии сокращения. Эти факторы повлияют на эффект конечной модели, но у нее также есть преимущества, которых нет у AdaBoost, из-за случайности мы можемРаскройте некоторые комбинации признаков, которые не могут быть найдены обычными методами.Или уникальное распределение данных в выборке. Вот почему после правильной настройки параметров случайные леса часто могут достигать очень хороших результатов.

На этом введение в случайный лес подходит к концу. Если вам понравилась эта статья, если сможете, пожалуйстаобращать внимание, подбодрите меня и облегчите доступ к другим статьям.

В этой статье используетсяmdniceнабор текста