遗传算法与深度学习实战(27)——进化卷积神经网络

0. 前言

DEAP toolbox 中提供的标准遗传操作符对于自定义的网络架构基因序列来说是不够的。这是因为任何标准的交叉算子都可能破坏卷积神经网络 (Convolutional Neural Network, CNN) 的基因序列的格式。相反,为了构建进化卷积神经网络,我们需要为交叉和突变都构建自定义遗传算子。在本节中,我们首先介绍如何自定义交叉和突变算子,然后基于自定义遗传算子,构建进化卷积神经网络 (Evolutionary Convolutional Neural Network, EvoCNN)。

1. 自定义交叉算子

下图展示了如何将自定义交叉算子应用于父代双亲,该操作通过将两个父本中的各种层集合提取到不同列表中——一个用于卷积,一个用于池化等等。从每个列表中,随机选择一对层进行基因序列之间的交换,生成的基因序列为后代基因。

遗传算法与深度学习实战(27)——进化卷积神经网络-LMLPHP

这只是执行交叉操作的一种方式,我们也可以考虑使用其他方式,重要的是保证在交叉操作后基因序列保持正确的格式。

(1) 首先,导入所需库、超参数、数据集以及辅助函数:

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
import numpy as np
import math
import time
import random

import matplotlib.pyplot as plt
from livelossplot import PlotLossesKeras

dataset = datasets.fashion_mnist
(x_train, y_train), (x_test, y_test) = dataset.load_data()

# normalize and reshape data
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1).astype("float32") / 255.0
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1).astype("float32") / 255.0

x_train = x_train[:1000]
y_train= y_train[:1000]
x_test = x_test[:100]
y_test= y_test[:100]

class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
               'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']

def plot_data(num_images, images, labels):
    grid = math.ceil(math.sqrt(num_images))
    plt.figure(figsize=(grid*2,grid*2))
    for i in range(num_images):
        plt.subplot(grid,grid,i+1)
        plt.xticks([])
        plt.yticks([])
        plt.grid(False)     
        plt.imshow(images[i].reshape(28,28))
        plt.xlabel(class_names[labels[i]])      
    plt.show()

plot_data(25, x_train, y_train)

max_layers = 5
max_neurons = 128
min_neurons = 16
max_kernel = 5
min_kernel = 2
max_pool = 3
min_pool = 2

CONV_LAYER = -1
CONV_LAYER_LEN = 4
POOLING_LAYER = -2
POOLING_LAYER_LEN = 3
BN_LAYER = -3
BN_LAYER_LEN = 1
DENSE_LAYER = -4
DENSE_LAYER_LEN = 2

def generate_neurons():
    return random.randint(min_neurons, max_neurons)

def generate_kernel():
    part = []
    part.append(random.randint(min_kernel, max_kernel))
    part.append(random.randint(min_kernel, max_kernel))
    return part

def generate_bn_layer():
    part = [BN_LAYER] 
    return part

def generate_pooling_layer():
    part = [POOLING_LAYER] 
    part.append(random.randint(min_pool, max_pool))
    part.append(random.randint(min_pool, max_pool))
    return part

def generate_dense_layer():
    part = [DENSE_LAYER] 
    part.append(generate_neurons())  
    return part

def generate_conv_layer():
    part = [CONV_LAYER] 
    part.append(generate_neurons())
    part.extend(generate_kernel())
    return part

def create_offspring():
    ind = []
    for i in range(max_layers):
        if random.uniform(0,1)<.5:
            #add convolution layer
            ind.extend(generate_conv_layer())
            if random.uniform(0,1)<.5:
                #add batchnormalization
                ind.extend(generate_bn_layer())
            if random.uniform(0,1)<.5:
                #add max pooling layer
                ind.extend(generate_pooling_layer())
    ind.extend(generate_dense_layer())
    return ind
        
individual = create_offspring()
print(individual)

(2) 定义函数 build_model() 用于根据基因创建神经网络模型:

def build_model(individual):
    model = models.Sequential()
    il = len(individual)
    i = 0
    while i < il:
        if individual[i] == CONV_LAYER: 
            n = individual[i+1]
            k = (individual[i+2], individual[i+3])
            i += CONV_LAYER_LEN
            if i == 0: #first layer, add input shape      
                model.add(layers.Conv2D(n, k, activation='relu', padding="same", input_shape=(28, 28, 1)))      
            else:
                model.add(layers.Conv2D(n, k, activation='relu', padding="same"))    
        elif individual[i] == POOLING_LAYER: #add pooling layer
            k = k = (individual[i+1], individual[i+2])
            i += POOLING_LAYER_LEN
            model.add(layers.MaxPooling2D(k, padding="same"))      
        elif individual[i] == BN_LAYER: #add batchnormal layer
            model.add(layers.BatchNormalization())
            i += 1      
        elif individual[i] == DENSE_LAYER: #add dense layer
            model.add(layers.Flatten())      
            model.add(layers.Dense(individual[i+1], activation='relu'))
            i += 2
    model.add(layers.Dense(10))
    return model

model = build_model(individual) 

(3) get_layers() 函数用于从每个基因序列中提取网络层索引,可以使用一个列表推导式来完成此任务,通过检查序列中的每个值并提取列表中的匹配位置:

def get_layers(ind, layer_type):
    return [a for a in range(len(ind)) if ind[a] == layer_type]

(4) swap() 它负责交换每个个体的网络层块。swap() 函数通过从给定索引处提取序列中的每个网络层块进行交换。由于网络层类型的长度始终相同,因此可以简单的使用索引替换。需要注意的是,如果网络层块长度可变,就需要使用其它复杂的解决方案:

def swap(ind1, iv1, ind2, iv2, ll):
    ch1 = ind1[iv1:iv1+ll]
    ch2 = ind2[iv2:iv2+ll]
    print(ll, iv1, ch1, iv2, ch2)
    ind1[iv1:iv1+ll] = ch2
    ind2[iv2:iv2+ll] = ch1
    return ind1, ind2

(5) swap_layers() 函数是从序列中提取每种网络层类型并进行随机交换的地方,首先根据每个序列获取网络层的类型列表,c1c2 都是索引列表,通过循环确定交换点。从这些列表中,我们随机选择一个值来交换每个序列,并使用 swap() 函数执行交换:

def swap_layers(ind1, ind2, layer_type, layer_len):
    c1, c2 = get_layers(ind1, layer_type), get_layers(ind2, layer_type) 
    min_c = min(len(c1), len(c2))
    for i in range(min_c):
        if random.random() < 1:
            i1 = random.randint(0, len(c1)-1)
            i2 = random.randint(0, len(c2)-1)      
            iv1 = c1.pop(i1)
            iv2 = c2.pop(i2)    
            ind1, ind2 = swap(ind1, iv1, ind2, iv2, layer_len) 
    return ind1, ind2 

(6) 交叉函数 crossover() 为每组网络层调用 swap_layers() 函数:

def crossover(ind1, ind2):
    ind1, ind2 = swap_layers(ind1, ind2, CONV_LAYER, CONV_LAYER_LEN)
    ind1, ind2 = swap_layers(ind1, ind2, POOLING_LAYER, POOLING_LAYER_LEN)
    ind1, ind2 = swap_layers(ind1, ind2, BN_LAYER, BN_LAYER_LEN)
    ind1, ind2 = swap_layers(ind1, ind2, DENSE_LAYER, DENSE_LAYER_LEN)
    return ind1, ind2 

ind1 = create_offspring()
ind2 = create_offspring()
print(ind1)
print(ind2)

ind1, ind2 = crossover(ind1, ind2)
print(ind1)
print(ind2)

下图展示了在两个父代上执行 crossover() 函数后的结果,从中可以看出,交换了三个卷积层、一个池化层、一个批归一化层和一个全连接层组。

