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, но это сложно, а потом еще и многомерные интегралы брать.
Идея в следующем: идентифицировать режимы неизвестных факторов и делать модели по отдельности.
Можно, конечно, сэмплить подвыборки без пересечений, но это сложно и ненадежно.
Я попробовал следующий пайплайн:
- В процессе обучения строим кластеризацию фичей совместно с таргетом,
- Учим классификатор от фичей к кластеру,
- На каждый кластер учим свой регрессор.
- На инференсе используем классификатор и регрессоры.
Практика показала… что подход имеет место быть. А если сделать весь этот комбайн дифференцируемым… ну это пока отложим.
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)
А смотрите-ка, и вправду лучше сработало!
Похоже и действительно, считать что неведомые фичи — это аддитивный шум — это слегка примитивно.