Была классическая задача: по табличным данным предсказать некое событие — случится или нет. И как бы я к этим данным ни подбирался, с какого ракурса ни смотрел, результат, увы, не впечатлял. Данных было мало, а то, что было, обладало слабой предсказательной силой. Хотя казалось, что что-то вытащить все-таки можно.
И вот, просматривая отдельные деревья решений, меня осенило — попробую-ка я обрезать все деревья, используемые в Random Forest, до одной, но самой эффективной ветки. И — о чудо! — заметно выросла как точность (precision), так и полнота (recall). И особенно полнота выросла на высоких уровнях точности.
Проверил этот способ на других задачах. И везде при 100% точности заметно выростала полнота. Что же я сделал?
Идея
Обрезка или стрижка (pruning) дерева — это способ борьбы с переобучением. Обычно сводится к указанию критерия остановки роста (максимальная глубина дерева, минимальное количество объектов в листе и т. д.). Это так называемая предобрезка (pre-pruning), а по сути, просто ограничение роста.
Но есть ещё и _в_самом_делешная_ обрезка построенного дерева (post-pruning). Например, техника minimal cost complexity pruning. В моём случае она не давала каких-либо улучшений.
Эффект же дал совсем простой вариант, и даже скорее не обрезки, а именно выдергивания одной ветки из каждого дерева.
Пример дерева в моей задаче:
Видно, что объекты класса, который нужно предсказать, хорошо выделяются в том числе по самой правой ветке (листья синего цвета). Такая картина почти на всех деревьях. На тот момент у меня уже были обучены модели на других данных, и в качестве признаков я использовал и их вперемешку с исходными. Поэтому ожидаемо, что в листе правой ветки доминирует положительный класс почти всегда.
Интуитивно мне казалось, что в таком случае переобучение будет в меньшей степени приходиться на правую ветку и в большей степени — на оставшееся дерево. И что неплохо было бы собрать кучу из этих правых веток без каких-либо ограничений на их рост, пусть там в конце будет даже один объект, главное — побольше собрать таких веток.
И добавлять в кучу, конечно, не все ветки, а те, в которых нужный класс заметно доминирует (по опыту, его доля должна быть больше 80%). И не на данных для построения дерева, а на новых для дерева данных, этакий внутренний механизм перекрестной проверки.
И когда у нас уже будет куча отобранных таким образом веток, можно предсказывать на тестовых данных: чем в большем количестве веток новый объект поучаствует, тем выше вероятность отнесения его к целевому классу.
Вот так просто!
В итоге получил вот такие кривые точности и полноты для Random Forest и «кучи веток»:
Способ хорошо работает именно для увеличения полноты на высоких уровнях точности, т. е. в левой верхней области графика. Оно и понятно, ведь самим алгоритмом обрезки мы концентрируемся на этой области. Но будет ли работать данный алгоритм с обычными признаками, а не только как стэкинг выходов других моделей, как в моем случае?
Оказалось, что да! Причем, как и в случае с Random Forest, будет работать из коробки, в чем мы сейчас и убедимся.
Но сначала подробнее разберем сам алгоритм обрезки.
Random Branch
Класс RandomBranchClassifier можно использовать для бинарной классификации также, как RandomForestClassifier.
Код класса RandomBranchClassifier
import re
import numpy as np
import pandas as pd
from sklearn import tree
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.metrics import precision_score
from sklearn.model_selection import train_test_split
from rich.progress import track
class RandomBranchClassifier(BaseEstimator, ClassifierMixin):
def __init__(self, n_estimators = 4000, max_features = 14, dataset_size = None,
pruning_type = 'max_right', leaf_threshold = 0.8,
include_left = False, min_positive = 0,
tree_apply = 'cv', cv_size = 0.5, cv_stratify = False, cv_prec_threshold = 0,
criterion = 'entropy', splitter = 'best', min_samples_leaf = 1, max_depth = None,
min_samples_split = 2, max_leaf_nodes = None, min_weight_fraction_leaf = 0,
plot_trees = False, feature_names = None, remove_duplicates = 'n'):
self.n_estimators = n_estimators
self.max_features = max_features
self.dataset_size = dataset_size
self.pruning_type = pruning_type
self.leaf_threshold = leaf_threshold
self.include_left = include_left
self.min_positive = min_positive
self.tree_apply = tree_apply
self.cv_size = cv_size
self.cv_stratify = cv_stratify
self.cv_prec_threshold = cv_prec_threshold
self.criterion = criterion
self.splitter = splitter
self.min_samples_leaf = min_samples_leaf
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.max_leaf_nodes = max_leaf_nodes
self.min_weight_fraction_leaf = min_weight_fraction_leaf
self.plot_trees = plot_trees
self.feature_names = feature_names
self.remove_duplicates = remove_duplicates
self.features = []
self.conditions = []
self.conditions_a = []
self.conditions_b = []
self.tree_children_left = []
self.tree_children_right = []
self.tree_feature = []
self.tree_threshold = []
self.feature_count = []
self.nbranches = []
def fit(self, X, y):
if not isinstance(X, pd.DataFrame):
X = pd.DataFrame(X)
if not isinstance(y, pd.Series):
y = pd.Series(y)
if self.dataset_size != None:
if self.dataset_size < 1:
self.dataset_size = round(len(y)*self.dataset_size)
X = X.iloc[:self.dataset_size,:]
y = y[:self.dataset_size]
if self.feature_names != None:
self.features = self.feature_names
else:
self.features = X.columns
self.feature_count = [0] * len(self.features)
for i in track(range(self.n_estimators), description="Processing..."):
random_features = np.random.choice(len(self.features), size=self.max_features, replace=False)
selected_features = [self.features[idx] for idx in random_features]
if self.cv_stratify == True:
x_train, x_cv, y_train, y_cv = train_test_split(
X[selected_features], y, test_size=self.cv_size, random_state=i, stratify = y)
else:
x_train, x_cv, y_train, y_cv = train_test_split(
X[selected_features], y, test_size=self.cv_size, random_state=i)
clf = tree.DecisionTreeClassifier(criterion=self.criterion, min_samples_leaf=self.min_samples_leaf,
max_depth = self.max_depth,
min_weight_fraction_leaf = self.min_weight_fraction_leaf,
max_leaf_nodes = self.max_leaf_nodes,
min_samples_split = self.min_samples_split,
splitter = self.splitter)
clf.fit(x_train, y_train)
if self.tree_apply == 'cv':
leave_id = clf.apply(x_cv)
y_for_metrics = y_cv.to_list()
else:
leave_id = clf.apply(x_train)
y_for_metrics = y_train.to_list()
self.tree_children_left = clf.tree_.children_left
self.tree_children_right = clf.tree_.children_right
self.tree_feature = clf.tree_.feature
self.tree_threshold = clf.tree_.threshold
paths = {}
for leaf in np.unique(leave_id):
path_leaf = []
self.find_path(0, path_leaf, leaf)
paths[leaf] = np.unique(np.sort(path_leaf))
rules = {}
for key in paths:
rules[key] = self.get_rule(paths[key], selected_features)
y_pred = clf.predict(x_cv)
cv_prec_score = precision_score(y_cv, y_pred)
df_metrics = pd.DataFrame({'leave_id': leave_id, 'y': y_for_metrics})
df_metrics = df_metrics.groupby('leave_id').agg(positive=('y', 'sum'), total=('y', 'count')).reset_index()
df_metrics['precision'] = df_metrics['positive'] / df_metrics['total']
df_metrics.loc[df_metrics.positive < self.min_positive,'positive'] = 0
df_metrics['custome_score'] = (df_metrics['precision'] > self.leaf_threshold).astype(int) * df_metrics['positive']
if self.pruning_type == 'max_right' or self.pruning_type == 'max':
if cv_prec_score >= self.cv_prec_threshold and df_metrics.iloc[df_metrics['custome_score'].idxmax(), 4] != 0:
self.conditions.append(rules[df_metrics.iloc[df_metrics['custome_score'].idxmax(), 0].item()])
elif self.pruning_type == 'right' or self.pruning_type == 'all':
for j in range(len(rules)):
leaf_ratio = df_metrics.loc[df_metrics['leave_id'] == list(rules)[j],'precision'].item()
leaf_positive_count = df_metrics.loc[df_metrics['leave_id'] == list(rules)[j],'positive'].item()
if leaf_positive_count > self.min_positive and leaf_ratio >= self.leaf_threshold and cv_prec_score >= self.cv_prec_threshold:
self.conditions.append(rules[list(rules)[j]])
else:
print("Error: please choose correct pruning type")
return None
if self.plot_trees == True:
x_size = round(clf.tree_.node_count / 2)
plt.figure(figsize=(x_size,12))
tree.plot_tree(clf, fontsize=7, feature_names=selected_features, filled = True)
plt.show()
if self.pruning_type == 'max_right' or self.pruning_type == 'right':
if self.include_left == True:
left_conditions = [item for item in self.conditions if ">" not in item]
self.conditions = [item for item in self.conditions if "<" not in item]
self.conditions.extend(left_conditions)
else:
self.conditions = [item for item in self.conditions if "<" not in item]
for string in self.conditions:
self.feature_count = [count + res for count, res in zip(
self.feature_count, self.count_features_in_conditions(self.features, string))]
self.conditions_a = list(set(self.conditions))
self.conditions_b = self.remove_duplicates_from_conditions(self.conditions)
if self.remove_duplicates == 'a':
self.conditions = self.conditions_a
if self.remove_duplicates == 'b':
self.conditions = self.conditions_b
self.nbranches = len(self.conditions)
return self
def predict(self, X, threshold = 0.2, remove_duplicates = 'n', exclude = 'use_feature_name'):
y_proba = self.predict_proba(X, remove_duplicates = remove_duplicates, exclude = exclude)
y_pred = np.where(y_proba[:, 1] >= threshold, 1, 0)
return y_pred
def predict_proba(self, X, remove_duplicates = 'n', exclude = 'use_feature_name'):
if remove_duplicates == 'a':
conditions = self.conditions_a
elif remove_duplicates == 'b':
conditions = self.conditions_b
else:
conditions = self.conditions
d = 0
predictions = np.zeros((X.shape[0], 2))
df = X
df['count'] = 0
for i in range(len(conditions)):
if (len(conditions[i]) > 0) and (exclude not in conditions[i]):
d += 1
df.loc[eval(conditions[i]),'count'] += 1
predictions[:, 1] = df['count'] / d
predictions[:, 0] = 1 - predictions[:, 1]
return predictions
def find_path(self, node_numb, path, x):
path.append(node_numb)
if node_numb == x:
return True
left = False
right = False
if (self.tree_children_left[node_numb] !=-1):
left = self.find_path(self.tree_children_left[node_numb], path, x)
if (self.tree_children_right[node_numb] !=-1):
right = self.find_path(self.tree_children_right[node_numb], path, x)
if left or right :
return True
path.remove(node_numb)
return False
def get_rule(self, path, column_names):
mask = ''
for index, node in enumerate(path):
if index!=len(path)-1:
if (self.tree_children_left[node] == path[index+1]):
mask += "(df['{}']<= {}) \t ".format(column_names[self.tree_feature[node]], self.tree_threshold[node])
else:
mask += "(df['{}']> {}) \t ".format(column_names[self.tree_feature[node]], self.tree_threshold[node])
mask = mask.replace("\t", "&", mask.count("\t") - 1)
mask = mask.replace("\t", "")
return mask
def count_features_in_conditions(self, features, condition):
feature_counts = []
for feature in features:
pattern = r'\b{}\b'.format(feature)
matches = re.findall(pattern, condition)
feature_counts.append(len(set(matches)))
return feature_counts
def remove_duplicates_from_conditions(self, lst):
pattern = r"df\['(.*?)'\]"
extracted_lst = []
duplicates = set()
unique_lst = []
for item in lst:
match = re.findall(pattern, item)
extracted_str = " ".join(match)
extracted_lst.append(extracted_str)
if extracted_str in unique_lst:
duplicates.add(len(extracted_lst) - 1)
else:
unique_lst.append(extracted_str)
filtered_lst = [item for i, item in enumerate(lst) if i not in duplicates]
return filtered_lst
Три основных параметра — это pruning_type (тип обрезки), leaf_threshold (порог доли положительного класса в листе ветки) и min_positive (минимальное число объектов положительного класса в листе ветки).
Параметр pruning_type может быть right, max_right, max или all.
pruning_type = 'right'
— это как раз то, что я описывал выше. Берём все правые ветки (в условиях используется только знак «>»), в листе которых доля положительного класса выше leaf_threshold и объектов класса больше min_positive.
pruning_type = 'max_right'
— это тоже самое, что и 'right', только на ветку накладывается дополнительное условие: берем правую ветку, если в её листе больше всего объектов положительного класса среди всех листьев, удовлетворяющих условию доли и минимального наличия объектов. Проще говоря, правая ветка должна быть не только плодовитой на наш класс, но и самой обильной из всех плодовитых в дереве. Этот тип обрезки оказался ещё эффективнее, чем 'right'. Он дополнительно отсекает, пусть и удовлетворяющие критерию доли, но слабые ветки.
pruning_type = 'max'
— из всех веток в дереве, в листе которых доля положительного класса выше leaf_threshold, берём ту, в листе которой объектов класса максимальное и больше min_positive. Т.е. это как 'max_right', только без 'right'.
pruning_type = 'all'
— это все ветки дерева, в листе которых доля положительного класса выше leaf_threshold и объектов класса больше min_positive. Если leaf_threshold = 0 и min_positive = 0, то это просто все ветки всех деревьев. Но все ветки всех деревьев не делают Random Branch аналогичным Random Forest, так как у Random Branch иной механизм предсказания. Этот тип обрезки работает, если увеличивать значение min_positive.
Использование условия min_positive не аналогично использованию ограничения на рост дерева (min_samples_leaf). Используя min_positive, мы работаем с уже построенным деревом. И не берём ветку, если в её листе меньше объектов положительного класса, чем задано min_positive.
В целом, по опыту нескольких задач, самый эффективный и стабильный вариант — max_right, потом right. Варианты 'max' и 'all' не очень эффективны. Хотя вариант 'all' в сочетании с увеличением min_positive тоже неплох.
Поэтому по дефолту pruning_type = 'max_right', leaf_threshold = 0.8, min_positive = 0
.
Другие параметры (в скобках значения по дефолту):
n_estimators [ = 4000]
— количество создаваемых деревьевmax_features [ = 14]
— максимальное количество признаков случайно выбираемое каждый раз для создания дерева и поиска лучшего разбиенияremove_duplicates [ = 'n' ]
— удалять дубликаты выделяемых условий (правил, веток) или нет. n — не удалять; a — удалять точные дубликаты, когда совпадают как последовательность признаков, так и пороговые значения; b — удалять дубликаты по последовательности признаков, когда последовательность признаков совпадает, а вот пороговые значения могут быть разными. Это параметр для GridSearchCV. По факту, можно его не трогать, а когда используете метод predict или predict_proba указать там параметр remove_duplicatesdataset_size [ = None]
— ограничение размера выборки, встроил для использования с GridSearchCV. Можно указать либо долю от исходного датасета, либо конкретное значение размераinclude_left [ = False]
— включать левые ветки или нет. Только для вариантов обрезки 'right' или 'max_right'. Параметр не влияет на эффективность, но можно снизить количество создаваемых деревьевtree_apply [ = 'cv' ]
— важный параметр. Может быть 'cv' или 'train'. Если 'train', то долю нужного класса в листе считать на тех же данных, которые использовались для построения дерева. А если 'cv', то на новых для дерева данныхcv_size [ = 0.5 ]
— указывается доля от имеющихся данных для перекрестной проверкиcv_stratify [ = False ]
— если cv_stratify = True, то пропорции классов при разбиении данных на тренировочные и для перекрестной проверки будут одинаковыеcv_prec_threshold [ = 0 ]
— ещё одно возможное условие для отбора веток. На данных для перекрестной проверки считается точность предсказаний всего дерева. Данный параметр устанавливает порог точности, который дерево должно преодолеть, чтобы из него можно было брать ветки. Использую либо 0, либо минимальный. Параметр особо не влиял, но пока оставил. Фильтр слабых деревьевplot_trees [ = False ]
— если True, то в процессе обучения выводит графики с деревьями
А это параметры для построения самого дерева решений, которые по дефолту такие же, как в DecisionTreeClassifier (за исключением criterion, его поменял на entropy):
criterion [ = 'entropy' ]
splitter [ = 'best' ]
min_samples_leaf [ = 1 ]
max_depth [ = None ]
min_samples_split [ = 2 ]
max_leaf_nodes [ = None ]
min_weight_fraction_leaf [ = 0 ]
Влияние на результат может оказать параметр splitter, который указывает стратегию разделения на каждом узле. Может быть best или random.
У методов predict и predict_proba есть параметры:
remove_duplicates [ = 'n' ]
— описал выше, можно не обучать заново модель, а предсказывать, используя отобранные ветки как с дубликатами, так и безexclude
— можно указать название признака, чтобы исключить все ветки с ним при предсказанииthreshold [ = 0.2 ]
— у метода predict есть параметр порога отнесения к нужному классу
После обучения модели все условия (правила, ветки) будут в self.conditions (а в self.nbranches — количество таких веток), без полных дубликатов — в self.conditions_a, без дубликатов последовательности признаков — в self.conditions_b.
Обучение модели происходит так:
Среди набора признаков выбираем m (max_features) случайных.
Случайным образом разделяем данные для тренировки и для перекрестной проверки.
Строим дерево, оптимальное разделение ищем среди случайно отобранных признаков.
Применяем данное дерево либо к тем же данным, на которых оно строилось, либо к данным для перекрестной проверки.
Используем функции find_path и get_rule, чтобы извлечь все условия (правила, ветки) из дерева в словарь (rules). Считаем количество объектов положительного класса и их долю для каждого условия (результаты в df_metrics).
Отбираем те правила, которые удовлетворяют всем ограничениям (в self.conditions).
Повторяем всё n раз (n_estimators ). Да, n_estimators в классе привязан к random_state функции разбиения train_test_split.
Также внутри класса есть функция count_features_in_conditions, которая подсчитывает сколько раз какие признаки встречались в отобранных условиях. Записывает результат в self.feature_count. Потом можно посмотреть частоту использования каждого признака.
Функции find_path и get_rule взял у пользователя vlemaistre (спасибо ему!) на сайте Stack Overflow. А это документация по структуре дерева.
Тестируем!
Для теста возьмем несколько датасетов с Kaggle и сравним Random Branch с базовыми решениями Random Forest. В специфику данных не углублялся.
Первая задача — предсказание оттока клиентов (для скачивания данных нужна регистрация).
Собственно, код для преобразований и параметры для Random Forest взял из этого базового решения:
Код
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import precision_recall_curve
from sklearn.model_selection import GridSearchCV
import matplotlib.pyplot as plt
df = pd.read_csv("C:/WA_Fn-UseC_-Telco-Customer-Churn.csv")
def object_to_int(dataframe_series):
if dataframe_series.dtype == 'object':
dataframe_series = LabelEncoder().fit_transform(dataframe_series)
return dataframe_series
df = df.apply(lambda x: object_to_int(x))
x = df.drop(columns=['Churn'])
y = df['Churn'].values
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.3, random_state=40, stratify=y)
# Обучение модели Random Forest
model = RandomForestClassifier(n_estimators=1000, oob_score=True, n_jobs=-1,
random_state=50,
max_leaf_nodes=30)
model.fit(x_train, y_train)
y_pred_proba = model.predict_proba(x_test)[:, 1]
precision_rf, recall_rf, thresholds_rf = precision_recall_curve(
y_test, y_pred_proba)
fig, ax = plt.subplots()
ax.plot(recall_rf, precision_rf, color='purple')
ax.set_title('Precision-Recall Curve')
ax.set_ylabel('Precision')
ax.set_xlabel('Recall')
plt.show()
Теперь попробуем вариант с Random Branch. Но для начала продублируем все признаки со знаком «-», чтобы в случаях отрицательной корреляции с нужным нам классом этот признак тоже мог попасть в правую ветку.
Для этого используем функцию add_rev:
Код
def add_rev(df):
if not isinstance(df, pd.DataFrame):
df = pd.DataFrame(df)
df = df.add_prefix('ft_')
new_df = df.apply(lambda x: x * -1)
new_df = new_df.add_prefix('rev_')
return pd.concat([df, new_df], axis=1)
x_train_rev = add_rev(x_train)
x_test_rev = add_rev(x_test)
Далее обучим Random Branch с дефолтными параметрами, изменил только n_estimators на 10 000. А если использовать include_left = True, то n_estimators можно выставить на 5 000.
Код
model = RandomBranchClassifier(n_estimators=10000, max_leaf_nodes=30)
model.fit(x_train_rev, pd.Series(y_train))
print("Количество условий: ", model.nbranches)
print("Количество условий без полных дубликатов: ", len(model.conditions_a))
print("Количество условий без дубликатов последовательности признаков: ", len(model.conditions_b))
Вот пример дерева, где правая ветка удовлетворяет всем условиям варианта max_right с параметром leaf_threshold = 0.8. Доля больше 80%, а количество объектов положительного класса максимальное среди всех листьев с такой же долей положительного класса.
Теперь предскажем без дубликатов последовательности признаков (вариант b):
Код
y_pred_proba = model.predict_proba(x_test_rev, remove_duplicates='b')[:, 1]
precision_rb, recall_rb, thresholds_rb = precision_recall_curve(
y_test, y_pred_proba)
fig, ax = plt.subplots()
ax.plot(recall_rf, precision_rf, color='green', label="Random Forest")
ax.plot(recall_rb, precision_rb, color='purple', label="Random Branch")
ax.legend(loc="upper right")
ax.set_title('Precision-Recall Curve')
ax.set_ylabel('Precision')
ax.set_xlabel('Recall')
plt.show()
При 100% точности заметно подросла полнота. На 10 000 построенных деревьев мы отобрали всего 263 ветки, без дубликатов 228, а без дубликатов последовательности признаков (вариант b) всего 140. В этом ещё одна фишка метода, всего 140 условий, чтобы предсказывать со 100% точностью.
Теперь посмотрим частоту использования признаков:
Код
from IPython.display import display_html
feature_frequency = pd.DataFrame()
feature_frequency['feature'] = model.features
feature_frequency['counts'] = model.feature_count
feature_frequency['counts'] = feature_frequency['counts'] / model.nbranches
df_rev = feature_frequency[feature_frequency['feature'].str.startswith('rev_')]
df_not_rev = feature_frequency[~feature_frequency['feature'].str.startswith('rev_')]
df1_styler = df_not_rev.style.bar(
subset=['counts'],
align='mid',
color=['coral', 'yellowgreen'],
vmin=feature_frequency['counts'].min(),
vmax=feature_frequency['counts'].max()
).set_table_attributes("style='display:inline'").set_caption('Direct')
df2_styler = df_rev.style.bar(
subset=['counts'],
align='mid',
color=['coral', 'yellowgreen'],
vmin=feature_frequency['counts'].min(),
vmax=feature_frequency['counts'].max()
).set_table_attributes("style='display:inline'").set_caption('Reverse')
display_html(df1_styler._repr_html_()+df2_styler._repr_html_(), raw=True)
Слева исходные, справа продублированные со знаком «-»:
А это пример использования GridSearchCV. Как метрику используем полноту при 100% точности:
Код
model_empty = RandomBranchClassifier()
params = {'n_estimators': [10000],\
'tree_apply': ['cv'],\
'cv_size': [0.3,0.5],\
'pruning_type': ['max_right'],\
'cv_stratify': [True],\
'max_leaf_nodes': [30],\
'leaf_threshold': [0.8],\
'splitter': ['best'],\
'remove_duplicates': ['b']}
def recall_prec100_score(model, X, y):
y_proba = model.predict_proba(X)
precision, recall, thresholds = precision_recall_curve(y, y_proba[:, 1])
index = next((idx for idx, val in enumerate(precision) if val == 1), None)
r_100 = recall[index]
return r_100
scoring = {"recall_100": recall_prec100_score}
gs = GridSearchCV(model_empty, params, cv = 5, scoring=scoring, verbose = 3, refit="recall_100", n_jobs=-1)
gs.fit(x_train_rev, y_train)
gs.best_params_
Результаты можно отсортировать по метрике, чтобы сразу увидеть топ по параметрам:
Код
cvres = gs.cv_results_
sorted_indices = np.argsort(cvres['mean_test_recall_100'])[::-1]
for idx in sorted_indices:
score = cvres['mean_test_recall_100'][idx]
params = cvres['params'][idx]
print(round(score, 3), params)
Какие параметры следует смотреть?
'pruning_type': ['right', 'max_right','max','all']
'tree_apply': ['train','cv']
'cv_size': [0.1,0.3,0.5]
'leaf_threshold': [0.7,0.8,0.9]
'splitter': ['random','best']
'remove_duplicates': ['n','a','b']
'max_features'
'min_positives'
'cv_prec_threshold': [0,0.1,0.2...]
'cv_stratify': [True,False]
'n_estimators'
Файл с кодом можно скачать тут.
Вторая задача, которую я попробовал, — это предсказание сердечных заболеваний. Базовое решение взял это.
Код приводить уже не буду, проделал то же самое, Random Branch использовал с такими параметрами: n_estimators = 4000, tree_apply = 'cv', pruning_type = 'max_right', cv_stratify = True, leaf_threshold = 0.8
. По сути, тоже дефолтные. Предсказание без дубликатов последовательности признаков (вариант b). На 4 000 деревьев получилось 126 условий в варианте b. Почти столько же, сколько в задаче предсказания оттока на 10 000 деревьев.
Снова выросла полнота при 100% точности.
Random Branch работает с обычными признаками (а не только как стэкинг), потому что зачастую у нас есть связь: чем больше (или меньше) значение признака, тем больше вероятность какого-либо класса. Бывает, что даже один признак способен разделить данные так, что мы будем классифицировать несколько наблюдений со 100% точностью. Сочетая признаки, мы увеличиваем силу классификации.
Но особенность данного алгоритма в том, что мы концентрируемся только на краях пространства признаков, исходя из предположения, что там больше всего наблюдений, которые можно отличить со 100% точностью. И, как показывает практика, это предположение себя оправдывает.
Мне этот способ напоминает снятие пенки.
Комментарии (4)
katorovalexey
16.03.2024 01:10Интересный подход. Можно будет потестировать. Еще бы посмотреть на сравнение с бустингами
ValeriyPushkarev
iwtkl Автор
В исходной задаче, где в числе признаков были выводы других моделей, см. первый график.
ValeriyPushkarev
Вы используете субоптимальный алгоритм.
Я как-то раз предлагал сделать улучшения в стиле IsAppliciable - т.е. если вы можете быстро проверить что ваш алгоритм может немного улучшить результат - проверяйте, применяйте.
Правда, толка почти не будет.