put up all aima code
parent
e079ae5ad2
commit
54b24fce7a
@ -0,0 +1,8 @@
|
||||
|
||||
### demo
|
||||
|
||||
>>> min_conflicts(australia)
|
||||
{'WA': 'B', 'Q': 'B', 'T': 'G', 'V': 'B', 'SA': 'R', 'NT': 'G', 'NSW': 'G'}
|
||||
|
||||
>>> min_conflicts(usa)
|
||||
{'WA': 'B', 'DE': 'R', 'DC': 'Y', 'WI': 'G', 'WV': 'Y', 'HI': 'R', 'FL': 'B', 'WY': 'Y', 'NH': 'R', 'NJ': 'Y', 'NM': 'Y', 'TX': 'G', 'LA': 'R', 'NC': 'B', 'ND': 'Y', 'NE': 'B', 'TN': 'G', 'NY': 'R', 'PA': 'B', 'RI': 'R', 'NV': 'Y', 'VA': 'R', 'CO': 'R', 'CA': 'B', 'AL': 'R', 'AR': 'Y', 'VT': 'Y', 'IL': 'B', 'GA': 'Y', 'IN': 'Y', 'IA': 'Y', 'OK': 'B', 'AZ': 'R', 'ID': 'G', 'CT': 'Y', 'ME': 'B', 'MD': 'G', 'KA': 'Y', 'MA': 'B', 'OH': 'R', 'UT': 'B', 'MO': 'R', 'MN': 'R', 'MI': 'B', 'AK': 'B', 'MT': 'R', 'MS': 'B', 'SC': 'G', 'KY': 'B', 'OR': 'R', 'SD': 'G'}
|
@ -0,0 +1,43 @@
|
||||
"""Run all doctests from modules on the command line. For each
|
||||
module, if there is a "module.txt" file, run that too. However,
|
||||
if the module.txt file contains the comment "# demo",
|
||||
then the remainder of the file has its ">>>" lines executed,
|
||||
but not run through doctest. The idea is that you can use this
|
||||
to demo statements that return random or otherwise variable results.
|
||||
|
||||
Example usage:
|
||||
|
||||
python doctests.py *.py
|
||||
"""
|
||||
|
||||
import doctest, re
|
||||
|
||||
def run_tests(modules, verbose=None):
|
||||
"Run tests for a list of modules; then summarize results."
|
||||
for module in modules:
|
||||
tests, demos = split_extra_tests(module.__name__ + ".txt")
|
||||
if tests:
|
||||
if '__doc__' not in dir(module):
|
||||
module.__doc__ = ''
|
||||
module.__doc__ += '\n' + tests + '\n'
|
||||
doctest.testmod(module, report=0, verbose=verbose)
|
||||
if demos:
|
||||
for stmt in re.findall(">>> (.*)", demos):
|
||||
exec stmt in module.__dict__
|
||||
doctest.master.summarize()
|
||||
|
||||
|
||||
def split_extra_tests(filename):
|
||||
"""Take a filename and, if it exists, return a 2-tuple of
|
||||
the parts before and after '# demo'."""
|
||||
try:
|
||||
contents = open(filename).read() + '# demo'
|
||||
return contents.split("# demo", 1)
|
||||
except IOError:
|
||||
return ('', '')
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
modules = [__import__(name.replace('.py',''))
|
||||
for name in sys.argv if name != "-v"]
|
||||
run_tests(modules, ("-v" in sys.argv))
|
@ -0,0 +1,21 @@
|
||||
### This is an example module.txt file.
|
||||
### It should contain unit tests and their expected results:
|
||||
|
||||
>>> 2 + 2
|
||||
4
|
||||
|
||||
>>> '2' + '2'
|
||||
'22'
|
||||
|
||||
### demo
|
||||
|
||||
### After the part that says 'demo' we have statements that
|
||||
### are intended not as unit tests, but as demos of how to
|
||||
### use the functions and methods in the module. The
|
||||
### statements are executed, but the results are not
|
||||
### compared to the expected results. This can be useful
|
||||
### for nondeterministic functions:
|
||||
|
||||
|
||||
>>> import random; random.choice('abc')
|
||||
'c'
|
@ -0,0 +1,286 @@
|
||||
"""Games, or Adversarial Search. (Chapters 6)
|
||||
|
||||
"""
|
||||
|
||||
from utils import *
|
||||
import random
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Minimax Search
|
||||
|
||||
def minimax_decision(state, game):
|
||||
"""Given a state in a game, calculate the best move by searching
|
||||
forward all the way to the terminal states. [Fig. 6.4]"""
|
||||
|
||||
player = game.to_move(state)
|
||||
|
||||
def max_value(state):
|
||||
if game.terminal_test(state):
|
||||
return game.utility(state, player)
|
||||
v = -infinity
|
||||
for (a, s) in game.successors(state):
|
||||
v = max(v, min_value(s))
|
||||
return v
|
||||
|
||||
def min_value(state):
|
||||
if game.terminal_test(state):
|
||||
return game.utility(state, player)
|
||||
v = infinity
|
||||
for (a, s) in game.successors(state):
|
||||
v = min(v, max_value(s))
|
||||
return v
|
||||
|
||||
# Body of minimax_decision starts here:
|
||||
action, state = argmax(game.successors(state),
|
||||
lambda ((a, s)): min_value(s))
|
||||
return action
|
||||
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def alphabeta_full_search(state, game):
|
||||
"""Search game to determine best action; use alpha-beta pruning.
|
||||
As in [Fig. 6.7], this version searches all the way to the leaves."""
|
||||
|
||||
player = game.to_move(state)
|
||||
|
||||
def max_value(state, alpha, beta):
|
||||
if game.terminal_test(state):
|
||||
return game.utility(state, player)
|
||||
v = -infinity
|
||||
for (a, s) in game.successors(state):
|
||||
v = max(v, min_value(s, alpha, beta))
|
||||
if v >= beta:
|
||||
return v
|
||||
alpha = max(alpha, v)
|
||||
return v
|
||||
|
||||
def min_value(state, alpha, beta):
|
||||
if game.terminal_test(state):
|
||||
return game.utility(state, player)
|
||||
v = infinity
|
||||
for (a, s) in game.successors(state):
|
||||
v = min(v, max_value(s, alpha, beta))
|
||||
if v <= alpha:
|
||||
return v
|
||||
beta = min(beta, v)
|
||||
return v
|
||||
|
||||
# Body of alphabeta_search starts here:
|
||||
action, state = argmax(game.successors(state),
|
||||
lambda ((a, s)): min_value(s, -infinity, infinity))
|
||||
return action
|
||||
|
||||
def alphabeta_search(state, game, d=4, cutoff_test=None, eval_fn=None):
|
||||
"""Search game to determine best action; use alpha-beta pruning.
|
||||
This version cuts off search and uses an evaluation function."""
|
||||
|
||||
player = game.to_move(state)
|
||||
|
||||
def max_value(state, alpha, beta, depth):
|
||||
if cutoff_test(state, depth):
|
||||
return eval_fn(state)
|
||||
v = -infinity
|
||||
for (a, s) in game.successors(state):
|
||||
v = max(v, min_value(s, alpha, beta, depth+1))
|
||||
if v >= beta:
|
||||
return v
|
||||
alpha = max(alpha, v)
|
||||
return v
|
||||
|
||||
def min_value(state, alpha, beta, depth):
|
||||
if cutoff_test(state, depth):
|
||||
return eval_fn(state)
|
||||
v = infinity
|
||||
for (a, s) in game.successors(state):
|
||||
v = min(v, max_value(s, alpha, beta, depth+1))
|
||||
if v <= alpha:
|
||||
return v
|
||||
beta = min(beta, v)
|
||||
return v
|
||||
|
||||
# Body of alphabeta_search starts here:
|
||||
# The default test cuts off at depth d or at a terminal state
|
||||
cutoff_test = (cutoff_test or
|
||||
(lambda state,depth: depth>d or game.terminal_test(state)))
|
||||
eval_fn = eval_fn or (lambda state: game.utility(state, player))
|
||||
action, state = argmax(game.successors(state),
|
||||
lambda ((a, s)): min_value(s, -infinity, infinity, 0))
|
||||
return action
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Players for Games
|
||||
|
||||
def query_player(game, state):
|
||||
"Make a move by querying standard input."
|
||||
game.display(state)
|
||||
return num_or_str(raw_input('Your move? '))
|
||||
|
||||
def random_player(game, state):
|
||||
"A player that chooses a legal move at random."
|
||||
return random.choice(game.legal_moves())
|
||||
|
||||
def alphabeta_player(game, state):
|
||||
return alphabeta_search(state, game)
|
||||
|
||||
def play_game(game, *players):
|
||||
"Play an n-person, move-alternating game."
|
||||
state = game.initial
|
||||
while True:
|
||||
for player in players:
|
||||
move = player(game, state)
|
||||
state = game.make_move(move, state)
|
||||
if game.terminal_test(state):
|
||||
return game.utility(state, players[0])
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Some Sample Games
|
||||
|
||||
class Game:
|
||||
"""A game is similar to a problem, but it has a utility for each
|
||||
state and a terminal test instead of a path cost and a goal
|
||||
test. To create a game, subclass this class and implement
|
||||
legal_moves, make_move, utility, and terminal_test. You may
|
||||
override display and successors or you can inherit their default
|
||||
methods. You will also need to set the .initial attribute to the
|
||||
initial state; this can be done in the constructor."""
|
||||
|
||||
def legal_moves(self, state):
|
||||
"Return a list of the allowable moves at this point."
|
||||
abstract
|
||||
|
||||
def make_move(self, move, state):
|
||||
"Return the state that results from making a move from a state."
|
||||
abstract
|
||||
|
||||
def utility(self, state, player):
|
||||
"Return the value of this final state to player."
|
||||
abstract
|
||||
|
||||
def terminal_test(self, state):
|
||||
"Return True if this is a final state for the game."
|
||||
return not self.legal_moves(state)
|
||||
|
||||
def to_move(self, state):
|
||||
"Return the player whose move it is in this state."
|
||||
return state.to_move
|
||||
|
||||
def display(self, state):
|
||||
"Print or otherwise display the state."
|
||||
print state
|
||||
|
||||
def successors(self, state):
|
||||
"Return a list of legal (move, state) pairs."
|
||||
return [(move, self.make_move(move, state))
|
||||
for move in self.legal_moves(state)]
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % self.__class__.__name__
|
||||
|
||||
class Fig62Game(Game):
|
||||
"""The game represented in [Fig. 6.2]. Serves as a simple test case.
|
||||
>>> g = Fig62Game()
|
||||
>>> minimax_decision('A', g)
|
||||
'a1'
|
||||
>>> alphabeta_full_search('A', g)
|
||||
'a1'
|
||||
>>> alphabeta_search('A', g)
|
||||
'a1'
|
||||
"""
|
||||
succs = {'A': [('a1', 'B'), ('a2', 'C'), ('a3', 'D')],
|
||||
'B': [('b1', 'B1'), ('b2', 'B2'), ('b3', 'B3')],
|
||||
'C': [('c1', 'C1'), ('c2', 'C2'), ('c3', 'C3')],
|
||||
'D': [('d1', 'D1'), ('d2', 'D2'), ('d3', 'D3')]}
|
||||
utils = Dict(B1=3, B2=12, B3=8, C1=2, C2=4, C3=6, D1=14, D2=5, D3=2)
|
||||
initial = 'A'
|
||||
|
||||
def successors(self, state):
|
||||
return self.succs.get(state, [])
|
||||
|
||||
def utility(self, state, player):
|
||||
if player == 'MAX':
|
||||
return self.utils[state]
|
||||
else:
|
||||
return -self.utils[state]
|
||||
|
||||
def terminal_test(self, state):
|
||||
return state not in ('A', 'B', 'C', 'D')
|
||||
|
||||
def to_move(self, state):
|
||||
return if_(state in 'BCD', 'MIN', 'MAX')
|
||||
|
||||
class TicTacToe(Game):
|
||||
"""Play TicTacToe on an h x v board, with Max (first player) playing 'X'.
|
||||
A state has the player to move, a cached utility, a list of moves in
|
||||
the form of a list of (x, y) positions, and a board, in the form of
|
||||
a dict of {(x, y): Player} entries, where Player is 'X' or 'O'."""
|
||||
def __init__(self, h=3, v=3, k=3):
|
||||
update(self, h=h, v=v, k=k)
|
||||
moves = [(x, y) for x in range(1, h+1)
|
||||
for y in range(1, v+1)]
|
||||
self.initial = Struct(to_move='X', utility=0, board={}, moves=moves)
|
||||
|
||||
def legal_moves(self, state):
|
||||
"Legal moves are any square not yet taken."
|
||||
return state.moves
|
||||
|
||||
def make_move(self, move, state):
|
||||
if move not in state.moves:
|
||||
return state # Illegal move has no effect
|
||||
board = state.board.copy(); board[move] = state.to_move
|
||||
moves = list(state.moves); moves.remove(move)
|
||||
return Struct(to_move=if_(state.to_move == 'X', 'O', 'X'),
|
||||
utility=self.compute_utility(board, move, state.to_move),
|
||||
board=board, moves=moves)
|
||||
|
||||
def utility(self, state):
|
||||
"Return the value to X; 1 for win, -1 for loss, 0 otherwise."
|
||||
return state.utility
|
||||
|
||||
def terminal_test(self, state):
|
||||
"A state is terminal if it is won or there are no empty squares."
|
||||
return state.utility != 0 or len(state.moves) == 0
|
||||
|
||||
def display(self, state):
|
||||
board = state.board
|
||||
for x in range(1, self.h+1):
|
||||
for y in range(1, self.v+1):
|
||||
print board.get((x, y), '.'),
|
||||
print
|
||||
|
||||
def compute_utility(self, board, move, player):
|
||||
"If X wins with this move, return 1; if O return -1; else return 0."
|
||||
if (self.k_in_row(board, move, player, (0, 1)) or
|
||||
self.k_in_row(board, move, player, (1, 0)) or
|
||||
self.k_in_row(board, move, player, (1, -1)) or
|
||||
self.k_in_row(board, move, player, (1, 1))):
|
||||
return if_(player == 'X', +1, -1)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def k_in_row(self, board, move, player, (delta_x, delta_y)):
|
||||
"Return true if there is a line through move on board for player."
|
||||
x, y = move
|
||||
n = 0 # n is number of moves in row
|
||||
while board.get((x, y)) == player:
|
||||
n += 1
|
||||
x, y = x + delta_x, y + delta_y
|
||||
x, y = move
|
||||
while board.get((x, y)) == player:
|
||||
n += 1
|
||||
x, y = x - delta_x, y - delta_y
|
||||
n -= 1 # Because we counted move itself twice
|
||||
return n >= self.k
|
||||
|
||||
class ConnectFour(TicTacToe):
|
||||
"""A TicTacToe-like game in which you can only make a move on the bottom
|
||||
row, or in a square directly above an occupied square. Traditionally
|
||||
played on a 7x6 board and requiring 4 in a row."""
|
||||
|
||||
def __init__(self, h=7, v=6, k=4):
|
||||
TicTacToe.__init__(self, h, v, k)
|
||||
|
||||
def legal_moves(self, state):
|
||||
"Legal moves are any square not yet taken."
|
||||
return [(x, y) for (x, y) in state.moves
|
||||
if y == 0 or (x, y-1) in state.board]
|
@ -0,0 +1,586 @@
|
||||
"""Learn to estimate functions from examples. (Chapters 18-20)"""
|
||||
|
||||
from utils import *
|
||||
import agents, random, operator
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class DataSet:
|
||||
"""A data set for a machine learning problem. It has the following fields:
|
||||
|
||||
d.examples A list of examples. Each one is a list of attribute values.
|
||||
d.attrs A list of integers to index into an example, so example[attr]
|
||||
gives a value. Normally the same as range(len(d.examples)).
|
||||
d.attrnames Optional list of mnemonic names for corresponding attrs.
|
||||
d.target The attribute that a learning algorithm will try to predict.
|
||||
By default the final attribute.
|
||||
d.inputs The list of attrs without the target.
|
||||
d.values A list of lists, each sublist is the set of possible
|
||||
values for the corresponding attribute. If None, it
|
||||
is computed from the known examples by self.setproblem.
|
||||
If not None, an erroneous value raises ValueError.
|
||||
d.name Name of the data set (for output display only).
|
||||
d.source URL or other source where the data came from.
|
||||
|
||||
Normally, you call the constructor and you're done; then you just
|
||||
access fields like d.examples and d.target and d.inputs."""
|
||||
|
||||
def __init__(self, examples=None, attrs=None, target=-1, values=None,
|
||||
attrnames=None, name='', source='',
|
||||
inputs=None, exclude=(), doc=''):
|
||||
"""Accepts any of DataSet's fields. Examples can
|
||||
also be a string or file from which to parse examples using parse_csv.
|
||||
>>> DataSet(examples='1, 2, 3')
|
||||
<DataSet(): 1 examples, 3 attributes>
|
||||
"""
|
||||
update(self, name=name, source=source, values=values)
|
||||
# Initialize .examples from string or list or data directory
|
||||
if isinstance(examples, str):
|
||||
self.examples = parse_csv(examples)
|
||||
elif examples is None:
|
||||
self.examples = parse_csv(DataFile(name+'.csv').read())
|
||||
else:
|
||||
self.examples = examples
|
||||
map(self.check_example, self.examples)
|
||||
# Attrs are the indicies of examples, unless otherwise stated.
|
||||
if not attrs and self.examples:
|
||||
attrs = range(len(self.examples[0]))
|
||||
self.attrs = attrs
|
||||
# Initialize .attrnames from string, list, or by default
|
||||
if isinstance(attrnames, str):
|
||||
self.attrnames = attrnames.split()
|
||||
else:
|
||||
self.attrnames = attrnames or attrs
|
||||
self.setproblem(target, inputs=inputs, exclude=exclude)
|
||||
|
||||
def setproblem(self, target, inputs=None, exclude=()):
|
||||
"""Set (or change) the target and/or inputs.
|
||||
This way, one DataSet can be used multiple ways. inputs, if specified,
|
||||
is a list of attributes, or specify exclude as a list of attributes
|
||||
to not put use in inputs. Attributes can be -n .. n, or an attrname.
|
||||
Also computes the list of possible values, if that wasn't done yet."""
|
||||
self.target = self.attrnum(target)
|
||||
exclude = map(self.attrnum, exclude)
|
||||
if inputs:
|
||||
self.inputs = removall(self.target, inputs)
|
||||
else:
|
||||
self.inputs = [a for a in self.attrs
|
||||
if a is not self.target and a not in exclude]
|
||||
if not self.values:
|
||||
self.values = map(unique, zip(*self.examples))
|
||||
|
||||
def add_example(self, example):
|
||||
"""Add an example to the list of examples, checking it first."""
|
||||
self.check_example(example)
|
||||
self.examples.append(example)
|
||||
|
||||
def check_example(self, example):
|
||||
"""Raise ValueError if example has any invalid values."""
|
||||
if self.values:
|
||||
for a in self.attrs:
|
||||
if example[a] not in self.values[a]:
|
||||
raise ValueError('Bad value %s for attribute %s in %s' %
|
||||
(example[a], self.attrnames[a], example))
|
||||
|
||||
def attrnum(self, attr):
|
||||
"Returns the number used for attr, which can be a name, or -n .. n."
|
||||
if attr < 0:
|
||||
return len(self.attrs) + attr
|
||||
elif isinstance(attr, str):
|
||||
return self.attrnames.index(attr)
|
||||
else:
|
||||
return attr
|
||||
|
||||
def sanitize(self, example):
|
||||
"Return a copy of example, with non-input attributes replaced by 0."
|
||||
return [i in self.inputs and example[i] for i in range(len(example))]
|
||||
|
||||
def __repr__(self):
|
||||
return '<DataSet(%s): %d examples, %d attributes>' % (
|
||||
self.name, len(self.examples), len(self.attrs))
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def parse_csv(input, delim=','):
|
||||
r"""Input is a string consisting of lines, each line has comma-delimited
|
||||
fields. Convert this into a list of lists. Blank lines are skipped.
|
||||
Fields that look like numbers are converted to numbers.
|
||||
The delim defaults to ',' but '\t' and None are also reasonable values.
|
||||
>>> parse_csv('1, 2, 3 \n 0, 2, na')
|
||||
[[1, 2, 3], [0, 2, 'na']]
|
||||
"""
|
||||
lines = [line for line in input.splitlines() if line.strip() is not '']
|
||||
return [map(num_or_str, line.split(delim)) for line in lines]
|
||||
|
||||
def rms_error(predictions, targets):
|
||||
return math.sqrt(ms_error(predictions, targets))
|
||||
|
||||
def ms_error(predictions, targets):
|
||||
return mean([(p - t)**2 for p, t in zip(predictions, targets)])
|
||||
|
||||
def mean_error(predictions, targets):
|
||||
return mean([abs(p - t) for p, t in zip(predictions, targets)])
|
||||
|
||||
def mean_boolean_error(predictions, targets):
|
||||
return mean([(p != t) for p, t in zip(predictions, targets)])
|
||||
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class Learner:
|
||||
"""A Learner, or Learning Algorithm, can be trained with a dataset,
|
||||
and then asked to predict the target attribute of an example."""
|
||||
|
||||
def train(self, dataset):
|
||||
self.dataset = dataset
|
||||
|
||||
def predict(self, example):
|
||||
abstract
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class MajorityLearner(Learner):
|
||||
"""A very dumb algorithm: always pick the result that was most popular
|
||||
in the training data. Makes a baseline for comparison."""
|
||||
|
||||
def train(self, dataset):
|
||||
"Find the target value that appears most often."
|
||||
self.most_popular = mode([e[dataset.target] for e in dataset.examples])
|
||||
|
||||
def predict(self, example):
|
||||
"Always return same result: the most popular from the training set."
|
||||
return self.most_popular
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class NaiveBayesLearner(Learner):
|
||||
|
||||
def train(self, dataset):
|
||||
"""Just count the target/attr/val occurences.
|
||||
Count how many times each value of each attribute occurs.
|
||||
Store count in N[targetvalue][attr][val]. Let N[attr][None] be the
|
||||
sum over all vals."""
|
||||
N = {}
|
||||
## Initialize to 0
|
||||
for gv in self.dataset.values[self.dataset.target]:
|
||||
N[gv] = {}
|
||||
for attr in self.dataset.attrs:
|
||||
N[gv][attr] = {}
|
||||
for val in self.dataset.values[attr]:
|
||||
N[gv][attr][val] = 0
|
||||
N[gv][attr][None] = 0
|
||||
## Go thru examples
|
||||
for example in self.dataset.examples:
|
||||
Ngv = N[example[self.dataset.target]]
|
||||
for attr in self.dataset.attrs:
|
||||
Ngv[attr][example[attr]] += 1
|
||||
Ngv[attr][None] += 1
|
||||
self._N = N
|
||||
|
||||
def N(self, targetval, attr, attrval):
|
||||
"Return the count in the training data of this combination."
|
||||
try:
|
||||
return self._N[targetval][attr][attrval]
|
||||
except KeyError:
|
||||
return 0
|
||||
|
||||
def P(self, targetval, attr, attrval):
|
||||
"""Smooth the raw counts to give a probability estimate.
|
||||
Estimate adds 1 to numerator and len(possible vals) to denominator."""
|
||||
return ((self.N(targetval, attr, attrval) + 1.0) /
|
||||
(self.N(targetval, attr, None) + len(self.dataset.values[attr])))
|
||||
|
||||
def predict(self, example):
|
||||
"""Predict the target value for example. Consider each possible value,
|
||||
choose the most likely, by looking at each attribute independently."""
|
||||
possible_values = self.dataset.values[self.dataset.target]
|
||||
def class_probability(targetval):
|
||||
return product([self.P(targetval, a, example[a])
|
||||
for a in self.dataset.inputs], 1)
|
||||
return argmax(possible_values, class_probability)
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class NearestNeighborLearner(Learner):
|
||||
|
||||
def __init__(self, k=1):
|
||||
"k-NearestNeighbor: the k nearest neighbors vote."
|
||||
self.k = k
|
||||
|
||||
def predict(self, example):
|
||||
"""With k=1, find the point closest to example.
|
||||
With k>1, find k closest, and have them vote for the best."""
|
||||
if self.k == 1:
|
||||
neighbor = argmin(self.dataset.examples,
|
||||
lambda e: self.distance(e, example))
|
||||
return neighbor[self.dataset.target]
|
||||
else:
|
||||
## Maintain a sorted list of (distance, example) pairs.
|
||||
## For very large k, a PriorityQueue would be better
|
||||
best = []
|
||||
for e in examples:
|
||||
d = self.distance(e, example)
|
||||
if len(best) < k:
|
||||
e.append((d, e))
|
||||
elif d < best[-1][0]:
|
||||
best[-1] = (d, e)
|
||||
best.sort()
|
||||
return mode([e[self.dataset.target] for (d, e) in best])
|
||||
|
||||
def distance(self, e1, e2):
|
||||
return mean_boolean_error(e1, e2)
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class DecisionTree:
|
||||
"""A DecisionTree holds an attribute that is being tested, and a
|
||||
dict of {attrval: Tree} entries. If Tree here is not a DecisionTree
|
||||
then it is the final classification of the example."""
|
||||
|
||||
def __init__(self, attr, attrname=None, branches=None):
|
||||
"Initialize by saying what attribute this node tests."
|
||||
update(self, attr=attr, attrname=attrname or attr,
|
||||
branches=branches or {})
|
||||
|
||||
def predict(self, example):
|
||||
"Given an example, use the tree to classify the example."
|
||||
child = self.branches[example[self.attr]]
|
||||
if isinstance(child, DecisionTree):
|
||||
return child.predict(example)
|
||||
else:
|
||||
return child
|
||||
|
||||
def add(self, val, subtree):
|
||||
"Add a branch. If self.attr = val, go to the given subtree."
|
||||
self.branches[val] = subtree
|
||||
return self
|
||||
|
||||
def display(self, indent=0):
|
||||
name = self.attrname
|
||||
print 'Test', name
|
||||
for (val, subtree) in self.branches.items():
|
||||
print ' '*4*indent, name, '=', val, '==>',
|
||||
if isinstance(subtree, DecisionTree):
|
||||
subtree.display(indent+1)
|
||||
else:
|
||||
print 'RESULT = ', subtree
|
||||
|
||||
def __repr__(self):
|
||||
return 'DecisionTree(%r, %r, %r)' % (
|
||||
self.attr, self.attrname, self.branches)
|
||||
|
||||
Yes, No = True, False
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class DecisionTreeLearner(Learner):
|
||||
|
||||
def predict(self, example):
|
||||
if isinstance(self.dt, DecisionTree):
|
||||
return self.dt.predict(example)
|
||||
else:
|
||||
return self.dt
|
||||
|
||||
def train(self, dataset):
|
||||
self.dataset = dataset
|
||||
self.attrnames = dataset.attrnames
|
||||
self.dt = self.decision_tree_learning(dataset.examples, dataset.inputs)
|
||||
|
||||
def decision_tree_learning(self, examples, attrs, default=None):
|
||||
if len(examples) == 0:
|
||||
return default
|
||||
elif self.all_same_class(examples):
|
||||
return examples[0][self.dataset.target]
|
||||
elif len(attrs) == 0:
|
||||
return self.majority_value(examples)
|
||||
else:
|
||||
best = self.choose_attribute(attrs, examples)
|
||||
tree = DecisionTree(best, self.attrnames[best])
|
||||
for (v, examples_i) in self.split_by(best, examples):
|
||||
subtree = self.decision_tree_learning(examples_i,
|
||||
removeall(best, attrs), self.majority_value(examples))
|
||||
tree.add(v, subtree)
|
||||
return tree
|
||||
|
||||
def choose_attribute(self, attrs, examples):
|
||||
"Choose the attribute with the highest information gain."
|
||||
return argmax(attrs, lambda a: self.information_gain(a, examples))
|
||||
|
||||
def all_same_class(self, examples):
|
||||
"Are all these examples in the same target class?"
|
||||
target = self.dataset.target
|
||||
class0 = examples[0][target]
|
||||
for e in examples:
|
||||
if e[target] != class0: return False
|
||||
return True
|
||||
|
||||
def majority_value(self, examples):
|
||||
"""Return the most popular target value for this set of examples.
|
||||
(If target is binary, this is the majority; otherwise plurality.)"""
|
||||
g = self.dataset.target
|
||||
return argmax(self.dataset.values[g],
|
||||
lambda v: self.count(g, v, examples))
|
||||
|
||||
def count(self, attr, val, examples):
|
||||
return count_if(lambda e: e[attr] == val, examples)
|
||||
|
||||
def information_gain(self, attr, examples):
|
||||
def I(examples):
|
||||
target = self.dataset.target
|
||||
return information_content([self.count(target, v, examples)
|
||||
for v in self.dataset.values[target]])
|
||||
N = float(len(examples))
|
||||
remainder = 0
|
||||
for (v, examples_i) in self.split_by(attr, examples):
|
||||
remainder += (len(examples_i) / N) * I(examples_i)
|
||||
return I(examples) - remainder
|
||||
|
||||
def split_by(self, attr, examples=None):
|
||||
"Return a list of (val, examples) pairs for each val of attr."
|
||||
if examples == None:
|
||||
examples = self.dataset.examples
|
||||
return [(v, [e for e in examples if e[attr] == v])
|
||||
for v in self.dataset.values[attr]]
|
||||
|
||||
def information_content(values):
|
||||
"Number of bits to represent the probability distribution in values."
|
||||
# If the values do not sum to 1, normalize them to make them a Prob. Dist.
|
||||
values = removeall(0, values)
|
||||
s = float(sum(values))
|
||||
if s != 1.0: values = [v/s for v in values]
|
||||
return sum([- v * log2(v) for v in values])
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
### A decision list is implemented as a list of (test, value) pairs.
|
||||
|
||||
class DecisionListLearner(Learner):
|
||||
|
||||
def train(self, dataset):
|
||||
self.dataset = dataset
|
||||
self.attrnames = dataset.attrnames
|
||||
self.dl = self.decision_list_learning(Set(dataset.examples))
|
||||
|
||||
def decision_list_learning(self, examples):
|
||||
"""[Fig. 18.14]"""
|
||||
if not examples:
|
||||
return [(True, No)]
|
||||
t, o, examples_t = self.find_examples(examples)
|
||||
if not t:
|
||||
raise Failure
|
||||
return [(t, o)] + self.decision_list_learning(examples - examples_t)
|
||||
|
||||
def find_examples(self, examples):
|
||||
"""Find a set of examples that all have the same outcome under some test.
|
||||
Return a tuple of the test, outcome, and examples."""
|
||||
NotImplemented
|
||||
#______________________________________________________________________________
|
||||
|
||||
class NeuralNetLearner(Learner):
|
||||
"""Layered feed-forward network."""
|
||||
|
||||
def __init__(self, sizes):
|
||||
self.activations = map(lambda n: [0.0 for i in range(n)], sizes)
|
||||
self.weights = []
|
||||
|
||||
def train(self, dataset):
|
||||
NotImplemented
|
||||
|
||||
def predict(self, example):
|
||||
NotImplemented
|
||||
|
||||
class NNUnit:
|
||||
"""Unit of a neural net."""
|
||||
def __init__(self):
|
||||
NotImplemented
|
||||
|
||||
class PerceptronLearner(NeuralNetLearner):
|
||||
|
||||
def predict(self, example):
|
||||
return sum([])
|
||||
#______________________________________________________________________________
|
||||
|
||||
class Linearlearner(Learner):
|
||||
"""Fit a linear model to the data."""
|
||||
|
||||
NotImplemented
|
||||
#______________________________________________________________________________
|
||||
|
||||
class EnsembleLearner(Learner):
|
||||
"""Given a list of learning algorithms, have them vote."""
|
||||
|
||||
def __init__(self, learners=[]):
|
||||
self.learners=learners
|
||||
|
||||
def train(self, dataset):
|
||||
for learner in self.learners:
|
||||
learner.train(dataset)
|
||||
|
||||
def predict(self, example):
|
||||
return mode([learner.predict(example) for learner in self.learners])
|
||||
|
||||
#_____________________________________________________________________________
|
||||
# Functions for testing learners on examples
|
||||
|
||||
def test(learner, dataset, examples=None, verbose=0):
|
||||
"""Return the proportion of the examples that are correctly predicted.
|
||||
Assumes the learner has already been trained."""
|
||||
if examples == None: examples = dataset.examples
|
||||
if len(examples) == 0: return 0.0
|
||||
right = 0.0
|
||||
for example in examples:
|
||||
desired = example[dataset.target]
|
||||
output = learner.predict(dataset.sanitize(example))
|
||||
if output == desired:
|
||||
right += 1
|
||||
if verbose >= 2:
|
||||
print ' OK: got %s for %s' % (desired, example)
|
||||
elif verbose:
|
||||
print 'WRONG: got %s, expected %s for %s' % (
|
||||
output, desired, example)
|
||||
return right / len(examples)
|
||||
|
||||
def train_and_test(learner, dataset, start, end):
|
||||
"""Reserve dataset.examples[start:end] for test; train on the remainder.
|
||||
Return the proportion of examples correct on the test examples."""
|
||||
examples = dataset.examples
|
||||
try:
|
||||
dataset.examples = examples[:start] + examples[end:]
|
||||
learner.dataset = dataset
|
||||
learner.train(dataset)
|
||||
return test(learner, dataset, examples[start:end])
|
||||
finally:
|
||||
dataset.examples = examples
|
||||
|
||||
def cross_validation(learner, dataset, k=10, trials=1):
|
||||
"""Do k-fold cross_validate and return their mean.
|
||||
That is, keep out 1/k of the examples for testing on each of k runs.
|
||||
Shuffle the examples first; If trials>1, average over several shuffles."""
|
||||
if k == None:
|
||||
k = len(dataset.examples)
|
||||
if trials > 1:
|
||||
return mean([cross_validation(learner, dataset, k, trials=1)
|
||||
for t in range(trials)])
|
||||
else:
|
||||
n = len(dataset.examples)
|
||||
random.shuffle(dataset.examples)
|
||||
return mean([train_and_test(learner, dataset, i*(n/k), (i+1)*(n/k))
|
||||
for i in range(k)])
|
||||
|
||||
def leave1out(learner, dataset):
|
||||
"Leave one out cross-validation over the dataset."
|
||||
return cross_validation(learner, dataset, k=len(dataset.examples))
|
||||
|
||||
def learningcurve(learner, dataset, trials=10, sizes=None):
|
||||
if sizes == None:
|
||||
sizes = range(2, len(dataset.examples)-10, 2)
|
||||
def score(learner, size):
|
||||
random.shuffle(dataset.examples)
|
||||
return train_and_test(learner, dataset, 0, size)
|
||||
return [(size, mean([score(learner, size) for t in range(trials)]))
|
||||
for size in sizes]
|
||||
|
||||
#______________________________________________________________________________
|
||||
# The rest of this file gives Data sets for machine learning problems.
|
||||
|
||||
orings = DataSet(name='orings', target='Distressed',
|
||||
attrnames="Rings Distressed Temp Pressure Flightnum")
|
||||
|
||||
|
||||
zoo = DataSet(name='zoo', target='type', exclude=['name'],
|
||||
attrnames="name hair feathers eggs milk airborne aquatic " +
|
||||
"predator toothed backbone breathes venomous fins legs tail " +
|
||||
"domestic catsize type")
|
||||
|
||||
|
||||
iris = DataSet(name="iris", target="class",
|
||||
attrnames="sepal-len sepal-width petal-len petal-width class")
|
||||
|
||||
#______________________________________________________________________________
|
||||
# The Restaurant example from Fig. 18.2
|
||||
|
||||
def RestaurantDataSet(examples=None):
|
||||
"Build a DataSet of Restaurant waiting examples."
|
||||
return DataSet(name='restaurant', target='Wait', examples=examples,
|
||||
attrnames='Alternate Bar Fri/Sat Hungry Patrons Price '
|
||||
+ 'Raining Reservation Type WaitEstimate Wait')
|
||||
|
||||
restaurant = RestaurantDataSet()
|
||||
|
||||
def T(attrname, branches):
|
||||
return DecisionTree(restaurant.attrnum(attrname), attrname, branches)
|
||||
|
||||
Fig[18,2] = T('Patrons',
|
||||
{'None': 'No', 'Some': 'Yes', 'Full':
|
||||
T('WaitEstimate',
|
||||
{'>60': 'No', '0-10': 'Yes',
|
||||
'30-60':
|
||||
T('Alternate', {'No':
|
||||
T('Reservation', {'Yes': 'Yes', 'No':
|
||||
T('Bar', {'No':'No',
|
||||
'Yes':'Yes'})}),
|
||||
'Yes':
|
||||
T('Fri/Sat', {'No': 'No', 'Yes': 'Yes'})}),
|
||||
'10-30':
|
||||
T('Hungry', {'No': 'Yes', 'Yes':
|
||||
T('Alternate',
|
||||
{'No': 'Yes', 'Yes':
|
||||
T('Raining', {'No': 'No', 'Yes': 'Yes'})})})})})
|
||||
|
||||
def SyntheticRestaurant(n=20):
|
||||
"Generate a DataSet with n examples."
|
||||
def gen():
|
||||
example = map(random.choice, restaurant.values)
|
||||
example[restaurant.target] = Fig[18,2].predict(example)
|
||||
return example
|
||||
return RestaurantDataSet([gen() for i in range(n)])
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Artificial, generated examples.
|
||||
|
||||
def Majority(k, n):
|
||||
"""Return a DataSet with n k-bit examples of the majority problem:
|
||||
k random bits followed by a 1 if more than half the bits are 1, else 0."""
|
||||
examples = []
|
||||
for i in range(n):
|
||||
bits = [random.choice([0, 1]) for i in range(k)]
|
||||
bits.append(sum(bits) > k/2)
|
||||
examples.append(bits)
|
||||
return DataSet(name="majority", examples=examples)
|
||||
|
||||
def Parity(k, n, name="parity"):
|
||||
"""Return a DataSet with n k-bit examples of the parity problem:
|
||||
k random bits followed by a 1 if an odd number of bits are 1, else 0."""
|
||||
examples = []
|
||||
for i in range(n):
|
||||
bits = [random.choice([0, 1]) for i in range(k)]
|
||||
bits.append(sum(bits) % 2)
|
||||
examples.append(bits)
|
||||
return DataSet(name=name, examples=examples)
|
||||
|
||||
def Xor(n):
|
||||
"""Return a DataSet with n examples of 2-input xor."""
|
||||
return Parity(2, n, name="xor")
|
||||
|
||||
def ContinuousXor(n):
|
||||
"2 inputs are chosen uniformly form (0.0 .. 2.0]; output is xor of ints."
|
||||
examples = []
|
||||
for i in range(n):
|
||||
x, y = [random.uniform(0.0, 2.0) for i in '12']
|
||||
examples.append([x, y, int(x) != int(y)])
|
||||
return DataSet(name="continuous xor", examples=examples)
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def compare(algorithms=[MajorityLearner, NaiveBayesLearner,
|
||||
NearestNeighborLearner, DecisionTreeLearner],
|
||||
datasets=[iris, orings, zoo, restaurant, SyntheticRestaurant(20),
|
||||
Majority(7, 100), Parity(7, 100), Xor(100)],
|
||||
k=10, trials=1):
|
||||
"""Compare various learners on various datasets using cross-validation.
|
||||
Print results as a table."""
|
||||
print_table([[a.__name__.replace('Learner','')] +
|
||||
[cross_validation(a(), d, k, trials) for d in datasets]
|
||||
for a in algorithms],
|
||||
header=[''] + [d.name[0:7] for d in datasets], round=2)
|
||||
|
@ -0,0 +1,888 @@
|
||||
"""Representations and Inference for Logic (Chapters 7-10)
|
||||
|
||||
Covers both Propositional and First-Order Logic. First we have four
|
||||
important data types:
|
||||
|
||||
KB Abstract class holds a knowledge base of logical expressions
|
||||
KB_Agent Abstract class subclasses agents.Agent
|
||||
Expr A logical expression
|
||||
substitution Implemented as a dictionary of var:value pairs, {x:1, y:x}
|
||||
|
||||
Be careful: some functions take an Expr as argument, and some take a KB.
|
||||
Then we implement various functions for doing logical inference:
|
||||
|
||||
pl_true Evaluate a propositional logical sentence in a model
|
||||
tt_entails Say if a statement is entailed by a KB
|
||||
pl_resolution Do resolution on propositional sentences
|
||||
dpll_satisfiable See if a propositional sentence is satisfiable
|
||||
WalkSAT (not yet implemented)
|
||||
|
||||
And a few other functions:
|
||||
|
||||
to_cnf Convert to conjunctive normal form
|
||||
unify Do unification of two FOL sentences
|
||||
diff, simp Symbolic differentiation and simplification
|
||||
"""
|
||||
|
||||
from __future__ import generators
|
||||
import re
|
||||
import agents
|
||||
from utils import *
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class KB:
|
||||
"""A Knowledge base to which you can tell and ask sentences.
|
||||
To create a KB, first subclass this class and implement
|
||||
tell, ask_generator, and retract. Why ask_generator instead of ask?
|
||||
The book is a bit vague on what ask means --
|
||||
For a Propositional Logic KB, ask(P & Q) returns True or False, but for an
|
||||
FOL KB, something like ask(Brother(x, y)) might return many substitutions
|
||||
such as {x: Cain, y: Able}, {x: Able, y: Cain}, {x: George, y: Jeb}, etc.
|
||||
So ask_generator generates these one at a time, and ask either returns the
|
||||
first one or returns False."""
|
||||
|
||||
def __init__(self, sentence=None):
|
||||
abstract
|
||||
|
||||
def tell(self, sentence):
|
||||
"Add the sentence to the KB"
|
||||
abstract
|
||||
|
||||
def ask(self, query):
|
||||
"""Ask returns a substitution that makes the query true, or
|
||||
it returns False. It is implemented in terms of ask_generator."""
|
||||
try:
|
||||
return self.ask_generator(query).next()
|
||||
except StopIteration:
|
||||
return False
|
||||
|
||||
def ask_generator(self, query):
|
||||
"Yield all the substitutions that make query true."
|
||||
abstract
|
||||
|
||||
def retract(self, sentence):
|
||||
"Remove the sentence from the KB"
|
||||
abstract
|
||||
|
||||
|
||||
class PropKB(KB):
|
||||
"A KB for Propositional Logic. Inefficient, with no indexing."
|
||||
|
||||
def __init__(self, sentence=None):
|
||||
self.clauses = []
|
||||
if sentence:
|
||||
self.tell(sentence)
|
||||
|
||||
def tell(self, sentence):
|
||||
"Add the sentence's clauses to the KB"
|
||||
self.clauses.extend(conjuncts(to_cnf(sentence)))
|
||||
|
||||
def ask_generator(self, query):
|
||||
"Yield the empty substitution if KB implies query; else False"
|
||||
if not tt_entails(Expr('&', *self.clauses), query):
|
||||
return
|
||||
yield {}
|
||||
|
||||
def retract(self, sentence):
|
||||
"Remove the sentence's clauses from the KB"
|
||||
for c in conjuncts(to_cnf(sentence)):
|
||||
if c in self.clauses:
|
||||
self.clauses.remove(c)
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class KB_Agent(agents.Agent):
|
||||
"""A generic logical knowledge-based agent. [Fig. 7.1]"""
|
||||
def __init__(self, KB):
|
||||
t = 0
|
||||
def program(percept):
|
||||
KB.tell(self.make_percept_sentence(percept, t))
|
||||
action = KB.ask(self.make_action_query(t))
|
||||
KB.tell(self.make_action_sentence(action, t))
|
||||
t = t + 1
|
||||
return action
|
||||
self.program = program
|
||||
|
||||
def make_percept_sentence(self, percept, t):
|
||||
return(Expr("Percept")(percept, t))
|
||||
|
||||
def make_action_query(self, t):
|
||||
return(expr("ShouldDo(action, %d)" % t))
|
||||
|
||||
def make_action_sentence(self, action, t):
|
||||
return(Expr("Did")(action, t))
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class Expr:
|
||||
"""A symbolic mathematical expression. We use this class for logical
|
||||
expressions, and for terms within logical expressions. In general, an
|
||||
Expr has an op (operator) and a list of args. The op can be:
|
||||
Null-ary (no args) op:
|
||||
A number, representing the number itself. (e.g. Expr(42) => 42)
|
||||
A symbol, representing a variable or constant (e.g. Expr('F') => F)
|
||||
Unary (1 arg) op:
|
||||
'~', '-', representing NOT, negation (e.g. Expr('~', Expr('P')) => ~P)
|
||||
Binary (2 arg) op:
|
||||
'>>', '<<', representing forward and backward implication
|
||||
'+', '-', '*', '/', '**', representing arithmetic operators
|
||||
'<', '>', '>=', '<=', representing comparison operators
|
||||
'<=>', '^', representing logical equality and XOR
|
||||
N-ary (0 or more args) op:
|
||||
'&', '|', representing conjunction and disjunction
|
||||
A symbol, representing a function term or FOL proposition
|
||||
|
||||
Exprs can be constructed with operator overloading: if x and y are Exprs,
|
||||
then so are x + y and x & y, etc. Also, if F and x are Exprs, then so is
|
||||
F(x); it works by overloading the __call__ method of the Expr F. Note
|
||||
that in the Expr that is created by F(x), the op is the str 'F', not the
|
||||
Expr F. See http://www.python.org/doc/current/ref/specialnames.html
|
||||
to learn more about operator overloading in Python.
|
||||
|
||||
WARNING: x == y and x != y are NOT Exprs. The reason is that we want
|
||||
to write code that tests 'if x == y:' and if x == y were the same
|
||||
as Expr('==', x, y), then the result would always be true; not what a
|
||||
programmer would expect. But we still need to form Exprs representing
|
||||
equalities and disequalities. We concentrate on logical equality (or
|
||||
equivalence) and logical disequality (or XOR). You have 3 choices:
|
||||
(1) Expr('<=>', x, y) and Expr('^', x, y)
|
||||
Note that ^ is bitwose XOR in Python (and Java and C++)
|
||||
(2) expr('x <=> y') and expr('x =/= y').
|
||||
See the doc string for the function expr.
|
||||
(3) (x % y) and (x ^ y).
|
||||
It is very ugly to have (x % y) mean (x <=> y), but we need
|
||||
SOME operator to make (2) work, and this seems the best choice.
|
||||
|
||||
WARNING: if x is an Expr, then so is x + 1, because the int 1 gets
|
||||
coerced to an Expr by the constructor. But 1 + x is an error, because
|
||||
1 doesn't know how to add an Expr. (Adding an __radd__ method to Expr
|
||||
wouldn't help, because int.__add__ is still called first.) Therefore,
|
||||
you should use Expr(1) + x instead, or ONE + x, or expr('1 + x').
|
||||
"""
|
||||
|
||||
def __init__(self, op, *args):
|
||||
"Op is a string or number; args are Exprs (or are coerced to Exprs)."
|
||||
assert isinstance(op, str) or (isnumber(op) and not args)
|
||||
self.op = num_or_str(op)
|
||||
self.args = map(expr, args) ## Coerce args to Exprs
|
||||
|
||||
def __call__(self, *args):
|
||||
"""Self must be a symbol with no args, such as Expr('F'). Create a new
|
||||
Expr with 'F' as op and the args as arguments."""
|
||||
assert is_symbol(self.op) and not self.args
|
||||
return Expr(self.op, *args)
|
||||
|
||||
def __repr__(self):
|
||||
"Show something like 'P' or 'P(x, y)', or '~P' or '(P | Q | R)'"
|
||||
if len(self.args) == 0: # Constant or proposition with arity 0
|
||||
return str(self.op)
|
||||
elif is_symbol(self.op): # Functional or Propositional operator
|
||||
return '%s(%s)' % (self.op, ', '.join(map(repr, self.args)))
|
||||
elif len(self.args) == 1: # Prefix operator
|
||||
return self.op + repr(self.args[0])
|
||||
else: # Infix operator
|
||||
return '(%s)' % (' '+self.op+' ').join(map(repr, self.args))
|
||||
|
||||
def __eq__(self, other):
|
||||
"""x and y are equal iff their ops and args are equal."""
|
||||
return (other is self) or (isinstance(other, Expr)
|
||||
and self.op == other.op and self.args == other.args)
|
||||
|
||||
def __hash__(self):
|
||||
"Need a hash method so Exprs can live in dicts."
|
||||
return hash(self.op) ^ hash(tuple(self.args))
|
||||
|
||||
# See http://www.python.org/doc/current/lib/module-operator.html
|
||||
# Not implemented: not, abs, pos, concat, contains, *item, *slice
|
||||
def __lt__(self, other): return Expr('<', self, other)
|
||||
def __le__(self, other): return Expr('<=', self, other)
|
||||
def __ge__(self, other): return Expr('>=', self, other)
|
||||
def __gt__(self, other): return Expr('>', self, other)
|
||||
def __add__(self, other): return Expr('+', self, other)
|
||||
def __sub__(self, other): return Expr('-', self, other)
|
||||
def __and__(self, other): return Expr('&', self, other)
|
||||
def __div__(self, other): return Expr('/', self, other)
|
||||
def __truediv__(self, other):return Expr('/', self, other)
|
||||
def __invert__(self): return Expr('~', self)
|
||||
def __lshift__(self, other): return Expr('<<', self, other)
|
||||
def __rshift__(self, other): return Expr('>>', self, other)
|
||||
def __mul__(self, other): return Expr('*', self, other)
|
||||
def __neg__(self): return Expr('-', self)
|
||||
def __or__(self, other): return Expr('|', self, other)
|
||||
def __pow__(self, other): return Expr('**', self, other)
|
||||
def __xor__(self, other): return Expr('^', self, other)
|
||||
def __mod__(self, other): return Expr('<=>', self, other) ## (x % y)
|
||||
|
||||
|
||||
|
||||
def expr(s):
|
||||
"""Create an Expr representing a logic expression by parsing the input
|
||||
string. Symbols and numbers are automatically converted to Exprs.
|
||||
In addition you can use alternative spellings of these operators:
|
||||
'x ==> y' parses as (x >> y) # Implication
|
||||
'x <== y' parses as (x << y) # Reverse implication
|
||||
'x <=> y' parses as (x % y) # Logical equivalence
|
||||
'x =/= y' parses as (x ^ y) # Logical disequality (xor)
|
||||
But BE CAREFUL; precedence of implication is wrong. expr('P & Q ==> R & S')
|
||||
is ((P & (Q >> R)) & S); so you must use expr('(P & Q) ==> (R & S)').
|
||||
>>> expr('P <=> Q(1)')
|
||||
(P <=> Q(1))
|
||||
>>> expr('P & Q | ~R(x, F(x))')
|
||||
((P & Q) | ~R(x, F(x)))
|
||||
"""
|
||||
if isinstance(s, Expr): return s
|
||||
if isnumber(s): return Expr(s)
|
||||
## Replace the alternative spellings of operators with canonical spellings
|
||||
s = s.replace('==>', '>>').replace('<==', '<<')
|
||||
s = s.replace('<=>', '%').replace('=/=', '^')
|
||||
## Replace a symbol or number, such as 'P' with 'Expr("P")'
|
||||
s = re.sub(r'([a-zA-Z0-9_.]+)', r'Expr("\1")', s)
|
||||
## Now eval the string. (A security hole; do not use with an adversary.)
|
||||
return eval(s, {'Expr':Expr})
|
||||
|
||||
def is_symbol(s):
|
||||
"A string s is a symbol if it starts with an alphabetic char."
|
||||
return isinstance(s, str) and s[0].isalpha()
|
||||
|
||||
def is_var_symbol(s):
|
||||
"A logic variable symbol is an initial-lowercase string."
|
||||
return is_symbol(s) and s[0].islower()
|
||||
|
||||
def is_prop_symbol(s):
|
||||
"""A proposition logic symbol is an initial-uppercase string other than
|
||||
TRUE or FALSE."""
|
||||
return is_symbol(s) and s[0].isupper() and s != 'TRUE' and s != 'FALSE'
|
||||
|
||||
|
||||
## Useful constant Exprs used in examples and code:
|
||||
TRUE, FALSE, ZERO, ONE, TWO = map(Expr, ['TRUE', 'FALSE', 0, 1, 2])
|
||||
A, B, C, F, G, P, Q, x, y, z = map(Expr, 'ABCFGPQxyz')
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def tt_entails(kb, alpha):
|
||||
"""Use truth tables to determine if KB entails sentence alpha. [Fig. 7.10]
|
||||
>>> tt_entails(expr('P & Q'), expr('Q'))
|
||||
True
|
||||
"""
|
||||
return tt_check_all(kb, alpha, prop_symbols(kb & alpha), {})
|
||||
|
||||
def tt_check_all(kb, alpha, symbols, model):
|
||||
"Auxiliary routine to implement tt_entails."
|
||||
if not symbols:
|
||||
if pl_true(kb, model): return pl_true(alpha, model)
|
||||
else: return True
|
||||
assert result != None
|
||||
else:
|
||||
P, rest = symbols[0], symbols[1:]
|
||||
return (tt_check_all(kb, alpha, rest, extend(model, P, True)) and
|
||||
tt_check_all(kb, alpha, rest, extend(model, P, False)))
|
||||
|
||||
def prop_symbols(x):
|
||||
"Return a list of all propositional symbols in x."
|
||||
if not isinstance(x, Expr):
|
||||
return []
|
||||
elif is_prop_symbol(x.op):
|
||||
return [x]
|
||||
else:
|
||||
s = set(())
|
||||
for arg in x.args:
|
||||
for symbol in prop_symbols(arg):
|
||||
s.add(symbol)
|
||||
return list(s)
|
||||
|
||||
def tt_true(alpha):
|
||||
"""Is the sentence alpha a tautology? (alpha will be coerced to an expr.)
|
||||
>>> tt_true(expr("(P >> Q) <=> (~P | Q)"))
|
||||
True
|
||||
"""
|
||||
return tt_entails(TRUE, expr(alpha))
|
||||
|
||||
def pl_true(exp, model={}):
|
||||
"""Return True if the propositional logic expression is true in the model,
|
||||
and False if it is false. If the model does not specify the value for
|
||||
every proposition, this may return None to indicate 'not obvious';
|
||||
this may happen even when the expression is tautological."""
|
||||
op, args = exp.op, exp.args
|
||||
if exp == TRUE:
|
||||
return True
|
||||
elif exp == FALSE:
|
||||
return False
|
||||
elif is_prop_symbol(op):
|
||||
return model.get(exp)
|
||||
elif op == '~':
|
||||
p = pl_true(args[0], model)
|
||||
if p == None: return None
|
||||
else: return not p
|
||||
elif op == '|':
|
||||
result = False
|
||||
for arg in args:
|
||||
p = pl_true(arg, model)
|
||||
if p == True: return True
|
||||
if p == None: result = None
|
||||
return result
|
||||
elif op == '&':
|
||||
result = True
|
||||
for arg in args:
|
||||
p = pl_true(arg, model)
|
||||
if p == False: return False
|
||||
if p == None: result = None
|
||||
return result
|
||||
p, q = args
|
||||
if op == '>>':
|
||||
return pl_true(~p | q, model)
|
||||
elif op == '<<':
|
||||
return pl_true(p | ~q, model)
|
||||
pt = pl_true(p, model)
|
||||
if pt == None: return None
|
||||
qt = pl_true(q, model)
|
||||
if qt == None: return None
|
||||
if op == '<=>':
|
||||
return pt == qt
|
||||
elif op == '^':
|
||||
return pt != qt
|
||||
else:
|
||||
raise ValueError, "illegal operator in logic expression" + str(exp)
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
## Convert to Conjunctive Normal Form (CNF)
|
||||
|
||||
def to_cnf(s):
|
||||
"""Convert a propositional logical sentence s to conjunctive normal form.
|
||||
That is, of the form ((A | ~B | ...) & (B | C | ...) & ...) [p. 215]
|
||||
>>> to_cnf("~(B|C)")
|
||||
(~B & ~C)
|
||||
>>> to_cnf("B <=> (P1|P2)")
|
||||
((~P1 | B) & (~P2 | B) & (P1 | P2 | ~B))
|
||||
>>> to_cnf("a | (b & c) | d")
|
||||
((b | a | d) & (c | a | d))
|
||||
>>> to_cnf("A & (B | (D & E))")
|
||||
(A & (D | B) & (E | B))
|
||||
"""
|
||||
if isinstance(s, str): s = expr(s)
|
||||
s = eliminate_implications(s) # Steps 1, 2 from p. 215
|
||||
s = move_not_inwards(s) # Step 3
|
||||
return distribute_and_over_or(s) # Step 4
|
||||
|
||||
def eliminate_implications(s):
|
||||
"""Change >>, <<, and <=> into &, |, and ~. That is, return an Expr
|
||||
that is equivalent to s, but has only &, |, and ~ as logical operators.
|
||||
>>> eliminate_implications(A >> (~B << C))
|
||||
((~B | ~C) | ~A)
|
||||
"""
|
||||
if not s.args or is_symbol(s.op): return s ## (Atoms are unchanged.)
|
||||
args = map(eliminate_implications, s.args)
|
||||
a, b = args[0], args[-1]
|
||||
if s.op == '>>':
|
||||
return (b | ~a)
|
||||
elif s.op == '<<':
|
||||
return (a | ~b)
|
||||
elif s.op == '<=>':
|
||||
return (a | ~b) & (b | ~a)
|
||||
else:
|
||||
return Expr(s.op, *args)
|
||||
|
||||
def move_not_inwards(s):
|
||||
"""Rewrite sentence s by moving negation sign inward.
|
||||
>>> move_not_inwards(~(A | B))
|
||||
(~A & ~B)
|
||||
>>> move_not_inwards(~(A & B))
|
||||
(~A | ~B)
|
||||
>>> move_not_inwards(~(~(A | ~B) | ~~C))
|
||||
((A | ~B) & ~C)
|
||||
"""
|
||||
if s.op == '~':
|
||||
NOT = lambda b: move_not_inwards(~b)
|
||||
a = s.args[0]
|
||||
if a.op == '~': return move_not_inwards(a.args[0]) # ~~A ==> A
|
||||
if a.op =='&': return NaryExpr('|', *map(NOT, a.args))
|
||||
if a.op =='|': return NaryExpr('&', *map(NOT, a.args))
|
||||
return s
|
||||
elif is_symbol(s.op) or not s.args:
|
||||
return s
|
||||
else:
|
||||
return Expr(s.op, *map(move_not_inwards, s.args))
|
||||
|
||||
def distribute_and_over_or(s):
|
||||
"""Given a sentence s consisting of conjunctions and disjunctions
|
||||
of literals, return an equivalent sentence in CNF.
|
||||
>>> distribute_and_over_or((A & B) | C)
|
||||
((A | C) & (B | C))
|
||||
"""
|
||||
if s.op == '|':
|
||||
s = NaryExpr('|', *s.args)
|
||||
if len(s.args) == 0:
|
||||
return FALSE
|
||||
if len(s.args) == 1:
|
||||
return distribute_and_over_or(s.args[0])
|
||||
conj = find_if((lambda d: d.op == '&'), s.args)
|
||||
if not conj:
|
||||
return NaryExpr(s.op, *s.args)
|
||||
others = [a for a in s.args if a is not conj]
|
||||
if len(others) == 1:
|
||||
rest = others[0]
|
||||
else:
|
||||
rest = NaryExpr('|', *others)
|
||||
return NaryExpr('&', *map(distribute_and_over_or,
|
||||
[(c|rest) for c in conj.args]))
|
||||
elif s.op == '&':
|
||||
return NaryExpr('&', *map(distribute_and_over_or, s.args))
|
||||
else:
|
||||
return s
|
||||
|
||||
_NaryExprTable = {'&':TRUE, '|':FALSE, '+':ZERO, '*':ONE}
|
||||
|
||||
def NaryExpr(op, *args):
|
||||
"""Create an Expr, but with an nary, associative op, so we can promote
|
||||
nested instances of the same op up to the top level.
|
||||
>>> NaryExpr('&', (A&B),(B|C),(B&C))
|
||||
(A & B & (B | C) & B & C)
|
||||
"""
|
||||
arglist = []
|
||||
for arg in args:
|
||||
if arg.op == op: arglist.extend(arg.args)
|
||||
else: arglist.append(arg)
|
||||
if len(args) == 1:
|
||||
return args[0]
|
||||
elif len(args) == 0:
|
||||
return _NaryExprTable[op]
|
||||
else:
|
||||
return Expr(op, *arglist)
|
||||
|
||||
def conjuncts(s):
|
||||
"""Return a list of the conjuncts in the sentence s.
|
||||
>>> conjuncts(A & B)
|
||||
[A, B]
|
||||
>>> conjuncts(A | B)
|
||||
[(A | B)]
|
||||
"""
|
||||
if isinstance(s, Expr) and s.op == '&':
|
||||
return s.args
|
||||
else:
|
||||
return [s]
|
||||
|
||||
def disjuncts(s):
|
||||
"""Return a list of the disjuncts in the sentence s.
|
||||
>>> disjuncts(A | B)
|
||||
[A, B]
|
||||
>>> disjuncts(A & B)
|
||||
[(A & B)]
|
||||
"""
|
||||
if isinstance(s, Expr) and s.op == '|':
|
||||
return s.args
|
||||
else:
|
||||
return [s]
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def pl_resolution(KB, alpha):
|
||||
"Propositional Logic Resolution: say if alpha follows from KB. [Fig. 7.12]"
|
||||
clauses = KB.clauses + conjuncts(to_cnf(~alpha))
|
||||
new = set()
|
||||
while True:
|
||||
n = len(clauses)
|
||||
pairs = [(clauses[i], clauses[j]) for i in range(n) for j in range(i+1, n)]
|
||||
for (ci, cj) in pairs:
|
||||
resolvents = pl_resolve(ci, cj)
|
||||
if FALSE in resolvents: return True
|
||||
new.union_update(set(resolvents))
|
||||
if new.issubset(set(clauses)): return False
|
||||
for c in new:
|
||||
if c not in clauses: clauses.append(c)
|
||||
|
||||
def pl_resolve(ci, cj):
|
||||
"""Return all clauses that can be obtained by resolving clauses ci and cj.
|
||||
>>> pl_resolve(to_cnf(A|B|C), to_cnf(~B|~C|F))
|
||||
[(A | C | ~C | F), (A | B | ~B | F)]
|
||||
"""
|
||||
clauses = []
|
||||
for di in disjuncts(ci):
|
||||
for dj in disjuncts(cj):
|
||||
if di == ~dj or ~di == dj:
|
||||
dnew = unique(removeall(di, disjuncts(ci)) +
|
||||
removeall(dj, disjuncts(cj)))
|
||||
clauses.append(NaryExpr('|', *dnew))
|
||||
return clauses
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class PropHornKB(PropKB):
|
||||
"A KB of Propositional Horn clauses."
|
||||
|
||||
def tell(self, sentence):
|
||||
"Add a Horn Clauses to this KB."
|
||||
op = sentence.op
|
||||
assert op == '>>' or is_prop_symbol(op), "Must be Horn Clause"
|
||||
self.clauses.append(sentence)
|
||||
|
||||
def ask_generator(self, query):
|
||||
"Yield the empty substitution if KB implies query; else False"
|
||||
if not pl_fc_entails(self.clauses, query):
|
||||
return
|
||||
yield {}
|
||||
|
||||
def retract(self, sentence):
|
||||
"Remove the sentence's clauses from the KB"
|
||||
for c in conjuncts(to_cnf(sentence)):
|
||||
if c in self.clauses:
|
||||
self.clauses.remove(c)
|
||||
|
||||
def clauses_with_premise(self, p):
|
||||
"""The list of clauses in KB that have p in the premise.
|
||||
This could be cached away for O(1) speed, but we'll recompute it."""
|
||||
return [c for c in self.clauses
|
||||
if c.op == '>>' and p in conjuncts(c.args[0])]
|
||||
|
||||
def pl_fc_entails(KB, q):
|
||||
"""Use forward chaining to see if a HornKB entails symbol q. [Fig. 7.14]
|
||||
>>> pl_fc_entails(Fig[7,15], expr('Q'))
|
||||
True
|
||||
"""
|
||||
count = dict([(c, len(conjuncts(c.args[0]))) for c in KB.clauses
|
||||
if c.op == '>>'])
|
||||
inferred = DefaultDict(False)
|
||||
agenda = [s for s in KB.clauses if is_prop_symbol(s.op)]
|
||||
if q in agenda: return True
|
||||
while agenda:
|
||||
p = agenda.pop()
|
||||
if not inferred[p]:
|
||||
inferred[p] = True
|
||||
for c in KB.clauses_with_premise(p):
|
||||
count[c] -= 1
|
||||
if count[c] == 0:
|
||||
if c.args[1] == q: return True
|
||||
agenda.append(c.args[1])
|
||||
return False
|
||||
|
||||
## Wumpus World example [Fig. 7.13]
|
||||
Fig[7,13] = expr("(B11 <=> (P12 | P21)) & ~B11")
|
||||
|
||||
## Propositional Logic Forward Chanining example [Fig. 7.15]
|
||||
Fig[7,15] = PropHornKB()
|
||||
for s in "P>>Q (L&M)>>P (B&L)>>M (A&P)>>L (A&B)>>L A B".split():
|
||||
Fig[7,15].tell(expr(s))
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
# DPLL-Satisfiable [Fig. 7.16]
|
||||
|
||||
def dpll_satisfiable(s):
|
||||
"""Check satisfiability of a propositional sentence.
|
||||
This differs from the book code in two ways: (1) it returns a model
|
||||
rather than True when it succeeds; this is more useful. (2) The
|
||||
function find_pure_symbol is passed a list of unknown clauses, rather
|
||||
than a list of all clauses and the model; this is more efficient.
|
||||
>>> dpll_satisfiable(A&~B)
|
||||
{A: True, B: False}
|
||||
>>> dpll_satisfiable(P&~P)
|
||||
False
|
||||
"""
|
||||
clauses = conjuncts(to_cnf(s))
|
||||
symbols = prop_symbols(s)
|
||||
return dpll(clauses, symbols, {})
|
||||
|
||||
def dpll(clauses, symbols, model):
|
||||
"See if the clauses are true in a partial model."
|
||||
unknown_clauses = [] ## clauses with an unknown truth value
|
||||
for c in clauses:
|
||||
val = pl_true(c, model)
|
||||
if val == False:
|
||||
return False
|
||||
if val != True:
|
||||
unknown_clauses.append(c)
|
||||
if not unknown_clauses:
|
||||
return model
|
||||
P, value = find_pure_symbol(symbols, unknown_clauses)
|
||||
if P:
|
||||
return dpll(clauses, removeall(P, symbols), extend(model, P, value))
|
||||
P, value = find_unit_clause(clauses, model)
|
||||
if P:
|
||||
return dpll(clauses, removeall(P, symbols), extend(model, P, value))
|
||||
P = symbols.pop()
|
||||
return (dpll(clauses, symbols, extend(model, P, True)) or
|
||||
dpll(clauses, symbols, extend(model, P, False)))
|
||||
|
||||
def find_pure_symbol(symbols, unknown_clauses):
|
||||
"""Find a symbol and its value if it appears only as a positive literal
|
||||
(or only as a negative) in clauses.
|
||||
>>> find_pure_symbol([A, B, C], [A|~B,~B|~C,C|A])
|
||||
(A, True)
|
||||
"""
|
||||
for s in symbols:
|
||||
found_pos, found_neg = False, False
|
||||
for c in unknown_clauses:
|
||||
if not found_pos and s in disjuncts(c): found_pos = True
|
||||
if not found_neg and ~s in disjuncts(c): found_neg = True
|
||||
if found_pos != found_neg: return s, found_pos
|
||||
return None, None
|
||||
|
||||
def find_unit_clause(clauses, model):
|
||||
"""A unit clause has only 1 variable that is not bound in the model.
|
||||
>>> find_unit_clause([A|B|C, B|~C, A|~B], {A:True})
|
||||
(B, False)
|
||||
"""
|
||||
for clause in clauses:
|
||||
num_not_in_model = 0
|
||||
for literal in disjuncts(clause):
|
||||
sym = literal_symbol(literal)
|
||||
if sym not in model:
|
||||
num_not_in_model += 1
|
||||
P, value = sym, (literal.op != '~')
|
||||
if num_not_in_model == 1:
|
||||
return P, value
|
||||
return None, None
|
||||
|
||||
|
||||
def literal_symbol(literal):
|
||||
"""The symbol in this literal (without the negation).
|
||||
>>> literal_symbol(P)
|
||||
P
|
||||
>>> literal_symbol(~P)
|
||||
P
|
||||
"""
|
||||
if literal.op == '~':
|
||||
return literal.args[0]
|
||||
else:
|
||||
return literal
|
||||
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Walk-SAT [Fig. 7.17]
|
||||
|
||||
def WalkSAT(clauses, p=0.5, max_flips=10000):
|
||||
## model is a random assignment of true/false to the symbols in clauses
|
||||
## See ~/aima1e/print1/manual/knowledge+logic-answers.tex ???
|
||||
model = dict([(s, random.choice([True, False]))
|
||||
for s in prop_symbols(clauses)])
|
||||
for i in range(max_flips):
|
||||
satisfied, unsatisfied = [], []
|
||||
for clause in clauses:
|
||||
if_(pl_true(clause, model), satisfied, unsatisfied).append(clause)
|
||||
if not unsatisfied: ## if model satisfies all the clauses
|
||||
return model
|
||||
clause = random.choice(unsatisfied)
|
||||
if probability(p):
|
||||
sym = random.choice(prop_symbols(clause))
|
||||
else:
|
||||
## Flip the symbol in clause that miximizes number of sat. clauses
|
||||
raise NotImplementedError
|
||||
model[sym] = not model[sym]
|
||||
|
||||
|
||||
# PL-Wumpus-Agent [Fig. 7.19]
|
||||
class PLWumpusAgent(agents.Agent):
|
||||
"An agent for the wumpus world that does logical inference. [Fig. 7.19]"""
|
||||
def __init__(self):
|
||||
KB = FOLKB()
|
||||
x, y, orientation = 1, 1, (1, 0)
|
||||
visited = set() ## squares already visited
|
||||
action = None
|
||||
plan = []
|
||||
|
||||
def program(percept):
|
||||
stench, breeze, glitter = percept
|
||||
x, y, orientation = update_position(x, y, orientation, action)
|
||||
KB.tell('%sS_%d,%d' % (if_(stench, '', '~'), x, y))
|
||||
KB.tell('%sB_%d,%d' % (if_(breeze, '', '~'), x, y))
|
||||
if glitter: action = 'Grab'
|
||||
elif plan: action = plan.pop()
|
||||
else:
|
||||
for [i, j] in fringe(visited):
|
||||
if KB.ask('~P_%d,%d & ~W_%d,%d' % (i, j, i, j)) != False:
|
||||
raise NotImplementedError
|
||||
KB.ask('~P_%d,%d | ~W_%d,%d' % (i, j, i, j)) != False
|
||||
if action == None:
|
||||
action = random.choice(['Forward', 'Right', 'Left'])
|
||||
return action
|
||||
|
||||
self.program = program
|
||||
|
||||
def update_position(x, y, orientation, action):
|
||||
if action == 'TurnRight':
|
||||
orientation = turn_right(orientation)
|
||||
elif action == 'TurnLeft':
|
||||
orientation = turn_left(orientation)
|
||||
elif action == 'Forward':
|
||||
x, y = x + vector_add((x, y), orientation)
|
||||
return x, y, orientation
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def unify(x, y, s):
|
||||
"""Unify expressions x,y with substitution s; return a substitution that
|
||||
would make x,y equal, or None if x,y can not unify. x and y can be
|
||||
variables (e.g. Expr('x')), constants, lists, or Exprs. [Fig. 9.1]
|
||||
>>> unify(x + y, y + C, {})
|
||||
{y: C, x: y}
|
||||
"""
|
||||
if s == None:
|
||||
return None
|
||||
elif x == y:
|
||||
return s
|
||||
elif is_variable(x):
|
||||
return unify_var(x, y, s)
|
||||
elif is_variable(y):
|
||||
return unify_var(y, x, s)
|
||||
elif isinstance(x, Expr) and isinstance(y, Expr):
|
||||
return unify(x.args, y.args, unify(x.op, y.op, s))
|
||||
elif isinstance(x, str) or isinstance(y, str) or not x or not y:
|
||||
return if_(x == y, s, None)
|
||||
elif issequence(x) and issequence(y) and len(x) == len(y):
|
||||
return unify(x[1:], y[1:], unify(x[0], y[0], s))
|
||||
else:
|
||||
return None
|
||||
|
||||
def is_variable(x):
|
||||
"A variable is an Expr with no args and a lowercase symbol as the op."
|
||||
return isinstance(x, Expr) and not x.args and is_var_symbol(x.op)
|
||||
|
||||
def unify_var(var, x, s):
|
||||
if var in s:
|
||||
return unify(s[var], x, s)
|
||||
elif occur_check(var, x):
|
||||
return None
|
||||
else:
|
||||
return extend(s, var, x)
|
||||
|
||||
def occur_check(var, x):
|
||||
"Return true if var occurs anywhere in x."
|
||||
if var == x:
|
||||
return True
|
||||
elif isinstance(x, Expr):
|
||||
return var.op == x.op or occur_check(var, x.args)
|
||||
elif not isinstance(x, str) and issequence(x):
|
||||
for xi in x:
|
||||
if occur_check(var, xi): return True
|
||||
return False
|
||||
|
||||
def extend(s, var, val):
|
||||
"""Copy the substitution s and extend it by setting var to val; return copy.
|
||||
>>> extend({x: 1}, y, 2)
|
||||
{y: 2, x: 1}
|
||||
"""
|
||||
s2 = s.copy()
|
||||
s2[var] = val
|
||||
return s2
|
||||
|
||||
def subst(s, x):
|
||||
"""Substitute the substitution s into the expression x.
|
||||
>>> subst({x: 42, y:0}, F(x) + y)
|
||||
(F(42) + 0)
|
||||
"""
|
||||
if isinstance(x, list):
|
||||
return [subst(s, xi) for xi in x]
|
||||
elif isinstance(x, tuple):
|
||||
return tuple([subst(s, xi) for xi in x])
|
||||
elif not isinstance(x, Expr):
|
||||
return x
|
||||
elif is_var_symbol(x.op):
|
||||
return s.get(x, x)
|
||||
else:
|
||||
return Expr(x.op, *[subst(s, arg) for arg in x.args])
|
||||
|
||||
def fol_fc_ask(KB, alpha):
|
||||
"""Inefficient forward chaining for first-order logic. [Fig. 9.3]
|
||||
KB is an FOLHornKB and alpha must be an atomic sentence."""
|
||||
while True:
|
||||
new = {}
|
||||
for r in KB.clauses:
|
||||
r1 = standardize_apart(r)
|
||||
ps, q = conjuncts(r.args[0]), r.args[1]
|
||||
raise NotImplementedError
|
||||
|
||||
def standardize_apart(sentence, dic):
|
||||
"""Replace all the variables in sentence with new variables."""
|
||||
if not isinstance(sentence, Expr):
|
||||
return sentence
|
||||
elif is_var_symbol(sentence.op):
|
||||
if sentence in dic:
|
||||
return dic[sentence]
|
||||
else:
|
||||
standardize_apart.counter += 1
|
||||
dic[sentence] = Expr('V_%d' % standardize-apart.counter)
|
||||
return dic[sentence]
|
||||
else:
|
||||
return Expr(sentence.op, *[standardize-apart(a, dic) for a in sentence.args])
|
||||
|
||||
standardize_apart.counter = 0
|
||||
|
||||
def fol_bc_ask(KB, goals, theta):
|
||||
"A simple backward-chaining algorithm for first-order logic. [Fig. 9.6]"
|
||||
if not goals:
|
||||
yield theta
|
||||
q1 = subst(theta, goals[0])
|
||||
raise NotImplementedError
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
# Example application (not in the book).
|
||||
# You can use the Expr class to do symbolic differentiation. This used to be
|
||||
# a part of AI; now it is considered a separate field, Symbolic Algebra.
|
||||
|
||||
def diff(y, x):
|
||||
"""Return the symbolic derivative, dy/dx, as an Expr.
|
||||
However, you probably want to simplify the results with simp.
|
||||
>>> diff(x * x, x)
|
||||
((x * 1) + (x * 1))
|
||||
>>> simp(diff(x * x, x))
|
||||
(2 * x)
|
||||
"""
|
||||
if y == x: return ONE
|
||||
elif not y.args: return ZERO
|
||||
else:
|
||||
u, op, v = y.args[0], y.op, y.args[-1]
|
||||
if op == '+': return diff(u, x) + diff(v, x)
|
||||
elif op == '-' and len(args) == 1: return -diff(u, x)
|
||||
elif op == '-': return diff(u, x) - diff(v, x)
|
||||
elif op == '*': return u * diff(v, x) + v * diff(u, x)
|
||||
elif op == '/': return (v*diff(u, x) - u*diff(v, x)) / (v * v)
|
||||
elif op == '**' and isnumber(x.op):
|
||||
return (v * u ** (v - 1) * diff(u, x))
|
||||
elif op == '**': return (v * u ** (v - 1) * diff(u, x)
|
||||
+ u ** v * Expr('log')(u) * diff(v, x))
|
||||
elif op == 'log': return diff(u, x) / u
|
||||
else: raise ValueError("Unknown op: %s in diff(%s, %s)" % (op, y, x))
|
||||
|
||||
def simp(x):
|
||||
if not x.args: return x
|
||||
args = map(simp, x.args)
|
||||
u, op, v = args[0], x.op, args[-1]
|
||||
if op == '+':
|
||||
if v == ZERO: return u
|
||||
if u == ZERO: return v
|
||||
if u == v: return TWO * u
|
||||
if u == -v or v == -u: return ZERO
|
||||
elif op == '-' and len(args) == 1:
|
||||
if u.op == '-' and len(u.args) == 1: return u.args[0] ## --y ==> y
|
||||
elif op == '-':
|
||||
if v == ZERO: return u
|
||||
if u == ZERO: return -v
|
||||
if u == v: return ZERO
|
||||
if u == -v or v == -u: return ZERO
|
||||
elif op == '*':
|
||||
if u == ZERO or v == ZERO: return ZERO
|
||||
if u == ONE: return v
|
||||
if v == ONE: return u
|
||||
if u == v: return u ** 2
|
||||
elif op == '/':
|
||||
if u == ZERO: return ZERO
|
||||
if v == ZERO: return Expr('Undefined')
|
||||
if u == v: return ONE
|
||||
if u == -v or v == -u: return ZERO
|
||||
elif op == '**':
|
||||
if u == ZERO: return ZERO
|
||||
if v == ZERO: return ONE
|
||||
if u == ONE: return ONE
|
||||
if v == ONE: return u
|
||||
elif op == 'log':
|
||||
if u == ONE: return ZERO
|
||||
else: raise ValueError("Unknown op: " + op)
|
||||
## If we fall through to here, we can not simplify further
|
||||
return Expr(op, *args)
|
||||
|
||||
def d(y, x):
|
||||
"Differentiate and then simplify."
|
||||
return simp(diff(y, x))
|
||||
|
@ -0,0 +1,78 @@
|
||||
### PropKB
|
||||
>>> kb = PropKB()
|
||||
>>> kb.tell(A & B)
|
||||
>>> kb.tell(B >> C)
|
||||
>>> kb.ask(C) ## The result {} means true, with no substitutions
|
||||
{}
|
||||
>>> kb.ask(P)
|
||||
False
|
||||
>>> kb.retract(B)
|
||||
>>> kb.ask(C)
|
||||
False
|
||||
|
||||
>>> pl_true(P, {})
|
||||
>>> pl_true(P | Q, {P: True})
|
||||
True
|
||||
|
||||
# Notice that the function pl_true cannot reason by cases:
|
||||
>>> pl_true(P | ~P)
|
||||
|
||||
# However, tt_true can:
|
||||
>>> tt_true(P | ~P)
|
||||
True
|
||||
|
||||
# The following are tautologies from [Fig. 7.11]:
|
||||
>>> tt_true("(A & B) <=> (B & A)")
|
||||
True
|
||||
>>> tt_true("(A | B) <=> (B | A)")
|
||||
True
|
||||
>>> tt_true("((A & B) & C) <=> (A & (B & C))")
|
||||
True
|
||||
>>> tt_true("((A | B) | C) <=> (A | (B | C))")
|
||||
True
|
||||
>>> tt_true("~~A <=> A")
|
||||
True
|
||||
>>> tt_true("(A >> B) <=> (~B >> ~A)")
|
||||
True
|
||||
>>> tt_true("(A >> B) <=> (~A | B)")
|
||||
True
|
||||
>>> tt_true("(A <=> B) <=> ((A >> B) & (B >> A))")
|
||||
True
|
||||
>>> tt_true("~(A & B) <=> (~A | ~B)")
|
||||
True
|
||||
>>> tt_true("~(A | B) <=> (~A & ~B)")
|
||||
True
|
||||
>>> tt_true("(A & (B | C)) <=> ((A & B) | (A & C))")
|
||||
True
|
||||
>>> tt_true("(A | (B & C)) <=> ((A | B) & (A | C))")
|
||||
True
|
||||
|
||||
# The following are not tautologies:
|
||||
>>> tt_true(A & ~A)
|
||||
False
|
||||
>>> tt_true(A & B)
|
||||
False
|
||||
|
||||
### [Fig. 7.13]
|
||||
>>> alpha = expr("~P12")
|
||||
>>> to_cnf(Fig[7,13] & ~alpha)
|
||||
((~P12 | B11) & (~P21 | B11) & (P12 | P21 | ~B11) & ~B11 & P12)
|
||||
>>> tt_entails(Fig[7,13], alpha)
|
||||
True
|
||||
>>> pl_resolution(PropKB(Fig[7,13]), alpha)
|
||||
True
|
||||
|
||||
### [Fig. 7.15]
|
||||
>>> pl_fc_entails(Fig[7,15], expr('SomethingSilly'))
|
||||
False
|
||||
|
||||
### Unification:
|
||||
>>> unify(x, x, {})
|
||||
{}
|
||||
>>> unify(x, 3, {})
|
||||
{x: 3}
|
||||
|
||||
|
||||
>>> to_cnf((P&Q) | (~P & ~Q))
|
||||
((~P | P) & (~Q | P) & (~P | Q) & (~Q | Q))
|
||||
|
@ -0,0 +1,142 @@
|
||||
"""Markov Decision Processes (Chapter 17)
|
||||
|
||||
First we define an MDP, and the special case of a GridMDP, in which
|
||||
states are laid out in a 2-dimensional grid. We also represent a policy
|
||||
as a dictionary of {state:action} pairs, and a Utility function as a
|
||||
dictionary of {state:number} pairs. We then define the value_iteration
|
||||
and policy_iteration algorithms."""
|
||||
|
||||
from utils import *
|
||||
|
||||
class MDP:
|
||||
"""A Markov Decision Process, defined by an initial state, transition model,
|
||||
and reward function. We also keep track of a gamma value, for use by
|
||||
algorithms. The transition model is represented somewhat differently from
|
||||
the text. Instead of T(s, a, s') being probability number for each
|
||||
state/action/state triplet, we instead have T(s, a) return a list of (p, s')
|
||||
pairs. We also keep track of the possible states, terminal states, and
|
||||
actions for each state. [page 615]"""
|
||||
|
||||
def __init__(self, init, actlist, terminals, gamma=.9):
|
||||
update(self, init=init, actlist=actlist, terminals=terminals,
|
||||
gamma=gamma, states=set(), reward={})
|
||||
|
||||
def R(self, state):
|
||||
"Return a numeric reward for this state."
|
||||
return self.reward[state]
|
||||
|
||||
def T(state, action):
|
||||
"""Transition model. From a state and an action, return a list
|
||||
of (result-state, probability) pairs."""
|
||||
abstract
|
||||
|
||||
def actions(self, state):
|
||||
"""Set of actions that can be performed in this state. By default, a
|
||||
fixed list of actions, except for terminal states. Override this
|
||||
method if you need to specialize by state."""
|
||||
if state in self.terminals:
|
||||
return [None]
|
||||
else:
|
||||
return self.actlist
|
||||
|
||||
class GridMDP(MDP):
|
||||
"""A two-dimensional grid MDP, as in [Figure 17.1]. All you have to do is
|
||||
specify the grid as a list of lists of rewards; use None for an obstacle
|
||||
(unreachable state). Also, you should specify the terminal states.
|
||||
An action is an (x, y) unit vector; e.g. (1, 0) means move east."""
|
||||
def __init__(self, grid, terminals, init=(0, 0), gamma=.9):
|
||||
grid.reverse() ## because we want row 0 on bottom, not on top
|
||||
MDP.__init__(self, init, actlist=orientations,
|
||||
terminals=terminals, gamma=gamma)
|
||||
update(self, grid=grid, rows=len(grid), cols=len(grid[0]))
|
||||
for x in range(self.cols):
|
||||
for y in range(self.rows):
|
||||
self.reward[x, y] = grid[y][x]
|
||||
if grid[y][x] is not None:
|
||||
self.states.add((x, y))
|
||||
|
||||
def T(self, state, action):
|
||||
if action == None:
|
||||
return [(0.0, state)]
|
||||
else:
|
||||
return [(0.8, self.go(state, action)),
|
||||
(0.1, self.go(state, turn_right(action))),
|
||||
(0.1, self.go(state, turn_left(action)))]
|
||||
|
||||
def go(self, state, direction):
|
||||
"Return the state that results from going in this direction."
|
||||
state1 = vector_add(state, direction)
|
||||
return if_(state1 in self.states, state1, state)
|
||||
|
||||
def to_grid(self, mapping):
|
||||
"""Convert a mapping from (x, y) to v into a [[..., v, ...]] grid."""
|
||||
return list(reversed([[mapping.get((x,y), None)
|
||||
for x in range(self.cols)]
|
||||
for y in range(self.rows)]))
|
||||
|
||||
def to_arrows(self, policy):
|
||||
chars = {(1, 0):'>', (0, 1):'^', (-1, 0):'<', (0, -1):'v', None: '.'}
|
||||
return self.to_grid(dict([(s, chars[a]) for (s, a) in policy.items()]))
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
Fig[17,1] = GridMDP([[-0.04, -0.04, -0.04, +1],
|
||||
[-0.04, None, -0.04, -1],
|
||||
[-0.04, -0.04, -0.04, -0.04]],
|
||||
terminals=[(3, 2), (3, 1)])
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def value_iteration(mdp, epsilon=0.001):
|
||||
"Solving an MDP by value iteration. [Fig. 17.4]"
|
||||
U1 = dict([(s, 0) for s in mdp.states])
|
||||
R, T, gamma = mdp.R, mdp.T, mdp.gamma
|
||||
while True:
|
||||
U = U1.copy()
|
||||
delta = 0
|
||||
for s in mdp.states:
|
||||
U1[s] = R(s) + gamma * max([sum([p * U[s1] for (p, s1) in T(s, a)])
|
||||
for a in mdp.actions(s)])
|
||||
delta = max(delta, abs(U1[s] - U[s]))
|
||||
if delta < epsilon * (1 - gamma) / gamma:
|
||||
return U
|
||||
|
||||
def best_policy(mdp, U):
|
||||
"""Given an MDP and a utility function U, determine the best policy,
|
||||
as a mapping from state to action. (Equation 17.4)"""
|
||||
pi = {}
|
||||
for s in mdp.states:
|
||||
pi[s] = argmax(mdp.actions(s), lambda a:expected_utility(a, s, U, mdp))
|
||||
return pi
|
||||
|
||||
def expected_utility(a, s, U, mdp):
|
||||
"The expected utility of doing a in state s, according to the MDP and U."
|
||||
return sum([p * U[s1] for (p, s1) in mdp.T(s, a)])
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def policy_iteration(mdp):
|
||||
"Solve an MDP by policy iteration [Fig. 17.7]"
|
||||
U = dict([(s, 0) for s in mdp.states])
|
||||
pi = dict([(s, random.choice(mdp.actions(s))) for s in mdp.states])
|
||||
while True:
|
||||
U = policy_evaluation(pi, U, mdp)
|
||||
unchanged = True
|
||||
for s in mdp.states:
|
||||
a = argmax(mdp.actions(s), lambda a: expected_utility(a,s,U,mdp))
|
||||
if a != pi[s]:
|
||||
pi[s] = a
|
||||
unchanged = False
|
||||
if unchanged:
|
||||
return pi
|
||||
|
||||
def policy_evaluation(pi, U, mdp, k=20):
|
||||
"""Return an updated utility mapping U from each state in the MDP to its
|
||||
utility, using an approximation (modified policy iteration)."""
|
||||
R, T, gamma = mdp.R, mdp.T, mdp.gamma
|
||||
for i in range(k):
|
||||
for s in mdp.states:
|
||||
U[s] = R(s) + gamma * sum([p * U[s] for (p, s1) in T(s, pi[s])])
|
||||
return U
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
### demo
|
||||
|
||||
>>> m = Fig[17,1]
|
||||
|
||||
>>> pi = best_policy(m, value_iteration(m, .01))
|
||||
|
||||
>>> pi
|
||||
{(3, 2): None, (3, 1): None, (3, 0): (-1, 0), (2, 1): (0, 1), (0, 2): (1, 0), (1, 0): (1, 0), (0, 0): (0, 1), (1, 2): (1, 0), (2, 0): (0, 1), (0, 1): (0, 1), (2, 2): (1, 0)}
|
||||
|
||||
>>> m.to_arrows(pi)
|
||||
[['>', '>', '>', '.'], ['^', None, '^', '.'], ['^', '>', '^', '<']]
|
||||
|
||||
>>> print_table(m.to_arrows(pi))
|
||||
> > > .
|
||||
^ None ^ .
|
||||
^ > ^ <
|
||||
|
||||
>>> value_iteration(m, .01)
|
||||
{(3, 2): 1.0, (3, 1): -1.0, (3, 0): 0.12958868267972745, (0, 1): 0.39810203830605462, (0, 2): 0.50928545646220924, (1, 0): 0.25348746162470537, (0, 0): 0.29543540628363629, (1, 2): 0.64958064617168676, (2, 0): 0.34461306281476806, (2, 1): 0.48643676237737926, (2, 2): 0.79536093684710951}
|
||||
|
||||
>>> policy_iteration(m)
|
||||
{(3, 2): None, (3, 1): None, (3, 0): (0, -1), (2, 1): (-1, 0), (0, 2): (1, 0), (1, 0): (1, 0), (0, 0): (1, 0), (1, 2): (1, 0), (2, 0): (1, 0), (0, 1): (1, 0), (2, 2): (1, 0)}
|
||||
|
||||
>>> print_table(m.to_arrows(policy_iteration(m)))
|
||||
> > > .
|
||||
> None < .
|
||||
> > > v
|
@ -0,0 +1,170 @@
|
||||
"""A chart parser and some grammars. (Chapter 22)"""
|
||||
|
||||
from utils import *
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Grammars and Lexicons
|
||||
|
||||
def Rules(**rules):
|
||||
"""Create a dictionary mapping symbols to alternative sequences.
|
||||
>>> Rules(A = "B C | D E")
|
||||
{'A': [['B', 'C'], ['D', 'E']]}
|
||||
"""
|
||||
for (lhs, rhs) in rules.items():
|
||||
rules[lhs] = [alt.strip().split() for alt in rhs.split('|')]
|
||||
return rules
|
||||
|
||||
def Lexicon(**rules):
|
||||
"""Create a dictionary mapping symbols to alternative words.
|
||||
>>> Lexicon(Art = "the | a | an")
|
||||
{'Art': ['the', 'a', 'an']}
|
||||
"""
|
||||
for (lhs, rhs) in rules.items():
|
||||
rules[lhs] = [word.strip() for word in rhs.split('|')]
|
||||
return rules
|
||||
|
||||
class Grammar:
|
||||
def __init__(self, name, rules, lexicon):
|
||||
"A grammar has a set of rules and a lexicon."
|
||||
update(self, name=name, rules=rules, lexicon=lexicon)
|
||||
self.categories = DefaultDict([])
|
||||
for lhs in lexicon:
|
||||
for word in lexicon[lhs]:
|
||||
self.categories[word].append(lhs)
|
||||
|
||||
def rewrites_for(self, cat):
|
||||
"Return a sequence of possible rhs's that cat can be rewritten as."
|
||||
return self.rules.get(cat, ())
|
||||
|
||||
def isa(self, word, cat):
|
||||
"Return True iff word is of category cat"
|
||||
return cat in self.categories[word]
|
||||
|
||||
def __repr__(self):
|
||||
return '<Grammar %s>' % self.name
|
||||
|
||||
E0 = Grammar('E0',
|
||||
Rules( # Grammar for E_0 [Fig. 22.4]
|
||||
S = 'NP VP | S Conjunction S',
|
||||
NP = 'Pronoun | Noun | Article Noun | Digit Digit | NP PP | NP RelClause',
|
||||
VP = 'Verb | VP NP | VP Adjective | VP PP | VP Adverb',
|
||||
PP = 'Preposition NP',
|
||||
RelClause = 'That VP'),
|
||||
|
||||
Lexicon( # Lexicon for E_0 [Fig. 22.3]
|
||||
Noun = "stench | breeze | glitter | nothing | wumpus | pit | pits | gold | east",
|
||||
Verb = "is | see | smell | shoot | fell | stinks | go | grab | carry | kill | turn | feel",
|
||||
Adjective = "right | left | east | south | back | smelly",
|
||||
Adverb = "here | there | nearby | ahead | right | left | east | south | back",
|
||||
Pronoun = "me | you | I | it",
|
||||
Name = "John | Mary | Boston | Aristotle",
|
||||
Article = "the | a | an",
|
||||
Preposition = "to | in | on | near",
|
||||
Conjunction = "and | or | but",
|
||||
Digit = "0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9",
|
||||
That = "that"
|
||||
))
|
||||
|
||||
E_ = Grammar('E_', # Trivial Grammar and lexicon for testing
|
||||
Rules(
|
||||
S = 'NP VP',
|
||||
NP = 'Art N | Pronoun',
|
||||
VP = 'V NP'),
|
||||
|
||||
Lexicon(
|
||||
Art = 'the | a',
|
||||
N = 'man | woman | table | shoelace | saw',
|
||||
Pronoun = 'I | you | it',
|
||||
V = 'saw | liked | feel'
|
||||
))
|
||||
|
||||
def generate_random(grammar=E_, s='S'):
|
||||
"""Replace each token in s by a random entry in grammar (recursively).
|
||||
This is useful for testing a grammar, e.g. generate_random(E_)"""
|
||||
import random
|
||||
|
||||
def rewrite(tokens, into):
|
||||
for token in tokens:
|
||||
if token in grammar.rules:
|
||||
rewrite(random.choice(grammar.rules[token]), into)
|
||||
elif token in grammar.lexicon:
|
||||
into.append(random.choice(grammar.lexicon[token]))
|
||||
else:
|
||||
into.append(token)
|
||||
return into
|
||||
|
||||
return ' '.join(rewrite(s.split(), []))
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Chart Parsing
|
||||
|
||||
|
||||
class Chart:
|
||||
"""Class for parsing sentences using a chart data structure. [Fig 22.7]
|
||||
>>> chart = Chart(E0);
|
||||
>>> len(chart.parses('the stench is in 2 2'))
|
||||
1
|
||||
"""
|
||||
|
||||
def __init__(self, grammar, trace=False):
|
||||
"""A datastructure for parsing a string; and methods to do the parse.
|
||||
self.chart[i] holds the edges that end just before the i'th word.
|
||||
Edges are 5-element lists of [start, end, lhs, [found], [expects]]."""
|
||||
update(self, grammar=grammar, trace=trace)
|
||||
|
||||
def parses(self, words, S='S'):
|
||||
"""Return a list of parses; words can be a list or string."""
|
||||
if isinstance(words, str):
|
||||
words = words.split()
|
||||
self.parse(words, S)
|
||||
# Return all the parses that span the whole input
|
||||
return [[i, j, S, found, []]
|
||||
for (i, j, lhs, found, expects) in self.chart[len(words)]
|
||||
if lhs == S and expects == []]
|
||||
|
||||
def parse(self, words, S='S'):
|
||||
"""Parse a list of words; according to the grammar.
|
||||
Leave results in the chart."""
|
||||
self.chart = [[] for i in range(len(words)+1)]
|
||||
self.add_edge([0, 0, 'S_', [], [S]])
|
||||
for i in range(len(words)):
|
||||
self.scanner(i, words[i])
|
||||
return self.chart
|
||||
|
||||
def add_edge(self, edge):
|
||||
"Add edge to chart, and see if it extends or predicts another edge."
|
||||
start, end, lhs, found, expects = edge
|
||||
if edge not in self.chart[end]:
|
||||
self.chart[end].append(edge)
|
||||
if self.trace:
|
||||
print '%10s: added %s' % (caller(2), edge)
|
||||
if not expects:
|
||||
self.extender(edge)
|
||||
else:
|
||||
self.predictor(edge)
|
||||
|
||||
def scanner(self, j, word):
|
||||
"For each edge expecting a word of this category here, extend the edge."
|
||||
for (i, j, A, alpha, Bb) in self.chart[j]:
|
||||
if Bb and self.grammar.isa(word, Bb[0]):
|
||||
self.add_edge([i, j+1, A, alpha + [(Bb[0], word)], Bb[1:]])
|
||||
|
||||
def predictor(self, (i, j, A, alpha, Bb)):
|
||||
"Add to chart any rules for B that could help extend this edge."
|
||||
B = Bb[0]
|
||||
if B in self.grammar.rules:
|
||||
for rhs in self.grammar.rewrites_for(B):
|
||||
self.add_edge([j, j, B, [], rhs])
|
||||
|
||||
def extender(self, edge):
|
||||
"See what edges can be extended by this edge."
|
||||
(j, k, B, _, _) = edge
|
||||
for (i, j, A, alpha, B1b) in self.chart[j]:
|
||||
if B1b and B == B1b[0]:
|
||||
self.add_edge([i, k, A, alpha + [edge], B1b[1:]])
|
||||
|
||||
|
||||
|
||||
#### TODO:
|
||||
#### 1. Parsing with augmentations -- requires unification, etc.
|
||||
#### 2. Sequitor
|
@ -0,0 +1,23 @@
|
||||
>>> chart = Chart(E0)
|
||||
|
||||
>>> chart.parses('the wumpus that is smelly is near 2 2')
|
||||
[[0, 9, 'S', [[0, 5, 'NP', [[0, 2, 'NP', [('Article', 'the'), ('Noun', 'wumpus')], []], [2, 5, 'RelClause', [('That', 'that'), [3, 5, 'VP', [[3, 4, 'VP', [('Verb', 'is')], []], ('Adjective', 'smelly')], []]], []]], []], [5, 9, 'VP', [[5, 6, 'VP', [('Verb', 'is')], []], [6, 9, 'PP', [('Preposition', 'near'), [7, 9, 'NP', [('Digit', '2'), ('Digit', '2')], []]], []]], []]], []]]
|
||||
|
||||
### There is a built-in trace facility (compare [Fig. 22.9])
|
||||
>>> Chart(E_, trace=True).parses('I feel it')
|
||||
parse: added [0, 0, 'S_', [], ['S']]
|
||||
predictor: added [0, 0, 'S', [], ['NP', 'VP']]
|
||||
predictor: added [0, 0, 'NP', [], ['Art', 'N']]
|
||||
predictor: added [0, 0, 'NP', [], ['Pronoun']]
|
||||
scanner: added [0, 1, 'NP', [('Pronoun', 'I')], []]
|
||||
extender: added [0, 1, 'S', [[0, 1, 'NP', [('Pronoun', 'I')], []]], ['VP']]
|
||||
predictor: added [1, 1, 'VP', [], ['V', 'NP']]
|
||||
scanner: added [1, 2, 'VP', [('V', 'feel')], ['NP']]
|
||||
predictor: added [2, 2, 'NP', [], ['Art', 'N']]
|
||||
predictor: added [2, 2, 'NP', [], ['Pronoun']]
|
||||
scanner: added [2, 3, 'NP', [('Pronoun', 'it')], []]
|
||||
extender: added [1, 3, 'VP', [('V', 'feel'), [2, 3, 'NP', [('Pronoun', 'it')], []]], []]
|
||||
extender: added [0, 3, 'S', [[0, 1, 'NP', [('Pronoun', 'I')], []], [1, 3, 'VP', [('V', 'feel'), [2, 3, 'NP', [('Pronoun', 'it')], []]], []]], []]
|
||||
extender: added [0, 3, 'S_', [[0, 3, 'S', [[0, 1, 'NP', [('Pronoun', 'I')], []], [1, 3, 'VP', [('V', 'feel'), [2, 3, 'NP', [('Pronoun', 'it')], []]], []]], []]], []]
|
||||
[[0, 3, 'S', [[0, 1, 'NP', [('Pronoun', 'I')], []], [1, 3, 'VP', [('V', 'feel'), [2, 3, 'NP', [('Pronoun', 'it')], []]], []]], []]]
|
||||
|
@ -0,0 +1,7 @@
|
||||
"""Planning (Chapters 11-12)
|
||||
"""
|
||||
|
||||
from __future__ import generators
|
||||
from utils import *
|
||||
import agents
|
||||
import math, random, sys, time, bisect, string
|
@ -0,0 +1,171 @@
|
||||
"""Probability models. (Chapter 13-15)
|
||||
"""
|
||||
|
||||
from utils import *
|
||||
from logic import extend
|
||||
import agents
|
||||
import bisect, random
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class DTAgent(agents.Agent):
|
||||
"A decision-theoretic agent. [Fig. 13.1]"
|
||||
|
||||
def __init__(self, belief_state):
|
||||
agents.Agent.__init__(self)
|
||||
|
||||
def program(percept):
|
||||
belief_state.observe(action, percept)
|
||||
program.action = argmax(belief_state.actions(),
|
||||
belief_state.expected_outcome_utility)
|
||||
return program.action
|
||||
|
||||
program.action = None
|
||||
self.program = program
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class ProbDist:
|
||||
"""A discrete probability distribution. You name the random variable
|
||||
in the constructor, then assign and query probability of values.
|
||||
>>> P = ProbDist('Flip'); P['H'], P['T'] = 0.5, 0.5; P['H']
|
||||
0.5
|
||||
"""
|
||||
def __init__(self, varname='?'):
|
||||
update(self, prob={}, varname=varname, values=[])
|
||||
|
||||
def __getitem__(self, val):
|
||||
"Given a value, return P(value)."
|
||||
return self.prob[val]
|
||||
|
||||
def __setitem__(self, val, p):
|
||||
"Set P(val) = p"
|
||||
if val not in self.values:
|
||||
self.values.append(val)
|
||||
self.prob[val] = p
|
||||
|
||||
def normalize(self):
|
||||
"Make sure the probabilities of all values sum to 1."
|
||||
total = sum(self.prob.values())
|
||||
if not (1.0-epsilon < total < 1.0+epsilon):
|
||||
for val in self.prob:
|
||||
self.prob[val] /= total
|
||||
return self
|
||||
|
||||
epsilon = 0.001
|
||||
|
||||
class JointProbDist(ProbDist):
|
||||
"""A discrete probability distribute over a set of variables.
|
||||
>>> P = JointProbDist(['X', 'Y']); P[1, 1] = 0.25
|
||||
>>> P[1, 1]
|
||||
0.25
|
||||
"""
|
||||
def __init__(self, variables):
|
||||
update(self, prob={}, variables=variables, vals=DefaultDict([]))
|
||||
|
||||
def __getitem__(self, values):
|
||||
"Given a tuple or dict of values, return P(values)."
|
||||
if isinstance(values, dict):
|
||||
values = tuple([values[var] for var in self.variables])
|
||||
return self.prob[values]
|
||||
|
||||
def __setitem__(self, values, p):
|
||||
"""Set P(values) = p. Values can be a tuple or a dict; it must
|
||||
have a value for each of the variables in the joint. Also keep track
|
||||
of the values we have seen so far for each variable."""
|
||||
if isinstance(values, dict):
|
||||
values = [values[var] for var in self.variables]
|
||||
self.prob[values] = p
|
||||
for var,val in zip(self.variables, values):
|
||||
if val not in self.vals[var]:
|
||||
self.vals[var].append(val)
|
||||
|
||||
def values(self, var):
|
||||
"Return the set of possible values for a variable."
|
||||
return self.vals[var]
|
||||
|
||||
def __repr__(self):
|
||||
return "P(%s)" % self.variables
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def enumerate_joint_ask(X, e, P):
|
||||
"""Return a probability distribution over the values of the variable X,
|
||||
given the {var:val} observations e, in the JointProbDist P.
|
||||
Works for Boolean variables only. [Fig. 13.4]"""
|
||||
Q = ProbDist(X) ## A probability distribution for X, initially empty
|
||||
Y = [v for v in P.variables if v != X and v not in e]
|
||||
for xi in P.values(X):
|
||||
Q[xi] = enumerate_joint(Y, extend(e, X, xi), P)
|
||||
return Q.normalize()
|
||||
|
||||
def enumerate_joint(vars, values, P):
|
||||
"As in Fig 13.4, except x and e are already incorporated in values."
|
||||
if not vars:
|
||||
return P[values]
|
||||
Y = vars[0]; rest = vars[1:]
|
||||
return sum([enumerate_joint(rest, extend(values, Y, y), P)
|
||||
for y in P.values(Y)])
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class BayesNet:
|
||||
def __init__(self, nodes=[]):
|
||||
update(self, nodes=[], vars=[])
|
||||
for node in nodes:
|
||||
self.add(node)
|
||||
|
||||
def add(self, node):
|
||||
self.nodes.append(node)
|
||||
self.vars.append(node.variable)
|
||||
|
||||
def observe(self, var, val):
|
||||
self.evidence[var] = val
|
||||
|
||||
class BayesNode:
|
||||
def __init__(self, variable, parents, cpt):
|
||||
if isinstance(parents, str): parents = parents.split()
|
||||
update(self, variable=variable, parents=parents, cpt=cpt)
|
||||
|
||||
node = BayesNode
|
||||
|
||||
|
||||
T, F = True, False
|
||||
|
||||
burglary = BayesNet([
|
||||
node('Burglary', '', .001),
|
||||
node('Earthquake', '', .002),
|
||||
node('Alarm', 'Burglary Earthquake', {
|
||||
(T, T):.95,
|
||||
(T, F):.94,
|
||||
(F, T):.29,
|
||||
(F, F):.001}),
|
||||
node('JohnCalls', 'Alarm', {T:.90, F:.05}),
|
||||
node('MaryCalls', 'Alarm', {T:.70, F:.01})
|
||||
])
|
||||
#______________________________________________________________________________
|
||||
|
||||
def elimination_ask(X, e, bn):
|
||||
"[Fig. 14.10]"
|
||||
factors = []
|
||||
for var in reverse(bn.vars):
|
||||
factors.append(Factor(var, e))
|
||||
if is_hidden(var, X, e):
|
||||
factors = sum_out(var, factors)
|
||||
return pointwise_product(factors).normalize()
|
||||
|
||||
def pointwise_product(factors):
|
||||
pass
|
||||
|
||||
def sum_out(var, factors):
|
||||
pass
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def prior_sample(bn):
|
||||
x = {}
|
||||
for xi in bn.vars:
|
||||
x[xi.var] = xi.sample([x])
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
@ -0,0 +1,32 @@
|
||||
## We can build up a probability distribution like this (p. 469):
|
||||
>>> P = ProbDist()
|
||||
>>> P['sunny'] = 0.7
|
||||
>>> P['rain'] = 0.2
|
||||
>>> P['cloudy'] = 0.08
|
||||
>>> P['snow'] = 0.02
|
||||
|
||||
## and query it like this:
|
||||
>>> P['rain']
|
||||
0.20000000000000001
|
||||
|
||||
## A Joint Probability Distribution is dealt with like this (p. 475):
|
||||
>>> P = JointProbDist(['Toothache', 'Cavity', 'Catch'])
|
||||
>>> T, F = True, False
|
||||
>>> P[T, T, T] = 0.108; P[T, T, F] = 0.012; P[F, T, T] = 0.072; P[F, T, F] = 0.008
|
||||
>>> P[T, F, T] = 0.016; P[T, F, F] = 0.064; P[F, F, T] = 0.144; P[F, F, F] = 0.576
|
||||
|
||||
>>> P[T, T, T]
|
||||
0.108
|
||||
|
||||
## Ask for P(Cavity|Toothache=T)
|
||||
>>> PC = enumerate_joint_ask('Cavity', {'Toothache': T}, P)
|
||||
>>> PC.prob
|
||||
{False: 0.39999999999999997, True: 0.59999999999999998}
|
||||
|
||||
>>> 0.6-epsilon < PC[T] < 0.6+epsilon
|
||||
True
|
||||
|
||||
>>> 0.4-epsilon < PC[F] < 0.4+epsilon
|
||||
True
|
||||
|
||||
|
@ -0,0 +1,15 @@
|
||||
"""Reinforcement Learning (Chapter 21)
|
||||
"""
|
||||
|
||||
from utils import *
|
||||
import agents
|
||||
|
||||
class PassiveADPAgent(agents.Agent):
|
||||
"""Passive (non-learning) agent that uses adaptive dynamic programming
|
||||
on a given MDP and policy. [Fig. 21.2]"""
|
||||
NotImplementedError
|
||||
|
||||
class PassiveTDAgent(agents.Agent):
|
||||
"""Passive (non-learning) agent that uses temporal differences to learn
|
||||
utility estimates. [Fig. 21.4]"""
|
||||
NotImplementedError
|
@ -0,0 +1,68 @@
|
||||
|
||||
>>> ab = GraphProblem('A', 'B', romania)
|
||||
>>> breadth_first_tree_search(ab).state
|
||||
'B'
|
||||
>>> breadth_first_graph_search(ab).state
|
||||
'B'
|
||||
>>> depth_first_graph_search(ab).state
|
||||
'B'
|
||||
>>> iterative_deepening_search(ab).state
|
||||
'B'
|
||||
>>> depth_limited_search(ab).state
|
||||
'B'
|
||||
>>> astar_search(ab).state
|
||||
'B'
|
||||
>>> [node.state for node in astar_search(ab).path()]
|
||||
['B', 'P', 'R', 'S', 'A']
|
||||
|
||||
|
||||
### demo
|
||||
|
||||
>>> compare_graph_searchers()
|
||||
Searcher Romania(A,B) Romania(O, N) Australia
|
||||
breadth_first_tree_search < 21/ 22/ 59/B> <1158/1159/3288/N> < 7/ 8/ 22/WA>
|
||||
breadth_first_graph_search < 10/ 19/ 26/B> < 19/ 45/ 45/N> < 5/ 8/ 16/WA>
|
||||
depth_first_graph_search < 9/ 15/ 23/B> < 16/ 27/ 39/N> < 4/ 7/ 13/WA>
|
||||
iterative_deepening_search < 11/ 33/ 31/B> < 656/1815/1812/N> < 3/ 11/ 11/WA>
|
||||
depth_limited_search < 54/ 65/ 185/B> < 387/1012/1125/N> < 50/ 54/ 200/WA>
|
||||
astar_search < 3/ 4/ 9/B> < 8/ 10/ 22/N> < 2/ 3/ 6/WA>
|
||||
|
||||
>>> board = list('SARTELNID')
|
||||
>>> print_boggle(board)
|
||||
S A R
|
||||
T E L
|
||||
N I D
|
||||
|
||||
>>> f = BoggleFinder(board)
|
||||
|
||||
>>> len(f)
|
||||
206
|
||||
|
||||
>>> ' '.join(f.words())
|
||||
'LID LARES DEAL LIE DIETS LIN LINT TIL TIN RATED ERAS LATEN DEAR TIE LINE INTER STEAL LATED LAST TAR SAL DITES RALES SAE RETS TAE RAT RAS SAT IDLE TILDES LEAST IDEAS LITE SATED TINED LEST LIT RASE RENTS TINEA EDIT EDITS NITES ALES LATE LETS RELIT TINES LEI LAT ELINT LATI SENT TARED DINE STAR SEAR NEST LITAS TIED SEAT SERAL RATE DINT DEL DEN SEAL TIER TIES NET SALINE DILATE EAST TIDES LINTER NEAR LITS ELINTS DENI RASED SERA TILE NEAT DERAT IDLEST NIDE LIEN STARED LIER LIES SETA NITS TINE DITAS ALINE SATIN TAS ASTER LEAS TSAR LAR NITE RALE LAS REAL NITER ATE RES RATEL IDEA RET IDEAL REI RATS STALE DENT RED IDES ALIEN SET TEL SER TEN TEA TED SALE TALE STILE ARES SEA TILDE SEN SEL ALINES SEI LASE DINES ILEA LINES ELD TIDE RENT DIEL STELA TAEL STALED EARL LEA TILES TILER LED ETA TALI ALE LASED TELA LET IDLER REIN ALIT ITS NIDES DIN DIE DENTS STIED LINER LASTED RATINE ERA IDLES DIT RENTAL DINER SENTI TINEAL DEIL TEAR LITER LINTS TEAL DIES EAR EAT ARLES SATE STARE DITS DELI DENTAL REST DITE DENTIL DINTS DITA DIET LENT NETS NIL NIT SETAL LATS TARE ARE SATI'
|
||||
|
||||
>>> boggle_hill_climbing(list('ABCDEFGHI'))
|
||||
30 1 ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'S', 'I']
|
||||
35 2 ['A', 'B', 'S', 'D', 'E', 'F', 'G', 'S', 'I']
|
||||
36 10 ['A', 'B', 'O', 'D', 'E', 'F', 'G', 'S', 'I']
|
||||
41 11 ['A', 'B', 'O', 'D', 'O', 'F', 'G', 'S', 'I']
|
||||
46 13 ['A', 'B', 'O', 'D', 'O', 'C', 'G', 'S', 'I']
|
||||
48 14 ['A', 'M', 'O', 'D', 'O', 'C', 'G', 'S', 'I']
|
||||
55 16 ['A', 'M', 'L', 'D', 'O', 'C', 'G', 'S', 'I']
|
||||
60 17 ['A', 'M', 'L', 'D', 'O', 'C', 'G', 'S', 'E']
|
||||
67 23 ['A', 'M', 'L', 'D', 'O', 'A', 'G', 'S', 'E']
|
||||
70 29 ['A', 'B', 'L', 'D', 'O', 'A', 'G', 'S', 'E']
|
||||
73 33 ['A', 'N', 'L', 'D', 'O', 'A', 'G', 'S', 'E']
|
||||
80 55 ['A', 'N', 'L', 'D', 'O', 'A', 'G', 'S', 'W']
|
||||
84 115 ['A', 'N', 'R', 'D', 'O', 'A', 'G', 'S', 'W']
|
||||
100 116 ['A', 'N', 'R', 'D', 'O', 'A', 'G', 'S', 'T']
|
||||
111 140 ['E', 'N', 'R', 'D', 'O', 'A', 'G', 'S', 'T']
|
||||
123 169 ['E', 'P', 'R', 'D', 'O', 'A', 'G', 'S', 'T']
|
||||
|
||||
E P R
|
||||
D O A
|
||||
G S T
|
||||
(['E', 'P', 'R', 'D', 'O', 'A', 'G', 'S', 'T'], 123)
|
||||
|
||||
>>> random_weighted_selection(range(10), 3, lambda x: x * x)
|
||||
[8, 9, 6]
|
@ -0,0 +1,365 @@
|
||||
"""Statistical Language Processing tools. (Chapter 23)
|
||||
We define Unigram and Ngram text models, use them to generate random text,
|
||||
and show the Viterbi algorithm for segmentatioon of letters into words.
|
||||
Then we show a very simple Information Retrieval system, and an example
|
||||
working on a tiny sample of Unix manual pages."""
|
||||
|
||||
from utils import *
|
||||
from math import log, exp
|
||||
import re, probability, string, search
|
||||
|
||||
class CountingProbDist(probability.ProbDist):
|
||||
"""A probability distribution formed by observing and counting examples.
|
||||
If P is an instance of this class and o
|
||||
is an observed value, then there are 3 main operations:
|
||||
p.add(o) increments the count for observation o by 1.
|
||||
p.sample() returns a random element from the distribution.
|
||||
p[o] returns the probability for o (as in a regular ProbDist)."""
|
||||
|
||||
def __init__(self, observations=[], default=0):
|
||||
"""Create a distribution, and optionally add in some observations.
|
||||
By default this is an unsmoothed distribution, but saying default=1,
|
||||
for example, gives you add-one smoothing."""
|
||||
update(self, dictionary=DefaultDict(default), needs_recompute=False,
|
||||
table=[], n_obs=0)
|
||||
for o in observations:
|
||||
self.add(o)
|
||||
|
||||
def add(self, o):
|
||||
"""Add an observation o to the distribution."""
|
||||
self.dictionary[o] += 1
|
||||
self.n_obs += 1
|
||||
self.needs_recompute = True
|
||||
|
||||
def sample(self):
|
||||
"""Return a random sample from the distribution."""
|
||||
if self.needs_recompute: self._recompute()
|
||||
if self.n_obs == 0:
|
||||
return None
|
||||
i = bisect.bisect_left(self.table, (1 + random.randrange(self.n_obs),))
|
||||
(count, o) = self.table[i]
|
||||
return o
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Return an estimate of the probability of item."""
|
||||
if self.needs_recompute: self._recompute()
|
||||
return self.dictionary[item] / self.n_obs
|
||||
|
||||
def __len__(self):
|
||||
if self.needs_recompute: self._recompute()
|
||||
return self.n_obs
|
||||
|
||||
def top(self, n):
|
||||
"Return (count, obs) tuples for the n most frequent observations."
|
||||
items = [(v, k) for (k, v) in self.dictionary.items()]
|
||||
items.sort(); items.reverse()
|
||||
return items[0:n]
|
||||
|
||||
def _recompute(self):
|
||||
"""Recompute the total count n_obs and the table of entries."""
|
||||
n_obs = 0
|
||||
table = []
|
||||
for (o, count) in self.dictionary.items():
|
||||
n_obs += count
|
||||
table.append((n_obs, o))
|
||||
update(self, n_obs=float(n_obs), table=table, needs_recompute=False)
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class UnigramTextModel(CountingProbDist):
|
||||
"""This is a discrete probability distribution over words, so you
|
||||
can add, sample, or get P[word], just like with CountingProbDist. You can
|
||||
also generate a random text n words long with P.samples(n)"""
|
||||
|
||||
def samples(self, n):
|
||||
"Return a string of n words, random according to the model."
|
||||
return ' '.join([self.sample() for i in range(n)])
|
||||
|
||||
class NgramTextModel(CountingProbDist):
|
||||
"""This is a discrete probability distribution over n-tuples of words.
|
||||
You can add, sample or get P[(word1, ..., wordn)]. The method P.samples(n)
|
||||
builds up an n-word sequence; P.add_text and P.add_sequence add data."""
|
||||
|
||||
def __init__(self, n, observation_sequence=[]):
|
||||
## In addition to the dictionary of n-tuples, cond_prob is a
|
||||
## mapping from (w1, ..., wn-1) to P(wn | w1, ... wn-1)
|
||||
CountingProbDist.__init__(self)
|
||||
self.n = n
|
||||
self.cond_prob = DefaultDict(CountingProbDist())
|
||||
self.add_sequence(observation_sequence)
|
||||
|
||||
## sample, __len__, __getitem__ inherited from CountingProbDist
|
||||
## Note they deal with tuples, not strings, as inputs
|
||||
|
||||
def add(self, ngram):
|
||||
"""Count 1 for P[(w1, ..., wn)] and for P(wn | (w1, ..., wn-1)"""
|
||||
CountingProbDist.add(self, ngram)
|
||||
self.cond_prob[ngram[:-1]].add(ngram[-1])
|
||||
|
||||
def add_sequence(self, words):
|
||||
"""Add each of the tuple words[i:i+n], using a sliding window.
|
||||
Prefix some copies of the empty word, '', to make the start work."""
|
||||
n = self.n
|
||||
words = ['',] * (n-1) + words
|
||||
for i in range(len(words)-n):
|
||||
self.add(tuple(words[i:i+n]))
|
||||
|
||||
def samples(self, nwords):
|
||||
"""Build up a random sample of text n words long, using the"""
|
||||
n = self.n
|
||||
nminus1gram = ('',) * (n-1)
|
||||
output = []
|
||||
while len(output) < nwords:
|
||||
wn = self.cond_prob[nminus1gram].sample()
|
||||
if wn:
|
||||
output.append(wn)
|
||||
nminus1gram = nminus1gram[1:] + (wn,)
|
||||
else: ## Cannot continue, so restart.
|
||||
nminus1gram = ('',) * (n-1)
|
||||
return ' '.join(output)
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
|
||||
def viterbi_segment(text, P):
|
||||
"""Find the best segmentation of the string of characters, given the
|
||||
UnigramTextModel P."""
|
||||
# best[i] = best probability for text[0:i]
|
||||
# words[i] = best word ending at position i
|
||||
n = len(text)
|
||||
words = [''] + list(text)
|
||||
best = [1.0] + [0.0] * n
|
||||
## Fill in the vectors best, words via dynamic programming
|
||||
for i in range(n+1):
|
||||
for j in range(0, i):
|
||||
w = text[j:i]
|
||||
if P[w] * best[i - len(w)] >= best[i]:
|
||||
best[i] = P[w] * best[i - len(w)]
|
||||
words[i] = w
|
||||
## Now recover the sequence of best words
|
||||
sequence = []; i = len(words)-1
|
||||
while i > 0:
|
||||
sequence[0:0] = [words[i]]
|
||||
i = i - len(words[i])
|
||||
## Return sequence of best words and overall probability
|
||||
return sequence, best[-1]
|
||||
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
|
||||
class IRSystem:
|
||||
"""A very simple Information Retrieval System, as discussed in Sect. 23.2.
|
||||
The constructor s = IRSystem('the a') builds an empty system with two
|
||||
stopwords. Next, index several documents with s.index_document(text, url).
|
||||
Then ask queries with s.query('query words', n) to retrieve the top n
|
||||
matching documents. Queries are literal words from the document,
|
||||
except that stopwords are ignored, and there is one special syntax:
|
||||
The query "learn: man cat", for example, runs "man cat" and indexes it."""
|
||||
|
||||
def __init__(self, stopwords='the a of'):
|
||||
"""Create an IR System. Optionally specify stopwords."""
|
||||
## index is a map of {word: {docid: count}}, where docid is an int,
|
||||
## indicating the index into the documents list.
|
||||
update(self, index=DefaultDict(DefaultDict(0)),
|
||||
stopwords=set(words(stopwords)), documents=[])
|
||||
|
||||
def index_collection(self, filenames):
|
||||
"Index a whole collection of files."
|
||||
for filename in filenames:
|
||||
self.index_document(open(filename).read(), filename)
|
||||
|
||||
def index_document(self, text, url):
|
||||
"Index the text of a document."
|
||||
## For now, use first line for title
|
||||
title = text[:text.index('\n')].strip()
|
||||
docwords = words(text)
|
||||
docid = len(self.documents)
|
||||
self.documents.append(Document(title, url, len(docwords)))
|
||||
for word in docwords:
|
||||
if word not in self.stopwords:
|
||||
self.index[word][docid] += 1
|
||||
|
||||
def query(self, query_text, n=10):
|
||||
"""Return a list of n (score, docid) pairs for the best matches.
|
||||
Also handle the special syntax for 'learn: command'."""
|
||||
if query_text.startswith("learn:"):
|
||||
doctext = os.popen(query_text[len("learn:"):], 'r').read()
|
||||
self.index_document(doctext, query_text)
|
||||
return []
|
||||
qwords = [w for w in words(query_text) if w not in self.stopwords]
|
||||
shortest = argmin(qwords, lambda w: len(self.index[w]))
|
||||
docs = self.index[shortest]
|
||||
results = [(sum([self.score(w, d) for w in qwords]), d) for d in docs]
|
||||
results.sort(); results.reverse()
|
||||
return results[:n]
|
||||
|
||||
def score(self, word, docid):
|
||||
"Compute a score for this word on this docid."
|
||||
## There are many options; here we take a very simple approach
|
||||
return (math.log(1 + self.index[word][docid])
|
||||
/ math.log(1 + self.documents[docid].nwords))
|
||||
|
||||
def present(self, results):
|
||||
"Present the results as a list."
|
||||
for (score, d) in results:
|
||||
doc = self.documents[d]
|
||||
print "%5.2f|%25s | %s" % (100 * score, doc.url, doc.title[:45])
|
||||
|
||||
def present_results(self, query_text, n=10):
|
||||
"Get results for the query and present them."
|
||||
self.present(self.query(query_text, n))
|
||||
|
||||
class UnixConsultant(IRSystem):
|
||||
"""A trivial IR system over a small collection of Unix man pages."""
|
||||
def __init__(self):
|
||||
IRSystem.__init__(self, stopwords="how do i the a of")
|
||||
import os
|
||||
mandir = '../data/man/'
|
||||
man_files = [mandir + f for f in os.listdir(mandir)]
|
||||
self.index_collection(man_files)
|
||||
|
||||
class Document:
|
||||
"""Metadata for a document: title and url; maybe add others later."""
|
||||
def __init__(self, title, url, nwords):
|
||||
update(self, title=title, url=url, nwords=nwords)
|
||||
|
||||
def words(text, reg=re.compile('[a-z0-9]+')):
|
||||
"""Return a list of the words in text, ignoring punctuation and
|
||||
converting everything to lowercase (to canonicalize).
|
||||
>>> words("``EGAD!'' Edgar cried.")
|
||||
['egad', 'edgar', 'cried']
|
||||
"""
|
||||
return reg.findall(text.lower())
|
||||
|
||||
def canonicalize(text):
|
||||
"""Return a canonical text: only lowercase letters and blanks.
|
||||
>>> canonicalize("``EGAD!'' Edgar cried.")
|
||||
'egad edgar cried'
|
||||
"""
|
||||
return ' '.join(words(text))
|
||||
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
## Example application (not in book): decode a cipher.
|
||||
## A cipher is a code that substitutes one character for another.
|
||||
## A shift cipher is a rotation of the letters in the alphabet,
|
||||
## such as the famous rot13, which maps A to N, B to M, etc.
|
||||
|
||||
#### Encoding
|
||||
|
||||
def shift_encode(plaintext, n):
|
||||
"""Encode text with a shift cipher that moves each letter up by n letters.
|
||||
>>> shift_encode('abc z', 1)
|
||||
'bcd a'
|
||||
"""
|
||||
return encode(plaintext, alphabet[n:] + alphabet[:n])
|
||||
|
||||
def rot13(plaintext):
|
||||
"""Encode text by rotating letters by 13 spaces in the alphabet.
|
||||
>>> rot13('hello')
|
||||
'uryyb'
|
||||
>>> rot13(rot13('hello'))
|
||||
'hello'
|
||||
"""
|
||||
return shift_encode(plaintext, 13)
|
||||
|
||||
def encode(plaintext, code):
|
||||
"Encodes text, using a code which is a permutation of the alphabet."
|
||||
from string import maketrans
|
||||
trans = maketrans(alphabet + alphabet.upper(), code + code.upper())
|
||||
return plaintext.translate(trans)
|
||||
|
||||
alphabet = 'abcdefghijklmnopqrstuvwxyz'
|
||||
|
||||
def bigrams(text):
|
||||
"""Return a list of pairs in text (a sequence of letters or words).
|
||||
>>> bigrams('this')
|
||||
['th', 'hi', 'is']
|
||||
>>> bigrams(['this', 'is', 'a', 'test'])
|
||||
[['this', 'is'], ['is', 'a'], ['a', 'test']]
|
||||
"""
|
||||
return [text[i:i+2] for i in range(len(text) - 1)]
|
||||
|
||||
#### Decoding a Shift (or Caesar) Cipher
|
||||
|
||||
class ShiftDecoder:
|
||||
"""There are only 26 possible encodings, so we can try all of them,
|
||||
and return the one with the highest probability, according to a
|
||||
bigram probability distribution."""
|
||||
def __init__(self, training_text):
|
||||
training_text = canonicalize(training_text)
|
||||
self.P2 = CountingProbDist(bigrams(training_text), default=1)
|
||||
|
||||
def score(self, plaintext):
|
||||
"Return a score for text based on how common letters pairs are."
|
||||
s = 1.0
|
||||
for bi in bigrams(plaintext):
|
||||
s = s * self.P2[bi]
|
||||
return s
|
||||
|
||||
def decode(self, ciphertext):
|
||||
"Return the shift decoding of text with the best score."
|
||||
return argmax(all_shifts(ciphertext), self.score)
|
||||
|
||||
def all_shifts(text):
|
||||
"Return a list of all 26 possible encodings of text by a shift cipher."
|
||||
return [shift_encode(text, n) for n in range(len(alphabet))]
|
||||
|
||||
#### Decoding a General Permutation Cipher
|
||||
|
||||
class PermutationDecoder:
|
||||
"""This is a much harder problem than the shift decoder. There are 26!
|
||||
permutations, so we can't try them all. Instead we have to search.
|
||||
We want to search well, but there are many things to consider:
|
||||
Unigram probabilities (E is the most common letter); Bigram probabilities
|
||||
(TH is the most common bigram); word probabilities (I and A are the most
|
||||
common one-letter words, etc.); etc.
|
||||
We could represent a search state as a permutation of the 26 letters,
|
||||
and alter the solution through hill climbing. With an initial guess
|
||||
based on unigram probabilities, this would probably fair well. However,
|
||||
I chose instead to have an incremental representation. A state is
|
||||
represented as a letter-to-letter map; for example {'z': 'e'} to
|
||||
represent that 'z' will be translated to 'e'
|
||||
"""
|
||||
def __init__(self, training_text, ciphertext=None):
|
||||
self.Pwords = UnigramTextModel(words(training_text))
|
||||
self.P1 = UnigramTextModel(training_text) # By letter
|
||||
self.P2 = NgramTextModel(2, training_text) # By letter pair
|
||||
if ciphertext:
|
||||
return self.decode(ciphertext)
|
||||
|
||||
def decode(self, ciphertext):
|
||||
"Search for a decoding of the ciphertext."
|
||||
self.ciphertext = ciphertext
|
||||
problem = PermutationDecoderProblem(decoder=self)
|
||||
return search.best_first_tree_search(problem, self.score)
|
||||
|
||||
def score(self, ciphertext, code):
|
||||
"""Score is product of word scores, unigram scores, and bigram scores.
|
||||
This can get very small, so we use logs and exp."""
|
||||
text = decode(ciphertext, code)
|
||||
logP = (sum([log(self.Pwords[word]) for word in words(text)]) +
|
||||
sum([log(self.P1[c]) for c in text]) +
|
||||
sum([log(self.P2[b]) for b in bigrams(text)]))
|
||||
return exp(logP)
|
||||
|
||||
class PermutationDecoderProblem(search.Problem):
|
||||
def __init__(self, initial=None, goal=None, decoder=None):
|
||||
self.initial = initial or {}
|
||||
self.decoder = decoder
|
||||
|
||||
def successors(self, state):
|
||||
## Find the best
|
||||
p, plainchar = max([(self.decoder.P1[c], c)
|
||||
for c in alphabet if c not in state])
|
||||
succs = [extend(state, plainchar, cipherchar)] #????
|
||||
|
||||
def goal_test(self, state):
|
||||
"We're done when we get all 26 letters assigned."
|
||||
return len(state) >= 26
|
||||
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
@ -0,0 +1,122 @@
|
||||
## Create a Unigram text model from the words in the book "Flatland".
|
||||
>>> flatland = DataFile("flat11.txt").read()
|
||||
>>> wordseq = words(flatland)
|
||||
>>> P = UnigramTextModel(wordseq)
|
||||
|
||||
## Now do segmentation, using the text model as a prior.
|
||||
>>> s, p = viterbi_segment('itiseasytoreadwordswithoutspaces', P)
|
||||
>>> s
|
||||
['it', 'is', 'easy', 'to', 'read', 'words', 'without', 'spaces']
|
||||
>>> 1e-30 < p < 1e-20
|
||||
True
|
||||
>>> s, p = viterbi_segment('wheninthecourseofhumaneventsitbecomesnecessary', P)
|
||||
>>> s
|
||||
['when', 'in', 'the', 'course', 'of', 'human', 'events', 'it', 'becomes', 'necessary']
|
||||
|
||||
## Test the decoding system
|
||||
>>> shift_encode("This is a secret message.", 17)
|
||||
'Kyzj zj r jvtivk dvjjrxv.'
|
||||
|
||||
>>> ring = ShiftDecoder(flatland)
|
||||
>>> ring.decode('Kyzj zj r jvtivk dvjjrxv.')
|
||||
'This is a secret message.'
|
||||
>>> ring.decode(rot13('Hello, world!'))
|
||||
'Hello, world!'
|
||||
|
||||
## CountingProbDist
|
||||
## Add a thousand samples of a roll of a die to D.
|
||||
>>> D = CountingProbDist()
|
||||
>>> for i in range(10000):
|
||||
... D.add(random.choice('123456'))
|
||||
>>> ps = [D[n] for n in '123456']
|
||||
>>> 1./7. <= min(ps) <= max(ps) <= 1./5.
|
||||
True
|
||||
|
||||
## demo
|
||||
|
||||
## Compare 1-, 2-, and 3-gram word models of the same text.
|
||||
>>> flatland = DataFile("flat11.txt").read()
|
||||
>>> wordseq = words(flatland)
|
||||
>>> P1 = UnigramTextModel(wordseq)
|
||||
>>> P2 = NgramTextModel(2, wordseq)
|
||||
>>> P3 = NgramTextModel(3, wordseq)
|
||||
|
||||
## Generate random text from the N-gram models
|
||||
>>> P1.samples(20)
|
||||
'you thought known but were insides of see in depend by us dodecahedrons just but i words are instead degrees'
|
||||
|
||||
>>> P2.samples(20)
|
||||
'flatland well then can anything else more into the total destruction and circles teach others confine women must be added'
|
||||
|
||||
>>> P3.samples(20)
|
||||
'flatland by edwin a abbott 1884 to the wake of a certificate from nature herself proving the equal sided triangle'
|
||||
|
||||
## The most frequent entries in each model
|
||||
>>> P1.top(10)
|
||||
[(2081, 'the'), (1479, 'of'), (1021, 'and'), (1008, 'to'), (850, 'a'), (722, 'i'), (640, 'in'), (478, 'that'), (399, 'is'), (348, 'you')]
|
||||
|
||||
>>> P2.top(10)
|
||||
[(368, ('of', 'the')), (152, ('to', 'the')), (152, ('in', 'the')), (86, ('of', 'a')), (80, ('it', 'is')), (71, ('by', 'the')), (68, ('for', 'the')), (68, ('and', 'the')), (62, ('on', 'the')), (60, ('to', 'be'))]
|
||||
|
||||
>>> P3.top(10)
|
||||
[(30, ('a', 'straight', 'line')), (19, ('of', 'three', 'dimensions')), (16, ('the', 'sense', 'of')), (13, ('by', 'the', 'sense')), (13, ('as', 'well', 'as')), (12, ('of', 'the', 'circles')), (12, ('of', 'sight', 'recognition')), (11, ('the', 'number', 'of')), (11, ('that', 'i', 'had')), (11, ('so', 'as', 'to'))]
|
||||
|
||||
## Probabilities of some common n-grams
|
||||
>>> P1['the']
|
||||
0.061139348356200607
|
||||
|
||||
>>> P2[('of', 'the')]
|
||||
0.010812081325655188
|
||||
|
||||
>>> P3[('', '', 'but')]
|
||||
0.0
|
||||
|
||||
>>> P3[('so', 'as', 'to')]
|
||||
0.00032318721353860618
|
||||
|
||||
## Distributions given the previous n-1 words
|
||||
>>> P2.cond_prob['went',].dictionary
|
||||
>>> P3.cond_prob['in', 'order'].dictionary
|
||||
{'to': 6}
|
||||
|
||||
## Build and test an IR System
|
||||
>>> uc = UnixConsultant()
|
||||
>>> uc.present_results("how do I remove a file")
|
||||
76.83| ../data/man/rm.txt | RM(1) FSF RM(1)
|
||||
67.83| ../data/man/tar.txt | TAR(1) TAR(1)
|
||||
67.79| ../data/man/cp.txt | CP(1) FSF CP(1)
|
||||
66.58| ../data/man/zip.txt | ZIP(1L) ZIP(1L)
|
||||
64.58| ../data/man/gzip.txt | GZIP(1) GZIP(1)
|
||||
63.74| ../data/man/pine.txt | pine(1) pine(1)
|
||||
62.95| ../data/man/shred.txt | SHRED(1) FSF SHRED(1)
|
||||
57.46| ../data/man/pico.txt | pico(1) pico(1)
|
||||
43.38| ../data/man/login.txt | LOGIN(1) Linux Programmer's Manual
|
||||
41.93| ../data/man/ln.txt | LN(1) FSF LN(1)
|
||||
|
||||
>>> uc.present_results("how do I delete a file")
|
||||
75.47| ../data/man/diff.txt | DIFF(1) GNU Tools DIFF(1)
|
||||
69.12| ../data/man/pine.txt | pine(1) pine(1)
|
||||
63.56| ../data/man/tar.txt | TAR(1) TAR(1)
|
||||
60.63| ../data/man/zip.txt | ZIP(1L) ZIP(1L)
|
||||
57.46| ../data/man/pico.txt | pico(1) pico(1)
|
||||
51.28| ../data/man/shred.txt | SHRED(1) FSF SHRED(1)
|
||||
26.72| ../data/man/tr.txt | TR(1) User Commands TR(1)
|
||||
|
||||
>>> uc.present_results("email")
|
||||
18.39| ../data/man/pine.txt | pine(1) pine(1)
|
||||
12.01| ../data/man/info.txt | INFO(1) FSF INFO(1)
|
||||
9.89| ../data/man/pico.txt | pico(1) pico(1)
|
||||
8.73| ../data/man/grep.txt | GREP(1) GREP(1)
|
||||
8.07| ../data/man/zip.txt | ZIP(1L) ZIP(1L)
|
||||
|
||||
>>> uc.present_results("word counts for files")
|
||||
112.38| ../data/man/grep.txt | GREP(1) GREP(1)
|
||||
101.84| ../data/man/wc.txt | WC(1) User Commands WC(1)
|
||||
82.46| ../data/man/find.txt | FIND(1L) FIND(1L)
|
||||
74.64| ../data/man/du.txt | DU(1) FSF DU(1)
|
||||
|
||||
>>> uc.present_results("learn: date")
|
||||
>>> uc.present_results("2003")
|
||||
14.58| ../data/man/pine.txt | pine(1) pine(1)
|
||||
11.62| ../data/man/jar.txt | FASTJAR(1) GNU FASTJAR(1)
|
||||
|
@ -0,0 +1,169 @@
|
||||
>>> d = DefaultDict(0)
|
||||
>>> d['x'] += 1
|
||||
>>> d['x']
|
||||
1
|
||||
|
||||
>>> d = DefaultDict([])
|
||||
>>> d['x'] += [1]
|
||||
>>> d['y'] += [2]
|
||||
>>> d['x']
|
||||
[1]
|
||||
|
||||
>>> s = Struct(a=1, b=2)
|
||||
>>> s.a
|
||||
1
|
||||
>>> s.a = 3
|
||||
>>> s
|
||||
Struct(a=3, b=2)
|
||||
|
||||
>>> def is_even(x):
|
||||
... return x % 2 == 0
|
||||
>>> sorted([1, 2, -3])
|
||||
[-3, 1, 2]
|
||||
>>> sorted(range(10), key=is_even)
|
||||
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]
|
||||
>>> sorted(range(10), lambda x,y: y-x)
|
||||
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
|
||||
|
||||
>>> removeall(4, [])
|
||||
[]
|
||||
>>> removeall('s', 'This is a test. Was a test.')
|
||||
'Thi i a tet. Wa a tet.'
|
||||
>>> removeall('s', 'Something')
|
||||
'Something'
|
||||
>>> removeall('s', '')
|
||||
''
|
||||
|
||||
>>> list(reversed([]))
|
||||
[]
|
||||
|
||||
>>> count_if(is_even, [1, 2, 3, 4])
|
||||
2
|
||||
>>> count_if(is_even, [])
|
||||
0
|
||||
|
||||
>>> argmax([1], lambda x: x*x)
|
||||
1
|
||||
>>> argmin([1], lambda x: x*x)
|
||||
1
|
||||
|
||||
|
||||
# Test of memoize with slots in structures
|
||||
>>> countries = [Struct(name='united states'), Struct(name='canada')]
|
||||
|
||||
# Pretend that 'gnp' was some big hairy operation:
|
||||
>>> def gnp(country):
|
||||
... print 'calculating gnp ...'
|
||||
... return len(country.name) * 1e10
|
||||
|
||||
>>> gnp = memoize(gnp, '_gnp')
|
||||
>>> map(gnp, countries)
|
||||
calculating gnp ...
|
||||
calculating gnp ...
|
||||
[130000000000.0, 60000000000.0]
|
||||
>>> countries
|
||||
[Struct(_gnp=130000000000.0, name='united states'), Struct(_gnp=60000000000.0, name='canada')]
|
||||
|
||||
# This time we avoid re-doing the calculation
|
||||
>>> map(gnp, countries)
|
||||
[130000000000.0, 60000000000.0]
|
||||
|
||||
# Test Queues:
|
||||
>>> nums = [1, 8, 2, 7, 5, 6, -99, 99, 4, 3, 0]
|
||||
>>> def qtest(q):
|
||||
... return [q.extend(nums), [q.pop() for i in range(len(q))]][1]
|
||||
|
||||
>>> qtest(Stack())
|
||||
[0, 3, 4, 99, -99, 6, 5, 7, 2, 8, 1]
|
||||
|
||||
>>> qtest(FIFOQueue())
|
||||
[1, 8, 2, 7, 5, 6, -99, 99, 4, 3, 0]
|
||||
|
||||
>>> qtest(PriorityQueue(min))
|
||||
[-99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 99]
|
||||
|
||||
>>> qtest(PriorityQueue(max))
|
||||
[99, 8, 7, 6, 5, 4, 3, 2, 1, 0, -99]
|
||||
|
||||
>>> qtest(PriorityQueue(min, abs))
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, -99, 99]
|
||||
|
||||
>>> qtest(PriorityQueue(max, abs))
|
||||
[99, -99, 8, 7, 6, 5, 4, 3, 2, 1, 0]
|
||||
|
||||
>>> vals = [100, 110, 160, 200, 160, 110, 200, 200, 220]
|
||||
>>> histogram(vals)
|
||||
[(100, 1), (110, 2), (160, 2), (200, 3), (220, 1)]
|
||||
>>> histogram(vals, 1)
|
||||
[(200, 3), (110, 2), (160, 2), (220, 1), (100, 1)]
|
||||
>>> histogram(vals, 1, lambda v: round(v, -2))
|
||||
[(200.0, 6), (100.0, 3)]
|
||||
|
||||
>>> log2(1.0)
|
||||
0.0
|
||||
|
||||
>>> def fib(n):
|
||||
... return (n<=1 and 1) or (fib(n-1) + fib(n-2))
|
||||
|
||||
>>> fib(9)
|
||||
55
|
||||
|
||||
# Now we make it faster:
|
||||
>>> fib = memoize(fib)
|
||||
>>> fib(9)
|
||||
55
|
||||
|
||||
>>> q = Stack()
|
||||
>>> q.append(1)
|
||||
>>> q.append(2)
|
||||
>>> q.pop(), q.pop()
|
||||
(2, 1)
|
||||
|
||||
>>> q = FIFOQueue()
|
||||
>>> q.append(1)
|
||||
>>> q.append(2)
|
||||
>>> q.pop(), q.pop()
|
||||
(1, 2)
|
||||
|
||||
|
||||
>>> abc = set('abc')
|
||||
>>> bcd = set('bcd')
|
||||
>>> 'a' in abc
|
||||
True
|
||||
>>> 'a' in bcd
|
||||
False
|
||||
>>> list(abc.intersection(bcd))
|
||||
['c', 'b']
|
||||
>>> list(abc.union(bcd))
|
||||
['a', 'c', 'b', 'd']
|
||||
|
||||
## From "What's new in Python 2.4", but I added calls to sl
|
||||
|
||||
>>> def sl(x):
|
||||
... return sorted(list(x))
|
||||
|
||||
|
||||
>>> a = set('abracadabra') # form a set from a string
|
||||
>>> 'z' in a # fast membership testing
|
||||
False
|
||||
>>> sl(a) # unique letters in a
|
||||
['a', 'b', 'c', 'd', 'r']
|
||||
|
||||
>>> b = set('alacazam') # form a second set
|
||||
>>> sl(a - b) # letters in a but not in b
|
||||
['b', 'd', 'r']
|
||||
>>> sl(a | b) # letters in either a or b
|
||||
['a', 'b', 'c', 'd', 'l', 'm', 'r', 'z']
|
||||
>>> sl(a & b) # letters in both a and b
|
||||
['a', 'c']
|
||||
>>> sl(a ^ b) # letters in a or b but not both
|
||||
['b', 'd', 'l', 'm', 'r', 'z']
|
||||
|
||||
|
||||
>>> a.add('z') # add a new element
|
||||
>>> a.update('wxy') # add multiple new elements
|
||||
>>> sl(a)
|
||||
['a', 'b', 'c', 'd', 'r', 'w', 'x', 'y', 'z']
|
||||
>>> a.remove('x') # take one element out
|
||||
>>> sl(a)
|
||||
['a', 'b', 'c', 'd', 'r', 'w', 'y', 'z']
|
Loading…
Reference in New Issue