【问题标题】:How to speed up the multiple iterations of non-linear optimization in python?如何加快python中非线性优化的多次迭代?
【发布时间】:2021-04-04 08:32:52
【问题描述】:

我正在尝试使用 PyGMO package 解决非线性优化 优化类是单独定义的,然后通过单独的函数dyn_optimGMO 调用。必须为变量(节点)inits ( or init_val)

定义的 1000 个随机初始向量进行优化并保存

使用timeit 模块我发现每次迭代大约需要17 seconds 才能完成。这意味着1000 iterations 大约需要5 hours 。这是一个非常大的时间。

如果我必须为 20 perturb 节点重复此操作,那么总迭代次数将转到 200000,这将花费线性时间,如上面计算的那样。

我尝试通过使用 python multiprocessing 模块为 20 个扰动节点中的每一个并行化每组 1000 次迭代来解决这个问题。但这无济于事。

我也尝试使用 Numba jit 函数,但它们无法识别 pyGMO 模块,因此失败。

有什么方法可以并行化这段代码并使其在任意数量的迭代中更快?

如果我的问题足够清楚,请告诉我,如果没有,我会根据需要添加详细信息。

import numpy as np
import pygmo as pg

matL = np.random.rand(300,300) ; node_len = 300

inits = []; results = []


perturb = {25:0} #setting a random node, say, node 25 to 0

class my_constrained_udp:
    
    def __init__(self):
        pass
    
    def fitness(self, x):
        matA = np.matrix(x)
        obj1 = matA.dot(matL).dot(matA.T)[0,0] #this is int value
        ce1 = sum(init_val) - sum(x)                   
        return [obj1, ce1]
   
    def n_objs(self): # no of objectives
        return 1


    def get_nec(self): #no of equality constraints
        return 1   

 
    def get_nic(self): #no of in-equality constraints
        return 0                    


    def get_bounds(self): #lower and upper bounds: use this to perturb the node
        lowerB = np.array([0]*node_len); upperB = np.array([1]*node_len)
        if perturb:
            for k,v in perturb.items():
                lowerB[k] = v; upperB[k] = v
        return (lowerB,upperB)

  
    def gradient(self, x):
        return pg.estimate_gradient_h(lambda x: self.fitness(x), x)


def dyn_optimGMO(matL, node_len ,init):
        
    if perturb:
        for k,v in perturb.items(): init_val[k] = v  #setting perturbations in initial values
    
    inner_algo = pg.nlopt("slsqp"); inner_algo.maxeval = 5
    
    algo = pg.algorithm(uda = pg.mbh(inner_algo, stop = 2, perturb = .2))
    #algo.set_verbosity(10) # in this case this correspond to logs each 1 call to slsqp
    pop = pg.population(prob = my_constrained_udp(), size = 1000 , seed=123)
    pop.problem.c_tol = [1E-6] * 1 # get_nec + get_nic = 1, so multiplied by 1
    pop = algo.evolve(pop) 
   
    res = pop.champion_x   
    return res

# running above optimization code for 1000 random initializations

for i in range(1000):
    init_val = np.array([random.uniform(0, 1) for k in range(node_len)])
    
    if perturb:
        for k,v in perturb.items(): init_val[k] = v  #setting perturbations in initial values
    
    res = dyn_optimGMO(matL ,node_len ,init_val) # this function is defined here only
    
    inits.append(init_val); results.append(res)

编辑 1:

正如@Ananda 下面所建议的,我对目标函数进行了更改,将运行时间减少了近 7 倍。我已经使用 python multiprocessing 模块重写了代码以在 1000 iterations 上运行此代码。下面是我尝试生成进程以并行处理迭代的新代码。由于我的系统只有 8 个线程,所以我将池大小限制为 5,因为 PyGMO 也使用内部并行化,它也需要一些线程

import numpy as np
import pygmo as pg


matL = np.random.rand(300,300) ; node_len = 300

perturb = {12:1} # assign your perturb ID here

