深度学习

基于Python的研究和实现

Posted by LXG on February 9, 2026

查看系统中的python版本


lxg@lxg:~/code$ ls -al /usr/bin/python*
lrwxrwxrwx 1 root root      16  6月  3  2025 /usr/bin/python -> /usr/bin/python2
lrwxrwxrwx 1 root root       9  7月 28  2021 /usr/bin/python2 -> python2.7
-rwxr-xr-x 1 root root 3592536 12月 10  2024 /usr/bin/python2.7
lrwxrwxrwx 1 root root      10 11月 26  2024 /usr/bin/python3 -> python3.10
-rwxr-xr-x 1 root root 5937672  1月  8 14:52 /usr/bin/python3.10

NumPy

numpy 是 Python 里最重要的科学计算库之一,主要用来:

  • 数组运算(比 list 快很多)
  • 矩阵计算
  • 数学运算(线性代数、随机数等)
  • 深度学习底层计算(PyTorch、TensorFlow 都依赖它)

# 导入 Python 的 numpy 库,并把它起一个简写名字 np 使用
import numpy as np

def main():
    # Create a 3x3 array of random floats between 0 and 1
    array = np.random.rand(3, 3)
    
    # Print the array
    print("3x3 Array of Random Floats:")
    print(array)


if __name__ == "__main__":
    main()

NumPy 数组可以生成N维数组,数学上将一维数组称为向量, 二维数组称为矩阵,多维数组称为张量

名称 维度 例子
标量 0维 5
向量 1维 [1,2,3]
矩阵 2维 [[1,2],[3,4]] 比如:表格
张量 ≥3维 多维数组 比如:图片、视频、batch

广播不同维度数组之间的计算

Python VS C++

Python C++
类型 解释型 编译型
语法 简单 复杂
开发效率 非常高 较低
执行速度 非常快
内存控制 自动 手动
学习成本
适合人群 数据/AI/自动化 系统/性能/嵌入式

C++ 速度 = Python 的 10~100 倍

Matplotlib

Matplotlib 是 Python 最常用的数据可视化库,用来画图

函数 作用
plt.plot() 折线图
plt.scatter() 散点图
plt.bar() 柱状图
plt.imshow() 显示图像
plt.title() 标题
plt.xlabel() x轴
plt.ylabel() y轴
plt.legend() 图例
plt.show() 显示图

import numpy as np

import matplotlib.pyplot as plt

# 生成数据
x = np.arange(0, 6, 0.1)
y = np.sin(x)

# 创建图形
plt.plot(x, y)
plt.title("Sine Wave")
plt.xlabel("X Axis")
plt.ylabel("Y Axis")
plt.show()

运行后生成图形

python_matplotlib

显示图像


import numpy as np

import matplotlib.pyplot as plt

from matplotlib.image import imread

img= imread('car.jpg')
plt.imshow(img)

plt.show()

运行后显示结果

python_matplotlib_image

感知机

感知机就是一个“会加权判断的神经元”,深度学习的所有复杂神经网络都是由无数个感知机堆起来的

感知机的数学公式


y=f(w1​x1​+w2​x2​+...+wn​xn​+b)

符号 含义
x1, x2, …, xn 输入特征
w1, w2, …, wn 权重(代表每个特征的重要性)
b 偏置(可以调整阈值)
f() 激活函数,通常是阶跃函数(大于0就输出1,否则输出0)
y 输出(1 或 0)

权重相当于电流中的电阻, 电阻是决定电流流动难度的参数,电阻越低通过的电流越大。感知机则是权重越大,通过的信号就越大

简单逻辑电路

x1 x2 y
0 0 0
0 1 0
1 0 0
1 1 1

感知机公式


y=f(w1​x1​+w2​x2​+b)

逻辑门 权重 w1 w2 偏置 b
AND [1,1] -1.5
OR [1,1] -0.5
NOT [-1] 0.5

表示 AND 门 并不只有一组权重和偏置,它有 无限组等价解,只要满足条件就行

训练就是在无数可能解中找到一组合适的 w 和 b,让训练数据分类正确

机器学习是确定合适的参数的过程,人要做的是思考感知机的构造,并把训练数据交给计算机

开源大模型开源的是什么

部分 内容 是否通常开源
模型权重(Weights) 训练好的参数(神经网络里所有 w/b) ✅ 有些开源(如 LLaMA 2、MPT、Falcon 等)
模型架构(Architecture) Transformer 层数、注意力机制、激活函数、网络拓扑 ✅ 通常开源(论文 + 代码)
训练算法 / 数据 /训练框架 优化器(Adam、LoRA)、训练策略、训练数据、并行技巧、混合精度 ❌ 通常不开源或部分开源

DeepSeek开源程度

项目 权重是否开源 架构/代码是否开源 训练数据/算法是否开源
Qwen 系列 ✅ 多版本权重 ✅ 推理 + 架构代码 部分或未完全开源
DeepSeek 系列 ❗ 部分权重开源 ✅ 部分基础代码 ❌ 多数训练数据与细节不开源

