☰
Python 实战:手绘 sigmoid 与 tanh 曲线,搞懂梯度消失
2026/10/2 4:44:02 网站建设 项目流程

简介:这份PDF资料面向深度学习入门者与神经网络初学者,聚焦Sigmoid与Tanh两种常见激活函数的可视化实现,帮助读者理解其数学表达式、值域特征及在模型中的非线性作用。资源以逐行代码详解的方式,演示了分开绘制两条曲线与合并绘制在同一坐标系中的两种方案,并借助tanh(x)=2σ(2x)-1的数学推导简化绘图逻辑,便于直观比较两者的形状差异与梯度分布特点。压缩包内仅含1个PDF文件,大小约119KB,内容紧凑,适合作为手边速查与练习参考。目前已有4111人学习下载,说明该主题在入门阶段具有较高关注度。读者可从中获得可直接运行的matplotlib绘图代码、坐标轴与图例的定制技巧,以及关于二分类任务选Sigmoid、其他场景考虑Tanh的选型思路,为后续理解梯度消失等问题打下基础。

1. 从一条曲线看懂 sigmoid 与 tanh:为什么值得亲手画一遍

很多人第一次接触激活函数,是在教程里看到一张现成的图,然后记住「sigmoid 输出 0 到 1,tanh 输出 -1 到 1」就翻篇了。真到调模型的时候,梯度消失、输出不以零为中心、饱和区把梯度压成接近零,这些问题全冒出来,才发现当初那张图根本没看进去。这篇就干一件事:用 Python 把 sigmoid 和 tanh 分开画、合起来画,逐行拆代码,把两条曲线的形状、饱和区、对称性、导数关系全部落到你能自己跑出来的图上。适合刚入门深度学习、想搞懂激活函数到底长什么样的新手,也适合想快速复现对比图的老手。画图不是目的,看懂曲线背后的数值行为才是。

2. 先把公式和数值边界钉死:sigmoid 与 tanh 到底差在哪

2.1 两个函数的数学定义与值域

sigmoid 的标准形式是 σ(x) = 1 / (1 + e^(-x))。它的输出永远落在 (0, 1) 开区间内,x 趋近正无穷时逼近 1,趋近负无穷时逼近 0,x=0 时正好是 0.5。这个 0.5 很关键,它意味着 sigmoid 的输出不以零为中心,后面接全连接层时,输入全为正数会让权重梯度方向出现锯齿式更新,收敛变慢。

tanh 的标准形式是 tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x))。它的输出落在 (-1, 1) 开区间,x=0 时输出 0,关于原点对称。正因为输出以零为中心,tanh 在隐藏层的表现通常比 sigmoid 好,收敛更快。但两者有个共同的毛病:当 |x| 变大时,曲线进入饱和区,导数趋近于零,反向传播时梯度被反复相乘,很容易消失。

这里有个容易混的点:tanh 和 sigmoid 不是两个独立的东西,它们之间有恒等关系 tanh(x) = 2·σ(2x) - 1。也就是说,tanh 本质上是 sigmoid 缩放平移后的版本。理解这层关系,你就能明白为什么两者的曲线形状那么像,只是 tanh 被「拉宽」并「下移」了。

2.2 为什么选这两个函数来画,而不是 ReLU 或 GELU

现在主流隐藏层激活函数早就换成了 ReLU、GELU、SiLU 这些,但 sigmoid 和 tanh 依然是理解激活函数的最佳起点。原因有三:第一,它们处处可导,导数形式简洁,适合手推反向传播;第二,它们的饱和特性是梯度消失问题的经典案例,看懂它们再看 ReLU 的「死区」问题会顺很多;第三,sigmoid 至今仍活跃在二分类输出层和门控机制里,tanh 在 RNN 的隐藏状态里也没完全退场。

所以画这两条曲线,不是为了怀旧,是为了建立对「激活函数形状如何影响训练」的直觉。你后面看到 GELU 的平滑曲线、SiLU 的非单调性,都能拿这两条曲线做参照。

2.3 用 NumPy 生成数据并验证边界值

