☰
Python绘制tanh函数图像:从数学性质到matplotlib可视化实践
2026/10/14 5:13:30 网站建设 项目流程

1. 项目整体设计与思路拆解

提笔写这篇分享的起因非常简单:前两天帮某高校的同学排查一个数值计算作业,里面有一段代码是把激活函数替换成tanh(x)后做拟合,结果图像画出来怎么看怎么不对劲。我让他把图像绘制脚本发过来,扫了两眼就知道问题出在哪了——坐标范围取得太极端,他把横轴拉到了正负 50,而tanh(x)在|x| > 3左右就已经贴着 ±1 走,图像两端变成两根又平又直的“地平线”,中段变化被压成一条铅笔芯粗细的竖线。这种图拿来投论文、放汇报,第一眼就给评审留下“这人没理解函数特性”的印象。

这个项目标题叫“FuncPlotCalc:绘制双曲正切 y=tanh(x) 函数”,听起来简单得像热身题,但我把整个拆解过程走下来后发现,它其实非常适合当成“函数可视化”的一个标准案例来解剖。tanh(x)不算复杂,写一行公式就知道表达式,但要把它画“对”、画“好看”、画到能支撑后续分析和报告,至少涉及四层问题:

第一层是数学层,你要清楚这个函数的值域、单调性、奇偶性、渐近线、拐点这些基本性质。不清楚这些,你连坐标范围都不会设。第二层是工具层,Python 生态里画图首选matplotlib,代码本身不长,但坐标系、刻度、网格、线型、标注这些参数组合起来,细节非常多。第三层是视觉层,同一个函数,横轴范围从 [-1, 1] 改成 [-5, 5],或者 [-20, 20],呈现出来的“形状”完全不同,甚至会造成误导。第四层是应用层,tanh(x)是神经网络里经典的双曲正切激活函数,也是双曲函数家族里和工程结合最紧密的一个。你需要知道图像哪里该突出、哪里可以淡化,才能让图像真正为结论服务。

这篇文章面向的读者很广:刚接触 Python 绘图的学生、做数值分析的工程师、把tanh当激活函数用的算法初学者,都可以参考。我会把整个项目从思路拆解到代码实现到坑点排查完整走一遍,重点是回答三个“为什么”:为什么tanh(x)的图像长这样、为什么坐标范围要这样选、为什么图要这样画。这些东西弄明白了,以后你画任何带渐近线的函数,比如sigmoid、arctan、1/(1+e^{-x}),都能一次画对。

聊到这儿,我顺便把项目最终的成果先亮个底:用matplotlib绘制tanh(x)在 [-5, 5] 区间的图像,加上水平渐近线y = 1和y = -1,标注原点处的切线斜率,再叠加几条不同参数下的变换曲线做对比。整个过程代码不到 60 行,跑完输出一张可以直接用于报告的高质量图片。接下来我把每一步拆开讲。

2. 核心数学性质解析:动手画图前必懂的四件事

2.1 双曲正切的定义与计算路径

tanh(x)的全称是双曲正切函数,定义式是:

tanh(x) = sinh(x) / cosh(x)

其中sinh(x) = (e^x - e^(-x)) / 2,cosh(x) = (e^x + e^(-x)) / 2。小学除法大家都会,但这里有个工程上的细节值得先说清楚:如果你直接用定义式去算数值,在x很大的时候会遇到一个问题。e^x和e^(-x)的绝对值差距悬殊,x = 20时e^20 ≈ 4.85 × 10^8,而e^(-20)小到可忽略,cosh(20)和sinh(20)几乎相等,除法在浮点运算里会引入严重的精度损失。所以实际计算数值时,标准数学库都会改用等价形式:

tanh(x) = (e^(2x) - 1) / (e^(2x) + 1)

这个形式在x > 0的区域表现稳定,但如果x < 0且绝对值大,e^(2x)趋向 0,分子趋向 -1,分母趋向 1,结果接近 -1,依然稳定。更稳妥的做法是分区间处理,但 Python 的math.tanh()内部已经做了这些防溢出处理,我们绘图时直接调用即可,不用自己实现。

顺带补充一个容易被初学者忽略的点:tanh(x)和三角函数里的正切tan(x)没有任何亲戚关系。tan(x)是周期函数,有垂直渐近线,值域是全体实数;而tanh(x)单调递增,有水平渐近线,值域被牢牢压在开区间 (-1, 1) 里。两者的图像形状乍看有点接近,但本质完全不同。后文我会专门拿出一节讲“为什么图像看起来像 S 型曲线却又不完全像 sigmoid”。

