Go语言机器学习实战:聚类算法与无监督学习

无监督学习是机器学习的重要分支,它从无标签数据中发现模式和结构。聚类算法是无监督学习的核心,本文将深入探讨如何使用Go语言实现常见的聚类算法。

文章总体概览信息图

一、聚类算法概述

聚类是将数据点分组的过程,使得同一组内的数据点相似度较高,而不同组之间的数据点相似度较低。常见的聚类算法包括:

  • K-Means:基于距离的划分方法,简单高效
  • 层次聚类:构建层次化的聚类树
  • DBSCAN:基于密度的聚类,能发现任意形状的簇
  • 高斯混合模型:概率模型,考虑数据的分布

二、K-Means聚类实现

2.1 算法原理

K-Means算法的核心思想是:

  1. 随机选择K个初始质心
  2. 将每个数据点分配到最近的质心
  3. 重新计算每个簇的质心
  4. 重复步骤2-3直到收敛

2.2 Go语言实现

package main

import (
    "fmt"
    "math"
    "math/rand"
    "time"
)

type Point struct {
    Features []float64
}

type KMeans struct {
    K         int
    Centroids []Point
    MaxIter   int
}

func NewKMeans(k, maxIter int) *KMeans {
    return &KMeans{
        K:       k,
        MaxIter: maxIter,
    }
}

func (km *KMeans) distance(p1, p2 Point) float64 {
    var sum float64
    for i := 0; i < len(p1.Features); i++ {
        sum += math.Pow(p1.Features[i]-p2.Features[i], 2)
    }
    return math.Sqrt(sum)
}

func (km *KMeans) fit(points []Point) {
    rand.Seed(time.Now().UnixNano())
    
    // 随机初始化质心
    km.Centroids = make([]Point, km.K)
    for i := 0; i < km.K; i++ {
        idx := rand.Intn(len(points))
        km.Centroids[i] = points[idx]
    }
    
    for iter := 0; iter < km.MaxIter; iter++ {
        // 分配数据点到簇
        clusters := make([][]Point, km.K)
        for _, point := range points {
            minDist := math.MaxFloat64
            clusterIdx := 0
            for i, centroid := range km.Centroids {
                dist := km.distance(point, centroid)
                if dist < minDist {
                    minDist = dist
                    clusterIdx = i
                }
            }
            clusters[clusterIdx] = append(clusters[clusterIdx], point)
        }
        
        // 更新质心
        prevCentroids := make([]Point, km.K)
        copy(prevCentroids, km.Centroids)
        
        for i, cluster := range clusters {
            if len(cluster) == 0 {
                continue
            }
            newCentroid := Point{Features: make([]float64, len(cluster[0].Features))}
            for _, point := range cluster {
                for j := 0; j < len(point.Features); j++ {
                    newCentroid.Features[j] += point.Features[j]
                }
            }
            for j := 0; j < len(newCentroid.Features); j++ {
                newCentroid.Features[j] /= float64(len(cluster))
            }
            km.Centroids[i] = newCentroid
        }
        
        // 检查收敛
        converged := true
        for i := 0; i < km.K; i++ {
            if km.distance(km.Centroids[i], prevCentroids[i]) > 1e-6 {
                converged = false
                break
            }
        }
        if converged {
            fmt.Printf("算法在第%d次迭代收敛\n", iter+1)
            break
        }
    }
}

func (km *KMeans) predict(point Point) int {
    minDist := math.MaxFloat64
    clusterIdx := 0
    for i, centroid := range km.Centroids {
        dist := km.distance(point, centroid)
        if dist < minDist {
            minDist = dist
            clusterIdx = i
        }
    }
    return clusterIdx
}

2.3 使用示例

func main() {
    // 生成模拟数据
    points := []Point{
        {Features: []float64{1, 2}},
        {Features: []float64{2, 1}},
        {Features: []float64{2, 3}},
        {Features: []float64{8, 7}},
        {Features: []float64{9, 8}},
        {Features: []float64{7, 9}},
        {Features: []float64{15, 16}},
        {Features: []float64{16, 15}},
        {Features: []float64{17, 17}},
    }
    
    kmeans := NewKMeans(3, 100)
    kmeans.fit(points)
    
    fmt.Println("质心坐标:")
    for i, centroid := range kmeans.Centroids {
        fmt.Printf("簇%d: %v\n", i, centroid.Features)
    }
    
    // 预测新数据点
    testPoint := Point{Features: []float64{10, 10}}
    cluster := kmeans.predict(testPoint)
    fmt.Printf("数据点(10, 10)属于簇%d\n", cluster)
}

三、DBSCAN聚类实现

3.1 算法原理

DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一种基于密度的聚类算法:

  • 核心点:在指定半径ε内有至少MinPts个邻居
  • 边界点:在ε内邻居数少于MinPts,但属于某个核心点的邻域
  • 噪声点:既不是核心点也不是边界点

