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

深度学习day7-BP之梯度下降,过拟合与欠拟合

3 BP之梯度下降

梯度下降算法的目标是找到使损失函数L最小的参数,核心是沿着损失函数梯度的负方向更新参数,逐步逼近局部或者全局最优解,使模型更好地拟合训练数据。

1 数学描述

$$
w_{ij}^{new}= w_{ij}^{old} - \alpha \frac{\partial E}{\partial w_{ij}}
$$

α是学习率。

学习率要随着训练的进行而变化。

过程阐述

①.初始化参数:

$$
随机初始化模型的参数 \theta ,如权重 W和偏置 b。
$$

②.计算梯度:

$$
损失函数 L(\theta)对参数 \theta 的梯度 \nabla_\theta L(\theta),表示损失函数在参数空间的变化率。
$$

③.更新参数:

$$
按照梯度下降公式更新参数:\theta := \theta - \alpha \nabla_\theta L(\theta),其中,\alpha 是学习率,用于控制更新步长。
$$

④.迭代更新:

重复②-③,直到某个终止条件(梯度接近0,不再收敛,完成迭代次数等)

2 传统下降方式

根据计算梯度时数据量不同

1. 批量梯度下降-BGD

Batch Gradient Descent BGD

特点:每次更新参数时,使用整个训练集来计算梯度。

优点:收敛稳定,能准确地沿着损失函数的真实梯度方向下降

适用于小型数据集

缺点:对于大型数据集,更新速度慢

需要大量内存来存储整个数据集

公式:

$$
\theta := \theta - \alpha \frac{1}{m} \sum_{i=1}^{m} \nabla_\theta L(\theta; x^{(i)}, y^{(i)})
$$

$$
其中,m 是训练集样本总数,x^{(i)}, y^{(i)} 是第 i个样本及其标签。
$$

2.随机梯度下降-SGD

Stochastic Gradient Descent, SGD

特点:每次更新参数时,仅使用一个样本来计算梯度

优点:更新频率高,计算快,适合大规模数据集

能够跳出局部最小值,有助于找到全局最优解

缺点:收敛不稳定,容易震荡(每个样本的梯度可能都不完全代表整体方向)

需要较小的学习率来缓解震荡

公式:

$$
\theta := \theta - \alpha \nabla_\theta L(\theta; x^{(i)}, y^{(i)})
$$

$$
其中,x^{(i)}, y^{(i)} 是当前随机抽取的样本及其标签。
$$

3.小批量梯度下降-MGBD

Mini-batch Gradient Descent MGBD

特点:每次更新参数时,使用一小部分训练集(小批量)来计算梯度

优点:在计算效率和收敛稳定性直接取得平衡

能够利用向量化加速硬件,适合现代硬件(GPU)

缺点:选择合适的批量比较困难:太小接近SGD,太大接近BGD

根据硬件算力设置为2的次方

公式:

$$
\theta := \theta - \alpha \frac{1}{b} \sum_{i=1}^{b} \nabla_\theta L(\theta; x^{(i)}, y^{(i)})
$$

$$
其中,b 是小批量的样本数量,也就是 batch\_size。
$$

3问题

鞍点:导数为0,但不是极值点

收敛速度慢:BGD和MBGD使用固定学习率,太大会导致震荡,太小会收敛缓慢

局部最小值和鞍点问题:SGD遇见局部最小值或鞍点时容易停滞,导致模型难以达到全局最优

训练不稳定:SGD中的噪声容易导致训练过程中不稳定,使训练陷入震荡或者不收敛

4 优化梯度下降方式

1.指数加权平均

加权平均指的是给每个数赋予不同的权重求得平均数。

移动平均数,指的是计算最近邻的 N 个数来获得平均数。

指数移动加权平均(Exponential Moving Average简称EMA)则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较小),距离越近则对平均数的计算贡献就越大(权重越大)。

公式:

$$
\ S_t = \begin{cases} Y_1, & t=0\\ \beta*S_{t-1} +(1-\beta)*Y_t, & t>0 \end{cases}
$$

  • St 表示指数加权平均值(EMA);

  • Yt 表示 t 时刻的值;

  • $$
    \beta是平滑系数,取值范围为 0\leq \beta < 1。\beta 越接近1表示对历史数据依赖性越高;越接近0则越依赖当前数据。该值越大平均数越平缓
    $$

     