垂直领域大模型的训练方法

  • 利用 通用大模型的预训练知识(语言、视觉、常识)
  • 再用垂直领域数据进行微调,让模型在专业领域表现更好

通用大模型(预训练):
   ┌───────────┐
   │语言/视觉/常识│
   └───────────┘
           │
           ▼ 微调 / LoRA / 指令调优
垂直领域大模型(专业知识):
   ┌─────────────┐
   │医疗/法律/工业│
   └─────────────┘

Anthropic 垂直模型是怎样训练的


           ┌───────────────────────────────┐
           │ 1️⃣ 通用大模型(开源/预训练) │
           │ - 已学通用语言/知识            │
           │ - 可直接推理                   │
           └─────────────┬─────────────────┘
                         │
                         ▼
           ┌───────────────────────────────┐
           │ 2️⃣ 数据收集与标注              │
           │ - 企业内部文档、FAQ、对话      │
           │ - 高质量标注:问题 → 答案      │
           │ ⚠ 数据成本高                   │
           └─────────────┬─────────────────┘
                         │
                         ▼
           ┌───────────────────────────────┐
           │ 3️⃣ 微调/定制训练               │
           │ - 全量微调:成本高,效果佳      │
           │ - LoRA/Adapter:小数据低成本    │
           │ - 调整权重适应行业任务          │
           │ ⚠ 算力成本高                   │
           └─────────────┬─────────────────┘
                         │
                         ▼
           ┌───────────────────────────────┐
           │ 4️⃣ 模型评估与优化               │
           │ - 验证准确率、召回率、延迟       │
           │ - 推理优化:量化/剪枝/多卡并行 │
           │ ⚠ 工程/硬件成本                 │
           └─────────────┬─────────────────┘
                         │
                         ▼
           ┌───────────────────────────────┐
           │ 5️⃣ 部署与监控                   │
           │ - 企业服务器或云端部署           │
           │ - 实时监控模型性能               │
           │ - 数据更新 → 定期微调            │
           │ ⚠ 人才与维护成本                │
           └───────────────────────────────┘

vertical_model_training

OCR 和 LLM 图像识别的差异

特性 OCR LLM 图像识别/多模态模型
主要任务 文字识别 图像理解 + 语言生成
输入 图像 图像 + 可选文本提示
输出 文本(字符级) 文本(句子/回答)
精度关注 字符级精度 场景理解和语言表达
技术 CNN/RNN/Transformer 图像编码器 + LLM
应用场景 扫描文档、票据、证件 图像问答、图像描述、视觉推理

OCR VS LLM 训练流程


OCR                       LLM
------                     -----
手工标注图片 ──► 训练集       原始文本/图像 ──► 自动生成训练任务
        │                         │
        ▼                         ▼
     OCR 模型                 LLM 模型
        │                         │
        ▼                         ▼
  图片 → 文字                提示 → 回答/生成文本

感知机无法解决XOR问题

感知机的局限性就在于它只能表示一条直线分割的空间, 对于非线性空间,需要使用曲线分割,如下图:

perceptron_xor

多层感知机

逻辑门是计算机最基本的构建块,CPU、算术逻辑单元(ALU)、寄存器等都是由大量逻辑门组合而成的

无限神经元 + 无限层 + 循环记忆 → 可以模拟图灵机 → 理论上可以实现计算机

perceptron_multilayer

神经元

线性函数: 输出值是输入值大常数倍 非线性函数: 函数是曲线

激活函数-阶跃函数

阶跃函数


import numpy as np

import matplotlib.pyplot as plt


def step_function(x):
    return np.array(x > 0, dtype=np.int32)

x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)

plt.plot(x, y)
plt.ylim(-0.1, 1.1)
plt.title("Step Function")
plt.xlabel("X Axis")
plt.ylabel("Y Axis")
# plt.grid()
plt.show()

step_function

Sigmoid 函数


import numpy as np

import matplotlib.pyplot as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

x = np.arange(-10.0, 10.0, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)
plt.title("Sigmoid Function")
plt.xlabel("X Axis")
plt.ylabel("Y Axis")
# plt.grid()
plt.show()

sigmoid_funtion

阶跃函数 = 数字电路 Sigmoid = 模拟电路

深度学习其实是“可微分计算机”,它不是在执行逻辑,而是在优化函数

多维数组的计算

矩阵乘法


import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6]])
print(a.ndim)  # 输出数组的维度
print(a.shape)  # 输出数组的形状
print(a.dtype)  # 输出数组中元素的数据类型
print(a.size)  # 输出数组中元素的总数

# 矩阵乘法
# 1 2 3
# 4 5 6
# 乘以
# 7  8
# 9 10
# 11 12

b = np.array([[7, 8], [9, 10], [11, 12]])
c = np.dot(a, b)
print(c)  # 输出矩阵乘法的结果

# 2 x 3 乘以 3 x 2 得到 2 x 2 矩阵

# 58 64
# 139 154

# 矩阵乘法不支持交换律
d = np.dot(b, a)
print(d)  # 输出矩阵乘法的结果

# 3 x 2 乘以 2 x 3 得到 3 x 3 矩阵

# 39 54 69
# 49 68 87
# 59 82 105

