基于C#的机器学习--模糊逻辑-穿越障碍

 

模糊逻辑-穿越障碍

模糊逻辑。另一个我们经常听到的术语。但它的真正含义是什么?它是否意味着不止一件事?我们马上就会知道答案。

我们将使用模糊逻辑来帮助引导一辆自动驾驶汽车绕过障碍,如果我们做得正确,我们将避开沿途的障碍。我们的自动导航车辆(AGV)将在障碍物周围导航,感知其路径上的障碍物。它将使用一个推理系统来帮助引导它前进。你或者用户将能够创造障碍或通过的方式,AGV必须避开或通过。你可以观察跟踪光束的工作,以及跟踪AGV的路径沿其路线。AGV所采取的每一步都将在用户界面上进行更新,这样您就可以看到发生了什么。

在布尔逻辑中,事物或真或假,或黑或白。许多人不知道的是,还有一种被称为多值逻辑的东西,它的真实值介于10之间。模糊逻辑是处理部分真理的多值逻辑的概念实现。例如很多人也不知道的著名的sigmoid函数,它是一种模糊化的方法。

维基百科(建议大家多使用这个百科,百度百科真的只能用呵呵来形容)对此有很好的描述,如下:

维基百科:

"In this image, the meanings of the expressions cold, warm, and hot are represented by functions mapping a temperature scale. A point on that scale has three "truth values"-one for each of the three functions. The vertical line in the image represents a particular temperature that the three arrows (truth values) gauge. Since the red arrow points to zero,this temperature may be interpreted as "not hot". The orange arrow (pointing at 0.2) may describe it as "slightly warm" and the blue arrow (pointing at 0.8) "fairly cold"."

       在这幅图中,cold(冷)、warm(暖)和hot(热)表达式的含义由映射一个温标的函数表示。这个尺度上的一个点有三个“真值”,三个函数各有一个。图像中的垂直线表示三个箭头(真值)测量的特定温度。由于红色箭头指向零,这个温度可以解释为不热 橙色的箭头(指向0.2)可以描述为轻微温暖,蓝色的箭头(指向0.8)可以描述为轻微温暖很冷

       我们为什么要展示这个?因为,这个图表和描述非常准确地描述了什么是模糊逻辑。我们将使用AForge.NET开源机器学习框架。这是一个很好的框架,它展示了使用推理引擎完成任务是多么容易。

       在这一章中,我们将讨论:

  1. 模糊逻辑

  2. 避障与识别

  3. AGV

模糊逻辑

我们的应用程序将有两个简单的按钮,一个用于运行模糊集测试,另一个用于运行语言变量测试。下面是示例应用程序的快速快照:

创建此示例的代码相对较小且简单。这是当我们点击Run Fuzzy Set Test按钮时的样子。我们将创建两个模糊集(一个用于凉爽,一个用于温暖),并为每个模糊集添加一些成员数据值,然后绘制它们: 

    #region 创建两个模糊集来表示凉和暖的温度
            #region
            TrapezoidalFunction function1 = new TrapezoidalFunction(13, 18, 23, 28);
            FuzzySet fsCool = new FuzzySet("", function1);
            double[,] coolValues = new double[20, 2];
            for (int i = 10; i < 30; i++)
            {
                coolValues[i - 10, 0] = i;
                coolValues[i - 10, 1] = fsCool.GetMembership(i);
            }
            chart?.UpdateDataSeries("", coolValues);
            #endregion#region
            TrapezoidalFunction function2 = new TrapezoidalFunction(23, 28, 33, 38);
            FuzzySet fsWarm = new FuzzySet("", function2);
            double[,] warmValues = new double[20, 2];
            for (int i = 20; i < 40; i++)
            {
                warmValues[i - 20, 0] = i;
                warmValues[i - 20, 1] = fsWarm.GetMembership(i);
            }
            chart?.UpdateDataSeries("", warmValues);
            #endregion 暖 
    #endregion 创建两个模糊集来表示凉和暖的温度

运行语言变量测试的代码如下。同样,我们创建模糊集,但这次我们创建4个而不是2个。与我们的第一个测试一样,我们首先添加成员数据,然后绘图: 

       LinguisticVariable lvTemperature = new LinguisticVariable("温度", 0, 80);
            TrapezoidalFunction function1 = new TrapezoidalFunction(10, 15, TrapezoidalFunction.EdgeType.Right);
            FuzzySet fsCold = new FuzzySet("", function1);
            lvTemperature.AddLabel(fsCold);
            TrapezoidalFunction function2 = new TrapezoidalFunction(10, 15, 20, 25);
            FuzzySet fsCool = new FuzzySet("", function2);
            lvTemperature.AddLabel(fsCool);
            TrapezoidalFunction function3 = new TrapezoidalFunction(20, 25, 30, 35);
            FuzzySet fsWarm = new FuzzySet("", function3);
            lvTemperature.AddLabel(fsWarm);
            TrapezoidalFunction function4 = new TrapezoidalFunction(30, 35, TrapezoidalFunction.EdgeType.Left);
            FuzzySet fsHot = new FuzzySet("", function4);
            lvTemperature.AddLabel(fsHot);
            double[][,] chartValues = new double[4][,];
            for (int i = 0; i < 4; i++)
                chartValues[i] = new double[160, 2]; 

