掘金 人工智能 11月12日 15:31
Rust ndarray:赋能 AI 与具身智能的高效数据底座
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入介绍了Rust的ndarray库,强调其在AI和具身智能领域的关键价值。ndarray作为Rust生态中对标NumPy的高性能数值计算库,提供了内存安全、高效计算(SIMD、并行处理)和灵活的维度处理能力,使其成为搭建AI数据管道和处理具身智能传感器数据的理想工具。文章从基础的数组创建、维度形状管理、切片索引,到所有权与视图的平衡,再到机器学习数据预处理、机器人传感器数据融合、强化学习状态处理等实际应用场景,全面展示了ndarray的强大功能和广泛适用性。最终总结了ndarray在性能、安全性和生态兼容性方面的三大核心优势,指出其是开发者构建高性能AI和具身智能系统的有力支撑。

🚀 **ndarray的核心价值与优势**:ndarray是Rust中强大的N维数组库,对标NumPy,提供内存安全、高效计算(支持SIMD和并行处理)以及灵活的维度处理能力。它特别适用于实时传感器数据处理、神经网络权重管理、图像和点云处理等,是构建AI数据管道和具身智能系统的关键工具。

📦 **数组创建与形状管理**:文章详细介绍了ndarray的多种数组创建方法,包括全零、全一、线性空间、自定义填充以及从向量构造,并阐述了如何获取和变换数组的形状(shape)和维度(ndim),如reshape、transpose和permute_axes,这对于数据预处理和模型适配至关重要。

🔍 **切片、索引与所有权机制**:ndarray支持强大的切片和索引功能,允许高效访问数组的子集,且返回的是视图(ArrayView),避免数据复制。同时,文章解释了Array(拥有所有权)和ArrayView(共享访问)的区别,强调了Rust所有权语义如何保证内存安全和线程安全,为构建健壮的AI系统奠定基础。

💡 **实际应用场景**:通过机器学习数据预处理(如图像归一化)、机器人传感器数据融合(激光雷达与摄像头数据结合)以及强化学习状态处理(网格世界状态更新)等具体案例,生动展示了ndarray在AI和具身智能领域的实际应用能力,证明了其在复杂计算任务中的高效性和可靠性。

赋能 AI 与具身智能:Rust ndarray 构建安全高效的数据底座

一、引言:ndarray 在 AI 与具身智能中的核心价值

在AI和具身智能快速发展的今天,高效的数据处理能力成为了项目成功的基础。

作为Rust生态中最强大的多维数组库,ndarray 正在成为构建高性能AI系统的秘密武器。

ndarray 作为 Rust 生态中对标 NumPy 的高性能数值计算库,既提供了类似于 NumPy 的功能,又拥有 Rust 的内存安全保证和卓越性能,是搭建 AI 数据管道和具身智能传感器数据处理的核心工具,特别适合:

本文将从基础用法切入,解析其核心功能如何为复杂 AI 任务奠定基础。

二、快速入门:ndarray 简介与环境搭建

1. ndarray 特性与应用场景

ndarray 是 Rust 的高性能 N 维数组库,支持动态形状的数组操作,具备以下核心优势:

广泛应用于机器学习数据预处理、机器人感知数据融合、强化学习状态空间建模等场景。

例如,在图像识别中,可将图像数据表示为三维 ndarray(高度、宽度、通道数),利用 ndarray 进行卷积运算;在机器人导航中,利用 ndarray 处理激光雷达的点云数据。

2. 安装与基本引用

依赖配置

Cargo.toml中添加:

[dependencies]ndarray = "0.17.1"
代码引入

在 Rust 代码中,使用use语句引入 ndarray 库:

use ndarray::prelude::*;

这将导入常用的 ndarray 类型和操作,如ArrayArrayView和各种数组创建函数。

三、数组创建:从基础初始化到灵活构造

1. 基础数组初始化

全零数组(zeros)

zeros方法用于创建一个全零的数组,在 AI 开发中常用于初始化缓冲区,或者作为模型初始化时的默认状态矩阵。

该方法接受一个表示数组形状的元组作为参数,并且可以通过类型标注或上下文推断元素类型。

