测试运行

萤火虫算法优化

James McCaffrey

下载代码示例(VB)

James McCaffrey在机器学习中,数字优化算法经常用于查找相关变量(通常称为权重)的值集,其可以尽量减少一定程度的错误。例如,逻辑回归分类可以使用数学等式。在该等式中,如果有 n 个预测变量,则有 n+1 个必须确定的权重值。确定权重值的过程被称为定型模型。其原理是,使用含有已知正确输入值的定型数据的集合。优化算法可用于查找尽量减少错误的权重值,这些值与计算得出的输出值和正确的输出值不相同。

有许多不同的优化算法。本文介绍了一种相对新的技术,该技术称为萤火虫算法 (FA) 优化(2009 年首次发布)。FA 优化可轻松地模仿一大群萤火虫的行为。

要了解什么是 FA 优化以及了解本文要讨论的问题,较好的方法是查看图 1 中的演示程序。演示程序的目标在于,使用 FA 优化查找含有五个输入变量的 Michalewicz 函数的最小值。Michalewicz 函数是标准的基准测试函数,可用于评估数字优化算法的效果。通过五个输入值,该函数得出的最小值为 z = -4.6877,位于 x = (2.2029 1.5707, 1.2850, 1.9231, 1.7205) 处。

实际运用的萤火虫算法优化
图 1 实际运用的萤火虫算法优化

对于大多数优化算法而言,Michalewicz 函数很难,因为它含有若干个本地最小值和平面区域(其中所有的 z 值几乎相等)。虽然您无法轻松地看到含有五个输入值的 Michalewicz 函数,但是可以通过查看图 2 中所示的两个输入值的函数图形来了解该函数的特点。图形的底部显示了该函数在数学术语方面的定义。

两个变量的 Michalewicz 函数
图 2 两个变量的 Michalewicz 函数

该演示程序将萤火虫的数量设置为 40。每只萤火虫都有一个虚拟位置,该位置代表有关最小化问题的可能解决方案。在牺牲性能为代价的情况下,萤火虫越多,找到真正的最佳解决方案的可能性就越大。通常,FA 优化会使用 15 到 40 只萤火虫。

该演示将问题维度设置为 5,因为有五个输入值。FA 是一个迭代过程,并且需要一个最大循环计数器值。机器学习优化中的循环计数器变量通常称为“时期”,并且该演示将最大值设置为 1,000 个迭代。最大迭代数量会根据问题的不同而发生变化,但是 1,000 是合理的起始值。FA 含有一个随机元素,并且该演示将随机数生成器的种子值设置为任意值 0。

图 1 中运行的演示中,每 100 个时期显示一个到目前为止与找到的最佳位置相关联的最佳(最小)错误值。完成该算法之后,为任意萤火虫找到的最佳位置是 x = (2.2033, 1.5711, 1.2793, 1.1134, 2.2216)。虽然此解决方案非常接近最佳解决方案的 x = (2.2029 1.5707, 1.2850, 1.9231, 1.7205),但这并非完全相等。在 FA 找到的解决方案中,Michalewicz 函数的值是 -4.45,该值接近于实际最小值 -4.69。FA 解决方案的错误值为 0.0561。

本文假定您至少掌握了中级编程技能,但是不会假定您了解有关数字优化或萤火虫算法的所有知识。虽然使用了 C# 对该演示程序进行编码,但您应能够轻松使用其他语言(如 JavaScript 或 Python)重构代码。

本文显示了完整的演示代码以及少量小幅改动,以节省空间。还可以在本文随附的代码下载中获取该演示。该演示代码含有已删除的所有常规错误检查,以使主要概念清晰明了并减少代码的大小。

程序的整体结构

图 3 中显示了总体程序结构。为创建此演示,我启动了 Visual Studio 并创建了一个新的 C# 控制台应用程序,并命名为 FireflyAlgorithm。该演示对 Microsoft .NET Framework 的依赖程度并不明显,因此,任何最新的 Visual Studio 版本都可以正常运行。

图 3 萤火虫演示程序结构

using System;
namespace FireflyAlgorithm
{
  class FireflyProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin firefly demo");
      // Code here
      Console.WriteLine("End firefly demo");
      Console.ReadLine();
    }
    static void ShowVector(double[] v, int dec, bool nl)
    {
      for (int i = 0; i < v.Length; ++i)
        Console.Write(v[i].ToString("F" + dec) + " ");
      if (nl == true)
        Console.WriteLine("");
    }
    static double[] Solve(int numFireflies, int dim,
      int seed, int maxEpochs) { . . }
    static double Distance(double[] posA,
      double[] posB) { . . }
    static double Michalewicz(double[] xValues) { . . }
    static double Error(double[] xValues) { . . }
  } // Program
  public class Firefly : IComparable<Firefly>
  {
    // Defined here
  }
}

当该模板代码被加载到 Visual Studio 编辑器之后,我在解决方案资源管理器窗口中将文件 Program.cs 重命名为更具说明性质的 FireflyProgram.cs,并且 Visual Studio 会自动为我重命名 Program 类。在源代码的顶部,我会使用相关语句删除所有不必要的代码,同时保留对 System 的引用。

我使用主要静态方法技术(而非完整的面向对象的编程方法)对该演示进行编码。该演示具有 Main 中的所有控制逻辑。Main 方法首先显示该演示的目的:

Console.WriteLine("Goal is to solve the Michalewicz benchmark function");
Console.WriteLine("The function has a known minimum value of -4.687658");
Console.WriteLine("x = 2.2029 1.5707 1.2850 1.9231 1.7205");

接下来,设置 FA 所需的参数:

int numFireflies = 40;
int dim = 5;
int maxEpochs = 1000;
int seed = 0;

使用以下语句显示参数值:

Console.WriteLine("Setting numFireflies = " + numFireflies);
Console.WriteLine("Setting problem dim = " + dim);
Console.WriteLine("Setting maxEpochs = " + maxEpochs);
Console.WriteLine("Setting initialization seed = " + seed);

按照如下所示调用萤火虫算法:

Console.WriteLine("Starting firefly algorithm");
double[] bestPosition = Solve(numFireflies, dim, seed, maxEpochs);
Console.WriteLine("Finished");

Main 方法最后会显示 FA 结果:

Console.WriteLine("Best solution found: ");
Console.Write("x = ");
ShowVector(bestPosition, 4, true);
double z = Michalewicz(bestPosition);
Console.Write("Value of function at best position = ");
Console.WriteLine(z.ToString("F6"));
double error = Error(bestPosition);
Console.Write("Error at best position = ");
Console.WriteLine(error.ToString("F4"));

实际上,该萤火虫算法是元启发式算法,而非规定性算法。我的意思是,FA 是一组设计指南,可适应于很多不同类型的算法问题。

了解萤火虫算法

本文介绍的萤火虫算法基于 2009 年的研究论文,即 Xin-She Yang 著的“Firefly Algorithms for Multimodal Optimization”(萤火虫多模态优化算法)。图 4 中的图形阐释了萤火虫算法流程。该图形表示简化的虚拟最小值问题,其中只有两个输入值(X 和 Y),并且全局最小值位于 X = 0 和 Y = 0 处。有 3 只萤火虫。Firefly[0] 位于 (2, 1) 处,因此最接近正确的解决方案。Firefly[1] 位于 (-4, -4) 处。Firefly[2] 位于 (-8, 8) 处,并且最偏离解决方案。

萤火虫算法
图 4 萤火虫算法

真正的萤火虫是飞行昆虫,利用生物发光进行发光,大概是为了吸引异性。每只萤火虫可以发光的明暗度不同。在 FA 中,错误值越小的萤火虫越好,其发光的亮度更强。那么在图 4 中,firefly[0] 发光的亮度最强,firefly[1] 发光的亮度中等,而 firefly[2] 发光的亮度最弱。

