ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python绘制sigmoid与tanh曲线:公式、复现与避坑全指南

Python绘制sigmoid与tanh曲线:公式、复现与避坑全指南

简介:一份面向深度学习和神经网络初学者的PDF文档,系统讲解Sigmoid与Tanh激活函数的可视化绘制方法。内容基于Python的matplotlib和numpy库,以逐行代码注释的方式,先分开演示两种函数的单独绘制,再介绍利用数学关系将二者合并在同一张图中的技巧,帮助读者直观理解非线性变换和函数形态差异。文档还简要讨论了Sigmoid在零附近梯度较小、可能引发梯度消失,以及Tanh对称性更好等实用知识点,适合入门阶段结合代码动手实践。无论是完成机器学习课程作业还是自行尝试小实验,都能从中获得清晰的实现思路;代码中涉及坐标轴位置调整、刻度设置等常见绘图细节也有说明。资源为单个PDF文件,大小仅119KB,便于下载阅读;目前已有4114人学习浏览,是快速上手激活函数绘图与基础理论的实用参考。

1. 为什么画sigmoid和tanh这种基础函数,反而成了最容易翻车的第一步

很多刚接触深度学习的小白都经历过类似的场景:数学课上明明背过sigmoid和tanh的公式,也觉得“这有什么好画的”,可一打开编辑器,却不知道该从哪一行写起。更常见的是,照着网上代码敲完,出来的图要么是一条竖直的线,要么是一条横在中间的直线,甚至整个坐标轴上都找不到曲线。你盯着屏幕怀疑自己把公式抄错,可实际上,问题往往出在输入的x没有生成、函数返回类型不对、或者数值溢出变成了inf和nan。

这份笔记要做的就是一件事:把sigmoid和tanh的绘制过程拆到每一行代码都能看懂。我们既会分开画,也会合起来画,还会把那些让新手反复改代码的隐藏问题,像排雷一样一根根指出来。如果你正准备用Python把这些激活函数画进笔记、论文或教学PPT里,又不想在绘图细节上浪费半天时间,这篇就是为你准备的。它不追求花哨的可视化技巧,只求让你照着写一遍,就能得到干净、准确、可以放进任何场合的曲线图。

2. 手动画出sigmoid和tanh:函数定义与绘图准备

2.1 从数学公式到numpy代码:sigmoid与tanh的定义逻辑

先把两个函数的数学形式摆出来。sigmoid的定义是 σ(x) = 1 / (1 + e^(-x)),输出范围永远在0到1之间。tanh的定义是 tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x)),输出范围是-1到1。很多刚上手的同学会选择直接打开计算器输入公式去验证几个点,然后再写代码,但这样做容易忽略一个关键差异:sigmoid关于点(0, 0.5)中心对称,tanh关于原点(0, 0)中心对称。这个差异在画图时直接表现为两条曲线的位置,也是后面判断绘制是否正确的重要依据。

在Python里实现这两个公式,最常见的写法是使用numpy的exp函数。下面这一段代码展示了两种定义方式,第一种是严格按照数学公式做四则运算,第二种是调用numpy内置的tanh方法。两种写法在x取值比较温和时结果一致,但一旦x跑到十几甚至更大的数值,第一种写法就可能出现数值不稳定的问题。

import numpy as np # 严格照搬数学公式的定义方式 def sigmoid_naive(x): return 1 / (1 + np.exp(-x)) def tanh_naive(x): # 这种写法在x绝对值较大时会溢出 return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x)) # 数值上更稳定的简化写法 def tanh_stable(x): # exp(2x)的形式避免同时计算exp(x)和exp(-x) # 当x为正数且很大时,exp(-x)接近0,不会产生无穷大 return (1 - np.exp(-2 * x)) / (1 + np.exp(-2 * x)) # 直接用numpy内置函数,性能和稳定性最好 def tanh_builtin(x): return np.tanh(x)

这段代码里的逻辑说明:sigmoid_naive里先对x取负,再求exp,然后加1取倒数,每一步对应一个数学运算符;tanh_naive则同时计算了e^x和e^(-x),当x为正数且大于50时,exp(x)会超过1e21,而exp(-x)接近0,两者相减再相加,分子分母都趋向于原来的exp(x),虽然理论结果会接近1,但中间产生的浮点数可能已经触发numpy的溢出警告。至于参数说明,这里重点关注两点:第一,np.exp接受numpy数组,所以这些函数可以直接接收np.linspace生成的一整组x,而不用写for循环;第二,np.tanh内部用更稳健的算法实现,对小白来说,优先使用np.tanh即可,它不会让你处理奇怪的数值警告。

