掌握机器学习中的聚类算法:C# 实现 K-Means、KNN 等

0 阅读8分钟

机器学习(Machine Learning)已成为解决各行业复杂问题的基石,但如何驾驭各种技术和工具可能令人困惑。理解机器学习最直观的方法之一是通过聚类算法,它是一种无监督学习技术。聚类通过相似性将数据点组织到不同的组中,非常适合处理客户细分、模式识别和异常检测等任务。

在本文中,我们将通过聚类方法及其在 C# 中的实现,深入探索重要的机器学习概念。我们将介绍常见算法如 K-Means、K 最近邻(KNN)和 DBSCAN,并展示如何在 C# 中实现用于聚类的深度学习。我们还将简要提及这些概念如何在工业应用中的“几何”项目中使用,但不会深入技术细节。

1. 监督学习(Supervised Learning)与无监督学习(Unsupervised Learning)

在深入了解聚类之前,首先需要理解监督学习与无监督学习之间的关键区别。

bannerSupervised-vs.-Unsupervised-Learning-1.png

  • 监督学习是通过带标签的数据进行学习。算法知道每个数据点的“正确”答案,任务是学习输入和输出之间的关系。

  • 无监督学习则处理无标签的数据,目标是发现数据中隐藏的模式或分组,而无需预先定义的标签。

聚类是无监督学习的一种形式,算法在没有任何指导的情况下发现数据中的自然分组。


2. 几何项目中的常见数据结构

在处理空间数据的行业中,通常会有内部的“几何”项目来管理核心数据结构和算法。这些项目封装了处理点、地理坐标和投影的逻辑,从而使复杂的空间计算(如聚类)更加简便。

在深入聚类算法之前,了解这些几何项目中使用的关键数据结构非常重要。这些结构构成了大多数空间操作的基础,并有助于在整个系统中标准化计算。

墨卡托投影(Mercator Projection):内部表示

为了简化内部计算,墨卡托投影被广泛应用。它将地理坐标(纬度和经度)转换为二维平面,这使几何计算更加直观。在执行聚类等需要高效计算的任务时,这种转换非常有用。

Mercator 类用于将地理坐标转换为墨卡托投影,从而便于对空间数据进行处理:

public class Mercator
{
    public double X { get; private set; }
    public double Y { get; private set; }
    
    // 构造函数,直接从纬度和经度进行转换
    public Mercator(double lat, double lng)
    {
        X = lng * (Math.PI / 180.0);  // 将经度转换为弧度
        Y = Math.Log(Math.Tan((90.0 + lat) * Math.PI / 360.0));  // 墨卡托投影公式
    }

    // 工厂方法:通过 LatLng 对象创建一个 Mercator 对象
    public static Mercator FromLatLng(LatLng latLng)
    {
        return new Mercator(latLng.Latitude, latLng.Longitude);
    }
}

纬度/经度(Latitude/Longitude):外部表示

虽然墨卡托投影简化了内部计算,但外部系统(如 API)通常需要地理坐标的纬度和经度表示。在这种情况下,我们将内部表示转换回更为常见的地理坐标格式。

LatLng 类封装了这些地理坐标,并提供了从墨卡托投影转换的方法:

public class LatLng
{
    public double Latitude { get; set; }
    public double Longitude { get; set; }

    // 构造函数,直接初始化纬度和经度
    public LatLng(double latitude, double longitude)
    {
        Latitude = latitude;
        Longitude = longitude;
    }

    // 工厂方法:通过 Mercator 对象创建一个 LatLng 对象
    public static LatLng FromMercator(Mercator mercator)
    {
        double lat = (Math.Atan(Math.Exp(mercator.Y)) * 360.0 / Math.PI) - 90.0;  // 纬度的逆墨卡托公式
        double lng = mercator.X * 180.0 / Math.PI;  // 将 X(弧度)转换回经度
        return new LatLng(lat, lng);
    }
}

聚类的简化表示

为了便于讨论聚类算法,我们将使用一个简化的 Point 类。该类表示二维平面上的数据点,具有 XY 坐标,对于大多数聚类算法来说,这些坐标已经足够。

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public Point(double x, double y)
    {
        X = x;
        Y = y;
    }
}

这些数据结构—MercatorLatLngPoint—提供了处理不同格式空间数据的灵活性。在下一节中,我们将探讨如何使用这些结构在 C# 中实现 K-Means、KNN 和 DBSCAN 等聚类算法。


3. 常见的 C# 聚类算法

K-Means

K-Means 是最简单和最常用的聚类算法之一。它通过将数据点分成 K 个簇,其中每个点属于离质心最近的簇。

