MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python使用NumPy优化数值计算

2021-02-016.0k 阅读

一、NumPy简介

NumPy(Numerical Python)是Python中用于高效处理数值数据的核心库。它提供了高性能的多维数组对象,以及用于处理这些数组的各种函数。NumPy的诞生极大地提升了Python在数值计算领域的能力,使得Python在科学计算、数据分析、机器学习等众多领域都能大放异彩。

1.1 NumPy数组(ndarray)

NumPy的核心数据结构是ndarray(N - dimensional array),即多维数组。与Python原生的列表(list)不同,ndarray在存储和处理数据上更加高效。

ndarray具有以下特点:

  • 同构性:数组中的所有元素必须是相同的数据类型。这使得在存储和处理数据时,内存布局更加紧凑,从而提高了计算效率。例如,一个ndarray要么全是整数,要么全是浮点数,不能像列表那样混合不同类型的数据。
  • 固定大小:一旦ndarray被创建,其大小就固定了。如果需要改变大小,通常需要创建一个新的数组。这与列表的动态伸缩性有所不同,列表可以随时添加或删除元素改变大小。

以下是创建ndarray的简单示例:

import numpy as np

# 创建一维数组
arr1d = np.array([1, 2, 3, 4, 5])
print(arr1d)

# 创建二维数组
arr2d = np.array([[1, 2, 3], [4, 5, 6]])
print(arr2d)

在上述代码中,使用np.array()函数将Python列表转换为ndarray。通过这种方式创建的数组会根据传入数据自动推断数据类型。

1.2 数据类型(dtype)

ndarray的数据类型由dtype对象表示。NumPy支持多种数据类型,包括整数、浮点数、布尔值、复数等。常见的数据类型有:

  • 整数类型np.int8(8位有符号整数)、np.int16(16位有符号整数)、np.int32(32位有符号整数)、np.int64(64位有符号整数),以及对应的无符号整数类型np.uint8np.uint16np.uint32np.uint64。不同的整数类型适用于不同的数值范围和内存需求场景。例如,np.int8适用于表示较小范围的整数,占用内存较少;而np.int64则能表示更大范围的整数,但占用内存更多。
  • 浮点数类型np.float16(16位半精度浮点数)、np.float32(32位单精度浮点数)、np.float64(64位双精度浮点数)。在科学计算中,np.float64是最常用的浮点数类型,因为它能提供较高的精度。但在对内存敏感且对精度要求不特别高的场景下,np.float32可能更合适。
  • 布尔类型np.bool_,用于表示布尔值TrueFalse。在逻辑运算和条件判断等操作中经常使用。
  • 复数类型np.complex64(实部和虚部各占32位)、np.complex128(实部和虚部各占64位),用于处理复数运算。

可以通过dtype属性查看数组的数据类型,也可以在创建数组时指定数据类型:

arr = np.array([1, 2, 3], dtype=np.float32)
print(arr.dtype)

# 创建指定数据类型的数组
arr2 = np.array([1, 2, 3], dtype='int16')
print(arr2.dtype)

在上述代码中,arr的数据类型被指定为np.float32,通过arr.dtype可以查看该数据类型。arr2则使用字符串形式指定数据类型为int16

二、NumPy数组操作

2.1 数组索引和切片

NumPy数组的索引和切片操作与Python列表类似,但在多维数组上更加灵活。

对于一维数组,索引和切片方式与列表基本相同:

import numpy as np

arr = np.array([10, 20, 30, 40, 50])
# 获取单个元素
print(arr[2])
# 切片操作
print(arr[1:4])

在上述代码中,arr[2]获取数组的第三个元素,arr[1:4]获取从第二个元素到第四个元素(不包括第四个元素)的子数组。

对于二维数组,需要使用逗号分隔的索引来访问不同维度的元素:

arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# 获取单个元素
print(arr2d[1, 2])
# 切片操作
print(arr2d[0:2, 1:3])

