1. 程式人生 > >遺傳算法在自動組卷中的應用

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

init 替代 AI log LG 2個 code 要求 cor

遺傳算法

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

1. 基本概念

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

2. 算法流程

一個傳統的遺傳算法由以下幾個組成部分:

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

經典的遺傳算法的流程圖如下所示:

技術分享圖片

3. java實現

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

  • 個體適應度:通過比較個體與期望值的相同位置上的基因,相同則適應度加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. 測試結果

測試結果如下圖

技術分享圖片


遺傳算法與自動組卷

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

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;
}

測試結果如下:

技術分享圖片

可以看到改進後的遺傳算法具有較好的表現

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