OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / opencog-kb / language-learning / mvp / mi-factored.scm

277 lines · scm ref:
;
; mi-factored.scm -- Factored MI computation (N-independent storage)
;
; Stores PMI_raw = log₂[count(x,y)] - log₂[count(x,*)] - log₂[count(*,y)]
; Computes MI = PMI_raw + log₂[N] on demand
;
; Benefits:
;   - N changes don't invalidate cached values
;   - Only recompute PMI_raw when specific pair/marginal counts change
;   - Single global N adjustment at query time
;

(define-module (opencog learn mi-factored)
  #:use-module (opencog)
  #:use-module (opencog exec)
  #:use-module (opencog persist)
  #:use-module (srfi srfi-1)
  #:use-module (ice-9 optargs)
  #:export (
    make-factored-mi-api
    compute-pmi-raw
    get-mi-adjusted
    batch-compute-pmi-raw
    get-n-current
    set-n-current!
  ))

; ═══════════════════════════════════════════════════════════════════
; STORAGE KEYS
; ═══════════════════════════════════════════════════════════════════

;; Key for PMI_raw (N-independent part)
(define *pmi-raw-key* (Predicate "*-PMI-raw-*"))

;; Key for marginal version (for staleness detection)
(define *marginal-version-key* (Predicate "*-MarginalVersion-*"))

;; Global N tracking
(define *n-current* 0)
(define *n-at-batch* 0)

; ═══════════════════════════════════════════════════════════════════
; CORE MATH
; ═══════════════════════════════════════════════════════════════════

(define (safe-log2 x)
  "Log base 2, returns -inf for 0."
  (if (> x 0)
      (/ (log x) (log 2))
      -inf.0))

(define (compute-pmi-raw pair-count left-marginal right-marginal)
  "Compute PMI_raw = log₂[count(x,y)] - log₂[count(x,*)] - log₂[count(*,y)]
   This is the N-independent part of MI."
  (- (safe-log2 pair-count)
     (safe-log2 left-marginal)
     (safe-log2 right-marginal)))

(define (pmi-raw->mi pmi-raw n)
  "Convert PMI_raw to MI by adding log₂[N].
   MI = PMI_raw + log₂[N]"
  (+ pmi-raw (safe-log2 n)))

(define (mi->pmi-raw mi n)
  "Convert MI back to PMI_raw by subtracting log₂[N].
   PMI_raw = MI - log₂[N]"
  (- mi (safe-log2 n)))

; ═══════════════════════════════════════════════════════════════════
; GLOBAL N MANAGEMENT
; ═══════════════════════════════════════════════════════════════════

(define (get-n-current) *n-current*)

(define (set-n-current! n)
  "Update current N (total pair count)."
  (set! *n-current* n))

(define (get-n-at-batch) *n-at-batch*)

(define (set-n-at-batch! n)
  "Record N at last batch computation."
  (set! *n-at-batch* n))

(define (n-drift)
  "Return N drift as ratio: (N_current - N_batch) / N_batch"
  (if (> *n-at-batch* 0)
      (/ (- *n-current* *n-at-batch*) *n-at-batch*)
      0.0))

(define (n-adjustment)
  "Return the log₂ adjustment factor for N drift.
   This is the constant to add to all PMI_raw values."
  (if (> *n-at-batch* 0)
      (safe-log2 (/ *n-current* *n-at-batch*))
      0.0))

; ═══════════════════════════════════════════════════════════════════
; ATOM-LEVEL STORAGE
; ═══════════════════════════════════════════════════════════════════

(define (store-pmi-raw! pair pmi-raw)
  "Store PMI_raw value on a pair atom."
  (cog-set-value! pair *pmi-raw-key* (FloatValue pmi-raw)))

