SGDClassifier: Aprendizado on-line / parcial_fit com um rótulo anteriormente desconhecido


9

Meu conjunto de treinamento contém cerca de 50 mil entradas com as quais faço um aprendizado inicial. Semanalmente, ~ 5k entradas são adicionadas; mas a mesma quantidade "desaparece" (pois são os dados do usuário que precisam ser excluídos após algum tempo).

Portanto, uso o aprendizado on-line porque não tenho acesso ao conjunto de dados completo posteriormente. Atualmente estou usando um SGDClassifierque funciona, mas meu grande problema: novas categorias estão aparecendo e agora não posso mais usar meu modelo, pois elas não estavam na inicial fit.

Existe uma maneira SGDClassifierou outro modelo? Aprendizagem profunda?

Não importa se eu tenho que começar do zero AGORA (ou seja, usar algo diferente SGDClassifier), mas preciso de algo que permita o aprendizado on-line com novos rótulos.


11
Quando você diz que tem novas categorias, está falando de novas categorias nas suas variáveis ​​exógenas ( ) ou nas suas variáveis ​​endógenas ( X )? YX
Juan Esteban da Calle

Respostas:


9

Parece que você não deseja começar a treinar novamente o modelo toda vez que uma nova categoria de etiqueta aparecer. A maneira mais fácil de reter informações máximas de dados passados ​​seria treinar um classificador por categoria.

Dessa forma, você pode continuar treinando cada classificador de forma incremental ("online") com algo parecido SGDClassifiersem precisar treiná-los novamente. Sempre que uma nova categoria aparece, você adiciona um novo classificador binário apenas para essa categoria. Em seguida, você seleciona a classe com a maior probabilidade / pontuação entre o conjunto de classificadores.

Isso também não é muito diferente do que você está fazendo hoje, porque scikit's SDGClassifierjá lida com o cenário de várias classes, encaixando vários classificadores "Um vs Todos" sob o capô.

Se muitas novas categorias continuarem surgindo, é claro, essa abordagem pode se tornar um pouco complicada de gerenciar.


11
Inteligente! Esse método também pode funcionar bem com outros classificadores de scikit que têm a warm_startopção
Simon Larsson

5

Se novas categorias chegarem muito raramente, prefiro a solução "um contra todos" fornecida por @oW_ . Para cada nova categoria, você treina um novo modelo no número X de amostras da nova categoria (classe 1) e no número X de amostras do restante das categorias (classe 0).

No entanto, se novas categorias chegam frequentemente e você deseja usar um único modelo compartilhado , existe uma maneira de fazer isso usando redes neurais.

Em resumo, após a chegada de uma nova categoria, adicionamos um novo nó correspondente à camada softmax com pesos zero (ou aleatórios) e mantemos os pesos antigos intactos, depois treinamos o modelo estendido com os novos dados. Aqui está um esboço visual para a ideia (desenhada por mim):

Aqui está uma implementação para o cenário completo:

  1. O modelo é treinado em duas categorias,

  2. Chega uma nova categoria,

  3. Os formatos de modelo e destino são atualizados de acordo,

  4. O modelo é treinado em novos dados.

Código:

from keras import Model
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import Adam
from sklearn.metrics import f1_score
import numpy as np


# Add a new node to the last place in Softmax layer
def add_category(model, pre_soft_layer, soft_layer, new_layer_name, random_seed=None):
    weights = model.get_layer(soft_layer).get_weights()
    category_count = len(weights)
    # set 0 weight and negative bias for new category
    # to let softmax output a low value for new category before any training
    # kernel (old + new)
    weights[0] = np.concatenate((weights[0], np.zeros((weights[0].shape[0], 1))), axis=1)
    # bias (old + new)
    weights[1] = np.concatenate((weights[1], [-1]), axis=0)
    # New softmax layer
    softmax_input = model.get_layer(pre_soft_layer).output
    sotfmax = Dense(category_count + 1, activation='softmax', name=new_layer_name)(softmax_input)
    model = Model(inputs=model.input, outputs=sotfmax)
    # Set the weights for the new softmax layer
    model.get_layer(new_layer_name).set_weights(weights)
    return model


# Generate data for the given category sizes and centers
def generate_data(sizes, centers, label_noise=0.01):
    Xs = []
    Ys = []
    category_count = len(sizes)
    indices = range(0, category_count)
    for category_index, size, center in zip(indices, sizes, centers):
        X = np.random.multivariate_normal(center, np.identity(len(center)), size)
        # Smooth [1.0, 0.0, 0.0] to [0.99, 0.005, 0.005]
        y = np.full((size, category_count), fill_value=label_noise/(category_count - 1))
        y[:, category_index] = 1 - label_noise
        Xs.append(X)
        Ys.append(y)
    Xs = np.vstack(Xs)
    Ys = np.vstack(Ys)
    # shuffle data points
    p = np.random.permutation(len(Xs))
    Xs = Xs[p]
    Ys = Ys[p]
    return Xs, Ys


def f1(model, X, y):
    y_true = y.argmax(1)
    y_pred = model.predict(X).argmax(1)
    return f1_score(y_true, y_pred, average='micro')


seed = 12345
verbose = 0
np.random.seed(seed)

