用GitHub Copilot学AI是什么体验?手把手教你用代码理解神经网络核心概念

最近几年,AI从一个遥不可及的学术概念,变成了我们身边触手可及的工具。但很多朋友在入门时,总会卡在理论到实践的鸿沟上——那些复杂的数学公式、抽象的算法描述,看懂了,却又好像没完全懂。直到我开始尝试一种新的学习方式:让GitHub Copilot这位“AI编程伙伴”带着我,用一行行可运行的代码,去“触摸”和“感受”AI的核心原理。

这感觉就像学开车,光看说明书永远学不会,必须手握方向盘,感受油门和刹车的反馈。学习AI也是如此。本文将分享我如何利用VS Code和GitHub Copilot,搭建一个沉浸式的“代码实验室”,通过亲手实现从简单的感知机到反向传播、再到遗传算法等经典模型,将书本上晦涩的概念,转化为屏幕上清晰可见的数据流和可视化图表。无论你是正在啃《人工智能导论》的学生,还是希望夯实基础的开发者,这套方法或许能为你打开一扇新的大门。

1. 环境搭建:从零开始的AI代码实验室

工欲善其事,必先利其器。一个顺手的开发环境,能让你把精力完全集中在算法逻辑本身,而不是纠结于包版本冲突或环境配置。我们的核心工具链非常简单:VS Code + Python + Jupyter Notebook扩展 + GitHub Copilot

1.1 核心工具安装与配置

首先,确保你的机器上安装了Python(推荐3.8及以上版本)。接下来,在VS Code中安装几个必不可少的扩展:

  • Python扩展:由Microsoft官方提供,集成了代码调试、智能提示、Jupyter Notebook支持等核心功能。
  • Jupyter扩展:让我们能在VS Code内无缝地编写和运行.ipynb笔记本文件,这是进行交互式AI实验的绝佳载体。
  • GitHub Copilot:本篇文章的“主角”。安装后,你需要登录GitHub账号并完成授权。它的核心能力是代码补全和代码建议,但我们将用它来辅助理解和生成算法实现。

安装完成后,创建一个新的文件夹作为项目目录,并在VS Code中打开它。接着,我们通过终端初始化项目环境。我强烈建议为每个项目使用独立的虚拟环境,这能避免依赖污染。

# 在项目根目录下执行
python -m venv .venv

# 激活虚拟环境
# Windows:
.venv\Scripts\activate
# macOS/Linux:
source .venv/bin/activate

# 安装基础科学计算库
pip install numpy matplotlib pandas
# 安装Jupyter内核,以便在VS Code中使用
pip install ipykernel

提示:如果你在激活虚拟环境后,VS Code的终端没有显示(.venv)前缀,可能需要手动选择解释器。按下Ctrl+Shift+P,输入“Python: Select Interpreter”,然后选择路径中包含.venv的那个Python即可。

1.2 与Copilot协作:从“提问”开始

环境就绪,现在让我们感受一下Copilot如何改变学习方式。传统的学习路径是:看书 -> 理解概念 -> 尝试编码。而有了Copilot,我们可以尝试:提出想法 -> Copilot生成代码骨架 -> 阅读并理解代码 -> 修改和实验

例如,新建一个neural_network_basics.ipynb的Jupyter Notebook文件。在第一格,我们不用写代码,而是先写一个Markdown单元格,描述我们想实现什么:

# 目标:实现一个最简单的单层感知机(Perceptron),用于学习逻辑AND运算。
# 输入:两个二进制特征 (x1, x2)
# 输出:0 或 1
# 算法步骤:
# 1. 初始化权重w和偏置b(通常为小随机数或0)。
# 2. 定义激活函数(这里用阶跃函数)。
# 3. 前向传播计算输出。
# 4. 根据误差更新权重(感知机学习规则)。

写完这段描述后,新建一个代码单元格,直接开始输入函数定义:

def perceptron_train(X, y, learning_rate=0.1, epochs=100):
    """
    训练一个感知机模型。
    参数:
    X: 输入特征矩阵,形状为 (n_samples, n_features)
    y: 目标标签,形状为 (n_samples,)
    learning_rate: 学习率
    epochs: 训练轮数
    返回:
    weights: 训练后的权重向量
    bias: 训练后的偏置
    """