import numpy as np
import matplotlib as plt
def test01():
    np.random.seed(0)
    x=np.arange(30)
    print(x)
    y=np.random.randint(5,40,30)
    print(y)
    plt.plot(x,y,c='b')
    plt.scatter(x,y,c='r')
    plt.show()
​
def test02(beta=0.9):
    np.random.seed(0)
    x=np.arange(30)
    y=np.random.randint(5,40,30)
    y_e=np.array([])
    for i in range(30):
        if i==0:
            y_e.append[0]
        else:
            st=beta*y_e[-1]+(1-beta)*y[i]
            y_e.append(st)
    plt.plot(x,np.array(y_e),c='b')
    plt.scatter(x,np.array(y_e),c='r')
    plt.show()
​
    if __name__ == "__main__":
       test01()
2.Momentum-动量

动量(Momentum)是对梯度下降的优化方法,可以更好地应对梯度变化和梯度消失问题,从而提高训练模型的效率和稳定性。

特点:

惯性效应:加入前面梯度的累积,使得算法沿着当前的方向继续更新,即时遇见鞍点,也不会因为梯度逼近0而停滞

减少震荡:平滑了梯度更新,减少在鞍点附近的震荡,帮助优化过程稳定向前推进。

加速收敛: 在优化过程中持续沿着某个方向前进,能够更快地穿越鞍点区域,避免在鞍点附近长时间停留。

公式:

$$
Dt = β * S_{t-1 }+ (1- β) * Dt
$$

$$
1. S_{t-1 }表示历史梯度移动加权平均值 2. D_t 表示当前时刻的梯度值 3. β 为权重系数
$$

$$
W_{t+1}=W_t-α*D_t
$$

原理:

Momentum 动量梯度下降算法已经在先前积累了一些梯度值,很有可能使得跨过鞍点。

Momentum 使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程。一定程度上有利于降低 “峡谷” 问题的影响。

峡谷问题:就是会使得参数更新出现剧烈震荡

API

optimizer = optim.SGD(model.parameters(), lr=0.6, momentum=0.9) 
# 学习率和动量值可以根据实际情况调整,momentum 参数指定了动量系数,默认为0。动量系数通常设置为 0 到0.5 之间的一个值,但也可以根据具体的应用场景调整

总结:

  • 动量项更新:利用当前梯度和历史动量来计算新的动量项。

  • 权重参数更新:利用更新后的动量项来调整权重参数。

  • 梯度计算:在每个时间步计算当前的梯度,用于更新动量项和权重参数。

没有对学习率进行优化

import torch
#1.创建一个神经网络类:继承官方的nn.Module
class mynet(torch.nn.Module):
    #2.定义网络结构
    def   __init__(self,input_size,output_size):
       #3.初始化父类:python语法要求调用super方法生成父类的功能让子类对象去继承
       super(mynet,self).__init__()
       #4.定义网络结构
       self.hide1=torch.nn.Sequential(torch.nn.Linear(input_size,2),torch.nn.Sigmoid())#2个w,3个神经元
       self.hide2=torch.nn.Sequential(torch.nn.Linear(2,2),torch.nn.Sigmoid())
       self.hide3=torch.nn.Sequential(torch.nn.Linear(2,12),torch.nn.Sigmoid())
       self.out=torch.nn.Sequential(torch.nn.Linear(12,output_size),torch.nn.Sigmoid())
    def forward(self,input):
        input.shape[1]
        x=self.hide1(input)
        x=self.hide2(x)
        x=self.hide3(x)
        pred=self.out(x)
        return pred
​
def train():
    #数据集
    input=torch.tensor([0.5,0.10],
                       [0.05,0.180],
                       [0.05,0.310])
    target=torch.tensor([[1,2],[0,3],[1,123]],dtype=torch.float32)
    #5.创建网络
    net=mynet(2,2)
    #6.定义损失函数
    loss_func=torch.nn.MSELoss()
    #7.定义优化器
    optimizer=torch.optim.SGD(net.parameters(),lr=0.01,momentum=0.6)
    #8.训练
    for epoch in range(100):
        #9.前向传播
        y_pred=net(input)
        #10.计算损失
        loss=loss_func(y_pred,target)
        #11.梯度清零
        optimizer.zero_grad()
        #net.hide1[0].weight.grad is None
        #12.反向传播(计算每一层的w的梯度值)
        loss.backward()
        #print(net.hide1[0].weiget)
        #print(net.hide1[0].weiget.grad)
        #break
        #13.参数更新
        optimizer.step()#w=w-lr*当前的移动指数加权平均(s=momentum*s+(1-momentum)*w.grad)
        print(loss)
