Sélection de l'importance des caractéristiques pour les k voisins les plus proches (puits ou autres hyperparamètres) par descente similaire au gradient

Un vrai non-sens peut non seulement accomplir l'impossible, mais aussi servir d'exemple d'avertissement

En expérimentant la tâche d'apprentissage automatique la plus simple, j'ai trouvé qu'il serait intéressant de sélectionner 18 hyperparamètres en même temps sur une plage assez large. Dans mon cas, tout était si simple que la tâche pouvait être exécutée avec une puissance informatique brute.

Lorsque vous apprenez quelque chose, il peut être très intéressant d'inventer une sorte de vélo. Parfois, il s'avère vraiment trouver quelque chose de nouveau. Parfois, il s'avère que tout a été inventé avant moi. Mais même si je ne fais que répéter le chemin parcouru bien avant moi, en récompense, j'ai souvent une compréhension des mécanismes sous-jacents des algorithmes de leurs capacités et de leurs limites internes. Auquel je vous invite.

En Python et DS, pour faire simple, je suis un débutant et je fais beaucoup de choses qui peuvent être implémentées dans une seule équipe selon mon ancienne habitude de programmation, que Python punit en ralentissant, pas parfois, mais par ordre de grandeur. Par conséquent, je télécharge tout mon code dans le référentiel. Si vous savez comment l'implémenter beaucoup plus efficacement - ne soyez pas timide, modifiez-le ou écrivez dans les commentaires. https://github.com/kraidiky/GDforHyperparameters

Ceux qui sont déjà un cool datatanist et qui ont tout essayé dans cette vie seront intéressants, je crois, une visualisation du processus d'apprentissage, qui ne s'applique pas seulement à cette tâche.

Formulation du problème


Il y a un si bon cours DS de ODS.ai et il y a la troisième conférence Classification, arbres de décision et la méthode des voisins les plus proches . Là, il est montré sur des données extrêmement simples et probablement synthétiques comment l'arbre de décision le plus simple donne une précision de 94,5%, et la même méthode extrêmement simple de k voisins les plus proches donne 89% sans aucun prétraitement

Importer et charger des données
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()

Comparer le bois avec le tricot
%%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)

idem pour 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_neighbors': 9}, 0.8868409772824689, 0.891)
À ce stade, je me suis senti désolé pour knn qui était évidemment malhonnête, car nous n'avions aucun travail avec la métrique. Je ne pensais pas avec mon cerveau, j'ai pris feature_importances_ de l'arbre et normalisé l'entrée. Ainsi, plus la caractéristique est importante, plus sa contribution est grande la distance entre les points.

Nous alimentons les données normalisées à l'importance des fonctionnalités
%%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)))

5Nombre total de minutes par jour0,270386
17Appels au service client0,147185
8Total des minutes de veille0,135475
2Plan international0,097249
seizeTotal charge internationale0,091671
quinzeTotal appels internationaux09.090008
4Nombre de messages vmail0,050646
dixCharge totale la veille0,038593
7Charge journalière totale0,026422
3Plan de messagerie vocale0,017068
OnzeTotal des minutes de nuit0,014185
treizeCharge de nuit totale0,005742
12Total des appels de nuit0,005502
9Total des appels la veille0,003614
6Nombre total d'appels d'une journée0,002246
14Total des minutes internationales0.002009
0Account length0.001998
1Area code0.000000

{'n_neighbors': 5} 0,909129875696528 0,913

L'arbre vient de partager un peu de connaissances avec knn et maintenant nous en voyons 91%. Ce n'est pas si loin de 94,5% de la vanille. Et puis une idée m'est venue. Mais comment, en fait, devons-nous normaliser l'entrée pour que knn affiche le meilleur résultat?

