手把手教你基于SVM的数字识别( C++/opencv)(逐曦战队算法组寒假自学实战1装甲板数字识别讲解)
逐曦算法组寒假实践内容前两部分理解即可,选做第二部分代码搭建,需将第三部分移植进大作业装甲板识别代码实现装甲板数字识别部分。
本文主要用于新队员寒假内容教学,也具体讲述了SVM从理解计算到逐步环境配置、代码实现的全过程,可充分用于学习实践中,水平有限欢迎交流指正。
一、SVM(支持向量机)理解介绍
1、机器学习
机器学习的核心是“使用算法解析数据,从中学习,然后对世界上的某件事情做出决定或预测”。这意味着,与其显式地编写程序来执行某些任务,不如教计算机如何开发一个算法来完成任务。
在对机器学习的使用中,我们的任务就是通过给计算机输入数据,告诉它这些数据对应的标签。通过一系列训练,达到再输入其他数据时,计算机可以给出对应的结果标签。
机器学习有很多种类型,训练的分类方法也有很多种。机器学习还是人工智能的一个分支,使用特定的算法和编程方法实现人工智能。这里我们不做讨论。
2、SVM
(本部分内容参考Bilibili【数之道】视频,文中简化了视频内容并修改添加了一些我自己的简单思路,有时间可以去看一下原视频,讲的很好。)
(1)从直觉理解SVM处理的问题
任务目标是将两种颜色的点通过一条直线区分开,并在接下来添加新的点时能通过点相对于直线的位置判断点的类型。但在两种点间有很多种直线添加的方法。
分类边界线位于两类数据点中部,间隔越大意味着两类数据点差异越大。因此,求解两类数据的间隔,间隔正中就是我们所求的直线,即最佳决策线。
该分类可被应用在三维甚至四维空间中,称为决策超平面。
离直线最近的数据点被称为支持向量。因此此分类方法被称为支持向量机(support vector machine)。
存在异常值时
异常值与支持向量之间的间隔即为损失。在完成分类时,我们须考虑损失与间隔间的利益关系。
现有间隔称为硬间隔。异常值出现后,若使决策平面迎合异常值改变,则硬间隔将缩小。若保证异常值不变,将损失称为软间隔。
升维转换
显然,这个二维空间中无法通过一条直线对两种颜色的点进行有效区分,进行升维转换如下。
通过合适的维度转换方法,利用合适的转换函数对数据进行转换。
可通过核技巧,获得数据高纬度向量相似度,不用知道转换函数即可判断。
(2)SVM计算(求解最佳决策超平面)
优化问题
在决策超平面上取两个点,代入决策超平面表达式,得5、6,两式相减后化简,得出向量W与两点间向量点积为0,即w垂直与决策超平面。
保留一组支持向量,代入正负超平面方程得到等式1、2,相减化简得到式4,进而将两向量乘积转化为向量长度与夹角乘积的形式。
而两点间向量乘夹角即为两点间向量在向量w上的投影长度L。由于向量w与决策超平面垂直,该长度恰好为所要计算的间隔。
最后即可得到L用向量w表达的式子。我们的目的是要求出L的最大值,即找到向量w的最小长度。
为去除w最小长度求取中的根号,对w值取平方并*2,得出新函数f(w)求最小值。
此时,若约束条件为等式,可直接使用拉格朗日乘子法求解。
而回到对两种数据点进行区分的部分,数据点都位于正超平面上方或负超平面下方。约束点为不等式时,须增加非负变量变量将不等式转化为等式约束,再进行求解。得拉格朗日方程式。
补一点拉格朗日乘子法
寻找多元函数在一组约束下的极值的方法。引入拉格朗日乘子,可将有 d 个变量与 k个约束条件的最优化问题转化为具有 d+k个变量的无约束优化问题求解。minimize为待求最小值的式子,subject to为约束条件。
(对拉格朗日乘子法的理解可参考知乎文章https://zhuanlan.zhihu.com/p/154517678)
计算:(拉格朗日乘数法求最值 (可能是高数下?))
构造 L=目标函数+λ*(条件方程),分别求出每个变量的偏导令他们为零组成方程组,解出各变量值,得到的值即为驻点,检验每一个驻点是否为极值点、最值点,求最值。
偏导取0得到的四个式子,与本条件下中为满足他们必要的条件乘子>=0,一起组成五个方程,组成KKT条件。
SVM对偶性
为方便求解以及后续核变换使用,通常转化为对偶求解。
最优解在w*、b*处取得,建立新方程,以下不等式必然成立,化简得到不等式。
取最优下界q(λi*),即为对偶问题。最优下界与所求问题相等时,二者强对偶,同时取得最优值。
利用KKT条件求解对偶问题,仅使用支持向量参与计算,即可求解。
(3)核技巧
直接看图
(4)软间隔
二、逐步实现SVM数字分类
有上述理论基础后,SVM分类实现步骤主要就是:创建SVM模型——传入数据集训练——得到训练模型——使用模型测试/使用
cv::svm模块中自带的许多函数使这一流程十分便于实现,因此对此模型的实践意义极大在于调整模型参数达到更好的效果。
1、数据集准备
本文采用MNIST-image手写数字集进行训练和测试。(对于长相标致的装甲板来说手写分类是有点大材小用,但是顺带掌握一下)
数据集下载官网:http://yann.lecun.com/exdb/mnist/
(复制到浏览器打开)
这几个从上到下分别是训练集图像、训练集标签、测试集图像、测试集标签,点击即可下载。
下载好后将四个文件分别解压放入一个文件夹中。
2、SVM实现
完整代码在本部分结尾。
(1)训练数据准备
读取训练数据和测试数据并对数据进行预处理,对读取的图像数据进行归一化处理,将像素值的范围从 [0, 255] 缩放到 [0, 1],以便更好地应用于机器学习模型。
代码量主要就在读取数据的部分。
//读取训练标签数据 (60000,1) 类型为int32
Mat train_labels = read_mnist_label(train_labels_path);
//读取训练图像数据 (60000,784) 类型为float32 数据未归一化
Mat train_images = read_mnist_image(train_images_path);
//将图像数据归一化
train_images = train_images / 255.0;
//读取测试数据标签(10000,1) 类型为int32
Mat test_labels = read_mnist_label(test_labels_path);
//读取测试数据图像 (10000,784) 类型为float32 数据未归一化
Mat test_images = read_mnist_image(test_images_path);
//归一化
test_images = test_images / 255.0;
需要定义几个读取数据的函数。
int reverseInt(int i) {
unsigned char c1, c2, c3, c4;
c1 = i & 255;
c2 = (i >> 8) & 255;
c3 = (i >> 16) & 255;
c4 = (i >> 24) & 255;
return ((int)c1 << 24) + ((int)c2 << 16) + ((int)c3 << 8) + c4;
}
Mat read_mnist_image(const string fileName) {
int magic_number = 0;
int number_of_images = 0;
int n_rows = 0;
int n_cols = 0;
Mat DataMat;
ifstream file(fileName, ios::binary);
if (file.is_open())
{
cout << "成功打开图像集 ..." << endl;
file.read((char*)&magic_number, sizeof(magic_number));//幻数(文件格式)
file.read((char*)&number_of_images, sizeof(number_of_images));//图像总数
file.read((char*)&n_rows, sizeof(n_rows));//每个图像的行数
file.read((char*)&n_cols, sizeof(n_cols));//每个图像的列数
magic_number = reverseInt(magic_number);
number_of_images = reverseInt(number_of_images);
n_rows = reverseInt(n_rows);
n_cols = reverseInt(n_cols);
cout << "幻数(文件格式):" << magic_number
<< " 图像总数:" << number_of_images
<< " 每个图像的行数:" << n_rows
<< " 每个图像的列数:" << n_cols << endl;
cout << "开始读取Image数据......" << endl;
DataMat = Mat::zeros(number_of_images, n_rows * n_cols, CV_32FC1);
for (int i = 0; i < number_of_images; i++) {
for (int j = 0; j < n_rows * n_cols; j++) {
unsigned char temp = 0;
file.read((char*)&temp, sizeof(temp));
//可以在下面这一步将每个像素值归一化
float pixel_value = float(temp);
//按照行将像素值一个个写入Mat中
DataMat.at<float>(i, j) = pixel_value;
}
}
cout << "读取Image数据完毕......" << endl;
}
file.close();
return DataMat;
}
Mat read_mnist_label(const string fileName) {
int magic_number;
int number_of_items;
Mat LabelMat;
ifstream file(fileName, ios::binary);
if (file.is_open())
{
cout << "成功打开标签集 ... " << endl;
file.read((char*)&magic_number, sizeof(magic_number));
file.read((char*)&number_of_items, sizeof(number_of_items));
magic_number = reverseInt(magic_number);
number_of_items = reverseInt(number_of_items);
cout << "幻数(文件格式):" << magic_number << " ;标签总数:" << number_of_items << endl;
cout << "开始读取Label数据......" << endl;
//CV_32SC1代表32位有符号整型 通道数为1
LabelMat = Mat::zeros(number_of_items, 1, CV_32SC1);
for (int i = 0; i < number_of_items; i++) {
unsigned char temp = 0;
file.read((char*)&temp, sizeof(temp));
LabelMat.at<unsigned int>(i, 0) = (unsigned int)temp;
}
cout << "读取Label数据完毕......" << endl;
}
(2)构建svm训练模型并进行训练
创建SVM模型,设置模型参数,将数据导入计算并完成训练。
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
//设置类型为C_SVC代表分类
svm->setType(cv::ml::SVM::C_SVC);
//设置核函数
svm->setKernel(cv::ml::SVM::POLY);
//设置其它属性
svm->setGamma(3.0);
svm->setDegree(3.0);
//设置迭代终止条件
svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS, 300, 0.0001));
//开始训练
cv::Ptr<cv::ml::TrainData> train_data = cv::ml::TrainData::create(train_images, cv::ml::ROW_SAMPLE, train_labels);
cout << "开始进行训练..." << endl;
svm->train(train_data);
cout << "训练完成" << endl;
这段代码涉及到了如下参数:
-
SVM类型 (
setType
):cv::ml::SVM::C_SVC
通常用于分类问题,支持多类别分类。对于二分类问题,也可以考虑cv::ml::SVM::C_SVC
。cv::ml::SVM::NU_SVC
是一种另类的分类器,可以根据实际情况进行尝试,可能在某些情况下效果更好。
-
核函数 (
setKernel
):cv::ml::SVM::POLY
适用于处理非线性关系。如果数据集是线性可分的,可以尝试使用线性核函数cv::ml::SVM::LINEAR
。cv::ml::SVM::RBF
核函数(径向基函数)通常适用于各种问题,尤其是在数据维度较高时。
-
gamma
参数 (setGamma
):- 较小的
gamma
值可能导致决策边界变得更平滑,而较大的值可能导致决策边界更复杂。根据数据的特性,可以尝试不同的gamma
值。
- 较小的
-
degree
参数 (setDegree
):degree
参数控制多项式核的阶数。对于高阶数,模型可能更能够适应复杂的决策边界,但在某些情况下可能导致过拟合。可以尝试不同的degree
值。
-
迭代终止条件 (
setTermCriteria
):- 较小的迭代次数和较大的阈值可能导致模型提前停止,从而影响性能。可以通过交叉验证等方法来选择合适的终止条件。
可自行调整这些参数试探训练效果,理解参数含义。
(3)在测试数据集上预测计算准确率
直接svm->predict预测,最后svm->save保存模型。
Mat pre_out;
//返回值为第一个图像的预测值 pre_out为整个batch的预测值集合
cout << "开始进行预测..." << endl;
float ret = svm->predict(test_images, pre_out);
cout << "预测完成" << endl;
//计算准确率必须将两种标签化为同一数据类型
pre_out.convertTo(pre_out, CV_8UC1);
test_labels.convertTo(test_labels, CV_8UC1);
int equal_nums = 0;
for (int i = 0; i <pre_out.rows; i++)
{
if (pre_out.at<uchar>(i, 0) == test_labels.at<uchar>(i, 0))
{
equal_nums++;
}
}
float acc = float(equal_nums) / float(pre_out.rows);
cout << "测试数据集上的准确率为:" << acc * 100 << "%" << endl;
//保存模型
svm->save("mnist_svm.xml");
getchar();
return 0;
(4)训练完整代码
#include<iostream>
#include<opencv.hpp>
#include <string>
#include <fstream>
using namespace std;
using namespace cv;
//小端存储转换
int reverseInt(int i);
//读取image数据集信息
Mat read_mnist_image(const string fileName);
//读取label数据集信息
Mat read_mnist_label(const string fileName);
string train_images_path = "D:/ZhuXiRM/number/data/train-images.idx3-ubyte";
string train_labels_path = "D:/ZhuXiRM/number/data/train-labels.idx1-ubyte";
string test_images_path = "D:/ZhuXiRM/number/data/t10k-images.idx3-ubyte";
string test_labels_path = "D:/ZhuXiRM/number/data/t10k-labels.idx1-ubyte";
int main()
{
/*
---------第一部分:训练数据准备-----------
*/
//读取训练标签数据 (60000,1) 类型为int32
Mat train_labels = read_mnist_label(train_labels_path);
//读取训练图像数据 (60000,784) 类型为float32 数据未归一化
Mat train_images = read_mnist_image(train_images_path);
//将图像数据归一化
train_images = train_images / 255.0;
//读取测试数据标签(10000,1) 类型为int32
Mat test_labels = read_mnist_label(test_labels_path);
//读取测试数据图像 (10000,784) 类型为float32 数据未归一化
Mat test_images = read_mnist_image(test_images_path);
//归一化
test_images = test_images / 255.0;
/*
---------第二部分:构建svm训练模型并进行训练-----------
*/
cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
//设置类型为C_SVC代表分类
svm->setType(cv::ml::SVM::C_SVC);
//设置核函数
svm->setKernel(cv::ml::SVM::POLY);
//设置其它属性
svm->setGamma(3.0);
svm->setDegree(3.0);
//设置迭代终止条件
svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS, 300, 0.0001));
//开始训练
cv::Ptr<cv::ml::TrainData> train_data = cv::ml::TrainData::create(train_images, cv::ml::ROW_SAMPLE, train_labels);
cout << "开始进行训练..." << endl;
svm->train(train_data);
cout << "训练完成" << endl;
/*
---------第三部分:在测试数据集上预测计算准确率-----------
*/
Mat pre_out;
//返回值为第一个图像的预测值 pre_out为整个batch的预测值集合
cout << "开始进行预测..." << endl;
float ret = svm->predict(test_images, pre_out);
cout << "预测完成" << endl;
//计算准确率必须将两种标签化为同一数据类型
pre_out.convertTo(pre_out, CV_8UC1);
test_labels.convertTo(test_labels, CV_8UC1);
int equal_nums = 0;
for (int i = 0; i <pre_out.rows; i++)
{
if (pre_out.at<uchar>(i, 0) == test_labels.at<uchar>(i, 0))
{
equal_nums++;
}
}
float acc = float(equal_nums) / float(pre_out.rows);
cout << "测试数据集上的准确率为:" << acc * 100 << "%" << endl;
//保存模型
svm->save("mnist_svm.xml");
svm->save("mnist_svm.m");
getchar();
return 0;
}
;
int reverseInt(int i) {
unsigned char c1, c2, c3, c4;
c1 = i & 255;
c2 = (i >> 8) & 255;
c3 = (i >> 16) & 255;
c4 = (i >> 24) & 255;
return ((int)c1 << 24) + ((int)c2 << 16) + ((int)c3 << 8) + c4;
}
Mat read_mnist_image(const string fileName) {
int magic_number = 0;
int number_of_images = 0;
int n_rows = 0;
int n_cols = 0;
Mat DataMat;
ifstream file(fileName, ios::binary);
if (file.is_open())
{
cout << "成功打开图像集 ..." << endl;
file.read((char*)&magic_number, sizeof(magic_number));//幻数(文件格式)
file.read((char*)&number_of_images, sizeof(number_of_images));//图像总数
file.read((char*)&n_rows, sizeof(n_rows));//每个图像的行数
file.read((char*)&n_cols, sizeof(n_cols));//每个图像的列数
magic_number = reverseInt(magic_number);
number_of_images = reverseInt(number_of_images);
n_rows = reverseInt(n_rows);
n_cols = reverseInt(n_cols);
cout << "幻数(文件格式):" << magic_number
<< " 图像总数:" << number_of_images
<< " 每个图像的行数:" << n_rows
<< " 每个图像的列数:" << n_cols << endl;
cout << "开始读取Image数据......" << endl;
DataMat = Mat::zeros(number_of_images, n_rows * n_cols, CV_32FC1);
for (int i = 0; i < number_of_images; i++) {
for (int j = 0; j < n_rows * n_cols; j++) {
unsigned char temp = 0;
file.read((char*)&temp, sizeof(temp));
//可以在下面这一步将每个像素值归一化
float pixel_value = float(temp);
//按照行将像素值一个个写入Mat中
DataMat.at<float>(i, j) = pixel_value;
}
}
cout << "读取Image数据完毕......" << endl;
}
file.close();
return DataMat;
}
Mat read_mnist_label(const string fileName) {
int magic_number;
int number_of_items;
Mat LabelMat;
ifstream file(fileName, ios::binary);
if (file.is_open())
{
cout << "成功打开标签集 ... " << endl;
file.read((char*)&magic_number, sizeof(magic_number));
file.read((char*)&number_of_items, sizeof(number_of_items));
magic_number = reverseInt(magic_number);
number_of_items = reverseInt(number_of_items);
cout << "幻数(文件格式):" << magic_number << " ;标签总数:" << number_of_items << endl;
cout << "开始读取Label数据......" << endl;
//CV_32SC1代表32位有符号整型 通道数为1
LabelMat = Mat::zeros(number_of_items, 1, CV_32SC1);
for (int i = 0; i < number_of_items; i++) {
unsigned char temp = 0;
file.read((char*)&temp, sizeof(temp));
LabelMat.at<unsigned int>(i, 0) = (unsigned int)temp;
}
cout << "读取Label数据完毕......" << endl;
}
file.close();
return LabelMat;
}
(5)自己的图片测试
读取之前储存的模型,处理测试图片,传入测试图片,输出测试结果,代码如下:
#include<iostream>
#include<opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//读取图片(28,28)
Mat image = cv::imread("test6.png", 0);
Mat img_show = image.clone();
resize(image, image, Size(28, 28));
//更换数据类型有uchar->float32
image.convertTo(image, CV_32F);
//归一化
image = image / 255.0;
//(1,784)
image = image.reshape(1, 1);
//加载svm模型
cv::Ptr<cv::ml::SVM> svm = cv::ml::StatModel::load<cv::ml::SVM>("mnist_svm.xml");
//预测图片
float ret = svm->predict(image);
cout << ret << endl;
cv::imshow("img", img_show);
cv::waitKey(0);
getchar();
return 0;
}
测试结果如下:
三、寒假任务装甲板识别部署
1、前言
本文学习中使用的是手写数字数据集,在装甲板上识别效果不是特别好,感兴趣的可以自行找装甲板数字字体的数据集训练测试。
本项目旨在以数字识别为连接点,使大家初步认识机器学习。
2、部署
下载群文件中我的训练模型“svm.xml”至装甲板识别项目文件夹。
装甲板识别项目中,在找出装甲板之后将单独装甲板图片剪裁出来进行SVM实现中的图片测试代码处理。
即改编上一部分代码,最终完成装甲板框选,并在框上标注装甲板颜色及数字。
祝大家新年快乐!
更多推荐
所有评论(0)