Pytorch深度学习实践

深度学习基础

损失函数

(Cost Function / Loss Function)

评价模型的预测值和真实值不一样的程度,用来衡量模型“犯错”程度的函数,即预测值与真实值之间的差距。

损失 (Loss): 通常指单个样本的误差。

  • 公式:
    $$Loss=(y'-y)^2$$
    损失函数:
  • 评价模型性能,既预测结果和真实结果越接近性能越好。
  • 参数优化,通过计算损失函数,进行反向传播,不断更新参数。

均方差损失函数

用于线性回归问题,计算预测值与真实值之间的平均平方差

常用公式 (MSE - 均方误差):

  • $$ Cost(w,b) = \frac{1}{N} \sum_{i=1}^{N} \left(y_i' - y_i\right)^2 = \frac{1}{N} \sum_{i=1}^{N} \left(wx_i + b - y_i\right)^2 $$

核心思想: Cost 值越小,说明模型越好。我们所有优化的目标,就是最小化这个 Cost。

交叉熵损失函数

交叉熵是在分类任务中,衡量模型预测结果好坏的一种损失函数

它的核心思想是衡量两个概率分布之间的差异。 在机器学习中,这两个分布分别是:

  1. 真实分布 (True Distribution): 这是正确答案的概率分布。在分类问题中,它是一个“one-hot”向量。例如,一个三分类问题(猫、狗、鸟),一张猫的图片其真实分布就是 [1, 0, 0],表示“是猫的概率为100%,是狗的概率为0%,是鸟的概率为0%”。
  2. 预测分布 (Predicted Distribution): 这是你的模型经过计算后,输出的每个类别的预测概率。例如,模型可能预测这张图片是 [0.7, 0.2, 0.1],表示“70%的可能是猫,20%是狗,10%是鸟”。

交叉熵损失函数的作用就是计算这两个分布之间的“距离”。如果模型的预测分布与真实分布越接近,交叉熵损失就越小;反之,如果相差越大,损失就越大。 我们的训练目标就是通过调整模型参数,来最小化这个交叉熵损失。
$$L(y,y') = -[y \cdot \log(y') + (1-y) \cdot \log(1-y')]$$
负号的作用是将这个负的对数损失“扳正”,变成一个正数抵消对数函数对(0,1]区间的概率值取对数后产生的负号,从而将损失值转化为一个我们习惯于优化的、非负的、越小越好的正数。

image-20250709104104227

梯度下降

梯度下降是一种优化算法,用于寻找函数(在这里是代价函数)的最小值。

beab0e92-857d-4ad5-8313-5e7c6a4e8d33

  • 核心比喻 (下山):

    1. 站在山坡任意一点(随机初始化 wb)。
    2. 感受当前位置最陡峭的下坡方向(计算梯度)。
    3. 朝着这个方向迈出一小步(用学习率更新参数)。
    4. 不断重复,直到走到山谷最低点(代价函数的最小值)。

2. 关键组成

  • 梯度 (Gradient / 导数): $\frac{\partial Cost}{\partial w}$

    • 定义: 代价函数在某一点的斜率,指向函数值上升最快的方向。
    • 作用: 梯度的反方向 (-gradient) 就是代价函数值下降最快的方向。
  • 学习率 (Learning Rate, α):

    • 定义: 每次更新参数时迈出的“步长”。
    • 作用: 控制学习的速度。太小则收敛过慢,太大则可能在最低点附近来回“震荡”,甚至错过最低点。

3. 更新规则 (The Update Rule)

梯度下降算法的核心迭代公式。

  • 公式:

$$ w := w - \alpha \frac{\partial Cost}{\partial w} $$

$$ b := b - \alpha \frac{\partial Cost}{\partial b} $$

  • 工作原理:

    • w 的新值,等于 w 的旧值,减去 学习率 乘以 w 方向的梯度。
    • b 的更新同理。

4. 梯度下降的变种

特性批量梯度下降 (BGD)随机梯度下降 (SGD)小批量梯度下降 (Mini-batch GD)
每次更新数据全部训练数据1个随机样本一小批随机样本 (e.g., 32)
优点方向准确,路径平滑更新速度快效率与稳定性的最佳平衡
缺点计算开销大,慢路径震荡,不稳定需额外设置批大小
现状数据量大时基本不用很少单独使用现代深度学习的标配

激活函数

类似于大脑中的神经元: 一个神经元会接收来自成百上千个其他神经元的电信号。它把这些信号全部加起来。但它不是简单地把这个总和再传下去。它有一个“触发阈值”。

  • 如果所有信号的总和非常微弱,低于这个阈值,这个神经元就保持沉默,什么也不做,我们说它“未被激活”。
  • 如果信号总和足够强,超过了阈值,这个神经元就会“开火”(Fire),产生一个强烈的电脉冲,传递给下游的神经元。我们说它“被激活了”。

神经网络中的激活函数: 它扮演的就是这个“触发机制”或“开关”的角色。

  • 神经元先把所有输入 x 和对应的权重 w 相乘再求和,得到一个总的输入信号 z = Σwᵢxᵢ + b
  • 然后,激活函数会接收这个总信号z,并“决定”这个神经元最终应该输出什么。它决定了神经元是否“开火”,以及“火力”有多猛。

    目的是为了通过在架构中强制加入非线性模块(激活函数),赋予了它塑造非线性关系的能力

Sigmoid

image-20250810151219841

Tanh

image-20250810151533195

ReLu

image-20250810151718818

反向传播

反向传播算法利用链式法则,通过从输出层向输入层逐层计算误差梯度,高效求解神经网络参数的偏导数,以实现网络参数的优化和损失函数的最小化。

一个简单的例子

img_v3_02p4_599dfe25-6b49-44da-83e1-6c947049c9dg

img_v3_02p4_4910598c-c0b7-48ce-ba51-4512a5318b6g

矩阵计算

img_v3_02p4_82b415b5-05a3-4c70-b7a2-1e94cafd37cg

线性回归(线性模型)

1. 核心目标

线性模型的目标是找到一个线性的、直线的函数关系,来描述输入特征 X 和输出标签 y 之间的关系。

2. 假设函数 (Hypothesis Function)

  • 公式: $y' = wX + b$
  • 参数说明:

    • X: 输入特征 (e.g., 房屋面积)。
    • y': 模型的预测值 (e.g., 预测的房价)。
    • w: 权重 (Weight),代表特征的重要性 (e.g., 每平米多少钱)。
    • b: 偏置 (Bias),代表模型的基准线或偏移量 (e.g., 房屋的起步价)。
  • 学习目标: 找到最优的 wb,使得模型的预测值 y′ 无限接近真实值 y

逻辑回归(分类问题)

1. 核心目标

在线性回归模型的基础上,使用概率模型(如Sigmoid函数),将线性模型的结果压缩到[0,1]之间,使其拥有概率意义,它可以将任意输入映射到[0,1]区间,实现值到概率转换

处理多维特征的输入

利用矩阵的空间变化,讲高维降到低维

image-20250709160930623

image-20250709161006391

建立模型

class DiabetesModel(nn.Module):
    def __init__(self, input_size):
        super(DiabetesModel, self).__init__()
        # 定义网络结构
        self.layer1 = nn.Linear(input_size, 32) # 输入层 -> 隐藏层1
        self.relu = nn.ReLU() # ReLU激活函数
        self.layer2 = nn.Linear(32, 16)        # 隐藏层1 -> 隐藏层2
        self.output_layer = nn.Linear(16, 1)   # 隐藏层2 -> 输出层

    def forward(self, x):
        # 定义前向传播路径
        x = self.layer1(x)
        x = self.relu(x)
        x = self.layer2(x)
        x = self.relu(x)
        x = self.output_layer(x)
        return x

加载数据集

image-20250709180548150

Epoch

一个 Epoch 指的是整个训练数据集中的所有样本都被模型“过目”了一遍的过程(forward+backward)

Batch-Size

一次迭代(即一次参数更新)中所用到的样本数量

Batch-Size 越大: 每次更新时考虑的样本更多,梯度方向更准确、稳定;能更好地利用硬件的并行计算能力,每个Epoch的训练时间更短。但需要更多内存。

Batch-Size 越小: 引入的随机性更大,训练过程更“震荡”,有时反而有助于模型跳出局部最优解;内存占用小。但训练时间可能更长。

Iterations

完成一个 Epoch所需要的批次数量,也等于一个Epoch中模型参数更新的次数image-20250709181234308

完整的例子

  • 总样本数 (Total Samples): 8000
  • 批量大小 (Batch-Size): 100
  • 轮次数 (Epochs): 10

我们可以计算出:

  • 每个Epoch的迭代次数 (Iterations per Epoch): 8000 / 100 = 80
  • 总迭代次数 (Total Iterations): 10 Epochs * 80 Iterations/Epoch = 800

Dataset和DataLoader

class DiabetesDataset(Dataset):
    def __init__(self):
        pass

    def __len__(self):
        pass

    def __getitem__(self, idx):
        pass

dataset = DiabetesDataset()
train_loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=2)

1. def __init__(self): (构造函数初始化)

在创建数据集实例时调用一次,例如 dataset = DiabetesDataset()。负责所有一次性的准备工作。

加载数据文件的路径。将所有数据一次性读入内存。计算并存储数据集的总长度

2. def __len__(self): (数据总数)

DataLoader在初始化时,以及在每个epoch开始前,会调用这个方法。必须返回一个整数,代表这个数据集中样本的总数量。通常就是简单地返回在 __init__ 中计算好的 self.len

3. def __getitem__(self, index): (根据索引取出数据)

DataLoader在构建一个批次(batch)时,会频繁地、逐个地调用这个方法。根据传入的索引(index)idx,准确地获取并返回一个样本的数据及其对应的标签。

  • self.features 中根据 index 取出第 index 个样本的特征。
  • self.labels 中根据 index 取出第 index 个样本的标签。
  • 将它们作为一个元组 (feature, label) 返回。

DataLoader 的核心参数:

  • dataset=dataset: 告诉叉车要去哪个仓库工作。这里就是我们刚刚实例化的dataset对象。
  • batch_size=32: 定义一次有多少min-batch。这里它会一次性从数据集中取出32个样本,并将它们打包成一个批次(batch)。
  • shuffle=True: True表示在每个epoch开始前,索引完全打乱,可以有效防止模型学习到数据的排列顺序,增强模型的泛化能力。在训练时通常设置为True,在测试时则设置为False
  • num_workers=2: 这是性能优化的关键参数,代表使用多少个子进程来预加载数据

    • 如果 num_workers=0 (默认),数据加载会在主进程中进行。当GPU在训练当前批次时,CPU就在“休息”。
    • 如果 num_workers=2,PyTorch会启动2个额外的进程。当GPU在忙于训练第N个批次时,这两个“工人”已经在后台马不停蹄地准备第N+1、N+2个批次的数据了。这样一来,GPU训练完后无需等待,可以直接拿到新数据开始下一轮计算,极大地提高了训练效率

多分类问题(Softmax 分类器)

Softmax函数

$$p(y=k|x)=\frac{e^{x_{y}}}{\sum_{j=1}^{K}e^{x_{j}}}$$

Sigmoid 函数是将一个数压缩到 (0, 1) 区间,而 Softmax 函数则是将一组数进行同样的操作,并且让它们的总和为1

  • 作用: Softmax 接收一组任意的实数(logits),并将它们转换成一个概率分布
  • 特性:

    1. 大于0: 输出的每个数值都在 (0, 1) 区间内。
    2. 和为1: 所有输出数值的总和等于1。

NLLLoss

image-20250707103253135

One-Hot编码 描述的是单个样本的真实标签属于哪个类别。它是一个长度等于类别总数的向量,其中,正确类别的位置为1,其余所有位置为0。One-hot编码提供了一个与模型输出的概率分布(如 [0.7, 0.2, 0.1])格式完全对应的真实概率分布,从而可以计算它们之间的交叉熵损失。

NLLLoss (Negative Log Likelihood Loss, 负对数似然损失)

从经过Softmax 计算后的一组对数概率中,“挑出”真实标签所对应的那一个对数概率,再给它取个负号,就得到了最终的损失。

例子:

  • 真实标签: 2 (One-hot: [1, 0, 0])
  • 模型的Softmax输出: [0.38 0.34 0.28]
  • NLLLoss 会挑出索引为 1 的值 -0.97,然后取负号,得到最终 loss = 0.97

image-20250707103439075

Pytorch中的CrossEntrepyLoss

image-20250707103526859

nn.CrossEntropyLoss = LogSoftmax + NLLLoss

  1. 将模型的原始、未经任何激活函数处理的 logits 直接喂给它。
  2. 非One-hot形式的、整数的真实标签(例如 2)也喂给它。
  3. 它会在内部自动帮你完成 Log-Softmax 的计算,以及 NLLLoss 的挑选和取负操作。

特征缩放

# 使用 train_test_split 进行划分
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)

# 特征缩放: 在训练集上fit,然后在训练集和验证集上transform
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val) # 注意:验证集只用transform

