前言

前段时间一直在用自己写的遗传算法框架测试算法在优化力场参数的效果,但是跑起来效率很慢,因为适应度函数需要调用多次力场程序计算能量,但是还是比我预想中的慢我也没有及时对程序进行profiling和优化。直到放假前在github有个使用gaft做SVM参数优化的童鞋开了个issue中说道在gaft优化的过程中会大量调用适应度函数,这才使我在国庆放假期间对gaft进行了profiling找到程序瓶颈并针对性的优化。

本文就记录下自己gaft做profiling并优化的过程以及优化的效果。

正文

对GAFT进行性能分析(Profiling)

关于如何对Python程序进行性能分析生成分析报告并可视化分析报告,我在之前的一篇博客里《Python优化第一步: 性能分析实践》进行了详细的介绍,这里我就直接分析了。

为了能针对gaft中不同的函数进行分析,借助Python内置的cProfilepstats模块我写了个装饰器方便分析并生成不同的分析统计文件。

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def do_profile(filename, sortby='tottime'):
    '''
    Constructor for function profiling decorator.
    '''
    def _do_profile(func):
        '''
        Function profiling decorator.
        '''
        @wraps(func)
        def profiled_func(*args, **kwargs):
            '''
            Decorated function.
            '''
            # Flag for doing profiling or not.
            DO_PROF = os.getenv('PROFILING')
            if DO_PROF:
                profile = cProfile.Profile()
                profile.enable()
                result = func(*args, **kwargs)
                profile.disable()
                ps = pstats.Stats(profile).sort_stats(sortby)
                ps.dump_stats(filename)
            else:
                result = func(*args, **kwargs)
            return result
        return profiled_func
    return _do_profile

对上面的带参数的装饰器我在这里稍微解释下,装饰器构造器do_profile的两个参数filenamesortby分别指定分析结果报告的文件名以及统计结果的排序方式。它会对需要进行性能分析的函数进行装饰,然后在函数运行完后在当前目录生成结果报告。例如我需要对gaft中遗传算法迭代主循环进行分析,则需要:

 
 
 
 
 

Python

 
1
2
3
@do_profile(filename='gaft_run.prof')
def run(self, ng=100):
    ...

同时为了方便,我还需要一个环境变量PROFILING来启动分析:

 
 
 
 
 

Shell

 
1
export PROFILING=y

分析结果

这里为了方便查看函数的相互调用关系,我是用了pyprof2calltree 然后使用Mac上的QCacheGrind来可视化分析结果:

 
 
 
 
 

Shell

 
1
pyprof2calltree -i gaft_run.prof -k

将Python的profiling文件转换并直接调用QCacheGrind便可以方便的查看分析相关信息。

通过调用关系图可以看到,gaft的初始版本的min,max,mean等函数多次调用best_indvworst_indv会多次调用适应度函数来相互比较,而通常情况下用户自定义的适应度函数都是需要额外去调用外部程序的,一般都比较费时。所以必须要通过优化best_indvworst_indvfitness的调用次数才能提升gaft的效率。

优化GAFT

函数返回值缓存

从之前我写的best_indv中可以看到,我将fitness作为key用于获取最大值,Python内置的max函数会内部调用fitness进行相互比较来获取最大值,这个时候便对fitness进行了多余的调用,因为在遗传算法中,每一代的population中的个体是不会发生变化的我们只需要在每一次迭代的一开始调用fitnessn次就好了(n为种群大小),每一代中再次需要用到适应度值的地方直接获取。这样需要我们对种群中的个体进行惰性求值,也就是对所有的fitness的值进行缓存。这种操作我在优化自己的催化动力学程序的时候也使用过,叫做函数返回值缓存.

但是在gaft中这种缓存有稍微麻烦一点,因为缓存并不是缓存一次就可以一直用了,它会随着条件的变化需要重新计算种群中所有个体的适应度然后重新缓存。

重新计算适应度值需要同时满足的条件

  1. 种群中的所有个体没有发生任何变化 (如果变化了那肯定要重新计算适应度值了)。
  2. 已有缓存的适应度值 (如果是第一次那肯定需要计算一次所有个体的适应度值)。
  3. 计算适应度值的适应度函数与之前比较没有发生变化(如果计算适应度函数都改变了,那当然需要重新估计适应度值了)。

函数返回值缓存描述符

叉车问答

