Auswahl der Wichtigkeit von Merkmalen für k-nächste Nachbarn (Well oder andere Hyperparameter) durch Abstieg ähnlich dem Gradienten

Ein wahrer Unsinn kann nicht nur das Unmögliche erfüllen, sondern auch als Warnbeispiel dienen

Beim Experimentieren mit der einfachsten Aufgabe des maschinellen Lernens fand ich es interessant, 18 Hyperparameter gleichzeitig in einem ziemlich weiten Bereich auszuwählen. In meinem Fall war alles so einfach, dass die Aufgabe mit brutaler Computerleistung erledigt werden konnte.

Wenn man etwas lernt, kann es sehr interessant sein, eine Art Fahrrad zu erfinden. Manchmal stellt sich heraus, dass sich wirklich etwas Neues einfallen lässt. Manchmal stellt sich heraus, dass alles vor mir erfunden wurde. Aber selbst wenn ich nur den Weg wiederhole, den ich lange vor mir zurückgelegt habe, bekomme ich als Belohnung oft ein Verständnis für die zugrunde liegenden Mechanismen der Algorithmen hinsichtlich ihrer Fähigkeiten und internen Einschränkungen. Zu dem ich dich einlade.

In Python und DS, um es milde auszudrücken, bin ich ein Anfänger und mache viele Dinge, die gemäß meiner alten Programmiergewohnheit in einem Team implementiert werden können. Python bestraft dies, indem er nicht zeitweise, sondern um Größenordnungen langsamer wird. Daher lade ich meinen gesamten Code in das Repository hoch. Wenn Sie wissen, wie Sie es viel effizienter implementieren können, seien Sie nicht schüchtern, bearbeiten Sie es nicht und schreiben Sie nicht in die Kommentare. https://github.com/kraidiky/GDforHyperparameters

Wer bereits ein cooler Datatatanist ist und alles in diesem Leben ausprobiert hat, wird meiner Meinung nach eine Visualisierung des Lernprozesses interessant sein, die nicht nur für diese Aufgabe gilt.

Formulierung des Problems


Es gibt einen so guten DS-Kurs von ODS.ai und es gibt die dritte Vorlesungsklassifikation , Entscheidungsbäume und die Methode der nächsten Nachbarn . Dort wird an extrem einfachen und wahrscheinlich synthetischen Daten gezeigt, wie der einfachste Entscheidungsbaum eine Genauigkeit von 94,5% ergibt, und dieselbe extrem einfache Methode von k nächsten Nachbarn ergibt 89% ohne Vorverarbeitung

Daten importieren und laden
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()

Holz mit knn vergleichen
%%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)

Gleiches gilt für 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)
Zu diesem Zeitpunkt tat es mir leid, dass knn offensichtlich unehrlich war, da wir keine Arbeit mit der Metrik hatten. Ich habe nicht mit meinem Gehirn gedacht, ich habe feature_importances_ aus dem Baum genommen und die Eingabe normalisiert. Je wichtiger das Merkmal ist, desto größer ist sein Beitrag zum Abstand zwischen den Punkten.

Wir füttern die auf die Wichtigkeit von Merkmalen normierten Daten
%%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)))

5Gesamttagesminuten0,270386
17Kundendienstanrufe0,147185
8Gesamtabend Minuten0,135475
2Internationaler Plan0,097249
SechszehnGesamtgebühr0,091671
fünfzehnGesamtzahl der intl-Aufrufe09.090008
4Nummerieren Sie vmail-Nachrichten0,050646
10Gesamtvorabendgebühr0,038593
7Gesamttagesgebühr0,026422
3Voicemail-Plan0,017068
elfGesamtnachtminuten0,014185
dreizehnGesamtnachtgebühr0,005742
12Total Nachtanrufe0,005502
9Total Vorabend Anrufe0,003614
6Gesamtzahl der Tagesanrufe0,002246
14Insgesamt intl Minuten0.002009
0Account length0.001998
1Area code0.000000

{'n_neighbors': 5} 0.909129875696528 0.913

Der Baum hat nur ein wenig Wissen mit knn geteilt und jetzt sehen wir 91%. Das ist nicht so weit von 94,5% des Vanillebaums entfernt. Und dann kam mir eine Idee. Aber wie müssen wir die Eingabe tatsächlich normalisieren, damit knn das beste Ergebnis zeigt?

