c++K-means鸢尾花识别(自动生成非鸢尾花集,特征缩放)(来源于一篇大作业)

K-means聚类算法——鸢尾花识别

这次的代码还有一定的问题要优化,预测的准确率有待提高,我们实验最好结果是98的测试集准确率。正常只有30左右。

一、选题分析

鸢尾花识别项目基于经典的K-means聚类算法,旨在通过特征值输入来预测鸢尾花的种类。选题的具体分析如下:

背景

鸢尾花数据集是一个经典的数据集,广泛应用于分类和聚类算法的教学和研究中。该数据集包含三类鸢尾花:Setosa、Versicolor和Virginica,每类各50个样本,每个样本有4个特征:花萼长、花萼宽、花瓣长和花瓣宽。

目标

  1. 使用K-means聚类算法对鸢尾花数据进行聚类:通过无监督学习的方式,将鸢尾花数据分成四类(也就是分为所谓的4簇)。
  2. 预测鸢尾花种类:用户输入鸢尾花的特征值后,系统能够预测其所属的种类
  3. 展示预测的迭代过程:通过显示迭代过程中的质心变化,让我们可以更直观地理解K-means聚类算法的工作原理。
  4. 增加非鸢尾花数据:模拟实际环境中的数据混合情况,提高算法的鲁棒性(鲁棒性是指算法在面对噪声、数据缺失或异常数据等情况下仍然能保持其性能的能力。在鸢尾花识别项目中,确保K-means聚类算法的鲁棒性是非常重要的)而本项目正是通过添加非鸢尾花集,更好的模拟了实际情况。

需求

  • 数据处理:包括数据加载、特征缩放和数据集划分。
  • 算法实现:K-means聚类算法的实现,包括质心初始化、簇分配、质心更新和收敛判断
  • 结果展示:输入特征值进行预测,并显示预测结果及其迭代过程。

挑战

  1. 质心初始化的随机性:不同的初始化可能导致不同的聚类结果。
  2. 收敛判断:判断质心是否已经收敛需要设定合适的阈值。
  3. 处理混合数据:需要正确区分鸢尾花和非鸢尾花数据。

二、知识补充

1.k-means算法

K-means算法是一种常用的无监督学习方法,用于将数据点划分为K个簇,每个簇由其质心(centroid)表示。算法通过迭代的方式,最小化各点到其所属质心的距离,从而实现数据点的聚类。

K-means算法步骤
  1. 初始化
    • 随机选择K个点作为初始质心。
  2. 迭代步骤
    • 簇分配:将每个数据点分配到离它最近的质心所属的簇。
    • 质心更新:重新计算每个簇的质心,质心为簇内所有点的平均值。
  3. 收敛条件
    • 质心不再发生变化,或者达到最大迭代次数。

补充:

  • 一般情况下k-means算法是不确定有几个簇的,一般要运用“肘部法则”来判断几个簇,此题恰好已经分类好就不需要运用肘部法则来确定簇的数量。

三、整体设计思路

#include <iostream>
#include <fstream>
#include <vector>
#include <array>
#include <cmath>
#include <limits>
#include <algorithm>
#include <numeric>
#include <cstdlib>
#include <ctime>
#include <opencv2/opencv.hpp>

1.头文件的添加

  • <cstdlib>
    • 提供了标准库中的通用工具函数,包括动态内存分配、随机数生成、进程控制、环境查询和转换等功能。
  • <ctime>
    • 用于随机数生成和时间相关的操作,例如在运用**<cstdlib>** 初始化随机数种子 srand(static_cast<unsigned int>(time(0))) 和生成随机数 rand()
  • <limits>
    • 用于获取数据类型的极限值,例如 numeric_limits<double>::max()numeric_limits<double>::min(),在代码中用于特征缩放和初始化距离数组。
  • <numeric>
    • 用于数值算法操作,例如 accumulate 用于计算总距离 accumulate(distances.begin(), distances.end(), 0.0)

2.对数据集部分内容的相关处理——标签

struct Iris {
    array<double, 4> features;
    int label; // 0: Setosa, 1: Versicolour, 2: Virginica, 3: 非鸢尾花
};

// 将字符串标签映射为整数
int labelToInt(const string& label) {
    if (label == "setosa") return 0;
    if (label == "versicolor") return 1;
    if (label == "virginica") return 2;
    return 3;
}

// 将整数标签映射为字符串
string labelToString(int label) {
    if (label == 0) return "setosa";
    if (label == 1) return "versicolor";
    if (label == 2) return "virginica";
    return "non-iris";
}
  • 创建一个鸢尾花结构体

    • features:一个长度为 4 的 array<double, 4> 数组,用于存储鸢尾花的四个特征值(如花萼长、花萼宽、花瓣长、花瓣宽)。double是数据类型,4是数组大小。

      • 与原生数组相比,array 提供了更好的类型安全性和丰富的成员函数接口,使其使用更加方便和安全。

      • array与一般数组的区别

        特性传统数组array
        初始化方式支持支持
        获取大小需手动计算提供 size() 方法
        边界检查不支持支持(通过 at() 方法)
        迭代使用循环支持迭代器和范围 for 循环
        类型安全不完全完全类型安全
        与 STL 结合不直接支持完全支持
      • 运用array很好的避免了访问越界的现象。

    • label:一个 int 类型的标签,用于表示鸢尾花的种类。标签的含义如下:

      • 0:Setosa
      • 1:Versicolour
      • 2:Virginica
      • 3:非鸢尾花
  • 函数 labelToInt和函数 labelToString

    • 将表示鸢尾花种类的字符串标签和整数标签进行转化

    • 作用:

      • 数据加载:从文件中加载鸢尾花数据时,字符串标签需要转换为整数标签以便于后续处理。

      • 数据存储:处理后的数据需要保存到文件中时,需要将整数标签转换回字符串标签。

      • 分类和预测:在分类和预测过程中,标签的表示形式需要在整数和字符串之间转换,以便于显示和分析结果。

    • 这样提高了代码的可读性和维护性。

