2011/06/09

遺伝的アルゴリズムとか

ちょっと興味があったので、小高 知宏 著 『はじめての機械学習』 を読んでみた。

で。 5 章 2 節 「遺伝的アルゴリズムによる知識獲得」には、簡単な GA (SGA: Simple GA)の実装例が掲載されている。

今回は、その挙動を Processing で可視化してみた。

完成品はこんな感じ。
各世代における適応度の推移グラフをProcessingで可視化したもの。
赤はエリート個体の適応度、青は当該世代における適応度の平均値。



【例題】

下の表に示した 20 個の整数から 1 つ以上の整数を選び出し、それらの合計値が 499 にもっとも近くなる組合せを求めよ。

   番 号       数 値   
1 31
2 41
3 59
4 26
5 53
6 58
7 97
8 93
9 23
10 84
    
   番 号       数 値   
11 -62
12 -64
13 -33
14 -83
15 -27
16 -95
17 -2
18 -88
19 -41
20 -97


たとえば、1 番目の数値である 31 と 6 番目の数値の 58 を選ぶと、合計値は 31 + 58 = 89 となる。

これが、目標値である 499 に近いほど優良な解となるため、適応度のパラメータには、499 との差の絶対値 |499 - s| を用いる。ただし、このままでは値の小さい方が適応度が高くなるため、以下のように充分に大きな数 fmax から差の絶対値を引くことにする。

適応度 =  fmax - |499 - s|

遺伝子座の設計に関する詳細は、当該書籍を参照するか、ソースコードを解読してほしい(あまり詳しく書くと著者に迷惑がかかるため)。



【実装】

Processing で動くように適当に書き換えたところ、こんな感じになった(ただしコンソール)。

final int    DATANO   =    20;     // データ数
final int    POOLSIZE =    30;     // プールサイズ
final int    LASTG    =   200;     // 打ち切り世代
final double MRATE    =     0.01;  // 突然変異の確率
final int    YES      =     1;     // yesに対応する整数値
final int    NO       =     0;     // noに対応する整数値
final int    VALUE    =   499;     // 目標値
final int    MAXFIT   =  1000;     // 適応度の計算に用いる数値

/* 問題の数値の設定 */
int[] q = { 31,  41,  59,  26,  53,  58, 97,  93,  23,  84,
           -62, -64, -33, -83, -27, -95, -2, -88, -41, -97};
           
void setup() {
  int[][] pool = new int[POOLSIZE][DATANO];  // 遺伝子プール
  int generation;                            // 現在の遺伝子数
    
  /* 初期集団の生成 */
  initpool(pool);
  
  /* 打ち切り世代まで繰り返し */
  for(generation = 0; generation < LASTG; ++generation) {
    println(generation + "世代");
    mating(pool);
    mutation(pool);
    printp(pool);
  }
}

/* ======================================== */
/*            select() メソッド             */
/*                   選択                   */
/* ======================================== */
int select(int[] roulette, int totalfitness) {
  int ball;     // 玉(選択位置の数値)
  int acc = 0;  // 適応度の積算値
  
  ball = (int)random(totalfitness);
  int i;
  for(i = 0; i < POOLSIZE; ++i) {
    acc += roulette[i];    // 適応度を積算
    if(acc > ball) break;  // 対応する遺伝子
  }
  return i;
}

/* ======================================== */
/*            mating() メソッド             */
/*                   交叉                   */
/* ======================================== */
void mating(int[][] pool) {
  int totalfitness = 0;  // 適応度の合計値
  int[][] nextpool = new int[POOLSIZE][DATANO];
                         // 子の世代
  int[] roulette = new int[POOLSIZE];
                         // 適応度を格納
  int mama, papa;        // 親の遺伝子の番号
  
  /* ルーレットの作成 */
  for(int i = 0; i < POOLSIZE; ++i) {
    roulette[i] = evalfit(pool[i]);
    /* 適応度の合計値を計算 */
    totalfitness += roulette[i];
  }
  
  /* 選択と交叉を繰り返す */
  for(int i = 0; i < POOLSIZE / 2; ++i) {
    do {  /* 親の選択 */
      mama = select(roulette, totalfitness);
      papa = select(roulette, totalfitness);
    } while (mama == papa);  // 重複の削除
    /* 特定の2遺伝子の交叉 */
    crossing(pool[mama],    pool[papa],
             nextpool[i*2], nextpool[i*2+1]);
  }
  /* 次世代のプールをコピー */
  copypool(pool, nextpool);
}

