【Python打卡Day33】简单神经网络@浙大疏锦行

发布于:2025-06-12 ⋅ 阅读:(21) ⋅ 点赞:(0)

# DAY33

默认大家已经有一定的神经网络基础,该部分已经在复试班的深度学习部分介绍完毕,如果没有,你需要自行了解下MLP的概念。

你需要知道

梯度下降的思想

激活函数的作用

损失函数的作用

优化器

神经网络的概念

神经网络由于内部比较灵活,所以封装的比较浅,可以对模型做非常多的改进,而不像机器学习三行代码固定。


## 🧩 1. 神经网络的概念

*   **核心思想:** 模仿人脑神经元的工作方式(尽管是极度简化的版本),从数据中学习复杂的模式和关系。
*   **结构:**
    *   **输入层:** 接收原始数据(例如,一张图片的像素值、一段文本的单词编码、一个商品的多个特征)。每个输入节点代表数据的一个特征。
    *   **隐藏层:** 位于输入层和输出层之间,可以有一层或多层(“深度”学习就源于此)。这是网络进行复杂计算和特征提取的地方。每个隐藏层由多个**神经元**(或**节点**)组成。
    *   **输出层:** 产生网络的最终预测结果(例如,图片属于“猫”或“狗”的概率、明天的预测温度、用户点击广告的可能性)。节点数量取决于任务类型(分类任务通常对应类别数,回归任务通常1个节点)。
*   **神经元的工作方式:**
    1.  **接收输入:** 一个神经元接收来自前一层所有神经元的输入信号(`x₁, x₂, ..., xn`)。
    2.  **加权求和:** 每个输入信号乘以一个对应的**权重**(`w₁, w₂, ..., wn`),然后加上一个**偏置**(`b`)。`z = (w₁ * x₁) + (w₂ * x₂) + ... + (wn * xn) + b`。权重代表该输入信号的重要性,偏置提供灵活性。
    3.  **激活函数:** 对加权求和的结果 `z` 应用一个**激活函数**(`f`)。`a = f(z)`。这是引入**非线性**的关键步骤!没有它,多层网络就等价于单层网络,无法学习复杂模式。
    4.  **输出:** 计算得到的激活值 `a` 作为该神经元的输出,传递给下一层的神经元。
*   **连接:** 通常,每一层的每个神经元都与下一层的所有神经元相连(全连接层)。连接上承载着权重值。
*   **目标:** 通过调整网络中所有的权重 (`w`) 和偏置 (`b`),使得网络对于给定的输入,能够输出尽可能接近期望(真实)的输出。

## 📉 2. 损失函数的作用

*   **定义:** 损失函数(Loss Function),有时也叫代价函数(Cost Function),是一个**衡量模型预测值 (`ŷ`) 与真实值 (`y`) 之间差距(误差)的函数**。
*   **核心作用:**
    *   **量化性能:** 它给模型的当前表现打了一个“分数”。损失值越大,说明模型预测得越差;损失值越小,说明预测得越准。
    *   **优化目标:** 神经网络训练的核心目标就是找到一组权重和偏置参数,使得这个损失函数的值**最小化**。它是驱动模型学习的“指挥棒”。
*   **常见例子:**
    *   **均方误差:** 常用于**回归问题**(预测连续值,如房价、温度)。计算所有预测值与真实值之差的平方的平均值。`MSE = (1/n) * Σ(ŷᵢ - yᵢ)²`。
    *   **交叉熵损失:** 常用于**分类问题**(预测类别概率,如图像分类)。它特别擅长衡量两个概率分布(模型预测的概率分布 vs 真实的one-hot编码分布)之间的差异。当预测概率与真实类别(概率为1)越接近时,交叉熵损失越小。
*   **为什么需要它?** 没有损失函数,模型就不知道自己的预测是好是坏,也不知道该往哪个方向改进。它是模型学习的“灯塔”。

## 📈 3. 梯度下降的思想

*   **目标:** 找到损失函数的**最小值点**所对应的模型参数(权重 `w` 和偏置 `b`)。
*   **核心比喻:** 想象你蒙着眼站在一座崎岖的山上(山的高度代表损失值),目标是找到最低的山谷(最小损失)。你只能通过用脚感受脚下坡度的陡峭程度(梯度)来判断方向。
*   **步骤:**
    1.  **初始化:** 随机选择一个起始点(随机初始化模型的权重 `w` 和偏置 `b`)。
    2.  **计算梯度:** 在当前参数位置,计算损失函数关于每个参数(每个 `w` 和 `b`)的**梯度**。
        *   **梯度是什么?** 它是一个向量(可以看作一个箭头),指向损失函数在当前点**上升最快**的方向。那么,梯度的反方向(`-梯度`)就指向损失函数**下降最快**的方向。
        *   **计算方式:** 使用微积分的链式法则(反向传播算法高效计算整个网络的梯度)。
    3.  **更新参数:** 沿着梯度的反方向(下坡方向)迈出一步,更新参数。
        *   `新参数 = 旧参数 - 学习率 * 梯度`
        *   **学习率:** 一个非常重要的超参数!它控制着每次更新**步长**的大小。
            *   学习率太小:下山速度太慢,训练时间长,可能卡在局部极小点。
            *   学习率太大:步子迈得太大,可能跳过最低点,甚至导致损失震荡上升(发散)。
    4.  **重复:** 重复步骤2和3(计算新位置的梯度,更新参数),直到达到停止条件(例如,损失不再显著下降、达到预设的迭代次数)。