0__XwxbKHayTU8QG44.png

以下是 K-Means 的一个基本 C# 实现,使用数组来处理数据点:

public class KMeans
{
    public static KMeansResponse Cluster(List<Point> data, int k, int maxIterations = 100)
    {
        Random rand = new Random();
        List<Point> centroids = new List<Point>();
        List<int> labels = new List<int>(new int[data.Count]);

        // 随机初始化质心
        for (int i = 0; i < k; i++)
        {
            centroids.Add(data[rand.Next(data.Count)]);
        }

        for (int iter = 0; iter < maxIterations; iter++)
        {
            // 根据最近的质心分配标签
            for (int i = 0; i < data.Count; i++)
            {
                labels[i] = ClosestCentroid(data[i], centroids);
            }

            // 更新质心
            for (int j = 0; j < k; j++)
            {
                var clusterPoints = data.Where((_, idx) => labels[idx] == j).ToList();
                if (clusterPoints.Count > 0)
                {
                    centroids[j] = Mean(clusterPoints);
                }
            }
        }

        return new KMeansResponse(centroids, labels);
    }

    // 计算最接近的质心
    private static int ClosestCentroid(Point point, List<Point> centroids)
    {
        double minDistance = double.MaxValue;
        int closestIndex = -1;

        for (int i = 0; i < centroids.Count; i++)
        {
            double distance = EuclideanDistance(point, centroids[i]);
            if (distance < minDistance)
            {
                minDistance = distance;
                closestIndex = i;
            }
        }
        return closestIndex;
    }

    // 计算点集的均值
    private static Point Mean(List<Point> points)
    {
        double sumX = points.Sum(p => p.X);
        double sumY = points.Sum(p => p.Y);
        return new Point(sumX / points.Count, sumY / points.Count);
    }

    // 欧几里得距离计算
    private static double EuclideanDistance(Point a, Point b)
    {
        double deltaX = a.X - b.X;
        double deltaY = a.Y - b.Y;
        return Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
    }

    public class KMeansResponse
    {
        public List<Point> Centroids { get; set; }
        public List<int> Labels { get; set; }

        public KMeansResponse(List<Point> centroids, List<int> labels)
        {
            Centroids = centroids;
            Labels = labels;
        }
    }
}

K-最近邻(KNN)

虽然 KNN 通常用作分类算法,但它也可以用于聚类。它通过将数据点分配给其 k 个最近邻中最常见的群体来完成聚类。

image-8.png

以下是一个简单的 KNN 实现用于聚类的示例:

public class KNN
{
    public static int Classify(Point point, KNNTrainedData trainedData, int k)
    {
        // 计算输入点与所有训练数据点之间的距离
        var distances = trainedData.Data
                                   .Select((d, i) => (Distance: EuclideanDistance(point, d), Label: trainedData.Labels[i]))
                                   .OrderBy(d => d.Distance)
                                   .Take(k)
                                   .GroupBy(d => d.Label)
                                   .OrderByDescending(g => g.Count())
                                   .First().Key;

        return distances;
    }

    // 欧几里得距离计算
    private static double EuclideanDistance(Point p1, Point p2)
    {
        double deltaX = p1.X - p2.X;
        double deltaY = p1.Y - p2.Y;
        return Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
    }

    public class KNNTrainedData
    {
        public List<Point> Data { get; set; }
        public List<int> Labels { get; set; }

        public KNNTrainedData(List<Point> data, List<int> labels)
        {
            Data = data;
            Labels = labels;
        }
    }
}

DBSCAN(Density-based spatial clustering of applications with noise)

DBSCAN 适用于识别不同形状和大小的聚类。它将足够接近的点归为一组,而不符合条件的点则标记为噪点。

images.png

public class DBSCAN
{
    public static List<List<Point>> Cluster(List<Point> data, double eps, int minPts)
    {
        var clusters = new List<List<Point>>();
        var visited = new HashSet<int>();

        for (int i = 0; i < data.Count; i++)
        {
            if (!visited.Contains(i))
            {
                visited.Add(i);
                var neighbors = GetNeighbors(data, i, eps);

                if (neighbors.Count >= minPts)
                {
                    var cluster = ExpandCluster(data, neighbors, eps, minPts, visited);
                    clusters.Add(cluster);
                }
            }
        }

        return clusters;
    }

    // 获取 epsilon 范围内的邻居
    private static List<int> GetNeighbors(List<Point> data, int idx, double eps)
    {
        var neighbors = new List<int>();

        for (int i = 0; i < data.Count; i++)
        {
            if (EuclideanDistance(data[idx], data[i]) <= eps)
            {
                neighbors.Add(i);
            }
        }

        return neighbors;
    }