3.加载鸢尾花数据集

vector<Iris> loadIrisData(const string& filename) {
    vector<Iris> data;
    ifstream file(filename);
    if (!file.is_open()) {
        cerr << "无法打开文件" << endl;
        return data;
    }

    string line;
    while (getline(file, line)) {
        istringstream iss(line);//创建了一个流对象
        Iris iris;
        string label;
        if (iss >> iris.features[0] >> iris.features[1] >> iris.features[2] >> iris.features[3] >> label) {
            iris.label = labelToInt(label);
            data.push_back(iris);
        }
    }

    file.close();
    return data;
}
  • 初始化 vector<Iris>:声明一个 vector<Iris> 变量 data,用于存储从文件中读取的数据。
  • 打开文件:使用 ifstream 打开指定的文件。如果文件打开失败,输出错误信息并返回空的 vector
  • 读取文件内容:使用 getline 逐行读取文件内容。
    • 解析每一行:将每一行数据读入一个 istringstream 对象 iss
      • 方便解析istringstream 使得解析字符串变得非常简单,可以直接使用流提取运算符 >> 逐个提取数据项,而不需要手动拆分字符串。
      • 一致的接口:使用 istringstream 可以与从标准输入(如 cin)读取数据的方式一致,简化了代码。
      • 安全性:流操作符 >> 会自动处理不同类型的数据,并在数据类型不匹配时返回错误状态,使得解析过程更加安全。
    • 读取特征值和标签:从 iss 中提取四个特征值和一个标签字符串,并将特征值存储到 Iris 结构体的 features 数组中。
    • 转换标签:使用 labelToInt 函数将标签字符串转换为整数标签,并存储到 Iris 结构体的 label 字段中。
    • 添加到数据集:将解析后的 Iris 结构体对象添加到 data 向量中。
  • 关闭文件:读取完所有行后,关闭文件。
  • 返回数据集:返回包含所有读取和解析后的 Iris 数据的 vector

4.生成非鸢尾花数据

vector<Iris> generateNonIrisData(int numSamples) {
    vector<Iris> data;
    srand(static_cast<unsigned int>(time(0)));
    for (int i = 0; i < numSamples; ++i) {
        Iris nonIris;
        for (int j = 0; j < 4; ++j) {
            nonIris.features[j] = (rand() % 11)/10.0 ;
        }
        nonIris.label = 3; // 非鸢尾花
        data.push_back(nonIris);
    }
    return data;
}
  • 函数返回值以及参数

    • 该函数返回一个 std::vector<Iris> 容器,包含生成的非鸢尾花数据。

    • numSamples 参数指定要生成的样本数量。

  • 设置随机数种子

    • 使用当前时间设置随机数种子,确保每次运行程序时生成不同的随机数。
  • 生成非鸢尾花数据

    • 对于每一个样本:
      • 创建一个 Iris 对象 nonIris
      • 使用内层的 for 循环生成四个随机特征值,每个特征值在 0 到 1 之间。
      • 将标签设置为 3,表示非鸢尾花。
      • 将生成的 nonIris 对象添加到 data 容器中。
  • 增加非鸢尾花集更好的模拟实际情况,增加数据的多样性和复杂性。

5.将数据写入文件

void writeDataToFile(const vector<Iris>& data, const string& filename) {
    ofstream file(filename);
    if (!file.is_open()) {
        cerr << "无法打开文件" << endl;
        return;
    }

    for (const auto& iris : data) {
        file << iris.features[0] << " " << iris.features[1] << " " << iris.features[2] << " " << iris.features[3] << " " << labelToString(iris.label) << endl;
    }

    file.close();
}
  • for (const auto& iris : data)运用这种方式
    • 使用 auto 关键字可以让编译器自动推导出 iris 的类型。不需要显式地指定类型,减少了代码的冗长和可能的错误。
    • 使用 const auto& 可以避免不必要的拷贝操作,提高性能,并且通过引用来访问元素,这样修改 iris 的值不会影响原来的数据。另外,const 确保了在循环体内不会修改容器中的元素。

6.进行特征缩放

void scaleFeatures(vector<Iris>& data, array<double, 4>& minVal, array<double, 4>& maxVal) {
    // 找到每个特征的最小值和最大值
    for (const auto& iris : data) {
        for (int i = 0; i < 4; ++i) {
            if (iris.features[i] < minVal[i]) minVal[i] = iris.features[i];
            if (iris.features[i] > maxVal[i]) maxVal[i] = iris.features[i];
        }
    }

    // 进行特征缩放
    for (auto& iris : data) {
        for (int i = 0; i < 4; ++i) {
            iris.features[i] = (iris.features[i] - minVal[i]) / (maxVal[i] - minVal[i]);
        }
    }
}
  • 目的:通过特征缩放,可以使不同特征值在相同的范围内,有助于提高机器学习算法的性能和准确性。

  • 缩放后的特征值 = (特征值 − 最小值) / (最大值 − 最小值) 缩放后的特征值=(特征值−最小值)/(最大值−最小值) 缩放后的特征值=(特征值最小值)/(最大值最小值)

    这是特征缩放公式。

  • 步骤:

    • 找到每个特征的最小值和最大值
      • 遍历数据集中的每个 Iris 对象。
      • 对于每个特征(共有四个),找到当前特征的最小值和最大值,并更新 minValmaxVal 数组。
    • 进行特征缩放
  • 优点:

    • 消除特征值的量纲差异:特征缩放使得所有特征值在同一个量纲范围内,有助于提高机器学习算法(如 K-means)的性能和准确性。
    • 增强数值稳定性:通过归一化,可以减少数值计算中的溢出和下溢问题,提高数值稳定性。

