如何使用梯度下降法解决CNN问题

1.前言

有兴趣可以进一步交流, github Link

邮件: csnever163@gmail.com, csnever@163.com

1.1 本次分享面向的人群

  • 纯喜欢听工作内容无关的分享
  • 对机器学习感兴趣的小伙伴
  • 对于图像识别感兴趣的小伙伴
  • 对深度学习感兴趣的小伙伴
  • 想进一步了解深度学习原理的小伙伴
  • 喜欢数学推理的小伙伴

1.2 内容结构和知识难度

章节 主要内容点 分享目标 难度系数 收听建议
第二章 梯度下降法 推理介绍 介绍一下离散梯度、损失函数 以及 如何解决线性预测问题的。 3颗星 尽量跟上节奏,属于高等代数的基础知识。
第三章 CNN简介 介绍一下卷积和神经网络等数学概念,简单普及一下常见的图像学计算方法 4颗星 跟不上就不要勉强自己,看看图片和host的口才表演
第四章 梯度下降法 和 反向传播算法 CNN的核心数学原理,无论模型多么复杂,都是通过这个理论基础生成对应的数学计算过程 5颗星 能听懂和吸收就具备初级模型调参理论基础了。
第五章 VGG16 分享一个经典网络结构。 深度模型本质是堆个几十几百层,但核心难点是为什么要这么设计网络层次,以及这么多层会产生何种问题? 3颗星 不听也罢,有点太絮叨了。没必要理解这么详细的内容
第六章 简单的宠物识别 简单展示一个宠物识别的demo, 轻松的收尾 1颗星 看看入门级代码是什么样的,建议放在开头讲解。

2. 梯度下降介绍

1. 背景知识

2.1.1 梯度

首先 梯度是一个向量,包含了函数在每个方向上的偏导数,用于表示函数在多个维度上的变化率和方向.

在三维直角坐标系中的梯度函数展开公式为

g r a d ( f ) = ∇ f ( x , y , z ) = ∂ ∂ x x ⃗ + ∂ ∂ y y ⃗ + ∂ ∂ z z ⃗ grad (f)=\nabla f(x,y,z)=\frac{\partial}{\partial x}\vec{x} + \frac{\partial}{\partial y}\vec{y} + \frac{\partial}{\partial z}\vec{z} grad(f)=∇f(x,y,z)=∂x∂x +∂y∂y +∂z∂z 其中

x ⃗ , y ⃗ , z ⃗ \vec{x},\vec{y},\vec{z} x ,y ,z 分别为平面直角坐标系中的单位方向向量。

下图为一个曲面在直角坐标系中,各个整数坐标的梯度方向。假设最底端是我们想去的地方,那么需要在各个坐标的位置上不断的向梯度的反方向移动一小段距离△即可,注意每次移动都需要调整方向。这样即可把目标"如何达到最底端" 拆解成为一个可执行的数学公式。

2.1.2 解空间介绍

其次 我们将问题简化成为2维平面直角坐标系,提出一个泛化的问题: 如何模拟一个函数,它有有限的输入样本和结果?这个问题目前来看是正向不可解的,就连高中数学的不定方程组还必须增加几个特定的条件:线性连续性,次方的数(一次,二次 ),问题的空间局限性(明确是直线在有限的角度中切割圆形/椭圆形/三角形/正弦余弦函数等),结果是一个解空间。当问题脱离这些局限性后,就变成无固定数学公式可以套用的问题,而这类问题往往就是现实中最常见预测类问题。可以通过微分逼近的方式,在有限的样本中不断纠正模型的正确性,模拟出来问题的解空间,从而实现预测性。

假设一个问题:红色的点为房屋售价和面积的分布,如何拟合这个解空间?

直线 拟合 样本分布

曲线拟合 样本分布

2. 梯度下降法介绍

2.2.1 进一步简化问题为:

假设样本空间定义为

s a m p l e ⃗ = ∑ i = 1 N ( x i , y i ) \vec{sample}=\sum\limits_{i=1}^N (x_i , y_i) sample =i=1∑N(xi,yi)

假设解空间为线性函数(简化为只有一个变量x)

f ( x ) = a 0 + a 1 x f(x)=a_0 + a_1x f(x)=a0+a1x

如何求解上述房价的 拟合 函数F(x,y)?

2.2.2 损失函数

损失函数有很多种,详见这里和这里, 分别对应不同类型的问题规模:

  1. 0-1损失函数(zero-one loss)
  2. 绝对值损失函数
  3. log对数损失函数
  4. 平方损失函数 & 均方差损失
  5. 指数损失函数(exponential loss)
  6. Hinge 损失函数
  7. 感知损失(perceptron loss)函数
  8. 交叉熵损失函数 (Cross-entropy loss function)

这里因为预先假设了问题解空间为线性空间 ,所以采用均方差 损失函数

L o s s = 1 2 ∑ i = 1 N ( f ( x i ) − y i ) 2 Loss=\frac{1}{2}\sum\limits_{i=1}^N (f(x_i) - y_i)^2 Loss=21i=1∑N(f(xi)−yi)2

通过样本迭代计算出min Loss的过程.

2.2.3 梯度下降求解过程

上述损失函数的几何特性以为极限在下,向上开口的曲线,只需要计算曲线底部的Loss 函数对应的 f(x)表达式就可以得到目前为止最优的 Loss函数。

整个过程需要选择一个初始位置,不断的通过微分迭代调整下降的方向,直到达到这个"最底部"。整个过程类最直接的方式是沿着梯度反方向逐步向下尝试,类似于下山,俗称梯度下降法。

计算过程如下:

  1. 针对Loss进行梯度计算