# 矩阵乘法的行数和列数必须匹配,否则会报错

matrix_multiplication

神经网络的内积


x = np.array([1, 2])
print(x.shape)  # 输出数组的形状
y = np.array([[1, 3, 5], [2, 4,6]])
print(y.shape)  # 输出数组的形状
z = np.dot(x, y)  # 1 x 2 乘以 2 x 3 得到 1 x 3 矩阵
print(z)  # 输出矩阵乘法的结果

matrix_multiplication_2

多层神经元之间传递


import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

X = np.array([1.0, 0.5]) # 输入层的输入
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]) # 输入层到隐藏层的权重
B1 = np.array([0.1, 0.2, 0.3]) # 隐藏层的偏置

print("X shape:", X.shape) # (2,)
print("W1 shape:", W1.shape) # (2,3)
print("B1 shape:", B1.shape) # (3,)

A1 = np.dot(X, W1) + B1 # 计算隐藏层的加权和

Z1 = sigmoid(A1) # 计算隐藏层的输出

print(A1) # 输出隐藏层的加权和 [0.3 0.7 1.1]
print(Z1) # 输出隐藏层的输出 [0.57444252 0.66818777 0.75026011]

# 第二层(隐藏层到输出层)
W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]) # 隐藏层到输出层的权重
B2 = np.array([0.1, 0.2]) # 输出层的偏置

A2 = np.dot(Z1, W2) + B2 # 计算输出层的加权和

Z2 = sigmoid(A2) # 计算输出层的输出

print(A2) # 输出输出层的加权和 [0.4 0.8]
print(Z2) # 输出输出层的输出 [0.59868766 0.68997448]

# 第三层(输出层到最终输出)
W3 = np.array([[0.1, 0.3], [0.2, 0.4]]) # 输出层到最终输出的权重
B3 = np.array([0.1, 0.2]) # 最终输出的偏置
A3 = np.dot(Z2, W3) + B3 # 计算最终输出的加权和
Z3 = sigmoid(A3) # 计算最终输出
print(A3) # 输出最终输出的加权和 [0.42336002 0.78658716]

print(Z3) # 输出最终输出 [0.60451623 0.68621873]

multilayer_neuronal_trans

封装


import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def init_network():
    network = {}
    network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
    network['b2'] = np.array([0.1, 0.2])
    network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
    network['b3'] = np.array([0.1, 0.2])
    return network

# 前向传播函数
def forward(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)

    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)

    a3 = np.dot(z2, W3) + b3

    return a3

network = init_network()
x = np.array([1.0, 0.5])
output = forward(network, x)
print("Final output:", output)  # 输出最终结果

机器学习的问题大致可以分为分类问题回归问题

  • 分类问题是数据属于哪一个类别问题。比如图像中人大性别
  • 回归问题是根据输入预测一个(连续的)数值的问题。比如根据图像预测某个人大体重的问题

softmax 函数

把一组“任意数值”变成“概率分布”


def softmax(a):
    c = np.max(a)
    exp_a = np.exp(a - c)  # 防止溢出
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y

对比 Sigmoid Softmax
作用 单个输出变概率 一组输出变概率
关系 彼此独立 彼此竞争
用途 二分类 / 多标签 多分类(只能选一个)
输出和 不等于1 必定=1

为什么模型不用直接输出概率?

神经网络先输出“打分”,是因为打分是自然的、好学的、稳定的。概率是“比较后的结果”,适合最后一步再算。


特征 → 打分(logits) → softmax → 概率 → 决策

手写数字识别

MNIST数据集


url_base = 'https://storage.googleapis.com/cvdf-datasets/mnist/'
key_file = {
    'train_img':'train-images-idx3-ubyte.gz', # 60,000 训练图像
    'train_label':'train-labels-idx1-ubyte.gz', # 60,000 训练标签
    'test_img':'t10k-images-idx3-ubyte.gz', # 10,000 测试图像
    'test_label':'t10k-labels-idx1-ubyte.gz' # 10,000 测试标签
}

  • train_img + train_label → 用于训练模型
  • test_img + test_label → 用于评估模型
  • 图像是 28×28 灰度图,标签是 0~9 数字
  • 都是 gzip 压缩的 IDX 格式,需要解压和解析才能使用

minst.pkl文件的生成过程


┌───────────────────────────┐
│  原始手写数字扫描图像     │
│  (灰度扫描,每张不定尺寸) │
└─────────────┬─────────────┘
              │
              ▼
┌───────────────────────────┐
│  数据预处理 & 统一尺寸     │
│  - 裁剪 & 缩放到28×28      │
│  - 灰度化 (0~255)         │
└─────────────┬─────────────┘
              │
              ▼
┌───────────────────────────┐
│ 保存为 IDX 二进制文件       │
│ - 图像文件 (IDX3):        │
│   train-images-idx3-ubyte  │
│   t10k-images-idx3-ubyte   │
│ - 标签文件 (IDX1):        │
│   train-labels-idx1-ubyte  │
│   t10k-labels-idx1-ubyte   │
│ - 文件头+连续二进制数据     │
└─────────────┬─────────────┘
              │
              ▼