最后我们画出这些值: 

        #region 语言变量的形状——它的标签从开始到结束的形状
            int j = 0;
            for (float x = 0; x < 80; x += 0.5f, j++)
            {
                double y1 = lvTemperature.GetLabelMembership("", x);
                double y2 = lvTemperature.GetLabelMembership("", x);
                double y3 = lvTemperature.GetLabelMembership("", x);
                double y4 = lvTemperature.GetLabelMembership("", x);

                chartValues[0][j, 0] = x;
                chartValues[0][j, 1] = y1;
                chartValues[1][j, 0] = x;
                chartValues[1][j, 1] = y2;
                chartValues[2][j, 0] = x;
                chartValues[2][j, 1] = y3;
                chartValues[3][j, 0] = x;
                chartValues[3][j, 1] = y4;
            }
            chart.UpdateDataSeries("", chartValues[0]);
            chart.UpdateDataSeries("", chartValues[1]);
            chart.UpdateDataSeries("", chartValues[2]);
            chart.UpdateDataSeries("", chartValues[3]);
         #endregion 语言变量的形状——它的标签从开始到结束的形状 

语言变量的形状:

正如所看到的,我们能够很容易地展示出维基百科定义所展示的视觉定义。

模糊的自主移动小车

在我们继续之前,先看一下我们的应用程序将是什么样子的,然后对推理引擎进行简要的解释:

尽管AForge.NET使得我们很容易和透明的创建一个InferenceSystem推理系统)对象。

       让我先来解释下什么是模糊推理系统。模糊推理系统是一种能够进行模糊计算的模型。这是使用数据库、语言变量和规则库完成的,所有这些都可以存储在内存中。

       模糊推理系统的典型操作如下:

  1. 获取数字输入
  2. 结合激活规则的结果得到一个模糊输出
  3. 验证输入激活了来自规则库的哪些规则
  4. 利用带有语言变量的数据库获取每个数字输入的语言意义

对于我们来讲,大部分工作将在初始化我们的模糊逻辑系统时进行。让我们将其分解为前面概述的各个步骤。

首先,我们需要准备语言标签(模糊集)组成的距离。他们分别是Near()Medium()Far()

            #region 构成这些距离的语言标签(模糊集)

            FuzzySet fsNear = new FuzzySet("Near", new TrapezoidalFunction(15, 50, TrapezoidalFunction.EdgeType.Right));

            FuzzySet fsMedium = new FuzzySet("Medium", new TrapezoidalFunction(15, 50, 60, 100));

            FuzzySet fsFar = new FuzzySet("Far", new TrapezoidalFunction(60, 100, TrapezoidalFunction.EdgeType.Left));

            #endregion 构成这些距离的语言标签(模糊集)        

接下来,我们初始化所需的语言变量。第一个是lvRight,它将是右侧测量距离的变量:

            #region 右侧测量距离(输入)

            LinguisticVariable lvRight = new LinguisticVariable("RightDistance", 0, 120);

            lvRight.AddLabel(fsNear);

            lvRight.AddLabel(fsMedium);

            lvRight.AddLabel(fsFar);

            #endregion 右侧测量距离(输入)     

现在,我们对左侧测量距离的变量做同样的操作:           

        #region 左侧测量距离(输入)

            LinguisticVariable lvLeft = new LinguisticVariable("LeftDistance", 0, 120);

            lvLeft.AddLabel(fsNear);

            lvLeft.AddLabel(fsMedium);

            lvLeft.AddLabel(fsFar);

        #endregion 左侧测量距离(输入)

最后一个语言变量是前方测量距离:

        #region 前方测量距离(输入)

            LinguisticVariable lvFront = new LinguisticVariable("FrontalDistance", 0, 120);

            lvFront.AddLabel(fsNear);

            lvFront.AddLabel(fsMedium);

            lvFront.AddLabel(fsFar);

        #endregion 前方测量距离(输入) 

