神经网络入门(连载之六)

   最近在学习有关遗传算法和神经网络方面的知识,网上查看了很多这方面的秘笈,只怪小生天生愚钝、才疏学浅,不能很好的领悟秘笈中的真谛,往往被弄得晕头转向、不知所措快哭了委屈。直到有一天无意中看到了博主zzwu写的有关这方面的文章,初读之,如温旧习;渐深入,觉甚好;遂一气呵成,犹如拨云见日、茅塞顿开。余甚怕在茫茫Internet中再无机会拜读之,遂收藏于此,以便众人观之,绝无其他不良用途。在此对博主再次深表感谢。

博文转自:http://blog.csdn.net/zzwu/article/details/576533




游戏编程中的人工智能技术



.(连载之六)



4.10 功能的两个改进 (A Couple of Improvements)
 仅管扫雷机学习寻找地雷的本领十分不错,这里我仍有两件事情要告诉你,它们能进一步改进扫雷机的性能。

4.10.1改进1(Improvement Number One)

    首先,单点crossover算子留下了许多可改进的余地。按照它的规定,算子是沿着基因组长度任意地方切开的,这样常有可能使个别神经细胞的基因组在权重的中间被一刀两段地分开。
  为清楚起见,我们来考察图16的权重。这是我们以前在说明基因组如何编码时看过的一个简单网络。 在这里,杂交算子可以沿向量长度的任意一处切开,这样,就会有极大几率在某个神经细胞(如第二个)的权重中间断开,也就是说,在权重0.6和-0.1之间某处切开。这可能不会是我们想要的,因为,如果我们把神经细胞作为一个完整的单元来看待,则它在此以前所获得的任何改良就要被骚扰了。事实上,这样的杂交操作有可能非常非常象断裂性突变(disruptive mutation)操作所起的作用。


 
图16 简单的网络

   与此针锋相对,我已创建了另一种类型的杂交运算,它只在神经细胞的边界上进行切开。在图16的例子中,就是在第3、4或第6、7的两个基因之间切开,如小箭头所示。 为了实现这一算法,我已在CNeuralNet类中补充了另一个切割方法: CalculateSplitPoints。这一方法创建了一个用于保存所有网络权重边界的矢量,它的代码如下:


vector<int> CNeuralNet::CalculateSplitPoints() const

{

   vector<int> SplitPoints;

  int WeightCounter = 0;

   // 对每一层

   for (int i=O; i<m_NumHiddenLayers + 1; ++i)

    {

     // 对每一个神经细胞

     for (int j=O; j<m_vecLayers[i].m_NumNeurons; ++j)

    {

         // 对每一个权重

         for (int k=O; k<m_vecLayers[i].m_vecNeurons[j].m_NumInputs; ++k)

          {

             ++WeightCounter;

          }
        SplitPoints.push_back(WeightCounter - 1);

      }

   }
   return SplitPoints;

}
  这一方法是CController类构造函数在创建扫雷机并把断裂点向量传递给遗传算法类时调用的。它们被存储在一个名叫m_vecSplitPoints的std::vector向量中。然后遗传算法就利用这些断裂点来实现两点杂交操作,其代码如下:
void CGenAlg::CrossoverAtSplits(const vector<double>  &mum,
                                const vector<double>  &dad,
                                vector<double>        &babyl,
                                vector<double>        &baby2)

   {

    // 如果超过了杂交率,就不再进行杂交,把2个上代作为2个子代输出

    // 如果2个上辈相同,也把它们作为2个下辈输出

    if ( (RandFloat() > m_dCrossoverRate) || (mum == dad))

      {

        baby1 = mum;      baby2 = dad;
        return;

      }
     // 确定杂交的2个断裂点

    int index1 = RandInt(0, m_vecSplitPoints.size()-2);
    int index2 = RandInt(Index1, m_vecSplitPoints.size()-1);
    int    cp1 = m_vecSplitPoints[Index1];
    int    cp2 = m_vecSplitPoints[Index2];
    // 创建子代
    for (int i=0; i<mum.size(); ++i)

     {

      if ( (i<cp1) || (i>=cp2) ) 

       {
           // 如果在杂交点外,保持原来的基因

           babyl.push_back(mum[i]);

           baby2.push_back(dad[i]);

       }
     else

       {
           // 把中间段进行交换

           baby1.push_back(dad[1]);

           baby2.push_back(mum[1]);

       }

     }
     return;

  }

 

  根据我的经验,我已发现,在进行杂交时,把神经细胞当作一个不可分割的单位,比在染色体长度上任意一点分裂基因组,能得到更好的结果。


