嵌入式学习笔记 - 用泰勒公式解决 tanh函数

一 为什么要使用泰勒公式

先看一段代码

for (k = 0; k < 10; k++)

{

a += b_a[k] * (2.0 / (exp(-2.0 * ((c_a[k] * b_y1 + c_a[k + 10] * c_idx_1) + dv[k])) + 1.0) - 1.0);

}

由于这段函数包含指数运算,指数运算对于一般的单片机而言比较复杂,转换成泰勒多项式计算会大大简化计算的复杂度,减轻CPU的负担,缩短计算时间,尽管会在计算精度上有所损失

二 泰勒公式

1. 泰勒公式核心思想:用"多项式"逼近"复杂函数"

想象你是一个科学家,面对一个复杂的曲线函数,比如 f(x)=exf(x)=ex 或 cos⁡(x)cos(x)。你想在某个点(例如 x=0x=0)附近,用一个简单易懂的多项式来近似它。

为什么?

因为多项式只涉及加、减、乘乘方运算,计算非常简单,无论是人还是计算机都能轻松处理。

泰勒展开的核心目标就是:找到一个在特定点附近与复杂函数"最像"的多项式。

2 构建泰勒多项式

假设我们要在点 x=a附近逼近函数 f(x)。我们希望找到一个多项式 P(x):

P(x)=c0+c1(x−a)+c2(x−a)2+c3(x−a)3+...

关键问题是:系数 c0,c1,c2,...应该取什么值,才能让 P(x) 在 x=a 附近最像 f(x)?

答案是:让 P(x) 在 x=a处的函数值,以及尽可能多阶的导数值,都与 f(x)相等。

这里为什么都选用的x-a,而不是x,因为我们要求的是函数值跟导数。用x-a便于计算。

第0步:匹配函数值(位置)

在 x=a处,我们要求多项式等于函数值:

P(a)=f(a)

代入多项式:

c0+c1(0)+c2(0)2+⋯=f(a)  ⟹  c0=f(a)

我们得到了第1个系数: c0=f(a)

多项式等于函数值这个是条件,所以得出c0=F(a);

第1步:匹配一阶导数值(斜率)

我们希望多项式在 x=a 处的切线斜率也和原函数一样:

P′(a)=f′(a)

先求 P(x)的导数:

P′(x)=c1​+2c2​(x−a)+3c3​(x−a)2+...(第二个2.3是平方的意思)见下图,

代入 x=a:

P′(a)=c1+0+0+⋯=f′(a)  ⟹  c1=f′(a)

我们得到了第2个系数: c1=f′(a)

x=a 处的切线斜率也和原函数一样,这个是条件,所以得出c1=f′(a),具体求导数的方法要搜索幂函数求导法则,见下图,就是把指数拿下来当系数,然后指数减1。

第3步:匹配三阶导数

继续这个过程:

P′′′(a)=f′′′(a)

P′′′(x)=6c3+24c4(x−a)+...

代入 x=a:

P′′′(a)=6c3=f′′′(a)  ⟹  c3=f′′′(a)/6=f′′′(a)/3!

我们得到了第4个系数: c3=f′′′(a)/3!

3. 泰勒公式的最终形式

继续这个模式,我们就得到了著名的泰勒公式

特殊情形:当 a=0a=0 时 ,称为麦克劳林级数

三 Tanh 函数

先看一段代码

for (k = 0; k < 10; k++)

{

a += b_a[k] * (2.0 / (exp(-2.0 * ((c_a[k] * b_y1 + c_a[k + 10] * c_idx_1) + dv[k])) + 1.0) - 1.0);

}

由于这段函数包含指数运算,指数运算对于一般的单片机而言比较复杂,转换成泰勒多项式计算会大大简化计算的复杂度,减轻CPU的负担,缩短计算时间,尽管会在计算精度上有所损失

1. 数学形式

代码中的核心表达式是:

其中

即 t=w1x1+w2x2+b 的形式(这里 dv[k]是偏置项)。

2. 泰勒展开

我们要在 t=0 处展开 tanh⁡(t)。

注意不是所有函数都能用泰勒展开,tanh⁡(t)是双曲正切函数,能够使用泰勒展开式。

已知:


代入 t 的表达式

令:

那么:

展开到一阶(线性近似)

tanh(u)≈u

所以:

a≈k=0∑9​ba​[k]⋅u 见上文原代码等价式

展开到三阶

其中

最终计算得到

上式有误,知道意思即可

3. 代码表示:

换算成代码的形式可以使用deepseek,复制整段泰勒展开之前的代码进去,输入命令"将次段代码用5阶泰勒展开式替代"

1. 一阶泰勒展开(线性近似)

for (k = 0; k < 10; k++)

{

double t = c_a[k] * b_y1 + c_a[k + 10] * c_idx_1 + dv[k];

a += b_a[k] * t;

}

2. 三阶泰勒展开

for (k = 0; k < 10; k++)

{

double t = c_a[k] * b_y1 + c_a[k + 10] * c_idx_1 + dv[k];

double t_sq = t * t;

a += b_a[k] * (t - t * t_sq / 3.0);

}

3. 五阶泰勒展开(更精确)

for (k = 0; k < 10; k++)

{

double t = c_a[k] * b_y1 + c_a[k + 10] * c_idx_1 + dv[k];

double t_sq = t * t;

double t_fourth = t_sq * t_sq;

a += b_a[k] * (t - t * t_sq / 3.0 + 2.0 * t * t_fourth / 15.0);

}

4. 七阶泰勒展开(高精度)

for (k = 0; k < 10; k++)

