Python list 与 NumPy 数组:从向量存储到大模型计算的系统理解

一、问题

1.1 由两个问题引发的学习

  1. Python list 与 NumPy 数组在大模型向量存储中有什么差异?
  2. 为什么向量计算优先用 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 数组的优势主要来自:

  1. 数据类型统一:例如全部是 float32
  2. 内存更紧凑:不需要为每个数字保存完整 Python 对象开销。
  3. 内存访问更连续:更适合 CPU 缓存和批量计算。
  4. 底层高性能实现:大量运算在 C、Fortran、BLAS、SIMD 中完成。
  5. 向量化表达:避免 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,通常翻译为“检索增强生成”。

大致流程是:

  1. 把文档切分成多个片段。
  2. 使用 Embedding 模型把每个片段转换成向量。
  3. 把这些向量存入向量库。
  4. 用户提问时,也把问题转换成向量。
  5. 在向量库中检索语义最相近的文档片段。
  6. 把检索结果交给大模型生成答案。

如果有 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 原生容器 listtuple 通用数据组织
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]

它的特点是:

  1. 有序。
  2. 可变。
  3. 可以存放不同类型对象。
  4. 支持索引、切片、追加、删除、遍历。
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)

这段代码没有问题,但当数据规模变大时,性能瓶颈会很明显。

原因包括:

  1. Python 循环本身开销较大。
  2. 每个数字都是 Python 对象。
  3. 每次运算都要进行对象层面的处理。
  4. 数据对象可能分散在内存中,不利于缓存。
  5. 无法天然利用底层向量化计算能力。

所以 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 数组有几个关键特点:

  1. 元素类型统一
  2. 底层数据紧凑存储
  3. 支持 N 维数据结构
  4. 支持向量化运算
  5. 支持广播机制
  6. 大量运算由底层高性能代码执行
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 字节

在向量存储和深度学习中,常见的是 float32float16bfloat16 等数值类型。

NumPy 标准支持 float16float32bfloat16 在深度学习框架中更常见。

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 ndimsize

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 并不是互相替代的关系,而是经常配合使用。

通常流程是:

  1. 用 Python list 临时收集数据。
  2. 数据规模变大或需要数值计算时,转换成 NumPy 数组。
  3. 用 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 和推荐系统中,经常需要计算向量之间的相似度。

常见方式包括:

  1. 点积
  2. 余弦相似度
  3. 欧氏距离

余弦相似度示例:

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。

训练过程大致包括:

  1. 输入数据转换成 token。
  2. token 转换成向量表示。
  3. 模型进行前向计算,得到预测结果。
  4. 计算预测结果和真实目标之间的损失。
  5. 反向传播计算梯度。
  6. 优化器根据梯度更新模型参数。
  7. 重复以上过程很多次。

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 数组相似,但通常额外支持:

  1. GPU 加速。
  2. 自动求导。
  3. 与神经网络模块集成。
  4. 分布式训练支持。

对比:

对比点 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 显存中保存很多内容:

  1. 模型参数。
  2. 梯度。
  3. 优化器状态。
  4. 前向传播中间激活值。
  5. 输入 batch 数据。
  6. 临时计算结果。

模型参数越多、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 或语义检索中,一条数据通常包括:

  1. 原始文本。
  2. 文本切片后的 chunk。
  3. chunk 对应的 embedding 向量。
  4. 文档 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 的场景:

  1. 数据规模较小。
  2. 数据类型不统一。
  3. 主要操作是追加、删除、遍历。
  4. 保存的是业务对象,而不是纯数值。
  5. 需要与 JSON、API 等格式交互。

示例:

users = [
    {"name": "Tom", "age": 18},
    {"name": "Jerry", "age": 20},
]

13.2 什么时候用 NumPy?

适合使用 NumPy 的场景:

  1. 大规模数值数据。
  2. 向量、矩阵、张量计算。
  3. 批量数据处理。
  4. 科学计算。
  5. 图像、音频、信号等数组数据。
  6. Embedding 向量计算。
  7. 机器学习特征处理。

示例:

features = np.array([
    [0.1, 0.2, 0.3],
    [0.4, 0.5, 0.6],
], dtype=np.float32)

13.3 什么时候用 Tensor?

适合使用深度学习 Tensor 的场景:

  1. 需要 GPU 加速。
  2. 需要自动求导。
  3. 需要训练神经网络。
  4. 需要与模型参数、优化器、损失函数结合。
  5. 需要分布式训练或推理。

例如:

# 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 易错点清单

  1. Python list 不是链表,而是动态数组。
  2. list 内部保存的是对象引用,不是紧凑的原始数值。
  3. NumPy 数组通常要求元素类型统一。
  4. NumPy 的快主要来自底层向量化实现,而不是语法更短。
  5. 向量化不是没有循环,而是循环下沉到了 C/Fortran/BLAS 等底层。
  6. NumPy 主要运行在 CPU 上,大模型 GPU 训练主要使用 Tensor。
  7. NumPy 是理解 Tensor 的重要基础,但不等于 PyTorch。
  8. Embedding 向量在 API 中可能是 list,但批量计算时更适合数组或 Tensor。
  9. dtype 会影响内存占用、计算速度和数值精度。
  10. shape 不匹配是数组计算中非常常见的问题。
  11. NumPy 切片可能返回视图,修改视图可能影响原数组。
  12. 对象数组 dtype=object 会失去很多 NumPy 数值计算优势。
  13. 大规模向量存储要关注内存、索引、相似度计算和数据类型。
  14. GPU 显存不仅存模型参数,还要存梯度、优化器状态和中间激活。

14.2 学习这部分知识的意义

学习 list 与 NumPy 的区别,不只是为了知道“哪个更快”,而是为了理解 Python 在通用编程和数值计算之间的边界。

掌握这部分内容后,可以更好地理解:

  • 为什么 Python 原生循环在大规模数值计算中容易慢。
  • 为什么 NumPy 是科学计算和机器学习的基础。
  • 为什么向量计算需要关注 dtype、shape、内存连续性。
  • 为什么 Embedding 向量适合用数组或 Tensor 表示。
  • 为什么大模型训练需要 GPU 和低精度计算。
  • 为什么 NumPy 与 PyTorch Tensor 在概念上高度相似。
  • 为什么 RAG 和向量数据库需要高效的向量存储和相似度计算。

一句话总结:

list 解决的是 Python 通用数据组织问题,NumPy 解决的是高效数值计算问题,而深度学习 Tensor 则进一步解决 GPU 加速、自动求导和模型训练问题。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