大家好,我是菜菜卷!今天开始陆续和大家分享一些关于异常检测入门相关的实战项目(包括使用sklearn实现一些简单的机器学习模型或者使用pytorch实现简单的深度学习模型)
今天我们使用的模型是集成于sklearn内部实现的孤立森林算法。
孤立森林是一种传统的异常检测机器学习算法,他属于无监督学习的boost 树模型,对于每棵子树来说,针对数据集的不同特征值,会随机挑选特征值取值范围内的一个随机值,然后将所有数据按照该特征的这个随机值划分为两部分(根据该特征值大于或者小于该随机值),这样我们就可以逐渐将所有的数据分开,因为异常数据通常来说所占的比例都较少,所以一般使用较少的属性就可以将他们从正常数据中区分开来,因此平均路径长度便可以作为是否是异常值的衡量指标(异常数据的平均路径长度会比正常数据更短)
数据集获取地址:KDD1999Data
前往以上的地址,下载kddcup.data.gz即可。
该数据集是由若干正常和异常的数据分类,有41和特征和1个标签构成,因为后面我们只想对网页攻击方向的数据进行分析,所以要强调一下,第三个特征是表示异常的方向(即是哪种方面的异常,比如http还是其他)
numpy 1.15.3
pandas 0.23.4
scikit-learn 0.19.1
matplotlib 2.2.2
大家可以使用pip或者conda自行安装项目环境,为保证顺利无bug复现结果,建议使用所示版本的包
首先我们先导入实验所需的第三方包
import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import roc_auc_score
其中time
可以用来记录时间和程序运行时间;numpy
可以高效做矩阵运算,在数据处理中有广泛的运用;pandas
可以快速方便的读入和处理数据的格式;matplotlib
用于部分实验结果的可视化;sklearn
是一个非常丰富的机器学习库,内部集成了基本的机器学习方法、数据集处理方法、模型衡量指标等功能。
首先我们使用以下代码来读入数据:
data_path = r'dataset/kddcup.data.corrected'
columns = [str(i) for i in range(1, 42)]
columns.append('label')
df = pd.read_csv(data_path, sep=',', names=columns, index_col=None)
其中columns
是我们给每个特征的命名,具体是什么都可以,只需要后续操作特征的时候可以对应上最开始的命名就可以了,这里我就简单的将最开始的41列命名为1-41的str类型数字,最后一列(第42列)命名为'label'
;names=columns
表示我们使用自定义的columns
来命名数据中的42个列。
下面我们使用print(df.shape)
看一下数据的大小,结果如下所示:
(4898431, 42)
说明我们的数据集有接近500w的数据,其中每个数据有42个属性(其中前41个位特征值,第42个是标签label)。
接下来我们简单的处理一下数据,假设目前我们仅关心与http
相关的数据是否正常(第3列特征描述数据的分类),我们使用如下代码来挑选出数据集中所有和http
相关的数据:
df = df[df['3'] == 'http']
df = df.drop('3', axis=1)
columns.remove('3')
其中df['3'] == 'http'
用来判断数据df
的第三个属性是否是http
,是的话就是True
,否的话就是False
,所以df = df[df['3'] == 'http']
实现的功能就是找到所有第三列属性是http
的数据,因为在找到后,我们的第三列属性就无用了(因为都是http
了),所以我们用df = df.drop('3', axis=1)
将第三列属性去掉,下面我们再使用print(df.shape)
看看数据的shape
,结果如下所示:
(623091, 41)
我们可以发现,数据从原来的接近500w变成了现在的62w个,属性值也从42变成了41,说明我们以上操作成功了!
然后我们运行print(df['label'].value_counts())
来观察一下label
的取值,结果如下所示:
normal. 619046
back. 2203
neptune. 1801
portsweep. 16
ipsweep. 13
satan. 7
phf. 4
nmap. 1
Name: label, dtype: int64
从结果中我们可以发现,大多数数据都是正常的(normal.
),但是也有一些其他异常的数据,我们本次的核心任务就是使用孤立森林模型来完成对数据正常与否的检测。
我们希望更详细的观察一些数据的情况,使用print(df.head(5))
来显示前五个数据的详细信息:
1 2 4 5 6 7 8 9 ... 35 36 37 38 39 40 41 label
0 0 tcp SF 215 45076 0 0 0 ... 0.0 0.00 0.0 0.0 0.0 0.0 0.0 normal.
1 0 tcp SF 162 4528 0 0 0 ... 0.0 1.00 0.0 0.0 0.0 0.0 0.0 normal.
2 0 tcp SF 236 1228 0 0 0 ... 0.0 0.50 0.0 0.0 0.0 0.0 0.0 normal.
3 0 tcp SF 233 2032 0 0 0 ... 0.0 0.33 0.0 0.0 0.0 0.0 0.0 normal.
4 0 tcp SF 239 486 0 0 0 ... 0.0 0.25 0.0 0.0 0.0 0.0 0.0 normal.
通过结果我们可以发现,不同属性对应的类型也是不一样的,对于1,5,6
这种int
类型的或者35,36
这种float
类型的属性可以不做处理直接使用,但是对于2
和4
这种object
类型的属性的话,是不能直接使用的。需要对其进行编码,我们使用下面的方式对其编码:
for col in df.columns:
if df[col].dtype == 'object':
encoded = LabelEncoder()
encoded.fit(df[col])
df[col] = encoded.transform(df[col])
我们遍历数据中的所有特征,如果其类型是object
的话,我们要使用sklearn
中的LabelEncoder()
来对其编码,编码后我们使用print(df.head(5))
看一下编码后的效果:
1 2 4 5 6 7 8 9 ... 35 36 37 38 39 40 41 label
0 0 0 9 215 45076 0 0 0 ... 0.0 0.00 0.0 0.0 0.0 0.0 0.0 4
1 0 0 9 162 4528 0 0 0 ... 0.0 1.00 0.0 0.0 0.0 0.0 0.0 4
2 0 0 9 236 1228 0 0 0 ... 0.0 0.50 0.0 0.0 0.0 0.0 0.0 4
3 0 0 9 233 2032 0 0 0 ... 0.0 0.33 0.0 0.0 0.0 0.0 0.0 4
4 0 0 9 239 486 0 0 0 ... 0.0 0.25 0.0 0.0 0.0 0.0 0.0 4
[5 rows x 41 columns]
从上述结果展示中我们看到:之前第2列和第4列都已经成功变成了非object
类的属性了(这种方式其实在深度学习领域有个专门的名字叫OneHot
编码,感兴趣的同学可以深入了解一下),经过以上处理后,我们基本上已经得到了想要的数据集了,下面我们将详细介绍一下如何切分数据集。
除了要通过数据得到模型以外,我们还需要测试一下他的泛化能力(也就是在未见过的数据中是否也有同样好的效果),因此我们要将得到的数据划分为训练集、验证集和测试集。
因为我们不知道原数据集中的数据是否是有顺序的,所以为了避免异常数据集中出现在某一集合中(训练集或者验证集或者测试集),所以我们使用如下代码随机打乱数据的顺序:
for _ in range(3):
df = df.iloc[np.random.permutation(len(df))]
在获得乱序的数据集后,我们要进行数据的切分,此处我们使用前50w的数据来作为训练集和测试集,使用50w之后的数据作为验证集,其代码如下:
df2 = df[:500000]
labels = df2['label']
df_validate = df[500000:]
x_val, y_val = df_validate, df_validate['label']
x_train, x_test, y_train, y_test = train_test_split(df2, labels, test_size=0.2, random_state=42)
其中train_test_split
为sklearn
内置的一种划分训练集和测试集的方式,参数test_size=0.2
表示测试集所占的比例(我们此时使用前50w数据的20%作为测试集)。
下面我们看一下划分的结果:
print('the shape of x_train, x_test, x_val is {}, {} and {}'.format(x_train.shape, x_test.shape, x_val.shape))
其结果如下:
the shape of x_train, x_test, x_val is (400000, 41), (100000, 41) and (123091, 41)
我们可以看到,其结果如我们所料,这样数据集就划分好了!!
首先我们使用以下代码来构建并训练孤立森林模型:
isolation_forest = IsolationForest(n_estimators=100, max_samples=256, contamination=0.1, random_state=42)
print("---------------------start training-------------------")
start_time = time.time()
isolation_forest.fit(x_train)
print('the training costs {}s'.format(time.time() - start_time))
print("---------------------end training-------------------")
其中time.time()
只是用来记录开始训练和训练结束的时间,从而得到训练的耗时。
模型只在训练集上表现好是远远不够的,因为可能会出现过拟合的情况,所以还需要在训练时未见过的数据上测试其泛化能力(这也是我们划分训练集验证集和测试集的原因),首先我们使用训练好的孤立森林模型对验证集数据做预测:
anomaly_scores = isolation_forest.decision_function(x_val)
anomaly_scores
即为预测结果,其是一个一维的向量,存储了每个数据的预测分数,我们先使用以下代码可视化看一下其分数的分布情况:
plt.figure(figsize=(15, 10))
plt.hist(anomaly_scores, bins=100)
plt.xlabel('Average Path Lengths', fontsize=14)
plt.xlabel('Number of Data Points', fontsize=14)
plt.show()
其结果如下所示:
我们可以看到,大多数数据的最短路径都是较长的,但是也有个别的数据路径非常短(例如小于-0.19),那么我们就有很大的把握认为路径较短的数据是异常数据,假设我们认定路径长度小于-0.19的是异常数据,那么我们使用训练好的模型看看AUC
是多少:
anomalies = anomaly_scores > -0.19
matches = y_val == list(encoded.classes_).index('normal.')
# print('the shape of anomalies and matches are', anomalies.shape, matches.shape)
auc = roc_auc_score(anomalies, matches)
print("AUC:{:.2%}".format(auc))
其中anomaly_scores > -0.19
会生成一个list,其shape
与anomaly_scores
是相同的,每个元素都是True
或者False
,表示该数据的分数是否大于-0.19
,所以anomalies
存储的是模型的预测值;
我们在处理数据的时候曾经用LabelEncoder()
来将object
类的属性转换为onehot
编码的值,因此此处的encoded实际上就是最后一个object
类的属性(即我们自定义为'label'
的最后一列特征),所以list(encoded.classes_).index('normal.')
的功能就是获得'label'
列中原来为normal.
的数据编码后的onehot
编码,所以y_val == list(encoded.classes_).index('normal.')
也可以获得一个和y_val
的shape
相同的值为True
或者False
的list
,所以matches
存储的是真实值。
之后我们再使用sklearn
自带的roc_auc_score
来计算该模型在验证集上的auc
,其结果如下所示:
AUC:98.36%
可见我们训练好的孤立森林确实可以实现异常数据的检测,而且其效果和泛化能力还挺不错。
这些大致就是异常检测中传统机器学习算法孤立森林相关的实战内容了,其完整代码如下:
import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import roc_auc_score
if __name__ == '__main__':
data_path = r'dataset/kddcup.data.corrected'
columns = [str(i) for i in range(1, 42)]
columns.append('label')
df = pd.read_csv(data_path, sep=',', names=columns, index_col=None)
# print(df.shape)
df = df[df['3'] == 'http']
df = df.drop('3', axis=1)
columns.remove('3')
# print(df.head(5))
# print(columns)
# print(df.shape)
# print(df['label'].value_counts())
for col in df.columns:
if df[col].dtype == 'object':
encoded = LabelEncoder()
encoded.fit(df[col])
df[col] = encoded.transform(df[col])
# print(df.head(5))
for _ in range(3):
df = df.iloc[np.random.permutation(len(df))]
df2 = df[:500000]
labels = df2['label']
df_validate = df[500000:]
x_val, y_val = df_validate, df_validate['label']
x_train, x_test, y_train, y_test = train_test_split(df2, labels, test_size=0.2, random_state=42)
#
# print('the shape of x_train, x_test, x_val is {}, {} and {}'.format(x_train.shape, x_test.shape, x_val.shape))
isolation_forest = IsolationForest(n_estimators=100, max_samples=256, contamination=0.1, random_state=42)
print("---------------------start training-------------------")
start_time = time.time()
isolation_forest.fit(x_train)
print('the training costs {}s'.format(time.time() - start_time))
print("---------------------end training-------------------")
anomaly_scores = isolation_forest.decision_function(x_val) # 一个一维的list,存储了每个数据的路径长度
# plt.figure(figsize=(15, 10))
# plt.hist(anomaly_scores, bins=100)
# plt.xlabel('Average Path Lengths', fontsize=14)
# plt.xlabel('Number of Data Points', fontsize=14)
# plt.show()
anomalies = anomaly_scores > -0.19
matches = y_val == list(encoded.classes_).index('normal.')
# print('the shape of anomalies and matches are', anomalies.shape, matches.shape)
auc = roc_auc_score(anomalies, matches)
print("AUC:{:.2%}".format(auc))
我是菜菜卷,我们下篇再见!