无监督学习实用指南-三-

45 阅读1小时+

无监督学习实用指南(三)

原文:annas-archive.org/md5/5d48074db68aa41a4c5eb547fcbf1a69

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:群体分割

在 第五章 中,我们介绍了聚类,一种无监督学习方法,用于识别数据中的潜在结构并根据相似性将点分组。这些组(称为簇)应该是同质且明显不同的。换句话说,组内成员应该彼此非常相似,并且与任何其他组的成员非常不同。

从应用的角度来看,基于相似性将成员分组且无需标签指导的能力非常强大。例如,这样的技术可以应用于为在线零售商找到不同的消费者群体,为每个不同的群体定制营销策略(例如预算购物者、时尚达人、球鞋爱好者、技术爱好者、发烧友等)。群体分割可以提高在线广告的定位精度,并改进电影、音乐、新闻、社交网络、约会等推荐系统的推荐效果。

在本章中,我们将使用前一章的聚类算法构建一个应用型无监督学习解决方案 —— 具体来说,我们将执行群体分割。

Lending Club 数据

对于本章,我们将使用 Lending Club 的贷款数据,这是一家美国的点对点借贷公司。平台上的借款人可以以未担保的个人贷款形式借款 1,0001,000 到 40,000,期限为三年或五年。

投资者可以浏览贷款申请,并根据借款人的信用历史、贷款金额、贷款等级和贷款用途选择是否融资。投资者通过贷款支付的利息赚钱,而 Lending Club 则通过贷款起始费用和服务费赚钱。

我们将使用的贷款数据来自 2007–2011 年,并且可以在 Lending Club 网站 上公开获取。数据字典也可以在那里找到。

数据准备

像在前几章中一样,让我们准备好环境以处理 Lending Club 数据。

加载库

首先,让我们加载必要的库:

# Import libraries
'''Main'''
import numpy as np
import pandas as pd
import os, time, re
import pickle, gzip

'''Data Viz'''
import matplotlib.pyplot as plt
import seaborn as sns
color = sns.color_palette()
import matplotlib as mpl

%matplotlib inline

'''Data Prep and Model Evaluation'''
from sklearn import preprocessing as pp
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_recall_curve, average_precision_score
from sklearn.metrics import roc_curve, auc, roc_auc_score

'''Algorithms'''
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans
import fastcluster
from scipy.cluster.hierarchy import dendrogram, cophenet, fcluster
from scipy.spatial.distance import pdist

探索数据

接下来,让我们加载贷款数据并指定要保留哪些列:

原始贷款数据文件有 144 列,但大多数列为空,并且对我们的价值有限。因此,我们将指定一部分主要填充且值得在我们的聚类应用中使用的列。这些字段包括贷款请求金额、资助金额、期限、利率、贷款等级等贷款属性,以及借款人的就业长度、住房所有权状态、年收入、地址以及借款用途等借款人属性。

我们还将稍微探索一下数据:

# Load the data
current_path = os.getcwd()
file = '\\datasets\\lending_club_data\\LoanStats3a.csv'
data = pd.read_csv(current_path + file)

# Select columns to keep
columnsToKeep = ['loan_amnt','funded_amnt','funded_amnt_inv','term', \
                 'int_rate','installment','grade','sub_grade', \
                 'emp_length','home_ownership','annual_inc', \
                 'verification_status','pymnt_plan','purpose', \
                 'addr_state','dti','delinq_2yrs','earliest_cr_line', \
                 'mths_since_last_delinq','mths_since_last_record', \
                 'open_acc','pub_rec','revol_bal','revol_util', \
                 'total_acc','initial_list_status','out_prncp', \
                 'out_prncp_inv','total_pymnt','total_pymnt_inv', \
                 'total_rec_prncp','total_rec_int','total_rec_late_fee', \
                 'recoveries','collection_recovery_fee','last_pymnt_d', \
                 'last_pymnt_amnt']

data = data.loc[:,columnsToKeep]

data.shape

data.head()

数据包含 42,542 笔贷款和 37 个特征(42,542, 37)。

表 6-1 预览数据。

表 6-1. 贷款数据的前几行

loan_amntfunded_amntfunded_amnt_invtermint_rateinstsallmentgrade
05000.05000.04975.036 个月10.65%162.87B
12500.02500.02500.060 个月15.27%59.83C
22400.02400.02400.035 个月15.96%84.33C
310000.010000.010000.036 个月13.49%339.31C
43000.03000.03000.060 个月12.69%67.79B

将字符串格式转换为数值格式

一些特征,如贷款的期限、贷款的利率、借款人的就业时长以及借款人的循环利用率,需要从字符串格式转换为数值格式。让我们进行转换:

# Transform features from string to numeric
for i in ["term","int_rate","emp_length","revol_util"]:
    data.loc[:,i] = \
        data.loc[:,i].apply(lambda x: re.sub("[⁰-9]", "", str(x)))
    data.loc[:,i] = pd.to_numeric(data.loc[:,i])

对于我们的聚类应用程序,我们将只考虑数值特征,忽略所有的分类特征,因为非数值特征在当前形式下无法被我们的聚类算法处理。

填充缺失值

找到这些数值特征,并计算每个特征中 NaN 的数量。然后我们将用特征的平均值或者有时仅仅是数字零来填充这些 NaN,具体取决于从业务角度来看这些特征代表什么:

# Determine which features are numerical
numericalFeats = [x for x in data.columns if data[x].dtype != 'object']

# Display NaNs by feature
nanCounter = np.isnan(data.loc[:,numericalFeats]).sum()
nanCounter

下面的代码显示了每个特征中的 NaN 数量:

loan_amnt               7
funded_amnt             7
funded_amnt_inv         7
term                    7
int_rate                7
installment             7
emp_length              1119
annual_inc              11
dti                     7
delinq_2yrs             36
mths_since_last_delinq  26933
mths_since_last_record  38891
open_acc                36
pub_rec                 36
revol_bal               7
revol_util              97
total_acc               36
out_prncp               7
out_prncp_inv           7
total_pymnt             7
total_pymnt_inv         7
total_rec_prncp         7
total_rec_int           7
total_rec_late_fee      7
recoveries              7
collection_recovery_fee 7
last_pymnt_amnt         7
dtype: int64

大多数特征有少量的 NaN,而一些特征,例如自上次拖欠以来的月数和记录变更以来的时间,有很多 NaN。

让我们填充这些 NaN,这样我们在聚类过程中就不必处理任何 NaN:

# Impute NaNs with mean
fillWithMean = ['loan_amnt','funded_amnt','funded_amnt_inv','term', \
                'int_rate','installment','emp_length','annual_inc',\
                'dti','open_acc','revol_bal','revol_util','total_acc',\
                'out_prncp','out_prncp_inv','total_pymnt', \
                'total_pymnt_inv','total_rec_prncp','total_rec_int', \
                'last_pymnt_amnt']

# Impute NaNs with zero
fillWithZero = ['delinq_2yrs','mths_since_last_delinq', \
                'mths_since_last_record','pub_rec','total_rec_late_fee', \
                'recoveries','collection_recovery_fee']

# Perform imputation
im = pp.Imputer(strategy='mean')
data.loc[:,fillWithMean] = im.fit_transform(data[fillWithMean])

data.loc[:,fillWithZero] = data.loc[:,fillWithZero].fillna(value=0,axis=1)

让我们重新计算 NaN,以确保没有任何 NaN 保留。

我们现在是安全的。所有的 NaN 都已经填充:

numericalFeats = [x for x in data.columns if data[x].dtype != 'object']

nanCounter = np.isnan(data.loc[:,numericalFeats]).sum()
nanCounter
loan_amnt               0
funded_amnt             0
funded_amnt_inv         0
term                    0
int_rate                0
installment             0
emp_length              0
annual_inc              0
dti                     0
delinq_2yrs             0
mths_since_last_delinq  0
mths_since_last_record  0
open_acc                0
pub_rec                 0
revol_bal               0
revol_util              0
total_acc               0
out_prncp               0
out_prncp_inv           0
total_pymnt             0
total_pymnt_inv         0
total_rec_prncp         0
total_rec_int           0
total_rec_late_fee      0
recoveries              0
collection_recovery_fee 0
last_pymnt_amnt         0
dtype: int64

工程特征

让我们还要工程化几个新特征,以补充现有的特征集。这些新特征大多是贷款金额、循环余额、还款和借款人年收入之间的比率:

# Feature engineering
data['installmentOverLoanAmnt'] = data.installment/data.loan_amnt
data['loanAmntOverIncome'] = data.loan_amnt/data.annual_inc
data['revol_balOverIncome'] = data.revol_bal/data.annual_inc
data['totalPymntOverIncome'] = data.total_pymnt/data.annual_inc
data['totalPymntInvOverIncome'] = data.total_pymnt_inv/data.annual_inc
data['totalRecPrncpOverIncome'] = data.total_rec_prncp/data.annual_inc
data['totalRecIncOverIncome'] = data.total_rec_int/data.annual_inc

newFeats = ['installmentOverLoanAmnt','loanAmntOverIncome', \
            'revol_balOverIncome','totalPymntOverIncome', \
           'totalPymntInvOverIncome','totalRecPrncpOverIncome', \
            'totalRecIncOverIncome']

选择最终的特征集并执行缩放

接下来,我们将生成训练数据集,并为我们的聚类算法缩放特征:

# Select features for training
numericalPlusNewFeats = numericalFeats+newFeats
X_train = data.loc[:,numericalPlusNewFeats]

# Scale data
sX = pp.StandardScaler()
X_train.loc[:,:] = sX.fit_transform(X_train)

指定评估标签

聚类是一种无监督学习方法,因此不使用标签。然而,为了评估我们的聚类算法在找到这个 Lending Club 数据集中不同且同质化的借款人群组时的好坏程度,我们将使用贷款等级作为代理标签。

贷款等级目前由字母进行评分,“A” 级贷款最值得信赖和安全,“G” 级贷款最不值得:

labels = data.grade
labels.unique()
array(['B', 'C', 'A', 'E', 'F', 'D', 'G', nan], dtype=object)

贷款等级中有一些 NaN。我们将用值“Z”来填充这些 NaN,然后使用 Scikit-Learn 中的 LabelEncoder 将字母等级转换为数值等级。为了保持一致性,我们将这些标签加载到一个名为“y_train”的 Python 系列中:

# Fill missing labels
labels = labels.fillna(value="Z")

# Convert labels to numerical values
lbl = pp.LabelEncoder()
lbl.fit(list(labels.values))
labels = pd.Series(data=lbl.transform(labels.values), name="grade")

# Store as y_train
y_train = labels

labelsOriginalVSNew = pd.concat([labels, data.grade],axis=1)
labelsOriginalVSNew

表 6-2. 数字与字母贷款等级对比

gradegrade
01B
12C
22C
32C
41B
50A
62C
74E
85F
91B
102C
111B
122C
131B
141B
153D
162C

正如你从表 6-2 中所看到的,所有的“A”等级都被转换为 0,“B”等级为 1,以此类推。

让我们也检查一下是否“A”等级的贷款通常有最低的收费利率,因为它们是最不风险的,其他贷款的利率会逐渐增加:

# Compare loan grades with interest rates
interestAndGrade = pd.DataFrame(data=[data.int_rate,labels])
interestAndGrade = interestAndGrade.T

interestAndGrade.groupby("grade").mean()

表 6-3 证实了这一点。较高的字母等级贷款有较高的利率。¹

表 6-3. 等级与利率

gradeint_rate
0.0734.270844
1.01101.420857
2.01349.988902
3.01557.714927
4.01737.676783
5.01926.530361
6.02045.125000
7.01216.501563

聚类的优度

现在数据准备就绪。我们有一个包含所有 34 个数值特征的 X_train,以及一个包含数值贷款等级的 y_train,我们仅用于验证结果,而不是像在监督式机器学习问题中那样用于训练算法。在构建我们的第一个聚类应用之前,让我们介绍一个函数来分析我们使用聚类算法生成的聚类的优度。具体来说,我们将使用一致性的概念来评估每个聚类的优度。

如果聚类算法在 Lending Club 数据集中很好地分离借款人,那么每个集群都应该有非常相似的借款人,并且与其他组中的借款人不相似。假设相似并被分组在一起的借款人应该有相似的信用档案—换句话说,他们的信用价值应该相似。

如果是这种情况(而在现实世界中,这些假设大部分只是部分成立),给定集群中的借款人通常应被分配相同的数值贷款等级,我们将使用 y_train 中设置的数值贷款等级来验证。在每个集群中具有最频繁出现的数值贷款等级的借款人所占的百分比越高,聚类应用的效果就越好。

举例来说,考虑一个拥有一百名借款人的集群。如果有 30 名借款人的数值贷款等级为 0,25 名借款人的贷款等级为 1,20 名借款人的贷款等级为 2,剩余的借款人贷款等级在 3 到 7 之间,我们会说该集群的准确率为 30%,因为该集群中最频繁出现的贷款等级仅适用于该集群中的 30% 借款人。

