SGDClassifier: онлайн-обучение / part_fit с ранее неизвестным ярлыком


9

Мой тренировочный набор содержит около 50 тысяч записей, с которыми я делаю начальное обучение. На еженедельной основе добавляется ~ 5 тыс. Записей; но такое же количество «исчезает» (так как это пользовательские данные, которые должны быть удалены через некоторое время).

Поэтому я использую онлайн-обучение, потому что у меня нет доступа к полному набору данных позднее. В настоящее время я использую SGDClassifierметод, который работает, но моя большая проблема: появляются новые категории, и теперь я больше не могу использовать свою модель, поскольку их не было в исходной fit.

Есть ли способ с SGDClassifierкакой-либо другой моделью? Глубокое обучение?

Неважно, нужно ли мне начинать с нуля СЕЙЧАС (т.е. использовать что-то другое SGDClassifier), но мне нужно что-то, что позволяет онлайн-обучение с новыми лейблами.


1
Когда вы говорите, что у вас есть новые категории, вы говорите о новых категориях в ваших экзогенных переменных ( ) или в ваших эндогенных переменных ( )? YX
Хуан Эстебан де ла Калле

Ответы:


9

Похоже, вы не хотите начинать переподготовку модели каждый раз, когда появляется новая категория ярлыков. Самый простой способ сохранить максимальную информацию о прошлых данных - это подготовить один классификатор для каждой категории.

Таким образом, вы можете продолжать обучать каждый классификатор постепенно («онлайн»), SGDClassifierне прибегая к переподготовке. Всякий раз, когда появляется новая категория, вы добавляете новый двоичный классификатор только для этой категории. Затем вы выбираете класс с наибольшей вероятностью / баллом среди множества классификаторов.

Это также не сильно отличается от того, что вы делаете сегодня, потому что scikit's SDGClassifierуже обрабатывает мультиклассовый сценарий, помещая под капот несколько классификаторов «Один против всех».

Конечно, если появиться много новых категорий, такой подход может стать немного сложным для управления.


1
Умная! Этот метод может также хорошо работать с другими классификаторами scikit, у которых есть warm_startопция.
Саймон Ларссон

5

Если новые категории появляются очень редко, я сам предпочитаю решение «один против всех», предоставляемое @oW_ . Для каждой новой категории вы обучаете новую модель X количеству выборок из новой категории (класс 1) и X количеству выборок из остальных категорий (класс 0).

Однако, если новые категории появляются часто и вы хотите использовать одну общую модель, есть способ сделать это с помощью нейронных сетей.

Таким образом, по прибытии новой категории, мы добавляем соответствующий новый узел к слою softmax с нулевыми (или случайными) весами и сохраняем старые весы нетронутыми, затем мы обучаем расширенную модель новым данным. Вот визуальный набросок для идеи (нарисованный мной):

Вот реализация для полного сценария:

  1. Модель обучается по двум категориям,

  2. Новая категория прибывает,

  3. Модельные и целевые форматы обновляются соответственно,

  4. Модель обучается на новых данных.

Код:

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

Я должен объяснить два момента относительно этого вывода:

  1. Модель производительность снижается с 0.9275до 0.8925, просто добавляя новый узел. Это потому, что вывод нового узла также включен для выбора категории. На практике выходные данные нового узла должны включаться только после обучения модели на значительном образце. Например, [0.15, 0.30, 0.55]на этом этапе мы должны достичь максимума из первых двух записей , то есть 2-го класса.

  2. Производительность расширенной модели по двум (старым) категориям 0.88меньше, чем у старой модели 0.9275. Это нормально, потому что теперь расширенная модель хочет назначить вход одной из трех категорий вместо двух. Это снижение также ожидается, когда мы выбираем из трех двоичных классификаторов по сравнению с двумя двоичными классификаторами в подходе «один против всех».


1

Я должен сказать, что я не нашел никакой литературы по этой теме. Насколько я знаю, то, что вы спрашиваете, невозможно. Вы должны знать об этом, и владелец продукта должен быть тоже. Причина в том, что любая функция потерь опирается на известные метки, поэтому нет способа предсказать метку, которой нет в обучающих данных. Кроме того, научная фантастика заключается в том, что алгоритм машинного обучения может предсказать то, к чему он не был обучен

Сказав это, я думаю, что может быть обходной путь (позвольте мне отметить, что это мнение не основано на формальной литературе). Если классификатор является вероятностным, то выходным значением является вероятность для каждого класса быть истинным, а решение - более высокой вероятностью. Возможно, вы можете установить порог для этой вероятности, такой, чтобы модель предсказывала «неизвестно», если все вероятности ниже этого порога. Позвольте привести пример.

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

Что вы будете делать с этими неизвестными , зависит от логики бизнеса. Если они важны, вы можете создать их пул и переучить модель, используя доступные данные. Я думаю, что вы можете сделать своего рода «трансфер обучения» из обученной модели, изменив размерность результата. Но с этим я не сталкивался, поэтому просто говорю

Возьмите на подсчет, который SGDClassifierиспользует SVMпод, который не является вероятностным алгоритмом. В следующей SGDClassifierдокументации вы можете изменить lossаргумент на modified_huberили logдля получения вероятностных результатов.


0

Есть два варианта:

  1. Предсказать вероятность того, что точка данных принадлежит неизвестному или unkкатегории. Любые новые категории, которые появляются в потоке, должны быть предсказаны как unk. Это часто встречается в Natural Language Processing (NLP), потому что в текстовых потоках всегда появляются новые токены.

  2. Повторяйте модель каждый раз, когда появляется новая категория.

Поскольку вы упоминаете SGDClassifier, я предполагаю, что вы используете scikit-learn. Scikit-learn не очень хорошо поддерживает онлайн-обучение. Было бы лучше переключить среду, которая лучше поддерживает потоковое и онлайн-обучение, например, Spark .

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.