0%

SciPy 与 SymPy:数值算结果,符号推公式,各管各的

做科学计算的时候,你经常会遇到两种需求:

  • 算一个具体的数:比如 ∫₀¹ sin(x) dx = 0.4596...
  • 推一个数学公式:比如 ∫ x·sin(x) dx = -x·cos(x) + sin(x)

前者是数值计算(SciPy 的活),后者是符号运算(SymPy 的活)。数值给结果,符号给表达式。 两套工具,解决两类问题。

数值积分:函数没原函数,或者数据来自实验

很多工程问题需要算积分,但被积函数可能没有初等原函数,或者数据是测量来的(离散点,没有表达式)。这时候用数值积分。

一维定积分:quad(最常用)

1
2
3
4
5
6
from scipy.integrate import quad
import numpy as np

# 算 ∫₀¹ sin(x) dx
result, error = quad(np.sin, 0, 1)
print(f"{result:.10f}") # 0.4596976941

quad 返回两个值:积分结果和误差估计。误差估计别看小,它告诉你结果可不可信。 如果误差比结果还大,说明积分没收敛。

带参数的积分:

1
2
3
4
def integrand(x, a, b):
return np.exp(-a * x) * np.sin(b * x)

result, error = quad(integrand, 0, np.inf, args=(2.0, 3.0))

二重积分:dblquad(注意积分顺序)

1
2
3
4
5
6
7
8
9
10
11
12
13
from scipy.integrate import dblquad

# 算半球体积:∫∫ √(1-x²-y²) dy dx
def f(x, y):
return np.sqrt(max(0, 1 - x**2 - y**2))

volume, error = dblquad(
f,
-1, 1,
lambda x: -np.sqrt(1-x**2),
lambda x: np.sqrt(1-x**2)
)
print(f"{volume:.6f}") # 2.094395(= 2π/3)
阅读全文 »

pdb调试器

python dis操作(反汇编) python -m dis “print(‘hello’)”

矩阵求导?

线性回归->逻辑回归、Scikit-Learn 二分类、多分类(OVO/OVR/MVM、softmax)。如果不是分类问题的话,是什么问题呢

损失函数:极大似然估计、相对熵、交叉熵

正则化

神经网络是在做特征提取

图像进行稀疏编码

DBSCAN 本身并没有被直接嵌入到大模型的训练或推理架构中(你不会在 Transformer 的代码里找到 DBSCAN),但在大模型的研究、评估和数据工程环节,它正变得越来越重要。

简单来说:DBSCAN 不是大模型的”零件”,而是大模型生态中的”诊断工具”和”数据过滤器”。

以下是 DBSCAN 在大模型领域最核心的几个应用场景:

1. 模型内部表征分析(Mechanistic Interpretability)

这是目前最前沿的用法。研究者将大模型中间层的隐藏状态提取出来,用 DBSCAN 进行聚类,以理解模型”学到了什么”。

  • 发现语义簇:对同一层不同 token 的 embedding 做 DBSCAN,可以发现模型是否自发地将语法类别(名词/动词)、情感倾向、甚至事实知识编码成了独立的稠密簇。
  • 识别异常激活:DBSCAN 天然能识别噪声点(标记为 -1)。如果某些 token 的表征无法归入任何簇,往往意味着模型在该处出现了”困惑”或错误处理,这对定位幻觉(Hallucination)有重要价值。
  • 对比不同模型:比较两个模型在同一任务上的表征聚类结构,可以量化它们的”认知差异”。

💡 为什么选 DBSCAN 而不是 K-Means?
大模型的表征空间是高维且高度非凸的,簇的形状不规则、数量未知。K-Means 假设球形簇且需预设 K 值,在这种场景下几乎失效;而 DBSCAN 基于密度、无需预设 K、能发现任意形状的簇,完美适配神经网络表征的几何特性。

2. 训练数据去重与质量过滤

大模型训练数据动辄数万亿 token,其中包含大量重复和低质内容。DBSCAN 在这里扮演关键角色:

  • 语义级去重:先对文本做 embedding,再用 DBSCAN 聚类。同一个高密度簇内的样本高度相似,只保留代表性样本即可大幅减少冗余,同时避免过度删除(相比精确哈希匹配更鲁棒)。
  • 低质/有害数据识别:低质量文本(如乱码、机器生成垃圾)在嵌入空间中往往形成稀疏区域或被标记为噪声点,可以直接过滤。
  • 课程学习排序:根据簇的密度安排训练顺序——先学高密度簇(简单、典型样本),再学低密度区域(困难、边缘样本),已被证明能提升收敛效率。

3. 评估与基准测试构建

  • 自动构建评测集:对海量未标注数据做 DBSCAN 聚类,从每个簇中采样代表性样本,可以自动生成覆盖多样化的评测基准,避免人工标注偏差。
  • 输出多样性评估:对模型生成的多个回答做 embedding + DBSCAN,簇的数量直接反映生成多样性;如果所有回答都落入同一个簇,说明模型陷入了模式崩塌。

4. RAG 系统中的知识组织

在检索增强生成中,DBSCAN 用于对知识库文档块进行语义聚类:

  • 自动发现知识主题,辅助构建层次化索引
  • 检索时先在簇级别粗筛,再在簇内精排,提升召回效率和准确性

⚠️ 重要澄清:容易混淆的概念

概念 是否等于 DBSCAN 说明
Tokenizer 中的 BPE 基于字节对合并,是确定性算法,与密度聚类无关
MoE 的路由机制 通常用 Top-K gating 或负载均衡损失,不是 DBSCAN
向量数据库的 ANN 搜索 HNSW/IVF 等近似最近邻算法,用于检索而非聚类
RLHF 中的奖励模型聚类 ⚠️ 偶尔 少数研究用 DBSCAN 分析偏好数据的分布结构

💡 总结

DBSCAN 在大模型中不参与前向传播和反向传播,但它是理解和治理大模型不可或缺的外部分析工具。随着可解释性研究和数据工程的重要性日益凸显,DBSCAN(及其高维变体如 HDBSCAN)在大模型生态中的地位正在快速上升。如果你从事大模型研究或数据工程,掌握 DBSCAN 在高维嵌入空间中的应用是一项非常有价值的技能。

SciPy 数值优化:从最小二乘到非线性求解,遇到什么问题该用什么

写代码做数据处理,很多问题最终都能归结成一个核心需求:找到一组最优的参数

  • 拟合一条曲线 → 找参数使误差最小
  • 控制一个系统 → 找参数使性能最好
  • 解一个方程组 → 找一组值使所有等式成立

SciPy 的 optimize 模块就是干这个的。但问题是:算法太多了——leastsqcurve_fitfminfmin_bfgsfsolve……新手看了直接懵。


最小二乘拟合:数据点 + 模型 = 找参数

90% 的优化需求都能归到这一类:你有数据点,你有一个带参数的模型,你想让模型尽可能贴合数据。

标准写法(老派):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from scipy.optimize import leastsq
import numpy as np

x = np.array([1, 2, 3, 4, 5])
y = 2.5 * x + 1.3 + np.random.randn(5) * 0.5

def residuals(p, x, y):
k, b = p
return y - (k * x + b)

p0 = [1.0, 0.0]
result = leastsq(residuals, p0, args=(x, y))
k, b = result[0]
print(f"k={k:.3f}, b={b:.3f}")

更友好的写法(推荐):

1
2
3
4
5
6
7
from scipy.optimize import curve_fit

def model(x, k, b):
return k * x + b

popt, pcov = curve_fit(model, x, y, p0=[1.0, 0.0])
k, b = popt

curve_fitleastsq 好用得多——不用自己写残差函数,不用操心 args 传参方式,直接定义模型函数就行。新代码优先用 curve_fit