当你输入完函数签名和文档字符串后,暂停一下,Copilot很可能会自动为你补全整个训练循环的代码框架。它生成的代码可能类似这样:

    n_samples, n_features = X.shape
    weights = np.zeros(n_features)
    bias = 0

    for epoch in range(epochs):
        for idx in range(n_samples):
            # 计算线性输出
            linear_output = np.dot(X[idx], weights) + bias
            # 应用阶跃函数
            y_pred = 1 if linear_output >= 0 else 0
            # 计算误差
            error = y[idx] - y_pred
            # 更新权重和偏置
            weights += learning_rate * error * X[idx]
            bias += learning_rate * error

    return weights, bias

这就是我们第一个“可运行”的AI概念。你不必一开始就完全理解每一行,但你可以立刻运行它,用真实的数据去测试,观察权重如何变化,从而直观理解“权重更新”这个抽象概念。接下来,我们就用这个感知机,去解决一个经典问题。

2. 第一行AI代码:用感知机理解“学习”的本质

让我们把上面Copilot生成的函数用起来。我们准备AND运算的数据,并可视化训练过程。

2.1 实现与可视化

在下一个单元格中,我们准备数据并调用训练函数:

import numpy as np
import matplotlib.pyplot as plt

# AND 运算的真值表
X = np.array([[0, 0],
              [0, 1],
              [1, 0],
              [1, 1]])
y = np.array([0, 0, 0, 1])  # AND 运算结果

# 训练感知机
weights, bias = perceptron_train(X, y, learning_rate=0.1, epochs=10)

print(f"训练后的权重: {weights}")
print(f"训练后的偏置: {bias}")

# 定义一个预测函数
def perceptron_predict(X, weights, bias):
    linear_output = np.dot(X, weights) + bias
    return np.where(linear_output >= 0, 1, 0)

# 测试
predictions = perceptron_predict(X, weights, bias)
print(f"预测结果: {predictions}")
print(f"是否全部正确: {np.array_equal(predictions, y)}")

运行后,你会看到感知机成功地学会了AND逻辑。但这还不够直观。我们修改一下训练函数,让它记录下每一轮迭代后的决策边界,并动态绘制出来。

def perceptron_train_visualize(X, y, learning_rate=0.1, epochs=10):
    n_samples, n_features = X.shape
    weights = np.zeros(n_features)
    bias = 0
    history = []  # 用于记录权重和偏置的历史

    for epoch in range(epochs):
        for idx in range(n_samples):
            linear_output = np.dot(X[idx], weights) + bias
            y_pred = 1 if linear_output >= 0 else 0
            error = y[idx] - y_pred
            weights += learning_rate * error * X[idx]
            bias += learning_rate * error
        history.append((weights.copy(), bias.copy())) # 记录本轮结束后的状态

    return weights, bias, history

# 重新训练并获取历史
weights_final, bias_final, history = perceptron_train_visualize(X, y, epochs=5)

# 可视化
plt.figure(figsize=(12, 8))
for i, (w, b) in enumerate(history):
    plt.subplot(2, 3, i+1) # 创建2行3列的子图
    # 绘制数据点
    plt.scatter(X[y==0, 0], X[y==0, 1], c='blue', label='0', s=100)
    plt.scatter(X[y==1, 0], X[y==1, 1], c='red', marker='s', label='1', s=100)
    # 绘制决策边界: w1*x1 + w2*x2 + b = 0 => x2 = -(w1*x1 + b)/w2
    if w[1] != 0: # 避免除零错误
        x1_range = np.array([-0.5, 1.5])
        x2_range = -(w[0] * x1_range + b) / w[1]
        plt.plot(x1_range, x2_range, 'k--', linewidth=2, label=f'Epoch {i}')
    plt.xlim(-0.5, 1.5)
    plt.ylim(-0.5, 1.5)
    plt.grid(True, linestyle='--', alpha=0.7)
    plt.title(f'Epoch {i}: w={w.round(2)}, b={b:.2f}')
    plt.legend()
plt.tight_layout()
plt.show()