如果我们没有一个包含数值贷款等级的y_train来验证簇的好坏,我们可以采用替代方法。我们可以从每个簇中抽样一些借款人,手动确定他们的数值贷款等级,并确定我们是否会给这些借款人大致相同的数值贷款等级。如果是,则该簇是一个好簇——它足够同质化,我们会给我们抽样的借款人大致相同的数值贷款等级。如果不是,则该簇不够好——借款人过于异质化,我们应该尝试使用更多数据、不同的聚类算法等来改进解决方案。

尽管如此,我们不需要对借款人进行抽样和手动标记,因为我们已经有了数值贷款等级,但在没有标签的特定问题上,这一点很重要。

这是分析簇的函数:

def analyzeCluster(clusterDF, labelsDF):
    countByCluster = \
        pd.DataFrame(data=clusterDF['cluster'].value_counts())
    countByCluster.reset_index(inplace=True,drop=False)
    countByCluster.columns = ['cluster','clusterCount']

    preds = pd.concat([labelsDF,clusterDF], axis=1)
    preds.columns = ['trueLabel','cluster']

    countByLabel = pd.DataFrame(data=preds.groupby('trueLabel').count())

    countMostFreq = pd.DataFrame(data=preds.groupby('cluster').agg( \
        lambda x:x.value_counts().iloc[0]))
    countMostFreq.reset_index(inplace=True,drop=False)
    countMostFreq.columns = ['cluster','countMostFrequent']

    accuracyDF = countMostFreq.merge(countByCluster, \
        left_on="cluster",right_on="cluster")

    overallAccuracy = accuracyDF.countMostFrequent.sum()/ \
        accuracyDF.clusterCount.sum()

    accuracyByLabel = accuracyDF.countMostFrequent/ \
        accuracyDF.clusterCount

    return countByCluster, countByLabel, countMostFreq, \
        accuracyDF, overallAccuracy, accuracyByLabel

k-均值应用

我们使用这个 Lending Club 数据集的第一个聚类应用将使用k-均值,这在第五章中有介绍。回顾一下,在k-均值聚类中,我们需要指定所需的簇k,算法将每个借款人精确地分配到这些k簇中的一个。

该算法将通过最小化簇内变化(也称为惯性),使得所有k簇中的簇内变化之和尽可能小,来实现这一点。

我们不只是指定一个k值,而是进行一个实验,将k从 10 到 30 的范围内设置,并绘制我们在前一节定义的准确度测量结果。

基于哪种k度量表现最佳,我们可以构建使用这种最佳k度量的聚类管道:

from sklearn.cluster import KMeans

n_clusters = 10
n_init = 10
max_iter = 300
tol = 0.0001
random_state = 2018
n_jobs = 2

kmeans = KMeans(n_clusters=n_clusters, n_init=n_init, \
                max_iter=max_iter, tol=tol, \
                random_state=random_state, n_jobs=n_jobs)

kMeans_inertia = pd.DataFrame(data=[],index=range(10,31), \
                              columns=['inertia'])

overallAccuracy_kMeansDF = pd.DataFrame(data=[], \
    index=range(10,31),columns=['overallAccuracy'])

for n_clusters in range(10,31):
    kmeans = KMeans(n_clusters=n_clusters, n_init=n_init, \
                    max_iter=max_iter, tol=tol, \
                    random_state=random_state, n_jobs=n_jobs)

    kmeans.fit(X_train)
    kMeans_inertia.loc[n_clusters] = kmeans.inertia_
    X_train_kmeansClustered = kmeans.predict(X_train)
    X_train_kmeansClustered = pd.DataFrame(data= \
        X_train_kmeansClustered, index=X_train.index, \
        columns=['cluster'])

    countByCluster_kMeans, countByLabel_kMeans, \
    countMostFreq_kMeans, accuracyDF_kMeans, \
    overallAccuracy_kMeans, accuracyByLabel_kMeans = \
    analyzeCluster(X_train_kmeansClustered, y_train)

    overallAccuracy_kMeansDF.loc[n_clusters] = \
        overallAccuracy_kMeans

overallAccuracy_kMeansDF.plot()

图 6-1 显示了结果的图表。

使用 K-means 进行不同 K 值的整体准确率

图 6-1. 使用k-均值进行不同k度量的整体准确率

正如我们所见,准确率在大约 30 个簇时最佳,约为 39%。换句话说,对于任何给定的簇,大约 39%的借款人具有该簇中最常见的标签。其余 61%的借款人具有非最常见的标签。

下面的代码显示了k = 30 时的准确率:

0      0.326633
1      0.258993
2      0.292240
3      0.234242
4      0.388794
5      0.325654
6      0.303797
7      0.762116
8      0.222222
9      0.391381
10     0.292910
11     0.317533
12     0.206897
13     0.312709
14     0.345233
15     0.682208
16     0.327250
17     0.366605
18     0.234783
19     0.288757
20     0.500000
21     0.375466
22     0.332203
23     0.252252
24     0.338509
25     0.232000
26     0.464418
27     0.261583
28     0.376327
29     0.269129
dtype: float64

准确率在不同簇之间变化很大。有些簇比其他簇更加同质化。例如,簇 7 的准确率为 76%,而簇 12 的准确率仅为 21%。这是构建一个聚类应用程序的起点,用于根据其与其他借款人的相似度将申请 Lending Club 贷款的新借款人自动分配到预先存在的组中。基于这种聚类,可以自动为新借款人分配一个暂定的数值贷款等级,大约 39%的时间是正确的。

这不是最佳解决方案,我们应考虑是否获取更多数据、执行更多特征工程和选择、选择不同的k-均值算法参数或更改为其他聚类算法以改善结果。可能我们没有足够的数据能够像我们已经做的那样有效地将借款人分成不同且同质的群体;如果是这种情况,需要更多数据和更多的特征工程和选择。或者,对于我们拥有的有限数据,k-均值算法不适合执行此分离。

让我们转向层次聚类,看看我们的结果是否会有所改善。

层次聚类应用

请记住,在层次聚类中,我们不需要预先确定特定数量的群集。相反,我们可以在层次聚类运行结束后选择我们想要的群集数量。层次聚类将构建一个树状图,概念上可以视为倒置的树。底部的叶子是在 Lending Club 上申请贷款的个体借款人。

层次聚类根据借款人彼此之间的相似程度,随着我们垂直向上移动倒置树而将它们连接在一起。彼此最相似的借款人更早加入,而不那么相似的借款人则加入得更晚。最终,所有借款人都在倒置树的顶部——主干处一起加入。

从业务角度来看,这种聚类过程显然非常强大。如果我们能够找到彼此相似并将它们分组的借款人,我们可以更有效地为它们分配信用评级。我们还可以针对不同的借款人群体制定具体策略,并从关系的角度更好地管理它们,提供更好的整体客户服务。

一旦层次聚类算法运行完毕,我们可以确定我们想要切割树的位置。我们切得越低,留下的借款人群体就越多。

让我们首先像我们在第五章中所做的那样训练层次聚类算法:

import fastcluster
from scipy.cluster.hierarchy import dendrogram
from scipy.cluster.hierarchy import cophenet
from scipy.spatial.distance import pdist

Z = fastcluster.linkage_vector(X_train, method='ward', \
                               metric='euclidean')

Z_dataFrame = pd.DataFrame(data=Z,columns=['clusterOne', \
                'clusterTwo','distance','newClusterSize'])

表 6-4 展示了输出的数据框的样子。前几行是最底层借款人的初始联接。

表 6-4. 层次聚类的最底层叶子节点

clusterOneclusterTwodistancenewClusterSize
039786.039787.00.000000e+002.0
139788.042542.00.000000e+003.0
242538.042539.00.000000e+002.0
342540.042544.00.000000e+003.0
442541.042545.03.399350e-174.0
542543.042546.05.139334e-177.0
633251.033261.01.561313e-012.0
742512.042535.03.342654e-012.0
842219.042316.03.368231e-012.0
96112.021928.03.384368e-012.0
1033248.033275.03.583819e-012.0
1133253.033265.03.595331e-012.0
1233258.042552.03.719377e-013.0
1320430.023299.03.757307e-012.0
145455.032845.03.828709e-012.0
1528615.030306.03.900294e-012.0
169056 .09769.03.967378e-012.0
1711162.013857.03.991124e-012.0
1833270.042548.03.995620e-013.0
1917422.017986.04.061704e-012.0

请记住,最后几行表示倒置树的顶部,最终将 42,541 名借款人汇总在一起(见表 6-5)。

表 6-5. 层次聚类的最顶层叶节点

clusterOneclusterTwodistancenewClusterSize
4252185038.085043.0132.7157233969.0
4252285051.085052.0141.3865692899.0
4253285026.085027.0146.9767032351.0
4252485048.085049.0152.6601925691.0
4252585036.085059.0153.5122815956.0
4252685033.085044.0160.8259592203.0
4252785055.085061.0163.701428668.0
4252885062.085066.0168.1992956897.0
4252985054.085060.0168.9240399414.0
4253085028.085064.0185.2157693118.0
4253185067.085071.0187.83258815370.0
4253285056.085073.0203.21214717995.0
4253385057.085063.0205.2859939221.0
4253485068.085072.0207.9026605321.0
4253585069.085075.0236.7545819889.0
4253685070.085077.0298.58775516786.0
4253785058.085078.0309.94686716875.0
4253885074.085079.0375.69845834870.0
4253985065.085080.0400.71154737221.0
4250485076.085081.0644.04747242542.0

现在,让我们根据distance_threshold来截取树状图,以便获得可管理的集群数量。根据试验,设置distance_threshold为 100,结果得到 32 个集群,这是我们将在本例中使用的数量。

from scipy.cluster.hierarchy import fcluster
distance_threshold = 100
clusters = fcluster(Z, distance_threshold, criterion='distance')
X_train_hierClustered = pd.DataFrame(data=clusters,
 index=X_train_PCA.index,columns=['cluster'])

print("Number of distinct clusters: ",
 len(X_train_hierClustered['cluster'].unique()))

我们选择的距离阈值所给出的不同集群数量为 32:

countByCluster_hierClust, countByLabel_hierClust, countMostFreq_hierClust,
 accuracyDF_hierClust, overallAccuracy_hierClust, accuracyByLabel_hierClust =
 analyzeCluster(X_train_hierClustered, y_train)
print("Overall accuracy from hierarchical clustering: ",
 overallAccuracy_hierClust)

下面的代码展示了层次聚类的总体准确率:

Overall accuracy from hierarchical clustering: 0.3651685393258427

总体准确率约为 37%,略低于k-means 聚类。尽管如此,层次聚类与k-means 聚类的工作方式不同,可能会更准确地分组一些借款人,而k-means 可能会比层次聚类更准确地分组其他借款人。

换句话说,这两种聚类算法可能互补,值得通过合并两种算法并评估合并结果来探索。与k-means 一样,准确率在不同集群之间差异很大。一些集群比其他集群更同质化:

Accuracy by cluster for hierarchical clustering

0      0.304124
1      0.219001
2      0.228311
3      0.379722
4      0.240064
5      0.272011
6      0.314560
7      0.263930
8      0.246138
9      0.318942
10     0.302752
11     0.269772
12     0.335717
13     0.330403
14     0.346320
15     0.440141
16     0.744155
17     0.502227
18     0.294118
19     0.236111
20     0.254727
21     0.241042
22     0.317979
23     0.308771
24     0.284314
25     0.243243
26     0.500000
27     0.289157
28     0.365283
29     0.479693
30     0.393559
31     0.340875

HDBSCAN 应用

现在让我们转向 HDBSCAN,并将此聚类算法应用于在 Lending Club 数据集中对相似借款人进行分组。

回想一下,HDBSCAN 将根据借款人在高维空间中属性的密集程度将其分组在一起。与k-means 或分层聚类不同,不是所有的借款人都会被分组。一些与其他借款人群体非常不同的借款人可能保持未分组状态。这些是异常借款人,值得调查,看看它们与其他借款人不同的良好业务原因。可能可以为一些借款人群体自动分配数值贷款等级,但对于那些不同的借款人,可能需要更为细致的信用评分方法。

让我们看看 HDBSCAN 的表现:

import hdbscan

min_cluster_size = 20
min_samples = 20
alpha = 1.0
cluster_selection_method = 'leaf'

hdb = hdbscan.HDBSCAN(min_cluster_size=min_cluster_size, \
    min_samples=min_samples, alpha=alpha, \
    cluster_selection_method=cluster_selection_method)

X_train_hdbscanClustered = hdb.fit_predict(X_train)
X_train_hdbscanClustered = pd.DataFrame(data= \
    X_train_hdbscanClustered, index=X_train.index, \
    columns=['cluster'])

countByCluster_hdbscan, countByLabel_hdbscan, \
    countMostFreq_hdbscan, accuracyDF_hdbscan, \
    overallAccuracy_hdbscan, accuracyByLabel_hdbscan = \
    analyzeCluster(X_train_hdbscanClustered, y_train)

