在 SMOTETomek 之前和之后使用 train_test_split 时的不同分数

问题描述

我正在尝试将文本分类为 6 个不同的类。 由于我有一个不平衡的数据集,我也在使用 SMOTetomek 方法,该方法应该用额外的人工样本综合平衡数据集。

我注意到通过管道应用它与“循序渐进”应用时存在巨大的分数差异,其中唯一的区别是(我相信)我使用的地方 train_test_split

这是我的功能标签

for curr_features,label in self.training_data:
    features.append(curr_features)
    labels.append(label)

algorithms = [
    linear_model.SGDClassifier(loss='hinge',penalty='l2',alpha=1e-3,random_state=42,max_iter=5,tol=None),naive_bayes.MultinomialNB(),naive_bayes.BernoulliNB(),tree.DecisionTreeClassifier(max_depth=1000),tree.ExtraTreeClassifier(),ensemble.ExtraTreesClassifier(),svm.LinearSVC(),neighbors.NearestCentroid(),ensemble.RandomForestClassifier(),linear_model.RidgeClassifier(),]

使用流水线:

X_train,X_test,y_train,y_test = train_test_split(features,labels,test_size=0.2,random_state=42)

# Provide Report for all algorithms
score_dict = {}
for algorithm in algorithms:
    model = Pipeline([
        ('vect',CountVectorizer()),('tfidf',TfidfTransformer()),('smote',SMOTetomek()),('classifier',algorithm)
    ])
    model.fit(X_train,y_train)

    # score
    score = model.score(X_test,y_test)
    score_dict[model] = int(score * 100)

sorted_score_dict = {k: v for k,v in sorted(score_dict.items(),key=lambda item: item[1])}
for classifier,score in sorted_score_dict.items():
    print(f'{classifier.__class__.__name__}: score is {score}%')

逐步使用:

vectorizer = CountVectorizer()
transformer = TfidfTransformer()
cv = vectorizer.fit_transform(features)
text_tf = transformer.fit_transform(cv).toarray()

smt = SMOTetomek()
X_smt,y_smt = smt.fit_resample(text_tf,labels)

X_train,y_test = train_test_split(X_smt,y_smt,random_state=0)
self.test_classifiers(X_train,y_test,algorithms)

def test_classifiers(self,X_train,classifiers_list):
    score_dict = {}
    for model in classifiers_list:
        model.fit(X_train,y_train)

        # score
        score = model.score(X_test,y_test)
        score_dict[model] = int(score * 100)
       
    print()
    print("score:")
    sorted_score_dict = {k: v for k,key=lambda item: item[1])}
    for model,score in sorted_score_dict.items():
        print(f'{model.__class__.__name__}: score is {score}%')

我得到(对于最佳分类器模型)大约 65% 使用管道,而 90% 使用逐步。 不知道我错过了什么。

解决方法

您的代码本身没有任何问题。但是您的循序渐进方法使用了机器学习理论中的不良做法:

不要重新采样您的测试数据

在您的分步方法中,您首先对所有数据进行重新采样,然后将它们拆分为训练集和测试集。这将导致对模型性能的高估,因为您已经改变了测试集中类的原始分布,并且它不再代表原始问题。

您应该做的是将测试数据保留在其原始分布中,以便获得模型在原始数据上的表现的有效近似值,这代表了生产中的情况.因此,您使用管道的方法是可行的方法。

附带说明:您可以考虑将整个数据准备(矢量化和重采样)从拟合和测试循环中移出,因为无论如何您可能希望将模型性能与相同数据进行比较。然后,您只需运行这些步骤一次,您的代码就会执行得更快。

,

在这种情况下的正确方法在 Data Science SE 线程 Why you shouldn't upsample before cross validation 中自己的答案中有详细描述(尽管答案是关于 CV,但训练/测试拆分案例的基本原理也是相同的)。简而言之,任何重采样方法(包括 SMOTE)都应仅应用于训练数据,而不应用于验证或测试数据。

鉴于此,您的 Pipeline 方法是正确:您仅在拆分后将 SMOTE 应用于训练数据,并且根据 imblearn pipeline 的文档:

采样器仅在拟合期间应用。

因此,在 model.score 期间,实际上没有 SMOTE 应用于您的测试数据,这完全是应该的。

另一方面,你的循序渐进的方法在很多层面上都是错误的,而 SMOTE 只是其中之一;所有这些预处理步骤都应该在训练/测试分割之后应用,并且只适合数据的训练部分,这里不是这种情况,因此结果是无效的(难怪它们看起来“更好的”)。有关如何以及为什么应将此类预处理仅应用于训练数据的一般性讨论(和实际演示),请参阅我在 Should Feature Selection be done before Train-Test Split or after? 中的 (2) 个答案(同样,那里的讨论是关于特征选择,但它也适用于计数向量化和TF-IDF变换等特征工程任务)。