Engee 文档
Notebook

Flappy Bird AI:Julia上神经网络的进化学习

Flappy Bird是一款标志性的手机游戏,由于其简单和清晰的规则,它已成为演示机器学习原理的理想试验场。 在这个项目中,我们不仅实现了游戏本身,而且还实现了两种不同的神经网络架构,这些架构经过训练,可以使用进化算法来玩游戏。

项目的主要特点:

-在Julia标准库上完全实现,无需外部依赖
-游戏在终端中的符号渲染
-两种神经网络架构:全连接(DenseNet)和卷积(CNN)
-学习的遗传算法
-用于管理培训和测试的交互式菜单

该项目演示了如何,即使只使用编程语言的基本功能,也可以创建一个完整的机器学习系统。 飞扬的鸟不是偶然选择的-它是一个理想的试验台。:

-简单的规则,但困难的技能
-清晰的奖励系统(通过管道飞行的积分)
-离散动作(跳或不跳)
-视觉上可理解的学习进度

在命令行上使用符号图形表明,演示机器学习概念不需要复杂的图形库-算法和原理本身很重要。

技术栈:

-Julia作为科学计算的高性能语言
-遗传算法作为无梯度优化方法
-神经网络作为通用近似值
-最大可移植性的控制台界面

这个项目的目的是"从内部"展示神经网络和进化算法是如何工作的,没有深入学习框架的魔力。

现在让我们继续到项目的实际实施。 Flappy Bird实现基于两个封装所有游戏对象状态的关键数据结构,以及一组确定游戏物理和平衡的全局参数。 这种简约的方法是典型的游戏引擎,其中数据和逻辑的清晰分离确保了可预测性和易于调试。

鸟对象是一个受控的鸟,游戏的主角。 它的字段存储全动态状态:垂直位置y(平滑度的小数),速度v(正向下,负向上),当前分数s(通过的管道的整数个数)和布尔活动标志a,指示鸟是否活着。 这四个参数足以描述鸟类运动的整个机制-它对重力的反应,跳跃的冲动以及与边界的相互作用。

每个管道由包含三个参数的管道结构表示:水平x坐标(跟踪管道相对于鸟的位置),垂直通道位置gy(确定安全间隙在哪里)和通道标志p(消除重复评分)。 管道向鸟移动,创造了游戏的主要挑战-需要准确地通过狭窄的间隙飞行。

GP数组包含七个数字常量,定义游戏世界的几何和物理。 这些是领域的宽度和高度,重力和跳跃的力量,管道的大小,以及它们的运动速度。 这种集中式配置使得在不影响底层逻辑的情况下,可以轻松平衡游戏的复杂性并对参数进行实验。

Init_game函数创建一个起始状态,以零速度和空管道列表将鸟返回到屏幕中心。 这个简单的过程确保了每个游戏情节的可重复性,这对于学习过程至关重要—神经网络必须从一致的,确定性的例子中学习。

这些组件一起形成了一个紧凑但完整的游戏模型,其中每个参数都具有明确的作用,并且对象的交互由一组最小的规则来描述。

In [ ]:
mutable struct Bird
    y::Float64
    v::Float64
    s::Int
    a::Bool
end
mutable struct Pipe
    x::Float64
    gy::Int
    p::Bool
end

const GP = [40.0, 15.0, 0.4, -2.0, 4.0, 6.0, 1.2]
function init_game()
    Bird(GP[2] ÷ 2, 0.0, 0, true), Pipe[]
end
Out[0]:
init_game (generic function with 1 method)

功能 update_bird! 通过处理两个关键影响来实现鸟类运动的物理学:重力和跳跃。 每个游戏框架,鸟在跳跃时要么得到向上的提升,要么从重力中得到额外的向下加速度,这就产生了类似于原始游戏的特征性"跳跃"机制。

垂直速度首先更新:如果信号被发送 jump,速度设置为等于跳跃的力(向上运动的负值),否则重力被添加到当前速度。 然后鸟的位置变化的速度,创造一个平稳的运动。

接下来,该函数检查比赛场地的边界。 如果鸟下降到下限以下,它的位置固定在最小值,并且速度被重置,防止它离开屏幕。 如果鸟上升到上限以上,则视为失败-活动标志设置为 false,这将结束游戏插曲。 这些检查确保鸟在密闭空间内的正确行为。

