天天看點

深度學習與遺傳算法的碰撞——利用遺傳算法優化深度學習網絡結構(詳解與實作)前言優化深度學習分類器的架構隐藏層配置的染色體表示評估個體的适應度得分使用遺傳算法優化MLP架構結果分析

前言

近年來,深度學習模型性能取得了飛躍,可以在單個網絡中使用大量隐藏層。訓練深度學習模型可能會占用大量計算資源,并且通常在圖形處理單元(GPU)上進行,同時為了獲得最優的模型性能,可能需要網絡架構和超參數的反複修改和調整,通常此過程取決于實際問題和網絡架構設計人員的經驗,而利用遺傳算法可以将此過程自動化,同時可以在可接受的時間開銷内找到更好的網絡架構。專門的深度學習庫,例如 TensorFlow,能夠利用基于 GPU 的計算平台,本文使用 MNIST 資料集和 Tensorflow 建構簡單的全連接配接網絡,利用遺傳算法優化隐藏層數和每層的節點數。雖然全連接配接網絡是十分基礎簡單的網絡,但是,使用的原理同樣适用于更複雜的網絡和資料集。

以下是所用庫:

  1. tensorflow2.x
  2. deap
  3. matplotlib

優化深度學習分類器的架構

在建立神經網絡模型可以執行給定的機器學習任務時,一項關鍵工作是設計網絡體系結構的配置。對于多層感覺器,輸入層和輸出層中的節點數取決于目前問題的特征。是以,要做出的選擇是關于隐藏層——有多少層以及每層有多少個節點。可以采用一些經驗進行嘗試,但是在多數情況下,确定最佳架構可能需要反複試驗。

處理網絡體系結構參數的一種方法是将它們視為模型的超參數,使用這些超參數建構網絡,并将訓練後網絡的性能作為适應度進行評價。接下來,将使用遺傳算法找到隐藏層的最佳組合。

深度學習與遺傳算法的碰撞——利用遺傳算法優化深度學習網絡結構(詳解與實作)前言優化深度學習分類器的架構隐藏層配置的染色體表示評估個體的适應度得分使用遺傳算法優化MLP架構結果分析

隐藏層配置的染色體表示

由于 MLP 的體系結構由隐藏層配置決定,在 tensorflow.keras 中可通過改變 Dense 層的 units 參數獲得節點數不同的全連接配接隐藏層:

Dense(units, activation=None,...)      

同時,可以通過 for 來建構所需層數,例如,如果要為 MLP 配置三個有 20 個節點的隐藏層,則可以通過以下方式:

self.model = Sequential()
for l in range(3):  
    self.model.add(layers.Dense(20,activation='relu'))      

是以,我們需要提出既可以表示層數又可以表示每一層節點數的染色體。

同時,為了能夠使用标準遺傳算子,使用固定長度的染色體表示形式。使用這種方法時,預先确定最大層數,但為了層數可變,可以在染色體中設定無效位(也可以稱為終止參數),使模型建構提前終止。例如,将網絡限制為四個隐藏層,則染色體将如下所示:

[n1, n2, n3, n4]

其中,ni 表示 i 層中的節點數。

為了控制網絡中隐藏層的實際數量,其中一些值可能為零或負數。該值意味着之後不會再有其他層添加到網絡:

1. 染色體 [10, 20, -5, 15] 表示元組 (10, 20),因為 -5 是無效位。

2. 染色體 [10, 0, -5, 15] 表示元組 (10, ),因為 0 是無效位。

3. 染色體 [10, 20, 5, -15] 表示元組 (10, 20, 5),因為 -15 是無效位。

4. 染色體 [10, 20, 5, 15] 表示元組 (10, 20, 5, 15)。

為了保證至少有一個隐藏層,可以強制第一個參數始終大于零。其他層參數可以在零附近分布,以便可以控制它們成為終止參數。