┌───────────────────────────┐
│ gzip 压缩 (.gz)           │
│ - train-images-idx3-ubyte.gz │
│ - train-labels-idx1-ubyte.gz │
│ - t10k-images-idx3-ubyte.gz  │
│ - t10k-labels-idx1-ubyte.gz  │
└─────────────┬─────────────┘
              │
              ▼
┌───────────────────────────┐
│ 解压 .gz 文件              │
│ → 得到原始 IDX 二进制文件  │
└─────────────┬─────────────┘
              │
              ▼
┌───────────────────────────┐
│ 解析 IDX 文件 → NumPy 数组 │
│ - 图像: (num_samples,28,28)│
│ - 标签: (num_samples,)     │
└─────────────┬─────────────┘
              │
              ▼
┌───────────────────────────┐
│ 可选数据处理              │
│ - 归一化 (0~1)           │
│ - 展平 (28*28 → 784)      │
│ - one-hot 编码标签         │
└─────────────┬─────────────┘
              │
              ▼
┌───────────────────────────┐
│ 保存为 mnist.pkl           │
│ - Python 字典 pickle 文件   │
│ {train_img, train_label,   │
│  test_img, test_label}     │
└───────────────────────────┘

数据推理


import sys, os
import pickle
import numpy as np
import gzip
from PIL import Image

# MNIST数据保存路径
save_file = "./mnist.pkl"

# 
def _change_one_hot_label(X):
    T = np.zeros((X.size, 10))
    for idx, row in enumerate(T):
        row[X[idx]] = 1
        
    return T

def load_mnist(normalize=True, flatten=True, one_hot_label=False):
    """读入MNIST数据集
    
    Parameters
    ----------
    normalize : 将图像的像素值正规化为0.0~1.0
    one_hot_label : 
        one_hot_label为True的情况下,标签作为one-hot数组返回
        one-hot数组是指[0,0,1,0,0,0,0,0,0,0]这样的数组
    flatten : 是否将图像展开为一维数组
    
    Returns
    -------
    (训练图像, 训练标签), (测试图像, 测试标签)
    """
    # 判断mnist.pkl文件是否存在 
    with open(save_file, 'rb') as f:
        dataset = pickle.load(f)  # 载入数据集
    
    # 进行数据处理
    if normalize:
        for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].astype(np.float32)
            dataset[key] /= 255.0
    # one-hot编码
    if one_hot_label:
        dataset['train_label'] = _change_one_hot_label(dataset['train_label'])
        dataset['test_label'] = _change_one_hot_label(dataset['test_label'])

    # 展开为一维数组
    if not flatten:
         for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].reshape(-1, 1, 28, 28)

    # 返回训练数据和测试数据
    return (dataset['train_img'], dataset['train_label']), (dataset['test_img'], dataset['test_label']) 

def img_show(img):
    pil_img = Image.fromarray(np.uint8(img))
    pil_img.show()

# 读入MNIST数据集
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

img = x_train[0]
label = t_train[0]
print(label)  # 输出标签
print(img.shape)  # 输出图像形状(784,)
img = img.reshape(28, 28)  # 将一维数组形状转换为二维数组形状(28, 28)
print(img.shape)  # 输出图像形状(28, 28)
img_show(img)  # 显示图像

神经网络的输入层有784个神经元(28x28)算出, 输出层有10个神经元,对应10个阿拉伯数字

推理实现


# 获取MNIST数据集
def get_data():
    (x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
    return (x_train, t_train), (x_test, t_test)

# 学习到大权重参数
def init_network():
    with open("sample_weight.pkl", 'rb') as f:
        network = pickle.load(f)
    return network

# 预测函数
def sigmoid(x):
    # 数值稳定的 sigmoid 实现,避免在 np.exp 中出现溢出
    x = np.clip(x, -500, 500)
    return 1.0 / (1.0 + np.exp(-x))

# 前向传播中的softmax函数
def softmax(a):
    c = np.max(a)
    exp_a = np.exp(a - c)  # 防止溢出
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y

# 预测函数
def predict(network, x):
    # 神经网络中的权重
    W1, W2, W3 = network['W1'], network['W2'], network['W3']

    # 神经网络中的偏置
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    
    # 前向传播
    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    
    # 前向传播
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    
    # 前向传播 
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)
    
    return y

# 测试预测函数

(x_train, t_train), (x_test, t_test) = get_data()
# 使用测试集进行评估
x, t = x_test, t_test
network = init_network()

accuracy_cnt = 0
for i in range(len(x)):
    y = predict(network, x[i])
    p = np.argmax(y)  # 取得预测结果
    if p == t[i]:  # 预测正确
        accuracy_cnt += 1 # 统计正确次数

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))  # 输出正确率

几个概念

  • 识别精度:图别的识别率
  • 正规化:比如将图像的各个像素值除以255, 将数据的值限定在0.0 ~ 1.0 的范围内
  • 预处理:对神经网络的输入数据进行某种既定转换

批处理

输入数据和权重参数的形状(省略了偏置)

mnist_shape

输入一个由784个元素(28x28像素的数组)的一维数组后,输出一个有10个元素的一维数组


(x_train, t_train), (x_test, t_test) = get_data()
# 使用测试集进行评估
x, t = x_test, t_test

# print(x.shape)  # (10000, 784)
# print(t.shape)  # (10000,)

network = init_network()
batch_size = 100  # 每次处理的样本数量(一次送入模型预测100张图片)
accuracy_cnt = 0  # 用于累计预测正确的样本数

# 使用批处理方式对数据集进行评估
# 从第0个样本开始,每次步进 batch_size(100)
for i in range(0, len(x), batch_size):
    
    # 取出当前批次的数据,例如第0~99张图片、第100~199张图片……
    x_batch = x[i:i+batch_size]
    
    # 使用训练好的网络进行预测
    # 输出 y_batch 通常是每个样本属于各类别的概率(或得分)
    y_batch = predict(network, x_batch)
    
    # np.argmax(axis=1) 表示:
    # 在每一行(每张图片的预测结果)中,取概率最大的类别索引
    # 即模型最终预测的类别标签
    p = np.argmax(y_batch, axis=1)
    
    # 将预测结果 p 与真实标签 t[i:i+batch_size] 进行比较
    # p == t[...] 会得到一个布尔数组(True/False)
    # np.sum(...) 统计预测正确的数量
    accuracy_cnt += np.sum(p == t[i:i+batch_size])


print("Accuracy:" + str(float(accuracy_cnt) / len(x)))  # 输出正确率

神经网络的学习

学习是指从训练数据中自动获取最优权重参数的过程。学习的目的就是以损失函数为基准,找出能使函数的值达到最小的权重参数

学习的方式

deep_learning

对比项 传统机器学习(基于特征量) 深度学习(Deep Learning)
特征获取 人工设计特征(Feature Engineering) 自动从数据中学习特征
模型结构 浅层模型(如SVM、决策树、逻辑回归、KNN) 深层神经网络(CNN、RNN、Transformer等)
数据需求 少量数据即可 大量数据才有效
训练成本 高(需要GPU/TPU)
表达能力 有上限,难以处理复杂模式 极强,可拟合复杂非线性关系
复杂任务适应性 受限,如图像、语音、NLP难度大 擅长图像识别、语音、文本、生成任务
可解释性 高,可追踪每个特征贡献 低,黑盒性质强
鲁棒性 对噪声/变化敏感 对噪声和复杂变化鲁棒性强
工程门槛 中等 高,需要算力和深度学习框架
典型应用 信贷风控、传感器数据分析、预测建模 图像识别、语音识别、OCR、自动驾驶、生成式AI
优势场景 小数据、可解释、嵌入式/低算力 大数据、复杂模式、自动特征学习

深度学习有时也称为端到端机器学习,获得泛化能力是机器学习的最终目标

几个概念

  • 训练数据(监督数据)
  • 测试数据
  • 泛化能力
  • 过拟合:只对某个数据集推理有效

损失函数

损失函数是表示神经网络性能的指标

神经网络训练本质只有一件事:最小化损失函数

常见损失函数

分类 损失函数 用途 使用频率
回归 MSE 数值预测 ⭐⭐⭐⭐⭐
回归 MAE 抗异常值 ⭐⭐⭐⭐
回归 Huber MSE+MAE折中 ⭐⭐⭐
分类 Cross Entropy 多分类 ⭐⭐⭐⭐⭐
分类 Binary CrossEntropy 二分类 ⭐⭐⭐⭐⭐
分类 Focal Loss 类别不平衡 ⭐⭐⭐⭐
分割 Dice Loss 医学图像 ⭐⭐⭐⭐
分割 IoU Loss 目标分割 ⭐⭐⭐
检测 Smooth L1 目标框回归 ⭐⭐⭐⭐
检测 GIoU/CIoU YOLO/检测 ⭐⭐⭐⭐
NLP CTC Loss 语音/OCR ⭐⭐⭐
NLP KL Divergence 分布对齐 ⭐⭐⭐

数据标注

MNIST 属于 监督数据集, 每张图片都有:标签(0~9)

监督数据 = 带答案的数据集 训练数据 = 从监督数据中拿出来专门用来训练的那部分

标签(监督数据)是谁做的?

  • 人工标注
  • 半自动标注
  • 模型标注
  • 自监督生成

AI最贵的成本 = 数据标注,甚至比算力还贵

chatgpt 的数据是如何标注的

阶段1:预训练(几乎没有人工标注)

自监督学习(self-supervised):文本本身就是标签

数据来源:

  • 网页文本
  • 书籍
  • 论文
  • 代码
  • Wikipedia
  • 公共论坛

任务

给一句话,让模型预测下一个词

阶段2:监督微调(SFT)

开始出现“人工标注”

  1. 人类写高质量问答
  2. 人类修改模型回答

阶段3:RLHF(真正关键)

人类给回答打分,训练一个奖励模型

回答 人类评分
A
B 一般
C

真正昂贵的是这里,OpenAI、Anthropic、Google 都花巨资在这一步

原因:

  • 需要高质量人工评审
  • 需要专家
  • 需要大量对比数据

