2026.09.25 13:16
学习路线
rust
Python -> Python数据分析 -> LangChain -> LangGraph -> FastAPI + SQLAIchemy -> 机器学习 -> 深度学习 -> NLP -> Coze -> Dify -> 大模型应用基础 -> 大模型微调 -> 多模态 -> vibeCoding -> Hermes -> Docker
复习巩固总结
1. pytorch框架
text
pytorch框架
├── 创建张量 ← 已做完
├── 张量类型转换 ← 已做完
├── ① 张量运算
│ ├── 基本运算 + - * / ,add / sub / mul / div / neg
│ │ add_ / sub_ / ... 改原数据;t1 + 10
│ ├── 点乘 t1 * t2 ,t1.mul(t2)
│ └── 矩阵乘 t1 @ t2 ,t1.matmul(t2)
├── ① 张量运算函数
│ ├── 有 dim sum / mean / max / min
│ └── 逐元素 pow / sqrt / exp / log / log2 / log10
├── ② 张量索引操作 t1[行, 列]
│ ├── 作用 tensor[0轴下标, 1轴下标, ...]
│ ├── 下标取值 tensor[0] → 0轴第一个
│ │ tensor[:, 0] → 1轴第一个
│ ├── 列表取值 tensor[[0,1], [2,4]]
│ │ → 0轴第1个×1轴第3个,0轴第2个×1轴第5个
│ ├── 范围取值 tensor[起始:结束:步长]
│ └── 布尔取值 tensor[:, 0] > 10 得到 True/False
│ tensor[tensor[:, 0] > 10] 按条件抠行
├── ② 张量形状操作
│ ├── 不改摊平顺序 reshape / unsqueeze / squeeze / view
│ └── 换轴(摊平会变) transpose 一次两轴;permute 一次全排
├── ② 张量拼接操作
│ ├── cat 沿已有轴接,维数不变
│ └── stack 先开新轴再叠,维数 +1
├── ③ 自动微分模块
└── pytorch构建回归模型
text
w ──► * ──► + ──► z(预测值)──► loss ←── y(真实值)
b ──────────┘ │
│ 求导
▼
BP 把梯度送回 w、b,改参数
2. 张量
2.1 创建
2.1.1 按数据 / 形状 / 指定类型
| API | 记住 | 掌握 |
|---|---|---|
torch.tensor(数据) |
按已有数据,dtype 推断(整数 → int64,小数 → float32) | ✓ |
torch.Tensor(...) |
按形状或按数据,一律 float32 ;Tensor(10) 是长度 10 的空壳 |
|
torch.IntTensor / FloatTensor |
指定 int32 / float32 |
2.1.2 线性和随机
| API | 记住 | 掌握 |
|---|---|---|
arange(起, 止, 步长) |
步长固定,不含终点,整数默认 int64 | ✓ |
linspace(起, 止, 个数) |
个数固定,两端都含,float32 | ✓ |
initial_seed() |
看当前种子(默认时间戳) | |
manual_seed(n) |
锁定随机序列,才能复现 | ✓ |
rand(形状) |
(0,1) 均匀,float32 | |
randn(形状) |
N(0,1),float32 | |
randint(low, high, size) |
[low,high) 整数,int64 | ✓ |
2.1.3 全 0 / 全 1 / 指定值
| 灌什么 | 自己写形状 | 抄已有张量 | 掌握 |
|---|---|---|---|
| 全 0 | zeros(形状) |
zeros_like(t) |
✓ |
| 全 1 | ones(形状) |
ones_like(t) |
|
| 全是 v | full(形状, v) |
full_like(t, v) |
✓ |
2.1.4 元素类型转换
| 方法 | dtype | 掌握 |
|---|---|---|
.type(...) |
传入目标类型,如 torch.float64 |
✓ |
.half() |
float16 | |
.float() |
float32 | |
.double() |
float64 | |
.short() |
int16 | |
.int() |
int32 | |
.long() |
int64 |
2.2 基本运算
| 运算 | 不改原数据 | 改原数据 | 符号 |
|---|---|---|---|
| 加 | add |
add_ |
+ |
| 减 | sub |
sub_ |
− |
| 乘 | mul |
mul_ |
∗ |
| 除 | div |
div_ |
/ |
| 取负 | neg |
neg_ |
−t |
2.3 点乘
A=1324,B=5768
A∘B=1×53×72×64×8=5211232
形状必须一样(或能广播)。这就是 张量的基本运算 里的 mul / *,不是线性层里那一下。
2.4 矩阵乘法
第一个形状 (n,m),第二个 (m,p),中间维 m 相等 ,结果 (n,p)。
| PyTorch | NumPy | |
|---|---|---|
| 矩阵乘 | A @ B,torch.matmul(A, B) |
A @ B,np.dot(A, B) |
| 点乘 | A * B,A.mul(B) |
A * B |
@ 专做矩阵乘。matmul 对更高维也行,只要最后两维 满足 (n,m)×(m,p)。NumPy 二维矩阵乘常用 dot()。
还是上面的 A、 B,中间过程:
AB=1×5+2×73×5+4×71×6+2×83×6+4×8=19432250
和点乘的 5211232 完全不是一串数。线性层 z=wx+b 走的是矩阵乘 @ / matmul。
2.5 运算函数
拆成两拨------有没有 dim。
| 一拨 | 函数 | dim |
掌握 |
|---|---|---|---|
| 沿轴归约 | sum |
有。dim=0 按列,dim=1 按行;不写 dim 就是整份张量 |
✓ |
| 沿轴归约 | max |
同上 | ✓ |
| 沿轴归约 | min |
同上 | ✓ |
| 沿轴归约 | mean |
同上 | ✓ |
| 逐元素 | pow、sqrt、exp、log、log2、log10 |
没有。每个数单独算,形状不变 |
一张 2×3 的表 142536:
| 调用 | 结果在算什么 | 掌握 |
|---|---|---|
sum(dim=0) |
每列相加 → 5, 7, 9 | ✓ |
sum(dim=1) |
每行相加 → 6, 15 | ✓ |
sum() |
六个数全加 → 21 | ✓ |
max(dim=0) |
每列最大 → 4, 5, 6(还会带上最大值在该列的位置) | ✓ |
max() |
全局最大 → 6 | ✓ |
min() |
全局最小 → 1 | ✓ |
mean() |
六个数的平均 → 3.5(整数张量要先转成浮点,否则 mean 会报错) |
✓ |
min 和 max 同一套,只是取最小。
逐元素那拨,形状不变。输入 1,4,9:
| 调用 | 在算什么 | 结果 | 掌握 |
|---|---|---|---|
sqrt() |
开方 | 1, 2, 3 | |
exp() |
ex | e, e4, e9 | |
log() |
ln | ln1, ln4, ln9 | |
log2() |
log2 | 0, 2, log29 | |
log10() |
log10 | 0, log104, 2 | |
pow(n) |
每个数的 n 次方 | 如 pow(2) → 1, 16, 81 |
2.6 索引操作
取某些格子出来处理或改掉。准备一张 4×5:
06647839638551739031
| 种类 | 写法 | 取出什么 | 掌握 |
|---|---|---|---|
| 简单行列 | data[0] |
第 0 行 0,7,6,5,9 | ✓ |
| 简单行列 | data[:, 0] |
第 0 列 0,6,6,4 | ✓ |
| 列表 | data[[0,1], [1,2]] |
两个点 (0,1)、 (1,2) → 7, 3 | |
| 列表 | data[[[0],[1]], [1,2]] |
第 0、1 行 × 第 1、2 列,共 4 个数 7863 | |
| 范围 | data[:3, :2] |
前 3 行、前 2 列 066783 | ✓ |
| 范围 | data[2:, :2] |
第 2 行到末、前 2 列 6439 | ✓ |
| 布尔 | data[data[:, 2] > 5] |
第 2 列 >5 的那些行(第 0、2 行) | |
| 布尔 | data[:, data[1] > 5] |
第 1 行 >5 的那些列(第 0、1 列) |
列表那两行不是一回事:[0,1] 配 [1,2] 是配对取点 ;行写成列向量 [[0],[1]] 才和 [1,2] 拉开成一块小矩形。
范围左闭右开,和 Python 切片一样::3 是 0,1,2,2: 从第 2 行一直到头。
布尔:第 2 列是 6,3,8,5,大于 5 的是第 0、2 行;第 1 行是 6,8,3,1,0,大于 5 的是第 0、1 列。
多维按轴抠。下面这块形状是 (2,3,4):0 轴两块,每块一张 3×4。
text
[ 0轴,2 个元素(两块)
[ 1轴,3 个元素(每块 3 行)
[3, 4, 6, 5], ← 2轴,4 个元素
[8, 8, 8, 3],
[4, 9, 6, 7]
],
[ 1轴,3 个元素
[2, 8, 8, 5],
[6, 4, 2, 2],
[2, 7, 9, 4]
]
]
写成两张矩阵,一眼能对上:
第 0 块 384489686537 第 1 块 262847829524
加粗是每行开头,也就是 2 轴第 0 个。
| 轴 | 几个元素 | 对应谁 |
|---|---|---|
| 0 轴 | 2 | 上面两张表(两块) |
| 1 轴 | 3 | 每一张的 3 行 |
| 2 轴 | 4 | 每一行的 4 个数 |
: 表示这一轴全要,只钉死要的那一轴。
| 写法 | 取出什么 | 掌握 |
|---|---|---|
data[0, :, :] |
0 轴第 0 块:左边那张 384489686537 | ✓ |
data[:, 0, :] |
1 轴第 0 行:两张表各自第一行 3,4,6,5、 2,8,8,5 | ✓ |
data[:, :, 0] |
2 轴第 0 个数(加粗): 3,8,4 和 2,6,2 | ✓ |
2.7 形状操作
数一个不改,改的是怎么排。先按「摊平后顺序变不变」拆成两拨,再逐个看怎么用。
| 函数 | 干什么 | 摊平后顺序 | 掌握 |
|---|---|---|---|
reshape(...) |
另排形状,个数必须对上 | 不变 | ✓ |
unsqueeze(dim) |
指定轴塞一个长度为 1 的维(升维) | 不变 | ✓ |
squeeze() |
删掉所有长度为 1 的维(降维) | 不变 | |
view(...) |
另排形状,只吃连续张量 | 不变 | ✓ |
transpose(a, b) |
一次只换两个轴 | 变 | |
permute(...) |
一次按新顺序排完全部轴 | 变 | ✓ |
contiguous() |
按当前显示顺序,把内存拷成连续 | --- | |
is_contiguous() |
查内存顺序和显示顺序是否一致 | --- |
源数据 6,9,9,2,8,7,排成 (2,3):
629897
| 不改变内容 | 改变内容 | |
|---|---|---|
| 排成 | 698927 | 699287 |
| 再摊平 | 6,9,9,2,8,7 和源数据一样 | 6,2,9,8,9,7 顺序已经换了 |
左边按行接着读。右边按列抽:先第 0 列 6,2,再 9,8,再 9,7。reshape / unsqueeze / squeeze / view 走左边;transpose / permute 走右边。
2.7.1 unsqueeze / squeeze
只多一层壳或少一层壳,数的顺序不变。 (2,3) 上:
| 写法 | 形状 | 注意 | 掌握 |
|---|---|---|---|
unsqueeze(0) |
(1,2,3) 最外面加一块 | dim 只能是 0∼ 当前维数(含两端) |
✓ |
unsqueeze(1) |
(2,1,3) 每行外面加一层 | ✓ | |
unsqueeze(2) |
(2,3,1) 每个数竖成一列 | 和 unsqueeze(-1) 一样,最后一轴 |
✓ |
unsqueeze(3) |
越界,报错 | (2,3) 没有第 3 轴可插 | |
squeeze() |
(2,1,3,1,1)→(2,3) | 所有长度为 1 的维一起删,不是只删一个 |
dim=0 往最外面加一层;dim=-1 往最后加。squeeze() 不写轴就全挤;只想挤某一轴要写成 squeeze(dim)。
2.7.2 transpose / permute
轴对调,摊平顺序会变。transpose 一次只换两个轴 ;permute 一次按新顺序排完全部轴。
形状 (2,3,4):
| 写法 | 轴怎么走 | 新形状 | 掌握 |
|---|---|---|---|
transpose(0, 1) |
只换 0 和 1 | (3,2,4) | |
transpose(0, 2) / transpose(0, -1) |
只换 0 和最后一轴 | (4,3,2) | |
permute(2, 0, 1) |
新顺序 = 旧的 2,0,1 维 | (4,2,3) | ✓ |
形状 (3,4,5) 要变成 (4,5,3):permute(1, 2, 0) 一次写完;用 transpose 得先 transpose(0, 1) 再 transpose(1, 2)。
permute(1, 2, 0) 读法:新第 0 轴拿旧第 1 轴(长度 4),新第 1 轴拿旧第 2 轴(长度 5),新第 2 轴拿旧第 0 轴(长度 3)。* 是位置参数,轴号按顺序塞进去,和传列表 [1, 2, 0] 一样。
2.7.3 view / contiguous
view 也能改形状,但只吃连续张量 :内存里挨着存的顺序,要和逻辑上按行读的顺序一致。transpose / permute 之后这两套对不上,直接 view 会报错。先 contiguous() 按显示顺序拷一份,再 view。is_contiguous() 用来查。
104020503060(2,3)连续 True
| 步骤 | 得到 | 连续? | 掌握 |
|---|---|---|---|
view(3, 2) |
103050204060 摊平仍是 10,20,30,40,50,60 | True | ✓ |
transpose(0, 1) |
102030405060 | False | |
直接 view(2, 3) |
报错 | ||
contiguous().view(2, 3) |
105040302060 | True |
和 reshape 的差别:个数都得对上;不连续时 reshape 往往自己拷一份,view 必须你先 contiguous()。新张量默认连续,转置过后才断。
2.8 拼接操作
两张表接在一起。先看差在哪,再分别写 dim。
| 函数 | 维数 | 形状要求 | 掌握 |
|---|---|---|---|
torch.cat((A, B), dim) |
不增加,2 维还是 2 维 | 没被指定的那几维长度要相同 | ✓ |
torch.stack((A, B), dim) |
多出一个新轴,2 维变成 3 维 | 参加拼接的张量形状必须完全一样 | ✓ |
cat 是把已有的轴拉长;stack 是先 unsqueeze 再 cat。
2.8.1 torch.cat:沿已有轴接
两块都是 (1,2,3):
A=768376B=376550
dim |
在接哪 | 形状 | 结果 |
|---|---|---|---|
| 0 | 最外层叠块 | (2,2,3) | 上下两块 A、 B |
| 1 | 行方向接 | (1,4,3) | 763783657650 |
| 2 | 列方向接 | (1,2,6) | 768376376550 |
两张 (2,3) 更直观:cat(dim=0) → (4,3) 往下接行;cat(dim=1) → (2,6) 往右接列。
没被指定的那几维长度要相同,否则接不上。 (2,3) 配 (2,4):dim=0 报错(列 3 对 4),dim=1 能接成 (2,7)。 (2,3) 配 (5,3):dim=1 报错(行 2 对 5),dim=0 能接成 (7,3)。
2.8.2 torch.stack:先开一个新轴再叠
两张 (2,3):
A=692883B=902789
dim |
新轴插在哪 | 形状 | 结果 |
|---|---|---|---|
| 0 | 最外面 | (2,2,3) | 第 0 块是 A,第 1 块是 B |
| 1 | 行之间 | (2,2,3) | 每张表的第 i 行对到一起 |
2 / -1 |
列之间 | (2,3,2) | 628928 和 983079 (对应位置的数配成一对) |
| 3 | 越界,报错 | (2,3) 最多插到第 2 轴 |
stack 无论新旧轴,两边形状都得一模一样: (2,3) 配 (2,4) 或 (5,3) 都会报错。dim=-1 插在最后一轴,二维时和 dim=2 一样。
2.9 和Numpy之间互相转换
CPU 上的张量和 NumPy 的 ndarray 能互转。关键点是改一边,另一边会不会跟着变(是否共享内存)。
| 方向 | API | 共享内存 | 掌握 |
|---|---|---|---|
| 张量 → ndarray | t.numpy() |
是,改 n 会改 t |
|
| 张量 → ndarray | t.numpy().copy() |
否 | |
| 张量 → ndarray(带梯度时) | t.detach().numpy() |
先切断计算图再转 | ✓ |
| ndarray → 张量 | torch.from_numpy(n) |
是 | |
| ndarray → 张量 | torch.tensor(n) |
否,拷一份 | ✓ |
| 单元素张量 → Python 数 | t.item() |
--- | ✓ |
3. 自动微分
一句话:自动微分就是求导 。对损失函数求导,结合反向传播,更新权重 w、偏置 b。
3.1 小问题_detach函数
python
"""
案例:
演示 detach() 函数的功能,解决 自动微分的弊端
回顾:
自动微分 = 求导,即:基于损失函数,计算梯度
结合权重更新公式:W新 = W旧 - 学习率 * 梯度,来更新权重的
问题:
一个张量一旦设置了自动微分,这个张量就不能直接转成 nunmpy 的 ndarray 对象了,需要通过 detach() 函数解决
"""
# 导包
import torch
import numpy as np
# 1. 定义张量
# 参1:数据 参2:是否需要自动微分 参3:数据类型
# t1 = torch.tensor([10, 20], dtype=torch.float)
t1 = torch.tensor([10, 20], requires_grad=True, dtype=torch.float)
print(f't1: {t1}, type: {type(t1)}')
# 2. 尝试把上述的张量 -> numpy对象
# n1 = t1.numpy() # 报错
# print(f'n1: {n1}, type: {type(n1)}')
# 3. 解决办法:通过 detach() 函数,拷贝一份张量,然后转换
t2 = t1.detach()
print(f't2: {t2}, type: {type(t2)}')
# 4. 测试上述的t1和t2是否共享一块空间 -> 共享
# t1[0] = 100
# print(t1.data)
t1.data[0] = 100
print(f't1: {t1}, type: {type(t1)}')
print(f't2: {t2}, type: {type(t2)}')
print('-' * 30)
# 5. 查看t1和t2谁可以自动微分
print(f't1.requires_grad: {t1.requires_grad}') # True
print(f't2.requires_grad: {t2.requires_grad}') # False
print('-' * 30)
# 6. 把t2转numpy对象
n2 = t2.numpy()
print(f'n2: {n2}, type: {type(n2)}')
# 7. 最终版
n3 = t1.detach().numpy()
print(f'n3: {n3}, type: {type(n3)}')
3.2 更新一次参数
PyTorch 只支持标量对张量求导 。 x 可以是一串数, y 必须是一个数 才能 backward()。损失若是向量,先 loss.sum().backward() 收成标量。.grad 会累加上一次的梯度,一步更新看完要心里有数。
走一遍 L=2w2, w=10,学习率 0.01。创建时 requires_grad=True,并且用浮点。
L=2w2=2×102=200,dwdL=4w=40
| 步骤 | 干什么 | 得到 |
|---|---|---|
| 正向 | loss = 2 * w ** 2 |
200 |
| 反向 | loss.sum().backward()(这里本就是标量,.sum() 可省) |
w.grad=40 |
| 更新 | w←w−0.01×40,写在 w.data 上以免再进计算图 |
10−0.4=9.6 |
和手推那刀一样。自动微分只替你算出这个 40。
python
"""
案例:
演示自动微分模块,具体如何求导
回顾:
权重更新公式:
W新 = W旧 - 学习率 * 梯度
梯度 = 损失函数的导数
关于损失函数的导数,无需我们手动计算,因为非常常用,所以 PyTorch 模块内置有 自动微分模块,专门实现针对于不同的损失函数求导,
从而实现结合反向传播,更新 权重参数w 和 偏置参数b
细节:
只有标量张量才能求导,且大多数底层操作的都是 浮点型,记得转型
"""
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
# 导包
import torch
# 1. 定义变量,记录:初始的权重w(旧)
# 参1:初始值 参2:是否自动微分(求导) 参3:数据类型
w = torch.tensor(10.0, requires_grad=True, dtype=torch.float)
# 2. 定义loss变量,表示损失函数
loss = 2 * w ** 2 # loss = 2w² -> 求导:4w
# 3. 打印梯度函数类型(了解)
print(f'梯度函数类型: {type(w.grad_fn)}') # <class 'MulBackward0'>
print(loss.sum()) # 2 * 100² = 200
# 4. 计算梯度,梯度 = 损失函数的导数,计算完毕后,会记录到 w.grad 属性中
# loss.backward() # 因为这里y本身就是标量,可以不写sum()
loss.sum().backward() # 保证loss是1个标量
# 5. 代入 权重更新公式:w新 = w旧 - 学习率 * 梯度
w.data = w.data - 0.01 * w.grad
# 6. 打印最终结果
print(f'更新后的权重w: {w}') # 9.6
3.3 循环更新参数
公式还是
w←w−r⋅grad
r 是学习率。.grad 会累加,每轮 backward() 之前 要 w.grad.zero_() 清掉上一次。第一轮还没反传过,.grad 是 None,得先判断非空再清。
求 y=w2+20 的极小值。开口向上,最低在 w=0,那时 y=20。起点 w=10,浮点、requires_grad=True,学习率 0.01,转上百轮(有的写法写 1000)。
y=w2+20=120,y′=2w=20
每一轮:
| 顺序 | 干什么 |
|---|---|
| 1 正向 | 用当前 w 算 y=w2+20 |
| 2 清零 | zero_(),否则梯度叠在旧的上面 |
| 3 反向 | y.sum().backward(),.grad 里是 2w |
| 4 更新 | w.data = w.data - 0.01 * w.grad |
第一刀: 10−0.01×20=9.8, y 从 120 往下降。再走, w 往 0 贴, y 往 20 贴。改的是 w.data,避免更新本身再进计算图。
python
"""feature_cols
案例:
演示自动微分模块,循环实现 计算梯度,更新参数
需求:
求 y = x ** 2 + 20 的极小值点,并打印y是最小时 w的值(梯度)
解题步骤:
1. 定一点 w = 10 requires_grad = True, dtype = float
2. 定义函数 loss = w ** 2 + 20
3. 利用梯度下降法 循环迭代 1000 求最优解
3.1 正向计算(前向传播)
3.2 梯度清零 w.grad.zero_()
3.3 反向传播
3.4 梯度更新 w.data = w.data - 0.01 * w.grad
回顾:
W新 = W旧 - 学习率 * 梯度
"""
# 导包
import torch
# 1. 定一点 w = 10 requires_grad = True, dtype = float
# 参1:初始值 参2:自动微分(求导) 参3:数据类型,浮点型
w = torch.tensor(10.0, requires_grad=True, dtype=torch.float)
# 2. 定义函数 loss = w ** 2 + 20
loss = w ** 2 + 20 # 求导:loss' = 2w
# 3. 利用梯度下降法 循环迭代 100 求最优解
print(f'开始 权重初始值:{w}, (0.01 * w.grad):无,loss:{loss}') # 10,无,120
# 迭代100次,求最优解
for i in range(1, 101):
# 3.1 正向计算(前向传播)
loss = w ** 2 + 20 # 求导:loss' = 2w
# 3.2 梯度清零 w.grad.zero_() 默认梯度会累加
# 至此(第一次的时候),还没有计算梯度,所以w.grad=None,需要做非空判断
if w.grad is not None:
w.grad.zero_()
# 3.3 反向传播
# loss.backward()
loss.sum().backward()
# 3.4 梯度更新 w.data = w.data - 0.01 * w.grad
print(f'梯度值为:{w.grad}')
w.data = w.data - 0.01 * w.grad
# 3.5 打印本次梯度更新后的权重参数结果
print(f'第{i}次 权重参数更新后:{w}, (0.01 * w.grad):{0.01 * w.grad:.5f}, loss:{loss:.5f}')
# 4. 打印最终结果
print(f'最终 权重参数:{w}, 梯度:{w.grad:.5f}, loss:{loss:.5f}')
3.4 真实应用场景
python
"""
案例:
演示自动微分的真实应用场景
结论:
1. 先前向传播(正向传播)计算出 预测值(z)
2. 基于损失函数,结合 预测值(z)和 真实值(y),来计算 梯度
3. 结合权重更新公式:W新 = W旧 - 学习率 * 梯度,来更新权重
"""
# 导包
import torch
# 1. 定义x,表示:特征(输入数据),假设:2行5列,全1矩阵
x = torch.ones(2, 5)
print(f'x: {x}')
# 2. 定义y,表示:真标签(真实值),假设:2行3列,全0矩阵
y = torch.zeros(2, 3)
print(f'y: {y}')
# 3. 初始化(可自动微分的)权重和偏置
w = torch.randn(5, 3, requires_grad=True) # x @ w + b
print(f'w: {w}')
b = torch.randn(3, requires_grad=True)
print(f'b: {b}')
# 4. 前向传播(正向传播)计算出 预测值(z)
# z = torch.matmul(x, w) + b
z = x @ w + b
print(f'z: {z}')
# 5. 定义损失函数
criterion = torch.nn.MSELoss() # neural network:神经网络
loss = criterion(z, y) # loss = 损失
print(f'loss: {loss}')
# 6. 进行自动微分,求导,结合反向传播,更新权重
loss.backward()
# 7. 打印w, b用来更新的梯度
print(f'w.grad: {w.grad}')
print(f'b.grad: {b.grad}')
# 后续就是:W新 = W旧 - 学习率 * 梯度,来更新权重
4. PyTorch模拟线性回归
用手写的 w、 b、循环改参数,换成 PyTorch 组件。整条流程四步:
| 步 | 干什么 | 对应组件 |
|---|---|---|
| 1 | 准备训练集 | make_regression → 张量 →(后面)TensorDataset / DataLoader |
| 2 | 构建模型 | 继承 nn.Module;假设函数用 nn.Linear |
| 3 | 损失和优化器 | nn.MSELoss、optim.SGD |
| 4 | 训练循环 | 正向 → 反向 → 更新 |
手写那套和组件的对应:
| 原来自己写 | 换成 |
|---|---|
| 平方损失 L=(z−y)2 | nn.MSELoss() |
| 按批喂数 | data.DataLoader |
| w←w−r⋅grad | optim.SGD |
| z=wx+b | nn.Linear |
python
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
# 导入相关模块
import torch
from torch.utils.data import TensorDataset, DataLoader # 构造数据集对象,数据加载器(numpy对象 -> 张量Tensor -> 数据集对象TensorDataset -> 数据加载器DataLoader)
from torch import nn # nn模块中有平方损失函数和假设函数
from torch import optim # optim模块中有优化器函数
from sklearn.datasets import make_regression # 创建线性回归模型数据集
import matplotlib.pyplot as plt # 可视化
plt.rcParams['font.family'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# 1. 定义函数,创建线性回归样本数据
def create_dataset():
# 1.1 创建数据集对象
x, y, coef = make_regression(
n_samples=100, # 100条样本(100个样本点)
n_features=1, # 1个特征(1个特征点)
noise=10, # 噪声,噪声越大,样本点越散,噪声越小,样本点越集中
coef=True, # 是否返回系数,默认为False,返回值为None
bias=14.5, # 偏置
random_state=3 # 随机种子,随机种子相同,输出数据相同
)
# print(type(x)) # <class 'numpy.ndarray'>
# 1.2 把上述的数据封装成张量对象
x = torch.tensor(x, dtype=torch.float)
y = torch.tensor(y, dtype=torch.float)
# 1.3 返回结果
return x, y, coef
# 2. 定义函数,表示模型训练
def train(x, y, coef):
# pass
# 2.1 创建数据集对象,把 tensor -> 数据集对象 -> 数据加载器
dataset = TensorDataset(x, y)
# 2.2 创建数据加载器对象
# 参1:数据集对象 参2:批量大小 参3:是否打乱数据(训练集打乱,测试集不打乱)
dataloader = DataLoader(dataset, batch_size=16, shuffle=True)
# 2.3 创建初始的 线性回归模型
# 参1:输入特征维度 参2:输出特征维度
model = nn.Linear(1, 1)
# 2.4 创建损失函数对象
criterion = nn.MSELoss()
# 2.5 创建优化器对象
# 参1:模型参数 参2:学习率
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 2.6 具体的训练过程
# 2.6.1 定义变量,分别表示:训练轮数,每轮的(平均)损失值,训练总损失值,训练的样本数
epochs, loss_list, total_loss, total_sample = 100, [], 0, 0
# 2.6.2 开始训练,按轮训练
for epoch in range(epochs): # epoch的值:0, 1, 2, ..., 99
# 每轮重新累计,否则平均损失会混进前面所有轮
total_loss, total_sample = 0, 0
# 2.6.3 每轮是分批次训练的,所以从数据加载器中获取批次数据
for train_x, train_y in dataloader: # 7批(16, 16, 16, 16, 16, 16, 4)
# 2.6.3.1 模型预测
y_pred = model(train_x)
# 2.6.3.2 计算(每批的平均)损失值
loss = criterion(y_pred, train_y.reshape(-1, 1)) # -1 自动计算
# 2.6.3.3 计算总损失 和 批次个数
total_loss += loss.item()
total_sample += 1
# 2.6.3.4 梯度清零 + 反向传播 + 梯度更新
optimizer.zero_grad() # 梯度清零
loss.backward() # 反向传播,计算梯度
optimizer.step() # 梯度更新
# 2.6.4 把本轮的(平均)损失值,添加到列表中
loss_list.append(total_loss / total_sample)
print(f'轮数:{epoch+1}, 平均损失值:{loss_list[-1]:.4f}')
# 2.7 打印(最终的)训练结果
print(f'{epochs}轮的平均损失分别为:{loss_list}')
print(f'模型参数,权重:{model.weight}, 偏置:{model.bias}')
# 2.8 绘制损失曲线
# 100轮 每轮的平均损失值
plt.plot(range(epochs), loss_list)
plt.title('损失值曲线变化图')
plt.grid() # 绘制网格线
plt.show()
# 2.9 绘制预测值和真实值的关系
# 2.9.1 绘制样本点分布情况
plt.scatter(x, y)
# 2.9.2 绘制训练模型的预测值
# x: 100个样本点的特征
y_pred = torch.tensor(data = [v * model.weight + model.bias for v in x])
# 2.9.3 计算真实值
y_true = torch.tensor(data = [v * coef + 14.5 for v in x])
# 2.9.4 绘制预测值和真实值的折线图
plt.plot(x, y_pred, color='red', label='预测值')
plt.plot(x, y_true, color='blue', label='真实值')
# 2.9.5 图例,网格
plt.legend()
plt.grid()
# 2.9.6 显示图像
plt.show()
# 3. 测试
if __name__ == '__main__':
# 3.1 创建数据集
x, y, coef = create_dataset()
# print(f'x: {x}, y: {y}, coef: {coef}')
# 3.2 模型训练
train(x, y, coef)
深度学习
34. 如何构建神经网络(neural network)
机器学习、深度学习都是实现 AI 的一种途径。
| 怎么来的 | |
|---|---|
| 机器学习 | 数学好,公式能推出来 |
| 深度学习 | 解释性稍差。6 层比 5 层好,为什么好往往说不清,是实验试出来的 |
后面按这条往下:神经网络 → 损失函数 → 网络优化方法 → 正则化方法 → 价格分类案例。这一节先把网络本身怎么搭起来。
34.1 什么是神经网络
人工神经网络(Artificial Neural Network,ANN),也简称神经网络(NN)。它是模仿生物神经网络结构和功能的计算模型。
人脑就是一堆神经元连起来的。神经元传电信号:树突接收输入,处理完,通过轴突把信号送出去。电信号从树突进到细胞核,电荷慢慢攒;攒到一定电位,细胞被激活,再从轴突发出去。
| 生物 | 网络里 |
|---|---|
| 树突,输入信号 | 输入层 |
| 细胞核里攒电荷,到电位才激活 | 隐藏层:先线性算出预测值,再过激活函数 |
| 轴突末梢,输出 | 输出层 |
一条计算链:输入数据 → 线性模型算出预测值 → Sigmoid → 概率,落在 0,1。
34.2 一个神经元
神经网络由多个神经元组成。构建神经网络,就是在构建神经元。
每个输入 xi 带一个权重 wi,再加偏置 b。先做线性的加权求和,再交给激活函数 f 做非线性,吐出 f(x)。分类时这个输出当概率。
z=b+i=1∑nxiwi=wx+b,y=f(z)
| 一段 | 结构 | 干什么 |
|---|---|---|
| 加权求和 ∑ | 线性 | 不同树突带来的信息,各自乘上权重,在细胞里加起来 |
| 激活函数 f | 非线性 | 把前面的和映射出去。常用 Sigmoid,映到 0,1 |
特征 → 线性回归得到预测值 → Sigmoid → 0,1。
例如用身高、体重、学历、专业、考试成绩、工作经验、性别去预测就业薪资:有几个特征,输入就有几路,每路一个权重。
34.3 多个神经元连成网
相邻层的神经元互相连接,每条连接分配一个强度(权重)。同层神经元之间互相隔离,不连。上一层每个都连下一层每一个,就是全连接(Full)。
有几个特征,输入层就有几个神经元。隐藏层可以看成若干个这样的人,每人收齐上一层全部输入。前一层的输出就是后一层的输入。
| 层 | 干什么 |
|---|---|
| Input 输入层 | 特征个数 = 神经元个数 |
| Hidden Layer 1 | 加权求和 |
| Hidden Layer 2 | 激活函数 |
| Output 输出层 | 得到 y1、 y2。激活常用 Sigmoid、Tanh、ReLU、Softmax |
连续输出要收成类别:例如 0.25 近似当成 1;大于等于 0 的都当成 1。
权重还是那条更新公式:
W1=W0−学习率×梯度
35. 神经网络_文字介绍
信息只朝一个方向走:从输入节点向前,经过隐藏节点,再到输出节点。34.3 里的全连接,用文字钉成下面这几条。
35.1 三层
| 层 | 是什么 | 干什么 |
|---|---|---|
| 输入层 Input Layer | 输入 x 的那一层。图像、文本、声音都从这里进 | 每个输入特征对应一个神经元,把数据传给下一层 |
| 隐藏层 Hidden Layers | 夹在输入和输出之间,可以有多层 | 加权求和,再过激活函数,把结果往后传 |
| 输出层 Output Layer | 输出 y 的那一层 | 给出结果 |
图像进网络之前,一种排法是 HWC:高、宽、通道。 (32,32,3) 就是高 32、宽 32、3 个通道。
三特征进第一层隐藏、出来 5 个神经元,写成 nn.Linear(3, 5)。输入层神经元个数 = 特征个数。再往后若仍是 5 个神经元,形状就是 (5,5)。
35.2 连接规则
- 同一层的神经元之间没有连接,互相隔离
- 第 N 层的每个神经元,和第 N−1 层的所有神经元相连(全连接)
- 第 N−1 层的输出,就是第 N 层的输入
- 每条连接都有权重: w 和 b
- 全连接网络接收的样本数据是二维的
35.3 内部状态值和激活值
每个神经元干活时带四个数。前向传播产出两个,反向传播再产出两个梯度。
| 值 | 梯度 | |
|---|---|---|
| 内部状态 | 内部状态值(加权求和) | 内部状态值梯度 |
| 激活 | 激活值 | 激活值梯度 |
隐藏层里每个神经元都是这四格:左上内部状态值,右上激活值,左下内部状态值梯度,右下激活值梯度。
内部状态值是神经元里存着的那个数,反映当前收到的输入、历史信息,以及网络内部的状态。
z=W⋅x+b
W 是权重矩阵, x 是输入, b 是偏置。
激活值 是用激活函数对 z 做非线性变换之后的结果。常见的有 ReLU、Sigmoid、Tanh。
a=f(z)
f 是激活函数, z 是内部状态值。前向先有 z,再有 a;反向前,梯度从激活值回到内部状态值。
36. 激活函数介绍
激活函数对每一层的输出做变换,给整个网络注入非线性。有了它,网络才能拟合各种曲线。
- 没有非线性的网络,等价于用一个线性模型去拟合
- 在输出上加激活函数,才引入非线性,网络才能逼近任意函数,复杂问题才拟合得动
36.1 不用激活函数,多层还是一条直线
两路输入 x0、 x1,中间两个神经元 c1、 c2,最后一个输出。中间不套 f,只做加权求和:
fc1fc2=w0c1x0+w1c1x1+bc1=w0c2x0+w1c2x1+bc2
输出再对 fc1、 fc2 做一次同样的线性组合:
fout=w0outfc1+w1outfc2+bout
把 fc1、 fc2 代进去,按 x0、 x1 合并系数:
k0k1c=w0outw0c1+w1outw0c2=w0outw1c1+w1outw1c2=w0outbc1+w1outbc2+bout
fout=k0x0+k1x1+c
层数看起来多,展开后仍是 k0x0+k1x1+c,和一层线性回归同一句话。激活函数就是用来打断这次合并的。
36.2 在浏览器里看这条差别
Neural Network Playground 是 TensorFlow 的网页演示,不用装环境。左边选一批点(分类或回归),中间加隐藏层,上面换学习率、激活函数、正则。激活可选 ReLU、Tanh、Sigmoid、Linear。
选 Linear 时,中间层再多,决策边界也弯不起来,对应上面「没有非线性就还是一条直线」。换成 ReLU、Tanh 或 Sigmoid,同样的层才能把点分开、把曲线拟合出来。它只用来看结构怎么影响结果,不拿来训练真实数据。
37. Sigmoid激活函数介绍
把任意输入压到 (0,1)。 x 是前面的加权求和, x 越大,输出越靠近 1。
f(x)=1+e−x1
导数可以只用函数值自己写出来。记 a=f(x):
f′(x)=f(x)(1−f(x))=a(1−a)=a−a2
a−a2 的最大值:对 a 求导, 1−2a=0,得 a=0.5。代回去:
0.5×(1−0.5)=0.25
所以导数落在 (0,0.25],最高只有 0.25,出现在输出正好是 0.5 的时候。
37.1 哪一段还有差别
| 输入 | 输出 | 导数 |
|---|---|---|
| −3,3 | 差别比较明显 | 靠近峰值 0.25 |
| −6,6 | 还有可见差别 | 还没贴到 0 |
| <−6 或 >6 | 几乎都挤在 0 或 1 | 接近 0 |
输入 100 和 10000 过 Sigmoid 之后都约等于 1。两个数相差 100 倍,激活值里看不出来,这部分信息丢了。
37.2 梯度消失
输入 <−6 或 >6 时导数接近 0,参数更新极慢,甚至更新不动。层数一多,每层都乘一个不超过 0.25 的数,梯度很快被乘没。一般 5 层之内就会出现梯度消失。
它的输出在 (0,1),不是以 0 为中心,实践里隐藏层很少用。Sigmoid 一般只放在二分类的输出层,把结果当概率。
37.3 代码演示图像
python
"""
案例:
绘制激活函数Sigmoid的 函数图像 和 导数图像.
Sigmoid激活函数介绍:
激活函数的目的:
给模型增加非线性功能, 让模型(神经元)既可以做分类, 还可以做回归问题.
激活函数的分类:
Sigmoid:
ReLU:
Tanh:
Softmax:
Sigmoid激活函数:
主要应用于 二分类的输出层, 且适用于 浅层神经网络(不超过5层).
数据在 [-6, 6]之间有效果, 在[-3, 3]之间效果明显.
求导后范围在 [0, 0.25]
细节:
绘制激活函数图像时出现以下提示, 需要将 anaconda3/Lib/site-packages/torch/lib 目录下的
libiomp5md.dll 文件删除
OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
"""
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE' # 避免上面的 OpenMP 重复初始化;不必去删 dll
# 导包
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# 1. 创建画布和坐标轴,1行2列.
fig, axes = plt.subplots(nrows=1, ncols=2)
# 2. 生成 -20 ~ 20之间的 1000个数据点.
x = torch.linspace(-20, 20, 1000)
# print(f'x: {x}')
# 3. 计算上述1000个点,Sigmoid激活函数处理后的值.
y = torch.sigmoid(x)
# print(f'y: {y}')
# 4. 在第1个子图中绘制Sigmoid激活函数的图像.
axes[0].plot(x, y)
axes[0].set_title('Sigmoid激活函数图像')
axes[0].grid()
# 5. 在第2个图上,绘制Sigmoid激活函数的导数图像.
# 5.1 重新生成 -20 ~ 20之间的 1000个数据点.
# 参1: 起始值, 参2: 结束值, 参3: 元素的个数, 参4: 是否需要求导.
x = torch.linspace(-20, 20, 1000, requires_grad=True)
# 5.2 具体的计算上述1000个点,Sigmoid激活函数导数后的值.
torch.sigmoid(x).sum().backward()
# 5.3 绘制图像.
axes[1].plot(x.detach(), x.grad)
axes[1].set_title('Sigmoid激活函数导数图像')
axes[1].grid()
plt.show()
37.4 拓展QA
激活函数和损失函数有什么关系?
激活函数和损失函数不是一回事,接在同一条链的两头。
| 激活函数 | 损失函数 | |
|---|---|---|
| 放在哪 | 每一层神经元里面,输出层也算一层 | 整张网的最后,只算一次 |
| 吃什么 | 内部状态值 z=wx+b | 预测值(最后一层激活的输出)和真实值 y |
| 吐出什么 | 激活值 a=f(z),给下一层或当最终预测 | 一个标量,预测和真实差多少 |
| 导数干什么 | 决定这一层的梯度还能不能传回去 | 决定参数该往哪边改、改多大 |
前向:加权求和 → 激活 →(下一层再来一遍)→ 最后一层的激活值当成预测 → 损失拿预测和 y 比。反向:先对损失求导,再乘每一层激活函数的导数,才写进 w.grad。激活的导数接近 0(Sigmoid 饱和),损失再大,梯度也传不回去,参数几乎不动。
常见搭配看最后一层在干什么,不是随便接:
| 任务 | 最后一层激活 | 损失 |
|---|---|---|
| 回归,预测一个实数 | 不加,或 Linear | MSE |
| 二分类,输出当概率 | Sigmoid,压到 (0,1) | 二元交叉熵 |
| 多分类 | Softmax | 交叉熵 |
隐藏层的激活(ReLU、Tanh)只负责把网络弯起来,不直接和损失配对。损失只认最后吐出来的那个预测值。
38. Tanh激活函数介绍
把任意输入压到 (−1,1)。 x 是前面的加权求和, x 越大越靠近 1, x 越小越靠近 −1。图像以 0 为中心,关于 0 对称。
f(x)=1+e−2x1−e−2x
导数只用函数值自己写:
f′(x)=1−f(x)2=1−a2
1−a2 的最大值:对 a 求导, −2a=0,得 a=0。代回去是 1。所以导数落在 (0,1],最高是 1,出现在输出正好是 0 的时候。Sigmoid 的峰值只有 0.25。
38.1 哪一段还有差别
| 还有差别 | 效果比较明显 | 输出 | 导数 | |
|---|---|---|---|---|
| Sigmoid | −6,6 | −3,3 | (0,1) | (0,0.25] |
| Tanh | −3,3 | −1,1 | (−1,1) | (0,1] |
输入大概 <−3 或 >3 时,输出被挤到 −1 或 1,导数接近 0。
38.2 和 Sigmoid 比
它以 0 为中心,梯度比 Sigmoid 大。更新还是 W新=W旧−学习率×梯度,梯度更大,一步走得更远,收敛更快,迭代次数更少。
两侧导数同样会到 0,层数一深还是梯度消失。用的话,隐藏层放 Tanh,输出层仍放 Sigmoid。
38.3 代码演示图像
python
"""
案例:
绘制激活函数Tanh的 函数图像 和 导数图像.
Sigmoid激活函数介绍:
激活函数的目的:
给模型增加非线性功能, 让模型(神经元)既可以做分类, 还可以做回归问题.
激活函数的分类:
Sigmoid:
ReLU:
Tanh:
Softmax:
Sigmoid激活函数:
主要应用于 二分类的输出层, 且适用于 浅层神经网络(不超过5层).
数据在 [-6, 6]之间有效果, 在[-3, 3]之间效果明显,会将数据值映射到:[0, 1]
求导后范围在 [0, 0.25]
Tanh:
主要用于隐藏层(浅层),且适用于 浅层神经网络(不超过5层)
数据在 [-3, 3]之间有效果, 在[-1, 1]之间效果明显,会将数据值映射到:[-1, 1]
求导后范围在 [0, 1],较之于Sigmoid,收敛速度快
细节:
绘制激活函数图像时出现以下提示, 需要将 anaconda3/Lib/site-packages/torch/lib 目录下的
libiomp5md.dll 文件删除
OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
"""
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE' # 避免上面的 OpenMP 重复初始化;不必去删 dll
# 导包
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# 1. 创建画布和坐标轴,1行2列.
fig, axes = plt.subplots(nrows=1, ncols=2)
# 2. 生成 -20 ~ 20之间的 1000个数据点.
x = torch.linspace(-20, 20, 1000)
# print(f'x: {x}')
# 3. 计算上述1000个点,Tanh激活函数处理后的值.
y = torch.tanh(x)
# print(f'y: {y}')
# 4. 在第1个子图中绘制Tanh激活函数的图像.
axes[0].plot(x, y)
axes[0].set_title('Tanh激活函数图像')
axes[0].grid()
# 5. 在第2个图上,绘制Tanh激活函数的导数图像.
# 5.1 重新生成 -20 ~ 20之间的 1000个数据点.
# 参1: 起始值, 参2: 结束值, 参3: 元素的个数, 参4: 是否需要求导.
x = torch.linspace(-20, 20, 1000, requires_grad=True)
# 5.2 具体的计算上述1000个点,Tanh激活函数导数后的值.
torch.tanh(x).sum().backward()
# 5.3 绘制图像.
axes[1].plot(x.detach(), x.grad)
axes[1].set_title('Tanh激活函数导数图像')
axes[1].grid()
plt.show()