将所有数值型特征放在一个公平的起跑线上,以帮助模型更好地学习

1. 为什么需要特征缩放?(Why?)

  • Age: 数值范围大约在 0 到 80 之间。
  • Pclass (船舱等级): 数值范围是 1, 2, 3。

对于一个模型来说,它看到Age的数值(比如40)远大于Pclass的数值(比如2)。如果不对它们进行处理,模型可能会错误地认为Age这个特征比Pclass重要得多,仅仅因为它数值大。

特征缩放的目的就是消除这种由数值范围带来的“偏见”,将所有特征都转换到一个相似的、较小的尺度上。这能让梯度下降等优化算法工作得更稳定、收敛得更快。

2. StandardScaler 是做什么的?(What?)

StandardScalersklearn库中提供的一种特征缩放方法,它的策略是标准化 (Standardization)

它会把每一列特征的数据都转换成均值为0,标准差为1的分布。这就像把每个班级的考试成绩都转换成标准分,这样就可以公平地比较不同班级的学生表现了。

3. fit_transformtransform 的区别 (How?)

这是最关键、也最容易混淆的地方。我们可以用一个“制作模具”的比喻来理解:

  • scaler.fit(X_train): 这是学习制作模具的步骤。程序会只分析训练集 X_train,计算出训练集中每一列特征的平均值(μ)和标准差(σ)。它把这些计算出来的“规则”保存在scaler这个对象里。这就好比根据一块泥土(训练集)制作了一个模具。
  • scaler.transform(X): 这是应用使用模具的步骤。它会使用已经学习到的平均值和标准差,来转换任何给定的数据。它不会再计算新的平均值或标准差。这就好比用已经做好的模具去塑造新的泥土。
  • scaler.fit_transform(X_train): 这是一个方便的快捷方式,它把上面两个步骤合二为一,只对训练集使用。它先在X_train上学习(fit),然后立刻用学到的规则来转换X_train(transform)。

