银行电话营销客户订阅评估:从数据到模型部署
1. 应用部署与数据背景
在开发应用时,若使用Scala Play Framework开发期权交易应用,需要对Web服务器进行配置,使其映射到应用端口。可将应用的HTTP端口设置为9000,以独立服务器的形式部署应用,命令如下:
$ /path/to/bin/<project-name> -Dhttp.port=9000
若要在同一服务器上托管多个应用,或为实现可扩展性和容错性对应用的多个实例进行负载均衡,则可使用前端HTTP服务器。不过,直接使用Play服务器的性能通常比使用前端HTTP服务器更好。但HTTP服务器在处理HTTPS、条件GET请求和静态资源方面表现出色,且许多服务假定前端HTTP服务器是架构的一部分,更多信息可查看 相关文档 。
接下来聚焦银行营销数据集,该数据与葡萄牙银行机构的直接营销活动相关,营销活动基于电话呼叫,目标是预测客户是否会订阅定期存款,这是一个端到端的项目。
2. 数据集介绍
2.1 数据集来源与类型
该数据集曾用于研究论文,后被捐赠给UCI机器学习库,可从 此处 下载。共有四个数据集:
| 数据集名称 | 示例数量 | 输入数量 | 特点 |
| — | — | — | — |
| bank-additional-full.csv | 41,188 | 20 | 包含所有示例,按日期排序(2008年5月至2010年11月),与Moro等人2014年分析的数据接近 |
| bank-additional.csv | 4,119(占10%) | 20 | 从bank-additional-full.csv中随机选取10%的示例 |
| bank-full.csv | 全部 | 17 | 包含所有示例,按日期排序,是旧版本,输入较少 |
| bank.csv | 占10% | 17 | 从bank-full.csv中随机选取10%的示例,旧版本且输入较少 |
2.2 数据集属性
数据集包含21个属性,独立变量(特征)可进一步分类,因变量为 y ,表示客户是否订阅定期存款,具体如下:
| ID | Attribute | Explanation |
| — | — | — |
| 1 | age | 年龄(数字) |
| 2 | job | 工作类型(分类格式,可能值:admin, blue-collar等) |
| 3 | marital | 婚姻状况(分类格式,可能值:divorced, married等) |
| 4 | education | 教育背景(分类格式,可能值:basic.4y, basic.6y等) |
| 5 | default | 信用违约情况(分类格式,可能值:no, yes, unknown) |
| 6 | housing | 客户是否有住房贷款 |
| 7 | loan | 个人贷款情况(分类格式,可能值:no, yes, unknown) |
| 8 | contact | 联系沟通类型(分类格式,可能值:cellular, telephone) |
| 9 | month | 当年最后联系月份(分类格式,可能值:jan, feb等) |
| 10 | day_of_week | 最后联系的星期几(分类格式,可能值:mon, tue等) |
| 11 | duration | 最后联系时长(秒,数值),该属性对输出目标影响大,但通话前未知,仅用于基准测试,构建现实预测模型时应丢弃 |
| 12 | campaign | 当前营销活动中与该客户的联系次数 |
| 13 | pdays | 客户上次被前一营销活动联系后经过的天数(数值,999表示未被联系过) |
| 14 | previous | 本次营销活动前与该客户的联系次数(数值) |
| 15 | poutcome | 前一营销活动的结果(分类:failure, nonexistent, success) |
| 16 | emp.var.rate | 就业变化率(季度指标,数值) |
| 17 | cons.price.idx | 消费者价格指数(月度指标,数值) |
| 18 | cons.conf.idx | 消费者信心指数(月度指标,数值) |
| 19 | euribor3m | 欧元3个月利率(每日指标,数值) |
| 20 | nr.employed | 员工数量(季度指标,数值) |
| 21 | y | 表示客户是否订阅定期存款(二进制值:yes, no) |
3. 数据探索分析工具:Apache Zeppelin
3.1 工具简介
Apache Zeppelin是一个基于Web的笔记本,可实现交互式数据分析。它支持多种解释器,如Apache Spark、Python、JDBC等,能让数据科学家、工程师和从业者使用多种编程语言后端进行数据探索、可视化、共享和协作。本文使用仅包含Spark解释器的二进制包,在Zeppelin上使用Spark,代码用Scala编写。
3.2 安装与配置
3.2.1 安装环境要求
| 要求 | 值/版本 |
|---|---|
| Oracle JDK | 1.7+(需设置 JAVA_HOME ) |
| OS | Mac OS X、Ubuntu 14.X+、CentOS 6.X+、Windows 7 Pro SP1+ |
3.2.2 下载与解压
可从 官网 下载最新版本的Apache Zeppelin,每个版本有三种选项:
- 包含所有解释器的二进制包:支持众多解释器,如Spark、JDBC等。
- 包含Spark解释器的二进制包:通常仅包含Spark解释器,还有解释器网络安装脚本。
- 源代码:可从GitHub仓库获取最新更改并构建。
下载包含Spark解释器的二进制包后,将其解压到本地,假设解压路径为 /home/Zeppelin/ 。
3.2.3 从源代码构建
若想从源代码构建Zeppelin,需先安装以下依赖:
- Git:任意版本
- Maven:3.1.x或更高版本
- JDK:1.7或更高版本
未安装Git和Maven的用户可查看 构建要求 。
3.3 启动与停止
3.3.1 UNIX-like平台
在Ubuntu、Mac等UNIX-like平台上,使用以下命令启动:
$ bin/zeppelin-daemon.sh start
若命令执行成功,终端会显示相应日志。
3.3.2 Windows平台
在Windows上,使用以下命令启动:
$ binzeppelin.cmd
启动成功后,在浏览器中访问 http://localhost:8080 ,即可看到Zeppelin正在运行。
3.3.3 停止Zeppelin
使用以下命令停止Zeppelin:
$ bin/zeppelin-daemon.sh stop
3.4 创建笔记本
在 http://localhost:8080 页面,点击“Create new note”创建新笔记本,由于下载的是仅含Spark解释器的二进制包,下拉列表中只会显示Spark。
4. 数据集探索分析
4.1 加载数据集
在Zeppelin中使用以下代码加载数据集:
val trainDF = spark.read.option("inferSchema", "true")
.format("com.databricks.spark.csv")
.option("delimiter", ";")
.option("header", "true")
.load("data/bank-additional-full.csv")
trainDF.registerTempTable("trainData")
4.2 各属性与订阅决策的相关性分析
4.2.1 标签分布
使用SQL查询查看类别分布:
%sql select y, count(1) from trainData group by y order by y
4.2.2 工作分布
查看工作头衔与订阅决策的相关性:
%sql select job,y, count(1) from trainData group by job, y order by job, y
从图表可知,大多数客户的工作为admin、blue-collar或technician,学生和退休客户的订阅比例最高。
4.2.3 婚姻状况分布
查看婚姻状况与订阅决策的相关性:
%sql select marital,y, count(1) from trainData group by marital,y order by marital,y
分布显示,无论客户婚姻状况如何,订阅数与实例数成正比。
4.2.4 教育程度分布
查看教育程度与订阅决策的相关性:
%sql select education,y, count(1) from trainData group by education,y order by education,y
与婚姻状况类似,教育程度对订阅情况无明显影响。
4.2.5 其他属性分布
还对默认信用、住房、贷款、联系方式、月份、星期几、前一营销活动结果、年龄、通话时长、营销活动联系次数、pdays、previous、就业变化率、消费者价格指数、消费者信心指数、欧元3个月利率、员工数量等属性与订阅决策的相关性进行了分析,具体SQL查询如下:
-- 默认信用分布
%sql select default,y, count(1) from trainData group by default,y order by default,y
-- 住房分布
%sql select housing,y, count(1) from trainData group by housing,y order by housing,y
-- 贷款分布
%sql select loan,y, count(1) from trainData group by loan,y order by loan,y
-- 联系方式分布
%sql select contact,y, count(1) from trainData group by contact,y order by contact,y
-- 月份分布
%sql select month,y, count(1) from trainData group by month,y order by month,y
-- 星期几分布
%sql select day_of_week,y, count(1) from trainData group by day_of_week,y order by day_of_week,y
-- 前一营销活动结果分布
%sql select poutcome,y, count(1) from trainData group by poutcome,y order by poutcome,y
-- 年龄特征
%sql select age,y, count(1) from trainData group by age,y order by age,y
-- 通话时长分布
%sql select duration,y, count(1) from trainData group by duration,y order by duration,y
-- 营销活动联系次数分布
%sql select campaign, count(1), y from trainData group by campaign,y order by campaign,y
-- pdays分布
%sql select pdays, count(1), y from trainData group by pdays,y order by pdays,y
-- previous分布
%sql select previous, count(1), y from trainData group by previous,y order by previous,y
-- 就业变化率分布
%sql select emp_var_rate, count(1), y from trainData group by emp_var_rate,y order by emp_var_rate,y
-- 消费者价格指数特征
%sql select cons_price_idx, count(1), y from trainData group by cons_price_idx,y order by cons_price_idx,y
-- 消费者信心指数分布
%sql select cons_conf_idx, count(1), y from trainData group by cons_conf_idx,y order by cons_conf_idx,y
-- 欧元3个月利率分布
%sql select euribor3m, count(1), y from trainData group by euribor3m,y order by euribor3m,y
-- 员工数量分布
%sql select nr_employed, count(1), y from trainData group by nr_employed,y order by nr_employed,y
4.3 数值特征统计
使用以下代码计算数值特征的统计信息:
import org.apache.spark.sql.types._
val numericFeatures = trainDF.schema.filter(_.dataType != StringType)
val description = trainDF.describe(numericFeatures.map(_.name): _*)
val quantils = numericFeatures
.map(f=>trainDF.stat.approxQuantile(f.name,
Array(.25,.5,.75),0)).transpose
val rowSeq = Seq(Seq("q1"+:quantils(0): _*),
Seq("median"+:quantils(1): _*),
Seq("q3"+:quantils(2): _*))
val rows = rowSeq.map(s=> s match{
case Seq(a:String,b:Double,c:Double,d:Double,
e:Double,f:Double,g:Double,
h:Double,i:Double,j:Double,k:Double)=>
(a,b,c,d,e,f,g,h,i,j,k)})
val allStats = description.unionAll(sc.parallelize(rows).toDF)
allStats.registerTempTable("allStats")
执行以下SQL查询查看统计信息:
%sql select * from allStats
统计信息如下:
| summary | age | duration | campaign | pdays | previous | emp_var_rate | cons_price_idx | cons_conf_idx | euribor3m | nr_employed |
| — | — | — | — | — | — | — | — | — | — | — |
| count | 41188.00 | 41188.00 | 41188.00 | 41188.00 | 41188.00 | 41188.00 | 41188.00 | 41188.00 | 41188.00 | 41188.00 |
| mean | 40.02 | 258.29 | 2.57 | 962.48 | 0.17 | 0.08 | 93.58 | -40.50 | 3.62 | 5167.04 |
| stddev | 10.42 | 259.28 | 2.77 | 186.91 | 0.49 | 1.57 | 0.58 | 4.63 | 1.73 | 72.25 |
| min | 17.00 | 0.00 | 1.00 | 0.00 | 0.00 | -3.40 | 92.20 | -50.80 | 0.63 | 4963.60 |
| max | 98.00 | 4918.00 | 56.00 | 999.00 | 7.00 | 1.40 | 94.77 | -26.90 | 5.05 | 5228.10 |
| q1 | 32.00 | 102.00 | 1.00 | 999.00 | 0.00 | -1.80 | 93.08 | -42.70 | 1.34 | 5099.10 |
| median | 38.00 | 180.00 | 2.00 | 999.00 | 0.00 | 1.10 | 93.75 | -41.80 | 4.86 | 5191.00 |
| q3 | 47.00 | 319.00 | 3.00 | 999.00 | 0.00 | 1.40 | 93.99 | -36.40 | 4.96 | 5228.10 |
5. 客户订阅评估模型实现
5.1 创建Spark会话
val spark = SparkSession.builder
.master("local[*]")
.config("spark.sql.warehouse.dir", "E:/Exp/") // 可根据实际情况修改
.appName(s"OneVsRestExample")
.getOrCreate()
5.2 加载数据集
spark.sqlContext.setConf("spark.sql.caseSensitive", "false");
val trainDF = spark.read.option("inferSchema","true")
.format("com.databricks.spark.csv")
.option("delimiter", ";")
.option("header", "true")
.load("data/bank-additional-full.csv")
5.3 数据预处理
由于数据集中的分类特征域较小,无需使用 StringIndexer ,H2O默认使用One Hot Encoding对枚举类型进行编码。同时, duration 特征在标签已知后才可用,不能用于预测,需将其删除:
val withoutDuration = trainDF.drop("duration")
5.4 设置H2O并导入隐式转换
implicit val h2oContext = H2OContext.getOrCreate(spark.sparkContext)
import h2oContext.implicits._implicit
val sqlContext = SparkSession.builder().getOrCreate().sqlContext
import sqlContext.implicits._
5.5 数据转换与分类特征处理
将训练数据集打乱并转换为H2O帧,然后将字符串特征转换为分类特征:
val H2ODF: H2OFrame = withoutDuration.orderBy(rand())
H2ODF.types.zipWithIndex.foreach(c=> if(c._1.toInt== 2)
toCategorical(H2ODF,c._2))
def toCategorical(f: Frame, i: Int): Unit =
{f.replace(i,f.vec(i).toCategoricalVec)f.update()}
5.6 数据集划分
将数据集划分为60%的训练集、20%的验证集和20%的测试集:
val sf = new FrameSplitter(H2ODF, Array(0.6, 0.2),
Array("train.hex", "valid.hex", "test.hex")
.map(Key.make[Frame](_)), null)
water.H2O.submitTask(sf)
val splits = sf.getResult
val (train, valid, test) = (splits(0), splits(1), splits(2))
5.7 训练深度学习模型
使用自定义函数 buildDLModel 训练深度学习模型:
val dlModel = buildDLModel(train, valid)
def buildDLModel(train: Frame, valid: Frame,epochs: Int = 10,
l1: Double = 0.001,l2: Double = 0.0,
hidden: Array[Int] = Array[Int](256, 256, 256)
)(implicit h2oContext: H2OContext):
DeepLearningModel = {
import h2oContext.implicits._
// 构建模型
val dlParams = new DeepLearningParameters()
dlParams._train = train
dlParams._valid = valid
dlParams._response_column = "y"
dlParams._epochs = epochs
dlParams._l1 = l2
dlParams._hidden = hidden
val dl = new DeepLearning(dlParams, water.Key.make("dlModel.hex"))
dl.trainModel.get
}
5.8 模型评估
5.8.1 训练集评估
训练完成后,打印训练集的AUC和分类误差:
val auc = dlModel.auc()
println("Train AUC: "+auc)
println("Train classification error" + dlModel.classification_error())
输出结果:
Train AUC: 0.8071186909427446
Train classification error: 0.13293674881631662
5.8.2 测试集评估
预测测试集的标签,添加原始标签,转换为Spark DataFrame并打印混淆矩阵:
val result = dlModel.score(test)('predict)
result.add("actual",test.vec("y"))
val predict_actualDF = h2oContext.asDataFrame(result)
predict_actualDF.groupBy("actual","predict").count.show
使用Vegas绘制混淆矩阵的图形表示:
Vegas().withDataFrame(predict_actualDF)
.mark(Bar)
.encodeY(field="*", dataType=Quantitative, AggOps.Count,
axis=Axis(title="",format=".2f"),hideAxis=true)
.encodeX("actual", Ord)
.encodeColor("predict", Nominal,
scale=Scale(rangeNominals=List("#FF2800", "#1C39BB")))
.configMark(stacked=StackOffset.Normalize)
.show()
5.8.3 测试集AUC与曲线绘制
计算测试集的AUC,绘制精确率 - 召回率曲线、灵敏度 - 特异度曲线以及不同预测阈值下的真阳性、假阳性、真阴性和假阴性曲线:
val trainMetrics = ModelMetricsSupport.modelMetrics[ModelMetricsBinomial](dlModel, test)
println(trainMetrics)
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("re-call", 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("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
5.9 定义Scala case类
定义三个Scala case类用于计算精确率、召回率、灵敏度、特异度、真阳性、真阴性、假阳性、假阴性等:
case class r(precision: Double, recall: Double)
case class r2(sensitivity: Double, specificity: Double)
case class r3(tp: Double, fp: Double, tn: Double, fn: Double, th: Double)
5.10 停止Spark会话和H2O上下文
h2oContext.stop(stopSparkContext = true)
spark.stop()
6. 超参数调优与特征选择
6.1 超参数调优的挑战
神经网络的灵活性带来了许多需要调整的超参数,如层数、每层神经元数量、激活函数类型、训练轮数、学习率、权重初始化逻辑、丢弃保留概率等。对于深度学习模型,由于超参数众多且在大型数据集上训练神经网络耗时较长,在合理时间内只能探索超参数空间的一小部分。
6.2 超参数调整建议
6.2.1 隐藏层数量
对于许多问题,可从一到两个隐藏层开始,在大致相同的训练时间内,使用两个隐藏层且总神经元数量相同的效果通常较好。对于更复杂的问题,可逐渐增加隐藏层数量,直到出现过拟合。大型图像分类或语音识别等非常复杂的任务通常需要数十层的网络和大量的训练数据。
6.2.2 每层神经元数量
输入和输出层的神经元数量由任务的输入和输出类型决定。例如,若数据集形状为28 x 28,则输入神经元应为784,输出神经元数量等于要预测的类别数。在本项目中,每个隐藏层设置256个神经元,可将其作为一个超参数进行调整,可逐渐增加神经元数量,直到网络出现过拟合。
6.2.3 激活函数
大多数情况下,隐藏层可使用ReLU激活函数,它计算速度较快,且梯度下降在平台上不易卡住。对于输出层,分类任务通常使用softmax激活函数,回归任务可不使用激活函数。H2O基于的深度学习模型支持的激活函数包括ExpRectifier、ExpRectifierWithDropout等。
6.2.4 权重和偏置初始化
- 避免全零初始化:将所有初始权重设为零在实践中不可行,因为若网络中每个神经元的输出相同,神经元之间将无不对称性。
- 小随机数:可将神经元的权重初始化为小随机数,但不能全为零,也可使用从均匀分布中抽取的小数字。
- 偏置初始化:通常将偏置初始化为零,因为权重中的小随机数可提供不对称性。将偏置设置为小常数(如0.01)虽能确保所有ReLU单元传播梯度,但性能不佳且无持续改进,因此建议设为零。
6.2.5 正则化
可通过以下方法控制神经网络的训练,防止过拟合:
- L2正则化:最常见的正则化形式,使用梯度下降参数更新时,L2正则化使每个权重线性衰减至零。
- L1正则化:为每个权重w添加项λ∣w∣到目标函数,也可结合L1和L2正则化实现弹性网络正则化。
- 最大范数约束:对每个隐藏层神经元的权重向量的大小施加绝对上限,可使用投影梯度下降进一步强制执行该约束。
- 丢弃法:在训练神经网络时,需要调整丢弃率这一超参数,训练时以一定概率保留神经元,测试时不使用丢弃法,网络权重为训练权重的缩放版本。
6.3 特征选择
使用H2O的深度学习算法的一个优势是可获取相对变量/特征重要性。若模型性能不佳,可考虑丢弃不太重要的特征并重新训练。在监督训练过程中可找到特征重要性。
综上所述,通过对银行营销数据集的分析和建模,我们可以预测客户是否会订阅定期存款,并通过超参数调优和特征选择来提高模型性能。
7. 模型评估指标可视化与分析
7.1 精确率 - 召回率曲线分析
精确率 - 召回率曲线展示了模型在不同阈值下精确率和召回率的变化关系。通过代码计算并绘制该曲线:
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("re-call", Quantitative)
.encodeY("precision", Quantitative)
.show()
从曲线中可以直观地看到,精确率和召回率之间存在一种权衡关系。通常希望在两者之间找到一个平衡点,以达到最佳的模型性能。
7.2 灵敏度 - 特异度曲线分析
灵敏度 - 特异度曲线反映了模型在正确预测正类和负类之间的关系。代码如下:
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()
该曲线可以帮助我们理解模型在不同阈值下对正类和负类的分类能力。例如,如果我们希望尽可能正确地分类正类(如欺诈案例),可能需要牺牲一些对负类的分类准确性。
7.3 不同预测阈值下的各类指标曲线分析
通过绘制不同预测阈值下的真阳性、假阳性、真阴性和假阴性曲线,我们可以更深入地了解模型在不同阈值下的性能。
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("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
这些曲线告诉我们,当将预测阈值从默认的0.5提高到0.6时,可以在不损失正确分类的正类案例的情况下,增加正确分类的负类案例数量。
8. 总结与实践建议
8.1 模型性能总结
通过对银行营销数据集的分析和建模,我们使用H2O的深度学习模型预测客户是否会订阅定期存款。训练集的AUC为0.8071186909427446,分类误差为0.13293674881631662;测试集的AUC为76%,模型性能还有一定的提升空间。
8.2 超参数调优总结
超参数调优是提高模型性能的关键,但由于神经网络的超参数众多,在合理时间内只能探索超参数空间的一小部分。我们给出了一些超参数调整的建议:
| 超参数 | 建议 |
| — | — |
| 隐藏层数量 | 从一到两个隐藏层开始,根据问题复杂度逐渐增加 |
| 每层神经元数量 | 根据输入输出类型确定,逐渐增加直到过拟合 |
| 激活函数 | 隐藏层使用ReLU,输出层分类任务用softmax,回归任务可不使用 |
| 权重和偏置初始化 | 权重用小随机数,偏置设为零 |
| 正则化 | 使用L2、L1、最大范数约束和丢弃法 |
8.3 特征选择总结
使用H2O的深度学习算法可以获取特征重要性,若模型性能不佳,可丢弃不太重要的特征并重新训练。
8.4 实践建议
- 在实际应用中,可以根据业务需求调整预测阈值,以平衡正确分类的正类和负类案例数量。
- 持续进行超参数调优和特征选择,不断提高模型性能。
- 结合更多的数据集和特征,进一步提升模型的准确性和泛化能力。
8.5 流程图:模型构建与优化流程
graph LR
A[加载数据集] --> B[数据探索分析]
B --> C[数据预处理]
C --> D[数据集划分]
D --> E[训练模型]
E --> F[模型评估]
F --> G{性能是否满足要求?}
G -- 是 --> H[应用模型]
G -- 否 --> I[超参数调优和特征选择]
I --> E
通过以上的分析和实践,我们可以更好地理解银行营销数据,构建更准确的客户订阅评估模型,为银行的营销决策提供有力支持。
超级会员免费看

被折叠的 条评论
为什么被折叠?



