Python list 与 NumPy 数组:从向量存储到大模型计算的系统理解
Python list 与 NumPy 数组:从向量存储到大模型计算的系统理解
一、问题
1.1 由两个问题引发的学习
- Python
list与 NumPy 数组在大模型向量存储中有什么差异? - 为什么向量计算优先用 NumPy?
这两个问题表面上是在比较 list 和 NumPy 数组,实际上背后连接了很多 Python 核心基础与 AI 工程中的重要知识:
- Python
list的底层结构 - NumPy
ndarray的内存模型 - 向量、矩阵与张量
- 同构数据与异构数据
- 连续内存、缓存友好、SIMD、BLAS
- 向量化计算为什么快
- Embedding 向量如何存储和计算
- RAG 向量检索为什么关心存储格式
- 大模型训练中的 CPU、GPU、Tensor、显存
- NumPy 与 PyTorch、TensorFlow、JAX 等深度学习框架的关系
因此,本文不只是为了回答“list 和 NumPy 谁更快”这个问题,而是以它为入口,系统梳理 Python 中列表、数组、向量计算以及大模型训练相关的基础知识。
1.2 简要回答
list 和 NumPy 数组的核心区别可以概括为:
| 对比点 | Python list |
NumPy ndarray |
|---|---|---|
| 数据类型 | 可以混合不同类型对象 | 通常要求同一数组中元素类型一致 |
| 存储方式 | 存储的是对象引用 | 存储紧凑的原始数值数据 |
| 内存连续性 | 元素对象本身通常分散在内存中 | 数值数据通常连续存储 |
| 运算方式 | Python 层循环逐个处理 | 底层 C/Fortran/BLAS 向量化处理 |
| 运算效率 | 大规模数值计算较慢 | 大规模数值计算更快 |
| 内存开销 | 每个元素是 Python 对象,额外开销大 | dtype 固定,内存更紧凑 |
| 适合场景 | 通用数据容器、少量数据、异构对象 | 数值计算、矩阵运算、向量存储、科学计算 |
需要特别纠正一个常见误解:
Python
list不是链表,而是动态数组。它内部保存的是一组连续的对象引用;但这些引用指向的 Python 对象本身可以分散在内存中。
NumPy 数组的优势主要来自:
- 数据类型统一:例如全部是
float32。 - 内存更紧凑:不需要为每个数字保存完整 Python 对象开销。
- 内存访问更连续:更适合 CPU 缓存和批量计算。
- 底层高性能实现:大量运算在 C、Fortran、BLAS、SIMD 中完成。
- 向量化表达:避免 Python 层显式循环。
对于大模型向量存储和向量计算来说,NumPy 通常比原生 list 更适合,因为 Embedding 向量本质上是高维数值数组,而不是普通 Python 对象集合。
二、思考:围绕这个问题还能追问什么?
一个问题真正值得学习,是因为它可以继续展开出一系列更深的问题。
围绕 “Python list 与 NumPy 数组在向量计算中的差异” 可以继续思考:
2.1 Python list 明明也能存数字,为什么不适合大规模数值计算?
例如:
vector = [0.1, 0.2, 0.3, 0.4]
这看起来也是一个向量,但它的每个元素都是 Python 对象。
在 CPython 中,list 内部主要保存的是指向对象的引用,而不是直接紧凑存储的原始浮点数。
也就是说,一个列表大致可以理解为:
list 对象
├── 引用 -> float 对象 0.1
├── 引用 -> float 对象 0.2
├── 引用 -> float 对象 0.3
└── 引用 -> float 对象 0.4
而 NumPy 数组更接近:
ndarray 对象
└── 连续数值内存: [0.1, 0.2, 0.3, 0.4]
这导致两者在内存占用、缓存命中、批量计算效率上有明显差异。
2.2 NumPy 数组为什么要求元素类型统一?
因为 NumPy 主要面向数值计算。
当一个数组中的元素类型统一时,底层就可以用固定字节宽度连续存储数据。
例如:
import numpy as np
arr = np.array([1, 2, 3], dtype=np.int32)
print(arr.dtype) # int32
int32 表示每个元素占 4 字节。
这样 NumPy 可以很清楚地知道:
- 每个元素占多少字节。
- 下一个元素在哪里。
- 如何批量进行加法、乘法、矩阵运算。
如果每个元素类型都不一样,底层就很难进行统一的高性能批量计算。
2.3 为什么向量计算要避免 Python 层循环?
Python 层循环的开销比较大。
例如:
result = []
for x in data:
result.append(x * 2)
每次循环都涉及:
- 取出 Python 对象
- 判断对象类型
- 执行 Python 层操作
- 创建或引用新对象
- 追加到列表中
而 NumPy 的向量化计算:
result = arr * 2
表面上只是一行代码,底层会把循环交给 C 层执行,减少 Python 解释器的参与。
这就是 NumPy 快的重要原因之一:
不是没有循环,而是循环从 Python 层下沉到了更高效的底层实现中。
2.4 为什么大模型里到处都是“向量”?
大模型处理文本时,并不是直接处理汉字、英文单词或句子本身,而是把它们转换成数字表示。
例如,一个词、一个 token、一句话或一段文本,都可以被表示成向量。
"Python" -> [0.12, -0.03, 0.88, ..., 0.25]
这种向量通常叫做 Embedding 向量。
向量的意义是:
- 把文本变成机器可以计算的数字。
- 把语义相近的内容映射到向量空间中较近的位置。
- 让相似度计算、聚类、检索、分类成为可能。
2.5 RAG 为什么需要存储大量向量?
RAG 是 Retrieval-Augmented Generation,通常翻译为“检索增强生成”。
大致流程是:
- 把文档切分成多个片段。
- 使用 Embedding 模型把每个片段转换成向量。
- 把这些向量存入向量库。
- 用户提问时,也把问题转换成向量。
- 在向量库中检索语义最相近的文档片段。
- 把检索结果交给大模型生成答案。
如果有 100 万个文档片段,每个向量 1536 维,那么就需要存储:
1,000,000 × 1536 个浮点数
如果使用 float32,每个浮点数 4 字节,那么仅向量原始数据就大约是:
1,000,000 × 1536 × 4 bytes ≈ 6.1 GB
这还不包括索引结构、元数据、数据库开销等。
因此,向量存储天然关心:
- 数据类型
- 内存占用
- 批量计算速度
- 相似度计算效率
- 与向量库和深度学习框架的兼容性
2.6 NumPy 和大模型 GPU 训练是什么关系?
NumPy 本身主要运行在 CPU 上,不直接负责大模型 GPU 训练。
大模型训练通常使用:
- PyTorch Tensor
- TensorFlow Tensor
- JAX Array
- GPU / TPU 加速计算
但 NumPy 依然非常重要,因为它是理解张量计算的基础。
可以这样理解:
| 层次 | 代表工具 | 主要作用 |
|---|---|---|
| Python 原生容器 | list、tuple |
通用数据组织 |
| CPU 数值计算 | NumPy ndarray |
高效数组与矩阵计算 |
| GPU 深度学习计算 | PyTorch / TensorFlow / JAX Tensor | 自动求导、GPU 加速、模型训练 |
NumPy 不等于深度学习框架,但它提供了理解数组、向量、矩阵、广播、shape、dtype 等概念的基础。
三、Python list 基础与底层特点
3.1 list 是什么?
list 是 Python 中最常用的可变序列容器。
items = [1, "hello", 3.14, True]
它的特点是:
- 有序。
- 可变。
- 可以存放不同类型对象。
- 支持索引、切片、追加、删除、遍历。
nums = [1, 2, 3]
nums.append(4)
nums[0] = 100
print(nums) # [100, 2, 3, 4]
3.2 list 不是链表,而是动态数组
很多资料会把 Python list 误说成“链表”,这是不准确的。
在 CPython 中,list 更接近动态数组:
- 列表内部维护一块连续空间。
- 这块空间存放的是对象引用。
- 当容量不够时,会扩容并搬迁引用数组。
简化理解:
list 内部引用数组:
[ref1, ref2, ref3, ref4]
↓ ↓ ↓ ↓
obj1 obj2 obj3 obj4
因此:
- 按索引访问很快。
- 尾部追加通常很快。
- 中间插入或删除可能需要移动大量引用。
3.3 list 为什么灵活?
因为列表保存的是对象引用,所以它可以保存任意类型对象。
data = [1, "Python", [1, 2], {"name": "Tom"}]
这种灵活性非常适合普通业务开发,例如:
- 保存一组用户对象。
- 保存不同类型配置项。
- 保存接口返回的复杂结构。
- 临时组织数据。
但这种灵活性也意味着它不适合极致数值计算。
3.4 list 做数值计算的问题
假设要让一个列表中所有数字乘以 2:
nums = [1, 2, 3, 4]
result = []
for num in nums:
result.append(num * 2)
print(result)
这段代码没有问题,但当数据规模变大时,性能瓶颈会很明显。
原因包括:
- Python 循环本身开销较大。
- 每个数字都是 Python 对象。
- 每次运算都要进行对象层面的处理。
- 数据对象可能分散在内存中,不利于缓存。
- 无法天然利用底层向量化计算能力。
所以 list 适合通用容器,不适合大规模数值数组计算。
四、NumPy 数组基础
4.1 NumPy 是什么?
NumPy 是 Python 中最重要的数值计算基础库之一。
它提供了核心数据结构:ndarray。
import numpy as np
arr = np.array([1, 2, 3])
print(arr)
print(type(arr))
ndarray 可以理解为 N 维数组。
- 1 维数组:向量
- 2 维数组:矩阵
- 3 维及以上数组:更高维张量
4.2 ndarray 的核心特点
NumPy 数组有几个关键特点:
- 元素类型统一。
- 底层数据紧凑存储。
- 支持 N 维数据结构。
- 支持向量化运算。
- 支持广播机制。
- 大量运算由底层高性能代码执行。
arr = np.array([1, 2, 3], dtype=np.float32)
print(arr.dtype) # float32
print(arr.shape) # (3,)
print(arr.ndim) # 1
print(arr.size) # 3
4.3 dtype:数据类型
dtype 表示数组中元素的数据类型。
arr1 = np.array([1, 2, 3], dtype=np.int32)
arr2 = np.array([1, 2, 3], dtype=np.float32)
print(arr1.dtype)
print(arr2.dtype)
常见 dtype:
| dtype | 含义 | 每个元素大小 |
|---|---|---|
int32 |
32 位整数 | 4 字节 |
int64 |
64 位整数 | 8 字节 |
float32 |
32 位浮点数 | 4 字节 |
float64 |
64 位浮点数 | 8 字节 |
float16 |
16 位浮点数 | 2 字节 |
bool |
布尔值 | 通常 1 字节 |
在向量存储和深度学习中,常见的是 float32、float16、bfloat16 等数值类型。
NumPy 标准支持 float16 和 float32;bfloat16 在深度学习框架中更常见。
4.4 shape:数组形状
shape 表示数组每个维度的大小。
arr = np.array([
[1, 2, 3],
[4, 5, 6],
])
print(arr.shape) # (2, 3)
(2, 3) 表示 2 行 3 列。
在大模型中,shape 非常重要。例如:
(batch_size, sequence_length, hidden_size)
可能表示:
- 一批样本数量
- 每个样本的 token 数量
- 每个 token 的向量维度
4.5 ndim 与 size
arr = np.array([
[1, 2, 3],
[4, 5, 6],
])
print(arr.ndim) # 2
print(arr.size) # 6
ndim:维度数量。size:元素总数。
4.6 NumPy 数组的内存更紧凑
例如,一个 float32 数组中,每个元素只占 4 字节。
arr = np.array([1.0, 2.0, 3.0], dtype=np.float32)
print(arr.nbytes) # 12
nbytes 表示数组底层数据区占用的字节数。
这还不包括 ndarray 对象本身的元信息,但对大规模数值数据来说,主要占用来自数据区。
五、list 与 NumPy 数组的关系
5.1 NumPy 数组经常由 list 创建
最常见的方式就是用 list 创建 NumPy 数组。
import numpy as np
lst = [1, 2, 3]
arr = np.array(lst)
print(arr)
print(type(arr))
这说明 list 和 NumPy 并不是互相替代的关系,而是经常配合使用。
通常流程是:
- 用 Python
list临时收集数据。 - 数据规模变大或需要数值计算时,转换成 NumPy 数组。
- 用 NumPy 进行批量计算、矩阵运算或保存。
5.2 list 更像通用容器,NumPy 更像数值计算容器
| 场景 | 更适合使用 |
|---|---|
| 保存少量通用对象 | list |
| 保存不同类型数据 | list |
| 动态追加复杂对象 | list |
| 大规模数值计算 | NumPy |
| 矩阵运算 | NumPy |
| 向量相似度计算 | NumPy |
| 图像、音频、科学计算数据 | NumPy |
| 与深度学习张量转换 | NumPy 或 Tensor |
5.3 list 转 NumPy 数组
lst = [1, 2, 3]
arr = np.array(lst)
print(arr)
也可以指定 dtype:
arr = np.array(lst, dtype=np.float32)
print(arr.dtype)
5.4 NumPy 数组转 list
arr = np.array([1, 2, 3])
lst = arr.tolist()
print(lst)
print(type(lst))
在实际业务中,经常需要在 NumPy、JSON、数据库、API 之间转换数据格式。
例如很多 JSON 接口不能直接序列化 NumPy 数组,就需要先转成 list。
5.5 混合类型 list 转数组时会发生什么?
arr = np.array([1, 2.5, 3])
print(arr)
print(arr.dtype)
NumPy 会尝试找到一个统一类型,例如把整数提升为浮点数。
如果混合了数字和字符串:
arr = np.array([1, "hello", 3])
print(arr)
print(arr.dtype)
NumPy 可能会把它们统一成字符串类型。
如果使用 dtype=object,NumPy 数组可以保存 Python 对象:
arr = np.array([1, "hello", [1, 2]], dtype=object)
但这种对象数组会失去很多数值计算优势,一般不用于高性能数值计算。
六、向量、矩阵、张量:理解大模型计算的基础
6.1 什么是标量、向量、矩阵、张量?
| 概念 | 直观理解 | 示例 |
|---|---|---|
| 标量 | 一个数 | 3.14 |
| 向量 | 一组数 | [0.1, 0.2, 0.3] |
| 矩阵 | 二维表格 | [[1, 2], [3, 4]] |
| 张量 | 更高维数组 | shape 为 (2, 3, 4) 的数组 |
NumPy 可以表示这些结构:
scalar = np.array(3.14)
vector = np.array([1, 2, 3])
matrix = np.array([[1, 2], [3, 4]])
tensor = np.zeros((2, 3, 4))
print(vector.shape) # (3,)
print(matrix.shape) # (2, 2)
print(tensor.shape) # (2, 3, 4)
6.2 Embedding 向量是什么?
Embedding 是把离散对象转换成连续向量的表示方式。
离散对象可以是:
- 字
- 词
- token
- 句子
- 文档片段
- 图片
- 用户
- 商品
例如:
"机器学习" -> [0.12, -0.25, 0.88, ..., 0.31]
这个向量不是随便生成的,而是模型根据大量数据学习出来的语义表示。
语义相近的文本,向量距离通常更近。
6.3 向量相似度计算
在 RAG 和推荐系统中,经常需要计算向量之间的相似度。
常见方式包括:
- 点积
- 余弦相似度
- 欧氏距离
余弦相似度示例:
import numpy as np
a = np.array([1, 2, 3], dtype=np.float32)
b = np.array([2, 3, 4], dtype=np.float32)
cosine = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
print(cosine)
如果使用 list,也能算,但需要手动循环:
a = [1, 2, 3]
b = [2, 3, 4]
dot = sum(x * y for x, y in zip(a, b))
数据量一大,NumPy 的优势就会非常明显。
6.4 批量向量计算
假设有多个向量组成一个矩阵:
vectors = np.array([
[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6],
[0.7, 0.8, 0.9],
], dtype=np.float32)
query = np.array([0.1, 0.2, 0.3], dtype=np.float32)
scores = vectors @ query
print(scores)
vectors @ query 表示矩阵与向量相乘,可以一次性得到 query 与多个向量的点积。
这种批量计算就是 NumPy 的优势场景。
七、为什么向量计算优先用 NumPy?
7.1 向量化计算
NumPy 支持直接对整个数组做运算。
arr = np.array([1, 2, 3])
print(arr * 2) # [2 4 6]
如果用 list:
lst = [1, 2, 3]
print([x * 2 for x in lst])
两者看起来都不复杂,但 NumPy 在大规模数据下更快,因为循环发生在底层高性能代码中。
7.2 避免 Python 解释器循环开销
Python 是解释型语言,普通循环每次迭代都有解释器开销。
NumPy 的很多操作会把循环交给 C 层执行。
arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])
result = arr1 + arr2
print(result) # [5 7 9]
这个加法不是 Python 一个元素一个元素解释执行,而是由 NumPy 底层完成。
7.3 连续内存与 CPU 缓存友好
现代 CPU 访问连续内存通常更高效。
NumPy 数组中的数值数据通常连续存放,因此更容易利用 CPU 缓存。
而 Python list 存的是对象引用,引用指向的对象可能分散在内存中。
这会带来更多间接访问和缓存不命中的可能。
7.4 底层库优化
NumPy 的很多线性代数运算会调用高度优化的底层库,例如 BLAS、LAPACK 等。
例如矩阵乘法:
A = np.random.rand(1000, 1000)
B = np.random.rand(1000, 1000)
C = A @ B
矩阵乘法背后可能会使用经过多年优化的底层数学库,而不是普通 Python 循环。
7.5 广播机制
广播可以让不同形状的数组在满足规则时自动扩展参与运算。
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
])
bias = np.array([10, 20, 30])
print(matrix + bias)
输出:
[[11 22 33]
[14 25 36]]
这里 bias 会被广播到每一行。
广播机制在机器学习中非常常见,例如给一批样本加偏置项。
八、内存占用:为什么 NumPy 更适合大规模向量存储?
8.1 Python 数字对象有额外开销
在 Python list 中,每个浮点数都是一个 Python 对象。
一个 Python float 不只是 8 字节的浮点数本身,还包含对象头、引用计数、类型指针等额外信息。
列表本身还要保存一组对象引用。
所以:
lst = [1.0, 2.0, 3.0]
它的实际内存开销远大于 3 个浮点数本身。
8.2 NumPy 按 dtype 紧凑存储
NumPy 数组如果使用 float32:
arr = np.array([1.0, 2.0, 3.0], dtype=np.float32)
print(arr.nbytes) # 12
每个元素就是 4 字节,数据区非常紧凑。
8.3 向量存储中的内存估算
假设有 100 万个向量,每个向量 768 维。
如果使用 float32:
1,000,000 × 768 × 4 bytes ≈ 3.07 GB
如果使用 float16:
1,000,000 × 768 × 2 bytes ≈ 1.54 GB
如果用 Python list 存这些浮点数,额外对象开销会大得多,不适合大规模向量存储。
8.4 dtype 选择会影响精度和内存
| dtype | 优点 | 缺点 | 常见场景 |
|---|---|---|---|
float64 |
精度高 | 内存大、计算更重 | 科学计算、高精度计算 |
float32 |
精度和内存较平衡 | 比 float64 精度低 | 机器学习、向量存储常见 |
float16 |
内存更小、带宽压力低 | 精度更低 | 深度学习、GPU 计算、向量压缩 |
在向量检索中,很多场景会使用 float32;在更追求存储效率时,也可能使用 float16、量化向量或专门的压缩索引。
九、大模型 GPU 训练是什么?
9.1 大模型训练在做什么?
大模型训练可以简单理解为:
用大量数据不断调整模型参数,使模型输出越来越接近目标结果。
例如语言模型训练中,模型会根据前面的 token 预测下一个 token。
训练过程大致包括:
- 输入数据转换成 token。
- token 转换成向量表示。
- 模型进行前向计算,得到预测结果。
- 计算预测结果和真实目标之间的损失。
- 反向传播计算梯度。
- 优化器根据梯度更新模型参数。
- 重复以上过程很多次。
9.2 为什么需要 GPU?
大模型训练中最核心的计算是大量矩阵乘法和张量运算。
GPU 擅长并行计算,尤其适合处理大量相同类型的数值运算。
CPU 更像少数几个强大的工人,适合复杂控制逻辑;GPU 更像大量相对简单的工人,适合并行做重复计算。
大模型中常见计算:
矩阵 × 矩阵
矩阵 × 向量
批量张量运算
注意力机制中的 QK^T
前馈网络中的线性层
这些都非常适合 GPU 加速。
9.3 GPU 训练中用的是 NumPy 吗?
严格来说,大模型 GPU 训练主要使用深度学习框架中的 Tensor,而不是 NumPy 数组。
例如 PyTorch:
import torch
x = torch.tensor([1.0, 2.0, 3.0], device="cuda")
print(x)
这个 x 是 PyTorch Tensor,可以放在 GPU 上。
NumPy 数组通常在 CPU 内存中,不能直接在 GPU 上训练大模型。
但 NumPy 和 Tensor 很像,都有:
- shape
- dtype
- 多维数组结构
- 广播
- 向量化运算
- 矩阵计算
所以学习 NumPy 是理解深度学习 Tensor 的重要基础。
9.4 Tensor 是什么?
Tensor 可以理解为多维数组,是深度学习框架中的核心数据结构。
它和 NumPy 数组相似,但通常额外支持:
- GPU 加速。
- 自动求导。
- 与神经网络模块集成。
- 分布式训练支持。
对比:
| 对比点 | NumPy ndarray |
深度学习 Tensor |
|---|---|---|
| 主要运行位置 | CPU | CPU / GPU / TPU |
| 自动求导 | 不支持 | 支持 |
| 适合场景 | 通用数值计算 | 模型训练与推理 |
| 典型库 | NumPy | PyTorch、TensorFlow、JAX |
9.5 大模型训练中的 dtype
大模型训练常用低精度数据类型来降低显存占用、提高吞吐。
常见类型:
| 类型 | 含义 | 特点 |
|---|---|---|
| FP32 | 32 位浮点 | 精度较高,显存占用大 |
| FP16 | 16 位浮点 | 显存更省,速度更快,但数值范围较小 |
| BF16 | bfloat16 | 范围接近 FP32,精度低于 FP32,训练中常见 |
| INT8 / INT4 | 整数量化 | 常用于推理压缩和加速 |
这和 NumPy 中的 dtype 思想是一脉相承的:
dtype 决定每个数如何存储、占多少内存、计算精度如何。
9.6 显存为什么容易不够?
大模型训练需要在 GPU 显存中保存很多内容:
- 模型参数。
- 梯度。
- 优化器状态。
- 前向传播中间激活值。
- 输入 batch 数据。
- 临时计算结果。
模型参数越多、batch size 越大、序列长度越长,显存压力越大。
这也是为什么大模型训练会使用:
- 混合精度训练
- 梯度检查点
- 数据并行
- 张量并行
- 流水线并行
- ZeRO 优化
- 参数量化
这些技术本质上都是在计算效率、显存占用和训练稳定性之间做平衡。
十、NumPy 与 PyTorch、TensorFlow 的关系
10.1 NumPy 是科学计算基础
NumPy 是 Python 数值计算生态的基础之一。
很多库都围绕 NumPy 构建,例如:
- pandas
- SciPy
- scikit-learn
- Matplotlib
- OpenCV 的部分 Python 接口
NumPy 的数组思想也深刻影响了深度学习框架。
10.2 PyTorch Tensor 与 NumPy 数组互转
在 CPU 上,PyTorch Tensor 和 NumPy 数组可以方便互转。
import numpy as np
import torch
arr = np.array([1, 2, 3], dtype=np.float32)
tensor = torch.from_numpy(arr)
print(tensor)
Tensor 转 NumPy:
arr2 = tensor.numpy()
print(arr2)
需要注意:在 PyTorch 中,GPU Tensor 不能直接转 NumPy,通常需要先转回 CPU。
# arr = gpu_tensor.cpu().numpy()
10.3 数据准备阶段经常会用 NumPy
即使模型训练本身使用 Tensor,数据准备阶段仍然可能用到 NumPy:
- 读取数值数据
- 数据清洗
- 特征归一化
- 图像数组处理
- 音频数组处理
- 向量相似度预处理
- 保存或加载
.npy文件
所以 NumPy 是连接 Python 数据处理和深度学习训练的重要桥梁。
十一、Embedding 向量存储与 RAG 场景
11.1 Embedding 向量通常怎么存?
在 RAG 或语义检索中,一条数据通常包括:
- 原始文本。
- 文本切片后的 chunk。
- chunk 对应的 embedding 向量。
- 文档 ID、来源、页码等元数据。
简单示例:
record = {
"id": "doc_001_chunk_001",
"text": "Python 是一种动态类型语言",
"embedding": [0.12, -0.03, 0.25],
"metadata": {"source": "python.md"},
}
这里 embedding 在接口或 JSON 中可能表现为 list,但在计算时通常会转换成 NumPy 数组或 Tensor。
11.2 为什么向量库喜欢数组格式?
向量库需要做大量相似度计算,例如:
- 查找最相近的 Top K 向量。
- 构建 ANN 近似最近邻索引。
- 批量计算距离。
- 使用压缩或量化降低存储成本。
这些操作都要求数据具有稳定的数值类型和规整的维度。
NumPy 数组天然适合表示:
(num_vectors, embedding_dim)
例如:
embeddings = np.array([
[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6],
[0.7, 0.8, 0.9],
], dtype=np.float32)
print(embeddings.shape) # (3, 3)
11.3 list 在向量存储中完全不能用吗?
不是。
list 仍然常见于:
- API 请求和响应。
- JSON 序列化。
- 少量向量临时表示。
- 配置或调试阶段。
- 与某些向量数据库 SDK 交互。
例如很多 Embedding API 返回的向量就是普通 list。
但是当需要大量计算、批量处理、矩阵运算和高效存储时,应该转换成 NumPy 数组或深度学习 Tensor。
11.4 相似度检索的简单例子
import numpy as np
# 文档向量,shape = (3, 4)
docs = np.array([
[0.1, 0.2, 0.3, 0.4],
[0.2, 0.1, 0.0, 0.5],
[0.9, 0.8, 0.7, 0.6],
], dtype=np.float32)
# 查询向量,shape = (4,)
query = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32)
# 点积相似度
scores = docs @ query
print(scores)
print(scores.argmax()) # 最相似文档下标
如果要计算余弦相似度,需要先归一化:
docs_norm = docs / np.linalg.norm(docs, axis=1, keepdims=True)
query_norm = query / np.linalg.norm(query)
scores = docs_norm @ query_norm
print(scores)
十二、NumPy 的几个关键机制
12.1 广播机制
广播允许不同形状的数组进行运算。
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
])
bias = np.array([10, 20, 30])
print(matrix + bias)
bias 会自动扩展到每一行。
广播在机器学习中非常常见,例如:
- 给每个样本加同一个 bias。
- 对每个特征做归一化。
- 对 batch 数据做统一变换。
12.2 shape 对齐
很多 NumPy 错误都来自 shape 不匹配。
a = np.array([1, 2, 3])
b = np.array([1, 2])
# ValueError: operands could not be broadcast together
# print(a + b)
做数组运算前,应该先确认:
print(a.shape)
print(b.shape)
12.3 视图与拷贝
NumPy 中有些操作返回视图,有些操作返回拷贝。
视图共享原数组数据:
arr = np.array([1, 2, 3, 4])
view = arr[1:3]
view[0] = 100
print(arr) # [ 1 100 3 4]
如果希望复制数据,可以使用 copy()。
arr = np.array([1, 2, 3, 4])
copied = arr[1:3].copy()
copied[0] = 100
print(arr) # [1 2 3 4]
print(copied) # [100 3]
这和 Python 中“引用共享、浅拷贝、深拷贝”的思想有相通之处。
12.4 reshape
reshape 可以改变数组形状。
arr = np.array([1, 2, 3, 4, 5, 6])
reshaped = arr.reshape(2, 3)
print(reshaped)
前提是元素总数不变。
# 6 个元素不能 reshape 成 (4, 2)
# arr.reshape(4, 2)
12.5 axis
axis 表示沿哪个维度操作。
arr = np.array([
[1, 2, 3],
[4, 5, 6],
])
print(arr.sum(axis=0)) # 按列求和:[5 7 9]
print(arr.sum(axis=1)) # 按行求和:[6 15]
理解 axis 对学习 NumPy、pandas、深度学习 Tensor 都很重要。
十三、常见实践建议
13.1 什么时候用 list?
适合使用 list 的场景:
- 数据规模较小。
- 数据类型不统一。
- 主要操作是追加、删除、遍历。
- 保存的是业务对象,而不是纯数值。
- 需要与 JSON、API 等格式交互。
示例:
users = [
{"name": "Tom", "age": 18},
{"name": "Jerry", "age": 20},
]
13.2 什么时候用 NumPy?
适合使用 NumPy 的场景:
- 大规模数值数据。
- 向量、矩阵、张量计算。
- 批量数据处理。
- 科学计算。
- 图像、音频、信号等数组数据。
- Embedding 向量计算。
- 机器学习特征处理。
示例:
features = np.array([
[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6],
], dtype=np.float32)
13.3 什么时候用 Tensor?
适合使用深度学习 Tensor 的场景:
- 需要 GPU 加速。
- 需要自动求导。
- 需要训练神经网络。
- 需要与模型参数、优化器、损失函数结合。
- 需要分布式训练或推理。
例如:
# PyTorch 示例
# x = torch.tensor([[1.0, 2.0]], device="cuda")
简单判断:
| 需求 | 推荐 |
|---|---|
| 通用数据容器 | list |
| CPU 数值计算 | NumPy |
| GPU 训练/推理 | Tensor |
| JSON 传输 | list |
| 大规模向量矩阵计算 | NumPy / Tensor |
13.4 不要为了 NumPy 而 NumPy
如果只是处理几个数字:
scores = [90, 85, 100]
print(sum(scores) / len(scores))
完全没有必要强行使用 NumPy。
NumPy 的优势通常在数据规模较大、运算较密集、结构较规整时才明显。
十四、常见易错点与知识总结
14.1 易错点清单
- Python
list不是链表,而是动态数组。 list内部保存的是对象引用,不是紧凑的原始数值。- NumPy 数组通常要求元素类型统一。
- NumPy 的快主要来自底层向量化实现,而不是语法更短。
- 向量化不是没有循环,而是循环下沉到了 C/Fortran/BLAS 等底层。
- NumPy 主要运行在 CPU 上,大模型 GPU 训练主要使用 Tensor。
- NumPy 是理解 Tensor 的重要基础,但不等于 PyTorch。
- Embedding 向量在 API 中可能是 list,但批量计算时更适合数组或 Tensor。
- dtype 会影响内存占用、计算速度和数值精度。
- shape 不匹配是数组计算中非常常见的问题。
- NumPy 切片可能返回视图,修改视图可能影响原数组。
- 对象数组
dtype=object会失去很多 NumPy 数值计算优势。 - 大规模向量存储要关注内存、索引、相似度计算和数据类型。
- GPU 显存不仅存模型参数,还要存梯度、优化器状态和中间激活。
14.2 学习这部分知识的意义
学习 list 与 NumPy 的区别,不只是为了知道“哪个更快”,而是为了理解 Python 在通用编程和数值计算之间的边界。
掌握这部分内容后,可以更好地理解:
- 为什么 Python 原生循环在大规模数值计算中容易慢。
- 为什么 NumPy 是科学计算和机器学习的基础。
- 为什么向量计算需要关注 dtype、shape、内存连续性。
- 为什么 Embedding 向量适合用数组或 Tensor 表示。
- 为什么大模型训练需要 GPU 和低精度计算。
- 为什么 NumPy 与 PyTorch Tensor 在概念上高度相似。
- 为什么 RAG 和向量数据库需要高效的向量存储和相似度计算。
一句话总结:
list解决的是 Python 通用数据组织问题,NumPy 解决的是高效数值计算问题,而深度学习 Tensor 则进一步解决 GPU 加速、自动求导和模型训练问题。
更多推荐



所有评论(0)