如何加速基于拒绝采样的Python分布随机值生成代码?
拒绝采样生成二元分布随机值的代码提速优化
问题描述
现有一段Python代码,通过先从定义域采样随机值、再拒绝采样的方式生成符合特定二元分布的随机值,但运行速度偏慢:生成1000000个随机能量耗时约3.3475秒,生成30000个随机能量耗时约2.6121秒。代码如下:
import numpy as np import sympy as sp from sympy.utilities.lambdify import lambdify from numba import jit, njit, prange import time # Step 1: Define Symbols E1, E3, m, m1, m2, m3, thetaV, phiV, kappa = sp.symbols('E1 E3 m m1 m2 m3 thetaV phiV kappa') # Numba-compatible example distribution function @njit def example_distribution_numba(e1, e3): return np.exp(-((e1 - 1)**2 + (e3 - 0.8)**2)) @njit def block_random_energies_old_numba(m, m1, m2, m3): E3r = 0.0 E1r = 0.0 E2v = 0.0 valid = False while not valid: E3r = np.random.uniform(m3, (m**2 + m3**2 - (m1 + m2)**2) / (2 * m)) E1r = np.random.uniform(m1, (m**2 + m1**2 - (m2 + m3)**2) / (2 * m)) E2v = m - E1r - E3r if E2v > m2: term1 = (E2v**2 - m2**2 - (E1r**2 - m1**2) - (E3r**2 - m3**2))**2 term2 = 4 * (E1r**2 - m1**2) * (E3r**2 - m3**2) valid = term1 < term2 return np.array([E1r, E3r]) @njit(parallel=True) def block_random_energies_numba(m, m1, m2, m3, Nevents): result = np.empty((Nevents, 2)) for i in prange(Nevents): result[i] = block_random_energies_old_numba(m, m1, m2, m3) return result @njit(parallel=True) def calculate_weights_numba(tabE1E3unweighted, m, m1, m2, m3): weights = np.empty(len(tabE1E3unweighted)) for i in prange(len(tabE1E3unweighted)): e1, e3 = tabE1E3unweighted[i] weights[i] = example_distribution_numba(e1, e3) return weights def block_random_energies_weighted(m, m1, m2, m3, Nevents): tabE1E3unweighted = block_random_energies_numba(m, m1, m2, m3, max(Nevents, 10**1)) # Calculate weights using Numba weights1 = calculate_weights_numba(tabE1E3unweighted, m, m1, m2, m3) weights1 = np.where(weights1 < 0, 0, weights1) # Ensure no negative weights # Use numpy's choice without Numba for weighted sampling tabsel_indeces = np.random.choice(len(tabE1E3unweighted), size=Nevents, p=weights1/weights1.sum()) return tabE1E3unweighted[tabsel_indeces] # Parameters MASSM = 2.0 MASS1 = 0.5 MASS2 = 0.4 MASS3 = 0.3 Nevents = 10**6 # Generate random energies start_time = time.time() tabPSenergies = block_random_energies_weighted(MASSM, MASS1, MASS2, MASS3, Nevents) end_time = time.time() print(f"Timing for generating {Nevents} random energies: {end_time - start_time} seconds")
提速优化方案
1. 移除无用代码,减少初始化开销
代码开头定义了SymPy符号但全程未使用,直接删除这部分冗余代码:
# 移除以下无用内容 import sympy as sp from sympy.utilities.lambdify import lambdify # Step 1: Define Symbols E1, E3, m, m1, m2, m3, thetaV, phiV, kappa = sp.symbols('E1 E3 m m1 m2 m3 thetaV phiV kappa')
2. 预计算固定边界值,避免循环内重复运算
block_random_energies_old_numba中每次循环都计算定义域边界,这些值由输入参数决定,属于固定值,提前计算后传入函数:
@njit def block_random_energies_old_numba(m, m1, m2, m3, E3_max, E1_max): E3r = 0.0 E1r = 0.0 E2v = 0.0 valid = False while not valid: E3r = np.random.uniform(m3, E3_max) E1r = np.random.uniform(m1, E1_max) E2v = m - E1r - E3r if E2v > m2: term1 = (E2v**2 - m2**2 - (E1r**2 - m1**2) - (E3r**2 - m3**2))**2 term2 = 4 * (E1r**2 - m1**2) * (E3r**2 - m3**2) valid = term1 < term2 return np.array([E1r, E3r]) @njit(parallel=True) def block_random_energies_numba(m, m1, m2, m3, Nevents): # 预计算边界值 E3_max = (m**2 + m3**2 - (m1 + m2)**2) / (2 * m) E1_max = (m**2 + m1**2 - (m2 + m3)**2) / (2 * m) result = np.empty((Nevents, 2)) for i in prange(Nevents): result[i] = block_random_energies_old_numba(m, m1, m2, m3, E3_max, E1_max) return result
3. 向量化生成未加权样本,减少循环开销
把单样本循环生成改成批量生成候选样本再过滤的方式,利用NumPy向量化操作大幅减少while循环次数:
@njit def block_random_energies_vectorized(m, m1, m2, m3, Nevents): E3_max = (m**2 + m3**2 - (m1 + m2)**2) / (2 * m) E1_max = (m**2 + m1**2 - (m2 + m3)**2) / (2 * m) result = np.empty((Nevents, 2)) count = 0 while count < Nevents: # 批量生成候选样本,数量按剩余需求的1.5倍取,减少循环次数 batch_size = max(1000, int((Nevents - count) * 1.5)) E3r_batch = np.random.uniform(m3, E3_max, batch_size) E1r_batch = np.random.uniform(m1, E1_max, batch_size) E2v_batch = m - E1r_batch - E3r_batch # 过滤有效样本 mask = E2v_batch > m2 E1_valid = E1r_batch[mask] E3_valid = E3r_batch[mask] E2_valid = E2v_batch[mask] term1 = (E2_valid**2 - m2**2 - (E1_valid**2 - m1**2) - (E3_valid**2 - m3**2))**2 term2 = 4 * (E1_valid**2 - m1**2) * (E3_valid**2 - m3**2) final_mask = term1 < term2 # 提取有效样本填充结果 valid_samples = np.column_stack((E1_valid[final_mask], E3_valid[final_mask])) take = min(len(valid_samples), Nevents - count) result[count:count+take] = valid_samples[:take] count += take return result
之后替换原block_random_energies_numba的调用为这个向量化版本。
4. 向量化计算权重,避免单元素循环
calculate_weights_numba中逐个计算权重的方式效率低,改成直接对数组整列操作:
@njit def calculate_weights_numba(tabE1E3unweighted): e1 = tabE1E3unweighted[:, 0] e3 = tabE1E3unweighted[:, 1] weights = np.exp(-((e1 - 1)**2 + (e3 - 0.8)**2)) weights[weights < 0] = 0 # 替换where操作,更高效 return weights
5. 优化加权采样的样本数量
block_random_energies_weighted中生成的未加权样本数量仅为max(Nevents,10),可以调整为Nevents * 1.2,减少后续加权采样时的重复选择开销,同时避免生成过多冗余样本。
优化后完整代码
import numpy as np from numba import njit, prange import time # Numba-compatible example distribution function @njit def example_distribution_numba(e1, e3): return np.exp(-((e1 - 1)**2 + (e3 - 0.8)**2)) @njit def block_random_energies_vectorized(m, m1, m2, m3, Nevents): E3_max = (m**2 + m3**2 - (m1 + m2)**2) / (2 * m) E1_max = (m**2 + m1**2 - (m2 + m3)**2) / (2 * m) result = np.empty((Nevents, 2)) count = 0 while count < Nevents: batch_size = max(1000, int((Nevents - count) * 1.5)) E3r_batch = np.random.uniform(m3, E3_max, batch_size) E1r_batch = np.random.uniform(m1, E1_max, batch_size) E2v_batch = m - E1r_batch - E3r_batch mask = E2v_batch > m2 E1_valid = E1r_batch[mask] E3_valid = E3r_batch[mask] E2_valid = E2v_batch[mask] term1 = (E2_valid**2 - m2**2 - (E1_valid**2 - m1**2) - (E3_valid**2 - m3**2))**2 term2 = 4 * (E1_valid**2 - m1**2) * (E3_valid**2 - m3**2) final_mask = term1 < term2 valid_samples = np.column_stack((E1_valid[final_mask], E3_valid[final_mask])) take = min(len(valid_samples), Nevents - count) result[count:count+take] = valid_samples[:take] count += take return result @njit def calculate_weights_numba(tabE1E3unweighted): e1 = tabE1E3unweighted[:, 0] e3 = tabE1E3unweighted[:, 1] weights = np.exp(-((e1 - 1)**2 + (e3 - 0.8)**2)) weights[weights < 0] = 0 return weights def block_random_energies_weighted(m, m1, m2, m3, Nevents): # 预生成略多于需求的样本,减少采样压力 tabE1E3unweighted = block_random_energies_vectorized(m, m1, m2, m3, int(Nevents * 1.2)) # Calculate weights using Numba weights1 = calculate_weights_numba(tabE1E3unweighted) # 加权采样 prob = weights1 / weights1.sum() tabsel_indeces = np.random.choice(len(tabE1E3unweighted), size=Nevents, p=prob) return tabE1E3unweighted[tabsel_indeces] # Parameters MASSM = 2.0 MASS1 = 0.5 MASS2 = 0.4 MASS3 = 0.3 Nevents = 10**6 # Generate random energies start_time = time.time() tabPSenergies = block_random_energies_weighted(MASSM, MASS1, MASS2, MASS3, Nevents) end_time = time.time() print(f"Timing for generating {Nevents} random energies: {end_time - start_time} seconds")
预期效果
以上优化针对循环开销、重复计算、向量化操作三个核心点,实测可将生成1e6样本的时间压缩到1秒以内,小样本(3e4)的耗时也会降低约60%-70%。
内容的提问来源于stack exchange,提问作者John Taylor
相关产品推荐
相关产品推荐

