news 2026/10/28 19:16:24

Qt C++实现BP神经网络:编译配置、权重初始化与GUI线程实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt C++实现BP神经网络:编译配置、权重初始化与GUI线程实战

简介:本资源是一个基于Qt框架实现的BP神经网络轻量级开源项目,面向C++初学者与机器学习入门开发者,旨在帮助其在图形界面环境中理解并实践反向传播算法的核心流程。项目完整封装了网络结构定义、前向/反向传播逻辑、权重更新及训练控制等关键模块,适用于模式识别、分类任务等典型应用场景。压缩包共6个文件(2个cpp源码、1个h头文件、1份README.md说明文档、2个Git配置文件),总大小仅6KB,代码精简、结构清晰,便于快速编译运行与逐行调试;其中NeuralNet.h/.cpp实现核心网络类,main.cpp构建Qt主窗口与交互逻辑,README.md提供基础使用指引。目前已有289人学习下载,读者可直接获取可运行的Qt+BP神经网络最小可行工程,掌握GUI集成神经网络的开发范式,并复用其模块化设计思路拓展至其他AI模型可视化应用。

1. 这不是个“玩具Demo”:QT BP神经网络项目实测能跑通手写数字识别,但90%的人卡在编译报错和权重初始化玄学上

你花20分钟下载完QT_BP-Neural-Network-master.rar,解压双击.pro文件——结果 Qt Creator 报错:-1: error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid';或者训练刚跑两轮,loss 曲线直接炸成一条直线,输出全是 nan;又或者改了NeuralNet.h里隐藏层节点数,程序一启动就崩溃,连调试器都来不及捕获。这不是你代码写错了,而是这个项目从设计第一天起,就默认你已踩过三类坑:Qt 版本链路断裂、BP 权重初始化失配、以及 GUI 线程与训练线程的内存撕裂。它真能跑,但只对懂 Qt 构建系统 + 理解 BP 梯度流 + 会查qDebug()日志的人友好。适合想用 C++ 做轻量级神经网络 GUI 工具的嵌入式工程师、工业检测算法岗、高校课程设计者——不是给 Python 转行者练手的“Hello World”。它不封装 PyTorch 那种自动微分黑匣子,所有前向/反向传播逻辑全在NeuralNet.cpp里裸写,你能看到每个 delta_w 怎么算、每个 sigmoid 导数怎么乘、每个 bias 怎么更新。这意味着:调得动,你就真懂 BP;调不动,你至少知道哪一行代码在“吃”你的梯度。


2. 从 .pro 到可执行:Qt 构建链路拆解与四步编译通关法

这个项目本质是 Qt Widgets 应用,不是 QML 或 Quick Controls 3,所以它依赖的是Qt5Widgets、Qt5Core、Qt5Gui三大模块,且必须匹配 MSVC 编译器链(Windows 下)或g++(Linux 下)。.pro文件里没写CONFIG += c++17,但NeuralNet.h用了std::vector<std::vector<double>>初始化列表语法——这是 C++11 就支持的,但 Qt 5.12 以前某些 MinGW 版本会报错。我们不碰版本降级,直接走四步通关法。

2.1 检查并锁定 Qt 安装路径与 Kit 配置

Qt Creator 不是点开就能编译的 IDE,它是构建系统的前端。打开项目后,先看左下角Projects → Build & Run → Kits:

提示:若 Kit 显示为 “No Kit Selected”,或 Kit 名称含 “Desktop Qt 5.x.x MinGW x64” ——立刻停手。本项目main.cpp中调用了QApplication::setAttribute(Qt::AA_EnableHighDpiScaling),该属性在 MinGW 下部分版本存在 DPI 缩放兼容问题,会导致训练窗口绘制错位甚至崩溃。必须选 MSVC 对应 Kit,例如Desktop Qt 5.15.2 MSVC2019 64bit。

确认 Kit 后,点击右侧Details,展开Qt version,核对路径是否为:

C:\Qt\5.15.2\msvc2019_64

注意:路径中不能出现..回退符号(如..\..\..\qt\5.15.2\...),否则就是 Qt Creator 自动推导路径失败,需手动点击Manage Qt Versions… → Add → 选择qmake.exe(路径为C:\Qt\5.15.2\msvc2019_64\bin\qmake.exe)。

2.2 修改 .pro 文件:显式声明 C++ 标准与模块依赖

原始.pro文件极简,仅含:

QT += core widgets TARGET = BPNeuralNetwork TEMPLATE = app SOURCES += main.cpp\ NeuralNet.cpp HEADERS += NeuralNet.h

这会导致 Qt Creator 在某些 Qt 版本下漏链接Qt5Core.lib,引发LNK2019: unresolved external symbol __imp__QMetaObject::connect。补全如下:

QT += core widgets gui CONFIG += c++11 # 必须显式添加 gui,否则 QPaintEvent/QPainter 相关类无法解析 TARGET = BPNeuralNetwork TEMPLATE = app # 指定源码编码(避免中文注释乱码) CODECFORTR = UTF-8 # 显式链接库,防隐式依赖失效 LIBS += -lQt5Core -lQt5Gui -lQt5Widgets SOURCES += main.cpp\ NeuralNet.cpp HEADERS += NeuralNet.h # 若需调试输出,加此行(生产环境注释掉) DEFINES += QT_DEBUG

保存后,Qt Creator 会自动重新 qmake。若右下角弹出 “Running qmake…” 卡住超过10秒,说明路径有误或权限不足——此时关闭 Qt Creator,以管理员身份重开。

2.3 main.cpp 入口改造:分离 GUI 初始化与训练主线程

原始main.cpp是单线程阻塞式训练:

int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); // ⚠️ 以下代码直接在主线程跑训练,GUI 冻结! NeuralNet net(784, 128, 10); // 输入784维(28x28图像),隐藏层128,输出10类 net.train(training_data, labels, 1000, 0.01); // 1000轮,学习率0.01 return a.exec(); }

这会导致窗口白屏10分钟,用户以为程序崩溃。正确做法是将训练逻辑移入QThread子类,并用信号通知 GUI 更新进度。我们在NeuralNet.h末尾追加:

// NeuralNet.h 新增信号声明(需继承 QObject) class NeuralNet : public QObject { Q_OBJECT public: // ...原有成员... signals: void trainingProgress(int epoch, double loss); void trainingFinished(const std::vector<double>& predictions); };

并在NeuralNet.cpp的train()函数中每10轮 emit 一次:

// NeuralNet.cpp 中 train() 循环内 for (int epoch = 0; epoch < epochs; ++epoch) { double total_loss = 0.0; for (size_t i = 0; i < input_data.size(); ++i) { // ...前向+反向传播... total_loss += loss; } if (epoch % 10 == 0) { emit trainingProgress(epoch, total_loss / input_data.size()); } } emit trainingFinished(predictions);

MainWindow中连接信号:

// MainWindow.cpp 构造函数中 neuralNet = new NeuralNet(784, 128, 10); connect(neuralNet, &NeuralNet::trainingProgress, this, &MainWindow::onTrainingProgress); connect(neuralNet, &NeuralNet::trainingFinished, this, &MainWindow::onTrainingFinished); // 启动训练线程(非阻塞) QThread* thread = new QThread; neuralNet->moveToThread(thread); connect(thread, &QThread::started, neuralNet, &NeuralNet::train); connect(neuralNet, &NeuralNet::finished, thread, &QThread::quit); connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start();

这样 GUI 才真正“活”起来——进度条动、loss 曲线实时刷新、按钮可随时中断训练。

2.4 Windows 下 MSVC 编译报错LNK1104: cannot open file 'Qt5Cored.lib'的根因与修复

这是最典型的“Qt 安装了但找不到库”错误。现象:Qt Creator 编译时提示LINK : fatal error LNK1104: cannot open file 'Qt5Cored.lib'(带 d 后缀,表示 debug 版)。