3.2 Go语言实现

type DBSCAN struct {
    Epsilon   float64
    MinPts    int
    labels    []int
}

func NewDBSCAN(epsilon float64, minPts int) *DBSCAN {
    return &DBSCAN{
        Epsilon: epsilon,
        MinPts:  minPts,
    }
}

func (d *DBSCAN) fit(points []Point) {
    n := len(points)
    d.labels = make([]int, n)
    for i := range d.labels {
        d.labels[i] = -1 // -1表示未访问
    }
    
    clusterID := 0
    for i := 0; i < n; i++ {
        if d.labels[i] != -1 {
            continue
        }
        
        neighbors := d.rangeQuery(points, i)
        
        if len(neighbors) < d.MinPts {
            d.labels[i] = 0 // 标记为噪声
            continue
        }
        
        // 扩展簇
        d.expandCluster(points, i, neighbors, clusterID)
        clusterID++
    }
}

func (d *DBSCAN) rangeQuery(points []Point, idx int) []int {
    neighbors := []int{}
    for i := 0; i < len(points); i++ {
        if i == idx {
            continue
        }
        dist := d.distance(points[idx], points[i])
        if dist <= d.Epsilon {
            neighbors = append(neighbors, i)
        }
    }
    return neighbors
}

func (d *DBSCAN) expandCluster(points []Point, idx int, neighbors []int, clusterID int) {
    d.labels[idx] = clusterID
    queue := neighbors
    
    for len(queue) > 0 {
        current := queue[0]
        queue = queue[1:]
        
        if d.labels[current] == 0 {
            d.labels[current] = clusterID
        }
        
        if d.labels[current] != -1 {
            continue
        }
        
        d.labels[current] = clusterID
        currentNeighbors := d.rangeQuery(points, current)
        
        if len(currentNeighbors) >= d.MinPts {
            queue = append(queue, currentNeighbors...)
        }
    }
}

func (d *DBSCAN) distance(p1, p2 Point) float64 {
    var sum float64
    for i := 0; i < len(p1.Features); i++ {
        sum += math.Pow(p1.Features[i]-p2.Features[i], 2)
    }
    return math.Sqrt(sum)
}

四、层次聚类

4.1 算法原理

层次聚类有两种策略:

  • 凝聚式:从单个点开始,逐步合并相似的簇
  • 分裂式:从整个数据集开始,逐步分裂

4.2 Go语言实现

type HierarchicalClustering struct {
    linkage string // "single", "complete", "average"
}

func NewHierarchicalClustering(linkage string) *HierarchicalClustering {
    return &HierarchicalClustering{linkage: linkage}
}

func (hc *HierarchicalClustering) fit(points []Point) [][]Point {
    // 初始化每个点为一个簇
    clusters := make([][]Point, len(points))
    for i, point := range points {
        clusters[i] = []Point{point}
    }
    
    for len(clusters) > 1 {
        minDist := math.MaxFloat64
        mergeI, mergeJ := 0, 1
        
        // 找到距离最近的两个簇
        for i := 0; i < len(clusters); i++ {
            for j := i + 1; j < len(clusters); j++ {
                dist := hc.clusterDistance(clusters[i], clusters[j])
                if dist < minDist {
                    minDist = dist
                    mergeI, mergeJ = i, j
                }
            }
        }
        
        // 合并两个簇
        merged := append(clusters[mergeI], clusters[mergeJ]...)
        clusters = append(clusters[:mergeJ], clusters[mergeJ+1:]...)
        clusters[mergeI] = merged
    }
    
    return clusters
}

func (hc *HierarchicalClustering) clusterDistance(c1, c2 []Point) float64 {
    switch hc.linkage {
    case "single":
        return hc.singleLinkage(c1, c2)
    case "complete":
        return hc.completeLinkage(c1, c2)
    case "average":
        return hc.averageLinkage(c1, c2)
    default:
        return hc.singleLinkage(c1, c2)
    }
}

func (hc *HierarchicalClustering) singleLinkage(c1, c2 []Point) float64 {
    minDist := math.MaxFloat64
    for _, p1 := range c1 {
        for _, p2 := range c2 {
            dist := hc.pointDistance(p1, p2)
            if dist < minDist {
                minDist = dist
            }
        }
    }
    return minDist
}

func (hc *HierarchicalClustering) completeLinkage(c1, c2 []Point) float64 {
    maxDist := 0.0
    for _, p1 := range c1 {
        for _, p2 := range c2 {
            dist := hc.pointDistance(p1, p2)
            if dist > maxDist {
                maxDist = dist
            }
        }
    }
    return maxDist
}

func (hc *HierarchicalClustering) averageLinkage(c1, c2 []Point) float64 {
    var sumDist float64
    count := 0
    for _, p1 := range c1 {
        for _, p2 := range c2 {
            sumDist += hc.pointDistance(p1, p2)
            count++
        }
    }
    return sumDist / float64(count)
}