Premièrement, nous allons estimer dans notre esprit combien cela sera désormais considéré comme «front». 18 paramètres, pour chacun nous faisons, disons, 10 étapes possibles des facteurs dans l'échelle logarithmique. Nous avons des options 10e18. Une option avec tout le nombre impair possible de voisins est inférieure à 10 et la validation croisée est également de 10, je pense environ 1,5 seconde. Il s'avère que 42 milliards d'années. Peut-être faudra-t-il abandonner l'idée de quitter le calcul pour la nuit. :) Et quelque part par ici, je me suis dit: «Hé! Je vais donc faire un vélo qui volera! "

Recherche par dégradé


En fait, cette tâche n'a probablement qu'un seul maximum disponible. Eh bien, ce n'est pas un bien sûr, tout un domaine de bons résultats, mais ils se ressemblent à peu près. Par conséquent, nous pouvons simplement marcher le long du gradient et trouver le point le plus approprié. La première pensée a été de généraliser l'algorithme génétique, mais ici le terrain adaptatif ne semble pas très traversé, et ce serait un peu exagéré.

Je vais essayer de le faire manuellement pour commencer. Pour pousser des facteurs en tant qu'hyperparamètres, je dois faire face à des détartreurs. Dans l'exemple précédent, comme dans la leçon, j'ai utilisé StandartScaler, qui a centré l'échantillon d'apprentissage en moyenne et a fait sigma = 1. Afin de bien le mettre à l'échelle à l'intérieur du pipeline, l'hyperparamètre doit être rendu un peu plus délicat. J'ai commencé à rechercher parmi les convertisseurs se trouvant dans sklearn.preprocessing quelque chose qui convenait à mon cas, mais je n'ai rien trouvé. Par conséquent, j'ai essayé d'hériter de StandartScaler en y suspendant un ensemble supplémentaire de facteurs.

Classe de nominalisation puis de multiplication par échelle légèrement compatible avec le pipeline 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)

Essayer d'appliquer cette classe
%%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_neighbors': 5, 'scaler__normalization': Name: importance, dtype: float64}, 0.909558508358337, 0.913)

Le résultat est légèrement différent de mes attentes. Eh bien, en principe, tout fonctionne. Juste pour comprendre cela, j'ai dû reproduire cette classe avec toutes les tripes à partir de zéro en trois heures, et seulement alors j'ai réalisé que l'impression n'imprimait pas non pas parce que sklearn était mal fait, mais parce que GridSearchCV crée des clones dans le flux principal , mais les configure et les entraîne dans d'autres threads. Et tout ce que vous imprimez dans d'autres flux disparaît dans l'oubli. Mais si vous mettez n_jobs = 1, tous les appels aux fonctions remplacées sont affichés comme mignons. Les connaissances sont sorties très chères, maintenant vous les avez aussi, et vous les avez payées en lisant un article fastidieux.

Bon, passons. Maintenant, je veux donner une certaine variance pour chacun de leurs paramètres, puis le donner un peu moins autour de la meilleure valeur, et ainsi de suite jusqu'à ce que j'obtienne un résultat similaire à la réalité. Ce sera la première ligne de base grossière de ce qui devrait finalement obtenir l'algorithme de mes rêves.

Je formerai plusieurs options de repondération, différant par plusieurs paramètres
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   ,               .

Eh bien, l'ensemble de données pour la première expérience est prêt. Maintenant, je vais essayer d'expérimenter avec les données, pour commencer par une recherche exhaustive des 15 options résultantes.

Nous faisons une sélection d'essai des paramètres comme dans l'article
%%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))

Eh bien, tout va mal, du temps a été consacré à une percée et le résultat est très instable. Cela se voit également à partir de la vérification X_holdout, le résultat danse comme dans un kaléidoscope avec des modifications mineures des données d'entrée. Je vais essayer une approche différente. Je ne changerai qu'un paramètre à la fois, mais avec une discrétisation beaucoup plus importante.

Je change une 4ème propriété
%%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 Nom: importance, dtype: float64}, 0.9099871410201458, 0.913)