下面的代码显示了 HDBSCAN 的整体准确率:

Overall accuracy from HDBSCAN: 0.3246203751586667

如此所见,整体准确率约为 32%,比k-means 或分层聚类的准确率都差。

表 6-6 显示了各种簇及其簇大小。

表 6-6. HDBSCAN 的簇结果

簇计数
0–132708
174070
223668
311096
44773
50120
6649
7338
8520

32,708 名借款人属于簇-1,意味着它们未分组。

以下显示了各簇的准确率:

0       0.284487
1       0.341667
2       0.414234
3       0.332061
4       0.552632
5       0.438551
6       0.400000
7       0.408163
8       0.590663

在这些簇中,准确率从 28%到 59%不等。

结论

在本章中,我们基于从 2007 年到 2011 年在 Lending Club 申请无抵押个人贷款的借款人构建了一个无监督的聚类应用程序。这些应用程序基于k-means、分层聚类和分层 DBSCAN。k-means 表现最佳,整体准确率约为 39%。

虽然这些应用程序表现还可以,但它们可以大幅改进。你应该尝试使用这些算法来改进解决方案。

这结束了本书中使用 Scikit-Learn 的无监督学习部分。接下来,我们将探索基于神经网络的形式的无监督学习,使用 TensorFlow 和 Keras。我们将从第七章中的表示学习和自编码器开始。

¹ 我们可以忽略评级为“7”的,这对应于贷款等级“Z”。这些是我们不得不填补贷款等级缺失的贷款。

² 我们在第二章中探讨了集成学习。如果需要复习,请参考“集成”。

第三部分:使用 TensorFlow 和 Keras 进行无监督学习

我们刚刚完成了基于 Scikit-Learn 的无监督学习部分。现在我们将转向基于神经网络的无监督学习。在接下来的几章中,我们将介绍神经网络,包括应用它们的流行框架 TensorFlow 和 Keras。

在第七章中,我们将使用自编码器——一个浅层神经网络——自动进行特征工程和特征选择。在此基础上,在第八章中,我们将把自编码器应用到一个真实世界的问题上。随后,在第九章中,我们将探讨如何将无监督学习问题转化为半监督学习问题,利用少量标签来提高纯无监督模型的精确度和召回率。

完成浅层神经网络的回顾后,我们将在本书的最后部分讨论深层神经网络。

第七章:自编码器

本书的前六章探讨了如何利用无监督学习进行降维和聚类,我们讨论的概念帮助我们构建了检测异常和基于相似性分割群组的应用程序。

然而,无监督学习能够做的远不止这些。无监督学习在特征提取方面表现出色,特征提取是一种从原始特征集生成新特征表示的方法;新的特征表示称为学习表示,并用于提高监督学习问题的性能。换句话说,特征提取是无监督学习到监督学习的手段。

自编码器是特征提取的一种形式。它们使用前馈、非递归神经网络执行表示学习。表示学习是涉及神经网络的整个机器学习分支的核心部分。

在自编码器中——它们是一种表示学习的形式——神经网络的每一层学习原始特征的表示,后续层基于前面层学到的表示进行构建。逐层递进,自编码器从简单的表示学习逐步建立更为复杂的表示,形成所谓的层次概念,并且这些概念变得越来越抽象。

输出层是原始特征的最终新学习表示。然后,可以将这种学习表示用作监督学习模型的输入,以改进泛化误差。

但在我们过多深入之前,让我们先介绍神经网络以及 Python 框架 TensorFlow 和 Keras。

神经网络

在其根本上,神经网络执行表示学习,即神经网络的每一层从前一层学习到一个表示。通过逐层构建更加细致和详细的表示,神经网络可以完成非常惊人的任务,如计算机视觉、语音识别和机器翻译。

神经网络有两种形式——浅层和深层。浅层网络有少量层,而深层网络有许多层。深度学习因其使用深度(多层)神经网络而得名。浅层神经网络并不特别强大,因为表示学习的程度受到层次较少的限制。另一方面,深度学习非常强大,目前是机器学习中最热门的领域之一。

明确一点,使用神经网络进行浅层和深层学习只是整个机器学习生态系统的一部分。使用神经网络和传统机器学习之间的主要区别在于,神经网络自动执行了大部分特征表示,而在传统机器学习中则是手动设计的。

神经网络具有输入层、一个或多个隐藏层和一个输出层。隐藏层的数量定义了神经网络的深度。您可以将这些隐藏层视为中间计算;这些隐藏层共同允许整个神经网络执行复杂的函数逼近。

每个层次有一定数量的节点(也称为神经元单元)组成该层。然后,每层的节点连接到下一层的节点。在训练过程中,神经网络确定分配给每个节点的最佳权重。

除了增加更多的层次外,我们还可以向神经网络添加更多节点,以增加神经网络模拟复杂关系的能力。这些节点被输入到一个激活函数中,该函数决定了当前层的值被馈送到神经网络的下一层。常见的激活函数包括线性sigmoid双曲正切修正线性单元(ReLU)激活函数。最终的激活函数通常是softmax 函数,它输出输入观察值属于某个类的概率。这对于分类问题非常典型。

神经网络可能还包括偏置节点;这些节点始终是常量值,并且与前一层的节点不连接。相反,它们允许激活函数的输出向上或向下偏移。通过隐藏层(包括节点、偏置节点和激活函数),神经网络试图学习正确的函数逼近,以便将输入层映射到输出层。

在监督学习问题中,这相当直观。输入层表示馈送到神经网络的特征,输出层表示分配给每个观察的标签。在训练过程中,神经网络确定了在整个神经网络中哪些权重有助于最小化每个观察的预测标签与真实标签之间的误差。在无监督学习问题中,神经网络通过各个隐藏层学习输入层的表示,但不受标签的指导。

神经网络非常强大,能够模拟复杂的非线性关系,这是传统机器学习算法难以处理的。总体来说,这是神经网络的一个伟大特性,但也存在潜在风险。因为神经网络能够建模如此复杂的非线性关系,它们也更容易过拟合,这是在设计使用神经网络的机器学习应用时需要注意和解决的问题。¹

尽管有多种类型的神经网络,比如递归神经网络(数据可以在任何方向上流动,用于语音识别和机器翻译)和卷积神经网络(用于计算机视觉),我们将专注于更为直接的前馈神经网络,其中数据仅向一个方向移动:向前。

我们还必须进行更多的超参数优化,以使神经网络表现良好——包括选择成本函数、用于最小化损失的算法、起始权重的初始化类型、用于训练神经网络的迭代次数(即周期数)、每次权重更新前要喂入的观察次数(即批量大小)以及在训练过程中移动权重的步长(即学习率)。

TensorFlow

在介绍自动编码器之前,让我们先探索一下TensorFlow,这是我们用来构建神经网络的主要库。TensorFlow 是一个开源软件库,用于高性能数值计算,最初由 Google Brain 团队为内部使用开发。在 2015 年 11 月,它作为开源软件发布。²

TensorFlow 可在许多操作系统上使用(包括 Linux、macOS、Windows、Android 和 iOS),并且可以在多个 CPU 和 GPU 上运行,使得软件在快速性能方面非常具有可扩展性,并且可以部署到桌面、移动、网络和云端用户。

TensorFlow 的美妙之处在于用户可以在 Python 中定义神经网络——或者更普遍地说,定义计算图——然后使用 C++ 代码运行这个神经网络,这比 Python 快得多。

TensorFlow 还能够并行化计算,将整个操作序列分解为多个部分,并在多个 CPU 和 GPU 上并行运行。对于像 Google 为其核心操作(如搜索)运行的大规模机器学习应用程序来说,这样的性能非常重要。

尽管有其他能够实现类似功能的开源库,TensorFlow 已经成为最受欢迎的一个,部分原因是 Google 的品牌。

TensorFlow 示例

在我们继续之前,让我们建立一个 TensorFlow 计算图并运行一个计算。我们将导入 TensorFlow,使用 TensorFlow API 定义几个变量(类似于我们在之前章节中使用的 Scikit-Learn API),然后计算这些变量的值:

import tensorflow as tf

b = tf.constant(50)
x = b * 10
y = x + b

with tf.Session() as sess:
    result = y.eval()
    print(result)

很重要的一点是,这里有两个阶段。首先,我们构建计算图,定义了 b、x 和 y。然后,通过调用 tf.Session() 执行计算图。在调用之前,CPU 和/或 GPU 不会执行任何计算。而是仅仅存储计算的指令。执行此代码块后,您将如预期看到结果为“550”。

后面,我们将使用 TensorFlow 构建实际的神经网络。

Keras

Keras 是一个开源软件库,提供在 TensorFlow 之上运行的高级 API。它为 TensorFlow 提供了一个更加用户友好的接口,使数据科学家和研究人员能够比直接使用 TensorFlow 命令更快速、更轻松地进行实验。Keras 的主要作者也是一位 Google 工程师,弗朗索瓦·朱勒。

当我们开始使用 TensorFlow 构建模型时,我们将亲自动手使用 Keras 并探索其优势。

自编码器:编码器和解码器

现在我们已经介绍了神经网络及其在 Python 中的流行库——TensorFlow 和 Keras,让我们来构建一个自编码器,这是最简单的无监督学习神经网络之一。

自编码器包括两部分,一个编码器和一个解码器。编码器将输入的特征集通过表示学习转换为不同的表示,解码器将这个新学到的表示转换回原始格式。

自编码器的核心概念与我们在第三章中学习的降维概念类似。类似于降维,自编码器不会记忆原始观察和特征,这将是所谓的恒等函数。如果它学到了确切的恒等函数,那么自编码器就没有用处。相反,自编码器必须尽可能接近但不完全复制原始观察,使用新学到的表示;换句话说,自编码器学习了恒等函数的近似。

由于自编码器受到约束,它被迫学习原始数据的最显著特性,捕获数据的基础结构;这与降维中发生的情况类似。约束是自编码器的一个非常重要的属性——约束迫使自编码器智能地选择要捕获的重要信息和要丢弃的不相关或较不重要的信息。

自编码器已经存在几十年了,你可能已经怀疑它们已广泛用于降维和自动特征工程/学习。如今,它们经常用于构建生成模型,例如生成对抗网络

不完全自编码器

在自编码器中,我们最关心的是编码器,因为这个组件是学习原始数据新表示的组件。这个新表示是从原始特征和观察得到的新特征集。

我们将自编码器的编码器函数称为h = f(x),它接收原始观察x并使用函数f中捕获的新学到的表示输出h。解码器函数使用编码器函数重建原始观察,其形式为r = g(h)

如您所见,解码器函数将编码器的输出h馈入并使用其重构函数g重构观察结果,称为r。如果做得正确,g(f(x))不会在所有地方完全等于x,但会足够接近。

我们如何限制编码器函数来近似x,以便它只能学习x的最显著属性而不是精确复制它?

我们可以约束编码器函数的输出h,使其维数少于x。这被称为欠完备自编码器,因为编码器的维数少于原始输入的维数。这再次类似于降维中发生的情况,其中我们接收原始输入维度并将其减少到一个更小的集合。

在这种方式下受限制,自编码器试图最小化我们定义的一个损失函数,使得解码器近似地使用编码器的输出重构观察结果后的重构误差尽可能小。重要的是要意识到隐藏层是维度受限的地方。换句话说,编码器的输出比原始输入的维数少。但解码器的输出是重构的原始数据,因此与原始输入具有相同数量的维数。

当解码器为线性且损失函数为均方误差时,欠完备自编码器学习的是与 PCA 相同类型的新表示,PCA 是我们在第三章介绍的一种降维方法。然而,如果编码器和解码器函数是非线性的,自编码器可以学习更复杂的非线性表示。这才是我们最关心的。但要注意——如果自编码器被赋予了太多的容量和自由度来建模复杂的、非线性的表示,它将简单地记住/复制原始观察结果,而不是从中提取最显著的信息。因此,我们必须有意义地限制自编码器,以防止这种情况发生。

过完备自编码器

如果编码器在比原始输入维度更多的维度上学习表示,那么自编码器被认为是过完备的。这样的自编码器简单地复制原始观察结果,并且不像欠完备自编码器那样被迫有效而紧凑地捕获原始分布的信息。话虽如此,如果我们采用某种形式的正则化,对神经网络学习不必要复杂函数进行惩罚,过完备自编码器可以成功用于降维和自动特征工程。

与欠完备自编码器相比,正则化超完备自编码器更难成功设计,但可能更强大,因为它们可以学习到更复杂但不过度复杂的表示,从而更好地近似原始观察结果而不是精确复制它们。

简而言之,表现良好的自编码器是那些学习到新表示,这些表示足够接近原始观察结果但并非完全相同的自编码器。为了做到这一点,自编码器本质上学习了一个新的概率分布。

密集自编码器 vs. 稀疏自编码器