In [ ]:
function update_bird!(b, jump)
    b.v = jump ? GP[4] : b.v + GP[3]
    b.y += b.v
    if b.y < 1
        b.y = 1; b.v = 0
    elseif b.y > GP[2]
        b.a = false
    end
end
Out[0]:
update_bird! (generic function with 1 method)

功能 update_pipes! 负责障碍物的生命周期-它们的运动,与玩家的互动以及新管道的产生。 这是创建连续游戏玩法和评分系统的关键机制。

首先,该功能移除超出屏幕左侧边框完全消失的管道。 它支持一个数组 pipes 通过删除不再参与游戏的对象来保持最新状态。 然后,对于每个剩余的管道,它以恒定的速度向左移动,创造了一只鸟向前移动的错觉。 与此同时,检查鸟是否已经通过管道。 如果管道已经越过检查点并且之前没有被计数,则玩家的分数增加,并且管道被标记为通过。

新管道的产生基于保持障碍物恒定流动的原则。 如果没有管道或最后一个管道移动得足够远,则会在屏幕右侧创建一个新管道。 其垂直通过位置在可接受的范围内随机选择,提供多种轨迹。 这种机制保证了无尽的和不可预测的游戏性,这是学习适应性行为所必需的。

In [ ]:
function update_pipes!(pipes, b)
    filter!(p -> p.x > -GP[5], pipes)
    for p in pipes
        p.x -= GP[7]
        if !p.p && p.x < 5
            p.p = true
            b.s += 1
        end
    end
    if isempty(pipes) || pipes[end].x < GP[1] - 20
        push!(pipes, Pipe(GP[1], rand(4:Int(GP[2]) - Int(GP[6]) - 2), false))
    end
end
Out[0]:
update_pipes! (generic function with 1 method)

功能 check_collision 实现了鸟碰撞与管道的检测,这是决定游戏成功的关键机制。 该算法检查鸟是否在与任何活动管道的碰撞区,并在检测到触摸时结束游戏。

首先,鸟的位置四舍五入为整数以简化计算,该整数对应于终端中的离散渲染网格。 然后对每个管道检查水平重叠:如果鸟(位于固定水平位置5处)在管道宽度内,则启动垂直碰撞检查。

垂直检查确定鸟是否进入了管道之间的安全间隙。 如果鸟的垂直位置在上部管道上方或下部管道下方,则记录碰撞。 在这种情况下,鸟类活动标志设置为 false,从而立即结束游戏周期。 该函数提前返回控制,因为碰撞后不需要进一步检查。

In [ ]:
function check_collision(b, pipes)
    by = round(Int, b.y)
    for p in pipes
        if 5 >= p.x && 5 <= p.x + GP[5] - 1
            if by < p.gy || by > p.gy + GP[6] - 1
                b.a = false
                return
            end
        end
    end
end
Out[0]:
check_collision (generic function with 1 method)

功能 extract_state 执行将游戏状态转换为适合由神经网络处理的格式的关键任务。 它实现了两种根本不同的数据表示方法,对应于两种类型的神经网络架构。

该算法首先搜索尚未被鸟通过并且位于其前面的最近的无法通行的管道。 如果没有这样的管道(在游戏开始时或在所有管道之后),则返回默认状态:完全连接网络的零向量,或者卷积网络的中心有鸟的地图。

对于卷积网络 (state_type == :conv) 创建了一个比赛场地大小的二维矩阵,这是一种"可见性地图"。 鸟的位置以1.0的值标记,并且管道在它们占据空间的那些笼子中以0.5的值标记。 这种栅格方法允许卷积网络独立地识别空间模式和对象之间的关系。

对于完全连接的网络 (state_type == :basic) 形成了五个归一化特征的紧凑向量:鸟的相对垂直位置,其速度(受范围[-1,1]限制),到最近管道的相对水平距离,以及安全通道的上下边界。 这种表示提取决策最相关的参数,为网络提供已经准备好的高级特征。

关键方面是将所有值归一化到[0.1]或-1,1]周围的范围,这加速并稳定了神经网络的训练,防止了梯度问题并确保了优化算法的收敛。

