-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Kanren2 #17
[WIP] Kanren2 #17
Conversation
l = components[0] | ||
r = components[1] | ||
l = set(l.terms.terms if type(l) is Terms else l.terms) | ||
r = set(r.terms.terms if type(r) is Terms else r.terms) | ||
diff = l.difference(r) | ||
if l == diff: | ||
difference = None | ||
else: | ||
terms = list(diff) | ||
if len(terms) == 0: | ||
difference = None # TODO: what is a better way to handle this? | ||
elif len(terms) == 1: | ||
difference = terms[0] | ||
else: | ||
difference = Compound(components[0].connector, *list(diff)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a built-in difference operation of Compound
. You can simply write, for example,
difference = components[0] - components[1]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, that's good to know. I figured you already had something like that.
There is an edge case that i found though, (&&, A, B) - (&&, B, C)
produces A
but what we want for conditional rule is for it to return False I believe. If that's correct, is there another method that would allow us to know if the right part is fully contained in the left?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh i think i found it, there's contains
method on the Compound which should do the trick!
difference = -1 # result of applying diff | ||
|
||
if type(c) is Statement: | ||
if type(c.subject) is Compound and c.copula is Copula.Implication: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a variable indicating the type of a term.
c.subject.is_compound
is better than type(c.subject) is Compound
.
The former one is more efficient (though we don't much care about runtime efficiency in python)
|
||
prefix = '_rule_' | ||
|
||
def logic(term, rule=False): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to add a "type hint" when defining a function.
For example,
def logic(term: Term, rule=False):
prefix = '_rule_' | ||
|
||
def logic(term, rule=False): | ||
if term.type is TermType.ATOM: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
term.is_atom
is preferable. The same things in the following.
return Compound(con, *terms) | ||
return logic # atom or cons | ||
|
||
def toList(pair) -> list: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we shall make a convention on the naming rule of function, class, and variable. I prefer to use the name to_list
here, but that's ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah... that's my Swift experience showing :) I'll keep it in mind that in Python underscore is more common.
|
||
from unification import unify, reify | ||
|
||
def variable_elimination(t1: Term, t2: Term) -> list: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bowen-xu I didn't do extensive testing but seems to work in the few test cases we looked at during our call
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least it works in these cases.
|
||
from unification import unify, reify | ||
|
||
def variable_elimination(t1: Term, t2: Term) -> list: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least it works in these cases.
substitution.append(d) | ||
result = [] | ||
for s in substitution: | ||
reified = reify(logic(t1), s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This answers my question. reify
can be used for substitution.
I can see a long way to go, but it's promising. The good news is that in the current structure of the project, the logic part and the control part are seperated, thus it's ok to explore the implementation of each of them. I believe we can get an elegant implementation of inference engine by uisng miniKanran. |
I think there are two steps to enhance the inference engine. The first is to implement all of the rules from NAL-1 to NAL-9. The second is to construct the rules only once when initializing the program (rather than construct them when used). |
The good news is that there's no need to implement the rules, only add their Narsese signatures to the array ;) |
Yeah, that's what I mean by "implement all the rules", sorry for my inaccuracy. We don't need to explicitly implement certain functions to do the transformation, but just describe them in the array. |
Tests/utils_for_test.py
Outdated
nars = Reasoner(100, 100) | ||
engine: GeneralEngine = nars.inference | ||
|
||
kanren: KanrenEngine = KanrenEngine() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bowen-xu please ignore my changes to the tests, it was just me temporarily changing the code just to see if KanrenEngine returns the same values as the GeneralEngine. When this branch is ready to merge I will delete all of this code plus it's probably going to be gone anyway when we rewrite the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it.
return task.term if rule else task.sentence | ||
|
||
class KanrenEngine: | ||
_all_rules = '''{<M --> P>. <S --> M>} |- <S --> P> .ded |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bowen-xu do you know of a convenient way to get a list of all rules grouped by NAL level? I just dumped all the rules I had in Swift and formatted them a bit but in order to accept a parameter similar to how GeneralEngine does it for NAL levels, it would be better to have the rules grouped, I just couldn't find a grouped list at the moment, in the book's appendix the rules aren't separated by NAL levels so if you know some document or resource I can use as a reference to build a complete list of rules separated by NAL level that would be great!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no such a document. However, the rules are grouped in PyNARS by NAL level. For example, you can see all the rules on NAL1 from pynars/NARS/InferenceEngine/GeneralEngine/Rules/NAL1.py
Some of the functions which implement corresponding rules have very clear comment, from which you could know clearly which rule(s) it implements. But not all comments are correct, some might be misleading. Before getting a chance to check and correct all those comments, one have to read the code of the rule functions to double-check. I know that would cost time, but there might be no better ways :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bummer. i guess we can compile it and it'll be a good thing to have for the future
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, and Patrick just said there's such a list in ONA, which I didn't know before. That would be a good reference.
c = self.logic(c, True) | ||
return ((p1, p2, c), r) | ||
|
||
def inference(self, t1: Sentence, t2: Sentence) -> list: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code here needs to handle more than judgements but this was just a quick test.
- Theorems can be handled explicitly using miniKanren | ||
should they be applied at the same time as immediate rules? | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bowen-xu this is my best understanding of the breakdown of rules by level.
will need to find answers to the questions above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When should we apply immediate rules?
When to apply decomposition rules?
Is any arbitrary S valid for this rule?
Should these be handled outside of normal flow?
should they be applied at the same time as immediate rules?
I thinks those questions refer to the same answer. Not all theorems or the rules you mentioned should be applied until there is a hint. For example, the rule {S, P} |- (S && P)
is always valid. However, the rule is applied (for backward inference) only when there a goal (S && P)!
or question appears. Otherwise, combinatorial explosion would happen.
Can these also be the variable introduction rules (substituting {M/#x})?
I don't quite understand this question. What to be the variable introduction rules?
But I'm sure that the rules on variable are not complete, even in NAL book. In NAL book, it just describes some overall principles and considerations on variable and gives some sample rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess what I mean is {S, P} |- (S && P)
is not applied to arbitrary S
and P
, right? They have to be related so this rule in forward inference is mainly used for events using temporal relation as a hint or for variable introduction like in Table 10.3 or as you mentioned for backward inference when there is a corresponding goal.
Regarding theorems:
In the technical report (and maybe in the book) it says that immediate rules are applied when the task enters the system, which to me sounds like it is done once for each task, not repeatedly. So I wonder if theorems can have a similar treatment. I'm trying to understand if the many permutations of rules are necessary or if the same effect can be achieved in other ways.
Regarding variables:
It is exactly the principles I'm trying to capture instead of the actual rules. As far as I can tell the principles can be described at the meta-level as applying certain rules, for example, compositional intersection rule, and then performing the substitution on the conclusion. This is how one of the variable rules in Table 10.4 is described in the NAL book so I'm wondering if the variable introduction rule
needs to be made explicit or if we can just have the procedure
by applying the substitution to the result of inference. The question is then more about when to apply it? Every time such a conclusion is formed? Or should there be some limit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess what I mean is {S, P} |- (S && P) is not applied to arbitrary S and P, right?
In forward reasoning, S and P can be arbitrary sentences, as long as they are no the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I wonder if theorems can have a similar treatment. I'm trying to understand if the many permutations of rules are necessary or if the same effect can be achieved in other ways.
Well, I don't think there is a standard answer to this question. The issue is whether the system can afford the resources regarding the permutations. There might be plenty of useless conclusions by applying the theorems without hints, and that might harm the system achieving goals. I'm not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sorry, I meant the other rule, {P,S} |- S => P
. This one isn’t applied to arbitrary S and P, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if the variable introduction rule needs to be made explicit or if we can just have the procedure by applying the substitution to the result of inference. The question is then more about when to apply it? Every time such a conclusion is formed? Or should there be some limit?
My solution is to make them explicit rules in PyNARS, though this might not be an optimal solution.
For example, there is a variable introduction
rule in pynars:
{<S-->M>, <S-->P>} |- <<$x-->M> ==> <$x-->P>>
This rule is applied only when the pattern of the premises matches this form {<S-->M>, <S-->P>}
.
The substitution happens when the rule is applied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sorry, I meant the other rule,
{P,S} |- S => P
. This one isn’t applied to arbitrary S and P, right?
No, not right. This rule is applied to arbitrary S and P in forward reasoning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure? In the book when these rules are introduced it states:
“The rules in Table 9.5 are not syllogistic, but compositional, since the statements in the conclusions are compound terms composed by the terms in the premises. Because they are applicable only when the premises “can be seen as based on the same implicit condition”, NAL does not take two arbitrary judgments as premises and apply these rules on them.”
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In NAL book, Table 9.5 is to provide justifications for the rule {S, P} |- <S==>P>
. It supposes there is an implicit condition E
, so that <E ==> P>
and <E ==> S>
, then syllogistic induction rule can be applied to derive <S ==> P>
.
In logic, they are not arbitrary. However, in NARS, the reasoning system, what is the implicit condition E
?
You can think all the statements have a common implicit condition E
, so that arbitrary S
and P
are valid for that rule. Nevertheless, NARS doesn't do it even if the rule is applicable, because that will leads to combinatorial explosion.
Can we consider the case where the two premises have common evidential base? Maybe. But I don't think it's going to get any better regarding combinatorial explosion.
can discuss with Pei in the group meeting next week.
This reverts commit 3ae6f6a.
@bowen-xu @ccrock4t regarding inference times I ran some quick tests this morning, to measure just the run time of t0 = time()
results = self.inference(task.sentence, belief.sentence)
t1 = time() - t0
print("inf:", 1 // t1, "per second")
self._run_count += 1
self._inference_time_avg += (t1 - self._inference_time_avg) / self._run_count
print("avg:", 1 // self._inference_time_avg, "per second") The results so far on a small sample indicate ~300-400 calls per second on my laptop.
|
Expanding on the previous attempt at using miniKanren for rule matching.
{<(&&, C, S) ==> P>. S} |- <C ==> P>
making the difference operation explicit (not production ready solution, just an example)