用自编码方法预测罕见事件

1. 问题:罕见事件与非平衡数据

发生概率低于5%的事件被称作罕见事件(rare events),罕见事件形成的数据是不平衡数据(unbalanced data)——平衡数据是指阳性案例与阴性案例相当的数据,比如各占50%——多数深度学习方法在平衡数据上表现非常好,遇到不平衡数据就会出问题。

举例来说,Kaggle上面有一个识别信用卡诈骗的数据,在28.4万条交易记录中,只有492个记录是诈骗交易(又叫阳性案例,在这个例子当中是罕见事件),占总记录0.172%,那么在诈骗和正常交易这两类记录上,数据就是高度不平衡的。

在这个例子里,不平衡数据带来的问题是,即便什么算法都不用,只需预测每个记录为“正常交易”(阴性),正确率仍可高达99.828%(= 1 - 0.172%)。对于多数项目来说,预测如果能达到这个准确率已经非常好了,所以使用常用的监督学习方法,以追求准确率为目标,最终结果很可能是预测每个交易都正常。

拿计量方法做对比的话,比如使用probit或者logit拟合(或者分类),相当于所有回归系数都不显著。把这组零系数代入回归模型,给出的预测自然也是全是零(严格的来说,应该是截距项对应的阳性概率,即0.172%)。

手头这个项目就遇到了这个问题,全部预测都是阴性。你事先知道某个结论是最优的,人工智能跑了几个小时之后告诉你,这个结论确实最优,这会大大降低你对人工智能的尊敬。

2. 常见解决办法

有两个解决办法。第一,增加阳性样本的样本权重,相应就降低了阴性样本的权重,但这会引起过拟合。第二,使用其他精度的测量,而不只是预测值和实际值的差异,比如ROC AUC得分。分别见这里这里

由于手头问题的特殊性,上两个方法没有奏效。

3. 非监督学习方法:自编码

在Rstudio AI Blog找到的这个方法,由于blog原文对该方法的原理没有做详细解释,通过这几天照抄代码和反复阅读,获得了一些理解,在这里记录一下该方法的思想和实施。
原文:Predicting Fraud with Autoencoders and Keras

3.1 问题与目标

如前,就是前面提到Kaggle上的预测信用卡诈骗交易。

3.2 思想与方法

3.2.1 思想

与常用方法不一样,该方法并不利用案例当中诈骗交易和正常交易分类的信息,最终也不会通过x去预测y。而是试图先构造出正常交易当中x应该具有什么样的分布,再用实际案例的x跟构造出的分布进行比较,差距过大的就是可疑交易。

3.2.2 方法

根据这一想法,需要实现以下各步骤:

  • a. 确定诈骗交易和正常交易在x分布上确实存在差异
  • b. 构造出特征(x)的“正常”分布
  • c. 比较数据中x的分布和上一步所构造出的分布,识别诈骗交易
  • d. 调参

下面进行详细说明。

a. 确定诈骗交易和正常交易在x分布上确实存在差异
(原文Visualization一节)

这是该方法能够奏效的前提,因此必须事先予以确认。不过,这一步本身只是一个描述性统计,并不涉及深度学习。

在原文中,Visualization那一节提供了快速浏览和比较所有变量在两类案例中的分布的代码。使用pipe的话只需要一行代码,这是个具有普适性的神技能,值得多敲几遍记住。

当得出分布的图以后,也建议停下来想一下每个变量为什么会有这样的分布,对阳性案例和阴性案例来说分别意味着什么,这是进一步完善模型的基础。

b. 构造出特征(x)的“正常”分布
(原文
Model definitionTraining the model**两节)

这一步使用的方法就是自编码(autoencoder),原文给出了两个对自编码方法进行介绍的链接,可以沿着细读。这些年的经验是,读一个充满术语和数学的正式材料,只有能够使用非正式表达解释清楚,才算是真正理解这个材料。所以接下来就使用白话记录自己的理解。

首先要回答的问题是,x的分布本来就存在,只需要把初始样本中的正常交易拿过来就能得到,为什么还要大费周折用自编码进行重构?

我猜的答案是,初始数据中包含很多噪音,直接拿来当做比较的基础可能得不到任何有用的信息。打个比方,使用初始数据作为对照基础,就像用一张充满噪点的照片去找人,只能看出照片上有张人脸,但无法辨认出是谁。用这样的照片找人,给你一百张人脸(记住当中可能只有一个是照片上的人,因此是个高度不平衡数据),最佳策略是不管拿出的是哪张人脸,都回答这不是我要找的人,正确概率高达99%。而随机抽一个就说这是要找的人,答错的概率是99%(所以你看,同样是划水,是否讲究技术,结果简直是天壤之别。混日子也得有人工智能来指导,理性躺平才是人生赢家)。积极的解决办法是,找人之前就要先把噪点去掉,autoencoder就是降噪的一个方法,理论上能把照片上人脸的核心特征提炼出来。

去掉噪音的过程是,先把所有的x硬往一个低维度的层里面塞(比如x包括20个变量,第一层只规定往外吐10个变量),在这个过程中,不那么重要的特征就被挤掉了(所谓的降维),然后再把低维度的数据还原到x的维度。在迭代过程中,算法的目标是让还原出来的x和初始数据的x尽量靠近。这时候就能看出来为什么第一步要使用低于x维度的层,如果层的维度跟x的维度一样,最后得出的结果一定是让还原出来的x跟初始数据中的x一样,取得最大拟合,噪音完全被保留了下来。

