16、利用自编码器和异常检测进行欺诈分析

利用自编码器和异常检测进行欺诈分析

在金融领域,如银行、保险公司和信用社,检测和预防欺诈是推动业务增长的重要任务。本文将围绕使用自编码器和异常检测技术来构建欺诈分析预测模型展开,详细介绍从数据探索、模型训练到评估的整个过程。

1. 异常检测的重要性

异常是指观察世界中不寻常和意外的模式。在数据挖掘中,分析、识别、理解和预测异常是至关重要的任务。异常检测在信用卡欺诈检测、网络安全入侵检测等多个领域都有广泛应用。

当探索高度不平衡的数据集时,需要通过数据探索回答以下问题:
- 所有可用字段中,无空值或缺失值的数据占总数据的百分比是多少?并合理处理这些缺失值。
- 各字段之间的相关性如何?每个字段与预测变量的相关性怎样?它们取值的类型(如分类、数值等)是什么?
- 数据分布是否偏斜?可以通过观察异常值或长尾来判断偏斜情况。数据的峰度有三种可能:
- 当峰度测量值略小于 3 时为常峰态(Mesokurtic)。
- 当峰度测量值大于 3 时为尖峰态(Leptokurtic)。
- 当峰度测量值小于 3 时为低峰态(Platykurtic)。

例如,记录四周内(不包括周末)完成 4 公里步行的时间,计算并解释这些值的偏度和峰度,通过 R 语言生成的密度图显示数据右偏且为尖峰态,最右侧的数据点可能是异常或可疑的。

虽然去除长尾不能完全消除数据不平衡,但进行异常值检测并去除这些数据点是有帮助的。此外,还可以查看每个特征的箱线图,寻找超出三个四分位距(IQR)的异常值。

即使模型不能完美分类,但均方误差(MSE)可以为寻找异常值提供线索。例如,欺诈交易的平均 MSE 通常高于正常交易,我们可以设定一个 MSE 阈值来识别异常值。

2. 自编码器与无监督学习

自编码器是一种能够在无监督(即训练集无标签)的情况下学习输入数据有效表示的人工神经网络。其编码通常具有比输入数据低得多的维度,因此可用于降维。更重要的是,自编码器可作为强大的特征检测器,用于深度神经网络的无监督预训练。

自编码器的工作原理如下:
- 它是一个具有三层或更多层的网络,输入层和输出层的神经元数量相同,中间(隐藏)层的神经元数量较少。
- 网络的训练目标是使每个输入数据的输出与输入具有相同的活动模式。由于隐藏层神经元数量较少,如果网络能够从示例中学习并在一定程度上进行泛化,就可以实现数据压缩。

自编码器的训练方式有两种:
- 一次性训练整个层,类似于多层感知器(MLP),但在计算成本函数时使用输入本身,成本函数表示实际输入与重建输入之间的差异。
- 逐层贪婪训练,这种方法源于监督学习中反向传播方法在处理大量层时计算梯度缓慢且不准确的问题。Geoffrey Hinton 提出的预训练方法每次对相邻两层进行初始化。

自编码器在解决监督学习系统中的“维度灾难”问题上有重要作用。当输入空间维度增加时,监督学习系统的性能会逐渐下降,因为获得足够输入空间采样所需的样本数量会随着维度的增加呈指数增长。自编码器网络可以将输入模式转换为自身,即使输入模式不完整或有损坏,也能恢复原始模式。

例如,比较两组数字序列,虽然第一组看起来更容易记忆,但第二组存在特定的数字规律。就像专家棋手能够快速记忆棋盘上棋子的位置一样,自编码器可以观察输入,将其转换为更好的内部表示,并识别数据中的模式。

3. 欺诈分析模型的开发
3.1 数据集描述

我们使用来自 Kaggle 的信用卡欺诈检测数据集,该数据集包含 2013 年 9 月欧洲持卡人两天内的信用卡交易记录,共 285,299 笔交易,其中只有 492 笔欺诈交易,数据集高度不平衡,欺诈类占比仅 0.172%。