​
​
​
if __name__ == "__main__":
    train()
​
​

3.AdaGrad-学习率

AdaGrad(Adaptive Gradient Algorithm)为每个参数引入独立的学习率,根据历史梯度的平方和来调整这些学习率,这样就使得参数具有较大的历史梯度的学习率减小,而参数具有较小的历史梯度的学习率保持较大。AdaGrad避免了统一学习率的不足,更多用于处理稀疏数据和梯度变化较大的问题。

步骤:

  1. 初始化学习率 α、初始化参数 θ(w)、小常数 σ = 1e-6

  2. 初始化梯度累积变量 s = 0

  3. 从训练集中采样 m 个样本的小批量,计算梯度 g

  4. 累积平方梯度 s = s + g ⊙ g,⊙ 表示各个分量相乘

  5. 学习率 α 的计算公式如下:

$$
\alpha = \frac{\alpha}{\sqrt{s} + \sigma}
$$

6.参数更新公式如下:

$$
\theta =\theta- \frac{\alpha}{\sqrt{s} + \sigma}·g
$$

$$
\alpha是全局的初始学习率;\sigma 是一个非常小的常数,用于避免除零操作(通常取 10^{-8});\frac{\alpha}{\sqrt{s }+\sigma} 是自适应调整后的学习率。
$$

优点

  • 自适应学习率:由于每个参数的学习率是基于其梯度的累积平方和 来动态调整的,这意味着学习率会随着时间步的增加而减少,对梯度较大且变化频繁的方向非常有用,防止了梯度过大导致的震荡。

  • 适合稀疏数据:AdaGrad 在处理稀疏数据时表现很好,因为它能够自适应地为那些较少更新的参数保持较大的学习率。

缺点

  1. 学习率过度衰减:随着时间的推移,累积的时间步梯度平方值越来越大,导致学习率逐渐接近零,模型会停止学习。

  2. 不适合非稀疏数据:在非稀疏数据的情况下,学习率过快衰减可能导致优化过程早期停滞。

API:

optimizer = optim.Adagrad(model.parameters(), lr=0.9)  # 设置学习率
4 .RMSProp

RMSProp(Root Mean Square Propagation)在时间步中使用指数加权平均来逐步衰减过时的梯度信息。这种方法专门用于解决AdaGrad在训练过程中学习率过度衰减的问题。

步骤:

$$
1.初始化学习率 α、初始化参数 θ、小常数 σ = 1e-8( 用于防止除零操作(通常取 10^{-8} ))
$$

  1. 初始化参数 θ

  2. 初始化梯度累计变量 s=0

  3. 从训练集中采样 m 个样本的小批量,计算梯度 g

  4. 使用指数移动平均累积历史梯度,公式如下:

    $$
    s=\beta·s+(1-\beta)g⊙g
    $$

     

5.学习率α的公式:

$$
\alpha = \frac{\alpha}{\sqrt{s} + \sigma}
$$

6.参数更新公式:

$$
\theta =\theta- \frac{\alpha}{\sqrt{s} + \sigma}·g
$$

优点

  • 适应性强:RMSProp自适应调整每个参数的学习率,对于梯度变化较大的情况非常有效,使得优化过程更加平稳。

  • 适合非稀疏数据:相比于AdaGrad,RMSProp更加适合处理非稀疏数据,因为它不会让学习率减小到几乎为零。

  • 解决过度衰减问题:通过引入指数加权平均,RMSProp避免了AdaGrad中学习率过快衰减的问题,保持了学习率的稳定性

缺点:

1.依赖于超参数的选择:RMSProp的效果对衰减率 β和学习率 α 的选择比较敏感,需要一些调参工作。

AdaGrad 和 RMSProp 都是对于不同的参数分量使用不同的学习率,如果某个参数分量的梯度值较大,则对应的学习率就会较小,如果某个参数分量的梯度较小,则对应的学习率就会较大一些

API:

optimizer = optim.RMSprop(model.parameters(), lr=0.7, momentum=0.9)  # 设置学习率和动量
5.Adam