另外,由于染色體中值有限定區間,可以選擇使用浮點數清單建構染色體。使用浮點數清單使我們能夠使用現有的遺傳算子。為了建構網絡可以使用round()函數可以将浮點數轉換回整數:

1. 染色體 [9.35, 10.71, -2.51, 17.99] 可以轉化為元組 (9, 11)

2. 染色體 [9.35, 10.71, 2.51, -17.99] 可以轉化為元組 (9, 11, 3)

要評估建構的網絡結構,需要建立實作這些層的 MLP 分類器,對其進行訓練并進行評估。

評估個體的适應度得分

MLPLayers 類封裝了 MNIST 資料集的 MLP 分類器的建構以及模型準确率的評估。

MLPLayers 類主要包括以下方法:

1. preprocess(self,x,y) 用于建構訓練資料集的預處理

2. initDataset(self) 用于建構訓練資料集

3. convertParams(self,params) 将 params 的清單轉換為可以有效構模組化型的元組

4. getAccuracy(self,params) 構模組化型,訓練,并傳回最後一個 epoch 的驗證準确率,用于适應度評估。

5. testLayer(self),使用經驗值建構的分類模型,用于和優化得到的網絡進行對比

6. formatParams(self, params) 用于格式化輸出染色體

class MLPLayers:
    def __init__(self):
        self.initDataset()
   
    def preprocess(self,x,y):
        x = tf.reshape(x, [-1])  
        return x,y
   
    def initDataset(self):
        (self.X_train,self.y_train),(self.X_test,self.y_test) = datasets.mnist.load_data()
       
        self.X_train = tf.convert_to_tensor(self.X_train,dtype=tf.float32) / 255.
        self.X_test = tf.convert_to_tensor(self.X_test,dtype=tf.float32) / 255.
        self.y_train = tf.convert_to_tensor(self.y_train,dtype=tf.int32)
        self.y_test = tf.convert_to_tensor(self.y_test,dtype=tf.int32)
        self.y_train = tf.one_hot(self.y_train,depth=10)
        self.y_test = tf.one_hot(self.y_test,depth=10)
        self.train_db = tf.data.Dataset.from_tensor_slices((self.X_train,self.y_train))
        self.validation_db = tf.data.Dataset.from_tensor_slices((self.X_test,self.y_test))
        self.train_db = self.train_db.shuffle(1000).map(self.preprocess).batch(128)
        self.validation_db = self.validation_db.shuffle(1000).map(self.preprocess).batch(128)
    def convertParams(self,params):
        if round(params[1]) <= 0:
            hiddenLayerSizes = round(params[0]),
        elif round(params[2]) <= 0:
            hiddenLayerSizes = (round(params[0]), round(params[1]))
        elif round(params[3]) <= 0:
            hiddenLayerSizes = (round(params[0]), round(params[1]), round(params[2]))
        else:
            hiddenLayerSizes = (round(params[0]), round(params[1]), round(params[2]), round(params[3]))
        return hiddenLayerSizes
   
    def getAccuracy(self,params):
        #将染色體轉化為可以有效建構網絡的元組
        hiddenLayerSizes = self.convertParams(params)
        self.model = Sequential()
        #建構網絡
        for l in hiddenLayerSizes:
            self.model.add(layers.Dense(l,activation='relu'))
        self.model.add(layers.Dense(10,activation='relu'))
        self.model.build(input_shape=(4,28*28))
        self.model.summary()
        self.model.compile(optimizer=optimizers.Adam(lr=0.01),
                loss=losses.CategoricalCrossentropy(from_logits=True),
                metrics=['accuracy'])
        # 指定訓練集為db,驗證集為val_db,訓練5個epochs,每1個epoch驗證一次
        history = self.model.fit(self.train_db, epochs=5, validation_data=self.validation_db, validation_freq=1,verbose=2)
       
        #傳回最後一個epoch訓練後的驗證準确率,用于适應度評估
        return history.history['val_accuracy'][-1]
    def testLayer(self):
        # 建立5層的全連接配接層網絡
        network = Sequential([layers.Dense(256, activation='relu'),
                            layers.Dense(128, activation='relu'),
                            layers.Dense(64, activation='relu'),
                            layers.Dense(32, activation='relu'),
                            layers.Dense(10)])
        network.build(input_shape=(4, 28*28))
        network.summary()
        # 采用Adam優化器,學習率為0.01;采用交叉熵損失函數,包含Softmax
        network.compile(optimizer=optimizers.Adam(lr=0.01),
                loss=losses.CategoricalCrossentropy(from_logits=True),
                metrics=['accuracy'] # 設定測量名額為準确率
        )
        # 指定訓練集為db,驗證集為val_db,訓練5個epochs,每1個epoch驗證一次
        history = network.fit(self.train_db, epochs=5, validation_data=self.validation_db, validation_freq=1,verbose=2)
       
        #列印結果
        print(history.history['val_accuracy'][-1])
   
    def formatParams(self, params):
        return "'hidden_layer_sizes'={}".format(self.convertParams(params))      

