目录
序-重要
异常检测中,算法选择只是其中一环,前期最重要的是依据业务场景、业务目标来进行目标相关特征挖掘(如应用于信贷/交易欺诈,则需要着重挖掘欺诈特征)、把握数据分布、特征筛选、再依据特征分布情况选择合适的算法,另外部分业务场景还得考虑解释性;而异常检测本身是无监督算法,落地更适用于监督模型的辅助、而非独立决策。本系列会尽可能全面的覆盖到这些要点,也欢迎读者们交流讨论。
论文地址:Isolation Forest
一、原理
孤立森林(Isolation Forest)是一种基于树结构的异常检测算法,其原理可以简述为:通过随机选择特征和随机选择分割点的方式,将数据集逐步分割成较小的子集,直到每个子集只包含一个数据点,这样就形成了许多隔离的数据点,而异常点只需要经过较少的分割就可以被隔离出来,因此它们的路径长度会比正常点短,在所有树中平均路径越短的样本、异常得分越高。具体的计算过程如下:
-
构建孤立森林:设定孤立森林的树的数量和每棵树的最大深度,对于每棵树,随机选择数据集的一部分作为样本,从中随机选择特征和特征值,进行二叉树的递归分割,直到每个分支只有一个样本点或者达到树深阈值。
-
计算路径长度:对于每个数据点 x,在每棵树中计算其在树中的路径长度 h(x,T),并求出所有树的路径长度的平均值 h(x)。
-
计算异常得分:根据路径长度,计算每个数据点的异常得分 s(x),公式如下:
其中 H(i) 是调和级数,n 是数据集中的样本数量;c(n) 是平均路径长度的期望值,公式如下:
-
筛选异常点:根据异常得分和预定的阈值,筛选出异常点。如果 $s(x)$ 大于阈值,则将 $x$ 视为异常点。
二、参数详解
params={
'n_estimators' : 1000 , : 迭代次数、孤立树的数量
'max_samples' : 'auto' , : 每个孤立树中采样的样本数量,“auto”表示训练样本量
'contamination' : 'auto' , : 数据集中异常值的比例,“auto”表示训练样本中异常值比例
'max_features' : 1.0 , : 列采样比例
'bootstrap' : False , : 是否使用 bootstrap 重复采样
'n_jobs' : 4 , : 并行核数
'random_state' : 1 ,
'verbose' : 0 : 信息显示
}
三、实战
1、数据介绍
数据来源:使用DataFoutain上 基于UEBA的用户上网异常行为分析 比赛数据,可以从下面链接下载(需要先登录报名)、也可以关注威心公众号Python风控模型与数据分析回复 异常检测IF实战 获取
基于UEBA的用户上网异常行为分析 Competitions - DataFountain
赛题介绍:
随着企业信息化水平的不断提升,数据作为一种资产成为越来越多企业的共识,企业在产业与服务、营销支持、业务运营、风险管控、信息纰漏等生产、经营、管理活动中涉及到大量的商业秘密、工作秘密以及员工和客户的隐私信息。
目前绝大多数企业围绕敏感数据保护都出台了相关管理办法和操作行为准则,但是仍然存在导致敏感数据泄露的异常操作行为,《Securonix 2020 内部威胁报告》指出,涉及60%的内部网络安全和数据泄露事件都与企业用户的异常操作行为相关。
为了有效保护企业敏感数据,践行企业安全操作行为准则,杜绝由异常操作行为导致的企业敏感数据泄露安全事件发生,用户异常行为分析与识别成为重难点技术之一。
利用机器学习、深度学习,UEBA等人工智能方法,基于无标签的用户日常上网日志数据,构建用户上网行为基线和上网行为评价模型,依据上网行为与基线的距离确定偏离程度。
(1)通过用户日常上网数据构建行为基线;
(2)采用无监督学习模型,基于用户上网行为特征,构建上网行为评价模型,评价上网行为与基线的偏离程度。
2、数据预处理
训练集数据量为52w+,其中ret为异常评分。对 account,group,IP,url,switchIP,port,vlan 等字段进行Label编码;从time字段提取出年、月、周、日、时等时间字段
3、特征衍生
分别按照group、account等分组统计IP,url,switchIP,port,vlan等字段的唯一值数量,如group_IP_nunique;
按照group、IP分组统计数据条数得到特征 group_IP_cnt,其他字段同理;
1-3 代码部分:
# 导包
import re
import os
from sqlalchemy import create_engine
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
import sklearn
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve,roc_auc_score
from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt
import gc
from sklearn import preprocessing
from sklearn.preprocessing import MinMaxScaler
import math
from sklearn import metrics
from sklearn.metrics import mean_absolute_error,mean_squared_error,r2_score
import time
from sklearn.model_selection import KFold,cross_val_score
# 1、数据读取
df=pd.read_csv('数据文件/基于UEBA的用户上网异常行为分析.csv',encoding='gbk')
print(df.shape)
df.head()
# 2、特征编码及特征衍生
def get_label_encode(df,col_list):
def label_encode(df,col):
value_list=list(df[col].unique())
return {value:i+1 for value,i in zip(value_list,range(len(value_list)))}
result={}
for col in col_list:
result[col]=label_encode(df,col)
return result
def data_label_encode(df,map_dic):
df_copy=df.copy()
for col in map_dic.keys():
if col in df.columns:
df_copy[col]=df_copy[col].map(map_dic[col])
return df_copy
def time_pre(df):
df.time=pd.to_datetime(df.time)
df['year']=df.time.dt.year
df['month']=df.time.dt.month
df['week']=df.time.dt.week
df['day']=df.time.dt.day
df['hour']=df.time.dt.hour
df['minute']=df.time.dt.minute
return df
def fea_derive(df_copy):
result=(
df_copy
.merge(df_copy.groupby(['group','IP']).id.count().reset_index().rename(columns={'id':'group_IP_cnt'}),how='left',on=['group','IP'])
.merge(df_copy.groupby(['group','switchIP']).id.count().reset_index().rename(columns={'id':'group_switchIP_cnt'}),how='left',on=['group','switchIP'])
.merge(df_copy.groupby(['group','vlan']).id.count().reset_index().rename(columns={'id':'group_vlan_cnt'}),how='left',on=['group','vlan'])
.merge(df_copy.groupby(['group','port']).id.count().reset_index().rename(columns={'id':'group_port_cnt'}),how='left',on=['group','port'])
.merge(df_copy.groupby(['account','IP']).id.count().reset_index().rename(columns={'id':'account_IP_cnt'}),how='left',on=['account','IP'])
.merge(df_copy.groupby(['account','switchIP']).id.count().reset_index().rename(columns={'id':'account_switchIP_cnt'}),how='left',on=['account','switchIP'])
.merge(df_copy.groupby(['account','vlan']).id.count().reset_index().rename(columns={'id':'account_vlan_cnt'}),how='left',on=['account','vlan'])
.merge(df_copy.groupby(['account','port']).id.count().reset_index().rename(columns={'id':'account_port_cnt'}),how='left',on=['account','port'])
.merge(df_copy.groupby(['group']).IP.nunique().reset_index().rename(columns={'IP':'group_IP_nunique'}),how='left',on=['group'])
.merge(df_copy.groupby(['group']).switchIP.nunique().reset_index().rename(columns={'switchIP':'group_switchIP_nunique'}),how='left',on=['group'])
.merge(df_copy.groupby(['group']).vlan.nunique().reset_index().rename(columns={'vlan':'group_vlan_nunique'}),how='left',on=['group'])
.merge(df_copy.groupby(['group']).port.nunique().reset_index().rename(columns={'port':'group_port_nunique'}),how='left',on=['group'])
.merge(df_copy.groupby(['account']).IP.nunique().reset_index().rename(columns={'IP':'account_IP_nunique'}),how='left',on=['account'])
.merge(df_copy.groupby(['account']).switchIP.nunique().reset_index().rename(columns={'switchIP':'account_switchIP_nunique'}),how='left',on=['account'])
.merge(df_copy.groupby(['account']).vlan.nunique().reset_index().rename(columns={'vlan':'account_vlan_nunique'}),how='left',on=['account'])
.merge(df_copy.groupby(['account']).port.nunique().reset_index().rename(columns={'port':'account_port_nunique'}),how='left',on=['account'])
)
return result
map_dic=get_label_encode(df,['account', 'group', 'IP', 'url', 'switchIP','port','vlan'])
df_copy=df.pipe(data_label_encode,map_dic).pipe(time_pre).pipe(fea_derive)
df_copy.head()
4、异常分析及特征筛选
由于异常检测针对的就是数据分布,从中找到与大多数据分布不同的数据(低频值)视为异常值,因此可以考虑对特征分布情况进行统计分析。
首先查看IP,url,switchIP,port,vlan等特征,其中url、port均存在大量低频值,这样的特征对于异常检测没有太大贡献,将其剔除;
IP、vlan、switchIP分布正常,保留这部分特征
时间特征中month、week、day均为均匀分布,所以仅保留hour,如图所示,12点-23点为低频上网时段。
cnt、nunique统计特征根据分布情况,仅保留account_IP_nunique, account_switchIP_nunique, account_port_nunique三个
import pyecharts.options as opts
from pyecharts.charts import Bar, Line
def distribution_plot_df(df,col,plot='bar'):
sta=df_copy[col].value_counts().sort_index()
x,y=list(sta.index),list(sta)
return distribution_plot(x,y,col,plot)
def distribution_plot(x,y,col,plot='bar'):
if plot=='bar':
return distribution_bar(x,y,col)
else:
return distribution_line(x,y,col)
def distribution_bar(x,y,col):
bar = (
Bar(init_opts=opts.InitOpts(width="700px", height="500px"))
.add_xaxis(x)
.add_yaxis(
col,
y,
label_opts=opts.LabelOpts(is_show=False),
markpoint_opts=opts.MarkPointOpts(
label_opts=opts.LabelOpts(
font_size=13,
border_width=10
),
),
)
.set_global_opts(
# title_opts=opts.TitleOpts(title=title),
tooltip_opts=opts.TooltipOpts(trigger="axis"),
yaxis_opts=opts.AxisOpts(
splitline_opts=opts.SplitLineOpts(
is_show=True, linestyle_opts=opts.LineStyleOpts(opacity=1)
),
),
)
)
return bar
def distribution_line(x,y,col):
line = (
Line(init_opts=opts.InitOpts(width="900px", height="500px"))
.add_xaxis(x)
.add_yaxis(
col,
y,
label_opts=opts.LabelOpts(is_show=False),
is_smooth=True,
is_symbol_show=False,
linestyle_opts=opts.LineStyleOpts(width=1.3),
)
.set_global_opts(
# title_opts=opts.TitleOpts(title=title),
tooltip_opts=opts.TooltipOpts(trigger="axis"),
yaxis_opts=opts.AxisOpts(
type_="value",
splitline_opts=opts.SplitLineOpts(
is_show=True, linestyle_opts=opts.LineStyleOpts(opacity=1)
),
),
)
)
return line
5、模型构建及评估
最终特征列表:account,group,hour,vlan,switchIP,IP,account_IP_nunique,account_switchIP_nunique,account_port_nunique
按照8:2的比例划分训练集、测试集,构建IF模型进行预测,由于ret异常得分范围在0-1之间、IF预测结果在-0.2-0.1之间,所以对预测结果进行了归一化。最终分别在训练集、测试集上使用RMSE和Score进行评估,Score越趋近于1效果越好。
真实值和预测值的分布如下,可以考虑对预测结果的分布进行调整、以逼近于目标分布
from sklearn.ensemble import IsolationForest
def distribution_line2(x,y1,y2): # 拟合曲线
line = (
Line(init_opts=opts.InitOpts(width="650px", height="400px"))
.add_xaxis(x)
.add_yaxis(
series_name="true",
y_axis=y1,
label_opts=opts.LabelOpts(is_show=False),
is_smooth=True,
is_symbol_show=False,
linestyle_opts=opts.LineStyleOpts(width=1.1),
)
.add_yaxis(
series_name="pred",
y_axis=y2,
label_opts=opts.LabelOpts(is_show=False),
is_smooth=True,
is_symbol_show=False,
linestyle_opts=opts.LineStyleOpts(width=1.1),
)
.set_global_opts(
tooltip_opts=opts.TooltipOpts(trigger="axis"),
xaxis_opts=opts.AxisOpts(
type_="category",
axislabel_opts=opts.LabelOpts(rotate=30)
),
yaxis_opts=opts.AxisOpts(
type_="value",
splitline_opts=opts.SplitLineOpts(
is_show=True, linestyle_opts=opts.LineStyleOpts(opacity=1)
),
),
)
.set_series_opts(
areastyle_opts=opts.AreaStyleOpts(opacity=0.05),
label_opts=opts.LabelOpts(is_show=False),
)
)
return line
def rmse_value(y_true,y_pred):
mse=mean_squared_error(y_true, y_pred)
rmse=mse**0.5
score=1/(1+rmse)
return rmse,score
def get_if_model(df,fea_list): # 训练模型
params={
'n_estimators' : 1000 , # 迭代次数、孤立树的数量
'max_samples' : 'auto' , # 每个孤立树中采样的样本数量,“auto”表示训练数据集的样本数量
'contamination' : 'auto' , # 数据集中异常值的比例,“auto”表示训练样本中异常值比例
'max_features' : 1.0 , # 列采样比例
'bootstrap' : False , # 是否使用 bootstrap 重复采样
'n_jobs' : 4 , # 并行核数
'random_state' : 1 ,
'verbose' : 0 # 信息显示
}
if_model = IsolationForest(**params)
if_model.fit(df[df['sample']=='train'][fea_list])
return if_model
fea_list=['account', 'group', 'hour', 'vlan', 'switchIP', 'IP',
'account_IP_nunique', 'account_switchIP_nunique', 'account_port_nunique']
if_model=get_if_model(df_copy,fea_list)
df_copy['if_pred']=if_model.decision_function(df_copy[fea_list])
scaler = MinMaxScaler()
df_copy['if_pred_adjust']=1-pd.DataFrame(scaler.fit_transform(df_copy[['if_pred']]))[0]
rmse_value(df_copy.ret,df_copy.if_pred_adjust)
6、类别型变量编码优化
在 2、数据预处理 中,对IP、vlan等类别型变量直接使用的label编码,这里我们再尝试按照频数高低进行编码,把低频特征放在尾部、形成长尾分布,让孤立森林更容易将低频值的样本提前划分出来
保持入模特征列表、模型参数不变,重新训练IF模型进行评估,测试集效果从0.8202涨到了0.8216
划重点
关注威心公众号Python风控模型与数据分析回复 异常检测IF实战 获取本文数据、完整代码!还获取更多理论知识与代码分享
参考
2021 CCF BDCI 成果汇编 中的参赛方案