7.k-means类

  • +(public)、-(private)用于修饰
  • 这是用plantUML绘制的用例图
1.构造函数
 KMeans(int k, int maxIterations, double tol = 1e-4, int nInit = 3) : k(k), maxIterations(maxIterations), tol(tol), nInit(nInit) {}
  • 初始化成员变量

    • k:聚类的数量(簇的数量)。

    • maxIterations:KMeans 算法的最大迭代次数。

    • tol:用于判断收敛的容差,默认为 1e-4

    • nInit:初始化次数,即多次运行 KMeans++ 算法以选择最佳质心,默认为 3

2.euclideanDistance函数——计算欧氏距离
double euclideanDistance(const array<double, 4>& a, const array<double, 4>& b) {
    double sum = 0.0;
    for (int i = 0; i < 4; ++i) {
        sum += pow(a[i] - b[i], 2);
    }
    return sqrt(sum);
}
  • 欧氏距离的数学公式

    • 欧氏距离(Euclidean Distance)是两个向量之间最常用的距离度量方法,其公式如下:

      在这里插入图片描述

    • 使用一个 for 循环遍历向量的每个维度。

      对于每个维度 i,计算 a[i]b[i] 的差的平方,并将其累加到 sum 中。

      使用 pow 计算平方。

3.initializeCentroids函数——初始化质心
void initializeCentroids(const vector<Iris>& data) {
        centroids.push_back(data[rand() % data.size()].features);
        for (int i = 1; i < k; ++i) {
            vector<double> distances(data.size(), numeric_limits<double>::max());

            for (size_t j = 0; j < data.size(); ++j) {
                for (int m = 0; m < i; ++m) {
                    double dist = euclideanDistance(data[j].features, centroids[m]);
                    if (dist < distances[j]) {
                        distances[j] = dist;
                    }
                }
            }

            double totalDistance = accumulate(distances.begin(), distances.end(), 0.0);
            double randVal = (double)rand() / RAND_MAX * totalDistance;
            for (size_t j = 0; j < data.size(); ++j) {
                randVal -= distances[j];
                if (randVal <= 0) {
                    centroids.push_back(data[j].features);
                    break;
                }
            }
        }
    }
  • 选择第一个质心

    • 这里使用 rand() % data.size() 随机选择数据集中的一个点作为第一个质心。
    • data[rand() % data.size()].features 获取这个随机点的特征并将其添加到 centroids 向量中。
  • 选择剩余质心

    • 外层 for 循环从 i = 1 开始,直到选择出 k 个质心。
    • 初始化一个向量 distances,长度与数据集大小相同,初始值设置为 numeric_limits<double>::max()(将此作为一个极大值),用于存储每个点到最近质心的距离。
  • 计算每个点到最近质心的距离

    • 内层 for 循环遍历数据集中的每个点 j,并对每个点计算其到所有已有质心(从 0i-1)的距离。
    • 调用 euclideanDistance 函数(计算欧式距离)计算数据点 data[j].features 到质心 centroids[m] 的欧氏距离。
    • 如果计算出的距离 dist 小于 distances[j],则更新 distances[j]
  • 计算总距离

    • 使用 accumulate 计算 distances 向量中所有元素的和,即所有数据点到最近质心的距离之和,存储在 totalDistance 中。
      • 两个参数是迭代器的起始位置和结束位置
  • 选择下一个质心

    • double randVal = (double)rand() / RAND_MAX * totalDistance;这行代码生成一个随机值 randVal,它在 0totalDistance 之间。首先 (double)rand() / RAND_MAX 生成一个 01 之间的随机浮点数,然后乘以 totalDistance,这样 randVal 就在 0totalDistance 之间。
      • RAND_MAX 是 C++ 标准库中定义的一个宏,它表示 rand() 函数能够生成的最大值
    • 遍历数据集中的所有点。
      • 对于每个点,将 randVal 减去该点到已有质心的最短距离。
      • 如果 randVal 变为非正数,将当前点作为新的质心添加到 centroids 中并跳出循环。

好处:

  • 通过一种加权随机选择的方法,确保距离已有质心较远的点有更高的概率被选为新的质心,这种方法有效地分散了质心的位置,从而提高k-means算法的初始化质量。
4.获取某个点最近的质心
int getClosestCentroid(const Iris& point) {
    int bestIdx = 0;
    double bestDist = numeric_limits<double>::max();

    for (int i = 0; i < k; ++i) {
        double dist = euclideanDistance(point.features, centroids[i]);
        if (dist < bestDist) {
            bestDist = dist;
            bestIdx = i;
        }
    }

    return bestIdx;
}

作用:这个函数的作用是找到一个点point离哪个质心最近,并返回该质心的索引。

  • 初始化最优索引和最优距离

    • bestIdx:存储最近质心的索引,初始值为0
    • bestDist:存储最近质心的距离,初始值为numeric_limits<double>::max(),即最大的可能的double值。
  • 遍历所有质心

    • 遍历所有k个质心,计算每个质心到点point的欧几里得距离.
    • euclideanDistance(point.features, centroids[i]):计算点point的特征向量与第i个质心之间的欧几里得距离。
    • 如果计算得到的距离dist小于当前的最优距离bestDist,则更新最优距离和最优索引
  • 返回最优索引

