Seleção da importância dos recursos para os vizinhos k-mais próximos (poço ou outros hiperparâmetros) por descida semelhante ao gradiente

Um verdadeiro absurdo pode não apenas cumprir o impossível, mas também servir como um exemplo de aviso

Experimentando a tarefa mais simples do aprendizado de máquina, descobri que seria interessante selecionar 18 hiperparâmetros ao mesmo tempo em uma faixa bastante ampla. No meu caso, tudo era tão simples que a tarefa podia ser realizada com a força bruta do computador.

Ao aprender algo, pode ser muito interessante inventar algum tipo de bicicleta. Às vezes, acaba surgindo algo novo. Às vezes acontece que tudo foi inventado antes de mim. Mas mesmo se eu apenas repetir o caminho percorrido muito antes de mim, como recompensa, muitas vezes entendo os mecanismos subjacentes dos algoritmos de suas capacidades e limitações internas. Para o qual eu convido você.

No Python e no DS, para dizer o mínimo, eu sou iniciante e faço muitas coisas que podem ser implementadas em uma equipe de acordo com meu antigo hábito de programação, que o Python pune diminuindo a velocidade, não às vezes, mas por ordens de magnitude. Portanto, carrego todo o meu código no repositório. Se você sabe como implementá-lo com muito mais eficiência - não seja tímido, edite ou escreva nos comentários. https://github.com/kraidiky/GDforHyperparameters

Aqueles que já são especialistas em dados e experimentaram tudo nesta vida serão interessantes, acredito, uma visualização do processo de aprendizagem, aplicável não apenas a esta tarefa.

Formulação do problema


Existe um curso de DS tão bom no ODS.ai e há a terceira palestra Classificação, árvores de decisão e o método dos vizinhos mais próximos . Lá, são mostrados em dados extremamente simples e provavelmente sintéticos como a árvore de decisão mais simples fornece uma precisão de 94,5%, e o mesmo método extremamente simples de k vizinhos mais próximos fornece 89% sem pré-processamento

Importar e carregar dados
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()

Compare madeira com 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)

o mesmo 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_neighbors': 9}, 0,8868409772824689, 0,891)
Nesse ponto, senti pena de knn que era obviamente desonesto, porque não tínhamos trabalho com a métrica. Eu não pensei com meu cérebro, peguei feature_importances_ da árvore e normalizei a entrada. Assim, quanto mais importante o recurso, maior sua contribuição é a distância entre os pontos.

Alimentamos os dados normalizados para a importância dos recursos
%%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)))

5Minutos totais do dia0,270386
17Chamadas de atendimento ao cliente0,147185
8Total de minutos da véspera0.135475
2Plano internacional0,097249
dezesseisCobrança interna total0,091671
quinzeTotal de chamadas internacionais09.090008
4Número de mensagens do vmail0,050646
10Carga total de véspera0,038593
7Cobrança total por dia0,026422
3Plano de correio de voz0,017068
onzeMinutos totais da noite0,014185
trezeCusto total da noite0,005742
12Total de chamadas noturnas0,005502
9Total de chamadas de véspera0,003614
6Total de chamadas diárias0,002246
14Minutos intl totais0.002009
0Account length0.001998
1Area code0.000000

{'n_neighbors': 5} 0.909129875696528 0.913

A árvore apenas compartilhou um pouco de conhecimento com knn e agora vemos 91%, o que não é tão distante de 94.5% da árvore de baunilha. E então uma ideia veio a mim. Mas como, de fato, precisamos normalizar a entrada para que knn mostre o melhor resultado?

Primeiro, estimaremos quanto isso agora será considerado "testa". 18 parâmetros, para cada um de nós, digamos, 10 possíveis etapas dos fatores em uma escala logarítmica. Temos 10e18 opções. Uma opção com todo o possível número ímpar de vizinhos é menor que 10 e a validação cruzada também é 10, penso em 1,5 segundos. Acontece 42 bilhões de anos. Talvez a idéia de deixar o acerto de contas para a noite tenha que ser abandonada. :) E em algum lugar por aqui pensei: “Ei! Então, eu vou fazer uma bicicleta que voe!

Pesquisa de gradiente


De fato, essa tarefa provavelmente tem apenas um máximo disponível. Bem, isso não é claro, é uma área inteira de bons resultados, mas eles são praticamente iguais. Portanto, podemos apenas caminhar ao longo do gradiente e encontrar o ponto mais adequado. O primeiro pensamento foi generalizar o algoritmo genético, mas aqui o terreno adaptativo não parece ser muito cruzado, e isso seria um pouco exagerado.