model = Sequential()
model.add(Dense(5, input_shape=(2,), activation='tanh', name='pre_soft_layer'))
model.add(Dense(2, input_shape=(2,), activation='softmax', name='soft_layer'))
model.compile(loss='categorical_crossentropy', optimizer=Adam())

# In 2D feature space,
# first category is clustered around (-2, 0),
# second category around (0, 2), and third category around (2, 0)
X, y = generate_data([1000, 1000], [[-2, 0], [0, 2]])
print('y shape:', y.shape)

# Train the model
model.fit(X, y, epochs=10, verbose=verbose)

# Test the model
X_test, y_test = generate_data([200, 200], [[-2, 0], [0, 2]])
print('model f1 on 2 categories:', f1(model, X_test, y_test))

# New (third) category arrives
X, y = generate_data([1000, 1000, 1000], [[-2, 0], [0, 2], [2, 0]])
print('y shape:', y.shape)

# Extend the softmax layer to accommodate the new category
model = add_category(model, 'pre_soft_layer', 'soft_layer', new_layer_name='soft_layer2')
model.compile(loss='categorical_crossentropy', optimizer=Adam())

# Test the extended model before training
X_test, y_test = generate_data([200, 200, 0], [[-2, 0], [0, 2], [2, 0]])
print('extended model f1 on 2 categories before training:', f1(model, X_test, y_test))

# Train the extended model
model.fit(X, y, epochs=10, verbose=verbose)

# Test the extended model on old and new categories separately
X_old, y_old = generate_data([200, 200, 0], [[-2, 0], [0, 2], [2, 0]])
X_new, y_new = generate_data([0, 0, 200], [[-2, 0], [0, 2], [2, 0]])
print('extended model f1 on two (old) categories:', f1(model, X_old, y_old))
print('extended model f1 on new category:', f1(model, X_new, y_new))

quais saídas:

y shape: (2000, 2)
model f1 on 2 categories: 0.9275
y shape: (3000, 3)
extended model f1 on 2 categories before training: 0.8925
extended model f1 on two (old) categories: 0.88
extended model f1 on new category: 0.91

Eu devo explicar dois pontos em relação a esta saída:

  1. O desempenho do modelo é reduzido de 0.9275para 0.8925apenas adicionando um novo nó. Isso ocorre porque a saída do novo nó também é incluída para a seleção de categoria. Na prática, a saída do novo nó deve ser incluída somente após o modelo ser treinado em uma amostra considerável. Por exemplo, devemos atingir o pico da maior das duas primeiras entradas [0.15, 0.30, 0.55], ou seja, da 2ª classe, nesta fase.

  2. O desempenho do modelo estendido em duas categorias (antigas) 0.88é menor que o modelo antigo 0.9275. Isso é normal, porque agora o modelo estendido deseja atribuir uma entrada a uma das três categorias em vez de duas. Essa diminuição também é esperada quando selecionamos entre três classificadores binários em comparação com dois classificadores binários na abordagem "um contra todos".


1

Devo dizer que não encontrei nenhuma literatura sobre esse tópico. Até onde eu sei, o que você pergunta é impossível. Você deve estar ciente disso e o proprietário do produto também. O motivo é que qualquer função de perda depende de rótulos conhecidos; portanto, não há como prever um rótulo que não esteja nos dados de treinamento. Além disso, é ficção científica que um algoritmo de aprendizado de máquina pode prever algo para o qual não foi treinado

Dito isto, acho que pode haver uma solução alternativa (deixe-me salientar que essa é uma opinião não baseada na literatura formal). Se o classificador for probabilístico, a saída é a probabilidade de cada classe ser verdadeira e a decisão é o maior prob. Talvez você possa definir um limite para essa probabilidade, de modo que o modelo preveja "desconhecido" se todas as probabilidades estiverem abaixo desse limite. Deixe-me lhe dar um exemplo.

M(x)xxc1 1,c2,c3MppM(x)=p(x)=(0,2,0,76,0,5)xc2τpEuτx

O que você faz com os desconhecidos depende da lógica dos negócios. Se eles são importantes, você pode criar um pool deles e treinar novamente o modelo usando os dados disponíveis. Eu acho que você pode "transferir o aprendizado" do modelo treinado, alterando a dimensão do resultado. Mas isso é algo que eu não enfrentei, então estou apenas dizendo

Assumir a contagem que SGDClassifierusa SVMembaixo, o que não é um algoritmo probabilístico. Após a SGDClassifierdocumentação, você pode modificar o lossargumento para modified_huberou logpara obter resultados probabilísticos.


0

Existem duas opções:

  1. Preveja a chance de um ponto de dados pertencer a um desconhecido ou unkcategoria. Quaisquer novas categorias que aparecem no fluxo devem ser previstas como unk. Isso é comum no NLP (Processamento de linguagem natural) porque sempre existem novos tokens de palavras aparecendo nos fluxos de palavras.

  2. Treine novamente o modelo sempre que uma nova categoria for exibida.

Desde que você mencionou SGDClassifier, presumo que você use o scikit-learn. O Scikit-learn não suporta muito bem o aprendizado on-line. Seria melhor mudar uma estrutura que suporte melhor o streaming e o aprendizado on-line, como o Spark .

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.