想象你是个洗衣工,面前堆着小山一样的脏衣服。如果一件件手洗:捞起、搓洗、换水、拧干,再捞起下一件……一整天下来,腰酸背痛,衣服还没洗完。可如果你把整桶衣服扔进洗衣机,选好程序,按一下启动键,“哗”的一声,几十件衣服在滚筒里一起翻滚、去污、漂洗、甩干,一个钟头全部搞定。 向量化思维(vectorization)的核心,正是这种“从一件一件处理”到“整批一起处理”的转身。拿到一个编程任务,你先问自己:“能不能把所有数据看成整桶衣服,交给机器批量处理?”能,就用数组运算一步到位;不能,再退回循环。不过批量处理有它自己的规则:机器启动要先注水(有固定开销),深色浅色要分袋(布尔掩码),再多的衣服也在同一个滚筒里翻滚(广播机制)。理解这些规则,你才不会盲目地“逢循环必向量化”。

岗位关联

对 AI 从业者:这是写出高效数值代码的底层心智。模型训练、数据预处理中 90% 的热点循环都能用数组运算替代,掌握它,代码快一个数量级,还更清晰易读。

目录
  1. 可视化演示
  2. 向量化本质
  3. 数组批量运算
  4. 布尔掩码
  5. 矩阵乘法
  6. 广播机制
  7. 性能权衡
  8. 心智转变
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示向量化思维的核心模式:循环vs向量化性能对比、布尔掩码条件筛选、矩阵乘法替代嵌套循环、广播机制自动对齐,交互切换四种模式理解数组运算优势。

本节配有交互动效(进入视口后自动加载,约 3KB)。

循环vs向量化:整体运算替代逐元素 传统循环逐元素处理,向量化用一次数组运算完成全部操作 for i: a[i]=b[i]+c[i] → a=b+c 底层SIMD指令并行加速,性能提升数十倍。

向量化本质

还是那个洗衣工。手洗时,你从盆里捞起一件衣服,搓两下,放下;再捞起下一件……这段“捞起—搓—放下”的重复小动作,就是编程里的循环。每一件衣服都是一个数组元素,循环就是逐个访问它们。这种写法没错,但它把精力耗在了“怎么访问每一件”上,而不是“把整批衣服变成什么”。

向量化的本质,是换一种提问方式:不再问“这件衣服怎么洗”,而是问“这一整桶衣服一起怎么处理”。在 Python 里,x 不再是一个“装着元素的列表”,而是一个完整的数组对象。你对整个数组做一次操作,等于同时对所有元素做操作——这正是洗衣机的工作原理:不是你手动把每件衣服按进水里,而是滚筒让所有衣服一起翻滚。

大多数数值计算库(如 NumPy)都支持这种整体操作。$ \mathbf{x} + 1 $ 的意思并不是“循环里给每个元素加 1”,而是“整桶衣服同时加一份漂白剂”——一次作用于全部元素。性能差距来自底层:数组运算走的是编译好的 C 代码和 SIMD 指令,Python 循环则是一个字节一个字节地在解释器里慢慢走。

一旦尝到“整桶处理”的甜头,你就很难再回到一件件手洗的日子。向量化本质上是心态的重置:数组是一个可被整体操作的“物体”,而不是一堆需要逐个拜访的格子。

y=x+1\displaystyle \mathbf{y} = \mathbf{x} + 1
这个式子的关键不在于“加 1”,而在于“没有下标”。y 的每一个元素都等于 x 对应位置的元素加 1,整个过程没有出现 i、j 这类下标。
  1. x 代表整批数据$ \mathbf{x} = [x_1, x_2, \dots, x_n] $,一整个序列作为一个整体出现。
  2. 加 1 是整体操作$ \mathbf{x} + 1 $ 表示对每个分量同时加 1,对整桶衣服统一操作。
  3. 结果 y 同样是数组$ \mathbf{y} = [x_1+1, x_2+1, \dots, x_n+1] $,形状和 x 保持一致。
  4. 没有下标索引不再写 y[i] = x[i] + 1,下标从你的思维和代码里同时消失。