/* ======================================== */
/*           crossing() メソッド            */
/*           特定の2遺伝子の交叉            */
/* ======================================== */
void crossing(int[] m, int[] p, int[] c1, int[] c2) {
  int cp;    // 交叉する点
  
  /* 交叉点の決定 */
  cp = (int)random(DATANO);
  
  int j;
  /* 遺伝子の前半部分をコピー */
  for(j = 0; j < cp; ++j) {
    c1[j] = m[j];
    c2[j] = p[j];
  }
  
  /* 遺伝子の後半部分をコピー */
  for(; j < DATANO; ++j) {
    c2[j] = m[j];
    c1[j] = p[j];
  }
}

/* ======================================== */
/*           copypool() メソッド            */
/*          次世代のプールをコピー          */
/* ======================================== */
void copypool(int[][] pool, int[][] nextpool) {
  for(int i = 0; i < POOLSIZE; ++i) {
    for(int j = 0; j < DATANO; ++j) {
      pool[i][j] = nextpool[i][j];
    }
  }
}

/* ======================================== */
/*            evalfit() メソッド            */
/*               適応度の計算               */
/* ======================================== */
int evalfit(int[] g) {
  int fitness = 0;          // 適応度の途中値
  for(int i = 0; i < DATANO; ++i) {
    fitness += g[i] * q[i];
  }
  return MAXFIT - abs(VALUE - fitness);
}

/* ======================================== */
/*            printp() メソッド             */
/*                 結果出力                 */
/* ======================================== */
void printp(int[][] pool) {
  int    fitness;           // 適応度
  double totalfitness = 0;  // 適応度の合計値
  int    elite        = 0;  // エリート遺伝子の処理用変数
  int    bestfit      = 0;
  
  for(int i = 0; i < POOLSIZE; ++i) {
    for(int j = 0; j < DATANO; ++j) {
      print(pool[i][j]);
    }
    fitness = evalfit(pool[i]);
    println("\t" + fitness);
    if(fitness > bestfit) { /* エリート解 */
      bestfit = fitness;
      elite = i;
    }
    totalfitness += fitness;
  }
  
  /* エリート解の適応度を出力 */
  print(elite + "\t" + bestfit + "\t");
  
  /* 平均の適応度を出力 */
  println(totalfitness / POOLSIZE);
}

/* ======================================== */
/*           initpool() メソッド            */
/*              初期集団の生成              */
/* ======================================== */
void initpool(int[][] pool) {
  for(int i = 0; i < POOLSIZE; ++i) {
    for(int j = 0; j < DATANO; ++j) {
      pool[i][j] = (int)random(2);
    }
  }
}

/* ======================================== */
/*           mutation() メソッド            */
/*                 突然変異                 */
/* ======================================== */
void mutation(int[][] pool) {
  for(int i = 0; i < POOLSIZE; ++i) {
    for(int j = 0; j < DATANO; ++j) {
      if(random(100) / 100.0 <= MRATE) {
        /* 反転の突然変異 */
        pool[i][j] = notval(pool[i][j]);
      }
    }
  }
}

/* ======================================== */
/*            notval() メソッド             */
/*               真理値の反転               */
/* ======================================== */
int notval(int v) {
  if(v == YES) return NO;
  else return YES;
}

ここまではコンソールアプリケーションなので、実行してもあまり見栄えはよくない。

グラフ描画に必要なのは、各世代におけるエリート個体の適応度、および(各世代における)全個体の適応度の平均値なので、それらを得るべく printp メソッドを改造してこんなものを作った。

/* ======================================== */
/*    getFitnessValueOfElite() メソッド     */
/*        エリート個体の適応度を得る        */
/* ======================================== */
int getFitnessValueOfElite(int[][] pool) {
  int    fitness;           // 適応度
  int    elite        = 0;  // エリート遺伝子の処理用変数
  int    bestfit      = 0;
  
  for(int i = 0; i < POOLSIZE; ++i) {
    fitness = evalfit(pool[i]);
    if(fitness > bestfit) { /* エリート解 */
      bestfit = fitness;
      elite = i;
    }
  }
  return bestfit;
}

/* ======================================== */
/*    getAvarageFitnessValue() メソッド     */
/*            適応度の平均を得る            */
/* ======================================== */
float getAvarageFitnessValue(int[][] pool) {
  double totalfitness = 0;  // 適応度の合計値
  
  for(int i = 0; i < POOLSIZE; ++i) {
    totalfitness += evalfit(pool[i]);
  }
  
  /* 平均の適応度を出力 */
  return (float)totalfitness / POOLSIZE;
}



各世代における適応度を配列に保存しておき、draw メソッドで描画する。

