#!/usr/bin/env python
# encoding: utf-8
"""
maxent.py
Created by Marc-André on 2012-03-06.
Copyright (c) 2012 IGBMC. All rights reserved.
"""
from __future__ import print_function
import sys
import os
import math
import numpy as np
import scipy
import scipy.optimize
import unittest
from ..util.counter import counting, timeit
from ..Display import testplot
plt = testplot.plot()
__version__ = 0.3
__date__ = "march 2013"
########################################################################
# UTILITIES for testing
[docs]def br():
plt.show()
os._exit(0)
[docs]def initial_scene_delta(size = 100,sc=1.0):
"""draw test scene"""
# Ideal = 0.1*sc*np.ones((size,))
Ideal = np.zeros((size,))
Ideal[25]=sc
return Ideal
#
[docs]def initial_scene_2delta(size = 100,sc=1.0):
"""draw test scene"""
# Ideal = 0.1*sc*np.ones((size,))
Ideal = np.zeros((size,))
Ideal[25]=sc
Ideal[40]=sc
return Ideal
[docs]def initial_scene(size = 100,sc=1.0):
"""draw test scene"""
# Ideal = 0.1*sc*np.ones((size,))
Ideal = np.zeros((size,))
Ideal[15]=5*sc
for i in range(10):
Ideal[i+30]=sc
for i in range (20):
Ideal[i+65]=i*sc/20.0
return Ideal
figdef = 0
[docs]def plot(buf, text=None, fig=0, logy=False):
"plot with a text and return the figure number"
global figdef
if fig == 0:
figdef += 1
lfig = figdef
else:
lfig = fig
plt.figure(lfig)
if logy:
plt.semilogy(buf,label=text)
else:
plt.plot(buf,label=text)
if text and fig == 0:
plt.title(text)
plt.legend()
return lfig
#
[docs]def estimate_SNR(estim_s, true_s):
err = true_s - estim_s
return 10*np.log10(sum(abs(true_s)**2)/sum(abs(err)**2))
################################################################################
[docs]class ExpData(object):
"""
Implement an experimental data to be analysed
Combined with the definition of the transform ( TransferFunction ), defines the scene for the MaxEnt solver
"""
def __init__(self, data):
"""
defines
data : an array that contains the experimental data
noise : the value of the noise, in the same unit as data
window : allows to define error on data for each data point - initialized to 1.0
error in data[i] is noise/window[i]
"""
self.data = data
self.window = np.ones(data.shape)
self.noise = 0
[docs]class TransferFunction(object):
""" defines the transfert functions for a given problem to be solved by MaxEnt
data <-- transform -- image The experimental transfer function
data -- t_transform --> image The transpose of `transform'
must implement
transform() // defined above
t_transform()
init_transform()
norm // the norm of the transform operator, defined as ||tr(x)|| / ||x||
"""
#-------------------------------
def __init__(self):
"""
sets-up the default function, which should be overloaded by a sub classer
"""
self.init_transform()
#-------------------------------
#@counting
#-------------------------------
#@counting
#-------------------------------
#########################################################
#-------------------------------
[docs]def entropy(F, sc):
"""
compute the entropy of the ndarray a, given scaling value sc
S = sum of -Z(i)*LOG(Z(i)), where Z(i) = a(i)/sc )
"""
l = np.abs(F) / sc
S = - np.sum(l*np.log(l))
return S
#-------------------------------
[docs]def entropy_prior(F, A):
"""
compute the entropy of the ndarray F, given a prior knowledge in ndarray A
S = - sum [F(i) - A(i) + F(i)*LOG(F(i)/A(i)) ]
"""
l = np.abs(F/A)
S = - np.sum(F - A + F*np.log(l))
return S
#-------------------------------
[docs]def d_entropy_Gifa(F, S, sc):
"""
compute the derivative of entropy of the ndarray a, given entropy S and scaling value sc
Gifa : dS/dFi = -1/sc (S+log(Fi/A))
"""
l = np.abs(F) / sc
return (-S - np.log(l))/sc
#-------------------------------
[docs]def d_entropy_Skilling(F, sc): # to change - MAD
"""
compute the derivative of entropy of the ndarray a, given entropy S and scaling value sc
Skilling : dS/dFi = -1/sc (1+log(Fi/A))
"""
l = np.abs(F)/sc
return (-1.0/sc) * (1.0 + np.log(l))
#-------------------------------
[docs]def d_entropy_prior(F, A): # to change - MAD
"""
compute the derivative of entropy of the ndarray F, given a prior knowledge in ndarray A
"""
return (-np.log(F/A))
########################################################
[docs]class MaxEnt(object):
"""
implements the Maximum Entropy solver
given M
finds the minimum of Q = S(M) - lamb*Chi2(M)
with lamb such that Chi2(M) is normalized
"""
#-------------------------------
def __init__(self, transferfunction, expdata, iterations=10, debug=0 ):
"""
initialize a MaxEnt minimiser
sets expdata and transferfunction attributes from arguments
expdata should be a ExpData instance
transferfunction should be a TransferFunction instance
debug == 0 : production code
debug == 1 : display evolution
debug == 2 : full debug
"""
self.debug = debug
self.expdata = expdata
if self.expdata.noise > 0:
self.sigma = self.expdata.noise
else:
self.sigma = 0.01*max(self.expdata.buffer) # assume a 1% noise level
self.tf = transferfunction
self.iterations = iterations
# then initialisation
self.setup()
self.iter = 0 # current iteration counter
self.lchi2 = [] # will be used to store avancement
self.lentropy = []
self.llamb = []
# initialize image
tt = self.tf.t_transform(self.expdata.data)
mm = max(tt)/tt.size
self.image = mm*np.ones(tt.shape)
self.sum = np.sum(self.image)
if self.debug>2:
plot(self.image,"image initiale")
self.chi2 = self.do_chisquare(self.image, mode='none')
self.S = self.do_entropy(self.image, mode='none')
self.lamb = self.S / self.chi2
self.true_s = None
[docs] def report(self):
"""report internal state values"""
for i in dir(self):
att = getattr(self,i)
if not callable(att) and not i.startswith('_'):
print("%s\t:\t%s"% (i, att))
[docs] def setup(self, mode="default"):
"""
Set-up internal MaxEnt parameters so that a kind of optimal state is given
mode can be (case insensitive)
"Descent"
"Newton"
"Gifa"
"default" : currently sets to Descent
modifies (M the MaxEnt instance):
M.algo M.exptang M.lambdacont M.lambdamax M.lambdasp M.miniter M.stepmax
"""
if mode.lower() in ('descent', 'default'):
# convergence control
self.algo = "steepest" # convergence algorithm : steepest cg ncg bfgs Gifa
self.stepmax = 2.0 # maximum step along search axis
self.miniter = 10 # number of iterations for 1D search
self.exptang = 1.0 # point at which exp() function switches to linear - used by Gifa algo
self.chi2target = 1.0 # stopping criterium - stops when normalized chi2 reaches 1.0
# lambda control
self.lambdacont = "increment" # algo for driving lambda : none increment angle cosine
self.lambdasp = 1.1 # how hard we push on lambda
self.lambdamax = 1.3 # how fast we modify it
elif mode.lower() == 'gifa' :
self.algo = "Gifa"
self.stepmax = 2.0
self.miniter = 10
self.exptang = 0.0
self.chi2target = 1.0
self.lambdacont = "cosine"
self.lambdasp = 2.0
self.lambdamax = 1.5
else:
raise (Exception("Wrong mode in MaxEnt.setup()"))
return
#-------------------------------
[docs] def do_chisquare(self,image,mode='derivative'):
"""
computes chi2 (scalar) and its derivative (array),
computed between the current image
and self.expdata which is the experimental data
returned as (chi2, dchi2)
if mode != 'derivative' then only chi2 is computed, and returned
"""
#chi2 = Sum (TF(image)-data)**2)*window / sigma
residu = (self.tf.transform(image)-self.expdata.data)/self.sigma
chi2 = np.dot(residu,residu) / residu.size
if mode == 'derivative': # then
# dCHI = 2*tTF(TF(image)-data))/sigma
dchi2 = (2.0/residu.size) * self.tf.t_transform( residu )
if mode == 'derivative':
return (chi2, dchi2)
else:
return chi2
[docs] def do_entropy(self,image,mode='derivative'):
"""
computes the entropy S (scalar) and its derivative (array),
computed on image
returned as (S, dS)
if mode != 'derivative' then only S is computed, and returned
"""
S = entropy(image, self.sum)
if mode == 'derivative': # compute derivative of entropy
dS = d_entropy_Gifa(self.image, S, self.sum)
return (S, dS)
else:
return S
#-------------------------------
#@counting
[docs] def Q(self, im):
"""
the Q function returns the value of -Q for a given image
-Q so that it can be minimized
"""
imclp = im.clip(1.0E-8)
S = self.do_entropy(imclp, mode="entropy")
Chi2 = self.do_chisquare(imclp, mode="Chi2")
Q = S - self.lamb*Chi2
if self.debug>2: print("in Qfunc S=%f, Chi2=%f, Q=%f"%( S, Chi2, Q))
return -Q
#-------------------------------
[docs] def sexp(self,x):
"""
a local and linearized version of the exponential
exptang is the tangent point
"""
return np.where(x<self.exptang, np.exp(x), math.exp(self.exptang)*(x+1.0-self.exptang))
#-------------------------------
[docs] def drive_conv(self,dchi,dS):
"""
returns (conv, lambda_optim)
given the derivative dchi and dS, and the current settings
"""
# convergence test cosine(dS,dC) = dS.dC/(|dS| |dC|)
if self.iter == 0:
conv=0.0
lambda_optim = self.lamb
else:
dSsq = np.dot(dS,dS)
dCsq = np.dot(dchi,dchi)
dSdC = np.dot(dchi,dS)
if dSsq*dCsq != 0.0 : # We'd be in trouble !
conv = dSdC / math.sqrt(dSsq*dCsq) # conv is the cos(angle) between dC and dS
SCratio = math.sqrt(dSsq/dCsq)
else:
if self.debug>0: print("*** WARNING in drive_conv, dSsq*dCsq == 0.0")
conv = 0.0
SCratio = 1.0
# lambda control
if self.lambdacont == "none":
lambda_optim = self.lamb
elif self.lambdacont == "increment":
lambda_optim = self.lamb*self.lambdamax
#if lambcont == "angle"
# the best lambda could be such as dQ.dS = 0.0
# i.e. descending Q does not modify the entropy
# => lambda = dS dS / dS dC
# but requires that conv.gt.0.0 (dC dS angle larger than pi/2)
elif self.lambdacont == "angle":
if (conv > 0.0):
lambda_optim = dSsq / dSdC
else:
lambda_optim = 0.0
print("Warning : Inverse geometry in Angle")
# or such as cos(dQ,-dC) = cos(dQ.dS) if lambcont == "cosine"
elif self.lambdacont == "cosine":
lambda_optim = (SCratio*dSdC + dSsq)/(SCratio*dCsq + dSdC)
sqdSsq = np.sqrt(dSsq)
sqdCsq = np.sqrt(dCsq)
# lambda_optim = (sqdCsq*dSsq + sqdSsq*dSdC) / (sqdCsq*dSdC - sqdSsq*dCsq)
else:
raise Exception("error in lambdacont value")
if lambda_optim < 0.0:
if self.debug>1:
print("lambda_optim neg",lambda_optim)
lambda_optim = 0.0
return (conv, lambda_optim)
#-------------------------------
[docs] def update_lambda(self, newlambda, inc=0.5):
"""
moves self.lamb to new value, using new value newlambda as internal control
typically : l = l (1-inc) * l inc
restricted to +/- lambdamax
"""
if self.iter>0:
new_lamb = (1.0-inc)*self.lamb + inc*self.lambdasp*newlambda
self.lamb = min(new_lamb,self.lambdamax*self.lamb)
else:
self.lamb = newlambda
if self.debug>0:
self.llamb.append(self.lamb)
#@counting
[docs] def concentring(self):
"used when no valid step is found"
# set as a function to allow counting
self.image *= 0.99
if self.debug>0: print("concentring")
#-------------------------------
[docs] @timeit
def solve(self):
"""
main entry point for MaxEnt
after having been initialised, and all attributes set,
calling solve will realize the MaxEnt convergence.
after having called solve(), the following fields are updated : (see self.report_convergence())
self.iter The number of iteration performed so far
self.image The final image obtained on the last iteration
self.chi2 The chi2 obtained on the last iteration
self.S The entropy obtained on the last iteration
self.sum The integral of self.image
self.lamb The lambda obtained on the last iteration
self.dchi2 The derivative of chi2 obtained on the last iteration
self.dS The derivative of S obtained on the last iteration
the following will be populated only if self.debug>0 :
self.lchi2 The list of chi2 values since the beginning
self.lentropy The list of S values since the beginning
self.llamb The list of lambda values since the beginning
"""
def clp(im):
"force positivity of im"
im = im.clip(1e-8)
return im
def foptim(x):
"""
return -Q( image + x.dQ )
this function get minimized during 1D line-search
"""
im = self.image + x*dQ
im = clp(im)
return self.Q(im)
def Qprime(im):
"""returns the derivative of -Q"""
lim = im
(chi2,dchi) = self.do_chisquare(lim)
(S,dS) = self.do_entropy(lim)
return self.lamb * dchi - dS
def Qhess_p(im, p):
"""
the Qhess_p function returns the result of the matrix product of the hessian of -Q at given image with vector p
"""
im = np.clip(im,0.001,np.Inf)
H = (1.0/im) + 2*self.lamb * self.tf.norm**2 # estimate of the Hessian - Cornwell et al algorithm
return p/H
#-------------------------------
if self.debug >1:
fig = plot(self.image,'iter 0')
while (self.iter <= self.iterations and self.chi2 > self.chi2target):
# print "self.chi2 > self.chi2target",self.chi2, self.chi2target, self.chi2 > self.chi2target
# prepare
self.sum = np.sum(self.image)
print("----------- ITER:",self.iter)
if self.debug>1:
print("Sum : %f"%self.sum)
# compute values and derivatives
(chi2,dchi) = self.do_chisquare(self.image)
self.chi2 = chi2
(S,dS) = self.do_entropy(self.image)
self.S = S # / math.log(self.image.size)
self.dchi2 = dchi
self.dS = dS
if self.debug>0:
print("S : %f Chi2 : %f"%(self.S, self.chi2))
self.lchi2.append(chi2)
self.lentropy.append(S)
# compute convergence and lambda
conv, lambda_optim = self.drive_conv(dchi,dS)
self.update_lambda(lambda_optim)
print("convergence : %F lambda : %f"%(conv,self.lamb))
step = 0.0
if self.debug>0:
print("lambda_optim : %f, new_lambda : %f"%(lambda_optim, self.lamb))
Q0 = self.Q(self.image) # store for later
# make one step
if self.algo == "steepest": #here steepest descent !
dQ = self.lamb * dchi - dS
step = scipy.optimize.brent(foptim, brack=(0.0, self.stepmax), maxiter=self.miniter, full_output=0)
if step != 0:
self.image = clp(self.image + step*dQ)
else:
self.concentring()
elif self.algo == "Gifa": #Gifa is a kind of fixed point
dQ = self.sum * self.sexp(-self.sum*dchi - self.S) - self.image # sexp() is an ad-hoc version of exp()
step = scipy.optimize.brent(foptim, brack=(0.0, self.stepmax), maxiter=self.miniter, full_output=0)
# step = scipy.optimize.fminbound(foptim, x1=0.0, x2=self.stepmax, maxfun=self.miniter, full_output=False)
if step != 0:
self.image = clp(self.image + step*dQ)
else:
self.concentring()
# These are increasing complexity minimiser. ncg seems to perform best
elif self.algo == "cg": # conjugated gradient
self.image = scipy.optimize.fmin_cg(self.Q, self.image, fprime=Qprime, gtol=1e-4, maxiter=self.miniter, disp=0, callback=clp)
elif self.algo == "bfgs": # Broyden, Fletcher, Goldfarb, and Shanno
self.image = scipy.optimize.fmin_bfgs(self.Q, self.image, Qprime, gtol=1e-4, maxiter=self.miniter, disp=0, callback=clp)
elif self.algo == "ncg": # newton conjugated gradient
self.image = scipy.optimize.fmin_ncg( self.Q, self.image, Qprime, fhess_p=Qhess_p, fhess=None, maxiter=self.miniter, disp=1, callback=clp)
elif self.algo == "slsqp": # not debuged yet ! Sequential Least-SQuare Programing
self.image = scipy.optimize.fmin_bfgs(self.Q, self.image, Qprime, gtol=1e-4, maxiter=self.miniter, disp=0, callback=clp)
else:
raise Exception('Error with algo')
# and update
if self.debug>0:
print("Q avant %f Q apres %f inc %f"%(Q0, self.Q(self.image), step))
self.iter += 1
if self.debug >1:
plot(self.image,"iter %d"%self.iter,fig=fig)
if self.true_s is not None:
print('SNR ',estimate_SNR(self.image, self.true_s))
if self.debug >0:
plot(self.lentropy,"Entropy")
plot(self.lchi2, "$\chi^2$",logy=True)
plot(self.llamb, "$\lambda$ evolution")
return
[docs] def report_convergence(self):
"""
returns a set of array, which describe the way convergence was handled
returns
self.iter The number of iteration performed so far
self.image The final image obtained on the last iteration
self.chi2 The chi2 obtained on the last iteration
self.S The entropy obtained on the last iteration
self.sum The integral of self.image
self.lamb The lambda obtained on the last iteration
self.dchi2 The derivative of chi2 obtained on the last iteration
self.dS The derivative of S obtained on the last iteration
(following are [] if self.debug==0 )
self.lchi2 The list of chi2 values since the beginning
self.lentropy The list of S values since the beginning
self.llamb The list of lambda values since the beginning
"""
if self.iter>0:
r = [self.iter, self.image, self.chi2, self.S, self.sum, self.lamb, self.dchi2, self.dS, self.lchi2, self.lentropy, self.llamb]
else:
r = [self.iter, self.image]
return r
#-------------------------------
# TESTS
[docs]class maxent_Tests(unittest.TestCase):
[docs] def setup(self, size = 100, noise = 0.1, sc = 1):
"set-up scene"
# initialize TransferFunction
T = TransferFunction()
# create starting image
Ideal = initial_scene(size,sc)
Exp = T.transform(Ideal)
Exp += noise*np.random.randn(Exp.size)
# initialize ExpData and load data
D = ExpData(Exp)
D.noise = noise
return Ideal, D, T
[docs] def test_sexp(self):
"draw sexp()"
Ideal, D, T = self.setup()
M = MaxEnt(T, D)
x = np.linspace(-5,5)
for i in range(5):
M.exptang = float(i)
plt.plot(x, M.sexp(x), label = "exptang=" + str(i))
plt.legend()
[docs] def setup_test(self, M):
"set-up M fr tests"
M.algo = "Gifa"
# M.chi2target = 0.9
M.exptang = 0.0
M.lambdacont = "cosine" #"increment"# "cosine"
# M.lamb = 80
M.lambdamax = 1.5
M.lambdasp = 2.0
M.miniter = 100
[docs] def test_MaxEnt(self):
"run MaxEnt test"
Ideal, D, T = self.setup(noise = 0.03)
M = MaxEnt(T, D, debug = 1, iterations = 500)
M.true_s = Ideal
self.setup_test(M)
M.report()
M.solve()
f = plot(Ideal,'ideal') # dummy, just to create a new image
plt.subplot(411)
plot(Ideal,'ideal',fig = f)
plt.subplot(412)
plot(D.data,'experimental (convoluted, noised)',fig = f)
plt.subplot(413)
plot(M.image,'finale',fig = f)
residu = T.transform(M.image) - D.data
plt.subplot(414)
plot(residu, 'residu',fig = f)
print("Noise in residu :", np.sum(residu**2), M.chi2)
return M.report_convergence()
if __name__ == '__main__':
unittest.main()