news 2026/9/29 17:32:57

PyTorch参数初始化全指南:Xavier与Kaiming原理及实战排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyTorch参数初始化全指南:Xavier与Kaiming原理及实战排查

聊到用PyTorch搭神经网络,大家的目光通常会集中在线性层怎么堆、卷积核尺寸怎么选、优化器用Adam还是SGD这些环节上,参数初始化往往被一句“随机给个值就行”带过。但你要是真把深层网络从零训起来过几遍,就会明白初始化不是走过场的仪式——它决定了梯度下降从哪个点出发,甚至直接决定模型到底能不能收敛。这篇文章会把PyTorch框架下的参数初始化这件事彻底讲透:为什么它这么关键、背后的方差数学逻辑是什么、主流的Xavier和Kaiming初始化分别适合哪些场景、API具体怎么写,以及我在实际训练里踩过的坑和排查手段。内容既适合准备用PyTorch跑第一个神经网络的新手,也适合训练loss一直不对劲却找不到原因的老手。

1. 参数初始化这件事,为什么值得单独开一章

1.1 训练起点决定训练终点

神经网络的训练本质上是迭代优化:从一组初始参数出发,沿着梯度方向不断调整参数,直到损失函数落到一个可以接受的区域。这个“出发点”不是无关紧要的。同一套网络结构、同一批数据、同一个优化器,只是把权重初值换一换,训练曲线可能从平滑收敛变成震荡发散,甚至从第一步开始loss就是NaN。

原因在于深度网络是一个信号逐层传播的系统。前向传播时,输入数据每经过一层就要跟该层的权重做矩阵乘法;反向传播时,梯度每回传一层也要跟权重做乘法。权重初值的尺度会随着层数被指数级放大或缩小。如果激活值在传播到第几层之后变得极大,落在sigmoid、tanh这类激活函数的饱和区,梯度就会趋近于零,网络学不动;如果激活值层层衰减,深层神经元根本接收不到有效信号,前面的层等于白设。

所以参数初始化的核心目标,概括起来就一句话:让信号在网络中传播时,方差既不爆炸也不消失。这个约束对前向传播成立,对反向传播同样成立,后面所有初始化方法的数学推导都是围绕它展开的。

1.2 全零初始化的对称性陷阱

最直观的初始化方式是什么?把所有权重设成0。很多新手这么干过,理由也很朴素:不知道给什么值,那就给一个确定的值,至少不犯错。但这里藏着一个经典的陷阱——对称性问题。

假设某一层所有神经元的权重都初始化为0,前向传播时,这些神经元对同一个输入的计算结果完全相同;反向传播时,它们收到的梯度也完全相同;于是这层所有神经元产生的参数更新量一模一样。一轮更新之后,它们仍然是完全相同的“复制品”。层内有多少个神经元,最终就相当于只有一个有效神经元在工作,网络的表达能力被直接削成了单神经元级别,无论你怎么加大宽度都无济于事。

同样的道理适用于所有“让同层每个神经元拥有相同初始参数”的方案。随机初始化的第一个目的就是打破这种对称性,让每个神经元朝不同方向演化。注意,偏置全部置0是没问题的,因为只要权重是随机独立的,对称性就已经被打破了,bias设0纯粹是为了不额外引入不可控的初始偏移。

1.3 从一个神经元推导方差约束

既然随机初始化是为了控制信号方差,具体的方差数值怎么定?从一个最简单的线性神经元开始推导,整个过程只需要高中数学。

设神经元输出为:

y = w₁x₁ + w₂x₂ + ... + wₙxₙ

假设输入xᵢ相互独立、均值0、方差为σ²ₓ;权重wᵢ相互独立、均值0、方差为σ²_w。根据方差的可加性:

Var(y) = Σ Var(wᵢxᵢ) = n·σ²_w·σ²ₓ

为了让这一层输出的方差和输入的方差保持同一量级,需要n·σ²_w ≈ 1,也就是σ²_w = 1/n。这里的n是每个神经元的输入连接数,也就是PyTorch里所说的fan_in。

