数学基础

线性代数

标量

一个标量就是一个单独的数

向量

  • 一个向量是一列数
  • 可以把向量看做空间中的几个点,每个元素是不同坐标轴上的坐标
  • 向量中有几个数,就叫几维向量
  • 如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
  • 向量夹角余弦
  • \cos \Theta = A * B / |A|\cdot|B|
  • 向量的模:|A| = \sqrt{x1^2 + x2^2+...+xn^2}

矩阵

是一个二维数组,矩阵中的每个值是一个标量,可以通过行号和列号进行索引。

\begin{bmatrix} 1 &2 \\ 3& 4 \end{bmatrix}  2 * 2的矩阵,\begin{bmatrix} 1 & 3\\ 2& 4\\ 3& 5 \end{bmatrix}   3 * 2的矩阵

矩阵的运算
矩阵加法(需要维度相同)

\begin{bmatrix} 1 & 2\\ 3& 4 \end{bmatrix} +\begin{bmatrix} 4 &2 \\ 3&1 \end{bmatrix} =\begin{bmatrix} 5 &4 \\ 6&5 \end{bmatrix}

矩阵乘法(不满足交换律)
  • A * B != B * A
  • 当左矩阵A的列数等于右矩阵B的行数,A与B可以相乘
  • M*N矩阵乘以N*P的矩阵得到M*P的矩阵,A ∈R^{M*N}  * B ∈R^{N*P} = C∈R^{M*P}(R代表的是实数集)

   

  • 符合分配律
  • A *(B + C) = A*B + A*C
  • 符合结合律
  • A * (B * C) = (A*B)*C
矩阵点乘

注意:两矩阵必须形状一致

同位相乘,得到的维度也没有变

\begin{bmatrix} 1 &2 \\ 3&4 \end{bmatrix}\cdot \begin{bmatrix} 1 &2 \\ 0&-1 \end{bmatrix}= \begin{bmatrix} 1 &4 \\ 0 & -4 \end{bmatrix}

常见的矩阵操作
矩阵转置(行列互换transpose)

向量<==>矩阵

[1,2,3,4] ---reshape 2*2-->\begin{bmatrix} 1 &2 \\ 3&4 \end{bmatrix}

\begin{bmatrix} 1 &2 \\ 3&4 \\ 5&6 \end{bmatrix}  ---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)

导数

导数表示函数变化的方向

常见导数

求导法则

加减法则

[f(x) + g(x)]' = f(x)' + g(x)'

乘法法则

[f(x)*g(x)]' = f(x)'g(x) + g(x)'f(x)

除法法则

[f(x)/g(x)]' = [f(x)'g(x) - g(x)'f(x)] / g(x)^2

链式法则

若h(x) = f(g(x)),则

h(x)' = f(g(x))'g(x)'

下面举几个例子

梯度下降算法

找极小值问题

函数f(x)的值受x影响

目标:找到合适的x值,使得f(x)最小

方法:

1.任取一点x0,计算在这一点的导数值f(x0)

2.根据导数的正负,决定x0应当调大还是调小

3.迭代进行1,2直到x不在变化(或变化极小)

例如:

原函数为y=x^{2}  导函数为  y = 2 * x

x = -1这个点,导数值为 -2

该点导数为负数,说明在这一点,如果x增大,y会减小