如果你还记得,在第三章中,我们有密集(正常)和稀疏版本的降维算法。自编码器的工作原理类似。到目前为止,我们只讨论了输出密集最终矩阵的普通自编码器,以便少数特征具有有关原始数据的最显著信息。然而,我们可能希望输出一个稀疏的最终矩阵,以便捕获的信息更好地分布在自编码器学习到的特征之间。

为了做到这一点,我们需要在自编码器中包括不仅作为一部分的重构误差,还要包括稀疏惩罚,以便自编码器必须考虑最终矩阵的稀疏性。稀疏自编码器通常是超完备的——隐藏层的单元数比输入特征的数量多,但只有很小一部分隐藏单元被允许同时处于活动状态。这样定义的稀疏自编码器将输出一个具有更多零值的最终矩阵,所捕获的信息将更好地分布在学习到的特征中。

对于某些机器学习应用,稀疏自编码器具有更好的性能,并且学习到的表示也与正常(密集)自编码器略有不同。稍后,我们将使用真实示例来看看这两种类型的自编码器之间的区别。

去噪自编码器

正如你现在所知,自编码器能够从原始输入数据中学习新的(并且改进的)表示,捕获最显著的元素,但忽略原始数据中的噪音。

在某些情况下,我们可能希望设计的自编码器更积极地忽略数据中的噪声,特别是如果我们怀疑原始数据在某种程度上被损坏。想象一下在白天嘈杂的咖啡店里记录两个人之间的对话。我们希望将对话(信号)与背景嘈杂声(噪音)隔离开来。又或者,想象一下由于低分辨率或某种模糊效果而导致图像有颗粒感或失真的数据集。我们希望将核心图像(信号)与失真(噪音)隔离开来。

针对这些问题,我们可以设计一个去噪自编码器,它接收损坏的数据作为输入,并训练以尽可能地输出原始未损坏的数据。当然,尽管这并不容易,但这显然是自编码器应用于解决现实问题的一个非常强大的应用。

变分自编码器

到目前为止,我们已经讨论了使用自编码器来学习原始输入数据的新表示(通过编码器),以最小化新重构数据(通过解码器)与原始输入数据之间的重构误差。

在这些示例中,编码器的大小是固定的,为n,其中n通常比原始维度的数量小——换句话说,我们训练了一个欠完备自编码器。或者n可能大于原始维度的数量——一个过完备自编码器——但通过使用正则化惩罚、稀疏性惩罚等进行约束。但在所有这些情况下,编码器输出一个固定大小为n的单个向量。

一种替代的自编码器被称为变分自编码器,其编码器输出两个向量而不是一个:一个均值向量mu和一个标准差向量sigma。这两个向量形成随机变量,使得musigma的第i个元素对应于第i个随机变量的均值标准差。通过编码器形成这种随机输出,变分自编码器能够基于其从输入数据中学到的知识在连续空间中进行采样。

变分自编码器不仅局限于它训练过的示例,还可以进行泛化并输出新的示例,即使它可能以前从未见过完全相似的示例。这非常强大,因为现在变分自编码器可以生成看起来属于从原始输入数据学习的分布中的新合成数据。像这样的进展导致了一个完全新的和趋势的无监督学习领域,被称为生成建模,其中包括生成对抗网络。使用这些模型,可以生成合成图像、语音、音乐、艺术等,为 AI 生成数据开辟了无限可能。

结论

在本章中,我们介绍了神经网络及其流行的开源库 TensorFlow 和 Keras。我们还探讨了自编码器及其从原始输入数据学习新表示的能力。变种包括稀疏自编码器、去噪自编码器和变分自编码器,等等。

在第八章中,我们将使用本章讨论的技术构建实际应用程序。

在我们继续之前,让我们重新思考一下为什么自动特征提取如此重要。如果没有自动提取特征的能力,数据科学家和机器学习工程师将不得不手工设计可能在解决现实世界问题中重要的特征。这是非常耗时的,并且会极大地限制人工智能领域的进展。

实际上,直到 Geoffrey Hinton 和其他研究人员开发出使用神经网络自动学习新特征的方法——从 2006 年开始引发了深度学习革命——涉及计算机视觉、语音识别、机器翻译等问题一直大多数难以解决。

一旦自动编码器和其他神经网络变种被用来自动从输入数据中提取特征,许多这些问题就变得可以解决,导致过去十年间机器学习领域的一些重大突破。

通过在第八章中的自动编码器的实际应用中,你将看到自动特征提取的力量。

¹ 这个过程被称为正则化。

² 欲了解更多有关 TensorFlow 的信息,请查阅网站

第八章:自动编码器实战

在本章中,我们将构建使用各种版本的自动编码器的应用程序,包括欠完备、过完备、稀疏、去噪和变分自动编码器。

首先,让我们回到我们在 第三章 中介绍的信用卡欺诈检测问题。对于这个问题,我们有 284,807 笔信用卡交易,其中只有 492 笔是欺诈性的。使用监督模型,我们实现了平均精度为 0.82,这非常令人印象深刻。我们可以找到超过 80% 的欺诈,并且精度超过 80%。使用无监督模型,我们实现了平均精度为 0.69,考虑到我们没有使用标签,这也是非常好的。我们可以找到超过 75% 的欺诈,并且精度超过 75%。

让我们看看如何使用自动编码器来解决同样的问题,它也是一种无监督算法,但使用了神经网络。

数据准备

让我们首先加载必要的库:

'''Main'''
import numpy as np
import pandas as pd
import os, time, re
import pickle, gzip

'''Data Viz'''
import matplotlib.pyplot as plt
import seaborn as sns
color = sns.color_palette()
import matplotlib as mpl

%matplotlib inline

'''Data Prep and Model Evaluation'''
from sklearn import preprocessing as pp
from sklearn.model_selection import train_test_split
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import log_loss
from sklearn.metrics import precision_recall_curve, average_precision_score
from sklearn.metrics import roc_curve, auc, roc_auc_score

'''Algos'''
import lightgbm as lgb

'''TensorFlow and Keras'''
import tensorflow as tf
import keras
from keras import backend as K
from keras.models import Sequential, Model
from keras.layers import Activation, Dense, Dropout
from keras.layers import BatchNormalization, Input, Lambda
from keras import regularizers
from keras.losses import mse, binary_crossentropy

接下来,加载数据集并准备使用。我们将创建一个 dataX 矩阵,其中包含所有的 PCA 成分和特征 Amount,但排除 ClassTime。我们将把 Class 标签存储在 dataY 矩阵中。我们还将对 dataX 矩阵中的特征进行缩放,使所有特征的平均值为零,标准差为一。

data = pd.read_csv('creditcard.csv')
dataX = data.copy().drop(['Class','Time'],axis=1)
dataY = data['Class'].copy()
featuresToScale = dataX.columns
sX = pp.StandardScaler(copy=True, with_mean=True, with_std=True)
dataX.loc[:,featuresToScale] = sX.fit_transform(dataX[featuresToScale])

就像我们在 第三章 中所做的那样,我们将创建一个训练集,其中包含三分之二的数据和标签,并创建一个测试集,其中包含三分之一的数据和标签。

让我们将训练集和测试集分别存储为 X_train_AEX_test_AE,我们很快将在自动编码器中使用它们。

X_train, X_test, y_train, y_test = \
    train_test_split(dataX, dataY, test_size=0.33, \
                     random_state=2018, stratify=dataY)

X_train_AE = X_train.copy()
X_test_AE = X_test.copy()

让我们还要重用本书中早期介绍的函数 anomalyScores,来计算原始特征矩阵与新重构特征矩阵之间的重构误差。该函数计算平方误差的总和,并将其归一化到零到一的范围内。

这是一个关键的函数。误差接近于一的交易最异常(即具有最高的重构误差),因此最可能是欺诈性的。误差接近于零的交易具有最低的重构误差,最可能是正常的。

def anomalyScores(originalDF, reducedDF):
    loss = np.sum((np.array(originalDF) - \
                   np.array(reducedDF))**2, axis=1)
    loss = pd.Series(data=loss,index=originalDF.index)
    loss = (loss-np.min(loss))/(np.max(loss)-np.min(loss))
    return loss

我们还将重用一个名为 plotResults 的函数来绘制精确率-召回率曲线、平均精度和 ROC 曲线。

def plotResults(trueLabels, anomalyScores, returnPreds = False):
    preds = pd.concat([trueLabels, anomalyScores], axis=1)
    preds.columns = ['trueLabel', 'anomalyScore']
    precision, recall, thresholds = \
        precision_recall_curve(preds['trueLabel'], \
                               preds['anomalyScore'])
    average_precision = average_precision_score( \
                        preds['trueLabel'], preds['anomalyScore'])

    plt.step(recall, precision, color='k', alpha=0.7, where='post')
    plt.fill_between(recall, precision, step='post', alpha=0.3, color='k')

    plt.xlabel('Recall')
    plt.ylabel('Precision')
    plt.ylim([0.0, 1.05])
    plt.xlim([0.0, 1.0])

    plt.title('Precision-Recall curve: Average Precision = \
 {0:0.2f}'.format(average_precision))

    fpr, tpr, thresholds = roc_curve(preds['trueLabel'], \
                                     preds['anomalyScore'])
    areaUnderROC = auc(fpr, tpr)

    plt.figure()
    plt.plot(fpr, tpr, color='r', lw=2, label='ROC curve')
    plt.plot([0, 1], [0, 1], color='k', lw=2, linestyle='--')
    plt.xlim([0.0, 1.0])
    plt.ylim([0.0, 1.05])
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.title('Receiver operating characteristic: Area under the \
 curve = {0:0.2f}'.format(areaUnderROC))
    plt.legend(loc="lower right")
    plt.show()

    if returnPreds==True:
        return preds

自动编码器的组成部分

首先,让我们构建一个非常简单的自动编码器,包括输入层、单隐藏层和输出层。我们将原始特征矩阵 x 输入到自动编码器中——这由输入层表示。然后,激活函数将应用于输入层,生成隐藏层。这个激活函数称为 f,代表自动编码器的 编码器 部分。隐藏层称为 h(等于 f(x)),代表新学习到的表示。

接下来,激活函数应用于隐藏层(即新学到的表示),以重构原始观测数据。这个激活函数称为g,代表自动编码器的解码器部分。输出层称为r(等于g(h)),代表新重构的观测数据。为了计算重构误差,我们将比较新构建的观测数据r与原始观测数据x

激活函数

在我们决定在这个单隐藏层自动编码器中使用的节点数之前,让我们讨论一下激活函数。

神经网络学习在每个层的节点上应用的权重,但节点是否激活(用于下一层)由激活函数决定。换句话说,激活函数应用于每层的加权输入(加上偏置,如果有的话)。我们将加权输入加偏置称为Y

激活函数接收Y,如果Y超过某个阈值,则激活;否则,不激活。如果激活,则给定节点中的信息传递到下一层;否则,不传递。但是,我们不希望简单的二进制激活。相反,我们希望一系列激活值。为此,我们可以选择线性激活函数或非线性激活函数。线性激活函数是无界的。它可以生成介于负无穷到正无穷之间的激活值。常见的非线性激活函数包括 sigmoid、双曲正切(或简称 tanh)、修正线性单元(或简称 ReLu)和 softmax:

Sigmoid 函数

Sigmoid 函数是有界的,并且可以生成介于零和一之间的激活值。

Tanh 函数

tanh 函数也是有界的,并且可以生成介于负一到正一之间的激活值。其梯度比 sigmoid 函数更陡。

ReLu 函数

ReLu 函数具有一个有趣的性质。如果Y是正的,ReLu 将返回Y。否则,将返回零。因此,对于正值的Y,ReLu 是无界的。

Softmax 函数

softmax 函数用作神经网络中分类问题的最终激活函数,因为它将分类概率归一化为总和为一的值。

在所有这些函数中,线性激活函数是最简单且计算开销最小的。ReLu 是接下来计算开销第二小的,其它则依次类推。

我们的第一个自动编码器

让我们从一个具有线性激活函数的两层自动编码器开始。请注意,只有隐藏层的数量加上输出层计入神经网络的层数。由于我们有一个隐藏层,因此这被称为两层神经网络。

要使用 TensorFlow 和 Keras 构建这一过程,我们首先需要调用Sequential model API。Sequential 模型是层的线性堆叠,在编译模型并在数据上进行训练之前,我们将把我们想要的层类型传递到模型中。¹

# Model one
# Two layer complete autoencoder with linear activation

# Call neural network API
model = Sequential()

一旦我们调用了 Sequential 模型,我们接下来需要指定输入形状,即指定与原始特征矩阵dataX中维度数量相匹配的维度数,这个数字是 29。

我们还需要指定应用于输入层的激活函数(也称为编码器函数)以及我们希望隐藏层具有的节点数。我们将使用linear作为激活函数。

首先,让我们使用一个完整的自编码器,其中隐藏层中的节点数等于输入层中的节点数,即 29。所有这些都可以使用一行代码完成:

model.add(Dense(units=29, activation='linear',input_dim=29))

