C++ 模糊逻辑 API + 简单 DSL






4.59/5 (11投票s)
使用 C++ 实现的模糊逻辑 API 和模糊逻辑 DSL。
什么是模糊逻辑?
在本文中,我将向您介绍我的模糊逻辑实现,称为 FuzzyLogic API。我相信这个实现是简单、有效且非常快速的。您知道什么是模糊逻辑吗?在这里我不会过多地谈论模糊逻辑理论,因为有很多很棒的关于模糊逻辑的文章(见下方的链接)。
模糊逻辑实现
在本节中,我将描述我的 FuzzyLogic API 实现。我试图使其清晰简单,以便每个人都能轻松理解。首先,我将尝试描述一些简单的类,这些类将在 FuzzyLogic API 中更复杂和更重要的类中使用。
CLinguisticVariable
CLinguisticVariable
是用于表示语言变量的类。它有三个类变量。variableName
用于定义语言变量的名称,并且应该是唯一的,因为我们使用哈希映射来存储 CLinguisticVariable
,因此我们可以在 O(1) 时间内找到特定的语言变量。下一个变量 b_output
声明该语言变量是否是输出变量。请注意,FuzzyLogic API 中只能有一个输出变量。最后一个变量 h_linguisticValuesHashMap
用于存储语言值。一个语言变量可以有一个或多个语言值。有几个类方法。AddLinguisticValue
向选定的 CLinguisticVariable
添加 LinguisticValue
。在下一个图像中,您可以看到四个语言变量:Distance
、Approaching
、Speed
和 Signal
。
下一个方法将语言值添加到 hash_map
。
//Method add Linguistic value to hash map
void AddLinguisticValue(LinguisticValue* p_linguisticValue)
{
h_linguisticValuesHashMap.insert(LinguisticValue_Pairs(
p_linguisticValue->ToString(), p_linguisticValue));
}
其中 LinguisticValue_Pairs
是在以下位置声明的 typedef
//Typedef for linguistic values hash map.
typedef pair <string, LinguisticValue*> LinguisticValue_Pairs;
ResetLinguisticValues
方法只是将所有语言值重置为 -1。一个更有趣的方法是 FindLinguisticValueByName
。该方法的输入参数是语言值的名称,返回值是 LinguisticValue
对象的指针。因为我们使用哈希映射,所以时间复杂度是 O(1)。
LinguisticValue* FindLinguisticValueByName(string name)
{
//Define iterator
hash_map <string, LinguisticValue*> :: const_iterator iterator;
//Fint linguistic value by name
iterator = h_linguisticValuesHashMap.find(name);
if(iterator == h_linguisticValuesHashMap.end())
{
//SASO, this should never happends!!!!!
assert(NULL);
return NULL;
}
//Return LinguisticValue
return (LinguisticValue*)iterator->second;
}
CalculateLinguisticValueByName
只是 CFuzzyReasoner
和 LinguisticValue
之间用于计算语言值的桥梁。UpdateLinguisticValueByName
稍微复杂一些。首先,我们必须按名称查找 LinguisticValue
。如果已经存储了一个值,我们将执行模糊 OR 操作,否则我们将只存储 newVal
。让我们看看代码
//Update Linguistic value by name
void UpdateLinguisticValueByName(string name, double newVal)
{
//Find LinguisticValue
LinguisticValue* value = FindLinguisticValueByName(name);
//If there is value, we should perform operator OR
if(value->GetLinguisticValue() != -1)
value->SetLinguisticValue(CFuzzyCalculator::OR(value->GetLinguisticValue(), newVal));
else
value->SetLinguisticValue(newVal); //There is no value, just set newVal
}
LinguisticValue
LinguisticValue
类用于存储语言值。变量 A
、B
、C
和 D
是梯形(梯形)的“边界”。例如,语言变量 Distance
有三个语言值(Low
、Average
和 Height
),对于语言值“Low
”,变量 A
、B
、C
和 D
将是 A = 0、B = 0、C = 1、D = 2。每个 LinguisticValue
都有一个唯一的名称(就像在 CLinguisticVariable
中一样)。变量 linguisticValue
用于存储数值。默认值为 -1。此值表示输入值在多大程度上属于此 LinguisticValue
梯形。对于语言值 Average
(语言变量 Speed
,输入:0(km/h)),此值将等于 0.5(参见上图)。只有一个方法(除了构造函数、getter 和 setter)称为 CalculateLinguisticValue
。我们必须注意输入的正确性,因此如果输入超出范围,我们应该对其进行归一化。然后,我们需要计算梯形函数。梯形函数的结果是特定变量的语言值。
double CalculateLinguisticValue(double input)
{
//Normalize variables
if(input < A)
input = A;
else if(input > D)
input = D;
//Calculating Trapezoid function
if ((input <= A)
|| (input > D ))
return 0;
else if ((A < input) && (input < B))
return (input - A) / (B - A);
else if ((B <= input) && (input <= C))
return 1;
else
return (D - input) / (D - C);
}
CFuzzyCalculator
可能是项目中类最简单的一个。只有两个静态方法。AND
方法实现了 AND 运算符,该运算符等于返回最小值。
//Operator AND
static double AND(double a, double b)
{
if(a < b)
return a;
return b;
}
OR
方法实现了 OR 运算符,返回最大值。
//Operator OR
static double OR(double a, double b)
{
if(a > b)
return a;
return b;
}
CFuzzyRule
模糊逻辑由语言变量和模糊规则组成。此类用于存储模糊规则。模糊规则由 CFuzzyToken
类对象组成。此类唯一的函数是向规则添加 CFuzzyRuleToken
。例如,CFuzzyRule
可以是
"IF NOT SPEED = SLOW AND DISTANCE = CLOSE, SIGNAL = BRAKE"
每条模糊规则都应该有一个输出变量!
CFuzzyRuleToken
正如我之前所说,CFuzzyRuleToken
只是 CFuzzyRule
的一部分。让我们描述所有变量。operation
描述了前一个和当前的 CFuzzyRuleToken
之间的操作类型。支持的操作符是 OR、AND 和 EMPTY。EMPTY 表示没有操作符(“VOID”)。EMPTY 令牌仅用于规则中的第一个和最后一个令牌。令牌也可以被否定(变量 b_negation
)。每个 CFuzzyToken
都有一个指向 CLinguisticVariable
的指针以及语言值的名称。让我们更仔细地看看 CFuzzyRule
示例。
"IF NOT SPEED = SLOW AND DISTANCE = CLOSE, SIGNAL = BRAKE"
什么是令牌?有三个令牌示例
Token_1
operation = EMPTY
b_Negate = true
p_linguisticVariable= SPEED
linguisticValueName = "SLOW"
Token_2 operation = AND
b_Negate = false
p_linguisticVariable= DISTANCE
linguisticValueName = "CLOSE"
Token_3
operation =EMPTY
b_Negate = false
p_linguisticVariable= SIGNAL
linguisticValueName = BRAKE"
请注意,TOKEN_3
是输出令牌!每条 CFuzzyRule
都必须有一个带有输出变量的令牌!!!
CalculateTokenOutputValue
方法用于从哈希映射 h_fuzzyInputHashMap
获取输入值并调用方法来计算语言值。输入变量 h_fuzzyInputHashMap
是一个填充了对象(CFuzzyInput
类)的 hash_map
。在 CFuzzyInput
中存储了特定语言变量的输入值。稍后将解释 CFuzzyInput
。
//Calculate token value
double CalculateTokenOutputValue(hash_map <string, CFuzzyInput*> h_fuzzyInputHashMap)
{
hash_map <string,> :: const_iterator iterator;
//Find linguistic variable
iterator = h_fuzzyInputHashMap.find(p_linguisticVariable->ToString());
CFuzzyInput* temp = (CFuzzyInput*)iterator->second;
double input = temp->GetInputValue();
//Calculate linguistic value
return p_linguisticVariable->CalculateLinguisticValueByName(linguisticValueName, input);
}
CFuzzyInput
CFuzzyInput
只有两个变量,variableName
用于语言变量名称,inputValue
(类型 double
)用于值。例如,CFuzzyInput
可以是 variableName = "SPEED"
和 inputValue = "43"
(km/h)。
CFuzzyReasoner
CFuzzyReasoner
是 FuzzyLogic 系统的核心。它用于计算模糊规则的输出值和最终的去模糊化。CFuzzyReasoner
变量 v_fuzzyRulesVector
用于存储模糊规则(CFuzzyRule
)。让我们看看 FuzzyLogic 中最重要的函数 FuzzyRuleReasoner
。该函数以模糊规则和填充了模糊输入的哈希映射作为输入。检查每个模糊规则令牌是否为输出变量。如果不是,则输出变量是当前令牌变量的计算语言值。然后我们检查令牌是否被否定。如果语言变量不是模糊规则中的第一个变量,并且不是输出变量,那么我们应该有一个不同于 EMPTY 的操作符。只有模糊规则中的第一个变量必须具有 EMPTY 操作符(对于输出值,操作符不重要)。对于输出值,我们只设置一个新值(resetVal
)并返回指向输出令牌的指针。
//Start fuzzy rule reasoner
CFuzzyRuleToken* FuzzyRuleReasoner(CFuzzyRule* fuzzyRule,
hash_map <string, CFuzzyInput*> h_fuzzyInputs)
{
double resultVal = 0;
for(unsigned int i = 0; i < fuzzyRule->GetFuzzyRuleTokens().size(); i++)
{
//Get fuzzy rule token
CFuzzyRuleToken* token = fuzzyRule->GetFuzzyRuleTokens()[i];
if(token->IsOutput())
{
//Update output object
token->UpdateTokenValue(resultVal);
//Return result token
return token;
}
else
{
double tokenVal = token->CalculateTokenOutputValue(h_fuzzyInputs);
token->UpdateTokenValue(tokenVal);
if(token->IsNegated())
tokenVal = 1 - tokenVal; //Negate value
if(i == 0)
resultVal = tokenVal; //Set value
else if(token->IsOrOperator())
resultVal = CFuzzyCalculator::OR(resultVal, tokenVal); //OR operator
else if(token->IsAndOperator())
resultVal = CFuzzyCalculator::AND(resultVal, tokenVal); //AND operator
}
}
//This won't happends saso
assert(NULL);
return NULL;
}
以下方法仅为 FuzzyLogic API 中的每个模糊规则调用推理。
//Start reasoner for fuzzy rulles
CFuzzyRuleToken* CalculateFuzzyRules(hash_map <string, CFuzzyInput*> h_fuzzyInputs)
{
//Reset all values
CFuzzyRuleToken* outputObject;
//Calculate all fuzzy rules
for(unsigned int i = 0; i < v_fuzzyRulesVector.size(); i++)
{
//Glede na izhodno vrednost izhodne spremeljivke naredi update izhodnega objekta
outputObject = FuzzyRuleReasoner((CFuzzyRule*)v_fuzzyRulesVector[i], h_fuzzyInputs);
}
//Return output object
return outputObject;
}
模糊逻辑推理的最后一步是去模糊化。它在接下来的两个方法中实现。
//Defuzzyfication
double Defuzzy(CFuzzyRuleToken *outputToken)
{
//For every output value
CLinguisticVariable* lVar = outputToken->GetLinguisticVariable();
vector<LinguisticValue*> valuesList = lVar->GetLinguisticValuesList();
double upEqualation = 0;
double downEqualation = 0;
//Calculating defuzzy value
for(unsigned int i = 0; i < valuesList.size(); i++)
{
LinguisticValue* val = valuesList.at(i);
upEqualation += val->GetLinguisticValue()
* CalculateTrapezoidBalance(val->GetA(), val->GetB(), val->GetC(), val->GetD());
downEqualation += val->GetLinguisticValue();
}
//Return output value of system
if(downEqualation == 0)
return 0;
return upEqualation / downEqualation;
}
//Calculating surface of trapezoid
double CalculateTrapezoidBalance(double A, double B, double C, double D)
{
return ((1 / (B - A)) * (2 * pow(B,3) - 3 * A * pow(B,2) + pow(A,3)) +
3 * (pow(C,2) - pow(B,2)) + (1 / (D-C)) * (2 * pow(C,3) - 3 * D * pow(C,2) + pow(D,3)))
/ (3 * (B - A) + 6 * (C - B) + 3 * (D - C));
}
如何使用它?
您可能想知道如何使用此 FuzzyLogic API。这并不复杂。为了让您生活更轻松,我制作了三个简单的示例。第一个是用于模糊巡航控制,第二个是用于篮球运动员,最后一个是用于滑雪跳跃者。所有示例都在名为 CExperiments
的类中。让我们看看简单的篮球运动员示例。我们创建三个语言变量,然后将语言值添加到语言变量中。使用语言变量,我们创建两个规则并将规则添加到 FuzzyLogic 中。
void BuildBasketballPlayersRules()
{
//Method for building necessary rules for Basketball players
CLinguisticVariable *height = new CLinguisticVariable(false,"Height");
CLinguisticVariable *age = new CLinguisticVariable(false,"Age");
CLinguisticVariable *basketball_player =
new CLinguisticVariable(true,"Basketball_player");
//Add linguistic values to variable
height->AddLinguisticValue(new LinguisticValue("Low_height",150,150,170,180));
height->AddLinguisticValue(new LinguisticValue("Average_height",170,180,185,190));
height->AddLinguisticValue(new LinguisticValue("Tall",185,195,210,210));
age->AddLinguisticValue(new LinguisticValue("Young",10,10,25,30));
age->AddLinguisticValue(new LinguisticValue("Average_young",25,35,40,45));
age->AddLinguisticValue(new LinguisticValue("Old",50,60,80,80));
age->AddLinguisticValue(new LinguisticValue("Average_old",40,45,50,55));
basketball_player->AddLinguisticValue(new LinguisticValue("Suitable",0.5,0.6,1,1));
basketball_player->AddLinguisticValue(new LinguisticValue("Unsuitable ",0,0,0.4,0.5));
//Rule1
//IF age = Young AND height = Tall THEN basketball_player = "Suitable"
CFuzzyRule* rule1 = new CFuzzyRule();
rule1->AddTokenToRule(new CFuzzyRuleToken(false,EMPTY, age,"Young"));
rule1->AddTokenToRule(new CFuzzyRuleToken(false,AND, height,"Tall"));
rule1->AddTokenToRule(new CFuzzyRuleToken(false,EMPTY, basketball_player,"Suitable"));
//Rule2
//IF age = "Old" THEN basketball_player = "Unsuitable"
CFuzzyRule* rule2 = new CFuzzyRule();
rule2->AddTokenToRule(new CFuzzyRuleToken(false,EMPTY, age,"Old"));
rule2->AddTokenToRule(new CFuzzyRuleToken(false,EMPTY,
basketball_player,"Unsuitable "));
//Add rules
fr->AddFuzzyRule(rule1);
fr->AddFuzzyRule(rule2);
//Add linguistic variables, if you want to obtain
//more informations about fuzzy calculations
AddLinguisticVariables(height);
AddLinguisticVariables(age);
AddLinguisticVariables(basketball_player);
}
如何执行示例
通过下一个示例,您应该了解如何使用 CExperiment
类。
int _tmain(int argc, _TCHAR* argv[])
{
//Create instance of CExperiments.
CExperiments* experiment = new CExperiments();
//Create rules
experiment->BuildCruiseControlRules();
//Hash_map for storing inputs
hash_map <string, CFuzzyInput*> h_fuzzyInputs;
//Put some values to inputs
CFuzzyInput* inputRazdalja = new CFuzzyInput("Distance", 1.8);
CFuzzyInput* inputPriblizevanje = new CFuzzyInput("Approaching", 15);
CFuzzyInput* inputHitrost = new CFuzzyInput("Speed", -8);
//Insert inputs to the hash_map
h_fuzzyInputs.insert(CFuzzyInput_Pairs(inputRazdalja->GetVariableName(), inputRazdalja));
h_fuzzyInputs.insert(CFuzzyInput_Pairs(inputPriblizevanje->GetVariableName(), inputPriblizevanje));
h_fuzzyInputs.insert(CFuzzyInput_Pairs(inputHitrost->GetVariableName(), inputHitrost));
//Calculate fuzzy Logic output signal
double outputSignal = experiment->CalculateFuzzyRules(h_fuzzyInputs);
//Get some additional informations from fuzzy logic
double t = experiment->GetC_ByName("Distance","Average");
double v1 = experiment->GetLinguisticVariableValue("Signal","Brake");
double v2 = experiment->GetLinguisticVariableValue("Signal","Maintain");
double v3 = experiment->GetLinguisticVariableValue("Signal","Accelerate");
double v4 = experiment->GetLinguisticVariableValue("Distance","Low");
double v5 = experiment->GetLinguisticVariableValue("Distance","Average");
double v6 = experiment->GetLinguisticVariableValue("Distance","High");
double v7 = experiment->GetLinguisticVariableValue("Approaching","Slow");
double v8 = experiment->GetLinguisticVariableValue("Approaching","Average");
double v9 = experiment->GetLinguisticVariableValue("Approaching","Fast");
double v10 = experiment->GetLinguisticVariableValue("Speed","Slow");
double v11 = experiment->GetLinguisticVariableValue("Speed","Acceptable");
double v12 = experiment->GetLinguisticVariableValue("Speed","Fast");
//YOU HAVE TO RESET TOKEN VALUES BEFORE NEW CALCULATIONS!!!
experiment->ResetTokenValues();
delete experiment;
return 0;
}
太复杂了?
为了更轻松地定义规则和属性,我制作了一个简单的 DSL(领域特定语言)。
CFuzzyLanguageParser
项目 CFuzzyLogicDSL
的唯一类是 CFuzzyLanguageParser
。此类用于解析模糊规则和属性。输入值是包含模糊规则/属性的文件的路径,输出值是 CExperiment
中更新的规则和属性(您必须将 CExperiment*
指针放入 CFuzzyLanguageParser
构造函数)。让我们看看输入文件的格式。请,请非常准确地处理空格。如果您忘记空格,解析器将无法工作(我已尝试保持一个简单的实现)。您可以使用以下格式定义属性
Linguistic_Variable_Name Input/Output ValueName_0 (A,B,C,D) ValueName_N (A,B,C,D)
和规则
IF Linguistic_Variable_Name = Linguistic_Value_Name AND ...
AND Linguistic_Variable_Name NOT Linguistic_Value_Name THEN
Linguistic_Variable_Name = Linguistic_Value_Name
还可以使用以下方法写注释:
% This is comment
下一步是代码解释。“main”方法是 Parse
。逐行简单读取,按空格分割令牌,然后检查它是注释、空行、规则还是属性。
void Parse(char* filePath)
{
ifstream ifs(filePath);
string line;
while( getline( ifs, line ) )
{
vector<string> tokens = SplitBy(line,' ');
//Skip lines without text
if(tokens.size() > 0)
{
//Skip comments
if(!IsComment(tokens[0]))
{
if(IsRule(tokens[0]))
ParseFuzzyRule(tokens);
else
ParseFuzzyAtribute(tokens);
}
}
}
//Close stream
ifs.close();
}
以下是检查一行是否为注释或规则的方法
//Check is line is comment
bool IsComment(string token)
{
//Return true if is comment
if(token.at(0) == '%')
return true;
return false;
}
bool IsRule(string token)
{
//Return true if is rule
if(token == "IF")
return true;
return false;
}
我们如何解析规则和属性?查看接下来的两个简单方法。ParseFuzzyRule
仅遍历所有令牌(我们不需要处理第一个令牌,它应该是“IF”)并检查它是否是操作符令牌(AND/OR)并设置适当的操作。然后我们检查是否有否定,或者我们处于“set”(令牌“=”)状态。如果此条件为真,我们只需设置前一个和后一个令牌,创建一个新的 CFuzzyRuleToken
,然后将其添加到规则中。
void ParseFuzzyRule(vector<string> ruleTokens)
{
//Rule example
//IF distance = low AND approaching NOT slow THEN signal=breake
//SKIP IF token
bool negated = false;
string leftSide;
string rightSide;
Operation operation = EMPTY;
CFuzzyRule* rule = new CFuzzyRule();
for(unsigned int i = 1; i < ruleTokens.size(); i++)
{
//Check for operator
//OPERATORS =, Operator(AND, OR), NOT, THEN
if((ruleTokens[i] == "AND") || (ruleTokens[i] == "OR"))
{
if(ruleTokens[i] == "AND")
operation = AND;
else
operation = OR;
}
else if((ruleTokens[i] == "=") || (ruleTokens[i] == "NOT"))
{
leftSide = ruleTokens[i - 1];
//Next token is right side
if(ruleTokens[i] == "NOT")
negated = true;
rightSide = ruleTokens[++i];
//ADD TOKEN TO RULE
CLinguisticVariable* linVar =
fuzzyInterface->FindLinguisticVariableByName(leftSide);
assert(linVar != NULL);
rule->AddTokenToRule(
new CFuzzyRuleToken(negated,operation,linVar,rightSide));
//RESET
operation = EMPTY;
negated = false;
}
}
fuzzyInterface->AddFuzzyRule(rule);
}
现在,解析属性。第一个令牌始终是语言变量名,第二个令牌应该是属性信息。可以有 N 个语言值。我们必须遍历所有令牌,解析是输入还是输出。有了这些信息,我们就可以创建 CLinguisticVariable
。然后我们可以开始解析语言值。我们必须遍历所有剩余的令牌。下一个令牌必须是语言值名称,后跟语言值(ParseLinguisticValues
方法)。解析完所有语言值后,我们可以将 LinguisticVariable
添加到列表中(在 CExperiment
中)。
void ParseFuzzyAtribute(vector<string> attTokens)
{
//Distance Input Low (0,0,1,2) Average (1,2,3,4) High (3,4,5,5)
//Linguistic variable name
//Input or output
//Linguistic value name
//Values A,B,C,D
string linguisticVariableName = attTokens.at(0);
bool output = true;
if(attTokens.at(1) == "Input")
output = false;
//Creating new linguistic variable
CLinguisticVariable *linguisticVariable =
new CLinguisticVariable(output,linguisticVariableName);
//Adding linguistic values to variable
for(unsigned int i = 2; i < attTokens.size(); i++)
{
string linguisticValueName = attTokens.at(i++);
vector<double> linguisticValues = ParseLinguisticValues(attTokens.at(i));
//Check for error!
assert(linguisticValues.size() == 4);
LinguisticValue* linValue = new LinguisticValue(linguisticValueName,
linguisticValues[0],
linguisticValues[1],
linguisticValues[2],
linguisticValues[3]);
linguisticVariable->AddLinguisticValue(linValue);
}
fuzzyInterface->AddLinguisticVariables(linguisticVariable);
}
vector<double> ParseLinguisticValues(string token)
{
vector<double> linguisticValues;
//Remove first and last char
token = token.erase(0,1);
token = token.erase(token.size() - 1, 1);
vector<string> values = SplitBy(token, ',');
//Values size should be 4!
for(unsigned int i = 0; i < values.size(); i++)
linguisticValues.push_back(strtod(values.at(i).c_str(),NULL));
return linguisticValues;
}
输入文件示例
%ATTRIBUTES
Distance Input Low (0,0,1,2) Average (1,2,3,4) High (3,4,5,5)
Approaching Input Slow (0,0,10,20) Average (10,20,30,40) Fast (30,40,50,50)
Speed Input Slow (-50,-50,-10,0) Acceptable (-10,0,0,10) Fast (0,10,50,50)
Signal Output Brake (-5,-5,-2,-1) Maintain (-2,-1,1,2) Accelerate (1,2,5,5)
%RULES
IF Distance = Low AND Approaching NOT Slow THEN Signal = Brake
IF Distance = Average AND Approaching = Fast THEN Signal = Brake
IF Speed = Fast THEN Signal = Brake
IF Distance NOT Low AND Approaching = Average THEN Signal = Maintain
IF Distance = High AND Approaching NOT Fast AND Speed = Acceptable THEN Signal = Maintain
IF Distance = Average AND Speed = Slow THEN Signal = Maintain
IF Distance = High AND Speed = Slow THEN Signal = Accelerate
我想看看!
我的朋友 Jan Bezget 使用 OGRE 制作了一个非常酷的巡航控制示例 3D 可视化。您可以在此处找到他的可视化:点击我!
摘要
您现在已经看到,模糊逻辑的实现可以非常简单且有效。令人印象深刻的是,像模糊逻辑这样简单的 AI 方法可以如此有用。佳能使用模糊逻辑进行自动对焦,它也常用于空调,甚至用于仙台地铁系统!我希望您喜欢我的文章和我的 FuzzyLogic API 实现。谢谢阅读 :)