拟合正弦波(复杂模型示例):

阅读全文 »

NumPy 核心原理:搞懂内存结构和广播,性能问题不用猜

用 NumPy 的人很多,但遇到性能问题或奇怪报错的时候,大多数人是靠试——换个写法试试,不行再换。靠猜也能解决问题,但下次遇到类似问题还是懵。

搞懂 ndarray 的内存结构和广播机制,能让你从”靠猜”变成”靠判断”。

  • 为什么 a[1:3, 2:5] 这种切片几乎不花时间?
  • 为什么修改切片有时候会影响原数组,有时候不影响?
  • 为什么同样的运算,某些写法快几十倍?

答案都在 ndarray 的内存模型里。

ndarray 长什么样?数据 + 说明书

一个 ndarray 对象在内存里就两样东西:

  1. 实际数据:一块连续的内存
  2. 元数据:描述怎么解读这块内存
1
2
3
4
5
6
7
8
┌─────────────────────────────────────────┐
│ ndarray 对象 │
├─────────────────────────────────────────┤
│ data → 数值数据(连续内存) │
│ dtype → 元素类型(int32 / float64) │
│ shape → 各轴长度 (3, 4) │
│ strides → 各轴步长 (16, 4) │
└─────────────────────────────────────────┘

关键认知:数据只存一份。 切片、转置、大部分 reshape 都是只改说明书(元数据),不改数据本身。

dtype:每个元素占多少字节

dtype 告诉你每个元素占几个字节、怎么解释这串字节。

1
2
3
4
import numpy as np

np.dtype('int32').itemsize # 4 字节
np.dtype('float64').itemsize # 8 字节

当你从二进制文件(WAV 音频、BMP 图片)读数据时,dtype 必须跟文件格式匹配,否则读出来的数据完全错乱。这不是”报错”,而是”数据对不上但程序不吭声”,更难排查。

strides:步长,理解 NumPy 性能的钥匙

strides 记录了在某个轴上前进 1 步,内存里要跳过多少字节

1
2
3
4
a = np.array([[1, 2, 3, 4],
[5, 6, 7, 8]], dtype=np.float64)

print(a.strides) # (32, 8)
  • strides[0] = 32:行前进 1 步 = 4 个元素 × 8 字节 = 32 字节
  • strides[1] = 8:列前进 1 步 = 1 个元素 × 8 字节 = 8 字节

切片 a[1:3, 1:3] 几乎不花时间——它只是新建了一个 ndarray 对象,改了 shapestrides数据没动

1
2
b = a[1:3, 1:3]
b.base is a # True —— 共享同一块数据

转置也是改步长:

1
2
c = a.T
c.strides # (8, 32) —— 步长交换了

reshape 不一定”免费”:

1
2
3
4
5
#  免费:数据在内存里是连续的
a.reshape(2, 6)

# 可能触发复制:转置后数据不连续
a.T.reshape(2, 6) # 可能产生副本

因为 a.T 的数据在内存里不连续,没法只靠改元数据来 reshape。

内存布局:C 风格 vs Fortran 风格

1
2
3
4
5
# C 风格(行优先):最后一轴变化最快
c_style = np.arange(12).reshape(3, 4)

# Fortran 风格(列优先):第一轴变化最快
f_style = np.array(c_style, order='F')

调用底层库(特别是 LAPACK 这种 Fortran 写的)时,内存布局不匹配会触发隐式复制,拖慢性能。知道这个,就能在创建数组时直接指定 order,避免后面被动复制。

广播机制:不同形状也能运算

广播允许不同形状的数组做算术运算,不复制数据

1
2
3
4
5
6
a = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
b = np.array([10, 20, 30]) # shape (3,)

c = a + b # 结果 (2, 3)
# b 被"广播"到每一行

广播的两条规则(背下来):

  1. 从最后一个维度开始往前比对
  2. 两个维度要么相等,要么其中一个是 1,要么不存在