void draw() {
  background(0);
  smooth();
  
  // 目盛り表示
  stroke(50);
  for(int i = 0; i < width; i += 50) {
    line(i, 0, i, height);
  }
  for(int i = 0; i < height; i += 50) {
    line(0, i, width, i);
  }
  
  for(int i = 0; i < LASTG-1; ++i) {
    stroke(255, 0, 0);
    line(i    , MAXFIT - eliteFitness[i],
         i + 1, MAXFIT - eliteFitness[i+1]);
    
    stroke(0, 0, 255);
    line(i    , MAXFIT - avgFitness[i],
         i + 1, MAXFIT - avgFitness[i+1]);
  }
  
  // ガイドライン表示
  if(0 <= mouseX && mouseX < LASTG) {
    stroke(255);
    line(mouseX, mouseY, 
         mouseX, MAXFIT - eliteFitness[mouseX]);

  // 結果文字列表示
  if(mouseX < width / 2) textAlign(LEFT);
  else textAlign(RIGHT);
  text("第" + mouseX + "世代(適応度:" + eliteFitness[mouseX]+")",
       mouseX, 
       MAXFIT - eliteFitness[mouseX] + 12);
  }
}

これでよし。

ちなみにフルバージョンのソースコードはこちら。

final int    DATANO   =    20;     // データ数
final int    POOLSIZE =    30;     // プールサイズ
final int    LASTG    =   600;     // 打ち切り世代
final double MRATE    =     0.01;  // 突然変異の確率
final int    YES      =     1;     // yesに対応する整数値
final int    NO       =     0;     // noに対応する整数値
final int    VALUE    =   499;     // 目標値
final int    MAXFIT   =  1000;     // 適応度の計算に用いる数値

/* 問題の数値の設定 */
int[] q = { 31,  41,  59,  26,  53,  58, 97,  93,  23,  84,
           -62, -64, -33, -83, -27, -95, -2, -88, -41, -97};

/* 適応度の推移を格納する配列 */
int[] eliteFitness;
float[] avgFitness;

void setup() {
  int[][] pool = new int[POOLSIZE][DATANO];  // 遺伝子プール
  int generation;                            // 現在の遺伝子数
    
  size(LASTG, VALUE);
  
  /* 初期集団の生成 */
  initpool(pool);
  
  /* 配列を初期化 */
  eliteFitness = new int[LASTG];
  avgFitness   = new float[LASTG];
  
  /* 打ち切り世代まで繰り返し */
  for(generation = 0; generation < LASTG; ++generation) {
    mating(pool);
    mutation(pool);
    
    eliteFitness[generation] = getFitnessValueOfElite(pool);
    avgFitness[generation]   = getAvarageFitnessValue(pool);
  }
}

void draw() {
  background(0);
  smooth();
  
  // 目盛り表示
  stroke(50);
  for(int i = 0; i < width; i += 50) {
    line(i, 0, i, height);
  }
  for(int i = 0; i < height; i += 50) {
    line(0, i, width, i);
  }
  
  for(int i = 0; i < LASTG-1; ++i) {
    stroke(255, 0, 0);
    line(i    , MAXFIT - eliteFitness[i],
         i + 1, MAXFIT - eliteFitness[i+1]);
    
    stroke(0, 0, 255);
    line(i    , MAXFIT - avgFitness[i],
         i + 1, MAXFIT - avgFitness[i+1]);
  }
  
  // ガイドライン表示
  if(0 <= mouseX && mouseX < LASTG) {
    stroke(255);
    line(mouseX, mouseY, 
         mouseX, MAXFIT - eliteFitness[mouseX]);

  // 結果文字列表示
  if(mouseX < width / 2) textAlign(LEFT);
  else textAlign(RIGHT);
  text("第" + mouseX + "世代(適応度:" + eliteFitness[mouseX]+")",
       mouseX, 
       MAXFIT - eliteFitness[mouseX] + 12);
  }
}

/* ======================================== */
/*            select() メソッド             */
/*                   選択                   */
/* ======================================== */
int select(int[] roulette, int totalfitness) {
  int ball;     // 玉(選択位置の数値)
  int acc = 0;  // 適応度の積算値
  
  ball = (int)random(totalfitness);
  int i;
  for(i = 0; i < POOLSIZE; ++i) {
    acc += roulette[i];    // 適応度を積算
    if(acc > ball) break;  // 対応する遺伝子
  }
  return i;
}

/* ======================================== */
/*            mating() メソッド             */
/*                   交叉                   */
/* ======================================== */
void mating(int[][] pool) {
  int totalfitness = 0;  // 適応度の合計値
  int[][] nextpool = new int[POOLSIZE][DATANO];
                         // 子の世代
  int[] roulette = new int[POOLSIZE];
                         // 適応度を格納
  int mama, papa;        // 親の遺伝子の番号
  
  /* ルーレットの作成 */
  for(int i = 0; i < POOLSIZE; ++i) {
    roulette[i] = evalfit(pool[i]);
    /* 適応度の合計値を計算 */
    totalfitness += roulette[i];
  }
  
  /* 選択と交叉を繰り返す */
  for(int i = 0; i < POOLSIZE / 2; ++i) {
    do {  /* 親の選択 */
      mama = select(roulette, totalfitness);
      papa = select(roulette, totalfitness);
    } while (mama == papa);  // 重複の削除
    /* 特定の2遺伝子の交叉 */
    crossing(pool[mama],    pool[papa],
             nextpool[i*2], nextpool[i*2+1]);
  }
  /* 次世代のプールをコピー */
  copypool(pool, nextpool);
}

