Если новые категории появляются очень редко, я сам предпочитаю решение «один против всех», предоставляемое @oW_ . Для каждой новой категории вы обучаете новую модель X количеству выборок из новой категории (класс 1) и X количеству выборок из остальных категорий (класс 0).
Однако, если новые категории появляются часто и вы хотите использовать одну общую модель, есть способ сделать это с помощью нейронных сетей.
Таким образом, по прибытии новой категории, мы добавляем соответствующий новый узел к слою softmax с нулевыми (или случайными) весами и сохраняем старые весы нетронутыми, затем мы обучаем расширенную модель новым данным. Вот визуальный набросок для идеи (нарисованный мной):
Вот реализация для полного сценария:
Модель обучается по двум категориям,
Новая категория прибывает,
Модельные и целевые форматы обновляются соответственно,
Модель обучается на новых данных.
Код:
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))
какие выводы:
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
Я должен объяснить два момента относительно этого вывода:
Модель производительность снижается с 0.9275
до 0.8925
, просто добавляя новый узел. Это потому, что вывод нового узла также включен для выбора категории. На практике выходные данные нового узла должны включаться только после обучения модели на значительном образце. Например, [0.15, 0.30, 0.55]
на этом этапе мы должны достичь максимума из первых двух записей , то есть 2-го класса.
Производительность расширенной модели по двум (старым) категориям 0.88
меньше, чем у старой модели 0.9275
. Это нормально, потому что теперь расширенная модель хочет назначить вход одной из трех категорий вместо двух. Это снижение также ожидается, когда мы выбираем из трех двоичных классификаторов по сравнению с двумя двоичными классификаторами в подходе «один против всех».