def optimizationFN(var):

    results = []
    
    inits = var[0]; perturb = var[1]

    
    class my_constrained_udp:
        
        def fitness(self, x):
            obj1 = x[None,:] @ matL @ x[:,None] # @ is mat multiplication operator
            ce1 = np.sum(inits) - np.sum(x)                   
            return [obj1, ce1]
       
        def n_objs(self): # no of objectives
            return 1
        
        def get_nec(self): #no of equality constraints
            return 1    
        
        def get_nic(self): #no of in-equality constraints
            return 0                    
        
        def get_bounds(self): #lower and upper bounds: use this to perturb the node
            lowerB = np.array([0]*node_len); upperB = np.array([1]*node_len)
            if perturb:
                for k,v in perturb.items():
                    lowerB[k] = v; upperB[k] = v
            return (lowerB,upperB)
        
        def gradient(self, x):
            return pg.estimate_gradient_h(lambda x: self.fitness(x), x)
    
    def dyn_optimGMO(matL, node_len ,inits):
        '''
        perturb should be a dict of node index and value as 0 or 1. Type(node_index) = int
        '''  
        if perturb:
            for k,v in perturb.items(): inits[k] = v  #setting perturbations in initial values
            
        inner_algo = pg.nlopt("slsqp"); inner_algo.maxeval = 5
        
        algo = pg.algorithm(uda = pg.mbh(inner_algo, stop = 2, perturb = .2))
        
        #algo.set_verbosity(10) # in this case this correspond to logs each 1 call to slsqp
        
        pop = pg.population(prob = my_constrained_udp(), size = 100 , seed=123)
        
        pop.problem.c_tol = [1E-6] * 1 # get_nec + get_nic = 1, so multiplied by 1
        pop = algo.evolve(pop) 
       
        res = pop.champion_x   
        return res
    
    
    if perturb:
        for k,v in perturb.items(): inits[k] = v  #setting perturbations in initial values
    
    res = dyn_optimGMO(matL ,node_len ,inits) # this function is defined here only
    
    results.append(res)
    
    return results

import time

st = time.time()
    
#1000 random initialisations
initial_vals = []
for i in range(1000): initial_vals.append(np.array([random.uniform(0, 1) for k in range(node_len)]))
initial_vals = np.array(initial_vals)

inp_val = []
for i in range(len(initial_vals)): inp_val.append([initial_vals[i],perturb])

#running evaluations
#eqVal = optimizationFN(initial_vals,perturb=perturb)
from multiprocessing import Pool


myPool = Pool(8)

data = myPool.map(optimizationFN,inp_val)

myPool.close(); myPool.join()


print('Total Time: ',round(time.time()-st,4))

这会在1.13 hours 中执行全部 1000 次迭代。

不过,有没有其他可能让我可以让它更快?

【问题讨论】:

  • CUDA 与您的问题完全无关,我已删除标签
  • 据我所知,该代码存在错误,请发布工作代码。对我来说,它在for k, v in perturb.items(): init_val[k] = v 处抛出错误
  • @Ananda 完成了编辑。现在它应该正在运行。
  • 您要求进行代码审查 - 这并不是 StackOverflow 的真正用途。如果您对如何改进有具体的想法,并且尝试过但遇到了麻烦,请描述具体问题并寻求建议。
  • 您要求其他人“找到一种方法让它运行得更快或进一步并行化” - 即代码审查?您需要解决什么具体问题?你尝试了什么?你遇到了什么问题?

标签: python numpy optimization parallel-processing python-multiprocessing


【解决方案1】:

Q“有没有什么方法可以并行化这段代码并使其在任意数量的迭代中更快?”

是的。鉴于尝试numba.jit()“来自外部”的代码(由于 Numba 编译警告的原因而失败),您可以诉诸分发大约所述 1k+ 独立初始化的批次部分,并让它们并行计算并之后收集结果。

这样做的好处是性能瞬间提高 1000 倍,并且可以进一步扩展。