动手之前先把数值算一遍,确认公式没记错。下面这段代码生成 -10 到 10 的等间距点,分别算 sigmoid 和 tanh,并打印几个关键位置的输出。

import numpy as np # 生成 -10 到 10 之间 1000 个等间距点 x = np.linspace(-10, 10, 1000) # sigmoid 定义 sigmoid = 1 / (1 + np.exp(-x)) # tanh 定义,也可以直接用 np.tanh(x) tanh = (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x)) # 验证关键点 for val in [-10, -1, 0, 1, 10]: idx = np.argmin(np.abs(x - val)) print(f"x={val:>3} sigmoid={sigmoid[idx]:.6f} tanh={tanh[idx]:.6f}")

运行后你会看到 x=0 时 sigmoid 输出 0.5、tanh 输出 0;x=10 时 sigmoid 已经到 0.999955,tanh 到 0.999999;x=-10 时对称地逼近 0 和 -1。这就是饱和区的直观体现——输入稍微大一点,输出几乎不再变化。

参数说明:np.linspace(-10, 10, 1000)的三个参数分别是起点、终点、点数,点数越多曲线越平滑,画图用 1000 足够。np.exp对数组做逐元素指数运算,注意当 x 很大时np.exp(x)可能溢出,所以 tanh 更推荐直接用np.tanh,它内部做了数值稳定处理。这也是一个踩坑点:自己手写 tanh 公式时,x 超过 700 左右np.exp(x)就会返回 inf,导致结果是 nan。

3. 分开画:用 Matplotlib 把 sigmoid 和 tanh 各自画清楚

3.1 画 sigmoid 的最小可用代码

先单独画 sigmoid,把坐标轴、网格、关键点标注都加上,这样一张图就能说明问题。

import numpy as np import matplotlib.pyplot as plt x = np.linspace(-10, 10, 1000) sigmoid = 1 / (1 + np.exp(-x)) plt.figure(figsize=(8, 5)) plt.plot(x, sigmoid, color='#1f77b4', linewidth=2, label='sigmoid(x)') plt.axhline(y=0.5, color='gray', linestyle='--', linewidth=0.8) plt.axvline(x=0, color='gray', linestyle='--', linewidth=0.8) plt.scatter([0], [0.5], color='red', zorder=5) plt.annotate('(0, 0.5)', xy=(0, 0.5), xytext=(1.5, 0.35), arrowprops=dict(arrowstyle='->', color='red')) plt.title('Sigmoid Activation Function') plt.xlabel('x') plt.ylabel('sigmoid(x)') plt.grid(True, alpha=0.3) plt.legend() plt.tight_layout() plt.savefig('sigmoid_alone.png', dpi=150) plt.show()

逻辑说明:plt.axhline和plt.axvline画出 y=0.5 和 x=0 的参考线,帮你一眼看出中心点位置。plt.scatter单独标出 (0, 0.5) 这个点,plt.annotate加箭头和文字。zorder=5保证散点画在曲线上面。dpi=150让保存的图足够清晰,直接放进文档也不糊。

参数说明:figsize=(8, 5)控制画布宽高比,8:5 比较适合展示单条曲线。linewidth=2让曲线够粗,投影或截图后依然清楚。alpha=0.3让网格线变淡,不抢曲线视觉。如果你在 Jupyter 里跑,plt.show()会直接内嵌显示;在脚本里跑,savefig会存成文件。

3.2 画 tanh 并对比饱和速度

tanh 的画法几乎一样,但要注意它的值域是 (-1, 1),所以参考线画在 y=0。

import numpy as np import matplotlib.pyplot as plt x = np.linspace(-10, 10, 1000) tanh = np.tanh(x) plt.figure(figsize=(8, 5)) plt.plot(x, tanh, color='#d62728', linewidth=2, label='tanh(x)') plt.axhline(y=0, color='gray', linestyle='--', linewidth=0.8) plt.axvline(x=0, color='gray', linestyle='--', linewidth=0.8) plt.scatter([0], [0], color='blue', zorder=5) plt.annotate('(0, 0)', xy=(0, 0), xytext=(1.5, 0.3), arrowprops=dict(arrowstyle='->', color='blue')) plt.title('Tanh Activation Function') plt.xlabel('x') plt.ylabel('tanh(x)') plt.grid(True, alpha=0.3) plt.legend() plt.tight_layout() plt.savefig('tanh_alone.png', dpi=150) plt.show()