arr2d[1, 2]中,第一个索引1表示第二行,第二个索引2表示第三列,因此获取的是6arr2d[0:2, 1:3]表示获取前两行中第二列和第三列的子数组。

2.2 数组形状操作

NumPy提供了多种函数来操作数组的形状,例如改变数组的维度、重塑数组等。

reshape函数:可以在不改变数据的情况下,将数组重塑为指定的形状。例如,将一个一维数组转换为二维数组:

arr = np.array([1, 2, 3, 4, 5, 6])
new_arr = arr.reshape(2, 3)
print(new_arr)

在上述代码中,arr是一个包含6个元素的一维数组,通过reshape(2, 3)将其转换为一个2行3列的二维数组。

ravel函数:与reshape相反,ravel函数将多维数组展平为一维数组:

arr2d = np.array([[1, 2, 3], [4, 5, 6]])
flat_arr = arr2d.ravel()
print(flat_arr)

这里arr2d是一个二维数组,ravel函数将其转换为一维数组[1 2 3 4 5 6]

transpose函数:用于转置数组,即将数组的行和列进行交换。对于二维数组,转置操作比较直观:

arr2d = np.array([[1, 2, 3], [4, 5, 6]])
transposed_arr = arr2d.transpose()
print(transposed_arr)

在上述代码中,arr2d的转置结果是将原数组的行变成列,列变成行。

2.3 数组拼接和分裂

在实际应用中,经常需要将多个数组合并在一起,或者将一个数组分裂成多个子数组。

concatenate函数:用于沿指定轴连接多个数组。例如,将两个一维数组合并为一个一维数组,或者将两个二维数组按行或按列合并:

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])
# 合并一维数组
combined_1d = np.concatenate((arr1, arr2))
print(combined_1d)

arr3 = np.array([[1, 2], [3, 4]])
arr4 = np.array([[5, 6], [7, 8]])
# 按行合并二维数组
combined_row = np.concatenate((arr3, arr4), axis=0)
print(combined_row)
# 按列合并二维数组
combined_col = np.concatenate((arr3, arr4), axis=1)
print(combined_col)

在上述代码中,np.concatenate((arr1, arr2))arr1arr2合并为一个一维数组。对于二维数组,axis = 0表示按行合并,axis = 1表示按列合并。

split函数:与concatenate相反,split函数用于将一个数组分裂成多个子数组。例如,将一个一维数组平均分成多个子数组:

arr = np.array([1, 2, 3, 4, 5, 6])
sub_arrays = np.split(arr, 3)
for sub_arr in sub_arrays:
    print(sub_arr)

在上述代码中,np.split(arr, 3)arr平均分成3个子数组,并通过循环打印出每个子数组。

三、NumPy数值计算

3.1 基本算术运算

NumPy数组支持各种基本算术运算,如加、减、乘、除等。这些运算都是元素级的,即对数组中的每个元素进行相应的操作。

import numpy as np

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

# 加法
add_result = arr1 + arr2
print(add_result)

# 减法
sub_result = arr1 - arr2
print(sub_result)

# 乘法
mul_result = arr1 * arr2
print(mul_result)

# 除法
div_result = arr1 / arr2
print(div_result)

在上述代码中,arr1 + arr2arr1arr2中对应位置的元素相加,得到一个新的数组。其他运算同理。

除了数组与数组之间的运算,NumPy还支持数组与标量(单个数值)之间的运算。这种情况下,标量会与数组中的每个元素进行运算:

arr = np.array([1, 2, 3])
scalar = 2

# 数组与标量相乘
mul_scalar_result = arr * scalar
print(mul_scalar_result)

这里arr * scalararr中的每个元素都乘以2,得到[2 4 6]

3.2 矩阵运算

在数值计算中,矩阵运算是非常重要的一部分。NumPy提供了专门的函数来进行矩阵乘法等运算。

dot函数:用于计算两个数组的点积。对于二维数组,点积等同于矩阵乘法:

arr1 = np.array([[1, 2], [3, 4]])
arr2 = np.array([[5, 6], [7, 8]])

# 矩阵乘法
matrix_mul_result = np.dot(arr1, arr2)
print(matrix_mul_result)

在上述代码中,np.dot(arr1, arr2)计算了arr1arr2的矩阵乘积。

除了二维数组的矩阵乘法,dot函数还能处理一维数组与二维数组之间的点积运算,以及多维数组之间符合广播规则的点积运算(广播规则将在后续介绍)。

3.3 统计运算

NumPy提供了丰富的统计函数,用于计算数组的各种统计量,如求和、均值、标准差等。

sum函数:用于计算数组元素的总和。对于一维数组,直接计算所有元素的和;对于多维数组,可以通过指定axis参数来计算沿某个轴的和:

arr = np.array([[1, 2, 3], [4, 5, 6]])

# 计算所有元素的和
total_sum = np.sum(arr)
print(total_sum)

# 计算每行的和
row_sum = np.sum(arr, axis = 1)
print(row_sum)

# 计算每列的和
col_sum = np.sum(arr, axis = 0)
print(col_sum)

在上述代码中,np.sum(arr)计算了arr所有元素的总和。np.sum(arr, axis = 1)计算了每行的和,axis = 1表示按行方向进行计算。np.sum(arr, axis = 0)计算了每列的和,axis = 0表示按列方向进行计算。

mean函数:用于计算数组元素的均值,同样可以通过axis参数指定计算方向:

arr = np.array([[1, 2, 3], [4, 5, 6]])

# 计算所有元素的均值
total_mean = np.mean(arr)
print(total_mean)

# 计算每行的均值
row_mean = np.mean(arr, axis = 1)
print(row_mean)

# 计算每列的均值
col_mean = np.mean(arr, axis = 0)
print(col_mean)

这里np.mean(arr)计算了arr所有元素的均值,其他按行和按列的计算与sum函数类似。

此外,NumPy还提供了std(标准差)、var(方差)、min(最小值)、max(最大值)等统计函数,使用方法与summean类似。

四、广播机制

4.1 广播的概念

广播(Broadcasting)是NumPy中一个强大的机制,它允许在形状不同的数组之间进行算术运算。当两个数组的形状不完全相同时,NumPy会自动尝试对它们进行广播,使它们能够进行运算。

广播的基本规则如下:

  1. 如果两个数组的维度数不同,那么在较小维度的数组前面添加长度为1的维度,直到两个数组的维度数相同。
  2. 对于每个维度,比较两个数组在该维度上的长度。如果长度相等,或者其中一个数组在该维度上的长度为1,则可以进行广播;否则,广播失败,无法进行运算。

例如,假设有一个形状为(3, 1)的数组A和一个形状为(1, 4)的数组B

import numpy as np

A = np.array([[1], [2], [3]])
B = np.array([[4, 5, 6, 7]])

result = A + B
print(result)

在上述代码中,A的形状是(3, 1)B的形状是(1, 4)。根据广播规则,A在第二个维度上的长度为1,B在第一个维度上的长度为1,因此可以进行广播。广播后的形状为(3, 4),相当于将A沿第二个维度复制4次,将B沿第一个维度复制3次,然后进行元素级的加法运算。

4.2 广播的应用场景

广播机制在实际数值计算中非常有用,尤其是在处理向量与矩阵、矩阵与矩阵之间的运算时,可以避免显式地进行循环和重复数据。

例如,在计算向量与矩阵的每一行相加时:

vector = np.array([1, 2, 3])
matrix = np.array([[4, 5, 6], [7, 8, 9], [10, 11, 12]])

result = matrix + vector
print(result)

这里vector的形状是(3,)matrix的形状是(3, 3)。通过广播机制,vector被自动广播为形状(3, 3),相当于每个元素在矩阵的每一行上进行相加,从而得到最终结果。

再比如,在对矩阵的每一个元素进行某种标量运算时,广播机制可以简化代码。假设要将矩阵的每个元素乘以一个标量2