Eh bien, qu'avons -nous avec une oie? Décalages d'un à deux dixièmes de pour cent sur la validation croisée, et un demi-pour cent sur X_holdout si vous regardez les propriétés affectées différentes. Apparemment, il est essentiel et bon marché d'améliorer la situation si vous commencez avec le fait que l'arbre nous donne, c'est impossible sur de telles données. Mais supposons que nous n'ayons pas de distribution de poids initiale connue, et essayons de faire la même chose à un moment arbitraire du cycle avec de petites étapes. Il est très intéressant de savoir à quoi nous en viendrons.

Remplissage initial
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)))]

Fonction modifiant légèrement un paramètre (avec les journaux de débogage)
%%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)

Je n’ai rien optimisé nulle part et, par conséquent, j’ai franchi la prochaine étape décisive pendant près d’une demi-heure:

Texte masqué
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 précision résultante de knn: 91,9% meilleure que lorsque nous déchirons les données de l'arbre. Et beaucoup, beaucoup mieux que dans la version originale. Comparez ce que nous avons avec l'importance des fonctionnalités selon l'arbre de décision:

Visualisation de l'importance des fonctionnalités selon 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





Semble être? Oui, semble-t-il. Mais loin d'être identique. Observation intéressante. Il existe plusieurs fonctionnalités dans l'ensemble de données qui se dupliquent complètement, par exemple, «Total des minutes de nuit» et «Total de nuit de charge». Alors faites attention, knn a lui-même scié une partie importante de ces caractéristiques répétées.

Nous enregistrerons les résultats dans un fichier, sinon il est quelque peu gênant de retourner au travail ...
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')

résultats


Eh bien, le résultat .919 en soi n'est pas mauvais pour knn, il y a 1,5 fois moins d'erreurs que dans la version vanille et 7% de moins que lorsque nous avons pris l'arborescence feature_importance pour conduire. Mais la chose la plus intéressante est que nous avons maintenant feature_importance selon knn lui-même. C'est quelque peu différent de ce que l'arbre nous a dit. Par exemple, tree et knn ont des opinions différentes sur lesquels des signes ne sont pas importants du tout pour nous.

Enfin, finalement. Nous avons obtenu quelque chose de relativement nouveau et inhabituel, ayant une réserve de connaissances de trois conférences mlcourse.ai

ods et Google pour répondre à des questions simples sur python. À mon avis, pas mal.

Maintenant diapositives


Un sous-produit du travail de l'algorithme est le chemin qu'il a parcouru. Certes, le chemin est à 18 dimensions, ce qui entrave un peu sa conscience, eh bien, pour suivre en temps réel ce que fait l'algorithme là-bas, apprendre ou utiliser des ordures n'est pas si pratique. Selon le calendrier des erreurs, cela n'est en fait pas toujours visible. L'erreur peut ne pas changer sensiblement pendant longtemps, mais l'algorithme est très occupé, rampant le long d'une longue vallée étroite dans l'espace adaptatif. Par conséquent, j'appliquerai, pour commencer, la première approche la plus simple mais la plus informative - je projette au hasard un espace à 18 dimensions sur un espace à deux dimensions afin que les contributions de tous les paramètres, quelle que soit leur signification, soient uniques. En fait, le chemin à 18 dimensions est très petit, dans notre article Peeping Over the Throws of a Neural Network J'ai également admiré l'espace des échelles de toutes les synapses que possédait le réseau neuronal et c'était agréable et instructif.

J'ai lu les données du fichier, si je retourne au travail, après avoir passé la formation elle-même
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))

L'erreur sur la validation cesse de changer à partir d'un certain point. Ici, il serait possible de visser un arrêt automatique d'apprentissage et d'utiliser la fonction reçue pour le reste de ma vie, mais j'ai déjà un peu de temps. :(

