8.5 检测模型

恶意程序的检测过程是个典型的二分类问题,工业界和学术届经常使用的方法包括多层感知机(MLP)、卷积神经网络(CNN)、梯度提升决策树(GBDT)和XGBoost,本套图书的第二本《Web安全之深度学习实战》中介绍了比较多的深度学习算法,这里我们重点介绍GBDT和XGBoost。

1.多层感知机

Joshua Saxe和Konstantin Berlin使用的检测模型正是MLP,如图8-8所示,该MLP输入层有1024个节点,隐藏层一共有两层,每层都是1024个节点,最后输出层1个节点。

图8-8 恶意程序检测使用的MLP

2.梯度提升决策树

GBDT(Gradient Boosting Decision Tree)又叫MART(Multiple Additive Regression Tree),是一种迭代的决策树算法,该算法由多棵决策树组成,所有树的结论累加起来做最终答案。GBDT的思想使其具有天然优势,可以发现多种有区分性的特征以及特征组合。GBDT泛化能力强,自身抗过拟合能力强,在工业界有广泛应用。

Boost是提升的意思,一般Boosting算法都是一个迭代的过程,每一次新的训练都是为了改进上一次的结果。原始的Boost算法是在算法开始的时候,为每一个样本赋予一个权重值,初始的时候,大家都是一样重要的。在每一步训练中得到的模型,会使得数据点的估计有对有错,我们就在每一步结束后,增加分错的点的权重,减少分对的点的权重,这样使得某些点如果老是被分错,那么就会被严重关注,也就被赋予一个很高的权重。然后等进行了N次迭代,将会得到N个简单的分类器,然后我们将它们组合起来,得到一个最终的模型。GBDT与传统的Boost的区别是,每一次的计算是为了减少上一次的残差,为了消除残差,我们可以在残差减少的梯度方向上建立一个新的模型 [1]

在Scikit-Learn中,GBDT的分类器使用GradientBoostingClassifier实现。代码如下:


class sklearn.ensemble.GradientBoostingClassifier(loss='deviance',learning_rate=0.1, n_estimators=100, subsample=1.0, 
criterion='friedman_mse', min_samples_split=2, 
min_samples_leaf=1, min_weight_fraction_leaf=0.0, 
max_depth=3, min_impurity_decrease=0.0, min_impurity_split=None, 
init=None, random_state=None, max_features=None, verbose=0, max_leaf_nodes=None, warm_start=False, presort=’auto’)

其中,几个比较的参数介绍如下:

·n_estimators,弱学习器的最大迭代次数,或者说最大的弱学习器的个数。一般来说,n_estimators太小,容易欠拟合;n_estimators太大,又容易过拟合。通常选择一个适中的数值,默认是100。

·learning_rate,每个弱学习器的权重缩减系数,也称作步长。

·loss,GBDT算法中的损失函数,建议使用deviance。

·max_depth,决策树最大深度,通常取值范围在10~100之间。

·max_features,划分时考虑的最大特征数,通常使用"sqrt"或者"auto"。

·random_state,随机数种子。

3.XGBoost

XGBoost在计算速度和准确率上,较GBDT有明显的提升。XGBoost的全称是eXtreme Gradient Boosting,它是Gradient Boosting Machine的一个C++实现,作者为正在华盛顿大学研究机器学习的陈天奇。XGBoost最大的特点在于,它能够自动利用CPU的多线程进行并行计算,同时在算法上加以改进,提高了精度。

XGBoost提供了Scikit-Learn接口风格的分类器xgboost.XGBClassifier。代码如下:


class xgboost.XGBClassifier(max_depth=3, learning_rate=0.1, n_estimators=100, silent=True, objective='binary:logistic', booster='gbtree', n_jobs=1, nthread=None, gamma=0, min_child_weight=1, max_delta_step=0, subsample=1, colsample_bytree=1, colsample_bylevel=1, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, base_score=0.5, random_state=0, seed=None, missing=None, **kwargs) 

其中,几个比较的参数介绍如下:

·max_depth,树的最大深度,典型值为3~10。

·learning_rate,学习速率,通常从1逐步减小优化。

·n_estimators,弱学习器的最大迭代次数,或者说最大的弱学习器的个数。一般来说,n_estimators太小,容易欠拟合;n_estimators太大,又容易过拟合。通常选择一个适中的数值,默认是100。

