Что ж, цепь порвалась. Оказывается, поддерживать постоянный график написания статей сложнее, чем я себе представлял. Тем не менее, я не прекращал заниматься интересными проектами. Когда я в последний раз писал, я представлял себе следующий пост как более глубокое погружение в стандартный проект, о котором я писал ранее. Пока я все еще работаю над этим проектом, я несколько увяз (оказывается, единственное, что сложнее, чем соблюдение графика написания, — это обучение с подкреплением!). Итак, в прошлые выходные в качестве своего рода очистителя поддонов я решил поработать с совершенно другим набором данных: набором данных Ames Housing от Kaggle (я дам ссылку на него внизу поста). В этом посте мы будем использовать стандартные панды и инструменты обучения из научного набора для построения модели GradientBoostedRegressor для прогнозирования цены продажи домов в городе Эймс, штат Айова. Сначала мы поговорим о данных, затем немного о теории повышения градиента, и, наконец, мы перейдем к делу и посмотрим на код. Давайте начнем!

Данные

Набор данных Ames Iowa Housing Dataset был составлен Дином Де Коком в 2011 году в качестве альтернативы чрезвычайно популярному набору данных о жилье Boston, с которым знакомо большинство людей, которые занимались машинным обучением. Важно отметить, что он имеет массу объясняющих переменных (всего 79), большинство из которых я бы назвал переменными строительных блоков. Эти функции сами по себе не очень полезны, но их можно использовать для создания очень интересных функций. Показательный пример: общая площадь подвальных помещений (TotalBsmtSF в наборе данных). Для всех, кто заинтересован в покупке дома, это, вероятно, не первая метрика, которую вы бы искали, но она тесно связана с жизненно важной точкой данных на рынке недвижимости: квадратными метрами. Эта метрика отсутствует в данных Эймса, по крайней мере, номинально, но, как мы увидим, ее легко спроектировать на основе доступных функций.

Еще одним важным аспектом набора данных, особенно когда мы переходим к этапу обработки, является относительное обилие категориальных признаков. Целых 54% необработанных функций являются категориальными. На первый взгляд, в этом есть смысл: многое из того, что определяет стоимость дома, не является количественным по своей природе, это относится как к более неосязаемым, квалиа, так и к чистым квадратным метрам. Однако это означает, что нам нужно будет правильно и правильно привести наши конвейеры данных в порядок, если мы хотим использовать модель машинного обучения, чтобы разобраться во всех этих качественных факторах.

Наконец, и, возможно, это наименее важно, мы должны отметить в начале, что наблюдаемые цены продажи домов в обучающей выборке асимметричны. В нижней части распределения находится значительное количество домов (стоимостью ‹~250 тыс.), и по мере роста цен продается все меньше и меньше. Я упоминаю об этом, чтобы мы могли помнить об этом как о хорошей проверке наших прогнозов на здравомыслие. Если наша модель начнет предсказывать высокую плотность продаж домов за полмиллиона долларов, можно поспорить, что модель ошибочна.

Повышение градиента

Я просто хотел кратко затронуть тему повышения градиента, прежде чем мы начнем всерьез, частично потому, что это полезно в этом проекте, но в основном потому, что я нахожу эту идею действительно мощной не только при использовании поверхностных алгоритмов, но и в глубоком обучении. Так что же значит повысить? Проще говоря, это означает взять базовую модель (обычно «слабо обучающуюся» или не очень мощную модель), а затем обучить другую идентичную (или похожую) модель, чтобы предсказать ошибки первой. Затем повторите это столько раз, сколько захотите. Идея состоит в том, что даже если ваша первоначальная модель может быть не слишком мощной, многие модели, каждая из которых убирает беспорядок, созданный предыдущей, в конечном итоге сойдутся на довольно хорошем прогнозе.

Теперь в этом случае использования мы будем использовать готовый метод sci-kitlearn для этого: GradientBoostingRegressor , но это не обязательно. Вы можете улучшить алгоритм глубокого обучения, если последовательность нейронных сетей будет предсказывать ошибки последнего. Фактически, этот метод можно обобщить на любую функцию потерь, которую вы хотите, и, таким образом, он является очень гибким методом.