Adam(Adaptive Moment Estimation)算法将动量法和RMSProp的优点结合在一起:

  • 动量法:通过一阶动量(即梯度的指数加权平均)来加速收敛,尤其是在有噪声或梯度稀疏的情况下。

  • RMSProp:通过二阶动量(即梯度平方的指数加权平均)来调整学习率,使得每个参数的学习率适应其梯度的变化。

  • Momentum 使用指数加权平均计算当前的梯度值、AdaGrad、RMSProp 使用自适应的学习率,Adam 结合了 Momentum、RMSProp 的优点,使用:移动加权平均的梯度和移动加权平均的学习率。使得能够自适应学习率的同时,也能够使用 Momentum 的优点。

  • 优点

    1. 高效稳健:Adam结合了动量法和RMSProp的优势,在处理非静态、稀疏梯度和噪声数据时表现出色,能够快速稳定地收敛。

    2. 自适应学习率:Adam通过一阶和二阶动量的估计,自适应调整每个参数的学习率,避免了全局学习率设定不合适的问题。

    3. 适用大多数问题:Adam几乎可以在不调整超参数的情况下应用于各种深度学习模型,表现良好。

    缺点

    $$
    1.超参数敏感:尽管Adam通常能很好地工作,但它对初始超参数(如 \beta_1、 \beta_2 和 \eta)仍然较为敏感,有时需要仔细调参。
    $$

     

2.过拟合风险:由于Adam会在初始阶段快速收敛,可能导致模型陷入局部最优甚至过拟合。因此,有时会结合其他优化算法(如SGD)使用。

API

optimizer = optim.Adam(model.parameters(), lr=0.05)  # 设置学习率

梯度下降算法通过不断更新参数来最小化损失函数,是反向传播算法中计算权重调整的基础。在实际应用中,根据数据的规模和计算资源的情况,选择合适的梯度下降方式(批量、随机、小批量)及其变种(如动量法、Adam等)可以显著提高模型训练的效率和效果。

Adam是目前最为流行的优化算法之一,因其稳定性和高效性,广泛应用于各种深度学习模型的训练中。Adam结合了动量法和RMSProp的优点,能够在不同情况下自适应调整学习率,并提供快速且稳定的收敛表现。

八 过拟合与欠拟合

则化技术主要就是用于防止过拟合,提升模型的泛化能力(对新数据表现良好)和鲁棒性(对异常数据表现良好)。

1 概念

1 过拟合

过拟合是指模型对训练数据拟合能力很强并表现很好,但在测试数据上表现较差。

过拟合常见原因有:

  1. 数据量不足:当训练数据较少时,模型可能会过度学习数据中的噪声和细节。

  2. 模型太复杂:如果模型很复杂,会过度学习训练数据中的细节和噪声。

  3. 正则化强度不足:如果正则化强度不足,可能会导致模型过度学习训练数据中的细节和噪声。

2 欠拟合

欠拟合是由于模型学习能力不足,无法充分捕捉数据中的复杂关系。

3 判断

过拟合

训练误差低,但验证时误差高。模型在训练数据上表现很好,但在验证数据上表现不佳,说明模型可能过度拟合了训练数据中的噪声或特定模式。

欠拟合

训练误差和测试误差都高。模型在训练数据和测试数据上的表现都不好,说明模型可能太简单,无法捕捉到数据中的复杂模式。

2 解决欠拟合

  1. 增加模型复杂度:引入更多的参数、增加神经网络的层数或节点数量,使模型能够捕捉到数据中的复杂模式。

  2. 增加特征:通过特征工程添加更多有意义的特征,使模型能够更好地理解数据。

  3. 减少正则化强度:适当减小 L1、L2 正则化强度,允许模型有更多自由度来拟合数据。

  4. 训练更长时间:如果是因为训练不足导致的欠拟合,可以增加训练的轮数或时间。

3 解决过拟合

避免模型参数过大

1 L2正则化

在损失函数中添加权重参数的平方和来实现,目标是惩罚过大的参数值。

1.数学表示

$$
设损失函数为 L(\theta),其中 \theta 表示权重参数,加入L2正则化后的损失函数表示为:L_{\text{total}}(\theta) = L(\theta) + \lambda \cdot \frac{1}{2} \sum_{i} \theta_i^2
$$

