如何限制 GAMS 混合整数非线性规划中非零变量的数量?

How to limit the count of nonzero variables in GAMS mixed-integer nonlinear programming?

问题背景:

Neo 区块链上有 25 位候选人获得投票。除了我以外的每个选民都投票了。 排名第 1 至第 7 的候选人每人将给予 200 美元,按比例分配给投票给他们的人。 8日至21日每人派发100元。比如我投了100票给目前第8位的候选人,把him/her推到了第7位,其他人投了900票给他,所以我得到候选人200*100/(900+100)=20美金.

我知道所有其他选民的投票结果,我最多可以额外投N票给n个候选人,因为我只有n个代理人代表们。也就是说,我的投票向量不应超过 n 个非零值,并且我的投票向量的总和应等于 N。 我应该如何投票获得最高利润?

("Dollar"在下面的代码中写成"GAS"。我可以投的票数在代码中写成"NEO")

我的 GAMS 代码:

这些代码出错:Endogenous $ operation not allowed
(因为我的投票,我还没有想好如何处理立场的变化。这可能很难。) (LINDO 求解器给出了目前最好的结果)

Set i "candidate index" /
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24
    /;

Parameters
    V(i) "other people's votes" /
        0 11409980
        1 3024833
        2 2200734
        3 2040928
        4 2032100
        5 2017067
        6 2010927
        7 2007339
        8 2001625
        9 1008817
        10 1000025
        11 1000000
        12 641913
        13 396926
        14 392171
        15 390364
        16 383919
        17 383722
        18 382447
        19 382310
        20 381850
        21 923
        22 272
        23 180
        24 0
    /
    NEO  "our NEOs" /1162/
    num_agents  "our num of agents" /2/
    reward_factor(i) "how much gas is given by the candidate at each rank" /
        0 200
        1 200
        2 200
        3 200
        4 200
        5 200
        6 200
        7 100
        8 100
        9 100
        10 100
        11 100
        12 100
        13 100
        14 100
        15 100
        16 100
        17 100
        18 100
        19 100
        20 100
        21 0
        22 0
        23 0
        24 0
    /;
    
Integer Variable vote(i) "voting vector that needs to be optimized";
vote.up(i) = NEO;
Free Variable GAS_reward "total GAS reward";

Binary Variable is_nonzero(i) "whether an element in the voting vector is zero";
Integer Variable count_nonzero "count of nonzero values in vote(i)";


Equations
    GAS_reward_eqn "the reward according to our voting vector v"
    sum_v_eqn "sum of voting vector equals num of our NEOs"
    is_nonzero_eqn(i) "whether the element vote(i) is not zero"
    num_agents_eqn "sum of nonzero element in voting vector does not exceed num of our agents";

GAS_reward_eqn..  GAS_reward =e= sum(i, reward_factor(i)*vote(i)/(V(i)+vote(i)+0.000001));
sum_v_eqn..           NEO =e= sum(i, vote(i));
is_nonzero_eqn(i)..  is_nonzero(i) =e= 1 $ (vote(i));
num_agents_eqn..  sum(i, is_nonzero(i)) =l= num_agents;

Model NEOBurger /all/;
Option MINLP=lindo;
Solve NEOBurger using MINLP maximizing GAS_reward;

(其中代码 is_nonzero_eqn(i).. is_nonzero(i) =e= 1 $ (vote(i)); 是非线性的!)

如果您需要额外的测试用例和参考,这里是问题的 Python(3.7 或 3.8)代码 strategy_multi_agents.py

# Good profit
# Can somehow deal with sorting problem
# Cannot restrict num of agents
# Cannot restrict to integer
from typing import Union, List

import numpy as np
np.set_printoptions(suppress=True)
from scipy.optimize import minimize