∇ L o s s = 2 ∗ 1 2 ∑ i = 1 N d ( f ( x i ) − y i ) \nabla Loss = 2*\frac{1}{2}\sum\limits_{i=1}^N d(f(x_i) - y_i) ∇Loss=2∗21i=1∑Nd(f(xi)−yi) 其中i为样本编号, N为样本空间

  1. 展开 Loss针对三个模型变量 a0, a1的偏导数:

    因为 f ( x ) = a 0 + a 1 x f(x)=a_0 + a_1x f(x)=a0+a1x

    所以Loss的梯度可以展开为分别针对a0和a1的梯度公式:

    ∂ ∂ a 0 L o s s = ∑ i = 1 N ( f ( x i ) − y i ) ∂ f ( x i ) ∂ a 0 = ∑ i = 1 N ( f ( x i ) − y i ) \frac{\partial}{\partial a_0} Loss = \sum\limits_{i=1}^N (f(x_i) - y_i)\frac{\partial f(x_i)}{\partial a_0}= \sum\limits_{i=1}^N (f(x_i) - y_i) ∂a0∂Loss=i=1∑N(f(xi)−yi)∂a0∂f(xi)=i=1∑N(f(xi)−yi)

    ∂ ∂ a 1 L o s s = ∑ i = 1 N ( f ( x i ) − y i ) ∂ f ( x i ) ∂ a 1 = ∑ i = 1 N ( f ( x i ) − y i ) x i \frac{\partial}{\partial a_1} Loss = \sum\limits_{i=1}^N (f(x_i) - y_i)\frac{\partial f(x_i)}{\partial a_1}= \sum\limits_{i=1}^N (f(x_i) - y_i)x_i ∂a1∂Loss=i=1∑N(f(xi)−yi)∂a1∂f(xi)=i=1∑N(f(xi)−yi)xi

  2. 引入迭代次数k, 每一次迭代a0, a1都会向着梯度的反方向前进一小步α

a 0 ( k + 1 ) = a 0 ( k ) − α ∂ ∂ a 0 L o s s = a 0 ( k ) − α ∑ i = 1 N ( f ( x i ) − y i ) = a 0 ( k ) − α ∑ i = 1 N ( a 0 k + a 1 k x i − y i ) a_0^{(k+1)} = a_0^{(k)}-α \frac{\partial}{\partial a_0} Loss = a_0^{(k)}-α\sum\limits_{i=1}^N (f(x_i) - y_i) = a_0^{(k)}-α\sum\limits_{i=1}^N (a_0^k + a_1^k x_i - y_i) a0(k+1)=a0(k)−α∂a0∂Loss=a0(k)−αi=1∑N(f(xi)−yi)=a0(k)−αi=1∑N(a0k+a1kxi−yi)

a 1 ( k + 1 ) = a 1 ( k ) − α ∂ ∂ a 0 L o s s = a 0 ( k ) − α ∑ i = 1 N ( f ( x i ) − y i ) x i = a 0 ( k ) − α ∑ i = 1 N ( a 0 k + a 1 k x i − y i ) x i a_1^{(k+1)} = a_1^{(k)}-α \frac{\partial}{\partial a_0} Loss = a_0^{(k)}-α\sum\limits_{i=1}^N (f(x_i) - y_i)x_i= a_0^{(k)}-α\sum\limits_{i=1}^N (a_0^k + a_1^k x_i - y_i)x_i a1(k+1)=a1(k)−α∂a0∂Loss=a0(k)−αi=1∑N(f(xi)−yi)xi=a0(k)−αi=1∑N(a0k+a1kxi−yi)xi

  1. 初始化 a0=1, a1=1, 然后进行k=100的上述计算,直到如下条件收敛条件出现,否则算作收敛失败

lim ⁡ a i ( k + 1 ) − a i ( k ) ≼ ϕ \lim{a_i(k+1)-a_i(k)}\preccurlyeq \phi limai(k+1)−ai(k)≼ϕ

扩展为多变量时

f ( x ) = a 0 + a 1 x 1 + a 2 x 2 + ... ... a m x m = ∑ j = 0 m a j x j , ( x 0 = 1 ) f(x)=a_0 + a_1x_1 + a_2x_2 + ...... a_mx_m =\sum\limits_{j=0}^ma_jx_j, ( x_0=1) f(x)=a0+a1x1+a2x2+......amxm=j=0∑majxj,(x0=1)

则迭代函数为

a j ( k + 1 ) = a j ( k ) − α ∂ ∂ a 0 L o s s = a j ( k ) − α ∑ i = 1 N ( ∑ j = 0 m a j k x j i − y i ) x j i a_j^{(k+1)} = a_j^{(k)}-α \frac{\partial}{\partial a_0} Loss = a_j^{(k)}-α\sum\limits_{i=1}^N (\sum\limits_{j=0}^ma_j^kx_j^i - y_i)x_j^i aj(k+1)=aj(k)−α∂a0∂Loss=aj(k)−αi=1∑N(j=0∑majkxji−yi)xji

2.2.4 梯度下降法的Python伪代码

Python 复制代码
# 初始化参数, 这里为矩阵数组
theta = initial_theta

# 设置学习率和迭代次数
learning_rate = 0.01
max_iterations = 1000

# 设置提前收敛条件
tolerance = 0.0001

# Define a function to make predictions
def train(theta, features):
    # Initialize the predicted value
    predicted_value = 0
    
    # Calculate the predicted value as a weighted sum of features
    for i in range(len(theta)):
        predicted_value += theta[i] * features[i]
    
    return predicted_value
·
def calculate_loss_gradient(theta, data, labels):
    num_samples = len(data)
    num_features = len(theta)
    loss = 0
    
    # Initialize an array to store the gradient for each parameter
    gradient = [0] * num_features
    
    for i in range(num_samples):
        train_value = train(theta, data[i])  # Predicted value using the current theta
        error = train_value - labels[i]  # Error for the current sample
        loss += error**2  # Add squared error to the loss
        # Update the gradient for each parameter (feature)
        for j in range(num_features):
            gradient[j] += error * data[i][j]
    
    loss /= (2 * num_samples)  # Calculate the MSE loss
    
    for j in range(num_features):
        gradient[j] /= num_samples
    
    return gradient, loss


# 迭代优化过程
for i in range(max_iterations):
    # 计算梯度 和 损失函数
    gradient, loss = calculate_loss_gradient(theta)
    
    # 更新参数
    theta = theta - learning_rate * gradient
    
    # 检查是否达到提前收敛条件
    if abs(previous_loss - loss) < tolerance:
        break

    # 更新前一次的损失值
    previous_loss = loss

2.2.5 梯度下降法的缺陷

收敛太慢

局部最优问题

2.2.5 梯度下降法优化方案

详见:blog.csdn.net/weixin_4438...

3. CNN介绍

3.1 什么是卷积(Convolution)

简易说明 针对信号的一个叠加计算,使得输出的结果更加平整和容易理解,最早是用来对雷达波进行噪声过滤。在图形图像处理中,使用离散卷积计算叠加使得生成一个新的图片。离散卷积的计算公式如下:

y n = ∑ i = 0 n P i ∗ C o n v ( n ) = ∑ i = 0 n P i ∗ C o n v ( i ) yn=\sum\limits_{i=0}^nP_i*Conv(n) =\sum\limits_{i=0}^nP_i\*Conv(i) yn=i=0∑nPi∗Conv(n)=i=0∑nPi∗Conv(i) 其中 Conv(n) 为卷积核, P为像素pixel, P(i)为第i个像素, Conv(i)为卷积核的第i个元素.

卷积计算的原理

3.1.1 常见的 卷积核 以及 滤波后的效果 介绍

  1. 均值滤波器 & 高斯滤波器 -> 消除图像噪声点
  1. Sobel 算子 -> 提取图像边缘
  1. 双边滤波器 -> 保留明显边缘的特征情况下,针对内部纹理进行高度模糊处理

3.1.2 早期的人脸识别是怎样的?

***我的归纳观点*** 10年前传统的机器学习模型, 它们是人工做特征提取后再进行模型训练的方式,例如: 线性/逻辑回归, 朴素贝叶斯, 决策树, 随机森林, SVM, Booting家族(GBDT or Adaboost)。 举个例子,Haar特征(一类检测对称性的卷积核) 做人脸检测非常依赖对人脸对称性的特点理解,同时还需要辅助各种滤波器过滤阴影,敏感度等噪声因素,并且还需要针对图片局部关键信号区域做放大和缩小的处理,并且在这些不同scale的层次内撒下不同人工筛选的对称性相关的卷积核,然后让模型在样本空间中筛选出来最合适的解空间向量。

3.2 什么是神经网络(Neural Network)

3.2.1 基本概念

神经元定义

  • a1~an为输入向量X的各个分量
  • w1~wn为神经元各个突触的权重值(weight)
  • b为 偏置 (bias)
  • f为传递函数,通常为非线性函数。一般有traingd(),tansig(),hardlim()。以下默认为hardlim()
  • t为神经元输出

神经网络定义

基本的神经元网络形式,由有限个神经元构成,所有神经元的输入向量都是同一个向量。由于每一个神经元都会产生一个标量结果,所以单层神经元的输出是一个向量,向量的维数等于神经元的数目。

3.2.2 神经网络的背后思想特点

***我的归纳观点*** 神经网络模型认为人工提取特征的步骤是多余的,因为非常依赖算法工程师对于样本分布特性、领域建模和解空间的理解经验。人工筛选制造特征的过程会丢失大量的信号,因为每一个像素点都有可能对结果产生影响,谁也不能保证常见的haar特征枚举了全部的人脸特点,谁也不能保证尺度变化有没有丢失关键信号。唯有让所有像素点都参与决策计算,然后让模型计算出每个像素点对应的系数(可能大部分为0)。但是剩下的显著信号,就是这组样本空间中最显著的特征,这种方式比人工定义的Haar特征更靠谱。 因此诞生了BP神经网络的模型算法,因为其全连接的特性很像人类大脑神经元树突全连接的特性,因此命名为神经网络模型。在14年之前其他模型的学者所鄙视,认为其推理过程严重缺乏可解释性和数学的理性,全部依赖概率论,产出结果收敛性也不稳定,所以也被称为一把梭炼丹器,练出来的是神丹还是废丹完全看随机的效果。 当年由于算力的不足,备受各种鄙夷的指责。我曾经不止一次听到各个导师臭骂研究神经网络的学生。

***我的归纳观点*** 理论上神经网络模型层次越深效果越好,但是由于微分逼近的特性很有可能造成梯度消失(参考所有不可导的方式)从而让一个关键的神经元在前几层就失去了参数效果;也有一些不重要的神经元由于梯度的叠加,被放大的无数倍,最终成为了噪声点。一切的一切,都在Microsoft的 ResNet 残差神经网络模型提出后得到了有效的解决:引入了 残差块(residual block)的概念,可以绕过某一层计算,直接输入到下一层,避免了过多计算弱化或者过多叠加废信号。由模型在反向传播计算中选择是否绕过,有效的抑制了微分梯度下降法本身的梯度计算缺陷问题。

3.3 卷积神经网络的特点

3.3.1 传统神经网络的缺陷

  1. 图像需要处理的数据量太大,导致成本很高,效率很低。
  2. 图像在数字化的过程中很难保留原有的特征,导致图像处理的准确率不高。例如下图的两个例子本质内容是一样的,但是进行数字化后却不一样。

3.3.2 卷积的使用

  1. **卷积层** 突出局部特征. 例如如下卷积核的作用
  1. **池化层** 降维: 1024 *1024 * RGB 3通道的 图形 被压缩成为 200像素的图像依然不影响视觉效果,狗还是狗、猫还是猫。

    1. 最大池化
    2. 平均池化

3.3.3 激活函数

针对信号进行放大和过滤计算,如果一个卷积层的output是负值,那么它一定会影响模型的稳定性;如果它数量级过大,同样对下一层的系数影响巨大。所以每层计算后都需要使用激活函数判断output是否、如何进入下一层的计算。

ReLU 家族激活函数: 减少负数的影响

Sigmoid系列: 平滑、易于求导

3.3.4 Dropout

训练过程中随机丢掉一定量的神经元,防止出现过拟合

4. 梯度下降法与反向传播过程

4.1 基本概念

我们已经了解了CNN网络的基本概念,接下来需要继续了解正向传导 和反向传播概念

  • 正向传导: 输入的图片信号 按照顺序通过神经网络的每一层,直到最后产生预测的结果,也称为推理过程。
  • 反向传播 : 误差信息 从末端传递到输入端的过程。误差就是损失函数,由于末端直面预测的输出结果,因此梯度下降计算损失函数的过程也应该是从末端逐步往首端传递,直到到一层网络为止,也称之为训练过程。

4.2 推理反向传播的计算全连接神经网络过程

4.2.1 定义一个3层的单输入X, 单神经元Y的网络数学结构

假设单变量网络输入为x, Y1 , Y2 为中间层, 输出层为Y3 , E 为均方差损失函数, t 为标签。中间存在Y1 , Y2中间过程均为标准神经元函数, 并且已知整体的信号传导公式如下图所示,

