LSR/README.md

242 lines
8.0 KiB
Markdown
Raw Normal View History

2020-06-04 19:21:01 +02:00
An FCL parser with a scikit-fuzzy back-end
=======================================
This is a parser for the Fuzzy Control Language
[FCL](https://en.wikipedia.org/wiki/Fuzzy_Control_Language)
along with a back-end for
[scikit-fuzzy](https://github.com/scikit-fuzzy/scikit-fuzzy),
a fuzzy logic toolkit for SciPy.
The basic use-case is to parse a FCL file and then use the fuzzy rules
in your `scikit-fuzzy` code. For example:
```python
from fcl_parser import FCLParser
p = FCLParser() # Create the parser
p.read_fcl_file('tipper.fcl') # Parse a file
# ... and so on, as usual for skfuzzy:
cs = ctrl.ControlSystem(p.rules)
```
After reading a file the parser object has attributes to supply the
`rules` (as above) or the `antecedents`, the `consequents`, or all the
`fuzzy_variables` All these are represented via lists of their
corresponding `scikit-fuzzy` objects.
Other Entry Points
------------------
The parser can be used to accept program fragments, so you can
interleave its use with regular `scikit-fuzzy` code.
For example, in the following `scikit-fuzzy` code we set up the
tipping example in the usual way by specifying the variables and
defining some membership functions for the inputs:
```python
# First we set up the variables in the usual way:
food = ctrl.Antecedent(np.linspace(0, 10, 11), 'quality')
service = ctrl.Antecedent(np.linspace(0, 10, 11), 'service')
tip = ctrl.Consequent(np.linspace(0, 25, 26), 'tip')
# Auto-generate the membership functions for the inputs:
food.automf(3)
service.automf(3)
```
We can define the output variable using FCL code, in this case getting
the parser to parse a membership function `mf` definition:
```python
# Define a FCL parser-object:
p = FCLParser()
# Use FCL to define membership functions for the output:
tip['bad'] = p.mf('Triangle 0 0 13', tip.universe)
tip['middling'] = p.mf('Triangle 0 13 25', tip.universe)
tip['lots'] = p.mf('Triangle 13 25 25', tip.universe)
```
Last, we can define the rules in FCL, and get a `scikit-fuzzy` rule
object for each of them if we like:
```python
# We need to tell the parser about the variables before we parse any rules:
p.add_vars([food, service, tip])
# Now use FCL to define three rules:
rule1 = p.rule('IF quality is poor OR service is poor THEN tip is bad')
rule2 = p.rule('IF service is average THEN tip is middling')
rule3 = p.rule('IF service is good OR quality is good THEN tip is lots')
# To get the control system, just add the rules (from the parser):
tipping = ctrl.ControlSystem(p.rules)
```
There are some more examples of mixed FCL/skfuzzy use in the file
[tests/test_fcl_parser.py](./tests/test_fcl_parser.py)
Dependencies
------------
The scanner is written using
[PLY](http://www.dabeaz.com/ply/ply.html) (Python Lex-Yacc),
so you need to install PLY before the code here will work.
$ pip install ply
You don't need to import this anywhere, my scanner code just needs it.
The parser is hand-written so we don't actually use the
parser-generation features of PLY.
What's implemented
------------------
Much of FCL is implemented, concentrating on
the subset of FCL that can be translated easily into
`scikit-fuzzy`. That includes most parts of a standard
(Mamdani-style) fuzzy system.
At the moment the main options are for:
* defuzzification methods: cog, coa, lm, rm, mom
* membership functions: quite a collection; have a look in
[fcl_symbols.py](./fcl_symbols.py) for a list.
* and/or methods (norms and co-norms): again, quite a few,
including (norms) min, prod, bdif, drp, eprod, hprod, nilmin
and their co-norm duals.
I was doing this with an eye on the XML standard, hence the rather
large selection of membership functions and norms.
I've also implemented the *hedge functions* listed in the IEEE standard,
so you can write things like:
```python
rule1 = p.rule('IF quality is slightly poor OR service is very poor THEN tip is extremely bad')
```
What happens here is that when the rule is processed, the hedge
functions are applied to the corresponding membership function, and a
new membership function is generated and added to the variable. For
example, a membership function called `_slightly_poor` would be added
to the variable `quality` above.
What's not implemented
------------------
Most notably _not_ implemented (yet) are options for:
* activation method (this is hard-wired to `MIN`).
At the moment `scikit-fuzzy` doesn't have an option to change this;
its CrispValueCalculator always uses np.minimum.
* accumulation method (well, not exactly).
This is a small incompatibility: FCL sees the accumulation as a
property of the rule-base, whereas `scikit-fuzzy` sees it as a
property of the output variables. I could fix the parser to
propagate the setting from the rules to the variables used in those
rules, but this might cause unexpected behaviour if the variables
are used in more than one rule base.
You can set an 'ACCU' option as part of an (output) variable
definition, and this will be propagated through to `scikit-fuzzy`.
* default values for variables.
In FCL these values are used in defuzzification when all the
memberships have been cut to zero area. As far as I can see this
case will raise an exception in `scikit-fuzzy`.
The parser accepts these, I just haven't figured out how to get them
into the `scikit-fuzzy` code, so they are ignored for the moment.
Compliance
----------
First of all, I'm working from the draft of the FCL standard (IEC
TC65/WG 7/TF8), plus any examples I could find, so I may have missed a
few things.
Second, the parser does not enforce strict conformance to the FCL standard,
and is somewhat liberal in the kind of FCL code it will accept.
This is intended as a feature, not a bug.
In particular:
* Case is not relevant for keywords
(so `rule` and `RULE` are the same)
but note that case _is_ relevant for identifiers (e.g. variable names).
* The semi-colon at the end of lines can be left out in most cases.
* The parser doesn't impose a strict ordering on the contents of
variable definitions, so you can mix `TERM`, `RANGE`, `METHOD`
etc. in your preferred order.
I only made one real change to the FCL language
to better support `scikit-fuzzy`:
* When defining a variable range (universe) you can specify
the granularity using an optional `WITH` setting, thus:
```
RANGE := (0 .. 2.1) WITH 0.01
```
This maps directly to a NumPy `arange(1, 2.1, 0.01)` expression.
This is due to the way `scikit-fuzzy` calculates its membership
functions: these get worked out to point-lists when they are defined,
so I need to know the granularity to get this right.
This working-out is also the reason we can't really generate FCL from
a `scikit-fuzzy` program, since the information on the original
definition of the membership functions is not retained once the
point-sets have been calculated.
Reading the code
----------------
The main functionality is in [fcl_parser.py](./fcl_parser.py)
which contains the
hand-written top-down parser. This is essentially a context-free
grammar, with a Python method for each non-terminal.
This can be called from the command-line if you just want to parse a file;
for example:
```
$ python fcl_parser.py tests/tipper.fcl
```
The scanner code is in [fcl_scanner.py](./fcl_scanner.py).
This uses a few tricks related
to PLY, but us essentially a list of regular expressions plus some
extra code to check tokens etc.
The symbol table is in [fcl_symbols.py](./fcl_symbols.py)
and contains a list of the
variables and rules, added in as they are processed. The mappings
between option names (membership functions, defuzzification method
etc.) is also kept here.
The other files are simple auxiliary definitions: some extra
membership functions (that are not in `scikit-fuzzy`) are defined in
[extramf.py](./extramf.py)
and the t-norms and their duals are defined in
[norms.py](./norms.py).
The set of hedge functions as defined in the IEEE standard is implemented in
[hedges.py](./hedges.py).
[James Power](http://www.cs.nuim.ie/~jpower/),
27 August 2018.