Selección de la importancia de las características para los vecinos k más cercanos (pozo u otros hiperparámetros) por descenso similar al gradiente

Una verdadera tontería no solo puede cumplir lo imposible, sino que también sirve como ejemplo de advertencia

Al experimentar con la tarea más simple del aprendizaje automático, descubrí que sería interesante seleccionar 18 hiperparámetros al mismo tiempo en un rango bastante amplio. En mi caso, todo era tan simple que la tarea se podía realizar con la potencia bruta de la computadora.

Al aprender algo, puede ser muy interesante inventar algún tipo de bicicleta. A veces resulta que realmente surge algo nuevo. A veces resulta que todo fue inventado antes que yo. Pero incluso si solo repito el camino recorrido mucho antes que yo, como recompensa, a menudo entiendo los mecanismos subyacentes de los algoritmos de sus capacidades y limitaciones internas. A lo que te invito.

En Python y DS, por decirlo suavemente, soy un principiante y hago muchas cosas que se pueden implementar en un equipo de acuerdo con mi viejo hábito de programación, que Python castiga al disminuir la velocidad, no a veces, sino por órdenes de magnitud. Por lo tanto, subo todo mi código al repositorio. Si sabe cómo implementarlo de manera mucho más eficiente, no sea tímido, edite allí o escriba los comentarios. https://github.com/kraidiky/GDforHyperparameters

Aquellos que ya son un experto en datos y han intentado todo en esta vida serán interesantes, creo, una visualización del proceso de aprendizaje, que es aplicable no solo a esta tarea.

Formulación del problema


Hay un buen curso de DS de ODS.ai y está la tercera clase de Clasificación, árboles de decisión y el método de los vecinos más cercanos . Allí, se muestra en datos extremadamente simples y probablemente sintéticos cómo el árbol de decisión más simple da una precisión del 94.5%, y el mismo método extremadamente simple de k vecinos más cercanos da el 89% sin ningún preprocesamiento

Importar y cargar datos
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
%matplotlib inline
import warnings
warnings.filterwarnings('ignore')

df = pd.read_csv('data/telecom_churn.csv')
df['Voice mail plan'] = pd.factorize(df['Voice mail plan'])[0]
df['International plan'] = pd.factorize(df['International plan'])[0]
df['Churn'] = df['Churn'].astype('int32')
states = df['State']
y = df['Churn']
df.drop(['State','Churn'], axis = 1, inplace=True)
df.head()

Comparar madera con knn
%%time
from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV, cross_val_score
from sklearn.metrics import accuracy_score

X_train, X_holdout, y_train, y_holdout = train_test_split(df.values, y, test_size=0.3,
random_state=17)

tree = DecisionTreeClassifier(random_state=17, max_depth=5)
knn = KNeighborsClassifier(n_neighbors=10)

tree_params = {'max_depth': range(1,11), 'max_features': range(4,19)}
tree_grid = GridSearchCV(tree, tree_params, cv=10, n_jobs=-1, verbose=False)
tree_grid.fit(X_train, y_train)
tree_grid.best_params_, tree_grid.best_score_, accuracy_score(y_holdout, tree_grid.predict(X_holdout))

({'max_depth': 6, 'max_features': 16}, 0.944706386626661, 0.945)

lo mismo para knn
%%time
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

knn_pipe = Pipeline([('scaler', StandardScaler()), ('knn', KNeighborsClassifier(n_jobs=-1))])
knn_params = {'knn__n_neighbors': range(1, 10)}
knn_grid = GridSearchCV(knn_pipe, knn_params, cv=10, n_jobs=-1, verbose=False)

knn_grid.fit(X_train, y_train)
knn_grid.best_params_, knn_grid.best_score_, accuracy_score(y_holdout, knn_grid.predict(X_holdout))

({'knn__n_neighours': 9}, 0.8868409772824689, 0.891)
En este punto, sentí pena por knn que obviamente era deshonesto, porque no teníamos trabajo con la métrica. No pensé con mi cerebro, tomé características_importancias_ del árbol y normalicé la entrada. Por lo tanto, cuanto más importante es la característica, mayor es su contribución a la distancia entre puntos.