4.10.2 改进2(Improvement Number Two)
     我想和你讨论的另一个性能改进,是用另一种方式来观察网络的那些输入。在你已看到的例中,我们为网络使用了4个输入参数: 2个用于表示扫雷机视线方向的向量,另外2个用来指示扫雷机与其最靠近的地雷的方向的向量。然而,有一种办法,可以把这些参数的个数减少到只剩下一个。
     其实你想一想就可知道,扫雷机为了确定地雷的位置,只要知道从它当前的位置和朝向出发,需要向左或向右转动多大的一个角度这一简单的信息就够了(如果你已经考虑到了这一点,那我在这里要顺便向您道贺了)。由于我们已经计算了扫雷机的视线向量和从它到最邻近地雷的向量,再来计算它们之间的角度(θ)应是一件极为简单的事情 – 这就是这两个向量的点积,这我们在第6章“使登陆月球容易一点”中已讨论过。见图17。


 
图17 计算到最邻近地雷的转动角度


   不幸的是,点积仅仅给出角度的大小; 它不能指示这一角度是在扫雷机的那一侧。因此,我已写了另一个向量函数返回一个向量相对于另一个向量的正负号。该函数的原型如下所示:
      inline int Vec2DSign(SVector2D &v1,SVector2D &v2);
   如果你对它的机理感兴趣,你可以在文件SVector2D.h中找到它的源码。但它的基本点就是: 如果v1至v2是按顺时针方向转的,则函数返回 +1;如果v1至v2是按逆时针方向转,则函数返回 -1。

  把点积和Vec2Dsign二者联合起来,就能把输入的精华提纯出来,使网络只需接受一个输入就行了。下面就是新的CMinesweeper::Update函数有关段落的代码形式:

    // 计算到最邻近地雷的向量
      SVector2D vClosestMine = GetClosestMine(mines);

    // 将它规范化
     Vec2DNormalize(vClosestMine);

     //计算扫雷机视线向量和它到最邻近地雷的向量的点积。它给出了我们要面对最邻近地雷所需转动的角度
      double dot = Vec2DDot(m_vLookAt, vClosestMine);

    // 计算正负号
      int sign = Vec2DSign(m_vLookAt, vClosestMine);
      Inputs.push_back(dot*sign);

  运行一下光盘Chapter7/Smart Sweepers v1.1目录下的可执行程序executable,你就知道经过以上2个改进,能为演化过程提速多少。

  需要注意的一桩重要事情是,带有4个输入的网络要花很长时间进行演化,因为它必须在各输入数据之间找出更多的关系才能确定它应如何行动。事实上,网络实际就是在学习怎么做点积并确定它的正负极性。因此,当你设计自己的网络时,你应仔细权衡一下,是由你自己预先来计算许多输入数据好呢(它将使CPU负担增加,但导致进化时间加快)还是让网络来找输入数据之间的复杂关系好(它将使演化时间变长,但能使CPU减少紧张)?


5 结束语(last words)

   我希望你已享受到了你第一次攻入神经网络这一奇妙世界的快乐。我打赌你一定在为如此简单就能使用它们而感到惊讶吧,对吗?我想我是猜对了。

   在下面几章里我将要向你介绍更多的知识,告诉你一些新的训练手段和演绎神经网络结构的更多的方法。但首先请你利用本章下面的提示去玩一下游戏是有意义的。

 

6 练习题 (Stuff to Try)

  1。 在v1.0中,不用look-at向量作为输入,而改用旋转角度θ作为输入,由此就可以使网络的输入个数减少成为1个。请问这对神经网络的演化有什么影响?你对此的看法怎样?

  2。 试以扫雷机的位置(x1,y1)、和扫雷机最接近的地雷的位置(x2,y2)、以及扫雷机前进方向的向量(x3,y3)等6个参数作为输入,来设计一个神经网络,使它仍然能够演化去寻找地雷。

  3。 改变激励函数的响应。试用O.1 - O.3 之间的低端值,它将产生和阶跃函数非常相像的一种激励函数。然后再试用高端值,它将给出较为平坦的响应曲线。考察这些改变对演化进程具有什么影响?

  4。 改变神经网络的适应性函数,使得扫雷机不是去扫除地雷,而是要演化它,使它能避开地雷。

  5。 理一理清楚有关遗传算法的各种不同设置和运算中使你感到模糊的东西!

  6。 加入其他的对象类型,比如人。给出一个新环境来演化扫雷机,使它能避开人,但照样能扫除地