4. 为什么验证集只能用 transform

这是为了模拟真实世界,防止“数据泄露 (Data Leakage)”。

  • 验证集/测试集 的作用是模拟模型在未来遇到的全新的、未知的数据
  • 在现实世界中,我们不可能提前知道未来数据的平均值和标准差。我们唯一拥有的信息就是我们手上的训练集
  • 因此,我们必须用从训练集中学到的“规则”(即平均值和标准差)来处理验证集。我们假装对验证集一无所知,只能用旧的模具来塑造它。

如果对验证集也使用 fit_transform 会发生什么? 那就意味着我们的模型在训练阶段,就已经“偷看”了验证集的数据分布(知道了验证集的平均值和标准差)。这会让模型在验证集上的表现看起来过于乐观,从而导致我们对模型的泛化能力做出错误的评估。

  • fit_transform(): 只对训练集使用,让缩放器学习并转换训练数据。
  • transform(): 对验证集和测试集使用,用从训练集学到的规则来转换新数据。

全连接神经网络

神经网络的本质是寻找非线性的空间变换函数

image-20250808155144807

卷积神经网络(CNN)

image-20250715115138518

卷积层

负责提取特征。它的卷积核是可学习的,专门用来在图片中寻找特定的图案(边缘、纹理、形状等)。