Alimentamos los datos normalizados a la importancia de las características
%%time
feature_importances = pd.DataFrame({'features': df.columns, 'importance':tree_grid.best_estimator_.feature_importances_})
print(feature_importances.sort_values(by=['importance'], inplace=False, ascending=False))

scaler = StandardScaler().fit(X_train)
X_train_transformed = scaler.transform(X_train)
X_train_transformed = X_train_transformed * np.array(feature_importances['importance'])

X_holdout_transformed = scaler.transform(X_holdout)
X_holdout_transformed = X_holdout_transformed * np.array(feature_importances['importance'])

knn_grid = GridSearchCV(KNeighborsClassifier(n_jobs=-1), {'n_neighbors': range(1, 11, 2)}, cv=5, n_jobs=-1, verbose=False)
knn_grid.fit(X_train_transformed, y_train)
print (knn_grid.best_params_, knn_grid.best_score_, accuracy_score(y_holdout, knn_grid.predict(X_holdout_transformed)))

5 5Total de minutos del día0.270386
17Llamadas de servicio al cliente0.147185
8Total de minutos de víspera0.135475
2Plan internacional0,097249
dieciséisCarga total internacional0,091671
quinceTotal de llamadas internacionales09.090008
4 4Numerar mensajes de vmail0,050646
10Cargo total de la víspera0,038593
7 7Cargo total por día0,026422
3Plan de correo de voz0,017068
onceTotal de minutos nocturnos0,014185
treceCargo total nocturno0.005742
12Total de llamadas nocturnas0.005502
9 9Total de vísperas0.003614
6 6Total de llamadas diarias0.002246
14Total de minutos internacionales0.002009
0Account length0.001998
1Area code0.000000

{'n_neighbours': 5} 0.909129875696528 0.913

El árbol acaba de compartir su conocimiento un poco con knn y ahora vemos el 91%. Eso no está muy lejos del 94.5% del árbol de vainilla. Y entonces se me ocurrió una idea. Pero, ¿cómo, de hecho, necesitamos normalizar la entrada para que knn muestre el mejor resultado?

Primero, calcularemos en nuestra mente cuánto se considerará esto ahora como "frente". 18 parámetros, para cada uno que hacemos, digamos, 10 pasos posibles de los factores en una escala logarítmica. Tenemos 10e18 opciones. Una opción con todos los posibles números impares de vecinos es menor que 10 y la validación cruzada también es 10, creo que son 1.5 segundos. Resulta 42 mil millones de años. Quizás la idea de dejar el juicio final por la noche tendrá que ser abandonada. :) Y en algún lugar por aquí pensé: “¡Hey! ¡Así que haré una bicicleta que volará!

Búsqueda de gradiente


De hecho, esta tarea probablemente solo tenga un máximo disponible. Bueno, ese no es uno, por supuesto, un área completa de buenos resultados, pero son bastante parecidos. Por lo tanto, podemos caminar a lo largo del gradiente y encontrar el punto más adecuado. El primer pensamiento fue generalizar el algoritmo genético, pero aquí el terreno adaptativo no parece estar muy cruzado, y esto sería un poco exagerado.

Intentaré hacerlo manualmente para empezar. Para impulsar factores como hiperparámetros, necesito lidiar con los escaladores. En el ejemplo anterior, como en la lección, utilicé StandartScaler, que centró la muestra de entrenamiento en promedio e hice sigma = 1. Para escalarlo bien dentro de la tubería, el hiperparámetro debe hacerse un poco más complicado. Comencé a buscar entre los convertidores que se encontraban en sklearn.procesando algo adecuado para mi caso, pero no encontré nada. Por lo tanto, traté de heredar de StandartScaler colgando un paquete adicional de factores en él.

