Skip to content

第 18 章 · 数组的创建与形状 ​

这一章要解决什么:熟练造出各种形状的数组,并能在不改变数据的前提下变换形状。 打掉一个高频错误:形状参数必须是元组 —— np.zeros(2, 3) 会报错,np.zeros((2, 3)) 才对。

本章你需要完成的代码:demos/ch18-array-create.py

18.1 arange:起点、终点(不含)、步长 ​

python
import numpy as np

print(np.arange(5))              # [0 1 2 3 4]
print(np.arange(1, 6))           # [1 2 3 4 5]
print(np.arange(0, 10, 2))       # [0 2 4 6 8]
print(np.arange(5, 0, -1))       # [5 4 3 2 1]

和 range() 的规则完全一样,也是左闭右开 —— 终点取不到。 这是本课程第三次强调"左闭右开"了(列表切片、range、现在是 arange), 因为它们背后是同一套索引规则。

python
print(np.arange(0, 1, 0.25))     # [0.   0.25 0.5  0.75]  ← 注意没有 1.0
点开:为什么小数步长有风险?

浮点数不能精确表示所有小数,所以 arange 用小数步长时可能出现 "多一个"或"少一个"元素的情况:

python
print(len(np.arange(0, 1, 0.1)))     # 10
print(np.arange(0, 1, 0.1)[-1])      # 0.9999999999999999  ← 不是 1.0

按"每 0.1 取一个、到 1 之前停"的直觉,应该是 10 个(0, 0.1, ..., 0.9), 结果确实对。但换成 arange(0, 1.2, 0.1) 就可能出现尾部误差累积, 导致元素个数与你的心算不符。

结论:凡是要"几个点"而不是"每隔多少取一个",都用 linspace。

python
print(np.linspace(0, 1, 11))          # 明确 11 个点,一定含两端

18.2 linspace:要"几个点"而不是"步长" ​

python
axis = np.linspace(0.80, 1.00, 21)
print(len(axis))          # 21
print(axis[0], axis[-1])  # 0.8 1.0    ← 含两端
print(axis[:5])           # [0.8  0.81 0.82 0.83 0.84]

签名是 linspace(起点, 终点, 点的个数) —— 第三个参数是个数,不是步长。

"lin"是 linear(线性)的意思:在起点和终点之间等距插入。

含两端是它和 arange 最大的行为差异:

arange(0.8, 1.0, 0.05)linspace(0.8, 1.0, 5)
第三个参数步长点的个数
含终点吗❌ 不含✅ 含
会受浮点误差影响吗会不会(按个数精确划分)
结果[0.8 0.85 0.9 0.95][0.8 0.85 0.9 0.95 1.0]

典型用途:

python
# 画图的坐标轴刻度
np.linspace(0, 1500, 6).astype(int)      # [0 300 600 900 1200 1500]

# 分箱边界(第 33 章 IQR 分箱)
np.linspace(38, 90, 11)                  # 内阻从 38 到 90 分 10 段

# 阈值扫描(第 32 章找最佳异常阈值)
np.linspace(2.0, 4.0, 21)                # 试 21 个 z 阈值

18.3 批量填充:zeros / ones / full / eye / empty ​

python
print(np.zeros(3))                  # [0. 0. 0.]
print(np.ones((2, 2)))              # 2×2 全 1
print(np.full((2, 3), 2.5))         # 2×3 全是 2.5(额定容量)
print(np.eye(3))                    # 3×3 单位阵
函数作用常见用途
zeros(shape)全 0占位、累加器初值、掩码
ones(shape)全 1权重初值、计数基准
full(shape, value)全是某个值填额定容量、填默认阈值
eye(n)n×n 单位阵线性代数、one-hot 的雏形
empty(shape)不初始化(内容是内存残留)马上要覆盖全部元素时(快一点)
点开:empty 为什么危险又为什么有用?
python
print(np.empty(5))       # [1.23e-312 6.95e-310 ...]  ← 完全随机的垃圾值

np.empty 不写任何数据,只是"要一块内存"。你看到的是那块内存里残留的旧数据。

为什么会有这种函数:写数据到内存需要时间。如果你马上就要覆盖全部元素, 先清零是白费功夫:

python
# 慢:先清零,再全部覆写
buf = np.zeros(1_000_000)
buf[:] = np.arange(1_000_000)

