Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Algorithms] Add clustering algorithms #20

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
8 changes: 8 additions & 0 deletions Assets/Scripts/Algorithms.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Assets/Scripts/Algorithms/Clustering.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 99 additions & 0 deletions Assets/Scripts/Algorithms/Clustering/AgglomerativeClusterer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// The agglomerative clusterer class performs agglomerative clustering with the stopping condition
// given by the size and radius constraints.
public class AgglomerativeClusterer : ISizeAndRadiusConstrainedClusterer {
public AgglomerativeClusterer(List<GameObject> objects, int maxSize, float maxRadius)
: base(objects, maxSize, maxRadius) {}

// Cluster the game objects.
public override void Cluster() {
// Add a cluster for every game object.
foreach (var obj in _objects) {
Cluster cluster = new Cluster(obj);
cluster.AddObject(obj);
_clusters.Add(cluster);
}

// Create a set containing all valid cluster indices.
HashSet<int> validClusterIndices = new HashSet<int>();
for (int i = 0; i < _clusters.Count; ++i) {
validClusterIndices.Add(i);
}

// Find the pairwise distances between all clusters.
// The upper triangular half of the distances matrix is unused.
float[,] distances = new float[_clusters.Count, _clusters.Count];
for (int i = 0; i < _clusters.Count; ++i) {
for (int j = 0; j < i; ++j) {
distances[i, j] = Vector3.Distance(_clusters[i].Coordinates, _clusters[j].Coordinates);
}
}

while (true) {
// Find the minimum distance between two clusters.
float minDistance = Mathf.Infinity;
int clusterIndex1 = -1;
int clusterIndex2 = -1;
for (int i = 0; i < _clusters.Count; ++i) {
for (int j = 0; j < i; ++j) {
if (distances[i, j] < minDistance) {
minDistance = distances[i, j];
clusterIndex1 = i;
clusterIndex2 = j;
}
}
}

// Check whether the minimum distance exceeds the maximum cluster radius, in which case the
// algorithm has converged. This produces a conservative solution because the radius of a
// merged cluster is less than or equal to the sum of the original cluster radii.
if (minDistance >= _maxRadius) {
break;
}

// Check whether merging the two clusters would violate the size constraint.
if (_clusters[clusterIndex1].Size() + _clusters[clusterIndex2].Size() > _maxSize) {
distances[clusterIndex1, clusterIndex2] = Mathf.Infinity;
continue;
}

// Merge the two clusters together.
int minClusterIndex = Mathf.Min(clusterIndex1, clusterIndex2);
int maxClusterIndex = Mathf.Max(clusterIndex1, clusterIndex2);
_clusters[minClusterIndex].Merge(_clusters[maxClusterIndex]);
_clusters[minClusterIndex].Recenter();
validClusterIndices.Remove(maxClusterIndex);

// Update the distances matrix using the distance between the cluster centroids.
// TODO(titan): Change the distance metric to use average or maximum linkage.
for (int i = 0; i < minClusterIndex; ++i) {
if (distances[minClusterIndex, i] < Mathf.Infinity) {
distances[minClusterIndex, i] =
Vector3.Distance(_clusters[minClusterIndex].Coordinates, _clusters[i].Coordinates);
}
}
for (int i = minClusterIndex + 1; i < _clusters.Count; ++i) {
if (distances[i, minClusterIndex] < Mathf.Infinity) {
distances[i, minClusterIndex] =
Vector3.Distance(_clusters[minClusterIndex].Coordinates, _clusters[i].Coordinates);
}
}
for (int i = 0; i < maxClusterIndex; ++i) {
distances[maxClusterIndex, i] = Mathf.Infinity;
}
for (int i = maxClusterIndex + 1; i < _clusters.Count; ++i) {
distances[i, maxClusterIndex] = Mathf.Infinity;
}
}

// Select only the valid clusters.
for (int i = _clusters.Count - 1; i >= 0; --i) {
if (!validClusterIndices.Contains(i)) {
_clusters.RemoveAt(i);
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

88 changes: 88 additions & 0 deletions Assets/Scripts/Algorithms/Clustering/Cluster.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

// The cluster class represents a collection of game objects.
public class Cluster {
// Coordinates of the cluster.
private Vector3 _coordinates = Vector3.zero;

// List of game objects in the cluster.
private List<GameObject> _objects = new List<GameObject>();

public Cluster() {}
public Cluster(in Vector3 coordinates) {
_coordinates = coordinates;
}
public Cluster(in GameObject obj) {
_coordinates = obj.transform.position;
}

// Get the cluster coordinates.
public Vector3 Coordinates {
get { return _coordinates; }
}

// Get the list of game objects.
public IReadOnlyList<GameObject> Objects {
get { return _objects; }
}

// Return the size of the cluster.
public int Size() {
return _objects.Count;
}

// Check whether the cluster is empty.
public bool IsEmpty() {
return Size() == 0;
}

// Calculate the radius of the cluster.
public float Radius() {
if (IsEmpty()) {
return 0;
}

Vector3 centroid = Centroid();
return _objects.Max(obj => Vector3.Distance(centroid, obj.transform.position));
}

// Calculate the centroid of the cluster.
public Vector3 Centroid() {
if (IsEmpty()) {
return Vector3.zero;
}

Vector3 centroid = Vector3.zero;
foreach (var obj in _objects) {
centroid += obj.transform.position;
}
centroid /= _objects.Count;
return centroid;
}

// Recenter the cluster's centroid to be the mean of all game objects' positions in the cluster.
public void Recenter() {
_coordinates = Centroid();
}

// Add a game object to the cluster.
// This function does not update the centroid of the cluster.
public void AddObject(in GameObject obj) {
_objects.Add(obj);
}

// Add multiple game objects to the cluster.
// This function does not update the centroid of the cluster.
public void AddObjects(in IReadOnlyList<GameObject> objects) {
_objects.AddRange(objects);
}

// Merge another cluster into this one.
// This function does not update the centroid of the cluster.
public void Merge(in Cluster cluster) {
AddObjects(cluster.Objects);
}
}
2 changes: 2 additions & 0 deletions Assets/Scripts/Algorithms/Clustering/Cluster.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions Assets/Scripts/Algorithms/Clustering/Clusterer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// The clusterer class is an interface for clustering algorithms.
public abstract class IClusterer {
// List of game objects to cluster.
protected List<GameObject> _objects = new List<GameObject>();

// List of clusters.
protected List<Cluster> _clusters = new List<Cluster>();

public IClusterer(List<GameObject> objects) {
_objects = objects;
}

// Get the list of game objects.
public IReadOnlyList<GameObject> Objects {
get { return _objects; }
}

// Get the list of clusters.
public IReadOnlyList<Cluster> Clusters {
get { return _clusters; }
}

// Cluster the game objects.
public abstract void Cluster();
}

// The size and radius-constrained clusterer class is an interface for clustering algorithms with
// size and radius constraints. The size is defined as the maximum number of game objects within a
// cluster, and the radius denotes the maximum distance from the cluster's centroid to any of its
// assigned game objects.
public abstract class ISizeAndRadiusConstrainedClusterer : IClusterer {
// Maximum cluster size.
protected readonly int _maxSize = 0;

// Maximum cluster radius.
protected readonly float _maxRadius = 0;

public ISizeAndRadiusConstrainedClusterer(List<GameObject> objects, int maxSize, float maxRadius)
: base(objects) {
_maxSize = maxSize;
_maxRadius = maxRadius;
}
}
2 changes: 2 additions & 0 deletions Assets/Scripts/Algorithms/Clustering/Clusterer.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

119 changes: 119 additions & 0 deletions Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// The k-means clusterer class performs k-means clustering.
public class KMeansClusterer : IClusterer {
public const float Epsilon = 1e-3f;

// Number of clusters.
private int _k = 0;

// Maximum number of iterations.
private int _maxIterations = 20;

public KMeansClusterer(List<GameObject> objects, int k, int maxIterations = 20) : base(objects) {
_k = k;
_maxIterations = maxIterations;
}

// Cluster the game objects.
public override void Cluster() {
// Initialize the clusters with centroids located at random game objects.
// Perform Fisher-Yates shuffling to find k random game objects.
System.Random random = new System.Random();
for (int i = _objects.Count - 1; i >= _objects.Count - _k; --i) {
int j = random.Next(i + 1);
(_objects[i], _objects[j]) = (_objects[j], _objects[i]);
}
for (int i = _objects.Count - 1; i >= _objects.Count - _k; --i) {
_clusters.Add(new Cluster(_objects[i]));
}

bool converged = false;
int iteration = 0;
while (!converged && iteration < _maxIterations) {
AssignObjectsToCluster();

// Calculate the new clusters as the mean of all assigned game objects.
converged = true;
for (int clusterIndex = 0; clusterIndex < _clusters.Count; ++clusterIndex) {
Cluster newCluster;
if (_clusters[clusterIndex].IsEmpty()) {
int objectIndex = random.Next(_objects.Count);
newCluster = new Cluster(_objects[objectIndex]);
} else {
newCluster = new Cluster(_clusters[clusterIndex].Centroid());
}

// Check whether the algorithm has converged by checking whether the cluster has moved.
if (Vector3.Distance(newCluster.Coordinates, _clusters[clusterIndex].Coordinates) >
Epsilon) {
converged = false;
}

_clusters[clusterIndex] = newCluster;
}

++iteration;
}

AssignObjectsToCluster();
}

private void AssignObjectsToCluster() {
// Determine the closest centroid to each game object.
foreach (var obj in _objects) {
float minDistance = Mathf.Infinity;
int minIndex = -1;
for (int clusterIndex = 0; clusterIndex < _clusters.Count; ++clusterIndex) {
float distance =
Vector3.Distance(_clusters[clusterIndex].Coordinates, obj.transform.position);
if (distance < minDistance) {
minDistance = distance;
minIndex = clusterIndex;
}
}
_clusters[minIndex].AddObject(obj);
}
}
}

// The constrained k-means clusterer class performs k-means clustering under size and radius
// constraints.
public class ConstrainedKMeansClusterer : ISizeAndRadiusConstrainedClusterer {
public ConstrainedKMeansClusterer(List<GameObject> objects, int maxSize, float maxRadius)
: base(objects, maxSize, maxRadius) {}

// Cluster the game objects.
public override void Cluster() {
int numClusters = (int)Mathf.Ceil(_objects.Count / _maxSize);
KMeansClusterer clusterer;
while (true) {
clusterer = new KMeansClusterer(_objects, numClusters);
clusterer.Cluster();

// Count the number of over-populated and over-sized clusters.
int numOverPopulatedClusters = 0;
int numOverSizedClusters = 0;
foreach (var cluster in clusterer.Clusters) {
if (cluster.Size() > _maxSize) {
++numOverPopulatedClusters;
}
if (cluster.Radius() > _maxRadius) {
++numOverSizedClusters;
}
}

// If all clusters satisfy the size and radius constraints, the algorithm has converged.
if (numOverPopulatedClusters == 0 && numOverSizedClusters == 0) {
break;
}

numClusters +=
(int)Mathf.Ceil(Mathf.Max(numOverPopulatedClusters, numOverSizedClusters) / 2.0f);
}
_clusters = new List<Cluster>(clusterer.Clusters);
}
}
2 changes: 2 additions & 0 deletions Assets/Scripts/Algorithms/Clustering/KMeansClusterer.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading