sklearn 中的preprocessing数据预处理_sklearn preprocessing-程序员宅基地

技术标签: preprocessing  sklearn  shuffle  Kaggle  

1. sklearn preprocessing

Standardization即标准化,尽量将数据转化为均值为零,方差为一的数据,形如标准正态分布(高斯分布)。实际中我们会忽略数据的分布情况,仅仅是通过改变均值来集中数据,然后将非连续特征除以他们的标准差。

1.1 标准化:去均值,方差规模化

Standardization标准化:将特征数据的分布调整成标准正太分布,也叫高斯分布,也就是使得数据的均值为0,方差为1.
标准化的原因在于如果有些特征的方差过大,则会主导目标函数从而使参数估计器无法正确地去学习其他特征。
标准化的过程为两步:去均值的中心化(均值变为0);方差的规模化(方差变为1)。

(1) 在sklearn.preprocessing中提供了一个scale的方法

In [149]: from sklearn import preprocessing

In [150]: import numpy as np

In [151]: X = np.array([[1., -1., 2.], [2., 0., 0.], [0., 1., -1.]])

In [152]: X_scaled = preprocessing.scale(X)

In [153]: X_scaled 
Out[153]: 
array([[ 0.        , -1.22474487,  1.33630621],
       [ 1.22474487,  0.        , -0.26726124],
       [-1.22474487,  1.22474487, -1.06904497]])
       
#scaled之后的数据零均值,单位方差
In [154]: X_scaled.mean(axis=0)
Out[154]: array([0., 0., 0.])
# # axis=1表示对每一行去做这个操作,axis=0表示对每一列做相同的这个操作
In [155]: X_scaled.std(axis=0)
Out[155]: array([1., 1., 1.])

(2) StandardScaler计算训练集的平均值和标准差,以便测试数据集使用相同的变换

preprocessing这个模块还提供了一个实用类StandarScaler,它可以在训练数据集上做了标准转换操作之后,把相同的转换应用到测试训练集中。
这样就可以对训练数据,测试数据应用相同的转换,以后有新的数据进来也可以直接调用,不用再重新把数据放在一起再计算一次了。

>>> from sklearn.preprocessing import StandardScaler
>>> data = [[0, 0], [0, 0], [1, 1], [1, 1]]
>>> scaler = StandardScaler()
>>> print(scaler.fit(data))  # 调用fit方法,根据已有的训练数据创建一个标准化的转换器
StandardScaler(copy=True, with_mean=True, with_std=True)
>>> print(scaler.mean_)
[0.5 0.5]
>>> print(scaler.transform(data))  # 使用上面这个转换器去转换数据data,调用transform方法
[[-1. -1.]
 [-1. -1.]
 [ 1.  1.]
 [ 1.  1.]]
>>> print(scaler.transform([[2, 2]])) # 对于新来的一组样本,也想得到相同的转换
[[3. 3.]]

注 :
1)StandardScaler()中可以传入两个参数:with_mean,with_std.这两个都是布尔型的参数,默认情况下都是true,但也可以自定义成false.即不要均值中心化或者不要方差规模化为1.
2)scale和StandardScaler可以用于回归模型中的目标值处理。

(3) 将数据特征缩放至某一范围(from sklearn.preprocessing import MinMaxScaler)

也就是使得特征的分布是在一个给定最小值和最大值的范围内的。一般情况下是在[0,1]之间,或者是特征中绝对值最大的那个数为1,其他数以此维标准分布在[[-1,1]之间
以上两者分别可以通过MinMaxScaler 或者 MaxAbsScaler方法来实现。
之所以需要将特征规模化到一定的[0,1]范围内,是为了对付那些标准差相当小的特征并且保留下稀疏数据中的0值。

MinMaxScaler(最小最大值标准化)

X _ s t d = ( X − X . m i n ( a x i s = 0 ) ) ( X . m a x ( a x i s = 0 ) − X . m i n ( a x i s = 0 ) ) X\_std = \frac{ (X - X.min(axis=0))}{(X.max(axis=0) - X.min(axis=0))} X_std=(X.max(axis=0)X.min(axis=0))(XX.min(axis=0))
X _ s c a l e r = X s t d ( m a x − m i n ) + m i n X\_scaler =\frac {X_std} {(max - min)} + min X_scaler=(maxmin)Xstd+min

In [159]: from sklearn import preprocessing

# 将数据缩放至[0, 1]间。训练过程: fit_transform()
In [160]: X_train = np.array([[1., -1., 2.], [2., 0., 0.], [0., 1., -1.]])

In [161]: min_max_scaler = preprocessing.MinMaxScaler()

In [162]: X_train_minmax = min_max_scaler.fit_transform(X_train)

In [163]: X_train_minmax
Out[163]: 
array([[0.5       , 0.        , 1.        ],
       [1.        , 0.5       , 0.33333333],
       [0.        , 1.        , 0.        ]])

# 将上述得到的scale参数应用至测试数据
In [164]: X_test= np.array([[ -3., -1., 4.]])

In [165]: X_test_minmax = min_max_scaler.transform(X_test)

In [166]: X_test_minmax
Out[166]: array([[-1.5       ,  0.        ,  1.66666667]])

# 可以用以下方法查看scaler的属性
In [167]: min_max_scaler.scale_
Out[167]: array([0.5       , 0.5       , 0.33333333])

In [168]: min_max_scaler.min_
Out[168]: array([0.        , 0.5       , 0.33333333])

MaxAbsScaler(绝对值最大标准化)

与上述标准化方法相似,但是它通过除以最大值将训练集缩放至[-1,1]。这意味着数据已经以0为中心或者是含有非常多0的稀疏数据。

In [169]: X_train = np.array([[ 1., -1.,  2.],[ 2.,  0.,  0.],[ 0.,  1., -1.]])

In [170]: max_abs_scaler = preprocessing.MaxAbsScaler()

In [171]: X_train_maxabs =max_abs_scaler.fit_transform(X_train)

In [172]: X_train_maxabs
Out[172]: 
array([[ 0.5, -1. ,  1. ],
       [ 1. ,  0. ,  0. ],
       [ 0. ,  1. , -0.5]])

In [173]: X_test = np.array([[ -3., -1.,  4.]])

In [174]: X_test_maxabs = max_abs_scaler.transform(X_test)

In [175]: X_test_maxabs
Out[175]: array([[-1.5, -1. ,  2. ]])

In [176]: max_abs_scaler.scale_
Out[176]: array([2., 1., 2.])

(4) 规模化稀疏数据

对稀疏数据进行去均值的中心化会破坏稀疏的数据结构。此时可以用其他方法对稀疏的输入数据进行转换,特别是那些特征之间的数据规模不一样的数据。MaxAbsScaler 和 maxabs_scale这两个方法是专门为稀疏数据的规模化所设计的。

(5) 规模化有异常值的数据

如果数据有许多异常值,那么使用数据的均值与方差去做标准化就不行了
在这里,你可以使用robust_scale 和 RobustScaler这两个方法。它会根据中位数或者四分位数去中心化数据。

更多的数据预处理方法参考官方文档:http://scikit-learn.org/stable/modules/preprocessing.html#standardization-or-mean-removal-and-variance-scaling

1.2 正则化Normalization
正则化是将样本在向量空间模型上的一个转换,经常被使用在分类与聚类中。

函数normalize 提供了一个快速有简单的方式在一个单向量上来实现这正则化的功能。正则化有l1,l2等,这些都可以用上:

In [42]: import numpy as np

# # 创建一组特征数据,每一行表示一个样本,每一列表示一个特征
In [43]: x = np.array([[1., -1., 2.],[2., 0., 0.],[0., 1., -1.]])

In [44]: from sklearn import preprocessing

In [45]: x_normalized = preprocessing.normalize(x, norm='l2')

In [46]: x
Out[46]: 
array([[ 1., -1.,  2.],
       [ 2.,  0.,  0.],
       [ 0.,  1., -1.]])

In [47]: x_normalized
Out[47]: 
array([[ 0.40824829, -0.40824829,  0.81649658],
       [ 1.        ,  0.        ,  0.        ],
       [ 0.        ,  0.70710678, -0.70710678]])

# preprocessing这个模块还提供了一个实用类Normalizer,实用transform方法同样也可以对新的数据进行同样的转换
In [48]: normalizer = preprocessing.Normalizer().fit(x)

In [49]: normalizer 
Out[49]: Normalizer(copy=True, norm='l2')

#  对训练数据进行正则
In [50]: normalizer.transform(x)
Out[50]: 
array([[ 0.40824829, -0.40824829,  0.81649658],
       [ 1.        ,  0.        ,  0.        ],
       [ 0.        ,  0.70710678, -0.70710678]])

# 对新的测试数据进行正则
In [51]: normalizer.transform([[-1., 1., 0.]])
Out[51]: array([[-0.70710678,  0.70710678,  0.        ]])

normalize和Normalizer都既可以用在密集数组也可以用在稀疏矩阵(scipy.sparse)中

1.3 特征的二值化

特征的二值化是指将数值型的特征数据转换成布尔类型的值。可以使用使用类Binarizer。

In [52]: binarizer = preprocessing.Binarizer().fit(x)

In [53]: binarizer.transform(x) # 默认是根据0来二值化,大于0的都标记为1,小于等于0的都标记为0
Out[53]: 
array([[1., 0., 1.],
       [1., 0., 0.],
       [0., 1., 0.]])

In [54]: binarizer = preprocessing.Binarizer(threshold=1.5) # 也可以自己设置这个阀值,只需传出参数threshold即可

In [55]: binarizer.transform(x)
Out[55]: 
array([[0., 0., 1.],
       [1., 0., 0.],
       [0., 0., 0.]])

binarize and Binarizer都可以用在密集向量和稀疏矩阵上。

1.4 类别特征编码
我们知道特征可能是连续型的也可能是类别型的变量,比如说:
特征一的特征值:[“male”, “female”],
特征二的特征值:[“from Europe”, “from US”, “from Asia”],
特征三的特征值:[“uses Firefox”, “uses Chrome”, “uses Safari”, “uses Internet Explorer”].

这些类别特征无法直接进入模型,它们需要被转换成整数来表征,比如:
[“male”, “from US”, “uses Internet Explorer”] 对应的编码 [0, 1, 3]
[“female”, “from Asia”, “uses Chrome”] 对应的编码 [1, 2, 1].
. . . ... ...

然而上面这种表征的方式仍然不能直接为scikit-learn的模型所用,因为模型会把它们当成序列型的连续变量。

要想使得类别型的变量能最终被模型直接使用,可以使用one-of-k编码或者one-hot编码。这些都可以通过OneHotEncoder实现,它可以将有n种值的一个特征变成n个二元的特征。

In [61]: enc = preprocessing.OneHotEncoder()

In [62]: enc.fit([[0, 0, 3], [1, 1, 0], [0, 2, 1], [1, 0, 2]])
Out[62]: 
OneHotEncoder(categorical_features='all', dtype=<class 'numpy.float64'>,
       handle_unknown='error', n_values='auto', sparse=True)

In [63]: enc.transform([[0,1,3]]).toarray()
Out[63]: array([[1., 0., 0., 1., 0., 0., 0., 0., 1.]])

In [64]: enc = preprocessing.OneHotEncoder(n_values=[2,3,4])

In [65]: enc.fit([[1, 2, 3], [0, 2, 0]])
Out[65]: 
OneHotEncoder(categorical_features='all', dtype=<class 'numpy.float64'>,
       handle_unknown='error', n_values=[2, 3, 4], sparse=True)

In [66]: enc.transform([[1,0,0]]).toarray()
Out[66]: array([[0., 1., 1., 0., 0., 1., 0., 0., 0.]])

特征1中有(0,1)两个值,特征2中有(0,1,2)3个值,特征3中有(0,1,2,3)4个值,所以编码之后总共有9个二元特征。

但也会存在这样的情况,某些特征中可能对一些值有缺失,比如明明有男女两个性别,样本数据中都是男性,这样就会默认被判别为只有一类值。这个时候我们可以向OneHotEncoder传如参数n_values,用来指明每个特征中的值的总个数,如上enc = preprocessing.OneHotEncoder(n_values=[2,3,4])

1.5 弥补缺失数据

在scikit-learn的模型中都是假设输入的数据是数值型的,并且都是有意义的,如果有缺失数据是通过NAN,或者空值表示的话,就无法识别与计算了。

要弥补缺失值,可以使用均值,中位数,众数等等。Imputer这个类可以实现。

In [67]: from sklearn.preprocessing import Imputer

In [68]: imp = Imputer(missing_values='NaN', strategy='mean', axis=0)

In [69]: imp.fit([[1, 2], [np.nan, 3], [7, 6]])
Out[69]: Imputer(axis=0, copy=True, missing_values='NaN', strategy='mean', verbose=0)

In [70]: x = [[np.nan, 2], [6, np.nan], [7, 6]]

In [71]: imp.transform(x)
Out[71]: 
array([[4.        , 2.        ],
       [6.        , 3.66666667],
       [7.        , 6.        ]])
       
In [72]: import scipy.sparse as sp # Imputer类同样也可以支持稀疏矩阵,以下例子将0作为了缺失值,为其补上均值

In [73]: x = sp.csc_matrix([[1, 2], [0, 3], [7, 6]])   # 创建一个稀疏矩阵

In [74]: x
Out[74]: 
<3x2 sparse matrix of type '<class 'numpy.int64'>'
	with 5 stored elements in Compressed Sparse Column format>

In [75]: imp = Imputer(missing_values=0, strategy='mean', verbose=0)

In [76]: imp.fit(x)
Out[76]: Imputer(axis=0, copy=True, missing_values=0, strategy='mean', verbose=0)

In [77]: x_test = sp.csc_matrix([[0, 2], [6, 0], [7, 6]])

In [78]: imp.transform(x_test)
Out[78]: 
array([[4.        , 2.        ],
       [6.        , 3.66666667],
       [7.        , 6.        ]])

1.6 创建多项式特征
有的时候线性的特征并不能做出美的模型,于是我们会去尝试非线性。非线性是建立在将特征进行多项式地展开上的。

比如将两个特征 ( X 1 , X 2 ) (X_1, X_2) (X1,X2),它的平方展开式便转换成5个特征 ( 1 , X 1 , X 2 , X 1 2 , X 1 X 2 , X 2 2 ) (1, X_1, X_2, X_1^2, X_1X_2, X_2^2) (1,X1,X2,X12,X1X2,X22),其中1是Bias:

In [79]: import numpy as np

In [80]: from sklearn.preprocessing import PolynomialFeatures

In [81]: x = np.arange(6).reshape(3, 2)

In [82]: x
Out[82]: 
array([[0, 1],
       [2, 3],
       [4, 5]])

In [83]: poly = PolynomialFeatures(2)

In [84]: poly.fit_transform(x)
Out[84]: 
array([[ 1.,  0.,  1.,  0.,  0.,  1.],
       [ 1.,  2.,  3.,  4.,  6.,  9.],
       [ 1.,  4.,  5., 16., 20., 25.]])

In [85]: x = np.arange(9).reshape(3, 3)

In [86]: poly = PolynomialFeatures(degree=3, interaction_only=True)

In [87]: poly.fit_transform(x)
Out[87]: 
array([[  1.,   0.,   1.,   2.,   0.,   0.,   2.,   0.],
       [  1.,   3.,   4.,   5.,  12.,  15.,  20.,  60.],
       [  1.,   6.,   7.,   8.,  42.,  48.,  56., 336.]])

也可以自定义选择只要保留特征相乘的项: 即将 ( X 1 , X 2 , X 3 ) (X_1, X_2, X_3) (X1,X2,X3) 转换成
( 1 , X 1 , X 2 , X 3 , X 1 X 2 , X 1 X 3 , X 2 X 3 , X 1 X 2 X 3 ) (1, X_1, X_2, X_3, X_1X_2, X_1X_3, X_2X_3, X_1X_2X_3) (1,X1,X2,X3,X1X2,X1X3,X2X3,X1X2X3),如上所示。

1.7 自定义特征的转换函数
把原始的特征放进一个函数中做转换,这个函数出来的值作为新的特征。比如说将特征数据做log转换,做倒数转换等等。FunctionTransformer 可以实现这个功能

from sklearn.preprocessing import FunctionTransformer

transformer = FunctionTransformer(np.log1p)

x = np.array([[0, 1], [2, 3]])

transformer.transform(x)
Out[91]: 
array([[0.        , 0.69314718],
       [1.09861229, 1.38629436]])

总结:当我们拿到一批原始的数据

  1. 首先要明确有多少特征,哪些是连续的,哪些是类别的。
  2. 检查有没有缺失值,对确实的特征选择恰当方式进行弥补,使数据完整。
  3. 对连续的数值型特征进行标准化,使得均值为0,方差为1。
  4. 对类别型的特征进行one-hot编码。
  5. 将需要转换成类别型数据的连续型数据进行二值化。
  6. 为防止过拟合或者其他原因,选择是否要将数据进行正则化。
  7. 在对数据进行初探之后发现效果不佳,可以尝试使用多项式方法,寻找非线性的关系。
  8. 根据实际问题分析是否需要对特征进行相应的函数转换。
2. numpy.random.shuffle打乱顺序函数
In [2]: import numpy as np

In [3]: arr = np.arange(10)

In [4]: arr
Out[4]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [5]: np.random.shuffle(arr)

In [6]: arr
Out[6]: array([6, 8, 3, 7, 5, 4, 1, 2, 0, 9])

In [7]: arr = np.arange(9).reshape((3, 3))

In [8]: arr
Out[8]: 
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
# 多维矩阵中,只对第一维(行)做打乱顺序操作
In [9]: np.random.shuffle(arr)

In [10]: arr
Out[10]: 
array([[0, 1, 2],
       [6, 7, 8],
       [3, 4, 5]])

参考:
https://blog.csdn.net/csmqq/article/details/51461696
https://blog.csdn.net/sinat_33761963/article/details/53433799

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lilong117194/article/details/82911073

智能推荐

【办公自动化】python一键批量给视频添加随机位置水印-程序员宅基地

文章浏览阅读139次,点赞7次,收藏3次。使用了Python的对一个视频文件进行文字水印处理。具体来说,它通过随机选择位置,在视频中添加了一个文字水印。这个文字水印包括了这段文本,并使用了字体(SimSun.ttc),字号为50,颜色为红色。最后会输出一个名为"xxx.mp4"的视频文件。使用场景:随着自媒体的兴起,很多人发布的视频都需要有加水印的需求,但是如果固定到位置,水印很容易就会被处理掉使用python给视频添加水印(位置随机),大大提高处理水印的难度。

react native realm 与 nodejs 版本之间的坑_default.realm: unable to open a realm at path-程序员宅基地

文章浏览阅读2.3k次。现在手上这个android RN项目出现一个问题,这里记录一下,其中package.json文件中配置了realm的版本:"realm": "^1.1.1"。然后用命令行npm install进行安装node_modules的时候,始终安装不起realm,下面的链接是当初出现这个问题的描述。https://ask.csdn.net/questions/716880当时,有人说是把版本降低..._default.realm: unable to open a realm at path

Android中Module的详细使用教程_andrioid module设置-程序员宅基地

文章浏览阅读3.5k次,点赞2次,收藏10次。本文首先介绍Module是什么,然后再介绍Module的用法、和Module移植导出。首先新手玩家可能会不理解,Module是什么,我从百度摘下来这么一段话:Android Studio中的Module 相当于Eclipse 中的library在使用Android Studio(以下简称AS)新建项目时都会有这样一个概念:Eclipse中的WorkSpace相当于AS中的Proj..._andrioid module设置

电商价格战有望收敛:行业或将整体走向冷静-程序员宅基地

文章浏览阅读380次。电商价格战有望收敛:行业或将整体走向冷静 B2C网上商城k518分类信息网和麦考林已相继公布了2011年财报。与2010年风光上市并全部实现盈利不同,2011年,这两家上市B2C企业双双以亏损收官。对于网商惯用的郴州运动娱乐网价格战问题,业内人士认为,今年的石嘴山水路运输网价格战密度和频次会大幅下降。 现状 两大上市网商双双报亏 麦考林2011年财报显示,净营业收入为2.18亿美元,相比...

ionic4的ion-reorder-group拖拽排序_ion-virtual-scroll-程序员宅基地

文章浏览阅读371次。1.html代码<ion-reorder-group id=“reorder” [disabled]=“false” (ionItemReorder)='Reorder(event)′><ion−reorder∗ngFor="letitemoftoList;leti=index"><divclass="to−itemcw−flex"[ngClass]="′to−item−first′:i===0"><pclass="to−item−num">0i+1<_ion-virtual-scroll

关于日期时间操作工具类DateUtil(一)-----对java.util.Date 的操作._dateutil 判断两个时间是否为一个自然周-程序员宅基地

文章浏览阅读2.4k次。最近发现我们项目里的对日期时间操作的工具类DateUtil感觉挺好用的,现在就总结一下,便于日后查看。 /** * 利用指定SimpleDateFormat instance转换java.util.Date到String * * @param dt * java.util.Date instance * @param formatter_dateutil 判断两个时间是否为一个自然周

随便推点

磁盘调度算法的C++实现(FCFS、SSTF、SCAN、CSCAN、NStepSCAN)_nstepscan算法实现-程序员宅基地

文章浏览阅读1.3w次,点赞4次,收藏57次。Description因为代码结构过于冗余,再加上有小伙伴私信我能不能重写一下,我就重写了,新代码在这里,请移步,谢谢!本实验是模拟操作系统的磁盘寻道方式,运用磁盘访问顺序的不同来设计磁盘的调度算法。实现的磁盘调度算法有FCFS,SSTF,SCAN,CSCAN和 NStepSCAN算法。 设定开始磁道号寻道范围,依据起始扫描磁道号和最大磁道号数,随机产生要进行寻道的磁道号序列。 选..._nstepscan算法实现

AttributeError: module 'torch._C' has no attribute '_cuda_setDevice'(在python命令后面加上 --gpu_ids -1)-程序员宅基地

文章浏览阅读4.5w次,点赞42次,收藏57次。1. 问题:AttributeError: module 'torch._C' has no attribute '_cuda_setDevice'运行命令python train.py --data_dir sample_data时,出现上述错误。2. 参考:https://github.com/KupynOrest/DeblurGAN/issues/74从这里知道了下一个链..._attributeerror: module 'torch._c' has no attribute '_cuda_setdevice

2018年html5移动开发框架,HTML5——7个最牛的HTML5移动开发框架-程序员宅基地

文章浏览阅读273次。0.前言html你并不须要任何的原生应用编程经验,你只须要一些HTML、CSS和JavaScript的知识。首先HTML5会愈来愈好,由于移动端的硬件也会愈来愈强,其实你手机上的不少应用已经悄悄的使用混合式开发了,这也许就是HTML5的魅力所在吧。编程1. 开发跨平台的移动应用bootstrap目前已经有不少的框架能够帮助你开发跨平台的移动应用,在这篇文章中,咱们只介绍最牛的7个。后端1.1 IO..._后端html框架

欢迎来到云络博客!-程序员宅基地

文章浏览阅读1.7k次。云络博客全新上线啦!在这里我们会介绍云络的业务,而更多的内容是关于我们的运维、云计算、技术、Linux、客户、商务、服务和其他我们觉得比较有趣并且想与您分享的信息。以后,我们可能也会把这分成几个不同主题的博客,诸如商务、技术与工具,以及服务等。 云络联合创始人兼首席执行官,Steve Mushero,将负责博客的大部分内容。有时,特邀作者也将帮忙提供博客内容,包括工程师团队,管理者,销售和_络博客

Oracle、Mysql、SqlServer中插入多条数据_sqlsever inset into 多条-程序员宅基地

文章浏览阅读1.2k次。(1) Oracle中:insert into product (id,names, price, code) select 100,'a',1,1 from dual union select 101,'b',2,2 from dual;这里最好用一次insert,不然效率不高,用多个select. (2)Mysql中:insert into 表名(id,_sqlsever inset into 多条

计算二叉树任意两个节点之间的最短路径长度(Java)_java 求二叉树两个节点最短路径-程序员宅基地

文章浏览阅读5.3k次。题目计算二叉树任意两个节点之间的最短路径长度例如:在这个二叉树中,计算节点7和节点3的最短路径长度输出4(7—4—2—1—3)思路先找出两个节点的最近公共祖先(在上面的例子中,节点7和节点3的最近公共祖先就是节点1)分别求出两个节点到最近公共祖先的路径长度(节点7到节点1的长度为3,节点3到节点1的长度为1)求出两个节点的路径长度(3+1=4)代码package Tests;import java.util.LinkedList;/** * @author zj_java 求二叉树两个节点最短路径