Fix up bad git subtree import.
This commit is contained in:
299
misc/aqe/KnowledgeBase example.ipynb
Normal file
299
misc/aqe/KnowledgeBase example.ipynb
Normal file
@@ -0,0 +1,299 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import actions, kb, sample"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## AQE: A Query Engine\n",
|
||||
"\n",
|
||||
"This is an implementation of a knowledge base, hacked together in Python\n",
|
||||
"3 (it won't work in Python 2 for reasons of modules) for now to quickly\n",
|
||||
"iterate on ideas.\n",
|
||||
"\n",
|
||||
"The `KnowledgeBase` is a repository of facts."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"skb = sample.load()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"A fact is a tuple: (relationship, subject, object). `object` is admittedly a terrible name (and is subject to change) but it's what I came up with and what I'm working with for now."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[('is', 'cbr600', 'Driver'),\n",
|
||||
" ('at', 'cbr600', 'oakland'),\n",
|
||||
" ('at', 'airliner', 'denver'),\n",
|
||||
" ('is', 'oakland', 'Airport'),\n",
|
||||
" ('is', 'airliner', 'Flyer'),\n",
|
||||
" ('is', 'oakland', 'City'),\n",
|
||||
" ('is', 'trooper', 'Driver'),\n",
|
||||
" ('is', 'denver', 'City'),\n",
|
||||
" ('is', 'denver', 'Airport')]"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"skb.facts()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"A KB can be told a fact with the `tell` method."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"skb.tell(('is', 'san francisco', 'cool'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Similarly, the KB can be told a fact is *not* true with the `retract` method."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"skb.retract(('is', 'san francisco', 'cool'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The KB can be queried about the facts it has. There are two types of queries. The first is done with a full fact, and represents the question \"Is this fact true?\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[('is', 'oakland', 'City')]\n",
|
||||
"[]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(skb.ask(('is', 'oakland', 'City')))\n",
|
||||
"print(skb.ask(('is', 'cbr600', 'City')))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"A query returns a list of facts; the empty list means no facts were found. This might seem an odd way to represent this first question; an invalid fact is represented by an empty list, or it returns a list of a single fact. The reason for doing it this way is to support the second type of question: \"What are the facts for which this query is valid?\" This is done by providing a `None` value to *either* the subject or object. (Eventually, I'll get around to adding support for empty relationships too...)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[('is', 'oakland', 'City'), ('is', 'denver', 'City')]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(skb.ask(('is', None, 'City')))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Another thing the KB can do is provide some basic substution using the `subst` method. It takes a fact template, a subject, and an object, and returns a fact (without making any statement as to the validity of the fact). The subject and object can be one of several values:\n",
|
||||
"\n",
|
||||
"+ `None`: the subject or object (depending on which position is `None`) from the arguments is substituted into the fact.\n",
|
||||
"+ `?subject`: substitutes the subject.\n",
|
||||
"+ `?object`: substitutes the object.\n",
|
||||
"+ `?current`: the current value is kept --- this must be used only with singleton facts.\n",
|
||||
"+ `?any`: the value is kept as `None`.\n",
|
||||
"\n",
|
||||
"Some examples should clarify this."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"('is', 'oakland', 'City')\n",
|
||||
"('at', 'cbr600', 'oakland')\n",
|
||||
"('is', 'City', 'oakland')\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(skb.subst(('is', None, 'City'), 'oakland', None))\n",
|
||||
"print(skb.subst(('at', '?subject', '?current'), 'cbr600', None))\n",
|
||||
"print(skb.subst(('is', '?object', '?subject'), 'oakland', 'City'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"To understand `subst`, it's useful to note that it was written to support actions.\n",
|
||||
"\n",
|
||||
"Actions are initialised with a positive precondition (facts that must be valid for the action to be performed), a negative precondition (facts that must not be valid for the action to be performed), a set of retractions, and a set of updates.\n",
|
||||
"\n",
|
||||
"To illustrate this, here's a small example of airplanes and airports."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"airport_kb = kb.from_facts([\n",
|
||||
" ('is', 'N29EO', 'Plane'),\n",
|
||||
" ('at', 'N29EO', 'dia'),\n",
|
||||
" ('is', 'N10IV', 'Plane'),\n",
|
||||
" ('at', 'N10IV', 'oak'),\n",
|
||||
" ('is', 'N33FR', 'Plane'),\n",
|
||||
" ('at', 'N33FR', 'lga'),\n",
|
||||
" ('is', 'dia', 'Airport'),\n",
|
||||
" ('is', 'lga', 'Airport'),\n",
|
||||
" ('is', 'oak', 'Airport'),\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"fly = actions.Action(\n",
|
||||
" [('is', '?subject', 'Plane'), ('is', '?object', 'Airport')], # Positive preconditions.\n",
|
||||
" [('at', '?subject', '?object'),], # Negative preconditions.\n",
|
||||
" [('at', '?subject', '?current'),], # Retractions.\n",
|
||||
" [('at', '?subject', '?object')]) # Updates."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"For a `fly` action to be performed, there's a few facts we should make sure are true:\n",
|
||||
"\n",
|
||||
"1. The subject of the action is a `Plane`, and\n",
|
||||
"2. The object of the action is an `Airport`.\n",
|
||||
"\n",
|
||||
"We should make sure that the subject isn't currently at our target airport.\n",
|
||||
"\n",
|
||||
"If these hold, we can perform the action. The retraction says that the subject is no longer at the airport it was at before the action, and the KB is updated to say that the plane is at a new airport."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 24,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Before flying, is N10IV at LGA? []\n",
|
||||
"Before flying, is N10IV at OAK? [('at', 'N10IV', 'oak')]\n",
|
||||
"After flying, is N10IV at LGA? [('at', 'N10IV', 'lga')]\n",
|
||||
"After flying, is N10IV at OAK? []\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print('Before flying, is N10IV at LGA? ', airport_kb.ask(('at', 'N10IV', 'lga')))\n",
|
||||
"print('Before flying, is N10IV at OAK? ', airport_kb.ask(('at', 'N10IV', 'oak')))\n",
|
||||
"\n",
|
||||
"new_airport_kb = fly.perform(airport_kb, 'N10IV', 'lga')\n",
|
||||
"\n",
|
||||
"print('After flying, is N10IV at LGA? ', new_airport_kb.ask(('at', 'N10IV', 'lga')))\n",
|
||||
"print('After flying, is N10IV at OAK? ', new_airport_kb.ask(('at', 'N10IV', 'oak')))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"There's more work to be done, but this represents a solid night of putting the plan into action based on what I'd learned from the AI nanodegree. I've got a bigger vision for what I want to do out of this, but it's nice to have a baseline to reason about."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.6.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
61
misc/aqe/NOTES.txt
Normal file
61
misc/aqe/NOTES.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
Inference example:
|
||||
|
||||
Given
|
||||
airport_kb = kb.from_facts([
|
||||
('is', 'N29EO', 'Plane'),
|
||||
('at', 'N29EO', 'dia'),
|
||||
('is', 'N10IV', 'Plane'),
|
||||
('at', 'N10IV', 'oak'),
|
||||
('is', 'N33FR', 'Plane'),
|
||||
('at', 'N33FR', 'lga'),
|
||||
('is', 'dia', 'Airport'),
|
||||
('is', 'lga', 'Airport'),
|
||||
('is', 'oak', 'Airport'),
|
||||
])
|
||||
|
||||
fly = actions.Action(
|
||||
[('is', '?subject', 'Plane'), ('is', '?object', 'Airport')],
|
||||
[('at', '?subject', '?object'),],
|
||||
[('at', '?subject', '?current'),],
|
||||
[('at', '?subject', '?object')])
|
||||
|
||||
Should be able to do something like:
|
||||
> infer(airport_kb, [fly], ('at', 'N10IV', 'lga'))
|
||||
('fly', 'N10IV', 'lga')
|
||||
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
Inference search example:
|
||||
|
||||
airport_kb = [
|
||||
('is', 'N29EO', 'Plane'),
|
||||
('at', 'N29EO', 'dia'),
|
||||
('is', 'N10IV', 'Plane'),
|
||||
('at', 'N10IV', 'oak'),
|
||||
('is', 'N33FR', 'Plane'),
|
||||
('at', 'N33FR', 'lga'),
|
||||
('is', '1Z12345E0205271688', 'Package'),
|
||||
('at', '1Z12345E0205271688', 'dia'),
|
||||
('is', '1Z12345E6605272234', 'Package'),
|
||||
('at', '1Z12345E6605272234', 'dia'),
|
||||
('is', '1Z12345E0305271640', 'Package'),
|
||||
('at', '1Z12345E0305271640', 'oak'),
|
||||
('is', '1Z12345E1305277940', 'Package'),
|
||||
('at', '1Z12345E1305277940', 'lga'),
|
||||
('is', '1Z12345E6205277936', 'Package'),
|
||||
('at', '1Z12345E6205277936', 'lga'),
|
||||
('is', 'dia', 'Airport'),
|
||||
('is', 'lga', 'Airport'),
|
||||
('is', 'oak', 'Airport'),
|
||||
]
|
||||
|
||||
fly = actions.Action(
|
||||
[('is', '?subject', 'Plane'), ('is', '?object', 'Airport')],
|
||||
[('at', '?subject', '?object'),],
|
||||
[('at', '?subject', '?current'),],
|
||||
[('at', '?subject', '?object')])
|
||||
|
||||
Trying to define load which requires that both package and airplane are
|
||||
at the same place: how can this be expressed?
|
||||
|
||||
44
misc/aqe/README.md
Normal file
44
misc/aqe/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
## AQE: A Query Engine
|
||||
|
||||
This is an implementation of a knowledge base, hacked together in Python
|
||||
3 (it won't work in Python 2 for reasons of modules) for now to quickly
|
||||
iterate on ideas.
|
||||
|
||||
There are a few key points:
|
||||
|
||||
+ A `KnowledgeBase` contains facts.
|
||||
+ A fact is a tuple: (relationship, subject, object). For example,
|
||||
`('is', 'sky', 'blue')`.
|
||||
+ A `KnowledgeBase` has three core methods: ask, retract, and tell.
|
||||
+ The `ask` method queries the `KnowledgeBase` to ascertain whether
|
||||
a fact is true. Either the subject or the object may be `None`,
|
||||
in which case all satisifiable facts are returned.
|
||||
+ The `retract` method tells the `KnowledgeBase` that the fact is
|
||||
no longer true. If it's rainy, we might retract our fact about the
|
||||
sky being blue.
|
||||
+ The `tell` method tells the `KnowledgeBase` that the fact is
|
||||
now true. For example, if it's rainy (and we've retracted the previous
|
||||
'sky is blue' fact), we might tell the `KnowledgeBase` that
|
||||
`('is', 'sky', 'grey')`.
|
||||
+ A `KnowledgeBase` can also perform substitutions.
|
||||
+ An action contains positive and negative preconditions, retractions,
|
||||
and updates. The positive condition list contains facts that must
|
||||
be true for a knowledge base, and the negative condition list contains
|
||||
facts that must be false. If these preconditions hold, the retractions
|
||||
are applied, followed by the updates.
|
||||
+ See `test_actions.py` for an example.
|
||||
|
||||
### Limitations
|
||||
|
||||
+ Singleton facts aren't supported; that is, there is no way to make a
|
||||
`KnowledgeBase` assert that there is only one relationship → subject
|
||||
mapping. For example, the `KnowledgeBase` will admit that
|
||||
`('is', 'shrödingers cat', 'alive')` and
|
||||
`('is', 'schrödingers cat', 'dead')` are both true simultaneously.
|
||||
|
||||
### TODO
|
||||
|
||||
+ Inference: given a list of actions, how to go from one state to
|
||||
another. The first step would be single-step, then integrating
|
||||
a search into the inference.
|
||||
+ Rewrite in C++?
|
||||
0
misc/aqe/__init__.py
Normal file
0
misc/aqe/__init__.py
Normal file
33
misc/aqe/actions.py
Normal file
33
misc/aqe/actions.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import copy
|
||||
import logging
|
||||
|
||||
class Action:
|
||||
|
||||
def __init__(self, pos_precond, neg_precond, retracts, updates):
|
||||
self.pos_precond = copy.deepcopy(pos_precond)
|
||||
self.neg_precond = copy.deepcopy(neg_precond)
|
||||
self.retracts = copy.deepcopy(retracts)
|
||||
self.updates = copy.deepcopy(updates)
|
||||
|
||||
def satisfied(self, kb, subject, obj):
|
||||
for fact in self.pos_precond:
|
||||
if not kb.ask(kb.subst(fact, subject, obj)):
|
||||
logging.warning('{} is not valid in the current knowledgebase'.format(fact))
|
||||
return False
|
||||
|
||||
for fact in self.neg_precond:
|
||||
if kb.ask(kb.subst(fact, subject, obj)):
|
||||
logging.warning('{} is valid in the current knowledgebase'.format(fact))
|
||||
return False
|
||||
return True
|
||||
|
||||
def perform(self, kb, subject, obj):
|
||||
if not self.satisfied(kb, subject, obj):
|
||||
return None
|
||||
kbprime = copy.deepcopy(kb)
|
||||
for retraction in self.retracts:
|
||||
kbprime.retract(kb.subst(retraction, subject, obj))
|
||||
for update in self.updates:
|
||||
kbprime.tell(kb.subst(update, subject, obj))
|
||||
return kbprime
|
||||
|
||||
1
misc/aqe/data/corpus.json
Normal file
1
misc/aqe/data/corpus.json
Normal file
File diff suppressed because one or more lines are too long
153
misc/aqe/example.md
Normal file
153
misc/aqe/example.md
Normal file
@@ -0,0 +1,153 @@
|
||||
|
||||
|
||||
```python
|
||||
import actions, kb, sample
|
||||
```
|
||||
|
||||
## AQE: A Query Engine
|
||||
|
||||
This is an implementation of a knowledge base, hacked together in Python
|
||||
3 (it won't work in Python 2 for reasons of modules) for now to quickly
|
||||
iterate on ideas.
|
||||
|
||||
The `KnowledgeBase` is a repository of facts.
|
||||
|
||||
|
||||
```python
|
||||
skb = sample.load()
|
||||
```
|
||||
|
||||
A fact is a tuple: (relationship, subject, object). `object` is admittedly a terrible name (and is subject to change) but it's what I came up with and what I'm working with for now.
|
||||
|
||||
|
||||
```python
|
||||
skb.facts()
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
[('is', 'cbr600', 'Driver'),
|
||||
('at', 'cbr600', 'oakland'),
|
||||
('at', 'airliner', 'denver'),
|
||||
('is', 'oakland', 'Airport'),
|
||||
('is', 'airliner', 'Flyer'),
|
||||
('is', 'oakland', 'City'),
|
||||
('is', 'trooper', 'Driver'),
|
||||
('is', 'denver', 'City'),
|
||||
('is', 'denver', 'Airport')]
|
||||
|
||||
|
||||
|
||||
A KB can be told a fact with the `tell` method.
|
||||
|
||||
|
||||
```python
|
||||
skb.tell(('is', 'san francisco', 'cool'))
|
||||
```
|
||||
|
||||
Similarly, the KB can be told a fact is *not* true with the `retract` method.
|
||||
|
||||
|
||||
```python
|
||||
skb.retract(('is', 'san francisco', 'cool'))
|
||||
```
|
||||
|
||||
The KB can be queried about the facts it has. There are two types of queries. The first is done with a full fact, and represents the question "Is this fact true?"
|
||||
|
||||
|
||||
```python
|
||||
print(skb.ask(('is', 'oakland', 'City')))
|
||||
print(skb.ask(('is', 'cbr600', 'City')))
|
||||
```
|
||||
|
||||
[('is', 'oakland', 'City')]
|
||||
[]
|
||||
|
||||
|
||||
A query returns a list of facts; the empty list means no facts were found. This might seem an odd way to represent this first question; an invalid fact is represented by an empty list, or it returns a list of a single fact. The reason for doing it this way is to support the second type of question: "What are the facts for which this query is valid?" This is done by providing a `None` value to *either* the subject or object. (Eventually, I'll get around to adding support for empty relationships too...)
|
||||
|
||||
|
||||
```python
|
||||
print(skb.ask(('is', None, 'City')))
|
||||
```
|
||||
|
||||
[('is', 'oakland', 'City'), ('is', 'denver', 'City')]
|
||||
|
||||
|
||||
Another thing the KB can do is provide some basic substution using the `subst` method. It takes a fact template, a subject, and an object, and returns a fact (without making any statement as to the validity of the fact). The subject and object can be one of several values:
|
||||
|
||||
+ `None`: the subject or object (depending on which position is `None`) from the arguments is substituted into the fact.
|
||||
+ `?subject`: substitutes the subject.
|
||||
+ `?object`: substitutes the object.
|
||||
+ `?current`: the current value is kept --- this must be used only with singleton facts.
|
||||
+ `?any`: the value is kept as `None`.
|
||||
|
||||
Some examples should clarify this.
|
||||
|
||||
|
||||
```python
|
||||
print(skb.subst(('is', None, 'City'), 'oakland', None))
|
||||
print(skb.subst(('at', '?subject', '?current'), 'cbr600', None))
|
||||
print(skb.subst(('is', '?object', '?subject'), 'oakland', 'City'))
|
||||
```
|
||||
|
||||
('is', 'oakland', 'City')
|
||||
('at', 'cbr600', 'oakland')
|
||||
('is', 'City', 'oakland')
|
||||
|
||||
|
||||
To understand `subst`, it's useful to note that it was written to support actions.
|
||||
|
||||
Actions are initialised with a positive precondition (facts that must be valid for the action to be performed), a negative precondition (facts that must not be valid for the action to be performed), a set of retractions, and a set of updates.
|
||||
|
||||
To illustrate this, here's a small example of airplanes and airports.
|
||||
|
||||
|
||||
```python
|
||||
airport_kb = kb.from_facts([
|
||||
('is', 'N29EO', 'Plane'),
|
||||
('at', 'N29EO', 'dia'),
|
||||
('is', 'N10IV', 'Plane'),
|
||||
('at', 'N10IV', 'oak'),
|
||||
('is', 'N33FR', 'Plane'),
|
||||
('at', 'N33FR', 'lga'),
|
||||
('is', 'dia', 'Airport'),
|
||||
('is', 'lga', 'Airport'),
|
||||
('is', 'oak', 'Airport'),
|
||||
])
|
||||
|
||||
fly = actions.Action(
|
||||
[('is', '?subject', 'Plane'), ('is', '?object', 'Airport')], # Positive preconditions.
|
||||
[('at', '?subject', '?object'),], # Negative preconditions.
|
||||
[('at', '?subject', '?current'),], # Retractions.
|
||||
[('at', '?subject', '?object')]) # Updates.
|
||||
```
|
||||
|
||||
For a `fly` action to be performed, there's a few facts we should make sure are true:
|
||||
|
||||
1. The subject of the action is a `Plane`, and
|
||||
2. The object of the action is an `Airport`.
|
||||
|
||||
We should make sure that the subject isn't currently at our target airport.
|
||||
|
||||
If these hold, we can perform the action. The retraction says that the subject is no longer at the airport it was at before the action, and the KB is updated to say that the plane is at a new airport.
|
||||
|
||||
|
||||
```python
|
||||
print('Before flying, is N10IV at LGA? ', airport_kb.ask(('at', 'N10IV', 'lga')))
|
||||
print('Before flying, is N10IV at OAK? ', airport_kb.ask(('at', 'N10IV', 'oak')))
|
||||
|
||||
new_airport_kb = fly.perform(airport_kb, 'N10IV', 'lga')
|
||||
|
||||
print('After flying, is N10IV at LGA? ', new_airport_kb.ask(('at', 'N10IV', 'lga')))
|
||||
print('After flying, is N10IV at OAK? ', new_airport_kb.ask(('at', 'N10IV', 'oak')))
|
||||
```
|
||||
|
||||
Before flying, is N10IV at LGA? []
|
||||
Before flying, is N10IV at OAK? [('at', 'N10IV', 'oak')]
|
||||
After flying, is N10IV at LGA? [('at', 'N10IV', 'lga')]
|
||||
After flying, is N10IV at OAK? []
|
||||
|
||||
|
||||
There's more work to be done, but this represents a solid night of putting the plan into action based on what I'd learned from the AI nanodegree. I've got a bigger vision for what I want to do out of this, but it's nice to have a baseline to reason about.
|
||||
159
misc/aqe/kb.py
Normal file
159
misc/aqe/kb.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
AQE: A Query Engine
|
||||
|
||||
This is a proof of concept of a baseline query engine for AI work.
|
||||
"""
|
||||
|
||||
class InvalidQuery(Exception):
|
||||
pass
|
||||
|
||||
class Inconsistency(Exception):
|
||||
def __init__(self, fact):
|
||||
self.fact = fact
|
||||
|
||||
def __str__(self):
|
||||
return 'Inconsistency: {}'.format(self.fact)
|
||||
|
||||
class KnowledgeBase:
|
||||
|
||||
def __init__(self):
|
||||
# TODO(kyle): support loading an initial set of facts.
|
||||
self.__kb__ = {}
|
||||
self.__facts__ = set()
|
||||
|
||||
def tell(self, fact):
|
||||
relationship, subject, obj = fact
|
||||
|
||||
# NB: in the future, these assertions may not need to be true; there
|
||||
# might be space in the world for "fuzzy" facts.
|
||||
assert(relationship)
|
||||
assert(subject)
|
||||
assert(obj)
|
||||
if relationship not in self.__kb__:
|
||||
self.__kb__[relationship] = {'subjects':{}, 'objects': {}}
|
||||
|
||||
if subject not in self.__kb__[relationship]['subjects']:
|
||||
self.__kb__[relationship]['subjects'][subject] = set()
|
||||
self.__kb__[relationship]['subjects'][subject].add(obj)
|
||||
|
||||
if obj not in self.__kb__[relationship]['objects']:
|
||||
self.__kb__[relationship]['objects'][obj] = set()
|
||||
self.__kb__[relationship]['objects'][obj].add(subject)
|
||||
self.__facts__.add(fact)
|
||||
|
||||
def retract(self, fact):
|
||||
relationship, subject, obj = fact
|
||||
|
||||
# For now, these assertions are required. In the future, it would be
|
||||
# interesting to say something to the effect of "forget everything you
|
||||
# know about X".
|
||||
assert(relationship)
|
||||
assert(subject)
|
||||
assert(obj)
|
||||
|
||||
# TODO(kyle): answer existential question: if I delete all the objects
|
||||
# from a subject (or vice versa), should that subject/object be kept or
|
||||
# removed entirely? This is the difference between "I have no concept
|
||||
# of X" and "I am aware that X exists but I don't know anything about it".
|
||||
# For now, I'm electing to keep the entry.
|
||||
#
|
||||
# Similarly, if the relationship is empty, we could make the argument
|
||||
# for removing it --- at the expense of now saying that we have no
|
||||
# concept of this relationship.
|
||||
try:
|
||||
self.__kb__[relationship]['subjects'][subject].remove(obj)
|
||||
self.__kb__[relationship]['objects'][obj].remove(subject)
|
||||
self.__facts__.remove(fact)
|
||||
except KeyError:
|
||||
# Being told to forget something about something you don't know
|
||||
# isn't an error.
|
||||
pass
|
||||
pass
|
||||
|
||||
def ask(self, fact):
|
||||
relationship, subject, obj = fact
|
||||
|
||||
# A future milestone will remove this requirement to support free
|
||||
# variables.
|
||||
assert(relationship)
|
||||
|
||||
if relationship and subject and obj:
|
||||
if fact in self.__facts__:
|
||||
return [fact,]
|
||||
return []
|
||||
|
||||
if relationship and subject:
|
||||
return [(relationship, subject, _obj) for _obj
|
||||
in self.__kb__[relationship]['subjects'][subject]]
|
||||
|
||||
if relationship and obj:
|
||||
return [(relationship, _subject, obj) for _subject
|
||||
in self.__kb__[relationship]['objects'][obj]]
|
||||
|
||||
def facts(self):
|
||||
return list(self.__facts__)
|
||||
|
||||
def is_consistent(self):
|
||||
try:
|
||||
for fact in self.__facts__:
|
||||
relationship, subject, obj = fact
|
||||
if obj not in self.__kb__[relationship]['subjects'][subject]:
|
||||
raise Inconsistency(fact)
|
||||
if subject not in self.__kb__[relationship]['objects'][obj]:
|
||||
raise Inconsistency(fact)
|
||||
|
||||
for relationship, v in self.__kb__.items():
|
||||
for subject in v['subjects'].keys():
|
||||
for obj in v['subjects'][subject]:
|
||||
if (relationship, subject, obj) not in self.__facts__:
|
||||
raise Inconsistency(fact)
|
||||
|
||||
for obj in v['objects'].keys():
|
||||
for subject in v['objects'][obj]:
|
||||
if (relationship, subject, obj) not in self.__facts__:
|
||||
raise Inconsistency(fact)
|
||||
except KeyError:
|
||||
raise Inconsistency(fact)
|
||||
|
||||
return True
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__facts__)
|
||||
|
||||
def subst(self, fact, subject, obj):
|
||||
relationship, _subject, _obj = fact
|
||||
if _subject is None:
|
||||
_subject = subject
|
||||
if _subject == '?any':
|
||||
_subject = None
|
||||
elif _subject == '?subject':
|
||||
_subject = subject
|
||||
elif _subject == '?object':
|
||||
_subject = obj
|
||||
|
||||
if _obj is None:
|
||||
_obj = obj
|
||||
if _obj == '?any':
|
||||
_obj = None
|
||||
elif _obj == '?subject':
|
||||
_obj = subject
|
||||
elif _obj == '?object':
|
||||
_obj = obj
|
||||
|
||||
if _subject == '?current':
|
||||
possibilities = self.ask((relationship, None, _obj))
|
||||
assert(len(possibilities) == 1)
|
||||
_, _subject, _ = possibilities[0]
|
||||
elif _obj == '?current':
|
||||
possibilities = self.ask((relationship, subject, None))
|
||||
assert(len(possibilities) == 1)
|
||||
_, _, _obj = possibilities[0]
|
||||
|
||||
return (relationship, _subject, _obj)
|
||||
|
||||
|
||||
def from_facts(facts):
|
||||
kb = KnowledgeBase()
|
||||
for fact in facts:
|
||||
kb.tell(fact)
|
||||
return kb
|
||||
47
misc/aqe/sample.py
Normal file
47
misc/aqe/sample.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import base64
|
||||
import itertools
|
||||
import json
|
||||
import kb
|
||||
import pickle
|
||||
import random
|
||||
|
||||
FACTS = """
|
||||
gANdcQAoWAIAAABpc3EBWAgAAABhaXJsaW5lcnECWAUAAABGbHllcnEDh3EEaAFYBwAAAG9ha2xh
|
||||
bmRxBVgHAAAAQWlycG9ydHEGh3EHaAFoBVgEAAAAQ2l0eXEIh3EJaAFYBgAAAGRlbnZlcnEKaAaH
|
||||
cQtoAWgKaAiHcQxoAVgGAAAAY2JyNjAwcQ1YBgAAAERyaXZlcnEOh3EPaAFYBwAAAHRyb29wZXJx
|
||||
EGgOh3ERWAIAAABhdHESaAJoCodxE2gSaA1oBYdxFGUu
|
||||
"""
|
||||
|
||||
def load():
|
||||
facts = base64.decodebytes(FACTS.encode('ascii'))
|
||||
facts = pickle.loads(facts)
|
||||
skb = kb.KnowledgeBase()
|
||||
for fact in facts:
|
||||
skb.tell(fact)
|
||||
|
||||
return skb
|
||||
|
||||
def load_facts(corpus_path='data/corpus.json', is_count=1000000):
|
||||
facts = set()
|
||||
corpus = json.loads(open(corpus_path).read())
|
||||
if 'nouns' in corpus and 'adjectives' in corpus:
|
||||
perms = list(itertools.product(corpus['nouns'],
|
||||
corpus['adjectives']))
|
||||
if len(perms) < is_count:
|
||||
is_count = len(perms)-1;
|
||||
pool = random.choices(perms, k=is_count)
|
||||
for noun, adjective in pool:
|
||||
facts.add(('is', noun, adjective))
|
||||
|
||||
if 'cities' in corpus:
|
||||
for city in corpus['cities']:
|
||||
facts.add(('is', city, 'City'))
|
||||
|
||||
return facts
|
||||
|
||||
def generate_tail_number():
|
||||
letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
tailno = 'N' + str(random.randint(10, 99))
|
||||
tailno += random.choice(letters)
|
||||
tailno += random.choice(letters)
|
||||
return tailno
|
||||
67
misc/aqe/test_actions.py
Normal file
67
misc/aqe/test_actions.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import actions
|
||||
import kb
|
||||
import unittest
|
||||
|
||||
INITIAL_FACTS = [
|
||||
('is', 'N29EO', 'Plane'),
|
||||
('at', 'N29EO', 'dia'),
|
||||
('is', 'N10IV', 'Plane'),
|
||||
('at', 'N10IV', 'oak'),
|
||||
('is', 'N33FR', 'Plane'),
|
||||
('at', 'N33FR', 'lga'),
|
||||
('is', '1Z12345E0205271688', 'Package'),
|
||||
('at', '1Z12345E0205271688', 'dia'),
|
||||
('is', '1Z12345E6605272234', 'Package'),
|
||||
('at', '1Z12345E6605272234', 'dia'),
|
||||
('is', '1Z12345E0305271640', 'Package'),
|
||||
('at', '1Z12345E0305271640', 'oak'),
|
||||
('is', '1Z12345E1305277940', 'Package'),
|
||||
('at', '1Z12345E1305277940', 'lga'),
|
||||
('is', '1Z12345E6205277936', 'Package'),
|
||||
('at', '1Z12345E6205277936', 'lga'),
|
||||
('is', 'dia', 'Airport'),
|
||||
('is', 'lga', 'Airport'),
|
||||
('is', 'oak', 'Airport'),
|
||||
]
|
||||
|
||||
FLY_POS_PRECONDS = [
|
||||
('is', '?subject', 'Plane'),
|
||||
('is', '?object', 'Airport'),
|
||||
]
|
||||
|
||||
FLY_NEG_PRECONDS = [
|
||||
('at', '?subject', '?object'),
|
||||
]
|
||||
|
||||
FLY_RETRACTIONS = [
|
||||
('at', '?subject', '?current'),
|
||||
]
|
||||
|
||||
FLY_UPDATES = [
|
||||
('at', '?subject', '?object'),
|
||||
]
|
||||
|
||||
fly = actions.Action(FLY_POS_PRECONDS, FLY_NEG_PRECONDS,
|
||||
FLY_RETRACTIONS, FLY_UPDATES)
|
||||
|
||||
class ActionTestSuite(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.kb = kb.from_facts(INITIAL_FACTS)
|
||||
|
||||
def test_a_flight(self):
|
||||
self.assertTrue(self.kb.ask(('at', 'N10IV', 'oak')))
|
||||
self.assertFalse(self.kb.ask(('at', 'N10IV', 'lga')))
|
||||
|
||||
shadow = fly.perform(self.kb, 'N10IV', 'lga')
|
||||
self.assertTrue(shadow)
|
||||
|
||||
# Shadow should reflect the updates and retractions.
|
||||
self.assertTrue(shadow.ask(('at', 'N10IV', 'lga')))
|
||||
self.assertFalse(shadow.ask(('at', 'N10IV', 'oak')))
|
||||
|
||||
# The original shouldn't be touched.
|
||||
self.assertTrue(self.kb.ask(('at', 'N10IV', 'oak')))
|
||||
self.assertFalse(self.kb.ask(('at', 'N10IV', 'lga')))
|
||||
|
||||
|
||||
59
misc/aqe/test_kb.py
Normal file
59
misc/aqe/test_kb.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import copy
|
||||
import kb
|
||||
import random
|
||||
import sample
|
||||
import unittest
|
||||
|
||||
|
||||
class KnowledgeBaseTestSuite(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.kb = sample.load()
|
||||
|
||||
def test_a_sanity_check(self):
|
||||
assert(self.kb.is_consistent())
|
||||
for fact in self.kb.__facts__:
|
||||
self.assertTrue(self.kb.ask(fact))
|
||||
|
||||
def test_tell(self):
|
||||
new_fact = ('is', 'berkeley', 'City')
|
||||
|
||||
# make sure it's not something we already know
|
||||
self.assertFalse(self.kb.ask(new_fact))
|
||||
self.kb.tell(new_fact)
|
||||
answer = self.kb.ask(new_fact)
|
||||
self.assertListEqual(answer, [new_fact,])
|
||||
|
||||
def test_inconsistency(self):
|
||||
badkb = copy.deepcopy(self.kb)
|
||||
badfact = random.choice(badkb.facts())
|
||||
relationship, subject, obj = badfact
|
||||
|
||||
# muck with subjects part
|
||||
badkb.__kb__[relationship]['subjects'][subject].remove(obj)
|
||||
with self.assertRaises(kb.Inconsistency):
|
||||
badkb.is_consistent()
|
||||
|
||||
# muck with objects part
|
||||
badkb = copy.deepcopy(self.kb)
|
||||
badkb.__kb__[relationship]['objects'][obj].remove(subject)
|
||||
with self.assertRaises(kb.Inconsistency):
|
||||
badkb.is_consistent()
|
||||
|
||||
# muck with facts part
|
||||
badkb = copy.deepcopy(self.kb)
|
||||
badkb.__facts__.remove(badfact)
|
||||
with self.assertRaises(kb.Inconsistency):
|
||||
badkb.is_consistent()
|
||||
|
||||
# inject false data into the subject
|
||||
badkb = copy.deepcopy(self.kb)
|
||||
badkb.__kb__[relationship]['subjects'][subject].add('false memory')
|
||||
with self.assertRaises(kb.Inconsistency):
|
||||
badkb.is_consistent()
|
||||
|
||||
# inject false data into the object
|
||||
badkb = copy.deepcopy(self.kb)
|
||||
badkb.__kb__[relationship]['objects'][obj].add('false memory')
|
||||
with self.assertRaises(kb.Inconsistency):
|
||||
badkb.is_consistent()
|
||||
8
misc/aqe/util.py
Normal file
8
misc/aqe/util.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import random
|
||||
|
||||
def generate_tail_number():
|
||||
letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
tailno = 'N' + random.randint(10, 99)
|
||||
tailno += random.choice(letters)
|
||||
tailno += random.choice(letters)
|
||||
return tailno
|
||||
Reference in New Issue
Block a user