AI人工智能-深度学习的基本原理-第二周(小白)
数学基础
线性代数
标量
一个标量就是一个单独的数
向量
- 一个向量是一列数
- 可以把向量看做空间中的几个点,每个元素是不同坐标轴上的坐标
- 向量中有几个数,就叫几维向量
- 如4维向量:[1,2,3,4]

向量的运算
注意:以下都需要维度相同
- 向量加和:A + B = B + A
- 例子:[1, 2] + [3, 4] = [4, 6]
- 向量的内积:A * B = B * A
- 例子:[1, 2] * [3, 4] = 1 * 3 + 2 * 4 = 11
- 向量夹角余弦
= A * B / |A|
|B|
- 向量的模:|A| =
矩阵
是一个二维数组,矩阵中的每个值是一个标量,可以通过行号和列号进行索引。
2 * 2的矩阵,
3 * 2的矩阵
矩阵的运算
矩阵加法(需要维度相同)
矩阵乘法(不满足交换律)
- A * B != B * A
- 当左矩阵A的列数等于右矩阵B的行数,A与B可以相乘
- M*N矩阵乘以N*P的矩阵得到M*P的矩阵,A ∈
* B ∈
= C∈
(R代表的是实数集)


- 符合分配律
- A *(B + C) = A*B + A*C
- 符合结合律
- A * (B * C) = (A*B)*C
矩阵点乘
注意:两矩阵必须形状一致
同位相乘,得到的维度也没有变
常见的矩阵操作
矩阵转置(行列互换transpose)

向量<==>矩阵
[1,2,3,4] ---reshape 2*2-->
---flatten---> [1,2,3,4,5,6]
张量(tensor)
通俗的说:将三个2*2的矩阵排列在一起,就可以称为一个3*2*2的张量,将4个3*2*2的张量排列在一起,就可以称为4*3*2*2的张量
是神经网络训练中最为常见的数据形式。
所有的输入,输出,中间结果,几乎都是以张量的形式存在。