(define (fetch-pmi-raw pair)
  "Get stored PMI_raw value from pair, or #f if not computed."
  (let ((val (cog-value pair *pmi-raw-key*)))
    (if val
        (cog-value-ref val 0)
        #f)))

(define (get-mi-adjusted pair)
  "Get MI value adjusted for current N.
   MI = PMI_raw + log₂[N_current]"
  (let ((pmi-raw (fetch-pmi-raw pair)))
    (if pmi-raw
        (pmi-raw->mi pmi-raw *n-current*)
        #f)))

; ═══════════════════════════════════════════════════════════════════
; MARGINAL VERSION TRACKING
; ═══════════════════════════════════════════════════════════════════

(define *global-marginal-version* 0)

(define (bump-marginal-version! word)
  "Increment marginal version for a word (call when its count changes)."
  (set! *global-marginal-version* (+ 1 *global-marginal-version*))
  (cog-set-value! word *marginal-version-key*
                  (FloatValue *global-marginal-version*)))

(define (get-marginal-version word)
  "Get marginal version for a word."
  (let ((val (cog-value word *marginal-version-key*)))
    (if val (cog-value-ref val 0) 0)))

; ═══════════════════════════════════════════════════════════════════
; FACTORED MI API (wraps matrix API)
; ═══════════════════════════════════════════════════════════════════

(define* (make-factored-mi-api LLOBJ #:key (storage #f))
  "Create a factored MI API wrapper around a matrix LLOBJ.

   This API stores PMI_raw instead of MI, and computes MI = PMI_raw + log₂[N]
   on demand. N changes don't require recomputation.

   Methods:
     'pair-pmi-raw PAIR   - Get stored PMI_raw
     'pair-mi PAIR        - Get MI adjusted for current N
     'set-pmi-raw! PAIR V - Store PMI_raw value
     'compute-pmi-raw! PAIR - Compute and store PMI_raw from counts
     'n-current           - Get current N
     'n-drift             - Get N drift ratio
     'n-adjustment        - Get log₂ adjustment factor
  "

  (let ((llobj LLOBJ)
        (store storage))

    ;; Get count from underlying matrix
    (define (get-count pair)
      (llobj 'pair-count pair))

    ;; Get left marginal count(x,*)
    (define (get-left-marginal pair)
      (llobj 'left-wild-count pair))

    ;; Get right marginal count(*,y)
    (define (get-right-marginal pair)
      (llobj 'right-wild-count pair))

    ;; Compute and store PMI_raw for one pair
    (define (compute-and-store-pmi-raw! pair)
      (let* ((cnt (get-count pair))
             (left-marg (get-left-marginal pair))
             (right-marg (get-right-marginal pair))
             (pmi-raw (compute-pmi-raw cnt left-marg right-marg)))
        (store-pmi-raw! pair pmi-raw)
        (when store (store-atom pair))
        pmi-raw))

    ;; Dispatch
    (lambda (message . args)
      (case message
        ;; PMI_raw access
        ((pair-pmi-raw)
         (fetch-pmi-raw (car args)))

        ((pair-mi)
         (get-mi-adjusted (car args)))

        ((set-pmi-raw!)
         (store-pmi-raw! (car args) (cadr args)))

        ((compute-pmi-raw!)
         (compute-and-store-pmi-raw! (car args)))

        ;; N management
        ((n-current) *n-current*)
        ((n-at-batch) *n-at-batch*)
        ((n-drift) (n-drift))
        ((n-adjustment) (n-adjustment))
        ((set-n-current!) (set-n-current! (car args)))
        ((set-n-at-batch!) (set-n-at-batch! (car args)))

        ;; Pass through to underlying LLOBJ
        (else (apply llobj message args))))))

; ═══════════════════════════════════════════════════════════════════
; BATCH OPERATIONS
; ═══════════════════════════════════════════════════════════════════

(define* (batch-compute-pmi-raw LLOBJ #:key (storage #f))
  "Compute PMI_raw for all pairs in matrix LLOBJ.

   This replaces batch-all-pair-mi for the factored approach.
   Stores PMI_raw (N-independent) instead of MI.
  "
  (define start-time (current-time))
  (define pair-count 0)

  (display "Computing PMI_raw for all pairs...\n")

  ;; Get total N
  (define total-count (LLOBJ 'wild-wild-count))
  (set-n-current! total-count)
  (set-n-at-batch! total-count)
  (format #t "  Total N: ~a\n" total-count)

  ;; Create factored API
  (define fmi (make-factored-mi-api LLOBJ #:storage storage))

  ;; Iterate all pairs and compute PMI_raw
  (for-each
    (lambda (pair)
      (fmi 'compute-pmi-raw! pair)
      (set! pair-count (+ 1 pair-count))
      (when (zero? (modulo pair-count 100000))
        (format #t "  Processed ~a pairs...\n" pair-count)))
    (LLOBJ 'get-all-elts))

  (let ((elapsed (- (current-time) start-time)))
    (format #t "  Done: ~a pairs in ~a seconds\n" pair-count elapsed))

  fmi)

; ═══════════════════════════════════════════════════════════════════
; CONVERSION UTILITIES
; ═══════════════════════════════════════════════════════════════════

(define (convert-mi-to-pmi-raw! LLOBJ n-at-computation)
  "Convert existing MI values to PMI_raw.
   Use when you have pre-computed MI and want to switch to factored storage.

   PMI_raw = MI - log₂[N]
  "
  (define log2-n (safe-log2 n-at-computation))
  (define count 0)

  (format #t "Converting MI to PMI_raw (N=~a, log₂N=~,3f)...\n"
          n-at-computation log2-n)

  (for-each
    (lambda (pair)
      (let ((mi (LLOBJ 'pair-fmi pair)))
        (when mi
          (let ((pmi-raw (- mi log2-n)))
            (store-pmi-raw! pair pmi-raw)
            (set! count (+ 1 count))))))
    (LLOBJ 'get-all-elts))

  (set-n-at-batch! n-at-computation)
  (set-n-current! n-at-computation)

  (format #t "  Converted ~a pairs\n" count))