2.2 值域、单调性与奇偶性:图像形状的底层约束

tanh(x)的值域是 (-1, 1),这意味着无论x取多大,函数值永远不可能等于或超过 1,也不会低于 -1。这点直接决定了绘图的纵轴范围:ylim至少要覆盖 [-1.2, 1.2] 左右,才能让曲线和渐近线之间留出呼吸空间。如果你把纵轴设成 [-2, 2] 甚至更大,曲线会被压扁成一条水平线,视觉上完全看不出单调性,这个问题我在开头提到的案例里见过太多次。

单调性方面,tanh(x)在实数域上严格单调递增。严格递增的证明用导数看一眼就明白:

tanh'(x) = 1 - tanh^2(x)

因为tanh^2(x) < 1恒成立(值域决定了它到不了 ±1),所以导数始终为正。这个性质很重要:当tanh被用作神经网络激活函数时,单调性保证了“输入越大输出越大”的语义,不会出现输出随输入震荡的情况。

奇偶性方面,tanh(x)是奇函数,即tanh(-x) = -tanh(x)。这个性质用定义式直接验证:sinh是奇函数,cosh是偶函数,奇函数除以偶函数结果是奇函数。图像上表现为曲线关于原点中心对称。在绘制时,如果你发现画出来的曲线不是中心对称的,不用怀疑数学,先检查代码:大概率是x数组的设置有问题,比如左端点不是 -a、右端点不是 a,或者端点间距不均匀。

2.3 渐近线与拐点:图像上最值得标注的两处特征

水平渐近线是tanh(x)图像上最直观的特征,当x → +∞时tanh(x) → 1,当x → -∞时tanh(x) → -1。但真正画图时你会发现一个有意思的现象:tanh(3) ≈ 0.99505,tanh(5) ≈ 0.99991。也就是说,横轴走到 3 的时候,曲线已经完成了 99.5% 的全程变化;走到 5 的时候,基本就是贴着渐近线在走。所以如果你想展示一个“标准 S 型全貌”,横轴范围选 [-5, 5] 已经绰绰有余。范围拉到 [-10, 10] 也不会错,但多出来的区间里曲线几乎是平的,信息量趋近于零。

另一个值得标注的特征是原点 (0, 0),这个点是曲线的拐点,也是中心对称点。在原点处,导数tanh'(0) = 1 - tanh^2(0) = 1,这意味着曲线在原点处的切线斜率是 1,图像以 45 度角穿过原点。这个特性很优雅:tanh在原点附近表现得像一个线性函数y = x,所以当神经网络输入落在零附近时,tanh近似线性、梯度稳定,这比后面要讲的 sigmoid 在某些场景下表现更好。

我在绘图代码里特意加了一条辅助切线y = x(在原点附近),并把这条切线用虚线单独标出来。这样做的好处非常直观:读者一眼就能看出函数在原点区域的线性段有多长,间接理解了“为什么 tanh 在深度学习里能缓解梯度消失”。

2.4 与 sigmoid 的对比:为什么用 tanh 而不是 sigmoid

很多教程在讲激活函数时,习惯把sigmoid和tanh放一起对比。sigmoid(x) = 1 / (1 + e^(-x)),值域 (0, 1),也是 S 型曲线。但两者有一个关键差异:sigmoid的输出均值不是零,而是 0.5。这个“非零均值”在神经网络训练时会带来一种被称为“偏移效应”的问题:后一层神经元的输入总是正的,这会导致梯度更新时偏向同一个方向,收敛变慢。而tanh的值域是 (-1, 1),输出零均值,能天然缓解这个现象。这也是为什么很多经典网络结构在隐藏层更青睐tanh而非sigmoid的原因之一。

从绘图角度看,tanh的图像比sigmoid更“对称美观”,居中在原点,左右对称,视觉上天然适合展示。而sigmoid需要把坐标原点拆到(0, 0.5)上分析,多一层换算。所以这个项目选tanh作为绘制对象,除了函数本身重要之外,“画起来能讲清楚的事更多”也是原因之一。

3. 核心实操环节:从环境准备到代码落地的完整过程

3.1 工具选型:为什么用 Python + matplotlib

函数绘图工具可选的很多:MATLAB 的绘图功能很强,但授权和部署成本摆在那里;GeoGebra 适合交互演示,但难以批量产出工程图;Excel 画简单折线可以,但坐标轴控制不自由,做不了渐近线、标切线、定制刻度这些精细操作。综合来看,Python +matplotlib是最平衡的方案:开源免费、代码可控、出图质量高,还能无缝接入numpy做数值计算。

