WPF实现KMEANS算法

本文介绍了一个基于C#的KMeans聚类算法实现案例,包括UI交互、数据处理及核心算法逻辑。作者分享了从界面设计到算法实现的全过程,特别强调了如何利用KMeans算法进行数据聚类。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

KMEANS算法很简单,用C实现整个算法应该不到100行代码吧(不要写界面),但是我拥有将100行代码进化为1000行代码的超能力(真是无力吐槽),所以写这么简单的东西居然花了我差不多2天。其实主要还是界面编程不熟悉,我还得要多练练啊。

KMEANS算法在这里就不多做介绍了,下面简要说一下代码:

1)XAML:

<Window Name="window" x:Class="KMEANS.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:k="clr-namespace:KMEANS"
        Title="MainWindow" Height="650" Width="700">
    <Grid Name="RootGd" Margin="0,0,0,0">
        <TextBlock HorizontalAlignment="Left" Margin="281,23,0,0" TextWrapping="Wrap" VerticalAlignment="Top" RenderTransformOrigin="-0.849,0.384"><Run Language="zh-cn" Text="划分数量"/></TextBlock>
        <TextBox x:Name="CommunityAmount" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Margin="365,19,0,0"/>
        <Button x:Name="BeginBt" Content="开始" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" RenderTransformOrigin="7.774,2.89" Margin="524,21,0,0" Click="BeginBt_Click"/>
        <k:MyCanvasClass x:Name="MyCanvas" HorizontalAlignment="Left" Height="550" Margin="28,58,0,0" VerticalAlignment="Top" Width="620">
            <Border BorderBrush="#FFD61B1B" BorderThickness="4" Height="550" Width="620" Canvas.Top="2"/>
        </k:MyCanvasClass>

    </Grid>
</Window>


界面相当简单,因为我的数据是从硬盘上面读取的所以就不做数据的输入框了。那个聚落数量是要自己设置的,而且要小于等于10.


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace KMEANS
{
    public partial class MainWindow : Window
    {
        private static int pointAmount = 0;
        private static int communityAmount = 0;
        // 所有点的起始坐标
        private List<Point> points;
        // 聚集点坐标
        private List<Point> communitys;

        // 所有点的自定义图形
        private List<MyEllipse> pointsE;
        // 聚集点的自定义图形
        private List<MyEllipse> communitysE;

        private SolidColorBrush pointBrush;
        private SolidColorBrush communityBrush;

        public delegate void InvokeSetTitle(string str);
        public delegate void InvokeDrawPoint();
        // 计算KMEANS完毕之后返回事件
        public delegate void InvokeSetResult(StateChangedEventArgs args);

        private Calculator calculator;

        public MainWindow()
        {
            InitializeComponent();
            pointBrush = new SolidColorBrush(Colors.Red);
            communityBrush = new SolidColorBrush(Colors.Black);
            pointsE = new List<MyEllipse>();
            communitysE = new List<MyEllipse>();
        }

        private void BeginBt_Click(object sender, RoutedEventArgs e)
        {
            //pointAmount = int.Parse(PointAmount.Text);
            communityAmount = int.Parse(CommunityAmount.Text);
            window.Title = "正在生成随机点...";
            Thread t = new Thread(new ThreadStart(init));
            t.Start();
        }

        /// <summary>
        /// 生成随机点的函数
        /// </summary>
        private void init()
        {
            points = new List<Point>();
            communitys = new List<Point>();
            Random ra = new Random();
            try
            {
                FileStream fs = new FileStream("Data.txt", FileMode.Open);
                StreamReader sr = new StreamReader(fs);
                String line = sr.ReadLine();
                while (line != null)
                {
                    pointAmount++;
                    Point p = new Point();
                    String[] point = line.Split(' ');        
                    p.X = float.Parse(point[0]);
                    p.Y = float.Parse(point[1]);
                    points.Add(p);
                    line = sr.ReadLine();
                }
                sr.Close();
            }
            catch(IOException e)
            {
                MessageBox.Show("文件读取失败!"+e.Message);
                return;
            }

            for (int i = 0; i < communityAmount; i++)
            {
                int x = ra.Next(550);
                int y = ra.Next(620);
                Point p = new Point();
                p.X = x;
                p.Y = y;
                communitys.Add(p);
            }

            InvokeSetTitle m = new InvokeSetTitle(SetWindowTitle);
            Dispatcher.BeginInvoke(m, new object[] { "随机点已生成,正在绘制..." });

            InvokeDrawPoint m2 = new InvokeDrawPoint(drawPoint);
            Dispatcher.BeginInvoke(m2, null);
        }

        /// <summary>
        /// 改变标题栏
        /// </summary>
        /// <param name="s">设置字符串</param>
        private void SetWindowTitle(String s)
        {
            window.Title = s;
        }


        /// <summary>
        /// 根据传入的p在Canvas上面画点
        /// </summary>
        /// <param name="p"></param>
        private void drawPoint()
        {
            for (int i = 1; i <= points.Count; i++)
            {
                MyEllipse e = new MyEllipse();
                e.E = new Ellipse();
                e.E.Height = 3.0;
                e.E.Width = 3.0;
                e.ColorR = ColorResources.GetColor(1);
                e.CurPoint = points[i - 1];
                e.ID = i;
                e.BelongCommunity = 0;
                pointsE.Add(e);
                MyCanvas.drawPoint(e);
            }
            for (int i = 1; i <= communitys.Count; i++)
            {
                MyEllipse e = new MyEllipse();
                e.E = new Ellipse();
                e.E.Height = 8.0;
                e.E.Width = 8.0;
                e.ColorR = ColorResources.GetColor(11);
                e.CurPoint = points[i - 1];
                e.ID = i;
                e.BelongCommunity = 0;
                communitysE.Add(e);
                MyCanvas.drawPoint(e);
            }
            window.Title = "绘制完毕,正在计算...";

            StateChangedEventArgs args = new StateChangedEventArgs();
            args.communitysE = communitysE;
            args.pointsE = pointsE;
            Thread t = new Thread(delegate() { cal(args); });
            t.Start();
        }

        private void cal(StateChangedEventArgs args)
        {
            calculator = new Calculator();
            calculator.StateChangedEvent += afterCalculate;
            calculator.Calculate(args);
        }

        /// <summary>
        /// 计算完毕KMEANS之后把相应点的参数回传,并且在此函数进行绘制
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void afterCalculate(Calculator sender, StateChangedEventArgs args)
        {
            InvokeSetResult m = new InvokeSetResult(SetResult);
            Dispatcher.BeginInvoke(m, new object[] { args });
        }

        private void SetResult(StateChangedEventArgs args)
        {
            var p = args.pointsE;
            foreach (MyEllipse e in p)
            {
                MyCanvas.ChangeEState(e, MyCanvasClass.POINT);
            }

            var c = args.communitysE;
            foreach (MyEllipse e in c)
            {
                MyCanvas.ChangeEState(e, MyCanvasClass.COMMUNITY);
            }
            window.Title = "计算完毕";
        }
    }
}

