首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

【深度学习入门】正则化

  • 25-02-16 04:41
  • 4515
  • 8090
blog.csdn.net

文章目录

  • 偏差与方差
    • 1 数据集划分
    • 2 偏差与方差的意义
    • 3 解决方法
  • 2 正则化(Regularization)
    • 1 逻辑回归的L1与L2正则化
    • 2 正则化项的理解
    • 3 神经网络中的正则化
    • 4 正则化为什么能够防止过拟合
  • 3 Dropout正则化
    • 1 Inverted droupout
    • 2 droupout为什么有效总结
  • 4 其它正则化方法
    • 1 早停止法(Early Stopping)
    • 2 数据增强
  • 5 总结
  • 完整代码

偏差与方差

1 数据集划分

首先我们对机器学习当中涉及到的数据集划分进行一个简单的复习

  • 训练集(train set):用训练集对算法或模型进行训练过程;
  • 验证集(development set):利用验证集(又称为简单交叉验证集,hold-out cross validation set)进行交叉验证,选择出最好的模型;
  • 测试集(test set):最后利用测试集对模型进行测试,对学习方法进行评估。

在小数据量的时代,如 100、1000、10000 的数据量大小,可以将数据集按照以下比例进行划分:

  • 无验证集的情况:70% / 30%
  • 有验证集的情况:60% / 20% / 20%

而在如今的大数据时代,拥有的数据集的规模可能是百万级别的,所以验证集和测试集所占的比重会趋向于变得更小。

  • 100 万数据量:98% / 1% / 1%
  • 超百万数据量:99.5% / 0.25% / 0.25%

以上这些比例可以根据数据集情况选择。

2 偏差与方差的意义

**“偏差-方差分解”(bias-variance decomposition)**是解释学习算法泛化性能的一种重要工具。

泛化误差可分解为偏差、方差与噪声,泛化性能是由学习算法的能力、数据的充分性以及学习任务本身的难度所共同决定的。

  • 偏差:度量了学习算法的期望预测与真实结果的偏离程度,即刻画了学习算法本身的拟合能力
  • 方差:度量了同样大小的训练集的变动所导致的学习性能的变化,即刻画了数据扰动所造成的影响
  • 噪声:表达了在当前任务上任何学习算法所能够达到的期望泛化误差的下界,即刻画了学习问题本身的难度。

那么偏差、方差与我们的数据集划分到底有什么关系呢?

  • 1、训练集的错误率较小,而验证集/测试集的错误率较大,说明模型存在较大方差,可能出现了过拟合
  • 2、训练集和测试集的错误率都较大,且两者相近,说明模型存在较大偏差,可能出现了欠拟合
  • 3、训练集和测试集的错误率都较小,且两者相近,说明方差和偏差都较小,这个模型效果比较好。

所以我们最终总结,方差一般指的是数据模型得出来了,能不能对未知数据的扰动预测准确。而偏差说明在训练集当中就已经误差较大了,基本上在测试集中没有好的效果。

所以如果我们的模型出现了较大的方差或者同时也有较大的偏差,该怎么去解决?

3 解决方法

对于高方差(过拟合),有以下几种方式:

  • 获取更多的数据,使得训练能够包含所有可能出现的情况
  • 正则化(Regularization)
  • 寻找更合适的网络结构

对于高偏差(欠拟合),有以下几种方式:

  • 扩大网络规模,如添加隐藏层或者神经元数量
  • 寻找合适的网络架构,使用更大的网络结构,如AlexNet
  • 训练时间更长一些

不断尝试,直到找到低偏差、低方差的框架。

2 正则化(Regularization)

正则化,即在损失函数中加入一个正则化项(惩罚项),惩罚模型的复杂度,防止网络过拟合

1 逻辑回归的L1与L2正则化

逻辑回归的参数W数量根据特征的数量而定,那么正则化如下

  • 逻辑回归的损失函数中增加L2正则化
    J ( w , b ) = 1 m ∑ i = 1 m L ( y ^ ( i ) , y ( i ) ) + λ 2 m ∣ ∣ w ∣ ∣ 2 2 J(w,b) = \frac{1}{m}\sum_{i=1}^mL(\hat{y}^{(i)},y^{(i)})+\frac{\lambda}{2m}{||w||}^2_2 J(w,b)=m1​i=1∑m​L(y^​(i),y(i))+2mλ​∣∣w∣∣22​

