;
; 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))