*   **为什么有效?** 梯度提供了最陡峭的下降方向,是找到局部最小值(有时甚至是全局最小值)的高效方法。

## ⚙ 4. 优化器

*   **定义:** 优化器是实现梯度下降算法的具体策略或变体。它决定了如何利用计算的梯度来更新模型的参数。
*   **为什么需要优化器?** 基础的梯度下降(也称为**批量梯度下降**)有一些缺点:
    *   计算整个训练集的梯度非常慢(尤其大数据集)。
    *   容易陷入局部极小点或鞍点(梯度为0但不是最优的点)。
    *   在山谷中震荡前进,收敛慢。
*   **常见优化器(都是对基础梯度下降的改进):**
    *   **随机梯度下降:** 每次随机选取**一个**样本计算梯度并更新参数。速度快,波动大,有“噪声”可能帮助跳出局部极小点,但收敛路径不稳定。
    *   **小批量梯度下降:** **最常用!** 每次随机选取**一小批**样本(如32、64、128个)计算梯度并更新参数。平衡了计算效率和稳定性,是SGD和BGD的折中。
    *   **动量:** 引入“动量”概念,不仅考虑当前梯度,还考虑之前更新的方向(类似于物理中的惯性)。有助于加速在相关方向上的学习,抑制震荡,更容易冲出局部极小点或平坦区域。
    *   **RMSProp:** 自适应地调整每个参数的学习率。对频繁更新的参数使用较小的学习率,对不频繁更新的参数使用较大的学习率。有助于处理稀疏数据。
    *   **Adam:** **最流行!** 结合了动量(一阶矩估计)和RMSProp(二阶矩估计)的思想。通常能快速收敛且对超参数(尤其是学习率)相对鲁棒。它自适应地为每个参数计算不同的学习率。
*   **核心作用:** 优化器决定了如何更高效、更稳定地利用梯度信息来最小化损失函数,加速训练过程,并可能找到更好的解。

## ⚡ 5. 激活函数的作用

*   **定义:** 应用于神经元加权求和输出 `z` 上的**非线性函数**。`a = f(z)`。
*   **核心作用:**
    *   **引入非线性:** 这是激活函数最重要的作用!没有非线性激活函数,无论神经网络有多少层,最终输出都只是输入特征的线性组合(叠加多个线性变换还是线性变换)。**非线性**使得神经网络能够学习和表示现实世界中极其复杂的非线性关系(如图像识别、自然语言理解)。这是神经网络强大拟合能力的根源。
    *   **决定神经元是否激活:** 根据输入信号的加权和,激活函数决定该神经元是否被“激活”(即输出一个较强的信号)或抑制(输出一个较弱的信号或0)。
*   **常见激活函数:**
    *   **Sigmoid:** `f(z) = 1 / (1 + e⁻ᶻ)`。输出范围(0, 1),常用于二分类问题的输出层(表示概率)。**缺点:** 容易导致梯度消失(当输入很大或很小时,梯度接近0,导致深层网络训练困难);输出不以0为中心。
    *   **Tanh:** `f(z) = (eᶻ - e⁻ᶻ) / (eᶻ + e⁻ᶻ)`。输出范围(-1, 1),比Sigmoid以0为中心,收敛通常更快。**缺点:** 同样存在梯度消失问题。
    *   **ReLU:** `f(z) = max(0, z)`。这是目前**最常用**的隐藏层激活函数!计算简单高效;在正区间解决了梯度消失问题(梯度恒为1);生物学解释性相对较好。**缺点:** “Dying ReLU”问题(输入为负时梯度为0,神经元永久失效,不再更新)。变种如Leaky ReLU、Parametric ReLU (PReLU)、Exponential Linear Unit (ELU) 试图解决此问题。
    *   **Softmax:** 常用于**多分类问题**的**输出层**。它将多个神经元的输出(通常称为logits)转换为一个概率分布(所有输出值在0到1之间,且总和为1)。`Softmax(zᵢ) = eᶻⁱ / Σⱼ eᶻʲ`。