matrix = np.array([[1, 2, 3], [4, 5, 6]])
scalar = 2

result = matrix * scalar
print(result)

这里scalar被广播为与matrix相同形状的数组,然后进行元素级的乘法运算。

五、NumPy性能优化原理

5.1 底层实现与内存布局

NumPy的高性能主要得益于其底层的实现和内存布局。ndarray是用C语言实现的,这使得它能够直接操作内存,避免了Python解释器的一些开销。

在内存布局上,ndarray采用连续的内存块来存储数据。对于一维数组,数据在内存中是按顺序连续存储的;对于多维数组,数据按照行优先(C - order)或列优先(Fortran - order)的方式存储。这种连续的内存布局使得CPU在读取数据时能够利用缓存机制,提高数据读取速度。

例如,对于一个二维数组arr2d,如果它是按行优先存储,那么同一行的数据在内存中是连续的。当进行按行遍历或运算时,CPU可以更高效地从内存中读取数据,因为缓存命中率更高。

5.2 向量化运算

NumPy的另一个重要优化手段是向量化运算。向量化运算指的是用数组操作代替显式的循环。在Python中,传统的循环操作由于解释器的开销和动态类型检查,效率相对较低。而NumPy的向量化运算直接在底层的C语言实现上进行,避免了Python循环的开销。

例如,计算一个数组中每个元素的平方,如果使用Python原生的列表和循环:

import time

python_list = list(range(1000000))
start_time = time.time()
result_list = []
for num in python_list:
    result_list.append(num ** 2)
end_time = time.time()
print("Python list loop time:", end_time - start_time)

如果使用NumPy数组和向量化运算:

import numpy as np
import time

np_array = np.array(range(1000000))
start_time = time.time()
result_np = np_array ** 2
end_time = time.time()
print("NumPy vectorized operation time:", end_time - start_time)

通过对比可以发现,NumPy的向量化运算在处理大规模数据时,速度远远快于Python原生的循环操作。这是因为向量化运算利用了底层的优化,一次处理多个数据元素,而不是逐个处理。

5.3 多线程与并行计算

NumPy在一些计算密集型操作中还利用了多线程和并行计算技术。例如,在进行大规模矩阵乘法或其他复杂运算时,NumPy会根据系统的CPU核心数自动分配任务,利用多个线程并行执行计算,从而提高整体的计算效率。

不过,需要注意的是,并非所有的NumPy操作都能充分利用多线程。一些简单的元素级操作可能由于任务粒度太小,多线程带来的调度开销反而会抵消并行计算的优势。但对于大规模的数值计算任务,多线程和并行计算能够显著提升性能。

六、NumPy在实际项目中的应用

6.1 科学计算

在科学计算领域,NumPy是不可或缺的工具。例如在物理学中,计算物体的运动轨迹、模拟物理现象等都需要进行大量的数值计算。

假设要计算一个物体在重力作用下的自由落体运动轨迹。已知物体的初始位置、初始速度和重力加速度,通过数值积分的方法可以计算出物体在不同时间点的位置。

import numpy as np
import matplotlib.pyplot as plt

# 初始条件
initial_position = 0
initial_velocity = 0
gravity = 9.81

# 时间范围
time = np.linspace(0, 10, 1000)

# 计算位置
position = initial_position + initial_velocity * time + 0.5 * gravity * time ** 2

# 绘制轨迹
plt.plot(time, position)
plt.xlabel('Time (s)')
plt.ylabel('Position (m)')
plt.title('Free - Fall Motion')
plt.show()

在上述代码中,使用np.linspace生成时间序列,然后通过向量化运算计算出不同时间点的位置。最后使用matplotlib库绘制出物体的运动轨迹。

6.2 数据分析

在数据分析中,NumPy经常与其他库如pandas一起使用。pandas的数据结构(如DataFrame)底层很多时候依赖于NumPy数组。

