Meta Data Science


CRISP-DM начинается с этапа business understanding. Что нужно бизнесу? Не просто рост (его бизнес-) метрик, а на самом деле их максимизация.

M -> max

Однако вторым этапом CRISP-DM идёт работа с данными. А данные, они такие, что:

M = M(x1, …, xn, w1, …, wk)

где x1, …, xn — известные в данных признаки, а w1, …, wk — неизвестные по данным факторы. И что делают?

Берут M’ = M’ (x1, …, xn, phi_1(x1, …, xn), …, phi_k(x1, …, xn)), без потери общности можно считать M’ = f(x1, …, xn).

И вытворяют ||M — f|| -> min.

А тут вот в чем беда. Поскольку есть ненаблюдаемые факторы, M -> max не эквивалентно f -> max!

По сути, неизвестные факторы объявляются постоянными.

M = G(w1, …, wk) + f(x1, …, xn) — стандартное разложение для неизвестных факторов. И эта вот G — а она не постоянная.

У неё есть (ну или должны быть) режимы — сочетания неизвестных факторов. Можно конечно работать с частными производными по dM/dxi, но это сложно, а потом еще и многомерные интегралы брать.

Идея в следующем: идентифицировать режимы неизвестных факторов и делать модели по отдельности.

Можно, конечно, сэмплить подвыборки без пересечений, но это сложно и ненадежно.

Я попробовал следующий пайплайн:

  1. В процессе обучения строим кластеризацию фичей совместно с таргетом,
  2. Учим классификатор от фичей к кластеру,
  3. На каждый кластер учим свой регрессор.
  4. На инференсе используем классификатор и регрессоры.

Практика показала… что подход имеет место быть. А если сделать весь этот комбайн дифференцируемым… ну это пока отложим.

space = np.linspace(0, 10, 10 * 1000)
X = np.column_stack([
    space ** 2, np.exp(space)
])
y = X[:, 0] / X[:, 1]

plt.plot(y);
cross_val_score(MLPRegressor([32, 32], max_iter=1000, random_state=1), X=train_X[:, :1], y=train_y, cv=3).mean(), \
cross_val_score(MLPRegressor([32, 32], max_iter=1000, random_state=1), X=train_X[:, 1:], y=train_y, cv=3).mean()
(0.978530694329737, 0.7954231782888691)
from sklearn.base import BaseEstimator
from sklearn.metrics import r2_score

class ClusterClasses(BaseEstimator):
    def __init__(self, clusters=3):
        super(ClusterClasses, self).__init__()
        self.clusters = clusters
    def fit(self, X, y=None):
        self.clusterer = KMeans(
          n_clusters=self.clusters, n_init=10, random_state=1
        ).fit(np.column_stack([X, y]))
        self.classifier = MLPClassifier(
          [32, 32], max_iter=1000, random_state=1
        ).fit(X, self.clusterer.labels_)
        labels = self.classifier.predict(X)
        self.regressors = {}
        for index in range(self.clusters):
            if not sum(labels == index):
                continue
            slice_X = X[labels == index]
            slice_y = y[labels == index]
            self.regressors[index] = MLPRegressor(
              [32, 32], max_iter=1000, random_state=1
            ).fit(slice_X, slice_y)
        return self
    def predict(self, X):
        clusters = self.classifier.predict(X)
        result = []
        for x, cluster in zip(X, clusters):
            result.append(self.regressors[cluster].predict([x])[0])
        return np.row_stack(result)
    def score(self, X, y):
        return r2_score(y, self.predict(X))

cross_val_score(ClusterClasses(), X=train_X[:, :1], y=train_y, cv=3).mean(), \
cross_val_score(ClusterClasses(), X=train_X[:, 1:], y=train_y, cv=3).mean()
(0.9924120428720503, 0.8008522560724876)

А смотрите-ка, и вправду лучше сработало!

Похоже и действительно, считать что неведомые фичи — это аддитивный шум — это слегка примитивно.


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *