《代数式代码》课程实践
这学期选了个课叫做《代数式代码与 AI 框架》
最近刚讲到了 AI 框架了。
出于兴趣,用 rust 语言把前几节课讲的代数式代码(基于范畴论)有关的知识实践一下。
一、柯里化
柯里化就是把一个多元函数转变为多个单元函数的复合函数的过程。
通过柯里化,我们可以把所有函数都看作是一元函数,使得对函数的研究更加接近本质。
这里我不打算讨论柯里化的用处(因为我根本还对范畴论一窍不通)。
只是用 Rust 实现一下柯里化的过程,下面是一个二元函数柯里化的过程。
rustfn currying(f: fn(i32, i32) -> i32, a: i32) -> impl Fn(i32) -> i32 {
move |b| f(a, b)
}
fn main() {
let add = |a, b| a + b;
let add_5 = currying(add, 5);
println!("5 + 7 = {}", add_5(7));
let mul = |a, b| a * b;
let mul_3 = currying(mul, 3);
println!("3 * 4 = {}", mul_3(4));
}
plaintext5 + 7 = 12
3 * 4 = 12
可以看到我们把加法函数和乘法函数通过柯里化创建了一个 “加5”函数和“乘3”函数。
感觉挺神奇的,我目前的感悟就是这种机制也只有把函数当作一个动态变量类型的现代语言(我感觉就是那些实现了 lambda 表达式特征的语言,因为这个柯里化的过程在 lambda 演算中非常基础)才能实现吧。因为可以在运行中创建一个新的函数,像 C语言 这种所有函数都是实现编写好的代码肯定是做不到柯里化的。
二、函子
函子,英文名是 functor。在范畴论上是不同范畴之间的映射。
因为我还不懂范畴论,所以在编程上,我的理解函子就是一个把一个函数映射为另一个函数的映射,并且映射前后的函数还拥有着相似的性质。
代码如下
rustfn functor<T>(func: fn(T) -> T) -> impl Fn(Vec<T>) -> Vec<T> {
move |v: Vec<T>|
v.into_iter()
.map(func)
.collect()
}
这个一个输入参数是一元函数的函子,输出是一个作用于向量的函数。
利用这个一元函子,我们可以把一元函数转换为对应操作的向量函数。
rust let list = vec![0., PI/6., PI/4., PI/3.];
let sin = functor(|x: f32| x.sin());
let sin_list = sin(list);
println!("{:?}", sin_list);
plaintext[0.0, 0.5, 0.70710677, 0.86602545]
通过给不同类型的张量实现函子,我们就可以把任何对一元函数转变为对张量的函数,非常方便。
针对二元函数的函子也是可以的实现的
rust let add = double_functor(|x: f32, y: f32| x + y);
let list1 = vec![1., 2., 3.];
let list2 = vec![4., 5., 6.];
let add_list = add(list1, list2);
println!("{:?}", add_list);
plaintext[5.0, 7.0, 9.0]
三、变体类型
变体类型(Variant Types)也称作联合类型(Union Types),对应着代数数据类型(ADTs)中的和类型。(ADTs 还有一种类型叫做 积类型,对应着编程语言中的 结构体)。
变体类型支持递归定义,因此可以用来定义复杂的代数类型。
比如定义神经网络:
rust#[derive(Debug, Clone)]
enum ActFunc {
Relu,
Sigmoid,
Tanh,
Linear,
}
#[derive(Debug, Clone)]
enum NN {
Sequential(Box<NN>, Box<NN>),
Layer {
weights: Matrix,
bias: Vec<f64>,
act: ActFunC,
}
}
还有就是我这次实践的定义表达式,并且实现求值和导数
表达式求值和求导代码实现
定义表达式变体类型如下
rust#[derive(Debug, Clone)]
enum Expr {
// 基本类型
C(f64), // 常数
V(i32), // 变量
// 单目运算符
Neg(Box<Expr>), // 负号
Pow(Box<Expr>, f64), // 幂函数
Exp(f64, Box<Expr>), // 指数函数
Log(f64, Box<Expr>), // 对数函数
Sin(Box<Expr>), // 正弦函数
Cos(Box<Expr>), // 余弦函数
// 双目运算符
Add(Box<Expr>, Box<Expr>), // 加法
Sub(Box<Expr>, Box<Expr>), // 减法
Mul(Box<Expr>, Box<Expr>), // 乘法
Div(Box<Expr>, Box<Expr>), // 除法
}
为表达式创建一些构建方法(不然直接使用Box,Rust 就会让你知道什么是“可读性”)
rustimpl Expr {
fn c(a: f64) -> Self { Expr::C(a) }
fn v(i: i32) -> Self { Expr::V(i) }
fn neg(e: Expr) -> Self { Expr::Neg(Box::new(e)) }
fn pow(e: Expr, a: f64) -> Self { Expr::Pow(Box::new(e), a) }
fn exp(a: f64, e: Expr) -> Self { Expr::Exp(a, Box::new(e)) }
fn log(a: f64, e: Expr) -> Self { Expr::Log(a, Box::new(e)) }
fn sin(e: Expr) -> Self { Expr::Sin(Box::new(e)) }
fn cos(e: Expr) -> Self { Expr::Cos(Box::new(e)) }
fn add(e1: Expr, e2: Expr) -> Self { Expr::Add(Box::new(e1), Box::new(e2)) }
fn sub(e1: Expr, e2: Expr) -> Self { Expr::Sub(Box::new(e1), Box::new(e2)) }
fn mul(e1: Expr, e2: Expr) -> Self { Expr::Mul(Box::new(e1), Box::new(e2)) }
fn div(e1: Expr, e2: Expr) -> Self { Expr::Div(Box::new(e1), Box::new(e2)) }
}
接下来是表达式的计算,比较简单。
rustimpl Expr {
// 计算表达式的值,vars为变量值数组
fn eval(&self, vars: &[f64]) -> f64 {
match self {
Expr::C(a) => *a,
Expr::V(i) => vars[*i as usize],
Expr::Neg(e) => -e.eval(vars),
Expr::Pow(e, a) => e.eval(vars).powf(*a),
Expr::Exp(a, e) => a.powf(e.eval(vars)),
Expr::Log(a, e) => e.eval(vars).log(*a),
Expr::Sin(e) => e.eval(vars).sin(),
Expr::Cos(e) => e.eval(vars).cos(),
Expr::Add(e1, e2) => e1.eval(vars) + e2.eval(vars),
Expr::Sub(e1, e2) => e1.eval(vars) - e2.eval(vars),
Expr::Mul(e1, e2) => e1.eval(vars) * e2.eval(vars),
Expr::Div(e1, e2) => e1.eval(vars) / e2.eval(vars),
}
}
}
然后是表达式的求导。
使用变体类型的神奇之处就在于,只要定义好每个类型的运算规则,复杂的运算就可以直接交给递归就可以了。
rust// 对表达式进行求导的函数
fn derivative(e: Expr, i: i32) -> Expr {
match e {
// 常数的导数为0
Expr::C(_a) => Expr::C(0.0),
// 如果变量i等于当前变量,则导数为1,否则为0
Expr::V(_i) => {
if _i == i {
Expr::C(1.0)
} else {
Expr::C(0.0)
}
},
// 对负号求导,就是求导的负号
Expr::Neg(e) => Expr::neg(derivative(*e, i)),
// 幂函数 (e^a) 的导数 = e' * a * e^(a-1)
Expr::Pow(e, a) =>
Expr::mul(
derivative(*e.clone(), i),
Expr::mul(
Expr::c(a),
Expr::pow(*e, a - 1.0)
)
),
// 指数函数 (a^e) 的导数 = e' * a^e * lna
Expr::Exp(a, e) => {
Expr::mul(
derivative(*e.clone(), i),
Expr::mul(
Expr::exp(a, *e),
Expr::log(E, Expr::c(a))
)
)
},
// 对数函数 (log_b e) 的导数 = e' * 1 / (e * lna)
Expr::Log(a, e) => {
Expr::mul(
derivative(*e.clone(), i),
Expr::div(
Expr::c(1.0),
Expr::mul(
*e,
Expr::log(E, Expr::c(a))
)
)
)
},
// 正弦函数的导数 sin(e) = e' cos(e)
Expr::Sin(e) => {
Expr::mul(
derivative(*e.clone(), i),
Expr::cos(*e),
)
},
// 余弦函数的导数 cos(e) = - e' sin(e)
Expr::Cos(e) => {
Expr::neg(
Expr::mul(
derivative(*e.clone(), i),
Expr::sin(*e)
)
)
},
// 加法的导数 e1 + e2 = e1' + e2'
Expr::Add(e1, e2) => {
Expr::add(
derivative(*e1, i),
derivative(*e2, i)
)
},
// 减法的导数 e1 - e2 = e1' - e2'
Expr::Sub(e1, e2) => {
Expr::sub(
derivative(*e1, i),
derivative(*e2, i)
)
},
// 乘法的导数(乘积法则) e1 * e2 = e1' * e2 + e1 * e2'
Expr::Mul(e1, e2) => {
Expr::add(
Expr::mul(*e1.clone(), derivative(*e2.clone(), i)),
Expr::mul(*e2, derivative(*e1, i))
)
},
// 除法的导数(商法则) e1 / e2 = (e1' * e2 - e1 * e2') / e2^2
Expr::Div(e1, e2) => {
Expr::div(
Expr::sub(
Expr::mul(derivative(*e1.clone(), i), *e2.clone()),
Expr::mul(*e1, derivative(*e2.clone(), i))
),
Expr::pow(*e2, 2.0)
)
}
}
}
测试
幂函数 + 三角函数 + 乘积
rust // 示例:计算 f(x) = x^2 * sin(x) 的导数并打印
let x = Expr::v(0);
let f = Expr::mul(Expr::pow(x.clone(), 2.0), Expr::sin(x.clone()));
let df = derivative(f.clone(), 0);
println!("f(x) = {:?}", f);
println!("f'(x) = {:?}", df);
// 计算x=Π时的值和导数值
let x_value = PI;
let result_f = f.eval(&[x_value]);
let result_df = df.eval(&[x_value]);
println!("f(Π) = {:.5}", result_f);
println!("f'(Π) = {:.5}", result_df);
println!();
plaintextf(x) = Mul(Pow(V(0), 2.0), Sin(V(0)))
f'(x) = Add(Mul(Pow(V(0), 2.0), Mul(C(1.0), Cos(V(0)))), Mul(Sin(V(0)), Mul(C(1.0), Mul(C(2.0), Pow(V(0), 1.0)))))
f(Π) = 0.00000
f'(Π) = -9.86960
幂函数 + 加法 + 除法
rust // 示例:计算 g(x) = (x^2 + 2x + 1) / (x^2 + 1) 的导数并打印
let x = Expr::v(0);
let g = Expr::div(
Expr::add(
Expr::add(
Expr::pow(x.clone(), 2.0),
Expr::mul(Expr::c(2.0), x.clone())
),
Expr::c(1.0)
),
Expr::add(
Expr::pow(x.clone(), 2.0),
Expr::c(1.0)
)
);
let dg = derivative(g.clone(), 0);
println!("g(x) = {:?}", g);
println!("g'(x) = {:?}", dg);
println!();
plaintextg(x) = Div(Add(Add(Pow(V(0), 2.0), Mul(C(2.0), V(0))), C(1.0)), Add(Pow(V(0), 2.0), C(1.0)))
g'(x) = Div(Sub(Mul(Add(Add(Mul(C(1.0), Mul(C(2.0), Pow(V(0), 1.0))), Add(Mul(C(2.0), C(1.0)), Mul(V(0), C(0.0)))), C(0.0)), Add(Pow(V(0), 2.0), C(1.0))), Mul(Add(Add(Pow(V(0), 2.0), Mul(C(2.0), V(0))), C(1.0)), Add(Mul(C(1.0), Mul(C(2.0), Pow(V(0), 1.0))), C(0.0)))), Pow(Add(Pow(V(0), 2.0), C(1.0)), 2.0))
对数 + 乘法
rust // 计算x=E时的值和导数值
let x_value = E;
let result_h = h.eval(&[x_value]);
let result_dh = dh.eval(&[x_value]);
println!("h(1) = {:.5}", result_h);
println!("h'(1) = {:.5}", result_dh)
plaintexth(x) = Mul(V(0), Log(2.718281828459045, V(0)))
h'(x) = Add(Mul(V(0), Mul(C(1.0), Div(C(1.0), Mul(V(0), Log(2.718281828459045, C(2.718281828459045)))))), Mul(Log(2.718281828459045, V(0)), C(1.0)))
h(1) = 2.71828
h'(1) = 2.00000
结语
学这个 《代数式代码》 的课程目前给我的最大感受就是“原来有这么高级的编程技巧”,以及“果然还是要从数学入手”,所以我又开新坑,准备开始学一学范畴论了。
之前看到了某个网友范畴论的学习路线
lambda演算 -> 抽象代数 -> 图论 -> 计算机科学中的范畴论
目前我先大致了解了下和图灵机齐名的 lambda 演算。在进入抽象代数之前,我打算先学习(复习)一下群论的知识,目前正在看《群论彩图版》这个比较入门级的书。
上上周终于是把 Rust 圣经的高级实践部分看了,上一周 BB 的 Rust服务器也是写好了,不过还没来得及整理成文章(后面再说吧,感觉乏善可陈,就是在练 Rust 的熟练度)。也终于可以开始再次挑战 rCore了。
路漫漫其修远兮,吾将上下而求索。
💬 评论区
暂无评论,来发表第一条吧。