张量的常见操作
x
x.shape = 2 * 2 * 2
x.transpose(1,2)
x.shape = 2*2*2
作用
transpose()用于交换张量的两个指定维度。
特点
-
交换维度:只改变维度的顺序
-
不改变数据存储:与view类似,返回视图而非副本
-
可以交换任意两个维度:比简单的转置更灵活
语法
tensor.transpose(dim0, dim1)
import torch
# 创建一个3×4的张量
x = torch.tensor([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
print("原始形状:", x.shape) # torch.Size([3, 4])
# 交换第0维和第1维(行和列交换)
x_t = x.transpose(0, 1)
print("转置后形状:", x_t.shape) # torch.Size([4, 3])
print(x_t)
# tensor([[ 1, 5, 9],
# [ 2, 6, 10],
# [ 3, 7, 11],
# [ 4, 8, 12]])
# 对于3D张量
x_3d = torch.randn(2, 3, 4)
print("3D原始形状:", x_3d.shape) # torch.Size([2, 3, 4])
# 交换第0维和第2维
x_3d_t = x_3d.transpose(0, 2)
print("交换后形状:", x_3d_t.shape) # torch.Size([4, 3, 2])
x.view(4,2)
x.shape = 4*2
view()用于改变张量的形状(shape),但不改变张量中的数据本身,只改变数据的"视图"。
特点
-
不改变数据存储:只改变数据的解释方式
-
要求元素总数不变:新形状的元素数量必须与原形状相同
-
返回共享存储的视图:修改view后的张量会影响原始张量
语法
tensor.view(*shape)
import torch
# 原始张量
x = torch.arange(12) # [0, 1, 2, ..., 11]
print("原始张量:", x.shape) # torch.Size([12])
# 改变形状
x_2d = x.view(3, 4)
print("2D视图:", x_2d.shape) # torch.Size([3, 4])
print(x_2d)
# tensor([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
# 再改变形状
x_3d = x.view(2, 3, 2)
print("3D视图:", x_3d.shape) # torch.Size([2, 3, 2])
# 使用-1自动计算维度大小
auto_shape = x.view(3, -1) # -1会被自动计算为4
print("自动计算:", auto_shape.shape) # torch.Size([3, 4])
numpy常用操作






可以尝试执行下面大代码来验证自己的理解
#coding:utf8
import torch
import numpy as np
#numpy基本操作
x = np.array([[1,2,3], [4,5,6]])
#
print(x.ndim)
print(x.shape)
print(x.size)
print(np.sum(x))
print(np.sum(x, axis=0))
print(np.sum(x, axis=1))
print(np.reshape(x, (3,2)))
print(np.sqrt(x))
print(np.exp(x))
print(x.transpose())
print(x.flatten())
#
# print(np.zeros((3,4,5)))
# print(np.random.rand(3,4,5))
#
# x = np.random.rand(3,4,5)
x = torch.FloatTensor(x)
print(x.shape)
print(torch.exp(x))
print(torch.sum(x, dim=0))
print(torch.sum(x, dim=1))
print(x.transpose(1, 0))
print(x.flatten())
np.dot(x, x.T)
导数

导数表示函数变化的方向

常见导数


求导法则
加减法则
乘法法则
除法法则
链式法则
若h(x) = f(g(x)),则
下面举几个例子




梯度下降算法
找极小值问题
函数f(x)的值受x影响
目标:找到合适的x值,使得f(x)最小
方法:
1.任取一点x0,计算在这一点的导数值f(x0)
2.根据导数的正负,决定x0应当调大还是调小
3.迭代进行1,2直到x不在变化(或变化极小)

例如:
原函数为 导函数为 y = 2 * x
在x = -1这个点,导数值为 -2
该点导数为负数,说明在这一点,如果x增大,y会减小
所以f(x)最小值的点应当在-1的右侧(大于-1)

原函数为 导函数为 y = 2*x
在x = 1这个点,导数值为 2
该点导数为正数,说明在这一点,如果x增大,y会增大
所以f(x)最小值的点应当在1的左侧(小于1)

梯度
可以理解为多元函数的导数,意义与导数基本一致
- 原函数:
- 导函数: y = 6 * x
- 在x=1处的导数值:6
- 原函数:
- 导函数:
- 在[1,1,1]处的梯度是[6,8,10]
- 梯度是个向量

梯度下降法
根据梯度,更新权重
学习率控制权重更新的幅度
SGD(Stochastic gradient descent)
公式
权重 权重 学习率 梯度
下面是大概的一个流程图

我们可以通过代码来理解
import matplotlib.pyplot as pyplot
import math
import sys
# 训练数据的原始数据
X = [0.01 * x for x in range(100)]
Y = [2*x**2 + 3*x + 4 for x in X]
# pyplot.scatter(X, Y, color='red')
# pyplot.show()
# input()
# 前向传播函数,定义模型结构 w1*x² + w2*x + w3
def func(x):
y = w1 * x ** 2 + w2 * x + w3
return y
# 损失函数,使用均方误差(MSE)衡量预测值与真实值的差距
def loss(y_pred, y_true):
return (y_pred - y_true) ** 2
# 权重随机初始化
w1, w2, w3 = 0, 0, 0
# 学习率设置
lr = 0.1
#batch size
batch_size = 10
# 训练过程
for epoch in range(2000):
epoch_loss = 0
grad_w1 = 0
grad_w2 = 0
grad_w3 = 0
counter = 0
# zip的作用是将多个可迭代对象中对应位置的元素打包成一个个元组,返回一个迭代器
for x, y_true in zip(X, Y):
y_pred = func(x) # 前向传播:计算预测值
epoch_loss += loss(y_pred, y_true) # 累加损失
counter += 1
#梯度计算--链式法则
grad_w1 += 2 * (y_pred - y_true) * x ** 2
grad_w2 += 2 * (y_pred - y_true) * x
grad_w3 += 2 * (y_pred - y_true)
if counter == batch_size:
#权重更新
# 使用梯度下降公式:w = w - lr * ∇w
w1 = w1 - lr * grad_w1/batch_size #sgd
w2 = w2 - lr * grad_w2/batch_size
w3 = w3 - lr * grad_w3/batch_size
counter = 0
grad_w1 = 0
grad_w2 = 0
grad_w3 = 0
epoch_loss = epoch_loss/len(X)
print("第%d轮, loss %f" % (epoch, epoch_loss))
if epoch_loss < 0.00001:
break
print(f"训练后权重:w1:{w1} w2:{w2} w3:{w3}")
#使用训练后模型输出预测值
Yp = [func(i) for i in X]
#预测值与真实值比对数据分布
pyplot.scatter(X, Y, color="red")
pyplot.scatter(X, Yp)
pyplot.show()
# 求导说明:
# 对损失函数 :(y_pred - y_true) ** 2,求w1的梯度
# 链式法则
# 令 u = y_pred - y_true = w1 * x ** 2 + w2 * x + w3 - y_true, 即损失函数为 = u ** 2
# 所以根据链式法则,导函数 = 2u * du/dw1 (u对w1的偏导数)
# du/dw1 = x ** 2 (除w1外,其他字母都看做常数,常数导数为0)
# 所以导函数 = 2u * x ** 2
# 同理可得w2,w3的导函数
# 所以梯度 = [2u * x ** 2, 2u * x, 2u]
求解目标,损失函数

损失函数越小,模型越好
学习的目标是损失函数最小化
模型权重影响损失函数值
通过梯度下降来找到最优权重
核心比喻:蒙眼寻宝
想象一下,你被蒙上眼睛,放在一个广阔的山谷里。你的目标是找到山谷的最低点(宝藏所在处)。
-
山谷的地形 = 损失函数
-
你的位置 = 模型的当前权重
-
山谷的海拔高度 = 损失函数的值
-
你的目标(找到最低点) = 损失函数最小化
现在,我们来逐句理解:
1. “损失函数越小,模型越好”
在这个比喻里:
-
“损失函数” 就是衡量你位置好坏的指标,也就是你脚下的海拔高度。
-
“模型越好” 意味着你越接近宝藏。
为什么损失函数小就好?
-
如果你站在海拔1000米的地方,说明你离谷底(宝藏)还很远,模型预测得很不准。
-
如果你站在海拔10米的地方,说明你已经非常接近谷底了,模型预测得很准。
-
所以,海拔高度(损失函数值)越低,代表你的位置(模型)越好。
在机器学习中,损失函数直接衡量了模型预测值与真实值之间的差距。差距越小,模型自然越好。
2. “学习的目标是损失函数最小化”
这对应了寻宝游戏的最终目标:找到那个海拔最低的点。
你蒙上眼睛后,脑子里唯一的念头就是:“我该怎么走,才能让我脚下的海拔越来越低?” 这个“让海拔最低”的念头,就是“损失函数最小化”。
对于模型来说,它学习的所有努力,都围绕着“如何调整自己,让预测误差(损失)降到最低”这个核心目标。
3. “模型权重影响损失函数值”
这指的是你移动会改变你的海拔。
-
模型的权重:就像是你的坐标 (x, y)。你站在山谷的不同位置(不同的权重组合),就会对应不同的海拔高度(不同的损失值)。
-
只要你移动(改变权重),你脚下的高度(损失值)就一定会改变。
所以,要想改变损失函数,唯一的方法就是调整模型的权重。
4. “通过梯度下降来找到最优权重”
这是你寻宝的具体策略:梯度下降。
虽然你蒙着眼睛看不见全貌,但你可以用脚感受一下脚下最陡峭的下坡方向。这个“最陡峭的下坡方向”就是梯度(准确说是负梯度)。
梯度下降的步骤就是:
-
感受坡度:计算当前位置的梯度(哪个方向最陡)。
-
迈出一步:沿着这个最陡的下坡方向走一小步(更新权重)。
-
新权重 = 旧权重 - 学习率 × 梯度 -
学习率就是你这一步的步长。步长太大可能会越过最低点;步长太小则下山太慢。
-
-
重复过程:到了新位置后,再次感受坡度,再迈一步……如此反复。
最终,你会一步一步地走到山谷的某个低点(可能是全局最低,也可能是局部最低)。
完整的反向传播过程
- 根据输入x和模型当前权重,计算预测值y'
- 根据y'和y使用loss函数计算loss
- 根据loss计算模型权重的梯度
- 使用梯度和学习率,根据优化器调整模型权重
权重的更新方式
举个例子说下下面三种方式
假设我们有10,000张图片(5,000张猫,5,000张非猫),要训练一个猫识别器。
所有样本一起计算梯度(累加)
# 每次迭代:
看完全部10,000张图片
计算平均错误程度
调整模型参数
# 完成一次参数更新
每次使用一个样本计算梯度
# 每次迭代:
随机看1张图片(比如第3,427张)
计算这张图片的错误程度
立即调整模型参数
# 完成一次参数更新,准备看下一张
每次使用n个样本计算梯度(累加)
# 每次迭代:
随机选择一小批数据(比如32张图片)
计算这32张图片的平均梯度
更新模型参数
网络结构
全连接层
又称线性层
计算公式
A和b是参与训练的参数(在很多时候也称为w和b);
A的维度决定了输出的维度
输入:x (维度1 x 3)
A.shape = 5 x 3
b.shape = 1 x 5
x *
shape = 1 x 5
X*
+ b shape = 1 x 5
import torch
import torch.nn as nn
import numpy as np
class TorchModel(nn.Module):
def __init__(self, input_size=3, hidden_size1=5, hidden_size2=2):
super(TorchModel, self).__init__()
# nn.Linear(input_size, output_size):PyTorch中的全连接层
# layer1: 3输入 → 5输出(权重矩阵:5×3)
# layer2: 5输入 → 2输出(权重矩阵:2×5)
self.layer1 = nn.Linear(input_size, hidden_size1)
self.layer2 = nn.Linear(hidden_size1, hidden_size2)
def forward(self, x):
'''
定义前向传播,数据如何流过网络
'''
x = self.layer1(x)
y_pred = self.layer2(x)
return y_pred
# 进行验证
x = np.array([[3.1, 1.3, 1.2],
[2.1, 1.3, 13]])
torch_model = TorchModel(3, 5, 2)
# 返回模型的所有参数(权重和偏置)
state_dict = torch_model.state_dict()
torch_model_w1 = state_dict.get("layer1.weight").numpy()
torch_model_b1 = state_dict.get("layer1.bias").numpy()
torch_model_w2 = state_dict.get("layer2.weight").numpy()
torch_model_b2 = state_dict.get("layer2.bias").numpy()
print(torch_model_w1, "torch w1 权重")
print(torch_model_b1, "torch b1 权重")
print("-----------")
print(torch_model_w2, "torch w2 权重")
print(torch_model_b2, "torch b2 权重")
print("-----------")
torch_x = torch.FloatTensor(x)
y_pred = torch_model.forward(torch_x)
print("torch模型预测结果:", y_pred)
上面是torch框架的模型,下面我们自定义函数来进行验证,验证的时候两个要放到一起执行
# 自定义模型
class DiyModel:
def __init__(self, w1, b1, w2, b2):
self.w1 = w1
self.b1 = b1
self.w2 = w2
self.b2 = b2
def forward(self, x):
hidden = np.dot(x, self.w1.T) + self.b1
y_pred = np.dot(hidden, self.w2.T) + self.b2
return y_pred
# 这里要注意,这里的输入是把torch模型权重拿过来自己实现计算过程
diy_model = DiyModel(torch_model_w1, torch_model_b1, torch_model_w2, torch_model_b2)
y_pred_diy = diy_model.forward(x)
print("diy模型预测结果:", y_pred_diy)
激活函数
为模型添加非线性因素,使模型具有拟合非线性函数的能力。也可以称为神经元的“开关”或者“调节器”
为什么需要激活函数?
无激活函数
# 多个线性层叠加仍然是线性的
output = W3 × (W2 × (W1 × input + b1) + b2) + b3
# 这等价于:output = W_total × input + b_total
# 无论多少层,都只能表示线性关系!有激活函数
# 非线性激活函数让网络能够学习复杂模式
output = activation(W3 × activation(W2 × activation(W1 × input + b1) + b2) + b3)
# 现在可以表示非常复杂的非线性关系!
常见的激活函数
Sigmoid函数
应用场景:
- 主要用于二分类问题的输出层,将输出解释为概率

公式为
导数为

计算方法比较简单,简单说一下,其实就是使用链式法则:
令 , 令 v = -x,则
,套用这个公式就可以得出上面的结论,自己在纸上算下吧
tanh函数
应用场景:
-
在隐藏层中,Tanh通常比Sigmoid表现更好,因为它具有零均值的特性。
-
在LSTM等门控循环网络中,Tanh常被用作候选状态生成的激活函数。

,很多给的是
,这个是化简得来的,分子分母,上下同时乘以
即可得到
但是它又等于,怎么得来的呢,就是给上面的
,减2 然后加2,进行化简得到。
Relu函数
应用场景:
-
目前最常用、默认的隐藏层激活函数,尤其适用于卷积神经网络(CNN)和深度前馈神经网络。

ReLU(x) = max(0, x)
-
计算简单,收敛快
-
解决梯度消失问题(正区间)
-
问题:神经元"死亡"(负输入梯度为0)
Gelu函数
公式

softmax函数
应用场景:
-
专门用于多分类问题的输出层,通常与交叉熵损失函数结合使用。
-
绝对不要在隐藏层使用Softmax。
与Sigmoid的关系:
-
可以认为 Sigmoid是类别数i=2(i是下面公式中的)时的Softmax特例。
将任意实数向量转化为概率分布的激活函数
#coding:utf8
import torch
import numpy
'''
softmax的计算
'''
def softmax(x):
res = []
for i in x:
res.append(numpy.exp(i))
res = [r / sum(res) for r in res]
return res
x = [1,2,3,4]
#torch实现的softmax
print(torch.softmax(torch.Tensor(x), 0))
#自己实现的softmax
print(softmax(x))
损失函数
均方差
MSE mean square error
对均方差在做开根号,可以得到根方差
适用于回归任务(预测结果是个概率值)
![]()

来个计算示例:
-
真实值:
[4, -0.5, 2, 7] -
预测值:
[2.5, 0.0, 2, 8]
误差平方:
(4-2.5)² = 2.25
(-0.5-0)² = 0.25
(2-2)² = 0
(7-8)² = 1
求和:2.25 + 0.25 + 0 + 1 = 3.5
均方差:3.5 / 4 = 0.875
疑问:为什么要使用平方?
解决符号问题:
-
如果直接用误差
(y_true - y_pred),正负误差会相互抵消 -
平方确保所有误差都为正值,避免抵消问题
放大大误差
-
平方运算使较大的误差被显著放大
-
这迫使模型更加关注那些预测得很差的样本
交叉熵 Cross Entropy
交叉熵衡量两个概率分布之间的差异,在深度学习中用于衡量预测概率分布与真实概率分布的差距。
适用于分类任务
分类任务中,模型输出经常是在所有类别上的概率分布
![]()
-
P:真实分布(真实标签) -
Q:预测分布(模型输出)
假设一个三分类任务,某样本的正确标签是第一类,则p = [1, 0, 0], 模型预测值假设为[0.5, 0.4, 0.1], 则交叉熵计算如下:
![]()
import torch
import torch.nn as nn
import numpy as np
'''
手动实现交叉熵的计算
'''
#使用torch计算交叉熵
ce_loss = nn.CrossEntropyLoss()
#假设有3个样本,每个都在做3分类
pred = torch.FloatTensor([[0.3, 2.1, 0.3],
[0.19, 1.2, 0.9],
[0.5, 0.4, 0.2]]) #n*class_num
#正确的类别分别为1,2,0
target = torch.LongTensor([1,2,0]) #n
'''
[0,1,0]
[0,0,1]
[1,0,0]
'''
loss = ce_loss(pred, target)
print(loss, "torch输出交叉熵")
#实现softmax函数
def softmax(matrix):
return np.exp(matrix) / np.sum(np.exp(matrix), axis=1, keepdims=True)
#验证softmax函数
# print(torch.softmax(pred, dim=1))
# print(softmax(pred.numpy()))
#将输入转化为onehot矩阵
def to_one_hot(target, shape):
one_hot_target = np.zeros(shape)
for i, t in enumerate(target):
one_hot_target[i][t] = 1
return one_hot_target
#手动实现交叉熵
def cross_entropy(pred, target):
batch_size, class_num = pred.shape
pred = softmax(pred)
target = to_one_hot(target, pred.shape)
entropy = - np.sum(target * np.log(pred), axis=1)
return sum(entropy) / batch_size
print(cross_entropy(pred.numpy(), target.numpy()), "手动实现交叉熵")
# print(np.log(2.7))
交叉熵+softmax的对比
总结对比
| 组件 | 作用 | 输入 | 输出 | 使用场景 |
|---|---|---|---|---|
| Softmax | 将Logits转换为概率分布 | 任意实数向量 | 概率分布(和为1) | 多分类输出层 |
| 交叉熵 | 衡量预测与真实的差异 | 两个概率分布 | 标量损失值 | 多分类损失函数 |
| 组合使用 | 完整的分类流水线 | Logits + 真实标签 | 可优化的损失 | 绝大多数分类任务 |
代码demo
# coding:utf8
# 解决 OpenMP 库冲突问题
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
import torch
import torch.nn as nn
import numpy as np
import random
import json
import matplotlib.pyplot as plt
"""
基于pytorch框架编写模型训练
实现一个自行构造的找规律(机器学习)任务
规律:x是一个5维向量,如果第1个数>第5个数,则为正样本,反之为负样本
"""
class TorchModel(nn.Module):
def __init__(self, input_size):
super(TorchModel, self).__init__()
self.linear = nn.Linear(input_size, 1) # 线性层
self.activation = torch.sigmoid # nn.Sigmoid() sigmoid归一化函数
self.loss = nn.functional.binary_cross_entropy # loss函数采用二分类交叉熵 或者使用nn.BCELoss()
# 当输入真实标签,返回loss值;无真实标签,返回预测值
def forward(self, x, y=None):
x = self.linear(x) # (batch_size, input_size) -> (batch_size, 1)
y_pred = self.activation(x) # (batch_size, 1) -> (batch_size, 1)
if y is not None:
return self.loss(y_pred, y) # 预测值和真实值计算损失
else:
return y_pred # 输出预测结果
# 生成一个样本, 样本的生成方法,代表了我们要学习的规律
# 随机生成一个5维向量,如果第一个值大于第五个值,认为是正样本,反之为负样本
def build_sample():
x = np.random.random(5)
if x[0] > x[4]:
return x, 1
else:
return x, 0
# 随机生成一批样本
# 正负样本均匀生成
def build_dataset(total_sample_num):
X = []
Y = []
for i in range(total_sample_num):
x, y = build_sample()
X.append(x)
Y.append([y])
# print(X)
# print(Y)
return torch.FloatTensor(X), torch.FloatTensor(Y)
# 测试代码
# 用来测试每轮模型的准确率
def evaluate(model):
model.eval()
test_sample_num = 100
x, y = build_dataset(test_sample_num)
print("本次预测集中共有%d个正样本,%d个负样本" % (sum(y), test_sample_num - sum(y)))
correct, wrong = 0, 0
with torch.no_grad():
y_pred = model(x) # 模型预测 model.forward(x)
for y_p, y_t in zip(y_pred, y): # 与真实标签进行对比
if float(y_p) < 0.5 and int(y_t) == 0:
correct += 1 # 负样本判断正确
elif float(y_p) >= 0.5 and int(y_t) == 1:
correct += 1 # 正样本判断正确
else:
wrong += 1
print("正确预测个数:%d, 正确率:%f" % (correct, correct / (correct + wrong)))
return correct / (correct + wrong)
def main():
# 配置参数
epoch_num = 20 # 训练轮数
batch_size = 20 # 每次训练样本个数
train_sample = 5000 # 每轮训练总共训练的样本总数
input_size = 5 # 输入向量维度
learning_rate = 0.001 # 学习率
# 建立模型
model = TorchModel(input_size)
# 选择优化器
optim = torch.optim.Adam(model.parameters(), lr=learning_rate)
log = []
# 创建训练集,正常任务是读取训练集
train_x, train_y = build_dataset(train_sample)
# 训练过程
for epoch in range(epoch_num):
model.train()
watch_loss = []
for batch_index in range(train_sample // batch_size):
#取出一个batch数据作为输入 train_x[0:20] train_y[0:20] train_x[20:40] train_y[20:40]
x = train_x[batch_index * batch_size : (batch_index + 1) * batch_size]
y = train_y[batch_index * batch_size : (batch_index + 1) * batch_size]
loss = model(x, y) # 计算loss model.forward(x,y)
loss.backward() # 计算梯度
optim.step() # 更新权重
optim.zero_grad() # 梯度归零
watch_loss.append(loss.item())
print("=========\n第%d轮平均loss:%f" % (epoch + 1, np.mean(watch_loss)))
acc = evaluate(model) # 测试本轮模型结果
log.append([acc, float(np.mean(watch_loss))])
# 保存模型
torch.save(model.state_dict(), "model.bin")
# 画图
print(log)
plt.plot(range(len(log)), [l[0] for l in log], label="acc") # 画acc曲线
plt.plot(range(len(log)), [l[1] for l in log], label="loss") # 画loss曲线
plt.legend()
plt.show()
return
# 使用训练好的模型做预测
def predict(model_path, input_vec):
input_size = 5
model = TorchModel(input_size)
model.load_state_dict(torch.load(model_path)) # 加载训练好的权重
print(model.state_dict())
model.eval() # 测试模式
with torch.no_grad(): # 不计算梯度
result = model.forward(torch.FloatTensor(input_vec)) # 模型预测
for vec, res in zip(input_vec, result):
print("输入:%s, 预测类别:%d, 概率值:%f" % (vec, round(float(res)), res)) # 打印结果
if __name__ == "__main__":
main()
# test_vec = [[0.07889086,0.15229675,0.31082123,0.03504317,0.88920843],
# [0.74963533,0.5524256,0.95758807,0.95520434,0.84890681],
# [0.90797868,0.67482528,0.13625847,0.34675372,0.19871392],
# [0.99349776,0.59416669,0.92579291,0.41567412,0.1358894]]
# predict("model.bin", test_vec)
知识点:
上面实例化之后的 self.linear在使用时可以使用()符号是因为nn.Linear继承自nn.Module,而nn.Module类重写了__call__方法:
# 简化的PyTorch内部实现
class Module:
def __call__(self, *input, **kwargs):
return self.forward(*input, **kwargs)
这意味着当你实例化一个层后,调用这个实例实际上是在调用它的forward方法。
__call__的作用:
当一个类定义了 __call__ 方法后,创建该类的实例后,就可以直接对实例使用 () 运算符,此时会自动调用 __call__ 方法中的逻辑。
多分类
# coding:utf8
# 解决 OpenMP 库冲突问题
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
import torch
import torch.nn as nn
import numpy as np
import random
import json
import matplotlib.pyplot as plt
"""
基于pytorch框架编写模型训练
实现一个多分类任务:判断5维向量中最大值的维度
"""
class TorchModel(nn.Module):
def __init__(self, input_size, num_classes):
super(TorchModel, self).__init__()
self.linear = nn.Linear(input_size, num_classes)
#self.linear = nn.Linear(input_size, 128) # 增加隐藏层
#self.relu = nn.ReLU() # 使用ReLU激活函数
#self.output = nn.Linear(128, num_classes) # 输出层,5个类别
self.loss = nn.CrossEntropyLoss() # 交叉熵损失函数
# 当输入真实标签,返回loss值;无真实标签,返回预测值
def forward(self, x, y=None):
x = self.linear(x) # (batch_size, input_size) -> (batch_size, 128)
#x = self.relu(x) # 激活函数
#x = self.output(x) # (batch_size, 128) -> (batch_size, 5)
if y is not None:
return self.loss(x, y) # 计算交叉熵损失
else:
return torch.softmax(x, dim=1) # 输出softmax概率分布
# 生成一个样本, 样本的生成方法,代表了我们要学习的规律
# 随机生成一个5维向量,找出最大值的索引作为标签
def build_sample():
x = np.random.random(5)
max_index = np.argmax(x) # 找到最大值的索引
return x, max_index
# 随机生成一批样本
def build_dataset(total_sample_num):
X = []
Y = []
for i in range(total_sample_num):
x, y = build_sample()
X.append(x)
Y.append(y) # 现在Y是类别索引,不是one-hot编码
# 将数据转换为PyTorch张量
# 注意:CrossEntropyLoss期望的标签是类别索引(LongTensor),不是one-hot编码
return torch.FloatTensor(X), torch.LongTensor(Y)
# 测试代码
# 用来测试每轮模型的准确率
def evaluate(model):
model.eval()
test_sample_num = 100
x, y = build_dataset(test_sample_num)
# 统计每个类别的样本数
class_counts = [0] * 5
for label in y:
class_counts[label] += 1
print("各类别样本数量:", class_counts)
correct, wrong = 0, 0
with torch.no_grad():
y_pred = model(x) # 模型预测,得到概率分布
predicted_classes = torch.argmax(y_pred, dim=1) # 取概率最大的类别
for pred, true in zip(predicted_classes, y):
if pred == true:
correct += 1
else:
wrong += 1
accuracy = correct / (correct + wrong)
print("正确预测个数:%d, 错误预测个数:%d, 正确率:%f" % (correct, wrong, accuracy))
return accuracy
def main():
# 配置参数
epoch_num = 50 # 增加训练轮数
batch_size = 32 # 批大小
train_sample = 5000 # 每轮训练总共训练的样本总数
input_size = 5 # 输入向量维度
num_classes = 5 # 输出类别数(0-4)
learning_rate = 0.001 # 学习率
# 建立模型
model = TorchModel(input_size, num_classes)
# 选择优化器
optim = torch.optim.Adam(model.parameters(), lr=learning_rate)
log = []
# 创建训练集
train_x, train_y = build_dataset(train_sample)
# 训练过程
for epoch in range(epoch_num):
model.train()
watch_loss = []
# 打乱数据
indices = torch.randperm(train_sample)
train_x_shuffled = train_x[indices]
train_y_shuffled = train_y[indices]
for batch_index in range(train_sample // batch_size):
# 取出一个batch数据
start_idx = batch_index * batch_size
end_idx = (batch_index + 1) * batch_size
x = train_x_shuffled[start_idx:end_idx]
y = train_y_shuffled[start_idx:end_idx]
loss = model(x, y) # 计算loss
loss.backward() # 计算梯度
optim.step() # 更新权重
optim.zero_grad() # 梯度归零
watch_loss.append(loss.item())
avg_loss = np.mean(watch_loss)
print("=========\n第%d轮平均loss:%f" % (epoch + 1, avg_loss))
acc = evaluate(model) # 测试本轮模型结果
log.append([acc, float(avg_loss)])
# 保存模型
torch.save(model.state_dict(), "multiclass_model.bin")
# 画图
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(range(len(log)), [l[0] for l in log], label="accuracy")
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Training Accuracy')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(range(len(log)), [l[1] for l in log], label="loss", color='red')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.legend()
plt.tight_layout()
plt.show()
return
# 使用训练好的模型做预测
def predict(model_path, input_vec):
input_size = 5
num_classes = 5
model = TorchModel(input_size, num_classes)
model.load_state_dict(torch.load(model_path)) # 加载训练好的权重
model.eval() # 测试模式
with torch.no_grad(): # 不计算梯度
result = model.forward(torch.FloatTensor(input_vec)) # 模型预测
for vec, res in zip(input_vec, result):
predicted_class = torch.argmax(res).item()
probabilities = res.numpy()
print("输入:%s, 预测类别:%d, 概率分布:%s" % (vec, predicted_class, probabilities))
if __name__ == "__main__":
main()
# 测试预测
test_vec = [
[0.1, 0.2, 0.9, 0.3, 0.4], # 最大值在索引2
[0.8, 0.1, 0.2, 0.3, 0.4], # 最大值在索引0
[0.1, 0.7, 0.2, 0.3, 0.4], # 最大值在索引1
[0.1, 0.2, 0.3, 0.9, 0.4], # 最大值在索引3
[0.1, 0.2, 0.3, 0.4, 0.9] # 最大值在索引4
]
predict("multiclass_model.bin", test_vec)
在上面多分类的训练中,加了激活函数的学习率是没有加激活函数的学习率的十分之一。
# 线性模型:相对平滑的凸优化问题
# 非线性模型:更复杂、可能有多个局部最小值的非凸优化问题# 高学习率在复杂曲面上容易"跳过"好的区域
# 低学习率能够更精细地探索损失曲面
# 经验法则:
# 添加一层ReLU ≈ 学习率降低2-10倍
# 添加BatchNorm ≈ 学习率可以适当提高
# 更深的网络 ≈ 需要更小的学习率# 常见配置:
learning_rate_without_activation = 0.01
learning_rate_with_relu = 0.001 # 降低10倍
learning_rate_with_batchnorm = 0.005 # 可以稍高
# 高学习率 + ReLU 可能出现的问题:
# 1. 训练不稳定(损失震荡)
# 2. 梯度爆炸
# 3. 无法收敛到好的局部最小值# 低学习率 + ReLU 的优势:
# 1. 训练稳定
# 2. 更好的泛化性能
# 3. 更容易找到好的最小值
补充
下面补充了均方差和交叉熵这两个的核心损失函数的本质区别/应用场景及选择策略
1.核心概念与数学本质
均方差衡量的是数值的差距。
-
公式(回归任务):
MSE = (1/n) * Σ (y_true - y_pred)^2 -
直观理解:它直接计算模型预测值
y_pred与目标真实值y_true之间的欧氏距离的平方。值越小,表示预测得越准。
交叉熵衡量的是概率分布的差距。
-
公式(二分类):
Binary CE = - [y_true * log(y_pred) + (1 - y_true) * log(1 - y_pred)] -
直观理解:它评估模型预测的概率分布
y_pred与真实的概率分布y_true之间的“信息”差异。在分类中,y_true通常是一个“一位有效”的编码。
2. 核心区别深度对比
下表从多个维度揭示了二者的本质差异:
| 对比维度 | 均方差 (MSE) | 交叉熵 (Cross-Entropy) |
|---|---|---|
| 核心任务 | 回归 - 预测连续值 | 分类 - 预测离散类别 |
| 数学本质 | 基于欧几里得距离的误差平方 | 基于信息论的概率分布差异 |
| 输出层激活 | 通常无激活或线性激活 | 必须配合Softmax(多分类)或Sigmoid(二分类) |
| 梯度行为 | 梯度与误差 (y_pred - y_true) 成正比。当预测严重错误时,梯度可能很大,但不一定高效。 |
梯度直接与 (y_pred - y_true) 成正比,且形式简洁。当预测错误时,梯度信号强而直接;预测正确时,梯度趋于平缓。这是其训练高效的关键。 |
| 对错误的惩罚 | 对大的绝对误差给予平方级的惩罚,对异常值敏感。 | 从概率角度惩罚:预测正确的概率越低,惩罚呈对数式增长,激励模型做出“坚定”的正确预测。 |
| 输出范围 | 输出可以是任意实数值。 | 输出被解释为概率,值在 [0, 1] 区间,且各类别概率之和为1。 |
3.应用场景与选择策略
均方差典型场景
-
数值回归预测:房价预测、气温预报、股票价格趋势、信号重建。
-
生成任务中的像素级重建:图像超分辨率、去噪、自编码器的重建阶段。此时目标是让生成的图像在像素值上接近目标图像。
-
物理场预测:计算流体动力学中预测速度场、压力场。
交叉熵典型场景
-
单标签分类:图像分类(ImageNet)、手写数字识别(MNIST)、垃圾邮件检测。使用分类交叉熵。
-
二分类任务:疾病诊断(阳性/阴性)、情感分析(正面/负面)。使用二元交叉熵。
-
多标签分类:一张图片中包含多个物体(如同时有“猫”、“狗”、“树”)。通常为每个类别独立使用一个带Sigmoid的二元交叉熵。
-
序列生成任务:机器翻译、文本生成。在每一步,模型预测的是下一个词在词汇表上的概率分布,使用分类交叉熵。
4.特例与一些讨论
-
为什么分类不用MSE?
在分类问题中,如果对Sigmoid/Softmax的输出使用MSE,在预测概率接近0或1(即信心很足但错了)时,梯度会变得非常小(饱和),导致梯度消失,学习极其缓慢。交叉熵则完美避免了这个问题。 -
交叉熵用于回归?
一般不会。但有一种特例:如果你想预测的连续值本质上是一个分布参数(例如,预测一个高斯分布的均值和方差),那么可以使用负对数似然作为损失函数,它在形式上与交叉熵同源。 -
结合使用的场景
在一些复杂模型中,可能会组合多个损失。例如:-
目标检测模型:可能用交叉熵负责分类分支,用MSE(或更平滑的L1 Loss)负责边界框坐标回归分支。
-
生成对抗网络:判别器使用交叉熵来区分真/假;生成器的损失可能基于判别器的输出(交叉熵的变体)或结合像素级MSE。
-
5. 实践决策清单
当你需要选择时,可以按此清单自问:
-
我的输出是什么?
-
连续实数 -> 优先考虑 MSE 或其变体(如MAE、Huber)。
-
单个离散类别 -> 使用 分类交叉熵(Softmax)。
-
两个独立选项 -> 使用 二元交叉熵(Sigmoid)。
-
多个非互斥标签 -> 使用 多个二元交叉熵(Sigmoid)。
-
-
我的输出层激活函数是什么?
-
无激活/线性 -> 适配 MSE。
-
Softmax -> 必须 搭配 分类交叉熵。
-
Sigmoid -> 必须 搭配 二元交叉熵。错误组合(如Sigmoid+MSE)会导致训练困难。
-
-
我是否关心异常值?
-
如果你的数据噪声大、异常值多,MSE的平方特性会使其影响过大。此时可考虑MAE或Hber Loss,或在回归中尝试分位数损失。
-
-
我的模型是否难以训练?
-
在分类任务中,如果模型收敛慢,首先检查损失函数和激活函数的搭配是否正确。错误搭配是新手最常见的训练失败原因之一。
-
更多推荐
所有评论(0)