SGDClassifier: aprendizaje en línea / parcial_fit con una etiqueta previamente desconocida


9

Mi conjunto de entrenamiento contiene aproximadamente 50k entradas con las que hago un aprendizaje inicial. Semanalmente, se agregan ~ 5k entradas; pero la misma cantidad "desaparece" (ya que son los datos del usuario los que deben eliminarse después de un tiempo)

Por lo tanto, uso el aprendizaje en línea porque no tengo acceso al conjunto de datos completo en un momento posterior. Actualmente estoy usando uno SGDClassifierque funciona, pero mi gran problema: aparecen nuevas categorías y ahora ya no puedo usar mi modelo, ya que no estaban en la inicial fit.

¿Hay alguna manera con SGDClassifiero algún otro modelo? ¿Aprendizaje profundo?

No importa si tengo que comenzar desde cero AHORA (es decir, usar algo diferente a SGDClassifier), pero necesito algo que permita el aprendizaje en línea con nuevas etiquetas.


1
Cuando dice que tiene nuevas categorías, ¿está hablando de nuevas categorías en sus variables exógenas ( ) o en sus variables endógenas ( X )? YX
Juan Esteban de la Calle

Respuestas:


9

Parece que no desea comenzar a reentrenar el modelo cada vez que aparece una nueva categoría de etiqueta. La forma más fácil de retener la información máxima de datos pasados ​​sería entrenar un clasificador por categoría.

De esta manera, puede continuar entrenando cada clasificador de forma incremental ("en línea") con algo así como SGDClassifiersin tener que volver a entrenarlos. Cada vez que aparece una nueva categoría, agrega un nuevo clasificador binario solo para esa categoría. Luego selecciona la clase con la mayor probabilidad / puntaje entre el conjunto de clasificadores.

Esto tampoco es muy diferente de lo que está haciendo hoy, porque scikit's SDGClassifierya maneja el escenario multiclase al colocar múltiples clasificadores "Uno contra todos" debajo del capó.

Si siguen apareciendo muchas categorías nuevas, por supuesto, este enfoque podría volverse un poco difícil de manejar.


1
¡Inteligente! Este método también podría funcionar bien con otros clasificadores de scikit que tienen la warm_startopción.
Simon Larsson el

5

Si las nuevas categorías llegan muy raramente, yo mismo prefiero la solución "uno contra todos" proporcionada por @oW_ . Para cada nueva categoría, entrena un nuevo modelo en X número de muestras de la nueva categoría (clase 1) y X número de muestras del resto de categorías (clase 0).

Sin embargo, si las nuevas categorías llegan con frecuencia y desea usar un solo modelo compartido , hay una manera de lograr esto usando redes neuronales.

En resumen, a la llegada de una nueva categoría, agregamos un nuevo nodo correspondiente a la capa softmax con pesos cero (o aleatorios), y mantenemos los pesos viejos intactos, luego entrenamos el modelo extendido con los nuevos datos. Aquí hay un boceto visual de la idea (dibujado por mí mismo):

Aquí hay una implementación para el escenario completo:

  1. El modelo está entrenado en dos categorías,

  2. Llega una nueva categoría,

  3. Los formatos de modelo y destino se actualizan en consecuencia,

  4. El modelo está entrenado en nuevos datos.

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

que salidas:

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

Debería explicar dos puntos con respecto a esta salida:

  1. El rendimiento del modelo se reduce de 0.9275a 0.8925simplemente agregando un nuevo nodo. Esto se debe a que la salida del nuevo nodo también se incluye para la selección de categoría. En la práctica, la salida del nuevo nodo debe incluirse solo después de que el modelo se haya entrenado en una muestra considerable. Por ejemplo, deberíamos alcanzar el pico de la primera de las dos primeras entradas [0.15, 0.30, 0.55], es decir, segunda clase, en esta etapa.

  2. El rendimiento del modelo extendido en dos categorías (antiguas) 0.88es menor que el modelo anterior 0.9275. Esto es normal, porque ahora el modelo extendido quiere asignar una entrada a una de tres categorías en lugar de dos. También se espera esta disminución cuando seleccionamos entre tres clasificadores binarios en comparación con dos clasificadores binarios en el enfoque "uno contra todos".


1

Tengo que decir que no he encontrado ninguna literatura sobre este tema. Que yo sepa, lo que pides es imposible. Debería ser consciente de esto, y el propietario del producto también debería serlo. La razón es que cualquier función de pérdida se basa en etiquetas conocidas, por lo que no hay forma de predecir una etiqueta que no esté en los datos de entrenamiento. Además, es ciencia ficción que un algoritmo de aprendizaje automático puede predecir algo para lo que no ha sido entrenado

Dicho esto, creo que puede haber una solución alternativa (permítanme señalar que esta es una opinión no basada en literatura formal). Si el clasificador es probabilístico, la salida es la probabilidad de que cada clase sea verdadera y la decisión es el problema más alto. Tal vez pueda establecer un umbral para esa probabilidad, de modo que el modelo prediga "desconocido" si todas las probabilidades están por debajo de ese umbral. Dejame darte un ejemplo.

M(x)xxc1,c2,c3MppM(x)=p(x)=(0.2,0.76,0.5)xc2τpiτx

Qué hacer con esos desconocidos depende de la lógica de negocio. Si son importantes, puede crear un grupo de ellos y volver a entrenar el modelo con los datos disponibles. Creo que puede hacer una especie de "aprendizaje de transferencia" desde el modelo entrenado cambiando la dimensión de la salida. Pero esto es algo que no he enfrentado, así que solo digo

Tenga en cuenta el SGDClassifieruso SVMdebajo, que no es un algoritmo probabilístico. Después de la SGDClassifierdocumentación, puede modificar el lossargumento modified_hubero logpara obtener resultados probabilísticos.


0

Hay dos opciones:

  1. Predecir la posibilidad de que un punto de datos pertenezca a un desconocido o unkcategoría. Cualquier categoría nueva que aparezca en la transmisión debe predecirse como unk. Esto es común en el procesamiento del lenguaje natural (PNL) porque siempre aparecen nuevos tokens de palabras en los flujos de palabras.

  2. Vuelva a entrenar el modelo cada vez que aparezca una nueva categoría.

Como mencionas SGDClassifier, supongo que estás usando scikit-learn. Scikit-learn no admite el aprendizaje en línea muy bien. Sería mejor cambiar un marco que sea más compatible con la transmisión y el aprendizaje en línea, como Spark .

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.