8.4 打卡 DAY 33: 第一个神经网络 - MLP的构建与训练
DAY 33: 第一个神经网络 - MLP的构建与训练
核心概念回顾
- 多层感知机 (MLP):神经网络的基本形态。
- 梯度下降 (Gradient Descent):神经网络的学习方式。
- 激活函数 (Activation Function):为网络注入“非线性”。
- 损失函数 (Loss Function):衡量“预测”与“现实”的差距。
- 优化器 (Optimizer):更新网络参数的工具。
1. 什么是多层感知机 (MLP)?
多层感知机(Multi-Layer Perceptron),简称MLP,是神经网络最基本的一种结构。你可以把它想象成一个由多个“计算层”堆叠起来的系统:
- 输入层 (Input Layer):接收最原始的数据。比如在我们的鸢尾花项目中,输入层就有4个神经元,分别对应4个特征(花萼长、宽等)。
- 隐藏层 (Hidden Layers):位于输入层和输出层之间,负责从数据中提取更复杂、更抽象的特征。一个MLP可以没有隐藏层,也可以有一个或多个。正是这些隐藏层让神经网络拥有了强大的学习能力。
- 输出层 (Output Layer):产生最终的预测结果。在我们的项目中,输出层有3个神经元,分别代表3种鸢尾花的类别。
数据从输入层开始,逐层向前传递并进行计算,直到输出层得出结果,这个过程被称为前向传播 (Forward Propagation)。
2. 梯度下降:机器如何“学习”?
神经网络的学习过程,本质上是一个**寻找最优参数(权重和偏置)**的过程,目标是让模型的预测结果与真实标签尽可能地接近。梯度下降就是实现这个目标最核心的算法。
想象你在一个漆黑的山谷里,想要走到谷底(最低点)。你看不清路,但你能感知到脚下哪个方向是向下的。于是,你每走一步,都选择当前位置最陡峭的下坡方向迈出一小步。不断重复这个过程,最终你就能到达谷底。
在这个比喻中:
- 山谷的形状:由损失函数定义,它描述了预测错误程度。
- 你的位置:代表当前模型的参数。
- 谷底:代表能使预测错误最小化的最优参数。
- 最陡峭的下坡方向:这就是梯度 (Gradient),它指向损失增长最快的方向,因此它的反方向就是损失下降最快的方向。
- 你迈出的一小步:由学习率 (Learning Rate) 控制,决定了每次参数更新的幅度。
3. 激活函数:赋予网络“拐弯”的能力
如果神经网络的每一层都只是简单的线性计算(如 y = wx + b
),那么无论叠加多少层,整个网络最终也只能表示一个复杂的线性关系。这就像你只能画直线,无法画出曲线一样,表达能力非常有限。
激活函数的作用就是为网络引入非线性因素。它作用于每个神经元的输出上,将线性计算的结果进行一次非线性转换。最常用的激活函数之一是 ReLU (Rectified Linear Unit),它的规则非常简单:输入大于0时,输出等于输入;输入小于等于0时,输出为0。
正是这些非线性“拐弯”的能力,让神经网络可以拟合各种复杂的函数关系。
4. 损失函数:一把衡量“差距”的尺子
损失函数(也叫成本函数或目标函数)是用来量化模型预测值与真实标签之间差距的工具。这个差距值(损失值)越小,说明模型预测得越准。
- 回归问题:常用均方误差损失 (MSE Loss),计算预测值与真实值之差的平方和的平均值。
- 分类问题:常用交叉熵损失 (Cross-Entropy Loss),它能量化两个概率分布之间的差异。在多分类任务中,它非常有效。
整个训练过程的目标,就是通过调整模型参数来最小化这个损失值。
5. 优化器:参数更新的“指挥官”
优化器是梯度下降算法的具体实现者。它接收损失函数计算出的梯度,并按照一定的规则来更新模型的参数。
- 随机梯度下降 (SGD):最基础的优化器。它根据当前批次数据的梯度来更新参数。
- Adam (Adaptive Moment Estimation):一种更高级的优化器。它能为每个参数自适应地调整学习率,通常能更快、更稳定地收敛,是目前非常流行的选择。
炼丹前的准备:安装PyTorch与CUDA
深度学习涉及大量的简单并行计算,这正是GPU(图形处理器)的优势所在。相较于一个能处理复杂任务的博士生(CPU),GPU更像是100个能飞快完成简单加减乘除的小学生,人多力量大。
我们主要使用支持NVIDIA显卡CUDA并行计算架构的PyTorch。
-
硬件检查 (NVIDIA显卡用户)
在你的电脑CMD(命令提示符)中输入nvidia-smi
命令,可以查看显卡信息。重点关注两个信息:
- CUDA Version:显卡驱动支持的最高CUDA版本(如12.7)。
- 显存大小:例如12288 MiB (12 GB),这决定了你能训练多大的模型。
-
环境安装
强烈建议为深度学习项目创建一个独立的Conda新环境,例如命名为DL
。请参考我们提供的安装教程或在B站搜索相关视频,完成PyTorch和(如果适用)CUDA、cuDNN的安装。- 怕麻烦的同学:可以直接安装CPU版本的PyTorch,代码同样可以跑通。
- 追求性能的同学:请务必安装与你显卡兼容的GPU版本。
-
代码验证CUDA
安装完成后,可以用以下代码在Python中检查PyTorch是否能成功调用CUDA。import torch# 检查CUDA是否可用 if torch.cuda.is_available():print("CUDA可用!")print(f"可用的CUDA设备数量: {torch.cuda.device_count()}")print(f"当前CUDA设备的名称: {torch.cuda.get_device_name(torch.cuda.current_device())}")print(f"PyTorch检测到的CUDA版本: {torch.version.cuda}") else:print("CUDA不可用,将使用CPU进行训练。")
注意:这里显示的CUDA版本是PyTorch编译时所用的版本,它需要小于等于你显卡驱动支持的最高版本。
2. 神经网络的流程:一步一脚印
a. 数据预处理
我们依然使用经典的鸢尾花数据集(4个特征,3个类别)。神经网络对输入数据的尺度非常敏感,因此归一化是必不可少的步骤。此外,PyTorch使用一种名为**张量(Tensor)**的特殊数据结构进行计算。
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
import torch
import numpy as np# 1. 加载和划分数据
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)# 2. 归一化数据 (非常重要!)
scaler = MinMaxScaler()
X_train = scaler.fit_transform(X_train)
# 注意:测试集使用训练集学习到的scaler进行transform,保证数据尺度一致
X_test = scaler.transform(X_test) # 3. 转换为 PyTorch 张量
X_train = torch.FloatTensor(X_train)
X_test = torch.FloatTensor(X_test)
# 标签y是整数类别,需要转换为LongTensor
y_train = torch.LongTensor(y_train)
y_test = torch.LongTensor(y_test)print("训练数据尺寸:", X_train.shape)
print("训练标签尺寸:", y_train.shape)
预处理补充:
- 分类任务:标签通常是整数(0, 1, 2…),需要转换为
torch.LongTensor
。 - 回归任务:标签通常是连续的浮点数,需要转换为
torch.FloatTensor
。
b. 模型的定义
我们来定义一个简单的MLP模型,它包含一个输入层、一个隐藏层和一个输出层。在PyTorch中,自定义模型通常需要:
- 继承
nn.Module
类。 - 在
__init__
(初始化方法) 中定义好每一“层”的结构。 - 在
forward
(前向传播方法) 中定义数据是如何从输入层流经每一层最终到达输出层的。
import torch.nn as nn
import torch.optim as optimclass MLP(nn.Module):def __init__(self):# 调用父类的初始化函数 (八股文)super(MLP, self).__init__()# 定义网络层# 输入层(4个特征) -> 隐藏层(10个神经元)self.fc1 = nn.Linear(4, 10) # 激活函数self.relu = nn.ReLU()# 隐藏层(10个神经元) -> 输出层(3个类别)self.fc2 = nn.Linear(10, 3)def forward(self, x):# 定义数据前向传播的顺序out = self.fc1(x)out = self.relu(out)out = self.fc2(out)return out# 实例化我们定义的模型
model = MLP()
print(model)
注意:输出层通常不需要激活函数,因为像交叉熵这样的损失函数内部已经包含了Softmax操作,会将输出转换为概率。
c. 定义损失函数和优化器
- 损失函数 (Criterion):衡量模型预测值与真实值之间的差距。对于多分类问题,我们使用交叉熵损失 (
nn.CrossEntropyLoss
)。 - 优化器 (Optimizer):根据损失函数计算出的梯度,来更新模型的参数(权重和偏置),以使损失最小化。我们这里使用经典的随机梯度下降 (
optim.SGD
)。
# 定义损失函数
criterion = nn.CrossEntropyLoss()# 定义优化器,传入模型的所有参数(model.parameters())和学习率(lr)
optimizer = optim.SGD(model.parameters(), lr=0.01)
d. 定义训练流程 (CPU版本)
训练过程就是一个循环,在每一轮(Epoch)中:
- 前向传播:将训练数据输入模型,得到预测输出。
- 计算损失:用损失函数计算预测输出和真实标签的差距。
- 反向传播:
optimizer.zero_grad()
:清除上一轮的梯度。loss.backward()
:计算当前损失关于模型各参数的梯度。optimizer.step()
:优化器根据梯度更新模型参数。
num_epochs = 20000 # 训练的总轮数
losses = [] # 用于存储每一轮的损失值for epoch in range(num_epochs):# 前向传播outputs = model(X_train) # 隐式调用forward方法loss = criterion(outputs, y_train)# 反向传播和优化optimizer.zero_grad() # 梯度清零loss.backward() # 计算梯度optimizer.step() # 更新参数# 记录损失值losses.append(loss.item()) # .item()用于从张量中提取数值# 每100轮打印一次训练信息if (epoch + 1) % 100 == 0:print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
e. 可视化Loss过程
通过绘制损失曲线,我们可以直观地看到模型训练过程中损失是否在稳定下降,这是判断模型是否有效学习的重要依据。
import matplotlib.pyplot as pltplt.plot(range(num_epochs), losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss over Epochs')
plt.show()
从图中可以看到,损失值随着训练轮数的增加而平稳下降并最终收敛,说明我们的模型学到东西了!
@浙大疏锦行