// 创建一个形状为 (2, 3) 的二维全零数组,元素类型为 f64let zeros_array: Array<f64, _> = Array::zeros((2, 3));println!("{:?}", zeros_array);// 输出:// [[0.0, 0.0, 0.0],//  [0.0, 0.0, 0.0]], shape=[2, 3], strides=[3, 1], layout=Cc (0x5), const ndim=2

上述代码创建了一个 2 行 3 列的二维数组,每个元素都是0.0 。这种方式在神经网络的权重初始化中很常见,例如初始化卷积层的权重矩阵。

从向量构造(from_shape_vec)

from_shape_vec方法可以将一维向量转换为指定形状的多维数组,在处理从外部数据源(如 CSV 文件、传感器数据流)加载的数据时非常有用。

需要注意的是,向量的长度必须与指定形状的元素总数匹配,否则会返回 Result类型,包含 Err 信息。

use std::result::Result;// 尝试将向量转换为形状为 (2, 2) 的二维数组let data = vec![1, 2, 3, 4];let result: Result<Array<i32, _>, _> = Array::from_shape_vec((2, 2), data);match result {    Ok(array) => println!("{:?}", array),    Err(_) => println!("形状不匹配,无法转换"),}// 输出:// [[1, 2],//  [3, 4]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2

如果向量元素数量与形状不匹配,from_shape_vec会返回 Err ,开发者可以通过 Resultmatch 语句进行错误处理,确保数据转换的可靠性。

2. 高级创建方法

除了基本的创建方法,ndarray还提供了一系列便捷函数,用于生成特殊模式的数组。

// 创建一个形状为 (3, 1) 的二维全一数组,元素类型为 f32let ones_array: Array<f32, _> = Array::ones((3, 1));println!("{:?}", ones_array);// 输出:// [[1.0],//  [1.0],//  [1.0]], shape=[3, 1], strides=[1, 1], layout=CFcf (0xf), const ndim=2
// 生成一个包含5个元素,在0.0到1.0之间线性分布的一维数组let linspace_array = Array::linspace(0.0, 1.0, 5);println!("{:?}", linspace_array);// 输出:// [0.0, 0.25, 0.5, 0.75, 1.0], shape=[5], strides=[1], layout=CFcf (0xf), const ndim=1
// 创建一个形状为 (2, 2) 的二维数组,并用值7填充let custom_array = Array::from_elem((2, 2), 7);println!("{:?}", custom_array);// 输出:// [[7, 7],//  [7, 7]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2

这些高级创建方法极大地提高了数组初始化的灵活性,满足了 AI 和具身智能开发中多样化的数据准备需求 。

四、维度与形状:掌握数据结构的核心特征

1. 形状信息获取

ndarray 中,形状(shape)是描述数组维度大小的关键属性。通过shape方法可以获取一个表示数组各维度大小的元组,而ndim方法则返回数组的维度数。这两个属性在数据处理中非常重要,比如在矩阵乘法中,需要确保两个矩阵的维度匹配才能进行运算。

// 创建一个形状为 (2, 3) 的二维数组let array: Array<f64, _> = Array::zeros((2, 3));// 获取数组形状let shape = array.shape();println!("数组形状: {:?}", shape);// 数组形状: [2, 3]// 获取数组维度数let ndim = array.ndim();println!("数组维度数: {}", ndim);// 数组维度数: 2

上述代码中,shape方法返回的数组切片[2, 3]分别表示数组的第一维(行数)大小为 2,第二维(列数)大小为 3;ndim方法返回 2,表示这是一个二维数组。这种形状信息的获取在复杂的数据处理流程中,有助于确保数据的正确性和兼容性。

2. 形状变换与操作

重塑形状(reshape)

to_shape方法是改变数组形状的重要工具,它可以在不改变数组元素总数的前提下,重新调整数组的维度。在 AI 开发中,经常需要将输入数据的形状进行变换以适应模型的输入要求,比如将一维数据重塑为二维矩阵用于线性代数运算。