同样地,我们需要指定应用于隐藏层的激活函数(也称为解码器函数),以重构观察结果,并且我们希望输出层具有的维数。由于我们希望最终重构的矩阵与原始矩阵具有相同的维度,维数需要为 29。此外,我们还将在解码器中使用线性激活函数:

model.add(Dense(units=29, activation='linear'))

接下来,我们需要编译我们为神经网络设计的层。这需要我们选择一个损失函数(也称为目标函数)来指导权重的学习,一个优化器来设定权重学习的过程,并列出一系列度量标准以帮助我们评估神经网络的好坏。

损失函数

让我们从损失函数开始。回想一下,我们根据自编码器基于重构后的特征矩阵与我们输入自编码器的原始特征矩阵之间的重构误差来评估模型。

因此,我们希望将均方误差作为评估指标。(对于我们自定义的评估函数,我们使用平方误差之和,这类似。)²

优化器

神经网络训练多个回合(称为epochs)。在每个回合中,神经网络调整其学习的权重,以减少与上一个回合相比的损失。设置学习这些权重的过程由优化器决定。我们希望找到一个过程,帮助神经网络高效地学习各层节点的最佳权重,从而最小化我们选择的损失函数。

要学习最佳权重,神经网络需要智能地调整其对最佳权重的“猜测”。一种方法是迭代地朝着有助于逐步减少损失函数的方向移动权重。但更好的方法是以一定的随机性朝着这个方向移动权重,换句话说,随机地移动权重。

尽管还有更多内容,这个过程被称为随机梯度下降(或简称 SGD),是训练神经网络中最常用的优化器。³ SGD 具有一个称为alpha的单一学习率,用于所有权重更新,而这个学习率在训练过程中不会改变。然而,在大多数情况下,调整学习率是更好的选择。例如,在早期的 epochs 中,通过较大的程度调整权重更为合理,换句话说,具有较大的学习率或 alpha。

在后续的 epochs 中,当权重更加优化时,微调权重的程度比单向或另一方向上的大步调整更为合理。因此,比 SGD 更好的优化器是Adam 优化算法,它源自自适应矩估计。Adam 优化器动态调整学习率,而不像 SGD 那样在训练过程中保持不变,并且这是我们将使用的优化器。⁴

对于这个优化器,我们可以设置α,这决定了权重更新的速度。较大的α值在更新学习率之前会导致更快的初始学习速度。

训练模型

最后,我们需要选择评估指标,我们将其设置为accuracy以保持简单:⁵

model.compile(optimizer='adam',
              loss='mean_squared_error',
              metrics=['accuracy'])

接下来,我们需要选择 epoch 数量和批次大小,然后通过调用fit方法开始训练过程。epoch 数量决定了整个传递到神经网络中的数据集训练次数。我们将这个设置为 10 来开始。

批次设置了神经网络在进行下一个梯度更新之前训练的样本数量。如果批次等于观察的总数,神经网络将每个 epoch 仅进行一次梯度更新。否则,它将在每个 epoch 中进行多次更新。我们将这个设置为通用的 32 个样本来开始。

在 fit 方法中,我们将传入初始输入矩阵x和目标矩阵y。在我们的案例中,xy都将是原始特征矩阵X_train_AE,因为我们希望比较自编码器的输出——重构特征矩阵与原始特征矩阵来计算重构误差。

记住,这是一个完全无监督的解决方案,所以我们根本不会使用y矩阵。我们将在整个训练矩阵上测试重构误差来验证我们的模型:

num_epochs = 10
batch_size = 32

history = model.fit(x=X_train_AE, y=X_train_AE,
                    epochs=num_epochs,
                    batch_size=batch_size,
                    shuffle=True,
                    validation_data=(X_train_AE, X_train_AE),
                    verbose=1)

由于这是一个完整的自编码器——隐藏层与输入层具有相同的维数,因此对于训练集和验证集,损失都非常低:

Training history of complete autoencoder

Train on 190820 samples, validate on 190820 samples
Epoch 1/10
190820/190820 [==============================] - 29s 154us/step - loss: 0.1056
- acc: 0.8728 - val_loss: 0.0013 - val_acc: 0.9903
Epoch 2/10
190820/190820 [==============================] - 27s 140us/step - loss: 0.0012
- acc: 0.9914 - val_loss: 1.0425e-06 - val_acc: 0.9995
Epoch 3/10
190820/190820 [==============================] - 23s 122us/step - loss: 6.6244
e-04 - acc: 0.9949 - val_loss: 5.2491e-04 - val_acc: 0.9913
Epoch 4/10
190820/190820 [==============================] - 23s 119us/step - loss: 0.0016
- acc: 0.9929 - val_loss: 2.2246e-06 - val_acc: 0.9995
Epoch 5/10
190820/190820 [==============================] - 23s 119us/step - loss: 5.7424
e-04 - acc: 0.9943 - val_loss: 9.0811e-05 - val_acc: 0.9970
Epoch 6/10
190820/190820 [==============================] - 22s 118us/step - loss: 5.4950
e-04 - acc: 0.9941 - val_loss: 6.0598e-05 - val_acc: 0.9959
Epoch 7/10
190820/190820 [==============================] - 22s 117us/step - loss: 5.2291
e-04 - acc: 0.9946 - val_loss: 0.0023 - val_acc: 0.9675
Epoch 8/10
190820/190820 [==============================] - 22s 117us/step - loss: 6.5130
e-04 - acc: 0.9932 - val_loss: 4.5059e-04 - val_acc: 0.9945
Epoch 9/10
190820/190820 [==============================] - 23s 122us/step - loss: 4.9077
e-04 - acc: 0.9952 - val_loss: 7.2591e-04 - val_acc: 0.9908
Epoch 10/10
190820/190820 [==============================] - 23s 118us/step - loss: 6.1469
e-04 - acc: 0.9945 - val_loss: 4.4131e-06 - val_acc: 0.9991

这并不是最优的——自编码器对原始特征矩阵进行了过于精确的重构,记住了输入。

请回想一下,自编码器旨在学习一个新的表示,捕捉原始输入矩阵中最显著的信息,同时丢弃不太相关的信息。简单地记忆输入——也称为学习恒等函数——不会带来新的和改进的表示学习。

在测试集上评估

让我们使用测试集来评估这个自编码器在识别信用卡交易中的欺诈问题上的成功程度。我们将使用predict方法来完成这个任务:

predictions = model.predict(X_test, verbose=1)
anomalyScoresAE = anomalyScores(X_test, predictions)
preds = plotResults(y_test, anomalyScoresAE, True)

如图 8-1 所示,平均精度为 0.30,这并不是很好的结果。在第四章的无监督学习中,使用无监督学习的最佳平均精度为 0.69,有监督系统的平均精度为 0.82。然而,每次训练过程将为训练后的自编码器产生略有不同的结果,因此您可能不会在您的运行中看到相同的性能。

为了更好地了解两层完整自编码器在测试集上的表现,让我们分别运行这个训练过程十次,并存储每次运行在测试集上的平均精度。我们将根据这 10 次运行的平均精度来评估这个完整自编码器在捕捉欺诈方面的能力。

完整自编码器的评估指标

第 8-1 图。完整自编码器的评估指标

为了总结我们迄今为止的工作,这里是从头到尾模拟 10 次运行的代码:

# 10 runs - We will capture mean of average precision
test_scores = []
for i in range(0,10):
    # Call neural network API
    model = Sequential()

    # Apply linear activation function to input layer
    # Generate hidden layer with 29 nodes, the same as the input layer
    model.add(Dense(units=29, activation='linear',input_dim=29))

    # Apply linear activation function to hidden layer
    # Generate output layer with 29 nodes
    model.add(Dense(units=29, activation='linear'))

    # Compile the model
    model.compile(optimizer='adam',
                  loss='mean_squared_error',
                  metrics=['accuracy'])

    # Train the model
    num_epochs = 10
    batch_size = 32

    history = model.fit(x=X_train_AE, y=X_train_AE,
                        epochs=num_epochs,
                        batch_size=batch_size,
                        shuffle=True,
                        validation_data=(X_train_AE, X_train_AE),
                        verbose=1)

    # Evaluate on test set
    predictions = model.predict(X_test, verbose=1)
    anomalyScoresAE = anomalyScores(X_test, predictions)
    preds, avgPrecision = plotResults(y_test, anomalyScoresAE, True)
    test_scores.append(avgPrecision)

print("Mean average precision over 10 runs: ", np.mean(test_scores))
test_scores

下面的代码总结了这 10 次运行的结果。平均精度为 0.30,但平均精度从 0.02 到 0.72 不等。变异系数(定义为 10 次运行中标准差除以平均值)为 0.88。

Mean average precision over 10 runs: 0.30108318944579776
Coefficient of variation over 10 runs: 0.8755095071789248

[0.25468022666666157,
0.092705950994909,
0.716481644928299,
0.01946589342639965,
0.25623865457838263,
0.33597083510378234,
0.018757053070824415,
0.6188569405068724,
0.6720552647581304,
0.025619070873716072]

让我们尝试通过构建这个自编码器的变种来改进我们的结果。

具有线性激活函数的两层欠完整自编码器

让我们尝试一个欠完整自编码器,而不是完整的自编码器。

与先前的自编码器相比,唯一变化的是隐藏层中节点的数量。不再将其设置为原始维度的数量(29),我们将节点数设置为 20。换句话说,这个自编码器是一个受限制的自编码器。编码器函数被迫用较少的节点捕捉输入层中的信息,解码器则必须将这个新的表示用于重构原始矩阵。

我们应该预期这里的损失比完整自编码器的损失更高。让我们运行代码。我们将执行 10 次独立运行,以测试各种欠完整自编码器在捕捉欺诈方面的表现:

# 10 runs - We will capture mean of average precision
test_scores = []
for i in range(0,10):
    # Call neural network API
    model = Sequential()

    # Apply linear activation function to input layer
    # Generate hidden layer with 20 nodes
    model.add(Dense(units=20, activation='linear',input_dim=29))

    # Apply linear activation function to hidden layer
    # Generate output layer with 29 nodes
    model.add(Dense(units=29, activation='linear'))

    # Compile the model
    model.compile(optimizer='adam',
                  loss='mean_squared_error',
                  metrics=['accuracy'])

    # Train the model
    num_epochs = 10
    batch_size = 32

    history = model.fit(x=X_train_AE, y=X_train_AE,
                        epochs=num_epochs,
                        batch_size=batch_size,
                        shuffle=True,
                        validation_data=(X_train_AE, X_train_AE),
                        verbose=1)

    # Evaluate on test set
    predictions = model.predict(X_test, verbose=1)
    anomalyScoresAE = anomalyScores(X_test, predictions)
    preds, avgPrecision = plotResults(y_test, anomalyScoresAE, True)
    test_scores.append(avgPrecision)

print("Mean average precision over 10 runs: ", np.mean(test_scores))
test_scores

如下所示,欠完整自编码器的损失远高于完整自编码器的损失。显然,自编码器学习了一个比原始输入矩阵更加新颖和受限制的表示——自编码器并非简单地记忆输入:

Training history of undercomplete autoencoder with 20 nodes

Train on 190820 samples, validate on 190820 samples
Epoch 1/10
190820/190820 [==============================] - 28s 145us/step - loss: 0.3588
- acc: 0.5672 - val_loss: 0.2789 - val_acc: 0.6078
Epoch 2/10
190820/190820 [==============================] - 29s 153us/step - loss: 0.2817
- acc: 0.6032 - val_loss: 0.2757 - val_acc: 0.6115
Epoch 3/10
190820/190820 [==============================] - 28s 147us/step - loss: 0.2793
- acc: 0.6147 - val_loss: 0.2755 - val_acc: 0.6176
Epoch 4/10
190820/190820 [==============================] - 30s 155us/step - loss: 0.2784
- acc: 0.6164 - val_loss: 0.2750 - val_acc: 0.6167
Epoch 5/10
190820/190820 [==============================] - 29s 152us/step - loss: 0.2786
- acc: 0.6188 - val_loss: 0.2746 - val_acc: 0.6126
Epoch 6/10
190820/190820 [==============================] - 29s 151us/step - loss: 0.2776
- acc: 0.6140 - val_loss: 0.2752 - val_acc: 0.6043
Epoch 7/10
190820/190820 [==============================] - 30s 156us/step - loss: 0.2775
- acc: 0.5947 - val_loss: 0.2745 - val_acc: 0.5946
Epoch 8/10
190820/190820 [==============================] - 29s 149us/step - loss: 0.2770
- acc: 0.5903 - val_loss: 0.2740 - val_acc: 0.5882
Epoch 9/10
190820/190820 [==============================] - 29s 153us/step - loss: 0.2768
- acc: 0.5921 - val_loss: 0.2770 - val_acc: 0.5801
Epoch 10/10
190820/190820 [==============================] - 29s 150us/step - loss: 0.2767
- acc: 0.5803 - val_loss: 0.2744 - val_acc: 0.5743
93987/93987[==============================] - 3s 36us/step