数据集中仅包含数值输入变量,是主成分分析(PCA)转换的结果。除了 Time 和 Amount 字段外,还有 28 个特征(V1 - V28)是通过 PCA 获得的主成分。Class 字段是响应变量,欺诈交易取值为 1,正常交易取值为 0。

由于数据集的类别不平衡,建议使用精确 - 召回曲线下面积(AUPRC)来衡量准确性,混淆矩阵的准确率对于不平衡分类没有意义。可以尝试使用线性机器学习模型,如随机森林、逻辑回归或支持向量机,并应用过采样或欠采样技术。同时,也可以探索深度学习模型,如自编码器,并结合异常检测来寻找数据中的异常。

3.2 编程环境准备

为了完成这个项目,需要使用以下工具和技术:
- H2O/Sparking water:用于深度学习平台。
- Apache Spark:用于数据处理环境。
- Vegas:类似于 Python 的 Matplotlib,用于绘图,可与 Spark 集成。
- Scala:作为项目的编程语言。

创建一个 Maven 项目,将依赖项注入到 pom.xml 文件中:

<dependencies>
   <dependency>
      <groupId>ai.h2o</groupId>
      <artifactId>sparkling-water-core_2.11</artifactId>
      <version>2.2.2</version>
   </dependency>
   <dependency>
      <groupId>org.vegas-viz</groupId>
      <artifactId>vegas_2.11</artifactId>
      <version>0.3.11</version>
   </dependency>
   <dependency>
     <groupId>org.vegas-viz</groupId>
     <artifactId>vegas-spark_2.11</artifactId>
     <version>0.3.11</version>
     </dependency>
</dependencies>

使用 Eclipse 或其他 IDE 拉取所有依赖项,然后创建一个 Scala 文件。

3.3 具体操作步骤
  1. 加载所需的包和库
package com.packt.ScalaML.FraudDetection
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
import org.apache.spark.sql._
import org.apache.spark.h2o._
import _root_.hex.FrameSplitter
import water.Key
import water.fvec.Frame
import _root_.hex.deeplearning.DeepLearning
import _root_.hex.deeplearning.DeepLearningModel.DeepLearningParameters
import _root_.hex.deeplearning.DeepLearningModel.DeepLearningParameters.Activation
import java.io.File
import water.support.ModelSerializationSupport
import _root_.hex.{ ModelMetricsBinomial, ModelMetrics }
import org.apache.spark.h2o._
import scala.reflect.api.materializeTypeTag
import water.support.ModelSerializationSupport
import water.support.ModelMetricsSupport
import _root_.hex.deeplearning.DeepLearningModel
import vegas._
import vegas.sparkExt._
import org.apache.spark.sql.types._
  1. 创建 Spark 会话并导入隐式转换
val spark = SparkSession
        .builder
        .master("local[*]")
        .config("spark.sql.warehouse.dir", "tmp/")
        .appName("Fraud Detection")
        .getOrCreate()
implicit val sqlContext = spark.sqlContext
import sqlContext.implicits._
val h2oContext = H2OContext.getOrCreate(spark)
import h2oContext._
import h2oContext.implicits._
  1. 加载和解析输入数据
val inputCSV = "data/creditcard.csv"
val transactions = spark.read.format("com.databricks.spark.csv")
        .option("header", "true")
        .option("inferSchema", true)
        .load(inputCSV)
  1. 输入数据的探索性分析
    • 计算类分布并绘制图表:
val distribution = transactions.groupBy("Class").count.collect
Vegas("Class Distribution").withData(distribution.map(r => Map("class" -> r(0), "count" -> r(1)))).encodeX("class", Nom).encodeY("count", Quant).mark(Bar).show
- 分析时间对可疑交易的影响,创建 Day 和 dayTime 列:
val daysUDf = udf((s: Double) => if (s > 3600 * 24) "day2" else "day1")
val t1 = transactions.withColumn("day", daysUDf(col("Time")))
val dayDist = t1.groupBy("day").count.collect
Vegas("Day Distribution").withData(dayDist.map(r => Map("day" -> r(0), "count" -> r(1)))).encodeX("day", Nom).encodeY("count", Quant).mark(Bar).show
val dayTimeUDf = udf((day: String, t: Double) => if (day == "day2") t - 86400 else t)
val t2 = t1.withColumn("dayTime", dayTimeUDf(col("day"), col("Time")))
t2.describe("dayTime").show()
- 计算分位数并创建时间区间:
val d1 = t2.filter($"day" === "day1")
val d2 = t2.filter($"day" === "day2")
val quantiles1 = d1.stat.approxQuantile("dayTime", Array(0.25, 0.5, 0.75), 0)
val quantiles2 = d2.stat.approxQuantile("dayTime", Array(0.25, 0.5, 0.75), 0)
val bagsUDf = udf((t: Double) =>
    if (t <= (quantiles1(0) + quantiles2(0)) / 2) "gr1"
    else if (t <= (quantiles1(1) + quantiles2(1)) / 2) "gr2"
    else if (t <= (quantiles1(2) + quantiles2(2)) / 2) "gr3"
    else "gr4")
val t3 = t2.drop(col("Time")).withColumn("Time", bagsUDf(col("dayTime")))
- 分析不同类别的金额分布:
val c0Amount = t3.filter($"Class" === "0").select("Amount")
val c1Amount = t3.filter($"Class" === "1").select("Amount")
println(c0Amount.stat.approxQuantile("Amount", Array(0.25, 0.5, 0.75), 0).mkString(","))
Vegas("Amounts for class 0").withDataFrame(c0Amount).mark(Bar).encodeX("Amount", Quantitative, bin = Bin(50.0)).encodeY(field = "*", Quantitative, aggregate = AggOps.Count).show
Vegas("Amounts for class 1").withDataFrame(c1Amount).mark(Bar).encodeX("Amount", Quantitative, bin = Bin(50.0)).encodeY(field = "*", Quantitative, aggregate = AggOps.Count).show
- 由于 dayTime 列不太重要,将其删除:
val t4 = t3.drop("day").drop("dayTime")
  1. 准备 H2O 数据框
val creditcard_hf: H2OFrame = h2oContext.asH2OFrame(t4.orderBy(rand()))
val sf = new FrameSplitter(creditcard_hf, Array(.4, .4),
                Array("train_unsupervised", "train_supervised", "test")
                .map(Key.make[Frame](_)), null)
water.H2O.submitTask(sf)
val splits = sf.getResult
val (train_unsupervised, train_supervised, test) = (splits(0), splits(1), splits(2))
toCategorical(train_unsupervised, 30)
toCategorical(train_supervised, 30)
toCategorical(test, 30)
  1. 使用自编码器进行无监督预训练
val response = "Class"
val features = train_unsupervised.names.filterNot(_ == response)
var dlParams = new DeepLearningParameters()
dlParams._ignored_columns = Array(response)
dlParams._train = train_unsupervised._key
dlParams._autoencoder = true
dlParams._reproducible = true
dlParams._seed = 42
dlParams._hidden = Array[Int](10, 2, 10)
dlParams._epochs = 100
dlParams._activation = Activation.Tanh
dlParams._force_load_balance = false
var dl = new DeepLearning(dlParams)
val model_nn = dl.trainModel.get
val uri = new File(new File(inputCSV).getParentFile, "model_nn.bin").toURI
ModelSerializationSupport.exportH2OModel(model_nn, uri)
val model: DeepLearningModel = ModelSerializationSupport.loadH2OModel(uri)
println(model)
  1. 使用隐藏层进行降维
