基因演算法為 University of Michigan 學者 John Holland 於 1975 年所提出,主要是將數值基因化 (即以 0、1 表示),並模擬生物演進過程的複製 (Reproduction)、交配 (Crossover)、突變 (Mutation) 等過程,尋找問題的近似較佳解。
基因演算法的應用近年來逐漸受到重視,而 C、C++ 等較偏工程方面的程式語言也較常被用來開發此類程式。然而,當計算 Fitness Value 需用到大量資料時,就必須與資料庫作某種程度的結合,方可順利達到目的;但應如何使用資料庫、並兼顧整體的效能,也就成為一個值得思考的問題。筆者在此提供一解決方案,即以 SQL Server 所提供的程式語言 (Transact-SQL) 進行基因演算法核心邏輯程式的開發,將基因演算法寫成一個 Stored Procedure,目的就是將演算邏輯直接植入資料庫,減少演算程式獲取資料的時間,進而提升整體運作效能。本程式會呼叫其他三個 Stored Procedures --
- usp_IEEE754_Generate :產生 IEEE 754 (32 bits) 格式的 2 進制基因碼 (程式碼請見文章 Transact-SQL IEEE 754 轉換程式 -- 32 bits)。
- usp_IEEE754_Restore: 將 IEEE 754 (32 bits) 格式的 2 進制基因碼反轉為 Float (程式碼請見文章 Transact-SQL IEEE 754 反轉程式 -- 32 bits)
- usp_Polynomial:此處使用一元二次多項式 (2X^2 + 3X + 7) 的 Fitness Value 產生程式,以找出該多項式的最小值 (詳細程式碼請見本文最後 [註] usp_Polynomial 程式碼)。
範例程式
本程式於複製(Reproduction)階段,採用輪盤式選擇(Roulette Wheel Selection);交配 (Crossover) 階段,採用雙點交配 (Two Point Crossover);突變 (Mutation) 階段,採用單點突變 (Single Point Mutation)。
參數說明及呼叫方式:請見程式碼第 16 ~ 33 行
Create Proc usp_Genetic_Algorithm @prm_total_generation_no int,
@prm_total_item_no int,
@prm_crossover_rate float,
@prm_mutation_rate float,
@prm_boundary_high float,
@prm_boundary_low float,
@rtn_result float output
/*-----------------------------------------------------------------------------------+
| Author: Wei-jie Yang |
| Date: 2012/06/14 |
| All Rights Reserved |
| |
| [Description] |
| This stored procedure is designed to perform Genetic Algorithm. |
| |
| [Parameters] |
| INPUT Parameters |
| - @prm_total_generation_no: Total Generation Number |
| - @prm_total_item_no: Total Item Number Per Generation (even) |
| - @prm_crossover_rate: Crossover Rate |
| - @prm_mutation_rate: Mutation Rate |
| - @prm_boundary_high: Upper Boundary |
| - @prm_boundary_low: Lower Boundary |
| |
| OUTPUT Parameter |
| - @rtn_result: Final Result |
| |
| [Usage] |
| declare @return_result float |
| |
| exec usp_Genetic_Algorithm 200, 100, 0.8, 0.1, 100, -100, @return_result output |
| |
| select @return_result Result |
+-----------------------------------------------------------------------------------*/
as
declare @avg_fitness_value float,
@crossover_digit_1 int,
@crossover_digit_2 int,
@crossover_digit_temp int,
@crossover_item_no_1 int,
@crossover_item_no_2 int,
@crossover_times int,
@fitness_value float,
@generation_no int,
@gene_1 varchar(32),
@gene_1_1 varchar(32),
@gene_1_2 varchar(32),
@gene_1_3 varchar(32),
@gene_2 varchar(32),
@gene_2_1 varchar(32),
@gene_2_2 varchar(32),
@gene_2_3 varchar(32),
@gene_codes varchar(32),
@gene_codes_binary binary(32),
@gene_digit bit,
@i int,
@item_counter int,
@item_no int,
@item_value float,
@j int,
@k int,
@l int,
@mutation_digit int,
@mutation_gene_codes varchar(32),
@mutation_gene_1 varchar(32),
@mutation_gene_2 char(1),
@mutation_gene_3 varchar(32),
@mutation_item_no int,
@new_item_no int,
@overflow_item_amount int,
@pre_perfect_fitness_value float,
@pre_perfect_gene_codes varchar(32),
@pre_perfect_item_value float,
@selected_numbers int,
@sql nvarchar(2000),
@tmp_fitness_value float,
@tmp_gene_codes varchar(32),
@tmp_item_value float,
@tmp_reproduction_no int,
@tmp_sn int,
@total_digit_length tinyint,
@total_reproduction int
declare @tbl_generation table(
sn int identity,
generation_no int,
item_no int,
item_value float,
gene_codes varchar(32),
fitness_value float,
reproduction_no int,
crossover_selected char(1), -- Selected Y/N (Crossover Stage)
mutation_selected char(1), -- Selected Y/N (Mutation Stage)
refresh_yn char(1)
)
declare @tbl_single_generation table(
sn int,
item_no int,
item_value float,
gene_codes varchar(32),
fitness_value float,
reproduction_no int
)
set nocount on
-- Initialization (begin) =============================================================================================
select @total_digit_length = 32
select @generation_no = 0
-- Initialization (end) ===============================================================================================
-- Generation Zero (begin) ============================================================================================
select @item_counter = 1
while @item_counter <= @prm_total_item_no
begin
select @gene_codes = ''
select @item_value = 0
-- Gene Codes, Item Value (begin) -------------------------------------------------------------
-- Item Value ...................................................
select @item_value = (@prm_boundary_high - @prm_boundary_low) * rand() + @prm_boundary_low
-- Gene Codes ...................................................
select @sql = ''
select @sql = 'exec usp_IEEE754_Generate ' + cast(@item_value as varchar(100)) +
', ' + '@inner1 output, @inner2 output '
exec sp_executesql @sql,
N'@inner1 varchar(32) output, @inner2 binary(32) output ',
@inner1 = @gene_codes output,
@inner2 = @gene_codes_binary output
-- Gene Codes, Item Value (end) ---------------------------------------------------------------
-- Fitness Value (begin) ----------------------------------------------------------------------
-- Call Fitness Value Calculating Stored Procedure ..............
select @sql = ''
select @sql = 'exec usp_Polynomial ' + cast(@item_value as varchar(100)) +
', ' + '@inner output '
exec sp_executesql @sql,
N'@inner float output ',
@inner = @fitness_value output
-- Fitness Value (end) ------------------------------------------------------------------------
insert into @tbl_generation (generation_no, item_no, item_value, gene_codes, fitness_value, reproduction_no, crossover_selected, mutation_selected, refresh_yn)
values(@generation_no, @item_counter, @item_value, @gene_codes, @fitness_value, 0, 'N', 'N', 'N')
select @item_counter = @item_counter + 1
end
-- Reproduction No (begin) ------------------------------------------------------------------------
select @avg_fitness_value = avg(fitness_value)
from @tbl_generation
where generation_no = @generation_no
update @tbl_generation
set reproduction_no = round(fitness_value / @avg_fitness_value, 0)
where generation_no = @generation_no
select @total_reproduction = sum(reproduction_no)
from @tbl_generation
where generation_no = @generation_no
if @total_reproduction < @prm_total_item_no
begin
select top 1 @item_no = item_no
from @tbl_generation
where generation_no = @generation_no
order by fitness_value desc
update @tbl_generation
set reproduction_no = reproduction_no + (@prm_total_item_no - @total_reproduction)
where generation_no = @generation_no
and item_no = @item_no
end
if @total_reproduction > @prm_total_item_no
begin
select @overflow_item_amount = @total_reproduction - @prm_total_item_no
select @i = 1
while @i <= @overflow_item_amount
begin
select top 1 @item_no = item_no
from @tbl_generation
where generation_no = @generation_no
and reproduction_no > 0
order by fitness_value asc
update @tbl_generation
set reproduction_no = reproduction_no - 1
where generation_no = @generation_no
and item_no = @item_no
select @i = @i + 1
end
end
-- Reproduction No (end) --------------------------------------------------------------------------
-- Generation Zero (end) ==============================================================================================
WHILE @generation_no <= @prm_total_generation_no
BEGIN
select @generation_no = @generation_no + 1
-- STAGE 1. Reproduction (begin) ==================================================================================
delete from @tbl_single_generation where 1 = 1
insert into @tbl_single_generation (item_no, item_value, gene_codes, fitness_value, reproduction_no)
select item_no, item_value, gene_codes, fitness_value, reproduction_no
from @tbl_generation
where generation_no = @generation_no - 1
declare Reproduction cursor for
select item_value, gene_codes, fitness_value, reproduction_no
from @tbl_single_generation
open Reproduction
fetch next from Reproduction
into @tmp_item_value, @tmp_gene_codes, @tmp_fitness_value, @tmp_reproduction_no
select @new_item_no = 1
while @@fetch_status = 0
begin
select @j = 1
while @j <= @tmp_reproduction_no
begin
insert into @tbl_generation (generation_no, item_no, item_value, gene_codes, fitness_value, reproduction_no, crossover_selected, mutation_selected, refresh_yn)
values(@generation_no, @new_item_no, @tmp_item_value, @tmp_gene_codes, @tmp_fitness_value, 0, 'N', 'N', 'N')
select @new_item_no = @new_item_no + 1
select @j = @j + 1
end
fetch next from Reproduction
into @tmp_item_value, @tmp_gene_codes, @tmp_fitness_value, @tmp_reproduction_no
end
close Reproduction
deallocate Reproduction
-- STAGE 1. Reproduction (end) ====================================================================================
-- Out of Range Item(s) Handling (begin) --------------------------------------------------------------------------
select top 1
@pre_perfect_item_value = item_value,
@pre_perfect_gene_codes = gene_codes,
@pre_perfect_fitness_value = fitness_value
from @tbl_generation
where generation_no = @generation_no - 1
and item_value > 0
order by fitness_value desc
update @tbl_generation
set item_value = @pre_perfect_item_value,
gene_codes = @pre_perfect_gene_codes,
fitness_value = @pre_perfect_fitness_value,
reproduction_no = -1
where generation_no = @generation_no
and ((item_value > @prm_boundary_high) or
(item_value < @prm_boundary_low))
-- Out of Range Item(s) Handling (end) ----------------------------------------------------------------------------
-- STAGE 2. Crossover (begin) =====================================================================================
select @crossover_times = @prm_total_item_no / 2
select @k = 1
while @k <= @crossover_times
begin
if rand() < @prm_crossover_rate
begin
select @crossover_item_no_1 = cast(rand() * (@prm_total_item_no - 1) + 1 as int)
select @crossover_item_no_2 = cast(rand() * (@prm_total_item_no - 1) + 1 as int)
select @selected_numbers = count(*)
from @tbl_generation
where crossover_selected = 'N'
and generation_no = @generation_no
and item_no in (@crossover_item_no_1, @crossover_item_no_2)
if @selected_numbers = 2 -- Both items are not selected yet. Perform Double Point Crossover.
begin
if rand() < @prm_crossover_rate
begin
select @crossover_digit_1 = cast(rand() * (@total_digit_length - 1) + 1 as int)
select @crossover_digit_2 = cast(rand() * (@total_digit_length - 1) + 1 as int)
while @crossover_digit_1 = @crossover_digit_2
begin
select @crossover_digit_2 = cast(rand() * (@total_digit_length - 1) + 1 as int)
end
if @crossover_digit_1 > @crossover_digit_2 -- Keep @crossover_digit_1 < @crossover_digit_2 (consider as string)
begin
select @crossover_digit_temp = @crossover_digit_1
select @crossover_digit_1 = @crossover_digit_2
select @crossover_digit_2 = @crossover_digit_temp
end
-- Retrieve gene codes
select @gene_1 = gene_codes
from @tbl_generation
where generation_no = @generation_no
and item_no = @crossover_item_no_1
select @gene_2 = gene_codes
from @tbl_generation
where generation_no = @generation_no
and item_no = @crossover_item_no_2
-- Divide each gene codes into three parts
if @crossover_digit_1 = 1
begin
select @gene_1_1 = ''
select @gene_2_1 = ''
end
else
begin
select @gene_1_1 = substring(@gene_1, 1, @crossover_digit_1 - 1)
select @gene_2_1 = substring(@gene_2, 1, @crossover_digit_1 - 1)
end
select @gene_1_2 = substring(@gene_1, @crossover_digit_1, @crossover_digit_2 - @crossover_digit_1 + 1)
select @gene_2_2 = substring(@gene_2, @crossover_digit_1, @crossover_digit_2 - @crossover_digit_1 + 1)
if @crossover_digit_2 = @total_digit_length
begin
select @gene_1_3 = substring(@gene_1, @crossover_digit_2, 1)
select @gene_2_3 = substring(@gene_2, @crossover_digit_2, 1)
end
else
begin
select @gene_1_3 = substring(@gene_1, @crossover_digit_2 + 1, @total_digit_length - @crossover_digit_2)
select @gene_2_3 = substring(@gene_2, @crossover_digit_2 + 1, @total_digit_length - @crossover_digit_2)
end
-- Crossover
select @gene_1 = @gene_1_1 + @gene_1_2 + @gene_1_3
select @gene_2 = @gene_2_1 + @gene_2_2 + @gene_2_3
end
end
if @selected_numbers = 2
begin
update @tbl_generation
set gene_codes = @gene_1,
crossover_selected = 'Y',
refresh_yn = 'Y'
where generation_no = @generation_no
and item_no = @crossover_item_no_1
update @tbl_generation
set gene_codes = @gene_2,
crossover_selected = 'Y',
refresh_yn = 'Y'
where generation_no = @generation_no
and item_no = @crossover_item_no_2
end
else
begin
update @tbl_generation
set crossover_selected = 'Y'
where generation_no = @generation_no
and item_no in (@crossover_item_no_1, @crossover_item_no_2)
end
end
select @k = @k + 1
end
-- STAGE 2. Crossover (end) =======================================================================================
-- STAGE 3. Mutation (begin) ======================================================================================
select @l = 1
while @l <= @prm_total_item_no
begin
if rand() < @prm_mutation_rate
begin
select @mutation_item_no = cast(rand() * (@prm_total_item_no - 1) + 1 as int)
select @mutation_digit = cast(rand() * (@total_digit_length - 1) + 1 as int)
select @mutation_gene_codes = gene_codes
from @tbl_generation
where generation_no = @generation_no
and item_no = @mutation_item_no
and mutation_selected = 'N'
if @mutation_digit = 1
begin
select @mutation_gene_1 = ''
select @mutation_gene_3 = substring(@mutation_gene_codes, 2, @total_digit_length - 1)
end
else
begin
if @mutation_digit = @total_digit_length
begin
select @mutation_gene_1 = substring(@mutation_gene_codes, 1, @total_digit_length - 1)
select @mutation_gene_3 = ''
end
else
begin
select @mutation_gene_1 = substring(@mutation_gene_codes, 1, @mutation_digit - 1)
select @mutation_gene_3 = substring(@mutation_gene_codes, @mutation_digit + 1, @total_digit_length - @mutation_digit)
end
end
select @mutation_gene_2 = substring(@mutation_gene_codes, @mutation_digit, 1)
if @mutation_gene_2 = '0'
select @mutation_gene_codes = @mutation_gene_1 + '1' + @mutation_gene_3
else
select @mutation_gene_codes = @mutation_gene_1 + '0' + @mutation_gene_3
update @tbl_generation
set gene_codes = @mutation_gene_codes,
mutation_selected = 'Y',
refresh_yn = 'Y'
where generation_no = @generation_no
and item_no = @mutation_item_no
end
select @l = @l + 1
end
-- STAGE 3. Mutation (end) ========================================================================================
-- Refresh (begin) ================================================================================================
delete from @tbl_single_generation where 1 = 1
insert into @tbl_single_generation (sn, gene_codes)
select sn, gene_codes
from @tbl_generation
where generation_no = @generation_no
and refresh_yn = 'Y'
declare Refresh cursor for
select sn, gene_codes
from @tbl_single_generation
open Refresh
fetch next from Refresh into @tmp_sn, @tmp_gene_codes
while @@fetch_status = 0
begin
-- Item Value (begin) ---------------------------------------
select @gene_codes = @tmp_gene_codes
select @sql = ''
select @sql = 'exec usp_IEEE754_Restore ' + '''' + @gene_codes + '''' +
', ' + '@inner output '
exec sp_executesql @sql,
N'@inner float output ',
@inner = @item_value output
-- Item Value (end) -----------------------------------------
-- Fitness Value (begin) ------------------------------------
-- Call Fitness Value Calculating Stored Procedure
select @sql = ''
select @sql = 'exec usp_Polynomial ' + cast(@item_value as varchar(100)) +
', ' + '@inner output '
exec sp_executesql @sql,
N'@inner float output ',
@inner = @fitness_value output
-- Fitness Value (end) --------------------------------------
update @tbl_generation
set item_value = @item_value,
fitness_value = @fitness_value
where sn = @tmp_sn
fetch next from Refresh into @tmp_sn, @tmp_gene_codes
end
close Refresh
deallocate Refresh
-- Refresh (end) ==================================================================================================
-- Generate Reproduction No (begin) ===============================================================================
select @avg_fitness_value = avg(fitness_value)
from @tbl_generation
where generation_no = @generation_no
update @tbl_generation
set reproduction_no = round(fitness_value / @avg_fitness_value, 0)
where generation_no = @generation_no
select @total_reproduction = sum(reproduction_no)
from @tbl_generation
where generation_no = @generation_no
if @total_reproduction < @prm_total_item_no
begin
select top 1 @item_no = item_no
from @tbl_generation
where generation_no = @generation_no
order by fitness_value desc
update @tbl_generation
set reproduction_no = reproduction_no + (@prm_total_item_no - @total_reproduction)
where generation_no = @generation_no
and item_no = @item_no
end
if @total_reproduction > @prm_total_item_no
begin
select @overflow_item_amount = @total_reproduction - @prm_total_item_no
select @i = 1
while @i <= @overflow_item_amount
begin
select top 1 @item_no = item_no
from @tbl_generation
where generation_no = @generation_no
and reproduction_no > 0
order by fitness_value
update @tbl_generation
set reproduction_no = reproduction_no - 1
where generation_no = @generation_no
and item_no = @item_no
select @i = @i + 1
end
end
-- Geberate Reproduction No (end) =================================================================================
END
select top 1 @rtn_result = item_value
from @tbl_generation
where generation_no = @prm_total_generation_no
and item_value <= @prm_boundary_high
and item_value >= @prm_boundary_low
order by fitness_value desc
select @rtn_result Result
--select *
-- from @tbl_generation
-- where generation_no = @prm_total_generation_no
set nocount off
return
Error_Handler:
set nocount off
return
[註] usp_Polynomial 程式碼
create proc usp_Polynomial @prm_num float,
@rtn_value float output
as
declare @fx float
-- 2X^2 + 3X + 7 Ans: -0.75
select @fx = 2 * (@prm_num * @prm_num) + (3 * @prm_num) + 7
select @rtn_value = 100000 * 1 / @fx
return