当只有几个正样本,你如何分类无标签数据
假设您有一个交易业务数据集。有些交易被标记为欺诈,其余交易被标记为真实交易,因此您需要设计一个模型来区分欺诈交易和真实交易。假设您有足够的数据和良好的特征,这似乎是一项简单的分类任务。但是,假设数据集中只有15%的数据被标记,并且标记的样本仅属于一类,即训练集15%的样本标记为真实交易,而其余样本未标记,可能是真实交易样本,也可能是欺诈样本。您将如何对其进行分类?样本不均衡问题是否使这项任务变成了无监督学习问题?好吧,不一定。
此问题通常被称为PU(正样本和未标记)分类问题,首先要将该问题与两个相似且常见的“标签问题”相区别,这两个问题使许多分类任务复杂化。第一个也是最常见的标签问题是小训练集问题。当您有大量数据但实际上只有一小部分被标记时,就会出现这种情况。这个问题有很多种类和许多特定的训练方法。另一个常见的标签问题(通常与PU问题混为一谈)是,训练的数据集是被完全标记的但只有一个类别。例如,假设我们拥有的只是一个非欺诈性交易的数据集,并且我们需要使用该数据集来训练一个模型,以区分非欺诈性交易和欺诈性交易。这也是一个常见问题,通常被视为无监督的离群点检测问题,在机器学习领域中也有很多工具专门用于处理这些情况(OneClassSVM可能是最著名的)。
相比之下,PU分类问题涉及的训练集,其中仅部分数据被标记为正,而其余数据未标记,可能为正或负。例如,假设您在银行工作,可以获得很多交易数据,但只能确认其中一部分是100%真实的。我将要举的例子将与伪钞相关。这个例子包含一个1200张钞票的数据集,其中大部分未标记,只有一部分被确认为真钞。尽管PU问题也很普遍,但是与前面提到的两个分类问题相比,这个问题被讨论的次数通常要少得多,而且很少有实际的示例或库可供使用。
这篇文章的目的是提出一种可行的办法来解决PU问题,这个方法我最近在一个分类项目中使用过。它基于Charles Elkan和Keith Noto的论文《Learning classifiers from only positive and unlabeled data》(2008年),以及Alexandre Drouin撰写的一些代码。尽管在科学出版物中有更多的PU学习方法(我打算在以后的文章中讨论另一种颇受欢迎的方法),但是Elkan和Noto(E&N)的方法非常简单,并且可以在Python中轻松实现。
一点点理论
E&N方法本质上认为,在给定一个具有正样本和未标记样本的数据集的情况下,某个样本为正的概率[P(y = 1 | x)]等于一个样本被标记的概率[P(s = 1 | x)]除以一个正样本被标记的概率[P(s = 1 | y = 1)]。
如果这个说法是正确的(我并不会去证明或反驳它-您可以阅读论文本身的证明并验证代码),那么实现起来似乎相对容易。之所以这样,是因为尽管我们没有足够的标记数据来训练分类器来告诉我们样本是正还是负,但在PU问题中,我们有足够的标记数据来告诉我们一个正样本是否可能被标记。根据E&N的方法,这足以估算一个样本是否是正样本
更正式地讲,给定一个未标记的数据集,其中只有一组样本被标记为正样本。幸运的是,如果我们可以估计P(s = 1 | x)/ P(s = 1 | y = 1),那么就可以根据以下步骤使用任何基于sklearn的分类器进行估算:
(1)将分类器使用在包含标签和无标签样本的数据集上,同时使用已标记的指示器作为目标y,以这种方式拟合分类器对其进行训练,以预测给定样本x被标记的概率P(s = 1 | x)。
(2)使用分类器来预测数据集中已知正样本被标记的概率,使用预测的结果表示对正样本被标记的概率— P(s = 1 | y = 1 | x)
计算这些预测概率的均值,得到P(s=1|y=1).
在估计了P(s = 1 | y = 1)之后,为了根据E&N方法预测数据点k为正样本的概率,我们要做的就是估计P(s = 1 | k)或K被标记的概率,这正是分类器(1)所做的。
(3)使用我们训练的分类器(1)来估计K被标记的概率或者P(s=1|k)
(4)一旦我们估计了P(s = 1 | k),我们就可以通过将k除以在步骤(2)中估计的P(s = 1 | y = 1)来对k进行分类,然后获得它属于这两个类的实际概率。
现在编写代码并进行测试
上述1-4可按如下方式实施:
# prepare data x_data = the training set y_data = target var (1 for the positives and not-1 for the rest) # fit the classifier and estimate P(s=1|y=1) classifier, ps1y1 = fit_PU_estimator(x_data, y_data, 0.2, Estimator()) # estimate the prob that x_data is labeled P(s=1|X) predicted_s = classifier.predict_proba(x_data) # estimate the actual probabilities that X is positive # by calculating P(s=1|X) / P(s=1|y=1) predicted_y = estimated_s / ps1y1
让我们从这里的主要方法开始:fit_PU_estimator()方法。
fit_PU_estimator()方法完成了2个主要任务:它适合您在正样本和未标记样本的数据集中选择合适的分类器,然后估计正样本被标记的概率。相应地,它返回一个拟合的分类器(已学会估计给定样本被标记的概率)和估计概率P(s = 1 | y = 1)。之后,我们要做的就是找到P(s = 1 | x)或x被标记的概率。因为分类器被这样训练过,所以我们只需要调用其predict_proba()方法即可。最后,为了对样本x进行实际分类,我们只需要将结果除以已经得到的P(s = 1 | y = 1)。
用代码表示为:
pu_estimator, probs1y1 = fit_PU_estimator( x_train, y_train, 0.2, xgb.XGBClassifier()) predicted_s = pu_estimator.predict_proba(x_train) predicted_s = predicted_s[:,1] predicted_y = predicted_s / probs1y1
fit_PU_estimator()方法本身的实现是非常简单的
def fit_PU_estimator(X,y, hold_out_ratio, estimator): # The training set will be divided into a fitting-set that will be used # to fit the estimator in order to estimate P(s=1|X) and a held-out set of positive samples # that will be used to estimate P(s=1|y=1) # -------- # find the indices of the positive/labeled elements assert (type(y) == np.ndarray), "Must pass np.ndarray rather than list as y" positives = np.where(y == 1.)[0] # hold_out_size = the *number* of positives/labeled samples # that we will use later to estimate P(s=1|y=1) hold_out_size = int(np.ceil(len(positives) * hold_out_ratio)) np.random.shuffle(positives) # hold_out = the *indices* of the positive elements # that we will later use to estimate P(s=1|y=1) hold_out = positives[:hold_out_size] # the actual positive *elements* that we will keep aside X_hold_out = X[hold_out] # remove the held out elements from X and y X = np.delete(X, hold_out,0) y = np.delete(y, hold_out) # We fit the estimator on the unlabeled samples + (part of the) positive and labeled ones. # In order to estimate P(s=1|X) or what is the probablity that an element is *labeled* estimator.fit(X, y) # We then use the estimator for prediction of the positive held-out set # in order to estimate P(s=1|y=1) hold_out_predictions = estimator.predict_proba(X_hold_out) #take the probability that it is 1 hold_out_predictions = hold_out_predictions[:,1] # save the mean probability c = np.mean(hold_out_predictions) return estimator, c def predict_PU_prob(X, estimator, prob_s1y1): prob_pred = estimator.predict_proba(X) prob_pred = prob_pred[:,1] return prob_pred / prob_s1y1
为了对此进行测试,我使用了钞票数据集,该数据集基于从真实和伪造钞票的图像中提取的4个数据点。我首先在标记的数据集上使用分类器以设置基线,然后删除75%的样本的标签以测试其在P&U数据集上的表现。如输出所示,确实该数据集并不是最难分类的数据集,但是您可以看到,尽管PU分类器仅了解约153个正样本,而其余所有1219均未标记,但与全标签分类器相比,它的表现相当出色 。但是,它确实损失了大约17%的召回率,因此损失了很多正样本。但是,我相信与其他方案相比,这个结果是令人相当满意的。
===>> load data set <<===data size: (1372, 5)Target variable (fraud or not): 0 762 1 610===>> create baseline classification results <<===Classification results:f1: 99.57% roc: 99.57% recall: 99.15% precision: 100.00%===>> classify on all the data set <<===Target variable (labeled or not): -1 1219 1 153Classification results:f1: 90.24% roc: 91.11% recall: 82.62%
几个要点
首先,这种方法的性能很大程度上取决于数据集的大小。在此示例中,我使用了大约150个正样本和大约1200个未标记的样本。这远不是该方法的理想数据集。例如,如果我们只有100个样本,则分类器的效果会非常差。其次,如随附的笔记所示,有一些变量需要调整(例如要设置的样本大小,用于分类的概率阈值等),但是最重要的可能是选择的分类器及其参数。我之所以选择使用XGBoost,是因为它在特征少的小型数据集上的性能相对较好,但是需要注意的是,它并非在每种情况下都表现得很好,并且测试正确的分类器也很重要。