其中的L2范数可以理解:
λ 2 m ∣ ∣ w ∣ ∣ 2 2 = λ 2 m ∑ j = 1 n x w j 2 = λ 2 m w T w \frac{\lambda}{2m}{||w||}^2_2=\frac{\lambda}{2m}\sum_{j=1}^{n_x}w^2_j = \frac{\lambda}{2m}w^Tw 2mλ​∣∣w∣∣22​=2mλ​j=1∑nx​​wj2​=2mλ​wTw

解释:所有w参数的平方和的结果

  • 逻辑回归的损失函数中增加L1正则化
    J ( w , b ) = 1 m ∑ i = 1 m L ( y ^ ( i ) , y ( i ) ) + λ 2 m ∣ ∣ w ∣ ∣ 1 J(w,b) = \frac{1}{m}\sum{i=1}^mL(\hat{y}^{(i)},y^{(i)}) + \frac{\lambda}{2m}{||w||}_1 J(w,b)=m1​∑i=1mL(y^​(i),y(i))+2mλ​∣∣w∣∣1​

其中L1范数可以理解为:
λ 2 m ∣ ∣ w ∣ ∣ 1 = λ 2 m ∑ j = 1 n x ∣ w j ∣ \frac{\lambda}{2m}{||w||}1 = \frac{\lambda}{2m}\sum_{j=1}^{n_x}{|w_j|} 2mλ​∣∣w∣∣1=2mλ​j=1∑nx​​∣wj​∣

注:其中,λ 为正则化因子,是超参数。由于 L1 正则化最后得到 w 向量中将存在大量的 0,使模型变得稀疏化,因此 L2 正则化更加常用。

2 正则化项的理解

在损失函数中增加一项,那么其实梯度下降是要减少损失函数的大小,对于L2或者L1来讲都是要去减少这个正则项的大小,那么也就是会减少W权重的大小。这是我们一个直观上的感受。

  • 接下来我们通过方向传播来理解这个其中的L2,对于损失函数我们要反向传播求参数梯度:

(1)
d W = ∂ L ∂ w + λ m W dW = \frac{\partial L}{\partial w}+ \frac{\lambda}{m} {W} dW=∂w∂L​+mλ​W
前面的默认损失函数的梯度计算结果默认为backprop,那么更新的参数就为

(2)
W : = W − α d W W := W - \alpha dW W:=W−αdW
那么我们将第一个公式带入第二个得到

− − > W : = W − α ( ∂ L ∂ w + λ m W ) -->W := W - \alpha(\frac{\partial L}{\partial w} + \frac{\lambda}{m}W) −−>W:=W−α(∂w∂L​+mλ​W)

− − > = W − α λ m W − α ∗ ∂ L ∂ w -->=W - \frac{\alpha \lambda}{m}W - \alpha*\frac{\partial L}{\partial w} −−>=W−mαλ​W−α∗∂w∂L​

所以每次更新的时候都会让
W ( 1 − α λ m ) W(1 - \frac{\alpha \lambda}{m}) W(1−mαλ​)
,这个系数永远小于1,所以我们通常称L2范数为权重衰减。

3 神经网络中的正则化

神经网络中的正则化与逻辑回归相似,只不过参数W变多了,每一层都有若干个权重,可以理解成一个矩阵

我们把w[l]理解某一层神经元的权重参数,其中这是加入了L2范数,可以是

∥ w [ l ] ∥ F 2 = ∑ n [ l − 1 ] i = 1 ∑ n [ l ] j = 1 ( w i j [ l ] ) 2 {

∥w[l]∥‖w[l]‖
}^2_F = \sum^{n^{[l-1]}}{i=1}\sum^{n^{[l]}}{j=1}(w^{[l]}_{ij})^2 ​w[l]​ ​F2​=∑n[l−1]​i=1∑n[l]​j=1(wij[l]​)2
对于矩阵的L2范数,有个专业名称叫弗罗贝尼乌斯范数(Frobenius Norm)

