陳鍾誠

Version 1.0

實作:謂詞邏輯的推論引擎

接著、讓我們將上述的「布林推論引擎」擴充,成為一個可以包含「布林函數」的「謂詞邏輯」推論引擎。

程式碼:謂詞推理引擎

檔案:pkb.js (Predicate Knowledge Base 的簡寫)

var util = require('util')

var isEmpty = function (map) {
  return Object.keys(map).length === 0
}
var format = function () {
  return util.format.apply(null, arguments)
}

var Predicate = function () {}

Predicate.prototype.load = function (str) {
  var m = str.match(/^([^(]*)\(([^)]*)\)$/)
  this.name = (m[1] == null) ? '' : m[1].trim()
  this.params = (m[2] == null) ? '' : m[2].trim().split(/[,]/)
  return this
}

Predicate.prototype.toString = function () {
  return format('%s%j', this.name, this.params).replace(/'/gi, '').replace(/\[/gi, '(').replace(/\]/gi, ')')
}

Predicate.prototype.unify = function (fact) {
  var map = {}
  if (this.name !== fact.name || this.params.length !== fact.params.length) return null
  for (var i = 0; i < this.params.length; i++) {
    var p = this.params[i]
    var fp = fact.params[i]
    if (map[p] == null) { // 參數 p 沒有 bind,所以就 bind 上去
      if (p.match(/^[a-z]+$/) != null && fp.match(/^[A-Z].*$/) != null) map[p] = fp
    } else { // 參數 p 已 bind,檢查是否有衝突。
      if (map[p] === fp) map[p] = fp // 沒衝突,加入。
      else return null // 有衝突,傳回 null
    }
  }
  return map
}

// 若每個變數都填入了,那麼就是滿足了 (satisfy)
Predicate.prototype.satisfy = function (map) {
  for (var i in this.params) {
    var p = this.params[i]
    if (map[p] == null) return false
  }
  return true
}

Predicate.prototype.mapping = function (map) {
  var term = new Predicate()
  term.name = this.name
  term.params = []
  for (var i in this.params) {
    var p = this.params[i]
    term.params.push(map[p])
  }
  return term
}

var Rule = function () {}

Rule.prototype.clone = function () {
  var r = new Rule()
  r.head = this.head
  r.terms = this.terms
  r.map = this.map
  return r
}

Rule.prototype.load = function (line) {
  var m = line.match(/^([^<=]*)(<=([^{$]*))$/)
  var head = (m[1] == null) ? '' : m[1].trim()
  var terms = (m[3] == null) ? '' : m[3].trim().split(/[&]+/)
  this.head = new Predicate().load(head)
  this.terms = []
  for (var i = 0; i < terms.length; i++) {
    this.terms.push(new Predicate().load(terms[i]))
  }
  this.map = {}
  return this
}

Rule.prototype.toString = function () {
  return format('%s<=%s%j', this.head, this.terms.join('&'), this.map)
}

Rule.prototype.resolve = function (fact) {
  var rmap = this.map
  for (var i in this.terms) {
    var term = this.terms[i]
    var tmap = term.unify(fact)
    if (tmap == null) continue
    var isConflict = false
    for (var mi in tmap) {
      if (rmap[mi] != null && rmap[mi] !== tmap[mi]) isConflict = true
    }
    if (!isConflict) {
      return tmap
    }
  }
  return null
}

var KB = function () {
  this.rules = []
  this.facts = []
  this.ruleMap = {}
  this.factMap = {}
  this.resolveMap = {}
}

KB.prototype.load = function (code) {
  code = code.replace(/\s/gi, '')
  var lines = code.split(/[.]+ ?/)
  console.log('%j', lines)
  for (var i in lines) {
    var line = lines[i].trim()
    if (line.length === 0) continue
    if (line.indexOf('<=') >= 0) {
      this.addRule(new Rule().load(line))
    } else {
      this.addFact(new Predicate().load(line))
    }
  }
  this.dump()
}

KB.prototype.dump = function () {
  console.log('=====facts========')
  console.log(this.facts.join('\n'))
  console.log('========rules=======')
  console.log(this.rules.join('\n'))
// console.log('========resolveMap=======\n')
// console.log(Object.keys(this.resolveMap).join('|'))
}

KB.prototype.addFact = function (fact) {
  if (this.factMap[fact.toString()] == null) {
    console.log('addFact:%s', fact)
    this.facts.push(fact)
    this.factMap[fact.toString()] = fact
    return true
  } else {
    return false
  }
}

KB.prototype.addRule = function (rule) {
  if (this.ruleMap[rule.toString()] == null) {
    console.log('addRule:%s', rule.toString())
    this.rules.push(rule)
    this.ruleMap[rule.toString()] = rule
    return true
  } else {
    return false
  }
}

KB.prototype.genNew = function (rule, fact) {
  var fmap = rule.resolve(fact)
  if (fmap == null) return null
  var rmap = Object.assign({}, rule.map, fmap) // ml.merge(rule.map, fmap)
  if (rule.head.satisfy(rmap)) {
    var newFact = rule.head.mapping(rmap)
    if (this.addFact(newFact)) return newFact
  } else {
    if (!isEmpty(fmap)) {
      var newRule = rule.clone()
      newRule.map = rmap
      if (this.addRule(newRule)) return newRule
    }
  }
  return null
}

KB.prototype.forwardChaining = function () {
  do {
    console.log('======forwardChaining============')
    var anyNew = false
    for (var fi = 0; fi < this.facts.length; fi++) {
      var fact = this.facts[fi]
      for (var ri in this.rules) {
        var rule = this.rules[ri]
        if (this.resolveMap[fi + ',' + ri] == null) {
          var newObj = this.genNew(rule, fact)
          if (newObj != null) {
            console.log('  %s;%s\n', rule, fact)
            anyNew = true
          }
        } else {
          this.resolveMap[fi + ',' + ri] = true
        }
      }
    }
  } while (anyNew)
  this.dump()
}

KB.prototype.test = function () {
//  var fxy = new Predicate().load('father(x,y)')
//  var fjj = new Predicate().load('father(John,Johnson)')
//  var rp = new Rule().load('parent(x,y)<=father(x,y)')
  var ra = new Rule().load('ancestor(x,z)<=ancestor(x,y)&parent(y,z)')
  var pgj = new Predicate().load('ancestor(George,John)')
  var pjj = new Predicate().load('parent(John,Johnson)')
  ra.map = ra.resolve(pgj)
  ra.map = Object.assign({}, ra.map, ra.resolve(pjj)) // ml.merge(ra.map, ra.resolve(pjj))
  console.log('ra=%s', ra)
  console.log(' satisfy=%d', ra.head.satisfy(ra.map))
}

module.exports = KB

前向推論主程式:pkbReason.js

檔案:pkbReason.js

var fs = require('fs'); // 引用檔案物件
var kb = require('./pkb');

var kb1 = new kb();
var code = fs.readFileSync(process.argv[2], "utf8").replace(/\n/gi, ""); // 讀取檔案
kb1.load(code);
kb1.forwardChaining();

推論規則範例

規則檔:family.pkb

parent(x,y)   <= father(x,y).
parent(x,y)   <= mother(x,y).
ancestor(x,y) <= parent(x,y).
ancestor(x,z) <= ancestor(x,y) & parent(y,z).

father(John, Johnson).
mother(Mary, Johnson).
father(George, John).
father(John, Jake).

執行結果

C:\Dropbox\Public\web\ai\code\PKB>node pkbReason family.pkb
["parent(x,y)<=father(x,y)","parent(x,y)<=mother(x,y)","ancestor(x,y)<=parent(x,
y)","ancestor(x,z)<=ancestor(x,y)&parent(y,z)","father(John,Johnson)","mother(Ma
ry,Johnson)","father(George,John)","father(John,Jake)",""]
addRule:parent(x,y)<=father(x,y){}
addRule:parent(x,y)<=mother(x,y){}
addRule:ancestor(x,y)<=parent(x,y){}
addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){}
addFact:father(John,Johnson)
addFact:mother(Mary,Johnson)
addFact:father(George,John)
addFact:father(John,Jake)
=====facts========
father(John,Johnson)
mother(Mary,Johnson)
father(George,John)
father(John,Jake)
========rules=======
parent(x,y)<=father(x,y){}
parent(x,y)<=mother(x,y){}
ancestor(x,y)<=parent(x,y){}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){}
======forwardChaining============
addFact:parent(John,Johnson)
  parent(x,y)<=father(x,y){};father(John,Johnson)

addFact:parent(Mary,Johnson)
  parent(x,y)<=mother(x,y){};mother(Mary,Johnson)

addFact:parent(George,John)
  parent(x,y)<=father(x,y){};father(George,John)

addFact:parent(John,Jake)
  parent(x,y)<=father(x,y){};father(John,Jake)

addFact:ancestor(John,Johnson)
  ancestor(x,y)<=parent(x,y){};parent(John,Johnson)

addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){"y":"John","z":"Johnson"}
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){};parent(John,Johnson)

addFact:ancestor(Mary,Johnson)
  ancestor(x,y)<=parent(x,y){};parent(Mary,Johnson)

addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){"y":"Mary","z":"Johnson"}
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){};parent(Mary,Johnson)

addFact:ancestor(George,John)
  ancestor(x,y)<=parent(x,y){};parent(George,John)

addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){"y":"George","z":"John"}
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){};parent(George,John)

addFact:ancestor(John,Jake)
  ancestor(x,y)<=parent(x,y){};parent(John,Jake)

addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){"y":"John","z":"Jake"}
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){};parent(John,Jake)

addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"John","y":"Johnson"}
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){};ancestor(John,Johnson)

addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"Mary","y":"Johnson"}
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){};ancestor(Mary,Johnson)

addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"George","y":"John"}
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){};ancestor(George,John)

