Линейная регрессия, регуляризация, кросс-валидация и Grid Search в PySpark

Автор Категория , ,
Линейная регрессия, регуляризация, кросс-валидация и Grid Search в PySpark

В прошлый раз мы говорили о решении задачи классификации в рамках Machine Learning с помощью PySpark MLlib. Сегодня рассмотрим задачу регрессии. Читайте далее: что такое линейная регрессия, L1 и L2 регуляризация, алгоритм подбора значений гиперпараметров Grid Search, а также применение кросс-валидации в PySpark.

Датасет с домами на продажу

Обучать модель машинного обучения (Machine Learning) будем на датасете с домами на продажу в округе Кинг (Вашингтон, США). Его можно скачать напрямую с Kaggle или воспользоваться Kaggle API, как мы описывали здесь. Датасет содержит такие атрибуты, как цена, количество комнат, количество ванных комнат, дату постройки, площадь на квадратный фут (1 фут = 0.3 метра) и другие. Код на Python для инициализации Spark-приложения и создания DataFrame выглядит следующим образом:

from pyspark.sql import SparkSession
spark = SparkSession.builder.master("local[*]").getOrCreate()
data = spark.read.csv(
    'kc_house_data.csv', 
    inferSchema=True, header=True)

Векторизация и разделение выборки на обучающую и тестовую

Как мы уже говорили в предыдущей статье, алгоритмы Machine Learning в PySpark принимают на вход только вектора, поэтому нам необходимо предварительно векторизовать данные.

Мы будем обучать модель на 5 признаках: площадь на квадратный фут, количество комнат, количество ванных комнат и дата постройки. Отберем их с помощью метода select:

features = ['sqft_living', 'bedrooms', 
            'bathrooms', 'yr_built']
target = 'price'
attributes = features + [target]
sample = data.select(attributes)

А чтобы векторизовать выбранные признаки, воспользуемся классом VectorAssembler. Он принимает в качестве аргументов список признаков, которые необходимо преобразовать в вектор, и название преобразованного вектора. Вот так это выглядит в Python:

from pyspark.ml.feature import VectorAssembler

assembler = VectorAssembler(inputCols=features,
                            outputCol='features')
output = assembler.transform(sample)

Теперь разобьём выборку на обучающую и тестовую. На обучающей создадим модель Machine Learning, а на тестовой проверим эффективность этой модели. Причём, разделим данные в пропорции 9:1. Python-код:

train, test = output.randomSplit([0.9, 0.1])

Grid Search: оптимизация гиперпараметров

Одна из задач машинного обучения является подбор гиперпараметров, которые задает Data Scientist перед обучением. У каждого алгоритма Machine Learning они свои. Однако подбирать их вручную утомительно и затратно, поэтому существуют специальные алгоритмы подбора. В PySpark встроен один из таких алгоритмов – Grid Search.

Grid search есть не что иное, как комбинация из всех значений заданных гиперпараметров. Например, пусть имеется два гиперпараметра: первый может принимать 2 значения, а второй – 3. Тогда возможно всего 6 комбинаций:

a = [1, 2]
b = [3, 4, 5]
grid_search = [(1,3), (1,4), (1,5),
               (2,3), (2,4), (2,5)] 

Гиперпараметры линейной регрессии в PySpark

В модуле MLlib имеется класс LinearRegression, который отвечает за линейную регрессию. У него есть два основных гиперпараметра:

  • regParam;

  • elasticNetParam.

Эти параметры задают вид регуляризации: L1 или L2. Единственное, что делает регуляризация — это добавляет ещё один параметр к целевой функции, а обучение остаётся тем же самым. Как известно, цель машинного обучения минимизировать целевую функцию. В линейной регрессии целевая функция — это сумма квадратов остатков [1]. Она определяется как:

RSS = sum((y_real - y_predicted)**2)

В L1-регуляризации к целевой функции добавляется сумма весов, и такая регуляризация называется Lasso. Выражается она так:

RSS = sum((y_real – y_predicted)**2)
lasso = RSS + theta * sum(W)

В L2-регуляризации к целевой функции добавляется сумма квадратов весов, и такая регуляризация называется Ridge. Выражается она так:

RSS = sum((y_real – y_predicted)**2)
ridge = RSS + lambda * sum(W*W)

Кроме того, эти два вида регуляризации можно соединить вместе, и такая регуляризация называется Elastic Net.

В линейной регрессии без регуляризации предполагается, что коэффициенты регрессии (веса) могут равновероятно принимать любые значения. В L1 регуляризации предполагается, что эти коэффициенты распределены по закону Лапласа, в L2 — по закону Гаусса. И та, и другая регуляризация не даёт весам быть слишком большими, и, возможно, повысится обобщающая способность модели.

