最近重读《集体智慧编程》,这本当年出版的介绍推荐系统的书,在当时看来很引领潮流,放眼现在已经成了各互联网公司必备的技术。
这次边阅读边尝试将书中的一些Python语言例子用C#来实现,利于自己理解,代码贴在文中方便各位园友学习。
由于本文可能涉及到的与原书版权问题,请第三方不要以任何形式转载,谢谢合作。
第十部分 智能进化
这一部分介绍的内容与之前章节大有不同,我们尝试自动构造出解决某一问题的最佳程序。本质上来说就是实现一个可以构造算法的算法。
这种问题一般被称为遗传编程。本节中我们使用遗传编程来解决一个小问题,通过给定的数据集构造出一个数学函数。
当下计算能力是遗传编程的最大限制。
遗传编程
遗传编程的思想来自生物进化理论。一般来说其工作方式为,随机产生一大批程序,然后按照预定的标准选出其中一部分较优的,然后对这些程序采用几种方法来使其进化,如对程序的一小部分进行“变异”(对程序的某些部分稍作修改),或者将其中两个程序进行交叉(将一个程序的某些部分用另一个程序的某些部分进行替换)。经过这样的过程得出一批新程序,然后重复之前的过程,直到找到最优解或足够好的解(以成本函数评价)或达到迭代次数的上限,或者多次迭代后不再有改善。
遗传编程的原理类似本系列第4篇文章中所介绍的遗传算法。
如我们即将完成的例子,一般来说一定次数迭代后都能找到一个最优的函数。
寻找函数
这一部分要解决的问题如下。
现在有如下这样一个数据集,其包含了输入与对应的输出。
X | Y | 结果 |
---|---|---|
26 | 35 | 829 |
8 | 24 | 141 |
20 | 1 | 467 |
33 | 11 | 1215 |
37 | 16 | 1517 |
我们要做的就是寻找是这个数据集是由什么样的函数生成的。这里可以首先透露给读者这个数据集由x^2+2*y+3*x+5
这个函数生成。提前透露是为了我们可以实现一个方法去生成测试数据集。
我们新建一个名为Gp
的类,在其中添加如下方法:
public class Gp
{
private readonly Action<object> _outputWriter;
public Gp()
{
}
public Gp(Action<object> outputAction)
{
_outputWriter = outputAction;
}
private int HiddenFunction(int x, int y)
{
return x * x + 2 * y + 3 * x + 5;
}
public List<ValueTuple<int, int, int>> BuildHiddenSet()
{
var rows = new List<ValueTuple<int, int, int>>();
for (int i = 0; i < 200; i++)
{
var x = RndGenerator.RndInt32(0, 40);
var y = RndGenerator.RndInt32(0, 40);
rows.Add((x, y, HiddenFunction(x, y)));
}
return rows;
}
}
其中BuildHiddenSet
方法就可以生成上面我们看到的那样的数据集(不完全一样,只是按照同一个函数,传入随机值生成的)。
我们新建一个GpTest
类来放置下文出现的测试代码,这里我们先写了一个TestBuildHiddenset
来测试数据集的生成。
public class GpTest
{
private readonly ITestOutputHelper _output;
public GpTest(ITestOutputHelper output)
{
_output = output;
}
private void TestOutput(object obj)
{
_output.WriteLine(obj.ToString());
}
[Fact]
public void TestBuildHiddenset()
{
var gp = new Gp();
var hiddenset = gp.BuildHiddenSet();
TestOutput(JsonConvert.SerializeObject(hiddenset));
}
}
下文将一步步的去介绍用遗传编程的方法去找到数据集背后的函数。
编写可以生成函数的代码
首先需要找到一种使用代码来生成函数的方法。原书中构造了一些类来表示函数的组成部分,并使其可以被以程序树的形式随机生成,组合以及计算出结果。C#的表达式树,天生适合完成文中构造程序树的工作。
对于原书中用Python实现的几个类,可以用如下ExpressionTree
来完成同等的工作。
Action<T>/Func<T,...>(Expression.Compile())`` 大致相当于
fwrapper`Expression<T>(Expression.Lambda<>())`` 大致相当于
node`ParameterExpresion(Expression.Parameter(Type,string))
相当于paramnode
ConstantExpression(Expression.Constant(object))
相当于constnode
而几个计算方法的等价实现如下:
BinaryExpression(Expression.Add(Expression,Expression))
功能如addw
BinaryExpression(Expression.Subtract(Expression,Expression))
功能如subw
BinaryExpression(Expression.Multiply(Expression,Expression))
功能如mulw
Expression.IfThenElse()
用来实现iffunc
与isgreater
对于C#表达式树的一些使用知识本文不再介绍了,推荐园子里腾飞(Jesse)的几篇文章
博主的代码中实现了一个名为ExpFactory
的类用来完成随机选择一个计算方法。调用其中的Choice
方法就会返回一个表示计算的Expression
。ExpFactory
类实现如下:
public class ExpFactory
{
public static (Func<Expression[], Expression>, int) Choice()
{
return _supportFunc[RndGenerator.RndInt32(0, _supportFunc.Count)];
}
private static readonly List<ValueTuple<Func<Expression[], Expression>, int>> _supportFunc =
new List<ValueTuple<Func<Expression[], Expression>, int>>()
{
(inputExp=>Expression.Add(inputExp[0],inputExp[1]),2),
(inputExp=>Expression.Subtract(inputExp[0],inputExp[1]),2),
(inputExp=>Expression.Multiply(inputExp[0],inputExp[1]),2),
(inputExp=>ConstructGt0Expe(inputExp),3),
(inputExp=>ConstructGtExpe(inputExp),2),
};
private static Expression ConstructGt0Expe(Expression[] inputExp)
{
var returnLabel = Expression.Label(typeof(int));
var exp = Expression.Block(
Expression.IfThenElse(
Expression.GreaterThan(inputExp[0], Expression.Constant(0)),
Expression.Return(returnLabel, inputExp[1]),
Expression.Return(returnLabel, inputExp[2])),
Expression.Label(returnLabel, Expression.Constant(0))
);
return exp;
}
private static Expression ConstructGtExpe(Expression[] inputExp)
{
var returnLabel = Expression.Label(typeof(int));
var exp = Expression.Block(
Expression.IfThenElse(
Expression.GreaterThan(inputExp[0], inputExp[1]),
Expression.Return(returnLabel, Expression.Constant(0)),
Expression.Return(returnLabel, Expression.Constant(1))),
Expression.Label(returnLabel, Expression.Constant(0))
);
return exp;
}
}
代码中将IfThenElse
包于一个Block
表达式中是为了返回值的需要。默认IfThenElse
的ifTrue
和ifFalse
表达式是不作为返回值的。这样就会使IfThenElse
的返回值被作为Void
,而这样的表达式无法满足进一步构造表达式的需要。使用代码中的方法将IfThenElse
进行包装,得到的Block
表达式的返回值就是我们需要的特定的值类型。从而可以与其它表达式混合构造函数表达式。
为了验证下表达式可以完成我们需要的工作编写如下测试。测试方法TestExpressionTree
位于GpTest
类中:
[Fact]
public void TestExpressionTree()
{
var xParamExp = Expression.Parameter(typeof(int), "x");
var yParamExp = Expression.Parameter(typeof(int), "y");
var returnLabel = Expression.Label(typeof(int));
var blockExp = Expression.Block(
Expression.IfThenElse(
//if判断
Expression.GreaterThan(xParamExp, Expression.Constant(3)),
//if为true执行
Expression.Return(returnLabel, Expression.Add(yParamExp, Expression.Constant(5))),
//else执行
Expression.Return(returnLabel, Expression.Subtract(yParamExp, Expression.Constant(2)))
),
Expression.Label(returnLabel, Expression.Constant(0))
);
var lambdaExp = Expression.Lambda<Func<int, int, int>>(blockExp, xParamExp, yParamExp);
var lambda = lambdaExp.Compile();
TestOutput(lambda(5, 5));
TestOutput(lambda(0, 0));
}
这段代码测试了最基本的表达式树的使用 - 构造一个表达式树并使用其编译得到的Lambda来完成计算。其中的表达式与原书中的表达式完成了同样的运算。
以可视化的方式展示表达式
当我们构造一个表达式后(包括后文自动生成表达式),我们希望有一种直观的方式可以看到表达式的构造,所以编写一个打印表达式树结构的代码就很有必要。.NET Framework提供了ExpressionVisitor这个很好的基础来完成表达式树的遍历。
这里继承ExpressionVisitor
类实现了一个GpPrinter
类来打印表达式的结构:
public class GpPrinter : ExpressionVisitor
{
private readonly StringBuilder _stringBuilder;
private const int Tab = 2;
private int _depth;
public GpPrinter()
{
_stringBuilder = new StringBuilder();
}
private void Indent()
{
_depth += Tab;
}
private void Dedent()
{
_depth -= Tab;
}
private void Out(string output)
{
if (_depth > 0)
_stringBuilder.Append(new string(' ', _depth));
_stringBuilder.AppendLine(output);
}
public string Display(Expression expression)
{
_stringBuilder.Clear();
this.Visit(expression);
return _stringBuilder.ToString();
}
protected override Expression VisitConditional(ConditionalExpression node)
{
Out("if");
Indent();
Visit(node.Test);
Visit(node.IfTrue);
Visit(node.IfFalse);
Dedent();
return node;
}
protected override Expression VisitBinary(BinaryExpression node)
{
Out(node.NodeType.ToString());
Indent();
Visit(node.Left);
Visit(node.Right);
Dedent();
return node;
}
protected override Expression VisitParameter(ParameterExpression node)
{
Out($"p_{node.Name}");
return node;
}
protected override Expression VisitConstant(ConstantExpression node)
{
Out(node.Value.ToString());
return node;
}
}
下面的测试代码生成一个表达式,并打印其结构:
[Fact]
public void TestExpressionTreeDisplay()
{
var xParamExp = Expression.Parameter(typeof(int), "x");
var yParamExp = Expression.Parameter(typeof(int), "y");
var returnLabel = Expression.Label(typeof(int));
var blockExp = Expression.Block(
Expression.IfThenElse(
//if判断
Expression.GreaterThan(xParamExp, Expression.Constant(3)),
//if为true执行
Expression.Return(returnLabel, Expression.Add(yParamExp, Expression.Constant(5))),
//else执行
Expression.Return(returnLabel, Expression.Subtract(yParamExp, Expression.Constant(2)))
),
Expression.Label(returnLabel, Expression.Constant(0))
);
var printer = new GpPrinter();
TestOutput(printer.Display(blockExp));
}
构造初始“种群”
上面对表达式树完成这个工作的方法已有了足够的了解。接下来就利用上面的知识来构造初始的表达式“种群”。
初始的表达式是随机生成的表达式,我们在Gp
类中实现一个MakeRandomTree
方法来生成随机的表达式:
public static Expression MakeRandomTree(ParameterExpression[] paramExps, int maxTreeDepth = 4,
double fpr = 0.5, double ppr = 0.6)
{
if (RndGenerator.RndDouble() < fpr && maxTreeDepth > 0)
{
(var funcExp, var pc) = ExpFactory.Choice();
var children = new Expression[pc];
for (int i = 0; i < pc; i++)
{
children[i] = MakeRandomTree(paramExps, maxTreeDepth - 1, fpr, ppr);
}
return funcExp(children);
}
else if (RndGenerator.RndDouble() < ppr)
{
return paramExps[RndGenerator.RndInt32(0, paramExps.Length)];
}
else
{
return Expression.Constant(RndGenerator.RndInt32(0, 10));
}
}
代码中按照随机概率去选择一个表达式的组成模块并完成构造。为了让随机效果更好,将随机方法做了如下包装:
public static class RndGenerator
{
public static double RndDouble()
{
var rnd = new Random(Guid.NewGuid().GetHashCode());
return rnd.NextDouble();
}
public static int RndInt32(int min, int max)
{
var rnd = new Random(Guid.NewGuid().GetHashCode());
return rnd.Next(min, max);
}
}
接着来测试下随机生成程序(表达式)的效果:
[Fact]
public void TestMakeRandomTree()
{
var input1ParamExp = Expression.Parameter(typeof(int), "input1");
var input2ParamExp = Expression.Parameter(typeof(int), "input2");
var paramArr = new[] { input1ParamExp, input2ParamExp };
var random1 = Gp.MakeRandomTree(paramArr);
var printer = new GpPrinter();
TestOutput(printer.Display(random1));
var func = Expression.Lambda<Func<int, int, int>>(random1, paramArr).Compile();
TestOutput(func(7, 1));
TestOutput(func(2, 4));
var random2 = Gp.MakeRandomTree(paramArr);
TestOutput(printer.Display(random2));
var func2 = Expression.Lambda<Func<int, int, int>>(random2, paramArr).Compile();
TestOutput(func2(5, 3));
TestOutput(func2(5, 20));
}
测试代码中生成了两个随机的表达式,打印了其结构,并用其进行计算。
为了方便由Expression生成Lambda这里实现了一个扩展方法Compile
:
public static class ExpressionExtension
{
public static T Compile<T>(this Expression exp, ParameterExpression[] expParams)
{
return Expression.Lambda<T>(exp, expParams).Compile();
}
}
衡量程序的好坏
上面随机生成的函数肯定很难一次就满足我们程序集的生成。需要有一种方法来评价这个随机生成的程序的好坏。
这样的函数类似于本系列第4篇文章“优化”中所介绍的成本函数。在Gp
类中添加ScoreFunction
:
public long ScoreFunction(Func<int, int, int> func, List<ValueTuple<int, int, int>> s)
{
var dif = 0L;
foreach ((int x, int y, int r) data in s)
{
var v = func(data.x, data.y);
dif += Math.Abs(v - data.r);
}
return dif;
}
这个函数将程序集的输入传入随机生成的函数得到结果与数据集的结果差的绝对值的和作为评判生成函数好坏的标准。
下面的测试代码可以看到我们随机生成的函数得到的结果误差有多大:
[Fact]
public void TestScoreFunction()
{
var gp = new Gp();
var input1ParamExp = Expression.Parameter(typeof(int), "input1");
var input2ParamExp = Expression.Parameter(typeof(int), "input2");
var paramArr = new[] { input1ParamExp, input2ParamExp };
var random1 = Gp.MakeRandomTree(paramArr);
var func = Expression.Lambda<Func<int, int, int>>(random1, paramArr).Compile();
var hiddenset = gp.BuildHiddenSet();
var diff = gp.ScoreFunction(func, hiddenset);
TestOutput(diff);
var random2 = Gp.MakeRandomTree(paramArr);
var func2 = Expression.Lambda<Func<int, int, int>>(random2, paramArr).Compile();
diff = gp.ScoreFunction(func2, hiddenset);
TestOutput(diff);
}
程序进化
这一步是遗传编程最关键的所在。上面我们随机生成的程序不满足数据集的情况下怎么去做呢?一般方法都是在所有随机生成的程序中利用成本函数选出其中较好的一部分然后采用变异或交叉的手段来生成一的一组程序集,然后按照上面的过程继续直到找到最优的函数。
变异
第一种改变程序的方法就是变异。
对表达式进行变异有很多种方式,比如更改一个节点的运算,更改一个节点的子节点的数量等。这里采用一种最容易实现的方式 - 使用一个新的子树替换某个子节点。
当然对表达式树的一次变异不宜过大,所以我们在递归处理每个节点时,都随机生成一个数值,当其小于指定概率时才进行变异。
由于我们要递归处理表达式节点,ExpressionVisitor
依然是最佳选择。我们先实现一个测试的用于“变异”的实现来验证其可行性:
public class ExpTestMutate : ExpressionVisitor
{
public Expression Mutate(Expression expression)
{
return this.Visit(expression);
}
protected override Expression VisitConditional(ConditionalExpression node)
{
var newTrue = Expression.Constant(-100);
var newFalse = Expression.Constant(-200);
node = node.Update(node.Test, newTrue, newFalse);
return node;
}
}
由于C#的表达式树是不可变的,所以每次更新后都要返回新创建的表达式(每一个override
的方法只要是有更新表达式树的操作就要返回新的表达式对象)。
调用这个方法执行变异,并打印变异前后的表达式树来看看变异是否真正起效:
[Fact]
public void TestExpressionTreeMutate()
{
var xParamExp = Expression.Parameter(typeof(int), "x");
var yParamExp = Expression.Parameter(typeof(int), "y");
var returnLabel = Expression.Label(typeof(int));
var blockExp = Expression.Block(
Expression.IfThenElse(
//if判断
Expression.GreaterThan(xParamExp, Expression.Constant(3)),
//if为true执行
Expression.Return(returnLabel, Expression.Add(yParamExp, Expression.Constant(5))),
//else执行
Expression.Return(returnLabel, Expression.Subtract(yParamExp, Expression.Constant(2)))
),
Expression.Label(returnLabel, Expression.Constant(0))
);
var printer = new GpPrinter();
TestOutput(printer.Display(blockExp));
var mutater = new ExpTestMutate();
var newExp = mutater.Mutate(blockExp);
TestOutput(printer.Display(newExp));
}
验证过这种变异的方法可行后,我们来编写符合我们要求的变异代码:
public class ExpMutate : ExpressionVisitor
{
private readonly ParameterExpression[] _paramExps;
private readonly double _probchange;
public ExpMutate(ParameterExpression[] paramExps, double probchange = 0.1)
{
_paramExps = paramExps;
_probchange = probchange;
}
public Expression Mutate(Expression expression)
{
return this.Visit(expression);
}
protected override Expression VisitBinary(BinaryExpression node)
{
if (RndGenerator.RndDouble() < _probchange)
return Gp.MakeRandomTree(_paramExps);
var newLeft = Visit(node.Left);
var newRight = Visit(node.Right);
node = node.Update(newLeft, node.Conversion, newRight);
return node;
}
protected override Expression VisitBlock(BlockExpression node)
{
if (RndGenerator.RndDouble() < _probchange)
return Gp.MakeRandomTree(_paramExps);
// 针对我们ExpFactory构造的表达式,并非通用
node = node.Update(node.Variables, new[] { Visit(node.Expressions[0]), node.Expressions[1] });
return node;
}
protected override Expression VisitConditional(ConditionalExpression node)
{
var newTrue = Visit(node.IfTrue);
var newFalse = Visit(node.IfFalse);
node = node.Update(node.Test, newTrue, newFalse);
return node;
}
protected override Expression VisitGoto(GotoExpression node)
{
var newVal = Visit(node.Value);
return node.Update(node.Target, newVal);
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (RndGenerator.RndDouble() < _probchange)
return Gp.MakeRandomTree(_paramExps);
return base.VisitParameter(node);
}
protected override Expression VisitConstant(ConstantExpression node)
{
if (RndGenerator.RndDouble() < _probchange)
return Gp.MakeRandomTree(_paramExps);
return base.VisitConstant(node);
}
}
前文我们已经编写了许多有关表达式树的代码,如果那些可以看懂,理解这个也很容易。
比较值得注意的是,如果一个表达式树的子集被修改了,则其也要执行Update
来返回一个新的表达式。就如同代码中Block
和IfThenElse
表达式这样有嵌套关系的表达式。当IfThenElse
被修改后,即使Block
不需要修改,也要重新Update
来生成包含新的IfThenElse
的表达式。
接着试一下这个变异方法的效果:
[Fact]
public void TestMutate()
{
var gp = new Gp();
var input1ParamExp = Expression.Parameter(typeof(int), "input1");
var input2ParamExp = Expression.Parameter(typeof(int), "input2");
var paramArr = new[] { input1ParamExp, input2ParamExp };
var random1 = Gp.MakeRandomTree(paramArr);
var printer = new GpPrinter();
TestOutput(printer.Display(random1));
TestOutput("-----------我是分隔线-------------");
var newExp = gp.Mutate(random1, paramArr);
TestOutput(printer.Display(newExp));
}
还可以看一看变异后的表达式是否是更优的题解:
[Fact]
public void TestMutateResult()
{
var gp = new Gp();
var input1ParamExp = Expression.Parameter(typeof(int), "input1");
var input2ParamExp = Expression.Parameter(typeof(int), "input2");
var paramArr = new[] { input1ParamExp, input2ParamExp };
var random1 = Gp.MakeRandomTree(paramArr);
var hiddenset = gp.BuildHiddenSet();
var func1 = random1.Compile<Func<int, int, int>>(paramArr);
TestOutput(gp.ScoreFunction(func1,hiddenset));
TestOutput("-----------我是分隔线-------------");
var newExp = gp.Mutate(random1, paramArr);
var funcMutate = newExp.Compile<Func<int, int, int>>(paramArr);
TestOutput(gp.ScoreFunction(funcMutate, hiddenset));
}
在这一步,题解即使更坏也是正常,毕竟变异也是随机产生。后文会实现代码让这个变异过程进行迭代,使题解逐步变好。
最后简单的把这个ExpressionVisitor
的子类进行包装,方便进行“变异”的调用。
public Expression Mutate(Expression t, ParameterExpression[] paramExps, double probchange = 0.1)
{
var expMutate = new ExpMutate(paramExps, probchange);
return expMutate.Mutate(t);
}
交叉
除了变异外另一种修改程序的方法就是“交叉”,也可称作“配对”。其做法是将两个表现较好的表达式进行组合。通常的做法是,同时遍历两棵表达式树,并以随机概率选择第二棵树的节点来取代第一棵树相应位置的节点。
由于ExpressionVisitor
无法一次遍历两棵表达式树,这一部分,我们自己来实现遍历。代码实现的也是一个递归操作,我们通过GetChildren
方法找到一节点的子节点并在子节点上递归调用“交叉”方法。同时在特定概率下我们用第二棵树的节点来取代第一棵树相应位置的节点,这个操作由UpdateChildren
方法来实现。仍然要时时牢记表达式是不可变的,任何修改都是生成新的表达式。
这里的
GetChildren
和UpdateChildren
基本上只适用于前文指定的计算类型的表达式。如果增加了新的计算类型,也要扩展这两个方法的实现。
public Expression CrossOver(Expression t1, Expression t2, double probswap = 0.7, bool top = true)
{
if (RndGenerator.RndDouble() < probswap && !top)
return t2;
var result = t1;
var childrenExpsT1 = GetChildren(t1);
var childrenExpsT2 = GetChildren(t2);
if (childrenExpsT1 == null || childrenExpsT2 == null)
{
return result;
}
var newChildren = new List<Expression>();
foreach (var expression in childrenExpsT1)
{
newChildren.Add(CrossOver(expression, childrenExpsT2[RndGenerator.RndInt32(0, childrenExpsT2.Count)], probswap, false));
}
return UpdateChildren(result, newChildren);
}
private List<Expression> GetChildren(Expression exp)
{
if (exp is BinaryExpression)
{
var binExp = (BinaryExpression)exp;
return new List<Expression>()
{
binExp.Left,
binExp.Right
};
}
if (exp is BlockExpression)
{
var ifelseExp = ((BlockExpression)exp).Expressions[0] as ConditionalExpression;
if (ifelseExp != null)
{
return new List<Expression>()
{
((GotoExpression)ifelseExp.IfTrue).Value,
((GotoExpression)ifelseExp.IfFalse).Value
};
}
}
//如果是ConstantExpression或ParameterExpression则直接返回null
return null;
}
private Expression UpdateChildren(Expression origin, List<Expression> children)
{
if (origin is BinaryExpression)
{
var binExp = (BinaryExpression)origin;
return binExp.Update(children[0], binExp.Conversion, children[1]);
}
if (origin is BlockExpression)
{
var blockExp = (BlockExpression)origin;
var ifelseExp = blockExp.Expressions[0] as ConditionalExpression;
if (ifelseExp != null)
{
var trueExp = ifelseExp.IfTrue as GotoExpression;
var falseExp = ifelseExp.IfFalse as GotoExpression;
var newTrueExp = trueExp.Update(trueExp.Target, children[0]);
var newFalseExp = falseExp.Update(falseExp.Target, children[1]);
var newIfelseExp = ifelseExp.Update(ifelseExp.Test, newTrueExp, newFalseExp);
return blockExp.Update(blockExp.Variables, new[] { newIfelseExp, blockExp.Expressions[1] });
}
throw new Exception("无法解析的表达式");
}
throw new Exception("无法解析的表达式");
}
来试试“交叉”的效果:
[Fact]
public void TestCrossOver()
{
var gp = new Gp();
var printer = new GpPrinter();
var input1ParamExp = Expression.Parameter(typeof(int), "input1");
var input2ParamExp = Expression.Parameter(typeof(int), "input2");
var paramArr = new[] { input1ParamExp, input2ParamExp };
var random1 = Gp.MakeRandomTree(paramArr);
TestOutput(printer.Display(random1));
TestOutput("-----------我是分隔线-------------");
var random2 = Gp.MakeRandomTree(paramArr);
TestOutput(printer.Display(random2));
TestOutput("-----------我是分隔线-------------");
var crossed = gp.CrossOver(random1,random2);
TestOutput(printer.Display(crossed));
}
执行遗传编程计算
到这步,一切准备工作都完成妥当。我们把上文说的遗传编程的过程实现为代码:
public Func<int,int,int> Evolve(ParameterExpression[] pc, int popsize,
Func<List<ValueTuple<Func<int, int, int>,Expression>>, List<ValueTuple<long, Func<int, int, int>,Expression>>> rankfunction, int maxgen = 500, double mutationrate = 0.1,
double breedingreate = 0.4, double pexp = 0.7, double pnew = 0.05)
{
//返回一个随机数,通常是一个较小的数
//pexp的取值越小,我们得到的随机数就越小
Func<int> selectIndex =()=> (int) (Math.Log(RndGenerator.RndDouble()) / Math.Log(pexp));
// 创建一个随机的初始种群
var population = new List<ValueTuple<Func<int,int,int>,Expression>>(popsize);
for (int i = 0; i < popsize; i++)
{
var exp = MakeRandomTree(pc);
var func= exp.Compile<Func<int, int, int>>(pc);
population.Add((func,exp));
}
List<ValueTuple<long, Func<int, int, int>, Expression>> scores = null;
for (int i = 0; i < maxgen; i++)
{
scores = rankfunction(population);
_outputWriter?.Invoke(scores[0].Item1);
if (scores[0].Item1 == 0) break;
// 取两个最优的程序
var newpop = new List<ValueTuple<Func<int,int,int>,Expression>>()
{
(scores[0].Item2 ,scores[0].Item3),
(scores[1].Item2, scores[1].Item3)
};
//构造下一代
while (newpop.Count<popsize)
{
if(RndGenerator.RndDouble()>pnew)
{
var exp = Mutate(
CrossOver(scores[selectIndex()].Item3,
scores[selectIndex()].Item3, breedingreate), pc, mutationrate);
var func = exp.Compile<Func<int, int, int>>(pc);
newpop.Add((func,exp));
}
else
{
//加入一个随机节点,增加种群的多样性
var exp = MakeRandomTree(pc);
var func = exp.Compile<Func<int, int, int>>(pc);
newpop.Add((func,exp));
}
}
population = newpop;
}
var printer = new GpPrinter();
_outputWriter?.Invoke(printer.Display(scores[0].Item3));
return scores[0].Item2;
}
参数maxgen
表示程序最多循环的次数。另外,为了让这个进化方法更通用,其成本函数是通过参数rankfunction
来传入。
这里我们包装了之前实现的成本函数,这个函数会用之前成本函数对生成表达式进行评估并排序。
public Func<List<ValueTuple< Func<int, int, int>,Expression>>, List<ValueTuple<long, Func<int, int, int>,Expression>>>
GetRankFunction(List<ValueTuple<int, int, int>> dataset)
{
Func<List<ValueTuple<Func<int, int, int>, Expression>>,List<ValueTuple<long,Func<int,int,int>, Expression>>> rankfunction = poplation =>
{
var scores = poplation.Select(t => (ScoreFunction(t.Item1, dataset), t.Item1,t.Item2)).ToList();
scores.Sort((x,y)=>x.Item1.CompareTo(y.Item1));
return scores;
};
return rankfunction;
}
进化函数其他几个参数的含义如下:
mutationrate
发生变异的概率,用于Mutate
方法。breedingreate
发生交叉的概率,用于CrossOver
方法。pexp
在构造新种群时选择评价较低的表达式的递减比率。这个递减比率越高,则只选择评价最高者作为复制对象的概率就越大。pnew
在构造新的种群时,引入一个全新随机程序的概率。
最后我们就可以尝试这个进化程序,看看可以在多少步内找到一个能生成之前数据集的函数。
找到的函数可能不和我们之前给出的函数的一模一样,但经过函数化简后,可以看到两个函数在本质上是相同的。
[Fact]
public void TestEvolve()
{
var gp = new Gp(TestOutput);
var rf = gp.GetRankFunction(gp.BuildHiddenSet());
var input1ParamExp = Expression.Parameter(typeof(int), "input1");
var input2ParamExp = Expression.Parameter(typeof(int), "input2");
var paramArr = new[] { input1ParamExp, input2ParamExp };
gp.Evolve(paramArr,500,rf,mutationrate: 0.2,breedingreate: 0.1,pexp: 0.7,pnew: 0.1);
}
函数会在进化过程中输出成本函数的返回值,这样我们可以直观的看到生成的函数的确是一步步的在进步。直到成本函数返回值为0说明我们已经找到了那个函数。
最后说一下pexp
这个参数的作用,我们之前在优化问题遗传算法那部分也导论过陷入局部最优化的问题。pexp
可以调整构造新种群时选择之前种群中评价较低的表达式的数量,这样来做可以防止后代种群出现僵化、陷入局部最优而无法找到可行解。同样通过pnew
来控制以一定的概率在进化过程中加入新的随机子树也可以防止后代僵化。
这个示例到此也就结束了。这基本上只是遗传编程最简单的例子。要想扩展上面的方法可以加入更多的数据类型支持(这是一个比较麻烦的问题,怎样能让返回不同类型值的表达式共同工作是需要好好考虑的),加入更多种类的计算(如三角函数等)。
本系列到底也告一段落。《集体智慧编程》是非常好的一本书,其中的内容是现在流行的推荐系统、人工智能、机器学习等研究领域的基础。学完这本书,博主感觉收获很大,也深感这些领域的水很深。继续努力吧,大家共勉。
本系列示例代码下载。