SGDClassifier:具有先前未知标签的在线学习/ partial_fit


9

我的训练集包含大约50k项,我可以通过它们进行初步学习。每周添加约5k条目;但“消失”的数量相同(因为用户数据必须在一段时间后删除)。

因此,我使用在线学习是因为以后无法访问完整的数据集。目前,我正在使用一个SGDClassifier有效的工具,但我遇到了一个大问题:新类别正在出现,现在我不能再使用模型了,因为它们不在最初的模型中fit

有没有办法使用SGDClassifier其他模型?深度学习?

我现在是否必须从头开始都没关系(即使用以外的东西SGDClassifier),但是我需要一些能够使用新标签进行在线学习的东西。


1
当您说有新类别时,您是在谈论外生变量()还是内生变量()中的新类别?XÿX
Juan Esteban de la Calle

Answers:


9

听起来好像您不想每次出现新标签类别时都开始重新训练模型。保留过去数据的最大信息的最简单方法是为每个类别训练一个分类器。

这样,您可以继续使用类似的方法逐步训练每个分类器(“在线”),SGDClassifier而不必重新训练它们。 每当出现新类别时,就为该类别添加新的二进制分类器。然后,在分类器集中选择概率/得分最高的分类。

这与您现在所做的也没有太大不同,因为scikit's SDGClassifier已经通过在引擎盖下安装多个“一个对所有”分类器来处理多类情况。

当然,如果出现了许多新类别,这种方法可能会变得有些棘手。


1
聪明!此方法也可以与其他具有该warm_start选项的scikit分类器一起使用。
西蒙·拉尔森

5

如果很少出现新类别,我自己更喜欢@oW_提供的“一对一”解决方案。对于每个新类别,您将在新类别(类别1)的X个样本和其余类别(类别0)的X个样本上训练新模型。

但是,如果经常出现新类别,并且您想使用一个共享模型,则可以使用神经网络来实现。

总而言之,在出现新类别时,我们将零权重(或随机权重)的相应新节点添加到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.92750.8925。这是因为新节点的输出也包括在类别选择中。实际上,仅在对相当大的样本进行模型训练之后,才应包括新节点的输出。例如,[0.15, 0.30, 0.55]在此阶段,我们应该使,即2nd class中的前两个条目中最大的一个达到峰值。

  2. 扩展模型在两个(旧)类别0.88上的性能要低于旧模型0.9275。这是正常现象,因为现在扩展模型希望将输入分配给三个类别之一,而不是两个。当我们从三个二元分类器中选择两个与之相对的两个二元分类器时,这种减少也是可以预期的。


1

我要说的是,我还没有找到有关该主题的任何文献。据我所知,您的要求是不可能的。您应该意识到这一点,产品所有者也应该注意这一点。原因是任何损失函数都依赖于已知标签,因此您无法预测训练数据中未包含的标签。另外,科幻小说是机器学习算法可以预测某些未经训练的内容

话虽如此,我认为可以有一种解决方法(让我指出,这是不基于正式文献的观点)。如果分类器是概率分类器,则输出是每个分类为真的概率,而决定是较高的概率。也许您可以为该概率设置一个阈值,以便如果所有概率都低于该阈值,则模型可以预测“未知”。让我举一个例子。

中号XXXC1个C2C3中号pp中号X=pX=0.20.760.5XC2τp一世τX

您如何处理这些未知数取决于业务逻辑。如果它们很重要,则可以创建它们的池并使用可用数据重新训练模型。我认为您可以通过更改输出的维度来从经过训练的模型中进行某种“转移学习”。但这是我没有面对过的,所以我只是说

假设SGDClassifier使用SVM下面的计数,这不是一个概率算法。根据SGDClassifier文档,您可以将loss参数修改为modified_huberlog以获得概率输出。


0

有两种选择:

  1. 预测数据点属于未知或unk类别的机会。流中出现的任何新类别都应预测为unk。这在自然语言处理(NLP)中很常见,因为单词流中总是出现新的单词标记。

  2. 每次出现新类别时都要重新训练模型。

既然您提到了SGDClassifier,我假设您使用scikit-learn。Scikit-learn无法很好地支持在线学习。最好切换一个更好地支持流媒体和在线学习的框架,例如Spark

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.