# 快:跳过清零
buf = np.empty(1_000_000)
buf[:] = np.arange(1_000_000)

危险之处:如果你忘了覆盖某一部分,那里就是随机数,而且不报错。

实践建议:

  • 除非性能敏感且你确信会全覆盖,否则一律用 zeros;
  • 看到陌生的 empty 出现在别人代码里,去检查它有没有被完整填充。

18.4 ⚠ 形状参数必须是元组 ​

这是本章的第一个高频错误:

python
np.zeros(2, 3)
# TypeError: Cannot interpret '3' as a data type

为什么:np.zeros 的签名是 zeros(shape, dtype=float, order='C')。 你写的 3 被当成了第二个位置参数 dtype,而"3"不是一个合法的数据类型。

python
np.zeros((2, 3))        # ✅ 多一层括号,把形状包成元组

同类错误(记住"形状是元组"这一条):

python
np.ones(5, 5)               # ❌ 应该 np.ones((5, 5))
arr.reshape(3, 4)           # ✅ reshape 例外!它接受分开的参数
np.full(2, 3, 0)            # ❌ 应该 np.full((2, 3), 0)

为什么 reshape 例外:它的签名是 reshape(shape) 或 reshape(*shape), 两种写法都支持。但为了统一,本书建议一律写元组形式:arr.reshape((3, 4)) 或 arr.reshape(3, 4) 都行。

18.5 reshape:换一种看法,不改数据 ​

python
flat = np.arange(12)
print(flat.shape)               # (12,)

m = flat.reshape(3, 4)
print(m.shape)                  # (3, 4)
print(m)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

核心认知:reshape 不复制数据。

它返回的是一个视图 —— "同一块内存的另一种看法"。 12 个数字还在原地,只是现在按 3 行 4 列来解读它们。

这意味着:

python
flat = np.arange(12)
m = flat.reshape(3, 4)
m[0, 0] = 99
print(flat[0])                  # 99  ← 原数组也变了!

为什么这样设计:重新排布一亿个元素要花时间和内存。 如果只是"换个解读方式",就没必要动数据。这是第 19 章"视图 vs 拷贝"的第一次出现。

-1:让 NumPy 帮你算 ​

python
print(flat.reshape(2, -1).shape)     # (2, 6)   12/2 = 6
print(flat.reshape(-1, 6).shape)     # (2, 6)   12/6 = 2

-1 是通配符:"这一维你自己算,从总数推出来。"

实用场景:处理一批图像时,你知道是 (N, 28, 28),想展平成 (N, 784):

python
images = np.zeros((100, 28, 28))
flat_images = images.reshape(len(images), -1)     # (100, 784)
#                                    ↑ 用 len() 而不是硬编码 100

这样换一批数据(比如 500 张)时,代码不用改。

规则:-1 只能出现一次。reshape(-1, -1) 会报错,因为信息不足。

元素总数必须守恒 ​

python
flat.reshape(5, 5)
# ValueError: cannot reshape array of size 12 into shape (5,5)

12 个元素摆不成 5×5(需要 25 个)。报错信息里会告诉你实际大小和目标形状 —— 看到 cannot reshape array of size 12 into shape (5,5) 就去检查:5 × 5 = 25 ≠ 12。

18.6 转置、展平、增减维度 ​

python
X = np.arange(1, 7).reshape(2, 3)
print(X.shape)          # (2, 3)
print(X.T.shape)        # (3, 2)   ← .T 是转置(行列互换)

转置的用途:把"样本 × 特征"转成"特征 × 样本"。 在矩阵乘法、求协方差、某些库的接口对齐时会用到。

ravel() vs flatten():一个视图一个拷贝 ​

python
print(X.ravel())        # [1 2 3 4 5 6]   ← 视图(改了会影响 X)
print(X.flatten())      # [1 2 3 4 5 6]   ← 深拷贝(改了不影响 X)
ravel()flatten()
返回视图拷贝
内存不额外占额外占一份
改它会影响原数组会不会
该用哪个只读遍历时需要独立副本时

记忆法:flatten 比 ravel 长 —— 它多做了"复制"这件事。

newaxis:给数组加一个维度 ​

python
row = np.arange(3)
print(row.shape)                          # (3,)

print(row[np.newaxis, :].shape)           # (1, 3)   行向量
print(row[:, np.newaxis].shape)           # (3, 1)   列向量

