레이블이 세일즈맨인 게시물을 표시합니다. 모든 게시물 표시
레이블이 세일즈맨인 게시물을 표시합니다. 모든 게시물 표시

GA - 장사꾼여행문제(TSP) [5] - 실행 결과

9. 결과

앞에서 설명한 식으로 150개 도시를 여행하는 최적해를 찾아보았습니다. 이때 Tournament법에 사용하는 상수 T의 값을 0.7부터 1.0까지 바꾸어 가며 10000세대동안 돌려보았습니다.
그 결과는 다음과 같습니다.



여기서 볼 수 있듯이 T값이 1에 가까울수록 수렴속도가 빨라짐을 알 수 있습니다.



이것은 마지막 10000세대에 달성된 최적값을 모아놓은 도표입니다. T값이 약 0.85~1일 경우 거의 비슷한 값이 나타나지만* 그 이후부터는 최적값이 급격히 올라가는 것이 보입니다.**

다음은 T값이 0.99일 때와 0.975일 때 10000세대만에 나온 150개 도시여행 근사해입니다.



결과는 T값이 0.975일 때가 더 좋은 근사값임을 알 수 있습니다.*

* T값이 0.85 이상일 경우에, 10000세대에서의 최적경로가 T값에 따라 약간의 차이가 있습니다. 그뿐 아니라 같은 T값을 사용하는 경우에도 여러번 반복하면 최적경로가 달라지는 경우가 있습니다. 이것은 초기 랜덤함수의 상태(srand())에 의한 것으로 생각됩니다.(확인하고 싶지만 한번 10000세대 돌리는데 2,3시간은 족히 걸리는지라..ㅡㅡ) 그러므로 좋은 근사값을 얻기 위해서는 0.85 이상의 T값에서 여러번 돌려보고 가장 좋은 값을 사용하는 것도 방법일 수 있습니다.
** T값이 0.85 이하일 경우에도 그래프에서 보면 감소세가 확실합니다. 그러므로 시간을 충분히 주면 근사값을 발견할 가능성도 없지 않습니다(이것 역시 시간의 압박 때문에...ㅡㅡ)

GA - 장사꾼여행문제(TSP) [4] - 돌연변이

8. 돌연변이

교차와 마찬가지로 돌연변이 역시 유전자 최소단위(비트-bit)를 임의로 바꾸는 일반적인 돌연변이를 사용할 수 없습니다. 모든 도시를 한번만 방문할 수 있다는 제한 때문이죠.
여기서 사용한 돌연변이는 두가지입니다.
만약 유전자가 7385920416이라면

㉠ 임의의 경로 뒤집기
임의의 도시와 돌연변이시킬 길이를 임의로 정합니다. 2번도시와 길이 4가 정해졌다면
먼저 2번도시를 첫머리로 옮깁니다.

2041673859

그 후에는 앞에서 4개 길이를 뒤집습니다. 즉 2->0->4->1을 1->4->0->2로 바꿉니다.

1402673859

㉡ 임의의 경로 바꾸기
임의의 도시와 돌연변이시킬 범위의 앞뒤를 임의로 정합니다. 8번도시와 3~7이 정해졌다면, 먼저 8번도시를 첫머리로 재배열합니다.

8592041673

다음에는 0~2와 3~7까지의 경로를 뒤바꿉니다.

2041859673

㉢ 돌연변이 확률
꼬마자동차의 경우처럼 일반적인 돌연변이는 비트 하나하나에 대해 돌연변이 확률을 계산합니다. 그러므로 비트 하나하나에 대한 돌연변이 확률이 작더라도 개체 하나에 대한 돌연변이는 생각보다 크게 나오는 경우가 많습니다.
이 문제의 경우에는 비트 하나하나에 대한 돌연변이가 아니라 개체 전체에 대한 돌연변이를 채택했기에 돌연변이 확률은 좀 높이는 것이 좋습니다.