遗传算法与深度学习实战(27)——进化卷积神经网络-LMLPHP

(7) 构建、编译、训练生成的个体,并输出结果。观察输出结果,确保交叉操作不会破坏基因序列的格式。现在,我们已经有了用于交叉和产生后代的交叉算子,接下来,将继续研究突变算子:

model = build_model(ind2) 

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

history = model.fit(x_train, y_train, epochs=3, 
                    validation_data=(x_test, y_test),
                    callbacks=[PlotLossesKeras()],
                    verbose=0)

model.summary()
model.evaluate(x_test, y_test)

2. 自定义突变操作符

DEAP 中提供的标准变异操作符对于我们的自定义基因序列是无效的。因此,我们需要自定义变异操作符来模拟对基因序列应用的突变操作。在本节中,我们保持突变相对简单,只改变当前的网络层块。在更高级的应用中,突变可以删除或添加新的网络层块。

(1) 突变函数 mutation() 首先检查个体是否为空,如果不为空,对每个网络层组执行突变。最后,按照 DEAP 约定,以元组形式返回结果:

def mutate(part, layer_type):
    if layer_type == CONV_LAYER and len(part)==CONV_LAYER_LEN:
        part[1] = int(part[1] * random.uniform(.9, 1.1))
        part[2] = random.randint(min_kernel, max_kernel)
        part[3] = random.randint(min_kernel, max_kernel)
    elif layer_type == POOLING_LAYER and len(part)==POOLING_LAYER_LEN:
        part[1] = random.randint(min_kernel, max_kernel)
        part[2] = random.randint(min_kernel, max_kernel)
    elif layer_type == DENSE_LAYER and len(part)==DENSE_LAYER_LEN:
        part[1] = int(part[1] * random.uniform(.9, 1.1)) 
    else:
        error = f"mutate ERROR {part}"    
        raise Exception(error) 
    return part

(2) mutate_layers() 函数循环遍历特定类型的网络层组,并仅相应的超参数发生突变。首先使用 get_layers() 函数提取给定类型的网络层组索引。然后,在 try/except 块中,通过调用 mutate() 函数替换给定索引的网络层块应用突变:

def mutate_layers(ind, layer_type, layer_len):
    layers = get_layers(ind1, layer_type)
    for layer in layers:
        if random.random() < 1:
            try:
                ind[layer:layer+layer_len] = mutate(
                    ind[layer:layer+layer_len], layer_type) 
            except:
                print(layers)
    return ind      

(3) mutate() 函数首先检查提取的部分是否具有正确的长度,这是为了防止个体发生潜在的格式损坏问题。接下来,根据网络层类型,可以改变滤波器的数量和卷积核大小。需要注意的是,我们将卷积核大小限制在给定最小/最大范围内的值,但允许滤波器的数量增加或减少。此时,还检查个体基因序列是否有任何损坏的块,即不匹配所需长度的块。如果在突变过程中发现基因序列损坏,则会抛出异常,异常会在 mutation() 函数中捕获到:

def mutation(ind):  
    if len(ind) > CONV_LAYER_LEN: #only mutate conv individuals
        ind = mutate_layers(ind, CONV_LAYER, CONV_LAYER_LEN)
        ind = mutate_layers(ind, DENSE_LAYER, DENSE_LAYER_LEN)
        ind = mutate_layers(ind, POOLING_LAYER, POOLING_LAYER_LEN)
    return ind,

print(ind1)
ind1 = mutation(ind1)[0]
print(ind1)

下图显示了在个体基因序列上执行突变函数的结果。可以看到,只有定义网络层组的滤波器数量或卷积核大小的超参数被修改。

遗传算法与深度学习实战(27)——进化卷积神经网络-LMLPHP