*   **为什么必须要有它?** 没有激活函数,神经网络就退化为线性回归模型,无法解决任何稍微复杂的非线性问题。它是神经网络能力的“灵魂”。

## 🌀 总结:它们如何协同工作?

1.  **初始化:** 构建一个神经网络结构(输入层、隐藏层、输出层、激活函数),随机初始化所有参数 (`w`, `b`)。
2.  **前向传播:** 输入一批数据,通过网络逐层计算(加权求和 -> 激活函数),得到最终的预测输出 `ŷ`。
3.  **计算损失:** 使用**损失函数**计算预测值 `ŷ` 和真实标签 `y` 之间的误差 `L`。
4.  **反向传播:** 通过链式法则,从输出层向输入层反向计算损失函数 `L` 关于网络中**每一个参数 (`w`, `b`)** 的**梯度** (`∂L/∂w`, `∂L/∂b`)。这告诉我们每个参数对总误差的“贡献”大小和方向。
5.  **参数更新:** **优化器** 接收这些梯度,并根据其特定的算法(如SGD, Adam)使用公式(如 `w = w - 学习率 * ∂L/∂w`)来更新所有的权重和偏置。目标就是让损失 `L` **减小**。
6.  **重复:** 重复步骤2-5(前向传播 -> 计算损失 -> 反向传播 -> 优化器更新参数),遍历整个训练数据集多次(每个完整遍历称为一个Epoch),直到模型性能(损失)不再显著提升或达到预设停止条件。

**打个比方:**

*   **神经网络** 是一个复杂的学习机器(像学生的大脑🧠)。
*   **损失函数** 是考试分数📝 - 告诉学生(模型)这次考得有多差。
*   **梯度** 是试卷的详细解析📖 - 指出每道错题(每个参数)的错误方向和程度。
*   **优化器** 是学生的学习方法📚 - 他如何根据错题解析(梯度)来调整自己的知识(参数)。是死记硬背(SGD)?还是总结规律(动量)?或是查漏补缺(自适应优化器)?
*   **激活函数** 是学生大脑神经元处理信息的方式⚡ - 决定信息是被强化传递还是被抑制(非线性思考能力的关键)。
*   **梯度下降** 是整个学习过程的核心理念📉 - 通过不断分析错误(计算梯度)并针对性改进(更新参数)来降低错误(最小化损失)。


PyTorch的安装

我们后续完成深度学习项目中,主要使用的包为pytorch,所以需要安装,你需要去配置一个新的环境。

未来在复现具体项目时候,新环境命名最好是python版本_pytorch版本_cuda版本,例如 py3.10_pytorch2.0_cuda12.2 ,因为复杂项目对运行环境有要求,所以需要安装对应版本的包。

我们目前主要不用这么严格,先创建一个命名为DL的新环境即可,也可以沿用之前的环境

conda create -n DL python=3.8
conda env list 
conda activate DL
conda install jupyter (如果conda无法安装jupyter就参考环境配置文档的pip安装方法)
pip insatll scikit-learn
然后对着下列教程安装pytorch

深度学习主要是简单的并行计算,所以gpu优势更大,简单的计算cpu发挥不出来他的价值,我们之前说过显卡和cpu的区别:

  1. cpu是1个博士生,能够完成复杂的计算,串行能力强。
  2. gpu是100个小学生,能够完成简单的计算,人多计算的快。

这里的gpu指的是英伟达的显卡,它支持cuda可以提高并行计算的能力。

如果你是amd的显卡、苹果的电脑,那样就不需要安装cuda了,直接安装pytorch-gpu版本即可。cuda只支持nvidia的显卡。

安装教程

或者去b站随便搜个pytorch安装视频。

  1. 怕麻烦直接安装cpu版本的pytorch,跑通了用云服务器版本的pytorch-gpu
  2. gpu的pytorch还需要额外安装cuda cudnn组件

准备工作

可以在你电脑的cmd中输入nvidia-smi来查看下显卡信息

其中最重要的2个信息,分别是:

  1. 显卡目前驱动下最高支持的cuda版本,12.7
  2. 显存大小,12288 MiB ÷ 1024 = 12

PS:之所以输入这个命令,可以弹出这些信息,是因为为系统正确安装了 NVIDIA 显卡驱动程序,并且相关路径被添加到了环境变量中。如果你不是英伟达的显卡,自然无法使用这个命令。

安装好后

import torch
 
print(torch.__version__)
print(torch.version.cuda)
print(torch.cuda.is_available())  #输出为True,则安装成功

2.7.1+cu128
12.8
True

import torch

