• 使用Scipy优化梯度下降问题


    目    录

    问题重述

    附加问题

    步骤实施

    1.查看Scipy官网SciPy,找到优化有关的模块(Optimize)

    2.研究多种优化策略,选择最符合代码的方案进行优化

    3.minimize函数参数及其返回值

    4.代码展示

    5.结果展示

    6.进一步优化

    6.1对如下函数方法进行优化

    6.2基准测试

    6.3 发现

    测试文件附录

    任务清单


    问题重述

    在二维平面有n个点,如何画一条直线,使得所有点到该直线距离之和最短

    如果能找到,请给出其损失函数

    附加问题

    1.使用Scipy优化上述问题

    2.主代码中不得出现任何循环语法,出现一个扣10分

    步骤实施

    1.查看Scipy官网SciPy,找到优化有关的模块(Optimize

    2.研究多种优化策略,选择最符合代码的方案进行优化

    优化方法
    名称特点应用场景
    Scalar Functions Optimization用于最小化或最大化单个标量函数的,通常用于解决一维问题目标函数只返回一个标量(单个值)
    Local (Multivariate) Optimization适用于多变量问题,需要梯度函数,不过会自动寻找梯度更新目标值在参数空间中找到局部最小值或最大值
    Global Optimization寻找函数的全局最小值或最大值,包含多个局部最值在计算条件允许的条件下可以得到全局最优解

    优化方法
    序号名称使用方法适用条件
    1Nelder-MeadNelder-Mead单纯形法适用于一般的非线性问题
    2PowellPowell方法适合多维非约束优化的方法
    3CG共轭梯度法(Conjugate Gradient)适用于二次优化问题或大规模问题
    4BFGS拟牛顿BFGS算法适用于大多数非线性优化问题的常用方法,尤其是当梯度信息可用时
    5Newton-CG牛顿共轭梯度法适用于大多数非线性优化问题,但相对于BFGS需要更多的内存
    6L-BFGS-B限制内存BFGS算法适用于大规模问题,因为它限制了内存使用
    7TNC截断牛顿法适用于大多数非线性优化问题,并且能够处理约束条件
    8COBYLA约束优化适用于具有约束条件的问题
    9SLSQP顺序最小二乘法适用于具有约束条件的问题,并且能够处理线性和非线性约束
    10trust-constr信任区域约束优化方法适用于有约束条件的问题,并且可以处理线性和非线性约束
    11dogleg信任域Dogleg方法适用于具有约束条件的问题
    12trust-ncg信任区域牛顿共轭梯度法适用于约束优化问题
    13trust-krylov信任区域Krylov子空间法适用于约束优化问题
    14trust-exact精确信任区域方法适用于约束优化问题

     此问题我们需要求最小值,所以我们采用minimize函数,并选择常用的BFGS策略

    3.minimize函数参数及其返回值

    原型如下:

    scipy.optimize.minimize(fun, x0, args=(), method=None, jac=None, hess=None, hessp=None, bounds=None, constraints=(), tol=None, callback=None, options=None)
    

    挑五个主要的参数讲

    1.fun:需要最小化的目标函数

    这个函数应该接受一个输入向量,返回一个标量(单个值),表示损失函数的值。

    2.x0:起始参数的初始猜测值

    通常是一个数组或列表,表示参数的初始估计。

    3.args:传递给目标函数的额外参数的元组

    如果目标函数需要额外的参数,可以将它们作为元组传递给args参数。

    4.method:选择优化方法的字符串

    这是一个可选参数,如果未指定,默认使用'Nelder-Mead'方法。可以选择其他方法,

    5.jac:表示目标函数的梯度(导数)的函数

    如果提供了梯度函数,通常可以加速优化过程。如果不提供,优化算法会尝试数值估计梯度。

    所以我们在优化代码的时候,

    可以将 calcLoseFunction函数作为fun,

    而k,b两个参数打成列表作为x0,

    将XData,YData组成元组传递给arg

    method选择BFGS

    最后jac选择不写,便于对比两者速度差异

    其返回值说明如下

    1.x:优化的参数值。这是一个数组,包含找到的最优参数。

    2.fun:最小化目标函数的最小值(损失函数的最小值)。

    3.success:一个布尔值,表示优化是否成功收敛到最小值。

    4.message:一个字符串,描述优化的终止消息。

    5.nit:迭代次数,表示优化算法运行的迭代次数。

    6.nfev:函数调用次数,表示评估目标函数的次数。

    7.njev:梯度计算次数,表示计算目标函数梯度的次数(如果提供了梯度函数)。

    8.hess_inv:Hessian矩阵的逆矩阵(如果提供了Hessian信息)。

    9.jac:目标函数的梯度值。

    4.代码展示

    1. import numpy #发现直接用List就行了
    2. import random
    3. import matplotlib.pyplot as plt
    4. from scipy.optimize import minimize
    5. from commonTools import *
    6. # random.random()
    7. # random.randint(start,stop)
    8. #################全局数据定义区
    9. # 数组大小
    10. listSize=10
    11. # 定义学习率 取尽量小0.001
    12. learningRate=0.0001
    13. #定义初始直线的 斜率k 和 截距b 45° 1单位距离
    14. # 现在设置 k=0.5 检验程序
    15. k,b=0.5,1
    16. initialParams=[k,b]
    17. #定义迭代次数
    18. bfsNums=9999
    19. #################全局数据定义区END
    20. # 生成随机数
    21. def generateRandomInteger(start, end):
    22. # [1-100]
    23. return random.randint(start, end)
    24. # 打印本次随机生成的X,Y 便于快速粘贴复现
    25. def printXYArray(XData,YData):
    26. # 打印X
    27. print("[", ",".join([str(i) for i in XData]), "]")
    28. # 打印Y
    29. print("[", ",".join([str(i) for i in YData]), "]")
    30. #调用公共模块进行打印 便于快速查看粘贴
    31. def printXYData(XData,YData):
    32. loc=locals()
    33. printArray(XData,loc)
    34. printArray(YData,loc)
    35. # 最小二乘法定义损失函数 并计算
    36. #参考链接:https://blog.csdn.net/zy_505775013/article/details/88683460
    37. # 求最小二乘法的最小值 最终结果应当是在learningRate一定情况下 这个最小的sum
    38. def calcLoseFunction(params,XData,YData):
    39. k, b = params
    40. sum=0
    41. for i in range(0,listSize):
    42. # 使用偏离值的平方进行累和
    43. sum+=(YData[i]-(k*XData[i]+b))**2
    44. return sum
    45. #梯度下降法
    46. def calcGradientCorrection(b, k, XData, YData, learningRate, bfsNums):
    47. for i in range(0, bfsNums):
    48. sumk, sumb = 0, 0
    49. for j in range(0, listSize):
    50. # 定义预测值Y'
    51. normalNum = k * XData[j] + b
    52. # 计算逆梯度累和
    53. sumk += -(2 / listSize) * (normalNum - YData[j]) * XData[j]
    54. sumb += -(2 / listSize) * (normalNum - YData[j])
    55. # 在逆梯度的方向上进行下一步搜索
    56. k += learningRate * sumk
    57. b += learningRate * sumb
    58. return k, b
    59. # 随机生成横坐标
    60. XData=[generateRandomInteger(1,100) for i in range(listSize) ]
    61. # 随机生成纵坐标
    62. YData=[XData[i]+generateRandomInteger(-10,10) for i in range(listSize) ]
    63. # 纯随机生成 但是可视化效果不直观
    64. # YData=[generateRandomInteger(1,100) for i in range(listSize) ]
    65. # 死值替换区
    66. # XData=testArrayX
    67. # YData=testArrayY
    68. print("初始选取k={},b={}的情况下的损失函数值为sum={}".format(k,b,calcLoseFunction(initialParams,XData,YData)))
    69. # 对k,b进行梯度修正
    70. # k,b=calcGradientCorrection(b,k,XData,YData,learningRate,bfsNums)
    71. #使用Scipy进行求解
    72. result = minimize(calcLoseFunction, initialParams, args=(XData, YData), method='BFGS')
    73. resultk,resultb=result.x
    74. print("修正后:k={},b={},最小损失sum={},最小二乘法损失sums={}".format(resultk,resultb,result.fun,calcLoseFunction([resultk,resultb],XData,YData)))
    75. print("调试数组")
    76. printXYArray(XData,YData)
    77. #画图
    78. plt.plot(XData, YData, 'b.')
    79. plt.plot(XData, resultk*numpy.array(XData)+resultb, 'r')
    80. plt.show()
    81. print("END")

    5.结果展示

    6.进一步优化

    两个目标

    1.优化损失函数中的for循环

    2.对使用Scipy优化前后的代码进行基准测试,比较运行速度

    6.1对如下函数方法进行优化
    1. def calcLoseFunction(params,XData,YData):
    2. k, b = params
    3. sum=0
    4. for i in range(0,listSize):
    5. # 使用偏离值的平方进行累和
    6. sum+=(YData[i]-(k*XData[i]+b))**2
    7. return sum

    使用numpy,优化后如下:

    1. def calcLoseFunction(params,XData,YData):
    2. XData,YData=np.array(XData),np.array(YData)
    3. k, b = params
    4. sum=np.sum((YData - (k * XData + b))**2)
    5. return sum

    无for优化后代码如下:

    1. import numpy as np
    2. import random
    3. import matplotlib.pyplot as plt
    4. from scipy.optimize import minimize
    5. from commonTools import *
    6. #################全局数据定义区
    7. # 数组大小
    8. listSize=10
    9. #定义初始直线的 斜率k 和 截距b 45° 1单位距离
    10. # 现在设置 k=0.5 检验程序
    11. k,b=0.5,1
    12. initialParams=[k,b]
    13. #################全局数据定义区END
    14. # 生成随机数
    15. def generateRandomInteger(start, end):
    16. return random.randint(start, end)
    17. #调用公共模块进行打印 便于快速查看粘贴
    18. def printXYData(XData,YData):
    19. loc=locals()
    20. printArray(XData,loc)
    21. printArray(YData,loc)
    22. # 最小二乘法定义损失函数 并计算
    23. def calcLoseFunction(params,XData,YData):
    24. XData,YData=np.array(XData),np.array(YData)
    25. k, b = params
    26. sum=np.sum((YData - (k * XData + b))**2)
    27. return sum
    28. # 随机生成横坐标
    29. XData=[generateRandomInteger(1,100) for i in range(listSize) ]
    30. # 随机生成纵坐标
    31. YData=[XData[i]+generateRandomInteger(-10,10) for i in range(listSize) ]
    32. # 纯随机生成 但是可视化效果不直观
    33. # YData=[generateRandomInteger(1,100) for i in range(listSize) ]
    34. # 死值替换区
    35. # XData=[ 49,74,62,54,20,14,27,74,23,50 ]
    36. # YData=[ 47,65,56,57,21,21,32,81,27,46 ]
    37. print("初始选取k={},b={}的情况下的损失函数值为sum={}".format(k,b,calcLoseFunction(initialParams,XData,YData)))
    38. #使用Scipy进行求解
    39. result = minimize(calcLoseFunction, initialParams, args=(XData, YData), method='BFGS')
    40. resultk,resultb=result.x
    41. print("修正后:k={},b={},最小损失sum={},最小二乘法损失sums={}".format(resultk,resultb,result.fun,calcLoseFunction([resultk,resultb],XData,YData)))
    42. print("调试数组")
    43. printXYData(XData,YData)
    44. #画图
    45. plt.plot(XData, YData, 'b.')
    46. plt.plot(XData, resultk*np.array(XData)+resultb, 'r')
    47. plt.show()
    48. print("END")

    其中公共模块commonTools.py 代码如下:

    1. #########导包区
    2. #########说明
    3. #1.想要在公共模块区域使用变量列表 必须传进来 因为彼此的变量作用域不同
    4. #########公共变量定义区
    5. #这个locals应该是被引入的界面传进来,而不是从这拿
    6. # loc=locals()
    7. #########函数书写区
    8. #1.获取变量名称
    9. def getVariableName(variable,loc):
    10. for k,v in loc.items():
    11. if loc[k] is variable:
    12. return k
    13. #附带的打印变量名
    14. def printValue(object,loc):
    15. print("变量{}的值是{}".format(getVariableName(object,loc),object))
    16. # 2.组装列表为字符串
    17. def mergeInSign(dataList,sign):
    18. # print(str(sign).join([str(i) for i in dataList]))
    19. return str(sign).join([str(i) for i in dataList])
    20. # 3.打印一个列表
    21. def printArray(dataArray,loc):
    22. print("列表{}的内容是:".format(getVariableName(dataArray,loc)),\
    23. "[", ",".join([str(i) for i in dataArray]), "]"\
    24. )

    原先的代码如下:

    1. import numpy #发现直接用List就行了
    2. import random
    3. import matplotlib.pyplot as plt
    4. # random.random()
    5. # random.randint(start,stop)
    6. #################全局数据定义区
    7. # 数组大小
    8. listSize=10
    9. # 定义学习率 取尽量小0.001
    10. learningRate=0.0001
    11. #定义初始直线的 斜率k 和 截距b 45° 1单位距离
    12. # 现在设置 k=0.5 检验程序
    13. k,b=0.5,1
    14. #定义迭代次数
    15. bfsNums=9999
    16. #################全局数据定义区END
    17. # 生成随机数
    18. def generateRandomInteger(start, end):
    19. # [1-100]
    20. return random.randint(start, end)
    21. # 打印本次随机生成的X,Y 便于快速粘贴复现
    22. def printXYArray(XData,YData):
    23. # 打印X
    24. print("[", ",".join([str(i) for i in XData]), "]")
    25. # 打印Y
    26. print("[", ",".join([str(i) for i in YData]), "]")
    27. # 最小二乘法定义损失函数 并计算
    28. #参考链接:https://blog.csdn.net/zy_505775013/article/details/88683460
    29. # 求最小二乘法的最小值 最终结果应当是在learningRate一定情况下 这个最小的sum
    30. def calcLoseFunction(k,b,XData,YData):
    31. sum=0
    32. for i in range(0,listSize):
    33. # 使用偏离值的平方进行累和
    34. sum+=(YData[i]-(k*XData[i]+b))**2
    35. return sum
    36. #梯度下降法
    37. def calcGradientCorrection(b, k, XData, YData, learningRate, bfsNums):
    38. for i in range(0, bfsNums):
    39. sumk, sumb = 0, 0
    40. for j in range(0, listSize):
    41. # 定义预测值Y'
    42. normalNum = k * XData[j] + b
    43. # 计算逆梯度累和 注意这里求偏导应当是两倍 不知道为什么写成1了
    44. # 求MSE的偏导
    45. sumk += -(2 / listSize) * (normalNum - YData[j]) * XData[j]
    46. sumb += -(2 / listSize) * (normalNum - YData[j])
    47. # 在逆梯度的方向上进行下一步搜索
    48. k += learningRate * sumk
    49. b += learningRate * sumb
    50. return k, b
    51. # 随机生成横坐标
    52. XData=[generateRandomInteger(1,100) for i in range(listSize) ]
    53. # 随机生成纵坐标
    54. YData=[XData[i]+generateRandomInteger(-10,10) for i in range(listSize) ]
    55. # 纯随机生成 但是可视化效果不直观
    56. # YData=[generateRandomInteger(1,100) for i in range(listSize) ]
    57. # 死值替换区
    58. # XData=testArrayX
    59. # YData=testArrayY
    60. print("初始选取k={},b={}的情况下的损失函数值为sum={}".format(k,b,calcLoseFunction(k,b,XData,YData)))
    61. # 对k,b进行梯度修正
    62. k,b=calcGradientCorrection(b,k,XData,YData,learningRate,bfsNums)
    63. print("修正后:k={},b={},最小损失sum={}".format(k,b,calcLoseFunction(k,b, XData, YData)))
    64. print("调试数组")
    65. printXYArray(XData,YData)
    66. #画图
    67. plt.plot(XData, YData, 'b.')
    68. plt.plot(XData, k*numpy.array(XData)+b, 'r')
    69. plt.show()
    70. print("END")

     到此,使用scipy并对for循环进行优化已经完成,下面我们使用程序对比优化后时间效率上有没有改进。

    6.2基准测试

    我们将先后代码的画图部分都注释

    目录结构如下:

     test.py代码如下:

    1. import os #执行调用
    2. import time #记录时间
    3. DEBUG=False
    4. execFileName="old.py" if DEBUG else "new.py"
    5. if __name__=="__main__":
    6. startTime = time.time()
    7. os.system("python {}".format(execFileName))
    8. endTime = time.time()
    9. print("文件:{}执行耗时:{}ms".format(execFileName,endTime-startTime))

    DEBUG为False

    DEBUG为True

    额,调用minimize函数在时间上不如自己写的梯度下降。。。。。

    多次随机测试后发现结果依旧如此,可能是因为scipy引入了其他策略,导致了执行时间变长

    6.3 发现

    使用scipy在某种程度上可能能优化执行效率,但是在部分情况下可能耗时会略长于基本实现

    测试文件附录

    commonTools.py

    1. #########导包区
    2. #########说明
    3. #1.想要在公共模块区域使用变量列表 必须传进来 因为彼此的变量作用域不同
    4. #########公共变量定义区
    5. #这个locals应该是被引入的界面传进来,而不是从这拿
    6. # loc=locals()
    7. #########函数书写区
    8. #1.获取变量名称
    9. def getVariableName(variable,loc):
    10. for k,v in loc.items():
    11. if loc[k] is variable:
    12. return k
    13. #附带的打印变量名
    14. def printValue(object,loc):
    15. print("变量{}的值是{}".format(getVariableName(object,loc),object))
    16. # 2.组装列表为字符串
    17. def mergeInSign(dataList,sign):
    18. # print(str(sign).join([str(i) for i in dataList]))
    19. return str(sign).join([str(i) for i in dataList])
    20. # 3.打印一个列表
    21. def printArray(dataArray,loc):
    22. print("列表{}的内容是:".format(getVariableName(dataArray,loc)),\
    23. "[", ",".join([str(i) for i in dataArray]), "]"\
    24. )

    test.py

    1. import os #执行调用
    2. import time #记录时间
    3. DEBUG=True
    4. execFileName="old.py" if DEBUG else "new.py"
    5. if __name__=="__main__":
    6. startTime = time.time()
    7. os.system("python {}".format(execFileName))
    8. endTime = time.time()
    9. print("文件:{}执行耗时:{}ms".format(execFileName,endTime-startTime))

    new.py

    1. import numpy as np
    2. import random
    3. import matplotlib.pyplot as plt
    4. from scipy.optimize import minimize
    5. from commonTools import *
    6. #################全局数据定义区
    7. # 数组大小
    8. listSize=10
    9. #定义初始直线的 斜率k 和 截距b 45° 1单位距离
    10. # 现在设置 k=0.5 检验程序
    11. k,b=0.5,1
    12. initialParams=[k,b]
    13. #################全局数据定义区END
    14. # 生成随机数
    15. def generateRandomInteger(start, end):
    16. return random.randint(start, end)
    17. #调用公共模块进行打印 便于快速查看粘贴
    18. def printXYData(XData,YData):
    19. loc=locals()
    20. printArray(XData,loc)
    21. printArray(YData,loc)
    22. # 最小二乘法定义损失函数 并计算
    23. def calcLoseFunction(params,XData,YData):
    24. XData,YData=np.array(XData),np.array(YData)
    25. k, b = params
    26. sum=np.sum((YData - (k * XData + b))**2)
    27. return sum
    28. # # 随机生成横坐标
    29. # XData=[generateRandomInteger(1,100) for i in range(listSize) ]
    30. # # 随机生成纵坐标
    31. # YData=[XData[i]+generateRandomInteger(-10,10) for i in range(listSize) ]
    32. # 纯随机生成 但是可视化效果不直观
    33. # YData=[generateRandomInteger(1,100) for i in range(listSize) ]
    34. # 死值替换区
    35. XData=[ 49,74,62,54,20,14,27,74,23,50 ]
    36. YData=[ 47,65,56,57,21,21,32,81,27,46 ]
    37. print("初始选取k={},b={}的情况下的损失函数值为sum={}".format(k,b,calcLoseFunction(initialParams,XData,YData)))
    38. #使用Scipy进行求解
    39. result = minimize(calcLoseFunction, initialParams, args=(XData, YData), method='BFGS')
    40. resultk,resultb=result.x
    41. print("修正后:k={},b={},最小损失sum={},最小二乘法损失sums={}".format(resultk,resultb,result.fun,calcLoseFunction([resultk,resultb],XData,YData)))
    42. print("调试数组")
    43. printXYData(XData,YData)
    44. #画图
    45. # plt.plot(XData, YData, 'b.')
    46. # plt.plot(XData, resultk*np.array(XData)+resultb, 'r')
    47. # plt.show()
    48. print("END")

    old.py 

    1. import numpy # 发现直接用List就行了
    2. import random
    3. import matplotlib.pyplot as plt
    4. from commonTools import *
    5. # random.random()
    6. # random.randint(start,stop)
    7. #################全局数据定义区
    8. # 数组大小
    9. listSize = 10
    10. # 定义学习率 取尽量小0.001
    11. learningRate = 0.0001
    12. # 定义初始直线的 斜率k 和 截距b 45° 1单位距离
    13. # 现在设置 k=0.5 检验程序
    14. k, b = 0.5, 1
    15. # 定义迭代次数
    16. bfsNums = 9999
    17. #################全局数据定义区END
    18. # 生成随机数
    19. def generateRandomInteger(start, end):
    20. # [1-100]
    21. return random.randint(start, end)
    22. # 打印本次随机生成的X,Y 便于快速粘贴复现
    23. def printXYArray(XData, YData):
    24. # 打印X
    25. print("[", ",".join([str(i) for i in XData]), "]")
    26. # 打印Y
    27. print("[", ",".join([str(i) for i in YData]), "]")
    28. # 最小二乘法定义损失函数 并计算
    29. # 参考链接:https://blog.csdn.net/zy_505775013/article/details/88683460
    30. # 求最小二乘法的最小值 最终结果应当是在learningRate一定情况下 这个最小的sum
    31. def calcLoseFunction(k, b, XData, YData):
    32. sum = 0
    33. for i in range(0, listSize):
    34. # 使用偏离值的平方进行累和
    35. sum += (YData[i] - (k * XData[i] + b)) ** 2
    36. return sum
    37. # 梯度下降法
    38. def calcGradientCorrection(b, k, XData, YData, learningRate, bfsNums):
    39. for i in range(0, bfsNums):
    40. sumk, sumb = 0, 0
    41. for j in range(0, listSize):
    42. # 定义预测值Y'
    43. normalNum = k * XData[j] + b
    44. # 计算逆梯度累和 注意这里求偏导应当是两倍 不知道为什么写成1了
    45. # 求MSE的偏导
    46. sumk += -(2 / listSize) * (normalNum - YData[j]) * XData[j]
    47. sumb += -(2 / listSize) * (normalNum - YData[j])
    48. # 在逆梯度的方向上进行下一步搜索
    49. k += learningRate * sumk
    50. b += learningRate * sumb
    51. return k, b
    52. # 随机生成横坐标
    53. XData = [generateRandomInteger(1, 100) for i in range(listSize)]
    54. # 随机生成纵坐标
    55. YData = [XData[i] + generateRandomInteger(-10, 10) for i in range(listSize)]
    56. # 纯随机生成 但是可视化效果不直观
    57. # YData=[generateRandomInteger(1,100) for i in range(listSize) ]
    58. # 死值替换区
    59. # XData=[ 49,74,62,54,20,14,27,74,23,50 ]
    60. # YData=[ 47,65,56,57,21,21,32,81,27,46 ]
    61. print("初始选取k={},b={}的情况下的损失函数值为sum={}".format(k, b, calcLoseFunction(k, b, XData, YData)))
    62. # 对k,b进行梯度修正
    63. k, b = calcGradientCorrection(b, k, XData, YData, learningRate, bfsNums)
    64. print("修正后:k={},b={},最小损失sum={}".format(k, b, calcLoseFunction(k, b, XData, YData)))
    65. print("调试数组")
    66. printXYArray(XData, YData)
    67. # 画图
    68. # plt.plot(XData, YData, 'b.')
    69. # plt.plot(XData, k * numpy.array(XData) + b, 'r')
    70. # plt.show()
    71. print("END")

    任务清单

    1.算法程序不使用任何for循环(已完成)

    2.使用scipy对原先的代码进行优化(已完成)

    3.对优化前后代码进行基准测试(已完成)

  • 相关阅读:
    6、Nacos服务多级存储模型
    JAVA--企业级分层开发模式
    【无标题】
    feign 和 openFeign 的区别
    DBSCAN算法
    SpringBoot-基础篇复习(全)
    通达OA通用版V12的表单js定制开发,良好实践总结-持续更新
    简单介绍Spring中的事物
    Linux中防火墙firewalld
    非DBA人员从零到一,MySQL InnoDB数据库调优之路(四)-数据备份与迁移
  • 原文地址:https://blog.csdn.net/m0_72678953/article/details/133606066