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

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

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

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

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

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

  • 实时传感器数据处理(机器人感知)
  • 神经网络权重管理
  • 图像和点云处理
  • 数值计算和线性代数运算

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

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

1. ndarray 特性与应用场景

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

  • 内存安全:通过 Rust 所有权系统避免缓冲区溢出、悬垂指针等底层错误。

  • 高效计算 :支持 SIMD 向量化运算 和 并行处理(配合 rayon 库)。

  • 维度灵活 :轻松处理 1DnD 数组,兼容多种数据布局(C/Fortran 序)。

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

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

2. 安装与基本引用

依赖配置

Cargo.toml中添加:

toml 复制代码
[dependencies]
ndarray = "0.17.1"
代码引入

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

rust 复制代码
use ndarray::prelude::*;

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

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

1. 基础数组初始化

全零数组(zeros)

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

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

rust 复制代码
// 创建一个形状为 (2, 3) 的二维全零数组,元素类型为 f64
let 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 信息。

rust 复制代码
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还提供了一系列便捷函数,用于生成特殊模式的数组。

  • 全一数组Array::ones(shape) 使用ones方法可以创建一个全为 1 的数组,形状由传入的参数指定。常用于初始化权重矩阵为全 1 的情况,方便后续的归一化操作。
rust 复制代码
// 创建一个形状为 (3, 1) 的二维全一数组,元素类型为 f32
let 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
  • 线性空间Array::linspace(start, end, count) linspace方法用于生成一个包含count个元素的数组,元素在startend之间线性分布。在 AI 算法中,常用于生成等间距的样本点,比如在训练线性回归模型时,生成等间距的特征值。
rust 复制代码
// 生成一个包含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
  • 自定义填充Array::from_elem(shape, value) from_elem方法用指定的值value填充数组,形状由shape参数决定。这在初始化神经网络的偏置向量时非常有用,可以将所有偏置初始化为同一个值。
rust 复制代码
// 创建一个形状为 (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方法则返回数组的维度数。这两个属性在数据处理中非常重要,比如在矩阵乘法中,需要确保两个矩阵的维度匹配才能进行运算。

rust 复制代码
// 创建一个形状为 (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 开发中,经常需要将输入数据的形状进行变换以适应模型的输入要求,比如将一维数据重塑为二维矩阵用于线性代数运算。

rust 复制代码
// 创建一个包含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

转置与轴操作
  1. 矩阵转置 :对于二维数组(矩阵),转置是一种常见的操作,用于交换矩阵的行和列。在 ndarray 中,可以使用t方法实现矩阵转置。
rust 复制代码
// 创建一个形状为 (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=2
println!("转置后的矩阵: \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]的转置矩阵。转置操作在图像数据处理中经常用于调整图像的通道顺序,或者在矩阵运算中满足特定的维度要求。

  1. 维度交换 :对于 n 维数组,permute_axes方法提供了更灵活的维度交换功能。它接受一个表示新轴顺序的元组,用于重新排列数组的维度。
rust 复制代码
// 创建一个形状为 (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=3
println!("原张量形状: {:?}", 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=3
println!("交换后的张量形状: {:?}", tensor.shape());
// 交换后的张量形状: [3, 4, 2]

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

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

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

rust 复制代码
// 创建一个形状为 (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能够灵活地进行这些修改操作。

rust 复制代码
// 创建一个形状为 (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来处理这些数据可以避免不必要的内存拷贝,从而加快推理速度。

rust 复制代码
// 创建一个形状为 (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. 所有权转移与视图限制

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

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

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

rust 复制代码
// 创建一个形状为 (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 来加载和预处理这些数据。

rust 复制代码
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.0
fn 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),将摄像头图像数据表示为一个三维数组,如上述图像数据处理部分所述。

rust 复制代码
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 可以用来高效地存储和更新这些状态信息,并且支持在不同状态表示之间进行转换。

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

rust 复制代码
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 凭借其独特优势,成为处理多维数据的首选工具。

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

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

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

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

相关推荐
Asus.Blogs1 分钟前
SSE + Resty + Goroutine + Channel 完整学习笔记
笔记·学习·golang
雍凉明月夜7 分钟前
深度学习网络笔记Ⅱ(常见网络分类1)
人工智能·笔记·深度学习
卷心菜_7 分钟前
代码随想录笔记-背包问题
笔记
北岛寒沫7 分钟前
北京大学国家发展研究院 经济学辅修 经济学原理课程笔记(第十三课 垄断竞争)
人工智能·经验分享·笔记
AI营销实验室9 分钟前
AI 工具何高质量的为销售线索打分?
大数据·人工智能
Wang2012201310 分钟前
RNN和LSTM对比
人工智能·算法·架构
xueyongfu14 分钟前
从Diffusion到VLA pi0(π0)
人工智能·算法·stable diffusion
jackylzh30 分钟前
配置pytorch环境,并调试YOLO
人工智能·pytorch·yolo
杜子不疼.38 分钟前
AI Ping双款新模型同步免费解锁:GLM-4.7与MiniMax M2.1实测
人工智能
打码人的日常分享38 分钟前
企业数据资产管控和数据治理解决方案
大数据·运维·网络·人工智能·云计算