目录
介绍
聚类分析是机器学习和数据分析中的一种技术,涉及根据某些特征或特征将相似的数据点分组在一起。聚类的目标是识别数据集中的自然模式或结构,将其组织成子集或聚类。与其他聚类中的数据点相比,同一集群中的数据点彼此之间应该更相似。
从本质上讲,聚类分析允许发现数据中的固有结构或关系,从而提供对其底层组织的见解。该技术广泛应用于各个领域,包括模式识别、图像分析、客户细分和异常检测。常见的聚类算法包括K-Means、分层聚类、CURE和DBSCAN。
在本系列文章中,我们将在C#中实现主要的聚类分析技术,并努力比较每种技术,评估它们的优缺点。这种探索将提供对每种算法更适合的情况和数据类型的见解。
本文最初发布在这里。
什么是聚类?
聚类分析是机器学习和数据分析中的一种技术,它涉及根据其固有的相似性将一组数据点分组为子集或聚类。聚类的主要目标是组织和发现数据中的模式,从而识别有意义的结构或关系。
该过程涉及将数据划分为组,以便同一集群中的数据点彼此之间比其他集群中的数据点更相似。相似性通常使用距离度量来衡量,并且使用各种算法(如K-Means、分层聚类和DBSCAN)来执行聚类任务。例如,识别出上图中明确分开的三个不同簇似乎很自然。
但是,聚类有时可能会变得更具挑战性,如下面的示例所示,其中聚类表现出更不规则的形状,并且不容易通过线性边界分开。
聚类算法应该具备哪些特征?
典型的反应是,一如既往:它各不相同。这实际上取决于所讨论的具体应用。实际上,某些算法在某些情况下表现出色,而在其他情况下表现不佳。一些算法可能表现出对具有挑战性的场景的适应性,但计算成本很高。在本节中,我们的目的是强调通常必不可少的特征。
为了确保最大的简洁性,我们的研究将只关注连续输入变量,只考虑可以定义距离和角度的欧几里得空间。这种限制是由于某些算法涉及点平均值的计算,这项任务在欧几里得空间中明显更简单。
我们将如何比较算法?
我们在本系列中的目标是评估各种最先进的算法,我们将通过利用仅包含两个特征(X和Y)的简单预定义数据集来实现这一目标。
- 第一个数据集是最基本的数据集,它将使我们能够观察算法在简单场景中的表现。
- 第二个数据集几乎相同,但包含两个异常值,提供了在存在异常值的情况下对算法行为的洞察。
- 最后,第三个数据集探讨了数据不可线性分离或呈现不规则形状时的算法性能。
目标是评估每个分析算法在这三个数据集上的表现,并确定结果聚类的相关性。具体来说,我们将非常详细地研究以下问题。
是否需要预定义集群数量?
确定适当的聚类数量一直是一个挑战,导致一些算法(例如成熟的K-Means)需要这个数字作为输入。这需要事先了解数据集中潜在的组数。
重要
需要注意的是,此要求不一定是缺点,因为指定集群数量可以与特定的业务需求保持一致。
相反,其他算法不会强制执行预定数量的聚类;相反,他们自主地将其作为其固有过程的一部分。
对异常值的敏感性意味着什么?
异常值是与数据集的其余部分显著偏离的数据点,通常与分布的中心趋势相距甚远。与数据集中的大多数其他观测值相比,这些数据点被认为是不寻常的或非典型的。异常值可能会影响统计分析和机器学习算法,可能导致结果偏斜或模型有偏差。识别和处理异常值在数据预处理中至关重要,以确保分析过程的稳健性和准确性。
以红色突出显示的两个点显然是异常的,因为它们与其他点不一致。
第二个数据集将使我们能够观察算法对异常值的敏感程度。
当数据呈现不规则形状时会发生什么情况?
大多数算法都可以有效地处理线性可分离的数据,这意味着可以用直线或平面划分的数据。但是,可能会出现数据显示不规则或退化形状的情况,如下图所示。在这种情况下,该算法是否具备对数据集进行准确聚类的能力?
算法的实现是否简单明了,是否表现出效率?
最后但并非最不重要的一点是,评估该算法是否可以在特定的编程语言中轻松实现和理解至关重要。过于复杂、需要非凡的数学技能或难以表达的算法可能不会得到广泛的应用。
同样重要的是它的效率。问题再次出现:如果计算复杂度呈指数级增长,那么能够处理异常值或区分不规则形状的高效方法有多大价值?
在本系列中,我们将努力仔细研究所研究算法的这些特征中的每一个。这种方法将更清楚地了解每种算法的优缺点。
我们要分析哪些算法?
存在多种聚类算法以及每种算法的不同版本。在这种情况下,我们将特别关注三个:首先,广泛使用的K-Means,其次是分层方法,最后是经常被低估的DBSCAN技术。
在深入研究这三种算法的C#实现之前,我们将在这篇文章中简要概述Visual Studio项目的逻辑结构。
如上一篇文章所述,我们将专门使用具有两个特征X和Y的数据集。因此,我们将有一个非常简单的DataPoint类来对记录进行建模。
public class DataPoint : IEquatable<DataPoint>
{
public double X { get; set; }
public double Y { get; set; }
public double ComputeDistance(DataPoint other)
{
var X0 = other.X; var Y0 = other.Y;
return Math.Sqrt((X-X0)*(X-X0) + (Y-Y0) * (Y-Y0));
}
public bool Equals(DataPoint? other)
{
return other != null && X == other.X && Y == other.Y;
}
public override int GetHashCode()
{
return X.GetHashCode() ^ Y.GetHashCode();
}
}
重要
请注意,我们已经实现了一种ComputeDistance方法来评估点之间的相似性或不同性。选择的距离度量是欧几里得度量。在实际场景中,应通过依赖注入在构造函数中将距离作为参数提供。
dataset只是一个DataPoint对象列表。
public class Dataset
{
public List<DataPoint> Points { get; set; }
private Dataset() { Points = new List<DataPoint>(); }
public static Dataset Load(string path)
{
var set = new Dataset();
var contents = File.ReadAllLines(path);
foreach (var content in contents.Skip(1))
{
var d = content.Split(';');
var point = new DataPoint()
{ X = Convert.ToDouble(d[0]), Y = Convert.ToDouble(d[1]) };
set.Points.Add(point);
}
return set;
}
}
重要
数据通过文件提供,并使用该Load方法加载。
由于目标是定义集群,因此我们需要在C#中对此实体进行建模。因此,DataCluster只是一个标有簇号的DataPoint对象列表。
public class DataCluster : IEquatable<DataCluster>
{
public List<DataPoint> Points { get; set; }
public string Cluster { get; set; }
public DataPoint GetCentroid()
{
var avgx = Points.Average(t => t.X);
var avgy = Points.Average(t => t.Y);
return new DataPoint() { X = avgx, Y = avgy };
}
public bool Equals(DataCluster? other)
{
return other != null && Cluster == other.Cluster;
}
public override int GetHashCode()
{
return Cluster.GetHashCode();
}
}
有了这个基础代码,就可以定义一个所有算法都需要实现的接口。
public interface IClusteringStragtegy
{
List<DataCluster> Cluster(Dataset set, int clusters);
}
此接口仅包含一种方法,其目的是从数据集中获取DataCluster对象和预定义数量的要发现的聚类。
为了全面起见,包含数据的文件类似于前面介绍的第一个数据集的以下格式。
X;Y
0.2;0.22
0.22;0.2
0.18;0.205
0.188;0.18
0.205;0.185
0.8;0.792
0.82;0.8
0.78;0.82
0.795;0.825
0.815;0.78
0.34;0.84
0.32;0.86
0.335;0.88
0.345;0.82
0.35;0.855
我们现在可以开始我们的探索,从古老的K-Means开始。
什么是K-Means?
K-Means是一种常用的聚类算法,用于机器学习和数据分析。它属于分区方法类别,旨在将数据集划分为特定数量的聚类(K),其中每个数据点都属于平均值最接近的聚类。该算法通过将数据点分配给具有最接近质心的聚类并根据分配点的平均值更新质心来迭代细化其聚类。
因此,K-Means算法的基本步骤如下。请注意,我们使用了维基百科中的图像。
- 初始化
我们从数据集中随机选择K个数据点作为初始聚类质心。
- 分配
我们将每个数据点分配给质心最接近的聚类(通常基于欧几里得距离)。
- 更新
我们根据分配给该聚类的数据点的平均值重新计算每个聚类的质心。
- 重复
我们重复分配和更新步骤,直到收敛,质心不再显着变化或达到预定义的迭代次数。
重要1
聚类的质心是该聚类中所有数据点的算术平均值。
重要2
K-Means中点的初始化并非完全随机;相反,它旨在以一种可能将KK点分布在不同集群中的方式选择点。为此,存在各种技术,但我们不会在这里深入研究这些技术的细节。有兴趣的读者可以参考实现的代码。
重要3
在实际场景中,通常的做法是在处理数据之前对数据进行规范化。
在C#语言中实现
如上一篇文章所述,我们需要实现IClusteringStrategy接口。
public class KMeansClusteringStrategy : IClusteringStragtegy
{
public KMeansClusteringStrategy()
{
// DI: Inject distance in a real-world scenario
}
public List<DataCluster> Cluster(Dataset set, int clusters)
{
var groups = Initialize(set, clusters);
while (true)
{
var changed = false;
foreach (var point in set.Points)
{
var currentCluster = groups.FirstOrDefault
(t => t.Points.Any(x => x == point));
if (currentCluster == null) changed = true;
var d = double.MaxValue; DataCluster selectedCluster = null;
foreach (var group in groups)
{
var distance = point.ComputeDistance(group.GetCentroid());
if (d > distance)
{
selectedCluster = group;
d = distance;
}
}
if (selectedCluster != currentCluster)
{
selectedCluster?.Points.Add(point);
currentCluster?.Points.Remove(point);
changed = true;
}
}
if (!changed) break;
}
return groups;
}
#region Private Methods
private List<DataCluster> Initialize(Dataset set, int clusters)
{
var results = new List<DataCluster>();
var selectedPoints = new List<DataPoint>();
var r = new Random(); var k = r.Next(set.Points.Count);
var p = set.Points.ElementAt(k); selectedPoints.Add(p);
var c = 1;
var cluster = new DataCluster()
{ Cluster = c.ToString(), Points = new List<DataPoint>() { p } };
results.Add(cluster);
while (c < clusters)
{
var remainingPoints = set.Points.Except(selectedPoints).ToList();
var maximum = double.MinValue; DataPoint other = null;
foreach (var point in remainingPoints)
{
var distance = selectedPoints.Select(x => x.ComputeDistance(point)).Min();
if (distance > maximum)
{
maximum = distance;
other = point;
}
}
selectedPoints.Add(other);
c++;
var newCluster = new DataCluster()
{ Cluster = c.ToString(), Points = new List<DataPoint>() { other } };
results.Add(newCluster);
}
return results;
}
#endregion
}
K-Means以其简单性而闻名,这在前面只有几行的代码中很明显。毫无疑问,这种简单性有助于其广泛使用。就我们而言,我们将探讨该算法在我们的三个数据集上的表现。
第一个数据集的结果
请记住,第一个数据集纯粹是学术性的,可以作为一个简单的测试,以确保算法在简单案例中发挥作用。
我们可以观察到,正如预期的那样,集群被完美地识别出来,关于这个特定案例没有什么需要注意的。
第二个数据集的结果
第二个数据集用于说明异常值的影响。
这一结果引发了进一步的评论。尽管只有两个异常值(尽管被夸大了),但该算法似乎很挣扎。这强调了本系列的一个关键见解:K-Means对异常值高度敏感,在不加选择地应用此方法之前,应实施专门为异常值检测而设计的强大预处理步骤。
第三个数据集的结果
第三个数据集用于演示该算法在更具挑战性或病理的情况下的表现。
谨慎
在本例中,有两个集群(而不是像以前那样有三个)。
再一次,这一结果引发了进一步的评论:K-Means在处理不规则形状或不可线性可分离的数据时遇到了很大的困难。这一特性可能会带来挑战。在我们的图中,相对容易辨别数据是病态的,但在具有非常高维度的现实世界场景中,可视化这一事实变得更加具有挑战性。我们需要相信我们对手头问题的理解,以确保我们能够应用K-Means并获得准确的结果。
值得铭记
K-Means易于理解和实现,在处理简单和球形聚类时执行高效。但是,它有局限性:
- 它对异常值高度敏感。
- 它需要预定义数量的集群作为输入。
- 它不太适合形状不规则的数据集。
我们能否改进和解决此处提到的一些限制?这是下一篇文章的主题,专门讨论分层聚类。
什么是分层聚类?
分层聚类是一种聚类算法,用于创建聚类层次结构。该算法根据聚类之间的相似性或不相似性迭代合并或划分聚类,直到满足预定义的停止条件。
分层聚类有两种主要类型:聚集性聚类或分裂性聚类。
- 集聚分层聚类从每个数据点开始作为单独的聚类,并依次合并最接近的聚类对,直到只剩下一个聚类。合并过程通常基于距离度量,如欧几里得距离。
- 分割分层聚类从单个聚类中的所有数据点开始,然后递归地将聚类划分为更小的聚类,直到每个数据点都位于自己的聚类中。与集聚聚类类似,分裂聚类使用距离度量或链接标准进行分裂过程。
重要
在这篇文章中,我们将只实现一个聚集的分层聚类。
从理论上讲,分层聚类是有利的,因为它不需要事先指定聚类的数量。但是,它需要一个不容易有机确定的停止标准。因此,在这篇文章中,我们将预定义要识别的集群数量。
如何实现分层聚类?
想象一下,我们需要对以下数据集进行聚类。
我们从其自身集群中的每个点开始(在我们的例子中,我们从11个集群开始)。随着时间的流逝,通过合并两个较小的集群将形成更大的集群。但是我们如何决定合并哪两个集群呢?我们将在下一节中深入探讨,但现在,该算法可以描述如下:
WHILE we have the right number of clusters DO
pick the best two clusters to merge
combine those clusters into one cluster
END
我们如何决定合并哪两个集群?
事实上,选择要合并的最佳集群有几个规则。在这篇文章中,我们将使用任意两点之间的最小距离,从每个聚类中选择一个点。其他规则是可能的;例如,我们可以识别质心之间距离最小的一对。
- 根据我们的规则,最近的两个簇是9和11,所以我们合并了这两个簇。
- 现在,我们选择将点8与之前的两品脱聚类(9和11)聚类,因为点8和9非常接近,并且没有其他对未聚类的点如此接近。
- 然后,我们选择对点5和7进行聚类。
- 依此类推,直到我们达到所需的集群数量。
在C#语言中实现
我们需要实现IClusteringStrategy接口。
public class HierarchicalClusteringStrategy : IClusteringStragtegy
{
public List<DataCluster> Cluster(Dataset set, int clusters)
{
var results = new List<DataCluster>(); var c = 1;
foreach (var point in set.Points)
{
var cluster = new DataCluster()
{
Cluster = c.ToString(),
Points = new List<DataPoint> { point }
};
results.Add(cluster);
c++;
}
var currentNumber = results.Count;
while (currentNumber > clusters)
{
var minimum = double.MaxValue; var clustersToMerge = new List<DataCluster>();
foreach (var cluster in results)
{
var remainingClusters = results.Where(x => x != cluster).ToList();
foreach (var remainingCluster in remainingClusters)
{
//var distance = cluster.GetCentroid().ComputeDistance
//(remainingCluster.GetCentroid());
var distance = ComputeDistanceBetweenClusters
(cluster, remainingCluster);
if (distance < minimum)
{
minimum = distance;
clustersToMerge = new List<DataCluster>()
{ cluster, remainingCluster };
}
}
}
// Merge clusters
var newCluster = new DataCluster()
{
Cluster = clustersToMerge.Select
(x => Convert.ToInt32(x.Cluster)).Min().ToString(),
Points = clustersToMerge.SelectMany(x => x.Points).ToList()
};
foreach (var delete in clustersToMerge)
results.Remove(delete);
results.Add(newCluster);
currentNumber = results.Count;
}
return results;
}
private double ComputeDistanceBetweenClusters
(DataCluster dataCluster1, DataCluster dataCluster2)
{
var minimum = double.MaxValue;
foreach (var point1 in dataCluster1.Points)
{
foreach (var point2 in dataCluster2.Points)
{
var distance = point1.ComputeDistance(point2);
if (distance < minimum)
{
minimum = distance;
}
}
}
return minimum;
}
}
第一个数据集的结果
请记住,第一个数据集纯粹是学术性的,可以作为一个简单的测试,以确保算法在简单案例中发挥作用。
我们可以观察到,正如预期的那样,集群被完美地识别出来,关于这个特定案例没有什么需要注意的。
第二个数据集的结果
第二个数据集用于说明异常值的影响。
尽管存在异常值,但很明显,分层聚类可以适当地响应并有效地识别精确的聚类。因此,与K均值相比,分层聚类对异常值表现出更大的弹性。
第三个数据集的结果
第三个数据集用于演示该算法在更具挑战性或病理的情况下的表现。
谨慎
在本例中,有两个集群(而不是像以前那样有三个)。
因此,分层聚类似乎可以熟练地处理不规则的形状,并且不受非线性可分离数据的阻碍。
值得铭记
分层聚类易于理解和实现,并且对异常值非常可靠。此外,它似乎非常适合具有不规则形状的数据集。
此外,分层聚类易于理解和实现,并且对异常值非常健壮。此外,它似乎非常适合具有不规则形状的数据集。分层聚类似乎是一种神奇的方法,这引发了一个问题,即为什么它没有被更广泛地使用。然而,在前面的例子中,一个关键点并不明显:与K-Means等方法相比,分层聚类可能计算密集,不太适合大型数据集。
分层聚类的算法不是很有效。在每一步中,我们必须计算每对集群之间的距离,以便找到最佳合并。初始步骤需要O(n2)时间[nn是数据集中的记录数],但后续步骤需要的时间时间与(n−1)2、(n−2)2、...直到nn的平方和是O(n3),因此算法是立方的。因此,除了相当少量的点之外,它无法运行。
Leskovec, Rajaraman, Ullman (海量数据集的挖掘)
即使存在此算法的更优化版本(尤其是那些包含优先级队列的版本),执行时间仍然相对较长。因此,此方法只能用于小型数据集。
我们能否改进和解决此处提到的一些限制?为避免本文超载,对答案感兴趣的读者可以在这里找到它。
https://www.codeproject.com/Articles/5375470/Comparing-K-Means-and-Others-Algorithms-for-Data-C