Zunächst werden wir uns überlegen, wie viel dies jetzt als "Stirn" betrachtet wird. 18 Parameter, für jeden machen wir beispielsweise 10 mögliche Schritte der Faktoren in der logarithmischen Skala. Wir bekommen 10e18 Optionen. Eine Option mit all der möglichen ungeraden Anzahl von Nachbarn ist weniger als 10 und die Kreuzvalidierung ist auch 10, ich denke über 1,5 Sekunden. Es stellt sich heraus, 42 Milliarden Jahre. Vielleicht muss die Idee, die Abrechnung für die Nacht zu verlassen, aufgegeben werden. :) Und irgendwo hier dachte ich: „Hey! Also mache ich ein Fahrrad, das fliegen wird! “

Verlaufssuche


Tatsächlich steht für diese Aufgabe höchstwahrscheinlich nur ein Maximum zur Verfügung. Nun, das ist natürlich kein Bereich mit guten Ergebnissen, aber sie sind sich ziemlich ähnlich. Daher können wir einfach entlang des Gefälles gehen und den am besten geeigneten Punkt finden. Der erste Gedanke war, den genetischen Algorithmus zu verallgemeinern, aber hier scheint das adaptive Terrain nicht sehr gekreuzt zu sein, und dies wäre ein wenig übertrieben.

Ich werde versuchen, es zunächst manuell zu tun. Um Faktoren als Hyperparameter zu pushen, muss ich mich mit Skalierern befassen. Im vorherigen Beispiel habe ich wie in der Lektion StandartScaler verwendet, der das Trainingsmuster im Durchschnitt zentrierte und Sigma = 1 machte. Um es innerhalb der Pipeline gut zu skalieren, muss der Hyperparameter etwas kniffliger gemacht werden. Ich begann unter den in sklearn liegenden Konvertern nach etwas zu suchen, das für meinen Fall geeignet war, fand aber nichts. Daher habe ich versucht, von StandartScaler zu erben, indem ich ein zusätzliches Bündel von Faktoren daran aufgehängt habe.

Klasse für Nominalisierung und anschließende Multiplikation mit einer Skala, die leicht mit der sklearn-Pipeline kompatibel ist
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)

Ich versuche, diese Klasse anzuwenden
%%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: Wichtigkeit, Typ: float64}, 0.909558508358337, 0.913)

Das Ergebnis weicht geringfügig von meinen Erwartungen ab. Das heißt, im Prinzip funktioniert alles. Um dies zu verstehen, musste ich diese Klasse in drei Stunden mit allem Mut von Grund auf neu reproduzieren. Erst dann wurde mir klar, dass der Druck nicht gedruckt wird, nicht weil sklearn irgendwie falsch gemacht wurde, sondern weil GridSearchCV Klone im Hauptstrom erstellt , konfiguriert und trainiert sie jedoch in anderen Threads. Und alles, was Sie in anderen Streams drucken, verschwindet in Vergessenheit. Wenn Sie jedoch n_jobs = 1 setzen, werden alle Aufrufe überschriebener Funktionen als niedlich angezeigt. Wissen war sehr teuer, jetzt haben Sie es auch und Sie haben es bezahlt, indem Sie einen langwierigen Artikel gelesen haben.

Okay, lass uns weitermachen. Jetzt möchte ich für jeden ihrer Parameter eine gewisse Varianz angeben und sie dann um den besten Wert herum etwas weniger angeben, und so weiter, bis ich ein realitätsähnliches Ergebnis erhalte. Dies wird die erste unhöfliche Grundlinie dessen sein, was irgendwann den Algorithmus meiner Träume bekommen sollte.

Ich werde verschiedene Optionen für die Neugewichtung bilden, die sich in mehreren Parametern unterscheiden
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   ,               .

Nun, der Datensatz für das erste Experiment ist fertig. Jetzt werde ich versuchen, mit den Daten zu experimentieren, um zunächst die resultierenden 15 Optionen gründlich zu durchsuchen.

Wir treffen eine Versuchsauswahl von Parametern wie im Artikel
%%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))

Nun, alles ist schlecht, die Zeit wurde für einen Durchbruch aufgewendet und das Ergebnis ist sehr instabil. Dies geht auch aus der X_holdout-Prüfung hervor, bei der das Ergebnis wie in einem Kaleidoskop mit geringfügigen Änderungen an den Eingabedaten tanzt. Ich werde einen anderen Ansatz versuchen. Ich werde jeweils nur einen Parameter ändern, jedoch mit einer viel größeren Diskretisierung.

Ich ändere eine 4. Eigenschaft
%%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 Name: Wichtigkeit, Typ: float64}, 0.9099871410201458, 0.913)

