+-
手撸golang 基本数据结构与算法 k-means聚类算法
首页 专栏 golang 文章详情
0

手撸golang 基本数据结构与算法 k-means聚类算法

ioly 发布于 今天 00:26

缘起

最近阅读<<我的第一本算法书>>(【日】石田保辉;宫崎修一)
本系列笔记拟采用golang练习之

k-means聚类算法

聚类就是在输入为多个数据时,
将“相似”的数据分为一组的操作。

k-means算法是聚类算法中的一种。
首先随机选择k个点作为簇的中心点,
然后重复执行“将数据分到相应的簇中”和
“将中心点移到重心的位置”这两个操作,
直到中心点不再发生变化为止。

k-means算法中,随着操作的不断重复,
中心点的位置必定会在某处收敛,
这一点已经在数学层面上得到证明。

摘自 <<我的第一本算法书>> 【日】石田保辉;宫崎修一

场景

某地突然爆发新冠疫情, 现防疫急需根据病例分布, 查找可能的病源地 首先将病例分布的坐标, 录入系统 然后根据k-means算法, 按k从1到3, 分别进行聚类 聚类的中心点, 可能就是病源地

流程

给定若干样本, 和样本距离计算器, 需要求解k个样本中心点 首先从样本中随机抽取k个点, 作为中心点

循环每个样本

分别计算样本点到k个中心点的距离 判断距离样本点最小的中心点 将样本划分到该最小中心点的簇

计算每个簇的中心点, 作为新的中心点

循环簇中的每个样本 计算该样本, 到本簇其他样本的距离之和 与其他样本的距离和最小的点, 就是新的中心点 重复3-4, 直到中心点不再变化, 计算完毕

设计

IPoint: 样本点接口, 其实是一个空接口 IDistanceCalculator: 距离计算器接口 IClassifier: 分类器接口, 将samples聚类成k个, 并返回k个中心点 tPerson: 病例样本点, 实现IPoint接口, 含x,y坐标 tPersonDistanceCalculator: 病例距离计算器, 计算两点间x,y坐标的直线距离 tKMeansClassifier: k-means聚类器, 实现IClassifier接口.

单元测试

k_means_test.go

package others

import (
    km "learning/gooop/others/k_means"
    "strings"
    "testing"
)

func Test_KMeans(t *testing.T) {
    // 创建样本点
    samples := []km.IPoint {
        km.NewPerson(2, 11),
        km.NewPerson(2, 8),
        km.NewPerson(2, 6),

        km.NewPerson(3, 12),
        km.NewPerson(3, 10),

        km.NewPerson(4, 7),
        km.NewPerson(4, 3),

        km.NewPerson(5, 11),
        km.NewPerson(5, 9),
        km.NewPerson(5, 2),

        km.NewPerson(7, 9),
        km.NewPerson(7, 6),
        km.NewPerson(7, 3),

        km.NewPerson(8, 12),

        km.NewPerson(9, 3),
        km.NewPerson(9, 5),
        km.NewPerson(9, 10),

        km.NewPerson(10, 3),
        km.NewPerson(10, 6),
        km.NewPerson(10, 12),

        km.NewPerson(11, 9),
    }

    fnPoints2String := func(points []km.IPoint) string {
        items := make([]string, len(points))
        for i,it := range points {
            items[i] = it.String()
        }
        return strings.Join(items, " ")
    }

    for k:=1;k<=3;k++ {
        centers := km.KMeansClassifier.Classify(samples, km.PersonDistanceCalculator, k)
        t.Log(fnPoints2String(centers))
    }
}

测试输出

$ go test -v k_means_test.go 
=== RUN   Test_KMeans
    k_means_test.go:53: p(7,6)
    k_means_test.go:53: p(5,9) p(7,3)
    k_means_test.go:53: p(9,10) p(3,10) p(7,3)
--- PASS: Test_KMeans (0.00s)
PASS
ok      command-line-arguments  0.002s

IPoint.go

样本点接口, 其实是一个空接口

package km

import "fmt"

type IPoint interface {
    fmt.Stringer
}

IDistanceCalculator.go

距离计算器接口

package km

type IDistanceCalculator interface {
    Calc(a, b IPoint) int
}

IClassifier.go

分类器接口, 将samples聚类成k个, 并返回k个中心点

package km

type IClassifier interface {
    // 将samples聚类成k个, 并返回k个中心点
    Classify(samples []IPoint, calc IDistanceCalculator, k int) []IPoint
}

tPerson.go