FA 的基本概念是,某只萤火虫可以受到发光亮度更强的其他任何萤火虫的吸引,并且在两只萤火虫之间的距离更小的情况下,这种吸引力(向发光亮度更强的萤火虫移动的距离)强度更大。因此,在图 4 中,firefly[0] 的亮度最高,从而不会移动。Firefly[1] 和 firefly[2] 都会被吸引并且向 firefly[0] 移动。因为与 firefly[2] 相比,firefly[1] 更接近 firefly[0],所以 firefly[1] 移动的距离大于 firefly[2] 移动的距离。

如果在高级伪代码中表达的话,萤火虫算法如图 5 所示。初看之下,该算法看上去非常简单;但是,该算法非常精细,正如您将看到显示代码的实施时间一样。

第一个主要问题是定义萤火虫的发光强度。由于 FA 是元启发式的方法,因此您可以随意定义发光强度,只要较强的发光强度与更好的解决方案/位置关联即可。下一个主要问题是定义吸引力,因此距离越近的萤火虫向发光强度更大的萤火虫移动的距离大于距离越远的萤火虫。

Figure 5 Firefly Algorithm

initialize n fireflies to random positions
loop maxEpochs times
  for i := 0 to n-1
    for j := 0 to n-1
      if intensity(i) < intensity(j)
        compute attractiveness
        move firefly(i) toward firefly(j)
        update firefly(i) intensity
      end for
    end for
  sort fireflies
end loop
return best position found

实施萤火虫算法

Solve 方法的定义开头如下:

static double[] Solve(int numFireflies, int dim, int seed, int maxEpochs)
{
  Random rnd = new Random(seed);
  double minX = 0.0;
  double maxX = 3.2;
  double B0 = 1.0;
  double g = 1.0;
  double a = 0.20;
  int displayInterval = maxEpochs / 10;
...

本地变量 minX 和 maxX 为每只萤火虫的位置创建边界。此处使用的值 0.0 和 3.2(约等于 Pi)是特定于 Michalewicz 函数的。对于含有规范化数据的机器学习优化,-10.0 和 +10.0 值是常见值。

本地变量 B0(基本贝塔)、g(伽马)和 a(阿尔法)控制一只萤火虫对另一只萤火虫的吸引力。源研究论文推荐使用的值(1.0、1.0 和 0.20)。本地变量 displayInterval 可控制显示进度消息的频次。

接下来,空的萤火虫群创建完成:

double bestError = double.MaxValue;
double[] bestPosition = new double[dim]; // Best ever
Firefly[] swarm = new Firefly[numFireflies]; // All null

Firefly 对象是编程定义的,并且可封装位置、相关联的错误值和相应的发光强度。最初,所有萤火虫都是空的对象。Firefly 类的定义将在本文的下一节中说明。接下来,对萤火虫群进行实例化,并将它们放到任意位置。对于每只萤火虫,系统会调用 Firefly 构造函数:

for (int i = 0; i < numFireflies; ++i)
{
  swarm[i] = new Firefly(dim);
  for (int k = 0; k < dim; ++k) // Random position
    swarm[i].position[k] = (maxX - minX) * rnd.NextDouble() + minX;
...

该构造函数以隐式的方式将位置设置为 (0.0, 0.0, 0.0, 0.0, 0.0) 以及将相关联的错误值和发光强度设置为虚拟值 0.0。然后,该位置数组的每个组件被设置为 minX 和 maxX(0.0 和 3.2)之间的任意值。接下来,计算当前萤火虫的错误值和发光强度:

swarm[i].error = Error(swarm[i].position);
  swarm[i].intensity = 1 / (swarm[i].error + 1);
...

很快,系统会显示 Error 函数。此处,萤火虫的发光强度被定义为与该错误值相反,以便于用较小的错误值表示较高的强度,较大的错误值表示较低的强度。当错误值为 0 时,分母中为 1 可以防止零除情况的出现。初始化最终会检查新建的萤火虫,以看看它是否找到了最佳位置:

...
  if (swarm[i].error < bestError)
  {
    bestError = swarm[i].error;
    for (int k = 0; k < dim; ++k)
      bestPosition[k] = swarm[i].position[k];
  }
} // For each firefly

主处理循环开头为以下语句:

int epoch = 0;
while (epoch < maxEpochs)
{
  if (epoch % displayInterval == 0 && epoch < maxEpochs)
  {
    string sEpoch = epoch.ToString().PadLeft(6);
    Console.Write("epoch = " + sEpoch);
    Console.WriteLine(" error = " + bestError.ToString("F14"));
  }
...

固定数量迭代的备用方法是,在 bestError 的值下降到低于一些较小的阈值(0.00001 很常见)时中断。使用内嵌的 for 循环将每只萤火虫与其他所有萤火虫进行比较:

for (int i = 0; i < numFireflies; ++i) // Each firefly
{
  for (int j = 0; j < numFireflies; ++j) // Others
  {
    if (swarm[i].intensity < swarm[j].intensity)
    {
      // Move firefly(i) toward firefly(j)
...

请注意,因为每个循环指数都从 0 开始,所以在 while 循环过程中的每个迭代中,每对萤火虫将被比较两次。若要将某只萤火虫向发光强度更大的另一只萤火虫移动,请务必先计算吸引程度:

double r = Distance(swarm[i].position, swarm[j].position);
double beta = B0 * Math.Exp(-g * r * r);
...

变量 beta 可定义吸引程度并且会在移动 firefly[i] 时使用。它的值取决于 fireflies [i] 和 [j] 之间距离的平方,可通过帮助程序方法 Distance 计算得出。Distance 方法会返回两个位置之间的 Euclidean 距离。例如,如果两个维度中的 firefly[i] 位于 (3.0, 4.0) 处,而 firefly[j] 位于 (5.0, 9.0) 处,则它们之间的距离是 sqrt((5 - 3)^2 + (9 - 4)^2) = sqrt(4 + 25) = sqrt(29) = 5.4。如果您决定使用不同的测距方法,请注意 beta 使用平方后的距离,这与平方根操作相反,因此计算 beta 比较简单,但这是以牺牲灵活性为代价的。

实际移动通过以下语句来完成:

for (int k = 0; k < dim; ++k)
{
  swarm[i].position[k] += beta *
    (swarm[j].position[k] - swarm[i].position[k]);
  swarm[i].position[k] += a * (rnd.NextDouble() - 0.5);
  if (swarm[i].position[k] < minX)
    swarm[i].position[k] = (maxX - minX) * rnd.NextDouble() + minX;
  if (swarm[i].position[k] > maxX)
    swarm[i].position[k] = (maxX - minX) * rnd.NextDouble() + minX;
}
...

firefly[i] 位置的第 k 个组件是 firefly[i] 和 firefly[j] 向 firefly[j] 移动之间的距离的 beta 部分。然后,一个小的随机字词被添加到每个第 k 个位置组件。这有助于防止该算法陷入非最佳解决方案中。检查每个位置组件以看看它是否超出范围。如果超出范围,系统会分配一个随机的范围内的值。

通过更新刚刚移动萤火虫的错误值和发光强度来完成内嵌的循环移动代码:

swarm[i].error = Error(swarm[i].position);
      swarm[i].intensity = 1 / (swarm[i].error + 1);
    } // If firefly(i) < firefly(j)
  } // j
} // i each firefly
...

Solve 方法最后使用以下语句:

...
   Array.Sort(swarm); // low error to high
    if (swarm[0].error < bestError)
    {
      bestError = swarm[0].error;
      for (int k = 0; k < dim; ++k)
        bestPosition[k] = swarm[0].position[k];
    }
    ++epoch;
  } // While
  return bestPosition;
} // Solve

