目录
第6章 与学习相关的技巧
6.1 参数的更新
6.1.1 探险家的故事
6.1.2 SGD
6.1.3 SGD的缺点
6.1.4 Momentum
6.1.5 AdaGrad
6.1.6 Adam
6.1.7 使用哪种更新方法呢
6.1.8 基于MNIST数据集的更新方法的比较
6.2 权重的初始值
6.2.1 可以将权重初始值设为0吗
6.2.2 隐藏层的激活值的分布
6.2.3 ReLU的权重初始值
6.2.4 基于MNIST数据集的权重初始值的比较
6.3 Batch Normalization
6.3.1 Batch Normalization的算法
6.3.2 Batch Normalization的评估
6.4 正则化
6.4.1 过拟合
6.4.2 权值衰减
6.4.3 Dropout
6.5 超参数的验证
6.5.1 验证数据
6.5.2 超参数的最优化
6.5.3 超参数最优化的实现
6.6 小结
第6章 与学习相关的技巧
6.1 参数的更新
神经网络的学习目的是找到使损失函数的值尽可能小的参数,这是寻找最优参数的问题,解决这个问题的过程称为最优化。
为了找到最优参数,我们将参数的梯度(导数)作为了线索。使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠近最优参数,这个过程称为随机下降梯度算法,简称SGD。本章我们将指出SGD的缺点,并介绍SGD以外的其他最优化方法。
6.1.1 探险家的故事
探险家虽然看不到周围的情况,但是能够知道当前所在位置的坡度(通过脚底感受地面的倾斜情况)。于是,朝着当前所在位置的坡度最大的方向前进,就是SGD的策略。勇敢的探险家心里可能想着只要重复这一策略,总有一天可以到达“至深之地”。
6.1.2 SGD
用数学式可以将SGD写成如下的式:
class SGD: def __init__(self, lr=0.01): self.lr = lr def update(self, params, grads): for key in params.keys(): params[key] -= self.lr * grads[key]
使用这个SGD类,可以按如下方式进行神经网络的参数的更新(下面的代码是不能实际运行的伪代码) :
network = TwoLayerNet(...)optimizer = SGD()for i in range(10000): ... x_batch, t_batch = get_mini_batch(...) # mini_batch grads = network.gradient(x_batch, t_batch) params = network.params optimizer.update(params, grads)
很多深度学习框架都实现了各种最优化方法,并且提供了可以简单切换这些方法的构造,用户可以从中选择自己想用的最优化方法。
6.1.3 SGD的缺点
虽然SGD简单,并且容易实现,但是在解决某些问题时可能没有效率。
在上图中,SGD呈“之”字形移动,这是一个相当低效的路径。也就是说,SGD的缺点是,如果函数的形状非均向,比如呈延伸状,搜索的路径就会非常低效。因此,我们需要比单纯朝梯度方向前进的SGD更聪明的方法。SGD低效的根本原因是,梯度的方向并没有指向最小值的方向。
为了改正SGD的缺点,我们将介绍Momentum、AdaGrad、Adam这3种方法来取代SGD。
6.1.4 Momentum
如上图所示,Momentum方法给人的感觉就像是小球在地面上滚动。
import numpy as npclass Momentum: def __init__(self, lr=0.01, momentum=0.9): self.lr = lr self.momentum = momentum self.v = None def update(self, params, grads): if self.v is None: self.v = {} for key, val in params.items(): self.v[key] = np.zeros_like(val) for key in params.keys(): self.v[key] = self.momentum * self.v[key] - self.lr*grads[key] params[key] += self.v[key]
在上图中,更新路径就像小球在碗中滚动一样。和SGD相比,我们发现“之”字形的“程度” 减轻了。这是因为虽然x轴方向上受到的力非常小,但是一直在同一方向上受力,所以朝同一个方向会有一定的加速。反过来,虽然y轴方向上受到的力很大,但是因为交互地受到正方向和反方向的力,它们会互相抵消,所以y轴方向上的速度不稳定。因此,和SGD时的情形相比,可以更快地朝x轴方向靠近,减弱“之”字形的变动程度。
6.1.5 AdaGrad
在神经网络的学习中,学习率(数学式中记为)的值很重要。学习率过小,会导致学习花费过多时间;反过来,学习率过大,则会导致学习发散而不能正确进行。
在关于学习率的有效技巧中,有一种被称为学习率衰减的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多学”,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
逐渐减小学习率的想法,相当于将“全体”参数的学习率值一起降低,而AdaGrad进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值。
AdaGrad会为参数的每个元素适当地调整学习率,与此同时进行学习。
AdaGrad会记录过去所有梯度的平方和。因此,学习越深入,更新的幅度就越小。实际上,如果无止境地学习,更新量就会变为0,完全不再更新。为了改善这个问题,可以使用RMSProp方法,该方法并不是将过去所有的梯度一视同仁地相加,而是逐渐地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。这种操作从专业上讲,称为“指数移动平均” ,呈指数函数式地减小过去的梯度的尺度。
import numpy as npclass AdaGrad: def __init__(self, lr=0.01): self.lr = lr self.h = None def update(self, params, grads): if self.h is None: self.h = {} for key, val in params.items(): self.h[key] = np.zeros_like(val) for key in params.keys(): self.h[key] += grads[key] * grads[key] params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
由上图的结果可知,函数的取值高效地向着最小值移动。由于y轴方向上的梯度较大,因此刚开始变动较大,但是后面会根据这个较大的变动按比例进行调整,减小更新的步伐。因此,y轴方向上的更新程度被减弱,“之”字形的变动程度有所衰减。
6.1.6 Adam
Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐,如果将这两个方法融合在一起会怎么样呢?这就是Adam方法的基本思路。
import numpy as npclass Adam: def __init__(self, lr=0.01, beta1=0.9, beta2=0.999): self.lr = lr self.beta1 = beta1 self.beta2 = beta2 self.iter = 0 self.m = None self.v = None def update(self, params, grads): if self.m is None: self.m, self.v = {}, {} for key, val in params.items(): self.m[key] = np.zeros_like(val) self.v[key] = np.zeros_like(val) self.iter += 1 lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter) for key in params.keys(): self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key]) self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key]) params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
在上图中,基于Adam的更新过程就像小球在碗中滚动一样,虽然Momentum也有类似的移动,但是相比之下,Adam的小球左右摇晃的程度有所减轻,这得益于学习的更新程度被适当地调整了。
6.1.7 使用哪种更新方法呢
目前并不存在能在所有问题中都表现良好的方法,这四种方法各有各的特点,都有各自擅长解决的问题和不擅长解决的问题。
6.1.8 基于MNIST数据集的更新方法的比较
一般而言,与SGD相比,其他3种方法可以学习得更快,有的最终的识别精度也更高。
6.2 权重的初始值 6.2.1 可以将权重初始值设为0吗
权值衰减就是一种以减小权重参数的值为目的进行学习的方法,通过减小权重参数的值来抑制过拟合的发生。
如果想减小权重的值,一开始就将初始值设为较小的值才是正途。如果我们把权重初始值全部设为0以减小权重的值,会怎么样呢?从结论来说,将权重初始值设为0不是一个好主意。事实上,将权重初始值设为0的话,将无法正确进行学习。为了防止“权重均一化”(严格地讲,是为了瓦解权重的对称结构),必须随机生成初始值。
6.2.2 隐藏层的激活值的分布
我们来做一个简单的实验,观察权重初始值是如何影响隐藏层的激活值的分布的。这里要做的实验是,向一个5层神经网络(激活函数使用sigmoid函数)传入随机生成的输入数据,用直方图绘制各层激活值的数据分布。
import numpy as npimport matplotlib.pyplot as pltdef sigmoid(x): return 1 / (1 + np.exp(-x))def ReLU(x): return np.maximum(0, x)def tanh(x): return np.tanh(x)input_data = np.random.randn(1000, 100)node_num = 100hidden_layer_size = 5activations = {}x = input_datafor i in range(hidden_layer_size): if i != 0: x = activations[i-1] w = np.random.randn(node_num, node_num) * 1 a = np.dot(x, w) z = sigmoid(a) activations[i] = zfor i, a in activations.items(): plt.subplot(1, len(activations), i+1) plt.title(str(i+1) + '-layer') if i != 0: plt.yticks([], []) plt.hist(a.flatten(), 30, range=(0, 1))plt.show()
从上图可知,各层的激活值呈偏向0和1的分布。这里使用的sigmoid函数是S型函数,随着输出不断地靠近0(或者靠近1),它的导数的值逐渐接近0。因此,偏向0和1的数据分布会造成反向传播中梯度的值不断变小,最后消失,这个问题称为梯度消失。层次加深的深度学习中,梯度消失的问题可能会更加严重。
# w = np.random.randn(node_num, node_num) * 1 w = np.random.randn(node_num, node_num) * 0.01
各层的激活值的分布都要求有适当的广度,因为通过在各层间传递多样性的数据,神经网络可以进行高效的学习。反过来,如果传递的是有所偏向的数据,就会出现梯度消失或者“表现力受限”的问题,导致学习可能无法顺利进行。
从上图可知,越是后面的层,图像变得越歪斜,但是呈现了比之前更有广度的分布。因为各层间传递的数据有适当的广度,所以sigmoid函数的表现力不受限制,有望进行高效的学习。
6.2.3 ReLU的权重初始值
观察实验结果可知,当“std=0.01” 时,各层的激活值非常小。神经网络传递的是非常小的值,说明逆向传播时权重的梯度也同样很小,这是很严重的问题,实际上学习基本没有进展。
接下来是初始值为Xavier初始值时的结果。在这种情况下,随着层的加深,偏向一点点变大。实际上,层加深后,激活值的偏向变大,学习时会出现梯度消失的问题。而当初始值为He初始值时,各层中分布的广度相同。由于即使层加深,数据的广度也能保持不变,因此逆向传播时,也会传递合适的值。
总结一下,当激活函数使用ReLU时,权重初始值使用He初始值,当激活函数为sigmoid或tanh等S型曲线函数时,初始值使用Xavier初始值。这是目前的最佳实践。
6.2.4 基于MNIST数据集的权重初始值的比较
由上图可知,std=0.01时完全无法进行学习,这和刚才观察到的激活值的分布一样,是因为正向传播中传递的值很小(集中在0附近的数据)。因此,逆向传播时求到的梯度也很小,权重几乎不进行更新。相反,当权重初始值为Xavier初始值和He初始值时,学习进行得很顺利,并且,我们发现He初始值时的学习进度更快一些。
综上,在神经网络的学习中,权重初始值非常重要。很多时候权重初始值的设定关系到神经网络的学习能否成功。权重初始值的重要性容易被忽视,而任何事情的开始(初始值)总是关键的。
6.3 Batch Normalization 6.3.1 Batch Normalization的算法
Batch Norm有以下优点:
1.可以使学习快速进行(可以增大学习率);
2.不那么依赖初始值(对于初始值不用那么神经质);
3.抑制过拟合(降低Dropout等的必要性)。
Batch Norm的思路是调整各层的激活值分布使其拥有适当的广度,为此,要向神经网络中插入对数据分布进行正规化的层,即Batch Normalization层(下文简称Batch Norm层)。
Batch Norm顾名思义,以进行学习时的mini-batch为单位,按mini-batch进行正规化。具体而言,就是进行数据分布的均值为0、方差为1的正规化。
6.3.2 Batch Normalization的评估
首先,使用MNIST数据集,观察使用Batch Norm和不使用Batch Norm层时学习的过程会如何变化。
我们发现,几乎所有的情况下都是使用Batch Norm时学习进行得更快。同时也可以发现,实际上,在不使用Batch Norm的情况下,如果不赋予一个尺度好的初始值,学习将完全无法进行。
综上,通过使用Batch Norm,可以推动学习的进行。并且,对权重初始值变得健壮(“对初始值健壮”表示不那么依赖初始值)。
6.4 正则化
机器学习的问题中,过拟合是一个很常见的问题。过拟合指的是只能拟合训练数据,但不能很好地拟合不包含在训练数据中的其他数据的状态。机器学习的目标是提高泛化能力,即便是没有包含在训练数据里的为观测数据,也希望模型可以进行正确的识别。我们可以制作复杂的、表现力强的模型,但是相应地,抑制过拟合的技巧也很重要。
6.4.1 过拟合
发生过拟合的原因,主要有以下两个。
1.模型拥有大量参数、表现力强;
2.训练数据少。
这里,我们故意满足这两个条件,制造过拟合现象。
import osimport syssys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定import numpy as npimport matplotlib.pyplot as pltfrom dataset.mnist import load_mnistfrom common.multi_layer_net import MultiLayerNetfrom common.optimizer import SGD(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)# 为了再现过拟合,减少学习数据x_train = x_train[:300]t_train = t_train[:300]# weight decay(权值衰减)的设定 =======================#weight_decay_lambda = 0 # 不使用权值衰减的情况weight_decay_lambda = 0.1# ====================================================network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10, weight_decay_lambda=weight_decay_lambda)optimizer = SGD(lr=0.01)max_epochs = 201train_size = x_train.shape[0]batch_size = 100train_loss_list = []train_acc_list = []test_acc_list = []iter_per_epoch = max(train_size / batch_size, 1)epoch_cnt = 0for i in range(1000000000): batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] t_batch = t_train[batch_mask] grads = network.gradient(x_batch, t_batch) optimizer.update(network.params, grads) if i % iter_per_epoch == 0: train_acc = network.accuracy(x_train, t_train) test_acc = network.accuracy(x_test, t_test) train_acc_list.append(train_acc) test_acc_list.append(test_acc) print("epoch:" + str(epoch_cnt) + ", train acc:" + str(train_acc) + ", test acc:" + str(test_acc)) epoch_cnt += 1 if epoch_cnt >= max_epochs: break# 3.绘制图形==========markers = {'train': 'o', 'test': 's'}x = np.arange(max_epochs)plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)plt.plot(x, test_acc_list, marker='s', label='test', markevery=10)plt.xlabel("epochs")plt.ylabel("accuracy")plt.ylim(0, 1.0)plt.legend(loc='lower right')plt.show()
6.4.2 权值衰减
权值衰减是一直以来经常被使用的一种抑制过拟合的方法,该方法通过在学习的过程中对大的权重进行惩罚,来抑制过拟合,很多过拟合原本就是因为权重参数取值过大才发生的。
现在我们来进行实验,对于刚刚进行的实验,应用的权值衰减,结果如下图所示:
虽然训练数据的识别精度和测试数据的识别精度之间有差距,但是与没有使用权值衰减的结果相比,差距变小了,这说明过拟合受到了抑制。
6.4.3 Dropout
Dropout是一种在学习的过程中随机删除神经元的方法。训练时,随机选出隐藏层的神经元,然后将其删除,被删除的神经元不再进行信号的传递。训练时,每传递一次数据,就会随机选择要删除的神经元。然后,测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,要乘上训练时的删除比例后再输出。
# coding: utf-8import osimport syssys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定import numpy as npimport matplotlib.pyplot as pltfrom dataset.mnist import load_mnistfrom common.multi_layer_net_extend import MultiLayerNetExtendfrom common.trainer import Trainer(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)# 为了再现过拟合,减少学习数据x_train = x_train[:300]t_train = t_train[:300]# 设定是否使用Dropuout,以及比例 ========================use_dropout = True # 不使用Dropout的情况下为Falsedropout_ratio = 0.2# ====================================================network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10, use_dropout=use_dropout, dropout_ration=dropout_ratio)trainer = Trainer(network, x_train, t_train, x_test, t_test, epochs=301, mini_batch_size=100, optimizer='sgd', optimizer_param={'lr': 0.01}, verbose=True)trainer.train()train_acc_list, test_acc_list = trainer.train_acc_list, trainer.test_acc_list# 绘制图形==========markers = {'train': 'o', 'test': 's'}x = np.arange(len(train_acc_list))plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)plt.plot(x, test_acc_list, marker='s', label='test', markevery=10)plt.xlabel("epochs")plt.ylabel("accuracy")plt.ylim(0, 1.0)plt.legend(loc='lower right')plt.show()
在上图中,通过使用Dropout,训练数据和测试数据的识别精度的差距变小了,并且,训练数据也没有到达100%的识别精度。像这样,通过使用Dropout,即便是表现力强的网络,也可以抑制过拟合。
机器学习中经常使用集成学习。所谓集成学习,就是让多个模型单独进行学习,推理时再取多个模型的输出的平均值。通过进行集成学习,神经网络的识别精度可以提高好几个百分点。这个集成学习与Dropout有密切的关系,这是因为可以将Dropout理解为,通过在学习过程中随机删除神经元,从而每一次都让不同的模型进行学习。并且,推理时,通过对神经元的输出乘以删除比例,可以取得模型的平均值。也就是说,可以理解成,Dropout将集成学习的效果(模拟地)通过一个网络实现了。
6.5 超参数的验证
神经网络中,除了权重和偏置等参数,超参数也经常出现。这里所说的超参数是指,比如各层的神经元数量、batch大小、参数更新时的学习率或权值衰减等。如果这些超参数没有设置合适的值,模型的性能就会很差。虽然超参数的取值非常重要,但是在决定超参数的过程中一般会伴随很多的试错。
6.5.1 验证数据
为什么不能用测试数据评估超参数的性能呢?这是因为如果使用测试数据调整超参数,超参数的值会对测试数据发生过拟合。换句话说,用测试数据确认超参数的值的“好坏”,就会导致超参数的值被调整为只拟合测试数据。这样的话,可能就会得到不能拟合其他数据、泛化能力低的模型。
因此,调整超参数时,必须使用超参数专用的确认数据。用于调整超参数的数据,一般称为验证数据。我们使用这个验证数据来评估超参数的好坏。
测试数据用于参数(权重和偏置)的学习,验证数据用于超参数的性能评估。为了确认泛化能力,要在最后使用(比较理想的是只用一次)测试数据。
根据不同的数据集,有的会事先分成训练集、验证数据、测试数据三部分,有的只分成训练数据和测试数据两部分,有的则不进行分割。在这种情况下,用户需要自行进行分割。如果是MNIST数据集,获得验证数据的最简单的方法就是从训练数据中事先分割20%作为验证数据。
6.5.2 超参数的最优化
进行超参数的最优化时,逐渐缩小超参数的“好值”的存在范围非常重要。所谓逐渐缩小范围,是指一开始先大致设定一个范围,从这个范围中随机选出一个超参数(采样),用这个采样到的值进行识别精度的评估;然后,多次重复该操作,观察识别精度的结果,根据这个结果缩小超参数的“好值”的范围。通过重复这一操作,就可以逐渐确认超参数的合适范围。
有报告显示,在进行神经网络的超参数的最优化时,与网格搜索等有规律的搜索相比,随机采样的搜索方式效果更好。这是因为在多个超参数中,各个超参数对最终高的识别精度的影响程度不同。
在超参数的最优化中,要注意的是深度学习需要很长时间。因此,在超参数的搜索中,需要尽早放弃那些不符合逻辑的超参数。于是,在超参数的最优化中,减少学习的epoch,缩短一次评估所需的时间是一个不错的方法。
简单归纳以下,如下所示:
步骤0
设定超参数的范围
步骤1
从设定的超参数范围中随机采样
步骤2
使用步骤1中采样到的超参数的值进行学习,通过验证数据评估识别精度(但是要将epoch设置得很小)
步骤3
重复步骤1和步骤2(100次等),根据它们的识别精度的结果,缩小超参数的范围。
反复进行上述操作,不断缩小超参数的范围,在缩小到一定程度时,从该范围中选出一个超参数的值,这就是进行超参数的最优化的一种方法。
6.5.3 超参数最优化的实现
上图按照识别精度从高到低的顺序排列了验证数据的学习的变化。 学习率在0.001到0.01、权值衰减系数在到之间时,学习可以顺利进行。像这样,观察可以使学习顺利进行的超参数的范围,从而缩小值的范围,然后在这个缩小的范围中重复相同的操作,这样就能缩小到合适的超参数的存在范围,然后在某个阶段,选择一个最终的超参数的值。
import sys, ossys.path.append(os.pardir)import numpy as npimport matplotlib.pyplot as pltfrom dataset.mnist import load_mnistfrom common.multi_layer_net import MultiLayerNetfrom common.util import shuffle_datasetfrom common.trainer import Trainer(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)# 为了实现高速化,减少训练数据x_train = x_train[:500]t_train = t_trian[:500]# 分割验证数据validation_rate = 0.20validation_num = int(x_train.shape[0] * validation_rate)x_train, t_train = shuffle_dataset(x_train, t_train)x_val = x_train[:validation_num]t_val = t_train[:validation_num]x_train = x_train[validation_num:]t_train = t_train[validation_num:]def __train(lr, weight_decay, epocs=50): network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10, weight_decay_lambda=weight_decay) trainer = Trainer(network, x_train, t_train, x_val, t_val, epochs=epocs, mini_batch_size=100, optimizer='sgd', optimizer_param={'lr':lr}, verbose=False) trainer.train() return trainer.test_acc_list, trainer.train_acc_list# 超参数的随机搜索======================================optimization_trial = 100results_val = {}results_train = {}for _ in range(optimization_trial): # 指定搜索的超参数的范围================= weight_decay = 10 ** np.random.uniform(-8, -4) lr = 10 ** np.random.uniform(-6, -2) #================================================ val_acc_list, train_acc_list = __train(lr, weight_decay) print('val acc:' + str(val_acc_list[-1]) + ' | lr:' + str(lr) + ', weight decay:' + str(weight_decay)) key = 'lr:' + str(lr) + ', weight decay:' + str(weight_decay) results_val[key] = val_acc_list results_train[key] = train_acc_list#绘制图形==============================================print('============ Hyper-Parameter Optimization Result ============')grap_draw_num = 20col_num = 5row_num = int(np.ceil(grap_draw_num / col_num))i = 0for key, val_acc_list in sorted(results_val.items(), key=lambda x:x[1][-1], reverse=True): print('Best-' + str(i+1) + '(val acc:' + str(val_acc_list[-1]) + ') |' + key) plt.subplot(row_num, col_num, i+1) plt.title('Best-' + str(i+1)) plt.ylim(0.0, 1.0) if i % 5: plt.yticks([]) plt.xticks([]) x = np.arange(len(val_acc_list)) plt.plot(x, val_acc_list) plt.plot(x, results_train[key], '---') i += 1 if i >= graph_draw_num: breakplt.show()
6.6 小结
本章我们介绍了神经网络的学习中的几个重要技巧,参数的更新方法、权值初始值的赋值方法、Batch Normalization、Dropout等,这些都是现代神经网络中不可或缺的技术。另外,这里介绍的技巧,在最先进的深度学习中也被频繁使用。
1.参数的更新方法,除了SGD之外,还有Momentum、AdaGrad、Adam等方法;
2.权重初始值的赋值方法对进行正确的学习非常重要;
3.作为权重初始值,Xavier初始值、He初始值等比较有效;
4.通过使用Batch Normalization,可以加速学习,并且对初始值变得健壮;
5.抑制过拟合的正则化技术有权值衰减、Dropout等;
6.逐渐缩小“好值”存在的范围是搜索超参数的一个有效方法。