数组批量运算

洗衣机上有好几个程序:强力洗、轻柔洗、漂洗、甩干。无论选哪个程序,它都是整桶一起作用——你不会把每件衣服单独塞进一个小桶。数组的批量运算也一样:加、减、乘、除、乘方、比较,所有这些操作默认都是“人人有份”,整个数组的所有元素同时经历同一套变换。

从手洗到机洗的过渡,也是一个身份的转变。写循环时,你是“操作者”,亲自蹲在盆边一件一件搓;写向量化代码时,你是“导演”,选好程序(运算符),一桶衣服自动完成流程。比如 data = data * 2 + 1:先让所有数据都放大一倍,再统一平移一个单位,一次到位。

批量运算的技术底气来自硬件和底层库。现代 CPU 的 SIMD(单指令多数据)指令可以一条指令同时处理多个数,NumPy 之类的库把循环下沉到 C 语言里,彻底绕开 Python 解释器逐行执行的慢速路径。于是同样数量的计算,向量化版本通常快几十倍。要小心的是,逐元素操作要求两个数组形状一致——两个洗衣桶大小不同是没法按位置对齐的。

批量运算的哲学是“程序选好,整桶走一遍”。当你写数值代码时,优先用这些整体操作去表达变换,而不是立刻展开成循环——你会发现自己少写了几十行 for,多出了大量思考算法本身的时间。

z=x⊙y\displaystyle \mathbf{z} = \mathbf{x} \odot \mathbf{y}
这里的 $ \odot $ 表示“逐元素相乘”:z 的第 i 个元素等于 x 的第 i 个元素乘以 y 的第 i 个元素,两组数按位置一一配对。
  1. 符号 ⊙ 的含义$ \mathbf{x} \odot \mathbf{y} = [x_1 y_1, x_2 y_2, \dots, x_n y_n] $,逐位相乘。
  2. 没有嵌套循环数学上写这个式子不需要 for 循环,计算上也不需要,一次完成。
  3. 形状一致性前提$ \mathrm{shape}(\mathbf{x}) = \mathrm{shape}(\mathbf{y}) $,两个“桶”大小相同才能逐元素对齐。
  4. 结果形状不变$ \mathrm{shape}(\mathbf{z}) = \mathrm{shape}(\mathbf{x}) $,批量运算不改变数据外形。

布尔掩码

手洗时最怕什么?深色衣服和浅色衣服混在一起,白衬衫被染成花的。有经验的洗衣工会先把深色挑出来单独洗。但如果用洗衣机,你不需要一件件挑——把所有深色衣服装进一个洗衣袋,扔进滚筒,机器洗完后袋子里就是深色衣服,袋子外是浅色衣服。这个洗衣袋,就是布尔掩码。

“挑出深色衣服”这个动作,在程序里本质上是一个条件判断:if color == 'dark' 就放进另一个篮子。向量化的做法是,用一个布尔数组(True/False 组成的数组)来标记“哪些元素满足条件”,然后用这个布尔数组直接去索引原数组:x[x > 0]。这一行,就替掉了整个 for + if 的组合。

再进一步,np.where(mask, a, b) 是“三目选择”:对每个位置,掩码为真取 a,否则取 b。这就像洗衣机的“分类洗涤”程序——同一个滚筒里,洗衣袋内的衣服走高温强洗,袋外的走轻柔模式。批量地筛选、过滤、选择,从此不再需要显式循环。

布尔掩码让你从“逐个检查”变成“一次性分袋”。写数据处理代码时,看到 if 就想想能不能用掩码;看到筛选、去极值、条件替换,都先想到“洗衣袋”——一个布尔数组解决所有分类问题。

