fuzzylogic icon indicating copy to clipboard operation
fuzzylogic copied to clipboard

Optional numba performance boost needed?

Open amogorkon opened this issue 4 years ago • 3 comments

amogorkon avatar Apr 20 '21 22:04 amogorkon

Well, preliminary timing tests show that fuzzylogic functions are up to 3 times faster than the membership functions in scikit-fuzzy (while having more functionality/safety!), but it's not easy to make numba happy.

from time import perf_counter_ns as time
from statistics import mean, stdev, median

import numpy as np

from numba import jit, njit

from random import randint, random
from fuzzylogic.functions import gauss

def timeit(func):
    res = []
    for _ in range(100000):
        before = time()
        func()
        res.append(time() - before)
    for f in (min, mean, median, stdev):
        print(f(res))
    return res

def gaussmf(x, mean, sigma):
    """
    Gaussian fuzzy membership function.
    Parameters
    ----------
    x : 1d array or iterable
        Independent variable.
    mean : float
        Gaussian parameter for center (mean) value.
    sigma : float
        Gaussian parameter for standard deviation.
    Returns
    -------
    y : 1d array
        Gaussian membership function for x.
    """
    return np.exp(-((x - mean)**2.) / (2 * sigma**2.))

gauss_vars = [randint(0,50) for _ in range(100)]

@jit
def gauss_numba(c:float, b:float, *, c_m=1):
    """Defined by ae^(-b(x-x0)^2), a gaussian distribution.
    
    Basically a triangular sigmoid function, it comes close to human perception.

    vars
    ----
    c_m (a)
        defines the maximum y-value of the graph
    b
        defines the steepness
    c (x0)
        defines the symmetry center/peak of the graph
    """
    assert 0 < c_m <= 1
    assert 0 < b, "b must be greater than 0"

    def f(x):
        try:
            o = (x - c)**2
        except OverflowError:
            return 0
        return c_m * exp(-b * o)
    return f

def test_gaussmf():
    return [gaussmf(x, 10, 5) for x in gauss_vars]

def test_gauss():
    g = gauss(10,5)
    return [g(x) for x in gauss_vars]

def test_gauss_numba():
    g = gauss_numba(10,5, 1)
    return [g(x) for x in gauss_vars]

for x in (test_gauss_numba,):
    print(x.__name__)
    timeit(x)

fails with

Traceback (most recent call last):
  File "F:\Dropbox (Privat)\code\justuse\tests\.tests\.test2.py", line 81, in <module>
    timeit(x)
  File "F:\Dropbox (Privat)\code\justuse\tests\.tests\.test2.py", line 16, in timeit
    func()
  File "F:\Dropbox (Privat)\code\justuse\tests\.tests\.test2.py", line 76, in test_gauss_numba
    g = gauss_numba(10,5, 1)
  File "G:\Python398\lib\site-packages\numba\core\dispatcher.py", line 468, in _compile_for_args
    error_rewrite(e, 'typing')
  File "G:\Python398\lib\site-packages\numba\core\dispatcher.py", line 409, in error_rewrite
    raise e.with_traceback(None)
numba.core.errors.TypingError: Failed in object mode pipeline (step: convert make_function into JIT functions)
[1mCannot capture the non-constant value associated with variable 'b' in a function that will escape.
[1m
File ".test2.py", line 60:[0m
[1m
[1m    def f(x):
[0m    [1m^[0m[0m
[0m

amogorkon avatar Feb 28 '22 07:02 amogorkon

Okay, turns out it is possible to njit the inner functions of those closures, preserving the nice behaviour of the outer functions, but numba can't work with try/except, so this is feasible:

def gauss_numba(c:float, b:float, *, c_m=1):
    """Defined by ae^(-b(x-x0)^2), a gaussian distribution.
    
    Basically a triangular sigmoid function, it comes close to human perception.

    vars
    ----
    c_m (a)
        defines the maximum y-value of the graph
    b
        defines the steepness
    c (x0)
        defines the symmetry center/peak of the graph
    """
    assert 0 < c_m <= 1
    assert 0 < b, "b must be greater than 0"
    max_float = sqrt(sys.float_info.max)

    @njit
    def f(x):
        # instead of try-except OverflowError
        if (x-c) > max_float:
            return 0
        else:
            o = (x - c)**2
        return c_m * exp(-b * o)
    return f

This function is now 3 times as fast as the original one. The original is 3 times as fast as the scikit-fuzzy equivalent.

amogorkon avatar Feb 28 '22 08:02 amogorkon