{

double t = c_a[k] * b_y1 + c_a[k + 10] * c_idx_1 + dv[k];

double t_sq = t * t;

double t_fourth = t_sq * t_sq;

double t_sixth = t_fourth * t_sq;

a += b_a[k] * (t - t * t_sq / 3.0 + 2.0 * t * t_fourth / 15.0 - 17.0 * t * t_sixth / 315.0);

}

推荐使用

对于大多数应用,五阶展开在精度和计算效率之间提供了良好的平衡:

总结:

  1. 适用条件 :泰勒展开在 t 接近 0 时精度最高,当 |t| > 1.5 时误差会明显增大

  2. 性能考虑:原函数包含 exp 计算,泰勒展开用多项式替换可以显著提高计算速度

  3. 精度权衡 :如果 t 的范围较大,建议使用五阶或七阶展开,或者保持原函数

你可以根据 t 的典型取值范围选择合适的展开阶数。

四 为什么 exp(x) 在单片机上很慢?

1 原因

(1) 复杂的数学运算

  • exp(x) 不是像加减乘除那样的基本指令

  • 需要一系列乘、加、除和迭代计算

  • 通常通过泰勒展开、查表法或CORDIC算法实现,这些都涉及多次循环和计算

(2) 缺乏硬件支持

  • 大多数低端单片机没有硬件浮点单元(FPU)

  • 即使有FPU,exp() 也通常不是单指令操作

  • 需要软件库实现,计算步骤繁多

(3) 内存和速度限制

  • 精确计算需要高精度迭代,消耗大量CPU周期

  • 可能还需要存储常数表,占用宝贵的内存


2. 实际性能对比

举个例子,在常见的STM32系列单片机上(无硬件FPU):

运算类型 大致CPU周期 时间 @ 72MHz
整数加法 1 cycle ~0.014μs
浮点乘法 ~20 cycles ~0.28μs
exp(x) ~1000-5000 cycles ~14-70μs

可以看到,一次 exp() 计算可能相当于几百次甚至几千次基本运算!


3. 你在原始代码中的情况

你的代码:

c

复制代码
a += b_a[k] * (2.0 / (exp(-2.0 * t) + 1.0) - 1.0);

这实际上是在计算 tanh(t),其中涉及:

  1. 一次乘法:-2.0 * t

  2. 一次指数:exp(...)主要瓶颈!

  3. 一次加法:... + 1.0

  4. 一次除法:2.0 / ...

  5. 一次减法:... - 1.0

指数运算 exp() 消耗了绝大部分计算时间。


4. 解决方案:为什么用泰勒展开

这就是为什么之前建议你用泰勒展开来替换:

原始代码(慢):

c

复制代码
// 包含昂贵的 exp() 调用
result = 2.0 / (exp(-2.0 * t) + 1.0) - 1.0;

泰勒展开(快):

c

复制代码
// 只用加减乘,没有 exp()
double t3 = t * t * t;
double t5 = t3 * t * t;
result = t - t3/3.0 + 2.0*t5/15.0;  // 五阶近似

性能提升可能达到10倍甚至100倍!


5. 其他优化方法

除了泰勒展开,还有:

(1) 查表法

c

复制代码
// 预计算 tanh 值表
const float tanh_table[100] = { ... };

// 使用时直接查表
int index = (int)(t * 10 + 50);  // 映射到数组索引
if (index < 0) index = 0;
if (index > 99) index = 99;
result = tanh_table[index];

(2) 分段近似

  • 小值时用泰勒展开

  • 大值时直接返回 ±1(因为 tanh 饱和)

(3) 有理函数近似

用多项式比值来获得更好的精度:

c

复制代码
// Pade 近似等

6. 实际建议

对于你的具体应用:

  1. 如果 t 的范围较小(如 |t| < 2.0):

    • 使用3阶或5阶泰勒展开

    • 在精度和速度间取得良好平衡

  2. 如果需要高精度

    • 使用查表法 + 线性插值

    • 或者使用优化的近似公式

  3. 如果性能极其敏感

    • 考虑使用定点数运算代替浮点数

    • 使用汇编优化关键部分


总结

是的,单片机计算 exp() 等指数运算非常耗时,应该尽量避免在循环或实时处理中使用。泰勒展开、查表法和各种近似方法是嵌入式开发中常用的优化手段。

在你的代码中,用泰勒多项式替换 tanh(t) 可以显著提高运行速度,这是嵌入式优化中的经典技巧!

相关推荐
闲人编程5 分钟前
CPython与PyPy性能对比:不同解释器的优劣分析
python·算法·编译器·jit·cpython·codecapsule
杜子不疼.11 分钟前
【C++】深入解析AVL树:平衡搜索树的核心概念与实现
android·c++·算法
小武~13 分钟前
Leetcode 每日一题C 语言版 -- 88 merge sorted array
c语言·算法·leetcode
e***U82016 分钟前
算法设计模式
算法·设计模式
charlie1145141911 小时前
使用 Poetry + VS Code 创建你的第一个 Flask 工程
开发语言·笔记·后端·python·学习·flask·教程
菥菥爱嘻嘻1 小时前
langchain学习-RAG+prompt+OutPutParse
学习·langchain·prompt
Jing_jing_X1 小时前
ChatGPT 四种模式:普通对话、推理思考、深度研究、学习模式有什么区别?
人工智能·学习·chatgpt
AA陈超1 小时前
Lyra项目中的输入系统
c++·笔记·学习·游戏·ue5·lyra
徐子童1 小时前
数据结构----排序算法
java·数据结构·算法·排序算法·面试题
hansang_IR1 小时前
【记录】四道双指针
c++·算法·贪心·双指针