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.