# 检查CUDA是否可用
if torch.cuda.is_available():
    print("CUDA可用!")
    # 获取可用的CUDA设备数量
    device_count = torch.cuda.device_count()
    print(f"可用的CUDA设备数量: {device_count}")
    # 获取当前使用的CUDA设备索引
    current_device = torch.cuda.current_device()
    print(f"当前使用的CUDA设备索引: {current_device}")
    # 获取当前CUDA设备的名称
    device_name = torch.cuda.get_device_name(current_device)
    print(f"当前CUDA设备的名称: {device_name}")
    # 获取CUDA版本
    cuda_version = torch.version.cuda
    print(f"CUDA版本: {cuda_version}")
else:
    print("CUDA不可用。")

CUDA可用!
可用的CUDA设备数量: 1
当前使用的CUDA设备索引: 0
当前CUDA设备的名称: NVIDIA GeForce RTX 5070 Laptop GPU
CUDA版本: 12.8

# 仍然用4特征,3分类的鸢尾花数据集作为我们今天的数据集
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import numpy as np

# 加载鸢尾花数据集
iris = load_iris()
X = iris.data  # 特征数据
y = iris.target  # 标签数据
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 打印下尺寸
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)

(120, 4)
(120,)
(30, 4)
(30,)

# 归一化数据,神经网络对于输入数据的尺寸敏感,归一化是最常见的处理方式
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test) #确保训练集和测试集是相同的缩放

# 将数据转换为 PyTorch 张量,因为 PyTorch 使用张量进行训练
# y_train和y_test是整数,所以需要转化为long类型,如果是float32,会输出1.0 0.0
X_train = torch.FloatTensor(X_train)
y_train = torch.LongTensor(y_train)
X_test = torch.FloatTensor(X_test)
y_test = torch.LongTensor(y_test)

## 2.模型架构定义

定义一个简单的全连接神经网络模型,包含一个输入层、一个隐藏层和一个输出层。

定义层数+定义前向传播顺序

import torch
import torch.nn as nn
import torch.optim as optim

class MLP(nn.Module): # 定义一个多层感知机(MLP)模型,继承父类nn.Module
    def __init__(self): # 初始化函数
        super(MLP, self).__init__() # 调用父类的初始化函数
 # 前三行是八股文,后面的是自定义的

        self.fc1 = nn.Linear(4, 10)  # 输入层到隐藏层
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(10, 3)  # 隐藏层到输出层
# 输出层不需要激活函数,因为后面会用到交叉熵函数cross_entropy,交叉熵函数内部有softmax函数,会把输出转化为概率

    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out)
        out = self.fc2(out)
        return out

# 实例化模型
model = MLP()

#其实模型层的写法有很多,relu也可以不写,在后面前向传播的时候计算下即可,因为relu其实不算一个层,只是个计算而已。

    # def forward(self,x): #前向传播
    #     x=torch.relu(self.fc1(x)) #激活函数
    #     x=self.fc2(x) #输出层不需要激活函数,因为后面会用到交叉熵函数cross_entropy
    #     return x

## 3.模型训练(CPU版本)

定义损失函数和优化器

# 分类问题使用交叉熵损失函数
criterion = nn.CrossEntropyLoss()

# 使用随机梯度下降优化器
optimizer = optim.SGD(model.parameters(), lr=0.01)

# # 使用自适应学习率的化器
optimizer = optim.Adam(model.parameters(), lr=0.001)




开始循环训练

实际上在训练的时候,可以同时观察每个epoch训练完后测试集的表现:测试集的loss和准确度

# 训练模型
num_epochs = 20000 # 训练的轮数

# 用于存储每个 epoch 的损失值
losses = []

for epoch in range(num_epochs): # range是从0开始,所以epoch是从0开始
    # 前向传播
    outputs = model.forward(X_train)   # 显式调用forward函数
    # outputs = model(X_train)  # 常见写法隐式调用forward函数,其实是用了model类的__call__方法
    loss = criterion(outputs, y_train) # output是模型预测值,y_train是真实标签

    # 反向传播和优化
    optimizer.zero_grad() #梯度清零,因为PyTorch会累积梯度,所以每次迭代需要清零,梯度累计是那种小的bitchsize模拟大的bitchsize
    loss.backward() # 反向传播计算梯度
    optimizer.step() # 更新参数

    # 记录损失值
    losses.append(loss.item())

    # 打印训练信息
    if (epoch + 1) % 100 == 0: # range是从0开始,所以epoch+1是从当前epoch开始,每100个epoch打印一次
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

如果你重新运行上面这段训练循环,模型参数、优化器状态和梯度会继续保留,导致训练结果叠加,模型参数和优化器状态(如动量、学习率等)不会被重置。这会导致训练从之前的状态继续,而不是从头开始

## 4.可视化结果

import matplotlib.pyplot as plt
# 可视化损失曲线
plt.plot(range(num_epochs), losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss over Epochs')
plt.show()

@浙大疏锦行


网站公告

今日签到

点亮在社区的每一天
去签到