病例样本点, 实现IPoint接口, 含x,y坐标

package km

import "fmt"

type tPerson struct {
    x int
    y int
}

func NewPerson(x, y int) IPoint {
    return &tPerson{x, y, }
}

func (me *tPerson) String() string {
    return fmt.Sprintf("p(%v,%v)", me.x, me.y)
}

tPersonDistanceCalculator.go

病例距离计算器, 计算两点间x,y坐标的直线距离

package km


type tPersonDistanceCalculator struct {
}

var gMaxInt = 0x7fffffff_ffffffff

func newPersonDistanceCalculator() IDistanceCalculator {
    return &tPersonDistanceCalculator{}
}

func (me *tPersonDistanceCalculator) Calc(a, b IPoint) int {
    if a == b {
        return 0
    }

    p1, ok := a.(*tPerson)
    if !ok {
        return gMaxInt
    }

    p2, ok := b.(*tPerson)
    if !ok {
        return gMaxInt
    }

    dx := p1.x - p2.x
    dy := p1.y - p2.y

    d := dx*dx + dy*dy
    if d < 0 {
        panic(d)
    }
    return d
}

var PersonDistanceCalculator = newPersonDistanceCalculator()

tKMeansClassifier.go

k-means聚类器, 实现IClassifier接口.

package km

import (
    "math/rand"
    "time"
)

type tKMeansClassifier struct {
}

type tPointEntry struct {
    point IPoint
    distance int
    index int
}

func newPointEntry(p IPoint, d int, i int) *tPointEntry {
    return &tPointEntry{
        p, d, i,
    }
}

func newKMeansClassifier() IClassifier {
    return &tKMeansClassifier{}
}

// 将samples聚类成k个, 并返回k个中心点
func (me *tKMeansClassifier) Classify(samples []IPoint, calc IDistanceCalculator, k int) []IPoint {
    sampleCount := len(samples)
    if sampleCount <= k {
        return samples
    }

    // 初始化, 随机选择k个中心点
    rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
    centers := make([]IPoint, k)
    for selected, i:= make(map[int]bool, 0), 0;i < k; {
        n := rnd.Intn(sampleCount)
        _,ok := selected[n]

        if !ok {
            selected[n] = true
            centers[i] = samples[n]
            i++
        }
    }


    // 根据到中心点的距离, 划分samples
    for {
        groups := me.split(samples, centers, calc)

        newCenters := make([]IPoint, k)
        for i,g := range groups {
            newCenters[i] = me.centerOf(g, calc)
        }

        if me.groupEquals(centers, newCenters) {
            return centers
        }
        centers = newCenters
    }
}

// 将样本点距离中心点的距离进行分簇
func (me *tKMeansClassifier) split(samples []IPoint, centers []IPoint, calc IDistanceCalculator) [][]IPoint {
    k := len(centers)
    result := make([][]IPoint, k)
    for i := 0;i<k;i++ {
        result[i] = make([]IPoint, 0)
    }

    entries := make([]*tPointEntry, k)
    for i,c := range centers {
        entries[i] = newPointEntry(c, 0, i)
    }

    for _,p := range samples {
        for _,e := range entries {
            e.distance = calc.Calc(p, e.point)
        }

        center := me.min(entries)
        result[center.index] = append(result[center.index], p)
    }

    return result
}

// 计算一簇样本的重心. 重心就是距离各点的总和最小的点
func (me *tKMeansClassifier) centerOf(samples []IPoint, calc IDistanceCalculator) IPoint {
    entries := make([]*tPointEntry, len(samples))
    for i,src := range samples {
        distance := 0
        for _,it := range samples {
            distance += calc.Calc(src, it)
        }
        entries[i] = newPointEntry(src, distance, i)
    }

    return me.min(entries).point
}

// 判断两组点是否相同
func (me *tKMeansClassifier) groupEquals(g1, g2 []IPoint) bool {
    if len(g1) != len(g2) {
        return false
    }

    for i,v := range g1 {
        if g2[i] != v {
            return false
        }
    }

    return true
}

// 查找距离最小的点
func (me *tKMeansClassifier) min(entries []*tPointEntry) *tPointEntry {
    minI := 0
    minD := gMaxInt
    for i,it := range entries {
        if it.distance < minD {
            minI = i
            minD = it.distance
        }
    }

    return entries[minI]
}


var KMeansClassifier = newKMeansClassifier()

(end)

算法 数据结构 golang k-means 聚类
阅读 28 更新于 今天 00:26
收藏
分享
本作品系原创, 采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议
avatar
ioly

