FuzzyAdvisor – простая экспертная система с нечеткой логикой на F#

ОГЛАВЛЕНИЕ

В данной статье представлена FuzzyAdvisor –  простая экспертная система на базе нечеткой логики, помогающая делать выбор исходя из простых оценок значений параметров.

•    Скачать исходники - 108 Кб

Введение

Более 15 лет назад разрабатывали проект (Brul? и др., 1995), требовавший экспертную систему, выбирающую подходящий вариант исходя из некоторых основных параметров. Были опробованы несколько подходов, в том числе использование исчисления предикатов (т.е. пролог). По сути, ни один  из подходов не работал хорошо. В итоге было опрошено несколько специалистов в области. У них спрашивали, какой выбор бы они сделали с учетом набора параметров, и они всегда отвечали нечто вроде "Если X равно Y, то я бы использовал A, но если X равен Z, то я бы использовал B" -- где X является параметром (т.е. глубина воды), Y и Z являются квалификаторами (глубоко), а A и B являются вариантами для выбора. По сути, они описывали нечеткую систему. В итоге была написана простая экспертная система на базе нечеткой логики, удовлетворительно решившая проблему.

F# служит для функционального программирования. Его возможности хорошо подходят для экспертной системы, подобной разработанной в прошлом веке. Вводные замечания по F# и разработка приложения форм Windows изложены в предыдущей статье, Знакомство с F# - приложение форм Windows.

В данной статье представлена FuzzyAdvisor –  простая экспертная система на базе нечеткой логики, помогающая делать выбор исходя из простых оценок значений параметров. Загружаемый архив содержит три проекта Visual Studio 2008:
•    FuzzyAdvisor – корневая библиотека (*.dll), написанная на F#, реализующая систему консультанта.
•    FuzzyWorkshop - приложение F#, разрешающее определяющие правила, отображение нечетких множеств и тестирование системы FuzzyAdvisor.
•    FuzzyTest - проект C#, использующий FuzzyAdvisor как пример объединения языков.

Код не совсем элегантный и эффектный, но он работает.

Описание системы FuzzyAdvisor

В нефтяном бизнесе и в других отраслях часто необходимо предварительно оценить затраты до поступления большого объема данных. При разработке морских нефтепромыслов стоимость морской платформы или другого типа конструкции аппаратуры очень сильно влияет на затраты, период подготовки перед установкой, ограничения на бурение, экологические риски и т. д. Из-за этих опасений очень важно правильно выбрать аппаратуру в начале проекта, задолго до появления любых других значащих данных. Рассмотренный здесь пример использует некоторые элементарные данные для обоснованного выбора лучшего типа или типов шельфовых нефтепромыслов. Следует заметить, что первая версия системы Fuzzy Advisor(нечеткий консультант) успешно использовалась во многих других аналогичных процессах принятия решений, в том числе в выборе газовых компрессоров, насосов и нефтеперерабатывающего оборудования.

Так как неизвестны точные применяемые правила, тут представлена искусственная, но разумная информация. Эти правила не годятся для принятия реальных решений! Придется выяснить собственные правила или нанять эксперта в помощь.

Общая конструкция FuzzyAdvisor состоит в том, чтобы обрабатывать нечеткие правила вида:

если <параметр> равен <квантификатор>, то <вариант> <вес>

Например:

если «глубина воды» равна «очень глубоко», то «подводные скважины» (0.9)

В примере глубина воды представляет собой параметр данных, VeryDeep описывает нечеткое множество, SubseaCompletions – вариант для выбора, и 0.9 является весовым коэффициентом, описывающим важность правила.

Утверждение легко читается на разговорном языке даже экспертами, ничего не знающими о нечеткой логике или программировании, но просто переводится на нечеткие логические операции. Эта форма грамматики утверждения крайне важна, поскольку эксперты в предметной области могут легко прочитать правила и решить, разумны они или нет. Такие соображения часто имеют огромное значение для получения признания системы и отсутствия отстраненности экспертов в предметной области от процесса принятия решения.