5.计算簇的质心
array<double, 4> calculateCentroidWithPoint(int clusterIdx, const Iris& point) {
    array<double, 4> newCentroid = centroids[clusterIdx];
    for (int i = 0; i < 4; ++i) {
        newCentroid[i] = (centroids[clusterIdx][i] + point.features[i]) / 2.0;
    }
    return newCentroid;
}
  • 参数解释

    • int clusterIdx:这是簇的索引,即我们要更新的质心所在簇的索引。
    • const Iris& point:这是要添加到簇中的新点。、
      • Iris 是一个包含 featureslabel 的结构体,其中 features 是一个包含四个特征值的数组。
  • 创建新质心:

    • 从当前簇的质心开始创建一个新质心,newCentroid 被初始化为当前簇(clusterIdx)的质心。
  • 计算包含新点后的质心:

    • 遍历质心的每一个特征。
    • 对于每个特征,计算当前质心和新点在该维度的平均值。
  • 示例

    • 假设我们有一个簇的质心为 [2.0, 3.0, 4.0, 5.0],并且有一个新点的特征为 [4.0, 5.0, 6.0, 7.0]
    • 返回的结果为: {3.0, 4.0, 5.0, 6.0}
6.判断质心是否收敛
 bool hasConverged(const array<double, 4>& oldCentroid, const array<double, 4>& newCentroid) {
     double sum = 0.0;
     for (int i = 0; i < 4; ++i) {
         sum += pow(oldCentroid[i] - newCentroid[i], 2);
     }
     return sqrt(sum) < tol;
 }
  • hasConverged 函数用于判断旧质心和新质心之间的距离是否小于给定的容差 tol,从而确定质心是否收敛。
    • 初始化距离和
    • 计算欧式距离
    • 返回是否收敛
7.计算误差平方和
double calculateSSE(const vector<Iris>& data) {
    double sse = 0.0;
    for (const auto& point : data) {
        int clusterIdx = getClosestCentroid(point);
        sse += pow(euclideanDistance(point.features, centroids[clusterIdx]), 2);
    }
    return sse;
}
  • 作用:

    • 计算当前质心集合的平方和误差 (SSE),用于衡量聚类效果的好坏。SSE 是每个数据点到其最近质心的距离的平方和。SSE 越小,表示聚类效果越好,因为数据点离它们的质心越近。
  • 初始化 SSE

  • 遍历数据集并计算 SSE

8.数据聚类
    void fit(const vector<Iris>& data) {
        double bestSSE = numeric_limits<double>::max();
        vector<array<double, 4>> bestCentroids;

        for (int init = 0; init < nInit; ++init) {
            centroids.clear();
            initializeCentroids(data);

            for (int iter = 0; iter < maxIterations; ++iter) {
                // 分配簇
                vector<vector<Iris>> clusters(k);
                for (const auto& point : data) {
                    int clusterIdx = getClosestCentroid(point);
                    clusters[clusterIdx].push_back(point);
                }

                // 更新质心
                bool converged = true;
                for (int i = 0; i < k; ++i) {
                    if (!clusters[i].empty()) {
                        array<double, 4> newCentroid = calculateCentroid(clusters[i]);
                        if (!hasConverged(centroids[i], newCentroid)) {
                            converged = false;
                        }
                        centroids[i] = newCentroid;
                    }
                }
                if (converged) {
                    break;
                }
            }

            // 计算当前初始化的SSE
            double currentSSE = calculateSSE(data);
            if (currentSSE < bestSSE) {
                bestSSE = currentSSE;
                bestCentroids = centroids;
            }
        }

        centroids = bestCentroids; // 使用最佳质心
    }
  • 作用:用于执行 KMeans 聚类算法,将数据分成 k 个簇。函数多次初始化质心,选择平方和误差最小的初始化结果作为最终质心。

  • 步骤:

    • 初始化最佳 SSE 和最佳质心

      • bestSSE 用于存储最小的 SSE。
      • bestCentroids 用于存储具有最小 SSE 的质心。
    • 多次初始化质心并运行 KMeans

      • 外层循环 nInit 次,用于多次初始化质心。
      • 每次初始化前清空 centroids,然后调用 initializeCentroids 初始化质心。
    • 进行 KMeans 聚类

      • 内层循环 maxIterations 次,进行最大迭代次数的 KMeans 聚类。
      • 创建一个大小为 kclusters 向量,用于存储每个簇中的点。
      • 遍历数据集中的每个点,将其分配到最近的质心对应的簇中。
    • 更新质心

      • 遍历每个簇,计算新的质心。
      • 检查簇是否为空如果簇不为空,则计算新质心。
        • 调用 calculateCentroid 函数计算当前簇 clusters[i] 的新质心。
        • 调用 hasConverged 函数检查旧质心 centroids[i] 和新质心 newCentroid 之间的距离是否小于容差 tol
          • 如果质心没有收敛(距离大于容差),则将 converged 设为 false
      • 更新质心为新的质心。
      • 如果所有质心都收敛(converged 仍为 true),则提前结束循环。
    • 计算 SSE 并更新最佳质心

      • 计算当前质心集合的 SSE。
      • 如果当前 SSE 小于最佳 SSE,则更新最佳 SSE 和最佳质心。
    • 使用最佳质心