构建、编译和训练突变的基因序列,以确认我们仍然可以生成一个有效的 Keras 模型。多次执行突变操作,以确认输出的基因序列是有效的。我们已经学习了构建用于处理交叉和突变操作的自定义运算符,接下来,我们继续应用进化算法。
Keras 的模型编译具有健壮性和宽容性,这在我们随机构建的一些模型可能存在问题并且无法产生良好结果时非常有用。相比之下,像 PyTorch 这样的框架宽容性较差,并且可能会对构建问题提产生阻塞错误。使用 Keras,我们可以进行最小的错误处理,因为大多数模型都可以运行;然而,它们可能运行效果不佳。如果我们在 PyTorch 上应用相同的进化算法,可能会遇到更多的构建问题,因为它对一些较小的问题也非常敏感,导致更少的后代幸存下来。相反,Keras 将产生更多可行的后代,可能发展成为更合适的解决方案。

3. 进化卷积神经网络

我们已经了解了自定义运算符的工作原理,在本节中,我们将其扩展为执行进化架构搜索,实现进化卷积神经网络 (Evolutionary Convolutional Neural Network, EvoCNN)。

(1) 设置 DEAP toolbox,重用 create_offspring() 函数,并在 toolbox 注册为 network,用于创建新的后代。然后,使用列表来保存个体基因序列,使用列表能够创建一组基因长度不同的个体:

creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
creator.create("Individual", list, fitness=creator.FitnessMin)

toolbox = base.Toolbox()
toolbox.register("network", create_offspring)
toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.network)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)

toolbox.register("select", tools.selTournament, tournsize=5)

(2) 注册自定义的交叉和突变函数:

toolbox.register("mate", crossover)
toolbox.register("mutate", mutation)

(3) 训练网络,在 compile_train() 函数中,将训练固定为 3epochs

def clamp(num, min_value, max_value):
    return max(min(num, max_value), min_value)

def compile_train(model):
    model.compile(optimizer='adam',
                loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
                metrics=['accuracy'])
    model.fit(x_train, y_train, epochs=3,                     
                        verbose=0)
    return model

(4) 构建、编译、训练并评估模型。evaluate() 函数首先使用 build_model() 函数构建模型,然后使用 compile_train() 函数编译和训练模型,之后,返回值 1/accuracy (accuracy 范围在 01 之间),这样做是因为我们希望通过 1/accuracy 来最小化适应度。需要注意的是,使用 try/except 语句将代码包装起来,以确保在任何失败情况下都能优雅地恢复。由于代码仍然可能构建出毫无意义的模型,使用 try/except 语句可以防止失败。如果模型构建失败,返回 1/.5。通过这种方式,能够将这些失败个体保留在种群中,并在之后突变为更好的解决方案:

def evaluate(individual):  
    try:
        model = build_model(individual)
        model = compile_train(model)
        print('.', end='')    
        return 1/clamp(model.evaluate(x_test, y_test, verbose=0)[1], .00001, 1),
    except:
        return 1/.5,  

toolbox.register("evaluate", evaluate)   

适者生存,通过给失败的个体一定的基础适应度,使这些基因序列有机会留在种群池中。

(5) 设置进化过程,并可视化进化输出,由于基因序列相对较小,通常可以快速收敛,准确率大约为 81%。可以尝试增加种群的规模或代数的数量,查看其对结果的影响:

MU = 10 #@param {type:"slider", min:5, max:1000, step:5}
NGEN = 5 #@param {type:"slider", min:5, max:100, step:1}
RGEN = 1 #@param {type:"slider", min:1, max:100, step:1}
CXPB = .6
MUTPB = .3

pop = toolbox.population(n=MU)
hof = tools.HallOfFame(1)
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("std", np.std)
stats.register("min", np.min)
stats.register("max", np.max)

best = None
groups = { "fitness" : {"min", "max"}}
plotlosses = PlotLosses(groups=groups)

for g in range(NGEN):
    pop, logbook = algorithms.eaSimple(pop, toolbox, 
                cxpb=CXPB, mutpb=MUTPB, ngen=RGEN, stats=stats, halloffame=hof, verbose=False)
    best = hof[0] 
    
    print(f"Gen ({(g+1)*RGEN})")      
    for l in logbook:
        plotlosses.update({'min': 1/l["max"], 'max': 1/l["min"]})
    plotlosses.send()  # draw, update logs, etc

