codex/boltzmann-pair-learning.py

boltzmann-pair-learning.py
"""Exact two-visible-unit Boltzmann learning; Python 3.14, NumPy 2.3, Matplotlib 3.10."""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt

states = np.array([[1,1], [1,-1], [-1,1], [-1,-1]])
pair = states[:,0]*states[:,1]
q = np.array([3,1,1,3])/8

def model(j):
    weights = np.exp(j*pair)
    return weights / weights.sum()

def likelihood(j):
    return q @ np.log(model(j))

for j in (-.7, 0, .25, .5493061443340549, 1.1):
    assert np.isclose(np.exp(j*pair).sum(),4*np.cosh(j))
    assert np.isclose(model(j) @ pair, np.tanh(j))
    step = 1e-5
    derivative = (likelihood(j+step)-likelihood(j-step))/(2*step)
    assert abs(derivative - (.5-np.tanh(j))) < 1e-9
assert np.allclose(model(np.log(3)/2),q)
assert likelihood(.25) > likelihood(0)

plt.rcParams.update({'font.size': 11, 'axes.spines.top':False, 'axes.spines.right':False})
fig,(ax,bx)=plt.subplots(1,2,figsize=(10,4.8),gridspec_kw={'width_ratios':[1.1,1]})
fig.patch.set_facecolor('white')
x=np.arange(4)
for offset, values, label, color in [(-.26,q,'Data','#294e76'),(0,model(0),'Initial J = 0','#b0bdc9'),(.26,model(.25),'After step J = 1/4','#dc6b20')]:
    ax.bar(x+offset,values,width=.24,label=label,color=color)
ax.set(xticks=x,xticklabels=['(+,+)','(+,−)','(−,+)','(−,−)'],ylabel='Probability',ylim=(0,.46),xlabel='Visible state',title='Enumerate all four states')
ax.legend(frameon=False,fontsize=9,loc='upper center',ncol=1)
ax.grid(axis='y',alpha=.16)
j=np.linspace(-.15,1.25,250)
bx.plot(j,np.tanh(j),label=r'Free model: $\langle x_1x_2\rangle_-=\tanh J$',color='#294e76',lw=2)
bx.axhline(.5,color='#6d7f36',ls='--',label=r'Clamped data: $\langle x_1x_2\rangle_+=1/2$')
bx.annotate('',xy=(0,.5),xytext=(0,0),arrowprops={'arrowstyle':'<->','color':'#dc6b20','lw':2})
bx.text(.025,.22,'Initial gradient\n= 1/2',color='#a94912',fontsize=10)
opt=np.log(3)/2
bx.scatter([opt],[.5],s=55,color='#6d7f36',zorder=4)
bx.axvline(opt,color='#6d7f36',ls=':',alpha=.7)
bx.text(opt+.045,.31,r'$J_* = \frac{\log 3}{2}$'+'\n≈ 0.5493',fontsize=12)
bx.scatter([.25],[np.tanh(.25)],color='#dc6b20',zorder=4)
bx.set(xlabel='Coupling J',ylabel='Mean pair product',ylim=(-.22,.94),title='Learning closes the correlation gap')
bx.legend(loc='upper left',frameon=False,fontsize=9)
bx.grid(alpha=.16)
fig.suptitle('Boltzmann learning: observed association minus generated association',fontsize=14)
fig.text(.5,.025,r'$E=-Jx_1x_2$, $\beta=1$, $Z=4\cosh J$.  One ascent step: $J=0+(1/2)(1/2)=1/4$.',ha='center',fontsize=11)
fig.tight_layout(rect=(0,.065,1,.93),w_pad=2)
fig.savefig(Path(__file__).stem+'.png',dpi=120,facecolor='white')