现在我们关注组成这个角度的语言标签(模糊集)。我们需要完成这一步,这样才能创建最终的语言变量:

      #region 组成角度的语言标签(模糊集)

            FuzzySet fsVN = new FuzzySet("VeryNegative", new TrapezoidalFunction(-40, -35, TrapezoidalFunction.EdgeType.Right));

            FuzzySet fsN = new FuzzySet("Negative", new TrapezoidalFunction(-40, -35, -25, -20));

            FuzzySet fsLN = new FuzzySet("LittleNegative", new TrapezoidalFunction(-25, -20, -10, -5));

            FuzzySet fsZero = new FuzzySet("Zero", new TrapezoidalFunction(-10, 5, 5, 10));

            FuzzySet fsLP = new FuzzySet("LittlePositive", new TrapezoidalFunction(5, 10, 20, 25));

            FuzzySet fsP = new FuzzySet("Positive", new TrapezoidalFunction(20, 25, 35, 40));

            FuzzySet fsVP = new FuzzySet("VeryPositive", new TrapezoidalFunction(35, 40, TrapezoidalFunction.EdgeType.Left));

        #endregion 组成角度的语言标签(模糊集) 

现在我们可以创建角度的最终语言变量:

        #region

            LinguisticVariable lvAngle = new LinguisticVariable("Angle", -50, 50);

            lvAngle.AddLabel(fsVN);

            lvAngle.AddLabel(fsN);

            lvAngle.AddLabel(fsLN);

            lvAngle.AddLabel(fsZero);

            lvAngle.AddLabel(fsLP);

            lvAngle.AddLabel(fsP);

            lvAngle.AddLabel(fsVP);

        #endregion 角 

现在我们可以继续创建我们的模糊数据库。对于我们的应用程序,这是一个语言变量的内存字典,如果您愿意的话可以将其实现为SQLNoSQL或任何其他类型的具体数据库。

        #region 数据库

            Database fuzzyDB = new Database();

            fuzzyDB.AddVariable(lvFront);

            fuzzyDB.AddVariable(lvLeft);

            fuzzyDB.AddVariable(lvRight);

            fuzzyDB.AddVariable(lvAngle);

        #endregion 数据库 

接下来,我们将创建主推理引擎。下一行代码中最有趣的是CentroidDifuzzifier。在推理过程的最后,我们需要一个数值来控制过程的其他部分。为了得到这个数字,我们采用了一种去模糊化的方法。

我们的模糊推理系统的输出是一组点火强度大于零的规则。这种点火强度对随后的模糊规则集施加了约束。当我们把这些模糊集合放在一起时,它们会形成一个形状,这就是语言输出的意义。重心法将计算我们的形状面积的中心,以获得输出的数值表示。它使用近似数,所以会选择几个区间进行计算。随着时间间隔的增加,输出的精度也会增加:     

// 创建推理系统
IS = new InferenceSystem(fuzzyDB, new CentroidDefuzzifier(1000));

接下来,我们可以开始向我们的推理系统添加规则: 

            // 直走
            IS.NewRule("规则 1", "IF FrontalDistance IS Far THEN Angle IS Zero");

            // 直走(如果可以走到任何地方)
            IS.NewRule("规则 2", "IF FrontalDistance IS Far AND RightDistance IS Far AND LeftDistance IS Far THEN Angle IS Zero");

            // 右侧有墙
            IS.NewRule("规则 3", "IF RightDistance IS Near AND LeftDistance IS Not Near THEN Angle IS LittleNegative");

            // 左侧有墙
            IS.NewRule("规则 4", "IF RightDistance IS Not Near AND LeftDistance IS Near THEN Angle IS LittlePositive");

            // 前方有墙 - 房间在右侧
            IS.NewRule("规则 5", "IF RightDistance IS Far AND FrontalDistance IS Near THEN Angle IS Positive");

            // 前方有墙 - 房间在左侧
            IS.NewRule("规则 6", "IF LeftDistance IS Far AND FrontalDistance IS Near THEN Angle IS Negative");

            // 前方有墙 -两边都是房间 - 向右走
            IS.NewRule("规则 7", "IF RightDistance IS Far AND LeftDistance IS Far AND FrontalDistance IS Near THEN Angle IS Positive");

经过所有这些工作,我们的推理系统就已经准备好了!

            if (FirstInference)
                GetMeasures();

            try
            {
                DoInference();

                MoveAGV();

                GetMeasures();
            }

            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            } 

应用程序的主代码循环如下所示。我们将详细描述每个功能:

让我们快速看一下GetMeasures函数。获取当前地图以及AGV的位置后,调用HandleAGVOnWall函数,用于处理AGV碰到墙壁无法移动的情况。在这之后,DrawAGV在地图中绘制AGV。最后,RefreshTerrain 刷新地图:

        /// <summary>
        /// 得到传感器的测量结果
        /// </summary>
        private void GetMeasures()

        {
            #region 获得自主移动小车的位置

            pbTerrain.Image = CopyImage(OriginalMap);

            Bitmap b = (Bitmap)pbTerrain.Image;

            Point pPos = new Point(pbRobot.Left - pbTerrain.Left + 5, pbRobot.Top - pbTerrain.Top + 5);

            #endregion 获得自主移动小车的位置

            HandleAGVOnWall(b, pPos);

            DrawAGV(pPos, b);

            RefreshTerrain();
        } 