为什么需要这个:这是第 21 章广播的关键工具。

python
col = np.arange(3)[:, np.newaxis]         # (3, 1)
r = np.arange(3)                          # (3,)
print((col + r).shape)                    # (3, 3)   ← 一张 3×3 的加法表

心智模型:(3,) 是一个"没有方向"的一维数组。 用 [:, np.newaxis] 把它变成"竖着的" (3, 1),用 [np.newaxis, :] 变成"横着的" (1, 3)。 方向不同,广播出来的结果就不同。

点开:还有更可读的写法 —— np.expand_dims
python
print(np.expand_dims(row, axis=0).shape)     # (1, 3)
print(np.expand_dims(row, axis=1).shape)     # (3, 1)

完全等价,但 np.expand_dims(row, axis=1) 比 row[:, np.newaxis] 更直白 ("在第 1 维插入一个长度为 1 的轴")。

选哪个:

  • 写短表达式、熟悉 NumPy 的人 → np.newaxis(更紧凑)
  • 追求可读性、团队协作 → np.expand_dims
  • 本书两个都会用,看到不认识的知道它们是同一件事就行。

在 PyTorch 里对应的是 x.unsqueeze(dim) 和 x.squeeze(dim)(去掉长度为 1 的维度)。

18.7 随机数:可复现是底线 ​

python
rng = np.random.default_rng(42)              # 固定种子
r1 = rng.normal(2.5, 0.01, 5)

rng2 = np.random.default_rng(42)             # 同样的种子
r2 = rng2.normal(2.5, 0.01, 5)

print(np.allclose(r1, r2))                   # True  ← 两次完全一致

为什么必须固定种子:

如果每次运行得到不同的随机数据,别人就无法复现你的结果, 你自己也无法判断"这次结果变好是因为改了方法,还是因为随机数不同"。

这就是本书数据用 seed=42 生成的原因 —— 你在自己电脑上跑出来的行数、 每支电芯的 β 值,必须和书上完全一致。不一致就说明哪里错了。

API 说明:

写法评价
np.random.default_rng(seed)✅ 推荐(新式,返回一个独立的 Generator 对象)
np.random.seed(seed) + np.random.normal(...)能用(老式,改全局状态)

为什么推荐新式:default_rng 返回的对象是独立的。 如果你在项目里同时用了两个库、它们都想用随机数,老式的全局种子会互相干扰; 新式的各管各的。

常用分布:

python
rng = np.random.default_rng(42)
rng.normal(2.5, 0.01, 5)         # 正态分布(均值 2.5、标准差 0.01)
rng.uniform(0.8, 1.6, 5)         # 均匀分布
rng.integers(800, 1501, 5)       # 整数 [800, 1501) —— 注意右端不含
rng.choice(["A", "B", "C"], 5)   # 从列表里随机选
rng.permutation(5)               # 打乱顺序(洗牌)

注意 integers:右端不含,所以想取到 1500 要写 1501。

本书数据里每支电芯的寿命就有个这样的区间(最短 859 圈、最长 1423 圈)—— integers(800, 1501) 就是构造这类"随机寿命"的典型写法。

18.8 完整示例与输出 ​

bash
cd code
python demos/ch18-array-create.py

真实输出(节选):

text
=== ② linspace:要「几个点」而不是「步长」 ===
  np.linspace(0.80, 1.00, 21) → 21 个点
  首 0.80  末 1.00  含两端 ✅

=== ④ 形状参数必须是元组(高频错误) ===
  np.zeros(2, 3) → TypeError: Cannot interpret '3' as a data type
  正确写法:np.zeros((2, 3))  ← 多一层括号

=== ⑤ reshape ===
  reshape(2, -1)  → shape=(2, 6)  ← -1 表示「你帮我算」
  reshape(5, 5) → ValueError: cannot reshape array of size 12 into shape (5,5)
  reshape 不复制数据,只是换了个「看待同一批数字」的视角(视图)。

=== ⑥ 转置 / 展平 / 增减维度 ===
  展平 ravel()  = [1 2 3 4 5 6]   ← 视图,改了会影响 X
  展平 flatten()= [1 2 3 4 5 6]  ← 深拷贝,改了不影响 X
  列向量在广播里非常有用(第 21 章):(n,1) 与 (m,) 能算出 (n,m) 的表。

