Якщо нові категорії надходять дуже рідко, я сам вважаю за краще рішення "один проти всіх", яке надає @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
. Це нормально, тому що тепер розширена модель хоче призначити вхід одній із трьох категорій замість двох. Таке зменшення також очікується, коли ми вибираємо з трьох двійкових класифікаторів порівняно з двома двійковими класифікаторами у підході «один проти всіх».