4 正则化为什么能够防止过拟合

正则化因子设置的足够大的情况下,为了使成本函数最小化,权重矩阵 W 就会被设置为接近于 0 的值,直观上相当于消除了很多神经元的影响,那么大的神经网络就会变成一个较小的网络。
在这里插入图片描述

在加入正则化项后,当λ增大,导致
W [ l ] W^[l] W[l]
减小,
Z [ l ] = W [ l ] a [ l − 1 ] + b [ l ] Z^{[l]} = W^{[l]}a^{[l-1]} + b^{[l]} Z[l]=W[l]a[l−1]+b[l]
便会减小。由上图可知,在 z 较小(接近于 0)的区域里,函数近似线性,所以每层的函数就近似线性函数,整个网络就成为一个简单的近似线性的网络,因此不会发生过拟合。

3 Dropout正则化

Droupout论文地址:http://jmlr.org/papers/volume15/srivastava14a.old/srivastava14a.pdf
在这里插入图片描述

Droupout:随机的对神经网络每一层进行丢弃部分神经元操作。

在这里插入图片描述

对于网络的每一层会进行设置保留概率,即keep_prob。假设keep_prob为0.8,那么也就是在每一层所有神经元有20% 的概率直接失效,可以理解为0.

1 Inverted droupout

这种方式会对每层进行如下代码操作

# 假设设置神经元保留概率
keep_prob = 0.8
# 随机建立一个标记1 or 0的矩阵,表示随机失活的单元,占比20%
dl = np.random.rand(al.shape[0], al.shape[1]) < keep_prob
# 让a1对应d1的为0地方结果为0
al = np.multiply(al, dl)

# 为了测试的时候,每一个单元都参与进来
al /= keep_prob
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 训练练的时候只有占比为pp的隐藏层单元参与训练。
  • 增加最后一行代码的原因,在预测的时候,所有的隐藏层单元都需要参与进来,就需要测试的时候将输出结果除以以pp使下一层的输入规模保持不变。

假设keep_prob=p=0.8

z l = w l a l − 1 + b l z^{l}=w^{l}a^{l-1}+b^{l} zl=wlal−1+bl
,当 l − 1 l-1 l−1层有比例为 1 − p = 0.2 1-p=0.2 1−p=0.2单元drop后, a l − 1 a l − 1 a^{l-1}al−1 al−1al−1大约会变为原来的80%,为了保证ll层的zz值期望(可以理解为均值)不变,所以要在 a l − 1 a^{l-1} al−1与dropout矩阵乘积后的权重进行扩大,要乘以$\frac{1}{p}=10/8 $(增大)

注:原始:(1+1+1+1+1+1+1+1+1+1)/10 = 1,现在其中20%失效,则平均值为0.8,所以0.8 * (10/8) = 1. 相当于其中8个神经元参数增大了(10/8)倍

2 droupout为什么有效总结

加入了 dropout 后,输入的特征都存在被随机清除的可能,所以该神经元不会再特别依赖于任何一个输入特征,也就是不会给任何一个输入特征设置太大的权重。通过传播过程,dropout 将产生和 L2 正则化相同的收缩权重的效果。

  • 对于不同的层,设置的keep_prob大小也不一致,神经元较少的层,会设keep_prob为 1.0,而神经元多的层则会设置比较小的keep_prob
  • 通常被使用在计算机视觉领域,图像拥有更多的特征,场景容易过拟合,效果被实验人员证明是很不错的。

调试时候使用技巧:

  • dropout 的缺点是损失函数无法被明确定义,因为每次会随机消除一部分神经元,所以参数也无法确定具体哪一些,在反向传播的时候带来计算上的麻烦,也就无法保证当前网络是否损失函数下降的。如果要使用droupout,会先关闭这个参数,保证损失函数是单调下降的,确定网络没有问题,再次打开droupout才会有效。