Nun, was haben wir mit einer Gans? Verschiebungen von ein bis zwei Zehntel Prozent bei der Kreuzvalidierung und ein halbes Prozent Sprung bei X_holdout, wenn Sie sich verschiedene betroffene Eigenschaften ansehen. Anscheinend ist es wichtig und billig, die Situation zu verbessern, wenn Sie mit der Tatsache beginnen, dass der Baum uns gibt, dass es mit solchen Daten unmöglich ist. Nehmen wir jedoch an, wir haben keine anfängliche, bekannte Gewichtsverteilung und versuchen, dies an einem beliebigen Punkt im Zyklus mit winzigen Schritten zu tun. Es ist sehr interessant, wozu wir kommen werden.

Erstbefüllung
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)))]

Funktion, die einen Parameter leicht ändert (mit Debug-Protokollen)
%%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)

Ich habe nirgendwo etwas optimiert und deshalb fast eine halbe Stunde lang den nächsten entscheidenden Schritt getan:

Versteckter Text
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)

Die resultierende Genauigkeit von knn: 91,9% Besser als wenn wir die Daten aus dem Baum reißen. Und viel, viel besser als in der Originalversion. Vergleichen Sie das, was wir haben, mit der Wichtigkeit von Merkmalen gemäß dem Entscheidungsbaum:

Visualisierung der Wichtigkeit von Merkmalen nach 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





Scheinen? Ja, es scheint. Aber alles andere als identisch. Interessante Beobachtung. Der Datensatz enthält mehrere Funktionen, die sich vollständig duplizieren, z. B. "Gesamtnachtminuten" und "Gesamtnachtgebühr". Achten Sie also darauf, dass Sie selbst einen wesentlichen Teil dieser wiederholten Merkmale herausgesägt haben.

Wir werden die Ergebnisse in einer Datei speichern, andernfalls ist es etwas unpraktisch, zur Arbeit zurückzukehren.
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')

Ergebnisse


Nun, das Ergebnis .919 an sich ist nicht schlecht für knn, es gibt 1,5-mal weniger Fehler als in der Vanilla-Version und 7% weniger als zu dem Zeitpunkt, als wir den Baum feature_importance zum Fahren genommen haben. Aber das Interessanteste ist, dass wir jetzt nach knn selbst feature_importance haben. Es ist etwas anders als das, was der Baum uns gesagt hat. Zum Beispiel haben Baum und Knn unterschiedliche Meinungen darüber, welche der Zeichen für uns überhaupt nicht wichtig sind.

Nun, am Ende. Wir haben etwas relativ Neues und Ungewöhnliches, das über eine Wissensreserve von drei Vorträgen mlcourse.ai

ods und Google verfügt, um einfache Fragen zu Python zu beantworten. Meiner Meinung nach nicht schlecht.

Jetzt Folien


Ein Nebenprodukt der Arbeit des Algorithmus ist der Weg, den er zurückgelegt hat. Der Pfad ist jedoch 18-dimensional, was sein Bewusstsein ein wenig behindert, in Echtzeit zu verfolgen, was der Algorithmus dort tut. Lernen oder Verwenden von Müll ist nicht so bequem. Nach dem Fehlerplan ist dies tatsächlich nicht immer sichtbar. Der Fehler ändert sich möglicherweise lange Zeit nicht merklich, aber der Algorithmus ist sehr beschäftigt und kriecht im adaptiven Raum entlang eines langen, engen Tals. Daher werde ich zunächst den ersten einfachsten, aber recht informativen Ansatz anwenden - ich projiziere zufällig einen 18-dimensionalen Raum auf einen zweidimensionalen Raum, so dass die Beiträge aller Parameter unabhängig von ihrer Bedeutung einzeln sind. Tatsächlich ist der 18-dimensionale Pfad in unserem Artikel Peeping Over the Throws of a Neural Network sehr klein Ich bewunderte ebenfalls den Raum der Skalen aller Synapsen, die das neuronale Netzwerk hatte, und es war nett und informativ.

Ich lese die Daten aus der Datei, wenn ich zur Arbeit zurückkehre, nachdem ich die Schulungsphase selbst bestanden habe
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))

