相信学过算法的童鞋都听说过一个很经典的问题:TSP问题,这个问题是NP问题,无法在多项式时间内进行求解。当问题规模较小时,还可以用穷举的方法进行求解,但是当城市一旦变多,穷举的时间将会指数级增加。就算采用启发式搜索,估计也很难求解。

但是这个问题是可以尝试解决的,人工智能给我们提供了强大的武器,也许尽管无法求得全局最优解,但我们也能得到一个很不错的解。最主要的是,我们可以在可以忍耐的时间内得到一个解。下面给出人工智能中对TSP问题求解的几种方法,这也是刘峡壁老师在课堂上反复强调的。

1、使用霍普菲尔德网络求解TSP问题

首先我们要把TSP的图转换为二维矩阵,就像下面这样,这里是一个以城市为元素的矩阵,同时每个元素都对应一个神经网络的神经元:

presto 大规模应用 大规模tsp问题_presto 大规模应用

限制条件是:

  • 每行仅有一个神经元处于活跃状态
  • 每列仅有一个神经元处于活跃状态
  • 对n个城市的问题,需要有n个神经元处于活跃状态

例如针对五个城市,列代表到达某个城市的时间,行代表城市,则可以如下表示:




presto 大规模应用 大规模tsp问题_presto 大规模应用_02




接下来我们就可以按照霍普菲尔德网络求解的一般做法进行处理,我们可以如下定义能量函数:



presto 大规模应用 大规模tsp问题_人工智能_03




然后可以把上面的式子转换为符合二维霍普菲尔德网络的形式:



presto 大规模应用 大规模tsp问题_presto 大规模应用_04




最后我们只需要随机给定初始值,对神经元进行扰动,当网络能量达到最小时会稳定下来,我们就会得到需要的解,下面是别人针对4个城市的一个求解过程:



presto 大规模应用 大规模tsp问题_ci_05





2、使用遗传算法求解TSP问题



遗传算法主要有三个操作,选择,交叉和变异。个人觉得在求解TSP问题时,遗传算法的优势就是能在全局和局部直接达到平衡,它不像穷举算法那样耗费很多时间进行全局搜索,而是能较快确定解的范围,当确定范围以后,它又能对局部进行比较细致的搜索,找到一个比较好的解。


全局探索(Exploration):交叉重组与突变是算法全局探测能力的主要构成要素。

局部探测(Exploitation):对种群个体的选择是算法局部探测能力的主要构成要素。

平衡方法主要通过调节各要素的随机变化的参数实现,较大突变概率具有较强的全局探测能力,较大的选择概率意味着较强局部搜索能力。



主要数据结构:

基因表示方法:城市遍历顺序,如G =[1,3,2],表示遍历顺序为1->3->2->1

class Map{
private:
int **distance;//用于存储城市之间的距离
public:
int Distance(int city_1,city_2);//返回两个城市的距离
}
class Population{
private:
	int Max_number;
	int RanParNum;//每次随机选择的父体个数
	double pro;//变异概率
	int **GenList;//用于存储所有群体基因
	int **RandomParent;	//用于存储随机选择的父母群体
	int **BestTwoParentFromRP;//用于存储从RandomParent中选取的最好的两个个体
	int *NewChild;//用于存储即将进行变异和更新群体的孩子个体
	int *Solution;
public:
	static int GenLen;
	Population(int m=100,int RPN=5,double p=0.8,int i=0,int **GL=NULL,
		int **RP=NULL,int **BTPFRP=NULL,int *NC=NULL,int *S=NULL)
		:Max_number(m),RanParNum(RPN),pro(p),icount(i),
		GenList(GL),RandomParent(RP),
		BestTwoParentFromRP(BTPFRP),NewChild(NC),Solution(S){}//构造参数
	void randominit();//初始化群体及相关参数
	bool Evlote(int MaxTime);
	void GetParents();//选取少量个体
	void SelectBestParent();//从少量个体中选取两个作为父母
	void ReConbination();//基因重组,并保证子个体是合法的
	void Swap();//交换变异
	int Replace();//更新群体
void Display();//输出状态
	int fit(int* person);//计算适应度,这里为对应方案的路程的倒数
};

//主要进化算法:
		for(i=0;i<MaxTime;i++)//进化过程
		{
			GetParents();
			SelectBestParent();
			int *AnotherChild = new int[GenLen+1];//用于暂时存储重组后的一个孩子个体,另一个存储在Population的NewChild中,用于即将进行的个体变异
			ReConbination(&AnotherChild);//重组
			Swap();//变异
			Replace();//更新群体
			for(int i=0;i<=GenLen;i++)//将AnotherChild赋值给NewChild进行个体变异
			{
				NewChild[i]=AnotherChild[i];
			}
			delete AnotherChild;
			Swap();//变异
			Replace();//更新群体
		}