每对萤火虫进行比较并且发光强度较弱的萤火虫已向发光强度更强的萤火虫移动之后,Firefly 对象的数组会按照从较低错误值到较高错误值进行排序,以便于最佳值位于 swarm[0] 处。检查此对象,以看看是否找到新的最佳解决方案。此外,对 Firefly 对象的数组进行排序对更改该数组中的位置具有重大影响,因此每次都会通过 while 循环按照不同的顺序对这些对象进行处理。

帮助程序方法

Method Solve 可调用帮助程序方法 Distance 和 Error,其会反过来调用帮助程序方法 Michalewicz。帮助程序方法 Distance 的定义如下:

static double Distance(double[] posA, double[] posB)
{
  double ssd = 0.0; // sum squared diffrences
  for (int i = 0; i < posA.Length; ++i)
    ssd += (posA[i] - posB[i]) * (posA[i] - posB[i]);
  return Math.Sqrt(ssd);
}

帮助程序方法 Michalewicz 的定义如下:

static double Michalewicz(double[] xValues)
{
  double result = 0.0;
  for (int i = 0; i < xValues.Length; ++i) {
    double a = Math.Sin(xValues[i]);
    double b = Math.Sin(((i+1) * xValues[i] * xValues[i]) / Math.PI);
    double c = Math.Pow(b, 20);
    result += a * c;
  }
  return -1.0 * result;
}

如果您更喜欢图 2 底部中的 Michalewicz 函数的数学定义,您将看到该函数含有指数 2m。但是,通常 m 的值会被设置为 10,因此在该代码中,使用常量值 20。帮助程序方法 Error 的定义如下:

static double Error(double[] xValues)
{
  int dim = xValues.Length;
  double trueMin = 0.0;
  if (dim == 2)
    trueMin = -1.8013; // Approx.
  else if (dim == 5)
    trueMin = -4.687658; // Approx.
  double calculated = Michalewicz(xValues);
  return (trueMin - calculated) * (trueMin - calculated);
}

错误值方法只返回 Michalewicz 函数的已知最小值和计算得出值之间的平方差的结果。可以很快地对该虚拟 error 函数进行计算,但是在大多数机器学习方案中,error 函数非常耗时。

Firefly 类

Firefly 类的定义开头为:

public class Firefly : IComparable<Firefly>
{
  public double[] position;
  public double error;
  public double intensity;
...

该类继承 IComparable 接口,以便于可以自动对包含该对象的数组和列表进行排序。为了简洁起见,使用公开域定义数据字段。由于错误值和强度之间有着一对一的映射,因此两个字段中的一个可能被放弃。类构造函数是:

public Firefly(int dim)
{
  this.position = new double[dim];
  this.error = 0.0;
  this.intensity = 0.0;
}

有许多设计替代方法可供您考虑使用。此处,该构造函数会直接为位置数组分配空间。其他公共方法只有 CompareTo:

public int CompareTo(Firefly other)
  {
    if (this.error < other.error) return -1;
    else if (this.error > other.error) return +1;
    else return 0;
  }
} // Class Firefly

CompareTo 方法将 Firefly 对象按照从最低错误值到最高错误值进行排序。对等的替代方法是从最高强度到最低强度进行排序。

几点注释

本文中演示的萤火虫算法的实施建立在 2009 年论文的基础上。原始算法已衍生出几种变体。该研究论文显示,某些数据指示 FA 优于粒子群优化,至少在某些虚拟的基准测试优化问题方面具有优势。我有点怀疑。不过,我认为,当要最小化的目标函数含有多种解决方案时,FA 在这种情况下非常有用。尽管不完全明显,但事实证明,FA 可自动自组织多个子群以同时查找多个解决方案。


James McCaffrey 博士 任职于华盛顿州雷德蒙德市的 Microsoft 研究中心。他长期从事多个 Microsoft 产品(包括 Internet Explorer 和 Bing)的研发工作。可以在 jammc@microsoft.com 上联系 McCaffrey 博士。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Todd Bello、Marciano Moreno Diaz Covarrubias 和 Alisson Sol