已知数学要素如下:

E = ( Y 3 ( x ) − t ) 2 / 2 , Y 3 ( x ) = w 3 ∗ Y 2 ( x ) + b 3 , Y 2 ( x ) = w 2 ∗ Y 1 ( x ) + b 2 , Y 1 ( x ) = w 1 ∗ x + b 1 E=(Y3(x)-t)^2/2, Y3(x)=w_3*Y2(x)+b_3, Y2(x)=w_2 * Y1(x) + b_2, Y1(x)=w_1*x+b_1 E=(Y3(x)−t)2/2,Y3(x)=w3∗Y2(x)+b3,Y2(x)=w2∗Y1(x)+b2,Y1(x)=w1∗x+b1

E真多推导过程中所有参数的梯度矩阵为:

g r a d i e n t = ∂ E ∂ w 1 , ∂ E ∂ b 1 , ∂ E ∂ w 1 , ∂ E ∂ b 2 , ∂ E ∂ w 3 , ∂ E ∂ b 3 gradient=\\frac{\\partial E}{\\partial w_1}, \\frac{\\partial E}{\\partial b_1}, \\frac{\\partial E}{\\partial w_1}, \\frac{\\partial E}{\\partial b_2}, \\frac{\\partial E}{\\partial w_3}, \\frac{\\partial E}{\\partial b_3} gradient=∂w1∂E,∂b1∂E,∂w1∂E,∂b2∂E,∂w3∂E,∂b3∂E

4.2.2 推理展开gradient矩阵中的每一个偏导数

  1. 首先计算最后一层, 以E 为结论推理倒数第2层的参数**w3,b3** . 由于Y1(x), Y2(x) 不包含该参数,因此求E 针对**w3,b3** 的偏导数时Y1(x),Y2(x)被视作常量

∂ E ∂ w 3 = ∂ E ∂ Y 3 ⋅ ∂ Y 3 ∂ w 3 = ( Y 3 − t ) ⋅ ∂ Y 2 ∂ w 3 = ( Y 3 − t ) Y 2 \frac{\partial E}{\partial w_3}=\frac{\partial E}{\partial Y3}\cdot\frac{\partial Y3}{\partial w_3}=(Y3-t)\cdot \frac{\partial Y2}{\partial w_3}=(Y3-t)Y_2 ∂w3∂E=∂Y3∂E⋅∂w3∂Y3=(Y3−t)⋅∂w3∂Y2=(Y3−t)Y2

∂ E ∂ b 3 = ∂ E ∂ Y 3 ⋅ ∂ Y 3 ∂ b 3 = ( Y 3 − t ) ⋅ ∂ Y 2 ∂ b 3 = Y 3 − t \frac{\partial E}{\partial b_3}=\frac{\partial E}{\partial Y3}\cdot\frac{\partial Y3}{\partial b_3}=(Y3-t)\cdot \frac{\partial Y2}{\partial b_3}=Y3-t ∂b3∂E=∂Y3∂E⋅∂b3∂Y3=(Y3−t)⋅∂b3∂Y2=Y3−t

  1. 往前传播一层,进一步计算以E 为结论推理倒数第3层参数**w3,b3** . 由于Y1(x) 不包含该参数, Y3 包含, 因此求E 针对**w2,b2** 的偏导数时Y1(x)被视作常量, Y3(x) 需要进一步展开

∂ E ∂ w 2 = ∂ E ∂ Y 3 ⋅ ∂ Y 3 ∂ Y 2 ⋅ ∂ Y 2 ∂ w 2 = ( Y 3 − t ) ⋅ w 3 ⋅ Y 1 \frac{\partial E}{\partial w_2}=\frac{\partial E}{\partial Y3}\cdot\frac{\partial Y3}{\partial Y_2}\cdot\frac{\partial Y2}{\partial w_2}=(Y3-t)\cdot w_3\cdot Y1 ∂w2∂E=∂Y3∂E⋅∂Y2∂Y3⋅∂w2∂Y2=(Y3−t)⋅w3⋅Y1

∂ E ∂ b 2 = ∂ E ∂ Y 3 ⋅ ∂ Y 3 ∂ Y 2 ⋅ ∂ Y 2 ∂ b 2 = ( Y 3 − t ) ⋅ w 3 \frac{\partial E}{\partial b_2}=\frac{\partial E}{\partial Y3}\cdot\frac{\partial Y3}{\partial Y_2}\cdot\frac{\partial Y2}{\partial b_2}=(Y3-t)\cdot w_3 ∂b2∂E=∂Y3∂E⋅∂Y2∂Y3⋅∂b2∂Y2=(Y3−t)⋅w3

  1. 继续反向传播到第一层, 进一步计算以E 为结论推理倒数第4层参数**w1,b1** , 由于Y3包含Y2包含Y1包含w1,b1 ,因此需要全部展开

∂ E ∂ w 1 = ∂ E ∂ Y 3 ⋅ ∂ Y 3 ∂ Y 2 ⋅ ∂ Y 2 ∂ Y 1 ⋅ ∂ Y 1 ∂ w 1 = ( Y 3 − t ) ⋅ w 3 ⋅ w 2 ⋅ x \frac{\partial E}{\partial w_1}=\frac{\partial E}{\partial Y3}\cdot\frac{\partial Y3}{\partial Y_2}\cdot\frac{\partial Y2}{\partial Y_1}\cdot\frac{\partial Y1}{\partial w_1}=(Y3-t)\cdot w_3\cdot w_2 \cdot x ∂w1∂E=∂Y3∂E⋅∂Y2∂Y3⋅∂Y1∂Y2⋅∂w1∂Y1=(Y3−t)⋅w3⋅w2⋅x

∂ E ∂ b 1 = ∂ E ∂ Y 3 ⋅ ∂ Y 3 ∂ Y 2 ⋅ ∂ Y 2 ∂ Y 1 ⋅ ∂ Y 1 ∂ b 1 = ( Y 3 − t ) ⋅ w 3 ⋅ w 2 \frac{\partial E}{\partial b_1}=\frac{\partial E}{\partial Y3}\cdot\frac{\partial Y3}{\partial Y_2}\cdot\frac{\partial Y2}{\partial Y_1}\cdot\frac{\partial Y1}{\partial b_1}=(Y3-t)\cdot w_3\cdot w_2 ∂b1∂E=∂Y3∂E⋅∂Y2∂Y3⋅∂Y1∂Y2⋅∂b1∂Y1=(Y3−t)⋅w3⋅w2

