前言
近年來,深度學習模型性能取得了飛躍,可以在單個網絡中使用大量隐藏層。訓練深度學習模型可能會占用大量計算資源,并且通常在圖形處理單元(GPU)上進行,同時為了獲得最優的模型性能,可能需要網絡架構和超參數的反複修改和調整,通常此過程取決于實際問題和網絡架構設計人員的經驗,而利用遺傳算法可以将此過程自動化,同時可以在可接受的時間開銷内找到更好的網絡架構。專門的深度學習庫,例如 TensorFlow,能夠利用基于 GPU 的計算平台,本文使用 MNIST 資料集和 Tensorflow 建構簡單的全連接配接網絡,利用遺傳算法優化隐藏層數和每層的節點數。雖然全連接配接網絡是十分基礎簡單的網絡,但是,使用的原理同樣适用于更複雜的網絡和資料集。
以下是所用庫:
- tensorflow2.x
- deap
- matplotlib
優化深度學習分類器的架構
在建立神經網絡模型可以執行給定的機器學習任務時,一項關鍵工作是設計網絡體系結構的配置。對于多層感覺器,輸入層和輸出層中的節點數取決于目前問題的特征。是以,要做出的選擇是關于隐藏層——有多少層以及每層有多少個節點。可以采用一些經驗進行嘗試,但是在多數情況下,确定最佳架構可能需要反複試驗。
處理網絡體系結構參數的一種方法是将它們視為模型的超參數,使用這些超參數建構網絡,并将訓練後網絡的性能作為适應度進行評價。接下來,将使用遺傳算法找到隐藏層的最佳組合。
隐藏層配置的染色體表示
由于 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.
算法運作過程中統計結果如下:
而依靠經驗設計的網絡結構及其準确率如下
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),參數量減少一倍以上,可以說遺傳算法的優化已經達到預期。可以通過将更多超參數加入遺傳算法優化的清單中,檢視不同效果。