这段代码会生成一系列子图,清晰展示感知机如何通过一次次调整权重和偏置,最终找到那条完美分割蓝色点和红色正方形的直线。你亲眼看到了“学习”的发生。这就是代码带来的、理论无法替代的具象理解。

2.2 从感知机到多层网络:引入非线性

感知机能解决AND问题,但它解决不了XOR(异或)问题。这是AI历史上一个著名的瓶颈,也直接推动了多层神经网络的发展。我们可以让Copilot帮我们快速构建一个简单的两层网络(含一个隐藏层)来尝试解决XOR。

# 让我们构建一个简单的两层神经网络来解决XOR问题
# 网络结构:输入层(2) -> 隐藏层(2, 使用sigmoid) -> 输出层(1, 使用sigmoid)

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    return x * (1 - x)

# 初始化参数
def initialize_parameters(input_size, hidden_size, output_size):
    np.random.seed(42)
    W1 = np.random.randn(input_size, hidden_size) * 0.1
    b1 = np.zeros((1, hidden_size))
    W2 = np.random.randn(hidden_size, output_size) * 0.1
    b2 = np.zeros((1, output_size))
    return W1, b1, W2, b2

# 前向传播
def forward_propagation(X, W1, b1, W2, b2):
    Z1 = np.dot(X, W1) + b1
    A1 = sigmoid(Z1)
    Z2 = np.dot(A1, W2) + b2
    A2 = sigmoid(Z2)
    return Z1, A1, Z2, A2

# 计算损失(均方误差)
def compute_loss(y, A2):
    m = y.shape[0]
    loss = (1/(2*m)) * np.sum(np.square(A2 - y))
    return loss

# 反向传播
def backward_propagation(X, y, Z1, A1, Z2, A2, W2):
    m = y.shape[0]
    dZ2 = A2 - y
    dW2 = (1/m) * np.dot(A1.T, dZ2)
    db2 = (1/m) * np.sum(dZ2, axis=0, keepdims=True)
    dA1 = np.dot(dZ2, W2.T)
    dZ1 = dA1 * sigmoid_derivative(A1)
    dW1 = (1/m) * np.dot(X.T, dZ1)
    db1 = (1/m) * np.sum(dZ1, axis=0, keepdims=True)
    return dW1, db1, dW2, db2

# 更新参数
def update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate):
    W1 -= learning_rate * dW1
    b1 -= learning_rate * db1
    W2 -= learning_rate * dW2
    b2 -= learning_rate * db2
    return W1, b1, W2, b2

现在,我们可以用Copilot快速补全训练循环,并观察这个小型网络如何学习XOR。

# XOR 数据
X_xor = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y_xor = np.array([[0], [1], [1], [0]]) # 注意形状为(4,1)

# 超参数
input_size = 2
hidden_size = 2
output_size = 1
learning_rate = 0.5
epochs = 10000

# 初始化
W1, b1, W2, b2 = initialize_parameters(input_size, hidden_size, output_size)

loss_history = []
# 训练循环
for i in range(epochs):
    # 前向传播
    Z1, A1, Z2, A2 = forward_propagation(X_xor, W1, b1, W2, b2)
    # 计算损失
    loss = compute_loss(y_xor, A2)
    loss_history.append(loss)
    # 反向传播
    dW1, db1, dW2, db2 = backward_propagation(X_xor, y_xor, Z1, A1, Z2, A2, W2)
    # 更新参数
    W1, b1, W2, b2 = update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate)
    if i % 1000 == 0:
        print(f"Epoch {i}, Loss: {loss:.6f}")

# 预测
_, _, _, predictions = forward_propagation(X_xor, W1, b1, W2, b2)
print("\n最终预测值(四舍五入):")
print(np.round(predictions))
print("真实标签:")
print(y_xor)

# 绘制损失下降曲线
plt.figure(figsize=(10, 5))
plt.plot(loss_history)
plt.yscale('log') # 使用对数坐标更清晰地观察损失下降
plt.xlabel('Epoch')
plt.ylabel('Loss (Log Scale)')
plt.title('Training Loss over Epochs for XOR Problem')
plt.grid(True)
plt.show()