procedure Mutantation(Salesman s)
.. if Random(0, 10) == 0 then // 10% 확률로 돌연변이
..... if Random(0, 2) == 0 then // 경로 뒤집기
........ startcity := Random(0, 150)
........ Rolling(s.gene, startcity)
........ startpnt := 0
........ endpnt := Random(1, 150 / 2 + 1)
........ while endpnt > startpnt do
........... // startpnt도시와 endpnt도시를 바꿈
........... tmp := s.gene[startpnt]
........... s.gene[startpnt] := s.gene[endpnt]
........... s.gene[endpnt] := tmp
........... startpnt := startpnt + 1
........... endpnt := endpnt - 1
........ end
..... else // 경로 바꾸기
........ startcity := Random(0, 150)
........ Rolling(s.gene, startcity)
........ startpnt := Random(1, 150 / 2 + 1)
........ endpnt := Random(startpnt + 1, startpnt + 150 / 3 + 1)
........ Salesman tmp
........ tmp := s // 복사본을 만들어 놓음
........ sub := 0
........ for k := startpnt, endpnt do // startpnt~endpnt까지 앞에 복사
........... s.gene[sub] := tmp.gene[k]
........... sub := sub + 1
........ end
........ for k := 0, startpnt do // 0~startpnt까지 그 뒤에 복사
........... s.gene[sub] := tmp.gene[k]
........... sub := sub + 1
........ end
..... end
.. end
end

GA - 장사꾼여행문제(TSP) [3] - 교차

6. 필요한 프로시져
앞에서 말했듯 1234567890과 4567890123, 5432109876 같은 유전자들은 모두 동일한 유전자입니다. 이렇게 유전자를 변형시키는 프로시저가 필요합니다. 자세한 알고리즘은 생략하겠습니다.
Rolling(Gene gene, CityID city)
gene에서 도시들의 순서는 변화시키지 않고 특정 도시를 가장 앞으로 끌어오는 프로시져입니다.
즉 gene이 0123456789일때 Rolling(gene, 7)을 하면 gene이 7890123456으로 바뀝니다.
Flip(Gene gene)
gene에서 첫째 도시는 건드리지 않고 전체 도시 순서를 역순으로 바꿉니다. 즉 gene이 7890123456일때 을 Flip(gene)하면 gene은 7654321098이 됩니다.

7. 교차
잘 아시다시피 교차는 특정한 유전자를 교환하는 작업입니다. 그런데 이 문제에서와 같이 유전자에 제약이 있는 경우에는 함부로 유전자를 교환할 수 없습니다.
한가지 보기로 다음과 같은 두 유전자가 있습니다.

A : 0219746835
B : 7150382946
이 유전자를 1점교배로 교환하면

A : 0219382946
B :
7150746835

이런 유전자가 생깁니다. A는 2, 9번 도시를 두번씩, B는 7, 5번 도시를 두번씩 방문하는군요. 즉 일반적인 교차를 사용하면 제약을 만족하는 유전자를 얻을 수가 없습니다. 그러므로 이 문제에서는 다음과 같은 방식으로 교차를 시킵니다.

㉠ 먼저 교차영역을 정해야 합니다. 임의의 도시를 정해 양쪽 모두 Rolling프로시저를 호출합니다. 만약 4번도시가 선택되었다면

A : 4683502197
B :
4671503829

와 같이 변환시킵니다.
이후에는 앞쪽부터 살펴가면서 경로가 나뉘어졌다가 다시 합류하는 부분을 찾습니다.

A : 4683502197
B :
4671503829

두 유전자의 경로는 6번 도시 이후부터 나뉘기 시작하고 5번 도시에서 다시 일치합니다. 그러므로 이 경우에는 4번도시부터 5번도시까지의 경로를 교차영역으로 삼습니다.
만약 교차영역을 찾을 수가 없다면 이 두 유전자는 교차를 포기하게 됩니다.

㉡ A'와 B'를 만들어 일단 상대방의 교차영역을 복사합니다.

A' :
46715
B' : 46835

㉢ 다음에는 유전자의 나머지 영역을 완성할 차례입니다.
A와 B는 교차영역의 끝(5)을 첫머리로 돌립니다.

A :
5021974683
B : 5038294671

다음에는 A'와 B'에 A와 B를 연결하는데, A'와 B'에 이미 들어가 있는 도시는 제외하면서 연결합니다.

A' :
4671502983
B' : 4683502971

procedure CrossOver(Salesman A, Salesman B)
.. if Random(0, 100) > 50 then // 일정확률로 유전자 하나를 뒤집음
..... Flip(A.gene)
.. end

.. crosspoint := Random(0, 150) // 교차 시작할 도시 정해서 첫머리로
.. Rolling(A.gene, crosspoint)
.. Rolling(B.gene, crosspoint)

