From f2b710e9f9d25a6bb09a920ab475482323ff94b6 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Fri, 13 Aug 2021 05:56:38 -0500 Subject: [PATCH] AlvLouWeh2017: Implement remaining features --- miplearn/features/extractor.py | 89 +++++++++++++---- tests/features/test_extractor.py | 160 +++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+), 17 deletions(-) diff --git a/miplearn/features/extractor.py b/miplearn/features/extractor.py index 17e3ef5..398a3e4 100644 --- a/miplearn/features/extractor.py +++ b/miplearn/features/extractor.py @@ -329,6 +329,9 @@ class FeaturesExtractor: c_sa_down: Optional[np.ndarray] = None, c_sa_up: Optional[np.ndarray] = None, values: Optional[np.ndarray] = None, + with_m1: bool = True, + with_m2: bool = True, + with_m3: bool = True, ) -> np.ndarray: """ Computes static variable features described in: @@ -339,12 +342,8 @@ class FeaturesExtractor: assert b is not None assert c is not None nvars = len(c) - - c_pos_sum = c[c > 0].sum() - c_neg_sum = -c[c < 0].sum() - curr = 0 - max_n_features = 30 + max_n_features = 40 features = np.zeros((nvars, max_n_features)) def push(v: np.ndarray) -> None: @@ -356,17 +355,33 @@ class FeaturesExtractor: push(np.sign(v)) push(np.abs(v)) + def maxmin(M: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + # Compute max using regular numpy operations + M_max = np.ravel(M.max(axis=0).todense()) + + # Compute min by iterating through the sparse matrix data, so that + # we skip non-zero entries + M_min = np.array( + [ + 0.0 if len(M[:, j].data) == 0 else M[:, j].data.min() + for j in range(M.shape[1]) + ] + ) + return M_max, M_min + with np.errstate(divide="ignore", invalid="ignore"): # Feature 1 push(np.sign(c)) # Feature 2 + c_pos_sum = c[c > 0].sum() push(np.abs(c) / c_pos_sum) # Feature 3 + c_neg_sum = -c[c < 0].sum() push(np.abs(c) / c_neg_sum) - if A is not None: + if A is not None and with_m1: # Compute A_ji / |b_j| M1 = A.T.multiply(1.0 / np.abs(b)).T.tocsr() @@ -394,13 +409,12 @@ class FeaturesExtractor: push_sign_abs(M1_neg_min) push_sign_abs(M1_neg_max) - if A is not None: + if A is not None and with_m2: # Compute |c_i| / A_ij M2 = A.power(-1).multiply(np.abs(c)).tocsc() # Compute max/min - M2_max = np.ravel(M2.max(axis=0).todense()) - M2_min = np.array([M2[:, j].data.min() for j in range(nvars)]) + M2_max, M2_min = maxmin(M2) # Make copies of M2 and erase elements based on sign(c) M2_pos_max = M2_max.copy() @@ -418,6 +432,45 @@ class FeaturesExtractor: push_sign_abs(M2_neg_min) push_sign_abs(M2_neg_max) + if A is not None and with_m3: + # Compute row sums + S_pos = A.maximum(0).sum(axis=1) + S_neg = np.abs(A.minimum(0).sum(axis=1)) + + # Divide A by positive and negative row sums + M3_pos = A.multiply(1 / S_pos).tocsr() + M3_neg = A.multiply(1 / S_neg).tocsr() + + # Remove +inf and -inf generated by division by zero + M3_pos.data[~np.isfinite(M3_pos.data)] = 0.0 + M3_neg.data[~np.isfinite(M3_neg.data)] = 0.0 + M3_pos.eliminate_zeros() + M3_neg.eliminate_zeros() + + # Split each matrix into positive and negative parts + M3_pos_pos = M3_pos.maximum(0) + M3_pos_neg = -(M3_pos.minimum(0)) + M3_neg_pos = M3_neg.maximum(0) + M3_neg_neg = -(M3_neg.minimum(0)) + + # Calculate max/min + M3_pos_pos_max, M3_pos_pos_min = maxmin(M3_pos_pos) + M3_pos_neg_max, M3_pos_neg_min = maxmin(M3_pos_neg) + M3_neg_pos_max, M3_neg_pos_min = maxmin(M3_neg_pos) + M3_neg_neg_max, M3_neg_neg_min = maxmin(M3_neg_neg) + + # Features 20-35 + push_sign_abs(M3_pos_pos_max) + push_sign_abs(M3_pos_pos_min) + push_sign_abs(M3_pos_neg_max) + push_sign_abs(M3_pos_neg_min) + push_sign_abs(M3_neg_pos_max) + push_sign_abs(M3_neg_pos_min) + push_sign_abs(M3_neg_neg_max) + push_sign_abs(M3_neg_neg_min) + + # Feature 36: only available during B&B + # Feature 37 if values is not None: push( @@ -427,22 +480,24 @@ class FeaturesExtractor: ) ) + # Features 38-43: only available during B&B + # Feature 44 if c_sa_up is not None: - push(np.sign(c_sa_up)) + assert c_sa_down is not None - # Feature 46 - if c_sa_down is not None: + # Features 44 and 46 + push(np.sign(c_sa_up)) push(np.sign(c_sa_down)) - # Feature 47 - if c_sa_down is not None: - push(np.log(c - c_sa_down / np.sign(c))) + # Feature 45 is duplicated - # Feature 48 - if c_sa_up is not None: + # Feature 47-48 + push(np.log(c - c_sa_down / np.sign(c))) push(np.log(c - c_sa_up / np.sign(c))) + # Features 49-64: only available during B&B + features = features[:, 0:curr] _fix_infinity(features) return features diff --git a/tests/features/test_extractor.py b/tests/features/test_extractor.py index 26cdcd5..8fdab91 100644 --- a/tests/features/test_extractor.py +++ b/tests/features/test_extractor.py @@ -86,6 +86,22 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, + 1.0, + 0.264368, + 1.0, + 0.264368, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 23.0, + 1.0, + 23.0, + 0.0, + 0.0, + 0.0, + 0.0, ], [ 26.0, @@ -109,6 +125,22 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, + 1.0, + 0.298851, + 1.0, + 0.298851, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 26.0, + 1.0, + 26.0, + 0.0, + 0.0, + 0.0, + 0.0, ], [ 20.0, @@ -132,6 +164,22 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, + 1.0, + 0.229885, + 1.0, + 0.229885, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 20.0, + 1.0, + 20.0, + 0.0, + 0.0, + 0.0, + 0.0, ], [ 18.0, @@ -155,6 +203,22 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, + 1.0, + 0.206897, + 1.0, + 0.206897, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 18.0, + 1.0, + 18.0, + 0.0, + 0.0, + 0.0, + 0.0, ], [ 0.0, @@ -178,6 +242,22 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.011494, + 1.0, + 0.011494, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, ], ] ), @@ -282,6 +362,22 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, + 1.0, + 0.264368, + 1.0, + 0.264368, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 23.0, + 1.0, + 23.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 1.0, 1.0, @@ -318,6 +414,22 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, + 1.0, + 0.298851, + 1.0, + 0.298851, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 26.0, + 1.0, + 26.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.076923, 1.0, 1.0, @@ -354,6 +466,22 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, + 1.0, + 0.229885, + 1.0, + 0.229885, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 20.0, + 1.0, + 20.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 1.0, 1.0, @@ -390,6 +518,22 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, + 1.0, + 0.206897, + 1.0, + 0.206897, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 18.0, + 1.0, + 18.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 1.0, -1.0, @@ -427,6 +571,22 @@ def test_knapsack() -> None: 0.0, 0.0, 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.011494, + 1.0, + 0.011494, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, 1.0, -1.0, 5.265874,