用C++写一个基于LeNet识别手写数字的程序并带注释
时间: 2024-03-05 14:49:32 浏览: 109
好的,这是一个基于LeNet的手写数字识别程序,使用C++编写,带详细注释。
首先,我们需要包含一些头文件,包括OpenCV和一些标准库文件。
```c++
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <cmath>
```
接下来,我们定义一些常量和全局变量。这里的常量和变量都是LeNet网络中使用到的,包括输入图像的大小、卷积核的大小、池化的大小、网络的层数、隐藏层的神经元数目等。
```c++
// define constants
const int WIDTH = 28; // 输入图像宽度
const int HEIGHT = 28; // 输入图像高度
const int CHANNELS = 1; // 输入图像通道数
const int KERNEL_SIZE = 5; // 卷积核大小
const int POOL_SIZE = 2; // 池化大小
const int NUM_HIDDEN = 128; // 隐藏层神经元数
const int NUM_LABELS = 10; // 标签数目
// 定义全局变量
cv::Mat inputImage; // 输入图像
cv::Mat outputImage; // 输出图像
std::vector<cv::Mat> kernels; // 卷积核
std::vector<cv::Mat> bias; // 偏置
std::vector<cv::Mat> convOutputs; // 卷积输出
std::vector<cv::Mat> poolOutputs; // 池化输出
cv::Mat hiddenWeights; // 隐藏层权重
cv::Mat hiddenBias; // 隐藏层偏置
cv::Mat outputWeights; // 输出层权重
cv::Mat outputBias; // 输出层偏置
```
接下来是定义函数的部分。我们需要定义一些辅助函数,包括读取数据集、对图像进行预处理、进行卷积操作、进行池化操作、进行激活函数操作、进行全连接操作等。
```c++
// 辅助函数:读取数据集
std::vector<std::pair<cv::Mat, int>> readDataset(const std::string& filename) {
std::vector<std::pair<cv::Mat, int>> dataset;
std::ifstream file(filename);
if (file.is_open()) {
std::string line;
while (std::getline(file, line)) {
std::replace(line.begin(), line.end(), ',', ' ');
std::stringstream ss(line);
std::vector<int> values;
int label;
for (int i = 0; i < WIDTH * HEIGHT; i++) {
int value;
ss >> value;
values.push_back(value);
}
ss >> label;
cv::Mat image = cv::Mat(WIDTH, HEIGHT, CV_8UC1, cv::Scalar(0));
for (int i = 0; i < WIDTH; i++) {
for (int j = 0; j < HEIGHT; j++) {
image.at<uchar>(i, j) = values[i*WIDTH+j];
}
}
dataset.push_back(std::make_pair(image, label));
}
file.close();
}
return dataset;
}
// 辅助函数:对图像进行预处理
cv::Mat preprocessImage(const cv::Mat& image) {
cv::Mat grayImage;
cv::cvtColor(image, grayImage, cv::COLOR_BGR2GRAY);
cv::Mat resizedImage;
cv::resize(grayImage, resizedImage, cv::Size(WIDTH, HEIGHT));
cv::Mat normalizedImage;
resizedImage.convertTo(normalizedImage, CV_32FC1);
normalizedImage /= 255.0f;
return normalizedImage;
}
// 辅助函数:进行卷积操作
void convolve(const cv::Mat& input, const cv::Mat& kernel, cv::Mat& output) {
cv::filter2D(input, output, -1, kernel, cv::Point(-1,-1), 0, cv::BORDER_DEFAULT);
}
// 辅助函数:进行池化操作
void pool(const cv::Mat& input, cv::Mat& output) {
cv::Size poolSize(POOL_SIZE, POOL_SIZE);
cv::Size strideSize(POOL_SIZE, POOL_SIZE);
cv::maxPool(input, output, poolSize, strideSize);
}
// 辅助函数:进行激活函数操作
void activate(const cv::Mat& input, cv::Mat& output) {
cv::Mat temp;
cv::exp(-input, temp);
output = 1.0 / (1.0 + temp);
}
// 辅助函数:进行全连接操作
void fullyConnected(const cv::Mat& input, const cv::Mat& weights, const cv::Mat& bias, cv::Mat& output) {
cv::gemm(input, weights, 1.0, bias, 1.0, output);
}
// 初始化函数:对卷积核、偏置、权重、偏置进行初始化
void init() {
// 初始化卷积核和偏置
kernels.push_back(cv::Mat(KERNEL_SIZE, KERNEL_SIZE, CV_32FC1, cv::Scalar(0.1)));
kernels.push_back(cv::Mat(KERNEL_SIZE, KERNEL_SIZE, CV_32FC1, cv::Scalar(0.2)));
kernels.push_back(cv::Mat(KERNEL_SIZE, KERNEL_SIZE, CV_32FC1, cv::Scalar(0.3)));
kernels.push_back(cv::Mat(KERNEL_SIZE, KERNEL_SIZE, CV_32FC1, cv::Scalar(0.4)));
bias.push_back(cv::Mat::ones(cv::Size(WIDTH-KERNEL_SIZE+1, HEIGHT-KERNEL_SIZE+1), CV_32FC1));
bias.push_back(cv::Mat::ones(cv::Size(WIDTH-KERNEL_SIZE+1, HEIGHT-KERNEL_SIZE+1), CV_32FC1));
bias.push_back(cv::Mat::ones(cv::Size(WIDTH-KERNEL_SIZE+1, HEIGHT-KERNEL_SIZE+1), CV_32FC1));
bias.push_back(cv::Mat::ones(cv::Size(WIDTH-KERNEL_SIZE+1, HEIGHT-KERNEL_SIZE+1), CV_32FC1));
// 初始化隐藏层权重和偏置
hiddenWeights = cv::Mat(NUM_HIDDEN, WIDTH*HEIGHT*3, CV_32FC1);
cv::randn(hiddenWeights, 0.0, 1.0);
hiddenBias = cv::Mat::ones(cv::Size(NUM_HIDDEN, 1), CV_32FC1);
// 初始化输出层权重和偏置
outputWeights = cv::Mat(NUM_LABELS, NUM_HIDDEN, CV_32FC1);
cv::randn(outputWeights, 0.0, 1.0);
outputBias = cv::Mat::ones(cv::Size(NUM_LABELS, 1), CV_32FC1);
}
```
接下来是主函数的部分,我们需要读入数据集、对数据集中的图像进行预处理、对图像进行卷积、池化、激活函数、全连接等一系列操作,最终输出预测的标签。
```c++
int main(int argc, char** argv) {
// 读取数据集
std::vector<std::pair<cv::Mat, int>> dataset = readDataset("mnist.csv");
// 初始化
init();
// 循环处理每个图像
for (int i = 0; i < dataset.size(); i++) {
// 读取图像
inputImage = dataset[i].first;
// 对图像进行卷积和池化操作
for (int j = 0; j < kernels.size(); j++) {
convolve(inputImage, kernels[j], convOutputs[j]);
convOutputs[j] += bias[j];
pool(convOutputs[j], poolOutputs[j]);
}
// 将池化后的图像转化为向量
cv::Mat flattenedImage = cv::Mat::zeros(cv::Size(1, WIDTH*HEIGHT*3), CV_32FC1);
int index = 0;
for (int c = 0; c < 3; c++) {
for (int x = 0; x < WIDTH/POOL_SIZE; x++) {
for (int y = 0; y < HEIGHT/POOL_SIZE; y++) {
flattenedImage.at<float>(index++, 0) = poolOutputs[c].at<float>(x, y);
}
}
}
// 对隐藏层进行操作
cv::Mat hiddenOutput;
fullyConnected(flattenedImage, hiddenWeights, hiddenBias, hiddenOutput);
activate(hiddenOutput, hiddenOutput);
// 对输出层进行操作
cv::Mat output;
fullyConnected(hiddenOutput, outputWeights, outputBias, output);
activate(output, output);
// 找到最大值的索引
cv::Point maxLoc;
cv::minMaxLoc(output, NULL, NULL, NULL, &maxLoc);
int predictedLabel = maxLoc.y;
// 输出预测的标签
std::cout << "Predicted label: " << predictedLabel << std::endl;
}
return 0;
}
```
这样,我们就完成了一个基于LeNet的手写数字识别程序,使用C++编写。
阅读全文