FuzzyAdvisor - F# 中的简单模糊逻辑专家系统






4.84/5 (20投票s)
使用 F# 实现一个可从 C# 调用的简单专家系统。
引言
大约 15 年前,我参与了一个项目(Brulé 等人,1995),该项目需要一个专家系统,根据一些基本参数选择合适的选项。尝试了几种方法,包括谓词演算(即 Prolog)。基本上,没有一种方法效果很好。最后,我采访了几位该主题的人类专家。我问他们给定一组参数会做出什么选择,他们总是会回答类似“如果 X 是 Y,那么我会选择 A,但如果 X 是 Z,那么我会选择 B”之类的话——其中 X 是一个参数(即水深),Y 和 Z 是限定词(深),A 和 B 是可选项。经过一番思考,我突然意识到他们描述的是一个模糊系统。因此,我最终编写了一个简单的基于模糊逻辑的专家系统,并令人满意地解决了这个问题。
最近,我决定深入研究 F#。作为函数式编程的新手,我认为 F# 的一些特性可能非常适合一个简单的专家系统,类似于我在上个世纪开发的那种。我对 F# 和编写 Windows Forms 应用程序的初步观察在我之前的文章 F# 入门 - Windows Forms 应用程序[^] 中。
在本文中,我将介绍 FuzzyAdvisor,一个简单的基于模糊逻辑的专家系统,可用于根据简单的参数估计做出选择。下载包含三个 Visual Studio 2008 项目:
- FuzzyAdvisor - 用 F# 编写的核心库(*.dll),实现顾问系统。
- FuzzyWorkshop - 一个 F# 应用程序,允许指定规则、可视化模糊集和测试
FuzzyAdvisor
系统。 - FuzzyTest - 一个 C# 项目,使用 FuzzyAdvisor 作为结合语言的示例。
当然,我会加上我惯常的免责声明:我仍然是 F# 的初学者,所以我不声称代码是优雅、高效等的。它确实有效,而且一如既往,任何关于编码风格或替代 F# 技术的评论都将受到赞赏。
FuzzyAdvisor 系统描述
在石油行业以及其他行业中,在数据量还不多的时候,经常需要对成本进行初步估算。在海上油田开发中,海上平台或其他类型设施设计的成本对成本、安装前的时间、钻井限制、环境风险等方面都有极其重要的影响。由于这些担忧,在项目开始时,远在任何其他有意义的数据可用之前,选择一个合理的设施类型就变得非常重要。这里讨论的例子将使用一些初步数据来估算最佳的海上石油设施类型。应指出的是,该 Fuzzy Advisor 系统的早期版本已成功用于许多其他类似的决策过程,包括天然气压缩机、泵和炼油设备的选择。
您也可以放心,我不知道应该使用确切的规则,因此这里提供的信息是虚构的,但合理的。换句话说,不要认为您可以将这些规则应用于现实世界的决策!您需要自己弄清楚规则,或聘请专家来帮助您。
FuzzyAdvisor 的整体设计是处理形式为
if <parameter> is <quantifier> then <option> <weight>
例如
if Water Depth is VeryDeep then SubseaCompletions (0.9)
在示例中,请注意 水深 代表一个数据参数,非常深
描述了一个模糊集,海底完井
是一个可能被选择的选项,0.9 是一个描述规则重要性的权重因子。
还要注意,该语句即使对于一无所知的专家来说,也可以用清晰的英语轻松阅读,并且可以非常直接地翻译成模糊逻辑运算。这种语句语法形式极其重要,因为主题专家可以轻松阅读规则并判断它们是否有意义。这些考虑因素在获得系统认可并确保主题专家不会在决策过程中被疏远方面通常至关重要。
整个系统则由规则列表、参数值列表、模糊集列表和选项列表组成。一旦指定了参数,就评估每个模糊集的隶属度,并将调整后的权重添加到每条规则中指定的选项中。当所有规则都评估完毕后,选项将按值降序排序并呈现给用户考虑。
同样重要的是要注意,在“现实世界”中,通常没有“正确答案”,因为一切都涉及时间、资源、成本和其他因素的权衡。FuzzyAdvisor 通过对选项进行排名来认识到这一点,并让用户根据他们可能拥有的任何其他主观信息来选择最佳选项。另一方面,FuzzyAdvisor,如果具有定义的选项集和足够校准的规则,将指示任何完全不切实际或明显优于其他选择的选项。
在实现 FuzzyAdvisor 系统时,需要定义规则、确定模糊集并为每条规则指定权重因子。虽然我不会详细介绍该过程,但初步定义通常是通过访谈主题专家来确定的。一旦确定了初步的规则、模糊集和权重,系统将被实现并再次由专家审查。当他们注意到一个糟糕的决定时,就需要弄清楚是调整模糊集或权重,或者添加不同的规则。实际上,这超出了本文的范围。
FuzzyAdvisor 语法
FuzzyAdvisor 系统使用一种非常简单的语法以纯文本格式读取参数、模糊集定义和模糊规则。语法中有三种类型的语句:
- 参数:
VAR <varname> of <context> = <value>
- 模糊集:
FSET <fsetname> of <context> <membership list>
- 模糊规则:
IF <varname> of <context> IS <fsetname> THEN <option> <weight>
请注意,参数和模糊集的上下文必须匹配,以区分具有相似或相同名称但含义不同的参数;例如,“水深”与“井深”。此外,为了方便起见,任何 <name> of <context>
结构都可以替换为 <context> <name>
以增加灵活性。换句话说,我们可以等价地写成 水深
或 深度 水
。
FuzzyAdvisor 是用 F# 实现的,使用了四种类型。完整代码在项目文件中,但我将提及一些有趣的要点。
首先,由于各种类型(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 // Fuzzy set name i.e. hot
let context = FContext // Fuzzy set context i.e. water temperature
let values = FValues // list of (value, membership) tuples in
// increasing order of value
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
// Ignore comment and blank lines
| "//"::_ -> null
| [] -> null
// Parse Variables
| 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()))
// Parse FuzzySet definitions
| 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()))
// Parse FuzzyRules
| 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
// If none of those match, it must be an error
| _ -> failwith ("Invalid line "^(iLine.ToString())^"= "^lineRead)
FuzzyWorkshop
FuzzyWorkshop 应用程序是一个完全用 F# 编写的 Windows Forms 应用程序,它允许测试 FuzzyAdvisor 系统。主窗体上的 TabControl
包含文本、项目(模糊集和参数)、规则和结果的页面。可以使用菜单选项将文本保存到文本文件或从文本文件中读取。按 解析 按钮将尝试解析文本并确定模糊集、变量、模糊规则和选项。解析后,双击模糊集将显示该集合的隶属度图,以帮助进行故障排除。在 结果 选项卡上,按 计算 按钮将处理规则并显示所有可能选项的加权排名。
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
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
组件添加到常规窗体中,该窗体在 FuzzySet
列表框的 MouseDoubleClick
事件上加载。下面显示了部分适用代码。
type FSharpGraph() as graph =
inherit UserControl()
let mutable components = new System.ComponentModel.Container()
// Mouse control
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 definitions
member this.InitializeForm =
// Set Form attributes
this.FormBorderStyle <- FormBorderStyle.Sizable
this.Text <- "Fuzzy Set Viewer"
this.Width <- 300
this.Height <- 300
...
// graph
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 应用程序是一个用 C# 编写的简单 Windows Forms 应用程序,它访问用 F# 编写的 FuzzyAdvisor。下面显示了用于访问 FuzzyAdvisor
系统的 C# 源代码。请注意,创建了 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 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# 并弄清楚如何创建组件、窗体、库以及如何与 C# 一起使用 F# 代码后,我认为 F# 在短期内的最佳用途是处理主要涉及非图形和非用户输入的过程。部分原因是窗体设计器的可用性,C# 等其他语言在用户界面方面似乎更好。
另一方面,能够简单地进行递归编程、定义通用一等函数以及处理列表使得 F# 非常适合某些任务。特别是,一旦我们习惯了语法,在其他语言中比较复杂的迭代在 F# 中就变成了优雅的一行代码。
最后,我仍然建议任何有一点好奇心的人花点时间学习如何使用 F#。能够为自己的编程工具箱添加另一个工具总是有益的,能够从不同的角度看待问题也没有坏处。就个人而言,我确信将来我会发现 F# 的用途,并将其与其他编程语言结合使用。
参考文献
- Brulé, Mike, Walt Fair, Jr., Jun Jiang, Ron Sanvido, A RAD Approach to Client/Server System Development, SPE Computer Applications, Society of Petroleum Engineers, October 1995, p122ff.
历史
- 2008 年 11 月 8 日 - 初次提交。