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

Вы можете задаться вопросом, чем случайный лес отличается от единого дерева решений (CART). Основное различие между случайным лесом и единым деревом решений (CART) заключается в ансамблевом подходе. Случайный лес объединяет несколько деревьев решений, каждое из которых обучено на случайном подмножестве данных и функций, чтобы уменьшить переобучение и повысить точность прогнозирования. CART, с другой стороны, строит единое дерево решений на основе всего набора данных, которое может быть более подвержено переоснащению.

По сути, случайный лес построен на двух основных концепциях: упаковке и случайном подпространстве:

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

Какова функция потерь в случайном лесу?

Функция потерь в случайном лесу и функция потерь в CART одинаковы и определяются параметром criterion. Подробную информацию вы можете прочитать в разделе о функции потерь в статье CART.

Как определяется окончательный прогноз в случайном лесу?

Хотя каждое дерево предсказывает результат, вы, возможно, задаетесь вопросом, как определяется окончательный прогноз. Давайте объясним это!
В случайном лесу «голосование» и «усреднение» — это два разных метода, используемых для объединения прогнозов, сделанных отдельными деревьями решений в ансамбле. Выбор между голосованием и усреднением зависит от того, работаете ли вы над задачей классификации или регрессии.

  1. Голосование (задачи классификации): в задачах классификации случайный лес использует механизм голосования для определения окончательного прогноза. Каждое дерево решений в ансамбле предоставляет свой собственный прогноз класса, и класс, который получает большинство голосов среди деревьев, выбирается в качестве окончательного прогнозируемого класса.

??? ​ ​ ​ ​ ​ ​ ​ ​ ​и 70 из них предсказывают класс A, а 30 прогнозируют класс B для ​ ​​ ​ ​ ​​​​​ ​  ​  ​
​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ Согласно конкретным данным, окончательным прогнозом ансамбля будет
         Класс А, поскольку он получил большинство голосов.

​ ​ ​ ​ ​ ​ ​ ​ -​​​​​ Этот механизм голосования помогает улучшить общую классификацию
​ ​ ​ ​ ​​​ ​ ​ ​ ​ ​ точность и делает модель менее склонной к неправильным ​ ​​​​​​​​​​​ ???

2. Усреднение (задачи регрессии): В задачах регрессии используется случайный лес
​​​​​​​​​​​​​​​​​​​​​​​ Для определения окончательного прогноза используется метод усреднения. Каждое
​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​..»» ​​​ и окончательный прогноз получается путем усреднения этих числовых
​​​​​​​​ ​​​​​ ​​​​​ прогнозов.

​ ​ ​ ​ ​ ​ -​​​​​ Например, если у вас есть случайный лес со 100 деревьями решений,
​ ​​​ ​ ​ ​ ​ ​ ​ ​ ​и каждое дерево предсказывает различное числовое значение для конкретного
​ ​ ​ ​ ​ ​​ ​ ​ ​ ​ ​ ​точка данных, окончательный прогноз ансамбля является средним из всех этих
       Численные прогнозы.

??? ??? Регрессионные прогнозы.

Кодирование

Давайте немного напишем код, используя набор данных диабет!



ПРИМЕЧАНИЕ: Поскольку здесь мы будем моделировать дерево решений классификации, мы импортируем RandomForestClassifier из SciKit Learn. Однако для задач регрессии мы должны импортировать RandomForestRegressor

################################################
# IMPORT
################################################

import warnings
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV, cross_validate, validation_curve
from sklearn.preprocessing import MinMaxScaler
from sklearn.impute import KNNImputer

pd.set_option('display.max_columns', None)
pd.set_option('display.width', 500)
warnings.simplefilter(action='ignore', category=Warning)

df = pd.read_csv("datasets/diabetes.csv")




####################################
# FUNCTIONS
####################################

def outlier_thresholds(dataframe ,col ,q1=.05 , q3=.95, decimal=3):
    quartile1=dataframe[col].quantile(q1)
    quartile3=dataframe[col].quantile(q3)
    iqr=quartile3-quartile1
    low_limit= round(quartile1 - (iqr*1.5) , decimal)
    up_limit= round(quartile3 + (iqr*1.5), decimal)
    return low_limit , up_limit