var train_features = model_nn.scoreDeepFeatures(train_unsupervised, 1)
train_features.add("Class", train_unsupervised.vec("Class"))
train_features.setNames(train_features.names.map(_.replaceAll("[.]", "-")))
train_features._key = Key.make()
water.DKV.put(train_features)
val tfDataFrame = asDataFrame(train_features)
Vegas("Compressed").withDataFrame(tfDataFrame).mark(Point).encodeX("DF-L2-C1", Quantitative).encodeY("DF-L2-C2", Quantitative).encodeColor(field = "Class", dataType = Nominal).show
train_features = model_nn.scoreDeepFeatures(train_unsupervised, 2)
train_features._key = Key.make()
train_features.add("Class", train_unsupervised.vec("Class"))
water.DKV.put(train_features)
val features_dim = train_features.names.filterNot(_ == response)
val train_features_H2O = asH2OFrame(train_features)
dlParams = new DeepLearningParameters()
dlParams._ignored_columns = Array(response)
dlParams._train = train_features_H2O
dlParams._autoencoder = true
dlParams._reproducible = true
dlParams._ignore_const_cols = false
dlParams._seed = 42
dlParams._hidden = Array[Int](10, 2, 10)
dlParams._epochs = 100
dlParams._activation = Activation.Tanh
dlParams._force_load_balance = false
dl = new DeepLearning(dlParams)
val model_nn_dim = dl.trainModel.get
ModelSerializationSupport.exportH2OModel(model_nn_dim, new File(new File(inputCSV).getParentFile, "model_nn_dim.bin").toURI)
val test_dim = model_nn.scoreDeepFeatures(test, 2)
val test_dim_score = model_nn_dim.scoreAutoEncoder(test_dim, Key.make(), false)
val result = confusionMat(test_dim_score, test, test_dim_score.anyVec.mean)
println(result.deep.mkString("n"))
  1. 异常检测
test_dim_score.add("Class", test.vec("Class"))
val testDF = asDataFrame(test_dim_score).rdd.zipWithIndex.map(r => Row.fromSeq(r._1.toSeq :+ r._2))
val schema = StructType(Array(StructField("Reconstruction-MSE", DoubleType, nullable = false), StructField("Class", ByteType, nullable = false), StructField("idRow", LongType, nullable = false)))
val dffd = spark.createDataFrame(testDF, schema)
dffd.show()
Vegas("Reduced Test", width = 800, height = 600).withDataFrame(dffd).mark(Point).encodeX("idRow", Quantitative).encodeY("Reconstruction-MSE", Quantitative).encodeColor(field = "Class", dataType = Nominal).show
  1. 预训练的监督模型
toCategorical(train_supervised, 29)
val train_supervised_H2O = asH2OFrame(train_supervised)
dlParams = new DeepLearningParameters()
dlParams._pretrained_autoencoder = model_nn._key
dlParams._train = train_supervised_H2O
dlParams._reproducible = true
dlParams._ignore_const_cols = false
dlParams._seed = 42
dlParams._hidden = Array[Int](10, 2, 10)
dlParams._epochs = 100
dlParams._activation = Activation.Tanh
dlParams._response_column = "Class"
dlParams._balance_classes = true
dl = new DeepLearning(dlParams)
val model_nn_2 = dl.trainModel.get
val predictions = model_nn_2.score(test, "predict")
test.add("predict", predictions.vec("predict"))
asDataFrame(test).groupBy("Class", "predict").count.show
Vegas().withDataFrame(asDataFrame(test)).mark(Bar).encodeY(field = "*", dataType = Quantitative, AggOps.Count, axis = Axis(title = "", format = ".2f"), hideAxis = true).encodeX("Class", Ord).encodeColor("predict", Nominal, scale = Scale(rangeNominals = List("#EA98D2", "#659CCA"))).configMark(stacked = StackOffset.Normalize).show
  1. 在高度不平衡数据上的模型评估
