恶意程序的检测过程是个典型的二分类问题,工业界和学术届经常使用的方法包括多层感知机(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