y=x[ x>0 ]\displaystyle \mathbf{y} = \mathbf{x}[\, \mathbf{x} > 0 \,]
这个式子表达的是“从 x 中挑出所有正数”。内层的 x > 0 生成一个布尔掩码(哪些位置是正数),外层用这个掩码去索引 x,得到的结果只含正数。
  1. 内层生成掩码$ \mathbf{x} > 0 $ 逐元素比较,得到 $ [x_1>0, x_2>0, \dots, x_n>0] $。
  2. 掩码是布尔数组每个位置为 True 或 False,像洗衣袋上“装了什么”的标签。
  3. 外层索引筛选$ \mathbf{x}[\text{mask}] $ 只保留掩码为 True 位置的元素。
  4. 结果是一维子集$ \mathbf{y} $ 只包含原数组的正数,数量由数据决定。

矩阵乘法

现在换个场景:衣服洗完要熨烫。熨烫是流水线,要经过喷水、压平、折叠三道工序。假设你负责给一整排衣服熨烫,手工做法是最外层循环每一件衣服,内层循环每一道工序——这就是典型的嵌套循环。而矩阵乘法,相当于把三台机器首尾相接,衣服一件接一件地从这头进去、那头出来,整排衣服同时在线,没人闲着。

数学上,矩阵乘法把“所有衣服 × 所有工序”的组合一次性算完。$ C_{ij} $ 表示第 i 件衣服完成所有工序后的结果与第 j 个标准模板的匹配程度,它由第 i 件衣服的各状态与第 j 个模板的各权重逐项相乘再累加得到。那个“逐项相乘再累加”的内层累加,正是内层循环的化身。

这个运算太重要了,以至于底层库(BLAS)把它优化到极致。在 Python 里,np.dot(A, B) 或 A @ B 一行调用的,是经过几十年优化的高性能矩阵乘法,远非手写三层 for 能比。神经网络里的 Wx + b,图像变换里的投影矩阵,本质都是矩阵乘法在批量地“同时处理所有样本和所有特征”。

矩阵乘法的思维是“流水线并联”:与其一件件熨,不如让整排衣服一起走完流水线。在深度学习里,对整批数据做矩阵乘法,就是在用这个思路同时训练成千上万个样本。

Cij=∑k=1nAikBkj\displaystyle C_{ij} = \sum_{k=1}^{n} A_{ik} B_{kj}
结果矩阵 C 的第 i 行第 j 列,等于 A 的第 i 行与 B 的第 j 列逐项相乘再全部加起来。那个求和符号就是被矩阵乘法消灭的“内层循环”。
  1. 下标 i、j 定位结果$ C_{ij} $ 表示第 i 行第 j 列,是“第 i 件衣服 × 第 j 个模板”的输出。
  2. 下标 k 是累加轴$ A_{ik} $ 和 $ B_{kj} $ 共享 k,k 从 1 到 n 取遍所有“工序”。
  3. 求和号 ∑ 替代内层循环$ \sum_{k=1}^{n} $ 把 n 次相乘的结果累加成一个数。
  4. C 的所有元素同时计算外层 i、j 的变化对应“所有衣服 × 所有工序”的笛卡尔组合。

广播机制

一件大号羽绒服和一双小袜子,能放进同一台洗衣机吗?能——滚筒的空隙足够大,小袜子根本不需要单独占用一台机器。它“适应”了滚筒里的剩余空间。数组的广播(broadcasting)就是这个道理:形状不完全相同的两个数组,在运算时自动扩展,让彼此对齐。

比如一个形状为 (n, 1) 的列向量和一个形状为 (1, m) 的行向量相加。物理上,列向量只占一列,行向量只占一行,Broadcasting 会把列向量在横向上“复制”m 份,把行向量在纵向上“复制”n 份,然后逐元素相加,得到 (n, m) 的结果矩阵。这个过程在逻辑上发生了复制,但内存里并没有真的复制——省下了实实在在的空间。