$$
L(\theta) 是原始损失函数(比如均方误差、交叉熵等);\lambda 是正则化强度,控制正则化的力度。
$$

$$
\theta_i 是模型的第 i 个权重参数;\frac{1}{2} \sum_{i} \theta_i^2 是所有权重参数的平方和,称为 L2 正则化项。
$$

L2 正则化会惩罚权重参数过大的情况,通过参数平方值对损失函数进行约束。

2.梯度更新

$$
\theta_{t+1} = \theta_t - \eta \left( \nabla L(\theta_t) + \lambda \theta_t \right)
$$

$$
\eta是学习率;\nabla L(\theta_t)是损失函数关于参数 \theta_t 的梯度;\lambda \theta_t 是 L2 正则化项的梯度,对应的是参数值本身的衰减。
$$

参数越大惩罚力度就越大,从而让参数逐渐趋向于较小值,避免出现过大的参数。

3.作用
  1. 防止过拟合:当模型过于复杂、参数较多时,模型会倾向于记住训练数据中的噪声,导致过拟合。L2 正则化通过抑制参数的过大值,使得模型更加平滑,降低模型对训练数据噪声的敏感性。

  2. 限制模型复杂度:L2 正则化项强制权重参数尽量接近 0,避免模型中某些参数过大,从而限制模型的复杂度。通过引入平方和项,L2 正则化鼓励模型的权重均匀分布,避免单个权重的值过大。

  3. 提高模型的泛化能力:正则化项的存在使得模型在测试集上的表现更加稳健,避免在训练集上取得极高精度但在测试集上表现不佳。

  4. 平滑权重分布:L2 正则化不会将权重直接变为 0,而是将权重值缩小。这样模型就更加平滑的拟合数据,同时保留足够的表达能力。

4.代码实现

2 L1正则化

在损失函数中添加权重参数的绝对值之和来约束模型的复杂度。

1.数学表示

$$
设模型的原始损失函数为 L(\theta),其中 \theta 表示模型权重参数,则加入 L1 正则化后的损失函数表示为:L_{\text{total}}(\theta) = L(\theta) + \lambda \sum_{i} |\theta_i|
$$

$$
L(\theta) 是原始损失函数;\lambda 是正则化强度,控制正则化的力度;|\theta_i| 是模型第i个参数的绝对值。
$$

$$
\sum_{i} |\theta_i| 是所有权重参数的绝对值之和,这个项即为 L1 正则化项。
$$

2.梯度更新

$$
\theta_{t+1} = \theta_t - \eta \left( \nabla L(\theta_t) + \lambda \cdot \text{sign}(\theta_t) \right)
$$

$$
\eta是学习率;\nabla L(\theta_t) 是损失函数关于参数 \theta_t 的梯度。
$$

$$
\text{sign}(\theta_t) 是参数 \theta_t 的符号函数,表示当 \theta_t 为正时取值为 1,为负时取值为 -1,等于 0 时为 0。
$$

L1 正则化依赖于参数的绝对值,其梯度更新时不是简单的线性缩小,而是通过符号函数来直接调整参数的方向。这就是为什么 L1 正则化能促使某些参数完全变为 0。

3.作用
  1. 稀疏性:L1 正则化的一个显著特性是它会促使许多权重参数变为 。这是因为 L1 正则化倾向于将权重绝对值缩小到零,使得模型只保留对结果最重要的特征,而将其他不相关的特征权重设为零,从而实现 特征选择 的功能。

  2. 防止过拟合:通过限制权重的绝对值,L1 正则化减少了模型的复杂度,使其不容易过拟合训练数据。相比于 L2 正则化,L1 正则化更倾向于将某些权重完全移除,而不是减小它们的值。

  3. 简化模型:由于 L1 正则化会将一些权重变为零,因此模型最终会变得更加简单,仅依赖于少数重要特征。这对于高维度数据特别有用,尤其是在特征数量远多于样本数量的情况下。

  4. 特征选择:因为 L1 正则化会将部分权重置零,因此它天然具有特征选择的能力,有助于自动筛选出对模型预测最重要的特征。

4.L1和L2对比
  • L1 正则化 更适合用于产生稀疏模型,会让部分权重完全为零,适合做特征选择。

  • L2 正则化 更适合平滑模型的参数,避免过大参数,但不会使权重变为零,适合处理高维特征较为密集的场景。

