摘要:在python编程的世界里,NumPy无疑是处理数值计算的“主力军”,很多数据分析、机器学习的应用都离不开它。但你有没有遇到过这样的情况:面对海量数据,即使是NumPy,计算速度也慢了下来,让你等得有些心焦?今天,我就要给大家介绍一个“隐藏高手”——NumEx
在python编程的世界里,NumPy无疑是处理数值计算的“主力军”,很多数据分析、机器学习的应用都离不开它。但你有没有遇到过这样的情况:面对海量数据,即使是NumPy,计算速度也慢了下来,让你等得有些心焦?今天,我就要给大家介绍一个“隐藏高手”——NumExpr,它能在某些复杂的数值计算中,让你的Python代码运行得更快,甚至比NumPy快好几倍。
你可能会好奇,NumPy已经够快了,NumExpr又是何方神圣,能超越它呢?简单来说,NumExpr是一个专门为NumPy设计的快速数值表达式评估器。这意味着,当你在NumPy中写下复杂的数学表达式时,NumExpr能用更聪明、更高效的方式来处理它们。
根据相关资料显示,NumExpr有两大“绝活”:
第一,计算加速。它能优化那些作用在数组上的复杂表达式。举个例子,你可能写了一个像 2*a + 3*b 这样的表达式,NumPy会一步一步地计算,而NumExpr能把整个表达式看作一个整体,用更优化的方式一次性完成计算,就像高速公路上的直通车,效率更高。
第二,内存更省。除了速度快,NumExpr还能在进行计算时,减少内存的占用。在处理超大数据集时,这一点尤其重要,能帮助你的电脑更流畅地运行,避免出现内存不足的提示。
更厉害的是,NumExpr还支持多线程。这意味着什么呢?你的电脑通常都有好几个CPU核心,NumPy在执行计算时,可能主要依赖一个核心。而NumExpr能够调动你所有的CPU核心一起工作,让它们并行处理任务。这就像一支队伍,NumPy可能只派了一个人去干活,而NumExpr能让整个团队一起上,效率自然大大提升。
正因为这些特性,NumExpr在面对计算密集型任务时,能够展现出显著的性能优势。
光说不练假把式。为了直观地展示NumExpr的加速能力,我们准备了几个实际的测试案例,从简单的数组运算到复杂的图像处理和科学计算,带你一步步看看NumExpr和NumPy的真实表现。
在开始代码测试之前,我们需要先搭建一个干净、独立的Python环境。这能确保我们的测试不受其他软件的影响,结果更准确。
你可以使用conda来创建和管理环境:
# 创建一个新的conda环境,并指定Python版本为3.11# 根据原始资料显示,NumExpr目前支持Python版本最高到3.11。conda create -n numexpr_test python=3.11 -y# 激活这个新环境conda activate numexpr_test# 安装我们需要的库:numexpr、numpy和jupyterpip install numexpr numpy jupyter环境搭建好后,在命令行输入Jupyter Notebook,就可以启动Jupyter Notebook,开始我们的性能测试了。
我们从最基础的数组加法开始。想象一下,我们有两个包含100万个随机浮点数的大数组 a 和 b。我们要计算 2*a + 3*b 这个简单的表达式,并重复这个操作5000次,看看NumPy和NumExpr分别需要多长时间。
import numpy as npimport numexpr as neimport timeita = np.random.rand(1000000) # 生成一个包含100万个随机浮点数的数组ab = np.random.rand(1000000) # 生成一个包含100万个随机浮点数的数组b# 使用NumPy执行计算并计时time_np_expr = timeit.timeit(lambda: 2*a + 3*b, number=5000)# 使用NumExpr执行计算并计时time_ne_expr = timeit.timeit(lambda: ne.evaluate("2*a + 3*b"), number=5000)print(f"NumPy执行时间: {time_np_expr} 秒")print(f"NumExpr执行时间: {time_ne_expr} 秒")测试结果可能会让你眼前一亮:
NumPy执行时间: 12.03680682599952 秒 NumExpr执行时间: 1.8075962659931974 秒
看到了吗?在这样一个简单的加法运算中,NumExpr的执行时间只有NumPy的六分之一左右!这速度差距,是不是很惊人?为了确保结果的准确性,我们也验证了两者计算出来的结果是完全一致的。速度和准确性,NumExpr都兼顾到了。
接下来,我们来挑战一个更复杂的任务:使用蒙特卡洛模拟来估算圆周率Pi的值。蒙特卡洛模拟通常涉及大量的随机迭代和计算,对性能要求较高。我们将模拟100万个随机点,并重复这个过程1000次,来观察两者的表现。
NumPy实现:
import numpy as npimport timeitdef monte_carlo_pi_numpy(num_samples): x = np.random.rand(num_samples) # 生成随机x坐标 y = np.random.rand(num_samples) # 生成随机y坐标 # 判断点是否落在单位圆内 inside_circle = (x**2 + y**2)NumPy的执行时间:10.642843848007033 秒
NumExpr实现:
import numpy as npimport numexpr as neimport timeitdef monte_carlo_pi_numexpr(num_samples): x = np.random.rand(num_samples) y = np.random.rand(num_samples) # 使用NumExpr评估表达式 inside_circle = ne.evaluate("(x**2 + y**2)NumExpr的执行时间:8.077501275009126 秒
虽然这次的加速效果不如第一个例子那么显著,但NumExpr仍然带来了大约20%的性能提升。这主要是因为在最终的求和(np.sum)这一步,NumExpr目前还没有专门的优化,所以我们不得不使用NumPy来完成。即便如此,对于这种计算量大的任务,20%的提升也相当可观了。
图像处理是另一个对计算性能有较高要求的领域。我们将实现一个Sobel滤波器,它常用于图像的边缘检测,比如从一张照片中识别出物体的轮廓。我们将处理一张图片,对比NumPy和NumExpr的效率。
Sobel滤波器通过计算图像像素的梯度来突出边缘。我们将使用scipy.ndimage.convolve进行卷积操作,并通过NumExpr来加速梯度幅度的计算。
NumPy实现:
import numpy as npfrom scipy.ndimage import convolvefrom PIL import Imageimport timeitsobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]) # Sobel X方向核sobel_y = np.array([[-1, -2, -1], [ 0, 0, 0], [ 1, 2, 1]]) # Sobel Y方向核def sobel_filter_numpy(image): img_array = np.array(image.convert('L')) # 转换为灰度图 gradient_x = convolve(img_array, sobel_x) # X方向梯度 gradient_y = convolve(img_array, sobel_y) # Y方向梯度 gradient_magnitude = np.sqrt(gradient_x**2 + gradient_y**2) # 梯度幅度 gradient_magnitude *= 255.0 / gradient_magnitude.max # 归一化 return Image.fromarray(gradient_magnitude.astype(np.uint8))image_path = "/mnt/d/test/taj_mahal.png" # 请替换为你的图片路径image = Image.open(image_path)time_np_sobel = timeit.timeit(lambda: sobel_filter_numpy(image), number=100)sobel_image_np = sobel_filter_numpy(image)sobel_image_np.save("/mnt/d/test/sobel_taj_mahal_numpy.png") # 保存结果print(f"NumPy执行时间: {time_np_sobel} 秒")NumPy执行时间: 8.093792188999942 秒
NumExpr实现:
import numpy as npimport numexpr as nefrom scipy.ndimage import convolvefrom PIL import Imageimport timeitsobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])sobel_y = np.array([[-1, -2, -1], [ 0, 0, 0], [ 1, 2, 1]])def sobel_filter_numexpr(image): img_array = np.array(image.convert('L')) gradient_x = convolve(img_array, sobel_x) gradient_y = convolve(img_array, sobel_y) gradient_magnitude = ne.evaluate("sqrt(gradient_x**2 + gradient_y**2)") # 使用NumExpr加速 gradient_magnitude *= 255.0 / gradient_magnitude.max return Image.fromarray(gradient_magnitude.astype(np.uint8))image_path = "/mnt/d/test/taj_mahal.png" # 请替换为你的图片路径image = Image.open(image_path)time_ne_sobel = timeit.timeit(lambda: sobel_filter_numexpr(image), number=100)sobel_image_ne = sobel_filter_numexpr(image)sobel_image_ne.save("/mnt/d/test/sobel_taj_mahal_numexpr.png") # 保存结果print(f"NumExpr执行时间: {time_ne_sobel} 秒")NumExpr执行时间: 4.938702256011311 秒
这次,NumExpr再次展现出它的加速能力,性能几乎是NumPy的两倍。在图像处理这种对计算性能要求较高的场景下,NumExpr的优势体现得淋漓尽致。
傅里叶级数近似是一种通过叠加一系列正弦波来模拟复杂周期函数的方法。虽然听起来有些复杂,但它的核心就是大量的重复计算。迭代次数越多,结果越精确,但计算时间也越长。我们将通过近似一个方波来展示NumExpr的性能。
import numpy as npimport numexpr as neimport timeimport matplotlib.pyplot as pltpi = np.pi # 显式定义pit = np.linspace(0, 1, 1000000) # 时间向量signal = np.sign(np.sin(2 * np.pi * 5 * t)) # 方波信号n_terms = 10000 # 傅里叶级数项数# NumPy傅里叶级数近似start_time = time.timeapprox_np = np.zeros_like(t)for n in range(1, n_terms + 1, 2): # 只考虑奇数项 approx_np += (4 / (np.pi * n)) * np.sin(2 * np.pi * n * 5 * t)numpy_time = time.time - start_time# NumExpr傅里叶级数近似start_time = time.timeapprox_ne = np.zeros_like(t)for n in range(1, n_terms + 1, 2): # 使用NumExpr评估每一项的累加 approx_ne = ne.evaluate("approx_ne + (4 / (pi * n)) * sin(2 * pi * n * 5 * t)", local_dict={"pi": pi, "n": n, "approx_ne": approx_ne, "t": t})numexpr_time = time.time - start_timeprint(f"NumPy傅里叶级数时间: {numpy_time:.6f} 秒")print(f"NumExpr傅里叶级数时间: {numexpr_time:.6f} 秒")根据原始资料显示,在这个案例中,NumExpr实现了对NumPy高达5倍的性能提升。这充分说明,在涉及大量迭代和复杂表达式的场景下,NumExpr能够显著缩短计算时间。
通过上面这些实际测试,我们看到了NumExpr在某些场景下,确实能够带来显著的性能提升。那么,这个库到底能在哪些地方发挥作用,又会对我们的工作产生什么影响呢?
NumPy和NumExpr就像两位“武林高手”。NumPy是那个全面发展的“全能型选手”,功能强大,应用广泛,是Python科学计算的基石。而NumExpr则更像是一个“专精型选手”,它在处理复杂数值表达式和利用多核CPU方面有着特别的优势。两者不是竞争关系,而是互补关系,结合使用能让你如虎添翼。
大数据处理: 当你需要处理PB级别甚至更大的数据时,哪怕是百分之几的性能提升,也能为你节省大量的时间和资源。NumExpr可以加速数据清洗、特征工程和数据聚合等环节的计算,让你的大数据分析工作更高效。机器学习: 无论是深度学习模型的训练,还是传统机器学习算法的运行,都涉及大量的矩阵运算和向量计算。NumExpr能够优化这些底层计算,从而缩短模型训练时间,尤其是在没有GPU加速的CPU环境下,它的作用更加明显。科学模拟与工程计算: 物理模拟、金融建模、气候预测等领域,常常需要进行复杂的数值迭代和大规模计算。NumExpr的多线程能力能够充分利用多核CPU,显著提升模拟效率,让科学家和工程师们更快地得到结果。图像和信号处理: 图像滤波、特征提取、信号分析等任务,都需要对大量的像素点或采样点进行高速运算。前面Sobel滤波器的例子已经表明,NumExpr能够有效加速这类任务,让你在处理图像和信号时更游刃有余。实时数据流处理: 在需要对实时数据进行快速分析和响应的场景,比如金融交易系统、物联网设备数据分析,NumExpr的低延迟计算优势将尤为突出,帮助你及时做出决策。NumExpr的出现,为Python在高性能计算领域带来了新的可能性:
降低高性能计算门槛: 以前,如果你追求极致性能,可能需要学习C++或Fortran等更复杂的语言,或者投入大笔资金购买昂贵的GPU。而NumExpr让Python用户在不改变现有代码架构的情况下,就能通过简单的调用获得显著的性能提升,大大降低了高性能计算的学习和实践门槛。优化资源利用率: NumExpr能够充分利用你的多核CPU,这意味着你不需要购买更强的硬件,就能让现有设备发挥出更大的效能,在一定程度上节约了成本。提升Python在计算密集型领域的竞争力: 性能瓶颈常常是Python被诟病的原因之一。NumExpr的出现,进一步增强了Python在科学计算和数据分析领域的竞争力,使其能够更好地应对更复杂的、对性能要求更高的任务。虽然NumExpr性能强大,但它并不是万能的,也不是所有NumPy代码都能直接用它来替代。NumExpr最适合用于那些涉及复杂数学表达式的数组操作,特别是当这些表达式需要反复计算时。比如,你的计算公式中包含了多个变量、指数、对数、三角函数等复杂运算。
如果你是NumPy的重度用户,并且在你的代码中发现某个部分在运行过程中特别慢,成为了明显的“瓶颈”,特别是涉及大量复杂数组运算的地方,那么强烈建议你尝试一下NumExpr。它的集成成本非常低,你只需要在现有的表达式外围套上一个简单的ne.evaluate函数,就可能给你的代码带来意想不到的“速度惊喜”。
总而言之,NumExpr是一个值得所有Python数据科学和工程计算爱好者关注的“黑科技”。它以其独特的优化机制,为我们打开了一扇通往更高效、更快速计算的大门。掌握并合理运用NumExpr,将帮助你提升代码性能,在数据科学和工程计算的道路上走得更远。下次当你面对计算速度的挑战时,不妨试试这个NumPy的“好搭档”!
NumExpr GitHub 项目地址:https://github.com/pydata/numexpr
来源:高效码农