当前位置: 首页 > ai >正文

深度神经网络原理学习记录

本人不是AI领域从业者,了解这方面的知识只是出于好奇。

以下这个简单的深度神经网络代码是AI写的,可以帮助理解原理:

// 简单神经网络类
class SimpleNeuralNetwork {
public:// 构造函数:初始化网络参数SimpleNeuralNetwork(): w1(0.1), b1(0.1),   // 输入层到隐藏层的权重和偏置,初始化为0.1w2(0.1), b2(0.1),   // 隐藏层到输出层的权重和偏置,初始化为0.1learningRate(0.01) {} // 学习率设为0.01// 前向传播函数:计算给定输入x的网络输出double forward(double x) {// 隐藏层计算(没有使用sigmoid激活函数,所以是纯线性变换)hiddenOutput = w1 * x + b1;  // 公式: h = w1*x + b1// 输出层计算(也是线性变换)output = w2 * hiddenOutput + b2; // 公式: y^ = w2*h + b2return output; // 返回预测值}// 计算损失函数(均方误差)double loss(double predicted, double target) {// MSE公式: L = (y^ - y)^2return (predicted - target) * (predicted - target);}// 反向传播函数:计算梯度并更新参数void backward(double x, double predicted, double target) {// 计算预测误差double error = predicted - target; // e = y^ - y// 输出层梯度计算double dL_dw2 = error * hiddenOutput; // ∂L/∂w2 = e * hdouble dL_db2 = error;               // ∂L/∂b2 = e// 隐藏层梯度计算(链式法则)double dL_dhidden = error * w2;      // ∂L/∂h = e * w2double dL_dw1 = dL_dhidden * x;      // ∂L/∂w1 = (e * w2) * xdouble dL_db1 = dL_dhidden;          // ∂L/∂b1 = e * w2// 使用梯度下降更新参数(参数 = 参数 - 学习率 * 梯度)w2 -= learningRate * dL_dw2; // 更新w2b2 -= learningRate * dL_db2; // 更新b2w1 -= learningRate * dL_dw1; // 更新w1b1 -= learningRate * dL_db1; // 更新b1}// 训练一步:执行一次前向传播、损失计算和反向传播void trainStep(double x, double y) {// 1. 前向传播得到预测值double prediction = forward(x);// 2. 计算当前损失double lossValue = loss(prediction, y);// 3. 反向传播更新参数backward(x, prediction, y);// 打印训练信息(调试用)qDebug() << "输入 x =" << x<< "预测值 y^ =" << prediction<< "目标值 y =" << y<< "损失 Loss =" << lossValue;}private:// 网络参数double w1, b1;      // 输入层 -> 隐藏层的权重和偏置double w2, b2;      // 隐藏层 -> 输出层的权重和偏置double learningRate; // 学习率// 缓存前向传播的中间值(用于反向传播)double hiddenOutput; // 隐藏层的输出值double output;       // 网络的最终输出值
};int main(int argc, char *argv[])
{// 创建神经网络实例SimpleNeuralNetwork net;// 构造训练数据:y = 2x + 1(我们要让网络学习这个线性关系)std::vector<std::pair<double, double>> trainingData;for (double x = 0; x < 10; x += 1) {trainingData.push_back({x, 2 * x + 1}); // 生成(x, y)对}// 多轮训练(epoch指完整遍历数据集一次)for (int epoch = 0; epoch < 1000; ++epoch) {qDebug() << "\n【训练轮次】第" << epoch + 1 << "轮";// 遍历所有训练样本for (const auto& sample : trainingData) {// 对每个样本执行一次训练步骤net.trainStep(sample.first, sample.second);}}// 测试训练好的模型qDebug() << "\n【测试模型】";for (double x = 0; x < 5; ++x) {// 使用训练好的网络进行预测double prediction = net.forward(x);// 打印预测结果和真实值对比qDebug() << "输入 x =" << x<< "预测输出 y^ =" << prediction<< "真实输出 y =" << (2 * x + 1);}
}

 


前向传播(Forward Propagation)

输入数据通过网络层层计算,得到输出。就是从输入到输出的“推理”过程。