In [ ]:
function extract_state(b, pipes, state_type=:basic)
    closest = nothing
    for p in pipes
        if !p.p && p.x + GP[5] >= 5
            closest = p
            break
        end
    end
    
    if closest === nothing
        if state_type == :conv
            state = zeros(Float64, Int(GP[2]), Int(GP[1]))
            state[Int(GP[2])÷2, 5] = 1.0
            return state
        else
            return zeros(Float64, 5)
        end
    end
    
    if state_type == :conv
        state = zeros(Float64, Int(GP[2]), Int(GP[1]))
        by = clamp(round(Int, b.y), 1, Int(GP[2]))
        state[by, 5] = 1.0
        
        for p in pipes
            px = round(Int, p.x)
            start_x = max(1, px)
            end_x = min(Int(GP[1]), px + Int(GP[5]) - 1)
            
            if start_x <= end_x
                for x in start_x:end_x
                    for y in 1:Int(GP[2])
                        if y < p.gy || y > p.gy + Int(GP[6]) - 1
                            state[y, x] = 0.5
                        end
                    end
                end
            end
        end
        return state
    else
        return [
            b.y / GP[2],
            clamp(b.v / 5.0, -1.0, 1.0),
            (closest.x - 5) / GP[1],
            closest.gy / GP[2],
            (closest.gy + GP[6]) / GP[2]
        ]
    end
end
Out[0]:
extract_state (generic function with 2 methods)

run_episode() —这是执行整个游戏周期以评估神经网络的中心函数。 它封装了游戏环境和AI代理之间交互的逻辑。

该函数从初始化一个新的游戏状态开始-创建一只鸟和一个空的管道列表。 然后主游戏周期开始,只要鸟活着并且不超过步数限制,这种方法就会阻止无休止的游戏。 在每一步,以对应于网络架构的格式提取游戏的当前状态(basic 完全连接或 conv 为卷积)。

神经网络接收此状态并返回跳转的概率。 0.5的阈值将连续概率转换为二元解。: true 为了跳跃, false 继续下跌。 此操作应用于鸟,管道位置更新,并检查碰撞。

如果启用了渲染模式(render=true),在每个步骤中,游戏的图形表示在终端中以轻微的延迟显示,以便于观察。 完成后,该函数返回最终分数—成功完成的管道数,作为神经网络有效性的估计。

In [ ]:
function run_episode(network, state_type=:basic; render=false, max_steps=1000)
    bird, pipes = init_game()
    step = 0
    
    while bird.a && step < max_steps
        state = extract_state(bird, pipes, state_type)
        
        if state_type == :conv
            action = conv_forward(network, state) > 0.5
        else
            action = dense_forward(network, state) > 0.5
        end
        
        update_bird!(bird, action)
        update_pipes!(pipes, bird)
        check_collision(bird, pipes)
        
        if render
            draw(bird, pipes)
            sleep(0.1)
        end
        
        step += 1
    end
    
    return bird.s
end
Out[0]:
run_episode (generic function with 2 methods)

draw() 实现控制台图形以可视化游戏。 此功能仅使用标准终端功能来创建伪图界面,表明即使没有专门的库,也可以创建游戏的可视化表示。

主循环贯穿比赛场地的每一行。 h. 对于每一行,显示左边框"/",然后检查鸟是否在这个高度。 如果是,则显示表情符号"🤖",否则显示空格。 接下来,对于每个内部柱(边界区域除外),检查该位置是否位于管道内部。 如果是,并且该位置不落在管道之间的安全间隙内,则显示表示障碍物的"△"符号。

绘制整个字段后,显示下边框和游戏状态。 如果鸟已经死了,则会显示一条关于游戏结束的消息,其中包含表情符号"💀"和最终得分。

In [ ]:
function draw(b, pipes)
    print("\033[2J\033[H")
    w, h = Int(GP[1]), Int(GP[2])
    println("="^w)
    println("飞扬的鸟AI/帐户:$(b.s)")
    println("="^w)
    
    for y in 1:h
        print("|")
        print(y == round(Int, b.y) ? "🤖" : " ")
        
        for x in 7:w-2
            is_pipe = false
            for p in pipes
                px = round(Int, p.x)
                if x >= px && x < px + Int(GP[5])
                    if y < p.gy || y > p.gy + Int(GP[6]) - 1
                        print("█")
                        is_pipe = true
                        break
                    end
                end
            end
            !is_pipe && print(" ")
        end
        println("|")
    end
    
    println("="^w)
    println("测试运行")
    !b.a && println("\n💀游戏结束了! 帐户:$(b.s)")
end
Out[0]:
draw (generic function with 1 method)

