Commit bdfe43b2 authored by Viktor Reshniak's avatar Viktor Reshniak
Browse files

add loss class

parent 75a4b336
Loading
Loading
Loading
Loading
+93 −58
Original line number Diff line number Diff line
@@ -1262,6 +1262,83 @@ def numerical_hessian(x, fun, *args, **kwargs):

###############################################################################
###############################################################################
# Loss functions

class Loss(object):
	def __init__(self, bkgr_fun, peak_fun, points, data):
		self.func_calls = 0
		self.grad_calls = 0
		self.hess_calls = 0

		# background and peak functions
		self.bkgr_fun = bkgr_fun
		self.peak_fun = peak_fun

		# number of background and peak parameters
		self.nbkgr = self.bkgr_fun.nparams
		self.npeak = self.peak_fun.nparams

		# points and data to fit
		self.points = points
		self.data   = data

	def fit(self, params):
		self._fit = self.bkgr_fun(params[:self.nbkgr], self.points) + self.peak_fun(params[self.nbkgr:], self.points)
		return self._fit

	def __call__(self, params, *args, **kwargs):
		self.func_calls+=1
		self.func_params = np.asarray(params).copy()
		return self.loss(params)

	def gradient(self, params, *args, **kwargs):
		self.grad_calls += 1
		self.grad_params = np.asarray(params).copy()
		if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params):  g = self.__call__(params)

		dloss_dbkgr = self.bkgr_fun.gradient(params[:self.nbkgr], self.points, dloss_dfit=self.dloss_dfit)
		dloss_dpeak = self.peak_fun.gradient(params[self.nbkgr:], self.points, dloss_dfit=self.dloss_dfit)

		return np.concatenate((dloss_dbkgr,dloss_dpeak))

	def hessian(self, params, *args, **kwargs):
		self.hess_calls += 1
		self.hess_params = np.asarray(params).copy()
		if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params):  g = self.__call__(params)
		if not hasattr(self, 'grad_params') or not np.all(self.hess_params==self.grad_params): dg = self.gradient(params)

		d2loss_dfit2  = self.d2loss_dfit2
		d2loss_dbkgr2 = self.bkgr_fun.hessian(params[:self.nbkgr], self.points, dloss_dfit=self._dloss_dfit, d2loss_dfit2=d2loss_dfit2)
		d2loss_dpeak2 = self.peak_fun.hessian(params[self.nbkgr:], self.points, dloss_dfit=self._dloss_dfit, d2loss_dfit2=d2loss_dfit2)
		d2loss_dbkgr_dpeak = ((d2loss_dfit2.reshape((-1,1,1)) * self.bkgr_fun.grad[:,:,np.newaxis]) * self.peak_fun.grad[:,np.newaxis,:]).sum(axis=0)

		d2loss = np.block([
			[d2loss_dbkgr2,        d2loss_dbkgr_dpeak],
			[d2loss_dbkgr_dpeak.T, d2loss_dpeak2     ]
			])

		return d2loss


class MLELoss(Loss):
	def loss(self, params):
		fit = self.fit(params)
		return (fit-self.data*np.log(fit)).sum()

	@property
	def dloss_dfit(self):
		self._dloss_dfit = 1 - self.data / self._fit
		return self._dloss_dfit

	@property
	def d2loss_dfit2(self):
		return self.data / self._fit**2


###############################################################################
###############################################################################