Clase para nominalización y luego multiplicación por escala ligeramente compatible con la tubería sklearn
from sklearn.base import TransformerMixin
class StandardAndPoorScaler(StandardScaler, TransformerMixin):
    #normalization = None
    def __init__(self, copy=True, with_mean=True, with_std=True, normalization = None):
        #print("new StandardAndPoorScaler(normalization=", normalization.shape if normalization is not None else normalization, ") // ", type(self))
        self.normalization = normalization
        super().__init__(copy, with_mean, with_std)
    def fit(self, X, y=None):
        #print(type(self),".fit(",X.shape, ",", y.shape if y is not None else "<null>",")")
        super().fit(X, y)
        return self
    def partial_fit(self, X, y=None):
        #print(type(self),".partial_fit(",X.shape, ",", y.shape if y is not None else "<null>)")
        super().partial_fit(X, y)
        if self.normalization is None:
            self.normalization = np.ones((X.shape[1]))
        elif type(self.normalization) != np.ndarray:
            self.normalization = np.array(self.normalization)
        if X.shape[1] != self.normalization.shape[0]:
            raise "X.shape[1]="+X.shape[1]+" in equal self.scale.shape[0]="+self.normalization.shape[0]
    def transform(self, X, copy=None):
        #print(type(self),".transform(",X.shape,",",copy,").self.normalization", self.normalization)
        Xresult = super().transform(X, copy)
        Xresult *= self.normalization
        return Xresult
    def _reset(self):
        #print(type(self),"._reset()")
        super()._reset()
    
scaler = StandardAndPoorScaler(normalization = feature_importances['importance'])
scaler.fit(X = X_train, y = None)
print(scaler.normalization)

Intentando aplicar esta clase
%%time
knn_pipe = Pipeline([('scaler', StandardAndPoorScaler()), ('knn', KNeighborsClassifier(n_jobs=-1))])

knn_params = {'knn__n_neighbors': range(1, 11, 4), 'scaler__normalization': [feature_importances['importance']]}
knn_grid = GridSearchCV(knn_pipe, knn_params, cv=5, n_jobs=-1, verbose=False)

knn_grid.fit(X_train, y_train)
knn_grid.best_params_, knn_grid.best_score_, accuracy_score(y_holdout, knn_grid.predict(X_holdout))

({'knn__n_neighours': 5, 'scaler__normalization': Nombre: importancia, dtype: float64}, 0.909558508358337, 0.913)

El resultado es ligeramente diferente de mis expectativas. Bueno, eso es, en principio, todo funciona. Solo para entender esto, tuve que reproducir esta clase con todas las agallas desde cero en tres horas, y solo entonces me di cuenta de que la impresión no se imprime no porque sklearn se haya hecho de manera incorrecta, sino porque GridSearchCV crea clones en la transmisión principal , pero los configura y entrena en otros hilos. Y todo lo que imprime en otras transmisiones desaparece en el olvido. Pero si coloca n_jobs = 1, todas las llamadas a funciones anuladas se muestran como lindas. El conocimiento salió muy caro, ahora también lo tienes, y lo pagaste leyendo un artículo tedioso.

Bien, sigamos adelante. Ahora quiero dar alguna variación para cada uno de sus parámetros, y luego darle un poco menos en torno al mejor valor, y así sucesivamente hasta que obtenga un resultado similar a la realidad. Esta será la primera línea de base grosera de lo que eventualmente debería obtener el algoritmo de mis sueños.

Formaré varias opciones para volver a pesar, que difieren en varios parámetros
feature_base = feature_importances['importance']
searchArea = np.array([feature_base - .05, feature_base, feature_base + .05])
searchArea[searchArea < 0] = 0
searchArea[searchArea > 1] = 1
print(searchArea[2,:] - searchArea[0,:])

import itertools

affected_props = [2,3,4]
parametrs_ranges = np.concatenate([
    np.linspace(searchArea[0,affected_props], searchArea[1,affected_props], 2, endpoint=False),
    np.linspace(searchArea[1,affected_props], searchArea[2,affected_props], 3, endpoint=True)]).transpose()

print(parametrs_ranges) #      .  125 
recombinations = itertools.product(parametrs_ranges[0],parametrs_ranges[1],parametrs_ranges[1])

variances = []
for item in recombinations: #          ,       Python .
    varince = feature_base.copy()
    varince[affected_props] = item
    variances.append(varince)
print(variances[0])
print(len(variances))
#  knn   ,               .