运行这段代码,你会看到损失从较高的值逐渐下降到接近0,并且网络最终能正确预测XOR。通过这个亲手搭建的微型网络,你不仅理解了反向传播的代码实现,更关键的是,你直观地看到了非线性激活函数(sigmoid)和隐藏层如何赋予网络解决线性不可分问题的能力。Copilot在这里扮演了“高级脚手架”的角色,它帮你处理了繁琐的矩阵求导和维度对齐的细节,让你能聚焦于算法的主干逻辑。

3. 可视化反向传播:看清梯度如何流动

反向传播(Backpropagation)是神经网络训练的引擎,但它的数学推导常常让人望而生畏。我们可以利用一些可视化技巧,让这个“黑箱”过程变得透明。我们将创建一个更简单的网络,并手动“追踪”一次前向和反向传播,用数值和图表展示每一步。

3.1 构建一个可追踪的微型网络

假设我们有一个极其简单的网络:输入层1个神经元,隐藏层2个神经元,输出层1个神经元。我们使用Sigmoid激活函数和均方误差损失。我们为所有参数赋予一个固定的初始值,以便于跟踪。

# 固定随机种子,确保可复现
np.random.seed(123)

# 定义网络结构和固定初始参数
# 输入: x = 0.5, 真实标签: y_true = 0.8
x = np.array([[0.5]])
y_true = np.array([[0.8]])

# 初始化参数(我们手动指定以便追踪)
W1 = np.array([[0.1, 0.2]])  # 形状 (1, 2)
b1 = np.array([[0.05, 0.05]]) # 形状 (1, 2)
W2 = np.array([[0.3], [0.4]]) # 形状 (2, 1)
b2 = np.array([[0.1]])        # 形状 (1, 1)

print("初始参数:")
print(f"W1: {W1}")
print(f"b1: {b1}")
print(f"W2: {W2}")
print(f"b2: {b2}")
print("-" * 40)

3.2 分步计算与可视化

接下来,我们进行单次前向传播和反向传播,并打印出每一个中间变量的值。

# 步骤1: 前向传播
print("=== 前向传播 ===")
z1 = np.dot(x, W1) + b1  # 线性变换
print(f"z1 (隐藏层加权输入): {z1}")
a1 = sigmoid(z1)          # 激活
print(f"a1 (隐藏层激活输出): {a1}")

z2 = np.dot(a1, W2) + b2 # 线性变换
print(f"z2 (输出层加权输入): {z2}")
a2 = sigmoid(z2)          # 激活,即最终预测值 y_pred
print(f"a2 (最终输出 y_pred): {a2}")
print("-" * 40)

# 步骤2: 计算损失
print("=== 计算损失 ===")
loss = 0.5 * (a2 - y_true) ** 2
print(f"单个样本的损失 (MSE): {loss[0][0]:.6f}")
print("-" * 40)

# 步骤3: 反向传播 (链式法则)
print("=== 反向传播 (梯度计算) ===")
# 输出层误差
d_loss_d_a2 = a2 - y_true
print(f"∂Loss/∂a2: {d_loss_d_a2}")
d_a2_d_z2 = sigmoid_derivative(a2)
print(f"∂a2/∂z2 (sigmoid导数): {d_a2_d_z2}")
d_loss_d_z2 = d_loss_d_a2 * d_a2_d_z2
print(f"∂Loss/∂z2: {d_loss_d_z2}")

# 隐藏层误差
d_z2_d_a1 = W2.T
print(f"∂z2/∂a1 (即 W2.T): {d_z2_d_a1}")
d_loss_d_a1 = np.dot(d_loss_d_z2, d_z2_d_a1)
print(f"∂Loss/∂a1: {d_loss_d_a1}")
d_a1_d_z1 = sigmoid_derivative(a1)
print(f"∂a1/∂z1 (sigmoid导数): {d_a1_d_z1}")
d_loss_d_z1 = d_loss_d_a1 * d_a1_d_z1
print(f"∂Loss/∂z1: {d_loss_d_z1}")
print("-" * 40)