乐见无知 成长无尽

12 声望
6 粉丝
关注作者
0 条评论
得票 时间
提交评论
avatar
ioly

乐见无知 成长无尽

12 声望
6 粉丝
关注作者
宣传栏
目录

缘起

最近阅读<<我的第一本算法书>>(【日】石田保辉;宫崎修一)
本系列笔记拟采用golang练习之

k-means聚类算法

聚类就是在输入为多个数据时,
将“相似”的数据分为一组的操作。

k-means算法是聚类算法中的一种。
首先随机选择k个点作为簇的中心点,
然后重复执行“将数据分到相应的簇中”和
“将中心点移到重心的位置”这两个操作,
直到中心点不再发生变化为止。

k-means算法中,随着操作的不断重复,
中心点的位置必定会在某处收敛,
这一点已经在数学层面上得到证明。

摘自 <<我的第一本算法书>> 【日】石田保辉;宫崎修一

场景

某地突然爆发新冠疫情, 现防疫急需根据病例分布, 查找可能的病源地 首先将病例分布的坐标, 录入系统 然后根据k-means算法, 按k从1到3, 分别进行聚类 聚类的中心点, 可能就是病源地

流程

给定若干样本, 和样本距离计算器, 需要求解k个样本中心点 首先从样本中随机抽取k个点, 作为中心点

循环每个样本

分别计算样本点到k个中心点的距离 判断距离样本点最小的中心点 将样本划分到该最小中心点的簇

计算每个簇的中心点, 作为新的中心点

循环簇中的每个样本 计算该样本, 到本簇其他样本的距离之和 与其他样本的距离和最小的点, 就是新的中心点 重复3-4, 直到中心点不再变化, 计算完毕

设计

IPoint: 样本点接口, 其实是一个空接口 IDistanceCalculator: 距离计算器接口 IClassifier: 分类器接口, 将samples聚类成k个, 并返回k个中心点 tPerson: 病例样本点, 实现IPoint接口, 含x,y坐标 tPersonDistanceCalculator: 病例距离计算器, 计算两点间x,y坐标的直线距离 tKMeansClassifier: k-means聚类器, 实现IClassifier接口.

单元测试

k_means_test.go

package others

import (
    km "learning/gooop/others/k_means"
    "strings"
    "testing"
)

func Test_KMeans(t *testing.T) {
    // 创建样本点
    samples := []km.IPoint {
        km.NewPerson(2, 11),
        km.NewPerson(2, 8),
        km.NewPerson(2, 6),

        km.NewPerson(3, 12),
        km.NewPerson(3, 10),

        km.NewPerson(4, 7),
        km.NewPerson(4, 3),

        km.NewPerson(5, 11),
        km.NewPerson(5, 9),
        km.NewPerson(5, 2),

        km.NewPerson(7, 9),
        km.NewPerson(7, 6),
        km.NewPerson(7, 3),

        km.NewPerson(8, 12),

        km.NewPerson(9, 3),
        km.NewPerson(9, 5),
        km.NewPerson(9, 10),

        km.NewPerson(10, 3),
        km.NewPerson(10, 6),
        km.NewPerson(10, 12),

        km.NewPerson(11, 9),
    }

    fnPoints2String := func(points []km.IPoint) string {
        items := make([]string, len(points))
        for i,it := range points {
            items[i] = it.String()
        }
        return strings.Join(items, " ")
    }

    for k:=1;k<=3;k++ {
        centers := km.KMeansClassifier.Classify(samples, km.PersonDistanceCalculator, k)
        t.Log(fnPoints2String(centers))
    }
}

测试输出

$ go test -v k_means_test.go 
=== RUN   Test_KMeans
    k_means_test.go:53: p(7,6)
    k_means_test.go:53: p(5,9) p(7,3)
    k_means_test.go:53: p(9,10) p(3,10) p(7,3)
--- PASS: Test_KMeans (0.00s)
PASS
ok      command-line-arguments  0.002s

IPoint.go

样本点接口, 其实是一个空接口

package km

import "fmt"

type IPoint interface {
    fmt.Stringer
}

IDistanceCalculator.go

距离计算器接口

package km

type IDistanceCalculator interface {
    Calc(a, b IPoint) int
}

IClassifier.go

分类器接口, 将samples聚类成k个, 并返回k个中心点

package km

type IClassifier interface {
    // 将samples聚类成k个, 并返回k个中心点
    Classify(samples []IPoint, calc IDistanceCalculator, k int) []IPoint
}