4.2.3 局部梯度与参数梯度

局部梯度

只看 偏执值 **b3,b2,b1** 的偏导数**d3,d2,d1** 可以发现损失函数是一层层往前传递并逐层放大,数学上看是这样的

d 3 ⃗ = ∂ E ∂ b 3 = Y 3 − t \vec{d3}=\frac{\partial E}{\partial b_3}=Y3-t d3 =∂b3∂E=Y3−t

d 2 ⃗ = ∂ E ∂ b 2 = ( Y 3 − t ) ⋅ w 3 \vec{d2}=\frac{\partial E}{\partial b_2}=(Y3-t)\cdot w_3 d2 =∂b2∂E=(Y3−t)⋅w3 此时 Y3-t 上层已经计算好 (损失比上层放大 w3倍)

d 1 ⃗ = ∂ E ∂ b 1 = ( Y 3 − t ) ⋅ w 3 ⋅ w 2 \vec{d1}=\frac{\partial E}{\partial b_1}=(Y3-t)\cdot w_3\cdot w_2 d1 =∂b1∂E=(Y3−t)⋅w3⋅w2 此时 (Y3-t)*w3上层已经计算好 (损失比上层放大 w2倍)

因此从网络传导图上就相当于每一层把损失函数乘以对应的系数, 如下图所示

由于偏导数**d3,d2,d1** 与原始输入X 无关, 它们也被称之为局部梯度 , 表达目标函数对于本层的输入信号的偏导数

参数梯度

当局部梯度 计算完毕后,很容易根据输入X 计算推理出**w3,w2,w1** 的偏导数,称之为参数梯度

4.2.4 梯度下降法计算gradient矩阵

根据 2.2.3 进行梯度下降推理,E 为均方差损失函数, α 为步长, k 为迭代次数

w 3 ( k + 1 ) = w 3 ( k ) − α ∂ E ∂ w 3 w_3^{(k+1)} = w_3^{(k)}-α \frac{\partial E}{\partial w_3} w3(k+1)=w3(k)−α∂w3∂E

b 3 ( k + 1 ) = b 3 ( k ) − α ∂ E ∂ b 3 b_3^{(k+1)} = b_3^{(k)}-α \frac{\partial E}{\partial b_3} b3(k+1)=b3(k)−α∂b3∂E

w 2 ( k + 1 ) = w 2 ( k ) − α ∂ E ∂ w 2 w_2^{(k+1)} = w_2^{(k)}-α \frac{\partial E}{\partial w_2} w2(k+1)=w2(k)−α∂w2∂E

b 2 ( k + 1 ) = b 2 ( k ) − α ∂ E ∂ b 2 b_2^{(k+1)} = b_2^{(k)}-α \frac{\partial E}{\partial b_2} b2(k+1)=b2(k)−α∂b2∂E

w 1 ( k + 1 ) = w 1 ( k ) − α ∂ E ∂ w 1 w_1^{(k+1)} = w_1^{(k)}-α \frac{\partial E}{\partial w_1} w1(k+1)=w1(k)−α∂w1∂E

b 1 ( k + 1 ) = b 1 ( k ) − α ∂ E ∂ b 1 b_1^{(k+1)} = b_1^{(k)}-α \frac{\partial E}{\partial b_1} b1(k+1)=b1(k)−α∂b1∂E

至此,我们已经完整的推理了一个单X输入的3层神经网络的整体模型求解过程。

4.2.5 扩展X为矩阵, Y1为5个神经元的BP神经网络后的表达式

如果把输入X 扩展成4*4的矩阵后, 第一层 X ->Y1 的计算表达式为:

Y 1 ( x ) = w 1 ⃗ ∗ x ⃗ + b 1 ⃗ Y1(x)=\vec{w_1}*\vec{x}+\vec{b_1} Y1(x)=w1 ∗x +b1

x ⃗ = x 1 x 2 x 3 x 4 x 5 x 6 x 7 x 8 x 9 x 10 x 11 x 12 x 13 x 14 x 15 x 16 ⋅ w 11 w 21 w 31 w 41 w 51 w 12 w 22 w 32 w 42 w 52 ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ w 1 − 16 w 2 − 16 w 3 − 16 w 4 − 16 w 5 − 16 + b 1 1 b 1 2 b 1 3 b 1 4 b 1 5 \vec{x}=\begin{bmatrix} x_{1} & x_{2} & x_{3} & x_{4}\\ x_{5} & x_{6} & x_{7} & x_{8}\\ x_{9} & x_{10} & x_{11} & x_{12}\\ x_{13} & x_{14} & x_{15} & x_{16}\\ \end{bmatrix} \cdot \begin{bmatrix} w_{11} & w_{21} & w_{31} & w_{41} & w_{51} \\ w_{12} & w_{22} & w_{32} & w_{42} & w_{52} \\ \cdot & \cdot & \cdot & \cdot & \cdot \\ \cdot & \cdot & \cdot & \cdot & \cdot \\ \cdot & \cdot & \cdot & \cdot & \cdot \\ w_{1-16} & w_{2-16} & w_{3-16} & w_{4-16} & w_{5-16} \\ \end{bmatrix} + \begin{bmatrix} b1_1 & b1_2 & b1_3 & b1_4 & b1_5 \end{bmatrix} x = x1x5x9x13x2x6x10x14x3x7x11x15x4x8x12x16 ⋅ w11w12⋅⋅⋅w1−16w21w22⋅⋅⋅w2−16w31w32⋅⋅⋅w3−16w41w42⋅⋅⋅w4−16w51w52⋅⋅⋅w5−16 +b11b12b13b14b15