·booster,选择每次迭代的模型是基于树的模型gbtree还是线性模型gbliner,一般使用gbtree。

·nthread,线程数,默认值为最大可能的线程数。

·gamma,指定了节点分裂所需的最小损失函数下降值。这个参数的值越大,算法越保守。这个参数的值和损失函数息息相关,所以是需要调整的,默认为0。

·seed,随机数。

·min_child_weight,决定最小叶子节点样本权重和。这个参数用于避免过拟合。当它的值较大时,可以避免模型学习到局部的特殊样本。但是如果这个值过高,会导致欠拟合。

我们使用一个例子来介绍GBDT和XGBoost的使用。我们通过Scikit-Learn的函数随机创建样本数据,其中样本数量为1000,特征数为100,设置随机数为1,保证每次运行代码生成的数据相同,代码如下:


x, y = datasets.make_classification(n_samples=1000, n_features=100,n_redundant=0, random_state = 1)

创建一个KNN分类器,进行5折交叉验证,求其平均值:


knn = KNeighborsClassifier(n_neighbors=5)
score1 = cross_val_score(knn, x, y, cv=5, scoring='accuracy')
print(np.mean(score1))

创建一个GBDT分类器,进行5折交叉验证,求其平均值:


gbdt = GradientBoostingClassifier(n_estimators=100, learning_rate=1.0,max_depth = 1, random_state = 1)
score2 = cross_val_score(gbdt, x, y, cv=5, scoring='accuracy')
print(np.mean(score2))

创建一个XGBoost分类器,进行5折交叉验证,求其平均值:


xgboost = xgb.XGBClassifier()
score3 = cross_val_score(xgboost, x, y, cv=5, scoring='accuracy')
print(np.mean(score3))

运行程序,结果如下,GBDT和XGBoost的效果明显优于KNN,不过GBDT和XGBoost的参数还没有优化,这个结果还不能说明两者之间的差别。


0.703979074477
0.817111277782
0.861112077802

4.GBDT和XG Boost参数优化

GBDT和XGBoost都有众多参数可以优化,这里我们以GBDT为例,重点介绍参数对检测效果的影响。Scikit-Learn的GridSearchCV模块,能够在指定的范围内自动搜索具有不同超参数的不同模型组合,大大提高了我们的参数优化效率。GridSearchCV的定义如下:


class sklearn.model_selection.GridSearchCV(estimator, param_grid, scoring=None, fit_params=None, n_jobs=1, iid=True, refit=True, cv=None, verbose=0, pre_dispatch='2*n_jobs', error_score='raise', return_train_score=True)

其中,几个比较重要的参数介绍如下:

·estimator,指定需要优化的分类器。

·param_grid,需要优化的参数以及取值范围。

·scoring,衡量指标,常用的是accuracy,f1和roc_auc,详细列表可以参考相关文献 [2]

·cv,对应交叉验证方式,比如5代表5折交验。

我们先以优化n_estimators参数为例,介绍GridSearchCV的使用,数据集依然使用上例中使用的,代码如下:


x, y = datasets.make_classification(n_samples=1000, n_features=100, n_redundant=0, random_state=1)

假设n_estimators的取值范围是从50开始,步长为25,最大值为200:


parameters ={ 'n_estimators':range(50,200,25) }

创建GridSearchCV对象,分类器使用GBDT,衡量指标为accuracy,使用5折交叉验证:


gsearch = GridSearchCV(estimator=GradientBoostingClassifier(),
    param_grid=parameters, scoring='accuracy', iid=False, cv=5)
gsearch.fit(x, y)

输出accuracy最大的值以及对应的n_estimators取值:


print("gsearch.best_params_")
print(gsearch.best_params_)
print("gsearch.best_score_")
print(gsearch.best_score_)

运行程序,得到结果如下,当n_estimators取50时,accuracy达到最大值86.21%:


gsearch.best_params_
{'n_estimators': 50}
gsearch.best_score_
0.862097102428

如果希望查看参数变化时对应的accuracy取值情况,可以使用grid_scores_属性:


print(gsearch.grid_scores_)
[mean: 0.86210, std: 0.02909, params: {'n_estimators': 50}, 
mean: 0.86110, std: 0.03443, params: {'n_estimators': 75}, 
mean: 0.85910, std: 0.03469, params: {'n_estimators': 100}, 
mean: 0.85710, std: 0.03173, params: {'n_estimators': 125}, 
mean: 0.85910, std: 0.03106, params: {'n_estimators': 150}, 
mean: 0.85608, std: 0.02879, params: {'n_estimators': 175}]

我们现在考虑相对复杂点的情况,同时优化n_estimators和max_depth参数。我们定义n_estimators的取值范围为50~200,步长为25,max_depth的取值范围为2~10,步长为2:


parameters ={ 'n_estimators':range(50,200,25), 'max_depth':range(2,10,2)}

运行程序,得到结果如下,当n_estimators取75且max_depth取6时,accuracy达到最大值87.11%。


gsearch.best_params_
{'max_depth': 6, 'n_estimators': 75}
gsearch.best_score_
0.871112327808

GBDT参数众多,有兴趣的读者可以使用该方法进一步优化。

5.GBDT模型持久化

本套图书的前两本《Web安全之机器学习入门》和《Web安全之深度学习实战》中,我们介绍的方法都是把模型的训练和预测放在一个Python文件里面完成,这个在可行性验证和算法调优阶段是没问题的。在实际环境中,往往模型的训练以及模型的预测不在一台设备上,模型训练所使用的存储和计算资源往往非常大,需要使用大存储和GPU,但是使用模型进行预测往往计算和存储需求很小。如图8-9所示,以基于机器学习的杀毒软件为例,云端搜集了海量的恶意程序样本和正常文件样本,这个量级从几百G到几十T都有,处理如此大量的文件,机器学习算法会消耗大量的计算资源,通常为了加速训练的过程会使用GPU服务器。训练完成后把模型持久化成文件,这个模型文件的大小往往在几百K到几十M之间。终端杀毒软件下载该模型文件,在本地使用该模型对本地文件进行预测判断。那么在Python环境下如何实现模型的持久化呢?

Python的pickle模块实现了基本的数据序列和反序列化。通过pickle模块的序列化操作,我们能够将程序中运行的对象信息保存到文件中永久存储,通过pickle模块的反序列化操作,我们能够从文件中创建上一次程序保存的对象。使用pickle模块,我们也可以把机器学习的模型持久化成文件,并且可以通过加载该文件,还原出机器学习分类器,对本地数据进行预测。通常持久化的文件后缀为.pkl或者.pickle,使用joblib可以很方便地把模型保存成文件或者从文件中加载模型,代码如下:


from sklearn.externals import joblib
#保存模型
joblib.dump(gbdt, 'gbdt.pkl')
#加载模型
gbdt = joblib.load('gbdt.pkl')

6.使用GBDT进行恶意程序检测

通过之前的介绍,我们已经知道如何把一个PE转换成一个多维向量,我们定义一个类来完成这个功能:


feature_extractor =  PEFeatureExtractor()

图8-9 基于机器学习的终端杀毒架构

通过PEFeatureExtractor我们可以把一个PE文件转换成特征向量,其中PE文件保存在字节数组bytez中:


features = feature_extractor.extract( bytez )

从持久化文件中加载模型,并使用该模型对PE文件对应的特征向量进行预测,预测的结果是一个评分或者一个概率。


local_model = joblib.load('gbdt.pkl' )
score = local_model.predict_proba( features.reshape(1,-1) )[0,-1]

我们定义阈值local_model_threshold,如果超过阈值认为标签为1,反之为0。


local_model_threshold = 0.50
label = float( get_score_local(bytez) >= local_model_threshold )

需要特别说明的是,机器学习的库更新很快,旧版本训练生成的模型很有可能在加载新的库时会失败,对于这种情况要么基于新库重新生成模型,要么继续使用低版本库,常见的问题是Scikit-Learn的版本不兼容问题,对于需要下载旧版本库的可以参考网站https://pypi.python.org/pypi/scikit-learn/0.18.1

[1] http://www.cnblogs.com/LeftNotEasy/archive/2011/03/07/random-forest-and-gbdt.html

[2] http://scikit-learn.org/stable/modules/model_evaluation.html#scoring-parameter