Hi,各人好,我是半亩花海。 阿里云作为国内最大的云服务提供商,天天都面对着网络上海量的恶意攻击。恶意软件是一种被设计用来对目标盘算机造成破坏或者占用目标盘算机资源的软件,传统的恶意软件包罗蠕虫、木马等。本项目泉源于“阿里云天池”的“学习赛”的赛题,针对恶意软件检测的现实场景而提出,应用呆板学习相干模子实现阿里云安全恶意程序的检测,在一定水平上进步泛化能力,提升对恶意样本的识别率。数据集来自:阿里云安全恶意程序检测_数据集-阿里云天池 (aliyun.com)。
一、数据探索
1.1 数据分析
本项目提供的数据来自经过沙箱程序模仿运行后的 API 指令序列,全为 Windows 二进制可执行程序,经过脱敏处理;样本数据均来自互联网,此中恶意文件的类型有感染型病毒、木马程序、挖矿程序、DDoS木马、勒索病毒等,数据总计 6 亿条。
(1)训练数据调用记载近 9000 万次,文件 1 万多个(以文件编号汇总),字段形貌如表所示:
字段类型表明field_idbigint文件编号labelbigint文件标签apistring文件调用的 API 名称tidbigint调用 API 的线程编号indexstring线程中 API 调用的顺序编号 此中,文件标签有 8 种:0-正常 / 1-勒索病毒 / 2-挖矿程序 / 3-DDoS 木马 / 4-蠕虫病毒 / 5-感染型病毒 / 6-后门程序 / 7-木马程序。
注意:
① 一个文件调用的 API 数目有可能许多,对于一个 tid 中调用超过 5000 个 API 的文件,我们进行了截断,按照顺序保留了每个 tid 前 5000 个 API 的记载。
② 差异线程 tid 之间没有顺序关系,同一个 tid 中的 index 由小到大代表调用的先后顺序关系。
③ index 是单个文件在沙箱执行时的全局顺序,由于沙箱执行时间有精度限制,因此在同一个index 上会出现同线程或者差异线程都在多次执行 API 的情况,其可以包管与 tid 内部的顺序雷同,但不包管连续。
(2)测试数据调用记载近 8000 万次,文件 1 万多个。除了没有 label 字段,数据格式与
训练数据一致。
1.2 训练集数据探索
1.2.1 数据特征类型
- # 导入相关应用包
- import pandas as pd
- import numpy as np
- import seaborn as sns
- import matplotlib.pyplot as plt
- # 忽略警告信息
- import warnings
- warnings.filterwarnings("ignore")
- %matplotlib inline
- # 读取数据
- path = './security_data/'
- train = pd.read_csv(path + 'security_train.csv') # 训练集
- test = pd.read_csv(path + 'security_test.csv') # 测试集
复制代码 用 DataFrame.info() 函数查看训练集的大小、数据类型等信息。
从运行结果可以看出:
- 数据中有 4 个 int64 类型(file_id, label, tid, index)的数据和 1 个 object 类型的数据(api)
- 整个数据集的大小为 3.3 GB
- 数据一共有 89806693 条记载
用 DataFrame.head() 函数查看训练集的头几行数据。
用 DataFrame.describe() 函数查看训练集的统计信息。
1.2.2 数据分布
使用箱线图查看单个变量的分布情况。下面以训练集为例,取前10000条数据绘制“tid”变量的箱线图,代码和运行结果如下:
- sns.boxplot(x=train.iloc[:10000]["tid"])
复制代码 用 nunique() 函数查看训练会合变量取值的分布。
由运行结果可知,file_id 的含有的信息如下:
- 13887 个差异的值
- 8 种差异的 labe
- 295 个差异的 API
- 2782 个差异的 tid
- 5001 个差异的 index
1.2.3 缺失值
查看训练集数据的缺失情况。
从运行结果看,数据不存在缺失的情况。
1.2.4 非常值
分析训练集“index”特征:
- train['index'].describe()
复制代码 “index”特征的最小值为 0,最大值为 5000,刚好是 5001 个值,看不出非常值。
分析训练集的“tid”特征:
“tid”特征的最小值为 100,最大值为 20896,因为这个字段表现的是线程,所以我们目前也没办法判断是否有非常值。
1.2.5 标签分布探索
统计标签取值的分布情况。
“label”对应的寄义如下表所示:
值分类0正常1勒索病毒2挖矿程序3DDoS 木马4蠕虫病毒5感染型病毒6后门程序7木马程序- train['label'].value_counts()
复制代码 由标签分布可以发现:训练会合一共有 16375107 个正常文件(label=0);2254561 个勒索病毒(label=1);9693969 个挖矿程序(label=2);8117585 个DDoS木马(label=3);663815 个虫病毒(label=4);33033543 个感染型病毒(label=5);4586578 个后门程序(label=6);15081535 个木马程序(label=7)。
为了直观化,我们可以通过条型图看数据的大小,同时用饼图看数据的比例,代码和结果如下:
- plt.figure(figsize=(12, 4), dpi=150)train['label'].value_counts()
- .sort_index().plot(kind = 'bar')
复制代码- plt.figure(figsize=(4, 4), dpi=150)train['label'].value_counts()
- .sort_index().plot(kind = 'pie')
复制代码 1.3 测试集探索
1.3.1 数据信息
用 DataFrame.head() 函数查看测试集的头几行数据。
用 DataFrame.info() 函数查看测试集的大小、数据类型等信息。
从运行结果可以看出:
- 数据中有 3 个 int64 类型(file_id, tid, index)的数据和 1 个 object 类型的数据(api)
- 整个数据集的大小为 2.4 GB
- 数据一共有 79288375 条记载
1.3.2 缺失值
查看测试集数据的缺失情况。
可知,数据不存在缺失的情况。
1.3.3 数据分布
查看测试会合变量取值的分布。
- sns.boxplot(x=test.iloc[:10000]["tid"])
复制代码 由运行结果可知,file_id 的含有的信息如下:
- 12955 个差异的值
- 298 个差异的 API
- 2047 个差异的 tid
- 5001 个差异的 index
1.3.4 非常值
查看测试集的“index”特征:
由结果可知,“index”特征的最小值为 0,最大值为 5000,刚好是 5001 个值,看不出任何非常的情况。
查看测试集的“tid”特征:
由结果可知,“tid”特征的最小值为 100,最大值为 9196,因为这个字段表现的是线程,所以目前也没办法判断是否有非常值。
1.4 数据集联合分析
1.4.1 file_id 分析
对比分析 “fleid” 变量在训练集和测试会合分布的重合情况:
- train_fileids = train['file_id'].unique()
- test_fileids = test['file_id'].unique()
复制代码- len(set(train_fileids) - set(test_fileids))
复制代码 运行结果表明,有 932 个训练文件是测试文件中没有的。
- len(set(test_fileids) - set(train_fileids))
复制代码 运行结果表明,测试文件中有的文件在训练文件中都有。
我们发现训练数据集和测试数据集的 file_id 存在交叉,也就是说 file_id 在训练集和测试集不是完全无交集的,这个时间不能直接合并,必要进行其他的处理来区分训练集和测试集。
1.4.2 API 分析
对比分析 “API” 变量在训练集和测试会合分布的重合情况:
- train_apis = train['api'].unique()
- test_apis = test['api'].unique()
复制代码- set(test_apis) - set(train_apis)
复制代码 运行结果表明,测试会合有 6 个 API 未出现在训练会合,分别是 reateDirectoryExW,InternetGetConnectedStateExA,MessageBoxTimeoutW, NtCreateUserProcess, NtDeleteFile,TaskDialog。
- set(train_apis) - set(test_apis)
复制代码 运行结果表明,训练会合有 3 个 API 未出现在测试会合,分别是 EncryptMessage,RtlCompressBuffer 和 WSASendTo。
二、特征工程与基线模子
2.1 特征工程基础部分
- import numpy as np
- import pandas as pd
- from tqdm import tqdm
- class _Data_Preprocess:
- def __init__(self):
- self.int8_max = np.iinfo(np.int8).max
- self.int8_min = np.iinfo(np.int8).min
- self.int16_max = np.iinfo(np.int16).max
- self.int16_min = np.iinfo(np.int16).min
- self.int32_max = np.iinfo(np.int32).max
- self.int32_min = np.iinfo(np.int32).min
- self.int64_max = np.iinfo(np.int64).max
- self.int64_min = np.iinfo(np.int64).min
- self.float16_max = np.finfo(np.float16).max
- self.float16_min = np.finfo(np.float16).min
- self.float32_max = np.finfo(np.float32).max
- self.float32_min = np.finfo(np.float32).min
- self.float64_max = np.finfo(np.float64).max
- self.float64_min = np.finfo(np.float64).min
- def _get_type(self, min_val, max_val, types):
- if types == 'int':
- if max_val <= self.int8_max and min_val >= self.int8_min:
- return np.int8
- elif max_val <= self.int16_max <= max_val and min_val >= self.int16_min:
- return np.int16
- elif max_val <= self.int32_max and min_val >= self.int32_min:
- return np.int32
- return None
- elif types == 'float':
- if max_val <= self.float16_max and min_val >= self.float16_min:
- return np.float16
- if max_val <= self.float32_max and min_val >= self.float32_min:
- return np.float32
- if max_val <= self.float64_max and min_val >= self.float64_min:
- return np.float64
- return None
- def _memory_process(self, df):
- init_memory = df.memory_usage().sum() / 1024 ** 2 / 1024
- print('Original data occupies {} GB memory.'.format(init_memory))
- df_cols = df.columns
-
- for col in tqdm_notebook(df_cols):
- try:
- if 'float' in str(df[col].dtypes):
- max_val = df[col].max()
- min_val = df[col].min()
- trans_types = self._get_type(min_val, max_val, 'float')
- if trans_types is not None:
- df[col] = df[col].astype(trans_types)
- elif 'int' in str(df[col].dtypes):
- max_val = df[col].max()
- min_val = df[col].min()
- trans_types = self._get_type(min_val, max_val, 'int')
- if trans_types is not None:
- df[col] = df[col].astype(trans_types)
- except:
- print(' Can not do any process for column, {}.'.format(col))
- afterprocess_memory = df.memory_usage().sum() / 1024 ** 2 / 1024
- print('After processing, the data occupies {} GB memory.'.format(afterprocess_memory))
- return df
复制代码 2.2 基线模子
2.2.1 数据读取
- import pandas as pd
- import numpy as np
- import seaborn as sns
- import matplotlib.pyplot as plt
- import lightgbm as lgb
- from sklearn.model_selection import train_test_split
- from sklearn.preprocessing import OneHotEncoder
- import warnings
- warnings.filterwarnings('ignore')
- %matplotlib inline
复制代码- path = './security_data/'
- train = pd.read_csv(path + 'security_train.csv')
- test = pd.read_csv(path + 'security_test.csv')
复制代码 2.2.2 特征工程
(1)使用 count() 函数和 nunique() 函数生成特征:反映样本调用 api,tid,index 的频率信息。
- def simple_sts_features(df):
- simple_fea = pd.DataFrame()
- simple_fea['file_id'] = df['file_id'].unique()
- simple_fea = simple_fea.sort_values('file_id')
-
- df_grp = df.groupby('file_id')
- simple_fea['file_id_api_count'] = df_grp['api'].count().values
- simple_fea['file_id_api_nunique'] = df_grp['api'].nunique().values
-
- simple_fea['file_id_tid_count'] = df_grp['tid'].count().values
- simple_fea['file_id_tid_nunique'] = df_grp['tid'].nunique().values
-
- simple_fea['file_id_index_count'] = df_grp['index'].count().values
- simple_fea['file_id_index_nunique'] = df_grp['index'].nunique().values
-
- return simple_fea
复制代码 (2)使用 mean() 函数、min() 函数、std() 函数、max() 函数生成特征:tid,index 可以为是数值特征,可提取对应的统计特征。
- def simple_numerical_sts_features(df):
- simple_numerical_fea = pd.DataFrame()
- simple_numerical_fea['file_id'] = df['file_id'].unique()
- simple_numerical_fea = simple_numerical_fea.sort_values('file_id')
-
- df_grp = df.groupby('file_id')
-
- simple_numerical_fea['file_id_tid_mean'] = df_grp['tid'].mean().values
- simple_numerical_fea['file_id_tid_min'] = df_grp['tid'].min().values
- simple_numerical_fea['file_id_tid_std'] = df_grp['tid'].std().values
- simple_numerical_fea['file_id_tid_max'] = df_grp['tid'].max().values
-
- simple_numerical_fea['file_id_index_mean'] = df_grp['index'].mean().values
- simple_numerical_fea['file_id_index_min'] = df_grp['index'].min().values
- simple_numerical_fea['file_id_index_std'] = df_grp['index'].std().values
- simple_numerical_fea['file_id_index_max'] = df_grp['index'].max().values
-
- return simple_numerical_fea
复制代码 (3)使用界说的特征生成函数,并生成训练集和测试集的统计特征。
反映样本调用 api,tid,index 的频率信息的统计特征。
- %%time
- simple_train_fea1 = simple_sts_features(train)
复制代码 CPU times: total: 37 s
Wall time: 37.1 s
- %%time
- simple_test_fea1 = simple_sts_features(test)
复制代码 CPU times: total: 32.5 s
Wall time: 32.6 s
反映 tid,index 等数值特征的统计特征。
- %%time
- simple_train_fea2 = simple_numerical_sts_features(train)
复制代码 CPU times: total: 6.14 s
Wall time: 6.15 s
- %%time
- simple_test_fea2 = simple_numerical_sts_features(test)
复制代码 CPU times: total: 5.38 s
Wall time: 5.42 s
2.2.3 基线构建
获取标签:
- train_label = train[['file_id','label']].drop_duplicates(subset = ['file_id','label'], keep = 'first')
- test_submit = test[['file_id']].drop_duplicates(subset = ['file_id'], keep = 'first')
复制代码 训练集和测试集的构建:
- ### 训练集&测试集构建
- train_data = train_label.merge(simple_train_fea1, on ='file_id', how='left')
- train_data = train_data.merge(simple_train_fea2, on ='file_id', how='left')
- test_submit = test_submit.merge(simple_test_fea1, on ='file_id', how='left')
- test_submit = test_submit.merge(simple_test_fea2, on ='file_id', how='left')
复制代码 因为本赛题给出的指标和传统的指标略有差异,所以必要自己写评估指标,这样方便对比线下与线上的差距,以判断是否过拟合、是否出现线上线下不一致的问题等。
关于 LGB 的自界说评估指标的誊写,可以参考 LightGBM 的文档。以下为赛题的模子评估函数:
- def lgb_logloss(preds,data):
- labels_ = data.get_label()
- classes_ = np.unique(labels_)
- preds_prob = []
- for i in range(len(classes_)):
- preds_prob.append(preds[i*len(labels_):(i+1) * len(labels_)] )
-
- preds_prob_ = np.vstack(preds_prob)
-
- loss = []
- for i in range(preds_prob_.shape[1]): # 样本个数
- sum_ = 0
- for j in range(preds_prob_.shape[0]): #类别个数
- pred = preds_prob_[j,i] # 第i个样本预测为第j类的概率
- if j == labels_[i]:
- sum_ += np.log(pred)
- else:
- sum_ += np.log(1 - pred)
- loss.append(sum_)
- return 'loss is: ',-1 * (np.sum(loss) / preds_prob_.shape[1]),False
复制代码 线下验证。因为数据与时间的相干性并不是非常大,所以此处我们用传统的 5 折交叉验证来构建线下验证集。
- ### 模型验证
- train_features = [col for col in train_data.columns if col not in ['label','file_id']]
- train_label = 'label'
复制代码 使用 5 折交叉验证,采用 LightGBM 模子,代码和运行结果如下:
- %%time
- from sklearn.model_selection import StratifiedKFold,KFold
- params = {
- 'task':'train',
- 'num_leaves': 255,
- 'objective': 'multiclass',
- 'num_class': 8,
- 'min_data_in_leaf': 50,
- 'learning_rate': 0.05,
- 'feature_fraction': 0.85,
- 'bagging_fraction': 0.85,
- 'bagging_freq': 5,
- 'max_bin': 128,
- 'random_state': 100,
- 'verbose': 50,
- 'early_stopping_rounds': 100
- }
- folds = KFold(n_splits=5, shuffle=True, random_state=15)
- oof = np.zeros(len(train))
- predict_res = 0
- models = []
- for fold_, (trn_idx, val_idx) in enumerate(folds.split(train_data)):
- print("fold n°{}".format(fold_))
- trn_data = lgb.Dataset(train_data.iloc[trn_idx][train_features], label=train_data.iloc[trn_idx][train_label].values)
- val_data = lgb.Dataset(train_data.iloc[val_idx][train_features], label=train_data.iloc[val_idx][train_label].values)
-
- clf = lgb.train(params, trn_data, num_boost_round=2000,valid_sets=[trn_data,val_data], feval=lgb_logloss)
- models.append(clf)
复制代码 2.3.4 特征重要性分析
通过特征重要性分析,可以看到在当前指标显示的结果下,影响因子最高的特征因素,从而更好地理解题目,同时在此基础上进行特征工程的延伸。相应的代码和可视化结果如下:
- feature_importance = pd.DataFrame()
- feature_importance['fea_name'] = train_features
- feature_importance['fea_imp'] = clf.feature_importance()
- feature_importance = feature_importance.sort_values('fea_imp',ascending = False)
- plt.figure(figsize=[20, 10,])
- sns.barplot(x = feature_importance['fea_name'], y = feature_importance['fea_imp'])
- #sns.barplot(x = "fea_name",y = "fea_imp", data = feature_importance)
复制代码 由运行结果可以看出:
- (1)API 的调用次数和 API 的调用类别数是最重要的两个特征,即差异的病毒经常会调用差异的 API,而且由于有些病毒必要复制自身的原因,因此调用 API 的次数会明显比其他差异类别的病毒多。
- (2)第三到第五强的都是线程统计特征,这也较为容易理解,因为木马等病毒经常必要通过线程监听一些内容,所以在线程等使用上会表现的略有差异。
2.3.5 模子测试
至此,通过前期的数据探索分析和基础的特征工程,我们已经得到了一个可以线上提交的基线模子。此处我们采用 LightGBM 进行 5 折交叉验证,也就是用上面训练的 5 个 LightGBM 分别对测试集进行猜测,然后将全部猜测结果的均值作为最闭幕果。其猜测结果并不能 100% 超过单折全量数据的 LightGBM,但是大多数还是不错的,建议将此作为 Baseline 方法的候选方法。
- pred_res = 0
- fold = 5
- for model in models:
- pred_res += model.predict(test_submit[train_features]) * 1.0 / fold
- test_submit['prob0'] = 0
- test_submit['prob1'] = 0
- test_submit['prob2'] = 0
- test_submit['prob3'] = 0
- test_submit['prob4'] = 0
- test_submit['prob5'] = 0
- test_submit['prob6'] = 0
- test_submit['prob7'] = 0
- test_submit[['prob0','prob1','prob2','prob3','prob4','prob5','prob6','prob7']] = pred_res
- test_submit[['file_id','prob0','prob1','prob2','prob3','prob4','prob5','prob6','prob7']].to_csv('baseline.csv',index = None)
复制代码 三、高阶数据探索
3.1 数据读取
- import pandas as pdimport numpy as npimport seaborn as snsimport matplotlib.pyplot as plt%matplotlib inline### 数据读取path = './security_data/'
- train = pd.read_csv(path + 'security_train.csv')
- test = pd.read_csv(path + 'security_test.csv')
复制代码 3.2 多变量交叉探索
(1)通过统计特征 file_id_cnt,分析 变量 file_id 和 api 之间的关系。
- train_analysis = train[['file_id','label']].drop_duplicates(subset = ['file_id','label'], keep = 'last')
- dic_ = train['file_id'].value_counts().to_dict()
- train_analysis['file_id_cnt'] = train_analysis['file_id'].map(dic_).values
复制代码- train_analysis['file_id_cnt'].value_counts()
复制代码 可以看到,文件调用 API 次数出现最多的是 5001 次。
- sns.distplot(train_analysis['file_id_cnt'])
复制代码- print('There are {} data are below 10000'.format(np.sum(train_analysis['file_id_cnt'] <= 1e / train_analysis.shape[0]))
复制代码 There are 0.8012529704039749 data are below 10000
我们也发现,API 调用次数的 80% 都会合在 10000 次以下。
(2)为了便于分析变量 file_id_cnt 与 label 的关系,将数据按 file_id_cnt 变量(即 API 调用次数)取值划分为 16 个区间。
- ### file_id_cnt & label 分析
- def file_id_cnt_cut(x):
- if x< 15000:
- return x // 1e3
- else:
- return 15
- train_analysis['file_id_cnt_cut'] = train_analysis['file_id_cnt'].map(file_id_cnt_cut).values
复制代码 然后随机选取 4 个区间进行查看,代码及运行结果如下所示。
- plt.figure(figsize=[16,20])
- plt.subplot(321)
- train_analysis[train_analysis['file_id_cnt_cut'] == 0]['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('file_id_cnt_cut = 0')
- plt.xlabel('label')
- plt.ylabel('label_number')
- plt.subplot(322)
- train_analysis[train_analysis['file_id_cnt_cut'] == 1]['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('file_id_cnt_cut = 1')
- plt.xlabel('label')
- plt.ylabel('label_number')
-
- plt.subplot(323)
- train_analysis[train_analysis['file_id_cnt_cut'] == 14]['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('file_id_cnt_cut = 14')
- plt.xlabel('label')
- plt.ylabel('label_number')
- plt.subplot(324)
- train_analysis[train_analysis['file_id_cnt_cut'] == 15]['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('file_id_cnt_cut = 15')
- plt.xlabel('label')
- plt.ylabel('label_number')
- plt.subplot(313)
- train_analysis['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('All Data')
- plt.xlabel('label')
- plt.ylabel('label_number')
复制代码   从图中可以看到:当 API 调用次数越多时,该 API 是第五类病毒(感染型病毒)的可能性就越大。
用分簇散点图查看 label 下file_id_cnt的分布,由于绘制分簇散点图比较耗时,因此我们采用 1000 个样本点。
- plt.figure(figsize=[16, 10])
- sns.swarmplot(x = train_analysis.iloc[:1000]['label'], y = train_analysis.iloc[:1000]['file_id_cnt'])
复制代码 从图中得到以下结论:从频次上看,第 5 类病毒调用 API 的次数最多:从调用峰值上看第 2 类和 7 类病毒有时能调用 150000 次的 API。
(3)起首通过文件调用 API 的类别数 file_id_api_nunique,分析变量 file_id 和 API 的关系。
- dic_ = train.groupby('file_id')['api'].nunique().to_dict()
- train_analysis['file_id_api_nunique'] = train_analysis['file_id'].map(dic_).values
- sns.distplot(train_analysis['file_id_api_nunique'])
复制代码- train_analysis['file_id_api_nunique'].describe()
复制代码 文件调用 API 的类别数绝大部分都在 100 以内,最少的是 1 个,最多的是 170 个。
然后分析变量 file_id_api_nunique 和 label 的关系。
- train_analysis.loc[train_analysis.file_id_api_nunique >=100]['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('File with api nunique >= 100')
- plt.xlabel('label')
- plt.ylabel('label_number')
复制代码 从图中可以发现,第 5 类病毒调用差异 API 的次数是最多的。在上面的分析中,我们也发现第 5 类病毒调用 API 的次数最多,调用差异 API 的次数多也是可以理解的。
- plt.figure(figsize=[16, 10])
- sns.boxplot(x = train_analysis['label'], y = train_analysis['file_id_api_nunique'])
复制代码 从图中得到以下结论:第 3 类病毒调用差异 API 的次数相对较多,第 2 类病毒调用差异 API 的次数最少:第 4,6,7 类病毒的离群点较少,第 1 类病毒的离群点最多,第 3 类病毒的离群点主要在下方;第 0 类和第 5 类的离群点则会合在上方。
(4)起首,通过 file_id_index_nunique 和 file_id_index_max 两个统计特征,分析变量 file_id 和 index 之间的关系。有个奇怪的征象,我们发现调用 API 顺序编号的两个边沿(0 和 5001)的样本数是最多的,因此可以单独看一下这两个点的 label 分布。
- dic_ = train.groupby('file_id')['index'].nunique().to_dict()
- train_analysis['file_id_index_nunique'] = train_analysis['file_id'].map(dic_).values
- train_analysis['file_id_index_nunique'].describe()
复制代码- sns.distplot(train_analysis['file_id_index_nunique'])
复制代码- dic_ = train.groupby('file_id')['index'].max().to_dict()
- train_analysis['file_id_index_max'] = train_analysis['file_id'].map(dic_).values
- sns.distplot(train_analysis['file_id_index_max'])
复制代码 从图中可以看出,文件调用 index 有两个极度:一个是在 1 附近,另一个是在 5000 附近。
然后分析变量 file_id_index_nunique 和 file_id_index_max 与 label 的关系。
- plt.figure(figsize=[16,8])
- plt.subplot(121)
- train_analysis.loc[train_analysis.file_id_index_nunique == 1]['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('File with index nunique = 1')
- plt.xlabel('label')
- plt.ylabel('label_number')
- plt.subplot(122)
- train_analysis.loc[train_analysis.file_id_index_nunique == 5001]['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('File with index nunique = 5001')
- plt.xlabel('label')
- plt.ylabel('label_number')
复制代码 从图中可以发现,在文件顺序编号只有一个时,文件的标签只会是 0(正常)、2(挖矿程序)或 5(感染型病毒),而不会是其他病毒,而且最大概率可能是 5;对于顺序次数大于 5000 个的文件,其和上面调用 API 次数很大时雷同。
还可以通过绘制小提琴图、分类散点图分析,代码和结果如下:
- plt.figure(figsize=[16,10])
- sns.violinplot(x =train_analysis['label'], y = train_analysis['file_id_api_nunique'])
复制代码- plt.figure(figsize=[16,10])
- sns.violinplot(x =train_analysis['label'], y = train_analysis['file_id_index_max'])
复制代码- plt.figure(figsize=[16,10])
- sns.stripplot(x =train_analysis['label'], y = train_analysis['file_id_index_max'])
复制代码 从图中得到的结论:第 3 类病毒调用差异 index 次数的平均值最大;第 2 类病毒调用差异 index 次数的平均值最小;第 5,6,7 类病毒调用差异 index 次数的平均值相似。
(5)起首通过 file_id_tid_nunique和 file_id_tid_max 两个统计特征,分析变量 file_id 和 tid 之间的关系。
- dic_ = train.groupby('file_id')['tid'].nunique().to_dict()
- train_analysis['file_id_tid_nunique'] = train_analysis['file_id'].map(dic_).values
- train_analysis['file_id_tid_nunique'].describe()
复制代码- sns.distplot(train_analysis['file_id_tid_nunique'])
复制代码- dic_ = train.groupby('file_id')['tid'].max().to_dict()
- train_analysis['file_id_tid_max'] = train_analysis['file_id'].map(dic_).values
- train_analysis['file_id_tid_max'].describe()
复制代码- sns.distplot(train_analysis['file_id_tid_max'])
复制代码 然后分析变量 file_id_tid_nunique 和 file_id_tid_max 与 label 的关系。
- plt.figure(figsize=[16,8])
- plt.subplot(121)
- train_analysis.loc[train_analysis.file_id_tid_nunique < 5]['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('File with tid nunique < 5')
- plt.xlabel('label')
- plt.ylabel('label_number')
- plt.subplot(122)
- train_analysis.loc[train_analysis.file_id_tid_nunique >= 20]['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('File with tid nunique >= 20')
- plt.xlabel('label')
- plt.ylabel('label_number')
复制代码 此中,0:正常文件;1:勒索病毒;2:挖矿程序;3:DDoS 木马;4:蠕虫病毒;5:感染型病毒;6:后门程序;7:木马程序。
还可以通过箱线图和小提琴图进一步分析。
- plt.figure(figsize=[12,8])
- sns.boxplot(x =train_analysis['label'], y = train_analysis['file_id_tid_nunique'])
复制代码- plt.figure(figsize=[12,8])
- sns.violinplot(x =train_analysis['label'], y = train_analysis['file_id_tid_nunique'])
复制代码 分析 file_id 和 tid 的 max 特征,我们将 tid 最大值大于 3000 的数据和整体比较,发现分布差异并不是非常大。
- plt.figure(figsize=[16,8])
- plt.subplot(121)
- train_analysis.loc[train_analysis.file_id_tid_max >= 3000]['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('File with tid max >= 3000')
- plt.xlabel('label')
- plt.ylabel('label_number')
-
- plt.subplot(122)
- train_analysis['label'].value_counts().sort_index().plot(kind = 'bar')
- plt.title('All Data')
- plt.xlabel('label')
- plt.ylabel('label_number')
复制代码 从图中得出的结论:全部文件调用的线程都相对较少;第 7 类病毒调用的线程数的范围最大;第 0 类,3 类和 4 类调用的差异线程数雷同。
(6)分析变量 API 与 label 的关系,代码及运行结果如下:
- train['api_label'] = train['api'] + '_' + train['label'].astype(str)
- dic_ = train['api_label'].value_counts().to_dict()
- df_api_label = pd.DataFrame.from_dict(dic_,orient = 'index').reset_index()
- df_api_label.columns = ['api_label', 'api_label_count']
- df_api_label['label'] = df_api_label['api_label'].apply(lambda x:int(x.split('_')[-1]))
- labels = df_api_label['label'].unique()
- for label in range(8):
- print('*' * 50, label,'*' * 50)
- print(df_api_label.loc[df_api_label.label == label].sort_values('api_label_count').iloc[-5:][['api_label','api_label_count']])
- print('*' * 103)
复制代码 从结果可以得到以下结论:LdrGetProcedureAddress,全部病毒和正常文件都是调用比较多的;第 5 类病毒:Thread32Next 调用得较多;第 6 类和 7 类病毒:NtDelayExecution 调用得较多;第 2 类和 7 类病毒:Process32NextW 调用得较多。
四、特征工程进阶与方案优化
在快速 Baseline 的基础上,通过对多变量的交叉分析,可以增长新的 pivot 特征,迭代优化一般新的模子。
4.1 特征工程基础部分
(1)导入工具包,读取数据
- import pandas as pd
- import numpy as np
- import seaborn as sns
- import matplotlib.pyplot as plt
- import lightgbm as lgb
- from sklearn.model_selection import train_test_split
- from sklearn.preprocessing import OneHotEncoder
- from tqdm import tqdm_notebook
- import warnings
- warnings.filterwarnings('ignore')
- %matplotlib inline
- ### 数据读取
- path = './security_data/'
- train = pd.read_csv(path + 'security_train.csv')
- test = pd.read_csv(path + 'security_test.csv')
复制代码 (2)内存管理
- # 内存管理import numpy as np
- import pandas as pd
- from tqdm import tqdm
- class _Data_Preprocess:
- def __init__(self):
- self.int8_max = np.iinfo(np.int8).max
- self.int8_min = np.iinfo(np.int8).min
- self.int16_max = np.iinfo(np.int16).max
- self.int16_min = np.iinfo(np.int16).min
- self.int32_max = np.iinfo(np.int32).max
- self.int32_min = np.iinfo(np.int32).min
- self.int64_max = np.iinfo(np.int64).max
- self.int64_min = np.iinfo(np.int64).min
- self.float16_max = np.finfo(np.float16).max
- self.float16_min = np.finfo(np.float16).min
- self.float32_max = np.finfo(np.float32).max
- self.float32_min = np.finfo(np.float32).min
- self.float64_max = np.finfo(np.float64).max
- self.float64_min = np.finfo(np.float64).min
- def _get_type(self, min_val, max_val, types):
- if types == 'int':
- if max_val <= self.int8_max and min_val >= self.int8_min:
- return np.int8
- elif max_val <= self.int16_max <= max_val and min_val >= self.int16_min:
- return np.int16
- elif max_val <= self.int32_max and min_val >= self.int32_min:
- return np.int32
- return None
- elif types == 'float':
- if max_val <= self.float16_max and min_val >= self.float16_min:
- return np.float16
- if max_val <= self.float32_max and min_val >= self.float32_min:
- return np.float32
- if max_val <= self.float64_max and min_val >= self.float64_min:
- return np.float64
- return None
- def _memory_process(self, df):
- init_memory = df.memory_usage().sum() / 1024 ** 2 / 1024
- print('Original data occupies {} GB memory.'.format(init_memory))
- df_cols = df.columns
-
- for col in tqdm_notebook(df_cols):
- try:
- if 'float' in str(df[col].dtypes):
- max_val = df[col].max()
- min_val = df[col].min()
- trans_types = self._get_type(min_val, max_val, 'float')
- if trans_types is not None:
- df[col] = df[col].astype(trans_types)
- elif 'int' in str(df[col].dtypes):
- max_val = df[col].max()
- min_val = df[col].min()
- trans_types = self._get_type(min_val, max_val, 'int')
- if trans_types is not None:
- df[col] = df[col].astype(trans_types)
- except:
- print(' Can not do any process for column, {}.'.format(col))
- afterprocess_memory = df.memory_usage().sum() / 1024 ** 2 / 1024
- print('After processing, the data occupies {} GB memory.'.format(afterprocess_memory))
- return df
- memory_process = _Data_Preprocess()
复制代码 (3)基础特征工程制作
- def simple_sts_features(df):
- simple_fea = pd.DataFrame()
- simple_fea['file_id'] = df['file_id'].unique()
- simple_fea = simple_fea.sort_values('file_id')
-
- df_grp = df.groupby('file_id')
- simple_fea['file_id_api_count'] = df_grp['api'].count().values
- simple_fea['file_id_api_nunique'] = df_grp['api'].nunique().values
-
- simple_fea['file_id_tid_count'] = df_grp['tid'].count().values
- simple_fea['file_id_tid_nunique'] = df_grp['tid'].nunique().values
-
- simple_fea['file_id_index_count'] = df_grp['index'].count().values
- simple_fea['file_id_index_nunique'] = df_grp['index'].nunique().values
-
- return simple_fea
- def simple_numerical_sts_features(df):
- simple_numerical_fea = pd.DataFrame()
- simple_numerical_fea['file_id'] = df['file_id'].unique()
- simple_numerical_fea = simple_numerical_fea.sort_values('file_id')
-
- df_grp = df.groupby('file_id')
-
- simple_numerical_fea['file_id_tid_mean'] = df_grp['tid'].mean().values
- simple_numerical_fea['file_id_tid_min'] = df_grp['tid'].min().values
- simple_numerical_fea['file_id_tid_std'] = df_grp['tid'].std().values
- simple_numerical_fea['file_id_tid_max'] = df_grp['tid'].max().values
-
- simple_numerical_fea['file_id_index_mean'] = df_grp['index'].mean().values
- simple_numerical_fea['file_id_index_min'] = df_grp['index'].min().values
- simple_numerical_fea['file_id_index_std'] = df_grp['index'].std().values
- simple_numerical_fea['file_id_index_max'] = df_grp['index'].max().values
-
- return simple_numerical_fea
复制代码 (4)特征获取
- %%time
- simple_train_fea1 = simple_sts_features(train)
复制代码 CPU times: total: 37.2 s
Wall time: 37.4 s
- %%time
- simple_test_fea1 = simple_sts_features(test)
复制代码 CPU times: total: 31.4 s
Wall time: 31.5 s
- %%time
- simple_train_fea2 = simple_numerical_sts_features(train)
复制代码 CPU times: total: 5.95 s
Wall time: 6.02 s
- %%time
- simple_test_fea2 = simple_numerical_sts_features(test)
复制代码 CPU times: total: 5.28 s
Wall time: 5.32 s
4.2 特征工程进阶部分
(1)每个 API 调用线程 tid 的次数
- def api_pivot_count_features(df):
- tmp = df.groupby(['file_id','api'])['tid'].count().to_frame('api_tid_count').reset_index()
- tmp_pivot = pd.pivot_table(data = tmp, index = 'file_id', columns='api', values='api_tid_count', fill_value=0)
- tmp_pivot.columns = [tmp_pivot.columns.names[0] + '_pivot_'+ str(col) for col in tmp_pivot.columns]
- tmp_pivot.reset_index(inplace = True)
- tmp_pivot = memory_process._memory_process(tmp_pivot)
- return tmp_pivot
复制代码 (2)每个 API 调用差异线程 tid 的次数
- def api_pivot_nunique_features(df):
- tmp = df.groupby(['file_id','api'])['tid'].nunique().to_frame('api_tid_nunique').reset_index()
- tmp_pivot = pd.pivot_table(data = tmp, index = 'file_id', columns='api', values='api_tid_nunique', fill_value=0)
- tmp_pivot.columns = [tmp_pivot.columns.names[0] + '_pivot_'+ str(col) for col in tmp_pivot.columns]
- tmp_pivot.reset_index(inplace = True)
- tmp_pivot = memory_process._memory_process(tmp_pivot)
- return tmp_pivot
复制代码 (3)特征获取
- %%time
- simple_train_fea3 = api_pivot_count_features(train)
复制代码- %%time
- simple_test_fea3 = api_pivot_count_features(test)
复制代码- %%time
- simple_train_fea4 = api_pivot_count_features(train)
复制代码- %%time
- simple_test_fea4 = api_pivot_count_features(test)
复制代码 4.3 基于 LightGBM 的模子验证
(1)获取标签
- train_label = train[['file_id','label']].drop_duplicates(subset = ['file_id','label'], keep = 'first')
- test_submit = test[['file_id']].drop_duplicates(subset = ['file_id'], keep = 'first')
复制代码 (2)训练集与测试集的构建(将之前提取的特征与新生成的特征进行合并)
- train_data = train_label.merge(simple_train_fea1, on ='file_id', how='left')
- train_data = train_data.merge(simple_train_fea2, on ='file_id', how='left')
- train_data = train_data.merge(simple_train_fea3, on ='file_id', how='left')
- train_data = train_data.merge(simple_train_fea4, on ='file_id', how='left')
- test_submit = test_submit.merge(simple_test_fea1, on ='file_id', how='left')
- test_submit = test_submit.merge(simple_test_fea2, on ='file_id', how='left')
- test_submit = test_submit.merge(simple_test_fea3, on ='file_id', how='left')
- test_submit = test_submit.merge(simple_test_fea4, on ='file_id', how='left')
复制代码 (3)评估指标构建
分数采用 logloss 盘算公式,如下:
logloss = 1 N ∑ j N ∑ i M [ y i j log ( P i j ) + ( 1 − y i j ) log ( 1 − P i j ) ] \text { logloss }=\frac{1}{N} \sum_j^N \sum_i^M\left[y_{i j} \log \left(P_{i j}\right)+\left(1-y_{i j}\right) \log \left(1-P_{i j}\right)\right] logloss =N1j∑Ni∑M[yijlog(Pij)+(1−yij)log(1−Pij)]
此中,M 代表分类数,N 代表测试集样本数, y i j {y}_{ij} yij 为代表第 i 个样本是否为类别 j(1为是,0为否), P i j {P}_{ij} Pij 代表选手提交的第 i 个样本被猜测为类别 j 的概率,最终公布的 logloss 保留小数点后 6 位。
需特别注意,log 对于小于 1 的数是非常敏感的。比如 log0.1 和 log0.000001 的单个样本的误差为 10 左右,而 1og0.99 和 1og0.95 的单个误差为 0.1 左右。
logloss 和 AUC 的区别:AUC 只在乎把正样本排到前面的能力,logloss 更加注意评估的正确性。如果给猜测值乘以一个倍数,则 AUC 不会变,但是 logloss 会变。
- ### 评估指标构建
- def lgb_logloss(preds,data):
- labels_ = data.get_label()
- classes_ = np.unique(labels_)
- preds_prob = []
- for i in range(len(classes_)):
- preds_prob.append(preds[i*len(labels_):(i+1) * len(labels_)] )
-
- preds_prob_ = np.vstack(preds_prob)
-
- loss = []
- for i in range(preds_prob_.shape[1]):
- sum_ = 0
- for j in range(preds_prob_.shape[0]):
- pred = preds_prob_[j,i]
- if j == labels_[i]:
- sum_ += np.log(pred)
- else:
- sum_ += np.log(1 - pred)
- loss.append(sum_)
- return 'loss is: ',-1 * (np.sum(loss) / preds_prob_.shape[1]),False
复制代码 (4)模子采用 5 折交叉验证方式
- train_features = [col for col in train_data.columns if col not in ['label','file_id']]
- train_label = 'label'
- %%time
- from sklearn.model_selection import StratifiedKFold,KFold
- params = {
- 'task':'train',
- 'num_leaves': 255,
- 'objective': 'multiclass',
- 'num_class': 8,
- 'min_data_in_leaf': 50,
- 'learning_rate': 0.05,
- 'feature_fraction': 0.85,
- 'bagging_fraction': 0.85,
- 'bagging_freq': 5,
- 'max_bin':128,
- 'random_state':100,
- 'verbose': 50,
- 'early_stopping_rounds': 100
- }
- folds = KFold(n_splits=5, shuffle=True, random_state=15)
- oof = np.zeros(len(train))
- predict_res = 0
- models = []
- for fold_, (trn_idx, val_idx) in enumerate(folds.split(train_data)):
- print("fold n°{}".format(fold_))
- trn_data = lgb.Dataset(train_data.iloc[trn_idx][train_features], label=train_data.iloc[trn_idx][train_label].values)
- val_data = lgb.Dataset(train_data.iloc[val_idx][train_features], label=train_data.iloc[val_idx][train_label].values)
-
- clf = lgb.train(params, trn_data, num_boost_round=2000,valid_sets=[trn_data,val_data], feval=lgb_logloss)
- models.append(clf)
复制代码 4.4 模子结果分析
特征相干性分析: 盘算特征之间的相干性系数,并用热力图可视化显示。
这里采样 10000个 样本,观察此中 20 个特征的线性相干性。
- plt.figure(figsize=[10,8])
- sns.heatmap(train_data.iloc[:10000, 1:21].corr())
复制代码 通过查看特征变量与 label 的相干性,我们也可以再次验证之前数据探索 EDA 部分的结论,每个文件调用 API 的次数与病毒类型是强相干的。
特征重要性分析的代码和结果如下:
- ### 特征重要性分析
- feature_importance = pd.DataFrame()
- feature_importance['fea_name'] = train_features
- feature_importance['fea_imp'] = clf.feature_importance()
- feature_importance = feature_importance.sort_values('fea_imp',ascending = False)
- feature_importance.sort_values('fea_imp', ascending = False)
复制代码 我们把 LightGBM 的特征重要性进行排序并输出,同样也可以再次验证之前 EDA 部分的结论,每个文件调用 API 的次数与病毒类型是强相干的。
- plt.figure(figsize=[20, 10,])
- plt.figure(figsize=[20, 10,])
- sns.barplot(x = feature_importance.iloc[:10]['fea_name'], y = feature_importance.iloc[:10]['fea_imp'])
复制代码- plt.figure(figsize=[20, 10,])
- sns.barplot(x = feature_importance['fea_name'], y = feature_importance['fea_imp'])
复制代码 对特征的重要性分析也再一次验证了我们的想法:
API 的调用次数及 API 的调用类别数是最重要的两个特征,也就是说差异的病毒经常会调用差异的 API,而且因为有些病毒必要复制自身的原因,调用 API 的次数会非常多:第三到第五强的都是线程统计特征,这也较为容易理解,因为木马等病毒经常必要通过线程监听一些内容,所以在线程数目的使用上也会表现的略不雷同。
树模子绘制。我们把 LightGBM 的树模子依次输出,并结合绘制的树模子进行业务的理解。
- ax = lgb.plot_tree(clf, tree_index=1, figsize=(20, 8), show_info=['split_gain'])
- plt.show()
复制代码 4.5 模子测试
此处,我们采用 LightGBM 模子进行 5 折交叉验证,取猜测均值作为最闭幕果。
LightGBM 模子进行 5 折交叉验证的猜测并不能 100% 超过单折全量数据的 LightGBM,但是在大多数时间取得的结果还是不错的,建议将此作为 Baseline 方法的候选。
- pred_res = 0
- flod = 5
- for model in models:
- pred_res += model.predict(test_submit[train_features]) * 1.0 / flod
- test_submit['prob0'] = 0
- test_submit['prob1'] = 0
- test_submit['prob2'] = 0
- test_submit['prob3'] = 0
- test_submit['prob4'] = 0
- test_submit['prob5'] = 0
- test_submit['prob6'] = 0
- test_submit['prob7'] = 0
- test_submit[['prob0','prob1','prob2','prob3','prob4','prob5','prob6','prob7']] = pred_res
- test_submit[['file_id','prob0','prob1','prob2','prob3','prob4','prob5','prob6','prob7']].to_csv('baseline2.csv',index = None)
复制代码
最后这里有点小报错,但我不停不能很好地解决,尝试多次,暂罢,各人可以多多交换学习!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |