Article directory
- Preface
- 1. Weight matrix and neighborhood?
- 2. Aggregation function
- 3. Complete source code
- Summarize
Foreword
I have been studying multi-objective optimization recently. I was confused when I first read the MOEA\D paper, so I decided to take a look at the author’s source code. Because the author’s C source code has too many packages, the code is not intuitive enough. There are also many C++ implementations of MOEA\D on the Internet. Few, so I decided to write a C++ version of MOEA\D. This version is simple and direct, and clearly breaks down each step of the algorithm. I hope it can help people like me who are new to the MOEA\D algorithm and are a little confused.
1. Weight matrix and neighborhood?
When I first read the paper, I didn’t understand what the weight matrix and neighborhood were at all. The writing in the paper was also very unclear. After reading the source code, some other blogs and papers, I realized that this thing is actually not complicated at all.
The weight matrix actually divides the multi-objective into multiple single-objectives and the weight of each single-objective. This weight is only used for whether to retain when generating new individual solutions during population evolution, and the fitness of each individual for each single-objective problem. The value doesn’t matter, that’s the point! ! ! This is also the reason why I was most confused at the beginning, because this weight is only used in the last step to update the individual’s use of the aggregation function, and is not used in the evaluation of function fitness. What does it mean? Look at the code
//Initialization of target weight vector matrix void init_weights() {<!-- --> for (int i = 0; i <= H; i + + ) {<!-- --> weights[i][0] = i*1.0 / H; weights[i][1] = 1 - weights[i][0]; } }
The above is a uniform setting of weights. It is as simple as this. This H is customized. For a two-objective problem, each individual only needs two weights. The population size can be determined through H and the number of targets. There is a formula for this. You can read the paper
The test function is the one below. Look, this test function has nothing to do with the weight. It only matters when it comes to the aggregation function.
//Test function ZDT1 void evaluate_ind(individual* ind) {<!-- --> ind->fvalue[0] = ind->xreal[0]; double g = 1, sum = 0; for (int i = 1; i < Dimension; i + + ) {<!-- --> sum + = ind->xreal[i]; } sum = 9 * (sum / (Dimension - 1)); g + = sum; ind->fvalue[1] = g * (1 - sqrt(ind->xreal[0] / g)); evaluations + + ; }
Neighborhood is easy to understand. In fact, it is the weight matrix you set, and then set a neighborhood size T. The first T elements with the closest Euclidean distance between each weight matrix are the neighborhoods.
//Neighborhood matrix void init_neighbors() {<!-- --> double distance[pop_size][pop_size]; memset(distance, 0, sizeof(double)); double sum; for (int i = 0; i < pop_size; i + + ) {<!-- --> for (int j = i + 1; j < pop_size; j + + ) {<!-- --> sum = 0; for (int t = 0; t < Target; t + + ) {<!-- --> sum + = (weights[i][t] - weights[j][t]) * (weights[i][t] - weights[j][t]); } distance[i][j] = sum; distance[j][i] = sum; } } for (int i = 0; i < pop_size; i + + ) {<!-- --> for (int j = 0; j < T_size; j + + ) {<!-- --> double min_temp = 0x3f3f3f3f; int sp; for (int k = 0; k < pop_size; k + + ) {<!-- --> if (distance[i][k] < min_temp) {<!-- --> min_temp = distance[i][k]; sp = k; } } neighbors[i][j] = sp; distance[i][sp] = 0x3f3f3f3f; } } }
2. Aggregation function
Use Chebyshev aggregation. This is the last step to update the solution. Only at this time are the weight matrix and Chebyshev aggregation function used. The other steps are the most basic genetic or differential evolution. There is no difference. MOEA\D There are more things like weight matrices, neighborhoods, and aggregation functions.
//Update neighborhood solution void update_neighbors(int i) {<!-- --> // Set the weight vector of 0 to a smaller value for (int j = 0; j < T_size; j + + ) {<!-- --> int k = neighbors[i][j]; for (int t = 0; t < Target; t + + ) {<!-- --> if (weights[k][t] < 1e-6) {<!-- --> weights[k][t] = 0.00001; } } } // Chebyshev aggregation double oldobj, newobj; double max_temp = -0x3f3f3f3f, part[Target]; for (int j = 0; j < T_size; j + + ) {<!-- --> int k = neighbors[i][j]; //New individual aggregate function value for (int t = 0; t < Target; t + + ) {<!-- --> part[t] = abs(new_ind.fvalue[t] - Z[t]); if (max_temp < weights[k][t] * part[t]) {<!-- --> max_temp = weights[k][t] * part[t]; } } newobj = max_temp; max_temp = -0x3f3f3f3f; // Neighborhood individual aggregation function value for (int t = 0; t < Target; t + + ) {<!-- --> part[t] = abs(parent_pop[k].fvalue[t] - Z[t]); if (max_temp < weights[k][t] * part[t]) {<!-- --> max_temp = weights[k][t] * part[t]; } } oldobj = max_temp; if (newobj<oldobj) {<!-- --> parent_pop[k] = new_ind; } } }
3. Complete source code
#include#include #include using namespace std; #define URAND ((double)rand()/((double)RAND_MAX + 1.0)) // [0.0,1.0) uniformly distributed random numbers double rndreal(double low, double high) { double val; val = URAND * (high - low) + low; return val; } int rndint(int low, int high) { int res; if (low >= high) { res = low; } else { res = low + (int)(URAND * (high - low + 1)); if (res > high) { res = high; } } return res; } #define Dimension 30 // Decision variable dimension #define H 100 // Custom positive integer, the population number N can be determined based on the positive integer and the problem dimension #define Target 2 // Target quantity #define pop_size 101 //The population size is determined by H and the number of targets #define T_size 20 //The number of weight vectors in the field #define max_evaluations 150000 // Maximum number of evaluations typedef struct { double xreal[Dimension]; // value of each dimension double fvalue[2]; // objective function value }individual; // Weight matrix double weights[pop_size][Target]; // Area int neighbors[pop_size][T_size]; //Reference point (minimum value of each sub-objective function) double Z[Target]; // Parent population and new individual individual parent_pop[pop_size]; individual new_ind; double lowerBound[Dimension]; // lower bound double upperBound[Dimension]; // upper bound long int evaluations = 0; // Current number of evaluations int gen_no = 0; // Current iteration number // Target weight vector matrix initialization void init_weights() { for (int i = 0; i <= H; i + + ) { weights[i][0] = i*1.0 / H; weights[i][1] = 1 - weights[i][0]; } } //neighborhood matrix void init_neighbors() { double distance[pop_size][pop_size]; memset(distance, 0, sizeof(double)); double sum; for (int i = 0; i < pop_size; i + + ) { for (int j = i + 1; j < pop_size; j + + ) { sum = 0; for (int t = 0; t < Target; t + + ) { sum + = (weights[i][t] - weights[j][t]) * (weights[i][t] - weights[j][t]); } distance[i][j] = sum; distance[j][i] = sum; } } for (int i = 0; i < pop_size; i + + ) { for (int j = 0; j < T_size; j + + ) { double min_temp = 0x3f3f3f3f; int sp; for (int k = 0; k < pop_size; k + + ) { if (distance[i][k] < min_temp) { min_temp = distance[i][k]; sp = k; } } neighbors[i][j] = sp; distance[i][sp] = 0x3f3f3f3f; } } } //Initialize decision boundary void init_variables() { for (int i = 0; i < Dimension; i + + ) { lowerBound[i] = 0.0; upperBound[i] = 1.0; } } // Random initialization of population void init_pop_uniform() { double low, up; for (int i = 0; i < pop_size; i + + ) for (int j = 0; j < Dimension; j + + ) { low = lowerBound[j]; up = upperBound[j]; parent_pop[i].xreal[j] = rndreal(low, up); } } // Get reference point void get_Z(individual* pop) { for (int i = 0; i < Target; i + + ) { double min_temp = 0x3f3f3f3f; for (int j = 0; j < pop_size; j + + ) { if (pop[j].fvalue[i] < min_temp) { min_temp = pop[j].fvalue[i]; } } Z[i] = min_temp; } } //Test function ZDT1 void evaluate_ind(individual* ind) {<!-- --> ind->fvalue[0] = ind->xreal[0]; double g = 1, sum = 0; for (int i = 1; i < Dimension; i + + ) {<!-- --> sum + = ind->xreal[i]; } sum = 9 * (sum / (Dimension - 1)); g + = sum; ind->fvalue[1] = g * (1 - sqrt(ind->xreal[0] / g)); evaluations + + ; } // Group fitness evaluation void evaluate_pop(individual* pop, int size) { for (int i = 0; i < size; i + + ) { evaluate_ind( & amp;pop[i]); } } // Differential evolution void evolution(int i) { double CR = 0.9, F = 0.4; int r1, r2, r3; do { r1 = rndint(0, T_size-1); r1 = neighbors[i][r1]; } while (r1 == i); do { r2 = rndint(0, T_size-1); r2 = neighbors[i][r2]; } while (r2 == i || r2 == r1); do { r3 = rndint(0, T_size-1); r3 = neighbors[i][r3]; } while (r3 == i || r3 == r2 || r3 == r1); // Differential mutation for (int j = 0; j < Dimension; j + + ) { new_ind.xreal[j] = parent_pop[r1].xreal[j] + F * (parent_pop[r2].xreal[j] - parent_pop[r3].xreal[j]); // Boundary judgment if (new_ind.xreal[j] < lowerBound[j]) { new_ind.xreal[j] = lowerBound[j]; } if (new_ind.xreal[j] > upperBound[j]) { new_ind.xreal[j] = upperBound[j]; } } // crossover operator int j_rand = rndint(0, Dimension - 1); for (int j = 0; j < Dimension; j + + ) { if (rndreal(0, 1) < CR || j == j_rand) { new_ind.xreal[j] = new_ind.xreal[j]; } else { new_ind.xreal[j] = parent_pop[i].xreal[j]; } } } // Gaussian mutation increases population diversity void gaussian_mutation() { double pr = 1 / Dimension; for (int i = 0; i < Dimension; i + + ) { double rnd; double U1 = URAND, U2 = URAND; double Z = sqrt(-2 * log(U1)) * cos(2 * 3.1415926 * U2); // Normal distribution with mean 0 and variance 1 rnd = new_ind.xreal[i] + 1 / 20 * Z; // Normal distribution with mean 1 and variance 1, 20 double newparm = min(max(rnd, 0.0), 1.0); if (URAND new_ind.xreal[i] = newparm; } } } //Update neighborhood solution void update_neighbors(int i) {<!-- --> // Set the weight vector of 0 to a smaller value for (int j = 0; j < T_size; j + + ) {<!-- --> int k = neighbors[i][j]; for (int t = 0; t < Target; t + + ) {<!-- --> if (weights[k][t] < 1e-6) {<!-- --> weights[k][t] = 0.00001; } } } // Chebyshev aggregation double oldobj, newobj; double max_temp = -0x3f3f3f3f, part[Target]; for (int j = 0; j < T_size; j + + ) {<!-- --> int k = neighbors[i][j]; //New individual aggregate function value for (int t = 0; t < Target; t + + ) {<!-- --> part[t] = abs(new_ind.fvalue[t] - Z[t]); if (max_temp < weights[k][t] * part[t]) {<!-- --> max_temp = weights[k][t] * part[t]; } } newobj = max_temp; max_temp = -0x3f3f3f3f; // Neighborhood individual aggregation function value for (int t = 0; t < Target; t + + ) {<!-- --> part[t] = abs(parent_pop[k].fvalue[t] - Z[t]); if (max_temp < weights[k][t] * part[t]) {<!-- --> max_temp = weights[k][t] * part[t]; } } oldobj = max_temp; if (newobj<oldobj) {<!-- --> parent_pop[k] = new_ind; } } } void moead_method() { for (int i = 0; i < pop_size; i + + ) { evolution(i); // Differential evolution gaussian_mutation(); // Gaussian mutation evaluate_ind( & amp;new_ind); // Update fitness // Update Z for (int j = 0; j < Target; j + + ) { if (new_ind.fvalue[j] < Z[j]) { Z[j] = new_ind.fvalue[j]; } } update_neighbors(i); } } int main() { srand((unsigned)time(NULL)); \t init_weights(); init_neighbors(); init_variables(); init_pop_uniform(); evaluate_pop(parent_pop, pop_size); get_Z(parent_pop); gen_no = 1; while (evaluations <= max_evaluations & amp; & amp; gen_no <= 500) { moead_method(); cout << "Current iteration number:" << gen_no << " " << "Current solution:" << endl; for (int i = 0; i < pop_size; i + + ) { cout << "F1: " << parent_pop[i].fvalue[0] << " F2: " << parent_pop[i].fvalue[1] << endl; } gen_no + + ; } return 0; }
Summary
The code can run perfectly. I didn't explain the principle of the MOEA\D algorithm very clearly, but the code logic I wrote is still very clear. Regarding the principle, you can read the original paper and other blogs, but you can use mine for the code. .