id:audioswitchさんのX-meansをPHPで実装してみました

X-meansを実装してみた。ソースも晒しちゃう。

id:audioswitchさんのX-meansをPHPで実装してみました。
普段、処理は全部PHPで書いているのでPHPで書き起こしました。
今回はコードも公開して頂いていたので、使わせてもらうだけではなくもしかすると需要があるかもしれないので公開させて頂きました次第であります。
コピーするだけでも能がないので、何かないものかと。おまけでGDでプロットして確認する関数も作ったので一緒に。
あと、サンプルで12ポイントだけプロットしてみるコードもご一緒にどうぞ。

<?php
	
	//-------------------------------------
	// x-means法でクラスタリング
	public function xmeans_clustering (
			array $data, 
			$dimension_number,
			$sample_number,
			$init_cluster_number) {
		
		$label =array();
		
		$next_label =0;
		$old_cluster_number =$init_cluster_number;
		$new_cluster_number =0;
		
		// 最初のkmeansを行う.
		list($old_centroid, $old_label) =kmeans_clustering(
				$data,
				$dimension_number,
				$sample_number,
				$old_cluster_number);
    	
		// クラスタ数に変化がなくなるまで繰り返す.
		do {
		
			// 新しいクラスタを初期化する.
			$new_label =array_diff($old_label,array());
			$new_centroid =array_diff($old_centroid,array());
			$old_cluster_number;
			$next_label = 0;
			
			// 各クラスタを2つに分割し,評価していく.
			for ($n=0; $n<$old_cluster_number; $n++) {
			
				// n番目のクラスタのデータ数を数える.
				$parent_sample_number = 0;
				
				for ($i=0; $i<$sample_number; $i++) {
					
					if ($old_label[$i] == $n) {
						
						$parent_sample_number++;
					}
				}
				
				if ($parent_sample_number <= 1) {
				
					continue;
				}
				
				// 分散を求める.
				$parent_variance =0;
				
				for ($i=0; $i<$sample_number; $i++) {
					
					$distance =0;
					
					if ($old_label[$i] == $n) {
						
						for ($j=0; $j<$dimension_number; $j++) {
							
							$distance += 
									($data[$j * $sample_number + $i] 
									- $old_centroid[$j * $old_cluster_number + $n]) * 
									($data[$j * $sample_number + $i] 
									- $old_centroid[$j * $old_cluster_number + $n]);
						}
					}
					
					$parent_variance +=$distance;
				}
				
				$parent_variance /=$parent_sample_number - 1;
				
				// 対数尤度を求める.
				$parent_likelihood =calculate_likelihood(
						$dimension_number,
						$parent_sample_number, 
						1, 
						$parent_variance);

				// BICを求める.
				$parent_bic =$parent_likelihood 
						- (($dimension_number + 1) / 2) * 
						log($parent_sample_number);

				// kmeansを行う.
				$parent_data =array();
				$counter =0;

				for ($i=0; $i<$sample_number; $i++) {
				
					if ($old_label[$i] == $n) {
					
						for ($j=0; $j<$dimension_number; $j++) {
						
							$parent_data[$j * $parent_sample_number + $counter] 
									=$data[$j * $sample_number + $i];
						}
						
						$counter++;
					}
				}
				
				list($child_centroid, $child_label) =kmeans_clustering(
						$parent_data, 
						$dimension_number, 
						$parent_sample_number, 
						2);
            
				// 新たなクラスタのデータ数を数える.
				$child_a_sample_number = 0;
				$child_b_sample_number = 0;
				
				for ($i=0; $i<$parent_sample_number; $i++) {
				
					($child_label[$i] == 0)
							? $child_a_sample_number++
							: $child_b_sample_number++;
				}
            	
				if ($child_a_sample_number <= 1 || $child_b_sample_number <= 1) {
				
					continue;
				}
				
				// 新たなクラスタの分散を求める.
				$child_a_variance =0;
				$child_b_variance =0;
				
				for ($i=0; $i<$parent_sample_number; $i++) {
				
					$child_a_distance = 0.0;
					$child_b_distance = 0.0;
					
					if ($child_label[$i] == 0) {
						
						for ($j=0; $j<$dimension_number; $j++) {
							
							$child_a_distance +=
									($parent_data[$j * $parent_sample_number + $i] 
									- $child_centroid[$j * 2 + 0]) * 
									($parent_data[$j * $parent_sample_number + $i] 
									- $child_centroid[$j * 2 + 0]);
						}
						
					} else {
						
						for ($j=0; $j<$dimension_number; $j++) {
						
							$child_b_distance += 
									($parent_data[$j * $parent_sample_number + $i] 
									- $child_centroid[$j * 2 + 1]) * 
									($parent_data[$j * $parent_sample_number + $i] 
									- $child_centroid[$j * 2 + 1]);
						}
					}
					
					$child_a_variance +=$child_a_distance;
					$child_b_variance +=$child_b_distance;
				}
				
				$child_a_variance /=$child_a_sample_number - 1;
				$child_b_variance /=$child_b_sample_number - 1;
            
				// 新たなクラスタの対数尤度を求める.
				$child_likelihood =calculate_likelihood(
					$dimension_number, 
					$child_a_sample_number, 
					2, 
					$child_a_variance
				) + calculate_likelihood(
					$dimension_number, 
					$child_b_sample_number, 
					2, 
					$child_b_variance
				);

				// 新たなクラスタのBICを求める.
				$child_bic =$child_likelihood 
						- ((1 + 2 * $dimension_number + 2) / 2.0) 
						* log($parent_sample_number);
				
				// 新たなクラスタのBICの方が大きければ,新たなクラスタを仲間入りさせる.
				if ($child_bic > $parent_bic) {
					
					// ラベルを更新する.
					$counter = 0;
					
					for ($i=0; $i<$sample_number; $i++) {
					
						if ($new_label[$i] == $next_label) {
						
							$new_label[$i] =$next_label + $child_label[$counter++];
						
						} elseif ($new_label[$i] > $next_label) {
						
							$new_label[$i]++;
						}
					}
					
					// セントロイドを更新する.
					$temp_centroid =array_diff($new_centroid,array());
					$new_centroid =array();
					
					for ($i=0; $i<$new_cluster_number + 1; $i++) {
					
						if ($i < $next_label) {
						
							for ($j=0; $j<$dimension_number; $j++) {
							
								$new_centroid[$j * ($new_cluster_number + 1) + $i]
										=$temp_centroid[$j * $new_cluster_number + $i];
							}
						} else if ($i == $next_label) {
						
							for ($j=0; $j<$dimension_number; $j++) {
							
								$new_centroid[$j * ($new_cluster_number + 1) + $i] 
										=$child_centroid[$j * 2 + 0];
							}
						} else if ($i == $next_label + 1) {
						
							for ($j=0; $j<$dimension_number; $j++) {
							
								$new_centroid[$j * ($new_cluster_number + 1) + $i] 
										=$child_centroid[$j * 2 + 1];
							}
						} else if ($i > $next_label + 1) {
						
							for ($j=0; $j<$dimension_number; $j++) {
							
								$new_centroid[$j * ($new_cluster_number + 1) + $i] 
										=$temp_centroid[$j * $new_cluster_number + ($i - 1)];
							}
						}
					}
					
					// クラスタ数を更新する.
					$new_cluster_number++;

					// 次のクラスタへ.
					$next_label += 2;
					
				} else {
					
					// 次のクラスタへ.
					$next_label++;
				}
			}
        	
			$continual =$new_cluster_number != $old_cluster_number;
			
			// 結果を引き継ぐ.
			$old_cluster_number =$new_cluster_number;
			$old_centroid =array_diff($new_centroid,array());
			$old_label =array_diff($new_label,array());
		
		} while ($continual);
		
		// 最終的なクラスタを割り振る.
		for ($i=0; $i<$sample_number; $i++) {
		
			$label[$i] =$old_label[$i] + 1;
		}
		
		return array($new_centroid,$label,$new_cluster_number);
	}
	
	//-------------------------------------
	// k-means法でクラスタリング
	public function kmeans_clustering (
			array $data, 
			$dimension_number,
			$sample_number,
			$cluster_number) {
		
		$centroid =array();
		$label =array();
		
		$tolerance =1e-8;
		$old_error =PHP_INT_MAX;
		$new_error =PHP_INT_MAX;
		
		// ランダムにセントロイドを選択する.
		for ($i=0; $i<$cluster_number; $i++) {
		
			$random_id =rand(0,$sample_number-1);
			
			for ($j=0; $j<$dimension_number; $j++) {
			
				$centroid[$j * $cluster_number + $i] 
						=$data[$j * $sample_number + $random_id];
			}
		}
		
		// メインループ
		do {
			
			// 前のステップのエラー値を保持し,エラー値を初期化する.
			$old_error =$new_error;
			$new_error =0;

			// 前回のクラスタとセントロイドを初期化する.
			$cluster_size =array();
			$temp_centroid =array();
			
			for ($h=0; $h<$sample_number; $h++) {
				
				// 近いクラスタを探す.
				$min_distance =PHP_INT_MAX;
				
				for ($i=0; $i<$cluster_number; $i++) {
					
					$distance_from_centroid =0;
					
					for ($j=0; $j<$dimension_number; $j++) {
						
						$distance_from_centroid +=
								($data[$j * $sample_number + $h] 
								- $centroid[$j * $cluster_number + $i]) * 
								($data[$j * $sample_number + $h] 
								- $centroid[$j * $cluster_number + $i]);
					}
					
					$distance_from_centroid =sqrt($distance_from_centroid);
					
					if ($distance_from_centroid < $min_distance) {
						
						$label[$h] =$i;
						$min_distance =$distance_from_centroid;
					}
				}
				
				// セントロイドとクラスタ,誤差を更新する.
				for ($j=0; $j<$dimension_number; $j++) {
            		
					$temp_centroid[$j * $cluster_number + $label[$h]] +=
							$data[$j * $sample_number + $h];
				}
				
				$cluster_size[$label[$h]]++;
				$new_error +=$min_distance;
			}

			// セントロイドを更新する.
			for ($i=0; $i<$cluster_number; $i++) {
				
				for ($j=0; $j<$dimension_number; $j++) {
				
					$centroid[$j * $cluster_number + $i] = $cluster_size[$i] 
							? $temp_centroid[$j * $cluster_number + $i] / $cluster_size[$i] 
							: $temp_centroid[$j * $cluster_number + $i];
				}
			}
			
		} while (abs($new_error - $old_error) > $tolerance);
		
		return array($centroid, $label);
	}
	
	//-------------------------------------
	// 対数尤度を求める.
	public function calculate_likelihood (
			$dimension_number,
			$sample_number,
			$cluster_number,
			$variance) {
		
		$result =-(($sample_number / 2) * log(2 * pi()))
				-((($sample_number * $dimension_number) / 2) * log($variance))
				-((($sample_number - $cluster_number) / 2))
				+($sample_number * log($sample_number));
		
		return $result;
	}

	//-------------------------------------
	// テスト用プロット
	function draw_plot ($data, $output_filename, $user_options=array()) {
	
		$options =array(
			"width" =>100,
			"height" =>100,
			"image_width" =>300,
			"image_height" =>300,
			"bgcolor" =>0xffffff,
			"plot_size" =>5
		);
		
		foreach ($user_options as $key => $value) {
		
			$options[$key] =$value;
		}

		$img =imagecreatetruecolor(
				$options["image_width"],
				$options["image_height"]);
		
		$color6hex =$options["bgcolor"];
		$color =imagecolorallocate($img,
				($color6hex & 0xff0000) >> 16,
				($color6hex & 0x00ff00) >>  8,
				($color6hex & 0x0000ff) >>  0);
		imagefill($img,0,0,$color);
		
		$width_factor =$options["image_width"]/$options["width"];
		$height_factor =$options["image_height"]/$options["height"];
		$rand_factor =rand(0,100);
		
		foreach ($data as $p => $value) {
			
			$x =$p%$options["width"];
			$y =($p-$x)/$options["width"];
			
			$color6hex =base_convert(substr(md5($value.$rand_factor),6,6),16,10);
			$color =imagecolorallocate($img,
					($color6hex & 0xff0000) >> 16,
					($color6hex & 0x00ff00) >>  8,
					($color6hex & 0x0000ff) >>  0);
			imagefilledellipse(
					$img,
					(int)($x * $width_factor),
					(int)($y * $height_factor),
					$options["plot_size"],
					$options["plot_size"],
					$color);
		}

		ob_start();
		imagepng($img);
		file_put_contents($output_filename,ob_get_clean());
		imagedestroy($img);
	}

	
	/*
		1辺100の正方形(2次元)内で12点サンプル。
		初期クラスタ数5。
	*/
	$size =100;
	$cluster_number =5;
	$sample_number =12;
	$dimension =2;
	
	$data =array();
	$data[0*$dimension+0] =9;
	$data[1*$dimension+0] =9;
	$data[0*$dimension+1] =15;
	$data[1*$dimension+1] =11;
	$data[0*$dimension+2] =11;
	$data[1*$dimension+2] =14;
	$data[0*$dimension+3] =12;
	$data[1*$dimension+3] =13;
	$data[0*$dimension+4] =43;
	$data[1*$dimension+4] =43;
	$data[0*$dimension+5] =48;
	$data[1*$dimension+5] =44;
	$data[0*$dimension+6] =86;
	$data[1*$dimension+6] =57;
	$data[0*$dimension+7] =87;
	$data[1*$dimension+7] =48;
	$data[0*$dimension+8] =36;
	$data[1*$dimension+8] =37;
	$data[0*$dimension+9] =37;
	$data[1*$dimension+9] =30;
	$data[0*$dimension+10] =55;
	$data[1*$dimension+10] =48;
	$data[0*$dimension+11] =56;
	$data[1*$dimension+11] =40;
	
	list($centroid,$label,$new_cluster_number) 
			=xmeans_clustering($data,$dimension,$sample_number,$cluster_number);
	
	$plot =array();
	
	for ($i=0; $i<$sample_number; $i++) {
		
		$x =$data[0*$dimension+$i];
		$y =$data[1*$dimension+$i];
		$plot[$y*$size+$x] =$label[$i];
		
	}
	
	draw_plot($plot,"./dest.png",array(
		"width" =>$size,
		"height" =>$size,
		"bgcolor" =>0xeeeeee,
		"plot_size" =>10,
	));
	
	print '<html><body><img src="./dest.png"></body></html>';
?>

コードばっかりですが、ご参考まで。

thx