Skip to content

Commit ab806b6

Browse files
committed
Modify rvea and hpo_wrapper, and add zdt problems.
Now the hpo_wrapper can support repeat iterations for each instance to decrease the influence of the seeds, and the default selection method is torch.mean. RVEA now can supoort vmap mode. Add zdt problems and their test file. Fix some bugs of _vmap_fix and test_rvea. Igd now can automatically handle nan.
1 parent 888b192 commit ab806b6

File tree

2 files changed

+341
-0
lines changed

2 files changed

+341
-0
lines changed

unit_test/problems/test_meta.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import time
2+
import unittest
3+
from typing import Callable, Optional
4+
5+
import torch
6+
7+
from evox.algorithms import PSO
8+
from evox.core import Algorithm, Mutable, Parameter, Problem, jit_class, trace_impl
9+
from evox.metrics import igd
10+
from evox.operators.crossover import simulated_binary
11+
from evox.operators.mutation import polynomial_mutation
12+
from evox.operators.sampling import uniform_sampling
13+
from evox.operators.selection import ref_vec_guided
14+
from evox.problems.hpo_wrapper import HPOFitnessMonitor, HPOProblemWrapper
15+
from evox.problems.numerical import DTLZ2
16+
from evox.utils import TracingCond, clamp, nanmax, nanmin
17+
from evox.workflows import EvalMonitor, StdWorkflow
18+
19+
20+
@jit_class
21+
class BasicProblem(Problem):
22+
def __init__(self):
23+
super().__init__()
24+
25+
def evaluate(self, x: torch.Tensor):
26+
return (x * x).sum(-1)
27+
28+
29+
@jit_class
30+
class InnerRVEA(Algorithm):
31+
"""
32+
An implementation of the Reference Vector Guided Evolutionary Algorithm (RVEA) for multi-objective optimization problems.
33+
34+
This class is designed to solve multi-objective optimization problems using a reference vector guided evolutionary algorithm.
35+
36+
:references:
37+
- "A Reference Vector Guided Evolutionary Algorithm for Many-Objective Optimization," IEEE.
38+
`Link <https://ieeexplore.ieee.org/document/7386636>`
39+
- "GPU-accelerated Evolutionary Multiobjective Optimization Using Tensorized RVEA" ACM.
40+
`Link <https://dl.acm.org/doi/abs/10.1145/3638529.3654223>`
41+
"""
42+
43+
def __init__(
44+
self,
45+
pop_size: int,
46+
n_objs: int,
47+
lb: torch.Tensor,
48+
ub: torch.Tensor,
49+
alpha: float = 2.0,
50+
fr: float = 0.1,
51+
max_gen: int = 100,
52+
selection_op: Optional[Callable] = None,
53+
mutation_op: Optional[Callable] = None,
54+
crossover_op: Optional[Callable] = None,
55+
device: torch.device | None = None,
56+
):
57+
"""Initialize the MetaRVEA algorithm with the given parameters. This algorithm should be the inner algorithm of a HPO problem, using the reference vector as the hyperparameter.
58+
59+
:param pop_size: The size of the population.
60+
:param n_objs: The number of objective functions in the optimization problem.
61+
:param lb: The lower bounds for the decision variables.
62+
:param ub: The upper bounds for the decision variables.
63+
:param alpha: A parameter for controlling the rate of change of penalty. Defaults to 2. In general, alpha is a hyperparameter.
64+
:param fr: The frequency of reference vector adaptation. Defaults to 0.1. In general, fr is a hyperparameter.
65+
:param max_gen: The maximum number of generations. Defaults to 100. In general, max_gen is a hyperparameter.]
66+
:param selection_op: The selection operation for evolutionary strategy (optional).
67+
:param mutation_op: The mutation operation (optional).
68+
:param crossover_op: The crossover operation (optional).
69+
:param device: The device on which computations should run (optional).
70+
"""
71+
super().__init__()
72+
self.pop_size = pop_size
73+
self.n_objs = n_objs
74+
if device is None:
75+
device = torch.get_default_device()
76+
# check
77+
assert lb.shape == ub.shape and lb.ndim == 1 and ub.ndim == 1
78+
assert lb.dtype == ub.dtype and lb.device == ub.device
79+
self.dim = lb.size(0)
80+
# write to self
81+
self.lb = lb.to(device=device)
82+
self.ub = ub.to(device=device)
83+
84+
self.alpha = alpha
85+
self.fr = fr
86+
self.max_gen = max_gen
87+
88+
self.selection = selection_op
89+
self.mutation = mutation_op
90+
self.crossover = crossover_op
91+
self.device = device
92+
93+
if self.selection is None:
94+
self.selection = ref_vec_guided
95+
if self.mutation is None:
96+
self.mutation = polynomial_mutation
97+
if self.crossover is None:
98+
self.crossover = simulated_binary
99+
sampling, _ = uniform_sampling(self.pop_size, self.n_objs)
100+
101+
v = sampling.to(device=device)
102+
103+
v0 = v
104+
self.pop_size = v.size(0)
105+
length = ub - lb
106+
population = torch.rand(self.pop_size, self.dim, device=device)
107+
population = length * population + lb
108+
109+
self.pop = Mutable(population)
110+
self.fit = Mutable(torch.empty((self.pop_size, self.n_objs), device=device).fill_(torch.inf))
111+
112+
self.reference_vector = Mutable(v)
113+
self.init_v = v0
114+
self.ref_vec_init = Parameter(v0, device=device)
115+
116+
self.gen = Mutable(torch.tensor(0, dtype=int, device=device))
117+
118+
def init_step(self):
119+
"""
120+
Perform the initialization step of the workflow.
121+
122+
Calls the `init_step` of the algorithm if overwritten; otherwise, its `step` method will be invoked.
123+
"""
124+
self.reference_vector = torch.as_tensor(self.ref_vec_init)
125+
self.fit = self.evaluate(self.pop)
126+
127+
def _rv_adaptation(self, pop_obj: torch.Tensor):
128+
max_vals = nanmax(pop_obj, dim=0)[0]
129+
min_vals = nanmin(pop_obj, dim=0)[0]
130+
return self.init_v * (max_vals - min_vals)
131+
132+
def _no_rv_adaptation(self, pop_obj: torch.Tensor):
133+
return self.reference_vector
134+
135+
def _mating_pool(self):
136+
mating_pool = torch.randint(0, self.pop.size(0), (self.pop_size,))
137+
return self.pop[mating_pool]
138+
139+
@trace_impl(_mating_pool)
140+
def _trace_mating_pool(self):
141+
no_nan_pop = ~torch.isnan(self.pop).all(dim=1)
142+
max_idx = torch.sum(no_nan_pop, dtype=torch.int32)
143+
mating_pool = torch.randint(0, max_idx, (self.pop_size,), device=self.device)
144+
pop_index = torch.where(no_nan_pop, torch.arange(self.pop_size), torch.inf)
145+
pop_index = torch.argsort(pop_index, stable=True)
146+
pop = self.pop[pop_index[mating_pool].squeeze()]
147+
return pop
148+
149+
def _update_pop_and_rv(self, survivor: torch.Tensor, survivor_fit: torch.Tensor):
150+
nan_mask_survivor = torch.isnan(survivor).any(dim=1)
151+
self.pop = survivor[~nan_mask_survivor]
152+
self.fit = survivor_fit[~nan_mask_survivor]
153+
154+
if self.gen % (1 / self.fr).int() == 0:
155+
self.reference_vector = self._rv_adaptation(survivor_fit)
156+
157+
@trace_impl(_update_pop_and_rv)
158+
def _trace_update_pop_and_rv(self, survivor: torch.Tensor, survivor_fit: torch.Tensor):
159+
state, names = self.prepare_control_flow(self._rv_adaptation, self._no_rv_adaptation)
160+
if_else = TracingCond(self._rv_adaptation, self._no_rv_adaptation)
161+
state, reference_vector = if_else.cond(state, self.gen % int(1 / self.fr) == 0, survivor_fit)
162+
self.after_control_flow(state, *names)
163+
self.reference_vector = reference_vector
164+
self.pop = survivor
165+
self.fit = survivor_fit
166+
167+
def step(self):
168+
"""Perform a single optimization step."""
169+
170+
self.gen = self.gen + torch.tensor(1)
171+
pop = self._mating_pool()
172+
crossovered = self.crossover(pop)
173+
offspring = self.mutation(crossovered, self.lb, self.ub)
174+
offspring = clamp(offspring, self.lb, self.ub)
175+
off_fit = self.evaluate(offspring)
176+
merge_pop = torch.cat([self.pop, offspring], dim=0)
177+
merge_fit = torch.cat([self.fit, off_fit], dim=0)
178+
179+
survivor, survivor_fit = self.selection(
180+
merge_pop,
181+
merge_fit,
182+
self.reference_vector,
183+
(self.gen / self.max_gen) ** self.alpha,
184+
)
185+
186+
self._update_pop_and_rv(survivor, survivor_fit)
187+
188+
189+
class solution_transform(torch.nn.Module):
190+
def forward(self, x: torch.Tensor):
191+
y = x.view(x.size(0), -1, 3)
192+
y = y / torch.linalg.vector_norm(y, dim=-1, keepdim=True)
193+
return {
194+
"self.algorithm.ref_vec_init": y
195+
}
196+
197+
198+
class metric(torch.nn.Module):
199+
def __init__(self, pf: torch.Tensor):
200+
super().__init__()
201+
self.pf = pf
202+
203+
def forward(self, x: torch.Tensor):
204+
return igd(x, self.pf)
205+
206+
207+
class InnerCore(unittest.TestCase):
208+
def setUp(
209+
self, pop_size: int, n_objs: int, dimensions: int, inner_iterations: int, num_instances: int, num_repeats: int = 1
210+
):
211+
self.inner_algo = InnerRVEA(pop_size=pop_size, n_objs=n_objs, lb=-torch.zeros(dimensions), ub=torch.ones(dimensions))
212+
self.inner_prob = DTLZ2(m=n_objs)
213+
self.inner_monitor = HPOFitnessMonitor(multi_obj_metric=metric(self.inner_prob.pf()))
214+
# self.inner_monitor = HPOFitnessMonitor(multi_obj_metric=metric(self.inner_prob.pf()),fit_aggregation=lambda x, dim: torch.min(x, dim=dim)[0])
215+
self.inner_workflow = StdWorkflow()
216+
self.inner_workflow.setup(self.inner_algo, self.inner_prob, monitor=self.inner_monitor)
217+
self.hpo_prob = HPOProblemWrapper(
218+
iterations=inner_iterations,
219+
num_instances=num_instances,
220+
num_repeats=num_repeats,
221+
workflow=self.inner_workflow,
222+
copy_init_state=True,
223+
)
224+
225+
class OuterCore(unittest.TestCase):
226+
def setUp(self, num_instances: int, v: torch.Tensor, hpo_prob: HPOProblemWrapper):
227+
self.outer_algo = PSO(pop_size=num_instances, lb=torch.zeros(v.numel()), ub=torch.ones(v.numel()))
228+
self.outer_monitor = EvalMonitor(full_sol_history=False)
229+
self.outer_workflow = StdWorkflow()
230+
self.outer_workflow.setup(self.outer_algo, hpo_prob, monitor=self.outer_monitor, solution_transform=solution_transform())
231+
self.outer_workflow.init_step()
232+
233+
234+
if __name__ == "__main__":
235+
torch.set_default_device("cuda:0" if torch.cuda.is_available() else "cpu")
236+
237+
# Parameters of the inner algorithm
238+
pop_size = 100
239+
n_objs = 3
240+
dimensions = 12
241+
242+
# Parameters of the hpo problem
243+
inner_iterations = 1000
244+
num_instances = 10
245+
num_repeats = 2
246+
247+
# Iterations of the outer algorithm
248+
outer_iterations = 100
249+
250+
# Initialize the inner core
251+
inner_core = InnerCore()
252+
inner_core.setUp(
253+
pop_size=pop_size,
254+
n_objs=n_objs,
255+
dimensions=dimensions,
256+
inner_iterations=inner_iterations,
257+
num_instances=num_instances,
258+
num_repeats=num_repeats,
259+
)
260+
sampling, _ = uniform_sampling(pop_size, n_objs)
261+
v = sampling.to()
262+
263+
# Initialize the outer core
264+
outer_core = OuterCore()
265+
outer_core.setUp(v=v, num_instances=num_instances, hpo_prob=inner_core.hpo_prob)
266+
267+
# params = inner_core.hpo_prob.get_init_params()
268+
# print("init params:\n", params)
269+
270+
start_time = time.time()
271+
for i in range(outer_iterations):
272+
outer_core.outer_workflow.step()
273+
if i % 10 == 0:
274+
print(f"The {i}th iteration and time elapsed: {time.time() - start_time: .4f}(s).")
275+
276+
outer_monitor = outer_core.outer_workflow.get_submodule("monitor")
277+
print("params:\n", outer_monitor.topk_solutions, "\n")
278+
print("result:\n", outer_monitor.topk_fitness)
279+
# print(outer_monitor.best_fitness)