Код

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

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

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

class AddFeatures:
    
    def __init__(self, df):
        self.df = df
        self.final_df = self.run()
        
        
    def add_total_sf(self):
        """
        We're going to derive total square footage from 
        three places in our dataset: First floot square feet (1stFlrSF),
        second floor square feet (2ndFlrSF), and Total Basement square feet,
        (TotalBsmtSF). Then we'll add it to our dataframe.
        """
        temp = self.df
        total_SF = temp['1stFlrSF'] + temp['2ndFlrSF'] + temp['TotalBsmtSF']
        temp['totalSF'] = total_SF
        return temp
    
    def add_finished_sf(self):
        """
        Same as total square feet above but excludes unfinished basement
        space.
        """
        temp = self.df
        total_SF = temp['1stFlrSF'] + temp['2ndFlrSF'] + temp['TotalBsmtSF'] - temp['BsmtUnfSF']
        temp['finishedSF'] = total_SF
        return temp
    
    def add_high_quality_sf(self):
        """
        Same as total square feet above but excludes low-quality
        space.
        """
        temp = self.df
        total_SF = temp['1stFlrSF'] + temp['2ndFlrSF'] + temp['TotalBsmtSF'] - temp['LowQualFinSF']
        temp['qualitySF'] = total_SF
        return temp
    
    def add_total_bath(self):
        """
        Add FullBath, HalfBath.
        """
        temp = self.df
        total_bath = temp['FullBath'] + temp['HalfBath']
        temp['total_bath'] = total_bath
        return temp
    
    def run(self):
        final_df = pd.concat([self.add_total_sf(),
                   self.add_finished_sf(),
                   self.add_total_bath(),
                   self.add_high_quality_sf()]
                  ).drop_duplicates().reset_index(drop=True)
        return final_df

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

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

Теперь, когда у нас есть наши функции в порядке, пришло время обработать наши данные и подготовить их из файла GradientBoostingRegressor. Для этого мы разработаем следующий класс: Preprocessing, который будет удалять строки с отсутствующими категориальными, заполнять отсутствующие числовые значения их средними значениями, а One Hot Encode — категориальные столбцы. Мы хотим сделать его модульным, чтобы мы могли повторно использовать его в нашем файле test.csv. Вот код, который я придумал:

class Preprocess:
    
    def __init__(self, X, y=None):
        self.X = X
        self.y = y
        self.processed_df = self.run()
        
    def drop_columns(self):
        """
        Drop the columns that have low coverage in the training set.
        """
        dropped = ['Alley', 'Fence', 'MiscFeature', 'PoolQC']
        return self.X.drop(dropped, axis=1)
    
    def add_new_features(self, df):
        """
        Add our new engineered features to the df.
        """
        return AddFeatures(df).final_df
    
    def encode(self, df):
        """
        Encodes categorical and numerical features, fills numerical NaN values,
        drops categorcial NaN values.
        """
        numeric_features = df.drop(['Id'], axis=1)._get_numeric_data().columns
        cat_features = list(set(df.columns) - set(numeric_features))
        cat_features.remove('Id')
        numeric_transformer = Pipeline(
            steps=[("imputer", SimpleImputer(strategy="mean")), ("scaler", StandardScaler())])
        categorical_transformer = Pipeline(
            steps=[
                ("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
            ])
        df = df.dropna(subset=cat_features)
        preprocessor = ColumnTransformer(
            transformers=[
                ("numerical", numeric_transformer, numeric_features),
                ("categorical", categorical_transformer, cat_features),
            ])
        preprocessor.set_output(transform='pandas')
        temp = preprocessor.fit_transform(df)
        temp['Id'] = self.X['Id']
        if self.y is not None:
            temp['SalePrice'] = self.y
        return temp
    
    def run(self):
        """
        Driver code for the class.
        """
        df = self.drop_columns()
        df = self.add_new_features(df)
        return self.encode(df)

Здесь наш класс будет принимать функции X и метку y как отдельные значения, а не как один большой объект DataFrame в интересах модульности: набор тестовых данных не включает метку. Особое внимание следует уделить сохранению столбца Id, так как позже мы будем форматировать наши прогнозы именно так.

Однако обработка не совсем завершена. Наши обучающая и тестовая выборки по-прежнему не используют одни и те же столбцы из-за того, что некоторые категориальные типы, присутствующие в обучающей выборке, отсутствуют в тестовой выборке, или наоборот. Мое решение (не замеченное в коде) заключалось в том, чтобы просто удалить проблемные столбцы из DataFrame, возможно, слишком грубое решение, но я не мог придумать более элегантного способа решить проблему. Ради честного посмертного анализа я бы сказал, что это, вероятно, та часть проекта, из-за которой я чувствую себя хуже всего. Если у кого-то есть лучший способ обеспечить совместное использование столбцов разными One Hot Encoded DataFrames, я хотел бы узнать.

Но нам нужно двигаться дальше. Наш следующий шаг — самое интересное: обучение моделей! Как мы уже говорили ранее, мы собираемся использовать GradientBoostingRegressor, но я также хотел добавить еще один популярный алгоритм для целей сравнения: классический RandomForestRegressor. Вот код:

from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import GradientBoostingRegressor

np.random.seed(42)

X = clean_train.drop('SalePrice', axis=1)
y = clean_train['SalePrice']

X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2)