class BurgerNEOMultiAgentStrategy:
    """
    To find the best distribution of our GAS assuming that we do not affect the ranking
    """
    consensus_rank, consensus_gain = 7, 200
    council_rank, council_gain = 21, 100
    
    def __init__(self, candidate_votes: List[int], our_NEOs: int, num_of_agents: int = 2):
        """
        :param candidate_votes: should be sorted in descending order. All the results are based on the descending order.
        """
        self.len_candidate_votes = len(candidate_votes)
        if self.len_candidate_votes >= self.council_rank:
            self.candidate_votes = sorted(candidate_votes, reverse=True)
            if self.candidate_votes != candidate_votes:
                raise ValueError('You did not input a descending list of candidate_votes.')
            self.candidate_votes = np.array(self.candidate_votes, dtype=np.int_)  # descending order
        else:
            raise ValueError(f'len(candidate_votes) should be >= {self.council_rank}. Got {self.len_candidate_votes}.')
        self.our_NEOs = np.int_(our_NEOs)
        reward_factor = np.zeros(self.len_candidate_votes, dtype=np.int_)
        reward_factor[0:self.consensus_rank] = self.consensus_gain
        reward_factor[self.consensus_rank:self.council_rank] = self.council_gain
        self.reward_factor = reward_factor
        self.num_of_agents = num_of_agents
    
    def calc_GAS_gain(self, voting_vector: np.ndarray) -> Union[np.float_, np.int_]:
        new_voting_result = voting_vector + self.candidate_votes
        argsort = np.flip(new_voting_result.argsort())
        voting_vector = voting_vector[argsort]
        new_voting_result = new_voting_result[argsort]
        
        return np.sum(self.reward_factor * voting_vector / (new_voting_result + 1e-5))

    def calc_voting_vector(self):
        def sum_is_our_NEO(voting_vector: np.ndarray):
            return np.sum(voting_vector) - self.our_NEOs
        starting_point = self.our_NEOs / self.len_candidate_votes * np.ones(self.len_candidate_votes)
        constraints = [
            {'type': 'ineq', 'fun': lambda voting_vector: voting_vector},  # all greater than or equals 0
            {'type': 'eq','fun': sum_is_our_NEO},  # sum is our_NEO
            # {'type': 'ineq', 'fun': lambda x: -np.abs(x - np.round(x))},  # all integers
            # {'type': 'ineq', 'fun':
            #     lambda x: - np.count_nonzero(np.invert(np.isclose(x, np.zeros(self.len_candidate_votes), atol=1e-3/self.len_candidate_votes))) + self.num_of_agents},  # num of agents
        ]
        result = minimize(lambda voting_vector: (-self.calc_GAS_gain(voting_vector)),
                          starting_point, constraints=constraints, tol=1e-15, options={'maxiter': 1e9})
        # return np.round(result.x).astype(np.int_), -result.fun, np.sum(np.round(result.x).astype(np.int_))
        return result.x, -result.fun, np.sum(result.x)

if __name__ == '__main__':
    print(
        BurgerNEOMultiAgentStrategy(
        [11409980, 3024833, 2200734, 2040928, 2032100, 2017067, 2010927, 2007339, 2001625, 1008817, 1000025, 1000000, 641913, 396926, 392171, 390364, 383919, 383722, 382447, 382310, 381850, 923, 272, 180, 0],
        1162, 2).calc_voting_vector()
    )
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,99, 75,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 0,0,0,0, 0,0], 50, 2).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,99, 75,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 0,0], 25, 2).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,99, 75,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 1,1], 25, 2).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,5, 5,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 1,1], 25, 2).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,5, 5,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 1,0], 25, 2).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,5, 5,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 1,0], 1000, 2).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,5, 5,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 0,0], 26, 2).calc_voting_vector())
    print()

# Restricted to integer
# Restricted num of agents
# Not robust (sometimes constraints are broken)
# Not good profit
# Cannot deal with sorting
from typing import List
import random
from gekko import GEKKO
from strategy_multi_agents import BurgerNEOMultiAgentStrategy as ScipyStrategy

