基础知识(机器学习)(PyTorch)
数据操作
为了能够完成各种数据操作,我们需要某种方法来存储和操作数据。通常,我们需要做两件重要的事:1
.获取数据;2
.将数据读入计算机后对其进行处理。如果没有某种方法来存储数据,那么获取数据是没有意义的。首先,我们介绍n
维数组,也称为张量(tensor
)。使用过Python
中NumPy
计算包。无论使用哪个深度学习框架,它的张量类(在MXNet
中为ndarray
,在PyTorch
和TensorFlow
中为Tensor
)都与Numpy
的ndarray
类似。但深度学习框架又比Numpy
的ndarray
多一些重要功能:首先,GPU
很好地支持加速计算,而NumPy
仅支持CPU
计算;其次,张量类支持自动微分。这些功能使得张量类更适合深度学习。
张量表示一个由数值组成的数组,这个数组可能有多个维度。具有一个轴的张量对应数学上的向量(vector
);具有两个轴的张量对应数学上的矩阵(matrix
);具有两个轴以上的张量没有特殊的数学名称。首先,我们可以用arange
创建一个行向量x
。这个行向量包含以0
开始的前12
个整数。他们默认创建为整数。也可以指定创建类型为浮点数。张量中的每个值都称为张量的元素(element
)。例如,张量x
中有12
个元素。除非额外指定,新的张量将存储在内存中。并采用基于CPU
的计算。
1 | import torch as torch |
运算符
我们的兴趣不仅限于读取数据和写入数据。我们想在这些数据上执行数学运算,其中最简单且最有用的操作是按元素(elementwise
)运算。它们将标准标量运算符应用于数组的每个元素。对于将两个数组作为输入的函数,按元素运算将二元运算符应用于两个数组中的每对位置对应的元素。我们可以基于任何从标量到标量的函数来创建按元素函数。
在数学的表示中,我们将通过符号
对于任意具有相同形状的张量,常见的标准算术运算符(+、-、*、/
和**
)都可以被升级为按元素运算。我们可以在同一形状的任意两个张量上调用按元素操作。在下面的例子中,我们使用逗号来表示一个具有5
个元素的元组,其中每个元素都是按元素操作的结果。
1 | x = torch.tensor([1.0, 2, 4, 8]) |
广播机制
在上面的部分中,我们看到了如何在相同形状的两个张量上执行按元素操作。在某些情况下,即使形状不同,我们仍然可以通过调用广播机制(broadcasting mechanism
)来执行按元素操作。这种机制的工作方式如下:
- 通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状。
- 对生成的数组执行按元素操作。
在大多数情况下我么将沿着数组中长度为1的轴进行广播,如下例子:
1 | a = torch.arange(3).reshape((3, 1)) |
索引和切片
就像在其他任何Python
数组中一样,张量中的元素可以同索引来访问,第一个元素的索引为0
,做后一个元素索引为-1
。可以指定范围以包含第一个元素和最后一个之前的元素。如下所示,我们可以用[-1]选择最后一个元素,可以用[1:3]选择第二个和第三个元素:
1 | X[-1], X[1:3] |
节省内存
运行一些操作可能会导致新结果分配内存。例如,如果我们用Y = X + Y
,我们将取消引用Y
指向的张量,而是指向新分配的内存处的张量。在下面的例子中,我们用Python
的id()
函数演示了这一点,它给我们提供了内存中引用对象的确切地址。运行Y = Y + X
后,我们会发现id(Y)
指向另一个位置。这是因为Python
首先计算Y + X
,为结果分配新的内存,然后使Y
指向内存中的这个新位置。
1 | before = id(Y) |
转换为其它Python对象
将深度学习框架定义的张量转化为Numpy
张量(ndarray
)很容易,反之也同样容易。torch
张量和numpy
数组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。
1 | A = X.numpy() |
深度学习存储和操作数据的主要接口是张量(n
维数组)。
数据预处理
为了能用机器学习来解决现实世界的问题,我们经常从预处理原始数据开始,而不是哪些准备好的张量格式数据开始。在python
常用的数据分析工具中,我们经常使用pandas
软件包。像庞大的python
生态系统中的许多其它扩展包一样,pandas
可以与张量兼容。
读取数据集
举一个例子,我们首先创建一个人工数据集,并存储在CSV
(逗号分隔值)文件../data/house_tiny.csv
中。以其他格式存储的数据也可以通过类似的方式进行处理。下面我们将数据集按行写入CSV
文件中。
1 | import os |
处理缺失值
注意,”NaN
“项代表缺失值。为了处理缺失的数据,典型的方法包括插值法和删除法,其中插值法用一个替代值来弥补缺失值,而删除法直接忽略缺失值。通过位置索引iloc
,我们将data
分为inputs
和outputs
,其中前者为data
的前两列,而后者为data
的最后一列,对于inputs
中缺少的数值,我们用同一列的均值替换”NaN
“项。
1 | inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2] |
转换为张量格式
现在inputs
和outputs
中的所有条目都是数值类型,它们可以转换为张量格式。当数据采用张量格式之后,可以用张量函数进一步操作。
1 | X = torch.tensor(inputs.to_numpy(dtype=float)) |
线性代数
标量
如果你曾经在餐厅支付餐费,那么应该已经知道一些基本的线性代数,比如在数字间相加或相乘。例如,北京的温度为scalar
)。如果要将华氏度转换为更常用的摄氏度值,则可以计算表达式5、9
和32
都是标量值。符号
1 | import torch |
向量
向量可以被视为标量值组成的列表。这些标量值被称为向量的元素(element
)或分量(component
)。当向量表示数据集中的样本时,它们的值具有一定的现实意义。例如,如果我们正在训练一个模型来预测贷款违约风险,可能会将每个申请人与一个向量相关联,其分量与其收入、工作年限、过往违约次数和其他因素相对应。如果我们正在研究医院患者可能面临的心脏病发作风险,可能会用一个向量来表示每个患者,其分量为最近的生命体征、胆固醇水平、每天运动时间等。在数学表示法中,向量通常记为粗体、小写的符号(例如,
其中
1 | x[3] |
注意:维度(dimension
)在不同上下文中会有不同的含义,经常会使人感到困惑。为了清楚起见,在此明确一下:向量或轴的维度被用来表示向量或轴的长度,即向量或轴的元素数量。然而张量的维度用来表示张量具有的轴数。在这个意义上张量的某个轴的维数就是这个轴的长度。
矩阵
正如向量将标量从零阶推广到一阶,矩阵将向量从一阶推广到了二阶。矩阵,我们通常用粗体、大写字母来表示(例如,
对于任意square matrix
)。当调用函数来实例化张量时,我们可以通过指定两个分量
1 | A = torch.arange(20).reshape(5, 4) |
我们可以通过行索引(transpose
)。通常用
在代码中访问矩阵的转置。
1 | A.T |
作为方阵的一种特殊类型,对称矩阵(symmetric matrix
)
1 | B = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]]) |
矩阵是有用的数据结构:它们允许我们组织具有不同模式的数据。例如,矩阵中的行可能对应于不同的房屋(数据样本),而列可能对应于不同的属性。因此,尽管单个向量的默认方向是列向量,但在表示表格数据集的矩阵中,将每个数据样本作为矩阵中的行向量更为常见。
张量
就像向量是标量的推广,矩阵是向量的推广一样,我们可以构建更多轴的数据结构。张量是描述具有任意数量轴的n为数组的通用方法,例如,向量是一阶张量,矩阵是二阶张量。张量用特殊字体的大写字母表示(例如,
1 | X = torch.arange(24).reshape(2, 3, 4) |
标量、向量、矩阵和任意数量轴的张量有一些实用的属性。例如,从按元素操作的定义中可以注意到,任何按元素的一元运算都不会改变其操作数的形状。同样,给定具有相同形状的任意两个张量,任何按元素二元运算的结果都将是相同形状的张量。例如,将两个相同形状的矩阵相加,会在这两个矩阵上执行元素加法。
1 | A = torch.arange(20, dtype=torch.float32).reshape(5, 4) |
具体而言,连个矩阵的按元素乘法称为Hadamard
积(Hadamard product
) (数学符号为Hadamard
积为:
1 | A * B |
降维
我们可以对任意张量进行的一个有用的操作是计算其元素的和。数学表示法使用
1 | x = torch.arange(4, dtype=torch.float32) |
我们可以表示任意形状张量的元素和。例如,矩阵
1 | A.shape, A.sum() |
默认情况下,调用求和函数会沿所有的轴降低张量的维度,使它变为一个标量。我们还可以指定张量沿哪一个轴来通过求和降低维度。以矩阵为例,为了通过求和所有行的元素来降维(轴0
),可以在调用函数时指定axis=0
。由于输入矩阵沿0
轴降维以生成输出向量,因此输入轴0
的维数在输出形状中消失。
1 | A_sum_axis0 = A.sum(axis=0) |
非降维求和
但是,有时在调用函数来计算总和或均值时保持轴数不变会很有用。
1 | sum_A = A.sum(axis=1, keepdims=True) |
点积
我们已经学习了按元素操作,、求和和平均值,另一个最基本的操作之一是点积。给定两个向量dot product
)
1 | x = torch.tensor(4, dytype=torch.float32) |
点积在很多场合很有用。例如,给定一组向量1
(即weighted average
)。将两个向量规范化得到单位长度后,点积表示它们夹角的余弦。
矩阵-向量积
现在我们知道如何计算点积,可以开始理解矩阵-向量积(matrix-vector product
)。让我们将矩阵
其中每个
我们可以把一个矩阵mv
函数。当我们为矩阵A和向量x
调用torch.mv(A, x)
时,会执行矩阵-向量积。注意,A
的列维数(沿轴1
的长度)必须与x
的维数(其长度)相同。
1 | A.shape, x.shape, torch.mv(A, x) |
矩阵-矩阵乘法
在掌握点积和矩阵-向量积后,那么矩阵-矩阵乘法(matrix-matrix multiplication
)应该很简单。假设有两个矩阵
用行向量
当我们将简单的每个元素
我么可以将矩阵-矩阵乘法5
行4
列的矩阵,4
行3
列的矩阵。 两者相乘后,我们得到了一个5
行3
列的矩阵。
1 | B = torch.ones(4, 3) |
矩阵-矩阵乘法可以简单地称为矩阵乘法,不应与“Hadamard积
”混淆。
范数
线性代数中最有用的一些运算符是范数(norm
)。非正式地说,向量的范数是表示一个向量有多大。这里考虑的大小(size
)概念不涉及维度,而是分量的大小。在线性代数中,向量范数是将向量映射到标量的函数
第二个性质是熟悉的三角不等式:
第三个性质简单地说范数必须是非负的:
这是有道理的。因为在大多数情况下,任何东西的最小的大小是0
。 最后一个性质要求范数最小为0
,当且仅当向量全由0
组成。
范数听起来很像距离的度量。欧几里得距离和毕达哥拉斯定理中的非负性概念和三角不等式可能会给出一些启发。事实上,欧几里得距离是一个
其中,在
1 | u = torch.tensor([3.0, -4.0]) |
深度学习中经常使用
与
1 | torch.abs(u).sum() |
类似于向量的Frobenius
范数(Frobenius norm
)是矩阵元素平方和的平方根:
Frobenius
范数满足向量范数的所有性质,它就像是矩阵形向量的Frobenius
范数。
1 | torch.norm(torch.ones((4, 9))) |
在深度学习中,我们经常试图解决优化问题:最大化分配给观测数据的概率; 最小化预测和真实观测之间的距离。用向量表示物品(如单词、产品或新闻文章),以便最小化相似项目之间的距离,最大化不同项目之间的距离。目标,或许是深度学习算法最重要的组成部分(除了数据),通常被表达为范数。线性代数还有很多,其中很多数学对于机器学习非常有用。例如,矩阵可以分解为因子,这些分解可以显示真实世界数据集中的低维结构。机器学习的整个子领域都侧重于使用矩阵分解及其向高阶张量的泛化,来发现数据集中的结构并解决预测问题。当开始动手尝试并在真实数据集上应用了有效的机器学习模型,你会更倾向于学习更多数学。
微积分
在2500
年前,古希腊人把一个多边形分成三角形,并把它们面积相加,才找到计算多边形面积的方法,为了求出曲线形状的面积,古希腊人在这样的形状上内接多边形。内接多边形的等长越多,就越接近圆。这个过程也被称为逼近法(method of exhaustion
)。
事实上,逼近法就是积分(integral calculus
)的起源。2000
多年后,微积分的另一支,微分(differential calculus
)被发明出来。在微分学最重要的应用是优化问题,即考虑如何把事情做到最好。这种问题在深度学习中是无处不在的。在深度学习中,我们“训练”模型,不断更新它们,使它们在看到越来越多的数据时变得越来越好。通常情况下,变得更好意味着最小化一个损失函数(loss function
),即一个衡量“模型有多糟糕”这个问题的分数。最终,我们真正关心的是生成一个模型,它能够在从未见过的数据上表现良好。但“训练”模型只能将模型与我们实际能看到的数据相拟合。因此,我们可以将拟合模型的任务分解为两个关键问题:
- 优化(
optimization
):用模型拟合观测数据的过程; - 泛化(
generalization
):数学原理和实践者的智慧,能够指导我们生成出有效性超出用于训练的数据集本身的模型。
导数和微分
我们首先讨论导数的计算,这是几乎所有深度学习优化算法的关键步骤。在深度学习中,我们通常选择对于模型参数可微的损失函数。简而言之,对于每个参数,如果我们把这个参数增加或减少一个无穷小的量,可以知道损失会以多快的速度增加或减少。假设我们有一个函数
如果differentiable
)的。如果instantaneous
)变化率。所谓的瞬时变化率是基于0
。为了更好的解释导数,让我们做一个实验。定义
1 | import numpy as np |
通过0
,2
.虽然这个实验不是数学证明。但当2
。让我们熟悉一下导数的几个等价符号。给定
其中符号
( 是一个常熟)。 (幂率(power rule), 是任意实数)。 。 。
为了微分一个由一些常见函数组成的函数,下面的一些法则方便使用。假设函数
常数相乘法则:
加法法则:
乘法法则:
除法法则:
现在我们可以应用上述几个法则来计算2
,这一点得到了前面的实验的支持。当matplotlib
,这是一个Python中
流行的绘图库。要配置matplotlib
生成图形的属性,我们需要定义几个函数。在下面,use_svg_display
函数指定matplotlib
软件包输出svg
图表以获得更清晰的图像。
1 | def use_svg_display(): |
偏导数
到目前为止,我们只讨论了仅含一个变量的函数的微分。在深度学习中,函数通常依赖于许多变量。因此,我们需要将微分的思想推广到多元函数(multivariate function
)上。设partial derivative
)为:
为了计算
梯度
我们可以连结一个多元函数对其所有变量的偏导数,以得到该函数的梯度(gradient
)向量。具体而言,设函数n
维向量
其中
- 对于所有
,都有 。 - 对于所有
,都有 。 - 对于所有
,都有 。 。
同样,对于任何矩阵
链式法则
然而,上面方法可能很难找到梯度。这是因为在深度学习中,多元函数通常是复合(composite
)的,所以难以应用上述任何规则来微分这些函数。幸运的是,链式法则可以被用来微分复合函数。让我们先考虑单变量函数。假设函数
现在考虑一个更一般的场景,即函数具有任意数量的变量的情况。假设可微分函数
总结
微分和积分是微积分的两个分支,前者可以应用于深度学习中的优化问题。导数可以被解释为函数相对于其变量的瞬时变化率,它也是函数曲线的切线的斜率。梯度是一个向量,其分量是多变量函数相对于其所有变量的偏导数。链式法则可以用来微分复合函数。
自动微分
求导是几乎所有深度学习优化算法的关键步骤。虽然求导的计算很简单,只需要一些基本的微积分。但对于复杂的模型,手工进行更新是一件很痛苦的事情(而且经常容易出错)。深度学习框架通过自动计算导数,即自动微分(automatic differentiation
)来加快求导。实际中,根据设计好的模型,系统会构建一个计算图(computational graph
),来跟踪计算是哪些数据通过哪些操作组合起来产生输出。自动微分使系统能够随后反向传播梯度。这里,反向传播(backpropagate
)意味着跟踪整个计算图,填充关于每个参数的偏导数。
作为一个演示例子,假设我们想对函数x
并为其分配一个初始值。
1 | import torch |
在我们计算x
是一个长度为4
的向量,计算x
和x
的点积,得到了我们赋值给y
的标量输出。 接下来,通过调用反向传播函数来自动计算y
关于x
每个分量的梯度,并打印这些梯度。函数x
的另一个函数。
非标量变量的反向传播
当y
不是标量时,向量y
关于向量x
的导数的最自然解释是一个矩阵。对于高阶和高维的y
和x
,求导的结果可以是一个高阶张量。然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中),但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。
1 | # 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。 |
分离计算
有时,我们希望将某些计算移动到记录的计算图之外。例如,假设y
是作为x
的函数计算的,而z
则是作为y
和x
的函数计算的。想象一下,我们想计算z
关于x
的梯度,但由于某种原因,希望将y
视为一个常数,并且只考虑到x
在y
被计算后发挥的作用。这里可以分离y
来返回一个新变量u
,该变量与y
具有相同的值,但丢弃计算图中如何计算y
的任何信息。换句话说,梯度不会向后流经u
到x
。因此,下面的反向传播函数计算x
的偏导数,同时将u
作为常数处理,而不是x
的偏导数。
1 | x.grad.zero_() |
Python控制流的梯度计算
使用自动微分的一个好处是:即使构建函数的计算图需要通过Python
控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度。在下面的代码中,while循环的迭代次数和if语句的结果都取决于输入a
的值。
1 | def f(a): |
总结
深度学习框架可以自动计算导数:我们首先将梯度附加到想要对其计算偏导数的变量上,然后记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。
概率统计
简单地说,机器学习就是做出预测。根据病人的临床病史,我们可能想预测他们在下一年心脏病发作的概率。在飞机喷气发动机的异常检测中,我们想要评估一组发动机读数为正常运行情况的概率有多大。 在强化学习中,我们希望智能体(agent
)能在一个环境中智能地行动。这意味着我们需要考虑在每种可行的行为下获得高奖励的概率。当我们建立推荐系统时,我们也需要考虑概率。例如,假设我们为一家大型在线书店工作,我们可能希望估计某些用户购买特定图书的概率。为此,我们需要使用概率学。
基本概率论
假设我们掷骰子,想知道看到1的概率有多大,而不是看到另一个数字。如果骰子是公平的,那么所有六个结果1
发生的概率为event
)估计得概率值。大数定律告诉我们:随着投掷次数的增加,这个估计值越来越接近真实的潜在概率。
1 | import torch |
每条实线对应于骰子的6
个值中的一个,并给出骰子在每组实验后出现值的估计概率。当我们通过更多的实验获得更多的数据时,这6
条实体曲线向真实概率收敛。
概率论公理
在处理骰子掷出时,我们将集合sample space
)或结果空间(outcome space
),其中每个元素都是结果(outcome
)。事件(event
)是给定样本空间的随机结果。例如,看到5和看到奇数3
点,因为
概率(probability
)可以被认为是将集合映射到真实值的函数。在给定的样本空间
- 对于任意事件
,其概率从不会是负数,即 。 - 整个样本空间的概率为1,即P(S)=1。
- 对于互斥(
mutually exclusive
)事件(对于所有 )都有 的任意一个可数序列, 序列中任意一个事件发生的概率等于它们各自发生概率的和,即 。
以上也是概率论的公理,由科尔莫戈罗夫于1933
年提出。有了这个公理系统,我们可以避免任何关于随机性的哲学争论;相反,我们可以用数学语言严格地推理。例如,假设事件0
。
随机变量
在我们掷骰子的随机实验中,我们引入了随机变量的概念(random variable
)。随机变量几乎可以是任意数量,并且它可以在随机实验的一组可能性中取一个值。考虑一个随机变量distribution
):分布告诉我们
请注意,离散(discrete
)随机变量(如骰子的每一面)和连续(continuous
)随机变量(如人的体重和身高)之间存在微妙的区别。现实生活中,测量两个人是否具有完全相同的身高没有太大意义。如果我们进行足够精确的测量,最终会发现这个星球上没有两个人具有完全相同的身高。在这种情况下,询问某人的身高是否落入给定的区间,比如是否在1.79米和1.81米之间更有意义。在这些情况下,我们将这个看到某个数值的可能性量化为密度(density
)。 高度恰好为1.80米的概率为0,但密度不是0。在任何两个不同高度之间的区间,我们都有非零的概率。
处理多个随机变量
很多时候,我们会考虑多个随机变量。比如,我们可能需要对疾病和症状之间的关系进行建模。给定一个疾病和一个症状,比如“流感”和“咳嗽”,以某个概率存在或不存在于某个患者身上。我们需要估计这些概率以及概率之间的关系,以便我们可以运用我们的推断来实现更好的医疗服务。
再举一个更复杂的例子:图像包含数百万像素,因此有数百万个随机变量。在许多情况下,图像会附带一个标签(label
),标识图像中的对象。我们也可以将标签视为一个随机变量。我们甚至可以将所有元数据视为随机变量,例如位置、时间、光圈、焦距、ISO
、对焦距离和相机类型。所有这些都是联合发生的随机变量。当我们处理多个随机变量时,会有若干个变量是我们感兴趣的。
联合概率
第一个被称为联合概率(joint probability
)
条件概率
联合概率的不等式带给我们一个有趣的比率:conditional probability
),并用
贝叶斯定理
使用条件概率的定义,我们可以得出统计学中最有用的方程之一:Bayes
定理(Bayes’ theorem
)。根据乘法法则(multiplication rule
)可以得到
请注意,我们这里使用了紧凑的表示法:其中joint distribution
),conditional distribution
),这种分布可以在给定值
边际化
为了能进行事件概率求和,我们需要求和法则(sum rule
),即
这也称为边际化(marginalization
)。边际化结果的概率或分布称为边际概率(marginal probability
)或边际分布(marginal distribution
)。
独立性
另一个有用属性是依赖(dependence
)与独立(independence
)。如果两个随机变量
由于conditionally independent
),当且仅当
应用
假设一个医生对患者进行艾滋病病毒(HIV
)测试。这个测试是相当准确的,如果患者健康但测试显示他患病,这个概率只有1%
;如果患者真正感染HIV
,它永远不会检测不出。我们使用1
,如果阴性,则为0
),1
,如果阴性,则为0
)。下图中列出了这样的条件概率
条件概率 | ||
---|---|---|
1 | 0.01 | |
0 | 0.99 |
请注意,每列的加和都是1
(但每行的加和不是),因为条件概率需要总和为1
,就像概率一样。让我们计算如果测试出来呈阳性,患者感染HIV
的概率,即
因此,得到:
换句话说,尽管使用了非常准确的测试,患者实际上患有艾滋病的几率只有13.06%
。正如我们所看到的,概率可能是违反直觉的。患者在收到这样可怕的消息后应该怎么办?很可能,患者会要求医生进行另一次测试来确定病情。第二个测试具有不同的特性,它不如第一个测试那么精确,如下表所示
条件概率 | ||
---|---|---|
0.98 | 0.03 | |
0.02 | 0.97 |
不幸的是,第二次测试也显示阳性。让我们通过假设条件独立性来计算出应用Bayes
定理的必要概率:
现在我们可以应用边际化和乘法规则:
最后,鉴于存在两次阳性检测,患者患有艾滋病的概率为:
也就是说,第二次测试使我们能够对患病的情况获得更高的信心。尽管第二次检验比第一次检验的准确性要低得多,但它仍然显著提高我们的预测概率。
期望和方差
为了概括概率分布的关键特征,我们需要一些测量方法。一个随机变量expectation
,或平均值(average
))表示为:
当函数
在许多情况下,我们希望衡量随机变量
方差的平方根被称为标准差(standard deviation
)。随机变量函数的方差衡量的是:当从该随机变量分布中采样不同值时,
总结
我们可以从概率分布中采样。可以使用联合分布、条件分布、Bayes
定理、边缘化和独立性假设来分析多个随机变量。期望和方差为概率分布的关键特征的概括提供了实用的度量形式。