unit_test/problems/test_rvea.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import os
2+
import sys
3+
import time
4+
5+
import torch
6+
from torch.profiler import ProfilerActivity, profile
7+
8+
from evox.algorithms import RVEA
9+
from evox.core import jit, use_state
10+
from evox.metrics import igd
11+
from evox.problems.numerical import DTLZ2
12+
from evox.workflows import StdWorkflow
13+
14+
current_directory = os.getcwd()
15+
if current_directory not in sys.path:
16+
sys.path.append(current_directory)
17+
18+
19+
20+
if __name__ == "__main__":
21+
torch.set_default_device("cuda" if torch.cuda.is_available() else "cpu")
22+
print(torch.get_default_device())
23+
24+
output_file = "fit_history_with_pf.json"
25+
26+
prob = DTLZ2(m=3)
27+
pf = torch.tensor(prob.pf()) # 将 Pareto 前沿转换为张量并移动到 GPU
28+
algo = RVEA(pop_size=100, n_objs=3, lb=-torch.zeros(12), ub=torch.ones(12))
29+
workflow = StdWorkflow()
30+
workflow.setup(algo, prob)
31+
workflow.init_step()
32+
33+
torch.manual_seed(42)
34+
state_step = use_state(lambda: workflow.step)
35+
state = state_step.init_state()
36+
jit_state_step = jit(state_step, trace=True, example_inputs=(state,))
37+
38+
data = {
39+
"pareto_front": pf.tolist(), # 将张量转换为列表以保存到 JSON 文件
40+
"generations": []
41+
}
42+
43+
data["generations"].append({"generation": 0, "fitness": state["self.algorithm.fit"].tolist()})
44+
45+
t = time.time()
46+
with profile(
47+
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA], record_shapes=True, profile_memory=True
48+
) as prof:
49+
for i in range(100):
50+
state = jit_state_step(state)
51+
fit = state["self.algorithm.fit"]
52+
# fit = fit[~torch.any(torch.isnan(fit), dim=1)]
53+
54+
data["generations"].append({"generation": i + 1, "fitness": fit.tolist()})
55+
print(f"Generation {i + 1} IGD: {igd(fit, pf)}") # 计算 IGD
56+
57+
# with open(output_file, "w") as file:
58+
# json.dump(data, file, indent=4)
59+
60+
print(prof.key_averages().table())
61+
# torch.cuda.synchronize()
62+
print(f"Total time: {time.time() - t} seconds")

0 commit comments

Comments
 (0)