1 优化算法

最优化是指非线性最优化,解非线性最优化的方法有很多

比如梯度下降法共轭梯度法变尺度法步长加速法等

参考本站链接机器学习_最优化方法

优化器之间的关系脉络

1.1 概述

pytorch优化器

飞浆官方文档,总结到位

深度学习优化器方法及学习率衰减方式综述

An overview of gradient descent optimization algorithms

1847 1951 1983 2011 2012
GD(BGD) SGD SGDM(Momentum)、NAG AdaGrad Adadelta、RMSprop
2015 2016 2018
Adam、AdaMax Nadam AMSGrad

相对应的论文

A Stochastic Approximation Method SGD 1951

Learning representations by back-propagating errors Momentum 1983

A method for unconstrained convex minimization problem with the rate of convergence o(1/k2) NAG 1983

Adaptive subgradient methods for online learning and stochastic optimization AdaGrad 2011

ADADELTA: an adaptive learning rate method. Adadelta 2012

Neural Networks for Machine Learning Lecture 6a Overview of mini-batch gradient descent RMSprop 2012

Adam: A method for stochastic optimization Adam & AdaMax 2014

Incorporating Nesterov Momentum into Adam NAdam 2016

以下是优化器的发展脉络,按照时间顺序列出了一些重要的优化器及其年份:

  1. Gradient Descent (GD):最早的优化器之一,用于求解无约束优化问题。没有特定的年份,但早在20世纪50年代就开始被广泛应用

  2. Stochastic Gradient Descent (SGD):引入随机性来估计梯度的优化器,用于大规模数据集和深度学习模型。没有特定的年份,但在深度学习的早期就被广泛使用

  3. Momentum(1983):提出了动量概念,通过累积梯度的指数加权平均来加速收敛

  4. AdaGrad(2011):自适应梯度算法,通过对梯度进行归一化和调整学习率,适应不同参数的更新需求

  5. Adadelta(2012):改进了AdaGrad的缺点,通过考虑历史梯度的平均值来调整学习率

    RMSprop(2012):引入了指数加权移动平均的概念,用于调整学习率以平衡历史梯度信息

  6. Adam(2014):结合了动量和自适应学习率的优点,通过自适应调整学习率和梯度的一阶矩估计和二阶矩估计来进行参数更新

    AdaMax(2014):基于Adam算法,通过替换二阶矩估计的范数为无穷范数,提供了更稳定的更新规则

  7. Nadam(2016):结合了Nesterov动量和Adam算法,利用动量来加速收敛

  8. AMSGrad(2018):对Adam算法进行了改进,解决了Adam算法学习率下降不稳定的问题

这些是一些比较重要的优化器,并且按照时间顺序列出。然而,需要注意的是,并非所有的优化器都是线性发展的,而是相互借鉴、改进和结合的结果。优化器的发展是一个活跃的研究领域,仍然有许多新的优化算法被提出和改进

什么是优化器

深度学习的目标是通过不断改变网络参数,使得参数能够对输入做各种非线性变换拟合输出,本质上就是一个函数去寻找最优解,所以如何去更新参数是深度学习研究的重点

通常将更新参数的算法称为优化器,字面理解就是通过什么算法去优化网络模型的参数

梯度下降核心点

  1. 方向: 确定优化的方向,一般通过求导便可以求得
  2. 步长: 步子就是决定当前走多大,如果学习率设的过大,梯度会在最优点来回跳动,设的过小需要很久的训练才能达到最优点

优化器的主要作用

  1. 参数更新:优化器根据损失函数的梯度信息,计算出每个参数的更新量,并将更新量应用于参数,从而更新模型的参数。这样,模型的参数就可以朝着能够更好地拟合训练数据的方向进行调整
  2. 学习率调整:优化器通常会自动调整学习率,以控制参数更新的步幅。学习率决定了每次参数更新的幅度,过大的学习率可能导致参数更新过快而错过最优解,而过小的学习率可能导致收敛速度缓慢。优化器根据当前训练的进度和参数的变化情况,动态地调整学习率,以获得更好的训练效果
  3. 优化算法选择:优化器提供了多种不同的优化算法,如梯度下降、动量优化、自适应学习率等。这些算法在参数更新的方式、学习率调整策略等方面有所不同,可以根据具体任务的需求选择合适的优化算法