reg = RandomForestRegressor()
reg.fit(X_train, y_train)

rf_preds = reg.predict(X_val)

gbr = GradientBoostingRegressor(loss='squared_error')
gbr.fit(X_train, y_train)

gbr_preds = gbr.predict(X_val)

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

from sklearn.metrics import mean_squared_error
from math import sqrt

def evaluate(y_true, y_preds):
    return sqrt(mean_squared_error(y_true, y_preds))

Теперь осталось только оценить и гипернастроить модели. Прямо из коробки ни одна модель не сбивает вас с толку: случайный лес имеет RMSE 37 841,03, а регрессор с градиентным усилением имеет оценку 30 176,78. Однако уже сейчас, как мы видим, алгоритм с градиентным усилением значительно превосходит случайный лес. Однако мы по-прежнему включим случайный лес в гипернастройку (по крайней мере, на данный момент), просто для сравнения.

from sklearn.model_selection import GridSearchCV

parameters = {'n_estimators': [50, 100, 200, 400],
              'learning_rate': [0.1, 0.5, 0.75, 1],
              'subsample': [0.75, 1]
             }

gbr_model = GradientBoostingRegressor(loss='squared_error')

reg_cv = GridSearchCV(gbr_model, parameters)
reg_cv.fit(X_train, y_train)

rf_parameters = {'n_estimators': [100, 200, 400],
                 'min_samples_leaf': [1, 2, 4]
                }

rf_model = RandomForestRegressor()

rf_cv = GridSearchCV(rf_model, rf_parameters)
rf_cv.fit(X_train, y_train)

Лучшими параметрами GridSearchCV, найденными для нашей модели с градиентным усилением, были скорость обучения 0,01, n_estimators из 100 и subsample из единицы. Точно так же для случайного леса были найдены параметры 2 для min_samples_leaf и 100 для n_estimators . Тем не менее, даже после гипернастройки случайный лес все еще полностью превосходит классы: в то время как наш недавно гипернастроенный GradientBoostingRegressor теперь имеет RMSE 29 932,58 на проверочном наборе, случайный лес полностью на 10 тысяч хуже со RMSE 39 398,13. Я думаю, что довольно ясно, какую модель мы должны выбрать.

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

И это приятно косо-право! Это дает мне некоторую уверенность в том, что наша модель искусна.

Заключение

Набор данных Ames Housing — это интересный способ попрактиковаться в обработке данных и разработке функций. Кроме того, я использовал классную новую модель: GradientBoostingRegressor из набора инструментов для обучения научному набору. В целом, я думаю, что проект прошел хорошо, но он подчеркивает, как много мне нужно узнать, когда дело доходит до очистки и подготовки данных. Но, как всегда, я буду продолжать работать, продолжать учиться и продолжать совершенствоваться. Надеюсь, вам понравилась статья, увидимся на следующей неделе. N=1 (еще раз).

Ссылки



Полный блокнот: https://github.com/maym5/ames-housing