例如,在处理一个包含学生成绩的数据集时,可能需要对成绩进行各种统计分析。假设数据集存储在一个二维NumPy数组中,第一列是学生ID,后面几列是不同课程的成绩:

import numpy as np

# 模拟学生成绩数据
data = np.array([
    [1, 85, 90, 78],
    [2, 76, 88, 80],
    [3, 92, 89, 95]
])

# 提取成绩部分
scores = data[:, 1:]

# 计算平均成绩
average_scores = np.mean(scores, axis = 1)
print("Average scores:", average_scores)

# 计算每门课程的最高分
max_scores = np.max(scores, axis = 0)
print("Max scores per course:", max_scores)

在上述代码中,通过NumPy的索引和统计函数,方便地对学生成绩数据进行了分析,计算出每个学生的平均成绩以及每门课程的最高分。

6.3 机器学习

在机器学习领域,NumPy是基础数据处理和计算的核心库。例如在神经网络中,矩阵运算和向量操作是非常频繁的。

假设要实现一个简单的全连接层,其前向传播过程可以用NumPy来实现:

import numpy as np

# 假设输入数据形状为 (batch_size, input_size)
input_data = np.random.randn(10, 5)
# 权重矩阵形状为 (input_size, output_size)
weights = np.random.randn(5, 3)
# 偏置向量形状为 (output_size,)
bias = np.random.randn(3)

# 前向传播
output = np.dot(input_data, weights) + bias
print(output.shape)

在上述代码中,通过np.dot函数进行矩阵乘法,实现了全连接层的前向传播。NumPy的高效数组操作和广播机制使得在实现机器学习算法时更加简洁和高效。

七、NumPy与其他库的结合使用

7.1 NumPy与SciPy

SciPy(Scientific Python)是建立在NumPy基础上的科学计算库,它提供了更多高级的数值算法和工具,如优化、线性代数、积分、插值等。

在很多情况下,SciPy的函数会直接使用NumPy数组作为输入和输出。例如,使用SciPy的optimize模块进行函数优化:

import numpy as np
from scipy import optimize


def objective_function(x):
    return (x[0] - 1) ** 2 + (x[1] - 2.5) ** 2


initial_guess = np.array([0, 0])
result = optimize.minimize(objective_function, initial_guess)
print(result.x)

在上述代码中,optimize.minimize函数接受一个NumPy数组作为初始猜测值,并返回优化后的结果,也是一个NumPy数组。

7.2 NumPy与Matplotlib

Matplotlib是Python中常用的绘图库,用于数据可视化。NumPy数组经常作为Matplotlib绘图函数的输入数据。

例如,绘制一个简单的正弦函数图像:

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)

plt.plot(x, y)
plt.xlabel('x')
plt.ylabel('sin(x)')
plt.title('Sine Function')
plt.show()

在上述代码中,使用np.linspace生成x轴的数据,通过向量化运算计算出对应的y轴数据(正弦值),然后使用Matplotlib绘制出正弦函数图像。

7.3 NumPy与Pandas

Pandas是专门用于数据处理和分析的库,它的数据结构DataFrameSeries在底层很多时候依赖于NumPy数组。Pandas提供了更高级的数据处理功能,如数据清洗、分组、合并等,而NumPy则提供了高效的数值计算能力。

例如,将一个Pandas的DataFrame转换为NumPy数组进行数值计算,然后再将结果转换回DataFrame

import pandas as pd
import numpy as np

# 创建一个DataFrame
data = {
    'col1': [1, 2, 3],
    'col2': [4, 5, 6]
}
df = pd.DataFrame(data)

# 将DataFrame转换为NumPy数组
np_array = df.values

# 进行NumPy数组运算
result_array = np_array * 2

# 将结果转换回DataFrame
result_df = pd.DataFrame(result_array, columns = df.columns)
print(result_df)

在上述代码中,通过df.valuesDataFrame转换为NumPy数组,进行乘法运算后,再使用pd.DataFrame将结果转换回DataFrame。这种结合使用可以充分发挥两个库的优势,实现高效的数据处理和分析。