从forward函数可以看出这个神经网络具有:

  • 输入层:1 个神经元,接收输入 x
  • 隐藏层:1 个神经元,使用线性激活(即没有激活函数)
  • 输出层:1 个神经元,输出预测值 y^


损失函数(Loss Function)

衡量预测值与真实值之间的误差。

这里使用的是回归任务中常用的损失函数。


反向传播(Backward Propagation)

根据损失函数的梯度,反向调整网络参数。

 


实际上训练的结果是得到一组参数,这组参数带入到forward可以对输入内容做出预测。


以下这个版本是计算预测x^2的值, x^2函数图像是非线性的,要加上激活函数:

// Sigmoid 激活函数
double sigmoid(double x) {return 1.0 / (1.0 + exp(-x));
}// Sigmoid 导数(用于反向传播)
double sigmoidDerivative(double x) {double s = sigmoid(x);return s * (1 - s);
}// 改进的神经网络类(支持非线性任务)
class SimpleNeuralNetwork {
public:// 构造函数:初始化网络参数SimpleNeuralNetwork(): w1(0.1), b1(0.1),   // 输入层到隐藏层的权重和偏置w2(0.1), b2(0.1),   // 隐藏层到输出层的权重和偏置learningRate(0.1) {} // 学习率设为0.1(非线性任务需要更大的学习率)// 前向传播函数:计算给定输入x的网络输出(现在使用激活函数)double forward(double x) {// 隐藏层计算(使用sigmoid激活函数)hiddenInput = w1 * x + b1;  // 线性变换hiddenOutput = sigmoid(hiddenInput);  // 非线性激活// 输出层计算(线性变换,不加激活函数以便输出任意值)output = w2 * hiddenOutput + b2;return output; // 返回预测值}// 计算损失函数(均方误差)double loss(double predicted, double target) {return 0.5 * (predicted - target) * (predicted - target); // 乘以0.5方便求导}// 反向传播函数:计算梯度并更新参数(考虑激活函数)void backward(double x, double predicted, double target) {// 计算预测误差double error = predicted - target;// 输出层梯度计算double dL_dw2 = error * hiddenOutput; // ∂L/∂w2 = e * hdouble dL_db2 = error;               // ∂L/∂b2 = e// 隐藏层梯度计算(考虑sigmoid激活函数的导数)double dL_dhidden = error * w2;      // ∂L/∂h = e * w2// 乘以sigmoid的导数double dhidden_dz = sigmoidDerivative(hiddenInput); // ∂h/∂z = h*(1-h)double dL_dz = dL_dhidden * dhidden_dz; // ∂L/∂z = ∂L/∂h * ∂h/∂zdouble dL_dw1 = dL_dz * x;      // ∂L/∂w1 = ∂L/∂z * xdouble dL_db1 = dL_dz;          // ∂L/∂b1 = ∂L/∂z// 使用梯度下降更新参数w2 -= learningRate * dL_dw2;b2 -= learningRate * dL_db2;w1 -= learningRate * dL_dw1;b1 -= learningRate * dL_db1;}// 训练一步:执行一次前向传播、损失计算和反向传播void trainStep(double x, double y) {double prediction = forward(x);double lossValue = loss(prediction, y);backward(x, prediction, y);qDebug() << "输入 x =" << x<< "预测值 y^ =" << prediction<< "目标值 y =" << y<< "损失 Loss =" << lossValue;}void show() {qDebug() << "网络参数:";qDebug() << "w1 = " << w1;qDebug() << "b1 = " << b1;qDebug() << "w2 = " << w2;qDebug() << "b2 = " << b2;}private:// 网络参数double w1, b1;      // 输入层 -> 隐藏层的权重和偏置double w2, b2;      // 隐藏层 -> 输出层的权重和偏置double learningRate; // 学习率// 缓存前向传播的中间值(用于反向传播)double hiddenInput;  // 隐藏层的输入值(激活函数之前)double hiddenOutput; // 隐藏层的输出值(激活函数之后)double output;       // 网络的最终输出值
};int main(int argc, char *argv[]) {// 创建神经网络实例SimpleNeuralNetwork net;// 构造非线性训练数据:y = x^2(我们要让网络学习这个非线性关系)std::vector<std::pair<double, double>> trainingData;for (double x = -2.0; x <= 2.0; x += 0.2) {trainingData.push_back({x, x * x}); // 生成(x, x^2)对}// 多轮训练for (int epoch = 0; epoch < 1000; ++epoch) {qDebug() << "\n【训练轮次】第" << epoch + 1 << "轮";// 遍历所有训练样本for (const auto& sample : trainingData) {net.trainStep(sample.first, sample.second);}}// 测试训练好的模型qDebug() << "\n【测试模型】";for (double x = -2.0; x <= 2.0; x += 0.5) {double prediction = net.forward(x);qDebug() << "输入 x =" << x<< "预测输出 y^ =" << prediction<< "真实输出 y =" << (x * x)<< "误差 =" << (prediction - x * x);}net.show();
}