广播规则只有三条:从尾部维度开始对齐,两个维度要么相等,要么其中一个是 1,要么某一方缺这个维度。满足其一就能广播。比如对一批数据做标准化:(x - mean) / std,mean 和 std 是标量,广播自动把它们作用到每个元素上。但广播也有陷阱:如果两个数组形状完全不兼容,NumPy 会报错,而不是默默做出错误结果——这就像把羽绒服塞进只能洗袜子的迷你洗衣机,机器会直接拒绝启动。

广播让我们在写代码时“各写各的大小”,剩下的交给机器对齐。代价是,逻辑上扩展出来的矩阵可能大得吓人——一个 $ 10^6 \times 1 $ 和一个 $ 1 \times 10^6 $ 广播,结果是 $ 10^{12} $ 个元素的矩阵,内存直接爆炸。这正是下一节要谈的权衡。

Rij=ai+bj,a∈Rn×1,  b∈R1×m\displaystyle \mathbf{R}_{ij} = a_i + b_j, \quad \mathbf{a} \in \mathbb{R}^{n \times 1},\; \mathbf{b} \in \mathbb{R}^{1 \times m}
一个 n 行 1 列的列向量 a,与一个 1 行 m 列的行向量 b 相加,结果是一个 n 行 m 列的矩阵 R,每个格子里是 a_i 加 b_j。
  1. 列向量 a$ \mathbf{a} \in \mathbb{R}^{n \times 1} $,只有一列,像竖着排的一队衣服。
  2. 行向量 b$ \mathbf{b} \in \mathbb{R}^{1 \times m} $,只有一行,像横着排的一排衣服。
  3. 自动扩展对齐a 横向复制 m 份,b 纵向复制 n 份,逻辑上都是 $ n \times m $。
  4. 结果 R$ \mathbf{R}_{ij} = a_i + b_j $,每个位置都是 a 的第 i 个元素加 b 的第 j 个元素。

性能权衡

如果今天只有一只脏袜子,你会开洗衣机吗?等它注水、转动三十分钟、排水、甩干,天都黑了。手洗一只袜子只要 30 秒。向量化也一样:它有自己的固定开销——分配新数组、准备连续内存、调用底层库,这些“注水过程”和时间无关,只要启动就要付出。数据量很小时,固定开销可能超过你省下的循环时间。

设每个元素循环处理耗时 $ t_{\text{item}} $,n 个元素的总耗时是 $ n \cdot t_{\text{item}} $;向量化的耗时是固定启动 $ t_{\text{start}} $ 加上批量处理 $ t_{\text{batch}} $。当 n 很小,$ n \cdot t_{\text{item}} $ 小于 $ t_{\text{start}} $ 时,循环反而更快。经验上,几百个元素以内的操作,两者差距不大;到了万级、百万级,向量化能快几十上百倍。

除了启动开销,还要警惕广播带来的隐性内存膨胀。一个 $ (10^6, 1) $ 和一个 $ (1, 10^6) $ 的数组做加法,逻辑上要产生 $ 10^{12} $ 个元素的中间结果,内存瞬间见底。这时候要么分块处理,要么用 in-place 操作(如 np.add(..., out=...))。向量化不是银弹,它是需要判断的工程决策——就像洗衣店老板会看单量决定用哪台机器。

所以“用洗衣机”不是无脑选择。写代码时,先估算数据规模:小数据用循环图个省事,大数据上向量化图个速度。真正的工程师,是让正确的工具出现在正确的场景里。

