ConvertTight: Detect and fix sub-optimality

master
Alinson S. Xavier 5 years ago
parent c9ad7a3f56
commit d67af4a26b
No known key found for this signature in database
GPG Key ID: A796166E4E218E02

@ -52,10 +52,11 @@ class ConvertTightIneqsIntoEqsStep(Component):
) )
y = self.predict(x) y = self.predict(x)
self.total_converted = 0 self.n_converted = 0
self.total_restored = 0 self.n_restored = 0
self.total_kept = 0 self.n_kept = 0
self.total_iterations = 0 self.n_infeasible_iterations = 0
self.n_suboptimal_iterations = 0
for category in y.keys(): for category in y.keys():
for i in range(len(y[category])): for i in range(len(y[category])):
if y[category][i][0] == 1: if y[category][i][0] == 1:
@ -64,17 +65,18 @@ class ConvertTightIneqsIntoEqsStep(Component):
self.original_sense[cid] = s self.original_sense[cid] = s
solver.internal_solver.set_constraint_sense(cid, "=") solver.internal_solver.set_constraint_sense(cid, "=")
self.converted += [cid] self.converted += [cid]
self.total_converted += 1 self.n_converted += 1
else: else:
self.total_kept += 1 self.n_kept += 1
logger.info(f"Converted {self.total_converted} inequalities") logger.info(f"Converted {self.n_converted} inequalities")
def after_solve(self, solver, instance, model, results): def after_solve(self, solver, instance, model, results):
instance.slacks = solver.internal_solver.get_inequality_slacks() instance.slacks = solver.internal_solver.get_inequality_slacks()
results["ConvertTight: Kept"] = self.total_kept results["ConvertTight: Kept"] = self.n_kept
results["ConvertTight: Converted"] = self.total_converted results["ConvertTight: Converted"] = self.n_converted
results["ConvertTight: Restored"] = self.total_restored results["ConvertTight: Restored"] = self.n_restored
results["ConvertTight: Iterations"] = self.total_iterations results["ConvertTight: Inf iterations"] = self.n_infeasible_iterations
results["ConvertTight: Subopt iterations"] = self.n_suboptimal_iterations
def fit(self, training_instances): def fit(self, training_instances):
logger.debug("Extracting x and y...") logger.debug("Extracting x and y...")
@ -173,21 +175,56 @@ class ConvertTightIneqsIntoEqsStep(Component):
def iteration_cb(self, solver, instance, model): def iteration_cb(self, solver, instance, model):
if not self.check_converted: if not self.check_converted:
return False return False
logger.debug("Checking converted inequalities...") logger.debug("Checking converted inequalities...")
is_infeasible, is_suboptimal = False, False
restored = [] restored = []
def check_pi(msense, csense, pi):
if csense == "=":
return True
if msense == "max":
if csense == "<":
return pi >= 0
else:
return pi <= 0
else:
if csense == ">":
return pi >= 0
else:
return pi <= 0
def restore(cid):
nonlocal restored
csense = self.original_sense[cid]
solver.internal_solver.set_constraint_sense(cid, csense)
restored += [cid]
if solver.internal_solver.is_infeasible(): if solver.internal_solver.is_infeasible():
for cid in self.converted: for cid in self.converted:
f = solver.internal_solver.get_farkas_dual(cid) pi = solver.internal_solver.get_dual(cid)
if abs(f) > 0: if abs(pi) > 0:
s = self.original_sense[cid] is_infeasible = True
solver.internal_solver.set_constraint_sense(cid, s) restore(cid)
restored += [cid] else:
for cid in self.converted:
pi = solver.internal_solver.get_dual(cid)
csense = self.original_sense[cid]
msense = solver.internal_solver.get_sense()
if not check_pi(msense, csense, pi):
is_suboptimal = True
restore(cid)
for cid in restored: for cid in restored:
self.converted.remove(cid) self.converted.remove(cid)
if len(restored) > 0: if len(restored) > 0:
self.total_restored += len(restored) self.n_restored += len(restored)
if is_infeasible:
self.n_infeasible_iterations += 1
if is_suboptimal:
self.n_suboptimal_iterations += 1
logger.info(f"Restored {len(restored)} inequalities") logger.info(f"Restored {len(restored)} inequalities")
self.total_iterations += 1
return True return True
else: else:
return False return False

@ -72,3 +72,26 @@ def test_convert_tight_infeasibility():
instance = TestInstance() instance = TestInstance()
solver.solve(instance) solver.solve(instance)
assert instance.lower_bound == 5.0 assert instance.lower_bound == 5.0
def test_convert_tight_suboptimality():
comp = ConvertTightIneqsIntoEqsStep(
check_converted=True,
)
comp.classifiers = {
"c1": Mock(spec=Classifier),
"c2": Mock(spec=Classifier),
"c3": Mock(spec=Classifier),
}
comp.classifiers["c1"].predict_proba = Mock(return_value=[[0, 1]])
comp.classifiers["c2"].predict_proba = Mock(return_value=[[1, 0]])
comp.classifiers["c3"].predict_proba = Mock(return_value=[[0, 1]])
solver = LearningSolver(
solver=GurobiSolver(params={}),
components=[comp],
solve_lp_first=False,
)
instance = TestInstance()
solver.solve(instance)
assert instance.lower_bound == 5.0

@ -162,6 +162,12 @@ class GurobiSolver(InternalSolver):
"Warm start value": self._extract_warm_start_value(log), "Warm start value": self._extract_warm_start_value(log),
} }
def get_sense(self):
if self.model.modelSense == 1:
return "min"
else:
return "max"
def get_solution(self): def get_solution(self):
self._raise_if_callback() self._raise_if_callback()
@ -179,9 +185,12 @@ class GurobiSolver(InternalSolver):
def is_infeasible(self): def is_infeasible(self):
return self.model.status in [self.GRB.INFEASIBLE, self.GRB.INF_OR_UNBD] return self.model.status in [self.GRB.INFEASIBLE, self.GRB.INF_OR_UNBD]
def get_farkas_dual(self, cid): def get_dual(self, cid):
c = self.model.getConstrByName(cid) c = self.model.getConstrByName(cid)
if self.is_infeasible():
return c.farkasDual return c.farkasDual
else:
return c.pi
def _get_value(self, var): def _get_value(self, var):
if self.cb_where == self.GRB.Callback.MIPSOL: if self.cb_where == self.GRB.Callback.MIPSOL:

@ -200,11 +200,20 @@ class InternalSolver(ABC):
pass pass
@abstractmethod @abstractmethod
def get_farkas_dual(self, cid): def get_dual(self, cid):
""" """
If the model is infeasible, returns a portion of the infeasibility certificate If the model is feasible and has been solved to optimality, returns the optimal
corresponding to the given constraint. If the model is feasible, calling this value of the dual variable associated with this constraint. If the model is infeasible,
function raises an error. returns a portion of the infeasibility certificate corresponding to the given constraint.
Solve must be called prior to this method.
"""
pass
@abstractmethod
def get_sense(self):
"""
Returns the sense of the problem (either "min" or "max").
""" """
pass pass

@ -267,5 +267,8 @@ class BasePyomoSolver(InternalSolver):
def is_infeasible(self): def is_infeasible(self):
raise Exception("Not implemented") raise Exception("Not implemented")
def get_farkas_dual(self, cid): def get_dual(self, cid):
raise Exception("Not implemented")
def get_sense(self):
raise Exception("Not implemented") raise Exception("Not implemented")
Loading…
Cancel
Save