You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Rust实现简易自动微分:子节点数据结构设计难题

在Rust中实现自动微分的节点引用方案

在Rust里实现类似micrograd的自动微分,节点引用的核心需求是共享所有权(多个父节点可引用同一个子节点)和可变性(反向传播时需修改grad),下面给你几个贴合Rust安全规则的实用方案:

1. 保留Rc<RefCell<Value<T>>>——理解它的“复制”本质

你觉得Rc<RefCell<Value<T>>>需要复制,但其实Rc::clone()只是增加引用计数,完全不会拷贝Value的data/grad等核心数据,开销可以忽略不计。micrograd的计算图是无环图(DAG),基本不会出现循环引用问题,这个方案是最贴近Python引用逻辑的实现方式。

示例实现Add操作:

use std::rc::Rc;
use std::cell::RefCell;

enum Op {
    Add,
    Sub,
    Mul,
    Div,
}

struct Value<T: Default + Clone> {
    data: T,
    grad: T,
    children: Vec<Rc<RefCell<Value<T>>>>,
    op: Option<Op>,
}

// 为Rc<RefCell<Value<T>>>实现Add,贴合实际使用场景
impl<T: Default + Clone + std::ops::Add<Output = T>> std::ops::Add for Rc<RefCell<Value<T>>> {
    type Output = Rc<RefCell<Value<T>>>;

    fn add(self, rhs: Self) -> Self::Output {
        let lhs_data = self.borrow().data.clone();
        let rhs_data = rhs.borrow().data.clone();
        let result_data = lhs_data + rhs_data;

        // 创建结果节点,子节点通过clone Rc引用输入节点
        let result = Rc::new(RefCell::new(Value {
            data: result_data,
            grad: T::default(),
            children: vec![self.clone(), rhs.clone()],
            op: Some(Op::Add),
        }));

        result
    }
}

2. 用Weak<RefCell<Value<T>>>规避潜在内存泄漏

如果担心未来可能出现循环引用(虽在micrograd场景中极少发生),可以将children改为Vec<Weak<RefCell<Value<T>>>>,反向传播时再将Weak升级为Rc:

use std::rc::Weak;

struct Value<T: Default> {
    data: T,
    grad: T,
    children: Vec<Weak<RefCell<Value<T>>>>,
    op: Option<Op>,
}

impl<T: Default + Clone + std::ops::Add<Output = T>> std::ops::Add for Rc<RefCell<Value<T>>> {
    type Output = Rc<RefCell<Value<T>>>;

    fn add(self, rhs: Self) -> Self::Output {
        let lhs_data = self.borrow().data.clone();
        let rhs_data = rhs.borrow().data.clone();
        let result_data = lhs_data + rhs_data;

        let result = Rc::new(RefCell::new(Value {
            data: result_data,
            grad: T::default(),
            children: vec![Rc::downgrade(&self), Rc::downgrade(&rhs)],
            op: Some(Op::Add),
        }));

        result
    }
}

// 反向传播时升级Weak引用
fn backward<T: Default + std::ops::AddAssign + Clone>(value: &Rc<RefCell<Value<T>>>) {
    let mut value_mut = value.borrow_mut();
    value_mut.grad += T::default(); // 根节点梯度初始化为1(需根据数值类型调整,比如f32设为1.0)
    for child_weak in &value_mut.children {
        if let Some(child) = child_weak.upgrade() {
            match value_mut.op {
                Some(Op::Add) => child.borrow_mut().grad += value_mut.grad.clone(),
                // 其他操作的梯度逻辑
                _ => {}
            }
            backward(&child);
        }
    }
}

3. 索引+全局存储——彻底避开引用检查

如果不想用Rc/RefCell的运行时检查,可以把所有Value节点存入一个Vec,用usize索引指代子节点。这种方案在Rust数值计算库中很常见,完全符合Rust的借用规则:

enum Op {
    Add,
    Sub,
    Mul,
    Div,
}

struct Value<T: Default> {
    data: T,
    grad: T,
    children: Vec<usize>,
    op: Option<Op>,
}

struct Graph<T: Default> {
    nodes: Vec<Value<T>>,
}

impl<T: Default> Graph<T> {
    fn new() -> Self {
        Graph { nodes: Vec::new() }
    }

    // 添加原始节点,返回索引
    fn add_node(&mut self, data: T) -> usize {
        self.nodes.push(Value {
            data,
            grad: T::default(),
            children: Vec::new(),
            op: None,
        });
        self.nodes.len() - 1
    }

    // 基于两个节点索引执行Add操作,返回结果节点索引
    fn add(&mut self, lhs_idx: usize, rhs_idx: usize) -> usize
    where
        T: Clone + std::ops::Add<Output = T>,
    {
        let lhs_data = self.nodes[lhs_idx].data.clone();
        let rhs_data = self.nodes[rhs_idx].data.clone();
        let result_data = lhs_data + rhs_data;

        self.nodes.push(Value {
            data: result_data,
            grad: T::default(),
            children: vec![lhs_idx, rhs_idx],
            op: Some(Op::Add),
        });
        self.nodes.len() - 1
    }

    // 反向传播实现(含拓扑排序避免重复计算)
    fn backward(&mut self, root_idx: usize)
    where
        T: Default + std::ops::AddAssign + Clone,
    {
        let mut topo = Vec::new();
        let mut visited = vec![false; self.nodes.len()];
        self.traverse(root_idx, &mut visited, &mut topo);

        // 初始化根节点梯度(需根据数值类型调整,比如f32设为1.0)
        if let Some(grad) = self.nodes[root_idx].grad.as_mut() {
            // 示例:若T为f32,*grad = 1.0;
        }

        // 反向遍历拓扑排序后的节点
        for &idx in topo.iter().rev() {
            let node = &self.nodes[idx];
            let grad = node.grad.clone();
            match node.op {
                Some(Op::Add) => {
                    let lhs_idx = node.children[0];
                    let rhs_idx = node.children[1];
                    self.nodes[lhs_idx].grad += grad.clone();
                    self.nodes[rhs_idx].grad += grad;
                }
                // 其他操作的梯度逻辑
                _ => {}
            }
        }
    }

    // 拓扑排序遍历
    fn traverse(&self, idx: usize, visited: &mut [bool], topo: &mut Vec<usize>) {
        if visited[idx] {
            return;
        }
        visited[idx] = true;
        for &child_idx in &self.nodes[idx].children {
            self.traverse(child_idx, visited, topo);
        }
        topo.push(idx);
    }
}

为什么&mut Value<T>不可行?

你尝试的Vec<&mut Value<T>>违反了Rust的核心借用规则:

  1. 同一时间只能存在一个可变引用,无法满足多个父节点引用同一个子节点的需求;
  2. 引用的生命周期受限,无法长期存储在新节点的children中,会导致编译错误。
    因此&mut完全不适合这种需要共享和长期持有引用的场景。

内容的提问来源于stack exchange,提问作者RickarySanchez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 08:33:13