介绍
所以……我应该在我的 DevOps 弧上,但在这里我正在写一篇关于低级编程的文章!这条道路的吸引力太强了,我无法抗拒。我保证,我会在本周末回到 DevOps!
自动梯度
关于 Autograd 有很好的参考:
摘自 Pytorch 网站:
PyTorch 的 Autograd 功能是 PyTorch 灵活快速地构建机器学习项目的一部分。它允许在复杂的计算中快速、轻松地计算多个偏导数(也称为梯度)。此操作是基于反向传播的神经网络学习的核心。
一般用例中的机器学习。
机器学习已被证明在各个领域都非常有用:
-
医疗保健:促进新型和先进药物的制造进步。在我读过的一篇文章(但未能找到)中,机器学习能够实现快速药物发现:原本需要数十年的研发过程,现在可以在一年内完成。
-
物流:仓库管理、路线优化、供需预测等。
-
金融:信用风险评估、欺诈检测、宏观经济预测等。
-
…
股票投资组合管理中的机器学习
我想指出机器学习在我的主要领域的使用:资金管理。让我为您提供基于机器学习的投资组合(也称为量化投资组合)的历史表现图表:
-
截至 2024 年 10 月的年初至今。
性能最低的之一。 -
与其他策略相比 5 年。
5 年 CAR(复合年利率)远远落后于 HF 综合指数和股票。 -
10 年绩效比较。
从长远来看(5-10年),性能非常差。
所以,是的,你可以得出自己的结论,下次有人试图炒作或向你推销复杂的量化基金投资组合时,你可以做出更好的判断,而不是被误导。
我想说的一点是:到目前为止,与基于机器学习的量化基金相比,传统基金管理仍然占据上风。您不必相信我,您可以在这里下载 PDF 报告。
我可以写更多关于量化基金低于平均水平的表现,以及看看它们在 Covid-19 大流行爆发时的表现是多么有趣(剧透:这是最糟糕的基金之一!)。
回到代码!
好了,财务方面的事情已经讲完了,我们在这里讨论 Autograd 基准!我习惯于提供性能最好的语言的代码,因为如果我包含 4 种语言的所有代码,帖子就会变得太长。
我会将所有代码发布到代码存储库中,但同时..性能最高的是 Zig。
[更新]:您可以在pastebin中找到代码:
#1 缺点
和往常一样,在编写 Zig 时,我不喜欢 Zig 的冗长,因此这里的 const 是我为减少冗长而做出的努力。
const std = @import("std"); const testing = std.testing; const print = std.debug.print; const time = std.time.milliTimestamp; const Allocator = std.mem.Allocator; const ArenaAlloc = std.heap.ArenaAllocator; const pageAlloc = std.heap.page_allocator;
#2 枚举和结构
- OpType 枚举和 NaiveTape 结构是不言自明的。
- 操作结构:表示具有 OpType、两个输入变量和一个输出变量的操作。
const OpType = enum(u2) { Sum, Prod, Softplus, }; const NaiveVar = struct { val: f64, grad: f64, pub fn init(val: f64) NaiveVar { return .{ .val = val, .grad = 0.0 }; } }; const Operation = struct { op_type: OpType, inputs: [2]*NaiveVar, output: NaiveVar, };
#3 NaiveTape 结构
- init:创建新实例。
- deinit:释放分配的内存。
const NaiveTape = struct { ops: []Operation, allocator: Allocator, pub fn init(allocator: Allocator) NaiveTape { return .{ .ops = &[_]Operation{}, .allocator = allocator, }; } pub fn deinit(self: *NaiveTape) void { self.allocator.free(self.ops); } // ---Place the various methods here--- // // ---Place the various methods here--- // };
以下是 NaiveTape 结构体上的方法
#3a 求和方法
添加两个输入变量并将结果作为新变量返回。
pub fn sum(self: *NaiveTape, input1: *NaiveVar, input2: *NaiveVar) !*NaiveVar { const sum_val = input1.val + input2.val; const sum_var = NaiveVar.init(sum_val); const op = Operation{ .op_type = .Sum, .inputs = [_]*NaiveVar{ input1, input2 }, .output = sum_var, }; self.ops = try self.allocator.realloc(self.ops, self.ops.len + 1); self.ops[self.ops.len - 1] = op; return &self.ops[self.ops.len - 1].output; }
#3b 生产方法
将两个输入变量相乘并将结果作为新变量返回。
pub fn prod(self: *NaiveTape, var1: *NaiveVar, var2: *NaiveVar) !*NaiveVar { const prod_val = var1.val * var2.val; const prod_var = NaiveVar.init(prod_val); const op = Operation{ .op_type = .Prod, .inputs = [_]*NaiveVar{ var1, var2 }, .output = prod_var, }; self.ops = try self.allocator.realloc(self.ops, self.ops.len + 1); self.ops[self.ops.len - 1] = op; return &self.ops[self.ops.len - 1].output; }
#3c Softplus方法
将 softplus 激活函数应用于单个输入变量,并将结果作为新变量返回。
pub fn softplus(self: *NaiveTape, nvar: *NaiveVar) !*NaiveVar { const softplus_val = std.math.log1p(std.math.exp(nvar.val)); const softplus_var = NaiveVar.init(softplus_val); const op = Operation{ .op_type = .Softplus, .inputs = [_]*NaiveVar{nvar, undefined}, // Only one input needed, second is unused .output = softplus_var, }; self.ops = try self.allocator.realloc(self.ops, self.ops.len + 1); self.ops[self.ops.len - 1] = op; return &self.ops[self.ops.len - 1].output; }
#3d 后向法
- 该方法以与执行相反的顺序迭代操作(即从输出到输入)。
pub fn backward(self: *NaiveTape, nvar: *NaiveVar) void { nvar.grad += 1.0; var i = self.ops.len; while (i > 0) { i -= 1; const op = self.ops[i]; switch (op.op_type) { .Sum => { const output_grad = op.output.grad; op.inputs[0].grad += output_grad; op.inputs[1].grad += output_grad; }, .Prod => { const output_grad = op.output.grad; const input1_val = op.inputs[0].val; const input2_val = op.inputs[1].val; op.inputs[0].grad += input2_val * output_grad; op.inputs[1].grad += input1_val * output_grad; }, .Softplus => { const output_grad = op.output.grad; const input_val = op.inputs[0].val; op.inputs[0].grad += output_grad / (1.0 + std.math.exp(-input_val)); }, } } }
#5 主要
主要功能:
- 循环该过程 100 万次。
- 计算输入变量的总和、乘积和 softplus。
- 计算输出变量相对于输入变量的梯度。
- 打印运行循环的值、梯度和经过的时间。
pub fn main() !void { var arena = ArenaAlloc.init(pageAlloc); defer arena.deinit(); const allocator = arena.allocator(); const iterations: usize = 1000000; const start_time = time(); var i: usize = 0; while (i < iterations) : (i += 1) { var var1 = NaiveVar.init(1.0); var var2 = NaiveVar.init(2.0); var tape = NaiveTape.init(allocator); defer tape.deinit(); const sum_var = try tape.sum(&var1, &var2); const prod_var = try tape.prod(sum_var, sum_var); const softplus_var = try tape.softplus(prod_var); tape.backward(softplus_var); if (i == iterations - 1) { print("sum_var val: {d:}\n", .{sum_var.val}); print("prod_var val: {d:}\n", .{prod_var.val}); print("softplus_var val: {d:}\n", .{softplus_var.val}); print("sum_var grad: {d:}\n", .{sum_var.grad}); print("var1 grad: {d:}\n", .{var1.grad}); print("var2 grad: {d:}\n", .{var2.grad}); } } const end_time = time(); const elapsed_time = @as(f64, @floatFromInt(end_time - start_time)); print("\nElapsed time: {d:.3} ms\n", .{elapsed_time}); }
基准结果
==注意== ==:我在所有语言中应用了相同级别的代码优化==。目前我成功地将01
优化迭代应用于 4 个语言。
我实际上已经完成了02
优化,但仅针对 C,结果令人震惊:经过的时间下降到 0.3 毫秒。不过我还没有时间去实现02
版本的其余部分,一旦完成就会更新帖子。
接下来怎么办?
当我完成下一次优化迭代时,我将继续更新这篇文章。另外,我可能会写一些关于我所做的优化的系列文章,特别是在 C、Zig 和 Rust 中。
预览一下我所做的迭代:
- 00级:不启动优化,只是让程序正确运行。 (完毕)
- 01 级:优化的第一次迭代。 (完毕)
- 级别 02:第二次迭代。 (在 Zig 和 Rust 的 C.WIP 中完成)。
- 0x 级:x 次迭代…您明白了要点。
原文: https://hwisnu.bearblog.dev/autograd-in-c-zig-rust-and-pycodon/