From 66cd5991958a436ca62f5b3c0703284f0d38248a Mon Sep 17 00:00:00 2001 From: Ayush Joshi Date: Tue, 14 Nov 2023 11:40:33 +0530 Subject: [PATCH] Added search utility and boolean algebra for representing predicate logic Signed-off-by: Ayush Joshi --- ai/__init__.py | 6 +- ai/boolalg/__init__.py | 37 +++++ ai/boolalg/logic.py | 343 +++++++++++++++++++++++++++++++++++++++++ ai/search/__init__.py | 17 ++ ai/search/frontiers.py | 46 ++++++ 5 files changed, 447 insertions(+), 2 deletions(-) create mode 100644 ai/boolalg/__init__.py create mode 100644 ai/boolalg/logic.py create mode 100644 ai/search/__init__.py create mode 100644 ai/search/frontiers.py diff --git a/ai/__init__.py b/ai/__init__.py index 7831569..47cdb03 100644 --- a/ai/__init__.py +++ b/ai/__init__.py @@ -21,8 +21,11 @@ from bs4 import BeautifulSoup from urllib.parse import urlparse -from .stats import corrcoef +from .stats import (cov, corrcoef) from .mathematical_functions import proportion +from .boolalg import ( + Sentence, Symbol, Not, And, Or, Implication, Biconditional, model_check +) from .neighbors import KNeighborsClassifier from .naive_bayes import GaussianNaiveBayes @@ -76,7 +79,6 @@ def _PreprocessReadme(fpath: Union[str, pathlib.Path]) -> str: 'Introduction-to-ML.md', 'Descending-into-ML.md', 'Reducing-Loss.md', - 'Introduction-to-TensorFlow.md', 'Generalization.md', 'Training-and-Test-Sets.md', 'Validation-Set.md', diff --git a/ai/boolalg/__init__.py b/ai/boolalg/__init__.py new file mode 100644 index 0000000..b283202 --- /dev/null +++ b/ai/boolalg/__init__.py @@ -0,0 +1,37 @@ +# Copyright 2023 The AI Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=too-many-function-args, invalid-name, missing-module-docstring +# pylint: disable=missing-class-docstring + +"""Symbolic Propositional Logic for Knowledge representation. + +>>> from ai import (Symbol, And, Or, Not, Implication) +... +>>> rain = Symbol("rain") +>>> hagrid = Symbol("hagrid") +>>> dumbledore = Symbol("dumbledore") +... +>>> knowledge = And( +>>> Implication(Not(rain), hagrid), +>>> Or(hagrid, dumbledore), +>>> Not(And(hagrid, dumbledore)), +>>> dumbledore +>>> ) +... +>>> print(model_check(knowledge, rain)) +""" + +from .logic import ( + Sentence, Symbol, Not, And, Or, Implication, Biconditional, model_check +) diff --git a/ai/boolalg/logic.py b/ai/boolalg/logic.py new file mode 100644 index 0000000..99540cb --- /dev/null +++ b/ai/boolalg/logic.py @@ -0,0 +1,343 @@ +# Copyright 2023 The AI Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=too-many-function-args, invalid-name, missing-module-docstring +# pylint: disable=missing-class-docstring + +import itertools + + +class Sentence(): + """Parent class implementing abstract methods for constructing symbols. + + .. note:: + + See `Symbol` for more details. + """ + def evaluate(self, model) -> bool: + """Abstract method for evaluating the logical sentence. + + Args: + model: Model to evaluate. + + Raises: + NotImplementedError - Abstract method evaluates nothing. + """ + raise NotImplementedError("nothing to evaluate") + + def formula(self) -> str: + """Returns string formula representing logical sentence. + + Returns: + String formula representation of the logical sentence. + """ + return "" + + def symbols(self) -> str: + """Returns a set of all symbols in the logical sentence. + + Returns: + Set of all symbols in the logical sentence. + """ + return set() + + @classmethod + def validate(cls, sentence: 'Sentence'): + """Validates if the given sentence is actually a `Sentence` instance. + + Raises: + TypeError - In case of a non-logical sentence. + """ + if not isinstance(sentence, Sentence): + raise TypeError("must be a logical sentence") + + @classmethod + def parenthesize(cls, s: str) -> str: + """Parenthesizes an expression if not already parenthesized. + + Args: + s: Expression to parenthesize. + + Returns: + Parenthesized expression. + """ + def balanced(s: str) -> bool: + """Checks if a string has balanced parentheses. + + Args: + s: Expression to check for balanced parentheses. + + Returns: + `True` if expression contains balanced parentheses, `False` otherwise. + """ + count = 0 + for c in s: + if c == "(": + count += 1 + elif c == ")": + if count <= 0: + return False + count -= 1 + return count == 0 + + # Empty or alpha strings are always considered to be balanced. + if not len(s) or s.isalpha( + ) or (s[0] == "(" and s[-1] == ")" and balanced(s[1:-1])): + return s + else: + return f"({s})" + + +class Symbol(Sentence): + def __init__(self, name: str): + """Initializes the name of the `Symbol`. + + Args: + name: `Symbol` name. + """ + self.name = name + + def __eq__(self, other: 'Symbol') -> bool: + """Equates self with the other. + + Args: + other: `Symbol` instance. + + Returns: + `True` if the other symbol equates to self, `False` otherwise. + """ + return isinstance(other, Symbol) and self.name == other.name + + def __hash__(self) -> int: + """Returns the hash of the current `Symbol`. + + Returns: + Hash of the current `Symbol`. + """ + return hash(("symbol", self.name)) + + def __repr__(self): + return self.name + + def evaluate(self, model): + try: + return bool(model[self.name]) + except KeyError: + raise EvaluationException(f"variable {self.name} not in model") + + def formula(self): + return self.name + + def symbols(self): + return {self.name} + + +class Not(Sentence): + def __init__(self, operand): + Sentence.validate(operand) + self.operand = operand + + def __eq__(self, other): + return isinstance(other, Not) and self.operand == other.operand + + def __hash__(self): + return hash(("not", hash(self.operand))) + + def __repr__(self): + return f"Not({self.operand})" + + def evaluate(self, model): + return not self.operand.evaluate(model) + + def formula(self): + return "¬" + Sentence.parenthesize(self.operand.formula()) + + def symbols(self): + return self.operand.symbols() + + +class And(Sentence): + def __init__(self, *conjuncts): + for conjunct in conjuncts: + Sentence.validate(conjunct) + self.conjuncts = list(conjuncts) + + def __eq__(self, other): + return isinstance(other, And) and self.conjuncts == other.conjuncts + + def __hash__(self): + return hash(("and", tuple(hash(conjunct) for conjunct in self.conjuncts))) + + def __repr__(self): + conjunctions = ", ".join([str(conjunct) for conjunct in self.conjuncts]) + return f"And({conjunctions})" + + def add(self, conjunct): + Sentence.validate(conjunct) + self.conjuncts.append(conjunct) + + def evaluate(self, model): + return all(conjunct.evaluate(model) for conjunct in self.conjuncts) + + def formula(self): + if len(self.conjuncts) == 1: + return self.conjuncts[0].formula() + return " ∧ ".join( + [ + Sentence.parenthesize(conjunct.formula()) + for conjunct in self.conjuncts + ] + ) + + def symbols(self): + return set.union(*[conjunct.symbols() for conjunct in self.conjuncts]) + + +class Or(Sentence): + def __init__(self, *disjuncts): + for disjunct in disjuncts: + Sentence.validate(disjunct) + self.disjuncts = list(disjuncts) + + def __eq__(self, other): + return isinstance(other, Or) and self.disjuncts == other.disjuncts + + def __hash__(self): + return hash(("or", tuple(hash(disjunct) for disjunct in self.disjuncts))) + + def __repr__(self): + disjuncts = ", ".join([str(disjunct) for disjunct in self.disjuncts]) + return f"Or({disjuncts})" + + def evaluate(self, model): + return any(disjunct.evaluate(model) for disjunct in self.disjuncts) + + def formula(self): + if len(self.disjuncts) == 1: + return self.disjuncts[0].formula() + return " ∨ ".join( + [ + Sentence.parenthesize(disjunct.formula()) + for disjunct in self.disjuncts + ] + ) + + def symbols(self): + return set.union(*[disjunct.symbols() for disjunct in self.disjuncts]) + + +class Implication(Sentence): + def __init__(self, antecedent, consequent): + Sentence.validate(antecedent) + Sentence.validate(consequent) + self.antecedent = antecedent + self.consequent = consequent + + def __eq__(self, other): + return ( + isinstance(other, Implication) and + self.antecedent == other.antecedent and + self.consequent == other.consequent + ) + + def __hash__(self): + return hash(("implies", hash(self.antecedent), hash(self.consequent))) + + def __repr__(self): + return f"Implication({self.antecedent}, {self.consequent})" + + def evaluate(self, model): + return ( + (not self.antecedent.evaluate(model)) or self.consequent.evaluate(model) + ) + + def formula(self): + antecedent = Sentence.parenthesize(self.antecedent.formula()) + consequent = Sentence.parenthesize(self.consequent.formula()) + return f"{antecedent} => {consequent}" + + def symbols(self): + return set.union(self.antecedent.symbols(), self.consequent.symbols()) + + +class Biconditional(Sentence): + def __init__(self, left, right): + Sentence.validate(left) + Sentence.validate(right) + self.left = left + self.right = right + + def __eq__(self, other): + return ( + isinstance(other, Biconditional) and self.left == other.left and + self.right == other.right + ) + + def __hash__(self): + return hash(("biconditional", hash(self.left), hash(self.right))) + + def __repr__(self): + return f"Biconditional({self.left}, {self.right})" + + def evaluate(self, model): + return ( + (self.left.evaluate(model) and self.right.evaluate(model)) or + (not self.left.evaluate(model) and not self.right.evaluate(model)) + ) + + def formula(self): + left = Sentence.parenthesize(str(self.left)) + right = Sentence.parenthesize(str(self.right)) + return f"{left} <=> {right}" + + def symbols(self): + return set.union(self.left.symbols(), self.right.symbols()) + + +def model_check(knowledge, query): + """Checks if knowledge base entails query.""" + def check_all(knowledge, query, symbols, model): + """Checks if knowledge base entails query, given a particular model.""" + + # If model has an assignment for each symbol + if not symbols: + + # If knowledge base is true in model, then query must also be true + if knowledge.evaluate(model): + return query.evaluate(model) + return True + else: + + # Choose one of the remaining unused symbols + remaining = symbols.copy() + p = remaining.pop() + + # Create a model where the symbol is true + model_true = model.copy() + model_true[p] = True + + # Create a model where the symbol is false + model_false = model.copy() + model_false[p] = False + + # Ensure entailment holds in both models + return ( + check_all(knowledge, query, remaining, model_true) and + check_all(knowledge, query, remaining, model_false) + ) + + # Get all symbols in both knowledge and query + symbols = set.union(knowledge.symbols(), query.symbols()) + + # Check that knowledge entails query + return check_all(knowledge, query, symbols, dict()) diff --git a/ai/search/__init__.py b/ai/search/__init__.py new file mode 100644 index 0000000..f0146da --- /dev/null +++ b/ai/search/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2023 The AI Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=too-many-function-args, invalid-name, missing-module-docstring +# pylint: disable=missing-class-docstring + +from .frontiers import (StackFrontier, QueueFrontier) diff --git a/ai/search/frontiers.py b/ai/search/frontiers.py new file mode 100644 index 0000000..6b84165 --- /dev/null +++ b/ai/search/frontiers.py @@ -0,0 +1,46 @@ +# Copyright 2023 The AI Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=too-many-function-args, invalid-name, missing-module-docstring +# pylint: disable=missing-class-docstring + + +class Frontier(): + def __init__(self): + self.frontier = [] + + def add(self, node): + self.frontier.append(node) + + def empty(self): + return len(self.frontier) == 0 + + +class StackFrontier(Frontier): + def remove(self): + if self.empty(): + raise Exception("empty frontier") + else: + node = self.frontier[-1] + self.frontier = self.frontier[:-1] + return node + + +class QueueFrontier(Frontier): + def remove(self): + if self.empty(): + raise Exception("empty frontier") + else: + node = self.frontier[0] + self.frontier = self.frontier[1:] + return node