4 其它正则化方法

  • 早停止法(Early Stopping)
  • 数据增强

1 早停止法(Early Stopping)

通常我们在训练验证的时候,发现过拟合。可以得到下面这张损失图

在这里插入图片描述

通常不断训练之后,损失越来越小。但是到了一定之后,模型学到的过于复杂(过于拟合训练集上的数据的特征)造成测试集开始损失较小,后来又变大。模型的w参数会越来越大,那么可以在测试集损失减小一定程度之后停止训练。

但是这种方法治标不治本,得从根本上解决数据或者网络的问题。

2 数据增强

  • 数据增强

指通过剪切、旋转/反射/翻转变换、缩放变换、平移变换、尺度变换、对比度变换、噪声扰动、颜色变换等一种或多种组合数据增强变换的方式来增加数据集的大小。

在这里插入图片描述

即使卷积神经网络被放在不同方向上,卷积神经网络对平移、视角、尺寸或照度(或以上组合)保持不变性,都会认为是一个物体。

  • 为什么这样做?

在这里插入图片描述

假设数据集中的两个类。左边的代表品牌A(福特),右边的代表品牌B(雪佛兰)。

假设完成了训练,并且输入下面的图像(品牌A),但是你的神经网络输出认为它是品牌B的汽车!

在这里插入图片描述

为什么会发生这种现象? 因为算法可能会寻找区分一个类和另一个类的最明显特征。在这个例子中 ,这个特征就是所有品牌A的汽车朝向左边,所有品牌B的汽车朝向右边。神经网络的好坏取决于输入的数据。

怎么解决这个问题?

我们需要减少数据集中不相关特征的数量。对上面的汽车类型分类器来说,你只需要将现有的数据集中的照片水平翻转,使汽车朝向另一侧。现在,用新的数据集训练神经网络,通过过增强数据集,可以防止神经网络学习到不相关的模式,提升效果。(在没有采集更多的图片前提下)

  • 数据增强类别

那么我们应该在机器学习过程中的什么位置进行数据增强?在向模型输入数据之前增强数据集。

  • 离线增强。预先进行所有必要的变换,从根本上增加数据集的规模(例如,通过翻转所有图像,保存后数据集数量会增加2倍)。
  • 在线增强,或称为动态增强。可通过对即将输入模型的小批量数据的执行相应的变化,这样同一张图片每次训练被随机执行一些变化操作,相当于不同的数据集了。

那么我们的代码中也是进行这种在线增强。

  • 数据增强技术

下面一些方法基础但功能强大的增强技术,目前被广泛应用。

  • 翻转:tf.image.random_flip_left_right
    • 你可以水平或垂直翻转图像。一些架构并不支持垂直翻转图像。但,垂直翻转等价于将图片旋转180再水平翻转。下面就是图像翻转的例子。

在这里插入图片描述

                  从左侧开始分别是:原始图像,水平翻转图像,垂直翻转图像
  • 1
  • 旋转:rotate

在这里插入图片描述

                                      从左到右,图像相对于前一个图像顺时针旋转90度
  • 1
  • 剪裁:random_crop
    • 随机从原始图像中采样一部分,然后将这部分图像调整为原始图像大小。这个方法更流行的叫法是随机裁剪。

在这里插入图片描述

                 从左侧开始分别为:原始图像,从左上角裁剪出一个正方形部分,然后从右下角裁剪出一个正方形部分。剪裁的部分被调整为原始图像大小。
  • 1
  • 平移、缩放等等方法

数据增强的效果是非常好的,比如下面的例子,绿色和粉色表示没有数据增强之前的损失和准确率效果,红色和蓝色表示数据增强之后的损失和准确率结果,可以看到学习效果也改善较快。

在这里插入图片描述

那么TensorFlow 官方源码都是基于 vgg与inception论文的图像增强介绍,全部通过tf.image相关API来预处理图像。并且提供了各种封装过tf.image之后的API。那么TensorFlow 官网也给我们提供了一些模型的数据增强过程。

5 总结

  • 掌握偏差与方差的意义
  • 掌握L2正则化与L1正则化的数学原理
    • 权重衰减
  • 掌握droupout原理以及方法
    • Inverted droupout
  • 知道正则化的作用