val trainMetrics = ModelMetricsSupport.modelMetrics[ModelMetricsBinomial](model_nn_2, test)
val auc = trainMetrics._auc
val metrics = auc._tps.zip(auc._fps).zipWithIndex.map(x => x match { case ((a, b), c) => (a, b, c) })
val fullmetrics = metrics.map(_ match { case (a, b, c) => (a, b, auc.tn(c), auc.fn(c)) })
val precisions = fullmetrics.map(_ match { case (tp, fp, tn, fn) => tp / (tp + fp) })
val recalls = fullmetrics.map(_ match { case (tp, fp, tn, fn) => tp / (tp + fn) })
val rows = for (i <- 0 until recalls.length) yield r(precisions(i), recalls(i))
val precision_recall = rows.toDF()
Vegas("ROC", width = 800, height = 600).withDataFrame(precision_recall).mark(Line).encodeX("recall", Quantitative).encodeY("precision", Quantitative).show
val sensitivity = fullmetrics.map(_ match { case (tp, fp, tn, fn) => tp / (tp + fn) })
val specificity = fullmetrics.map(_ match { case (tp, fp, tn, fn) => tn / (tn + fp) })
val rows2 = for (i <- 0 until specificity.length) yield r2(sensitivity(i), specificity(i))
val sensitivity_specificity = rows2.toDF
Vegas("sensitivity_specificity", width = 800, height = 600).withDataFrame(sensitivity_specificity).mark(Line).encodeX("specificity ", Quantitative).encodeY("sensitivity", Quantitative).show
val withTh = auc._tps.zip(auc._fps).zipWithIndex.map(x => x match { case ((a, b), c) => (a, b, auc.tn(c), auc.fn(c), auc._ths(c)) })
val rows3 = for (i <- 0 until withTh.length) yield r3(withTh(i)._1, withTh(i)._2, withTh(i)._3, withTh(i)._4, withTh(i)._5)
Vegas("tp", width = 800, height = 600).withDataFrame(rows3.toDF).mark(Line).encodeX("th", Quantitative).encodeY("tp", Quantitative).show
Vegas("fp", width = 800, height = 600).withDataFrame(rows3.toDF).mark(Line).encodeX("th", Quantitative).encodeY("fp", Quantitative).show
Vegas("fp", width = 800, height = 600).withDataFrame(rows3.toDF).mark(Line).filter("datum.th > 0.01").encodeX("th", Quantitative).encodeY("fp", Quantitative).show
Vegas("tn", width = 800, height = 600).withDataFrame(rows3.toDF).mark(Line).encodeX("th", Quantitative).encodeY("tn", Quantitative).show
Vegas("fn", width = 800, height = 600).withDataFrame(rows3.toDF).mark(Line).encodeX("th", Quantitative).encodeY("fn", Quantitative).show
  1. 停止 Spark 会话和 H2O 上下文
h2oContext.stop(stopSparkContext = true)
spark.stop()
3.4 辅助类和方法
def toCategorical(f: Frame, i: Int): Unit = {
    f.replace(i, f.vec(i).toCategoricalVec)
    f.update()
}
def confusionMat(mSEs:water.fvec.Frame,actualFrame:water.fvec.Frame,thresh: Double):Array[Array[Int]] = {
    val actualColumn = actualFrame.vec("Class");
    val l2_test = mSEs.anyVec();
    val result = Array.ofDim[Int](2, 2)
    var i = 0
    var ii, jj = 0
    for (i <- 0 until l2_test.length().toInt) {
        ii = if (l2_test.at(i) > thresh) 1 else 0;
        jj = actualColumn.at(i).toInt
        result(ii)(jj) = result(ii)(jj) + 1
    }
    result
}

通过以上步骤,我们可以构建一个基于自编码器和异常检测的欺诈分析模型。在实际应用中,还可以进一步优化模型,如进行超参数调优、尝试不同的特征工程或算法,以提高模型的性能。

利用自编码器和异常检测进行欺诈分析

4. 模型评估指标的深入理解

在高度不平衡的信用卡欺诈检测数据集中,传统的模型评估指标如准确率(Accuracy)或曲线下面积(AUC)可能会给出过于乐观的结果,因为这些指标会受到多数类(正常交易)高比例正确分类的影响。因此,我们需要采用更适合不平衡数据的评估指标,如精确 - 召回曲线(Precision - Recall Curve)和灵敏度 - 特异度曲线(Sensitivity - Specificity Curve)。

  • 精确 - 召回曲线 :精确率(Precision)是指预测为欺诈的样本中实际为欺诈的比例,召回率(Recall)也称为灵敏度,是指实际为欺诈的样本中被正确预测为欺诈的比例。通过计算不同阈值下的精确率和召回率,并绘制曲线,可以直观地看到模型在不同阈值下的性能表现。
  • 灵敏度 - 特异度曲线 :灵敏度即召回率,特异度(Specificity)是指实际为正常交易的样本中被正确预测为正常交易的比例。该曲线展示了模型在正确分类欺诈和正常交易之间的权衡关系。