// 创建一个包含6个元素的一维数组let array: Array<i32, _> = Array::from_vec(vec![1, 2, 3, 4, 5, 6]);println!("重塑前的数组: \n{:?}", array);// 重塑前的数组: // [1, 2, 3, 4, 5, 6], shape=[6], strides=[1], layout=CFcf (0xf), const ndim=1// 将一维数组重塑为形状为 (2, 3) 的二维数组let array_views = array.view();let reshaped_array = array_views.to_shape((2, 3)).unwrap();println!("重塑后的数组: \n{:?}", reshaped_array);// 重塑后的数组: // [[1, 2, 3],// [4, 5, 6]], shape=[2, 3], strides=[3, 1], layout=Cc (0x5), const ndim=2

在这个例子中,to_shape方法将原本的一维数组[1, 2, 3, 4, 5, 6]成功转换为形状为[2, 3]的二维数组,其中元素按照行优先的顺序排列。需要注意的是,to_shape方法的参数必须保证新形状的元素总数与原数组相同,否则会返回Err

转置与轴操作
    矩阵转置:对于二维数组(矩阵),转置是一种常见的操作,用于交换矩阵的行和列。在 ndarray 中,可以使用t方法实现矩阵转置。
// 创建一个形状为 (2, 3) 的二维数组let matrix: Array2<f64> = arr2(&[[1., 2., 3.], [4., 5., 6.]]);// 转置矩阵let transposed_matrix = matrix.t();println!("原矩阵: \n{:?}", matrix);// 原矩阵: // [[1.0, 2.0, 3.0],//  [4.0, 5.0, 6.0]], shape=[2, 3], strides=[3, 1], layout=Cc (0x5), const ndim=2println!("转置后的矩阵: \n{:?}", transposed_matrix);// 转置后的矩阵: // [[1.0, 4.0],//  [2.0, 5.0],//  [3.0, 6.0]], shape=[3, 2], strides=[1, 3], layout=Ff (0xa), const ndim=2

上述代码中,matrix.t()将原矩阵的行和列进行了交换,得到一个形状为[3, 2]的转置矩阵。转置操作在图像数据处理中经常用于调整图像的通道顺序,或者在矩阵运算中满足特定的维度要求。

    维度交换:对于 n 维数组,permute_axes方法提供了更灵活的维度交换功能。它接受一个表示新轴顺序的元组,用于重新排列数组的维度。
// 创建一个形状为 (2, 3, 4) 的三维数组let data: Vec<f64> = vec![    1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,    17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0,];let mut tensor: Array3<f64> = Array::from_shape_vec((2, 3, 4), data).unwrap();println!("原张量:\n {:?}", tensor);// 原张量: // [[[1.0, 2.0, 3.0, 4.0],//  [5.0, 6.0, 7.0, 8.0],//  [9.0, 10.0, 11.0, 12.0]],// [[13.0, 14.0, 15.0, 16.0],//  [17.0, 18.0, 19.0, 20.0],//  [21.0, 22.0, 23.0, 24.0]]], shape=[2, 3, 4], strides=[12, 4, 1], layout=Cc (0x5), const ndim=3println!("原张量形状: {:?}", tensor.shape());// 张量形状: [2, 3, 4]// 将维度从 (2, 3, 4) 交换为 (3, 4, 2)tensor.permute_axes((1, 2, 0));println!("交换后的张量:\n {:?}", tensor);// 交换后的张量:// [[[1.0, 13.0],//  [2.0, 14.0],//  [3.0, 15.0],//  [4.0, 16.0]],// [[5.0, 17.0],//  [6.0, 18.0],//  [7.0, 19.0],//  [8.0, 20.0]],// [[9.0, 21.0],//  [10.0, 22.0],//  [11.0, 23.0],//  [12.0, 24.0]]], shape=[3, 4, 2], strides=[4, 1, 12], layout=Custom (0x0), const ndim=3println!("交换后的张量形状: {:?}", tensor.shape());// 交换后的张量形状: [3, 4, 2]

在这个例子中,permute_axes((1, 2, 0))将原三维数组的第一维与第二维交换,第二维与第三维交换,从而得到一个新的三维数组,形状变为[3, 4, 2]。这种操作在深度学习中处理多通道图像数据时非常有用,比如将(Channel, Height, Width)的图像数据格式转换为(Height, Width, Channel)

3. 切片与索引:高效访问子数据