Vou tentar fazê-lo manualmente para começar. Para empurrar fatores como hiperparâmetros, preciso lidar com scalers. No exemplo anterior, como na lição, usei o StandartScaler, que centralizou a amostra de treinamento em média e fez sigma = 1. Para escalar bem dentro do pipeline, o hiperparâmetro precisará ser um pouco mais complicado. Comecei a procurar entre os conversores no sklearn.preprocessing algo adequado para o meu caso, mas não encontrei nada. Portanto, tentei herdar do StandartScaler pendurando um pacote adicional de fatores.

Classe para nominalização e multiplicação por escala ligeiramente compatível com o pipeline do 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)

Tentando aplicar esta 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_neighbours': 5, 'scaler__normalization': Nome: importância, tipo: float64}, 0,909558508358337, 0,913)

O resultado é um pouco diferente das minhas expectativas. Bem, isto é, em princípio, tudo funciona. Só para entender isso, eu tive que reproduzir essa classe com toda a coragem do zero em três horas, e só então percebi que a impressão não é impressa não porque o sklearn é feito de alguma maneira incorreta, mas porque o GridSearchCV cria clones no fluxo principal , mas os configura e os treina em outros segmentos. E tudo o que você imprime em outros fluxos desaparece no esquecimento. Mas se você colocar n_jobs = 1, todas as chamadas para funções substituídas serão mostradas como fofas. O conhecimento saiu muito caro, agora você também o possui e pagou por isso lendo um artigo tedioso.

Ok, vamos seguir em frente. Agora, quero dar alguma variação para cada um de seus parâmetros e, em seguida, dar um pouco menos em torno do melhor valor, e assim por diante até obter um resultado semelhante à realidade. Esta será a primeira linha grosseira do que acabará por obter o algoritmo dos meus sonhos.

Vou formar várias opções para reponderação, diferindo em vários 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   ,               .

Bem, o conjunto de dados para o primeiro experimento está pronto. Agora vou tentar experimentar os dados, para começar pela pesquisa exaustiva das 15 opções resultantes.

Fazemos uma seleção experimental de parâmetros, como no artigo
%%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))

Bem, tudo está ruim, o tempo foi gasto em uma inovação e o resultado é muito instável. Isso também é visto na verificação X_holdout, o resultado dança como em um caleidoscópio com pequenas alterações nos dados de entrada. Vou tentar uma abordagem diferente. Mudarei apenas um parâmetro de cada vez, mas com uma discretização muito maior.

Eu mudo uma quarta propriedade
%%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 Nome: importância, tipo: float64}, 0.9099871410201458, 0.913)

Bem, o que temos com um ganso? Mudanças de um a dois décimos de por cento na validação cruzada e um salto de meio por cento no X_holdout se você observar diferentes propriedades afetadas. Aparentemente, é essencial e barato melhorar a situação se você começar com o fato de que a árvore nos fornece que é impossível nesses dados. Mas suponha que não tenhamos uma distribuição de peso conhecida inicial e tente fazer o mesmo em um ponto arbitrário do ciclo, com pequenos passos. É muito interessante o que chegaremos.

Enchimento 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)))]

Função alterando ligeiramente um parâmetro (com logs de depuração)
%%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)

Não otimizei nada em nenhum lugar e, como resultado, dei o próximo passo decisivo por quase meia 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)

A precisão resultante do knn: 91,9% melhor do que quando extraímos os dados da árvore. E muito, muito melhor do que na versão original. Compare o que temos com a importância dos recursos de acordo com a árvore de decisão:

Visualização da importância dos recursos de acordo com 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? Sim, parece. Mas longe de ser idêntico. Observação interessante. Existem vários recursos no conjunto de dados que se duplicam completamente, por exemplo, 'Total de minutos noturnos' e 'Total de cobrança noturna'. Portanto, preste atenção, o próprio knn viu uma parte significativa desses recursos repetidos.

Salvaremos os resultados em um arquivo, caso contrário, é um pouco inconveniente retornar ao trabalho ....
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')

achados


Bem, o resultado .919 em si não é ruim para o knn, há 1,5 vezes menos erros do que na versão vanilla e 7% menos do que quando levamos a árvore feature_importance para dirigir. Mas o mais interessante é que agora temos a importância da característica de acordo com o próprio knn. É um pouco diferente do que a árvore nos disse. Por exemplo, tree e knn têm opiniões diferentes sobre quais dos sinais não são importantes para nós.

Bem, no final. Temos algo relativamente novo e incomum, tendo uma reserva de conhecimento de três palestras mlcourse.ai

ods e Google para responder perguntas simples sobre python. Na minha opinião, não é ruim.

Agora slides