卷积计算

image-20250812165033103

卷积运算的目的利用卷积核是提取局部特征。

  • 拿这卷积核,在原始数据上逐块扫描将2x2的卷积核,覆盖到图片左上角的3x3区域上
  • 进行“逐元素相乘,然后全部相加”(Element-wise multiplication and sum)的计算。也就是说,将卷积核中的4个数字,与其覆盖的原始数据区域中的4个数据,一一对应相乘,最后把这4个乘积全部加起来,得到一个单独的数字
  • 这个数字就代表了卷积核在当前位置“匹配”到的特征强度。很大,说明这个区域的图案和卷积核要找的特征非常像,如果值很小或为负,说明不太像。

步幅

image-20250812171835289

步幅决定了卷积核在图片上滑动的步伐大小

  • 步幅 (Stride) = 1:这是最常见的情况。卷积核每次向右(或向下)只移动1个像素。这样可以对图片进行最精细、最密集的扫描。
  • 步幅 (Stride) = 2:卷积核每次移动2个像素。它会跳过一些像素,扫描得更粗略。

步幅是控制输出尺寸的一个重要工具。步幅越大,卷积核滑动的总次数就越少,最终得到的输出特征图尺寸就越小。 这也是一种实现下采样(Downsampling)的方式。

填充

在进行卷积运算之前,通常会在图片的四周填充一圈或多圈的0。这主要有两个非常重要的目的:

  1. 保持特征图的尺寸:如果不做填充,每经过一次卷积,特征图的尺寸就会缩小(例如,5x5的图片用3x3的卷积核处理后,输出会变成3x3)。如果网络很深,图片很快就会变得太小,丢失空间信息。通过填充,我们可以让输出尺寸与输入尺寸保持一致。
  2. 公平处理边缘像素:图片最边缘的像素点,被卷积核中心扫过的次数,远少于中心的像素点。这会导致边缘信息没有被充分利用。填充一圈0可以确保图片的所有像素(包括边缘)都被公平、充分地处理。