这是自编码器应该工作的方式——它应该学习一个新的表示。图 8-2 显示了这种新表示在识别欺诈方面的有效性。

使用 20 个节点的欠完全自编码器的评估指标

图 8-2. 使用 20 个节点的欠完全自编码器的评估指标

平均精度为 0.29,与完全自编码器的类似。

下面的代码显示了 10 次运行中平均精度的分布。平均精度的均值为 0.31,但离散度非常小(如 0.03 的离散系数所示)。这比使用完全自编码器设计的系统稳定得多。

Mean average precision over 10 runs: 0.30913783987972737
Coefficient of variation over 10 runs: 0.032251659812254876

[0.2886910204920736,
0.3056142045082387,
0.31658073591381186,
0.30590858583039254,
0.31824197682595556,
0.3136952374067599,
0.30888135217515555,
0.31234000424933206,
0.29695149753706923,
0.3244746838584846]

但我们仍然陷入相当平庸的平均精度。为什么欠完全自编码器表现不佳呢?可能是因为这个欠完全自编码器节点不够。或者,我们可能需要使用更多隐藏层进行训练。让我们逐个尝试这两种变化。

增加节点数量

下面的代码显示了使用 27 个节点的两层欠完全自编码器的训练损失:

Training history of undercomplete autoencoder with 27 nodes

Train on 190820 samples, validate on 190820 samples

Epoch 1/10
190820/190820 [==============================] - 29s 150us/step - loss: 0.1169
- acc: 0.8224 - val_loss: 0.0368 - val_acc: 0.8798
Epoch 2/10
190820/190820 [==============================] - 29s 154us/step - loss: 0.0388
- acc: 0.8610 - val_loss: 0.0360 - val_acc: 0.8530
Epoch 3/10
190820/190820 [==============================] - 30s 156us/step - loss: 0.0382
- acc: 0.8680 - val_loss: 0.0359 - val_acc: 0.8745
Epoch 4/10
190820/190820 [==============================] - 30s 156us/step - loss: 0.0371
- acc: 0.8811 - val_loss: 0.0353 - val_acc: 0.9021
Epoch 5/10
190820/190820 [==============================] - 30s 155us/step - loss: 0.0373
- acc: 0.9114 - val_loss: 0.0352 - val_acc: 0.9226
Epoch 6/10
190820/190820 [==============================] - 30s 155us/step - loss: 0.0377
- acc: 0.9361 - val_loss: 0.0370 - val_acc: 0.9416
Epoch 7/10
190820/190820 [==============================] - 30s 156us/step - loss: 0.0361
- acc: 0.9448 - val_loss: 0.0358 - val_acc: 0.9378
Epoch 8/10
190820/190820 [==============================] - 30s 156us/step - loss: 0.0354
- acc: 0.9521 - val_loss: 0.0350 - val_acc: 0.9503
Epoch 9/10
190820/190820 [==============================] - 29s 153us/step - loss: 0.0352
- acc: 0.9613 - val_loss: 0.0349 - val_acc: 0.9263
Epoch 10/10
190820/190820 [==============================] - 29s 153us/step - loss: 0.0353
- acc: 0.9566 - val_loss: 0.0343 - val_acc: 0.9477
93987/93987[==============================] - 4s 39us/step

图 8-3 展示了平均精度、精确率-召回率曲线和 auROC 曲线。

使用 27 个节点的欠完全自编码器的评估指标

图 8-3. 使用 27 个节点的欠完全自编码器的评估指标

平均精度显著提高至 0.70. 这比完全自编码器的平均精度更好,也比第四章中最佳的无监督学习解决方案更好。

下面的代码总结了 10 次运行中平均精度的分布。平均精度的均值为 0.53,比之前的约 0.30 平均精度好得多。平均精度的离散度也相当好,离散系数为 0.50。

Mean average precision over 10 runs: 0.5273341559141779
Coefficient of variation over 10 runs: 0.5006880691999009

[0.689799495450694,
0.7092146840717755,
0.7336692377321005,
0.6154173765950426,
0.7068800243349335,
0.35250757724667586,
0.6904117414832501,
0.02335388808244066,
0.690798140588336,
0.061289393556529626]

我们在先前基于自编码器的异常检测系统上有了明显改进。

添加更多隐藏层

让我们看看通过向自编码器添加额外的隐藏层是否可以改善我们的结果。目前我们将继续使用线性激活函数。

注意

实验是发现解决问题的最佳神经网络架构的重要组成部分。您所做的一些更改会带来更好的结果,而另一些则会带来更糟糕的结果。了解如何在搜索过程中修改神经网络和超参数以改进解决方案是非常重要的。

我们将不再使用 27 个节点的单隐藏层,而是使用一个 28 个节点的隐藏层和一个 27 个节点的隐藏层。这只是与先前使用的稍微不同。由于我们有两个隐藏层加上输出层,所以现在是一个三层神经网络。输入层不算在这个数目中。

这个额外的隐藏层只需要添加一行代码,如下所示:

# Model two
# Three layer undercomplete autoencoder with linear activation
# With 28 and 27 nodes in the two hidden layers, respectively

model = Sequential()
model.add(Dense(units=28, activation='linear',input_dim=29))
model.add(Dense(units=27, activation='linear'))
model.add(Dense(units=29, activation='linear'))

下面的代码总结了 10 次运行中平均精度的分布。平均精度的平均值为 0.36,比刚刚取得的 0.53 还要差。平均精度的离散度也更差,变异系数为 0.94(越高越差):

Mean average precision over 10 runs: 0.36075271075596366
Coefficient of variation over 10 runs: 0.9361649046827353

[0.02259626054852924,
0.6984699403560997,
0.011035001202665167,
0.06621450000830197,
0.008916986608776182,
0.705399684020873,
0.6995233144849828,
0.008263068338243631,
0.6904537524978872,
0.6966545994932775]

非线性自编码器

现在让我们使用非线性激活函数来构建一个欠完备自编码器。我们将使用 ReLu,但您也可以尝试 tanh、sigmoid 和其他非线性激活函数。

我们将包含三个隐藏层,分别有 27、22 和 27 个节点。在概念上,前两个激活函数(应用于输入和第一个隐藏层)执行编码,创建具有 22 个节点的第二个隐藏层。然后,接下来的两个激活函数执行解码,将 22 节点的表示重构为原始维度的数量,即 29:

model = Sequential()
model.add(Dense(units=27, activation='relu',input_dim=29))
model.add(Dense(units=22, activation='relu'))
model.add(Dense(units=27, activation='relu'))
model.add(Dense(units=29, activation='relu'))

下面的代码显示了这个自编码器的损失,而图 8-4 显示了平均精度、精确率-召回率曲线和 auROC 曲线:

Training history of undercomplete autoencoder with three hidden layers and ReLu
activation function

Train on 190820 samples, validate on 190820 samples

Epoch 1/10
190820/190820 [==============================] - 32s 169us/step - loss: 0.7010
- acc: 0.5626 - val_loss: 0.6339 - val_acc: 0.6983
Epoch 2/10
190820/190820 [==============================] - 33s 174us/step - loss: 0.6302
- acc: 0.7132 - val_loss: 0.6219 - val_acc: 0.7465
Epoch 3/10
190820/190820 [==============================] - 34s 177us/step - loss: 0.6224
- acc: 0.7367 - val_loss: 0.6198 - val_acc: 0.7528
Epoch 4/10
190820/190820 [==============================] - 34s 179us/step - loss: 0.6227
- acc: 0.7380 - val_loss: 0.6205 - val_acc: 0.7471
Epoch 5/10
190820/190820 [==============================] - 33s 174us/step - loss: 0.6206
- acc: 0.7452 - val_loss: 0.6202 - val_acc: 0.7353
Epoch 6/10
190820/190820 [==============================] - 33s 175us/step - loss: 0.6206
- acc: 0.7458 - val_loss: 0.6192 - val_acc: 0.7485
Epoch 7/10
190820/190820 [==============================] - 33s 174us/step - loss: 0.6199
- acc: 0.7481 - val_loss: 0.6239 - val_acc: 0.7308
Epoch 8/10
190820/190820 [==============================] - 33s 175us/step - loss: 0.6203
- acc: 0.7497 - val_loss: 0.6183 - val_acc: 0.7626
Epoch 9/10
190820/190820 [==============================] - 34s 177us/step - loss: 0.6197
- acc: 0.7491 - val_loss: 0.6188 - val_acc: 0.7531
Epoch 10/10
190820/190820 [==============================] - 34s 177us/step - loss: 0.6201
- acc: 0.7486 - val_loss: 0.6188 - val_acc: 0.7540
93987/93987 [==============================] - 5s 48 us/step

三层隐藏层和 ReLu 激活函数下的欠完备自编码器评估指标

图 8-4. 三层隐藏层和 ReLu 激活函数下的欠完备自编码器评估指标

结果显著更差。

下面的代码总结了 10 次运行中平均精度的分布。平均精度的平均值为 0.22,比之前的 0.53 要差。平均精度的离散度非常小,变异系数为 0.06:

Mean average precision over 10 runs:    0.2232934196381843
Coefficient of variation over 10 runs:   0.060779960264380296

[0.22598829389665595,
0.22616147166925166,
0.22119489753135715,
0.2478548473814437,
0.2251289336369011,
0.2119454446242229,
0.2126914064768752,
0.24581338950742185,
0.20665608837737512,
0.20949942328033827]

这些结果比使用线性激活函数的简单自编码器要糟糕得多。也许对于这个数据集来说,一个线性的、欠完备的自编码器是最佳解决方案。

对于其他数据集,情况可能并非总是如此。和往常一样,需要进行实验以找到最优解。改变节点数、隐藏层数和激活函数的组合,看看解决方案变得更好或更差了多少。

这种类型的实验被称为超参数优化。您正在调整超参数——节点数、隐藏层数和激活函数的组合,以寻找最优解。

具有线性激活的过完备自编码器

现在让我们来强调一下过完备自编码器的问题。过完备自编码器的隐藏层中的节点数比输入层或输出层都要多。由于神经网络模型的容量非常高,自编码器只是简单地记忆训练过的观测结果。

换句话说,自编码器学习了恒等函数,这正是我们想要避免的。自编码器会对训练数据过拟合,并且在区分欺诈信用卡交易和正常交易方面表现非常差。

记住,我们需要自编码器在训练集中学习信用卡交易的显著特征,这样它才能学习到正常交易的样子,而不是死记硬背不太正常和稀少的欺诈交易的信息。

只有当自编码器能够丢失一些训练集中的信息时,它才能够分离欺诈交易和正常交易:

model = Sequential()
model.add(Dense(units=40, activation='linear',input_dim=29))
model.add(Dense(units=29, activation='linear'))

下面的代码显示了这个自编码器的损失,并且图 8-6 显示了平均精度、精确率-召回率曲线和 auROC 曲线:

Training history of overcomplete autoencoder with single hidden layer and
 linear activation function

Train on 190820 samples, validate on 190820 samples
Epoch 1/10
190820/190820 [==============================] - 31s 161us/step - loss: 0.0498
- acc: 0.9438 - val_loss: 9.2301e-06 - val_acc: 0.9982
Epoch 2/10
190820/190820 [==============================] - 33s 171us/step - loss: 0.0014
- acc: 0.9925 - val_loss: 0.0019 - val_acc: 0.9909
Epoch 3/10
190820/190820 [==============================] - 33s 172us/step - loss: 7.6469
e-04 - acc: 0.9947 - val_loss: 4.5314e-05 - val_acc: 0.9970
Epoch 4/10
190820/190820 [==============================] - 35s 182us/step - loss: 0.0010
- acc: 0.9930 - val_loss: 0.0039 - val_acc: 0.9859
Epoch 5/10
190820/190820 [==============================] - 32s 166us/step - loss: 0.0012
- acc: 0.9924 - val_loss: 8.5141e-04 - val_acc: 0.9886
Epoch 6/10
190820/190820 [==============================] - 31s 163us/step - loss: 5.0655
e-04 - acc: 0.9955 - val_loss: 8.2359e-04 - val_acc: 0.9910
Epoch 7/10
190820/190820 [==============================] - 30s 156us/step - loss: 7.6046
e-04 - acc: 0.9930 - val_loss: 0.0045 - val_acc: 0.9933
Epoch 8/10
190820/190820 [==============================] - 30s 157us/step - loss: 9.1609
e-04 - acc: 0.9930 - val_loss: 7.3662e-04 - val_acc: 0.9872
Epoch 9/10
190820/190820 [==============================] - 30s 158us/step - loss: 7.6287
e-04 - acc: 0.9929 - val_loss: 2.5671e-04 - val_acc: 0.9940
Epoch 10/10
190820/190820 [==============================] - 30s 157us/step - loss: 7.0697
e-04 - acc: 0.9928 - val_loss: 4.5272e-06 - val_acc: 0.9994
93987/93987[==============================] - 4s 48us/step

单隐藏层和线性激活函数的过度完备自编码器的评估指标

