Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Problem Solved!

Problem Solved!

Using Logic Programming to Find Answers: putting AI approaches in the hands of developers, to turn problem solving over to the computer.

Delivered at RubyConf 2016.

Gavin McGimpsey

November 10, 2016
Tweet

Other Decks in Programming

Transcript

  1. Problem Solved! Using Logic Programming to find answers

  2. Gavin McGimpsey [email protected]

  3. Law School Admissions Test

  4. Bridge • Describe your cards through bidding • Deduce who

    has what
  5. Programming Early years: Logo, HyperCard Since then: C, C++, JavaScript,

    Ruby Interested in: Haskell, Elixir
  6. The ideal • Tell the computer how the world works

    • Get the answer to life, the universe, and everything class DeepThought def solve return 42 end end
  7. Creative Machines

  8. Artificial Intelligence!

  9. Logic Programming

  10. 1. Declarative 2. Relational 3. Inferential

  11. Being Declarative

  12. Atbash Cipher • Keep letters and numbers a ⇔ z

    b ⇔ y c ⇔ x d ⇔ w e ⇔ v f ⇔ u g ⇔ t h ⇔ s i ⇔ r j ⇔ q k ⇔ p l ⇔ o m ⇔ n RubyConf 2016! • Letters swap alphabet position • Downcase and chunk into “words” of 5 characters i f y b x l m u 2 0 1 6 ⇒
  13. Function Chaining Atbash Cipher • Keep letters and numbers •

    Letters swap alphabet position • Downcase and chunk into “words” of 5 characters class Atbash def self.key { 'a' => 'z', 'b' => 'y', ... } end def self.encode(str) str.chars .grep(/[[:alnum:]]/) .map { |ch| key[ch.downcase] || ch } .each_slice(5) .collect_concat(&:join) .join(' ') end end
  14. Hiding Process

  15. has_many

  16. Relational Thinking

  17. Relational Define relations between entities or types Query relationships, especially

    compound ones
  18. 18 + 24 = ?

  19. 18 + ? = 42

  20. Functional thinking Functions: known inputs yield unknown outputs sum(18, 24)

    ⇒ 42
  21. Relational thinking Relations: work with what you’ve got sum_r(18, 24,

    ?) ⇒ match ? with 42 sum_r(18, ?, 42) ⇒ match ? with 24
  22. 1. Declarative 2. Relational 3.

  23. SQL Build a database of relations (tables) and facts (rows)

    Execute queries declaratively SELECT * AS the_answer FROM thank JOIN goodness ON ... JOIN for ON ... JOIN query ON ... JOIN planners ON ...
  24. 3. Inference

  25. Problem Solving

  26. n Queens On an n by n chessboard, place n

    queens, such that they cannot attack each other. ♛
  27. n Queens Some variation on: • Pick an empty square

    and place a queen • Block affected squares • Repeat Options: • Breadth-first • Depth-first ◦ With backtracking?
  28. Constraint Processing

  29. Constraint Propagation • Repeatedly apply rules until reaching a fixpoint

    ◦ Eliminate each square that’s already attackable ◦ If a row/column has only one empty slot, place a queen • Branch to continue moving forward ◦ n Queens: pick an empty square and place a queen
  30. n Queens Propagate Constraints Eliminate attackable squares (No rows/columns are

    narrowed enough to force placement) ♛
  31. n Queens Branch Middle column (for example) has only two

    open squares, so pick one ♛ ♛
  32. n Queens Propagate Constraints • Eliminate attackable squares • Place

    queens in rows/columns with only one open square ♛ ♛
  33. n Queens Solved! ♛ ♛ ♛ ♛ ♛

  34. Sudoku

  35. Sudoku • 9 x 9 Grid or: a 3 x

    3 grid of 3 x 3 boxes • Fill in numbers 1 through 9 in each ◦ Row ◦ Column ◦ Box • No repeats within any of those three dimensions
  36. Brute Force • A single row: 9! • One remaining

    column: 8!
  37. O(n!!) Thanks to Aja Hammerly’s 2015 RubyConf Keynote, “Stupid Things

    for Many Computers”
  38. Constraint Procedure If: • a slot has one possibility, or

    • a unit has only one eligible slot for a needed value Then: • Set the slot • Remove that possibility from other slots in affected units A B C D E F G H I 1 2 3 4 5 6 7 8 9 9
  39. Sudoku – Constraint Propagation def cross(A, B): return [a+b for

    a in A for b in B] digits = '123456789' rows = 'ABCDEFGHI' cols = digits squares = cross(rows, cols) unitlist = ([cross(rows, c) for c in cols] + [cross(r, cols) for r in rows] + [cross(rs, cs) for rs in ('ABC','DEF','GHI') for cs in ('123','456','789')]) units = dict((s, [u for u in unitlist if s in u]) for s in squares) peers = dict((s, set(sum(units[s],[]))-set([s])) for s in squares) def parse_grid(grid): values = dict((s, digits) for s in squares) for s,d in grid_values(grid).items(): if d in digits and not assign(values, s, d): return False ## (Fail if we can't assign d to square s.) def grid_values(grid): chars = [c for c in grid if c in digits or c in '0.'] assert len(chars) == 81 return dict(zip(squares, chars)) def display(values): width = 1+max(len(values[s]) for s in squares) line = '+'.join(['-'*(width*3)]*3) for r in rows: print ''.join(values[r+c].center(width)+('|' if c in '36' else '') for c in cols) if r in 'CF': print line print def assign(values, s, d): other_values = values[s].replace(d, '') if all(eliminate(values, s, d2) for d2 in other_values): return values else: return False def eliminate(values, s, d): if d not in values[s]: return values ## Already eliminated values[s] = values[s].replace(d,'') if len(values[s]) == 0: return False elif len(values[s]) == 1: d2 = values[s] if not all(eliminate(values, s2, d2) for s2 in peers[s]): return False for u in units[s]: dplaces = [s for s in u if d in values[s]] if len(dplaces) == 0: return False ## Contradiction: no place for this value elif len(dplaces) == 1: # d can only be in one place in unit; assign it there if not assign(values, dplaces[0], d): return False return values def some(seq): for e in seq: if e: return e return False def search(values): if values is False: return False ## Failed earlier if all(len(values[s]) == 1 for s in squares): return values ## Solved! ## Chose the unfilled square s with the fewest possibilities n,s = min((len(values[s]), s) for s in squares if len(values[s]) > 1) return some(search(assign(values.copy(), s, d)) for d in values[s]) def solve(grid): return search(parse_grid(grid)) http://norvig.com/sudoku.html
  40. Russell

  41. Sudoku – Logic Solver require_relative 'logic' class Sudoku col_thirds =

    [%w{A B C}, %w{D E F}, %w{G H I}] row_thirds = [%w{1 2 3}, %w{4 5 6}, %w{7 8 9}] @@boxes = row_thirds.product(col_thirds).map {|set| set[1].product(set[0])} .map { |box| box.map { |combo| combo.join.to_sym } } col_signs = col_thirds.flatten row_signs = row_thirds.flatten @@cells = row_signs.product(col_signs).map { |combo| combo.reverse.join.to_sym } @@cols = @@cells.each_slice(9).to_a @@rows = @@cols.transpose @@units = @@cols + @@rows + @@boxes def parse_grid(str) @@cells.zip(str.chars) .keep_if { |cell, char| /[[:digit:]]/ === char } .map { |cell, char| [cell, char.to_i] } end def to_s @results.map do |result| @@cells.map { |cell| result[cell].to_s } .each_slice(9) .map(&:join) end.join("\n") end def initialize(str) starting_grid = parse_grid(str) solver = Logic::Solver.new do @@cells.each { |cell| assert domain_is( cell, (1..9) ) } @@units.each do |unit| assert has_unique_contents(unit) (1..9).each { |num| assert must_include(unit, num) } end starting_grid.each { |cell, value| assert equal(cell, value) } end solver.solve @results = solver.results end end
  42. Sudoku – Constraint Propagation Core def assign(values, s, d): other_values

    = values[s].replace(d, '') if all(eliminate(values, s, d2) for d2 in other_values): return values else: return False def eliminate(values, s, d): if d not in values[s]: return values ## Already eliminated values[s] = values[s].replace(d,'') if len(values[s]) == 0: return False elif len(values[s]) == 1: d2 = values[s] if not all(eliminate(values, s2, d2) for s2 in peers[s]): return False for u in units[s]: dplaces = [s for s in u if d in values[s]] if len(dplaces) == 0: return False ## Contradiction: no place for this value elif len(dplaces) == 1: # d can only be in one place in unit; assign it there if not assign(values, dplaces[0], d): return False return values def some(seq): for e in seq: if e: return e return False def search(values): if values is False: return False ## Failed earlier if all(len(values[s]) == 1 for s in squares): return values ## Solved! ## Chose the unfilled square s with the fewest possibilities n,s = min((len(values[s]), s) for s in squares if len(values[s]) > 1) return some(search(assign(values.copy(), s, d)) for d in values[s]) def solve(grid): return search(parse_grid(grid)) http://norvig.com/sudoku.html
  43. Sudoku – Logic Solver Core def initialize(str) starting_grid = parse_grid(str)

    solver = Logic::Solver.new do @@cells.each { |cell| assert domain_is( cell, (1..9) ) } @@units.each do |unit| assert has_unique_contents(unit) (1..9).each { |num| assert must_include(unit, num) } end starting_grid.each { |cell, value| assert equal(cell, value) } end solver.solve @results = solver.results end
  44. Solving Sudoku

  45. Using a Solver Declare: • Squares hold values 1 through

    9! • Units include all 9 digits! Then, let the solver figure it out! A B C D E F G H I 1 2 3 4 5 6 7 8 9
  46. Declarative Sudoku Squares must hold values 1 through 9! solver

    = Logic::Solver.new do squares.each { |sq| assert domain_is( sq, (1..9) ) } end
  47. Declarative Sudoku Units include all 9 digits! solver.more do [rows,

    columns, boxes].each do |unit| assert has_unique_contents(unit) (1..9).each { |num| assert must_include(unit, num) } end end
  48. Declarative Sudoku Off to the races! known_squares = [[:A1, 5],

    [:B1, 1], …] known_squares.each do |sq| assert equal(*sq) end A B C D E F G H I 1 2 3 4 5 6 7 8 9
  49. Sudoku – Core 1 starting_grid = parse_grid(str) 2 3 solver

    = Logic::Solver.new do 4 @@cells.each { |cell| assert domain_is( cell, (1..9) ) } 5 6 @@units.each do |unit| 7 assert has_unique_contents(unit) 8 (1..9).each { |num| assert must_include(unit, num) } 9 end 10 11 starting_grid.each { |cell, value| assert equal(cell, value) } 12 end 13 14 solver.solve 15 @results = solver.results
  50. Creating Relations

  51. Functional Completeness aka “Expressive Adequacy” Conjunction (and / all) Disjunction

    (or / any) Negation (not)
  52. has_unique_contents(collection) No two values are the same collection.combination(2) do |pair|

    assert not_equal(*pair) end
  53. exactly_one_member(collection, value) assert any( *collection.map do |item| assert equal(item, value)

    (collection - item).each do |other| assert not_equal(other, value) end end )
  54. Next iteration? def collection.exactly_one_member(value) collection.each do |item| assert item.equal_to(value) (collection

    - item).each do |other| assert other.not_equal_to(value) end end end
  55. Under the Hood

  56. Unification Pattern matching, with knowledge storage: • Build a substitution

    list of how unknowns map to values or each other • Query it to see what unknowns resolve to concat_r([m, ? 1 , t], [? 2 ], [m, a, t, z]) ⇒ match ? 1 with a, and ? 2 with z
  57. Substitution List Variable p matches with 5 Variable q matches

    with variable r Variable r matches with 8 Query Variable p matches with variable q ? ⇒ { p: 5 } ⇒ { p: 5, q: :r} ⇒ { p: 5, q: :r, r: 8 } ⇒ (5 == 8) Assertions
  58. Unification - the algorithm • “Walk” the values we want

    to unify through the substitution list • Outcomes: ◦ Initial values already match to the same known result ◦ Initial values match to known contradictory results ◦ Substitution chain ends with a still-unknown value
  59. Implementation

  60. Constraint Propagation in Ruby 1 def propagate_constraints 2 loop do

    3 reset_changed_flags 4 5 remaining_possibilities = @possibilities.reject(&:solved) 6 7 remaining_possibilities.each do |possibility| 8 possibility.apply(rules) 9 end 10 possibilities.keep_if(&:valid) 11 12 break unless something_changed 13 end 14 end
  61. Unification in Ruby 1 def unify?(left, right) 2 left, right

    = walk(left), walk(right) # walk returns the object if there 3 extension = SubstitutionList.new # isn’t a substitution for it 4 5 if [left, right].all? { |e| e.kind_of?(Symbol) } && (left == right) 6 return extension # no changes needed, so it stays empty 7 end 8 9 if left.kind_of? Symbol # if either side is an unknown 10 extension[left] = right # attach it to a known value or 11 elsif right.kind_of? Symbol # chain it to another unknown 12 extension[right] = left 13 else 14 extension = left.class.unify(left, right, self) # defaults to == 15 end 16 extension 17 end
  62. Unification in Ruby 1 def unify?(left, right) 2 left, right

    = walk(left), walk(right) # walk returns the object if there 3 extension = SubstitutionList.new # isn’t a substitution for it
  63. Unification in Ruby 3 extension = SubstitutionList.new 4 5 if

    [left, right].all? { |e| e.kind_of?(Symbol) } && (left == right) 6 return extension 7 end
  64. Unification in Ruby 7 end 8 9 if left.kind_of? Symbol

    10 extension[left] = right 11 elsif right.kind_of? Symbol 12 extension[right] = left 13 else
  65. Unification in Ruby 13 else 14 extension = left.class.unify(left, right,

    self) 15 end 16 extension 17 end
  66. Unification 1 def unify?(left, right) 2 left, right = walk(left),

    walk(right) 3 extension = SubstitutionList.new 4 5 if [left, right].all? { |e| e.kind_of?(Symbol) } && (left == right) 6 return extension 7 end 8 9 if left.kind_of? Symbol 10 extension[left] = right 11 elsif right.kind_of? Symbol 12 extension[right] = left 13 else 14 extension = left.class.unify(left, right, self) 15 end 16 extension 17 end
  67. Disjunction 1 would_unify = unifications.unify?(left, right) 2 if !would_unify 3

    # All set! 4 elsif would_unify.empty? 5 invalidate 6 else 7 disequalities.push(would_unify) 8 end
  68. API

  69. Equal and Not Equal 1 def equal(left, right) 2 [

    3 IdentityStore.new do 4 unify(left, right) 5 end 6 ] 7 end 1 def not_equal(left, right) 2 [ 3 IdentityStore.new do 4 disjoin(left, right) 5 end 6 ] 7 end
  70. AND and OR 1 def all(*stores_lists) 2 prod = Util.product(stores_lists)

    3 prod.map { |combo| combo.unshift(IdentityStore.new).inject(&:combine) } 4 .keep_if(&:valid) 5 end 1 def any(*stores_lists) 2 stores_lists.flatten.keep_if(&:valid) 3 end
  71. Using Logic Programming

  72. How • Embedded in Ruby ◦ Prolog style – rulog

    or ruby-prolog package/gem ◦ miniKanren style – kanren gem • External Solver ◦ ruby-minisat gem for MiniSAT boolean solver • Logic Languages ◦ Prolog ◦ Mercury ◦ Picat
  73. 1. Declarative 2. Relational 3. Inferential

  74. When to avoid • Can’t describe relations / how the

    world works • Too little information to go on Still waiting...
  75. When to use • Inputs: constrained and variable • Generating

    possibilities within a system • Enforcing rules
  76. Expert Systems • Airline ticketing • Natural Language Processing

  77. Music Exercises Potential Constraints • Note length • Note pitch

    • Key • Measure length
  78. Music Exercises music(? key , ? time_signature , [:1_8, :1_16],

    :treble)
  79. Next Steps • Get involved in building tooling for Ruby

    logic programming • Try writing an expert system / solver using existing tools
  80. Other Resources • Clojure’s core.logic • miniKanren papers/uncourse by William

    Byrd • The Art of Prolog by Leon Sterling and Ehud Shapiro • Constraint Processing by Rita Dechter • sentient-lang.org
  81. Thanks! Gavin McGimpsey [email protected] @gavinmcgimpsey ⇒ gavinmcg.com Code: ⇒ gitlab.com/gavinmcg/russell

  82. Sources Drawings: Doan Trang – https://www.fiverr.com/doantrang LSAT Example: Official LSAT

    Prep Test – www.lsac.org/docs/default-source/jd-docs/sampleptjune.pdf Sudoku puzzle: Vegard Hanssen Puzzle #5194746 “Super Hard” – www.menneske.no/sudoku Weather modeling image: https://upload.wikimedia.org/wikipedia/commons/c/c0/NOAA_W avewatch_III_Sample_Forecast.gif Musical staff image: https://upload.wikimedia.org/wikipedia/commons/4/4c/Grand_s taff.svg Peter Norvig’s Sudoku Solver: http://norvig.com/sudoku.html Watson image: https://i.ytimg.com/vi/P18EdAKuC1U/maxresdefault.jpg Music exercise generation: Oskar Wickström – https://wickstrom.tech/generative-music/2016/08/07/generating -sight-reading-exercises-using-constraint-logic-programming-in-c lojure-part-1.html