切片和索引是从数组中提取特定元素或子数组的重要手段。在 ndarray 中,通过slice方法配合s!宏可以实现多维切片操作,支持指定起始索引、结束索引和步长,还可以使用负索引表示从数组末尾开始计数。

// 创建一个形状为 (3, 4) 的二维数组let array: Array2<i32> = arr2(&[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]);// 切片操作,获取第一行所有元素let row_slice = array.slice(s![0, ..]);println!("第一行切片: {:?}", row_slice);// 第一行切片: [1, 2, 3, 4], shape=[4], strides=[1], layout=CFcf (0xf), const ndim=1// 切片操作,获取第二列所有元素let col_slice = array.slice(s![.., 1]);println!("第二列切片: {:?}", col_slice);// 第二列切片: [2, 6, 10], shape=[3], strides=[4], layout=Custom (0x0), const ndim=1// 切片操作,获取一个子矩阵 (形状为 (2, 2))let sub_matrix = array.slice(s![1..3, 1..3]);println!("子矩阵切片: \n{:?}", sub_matrix);// 子矩阵切片: // [[6, 7],//  [10, 11]], shape=[2, 2], strides=[4, 1], layout=c (0x4), const ndim=2

上述代码展示了不同类型的切片操作。s![0, ..]表示选取第一行(索引为 0)的所有元素;s![.., 1]表示选取第二列(索引为 1)的所有元素;s![1..3, 1..3]表示选取从第二行到第三行(不包括第三行)、第二列到第三列(不包括第三列)的子矩阵。

这些切片操作返回的是数组视图(ArrayView),不会复制数据,因此在处理大规模数据时非常高效,适合对数据进行局部处理和分析。

五、所有权与视图:平衡性能与安全性的关键

1. 所有权语义:Array 的独占访问

ndarray 中,Array类型代表拥有数据所有权的数组,这种所有权语义确保了内存安全,同时支持可变操作,使得开发者可以自由修改数组内容。

当一个Array变量被创建时,它独占其所包含的数据,在其生命周期内,只有该变量可以对数据进行读写操作。这在需要对数据进行独占修改的场景中非常有用,比如在实时传感器数据的预处理管道中,每个数据点都可能需要根据不同的规则进行变换,使用拥有所有权的Array能够灵活地进行这些修改操作。

// 创建一个形状为 (2, 2) 的二维数组,拥有数据所有权let mut owned_array: Array<i32, _> = Array::from_elem((2, 2), 1);// 对数组进行可变操作,修改元素值owned_array[[0, 0]] = 2;println!("拥有所有权的数组: \n{:?}", owned_array);// 拥有所有权的数组:// [[2, 1],//  [1, 1]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2

上述代码中,owned_array拥有数组数据的所有权,因此可以通过索引操作对其元素进行修改。这种独占访问机制在多线程环境下尤为重要,它可以避免数据竞争问题,因为同一时间只有一个线程可以拥有并修改数组数据。

2. 轻量视图:ArrayView 的共享访问

ArrayView 是一种不拥有数据所有权的只读视图类型,它通过引用的方式访问原始数组,因此非常适合在不希望复制数据的情况下进行数据读取操作。

ArrayView 可以高效地访问原始数组的子集,而不会增加额外的内存开销,这在处理大规模数据时能够显著提升性能。在 AI 模型推理过程中,输入数据(如图像张量)通常是大规模的,使用ArrayView来处理这些数据可以避免不必要的内存拷贝,从而加快推理速度。

// 创建一个形状为 (3, 3) 的二维数组let mut original_array: Array<i32, _> = Array::from_elem((3, 3), 5);original_array[[1, 1]] = 6;// 获取数组的视图let view: ArrayView<i32, _> = original_array.view();// 通过视图访问数组元素println!("视图访问元素: {}", view[[1, 1]]);// 视图访问元素: 6

在这个例子中,vieworiginal_array 的视图,它不拥有数据所有权,但可以安全地访问原始数组的元素。由于视图只是对原始数据的引用,所以创建和使用视图的开销非常小,这对于需要频繁访问大型数组的场景非常友好。

