Fuzzy Logic?
In this article, I will introduce you to my Fuzzy Logic implementation, called FuzzyLogic API. I believe this implementation is simple, effective, and quite fast.
Do you know what Fuzzy Logic is? Here I won't talk about Fuzzy Logic theory too much, because there are many existing cool articles about Fuzzy Logic (see links below).
Fuzzy Logic implementation
In this section, I will describe my FuzzyLogic API implementation. I have tried to make a clean and simple implementation that would be
easy to understand for everyone. At first I will try to describe some simple classes which we will later use in more complicated and important classes in the FuzzyLogic API.
CLinguisticVariable
CLinguisticVariable
is the class used for representing the linguistic variables. There are three class variables. variableName
is used for defining
the linguistic variable name and should be unique, because we're using a hash map for storing CLinguisticVariable
, so we can find specific linguistic variables in time O(1).
The next variable, b_output
declares if a linguistic variable is output or not. Please note, there can be only one output variable
in the FuzzyLogic API. The last variable, h_linguisticValuesHashMap
is used for storing linguistic values. One linguistic variable can have one or more linguistic values.
There are several class methods. AddLinguisticValue
adds LinguisticValue
to the selected CLinguisticVariable
.
In the next image, you can see four linguistic variables: Distance
, Approaching
, Speed
, and Signal
.
The next method adds the linguistic value to a hash_map
.
void AddLinguisticValue(LinguisticValue* p_linguisticValue)
{
h_linguisticValuesHashMap.insert(LinguisticValue_Pairs(
p_linguisticValue->ToString(), p_linguisticValue));
}
where LinguisticValue_Pairs
is a typedef
declared in:
typedef pair <string, LinguisticValue*> LinguisticValue_Pairs;
The method ResetLinguisticValues
just resets all linguistic values to -1. A more interesting method is FindLinguisticValueByName
.
The input parameter of the method is the linguistic value name and the return value is a pointer to the object of the LinguisticValue
.
Because we are using a hash map, the time complexity is O(1).
LinguisticValue* FindLinguisticValueByName(string name)
{
hash_map <string, LinguisticValue*> :: const_iterator iterator;
iterator = h_linguisticValuesHashMap.find(name);
if(iterator == h_linguisticValuesHashMap.end())
{
assert(NULL);
return NULL;
}
return (LinguisticValue*)iterator->second;
}
CalculateLinguisticValueByName
is just a bridge between CFuzzyReasoner
and LinguisticValue
for calculating the linguistic value. A little bit more
complicated is UpdateLinguisticValueByName
. At first we have to find LinguisticValue
by name. If there is a value stored already,
we will perform the fuzzy operation OR, else we will just store newVal
. Let's see the code:
void UpdateLinguisticValueByName(string name, double newVal)
{
LinguisticValue* value = FindLinguisticValueByName(name);
if(value->GetLinguisticValue() != -1)
value->SetLinguisticValue(CFuzzyCalculator::OR(value->GetLinguisticValue(), newVal));
else
value->SetLinguisticValue(newVal); }
LinguisticValue
The class LinguisticValue
is used for storing linguistic values. Variables A
, B
, C
, and D
are "borders"
of the trapezium (trapezoid). For example, the linguistic variable Distance
has three linguistic values (Low
,
Average
, and Height
) and for the linguistic value "Low
", the variables A
, B
,
C
, and D
would be A = 0, B = 0, C = 1, D = 2. Every LinguisticValue
has a unique name (like in CLinguisticVariable
).
The variable linguisticValue
is used for storing a numeric value. The default value is -1. This value means, how much input value belongs to this
LinguisticValue
trapezium. For the linguistic value Average
(linguistic variable Speed
, input: 0 (km/h)), this value would be equal
to 0.5 (see image above). There is only one method (except the constructor, getters, and setters) called CalculateLinguisticValue
. We have to take care
of the correctness of the inputs, so if inputs are out of range, we should normalize them. Then we need to calculate the trapezoidal function. The result of the trapezoidal function
is the linguistic value for a specific variable.
double CalculateLinguisticValue(double input)
{
if(input < A)
input = A;
else if(input > D)
input = D;
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
Probably the simplest class in the project. There are only two static methods. The method AND
implements the operator AND, which is equal to returning the minimal value.
static double AND(double a, double b)
{
if(a < b)
return a;
return b;
}
and method OR
implements operator OR, returning the maximal value.
static double OR(double a, double b)
{
if(a > b)
return a;
return b;
}
CFuzzyRule
Fuzzy Logic consists of linguistic variables and fuzzy rules. This class is used for storing a fuzzy rule. A fuzzy rule consists
of objects of the class CFuzzyToken
. The only functionality of the class is adding CFuzzyRuleToken
to a rule. For example, CFuzzyRule
could be:
"IF NOT SPEED = SLOW AND DISTANCE = CLOSE, SIGNAL = BRAKE"
Every fuzzy rule should have an output variable!
CFuzzyRuleToken
Like I said before, CFuzzyRuleToken
is just one part of a CFuzzyRule
. Let's describe all variables.
operation
describes what kind of operation is between the previous and current CFuzzyRuleToken
. Supported operators are
OR, AND, and EMPTY. EMPTY means there isn't any operator ("VOID"). The EMPTY token is just used in the first and last tokens in the rule.
Also tokens can be negated (variable b_negation
). Every CFuzzyToken
has a pointer to CLinguisticVariable
and the name of the linguistic value.
Let's see the CFuzzyRule
example a little bit closer.
"IF NOT SPEED = SLOW AND DISTANCE = CLOSE, SIGNAL = BRAKE"
What are tokens? There are three token examples:
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"
Please note, TOKEN_3
is the output token! Every CFuzzyRule
must have a token with an output variable!!!
The method CalculateTokenOutputValue
is used for getting an input value from the hash map h_fuzzyInputHashMap
and calling a method for calculating
the linguistic value. The input variable h_fuzzyInputHashMap
is a hash_map
filled with objects (class CFuzzyInput
).
In CFuzzyInput
is stored input values for specific linguistic variables. CFuzzyInput
will be explained later.
double CalculateTokenOutputValue(hash_map <string, CFuzzyInput*> h_fuzzyInputHashMap)
{
hash_map <string,> :: const_iterator iterator;
iterator = h_fuzzyInputHashMap.find(p_linguisticVariable->ToString());
CFuzzyInput* temp = (CFuzzyInput*)iterator->second;
double input = temp->GetInputValue();
return p_linguisticVariable->CalculateLinguisticValueByName(linguisticValueName, input);
}
CFuzzyInput
CFuzzyInput
has only two variables, variableName
for the linguistic variable name and inputValue
(type double
) for
the value. For example, CFuzzyInput
could be variableName = "SPEED"
and inputValue = "43"
(km/h).
CFuzzyReasoner
CFuzzyReasoner
is the heart of the FuzzyLogic system. It's used for calculating fuzzy rule output values and the final defuzzyfication. The CFuzzyReasoner
variable v_fuzzyRulesVector
is used for storing fuzzy rules (CFuzzyRule
). Let's see the most important method in FuzzyLogic,
FuzzyRuleReasoner
. The method takes as inputs a fuzzy rule and a hash map filled with fuzzy inputs. Every fuzzy rule token is checked if it is an output variable or not.
If not, output variables are calculated linguistic values of the current token variable. Then we check if the token is negated or not. If the linguistic
variable is not first in the fuzzy rule and it's not an output variable then we should have an operator different than EMPTY. Only the first variable
in the fuzzy rule must have the operator EMPTY (for the output value, operator isn't important). For the output value we just set a new value (resetVal
) and return
a pointer to the output token.
CFuzzyRuleToken* FuzzyRuleReasoner(CFuzzyRule* fuzzyRule,
hash_map <string, CFuzzyInput*> h_fuzzyInputs)
{
double resultVal = 0;
for(unsigned int i = 0; i < fuzzyRule->GetFuzzyRuleTokens().size(); i++)
{
CFuzzyRuleToken* token = fuzzyRule->GetFuzzyRuleTokens()[i];
if(token->IsOutput())
{
token->UpdateTokenValue(resultVal);
return token;
}
else
{
double tokenVal = token->CalculateTokenOutputValue(h_fuzzyInputs);
token->UpdateTokenValue(tokenVal);
if(token->IsNegated())
tokenVal = 1 - tokenVal; if(i == 0)
resultVal = tokenVal; else if(token->IsOrOperator())
resultVal = CFuzzyCalculator::OR(resultVal, tokenVal); else if(token->IsAndOperator())
resultVal = CFuzzyCalculator::AND(resultVal, tokenVal); }
}
assert(NULL);
return NULL;
}
The following method just calls reasoning for every fuzzy rule in the FuzzyLogic API.
CFuzzyRuleToken* CalculateFuzzyRules(hash_map <string, CFuzzyInput*> h_fuzzyInputs)
{
CFuzzyRuleToken* outputObject;
for(unsigned int i = 0; i < v_fuzzyRulesVector.size(); i++)
{
outputObject = FuzzyRuleReasoner((CFuzzyRule*)v_fuzzyRulesVector[i], h_fuzzyInputs);
}
return outputObject;
}
The last step in Fuzzy Logic reasoning is Defuzzyfication. It's implemented in the next two methods.
double Defuzzy(CFuzzyRuleToken *outputToken)
{
CLinguisticVariable* lVar = outputToken->GetLinguisticVariable();
vector<LinguisticValue*> valuesList = lVar->GetLinguisticValuesList();
double upEqualation = 0;
double downEqualation = 0;
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();
}
if(downEqualation == 0)
return 0;
return upEqualation / downEqualation;
}
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));
}
How to use it?
Probably you are wondering how to use this FuzzyLogic API. It's not so complicated. For making your life easier, I have made three
simple examples. The first one is for a Fuzzy Cruise Control, the second for a basketball player, and the last one for a ski jumper. All examples are in the class called CExperiments
.
Let's see the simple basketball player example. We create three linguistic variables, then we add linguistic values to the linguistic variables. With the linguistic variables,
we create two rules and add the rules to FuzzyLogic.
void BuildBasketballPlayersRules()
{
CLinguisticVariable *height = new CLinguisticVariable(false,"Height");
CLinguisticVariable *age = new CLinguisticVariable(false,"Age");
CLinguisticVariable *basketball_player =
new CLinguisticVariable(true,"Basketball_player");
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));
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"));
CFuzzyRule* rule2 = new CFuzzyRule();
rule2->AddTokenToRule(new CFuzzyRuleToken(false,EMPTY, age,"Old"));
rule2->AddTokenToRule(new CFuzzyRuleToken(false,EMPTY,
basketball_player,"Unsuitable "));
fr->AddFuzzyRule(rule1);
fr->AddFuzzyRule(rule2);
AddLinguisticVariables(height);
AddLinguisticVariables(age);
AddLinguisticVariables(basketball_player);
}
How to execute the example
With the next sample you should understand how to use the class CExperiment
.
int _tmain(int argc, _TCHAR* argv[])
{
CExperiments* experiment = new CExperiments();
experiment->BuildCruiseControlRules();
hash_map <string, CFuzzyInput*> h_fuzzyInputs;
CFuzzyInput* inputRazdalja = new CFuzzyInput("Distance", 1.8);
CFuzzyInput* inputPriblizevanje = new CFuzzyInput("Approaching", 15);
CFuzzyInput* inputHitrost = new CFuzzyInput("Speed", -8);
h_fuzzyInputs.insert(CFuzzyInput_Pairs(inputRazdalja->GetVariableName(), inputRazdalja));
h_fuzzyInputs.insert(CFuzzyInput_Pairs(inputPriblizevanje->GetVariableName(), inputPriblizevanje));
h_fuzzyInputs.insert(CFuzzyInput_Pairs(inputHitrost->GetVariableName(), inputHitrost));
double outputSignal = experiment->CalculateFuzzyRules(h_fuzzyInputs);
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");
experiment->ResetTokenValues();
delete experiment;
return 0;
}
Too complicated?
For even easier defining of rules and attributes, I made a simple DSL (domain specific language).
CFuzzyLanguageParser
The only class of the project CFuzzyLogicDSL
is CFuzzyLanguageParser
. This class is used for parsing fuzzy rules and
attributes. The input value is the path to the file with fuzzy rules / attributes, and output values are updated rules and attributes in CExperiment
(you have to put
a CExperiment*
pointer to the CFuzzyLanguageParser
constructor). Let's see the input file format. Please, please be very accurate with spacing. If you forget spaces,
the parser won't work (I have tried to keep a simple implementation). You can define the attributes in this format:
Linguistic_Variable_Name Input/Output ValueName_0 (A,B,C,D) ValueName_N (A,B,C,D)
and rules:
IF Linguistic_Variable_Name = Linguistic_Value_Name AND ...
AND Linguistic_Variable_Name NOT Linguistic_Value_Name THEN
Linguistic_Variable_Name = Linguistic_Value_Name
Also there is the possibility to write comments, using:
% This is comment
The next step is code explanation. The "main" method is Parse
. Simple reading line by line,
splitting tokens by space, then checking if is a commend, empty line, rule, or attribute.
void Parse(char* filePath)
{
ifstream ifs(filePath);
string line;
while( getline( ifs, line ) )
{
vector<string> tokens = SplitBy(line,' ');
if(tokens.size() > 0)
{
if(!IsComment(tokens[0]))
{
if(IsRule(tokens[0]))
ParseFuzzyRule(tokens);
else
ParseFuzzyAtribute(tokens);
}
}
}
ifs.close();
}
Here are the methods for checking if a line is a comment or rule:
bool IsComment(string token)
{
if(token.at(0) == '%')
return true;
return false;
}
bool IsRule(string token)
{
if(token == "IF")
return true;
return false;
}
How do we parse rules and attributes? Check the next two simple methods. ParseFuzzyRule
just goes over all tokens (we don't need to process the first token,
its should be "IF") and check if it is an operator token (AND / OR) and set the appropriate operation. Then we check if there is negation or we are
on the "set" (token "="). If this condition is true, we just have to set the previous and next token, create a new CFuzzyRuleToken
, and add it to the rule.
void ParseFuzzyRule(vector<string> ruleTokens)
{
bool negated = false;
string leftSide;
string rightSide;
Operation operation = EMPTY;
CFuzzyRule* rule = new CFuzzyRule();
for(unsigned int i = 1; i < ruleTokens.size(); i++)
{
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];
if(ruleTokens[i] == "NOT")
negated = true;
rightSide = ruleTokens[++i];
CLinguisticVariable* linVar =
fuzzyInterface->FindLinguisticVariableByName(leftSide);
assert(linVar != NULL);
rule->AddTokenToRule(
new CFuzzyRuleToken(negated,operation,linVar,rightSide));
operation = EMPTY;
negated = false;
}
}
fuzzyInterface->AddFuzzyRule(rule);
}
Now, parsing the attributes. The first token is always the linguistic variable name, the second token should be information of the attribute. There can be N linguistic values.
We have to go over all tokens and parsing is Input or Output. With that information, we can create CLinguisticVariable
. Then we can start to parse
the linguistic values. We have to go over all the remaining tokens. The next token has to be the linguistic value name, followed by the linguistic
values (method ParseLinguisticValues
). When all linguistic values are parsed, we can add LinguisticVariable
to the list (in CExperiment
).
void ParseFuzzyAtribute(vector<string> attTokens)
{
string linguisticVariableName = attTokens.at(0);
bool output = true;
if(attTokens.at(1) == "Input")
output = false;
CLinguisticVariable *linguisticVariable =
new CLinguisticVariable(output,linguisticVariableName);
for(unsigned int i = 2; i < attTokens.size(); i++)
{
string linguisticValueName = attTokens.at(i++);
vector<double> linguisticValues = ParseLinguisticValues(attTokens.at(i));
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;
token = token.erase(0,1);
token = token.erase(token.size() - 1, 1);
vector<string> values = SplitBy(token, ',');
for(unsigned int i = 0; i < values.size(); i++)
linguisticValues.push_back(strtod(values.at(i).c_str(),NULL));
return linguisticValues;
}
Input file example
%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
I want to see it!
My friend Jan Bezget made a really cool 3D visualization (using OGRE) of the Cruise Control example. You can find his
visualization here: Click me!
Summary
You have now seen that implementation of fuzzy logic can be very simple and effective. It's impressive that an easy AI approach such us fuzzy logic can
be so useful. Canon used fuzzy logic for autofocusing, it's often used also in air conditioners, even in the Sendai subway system! I hope that you liked my article
and my FuzzyLogic API implementation. Thank you for reading :)