/* ======================================== */
/*           crossing() メソッド            */
/*           特定の2遺伝子の交叉            */
/* ======================================== */
void crossing(int[] m, int[] p, int[] c1, int[] c2) {
  int cp;    // 交叉する点
  
  /* 交叉点の決定 */
  cp = (int)random(DATANO);
  
  int j;
  /* 遺伝子の前半部分をコピー */
  for(j = 0; j < cp; ++j) {
    c1[j] = m[j];
    c2[j] = p[j];
  }
  
  /* 遺伝子の後半部分をコピー */
  for(; j < DATANO; ++j) {
    c2[j] = m[j];
    c1[j] = p[j];
  }
}

/* ======================================== */
/*           copypool() メソッド            */
/*          次世代のプールをコピー          */
/* ======================================== */
void copypool(int[][] pool, int[][] nextpool) {
  for(int i = 0; i < POOLSIZE; ++i) {
    for(int j = 0; j < DATANO; ++j) {
      pool[i][j] = nextpool[i][j];
    }
  }
}

/* ======================================== */
/*            evalfit() メソッド            */
/*               適応度の計算               */
/* ======================================== */
int evalfit(int[] g) {
  int fitness = 0;          // 適応度の途中値
  for(int i = 0; i < DATANO; ++i) {
    fitness += g[i] * q[i];
  }
  return MAXFIT - abs(VALUE - fitness);
}

/* ======================================== */
/*            printp() メソッド             */
/*                 結果出力                 */
/* ======================================== */
void printp(int[][] pool) {
  int    fitness;           // 適応度
  double totalfitness = 0;  // 適応度の合計値
  int    elite        = 0;  // エリート遺伝子の処理用変数
  int    bestfit      = 0;
  
  for(int i = 0; i < POOLSIZE; ++i) {
    for(int j = 0; j < DATANO; ++j) {
      print(pool[i][j]);
    }
    fitness = evalfit(pool[i]);
    println("\t" + fitness);
    if(fitness > bestfit) { /* エリート解 */
      bestfit = fitness;
      elite = i;
    }
    totalfitness += fitness;
  }
  
  /* エリート解の適応度を出力 */
  print(elite + "\t" + bestfit + "\t");
  
  /* 平均の適応度を出力 */
  println(totalfitness / POOLSIZE);
}

/* ======================================== */
/*    getFitnessValueOfElite() メソッド     */
/*        エリート個体の適応度を得る        */
/* ======================================== */
int getFitnessValueOfElite(int[][] pool) {
  int    fitness;           // 適応度
  int    elite        = 0;  // エリート遺伝子の処理用変数
  int    bestfit      = 0;
  
  for(int i = 0; i < POOLSIZE; ++i) {
    fitness = evalfit(pool[i]);
    if(fitness > bestfit) { /* エリート解 */
      bestfit = fitness;
      elite = i;
    }
  }
  return bestfit;
}

/* ======================================== */
/*    getAvarageFitnessValue() メソッド     */
/*            適応度の平均を得る            */
/* ======================================== */
float getAvarageFitnessValue(int[][] pool) {
  double totalfitness = 0;  // 適応度の合計値
  
  for(int i = 0; i < POOLSIZE; ++i) {
    totalfitness += evalfit(pool[i]);
  }
  
  /* 平均の適応度を出力 */
  return (float)totalfitness / POOLSIZE;
}

/* ======================================== */
/*           initpool() メソッド            */
/*              初期集団の生成              */
/* ======================================== */
void initpool(int[][] pool) {
  for(int i = 0; i < POOLSIZE; ++i) {
    for(int j = 0; j < DATANO; ++j) {
      pool[i][j] = (int)random(2);
    }
  }
}

/* ======================================== */
/*           mutation() メソッド            */
/*                 突然変異                 */
/* ======================================== */
void mutation(int[][] pool) {
  for(int i = 0; i < POOLSIZE; ++i) {
    for(int j = 0; j < DATANO; ++j) {
      if(random(100) / 100.0 <= MRATE) {
        /* 反転の突然変異 */
        pool[i][j] = notval(pool[i][j]);
      }
    }
  }
}

/* ======================================== */
/*            notval() メソッド             */
/*               真理値の反転               */
/* ======================================== */
int notval(int v) {
  if(v == YES) return NO;
  else return YES;
}

0 件のコメント:

コメントを投稿

ひとことどうぞφ(・ω・,,)