Um subproduto do trabalho do algoritmo é o caminho que ele percorreu. O caminho, no entanto, é tridimensional, o que dificulta um pouco sua percepção de seguir em tempo real o que o algoritmo está fazendo ali, aprender ou usar lixo não é tão conveniente. De acordo com o cronograma de erros, isso, na verdade, nem sempre é visível. O erro pode não mudar visivelmente por muito tempo, mas o algoritmo está muito ocupado, rastejando por um vale longo e estreito no espaço adaptável. Portanto, utilizarei, para iniciantes, a primeira abordagem mais simples, mas bastante informativa - projeto aleatoriamente um espaço 18-dimensional em um espaço bidimensional, de modo que as contribuições de todos os parâmetros, independentemente de seu significado, sejam únicas. De fato, o caminho tridimensional é muito pequeno, em nosso artigo Peeping Over the Throws of a Neural Network Da mesma forma, eu admirava o espaço das escalas de todas as sinapses que a rede neural possuía e era agradável e informativo.

Eu leio os dados do arquivo, se eu voltar ao trabalho, depois de passar pelo estágio de treinamento
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))

O erro na validação deixa de mudar a partir de algum ponto. Aqui seria possível interromper automaticamente o aprendizado e usar a função recebida pelo resto da minha vida, mas já tenho um pouco de tempo. :(

Determinamos quanto estudar.
last = history_holdout_score[-1]
steps = np.arange(0, history_holdout_score.shape[0])[history_holdout_score != last].max()
print(steps/18)

35.5555555555555556 Alteramos
um parâmetro de cada vez, portanto, um ciclo de otimização consiste em 18 etapas. Acontece que tivemos 36 etapas significativas, ou algo assim. Agora vamos tentar visualizar a trajetória ao longo da qual o método foi treinado.


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



Pode-se observar que uma parte significativa da jornada foi concluída nas quatro primeiras etapas. Vejamos o resto do caminho com o aumento

Sem os 4 primeiros pontos
plt.plot(points[4:36,0],points[4:36,1]);
plt.savefig("images/pic3.png", format = 'png')



Vamos dar uma olhada na parte final do caminho e ver o que a professora fez depois de chegar ao seu destino.

chegando perto
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()





Pode-se ver que o algoritmo está sendo treinado intensamente. Até que ele encontre seu destino. O ponto específico, é claro, depende da randomização na validação cruzada. Mas, independentemente do ponto específico, a imagem geral do que está acontecendo é compreensível.

A propósito, eu costumava usar esse cronograma para demonstrar o processo de aprendizado.
Nem toda a trajetória é mostrada, mas os últimos passos com suavização deslizante da escala. Um exemplo pode ser encontrado em meu outro artigo, “Nós espionamos os lançamentos de uma rede neural”. E sim, é claro, todo mundo que encontra essa visualização imediatamente pergunta por que todos os fatores têm o mesmo peso, importância e, em seguida, diferentes. A última vez no artigo, tentei repensar a importância das sinapses, e isso se mostrou menos informativo.

Desta vez, armado com novos conhecimentos, tentarei usar o t-SNE para implantar espaço multidimensional em uma projeção na qual tudo pode ser melhor.

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();



O t-Sne parece ter desdobrado o espaço, de modo que ele comeu completamente a escala das alterações para os recursos que rapidamente pararam de mudar, o que tornou a imagem completamente pouco informativa. Conclusão - não tente deslizar os algoritmos para lugares não destinados a eles .: \

Você não pode ler além disso


Eu também tentei injetar tsne dentro para visualizar estados intermediários de otimização, na esperança de que a beleza acabasse. mas acabou não beleza, um pouco de lixo. Se estiver interessado, veja como fazê-lo. A Internet é um exemplo cheio de códigos injetáveis, mas, simplesmente, copiando eles não são armazenados como substitutos contidos na função interna _gradient_descent do sklearn.manifold.t_sne e, dependendo da versão, pode ser muito diferente na assinatura e no tratamento de variáveis ​​internas. Então, encontre as fontes em si mesmo, escolha sua versão da função e insira apenas uma linha nela que adicione dumps intermediários à sua variável:

position.append (p.copy ()) # Nós salvamos a posição atual.

E então, visualizamos lindamente o que obtemos como resultado:

Código de injeção
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

Aplique o t-SNE `` fixo ''
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 máx. 0.00027207955
[1] min -0.05136269 máx. 0.032607622
[2] min -4.392309 máx. 7.9074526
Os valores dançam em uma faixa muito ampla, portanto os dimensionarei antes de desenhá-los. Nos ciclos, tudo isso é feito kapets lentamente. :(

Eu escala
%%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))



É instrutivo em termos de habilidades, mas absolutamente inútil nessa tarefa e feio.

Por isso digo adeus a você. Espero que a idéia de que, mesmo sabendo, você possa obter algo novo e interessante, além de partes do código, o ajude a se divertir com os dados deste banquete intelectual durante a praga.

All Articles