为此我写了个装饰器来缓存函数的返回值:

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Memoized(object):
    '''
    Descriptor for population statistical varibles caching.
    '''
    def __init__(self, func):
        self.func = func
        self.result = None
        self.fitness = None
    def __get__(self, instance, cls):
        self.instance = instance
        return self
    def __call__(self, fitness):
        if ((not self.instance._updated)         # population not changed
                and (self.result is not None)    # result already cached
                and (fitness == self.fitness)):  # fitness not changed
            # Return cached result directly.
            return self.result
        else:
            # Update fitness function.
            self.fitness = fitness
            # Update and memoize result.
            self.result = self.func(self.instance, fitness)
            # Recover flag.
            self.instance._updated = False
            return self.result

动态监视种群的变化

好了上面我们可以通过描述符来缓存函数返回值,但是一旦种群不满足上述的三个条件就需要重新计算适应度值,那我们如何监控种群的变化呢?

我在GAPopulation中添加了一个标记_updated用于标记种群是否已经发生了变化, 然后我们的任务就是在其他能够影响到种群的地方试图去更新这个flag。

如何能更Pythonic的更新这个标记呢?

所谓的种群发生变化,也是就种群中的个体列表发生了变化,种群中的个体我都放在了一个列表中,我需要监控这个列表是否发生变化以便更新flag,具体又是那些变化呢?

  1. 列表整体是否发生了变化(赋值操作)
  2. 列表中的元素是否发生变化(对列表中的元素赋值操作,列表的appendextend操作等)

好了我们要具体怎么实现呢?

1)对于第一种,由于Python中无法进行赋值运算符重载,但是我们可以通过描述符的__set__来处理:

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
class GAIndividuals(object):
    '''
    Descriptor for all individuals in population.
    '''
    def __init__(self, name):
        self.name = '_{}'.format(name)
    def __get__(self, instance, owner):
        return instance.__dict__[self.name]
    def __set__(self, instance, value):
        instance.__dict__[self.name] = value
        # Update flag.
        instance._updated = True

2)对于第二种情况,我们需要对Python的List类型的相应方法进行override

但是嘞,即使重写了list的接口,又如何更新population中的变量呢?这个时候就需要用闭包了。在GAPopulation的构造函数__init__中定义list的派生类,并立即实例化,这时候派生类的便可以获取population对象了,于是GAPopulation的构造函数可以这些写:

 
 
 
 
 

Python

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def __init__(self, indv_template, size=100):
    # ...
    # Flag for monitoring changes of population.
    self._updated = False
    # Container for all individuals.
    class IndvList(list):
        '''
        A proxy class inherited from built-in list to contain all
        individuals which can update the population._updated flag
        automatically when its content is changed.
        '''
        # NOTE: Use 'this' here to avoid name conflict.
        def __init__(this, *args):
            super(this.__class__, this).__init__(*args)
        def __setitem__(this, key, value):
            '''
            Override __setitem__ in built-in list type.
            '''
            old_value = this[key]
            if old_value == value:
                return
            super(this.__class__, self).__setitem__(key, value)
            # Update population flag.
            self._updated = True
        def append(this, item):
            '''
            Override append method of built-in list type.
            '''
            super(this.__class__, this).append(item)
            # Update population flag.
            self._updated = True
        def extend(this, iterable_item):
            if not iterable_item:
                return
            super(this.__class__, this).extend(iterable_item)
            # Update population flag.
            self._updated = True
    self._individuals = IndvList()

优化效果

通过上面对代码的优化,我们看看我们优化的效果如何,使用分析描述符来分析GAEngine.run跑一代种群的情况,其中种群大小为10。如下图为cProfile生成的分析报告对比:

可以看到优化后的跑一代种群的时间缩短为将近原来的1/7! 优化效果还是很明显的。

然后看一看调用关系图:

energy_fitness的调用次数从3807降到了621次!

总结

本文记录了遗传算法框架GAFT的一次profiling和优化过程,通过缓存值的方式极大的减少了适值函数的调用次数,在时间上,跑一代种群的效率提升了7倍左右。

