深度学习基础 Week0 机器学习实例:初识KNN
目录
1背景
在工作中遇到以下问题,对于图片1和图片2,每张图片中均有四个光点(位置不论),我们认为是正常图片,而在图片3中只有三个光点,对于这种缺失光点或是有多余光点的图片,我们认为是不正常的图片。我们要解决的问题时如何在给定的多张图片中找到不合格的图片。在思考这个问题的时候,我想到了大学时期接触过的KNN算法。
2初识KNN
2.1KNN算法原理概述
KNN 算法的工作原理基于这样一个假设:相似的对象在特征空间中距离相近。具体来说,对于待分类的样本,KNN 算法首先计算它与训练集中每个样本之间的距离。然后,算法选取距离最小的 K 个样本,这些样本被称为 “邻居”。最后,根据这些邻居的类别标签,通过投票或加权平均等方式,确定待分类样本的类别。
KNN 算法的核心在于距离度量,它决定了样本之间的相似度。通过选择合适的距离度量方法,KNN 算法能够准确地找出与待分类样本最相似的邻居,从而进行准确的分类。
2.2距离度量方法(如欧氏距离、曼哈顿距离等)
在 KNN 算法中,距离度量方法的选择至关重要。常用的距离度量方法包括欧氏距离和曼哈顿距离。
欧氏距离是最常用的距离度量方法之一,它衡量的是多维空间中两点之间的直线距离。对于二维空间中的两个点 A (x1, y1) 和 B (x2, y2),它们的欧氏距离可以通过以下公式计算:
曼哈顿距离也称为城市街区距离,它衡量的是两点在标准坐标系上的绝对轴距总和。在二维空间中,曼哈顿距离可以通过以下公式计算:
除了欧氏距离和曼哈顿距离,还有其他一些距离度量方法,如切比雪夫距离、闵可夫斯基距离等。这些距离度量方法在不同的应用场景下可能具有不同的优势和适用性。
2.3确定K值
在 KNN 算法中,K 值的选择对分类结果具有重要影响。K 值太小可能导致过拟合,即算法对训练数据的噪声过于敏感;而 K 值太大则可能导致欠拟合,即算法忽略了训练数据中的有用信息。
确定 K 值的常用方法包括交叉验证和网格搜索。交叉验证是一种评估模型性能的方法,它将数据集划分为多个子集,通过多次训练和测试来选择最优的 K 值。网格搜索则是一种参数调优方法,它通过在一定的参数范围内进行穷举搜索,找到使得模型性能最优的 K 值。
在实际应用中,可以根据问题的具体需求和数据集的特性来选择合适的 K 值。通常,可以通过实验和比较不同 K 值下的分类性能来确定最优的 K 值。
3具体实现
3.1输入训练集
读取 3 个文件夹的图片,分别作为训练集 0、训练集 1、训练集 2(每个文件夹均包含20张图片),其中训练集0中的图片全为合格,训练集1中的图片都不合格,训练集2中的图片各占一半,并使用OpenCV进行图像预处理,将彩色图转单通道灰度图,减少数据维度,降低 KNN 计算量,只用灰度像素值作为图像特征。最后将二维灰度图片,拉直成一维长向量,作为 KNN 可识别的样本特征。
//====================1. 定义文件夹路径对象,读取第一类样本train0====================
// 创建QDir目录对象,绑定训练集0文件夹路径
QDir dir("/home/zty/000/train0");
// entryInfoList():获取文件夹下所有文件/子目录的详细信息列表(包含. .. 隐藏文件、图片等)
QFileInfoList list = dir.entryInfoList();
Mat img, gray; // img:存储读取的彩色原图;gray:存储转换后的灰度图
Mat data,labels; // data:KNN算法的特征样本矩阵;labels:对应每张图片的分类标签
// 遍历文件夹内文件,i从2开始:跳过下标0(.)、下标1(..)两个系统默认目录,只读取真实图片文件
for (int i = 2; i < list.size(); ++i)
{
// filePath()获取文件完整路径,转std::string格式;imread读取彩色图像存入img
img = imread(list[i].filePath().toStdString());
// 色彩空间转换:BGR彩色图转为单通道灰度图(OpenCV默认读取是BGR,宏COLOR_RGB2GRAY兼容通用灰度转换)
cvtColor(img, gray, COLOR_RGB2GRAY);
Mat tmp;
// 灰度图深拷贝到临时矩阵tmp,防止后续灰度图变量覆盖污染数据
gray.copyTo(tmp);
// reshape(0,1):通道数不变,把二维灰度图像重塑成【单行一维向量】(扁平化序列化)
// push_back:把扁平化后的单行向量追加到总特征矩阵data中,data一行对应一张图片特征
data.push_back(tmp.reshape(0,1));
// 当前train0全部样本统一打上标签0,存入标签矩阵labels,顺序和data样本一一对应
labels.push_back(0);
}
//====================2. 切换目录读取第二类样本train1,标签为1====================
// 修改dir对象绑定路径为train1文件夹
dir.setPath("/home/zty/000/train1");
list = dir.entryInfoList(); // 重新获取train1内所有文件信息
for (int i = 2; i < list.size(); ++i)
{
img = imread(list[i].filePath().toStdString()); // 读取单张图片
cvtColor(img, gray, COLOR_RGB2GRAY); // 转灰度
Mat tmp;
gray.copyTo(tmp);
data.push_back(tmp.reshape(0,1)); // 图像扁平化并入特征集
labels.push_back(1); // 二类标签1
}
//====================3. 切换目录读取第三类样本train2,标签为2====================
dir.setPath("/home/zty/000/train2");
list = dir.entryInfoList();
for (int i = 2; i < list.size(); ++i)
{
img = imread(list[i].filePath().toStdString());
cvtColor(img, gray, COLOR_RGB2GRAY);
Mat tmp;
gray.copyTo(tmp);
data.push_back(tmp.reshape(0,1));
labels.push_back(2); // 三类标签2
}
3.2数据类型转换
将训练集中的数据转换为适配KNN要求的格式。灰度图像素原生类型是uchar(0~255 8 位无符号整数),KNN 算法距离计算(欧式距离)需要浮点运算,必须转为 32 位浮点CV_32F,否则计算精度异常、模型训练失效。
//====================4. 数据类型转换,适配KNN输入要求====================
// 原图灰度像素默认是uchar(0~255无符号字节),KNN浮点计算需要32位浮点数CV_32F
data.convertTo(data, CV_32F);
3.3KNN 模型构建与训练
取k=1,只取距离最近1个样本的标签,以最简单的KNN训练模型。
//====================5. 初始化K近邻(KNN)分类器并训练模型====================
int K = 1; // 设置KNN近邻数量:K=1即最近邻算法(1-NN)
// TrainData:OpenCV统一训练数据封装类
// 参数1:特征矩阵data;参数2:ROW_SAMPLE代表data中【每一行是一个样本】;参数3:样本对应标签
Ptr<TrainData> tData = TrainData::create(data, ROW_SAMPLE, labels);
// 创建KNN模型智能指针对象
Ptr<KNearest> model = KNearest::create();
// 设置查找的近邻个数K值
model->setDefaultK(K);
// 设置任务模式:true=分类任务,false=回归任务
model->setIsClassifier(true);
// 使用封装好的训练数据集执行模型训练
model->train(tData);
3.4训练集自预测与正确率校验
拿全部训练样本回测,用来检验模型在训练集上的拟合效果。
//====================6. 用训练集自身做预测,验证训练集准确率====================
//正确率统计计数器
int correct0 = 0; // train0正确个数
int correct1 = 0; // train1正确个数
int correct2 = 0; // train2正确个数
int totalCorrect = 0; // 全局总正确
for (int i = 0; i < data.rows; i++)
{
Mat sample = data.row(i);
float predictRes = model->predict(sample);
int realLabel = labels.at<int>(i);
bool isRight = (fabs(predictRes - realLabel) <= FLT_EPSILON);
// 全局正确计数
if(isRight) totalCorrect++;
// 分组计数
if(i < 20)
{
// 前20个属于train0
if(isRight) correct0++;
}
else if(i < 40)
{
// 中间20个属于train1
if(isRight) correct1++;
}
else
{
// 后面20个属于train2
if(isRight) correct2++;
}
}
// ====================计算并打印各组、整体正确率====================
// 防止除以0
double acc0 = (double)correct0 / 20 * 100.0;
double acc1 = (double)correct1 / 20 * 100.0;
double acc2 = (double)correct2 / 20 * 100.0;
double accAll = (double)totalCorrect / 60 * 100.0;
qDebug() << "=====分类正确率结果=====";
qDebug() << "train0(标签0):正确" << correct0 << "/" << 20 << " 正确率:" << acc0 << "%";
qDebug() << "train1(标签1):正确" << correct1 << "/" << 20 << " 正确率:" << acc1 << "%";
qDebug() << "train2(标签2):正确" << correct2 << "/" << 20 << " 正确率:" << acc2 << "%";
qDebug() << "整体全部数据集:正确" << totalCorrect << "/" << 20 << " 总正确率:" << accAll << "%";
3.5预测结果展示