训练一个垂直行业 GPT 的完整数据流程


原始行业数据
    ↓
数据清洗 & 结构化
    ↓
任务设计(问答 / 推理 / 生成)
    ↓
自监督预训练 / 继续预训练
    ↓
监督微调(SFT)
    ↓
偏好对齐(RLHF / AI Feedback)
    ↓
评测 & 安全过滤
    ↓
上线 + 数据闭环

环节 成本占比
数据清洗 40%
标注 & SFT 30%
RLHF 20%
训练算力 10%

不是算力最贵,是数据

  • 美国 AI 公司:花钱买高质量数据 → 自动化清洗 → 高效训练 → 快速迭代
  • 中国 AI 公司:数据自己整合 → 人工清洗和标注 → 成本低但耗时 → 闭环能力有限

均方误差 MSE

均方误差: MSE, Mean Squared Error

模型每预测错一点,就扣分;错得越离谱,扣分越狠, 用于数值预测

公式


MSE = 平均(预测值 - 真实值)²

  • 均方误差用于回归问题,分类用会导致梯度消失或者收敛慢
  • 交叉熵直接基于概率 → 对分类问题优化更有效

交叉熵误差

cross entropy error

交叉熵就是一个衡量模型“自信对不对”的分数

  • 预测越接近真实 → 惩罚越少
  • 预测越偏离真实 → 惩罚越大

cross_entropy_error

函数图像

cross_entropy_function

mini-batch 学习

Mini-batch 学习 = 每次训练使用部分样本(比如 32、64、128 张图片)

  • 梯度计算:用这一小部分数据计算梯度
  • 更新模型参数
  • 循环遍历所有数据

优势

  1. 减少内存占用–>不必一次性加载全部训练数据
  2. 加快训练速度–>可以利用 GPU 并行处理 mini-batch
  3. 梯度噪声有益–>小幅度噪声可以帮助模型跳出局部最优

def _change_one_hot_label(X):
    T = np.zeros((X.size, 10))    # 1️⃣ 创建一个全零矩阵,行数 = 标签数量,列数 = 10(类别数)
    for idx, row in enumerate(T):
        row[X[idx]] = 1           # 2️⃣ 对应行 X[idx] 的位置设为 1
    return T                      # 3️⃣ 返回 one-hot 编码后的矩阵

# 对应上面的 X
array([[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],  # 类别 3
       [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],  # 类别 0
       [0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],  # 类别 4
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.]]) # 类别 1

_change_one_hot_label(X) 的作用就是把整数标签数组 X 转换成 one-hot 编码矩阵,用于神经网络训练时计算损失


import sys, os
import numpy as np
from mnist_demo import load_mnist, init_network, predict

(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False, one_hot_label=True)

print(x_train.shape)  # (60000, 784)
print(t_train.shape)  # (60000, 10)

train_size = x_train.shape[0]
batch_size = 10

# 从训练数据中随机抽取batch_size个样本
batch_mask = np.random.choice(train_size, batch_size)

# 选取对应的样本
xbatch = x_train[batch_mask]

# 选取对应的标签
tbatch = t_train[batch_mask]

def cross_entropy_error(y, t):
    """
    计算交叉熵误差(Cross-Entropy Loss)
    
    参数:
    y : np.ndarray
        神经网络输出的预测概率(Softmax 输出),形状为 (batch_size, 类别数)
    t : np.ndarray
        真实标签,one-hot 编码,形状为 (batch_size, 类别数) 或一维标签
    
    返回:
    float
        平均交叉熵损失值
    """
    
    # 如果 y 是一维数组(单个样本),则将其扩展为二维数组
    # 这样可以统一处理单样本和批量样本的情况
    if y.ndim == 1:
        t = t.reshape(1, t.size)  # 将真实标签从 (类别数,) 转为 (1, 类别数)
        y = y.reshape(1, y.size)  # 将预测概率从 (类别数,) 转为 (1, 类别数)
    
    # 获取批量样本的数量
    batch_size = y.shape[0]
    
    # 计算交叉熵误差
    # 公式:L = -1/N * Σ Σ t_ij * log(y_ij)
    # 1e-7 是为了避免 log(0) 出现数值错误
    # t * np.log(y + 1e-7) 是逐元素相乘,只保留真实标签对应位置的 log 概率
    # np.sum() 对所有样本和类别求和
    loss = -np.sum(t * np.log(y + 1e-7)) / batch_size
    
    return loss

为什么要设定损失函数

导数的概念

阶数 含义 通俗比喻
一阶 函数变化率 速度 / 坡度
二阶 函数变化率的变化率 加速度 / 坡度变化快慢
三阶 二阶变化率 加速度变化率 / 冲量
n阶 n-1 阶变化率的变化率 曲线更高阶变化

通俗理解导数

导数就是曲线上某一点的斜率

  • 一阶导数:你在山坡上,坡有多陡
  • 二阶导数:坡度变化快不快(弯曲)
  • 三阶导数:弯曲的变化快不快(颠簸感)

derivative

在神经网络的学习中,寻找最优参数(权重和偏置)时, 要寻找使损失函数的值尽可能小的参数, 需要计算参数的导数