func (hc *HierarchicalClustering) pointDistance(p1, p2 Point) float64 {
    var sum float64
    for i := 0; i < len(p1.Features); i++ {
        sum += math.Pow(p1.Features[i]-p2.Features[i], 2)
    }
    return math.Sqrt(sum)
}

五、高斯混合模型(GMM)

5.1 算法原理

GMM假设数据来自多个高斯分布的混合,使用EM算法进行参数估计。

5.2 Go语言实现

type Gaussian struct {
    Mean   float64
    StdDev float64
}

func (g *Gaussian) pdf(x float64) float64 {
    return math.Exp(-math.Pow(x-g.Mean, 2)/(2*math.Pow(g.StdDev, 2))) / (g.StdDev * math.Sqrt(2*math.Pi))
}

type GMM struct {
    Gaussians []Gaussian
    Weights   []float64
}

func NewGMM(k int) *GMM {
    return &GMM{
        Gaussians: make([]Gaussian, k),
        Weights:   make([]float64, k),
    }
}

func (gmm *GMM) fit(data []float64, maxIter int) {
    n := len(data)
    k := len(gmm.Gaussians)
    
    // 初始化
    for i := 0; i < k; i++ {
        gmm.Gaussians[i] = Gaussian{
            Mean:   data[i*n/k],
            StdDev: 1.0,
        }
        gmm.Weights[i] = 1.0 / float64(k)
    }
    
    for iter := 0; iter < maxIter; iter++ {
        // E步:计算后验概率
        responsibilities := make([][]float64, k)
        for i := range responsibilities {
            responsibilities[i] = make([]float64, n)
        }
        
        for j := 0; j < n; j++ {
            var sum float64
            for i := 0; i < k; i++ {
                responsibilities[i][j] = gmm.Weights[i] * gmm.Gaussians[i].pdf(data[j])
                sum += responsibilities[i][j]
            }
            for i := 0; i < k; i++ {
                responsibilities[i][j] /= sum
            }
        }
        
        // M步:更新参数
        for i := 0; i < k; i++ {
            var weightSum, meanSum, varSum float64
            for j := 0; j < n; j++ {
                weightSum += responsibilities[i][j]
                meanSum += responsibilities[i][j] * data[j]
            }
            
            gmm.Weights[i] = weightSum / float64(n)
            gmm.Gaussians[i].Mean = meanSum / weightSum
            
            for j := 0; j < n; j++ {
                varSum += responsibilities[i][j] * math.Pow(data[j]-gmm.Gaussians[i].Mean, 2)
            }
            gmm.Gaussians[i].StdDev = math.Sqrt(varSum / weightSum)
        }
    }
}

六、聚类评估指标

func SilhouetteScore(points []Point, labels []int) float64 {
    n := len(points)
    silhouette := make([]float64, n)
    
    for i := 0; i < n; i++ {
        // 计算a(i):同一簇内其他点的平均距离
        var a float64
        sameCluster := []Point{}
        for j := 0; j < n; j++ {
            if i != j && labels[j] == labels[i] {
                sameCluster = append(sameCluster, points[j])
            }
        }
        if len(sameCluster) > 0 {
            for _, p := range sameCluster {
                a += distance(points[i], p)
            }
            a /= float64(len(sameCluster))
        }
        
        // 计算b(i):最近簇的平均距离
        b := math.MaxFloat64
        clusters := make(map[int][]Point)
        for j := 0; j < n; j++ {
            if j != i {
                clusters[labels[j]] = append(clusters[labels[j]], points[j])
            }
        }
        for _, cluster := range clusters {
            if len(cluster) > 0 {
                var distSum float64
                for _, p := range cluster {
                    distSum += distance(points[i], p)
                }
                avgDist := distSum / float64(len(cluster))
                if avgDist < b {
                    b = avgDist
                }
            }
        }
        
        silhouette[i] = (b - a) / math.Max(a, b)
    }
    
    var score float64
    for _, s := range silhouette {
        score += s
    }
    return score / float64(n)
}

func distance(p1, p2 Point) float64 {
    var sum float64
    for i := 0; i < len(p1.Features); i++ {
        sum += math.Pow(p1.Features[i]-p2.Features[i], 2)
    }
    return math.Sqrt(sum)
}

七、总结

本文介绍了四种经典的聚类算法及其Go语言实现:

  1. K-Means:简单高效,适合大规模数据
  2. DBSCAN:基于密度,能发现任意形状的簇
  3. 层次聚类:构建层次化结构,无需指定K值
  4. 高斯混合模型:概率模型,考虑数据分布

每种算法都有其适用场景,选择合适的聚类算法需要考虑数据特性和业务需求。Go语言的高性能特性使其成为处理大规模数据聚类的理想选择。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