通过合适的优化器选择和参数调整,可以提高神经网络的训练效率和性能,加速收敛过程,使得模型能够更好地拟合训练数据,并在测试数据上取得较好的泛化能力

优化器分类

  1. 梯度下降优化器(Gradient Descent Optimizers):基于梯度信息来更新参数的优化器,包括批量梯度下降(BGD)、随机梯度下降(SGD)和小批量梯度下降(Mini-Batch Gradient Descent,MBGD)等
  2. 基于动量的优化器(Momentum-based Optimizers):在梯度下降的基础上引入动量的概念,旨在加速收敛过程并减少震荡,常见的包括动量优化器(Momentum Optimizer)、牛顿加速度动量优化法Nesterov Accelerated Gradient(NAG)等
  3. 自适应学习率优化器(Adaptive Learning Rate Optimizers):根据参数更新的情况动态地调整学习率,以提高收敛速度和效果,常见的包括AdaGrad、RMSprop、Adam、AdaDelta、Adamax等
  4. 学习率衰减优化器(Learning Rate Decay Optimizers):在训练过程中逐渐减小学习率的优化器,常见的包括Step Decay、Exponential Decay、Piecewise Decay等
  5. 正则化优化器(Regularization Optimizers):结合正则化技术,通过对损失函数添加正则化项来控制模型的复杂度,常见的包括L1正则化、L2正则化等
  6. 二阶优化器(Second-Order Optimizers):考虑参数二阶信息的优化器,如牛顿法(Newton's Method)、共轭梯度法(Conjugate Gradient)等

不同类型的优化器在更新参数的方式、学习率调整策略、收敛速度、对噪声和局部最优的鲁棒性等方面有所区别,选择合适的优化器取决于具体的问题和数据集特征

1.2 基本的梯度下降法

优化器综述

深度学习——优化器算法Optimizer详解(BGD、SGD、MBGD、Momentum、NAG、Adagrad、Adadelta、RMSprop、Adam)

优化器的存在就是确定优化的方向和面对当前的情况动态的调整步子

BGD、SGD和MBGD的区别

优化器 BGD SGD MBGD
样本数 N(所有) 1 batch_size

1.2.1 BGD

BGD(Batch Gradient Descent)采用整个训练集的数据来计算cost function对参数的梯度 其中为学习率,而为损失函数的一阶导数

BGD在计算梯度时会出现冗余

因为BGD在每一次迭代中都使用了整个训练集,而且在梯度计算过程中并没有考虑样本之间的相关性

因此,对于样本中的某些部分,其梯度计算可能会与其他样本的梯度计算重复,这种冗余计算可能会导致计算效率的降低,特别是在训练集很大的情况下

优点

  1. 收敛稳定:由于每次迭代使用整个训练集的所有样本进行参数更新,收敛过程相对稳定
  2. 参数更新准确:使用全局梯度来更新参数,对于凸优化问题,可以达到全局最优解

缺点

  1. 训练速度慢:BGD 是一种批量梯度下降算法,每次更新模型参数时使用整个训练数据集
  2. 计算开销大:需要计算整个训练集的梯度,对于大规模数据集或复杂模型,计算开销较高
  3. 内存占用高:需要存储整个训练集的数据和梯度信息

1.2.2 SGD

SGD(Stochastic Gradient Descent)是一种随机梯度下降算法,每次更新模型参数时使用单个样本一小批样本(通常称为mini-batch,也称MBGD) 其中为学习率,而为损失函数的一阶导数,为batch_size,当就是SGD,否则就是MBGD

优点

  1. 计算开销小:每次迭代只使用一个样本进行参数更新,计算开销较小
  2. 适用于大规模数据集:由于样本的随机选择,可以处理大规模数据集,且易于并行处理

缺点

  1. 参数更新不稳定:由于单个样本的梯度计算可能存在噪声,参数更新不稳定,可能引起参数在最优点附近震荡
  2. 收敛速度较慢:由于参数更新的不稳定性,收敛速度相对较慢

1.2.3 MBGD

MBGD(Mini-Batch Gradient Descent)每一次利用一小批样本,即batch_size个样本进行计算,这样它可以降低参数更新时的方差,收敛更稳定,另一方面可以充分地利用深度学习库中高度优化的矩阵操作来进行更有效的梯度计算 其中为学习率,而为损失函数的一阶导数,为batch_size,当就是SGD,否则就是MBGD

优点

  1. 平衡了开销和参数稳定性:使用一小批样本进行参数更新,综合了全局梯度和随机梯度的信息,计算开销和参数更新的稳定性得到一定的平衡
  2. 收敛速度较快:相对于BGD,使用较小的批量样本更新参数,收敛速度更快

缺点

  1. 批量大小需调优:批量大小的选择可能会影响模型的性能,需要进行调优
  2. 可能导致局部最优:较小的批量样本可能会引入一定的随机性,可能导致陷入局部最优而无法达到全局最优

1.3 动量优化法

1.3.1 SGDM

随机梯度下降法虽然有效,但容易陷入局部最小值点,甚至在驻点附近以及梯度值非常小的点附近时参数更新极为缓慢

为了抑制SGD的震荡,SGDM(Stochastic Gradient Descent Momentum)认为梯度下降过程可以加入惯性

主要思想是下降过程中,如果发现是陡坡,那就利用惯性跑的快一些。因此,其在SGD基础上引入了一阶动量

在坡度比较陡的地方,会有较大的惯性,这是下降的多。坡度平缓的地方,惯性较小,下降的会比较慢 其中为学习率,表示当前t时刻梯度,表示当前时刻的加权后的梯度,动量系数

的经验值为0.9(表示最大速度10倍于SGD),这就意味着下降方向主要是此前累积的下降方向,并略微偏向当前时刻的下降方向

一阶动量是各个时刻梯度方向的指数移动平均值,也就是说,时刻的下降方向,不仅由当前点的梯度方向决定,而且由此前累积的下降方向决定

梯度是如何累积的

这里将展开,可以看到当前时刻的梯度是对历史梯度进行加权得到的,其中,是一个介于0和1之间的参数,控制了历史梯度对当前动量的贡献程度

  • 较大的值会使历史梯度的贡献更大,从而使动量更加平滑
  • 较小的值会使当前梯度的贡献更大,从而对变化更为敏感

这种权重衰减的方式使得历史梯度的贡献逐渐减小,更加关注近期的梯度变化,有助于适应变化的数据和模型参数

优点

  1. 加速收敛:SGDM引入了动量的概念,通过累积之前的动量信息,有助于加速模型的收敛速度,特别是在存在平坦区域的情况下更为明显

  2. 减少震荡:动量的累积作用可以减少参数更新时的震荡现象,有助于更稳定地更新模型参数

  3. 尽可能跳出局部最优:动量的引入可以帮助模型跳出局部最优点,以便更好地搜索全局最优点

    当局部沟壑比较深,动量加持用完了,依然会困在局部最优里来回振荡

缺点

  1. 需要调整超参数:SGDM中的动量系数需要手动设置,选择合适的动量系数对于模型的性能影响较大,需要进行调试和调优
  2. 可能导致过拟合:当动量系数较大时,SGDM可能在优化过程中过度依赖之前的动量信息,导致模型过拟合
  3. 难以处理非平稳数据:对于非平稳数据,SGDM的动量累积可能会导致模型在变化快速的方向上过度追踪,而无法及时适应变化

改进方法

  1. 自适应调整动量系数:可以采用自适应的方式来调整动量系数,例如使用自适应的动量方法(如Adam)来根据梯度的变化自动调整动量系数
  2. 学习率调度策略:结合学习率调度策略,如学习率衰减自适应学习率方法,可以更好地控制模型的学习速度和方向
  3. 正则化技术:使用正则化技术,如L1正则化或L2正则化,可以缓解过拟合问题,使模型更具泛化能力

因为加入了动量因素,SGDM缓解了SGD在局部最优点梯度为0,无法持续更新的问题和振荡幅度过大的问题,但是并没有完全解决,当局部沟壑比较深,动量加持用完了,依然会困在局部最优里来回振荡

1.3.2 NAG

深度学习优化函数详解-- Nesterov accelerated gradient (NAG)

比Momentum更快:揭开Nesterov Accelerated Gradient的真面目

动量法每下降一步都是由前面下降方向的一个累积和当前点的梯度方向组合而成。于是一位大神(Nesterov)就开始思考,既然每一步都要将两个梯度方向(历史梯度、当前梯度)做一个合并再下降,那为什么不先按照历史梯度往前走那么一小步,按照前面一小步位置的超前梯度来做梯度合并呢

如此一来,小球就可以先不管三七二十一先往前走一步,在靠前一点的位置看到梯度,然后按照那个位置再来修正这一步的梯度方向,同SGDM比较的差一点如下公式所示

既然知道会走,就不需要还用当前位置的梯度,可以直接走到的位置计算梯度,这样子就有了超前眼光

有了超前的眼光,小球就会更加聪明, 这种方法被命名为牛顿加速梯度(Nesterov accelerated gradient)简称NAG,下图是SGDM下降法与NAG下降法的可视化比较

momentum下降法与NAG下降法比较

NAG算法公式表达如下:

为什么NAG比SGDM快

对NAG原来的更新公式进行变换,得到这样的等效形式(具体推导过程) 与Momentum的区别在于,本次更新方向多加了一个

直观含义就很明显了:如果这次的梯度比上次的梯度变大了,那么有可能会继续变大,可以把预计增大的部分提前加进来;变小的情况类似

这个多加上去的项就是在近似目标函数的二阶导嘛,因此,NAG本质上是多考虑了目标函数的二阶导信息,其实所谓往前看的说法,在牛顿法这样的二阶方法中也是经常提到的,从数学角度上看,则是利用了目标函数的二阶导信息

1.4 自适应学习率优化器

1.4.1 AdaGrad

李宏毅深度学习笔记-Adagrad算法

AdaGrad 算法

优化的变量对于目标函数的依赖是各不相同

在基本的梯度下降法优化中,有一个常见的问题是,要优化的变量对于目标函数的依赖是各不相同的

  • 对于某些变量,已经优化到了极小值附近,但是有的变量仍然在梯度很大的地方,这时候一个统一的全局学习率是可能出现问题的

  • 如果学习率太小,则梯度很大的变量会收敛很慢,如果梯度太大,已经优化差不多的变量就可能会不稳定

现实世界的数据集中,一些特征是稀疏的(大部分特征为零,所以它是稀疏的),而另一些则是密集的(dense,大部分特征是非零的),因此为所有权值保持相同的学习率不利于优化

针对这个问题,当时在伯克利加州大学读博士的Jhon Duchi,2011年提出了AdaGrad(Adaptive Gradient),也就是自适应学习率

基本思想

AdaGrad的基本思想是对每个变量用不同的学习率,设置了全局学习率之后,每次通过,全局学习率逐参数的除以历史梯度平方和的平方根,使得每个参数的学习率不同

这个学习率在一开始会比较大,用于快速梯度下降。随着优化过程的进行,对于已经下降很多的变量,则减缓学习率,对于还没怎么下降的变量,则保持一个较大的学习率

公式

其中为学习率,而为损失函数的一阶导数,是一个平滑项,避免了除以零(通常取值在左右),表示元素逐元素相乘操作

可以写成下式,每个参数的所有偏微分的平方和,是对梯度的缩写

优缺点

优点

  • 自适应的学习率,无需人工调节,AdaGrad在迭代过程中不断调整学习率,并让目标函数中的每个参数都分别拥有自己的学习率,学习率默认值为0.01
  • 有效地处理稀疏特征,因为它能够自动调整每个特征的学习率,使得稀疏特征的更新更少

缺点

  • 全局学习率: 仍需要手工设置一个全局学习率, 如果设置过大的话,会使regularizer过于敏感,对梯度的调节太大
  • 训练停止: 由于梯度平方和的累积,学习率会不断衰减,可能导致在训练后期学习率过小,造成收敛速度过慢或者提前停止训练的问题(Adadelta算法解决)

附上别人写的代码

def sgd_adagrad(parameters, sqrs, lr):
    eps = 1e-10
    for param, sqr in zip(parameters, sqrs):
        sqr[:] = sqr + param.grad.data ** 2
        div = lr / torch.sqrt(sqr + eps) * param.grad.data
        param.data = param.data - div

1.4.2 RMSProp

An overview of gradient descent optimization algorithms 2017

RMSProp(Root Mean Square Propagation)是Hinton大神于2012年在一门叫Neural Networks for Machine Learning的在线课程中提出(并未正式发表),是梯度下降优化算法的扩展

RMSProp实际上是Adagrad引入了Momentum,公式表达如下所示 是学习率,则类似于动量梯度下降法中的衰减因子,代表过去梯度对当前梯度的影响,一般取值0.9,是一个平滑项,避免了除以零(通常取值在左右),表示元素逐元素相乘操作(也可以省略不写)

公式里的累积梯度平方和可以展开写成下面的形式,是对梯度的缩写(同AdaGrad)

优点

缺点

代码(参考文档)

drad_squared = 0
for _ in num_iterations:
    dw = compute_gradients(x, y)
    grad_squared = 0.9 * grads_squared + 0.1 * dx * dx
    w = w - (lr / np.sqrt(grad_squared)) * dw

1.4.3 Adadelta

优化器(AdaGrad,AdaDelta,RmsProp,Adam,Nadam,Nesterovs,Sgd,momentum)

AdaDelta算法两种解决方案

Adadelta 优化器

由于AdaGrad调整学习率变化过于激进,我们考虑一个改变二阶动量计算方法的策略:不累积全部历史梯度,而只关注过去一段时间窗口的下降梯度

即Adadelta只累加固定大小的项,并且也不直接存储这些项,仅仅是近似计算对应的平均值(指数移动平均值),这就避免了二阶动量持续累积、导致训练过程提前结束的问题了

论文中提到了两种实现策略

方法一: Accumulate Over Window

从全部历史梯度变为当前时间向前的一个窗口期内的累积,计算定义为 相当于历史梯度信息的累计乘上一个衰减系数,然后用作为当前梯度的平方加权系数相加

梯度更新公式为 解决了对历史梯度一直累加而导致学习率一直下降的问题

方法二: Correct Units with Hessian Approximation

在1988年LeCun等人曾经提出一种用矩阵对角线元素来近似逆矩阵 diag函数指的是构造Hessian矩阵的对角矩阵,是常数项,防止分母为0

如果学过数值分析的同学应该知道,牛顿法用Hessian矩阵替代人工设置的学习率,在梯度下降的时候,可以完美的找出下降方向,不会陷入局部最小值当中,是理想的方法,但是Hessian矩阵的逆在数据很大的情况下根本没办法求

2012年,[Schaul&S. Zhang&LeCun]借鉴了AdaGrad的做法,提出了更精确的近似 指的是从当前t开始的前w个梯度状态的期望值

指的是从当前t开始的前w个梯度状态的平方的期望值

同样是基于Gradient的Regularizer,不过只取最近的w个状态,这样不会让梯度被惩罚至0

这里如果求期望的话,非常的麻烦,所以采取了移动平均法来计算。这里作者在论文中也给出了近似的证明 这里是当为指数型函数, 最后一个近似成立。 对于牛顿法: 由上式可得: 基中: 这里可以用局部的加权指数平滑来替代,即: 这里的RMS表示均方: 可以得到: 最后的更新公式为

优点

  1. 无需手动设置学习率:Adadelta能够自适应地调整学习率,无需手动设置
  2. 解决了学习率衰减问题:由于采用了衰减平均的方式,Adadelta能够解决学习率随时间衰减过快的问题,使得模型能够更好地收敛
  3. 不依赖全局学习率:Adadelta不需要设置全局学习率,因此可以适应不同参数的学习速度需求
  4. 对初始学习率不敏感:Adadelta相对于其他优化器对初始学习率的选择并不敏感,使得模型更具鲁棒性

缺点

  1. 存储额外的状态信息:Adadelta需要保存额外的状态信息(如梯度平方的累积),增加了存储的开销
  2. 算法参数要调整:Adadelta中的衰减系数需要进行适当的调整,不同任务可能需要不同的设置

1.4.4 Adam

Adam(Adaptive Moment Estimation)自适应矩估计,是另一种自适应学习率的算法,本质上是带有动量项的Adadelta或RMSprop

是Diederik P. Kingma等人在2014年提出的优化算法,引入了两个参数

思路

Adam不仅如RMSProp算法那样基于一阶矩均值计算适应性参数学习率,它同时还充分利用了梯度的二阶矩均值(即有偏方差),适合解决含大规模的数据和参数的优化目标,也适合解决包含高噪声或稀疏梯度的问题,让参数更新时保持稳定

其中控制一阶动量,控制二阶动量

最终的参数更新公式为 默认值设置

优点

  • 自适应学习率:Adam通过自适应地调整每个参数的学习率,可以有效地应对不同参数的梯度变化情况。这使得它在训练过程中更容易收敛,并且对于大多数任务具有较好的性能,但是需要注意的是它的效果有时候不如SGDM
  • 速度快:Adam结合了动量方法,能够在训练过程中积累梯度的动量,从而加速参数更新的速度,尤其在具有平坦或稀疏梯度的情况下更加明显
  • 结合了Adagrad善于处理稀疏梯度和RMSprop善于处理非平稳目标的优点
  • 适用性广泛:也适用于大多非凸优化,适用于大数据集和高维空间

缺点

  • 内存消耗较大:Adam需要存储每个参数的动量和平方梯度估计,这会占用较大的内存空间,特别是在具有大量参数的深度神经网络中

1.4.5 AdaMax

AdaMax是一种自适应学习率优化算法,是Adam优化器的一种变体

AdaMax使用了梯度的无穷范数来估计梯度的大小,而Adam使用了梯度的二范数(核心区别),变化如下所示 论文中AdaMax的

为什么是选择了无穷范数

AdaMax选择了无穷范数(范数)是因为在大多数情况下,范数具有稳定的行为

对于一些问题,特别是在深度学习中,范数可以提供更好的数值稳定性和收敛性

相比于其他范数,范数能够更好地控制梯度的最大值,从而减少参数更新的不稳定性

因此,AdaMax选择了范数作为其更新规则的一部分,以提高优化算法的稳定性和效果

1.4.6 Nadam

深度学习优化策略Nadam

Nadam(Nesterov-accelerated Adaptive Moment Estimation)是将Adam与Nesterov加速梯度结合在一起,它对学习率的约束将更强,具备二者的优势,使得此算法在某些问题上的效果更好

Nadam的更新规则与Adam类似,但在计算梯度更新时引入了Nesterov动量项。具体而言,Nadam在计算梯度的移动平均和梯度更新时,使用了Nesterov动量的修正梯度来更新模型参数。这使得Nadam在处理凸优化问题时能够更好地逼近最优解,并且在处理非凸问题时能够更快地收敛

1.4.7 其他优化器

AdamW(Adam with Weight Decay): AdamW是对Adam优化器的改进,通过添加权重衰减(Weight Decay)的正则化项来解决权重衰减对Adam优化器的影响。传统的Adam优化器在计算梯度更新时,会将权重衰减项也纳入梯度计算中,导致权重衰减效果不准确。而AdamW在计算梯度更新时将权重衰减项单独处理,使得权重衰减的效果更加准确和稳定

ASGD(Average Stochastic Gradient Descent): ASGD是一种随机梯度下降法的变体,它通过计算一定数量的随机梯度的平均值来更新模型参数。ASGD使用一个平均模型参数的历史记录,以减小训练过程中参数更新的方差。这样可以使模型的收敛速度更稳定,并且能够在训练过程中逐渐减小学习率,使得模型在训练后期更加趋于收敛

LBFGS(Limited-memory Broyden-Fletcher-Goldfarb-Shanno): LBFGS是一种基于拟牛顿法的优化算法,用于解决无约束非线性优化问题。它利用函数的一阶导数和二阶导数信息来逼近目标函数的局部二次模型,并通过迭代更新参数来寻找最优解。LBFGS使用有限的内存来存储历史信息,以减少内存消耗。由于它不需要显式计算二阶导数矩阵,LBFGS适用于参数较多的问题,并且通常具有较好的收敛性能

总结:

  • AdamW是对Adam优化器的改进,解决了权重衰减对Adam优化器的影响
  • ASGD是一种随机梯度下降法的变体,通过平均随机梯度来减小参数更新的方差,提高收敛速度和稳定性
  • LBFGS是一种基于拟牛顿法的优化算法,通过逼近目标函数的局部二次模型来寻找最优解,具有较好的收敛性能和适用性

2 学习率衰减

在模型优化中,常用到的几种学习率衰减方法有:分段常数衰减、多项式衰减、指数衰减、自然指数衰减、余弦衰减、线性余弦衰减、噪声线性余弦衰减

深度学习优化器方法及学习率衰减方式综述

Copyright © narutohyc.com 2021 all right reserved,powered by Gitbook该文件修订时间: 2024-07-29 02:32:55

results matching ""

    No results matching ""