如果你只是想快速出个图跑通流程,用 Jupyter Notebook 逐个 cell 执行最方便;如果想批量产出或接入自己的数据处理流程,推荐写成独立脚本。我用的是脚本方式,好处是改参数后重新运行即出新图,便于反复调整坐标范围、线型、字号。

本项目用到的主要库只有两个:numpy负责生成x数组并计算函数值,matplotlib负责绘图。numpy的linspace和tanh都是向量化操作,一条语句就能生成几百个点,比手动 for 循环快得多也简洁得多。

3.2 分步骤代码实现与参数说明

第一步,导入库并生成自变量数组:

import numpy as np import matplotlib.pyplot as plt # 生成 x 轴数据:从 -5 到 5,均匀取 400 个点 x = np.linspace(-5, 5, 400) y = np.tanh(x)

linspace(-5, 5, 400)的核心是“闭区间均匀采样”。为什么选 400 个点?因为画 S 型曲线时,在原点附近斜率最大,需要足够多的点来保证曲线平滑;在两端渐近线附近,函数值几乎不变,点再多也看不出区别。400 个点兼顾了曲线平滑和计算开销,实测放大后没有任何锯齿感。如果你只用 50 个点,曲线在拐弯处就会出现肉眼可见的折线段,非常掉价。

第二步,绘制主曲线并设置基本样式:

plt.figure(figsize=(8, 6)) plt.plot(x, y, label=r'$y = \tanh(x)$', color='#2E86C1', linewidth=2.5)

figsize=(8, 6)把画布设为横长纵短的比例,这样曲线不会被拉得过高或者过扁。线宽设成 2.5 是为了在报告里缩小图片后依然清晰。颜色选了一个偏蓝的色号,不是默认的纯蓝色,避免和其他标注元素撞色。

第三步,添加渐近线和切线标注:

# 水平渐近线 plt.axhline(y=1, color='gray', linestyle='--', linewidth=1.2, label='渐近线 y = 1') plt.axhline(y=-1, color='gray', linestyle='--', linewidth=1.2, label='渐近线 y = -1') # 原点切线 y = x,限定在 [-2, 2] 区间 x_tangent = np.linspace(-2, 2, 10) plt.plot(x_tangent, x_tangent, color='red', linestyle=':', linewidth=1.5, label='原点切线 y = x')

渐近线用虚线,切线用点线,主曲线用实线,三种线型可以明确区分层次。细心的读者会发现tanh在原点处的切线斜率正是 1,所以切线就是y = x。在图上把这条线叠加进去,标注出“原点附近线性近似”的几何意义,整个图的信息量立刻提升了。

第四步,坐标轴范围与刻度定制:

plt.xlim(-5.5, 5.5) plt.ylim(-1.2, 1.2) plt.xticks(np.arange(-5, 6, 1)) plt.yticks(np.arange(-1.0, 1.1, 0.5))

xlim比数据范围略宽是为了避免曲线端点贴到画布边缘;ylim设为 [-1.2, 1.2] 是因为函数值到不了 ±1,要留出渐近线的展示空间,但不能留太大,否则 S 型会被压扁。xticks用 1 步长、yticks用 0.5 步长,刻度密度适中。

第五步,添加网格、图例和标签:

plt.grid(True, linestyle='--', alpha=0.6) plt.legend(loc='upper left') plt.xlabel('x', fontsize=12) plt.ylabel('tanh(x)', fontsize=12) plt.title(r'双曲正切函数 $y = \tanh(x)$ 图像', fontsize=14)

网格的透明度调到 0.6,既有参照作用又不抢主线的戏。图例放在左上角,因为右上角被渐近线y = 1和曲线贴近的部分占住了,放左上角最干净。

第六步,保存并展示:

plt.savefig('tanh_function.png', dpi=300, bbox_inches='tight') plt.show()

dpi=300是高清出版的底线,投稿到期刊或打印纸质报告都够用。bbox_inches='tight'会自动裁剪掉多余留白,让图片四周的空白区域最小化。这一步虽然只有两行,但习惯了默认设置的人很容易忽略,等把图插进文档里才发现四边一大圈白边,非常难看。

3.3 扩展实验:叠加对比曲线,让图像更有说服力