2.2 绘制sigmoid曲线的第一版可复现代码

有了函数定义,接下来就可以画第一条曲线。很多教程喜欢一次性把三四十行代码甩出来,然后说“复制运行就能看到结果”,但这对小白并不友好,因为你根本不知道哪一步在做什么。我习惯的做法是先把最核心的三行写出来:生成x轴数据,计算y值,用plot画出曲线。然后在这三行的基础上逐步添加坐标轴标签、网格和图例。

下面这段代码是sigmoid曲线绘制的完整版本,每一行旁边都留了注释,方便你对照着看。

import numpy as np import matplotlib.pyplot as plt # 1. 生成x轴数据 # linspace用来产生从-10到10之间均匀分布的500个点 # 点数越多,曲线越光滑,但500对于sigmoid来说完全够用 x = np.linspace(-10, 10, 500) # 2. 计算对应的y值 # 将整个x数组传入sigmoid_naive函数,得到同样长度的y数组 y_sigmoid = sigmoid_naive(x) # 3. 创建绘图窗口并设置尺寸,单位是英寸 fig, ax = plt.subplots(figsize=(8, 5)) # 4. 绘制曲线 # label参数用于后续显示图例;linewidth控制线条粗细,默认是1.5 ax.plot(x, y_sigmoid, label='sigmoid', linewidth=2.5) # 5. 添加水平参考线,在y等于0.4和0.6的位置画虚线 # 这能帮助我们直观看出曲线是否关于0.5对称 ax.axhline(0.5, color='gray', linestyle='--', linewidth=0.8, alpha=0.7) # 6. 坐标轴与标题 ax.set_xlabel('x') ax.set_ylabel('sigmoid(x)') ax.set_title('Sigmoid Activation Function') # 7. 显示网格,alpha让网格颜色淡一点,看起来不刺眼 ax.grid(True, alpha=0.3) # 8. 显示图例并放置在右上角 ax.legend(loc='upper right') # 9. 展示图形 plt.show()

这段代码的逻辑说明:第一步用linspace生成一个包含500个元素的一维数组,范围从-10到10。第二步调用函数时,numpy会对数组里的每一个元素执行相同的运算,最终y_sigmoid也是一个500个元素的数组,两者的形状完全一致,这是plot能正确画出连续曲线的根本前提。第三步创建的fig和ax分别代表整个画布和画布上的一个坐标系,使用这种面向对象写法比直接用plt.plot更灵活,后面增加子图时会很方便。

参数说明中有三个值得你根据需求调整:x的范围越大,曲线两端的平台部分越明显,但中心区域的变化会被压缩;点数num在300以内时曲线会略带折线感,我一般用500或800;linewidth则完全看个人审美,打印到论文里通常建议早1.5到2.5之间。如果你运行后看到图窗里没有任何线条,请先检查y_sigmoid是不是全为同一个值,那往往意味着输入x数组没有正确生成值,而是被当成了一个标量。

2.3 绘图前的数据检查:shape、dtype与x的取值范围

绘图报错和画错这两件事,根因往往不在plot函数本身,而在数据的形状和类型上。这是个经验问题,不是理论问题。很多小白在写代码时,习惯先乱敲一通,等到画出离谱的图像再回头找bug,效率极低。更高效的做法是,在调用plot之前,先用几行代码确认三个信息:x和y的长度是否相等、数据类型是否为浮点数、以及x的范围是否符合预期。

# 检查数组形状 print("x shape:", x.shape) # 期望输出 (500,) print("y_sigmoid shape:", y_sigmoid.shape) # 期望输出 (500,) # 检查数据类型 print("x dtype:", x.dtype) # 期望输出 float64 print("y dtype:", y_sigmoid.dtype) # 期望输出 float64 # 检查数值范围 print("x range:", x.min(), "to", x.max()) # 期望 -10.0 to 10.0 print("y range:", y_sigmoid.min(), "to", y_sigmoid.max()) # 期望接近0和1

逻辑说明:shape一旦对不上,plot会直接抛错或者产生一条奇怪的线段;dtype若为int,则linspace的精度会受影响,极端情况下y值会变成离散的一串阶梯;x范围如果过窄,比如从-1到1,曲线会只显示中间最陡峭的一段,看起来不像教科书里的完整sigmoid。这一小步检查可以让你的调试时间缩短一大半。另外,这里的参数说明也很直白:x.shape返回的是元组,打印时看到(500,)表示一维数组,如果是(500,1),plot仍然能运行,但后续操作容易踩shape不匹配的坑。我始终建议在写绘图代码时,把这种print检查留在代码里,等图形确认无误后再删除,它们是你的临时仪表盘。

