我正在尝试解决类似于下面描述的玩具示例的优化问题。正如评论中所指出的,我目前使用 scipy 的实现太慢了,而且似乎没有收敛。我如何获得一个体面的解决方案?你可以使用 scipy、mystic 或任何你认为合适的库。
请注意,我不需要全局最小值,搜索可以立即停止loss(X) <= 1
。现实世界的目标主要是用 SQL 编写的,因此速度非常慢,所以我还希望优化在loss
被评估约 200 次时终止。(这不是硬性要求,您也可以在优化运行 5 分钟后终止。)
虽然这个问题类似于Minimizing non-convex function with linear constraint 和 bound in mystic,但它绝对不是重复的。这两个问题甚至没有处理相同的目标。
import numpy as np
from scipy.optimize import differential_evolution, LinearConstraint
# Inhabitants in a rural city are voting for the name of a newborn. The city houses 40 dwarves
# and 10 humans in total, and there are 100 candidate names to vote for.
dwarf_population, human_population = 40, 10
population = dwarf_population + human_population
candidate_count = 100
# Each inhabitant has different number of votes in their hand.
scores_per_citizen = np.random.randint(15, 20, population)
# Let's say the original result looks like this. (Yes, one can cast a fraction of their votes)
alpha = np.abs(np.random.normal(size=candidate_count))
scores = np.diag(scores_per_citizen).dot(np.random.dirichlet(alpha, population))
assert np.allclose(scores.sum(1), scores_per_citizen)
# Here is how the votes are counted.
def count(scores_: np.ndarray) -> np.ndarray:
# Dwarves have a weird tradition: the top 10 popular names chosen by dwarves will get all their votes.
# (I guess this is what makes our objective non-convex.)
scores_by_dwarves = scores_[0:40, :]
score_per_candidate_by_dwarves_raw = scores_by_dwarves.sum(1)
top_10_popular_name_indices = np.argsort(-score_per_candidate_by_dwarves_raw)[:10]
score_per_candidate_by_dwarves = np.zeros(candidate_count)
score_per_candidate_by_dwarves[top_10_popular_name_indices] = score_per_candidate_by_dwarves_raw[
top_10_popular_name_indices]
score_per_candidate_by_dwarves = scores_by_dwarves.sum() \
* score_per_candidate_by_dwarves \
/ score_per_candidate_by_dwarves.sum()
assert np.allclose(score_per_candidate_by_dwarves.sum(), score_per_candidate_by_dwarves_raw.sum())
# Humans, on the other hand, just adds everyone's votes together.
scores_by_humans = scores_[40:, :]
scores_per_candidate_by_humans = scores_by_humans.sum(0)
# The final result is the sum of the scores by dwarves and humans.
return score_per_candidate_by_dwarves + scores_per_candidate_by_humans
# So this is the legitimate result.
scores_per_candidate = count(scores)
# Now, you want to cheat a bit and make your proposal (assuming it's the first one) the most popular one.
target_scores_per_candidate = scores_per_candidate.copy()
argmax = scores_per_candidate.argmax()
target_scores_per_candidate[argmax] = scores_per_candidate[0]
target_scores_per_candidate[0] = scores_per_candidate[argmax]
assert np.allclose(scores_per_candidate.sum(), target_scores_per_candidate.sum())
# However, you cannot just manipulate the result, otherwise the auditors will find out!
# Instead, the raw scores must be manipulated such that your submission ranks top among others.
# You decide to solve for a multiplier to the original scores.
init_coef = np.ones_like(scores).reshape(-1)
# In other words, your goal is to find the minimum of the following objective.
def loss(coef_: np.ndarray) -> float:
scores_ = scores * coef_.reshape(scores.shape)
scores_per_candidate_ = count(scores_)
return ((scores_per_candidate_ - scores_per_candidate) ** 2).sum()
# This is a helper constant matrix. Ignore it for now.
A = np.concatenate([np.tile(np.concatenate([np.repeat(1, candidate_count),
np.repeat(0, population * candidate_count)]),
population - 1),
np.repeat(1, candidate_count)])
A = A.reshape((population, population * candidate_count))
# Meanwhile, some constraints must hold.
def constraints(coef_: np.ndarray):
# The total votes of each citizen must not change.
coef_reshaped = coef_.reshape((population, candidate_count))
assert np.allclose((coef_reshaped * scores).sum(1), scores_per_citizen)
# Another way to express the same thing with matrices.
assert np.allclose(A.dot(np.diag(scores.reshape(-1))).dot(coef_), scores_per_citizen)
# Additionally, all scores must be positive.
assert np.all(coef_reshaped * scores >= 0)
# Let's express the constraint with a LinearConstraint.
score_match_quota = LinearConstraint(A.dot(np.diag(scores.reshape(-1))), scores_per_citizen, scores_per_citizen)
# Run optimization (Spoiler: this is extremely slow, and doesn't seem to converge)
rv = differential_evolution(loss,
bounds=[(0, 1000)] * init_coef.size, # the 1000 here is superficial and can be replaced by other large numbers
init=np.vstack((init_coef, init_coef, init_coef, init_coef, init_coef)),
constraints=score_match_quota)
# Sanity check
constraints(rv.x)