Tloop=n⋅titem,Tvec=tstart+tbatch\displaystyle T_{\text{loop}} = n \cdot t_{\text{item}}, \quad T_{\text{vec}} = t_{\text{start}} + t_{\text{batch}}
循环的总时间随元素个数线性增长;向量化的总时间是“固定启动费”加“批量处理费”。前者像手洗每件都要付出体力,后者像开机就收个启动费,之后每件几乎免费。
  1. T_loop 线性增长$ T_{\text{loop}} = n \cdot t_{\text{item}} $,元素越多,手洗时间越长。
  2. T_vec 含固定开销$ T_{\text{vec}} = t_{\text{start}} + t_{\text{batch}} $,无论多少衣服,注水启动都要花时间。
  3. 批量费随 n 亚线性$ t_{\text{batch}} $ 虽然随 n 增长,但底层 SIMD 并行让它增长得很慢。
  4. 决策不等式当 $ n \cdot t_{\text{item}} < t_{\text{start}} + t_{\text{batch}} $ 时,循环更优;n 越大,向量化越占优。

心智转变

走到这一步,你已经从一个蹲在盆边手洗的洗衣工,变成了管理三台洗衣机的洗衣房店主:不再盯着“第 7 件衬衫”,而是盯着“这一桶正在跑哪个程序”。向量化思维的最后一步,是让这个转变彻底刻进你的第一反应里——拿到任何数值任务,第一个念头是“数组能怎么做”,而不是“循环要怎么写”。

这个心智有几个标志。第一,先写数组表达式,再问“能不能不用循环”:比如对图片加噪声,直接 img + noise,而不是遍历像素。第二,调试时先检查形状(shape):一个数组运算出错,90% 是形状没对齐,用 .shape 一眼就能看出问题,而不是打印一百个值。第三,用函数组合表达流程:把 x -> 标准化 -> 线性变换 -> 激活函数 串成一条流水线,每个环节都是整体操作,可读性反而比一堆 for 和 if 更清晰。

在深度学习中,这种心智每天都在用。一个 batch 有 B 个样本、每个样本 D 维特征,整个 batch 就是 $ B \times D $ 的矩阵。模型的前向计算 $ y = f(x) $ 是对整个矩阵做变换,而不是循环每个样本。你不再是“逐个处理数据的工人”,而是“设计流水线的导演”。

这条路的终点是习惯成自然:看到数据先想形状,写代码先用数组操作,遇到性能瓶颈再退回循环。你不再关心第 7 件衬衫洗得怎么样,你只关心整体流程是否顺畅——这就是“用数组想问题”的全部含义。

y=f(x),x∈RB×D\displaystyle \mathbf{y} = f(\mathbf{x}), \quad \mathbf{x} \in \mathbb{R}^{B \times D}
一整个 batch 的数据被看成一个矩阵,模型 f 一次性作用于全部样本,输出 y 仍是一个矩阵。没有“循环每个样本”的代码,这就是 batch 级向量化的直觉。
  1. x 是批次矩阵$ \mathbf{x} \in \mathbb{R}^{B \times D} $,B 个样本、D 维特征,整批待处理。
  2. f 是整体变换$ f(\mathbf{x}) $ 表示模型对整个 batch 做一次前向计算。
  3. y 是批量输出$ \mathbf{y} $ 的形状也是 $ B \times $ 输出维度,所有样本一次出结果。
  4. 没有样本级循环$ \mathbf{y} = f(\mathbf{x}) $ 中没有 for i in range(B),batch 并行藏在数组运算里。

AI 应用场景

图像批量预处理流水线

阶段:数据预处理

解决的问题:图像增强(调亮度、加噪声、归一化)如果用双层循环逐像素操作,10000 张图片要跑几十分钟;用数组运算整张图片一次处理,秒级完成。传统方式把像素当作“一个个格子”去访问,AI 方式把整张图当作一块大数组整体变换。

具体案例:某自动驾驶团队需要对 10 万张道路图片做亮度调整 img = img * 0.8 + 30,再统一归一化 (img - mean) / std。改用向量化后,整个预处理流程从 6 小时压缩到 20 分钟,代码从 200 行 for 循环缩成 10 行数组表达式。

批量损失函数与梯度计算

阶段:模型训练

