基于 Java 机器学习自学笔记 (第66至68天:主动学习之ALEC)

注意:本篇为50天后的Java自学笔记扩充,内容不再是基础数据结构内容而是机器学习中的各种经典算法。这部分博客更侧重于笔记以方便自己的理解,自我知识的输出明显减少,若有错误欢迎指正!


目录

前言

一、关于学习的分类

· 机器学习的几个层次

1.1 监督学习

1.2 半监督学习

1.3 主动学习

1.4 无监督学习

二、ALEC主动学习理论的一些关键问题

2.1 关于Density Peak的聚类

· Gaussian优化的Density Peak密度计算

2.2 Master Tree 与 ALEC基本思路

2.3 并查集(Union-Find)

三、算法逻辑与代码

3.1 成员变量与构造函数

3.2 一系列辅助函数一览

3.3 Master树的建立

3.4 二分分簇 

3.5 ALEC 主体代码

四、数据测试( 主要测试查询上限对于数据识别率影响问题 )


前言

        ALEC是老师@闵帆在机器学习的Active Learning领域17年方面发表的一篇论文,下面有这篇论文详情的资料链接:

Min Wang, Fan Min, Yan-Xue Wu, Zhi-Heng Zhang, Active learning through density clustering, Expert Systems with Applications 85 (2017) 305–317.(论文编号 31.)
publicationshttp://www.fansmale.com/publications.html

        文章中部分理解源于自老师的讲座中有关内容(博客链接:主动学习: 从三支决策到代价敏感_闵帆的博客-CSDN博客

一、关于学习的分类

· 机器学习的几个层次

  1. 场景(scenario) :并不具体到问题的输入输出,即不用给出定义的输入输出的数据结构,是一种学习的基本思路与方向,常见的有监督学习、半监督学习、主动学习、无监督学习等 。
  2. 问题(problem) :有确定输入、输出以及优化的目标和约束条件
  3. 算法(algorithm) :算法层面设计步骤,是对于特定场景下,特定的某个机器学习方法对于特定问题求解步骤的一种描述
  4. 应用(application) :将机器学习的全过程在实际环境中的实践于结合

1.1 监督学习

        监督学习的思维过程并不难理解,前几天完成许多学习算法都能实现这种基本的学习方法。简单来说,首先通过已知标签的\(N\)个数据进行训练,从而学习到这些标签与数据之间潜在的联系,即得到一种学习模型。这种模型可以是NB算法中的条件概率存储,也可以是ID3决策树中的树形存储结构,或者是一个AdaBoost集成器中的分类器数组。然后通过学习得到的模型,对于接下来可能给出的全新的\(k\)个未知标签的数据进行预测。

        这个过程中,最初的\(N\)个数据因为全部知道了标签,故全部学习了。机器仿佛就像高中时期的学生们,数据中的标签就像课本中的死知识,你在老师的每天的监督中完成了\(N\)个预定的,基本要求的学习,后之后对于\(k\)个内容的预测仿佛就如同考试,是对于学习内容的检验。

        监督学习中基于标签为离散字符信息还是具体数值而分为分类问题回归问题

  • 分类问题比如学习iris数据集从而对于后续得到的数据进行判断,判断属于哪一类花。这类花之前是有明确定义存在的。
  • 回归问题就类似于同个前几日气温变化的分析,从而学习得到明天的气温可能的取值,这个值可能是之前所没有遇到过的。
监督学习(supervised learning)

1.2 半监督学习

        半监督学习中,若我们当前总共有\(N+P\)个数据,但是其中仅有那\(N\)个数据存在标签,\(P\)个无标签。我们依旧对于这全部的\(N\)个数据进行学习,然后建立起模型,并且对接下来未知的\(k\)个数据进行分类。

        这里不同的地方出现了:还是那高中生活举例子,我们要学习的内容变多了,不仅仅是要在老师的监督下学习\(N\)个死知识(泛指有标签的数据),同时还要依靠自学完成\(P\)个知识(泛指已经数据但是不知道标签的数据)。虽然这个例子有些牵强,但是这也引出了半监督学习中我们所关心的问题,即学习额外的没有标签的\(P\)对于模型的准确度与健壮性构建有用吗?——有用!

        虽然额外的\(P\)个数据没法提供有效的标签支持,但是其提供了有效的数据分布。对于同样的具有\(N\)个含标签数据的数据集,附加使用\(P\)个无标签算法进行半监督学习的准确度要高于只采用\(N\)个带标签的监督学习。结合例子你可以认为,只是老师讲的东西你听懂了还不足以考试得高分,你还需自己多练习。半监督学习是良好的,因为我们现实问题当中,一方面是由于人工标记样本的成本很高,导致有标签的数据十分稀少;而另一方面,无标签的数据很容易被收集到,其数量往往是有标签样本的上百倍。

        半监督学习中对于非标签数据集的操作不同可进一步分为自我训练(self-training)与直推学习(transductive learning)

自我训练(self-training)​​​​
  • self-training可能是最早提出的半监督学习方法,最早可以追溯到1965。这个算法在有标签的数据上训练得到基本的模型,然后对未标注数据进行预测,取预测置信度最高的样本直接对其进行标签定义,从而更新模型,如此反复,直到模型的预测结果不再发生变化。这非常直观体现了未标签的\(P\)个数据对于我们模型建立的贡献,这种半监督学习又叫做纯(pure)半监督学习,因为这里未标记数据的确对未知的\(k\)个数据的分类起到了帮助。
直推学习(transductive learning)
  • transductive learning就是将未标签的环境直接作为我们分类的对象,即\(k=P\)。但是要注意,transductive learning实际上属于另一个更大的概念,它和归纳学习(Inductive Learning)属于两种相对的学派。

1.3 主动学习

        主动学习其实是半监督学习的一个子类。给你\(M\)个数据但是都没有标签,但是此刻机器有一个机会,机器可以主动选择这其中\(N\)个数据交给专家去打标签,然后后续就非常类似于半监督学习了,机器会根据这\(N\)个标签去建立模型。但是后续进行数据分类时,根据分类数据不同又分为了:

  • 封闭环境(close world),对剩余的\(P\)个样本进行分类
  • 开放环境(open world),对于全新给出的\(k\)个外界样本进行分类
主动学习(active learning)

        主动学习过程中对于标签的询问并不是乱问的,往往我们可能会考虑“ 不确定性 ”的标签去询问,或者通过先前对于\(M\)个数据的内部清理,从而筛选出了若干“ 有代表性 ”的问题去询问。

        另外这里的专家标记可能不是依次就标记了\(N\)个,这个过程可能是一个循环往复的过程,会多次咨询专家并且多次去学习。最终使得人类的经验知识越来越丰富,模型的泛化性能也越来越好,人机交互,各自获得较好的收益。

1.4 无监督学习

        无监督学习是最好理解的,之前讲KMeans的时候提过一嘴。所谓的无监督学习的典型代表就是聚类,最开始的数据没有任何的标签,只能通过我们对于数据的特性进行反复地分簇与中心点复置来尽可能得到可靠的分簇从而推导出可能的标签。这个不过多解释,具体可以参考我们的Kmeans介绍。   

二、ALEC主动学习理论的一些关键问题

        主动学习的关键问题就在选择合适的数据点作为区域信息的核心,这样的核心就是主动学习理论中那些“ 用于询问专业人士 ”的“ 具有代表性的数据 ”。当然,实际的一些已知标签的环境下,我们仍然可以利用这种思维,去找一些区域代表的核心数据的标签来代表这个区域,这就是ALEC的思路。那么怎么选取“ 核心 ”呢?——使用聚类,可以用KMeans,但是ALEC具体采用的策略是基于Density Peak的聚类选取方案。

2.1 关于Density Peak的聚类

        Density Peak的理论源于14年在Nature上发表的论文“Clustering by fast search and find of density peaks”,这篇文章提到的基于Density Peak的聚类方案对于某些环境下的聚类效果堪称恐怖,是一个非常强大的聚类算法。

        Density Peak的度量方法大致如下(下面以二维平面表征,推广到更高维):

  • 以样本\(x\)为中心,\(d_c\)为半径可以得到一个圆域,任何一个落入这个圆域的点都被纳入样本\(x\)的统计中,最终确定落入圆域中点的密度:

  •  所有点集都能以\(d_c\)为大小确定自己的圆域并且统计密度值Density,然后确定一个样本\(x\)及其密度,若先有一个样本\(y\),这个样本的密度值要高于\(x\),同时距离\(x\)最近。取这个距离为\(l\)。(下图中有三个点密度高于\(x\),但是只有6号点距离自己最近)

         现在我们定义,样本\(x\)的密度代表了这个点的重要性(importance),而距离\(l\)代表了样本\(x\)的独立性(independence),而这两个的乘积是样本\(x\)的代表性(representative)。

        为什么能如此定义呢?首先密度确实能很形象表示这个点周围的散点密度,同时也能说明这个点的周围信息,体现周围点的意志,可以用其表征重要性。而独立性是样本点\(x\)到另一个最近的密度比自己大的点集\(y\)之间的距离,若这个距离比较大,那么就可以说当前\(x\)不容易收到那些密度比自己大的点的影响,具有极强独立性。而重要性越强,自身越独立,那么这个点就能很好代表这个区域,即代表性。

· Gaussian优化的Density Peak密度计算

        Density Peak中密度的常规计算就是数样本\(x\)圆域内点的个数,但是除此之外其实还有一个更加优秀的优化策略,这个策略不再将密度定义为范围中散点的统计,而是上升为对于全体点集权值的度量。我们将一个样本\(x_i\)的密度定义为\(\rho_{i}\),有:\[\rho_{i}=\sum_{j \neq i} e^{-\left(\frac{d_{i j}}{d_{c}}\right)^{2}} \tag{1}\]        这个式子最鲜明的特点就在于其并没有抛弃以\(d_c\)为半径的圆外的数据,而是统一地将其纳入\(d_{ij}\)计算,只不过它们的影响非常小。

        假定\(k = \left(\frac{d_{i j}}{d_{c}}\right)^{2}\),当在圆域内时,\(d_{ij}<d_c\),\(0<k<1\),这时无论\(d_{ij}\)怎么变化,其都稳定在\([\frac{1}{e},1]\)很小范围,是可观的一个值;而\(d_{ij}>d_c\)时,\(k>1\),这个值可以扩展到1的若干倍,其值域是\((0,1)\),而且会随着数据距离圆域越远,这个\(k\)越大,从而指数函数也越趋近于0,得到的值不再是一个可观的值,对于总体\(\rho_{i}\)影响更小。

         这种优化是足够地好以至于人们后续使用Density Peak时普遍采用此公式计算密度。

2.2 Master Tree 与 ALEC基本思路

        为了方便后续的进行分簇的操作与体现Density Peak独立性的一些特征,我们需要建立一个非常特殊的树形结构:

         在这个树种满足:

  1. 每个结点表示一个数据行,即结点\(x_i\)表示第\(i\)行数据的内容
  2. 每个结点的父辈们的密度(重要性)都要大于自身
  3. 每个结点的父节点是所有密度大于自身的结点中距离自己最近的结点,即当前结点到父节点的距离代表着此结点的独立性
  4. 每个结点到父节点的距离乘上自身的密度等于此结点的代表性
  5. 根结点到父节点的距离为无限大,故代表性也最大

        满足上面这些特征的树本算法将其定义为Master树。那么ALEC算法是如何利用这颗树来进行主动学习的查询结点选定与标签的预测的呢?总结为3个步骤和2个出口

  1. 对于有\(n\)个结点的树结构,分别决定将代表性排前位的\(\sqrt n\)个结点进行查阅 (这就是主动学习中的所谓“ 交给专家识别 ”的部分),但是在查阅之前先进行两个并集判断,若这两个判断同时满足,那么就不用查阅进行投票(见下述)并退出当前树。否则继续进入第2步。
    1. 当前\(n\)的大小是否已经超过我们设定的最低底线
    2. 当前\(n\)个结点的树中已经存在有大于等于\(\sqrt n\)个结点已经被查阅过了
  2. 对于代表性排前位的\(\sqrt n\)个结点进行查阅,同时观察查阅的所有点是否标签一致(Pure?),若一致(Pure!),算法便对未查阅的点进行大胆预测——它们的标签与查阅结点的标签一致!于是此树预测完毕,退出当前树。若不同,则进入第3步。

  3. 在第三步中,我们选择这颗树中代表性最大的两个点为基础分别作为两个根,并且以这个根生成两个全新的子树,然后对于每颗子树进行步骤1,也就是分别递归地进入每颗子树进行上述操作的循环。在这个过程中,之前查阅的结果会继承并且影响。

        上述提到的树递归出口我用紫色文字表示了,此外当前我们算法执行过程中我们能查阅的次数是有限的,任何时刻如果我们查阅到上限时,无论当前子树中是否pure了,我们都应该结束并且进行投票(就是第一个出口)

        而投票是一种妥协政策,其目的就是在非pure结束时,给未预测结点预测一个当前子树中已查阅的结点中最多的标签。

        最后说明一下,为什么树递归结点小于一定数量时,算法就不再分配给当前递归子树部分查阅功能了呢?因为“ 查询 ”作为一种很宝贵的资源(人力查询是有限些花费时间的),我们尽可能要保证最多的查询操作适用于最广泛的邻域,要确保查询的广度而非深度。在子树结点很少时,简单估计一个可能的标签所造成的结点预测失误率要低于结点众多的一颗子树因为没有查询份额而无奈乱投票所导致的大量结点盲目确定标签所导致的失误率。

2.3 并查集(Union-Find)

       上述ALEC的三个步骤中的第三步的生成子树中“ 生成 ”俩字说的轻巧,其实这里数据二分的时候有非常明显的并查集(Union-Find)思想,为了方便后面3.4节对于二分分簇的理解,我先将并查集的思想和代码特征在这里先简单描述下。

        并查集思想是通过将多个点集与某个中心点建立联系,从而将原本离散的点集进行合并为新的集合的操作。当然这个定义是我的经验之谈,多有不严谨之处,但是大概能基本囊括其作用。

        并查集的应用非常广泛,相比于算法,其更像一种工具。对于每个参加过代码程序竞赛的同学们来说,都能体会到并查集这种工具的方便,因为它功能强大且实现代码非常地简练,可以广泛用于树形问题,图问题,MST问题,甚至一些抽象问题。在Kruskal算法连接边的时候就可以采用并查集避免连环;在判断连通域的时候,并查集也可以抽象地表示每个结点所属的连通域,从而确定连通域的情况。

        并查集有两大思想:

  1. 找源
  2. 并源

        并查集中有一个关键数组f,其可以指明某个结点的父级,就像双亲表示法那样,例如f[i] = k就是将结点i的父级设置为k。这里我们定义f[i] = i是一个孤点或者这个点已经没有父级了。往往在很多题目中,最初都定义为f[i] = i(类似于下图),或者定义f[i] = -1这样的非法值,这类操作统称为并查集的初始化

         后续通过题目的不同要求亦或是何种需求,需要我们将这些结点相互连接。方法也很简单,任意结点i与结点k如果需要连接,只需要协商确定谁为父亲,比如这里如果i是父亲那么就设置f[k]=i。继续构建情景,最初显然f = {0,1,2,3,4,5,6},假设现在f被我们合并为f = {0,5,5,5,3,5,4},那么行的树状图为(因为这种表示和双亲表示法一致,所以最终图是一棵树):

        1.并查集思想之一 —— 找源(Find Source)

        虽然图中的\(x_4\)、\(x_6\)都知道自己的父级,但是并不知道自己的源头是谁(源头是一颗树树最顶上的根),知道父级也许对于一颗树形结构足矣了,但是对于并查集并不满足。并查集需要用一次性地确定某点是否处于同个集合,采用的策略是将诸结点的源头定义为最大“ 共识 ”,同属一个“ 共识 ”的所有结点为一个集合。策略:

        若f[k] = i,但是f[i] ≠ i,那么f[k]的值可以从i更改为f[i];但是若f[i] = p,但是f[p]  ≠ p,那么我们又要事先将f[i]从p改为f[p];但是若......(略) 发现了吗?这是一个递归。加入我要教材料给学校,但是我不知道给哪个部门,于是我问宿舍长,宿舍长也不知道于是问班长,班长也不知道,于是问......(略)。可以非常简易的代码实现:

int getf(int v){
    if(f[v]  = v){
        return v;
    }else{
        f[v] = getf(f[v]);
        return f[v];
    }// Of if
} // Of getf

        于是我们的图整理为如下的样子。从树形整理成集合,怎么判断一个结点是否属于某个集合呢?只需要通过f[ ]数组判断就好,只要你我f[ ]一样,那么我们俩就是同一个集合。然后遍历f[ ]取出f[ ]实际的取值个数就可以得到集合的个数。(这个思路可以用于计算图的连通分量)

        最终被并查集调整之后的树形结构如下图,树高最多只有两层,除了根结点之外所有结点都是叶子或者没有其他结点。为了后续方便,我们简单称之为为并查集树(Union-Find Tree)

         2.并查集思想之二 —— 并源(Merge Source)

        懂得找源的思想,并源就很简单了,并源是将两个集合合并为一个集合的操作,简单来说只要找集合A的代表与集合B的代表,让B隶属A或者让A隶属于B,这样就可将它们整合在一起。例如我这个部落首领是黄帝,你们首领是炎帝,你我不相往来兵戎相见,但是某次炎帝和黄帝私下确定盟约,认为大家应该团结一致,最终炎帝宣誓作为黄帝附庸,于是两个部落何为一体(这就是炎黄联盟,哈哈,我们现在都属于这个联盟哟,给大家科普了些历史小知识)

void merge(int v, int u){
    int t1, t2;
    t1 = getf(v);
    t2 = getf(u);
    if(t1 != t2){
        f[t2] = t1;
    }// Of if
    return;
} // Of merge

         每个孤单点其实都可以看做一个小集合,因此之前提到的\(x_i\)与\(x_j\)的合并可以从f[k]=i修改为merge(i,k),这个代码会更好些,省去一些调整。

        最后提一下,上面的代码只并查集的基础代码,有些基本的优化我没写(如果不优化控制,有时并查集树并不能保证层数稳定在≤2层),毕竟只是并查集只是为ALEC铺垫而已,所以只明白含义即可。

三、算法逻辑与代码

3.1 成员变量与构造函数

    /**
	 * The whole dataset.
	 */
	Instances dataset;

	/**
	 * The maximal number of queries that can be provided.
	 */
	int maxNumQuery;

	/**
	 * The actual number of queries.
	 */
	int numQuery;

	/**
	 * The radius, also dc in the paper. It is employed for density computation.
	 */
	double radius;

	/**
	 * The densities of instances, also rho in the paper.
	 */
	double[] densities;
  1. 数据集
  2. maxNumQuery 对于当前数据集中,最多可查询的个数上限阈值,即主动学习中“ 专家 ”给机器的查询份额
  3. numQuery 实际查询个数,初始为0,每次查询便令numQuery++,当numQuery ≥ maxNumQuery
  4. radius 查询半径,即我们之前所描述的\(d_c\)
  5. densities[ ] 密度数组,即记录每个样本\(x_i\)的密度
    /**
	 * distanceToMaster
	 */
	double[] distanceToMaster;

	/**
	 * Sorted indices, where the first element indicates the instance with the
	 * biggest density.
	 */
	int[] descendantDensities;

	/**
	 * Priority
	 */
	double[] priority;

	/**
	 * The maximal distance between any pair of points.
	 */
	double maximalDistance;

	/**
	 * Who is my master?
	 */
	int[] masters;

	/**
	 * Predicted labels.
	 */
	int[] predictedLabels;

	/**
	 * Instance status. 0 for unprocessed, 1 for queried, 2 for classified.
	 */
	int[] instanceStatusArray;

	/**
	 * The descendant indices to show the representativeness of instances in a
	 * descendant order.
	 */
	int[] descendantRepresentatives;

	/**
	 * Indicate the cluster of each instance. It is only used in
	 * clusterInTwo(int[]);
	 */
	int[] clusterIndices;

	/**
	 * Blocks with size no more than this threshold should not be split further.
	 */
	int smallBlockThreshold = 3;
  1. distanceToMaster[ ] 记录当前结点到父级的距离
  2. descendantDensities[ ] 基于每个结点的密度将每个结点降序排序后,其下标对应的数组。假设当前descendantDensities[0] = 568,说明当前所有结点中第568号结点的密度densities[568]最大;descendantDensities[2] = 7,说明当前所有结点中第7号结点的密度densities[7]第三大。
  3. priority[ ] 这个数据数组存放的就是每个样本\(x_i\)的代表性(重要性 * 独立性)
  4. maximalDistance 通过遍历每个点之间的距离关系得到的当前图中最大的距离,可以保证所有结点之间的距离无法超过这个值,而每个子父级之间的距离distanceToMaster更不可能超过这个值,因此其是一个有效的可以用于求最小值的非法数据。
  5. masters[ ] 使用树的双亲表示法记录当前结点的父亲的数组(类似于并查集)
  6. predictedLabels[ ] 对于每个样本\(x_i\)预测一个Label,通过这个数组存储每个预测的标签ID
  7. instanceStatusArray[ ] 是一个状态标记器,类似于DFS的visited[],统计每个样本\(x_i\)的标记状态以方便操作:
    1. 当值为0是表示未处理
    2. 当值为1是表示已经查询
    3. 当值为2是表示已经分类
  8. descendantRepresentatives[ ] 类似于descendantDensities[ ],只不过其是针对代表性降序排列得到而非密度,假设当前descendantRepresentatives[0] = 99,说明当前所有结点中第99号结点的代表性priority[99]最大
  9. clusterIndices[ ] 之前在讲述分簇时我们已经涉及过此类数组的解释,用于统计不同样本\(x_i\)所属的簇。例如clusterIndeices[i] = j,说明样本\(x_i\)最终被分到了第\(j\)号簇中。同时,值得一提的是,簇内所有结点都指向某个中心结点的“ 分簇树(Cluster Tree) ”与2.4部分假定的“ 并查集树(Union-Find Tree) ”结构是一致的,这也就是为什么本算法能将并查集的思想用于分簇中。此外,因为ALEC使用是二分分簇,因此这里的\(k\)只有0/1两种情况(结点编号 = 此结点所代表的数据行在数据集中的下标

  10. smallBlockThreshold 控制分簇的最小阈值,当簇内元素小于这个值时,哪怕有不同的标签都不再分簇了。

        关于master与distanceToMaster的表示,下面用一个图更详细解释:

        当前若是根结点了(或者单纯地没有父级),那么其到父级的距离就是无限大(这里用非法值表示),并且其父级就是自身。 

	/**
	 ********************************** 
	 * The constructor.
	 * 
	 * @param paraFilename
	 *            The data filename.
	 ********************************** 
	 */
	public Alec(String paraFilename) {
		try {
			FileReader tempReader = new FileReader(paraFilename);
			dataset = new Instances(tempReader);
			dataset.setClassIndex(dataset.numAttributes() - 1);
			tempReader.close();
		} catch (Exception ee) {
			System.out.println(ee);
			System.exit(0);
		} // Of fry
		computeMaximalDistance();
		clusterIndices = new int[dataset.numInstances()];
	}// Of the constructor

	/**
	 ********************************** 
	 * Compute the maximal distance. The result is stored in a member variable.
	 ********************************** 
	 */
	public void computeMaximalDistance() {
		maximalDistance = 0;
		double tempDistance;
		for (int i = 0; i < dataset.numInstances(); i++) {
			for (int j = 0; j < dataset.numInstances(); j++) {
				tempDistance = distance(i, j);
				if (maximalDistance < tempDistance) {
					maximalDistance = tempDistance;
				} // Of if
			} // Of for j
		} // Of for i

		System.out.println("maximalDistance = " + maximalDistance);
	}// Of computeMaximalDistance

        构造函数完成基本的文件读数据、clusterIndices初始化,决策条件属性列的定义以及计算图中的最大距离等操作(最大距离的作用刚刚已经提到了)。distance( )函数采用的是欧式距离,这里就不再专门列出代码了。

        如果最开始就对于每个向量的值范围进行了[0,1]的归一化,那么理论向量最大间距就确定为\((0,0,...,0)_{m}\)与\((1,1,...,1)_{m}\)之间的距离,就没必要如此麻烦地全部遍历找最大。否则每次构造对象时都会有\(O(MN^{2})\)的开销,还是非常讨厌的一个累赘。

3.2 一系列辅助函数一览

        计算Density Peak我们采用的Gaussian Density的计算方案,参考公式1即可完成代码

	/**
	 ****************** 
	 * Compute the densities using Gaussian kernel.
	 * 
	 * @param paraBlock
	 *            The given block.
	 ****************** 
	 */
	public void computeDensitiesGaussian() {
		System.out.println("radius = " + radius);
		densities = new double[dataset.numInstances()];
		double tempDistance;

		for (int i = 0; i < dataset.numInstances(); i++) {
			for (int j = 0; j < dataset.numInstances(); j++) {
				tempDistance = distance(i, j);
				densities[i] += Math.exp(-tempDistance * tempDistance / radius / radius);
			} // Of for j
		} // Of for i

		System.out.println("The densities are " + Arrays.toString(densities) + "\r\n");
	}// Of computeDensitiesGaussian

        因为后续代码中需要从密度、代表性从高到低的依次遍历结点,因此需要对于密度、代表性数组进行降序排序。但是排序应当生成的是数据集的下标,只有这样这样的数组才是可利用的信息。所以需要将初始状态下的数组下标同数值绑定,然后等数值排序完毕之后利用原下标替换数值。这里老师专门写了一个归并排序,我为了方便就自建一个二元组然后用Java的排序库帮忙吧,稍微偷偷懒。

	/**
	 * A class to describe a pair
	 * 
	 * @author ALBlack
	 */
	public class MyPair {
		/**
		 * Key of this Pair.
		 */
		private int key;

		/**
		 * A Getter
		 */
		public int getKey() {
			return key;
		}// Of getkey

		/**
		 * Value of this this Pair.
		 */
		private double value;

		/**
		 * A Getter
		 */
		public double getValue() {
			return value;
		}// Of getValue

		/**
		 * A constructor
		 */
		public MyPair(int key, double value) {
			this.key = key;
			this.value = value;
		}// Of the constructor
	}// Of class MyPair

	/**
	 ********************************** 
	 * sort in descendant order to obtain an index array.
	 * 
	 * @param paraArray the original array
	 * @return The sorted indices.
	 ********************************** 
	 */
	public int[] sortToIndices(double[] paraArrayPair) {
		List<MyPair> arrayPair = new ArrayList<>();
		int[] resultArray = new int[paraArrayPair.length];

		for (int i = 0; i < paraArrayPair.length; i++) {
			arrayPair.add(new MyPair(i, paraArrayPair[i]));
		} // for i
		Collections.sort(arrayPair, new Comparator<MyPair>() {
			@Override
			public int compare(MyPair p1, MyPair p2) {
				if(p2.getValue() > p1.getValue()) {
					return 1;
				}else if(p2.getValue() < p1.getValue()) {
					return -1;
				}else {
					return 0;
				}// Of if
			};
		});
		for (int i = 0; i < paraArrayPair.length; i++) {
			resultArray[i] = arrayPair.get(i).getKey();
		} // for i
		return resultArray;
	}// Of sortToIndices

        代表性计算(通过之前介绍Density Peak可知代表性 = 重要性*独立性),而master树中,每个结点上方都所有父辈的重要性都比自己大,而父节点又是距离自己最近的,因此样本\(x_i\)到父亲的距离刚好符号独立性的定义。

	/**
	 ********************************** 
	 * Compute priority. Element with higher priority is more likely to be selected
	 * as a cluster center. Now it is rho * distanceToMaster. It can also be
	 * rho^alpha * distanceToMaster.
	 ********************************** 
	 */
	public void computePriority() {
		priority = new double[dataset.numInstances()];
		for (int i = 0; i < dataset.numInstances(); i++) {
			priority[i] = densities[i] * distanceToMaster[i];
		} // Of for i
	}// Of computePriority

3.3 Master树的建立

        之前在概念中已经确定了Master树的特征,即父辈结点的密度(重要性)总是要大于子节点,并且相邻的父亲与儿子总是距离最近。

        在这样的思路引导下,我们采用的策略是将所有结点按照重要性从高到低排序得到关于结点的重要性的数组,这时在数组中任意取得一个中间节点\(x_i\),这时其左边\(L_{pre}\)中任何结点密度(重要性)都要大于它,右边\(L_{post}\)都小于它。

         此刻只需要遍历数组\(L_{pre}\),找到距离\(x_i\)最小的结点\(x_j\),并且确定\(x_i\)的父节点为\(x_j\)。这个方法要注意一个细节,\(x_0\)没有前驱数组\(L_{pre}\),所以\(x_0\)就是密度最大的结点,按照我们定义变量是确定的规则,\(x_0\)的父辈就是其自身:0(刚好初始化时所有数组都是0,因此不操作即可),\(x_0\)到父辈的距离就是无穷大(非法不可及值maximalDistance)
        代码如下:

	/**
	 ********************************** 
	 * Compute distanceToMaster, the distance to its master.
	 ********************************** 
	 */
	public void computeDistanceToMaster() {
		distanceToMaster = new double[dataset.numInstances()];
		masters = new int[dataset.numInstances()];
		instanceStatusArray = new int[dataset.numInstances()];

		descendantDensities = sortToIndices(densities);
		distanceToMaster[descendantDensities[0]] = maximalDistance;

		double tempDistance;
		for (int i = 1; i < dataset.numInstances(); i++) {
			// Initialize.
			distanceToMaster[descendantDensities[i]] = maximalDistance;
			for (int j = 0; j <= i - 1; j++) {
				tempDistance = distance(descendantDensities[i], descendantDensities[j]);
				if (distanceToMaster[descendantDensities[i]] > tempDistance) {
					distanceToMaster[descendantDensities[i]] = tempDistance;
					masters[descendantDensities[i]] = descendantDensities[j];
				} // Of if
			} // Of for j
		} // Of for i
		System.out.println("First compute, masters = " + Arrays.toString(masters));
		System.out.println("descendantDensities = " + Arrays.toString(descendantDensities));
	}// Of computeDistanceToMaster

3.4 二分分簇 

        在讲二分分簇之前先确保明白了上述并查集与Master树的概念,ok?那么继续吧。

        二分分簇本身简单来说,就是通过Master树为辅助而构建一颗Cluster Tree的过程,同时也可以理解为以Master树为蓝本进行并查集的过程

        首先通过masters[ ]与clusterIndices[ ]两个数组,我们可以得到两个树(如下图),但是master是我们通过之前具体的连接构成的一颗实实在在的树(采用双亲表示法)。但是clusterIndices的树形尚未构造,因为clusterIndices需要动两个手脚——1.令所有结点独立,即不连接任何父级;2.选定代表性最大的两个结点,令其clusterIndices值分别为0与1(代码中paraBlock[ ]与descendantRepresentatives[ ]完全一样,只不过descendantRepresentatives[ ]代表全局,而paraBlock[ ] 只代表局部,所以\(x_{paraBlock[0]}\)与\(x_{paraBlock[1]}\)分别表示代表性最大的两个点

        // Reinitialize. In fact, only instances in the given block is
		// considered.
		Arrays.fill(clusterIndices, -1);

		// Initialize the cluster number of the two roots.
		for (int i = 0; i < 2; i++) {
			clusterIndices[paraBlock[i]] = i;
		} // Of for i

         上述代码的初始化令Cluster Tree中构造出了两个孤立簇,然后借助Master树的结构与并查集 查源 的方法,将无簇的孤点引导到对应源头。反过来想,选定了簇的结点可以其子代的传承,将簇的信息传播给它们的每个儿子、孙子、曾孙...当然再次强调,这都必须要Master Tree的父子关系引导下进行并查集的寻源操作而得到!

        试着模拟,上图clusterIndices[8] = -1,所以\(x_8\)不知道自己的源头(簇)是谁,于是通过Master树的信息找到自己的父亲\(x_1\),便向之询问源头,但\(x_1\)也不知道源头在哪,毕竟\(x_1\)自己的clusterIndices也为-1;于是\(x_1\)通过Master得知自己父亲是\(x_2\),于是便问\(x_2\),然后\(x_2\)回答说我们的源头是1。因此便进行回溯,分别确定\(x_1\)与\(x_8\)的源头(簇)是1。于是最后得到下面这颗Cluster Tree,或者说并查集树(因为通过并查集操作得到,至多两层,符合并查集树形结构)

         下面是寻源代码。这个部分代码与2.3中并查集的getf( )方法非常像,但是常规并查集是打破原有的用于引导的树f[ ],并在其基础上构造新的并查集树f[ ],都是在f[ ]上进行操作。但是ALEC算法并不破坏用于引导的树masters[ ],并且重构全新的并查集树clusterIndices[ ]。因此getf( )略有小差异。

	/**
	 ************************* 
	 * The block of a node should be same as its master. This recursive method is
	 * efficient.
	 * 
	 * @param paraIndex The index of the given node.
	 * @return The cluster index of the current node.
	 ************************* 
	 */
	public int coincideWithMaster(int paraIndex) {
		if (clusterIndices[paraIndex] == -1) {
			int tempMaster = masters[paraIndex];
			clusterIndices[paraIndex] = coincideWithMaster(tempMaster);
		} // Of if

		return clusterIndices[paraIndex];
	}// Of coincideWithMaster

        下面分簇的全部代码:

	/**
	 ************************* 
	 * Cluster a block in two. According to the master tree.
	 * 
	 * @param paraBlock The given block.
	 * @return The new blocks where the two most represent instances serve as the
	 *         root.
	 ************************* 
	 */
	public int[][] clusterInTwo(int[] paraBlock) {
		// Reinitialize. In fact, only instances in the given block is
		// considered.
		Arrays.fill(clusterIndices, -1);

		// Initialize the cluster number of the two roots.
		for (int i = 0; i < 2; i++) {
			clusterIndices[paraBlock[i]] = i;
		} // Of for i

		for (int i = 0; i < paraBlock.length; i++) {
			if (clusterIndices[paraBlock[i]] != -1) {
				// Already have a cluster number.
				continue;
			} // Of if

			clusterIndices[paraBlock[i]] = coincideWithMaster(masters[paraBlock[i]]);
		} // Of for i

		// The sub blocks.
		int[][] resultBlocks = new int[2][];
		int tempFistBlockCount = 0;
		for (int i = 0; i < clusterIndices.length; i++) {
			if (clusterIndices[i] == 0) {
				tempFistBlockCount++;
			} // Of if
		} // Of for i
		resultBlocks[0] = new int[tempFistBlockCount];
		resultBlocks[1] = new int[paraBlock.length - tempFistBlockCount];

		// Copy. You can design shorter code when the number of clusters is
		// greater than 2.
		int tempFirstIndex = 0;
		int tempSecondIndex = 0;
		for (int i = 0; i < paraBlock.length; i++) {
			if (clusterIndices[paraBlock[i]] == 0) {
				resultBlocks[0][tempFirstIndex] = paraBlock[i];
				tempFirstIndex++;
			} else {
				resultBlocks[1][tempSecondIndex] = paraBlock[i];
				tempSecondIndex++;
			} // Of if
		} // Of for i

		System.out.println("Split (" + paraBlock.length + ") instances " + Arrays.toString(paraBlock) + "\r\nto ("
				+ resultBlocks[0].length + ") instances " + Arrays.toString(resultBlocks[0]) + "\r\nand ("
				+ resultBlocks[1].length + ") instances " + Arrays.toString(resultBlocks[1]));
		return resultBlocks;
	}// Of clusterInTwo

        上面29~52行的内容就是把每个簇的内容分别按序输出到一个二维数组之中,因为是按序的,所以得到每个部分都如同一个“ 小的descendantRepresentatives ”代表局部,也意味着这个函数本身是可递归分治的。

3.5 ALEC 主体代码

        ALEC主体思想可参考2.2小节的三步骤三出口,这里就不再赘述。本文此处将以代码的数据变量与图例同步的方式来协助代码编写的理解。(注意!图中红色字体部分问本回合发生修改或者要注意的内容)

         首先第一步得到一颗全局的树,假设我们查阅的上限是10次(maxNumQuery),最初只用了0次(numQuery);然后这颗树的结点数目\(n=21\),通过根号计算得知我们本回合需要查阅的数目是4(tempExpectedQueries);结点按照代表性排序的数组是paraBlock,当前这颗树里面已经查询的结点数目是0(tempNumQuery)。

        又因为:1.这颗树里面已经查询的结点数目要小于本回合需要查阅的数目;2.结点数目大于我们设置的阈值5。因此:判断代码继续(Judgement:Program continues)。

        // Step 1. How many labels are queried for this block.
		int tempExpectedQueries = (int) Math.sqrt(paraBlock.length);
		int tempNumQuery = 0;
		for (int i = 0; i < paraBlock.length; i++) {
			if (instanceStatusArray[paraBlock[i]] == 1) {
				tempNumQuery++;
			} // Of if
		} // Of for i

		// Step 2. Vote for small blocks.
		if ((tempNumQuery >= tempExpectedQueries) && (paraBlock.length <= smallBlockThreshold)) {
			System.out.println(
					"" + tempNumQuery + " instances are queried, vote for block: \r\n" + Arrays.toString(paraBlock));
			vote(paraBlock);

			return;
		} // Of if

        instanceStatusArray[paraBlock[i]] == 1说明样本\(x_i\)已经在之前被查询过了。tempNumQuery是统计之前已经被查询过的内容。

        继续:

         算法对于前4(\(\sqrt n = 4\))个数据进行了查阅,总查阅numQuery + 4(numQuery加了之后记得判断是否越界),这些被查阅的结点数的标记器标记为1(已查询),预测标签设置为查询结果。最终判定这些标签是否都一个样?可惜,不全一样,那么本树不纯(Pure),进入二分分簇。

        代码:

        // Step 3. Query enough labels.
		for (int i = 0; i < tempExpectedQueries; i++) {
			if (numQuery >= maxNumQuery) {
				System.out.println("No more queries are provided, numQuery = " + numQuery + ".");
				vote(paraBlock);
				return;
			} // Of if

			if (instanceStatusArray[paraBlock[i]] == 0) {
				instanceStatusArray[paraBlock[i]] = 1;
				predictedLabels[paraBlock[i]] = (int) dataset.instance(paraBlock[i]).classValue();
				// System.out.println("Query #" + paraBlock[i] + ", numQuery = "
				// + numQuery);
				numQuery++;
			} // Of if
		} // Of for i

		// Step 4. Pure?
		int tempFirstLabel = predictedLabels[paraBlock[0]];
		boolean tempPure = true;
		for (int i = 1; i < tempExpectedQueries; i++) {
			if (predictedLabels[paraBlock[i]] != tempFirstLabel) {
				tempPure = false;
				break;
			} // Of if
		} // Of for i
		if (tempPure) {
			System.out.println("Classify for pure block: " + Arrays.toString(paraBlock));
			for (int i = tempExpectedQueries; i < paraBlock.length; i++) {
				if (instanceStatusArray[paraBlock[i]] == 0) {
					predictedLabels[paraBlock[i]] = tempFirstLabel;
					instanceStatusArray[paraBlock[i]] = 2;
				} // Of if
			} // Of for i
			return;
		} // Of if

        继续:

         进入二分分簇的具体细节交给3.4节吧,那里做了非常详尽的描述。这里一带而过就行了。

		// Step 5. Split in two and process them independently.
		int[][] tempBlocks = clusterInTwo(paraBlock);
		for (int i = 0; i < 2; i++) {
			// Attention: recursive invoking here.
			clusterBasedActiveLearning(tempBlocks[i]);
		} // Of for i

        通过二分分簇函数clusterInTwo( )计算得到裂开的tempBlock,分别带入本函数进行迭代。

        为了展示投票代码,下面我继续用图例跑完了接下来几步操作的部分,从而触发特殊结束,以示特例:

         这是簇0的子树,已经有两个结点之前查询过了,所以tempNumQuery = 2,但是当前希望查询3个(tempExpectedQueries),所以我们还需要补查1个(tempExpectedQueries - tempNumQuery =1)。同时这个树有10个结点,大于阈值5,因此判断程序继续进行。

         通过paraBlock的顺序,我们需要补查的样本是\(x_{10}\)(图中灰色结点)。查阅后numQuery+1(记得判断是否越界),状态标记为1(已查询),记录预测值。最终断言为不纯。

         对于这个进一步分簇的1号簇:

         这颗子树已经有2个结点被查询过了,而通过开根号得知,算法要求进一步要求这颗子树也要查询2个,因此tempExpectedQueries与tempNumQuery相等了。而且不妙的是,这颗子树的数目已经满足阈值了(5个),所以这颗子树递归返回,进行投票。

(因为这个判断条件)

		// Step 2. Vote for small blocks.
		if ((tempNumQuery >= tempExpectedQueries) && (paraBlock.length <= smallBlockThreshold)) {
			vote(paraBlock);
			return;
		} // Of if

         投票过程很简单,遍历已经查询过的结点,然后投票选择最多的标签。之后未查询的结点都被预测为此标签。同时因为这样的结点不是通过查询得到的,而是我们的预测得到的,因此其predictedLabels[ ]预测数组要标记为2(已分类)。这些点是有可能错误的,非100%有把握。但是predictedLabels[ ]预测数组要是标记为1,那么是专家提供(模拟代码中是来自DataSet数据集)的,因此是100%准确的,注意这个差别!

        如果我们当前查询的次数numQuery大于了maxNumQuery,那么也触发这个投票操作。

         投票代码:

	/**
	 ********************************** 
	 * Classify instances in the block by simple voting.
	 * 
	 * @param paraBlock The given block.
	 ********************************** 
	 */
	public void vote(int[] paraBlock) {
		int[] tempClassCounts = new int[dataset.numClasses()];
		for (int i = 0; i < paraBlock.length; i++) {
			if (instanceStatusArray[paraBlock[i]] == 1) {
				tempClassCounts[(int) dataset.instance(paraBlock[i]).classValue()]++;
			} // Of if
		} // Of for i

		int tempMaxClass = -1;
		int tempMaxCount = -1;
		for (int i = 0; i < tempClassCounts.length; i++) {
			if (tempMaxCount < tempClassCounts[i]) {
				tempMaxClass = i;
				tempMaxCount = tempClassCounts[i];
			} // Of if
		} // Of for i

		// Classify unprocessed instances.
		for (int i = 0; i < paraBlock.length; i++) {
			if (instanceStatusArray[paraBlock[i]] == 0) {
				predictedLabels[paraBlock[i]] = tempMaxClass;
				instanceStatusArray[paraBlock[i]] = 2;
			} // Of if
		} // Of for i
	}// Of vote

         让我们视角回到最初那个最大的子树的第二个簇:

         通过查阅得到结果发现都是一样的标签,故证明了此子树是纯(Pure)的!于是触发另一个子树退出代码:

        if (tempPure) {
			System.out.println("Classify for pure block: " + Arrays.toString(paraBlock));
			for (int i = tempExpectedQueries; i < paraBlock.length; i++) {
				if (instanceStatusArray[paraBlock[i]] == 0) {
					predictedLabels[paraBlock[i]] = tempFirstLabel;
					instanceStatusArray[paraBlock[i]] = 2;
				} // Of if
			} // Of for i
			return;
		} // Of if

        这个代码就没专门运行投票函数vote( )了,因为都是纯的了,投票结果肯定是这个树中那个唯一的那个标签呀~ 

         最后,让我贴出ALEC的主体代码全部:

	/**
	 ********************************** 
	 * Cluster based active learning. Prepare for
	 * 
	 * @param paraRatio       The ratio of the maximal distance as the dc.
	 * @param paraMaxNumQuery The maximal number of queries for the whole dataset.
	 * @parm paraSmallBlockThreshold The small block threshold.
	 ********************************** 
	 */
	public void clusterBasedActiveLearning(double paraRatio, int paraMaxNumQuery, int paraSmallBlockThreshold) {
		radius = maximalDistance * paraRatio;
		smallBlockThreshold = paraSmallBlockThreshold;

		maxNumQuery = paraMaxNumQuery;
		predictedLabels = new int[dataset.numInstances()];

		for (int i = 0; i < dataset.numInstances(); i++) {
			predictedLabels[i] = -1;
		} // Of for i

		computeDensitiesGaussian();
		computeDistanceToMaster();
		computePriority();
		descendantRepresentatives = sortToIndices(priority);
		System.out.println("descendantRepresentatives = " + Arrays.toString(descendantRepresentatives));

		numQuery = 0;
		clusterBasedActiveLearning(descendantRepresentatives);
	}// Of clusterBasedActiveLearning

	/**
	 ********************************** 
	 * Cluster based active learning.
	 * 
	 * @param paraBlock The given block. This block must be sorted according to the
	 *                  priority in descendant order.
	 ********************************** 
	 */
	public void clusterBasedActiveLearning(int[] paraBlock) {
		System.out.println("clusterBasedActiveLearning for block " + Arrays.toString(paraBlock));

		// Step 1. How many labels are queried for this block.
		int tempExpectedQueries = (int) Math.sqrt(paraBlock.length);
		int tempNumQuery = 0;
		for (int i = 0; i < paraBlock.length; i++) {
			if (instanceStatusArray[paraBlock[i]] == 1) {
				tempNumQuery++;
			} // Of if
		} // Of for i

		// Step 2. Vote for small blocks.
		if ((tempNumQuery >= tempExpectedQueries) && (paraBlock.length <= smallBlockThreshold)) {
			System.out.println(
					"" + tempNumQuery + " instances are queried, vote for block: \r\n" + Arrays.toString(paraBlock));
			vote(paraBlock);

			return;
		} // Of if

		// Step 3. Query enough labels.
		for (int i = 0; i < tempExpectedQueries; i++) {
			if (numQuery >= maxNumQuery) {
				System.out.println("No more queries are provided, numQuery = " + numQuery + ".");
				vote(paraBlock);
				return;
			} // Of if

			if (instanceStatusArray[paraBlock[i]] == 0) {
				instanceStatusArray[paraBlock[i]] = 1;
				predictedLabels[paraBlock[i]] = (int) dataset.instance(paraBlock[i]).classValue();
				// System.out.println("Query #" + paraBlock[i] + ", numQuery = "
				// + numQuery);
				numQuery++;
			} // Of if
		} // Of for i

		// Step 4. Pure?
		int tempFirstLabel = predictedLabels[paraBlock[0]];
		boolean tempPure = true;
		for (int i = 1; i < tempExpectedQueries; i++) {
			if (predictedLabels[paraBlock[i]] != tempFirstLabel) {
				tempPure = false;
				break;
			} // Of if
		} // Of for i
		if (tempPure) {
			System.out.println("Classify for pure block: " + Arrays.toString(paraBlock));
			for (int i = tempExpectedQueries; i < paraBlock.length; i++) {
				if (instanceStatusArray[paraBlock[i]] == 0) {
					predictedLabels[paraBlock[i]] = tempFirstLabel;
					instanceStatusArray[paraBlock[i]] = 2;
				} // Of if
			} // Of for i
			return;
		} // Of if

		// Step 5. Split in two and process them independently.
		int[][] tempBlocks = clusterInTwo(paraBlock);
		for (int i = 0; i < 2; i++) {
			// Attention: recursive invoking here.
			clusterBasedActiveLearning(tempBlocks[i]);
		} // Of for i
	}// Of clusterBasedActiveLearning

        主体函数还被它的重载函数调用,从而可以让我们的主要操作与初始化分离,是个不错的idea。这里要稍微说明一下主体函数的形参paraRatio,ALEC中定义每个结点密度的圆域\(d_c\)的值是来自 图中最大距离maximalDistance与paraRatio的乘积,通过经验,paraRatio的值一般取0.15左右,它与数据集有一定关系但是不是特别敏感。

四、数据测试( 主要测试查询上限对于数据识别率影响问题 )

        为了输出方便,将准确度计算纳入了toString( )函数的重载

	/**
	 ******************* 
	 * Show the statistics information.
	 ******************* 
	 */
	public String toString() {
		int[] tempStatusCounts = new int[3];
		double tempCorrect = 0;
		for (int i = 0; i < dataset.numInstances(); i++) {
			tempStatusCounts[instanceStatusArray[i]]++;
			if (predictedLabels[i] == (int) dataset.instance(i).classValue()) {
				tempCorrect++;
			} // Of if
		} // Of for i

		String resultString = "(unhandled, queried, classified) = " + Arrays.toString(tempStatusCounts);
		resultString += "\r\nCorrect = " + tempCorrect + ", accuracy = " + (tempCorrect / dataset.numInstances());

		return resultString;
	}// Of toString

	/**
	 ********************************** 
	 * The entrance of the program.
	 * 
	 * @param args: Not used now.
	 ********************************** 
	 */
	public static void main(String[] args) {
		long tempStart = System.currentTimeMillis();

		System.out.println("Starting ALEC.");
		String arffFilename = "D:/Java DataSet/iris.arff";

		Alec tempAlec = new Alec(arffFilename);
		tempAlec.clusterBasedActiveLearning(0.15, 30, 3);

		System.out.println(tempAlec);

		long tempEnd = System.currentTimeMillis();
		System.out.println("Runtime: " + (tempEnd - tempStart) + "ms.");
	}// Of main

         使用iris的测试结果如下。可以发现,虽然我们给定了30的查阅上限,但是实际在使用过程中并不能完全使用这些查询。这要根据具体情况而定,因为在某些子树结点过少时,或者子树分配预查询已经使用完了,这时子树部分递归是会提前结束的。这是ALEC的收敛导致。

         具体来说,基于数据集的不同,查询量对于算法的影响也是毋庸置疑的。因为查询更少,100%有把握的信息就更少,则更多的数据我们需要使用更少信息去估计(说难听点就是猜),自然难以窥见数据的全貌,准确度也不够理想。但是,通过图像也可以发现:查询阈值突破某个值时,算法可能突然查询了某个关键数据,在其指引下顺利地估计了其周围的数据,导致准确度突然地提升。就iris数据集来看,这样的阈值似乎在11~13之间发生。

        后续准确度达到最高后便不再变化,因为这时已经能保证查询到的所有样本都能最大代表每个子树内的结点,再多的查询也能同通过已知的查询正确预测。

        同时也发现:准确值达到稳定的时间要早于实际查询数目达到稳定的时间,大概在第13次查询时,整体的识别度就确定了下来;但是这时还能继续查询,直到查询次数到27次。

        这个27即满应该是ALEC对于极小子树的收敛条件导致的。当查询未到27时,按照部分子树的收敛判定来说还未完全收敛,可以继续查询,直到27次查询之后,全部子树都已经收敛,无法再查询。

        进一步测试了数据量更大的字符型数据mushroom,设置的查询上限是800,因为数据量比较大,跑了6s。可以发现依旧未达到800的查找上限而是最高只达到了465。

       然后跑了1h得到了曲线图:

         结果与我们最初分析的是一样的,准确度基本稳定要早于可查询的上限到达的时候,原因同刚刚iris数据的分析。此外,因为mushroom的数量比较大,数据中的某些关键数据可能不是1个,所以不同于iris中的一次陡增,mushroom中有多次陡增,甚至还有个别“ 假增加 ”。我的推测:这种假增加是一种算法的不确定导致的,ALEC在查询量较少时会对部分数据进行贴合中心标签的预测,这种预测本身就有随机性,若这个未知区域中恰好大量标签与我们预测的标签一致那么就会导致相似度增加;但是这时若我们有更多的查询权,可能一不小心查询到了一个不同的标签,那么最终可能分化了我最初的标签,导致识别率下降。

        当然这种假增加的推测本身是低概率的情况,因此可能在数据比较多时会出现频繁,这就是为什么只有150个数据的iris没有出现这种情况。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值