遺傳算法在自動組卷中的應用

遺傳算法

遺傳算法(Genetic Algorithm)是一種模擬天然界的進化規律-優勝劣汰演化來的隨機搜索算法,其在解決多種約束條件下的最優解這類問題上具備優秀的表現.java

1. 基本概念

在遺傳算法中有幾個基本的概念:基因、個體、種羣和進化.基因是個體的表現,不一樣個體的基因序列不一樣;個體是指單個的生命,個體是組成種羣的基礎;而進化的基本單位是種羣,一個種羣裏面有多個個體;進化是指一個種羣進過優勝劣汰的天然選擇後,產生一個新的種羣的過程,理論上進化會產生更優秀的種羣.算法

2. 算法流程

一個傳統的遺傳算法由如下幾個組成部分:數據庫

  • 初始化. 隨機生成一個規模爲N的種羣,設置最大進化次數以及中止進化條件.
  • 計算適應度. 適應度被用來評價個體的質量,且適應度是惟一評判因子.計算種羣中每一個個體的適應度,獲得最優秀的個體.
  • 選擇. 選擇是用來獲得一些優秀的個體來產生下一代.選擇算法的好壞相當重要,由於在必定程度上選擇會影響種羣的進化方向.經常使用的選擇算法有:隨機抽取、競標賽選擇以及輪盤賭模擬法等等.
  • 交叉. 交叉是兩個個體繁衍下一代的過程,其實是子代獲取父親和母親的部分基因,即基因重組.經常使用的交叉方法有:單點交叉、多點交叉等.
  • 變異. 變異即模擬突變過程.經過變異,種羣中個體變得多樣化.可是變異是有一個機率的.

經典的遺傳算法的流程圖以下所示:數組

3. java實現

爲了防止進化方向出現誤差,在本算法中採用精英主義,即每次進化都保留上一代種羣中最優秀的個體。dom

  • 個體適應度:經過比較個體與指望值的相同位置上的基因,相同則適應度加1
  • 選擇策略:隨機產生一個淘汰數組,選擇淘汰數組中的最優秀個體做爲選擇結果,即模擬優勝劣汰的過程
  • 交叉策略:對於個體的每一個基因,產生一個隨機數,若是隨機數小於交叉機率,則繼承父親該位置的基因,不然繼承母親的該位置的基因
  • 變異策略:個體的基因序列上的每一個基因都有變異的機會,若是隨機機率大於變異機率,則進行基因突變,本例中的突變策略是:隨機產生一個0或者1

計算適應度函數

/** * 經過和solution比較 ,計算個體的適應值 * @param individual 待比較的個體 * @return 返回適應度 */ public static int getFitness(Individual individual) { int fitness = 0; for (int i = 0; i < individual.size() && i < solution.length; i++) { if (individual.getGene(i) == solution[i]) { fitness++; } } return fitness; }

 

選擇算子測試

