m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

534 lines
15 KiB

6 months ago
  1. r"""This is rule-based deduction system for SymPy
  2. The whole thing is split into two parts
  3. - rules compilation and preparation of tables
  4. - runtime inference
  5. For rule-based inference engines, the classical work is RETE algorithm [1],
  6. [2] Although we are not implementing it in full (or even significantly)
  7. it's still worth a read to understand the underlying ideas.
  8. In short, every rule in a system of rules is one of two forms:
  9. - atom -> ... (alpha rule)
  10. - And(atom1, atom2, ...) -> ... (beta rule)
  11. The major complexity is in efficient beta-rules processing and usually for an
  12. expert system a lot of effort goes into code that operates on beta-rules.
  13. Here we take minimalistic approach to get something usable first.
  14. - (preparation) of alpha- and beta- networks, everything except
  15. - (runtime) FactRules.deduce_all_facts
  16. _____________________________________
  17. ( Kirr: I've never thought that doing )
  18. ( logic stuff is that difficult... )
  19. -------------------------------------
  20. o ^__^
  21. o (oo)\_______
  22. (__)\ )\/\
  23. ||----w |
  24. || ||
  25. Some references on the topic
  26. ----------------------------
  27. [1] https://en.wikipedia.org/wiki/Rete_algorithm
  28. [2] http://reports-archive.adm.cs.cmu.edu/anon/1995/CMU-CS-95-113.pdf
  29. https://en.wikipedia.org/wiki/Propositional_formula
  30. https://en.wikipedia.org/wiki/Inference_rule
  31. https://en.wikipedia.org/wiki/List_of_rules_of_inference
  32. """
  33. from collections import defaultdict
  34. from .logic import Logic, And, Or, Not
  35. def _base_fact(atom):
  36. """Return the literal fact of an atom.
  37. Effectively, this merely strips the Not around a fact.
  38. """
  39. if isinstance(atom, Not):
  40. return atom.arg
  41. else:
  42. return atom
  43. def _as_pair(atom):
  44. if isinstance(atom, Not):
  45. return (atom.arg, False)
  46. else:
  47. return (atom, True)
  48. # XXX this prepares forward-chaining rules for alpha-network
  49. def transitive_closure(implications):
  50. """
  51. Computes the transitive closure of a list of implications
  52. Uses Warshall's algorithm, as described at
  53. http://www.cs.hope.edu/~cusack/Notes/Notes/DiscreteMath/Warshall.pdf.
  54. """
  55. full_implications = set(implications)
  56. literals = set().union(*map(set, full_implications))
  57. for k in literals:
  58. for i in literals:
  59. if (i, k) in full_implications:
  60. for j in literals:
  61. if (k, j) in full_implications:
  62. full_implications.add((i, j))
  63. return full_implications
  64. def deduce_alpha_implications(implications):
  65. """deduce all implications
  66. Description by example
  67. ----------------------
  68. given set of logic rules:
  69. a -> b
  70. b -> c
  71. we deduce all possible rules:
  72. a -> b, c
  73. b -> c
  74. implications: [] of (a,b)
  75. return: {} of a -> set([b, c, ...])
  76. """
  77. implications = implications + [(Not(j), Not(i)) for (i, j) in implications]
  78. res = defaultdict(set)
  79. full_implications = transitive_closure(implications)
  80. for a, b in full_implications:
  81. if a == b:
  82. continue # skip a->a cyclic input
  83. res[a].add(b)
  84. # Clean up tautologies and check consistency
  85. for a, impl in res.items():
  86. impl.discard(a)
  87. na = Not(a)
  88. if na in impl:
  89. raise ValueError(
  90. 'implications are inconsistent: %s -> %s %s' % (a, na, impl))
  91. return res
  92. def apply_beta_to_alpha_route(alpha_implications, beta_rules):
  93. """apply additional beta-rules (And conditions) to already-built
  94. alpha implication tables
  95. TODO: write about
  96. - static extension of alpha-chains
  97. - attaching refs to beta-nodes to alpha chains
  98. e.g.
  99. alpha_implications:
  100. a -> [b, !c, d]
  101. b -> [d]
  102. ...
  103. beta_rules:
  104. &(b,d) -> e
  105. then we'll extend a's rule to the following
  106. a -> [b, !c, d, e]
  107. """
  108. x_impl = {}
  109. for x in alpha_implications.keys():
  110. x_impl[x] = (set(alpha_implications[x]), [])
  111. for bcond, bimpl in beta_rules:
  112. for bk in bcond.args:
  113. if bk in x_impl:
  114. continue
  115. x_impl[bk] = (set(), [])
  116. # static extensions to alpha rules:
  117. # A: x -> a,b B: &(a,b) -> c ==> A: x -> a,b,c
  118. seen_static_extension = True
  119. while seen_static_extension:
  120. seen_static_extension = False
  121. for bcond, bimpl in beta_rules:
  122. if not isinstance(bcond, And):
  123. raise TypeError("Cond is not And")
  124. bargs = set(bcond.args)
  125. for x, (ximpls, bb) in x_impl.items():
  126. x_all = ximpls | {x}
  127. # A: ... -> a B: &(...) -> a is non-informative
  128. if bimpl not in x_all and bargs.issubset(x_all):
  129. ximpls.add(bimpl)
  130. # we introduced new implication - now we have to restore
  131. # completeness of the whole set.
  132. bimpl_impl = x_impl.get(bimpl)
  133. if bimpl_impl is not None:
  134. ximpls |= bimpl_impl[0]
  135. seen_static_extension = True
  136. # attach beta-nodes which can be possibly triggered by an alpha-chain
  137. for bidx, (bcond, bimpl) in enumerate(beta_rules):
  138. bargs = set(bcond.args)
  139. for x, (ximpls, bb) in x_impl.items():
  140. x_all = ximpls | {x}
  141. # A: ... -> a B: &(...) -> a (non-informative)
  142. if bimpl in x_all:
  143. continue
  144. # A: x -> a... B: &(!a,...) -> ... (will never trigger)
  145. # A: x -> a... B: &(...) -> !a (will never trigger)
  146. if any(Not(xi) in bargs or Not(xi) == bimpl for xi in x_all):
  147. continue
  148. if bargs & x_all:
  149. bb.append(bidx)
  150. return x_impl
  151. def rules_2prereq(rules):
  152. """build prerequisites table from rules
  153. Description by example
  154. ----------------------
  155. given set of logic rules:
  156. a -> b, c
  157. b -> c
  158. we build prerequisites (from what points something can be deduced):
  159. b <- a
  160. c <- a, b
  161. rules: {} of a -> [b, c, ...]
  162. return: {} of c <- [a, b, ...]
  163. Note however, that this prerequisites may be *not* enough to prove a
  164. fact. An example is 'a -> b' rule, where prereq(a) is b, and prereq(b)
  165. is a. That's because a=T -> b=T, and b=F -> a=F, but a=F -> b=?
  166. """
  167. prereq = defaultdict(set)
  168. for (a, _), impl in rules.items():
  169. if isinstance(a, Not):
  170. a = a.args[0]
  171. for (i, _) in impl:
  172. if isinstance(i, Not):
  173. i = i.args[0]
  174. prereq[i].add(a)
  175. return prereq
  176. ################
  177. # RULES PROVER #
  178. ################
  179. class TautologyDetected(Exception):
  180. """(internal) Prover uses it for reporting detected tautology"""
  181. pass
  182. class Prover:
  183. """ai - prover of logic rules
  184. given a set of initial rules, Prover tries to prove all possible rules
  185. which follow from given premises.
  186. As a result proved_rules are always either in one of two forms: alpha or
  187. beta:
  188. Alpha rules
  189. -----------
  190. This are rules of the form::
  191. a -> b & c & d & ...
  192. Beta rules
  193. ----------
  194. This are rules of the form::
  195. &(a,b,...) -> c & d & ...
  196. i.e. beta rules are join conditions that say that something follows when
  197. *several* facts are true at the same time.
  198. """
  199. def __init__(self):
  200. self.proved_rules = []
  201. self._rules_seen = set()
  202. def split_alpha_beta(self):
  203. """split proved rules into alpha and beta chains"""
  204. rules_alpha = [] # a -> b
  205. rules_beta = [] # &(...) -> b
  206. for a, b in self.proved_rules:
  207. if isinstance(a, And):
  208. rules_beta.append((a, b))
  209. else:
  210. rules_alpha.append((a, b))
  211. return rules_alpha, rules_beta
  212. @property
  213. def rules_alpha(self):
  214. return self.split_alpha_beta()[0]
  215. @property
  216. def rules_beta(self):
  217. return self.split_alpha_beta()[1]
  218. def process_rule(self, a, b):
  219. """process a -> b rule""" # TODO write more?
  220. if (not a) or isinstance(b, bool):
  221. return
  222. if isinstance(a, bool):
  223. return
  224. if (a, b) in self._rules_seen:
  225. return
  226. else:
  227. self._rules_seen.add((a, b))
  228. # this is the core of processing
  229. try:
  230. self._process_rule(a, b)
  231. except TautologyDetected:
  232. pass
  233. def _process_rule(self, a, b):
  234. # right part first
  235. # a -> b & c --> a -> b ; a -> c
  236. # (?) FIXME this is only correct when b & c != null !
  237. if isinstance(b, And):
  238. for barg in b.args:
  239. self.process_rule(a, barg)
  240. # a -> b | c --> !b & !c -> !a
  241. # --> a & !b -> c
  242. # --> a & !c -> b
  243. elif isinstance(b, Or):
  244. # detect tautology first
  245. if not isinstance(a, Logic): # Atom
  246. # tautology: a -> a|c|...
  247. if a in b.args:
  248. raise TautologyDetected(a, b, 'a -> a|c|...')
  249. self.process_rule(And(*[Not(barg) for barg in b.args]), Not(a))
  250. for bidx in range(len(b.args)):
  251. barg = b.args[bidx]
  252. brest = b.args[:bidx] + b.args[bidx + 1:]
  253. self.process_rule(And(a, Not(barg)), Or(*brest))
  254. # left part
  255. # a & b -> c --> IRREDUCIBLE CASE -- WE STORE IT AS IS
  256. # (this will be the basis of beta-network)
  257. elif isinstance(a, And):
  258. if b in a.args:
  259. raise TautologyDetected(a, b, 'a & b -> a')
  260. self.proved_rules.append((a, b))
  261. # XXX NOTE at present we ignore !c -> !a | !b
  262. elif isinstance(a, Or):
  263. if b in a.args:
  264. raise TautologyDetected(a, b, 'a | b -> a')
  265. for aarg in a.args:
  266. self.process_rule(aarg, b)
  267. else:
  268. # both `a` and `b` are atoms
  269. self.proved_rules.append((a, b)) # a -> b
  270. self.proved_rules.append((Not(b), Not(a))) # !b -> !a
  271. ########################################
  272. class FactRules:
  273. """Rules that describe how to deduce facts in logic space
  274. When defined, these rules allow implications to quickly be determined
  275. for a set of facts. For this precomputed deduction tables are used.
  276. see `deduce_all_facts` (forward-chaining)
  277. Also it is possible to gather prerequisites for a fact, which is tried
  278. to be proven. (backward-chaining)
  279. Definition Syntax
  280. -----------------
  281. a -> b -- a=T -> b=T (and automatically b=F -> a=F)
  282. a -> !b -- a=T -> b=F
  283. a == b -- a -> b & b -> a
  284. a -> b & c -- a=T -> b=T & c=T
  285. # TODO b | c
  286. Internals
  287. ---------
  288. .full_implications[k, v]: all the implications of fact k=v
  289. .beta_triggers[k, v]: beta rules that might be triggered when k=v
  290. .prereq -- {} k <- [] of k's prerequisites
  291. .defined_facts -- set of defined fact names
  292. """
  293. def __init__(self, rules):
  294. """Compile rules into internal lookup tables"""
  295. if isinstance(rules, str):
  296. rules = rules.splitlines()
  297. # --- parse and process rules ---
  298. P = Prover()
  299. for rule in rules:
  300. # XXX `a` is hardcoded to be always atom
  301. a, op, b = rule.split(None, 2)
  302. a = Logic.fromstring(a)
  303. b = Logic.fromstring(b)
  304. if op == '->':
  305. P.process_rule(a, b)
  306. elif op == '==':
  307. P.process_rule(a, b)
  308. P.process_rule(b, a)
  309. else:
  310. raise ValueError('unknown op %r' % op)
  311. # --- build deduction networks ---
  312. self.beta_rules = []
  313. for bcond, bimpl in P.rules_beta:
  314. self.beta_rules.append(
  315. ({_as_pair(a) for a in bcond.args}, _as_pair(bimpl)))
  316. # deduce alpha implications
  317. impl_a = deduce_alpha_implications(P.rules_alpha)
  318. # now:
  319. # - apply beta rules to alpha chains (static extension), and
  320. # - further associate beta rules to alpha chain (for inference
  321. # at runtime)
  322. impl_ab = apply_beta_to_alpha_route(impl_a, P.rules_beta)
  323. # extract defined fact names
  324. self.defined_facts = {_base_fact(k) for k in impl_ab.keys()}
  325. # build rels (forward chains)
  326. full_implications = defaultdict(set)
  327. beta_triggers = defaultdict(set)
  328. for k, (impl, betaidxs) in impl_ab.items():
  329. full_implications[_as_pair(k)] = {_as_pair(i) for i in impl}
  330. beta_triggers[_as_pair(k)] = betaidxs
  331. self.full_implications = full_implications
  332. self.beta_triggers = beta_triggers
  333. # build prereq (backward chains)
  334. prereq = defaultdict(set)
  335. rel_prereq = rules_2prereq(full_implications)
  336. for k, pitems in rel_prereq.items():
  337. prereq[k] |= pitems
  338. self.prereq = prereq
  339. class InconsistentAssumptions(ValueError):
  340. def __str__(self):
  341. kb, fact, value = self.args
  342. return "%s, %s=%s" % (kb, fact, value)
  343. class FactKB(dict):
  344. """
  345. A simple propositional knowledge base relying on compiled inference rules.
  346. """
  347. def __str__(self):
  348. return '{\n%s}' % ',\n'.join(
  349. ["\t%s: %s" % i for i in sorted(self.items())])
  350. def __init__(self, rules):
  351. self.rules = rules
  352. def _tell(self, k, v):
  353. """Add fact k=v to the knowledge base.
  354. Returns True if the KB has actually been updated, False otherwise.
  355. """
  356. if k in self and self[k] is not None:
  357. if self[k] == v:
  358. return False
  359. else:
  360. raise InconsistentAssumptions(self, k, v)
  361. else:
  362. self[k] = v
  363. return True
  364. # *********************************************
  365. # * This is the workhorse, so keep it *fast*. *
  366. # *********************************************
  367. def deduce_all_facts(self, facts):
  368. """
  369. Update the KB with all the implications of a list of facts.
  370. Facts can be specified as a dictionary or as a list of (key, value)
  371. pairs.
  372. """
  373. # keep frequently used attributes locally, so we'll avoid extra
  374. # attribute access overhead
  375. full_implications = self.rules.full_implications
  376. beta_triggers = self.rules.beta_triggers
  377. beta_rules = self.rules.beta_rules
  378. if isinstance(facts, dict):
  379. facts = facts.items()
  380. while facts:
  381. beta_maytrigger = set()
  382. # --- alpha chains ---
  383. for k, v in facts:
  384. if not self._tell(k, v) or v is None:
  385. continue
  386. # lookup routing tables
  387. for key, value in full_implications[k, v]:
  388. self._tell(key, value)
  389. beta_maytrigger.update(beta_triggers[k, v])
  390. # --- beta chains ---
  391. facts = []
  392. for bidx in beta_maytrigger:
  393. bcond, bimpl = beta_rules[bidx]
  394. if all(self.get(k) is v for k, v in bcond):
  395. facts.append(bimpl)