Y 1 ⃗ = b 1 1 + x 1 ⋅ w 11 + x 2 ⋅ w 12 + ⋅ ⋅ ⋅ x 2 ⋅ w 1 − 16 b 1 2 + x 1 ⋅ w 21 + x 2 ⋅ w 22 + ⋅ ⋅ ⋅ x 2 ⋅ w 2 − 16 b 3 2 + x 1 ⋅ w 31 + x 2 ⋅ w 32 + ⋅ ⋅ ⋅ x 2 ⋅ w 3 − 16 b 4 2 + x 1 ⋅ w 41 + x 2 ⋅ w 42 + ⋅ ⋅ ⋅ x 2 ⋅ w 4 − 16 b 5 2 + x 1 ⋅ w 51 + x 2 ⋅ w 52 + ⋅ ⋅ ⋅ x 2 ⋅ w 5 − 16 \vec{Y1}= \begin{bmatrix} b1_1+x_1 \cdot w_{11} + x_2 \cdot w_{12} + \cdot \cdot \cdot x_2 \cdot w_{1-16} \\ b1_2+x_1 \cdot w_{21} + x_2 \cdot w_{22} + \cdot \cdot \cdot x_2 \cdot w_{2-16} \\ b3_2+x_1 \cdot w_{31} + x_2 \cdot w_{32} + \cdot \cdot \cdot x_2 \cdot w_{3-16} \\ b4_2+x_1 \cdot w_{41} + x_2 \cdot w_{42} + \cdot \cdot \cdot x_2 \cdot w_{4-16} \\ b5_2+x_1 \cdot w_{51} + x_2 \cdot w_{52} + \cdot \cdot \cdot x_2 \cdot w_{5-16} \\ \end{bmatrix} Y1 = b11+x1⋅w11+x2⋅w12+⋅⋅⋅x2⋅w1−16b12+x1⋅w21+x2⋅w22+⋅⋅⋅x2⋅w2−16b32+x1⋅w31+x2⋅w32+⋅⋅⋅x2⋅w3−16b42+x1⋅w41+x2⋅w42+⋅⋅⋅x2⋅w4−16b52+x1⋅w51+x2⋅w52+⋅⋅⋅x2⋅w5−16

而针对w1=w11, w12, w13, w14, w15 向量化的梯度下降计算公式 w 1 ⃗ ( k + 1 ) = w 1 ⃗ ( k ) − α ∂ E ∂ w 1 \vec{w_1}^{(k+1)} = \vec{w_1}^{(k)}-α \frac{\partial E}{\partial w_1} w1 (k+1)=w1 (k)−α∂w1∂E展开为

w 11 w 12 ⋅ ⋅ ⋅ w 1 − 16 w 21 w 22 ⋅ ⋅ ⋅ w 2 − 16 w 31 w 32 ⋅ ⋅ ⋅ w 3 − 16 w 41 w 42 ⋅ ⋅ ⋅ w 4 − 16 w 51 w 52 ⋅ ⋅ ⋅ w 5 − 16 ( k + 1 ) = w 11 w 12 ⋅ ⋅ ⋅ w 1 − 16 w 21 w 22 ⋅ ⋅ ⋅ w 2 − 16 w 31 w 32 ⋅ ⋅ ⋅ w 3 − 16 w 41 w 42 ⋅ ⋅ ⋅ w 4 − 16 w 51 w 52 ⋅ ⋅ ⋅ w 5 − 16 ( k ) − α ∂ E ∂ w 11 ∂ E ∂ w 12 ⋅ ⋅ ⋅ ∂ E ∂ w 1 − 16 ∂ E ∂ w 21 ∂ E ∂ w 22 ⋅ ⋅ ⋅ ∂ E ∂ w 2 − 16 ∂ E ∂ w 31 ∂ E ∂ w 32 ⋅ ⋅ ⋅ ∂ E ∂ w 13 − 16 ∂ E ∂ w 41 ∂ E ∂ w 42 ⋅ ⋅ ⋅ ∂ E ∂ w 41 − 16 ∂ E ∂ w 51 ∂ E ∂ w 52 ⋅ ⋅ ⋅ ∂ E ∂ w 5 − 16 \begin{bmatrix} w_{11} & w_{12} \cdot \cdot \cdot w_{1-16} \\ w_{21} & w_{22} \cdot \cdot \cdot w_{2-16} \\ w_{31} & w_{32} \cdot \cdot \cdot w_{3-16} \\ w_{41} & w_{42} \cdot \cdot \cdot w_{4-16} \\ w_{51} & w_{52} \cdot \cdot \cdot w_{5-16} \\ \end{bmatrix} ^{(k+1)} = \begin{bmatrix} w_{11} & w_{12} \cdot \cdot \cdot w_{1-16} \\ w_{21} & w_{22} \cdot \cdot \cdot w_{2-16} \\ w_{31} & w_{32} \cdot \cdot \cdot w_{3-16} \\ w_{41} & w_{42} \cdot \cdot \cdot w_{4-16} \\ w_{51} & w_{52} \cdot \cdot \cdot w_{5-16} \\ \end{bmatrix} ^{(k)}-α \begin{bmatrix} \frac{\partial E}{\partial w_{11}} & \frac{\partial E}{\partial w_{12}} \cdot \cdot \cdot \frac{\partial E}{\partial w_{1-16}} \\ \frac{\partial E}{\partial w_{21}} & \frac{\partial E}{\partial w_{22}} \cdot \cdot \cdot \frac{\partial E}{\partial w_{2-16}} \\ \frac{\partial E}{\partial w_{31}} & \frac{\partial E}{\partial w_{32}} \cdot \cdot \cdot \frac{\partial E}{\partial w_{13-16}} \\ \frac{\partial E}{\partial w_{41}} & \frac{\partial E}{\partial w_{42}} \cdot \cdot \cdot \frac{\partial E}{\partial w_{41-16}} \\ \frac{\partial E}{\partial w_{51}} & \frac{\partial E}{\partial w_{52}} \cdot \cdot \cdot \frac{\partial E}{\partial w_{5-16}} \\ \end{bmatrix} w11w21w31w41w51w12⋅⋅⋅w1−16w22⋅⋅⋅w2−16w32⋅⋅⋅w3−16w42⋅⋅⋅w4−16w52⋅⋅⋅w5−16 (k+1)= w11w21w31w41w51w12⋅⋅⋅w1−16w22⋅⋅⋅w2−16w32⋅⋅⋅w3−16w42⋅⋅⋅w4−16w52⋅⋅⋅w5−16 (k)−α ∂w11∂E∂w21∂E∂w31∂E∂w41∂E∂w51∂E∂w12∂E⋅⋅⋅∂w1−16∂E∂w22∂E⋅⋅⋅∂w2−16∂E∂w32∂E⋅⋅⋅∂w13−16∂E∂w42∂E⋅⋅⋅∂w41−16∂E∂w52∂E⋅⋅⋅∂w5−16∂E