3. 把tanh也画出来:分开画与合起来画的完整实现

3.1 同样用numpy计算tanh:两种算式的数值稳定性对比

tanh在数学公式上看着和sigmoid有点相似,但实际画起来需要注意一个非常隐蔽的问题:x取到较大绝对值时,e^x和e^(-x)的差距会指数级拉大。如果按最原始的公式“先算分子分母再相除”,你可能会在控制台看到numpy的RuntimeWarning提示“overflow encountered in exp”,然后图片里出现一条断裂的曲线。为了帮你看清这个问题,我们可以做一个小实验,分别用两种方式去计算x等于100时的tanh值。

import numpy as np x_test = 100.0 # 方式一:原始公式 with np.errstate(over='ignore'): # 临时忽略溢出警告,方便观察结果 result_naive = (np.exp(x_test) - np.exp(-x_test)) / (np.exp(x_test) + np.exp(-x_test)) # 方式二:利用1 - exp(-2x)的形式变形 result_stable = (1 - np.exp(-2 * x_test)) / (1 + np.exp(-2 * x_test)) # 方式三:调用内置函数 result_builtin = np.tanh(x_test) print("naive result:", result_naive) # 大概率是 nan 或者 inf print("stable result:", result_stable) # 接近 1.0 print("builtin result:", result_builtin) # 同样接近 1.0

这个演示的逻辑说明:原始公式在x趋向正无穷时,e^x会变得极大,而e^(-x)趋近于0,分子分母实际上都约等于e^x,两个极大或极小的数相除,浮点数精度不够就会输出nan。第二种写法把分子分母同时除以e^x,化简成用e^(-2x)表达的形式,当x为正数且很大时,e^(-2x)会自然落到可以精确表示的范围内。第三种直接调np.tanh是推荐方案,它由底层数学库实现,兼顾速度和精度。

参数说明里有个小细节值得记住:np.errstate是临时上下文管理器,它并不修复数值问题,只是让警告暂时不显示,方便你观察程序是否真的溢出。这只适合做实验,实际绘图时不要依赖这种方式来掩盖问题,因为得到的nan一旦进入绘图数据,整条曲线都会消失。现在你应该明白,为什么我建议像下面这样定义安全的tanh函数供绘图使用,因为它能承受更极端的数据范围,让你的图看起来更完整。

3.2 分开画两张图:Figure对象与人工控制画布

分开画,就是sigmoid和tanh各占一个独立的图形窗口或各占一个图片文件。这种需求在写教材、做对比实验时很常见,你要把两张图分别插入到文档的不同位置。最容易翻车的点在于,连续执行两次plt.show()时,第一次弹出的窗口可能会被立即覆盖,导致你只看到第二张图。要解决这个问题,需要用figure编号或保存图片的方式来做。

import matplotlib.pyplot as plt # 第一张图:sigmoid # figsize控制整张图的宽和高,单位是英寸 plt.figure(1, figsize=(7, 4)) plt.plot(x, y_sigmoid, color='blue', linewidth=2.5) plt.xlabel('x') plt.ylabel('sigmoid(x)') plt.title('Sigmoid') plt.grid(alpha=0.3) # 保存图片到当前目录,dpi控制分辨率,300适合打印 plt.savefig('sigmoid_curve.png', dpi=300) # 第二张图:tanh # 重新利用y数组,但重新计算tanh的y值 y_tanh = np.tanh(x) plt.figure(2, figsize=(7, 4)) plt.plot(x, y_tanh, color='red', linewidth=2.5) plt.xlabel('x') plt.ylabel('tanh(x)') plt.title('Tanh') plt.grid(alpha=0.3) plt.savefig('tanh_curve.png', dpi=300) # 显示所有已经创建的图形窗口 plt.show()

这段代码的逻辑是:先用plt.figure(1)创建第一个图像对象,之后的plot、xlabel等操作都会作用到这个图像上;紧接着创建plt.figure(2),当前绘图目标切换到第二个图。savefig会把图保存成文件,这样即使之后又画了别的图,前面的成果也已经留在磁盘上了。最后调用show时,两个窗口都会弹出来,不会互相覆盖。

参数说明:figure的第一个参数是编号,只要编号不同,就可以同时存在多个图。figsize是两个元素的元组,宽度在前,高度在后。dpi是分辨率,默认约为100,文档插图惯用300甚至600;如果只是屏幕上看看,100到150就足够了。还有一点值得注意:每次调用figure之后,后续的plt语句都会附着到最新创建的图上,如果中间切回figure(1)继续操作,那之后再执行figure(2)会新建另一个图,而不是回到原来的第二张图。

