1. 项目概述:从“激活”说起,理解深度学习的基石
“都2021年了,不会还有人连深度学习都不了解吧?”这个标题带着点调侃,也带着点紧迫感,它精准地戳中了很多想入门却又望而却步的朋友的痛点。深度学习听起来高大上,仿佛离我们很远,但实际上,它的核心思想和我们人脑处理信息的方式有异曲同工之妙。今天,我们不谈复杂的数学推导,也不一上来就堆砌那些让人眼花缭乱的网络结构,我们就从一个最基础、最核心,却又最容易被初学者忽略的组件聊起——激活函数。
你可以把深度学习模型想象成一个巨大的、多层的信号处理流水线。每一层都有成千上万个“神经元”,它们接收来自上一层的信息,经过一番计算,再把结果传递给下一层。那么问题来了,如果每一层的计算都只是简单地把输入信号加权求和一下,那这个模型本质上就退化成了一个巨大的线性回归模型。无论你堆叠多少层,它最终只能拟合线性关系,对于图像识别、自然语言处理这些充满复杂非线性模式的任务,它将完全无能为力。这就像你用一堆只能画直线的尺子,永远也拼不出一幅《蒙娜丽莎》。
激活函数,就是这个让“尺子”变成“画笔”的关键魔法。它的作用就是给神经元的输出引入非线性。简单说,它决定了一个神经元是否被“激活”,以及被激活到什么程度。没有它,深度学习就失去了“深度”的意义;有了它,神经网络才具备了拟合任意复杂函数的能力,成为真正的“万能近似器”。这篇文章,我就以一个过来人的身份,带你彻底搞懂激活函数的来龙去脉、家族谱系以及实战中的选择心法。无论你是刚打开PyTorch或TensorFlow的新手,还是想巩固基础的中级玩家,相信这些从实战中踩坑总结出的经验,都能让你对深度学习的理解更上一层楼。
2. 激活函数核心原理与价值深度解析
2.1 为什么非线性是深度学习的灵魂?
要理解激活函数的价值,我们必须先直面一个根本问题:为什么神经网络需要非线性?我们从一个最简单的例子开始。假设我们想用神经网络学习一个非常简单的逻辑:“与”操作。输入是(x1, x2),输出y在x1和x2都为1时为1,否则为0。
如果我们不使用激活函数,那么单个神经元的计算就是:y = w1*x1 + w2*x2 + b。这是一个线性方程,在二维坐标系里它是一条直线。无论我们如何调整权重w1、w2和偏置b,这条直线都无法完美地将(0,0),(0,1),(1,0)(输出应为0)和(1,1)(输出应为1)这四个点分开。因为不存在一条直线能把正方形对角线的两个点(一类)和另外两个点(另一类)完全分开。
这时,我们引入一个最简单的非线性激活函数,比如阶跃函数(虽然现在不用,但便于理解):当输入大于0时输出1,否则输出0。那么神经元的计算变为:y = step(w1*x1 + w2*x2 + b)。通过调整参数,我们可以让直线w1*x1 + w2*x2 + b = 0恰好落在(0,1)和(1,0)之间,这样,位于直线一侧的(1,1)点经过step函数输出1,另一侧的三个点输出0。问题迎刃而解。
这个例子说明,单个非线性神经元可以构成一个线性分类器。而当我们把许多这样的神经元分层堆叠起来时,每一层的非线性变换都在对数据空间进行复杂的扭曲和折叠。第一层可能学习到一些简单的边缘特征,第二层将这些边缘组合成角点或纹理,更深层的网络则能组合出眼睛、鼻子等高级语义特征。这种逐层抽象、逐层组合的能力,完全依赖于每一层引入的非线性变换。如果没有激活函数,无论你堆叠多少层,整个网络的复合函数依然是一个线性函数,其表达能力与单层网络无异,这无疑是巨大的资源浪费。
注意:这里常有一个误解,认为深度网络的优势仅仅在于参数多。实际上,深度带来的核心优势是“层次化的特征学习”,而这必须由非线性激活函数来保障。参数多只是实现这一优势的代价,而非优势本身。
2.2 激活函数的双重使命:引入非线性与梯度流控制
激活函数不仅仅是一个简单的“开关”或“扭曲器”。在深度神经网络训练的核心——反向传播算法中,激活函数扮演着更为关键的角色:控制梯度流的传播。
反向传播算法通过链式法则,将损失函数对权重的梯度从输出层逐层向前传递。在这个过程中,梯度会乘以每一层激活函数对其输入的导数(即梯度)。因此,激活函数的导数特性直接决定了梯度在传播过程中的“命运”。
- 梯度消失:如果激活函数的导数在很多区间都非常小(例如Sigmoid函数在两端的饱和区),那么当这些导数在多层网络中连续相乘时,梯度会以指数级速度迅速衰减到接近零。这意味着网络前几层的权重几乎得不到有效的更新,学习停滞。这是训练深度网络时最头疼的问题之一。
- 梯度爆炸:与消失相反,如果导数持续大于1,梯度可能在反向传播中指数级增长,导致权重更新步长巨大,网络参数剧烈震荡,无法收敛。
因此,一个理想的激活函数,不仅需要提供足够的非线性,还需要具备良好的梯度特性,确保在深度网络中梯度能够稳定、有效地流动。这直接关系到模型能否被成功训练,以及训练的速度和稳定性。我们在选择激活函数时,必须同时考量其非线性表达能力和对梯度流的影响。
3. 经典与现代激活函数全景图鉴与实战选型
3.1 上古“活化石”:Sigmoid与Tanh的功与过
Sigmoid(σ(x) = 1 / (1 + e^{-x})) 可以说是神经网络启蒙时代的标志。它将任意实数压缩到(0, 1)之间,输出平滑,物理意义清晰(可以理解为神经元的“激活概率”或“放电率”)。在逻辑回归、二分类问题的输出层,它至今仍有其用武之地。然而,将其用于深度网络的隐藏层,则几乎被视为一种“反模式”。
它的主要问题有三:
- 梯度消失:如之前所述,Sigmoid函数在两端的饱和区梯度接近于0。当输入值很大或很小时,梯度几乎为零,导致权重无法更新。
- 非零均值输出:Sigmoid的输出均值约为0.5,而非0。这对于下一层神经元来说,意味着其输入始终是正的。在反向传播时,权重的梯度更新会全部为正或全部为负(取决于上游梯度),导致优化路径呈“之”字形摆动,收敛缓慢。
- 计算成本较高:涉及指数运算。
Tanh(tanh(x) = (e^x - e^{-x}) / (e^x + e^{-x})) 可以看作是Sigmoid的“改进版”。它将输出范围压缩到(-1, 1),具有零均值的特性,这在一定程度上缓解了Sigmoid的第二个问题,使得收敛速度通常比Sigmoid快。然而,它依然没有解决梯度消失的核心痛点,在饱和区梯度依然很小。因此,在现代的深度网络中,Tanh也较少用于隐藏层,但在一些特定的循环神经网络(如LSTM、GRU)的门控机制中,因其输出范围对称,仍被使用。
实操心得:如今,除非你在复现一些非常古老的论文,或者在输出层需要概率解释,否则在隐藏层应坚决避免使用Sigmoid。Tanh可以谨慎尝试于RNN,但在CNN和全连接网络中,有更好的选择。
3.2 当代王者:ReLU家族及其变种
ReLU(ReLU(x) = max(0, x)) 的提出是深度学习发展史上的一个关键转折点。它的形式极其简单:正数原样通过,负数直接置零。正是这种简单,带来了巨大的优势:
- 缓解梯度消失:在正区间,梯度恒为1,彻底解决了饱和区的梯度消失问题,使得梯度可以畅通无阻地反向传播。
- 计算效率极高:只需要一个阈值比较和乘法,没有指数、除法等复杂运算。
- 带来稀疏激活性:大约50%的神经元会被置零,这使得网络更稀疏,可能具有更好的泛化能力,并降低了过拟合的风险。
然而,ReLU并非完美,它有一个著名的缺陷:Dying ReLU问题。如果一个神经元在训练过程中,其权重更新导致对于所有训练数据,该神经元的输入都小于0,那么它将被永远“杀死”,此后梯度永远为0,权重再也无法更新。在训练初期学习率设置过高时,这种情况尤为常见。
为了解决Dying ReLU问题,研究者们提出了多种变体:
- Leaky ReLU(
LReLU(x) = max(αx, x)):给负区间一个很小的斜率α(如0.01),使得负输入时也有一个微小的梯度,确保神经元不会完全“死亡”。这是一个简单有效的改进。 - Parametric ReLU:将Leaky ReLU中的斜率
α作为一个可学习的参数,让网络自己决定负区间的斜率。这增加了少量参数,但有时能获得更好的性能。 - Exponential Linear Unit (ELU):在负区间使用一个指数衰减函数,使得输出均值为零,同时负区间平滑。理论上具有比ReLU更好的归一化特性,但计算涉及指数,稍慢。
- Scaled Exponential Linear Unit (SELU):ELU的缩放版。论文证明,在满足特定条件(如使用LeCun正态初始化)的全连接网络中,SELU具有“自归一化”属性,能自动将激活值向零均值和单位方差拉拢,非常适合深层的全连接网络。
Swish/SiLU(Swish(x) = x * sigmoid(x)):这是谷歌大脑团队通过自动搜索发现的一个激活函数。它处处光滑可微,且非单调(在负区间有一个小的“凸起”)。实验表明,Swish在许多任务上,尤其是非常深的模型(如EfficientNet)上,表现略优于ReLU。它的计算比ReLU复杂,但通常带来的精度提升是值得的。你可以把它看作是ReLU的一个“平滑、自门控”的版本。
3.3 实战选型指南:没有银弹,只有场景
面对这么多选择,初学者很容易犯难。这里我结合自己的经验,给出一个清晰的选型思路:
- 默认首选:ReLU。对于绝大多数情况,尤其是CNN和不算特别深的MLP,ReLU是你的第一选择。它简单、快速、效果可靠。在项目初期,无脑用ReLU准没错。
- 担心Dying ReLU?用Leaky ReLU或它的变种。如果你训练的模型非常深,或者使用了较大的学习率,可以尝试将ReLU替换为Leaky ReLU (alpha=0.01) 或 PReLU。这几乎是一个无成本的保险策略。
- 深层全连接网络:试试SELU。如果你在搭建一个像Transformer中那样的深层前馈网络,SELU的“自归一化”特性可能会让训练更稳定,减少对Batch Normalization的依赖。但要注意其使用条件(特定初始化)。
- 追求极致精度:尝试Swish。在图像分类、目标检测等任务上,当你已经有一个不错的ReLU基线模型后,可以尝试将激活函数统一替换为Swish。这可能会带来零点几个百分点的提升,但代价是计算量小幅增加。在移动端等对计算敏感的场景需权衡。
- 输出层特例:
- 二分类:输出层用Sigmoid,将输出映射为概率。
- 多分类:输出层用Softmax,获得各类别的概率分布。
- 回归问题:通常输出层不使用激活函数(线性输出),或者根据值域使用Sigmoid(限定到0-1)/Tanh(限定到-1,1)。
记住一个核心原则:激活函数的选择是超参数调优的一部分。在重要的项目中,你完全可以在模型架构固定的情况下,将激活函数作为一个搜索项,用小部分数据跑一个快速的超参数扫描,让实验数据告诉你答案。
4. 激活函数与网络训练技巧的协同
4.1 与权重初始化的“共生关系”
激活函数和权重初始化是深度学习中一对密不可分的搭档。糟糕的初始化会直接“杀死”激活函数的效果,反之,合适的初始化能让激活函数的优势充分发挥。
以ReLU为例,因为它会将一半的激活值置零,如果权重初始化不当(比如方差过大),前向传播时激活值可能会迅速膨胀或收缩,导致梯度爆炸或消失。因此,针对ReLU系列,我们通常使用“He初始化”(也称为Kaiming初始化)。其核心思想是,在初始化时,根据前一层的神经元数量(fan_in),调整权重初始化的方差,使得每一层激活值的方差在传播过程中保持大致稳定。对于ReLU,He初始化的方差通常设为2 / fan_in。
对于SELU,则必须配合“LeCun正态初始化”才能发挥其自归一化的特性。而对于Sigmoid/Tanh,历史上常用“Xavier初始化”(Glorot初始化),其方差设为1 / fan_in。
踩坑实录:我曾经在一个项目里,把网络从ReLU换成SELU,但忘了改初始化方法,仍然用的He初始化。结果训练一开始损失就变成NaN。排查了半天才发现是初始化不匹配导致数值爆炸。这个教训很深刻:换激活函数,一定要检查并匹配对应的权重初始化策略。
4.2 与批量归一化的“黄金组合”
批量归一化(Batch Normalization, BN)的出现,极大地缓解了激活函数选择带来的部分压力。BN在每一层的激活函数之前(或之后,但之前是更常见的做法),对输入进行归一化,使其保持零均值和单位方差。
这样做的好处是:
- 减少对初始化的依赖:无论你用什么初始化,BN都能很快将数据分布拉回正常范围,让训练起点更稳定。
- 允许使用更高的学习率:归一化后的数据更平滑,梯度更稳定,因此可以使用更大的学习率加速收敛。
- 在一定程度上正则化模型:由于每个批次的均值和方差不同,引入了轻微噪声,有正则化效果。
当使用了BN之后,激活函数输入值的尺度被严格控制,这使得像ReLU这样的激活函数更加鲁棒,Dying ReLU的问题也会减轻。因此,在现代深度网络架构中,“Conv/Linear -> BN -> ReLU”已经成为标准模块。即使你使用了Sigmoid,前面加上BN也能防止其输入进入饱和区,从而缓解梯度消失。
但请注意,BN并非万能。在小批量训练、循环神经网络中,BN的应用有其局限性。此时,激活函数本身的稳健性就显得尤为重要,这也是SELU等激活函数被提出的背景之一。
5. 实战代码剖析与性能对比实验
理论说再多,不如一行代码。我们用一个简单的实验来直观感受不同激活函数的表现。我们将在一个标准的图像分类数据集(如CIFAR-10)上,训练一个结构相同但激活函数不同的卷积神经网络。
import torch import torch.nn as nn import torch.optim as optim import torchvision.transforms as transforms from torchvision.datasets import CIFAR10 from torch.utils.data import DataLoader # 定义一个基础块,方便替换激活函数 class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, activation='relu'): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm2d(out_channels) # 根据参数选择激活函数 if activation == 'relu': self.act = nn.ReLU(inplace=True) elif activation == 'leaky_relu': self.act = nn.LeakyReLU(0.01, inplace=True) elif activation == 'silu': self.act = nn.SiLU(inplace=True) # PyTorch 1.7+ 中的 Swish elif activation == 'selu': self.act = nn.SELU(inplace=True) else: raise ValueError(f"Unsupported activation: {activation}") def forward(self, x): identity = x out = self.conv1(x) out = self.bn1(out) out = self.act(out) out = self.conv2(out) out = self.bn2(out) # 如果通道数变化,用1x1卷积调整捷径连接 if identity.shape[1] != out.shape[1]: identity = nn.Conv2d(identity.shape[1], out.shape[1], kernel_size=1)(identity) out += identity out = self.act(out) # 第二个激活在相加之后 return out # 构建一个简单的网络 class SimpleNet(nn.Module): def __init__(self, activation='relu'): super().__init__() self.stem = nn.Sequential( nn.Conv2d(3, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True) # 第一层固定用ReLU ) self.layer1 = BasicBlock(64, 64, activation) self.layer2 = BasicBlock(64, 128, activation) self.layer3 = BasicBlock(128, 256, activation) self.pool = nn.AdaptiveAvgPool2d((1,1)) self.fc = nn.Linear(256, 10) def forward(self, x): x = self.stem(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.pool(x) x = torch.flatten(x, 1) x = self.fc(x) return x # 训练和评估函数(此处省略详细循环,仅展示框架) def train_model(activation_type, lr=0.1, epochs=50): print(f"\n=== 训练激活函数: {activation_type.upper()} ===") device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 1. 数据加载 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) trainset = CIFAR10(root='./data', train=True, download=True, transform=transform) trainloader = DataLoader(trainset, batch_size=128, shuffle=True) # 2. 模型、损失、优化器 model = SimpleNet(activation=activation_type).to(device) # 关键:根据激活函数调整初始化!这里为简化,使用默认初始化,实际应用应按前述规则调整。 criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9, weight_decay=5e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs) # 3. 训练循环 (伪代码,需补充) # for epoch in range(epochs): # model.train() # for inputs, labels in trainloader: # # ... 训练步骤 # scheduler.step() # 4. 返回最终精度 # final_acc = evaluate(model, testloader) # return final_acc print(f"模型结构已创建,使用 {activation_type} 激活函数。") return model # 尝试不同的激活函数 for act in ['relu', 'leaky_relu', 'silu', 'selu']: model = train_model(act)在实际运行中,你可能会观察到以下现象(基于普遍经验):
- ReLU:训练速度最快,收敛稳定,是可靠的基线。
- Leaky ReLU:训练曲线与ReLU非常相似,但在某些深度网络上可能更稳定,最终精度可能略有浮动。
- SiLU (Swish):训练初期损失下降曲线可能比ReLU更平滑,最终测试精度可能会高出0.2%-0.5%,但每个epoch的训练时间会稍长。
- SELU:在没有严格满足其初始化要求(LeCun normal)和网络架构要求(全连接为主)的情况下,在CNN上的表现可能不稳定,有时甚至不如ReLU。但在合适的场景下,它确实能实现“自归一化”,减少对BN的依赖。
这个实验的关键启示是:不要迷信论文中的绝对排名。最好的激活函数取决于你的具体任务、网络架构、初始化方法、优化器,甚至数据集。动手实验,用你的数据说话,才是工程实践中的金科玉律。
6. 常见陷阱、疑难排查与高级话题
6.1 激活函数使用中的典型“坑”
梯度爆炸/消失的误判:当训练出现Loss为NaN或者不下降时,很多人第一反应是激活函数问题。但根源可能更深。首先应该检查的是:数据预处理(归一化了吗?)、权重初始化(用对了吗?)、学习率(是不是太大了?)。用一个简单的调试方法:在训练开始前,跑几个批次的前向传播,打印每一层激活值的均值和标准差。如果某一层的值突然变得极大(>100)或极小(接近0),那么问题很可能出在初始化或数据上,而不是激活函数本身。
死ReLU的检测与处理:如何判断网络中出现了大量“死亡”的ReLU?一个简单的方法是统计在前向传播中,某一层ReLU输出中零元素的比例。如果这个比例长期高于90%,甚至达到100%,那说明该层神经元基本全部死亡。解决方法:① 使用Leaky ReLU、PReLU等变体;② 降低学习率;③ 尝试更好的权重初始化(He Init);④ 在激活函数前加入BatchNorm。
输出层激活函数误用:这是一个经典错误。例如,在二分类任务中,模型的输出层是单个神经元,你使用了Sigmoid激活函数,但损失函数却选用了
nn.CrossEntropyLoss。在PyTorch中,CrossEntropyLoss已经内置了Softmax操作,它期望的是原始的、未经过激活的分数(logits)。正确的做法是:输出层不使用任何激活函数,直接将logits传给CrossEntropyLoss。如果非要用Sigmoid,则应配合nn.BCEWithLogitsLoss(它内置了Sigmoid和BCE损失)。务必清楚你用的损失函数内部做了什么。
6.2 可视化:理解激活函数行为的利器
对于初学者,我强烈建议对激活函数及其梯度进行可视化。这能帮你建立最直观的感受。
import numpy as np import matplotlib.pyplot as plt def plot_activation_and_gradient(name, func, grad_func, x_range=(-4, 4)): x = np.linspace(x_range[0], x_range[1], 200) y = func(x) dy = grad_func(x) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) ax1.plot(x, y, linewidth=2) ax1.set_title(f'{name} Function') ax1.grid(True) ax1.set_xlabel('Input') ax1.set_ylabel('Output') ax2.plot(x, dy, linewidth=2, color='orange') ax2.set_title(f'{name} Gradient') ax2.grid(True) ax2.set_xlabel('Input') ax2.set_ylabel('Gradient') plt.tight_layout() plt.show() # 定义函数和梯度 def relu(x): return np.maximum(0, x) def relu_grad(x): return (x > 0).astype(float) def leaky_relu(x, alpha=0.01): return np.where(x > 0, x, alpha*x) def leaky_relu_grad(x, alpha=0.01): return np.where(x > 0, 1, alpha) def swish(x, beta=1.0): return x / (1 + np.exp(-beta*x)) # 简化版,近似SiLU def swish_grad(x, beta=1.0): sig = 1 / (1 + np.exp(-beta*x)) return sig * (1 + beta*x*(1-sig)) plot_activation_and_gradient('ReLU', relu, relu_grad) plot_activation_and_gradient('Leaky ReLU (α=0.01)', lambda x: leaky_relu(x, 0.01), lambda x: leaky_relu_grad(x, 0.01)) plot_activation_and_gradient('Swish (β=1)', lambda x: swish(x, 1.0), lambda x: swish_grad(x, 1.0))通过对比图像,你可以清晰地看到:ReLU的梯度在负区间是“死”的;Leaky ReLU给了负区间一条“生路”;Swish的梯度则处处平滑且非零。这种直观印象对你理解它们在实际训练中的行为至关重要。
6.3 超越固定函数:动态与自适应的激活
前沿研究正在探索更灵活的激活机制。例如:
- 可学习激活函数:如PReLU,将负区间的斜率作为参数学习。更复杂的如Swish的
β参数也可以学习。 - 自适应激活函数:让激活函数的形式或参数根据输入数据或网络状态动态变化。例如,Maxout单元可以学习一个分段线性凸函数。
- 注意力机制作为激活:在一些Transformer变体中,门控机制或轻量级注意力可以被视为一种动态的、内容相关的激活函数。
这些方法增加了模型的表达能力,但也带来了更多的参数和计算量,以及过拟合的风险。在资源充足且追求极致性能的场景下值得尝试,但对于大多数应用,经典的ReLU及其可靠变种仍然是性价比最高的选择。
7. 总结与个人实践心法
聊了这么多,最后分享几点我个人的实战心法,希望能帮你少走弯路:
- 从ReLU开始,保持简单:启动一个新项目时,毫不犹豫地选择ReLU作为所有隐藏层的激活函数。它是你最可靠的基线。在模型其他部分(架构、数据、优化器)都调优得差不多之后,再考虑将激活函数作为超参数进行微调。
- 激活函数、初始化、归一化必须视为一个整体:这三者共同决定了网络前向传播的稳定性和反向传播的梯度质量。改动其中任何一个,都要思考其对另外两个的影响。记住“ReLU -> He初始化”、“SELU -> LeCun初始化”、“Sigmoid/Tanh前最好有BN”这些黄金组合。
- 输出层的选择是任务驱动的:这没有商量余地。分类用Sigmoid/Softmax,回归通常用线性输出。不要在这里搞“创新”。
- 可视化与监控是你的好朋友:在训练初期,花点时间可视化第一层卷积核的权重、中间层的激活分布直方图。异常的分布(全部为0,或极大/极小)往往是问题最早的信号。利用TensorBoard或WandB等工具监控这些指标。
- 实验精神高于一切:深度学习充满经验主义。我上面说的所有“经验”和“指南”,都可能被你的特定数据集和任务推翻。如果时间允许,为你重要的模型设计一个小的消融实验:在完全相同的设置下,仅替换激活函数,看哪个效果最好。让数据做最终的决定。
激活函数是深度学习大厦里的一块砖,看似不起眼,却至关重要。理解它,就是理解神经网络如何从线性走向非线性,从脆弱走向强大。希望这篇长文能帮你把这块砖砌牢。在后续的文章里,我们会继续拆解深度学习其他核心组件,比如损失函数、优化器、归一化层等等。一步步来,你会发现,这座看似神秘的大厦,其实是由一块块设计精巧的砖石构筑而成的。