Scikit-Learn: организация процесса
предварительной обработки данных на Python

Scikit-Learn — мощный инструмент для решения задач машинного обучения (отечественный термин, пожалуй, объемлющий понятие machine learning — это распознавание образов и классификация), разработанный в виде пакета для языка программирования Python. Он позволяет решать многие задачи, связанные с обработкой данных, однако приоритетными его направлениями остаются классификационные задачи (по прецедентам — т.е. при наличии обучающей выборки, и без — задачи кластеризации).

Как правило, задачи классификации не стоят сами по себе. Прежде чем перейти к решению задачи классификации — обучению классифицирующего алгоритма и его применению, данные необходимо обработать. Предобработка данных — всегда неотъемлемый этап при решении практических задач, и этот этап можно реализовать в стиле обучение-преобразование (fit-transform) средствами scikit-learn.

Рассмотрим процедуру предобработки данных, состоящую из нескольких этапов, оформленную в виде единой последовательности scikit-learn (Pipeline). Scikit-learn имеет возможность создавать цепь последовательных преобразований, что может (и должно) использоваться для предварительной обработки данных.

Приведем пример создания такой последовательной процедуры обработки с использование Pipeline.

#coding: utf-8
#author: Dmitry E. Kislov
#e-mail: kislov@easydan.com

from __future__ import print_function
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
import pandas as pd


# Наследуемся от базовых классов  BaseEstimator, TransformerMixin, используемых в scikit-learn
# при реализации большинства алгоритмов.
# Наследование от этих классов обеспечивает единый протокол
# взаимодействия создаваемых алгоритмов в стиле scikit-learn
class TransformXColumn(BaseEstimator, TransformerMixin):
    '''Тестовый алгоритм предобработки колонки 'x' в датафрейме (или структурированном (structured)
    массиве numpy)
    '''
    
    def fit(self, df, y=None):
        # вычисляем все необходимые параметры для предобработки данных и запоминаем их, чтобы
        # использовать при применении преобразования (transform)
        # y - здесь указан специально, это групповая переменная, она тоже может 
        # участвовать в предобработке
        self._mean_x = df.x.mean()
        return self
    
    def transform(self, df, y=None):
        # Преобразование, которое получает на вход исходные данные (и опционально - групповую переменную)
        # и возвращает модифицированныые данные
        _df = df.copy()  # делаем копию данных (полагая, что df - датафрейм pandas),
        # также будет работать, если df - структурированный массив numpy
        
        # Модифицирующее правило, здесь мы обнуляем все значения колонки 'x'
        # в датафрейме, если они больше среднего значения для этой колонки
        _df.loc[_df['x'] > self._mean_x, 'x'] = 0.0 
        return _df


class TransformYColumn(BaseEstimator, TransformerMixin):
    '''Тестовый алгоритм предобработки колонки 'y' в датафрейме (или структурированном (structured)
    массиве numpy)
    '''
    
    def __init__(self, *args):
        self._new_value = args[0]


    def fit(self, df, y=None):
        return self


    def transform(self, df, y=None):
        # Здесь мы планируем выполнить преобразование, которое заключается
        # в замене встретившихся отсутствующих значений в колонке `y` датафрейма
        # df на знечение, подаваемое представителю данного класса при его создании (self._new_value).
        _df = df.copy()
        _df.loc[_df['y'].isnull(),'y'] = self._new_value
        return _df




# Тестовый датафрейм, который будет подвергнут предобработке
test_df = pd.DataFrame({'x': pd.np.random.rand(10),
                        'y': pd.np.random.choice(['a', 'b', None], p=[0.2, 0.3, 0.5], size=10)
                        })



my_pipe = Pipeline([('eval_x', TransformXColumn()),
                    ('eval_y', TransformYColumn('c'))
                    ])


my_pipe.fit(test_df)

new_df = my_pipe.transform(test_df)

print('Данные до обработки\n', test_df, '\nДанные после обработки\n', new_df)

При выполнении данного кода, мы получим что-то вроде следующего
 

Данные до обработки
           x     y
0  0.410585  None
1  0.329695     b
2  0.523571     b
3  0.286448  None
4  0.764460  None
5  0.662220  None
6  0.671150     a
7  0.435665     b
8  0.479983     b
9  0.340642  None 
Данные после обработки
           x  y
0  0.410585  c
1  0.329695  b
2  0.000000  b
3  0.286448  c
4  0.000000  c
5  0.000000  c
6  0.000000  a
7  0.435665  b
8  0.479983  b
9  0.340642  c

Казалось бы, нет необходимости вводить дополнительные классы, и намного легче сразу выполнить предобработку данных средствами pandas\numpy\python — это всего лишь дополнительная пара строк кода. Но организация обработки в виде последовательной цепи преобразований (Pipeline) позволяет сделать этот процесс единообразным. Это полезно не только с точки зрения поддержки и внесения изменений в сложные процедуры предобработки данных, состоящие из многих этапов. Представление этапов обработки данных в виде Pipeline   упрощает процесс подбора параметров, определяющих результаты этапов обработки. Для подбора параметров в этом случае может использоваться, например, GridSearchCV.

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

# Демонстрационный фрагмент кода

our_pipe = Pipeline([('eval1', Evaluator(par1=1)),
                     ('eval2', Evaluator(par1=3, par2=4)),
                       #...
                     ('final_estimator', Estimator())
                    )])

param_grid ={
    'eval1__par1': [1, 2, 3],
    'eval2__par1': [3, 4],
    'eval2__par2': [0, 4, 3]
    }

# Тогда будет проводится настройка по всевозможным комбинациям параметров в param_grid,
# это 3*2*3 комбинаций.
gsclf = GridSearchCV(our_pipe, param_grid=param_grid, cv=3)  

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

blog comments powered by Disqus