为什么激活函数使神经网络能够学习非线性关系

在没有激活函数时,神经网络只是多个线性变换的组合:

输出 = W₂(W₁X + b₁) + b₂ = (W₂W₁)X + (W₂b₁ + b₂)

这可以简化为:

输出 = W'X + b'

无论你堆叠多少层线性变换,最终结果仍然是一个线性变换。这意味着:

  • 网络只能学习线性关系
  • 无法解决非线性问题
  • 深度网络不会比单层网络更强大

当在层与层之间加入非线性激活函数(如sigmoid、ReLU等)时:

隐藏层输出 = σ(W₁X + b₁)  // σ表示激活函数

最终输出 = W₂(隐藏层输出) + b₂

现在网络不再是简单的线性组合,因为激活函数引入了非线性。这使得网络可以表示非线性函数:每个激活函数就像一个"弯曲器",将线性变换的结果进行非线性扭曲。通过组合多个这样的非线性变换,网络可以逼近任意复杂的非线性函数。

直观理解,想象要用乐高积木拼出一个圆形:

  • 只有直线积木(无激活函数):只能拼出多边形,永远无法接近圆形
  • 有弯曲积木(激活函数):可以用许多小弯曲积木拼出接近完美的圆形

激活函数就是提供了这些"弯曲积木",使网络能够构造复杂的非线性形状。

http://www.xdnf.cn/news/15797.html

相关文章:

  • 微服务雪崩防护最佳实践之sentinel
  • Django ORM系统
  • SearchService 该类只运行在数据节点
  • 【文件IO】认识文件描述符和内核缓冲区
  • SSH开启Socks5服务
  • C++ STL容器
  • 金融大前端中的 AI 应用:智能投资顾问与风险评估
  • 【Nature Communications】GaN外延层中位错辅助的电子和空穴输运
  • 0401聚类-机器学习-人工智能
  • nvm、npm、pnpm、cnpm、yarn
  • 《深入C++多态机制:从虚函数表到运行时类型识别》​
  • 数据并表技术全面指南:从基础JOIN到分布式数据融合
  • Spring Boot 自动装配用法
  • Materials Studio学习笔记(二十九)——尿素的几何优化
  • 树同构(Tree Isomorphism)
  • [特殊字符] 小程序 vs 智能体:下一代应用开发,谁主沉浮?
  • 【Java项目安全基石】登录认证实战:Session/Token/JWT用户校验机制深度解析
  • 基于自定义数据集微调SigLIP2-分类任务
  • PDF 编辑器:多文件合并 拆分 旋转 顺序随便调 加水印 密码锁 页码背景
  • [学习] 深入理解傅里叶变换:从时域到频域的桥梁
  • vscode环境下c++的常用快捷键和插件
  • 嵌入式通信DQ单总线协议及UART(一)
  • Linux练习二
  • 鸿蒙蓝牙通信
  • [AI风堇]基于ChatGPT3.5+科大讯飞录音转文字API+GPT-SOVITS的模拟情感实时语音对话项目
  • 字节跳动开源Seed-X 7B多语言翻译模型:28语种全覆盖,性能超越GPT-4、Gemini-2.5与Claude-3.5
  • 关于Vuex
  • GeoPandas 城市规划:Python 空间数据初学者指南
  • 零基础 “入坑” Java--- 十二、抽象类和接口
  • ndexedDB 与 LocalStorage:全面对比分析