PID控制器优化:一种梯度下降方法

PID控制器优化方法

使用机器学习来解决工程优化问题

梯度下降算法通过下坡步骤来最小化成本函数

机器学习。深度学习。人工智能。越来越多的人每天都在使用这些技术。这在很大程度上是由ChatGPT、Bard等部署的大型语言模型的崛起推动的。尽管它们被广泛使用,但相对较少的人熟悉支持这些技术的方法。

在本文中,我们将深入研究机器学习中部署的一种基本方法:梯度下降算法。

我们将不再通过神经网络的视角来看待梯度下降,而是将其作为解决经典工程优化问题的工具进行考察。

具体来说,我们将使用梯度下降来调整汽车巡航控制系统的PID(比例-积分-微分)控制器的增益。

采用这种方法的动机有两个:

首先,在神经网络中优化权重和偏置是一个高维问题。其中有许多移动部分,我认为这些会分散对梯度下降解决优化问题的基本实用性的注意力。

其次,正如您将看到的,梯度下降在应用于经典工程问题(如PID控制器调谐、机器人的逆运动学和拓扑优化)时可以是一个强大的工具。梯度下降是一种工具,我认为更多的工程师应该熟悉并能够利用。

阅读完本文后,您将了解什么是PID控制器,梯度下降算法如何工作以及如何应用于解决经典工程优化问题。您可能会被激发使用梯度下降来应对自己的优化挑战。

本文中使用的所有代码都可在GitHub上找到。

什么是PID控制器?

PID控制器是工程和自动化系统中广泛使用的反馈控制机制。它旨在通过基于设定点和系统测量输出(过程变量)之间的误差连续调整控制信号,以维持所需设定点。

PID控制器的典型阶跃响应

PID控制器在各个行业和领域中都有广泛的应用。它们在过程控制系统中广泛用于制造业的温度控制、化工厂的流量控制和暖通空调系统的压力控制等。PID控制器还被用于机器人的精确定位和运动控制,以及汽车系统的节流阀控制、发动机转速调节和防抱死制动系统。它们在航空航天应用中发挥着重要作用,包括飞机自动驾驶仪和姿态控制系统。

PID控制器由三个组成部分组成:比例项、积分项和微分项。比例项对当前误差提供即时响应,积分项累积并纠正过去的误差,微分项预测并抵消未来的误差趋势。

PID控制器的块图

PID控制器的控制回路如上图所示。r(t)是设定点,y(t)是过程变量。过程变量减去设定点得到误差信号e(t)。

控制信号u(t)是比例项、积分项和微分项之和。控制信号输入到过程中,这反过来导致过程变量更新。

PID控制器的控制信号u(t)

梯度下降算法

梯度下降是一种常用于机器学习和数学优化的优化算法。它通过迭代地根据成本函数梯度调整参数,以找到给定成本函数的最小值。梯度指向最陡上升的方向,因此通过朝相反的方向迈出步伐,算法逐渐收敛于最优解。

单次梯度下降更新步骤的定义如下:

梯度下降更新步骤

其中aₙ是输入参数的向量。下标n表示迭代次数。f(aₙ)是一个多变量的代价函数,∇f(a)是该代价函数的梯度。∇f(aₙ)代表最陡上升的方向,因此它从aₙ中减去,以减少下一次迭代中的代价函数。𝛾是学习率,它决定每次迭代的步长。

必须选择适当的𝛾值。如果太大,则每次迭代的步长将太大,导致梯度下降算法不收敛。如果太小,则梯度下降算法的计算成本会很高,并且收敛所需的时间会很长。

梯度下降算法应用于y=x²代价函数(初始x=5),𝛾=0.1(左)和𝛾=1.02(右)

梯度下降在各个领域和学科中都有应用。在机器学习和深度学习中,它是一种基本的优化算法,用于训练神经网络和优化其参数。通过根据代价函数的梯度迭代更新网络的权重和偏置,梯度下降使网络能够学习并随着时间的推移提高性能。

除了机器学习,梯度下降在工程学、物理学、经济学和其他领域的各种优化问题中都得到应用。它在参数估计、系统识别、信号处理、图像重建和许多其他需要找到函数的最小值或最大值的任务中发挥作用。梯度下降的多功能性和有效性使其成为解决优化问题、改进模型和系统的重要工具。

使用梯度下降优化PID控制器增益

有几种方法可以调整PID控制器。这些方法包括手动调整方法和启发式方法,如Ziegler-Nichols方法。手动调整方法可能耗时,并且可能需要多次迭代才能找到最佳值,而Ziegler-Nichols方法通常会产生过大的增益和大的超调,这意味着它不适用于某些应用。

这里介绍了一种使用梯度下降方法进行PID控制器优化的方法。我们将优化汽车巡航控制系统,该系统受到设定点的阶跃变化影响。