更多内容,请参看这里: https://www.ads.tuwien.ac.at/raidl/tspga/TSPGA.html



3、使用蚁群算法求解TSP问题



对于蚁群算法不想过多说明,具体看这里, Ant colony optimization algorithms  总之你可以小看一只蚂蚁,但不能小看一群蚂蚁,《 哥德尔、艾舍尔、巴赫集异璧之大成》这本奇书中也提到了这一点。



用蚁群求解TSP问题主要是利用蚁周模型,即蚂蚁每完成一次城市的遍历后才更新所有路径上的信息素,求解过程如下:



presto 大规模应用 大规模tsp问题_ci_06




下面是一个四个城市的例子:



presto 大规模应用 大规模tsp问题_presto 大规模应用_07



这里共有两只蚂蚁,一只沿着四条边走,另一只沿着对角线走,它们经过的距离肯定是不一样的,由于蚁周模型中信息素的变化是依据下面这个式子:



presto 大规模应用 大规模tsp问题_人工智能_08



所以前一只蚂蚁通过后在每条边留下的信息素为1/4=0.25 ;后一只蚂蚁为1/4.8≈0.21


经过叠加以后,上下两条边的信息素得到的加强,相比之下对角线的信息素值最小。随着时间推移和信息素的挥发,信息素值越大的路线上走过的蚂蚁越多,而蚂蚁的增多也促进了信息素的增加,相反信息素值越小将会越来越没有蚂蚁走,最终废弃。针对这个例子,最终的结果就是走正方形的四条边。



//主要数据结构:
public class ACO { 
	private Ant[] ants; //蚂蚁
	private int antNum; //蚂蚁数量
	private int cityNum; //城市数量
	private int MAX_GEN; //运行代数
	private float[][] pheromone; //信息素矩阵
	private int[][] distance; //距离矩阵
	private int bestLength; //最佳长度
	private int[] bestTour; //最佳路径
	
	//三个参数
	private float alpha; 
	private float beta;
	private float rho;

 public void init(String filename);//从文件初始化数据
}
//主要算法:
for (int g = 0; g < MAX_GEN; g++) {
	//蚂蚁依据信息素寻找解
	for (int i = 0; i < antNum; i++) {
		for (int j = 1; j < cityNum; j++) {
			ants[i].selectNextCity(pheromone);//选择下一个城市,并从禁忌表去除该城市
		}
		ants[i].getTabu().add(ants[i].getFirstCity());
		//更新最好Tour
		if (ants[i].getTourLength() < bestLength) {
			bestLength = ants[i].getTourLength();
			for (int k = 0; k < cityNum + 1; k++) {
				bestTour[k] = ants[i].getTabu().get(k).intValue();
			}
		}
		//计算此次释放的信息素
		for (int j = 0; j < cityNum; j++) {			ants[i].getDelta()[ants[i].getTabu().get(j).intValue()][ants[i].getTabu().get(j+1).intValue()] = (float) (1./ants[i].getTourLength());
			ants[i].getDelta()[ants[i].getTabu().get(j+1).intValue()][ants[i].getTabu().get(j).intValue()] = (float) (1./ants[i].getTourLength());
		}
	}

	//更新信息素
	   	//信息素挥发  
		for(int i=0;i<cityNum;i++)  
			for(int j=0;j<cityNum;j++)  
				pheromone[i][j]=pheromone[i][j]*(1-rho);  
		//信息素更新  
		for(int i=0;i<cityNum;i++){  
			for(int j=0;j<cityNum;j++){  
				for (int k = 0; k < antNum; k++) {
					pheromone[i][j] += ants[k].getDelta()[i][j];
				} 
			}  
		}  

	//重新初始化蚂蚁
	for(int i=0;i<antNum;i++){  
		ants[i].init(distance, alpha, beta);  
	}  
}


同样这里也存在全局和局部搜索问题


全局探索(Exploration):挥发系数(rho)越大;信息素的更新策略越均匀,收敛越慢,全局探索能力越强。

局部探测(Exploitation):与全局探索对应,挥发系数(rho)越小;信息素的更新策略越不均匀,收敛越快,局部探测能力越强。

 

可以使用一些有效的参数和更新策略选取算法,对多组参数和更新策略进行比较、验证,以获得效果理想的参数和更新策略。

小结:以上的三种方法分别对应于人工智能的三个子领域(神经网络,进化计算,群智能),在这里我们能看到使用多种方法求解某一类问题的妙处,这三种方法中究竟哪种方法最好并没有一个权威的论断,不过显然既然是从不同角度出发肯定都有各自的优缺点,难分伯仲。还是要说,人工智能是很有意思的一个领域,这个领域很宽泛,面极其广阔,包含了太多的东西,包括认知科学,机器学习,自然语言处理,机器人学,计算机博弈,自动定理证明,模式识别,计算机视觉等等等等,希望更多的人能涌入这个领域,AI前景无限美好。