机器学习基础

人工智能学习

1.1神经网络基础

E.G.

卷积神经网络–图像识别

1.1.1 MLP 多层感知器(vanilla)

神经元可以被理解为一个装有数字的容器。在标准的神经网络中,一个神经元通常包含以下几个核心部分:

  • 输入 (Input): 外部传入的数据,或者上一层神经元的输出。
  • 权重 (Weights): 代表每个输入对当前神经元影响的大小。
  • 偏置 (Bias): 每个神经元自带的偏移量,相当于这个神经元被激发的“门槛”。
  • 激活函数 (Activation): 将“加权求和”后的结果进行非线性转换,决定最终输出。

E.G.

image-20250517115230333

我们让每一个神经元中标注的数字代表灰度值, 1表示一个纯白像素, 0表示一个纯黑像素, 神经元里装着的值, 叫做激活值,整个图片的2828总共784个神经元组成了网络的*第一层

对于网络的最后一层, 一共输出十个神经元, 每个神经元的激活值, 代表着系统认为输入的图像对应着这个数字的可能性

中间的层数代表着隐含层, 里面进行着处理识别数字的具体工作,隐含层的本质就是将图片拆分后的部分识别出来,再有激活函数组合成整体,从而得出结果

E.G.激活函数的表格表示,红负绿正:乘算取加权和得到要关注的像素

image-20250709181815842

1.1.2 为什么需要激活函数?以及常见激活函数

为什么需要激活函数?
如果没有激活函数,无论你叠多少层神经网络,本质上都只是一堆线性方程的相加,最终等于一个一次函数——它只能画直线,无法拟合现实世界中复杂的曲线规律。 激活函数引入了非线性,让网络拥有了学习复杂图案和逻辑的能力。

  1. sigmoid:将一个数轴压缩到0–1区间(逻辑斯蒂曲线) 加偏置b,再sigmoid–>加权和区间
    image-20250709182020913

image-20250709182037636

  1. ReLU:超过某个阈值得到线性或者恒定输出

image-20250709183144875

  1. Tanh:
    $$
    \frac{e^x - e^{-x}}{e^x + e^{-x}}
    $$

