关于遗传算法(解决TSP问题

syan 发布于 2020-06-05 3846 次阅读


人工智能导论杀我

敢信?老师在跟我们讲了遗传之后,布置了遗传算法的大作业。

只讲遗传没讲算法,你们敢信??你们敢信??

硬着头皮冲。。。

题目

是的这是老师给我们的资料。。建议跳过

一、实验目的:

熟悉和掌握遗传算法的原理、流程和编码策略,并利用遗传求解函数优化问题,理解求解TSP问题的流程并测试主要参数对结果的影响。

二、实验原理:

旅行商问题,即TSP问题(Traveling Salesman Problem)是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路经的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。TSP问题是一个组合优化问题。该问题可以被证明具有NPC计算复杂性。因此,任何能使该问题的求解得以简化的方法,都将受到高度的评价和关注。

遗传算法的基本思想正是基于模仿生物界遗传学的遗传过程。它把问题的参数用基因代表,把问题的解用染色体代表(在计算机里用二进制码表示),从而得到一个由具有不同染色体的个体组成的群体。这个群体在问题特定的环境里生存竞争,适者有最好的机会生存和产生后代。后代随机化地继承了父代的最好特征,并也在生存环境的控制支配下继续这一过程。群体的染色体都将逐渐适应环境,不断进化,最后收敛到一族最适应环境的类似个体,即得到问题最优的解。要求利用遗传算法求解TSP问题的最短路径。

三、实验内容:

1、参考实验系统给出的遗传算法核心代码,用遗传算法求解TSP的优化问题,分析遗传算法求解不同规模TSP问题的算法性能。

2、对于同一个TSP问题,分析种群规模、交叉概率和变异概率对算法结果的影响。

3、增加1种变异策略和1种个体选择概率分配策略,比较求解同一TSP问题时不同变异策略及不同个体选择分配策略对算法结果的影响。

4、上交源代码。

四、实验报告要求:

1、画出遗传算法求解TSP问题的流程图。

2、 分析遗传算法求解不同规模的TSP问题的算法性能。

3、对于同一个TSP问题,分析种群规模、交叉概率和变异概率对算法结果的影响。

4、增加1种变异策略和1种个体选择概率分配策略,比较求解同一TSP问题时不同变异策略及不同个体选择分配策略对算法结果的影响。

遗传算法简述

现学现卖的,请别计较太多(扶额

遗传算法(GA),即模拟生物遗传进化构建的智能算法,通过将解作为种群中的个体,解空间视为种群,通过繁殖/变异/优胜劣汰等自然选择手段,对解空间进行约束,从而培养最优个体的算法。

即,通过对个体优劣性的评定,通过个体的淘汰和繁殖变异更新种群(是否可以加入自然死亡呢?

算法的基本流程被概述为这样一张图

img

再这么的,我们试着为生物概念与遗传算法建立一个对应表(参考

生物遗传概念遗传算法概念
个体
染色体解的编码
基因解编码中的结构化信息
碱基解编码中可存在的元素
适应性设计评价函数的值
种群生成或给定的一组解
繁殖通过解编码的重构生成新解
变异解编码部分发生顺应变化规则的改变
适者生存算法迭代过程中仅保留优质解
返祖指求解过程中得到解的爸爸(?)

让我们想想,如果遗传算法是原始社会的话,那么是否可以构建人类遗传算法

让我想想能plug点啥,作为DLC

人类社会概念HGA概念(?
单身率淘汰的缓和概念,单身过久导致死亡淘汰
多人运动交叉兼容多个体(好像不太对劲
死亡年龄/死亡率存活一定轮后随机死亡
结婚繁殖前提,优秀基因马太效应,其他人没差
近似价值个体优先与价值相似的个体喜结连理
相亲从池子里随机抽取个体,差不多就在一起吧
······回头再想,先干正事

问题建模

描述一下问题

TSP问题(Traveling Salesman Problem)。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路经的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。TSP问题是一个组合优化问题。该问题可以被证明具有NPC计算复杂性。因此,任何能使该问题的求解得以简化的方法,都将受到高度的评价和关注。

求解TSP问题的基本思路即是遍历所有顺序,然后再遍历形成的解集中寻找最优解。因此整个问题的复杂度是随着节点个数增加随阶乘增长的。因此通过传统方法难以计算。而我们所希望的是通过模拟生物遗传进化的方法,找到难以解的全局最优解的近似。

我们试着探索如何将TSP问题套进遗传算法的框架中

首先,我们可以考虑概念的对应:

遗传概念遗传算法TSP参数
个体优劣评优函数路径权和
基因构成数据字串遍历顺序

(我擦,说实话我之前以为比这还要困难些的欸)

那么,也许我们可以将TSP问题的一个解,即种群中的一个个体,表示为遍历地图顺序的字符串

TSP

从wiki上捞了一张典型的图片,我们取一个解为[A, B, C, D]罢,那么他的个体优劣程度,即定义为20+30+12=62,即边权之和。那么我们继续去另一个个体[A, B, D, C],她(并没有歧视男同性恋朋友们的意思)则有边权20+34+12=66。

那么很显然优胜劣汰就是将这位可怜的姑娘del了

(剩下的男孩子们注定孤独一生)

停,不插科打诨了

一个很严肃的问题,交叉与变异应该怎么实现,如果仅仅是对染色体进行交叉互换的话,毫无疑问是难以满足路径约束的,但我们要怎么克服这个问题呢。。。陷入沉思;

变异

先谈相对简单的变异问题,我准备将其表示为随机碱基在其邻域(可设定范围罢)内的前后位移,这样可以约束防止离谱结果发生,同时可以作为逃离次优的根据。感觉实战效果会较之交换的方案更好。

交叉

由于自己创造力是在有限,这里俺参考了CSDN上的文章遗传算法中几种交叉算子小结

基于路径不重复的约束,主要可以做的有几种方法

  1. 为交换的基因创建映射关系,即Partial-Mapped Crossover (PMX)算子,并通过映射关系修改原有算子,从而使节点不重复出现 image-20200605181826213
  2. Order Crossover (OX)算子,依据父本基因构建子代基因部分,然后用母本基因去除子代已有基因后的序列进行填充 image-20200605181843152
  3. Position-based Crossover (PBX)Order-Based Crossover (OBX)算子,不用决定连续位点,不过思路都和OX算法类似(自行查阅
  4. 另外还有我最心水的Cycle Crossover (CX)算子 寻找父代基因的环,环外基因的全局互换并不会影响基因结构 image-20200605181908899

结果这么看下来,交叉未必有我们想象得那么复杂?

代码构建

那么让我们来考虑一下,要如何通过代码的形式构建遗传算法

个体构建

首先是考虑构建个体,及基因组,从最原始的世代而言,通常都是随机生成的序列。同时我们也不考虑较为复杂的基因结构,创建一个原始的基因类

class Unit {
   public:
    vector<int> Gene;
    size_t Gene_length;
};

基础构造

同时,给出最简单的随机构造算法

Unit(const size_t L, const bool init_ = true) : Gene_length(L) {
    if (init_) randperm();
}
//生成随机基因
void randperm() {
    vector<int> serial_num;
    //生成序列,此处从1开始,视后续情况可改为从0开始
    for (int i = 1; i <= Gene_length; ++i) serial_num.push_back(i);
    //激情打乱
    for (int i = Gene_length; i > 0; i--) {
        int index = u(gen) % i;
        Gene.push_back(serial_num[index]);
        serial_num.erase(serial_num.begin() + index);
    }
}

繁殖交叉

// Cycle Crossover 交叉
Unit& CX_SIMPLE(const Unit& b) {
    Unit* result = new Unit(Gene_length, false);
    int nb = u(gen);  //锁定随机碱基(位于基因1上
    vector<int> temp;
    while (count(temp.begin(), temp.end(), nb) == 0) {
        //查找碱基在基因1上的位置
        int iter = findPosVector(Gene, nb);
        //储存当前碱基
        temp.push_back(nb);
        //指向下一碱基
        nb = b.Gene[iter];
    }
    //对不在环内的基因进行交叉操作
    for (size_t i = 0; i < Gene_length; i++)
        if (count(temp.begin(), temp.end(), Gene[i]) == 0)
            result->Gene.push_back(Gene[i]);
    else
        result->Gene.push_back(b.Gene[i]);
    return *result;
}

// 用于查找元素位置的函数
int findPosVector(vector<int> input, const int number) {
    vector<int>::iterator iter = std::find(
        input.begin(), input.end(), number);  //返回的是一个迭代器指针
    if (iter == input.end()) {
        return -1;
    } else {
        return std::distance(input.begin(), iter);
    }
}

遗传变异

// 偷懒用的变异
Unit& VG_SIMPLE_BACK() {
    int index = u(gen);
    Gene.push_back(Gene[index]);
    Gene.erase(Gene.begin() + index);
    return *this;
}

种群构建

我们首先考虑种群需要有什么属性

首先,种群是个体的集合,也就是创建一个数组存放种群中所有的个体,其次便是种群容量,即确定种群中的个体数目,即种群容量,其次是交叉与变异的概率,从资料看来通常被设置为1和0.1。

同时,个体的价值也是由种群需要来定义的,我们在种群中需要建立价值判定函数,以及迭代进化函数

这是我构想的框架

class Group {
   private:
    //内部参数
    //个体集合
    vector<Unit> Indiv;
    //价值评断
    vector<float> Values;
    //创建价值对,方便排序
    vector<pair<Unit, float>> unit_value;

    //优秀子代
    Unit* best_u;
    float best_v;

    //外部参数
    //基因长度(当前问题为城市数)
    size_t Gene_length;
    //种群容量
    size_t Group_size;
    //迭代次数
    size_t Iter_times;
    //交叉概率
    float Cross_rate;
    //变异概率
    float Vg_rate;

    //问题参数
    vector<vector<int>> param;

   public:
    Group(size_t Gene_l, size_t Group_s, size_t Iter_t,
          vector<vector<int>> param_t, float Vg_r = 0.1, float Cross_r = 1)
        : Gene_length(Gene_l),
          Group_size(Group_s),
          Iter_times(Iter_t),
          Cross_rate(Cross_r),
          Vg_rate(Vg_r) {
        best_u = new Unit(Gene_length);
        //填充种群容量个长度为指定的个体
        for (size_t i = 0; i < Group_size; i++) {
            Unit unit(Gene_length);
            Indiv.push_back(unit);
        }
        param = param_t;
        Compute_Values();
        for (size_t i = 0; i < Indiv.size(); i++) {
            pair<Unit, float> temp = make_pair(Indiv[i], Values[i]);
            unit_value.push_back(temp);
        }
        sort(unit_value.begin(), unit_value.end(), value_cmp);
        best_u = new Unit(unit_value[0].first);
        best_v = unit_value[0].second;
    }
    void Compute_Values() {}
    void Evolution() {}
    void Run() {}
};

价值计算

对种群内个体价值的计算是相对简单的,直接丢函数啦

    void Compute_Values() {
        for (size_t i = 0; i < Indiv.size(); i++) {
            float Value = 0;  //当前个体价值
            //检索参数表中两城市的距离,并累计加在个体价值上(价值越低的个体越好(?
            for (size_t j = 0; j < Gene_length - 1; j++)
                Value += param[Indiv[i].Gene[j] - 1][Indiv[i].Gene[j + 1] - 1];
            Value +=
                param[Indiv[i].Gene[Gene_length - 1] - 1][Indiv[i].Gene[0] - 1];

            Values.push_back(Value);
        }
    }

种群进化

那么接下来,我们考虑种群的进化

一个种群的进化拥有哪些指标呢,这是个好问题

先不考虑基因的寿命,仅仅考虑种群的隔代遗传

首先我们需要的是对种群进行一代繁殖,在这里我们可能会考虑价值较高(即values较低)的个体更高优先级同相似的个体进行繁殖,但是这也决定了种群是否需要经过两轮排序。由此经过交叉及变异,我们便能获得第一代子代。并将子代集合与父代集合进行并集处理,并淘汰价值较低,且超出了种群容量的个体。

给出我的实现?

    static bool value_cmp(const pair<Unit, float>& a,
                          const pair<Unit, float>& b) {
        return a.second < b.second;  //根据second的值升序排序
    }
    void Evolution() {
        //考虑婚嫁和繁殖
        bool married[Group_size] = {0};

        //只有前一半的人能主动婚嫁,假设只能找低于自己的(x
        //等会儿改成正态分布的数据
        for (size_t i = 0; i < Group_size / 2; i++) {
            //讨老婆(?
            // int wife = (i + rand() % (Group_size / 2)) % Group_size;
            //天命
            // int wife = rand() % Group_size;
            int wife = abs(int(dis(gen) * Group_size / 2)) % Group_size;
            //如果已婚
            if (married[wife] || married[i]) continue;

            //结婚登记(挪到前面来,避免了未婚先孕
            married[i] = true;
            married[wife] = true;

            //繁殖,以及是否变异
            if (rand() % 100 > Vg_rate * 100)
                Indiv.push_back(Indiv[i].CX_SIMPLE(Indiv[wife]));
            else {
                Indiv.push_back(Indiv[i].CX_SIMPLE(Indiv[wife]));
                Indiv[i].VG_SIMPLE_BACK();
            }
        }

        //建立价值等级表单
        Values.clear();
        unit_value.clear();
        Compute_Values();
        for (size_t i = 0; i < Indiv.size(); i++) {
            pair<Unit, float> temp = make_pair(Indiv[i], Values[i]);
            unit_value.push_back(temp);
        }
        sort(unit_value.begin(), unit_value.end(), value_cmp);
        //建立新生代数据库,落后于时代的勇士们依旧望着你
        Values.clear();
        Indiv.clear();
        for (size_t i = 0; i < Group_size; i++) {
            Values.push_back(unit_value[i].second);
            Indiv.push_back(unit_value[i].first);
        }
        if (best_v > unit_value[0].second) {
            best_u = new Unit(unit_value[0].first);
            best_v = unit_value[0].second;
        }
    }

运行/训练

    void Run() {
        int t = Iter_times;
        while (t--) Evolution();
        best_u->print();
        cout << "best has value:" << best_v << endl;
    }

运行测试

距离矩阵是csdn上扒来的hhhh,之后准备在更棒的数据上跑个爽

int main() {
    int test_times = 10;

    const int city_num = 10;   //城市数量
    const int unit_num = 300;  //群体规模
    const float vg_pro = 0.1;  //变异概率
    const int gen_maxn = 500;  //最大迭代数
    //城市距离表,作为全局参数
    int length_table[10][10] = {

        {0, 1, 1272, 2567, 1653, 2097, 1425, 1177, 3947, 1},

        {1, 0, 1, 2511, 1633, 2077, 1369, 1157, 3961, 1518},

        {1272, 1, 0, 1, 380, 1490, 821, 856, 3660, 385},

        {2567, 2511, 1, 0, 1, 2335, 1562, 2165, 3995, 933},

        {1653, 1633, 380, 1, 0, 1, 1041, 1135, 3870, 456},

        {2097, 2077, 1490, 2335, 1, 0, 1, 920, 2170, 1920},

        {1425, 1369, 821, 1562, 1041, 1, 0, 1, 4290, 626},

        {1177, 1157, 856, 2165, 1135, 920, 1, 0, 1, 1290},

        {3947, 3961, 3660, 3995, 3870, 2170, 4290, 1, 0, 1},

        {1, 1518, 385, 993, 456, 1920, 626, 1290, 1, 0}};

    vector<vector<int>> vt;
    int row = city_num;
    int column = city_num;

    //把城市距离邻接矩阵写入vector
    for (int i = 0; i < row; i++) {
        vector<int> tmp;
        for (int j = 0; j < column; j++) tmp.push_back(length_table[i][j]);
        vt.push_back(tmp);
    }

    //测试模型
    vector<Group> rua;
    int t = test_times;

    while (t--) rua.push_back(Group(city_num, unit_num, gen_maxn, vt, vg_pro));

    for (size_t i = 0; i < test_times; i++) rua[i].Run();

    system("pause");
    return 0;
}

我得去交作业了(?