image-20250812172528765

特征图输出

卷积核在整张输入图片上(可能经过了填充)滑动运算后,所得到的所有结果数字,共同组成了一个新的二维矩阵,这个矩阵就叫做特征图 (Feature Map)激活图 (Activation Map)

特征图上的每一个点,都代表了卷积核在原始图片对应位置上探测到的特征强度。 它是一张描绘了“某个特定特征在原图何处出现”的地图。

输出尺寸是如何决定的? 输出特征图的尺寸由以下四个因素共同决定:

  • W:输入图片的尺寸 (例如 5)
  • F:卷积核的尺寸 (例如 3)
  • P:填充的圈数 (例如 01)
  • S:步幅的大小 (例如 12)

输出尺寸的计算公式为:

$$ W_{out} = \frac{W + 2P-F}{S} + 1 $$

image-20250812173713072

多通道卷积

image-20250812174529624

  • 假设一个图片分rgb有3个通道,那么一个卷积核必须也有3个通道。
  • 每一个通道进行卷积计算最后相加输出一个特征图。
  • 设置一个有n个卷积核的卷积层

    • 一个卷积核 -> 一个特征图,N个卷积核 -> N个特征图
    • 每个卷积核都独立地在输入图像上进行运算,各自生成一个特征图。
    • 堆叠输出: 最后,我们将这N个特征图沿着深度方向堆叠在一起,形成一个 WxHxN 的输出数据块(张量)。

池化层

不负责提取新的特征(因为它没有可学习的参数)。它负责对已有的特征图进行整合与浓缩 (Summarize / Aggregate)

核心目的:

大幅降低特征图的尺寸(下采样):比如把一个28x28的特征图降到14x14。这能极大地减少后续网络层的计算量和参数数量,让网络更轻、更快。

增加特征的“局部不变性” (Local Invariance):这是池化一个非常重要的作用。它能让网络对特征在图片中的微小位移不那么敏感,从而使模型更加“鲁棒”(Robust)。

最大池化运算

image-20250812220419818

只选择最大的那个数值作为输出

只关心这个小区域内有没有出现非常强的特征信号。只要有一个强的,它就报告“有强信号”。它保留了最显著的特征,忽略了背景噪声。

平均池化运算

image-20250812220440435

计算区域内数值的平均值作为输出

它考虑了区域内所有信号的强度,给出了一个“整体表现”的平均分。它会把特征信号平滑化。

池化和步幅卷积的区别

步幅卷积 (Strided Convolution):跳跃式提取特征

  • 工作方式:一个步幅为2的卷积,只访问偶数,然后根据这些信息提取特征。
  • 信息处理:完全忽略(省略)了所有奇数。。
  • 结论:步幅卷积通过稀疏采样(跳着采样)来达到降维的目的。它会直接丢弃掉一些位置的信息。

池化层 (Pooling Layer):先全面调查再总结的“片区经理”

  • 工作方式:池化层通常跟在一个步幅为1的“精细”卷积层后面。先形成了一份特征图。然后池化层,进行提取。
  • 信息处理:没有忽略任何信息。根据一个规则,提炼出最重要的信息。
  • 结论:池化层是在完整信息的基础上进行局部信息的浓缩与概括。它不是“省略”信息,而是“总结”信息。

这就是二者最根本的区别。步幅卷积是“省略式降维”,池化是“总结式降维”。

总结:卷积层是为了提取特征,池化是为了下采样压缩数据

循环神经网络(RNN)