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。
交叉熵损失函数
交叉熵是在分类任务中,衡量模型预测结果好坏的一种损失函数。
它的核心思想是衡量两个概率分布之间的差异。 在机器学习中,这两个分布分别是:
- 真实分布 (True Distribution): 这是正确答案的概率分布。在分类问题中,它是一个“one-hot”向量。例如,一个三分类问题(猫、狗、鸟),一张猫的图片其真实分布就是
[1, 0, 0],表示“是猫的概率为100%,是狗的概率为0%,是鸟的概率为0%”。 - 预测分布 (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]区间的概率值取对数后产生的负号,从而将损失值转化为一个我们习惯于优化的、非负的、越小越好的正数。

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

核心比喻 (下山):
- 站在山坡任意一点(随机初始化
w和b)。 - 感受当前位置最陡峭的下坡方向(计算梯度)。
- 朝着这个方向迈出一小步(用学习率更新参数)。
- 不断重复,直到走到山谷最低点(代价函数的最小值)。
- 站在山坡任意一点(随机初始化
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

Tanh

ReLu

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


矩阵计算

线性回归(线性模型)
1. 核心目标
线性模型的目标是找到一个线性的、直线的函数关系,来描述输入特征 X 和输出标签 y 之间的关系。
2. 假设函数 (Hypothesis Function)
- 公式: $y' = wX + b$
参数说明:
X: 输入特征 (e.g., 房屋面积)。y': 模型的预测值 (e.g., 预测的房价)。w: 权重 (Weight),代表特征的重要性 (e.g., 每平米多少钱)。b: 偏置 (Bias),代表模型的基准线或偏移量 (e.g., 房屋的起步价)。
- 学习目标: 找到最优的
w和b,使得模型的预测值 y′ 无限接近真实值y。
逻辑回归(分类问题)
1. 核心目标
在线性回归模型的基础上,使用概率模型(如Sigmoid函数),将线性模型的结果压缩到[0,1]之间,使其拥有概率意义,它可以将任意输入映射到[0,1]区间,实现值到概率转换
处理多维特征的输入
利用矩阵的空间变化,讲高维降到低维


建立模型
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加载数据集

Epoch
一个 Epoch 指的是整个训练数据集中的所有样本都被模型“过目”了一遍的过程(forward+backward)
Batch-Size
一次迭代(即一次参数更新)中所用到的样本数量。
Batch-Size 越大: 每次更新时考虑的样本更多,梯度方向更准确、稳定;能更好地利用硬件的并行计算能力,每个Epoch的训练时间更短。但需要更多内存。
Batch-Size 越小: 引入的随机性更大,训练过程更“震荡”,有时反而有助于模型跳出局部最优解;内存占用小。但训练时间可能更长。
Iterations
完成一个 Epoch所需要的批次数量,也等于一个Epoch中模型参数更新的次数。
完整的例子
- 总样本数 (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),并将它们转换成一个概率分布。
特性:
- 大于0: 输出的每个数值都在 (0, 1) 区间内。
- 和为1: 所有输出数值的总和等于1。
NLLLoss

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。

Pytorch中的CrossEntrepyLoss

nn.CrossEntropyLoss = LogSoftmax + NLLLoss
- 将模型的原始、未经任何激活函数处理的 logits 直接喂给它。
- 将非One-hot形式的、整数的真实标签(例如
2)也喂给它。 - 它会在内部自动帮你完成
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?)
StandardScaler是sklearn库中提供的一种特征缩放方法,它的策略是标准化 (Standardization)。
它会把每一列特征的数据都转换成均值为0,标准差为1的分布。这就像把每个班级的考试成绩都转换成标准分,这样就可以公平地比较不同班级的学生表现了。
3. fit_transform 和 transform 的区别 (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(): 对验证集和测试集使用,用从训练集学到的规则来转换新数据。
全连接神经网络
神经网络的本质是寻找非线性的空间变换函数

卷积神经网络(CNN)

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

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

步幅决定了卷积核在图片上滑动的步伐大小。
步幅 (Stride) = 1:这是最常见的情况。卷积核每次向右(或向下)只移动1个像素。这样可以对图片进行最精细、最密集的扫描。步幅 (Stride) = 2:卷积核每次移动2个像素。它会跳过一些像素,扫描得更粗略。
步幅是控制输出尺寸的一个重要工具。步幅越大,卷积核滑动的总次数就越少,最终得到的输出特征图尺寸就越小。 这也是一种实现下采样(Downsampling)的方式。
填充
在进行卷积运算之前,通常会在图片的四周填充一圈或多圈的0。这主要有两个非常重要的目的:
- 保持特征图的尺寸:如果不做填充,每经过一次卷积,特征图的尺寸就会缩小(例如,
5x5的图片用3x3的卷积核处理后,输出会变成3x3)。如果网络很深,图片很快就会变得太小,丢失空间信息。通过填充,我们可以让输出尺寸与输入尺寸保持一致。 - 公平处理边缘像素:图片最边缘的像素点,被卷积核中心扫过的次数,远少于中心的像素点。这会导致边缘信息没有被充分利用。填充一圈0可以确保图片的所有像素(包括边缘)都被公平、充分地处理。

特征图输出
卷积核在整张输入图片上(可能经过了填充)滑动运算后,所得到的所有结果数字,共同组成了一个新的二维矩阵,这个矩阵就叫做特征图 (Feature Map) 或 激活图 (Activation Map)。
特征图上的每一个点,都代表了卷积核在原始图片对应位置上探测到的特征强度。 它是一张描绘了“某个特定特征在原图何处出现”的地图。
输出尺寸是如何决定的? 输出特征图的尺寸由以下四个因素共同决定:
W:输入图片的尺寸 (例如5)F:卷积核的尺寸 (例如3)P:填充的圈数 (例如0或1)S:步幅的大小 (例如1或2)
输出尺寸的计算公式为:
$$ W_{out} = \frac{W + 2P-F}{S} + 1 $$

多通道卷积

- 假设一个图片分rgb有3个通道,那么一个卷积核必须也有3个通道。
- 每一个通道进行卷积计算最后相加输出一个特征图。
设置一个有n个卷积核的卷积层
- 一个卷积核 -> 一个特征图,N个卷积核 -> N个特征图
- 每个卷积核都独立地在输入图像上进行运算,各自生成一个特征图。
- 堆叠输出: 最后,我们将这N个特征图沿着深度方向堆叠在一起,形成一个
WxHxN的输出数据块(张量)。
池化层
不负责提取新的特征(因为它没有可学习的参数)。它负责对已有的特征图进行整合与浓缩 (Summarize / Aggregate)。
核心目的:
大幅降低特征图的尺寸(下采样):比如把一个28x28的特征图降到14x14。这能极大地减少后续网络层的计算量和参数数量,让网络更轻、更快。
增加特征的“局部不变性” (Local Invariance):这是池化一个非常重要的作用。它能让网络对特征在图片中的微小位移不那么敏感,从而使模型更加“鲁棒”(Robust)。
最大池化运算

只选择最大的那个数值作为输出
只关心这个小区域内有没有出现非常强的特征信号。只要有一个强的,它就报告“有强信号”。它保留了最显著的特征,忽略了背景噪声。
平均池化运算

计算区域内数值的平均值作为输出
它考虑了区域内所有信号的强度,给出了一个“整体表现”的平均分。它会把特征信号平滑化。
池化和步幅卷积的区别
步幅卷积 (Strided Convolution):跳跃式提取特征
- 工作方式:一个步幅为2的卷积,只访问偶数,然后根据这些信息提取特征。
- 信息处理:完全忽略(省略)了所有奇数。。
- 结论:步幅卷积通过稀疏采样(跳着采样)来达到降维的目的。它会直接丢弃掉一些位置的信息。
池化层 (Pooling Layer):先全面调查再总结的“片区经理”
- 工作方式:池化层通常跟在一个步幅为1的“精细”卷积层后面。先形成了一份特征图。然后池化层,进行提取。
- 信息处理:没有忽略任何信息。根据一个规则,提炼出最重要的信息。
- 结论:池化层是在完整信息的基础上进行局部信息的浓缩与概括。它不是“省略”信息,而是“总结”信息。
这就是二者最根本的区别。步幅卷积是“省略式降维”,池化是“总结式降维”。
总结:卷积层是为了提取特征,池化是为了下采样压缩数据