=== ⑧ 随机数组要固定种子 ===
  第一次: [2.503  2.4896 2.5075 2.5094 2.4805]
  再来一次: [2.503  2.4896 2.5075 2.5094 2.4805]  两次一致吗? True

本章练习 ​

练习 1:用创建函数与 reshape 造数组

依次完成,每步打印 .shape 验证:

  1. 造一个 (12, 1) 的全零列向量;
  2. reshape 成 (3, 4),打印 shape 和 size;
  3. 用 -1 把它 reshape 回 1 维;
  4. 用 np.linspace 造 0.80 → 1.00 的 21 个点,打印首尾并验证"含两端";
  5. 用 np.eye(4) 造单位阵。
参考答案
python
import numpy as np

# ① 全零列向量:形状必须是元组
z = np.zeros((12, 1))
print(z.shape)                      # (12, 1)

# ② reshape 成 3×4
m = z.reshape(3, 4)
print(m.shape, m.size)              # (3, 4) 12
print(m.size == z.size)             # True  ← 元素总数守恒

# ③ 用 -1 回到 1 维
flat = m.reshape(-1)
print(flat.shape)                   # (12,)
print(m.ravel().shape)              # (12,)  ← ravel 也行,而且是视图

# ④ linspace 含两端
axis = np.linspace(0.80, 1.00, 21)
print(len(axis))                    # 21
print(axis[0], axis[-1])            # 0.8 1.0
print(axis[0] == 0.80 and axis[-1] == 1.00)   # True
print(axis[:5])                     # [0.8  0.81 0.82 0.83 0.84]
print(np.allclose(np.diff(axis), axis[1] - axis[0]))   # True 间距均匀

# ⑤ 单位阵
print(np.eye(4))

要确认的三件事:

  1. np.zeros((12, 1)) 的双层括号不能少 —— 少了就报 TypeError;
  2. reshape 前后元素总数不变:3 × 4 = 12;
  3. linspace(0.8, 1.0, 21) 含两端,而 arange(0.8, 1.0, 0.01) 不含 1.0。

完整练习:exercises/ex18-01-array-create

练习 2:为什么 reshape 改了数据,原数组也变了?
python
a = np.arange(12)
b = a.reshape(3, 4)
b[0, 0] = 99
print(a[0])

要求:先预测输出,再运行验证,然后解释原因。

参考答案

输出是 99,不是 0。

原因:reshape 返回的是视图(view),不是拷贝。

内存里的一串数字:  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
                      ↑
a 的看法:一维,12 个元素
b 的看法:3 行 4 列,同一块内存
          b[0,0] 就是内存里的第 0 个元素 —— 和 a[0] 是同一个位置

所以 b[0, 0] = 99 改的就是 a[0]。

怎么确认是视图:看 .base 属性。

python
print(b.base is a)          # True   ← 视图的 base 指向原数组
c = a.reshape(3, 4).copy()
print(c.base)               # None   ← 拷贝没有 base

哪些操作返回视图,哪些返回拷贝:

返回视图返回拷贝
切片 a[1:5]花式索引 a[[0, 2]]
reshape / ravel布尔索引 a[a > 5]
.T 转置flatten()
np.newaxis 增维astype()

记忆法:"只改变解读方式"的操作 → 视图;"需要挑挑拣拣或重新排版"的操作 → 拷贝。

为什么要这样设计:一亿个元素的切片如果都拷贝,内存直接翻倍。 视图让"切一片"变成零成本的"加个框"。

什么时候是坑:你以为自己在改一个副本,其实改到了原数据。

python
def normalize(arr):
    arr = arr.ravel()           # 视图!
    arr /= arr.max()            # 原地修改 → 调用方的数组被改了
    return arr

data = np.array([1.0, 2.0, 4.0])
normalize(data)
print(data)                     # [0.25 0.5  1.  ]  ← 调用方的 data 被改了!

修复:arr = arr.ravel().copy(),或者在函数内用 arr = arr / arr.max()(产生新数组而不是原地改)。

练习 3:随机数不可复现会带来什么问题
python
import numpy as np

# 版本 A
a1 = np.random.default_rng().normal(2.5, 0.01, 3)
a2 = np.random.default_rng().normal(2.5, 0.01, 3)