Bueno, el conjunto de datos para el primer experimento está listo. Ahora intentaré experimentar con los datos, comenzando por una búsqueda exhaustiva de las 15 opciones resultantes.

Hacemos una selección de prueba de parámetros como en el artículo
%%time
#scale = np.ones([18])
knn_pipe = Pipeline([('scaler', StandardAndPoorScaler()), ('knn', KNeighborsClassifier(n_neighbors = 7 , n_jobs=-1))])

knn_params = {'scaler__normalization': variances} # 'knn__n_neighbors': range(3, 9, 2), 
knn_grid = GridSearchCV(knn_pipe, knn_params, cv=10, n_jobs=-1, verbose=False)

knn_grid.fit(X_train, y_train)
knn_grid.best_params_, knn_grid.best_score_, accuracy_score(y_holdout, knn_grid.predict(X_holdout))

Bueno, todo está mal, se dedicó tiempo a un avance y el resultado es muy inestable. Esto también se ve en la verificación X_holdout, el resultado baila como en un caleidoscopio con pequeños cambios en los datos de entrada. Intentaré un enfoque diferente. Cambiaré solo un parámetro a la vez, pero con una discretización mucho mayor.

Cambio una cuarta propiedad
%%time
affected_property = 4
parametrs_range = np.concatenate([
    np.linspace(searchArea[0,affected_property], searchArea[1,affected_property], 29, endpoint=False),
    np.linspace(searchArea[1,affected_property], searchArea[2,affected_property], 30, endpoint=True)]).transpose()

print(searchArea[1,affected_property])
print(parametrs_range) # C   ,  .


variances = []
for item in parametrs_range: #          ,       Python .
    varince = feature_base.copy()
    varince[affected_property] = item
    variances.append(varince)
print(variances[0])
print(len(variances))
#  knn   ,               .

knn_pipe = Pipeline([('scaler', StandardAndPoorScaler()), ('knn', KNeighborsClassifier(n_neighbors = 7 , n_jobs=-1))])

knn_params = {'scaler__normalization': variances} # 'knn__n_neighbors': range(3, 9, 2), 
knn_grid = GridSearchCV(knn_pipe, knn_params, cv=10, n_jobs=-1, verbose=False)

knn_grid.fit(X_train, y_train)
knn_grid.best_params_, knn_grid.best_score_, accuracy_score(y_holdout, knn_grid.predict(X_holdout))

({'scaler__normalization': 4 0.079957 Nombre: importancia, dtype: float64}, 0.9099871410201458, 0.913)

Bueno, ¿qué tenemos con un ganso? Cambios de una a dos décimas de porcentaje en validación cruzada, y un salto de medio porcentaje en X_holdout si observa diferentes propiedades afectadas. Aparentemente, es esencial y barato mejorar la situación si comienzas con el hecho de que el árbol nos da que es imposible con esos datos. Pero supongamos que no tenemos una distribución de peso inicial y conocida, e intentamos hacer lo mismo en un punto arbitrario del ciclo con pequeños pasos. Es muy interesante a lo que llegaremos.

Llenado inicial
searchArea = np.array([np.zeros((18,)), np.ones((18,)) /18, np.ones((18,))])
print(searchArea[:,0])

history_parametrs = [searchArea[1,:].copy()]
scaler = StandardAndPoorScaler(normalization=searchArea[1,:])
scaler.fit(X_train)
knn = KNeighborsClassifier(n_neighbors = 7 , n_jobs=-1)
knn.fit(scaler.transform(X_train), y_train)
history_holdout_score = [accuracy_score(y_holdout, knn.predict(scaler.transform(X_holdout)))]