sigmoid不同的是, tanh是以0为中心的, 在实际应用中, tanh要比sigmoid好一些, 但是在饱和神经元的情况下,tanh是没有解决[梯度消失](#2.1.4 梯度消失)的问题的

使用情况: tanh输出间隔为1, 并且整个函数以0为中心, 比sigmoid函数更好
缺点: 仍然存在梯度饱和, 负输入被强映射为负数, 零输入趋近于0

img

  1. Leaky ReLU:

$f(x) = max(ax, x)$

img

使用情况: 解决了ReLU为负数时神经元死亡的问题

​ 线性非饱和的性质, 在SGD中能够快速收敛

1.1.3线性代数表示
image-20250709182601636

对结果的每一个元素取一次sigmoid

image-20250709182708900

所以神经网路的本质就是一个函数,输入A*A的自变量,得到B的输出

2.1 梯度和梯度下降

2.1.1 代价和损失

刚开始训练时,我们会随机赋予权重矩阵随机的值,随后输入图像,很显然预测结果会是混乱的这时候要给计算机一个评定结果是好是坏的标准,这就是训练单个样本的代价**(差的平方和)也就是损失**

image-20250709183853233

代价的平均值叫经验风险,代价函数也是一种函数

image-20250709184051679

2.1.2 如何优化:寻找最低点

由于参数(权重和偏置)往往多达几百万个,我们不可能用微积分直接算出最优解。梯度下降就是我们寻找最小值的核心方法。

想象你被蒙住双眼放在半山腰,要走到谷底。你该怎么做?
答案是:用脚探一探周围,哪边往下最陡峭,就往哪边走一步。

在数学中,衡量“陡峭程度”的工具叫梯度(所有参数的偏导数组成的向量)。计算出梯度后,我们**朝着梯度的反方向(向下的方向)**更新参数,不断重复,直到走到谷底。

image-20250709184404065

==注意:由于高维地形复杂,这种“蒙眼下山”的方法有时会让我们卡在一个山坳(局部最小值),而不是真正的谷底(全局最小值)。==

现在升维到二维(高维):

image-20250709184631133

对于多元微分,上升最快的方向是梯度的方向,那么下降最快的方向就是梯度的反方向,将输入的权重看作向量,那么梯度也是向量

对于代价函数,取得结果为总体平均值,这样对整体识别能力更好

这就是梯度下降,btw,我们希望代价函数是平滑的,便于找到MIN

2.1.3 另一种理解

梯度向量得出的结果实际上也是一种权重,告诉我们改变哪个参数性价比最高,参数越大,方向越陡峭,改变性价比越高

2.1.4 梯度消失
  • 直觉叙述: 我们知道深度网络需要反向传播, 如果信号在传递过程中越来越小, 传到前面的层时几乎为0, 前面的层就几乎学不到东西, 这就是宏观上的梯度消失

  • 数学叙述: [反向传播的本质是很多导数相乘](#3.1.3 反向传播的本质)以L层网络为例子
    image-20250818145651085

其中$J^{(L)}$包含本层激活函数的导数和权重的雅可比, 若这些矩阵点谱范数多数小于1, 就会被连乘压扁

比如:

  • 激活饱和: sigmoid 的导数小于等于0.25 , tanh的导数小于等于1, 且在两端接近0, 多层相乘衰减快
  • 深度高: CNN/MLP或者是回传步数多
  • 初始化: 权重矩阵特征值过小或者过大
2.1.5 学习率

学习率(Learning Rate)是一个超参数,用于控制机器在优化算法中每一步调整参数的距离大小
这就好比我们前面提到的“蒙眼下山”:你能感知到哪边是下坡,但你决定这一步跨多大,就是学习率。

为什么学习率很重要?

  • 合适的学习率:犹如稳健的步伐,一步步踏实地走到谷底(损失最小处)。
  • 过低的学习率:就像蚂蚁下山,一次挪动几毫米。虽然方向对,但训练极度缓慢,有时候甚至会卡在一个浅坑里出不来。
  • 过高的学习率:就像巨人跨步,一步跨到了对面的山坡上,下一步又跨回来(反复横跳),甚至越跳越高(发散),永远无法收敛。

3.1 反向传播算法

3.1.1 训练过程

在前面已经得知:对于正权重的神经元,需要调大其激活值;对于负权重的神经元,需要减少其激活值;这样使得总输出减小

那么对于一个随机的结果,我们需要增大其标记参数的输出值,减小其它非标记参数的输出值,如果我们根据它需要更改的程度来记录每个标记或者非标记参数的激活值需要增大或者减小的程度,最后将这些程度加和起来,得到一个总的改变趋势,也是总改变权重(向量)

这就是反向传播,传播出了对上一层如何改变神经元激活值的指示

image-20250709211214958

不断对上一层进行反向传播,得到初始的期望,从而改变权重,这就是反向传播算法

刚才我们关注的是单个样本对权重偏置的影响,如果只按照此权重更改,会将所有图像识别为一种图像,若想识别1-9,应该将训练集每一个样本过一次反向传播,为了整体识别效果最佳,取1-9的向量的平均值

==此处得到的平均值,不严格地说就是上一节的负梯度==

注:像素组成的图像有无数种形式,1-9只是其中的几种,因平均向量会让整体识别的更好

image-20250709212807874

3.1.2 训练简化(小批量)

如果每次都用整个训练集(哪怕是几百万张图片)的所有样本来算一次平均误差,再去更新一次参数,那计算机吃不消,太费时间!
所以,通常我们将训练集分成许多小批次(Mini-batch)。虽然每一小批算的梯度不是最精准的全局梯度,但只要大方向近似而且计算非常快,它就能在“跌跌撞撞”中快速奔向谷底。

这个技巧叫做随机梯度下降 (SGD 以及各类变体)

3.1.3 反向传播的本质(连乘“锅”之法)

直觉叙述: 反向传播就是利用数学上的“链式法则”来“分锅”。一层一层往回算:最后的误差有多大,上一层的神经元该负多少责任?依次推导到第一层。这就是损失函数对于 $W^{(1)}$ 的偏导。

比如一个三层网络: image-20250818143013526

损失$L(y, t)$依赖于所有层的输出, 想更新$W^{(1)}$, 必须知道损失对$W^{(1)}$的敏感度也就是损失函数对于$W^{(1)}$的偏导

根据偏导数链式法则: image-20250818144933553

实际上每个$\frac{\partial h^{(l)}}{\partial h^{(l-1)}}$都是本层使用的激活函数的导数*权重矩阵, 所以:

image-20250818145420798

此处体现了梯度消失的问题: 比如sigmoid的导数最大为0.25, 那么如果经过多层的反向传播,可能对第一层的矫正能力就消失了

3.1.4 求梯度和误差项

在bp([反向传播](#3.1 反向传播算法))中, 误差项$\delta$是损失函数$L$对于第$l$层加权输入$z^{(l)}$的导数

表示着这一层神经元的敏感度: 如果输入$z^{(l)}$变化了, 损失会怎么变

对于最后一层: ($h^{(l)} = f(z^{(l)})$)

image-20250818181429381

对于除了最后一层之外的第$l$层:

image-20250818181634656

那么梯度就很好求了:

  • 权重:

image-20250818182013268

  • 偏置:

    image-20250818182026967

利用误差项$\delta$更新权重矩阵: ($\eta$ = [学习率](# 2.1.5 学习率))

$\Delta w_{ki} = \eta \delta k x{ki}$, $w_{ki} = w_{ki} + \Delta w_{ki}$

4.1 k折交叉验证 (K-Fold Cross-Validation)

当我们训练出一个模型,需要评价它有几分能耐时,如果只把数据简单切割一次为训练集/测试集,可能因为运气好,刚好测试的都是它“背过”的题,使得结果欺骗了我们。
k折交叉验证的作用,就是使得我们对模型的评估尽可能的公平、可靠。

4.1.1 核心步骤(怎么做?)
  1. 随机打乱你的总数据集。
  2. 将数据平均切分为 $k$ 份(折)
  3. 第一轮:用第 1 份做测验,剩下的 $k-1$ 份充当训练教材。在此基础上得出一个分数。
  4. 第二轮:用第 2 份做测验,剩下的充当教材……
  5. 重复 $k$ 轮,直到每份数据都已经当过一次考卷。最终取平均分,就是该模型真实的水平。

【避坑!新手常犯错误】在使用交叉验证时,如果涉及数据缩放(归一化)或特征选择,绝对不能在总数据上提前算好的均值或方差应用。必须在每次单独切出来的 $k-1$ 个训练集上实时计算然后套用到那 1 份测试集上。否则叫“数据泄露”。

4.1.2 k 的配置
  • 具有代表性的 k = 10:公认的黄金标准,它在测试集与训练集的配比、计算耗时与模型防偏之间找准了平衡。
  • 留一法 k = N (LOOCV):极端的交叉验证。每次只留 1 个数据做测试,其余全部训练。仅当你的总数据少得可怜(比如只有五六十对数据)时使用,因为其计算量等于你要跑 N 遍训练模型。
4.1.3 工作实例

image-20250714121113349

4.1.4 交叉验证API

在 Python 环境下,往往不需要手写循环,经常调动 sklearn 的 KFold() 等函数来实现。

5 Pytorch基础和构建回归模型

5.1 Pytorch
5.1.1 张量(Tensor)

张量: 核心数据结构, 用于存储和操作多维数组, 张量可以看作一个多维数组, 支持加速计算, 在pytorch中, 张量就类似Numpy的数组, 但是pytorch可以运行在不同的设备上, 比如GPU和CPU, 使得非常适合于进行大规模并行计算, 特别是在深度学习领域

  • 维度(Dimensionality): 张量的维度就是数据的多维数组结构
  • 形状(Shape): 张量的形状指的是每个维度上的大小, 例如, 形状为(3, 4)的张量意味着有三行四列
  • 数据类型(Dtype): 张量中的数据类型, 比如整数: torch.int8, torch.int32… , 浮点: torch.float32, torch.float64和布尔: torch.bool

张量创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch

a = torch.zeros(2, 3) # 全0张量
b = torch.ones(2, 3)
c = torch.randn(2, 3) # 随机数张量

import numpy as np
numpy_array = np.array([1, 2], [3, 4])
tensor_from_numpy = torch.from_numpy(numpy_array) #将numpy数组转换成张量
# !! torch.from_numpy 不会复制数据, 而是直接与原始的numpy数组共享内存, 就是说, 如果你修改了numpy_array, tensor_from_numpy的数值也会随之改变, 反之亦然
print(tensor_from_numpy)

# 指定设备创建张量
device = torch.device("cuda" if torch.cuda.is_availbale() else "cpu")
d = torch.randn(2, 3, device=device)
print(d)

常用张量操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 张量相加
e = torch.randn(2, 3)
f = torch.randn(2, 3)
print(e + f)

# 逐个元素乘法
print(e * f)

# 转置
g = torch.randn(3, 2)
print(g.t()) # 或者二g.transpose(0, 1)

# 张量的形状
print(g.shape)

张量的移动

1
2
if torch.cuda.is_available():
tensor_gpu = tensor_from_list.to('cuda') #张量移动到gpu

梯度和自动微分

1
2
3
4
5
6
7
8
9
# 创建一个需要梯度的张量
tensor_requires_grad = torch.tensor([1.0], requires_grad = True)

# 进行一些操作
tensor_result = tensor_requires_grad * 2

# 计算梯度
tensor_result.backward()
print(tensor_requires_grad.grad) # 输出梯度
5.1.2 自动求导(Autograd)与计算图

自动求导(Automatic Differentiation): 是深度学习框架中的一个核心特性, 它允许计算机自动计算复杂的偏导数(梯度),帮我们省去了草稿纸手算微积分的麻烦。

计算图(Computation Graph) 是 Autograd 的底层原理:
计算图是一系列节点和边组成的“流程图”。当你使用 PyTorch 进行张量的加减乘除计算时,如果张量设定了 requires_grad=True,PyTorch 就会悄悄在后台动态地画图,记录每一个操作节点。

  • 前向传播:记录操作流(比如加法、乘法节点)并求出输出。
  • 反向传播:顺着这张图“原路返回”,利用链式法则通过 .backward() 计算出前置参数的梯度。

注意!计算图会占用显存/内存!(OOM 警告)
在训练模型的时候,每计算一个步骤就会画出对应的分支。如果为了省内存做纯测试预测 (推理),一定要按停计算图的绘制。

1
2
3
4
5
6
7
8
x = torch.randn(2, 2, requires_grad = True)
print(x)

y = x + 2
z = y * y * 3
out = z.mean() #.mean()后缀是求平均值
# dim = ... 指维度 print(x.mean(dim = 0))
print(out)
5.1.3 反向传播(Backpropagation)

一旦定义了计算图就可以使用.backward()来计算梯度

1
2
3
4
5
# 反向传播计算梯度
out.backward()

# 查看x的梯度
print(x.grad)

停止梯度计算

1
2
3
4
5
6
# 使用pytorch停止计算
with torch.no_grad():
y = x * 2

# 或者使用
requires_grad = False

**requires_grad属性 **

每个torch.Tensor都有这个属性, 如果是true就会被autograd跟踪, 记录计算图, false就不记录梯度

with torch.no_grad

上下文管理器, 控制整个代码块, 临时禁用autograd机制, 在此代码块里不会记录计算图也不计算梯度, 用于推理, 或者需要节省内存的情况, ==不改动requires_grad的属性==

model.eval()

torch.nn.Module的方法, 即评估模式, 影响Dropout, BatchNorm这类层的行为:

Dropout: 不再随机丢弃神经元

BatchNorm: 使用训练时统计的均值和方差,而不是batch里的

train()的区别就为以上两点

5.1 4 神经网络 (nn.Module)

神经网络是一种模仿人脑神经元连接, 由多层节点(神经元)组成, 用于学习数据之间的复杂模式和关系

包括前馈神经网络, 卷积神经网络, 循环神经网络和长短期记忆网络, 在图像识别, 语音处理, 自然语言处理等多个领域都有广泛的应用

Pytorch提供了一个非常方便的接口来构建神经网络模型, 即torch.nn.Module, 我们可以继承nn.Module类并定义自己的网络层

创建简单网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import torch.nn as nn
import torch.optim as optim # pytorch的优化模块, 根据梯度更新模型参数, 从而最小化损失函数

# 定义一个简单的全连接神经网络
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
self.fc1 = nn.Linear(2, 2) # 输入层到隐藏层
self.fc2 = nn.Linear(2, 1) # 隐藏层到输出层

def forward(self, x):
x = torch.relu(self.fc1(x)) # ReLU 激活函数
x = self.fc2(x)
return x

# 创建网络实例
model = SimpleNN()

# 打印模型结构
print(model)

训练过程:

  1. 前向传播(Forward Propagation): 在前向传播阶段,输入数据通过网络层传递,每层应用权重和激活函数,直到产生输出
  2. 计算损失(Calculate Loss): 根据网络的输出和真实标签,计算损失函数的值
  3. 反向传播(Backpropagation): 反向传播利用自动求导技术计算损失函数关于每个参数的梯度
  4. 参数更新(Parameter Update): 使用优化器根据梯度更新网络的权重和偏置
  5. 迭代(Iteration): 重复上述过程,直到模型在训练数据上的性能达到满意的水平

前向传播和损失计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 随机输入
x = torch.randn(1, 2)

# 前向传播
output = model(x)
print(output)

# 定义损失函数
criterion = nn.MSELoss()

# 假设目标值为1
target = torch.randn(1, 1)

# 计算损失
loss = criterion(output, target)
print(loss)

优化器(Optimizers)

优化器在训练过程中更新神经网络的参数, 来降低损失函数

pytorch提供SGD, Adam等等

使用优化器进行参数更新

1
2
3
4
5
6
7
# 定义优化器(Adam)
optimizer = optim.Adam(model.parameters(), lr = 0.001)

#训练步骤
optimizer.zero_grad() # 🌟 【关键步骤】清空上一轮残留的梯度!PyTorch默认是累加梯度的。
loss.backward() # 进行反向传播: 算出所有要求导的参数的梯度
optimizer.step() # 更新参数: 优化器根据刚才算出的梯度,让参数往下山的方向走一步
5.1.5 训练模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import torch
import torch.nn as nn
import torch.optim as optim

# 1.定义SimpleNN
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
self.fc1 = nn.Linear(2, 2) # 输入层到隐藏层
self.fc2 = nn.Linear(2, 1) # 隐藏层到输出

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

# 2. 创建模型实例
model = SimpleNN()

# 3. 定义损失函数和优化器
criterion = nn.MSELoss() # 均方误差损失函数
optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam 优化器

# 4. 假设我们有训练数据 X 和 Y
X = torch.randn(10, 2) # 10 个样本,2 个特征
Y = torch.randn(10, 1) # 10 个目标值

# 5. 训练循环
for epoch in range(100): # 训练 100 轮
optimizer.zero_grad() # ⭐ 1. 梯度清零(别漏了!)
output = model(X) # ⭐ 2. 前向传播:带着数据算输出
loss = criterion(output, Y) # ⭐ 3. 算一次损失(考得咋样?)
loss.backward() # ⭐ 4. 反向传播:把锅分好(算梯度)
optimizer.step() # ⭐ 5. 优化器动手改参数

# 每 10 轮输出一次损失
if (epoch+1) % 10 == 0:
print(f'Epoch [{epoch+1}/100], Loss: {loss.item():.4f}')
5.1.6 设备(Device)

PyTorch 允许你将模型和数据移动到 GPU 上进行加速。

使用 torch.device来指定计算设备。

将模型和数据移至 GPU:

1
2
3
4
5
6
7
8
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 将模型移动到设备
model.to(device)

# 将数据移动到设备
X = X.to(device)
Y = Y.to(device)

在训练过程中,所有张量和模型都应该移到同一个设备上(要么都在 CPU 上,要么都在 GPU 上)。

5.2实例

6 pytorch数据处理和加载

6.1 自定义Dataset

torch.utils.data.Dataset是一个抽象类, 允许你从自己的数据源中创建数据集

我们需要继承该类并实现以下两个方法:

  • __len__(self): 返回数据集中的样本数量
  • __getitem__(self, idx): 通过索引返回一个样本

假设已经有一个csv文件或者一些数据, 我们可以通过继承Dataset类来创建自己的数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import torch
from torch.utils.data import Dataset

# 自定义数据集类
class MyDataset(Dataset):
def __init__(self, X_data, Y_data):
'''
初始化数据集, X_data 和 Y_data 是两个列表或数据
X_data: 输入特征
Y_data: 目标标签
'''
self.X_data = X_data
self.Y_data = Y_data

def __len__(self):
'''
返回数据集的长度
'''
return len(self.X_data)

def __getitem__(self, index):
'''
返回数据集中第 index 个数据
'''
x = torch.tensor(self.X_data[index], dtype=torch.float32) # 转换为Tensor
y = torch.tensor(self.Y_data[index], dtype=torch.float32) # 转换为Tensor
return x, y

# 示例数据
X_data = [[1, 2], [3, 4], [5, 6], [7, 8]]
Y_data = [1, 0, 1, 0]

# 创建数据集实例
dataset = MyDataset(X_data, Y_data)

使用DataLoader加载数据

Dataloaderpytorch的一个重要工具, 用于从Dataset中按批次(batch)加载数据

DataLoader允许我们批量读取数据并且多线程加载, 从而大大提高训练效率(利用显卡的并行计算能力)。

1
2
3
4
5
6
7
8
9
10
11
from torch.utils.data import DataLoader

# 创建 DataLoader 实例, batch_size 设置每次加载的样本数量
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)

# 打印加载的数据
for epoch in range(1):
for batch_idx, (inputs, labels) in enumerate(dataloader):
print(f'Batch {batch_idx + 1}')
print(f'Input: {inputs}')
print(f'Labels: {labels}')
  • batch_size: 每次加载的样本数量(建议设为 16, 32, 64 等2的次方,以贴合GPU硬件特性)
  • shuffle: 是否对数据进行洗牌, 通常训练时需要将数据打乱,防止模型摸到死板的规律。
  • drop_last: 如果数据集中的样本不能被batch_size 整除, 设置True时, 丢弃最后一个不完整的batch,防止后面训练时计算张量维度出错。

机器学习基础
https://rubbishbro.github.io/2025/11/20/machine_learning_basics/
Author
John Doe
Posted on
November 20, 2025
Licensed under