该项目实现了两种神经网络架构,每种架构都由自己的数据结构表示。 这些结构封装了所有可训练的网络参数,这使得在进化学习期间有效地操纵它们成为可能。

mutable struct DenseNet 描述具有一个隐藏层的全连接神经网络。 它包含一个权重矩阵 w1 8×5的大小对于输入和隐藏层之间的连接,位移矢量 b1 对于隐藏层,权重矩阵 w2 隐藏层和输出层之间的连接尺寸为1×8,标量偏移 b2 为输出神经元。 这种结构反映了多层感知器的经典架构。

mutable struct ConvNet 表示卷积网络。 它包括一个四维数组 conv_weights 对于卷积核(3×3×1×4,其中4是滤波器的数量),位移向量 conv_bias 对于卷积层,矩阵 fc_weights 对于全连接层和矢量 fc_bias 为了它的偏移。 该框架支持现代图像处理方法。

In [ ]:
mutable struct DenseNet
    w1::Matrix{Float64}
    b1::Vector{Float64}
    w2::Matrix{Float64}
    b2::Vector{Float64}
end

mutable struct ConvNet
    conv_weights::Array{Float64,4}
    conv_bias::Vector{Float64}
    fc_weights::Matrix{Float64}
    fc_bias::Vector{Float64}
end

function init_dense()
    DenseNet(
        randn(8, 5) * 0.1,
        zeros(8),
        randn(1, 8) * 0.1,
        zeros(1)
    )
end
function init_conv()
    height, width = 15, 40
    filters = 4
    conv_size = (height - 2) * (width - 2) * filters
    ConvNet(
        randn(3, 3, 1, filters) * 0.1,
        zeros(filters),
        randn(1, conv_size) * 0.1,
        zeros(1)
    )
end
Out[0]:
init_dense (generic function with 1 method)

dense_forward()conv_forward() 它们为两种不同的神经网络架构实现直接传播。 这些功能是系统的计算核心,将游戏状态转换为动作决策。

全连接网络(dense_forward)与五个特征的紧凑向量一起工作。 它在权重方面执行线性变换 w1,添加偏移量 b1,应用非线性 tanh 对于隐藏层,然后通过执行第二个线性变换 w2b2,最后通过sigmoid将结果转换为概率。 这种两层架构对于处理少量高级特征是有效的。