Обобщенная формула для регуляризации в PySpark

В Python-библиотеке Scikit-learn все виды регуляризации линейной регрессии разделены по соответствующим классам Lasso, Ridge и ElasticNet. Но в PySpark это реализовано в общем виде и зависит от гиперпарметров regParam и elasticNetParam. Выражается это так:

L1 = W
L2 = W*W
reg = regParam * (elasticNetParam * L1 + (1 - elasticNetParam) * L2)

Таким образом, задавая определённые значения regParam и elasticNetParam, можно получить следующие результаты:

  • regParam = 0 – это линейная регрессия без регуляризации;

  • regParam > 0, elasticNetParam = 0 ведет к L2-регуляризация (Ridge);

  • regParam > 0, elasticNetParam = 1 ведет к L1-регуляризация (Lasso);

  • regParam > 0, 0 < elasticNetParam < 1 ведет к L1 + L2 (Elastic Net).

Создаем модель и задаем гиперпараметры с помощью Grid Search

Прежде всего создадим модель линейной регрессии из модуля Spark MLlib. Нужно указать название векторизованного атрибута и целевой атрибут, значения которого нужно предсказать. В нашем случае мы пытаемся предсказать цену на дом. Python-код:

from pyspark.ml.regression import LinearRegression
lin_reg = LinearRegression(
    featuresCol='features', 
    labelCol='price')  

Для инициализации Grid Search в PySpark используется класс ParamGridBuilder. В метод addGrid добавляются гиперпарметр и значения, которые он может принимать. Выберем 3 значения для regParam и два значения для elasticNetParam. Так это выглядит в Python:

grid_search = ParamGridBuilder() \
    .addGrid(lin_reg.regParam, [0.0, 0.01, 0.1]) \
    .addGrid(lin_reg.elasticNetParam, [0.5, 1.0]) \
    .build()

Метрика качества

Нам также нужно указать метрику качества для оценки модели. Для задачи регрессии используется RegressionEvaluator, который по умолчанию в качестве метрики использует среднюю квадратическую ошибку (MSE). MSE можно поменять на среднюю абсолютную ошибку (MAE). Кроме того, мы должны указать названия целевого и предсказанного атрибутов. Линейная регрессия в PySpark после обучения создает предсказанный атрибут под название prediction, а целевая так и остаётся price. Python-код выглядит так:

evaluator = RegressionEvaluator(predictionCol='prediction',
                                labelCol='price')

Кросс-валидация

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

Кросс-валидация PySpark
Кросс-валидация (N=3)

В PySpark кросс-валидация реализуется через CrossValidator. В нем нам нужно указать три аргумента:

  1. estimator — модель Machine Learning, в нашем случае линейная регрессия;

  2. estimatorParamMaps — алгоритм оптимизации гиперпараметров, в нашем случае Grid Search;

  3. evaluator — метрика качества, в нашем случае RegressionEvaluator.

Также можно указать в аргументах numFolds — количество блоков N (по умолчанию их 3). После указания значений этих аргументов вызывается метод fit для выполнения кросс-валидации. В Python это выглядит следующим образом:

cv = CrossValidator(estimator=lin_reg,
                    estimatorParamMaps=grid_search,
                    evaluator=evaluator)
cv_model = cv.fit(train)

Результаты кросс-валидации

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

Мы можем посмотреть, например, на среднюю абсолютную ошибку:

cv_model.bestModel.summary.meanAbsoluteError
# 145260.09296909513

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

cv_model.bestModel.extractParamMap()
#
{
Param(parent='LinearRegression_b64ded0857c9', name='elasticNetParam', doc='the ElasticNet mixing parameter, in range [0, 1]. For alpha = 0, the penalty is an L2 penalty. For alpha = 1, it is an L1 penalty'): 1.0,
 Param(parent='LinearRegression_b64ded0857c9', name='regParam', doc='regularization parameter (>= 0)'): 0.1,
}

Как видим, лучшая модель имеет гиперпараметр elasticNetParam равный 1, а regParam – 0.1. Значения этих параметров показывают, что лучшая модель использует L1-регуляризацию (regParam > 0, elasticNetParam=1).

Больше подробностей о линейной регрессии, L1 и L2 регуляризации, алгоритмах подбора гиперпараметрах, а также о кросс-валидации в PySpark на примерах задач Data Science и Big Data вы узнаете на наших практических курсах по Apache Spark и Machine Learning в лицензированном учебном центре обучения и повышения квалификации разработчиков, менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data в Москве:

Источники

  1. https://en.wikipedia.org/wiki/Residual_sum_of_squares