遗传算法与深度学习实战(27)——进化卷积神经网络-LMLPHP

(6) 进化完成后,构建、编译和训练最佳个体,查看结果,可以看到在 3epochs 后模型仍存在过拟合情况,这表明如果我们想要一个泛化能力更高的模型,可能需要增加训练 epochs,但会极大的增加演化时间:

build_compile_train(best, epochs=5)

遗传算法与深度学习实战(27)——进化卷积神经网络-LMLPHP

我们可以根据需要修改代码,并添加优化超参数:

  • 数据集大小:在本节中,我们大幅减小了原始数据集的大小以减少运行时间。如果增加数据集的大小,相应的运行时间也会增加
  • 训练 epochs:在本节中,我们将训练限制为 3epochs。根据数据集规模,可能需要增加或减少 epochs
  • 层类型:在本节中,我们只使用了标准层类型,如卷积、池化、批归一化和全连接层。我们也可以添加不同的层类型,如 dropout,或增加全连接层的数量等
  • 交叉/突变:在本节中,我们实现了自定义交叉和突变运算符。除了这种方式外,我们仍有进一步定制化的空间,例如在突变过程中添加或删除网络层块
  • 适应度/评估函数:本节中,个体的适应度基于准确率得分。如果我们想要最小化可训练参数或网络层数,也可以将其作为逻辑加入到 evaluate() 函数中

小结

卷积神经网络 (Convolutional Neural Network, CNN) 的设置和定义对于各种图像识别任务来说较为复杂的,通常得到最佳 CNN 超参数需要花费大量时间分析和调整。使用遗传算法进化一组个体,能够优化特定数据集上的 CNN 模型体系结构。本节中,介绍了自定义交叉和突变算子的构建方式,并使用自定义遗传算子实现进化卷积神经网络 (Evolutionary Convolutional Neural Network, EvoCNN)。

系列链接

遗传算法与深度学习实战(1)——进化深度学习
遗传算法与深度学习实战(2)——生命模拟及其应用
遗传算法与深度学习实战(3)——生命模拟与进化论
遗传算法与深度学习实战(4)——遗传算法(Genetic Algorithm)详解与实现
遗传算法与深度学习实战(5)——遗传算法中常用遗传算子
遗传算法与深度学习实战(6)——遗传算法框架DEAP
遗传算法与深度学习实战(7)——DEAP框架初体验
遗传算法与深度学习实战(8)——使用遗传算法解决N皇后问题
遗传算法与深度学习实战(9)——使用遗传算法解决旅行商问题
遗传算法与深度学习实战(10)——使用遗传算法重建图像
遗传算法与深度学习实战(11)——遗传编程详解与实现
遗传算法与深度学习实战(12)——粒子群优化详解与实现
遗传算法与深度学习实战(13)——协同进化详解与实现
遗传算法与深度学习实战(14)——进化策略详解与实现
遗传算法与深度学习实战(15)——差分进化详解与实现
遗传算法与深度学习实战(16)——神经网络超参数优化
遗传算法与深度学习实战(17)——使用随机搜索自动超参数优化
遗传算法与深度学习实战(18)——使用网格搜索自动超参数优化
遗传算法与深度学习实战(19)——使用粒子群优化自动超参数优化
遗传算法与深度学习实战(20)——使用进化策略自动超参数优化
遗传算法与深度学习实战(21)——使用差分搜索自动超参数优化
遗传算法与深度学习实战(22)——使用Numpy构建神经网络
遗传算法与深度学习实战(23)——利用遗传算法优化深度学习模型
遗传算法与深度学习实战(24)——在Keras中应用神经进化优化
遗传算法与深度学习实战(25)——使用Keras构建卷积神经网络
遗传算法与深度学习实战(26)——编码卷积神经网络架构

12-13 07:07