Der Fehler bei der Validierung ändert sich ab einem bestimmten Punkt nicht mehr. Hier wäre es möglich, einen automatischen Lernstopp einzuschalten und die empfangene Funktion für den Rest meines Lebens zu nutzen, aber ich habe bereits ein wenig Zeit. :(

Wir bestimmen, wie viel wir lernen sollen.
last = history_holdout_score[-1]
steps = np.arange(0, history_holdout_score.shape[0])[history_holdout_score != last].max()
print(steps/18)

35.5555555555555556
Wir haben jeweils einen Parameter geändert, sodass ein Optimierungszyklus aus 18 Schritten besteht. Es stellt sich heraus, dass wir 36 bedeutungsvolle Schritte hatten, oder so ähnlich. Versuchen wir nun, die Flugbahn zu visualisieren, auf der die Methode trainiert wurde.


Versteckter Text
%%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()



Es ist ersichtlich, dass ein wesentlicher Teil der Reise in den ersten vier Schritten abgeschlossen wurde. Schauen wir uns den Rest des Weges mit zunehmender Bedeutung an

Ohne die ersten 4 Punkte
plt.plot(points[4:36,0],points[4:36,1]);
plt.savefig("images/pic3.png", format = 'png')



Schauen wir uns den letzten Teil des Weges genauer an und sehen, was die Lehrerin getan hat, nachdem sie ihr Ziel erreicht hat.

rückt näher
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()





Es ist ersichtlich, dass der Algorithmus intensiv trainiert wird. Bis er sein Ziel findet. Der spezifische Punkt hängt natürlich von der Randomisierung bei der Kreuzvalidierung ab. Unabhängig vom konkreten Punkt ist das allgemeine Bild des Geschehens verständlich.

Übrigens habe ich einen solchen Zeitplan verwendet, um den Lernprozess zu demonstrieren.
Es wird nicht die gesamte Flugbahn angezeigt, sondern die letzten Schritte mit gleitender Glättung der Skala. Ein Beispiel finden Sie in meinem anderen Artikel „Wir spionieren die Würfe eines neuronalen Netzwerks aus“. Und ja, natürlich fragt jeder, der auf eine solche Visualisierung stößt, sofort, warum alle Faktoren das gleiche Gewicht und die gleiche Bedeutung haben und dann unterschiedliche. Das letzte Mal in diesem Artikel habe ich versucht, die Bedeutung von Synapsen neu zu gewichten, und es stellte sich als weniger informativ heraus.

Dieses Mal werde ich mit neuem Wissen versuchen, mit t-SNE mehrdimensionalen Raum in einer Projektion bereitzustellen, in der alles besser sein kann.

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 scheint den Raum so entfaltet zu haben, dass er das Ausmaß der Änderungen für jene Merkmale, die sich schnell nicht mehr änderten, vollständig verschlang, was das Bild völlig uninformativ machte. Schlussfolgerung - Versuchen Sie nicht, die Algorithmen an Stellen zu verschieben, die nicht für sie bestimmt sind: \

Sie können nicht weiter lesen


Ich habe auch versucht, tsne nach innen zu injizieren, um Zwischenoptimierungszustände zu visualisieren, in der Hoffnung, dass sich Schönheit herausstellen würde. aber es stellte sich heraus, nicht Schönheit, etwas Müll. Wenn Sie interessiert sind, sehen Sie, wie es geht. Das Internet ist Beispiele für solche Injektions Codes übersät sondern durch einfaches Kopieren sie nicht pa bieten , weil Ersatz in enthaltene sklearn.manifold.t_sne interner Funktion _gradient_descent , und es je nach Ausführung kann sehr unterschiedlich sein , sowohl in der Signatur und auf der Behandlung von internen Variablen. Suchen Sie also einfach die Quellen in sich selbst, wählen Sie dort Ihre Version der Funktion aus und fügen Sie nur eine Zeile ein, die Ihrer

eigenen Variablen Zwischendumps hinzufügt: position.append (p.copy ()) # Wir speichern die aktuelle Position.

Und dann visualisieren wir wunderschön, was wir als Ergebnis erhalten:

Injektionscode
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

Wenden Sie das `` feste '' t-SNE an
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
Die Werte tanzen in einem sehr weiten Bereich, daher werde ich sie vor dem Zeichnen skalieren. Bei Zyklen geschieht dies alles langsam. :(

Ich skaliere
%%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()

Animieren
%%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 ist lehrreich in Bezug auf Fähigkeiten, aber absolut nutzlos in dieser Aufgabe und hässlich.

Dazu verabschiede ich mich von dir. Ich hoffe, dass die Idee, dass Sie selbst von knn etwas Neues und Interessantes sowie Code erhalten können, Ihnen auch dabei helfen wird, Spaß mit den Daten bei diesem intellektuellen Fest während der Pest zu haben.

All Articles