Rework of chapter 5
This commit is contained in:
parent
4a6a542f8d
commit
440741fa3d
@ -1,99 +1,323 @@
|
||||
# Constraint Satisfaction Problems
|
||||
|
||||
Book pg. 164 to 169
|
||||
> [!NOTE]
|
||||
> From now on States have a factored representation of the following type
|
||||
>
|
||||
> - **Variables** ($\mathcal{X}$): Actual variables in the State
|
||||
> - **Domains**($\mathcal{D}$): Set of values that a variable can get (one per variable)
|
||||
> - **Constraints** ($\mathcal{C}$): Constraints about values assignable to variables
|
||||
|
||||
We have 3 components
|
||||
These problems concern about finding the right assignment of values to all variables so that
|
||||
the assignment respects all constraints. For example, let's imagine that we could only get
|
||||
cats in spaces with other pets, but small ones. This is indeed a CSP problem where we need to find
|
||||
a solution where cats are never with small pets.
|
||||
|
||||
- $\mathcal{X}$: Set of variables
|
||||
- $\mathcal{D}$: Set of domains, one (domain) for each variable
|
||||
- $\mathcal{C}$: Set of constraints
|
||||
```python
|
||||
class StateCSP(State):
|
||||
|
||||
Here we try to assign a value to variables, such that each variable holds a value contained in their
|
||||
domain, without violating any constraint.
|
||||
def __init__(
|
||||
self,
|
||||
variables: dict[str, Value],
|
||||
domains: dict[str, set[Value]],
|
||||
# The scope is represented as a list of strings
|
||||
# because these are variables in this representation
|
||||
constraints: dict[list[str], Rule | set[Value]]
|
||||
):
|
||||
self.__variables = variables
|
||||
self.__domains = domains
|
||||
self.__constraints = constraints
|
||||
|
||||
A solution to this problem must be:
|
||||
```
|
||||
|
||||
- Consistent: Each assignment is legal
|
||||
- Complete: All variables have a value assigned
|
||||
In this space, in constrast with the atomic one, we can see if we are doing a mistake during assignments. Whenever we detect such
|
||||
mistakes, we **prune** that path, since it is **not admissable**. This makes the computation a bit faster, since we can detect
|
||||
early failures.
|
||||
|
||||
## Inference in CSP
|
||||
## Constraints
|
||||
|
||||
We can inference a solution in these ways
|
||||
> [!CAUTION]
|
||||
> Constraints can be of these types:
|
||||
>
|
||||
> - **Unary**: One variable involved (For example Bob does not like Shirts)
|
||||
> - **Binary**: Two variables involved
|
||||
> - **Global**: More than Two variables are involved
|
||||
|
||||
- Generate successors and if not consistent assignments, prune and continue
|
||||
- Constraint Propagation:\
|
||||
Use contraints to reduce possible assignments and mix with search, if needed
|
||||
### Preference Contstraints
|
||||
|
||||
The idea is to treat each assignment as a **vertex** and each contraint as an **edge**, we our tree would not have inconsistent values, making
|
||||
the search faster.
|
||||
These are constraints that **should be** followed, but technically can be ignored as far as the problem is concerned.
|
||||
|
||||
## Consistency
|
||||
In order to account for these preferences, it is possible to **assign a higher cost** to moves that **break these constraints**
|
||||
|
||||
### Node consistency
|
||||
> [!NOTE]
|
||||
> In these cases we could also talk about COPs (Constraint Optimization Problems)
|
||||
|
||||
A domain already satisfies constraints
|
||||
### Common Global Constraints
|
||||
|
||||
- **Aldiff (all different)**: Each variable in the constraint must have a unique value
|
||||
- **Heuristic**: If the set of all values has a size lower than the number of variables, then it is impossible
|
||||
- **Heuristic Algorithm**:
|
||||
|
||||
```python
|
||||
def aldiff_heuristic(csp_problem):
|
||||
|
||||
# May be optimized with a heap or priority queue
|
||||
# sorted by domain_size
|
||||
variables : Queue = csp_problem.variables
|
||||
possible_values = set()
|
||||
|
||||
for variable in variables:
|
||||
prossible_values.extend(
|
||||
variable.domain
|
||||
)
|
||||
|
||||
halt = False
|
||||
|
||||
while not halt:
|
||||
|
||||
# Count variables to avoid overflow
|
||||
remaining_variables = len(variables)
|
||||
# Check if there were eliminable variables
|
||||
variables_with_singleton_domain = 0
|
||||
|
||||
# Check if any of these variables in a singleton
|
||||
# variable
|
||||
for _ in range(0, remaining_variables):
|
||||
|
||||
candidate_variable = variables.pop()
|
||||
domain_size = len(variable.domain)
|
||||
|
||||
# Check for domain size
|
||||
match(domain_size):
|
||||
|
||||
# Empty domain, problem can't be solved
|
||||
case 0:
|
||||
return false
|
||||
|
||||
# Singleton domain
|
||||
case 1:
|
||||
|
||||
# Add count
|
||||
variables_with_singleton_domain += 1
|
||||
# Take value
|
||||
value = candidate_variable.domain[0]
|
||||
# Remove value from all possible values
|
||||
possible_values.remove(value)
|
||||
|
||||
# Remove value from all domains
|
||||
for variable in variables:
|
||||
variable.domain.remove(value)
|
||||
break
|
||||
|
||||
# Variable has more values, reinsert in queue
|
||||
_:
|
||||
variables.push(variable)
|
||||
|
||||
# No singleton variables found, algorithm
|
||||
# would stall, halt now
|
||||
if variables_with_singleton_domain == 0:
|
||||
halt = True
|
||||
|
||||
# Check that we have enough values to
|
||||
# assign to all variables
|
||||
return len(possible_values) >= len(variables)
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
- **Atmost**: It defines a maximum amount of a resource
|
||||
- **Heuristic**: Check if the sum of all minimum values in all scope variables domain is higher than the max value
|
||||
|
||||
## Constraint Propagation
|
||||
|
||||
It's a step in which we **prune** some **values** from **domains**. This step can be alternated with search or be a preprocessing step.
|
||||
|
||||
For the sake of this step, we'll construct some graphs where **variables** are nodes and **binary-constraints** are edges.
|
||||
|
||||
> [!WARNING]
|
||||
> We are not talking about search graphs, as this is another step unreleated with search
|
||||
|
||||
> [!TIP]
|
||||
> Even though it's not a search, sometimes it can solve our problem before even beginning to search. Moreover each global constraint can become a binary constraint,
|
||||
> making this step always possible to implement (at a higher computational cost of either the machine or the human transforming these constraints)
|
||||
|
||||
### Node Consistency
|
||||
|
||||
A **node** is **node-consistent** when **all values in its domain satifies its unary constraints**
|
||||
|
||||
### Arc Consistency
|
||||
|
||||
A variable is arc consistent wrt another variable id its domain respects all constraints
|
||||
A **node** is **arc-consistent** when **all values in its domain satisfies all of binary constraints it is involved to**
|
||||
|
||||
> [!WARNING]
|
||||
> Arco Consistency is not useful for every problem
|
||||
AC3 is an Algorithm that can help in achieving this with a Time Complexity of $O(n^3)$ or $O(\text{constraints} \cdot \text{size\_values}^3)$
|
||||
|
||||
> [!NOTE]
|
||||
> AC-3 is an algorithm to achieve Arc Consistency (alorithm on page 171 book)
|
||||
```python
|
||||
def AC3(csp_problem):
|
||||
|
||||
# Take all constraints
|
||||
queue : queue[Constraint] = csp_problem.edges
|
||||
|
||||
# Examine all constraints
|
||||
while len(queue) > 0:
|
||||
|
||||
# Take the first and initialize relevant variables
|
||||
constraint = queue.pop
|
||||
X = constraint.scope[0]
|
||||
Y = constraint.scope[1]
|
||||
|
||||
# Correct domain if needed
|
||||
correction_applied = apply_constraint([X, Y], rule)
|
||||
|
||||
# If correction was applied, check that there
|
||||
# are still admissable variables
|
||||
if correction_applied:
|
||||
|
||||
# If there aren't, then notify problem is not
|
||||
# solvable
|
||||
if len(X.domain) == 0:
|
||||
return False
|
||||
|
||||
# If there are still admissable variables, focus
|
||||
# on its neighbours
|
||||
for neighbour in (X.neighbours - Y):
|
||||
queue.push(csp_problem.search(neighbour, X))
|
||||
|
||||
# Notify that problem has been correctly constrained
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def apply_constraint(variables: list[Variable], rule: Rule):
|
||||
|
||||
# Gather all sets of values
|
||||
x_domain = set(variables[0].domain)
|
||||
y_domain = set(variables[1].domain)
|
||||
|
||||
# Set modified to false
|
||||
x_domain_modified = False
|
||||
|
||||
# Check if all variables satisfy constraint
|
||||
for x in x_domain:
|
||||
rule_satisfied = False
|
||||
|
||||
# If a single y makes the couple satisfy the constraint,
|
||||
# break early
|
||||
for y in y_domain
|
||||
if rule.check(x, y):
|
||||
rule_satisfied = True
|
||||
break
|
||||
|
||||
# If no y satisfied the couple (x, y) then remove x
|
||||
if not rule_satisfied:
|
||||
x_domain.remove(x)
|
||||
x_domain_modified = True
|
||||
|
||||
# Return if domain was modified
|
||||
return x_domain_modified
|
||||
```
|
||||
|
||||
### Path Consistency
|
||||
|
||||
It is achievable on 3 variables and says that a set of 2 variable is path consistent wrt a 3rd only if for each assignment
|
||||
consistent with constraints between these variables makes it possible to make a legal assignment on the 3rd
|
||||
A **set** of **2 variables** is **path consistent to a 3rd variable** if for **each valid assignment satisfying their arc consistency** there exists **at least a value** for
|
||||
a **3rd variable** so that it **satisfies its binary constraints with the other two variables**
|
||||
|
||||
### K-Consistency
|
||||
|
||||
basically the same as the one above, but for k-variables, technically we need $O(n^2 d)$ to check if a problem is
|
||||
total k-consistent (consistent from 1 up to k), however it is expensive and only 2-Consistency is usually computed.
|
||||
A **variable** is **k-consistent** if it's also **(k-1)-consistent** down to **1-consistent (node-consistent)**
|
||||
|
||||
## Global Contraints
|
||||
### Bound Propagation
|
||||
|
||||
Constraints that take an arbitrary number of variables (but not necessarily all).
|
||||
Sometimes, when we deal with lots of numbers, it's better to represent possible values as a bound (or a union of bounds).
|
||||
|
||||
### Checking for Aldiff Constraint
|
||||
In this case we need to check that there exists a solution for both the maximum and minimum values for all variables in order to
|
||||
have **bound-consistency**
|
||||
|
||||
Aldiff constraint says that all variables must have different values.
|
||||
## Backtracking Search
|
||||
|
||||
A way to check for inconsistencies is to remove variables with single value domains and remove such value
|
||||
from all domains, then iterate until there are no more single value domain variables.
|
||||
This is a step coming **after** the **constraint propagation** when we have multiple values assignable to variables and is a *traditional search*
|
||||
over the problem.
|
||||
|
||||
If an empty domain is produced, or there are more variables than domain values, congrats!! You found an inconsistency
|
||||
It can be interleaved with [constraint propagation](#constraint-propagation) at each step to see if we are breaking a constraint
|
||||
|
||||
### Checking for Atmost Constraint
|
||||
### Commutativity
|
||||
|
||||
Atmost constraint says that we can assign max tot resources
|
||||
Since our problem **may** not care about the specific value we may take, we can just consider the number of assignments.
|
||||
In other words, we don't care much about the value we are assigning, but rather about the assignment itself
|
||||
|
||||
Sum all minimum resources for each problem, and if it goes over the max, we have an inconsistency
|
||||
### Heuristics
|
||||
|
||||
### Bound Consistency
|
||||
To make the algorithm faster, we can have some heuristics to help us choose the best strategy:
|
||||
|
||||
We check that for each variable X and for both the lower and higher bound of X there exists a value of U that satisfies the constraints between X and each Y.
|
||||
- **Choose Variable to Assign**:
|
||||
- **minimum-remaining-values (MRV)**: consists on choosing the **variable with the least amount of values in its domain**
|
||||
- **Pros**:
|
||||
- Detection of Failures is faster
|
||||
- **Cons**:
|
||||
- There may be still ties among variables
|
||||
- **degree heuristic**: choose the **variable involved the most in constraints**
|
||||
- **Pros**:
|
||||
- Fast detection of Failures
|
||||
- Can be a tie-breaker combined with MRV
|
||||
- **Cons**:
|
||||
- Worse results than MRV
|
||||
- **Choose Value to assign**:
|
||||
- **least-constraining-value**: choose the **value that rules out the fewest choices among the variable neighbours**
|
||||
- **Pros**:
|
||||
- We only case about assigning, not what we assign
|
||||
- **Inference**:
|
||||
- **forward checking**: **propagate constraintsfor all unassigned variables**
|
||||
- **Pros**:
|
||||
- Fast
|
||||
- **Cons**:
|
||||
- Does not detect all inconsistencies
|
||||
- **MAC - Maintaining Arc Consistency**: It's a **call to [AC3](#arc-consistency)** only with **constrainst involving our assigned variable**
|
||||
- **Pros**:
|
||||
- Detects all inconsistencies
|
||||
- **Cons**:
|
||||
- Slower than forward checking
|
||||
- **Backtracking - Where should we go back once we fail**
|
||||
- **chronological backtracing**: go a step back
|
||||
- **Pros**:
|
||||
- Easy to implement
|
||||
- **Cons**:
|
||||
- Slower time of computation
|
||||
- **backjumping**: We go to a step where we made an **assignment that led to a conflict**. This is accomplished by having conflicts sets, sets where we track\
|
||||
**variable assignments** to **variables involved in constraints**
|
||||
- **Pros**:
|
||||
- Faster time of computation
|
||||
- **Cons**:
|
||||
- Need to track a conflict set
|
||||
- Useless if any **inference** heuristic has been used
|
||||
- **conflict-directed backjumping**: We jump to the assignment that led to a **chain of conflicts**. Now, each time an we detect a failure, we propagate the conflict\
|
||||
set over the backtraced variable **recursively**
|
||||
- **Pros**:
|
||||
- Faster time of computation
|
||||
- **Cons**:
|
||||
- Harder to implement
|
||||
- **Constraint Learning**
|
||||
- **no-good**: We record a set of variables that lead to a failure. Whenever we meet such a set, we fail immediately, or we avoid it completely
|
||||
- **Pros**:
|
||||
- Faster time of computation
|
||||
- **Cons**:
|
||||
- Memory expensive
|
||||
|
||||
## Backtracking Search in CSP
|
||||
## Local Search
|
||||
|
||||
Book pg 175 to 178
|
||||
The idea is that instead of **building** our goal state, we start from a **completely assigned state** (usually randomically) and we proceed from there on.
|
||||
|
||||
After finishing the constraint propagation, reducing our options, we still need to search for a solution.
|
||||
Can also be useful during online searching
|
||||
|
||||
- The order of values is not relevant (we can save up on computation and memoru)
|
||||
### Local Search Heuristics
|
||||
|
||||
### Forward Checking
|
||||
|
||||
<!-- TODO -->
|
||||
|
||||
### Intelligent Backtracking (Backjumping)
|
||||
|
||||
<!-- TODO -->
|
||||
|
||||
## The structure of Problems
|
||||
|
||||
<!-- TODO -->
|
||||
|
||||
Book from 183 to 187
|
||||
- **min-conflicts**: Change a random variable (that has conflcts) to a value that would minimize conflicts
|
||||
- **Pros**:
|
||||
- Almost problem size independent
|
||||
- Somewhat fast
|
||||
- **Cons**:
|
||||
- Subsceptible to Plateaus (use taboo-search or simulated annealing)
|
||||
- **constraint weighting**: Concentrate the search over solving constraints that have a higher value (each one starting with a value of 1). Choose the\
|
||||
pair variable/value that minimizes this value and increment each unsolved constraint by one
|
||||
- **Pros**:
|
||||
- Adds Topology to plateaus
|
||||
- **Cons**:
|
||||
- Difficult to implement
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user