9.迭代预测给定点所属的簇,并输出迭代过程
  int predictWithIteration(Iris& point) {
      array<double, 4> currentCentroid = point.features;

      for (int iter = 0; iter < maxIterations; ++iter) {
          // 计算到每个质心的距离并打印
          for (int i = 0; i < k; ++i) {
              double dist = euclideanDistance(point.features, centroids[i]);
              cout << "第 " << iter + 1 << " 次迭代,簇 " << i + 1 << ",质心: ";
              for (int j = 0; j < 4; ++j) {
                  cout << centroids[i][j] << " ";
              }
              cout << ",距离: " << dist << endl;
          }

          // 寻找最接近的质心
          int clusterIdx = getClosestCentroid(point);

          // 打印预测的簇
          cout << "第 " << iter + 1 << " 次迭代预测簇: " << clusterIdx + 1 << endl;

          // 更新质心
          centroids[clusterIdx] = calculateCentroidWithPoint(clusterIdx, point);

          // 打印更新后的质心
          cout << "第 " << iter + 1 << " 次迭代,更新后的质心 " << clusterIdx + 1 << ": ";
          for (int j = 0; j < 4; ++j) {
              cout << centroids[clusterIdx][j] << " ";
          }
          cout << endl;

          if (hasConverged(currentCentroid, centroids[clusterIdx])) {
              break;
          }
          currentCentroid = centroids[clusterIdx];
      }

      return getClosestCentroid(point);
  }
  • 初始化当前质心

    • 将当前点的特征初始化为当前质心。
  • 开始迭代

    • 遍历每个簇,计算当前点到每个质心的欧几里得距离。

      打印每个质心的坐标及其距离。

  • 寻找最接近的质心

  • 打印预测的簇

  • 更新质心

  • 打印更新后的质心

  • 返回最终预测的簇

8.计算准确率

double calculateAccuracy(const vector<Iris>& data, KMeans& kmeans, vector<int>& predictions) {
    int correctPredictions = 0;

    for (const auto& point : data) {
        int predictedCluster = kmeans.predict(point);
        predictions.push_back(predictedCluster);
        if (predictedCluster == point.label) {
            correctPredictions++;
        }
    }

    return static_cast<double>(correctPredictions) / data.size();
}
  • 初始化正确预测计数器
    • 初始化一个计数器 correctPredictions,用于记录预测正确的样本数量。
  • 遍历数据集
  • 进行预测并保存预测结果
    • 对每个点调用 kmeans 模型的 predict 方法进行预测,得到预测的簇索引。
    • 将预测结果存储在 predictions 向量中。
  • 比较预测结果与真实标签
    • 如果预测的簇索引与点的真实标签相同,则增加 correctPredictions 计数器。
  • 计算并返回准确率
    • 将正确预测的数量 correctPredictions 转换为 double 类型,并除以数据集的大小 data.size(),计算预测的准确率。
    • 返回准确率。

9.显示图片并打印类别