Вся система состоит из списка правил, списка значений параметров, списка нечетких множеств и списка вариантов. После задания параметров оценивается принадлежность к каждому нечеткому множеству, и уточненный вес добавляется к варианту, указанному в каждом правиле. После оценки всех правил варианты сортируются в порядке убывания их важности и предъявляются пользователю на рассмотрение.
Важно заметить, что в реальности часто нет правильного ответа, потому что все подразумевает компромиссы по времени, ресурсам, стоимости и другим соображениям. FuzzyAdvisor учитывает это, путем ранжирования вариантов и позволяя пользователю выбрать лучший, исходя из имеющейся у него любой другой субъективной информации. Зато FuzzyAdvisor с правильно определенным множеством вариантов и хорошо выверенными правилами покажет любые варианты, которые абсолютно неосуществимы или же на голову выше остальных вариантов.

При реализации системы FuzzyAdvisor необходимо определить правила, определить нечеткие множества и указать весовые коэффициенты для каждого правила. Хотя детали этого процесса не рассматриваются, предварительные определения обычно определяются путем опроса экспертов в предметной области. Как только предварительные правила, нечеткие множества и веса определены, система реализуется и снова проверяется экспертами. Если они заметят плохое решение, это значит, что требуется скорректировать нечеткое множество или вес, или же добавить другие правила. Осуществление этого выходит за рамки данной статьи.


Грамматика FuzzyAdvisor

Система FuzzyAdvisor считывает параметры, определения нечетких множеств и нечеткие правила в текстовом формате с использованием очень простой грамматики. В грамматике есть три типа утверждений:
•    Параметры: VAR <имя переменной> of <контекст> = <значение>
•    Нечеткие множества: FSET <имя нечеткого множества> of <контекст> <список членства>
•    Нечеткие правила: IF <имя переменной> of <контекст> IS <имя нечеткого множества> THEN <вариант> <вес>

Контекст параметра и нечеткое множество должны совпадать, чтобы различать параметры с похожими или идентичными именами, но с разными смыслами; например, глубина воды, в отличие от глубины скважины. Кроме того, для удобства любую из конструкций <имя> of <контекст> можно заменить на <контекст> <имя> для гибкости. То есть можно эквивалентно написать Depth of Water или Water Depth.
FuzzyAdvisor реализован на F# с использованием 4 типов. Полный код находится в файлах проекта, но будет упомянут ряд интересных моментов.