addFact:ancestor(George,Johnson)
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){"y":"John","z":"Johnson"};ancestor(Ge
orge,John)

addFact:ancestor(George,Jake)
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){"y":"John","z":"Jake"};ancestor(Georg
e,John)

addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"John","y":"Jake"}
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){};ancestor(John,Jake)

addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"George","y":"Johnson"}
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){};ancestor(George,Johnson)

addRule:ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"George","y":"Jake"}
  ancestor(x,z)<=ancestor(x,y)&parent(y,z){};ancestor(George,Jake)

======forwardChaining============
=====facts========
father(John,Johnson)
mother(Mary,Johnson)
father(George,John)
father(John,Jake)
parent(John,Johnson)
parent(Mary,Johnson)
parent(George,John)
parent(John,Jake)
ancestor(John,Johnson)
ancestor(Mary,Johnson)
ancestor(George,John)
ancestor(John,Jake)
ancestor(George,Johnson)
ancestor(George,Jake)
========rules=======
parent(x,y)<=father(x,y){}
parent(x,y)<=mother(x,y){}
ancestor(x,y)<=parent(x,y){}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){"y":"John","z":"Johnson"}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){"y":"Mary","z":"Johnson"}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){"y":"George","z":"John"}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){"y":"John","z":"Jake"}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"John","y":"Johnson"}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"Mary","y":"Johnson"}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"George","y":"John"}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"John","y":"Jake"}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"George","y":"Johnson"}
ancestor(x,z)<=ancestor(x,y)&parent(y,z){"x":"George","y":"Jake"}

結語

以上我們用 JavaScript 實作了一個簡易的謂詞邏輯推論引擎,採用洪氏邏輯的語法,以及前向推論 (forwardChaining) 的方式。