找回密码
 立即注册
查看: 344|回复: 0

深度学习与遗传算法的碰撞——利用遗传算法优化深度学习 ...

[复制链接]
发表于 2021-12-7 17:55 | 显示全部楼层 |阅读模式
前言

近年来,深度学习模型性能取得了飞跃,可以在单个网络中使用大量隐藏层。训练深度学习模型可能会占用大量计算资源,并且通常在图形处理单元 (Graphics Processing Unit, GPU) 上进行,同时为了获得最优的模型性能,可能需要网络架构和超参数的反复修改和调整,通常此过程取决于实际问题和网络架构设计人员的经验,而利用遗传算法可以将此过程自动化,同时可以在可接受的时间开销内找到更好的网络架构。专门的深度学习库,例如 TensorFlow,能够利用基于 GPU 的计算平台,本文使用 MNIST 数据集和 Tensorflow 构建简单的全连接网络,利用遗传算法优化隐藏层数和每层的节点数。虽然全连接网络是十分基础简单的网络,但是,使用的原理同样适用于更复杂的网络和数据集。
以下是所用库:

  • Tensorflow2.3
  • 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'))因此,我们需要提出既可以表示层数又可以表示每一层节点数的染色体。
同时,为了能够使用标准遗传算子,使用固定长度的染色体表示形式。使用这种方法时,预先确定最大层数,但为了层数可变,可以在染色体中设置无效位(也可以称为终止参数),使模型构建提前终止。例如,将网络限制为四个隐藏层,则染色体将如下所示:


其中, 表示 层中的节点数。
为了控制网络中隐藏层的实际数量,其中一些值可能为零或负数。该值意味着之后不会再有其他层添加到网络:

  • 染色体 [10, 20, -5, 15] 表示元组 (10, 20),因为-5是无效位。
  • 染色体 [10, 0, -5, 15] 表示元组 (10, ),因为0是无效位。
  • 染色体 [10, 20, 5, -15] 表示元组 (10, 20, 5),因为-15是无效位。
  • 染色体 [10, 20, 5, 15] 表示元组 (10, 20, 5, 15)。
为了保证至少有一个隐藏层,可以强制第一个参数始终大于零。其他层参数可以在零附近分布,以便可以控制它们成为终止参数。
另外,由于染色体中值有限定区间,可以选择使用浮点数列表构建染色体。使用浮点数列表使我们能够使用现有的遗传算子。为了构建网络可以使用 round() 函数可以将浮点数转换回整数:

  • 染色体 [9.35, 10.71, -2.51, 17.99] 可以转化为元组 (9, 11)
  • 染色体 [9.35, 10.71, 2.51, -17.99] 可以转化为元组 (9, 11, 3)
要评估构建的网络结构,需要创建实现这些层的 MLP 分类器,对其进行训练并进行评估。
评估个体的适应度得分

MLPLayers 类封装了 MNIST 数据集的 MLP 分类器的构建以及模型准确率的评估。
MLPLayers 类主要包括以下方法:

  • preprocess(self, x, y) 用于构建训练数据集的预处理
  • initDataset(self) 用于构建训练数据集
  • convertParams(self, params) 将 params 的列表转换为可以有效构建模型的元组
  • getAccuracy(self, params) 构建模型,训练,并返回最后一个 epoch 的验证准确率,用于适应度评估。
  • testLayer(self),使用经验值构建的分类模型,用于和优化得到的网络进行对比
  • 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,
            BOUNDS_HIGH)

#创建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 eaSimpleWithElitism(population,
                        toolbox,
                        cxpb,
                        mutpb,
                        ngen,
                        stats=None,
                        halloffame=None,
                        verbose=__debug__):
    """
        使用halloffame来实现精英机制。 包含在名人堂麦中的个体被直接注入下一代,并且不受选择,交叉和突变的遗传算子的影响。
    """
   
    logbook = tools.Logbook()#用于监控算法运行,和统计数据
    logbook.header = ['gen','nevals'] + (stats.fields if stats else [])

    # 计算个体适应度
    invalid_ind = [ind for ind in population if not ind.fitness.valid]
    fitnesses = toolbox.map(toolbox.evaluate,invalid_ind)
    for ind,fit in zip(invalid_ind,fitnesses):
        ind.fitness.values = fit
   
    if halloffame is None:
        raise ValueError("halloffame parameter must not be empty!")
        #更新名人堂成员
    halloffame.update(population)
    hof_size = len(halloffame.items) if halloffame.items else 0

    record = stats.compile(population) if stats else {}
    logbook.record(gen=0,nevals=len(invalid_ind),**record)
    if verbose:
        print(logbook.stream)
   
    #开始遗传流程
    for gen in range(1,ngen + 1):
        #选择个体数目=种群个体数-名人堂成员数
        offspring = toolbox.select(population,len(population) - hof_size)

        #种群更新到下一代
        offspring = algorithms.varAnd(offspring,toolbox,cxpb,mutpb)

        #计算个体适应度
        invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
        fitnesses = toolbox.map(toolbox.evaluate,invalid_ind)
        for ind,fit in zip(invalid_ind,fitnesses):
            ind.fitness.values = fit
        
        #将名人堂成员添加到当前代
        offspring.extend(halloffame.items)

        #更新名人堂
        halloffame.update(offspring)

        #使用当前代替换种群
        population[:] = offspring

        #将当前统计信息附加到日志
        record = stats.compile(population) if stats else {}
        logbook.record(gen=gen,nevals=len(invalid_ind),**record)
        if verbose:
            print(logbook.stream)
        
    return population,logbook遗传流程

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×135+135×10+135+10),参数量减少一倍以上,可以说遗传算法的优化已经达到预期。可以通过将更多超参数加入遗传算法优化的列表中,查看不同效果。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 04:44 , Processed in 0.185077 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表