图 8-5. 单隐藏层、线性激活函数的过度完备自编码器的评估指标

如预期的那样,损失非常低,而且过度完备的自编码器在检测欺诈信用卡交易方面表现非常糟糕。

下面的代码总结了 10 次运行中平均精度的分布。平均精度的均值为 0.31,比我们之前实现的 0.53 要差。平均精度的离散度不是很紧,变异系数为 0.89:

Mean average precision over 10 runs: 0.3061984081568074
Coefficient of variation over 10 runs: 0.8896921668864564

[0.03394897465567298,
0.14322827274920255,
0.03610123178524601,
0.019735235731640446,
0.012571999125881402,
0.6788921569665146,
0.5411349583727725,
0.388474572258503,
0.7089617645810736,
0.4989349153415674]

使用线性激活和丢弃的过度完备自编码器

改进过度完备自编码器解决方案的一种方法是使用正则化技术来减少过拟合。其中一种技术被称为丢弃。使用丢弃时,我们强制自编码器从神经网络中的层中丢弃一定百分比的单元。

有了这个新的约束条件,过度完备自编码器就不能简单地记住训练集中的信用卡交易了。相反,自编码器必须更多地进行泛化。自编码器被迫学习数据集中更显著的特征,并丢失一些不太显著的信息。

我们将使用 10%的丢弃率,将其应用于隐藏层。换句话说,10%的神经元会被丢弃。丢弃率越高,正则化效果越强。这只需要一行额外的代码。

让我们看看这是否能改善结果:

model = Sequential()
model.add(Dense(units=40, activation='linear', input_dim=29))
model.add(Dropout(0.10))
model.add(Dense(units=29, activation='linear'))

下面的代码显示了这个自编码器的损失,并且图 8-6 显示了平均精度、精确率-召回率曲线和 auROC 曲线:

Training history of overcomplete autoencoder with single hidden layer,
dropout, and linear activation function

Train on 190820 samples, validate on 190820 samples
Epoch 1/10
190820/190820 [==============================] - 27s 141us/step - loss: 0.1358
- acc: 0.7430 - val_loss: 0.0082 - val_acc: 0.9742
Epoch 2/10
190820/190820 [==============================] - 28s 146us/step - loss: 0.0782
- acc: 0.7849 - val_loss: 0.0094 - val_acc: 0.9689
Epoch 3/10
190820/190820 [==============================] - 28s 149us/step - loss: 0.0753
- acc: 0.7858 - val_loss: 0.0102 - val_acc: 0.9672
Epoch 4/10
190820/190820 [==============================] - 28s 148us/step - loss: 0.0772
- acc: 0.7864 - val_loss: 0.0093 - val_acc: 0.9677
Epoch 5/10
190820/190820 [==============================] - 28s 147us/step - loss: 0.0813
- acc: 0.7843 - val_loss: 0.0108 - val_acc: 0.9631
Epoch 6/10
190820/190820 [==============================] - 28s 149us/step - loss: 0.0756
- acc: 0.7844 - val_loss: 0.0095 - val_acc: 0.9654
Epoch 7/10
190820/190820 [==============================] - 29s 150us/step - loss: 0.0743
- acc: 0.7850 - val_loss: 0.0077 - val_acc: 0.9768
Epoch 8/10
190820/190820 [==============================] - 29s 150us/step - loss: 0.0767
- acc: 0.7840 - val_loss: 0.0070 - val_acc: 0.9759
Epoch 9/10
190820/190820 [==============================] - 29s 150us/step - loss: 0.0762
- acc: 0.7851 - val_loss: 0.0072 - val_acc: 0.9733
Epoch 10/10
190820/190820 [==============================] - 29s 151us/step - loss: 0.0756
- acc: 0.7849 - val_loss: 0.0067 - val_acc: 0.9749
93987/93987 [==============================] - 3s 32us/step

单隐藏层、丢弃和线性激活函数的过度完备自编码器的评估指标

图 8-6. 具有单隐藏层、丢弃率和线性激活函数的过完备自编码器的评估指标

如预期的那样,损失非常低,而且过拟合的过完备自编码器在检测欺诈信用卡交易方面表现非常差。

以下代码总结了在 10 次运行中平均精度的分布。平均精度的均值为 0.21,比我们之前达到的 0.53 差。变异系数为 0.40:

Mean average precision over 10 runs: 0.21150415381770646
Coefficient of variation over 10 runs: 0.40295807771579256

[0.22549974304927337,
0.22451178120391296,
0.17243952488912334,
0.2533716906936315,
0.13251890273915556,
0.1775116247503748,
0.4343283958332979,
0.10469065867732033,
0.19480068075466764,
0.19537213558630712]

具有线性激活的稀疏过完备自编码器

另一种正则化技术是稀疏性。我们可以强制自编码器考虑矩阵的稀疏性,使得大多数自编码器的神经元大部分时间处于非活跃状态——换句话说,它们不会激活。这使得即使自编码器是过完备的,也更难记忆恒等函数,因为大多数节点无法激活,因此不能像以前那样轻易地过拟合观察结果。

我们将使用与之前相同的单隐藏层过完备自编码器,有 40 个节点,但只有稀疏性惩罚,而没有丢弃。

让我们看看结果是否从之前的 0.21 平均精度有所提高:

model = Sequential()
    model.add(Dense(units=40, activation='linear',  \
        activity_regularizer=regularizers.l1(10e-5), input_dim=29))
model.add(Dense(units=29, activation='linear'))

以下代码显示了这个自编码器的损失,而图 8-7 则展示了平均精度、精确-召回曲线和 auROC 曲线:

Training history of sparse overcomplete autoencoder with single hidden layer
and linear activation function

Train on 190820 samples, validate on 190820 samples
Epoch 1/10
190820/190820 [==============================] - 27s 142us/step - loss: 0.0985
- acc: 0.9380 - val_loss: 0.0369 - val_acc: 0.9871
Epoch 2/10
190820/190820 [==============================] - 26s 136us/step - loss: 0.0284
- acc: 0.9829 - val_loss: 0.0261 - val_acc: 0.9698
Epoch 3/10
190820/190820 [==============================] - 26s 136us/step - loss: 0.0229
- acc: 0.9816 - val_loss: 0.0169 - val_acc: 0.9952
Epoch 4/10
190820/190820 [==============================] - 26s 137us/step - loss: 0.0201
- acc: 0.9821 - val_loss: 0.0147 - val_acc: 0.9943
Epoch 5/10
190820/190820 [==============================] - 26s 137us/step - loss: 0.0183
- acc: 0.9810 - val_loss: 0.0142 - val_acc: 0.9842
Epoch 6/10
190820/190820 [==============================] - 26s 137us/step - loss: 0.0206
- acc: 0.9774 - val_loss: 0.0158 - val_acc: 0.9906
Epoch 7/10
190820/190820 [==============================] - 26s 136us/step - loss: 0.0169
- acc: 0.9816 - val_loss: 0.0124 - val_acc: 0.9866
Epoch 8/10
190820/190820 [==============================] - 26s 137us/step - loss: 0.0165
- acc: 0.9795 - val_loss: 0.0208 - val_acc: 0.9537
Epoch 9/10
190820/190820 [==============================] - 26s 136us/step - loss: 0.0164
- acc: 0.9801 - val_loss: 0.0105 - val_acc: 0.9965
Epoch 10/10
190820/190820 [==============================] - 27s 140us/step - loss: 0.0167
- acc: 0.9779 - val_loss: 0.0102 - val_acc: 0.9955
93987/93987 [==============================] - 3s 32us/step

具有单隐藏层和线性激活函数的稀疏过完备自编码器的评估指标

图 8-7. 具有单隐藏层和线性激活函数的稀疏过完备自编码器的评估指标

以下代码总结了在 10 次运行中平均精度的分布。平均精度的均值为 0.21,比我们之前达到的 0.53 差。变异系数为 0.99:

Mean average precision over 10 runs: 0.21373659011504448
Coefficient of variation over 10 runs: 0.9913040763536749

[0.1370972172100049,
0.28328895710699215,
0.6362677613798704,
0.3467265637372019,
0.5197889253491589,
0.01871495737323161,
0.0812609121251577,
0.034749761900336684,
0.04846036143317335,
0.031010483535317393]

具有线性激活和丢弃的稀疏过完备自编码器

当然,我们可以结合正则化技术来改善解决方案。这里是一个具有线性激活、单隐藏层中有 40 个节点和 5% 丢弃率的稀疏过完备自编码器:

model = Sequential()
    model.add(Dense(units=40, activation='linear',  \
        activity_regularizer=regularizers.l1(10e-5), input_dim=29))
    model.add(Dropout(0.05))
model.add(Dense(units=29, activation='linear'))

以下训练数据显示了这个自编码器的损失,而图 8-8 则展示了平均精度、精确-召回曲线和 auROC 曲线:

Training history of sparse overcomplete autoencoder with single hidden layer,
dropout, and linear activation function

Train on 190820 samples, validate on 190820 samples
Epoch 1/10
190820/190820 [==============================] - 31s 162us/step - loss: 0.1477
- acc: 0.8150 - val_loss: 0.0506 - val_acc: 0.9727
Epoch 2/10
190820/190820 [==============================] - 29s 154us/step - loss: 0.0756
- acc: 0.8625 - val_loss: 0.0344 - val_acc: 0.9788
Epoch 3/10
190820/190820 [==============================] - 29s 152us/step - loss: 0.0687
- acc: 0.8612 - val_loss: 0.0291 - val_acc: 0.9790
Epoch 4/10
190820/190820 [==============================] - 29s 154us/step - loss: 0.0644
- acc: 0.8606 - val_loss: 0.0274 - val_acc: 0.9734
Epoch 5/10
190820/190820 [==============================] - 31s 163us/step - loss: 0.0630
- acc: 0.8597 - val_loss: 0.0242 - val_acc: 0.9746
Epoch 6/10
190820/190820 [==============================] - 31s 162us/step - loss: 0.0609
- acc: 0.8600 - val_loss: 0.0220 - val_acc: 0.9800
Epoch 7/10
190820/190820 [==============================] - 30s 156us/step - loss: 0.0624
- acc: 0.8581 - val_loss: 0.0289 - val_acc: 0.9633
Epoch 8/10
190820/190820 [==============================] - 29s 154us/step - loss: 0.0589
- acc: 0.8588 - val_loss: 0.0574 - val_acc: 0.9366
Epoch 9/10
190820/190820 [==============================] - 29s 154us/step - loss: 0.0596
- acc: 0.8571 - val_loss: 0.0206 - val_acc: 0.9752
Epoch 10/10
190820/190820 [==============================] - 31s 165us/step - loss: 0.0593
- acc: 0.8590 - val_loss: 0.0204 - val_acc: 0.9808
93987/93987 [==============================] - 4s 38us/step

具有单隐藏层、丢弃率和线性激活函数的稀疏过完备自编码器的评估指标

图 8-8. 具有单隐藏层、丢弃率和线性激活函数的稀疏过完备自编码器的评估指标

以下代码总结了在 10 次运行中平均精度的分布。平均精度的均值为 0.24,比我们之前达到的 0.53 差。变异系数为 0.62:

Mean average precision over 10 runs: 0.2426994231628755
Coefifcient of variation over 10 runs: 0.6153219870606188

[0.6078198313533932,
0.20862366991302814,
0.25854513247057875,
0.08496595007072019,
0.26313491674585093,
0.17001322998258625,
0.15338215561753896,
0.1439107390306835,
0.4073422280287587,
0.1292563784156162]

处理嘈杂数据集

实际数据的一个常见问题是数据的嘈杂性,数据通常因为数据捕获、数据迁移、数据转换等问题而畸变。我们需要自编码器足够健壮,以便不被这种噪声所迷惑,并能够从数据中学习到真正重要的潜在结构。

为了模拟这种噪声,让我们向我们的信用卡交易数据集添加一个高斯随机噪声矩阵,然后在这个嘈杂的训练集上训练一个自编码器。然后,我们将看看这个自编码器在嘈杂的测试集上预测欺诈交易的表现:

noise_factor = 0.50
X_train_AE_noisy = X_train_AE.copy() + noise_factor * \
 np.random.normal(loc=0.0, scale=1.0, size=X_train_AE.shape)
X_test_AE_noisy = X_test_AE.copy() + noise_factor * \
 np.random.normal(loc=0.0, scale=1.0, size=X_test_AE.shape)

去噪自编码器

与原始的非失真数据集相比,对信用卡交易嘈杂数据集的过拟合惩罚要高得多。数据集中有足够的噪声,以至于一个对噪声数据拟合得太好的自编码器很难从正常交易和欺诈交易中检测出欺诈交易。

这应该是有道理的。我们需要一个自编码器,它能够很好地适应数据,以便能够足够好地重构大部分观测值,但又不能够过于好,以至于意外地重构了噪音。换句话说,我们希望自编码器能够学习到潜在的结构,但忽略数据中的噪音。