    // 通过访问邻居扩展簇
    private static List<Point> ExpandCluster(List<Point> data, List<int> neighbors, double eps, int minPts, HashSet<int> visited)
    {
        var cluster = new List<Point>();

        foreach (var neighbor in neighbors)
        {
            if (!visited.Contains(neighbor))
            {
                visited.Add(neighbor);
                var newNeighbors = GetNeighbors(data, neighbor, eps);

                if (newNeighbors.Count >= minPts)
                {
                    neighbors.AddRange(newNeighbors);
                }
            }

            cluster.Add(data[neighbor]);
        }

        return cluster;
    }

    // 欧几里得距离计算
    private static double EuclideanDistance(Point p1, Point p2)
    {
        double deltaX = p1.X - p2.X;
        double deltaY = p2.Y - p1.Y;
        return Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
    }
}

这些聚类算法展示了如何在 C# 中应用 K-Means、KNN 和 DBSCAN 来处理空间数据。接下来,您可以根据实际需求选择适合的算法。

Here’s the translation:

4. 在 C# 中实现深度学习进行聚类

虽然传统的聚类算法如 K-Means 和 DBSCAN 非常有效,但深度学习可以通过学习更复杂的数据表示将聚类提升到一个新水平。使用 ML.NET 等库,您可以实现基于神经网络的聚类。

1. 组织训练数据(Training Data)

首先,您需要使用适当的类来结构化训练数据,以便于 ML.NET 的数据管道。以下是如何定义输入数据类:

public class DataPoint
{
    public float Feature1 { get; set; }
    public float Feature2 { get; set; }
}

public class ClusteredDataPoint : DataPoint
{
    public uint PredictedClusterId { get; set; }  // 用于预测
}

接下来,使用 IDataView 组织数据,这是 ML.NET 结构化数据的格式:

var data = new List<DataPoint>
{
    new DataPoint { Feature1 = 1.0f, Feature2 = 2.0f },
    new DataPoint { Feature1 = 2.0f, Feature2 = 3.0f },
    new DataPoint { Feature1 = 3.0f, Feature2 = 4.0f }
    // 根据需要添加更多数据点
};

var trainingData = mlContext.Data.LoadFromEnumerable(data);

2. 构建和训练模型(Model)

组织好训练数据后,您可以定义管道,指定转换(连接特征)和训练器(KMeans 进行聚类)。

var pipeline = mlContext.Transforms.Concatenate("Features", new[] { "Feature1", "Feature2" })
    .Append(mlContext.Clustering.Trainers.KMeans("Features", numberOfClusters: 3));

// 训练模型
var model = pipeline.Fit(trainingData);

3. 保存训练好的模型

要将训练好的模型保存到文件中,您可以使用 ML.NET 的 Save 方法。这使您可以在以后重用模型,而无需重新训练。

using (var fileStream = new FileStream("kmeansModel.zip", FileMode.Create, FileAccess.Write, FileShare.Write))
{
    mlContext.Model.Save(model, trainingData.Schema, fileStream);
}

4. 加载模型以备将来使用

您可以稍后加载已保存的模型,这使您可以在不需要重新训练的情况下使用它进行预测。

ITransformer loadedModel;

using (var stream = new FileStream("kmeansModel.zip", FileMode.Open, FileAccess.Read, FileShare.Read))
{
    loadedModel = mlContext.Model.Load(stream, out var modelInputSchema);
}

5. 使用模型对新数据点进行分类

加载模型后,您可以使用它来预测新数据点的聚类:

var predictionEngine = mlContext.Model.CreatePredictionEngine<DataPoint, ClusteredDataPoint>(loadedModel);

var newPoint = new DataPoint { Feature1 = 2.5f, Feature2 = 3.5f };
var prediction = predictionEngine.Predict(newPoint);

Console.WriteLine($"预测的聚类:{prediction.PredictedClusterId}");

这个结构演示了使用 ML.NET 在 C# 中进行机器学习任务的典型工作流程,从数据准备到模型部署。深度学习模型提供了解决更复杂聚类任务的灵活性和能力,尽管它们带来了更高的计算成本。

结论

聚类提供了一个强有力的机器学习概念入门,特别是当在 C# 这种熟悉的语言中实现时。无论您是使用经典算法如 K-Means 还是利用深度学习的强大功能,聚类都可以帮助您发现数据中的隐藏模式。

通过学习如何应用这些技术,您将更好地应对现实应用中的无监督学习问题。请继续关注未来的文章,我们将深入探讨 C# 中的高级机器学习主题。