逻辑说明:这里直接用np.tanh(x)而不是手写公式,避免溢出问题。参考线画在 y=0,因为 tanh 关于原点对称。散点标出 (0, 0),这是 tanh 的中心点。

参数说明:颜色用#d62728和 sigmoid 的#1f77b4区分开,后面合起来画时也保持这个配色,读者一眼能对上。np.tanh是 NumPy 内置函数,内部对数值稳定性做了处理,比手写公式安全。

画完两张图放一起看,你会发现 tanh 在 x=±2 附近就已经接近饱和,而 sigmoid 要到 x=±5 左右才明显压平。这意味着 tanh 的梯度消失来得更早,但它的输出范围更大、以零为中心,实际训练中往往收敛更快。这个「饱和早但对称」的权衡,是选型时要记住的。

3.3 把导数曲线也画出来,理解梯度消失

光看原函数不够,导数的形状才决定梯度怎么传。sigmoid 的导数是 σ(x)·(1-σ(x)),tanh 的导数是 1-tanh²(x)。把导数画出来,饱和区的问题一目了然。

import numpy as np import matplotlib.pyplot as plt x = np.linspace(-10, 10, 1000) sigmoid = 1 / (1 + np.exp(-x)) tanh = np.tanh(x) sigmoid_grad = sigmoid * (1 - sigmoid) tanh_grad = 1 - tanh ** 2 plt.figure(figsize=(8, 5)) plt.plot(x, sigmoid_grad, color='#1f77b4', linewidth=2, label="sigmoid'(x)") plt.plot(x, tanh_grad, color='#d62728', linewidth=2, label="tanh'(x)") plt.axhline(y=0, color='gray', linestyle='--', linewidth=0.8) plt.title('Derivatives of Sigmoid and Tanh') plt.xlabel('x') plt.ylabel('gradient') plt.grid(True, alpha=0.3) plt.legend() plt.tight_layout() plt.savefig('gradients.png', dpi=150) plt.show()

逻辑说明:sigmoid 导数最大值在 x=0 处为 0.25,tanh 导数最大值在 x=0 处为 1。这意味着 tanh 在原点附近的梯度比 sigmoid 大 4 倍,反向传播时信号更强。但两者在 |x|>4 之后都趋近于零,这就是梯度消失的根源。

参数说明:sigmoid * (1 - sigmoid)是逐元素乘法,NumPy 会自动广播。tanh ** 2是逐元素平方。画在一起对比时,纵轴范围会自动适配,不需要手动设 ylim。

提示:如果你发现导数曲线在两端有轻微震荡,那是np.linspace点数不够导致的,把 1000 调到 5000 就平滑了。

4. 合起来画:一张图对比 sigmoid 与 tanh 的形状差异

4.1 双曲线叠加与图例配置

分开画看细节,合起来画看关系。把两条曲线放在同一坐标系里,值域差异、对称性、饱和速度的对比会非常直观。

import numpy as np import matplotlib.pyplot as plt x = np.linspace(-10, 10, 1000) sigmoid = 1 / (1 + np.exp(-x)) tanh = np.tanh(x) plt.figure(figsize=(9, 6)) plt.plot(x, sigmoid, color='#1f77b4', linewidth=2.5, label='sigmoid(x)') plt.plot(x, tanh, color='#d62728', linewidth=2.5, label='tanh(x)') plt.axhline(y=0, color='black', linestyle='-', linewidth=0.6) plt.axhline(y=0.5, color='#1f77b4', linestyle=':', linewidth=0.8, alpha=0.6) plt.axhline(y=-1, color='#d62728', linestyle=':', linewidth=0.8, alpha=0.6) plt.axhline(y=1, color='#d62728', linestyle=':', linewidth=0.8, alpha=0.6) plt.title('Sigmoid vs Tanh') plt.xlabel('x') plt.ylabel('f(x)') plt.grid(True, alpha=0.3) plt.legend(loc='upper left', fontsize=11) plt.tight_layout() plt.savefig('sigmoid_vs_tanh.png', dpi=150) plt.show()