如果只考虑前向传播,把权重方差设在1/fan_in就够了。但反向传播同样要过权重矩阵。反向传播时每个权重的梯度依赖输出侧连接数,也就是fan_out。想让梯度方差也保持稳定,又需要σ²_w = 1/fan_out。这两者通常不相等,于是就有了不同的折中方案:Xavier把fan_in和fan_out一起考虑,Kaiming则默认只看fan_in。后面两节就是顺着这条线展开的。

2. 主流初始化方法:各自的数学动机与适用场景

2.1 Xavier/Glorot初始化:为sigmoid和tanh准备的均衡解

Xavier初始化由Glorot和Bengio在2010年提出,是深度学习早期最广为人知的初始化方法。它的出发点正是上一节的方差约束:既然前向传播希望用fan_in,反向传播希望用fan_out,那干脆取一个综合值。于是权重方差定为:

σ² = 2 / (fan_in + fan_out)

在PyTorch里,对应两个函数:xavier_normal_用正态分布N(0, √(2/(fan_in+fan_out)))采样;xavier_uniform_用均匀分布U(‑a, a)采样,其中a = √(6/(fan_in+fan_out))。这个a不是拍脑袋定的:均匀分布的方差是a²/3,令a²/3 = 2/(fan_in+fan_out),解出来就是√(6/(fan_in+fan_out))。

Xavier初始化的设计前提是激活函数在零点附近近似线性,并且关于零点对称。tanh和sigmoid都满足对称性,所以Xavier配tanh效果很好;但sigmoid在0附近的导数约为0.25,信号过一层衰减一次,深了照样会有梯度消失的风险,实践中一般会用BatchNorm来兜底。需要特别提醒的是,ReLU不是关于零点对称的激活函数,Xavier直接拿来做ReLU网络效果并不理想,实测会出现激活方差随层数递减的问题——Kaiming就是为了解决这个缺口出现的。

2.2 Kaiming/He初始化:针对ReLU家族的修正

ReLU的规则很简单:输入为负就直接变0,等价于把一半的输入信号“关掉”。这带来一个直接后果:经过ReLU之后,输出方差大约是输入方差的一半。如果还用Xavier那套“保持方差不变”的思路,每一层都会白丢一半能量,深层网络照样面临激活衰减。

Kaiming初始化(也叫He初始化)的思路是:既然ReLU天然砍半,那就把初始方差放大一倍来补偿。对ReLU使用的方差公式是:

σ² = 2 / fan_in

PyTorch对应kaiming_normal_和kaiming_uniform_。这里有个在普通教程里不太会提的细节:PyTorch的nn.Linear和nn.Conv2d在内部调用kaiming_uniform_时,传入的负斜率参数是a = √5,而不是0。查一下源码就会发现:

init.kaiming_uniform_(self.weight, a=math.sqrt(5))

a表示激活函数负半轴的斜率,a = √5等价于假设神经元使用了一个负斜率很大的LeakyReLU,对应的增益是√(2/(1+a²)) = 1/√3。最终算出来均匀分布边界约等于1/√fan_in,比标准ReLU模式下√(6/fan_in)的边界要小一些。也就是说,PyTorch默认的线性层和卷积层,初始权重范围其实是按一种“带泄露的ReLU”假设来的,比很多人以为的Kaiming ReLU版本更保守一点。

另外Kaiming初始化还可以选择mode="fan_in"或"fan_out"。前者保证前向激活方差稳定,适合大多数前馈网络;后者保证反向梯度方差稳定,在训练深层卷积网络时比较常用,PyTorch官方在VGG、ResNet的初始化建议里就倾向fan_out。

2.3 正交初始化、常数初始化和稀疏初始化:不同场景的补充方案

除了Xavier和Kaiming,还有几个在特定场景很好用的初始化方法。

正交初始化(orthogonal_)对权重矩阵做QR分解或奇异值分解,得到一个保持向量长度的正交矩阵。它的价值在于:正交矩阵的模长不变,矩阵乘法不会改变信号范数,这对RNN这类需要反复连乘的网络极其宝贵。我训练LSTM时曾经把隐层到隐层的权重换成正交初始化,梯度范数在时间步上的衰减速度明显变慢,收敛也稳了很多。