使用遺傳算法優化MLP架構

現在,我們已經有了 MLP 的體系結構配置,以及确定每種配置的 MLP 準确率的方法,接下來,建立基于遺傳算法的優化程式以對配置進行搜尋——隐藏層的數量以及每層中的節點數量——産生最佳分類準确率。

詳細的步驟在注釋中進行介紹:

#建立MlpLayersTest類的執行個體,用于測試隐藏層架構的各種組合
test = MLPLayers()
# 首先為代表隐藏層的每個float值設定上下邊界。第一個隐藏層的範圍為[100,300],而其餘的層則從負值開始,增加終止層數的機會:
BOUNDS_LOW = [100,-25,-50,-75]
BOUNDS_HIGH = [300,200,100,50]
NUM_OF_PARAMS = len(BOUNDS_LOW)
#超參數:
POPULATION_SIZE = 50
P_CROSSOVER = 0.9
P_MUTATION = 0.5
MAX_GENERATIONS = 20
HALL_OF_FAME_SIZE = 5
CROWDING_FACTOR = 10.0
toolbox = base.Toolbox()
#定義最大化适用度政策:
creator.create("FitnessMax",base.Fitness,weights=(1.0,))
#基于清單建立個體類:
creator.create("Individual",list,fitness=creator.FitnessMax)
#由于解由一系列不同區間的浮點值表示,是以我們使用以下循環并為每個區間建立一個單獨的toolbox運算符(layer_size_attribute),用于在适當範圍内生成随機浮點值:
for i in range(NUM_OF_PARAMS):
    #"layer_size_attribute_0","layer_size_attribute_1"...
    toolbox.register("layer_size_attribute_"+str(i),
            random.uniform,
            BOUNDS_LOW[i],
            BOUNDS_HIGH[i])
#建立layer_size_attributes元組,其中包含我們剛剛為每個隐藏層建立的單獨的浮點數生成器:
layer_size_attributes = ()
for i in range(NUM_OF_PARAMS):
    layer_size_attributes = layer_size_attributes + (toolbox.__getattribute__("layer_size_attribute_"+str(i)),)
#将此layer_size_attributes元組與DEAP的内置initCycle()運算符結合使用,以建立一個新的individualCreator運算符,該運算符将随機生成的隐藏層值組合起來填充單個執行個體
toolbox.register("individualCreator",tools.initCycle,creator.Individual,layer_size_attributes,n=1)
#定義種群建立運算符:
toolbox.register("populationCreator",tools.initRepeat,list,toolbox.individualCreator)
#使用類的getAccuracy()方法進行适應度評估
def classificationAccuracy(individual):
    return test.getAccuracy(individual),