def replace_with_thresholds(dataframe, col_name, q1=.05, q3=.95, lower_limit = None, upper_limit = None):
    low_limit, up_limit = outlier_thresholds(dataframe, col_name, q1, q3)
    if lower_limit != None:
        dataframe.loc[(dataframe[col_name] < lower_limit), col_name] = lower_limit
    else:
        dataframe.loc[(dataframe[col_name] < low_limit), col_name] = low_limit

    if upper_limit != None:
        dataframe.loc[(dataframe[col_name] > upper_limit), col_name] = upper_limit
    else:
        dataframe.loc[(dataframe[col_name] > up_limit), col_name] = up_limit



def plot_importance(model, features, num:int = 0, save=False):
    if num <= 0:
        num = X.shape[1]
    feature_imp = pd.DataFrame({'Value': model.feature_importances_, 'Feature': features.columns})
    plt.figure(figsize=(10, 10))
    sns.set(font_scale=1)
    sns.barplot(x="Value", y="Feature", data=feature_imp.sort_values(by="Value",
                                                                     ascending=False)[0:num])
    plt.title('Features')
    plt.tight_layout()
    plt.show()
    if save:
        plt.savefig('importances.png')



def val_curve_params(model, X, y, param_name, param_range, scoring="accuracy", cv=5):
    train_score, test_score = validation_curve(
        model, X=X, y=y, param_name=param_name, param_range=param_range, scoring=scoring, cv=cv)

    mean_train_score = np.mean(train_score, axis=1)
    mean_test_score = np.mean(test_score, axis=1)

    plt.plot(param_range, mean_train_score,
             label="Training Score", color='b')

    plt.plot(param_range, mean_test_score,
             label="Validation Score", color='g')

    plt.title(f"Validation Curve for {type(model).__name__}")
    plt.xlabel(f"Number of {param_name}")
    plt.ylabel(f"{scoring}")
    plt.tight_layout()
    plt.legend(loc='best')
    plt.show(block=True)







###########################################
# DATA PREPROCESSING
###########################################

#Replacing outliers.
cols = [col for col in df.columns if col != "Outcome"]
for col in cols:
    replace_with_thresholds(df, col)



#Columns that cannot contain zero
problematic_cols = [col for col in df.columns if col not in ["Pregnancies",'DiabetesPedigreeFunction','Outcome']]


#Now replace these zeros with NaN
for col in problematic_cols:
    df[col]=df[col].replace(0,np.nan)


# Filling NaN values by using KNN Imputer
scaler=MinMaxScaler()
df=pd.DataFrame(scaler.fit_transform(df), columns=df.columns) 
imputer=KNNImputer(n_neighbors=5)
df=pd.DataFrame(imputer.fit_transform(df), columns=df.columns)
df=pd.DataFrame(scaler.inverse_transform(df), columns=df.columns)







##################################################
# FEATURE ENGINEERING
##################################################



df.loc[(df["Age"] <= 18 ), "NEW_AGE"] = "young"
df.loc[(df["Age"] > 18 ) & (df["Age"] <= 24), "NEW_AGE"] = "adult"
df.loc[(df["Age"] > 24 ) & (df["Age"] <= 59), "NEW_AGE"] = "mid_adult"
df.loc[(df["Age"] > 59), "NEW_AGE"] = "senior"



df.loc[(df["BMI"] < 18.5) , "BMI_CAT"] ="underweight"
df.loc[(df["BMI"] >= 18.5) & (df["BMI"] < 24.9) , "BMI_CAT"] ="normal"
df.loc[(df["BMI"] >= 24.9) & (df["BMI"] < 29.9) , "BMI_CAT"]="overweight"
df.loc[(df["BMI"] >= 29.9) , "BMI_CAT"] ="obese"



df.loc[(df["Insulin"] < 15) , "INSULIN_CAT"] ="low"
df.loc[(df["Insulin"] >= 15) & (df["Insulin"] < 166) , "INSULIN_CAT"] ="normal"
df.loc[(df["Insulin"] >= 166) , "INSULIN_CAT"] ="high"


# One Hot Encoding
ohe_cols = [col for col in df.columns if 10 >= df[col].nunique() > 2]
df= pd.get_dummies(df,columns= ohe_cols, drop_first=True)