完整代码

import numpy as np
import matplotlib.pyplot as plt

from utils import initialize_parameters, compute_cost, forward_propagation, backward_propagation, update_parameters
from utils import load_dataset, predict, sigmoid, relu


# -------
# 1、带有正则化的计算损失函数
# 2、正则化后的反向传播计算
# -------
def compute_cost_with_regularization(A3, Y, parameters, lambd):
    """
    损失函数中增加L2正则化
    """
    m = Y.shape[1]
    W1 = parameters["W1"]
    W2 = parameters["W2"]
    W3 = parameters["W3"]

    # 计算交叉熵损失
    cross_entropy_cost = compute_cost(A3, Y)

    # 开始
    L2_regularization_cost = (1. / m) * (lambd / 2) * \
                             (np.sum(np.square(W1)) + np.sum(np.square(W2)) + np.sum(np.square(W3)))

    cost = cross_entropy_cost + L2_regularization_cost
    # 结束

    return cost


def backward_propagation_with_regularization(X, Y, cache, lambd):
    """
    对增加了L2正则化后的损失函数进行反向传播计算
    """

    m = X.shape[1]
    (Z1, A1, W1, b1, Z2, A2, W2, b2, Z3, A3, W3, b3) = cache

    dZ3 = A3 - Y

    dW3 = 1. / m * (np.dot(dZ3, A2.T) + lambd * W3)
    db3 = 1. / m * np.sum(dZ3, axis=1, keepdims=True)

    dA2 = np.dot(W3.T, dZ3)
    dZ2 = np.multiply(dA2, np.int64(A2 > 0))
    dW2 = 1. / m * (np.dot(dZ2, A1.T) + lambd * W2)
    db2 = 1. / m * np.sum(dZ2, axis=1, keepdims=True)

    dA1 = np.dot(W2.T, dZ2)
    dZ1 = np.multiply(dA1, np.int64(A1 > 0))
    dW1 = 1. / m * (np.dot(dZ1, X.T) + lambd * W1)
    db1 = 1. / m * np.sum(dZ1, axis=1, keepdims=True)

    gradients = {"dZ3": dZ3, "dW3": dW3, "db3": db3, "dA2": dA2,
                 "dZ2": dZ2, "dW2": dW2, "db2": db2, "dA1": dA1,
                 "dZ1": dZ1, "dW1": dW1, "db1": db1}

    return gradients

# ----------
# 1、droupout前向传播过程
# 2、droupout反向传播过程
# ----------

def forward_propagation_with_dropout(X, parameters, keep_prob=0.5):
    """
    带有dropout的前向传播
    """

    np.random.seed(1)

    W1 = parameters["W1"]
    b1 = parameters["b1"]
    W2 = parameters["W2"]
    b2 = parameters["b2"]
    W3 = parameters["W3"]
    b3 = parameters["b3"]

    # LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SIGMOID
    # 计算第一层输出
    Z1 = np.dot(W1, X) + b1
    A1 = relu(Z1)

    # 开始
    # 初始化一个矩阵
    D1 = np.random.rand(A1.shape[0], A1.shape[1])
    # 标记为0和1
    D1 = D1 < keep_prob
    # 对于A1中的部分结果丢弃
    A1 = np.multiply(A1, D1)
    # 保持原来的期望值
    A1 /= keep_prob
    # 结束

    # 计算第二层输出
    Z2 = np.dot(W2, A1) + b2
    A2 = relu(Z2)

    # 开始
    D2 = np.random.rand(A2.shape[0], A2.shape[1])
    D2 = D2 < keep_prob
    A2 = np.multiply(A2, D2)
    A2 /= keep_prob
    # 结束

    # 最后一层输出
    Z3 = np.dot(W3, A2) + b3
    A3 = sigmoid(Z3)

    cache = (Z1, D1, A1, W1, b1, Z2, D2, A2, W2, b2, Z3, A3, W3, b3)

    return A3, cache