tPerson.go

病例样本点, 实现IPoint接口, 含x,y坐标

package km

import "fmt"

type tPerson struct {
    x int
    y int
}

func NewPerson(x, y int) IPoint {
    return &tPerson{x, y, }
}

func (me *tPerson) String() string {
    return fmt.Sprintf("p(%v,%v)", me.x, me.y)
}

tPersonDistanceCalculator.go

病例距离计算器, 计算两点间x,y坐标的直线距离

package km


type tPersonDistanceCalculator struct {
}

var gMaxInt = 0x7fffffff_ffffffff

func newPersonDistanceCalculator() IDistanceCalculator {
    return &tPersonDistanceCalculator{}
}

func (me *tPersonDistanceCalculator) Calc(a, b IPoint) int {
    if a == b {
        return 0
    }

    p1, ok := a.(*tPerson)
    if !ok {
        return gMaxInt
    }

    p2, ok := b.(*tPerson)
    if !ok {
        return gMaxInt
    }

    dx := p1.x - p2.x
    dy := p1.y - p2.y

    d := dx*dx + dy*dy
    if d < 0 {
        panic(d)
    }
    return d
}

var PersonDistanceCalculator = newPersonDistanceCalculator()

tKMeansClassifier.go

k-means聚类器, 实现IClassifier接口.

package km

import (
    "math/rand"
    "time"
)

type tKMeansClassifier struct {
}

type tPointEntry struct {
    point IPoint
    distance int
    index int
}

func newPointEntry(p IPoint, d int, i int) *tPointEntry {
    return &tPointEntry{
        p, d, i,
    }
}

func newKMeansClassifier() IClassifier {
    return &tKMeansClassifier{}
}

// 将samples聚类成k个, 并返回k个中心点
func (me *tKMeansClassifier) Classify(samples []IPoint, calc IDistanceCalculator, k int) []IPoint {
    sampleCount := len(samples)
    if sampleCount <= k {
        return samples
    }

    // 初始化, 随机选择k个中心点
    rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
    centers := make([]IPoint, k)
    for selected, i:= make(map[int]bool, 0), 0;i < k; {
        n := rnd.Intn(sampleCount)
        _,ok := selected[n]

        if !ok {
            selected[n] = true
            centers[i] = samples[n]
            i++
        }
    }


    // 根据到中心点的距离, 划分samples
    for {
        groups := me.split(samples, centers, calc)

        newCenters := make([]IPoint, k)
        for i,g := range groups {
            newCenters[i] = me.centerOf(g, calc)
        }

        if me.groupEquals(centers, newCenters) {
            return centers
        }
        centers = newCenters
    }
}

// 将样本点距离中心点的距离进行分簇
func (me *tKMeansClassifier) split(samples []IPoint, centers []IPoint, calc IDistanceCalculator) [][]IPoint {
    k := len(centers)
    result := make([][]IPoint, k)
    for i := 0;i<k;i++ {
        result[i] = make([]IPoint, 0)
    }

    entries := make([]*tPointEntry, k)
    for i,c := range centers {
        entries[i] = newPointEntry(c, 0, i)
    }

    for _,p := range samples {
        for _,e := range entries {
            e.distance = calc.Calc(p, e.point)
        }

        center := me.min(entries)
        result[center.index] = append(result[center.index], p)
    }

    return result
}

// 计算一簇样本的重心. 重心就是距离各点的总和最小的点
func (me *tKMeansClassifier) centerOf(samples []IPoint, calc IDistanceCalculator) IPoint {
    entries := make([]*tPointEntry, len(samples))
    for i,src := range samples {
        distance := 0
        for _,it := range samples {
            distance += calc.Calc(src, it)
        }
        entries[i] = newPointEntry(src, distance, i)
    }

    return me.min(entries).point
}

// 判断两组点是否相同
func (me *tKMeansClassifier) groupEquals(g1, g2 []IPoint) bool {
    if len(g1) != len(g2) {
        return false
    }

    for i,v := range g1 {
        if g2[i] != v {
            return false
        }
    }

    return true
}

// 查找距离最小的点
func (me *tKMeansClassifier) min(entries []*tPointEntry) *tPointEntry {
    minI := 0
    minD := gMaxInt
    for i,it := range entries {
        if it.distance < minD {
            minI = i
            minD = it.distance
        }
    }

    return entries[minI]
}


var KMeansClassifier = newKMeansClassifier()

(end)