X = df.drop(["Outcome"], axis=1)
y = df["Outcome"]

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

def feature_selecter(input_x, y, candidate_features_dict:dict, candidate_features_id:list, best_features:list, best_accuracy=0, verbose=True):
    if not candidate_features_id:
        return best_accuracy, best_features
    best_x = input_x   
    best_feature= -1 
    if best_accuracy == 0:
        rf_model = RandomForestClassifier(random_state=17).fit(input_x, y)
        
        cv_results = cross_validate(rf_model,
                                    input_x, y,
                                    cv=5,
                                    scoring="accuracy")

        best_accuracy = cv_results["test_score"].mean()

    if verbose:
       print(f"best accuracy(old) = {best_accuracy}")
       #print(candidate_features_id) 
 
    for feature in candidate_features_id:       
            X = input_x.copy(deep=True)

            # define your candidate feature here!
            if feature == 0:
                X[candidate_features_dict[feature]] = X["Insulin"]*X["Glucose"]

            elif feature == 1:
                X[candidate_features_dict[feature]] = X["Glucose"]/(X["Insulin"]+0.0001)

            elif feature == 2:
                X[candidate_features_dict[feature]] = X["Age"]*X["Pregnancies"]

            elif feature == 3:
                X[candidate_features_dict[feature]] = X["Age"]/(X["Pregnancies"]+0.0001)

            elif feature == 4:
                X[candidate_features_dict[feature]] = X["Age"]*X["Pregnancies"]*X["Glucose"]

            elif feature == 5:
                X[candidate_features_dict[feature]] = X["Glucose"]/(X["Age"]+0.0001)

            elif feature == 6:
                X[candidate_features_dict[feature]] = X["Insulin"]/(X["Age"]+0.0001)

            elif feature == 7:
                X[candidate_features_dict[feature]] = X["BMI"]*X["Pregnancies"]

            elif feature == 8:
                X[candidate_features_dict[feature]] = X["BMI"]*X["Age"]

            elif feature == 9:
                X[candidate_features_dict[feature]] =  X["BMI"]*(X["Age"])*X["Pregnancies"]

            elif feature == 10:
                X[candidate_features_dict[feature]] = X["BMI"]*(X["Glucose"])

            elif feature == 11:
                X[candidate_features_dict[feature]] = X["DiabetesPedigreeFunction"]*(X["Insulin"])

            elif feature == 12:
                X[candidate_features_dict[feature]] = X["SkinThickness"]*(X["Insulin"])

            elif feature == 13:
                X[candidate_features_dict[feature]] = X["Pregnancies"]/(X["Age"]+0.0001)
            
            elif feature == 14:
                X[candidate_features_dict[feature]] = X["Glucose"]+X["Insulin"]+X["SkinThickness"]
            
            elif feature == 15:
                X[candidate_features_dict[feature]] = X["BloodPressure"]/(X["Glucose"]+0.0001)

            
            
            rf_model = RandomForestClassifier(random_state=17).fit(X, y)
            
            cv_results = cross_validate(rf_model,
                                        X, y,
                                        cv=5,
                                        scoring="accuracy")
        
            accuracy = cv_results["test_score"].mean()
            if accuracy > best_accuracy:
                best_accuracy = accuracy
                best_feature = feature
                best_x = X

    if best_feature == -1: 
        return best_accuracy, best_features 
    
    best_features.append(best_feature)
    candidate_features_id.remove(best_feature)

    if verbose:
       print(f"best accuracy(new) = {best_accuracy}")  
       print(f"added feature  = {best_feature}", end = '\n\n')   
       #print(best_features)
    
    return feature_selecter(best_x, y, candidate_features_dict, candidate_features_id, best_features, best_accuracy, verbose)

Затем мы можем запустить функцию Feature_selecter(),

candidate_features = {0:"new_glucoseXinsulin",
            1:"new_glucose/insulin",
            2:"new_ageXpreg",
            3:"new_age/preg",
            4:"new_ageXpregXglucose",
            5:"new_glucose/age",
            6:"new_insulin/age",
            7:"new_bmiXpreg",
            8:"new_bmiXage",
            9:"new_bmiXageXpreg",
            10:"new_bmiXglucose",
            11:"new_degreeXinsulin",
            12:"new_skinXinsulin",
            13:"new_preg/age",
            14:"new_glucose+insulin+skin",
            15:"new_blood/glucose"}