雷。(这可能没有你想象那么容易!)

 


游戏编程中的人工智能技术



.(连载之六)



4.10 功能的两个改进 (A Couple of Improvements)
 仅管扫雷机学习寻找地雷的本领十分不错,这里我仍有两件事情要告诉你,它们能进一步改进扫雷机的性能。

4.10.1改进1(Improvement Number One)

    首先,单点crossover算子留下了许多可改进的余地。按照它的规定,算子是沿着基因组长度任意地方切开的,这样常有可能使个别神经细胞的基因组在权重的中间被一刀两段地分开。
  为清楚起见,我们来考察图16的权重。这是我们以前在说明基因组如何编码时看过的一个简单网络。 在这里,杂交算子可以沿向量长度的任意一处切开,这样,就会有极大几率在某个神经细胞(如第二个)的权重中间断开,也就是说,在权重0.6和-0.1之间某处切开。这可能不会是我们想要的,因为,如果我们把神经细胞作为一个完整的单元来看待,则它在此以前所获得的任何改良就要被骚扰了。事实上,这样的杂交操作有可能非常非常象断裂性突变(disruptive mutation)操作所起的作用。


 
图16 简单的网络

   与此针锋相对,我已创建了另一种类型的杂交运算,它只在神经细胞的边界上进行切开。在图16的例子中,就是在第3、4或第6、7的两个基因之间切开,如小箭头所示。 为了实现这一算法,我已在CNeuralNet类中补充了另一个切割方法: CalculateSplitPoints。这一方法创建了一个用于保存所有网络权重边界的矢量,它的代码如下:


vector<int> CNeuralNet::CalculateSplitPoints() const

{

   vector<int> SplitPoints;

  int WeightCounter = 0;

   // 对每一层

   for (int i=O; i<m_NumHiddenLayers + 1; ++i)

    {

     // 对每一个神经细胞

     for (int j=O; j<m_vecLayers[i].m_NumNeurons; ++j)

    {

         // 对每一个权重

         for (int k=O; k<m_vecLayers[i].m_vecNeurons[j].m_NumInputs; ++k)

          {

             ++WeightCounter;

          }
        SplitPoints.push_back(WeightCounter - 1);

      }

   }
   return SplitPoints;

}
  这一方法是CController类构造函数在创建扫雷机并把断裂点向量传递给遗传算法类时调用的。它们被存储在一个名叫m_vecSplitPoints的std::vector向量中。然后遗传算法就利用这些断裂点来实现两点杂交操作,其代码如下:
void CGenAlg::CrossoverAtSplits(const vector<double>  &mum,
                                const vector<double>  &dad,
                                vector<double>        &babyl,
                                vector<double>        &baby2)

   {

    // 如果超过了杂交率,就不再进行杂交,把2个上代作为2个子代输出

    // 如果2个上辈相同,也把它们作为2个下辈输出

    if ( (RandFloat() > m_dCrossoverRate) || (mum == dad))

      {

        baby1 = mum;      baby2 = dad;
        return;

      }
     // 确定杂交的2个断裂点

    int index1 = RandInt(0, m_vecSplitPoints.size()-2);
    int index2 = RandInt(Index1, m_vecSplitPoints.size()-1);
    int    cp1 = m_vecSplitPoints[Index1];
    int    cp2 = m_vecSplitPoints[Index2];
    // 创建子代
    for (int i=0; i<mum.size(); ++i)

     {

      if ( (i<cp1) || (i>=cp2) ) 

       {
           // 如果在杂交点外,保持原来的基因

           babyl.push_back(mum[i]);

           baby2.push_back(dad[i]);

       }
     else

       {
           // 把中间段进行交换

           baby1.push_back(dad[1]);

           baby2.push_back(mum[1]);

       }

     }
     return;

  }

 

  根据我的经验,我已发现,在进行杂交时,把神经细胞当作一个不可分割的单位,比在染色体长度上任意一点分裂基因组,能得到更好的结果。


