JAX基础操作:从数组到随机数,一个工程师的实战笔记

大家好,我是你们的老朋友。今天我们来聊聊JAX的基础操作。

说实话,我第一次接触JAX时,心里想的是:「又一个深度学习框架?我学不动了。」但真正用起来才发现,JAX的设计哲学非常优雅——它把NumPy的简洁和自动微分的强大结合在了一起。嗯,这很对我胃口。

核心要点:JAX的数组操作和NumPy几乎一模一样,但背后多了自动微分和GPU加速的能力。你只需要改变一行导入代码,就能获得飞一般的速度。

1. JAX数组创建与基本运算

先说说数组创建。我个人习惯用 jax.numpy 来替代 numpy,因为这样写出来的代码可以直接在GPU上跑。

import jax
import jax.numpy as jnp

# 创建数组
a = jnp.array([1, 2, 3, 4, 5])
b = jnp.zeros((3, 4))
c = jnp.ones((2, 3))
d = jnp.arange(10)
e = jnp.linspace(0, 1, 5)

print(a)  # [1 2 3 4 5]
print(b)  # 3x4的全零矩阵

基本运算也很直观。加减乘除、矩阵乘法,写法跟NumPy一模一样。

x = jnp.array([[1, 2], [3, 4]])
y = jnp.array([[5, 6], [7, 8]])

# 逐元素运算
print(x + y)   # 加法
print(x * y)   # 逐元素乘法

# 矩阵乘法
print(jnp.dot(x, y))  # 或者 x @ y

小技巧:我在项目中遇到过一个问题——用 jnp.dot 做矩阵乘法时,如果矩阵维度不匹配,报错信息比NumPy更详细。这点很贴心。

2. JAX与NumPy的互操作性

你想想看,如果JAX和NumPy不能互通,那得多麻烦?好在JAX设计者早就想到了这一点。

JAX数组可以轻松转成NumPy数组,反过来也行。但有个坑——JAX数组默认在GPU上,而NumPy数组在CPU上。转换时会有数据拷贝的开销。

import numpy as np

# JAX -> NumPy
jax_array = jnp.array([1, 2, 3])
numpy_array = np.array(jax_array)  # 或者 jax_array.__array__()

# NumPy -> JAX
numpy_data = np.random.randn(3, 4)
jax_data = jnp.array(numpy_data)

避坑指南:我曾经在训练循环里频繁做JAX和NumPy的转换,结果性能直接掉了一个数量级。记住:尽量在JAX的世界里完成所有操作,只在最后输出结果时转成NumPy。

还有一个实用函数——jax.device_put()。你可以显式地把数据放到指定设备上。

# 把数据放到GPU上
gpu_data = jax.device_put(numpy_data, jax.devices('gpu')[0])

3. 随机数生成与种子管理

随机数生成这块,JAX和NumPy有本质区别。NumPy用全局随机状态,而JAX要求你显式管理随机种子。为什么?因为JAX的函数式编程风格要求「无副作用」——每次调用随机函数,你都得传入一个状态,然后返回新的状态和随机数。

说白了,就是JAX不允许有隐藏的全局状态。这刚开始让我很不习惯,但用久了发现,这样反而更容易复现实验结果。

from jax import random

# 创建随机种子
key = random.PRNGKey(42)

# 生成随机数
rand_array = random.normal(key, shape=(3, 4))
print(rand_array)

# 生成新的种子
key, subkey = random.split(key)
rand_array2 = random.uniform(subkey, shape=(2, 3))

重要:永远不要重复使用同一个key!每次生成随机数后,都要用 random.split() 创建新key。否则你会得到完全相同的随机数序列——这在训练模型时会导致灾难性的结果。

我建议你养成这样的习惯:

# 好的做法
key = random.PRNGKey(0)
for epoch in range(10):
    key, subkey = random.split(key)
    batch = random.randint(subkey, shape=(32,), minval=0, maxval=100)
    # 训练代码...

为什么会这样设计?因为JAX的 jit 编译和 vmap 向量化要求函数是纯函数——同样的输入必须产生同样的输出。如果随机数生成依赖全局状态,那编译后的函数就没法保证确定性了。

知识体系总览

下面这张图帮你理清本章的核心逻辑:

JAX基础操作知识体系 数组创建与运算 jnp.array / zeros / ones 逐元素运算 + 矩阵乘法 NumPy互操作性 JAX ↔ NumPy 转换 device_put 显式设备控制 随机数生成 PRNGKey 显式种子管理 split 创建新种子 核心原则:函数式编程 + 无副作用 + 显式状态管理 所有操作都遵循「纯函数」原则,确保可复现和可编译

我的经验:刚开始用JAX时,我总是不自觉地用NumPy的随机函数,结果在jit编译时报错。后来我写了个小脚本,把所有 np.random 替换成 jax.random,并加上key管理逻辑。这个脚本我现在还在用。

好了,这一章的内容就到这里。记住:JAX的数组操作和NumPy几乎一样,但随机数管理完全不同。把key管理好,你的代码就能在GPU上飞起来。


专注资料整理