accuracy, new_features = feature_selecter(X,y,candidate_features, list(candidate_features.keys()), best_features=[])
'''
best accuracy(old) = 0.8086580086580086
best accuracy(new) = 0.812562600797895
added feature  = 6

best accuracy(old) = 0.812562600797895
'''




for feature in new_features:
    print(candidate_features[feature])
'''
new_insulin/age
'''



#So, it seems when we add the insulin/age feature, we obtain 81% accuracy.
#Let's see it.
X["new_insulin/age"] = X["Insulin"]/(X["Age"]+0.0001)








###############################################
# MODEL BUILDING AND EVALUATION
###############################################


#Params before GridSearchCV
rf_model = RandomForestClassifier(random_state=17)
rf_model.get_params()
'''
{'bootstrap': True,
 'ccp_alpha': 0.0,
 'class_weight': None,
 'criterion': 'gini',
 'max_depth': None,
 'max_features': 'sqrt',
 'max_leaf_nodes': None,
 'max_samples': None,
 'min_impurity_decrease': 0.0,
 'min_samples_leaf': 1,
 'min_samples_split': 2,
 'min_weight_fraction_leaf': 0.0,
 'n_estimators': 100,
 'n_jobs': None,
 'oob_score': False,
 'random_state': 17,
 'verbose': 0,
 'warm_start': False}
'''

#Accuracy before GridSearchCV
cv_results = cross_validate(rf_model, X, y, cv=5, scoring=["accuracy", "precision", "recall", "f1", "roc_auc"])
print(f"Accuracy : {cv_results['test_accuracy'].mean()}")    # 0.8125
print(f"Precision : {cv_results['test_precision'].mean()}")  # 0.7598
print(f"Recall : {cv_results['test_recall'].mean()}")        # 0.6831
print(f"F1 Score : {cv_results['test_f1'].mean()}")          # 0.7176
print(f"ROC AUC : {cv_results['test_roc_auc'].mean()}")      # 0.8744

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

  1. max_length: этот гиперпараметр определяет максимальную глубину каждого дерева решений в случайном лесу. Он ограничивает глубину дерева, ограничивая количество узлов от корня до листа. Меньшее значение max_depth делает отдельные деревья в лесу более мелкими и менее сложными, что может помочь предотвратить переобучение. С другой стороны, большее значение позволяет деревьям расти глубже, потенциально фиксируя более сложные закономерности в данных, но увеличивая риск переобучения. Значение по умолчанию для гиперпараметра max_depth в моделях на основе дерева решений, включая случайный лес, установлено на «Нет».
    «Нет» означает, что деревья решений им разрешено расти до тех пор, пока они не будут содержать менее min_samples_split образцов в каждом листовом узле (еще один гиперпараметр) или пока все листья не станут чистыми (все образцы в конечном узле принадлежат к одному и тому же классу). Хотя отсутствие установки максимальной глубины может быть полезным во многих случаях, это также может привести к переобучению, особенно если набор данных зашумлен или если есть нерелевантные функции. Поэтому важно настроить гиперпараметр max_depth в процессе обучения модели.
  2. max_features: max_features определяет максимальное количество функций (переменных или атрибутов), которые следует учитывать для разделения в каждом узле дерева решений. Его можно установить как фиксированное число, долю от общего количества объектов или одно из предопределенных значений, например «sqrt» (квадратный корень из общего количества объектов) или «log2» (логарифм по основанию 2 от общего количества объектов). . Ограничивая количество признаков, вы можете внести в случайный лес случайность и разнообразие, что помогает предотвратить слишком большую зависимость модели от одного признака и улучшает обобщение. Значение по умолчанию для гиперпараметра max_features установлено на «auto», что означает, что алгоритм автоматически определит количество признаков, которые следует учитывать для разделения на каждом узле решения. деревья. Использование «авто» соответствует установке max_features квадратному корню из общего количества функций (sqrt(n_features)). Эмпирические исследования и практический опыт показали, что установка для max_features значения «авто» или его квадратного корня часто дает хорошие результаты для широкого спектра наборов данных. Он обеспечивает баланс между изучением различных аспектов данных и предотвращением переобучения модели. Хотя «авто» является хорошим выбором по умолчанию, важно отметить, что вы можете настроить гиперпараметр max_features так, чтобы он лучше соответствовал вашему конкретному набору данных и проблеме.
  3. n_estimators: этот гиперпараметр определяет количество деревьев решений, которые будут включены в ансамбль случайного леса. Более высокое значение n_estimators обычно приводит к более надежной и точной модели. Однако после определенного момента отдача может снижаться, поскольку добавление большего количества деревьев может увеличить вычислительные затраты без значительного улучшения производительности. Обычно начинают с разумного количества деревьев, а затем используют перекрестную проверку, чтобы найти оптимальное значение. По по умолчанию для n_estimator установлено значение 100.
  4. min_samples_split: min_samples_split устанавливает минимальное количество выборок, необходимое для разделения внутреннего узла в дереве решений. Если количество выборок в узле меньше min_samples_split, дальнейшее разделение останавливается, и узел становится листом. Этот гиперпараметр может помочь контролировать глубину и сложность деревьев. Меньшее значение может привести к созданию более глубоких деревьев с более мелкими разбиениями, тогда как большее значение может привести к более мелким деревьям с меньшим количеством разбиений. По по умолчанию для min_samples_split установлено значение 2.