如果您使用的是大约 1k+ 个节点的大学集群,那么您的 1k 批计算可能会在大约同一时间内产生结果,一次单独运行将在 1k 长序列中进行第一次运行(这里的通信成本可以忽略不计,见Amdahl's Argument)。

【讨论】:

  • 点击更多和更多的反对票不会产生更强有力的反驳。在这种许多独立初始化的进化模型中,大规模并行流程是最强大的性能提升器。以这种方式处理相同大小的批次(数量)的 PyGMO 作业比在纯 [SERIAL] 队列类似序列中处理它们要快得多,这对模拟管道的某些部分有有益的加速。否决票根本不会改变这一点,也不会产生任何有意义的反论点 - 因此它们呈现出纯粹的仇恨和愤怒行为,而不是任何好处
  • 我没有投票,但不知道是不是因为人们一直在告诉你的原因:这是一个没有太多发言权的夸夸其谈的写作,并且具有过多的应用程序使帖子更难阅读的格式。
  • 您介意解释一下@halfer,您从哪里得出“没有说太多”的结论?它做了。它 a) 显示,为什么与 numba 相关的期望失败并且将再次失败 + b) 将用户引导到一个也是唯一一个可行的方向,以确实提高 E2E 性能(使用分布式计算,其中资源在所谓的“尴尬地”上工作并行”负载,没有其他的,再次强调 NO OTHER 方法在 2020/Q4 出现的 HW-arch.s 上几乎可以做到?你之前的建议得到了尊重。Occam razor 导致用最少的词说最多,不是吗?
  • 我进行了编辑,建议如何使这个答案更容易阅读,从而更容易阅读。不必要的词被剪掉了,更容易集中在核心点上。
【解决方案2】:

在尝试并行化等之前,请尝试找出性能瓶颈到底是什么并尝试解决它。​​

如果您使用line profiler 分析您的健身功能,

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    28                                               @profile
    29                                               def fitness(self, x):
    30     96105    3577978.0     37.2      9.5          matA = np.matrix(x)
    31     96105    8548353.0     88.9     22.7          obj1 = matA.dot(matL).dot(matA.T)[0,0] #this is int value
    32     96105   25328835.0    263.6     67.4          ce1 = sum(init_val) - sum(x)
    33     96105     121800.0      1.3      0.3          return [obj1, ce1]

如您所见,大部分时间都花在 dotsum 函数上,还有大量时间花在创建 matA 上。

我会像这样重写函数-

def fitness(self, x):

    obj1 = x[None, :] @ matL @ x[:, None]
    ce1 = np.sum(init_val) - np.sum(x)

    return [obj1, ce1]

如果你分析一下你可以看到的函数,

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    20                                               @profile
    21                                               def fitness(self, x):
    22                                           
    23     77084    3151649.0     40.9     48.9          obj1 = x[None, :] @ matL @ x[:, None]
    24     77084    3214012.0     41.7     49.9          ce1 = np.sum(init_val) - np.sum(x)
    25                                           
    26     77084      79439.0      1.0      1.2          return [obj1, ce1]

完整功能的每次点击总时间从大约 380 下降到 80。

np.matrix 方法建议不要再使用并且将被弃用。而且使用原生pythonsum代替np.sum会大大降低性能。

在我的机器上,它将性能从 33 秒/it 提高到 6 秒/迭代。性能提升约 5 倍。

【讨论】:

  • @Naveen 我正在使用 line_profiler github.com/rkern/line_profiler
  • 这实际上大大提高了运行时间,在我的机器上几乎快了 7 倍。 LineProfiler 是一个很棒的工具。但是,您能否建议一种方法来并行化上述执行以使其更快?
  • 我对此表示怀疑,如果您想推得更多,您可能会远离 Python 并使用 C++ 或其他东西。据我所知,多处理 + 确保事情被正确矢量化可能是你在 python 中可以做的最好的事情。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-13
  • 1970-01-01
  • 2018-06-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-20
相关资源
最近更新 更多