向量w1 一共是 445=80个需要进行梯度下降的参数。 其他层的参数就不展开了.

4.3 梯度下降法如何计算卷积

根据 3.1 章节可知 离散卷积的数学表达式为

y n = ∑ i = 0 n P i ∗ C o n v ( n ) = ∑ i = 0 n P i ∗ C o n v ( i ) yn=\sum\limits_{i=0}^nP_i*Conv(n) =\sum\limits_{i=0}^nP_i\*Conv(i) yn=i=0∑nPi∗Conv(n)=i=0∑nPi∗Conv(i)

具体把P 展开成像素矩阵, Conv展开成卷积核矩阵后,数学表达式进一步为

计算过程是一个滑动窗口, 其中 第一个3*3 的像素xij 与 卷积核向量K 点积计算,然后加上偏执值向量B

从线性代数的计算过程来看,其是不是与 4.2.5 的 Y1 神经元向量表达式类似。

虽然卷积计算和神经网络全连接计算目标不一样,但是从梯度下降和反向传播的数学推理公式来看是一模一样的。所以抽象的表达式 f(x)=wx+b 可以完全cover如下4个计算过程:

  1. 神经元的推理计算。
  2. 卷积窗口的特征提取计算。
  3. 池化卷积窗口的降维计算。
  4. 激活函数的计算。

其实还有其他的业务含义,例如Yolo3针对连续视频中的多目标分类结果,都可以融入梯度下降法解决 抽象的表达式 f(x)=wx+b 线性代数和离散微分计算框架中。

5. VGG16模型介绍

5.1 VGG16神经网络层次说明

  • 输入图片格式要求为244*244 的 RGB 3通道图片
  • 13个卷积层,用conv3-xxx表示;
  • 5个池化层,用maxpool表示;
  • 3个全连接层,用FC-xxx表示。

由于VGG16模型中只有13个卷积层和3个全连接层能产生权重参数,故VGG16的16来自于13+3。

下图为VGGNet模型有A-E五种结构网络

5.2 每层网络的入参出参详解,以及计算规模评估

5.2.1 基础知识

  • 卷积核输出结果矩阵Size计算公式:

O u t p u t = I n p u t − K e r n e l S i z e + 2 ⋅ P a d d i n g S t e p Output= \frac{Input - KernelSize + 2\cdot Padding}{Step} Output=StepInput−KernelSize+2⋅Padding + 1 其中 Input为输入矩阵的长宽, KernelSize为卷积核的长宽,

  • 感受野: Receptive Field, 也就是 Kernel Size, 表达了卷积窗口针对输入矩阵的感知区域大小。

  • Padding:

    • Valid: 不填充像素(Padding=0),直接进行卷积计算,会导致图像最终缩小。
    • Same: 在图像外圈填充一圈值为0的像素, 保障图像的边缘边框像素能被卷积核捕捉到。

5.2.2 从Input到 Conv1 的计算过程详解:

  • 输入图像尺寸: 224 * 224 * 3
  • 卷积尺寸: 3\*3 \*3, Padding=1, Step=1 则输出目标矩阵Output1 大小为依然为224 * 224, 计算公式如下

W o u t = W i n p u t − W k e r n e l S i z e + 2 ⋅ P a d d i n g S t e p + 1 = 224 − 3 + 2 ∗ 1 1 + 1 = 224 W_{out}=\frac{W_{input} - W_{kernelSize} + 2\cdot Padding}{Step}+1=\frac{224 - 3 + 2 * 1} {1} + 1 = 224 Wout=StepWinput−WkernelSize+2⋅Padding+1=1224−3+2∗1+1=224

H o u t = H i n p u t − H k e r n e l S i z e + 2 ⋅ P a d d i n g S t e p + 1 = 224 − 3 + 2 ∗ 1 1 + 1 = 224 H_{out}=\frac{H_{input} - H_{kernelSize} + 2\cdot Padding}{Step}+1=\frac{224 - 3 + 2 * 1} {1} + 1 = 224 Hout=StepHinput−HkernelSize+2⋅Padding+1=1224−3+2∗1+1=224

  • 单次神经元进行卷积的计算参数向量w 为 3*3 *3 = 27个, 局部参数b 为1个. 根据公式 f(x)=wx+b 成矩阵形式如下:

f ( x ⃗ ) = x 1 x 2 ⋅ ⋅ ⋅ x 27 ⋅ w 1 w 2 ⋅ ⋅ ⋅ w 27 + b 1 f(\vec{x})=\begin{bmatrix} x_{1} & x_{2} & \cdot \cdot \cdot x_{27} \end{bmatrix} \cdot \begin{bmatrix} w_{1} & w_{2} & & \cdot \cdot \cdot w_{27} \end{bmatrix} + b1 f(x )=x1x2⋅⋅⋅x27⋅w1w2⋅⋅⋅w27+b1

  • 整体输出结果个数为 224 * 224 = 50176, 因此参数向量 W=50176 * 27 = 1354752 个。 局部参数b = 27 * 64 = 50176 个
  • Conv1输出层深度为64, 表达卷积核 3*3* 3 有64种变形种类,每一种都要进行一次上述矩阵的权重计算,因此第一层整体训练的参数向量 W= 1354752 * 64 = 86704128 个. 局部参数 **b=**50176 个 * 64 = 3211264个

所以最终输出 224 * 224 * 64 个神经元,需要进行梯度下降求解的参数数量为 86704128 个w + 3211264 个b1

5.2.5 从Conv1到 Conv2 的计算过程详解:

  • 输入矩阵尺寸: 224 * 224 * 64
  • 卷积尺寸: 3\*3 \*64, Padding=1, Step=1 则输出目标矩阵Output1 大小为依然为224 * 224
  • 单次神经元进行卷积的计算参数向量w 为 3*3 *64 = 576个, 局部参数b为1个
  • 整体输出结果个数为224 * 224 = 50176, 参数向量 W = 50176 * 576= 28901376个; b=576个
  • Conv2 深度为64, 卷积核 3\*3 *64 有64个变种,每一个都需要进行一次上述权重计算,因此最终参数量为**W=28901376 * 64 = 18.4亿个, b=576*64=36864个**