3.3 合起来画的三种姿势:subplot并排、overlay叠画与归一化对比

分开画虽然清楚,但在很多对比场景下,把两条曲线放进同一张图或者并排放在同一张图中更直观。这里我介绍三种可以按需选择的合起来画法。

第一种是subplot并排,本质是把一张画布拆成两个区域,左边画sigmoid,右边画tanh。第二种是overlay叠画,把两条曲线画在同一个坐标系中,用颜色和图例区分。第三种是归一化对比,把sigmoid和tanh都映射到0到1的范围后再叠画,这种适合分析两者斜率变化的相似性。我先给出subplot和overlay的完整代码。

# 方式一:subplot并排 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) # 左边:sigmoid ax1.plot(x, y_sigmoid, color='blue', linewidth=2.5) ax1.set_title('Sigmoid') ax1.set_xlabel('x') ax1.set_ylabel('sigmoid(x)') ax1.grid(alpha=0.3) # 右边:tanh ax2.plot(x, y_tanh, color='red', linewidth=2.5) ax2.set_title('Tanh') ax2.set_xlabel('x') ax2.set_ylabel('tanh(x)') ax2.grid(alpha=0.3) # 自动调整子图之间的间距,避免标题重叠 plt.tight_layout() plt.savefig('sigmoid_tanh_subplot.png', dpi=300) plt.show()
# 方式二:overlay叠画 plt.figure(figsize=(8, 5)) plt.plot(x, y_sigmoid, label='sigmoid', color='blue', linewidth=2.5) plt.plot(x, y_tanh, label='tanh', color='red', linewidth=2.5) plt.axhline(0, color='black', linewidth=0.8) # 画一条y=0的参考线 plt.xlabel('x') plt.ylabel('value') plt.title('Sigmoid vs Tanh') plt.legend(loc='upper left') plt.grid(alpha=0.3) plt.savefig('sigmoid_tanh_overlay.png', dpi=300) plt.show()

逻辑说明:subplots(1, 2)返回一个包含两个Axes对象的数组,ax1和ax2分别代表左右两个坐标系,所有针对ax1的绘图指令只会作用于左图。这样之后想单独调整左边某个细节,也不会影响右边。overlay方式的本质是在同一个坐标系中多次调用plot,每次调用相当于在同一块画布上叠一层图层,matplotlib会按照调用顺序绘制,后画的曲线会覆盖先画的曲线,所以一般把sigmoid放在前,tanh放在后,颜色深浅搭配好。

参数说明里有一个容易被忽略的关键参数:legend的loc。当两条曲线都处于单调增长状态时,sigmoid在0到1区间、tanh在-1到1区间,上图左侧会因tanh起始值更低而空出空间,所以loc='upper left'是安全选择;如果你画的是自己修改过的函数,图例位置就要根据曲线实际走向调整。关于subplot的布局,subplots(2, 1)是上下排列,subplots(1, 3)是一行三个图,你只需要记住第一个数是行数、第二个数是列数。做个不需要思考的选择题:对比sigmoid和tanh用一行两列最舒服,因为横轴统一,x方向可对齐。

4. sigmoid和tanh绘图的避坑指南:画得不顺的5个常见原因

4.1 现象一:曲线变成一条竖线,x轴数据顺序乱了

【现象】图像里出现一条笔直的竖线,从图底延伸到图顶,数学上完全解释不通。

【原因】最常见的是x数组没有形成单调递增的有序序列,比如你用列表直接拼了几个值,或者对x做了sort之后又把原来的顺序弄丢了。另一种可能是x的取值跨度太大,而y的显示范围又被自动缩放得很大,曲线在视觉上被“拉伸”成了一条接近竖直的线,比如x从-1000画到1000,中间核心区域的变化被平台区压缩。

【解决】先用sort保证x有序:x = np.sort(np.linspace(-10, 10, 500))。再把x范围缩小到-10到10之间,这个区间足够呈现出sigmoid和tanh的全部特征。绘图时手动固定y轴也行,用plt.ylim(-0.1, 1.1)给sigmoid留一点边距,曲线看起来会更舒服。

4.2 现象二:图像变成一条水平直线,数值溢出被numpy警告

【现象】曲线应该有的S形轮廓完全消失,取而代之的是一条平直的水平线,控制台还有RuntimeWarning。

【原因】数值溢出导致的inf或nan会让plot无法正常绘制。x过大时,e^(-x)转化为0,而exp(x)变为inf,数学上sigmoid应该趋近1,但程序拿到inf除以inf结果就成了nan,整个曲线断掉或拉平。