3. 所有权转移与视图限制

    不可变性保证ArrayView 只能读取数据,无法修改,这一特性确保了在并发场景下的线程安全。多个线程可以同时持有同一个数组的ArrayView,而不会出现数据竞争的问题,因为没有任何一个视图可以修改数据。

    生命周期绑定:视图的生命周期依赖于原始数组,这意味着只要原始数组存在,视图就可以安全地访问其数据,避免了悬空引用的风险。当原始数组超出作用域被销毁时,所有相关的视图也会自动失效。

    性能优势ArrayView 配合 mapv 等方法对视图进行操作时,Rust 的零成本抽象机制能够确保这些操作在编译期进行优化,从而实现高效的计算。mapv 方法可以对视图中的每个元素应用一个函数,并且不会产生额外的运行时开销。

// 创建一个形状为 (2, 2) 的二维数组let array: Array<i32, _> = arr2(&[[1, 2], [3, 4]]);// 获取数组视图并应用函数let result = array.view().mapv(|x| x * 2);println!("视图操作结果: \n{:?}", result);// 视图操作结果:// [[2, 4],//  [6, 8]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2

上述代码中,mapv 方法对 array 的视图中的每个元素乘以 2,得到一个新的数组。这个过程中,由于视图的高效性和 Rust 的零成本抽象,操作的执行非常高效,既避免了数据复制,又保证了计算的速度。

六、实践场景:AI 与具身智能中的典型应用

1. 机器学习数据预处理

在机器学习中,数据预处理是关键的第一步,ndarray 提供了强大的工具来高效地处理和转换数据。

例如,在处理图像数据时,通常需要将图像文件加载为多维数组,并进行归一化、裁剪等操作。假设我们有一批图像数据,每个图像的尺寸为 224x224,有 3 个颜色通道(RGB),我们可以使用 ndarray 来加载和预处理这些数据。

use image::{self, GenericImageView, Pixel, Rgb};use ndarray::prelude::*;fn main() {    let image_data = load_image_data("./src/assets/test.jpeg");    let normal_data = normalize_image_data(&image_data);    println!("图像归一化数据:\n{:?}", normal_data);}// 加载图像数据fn load_image_data(path: &str) -> Array3<u8> {    let img = image::open(path).unwrap();    let (width, height) = img.dimensions();    let mut data = Array3::<u8>::zeros((height as usize, width as usize, 3));    for y in 0..height {        for x in 0..width {            let Rgb([r, g, b]) = img.get_pixel(x, y).to_rgb();            data[[y as usize, x as usize, 0]] = r;            data[[y as usize, x as usize, 1]] = g;            data[[y as usize, x as usize, 2]] = b;        }    }    data}// 归一化图像数据到0.0 - 1.0fn normalize_image_data(data: &Array3<u8>) -> Array3<f32> {    data.mapv(|x| x as f32 / 255.0)}

在上述代码中,load_image_data 函数将图像文件加载为一个三维的Array3<u8> 数组,其中每个元素表示一个像素点的 RGB 值。

normalize_image_data 函数则对图像数据进行归一化处理,将每个像素值从 0 ~ 255 的范围映射到 0.0 ~ 1.0 的范围,以满足机器学习模型的输入要求。这种数据预处理操作在图像分类、目标检测等任务中非常常见,ndarray 的高效数组操作使得这些计算能够快速完成。

2. 机器人传感器数据融合

在具身智能的机器人应用中,通常需要融合多种传感器的数据,如激光雷达、摄像头、惯性测量单元(IMU)等,以获得对环境的全面感知。

ndarray 可以方便地处理这些不同类型传感器的数据,并进行融合计算。

以激光雷达和摄像头数据融合为例,激光雷达可以提供环境的深度信息,而摄像头可以提供视觉图像信息。我们可以将激光雷达的点云数据表示为一个二维数组,其中每一行表示一个激光点的坐标(x, y, z),将摄像头图像数据表示为一个三维数组,如上述图像数据处理部分所述。

