Packages

A toolkit for fuzzy logic, this library includes functions for creating fuzzy variables, sets, rules to create a Fuzzy Logic System (FLS).

Current section

Files

Jump to
flex lib system.ex
Raw

lib/system.ex

defmodule Flex.System do
@moduledoc """
An interface to create a Fuzzy Logic Control System (FLS).
The Fuzzy controllers are very simple conceptually. They consist of an input stage (fuzzification), a processing stage (inference), and an output stage (defuzzification).
"""
use GenServer
require Logger
alias Flex.EngineAdapter
alias Flex.EngineAdapter.{ANFIS, Mamdani, TakagiSugeno}
defmodule State do
@moduledoc false
defstruct rules: nil,
antecedent: nil,
consequent: nil,
engine_type: Mamdani,
engine_output: %EngineAdapter.State{},
sets_in_rules: [],
learning_rate: 0.05,
initial_gamma: 1000
end
@typedoc """
Fuzzy Logic System state.
- `:rules` - (list) A list of rules that defines the behavior of the Fuzzy logic systems.
- `:consequent` - Output variable.
- `:antecedent` - a list of the input variables.
- `:engine_type` - defines the inference engine behavior (default: Mamdini).
- `:sets_in_rules` - list of sets involve in the rules (optional, required by ANFIS).
- `:learning_rate` - is the speed at which the system parameters are adjusted (ANFIS only).
- `:initial_gamma` - is the speed at which the system parameters are adjusted (LSE, ANFIS only).
"""
@type t :: %Flex.System.State{
rules: [Flex.Rule.t(), ...],
antecedent: [Flex.Variable.t(), ...],
consequent: Flex.Variable.t(),
engine_type: Mamdani | TakagiSugeno | ANFIS,
engine_output: EngineAdapter.engine_state(),
sets_in_rules: list(),
learning_rate: number(),
initial_gamma: number()
}
@doc """
Spawns a Fuzzy Logic System.
The following options are require:
- `:rules` - Defines the behavior of the system based on a list of rules.
- `:antecedent` - (list) Defines the input variables.
- `:consequent` - Defines the output variable.
"""
def start_link(params, opt \\ []) do
GenServer.start_link(__MODULE__, params, opt)
end
def stop(pid) do
GenServer.stop(pid)
end
@doc """
Computes the Fuzzy Logic System output for a given input vector.
"""
@spec compute(atom | pid | {atom, any} | {:via, atom, any}, list) :: any
def compute(pid, input_vector) when is_list(input_vector) do
GenServer.call(pid, {:compute, input_vector})
end
@doc """
Adjust the consequent free parameters of the FIS (only avaliable with ANFIS engine), using the following methods:
- Learning method: Steepest gradient Backpropagation.
- Energy function: 0.5 * (target - output)^2
"""
@spec forward_pass(atom | pid | {atom, any} | {:via, atom, any}, number()) ::
{:ok, number()} | {:error, :einval}
def forward_pass(pid, desired_output) when is_number(desired_output) do
GenServer.call(pid, {:forward_pass, desired_output})
end
@doc """
Adjust the premise free parameters of the FIS (only avaliable with ANFIS engine), using the following methods:
- Learning method: Steepest gradient Backpropagation.
- Energy function: 0.5 * (target - output)^2
"""
@spec backward_pass(atom | pid | {atom, any} | {:via, atom, any}, number()) ::
{:ok, number()} | {:error, :einval}
def backward_pass(pid, desired_output) when is_number(desired_output) do
GenServer.call(pid, {:backward_pass, desired_output})
end
@doc """
Adjust the free parameters of the FIS (only avaliable with ANFIS engine), using the following methods:
- Learning method: Steepest gradient Backpropagation.
- Energy function: 0.5 * (target - output)^2
Note: this functions fires both forward and backward passes.
"""
@spec hybrid_online_learning(atom | pid | {atom, any} | {:via, atom, any}, number()) ::
{:ok, number()} | {:error, :einval}
def hybrid_online_learning(pid, desired_output) when is_number(desired_output) do
GenServer.call(pid, {:hybrid_online_learning, desired_output})
end
@doc """
Adjust the free parameters of the FIS (only avaliable with ANFIS engine), using the following methods:
- Forward method: Least Square Estimate.
- Learning method: Steepest gradient Backpropagation.
- Energy function: 0.5 * (target - output)^2
Note: this functions fires both forward and backward passes with a batch of data.
"""
@spec hybrid_offline_learning(
atom | pid | {atom, any} | {:via, atom, any},
list(),
list(),
number()
) ::
{:ok, number()} | {:error, :einval}
def hybrid_offline_learning(pid, inputs, targets, epochs)
when is_list(inputs) and is_list(targets) and is_number(epochs) do
GenServer.call(pid, {:hybrid_offline_learning, inputs, targets, epochs}, :infinity)
end
@doc """
Sets the Inference Engine type.
"""
@spec set_engine_type(atom | pid | {atom, any} | {:via, atom, any}, atom) ::
:ok | {:error, :einval}
def set_engine_type(pid, type) when type in [Mamdani, TakagiSugeno, ANFIS] do
GenServer.call(pid, {:set_engine_type, type})
end
def set_engine_type(_pid, _type), do: {:error, :einval}
@doc """
Sets the Learning rate (etha).
"""
@spec set_learning_rate(atom | pid | {atom, any} | {:via, atom, any}, number()) ::
:ok | {:error, :einval}
def set_learning_rate(pid, learning_rate) when is_number(learning_rate) do
GenServer.call(pid, {:set_learning_rate, learning_rate})
end
@doc """
Gets the current system state.
"""
@spec get_state(atom | pid | {atom, any} | {:via, atom, any}) :: Flex.System.t()
def get_state(pid) do
GenServer.call(pid, :get_state)
end
def init(params) do
rules = Keyword.fetch!(params, :rules)
antecedent = Keyword.fetch!(params, :antecedent)
consequent = Keyword.fetch!(params, :consequent)
engine_type = Keyword.get(params, :engine_type, Mamdani)
learning_rate = Keyword.get(params, :learning_rate, 0.05)
initial_gamma = Keyword.get(params, :initial_gamma, 1000)
sets_in_rules = Keyword.get(params, :sets_in_rules, [])
{:ok,
%State{
rules: rules,
antecedent: antecedent,
consequent: consequent,
engine_type: engine_type,
learning_rate: learning_rate,
sets_in_rules: sets_in_rules,
initial_gamma: initial_gamma
}}
end
def handle_call({:compute, input_vector}, _from, state) do
output = compute_fis(input_vector, state)
{:reply, output.crisp_output, %{state | engine_output: output}}
end
def handle_call(
{:forward_pass, target},
_from,
%{engine_type: engine_type, engine_output: engine_output} = state
)
when engine_type == ANFIS do
de_do5 = -(target - engine_output.crisp_output)
consequent = ANFIS.forward_pass(de_do5, state.learning_rate, engine_output)
{:reply, {:ok, de_do5}, %{state | consequent: consequent}}
end
def handle_call(
{:backward_pass, target},
_from,
%{engine_type: engine_type, engine_output: engine_output} = state
)
when engine_type == ANFIS do
de_do5 = -(target - engine_output.crisp_output)
antecedent = ANFIS.backward_pass(de_do5, state, engine_output)
{:reply, {:ok, de_do5}, %{state | antecedent: antecedent}}
end
def handle_call(
{:hybrid_online_learning, target},
_from,
%{engine_type: engine_type, engine_output: engine_output} = state
)
when engine_type == ANFIS do
de_do5 = -(target - engine_output.crisp_output)
consequent = ANFIS.forward_pass(de_do5, state.learning_rate, engine_output)
antecedent = ANFIS.backward_pass(de_do5, state, engine_output)
{:reply, {:ok, de_do5}, %{state | consequent: consequent, antecedent: antecedent}}
end
def handle_call(
{:hybrid_offline_learning, inputs, b_matrix, epochs},
_from,
%{
engine_type: engine_type,
initial_gamma: initial_gamma,
antecedent: antecedent,
consequent: consequent
} = state
)
when engine_type == ANFIS do
{antecedent, consequent} =
for _epoch <- 1..epochs, reduce: {antecedent, consequent} do
{antecedent, consequent} ->
a_matrix =
build_matrix_a(inputs, %{state | antecedent: antecedent, consequent: consequent})
consequent = ANFIS.least_square_estimate(a_matrix, b_matrix, initial_gamma, state)
antecedent =
for {input_vector, target} <- Enum.zip(inputs, b_matrix), reduce: antecedent do
antecedent ->
back_learning_state = %{state | antecedent: antecedent, consequent: consequent}
prediction = compute_fis(input_vector, back_learning_state)
de_do5 = -(target - prediction.crisp_output)
ANFIS.backward_pass(de_do5, back_learning_state, prediction)
end
{antecedent, consequent}
end
{:reply, :ok, %{state | antecedent: antecedent, consequent: consequent}}
end
def handle_call({:set_engine_type, type}, _from, state) do
{:reply, :ok, %{state | engine_type: type}}
end
def handle_call({:set_learning_rate, learning_rate}, _from, %{engine_type: engine_type} = state)
when engine_type == ANFIS do
{:reply, :ok, %{state | learning_rate: learning_rate}}
end
def handle_call({:set_learning_rate, _learning_rate}, _from, state),
do: {:reply, {:error, :einval}, state}
def handle_call(:get_state, _from, state),
do: {:reply, {:ok, state}, state}
# Catch invalid calls
def handle_call({_call, _target}, _from, state),
do: {:reply, {:error, :einval}, state}
defp compute_fis(input_vector, %{engine_type: engine_type} = state) do
%EngineAdapter.State{input_vector: input_vector, type: engine_type}
|> EngineAdapter.validation(state.antecedent, state.rules, state.consequent)
|> EngineAdapter.fuzzification(state.antecedent)
|> EngineAdapter.inference(state.rules, state.consequent)
|> EngineAdapter.defuzzification()
end
defp build_matrix_a(inputs, state) do
inputs
|> Enum.map(fn input_vector ->
output = compute_fis(input_vector, state)
w_n = get_wn(output.fuzzy_consequent)
build_vector_at(w_n, input_vector)
end)
end
defp get_wn(fuzzy_consequent) do
w =
fuzzy_consequent.fuzzy_sets
|> Enum.reduce([], fn output_fuzzy_set, acc ->
acc ++ [fuzzy_consequent.mf_values[output_fuzzy_set.tag]]
end)
|> List.flatten()
ws = Enum.sum(w)
Enum.map(w, fn w_i -> w_i / ws end)
end
defp build_vector_at(w_n, input_vector) do
Enum.reduce(w_n, [], fn w_n_i, acc ->
acc ++ Enum.map(input_vector ++ [1], fn x_i -> x_i * w_n_i end)
end)
end
end