5.代码实现

3 Dropout

Dropout 是一种在训练过程中随机丢弃部分神经元的技术。它通过减少神经元之间的依赖来防止模型过于复杂,从而避免过拟合。

1.基本实现

Dropout过程:

  1. 按照指定的概率把部分神经元的值设置为0;

  2. 为了规避该操作带来的影响,需对非 0 的元素使用缩放因子1/(1-p)进行强化。

    import torch
    def test01():
        x=torch.tensor([[1,2,3,1,2,3,1,2,3,1]],dtype=torch.float32)
        drop=torch.nn.Dropout(0.7)
        x=drop(x)
        print(x)
        print(x.shape)
        print(sum(sum(x!=0))/x.shape[1])
    ​
    def test02():
        x=torch.tensor([[1,2,3,1,2,3,1,2,3,1]],dtype=torch.float32)
        w=torch.tensor([[1,2,3,4,5,6,7,8,9,10]],dtype=torch.float32)
        drop=torch.nn.Dropout(0.5)
        x=drop(x)
        out=x*w
        loss=out.sum()
        print(out)
        out.backward()
        print(w.grad)
    ​
    if __name__ == "__main__":
        test01()

2.权重影响

如果所有的数据的对应特征都为0,则参数梯度为0

4 简化模型

  • 减少网络层数和参数: 通过减少网络的层数、每层的神经元数量或减少卷积层的滤波器数量,可以降低模型的复杂度,减少过拟合的风险。

  • 使用更简单的模型: 对于复杂问题,使用更简单的模型或较小的网络架构可以减少参数数量,从而降低过拟合的可能性。

5 数据增强

通过对训练数据进行各种变换(如旋转、裁剪、翻转、缩放等),可以增加数据的多样性,提高模型的泛化能力。

6 早停

早停是一种在训练过程中监控模型在验证集上的表现,并在验证误差不再改善时停止训练的技术。这样可避免训练过度,防止模型过拟合。

7 模型集成

通过将多个不同模型的预测结果进行集成,可以减少单个模型过拟合的风险。常见的集成方法包括投票法、平均法和堆叠法。

8 交叉验证

使用交叉验证技术可以帮助评估模型的泛化能力,并调整模型超参数,以防止模型在训练数据上过拟合。

这些方法可以单独使用,也可以结合使用,以有效地防止参数过大和过拟合。根据具体问题和数据集的特点,选择合适的策略来优化模型的性能。


http://www.kler.cn/a/414458.html

相关文章:

  • 微信小程序数据请求教程:GET与POST请求详解
  • Leetcode:349. 两个数组的交集
  • AI智算-正式上架GPU资源监控概览 Grafana Dashboard
  • ESP32 wifi smartConfig 配网时密码错误导致一直死循环问题解决
  • Unity shaderlab 实现LineSDF
  • 【mac】终端左边太长处理,自定义显示名称(terminal路径显示特别长)
  • PostgreSQL在Linux环境下的常用命令总结
  • 爬虫获取的数据如何用于市场分析?
  • vue3+vite使用vite-plugin-electron-renderer插件和script-loader插件有冲突
  • 安全基线检查
  • 上传镜像docker hub登不上和docker desktop的etx4.vhdx占用空间很大等解决办法
  • 【分布式】Redis分布式锁
  • C# Winform--ffmpeg图片合成视频以及参数设置
  • GPT视角下,如何在密码学研究中找到属于你的方向?
  • 大模型的认知记录:一次与4o讨论道德经的对话 - “我无法触碰“真实的花草树木”(无名),但通过语言(有名),我可以靠近人类的认知方式。”
  • Redis(配置文件属性解析)
  • vue3 使用XLSX导出
  • openssl编译安装升级为新版本
  • burpsuite(2)最新版burpsuite安装教程
  • k8s集群部署metrics-server
  • 浅谈网络 | 应用层之HTTP协议
  • 微知-git如何添加空目录的几种方式?(.gitkeep, githook, gitconfig)
  • 1.1 STM32_GPIO_基本知识
  • 【释放算力潜能】基于华为鲲鹏920 + 昇腾310B的VPX架构主板
  • 数学建模中的10大经典模型及其实际应用:从入门到精通!(一)线性规划模型
  • 【FPGA】UART串口通信