# 版本 B
rng = np.random.default_rng(42)
b1 = rng.normal(2.5, 0.01, 3)
b2 = rng.normal(2.5, 0.01, 3)

要求:说出 a1 和 a2、b1 和 b2 分别相不相等,以及各自适合什么场景。

参考答案
a1 和 a2:不相等(每次 default_rng() 不给种子,用的是系统熵)
b1 和 b2:也不相等!

注意 B 版本的反直觉之处:rng 是一个有状态的生成器。 b1 消耗了前 3 个随机数,b2 拿到的是接下来的 3 个 —— 所以两个数组内容不同!

但 B 版本是可复现的:重新运行整个脚本,b1 和 b2 的值会和上一次完全一样。

python
# 验证可复现性
rng = np.random.default_rng(42)
b1 = rng.normal(2.5, 0.01, 3)      # 每次运行都是同一组值

对比表:

写法每次运行一样吗适合
np.random.default_rng()(无种子)❌ 不一样真实随机需求(抽签、加密不适用)
np.random.default_rng(42)✅ 一样科研复现、教学、单元测试
同一个 rng 对象连续调用❌ 每次调用不同正常的多次采样

如果想让两批随机数完全相同,必须重建生成器:

python
r1 = np.random.default_rng(42).normal(2.5, 0.01, 3)
r2 = np.random.default_rng(42).normal(2.5, 0.01, 3)
print(np.allclose(r1, r2))     # True

为什么这件事对本书很重要:本书的数据用固定种子生成, 所以你在自己电脑上跑出来的 cycle_raw.csv 是 13,967 行、每支电芯的 β 值也和书上一致。 如果哪天你跑出来行数不对,第一件要检查的就是种子改没改。

在 AI 项目里的三处应用:

python
# ① 划分训练/测试集 —— 必须固定种子,否则每次划分不同、评估结果不可比
train = df.sample(frac=0.8, random_state=42)

# ② 模型初始化
torch.manual_seed(42)

# ③ 数据打乱 —— 要让实验可复现
DataLoader(dataset, shuffle=True, generator=torch.Generator().manual_seed(42))

面向 AI 开发:形状是 AI 里 90% 报错的根源 ​

如果说第 17 章教会你"张量就是数组",那这一章教的是每个 AI 工程师每天都要做的事:对齐形状。

一个真实的报错:

RuntimeError: Expected 4-dimensional input for 4-dimensional weight [64, 3, 7, 7],
but got 3-dimensional input of size [3, 224, 224] instead

翻译:卷积层要 (batch, channel, height, width) 四维, 你给的是 (channel, height, width) 三维 —— 少了一个 batch 维度。

修复就是用本章学的 newaxis:

python
img = np.zeros((3, 224, 224))                  # 单张图:(C, H, W)
batch = img[np.newaxis, :, :, :]                # (1, C, H, W)
print(batch.shape)                              # (1, 3, 224, 224)

AI 里最常遇到的四个形状错误:

错误原因修复
少 batch 维单样本没打包x[None] 或 x.unsqueeze(0)
多了 batch 维单样本当成 batchx.squeeze(0)
通道顺序不对(C,H,W) vs (H,W,C).permute() / .transpose()
展平前忘了 batch 维x.reshape(-1, 784) 把 batch 也展平了x.reshape(x.shape[0], -1)

最后一条最容易犯:

python
x = torch.zeros(32, 3, 224, 224)      # batch=32
bad = x.reshape(-1, 784)               # (32*3*224*224/784, 784) —— 全乱了!
good = x.reshape(x.shape[0], -1)       # (32, 150528)  ← 保住 batch 维

所以"用 x.shape[0] 而不是硬编码 batch 大小"是好习惯 —— 第 18 章讲 -1 通配符时提过这一点,在 AI 里它是硬性要求。

三个实用的调试习惯:

① 每个操作后 print(x.shape)。 这不是"新手做法",这是从业者的标准动作。

② 报错提到形状时,先看两个东西:

  • 报错里说"expected"什么形状、"got"什么形状;
  • 你喂进去的那个张量的 .shape 到底是什么。

③ 用 -1 和 x.shape[0] 表达"我不关心这一维",但永远对 batch 维显式。 这样换 batch size 时代码不用改。


下一章:第 19 章 · 数组的索引与切片(以及视图 vs 拷贝)