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左右执行完毕。
感想:
自己写代码的时候总喜欢把各种东西复杂化,还有很大进步空间。