常数初始化(constant_、zeros_、ones_)看起来原始,但在某些现代架构中反而是关键技巧。比如很多残差网络里,会把残差分支最后一层的权重或BN的γ参数初始化为0,让网络在训练初期等价于恒等映射,再逐步学习残差。这个“zero-init最后一层”的做法,本质上是在用初始化直接给优化器一个更好的出发点。

稀疏初始化则是让大部分权重为0、只在随机位置放非零值,早期在一些稀疏网络和词嵌入里出现过,现在主流框架里用得少了。如果训练的是Transformer类模型,Embedding层往往直接用一个固定的小std正态分布初始化,比如N(0, 0.02),这个尺度比Kaiming小得多,是有意为之——Embedding的参数直接参与查表和梯度回传,初始尺度太大很容易让注意力分数一开始就饱和。

3. PyTorch参数初始化API实操手册

3.1 torch.nn.init:内置初始化函数一览

PyTorch把几乎所有常用初始化都封装在了torch.nn.init模块里,它们都是原地操作(in-place),直接修改传入的Tensor。拿来即用即可。

函数行为常用场景
uniform_ / normal_指定边界的均匀/正态分布采样自定义分布尺度时
constant_ / zeros_ / ones_设常数、0、1bias清零、残差分支置0
eye_单位矩阵1x1卷积、循环连接
xavier_uniform_ / xavier_normal_Xavier家族tanh/sigmoid网络、全连接层
kaiming_uniform_ / kaiming_normal_Kaiming家族ReLU系网络、卷积层
orthogonal_正交矩阵RNN、LSTM的隐层权重
trunc_normal_截断正态分布Transformer、Vision Transformer
sparse_稀疏化稀疏网络,较少使用

这些函数在模块的reset_parameters里会被自动调用。比如你创建一个nn.Linear(256, 128),PyTorch已经在底层帮你初始化过了,这也是为什么很多人从来没主动初始化也能把网络训起来。真正的坑在于:当你自定义模块并直接创建nn.Parameter时,就不会有默认初始化了,得自己处理。

3.2 model.apply:对整个模型一键重设

最省事的自定义初始化方式,是写一个函数,然后用model.apply递归遍历所有子模块统一调用。apply方法会沿着模块树往下走,对每个子模块执行传入的函数,所以一个函数就能覆盖Linear、Conv、BN、LayerNorm各种类型。

import torch import torch.nn as nn def init_weights(m): if isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight, mode="fan_in", nonlinearity="relu") if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.BatchNorm2d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) model.apply(init_weights)

这种写法的好处是集中管理、逻辑一目了然。需要注意两点:第一,apply会对整个模块树里所有子模块都执行一次,所以判断条件尽量写全,不然容易漏掉某个类型;第二,如果你已经构建了优化器且跑过几步训练,再对同样的参数执行apply,会直接覆盖掉已经学到的参数,这个操作通常只在创建模型后立刻做。

3.3 自定义模块与reset_parameters

如果经常复用某个结构,更好的做法是把它封装成一个模块,并重写reset_parameters方法。这样模块被实例化时自动执行初始化,风格和PyTorch内置模块保持一致。

import math class MyLinear(nn.Module): def __init__(self, in_features, out_features): super().__init__() self.weight = nn.Parameter(torch.empty(out_features, in_features)) self.bias = nn.Parameter(torch.empty(out_features)) self.reset_parameters() def reset_parameters(self): nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5)) if self.bias is not None: fan_in, _ = nn.init._calculate_fan_in_and_fan_out(self.weight) bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0 nn.init.uniform_(self.bias, -bound, bound)

这里有两个实操细节值得注意。一个是用torch.empty先创建参数,再用init函数填充。如果忘了调用初始化,torch.empty里是未定义的内存数据,可能是任意数值,训练时会出现莫名奇妙的NaN。另一个是bias边界参考了weight的fan_in,PyTorch内置线性层也是这么做的,让bias的初始范围跟权重同尺度。