所以在原文中,除了最后一层,每一层units都小于x的维度。

同时,我们也得到了使用autoencoder的条件,就是x包含的变量当中要具有相当的相关性,也就是说某些变量包含的信息可以由其他变量推导出来,这些变量就是降维的目标。用计量的语言说,x里面要有多重共线性,或者多重共非线性一旦x中各变量是高度不相关的,就丧失了使用autoencoder的基础,硬挤的话会挤掉很重要的东西。

最后还要把低维数据还原到高维,不然在下一步没办法跟初始数据的x分布作比较。相当于原来的照片有256个像素点,虽然当中有噪音,还原来回也得是256个像素点。

实操中,要使用阴性样本来训练模型。

c. 比较数据中x的分布和上一步所构造出的分布,识别诈骗交易
(原文Making Prediction一节)

这一步也就是机器学习里的预测

我们要做的是拿构造出x分布与每一个需要预测的案例比较,挑出来相差远的,模型就可以发出警报。那么在这一步需要确定两个问题,第一,如何比较分布相差远近;第二,定义差距多大时发出警报。

第三步的完整过程在原文的Making Prediction部分。简单说,比较差异使用ROC AUC得分,可以通过Metric包实现。确定警报差距门槛值,可以采取最大精度原则,也可以采取最小成本原则(因为无法彻底排除伪阳性的可能,将一个正常交易识别为诈骗,银行损失一个交易,也就损失一笔手续费),两个原则存在一定的消长关系,但是使用不同原则结果可能差不了太多。

d. 调参
(原文Tuning with CloudML一节)

操作上调参应该在预测之前,但我急于理解这种方法,调参部分就跳过去了。再加上原文里是调用Google的CouldML进行调参,先学会以后再说吧。

至于数据处理(Preprocessing一节),没有什么特别值得说的操作。

4 潜在的扩展

这种方法并不保证通过降噪去掉的只有一种罕见事件,如果样本里、或者真实世界、存在多个罕见事件,假定所构造的正常分布非常有效,即可以通过对比这个分布识别出罕见事件,我们仍然不知道该罕见事件属于哪个类别,因此可能需要进一步分类。

另外,如果所使用的数据是时间序列或者面板数据,那么可能需要gru或者lstm层,这类层的具体表现如何,还不得而知。


跑个题。

参加过一个高科技EFT基金的路演,演讲者是复旦物理系博士,好像高中还拿过奥数金牌。路演当中提到了人工智能,说西方科学家曾经断言中国人做不了科研,因为中国文化里没有分析,没有理论建构的意识(我没有查是哪个科学家说得这种话,以及即便有这个人,他的这一说法是否得到了其他同行的认可)。但自打有了人工智能,一切不再是问题,因为通过数理分析建立起来的最优原则和各种定理,人工智能通过大量迭代的数值运算都能建立起来。换句话说,就算没有推理能力,不用建构,让人工智能这个傻子反复跑,最终也能到达那里。

博士口才出众,讲得激情四射,让我差点忘了他推销的是ETF,一个用不着智力投入的被动投资工具。

事实上,缺了分析,AI照样可以垃圾进垃圾出。


Update:在面板/时间序列数据使用自编码方法

Step-by-step understanding LSTM Autoencoder layersLSTM Autoencoder for Extreme Rare Event Classification in Keras

虽然上述教程是基于python,转成R并不难。比如,原文中的代码

# define model
model = Sequential()
model.add(LSTM(128, activation='relu', input_shape=(timesteps,n_features), return_sequences=True))
model.add(LSTM(64, activation='relu', return_sequences=False))
model.add(RepeatVector(timesteps))
model.add(LSTM(64, activation='relu', return_sequences=True))
model.add(LSTM(128, activation='relu', return_sequences=True))
model.add(TimeDistributed(Dense(n_features)))

model.compile(optimizer='adam', loss='mse')

model.summary()

对应的R代码应该是

model <- keras_model_sequential() %>%
  layer_lstm(units = 128, 
                    input_shape = c(timesteps, n_features), 
                    return_sequences = TRUE) %>%
  layer_lstm(units = 64, activation = "relu")  # return_sequences 默认为 FALSE
  layer_repeat_vector(timesteps) %>%
  layer_lstm(units = 64, activation = "relu",
                    return_sequences = TRUE) %>%
  layer_lstm(units = 128, activation = "relu", 
                    return_sequences = TRUE) %>%
  time_distributed(layer_dense(units = n_features)) 

model %>% compile(
  optimizer = "adam",
  loss = "mse")

summary(model)
      

作为练习,也可以用GRU替换LSTM,并尝试其他激活函数。

最后,非常重要的是,训练模型时要注意两点:(1)使用阴性案例作为样本,这是因为因为要获取正常案例的核心特征;(2)模型的输出也是样本的特征(x),而不是标签(y),这是因为训练的目的是获取X的分布。

对照前面信用卡欺诈的案例的代码看起来就很清楚

model %>% fit(
  x = x_train[y_train == 0,], 
  y = x_train[y_train == 0,], 
  ...
  validation_data = list(x_test[y_test == 0,], x_test[y_test == 0,]), 
  ...
)

x = x_train[y_train == 0,], y = x_train[y_train == 0,] 和后来验证集的定义同时实现上述两个要求。

对面板/时间序列数据,由于包含时间维度,代码相应改成x = x_train[y_train == 0, ,], y = x_train[y_train == 0, ,],也就是在样本选择的index里得多加一个逗号。

推荐阅读更多精彩内容