1. 反欺诈检检测
1.1 反欺诈检测的难点
反诈骗实际是个多分类问题,每种不同的诈骗都当做一种单独的类型。除了欺诈手段多样且持续变化,欺诈检测一般还面临以下问题:
1. 大部分情况下数据是没有标签的,各种成熟的监督学习没有用武之地;
2. 区分噪音和异常点时难度很大,甚至需要发挥一点点想象力和直觉;
3. 当多种诈骗数据混合在一起,区分不同的诈骗类型更难;
根本原因还是因为我们并不了解每一种诈骗定义。
4. 即使真的有诈骗的历史数据,在有标签的情况下用监督学习,也存在很大的风险。
用这样的历史数据学出的模型只能检测曾经出现过与历史诈骗相似的诈骗,而对于变种的诈骗和从未见过的诈骗,模型将无能为力。
在实际情况中,不建议直接用任何监督学习,至少不能单纯依靠一个监督学习模型来奢求检测到所有的诈骗。一般使用无监督学习,且需要领域专家来验证我们的预测,提供反馈,以便于及时的调整模型。
1.2 解决反欺诈问题的可能手段
1.2.1 迁移学习
源域样本和目标域样本分布有区别,目标域样本量又不够。通过算法缩小边缘分布之间和条件分布下的差异。
1. 基于实例迁移;
2. 基于特征的迁移;
3. 基于模型的迁移。
缺点:需要拥有与当前目标场景相关的源域数据。
1.2.2 专家模型
根据专家多年从业经验进行定性判断,它根据主观经验进行打分,而不是根据统计分析或者模型算法来进行客观的计算。操作:
1. 凭经验判断特征重要性;
2. 凭经验为变量加权。
缺点:需要大量的行业经验积累,有时候很难让人信服。
1.2.3 无监督算法
在风控领域中主要使用的是聚类和无监督异常检测。而聚类是发现样本间的相似性,异常检测则是发现样本间的相异性。这里主要介绍聚类算法:
(1)K-Means
(2)DBSCAN
DBSCAN是数据挖掘中最经典基于密度的聚类算法,其核心是:通过某个点 r 邻域内样本点的数量来衡量该点所在空间的密度。和k-means算法的不同的是:
1. 可以不需要事先指定cluster的个数;
2. 可以找出不规则形状的cluster。
(3)社区发现
对负样本聚类,将逾期客群描述成欺诈风险和信用风险两部分。社区发现算法也是当前识别团伙欺诈的主要手段之一,主要思想是通过知识图谱将小团体筛选出来。在金融领域,聚集意味着风险。
2. 异常点检测(Outlier detection)
又称为离群点检测,是找出与预期对象的行为差异较大的对象的一个检测过程。这些被检测出的对象被称为异常点或离群点。异常点检测应用非常广泛:
1. 信用卡反欺诈
2. 工业损毁检测
3. 广告点击反作弊
4. 刷好评,刷单检测
5. 羊毛党检测
异常检测一般是无监督的,往往看似是二分类,但其实是多分类(造成异常的原因各不相同)。
2.1 算法假设
异常数据跟样本中大多数数据不太一样;
异常数据在整体数据样本中占比比较小。
2.2 主要思想
主流异常检测方法都是基于样本(小群体)间的相似度proximity。
1. 距离
2. 密度
3. 角度
4. 隔离所需的难度
5. 簇
为什么要用无监督异常检测方法?
1. 样本群体有异构成分,可以对样本做筛选;
2. 很多场景没有标签或者标签很少,不能训练监督模型(比如冷启动项目、欺诈模型);
3. 样本总是在发生变换,只能从一个小群体内部发现异常(比如欺诈检测,手段多变,团伙欺诈通常集中在某段时间内);
4. 异常检测假设异常样本占比很少,并且从某种度量上远离其他样本,这符合我们个体欺诈的先验知识。但是在团体欺诈检测中就不太适用了。
2.3 异常点检验常用算法
2.3.1 z-score异常检测
假设样本服从正态分布,z-score 用于描述样本偏离正态分布的程度。通过计算 和 得到当前样本所属于的正态分布的表达式,然后分别计算每个样本在这个概率密度函数下被生成的概率,当概率小于某一阈值时,认为这个样本是不属于这个分布的,因此定义为异常值。
计算公式:Z-score = (x -μ) /δ
68% 的数据分布在 +/- 1 倍标准差之间;
95% 的数据分布在 +/- 2 倍标准差之间;
99.7% 的数据分布在 +/- 3 倍标准差之间。
结论:如果一个数据计算出它的z-score >2 甚至>3 说明这个数据和其它数据之间有很大差别。
缺点:需要假设样本满足正态分布,而我们大部分场景都不满足这种假设条件。
2.3.2 Local Outlier Factor
LOF是基于密度的经典算法,在 LOF 之前的异常检测算法大多是基于统计方法的,或者是借用了一些聚类算法用于异常点的识别(如DBSCAN)。
1. 基于统计的异常检测算法通常需要假设数据服从特定的概率分布,但假设往往不成立;
2. 聚类方法通常只能给出 0/1 的判断(即:是不是异常点),不能量化每个数据点的异常程度;
3. 基于密度的LOF算法要更简单、直观,不需要对数据的分布做太多要求,还能量化每个数据点的异常程度(outlierness)。
LOF会为每一个数据点计算出一个分数,通过这个分数的大小来判断数据是否异常。
LOF ≈1 ⇒ 非异常;
LOF ≫1 ⇒ 异常。
LOF计算过程:
1. 确定近邻点数量K;
2. LOF通过计算最近的K个点的距离来计算密度,然后将其与其它点的密度进行比较;
3. K的选择会对结果产生影响:
选择比较小的K值,会只计算附近的点,但会受到噪声的影响;
如果K选的比较大,可能会错过局部离群点。
几种距离简介:
(1)K-邻近距离(k-distance):
k-distance即点到第k个邻居的距离,如果k为3,则k-distance将是点到第三最近点的距离。
(2)可达距离(reachability distance): 表示的是两个点的距离和第二个点的k-distance中的最大值。
reach-dist(a,b) = max{k-distance(b), dist(a,b)}
如果点a在点b的k个邻居内,则reach-dist(a,b)将是b的k-距离。否则,它将是a和b的实际距离。为了便于理解,可以当做两点之间的距离。
(3)局部可达密度lrd(local reachability densit):
LRD可以通过reach-dist计算得出,点a的lrd,首先计算a到它的所有k个最近邻居的reach-dist,并取该数字的平均值,lrd是该平均值的倒数。 LRD代表一种密度,因此,到下一个近邻点的距离越长,相应点所在的区域就越稀疏。反之密度越小。
lrd(a) = 1/(sum(reach-dist(a,n))/k)
LRD告诉我们,从一点到另一个点或者另一堆点,距离多远。(LRD越小,密度越低,距离越远)。
(4)LOF 局部异常因子(local outlier factor):
将每个点的lrd与它们的k个邻居的lrd相比较
1. 某点的LOF: K个邻居的LRD的平均值/该点的LRD;
2. LRD 越小,密度越低,距离越远,离群点的LRD小,它的邻居的LRD会比较大;
3. 离群点的LOF: 较大的邻居的LRD平均值/ 较小的离群点的LRD >>1。
根据局部异常因子的定义:
1. 如果数据点 p 的 LOF 得分在1附近,表明数据点p的局部密度跟它的邻居们差不多;
2. 如果数据点 p 的 LOF 得分小于1,表明数据点p处在一个相对密集的区域,不像是一个异常点;
3. 如果数据点 p 的 LOF 得分远大于1,表明数据点p跟其他点比较疏远,很有可能是一个异常点。
LOF算法流程:
(1)首先对样本空间进行去重,分别计算每一个样本到样本空间内其余点的距离;
(2)将步骤1 中的距离升序排列;
(3)指定近邻样本个数k,对于每个样本点,寻找其k近邻样本,然后计算LOF分数,作为异常分数。
PyOD是一个用于检测数据中异常值的库。它提供对20多种不同算法的访问,以检测异常值。
from pyod.models.lof import LOF
clf = LOF(n_neighbors=20, algorithm='auto') # auto会根据传入的数据自动选择最合适算法
clf.fit(x)
train['out_pred'] = clf.predict_proba(x)[:,1]
key = train['out_pred'].quantile(0.93)
x = train[train.out_pred < key][feature_lst]
y = train[train.out_pred < key]['label']
val_x, val_y = val[feature_lst], val['label']
lr_model = LogisticRegression(C=0.1, class_weight='balanced')
lr_model.fit(x, y)
y_pred = lr_model.predict_proba(x)[:,1]
fpr_lr_train, tpr_lr_train, _ = roc_curve(y, y_pred)
train_ks = abs(fpr_lr_train - tpr_lr_train).max()
print('train_ks : ', train_ks)
画出相应orc曲线
y_pred = lr_model.predict_proba(val_x)[:,1]
fpr_lr, tpr_lr, _ = roc_curve(val_y, y_pred)
val_ks = abs(fpr_lr - tpr_lr).max()
print('val_ks : ', val_ks)
from matplotlib import pyplot as plt
plt.plot(fpr_lr_train, tpr_lr_train, label = 'train LR')
plt.plot(fpr_lr, tpr_lr, label = 'evl LR')
plt.plot([0,1], [0,1], 'k--')
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC Curve')
plt.legend(loc='best')
plt.show()
算法应用:
LOF 算法需要计算数据点两两之间的距离,造成整个算法时间复杂度为 。
LOF算法中关于局部可达密度的定义暗含了一个假设,即:不存在>= k 个重复的点。当这样的重复点存在的时候,这些点的平均可达距离为零,局部可达密度就变为无穷大,会给计算带来一些麻烦。在实际应用时,为了避免这样的情况出现,可以把 k-distance 改为 k-distinct-distance,不考虑重复的情况。或者,还可以考虑给可达距离都加一个很小的值,避免可达距离等于零。
为了提高算法效率,2012年提出了FastLOF 算法:先将整个数据随机的分成多个子集,然后在每个子集里计算 LOF 值。对于那些 LOF 异常得分<=1 的,从数据集里剔除,剩下的在下一轮寻找更合适的 nearest-neighbor,并更新 LOF 值。这种先将数据粗略分成多个部分,然后根据局部计算结果将数据过滤来减少计算量的想法,并不罕见。比如,为了改进 K-means 的计算效率, Canopy Clustering 算法也采用过比较相似的做法。
参考:不均衡学习和异常检测_正负样本不均衡异常检测-CSDN博客
2.3.3 孤立森林(Isolation Forest)
基本想法:
(1)一维数组
1. 假设现在有一组一维数据,我们要对这组数据进行随机切分,希望可以把点 A 和点 B 单独切分出来;
2. 先在最大值和最小值之间随机选择一个值 x,然后按照 <x 和 >=x 可以把数据分成左右两组;
3. 在这两组数据中分别重复这个步骤,直到数据不可再分。点 B 跟其他数据比较疏离,可能用很少的次数就可以把它切分出来;
4. 点 A 跟其他数据点聚在一起,可能需要更多的次数才能把它切分出来。
(2)二维数组
1. 先随机选择一个特征维度,在这个特征的最大值和最小值之间随机选择一个值,按照跟特征值的大小关系将数据进行左右切分;
2. 在左右两组数据中重复上述步骤,随机按某个特征维度的取值把数据细分,直到无法细分(剩下一个数据点,或剩下的数据都相同)。
3. 点B'跟其他数据点比较疏离,可能只需要很少的几次操作就可以将它细分出来;点A'需要的切分次数可能会更多一些。
按照之前提到的关于“异常”的两个假设,一般情况下在上面的例子中:
1. 点B和点B' 由于跟其他数据隔的比较远,会被认为是异常数据,而点A和点A' 会被认为是正常数据
2. 直观上,异常数据由于跟其他数据点较为疏离,可能需要较少几次切分就可以将它们单独划分出来,而正常数据恰恰相反。
3. 这正是Isolation Forest的核心概念。IF采用二叉树去对数据进行切分,数据点在二叉树中所处的深度反应了该条数据的“疏离”程度。
整个算法大致可以分为两步:
(1)训练:抽取多个样本,构建多棵二叉树iTree;
构建一棵 iTree 时,先从全量数据中抽取一批样本,然后随机选择一个特征作为起始节点,并在该特征的最大值和最小值之间随机选择一个值,将样本中小于该取值的数据划到左分支,大于等于该取值的划到右分支。然后,在左右两个分支数据中,重复上述步骤,直到满足如下条件:
1. 数据不可再分(只包含一条数据,或者全部数据相同);
2. 二叉树达到限定的最大深度。
(2)预测:综合多棵二叉树的结果,计算每个数据点的异常分值。
计算数据 x 的异常分值时,先要估算它在每棵 iTree 中的路径长度(深度)。具体的,先沿着一棵 iTree,从根节点开始按不同特征的取值从上往下,直到到达某叶子节点。假设 iTree 的训练样本中同样落在 x 所在叶子节点的样本数为 T.size,则数据 x 在这棵 iTree 上的路径长度 h(x)。
e 表示数据 x 从 iTree 的根节点到叶节点过程中经过的边的数目;
C(T.size) 可以认为是一个修正值,它表示在一棵用 T.size 条样本数据构建的二叉树的平均路径长度。
从异常分值的公式看:
1. 如果数据 x 在多棵 iTree 中的平均路径长度越短,得分越接近 1,表明数据 x 越异常;
2. 如果数据 x 在多棵 iTree 中的平均路径长度越长,得分越接近 0,表示数据 x 越正常;
3. 如果数据 x 在多棵 iTree 中的平均路径长度接近整体均值,则打分会在 0.5 附近。
data = pd.read_csv('Bcard.txt')
train = data[data.obs_mth != '2018-11-30'].reset_index().copy()
val = data[data.obs_mth == '2018-11-30'].reset_index().copy()
feature_lst = ['person_info','finance_info']
x, y = train[feature_lst], train['label']
val_x, val_y = val[feature_lst], val['label']
lr_model = LogisticRegression(C=0.1,class_weight='balanced')
lr_model.fit(x,y)
y_pred = lr_model.predict_proba(x)[:,1]
fpr_lr_train, tpr_lr_train, _ = roc_curve(y, y_pred)
train_ks = abs(fpr_lr_train - tpr_lr_train).max()
print('train_ks : ', train_ks)
y_pred = lr_model.predict_proba(val_x)[:,1]
fpr_lr, tpr_lr, _ = roc_curve(val_y, y_pred)
val_ks = abs(fpr_lr - tpr_lr).max()
print('val_ks : ', val_ks)
对原有数据进行清洗,将异常样本通过无监督算法进行筛选。
from pyod.models.iforest import IForest
# 'new' 为了兼容后续版本
clf = IForest(behaviour='new', n_estimators=500, n_jobs=-1)
clf.fit(x)
# 去掉原有数据中异常值可能性较大的进行建模
out_pred = clf.predict_proba(x, method ='linear')[:,1]
train['out_pred'] = out_pred
x = train[train.out_pred< 0.7][feature_lst]
y = train[train.out_pred < 0.7]['label']
val_x, val_y = val[feature_lst], val['label']
lr_model = LogisticRegression(C=0.1, class_weight='balanced')
lr_model.fit(x,y)
y_pred = lr_model.predict_proba(x)[:,1]
fpr_lr_train,tpr_lr_train,_ = roc_curve(y,y_pred)
train_ks = abs(fpr_lr_train - tpr_lr_train).max()
print('train_ks : ',train_ks)
通过样本异常程度进行分析:
train.out_pred.groupby(train.obs_mth).mean()
如果某个月份的平均异常概率较高/方差较大,需要查明原因(渠道问题,运营动作导致...)。