Wenn sehr selten neue Kategorien eintreffen, bevorzuge ich selbst die von @oW_ bereitgestellte "one vs all" -Lösung . Für jede neue Kategorie trainieren Sie ein neues Modell mit X Anzahl von Stichproben aus der neuen Kategorie (Klasse 1) und X Anzahl von Stichproben aus den übrigen Kategorien (Klasse 0).
Wenn jedoch häufig neue Kategorien eintreffen und Sie ein einzelnes gemeinsames Modell verwenden möchten , gibt es eine Möglichkeit, dies mithilfe neuronaler Netze zu erreichen.
Zusammenfassend lässt sich sagen, dass wir beim Eintreffen einer neuen Kategorie der Softmax-Ebene einen entsprechenden neuen Knoten mit null (oder zufälligen) Gewichten hinzufügen und die alten Gewichte intakt halten. Anschließend trainieren wir das erweiterte Modell mit den neuen Daten. Hier ist eine visuelle Skizze für die Idee (von mir gezeichnet):
Hier ist eine Implementierung für das gesamte Szenario:
Das Modell wird in zwei Kategorien trainiert:
Eine neue Kategorie kommt an,
Modell- und Zielformate werden entsprechend aktualisiert.
Das Modell wird auf neue Daten trainiert.
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))
welche Ausgänge:
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
Ich sollte zwei Punkte in Bezug auf diese Ausgabe erklären:
Die Modellleistung wird durch einfaches Hinzufügen eines neuen Knotens von 0.9275
bis verringert 0.8925
. Dies liegt daran, dass die Ausgabe des neuen Knotens auch für die Kategorieauswahl enthalten ist. In der Praxis sollte die Ausgabe eines neuen Knotens erst aufgenommen werden, nachdem das Modell an einer großen Stichprobe trainiert wurde. Zum Beispiel sollten wir [0.15, 0.30, 0.55]
zu diesem Zeitpunkt den größten der ersten beiden Einträge in der 2. Klasse erreichen.
Die Leistung des erweiterten Modells in zwei (alten) Kategorien 0.88
ist geringer als die des alten Modells 0.9275
. Dies ist normal, da das erweiterte Modell nun einer von drei Kategorien anstelle von zwei eine Eingabe zuweisen möchte. Dieser Rückgang wird auch erwartet, wenn wir aus drei binären Klassifikatoren im Vergleich zu zwei binären Klassifikatoren im Ansatz "Eins gegen Alle" auswählen.