SGDClassifier: apprendimento online / partial_fit con un'etichetta precedentemente sconosciuta


9

Il mio set di allenamento contiene circa 50.000 voci con le quali faccio un apprendimento iniziale. Su base settimanale, vengono aggiunte ~ 5k voci; ma la stessa quantità "scompare" (in quanto sono i dati dell'utente che devono essere eliminati dopo qualche tempo).

Pertanto utilizzo l'apprendimento online perché non ho accesso all'intero set di dati in un secondo momento. Attualmente sto usando un sistema SGDClassifierche funziona, ma il mio grosso problema: stanno comparendo nuove categorie e ora non posso più usare il mio modello perché non erano nell'iniziale fit.

C'è un modo con SGDClassifiero qualche altro modello? Apprendimento profondo?

Non importa se devo iniziare da zero ADESSO (ovvero usare qualcosa di diverso da SGDClassifier), ma ho bisogno di qualcosa che consenta l'apprendimento online con nuove etichette.


1
Quando dici di avere nuove categorie, stai parlando di nuove categorie nelle variabili esogene ( ) o nelle variabili endogene ( X )? YX
Juan Esteban de la Calle,

Risposte:


9

Sembra che tu non voglia iniziare a riqualificare il modello ogni volta che appare una nuova categoria di etichette. Il modo più semplice per conservare le informazioni massime dei dati passati sarebbe quello di formare un classificatore per categoria.

In questo modo puoi continuare ad addestrare ogni classificatore in modo incrementale ("online") con qualcosa di simile SGDClassifiersenza doverli riqualificare. Ogni volta che appare una nuova categoria, aggiungi un nuovo classificatore binario solo per quella categoria. Quindi selezionare la classe con la più alta probabilità / punteggio tra l'insieme di classificatori.

Anche questo non è molto diverso da quello che stai facendo oggi, perché scikit's SDGClassifiergestisce già lo scenario multiclasse inserendo più classificatori "One vs All" sotto il cofano.

Se molte nuove categorie continuano ad emergere, questo approccio potrebbe diventare un po 'complicato da gestire.


1
Intelligente! Questo metodo potrebbe anche funzionare bene con altri classificatori di scikit che hanno l' warm_startopzione.
Simon Larsson,

5

Se arrivano raramente nuove categorie, io stesso preferisco la soluzione "una contro tutte" fornita da @oW_ . Per ogni nuova categoria, si addestra un nuovo modello sul numero X di campioni dalla nuova categoria (classe 1) e sul numero X di campioni dal resto delle categorie (classe 0).

Tuttavia, se arrivano spesso nuove categorie e si desidera utilizzare un singolo modello condiviso , esiste un modo per ottenere ciò utilizzando le reti neurali.

In sintesi, all'arrivo di una nuova categoria, aggiungiamo un nuovo nodo corrispondente al layer softmax con pesi zero (o casuali) e manteniamo intatti i vecchi pesi, quindi addestriamo il modello esteso con i nuovi dati. Ecco uno schizzo visivo dell'idea (disegnato da me stesso):

Ecco un'implementazione per lo scenario completo:

  1. Il modello è formato su due categorie,

  2. Arriva una nuova categoria,

  3. I formati di modello e destinazione vengono aggiornati di conseguenza,

  4. Il modello è formato su nuovi dati.

Codice:

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

che produce:

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

Dovrei spiegare due punti riguardo a questo risultato:

  1. Le prestazioni del modello vengono ridotte da 0.9275a 0.8925semplicemente aggiungendo un nuovo nodo. Questo perché l'output del nuovo nodo è incluso anche per la selezione della categoria. In pratica, l'output del nuovo nodo dovrebbe essere incluso solo dopo che il modello è stato addestrato su un campione considerevole. Ad esempio, [0.15, 0.30, 0.55]in questa fase dovremmo raggiungere il picco della più grande delle prime due voci , ovvero la 2a classe.

  2. Le prestazioni del modello esteso su due (vecchie) categorie 0.88sono inferiori rispetto al vecchio modello 0.9275. Questo è normale, perché ora il modello esteso vuole assegnare un input a una di tre categorie anziché a due. Questa riduzione è prevista anche quando selezioniamo tre classificatori binari rispetto a due classificatori binari nell'approccio "uno contro tutti".


1

Devo dire che non ho trovato alcuna letteratura su questo argomento. Per quanto ne so, ciò che chiedi è impossibile. Dovresti esserne consapevole, e anche il proprietario del prodotto dovrebbe esserlo. Il motivo è che qualsiasi funzione di perdita si basa su etichette conosciute, quindi non è possibile prevedere un'etichetta che non si trova nei dati di addestramento. Inoltre, è fantascienza che un algoritmo di apprendimento automatico possa prevedere qualcosa per cui non è stato addestrato

Detto questo, penso che ci possa essere una soluzione (lasciatemi sottolineare che questa è un'opinione non basata sulla letteratura formale). Se il classificatore è probabilistico, l'output è la probabilità che ogni classe sia vera e la decisione è la prob maggiore. Forse puoi impostare una soglia per quella probabilità, in modo tale che il modello preveda "sconosciuto" se tutte le probabilità sono al di sotto di tale soglia. Lasciate che vi faccia un esempio.

M(X)XXc1,c2,c3MppM(X)=p(X)=(0.2,0.76,0.5)Xc2τpioτX

Cosa fai con quelle sconosciute dipende dalla logica del business. Se sono importanti, è possibile crearne un pool e riqualificare il modello utilizzando i dati disponibili. Penso che si possa fare una sorta di "trasferimento di apprendimento" dal modello addestrato modificando la dimensione dell'output. Ma questo è qualcosa che non ho affrontato, quindi sto solo dicendo

Prendi in considerazione ciò che SGDClassifierusa SVMsotto, che non è un algoritmo probabilistico. Dopo la SGDClassifierdocumentazione è possibile modificare l' lossargomento modified_hubero logper ottenere output probabilistici.


0

Esistono due opzioni:

  1. Prevedere la possibilità di un punto dati appartenente a uno sconosciuto o una unkcategoria. Qualsiasi nuova categoria che appare nello stream dovrebbe essere prevista come unk. Questo è comune in Natural Language Processing (NLP) perché ci sono sempre nuovi token di parole che appaiono nei flussi di parole.

  2. Retrain il modello ogni volta che appare una nuova categoria.

Da quando dici SGDClassifier, suppongo che tu usi scikit-learn. Scikit-learn non supporta molto bene l'apprendimento online. Sarebbe meglio cambiare un framework che supporti meglio lo streaming e l'apprendimento online, come Spark .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.