Давайте применим GridSearchCV и найдем лучшие гиперпараметры.

#Apply GridSearchCV
#We should also specify the default value of the parameters in the dictionary 
#below, so that we do not get a worse result than the initial performance.
rf_params = {"max_depth": [5, 8, None],
             "max_features": [3, 5, 7, "auto"],
             "min_samples_split": [2, 5, 8, 15, 20],
             "n_estimators": [100, 200, 500]}


rf_best_grid = GridSearchCV(rf_model, rf_params, cv=5, n_jobs=-1, verbose=True).fit(X,y)

print(rf_best_grid.best_params_)
'''
Fitting 5 folds for each of 180 candidates, totalling 900 fits
{'max_depth': None, 'max_features': 'auto', 'min_samples_split': 2, 'n_estimators': 100}
'''

Итак, лучшие значения гиперпараметра находятся как:
max_length = 'None'
max_feature = 'auto'
min_samples_split = 2
n_estimators = 100
Помните , это значения по умолчанию. Таким образом, настройка гиперпараметров не улучшила производительность. И все же построим окончательную модель по этим значениям:

#Build the final model with the hyperparameter values that we have found.
rf_final = RandomForestClassifier(**rf_best_grid.best_params_, random_state=17).fit(X, y)


#5-Fold CV results for the final model
cv_results = cross_validate(rf_final, X, y, cv=5, scoring=["accuracy", "precision", "recall", "f1", "roc_auc"])
print(f"Accuracy : {cv_results['test_accuracy'].mean()}")     # 0.8125
print(f"Precision : {cv_results['test_precision'].mean()}")   # 0.7598
print(f"Recall : {cv_results['test_recall'].mean()}")         # 0.6831
print(f"F1 Score : {cv_results['test_f1'].mean()}")           # 0.7176
print(f"ROC AUC : {cv_results['test_roc_auc'].mean()}")       # 0.8744

В результате наши окончательные показатели производительности таковы: ​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​ ​       Точность:​​​ ​ ​ 0,8125 ​
​ ​  ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ Точность: ??? ​ ​ ​ ​ ​ ​ Напомнить ​​​ ​ 0,6831 ​  ​
​ ​  ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​           ??? ​​​​0,7176​​ ​
​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​                     ​ ​   ROC AUC​​​​​​​​​:​​​​​ ​ ​ 0,8744

######################################
# FEATURE IMPORTANCE
######################################

plot_importance(rf_final, X)
#IMAGE IS BELOW (importance.png)

Глядя на график выше, мы видим, что созданная нами переменная «new_insulin/age» очень эффективна для успеха модели.

#######################################
# VALIDATION CURVE
#######################################

val_curve_params(rf_final, X, y, "max_depth", range(1, 11), scoring="accuracy")
#IMAGE IS BELOW (validation_curve.png)

Спасибо, что прочитали…