Mahout之k-means算法源码分析


org.apache.mahout.clustering.syntheticcontrol.kmeans.run(Configuration conf, Path input, Path output,DistanceMeasure measure, int k, double convergenceDelta,int maxIterations),这是我们分析的起点:


public static void run(Configuration conf, Path input, Path output,DistanceMeasure measure, int k, double convergenceDelta,int maxIterations) throws Exception {
		Path directoryContainingConvertedInput = new Path(output,DIRECTORY_CONTAINING_CONVERTED_INPUT);
		log.info("Preparing Input");
		InputDriver.runJob(input, directoryContainingConvertedInput,"org.apache.mahout.math.RandomAccessSparseVector");
		log.info("Running random seed to get initial clusters");
		Path clusters = new Path(output, Cluster.INITIAL_CLUSTERS_DIR);
		clusters = RandomSeedGenerator.buildRandom(conf,directoryContainingConvertedInput, clusters, k, measure);
		log.info("Running KMeans");
		KMeansDriver.run(conf, directoryContainingConvertedInput, clusters,output, measure, convergenceDelta, maxIterations, true, false);
		// run ClusterDumper
		ClusterDumper clusterDumper = new ClusterDumper(finalClusterPath(conf,output, maxIterations), new Path(output, "clusteredPoints"));
		clusterDumper.printClusters(null);
}

1.  将输入路径testdata下的数据文件转换格式:对于文件中的每一行,将其转化成一个VectorWritable对象,以顺序文件的形式写入到output/data目录下,对应的key为这一行数据的值的个数(向量的维度)。这些操作是由代码段完成:

log.info("Preparing Input");
InputDriver.runJob(input, directoryContainingConvertedInput,"org.apache.mahout.math.RandomAccessSparseVector");

2. 从第一步生成的文件中随机的选择k个VetorWritable对象作为初始k个聚类,每一个聚类中仅有一个选择的点,这是由代码段完成的:

log.info("Running random seed to get initial clusters");
Path clusters = new Path(output, Cluster.INITIAL_CLUSTERS_DIR);
clusters = RandomSeedGenerator.buildRandom(conf,directoryContainingConvertedInput, clusters, k, measure);

随机选择算法很简单,大致流程如下:对于输入路径(即output/data,存放着转换格式后的数据文件)下的文件(可能有多个),选择最开始的k个点(VectorWritable对象),由每个点组成一个聚类,由此得到k个聚类,对于文件中剩下的每一个点,以k/(k+1)的概率替换掉k个聚类中的一个聚类(这个聚类是随机选取的),最后将随机生成的k个Cluster保存在output/clusters-0目录下的 part-randomSeed文件中


3. 最后来分析一下k-meas算法的核心部分 :

log.info("Running KMeans");
KMeansDriver.run(conf, directoryContainingConvertedInput, clusters,output, measure, convergenceDelta, maxIterations, true, false);

在run()方法中有:

Path clustersOut = buildClusters(conf, input, clustersIn, output,measure, maxIterations, delta, runSequential);
if (runClustering) {
		log.info("Clustering data");
		clusterData(conf, input, clustersOut, new Path(output,AbstractCluster.CLUSTERED_POINTS_DIR), measure, delta,runSequential);
}

在buildClusters()方法中,input即为转换后的数据文件路径output/data,clustersIn即为第二步中生成的part-randomSeed文件。下面具体看一下buildClusters()方法:

public static Path buildClusters(Configuration conf, Path input,Path clustersIn, Path output, DistanceMeasure measure,int maxIterations, String delta, boolean runSequential)
			throws IOException, InterruptedException, ClassNotFoundException {
		if (runSequential) {
			return buildClustersSeq(conf, input, clustersIn, output, measure,maxIterations, delta);
		} else {
			return buildClustersMR(conf, input, clustersIn, output, measure,maxIterations, delta);
		}
}

private static Path buildClustersMR(Configuration conf, Path input,Path clustersIn, Path output, DistanceMeasure measure,int maxIterations, String delta) throws IOException,
			InterruptedException, ClassNotFoundException {
		boolean converged = false;
		int iteration = 1;
		while (!converged && iteration <= maxIterations) {
			log.info("K-Means Iteration {}", iteration);
			// point the output to a new directory per iteration
			Path clustersOut = new Path(output, AbstractCluster.CLUSTERS_DIR+ iteration);
			converged = runIteration(conf, input, clustersIn, clustersOut,measure.getClass().getName(), delta);
			// now point the input to the old output directory
			clustersIn = clustersOut;
			iteration++;
		}
		Path finalClustersIn = new Path(output, AbstractCluster.CLUSTERS_DIR+ (iteration - 1) + "-final");
		FileSystem.get(conf).rename(new Path(output, AbstractCluster.CLUSTERS_DIR+ (iteration - 1)), finalClustersIn);
		return finalClustersIn;
}
 每一次迭代生成生成的文件放在不同的目录(output/clusters-i),在runIteration()方法中启动一个mapReduce任务将输入文件中的全部点划分到不同的Cluster中,并更新Cluster的相关属性:

private static boolean runIteration(Configuration conf, Path input,Path clustersIn, Path clustersOut, String measureClass,
			String convergenceDelta) throws IOException, InterruptedException,ClassNotFoundException {

		conf.set(KMeansConfigKeys.CLUSTER_PATH_KEY, clustersIn.toString());
		conf.set(KMeansConfigKeys.DISTANCE_MEASURE_KEY, measureClass);
		conf.set(KMeansConfigKeys.CLUSTER_CONVERGENCE_KEY, convergenceDelta);

		Job job = new Job(conf,"KMeans Driver running runIteration over clustersIn: "+ clustersIn);
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(ClusterObservations.class);
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(Cluster.class);

		job.setInputFormatClass(SequenceFileInputFormat.class);
		job.setOutputFormatClass(SequenceFileOutputFormat.class);
		job.setMapperClass(KMeansMapper.class);
		job.setCombinerClass(KMeansCombiner.class);
		job.setReducerClass(KMeansReducer.class);

		FileInputFormat.addInputPath(job, input);
		FileOutputFormat.setOutputPath(job, clustersOut);

		job.setJarByClass(KMeansDriver.class);
		HadoopUtil.delete(conf, clustersOut);
		if (!job.waitForCompletion(true)) {
			throw new InterruptedException("K-Means Iteration failed processing " + clustersIn);
		}
		FileSystem fs = FileSystem.get(clustersOut.toUri(), conf);
		return isConverged(clustersOut, conf, fs);
	}

 KmeansMapper中,setup()方法通过读取当前clusterIn目录,获取上一次迭代结束时生成的k个聚类,将其保存在Collection clusters中,这样,mapper就可以访问到这k个聚类
protected void setup(Context context) throws IOException,InterruptedException {
		super.setup(context);
		Configuration conf = context.getConfiguration();
		DistanceMeasure measure = ClassUtils.instantiateAs(conf.get(KMeansConfigKeys.DISTANCE_MEASURE_KEY),DistanceMeasure.class);
		measure.configure(conf);

		this.clusterer = new KMeansClusterer(measure);

		String clusterPath = conf.get(KMeansConfigKeys.CLUSTER_PATH_KEY);
		if (clusterPath != null && !clusterPath.isEmpty()) {
			KMeansUtil.configureWithClusterInfo(conf, new Path(clusterPath),clusters);
			if (clusters.isEmpty()) {
				throw new IllegalStateException("No clusters found. Check your -c path.");
			}
		}
	}

在map 方法中,对于每一个输入点,调用KMeansClusterer类的emitPointToNearestCluster(point.get(), this.clusters,context)方法,找出离这个点最近的Cluster,最后,将这个最近的Cluster的id以及一个ClusterObservations对象写入到map输出中:

public void emitPointToNearestCluster(Vector point,Iterable clusters,	Mapper.Context context)
			throws IOException, InterruptedException {
		Cluster nearestCluster = null;
		double nearestDistance = Double.MAX_VALUE;
		for (Cluster cluster : clusters) {
			Vector clusterCenter = cluster.getCenter();
			double distance = this.measure.distance(clusterCenter.getLengthSquared(), clusterCenter, point);
			if (log.isDebugEnabled()) {
				log.debug("{} Cluster: {}", distance, cluster.getId());
			}
			if (distance < nearestDistance || nearestCluster == null) {
				nearestCluster = cluster;
				nearestDistance = distance;
			}
		}
		context.write(new Text(nearestCluster.getIdentifier()),	new ClusterObservations(1, point, point.times(point)));
}

 KMeansCombiner将一个map任务输出中key相同的键值对进行合并,以减少传递给reducer的传输量:

protected void reduce(Text key, Iterable values, Context context)
    throws IOException, InterruptedException {
    Cluster cluster = new Cluster();
    for (ClusterObservations value : values) {
      cluster.observe(value);
    }
    context.write(key, cluster.getObservations());
}

KMeansReducer中,setup()方法通过读取当前clusterIn目录,获取上一次迭代结束时生成的k个聚类,将其保存在一个HashMap(clusterMap变量)中。

protected void reduce(Text key, Iterable values, Context context)
    throws IOException, InterruptedException {
    Cluster cluster = clusterMap.get(key.toString());
    for (ClusterObservations delta : values) {
      cluster.observe(delta);
    }
    // force convergence calculation
    boolean converged = clusterer.computeConvergence(cluster, convergenceDelta);
    if (converged) {
      context.getCounter("Clustering", "Converged Clusters").increment(1);
    }
    cluster.computeParameters();
    context.write(new Text(cluster.getIdentifier()), cluster);
  }