class BurgerNEOMultiAgentStrategy:
    """
    To find the best distribution of our GAS assuming that we do not affect the ranking
    """
    consensus_rank, consensus_gain = 7, 200
    council_rank, council_gain = 21, 100
    
    def __init__(self, candidate_votes: List[int], our_NEOs: int, num_of_agents: int = 2):
        """
        :param candidate_votes: should be sorted in descending order. All the results are based on the descending order.
        """
        self.model = GEKKO(remote=False, name='neoburger-strategy')
        self.model.options.IMODE = 3
        self.model.options.MAX_ITER = 10000
        self.model.options.COLDSTART = 1
        self.model.options.AUTO_COLD = 1

        self.len_candidate_votes = len(candidate_votes)
        if self.len_candidate_votes >= self.council_rank:
            self.candidate_votes = sorted(candidate_votes, reverse=True)
            if self.candidate_votes != candidate_votes:
                raise ValueError('You did not input a descending list of candidate_votes.')
            self.candidate_votes = [self.model.Param(can) for can in self.candidate_votes]  # descending order
        else:
            raise ValueError(f'len(candidate_votes) should be >= {self.council_rank}. Got {self.len_candidate_votes}.')
        self.our_NEOs = self.model.Param(our_NEOs, name='our_NEOs')
        reward_factor = [self.consensus_gain] * self.consensus_rank \
                      + [self.council_gain] * (self.council_rank - self.consensus_rank) \
                      + [0] * (self.len_candidate_votes - self.council_rank)
        self.reward_factor = [self.model.Param(r) for r in reward_factor]
        self.num_of_agents = self.model.Param(num_of_agents, name='num_of_agents')
        
        # voting_vector = [self.our_NEOs / self.len_candidate_votes] * self.len_candidate_votes
        voting_vector = ScipyStrategy(candidate_votes, our_NEOs, num_of_agents).calc_voting_vector()[0]
        voting_vector = [max(0, round(v)) for v in voting_vector]
        self.voting_vector = [self.model.Var(v, lb=0, ub=our_NEOs, integer=True) for v in voting_vector]
        
    def calc_GAS_gain(self):
        # new_vote_result = [v+V for v, V in zip(self.voting_vector, self.candidate_votes)]
        # sorted_vote_result = [(v, V) for v, V in zip(self.voting_vector, new_vote_result)]  # sorted(, key=lambda x: x[1].value)
        #
        # sorted_voting_vector = [v for v, V in sorted_vote_result]
        # sorted_vote_result = [V for v, V in sorted_vote_result]
        # return sum([v*r/(V+v+0.0001) for v, V, r in zip(sorted_voting_vector, sorted_vote_result, self.reward_factor)])
        return sum([v*r/(V+v+1) for v, V, r in zip(self.voting_vector, self.candidate_votes, self.reward_factor)])

    def calc_voting_vector(self):
        # constraints
        # sum(voting_vector) == our_NEOs
        self.model.Equation(sum(self.voting_vector) == self.our_NEOs)
        # agent number
        nonzero_vote = sum([self.model.if3(v - 0.1, 0, 1) for v in self.voting_vector])
        # self.model.Equation(nonzero_vote >= 0)
        self.model.Equation(nonzero_vote <= self.num_of_agents)
        
        # objective
        self.model.Maximize(self.calc_GAS_gain())
        
        # self.model.options.RTOL = 1e-14
        self.model.options.OTOL = 1e-14
        self.model.options.REQCTRLMODE = 1
        self.model.solve(disp=False, debug=False, GUI=False)
        return self.voting_vector, -self.model.options.OBJFCNVAL