toolbox.register("evaluate",classificationAccuracy)
#遺傳算子定義:對于選擇運算符,使用錦标賽大小為2的錦标賽選擇,使用專門用于有界浮動清單染色體的交叉和變異運算符,并為它們提供定義的上下限:
toolbox.register("select",tools.selTournament,tournsize=2)
toolbox.register("mate",tools.cxSimulatedBinaryBounded,low=BOUNDS_LOW,up=BOUNDS_HIGH,eta=CROWDING_FACTOR)
toolbox.register("mutate",tools.mutPolynomialBounded,low=BOUNDS_LOW,up=BOUNDS_HIGH,eta=CROWDING_FACTOR,indpb=1.0/NUM_OF_PARAMS)      

帶精英主義政策的遺傳流程函數

使用名人堂可以用來保留進化過程中種群中曾經存在的最佳個體,并不會由于選擇,交叉或變異而失去了它們,HallOfFame 類在 tools 子產品中實作。

将Halloffame對象用于實作精英主義。 Halloffame對象中包含的個體被直接注入下一代,并且不受選擇,交叉和突變的遺傳算子的影響。

遺傳流程

def main():
    #建立初始種群:
    population = toolbox.populationCreator(n=POPULATION_SIZE)
    #注冊要監聽的統計資料:
    stats = tools.Statistics(lambda ind:ind.fitness.values)
    stats.register("max",np.max)
    stats.register("avg",np.mean)
    #定義名人堂對象:
    hof = tools.HallOfFame(HALL_OF_FAME_SIZE)
    #使用精英主義政策執行遺傳流程:
    population,logbook = eaSimpleWithElitism(population,toolbox,
            cxpb=P_CROSSOVER,mutpb=P_MUTATION,
            ngen=MAX_GENERATIONS,
            stats=stats,halloffame=hof,verbose=True)
   
    # 列印找到的最佳解:
    print("- Best solution is: ",test.formatParams(hof.items[0]),", accuracy = ",hof.items[0].fitness.values[0])
    # 擷取統計資料:
    maxFitnessValues, meanFitnessValues = logbook.select("max", "avg")
if __name__ == "__main__":
    main()      

結果分析

檢視找到的最佳解

- Best solution is:  'hidden_layer_sizes'=(135,) , accuracy =  0.9731000065803528      

可以到,僅使用一層具有 135 個節點的隐藏層,準确率就達到了 97.31.

算法運作過程中統計結果如下:

深度學習與遺傳算法的碰撞——利用遺傳算法優化深度學習網絡結構(詳解與實作)前言優化深度學習分類器的架構隐藏層配置的染色體表示評估個體的适應度得分使用遺傳算法優化MLP架構結果分析

而依靠經驗設計的網絡結構及其準确率如下

Layer (type)                 Output Shape              Param #    
=================================================================
dense_2812 (Dense)           (4, 256)                  200960    
_________________________________________________________________
dense_2813 (Dense)           (4, 128)                  32896      
_________________________________________________________________
dense_2814 (Dense)           (4, 64)                   8256      
_________________________________________________________________
dense_2815 (Dense)           (4, 32)                   2080      
_________________________________________________________________
dense_2816 (Dense)           (4, 10)                   330        
=================================================================
Total params: 244,522
Trainable params: 244,522
Non-trainable params: 0
...
469/469 - 1s - loss: 0.0911 - accuracy: 0.9754 - val_loss: 0.1547 - val_accuracy: 0.9653      

可以看出,相比于精心設計的網絡結構,遺傳算法得到的網絡結構,在 MNIST 資料集上有更高的準确率,雖然提升并不十分明顯,但是考慮到:MNIST 資料集較簡單,以及相比精心設計的網絡的參數量(244522),遺傳算法找到的最佳解的參數量僅為 107335(28*28*135+135*10+135+10),參數量減少一倍以上,可以說遺傳算法的優化已經達到預期。可以通過将更多超參數加入遺傳算法優化的清單中,檢視不同效果。