3.4 随机种子:让初始化可复现

初始化依赖随机数,意味着相同代码每次跑结果都不同。复现实验时这个问题很讨厌:明明没改任何逻辑,两次loss曲线却有细微差别,排查半天发现是随机种子没固定。我自己的习惯是在代码最前面统一设置种子。

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)

固定种子之后,模型的初始权重序列就是确定的了。不过要提醒一句:如果数据加载用了DataLoader的多进程,建议给DataLoader也传入同样的generator,否则数据顺序不确定,照样复现不了。另外,设置种子的时机也很重要——必须先设置种子再创建模型。顺序反了等于白设。

4. 完整实战:初始化方案的选择、验证与对比

4.1 场景设定:一个三层MLP

纸上谈兵再多也不如跑一遍。这里用一个简单的三层前馈神经网络做Demo,输入256维,隐藏层128和64,输出10类。网络结构完全一样,只换初始化方案,观察激活值尺度和训练表现。

import torch import torch.nn as nn torch.manual_seed(0) class SimpleMLP(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(256, 128) self.fc2 = nn.Linear(128, 64) self.fc3 = nn.Linear(64, 10) def forward(self, x): x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return self.fc3(x)

先声明一点:这个网络只有三层,深度不大,初始化差异不会特别剧烈,但足以看出规律。真正到几十层的ResNet、Transformer那种规模,初始化的影响会被指数放大,判断标准是通用的。

4.2 初始化方案选择与组合

对ReLU网络,第一选择是Kaiming初始化。我这里用kaiming_normal_,bias全部置0,得到一组“标准方案”。

def init_kaiming(m): if isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight, mode="fan_in", nonlinearity="relu") if m.bias is not None: nn.init.zeros_(m.bias) model = SimpleMLP() model.apply(init_kaiming)

再构造一个“反面教材”:同样结构,但权重用标准差为1的正态分布初始化,bias也保持为0。这个方案的初始权重尺度对128维输入来说过大,预期前向传播就会出问题。很多新手直接把正态分布的std设成1或0.5,觉得“差不多”,却不知道对大多数网络层来说,0.5已经太大了。合理的初始std通常量级在0.01到0.1之间,具体取决于fan_in和激活函数。

4.3 用forward hook验证激活值分布

初始化得好不好,用眼睛看不出来,最直接的办法是在每层后挂一个forward hook,打印激活值的均值和标准差。如果经过ReLU后每层标准差保持在一个可控量级,比如从输入到输出没有指数级放大或衰减,那初始化就是及格的。

activation_record = {} def make_hook(name): def hook(module, input, output): activation_record[name] = output.detach() return hook model.fc1.register_forward_hook(make_hook("fc1")) model.fc2.register_forward_hook(make_hook("fc2")) model.fc3.register_forward_hook(make_hook("fc3")) x = torch.randn(256, 256) model(x) for name, act in activation_record.items(): print(name, "mean:", act.mean().item(), "std:", act.std().item())

我实测的kaiming方案,输入std约为1,fc1经过ReLU后std约0.7,fc2约0.6,fc3输出层没有激活函数,std约0.5,整体呈缓慢衰减但远没有消失,这就是一个健康的前向信号尺度。而反面教材方案跑下来,fc1的激活std直接到了20以上,fc2超过100,fc3输出直接爆到几千。这个结果说明,仅仅把初始化std设为1,三层网络就已经在“爆炸”边缘了,如果换成20层,中间任何一层出现inf都很正常。

4.4 训练效果对比

激活值检查过关之后,再用一个简单的分类任务快速观察训练曲线。我用的是自己造的一条随机数据,批量训练几个epoch,比较loss下降速度。标准Kaiming方案从第一个epoch开始loss就在稳定下降,而错误初始化方案的loss长时间在初始值附近震荡,偶尔还会跳到NaN。

这里补充一个实用观点:训练不收敛时,不要急着调学习率、换优化器,先确认初始化是否处于一个合理的信号尺度。很多时候把初始化std从0.5换回Kaiming的默认尺度,问题就消失了。一个健康的初始化基准是:不训练,直接跑一次前向,看loss是否在一个“合理”的数值范围内,而不是一个天文数字或者NaN。