# 步骤4: 计算参数梯度
print("=== 参数梯度 ===")
# 关于W2和b2的梯度
d_loss_d_W2 = np.dot(a1.T, d_loss_d_z2)
d_loss_d_b2 = np.sum(d_loss_d_z2, axis=0, keepdims=True)
print(f"∂Loss/∂W2: {d_loss_d_W2}")
print(f"∂Loss/∂b2: {d_loss_d_b2}")

# 关于W1和b1的梯度
d_loss_d_W1 = np.dot(x.T, d_loss_d_z1)
d_loss_d_b1 = np.sum(d_loss_d_z1, axis=0, keepdims=True)
print(f"∂Loss/∂W1: {d_loss_d_W1}")
print(f"∂Loss/∂b1: {d_loss_d_b1}")

运行这段代码,控制台会输出每一步的计算结果。这就像给网络做了一次“X光检查”,你能清晰地看到误差(Loss)是如何从输出层一层层反向传播到输入层,并分解到每一个权重和偏置上的。

为了更直观,我们可以用图表来展示这个微型网络的结构和数据流动。我们可以用matplotlib的箭头和文本框来绘制。

# 绘制网络结构与数据流图(简化示意)
fig, ax = plt.subplots(figsize=(10, 6))
ax.axis('off')

# 定义节点位置
layers = [['x'], ['h1', 'h2'], ['y_pred']]
pos = {}
pos['x'] = (1, 5)
pos['h1'] = (3, 6)
pos['h2'] = (3, 4)
pos['y_pred'] = (5, 5)
pos['y_true'] = (6, 5) # 真实标签节点

# 绘制节点
for node, (x, y) in pos.items():
    ax.plot(x, y, 'o', markersize=40, color='lightblue' if node != 'y_true' else 'lightgreen')
    ax.text(x, y, node, ha='center', va='center', fontsize=12, fontweight='bold')

# 绘制连接和标注(以W1[0,0]和W2[0,0]为例)
# 连接 x -> h1
ax.annotate('', xy=pos['h1'], xytext=pos['x'], arrowprops=dict(arrowstyle='->', lw=2, color='gray'))
ax.text(2, 5.7, f'W1[0,0]={W1[0,0]:.2f}\nz1_h1={z1[0,0]:.3f}\na1_h1={a1[0,0]:.3f}', fontsize=9, bbox=dict(boxstyle="round,pad=0.3", fc="wheat", alpha=0.7))

# 连接 h1 -> y_pred
ax.annotate('', xy=pos['y_pred'], xytext=pos['h1'], arrowprops=dict(arrowstyle='->', lw=2, color='gray'))
ax.text(4, 5.7, f'W2[0,0]={W2[0,0]:.2f}\nz2={z2[0,0]:.3f}\ny_pred={a2[0,0]:.3f}', fontsize=9, bbox=dict(boxstyle="round,pad=0.3", fc="lightcoral", alpha=0.7))

# 标注损失和梯度
ax.text(5.5, 4, f'Loss={loss[0,0]:.4f}', fontsize=10, bbox=dict(boxstyle="round,pad=0.3", fc="yellow", alpha=0.7))
ax.text(4, 4.3, f'∂L/∂W2≈{d_loss_d_W2[0,0]:.4f}', fontsize=9, color='darkred')
ax.text(2.2, 4.3, f'∂L/∂W1≈{d_loss_d_W1[0,0]:.4f}', fontsize=9, color='darkred')

ax.set_xlim(0, 7)
ax.set_ylim(3, 7)
ax.set_title('反向传播数据流与梯度计算示意', fontsize=14)
plt.show()

这张图虽然简单,但它将前向传播的数值计算(z, a)和反向传播的梯度(∂L/∂W)关联了起来。当你看到∂L/∂W2这个数字时,你就能立刻对应到它是如何通过∂L/∂a2 -> ∂a2/∂z2 -> ∂z2/∂W2这条链计算出来的。这种将抽象数学与具体代码、可视化结合的方式,能极大地深化你对核心机制的理解。

4. 超越监督学习:用遗传算法解决优化问题