Función que cambia ligeramente un parámetro (con registros de depuración)
%%time
def changePropertyNormalization(affected_property, points_count = 15):
    test_range = np.concatenate([
        np.linspace(searchArea[0,affected_property], searchArea[1,affected_property], points_count//2, endpoint=False),
        np.linspace(searchArea[1,affected_property], searchArea[2,affected_property], points_count//2 + 1, endpoint=True)]).transpose()
    variances = [searchArea[1,:].copy() for i in range(test_range.shape[0])]
    for row in range(len(variances)):
        variances[row][affected_property] = test_range[row]
    
    knn_pipe = Pipeline([('scaler', StandardAndPoorScaler()), ('knn', KNeighborsClassifier(n_neighbors = 7 , n_jobs=-1))])
    knn_params = {'scaler__normalization': variances} # 'knn__n_neighbors': range(3, 9, 2), 
    knn_grid = GridSearchCV(knn_pipe, knn_params, cv=10, n_jobs=-1, verbose=False)

    knn_grid.fit(X_train, y_train)
    holdout_score = accuracy_score(y_holdout, knn_grid.predict(X_holdout))
    best_param = knn_grid.best_params_['scaler__normalization'][affected_property]
    print(affected_property,
          'property:', searchArea[1, affected_property], "=>", best_param,
          'holdout:', history_holdout_score[-1], "=>", holdout_score, '(', knn_grid.best_score_, ')')
    #             .
    before = searchArea[:, affected_property]
    propertySearchArea = searchArea[:, affected_property].copy()
    if best_param == propertySearchArea[0]:
        print('|<<')
        searchArea[0, affected_property] = best_param/2 if best_param > 0.01 else 0
        searchArea[2, affected_property] = (best_param + searchArea[2, affected_property])/2
        searchArea[1, affected_property] = best_param
    elif best_param == propertySearchArea[2]:
        print('>>|')
        searchArea[2, affected_property] = (best_param + 1)/2 if best_param < 0.99 else 1
        searchArea[0, affected_property] = (best_param + searchArea[0, affected_property])/2
        searchArea[1, affected_property] = best_param
    elif best_param < (propertySearchArea[0] + propertySearchArea[1])/2:
        print('<<')
        searchArea[0, affected_property] = max(propertySearchArea[0]*1.1 - .1*propertySearchArea[1], 0)
        searchArea[2, affected_property] = (best_param + propertySearchArea[2])/2
        searchArea[1, affected_property] = best_param
    elif best_param > (propertySearchArea[1] + propertySearchArea[2])/2:
        print('>>')
        searchArea[0, affected_property] = (best_param + propertySearchArea[0])/2
        searchArea[2, affected_property] = min(propertySearchArea[2]*1.1 - .1*propertySearchArea[1], 1)
        searchArea[1, affected_property] = best_param
    elif best_param < propertySearchArea[1]:
        print('<')
        searchArea[2, affected_property] = searchArea[1, affected_property]*.25 + .75*searchArea[2, affected_property]
        searchArea[1, affected_property] = best_param
    elif best_param > propertySearchArea[1]:
        print('>')
        searchArea[0, affected_property] = searchArea[1, affected_property]*.25 + .75*searchArea[0, affected_property]
        searchArea[1, affected_property] = best_param
    else:
        print('=')
        searchArea[0, affected_property] = searchArea[1, affected_property]*.25 + .75*searchArea[0, affected_property]
        searchArea[2, affected_property] = searchArea[1, affected_property]*.25 + .75*searchArea[2, affected_property]
    normalization = searchArea[1,:].sum() #,      .
    searchArea[:,:] /= normalization
    print(before, "=>",searchArea[:, affected_property])
    history_parametrs.append(searchArea[1,:].copy())
    history_holdout_score.append(holdout_score)
    
changePropertyNormalization(1, 9)
changePropertyNormalization(1, 9)

No optimicé nada en ningún lado y, como resultado, di el siguiente paso decisivo durante casi media hora:

Texto oculto
40 .
%%time
#   
searchArea = np.array([np.zeros((18,)), np.ones((18,)) /18, np.ones((18,))])
print(searchArea[:,0])

history_parametrs = [searchArea[1,:].copy()]
scaler = StandardAndPoorScaler(normalization=searchArea[1,:])
scaler.fit(X_train)
knn = KNeighborsClassifier(n_neighbors = 7 , n_jobs=-1)
knn.fit(scaler.transform(X_train), y_train)
history_holdout_score = [accuracy_score(y_holdout, knn.predict(scaler.transform(X_holdout)))]

for tick in range(40):
    for p in range(searchArea.shape[1]):
        changePropertyNormalization(p, 7)
    
print(searchArea[1,:])
print(history_holdout_score)

La precisión resultante de knn: 91.9% Mejor que cuando arrancamos los datos del árbol. Y mucho, mucho mejor que en la versión original. Compare lo que tenemos con la importancia de las características de acuerdo con el árbol de decisión:

Visualización de la importancia de las características según knn
feature_importances['knn_importance'] = history_parametrs[-1]
diagramma = feature_importances.copy()
indexes = diagramma.index
diagramma.index = diagramma['features']
diagramma.drop('features', 1, inplace = True)
diagramma.plot(kind='bar');
plt.savefig("images/pic1.png", format = 'png')
plt.show()
feature_importances





¿Parece ser? Si parece. Pero lejos de ser idéntico. Interesante observación. Hay varias características en el conjunto de datos que se duplican completamente entre sí, por ejemplo, 'Total night minutes' y 'Total night charge'. Así que presta atención, el propio knn aserró una parte significativa de tales características repetidas.

Guardaremos los resultados en un archivo, de lo contrario es un poco inconveniente volver al trabajo ...
parametrs_df = pd.DataFrame(history_parametrs)
parametrs_df['scores'] = history_holdout_score
parametrs_df.index.name = 'index'
parametrs_df.to_csv('parametrs_and_scores.csv')

recomendaciones


Bueno, el resultado .919 per se no es malo para knn, hay 1.5 veces menos errores que en la versión estándar y 7% menos que cuando tomamos el árbol de feature_importance para conducir. Pero lo más interesante es que ahora tenemos feature_importance según knn. Es algo diferente de lo que nos dijo el árbol. Por ejemplo, tree y knn tienen opiniones diferentes sobre cuáles de los signos no son importantes para nosotros.

Bueno, al final. Obtuvimos algo relativamente nuevo e inusual, teniendo una reserva de conocimiento de tres conferencias mlcourse.ai

ods y Google para responder preguntas simples sobre python. En mi opinión, no está mal.

Ahora diapositivas


Un subproducto del trabajo del algoritmo es el camino que ha recorrido. El camino, sin embargo, es de 18 dimensiones, lo que dificulta un poco su conciencia de seguir en tiempo real lo que el algoritmo está haciendo allí, aprender o usar basura no es tan conveniente. De acuerdo con la programación de errores, esto, de hecho, no siempre es visible. El error puede no cambiar notablemente durante mucho tiempo, pero el algoritmo está muy ocupado, arrastrándose a lo largo de un valle largo y estrecho en el espacio adaptativo. Por lo tanto, aplicaré, para empezar, el primer enfoque más simple pero bastante informativo: proyecto aleatoriamente un espacio de 18 dimensiones en un espacio de dos dimensiones para que las contribuciones de todos los parámetros, independientemente de su importancia, sean únicas. De hecho, el camino de 18 dimensiones es muy pequeño, en nuestro artículo Peeping Over the Throws of a Neural Network También admiré el espacio de las escalas de todas las sinapsis que tenía la red neuronal y fue agradable e informativo.

Leo los datos del archivo, si regreso al trabajo, después de haber superado la etapa de capacitación.
parametrs_df = pd.read_csv('parametrs_and_scores.csv', index_col = 'index')
history_holdout_score = np.array(parametrs_df['scores'])
parametrs_df.drop('scores',axis=1)
history_parametrs = np.array(parametrs_df.drop('scores',axis=1))

El error en la validación deja de cambiar desde algún punto. Aquí sería posible atornillar una parada automática de aprendizaje y usar la función recibida por el resto de mi vida, pero ya tengo un poco de tiempo. :(

Determinamos cuánto estudiar.
last = history_holdout_score[-1]
steps = np.arange(0, history_holdout_score.shape[0])[history_holdout_score != last].max()
print(steps/18)

35.5555555555555556
Cambiamos un parámetro a la vez, por lo que un ciclo de optimización consta de 18 pasos. Resulta que tuvimos 36 pasos significativos, o algo así. Ahora intentemos visualizar la trayectoria a lo largo de la cual se entrenó el método.


Texto oculto
%%time
#    :
import matplotlib.pyplot as plt
%matplotlib inline
import random
import math
random.seed(17)
property_projection = np.array([[math.sin(a), math.cos(a)] for a in [random.uniform(-math.pi, math.pi) for i in range(history_parametrs[0].shape[0])]]).transpose()
history = np.array(history_parametrs[::18]) #   - 18 .
#           . :(
points = np.array([(history[i] * property_projection).sum(axis=1) for i in range(history.shape[0])])
plt.plot(points[:36,0],points[0:36,1]);
plt.savefig("images/pic2.png", format = 'png')
plt.show()



Se puede ver que una parte importante del viaje se completó en los primeros cuatro pasos. Veamos el resto del camino con el aumento

Sin los primeros 4 puntos
plt.plot(points[4:36,0],points[4:36,1]);
plt.savefig("images/pic3.png", format = 'png')



Echemos un vistazo más de cerca a la parte final del camino y veamos qué hizo la maestra después de llegar a su destino.

acercarse
plt.plot(points[14:36,0],points[14:36,1]);
plt.savefig("images/pic4.png", format = 'png')
plt.show()
plt.plot(points[24:36,0],points[24:36,1]);
plt.plot(points[35:,0],points[35:,1], color = 'red');
plt.savefig("images/pic5.png", format = 'png')
plt.show()





Se puede ver que el algoritmo se está entrenando intensamente. Hasta que encuentre su destino. El punto específico, por supuesto, depende de la aleatorización en la validación cruzada. Pero independientemente del punto específico, la imagen general de lo que está sucediendo es comprensible.

Por cierto, solía usar ese horario para demostrar el proceso de aprendizaje.
No se muestra toda la trayectoria, sino los últimos pasos con suavizado deslizante de la escala. Un ejemplo se puede encontrar en mi otro artículo, "Espiamos los lanzamientos de una red neuronal". Y sí, por supuesto, todos los que se encuentran con tal visualización inmediatamente preguntan por qué todos los factores tienen el mismo peso, importancia, y luego tienen diferentes. La última vez en el artículo, traté de volver a evaluar la importancia de las sinapsis y resultó menos informativo.

Esta vez, armado con nuevos conocimientos, intentaré usar t-SNE para desplegar el espacio multidimensional en una proyección en la que todo puede ser mejor.

t-SNE
%%time
import sklearn.manifold as manifold
tsne = manifold.TSNE(random_state=19)
tsne_representation = tsne.fit_transform(history)
plt.plot(tsne_representation[:, 0], tsne_representation[:, 1])
plt.savefig("images/pic6.png", format = 'png')
plt.show();



t-Sne parece haber desplegado el espacio para que se comiera por completo la escala de los cambios para aquellas características que rápidamente dejaron de cambiar, lo que hizo que la imagen fuera completamente informativa. Conclusión: no intente deslizar los algoritmos en lugares que no estén destinados a ellos .: \

No puedes leer más


También intenté inyectar tsne en el interior para visualizar estados de optimización intermedios, con la esperanza de que resultara la belleza. pero resultó que no era belleza, algo de basura. Si está interesado, vea cómo hacerlo. Internet está lleno de ejemplos de este tipo de código de inyección, pero simplemente copiando no funcionan como sustitutos contenidos en sklearn.manifold.t_sne función interna _gradient_descent , y dependiendo de la versión puede ser muy diferente tanto en la firma como en el tratamiento de las variables internas. Así que solo encuentre las fuentes en usted, elija su versión de la función desde allí e inserte solo una línea que agregue volcados intermedios a su propia variable:

posiciones.append (p.copy ()) # Guardamos la posición actual.

Y luego, como, visualizamos bellamente lo que obtenemos como resultado:

Código de inyección
from time import time
from scipy import linalg
# This list will contain the positions of the map points at every iteration.
positions = []
def _gradient_descent(objective, p0, it, n_iter,
                      n_iter_check=1, n_iter_without_progress=300,
                      momentum=0.8, learning_rate=200.0, min_gain=0.01,
                      min_grad_norm=1e-7, verbose=0, args=None, kwargs=None):
    # The documentation of this function can be found in scikit-learn's code.
    if args is None:
        args = []
    if kwargs is None:
        kwargs = {}

    p = p0.copy().ravel()
    update = np.zeros_like(p)
    gains = np.ones_like(p)
    error = np.finfo(np.float).max
    best_error = np.finfo(np.float).max
    best_iter = i = it

    tic = time()
    for i in range(it, n_iter):
        positions.append(p.copy()) # We save the current position.
        
        check_convergence = (i + 1) % n_iter_check == 0
        # only compute the error when needed
        kwargs['compute_error'] = check_convergence or i == n_iter - 1

        error, grad = objective(p, *args, **kwargs)
        grad_norm = linalg.norm(grad)

        inc = update * grad < 0.0
        dec = np.invert(inc)
        gains[inc] += 0.2
        gains[dec] *= 0.8
        np.clip(gains, min_gain, np.inf, out=gains)
        grad *= gains
        update = momentum * update - learning_rate * grad
        p += update

        if check_convergence:
            toc = time()
            duration = toc - tic
            tic = toc

            if verbose >= 2:
                print("[t-SNE] Iteration %d: error = %.7f,"
                      " gradient norm = %.7f"
                      " (%s iterations in %0.3fs)"
                      % (i + 1, error, grad_norm, n_iter_check, duration))

            if error < best_error:
                best_error = error
                best_iter = i
            elif i - best_iter > n_iter_without_progress:
                if verbose >= 2:
                    print("[t-SNE] Iteration %d: did not make any progress "
                          "during the last %d episodes. Finished."
                          % (i + 1, n_iter_without_progress))
                break
            if grad_norm <= min_grad_norm:
                if verbose >= 2:
                    print("[t-SNE] Iteration %d: gradient norm %f. Finished."
                          % (i + 1, grad_norm))
                break

    return p, error, i

manifold.t_sne._gradient_descent = _gradient_descent

Aplicar el t-SNE `` fijo ''
tsne_representation = manifold.TSNE(random_state=17).fit_transform(history)
X_iter = np.dstack(position.reshape(-1, 2) for position in positions)
position_reshape = [position.reshape(-1, 2) for position in positions]
print(position_reshape[0].shape)
print('[0] min', position_reshape[0][:,0].min(),'max', position_reshape[0][:,0].max())
print('[1] min', position_reshape[1][:,0].min(),'max', position_reshape[1][:,0].max())
print('[2] min', position_reshape[2][:,0].min(),'max', position_reshape[2][:,0].max())

(41, 2)
[0] min -0.00018188123 max 0.00027207955
[1] min -0.05136269 max 0.032607622
[2] min -4.392309 max 7.9074526
Los valores bailan en un rango muy amplio, así que los escalaré antes de dibujarlos. En los ciclos, todo esto se hace kapets lentamente. :(

Yo escalo
%%time
from sklearn.preprocessing import MinMaxScaler
minMaxScaler = MinMaxScaler()
minMaxScaler.fit_transform(position_reshape[0])
position_reshape = [minMaxScaler.fit_transform(frame) for frame in position_reshape]
position_reshape[0].min(), position_reshape[0].max()

Animar
%%time

from matplotlib.animation import FuncAnimation, PillowWriter
#plt.style.use('seaborn-pastel')

fig = plt.figure()

ax = plt.axes(xlim=(0, 1), ylim=(0, 1))
line, = ax.plot([], [], lw=3)

def init():
    line.set_data([], [])
    return line,
def animate(i):
    x = position_reshape[i][:,0]
    y = position_reshape[i][:,1]
    line.set_data(x, y)
    return line,

anim = FuncAnimation(fig, animate, init_func=init, frames=36, interval=20, blit=True, repeat_delay = 1000)
anim.save('images/animate_tsne_learning.gif', writer=PillowWriter(fps=5))



Es instructivo en términos de habilidades, pero completamente inútil en esta tarea y feo.

En esto te digo adiós. Espero que la idea de que incluso desde knn puedas obtener algo nuevo e interesante, así como piezas de código, te ayude y te diviertas con los datos en esta fiesta intelectual durante la plaga.

All Articles