if __name__ == '__main__':
    print(
        BurgerNEOMultiAgentStrategy(
        [11409980, 3024833, 2200734, 2040928, 2032100, 2017067, 2010927, 2007339, 2001625, 1008817, 1000025, 1000000, 641913, 396926, 392171, 390364, 383919, 383722, 382447, 382310, 381850, 923, 272, 180, 0],
        1162, 6).calc_voting_vector()
    )
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,99, 75,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 0,0,0,0, 0,0], 50, 6).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,99, 75,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 0,0], 25, 6).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,99, 75,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 1,1], 25, 6).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,5, 5,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 1,1], 25, 6).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,5, 5,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 1,0], 25, 6).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,5, 5,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 1,0], 1000, 6).calc_voting_vector())
    print(BurgerNEOMultiAgentStrategy([100,100,100,100,100, 100,5, 5,5,5, 5,5,5,5,5, 5,5,5,5,5, 5, 1,1,1,1, 0,0], 26, 6).calc_voting_vector())
    print()

而且我还有一些代码(此处未显示),它可以让 1 或 2 个代理的一切正确。这不是通过优化实现的,而是通过手动数学推导实现的。

我会这样写:

* This is not using proper GAMS syntax:
*is_nonzero_eqn(i)..  is_nonzero(i) =e= 1 $ (vote(i));
* a linear version is:
is_nonzero_eqn(i)..   vote(i) =l= is_nonzero(i)*vote.up(i);

这是一个标准的 MIP 公式。使用:

Model NEOBurger /all/;
Option MINLP=baron, optcr = 0;
Solve NEOBurger using MINLP maximizing GAS_reward;

我们看到:

GAMS/BARON       36.1.0 r2c0a44a Released Aug 02, 2021 WEI x86 64bit/MS Window

===========================================================================
 BARON version 21.1.13. Built: WIN-64 Wed Jan 13 16:04:12 EST 2021

 BARON is a product of The Optimization Firm.
 For information on BARON, see https://minlp.com/about-baron

 If you use this software, please cite publications from
 https://minlp.com/baron-publications, such as: 

 Kilinc, M. and N. V. Sahinidis, Exploiting integrality in the global
 optimization of mixed-integer nonlinear programming problems in BARON,
 Optimization Methods and Software, 33, 540-562, 2018.
===========================================================================
 This BARON run may utilize the following subsolver(s)
 For LP/MIP/QP: ILOG CPLEX                                      
 For NLP: MINOS, SNOPT, GAMS external NLP, IPOPT, FILTERSD, FILTERSQP
===========================================================================
 Doing local search
 Solving bounding LP
 Preprocessing found feasible solution with value 0.203660597686E-001
 Starting multi-start local search
 Done with local search
===========================================================================
  Iteration    Open nodes         Time (s)    Lower bound      Upper bound
*         1             1             0.50     0.303390         3.71066    
          1             1             0.56     0.303390         0.303737    
*         2             2             0.62     0.303405         0.303737    
*         2             2             0.69     0.303428         0.303737    
*         4             2             0.83     0.303473         0.303737    
*         4             1             0.88     0.303639         0.303737    
*         5             1             0.92     0.303681         0.303737    
          5             0             0.92     0.303681         0.303681    

 Calculating duals

                         *** Normal completion ***            

 Wall clock time:                     4.05
 Total CPU time used:                 0.94

 Total no. of BaR iterations:       5
 Best solution found at node:       5
 Max. no. of nodes in memory:       2
 
 All done
===========================================================================

Solution      = 0.30368112407161  found at node 5
Best possible = 0.303681125072
Absolute gap  = 1.00039027062238E-9  optca = 1E-9
Relative gap  = 3.29421287011233E-9  optcr = 1E-9
  (Note that BARON uses a different formula to compute the relative gap as
   was used for the above reported value.)

如果我们显示我们看到的相关变量:

----     90 VARIABLE vote.L  voting vector that needs to be optimized

19 466.000,    20 696.000


----     90 VARIABLE is_nonzero.L  whether an element in the voting vector is zero

19 1.000,    20 1.000

这是正确的行为。

请注意 MINLP = NLP + MIP,因此我们可以(并且应该)在编写 MINLP 模型时使用我们了解的有关 MIP 建模的所有工具和技巧。