4总结
4.1代码不足与优化
- 输入训练集时三段重复读取逻辑,可以封装成一个加载样本函数减少冗余;
- 只用训练集自测会出现过拟合,正规流程需要拆分训练集 + 测试集分开评估;
- K=1 容易受噪声干扰,可调试 K=3、5、7 对比精度。
4.2未来展望
这次学习初步掌握了机器学习中KNN(K近邻)算法的基本原理和简单实现,能够完成基础的分类任务。具体来说,我理解了KNN算法的"惰性学习"特性,掌握了通过计算测试样本与训练样本之间的距离(如欧式距离)来寻找最近邻的方法,并能够使用sklearn的KNeighborsClassifier进行简单的模型训练和预测。
然而,在实际应用KNN算法时,我注意到以下几个关键环节尚未深入研究:
- 特征缩放:KNN算法对特征尺度非常敏感,不同量纲的特征会影响距离计算的结果。常见的标准化方法如Min-Max归一化和Z-score标准化都需要进一步实践。
- 缺失值处理:当训练数据存在缺失值时,如何处理才能不影响距离度量的准确性,是KNN算法应用中需要解决的重要问题。
- 距离度量选择:除了欧式距离,曼哈顿距离、余弦相似度等不同距离度量方法对模型性能的影响尚不清楚。
- 参数优化:如何通过交叉验证选择合适的K值,以及权重设置(如距离加权)对模型的影响需要进一步探索。
通过这次学习,我认识到机器学习算法的理论理解和工程实践之间存在一定差距,后续将更加系统地学习KNN算法的完整实现以及机器学习的其他内容,并寻找合适的项目进行实战练习。
更多推荐


所有评论(0)