解决的问题:训练神经网络时,如果一个样本一个样本地算损失和梯度(循环方式),一个 epoch 要跑几个钟头;把整个 batch 的预测和标签拼成矩阵,用矩阵乘法和广播一步算出所有样本的损失。这正是“整桶衣服一起洗”在训练循环里的体现。

具体案例:训练一个图像分类模型,batch size 为 256。预测输出 logits 是 $ (256, 10) $ 的矩阵,标签是 $ (256,) $ 的向量。用 np.mean(-np.log(probs[np.arange(256), labels])) 配合布尔索引,一次算出整个 batch 的平均交叉熵,无需任何样本级循环。

特征筛选与数据清洗

阶段:数据分析与特征工程

解决的问题:真实数据里总有过大离群值和缺失值。传统做法是遍历每一行做 if 判断,慢且容易出错;用布尔掩码一次性筛出合法样本 df[df['age'].between(0, 120)],既快又直观。洗衣袋思路直接搬进表格世界。

具体案例:风控建模团队处理 800 万条用户记录,用三个布尔条件合成掩码——年龄区间、收入非负、标签非空——一行代码把非法样本从 800 万筛到 620 万,随后用 np.where 对缺失字段做中位数填充。

Python 演示

python38 行
import numpy as npimport time  def relu_loop(x):    """逐个元素实现 ReLU:循环加 if 判断"""    n = x.size    out = np.empty_like(x)    for i in range(n):        if x[i] > 0:            out[i] = x[i]        else:            out[i] = 0.0    return out  def relu_vec(x):    """向量化实现 ReLU:布尔掩码与数组乘法"""    return x * (x > 0)  def main():    rng = np.random.default_rng(42)    x = rng.standard_normal(1_000_000)     t0 = time.perf_counter()    a = relu_loop(x)    t1 = time.perf_counter()    b = relu_vec(x)    t2 = time.perf_counter()     print("循环耗时:   {:.4f} s".format(t1 - t0))    print("向量化耗时: {:.4f} s".format(t2 - t1))    print("两版结果一致:", np.array_equal(a, b))  if __name__ == "__main__":    main()
目的:对比同一激活函数 ReLU 的循环实现与向量化实现,展示布尔掩码替代 if 的写法,并实测速度差距。
  1. for i in range(n):手洗式循环:逐个访问数组元素,是向量化要替代的“捞一件搓一件”。
  2. if x[i] > 0:循环里的条件判断,等价于手洗时“检查这是不是深色衣服”。
  3. return x * (x > 0)向量化的灵魂一行:x > 0 生成布尔掩码,再与 x 相乘,负数变 0,正数保留。
  4. x = rng.standard_normal(1_000_000)生成 100 万个标准正态分布样本,模拟“一大桶衣服”。
  5. np.array_equal(a, b)验证两种写法结果完全一致,证明向量化不是牺牲正确性换速度。

常见误区

误区我在任何地方都用 numpy 的写法,哪怕数组只有三个元素。教程都说循环慢,那循环就是错的,什么时候都用向量化准没错。

正解向量化有固定启动开销——分配内存、数据对齐、调用底层库,就像开洗衣机要先注水。只有 3 个元素时,循环的耗时是纳秒级,向量化的启动开销反而更慢。正确的思路是:规模小用循环,规模大用向量化,中间地带用性能分析工具(如 timeit)来实测,而不是凭信仰。

为何常见:教程为了强调向量化优势,总拿百万级数据举例;读者记住了结论,却忘了前提条件是“数据量足够大”。

误区我把自己的函数用 `np.vectorize` 包了一层,这样就有向量化的性能了,代码里不再出现循环。

正解np.vectorize 只是把 Python 循环隐藏在函数内部,它仍然逐个调用你的 Python 函数,本质还是手洗——只是把“捞衣服”的动作换了个包装。真正的向量化必须把操作下沉到底层 C/SIMD 层面,直接用 NumPy 的原生运算符和通用函数(ufunc)。