void showImage(const string& windowName, const string& imagePath, const string& text) {
    Mat image = imread(imagePath);
    if (image.empty()) {
        cerr << "无法打开图片文件: " << imagePath << endl;
        return;
    }
    putText(image, text, Point(10, 30), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 0, 255), 2);
    imshow(windowName, image);
    waitKey(0);
    destroyWindow(windowName);
}
  • 读取图像文件

    • 使用 imread 函数读取指定路径的图像文件,并存储在 image 变量中。
    • 如果图像文件无法打开,image 将为空矩阵。此时输出错误信息并返回。
  • 在图像上绘制文本

    • 使用 putText 函数在图像上绘制文本。
    • 参数说明:
      • image:目标图像
      • text:要绘制的文本字符串。
      • Point(10, 30):文本在图像上的左下角位置(坐标 (10, 30)
      • FONT_HERSHEY_SIMPLEX:字体类型。
      • 1:字体大小缩放因子。
      • Scalar(0, 0, 255):文本颜色(BGR,红色)。
      • 2:文本的线条粗细。
  • 显示图像

    • 使用 imshow 函数在名为 windowName 的窗口中显示图像。
    • 使用 waitKey(0) 等待用户按键,确保图像窗口保持打开状态。
    • 使用 destroyWindow 销毁图像窗口。

10.main函数

int main() {
    // 加载鸢尾花数据集
    string filename = "iris.txt";
    vector<Iris> irisData = loadIrisData(filename);

    // 生成非鸢尾花数据并加入到鸢尾花数据集中
    vector<Iris> nonIrisData = generateNonIrisData(50);
    irisData.insert(irisData.end(), nonIrisData.begin(), nonIrisData.end());

    // 将合并后的数据写回文件
    writeDataToFile(irisData, filename);

    // 进行特征缩放
    array<double, 4> minVal = { numeric_limits<double>::max(), numeric_limits<double>::max(), numeric_limits<double>::max(), numeric_limits<double>::max() };
    array<double, 4> maxVal = { numeric_limits<double>::min(), numeric_limits<double>::min(), numeric_limits<double>::min(), numeric_limits<double>::min() };
    scaleFeatures(irisData, minVal, maxVal);

    // 划分训练集和测试集
    vector<Iris> trainingSet;
    vector<Iris> testSet;

    // 从前150个鸢尾花数据中随机选择70个样本作为训练集,剩余的30个作为测试集
    int numIrisTrain = 70;
    int numIrisTest = 30;
    vector<int> irisIndices(150);
    iota(irisIndices.begin(), irisIndices.end(), 0);
    random_shuffle(irisIndices.begin(), irisIndices.end());

    for (int i = 0; i < numIrisTrain; ++i) {
        trainingSet.push_back(irisData[irisIndices[i]]);
    }
    for (int i = numIrisTrain; i < numIrisTrain + numIrisTest; ++i) {
        testSet.push_back(irisData[irisIndices[i]]);
    }

    // 从非鸢尾花数据集中随机选择30个样本作为训练集,剩余的20个作为测试集
    int numNonIrisTrain = 30;
    int numNonIrisTest = 20;
    vector<int> nonIrisIndices(nonIrisData.size());
    iota(nonIrisIndices.begin(), nonIrisIndices.end(), 150);
    random_shuffle(nonIrisIndices.begin(), nonIrisIndices.end());

    for (int i = 0; i < numNonIrisTrain; ++i) {
        trainingSet.push_back(irisData[nonIrisIndices[i]]);
    }
    for (int i = numNonIrisTrain; i < numNonIrisTrain + numNonIrisTest; ++i) {
        testSet.push_back(irisData[nonIrisIndices[i]]);
    }

    KMeans kmeans(4, 100, 1e-4, 500);
    kmeans.fit(trainingSet);

    vector<int> predictions;
    double accuracy = calculateAccuracy(testSet, kmeans, predictions);
    cout << "预测准确率: " << accuracy * 100 << "%" << endl;

    // 打印训练集的预测结果
    vector<int> trainPredictions;
    double trainAccuracy = calculateAccuracy(trainingSet, kmeans, trainPredictions);
    cout << "训练集准确率: " << trainAccuracy * 100 << "%" << endl;

    for (size_t i = 0; i < trainingSet.size(); ++i) {
        cout << "训练集样本 " << i + 1 << " 预测簇: " << trainPredictions[i] << " 实际标签: " << trainingSet[i].label << endl;
    }

    for (size_t i = 0; i < testSet.size(); ++i) {
        cout << "测试集样本 " << i + 1 << " 预测簇: " << predictions[i] << " 实际标签: " << testSet[i].label << endl;
    }

    while (true) {
        cout << "请输入四个特征值(花萼长,花萼宽,花瓣长,花瓣宽),用空格分隔,输入 -1 退出:" << endl;
        Iris input;
        for (int i = 0; i < 4; ++i) {
            cin >> input.features[i];
        }
        if (input.features[0] == -1) {
            break;
        }

        // 进行特征缩放
        for (int i = 0; i < 4; ++i) {
            input.features[i] = (input.features[i] - minVal[i]) / (maxVal[i] - minVal[i]);
        }

        int predictedCluster = kmeans.predictWithIteration(input);
        cout << "最终预测簇: " << predictedCluster << endl;

        switch (predictedCluster) {
        case 0:
            showImage("结果", "iris_setosa.jpg", "Iris-setosa");
            break;
        case 1:
            showImage("结果", "iris_versicolor.jpg", "Iris-versicolor");
            break;
        case 2:
            showImage("结果", "iris_virginica.jpg", "Iris-virginica");
            break;
        case 3:
            showImage("结果", "non_iris.jpg", "zhangsong");
            break;
        default:
            cerr << "未知簇" << endl;
            break;
        }
    }

    return 0;
}

四、效果图

在这里插入图片描述

五、测试分析

  • 这个整体的预测准确率有待提高,我们提高了初始化质心的次数,但依旧不够,一般准确率只有20~30,最好的时候是88.

六、源码

#include <iostream>
#include <fstream>
#include <vector>
#include <array>
#include <cmath>
#include <limits>
#include <algorithm>
#include <numeric>
#include <cstdlib>
#include <ctime>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

struct Iris {
    array<double, 4> features;
    int label; // 0: Setosa, 1: Versicolour, 2: Virginica, 3: 非鸢尾花
};

// 将字符串标签映射为整数
int labelToInt(const string& label) {
    if (label == "setosa") return 0;
    if (label == "versicolor") return 1;
    if (label == "virginica") return 2;
    return 3;
}

// 将整数标签映射为字符串
string labelToString(int label) {
    if (label == 0) return "setosa";
    if (label == 1) return "versicolor";
    if (label == 2) return "virginica";
    return "non-iris";
}

// 加载鸢尾花数据集
vector<Iris> loadIrisData(const string& filename) {
    vector<Iris> data;
    ifstream file(filename);
    if (!file.is_open()) {
        cerr << "无法打开文件" << endl;
        return data;
    }

    string line;
    while (getline(file, line)) {
        istringstream iss(line);
        Iris iris;
        string label;
        if (iss >> iris.features[0] >> iris.features[1] >> iris.features[2] >> iris.features[3] >> label) {
            iris.label = labelToInt(label);
            data.push_back(iris);
        }
    }

    file.close();
    return data;
}

// 生成非鸢尾花数据
vector<Iris> generateNonIrisData(int numSamples) {
    vector<Iris> data;
    srand(static_cast<unsigned int>(time(0)));
    for (int i = 0; i < numSamples; ++i) {
        Iris nonIris;
        for (int j = 0; j < 4; ++j) {
            nonIris.features[j] = (rand() % 100);
        }
        nonIris.label = 3; // 非鸢尾花
        data.push_back(nonIris);
    }
    return data;
}

// 将数据写回文件
void writeDataToFile(const vector<Iris>& data, const string& filename) {
    ofstream file(filename);
    if (!file.is_open()) {
        cerr << "无法打开文件" << endl;
        return;
    }

    for (const auto& iris : data) {
        file << iris.features[0] << " " << iris.features[1] << " " << iris.features[2] << " " << iris.features[3] << " " << labelToString(iris.label) << endl;
    }

    file.close();
}

// 进行特征缩放
void scaleFeatures(vector<Iris>& data, array<double, 4>& minVal, array<double, 4>& maxVal) {
    // 找到每个特征的最小值和最大值
    for (const auto& iris : data) {
        for (int i = 0; i < 4; ++i) {
            if (iris.features[i] < minVal[i]) minVal[i] = iris.features[i];
            if (iris.features[i] > maxVal[i]) maxVal[i] = iris.features[i];
        }
    }

    // 进行特征缩放
    for (auto& iris : data) {
        for (int i = 0; i < 4; ++i) {
            iris.features[i] = (iris.features[i] - minVal[i]) / (maxVal[i] - minVal[i]);
        }
    }
}

class KMeans {
public:
    KMeans(int k, int maxIterations, double tol = 1e-4, int nInit =100) : k(k), maxIterations(maxIterations), tol(tol), nInit(nInit) {}

    void fit(const vector<Iris>& data) {
        double bestSSE = numeric_limits<double>::max();
        vector<array<double, 4>> bestCentroids;

        for (int init = 0; init < nInit; ++init) {
            centroids.clear();
            initializeCentroids(data);

            for (int iter = 0; iter < maxIterations; ++iter) {
                // 分配簇
                vector<vector<Iris>> clusters(k);
                for (const auto& point : data) {
                    int clusterIdx = getClosestCentroid(point);
                    clusters[clusterIdx].push_back(point);
                }

                // 更新质心
                bool converged = true;
                for (int i = 0; i < k; ++i) {
                    if (!clusters[i].empty()) {
                        array<double, 4> newCentroid = calculateCentroid(clusters[i]);
                        if (!hasConverged(centroids[i], newCentroid)) {
                            converged = false;
                        }
                        centroids[i] = newCentroid;
                    }
                }
                if (converged) {
                    break;
                }
            }

            // 计算当前初始化的SSE
            double currentSSE = calculateSSE(data);
            if (currentSSE < bestSSE) {
                bestSSE = currentSSE;
                bestCentroids = centroids;
            }
        }

        centroids = bestCentroids; // 使用最佳质心
    }

    int predict(const Iris& point) {
        return getClosestCentroid(point);
    }

    int predictWithIteration(Iris& point) {
        array<double, 4> currentCentroid = point.features;

        for (int iter = 0; iter < maxIterations; ++iter) {
            // 计算到每个质心的距离并打印
            for (int i = 0; i < k; ++i) {
                double dist = euclideanDistance(point.features, centroids[i]);
                cout << "第 " << iter + 1 << " 次迭代,簇 " << i + 1 << ",质心: ";
                for (int j = 0; j < 4; ++j) {
                    cout << centroids[i][j] << " ";
                }
                cout << ",距离: " << dist << endl;
            }

            // 寻找最接近的质心
            int clusterIdx = getClosestCentroid(point);

            // 打印预测的簇
            cout << "第 " << iter + 1 << " 次迭代预测簇: " << clusterIdx + 1 << endl;

            // 更新质心
            centroids[clusterIdx] = calculateCentroidWithPoint(clusterIdx, point);

            // 打印更新后的质心
            cout << "第 " << iter + 1 << " 次迭代,更新后的质心 " << clusterIdx + 1 << ": ";
            for (int j = 0; j < 4; ++j) {
                cout << centroids[clusterIdx][j] << " ";
            }
            cout << endl;

            if (hasConverged(currentCentroid, centroids[clusterIdx])) {
                break;
            }
            currentCentroid = centroids[clusterIdx];
        }

        return getClosestCentroid(point);
    }

private:
    int k;
    int maxIterations;
    double tol;
    int nInit;
    vector<array<double, 4>> centroids;

    void initializeCentroids(const vector<Iris>& data) {
        centroids.push_back(data[rand() % data.size()].features);
        for (int i = 1; i < k; ++i) {
            vector<double> distances(data.size(), numeric_limits<double>::max());

            for (size_t j = 0; j < data.size(); ++j) {
                for (int m = 0; m < i; ++m) {
                    double dist = euclideanDistance(data[j].features, centroids[m]);
                    if (dist < distances[j]) {
                        distances[j] = dist;
                    }
                }
            }

            double totalDistance = accumulate(distances.begin(), distances.end(), 0.0);
            double randVal = (double)rand() / RAND_MAX * totalDistance;
            for (size_t j = 0; j < data.size(); ++j) {
                randVal -= distances[j];
                if (randVal <= 0) {
                    centroids.push_back(data[j].features);
                    break;
                }
            }
        }
    }

    //获取某个点最近的质心
    int getClosestCentroid(const Iris& point) {
        int bestIdx = 0;
        double bestDist = numeric_limits<double>::max();

        for (int i = 0; i < k; ++i) {
            double dist = euclideanDistance(point.features, centroids[i]);
            if (dist < bestDist) {
                bestDist = dist;
                bestIdx = i;
            }
        }

        return bestIdx;
    }

    //计算欧式距离
    double euclideanDistance(const array<double, 4>& a, const array<double, 4>& b) {
        double sum = 0.0;
        for (int i = 0; i < 4; ++i) {
            sum += pow(a[i] - b[i], 2);
        }
        return sqrt(sum);
    }

    //计算簇的质心
    array<double, 4> calculateCentroid(const vector<Iris>& cluster) {
        array<double, 4> newCentroid = { 0.0, 0.0, 0.0, 0.0 };
        for (const auto& point : cluster) {
            for (int i = 0; i < 4; ++i) {
                newCentroid[i] += point.features[i];
            }
        }
        for (int i = 0; i < 4; ++i) {
            newCentroid[i] /= cluster.size();
        }
        return newCentroid;
    }

    //计算簇的质心
    array<double, 4> calculateCentroidWithPoint(int clusterIdx, const Iris& point) {
        array<double, 4> newCentroid = centroids[clusterIdx];
        for (int i = 0; i < 4; ++i) {
            newCentroid[i] = (centroids[clusterIdx][i] + point.features[i]) / 2.0;
        }
        return newCentroid;
    }

    //判断质心是否收敛
    bool hasConverged(const array<double, 4>& oldCentroid, const array<double, 4>& newCentroid) {
        double sum = 0.0;
        for (int i = 0; i < 4; ++i) {
            sum += pow(oldCentroid[i] - newCentroid[i], 2);
        }
        return sqrt(sum) < tol;
    }

    //计算误差平方和
    double calculateSSE(const vector<Iris>& data) {
        double sse = 0.0;
        for (const auto& point : data) {
            int clusterIdx = getClosestCentroid(point);
            sse += pow(euclideanDistance(point.features, centroids[clusterIdx]), 2);
        }
        return sse;
    }
};

// 计算准确率
double calculateAccuracy(const vector<Iris>& data, KMeans& kmeans, vector<int>& predictions) {
    int correctPredictions = 0;

    for (const auto& point : data) {
        int predictedCluster = kmeans.predict(point);
        predictions.push_back(predictedCluster);
        if (predictedCluster == point.label) {
            correctPredictions++;
        }
    }

    return static_cast<double>(correctPredictions) / data.size();
}

// 显示图片并打印类别
void showImage(const string& windowName, const string& imagePath, const string& text) {
    Mat image = imread(imagePath);
    if (image.empty()) {
        cerr << "无法打开图片文件: " << imagePath << endl;
        return;
    }
    putText(image, text, Point(10, 30), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 0, 255), 2);
    imshow(windowName, image);
    waitKey(0);
    destroyWindow(windowName);
}