use ndarray::{Array2, Array3};// 简单的数据融合示例:将激光雷达的z值叠加到图像的亮度通道fn fuse_lidar_and_camera(lidar: &Array2<f32>, camera: &mut Array3<u8>) {    for i in 0..lidar.nrows() {        let (x, y, z) = (            lidar[[i, 0]] as usize,            lidar[[i, 1]] as usize,            lidar[[i, 2]],        );        if x < camera.shape()[1] && y < camera.shape()[0] {            let brightness = (camera[[y, x, 0]] as f32 + z * 10.0) as u8;            camera[[y, x, 0]] = brightness;            camera[[y, x, 1]] = brightness;            camera[[y, x, 2]] = brightness;        }    }}fn main() {    // 假设激光雷达数据,每一行是一个点的(x, y, z)坐标    let lidar_data: Array2<f32> = Array2::from_shape_vec((1000, 3), vec![2.0; 3000]).unwrap();    // 假设摄像头图像数据,形状为(height, width, channels)    let mut camera_data: Array3<u8> = Array3::ones((224, 224, 3));    fuse_lidar_and_camera(&lidar_data, &mut camera_data);    println!("camera data:\n {:?}", camera_data);}

在这个简单的示例中,fuse_lidar_and_camera 函数将激光雷达数据中的高度信息(z 值)叠加到摄像头图像的亮度通道中,实现了两种传感器数据的初步融合。

通过 ndarray 对不同形状和类型数组的灵活操作,机器人可以更有效地整合多源信息,做出更准确的决策,比如在导航、避障等任务中。

3. 强化学习状态处理

在强化学习中,智能体需要根据当前的环境状态做出决策,环境状态通常以多维数组的形式表示。

ndarray 可以用来高效地存储和更新这些状态信息,并且支持在不同状态表示之间进行转换。

例如,在一个简单的二维网格世界的强化学习任务中,智能体的状态可以表示为一个二维数组,其中每个元素表示网格中一个位置的特征(如是否为障碍物、是否为目标位置等)。

use ndarray::{Array2, Ix2};// 定义网格世界的大小const WIDTH: usize = 10;const HEIGHT: usize = 10;// 智能体移动函数,根据动作更新状态fn move_agent(state: &mut Array2<i32>, action: (i32, i32), agent_pos: &mut Ix2) {    let (dx, dy) = action;    let new_x = (agent_pos[0] as i32 + dx) as usize;    let new_y = (agent_pos[1] as i32 + dy) as usize;    if new_x < WIDTH && new_y < HEIGHT && state[[new_y, new_x]] == 0 {        state[[agent_pos[0], agent_pos[1]]] = 0;        *agent_pos = Ix2(new_y, new_x);        state[[agent_pos[0], agent_pos[1]]] = 2; // 标记智能体位置    }}fn main() {    // 初始化状态数组,0表示可通行,1表示障碍物    let mut state: Array2<i32> = Array2::zeros((HEIGHT, WIDTH));    state[[2, 3]] = 1; // 设置一个障碍物    let mut pos = Ix2(1, 1);    move_agent(&mut state, (1, 2), &mut pos);    println!("状态:{:?}", state);    println!("位置:{:?}", pos);}

在上述代码中,state 数组表示网格世界的状态,move_agent 函数根据智能体的动作(如向上、向下、向左、向右移动)更新状态数组和智能体的位置。

通过 ndarray 的高效数组操作,强化学习算法可以快速地处理大量的状态信息,计算最优策略,从而实现智能体在复杂环境中的自主学习和决策 。

七、总结:选择 ndarray 的三大理由

在 AI 与具身智能的开发中,ndarray 凭借其独特优势,成为处理多维数据的首选工具。

    性能保障:零运行时开销,支持底层硬件优化(SIMD、多线程),确保复杂计算高效执行,满足 AI 实时性需求。

    安全可靠:编译期所有权检查,避免 AI 系统中常见的内存错误,提升系统稳定性和可靠性。

    生态兼容:无缝对接 tch-rs(PyTorch 绑定)、rustlearn 等 AI 库,构建混合编程架构,助力开发者充分利用 Rust 生态资源。

通过掌握 ndarray 的核心功能,开发者可以在 AI 算法实现和具身智能系统开发中更高效地处理多维数据,为复杂模型训练和实时控制打下坚实基础。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

ndarray Rust AI 具身智能 数据处理 数值计算 内存安全 性能优化 机器学习 机器人 强化学习
相关文章