我们还可以通过手动设置不同的预测阈值,计算真正例(True Positive,TP)、假正例(False Positive,FP)、真反例(True Negative,TN)和假反例(False Negative,FN)的数量,来进一步分析模型在不同阈值下的性能。例如,当我们将预测阈值从默认的 0.5 提高到 0.6 时,可以增加正确分类的正常交易数量,同时不会损失太多正确分类的欺诈交易数量。

5. 模型优化建议

虽然我们已经构建了一个基于自编码器和异常检测的欺诈分析模型,并且取得了一定的效果,但仍然有许多可以优化的地方。以下是一些建议:

  • 超参数调优 :通过网格搜索(Grid Search)或随机搜索(Random Search)等方法,尝试不同的超参数组合,如隐藏层神经元数量、训练轮数、激活函数等,以找到最优的模型配置。
  • 特征工程 :回到原始特征,尝试不同的特征工程方法,如特征选择、特征提取、特征组合等,以提高模型的性能。例如,可以根据业务知识选择更有代表性的特征,或者通过主成分分析(PCA)等方法提取更有效的特征。
  • 尝试不同的算法 :除了自编码器和深度学习模型,还可以尝试其他机器学习算法,如支持向量机(SVM)、决策树(Decision Tree)、随机森林(Random Forest)等,并比较它们的性能。
  • 集成学习 :将多个不同的模型进行集成,如使用投票法(Voting)、堆叠法(Stacking)等,以提高模型的稳定性和准确性。
6. 总结与展望

本文详细介绍了如何使用自编码器和异常检测技术构建信用卡欺诈分析模型。从数据探索、模型训练到评估,我们逐步深入地了解了整个流程,并通过实际代码实现了各个步骤。

通过对数据的探索性分析,我们发现了数据的不平衡性以及时间和金额等特征与欺诈交易的关系。使用自编码器进行无监督预训练,帮助我们学习到数据的有效表示,并进行了降维处理。在异常检测阶段,通过计算均方误差(MSE),我们能够识别出可能的欺诈交易。最后,通过构建预训练的监督模型,并使用适合不平衡数据的评估指标,我们对模型的性能进行了评估。

未来的研究方向可以包括:

  • 实时欺诈检测 :在实际应用中,欺诈行为往往是实时发生的,因此需要开发实时欺诈检测系统,能够在短时间内对交易进行判断。
  • 多模态数据融合 :除了信用卡交易数据,还可以结合其他模态的数据,如用户行为数据、设备信息等,以提高欺诈检测的准确性。
  • 对抗攻击防御 :随着欺诈手段的不断升级,模型可能会受到对抗攻击的影响。因此,需要研究对抗攻击防御技术,提高模型的鲁棒性。

总之,信用卡欺诈检测是一个具有挑战性但又非常重要的领域,通过不断地研究和创新,我们可以开发出更高效、更准确的欺诈检测模型,为金融行业的安全保驾护航。

流程图

graph LR
    A[数据探索] --> B[自编码器无监督预训练]
    B --> C[降维处理]
    C --> D[异常检测]
    D --> E[预训练的监督模型]
    E --> F[模型评估]
    F --> G[模型优化]

表格:模型评估指标对比

评估指标 含义 适用场景
准确率(Accuracy) 预测正确的样本占总样本的比例 数据平衡时适用
精确率(Precision) 预测为正类的样本中实际为正类的比例 关注预测为正类的准确性时适用
召回率(Recall)/灵敏度(Sensitivity) 实际为正类的样本中被正确预测为正类的比例 关注正类样本的识别能力时适用
特异度(Specificity) 实际为负类的样本中被正确预测为负类的比例 关注负类样本的识别能力时适用
精确 - 召回曲线(Precision - Recall Curve) 展示不同阈值下精确率和召回率的关系 不平衡数据下评估模型性能
灵敏度 - 特异度曲线(Sensitivity - Specificity Curve) 展示不同阈值下灵敏度和特异度的关系 分析模型在正确分类正类和负类之间的权衡

通过以上内容,我们对信用卡欺诈检测模型有了更深入的了解,并且可以根据实际情况进行模型的优化和改进。希望本文能够为相关领域的研究和实践提供有价值的参考。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值