2.3.3 SVM 核函数
假如训练样本线性不可分,放到我们最开始的例子里面的意思就是:我们无法用一条直线将所有的样本正确分类为两类。如下面的“异或”问题:
图 6 异或问题
图 7 非线性映射
对于这种情况,我们可以将原始空间的训练样本映射到一个更高维度的特征空间,使得样本在这个特征空间内线性可分[9]。
令
表示将 x 映射到高维空间中的特征向量,那么对比前面线性可分的模型表示,同理可以得到相应的高维空间模型为:
同理可得:
同样可以得到对偶问题,如下:
映射后的特征空间维数可能会很高,会极大地加大我们训练时间和内存开销,直接计算
不一定可行。为了避开这个不稳定的区域,我们可以假设存在这样一个函数:
即在高维特征空间中映射向量的内积,与在原始样本空间中通过核函数
计算后的结果等效。这就是“核技巧”。上述式子可以改写为:
求解之后可以得到:
在线性不可分的情况中,核函数的选择是影响 SVM 模型的性能至关重要的因素。在明确特征映射的形式之前,我们并不知道具体选择哪一种核函数,而且核函数的选择也隐式的定义了特征空间。如果在模型的构建过程中选择了错误的核函数,那么由此建立的支持向量机模型的性能将会下降许多。
下列几种常用的核函数:
表 2-1 常用核函数表达式及其参数说明
此外,函数相互组合也可以得到核函数:
如果
和
都是核函数,那么对于任意正数
其线性组合:
也是一个核函数;
如果
和
都是核函数,那么核函数的直积:
也是一个核函数;
如果
是一个核函数,那么对于任意函数 g(x):
也是一个核函数。
3 具体方案设计
3.1 数据获取
目前主要的数据获取手段是通过互联网上的共享数据集,当前已经获得的数据集合有两个,其中一个用于初步测试,一个用于最终模型构建:
一个是来自罗马的一家通信科学研究所:Semeion Research Center of Sciences of Communication
该数据主要用于测试模型,数据特性如下:
图 8 钢板数据集属性
第二个数据集来自 GitHub 上一个 Fault Diagnosis 项目的自带的风力涡轮内部齿轮箱数据集。该 GitHub 项目地址为:Gearboxdata/Gear-Box-Fault-Diagnosis-Data-Set
该数据集内的数据分为两类,即正常运行数据和故障/异常状态下的数据。每一类数据下又按照 0-90 的不同载荷百分比,每 10 个百分点负载一个层次分为 10 种运行状态。合共 20 个文件,一共 2021119 条记录,每条记录包括载荷百分比在内一共 5 个属性。
图 9 风力涡轮齿轮箱数据集属性
这些数据虽然是连续的,但是经过一定的修改,比如固定为小数点后一位精度,这样可以很轻易的将其离散化,虽然对于精度有一定的影响,但是为了是的数据更为集中,不会出现一条数据记录就是一个分支的情况,离散化势在必行。而且后期新增了基于信息熵的离散化功能,可以很方便的进行区间划分从而提高预测的准确率。
数据整理我采用的是 C++ 编码来实现的,因为 C++ 对于数据读取有较好的速度支持,而且格式化能力也比较完善。主要的代码如下(GearData.cpp):
for (int i = 0; i < 10; ++i) { file="/Users/zhangzhaobo/Documents/Graduation-Design/Data/BrokenTooth Data/b30hz"+hz[i]+".txt"; ifstream in(file); while(in>>data[0]) { out<<setprecision(2)<<data[0]<<"\t\t"; for (int i = 1; i < 4; ++i) { in>>data[i]; out<<setprecision(2)<<data[i]<<"\t\t"; } out<<endl; } cout<<file<<" is done!"<<endl; in.close(); }
3.2 数据存取
如此大量的数据,采用文本读取这种方式很容易出现错误,所以结合数据库知识,最终选定了 MySQL 数据库作为工业大数据架构的数据存储层。
在安装好 MySQL 之后,建立 Graduation_Design 数据库,在其中建立了 gear 表格作为风力涡轮齿轮箱数据的存储表。表格信息如下:
mysql> show columns from gear;
图 11 齿轮箱数据存储格式
数据的存入与读取都是依赖于 Java 的一个外部包 mysql-connector-java.jar(JDBC),导入至本地项目后可以调用 JDBC 中的内置类,通过实例化一个数据库连接对象进行数据的存取。为了封装驱动,连接,会话等 JDBC 内容,新建了一个 Mysql_Connect 类提供数据库连接(Connect()),会话(getStatement()),断开连接(Dis_Connect())三个数据库常用的功能。
存储的过程中,由于数据量的问题,如果采用单条记录提交一次的方式进行两百万条数据的存储,那么一共需要两个小时,但是采用 JDBC 自带的批处理操作 Batch,可以将这个时间减少一半。具体操作如下:
String INSERT = getInsertQuery(id, Name, line); statement.addBatch(INSERT); id++; count++; //执行批量执行 if (count>40000) { statement.executeBatch(); count = 0; }
通过批处理操作,每一次与数据库的交互都能提交四万条数据,可以极大地减少 MySQL 连接,认证等的时间花销,提高存储效率。
而读取数据的时候,由于 Decision Tree 算法与 Support Vector Machine 算法需要的数据结构不同,所以定义了两个数据读取类,分别为:ReadData.java 与 SVMReadData.java,在 ReadData.java 中定义了静态方法 getSelectQuery()提供给所有需要生成查询语句的类调用。
另外每一个数据库读取类都提供了 readTrainData()和 readTestData()两类读取方式,提供给用户有选择的从数据库中读取出指定数量大小的数据记录。
数据读取的时候还需要一个Parameter类进行辅助,在这个类里面可以定义训练集与验证机的比例,训练集或者验证集的大小。每一个Parameter类内部定义了静态变量:训练样本数,测试样本数,以及二者之间的比例三个变量值。三者之间相互调节,并且因为静态变量的特性作用于全局。这样做的好处是不论在项目中定义多少个Parameter实例化对象,只需要在任意处调用调节方法进行修改,就可以直接作用于全局,保证不同方案之间的数据量一致性。
3.3 决策树实现
3.3.1 连续属性值离散化
因为获取的数据为浮点数表示的连续值,如果给每个取值开一个分支显然不可行,就算是将精度降低到0.1,那么五个属性延展开来最后还是会超过上亿种分支的可能,显然这个数量级对于及于决策树的分类问题是很不友好的,所以在进行模型构建之前需要进行数值离散化处理。
离散化常用方法为二分法。给定样本集 D 与连续属性 a,二分法试图找到一个划分点 t 将样本集 D 在属性 a 上分为 a ≤ t 与 a > t。但是这一点对于我们的数据规模和跨度不合适,所以采用一种基于熵的离散化方法[10]。
当频率(或概率)分布具有最大的属性值个数时,熵(或信息)被最大化[11]。当我们单独将某一个属性所有的值与对应的分类结果拿出来做一个单属性样本集X的时候,我们可以自定义若干个区间对属性进行区间分割,然后根据前面的信息熵的计算公式,计算出这个样本集的信息熵H(X)。另外根据凸函数的性质:
由参考文献中提到的离散化算法(EADC),可以将每个连续属性离散化为若干区间,区间数目由数据本身决定(但是最小区间划分数目为 10 个),之后找到合并两个区间后使得合并前后熵差最小,然后保存划分点,继续合并直到达到最佳平衡为止。是否达到最佳平衡的度量公式为:
式中,kmax 表示最大区间数,Hmax(p)表示最大熵值。在实际操作中,对其进行简化,取 40 个划分区间下的对应值。
连续属性离散化之前,决策树的数据精度设置为 1,正确率一直在 30% 左右;离散化后,数据精度设置为 0.1,正确率随着训练数据量增长而增长。训练数据量为 1000 条左右时正确率为 35% 左右,当数据量提升到 20000 条左右时,准确率有 53% 左右,而 SVM 数据因为不需要划分区间,所以正确率的增长与数据量的线性关系并不明显,从 1000 条数据到两万条数据仅仅波动了 1% 的正确率不到。可见决策树的正确率的提高,在初期更加依赖于训练数据量的积累。
整个离散化的过程如下:
从本地数据库读取设备运行记录数据,格式化后以实参形式传入到 EADC 离散化方法中;
在离散化方法中,针对单一的属性,取出其所有的值和分类数据,并且按照属性值进行排序;
排序后根据初始区间数划分区间,并且利用公式(2-2)的熵的计算公式,计算出改属性的初始熵,并且将度量数值 Ck 预设为 0 ;
先遍历整个区间集合,依次尝试合并两个相邻区间,记录每一次使合并前后的熵差,最后选择熵差最小的两个相邻区间进行合并,并且重置划分点,保存合并后的熵值;
根据上面的公式(3-2)提供的度量公式,计算出当前合并和后的度量值 Ck-1 = h;
如果 Ck-1 > Ck ,那么令 k = k - 1,跳回到第(4)步循环进行区间遍历、合并、计算;
如果 Ck-1 < Ck ,保存当前的区间划分,结束区间划分进程;
将最后的区间划分点返回至 EADC 方法调用者,由其对实际数据根据划分点进行离散化处理。
离散化流程图如下:
图 10 连续属性值离散化流程图
3.3.2 样本初始化
决策树的核心算法是 ID3 算法,其他的数据结构,数据处理等都是辅助内容,但是样本初始化在整个体系中也是举足轻重的。
首先定义属性名列表 attribute,也就是四个传感器的位置和负载百分比,然后是在此基础上增加一个分类属性名重新定义一个列表 attributr_Names,作为读取数据库内容时候的列名:
String[] attribute = new String[] {"Sensor1","Sensor2","Sensor3", "Sensor4", "Load"};
String[] attribute_Names = new String[] {"Sensor1","Sensor2","Sensor3","Sensor4","Load", "category"};
当属性列表定义完毕之后,就会进入 Decision Tree 算法的样本读取方法,readSample()。在这个方法中,通过在前面数据库模块定义的 ReadData 类中的 readTrainData()方法读取出数据之后进行样本整合。具体的操作为:
定义一个 Sample 类,内含属性名及其对应的属性值;
对读取出来的二维数组逐行读取,每一行定义为一个 Sample 实例;
按照所有实例的分类,定义与分类数目相同的链表,读取当前样本的分类,并且将当前 Sample 添加到相应的链表上去。如果没有这个分类,就新添加一条链表。
最后返回的数据结构为类别为键,包涵所有此类样本的链表为值的键值对 Map。
样本初始化整体流程如下:
图 12 样本初始化流程图
3.3.3 生成决策树
在样本初始化完毕后,就进入生成决策树的阶段。调用定义的generateDecisionTree()方法,传入样本集和属性列表。就可以得到一颗基于此样本集,用ID3算法构建的故障树了。
//生成决策树
Object decisionTree = generateDecisionTree(samples,attribute);
在这个方法当中,ID3算法担任了求出信息增益最大的属性的责任。整个generateDecisionTree()方法采用递归的方式,不断地向下延伸分支,直到将当前节点归类为叶节点才会停止。 流程解释如下;
(1) 判断是否当前分支的样本数目,如果为空或者分类只有一种,那么将当前样本的类别作为叶节点的分类;
(2) 如果属性用完,或者是同一个属性值对应的样本子集中无法通过数目将此节点归类,那么采用后验分布进行归类;
(3) 如果上述条件都不满足,那么就使用 ID3 算法计算出当前的分支属性,并且读取该分支属性的各个属性值构成分支向下延伸(进入递归),直到遇到上述的条件成为叶节点为止;
(4) 当满足生成叶节点的条件时,则递归止步于当前节点,当前节点作为叶节点返回至其父节点,直至抵达根节点为止。
整体流程如下:
图13 决策树生成流程示意图
至此,整个决策树就已经训练完毕了。我们从这一系列的操作中得到了一个 Tree 类的实例对象。每一个 Tree 都是由一个根节点和一系列的分支组成,除了叶节点不是 Tree 类型外,其他的子节点都是 Tree 类型。这一点也为我们提供了一个良好的搜索环境。只要检测当前节点是否为 Tree 类型,就可以判定是否已经搜索到了子节点了,这一特性在后面的数据测试中将会用到。
而在上面生成决策树时用到的 ID3 方法,下面进行详细的解释:
传入一个样本集,一个属性列表;
样本集形式为分类与此分类对应的所有样本,注意此处为分类而不是属性值;
对每一个属性值进行信息增益的计算,具体的实现方式为:
读取当前属性值,拿到一个键值对,【所属类别--> 样本集】
解析键值对,分解出 key 和 value,其中 key 为类别,value 为此类别所有的样本
对于 Value 里边读出来的每个样本,分别读取当前属性下的值,然后建立起来当前属性值相同的所有样本的样本集;
建立起了所有属性值对应的样本子集后,再在此样本子集的基础上按照分类的不同进行子集划分,相当于是二次划分样本集;
最终得到的数据结构如下:
图 14 ID3 信息增益计算的数据结构
根据上面的数据结构,计算每一个属性值的信息熵,然后结合所有的属性值计算出这个属性所对应的信息增益,最后得到信息增益最大的那个属性,即可将此属性的下标,对应的信息增益以及由这个属性所衍生出来的样本集打包成一个数组返回。
至此,ID3 算法执行完毕,返回一个数组供 generateDecisionTree()方法选择下一个分支的属性,并且递归调用自身产生子树分支。