所以f(x)最小值的点应当在-1右侧(大于-1

原函数为 y=x^{2}       导函数为 y = 2*x

x = 1这个点,导数值为 2

该点导数为正数,说明在这一点,如果x增大,y会增大

所以f(x)最小值的点应当在1左侧(小于1

梯度

可以理解为多元函数的导数,意义与导数基本一致

  • 原函数: y=3*x^{2}   
  • 导函数:   y = 6 * x       
  • x=1处的导数值:6

  • 原函数: y=3*x_{1}^{2} + 4*x_{2}^{2} + 5*x_{3}^{2} 
  • 导函数: y = \left \{ \right.6*x_{1},8*x_{2},10*x_{3}\left. \right \}
  • [111]处的梯度是[6810]
  • 梯度是个向量

梯度下降法

根据梯度,更新权重

学习率控制权重更新的幅度

SGD(Stochastic gradient descent

公式

\theta _{t+1} = \theta _t - \alpha \triangledown L(\theta_ t)

权重          权重    学习率  梯度

下面是大概的一个流程图

我们可以通过代码来理解

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. “通过梯度下降来找到最优权重”

这是你寻宝的具体策略梯度下降

虽然你蒙着眼睛看不见全貌,但你可以用脚感受一下脚下最陡峭的下坡方向。这个“最陡峭的下坡方向”就是梯度(准确说是负梯度)。

梯度下降的步骤就是:

  1. 感受坡度:计算当前位置的梯度(哪个方向最陡)。

  2. 迈出一步:沿着这个最陡的下坡方向走一小步(更新权重)。

    • 新权重 = 旧权重 - 学习率 × 梯度

    • 学习率就是你这一步的步长。步长太大可能会越过最低点;步长太小则下山太慢。

  3. 重复过程:到了新位置后,再次感受坡度,再迈一步……如此反复。

最终,你会一步一步地走到山谷的某个低点(可能是全局最低,也可能是局部最低)。

完整的反向传播过程
  1. 根据输入x和模型当前权重,计算预测值y'
  2. 根据y'和y使用loss函数计算loss
  3. 根据loss计算模型权重的梯度
  4. 使用梯度和学习率,根据优化器调整模型权重
权重的更新方式

举个例子说下下面三种方式

假设我们有10,000张图片(5,000张猫,5,000张非猫),要训练一个猫识别器。

所有样本一起计算梯度(累加)

# 每次迭代:
看完全部10,000张图片
计算平均错误程度
调整模型参数
# 完成一次参数更新

每次使用一个样本计算梯度

# 每次迭代:
随机看1张图片(比如第3,427张)
计算这张图片的错误程度
立即调整模型参数
# 完成一次参数更新,准备看下一张

每次使用n个样本计算梯度(累加)

# 每次迭代:
随机选择一小批数据(比如32张图片)
计算这32张图片的平均梯度
更新模型参数

网络结构

全连接层

又称线性层

计算公式

y = xA^T + b

A和b是参与训练的参数(在很多时候也称为w和b);

A的维度决定了输出的维度

输入:x (维度1 x 3

A.shape = 5 x 3

b.shape = 1 x 5

x *A^T      shape = 1 x 5

X*A^T + 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函数

应用场景:

  • 主要用于二分类问题输出层,将输出解释为概率

公式为

f(x) = \frac{1}{1 + e^{-x}}

导数为

计算方法比较简单,简单说一下,其实就是使用链式法则:

u= 1 + e^{_{-x}} , 令 v = -x,则

\frac{\mathrm{dy} }{\mathrm{d} x} = \frac{\mathrm{df} }{\mathrm{d} u}*\frac{\mathrm{du} }{\mathrm{d} v}*\frac{\mathrm{dv} }{\mathrm{d} x},套用这个公式就可以得出上面的结论,自己在纸上算下吧

tanh函数

应用场景:

  • 隐藏层中,Tanh通常比Sigmoid表现更好,因为它具有零均值的特性。

  • 在LSTM等门控循环网络中,Tanh常被用作候选状态生成的激活函数。

tanh(x) = (e^x - e^{-x}) / (e^x + e^{-x}),很多给的是tanh(x) = \frac{1-e^{-2x}}{1+e^{-2x}},这个是化简得来的,分子分母,上下同时乘以e^{_{-x}}即可得到

但是它又等于2sigmoid(2x) - 1,怎么得来的呢,就是给上面的1-e^{-2x},减2 然后加2,进行化简得到。

Relu函数

应用场景:

  • 目前最常用、默认的隐藏层激活函数,尤其适用于卷积神经网络(CNN)和深度前馈神经网络。

ReLU(x) = max(0, x)

  • 计算简单,收敛快

  • 解决梯度消失问题(正区间)

  • 问题:神经元"死亡"(负输入梯度为0)

Gelu函数

公式

GELU(x) = 0.5*x*(1 + Tanh(\sqrt{2/\pi }))*(x + 0.044715*x^{3})

softmax函数

应用场景:

  • 专门用于多分类问题输出层,通常与交叉熵损失函数结合使用。

  • 绝对不要在隐藏层使用Softmax

与Sigmoid的关系:

  • 可以认为 Sigmoid是类别数i=2(i是下面公式中的)时的Softmax特例

将任意实数向量转化为概率分布的激活函数

softmax(x_i) = \frac{e^{x_i}}{\sum_{j=1}^{n}e^{x_j}}

#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. 实践决策清单

当你需要选择时,可以按此清单自问:

  1. 我的输出是什么?

    • 连续实数 -> 优先考虑 MSE 或其变体(如MAE、Huber)。

    • 单个离散类别 -> 使用 分类交叉熵(Softmax)。

    • 两个独立选项 -> 使用 二元交叉熵(Sigmoid)。

    • 多个非互斥标签 -> 使用 多个二元交叉熵(Sigmoid)。

  2. 我的输出层激活函数是什么?

    • 无激活/线性 -> 适配 MSE

    • Softmax -> 必须 搭配 分类交叉熵

    • Sigmoid -> 必须 搭配 二元交叉熵。错误组合(如Sigmoid+MSE)会导致训练困难。

  3. 我是否关心异常值?

    • 如果你的数据噪声大、异常值多,MSE的平方特性会使其影响过大。此时可考虑MAE或Hber Loss,或在回归中尝试分位数损失。

  4. 我的模型是否难以训练?

    • 在分类任务中,如果模型收敛慢,首先检查损失函数和激活函数的搭配是否正确。错误搭配是新手最常见的训练失败原因之一。

更多推荐