卷积网络(conv_forward)处理游戏状态的二维矩阵。 它通过使用嵌套循环将多个3x3过滤器应用于地图上的所有可能位置来执行卷积操作。 对于每个滤波器,计算滤波器核心和输入图像的相应部分之间的点积,添加偏移,并且应用ReLU激活函数(max(..., 0.0)然后将卷积结果"展开"为一维向量,并通过输出处具有sigmoid激活的全连接层。

这两种方法之间的关键区别在于,完全连接的网络接收预处理的高级特征(位置、速度、到管道的距离),而卷积网络使用比赛场地的"原始"像素,并学会自行识别相关模式。 这两个函数都返回范围(0,1)中跳跃的概率,然后将其转换为阈值为0.5的二进制解。

In [ ]:
function dense_forward(net::DenseNet, state)
    h = tanh.(net.w1 * state .+ net.b1)
    logits = net.w2 * h .+ net.b2
    return 1.0 / (1.0 + exp(-logits[1]))
end

function conv_forward(net::ConvNet, state)
    height, width = size(state)
    filters = size(net.conv_weights, 4)
    
    conv_out = zeros(Float64, height-2, width-2, filters)
    
    for f in 1:filters
        for i in 1:height-2
            for j in 1:width-2
                sum_val = 0.0
                for ki in 1:3
                    for kj in 1:3
                        row = i + ki - 1
                        col = j + kj - 1
                        if row <= height && col <= width
                            sum_val += state[row, col] * net.conv_weights[ki, kj, 1, f]
                        end
                    end
                end
                conv_out[i, j, f] = max(sum_val + net.conv_bias[f], 0.0)
            end
        end
    end
    
    flattened = vec(conv_out)
    logits = net.fc_weights * flattened .+ net.fc_bias
    return 1.0 / (1.0 + exp(-logits[1]))
end
Out[0]:
dense_forward (generic function with 1 method)

evaluate_network() 评估神经网络在游戏环境中的性能。 该函数作为进化算法中的适应度函数,量化网络如何发挥Flappy Bird。

该功能启动几个独立的游戏集(默认情况下为3),禁用渲染以获得最大的计算速度。 对于每一集,她都打电话 run_episode,它返回最终得分-成功完成的管道数量。 所有的分数被总结,并返回他们的算术平均值。

使用多个事件而不是一个事件通过平滑诸如管道初始位置等随机因素的影响来增加评估的可靠性。 这三个情节提供了估计精度和计算成本之间的平衡,这在进化算法的每一代中评估数百个网络时非常重要。

参数 state_type 允许函数与两个网络架构一起工作-传递给 run_episode 供正确的输入数据格式。 返回的平均值作为网络适应度的客观度量,在此基础上,在遗传算法中进行选择和再现。

In [ ]:
function evaluate_network(network, state_type=:basic, episodes=3)
    total = 0.0
    for _ in 1:episodes
        score = run_episode(network, state_type; render=false)
        total += score
    end
    return total / episodes
end
Out[0]:
evaluate_network (generic function with 3 methods)

train_genetic!() 实现用于训练神经网络群体的遗传算法。 这是模拟自然选择的机器学习系统的核心,以逐步提高网络的游戏技能。

该算法代代相传。 每一代人都是从评估人口中的所有网络开始的。 evaluate_network. 保存三个关键指标:最佳、平均和最差结果,这些指标显示以跟踪进度。 最好的网络作为潜在的解决方案保持独立。

如果最佳生成网络超过目标分数 target_score. 培训提前结束-目标已经实现。 否则,选择适者生存的个体。:前25%的网络(精英)被选中创造下一代。 精英的规模保证至少有一个网络,即使在小群体中也是如此。

新一代的创造采用了"精英主义与突变"的策略。 所有精英网络在下一代保持不变(精英主义)。 其余的地方充满了通过克隆随机选择的精英个体并随后可能发生突变而创建的后代。

突变发生概率 mutation_rate (默认值为30%),并将正态分布的小随机变化添加到网络参数。 这些变化模仿基因突变,引入多样性并允许探索新的策略。 0.1的系数限制突变的大小,防止破坏性变化。

循环重复,直到达到最大代数或目标计数。 该函数返回最终人口,所有世代的统计数据,找到的最佳网络及其分数。 这种进化方法不需要梯度或错误的反向传播,使得学习可以用来演示人工智能的基本原理。

In [ ]:
function train_genetic!(population, state_type=:basic; target_score=20, max_generations=100, mutation_rate=0.3)
    results = []
    best_score = 0.0
    best_network = nothing
    
    for generation in 1:max_generations
        scores = [evaluate_network(net, state_type) for net in population]
        gen_best = maximum(scores)
        gen_avg = sum(scores) / length(scores)
        gen_worst = minimum(scores)
        
        push!(results, (generation, gen_best, gen_avg, gen_worst))
        
        println("一代/最好的一代: ", round(gen_best, digits=2), " /平均: ", round(gen_avg, digits=2), " /最坏的: ", round(gen_worst, digits=2))
        
        if gen_best > best_score
            best_score = gen_best
            best_idx = argmax(scores)
            best_network = deepcopy(population[best_idx])
        end
        
        if gen_best >= target_score
            println("✓目标帐户已达到$target_score")
            break
        end
        
        sorted_idx = sortperm(scores, rev=true)
        elite_size = max(1, length(population) ÷ 4)
        elites = population[sorted_idx[1:elite_size]]
              new_pop = deepcopy(elites)
        
        while length(new_pop) < length(population)
            parent = rand(elites)
            
            if state_type == :conv
                child = ConvNet(
                    copy(parent.conv_weights),
                    copy(parent.conv_bias),
                    copy(parent.fc_weights),
                    copy(parent.fc_bias)
                )
                
                if rand() < mutation_rate
                    child.conv_weights .+= randn(size(child.conv_weights)...) * 0.1
                    child.conv_bias .+= randn(size(child.conv_bias)...) * 0.1
                    child.fc_weights .+= randn(size(child.fc_weights)...) * 0.1
                    child.fc_bias .+= randn(size(child.fc_bias)...) * 0.1
                end
            else
                child = DenseNet(
                    copy(parent.w1),
                    copy(parent.b1),
                    copy(parent.w2),
                    copy(parent.b2)
                )
                
                if rand() < mutation_rate
                    child.w1 .+= randn(size(child.w1)...) * 0.1
                    child.b1 .+= randn(size(child.b1)...) * 0.1
                    child.w2 .+= randn(size(child.w2)...) * 0.1
                    child.b2 .+= randn(size(child.b2)...) * 0.1
                end
            end
            
            push!(new_pop, child)
        end
        
        population = new_pop[1:length(population)]
    end
    
    return population, results, best_network, best_score
end

function argmax(x)
    max_val = -Inf
    max_idx = 1
    for (i, val) in enumerate(x)
        if val > max_val
            max_val = val
            max_idx = i
        end
    end
    return max_idx
end
Out[0]:
train_genetic! (generic function with 2 methods)

struct NetworkResult 它是一个容器,用于存储有关训练好的神经网络及其学习过程的完整信息。 该结构提供了在项目框架内训练的各种模型的系统存储和比较。

network 存储训练好的神经网络实例本身 DenseNetConvNet,包含所有训练好的权重和偏移量。 state_type 指定网络使用哪种类型的输入数据(:basic 为特征向量,或 :conv 为状态矩阵),这在使用网络进行游戏时很重要。

test_score 它包含最终的网络性能得分,平均在几个测试集。 这是教育质量的客观指标。 training_stats 它存储了所有世代学习的详细统计数据-最佳,平均和最差结果的动态,允许您分析算法收敛的过程。

params 表示要训练的网络参数的总数,这给出了模型的复杂性和计算要求的想法。 这些字段共同构成了经过训练的网络的完整"护照",允许您重现实验,比较不同的方法并演示结果。

In [ ]:
struct NetworkResult
    name::String
    architecture::String
    network::Any
    state_type::Symbol
    test_score::Float64
    training_stats::Vector{Any}
    params::Int
end

下面的代码实现了一个完整的交互式系统,用于训练、测试和比较神经网络,作为Flappy Bird AI项目的一部分。 该系统围绕几个专门的菜单构建,每个菜单负责工作流程的特定阶段。

主菜单(main_menu 它作为一个中央枢纽,显示所有受过训练的网络的列表,并提供对程序主要功能的访问。 它支持会话之间的结果累积,允许您训练多个网络并相互比较。

全连接网络学习菜单(train_dense_menu)和卷积网络训练菜单train_conv_menu 它们具有类似的结构,但它们是定制的,以适应每个体系结构的具体情况。 两个菜单都询问用户的训练参数:网络名称、人口规模、代数、目标分数和测试集数。 收集参数后,他们运行进化学习算法并将结果保存到共享列表中。 不同之处在于体系结构的描述和参数数量的计算。

测试菜单(testing_menu)允许您选择任何训练有素的网络进行其游戏的可视化演示。 它包括防止错误输入的保护,关于中断演示的可能性的警告,以及稳定操作的异常处理。

结果比较功能(compare_results)按测试分数对所有受过训练的网络进行排名,使用emojis授予前三名。 它还显示了最佳网络训练的统计数据,展示了从初始到最终结果的进展。

所有菜单都使用统一的设计,屏幕清洁,分隔符和信息标题,创造一个专业的用户体验专门使用控制台。 该系统演示了如何通过结构良好的文本界面使复杂的机器学习过程易于访问和直观。

In [ ]:
function main_menu()
    results = NetworkResult[]
    
    while true
        print("\033[2J\033[H")
        println("="^60)
        println("          飞扬的鸟-进化算法")
        println("="^60)
        
        if !isempty(results)
            println("\损坏的网络:")
            for (i, r) in enumerate(results)
                println("$i. $(r.name) ($(r.architecture)): ", round(r.test_score, digits=2), " /参数:$(r.params)")
            end
        end
        
        println("\n" * "-"^60)
        println("学习菜单:")
        println("1. 训练一个完全连接的网络")
        println("2. 训练卷积网络(CNN)")
        println("3. 使用图形进行测试运行")
        println("4. 比较所有训练好的网络")
        println("5. 出口;出口")
        println("-"^60)
        print("\选择: ")
        
        choice = readline()
        
        if choice == "1"
            results = train_dense_menu(results)
        elseif choice == "2"
            results = train_conv_menu(results)
        elseif choice == "3"
            testing_menu(results)
        elseif choice == "4"
            compare_results(results)
        elseif choice == "5"
            println("出口;出口")
            return
        end
    end
end

function train_dense_menu(results)
    print("\033[2J\033[H")
    println("="^60)
    println("全连接网络训练(进化算法)")
    println("="^60)
    println("\网络架构:")
    println("输入层:5个神经元(位置,速度,到管道的距离)")
    println("隐藏层:8个具有TANH激活功能的神经元")
    println("输出层:1个带乙状结肠的神经元(跳跃概率)")
    println("总参数: 5×8 + 8 + 8×1 + 1 = 57")
    
    print("\输入网络的名称: ")
    name = readline()
    
    print("人口中的网络数量: ")
    pop_size = parse(Int, readline())
    
    print("最大代数: ")
    max_generations = parse(Int, readline())
    
    print("止损目标账户: ")
    target_score = parse(Float64, readline())
    
    print("训练后的测试集: ")
    test_episodes = parse(Int, readline())
    
    println("\人口的创造。..")
    population = [init_dense() for _ in 1:pop_size]
       println("进化学习的开始。..")
    println("目标:达到targ target_score帐户")
    println("-"^60)
    
    final_pop, training_stats, best_net, best_score = train_genetic!(
        population, :basic, 
        target_score=target_score, 
        max_generations=max_generations,
        mutation_rate=0.3
    )
    
    println("最佳网络的测试运行。..")
    test_score = evaluate_network(best_net, :basic, test_episodes)
    
    push!(results, NetworkResult(
        name,
        "全连接(5-8-1)",
        best_net,
        :basic,
        test_score,
        training_stats,
        57
    ))
    
    println("\培训完成!")
    println("最佳学习成绩: ", round(best_score, digits=2))
    println("测试帐户: ", round(test_score, digits=2))
    println("按Enter继续。..")
    readline()
    
    return results
end

function testing_menu(results)
    if isempty(results)
        println("没有训练有素的网络!")
        sleep(2)
        return
    end
    
    while true
        print("\033[2J\033[H")
        println("="^60)
        println("使用图形进行测试运行")
        println("="^60)
        println("\选择一个网络进行测试(0表示退出):")
        
        for (i, r) in enumerate(results)
            println("$i. $(r.name) ($(r.architecture)): ", round(r.test_score, digits=2))
        end
        
        print("\n网络号码: ")
        input = readline()
        
        if input == "0"
            return
        end
        
        idx = tryparse(Int, input)
        if idx === nothing || idx < 1 || idx > length(results)
            println("打错了!")
            sleep(2)
            continue
        end
        
        chosen = results[idx]
        
        println("\启动网络演示'$(chosen.name )'。..")
        println("按Ctrl+C退出演示")
        println("3秒后开始。..")
        sleep(3)
        
        try
            score = run_episode(chosen.network, chosen.state_type; render=true, max_steps=1000)
            println("\演示结束了! 分数:$分数")
        catch e
            if !(e isa InterruptException)
                println("\执行过程中的错误:$e")
            end
        end
        
        println("按Enter继续。..")
        readline()
    end
end

function train_conv_menu(results)
    print("\033[2J\033[H")
    println("="^60)
    println("卷积网络训练(CNN)是一种进化算法")
    println("="^60)
    println("\网络架构:")
    println("入口:15×40(比赛场地的高度×宽度)")
    println("卷积层:4个3×3滤波器,ReLU激活")
    println("全连接层:1个具有乙状结肠的神经元")
    println("总参数:~500")
    
    print("\输入网络的名称: ")
    name = readline()
    
    print("人口中的网络数量: ")
    pop_size = parse(Int, readline())
    
    print("最大代数: ")
    max_generations = parse(Int, readline())
    
    print("止损目标账户: ")
    target_score = parse(Float64, readline())
    
    print("训练后的测试集: ")
    test_episodes = parse(Int, readline())
    
    println("\人口的创造。..")
    population = [init_conv() for _ in 1:pop_size]
    
    println("进化学习的开始。..")
    println("目标:达到targ target_score帐户")
    println("-"^60)
        final_pop, training_stats, best_net, best_score = train_genetic!(
        population, :conv, 
        target_score=target_score, 
        max_generations=max_generations,
        mutation_rate=0.3
    )
    
    println("最佳网络的测试运行。..")
    test_score = evaluate_network(best_net, :conv, test_episodes)
    
    height, width = 15, 40
    filters = 4
    conv_params = 3 * 3 * 1 * filters + filters
    fc_params = (height-2) * (width-2) * filters + 1
    total_params = conv_params + fc_params
    
    push!(results, NetworkResult(
        name,
        "卷积(Conv3x3x4→FC)",
        best_net,
        :conv,
        test_score,
        training_stats,
        total_params
    ))
    
    println("\培训完成!")
    println("最佳学习成绩: ", round(best_score, digits=2))
    println("测试帐户: ", round(test_score, digits=2))
    println("按Enter继续。..")
    readline()
    
    return results
end

function compare_results(results)
    if isempty(results)
        println("没有结果可以比较!")
        sleep(2)
        return
    end
    
    print("\033[2J\033[H")
    println("="^60)
    println("训练网络的比较")
    println("="^60)
    
    sorted_results = sort(results, by=r->r.test_score, rev=true)
    
    println("\网络定价(基于测试帐户):")
    println("-"^60)
    println("地点/名称/架构|帐户/参数")
    println("-"^60)
    
    for (i, r) in enumerate(sorted_results)
        place = i == 1 ? "🥇" : i == 2 ? "🥈" : i == 3 ? "🥉" : "  $i."
        println("$place $(r.name) | $(r.architecture) | ", round(r.test_score, digits=2), " | $(r.params)")
    end
    
    if any(r -> !isempty(r.training_stats), sorted_results)
        println("\学习最佳网络的统计:")
        best = sorted_results[1]
        if !isempty(best.training_stats)
            println("代数:$(length(best.training_stats))")
            last_stat = best.training_stats[end]
            first_stat = best.training_stats[1]
            println("初始平均分: ", round(first_stat[3], digits=2))
            println("最后最好成绩: ", round(last_stat[2], digits=2))
            println("改善工程: ", round(last_stat[2] - first_stat[2], digits=2))
        end
    end
    
    println("按Enter继续。..")
    readline()
end
Out[0]:
main_menu (generic function with 1 method)

要启动项目,您需要在命令行中调用 main_menu(). 以下是我们项目启动的结果。 对于指数频率,两个网络的参数设置为相同。

image.png
![图像。png](附件:image_2.png)

image.png
![图像。png](附件:image_2.png)

如果您自己运行这两种类型的神经网络的训练,您会注意到卷积网络(CNN)的训练速度比完全连接的慢得多。 这是由于架构和正在处理的数据量的根本差异:CNN使用比赛场地的完整15x40状态矩阵(600个值)工作,在每个步骤对多个过滤器执行多个卷积操作。 与此同时,一个完全连接的网络只接收五个预处理特征作为输入,这就需要少几个数量级的计算。 这种学习率的差异清楚地表明了模型复杂性和计算成本之间的权衡—更强大的架构能够更好地解决问题,但需要更多的资源进行培训。

基于所提出的结果,可以得出关于进化算法的工作以及Flappy Bird game问题中各种神经网络架构的有效性的几个重要结论。

两个神经网络,全连接和卷积,实现了相同的最高得分58.0分

完全连接的网络表现出非凡的效率:仅使用57个参数,它实现了与具有2017个参数的卷积网络相同的结果(35倍以上)。 这说明了机器学习的一个重要原理:更复杂的模型并不总是意味着更好的性能,特别是当任务可以通过简单的手段成功解决时。

进化算法已经显示出很高的效率,在短短14代中实现了最大的结果。 人口的初始平均得分仅为0.49分,这表明随机初始化的网络实际上无法播放。 在14代人中,已经有57.51点的改进-生产力的指数增长,证明了进化优化方法的力量。

下面提供了两个网络如何决定发挥结果,两个网络上升到现场的顶部,并计算当他们需要停止跳跃进入洞的时刻。

test.gif

结论

  1. 简单与复杂:对于规则明确、状态空间有限的任务,简单的体系结构可以和复杂的体系结构一样高效,但需要的计算资源要少得多。

  2. 进化算法工作:无梯度遗传方法成功地优化了两种架构,显示了其对强化学习任务的适用性。

  3. 算法收敛:快速收敛(14代)表示进化算法的精心选择的参数-种群大小,突变率和选择策略。

  4. 性能上限:两个网络的最高得分相同,可能表明在给定的游戏配置中达到了最大可能的结果,也可能表明游戏环境本身的局限性。

这些结果证实,正确实现的进化算法即使对于复杂的任务也可以有效地训练神经网络,并且体系结构的选择应该与正在解决的问题的具体情况相对应,