1. 程式人生 > >基於C#的機器學習--模糊邏輯-穿越障礙

基於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將這種邏輯新增到我們的應用程式中是多麼的容易。在我們的下一章中,我們將開始深入研究自組織地圖,將我們的機器學習技能提升到一個新的層次。如果你還記得小學時上的美術課,這一章一定會給你帶來回憶。