int main() {
    // 加载鸢尾花数据集
    string filename = "iris.txt";
    vector<Iris> irisData = loadIrisData(filename);

    // 生成非鸢尾花数据并加入到鸢尾花数据集中
    vector<Iris> nonIrisData = generateNonIrisData(50);
    irisData.insert(irisData.end(), nonIrisData.begin(), nonIrisData.end());

    // 将合并后的数据写回文件
    writeDataToFile(irisData, filename);

    // 进行特征缩放
    array<double, 4> minVal = { numeric_limits<double>::max(), numeric_limits<double>::max(), numeric_limits<double>::max(), numeric_limits<double>::max() };
    array<double, 4> maxVal = { numeric_limits<double>::min(), numeric_limits<double>::min(), numeric_limits<double>::min(), numeric_limits<double>::min() };
    scaleFeatures(irisData, minVal, maxVal);

    // 划分训练集和测试集
    vector<Iris> trainingSet;
    vector<Iris> testSet;

    // 从鸢尾花数据集中随机选择70个样本作为训练集,剩余的30个作为测试集
    int numIrisTrain = 70;
    int numIrisTest = 30;
    vector<int> irisIndices(irisData.size() - nonIrisData.size());
    iota(irisIndices.begin(), irisIndices.end(), 0);
    random_shuffle(irisIndices.begin(), irisIndices.end());

    for (int i = 0; i < numIrisTrain; ++i) {
        trainingSet.push_back(irisData[irisIndices[i]]);
    }
    for (int i = numIrisTrain; i < numIrisTrain + numIrisTest; ++i) {
        testSet.push_back(irisData[irisIndices[i]]);
    }

    // 从非鸢尾花数据集中随机选择30个样本作为训练集,剩余的20个作为测试集
    int numNonIrisTrain = 30;
    int numNonIrisTest = 20;
    vector<int> nonIrisIndices(nonIrisData.size());
    iota(nonIrisIndices.begin(), nonIrisIndices.end(), irisData.size() - nonIrisData.size());
    random_shuffle(nonIrisIndices.begin(), nonIrisIndices.end());

    for (int i = 0; i < numNonIrisTrain; ++i) {
        trainingSet.push_back(irisData[nonIrisIndices[i]]);
    }
    for (int i = numNonIrisTrain; i < numNonIrisTrain + numNonIrisTest; ++i) {
        testSet.push_back(irisData[nonIrisIndices[i]]);
    }

    KMeans kmeans(4, 100, 1e-4, 3);
    kmeans.fit(trainingSet);

    vector<int> predictions;
    double accuracy = calculateAccuracy(testSet, kmeans, predictions);
    cout << "预测准确率: " << accuracy * 100 << "%" << endl;

    // 打印训练集的预测结果
    vector<int> trainPredictions;
    double trainAccuracy = calculateAccuracy(trainingSet, kmeans, trainPredictions);
    cout << "训练集准确率: " << trainAccuracy * 100 << "%" << endl;

    for (size_t i = 0; i < trainingSet.size(); ++i) {
        cout << "训练集样本 " << i + 1 << " 预测簇: " << trainPredictions[i] << " 实际标签: " << trainingSet[i].label << endl;
    }

    for (size_t i = 0; i < testSet.size(); ++i) {
        cout << "测试集样本 " << i + 1 << " 预测簇: " << predictions[i] << " 实际标签: " << testSet[i].label << endl;
    }

    while (true) {
        cout << "请输入四个特征值(花萼长,花萼宽,花瓣长,花瓣宽),用空格分隔,输入 -1 退出:" << endl;
        Iris input;
        for (int i = 0; i < 4; ++i) {
            cin >> input.features[i];
        }
        if (input.features[0] == -1) {
            break;
        }

        // 进行特征缩放
        for (int i = 0; i < 4; ++i) {
            input.features[i] = (input.features[i] - minVal[i]) / (maxVal[i] - minVal[i]);
        }

        int predictedCluster = kmeans.predictWithIteration(input);
        cout << "最终预测簇: " << predictedCluster << endl;

        switch (predictedCluster ) {
        case 0:
            showImage("结果", "iris_setosa.jpg", "Iris-setosa");
            break;
        case 1:
            showImage("结果", "iris_versicolor.jpg", "Iris-versicolor");
            break;
        case 2:
            showImage("结果", "iris_virginica.jpg", "Iris-virginica");
            break;
        case 3:
            showImage("结果", "non_iris.jpg", "zhangsong");
            break;
        default:
            cerr << "未知簇" << endl;
            break;
        }
    }

    return 0;
}
  • 23
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值