Во-первых, так как разные типы (классы в C#) ссылаются друг на друга, в F# они должны быть определены вместе. Перед первым типом идет ключевое слово type, но следующие типы вместо него используют ключевое слово and, как показано в следующем фрагменте кода:

type FuzzyVariable(VName : string, VContext : string, VValue : float) =
    let name = VName
    let context = VContext
    let mutable value = VValue
    new(vn, vc) = new FuzzyVariable(vn, vc, Double.NaN)
    override this.ToString() = sprintf "%s.%s" name context
    member this.Name = name
    member this.Context = context
    member this.Value
        with get() = value
        and  set v = value <- v

and FuzzySet(FName : string, FContext : string, FValues : (float * float) list) =
    let name = FName            // имя нечеткого множества       т.е. горячий
    let context = FContext      // контекст нечеткого множества    т.е. температура воды
    let values = FValues        // список кортежей (значение, принадлежность) в
                 // порядке возрастания значения
    new(fn, fc) = new FuzzySet(fn, fc, [])
    override this.ToString() = sprintf "%s.%s" name context
    member this.Name = name
    member this.Context = context
     ...
       
and FuzzyRule(AVar : FuzzyVariable, AFSet : FuzzySet,
            AChoice : string, AWeight : float) =
    let variable = AVar
    let fuzzySet = AFSet
    let choice = AChoice
    let weight = AWeight
    override this.ToString() = sprintf "%s (%A): %s is %s"
        choice weight (variable.ToString()) (fuzzySet.Name)
    member this.Var
        with get() = variable;
    member this.FSet
        with get() = fuzzySet;
    member this.Choice
        with get() = choice;
    member this.Weight
        with get() = weight;

and FuzzyAdvisorEngine() =
    let mutable fuzzySets:(FuzzySet list) = []
    let mutable fuzzyVars:(FuzzyVariable list) = []
    let mutable fuzzyChoices:((string * float) list) = []
    let mutable fuzzyRules:(FuzzyRule list) = []
     ...

Был переопределен нормальный метод ToString() для каждого объекта, что позволяет добавлять объекты в поле списка и отображать осмысленные имена.

Синтаксический анализ текста осуществляется методом перебора из-за простоты грамматики. В F# это легко достигается с помощью обработки списков и поиска по шаблону. Следующий фрагмент показывает часть синтаксического анализатора (парсера). Строка разбивается на список слов, разделенных пробелами, затем путем поиска по шаблону определяется, какой тип утверждения она представляет, исходя из первого слова. Наконец, путем поиска по шаблону извлекаются разные элементы <name><context> и любые связанные значения. FuzzyAdvisorEngine содержит функции для чтения и разбора текста из строк или текстового файла. В ходе разбора накапливаются списки переменных, нечеткие множества и нечеткие правила. Допустимые варианты также определяются из правил и сохраняются в списке вариантов.

    let Parse1Line(lineRead, iLine) = 
        let line = if lineRead <> null then String.split [' '] lineRead else []
        match line with
        // Игнорировать комментарии и пустые строки
        | "//"::_ -> null
        | [] -> null 
       
        // Разобрать переменные
        | x::words when x.ToUpper() = "VAR" ->     
            match words with
            | name::"of"::context::"="::value::_ when Double.TryParse(value) |> fst ->
                let var = new FuzzyVariable(name, context, Double.Parse value)
                fuzzyVars <- var :: fuzzyVars
            | context::name::"="::value::_ when Double.TryParse(value) |> fst ->
                let var = new FuzzyVariable(name, context, Double.Parse value)
                fuzzyVars <- var :: fuzzyVars
            | _ -> failwith ("Invalid VAR on line "^(iLine.ToString()))

        // Разобрать определения нечетких множеств
        | x::words when x.ToUpper() = "FSET" ->    
            match words with
            | name::context::(values:(string list)) ->
                let comparePoints (x1,_) (x2,_) = compare x1 x2
                let getPoint (s:(string list)) =
                    match s with
                    | x:string::y:string::[]
                        when (Double.TryParse(x) |> fst) &&
                    (Double.TryParse(y) |> fst) ->
                            (Double.Parse x, Double.Parse y)
                    | _ -> failwith ("Invalid FuzzySet Value on line "
                            ^(iLine.ToString()))
                let rec getValues (s:(string list)) =
                    match s with
                    | [] -> []
                    | x::y -> getPoint (String.split['(';',';')'] x) :: getValues y
                let fset = new FuzzySet(name, context, (getValues values) |>
                        List.sort comparePoints)
                fuzzySets <- fset::fuzzySets
            | _ -> failwith ("Invalid FSET on line "^(iLine.ToString()))
               
        // Разобрать нечеткие правила
        | x::words when x.ToUpper() = "IF" -> 
            let rule =  
                match words with
                | name::"of"::context::"is"::fsname::choice::value::_
                    when Double.TryParse(value) |> fst ->
                    let var = getVariable(fuzzyVars, name, context)
                    let fset = getFuzzySet(fuzzySets, fsname, context)
                    new FuzzyRule(var, fset, choice, Double.Parse value)
                | context::name::"is"::fsname::choice::value::_
                when Double.TryParse(value) |> fst ->
                    let var = getVariable(fuzzyVars, name, context)
                    let fset = getFuzzySet(fuzzySets, fsname, context)
                    new FuzzyRule(var, fset, choice, Double.Parse value)
                | _ -> failwith ("Invalid RULE on line "^(iLine.ToString()))
            fuzzyRules <- rule :: fuzzyRules
            if List.exists (fun (z,_) -> z = rule.Choice) fuzzyChoices then
                null
            else fuzzyChoices <- (rule.Choice, 0.0) :: fuzzyChoices
            |> ignore

        // Если ни один из них/(тех) не совпадает, то это ошибка
        | _ -> failwith ("Invalid line "^(iLine.ToString())^"= "^lineRead)
FuzzyWorkshop

Приложение FuzzyWorkshop является приложением форм Windows, написанным полностью на F#, и позволяющим тестировать систему FuzzyAdvisor. TabControl на главной форме содержит страницы для текста, элементов (нечеткие множества и параметры), правил и результатов. Текст можно сохранить  и прочитать из текстового файла с помощью пунктов меню. При нажатии кнопки Parse разбирается текст и определяются нечеткие множества, переменные, нечеткие правила и варианты. После разбора двойной щелчок по нечеткому множеству отобразит граф принадлежности множества, что поможет выявить ошибки. На вкладке Результаты нажатие кнопки Вычислить обработает правила и покажет взвешенные ранги всех возможных вариантов.

Большая часть кода FuzzyWorkshop понятна, но есть ряд интересных частей. Во-первых, TabControl добавляется в главную форму следующим образом, с требуемыми кнопками, полями списка и т. д., добавляемыми в качестве управляющих элементов на отдельные TabPage. Все это приходится делать вручную, так как пока нет конструкторов форм для F#.

    let tabControl = new TabControl()
    let tab1 = new TabPage()
    let tab2 = new TabPage()
    let tab3 = new TabPage()
    let tab4 = new TabPage()
    ...
        // элемент управления "набор вкладок"
        tabControl.Location <- new Point(5, 5)
        tabControl.Height <- 260
        tabControl.Width <- 280
        tabControl.Anchor <- AnchorStyles.Top |||
          AnchorStyles.Left ||| AnchorStyles.Right |||
          AnchorStyles.Bottom
        tabControl.TabPages.Add(tab1)
        tab1.Text <- "Text"
        tabControl.TabPages.Add(tab2)
        tab2.Text <- "Items"
        tabControl.TabPages.Add(tab3)
        tab3.Text <- "Rules"
        tabControl.TabPages.Add(tab4)
        tab4.Text <- "Results"
        tab1.Controls.AddRange([|
                                (btnParse:> Control);
                                (label1:> Control);
                                (txtInput:> Control);
                               |])
        tab2.Controls.AddRange([|
                                (label2:> Control);
                                (lstFuzzySets:> Control);
                                (label3:> Control);
                                (lstVariables:> Control);
                               |])
        tab3.Controls.AddRange([|
                                (label4:> Control);
                                (lstRules:> Control);
                                (btnCalculate:> Control)
                               |])
        tab4.Controls.AddRange([|
                                (btnCalculate:> Control);
                                (grid:> Control)
                               |])
    ...

Кроме того, FuzzyGraph реализован как пользовательский управляющий элемент. Подробности есть в исходных файлах, но, по сути, определяется тип FSharpGraph, наследуемый от .NET UserControl. Определяются члены, реагирующие на движения мыши, добавляющие данные в граф, и т. д. Вся графика программируется с помощью базовых методов GDI (интерфейс графического устройства). После определения компонент FSharpGraph добавляется на обычную форму, загружаемую при событии MouseDoubleClick для поля списка FuzzySet. Части соответствующего кода показаны ниже:

type FSharpGraph() as graph =
    inherit UserControl()
    let mutable components = new System.ComponentModel.Container()
    // управление мышью
    let mutable mouseSelecting = false
    let mutable mouseX1 = 0
    let mutable mouseY1 = 0
    let mutable mouseX2 = 0
    let mutable mouseY2 = 0
    let mutable graphMouseMove:(float -> float -> unit) = fun _ _ -> null
    ...

type FuzzySetViewerForm(fset : FuzzySet) as form =
    inherit Form()
    let label1 = new Label()
    let lblName = new Label()
    let label2 = new Label()
    let lblMousePosition = new Label()
    let graph = new FSharpGraph()
    let mutable FSet = fset
    do form.InitializeForm

    // определения члена
    member this.InitializeForm =
        // установить атрибуты формы
        this.FormBorderStyle <- FormBorderStyle.Sizable
        this.Text <- "Fuzzy Set Viewer"
        this.Width <- 300
        this.Height <- 300
        ...
        // граф
        graph.Location <- new Point(10,30)
        graph.Size <- new Size(270,220)
        graph.Anchor <-  AnchorStyles.Top |||
    AnchorStyles.Left ||| AnchorStyles.Right ||| AnchorStyles.Bottom
        graph.GraphMouseMove <- (fun x y -> this.GraphMouseMove(x, y))

Последняя строка кода выше присваивает функцию члену GraphMouseMove, чтобы перехватить параметры местоположения мыши и отобразить их на форме. Это похоже на использование делегата в C#.

Следующий код заполняет поля списка и другие отображаемые элементы после разбора текстового файла. Поразительно, что можно сделать с помощью одной строки кода и внутренних функций обработки списков F#, таких как List.iter и List.rev. Списки инвертируются с помощью List.rev, чтобы отображаемые элементы показывались в том же порядке, что и объявления текстового файла. Используя строку lstVariables в качестве примера, код F# приказывает пройти в цикле по списку переменных, добавляя каждую в поле списка и удаляя результат (целое число). Цикл проходит по списку fuzzyEngine.FVars после его инвертирования с помощью (List.rev fuzzyEngine.FVars).

let AddChoice (n,s) =
    let row = grid.Rows.Item(grid.Rows.Add())
    row.Cells.Item(0).Value <- n
    row.Cells.Item(1).Value <- s.ToString()
List.iter (fun x -> lstVariables.Items.Add(x) |> ignore)
        (List.rev fuzzyEngine.FVars)
List.iter (fun x -> lstFuzzySets.Items.Add(x) |> ignore)
        (List.rev fuzzyEngine.FSets)
List.iter (fun x -> lstRules.Items.Add(x) |> ignore)
        (List.rev fuzzyEngine.FRules)
List.iter (fun x -> AddChoice(x) |> ignore) (List.rev fuzzyEngine.FChoices)

FuzzyTest

Приложение FuzzyTest является простым приложением форм Windows, написанным на C#, и обращающимся к FuzzyAdvisor, написанному на F#. Исходный код C#, применяемый для обращения к системе FuzzyAdvisor, показан ниже. Создается FuzzyAdvisorEngine, выбирается текстовый файл с помощью стандартного диалогового окна .NET FileOpen, и механизм читает и разбирает файл и затем вычисляет варианты. В примере ранжированные варианты просто показываются с помощью MessageBox, но их можно представить и в альтернативной форме, или действия программы могут определяться исходя из рангов. К кортежам F#, определяющим варианты, обращаются из C# с помощью обобщенного класса Microsoft.FSharp.Core.Tuple<string,>.

FuzzyAdvisor.FuzzyAdvisorEngine Engine = null;

...

private void button1_Click(object sender, EventArgs e)
{
    if (dlgFileOpen.ShowDialog() == DialogResult.OK)
    {
        Engine = new FuzzyAdvisor.FuzzyAdvisorEngine();

        Engine.LoadFromFile(dlgFileOpen.FileName);
        Engine.get_CalculateChoices();
        foreach (Microsoft.FSharp.Core.Tuple<string, /> t in Engine.FChoices)
        {
            MessageBox.Show(t.Item1 + " = " + t.Item2.ToString());
        }
    }
}

Чтобы создать проект FuzzyTest, сначала был создан проект C#, затем существующий проект был добавлен к решению. Выбор проекта FuzzyAdvisor добавляет его к решению. Также необходимо добавить ссылки F# FSharp.Core и FuzzyAdvisor в проект C#, так как Visual Studio не узнает автоматически, что они нужны. После создания проектов их можно нормально компилировать и тестировать, и отладка может пошагово проходить код C# и F#.

Заключение

Хотя представленная здесь система FuzzyAdvisor довольно простая, она дает пример полноценной программы F# и показывает, как классы, написанные на F#, используются в C# или других языках .NET. Систему можно расширить, включив более сложную нечеткую логику, включив ограничения и разрешив диапазоны результатов, когда некоторые из переменных не известны точно. Кроме того, вместо предъявления вариантов можно было бы внедрить дополнительный шаг дефаззификации и затем использовать результаты для автоматического выполнения других действий. Такие приемы были испытаны и оказались пригодными для некоторых автоматизированных систем управления, но не были реализованы в данном примере.

После изучения использования F# и освоения принципов создания компонентов, форм, библиотек и использования кода F# вместе с C# можно применять F# для управления процессами, предусматривающими неграфический и непользовательский ввод. Частично из-за наличия конструкторов форм, другие языки вроде C# гораздо лучше подходят для пользовательских интерфейсов.

Зато простота рекурсивного программирования, определения обобщенных первоклассных функций и обработки списков делает F# идеальным для ряда задач. В частности, привыкнув к синтаксису F#, можно уместить в одну строку F# то, что в других языках требует сложных итераций.

Рекомендуется уделить некоторое время изучению применения F#. Приятно иметь возможность добавить лишний инструмент в арсенал программирования, и полезно уметь посмотреть на проблему с другой точки зрения. Для F# находится применение в сочетании с другими языками программирования.