Fuzzy Logic Example #2
Introduction
Few months ago I came across a paper titled, “Animated Fuzzy Logic” by G. Meehan and M. Joy (pdf). The abstract of the paper is as follows:
In this paper we aim to give an introduction to fuzzy logic using the language Haskell to implement our solutions. We shall see how the high-level, declarative nature of a functional language allows us to implement easily and efficiently solutions to problems using fuzzy logic and, in particular, how the presence of functions as first-class values allows us to model the key concept of the fuzzy subset in a natural way.
This paper serves as an excellent example how a subject such as fuzzy logic can be reasoned about using Haskell. The examples used in the paper to illustrate various concepts are very well thought out. Given the quality of the examples, I thought be interesting to convert some of the code from Haskell to Python.
All of the source code below can be found this file: fuzzylogic.py
Preliminaries
The first bit up for conversion is the implementation of fuzzy subsets. The given examples talks about a function profitable, the implementation of which is shown below in Haskell.
type Fuzzy a = a -> Double
type Percentage = Double
up :: Double -> Double -> Fuzzy Double
up a b x
| x < a = 0.0
| x < b = (x -a) / (b - a)
| otherwise = 1.0
profitable :: Fuzzy Percentage
profitable = up 0 15
In the above, up is a generic fuzzy subset. This subset can be represented graphically as a single line going up from a to b over the domain. profitable is a specific case of up in the domain [0, 15]
. The Python implementation is pretty straightforward.
from functols import partial
def up(a, b, x):
# make sure we are dealing with floats only
assert all([isinstance(val, float) for val in (a, b, x)])
if x < a:
return 0.0
if x < b:
return (x - a) / (b - a)
return 1.0
profitable = partial(up, 0., 15.)
If the use of partial
is frowned upon, profitable can be re-written to be:
def profitable(percentage):
return up(0.0, 15.0, percentage)
We can use profitable
like so:
>>> from fuzzylogic import *
>>> profitable(10.)
While on the on subject of membership functions we should also implement down, tri and trap.
def down(a, b, x):
assert all(isinstance(val, float) for val in (a, b, x))
return 1. - up(a, b, x)
def tri(a, b, x):
assert all([isinstance(val, float) for val in (a, b x)])
m = (a + b) / 2.
first = (x - a) / (m - a)
second = (b - x) / (b - m)
return max(min(first, second), 0.)
def trap(a, b, c, d, x):
assert all([isinstance(val, float) for val in (a, b, c, d, x)])
first = (x - a) / (b - a)
second = (d - x) / (d - c)
return max(min(first, 1., second), 0.)
Terms such as very, somewhat, are often used in conjunction with membership functions. For example, a company can be very profitable, while another company is somewhat profitable. In fuzzy logic, these words are referred to as hedges.
Meehan and Joy chose to implement hedges as higher order functions. The same can be done in Python. The example below shows a generic implementation of a hedge. Afterwards, this function is used to create the hedges: very, extremely, somewhat and slightly.
def hedge(p, mvalue):
"""Generic definition of a function that alters a given membership
function by intensifying it in the case of *very*, and of diluting it
in the case of *somewhat*. """
mvalue = float(mvalue)
if not p:
return 0.0
return math.pow(mvalue, p)
very = partial(hedge, 2.)
extermely = partial(hedge, 3.)
somewhat = partial(hedge, 0.5)
slightly = partial(hedge, 1. / 3.)
We can use the above defined functions as shown below:
>>> from fuzzylogic import *
>>> very(profitable(10.))
Fuzzy Database Queries
At this stage we have enough to run the, “Fuzzy Database Queries” example from the paper.
companies = [
('a', 500, 7), ('b', 600, -9), ('c', 800, 17),
('d', 850, 12), ('e', 900, -11), ('f', 1000, 15),
('g', 1100, 14), ('h', 1200, 1), ('i', 1300, -2),
('j', 1400, -6), ('k', 1500, 12)
]
profit = itemgetter(2)
sales = itemgetter(1)
percentages = map(float, range(-10, 30, 1))
profitable = partial(up, 0., 15.)
high = partial(up, 600., 1150.)
fand = min
def ffilter(predicate, items):
"""Filter out the companies where the membership value is 0.0"""
snd = itemgetter(1)
return filter(
lambda x: snd(x) != 0.0,
map(predicate, items)
)
def p1(company):
"""Profitable companies"""
value = profitable(profit(company))
# this is a slight hack to pass the resultant
# value through the filter.
return (company, fand(value, 1))
def p2(company):
"""Profitable companies that have high sales""""
a = profitable(profit(company))
b = high(sales(company))
return (company, fand(a, b))
def p3(company):
"""Somewhat profitable companies with very high sales."""
a = somewhat(profitable(profit(company)))
b = very(high(sales(company)))
return (company, fand(a, b))
With all of the code in the fuzzylogic.py module we can try out the queries.
>>> import fuzzylogic as fl
>>> from pprint import pprint
>>> result = fl.ffilter(fl.p1, fl.companies)
>>> pprint(result)
[(('a', 500, 7), 0.4666666666666667),
(('c', 800, 17), 1.0),
(('d', 850, 12), 0.8),
(('f', 1000, 15), 1.0),
(('g', 1100, 14), 0.9333333333333333),
(('h', 1200, 1), 0.06666666666666667),
(('k', 1500, 12), 0.8)]
>>> result = fl.ffilter(fl.p2, fl.companies)
>>> pprint(result)
[(('c', 800, 17), 0.36363636363636365),
(('d', 850, 12), 0.45454545454545453),
(('f', 1000, 15), 0.7272727272727273),
(('g', 1100, 14), 0.9090909090909091),
(('h', 1200, 1), 0.06666666666666667),
(('k', 1500, 12), 0.8)]
>>> result = fl.ffilter(fl.p3, fl.companies)
>>> pprint(result)
[(('c', 800, 17), 0.1322314049586777),
(('d', 850, 12), 0.20661157024793386),
(('f', 1000, 15), 0.5289256198347108),
(('g', 1100, 14), 0.8264462809917354),
(('h', 1200, 1), 0.2581988897471611),
(('k', 1500, 12), 0.8944271909999159)]
Shoe example
The entire shoe example from the paper is shown below. For the most part is uses the same building blocks seen previously. The most important new parts are rulebase
and centroid
functions. rulebase
is used to specify the rules of fuzzy logic controller. centroid
contains the implementation of the centroid defuzzification method.
def approximate(fuzz, n, domain):
hw = fuzz * (max(domain) - min(domain))
return partial(tri, n - hw, n + hw)
sizes = range(4, 13, 0.5)
short = partial(down, 1.5, 1.625)
medium = partial(tri, 1.525, 1.775)
tall = partial(tri, 1.675, 1.925)
very_tall = partial(up, 1.825, 1.95)
#small = partial(down, 4., 6.)
def small(size):
return down(4., 6., size)
#average = partial(tri, 5., 9.)
def average(size):
return tri(5., 9., size)
#big = partial(tri, 8., 12.)
def big(size):
return tri(8., 12., size)
#very_big = partial(up, 11., 13.)
def very_big(size):
return up(11., 13., size)
#fl.near(20, fl.range(0, 40, 1))(17.5)
near = partial(approximate, 0.125)
around = partial(approximate, 0.25)
roughly = partial(approximate, 0.375)
rules = [
(short, small),
(medium, average),
(tall, big),
(very_tall, very_big)
]
def updated_func(val, func, size):
first = func(size)
return (val * first)
def rulebase(height):
updated = []
for input_func, output_func in rules:
val = input_func(height)
updated.append(
partial(updated_func, val, output_func)
)
rulebase_function = lambda s: sum([r(s) for r in updated])
return rulebase_function
def centroid(domain, membership_function):
fdom = map(membership_function, domain)
first = sum([a * b for (a, b) in zip(domain, fdom)])
second = sum(fdom)
return first / second
def shoe_example(h):
result = centroid(sizes, rulebase(h))
return result
We can run this example like so:
>>> import fuzzylogic as fl
>>> height = 1.75
>>> fl.shoe_example(height)
9.25
Pricing Example
The last re-implemented example is that of pricing goods. The code is shown below.
def price_example(man_costs=13.25, comp_price=29.99):
"""
Pricing goods (Cox, 1994).
The price should be as high as possible to maximize takings but as low as
possible to maximize sales. We also want to make a healthy profit (100%
mark-up on the cost price). We also want to consider what the competition
is charging.
rule1: our price must be high
rule2: our price must be low
rule3: our price must be around twice the manufacturing costs.
rule4: if the competition price is not very high then our price must be
around the competition price.
"""
prices = range(15., 35., 0.5)
high = partial(up, 15., 35.)
low = lambda p: 1 - high(p)
not_very = lambda v: 1 - very(high(v))
our_price1 = centroid(prices, partial(mand, [high, low]))
our_price2 = centroid(
prices,
partial(mand, [high, low, around(2.0 * man_costs, prices)]),
)
our_price3 = centroid(
prices,
partial(
mand, [
high, low, around(2.0 * man_costs, prices),
lambda p: not_very(comp_price) * around(comp_price, prices)(p)
]
)
)
print our_price1, our_price2, our_price3
This example can be run like so:
>>> import fuzzylogic as fl
>>> fl.price_example()
25.0 26.2519685039 28.5892834973
The output of 25.0 corresponds to rules 1 and 2. The output of 26.25 corresponds to rules 1, 2 and 3. The output of 28.58 corresponds to the use of all four rules.
Conclusion
The code in the examples above is very close to the original Haskell source code. It is possible to re-write them in a more Pythonic style. However, I feel that this would take something away from the learning value.