参数的导数是指参数的调节方向

概念 比喻 作用
参数 旋钮 可调节,控制输出
损失函数 拼图评分 告诉你离目标有多远
参数导数 每个旋钮的调节方向 指示顺/逆、调多少
梯度 全部旋钮的调节指南 指示最优更新方向
精度 拼图是否完成 只看结果,不指导调整

数值微分

数值微分:利用微小的差分求导数的过程称为数值微分 解析性求导:是不含误差的真的导数

对比项 数值微分 解析求导
精度 近似 精确
计算成本 很高 低(推导后)
是否需要公式 不需要 必须有数学表达式
是否适合神经网络训练 ❌ 不适合 ✅ 必须使用
用途 验证梯度是否正确 实际训练

import numpy as np
import matplotlib.pylab as plt

def funcion_1(x):
    return 0.01 * x ** 2 + 0.1 * x

def numerical_diff(f, x):
    h = 1e-4  # 0.0001
    return (f(x + h) - f(x - h)) / (2 * h)

x = np.arange(0.0, 20.0, 0.1)
y = funcion_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x, y)


a = numerical_diff(funcion_1, 5)  # 0.2
b = numerical_diff(funcion_1, 10)  # 0.3

print(a)
print(b)

# 计算并绘制数值导数
y_diff = numerical_diff(funcion_1, x)
plt.plot(x, y_diff, color="orange", linestyle='--', label="f'(x)")

# 在 x=5 和 x=10 处画竖线、标记导数值并注释数值
plt.axvline(5, color='green', linestyle=':', linewidth=1)
plt.axvline(10, color='red', linestyle=':', linewidth=1)
plt.plot(5, a, 'o', color='green', label="f'(5)={:.3f}".format(a))
plt.plot(10, b, 'o', color='red', label="f'(10)={:.3f}".format(b))
plt.text(5, a, ' {:.3f}'.format(a), color='green', va='bottom')
plt.text(10, b, ' {:.3f}'.format(b), color='red', va='bottom')

plt.legend()
plt.grid(alpha=0.3)

plt.show()

numerical_diff

偏导数 和 梯度 gradient

偏导数:固定其他变量,只看某一个变量变化时,函数的变化率

名称 含义 比喻
偏导数 ∂f/∂x 多变量函数只看 x 的变化率 只沿 x 方向看坡度
偏导数 ∂f/∂y 多变量函数只看 y 的变化率 只沿 y 方向看坡度
梯度 ∇f 所有偏导数组成向量 指向坡度最大的方向

gradient


# coding: utf-8
"""
绘制简单二维函数的数值梯度(偏导)可视化。

本模块实现了使用中心差分法计算数值梯度的工具函数,并绘制函数
f(x) = x0^2 + x1^2 的梯度场(使用箭头图)。图中绘制负梯度以表示
最陡下降的方向,便于观察优化方向。

用法(直接运行脚本):
    python3 gradient_2d.py

脚本会在区间 [-2,2]x[-2,2] 上建立网格,计算每个网格点的梯度,并
用箭头表示 -∇f(下降方向)。
"""

import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D


def _numerical_gradient_no_batch(f, x):
    """对单个点(向量)x 计算数值梯度。

    使用中心差分近似对每一维进行求导:
        grad_i = (f(x + h*e_i) - f(x - h*e_i)) / (2*h)

    参数
    -----
    f : callable
        接受 numpy 数组 `x` 并返回标量 f(x) 的函数。
    x : numpy.ndarray
        表示 R^n 中单点的 1 维数组。

    返回
    ----
    grad : numpy.ndarray
        与 `x` 形状相同的 1 维数组,包含在点 `x` 处近似得到的偏导数值。
    """
    h = 1e-4  # 有限差分使用的步长(较小但不宜过小)
    grad = np.zeros_like(x)

    # 遍历每个维度,计算对应的偏导数
    for idx in range(x.size):
        tmp_val = x[idx]

        # f(x + h*e_i)
        x[idx] = float(tmp_val) + h
        fxh1 = f(x)

        # f(x - h*e_i)
        x[idx] = tmp_val - h
        fxh2 = f(x)


        # 中心差分公式
        grad[idx] = (fxh1 - fxh2) / (2 * h)

        # 恢复原始值以便计算下一维
        x[idx] = tmp_val

    return grad


def numerical_gradient(f, X):
    """对输入 X(批量或单点形式)计算数值梯度。

    若 `X` 为 1 维数组(单点),返回该点的梯度向量。若 `X` 为 2 维数组,
    则对 `X` 的每一行分别计算梯度并返回。

    注意:本函数保留了示例中使用的接口:以 `np.array([X_flat, Y_flat])`
    (形状为 (2, N))调用时,会返回与之对应形状的梯度数组。
    """
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)

        # 对 X 的每一行(或每个向量)计算梯度
        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)

        return grad


def function_2(x):
    """简单测试函数:f(x0,x1) = x0^2 + x1^2。

    可接受单个 1 维点 `x`(形状 (2,)),也可接受每行表示一个点的 2 维数组,
    分别返回标量或标量数组。
    """
    if x.ndim == 1:
        return np.sum(x ** 2)
    else:
        # If x is shape (N, 2), sum across axis=1 to produce N values
        return np.sum(x ** 2, axis=1)