遗传算法框架GAFT优化小记的更多相关文章

  1. 遗传算法框架-基于java jenetics库实现

    本篇并非介绍如何从0开始开发遗传算法框架,反而推荐各位使用已有的GA库jenetics来做遗传算法. GA算法的逻辑还是贴下: 好了,下面介绍的是基于jenetics开发的更贴近业务侧的框架,以及使用 ...

  2. CLion之C++框架篇-优化开源框架,引入curl,实现get方式获取资源(四)

      背景   结合上一篇CLion之C++框架篇-优化框架,引入boost(三),继续进行框架优化!在项目中,我们经常会通过get方式拉取第三方资源,这一版优化引入类库curl,用来拉取第三方资源库. ...

  3. CLion之C++框架篇-优化框架,引入boost(三)

      背景   结合上一篇CLion之C++框架篇-优化框架,单元测试(二),继续进行框架优化!这一版优化引入一个我们日常经常使用的操作库Boost,估算使用频率在70%以上!   Boost的优势在哪 ...

  4. 大并发下TCP内存消耗优化小记(86万并发业务正常服务)

    转自:http://blog.csdn.net/u010954257/article/details/54178160 最近在做一个大并发服务的测试(目前测到86万,当然有大量长连接,每天打的日志高到 ...

  5. PHP程序Laravel框架的优化技巧

    Laravel是一套简洁.优雅的php Web开发框架(PHP Web Framework).它可以让你从杂乱的代码中解脱出来,可以帮你构建一个完美的网络app,而且每行代码都简洁.富于表达力.而性能 ...

  6. Laravel 5 框架性能优化技巧

    性能一直是 Laravel 框架为人诟病的一个点,所以调优 Laravel 程序算是一个必学的技能. 接下来分享一些开发的最佳实践,还有调优技巧,大家有别的建议也欢迎留言讨论 1.配置缓存信息 使用l ...

  7. CLion之C++框架篇-优化框架,单元测试(二)

    背景   结合上一篇CLion之C++框架篇-安装工具,基础框架的搭建(一),继续进行框架优化!   googletest(GTest)是Google开源的C++测试框架,与CLion组合,对C++环 ...

  8. 配置 FIS 来适配 go revel 框架以优化前端缓存策略

    对于前端工程师来说,浏览器缓存优化是个永远的话题.前几天看了知乎上的一个问答:<大公司里怎样开发和部署前端代码?>,深以为然,所以决心使用 FIS 来优化自身的前端文件. 我们的项目使用了 ...

  9. Jenkins Kubernetes Slave 调度效率优化小记

    Jenkins K8S Slave 调度效率优化 by yue994488@126.com 使用kubernetes为测试工具Gatling进行大规模压测,压测期间发现Jenkins调度压测实例较慢, ...

随机推荐

  1. C# 参数关键字params的作用

    为了将方法声明为可以接受可变数量参数的方法,我们可以使用params关键字来声明数组,要求: (1)在方法声明中的 params 关键字之后不允许任何其他参数,并且在方法声明中只允许一个 params ...

  2. MySQL学习【第九篇存储引擎】

    一.存储引擎介绍 1.我们知道mysql程序构成由连接层,sql层,存储引擎层.存储引擎层和磁盘进行交互,由其去取数据,而我们取得数据是表的形式展现出来,谁做的呢?就是存储引擎结构化成表的形式返回给用 ...

  3. svn配置教程

    检查svn是否安装rpm -aq subversion如果没有安装yum安装yum install -y subversion 建立svn版本数据库存储根目录mkdir -p /application ...

  4. python 实现查找某个字符在字符串中出现次数,并以字典形式输出

    把字符串'aenabsascd'中的字符出现的次数统计出来,并以字典形式输出 方法一: def count_str(str): dic={} for i in str: dic[i]=str.coun ...

  5. Linux各个文件及其含义

    树状目录结构: 以下是对这些目录的解释: /bin:bin是Binary的缩写, 这个目录存放着最经常使用的命令. /boot:这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像 ...

  6. PHP程序员学Objective-C之后的变化

    趣味坎谈,不一定100%准确,以自己的实际情况为准; 如题,我2008年开始学PHP,PHP是我学的第二门编程语言,一直用到现在,2010年初开始做iOS开发,学习了Objective-C,学这2门语 ...

  7. 对大数据的批量导入MySQL数据库

    自己的库里有索引在用insert导入数据时会变慢很多 使用事务+批量导入 可以配置使用spring+mybatis整合的方式关闭自动提交事务(地址),选择批量导入每一百条导入使用list存储值传入到m ...

  8. day 32 管道,信号量,进程池,线程的创建

    1.管道(了解) Pipe(): 在进程之间建立一条通道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道. ...

  9. Python3 break与continue

    Infi-chu: http://www.cnblogs.com/Infi-chu/ break和continue都是中断循环的意思,但是他们的中断后的效果不同. 请看如下两个例子就懂了 ''' 这个 ...

  10. 记账APP(4)

    依旧是一个表格类型的增删改查,但是呢,在用节点做,有点懵,明天加油