逻辑说明:两条曲线用不同颜色和线宽区分。axhline画出 y=0、y=0.5、y=-1、y=1 四条参考线,其中 y=0.5 是 sigmoid 的中心,y=-1 和 y=1 是 tanh 的渐近线。这样读者能一眼看出 sigmoid 的值域是 (0,1),tanh 是 (-1,1)。

参数说明:linestyle=':'是点线,用来画渐近线,不抢主曲线视觉。alpha=0.6让参考线更淡。loc='upper left'把图例放左上角,避免遮挡曲线。fontsize=11让图例文字在缩放后依然可读。

4.2 用子图布局展示原函数与导数

一张图看形状,另一张图看梯度,用 subplot 并排展示,信息密度更高。

import numpy as np import matplotlib.pyplot as plt x = np.linspace(-10, 10, 1000) sigmoid = 1 / (1 + np.exp(-x)) tanh = np.tanh(x) sigmoid_grad = sigmoid * (1 - sigmoid) tanh_grad = 1 - tanh ** 2 fig, axes = plt.subplots(1, 2, figsize=(14, 5)) axes[0].plot(x, sigmoid, color='#1f77b4', linewidth=2.5, label='sigmoid') axes[0].plot(x, tanh, color='#d62728', linewidth=2.5, label='tanh') axes[0].set_title('Activation Functions') axes[0].set_xlabel('x') axes[0].set_ylabel('f(x)') axes[0].grid(True, alpha=0.3) axes[0].legend() axes[1].plot(x, sigmoid_grad, color='#1f77b4', linewidth=2.5, label="sigmoid'") axes[1].plot(x, tanh_grad, color='#d62728', linewidth=2.5, label="tanh'") axes[1].set_title('Derivatives') axes[1].set_xlabel('x') axes[1].set_ylabel("f'(x)") axes[1].grid(True, alpha=0.3) axes[1].legend() plt.tight_layout() plt.savefig('sigmoid_tanh_subplots.png', dpi=150) plt.show()

逻辑说明:plt.subplots(1, 2)创建一行两列的子图,axes[0]和axes[1]分别操作左右两张图。左图看原函数形状,右图看导数衰减。这样一张图就能同时回答「长什么样」和「梯度怎么变」两个问题。

参数说明:figsize=(14, 5)给两个子图足够的横向空间,避免标签重叠。tight_layout()自动调整子图间距,防止轴标签被裁掉。如果你要放进论文或报告,把dpi调到 300 会更清晰。

4.3 保存高分辨率图片与常见显示问题

画完图要保存,这里有几个实操细节。第一,savefig必须在show之前调用,否则保存的是空白图。第二,如果图片边缘被裁,加bbox_inches='tight'。第三,中文标签需要设置字体,否则会显示成方块。

import matplotlib.pyplot as plt # 设置中文字体,Windows 用 SimHei,Mac 用 Arial Unicode MS plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 保存时用 bbox_inches='tight' 防止边缘裁切 plt.savefig('output.png', dpi=300, bbox_inches='tight')

逻辑说明:plt.rcParams是全局配置,设置一次后续所有图都生效。axes.unicode_minus = False解决负号显示成方块的问题。bbox_inches='tight'自动计算内容边界,去掉多余白边。

参数说明:dpi=300是印刷级分辨率,文件会大一些但足够清晰。如果只是网页展示,150 就够。SimHei是 Windows 自带黑体,Mac 上换成Arial Unicode MS或PingFang SC。

注意:在 Jupyter Notebook 里,如果先调了plt.show()再调plt.savefig(),保存出来是空图。正确顺序永远是先savefig再show。