def tangent_line(f, x):
    """返回给定 1 维点 `x` 处切线的 lambda 表达式。

    该辅助函数计算点处的梯度(斜率),并构造一阶线性近似(切线),
    主要用于一维示例展示。
    """
    d = numerical_gradient(f, x)
    print(d)
    y = f(x) - d * x
    return lambda t: d * t + y


if __name__ == '__main__':
    # 在区域 [-2, 2] x [-2, 2] 上生成规则网格
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)
    X, Y = np.meshgrid(x0, x1)

    # 将网格坐标展开为点列表。展开后 X 和 Y 为长度为 N 的 1 维数组。
    X = X.flatten()
    Y = Y.flatten()

    # 为 numerical_gradient 准备输入。此处的约定是传入一个 2xN 的数组,
    # 其中第 0 行为所有 x 坐标,第 1 行为所有 y 坐标,numerical_gradient
    # 将对每一行计算对应的偏导。
    grad = numerical_gradient(function_2, np.array([X, Y]))

    # 将负梯度 (-∇f) 绘制为箭头。对于 f(x)=x^2,-∇f 指向原点(极小值点),
    # 绘制负梯度有助于直观展示最陡下降的方向。
    plt.figure()
    plt.quiver(X, Y, -grad[0], -grad[1], angles="xy", color="#666666")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    # 说明:当前图中未添加带标签的图例元素,调用 legend() 会为空,
    # 因此注释掉以避免警告;若后续添加带标签的图元,则可启用 legend().
    plt.draw()
    plt.show()

gradient_2d

梯度下降法

机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必须在学习时找到最优参数(权重和偏置)

这里说的最优参数是指损失函数取最小值时的参数。但是损失函数很复杂,参数空间庞大,我们不知道它在何处取得最小值。

通过巧妙的使用梯度来寻找函数的最小值的方法就是梯度法

鞍点:函数的极小值、最小值

局部最优(local minimum)和全局最优(global minimum)

gradient_descent

学习率

每次沿着坡往下走,你迈多大的一步

  • 梯度 = 告诉你往哪走(方向)
  • 学习率 = 决定你走多远(步长)

如果:

  • 步子太大 → 可能直接跨过谷底
  • 步子太小 → 很久都走不到谷底

learning_rate


# coding: utf-8
"""梯度下降法的实现与可视化。

本模块演示了梯度下降优化算法的基本原理:通过沿负梯度方向迭代更新参数,
最终使目标函数收敛到最小值附近。脚本会绘制优化过程中参数的轨迹。
"""

import numpy as np
import matplotlib.pylab as plt
from gradient_2d import numerical_gradient


def gradient_descent(f, init_x, lr=0.01, step_num=100):
    """使用梯度下降法优化目标函数。

    通过计算目标函数的数值梯度,沿负梯度方向迭代更新参数,直到达到
    指定的迭代步数。记录每一步的参数值以便后续绘制优化轨迹。

    参数
    -----
    f : callable
        目标函数,接受 numpy 数组并返回标量。
    init_x : numpy.ndarray
        初始参数值,维数为问题的维度。
    lr : float,可选
        学习率(步长),控制每步的更新幅度。默认值为 0.01。
    step_num : int,可选
        迭代步数。默认值为 100。

    返回
    -----
    x : numpy.ndarray
        最终优化得到的参数值。
    x_history : numpy.ndarray
        所有迭代步骤的参数值,形状为 (step_num, dim),可用于绘制优化轨迹。
    """
    x = init_x
    # 存储每次迭代的参数值以便后续绘制
    x_history = []

    for i in range(step_num):
        # 记录当前参数值
        x_history.append( x.copy() )

        # 计算目标函数在当前点的梯度(数值方法)
        grad = numerical_gradient(f, x)
        # 沿负梯度方向更新参数:x_{k+1} = x_k - lr * ∇f(x_k)
        x -= lr * grad

    # 返回最终参数和完整的优化轨迹
    return x, np.array(x_history)


def function_2(x):
    """目标函数:f(x0, x1) = x0^2 + x1^2。

    这是一个简单的凸函数,在原点 (0, 0) 处达到全局最小值 0。
    """
    return x[0]**2 + x[1]**2


# ============ 主程序 ============

# 设置初始参数值
init_x = np.array([-3.0, 4.0])

# 设置学习率和迭代步数
lr = 0.1  # 较大的学习率使收敛更快
step_num = 20  # 迭代 20 步

# 执行梯度下降优化
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)

# ============ 绘制结果 ============

# 绘制坐标轴参考线(x=0 和 y=0 轴)
plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')

# 绘制优化过程中的参数轨迹点
plt.plot(x_history[:,0], x_history[:,1], 'o')

# 设置图的范围和标签
plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")

# 显示图像
plt.show()

gradient_descent_2

概念总结

概念 本质
梯度 告诉往哪走
学习率 决定走多远
梯度下降 找谷底的方法
损失函数 山谷形状