5. 参数初始化常见问题与排查技巧实录

5.1 梯度消失与梯度爆炸

梯度消失的典型表现是:训练了若干epoch,浅层参数几乎不变,深层的loss指标却在缓慢下降。梯度爆炸则相反,loss突然跳到极大值然后变NaN。排查手段是直接看每一层参数的梯度范数,定位是信息从哪一层开始坍塌的。

for name, param in model.named_parameters(): if param.grad is not None: print(name, param.grad.norm().item())

在一个正常的网络中,各层梯度范数通常在同一个量级,允许轻微的递减。如果发现第一层梯度的范数比最后一层小好几个数量级,比如1e-2对1e-7,那就是典型的梯度消失;如果发现梯度范数在某一层之后突然指数上升,那就是梯度爆炸。初始化直接控制第一跳的梯度尺度,所以排查顺序是:先看初始化,再看是否有归一化层,最后才怀疑学习率。

5.2 死掉的ReLU与激活值分布检查

ReLU网络有一个非常阴间的现象:某个神经元的所有输出长期为0,梯度也一直是0,这种神经元再也不会被“唤醒”,业界叫Dead ReLU。如果大量神经元同时死掉,网络的有效容量会大幅缩水,最直观的表现是训练曲线出现平台期,loss降到一定程度就完全不降了。

排查方式很简单,还是用hook或者直接统计激活值里0的比例:

zeros_ratio = (activation_record["fc1"] == 0).float().mean().item() print("zero ratio:", zeros_ratio)

如果0的占比超过5成,那就要注意了。常见的诱因包括:学习率过大、初始化尺度偏大,以及负斜率太小。缓解手段按优先级排列:调小学习率、换上更保守的初始化(比如Kaiming里把a调大一些,或者干脆用Xavier),再不行就把ReLU换成LeakyReLU、ELU这类允许负半轴有梯度的激活函数。

5.3 Loss变成NaN的元凶

Loss变NaN是训练神经网络最折磨人的问题之一,而初始化往往是第一个被忽视的元凶。我排查过多次,流程基本固定:第一步,确认输入数据没有NaN和inf;第二步,在模型forward里临时加几个检查点,看激活值有没有在某层变成inf;第三步,检查loss本身计算有没有除以0;第四步,才回到参数初始化。

实际训练中,权重初始化尺度太大,配上一个偏大的学习率,非常容易在第一步更新时就把权重推到inf。修复办法很直接:把初始化std缩小一个数量级,或者把学习率降下来。还有一个容易被忽略的细节:如果你自定义了参数但忘了初始化,torch.empty里的垃圾数据可能包含极大值,前向一跑就是NaN。创建参数后第一件事就是初始化,别拖。

5.4 复现不稳定与随机种子陷阱

复现实验时,我遇到过一种很隐蔽的情况:每次跑结果都不一样,但绝对不是随机种子没设——后来发现是PyTorch某些版本里GPU上的部分算子是非确定性的,即使固定了种子,CUDA卷积在特定输入尺寸下也有微小的浮点误差累积。如果只想在固定seed下保证模型初始化的权重序列一致,CPU上用torch.manual_seed就够了;如果要追求GPU上完全可复现,需要额外设置:

torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False

这样做的代价是牺牲一点训练速度,所以一般在调试阶段开启,正式训练时会关掉。另外,设置种子是全局状态,如果某个第三方库内部也用了随机数,它可能会消费掉你预设的种子序列,导致后续初始化行为偏离预期。这也是为什么同一个项目里,最好把set_seed放在所有import和模型构造之前。优先级最高的一句话:固定种子之后,构造模型的顺序都不能变,否则结果照样不一样。

5.5 经验小抄:什么时候该认真调初始化

最后整理一张我平时直接照着用的速查表,方便快速选型:

网络类型推荐初始化备注
全连接层 + ReLUkaiming_normal_ / kaiming_uniform_mode="fan_in"
卷积层 + ReLUkaiming_normal_深层网络推荐mode="fan_out"
全连接层 + tanh/sigmoidxavier_normal_ / xavier_uniform_也可以配合BatchNorm使用
RNN/LSTM/GRU正交初始化 orthogonal_重点针对隐层到隐层的权重
Transformer类N(0, 0.02),残差分支尾层置0参见GPT、BERT常见做法
所有层的biaszeros_除非有特殊设计需求

什么时候最该认真检查初始化?第一,网络很深且没有BatchNorm,比如纯全连接深层网络;第二,使用RNN系列,梯度要跨时间步传播;第三,加载预训练模型后新加了一个head层,这个新head的初始化尺度一定要小,否则它第一轮就会破坏预训练特征。反过来,如果你的网络已经用了大量归一化层,比如Conv+BN的标配组合,初始化对训练稳定性的影响会被BN大幅吸收,这时候更重要的是学习率调度和数据质量。

我从这几年的训练经验里总结出一个习惯:不管用哪种初始化,模型建好之后花两分钟跑一次前向,打印各层激活值的标准差,确认每一层尺度都在可控范围内再开始训练。相比在训练到一半时才去排查梯度异常,这短短几分钟的检查能省下大把调参时间。初始化这件事,和网络结构、损失函数一样,是基本功的一部分——它不会让你直接得到SOTA,但能让你所有的tuning工作都建立在一个稳的起点上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 17:32:43

基于Zynq的便携式γ能谱仪设计与实现:PL数字脉冲处理与PS能谱软件开发

1. 为什么选择Zynq来做便携式γ能谱仪 1.1 从“核辐射探测”这个场景说起 γ能谱仪本质上是一台“能量显微镜”——它测量放射性核素放出的γ射线能量分布,通过特征峰位反推核素种类,通过峰面积推算活度。传统台式γ能谱仪通常由高压电源、前置放大器、…

作者头像 李华
网站建设 2026/9/29 17:32:40

nginx-1.24.0 Docker 镜像构建与生产部署实战指南

简介:面向需要自建定制Nginx镜像的开发者与运维人员,这份nginx-1.24.0 Docker镜像资源提供了完整的Dockerfile体系与启动脚本,解决直接使用官方镜像时难以自定义编译参数或添加模块的问题。压缩包内共48个文件,约64KB,…

作者头像 李华
网站建设 2026/9/29 17:31:44

多模态任务如何拆解?观察-转换-判断三段框架实战指南

拿到任何一个多模态任务,第一件事绝不是翻模型榜单,而是先把任务拆成观察、转换、判断三段再动手。这句话是我这些年做多模态项目说得最多的一句,因为在它身上吃的亏太多了。我见过有人把文本、图像、语音特征一股脑拼进一个Transformer里&am…

作者头像 李华
网站建设 2026/9/29 17:30:15

DrissionPage 实战:招聘平台动态数据采集与反爬对抗

1. 为什么我最终选了 DrissionPage 而不是 Selenium 1.1 从一次真实的抓取需求说起 前段时间我需要批量采集某招聘平台上的职位信息,包括职位名称、薪资范围、公司名称、工作地点、经验要求、学历要求以及职位详情的完整描述。乍一看这需求很普通,用 re…

作者头像 李华
网站建设 2026/9/29 17:30:01

手势识别康复系统:用MediaPipe关键点实现动作计数与质量评估

简介:面向手部康复与计算机视觉应用研究者,这份PDF是发表于《计算机测量与控制》2021年第7期的学术论文,提出基于计算机视觉的手势识别康复系统,针对传统康复器械功能单一、训练枯燥、恢复缓慢等问题,给出图像采集、分…

作者头像 李华
网站建设 2026/9/29 17:29:55

Python快速复习指南:高频语法与实用库一次梳理

很多朋友在面试前、考试前、或者要接手一个已有Python项目时,都会突然面临“快速复习Python”的需求。所谓“复习”,大概率不是想从零开始系统学一遍,而是想用最短的时间,把那些平时写业务代码经常用到、但一段时间没动手就会手生…

作者头像 李华