让我们从到目前为止表现良好的选项中尝试几个。首先,我们将尝试一个单隐藏层、27 节点的欠完全自编码器,采用线性激活。接下来,我们将尝试一个单隐藏层、40 节点的稀疏过完备自编码器,带有 dropout。最后,我们将使用一个带有非线性激活函数的自编码器。

两层去噪欠完全自编码器,采用线性激活

在嘈杂的数据集上,具有线性激活和 27 个节点的单隐藏层自编码器的平均精度为 0.69。让我们看看它在嘈杂的数据集上表现如何。这种自编码器——因为它正在处理一个嘈杂的数据集并试图去噪它——被称为去噪自编码器

代码与之前类似,只是现在我们将其应用于嘈杂的训练和测试数据集X_train_AE_noisyX_test_AE_noisy

for i in range(0,10):
    # Call neural network API
    model = Sequential()

    # Generate hidden layer with 27 nodes using linear activation
    model.add(Dense(units=27, activation='linear', input_dim=29))

    # Generate output layer with 29 nodes
    model.add(Dense(units=29, activation='linear'))

    # Compile the model
    model.compile(optimizer='adam',
                  loss='mean_squared_error',
                  metrics=['accuracy'])

    # Train the model
    num_epochs = 10
    batch_size = 32

    history = model.fit(x=X_train_AE_noisy, y=X_train_AE_noisy,
                        epochs=num_epochs,
                        batch_size=batch_size,
                        shuffle=True,
                        validation_data=(X_train_AE, X_train_AE),
                        verbose=1)

    # Evaluate on test set
    predictions = model.predict(X_test_AE_noisy, verbose=1)
    anomalyScoresAE = anomalyScores(X_test, predictions)
    preds, avgPrecision = plotResults(y_test, anomalyScoresAE, True)
    test_scores.append(avgPrecision)
    model.reset_states()

print("Mean average precision over 10 runs: ", np.mean(test_scores))
test_scores

以下训练数据显示了这个自编码器的损失,而图 8-9 展示了平均精度、精确率-召回率曲线和 auROC 曲线:

Training history of denoising undercomplete autoencoder with single hidden layer
and linear activation function

Train on 190820 samples, validate on 190820 samples
Epoch 1/10
190820/190820 [==============================] - 25s 133us/step - loss: 0.1733
- acc: 0.7756 - val_loss: 0.0356 - val_acc: 0.9123
Epoch 2/10
190820/190820 [==============================] - 24s 126us/step - loss: 0.0546
- acc: 0.8793 - val_loss: 0.0354 - val_acc: 0.8973
Epoch 3/10
190820/190820 [==============================] - 24s 126us/step - loss: 0.0531
- acc: 0.8764 - val_loss: 0.0350 - val_acc: 0.9399
Epoch 4/10
190820/190820 [==============================] - 24s 126us/step - loss: 0.0525
- acc: 0.8879 - val_loss: 0.0342 - val_acc: 0.9573
Epoch 5/10
190820/190820 [==============================] - 24s 126us/step - loss: 0.0530
- acc: 0.8910 - val_loss: 0.0347 - val_acc: 0.9503
Epoch 6/10
190820/190820 [==============================] - 24s 126us/step - loss: 0.0524
- acc: 0.8889 - val_loss: 0.0350 - val_acc: 0.9138
Epoch 7/10
190820/190820 [==============================] - 24s 126us/step - loss: 0.0531
- acc: 0.8845 - val_loss: 0.0343 - val_acc: 0.9280
Epoch 8/10
190820/190820 [==============================] - 24s 126us/step - loss: 0.0530
- acc: 0.8798 - val_loss: 0.0339 - val_acc: 0.9507
Epoch 9/10
190820/190820 [==============================] - 24s 126us/step - loss: 0.0526
- acc: 0.8877 - val_loss: 0.0337 - val_acc: 0.9611
Epoch 10/10
190820/190820 [==============================] - 24s 127us/step - loss: 0.0528
- acc: 0.8885 - val_loss: 0.0352 - val_acc: 0.9474
93987/93987 [==============================] - 3s 34us/step

图 8-9。去噪欠完全自编码器的评估指标,采用单隐藏层和线性激活函数

图 8-9。去噪欠完全自编码器的评估指标,采用单隐藏层和线性激活函数

平均精度现在为 0.28。您可以看出,线性自编码器在去噪这个嘈杂的数据集上是多么困难:

Mean average precision over 10 runs: 0.2825997155005206
Coeficient of variation over 10 runs: 1.1765416185187383

[0.6929639885685303,
0.008450118408150287,
0.6970753417267612,
0.011820311633718597,
0.008924124892696377,
0.010639537507746342,
0.6884911855668772,
0.006549332886020607,
0.6805304226634528,
0.02055279115125298]

它在将数据中真实的潜在结构与我们添加的高斯噪声分离方面存在困难。

具有线性激活函数的两层降噪过完备自编码器

现在让我们尝试一个单隐藏层过完备自编码器,有 40 个节点,稀疏性正则化器,以及 0.05%的 Dropout。

在原始数据集上,这个模型的平均精度为 0.56:

model = Sequential()
model.add(Dense(units=40, activation='linear',
 activity_regularizer=regularizers.l1(10e-5),
                input_dim=29))
model.add(Dropout(0.05))
model.add(Dense(units=29, activation='linear'))

以下训练数据显示了该自编码器的损失,而图 8-10 展示了平均精度、精确率-召回率曲线和 auROC 曲线:

Training history of denoising overcomplete autoencoder with dropout and linear
activation function

Train on 190820 samples, validate on 190820 samples
Epoch 1/10
190820/190820 [==============================] - 28s 145us/step - loss: 0.1726
- acc: 0.8035 - val_loss: 0.0432 - val_acc: 0.9781
Epoch 2/10
190820/190820 [==============================] - 26s 138us/step - loss: 0.0868
- acc: 0.8490 - val_loss: 0.0307 - val_acc: 0.9775
Epoch 3/10
190820/190820 [==============================] - 26s 138us/step - loss: 0.0809
- acc: 0.8455 - val_loss: 0.0445 - val_acc: 0.9535
Epoch 4/10
190820/190820 [==============================] - 26s 138us/step - loss: 0.0777
- acc: 0.8438 - val_loss: 0.0257 - val_acc: 0.9709
Epoch 5/10
190820/190820 [==============================] - 27s 139us/step - loss: 0.0748
- acc: 0.8434 - val_loss: 0.0219 - val_acc: 0.9787
Epoch 6/10
190820/190820 [==============================] - 26s 138us/step - loss: 0.0746
- acc: 0.8425 - val_loss: 0.0210 - val_acc: 0.9794
Epoch 7/10
190820/190820 [==============================] - 26s 138us/step - loss: 0.0713
- acc: 0.8437 - val_loss: 0.0294 - val_acc: 0.9503
Epoch 8/10
190820/190820 [==============================] - 26s 138us/step - loss: 0.0708
- acc: 0.8426 - val_loss: 0.0276 - val_acc: 0.9606
Epoch 9/10
190820/190820 [==============================] - 26s 139us/step - loss: 0.0704
- acc: 0.8428 - val_loss: 0.0180 - val_acc: 0.9811
Epoch 10/10
190820/190820 [==============================] - 27s 139us/step - loss: 0.0702
- acc: 0.8424 - val_loss: 0.0185 - val_acc: 0.9710
93987/93987 [==============================] - 4s 38us/step

使用 Dropout 和线性激活函数的降噪过完备自编码器的评估指标

图 8-10. 使用 Dropout 和线性激活函数的降噪过完备自编码器的评估指标

以下代码总结了 10 次运行中平均精度的分布情况。平均精度的均值为 0.10,比我们之前达到的 0.53 差。变异系数为 0.83:

Mean average precision over 10 runs: 0.10112931070692295
Coefficient of variation over 10 runs: 0.8343774832756188

[0.08283546387140524,
0.043070120657586454,
0.018901753737287603,
0.02381040174486509,
0.16038446580196433,
0.03461061251209459,
0.17847771715513427,
0.2483282420447288,
0.012981344347664117,
0.20789298519649893]

具有 ReLU 激活的两层降噪过完备自编码器

最后,让我们看看同一个自编码器使用 ReLU 作为激活函数而不是线性激活函数时的表现。回想一下,非线性激活函数的自编码器在原始数据集上的表现不如线性激活函数的表现:

model = Sequential()
    model.add(Dense(units=40, activation='relu',  \
        activity_regularizer=regularizers.l1(10e-5), input_dim=29))
    model.add(Dropout(0.05))
model.add(Dense(units=29, activation='relu'))

以下训练数据显示了该自编码器的损失,而图 8-11 展示了平均精度、精确率-召回率曲线和 auROC 曲线:

Training history of denoising overcomplete autoencoder with dropout and ReLU
activation function"

Train on 190820 samples, validate on 190820 samples
Epoch 1/10
190820/190820 [==============================] - 29s 153us/step - loss: 0.3049
- acc: 0.6454 - val_loss: 0.0841 - val_acc: 0.8873
Epoch 2/10
190820/190820 [==============================] - 27s 143us/step - loss: 0.1806
- acc: 0.7193 - val_loss: 0.0606 - val_acc: 0.9012
Epoch 3/10
190820/190820 [==============================] - 27s 143us/step - loss: 0.1626
- acc: 0.7255 - val_loss: 0.0500 - val_acc: 0.9045
Epoch 4/10
190820/190820 [==============================] - 27s 143us/step - loss: 0.1567
- acc: 0.7294 - val_loss: 0.0445 - val_acc: 0.9116
Epoch 5/10
190820/190820 [==============================] - 27s 143us/step - loss: 0.1484
- acc: 0.7309 - val_loss: 0.0433 - val_acc: 0.9136
Epoch 6/10
190820/190820 [==============================] - 27s 144us/step - loss: 0.1467
- acc: 0.7311 - val_loss: 0.0375 - val_acc: 0.9101
Epoch 7/10
190820/190820 [==============================] - 27s 143us/step - loss: 0.1427
- acc: 0.7335 - val_loss: 0.0384 - val_acc: 0.9013
Epoch 8/10
190820/190820 [==============================] - 27s 143us/step - loss: 0.1397
- acc: 0.7307 - val_loss: 0.0337 - val_acc: 0.9145
Epoch 9/10
190820/190820 [==============================] - 27s 143us/step - loss: 0.1361
- acc: 0.7322 - val_loss: 0.0343 - val_acc: 0.9066
Epoch 10/10
190820/190820 [==============================] - 27s 144us/step - loss: 0.1349
- acc: 0.7331 - val_loss: 0.0325 - val_acc: 0.9107
93987/93987 [==============================] - 4s 41us/step

使用 Dropout 和 ReLU 激活函数的降噪过完备自编码器的评估指标

图 8-11. 使用 Dropout 和 ReLU 激活函数的降噪过完备自编码器的评估指标

以下代码总结了 10 次运行中平均精度的分布情况。平均精度的均值为 0.20,比我们之前达到的 0.53 差。变异系数为 0.55:

Mean average precision over 10 runs: 0.1969608394689088
Coefficient of variation over 10 runs: 0.5566706365802669

[0.22960316854089222,
0.37609633487223315,
0.11429775486529765,
0.10208135698072755,
0.4002384343852861,
0.13317480663248088,
0.15764518571284625,
0.2406315655171392,
0.05080529996343734,
0.1650344872187474]

您可以尝试不同的节点数、层数、稀疏度、Dropout 百分比和激活函数,看看能否改善结果。

结论

在本章中,我们回顾了本书早期提到的信用卡欺诈问题,并开发了基于神经网络的无监督欺诈检测解决方案。

为了找到我们自编码器的最优结构,我们尝试了各种自编码器。我们尝试了完备、欠完备和过完备的自编码器,有单层或几层隐藏层。我们还使用了线性和非线性激活函数,并应用了两种主要的正则化方法,稀疏性和 Dropout。

我们发现,在原始信用卡数据集上,一个相当简单的两层欠完备神经网络,使用线性激活效果最佳,但在嘈杂的信用卡数据集中,我们需要一个稀疏的两层过完备自编码器,配备线性激活和 dropout 来处理噪声。

我们的许多实验都基于试错法进行——每次实验中,我们调整了几个超参数,并将结果与先前的迭代进行比较。可能存在更好的基于自编码器的欺诈检测解决方案,我鼓励您进行自己的实验,看看您能找到什么。

至此,本书中我们将监督学习和无监督学习视为独立且不同的方法,但在第九章中,我们将探讨如何同时使用监督和无监督方法,开发一个称为半监督解决方案,其表现优于任何单独的方法。

¹ 欲了解更多关于Keras Sequential model的信息,请访问官方文档。

² 欲了解更多关于损失函数的信息,请参阅官方 Keras 文档

³ 请查阅维基百科,了解更多关于随机梯度下降的信息。

⁴ 欲了解更多有关优化器的信息,请参阅文档

⁵ 欲了解更多评估指标,请参阅文档