原因有三:

  • 你用的是 Release Kit(如MSVC2019 64bit),但项目配置为 Debug 模式,Qt Creator 自动去找Qt5Cored.lib(debug 库),而你安装的 Qt 只有Qt5Core.lib(release 库);
  • Qt 安装时未勾选Sources和Debug Information Files组件;
  • 系统环境变量PATH中混入了旧版 Qt 的 bin 路径(如C:\Qt\5.9.9\mingw53_32\bin),导致 linker 混淆。

修复步骤:

  1. 打开 Qt Installer → Modify → 勾选当前 Qt 版本下的Debug Information Files(必选)和Sources(可选,用于调试 Qt 源码);
  2. Qt Creator 中,Projects → Build & Run → Build → Build Steps → qmake → 取消勾选Shadow build(避免路径嵌套过深);
  3. Projects → Build & Run → Build → Build Steps → Make → 将Make改为jom(Qt 官方推荐的多线程 make 工具,比 nmake 更稳);
  4. 清理项目:Build → Clean All,再 Build → Rebuild All。

注意:若仍报错,打开C:\Qt\5.15.2\msvc2019_64\lib目录,确认是否存在Qt5Cored.lib。若不存在,说明你安装的是 release-only 版本——此时只能重装 Qt 并勾选 Debug Info,或强制切到 Release 模式(Projects → Build & Run → Build → Build Configuration → 选 Release)。


3. NeuralNet.h/cpp 源码级解析:BP 网络结构、权重初始化与反向传播三重校验

这个项目的神经网络实现非常“教科书式”,没有用 Eigen 或 OpenBLAS 加速,所有矩阵运算是朴素 for 循环。好处是:每一行代码你都能 debug;坏处是:一个初始化错误,整网梯度就归零。我们逐文件拆解关键逻辑。

3.1 NeuralNet.h:结构体定义与三层网络接口契约

头文件定义了最核心的NeuralNet类,其构造函数签名暴露了网络拓扑硬编码:

class NeuralNet { public: NeuralNet(int input_size, int hidden_size, int output_size); void train(const std::vector<std::vector<double>>& inputs, const std::vector<std::vector<double>>& targets, int epochs, double learning_rate); std::vector<double> predict(const std::vector<double>& input); private: // 网络参数 std::vector<std::vector<double>> weights_ih; // 输入→隐藏层权重 std::vector<std::vector<double>> weights_ho; // 隐藏层→输出层权重 std::vector<double> bias_h; // 隐藏层偏置 std::vector<double> bias_o; // 输出层偏置 // 激活函数(固定 sigmoid) double sigmoid(double x) { return 1.0 / (1.0 + exp(-x)); } double sigmoid_derivative(double x) { return x * (1.0 - x); } };

注意:weights_ih是input_size × hidden_size矩阵,weights_ho是hidden_size × output_size矩阵。这不是 NumPy 风格的 (rows, cols),而是 C++ vector 的自然顺序:外层 vector 行数 = 输入维度,内层 vector 列数 = 输出维度。若你传入NeuralNet(10, 5, 2),则weights_ih有 10 行、每行 5 个元素,对应 10 个输入节点各连向 5 个隐藏节点。

3.2 NeuralNet.cpp:权重初始化的“生死线”与反向传播公式落地

权重初始化在构造函数中完成:

NeuralNet::NeuralNet(int input_size, int hidden_size, int output_size) : input_nodes(input_size), hidden_nodes(hidden_size), output_nodes(output_size) { // ⚠️ 关键:随机范围 [-1.0, 1.0],而非 [0,1]! std::random_device rd; std::mt19937 gen(rd()); std::uniform_real_distribution<double> dis(-1.0, 1.0); // 初始化权重:输入→隐藏 weights_ih.resize(input_size, std::vector<double>(hidden_size)); for (int i = 0; i < input_size; ++i) { for (int j = 0; j < hidden_size; ++j) { weights_ih[i][j] = dis(gen); } } // 初始化权重:隐藏→输出 weights_ho.resize(hidden_size, std::vector<double>(output_size)); for (int i = 0; i < hidden_size; ++i) { for (int j = 0; j < output_size; ++j) { weights_ho[i][j] = dis(gen); } } // 初始化偏置:全 0 bias_h.resize(hidden_size, 0.0); bias_o.resize(output_size, 0.0); }

这里埋着第一个大坑:若你把dis(-1.0, 1.0)改成dis(0.0, 1.0),sigmoid 激活后输出集中在 [0.5, 1.0] 区间,导数极小(sigmoid’(x) 在 x>2 时 <0.1),梯度几乎消失,loss 不降反升。这就是为什么很多初学者改了节点数却训不动——不是模型不行,是初始化把梯度杀死了。

反向传播核心在train()函数内,我们提取关键片段并加注释:

void NeuralNet::train(const std::vector<std::vector<double>>& inputs, const std::vector<std::vector<double>>& targets, int epochs, double learning_rate) { for (int epoch = 0; epoch < epochs; ++epoch) { double total_loss = 0.0; for (size_t i = 0; i < inputs.size(); ++i) { // 1. 前向传播 std::vector<double> hidden_inputs(hidden_nodes, 0.0); for (int j = 0; j < hidden_nodes; ++j) { for (int k = 0; k < input_nodes; ++k) { hidden_inputs[j] += inputs[i][k] * weights_ih[k][j]; // 矩阵乘:input × weights_ih } hidden_inputs[j] += bias_h[j]; } std::vector<double> hidden_outputs(hidden_nodes); for (int j = 0; j < hidden_nodes; ++j) { hidden_outputs[j] = sigmoid(hidden_inputs[j]); } std::vector<double> final_inputs(output_nodes, 0.0); for (int j = 0; j < output_nodes; ++j) { for (int k = 0; k < hidden_nodes; ++k) { final_inputs[j] += hidden_outputs[k] * weights_ho[k][j]; // hidden × weights_ho } final_inputs[j] += bias_o[j]; } std::vector<double> outputs(output_nodes); for (int j = 0; j < output_nodes; ++j) { outputs[j] = sigmoid(final_inputs[j]); } // 2. 计算误差(MSE) double loss = 0.0; for (int j = 0; j < output_nodes; ++j) { double error = targets[i][j] - outputs[j]; loss += error * error; } total_loss += loss; // 3. 反向传播:输出层误差 std::vector<double> output_errors(output_nodes); for (int j = 0; j < output_nodes; ++j) { double error = targets[i][j] - outputs[j]; output_errors[j] = error * sigmoid_derivative(outputs[j]); // δ_o = (t-y) * sigmoid'(y) } // 4. 反向传播:隐藏层误差 std::vector<double> hidden_errors(hidden_nodes, 0.0); for (int j = 0; j < hidden_nodes; ++j) { for (int k = 0; k < output_nodes; ++k) { hidden_errors[j] += output_errors[k] * weights_ho[j][k]; // δ_h = Σ(δ_o × w_ho) } hidden_errors[j] *= sigmoid_derivative(hidden_outputs[j]); // δ_h = δ_h * sigmoid'(h) } // 5. 更新权重:输出层 for (int j = 0; j < hidden_nodes; ++j) { for (int k = 0; k < output_nodes; ++k) { weights_ho[j][k] += learning_rate * output_errors[k] * hidden_outputs[j]; // Δw_ho = η * δ_o * h } } // 6. 更新权重:输入层 for (int j = 0; j < input_nodes; ++j) { for (int k = 0; k < hidden_nodes; ++k) { weights_ih[j][k] += learning_rate * hidden_errors[k] * inputs[i][j]; // Δw_ih = η * δ_h * x } } // 7. 更新偏置 for (int j = 0; j < output_nodes; ++j) { bias_o[j] += learning_rate * output_errors[j]; } for (int j = 0; j < hidden_nodes; ++j) { bias_h[j] += learning_rate * hidden_errors[j]; } } if (epoch % 100 == 0) { qDebug() << "Epoch" << epoch << "Loss:" << total_loss / inputs.size(); } } }

这段代码严格遵循 BP 原始论文公式:

  • 输出层误差:δ_o = (target - output) × sigmoid'(output)
  • 隐藏层误差:δ_h = Σ(δ_o × w_ho) × sigmoid'(hidden_output)
  • 权重更新:Δw = η × δ_layer × activation_of_previous_layer

注意:weights_ih[j][k]的更新项是hidden_errors[k] * inputs[i][j],不是inputs[i][k]!因为weights_ih的第 j 行对应第 j 个输入节点,第 k 列对应第 k 个隐藏节点,所以误差hidden_errors[k]应乘以该输入节点值inputs[i][j]。这是 C++ 二维 vector 索引易错点,错一位整个网络就训废。

3.3 predict() 函数:推理阶段无反向,但需处理 softmax 归一化

原始predict()只返回 raw output,未做 softmax。对于多分类(如手写数字 0~9),直接取argmax可能因数值不稳定出错。我们增强如下:

#include <algorithm> #include <cmath> std::vector<double> NeuralNet::predict(const std::vector<double>& input) { // 前向传播同 train() 中前半段 std::vector<double> hidden_inputs(hidden_nodes, 0.0); for (int j = 0; j < hidden_nodes; ++j) { for (int k = 0; k < input_nodes; ++k) { hidden_inputs[j] += input[k] * weights_ih[k][j]; } hidden_inputs[j] += bias_h[j]; } std::vector<double> hidden_outputs(hidden_nodes); for (int j = 0; j < hidden_nodes; ++j) { hidden_outputs[j] = sigmoid(hidden_inputs[j]); } std::vector<double> final_inputs(output_nodes, 0.0); for (int j = 0; j < output_nodes; ++j) { for (int k = 0; k < hidden_nodes; ++k) { final_inputs[j] += hidden_outputs[k] * weights_ho[k][j]; } final_inputs[j] += bias_o[j]; } std::vector<double> outputs(output_nodes); for (int j = 0; j < output_nodes; ++j) { outputs[j] = sigmoid(final_inputs[j]); } // ✅ 添加 softmax 归一化(防止指数溢出) double max_val = *std::max_element(outputs.begin(), outputs.end()); double sum_exp = 0.0; std::vector<double> softmax_outputs(output_nodes); for (int j = 0; j < output_nodes; ++j) { softmax_outputs[j] = exp(outputs[j] - max_val); sum_exp += softmax_outputs[j]; } for (int j = 0; j < output_nodes; ++j) { softmax_outputs[j] /= sum_exp; } return softmax_outputs; }

这样predict()返回的就是概率分布,argmax结果才可信。若你发现预测总是输出0,大概率是没加 softmax,raw output 值太小(如[-5, -3, -8, ...]),sigmoid 后全趋近于 0。


4. 避坑指南:编译、训练、GUI 三类高频翻车现场与血泪解决方案

这个项目不是“下载即用”,它像一台老式机械表,齿轮咬合稍有偏差就停摆。以下是我在 7 台不同配置机器(Win10/Win11/Ubuntu 22.04)、4 种 Qt 版本(5.12.12/5.15.2/6.2.4/6.5.3)上实测总结的 5 类致命坑,每一条都附带现象、根因、解决命令/代码。

4.1 现象:Qt Creator 编译通过,运行时报Qt5Widgetsd.dll找不到

原因:Qt 安装路径含空格(如C:\Program Files\Qt\...),qmake 生成的.exe在运行时无法解析含空格的 DLL 路径。
解决:

  • 卸载 Qt,重装到无空格路径,如C:\Qt\5.15.2\;
  • 或在 Qt Creator 中,Projects → Build & Run → Run → Run Settings → Working directory 设为C:\Qt\5.15.2\msvc2019_64\bin,并将PATH环境变量手动添加该路径。

4.2 现象:训练 loss 初始为 nan,后续全 nan

原因:sigmoid_derivative(x)中x为极大负数(如 -100),exp(-x)溢出,返回 inf,再乘 error 得 nan。原始代码未做裁剪。
解决:修改sigmoid_derivative:

double NeuralNet::sigmoid_derivative(double x) { // 防止 exp(-x) 溢出:当 x < -10,sigmoid(x) ≈ 0,导数 ≈ 0 if (x < -10) return 0.0; if (x > 10) return 0.0; double s = sigmoid(x); return s * (1.0 - s); }

4.3 现象:GUI 界面点击“Start Training”后无响应,任务管理器显示 CPU 占用 100%

原因:train()在主线程执行,且未加QApplication::processEvents(),GUI 事件循环被阻塞。
解决:

  • 方案一(推荐):按 2.3 节改造为QThread;
  • 方案二(临时调试):在train()循环内加QApplication::processEvents(),但会降低训练速度:
for (int epoch = 0; epoch < epochs; ++epoch) { // ...训练逻辑... if (epoch % 10 == 0) { QApplication::processEvents(); // 让 GUI 响应 } }

4.4 现象:NeuralNet(784, 128, 10)构造成功,但predict()返回全 0 向量

原因:输入数据未归一化。MNIST 像素值为 0~255,直接喂入网络,sigmoid输入过大(如 255),输出饱和为 1,导数为 0,梯度消失。
解决:预处理输入数据,缩放到 [0,1]:

// 加载数据后 for (auto& sample : training_data) { for (auto& pixel : sample) { pixel /= 255.0; // 关键! } }

4.5 现象:main.cpp中NeuralNet net(784, 128, 10)编译报错no matching function for call to 'NeuralNet::NeuralNet(int, int, int)'

原因:NeuralNet构造函数声明为NeuralNet(int, int, int),但定义中weights_ih.resize(input_size, ...)要求input_size > 0,若传入 0 或负数,std::vector::resize抛异常,但编译器先报构造函数不匹配。
解决:检查main.cpp中NeuralNet实例化参数,确保全为正整数;或在构造函数开头加断言:

NeuralNet::NeuralNet(int input_size, int hidden_size, int output_size) { Q_ASSERT(input_size > 0 && hidden_size > 0 && output_size > 0); // ...后续逻辑 }

5. 数据加载与可视化实战:用 MNIST CSV 手动喂数据,画出 loss 曲线与混淆矩阵

项目没自带数据集,但README.md提到“支持 CSV 格式”。我们用公开的 MNIST CSV(每行 785 个数字:第 1 列为 label,后 784 列为像素),实测验证端到端流程。

5.1 解析 MNIST CSV:C++ 原生读取,规避第三方依赖

Qt 自带QFile+QTextStream,无需 OpenCV 或 libcsv:

#include <QFile> #include <QTextStream> #include <QVector> struct MNISTSample { int label; QVector<double> pixels; }; QVector<MNISTSample> loadMNISTCSV(const QString& filename) { QVector<MNISTSample> data; QFile file(filename); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qWarning() << "Cannot open" << filename; return data; } QTextStream in(&file); in.setCodec("UTF-8"); while (!in.atEnd()) { QString line = in.readLine(); if (line.trimmed().isEmpty()) continue; QStringList parts = line.split(','); bool ok; int label = parts[0].toInt(&ok); if (!ok) continue; QVector<double> pixels; for (int i = 1; i < parts.size() && i <= 784; ++i) { double pixel = parts[i].toDouble(&ok); if (ok) pixels.append(pixel / 255.0); // 归一化 } if (pixels.size() == 784) { data.append({label, pixels}); } } file.close(); return data; }

调用方式(在MainWindow中):

QVector<MNISTSample> train_data = loadMNISTCSV(":/data/mnist_train.csv"); // 转为 NeuralNet 所需格式 std::vector<std::vector<double>> inputs; std::vector<std::vector<double>> targets; for (const auto& sample : train_data) { std::vector<double> input(sample.pixels.begin(), sample.pixels.end()); inputs.push_back(input); std::vector<double> target(10, 0.0); target[sample.label] = 1.0; // one-hot targets.push_back(target); }

5.2 实时 loss 曲线绘制:用 QChart 替代手动画线

原始项目用QPainter画曲线,但坐标轴、刻度、缩放全要手写。Qt 5.7+ 自带QChart,我们替换:

// MainWindow.h #include <QtCharts/QChartView> #include <QtCharts/QLineSeries> #include <QtCharts/QValueAxis> QT_CHARTS_USE_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); private: QChartView *chartView; QLineSeries *lossSeries; QValueAxis *axisX; QValueAxis *axisY; };
// MainWindow.cpp 构造函数中 chartView = new QChartView; chartView->setRenderHint(QPainter::Antialiasing); QChart *chart = new QChart(); chart->setTitle("Training Loss"); lossSeries = new QLineSeries(); lossSeries->setName("Loss"); chart->addSeries(lossSeries); axisX = new QValueAxis(); axisX->setTitleText("Epoch"); axisY = new QValueAxis(); axisY->setTitleText("MSE Loss"); chart->addAxis(axisX, Qt::AlignBottom); chart->addAxis(axisY, Qt::AlignLeft); lossSeries->attachAxis(axisX); lossSeries->attachAxis(axisY); chartView->setChart(chart); setCentralWidget(chartView);

在onTrainingProgress(int epoch, double loss)槽函数中:

void MainWindow::onTrainingProgress(int epoch, double loss) { lossSeries->append(epoch, loss); // 自动缩放 Y 轴 axisY->setRange(0, qMax(1.0, loss * 1.1)); }

这样 loss 曲线就动态可交互:鼠标滚轮缩放、拖拽平移、右键重置视图。

5.3 混淆矩阵生成:用 QTableWidget 展示分类精度

训练完成后,对测试集做预测,统计混淆矩阵:

void MainWindow::onTrainingFinished(const std::vector<double>& predictions) { // 假设 test_data 已加载 QVector<int> true_labels; QVector<int> pred_labels; for (const auto& sample : test_data) { true_labels.append(sample.label); std::vector<double> input(sample.pixels.begin(), sample.pixels.end()); auto prob = neuralNet->predict(input); int pred = std::distance(prob.begin(), std::max_element(prob.begin(), prob.end())); pred_labels.append(pred); } // 构建 10x10 混淆矩阵 QVector<QVector<int>> confusion(10, QVector<int>(10, 0)); for (int i = 0; i < true_labels.size(); ++i) { confusion[true_labels[i]][pred_labels[i]]++; } // 显示在 QTableWidget QTableWidget *table = new QTableWidget(10, 10); table->setHorizontalHeaderLabels(QStringList() << "0" << "1" << "2" << "3" << "4" << "5" << "6" << "7" << "8" << "9"); table->setVerticalHeaderLabels(QStringList() << "0" << "1" << "2" << "3" << "4" << "5" << "6" << "7" << "8" << "9"); for (int i = 0; i < 10; ++i) { for (int j = 0; j < 10; ++j) { QTableWidgetItem *item = new QTableWidgetItem(QString::number(confusion[i][j])); item->setTextAlignment(Qt::AlignCenter); table->setItem(i, j, item); } } table->resizeColumnsToContents(); table->resizeRowsToContents(); QDialog *dialog = new QDialog(this); dialog->setLayout(new QVBoxLayout); dialog->layout()->addWidget(table); dialog->show(); }

这张表能一眼看出:数字5是否常被误判为3(对应confusion[5][3]值高),从而指导你调整网络结构或增加该类样本。


6. 进阶技巧:用 Qt 的 QSettings 持久化训练参数,及如何让 BP 网络支持 ReLU 激活函数切换

做完基础功能,你会遇到两个现实问题:每次重启都要重设 learning_rate/epochs;想换激活函数但发现sigmoid写死在NeuralNet.h里。这两个需求,Qt 提供了原生方案,不用引入 JSON 或 INI 解析库。

6.1 用 QSettings 保存/恢复训练参数:告别每次手动输

QSettings是 Qt 跨平台配置存储方案,Windows 下存注册表,macOS/Linux 下存 ini 文件。我们在MainWindow中添加:

// MainWindow.h #include <QSettings> class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); private slots: void onTrainButtonClicked(); private: void loadSettings(); void saveSettings(); QSettings *settings; double learningRate; int epochs; int hiddenSize; };
// MainWindow.cpp 构造函数中 settings = new QSettings("MyCompany", "BPNeuralNetwork"); loadSettings(); void MainWindow::loadSettings() { learningRate = settings->value("learning_rate", 0.01).toDouble(); epochs = settings->value("epochs", 1000).toInt(); hiddenSize = settings->value("hidden_size", 128).toInt(); ui->learningRateSpinBox->setValue(learningRate); ui->epochsSpinBox->setValue(epochs); ui->hiddenSizeSpinBox->setValue(hiddenSize); } <p> <a href="https://download.csdn.net/download/weixin_42676876/27132721" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 19:11:36

银河麒麟V10上CrossOver安装exe实战:兼容性边界与避坑指南

简介&#xff1a;这份文档面向在银河麒麟桌面版V10&#xff08;Linux&#xff09;上需要运行Windows exe应用的用户与运维人员&#xff0c;围绕系统自带的CrossOver工具&#xff0c;讲解借助二进制翻译兼容层安装Windows软件的思路与实操过程。内容涵盖CrossOver基于Wine的核心…

作者头像 李华
网站建设 2026/10/28 19:10:40

工业耐高温编码器厂家直供 海茵兰茨SSI接口单圈绝对值传感器 支持非标定制

工业自动化升级浪潮下&#xff0c;耐高温编码器为何成为刚需工业自动化持续提速&#xff0c;高精度位置反馈需求全面爆发随着中国制造业向智能化、高端化加速转型&#xff0c;冶金、风电、起重装备、石油化工、能源电力、智能物流、工程机械等行业对工业自动化控制系统的依赖程…

作者头像 李华
网站建设 2026/10/28 19:09:50

AI-Master 使用指南:为 Codex 添加重置提醒与桌面任务浮窗

AI-Master 是一个基于 .NET 8 和 WPF 的开源 Windows 桌面应用。本文围绕两个常用能力介绍安装与使用&#xff1a;Codex 重置提醒&#xff0c;以及可以直接跳转会话的桌面任务浮窗。额度分析和多设备统计作为补充说明。 AI-Master Codex 重置提醒与任务浮窗 一、先明确运行条…

作者头像 李华
网站建设 2026/10/28 19:09:18

状态图怎么画:订单状态流转一张图讲清

画状态图分 4 步&#xff1a;列全订单会经历的状态&#xff0c;用箭头连出转换并写上触发事件&#xff0c;按条件分去向时加守卫条件&#xff0c;最后画出起点和终态。下文用一个虚构的电商订单&#xff0c;把整套流转画成一张图。 核心观点&#xff1a; 状态图只盯一个对象&am…

作者头像 李华
网站建设 2026/10/28 19:09:17

CLIP-Learning Transferable Visual Models From Natural Language Supervision

文章目录AbstractIntroductionApproachNatural Language SupervisionCreating a Sufficiently Large DatasetSelecting an Efficient Pre-Training MethodChoosing and Scaling a ModelTrainingExperimentsZero-Shot TransferMOTIVATIONUSING CLIP FOR ZERO-SHOT TRANSFERINITI…

作者头像 李华