SGDClassifier: apprentissage en ligne / partial_fit avec une étiquette inconnue auparavant

9

Mon ensemble de formation contient environ 50k entrées avec lesquelles je fais un apprentissage initial. Sur une base hebdomadaire, environ 5 000 entrées sont ajoutées; mais la même quantité "disparaît" (car ce sont les données utilisateur qui doivent être supprimées après un certain temps).

Par conséquent, j'utilise l'apprentissage en ligne car je n'ai pas accès à l'ensemble de données complet ultérieurement. Actuellement j'utilise un SGDClassifierqui fonctionne, mais mon gros problème: de nouvelles catégories apparaissent et maintenant je ne peux plus utiliser mon modèle comme il ne l'était pas au départ fit.

Existe-t-il un moyen avec SGDClassifierou un autre modèle? L'apprentissage en profondeur?

Peu importe si je dois recommencer à zéro MAINTENANT (c'est-à-dire utiliser autre chose que SGDClassifier), mais j'ai besoin de quelque chose qui permette l'apprentissage en ligne avec de nouvelles étiquettes.

swalkner
la source
1
Lorsque vous dites que vous avez de nouvelles catégories, parlez-vous de nouvelles catégories dans vos variables exogènes ( ) ou dans vos variables endogènes ( X )? YX
Juan Esteban de la Calle

Réponses:

9

Il semble que vous ne souhaitiez pas commencer à recycler le modèle chaque fois qu'une nouvelle catégorie d'étiquette apparaît. Le moyen le plus simple de conserver des informations maximales sur les données passées serait de former un classificateur par catégorie.

De cette façon, vous pouvez continuer à former chaque classificateur de manière incrémentielle ("en ligne") avec quelque chose comme SGDClassifiersans avoir à les recycler. Chaque fois qu'une nouvelle catégorie apparaît, vous ajoutez un nouveau classificateur binaire pour cette catégorie uniquement. Vous sélectionnez ensuite la classe avec la probabilité / le score le plus élevé parmi l'ensemble des classificateurs.

Cela n'est pas non plus très différent de ce que vous faites aujourd'hui, car il scikit's SDGClassifiergère déjà le scénario multiclasse en installant plusieurs classificateurs "One vs All" sous le capot.

Si de nombreuses nouvelles catégories continuent d'apparaître, bien sûr, cette approche pourrait devenir un peu délicate à gérer.

oW_
la source
1
Intelligent! Cette méthode pourrait également bien fonctionner avec d'autres classificateurs scikit qui ont l' warm_startoption.
Simon Larsson
5

Si de nouvelles catégories arrivent très rarement, je préfère moi-même la solution "one vs all" proposée par @oW_ . Pour chaque nouvelle catégorie, vous entraînez un nouveau modèle sur X nombre d'échantillons de la nouvelle catégorie (classe 1) et X nombre d'échantillons du reste des catégories (classe 0).

Cependant, si de nouvelles catégories arrivent fréquemment et que vous souhaitez utiliser un seul modèle partagé , il existe un moyen d'y parvenir en utilisant des réseaux de neurones.

En résumé, à l'arrivée d'une nouvelle catégorie, nous ajoutons un nouveau nœud correspondant à la couche softmax avec des poids zéro (ou aléatoires), et gardons les anciens poids intacts, puis nous entraînons le modèle étendu avec les nouvelles données. Voici un croquis visuel de l'idée (dessiné par moi-même):

Voici une implémentation pour le scénario complet:

  1. Le modèle est formé sur deux catégories,

  2. Une nouvelle catégorie arrive,

  3. Le modèle et les formats cibles sont mis à jour en conséquence,

  4. Le modèle est formé sur les nouvelles données.

Code:

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

qui génère:

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

Je devrais expliquer deux points concernant cette sortie:

  1. Les performances du modèle sont réduites de 0.9275à 0.8925simplement en ajoutant un nouveau nœud. En effet, la sortie du nouveau nœud est également incluse pour la sélection de catégorie. En pratique, la sortie du nouveau nœud doit être incluse uniquement après que le modèle a été formé sur un échantillon de taille importante. Par exemple, [0.15, 0.30, 0.55]à ce stade , nous devrions atteindre le sommet de la première des deux premières entrées , c'est- à -dire la 2e classe.

  2. Les performances du modèle étendu sur deux (anciennes) catégories 0.88sont inférieures à celles de l'ancien modèle 0.9275. C'est normal, car maintenant le modèle étendu veut affecter une entrée à l'une des trois catégories au lieu de deux. Cette diminution est également attendue lorsque nous sélectionnons parmi trois classificateurs binaires par rapport à deux classificateurs binaires dans l'approche "un contre tous".

Esmailian
la source
1

Je dois dire que je n'ai trouvé aucune littérature concernant ce sujet. Pour autant que je sache, ce que vous demandez est impossible. Vous devez en être conscient et le propriétaire du produit doit l'être également. La raison en est que toute fonction de perte repose sur des étiquettes connues, il n'y a donc aucun moyen de prédire une étiquette qui ne figure pas dans les données d'apprentissage. En outre, est-ce de la science-fiction qu'un algorithme d'apprentissage automatique peut prédire quelque chose pour lequel il n'a pas été formé

Cela dit, je pense qu'il peut y avoir une solution de contournement (permettez-moi de souligner qu'il s'agit d'une opinion non basée sur la littérature formelle). Si le classificateur est probabiliste, la sortie est la probabilité que chaque classe soit vraie et la décision est la probabilité la plus élevée. Vous pouvez peut-être définir un seuil pour cette probabilité, de sorte que le modèle prédit "inconnu" si toutes les probabilités sont inférieures à ce seuil. Laisse moi te donner un exemple.

Laisser M(X) être un modèle tel que: étant donné X, décide si X appartient à une catégorie sur trois c1,c2,c3. La sortie deM est un vecteur de probabilités p. La décision est prise en prenant le plus haut probp. Donc, une sortie deM(X)=p(X)=(0,2,0,76,0,5) correspondrait à la décision X appartient à c2. Vous pouvez modifier cette décision en définissant unτ tel si aucun des pjeτ alors la décision est X appartient à une classe inconnue

Que faites-vous de ces inconnus dépend de la logique des bussines. S'ils sont importants, vous pouvez en créer un pool et recycler le modèle à l'aide des données disponibles. Je pense que vous pouvez faire une sorte de «transfert d'apprentissage» à partir du modèle formé en changeant la dimension de la sortie. Mais c'est quelque chose que je n'ai pas rencontré, donc je dis juste

Prenez le compte qui SGDClassifierutilise en SVMdessous, qui n'est pas un algorithme probabiliste. La SGDClassifierdocumentation suivante vous permet de modifier l' lossargument en modified_huberou logafin d'obtenir des sorties probabilistes.

lsmor
la source
0

Il y a deux options:

  1. Prédisez la probabilité qu'un point de données appartienne à une unkcatégorie ou à un inconnu . Toutes les nouvelles catégories qui apparaissent dans le flux doivent être prédites comme unk. Ceci est courant dans le traitement du langage naturel (NLP) car il y a toujours de nouveaux jetons de mots qui apparaissent dans les flux de mots.

  2. Réentraînez le modèle chaque fois qu'une nouvelle catégorie apparaît.

Depuis que vous mentionnez SGDClassifier, je suppose que vous utilisez scikit-learn. Scikit-learn ne prend pas très bien en charge l'apprentissage en ligne. Il serait préférable de changer de cadre qui prend mieux en charge le streaming et l'apprentissage en ligne, comme Spark .

Brian Spiering
la source