神经网络是连接主义AI的代表,而遗传算法(Genetic Algorithm, GA)则属于进化计算范畴。它模拟自然选择过程,为解决复杂的组合优化问题提供了另一种思路。让我们用Copilot辅助,实现一个经典的GA应用:解决旅行商问题(TSP)的简化版。

4.1 定义问题与编码

假设有5个城市,我们需要找到访问所有城市一次并回到起点的最短路径。我们首先定义城市坐标和距离函数。

# 定义5个城市的坐标 (简化问题)
cities = {
    0: (0, 0),
    1: (1, 5),
    2: (3, 2),
    3: (5, 6),
    4: (7, 1)
}

def calculate_distance(path):
    """计算一条路径的总距离。路径是城市的排列,如 [0, 2, 1, 4, 3]"""
    total_distance = 0
    num_cities = len(path)
    for i in range(num_cities):
        city_a = path[i]
        city_b = path[(i + 1) % num_cities] # 回到起点
        x1, y1 = cities[city_a]
        x2, y2 = cities[city_b]
        total_distance += np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
    return total_distance

# 测试距离计算
sample_path = [0, 1, 2, 3, 4]
print(f"路径 {sample_path} 的总距离: {calculate_distance(sample_path):.2f}")

在遗传算法中,一条路径(解)就是一个“染色体”。我们使用城市索引的排列来表示。适应度函数(我们希望最大化)通常取距离的倒数。

4.2 实现遗传算法核心操作

现在,让我们实现GA的核心步骤:初始化种群、选择、交叉、变异。

def initialize_population(pop_size, num_cities):
    """初始化种群:生成pop_size个随机排列"""
    population = []
    for _ in range(pop_size):
        individual = list(range(num_cities))
        np.random.shuffle(individual)
        population.append(individual)
    return population

def fitness_evaluation(population):
    """评估种群中每个个体的适应度(距离越短,适应度越高)"""
    fitness_scores = []
    for individual in population:
        distance = calculate_distance(individual)
        # 适应度取距离的倒数,并乘以一个缩放因子避免过小
        fitness = 1000.0 / (distance + 1e-5) # 加上一个小数防止除零
        fitness_scores.append(fitness)
    return np.array(fitness_scores)

def selection(population, fitness_scores, num_parents):
    """轮盘赌选择:根据适应度比例选择父代"""
    probs = fitness_scores / fitness_scores.sum()
    selected_indices = np.random.choice(len(population), size=num_parents, p=probs, replace=True)
    parents = [population[i] for i in selected_indices]
    return parents

def crossover(parent1, parent2):
    """顺序交叉 (OX):产生一个子代"""
    size = len(parent1)
    # 随机选择两个切点
    start, end = sorted(np.random.choice(range(size), 2, replace=False))
    child = [None] * size
    # 将父代1的切片复制到子代
    child[start:end+1] = parent1[start:end+1]
    # 从父代2填充剩余位置,保持顺序
    pointer = 0
    for gene in parent2:
        if gene not in child:
            while child[pointer] is not None:
                pointer += 1
            child[pointer] = gene
    return child

def mutation(individual, mutation_rate):
    """交换变异:以一定概率随机交换两个位置的城市"""
    if np.random.rand() < mutation_rate:
        size = len(individual)
        idx1, idx2 = np.random.choice(range(size), 2, replace=False)
        individual[idx1], individual[idx2] = individual[idx2], individual[idx1]
    return individual

4.3 运行进化并可视化

有了这些基础函数,我们就可以构建主循环,并观察种群是如何一步步进化出更优解的。

# 遗传算法主函数
def genetic_algorithm_tsp(cities_dict, pop_size=50, generations=200, mutation_rate=0.1):
    num_cities = len(cities_dict)
    global cities
    cities = cities_dict # 更新全局变量

    # 初始化
    population = initialize_population(pop_size, num_cities)
    best_fitness_history = []
    avg_fitness_history = []
    best_individual_all_time = None
    best_fitness_all_time = -np.inf

    for gen in range(generations):
        # 评估
        fitness_scores = fitness_evaluation(population)
        avg_fitness = fitness_scores.mean()
        best_fitness_idx = fitness_scores.argmax()
        best_fitness = fitness_scores[best_fitness_idx]
        best_individual = population[best_fitness_idx]

        # 更新历史最佳
        if best_fitness > best_fitness_all_time:
            best_fitness_all_time = best_fitness
            best_individual_all_time = best_individual.copy()

        best_fitness_history.append(1/(best_fitness/1000)) # 转换回距离便于观察
        avg_fitness_history.append(1/(avg_fitness/1000))

        # 选择
        parents = selection(population, fitness_scores, pop_size)

        # 交叉与变异,生成新一代
        new_population = []
        for i in range(0, pop_size, 2):
            parent1, parent2 = parents[i], parents[i+1]
            child1 = crossover(parent1, parent2)
            child2 = crossover(parent2, parent1) # 可以交换父母顺序产生不同子代
            child1 = mutation(child1, mutation_rate)
            child2 = mutation(child2, mutation_rate)
            new_population.extend([child1, child2])
        population = new_population[:pop_size] # 确保种群大小不变

        if gen % 20 == 0:
            print(f"Generation {gen:3d} | Best Dist: {1/(best_fitness/1000):.2f} | Avg Dist: {1/(avg_fitness/1000):.2f}")

    print(f"\n最终找到的最佳路径: {best_individual_all_time}")
    print(f"最短距离: {1/(best_fitness_all_time/1000):.2f}")
    return best_individual_all_time, best_fitness_history, avg_fitness_history

# 运行算法
best_path, best_history, avg_history = genetic_algorithm_tsp(cities, pop_size=30, generations=150)

# 可视化结果
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 子图1:绘制最佳路径
ax1 = axes[0]
city_ids = list(cities.keys())
city_coords = np.array(list(cities.values()))
# 绘制城市点
ax1.scatter(city_coords[:, 0], city_coords[:, 1], c='red', s=200, zorder=5)
for i, (x, y) in enumerate(city_coords):
    ax1.text(x, y, str(i), fontsize=12, ha='center', va='center', color='white', fontweight='bold')
# 绘制路径
path_coords = city_coords[best_path + [best_path[0]]] # 回到起点形成闭环
ax1.plot(path_coords[:, 0], path_coords[:, 1], 'b-', linewidth=2, marker='o')
ax1.set_title(f'最佳旅行商路径 (距离: {calculate_distance(best_path):.2f})')
ax1.set_xlabel('X坐标')
ax1.set_ylabel('Y坐标')
ax1.grid(True)

# 子图2:绘制进化曲线
ax2 = axes[1]
generations = range(len(best_history))
ax2.plot(generations, best_history, 'r-', label='每代最佳距离', linewidth=2)
ax2.plot(generations, avg_history, 'b--', label='每代平均距离', linewidth=1.5)
ax2.set_xlabel('进化代数')
ax2.set_ylabel('路径距离')
ax2.set_title('遗传算法进化过程')
ax2.legend()
ax2.grid(True)

plt.tight_layout()
plt.show()

运行这段代码,你会看到控制台打印出进化过程中每代的最佳距离和平均距离,同时两张图会展示最终找到的路径和进化曲线。观察曲线,你会发现距离(我们想最小化的值)总体呈下降趋势,但过程中有波动,这体现了遗传算法通过交叉和变异进行探索的特性。

通过这个完整的例子,你不仅用代码实现了遗传算法,还看到了它如何应用于一个经典的NP-hard优化问题。Copilot在其中的作用是帮你快速搭建了算法骨架(如轮盘赌选择、顺序交叉的实现),让你能更专注于调整超参数(种群大小、变异率)和观察算法行为,理解“选择压力”、“探索与利用的平衡”这些概念的实际含义。

在这个过程中,我最大的体会是,Copilot像一个不知疲倦的结对编程伙伴。它不会直接给你答案,但能根据你的意图和上下文,快速生成高质量的代码草稿。这迫使你必须去思考“我想要什么”,然后阅读、调试、修改它生成的代码。这个“思考-生成-验证-理解”的循环,恰恰是深度学习最有效的方式。当你亲手让这些算法在屏幕上跑起来,看着损失曲线下降、看着路径被优化、看着决策边界移动时,那些原本停留在纸面上的公式和定理,才真正内化成了你的直觉和经验。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