1
2
3
4
5
6
7
# 可以广播
a.shape: (2, 3, 4)
b.shape: (3, 4) # 4=4✓, 3=3✓, a 多出一个2 → 广播

# 无法广播
a.shape: (2, 3)
b.shape: (4) # 3≠4 ✗ → ValueError

广播的实际场景

场景1:标准化数据

1
2
3
4
data = np.random.rand(100, 50)
mean = data.mean(axis=0) # shape (50,)
std = data.std(axis=0) # shape (50,)
normalized = (data - mean) / std # 广播自动处理

场景2:外积

1
2
3
x = np.array([1, 2, 3])   # (3,)
y = np.array([4, 5, 6]) # (3,)
outer = x[:, np.newaxis] * y # (3,1) × (3,) → (3,3)

场景3:生成网格(画 3D 图常用)

1
2
3
4
5
X, Y = np.meshgrid(
np.linspace(-1, 1, 100),
np.linspace(-1, 1, 100)
)
Z = np.sin(X**2 + Y**2)

广播 vs 手动扩展:内存差异巨大

1
2
3
4
5
6
7
8
9
a = np.random.rand(10000, 100)
b = np.random.rand(100)

# 广播:不复制数据
result1 = a + b

# 手动扩展:多占了 10000*100*8 字节
b_expanded = np.tile(b, (10000, 1))
result2 = a + b_expanded

两者结果一样,但广播的内存效率高得多。数据量小看不出来,数据量大的时候直接决定程序能不能跑得动。

GRU:从“遗忘”到“记住”,门控循环单元如何重塑序列建模?

传统RNN在长序列面前总“健忘”,GRU用两个门教会了神经网络什么时候该记、什么时候该忘。

一、什么是GRU?

如果你接触过循环神经网络(RNN),一定听说过它的“远亲”LSTM,以及它的“近亲”——GRU。

门控循环单元(GRU)是循环神经网络(RNN)的一种重要变体,由Cho等人于2014年提出。

要理解GRU,得先知道它从哪来。传统RNN有一个致命问题——梯度消失,导致它处理长序列时记不住太久远的信息。为了解决这个问题,LSTM在1997年被发明,它通过复杂的门控机制让RNN终于能“记住”长距离的依赖。

但LSTM的结构太复杂了——三个门、一个额外的细胞状态,参数多、计算慢。于是,Cho等人在LSTM的基础上做了一个“减法”,提出了GRU。它继承了LSTM的核心门控思想,但把结构大幅简化:三个门变成两个,去掉独立的细胞状态,用更少的参数达到相近的性能。

换句话说,GRU可以理解为LSTM的“轻量版”或“精简版”——在效率和效果之间找到了更优的平衡点。

那GRU和传统RNN最大的区别是什么?

想象一下你在读一本小说。传统RNN就像记忆力极差的人,读到第100页时,已经想不起第1页的主角名字了。而GRU则像一个聪明的读者,他会主动判断:哪些信息是重要的,需要记在心里;哪些是无关的,可以直接忽略;当剧情发生转折时,他会清空旧记忆、迎接新情节。

这种“智能记忆”的能力,就来自GRU内部的两个关键结构:

  • 重置门(Reset Gate):决定“忘记多少过去的记忆”。如果上一时刻的信息和当前任务无关,重置门可以把它“清空”,让模型从当前输入重新开始。

  • 更新门(Update Gate):决定“保留多少旧记忆、吸收多少新信息”。当更新门接近1时,模型会一直“记住”很久以前的信息;当它接近0时,模型会完全用新信息替换旧状态。

正是这两个门相互配合,让GRU既不会“健忘”,也不会“被噪音淹没”。


二、为什么需要GRU?它解决了什么问题?

传统RNN的致命缺陷:梯度消失

在GRU诞生之前,传统RNN虽然在语言建模、时间序列预测等领域崭露头角,但始终被一个致命问题困扰——梯度消失(Vanishing Gradient)

阅读全文 »