为何常见:函数名里带着 “vectorize“ 这个词,很容易让人误以为它完成了向量化;实际它只负责”语法上像向量化”。

误区广播能省代码,那我就故意把数据展开成大矩阵再运算,反正都是一行代码。广播这么方便,怎么会有问题?

正解广播的“扩展”在逻辑上是免费的,但在内存里可能很贵。一个 $ (10^6, 1) $ 的列向量和一个 $ (1, 10^6) $ 的行向量相加,逻辑结果有 $ 10^{12} $ 个元素,直接撑爆内存。广播是工具,不是咒语;使用前要估算中间结果的形状和内存占用,必要时用分块或 in-place 操作。

为何常见:初学者见过“小数组 + 标量”这种安全的广播,就想当然认为任意广播都安全;忽略了维度同时放大时的组合爆炸。

误区我把所有逻辑压缩成一行超长的矩阵运算,显得自己很厉害,而且向量化就是好的,所以可读性一定没问题。

正解一行 200 个字符、叠加 5 层操作的数组表达式,可读性可能比一个清晰的 for 循环更差。向量化的收益是速度和简洁,但简洁不等于隐晦。好的做法是拆成有名字的中间变量,如 normalized = (x - mean) / std,并加一句注释说明这步在做什么变换。

为何常见:“炫技”心理作祟,把“短”误认为“好”;其实可读性的核心是“意图是否清晰”,而不是“字符数是否少”。

面试题

Q1. 向量化(vectorization)最核心的思想是什么?

单选题 · easy

  • A.用更多内存换取更快的循环
  • B.对整个数组做整体运算,替代逐元素循环← 正确答案
  • C.把 Python 代码编译成 C 语言
  • D.用多线程并行执行同一个函数

答案:B。向量化的核心是把“一个元素一个元素处理”变成“整个数组一次处理”,对应 B。A 提到内存换速度是广播的一个副作用,不是核心思想;C 是编译优化,不是向量化本身;D 是并行,与向量化属于不同维度。

Q2. 关于 np.vectorize,下列说法正确的是?

单选题 · mid

  • A.它能把任意 Python 函数变成真正的高速向量化实现
  • B.它和手写 for 循环一样,本质仍是逐个调用 Python 函数← 正确答案
  • C.它自动把函数编译成 C 代码
  • D.它只适用于布尔函数

答案:B。np.vectorize 只是把 Python 循环藏进函数里,逐元素调用原函数,性能与手写 for 相当,答 B。它并没有下沉到 C/SIMD 层面,也不会编译,故 A、C 错;它能包装任何函数,不只布尔函数,D 错。这就像把“手洗”包装成“自动手洗”,本质还是手洗。

Q3. 以下哪些情况适合使用布尔掩码?

多选题 · mid

  • A.过滤数组中的负数← 正确答案
  • B.根据条件从两个数组中三目选择← 正确答案
  • C.计算两个矩阵的点积
  • D.去除离群点(超出指定范围的数据)← 正确答案

答案:ABD。布尔掩码用于条件筛选和三目选择:过滤负数、去离群点都是典型的 x[mask] 场景,where 实现三目选择,所以 A、B、D 正确。C 点积是矩阵乘法,属于另一类运算,不涉及布尔条件。

Q4. 数据量只有 5 个元素时,循环和向量化哪个更快?

单选题 · easy

  • A.一定向量化更快
  • B.一定循环更快
  • C.通常循环可能更快,因为向量化有固定启动开销← 正确答案
  • D.两者的速度永远相同

答案:C。向量化有启动开销(分配内存、底层调用准备),在极小数据量下这笔开销可能超过省下的时间,所以通常循环可能更快,选 C。这不是绝对规律(A、B 都太绝对),D 显然错误。就像只有一只袜子时,手洗比开洗衣机快。