DrawAGV向左和向右遇到任何障碍时,如果选中Show beam复选框,就会看到前面、左边和右边的避束检测器显示:

        /// <summary>
        /// 绘制AGV
        /// </summary>
        /// <param name="pPos">坐标</param>
        /// <param name="b">位图</param>
        private void DrawAGV(Point pPos, Bitmap b)
        {
            Point pFrontObstacle = GetObstacle(pPos, b, -1, 0);

            Point pLeftObstacle = GetObstacle(pPos, b, 1, 90);

            Point pRightObstacle = GetObstacle(pPos, b, 1, -90);


            #region 显示线束

            Graphics g = Graphics.FromImage(b);

            if (cbLasers.Checked)
            {
                g.DrawLine(new Pen(Color.Red, 1), pFrontObstacle, pPos);

                g.DrawLine(new Pen(Color.Red, 1), pLeftObstacle, pPos);

                g.DrawLine(new Pen(Color.Red, 1), pRightObstacle, pPos);
            }

            #endregion 显示线束


            #region 绘制AGV

            if (btnRun.Text != RunLabel)
            {
                g.FillEllipse(new SolidBrush(Color.Blue), pPos.X - 5, pPos.Y - 5, 10, 10);
            }

            g.DrawImage(b, 0, 0);

            g.Dispose();

            #endregion 绘制AGV


            #region 更新显示的距离

            txtFront.Text = GetDistance(pPos, pFrontObstacle).ToString();

            txtLeft.Text = GetDistance(pPos, pLeftObstacle).ToString();

            txtRight.Text = GetDistance(pPos, pRightObstacle).ToString();

            #endregion 更新显示的距离
    } 

DoInference函数运行我们的模糊推理系统的一个历元(实例、生成等等)。最终,它负责确定AGV的下一个角度。

        /// <summary>
        /// 运行模糊推理系统的一个纪元
        /// </summary>
        private void DoInference()
        {
            // 输入设置
            IS?.SetInput("RightDistance", Convert.ToSingle(txtRight.Text));

            IS?.SetInput("LeftDistance", Convert.ToSingle(txtLeft.Text));

            IS?.SetInput("FrontalDistance", Convert.ToSingle(txtFront.Text));

            // 输出设置
            try
            {
                double NewAngle = IS.Evaluate("Angle");

                txtAngle.Text = NewAngle.ToString("##0.#0");

                Angle += NewAngle;
            }
            catch (Exception)
            {
            }
       } 

MoveAGV函数负责将AGV移动一步。如果您检查了跟踪路径的话,会发现这个函数中大约50%的代码时用于绘制AGV的历史轨迹的。

        /// <summary>
        /// 移动AGV
        /// </summary>
        private void MoveAGV()
        {
            double rad = ((Angle + 90) * Math.PI) / 180;

            int Offset = 0;

            int Inc = -4;


            Offset += Inc;

            int IncX = Convert.ToInt32(Offset * Math.Cos(rad));

            int IncY = Convert.ToInt32(Offset * Math.Sin(rad));

            if (cbTrajeto.Checked)
            {
                Graphics g = Graphics.FromImage(OriginalMap);

                Point p1 = new Point(pbRobot.Left - pbTerrain.Left + pbRobot.Width / 2, pbRobot.Top - pbTerrain.Top + pbRobot.Height / 2);

                Point p2 = new Point(p1.X + IncX, p1.Y + IncY);

                g.DrawLine(new Pen(new SolidBrush(Color.Green)), p1, p2);

                g.DrawImage(OriginalMap, 0, 0);

                g.Dispose();
            }

            pbRobot.Top = pbRobot.Top + IncY;

            pbRobot.Left = pbRobot.Left + IncX;
       } 

主要应用与显示光束的选择:

随着应用程序的运行,AGV成功导航障碍物,显示路径和光束。角度是AGV当前所面对的角度,传感器读数与前、左、右波束传感器有关:

我们的AGV成功地完成了穿越障碍,并继续运行:

轨迹路径和显示光束可单独选择:

总结

       在这一章中,我们学习了各种类型的模糊逻辑实现,并看到了使用AForge.NET将这种逻辑添加到我们的应用程序中是多么的容易。在我们的下一章中,我们将开始深入研究自组织地图,将我们的机器学习技能提升到一个新的层次。如果你还记得小学时上的美术课,这一章一定会给你带来回忆。

posted @ 2019-01-13 18:04 王振耀 阅读(...) 评论(...) 编辑 收藏