上面是CODE BEHIND,主要进行和UI的交互。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace KMEANS
{
    public class MyCanvasClass : Canvas
    {
        public static int POINT = 0;
        public static int COMMUNITY = 1;
        public void drawPoint(MyEllipse e)
        {    
            e.E.Visibility = Visibility.Visible;
            e.E.Fill = new SolidColorBrush(e.ColorR);
            e.E.SetValue(Canvas.TopProperty, e.CurPoint.X);
            e.E.SetValue(Canvas.LeftProperty, e.CurPoint.Y);
            this.Children.Add(e.E);
        }

        /// <summary>
        /// 改变椭圆的位置。当计算出KMEANS之后会调用
        /// </summary>
        /// <param name="e">椭圆引用</param>
        /// <param name="type">椭圆类型</param>
        public void ChangeEState(MyEllipse e, int type)
        {
            e.E.Fill = new SolidColorBrush(e.ColorR);
            if (type == COMMUNITY)
            {
                e.E.SetValue(Canvas.TopProperty, e.CurPoint.X);
                e.E.SetValue(Canvas.LeftProperty, e.CurPoint.Y);
            }
        }

    }
}

这里是重写的Canvas类,主要负责把椭圆添加进去界面去(图形里面我用椭圆来替代点)。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace KMEANS
{
    /// <summary>
    /// 自定义控件
    /// </summary>
    public class MyEllipse : UIElement
    {
        public Ellipse E { get; set; }
        public int ID { get; set; }
        public Point CurPoint { get; set; }
        public Color ColorR { get; set; }
        
        // 只有Point才有的属性,在迭代中属于哪个Community
        public int BelongCommunity { get; set; }

        public MyEllipse()
        {
            E = new Ellipse();
            CurPoint = new Point();
        }
    }
}

这里是我写的椭圆类。里面要关联有一些关于这个点(椭圆)的相关信息。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;

namespace KMEANS
{
    /// <summary>
    /// 存储颜色资源
    /// </summary>
    public class ColorResources
    {
        private static Dictionary<int, Color> MyColorResources;
        private ColorResources() { }

