2. JAX核心数据结构:DeviceArray、PRNGKey随机数生成、数组创建与操作

好,咱们直接进入正题。JAX 的核心,说白了就是围绕几个关键数据结构转的。你如果搞懂了 DeviceArrayPRNGKey,那 JAX 的感知管线你就拿下一半了。我刚开始从 NumPy 切到 JAX 时,也踩过不少坑,今天咱们一个一个捋清楚。

2.1 DeviceArray:JAX 的“亲儿子”数组

JAX 里的数组,不叫 ndarray,叫 DeviceArray。名字已经告诉你答案了——它活在设备上(GPU/TPU)。

你想想看,在 NumPy 里你创建一个数组,数据就在 CPU 内存里。但在 JAX 里,默认情况下,数组直接扔到 GPU 上。这意味着什么?意味着你没法直接 print 看它的值,得先把它拉回 CPU。

核心区别一句话:NumPy 数组在 CPU 上,JAX 的 DeviceArray 在加速器上。

import jax
import jax.numpy as jnp

# 创建一个 DeviceArray
arr = jnp.array([1.0, 2.0, 3.0])
print(type(arr))  # <class 'jaxlib.xla_extension.ArrayImpl'>

# 想看看值?用 .block_until_ready() 或者直接 print
print(arr)  # 会自动同步,但最好养成显式同步的习惯

我个人习惯在调试时用 jax.device_get() 把数组拉回 NumPy,这样能避免很多隐式同步带来的性能问题。

小技巧:如果你想把 DeviceArray 转成 NumPy 数组,用 np.array(jax_array) 或者 jax_array.__array__()。但注意,这会触发一次设备到主机的拷贝,别在热循环里这么干。

2.2 PRNGKey:JAX 的随机数生成,跟你想的不一样

JAX 的随机数生成,可能是初学者最头疼的地方。为什么?因为它没有全局随机状态。

在 NumPy 里,你调 np.random.randn(),内部状态自动更新。但在 JAX 里,所有随机函数都需要你显式传入一个 PRNGKey。这设计一开始我也觉得烦,但后来发现——这其实是好事。

为什么?因为 JAX 的函数式编程范式要求“纯函数”——同样的输入,永远给出同样的输出。如果随机数生成器偷偷改了内部状态,那函数就不纯了。

from jax import random

# 创建一个 PRNGKey
key = random.PRNGKey(42)  # 42 是种子,随便选

# 生成随机数
key, subkey = random.split(key)  # 分裂 key,得到新的 key 和子 key
rand_vals = random.normal(subkey, shape=(3,))
print(rand_vals)

我曾经在项目中犯过一个低级错误:在循环里反复用同一个 key 生成随机数,结果每次迭代得到的随机序列完全一样。嗯,那模型训练直接废了。

避坑指南:千万不要重复使用同一个 key!每次生成随机数前,先用 random.split() 分裂出新的 key。我习惯在训练循环开头统一 split 一批 key,然后按需取用。

你可能会问:为什么不用 random.fold()fold 是把 key 和一个整数结合生成新 key,适合在循环里按索引生成。我个人更推荐 split,因为它更直观,不容易出错。

2.3 数组创建与操作:从 NumPy 到 JAX 的无缝迁移

好消息是,JAX 的数组操作 API 几乎和 NumPy 一模一样。你如果会 NumPy,那 JAX 上手基本零成本。

但有几个关键区别,我建议你记牢:

操作 NumPy JAX 注意点
创建全零数组 np.zeros((3,4)) jnp.zeros((3,4)) 完全一样
创建全一数组 np.ones((2,3)) jnp.ones((2,3)) 完全一样
创建单位矩阵 np.eye(5) jnp.eye(5) 完全一样
创建等差数列 np.arange(0, 10, 2) jnp.arange(0, 10, 2) 完全一样
创建均匀分布 np.linspace(0, 1, 5) jnp.linspace(0, 1, 5) 完全一样
重塑形状 arr.reshape(2, -1) arr.reshape(2, -1) 完全一样
转置 arr.T arr.T 完全一样
拼接 np.concatenate([a,b]) jnp.concatenate([a,b]) 完全一样

你看,基本就是换个前缀的事。但有一个地方要特别小心——原地修改

重要提醒:JAX 的数组是不可变的!你不能写 arr[0] = 1,会报错。如果你需要“修改”数组,实际上是在创建新数组。这在自动驾驶感知中处理点云数据时尤其要注意——别想着原地更新,用 jnp.wherejnp.scatter_add 这类函数式操作。

# 错误示范:原地修改
# arr[0] = 100  # 会报错!

# 正确做法:用索引更新
arr = jnp.array([1, 2, 3])
new_arr = arr.at[0].set(100)  # 返回新数组,原数组不变
print(new_arr)  # [100, 2, 3]

这个 .at[] 语法,我一开始也觉得别扭。但用习惯了你会发现,它其实更安全——你永远不会不小心改了原始数据。在自动驾驶感知中,我们经常要同时维护多个数据副本(比如原始点云、降采样后的点云、体素化后的点云),不可变性让这一切变得清晰可控。

2.4 知识体系总览

下面这张图,帮你把本章的核心逻辑串起来:

JAX 核心数据结构与操作 DeviceArray 活在 GPU/TPU 上的数组 PRNGKey 显式随机数生成器状态 数组创建与操作 API 与 NumPy 高度一致 不可变(Immutable) 延迟计算(Lazy) .block_until_ready() 同步 random.split() 分裂 random.fold() 折叠 不可重复使用同一 key 创建:zeros/ones/eye 操作:reshape/transpose 索引更新:.at[].set() 核心原则:函数式、不可变、显式状态

这张图把三个核心模块的关系画得很清楚。你从左边 DeviceArray 出发,经过 PRNGKey 的随机数生成,再到数组的各种创建和操作,最后都落到同一个原则:函数式、不可变、显式状态。记住这三点,JAX 的坑你就能避开一大半。

我的建议:刚开始用 JAX 时,别急着上复杂模型。先花一天时间,把 jnp 的各种操作和 random 模块玩熟。我在做激光雷达点云处理时,就是靠这些基础操作一步步搭起来的。基础不牢,后面写 JIT 编译和 vmap 时会很痛苦。


公众号:蓝海资料掘金营,微信deep3321