基础函数画完不算完,我还顺手做了一组扩展对比:绘制tanh(x)、sigmoid(x)映射到 [-1, 1] 的版本2*sigmoid(x)-1,以及x在 [-1, 1] 区间的线性参考线。生成的图像能直观看出tanh和“平移缩放后的 sigmoid”并不重合,两者只在原点附近接近,离开原点后偏离逐渐加大。

这组扩展代码并不复杂,核心是:

x2 = np.linspace(-5, 5, 400) sigmoid = 1 / (1 + np.exp(-x2)) sigmoid_scaled = 2 * sigmoid - 1 plt.plot(x2, sigmoid_scaled, label='2*sigmoid(x)-1', color='green', linestyle='-.', linewidth=1.8)

看到对比图像后,很多人才真正理解一个事:tanh和sigmoid不是简单的平移关系,tanh的导数在零附近是 1,而2*sigmoid(x)-1的导数在零附近是 0.5。这意味着即便经过线性映射让值域相同,两者的梯度特性依然有本质区别。这类延伸分析,往往是画一个“基础图像”时最容易忽略但最加分的地方。

4. 常见问题与排查技巧实录

4.1 问题一:曲线看起来像两段水平线加一条直线,S 型不明显

这个现象通常发生在横轴范围过大时。你把xlim设成 [-50, 50],而tanh(5)已经接近 0.9999,于是 [-50, -5] 这段区间曲线恒等于 -1,[5, 50] 这段恒等于 1,中间 [-5, 5] 的变化被挤到画布中心很小一块区域,肉眼看上去就像台阶一样,完全失去了 S 型的美感。

解决思路很简单:根据函数值的变化速率反推坐标范围。因为tanh的值域收窄速度极快,展示核心形态只需要取 ±5 附近;如果要侧重展示“渐近线逼近”的效果,取 ±8 也够了。我个人的习惯是先用默认范围跑一版,观察曲线两端是否已经走平,再按需压缩。记住一个原则:图像应该让读者一眼看到函数的关键行为,而不是用大坐标范围掩盖它。

4.2 问题二:计算大 x 值时报溢出或出现 nan

理论上tanh的定义域是全实数,但实际计算时不同语言的处理策略不同。Python 的numpy.tanh和math.tanh内部实现稳定处理,传入x = 1000也能正确返回 1.0。但在某些计算环境或旧版本库中,如果直接调(exp(2*x) - 1) / (exp(2*x) + 1)这种手动形式,exp(2000)会直接报 OverflowError。

规避方案有三层:最省事的是直接用库函数;如果必须手写公式,则对正数改用1 - 2 / (exp(2*x) + 1)的形式,避免exp膨胀到溢出;更稳妥的做法是分段处理,x > 20直接返回 1.0,x < -20返回 -1.0,中间区域用原始公式。这层处理在实际工程中经常遇到,尤其在嵌入式设备上跑代码时,标准库未必完整,手写数值方案就得心里有数。

4.3 问题三:直线段过粗,曲线边缘锯齿严重

出现锯齿通常是采样点太少。plot默认用直线段把相邻点连起来,如果x只有 20 个点,在斜率变化剧烈的原点附近就会看到明显的折线痕迹。把采样点数从 100 提到 400 是一个立竿见影的修复方式。另有一个隐藏因素:保存时dpi设置过低,锯齿感会被放大。如果你用savefig保存且不指定dpi,默认可能是 100,屏幕预览勉强能看,放大到文档里就会露馅。建议dpi=300起步。

如果图像里曲线边缘出现“毛刺”而非平滑锯齿,那就要检查数据源了,可能是x数组不是均匀采样,或者数据里混入了 NaN。numpy里可以用np.isfinite(y)做一次过滤,把非有限值定位出来后单独处理。

4.4 问题四:坐标轴十字线遮挡曲线

matplotlib默认绘制的坐标轴是上下左右四根边框线,曲线经过时会被边框裁切。视觉上最好看的方式是使用spines把上、右边框隐藏,并把左、下边框移到原点位置,形成“十字坐标系”的效果。代码很简单:

ax = plt.gca() ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['left'].set_position(('data', 0)) ax.spines['bottom'].set_position(('data', 0))

这样设置之后,曲线穿过坐标轴的位置正好在原点的十字交叉处,S 型的对称性展现得更充分。加上x、y轴的刻度标签正常展示,整张图干净得像教材插图。这个细节我踩过坑之后就一直沿用,推荐大家都试试。

4.5 问题五:图例遮挡曲线关键区域