        /// <summary>
        /// 只支持10种颜色的存储
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public static Color GetColor(int index)
        {
            if (MyColorResources == null)
            {
                MyColorResources = new Dictionary<int, Color>();
                MyColorResources.Add(1, Colors.Red);
                MyColorResources.Add(2, Colors.DarkSeaGreen);
                MyColorResources.Add(3, Colors.Blue);
                MyColorResources.Add(4, Colors.Brown);
                MyColorResources.Add(5, Colors.Coral);
                MyColorResources.Add(6, Colors.DarkBlue);
                MyColorResources.Add(7, Colors.Green);
                MyColorResources.Add(8, Colors.Orange);
                MyColorResources.Add(9, Colors.Pink);
                MyColorResources.Add(10, Colors.Yellow);
                MyColorResources.Add(11, Colors.Black);
            }
            if (index > 11 && index < 1)
            {
                throw new IndexException();
            }
            return MyColorResources[index];
        }
    }

    public class IndexException : Exception
    {
        public String e = "下标溢出";
    }
}

这个是我定义的颜色资源类。只定义了11种颜色。其中普通点可以采用前面10种,聚集点采用最后一种也就是黑色。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace KMEANS
{
    public class StateChangedEventArgs : EventArgs
    {
        // 所有点的自定义图形
        public List<MyEllipse> pointsE;
        // 聚集点的自定义图形
        public List<MyEllipse> communitysE;
    }
}

这是我自定义的传参数类,在后台计算引擎和MainWindow之间进行数据交互(事件需要)。


最后是最重要的算法的数据引擎。整个算法写在里面的,算法真的差不多100行左右。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace KMEANS
{
    /// <summary>
    /// 负责计算KMEANS的主要逻辑代码
    /// </summary>
    public class Calculator
    {
        // 委托  
        public delegate void StateChanged(Calculator sender, StateChangedEventArgs args);
        // 定义事件  
        public event StateChanged StateChangedEvent;

        // 所有点的自定义图形
        public List<MyEllipse> pointsE;
        // 聚集点的自定义图形
        public List<MyEllipse> communitysE;

        private StateChangedEventArgs parm;

        private int countTime = 10000;
        private const double NORMAL = 0.1;


        /// <summary>
        /// 计算KMEANS的入口函数
        /// </summary>
        /// <param name="args"></param>
        public void Calculate(StateChangedEventArgs args)
        {
            pointsE = args.pointsE;
            communitysE = args.communitysE;
            parm = args;
            int c = 0;

            while (countTime > 0)
            {
                Debug.WriteLine(c++);
                findNearest();
                if (findCenter() == false)
                {
                    break;
                }
                countTime--;
            }
            StateChangedEvent(this, args);
        }

        /// <summary>
        /// 在一次迭代中为Point找到最近的Community,并且改变Point的颜色,修改其所属Community
        /// </summary>
        private void findNearest()
        {
            foreach (MyEllipse e in pointsE)
            {
                Point a = e.CurPoint;
                int neareastId = 0;
                double distance = 9999999.9;
                foreach (MyEllipse c in communitysE)
                {
                    double d = getDistance(e.CurPoint, c.CurPoint);
                    if (d < distance)
                    {
                        neareastId = c.ID;
                        distance = d;
                    }
                }
                e.BelongCommunity = neareastId;
                if (neareastId > 0 && neareastId <= 10)
                {
                    e.ColorR = ColorResources.GetColor(neareastId);
                }
            }
        }

        /// <summary>
        /// 在一次迭代中(首先通过findNearest将Points分簇)将簇心重新定位,实际上改变该Community的位置
        /// <returns>true:程序找到了要变动的点  false:找不到,KMEANS停止</returns>
        /// </summary>
        private bool findCenter()
        {
            bool changed = false;
            foreach (MyEllipse e in communitysE)
            {
                double totalX = 0;
                double totalY = 0;
                int amount = 0;
                foreach (MyEllipse t in pointsE)
                {
                    if (t.BelongCommunity == e.ID)
                    {
                        totalX += t.CurPoint.X;
                        totalY += t.CurPoint.Y;
                        amount++;
                    }
                }
                Point newPoint = new Point(totalX / amount, totalY / amount);
                if (getDistance(newPoint, e.CurPoint) > NORMAL)
                {
                    changed = true;
                }
                e.CurPoint = newPoint;
            }
            return changed;
        }

        /// <summary>
        /// 计算两点间距离
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns>返回距离的平方</returns>
        private double getDistance(Point a, Point b)
        {
            return (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y);
        }

    }
}

上面是运行结束之后的结果。总共有3000多个点(我就不把数据贴出来了),大概1S左右执行完毕。

感想:

自己写代码的时候总喜欢把各种东西复杂化,还有很大进步空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值