123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766 |
- # Natural Language Toolkit: First-order Resolution-based Theorem Prover
- #
- # Author: Dan Garrette <dhgarrette@gmail.com>
- #
- # Copyright (C) 2001-2019 NLTK Project
- # URL: <http://nltk.org>
- # For license information, see LICENSE.TXT
- """
- Module for a resolution-based First Order theorem prover.
- """
- from __future__ import print_function, unicode_literals
- import operator
- from collections import defaultdict
- from functools import reduce
- from nltk.sem import skolemize
- from nltk.sem.logic import (
- VariableExpression,
- EqualityExpression,
- ApplicationExpression,
- Expression,
- NegatedExpression,
- Variable,
- AndExpression,
- unique_variable,
- OrExpression,
- is_indvar,
- IndividualVariableExpression,
- Expression,
- )
- from nltk.inference.api import Prover, BaseProverCommand
- from nltk.compat import python_2_unicode_compatible
- class ProverParseError(Exception):
- pass
- class ResolutionProver(Prover):
- ANSWER_KEY = 'ANSWER'
- _assume_false = True
- def _prove(self, goal=None, assumptions=None, verbose=False):
- """
- :param goal: Input expression to prove
- :type goal: sem.Expression
- :param assumptions: Input expressions to use as assumptions in the proof
- :type assumptions: list(sem.Expression)
- """
- if not assumptions:
- assumptions = []
- result = None
- try:
- clauses = []
- if goal:
- clauses.extend(clausify(-goal))
- for a in assumptions:
- clauses.extend(clausify(a))
- result, clauses = self._attempt_proof(clauses)
- if verbose:
- print(ResolutionProverCommand._decorate_clauses(clauses))
- except RuntimeError as e:
- if self._assume_false and str(e).startswith(
- 'maximum recursion depth exceeded'
- ):
- result = False
- clauses = []
- else:
- if verbose:
- print(e)
- else:
- raise e
- return (result, clauses)
- def _attempt_proof(self, clauses):
- # map indices to lists of indices, to store attempted unifications
- tried = defaultdict(list)
- i = 0
- while i < len(clauses):
- if not clauses[i].is_tautology():
- # since we try clauses in order, we should start after the last
- # index tried
- if tried[i]:
- j = tried[i][-1] + 1
- else:
- j = i + 1 # nothing tried yet for 'i', so start with the next
- while j < len(clauses):
- # don't: 1) unify a clause with itself,
- # 2) use tautologies
- if i != j and j and not clauses[j].is_tautology():
- tried[i].append(j)
- newclauses = clauses[i].unify(clauses[j])
- if newclauses:
- for newclause in newclauses:
- newclause._parents = (i + 1, j + 1)
- clauses.append(newclause)
- if not len(newclause): # if there's an empty clause
- return (True, clauses)
- i = -1 # since we added a new clause, restart from the top
- break
- j += 1
- i += 1
- return (False, clauses)
- class ResolutionProverCommand(BaseProverCommand):
- def __init__(self, goal=None, assumptions=None, prover=None):
- """
- :param goal: Input expression to prove
- :type goal: sem.Expression
- :param assumptions: Input expressions to use as assumptions in
- the proof.
- :type assumptions: list(sem.Expression)
- """
- if prover is not None:
- assert isinstance(prover, ResolutionProver)
- else:
- prover = ResolutionProver()
- BaseProverCommand.__init__(self, prover, goal, assumptions)
- self._clauses = None
- def prove(self, verbose=False):
- """
- Perform the actual proof. Store the result to prevent unnecessary
- re-proving.
- """
- if self._result is None:
- self._result, clauses = self._prover._prove(
- self.goal(), self.assumptions(), verbose
- )
- self._clauses = clauses
- self._proof = ResolutionProverCommand._decorate_clauses(clauses)
- return self._result
- def find_answers(self, verbose=False):
- self.prove(verbose)
- answers = set()
- answer_ex = VariableExpression(Variable(ResolutionProver.ANSWER_KEY))
- for clause in self._clauses:
- for term in clause:
- if (
- isinstance(term, ApplicationExpression)
- and term.function == answer_ex
- and not isinstance(term.argument, IndividualVariableExpression)
- ):
- answers.add(term.argument)
- return answers
- @staticmethod
- def _decorate_clauses(clauses):
- """
- Decorate the proof output.
- """
- out = ''
- max_clause_len = max([len(str(clause)) for clause in clauses])
- max_seq_len = len(str(len(clauses)))
- for i in range(len(clauses)):
- parents = 'A'
- taut = ''
- if clauses[i].is_tautology():
- taut = 'Tautology'
- if clauses[i]._parents:
- parents = str(clauses[i]._parents)
- parents = ' ' * (max_clause_len - len(str(clauses[i])) + 1) + parents
- seq = ' ' * (max_seq_len - len(str(i + 1))) + str(i + 1)
- out += '[%s] %s %s %s\n' % (seq, clauses[i], parents, taut)
- return out
- @python_2_unicode_compatible
- class Clause(list):
- def __init__(self, data):
- list.__init__(self, data)
- self._is_tautology = None
- self._parents = None
- def unify(self, other, bindings=None, used=None, skipped=None, debug=False):
- """
- Attempt to unify this Clause with the other, returning a list of
- resulting, unified, Clauses.
- :param other: ``Clause`` with which to unify
- :param bindings: ``BindingDict`` containing bindings that should be used
- during the unification
- :param used: tuple of two lists of atoms. The first lists the
- atoms from 'self' that were successfully unified with atoms from
- 'other'. The second lists the atoms from 'other' that were successfully
- unified with atoms from 'self'.
- :param skipped: tuple of two ``Clause`` objects. The first is a list of all
- the atoms from the 'self' Clause that have not been unified with
- anything on the path. The second is same thing for the 'other' Clause.
- :param debug: bool indicating whether debug statements should print
- :return: list containing all the resulting ``Clause`` objects that could be
- obtained by unification
- """
- if bindings is None:
- bindings = BindingDict()
- if used is None:
- used = ([], [])
- if skipped is None:
- skipped = ([], [])
- if isinstance(debug, bool):
- debug = DebugObject(debug)
- newclauses = _iterate_first(
- self, other, bindings, used, skipped, _complete_unify_path, debug
- )
- # remove subsumed clauses. make a list of all indices of subsumed
- # clauses, and then remove them from the list
- subsumed = []
- for i, c1 in enumerate(newclauses):
- if i not in subsumed:
- for j, c2 in enumerate(newclauses):
- if i != j and j not in subsumed and c1.subsumes(c2):
- subsumed.append(j)
- result = []
- for i in range(len(newclauses)):
- if i not in subsumed:
- result.append(newclauses[i])
- return result
- def isSubsetOf(self, other):
- """
- Return True iff every term in 'self' is a term in 'other'.
- :param other: ``Clause``
- :return: bool
- """
- for a in self:
- if a not in other:
- return False
- return True
- def subsumes(self, other):
- """
- Return True iff 'self' subsumes 'other', this is, if there is a
- substitution such that every term in 'self' can be unified with a term
- in 'other'.
- :param other: ``Clause``
- :return: bool
- """
- negatedother = []
- for atom in other:
- if isinstance(atom, NegatedExpression):
- negatedother.append(atom.term)
- else:
- negatedother.append(-atom)
- negatedotherClause = Clause(negatedother)
- bindings = BindingDict()
- used = ([], [])
- skipped = ([], [])
- debug = DebugObject(False)
- return (
- len(
- _iterate_first(
- self,
- negatedotherClause,
- bindings,
- used,
- skipped,
- _subsumes_finalize,
- debug,
- )
- )
- > 0
- )
- def __getslice__(self, start, end):
- return Clause(list.__getslice__(self, start, end))
- def __sub__(self, other):
- return Clause([a for a in self if a not in other])
- def __add__(self, other):
- return Clause(list.__add__(self, other))
- def is_tautology(self):
- """
- Self is a tautology if it contains ground terms P and -P. The ground
- term, P, must be an exact match, ie, not using unification.
- """
- if self._is_tautology is not None:
- return self._is_tautology
- for i, a in enumerate(self):
- if not isinstance(a, EqualityExpression):
- j = len(self) - 1
- while j > i:
- b = self[j]
- if isinstance(a, NegatedExpression):
- if a.term == b:
- self._is_tautology = True
- return True
- elif isinstance(b, NegatedExpression):
- if a == b.term:
- self._is_tautology = True
- return True
- j -= 1
- self._is_tautology = False
- return False
- def free(self):
- return reduce(operator.or_, ((atom.free() | atom.constants()) for atom in self))
- def replace(self, variable, expression):
- """
- Replace every instance of variable with expression across every atom
- in the clause
- :param variable: ``Variable``
- :param expression: ``Expression``
- """
- return Clause([atom.replace(variable, expression) for atom in self])
- def substitute_bindings(self, bindings):
- """
- Replace every binding
- :param bindings: A list of tuples mapping Variable Expressions to the
- Expressions to which they are bound
- :return: ``Clause``
- """
- return Clause([atom.substitute_bindings(bindings) for atom in self])
- def __str__(self):
- return '{' + ', '.join("%s" % item for item in self) + '}'
- def __repr__(self):
- return "%s" % self
- def _iterate_first(first, second, bindings, used, skipped, finalize_method, debug):
- """
- This method facilitates movement through the terms of 'self'
- """
- debug.line('unify(%s,%s) %s' % (first, second, bindings))
- if not len(first) or not len(second): # if no more recursions can be performed
- return finalize_method(first, second, bindings, used, skipped, debug)
- else:
- # explore this 'self' atom
- result = _iterate_second(
- first, second, bindings, used, skipped, finalize_method, debug + 1
- )
- # skip this possible 'self' atom
- newskipped = (skipped[0] + [first[0]], skipped[1])
- result += _iterate_first(
- first[1:], second, bindings, used, newskipped, finalize_method, debug + 1
- )
- try:
- newbindings, newused, unused = _unify_terms(
- first[0], second[0], bindings, used
- )
- # Unification found, so progress with this line of unification
- # put skipped and unused terms back into play for later unification.
- newfirst = first[1:] + skipped[0] + unused[0]
- newsecond = second[1:] + skipped[1] + unused[1]
- result += _iterate_first(
- newfirst,
- newsecond,
- newbindings,
- newused,
- ([], []),
- finalize_method,
- debug + 1,
- )
- except BindingException:
- # the atoms could not be unified,
- pass
- return result
- def _iterate_second(first, second, bindings, used, skipped, finalize_method, debug):
- """
- This method facilitates movement through the terms of 'other'
- """
- debug.line('unify(%s,%s) %s' % (first, second, bindings))
- if not len(first) or not len(second): # if no more recursions can be performed
- return finalize_method(first, second, bindings, used, skipped, debug)
- else:
- # skip this possible pairing and move to the next
- newskipped = (skipped[0], skipped[1] + [second[0]])
- result = _iterate_second(
- first, second[1:], bindings, used, newskipped, finalize_method, debug + 1
- )
- try:
- newbindings, newused, unused = _unify_terms(
- first[0], second[0], bindings, used
- )
- # Unification found, so progress with this line of unification
- # put skipped and unused terms back into play for later unification.
- newfirst = first[1:] + skipped[0] + unused[0]
- newsecond = second[1:] + skipped[1] + unused[1]
- result += _iterate_second(
- newfirst,
- newsecond,
- newbindings,
- newused,
- ([], []),
- finalize_method,
- debug + 1,
- )
- except BindingException:
- # the atoms could not be unified,
- pass
- return result
- def _unify_terms(a, b, bindings=None, used=None):
- """
- This method attempts to unify two terms. Two expressions are unifiable
- if there exists a substitution function S such that S(a) == S(-b).
- :param a: ``Expression``
- :param b: ``Expression``
- :param bindings: ``BindingDict`` a starting set of bindings with which
- the unification must be consistent
- :return: ``BindingDict`` A dictionary of the bindings required to unify
- :raise ``BindingException``: If the terms cannot be unified
- """
- assert isinstance(a, Expression)
- assert isinstance(b, Expression)
- if bindings is None:
- bindings = BindingDict()
- if used is None:
- used = ([], [])
- # Use resolution
- if isinstance(a, NegatedExpression) and isinstance(b, ApplicationExpression):
- newbindings = most_general_unification(a.term, b, bindings)
- newused = (used[0] + [a], used[1] + [b])
- unused = ([], [])
- elif isinstance(a, ApplicationExpression) and isinstance(b, NegatedExpression):
- newbindings = most_general_unification(a, b.term, bindings)
- newused = (used[0] + [a], used[1] + [b])
- unused = ([], [])
- # Use demodulation
- elif isinstance(a, EqualityExpression):
- newbindings = BindingDict([(a.first.variable, a.second)])
- newused = (used[0] + [a], used[1])
- unused = ([], [b])
- elif isinstance(b, EqualityExpression):
- newbindings = BindingDict([(b.first.variable, b.second)])
- newused = (used[0], used[1] + [b])
- unused = ([a], [])
- else:
- raise BindingException((a, b))
- return newbindings, newused, unused
- def _complete_unify_path(first, second, bindings, used, skipped, debug):
- if used[0] or used[1]: # if bindings were made along the path
- newclause = Clause(skipped[0] + skipped[1] + first + second)
- debug.line(' -> New Clause: %s' % newclause)
- return [newclause.substitute_bindings(bindings)]
- else: # no bindings made means no unification occurred. so no result
- debug.line(' -> End')
- return []
- def _subsumes_finalize(first, second, bindings, used, skipped, debug):
- if not len(skipped[0]) and not len(first):
- # If there are no skipped terms and no terms left in 'first', then
- # all of the terms in the original 'self' were unified with terms
- # in 'other'. Therefore, there exists a binding (this one) such that
- # every term in self can be unified with a term in other, which
- # is the definition of subsumption.
- return [True]
- else:
- return []
- def clausify(expression):
- """
- Skolemize, clausify, and standardize the variables apart.
- """
- clause_list = []
- for clause in _clausify(skolemize(expression)):
- for free in clause.free():
- if is_indvar(free.name):
- newvar = VariableExpression(unique_variable())
- clause = clause.replace(free, newvar)
- clause_list.append(clause)
- return clause_list
- def _clausify(expression):
- """
- :param expression: a skolemized expression in CNF
- """
- if isinstance(expression, AndExpression):
- return _clausify(expression.first) + _clausify(expression.second)
- elif isinstance(expression, OrExpression):
- first = _clausify(expression.first)
- second = _clausify(expression.second)
- assert len(first) == 1
- assert len(second) == 1
- return [first[0] + second[0]]
- elif isinstance(expression, EqualityExpression):
- return [Clause([expression])]
- elif isinstance(expression, ApplicationExpression):
- return [Clause([expression])]
- elif isinstance(expression, NegatedExpression):
- if isinstance(expression.term, ApplicationExpression):
- return [Clause([expression])]
- elif isinstance(expression.term, EqualityExpression):
- return [Clause([expression])]
- raise ProverParseError()
- @python_2_unicode_compatible
- class BindingDict(object):
- def __init__(self, binding_list=None):
- """
- :param binding_list: list of (``AbstractVariableExpression``, ``AtomicExpression``) to initialize the dictionary
- """
- self.d = {}
- if binding_list:
- for (v, b) in binding_list:
- self[v] = b
- def __setitem__(self, variable, binding):
- """
- A binding is consistent with the dict if its variable is not already bound, OR if its
- variable is already bound to its argument.
- :param variable: ``Variable`` The variable to bind
- :param binding: ``Expression`` The atomic to which 'variable' should be bound
- :raise BindingException: If the variable cannot be bound in this dictionary
- """
- assert isinstance(variable, Variable)
- assert isinstance(binding, Expression)
- try:
- existing = self[variable]
- except KeyError:
- existing = None
- if not existing or binding == existing:
- self.d[variable] = binding
- elif isinstance(binding, IndividualVariableExpression):
- # Since variable is already bound, try to bind binding to variable
- try:
- existing = self[binding.variable]
- except KeyError:
- existing = None
- binding2 = VariableExpression(variable)
- if not existing or binding2 == existing:
- self.d[binding.variable] = binding2
- else:
- raise BindingException(
- 'Variable %s already bound to another ' 'value' % (variable)
- )
- else:
- raise BindingException(
- 'Variable %s already bound to another ' 'value' % (variable)
- )
- def __getitem__(self, variable):
- """
- Return the expression to which 'variable' is bound
- """
- assert isinstance(variable, Variable)
- intermediate = self.d[variable]
- while intermediate:
- try:
- intermediate = self.d[intermediate]
- except KeyError:
- return intermediate
- def __contains__(self, item):
- return item in self.d
- def __add__(self, other):
- """
- :param other: ``BindingDict`` The dict with which to combine self
- :return: ``BindingDict`` A new dict containing all the elements of both parameters
- :raise BindingException: If the parameter dictionaries are not consistent with each other
- """
- try:
- combined = BindingDict()
- for v in self.d:
- combined[v] = self.d[v]
- for v in other.d:
- combined[v] = other.d[v]
- return combined
- except BindingException:
- raise BindingException(
- "Attempting to add two contradicting "
- "BindingDicts: '%s' and '%s'" % (self, other)
- )
- def __len__(self):
- return len(self.d)
- def __str__(self):
- data_str = ', '.join('%s: %s' % (v, self.d[v]) for v in sorted(self.d.keys()))
- return '{' + data_str + '}'
- def __repr__(self):
- return "%s" % self
- def most_general_unification(a, b, bindings=None):
- """
- Find the most general unification of the two given expressions
- :param a: ``Expression``
- :param b: ``Expression``
- :param bindings: ``BindingDict`` a starting set of bindings with which the
- unification must be consistent
- :return: a list of bindings
- :raise BindingException: if the Expressions cannot be unified
- """
- if bindings is None:
- bindings = BindingDict()
- if a == b:
- return bindings
- elif isinstance(a, IndividualVariableExpression):
- return _mgu_var(a, b, bindings)
- elif isinstance(b, IndividualVariableExpression):
- return _mgu_var(b, a, bindings)
- elif isinstance(a, ApplicationExpression) and isinstance(b, ApplicationExpression):
- return most_general_unification(
- a.function, b.function, bindings
- ) + most_general_unification(a.argument, b.argument, bindings)
- raise BindingException((a, b))
- def _mgu_var(var, expression, bindings):
- if var.variable in expression.free() | expression.constants():
- raise BindingException((var, expression))
- else:
- return BindingDict([(var.variable, expression)]) + bindings
- class BindingException(Exception):
- def __init__(self, arg):
- if isinstance(arg, tuple):
- Exception.__init__(self, "'%s' cannot be bound to '%s'" % arg)
- else:
- Exception.__init__(self, arg)
- class UnificationException(Exception):
- def __init__(self, a, b):
- Exception.__init__(self, "'%s' cannot unify with '%s'" % (a, b))
- class DebugObject(object):
- def __init__(self, enabled=True, indent=0):
- self.enabled = enabled
- self.indent = indent
- def __add__(self, i):
- return DebugObject(self.enabled, self.indent + i)
- def line(self, line):
- if self.enabled:
- print(' ' * self.indent + line)
- def testResolutionProver():
- resolution_test(r'man(x)')
- resolution_test(r'(man(x) -> man(x))')
- resolution_test(r'(man(x) -> --man(x))')
- resolution_test(r'-(man(x) and -man(x))')
- resolution_test(r'(man(x) or -man(x))')
- resolution_test(r'(man(x) -> man(x))')
- resolution_test(r'-(man(x) and -man(x))')
- resolution_test(r'(man(x) or -man(x))')
- resolution_test(r'(man(x) -> man(x))')
- resolution_test(r'(man(x) iff man(x))')
- resolution_test(r'-(man(x) iff -man(x))')
- resolution_test('all x.man(x)')
- resolution_test('-all x.some y.F(x,y) & some x.all y.(-F(x,y))')
- resolution_test('some x.all y.sees(x,y)')
- p1 = Expression.fromstring(r'all x.(man(x) -> mortal(x))')
- p2 = Expression.fromstring(r'man(Socrates)')
- c = Expression.fromstring(r'mortal(Socrates)')
- print('%s, %s |- %s: %s' % (p1, p2, c, ResolutionProver().prove(c, [p1, p2])))
- p1 = Expression.fromstring(r'all x.(man(x) -> walks(x))')
- p2 = Expression.fromstring(r'man(John)')
- c = Expression.fromstring(r'some y.walks(y)')
- print('%s, %s |- %s: %s' % (p1, p2, c, ResolutionProver().prove(c, [p1, p2])))
- p = Expression.fromstring(r'some e1.some e2.(believe(e1,john,e2) & walk(e2,mary))')
- c = Expression.fromstring(r'some e0.walk(e0,mary)')
- print('%s |- %s: %s' % (p, c, ResolutionProver().prove(c, [p])))
- def resolution_test(e):
- f = Expression.fromstring(e)
- t = ResolutionProver().prove(f)
- print('|- %s: %s' % (f, t))
- def test_clausify():
- lexpr = Expression.fromstring
- print(clausify(lexpr('P(x) | Q(x)')))
- print(clausify(lexpr('(P(x) & Q(x)) | R(x)')))
- print(clausify(lexpr('P(x) | (Q(x) & R(x))')))
- print(clausify(lexpr('(P(x) & Q(x)) | (R(x) & S(x))')))
- print(clausify(lexpr('P(x) | Q(x) | R(x)')))
- print(clausify(lexpr('P(x) | (Q(x) & R(x)) | S(x)')))
- print(clausify(lexpr('exists x.P(x) | Q(x)')))
- print(clausify(lexpr('-(-P(x) & Q(x))')))
- print(clausify(lexpr('P(x) <-> Q(x)')))
- print(clausify(lexpr('-(P(x) <-> Q(x))')))
- print(clausify(lexpr('-(all x.P(x))')))
- print(clausify(lexpr('-(some x.P(x))')))
- print(clausify(lexpr('some x.P(x)')))
- print(clausify(lexpr('some x.all y.P(x,y)')))
- print(clausify(lexpr('all y.some x.P(x,y)')))
- print(clausify(lexpr('all z.all y.some x.P(x,y,z)')))
- print(clausify(lexpr('all x.(all y.P(x,y) -> -all y.(Q(x,y) -> R(x,y)))')))
- def demo():
- test_clausify()
- print()
- testResolutionProver()
- print()
- p = Expression.fromstring('man(x)')
- print(ResolutionProverCommand(p, [p]).prove())
- if __name__ == '__main__':
- demo()
|