class PeakHistogram(object):
@@ -1841,74 +1918,32 @@ class PeakHistogram(object):
			result = least_squares(residual, #jac=jacobian_residual,
				x0=params_init, bounds=[params_lbnd,params_ubnd], method='trf', verbose=0, max_nfev=1000)
		elif loss=='mle':
			class MLELoss(object):
				def __init__(self, bkgr_fun, peak_fun):
					self.func_calls = 0
					self.grad_calls = 0
					self.hess_calls = 0

					# background and peak functions
					self.bkgr_fun = bkgr_fun
					self.peak_fun = peak_fun

					# number of background and peak parameters
					self.nbkgr = self.bkgr_fun.nparams
					self.npeak = self.peak_fun.nparams

				def __call__(self, params):
					self.func_calls+=1
					self.func_params = np.asarray(params).copy()

					# cache fit for reuse in derivative evaluations
					self.fit = self.bkgr_fun(params[:self.nbkgr], fit_points) + self.peak_fun(params[self.nbkgr:], fit_points)

					return (self.fit-fit_data*np.log(self.fit)).sum()

				def gradient(self, params, *args, **kwargs):
					self.grad_calls += 1
					self.grad_params = np.asarray(params).copy()
					if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params):  g = self.__call__(params)

					self.dloss_dfit = 1 - fit_data/self.fit
					dloss_dbkgr = self.bkgr_fun.gradient(params[:self.nbkgr], fit_points, dloss_dfit=self.dloss_dfit)
					dloss_dpeak = self.peak_fun.gradient(params[self.nbkgr:], fit_points, dloss_dfit=self.dloss_dfit)

					return np.concatenate((dloss_dbkgr,dloss_dpeak))

				def hessian(self, params, *args, **kwargs):
					self.hess_calls += 1
					self.hess_params = np.asarray(params).copy()
					if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params):  g = self.__call__(params)
					if not hasattr(self, 'grad_params') or not np.all(self.hess_params==self.grad_params): dg = self.gradient(params)

					d2loss_dfit2  = fit_data / self.fit**2
					d2loss_dbkgr2 = self.bkgr_fun.hessian(params[:self.nbkgr], fit_points, dloss_dfit=self.dloss_dfit, d2loss_dfit2=d2loss_dfit2)
					d2loss_dpeak2 = self.peak_fun.hessian(params[self.nbkgr:], fit_points, dloss_dfit=self.dloss_dfit, d2loss_dfit2=d2loss_dfit2)
					d2loss_dbkgr_dpeak = ((d2loss_dfit2.reshape((-1,1,1)) * self.bkgr_fun.grad[:,:,np.newaxis]) * self.peak_fun.grad[:,np.newaxis,:]).sum(axis=0)

					d2loss = np.block([
						[d2loss_dbkgr2,        d2loss_dbkgr_dpeak],
						[d2loss_dbkgr_dpeak.T, d2loss_dpeak2     ]
						])

					return d2loss
			loss_fun = MLELoss(bkgr_fun=self.bkgr_fun, peak_fun=self.peak_fun, points=fit_points, data=fit_data)

			# from scipy.optimize import BFGS
			# class myBFGS(BFGS):
			# 	def initialize(self, n, approx_type):
			# 		super().initialize(n, approx_type)
			# 		if self.approx_type == 'hess':
			# 			self.B = MLELoss().hessian(params_init)
			# 			# self.B = np.eye(n, dtype=float)
			# 			self.B = loss_fun.hessian(params_init)
			# 		else:
			# 			self.H = np.linalg.inv(MLELoss().hessian(params_init))
			# 			# self.H = np.eye(n, dtype=float)
			loss_fun = MLELoss(bkgr_fun=self.bkgr_fun, peak_fun=self.peak_fun)
			# 			self.H = np.linalg.inv(loss_fun.hessian(params_init))

			# result = fmin_bfgs_2(loss_fun, params_init, fprime=loss_fun.gradient, gtol=1e-6, maxiter=1000, disp=2, init_hess=None)#(loss_fun.hessian(params_init)+loss_fun.hessian(params_init).T)/2)
			# result = fmin_bfgs_2(loss_fun, result.x, fprime=loss_fun.gradient, gtol=1e-6, maxiter=1000, disp=2, init_hess=loss_fun.hessian(result.x))
			# result = minimize(loss_fun,
			# 	jac  = loss_fun.gradient,
			# 	hess = loss_fun.hessian,
			# 	x0=result.x, bounds=tuple(zip(params_lbnd,params_ubnd)),
			# 	method='Newton-CG', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':True} #_debug}
			# 	)

			result = minimize(loss_fun,
				jac  = loss_fun.gradient,
				hess = None, #loss_fun.hessian,
				hess = loss_fun.hessian,
				# hess = myBFGS(), #loss_fun.hessian,
				x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)),
				method=solver, options={'maxiter':100, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':_debug}
				method=solver, options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':True} #_debug}
				# method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True}
				# method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True}
				# method='CG', options={'maxiter':1000, 'gtol':1.e-5, 'norm':np.inf, 'disp':True}