【解决】不要用原始公式硬算极端值。改用np.tanh和稳定的sigmoid写法:1 / (1 + np.exp(-x))其实已经比较稳定,因为exp(-x)在x很大时不会溢出成正无穷。如果还不放心,可以把x的区间控制在-50到50以内,这样既保留曲线两端的平坦特征,又不会触碰浮点数的极限。

4.3 现象三:用了plt.show()但绘图窗口一闪而过

【现象】在命令行或脚本里运行代码,窗口瞬间出现又立刻消失,根本来不及看清曲线。

【原因】脚本执行完后进程结束,图形窗口随之关闭。这在Jupyter Notebook里不是问题,但用python xxx.py方式运行时就会碰到。

【解决】在show之前调用plt.savefig('figure.png')把图存到文件,然后从文件里查看;或者打开交互模式,在代码前加上plt.ion(),这样脚本结束后图窗仍然停留在屏幕上。对小白来说,我的建议是养成先savefig再show的习惯,图和文件都能拿到,也方便后续直接用图像查看器放大检查。

4.4 现象四:中文标题和坐标轴显示成方块乱码

【现象】title和label里的中文全变成了一个个方框,英文和数字正常。

【原因】matplotlib默认字体不包含中文字符,系统找不到合适字体时只能用方框占位。

【解决】在绘图前指定中文字体,常见写法是用plt.rcParams['font.sans-serif'] = ['SimHei']。如果你用的是没有SimHei的环境,可以安装所需字体或改成英文标签。最省事的方案是直接使用英文坐标轴说明,因为sigmoid和tanh本来就是国际通用词,在论文场景下英文标签反而更常见。

4.5 现象五:两组曲线在同一张图里被同一颜色覆盖,无法区分

【现象】在overlay叠画时,两条曲线颜色一模一样,眼睛完全分辨不出哪条是sigmoid哪条是tanh。

【原因】没有为第二次plot指定新颜色,matplotlib默认颜色循环在第二次调用时会换色,但如果代码里手动设置了相同的color,或者颜色序列恰好撞车,就会覆盖。

【解决】显式指定颜色,比如sigmoid用蓝色、tanh用红色;同时加上linestyle区分,比如一个实线、一个虚线,这样即使打印成黑白文档也能分辨。具体到本次代码,写上color='blue', linestyle='-'和color='red', linestyle='--'即可。图例label也要记得写清楚,否则依然要靠猜。

5. 从“能画”到“画得能放进论文”:三处让图变专业的细节

画出一条sigmoid曲线是基本操作,但如果你要把它放进报告、课程作业、甚至学术论文里,就得多留意三个细节。第一个细节是参考线和高亮关键点。sigmoid在x=0处取值为0.5,tanh在x=0处取值为0,这两个点是它们的中心对称点,也是提问率最高的关键位置。用ax.scatter([0], [0.5], color='green', zorder=5)突出这个点,并在旁边用ax.annotate标注“(0, 0.5)”,一下就能让读者抓住曲线特征。

第二个细节是图例与线条样式的组合选择。你不可能保证所有人都用彩色显示器阅读,所以只依赖颜色区分函数是危险的,加一条虚线或者加粗某一条线,都远比单靠颜色可靠。我在实际写报告时,sigmoid用蓝色实线、tanh用红色虚线,而且sigmoid的线宽设为2.5,tanh设为2.0,双保险。

第三个细节是验证自己画的函数是否正确。等到图像画好之后,你可以用np.allclose检查数值是否符合预期,例如检查tanh在0附近的值是否接近0,在±10处是否接近±1。

# 验证关键点的函数值是否合理 print(np.tanh(0)) # 期望 0.0 print(sigmoid_naive(0)) # 期望 0.5 print(np.tanh(-10.0)) # 期望接近 -1.0 print(sigmoid_naive(10.0)) # 期望接近 1.0

这一段验证的逻辑说明:函数值是否正确直接关系到图像的准确性,如果np.tanh(0)打印出来不是0,说明运行环境或函数定义有误。plot画出来的只是视觉结果,数值验证才是判断对错的最终标准。参数说明上,np.allclose(a, b, rtol=1e-5)可以判断两个数组是否在误差范围内相等,如果你想对比自己写的函数和np.tanh的结果,用这个函数就对了。

我自己的习惯是,每次画完这两条曲线,都会顺手在代码最后写上三行print验证,再盯着坐标轴确认一下对称性。这一步的投入非常少,但能拦住大量低级错误。希望这些拆解得够仔细,让你绕开学习初期那些不必要的时间损耗。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进