通过控制踏板位置,控制器的目标是将汽车加速到速度设定点,并尽量减小超调、调整时间和稳态误差。

汽车受到与踏板位置成比例的驱动力作用。滚动阻力和空气阻力力与驱动力方向相反。踏板位置由PID控制器控制,并限制在-50%至100%的范围内。当踏板位置为负数时,汽车正在制动。

在调整PID控制器增益时,拥有系统模型是有帮助的。这样我们就可以模拟系统的响应。为此,我在Python中实现了一个Car类:

import numpy as npclass Car:    def __init__(self, mass, Crr, Cd, A, Fp):        self.mass = mass # [kg]        self.Crr = Crr # [-]        self.Cd = Cd # [-]        self.A = A # [m^2]        self.Fp = Fp # [N/%]        def get_acceleration(self, pedal, velocity):        # Constants        rho = 1.225 # [kg/m^3]        g = 9.81 # [m/s^2]        # Driving force        driving_force = self.Fp * pedal        # Rolling resistance force        rolling_resistance_force = self.Crr * (self.mass * g)        # Drag force        drag_force = 0.5 * rho * (velocity ** 2) * self.Cd * self.A        acceleration = (driving_force - rolling_resistance_force - drag_force) / self.mass        return acceleration        def simulate(self, nsteps, dt, velocity, setpoint, pid_controller):        pedal_s = np.zeros(nsteps)        velocity_s = np.zeros(nsteps)        time = np.zeros(nsteps)        velocity_s[0] = velocity        for i in range(nsteps - 1):            # Get pedal position [%]            pedal = pid_controller.compute(setpoint, velocity, dt)            pedal = np.clip(pedal, -50, 100)            pedal_s[i] = pedal            # Get acceleration            acceleration = self.get_acceleration(pedal, velocity)                        # Get velocity            velocity = velocity_s[i] + acceleration * dt            velocity_s[i+1] = velocity            time[i+1] = time[i] + dt                return pedal_s, velocity_s, time

PIDController类的实现如下:

class PIDController:    def __init__(self, Kp, Ki, Kd):        self.Kp = Kp        self.Ki = Ki        self.Kd = Kd        self.error_sum = 0        self.last_error = 0        def compute(self, setpoint, process_variable, dt):        error = setpoint - process_variable                # 比例项        P = self.Kp * error                # 积分项        self.error_sum += error * dt        I = self.Ki * self.error_sum                # 微分项        D = self.Kd * (error - self.last_error)        self.last_error = error                # PID输出        output = P + I + D                return output

采用面向对象的编程方法使得在运行梯度下降算法时可以更容易地设置和运行具有不同PID控制器增益的多个模拟。

GradientDescent类的实现如下:

class GradientDescent:    def __init__(self, a, learning_rate, cost_function, a_min=None, a_max=None):        self.a = a        self.learning_rate = learning_rate        self.cost_function = cost_function        self.a_min = a_min        self.a_max = a_max        self.G = np.zeros([len(a), len(a)])        self.points = []        self.result = []        def grad(self, a):        h = 0.0000001        a_h = a + (np.eye(len(a)) * h)        cost_function_at_a = self.cost_function(a)        grad = []        for i in range(0, len(a)):            grad.append((self.cost_function(a_h[i]) - cost_function_at_a) / h)        grad = np.array(grad)        return grad        def update_a(self, learning_rate, grad):        if len(grad) == 1:            grad = grad[0]        self.a -= (learning_rate * grad)        if (self.a_min is not None) or (self.a_min is not None):            self.a = np.clip(self.a, self.a_min, self.a_max)        def update_G(self, grad):        self.G += np.outer(grad,grad.T)        def execute(self, iterations):        for i in range(0, iterations):            self.points.append(list(self.a))            self.result.append(self.cost_function(self.a))            grad = self.grad(self.a)            self.update_a(self.learning_rate, grad)        def execute_adagrad(self, iterations):        for i in range(0, iterations):            self.points.append(list(self.a))            self.result.append(self.cost_function(self.a))            grad = self.grad(self.a)            self.update_G(grad)            learning_rate = self.learning_rate * np.diag(self.G)**(-0.5)            self.update_a(learning_rate, grad)

通过调用executeexecute_adagrad方法来运行指定次数的算法。execute_adagrad方法执行一种改进的梯度下降算法,称为AdaGrad(自适应梯度下降)。

AdaGrad具有每个参数的学习率,对于稀疏参数增加,对于不那么稀疏的参数减少。学习率根据梯度的历史平方和在每次迭代后更新。

我们将使用AdaGrad来优化汽车巡航控制系统的PID控制器增益。使用AdaGrad,梯度下降更新方程变为:

AdaGrad梯度下降更新步骤

现在我们需要定义成本函数。成本函数必须将输入参数的向量作为输入,并返回一个单一的数字,即成本。汽车巡航控制的目标是以最小的超调量、调整时间和稳态误差将汽车加速到速度设定点。根据这个目标,我们可以基于许多方式来定义成本函数。在这里,我们将其定义为随时间积分的误差幅度:

汽车巡航控制成本函数

由于我们的成本函数是一个积分,我们可以将其视为误差幅度曲线下的面积。我们期望随着接近全局最小值,曲线下的面积减少。在编程上,成本函数定义如下:

def car_cost_function(a):    # 汽车参数    mass = 1000.0  # 汽车质量 [kg]    Cd = 0.2  # 阻力系数 []    Crr = 0.02 # 滚动阻力 []    A = 2.5 # 汽车的正面积 [m^2]    Fp = 30 # 每个%踏板位置的驱动力 [N/%]    # PID控制器参数    Kp = a[0]    Ki = a[1]    Kd = a[2]    # 模拟参数    dt = 0.1  # 时间步长    total_time = 60.0  # 总模拟时间    nsteps = int(total_time / dt)    initial_velocity = 0.0  # 汽车的初始速度 [m/s]    target_velocity = 20.0 # 汽车的目标速度 [m/s]    # 定义汽车和PIDController对象    car = Car(mass, Crr, Cd, A, Fp)    pid_controller = PIDController(Kp, Ki, Kd)    # 运行模拟    pedal_s, velocity_s, time = car.simulate(nsteps, dt, initial_velocity, target_velocity, pid_controller)    # 计算成本    cost = np.trapz(np.absolute(target_velocity - velocity_s), time)    return cost

成本函数包括模拟参数。模拟运行时长为60秒。在此期间,我们观察系统对从0 m/s到20 m/s的设定点步变的响应。通过对时间的积分误差幅度,计算每次迭代的成本。

现在,唯一要做的就是运行优化算法。我们将从初始值Kp = 5.0、Ki = 1.0和Kd = 0.0开始。这些值给出了一个稳定的、振荡的响应,具有超调,并最终收敛到设定点。从这个起点开始,我们将使用基本学习率为𝛾=0.1运行梯度下降算法500次迭代:

a = np.array([5.0, 1.0, 0.0])gradient_descent = GradientDescent(a, 0.1, car_cost_function, a_min=[0,0,0])gradient_descent.execute_adagrad(500)
汽车巡航控制器的步变响应(左侧),误差幅度(中间)和成本(右侧),随着梯度下降算法迭代朝着最优解逼近

上面的动画图显示了汽车巡航控制器的步变响应随着梯度下降算法调整PID控制器的Kp、Ki和Kd增益的演变过程。

在第25次迭代时,梯度下降算法消除了振荡响应。在此之后,发生了一些有趣的事情。算法陷入了一个局部最小值,其特点是超调约为3 m/s。这发生在6.0 < Kp < 7.5、Ki ~= 0.5、Kd = 0.0的区域,并持续到第300次迭代。

在第300次迭代之后,算法摆脱了局部最小值,找到了一个更令人满意的响应,接近全局最小值。现在的响应特点是零超调、快速稳定时间和接近零的稳态误差。

将梯度下降算法运行500次迭代后,我们得到了优化后的PID控制器增益;Kp = 8.33,Ki = 0.12和Kd = 0.00。

比例增益仍在稳步上升。运行更多迭代(此处未显示),随着Kp的缓慢增加,我们发现进一步减少成本函数是可能的,尽管这种影响越来越小。

总结

采用广泛用于解决机器学习和深度学习问题的方法,我们成功地优化了汽车巡航控制系统的PID控制器增益。

从初始值Kp = 5.0、Ki = 1.0和Kd = 0.0开始,并应用梯度下降算法的AdaGrad形式,我们观察到这个低维系统首先陷入局部最小值,然后最终找到了一个更令人满意的响应,具有零超调、快速稳定时间和接近零的稳态误差。

在本文中,我们看到梯度下降在应用于经典工程优化问题时可以是一个强大的工具。除了这里所示的示例之外,梯度下降还可以用于解决其他工程问题,如机器人的逆运动学、拓扑优化等等。

您是否有认为梯度下降可以应用于的优化问题?在下面的评论中告诉我。

喜欢阅读本文吗?

跟随并订阅更多类似内容 – 与您的网络分享 – 尝试将梯度下降应用于您自己的优化问题。

除非另有说明,所有图片均为作者所拍摄。

参考文献

网络

[1] GitHub (2023), pid_controller_gradient_descent

[2] Wikipedia (2023), Ziegler–Nichols method (访问时间:2023年7月10日)