Q5. 关于广播机制,以下说法正确的有哪些?

多选题 · hard

  • A.两个数组从尾部维度开始对齐← 正确答案
  • B.某个维度不相等但其中一个为 1 时可以广播← 正确答案
  • C.广播产生的中间数组永远不占额外内存
  • D.广播可能在逻辑上产生巨大的结果矩阵← 正确答案

答案:ABD。广播规则是从尾部对齐,维度相等或一方为 1 或缺维时可广播,所以 A、B 正确。C 错:逻辑扩展不复制数据,但最终结果矩阵仍占内存,且可能巨大;D 正确,如 $ (10^6,1) $ 与 $ (1,10^6) $ 广播产生 $ 10^{12} $ 的结果。这就是“大羽绒服和小袜子能共滚筒,但滚筒本身有大小限制”。

Q6. 矩阵乘法 $ C_{ij} = \sum_{k=1}^{n} A_{ik} B_{kj} $ 中的求和符号 ∑ 对应编程中的什么?

单选题 · mid

  • A.最外层循环
  • B.条件判断 if
  • C.内层循环的累加← 正确答案
  • D.函数调用

答案:C。求和符号把 k 从 1 到 n 逐项相乘再累加,这正是一段内层循环:对每个 k 做 sum += A[i][k] * B[k][j]。它代表被矩阵乘法“吸收”掉的嵌套循环的内层,选 C。最外层循环对应 i、j 的遍历,而不是求和号。

Q7. 以下哪行代码实现了 ReLU 激活函数(负数归零、正数保留)?

单选题 · easy

  • A.x = x + 1
  • B.y = x * (x > 0)← 正确答案
  • C.y = np.dot(x, x)
  • D.y = x[x > 0]

答案:B。x * (x > 0) 中 x > 0 生成布尔掩码,True 当作 1、False 当作 0,乘到 x 上后负数被清零、正数保留,正是 ReLU。A 是整体加 1;C 是矩阵乘;D 用掩码直接筛选,会丢掉负数的位置信息、改变形状,不是 ReLU。

Q8. 以下哪些做法能缓解广播造成的内存爆炸?

多选题 · hard

  • A.分块处理,每次只算一个子块← 正确答案
  • B.用 in-place 操作(如 np.add(..., out=...))← 正确答案
  • C.总是把数据复制成稠密矩阵
  • D.先估算结果形状和内存,再决定策略← 正确答案

答案:ABD。分块处理把大结果拆成小块逐块计算,A 正确;in-place 操作避免额外中间数组,B 正确;先估算内存再决策是理性的工程习惯,D 正确。C 恰好相反——复制成稠密矩阵只会让内存问题更严重。

Q9. 向量化代码比 Python 循环快的最根本原因是?

单选题 · mid

  • A.Python 解释器被绕过了,运算下沉到编译好的 C 代码和 SIMD 指令← 正确答案
  • B.for 循环本身在 Python 中不存在
  • C.向量化代码没有执行任何运算
  • D.向量化会自动使用 GPU

答案:A。向量化把逐元素运算交给底层的编译型库和 SIMD 指令批量执行,绕开了 Python 解释器的逐行解释,A 正确。for 循环当然存在,B 错;向量化一样有运算,C 错;向量化可以在 CPU 上就很快,D 的“自动 GPU”是另一回事。

Q10. 以下哪些是“向量化思维”的心智标志?

多选题 · easy

  • A.拿到任务先想“数组能不能整体做”← 正确答案
  • B.调试时先检查数组形状(shape)← 正确答案
  • C.每个变量都要用列表存储再逐个访问
  • D.用函数组合表达数据变换流水线← 正确答案

答案:ABD。向量化心智包含:先想数组整体操作(A)、调试时查形状而非逐值打印(B)、用函数组合表达流水线(D)。C 是典型的循环式思维——“用列表存储再逐个访问”正是向量化要替代的做法。