.. // 달라지기 시작하는 위치 찾기
.. sub := 0
.. while A.gene[sub] == B.gene[sub] do
..... sub := sub + 1
.. end
.. if sub >= 150 then // 두 유전자가 동일한 유전자
..... return // 교차 포기
.. end
.. // 교차 끝날 위치 찾기
.. while A.gene[sub] == B.gene[sub] do
..... sub := sub + 1
.. end
.. if sub >= 150 then // 두 유전자가 전혀 다른 유전자
..... return // 교차 포기
.. end
.. crossend := sub

.. Salesman AA, BB // 새로운 유전자를 만들 틀
.. // 상대방의 앞쪽부분을 복사
.. for sub := 0, crossend do
..... AA.gene[sub] := b.gene[sub]
..... BB.gene[sub] := a.gene[sub]
.. end
.. // 나머지 부분을 정리하기 위해
.. Rolling(A.gene, A.gene[crossend]) // 교차의 끝부분을 첫째도시로 재설정

.. // AA에 A를 추가
.. insertloc := crossend
.. for sub := 0, 150 do
..... // A.gene[sub]가 AA.gene에 포함되어 있는지를 확인하기 위해
..... for ct := 0, insertloc do
........ if AA.gene[ct] == A.gene[sub] then
........... break for loop
........ end
..... end
..... if ct >= insertloc then // 정상적 루프 탈출 - 중복되는 것이 없었음
........ AA.gene[insertloc] := A.gene[sub]
........
insertloc := insertloc + 1
..... end
.. end

.. // BB에 B를 추가
.. // 생략.. AA건과 동일

.. A := AA
.. B := BB
end


TSP 문제를 해결하기 위한 유전자 설계법이 여러가지인 것처럼, 여기서 사용한 경로표현법 유전자를 교차하는 방법도 여러가지입니다. 여기서 사용한 교차법은 OX법(어떤 말의 약자인지는 모르겠습니다. Order Xover가 아닐까 추측할 뿐입니다)을 약간 수정한 것입니다. OX법은 원리는 같지만, 교차 시작점과 끝점을 찾는 것이 아니라 그냥 랜덤하게 결정해서 교차시키는 방법입니다. 하지만 제가 테스트한 결과로는 전혀 수렴이 안되더군요. 물론 제가 OX법을 제대로 이해 못해서일 수도 있습니다만...^^

GA - 장사꾼여행문제(TSP) [2] - 유전자 초기화, 재생산

2. 유전자 초기화

일반적으로 유전자를 초기화하기 위해서는 유전자의 각 비트에 랜덤값을 넣으면 되지만, 이 경우에는 앞에서 말한 유전자의 제약 때문에 무조건 랜덤값을 넣을 수는 없습니다. 만약 도시 10개짜리 보기에서 유전자에 랜덤값을 넣어 5042852301란 유전자를 만들었다면, 이 유전자는 0번, 2번, 5번 도시는 두번씩, 6번, 7번, 9번 도시는 방문하지 않는 잘못된 유전자가 만들어집니다.

그러므로 이 경우에는 카드섞기 방법으로 초기화를 해야 합니다.


procedure InitGene(Gene gene)
.. // 0~149까지의 도시를 하나씩 넣음
.. for bit := 0, 150 do
..... gene[bit] := bit
.. end
.. //배열을 뒤섞음
.. for bit := 0, 150 do
..... exchange := Random(0, 150)
..... temp := gene[bit]
..... gene[bit] := gene[exchange]
..... gene[exchange] := temp
.. end
end

이와 같이 하면 150개의 도시에 대해 중복이나 결실 없는 무작위순열을 얻을 수 있습니다.


3. 적응도테스트 및 적응도

적응도를 계산하는 자세한 알고리즘은 생략하겠습니다. 유전자에 그려진 도시순서대로 한바퀴 돌며 이동하는 도시와 도시 사이의 거리를 합산하면 됩니다.