在reduce方法中,根据待迭代的key在clusterMap中取出对应的Cluster,因为key相同的键值对对应了一个Cluster中所有的点,所以遍历这个key对应的values,就可以遍历这个cluster的所有点。对每个value(ClusterObservations对象)调用cluster.observe(value)方法,在该方法中,实际上是将每个ClusterObservations的S0累加到cluster的S0属性中,S1累加到cluster的S1中,S2累加到cluster的S2中,并且在初始时,每个cluster的S0=0,S1=null,S2=null,这个从 cluster.computeParameters()方法中可以看出来。我的理解是S0是cluster中的点的个数,S1是cluster中每个点对应的VectorWritable对象各个分量的和构成的一个Vector,S2是cluster中每个点对应的VectorWritable对象各个分量的平方的和构成的一个Vector。

 

         迭代完每个key对应的values之后,这个cluster对应的S0(起始为0),S1(起始为null),S2(起始为null)三个属性的值都被赋与了新的值,可以利用这些值来计算当前cluster是否收敛:clusterer.computeConvergence(cluster, convergenceDelta),如果收敛,则将已经收敛的Cluster的计数器加1,然后,调用computeParameters方法计算这个cluster的各个其它属性值,包括numPoints,center,radius这三个属性,同时将S0置0,S1,S2置null :

public void computeParameters() {
		if (getS0() == 0) {
			return;
		}
		setNumPoints((int) getS0());
		setCenter(getS1().divide(getS0()));
		// compute the component stds
		if (getS0() > 1) {
			setRadius(getS2().times(getS0()).minus(getS1().times(getS1())).assign(new SquareRootFunction()).divide(getS0()));
		}
		setS0(0);
		setS1(null);
		setS2(null);
	}

最后将这个cluster的id作为key,cluster本身作为value写入到输出clustersOut(output/clusters-iteration),这里可以看一下一个Cluster的write()方法是如何将其写入到流中的,它的write()方法调用了父类的write()方法,我将它综合了一下,在Cluster的write方法中主要写入了以下内容:

out.writeUTF(measure.getClass().getName());
out.writeInt(id);
out.writeLong(getNumPoints());
VectorWritable.writeVector(out, getCenter());
VectorWritable.writeVector(out, getRadius());
out.writeBoolean(converged);

 可以看到,写入的内容有DistanceMeasure对象,cluster的id,点的个数,center,radius,是否收敛。


        再回到runIteration()方法中,最后要判断一下执行完本次迭代之后,是否所有的Cluster已经全部收敛。执行完mapReduce任务之后,本次迭代的结果(得到的k个Cluster)写入到了输出文件中clustersOut(output/clusters-iteration),这里读取该目录下的文件(如果有多少reducer的话,会生成多个文件,各个Cluster分布在这多个文件中),取出每一个Cluster,判断它是否已经收敛,只要有一个是不收敛的,就直接返回false,表明还没有全局收敛,要继续执行下一次迭代。

private static boolean isConverged(Path filePath, Configuration conf,FileSystem fs)throws IOException {
		for (FileStatus part : fs.listStatus(filePath, PathFilters.partFilter())) {
			SequenceFileValueIterator iterator = new SequenceFileValueIterator(part.getPath(), true, conf);
			while (iterator.hasNext()) {
				Cluster value = iterator.next();
				if (!value.isConverged()) {
					Closeables.closeQuietly(iterator);
					return false;
				}
			}
		}
		return true;
	}

       至此,一次迭代全部完成,它返回了这次迭代完成之后,是否已经达到全部Cluster 收敛。此时,程序返回到buildClustersMR()方法中,将本次迭代输出目录clusterOut(保存了最新得到了k个Cluster)赋值给clusterIn,即下次从这里读取最新的Cluster,同时迭代次数加1,如果还没有全部收敛并且没有达到最大迭代次数,则继续执行下一次迭代。while循环结束时,获取保存最终全部cluster文件所在的目录路径,并将其重新命名,即在后面加上了“-final”后缀
Path finalClustersIn = new Path(output, AbstractCluster.CLUSTERS_DIR+ (iteration - 1) + "-final");
FileSystem.get(conf).rename(new Path(output, AbstractCluster.CLUSTERS_DIR+ (iteration - 1)), finalClustersIn);
return finalClustersIn;

至此,buildClusters()方法执行完毕,程序返回到KMeansDriver.run()方法中,执行代码:

if (runClustering) {
			log.info("Clustering data");
			clusterData(conf, input, clustersOut, new Path(output,AbstractCluster.CLUSTERED_POINTS_DIR), measure, delta,runSequential);
		}

clusterData方法调用clusterDataMR()方法启动一个mapReduce任务,根据最后生成的clustersOut目录(保存了最终的每个Cluster的相关信息)和转换后的原始数据文件,决定各个Cluster分别包含哪些点。结果写入了output/clusteredPoints目录。至此KMeansDriver.run()方法结束。


最后 Job.run()方法使用ClusterDumper输出具体聚类结果(每个Cluster包含哪些点,点数,center,radius等)。


你可能感兴趣的:(Hadoop,Mahout)