Nous déterminons combien étudier.
last = history_holdout_score[-1]
steps = np.arange(0, history_holdout_score.shape[0])[history_holdout_score != last].max()
print(steps/18)

35.5555555555555556
Nous avons changé un paramètre à la fois, donc un cycle d'optimisation se compose de 18 étapes. Il s'avère que nous avons eu 36 étapes significatives, ou quelque chose comme ça. Essayons maintenant de visualiser la trajectoire le long de laquelle la méthode a été formée.


Texte masqué
%%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()



On peut voir qu'une partie importante du voyage a été réalisée au cours des quatre premières étapes. Regardons le reste du chemin avec l'augmentation

Sans les 4 premiers points
plt.plot(points[4:36,0],points[4:36,1]);
plt.savefig("images/pic3.png", format = 'png')



Examinons de plus près la dernière partie du chemin et voyons ce que l'enseignant a fait après avoir atteint sa destination.

se rapprocher
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()





On peut voir que l'algorithme est entraîné intensément. Jusqu'à ce qu'il trouve sa destination. Le point spécifique, bien sûr, dépend de la randomisation dans la validation croisée. Mais quel que soit le point spécifique, l'image générale de ce qui se passe est compréhensible.

Soit dit en passant, j'avais l'habitude d'utiliser un tel calendrier pour démontrer le processus d'apprentissage.
Non pas la trajectoire entière est affichée, mais les dernières étapes avec un lissage glissant de l'échelle. Un exemple peut être trouvé dans mon autre article, «Nous espions sur les lancers d'un réseau neuronal». Et oui, bien sûr, tous ceux qui rencontrent une telle visualisation demandent immédiatement pourquoi tous les facteurs ont le même poids, la même importance, alors ils ont différents. La dernière fois dans l'article, j'ai essayé de repenser l'importance des synapses et cela s'est avéré moins instructif.

Cette fois, armée de nouvelles connaissances, je vais essayer d'utiliser t-SNE pour déployer un espace multidimensionnel dans une projection où tout peut être mieux.

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 semble avoir déplié l'espace de sorte qu'il a complètement mangé l'échelle des changements pour les fonctionnalités qui ont rapidement cessé de changer, ce qui a rendu l'image complètement non informative. Conclusion - n'essayez pas de glisser les algorithmes dans des endroits qui ne leur sont pas destinés: \

Vous ne pouvez pas lire plus loin


J'ai également essayé d'injecter tsne à l'intérieur pour visualiser les états d'optimisation intermédiaires, dans l'espoir que la beauté se révèle. mais il s'est avéré que ce n'était pas de la beauté, des ordures. Si vous êtes intéressé, voyez comment le faire. Internet est parsemé d'exemples de ce type de code d'injection, mais en copiant simplement, ils ne pa botent pas car le substitut contenu dans sklearn.manifold.t_sne fonction interne _gradient_descent , et selon la version peut être très différent à la fois dans la signature et sur le traitement des variables internes. Il vous suffit donc de trouver les sources en vous-même, de sélectionner votre version de la fonction à partir de là et d'y insérer une seule ligne qui ajoute des vidages intermédiaires à votre variable:

positions.append (p.copy ()) # Nous enregistrons la position actuelle.

Et puis, comme, nous visualisons magnifiquement ce que nous obtenons en conséquence:

Code d'injection
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

Appliquer le t-SNE `` fixe ''
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
Les valeurs dansent dans une très large plage, je vais donc les mettre à l'échelle avant de les dessiner. Sur les cycles, tout cela se fait kapets lentement. :(

Je l'échelle
%%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()

Animer
%%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))



C'est instructif en termes de compétences, mais absolument inutile dans cette tâche et moche.

Sur ce, je vous dis au revoir. J'espère que l'idée que même de knn vous pouvez obtenir quelque chose de nouveau et d'intéressant, ainsi que des morceaux de code, vous aidera et vous amusera avec les données de cette fête intellectuelle pendant la peste.

All Articles