自动微分机制(c++ 实现)

56 阅读3分钟

一、什么是自动微分

自动微分基于链式法则,将复杂函数分解为基本的微分操作。它主要用于求解复杂函数的导数,尤其是对于高维、高阶导数的计算非常有用。在深度学习领域有着十分广泛的用途,减去了手动计算导数的痛苦工作

二、如何实现

这个就用比较简单的加减乘除作为示例, 有需要可以增加或者自定义函数

1.f(x) 形式

使用前向微分的形式

#include <iostream>

struct Node{
    float value;
    float grad;
    Node() = default;
    Node(float value);
    Node(float value, float gard);
};
Node::Node(float value){
    this->value = value;
    this->grad = 0;
}
Node::Node(float value, float gard){
    this->value = value;
    this->grad = gard;
}
Node add(Node op0, Node op1){
    return Node(op0.value + op1.value, op1.grad + op1.grad);
}
Node sub(Node op0, Node op1){
    return Node(op0.value - op1.value, op1.grad - op1.grad);
}
Node mul(Node op0, Node op1){
    return Node(op0.value * op1.value,
     op0.grad * op1.value + op0.value * op1.grad);
}
Node div(Node op0, Node op1){
    return Node(op0.value / op1.value,
     (op0.grad * op1.value - op0.value * op1.grad)/(op1.value * op1.value));
}
int main(){
    int val = 2;
    Node x(val, 1);
    Node revx = div(Node(1) ,x); // 1/x
    std::cout<<"value:"<<revx.value<<"\ngard:"<<revx.grad;
}

2.f(x, y) 等多元变量形式

#include <iostream>
struct Node{
    float value;
    float grad_x; 
    float grad_y;
    Node(float value, float grad_x, float gard_y):
    value(value),grad_x(grad_x),grad_y(gard_y){}
};
Node add(Node a, Node b) {
    return Node(a.value + b.value, a.grad_x + b.grad_x, a.grad_y + b.grad_y);
}
Node mul(Node a, Node b) {
    return Node(a.value * b.value, a.value * b.grad_x + b.value * a.grad_x, a.value * b.grad_y + b.value * a.grad_y);
}
//f(x, y) = x^2 + y^2 + 2xy + 1 
Node compute_function(Node x, Node y) {
    Node x2 = mul(x, x);         // x^2
    Node y2 = mul(y, y);         // y^2
    Node xyd = mul(Node(2, 0, 0), mul(x, y));
    Node sum = add(x2, add(y2, xyd));
    Node result = add(sum, Node(1, 0, 0));
    return result;
}
int main() {
    Node x = Node(3.0, 1.0, 0.0);
    Node y = Node(1.0, 0.0, 1.0); 
    Node result = compute_function(x, y);
    std::cout<<"f(x, y) = "<<result.value<<std::endl;
    std::cout<<"df/dx = "<<result.grad_x<<std::endl;
    std::cout<<"df/dy = "<< result.grad_y<<std::endl;
    return 0;
}

3. 不确定维度形式

问题来了,如果变量很多,而且路径不确定,前向微分算法对空间的占用会很大, 那么有没有什么可以记录需要指定变量梯度形式的方法呢?

这个相对来说比较复杂,前面的是正向微分,但是到这里是反向微分(确实反向微分更适合多元函数)

具体方法就是存路径,用一个比较简单的数据结构存储路径,在调用backward() 方法的时候按照路径计算需要求解的梯度

#include<iostream>
#include<vector>

namespace autograd{
    enum Operator{ADD, MUL,SUB,DIV};
    inline float operate(float vl, float vr, Operator op){
        switch (op){
            case ADD: return vl + vr; 
            case SUB: return vl - vr;
            case MUL: return vl * vr;
            case DIV: return vl / vr;
            default: std::cerr << "Unsupported operator!" << std::endl; exit(-1);
        }
    }

    struct BaseNode{
        float value;
        float grad;
        BaseNode* org_id;
        BaseNode():value(0),grad(0),org_id(this){}
        BaseNode(float value, float gard=0):value(value), grad(gard), org_id(this){}

        void accumulate(float DeltaGrad){
            this->grad += DeltaGrad;
        }
    };

    struct LinkNode: public BaseNode{
        BaseNode* left_node;
        BaseNode* right_node;
        Operator opr;

        LinkNode(BaseNode* l, BaseNode* r, Operator op):
        left_node(l), right_node(r), opr(op){
            this->org_id = this;
            this->grad = 0;
            this->value = operate(l->value, r->value, op);
            this->tape_line.push_back(this);
        }
        void backward(){
            this->zero_tape(true); //如果需要累计梯度就更改参数
            this->grad = 1.0f;
            for(auto it = tape_line.rbegin(); it!=tape_line.rend(); it++){
                if((*it)->org_id == this)  grad_backward((*it));
            }
        }
        static void clear_tape(){
            tape_line.clear();
        }
    private:
        static std::vector<LinkNode*> tape_line;
        void zero_tape(bool clear_leaf=false){
            tape_line.back()->grad = 0.0f;
            for(auto it = tape_line.rbegin(); it!=tape_line.rend(); it++){
                (*it)->grad = 0.0f;
                if(clear_leaf){
                    (*it)->left_node->grad = 0.0f;
                    (*it)->right_node->grad = 0.0f;
                }
            }
        }
        void grad_backward(LinkNode* t){
            BaseNode* l = t->left_node;
            BaseNode* r = t->right_node;
            Operator op = t->opr;
            l->org_id = r->org_id = t->org_id;
            switch (op){
                case ADD:l->accumulate(t->grad);r->accumulate(t->grad);return;
                case SUB:l->accumulate(t->grad);r->accumulate(-t->grad);return;
                case MUL:l->accumulate(t->grad * r->value);r->accumulate(t->grad * l->value);return;
                case DIV:l->accumulate(t->grad / r->value);r->accumulate(-t->grad * l->value / (r->value * r->value));return;
                default: std::cerr << "Unsupported operator!" << std::endl; exit(-1);
            }
        }
    };

    std::vector<LinkNode*> LinkNode::tape_line =
    std::vector<LinkNode*>();
}

using namespace std;
using namespace autograd;

int main() {
    BaseNode node1(5.0);
    BaseNode node2(3.0);
    LinkNode ln1(&node1, &node2, ADD);
    LinkNode ln2(&ln1, &node2, MUL);
    LinkNode ln3(&ln2, &node2, MUL);
    ln3.backward();
    cout<<node1.grad <<endl<<node2.grad << endl;
    return 0;
}