4.10.2 改进2(Improvement Number Two)
     我想和你讨论的另一个性能改进,是用另一种方式来观察网络的那些输入。在你已看到的例中,我们为网络使用了4个输入参数: 2个用于表示扫雷机视线方向的向量,另外2个用来指示扫雷机与其最靠近的地雷的方向的向量。然而,有一种办法,可以把这些参数的个数减少到只剩下一个。
     其实你想一想就可知道,扫雷机为了确定地雷的位置,只要知道从它当前的位置和朝向出发,需要向左或向右转动多大的一个角度这一简单的信息就够了(如果你已经考虑到了这一点,那我在这里要顺便向您道贺了)。由于我们已经计算了扫雷机的视线向量和从它到最邻近地雷的向量,再来计算它们之间的角度(θ)应是一件极为简单的事情 – 这就是这两个向量的点积,这我们在第6章“使登陆月球容易一点”中已讨论过。见图17。


 
图17 计算到最邻近地雷的转动角度


   不幸的是,点积仅仅给出角度的大小; 它不能指示这一角度是在扫雷机的那一侧。因此,我已写了另一个向量函数返回一个向量相对于另一个向量的正负号。该函数的原型如下所示:
      inline int Vec2DSign(SVector2D &v1,SVector2D &v2);
   如果你对它的机理感兴趣,你可以在文件SVector2D.h中找到它的源码。但它的基本点就是: 如果v1至v2是按顺时针方向转的,则函数返回 +1;如果v1至v2是按逆时针方向转,则函数返回 -1。

  把点积和Vec2Dsign二者联合起来,就能把输入的精华提纯出来,使网络只需接受一个输入就行了。下面就是新的CMinesweeper::Update函数有关段落的代码形式:

    // 计算到最邻近地雷的向量
      SVector2D vClosestMine = GetClosestMine(mines);

    // 将它规范化
     Vec2DNormalize(vClosestMine);

     //计算扫雷机视线向量和它到最邻近地雷的向量的点积。它给出了我们要面对最邻近地雷所需转动的角度
      double dot = Vec2DDot(m_vLookAt, vClosestMine);

    // 计算正负号
      int sign = Vec2DSign(m_vLookAt, vClosestMine);
      Inputs.push_back(dot*sign);

  运行一下光盘Chapter7/Smart Sweepers v1.1目录下的可执行程序executable,你就知道经过以上2个改进,能为演化过程提速多少。

  需要注意的一桩重要事情是,带有4个输入的网络要花很长时间进行演化,因为它必须在各输入数据之间找出更多的关系才能确定它应如何行动。事实上,网络实际就是在学习怎么做点积并确定它的正负极性。因此,当你设计自己的网络时,你应仔细权衡一下,是由你自己预先来计算许多输入数据好呢(它将使CPU负担增加,但导致进化时间加快)还是让网络来找输入数据之间的复杂关系好(它将使演化时间变长,但能使CPU减少紧张)?


5 结束语(last words)

   我希望你已享受到了你第一次攻入神经网络这一奇妙世界的快乐。我打赌你一定在为如此简单就能使用它们而感到惊讶吧,对吗?我想我是猜对了。

   在下面几章里我将要向你介绍更多的知识,告诉你一些新的训练手段和演绎神经网络结构的更多的方法。但首先请你利用本章下面的提示去玩一下游戏是有意义的。

 

6 练习题 (Stuff to Try)

  1。 在v1.0中,不用look-at向量作为输入,而改用旋转角度θ作为输入,由此就可以使网络的输入个数减少成为1个。请问这对神经网络的演化有什么影响?你对此的看法怎样?

  2。 试以扫雷机的位置(x1,y1)、和扫雷机最接近的地雷的位置(x2,y2)、以及扫雷机前进方向的向量(x3,y3)等6个参数作为输入,来设计一个神经网络,使它仍然能够演化去寻找地雷。

  3。 改变激励函数的响应。试用O.1 - O.3 之间的低端值,它将产生和阶跃函数非常相像的一种激励函数。然后再试用高端值,它将给出较为平坦的响应曲线。考察这些改变对演化进程具有什么影响?

  4。 改变神经网络的适应性函数,使得扫雷机不是去扫除地雷,而是要演化它,使它能避开地雷。

  5。 理一理清楚有关遗传算法的各种不同设置和运算中使你感到模糊的东西!

  6。 加入其他的对象类型,比如人。给出一个新环境来演化扫雷机,使它能避开人,但照样能扫除地

雷。(这可能没有你想象那么容易!)

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值