当图里有主曲线、渐近线、切线三条图例时,legend的默认位置很可能压住曲线拐点。热门的解决技巧是把legend位置设为'best'让matplotlib自动计算空白区域,但自动计算有时也不理想。更可靠的方案是手动指定位置,配合fancybox和framealpha参数让图例背景半透明,即使覆盖在曲线上,也不会完全挡住视觉信息:

plt.legend(loc='upper left', fancybox=True, framealpha=0.6)

经验之谈:绘图时尽量把图例放到“信息密度低”的区域,比如左上方或右下方。对于tanh图像,右上角在渐近线附近属于低变化区,但被图例占住后,读者会忽略渐近线的存在感,所以左上角更合适。

5. 场景应用延伸与实践心得

5.1 神经网络激活函数场景下的图像解读

tanh(x)最常见的应用场景是神经网络中的激活函数。把它作为激活函数使用时,关注点不是“图像好不好看”,而是三个数:值域是 (-1, 1)、原点导数是 1、取值范围关于 0 对称。这三个数分别对应:输出不会发散、梯度在初始阶段能有效传播、输出零均值减缓偏移效应。当你把tanh激活函数在不同初始化权重下的输出分布画成直方图时,能直观看到经过tanh后数据被压缩在 [-1, 1] 区间内分布。

这个项目标题里出现的“FuncPlotCalc”,虽然听起来像自造的工具名,但它本质上代表的是一类需求:把数学函数变成可视化资产。我见过不少人在理解激活函数时只看公式和文字描述,图像直觉完全没建立起来。等真正调网络时,梯度消失、输出饱和这些问题,其实都能从图像上预先看出来。tanh图像两端进入饱和区后,导数值趋近 0,梯度传不下去,这就是图像在“说话”。所以这个课题的核心价值从来不在于“画一条曲线”,而在于建立起“函数图像 <-> 数值性质 <-> 应用表现”三者之间的对应关系。

5.2 数值计算场景中的图像校验思路

tanh图像还有一类用途容易被忽略:作为计算校验的基准。比如你实现了一个数值微分算法,想验证它的正确性,可以直接拿tanh做测试函数,因为它的解析导数是1 - tanh²(x),可以精确对拍。用不同步长h算(tanh(x+h) - tanh(x-h)) / (2h),再与解析导数值对比,能快速评估算法的精度和适用步长范围。这个实验结合图像展示,效果会非常好:画一张x从 -3 到 3 的导数对比图,你会发现中心差分在原点附近精度极高,但靠近两端饱和区时,由于tanh数值本身变化极小,相对误差会迅速增大。

我实际带过的某项目就吃过这个亏:同事用tanh验证自动微分库,初值取在 x = 10 附近,怎么调都觉得导数算得不对,后来画了误差曲线才发现问题不在代码,而是函数在 x = 10 处的解析导数值小到 10^(-8),任何浮点误差都会被放大成“看起来很大的错误”。这类经验,只有真正动手画图的人才会积累到。

5.3 我对这类绘图项目的几点体会

画tanh(x)这件事,如果只把它当作一条简单曲线的绘制任务,那就太可惜了。我在反复操作中总结出一个方法论:任何函数可视化项目,都值得按“性质预判 — 绘图验证 — 异常追溯 — 场景延伸”四步走。先根据公式推导出值域、单调性、奇偶性、渐近线位置,然后绘图验证,如果图里出现了预判之外的形态,先别急着改代码,回到数学层找原因;确认无误后再考虑美观和排版;最后落到应用场景,想想这张图能说明什么问题。

从时间投入来看,真正写绘图代码只占一小时的三分之一,其余时间都花在理解函数数学性质、排查坐标范围合理性、调整标注信息优先级上。这些工作看似“不产出代码”,但恰恰决定了最终图像的专业程度。就像写文章,敲键盘只占 10% 的时间,另外 90% 都在想清楚要表达什么。

根据我个人经验,如果你以后要给团队做内部技术分享,或者写教程博客,tanh(x)是个极好的教学案例。它足够简单,能让听众把注意力放在绘图技巧上;又足够典型,能牵出渐近线、拐点、对称性、饱和区、梯度消失这一长串概念。一张图串起一整条知识链,性价比非常高。

最后再分享一个小技巧:绘图脚本里把所有关键参数,比如坐标范围、采样点数、渐近线位置、切线斜率,全部提到脚本开头,用变量定义好。这样后续改图时不需要在几十行代码里翻找,而且参数命名本身就在提醒你“为什么选这个值”。这个习惯,我从画第一个函数图开始保持到现在,省下的时间足够多画几百张图了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询