5.3 使用Keras 语法搭建 VGG16 模型

Keras 官网:keras.io/

Python 复制代码
    # Block 1
    x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv1')(img_input)
    x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv2')(x)
    x = MaxPooling2D((2, 2), strides=(2, 2), name='block1_pool')(x)

    # Block 2
    x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv1')(x)
    x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv2')(x)
    x = MaxPooling2D((2, 2), strides=(2, 2), name='block2_pool')(x)

    # Block 3
    x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv1')(x)
    x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv2')(x)
    x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv3')(x)
    x = MaxPooling2D((2, 2), strides=(2, 2), name='block3_pool')(x)

    # Block 4
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv1')(x)
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv2')(x)
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv3')(x)
    x = MaxPooling2D((2, 2), strides=(2, 2), name='block4_pool')(x)

    # Block 5
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv1')(x)
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv2')(x)
    x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv3')(x)
    x = MaxPooling2D((2, 2), strides=(2, 2), name='block5_pool')(x)

    #
    if include_top:
        # 分类 block
        x = Flatten(name='flatten')(x)
        x = Dense(4096, activation='relu', name='fc1')(x)
        x = Dense(4096, activation='relu', name='fc2')(x)
        x = Dense(classes, activation='softmax', name='predictions')(x)
    else:
        # 当 include_top为False时,设置pooling
        if pooling == 'avg':
            x = GlobalAveragePooling2D()(x)
        elif pooling == 'max':
            x = GlobalMaxPooling2D()(x)

6. 简单的宠物识别功能演示

6.1 安装环境

Anaconda 环境安装教程: blog.csdn.net/wejack/arti...

PIP安装: tensorflow, PIL, matplotlib

6.2 用inceptionV3 进行重新训练

训练代码 && 训练集*

css 复制代码
python3 /Users/caisheng/code/tensorflow/tensorflow/examples/image_retraining/retrain.py --bottleneck_dir bottleneck --how_many_training_steps 200 --model_dir /Users/caisheng/code/pythonLearn/models/inception_model --output_graph output/graph.pb --output_labels output/labels.txt --image_dir  images/
pause

识别模块代码(tensorflow) 和 测试集

Python 复制代码
import tensorflow as tf
import os
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt

## 测试集目录
input_path = "../test_sets/pets/"

## 模型路径
model_path = "../models/pets_model/"
model_file_name = model_path + "graph.pb"
label_file_name = model_path + "labels.txt"
label_chinese_name = model_path + "chinese.txt"

## 读取标签集合
uid_to_english = {}
labels = tf.io.gfile.GFile(label_file_name).readlines()
for uid, line in enumerate(labels):
    ##去掉换行符
    line = line.strip('\n')
    uid_to_english[uid] = line

##标签中文名
english_to_chinese = {}
chineseNames = tf.io.gfile.GFile(label_chinese_name).readlines()
for line in chineseNames:
    ##去掉换行符
    name_pair = line.strip('\n').split(',')
    english_to_chinese[name_pair[0]] = name_pair[1]

def id_to_string(node_id):
    if node_id not in uid_to_english:
        return ''

    english = uid_to_english[node_id]
    if english not in english_to_chinese:
        return english
    if not english_to_chinese[english].strip():
        return english
    return  english_to_chinese[english]

## 加载一个图存储自己训练好的宠物模型
with tf.io.gfile.GFile(model_file_name, 'rb') as f:
    graph_def = tf.compat.v1.GraphDef()
    graph_def.ParseFromString(f.read())
    tf.import_graph_def(graph_def, name = '')


## 识别宠物图像
with tf.compat.v1.Session() as sess:
    softmax_tensor = sess.graph.get_tensor_by_name('final_result:0')
    # 遍历图片目录
    for root, dirs, files in os.walk(input_path):
        for file in files:
            file_name, file_extend = os.path.splitext(file)
            if not file_extend == ".jpg":
                continue

            full_img_path = input_path + file
            ## 载入图片
            image_data = tf.io.gfile.GFile(full_img_path, 'rb').read()
            predictions = sess.run(softmax_tensor, {'DecodeJpeg/contents:0' : image_data})  ## 传入jpg格式的 图像tensor
            predictions = np.squeeze(predictions)  ## 结果转换为1维度数据

            ##打印数据
            print(full_img_path + " 可能是:")
            ## top3分类
            top_3 = predictions.argsort()[-3:][::-1]
            for node_id in top_3:
                human_string = id_to_string(node_id)
                #获取该分类的置信度
                score = predictions[node_id]
                print('%s (score = %.5f),' % (human_string, score))
            print("____________________________________\n")

            ## 显示图片
            img = Image.open(full_img_path)
            plt.imshow(img)
            plt.axis('off')
            plt.show()

6.3 识别效果

相关推荐
Discipline10292 小时前
尼泊尔河流流量:天气驱动的预测与高流量风险
人工智能·算法
XuCoder2 小时前
改一个数,右边全得重算,这题怎么扛住两万次查询
算法
小羊没烦恼!2 小时前
在Scrum中实施敏捷建模
java·开发语言·windows·算法·c#
weixin_307779133 小时前
基于睿擎工业开发平台的预训练视觉模型轻量化适配与低代码部署优化
开发语言·算法
hold?fish:palm3 小时前
45 二叉树的右视图
开发语言·c++·算法
粤鼎恒业4 小时前
惠州工厂电子料回收厂家推荐:从交接单反推筛选标准
大数据·数据库·算法·硬件架构·硬件工程·pcb工艺·材料工程
小爷毛毛(卓寿杰)4 小时前
【Agent 意图识别】输出协议、评估与置信度
人工智能·算法·大模型·prompt·大语言模型·agent
优质酸酸乳4 小时前
力扣100题之21合并两个有序链表
算法·leetcode·链表
旖旎夜光4 小时前
LeetCode 137 : 只出现一次的数字 II(位运算) —— 题解
c++·笔记·学习·算法·leetcode·力控
旋生万物4 小时前
Agent System Prompt 注入螺旋公理:让 AI 推理不跑偏的可复制模板(附完整 Prompt)
人工智能·算法