다만, 이경우에는 도시사이의 거리와 적응도는 역관계입니다. 거리가 길수록 적응도가 낮아지기 때문입니다. 여기서는 단순하게 적응도 = -(거리의 총합)으로 정의했습니다. 이렇게 하면 모든 적응도는 음수가 되긴 합니다만, 룰렛법을 사용할 수 없다는 점 이외에는 아무런 차이가 없습니다. 혹시 음수적응도가 마음에 안든다면, 가장 긴 이동거리를 가진 도시를 찾아서 (가장 긴 이동거리) - (거리의 총합) 으로 해도 됩니다. 이렇게 하면 적응도가 가장 떨어지는 개체가 0, 나머지는 양수의 적응도를 가질 수 있습니다.


4. 재생산대상 선택

다음 단계는 각자의 적응도에 따라 재생산할 대상을 고를 차례입니다. 윗 항에서처럼 적응도가 0보다 작으니 룰렛법을 사용할 수는 없지만 앞에서 사용했던 재계산법이나 순위법을 사용할 수는 있습니다.
하지만 이 문제에서는 새로운 선택법 - Tournament법을 사용하겠습니다.

토너먼트법이란, 일단 0과 1 사이의 수 T를 정합니다.
재생산 대상을 선택하기 위해 먼저 두개의 개체를 랜덤하게 정합니다. 그 둘 중 적합도가 높은 것을 Shigh, 적합도가 낮은 것을 Slow라 합니다.
다시한번 0과 1 사이의 랜덤값 r을 발생시킵니다. 그리고 r이 T보다 작다면 Shigh를, r이 T보다 크다면 Slow를 선택하는 방법입니다.

말로 해서 복잡한데, 알고리즘은 다음과 같습니다.

function SelectParent(double T)
.. // 임의의 두 개체 선택 후 Shigh에 적합도가 높은것, Slow에 적합도가 낮은 것을 나누어 넣음
.. A := Random(0, 150)
.. B := Random(0, 150)
.. if Salesman[A].fitness > Salesman[B].fitness then
..... Shigh := A
..... Slow := B
.. else
..... Shigh := B
..... Slow := A
.. end
.. // 다시한번 랜덤값 만듦
.. r = Random(0.0, 1.0) // 0~1 사이의 실수형 랜덤값 얻음
.. if T > r then
..... return
Salesman[Shigh]
.. else
..... return Salesman[Slow]
end

이때 T는 0.5 이상의 값을 선택해야 합니다. 만약 0.5 이하라면 오히려 적합도가 낮은 것이 선택될 확률이 커지죠.
일반적으로 T가 클수록 선택압이 높아져 빨리 수렴하는 대신 해의 품질이 떨어지는 경향이 있습니다. 반대로 T가 작다면 수렴속도가 느려지죠.
사실 토너먼트법은 그다지 선택압이 높은 방법이 아닙니다. 개체군의 크기(전체 개체군 수)를 N이라 한다면, 최고적합도를 가진 개체가 선택될 확률은 2(1/N * T)에 불과합니다.

여기서는 단순하게 2개의 개체를 선택했지만, 2n개의 개체를 선택한 후 토너먼트식 대결을 계속하여 최후의 승자를 선택하는 방법도 있습니다.


5. Elitism

엘리트주의란, 최대 적합도를 가진 개체를 보호하는 알고리즘입니다. 일반적인 재생산 과정은 두 개체의 유전자를 섞어 새로운 유전자를 만드는 것이므로, 최대적합도의 유전자가 선택되더라도 그 유전자가 다음 세대에 그대로 복제되지는 않습니다. 앞에서의 꼬마자동차 보기에서와 같이, 영웅이 탄생하더라도 다음 대에서는 일단 사라졌다가 영웅의 부분유전자들이 어느정도 증식한 후에야 다시 나타납니다. 엘리티즘은 이러한 현상을 막기 위해 최대적합도를 가진 개체를 다음세대에 복제하는 것을 말합니다.
엘리티즘을 잘못 사용하면 계속 복제되는 엘리트가 계속 재생산대상으로 선택되어 다양성이 훼손되는 결과가 생길 수 있습니다. 하지만 이 TSP에서는 토너먼트법이 워낙 선택압이 낮은 관계로 엘리티즘을 추가했습니다.

한 세대의 개체수는 30001개로 정의했습니다. 그중 하나는 전세대의 가장 우수한 개체를 복제하고, 나머지 30000개는 토너먼트방식으로 교차와 돌연변이를 통해 만듦니다.

엘리티즘을 포함한다면 유전자 알고리즘의 순서도에서 다음 부분이 바뀌어야 합니다.