5. 避坑与排查:画激活函数曲线时最容易翻车的 5 个地方

5.1 现象:曲线两端出现 nan 或 inf

原因:手写 tanh 公式时用了(np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x)),当 x 超过 700 左右,np.exp(x)溢出成 inf,inf 减 inf 得到 nan。

解决:直接用np.tanh(x),NumPy 内部做了数值稳定处理。如果非要手写,先对 x 做截断,比如x = np.clip(x, -500, 500)。

5.2 现象:sigmoid 曲线在 x=0 处不是 0.5

原因:np.linspace的点数设成了偶数,比如 1000 个点,中间没有精确的 0,最接近的是 -0.01 和 0.01,算出来是 0.4975 和 0.5025。

解决:把点数设成奇数,比如 1001,这样中间点正好是 0。或者不依赖采样点,直接单独算1 / (1 + np.exp(-0))验证。

5.3 现象:保存的图片是空白

原因:先调了plt.show(),Matplotlib 把画布清空了,再调plt.savefig()保存的就是空白。

解决:调整顺序,先savefig再show。或者在show之前用plt.gcf()获取当前画布再保存。

5.4 现象:中文标题显示成方块

原因:Matplotlib 默认字体不支持中文,找不到对应字形就画方块。

解决:设置plt.rcParams['font.sans-serif'] = ['SimHei'](Windows)或['Arial Unicode MS'](Mac),同时设plt.rcParams['axes.unicode_minus'] = False解决负号问题。

5.5 现象:两条曲线叠在一起分不清

原因:颜色太接近,或者线宽太细,或者没加图例。

解决:用对比色,比如蓝色#1f77b4和红色#d62728。线宽至少 2。必须加label和legend()。如果还是分不清,给其中一条加linestyle='--'虚线。

6. 进阶技巧:用交互式绘图和导数叠加验证数值行为

静态图看形状够了,但如果你想验证「tanh 的梯度在原点附近是 sigmoid 的 4 倍」这种数值结论,交互式绘图更直接。用 Plotly 可以鼠标悬停看具体数值,用 Matplotlib 的ginput也能手动取点。不过最实用的技巧是:把原函数和导数画在双 y 轴上,一眼看出梯度峰值位置。

import numpy as np import matplotlib.pyplot as plt x = np.linspace(-6, 6, 1000) sigmoid = 1 / (1 + np.exp(-x)) sigmoid_grad = sigmoid * (1 - sigmoid) fig, ax1 = plt.subplots(figsize=(9, 5)) ax1.plot(x, sigmoid, color='#1f77b4', linewidth=2.5, label='sigmoid') ax1.set_xlabel('x') ax1.set_ylabel('sigmoid(x)', color='#1f77b4') ax1.tick_params(axis='y', labelcolor='#1f77b4') ax2 = ax1.twinx() ax2.plot(x, sigmoid_grad, color='#ff7f0e', linewidth=2.5, linestyle='--', label="sigmoid'") ax2.set_ylabel("sigmoid'(x)", color='#ff7f0e') ax2.tick_params(axis='y', labelcolor='#ff7f0e') plt.title('Sigmoid and Its Derivative (Dual Axis)') fig.tight_layout() plt.savefig('sigmoid_dual_axis.png', dpi=150) plt.show()

逻辑说明:ax1.twinx()创建共享 x 轴的第二个 y 轴,左轴画原函数,右轴画导数。这样能直观看到:原函数在 x=0 处斜率最大,导数在 x=0 处达到峰值 0.25,向两侧迅速衰减。如果你把 sigmoid 换成 tanh,会看到导数峰值是 1,衰减也更慢一些。

参数说明:tick_params把 y 轴刻度和标签染成和曲线一样的颜色,避免左右轴混淆。linestyle='--'区分导数和原函数。fig.tight_layout()防止右轴标签被裁。

我自己的习惯是:每次换激活函数,先把原函数和导数画在双轴上跑一遍,确认梯度峰值位置和饱和区间。这个动作花不了两分钟,但能避免后面调参时凭感觉猜。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询