/** * 隨機選擇一個較優秀的個體。用於進行交叉 * @param pop 種羣 * @return */ private static Individual tournamentSelection(Population pop) { Population tournamentPop = new Population(tournamentSize, false); // 隨機選擇 tournamentSize 個放入 tournamentPop 中 for (int i = 0; i < tournamentSize; i++) { int randomId = (int) (Math.random() * pop.size()); tournamentPop.saveIndividual(i, pop.getIndividual(randomId)); } // 找到淘汰數組中最優秀的 Individual fittest = tournamentPop.getFittest(); return fittest; }

 

交叉算子優化

/** * 兩個個體交叉產生下一代 * @param indiv1 父親 * @param indiv2 母親 * @return 後代 */ private static Individual crossover(Individual indiv1, Individual indiv2) { Individual newSol = new Individual(); // 隨機的從兩個個體中選擇 for (int i = 0; i < indiv1.size(); i++) { if (Math.random() <= uniformRate) { newSol.setGene(i, indiv1.getGene(i)); } else { newSol.setGene(i, indiv2.getGene(i)); } } return newSol; }

 

變異算子編碼

/** * 突變個體。突變的機率爲 mutationRate * @param indiv 待突變的個體 */ private static void mutate(Individual indiv) { for (int i = 0; i < indiv.size(); i++) { if (Math.random() <= mutationRate) { // 生成隨機的 0 或 1 byte gene = (byte) Math.round(Math.random()); indiv.setGene(i, gene); } } }

 

4. 測試結果

測試結果以下圖atom


遺傳算法與自動組卷

隨着軟件和硬件技術的發展,在線考試系統正在逐漸取代傳統的線下筆試。對於一個在線考試系統而言,考試試卷的質量很大程度上表明着該系統的質量,試卷是否包含足夠多的題型、是否包含指定的知識點以及試卷總體的難度係數是否合適等等,這些都能做爲評價一個在線測評系統的指標.若是單純的根據組卷規則直接從數據庫中獲取必定數量的試題組成一套試卷,因爲只獲取一次,並不能保證這樣的組卷結果是一個合適的結果,並且能夠確定的是,這樣獲得的結果基本不會是一個優秀的解.顯而易見,咱們須要一個優秀的自動組卷算法,遺傳算法就很是適合解決自動組卷的問題,其具備自進化、並行執行等特色

1. 對遺傳算法的改進

使用傳統的遺傳算法進行組卷時會出現一些誤差,進化的結果不是很是理想.具體表現爲:進化方向出現誤差、搜索後期效率低、容易陷入局部最優解等問題.針對這些問題,本系統對傳統的遺傳算法作了一些改進,具體表現爲:使用精英主義模式(即每次進化都保留上一代種羣的最優解)、實數編碼以及選擇算子的優化.

1.1 染色體編碼方式的改進

染色體編碼是遺傳算法首先要解決的問題,是將個體的特徵抽象爲一套編碼方案.在傳統的遺傳算法解決方案中,二進制編碼使用的最多,就本系統而言,二進制編碼造成的基因序列爲整個題庫,這種方案不是很合適,由於二進制編碼按照題庫中試題的相對順序將題庫編碼成一個01字符串,1表明試題出現,0表明沒有顯然這樣的編碼規模太大,對於一個優秀的題庫而言,十萬的試題總量是很常見的,對於一個長度爲十萬的字符串進行編碼和解碼顯然太繁瑣. 通過查閱資料,因而決定採用實數編碼做爲替代,將試題的id做爲基因,試卷和染色體創建映射關係,同一類型的試題放在一塊兒.好比,要組一套java考試試卷,題目總數爲15:填空3道,單選10道,主觀題2道.那麼進行實數編碼後,其基因序列分佈表現爲:

1.2 初始化種羣設計

初始化試卷時不採起徹底隨機的方式.經過分析不難發現,組卷主要有題型、數量、總分、知識點和難度係數這五個約束條件,在初始化種羣的時候,咱們能夠根據組卷規則隨機產生指定數量的題型,這樣在一開始種羣中的個體就知足了題型、數量和總分的約束,使約束條件從5個減小爲2個:知識點和難度係數.這樣算法的迭代次數被減小,收斂也將加快.

1.3 適應度函數設計

在遺傳算法中,適應度是評價種羣中個體的優劣的惟一指標,適應度能夠影響種羣的進化方向.因爲在初始化時,種羣中個體已經知足了題型、數量和總分這三個約束條件,因此個體的適應度只與知識點和難度係數有關.

試卷的難度係數計算公式爲:

 

 
ni=1TiKini=1Ki∑i=1nTiKi∑i=1nKi

 

n是組卷規則要求的題目總數,Ti,Ki分別是第i題的難度係數和分數.

本例中使用知識點覆蓋率來評價知識點.即一套試卷要求包含N個知識點,而某個體中包含的知識點數目爲M(去重後的結果,M<=N),那麼該個體的知識點覆蓋率爲:M/N. 所以,適應度函數爲:

 

 
f=1(1MN)t1|EPP|t2f=1−(1−MN)∗t1−|EP−P|∗t2

 

其中,M/N爲知識點覆蓋率;EP爲用戶輸入的總體指望難度,P爲總體實際難度;知識點權重用t1表示,難度係數權重用t2表示.

1.4 選擇算子與交叉算子的改進

本例中的選擇策略爲:指定一個淘汰數組的大小(筆者使用的是5),從原種羣中隨機挑選個體組成一個淘汰種羣,將淘汰種羣中的最優個體做爲選擇算子的結果.

交叉算子其實是染色體的重組.本系統中採用的交叉策略爲:在(0,N)之間隨機產生兩個整數n1,n2,父親基因序列上n1到n2之間的基因所有遺傳給子代,母親基因序列上的n1到n2以外的基因遺傳給子代,可是要確保基因不重複,若是出現重複(實驗證實有較大的機率出現重複),那麼從題庫中挑選一道與重複題的題型相同、分值相同且包含的知識點相同的試題遺傳給子代.全部的遺傳都要保證基因在染色體上的相對位置不變.

1.5 變異算子的改進

基因變異的出現增長了種羣的多樣性.在本系統中,每一個個體的每一個基因都有變異的機會,若是隨機機率小於變異機率,那麼基因就能夠突變.突變基因的原則爲:與原題的同題型、同分數且同知識點的試題.有研究代表,對於變異機率的選擇,在0.1-0.001之間最佳,本例中選取了0.085做爲變異機率.

1.6 組卷規則

組卷規則是初始化種羣的依賴。組卷規則由用戶指定,規定了用戶指望的試卷的條件:試卷總分、包含的題型與數量、指望難度係數、指望覆蓋的知識點。在本例中將組卷規則封裝爲一個JavaBean

2. java實現

2.1 試卷個體

個體,即試卷.本例中將試卷個體抽象成一個JavaBean,其有id,適應度、知識點覆蓋率、難度係數、總分、以及個體包含的試題集合這6個屬性,以及計算知識點覆蓋率和適應度這幾個方法.在計算適應度的時候,知識點權重爲0.20,難度係數權重爲0.80.

/** * 計算試卷總分 * * @return */ public double getTotalScore() { if (totalScore == 0) { double total = 0; for (QuestionBean question : questionList) { total += question.getScore(); } totalScore = total; } return totalScore; } /** * 計算試卷個體難度係數 計算公式: 每題難度*分數求和除總分 * * @return */ public double getDifficulty() { if (difficulty == 0) { double _difficulty = 0; for (QuestionBean question : questionList) { _difficulty += question.getScore() * question.getDifficulty(); } difficulty = _difficulty / getTotalScore(); } return difficulty; } /** * 計算知識點覆蓋率 公式爲:個體包含的知識點/指望包含的知識點 * * @param rule */ public void setKpCoverage(RuleBean rule) { if (kPCoverage == 0) { Set<String> result = new HashSet<String>(); result.addAll(rule.getPointIds()); Set<String> another = questionList.stream().map(questionBean -> String.valueOf(questionBean.getPointId())).collect(Collectors.toSet()); // 交集操做 result.retainAll(another); kPCoverage = result.size() / rule.getPointIds().size(); } } /** * 計算個體適應度 公式爲:f=1-(1-M/N)*f1-|EP-P|*f2 * 其中M/N爲知識點覆蓋率,EP爲指望難度係數,P爲種羣個體難度係數,f1爲知識點分佈的權重 * ,f2爲難度係數所佔權重。當f1=0時退化爲只限制試題難度係數,當f2=0時退化爲只限制知識點分佈 * * @param rule 組卷規則 * @param f1 知識點分佈的權重 * @param f2 難度係數的權重 */ public void setAdaptationDegree(RuleBean rule, double f1, double f2) { if (adaptationDegree == 0) { adaptationDegree = 1 - (1 - getkPCoverage()) * f1 - Math.abs(rule.getDifficulty() - getDifficulty()) * f2; } } public boolean containsQuestion(QuestionBean question) { if (question == null) { for (int i = 0; i < questionList.size(); i++) { if (questionList.get(i) == null) { return true; } } } else { for (QuestionBean aQuestionList : questionList) { if (aQuestionList != null) { if (aQuestionList.equals(question)) { return true; } } } } return false; }

 

2.2 種羣初始化

種羣初始化。將種羣抽象爲一個Java類Population,其有初始化種羣、獲取最優個體的方法,關鍵代碼以下

/** * 初始種羣 * * @param populationSize 種羣規模 * @param initFlag 初始化標誌 true-初始化 * @param rule 規則bean */ public Population(int populationSize, boolean initFlag, RuleBean rule) { papers = new Paper[populationSize]; if (initFlag) { Paper paper; Random random = new Random(); for (int i = 0; i < populationSize; i++) { paper = new Paper(); paper.setId(i + 1); while (paper.getTotalScore() != rule.getTotalMark()) { paper.getQuestionList().clear(); String idString = rule.getPointIds().toString(); // 單選題 if (rule.getSingleNum() > 0) { generateQuestion(1, random, rule.getSingleNum(), rule.getSingleScore(), idString, "單選題數量不夠,組卷失敗", paper); } // 填空題 if (rule.getCompleteNum() > 0) { generateQuestion(2, random, rule.getCompleteNum(), rule.getCompleteScore(), idString, "填空題數量不夠,組卷失敗", paper); } // 主觀題 if (rule.getSubjectiveNum() > 0) { generateQuestion(3, random, rule.getSubjectiveNum(), rule.getSubjectiveScore(), idString, "主觀題數量不夠,組卷失敗", paper); } } // 計算試卷知識點覆蓋率 paper.setKpCoverage(rule); // 計算試卷適應度 paper.setAdaptationDegree(rule, Global.KP_WEIGHT, Global.DIFFCULTY_WEIGHt); papers[i] = paper; } } } private void generateQuestion(int type, Random random, int qustionNum, double score, String idString, String errorMsg, Paper paper) { QuestionBean[] singleArray = QuestionService.getQuestionArray(type, idString .substring(1, idString.indexOf("]"))); if (singleArray.length < qustionNum) { log.error(errorMsg); return; } QuestionBean tmpQuestion; for (int j = 0; j < qustionNum; j++) { int index = random.nextInt(singleArray.length - j); // 初始化分數 singleArray[index].setScore(score); paper.addQuestion(singleArray[index]); // 保證不會重複添加試題 tmpQuestion = singleArray[singleArray.length - j - 1]; singleArray[singleArray.length - j - 1] = singleArray[index]; singleArray[index] = tmpQuestion; } }

 

2.3 選擇算子與交叉算子的實現

選擇算子的實現:

/** * 選擇算子 * * @param population */ private static Paper select(Population population) { Population pop = new Population(tournamentSize); for (int i = 0; i < tournamentSize; i++) { pop.setPaper(i, population.getPaper((int) (Math.random() * population.getLength()))); } return pop.getFitness(); }

 

交叉算子的實現.本系統實現的算子爲兩點交叉,在算法的實現過程當中須要保證子代中不出現相同的試題.關鍵代碼以下:

/** * 交叉算子 * * @param parent1 * @param parent2 * @return */ public static Paper crossover(Paper parent1, Paper parent2, RuleBean rule) { Paper child = new Paper(parent1.getQuestionSize()); int s1 = (int) (Math.random() * parent1.getQuestionSize()); int s2 = (int) (Math.random() * parent1.getQuestionSize()); // parent1的startPos endPos之間的序列,會被遺傳到下一代 int startPos = s1 < s2 ? s1 : s2; int endPos = s1 > s2 ? s1 : s2; for (int i = startPos; i < endPos; i++) { child.saveQuestion(i, parent1.getQuestion(i)); } // 繼承parent2中未被child繼承的question // 防止出現重複的元素 String idString = rule.getPointIds().toString(); for (int i = 0; i < startPos; i++) { if (!child.containsQuestion(parent2.getQuestion(i))) { child.saveQuestion(i, parent2.getQuestion(i)); } else { int type = getTypeByIndex(i, rule); QuestionBean[] singleArray = QuestionService.getQuestionArray(type, idString.substring(1, idString .indexOf("]"))); child.saveQuestion(i, singleArray[(int) (Math.random() * singleArray.length)]); } } for (int i = endPos; i < parent2.getQuestionSize(); i++) { if (!child.containsQuestion(parent2.getQuestion(i))) { child.saveQuestion(i, parent2.getQuestion(i)); } else { int type = getTypeByIndex(i, rule); QuestionBean[] singleArray = QuestionService.getQuestionArray(type, idString.substring(1, idString .indexOf("]"))); child.saveQuestion(i, singleArray[(int) (Math.random() * singleArray.length)]); } } return child; }

 

2.4 變異算子的實現

本系統中變異機率爲0.085,對種羣的每一個個體的每一個基因都有變異機會.變異策略爲:在(0,1)之間產生一個隨機數,若是小於變異機率,那麼該基因突變.關鍵代碼以下:

/** * 突變算子 每一個個體的每一個基因都有可能突變 * * @param paper */ public static void mutate(Paper paper) { QuestionBean tmpQuestion; List<QuestionBean> list; int index; for (int i = 0; i < paper.getQuestionSize(); i++) { if (Math.random() < mutationRate) { // 進行突變,第i道 tmpQuestion = paper.getQuestion(i); // 從題庫中獲取和變異的題目類型同樣分數相同的題目(不包含變異題目) list = QuestionService.getQuestionListWithOutSId(tmpQuestion); if (list.size() > 0) { // 隨機獲取一道 index = (int) (Math.random() * list.size()); // 設置分數 list.get(index).setScore(tmpQuestion.getScore()); paper.saveQuestion(i, list.get(index)); } } } }

 

2.5 進化的總體流程

本系統中採用精英策略,每次進化都保留上一代最優秀個體.這樣就能避免種羣進化方向發生變化,出現適應度倒退的狀況.關鍵代碼以下:

// 進化種羣 public static Population evolvePopulation(Population pop, RuleBean rule) { Population newPopulation = new Population(pop.getLength()); int elitismOffset; // 精英主義 if (elitism) { elitismOffset = 1; // 保留上一代最優秀個體 Paper fitness = pop.getFitness(); fitness.setId(0); newPopulation.setPaper(0, fitness); } // 種羣交叉操做,從當前的種羣pop 來 建立下一代種羣 newPopulation for (int i = elitismOffset; i < newPopulation.getLength(); i++) { // 較優選擇parent Paper parent1 = select(pop); Paper parent2 = select(pop); while (parent2.getId() == parent1.getId()) { parent2 = select(pop); } // 交叉 Paper child = crossover(parent1, parent2, rule); child.setId(i); newPopulation.setPaper(i, child); } // 種羣變異操做 Paper tmpPaper; for (int i = elitismOffset; i < newPopulation.getLength(); i++) { tmpPaper = newPopulation.getPaper(i); mutate(tmpPaper); // 計算知識點覆蓋率與適應度 tmpPaper.setKpCoverage(rule); tmpPaper.setAdaptationDegree(rule, Global.KP_WEIGHT, Global.DIFFCULTY_WEIGHt); } return newPopulation; }

 

3. 測試結果

組卷規則爲:指望試卷難度係數0.82,共100分,20道選擇題,2分一道,10道填空題,2分一道,4道主觀題,10分一道,要求囊括6個知識點. 
外在的條件爲:題庫試題總量爲10950,指望適應度值爲0.98,種羣最多迭代100次. 
測試代碼以下:

/** * 組捲過程 * * @param rule * @return */ public static Paper generatePaper(RuleBean rule) { Paper resultPaper = null; // 迭代計數器 int count = 0; int runCount = 100; // 適應度指望值z double expand = 0.98; if (rule != null) { // 初始化種羣 Population population = new Population(20, true, rule); System.out.println("初次適應度 " + population.getFitness().getAdaptationDegree()); while (count < runCount && population.getFitness().getAdaptationDegree() < expand) { count++; population = GA.evolvePopulation(population, rule); System.out.println("第 " + count + " 次進化,適應度爲: " + population.getFitness().getAdaptationDegree()); } System.out.println("進化次數: " + count); System.out.println(population.getFitness().getAdaptationDegree()); resultPaper = population.getFitness(); } return resultPaper; }

 

測試結果以下:

能夠看到改進後的遺傳算法具備較好的表現

相關文章
相關標籤/搜索