..... T := 0.9 // 이 값은 변화 가능..... declear Creature nextgeneration[GenerationNo] // 다음세대 creature를 만들 버퍼
.....
store maxfitness to nextgeneration//전세대에서 최대적합도를 가진 개체를 그대로 넣음
..... while(not fill nextgeneration) do // nextgeneration이 채워질 동안 반복
........ Creature a, b
........ while 1 do........... a := SelectParent(T) // 적응도에 의해 재생산할 creature 선택
........... b := SelectParent(T) // a, b에 선택된 것의 복사본 만듦
........... if a != b then
// 다른 Creature간의 유전자 혼합을 위해
.............. break........... end
........ end
........ call CrossOver(a, b) // a, b의 유전자를 섞음
........ call Mutantation(a) // a, b에 적당한 돌연변이 가함
........ call Mutantation(b)
........ store a, b to nextgeneration // a, b를 다음 세대 버퍼에 넣음
..... end
.....
creature := nextgeneration // 새로 만들어진 creature들로 계속 실행

GA - 장사꾼여행문제(TSP) [1] - 유전자 설계

TSP(Travelling Salesman Problem)는 잘 알려진 대로, 여러개의 도시를 최단거리로 순회하는 방법을 찾는 대표적인 NP문제입니다. 쉽게 말하면 '다항식적으로 문제를 풀 수 없다' -> '무식하게 모든 경우의 수를 계산해서 풀어야 한다.'는 것이죠.

이 문제의 어려운 점은 도시가 늘어날수록 가능한 조합이 엄청난 속도로 증가한다는 것입니다. 이를테면 5개의 도시일 경우 5! = 120가지 조합을 계산해서 최소값을 찾으면 되지만, 10개의 도시라면 10! = 3628800가지 조합, 20개라면 20! = 2432902008176640000가지 조합을 따져야 합니다. 100개의 도시라면? 9로 시작하는 158자리 길이의 숫자만큼의 조합을 계산해야 합니다. 그야말로 '빅뱅부터 우주의 종말까지' 계산해도 끝나지 않을 계산량이죠.
그래서 이런 문제는, '최소값'을 찾는 것은 포기하고 '근사값'을 찾는 여러가지 알고리즘이 개발되어 있습니다.

여기서는 장사꾼이 최소 이동거리로 150개 도시를 지나는 문제를 유전자 알고리즘을 사용해서 근사값을 찾는 방법을 알아보겠습니다.


만약 '무식한 방법'으로 최선의 경로를 찾기 위해서는 150!개 경우의 수를 다 계산해야 합니다. 150!의 값은 다음과 같은 263자리 숫자입니다.
5713383956445854590478932865261054003189553578601126418254
83758331798291248453983931265744886753111453771078787468542
0416266625019868450446635594919592206657494259209573577892
9325357290444962472405416790722118445437122269675520000000
000000000000000000000000000000


1. 유전자 설계
TSP를 위한 가장 간단한 유전자는 돌아다닐 도시를 차례로 나열하는 것입니다. 만약 0~9까지 10개의 도시를 이동한다면, 유전자 9421850637은 9->4->2->1->8->5->0->6->3->7->9번 도시의 순서로 돌아다닌다는 의미입니다. 그러므로 1850637942나 0637942185, 3794218506 등도 동일한 유전자라고 할 수 있습니다. 또한 반대로 도는 2497360581 역시도 동일한 유전자입니다

역순유전자를 동일한 것으로 처리하기 위해서는, 모든 도시에 대해 A->B 이동시의 비용과 B->A 이동시의 비용이 동일해야 합니다. 만약 역방향으로 이동할 때의 비용이 차이난다면(바람이나 해류가 존재할 때 등) 역방향유전자를 동일한 것으로 처리할 수 없습니다.

이렇게 만드는 유전자의 경우에는 한가지 제약이 생깁니다. 같은 도시가 둘 이상 나와서는 안된다는 것이죠. 이를테면 9421850137과 같은 유전자는 6번도시는 빠뜨리고 1번도시는 두번 방문하게 되므로 제대로된 유전자가 아닙니다.
그러므로 최초 유전자를 초기화할 때라든지 교차, 돌연변이 등 유전자에 조작을 가할 경우에는 위와 같은 제약을 어기지 않도록 조심해야 할 필요가 있습니다.