def backward_propagation_with_dropout(X, Y, cache, keep_prob):
    """
    droupout的反向传播
    """

    m = X.shape[1]
    (Z1, D1, A1, W1, b1, Z2, D2, A2, W2, b2, Z3, A3, W3, b3) = cache

    dZ3 = A3 - Y
    dW3 = 1. / m * np.dot(dZ3, A2.T)
    db3 = 1. / m * np.sum(dZ3, axis=1, keepdims=True)
    dA2 = np.dot(W3.T, dZ3)
    dA2 = np.multiply(dA2, D2)
    dA2 /= keep_prob

    dZ2 = np.multiply(dA2, np.int64(A2 > 0))
    dW2 = 1. / m * np.dot(dZ2, A1.T)
    db2 = 1. / m * np.sum(dZ2, axis=1, keepdims=True)

    dA1 = np.dot(W2.T, dZ2)
    dA1 = np.multiply(dA1, D1)
    dA1 /= keep_prob

    dZ1 = np.multiply(dA1, np.int64(A1 > 0))
    dW1 = 1. / m * np.dot(dZ1, X.T)
    db1 = 1. / m * np.sum(dZ1, axis=1, keepdims=True)

    gradients = {"dZ3": dZ3, "dW3": dW3, "db3": db3, "dA2": dA2,
                 "dZ2": dZ2, "dW2": dW2, "db2": db2, "dA1": dA1,
                 "dZ1": dZ1, "dW1": dW1, "db1": db1}

    return gradients


def model(X, Y, learning_rate=0.3, num_iterations=30000, lambd=0, keep_prob=1):
    """
    使用三层网络,激活函数为:LINEAR->RELU->LINEAR->RELU->LINEAR->SIGMOID.
    第一个隐层:20个神经元
    第二个隐层:3个神经元
    输出层:1个神经元
    """

    grads = {}
    costs = []
    m = X.shape[1]
    layers_dims = [X.shape[0], 20, 3, 1]

    # 初始化网络参数
    parameters = initialize_parameters(layers_dims)

    # 梯度下降循环逻辑
    for i in range(0, num_iterations):

        # 前向传播计算
        # LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SIGMOID.
        # 如果keep_prob=1,进行正常前向传播
        # 如果keep_prob<1,说明需要进行droupout计算
        if keep_prob == 1:
            a3, cache = forward_propagation(X, parameters)
        elif keep_prob < 1:
            a3, cache = forward_propagation_with_dropout(X, parameters, keep_prob)

        # 计算损失
        # 如果传入lambd不为0,判断加入正则化
        if lambd == 0:
            cost = compute_cost(a3, Y)
        else:
            cost = compute_cost_with_regularization(a3, Y, parameters, lambd)

        # 只允许选择一个,要么L2正则化,要么Droupout
        assert (lambd == 0 or keep_prob == 1)

        if lambd == 0 and keep_prob == 1:
            grads = backward_propagation(X, Y, cache)
        elif lambd != 0:
            grads = backward_propagation_with_regularization(X, Y, cache, lambd)
        elif keep_prob < 1:
            grads = backward_propagation_with_dropout(X, Y, cache, keep_prob)

        # 更新参数
        parameters = update_parameters(parameters, grads, learning_rate)

        # 每10000词打印损失结果
        if i % 10000 == 0:
            print("迭代次数为 {}: 损失结果大小:{}".format(i, cost))
            costs.append(cost)

    # 画出损失变化结果图
    plt.plot(costs)
    plt.ylabel('损失')
    plt.xlabel('迭代次数')
    plt.title("损失变化图,学习率为" + str(learning_rate))
    plt.show()

    return parameters


if __name__ == '__main__':

    train_X, train_Y, test_X, test_Y = load_dataset()

    parameters = model(train_X, train_Y, keep_prob=0.86)

    print("训练集的准确率:")
    predictions_train = predict(train_X, train_Y, parameters)

    print("测试集的准确率:")
    predictions_test = predict(test_X, test_Y, parameters)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
注:本文转载自blog.csdn.net的道友老李的文章"https://blog.csdn.net/u014608435/article/details/144405225"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2491) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top