mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-15 13:43:49 +08:00
mod
This commit is contained in:
71
极客时间专栏/Python核心技术与实战/技术见闻与分享/41 | 硅谷一线互联网公司的工作体验.md
Normal file
71
极客时间专栏/Python核心技术与实战/技术见闻与分享/41 | 硅谷一线互联网公司的工作体验.md
Normal file
@@ -0,0 +1,71 @@
|
||||
<audio id="audio" title="41 | 硅谷一线互联网公司的工作体验" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bf/19/bf9e959571344f81837822311aebd919.mp3"></audio>
|
||||
|
||||
你好, 我是景霄。
|
||||
|
||||
前面四个版块,我们一起由浅入深地学习了Python这门语言,而最后一个版块,我想与你分享一些我的技术与工作见闻,谈谈我的领悟与理解。
|
||||
|
||||
首先,我想带你去了解一下,硅谷Top互联网公司的工作体验与文化,这里就以我工作的Facebook为例。
|
||||
|
||||
## 扁平化的管理制度
|
||||
|
||||
硅谷的一线互联网公司,都会实行扁平化的管理制度,当然FB也不例外。在这里,虽然也有上下级之分,比如 Software Engineer -> Engineering Manager -> Director -> VP,但是我们大家的思想中,并没有严格上下级这样的概念。
|
||||
|
||||
公司鼓励每个人积极发表自己的观点。比如,一个应届毕业生,因为一个问题和自己的老板,乃至老板的老板据理力争,这样的场景也是很常见的。
|
||||
|
||||
另外,公司每隔一段时间便会组织一次Q&A,我们大家可以向CEO、CTO等提问。比如你想了解某个产品的发展方向,公司目前的侧重点,甚至是一些敏感的问题,都可以提问。
|
||||
|
||||
同时,公司的领导,哪怕是上到CEO、CTO、COO这样的高层,都没有自己的单独办公室,都是和我们一起坐在开放的区域内办公,这样即拉近了距离,也是为了方便交流和讨论。
|
||||
|
||||
## 开放式的讨论平台
|
||||
|
||||
第二点是开放式的讨论平台。我一直觉得这个方式非常好,也很喜欢。FB用的是自己开发的workplace,相当于一个开放的社区,里面会有不同的群组,无论你有什么问题,都可以去相应的群组提问,那里会有各个领域的高手来帮你解答。
|
||||
|
||||
举个例子,如果你有Python相关的问题,便可以去Python的群组问;你如果有Spark的问题,就去Spark 群组问。
|
||||
|
||||
很多时候,各个组开发的产品,都会涉及很多的跨组合作,要用到其他组开发的一些API、算法、框架等等。这样,在使用的时候就难免会遇到一些问题,这个时候我们大家通常便会在对应的群组中提问。问题解决后也保存了下来,之后再有人遇到相同的问题时,便能直接搜索到对应的帖子及答案,大大提高了办公的效率。
|
||||
|
||||
除了上述Q&A形式的群组外,我们也会有很多其他形式的群组。比如,自己工作组内的群组,用于发布一些重要消息及技术交流;A/B测试的群组,用于大家讨论某个实验的结果等等。当然,还有很多非技术的群组,比如足球俱乐部、篮球俱乐部等用于休闲娱乐的平台。
|
||||
|
||||
在有了这么一个生态系统后,员工可以很方便地获取到自己想要的信息,也大大方便了公司内部员工的交流,可以算是一举多得的事情了。
|
||||
|
||||
## 数据驱动为中心
|
||||
|
||||
FB是一个典型的数据驱动型的公司,一切都以数据为依据,这样实际上极大地提高了工程师的地位。比如,在决定一个实验要不要最终发起时,我们都会首先关注各项指标,是不是能带来正向影响,是不是提高了用户的体验等等。
|
||||
|
||||
再比如,每次提出一个新项目时,我们都需要做大量的数据分析与调研,然后与组内的同事及上级领导 review 后再做决定。这样,每次绩效考核时,证明自己最好的依据,便是自己发起的实验对指标的提升等等。这样的一种策略,对于公司及个人的发展都更为有利。
|
||||
|
||||
举一个反例,之前的Snapchat,就是一个典型的不以数据驱动为中心的公司。他们产品的发布、改变,大多依赖一些产品经理和设计师的主观臆断,这样实际上是很偏颇的。后来的结局我们也都知道了,产品变得越来越不受用户喜欢,股价大跌,而我大部分在那里工作的同学,也都纷纷离职了。
|
||||
|
||||
## Bootcamp
|
||||
|
||||
Bootcamp是FB中很著名的一个项目,所有入职FB的员工,在正式进入具体的工作组之前,都会参加4-10周的Bootcamp;而每个员工也会分配一个导师,帮助其了解FB的技术栈、文化以及吃喝玩乐等等。
|
||||
|
||||
Bootcamp的前两周,通常会安排不少的课程,帮助新员工了解FB的内部工具。之后就会进入选组阶段,组和员工之间进行双向选择,形式通常是“聊天+做组内的项目”,这样双方都能对彼此有更深入的了解。
|
||||
|
||||
在Bootcamp期间,特别是对于应届毕业生来说,你可以尝试各种不同的方向,这对于未来的职业发展是非常有裨益的。公司也鼓励Bootcamp的员工参加各种娱乐活动,增进交流,而且这期间的吃喝玩乐都可以报销。我身边的每个同事都会有这样的感受:Bootcamp真是在公司最舒服的日子了。
|
||||
|
||||
## 鼓励工程师更换工作方向
|
||||
|
||||
在FB,无论是内部换组,还是更换工作方向都是非常普遍的现象。很多工程师在一个组做的时间久了,就会想尝试一些新的方向,这在公司是非常鼓励的。
|
||||
|
||||
方法也很简单,一般来说让你去新组做几个任务,或者花一个月的时间做一个Hackamonth就可以了。这种形式是对双方的考量,新组会对工程师的能力有一个大概的了解;而工程师也会对新组的工作、技术有所掌握,并进一步判断自己是否感兴趣。
|
||||
|
||||
因此,在FB,你会看到很多全栈工程师,比如我就是其中一个,对移动端、服务器端以及机器学习都有所涉猎。显然,这样的制度,非常有利于工程师的全面发展。
|
||||
|
||||
## 福利政策
|
||||
|
||||
FB的福利,应该可以算是全球互联网公司中最好之一了。公司为了留住人才,提供了很多外人看来非比寻常的福利。
|
||||
|
||||
首先从工位说起,其装备都是业内顶级标准。电脑是可以自己随意选配的,比如你可以随意选配7000多美金的iMac Pro,显示器也可以随意选配价值1000多美金的4K屏幕。至于可升降桌子和椅子,都是Herman Miller 标配,桌椅总价在2000美金以上。
|
||||
|
||||
在技术交流方面,除了正常的学习培训外,公司还鼓励员工每年外出参加一次会议,比如机器学习方向的ICML、KDD等等,给予全程报销。
|
||||
|
||||
另外,公司包一日三餐,包括内部的零食、甜品等全部免费。我们拥有一年21天带薪休假,女性还拥有6个月的带薪产假,同时提供免费的健身房、游泳池等,每年还会提供 720美金的健身私教报销等等。
|
||||
|
||||
## 写在最后
|
||||
|
||||
以上就是我在FB工作的主要工作体验。其实,在硅谷工作,不仅仅有技术上的收获,比如你可以直接接触到业内顶级大牛,了解到最新最前沿的技术;还有很多认知和思维方式上的影响,比如对于流程、合作、开源等的思考。
|
||||
|
||||
接下来的几篇文章,我会继续讲述,关于技术研发我这些年的工作经验和总结,以及对于职业方向的认识和思考。欢迎你在留言区和我一起讨论交流这些问题,经验分享和交流,是每个技术人成长必不可少的环节。
|
||||
|
||||
|
||||
76
极客时间专栏/Python核心技术与实战/技术见闻与分享/42 | 细数技术研发的注意事项.md
Normal file
76
极客时间专栏/Python核心技术与实战/技术见闻与分享/42 | 细数技术研发的注意事项.md
Normal file
@@ -0,0 +1,76 @@
|
||||
<audio id="audio" title="42 | 细数技术研发的注意事项" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3c/fb/3c9de0aa1d8bd05ef16014aea2c3f4fb.mp3"></audio>
|
||||
|
||||
你好,我是景霄。
|
||||
|
||||
技术研发一直以来都是各大公司的核心部分之一,其质量的好坏直接影响到了产品的质量以及用户对产品的体验。如何建立一套规范、健全的开发体系,就显得尤为重要。今天我就和你聊聊技术研发的注意事项。
|
||||
|
||||
## 选择合适的编程语言
|
||||
|
||||
比如我们正在开发一个系统,首先,根据具体的需求,我们需要对系统的各个部分选择合适的编程语言。一般来说,infra这层我们更偏向于使用C++,而纯的服务器端则是以Python、Java、PHP等等为主。以搜索引擎为例,下面我画了一个它的简略架构图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/72/d5/72caf6b3be8758651e6071bd49cb24d5.png" alt="">
|
||||
|
||||
你可以看到,大概的工作流程是:用户在客户端(client)输入一个查询(query),发送请求(request)到达服务器端(server-side);服务器端首先向NLP service发请求,并对请求进行分析,等到拿到各项信号(signal)后,再向后端(backend)发送请求;后端会做特征抽取(feature extraction),利用ML 模型进行结果的检索(candidate retrieval)、排序,最后再把结果返回给服务器端和客户端。
|
||||
|
||||
这里的NLP Service和后端,我们都会使用C++。因为这部分的处理最为复杂和耗时,都涉及到了特征抽取和model serving,对延迟(latency)的要求极高,只有C/C++这类语言才能满足需求。
|
||||
|
||||
而服务器端或者叫中间层(middle tier),我们则会使用Python、Java、PHP等语言。因为这部分并没有特别复杂的处理和对延迟的高需求,主要是以一些业务逻辑为主;并且,对程序员来说,使用这种高级语言也更容易上手和调试。
|
||||
|
||||
## 合理使用缓存
|
||||
|
||||
缓存(cache)在实际工程中十分重要,可以想像,如果没了缓存,我们今天所用的绝大多数产品估计都会崩溃。缓存为我们节约了大量的CPU 容量(capacity)和延迟。
|
||||
|
||||
还是以刚刚的搜索引擎系统为例,我们在客户端、服务器端和后端都会设置缓存。在客户端,我们一般会缓存用户的搜索记录,比如当你点击搜索框时,自动弹出的建议关键词的前几个,就可以是缓存的结果。这不需要向服务器端发请求,可以直接从客户端下载。
|
||||
|
||||
而在服务器端,我们也会设置缓存来存储一些搜索结果。这样,如果同一个用户多次发送相同的请求,就不需要再向后端请求,直接从缓存里面拿结果就可以了,快速又高效。
|
||||
|
||||
同样的,后端也需要缓存。比如在model serving这块儿,我们通常会有几个小时的缓存,不然每次提供实时的在线服务时,对CPU的负荷很大,延迟也会很高。
|
||||
|
||||
总而言之,如果没了缓存,容易造成很多问题。
|
||||
|
||||
- 服务器负荷迅速飙升,崩溃的几率大大增加。
|
||||
- 端对端的延迟迅速飙升,请求超时的概率大大增加。
|
||||
|
||||
但是不是缓存越多就越好呢?显然也不是。
|
||||
|
||||
第一,通常来说,缓存比较昂贵,所以在使用上,我们都会有一个限度,不能无限制索取。
|
||||
|
||||
第二,缓存不是万能的,过度增加缓存,也会损害用户的产品体验。比如搜索结果的retrieval和排序这两块,理想状况下,肯定是做实时的model serving最好,因为这样对用户的个性化推荐更准确和实时。之所以会对model有几个小时的缓存,更多的是出于性能的考虑,但如果把缓存从几小时改为几天,显然不合适,无疑会对用户的产品体验造成极大的负面影响。
|
||||
|
||||
因此,缓存到底取多久、取多少,往往是用户对产品参与度和性能的一个权衡,需要根据一些具体的分析以及A/B测试做出决定。
|
||||
|
||||
## 健全的日志记录系统
|
||||
|
||||
健全的日志记录系统也是尤其关键的一点。大型公司的系统,往往由成千十万个小系统组合而来,如果发生故障,比如Google、Facebook的某项服务突然宕机了,我们就需要以最快的速度找出原因并做出修复。这靠的是什么呢?靠的正是健全的日志记录系统,使得我们能够方便地分解错误原因,一层一层追溯,直到找到根源。
|
||||
|
||||
一般来说,在线上环境中,我们需要两种类型的日志记录模式。
|
||||
|
||||
一种是实时logging,考虑到服务器的压力,通常会做降采样(downsampling),比如log实际流量的1%。这样的好处是,可以及时跟踪各项指标,如果有情况,立即触发警报(alert)。
|
||||
|
||||
比如,某天的中午12点,一位工程师push了一段会造成服务器奔溃的代码进入产品,实时logging检测到异常,发出警报,这时有关人员便会进行排查。如果发现这个代码的push时间和警报触发时间一致,就能够最快地恢复(revert),最小化其带来的负面影响。
|
||||
|
||||
同时,实时logging也有利于我们进行各种线上实验。比如,ML组的A/B测试常常需要调参,我们的通常做法,就是每隔几小时查看实时 logging的table,根据各项指标,适度调整参数。
|
||||
|
||||
第二种是每天更新一次也就是daily的 full logging,有助于我们统计一些信息,进行分析,比如做成仪表板(dashboard),方便查看每天的各项指标,来跟踪进度。此外,full logging的table,也常常用于ML组的训练数据(training data)。
|
||||
|
||||
## Profiling必不可少
|
||||
|
||||
关于profile,之前我们也提到过,在实际开发中是非常重要的一项功能,能够帮助开发人员详细了解系统每个部分的效率,并加以提高。
|
||||
|
||||
在线上环境中,我们通常会在许多必要的地方加上profile的代码,这样我们就能够知道这段代码的延迟是多少,哪个部分的延迟特别严重等等,然后对症下药。
|
||||
|
||||
如果没有profile,很容易导致开发人员随意增加功能而不进行优化,这样以来,随着时间的推移,系统越来越冗余,延迟也会越来越高。因此,一个成熟的系统,一定会有profile的代码,帮助开发人员随时监控内部的各项指标变化。
|
||||
|
||||
## test、test、test
|
||||
|
||||
这一点,我也已经在前面的文章中强调过了,测试(test)一定不能少。无论是单元测试(unit test)、集成测试(integration test)还是其他,都是保证代码质量、减小bug发生概率的一个有效手段。
|
||||
|
||||
在真正规范的公司或是小组里,开发人员如果新增或改变了一个功能而不写测试,是过不了代码评审的。因此,测试一定要写,尤其是系统复杂了以后,很多工程师都要在上面开发各种不同的新功能,很难保证各个部分不受影响,测试便是一种很好的解决方法。
|
||||
|
||||
除了日常开发中所写的测试外,在代码push到线上之前,最好还要加一层测试。还是以刚刚的搜索引擎系统为例,我所知道的,Google或者Facebook的代码在push的过程中,都会有专门的service,去模拟不同的用户发送请求,然后看返回的响应是不是符合要求。如果出错,就会阻止代码的push,这也就告诉了开发人员,他们所写的代码可能存在问题,需要再次检查。
|
||||
|
||||
## 写在最后
|
||||
|
||||
关于技术研发的注意事项,我主要强调这些内容。事实上,日常开发工作中,很多的细节都值得特别关注,而对于易错的地方,用系统化的流程解决不失为一个高效的方案。那么,在你的日常工作中,有哪些特别留心的地方值得分享,或者有哪些疑惑的地方想要交流吗?欢迎在留言区写下你的想法。
|
||||
|
||||
|
||||
71
极客时间专栏/Python核心技术与实战/技术见闻与分享/43 | Q&A:聊一聊职业发展和选择.md
Normal file
71
极客时间专栏/Python核心技术与实战/技术见闻与分享/43 | Q&A:聊一聊职业发展和选择.md
Normal file
@@ -0,0 +1,71 @@
|
||||
<audio id="audio" title="43 | Q&A:聊一聊职业发展和选择" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a0/43/a0609a1961fc618b129025c2478ae943.mp3"></audio>
|
||||
|
||||
你好,我是景霄。
|
||||
|
||||
在前面几节课中,我分享了在FB工作的一些经验和感想,不少同学都提出了自己的困惑,也希望我能给出一些职业发展方面的建议。综合这些问题,我主要选取了下面三个主题,来说说职业发展、职业选择方面我的看法。
|
||||
|
||||
### Q:程序员的岗位主要有哪些类型?我该如何选择?
|
||||
|
||||
A:无论是在求职阶段,还是正式进入公司工作后,你都会发现,工程师普遍按技术的不同,分为下面几个岗位。
|
||||
|
||||
1. 前端:包括移动(Android、iOS)以及Web前端(JavaScript、CSS)开发。
|
||||
1. 后端(服务器端):主要是服务器端的开发,简单来说,就是输入为请求,输出为响应,发送给客户端。
|
||||
1. 算法:主要涉及到的是机器学习,比如推荐系统如何更好地实现个性化推荐,搜索引擎返回的结果如何才能更符合地用户的需求等等。
|
||||
1. 架构:涉及系统架构,偏底层,语言以C++为主。
|
||||
|
||||
从薪酬的角度来看,普遍来说:算法 > 架构 > 后端 > 前端。当然,这主要是由市场的供需关系决定的。
|
||||
|
||||
就拿算法岗来说,国内市场普遍缺少算法人才,也是因为这个岗位的培养难度更大,需要投入更大的精力。在顶尖互联网公司,参与核心产品研发的算法工程师们,工作三年,年收入100-200W人民币是很常见的。
|
||||
|
||||
不过,我这里所说的算法人才,绝不是指类似在校生那种,看过几篇论文,写过一些MATLAB,在学校做过几个科研项目的程度。算法工作岗位需要的算法能力,是你必须身体力行,有某些产品线的实践经历。还需要你真正了解市场,比如今日头条的推荐算法是怎样的,Google搜索引擎是怎么工作的,头条里的广告排序又是怎么做的等等。
|
||||
|
||||
再来说说架构,这也是目前一个热门的方向。我一直认为这是一个很偏工程、很硬核的领域,发展前景也相当不错,可以说是一个产品的基石。就拿刚刚提到的推荐系统来说,广告的定位和排序系统背后,都需要强有力的架构支撑。因此,这一行也可以称得上是人才紧缺,是企业舍得花高薪聘请的对象之一。
|
||||
|
||||
与算法不同的是,这个领域不会涉及很深的数学知识,工程师的主要关注点,在于如何提高系统性能,包括如何使系统高扩展、减小系统的延迟和所需CPU的容量等等。架构师需要很强的编程能力,常用的语言是C++;当然,最重要的还是不断积累大型项目中获得的第一手经验,对常见的问题有最principle的处理方式。
|
||||
|
||||
最后说说后端和前端,这是绝大多数程序员从事的岗位,也是我刚进公司时的选择。也许比起前两个岗位,不少人会认为,后端、前端工程师的薪酬较低,没有什么发展前景。这其实大错特错了!从一个产品的角度出发,你可以没有算法工程师、没有架构师,但是你能缺少后端和前端的开发人员吗?显然是不可能的。
|
||||
|
||||
后端和前端,相当于是一个产品的框架。框架搭好了,才会有机器学习、算法等的锦上添花。诚然,这两年来看,后端和前端没有前两者那么热门(还是市场供需关系的问题),但这并不代表,这些岗位没有发展前景,或者你就可以小看其技术含量。
|
||||
|
||||
比起算法和架构,后端、前端确实门槛更低些,但是其工作依然存在很高的技术含量。比如对一个产品或者其中的某些部件来说,如何设计搭建前后端的开发框架结构,使系统更加合理、可维护性更高,就是很多资深的开发工程师正在做的事。
|
||||
|
||||
前面聊了这么多,最后回到最根本的问题上:到底如何选择呢?
|
||||
|
||||
这里我给出的建议是:首先以自己的兴趣为出发点,因为只有自己感兴趣的东西,你才能做到最好。比如,一些人就是对前端感兴趣,那么为啥偏要去趟机器学习这趟浑水呢?当然不少人可能没有明确的偏好,那么这种情况下,我建议你尽可能多地去尝试,这是了解自己兴趣最好的方法。
|
||||
|
||||
另外,从广义的角度来看,计算机这门技术存在着study deep和study broad这两个方向,你得想清楚你属于哪类。所谓的study deep,就意味着数十年专攻一个领域,励志成为某个领域的专家;而study broad,便是类似于全栈工程师,对一个产品、系统的end to end都有一个了解,能够随时胜任任意角色的工作,这一点在初创公司身上体现得最为明显。
|
||||
|
||||
### Q:如何成为一个全栈工程师?
|
||||
|
||||
A:相信屏幕前的不少同学是在创业公司工作的,刚刚也提到了,创业公司里全栈工程师的需求尤为突出。那么,如何成为一个优秀的全栈工程师呢?
|
||||
|
||||
简单来说,最好的方法就是“尽可能地多接触、多实践不同领域的项目”。身体力行永远是学习新知识、提高能力的最好办法。
|
||||
|
||||
当然,在每个领域的初始阶段,你可能会感觉到异常艰难,比如从未接触过前端的人被要求写一个页面,一时间内显然会不知从何下手。这个时候,我建议你可以先从“依葫芦画瓢”开始,通过阅读别人相似的代码,并在此基础上加以修改,完成你要实现的功能。时间久了,你看的多了,用的多了,理解自然就越来越深,动起手来也就越来越熟练了。
|
||||
|
||||
有条件的同学,比如工作在类似于FB这种文化的公司,可以通过在公司内部换组的方式,去接触不同的项目。这自然是最好不过了,因为和特定领域的人合作,永远比一个人单干强得多,你能够迅速学到更多的东西。
|
||||
|
||||
不过,没这种条件的同学也不必绝望,你还可以利用业余时间“充电“,自己做一些项目来培养和加强别的领域的能力。毕竟,对于成年人来说,自学才是精进自己的主要方式。
|
||||
|
||||
这样,到了最后,你应该达到的结果便是,自己一个人能够扛起整条产品线的开发,也对系统的整个工作流程有一个全面而深入的理解。
|
||||
|
||||
### Q:学完本专栏后,在Python领域我该如何继续进阶呢?
|
||||
|
||||
A:在我看来,这个专栏的主要目的,是带你掌握Python这门语言的常见基本和高阶用法。接下来的进阶,便是Python本身在各种不同方向的运用,拿后端开发这个方向来说,比如,如何搭建大型系统的后台便是你需要掌握的。一个好的后端,自然离不开:
|
||||
|
||||
- 合理的系统、框架设计;
|
||||
- 简约高效的代码质量;
|
||||
- 稳健齐全的单元测试;
|
||||
- 出色的性能表现。
|
||||
|
||||
具体来说,你搭建的系统后端是不是易于拓展呢?比如过半年后,有了新的产品需求,需要增加新的功能。那么,在你的框架下,是否可以尽可能少地改动来实现新的功能,而不需要把某部分推倒重来呢?
|
||||
|
||||
再比如,你搭建的系统是不是符合可维护性高、可靠性高、单元测试齐全的要求,从而不容易在线上发生bug呢?
|
||||
|
||||
总之,在某一领域到了进阶的阶段,你需要关注的,绝不仅仅只是某些功能的实现,更需要你考虑所写代码的性能、质量,甚至于整个系统的设计等等。
|
||||
|
||||
虽然讲了这么多东西,但最后我想说的是,三百六十行,行行出状元。对于计算机行业,乃至整个职场来说,每一个领域都没有优劣之分,每个领域你都可以做得很牛逼,前提是你不懈地学习、实践和思考。
|
||||
|
||||
那么,对于职业选择和发展,你又是如何看待和理解的呢?欢迎留言和我一起交流探讨,也希望屏幕前的一直不懈学习的你,能找到属于自己的方向,不断前进和创新,实现自己的人生理想。
|
||||
|
||||
|
||||
977
极客时间专栏/Python核心技术与实战/技术见闻与分享/加餐 | 带你上手SWIG:一份清晰好用的SWIG编程实践指南.md
Normal file
977
极客时间专栏/Python核心技术与实战/技术见闻与分享/加餐 | 带你上手SWIG:一份清晰好用的SWIG编程实践指南.md
Normal file
@@ -0,0 +1,977 @@
|
||||
<audio id="audio" title="加餐 | 带你上手SWIG:一份清晰好用的SWIG编程实践指南" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f4/a2/f40b5ea3926293abe1eadc781c1c2aa2.mp3"></audio>
|
||||
|
||||
你好,我是卢誉声,Autodesk 数据平台和计算平台资深软件工程师,也是《移动平台深度神经网络实战》和《分布式实时处理系统:原理架构与实现》的作者,主要从事C/C++、JavaScript开发工作和平台架构方面的研发工作,对SWIG也有比较深的研究。很高兴受极客时间邀请来做本次分享,今天,我们就来聊一聊SWIG这个话题。
|
||||
|
||||
我们都知道,Python 是一门易于上手并实验友好的胶水语言。现在有很多机器学习开发或研究人员,都选择Python作为主力编程语言;流行的机器学习框架也都会提供Python语言的支持作为调用接口和工具。因此,相较于学习成本更高的C++来说,把Python作为进入机器学习世界的首选编程语言,就再合适不过了。
|
||||
|
||||
不过,像TensorFlow或PyTorch这样的机器学习框架的核心,是使用Python编写的吗?
|
||||
|
||||
显然不是。这里面的原因比较多,但最为显著的一个原因就是“性能”。通过C++编写的机器学习框架内核,加上编译器的优化能力,为系统提供了接近于机器码执行的效率。这种得天独厚的优势,让C++在机器学习的核心领域站稳了脚跟。我们前面所说的TensorFlow和PyTorch的核心,便都是使用C/C++开发的。其中,TensorFlow的内核,就是由高度优化的C++代码和CUDA编写而成。
|
||||
|
||||
因此,我们可以理解为,TensorFlow通过Python来描述模型,而实际的运算则是由高性能C++代码执行的。而且,在绝大多数情况下,不同操作之间传递的数据,并不会拷贝回Python代码的执行空间。机器学习框架,正是通过这样的方式确保了计算性能,同时兼顾了对框架易用性方面的考虑。
|
||||
|
||||
因此,当Python和C++结合使用的时候,Python本身的性能瓶颈就不那么重要了。它足够胜任我们给它的任务就可以了,至于对计算有更高要求的任务,就交给C++来做吧!
|
||||
|
||||
今天,我们就来讨论下,如何通过SWIG对C++程序进行Python封装。我会先带你编写一段Python脚本,来执行一个简单的机器学习任务;接着,尝试将计算密集的部分改写成C++程序,再通过SWIG对其进行封装。最后的结果就是,Python把计算密集的任务委托给C++执行。
|
||||
|
||||
我们会对性能做一个简单比较,并在这个过程中,讲解使用SWIG的方法。同时,在今天这节课的最后,我会为你提供一个学习路径,作为日后提高的参考。
|
||||
|
||||
明确了今天的学习目的,也就是使用SWIG来实现Python对C++代码的调用,那么,我们今天的内容,其实可以看成一份**关于SWIG的编程实践指南**。学习这份指南之前,我们先来简单了解一下SWIG。
|
||||
|
||||
## SWIG 是什么?
|
||||
|
||||
SWIG,是一款能够连接C/C++与多种高级编程语言(我们在这里特别强调Python)的软件开发工具。SWIG支持多种不同类型的目标语言,这其中,支持的常见脚本语言包括JavaScript、Perl、PHP、Tcl、Ruby和Python等,支持的高级编程语言则包括C#、D、Go语言、Java(包括对Android的支持)、Lua、OCaml、Octave、Scilab和R。
|
||||
|
||||
我们通常使用SWIG来创建高级解释或编译型的编程环境和接口,它也常被用来当作C/C++编写原型的测试工具。一个典型的应用场景,便是解析和创建C/C++接口,生成胶水代码供像Python这样的高级编程语言调用。近期发布的4.0.0版本,更是带来了对C++的显著改进和支持,这其中包括(不局限于)下面几点。
|
||||
|
||||
- 针对C#、Java和Ruby而改进的STL包装器。
|
||||
- 针对Java、Python和Ruby,增加C++11标准下的STL容器的支持。
|
||||
- 改进了对C++11和C++14代码的支持。
|
||||
- 修正了C++中对智能指针shared_ptr的一系列bug修复。
|
||||
- 一系列针对C预处理器的极端case修复。
|
||||
- 一系列针对成员函数指针问题的修复。
|
||||
- 低支持的Python版本为2.7、3.2-3.7。
|
||||
|
||||
## 使用Python实现PCA算法
|
||||
|
||||
借助于SWIG,我们可以简单地实现用Python调用C/C++库,甚至可以用Python继承和使用C++类。接下来,我们先来看一个你十分熟悉的使用Python编写的PCA(Principal Component Analysis,主成分分析)算法。
|
||||
|
||||
因为我们今天的目标不是讲解PCA算法,所以如果你对这个算法还不是很熟悉,也没有关系,我会直接给出具体的代码,我们把焦点放在如何使用SWIG上就可以了。下面,我先给出代码清单1。
|
||||
|
||||
代码清单1,基于Python编写的PCA算法 `testPCAPurePython.py` :
|
||||
|
||||
```
|
||||
import numpy as np
|
||||
|
||||
def compute_pca(data):
|
||||
m = np.mean(data, axis=0)
|
||||
datac = np.array([obs - m for obs in data])
|
||||
T = np.dot(datac, datac.T)
|
||||
[u,s,v] = np.linalg.svd(T)
|
||||
|
||||
pcs = [np.dot(datac.T, item) for item in u.T ]
|
||||
|
||||
pcs = np.array([d / np.linalg.norm(d) for d in pcs])
|
||||
|
||||
return pcs, m, s, T, u
|
||||
|
||||
def compute_projections(I,pcs,m):
|
||||
projections = []
|
||||
for i in I:
|
||||
w = []
|
||||
for p in pcs:
|
||||
w.append(np.dot(i - m, p))
|
||||
projections.append(w)
|
||||
return projections
|
||||
|
||||
def reconstruct(w, X, m,dim = 5):
|
||||
return np.dot(w[:dim],X[:dim,:]) + m
|
||||
|
||||
def normalize(samples, maxs = None):
|
||||
if not maxs:
|
||||
maxs = np.max(samples)
|
||||
return np.array([np.ravel(s) / maxs for s in samples])
|
||||
|
||||
```
|
||||
|
||||
现在,我们保存这段编写好的代码,并通过下面的命令来执行:
|
||||
|
||||
```
|
||||
python3 testPCAPurePython.py
|
||||
|
||||
```
|
||||
|
||||
## 准备SWIG
|
||||
|
||||
这样,我们已经获得了一些进展——使用Python编写了一个PCA算法,并得到了一些结果。接下来,我们看一下如何开始SWIG的开发工作。我会先从编译相关组件开始,再介绍一个简单使用的例子,为后续内容做准备。
|
||||
|
||||
首先,我们从SWIG的网站([http://swig.org/download.html](http://swig.org/download.html))下载源代码包,并开始构建:
|
||||
|
||||
```
|
||||
$ wget https://newcontinuum.dl.sourceforge.net/project/swig/swig/swig-4.0.0/swig-4.0.0.tar.gz # 下载路径可能会有所变化
|
||||
$ tar -xvf swig-4.0.0.tar.gz
|
||||
$ cd swig-4.0.0
|
||||
$ wget https://ftp.pcre.org/pub/pcre/pcre-8.43.tar.gz # SWIG需要依赖pcre工作
|
||||
$ sh ./Tools/pcre-build.sh # 该脚本会将pcre自动构建成SWIG使用的静态库
|
||||
$ ./configure # 注意需要安装bison,如果没有安装需要读者手动安装
|
||||
$ make
|
||||
$ sudo make install
|
||||
|
||||
```
|
||||
|
||||
一切就绪后,我们就来编写一个简单的例子吧。这个例子同样来源于SWIG网站([http://swig.org/tutorial.html](http://swig.org/tutorial.html))。我们先来创建一个简单的c文件,你可以通过你习惯使用的文本编辑器(比如vi),创建一个名为`example.c`的文件,并编写代码。代码内容我放在了代码清单2中。
|
||||
|
||||
代码清单2,`example.c`:
|
||||
|
||||
```
|
||||
#include <time.h>
|
||||
double My_variable = 3.0;
|
||||
|
||||
int fact(int n) {
|
||||
if (n <= 1) return 1;
|
||||
else return n*fact(n-1);
|
||||
}
|
||||
|
||||
int my_mod(int x, int y) {
|
||||
return (x%y);
|
||||
}
|
||||
|
||||
char *get_time()
|
||||
{
|
||||
time_t ltime;
|
||||
time(&ltime);
|
||||
return ctime(&ltime);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接下来,我们编写一个名为`example.i`的接口定义文件,和稍后用作测试的Python脚本,内容如代码清单3和代码清单4所示。
|
||||
|
||||
代码清单3,`example.i`:
|
||||
|
||||
```
|
||||
%module example
|
||||
%{
|
||||
/* Put header files here or function declarations like below */
|
||||
extern double My_variable;
|
||||
extern int fact(int n);
|
||||
extern int my_mod(int x, int y);
|
||||
extern char *get_time();
|
||||
%}
|
||||
|
||||
extern double My_variable;
|
||||
extern int fact(int n);
|
||||
extern int my_mod(int x, int y);
|
||||
extern char *get_time();
|
||||
|
||||
```
|
||||
|
||||
我来解释下清单3这段代码。第1行,我们定义了模块的名称为example。第2-8行,我们直接指定了`example.c`中的函数定义,也可以定义一个`example.h`头文件,并将这些定义加入其中;然后,在 `%{ … %}`结构体中包含`example.h`,来实现相同的功能。第`10-13`行,则是定义了导出的接口,以便你在Python中直接调用这些接口。
|
||||
|
||||
代码清单4,`testExample.py`:
|
||||
|
||||
```
|
||||
import example
|
||||
print(example.fact(5))
|
||||
print(example.my_mod(7,3))
|
||||
print(example.get_time())
|
||||
|
||||
```
|
||||
|
||||
好了, 到现在为止,我们已经准备就绪了。现在,我们来执行下面的代码,创建目标文件和最后链接的文件吧:
|
||||
|
||||
```
|
||||
swig -python example.i
|
||||
gcc -c -fPIC example.c example_wrap.c -I/usr/include/python3.6
|
||||
gcc -shared example.o example_wrap.o -o _example.so
|
||||
python3 testExample.py # 测试调用
|
||||
|
||||
```
|
||||
|
||||
其实,从代码清单4中你也能够看到,通过导入example,我们可以直接在Python脚本中,调用使用C实现的函数接口,并获得返回值。
|
||||
|
||||
## 通过SWIG封装基于C++编写的Python模块
|
||||
|
||||
到这一步,我们已经准备好了一份使用C++编写的PCA算法,接下来,我们就要对其进行一个简单的封装。由于C++缺少线性代数的官方支持,因此,为了简化线性代数运算,我这里用了一个第三方库Armadillo。在Ubuntu下,它可以使用`apt-get install libarmadillo-dev`安装支持。
|
||||
|
||||
另外,还是要再三说明一下,我们今天这节课的重点并不是讲解PCA算法本身,所以希望你不要困于此处,而错过了真正的使用方法。当然,为了完整性考虑,我还是会对代码做出最基本的解释。
|
||||
|
||||
封装正式开始。我们先来编写一个名为`pca.h`的头文件定义,内容我放在了代码清单5中。
|
||||
|
||||
代码清单5,`pca.h`:
|
||||
|
||||
```
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <armadillo>
|
||||
|
||||
class pca {
|
||||
public:
|
||||
pca();
|
||||
explicit pca(long num_vars);
|
||||
virtual ~pca();
|
||||
|
||||
bool operator==(const pca& other);
|
||||
|
||||
void set_num_variables(long num_vars);
|
||||
long get_num_variables() const;
|
||||
void add_record(const std::vector<double>& record);
|
||||
std::vector<double> get_record(long record_index) const;
|
||||
long get_num_records() const;
|
||||
void set_do_normalize(bool do_normalize);
|
||||
bool get_do_normalize() const;
|
||||
void set_solver(const std::string& solver);
|
||||
std::string get_solver() const;
|
||||
|
||||
void solve();
|
||||
|
||||
double check_eigenvectors_orthogonal() const;
|
||||
double check_projection_accurate() const;
|
||||
|
||||
void save(const std::string& basename) const;
|
||||
void load(const std::string& basename);
|
||||
|
||||
void set_num_retained(long num_retained);
|
||||
long get_num_retained() const;
|
||||
std::vector<double> to_principal_space(const std::vector<double>& record) const;
|
||||
std::vector<double> to_variable_space(const std::vector<double>& data) const;
|
||||
double get_energy() const;
|
||||
double get_eigenvalue(long eigen_index) const;
|
||||
std::vector<double> get_eigenvalues() const;
|
||||
std::vector<double> get_eigenvector(long eigen_index) const;
|
||||
std::vector<double> get_principal(long eigen_index) const;
|
||||
std::vector<double> get_mean_values() const;
|
||||
std::vector<double> get_sigma_values() const;
|
||||
|
||||
protected:
|
||||
long num_vars_;
|
||||
long num_records_;
|
||||
long record_buffer_;
|
||||
std::string solver_;
|
||||
bool do_normalize_;
|
||||
long num_retained_;
|
||||
arma::Mat<double> data_;
|
||||
arma::Col<double> energy_;
|
||||
arma::Col<double> eigval_;
|
||||
arma::Mat<double> eigvec_;
|
||||
arma::Mat<double> proj_eigvec_;
|
||||
arma::Mat<double> princomp_;
|
||||
arma::Col<double> mean_;
|
||||
arma::Col<double> sigma_;
|
||||
void initialize_();
|
||||
void assert_num_vars_();
|
||||
void resize_data_if_needed_();
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
接着,我们再来编写具体实现`pca.cpp`,也就是代码清单6的内容。
|
||||
|
||||
代码清单6,`pca.cpp`:
|
||||
|
||||
```
|
||||
#include "pca.h"
|
||||
#include "utils.h"
|
||||
#include <stdexcept>
|
||||
#include <random>
|
||||
|
||||
pca::pca()
|
||||
: num_vars_(0),
|
||||
num_records_(0),
|
||||
record_buffer_(1000),
|
||||
solver_("dc"),
|
||||
do_normalize_(false),
|
||||
num_retained_(1),
|
||||
energy_(1)
|
||||
{}
|
||||
|
||||
pca::pca(long num_vars)
|
||||
: num_vars_(num_vars),
|
||||
num_records_(0),
|
||||
record_buffer_(1000),
|
||||
solver_("dc"),
|
||||
do_normalize_(false),
|
||||
num_retained_(num_vars_),
|
||||
data_(record_buffer_, num_vars_),
|
||||
energy_(1),
|
||||
eigval_(num_vars_),
|
||||
eigvec_(num_vars_, num_vars_),
|
||||
proj_eigvec_(num_vars_, num_vars_),
|
||||
princomp_(record_buffer_, num_vars_),
|
||||
mean_(num_vars_),
|
||||
sigma_(num_vars_)
|
||||
{
|
||||
assert_num_vars_();
|
||||
initialize_();
|
||||
}
|
||||
|
||||
pca::~pca()
|
||||
{}
|
||||
|
||||
bool pca::operator==(const pca& other) {
|
||||
const double eps = 1e-5;
|
||||
if (num_vars_ == other.num_vars_ &&
|
||||
num_records_ == other.num_records_ &&
|
||||
record_buffer_ == other.record_buffer_ &&
|
||||
solver_ == other.solver_ &&
|
||||
do_normalize_ == other.do_normalize_ &&
|
||||
num_retained_ == other.num_retained_ &&
|
||||
utils::is_approx_equal_container(eigval_, other.eigval_, eps) &&
|
||||
utils::is_approx_equal_container(eigvec_, other.eigvec_, eps) &&
|
||||
utils::is_approx_equal_container(princomp_, other.princomp_, eps) &&
|
||||
utils::is_approx_equal_container(energy_, other.energy_, eps) &&
|
||||
utils::is_approx_equal_container(mean_, other.mean_, eps) &&
|
||||
utils::is_approx_equal_container(sigma_, other.sigma_, eps) &&
|
||||
utils::is_approx_equal_container(proj_eigvec_, other.proj_eigvec_, eps))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
void pca::resize_data_if_needed_() {
|
||||
if (num_records_ == record_buffer_) {
|
||||
record_buffer_ += record_buffer_;
|
||||
data_.resize(record_buffer_, num_vars_);
|
||||
}
|
||||
}
|
||||
|
||||
void pca::assert_num_vars_() {
|
||||
if (num_vars_ < 2)
|
||||
throw std::invalid_argument("Number of variables smaller than two.");
|
||||
}
|
||||
|
||||
void pca::initialize_() {
|
||||
data_.zeros();
|
||||
eigval_.zeros();
|
||||
eigvec_.zeros();
|
||||
princomp_.zeros();
|
||||
mean_.zeros();
|
||||
sigma_.zeros();
|
||||
energy_.zeros();
|
||||
}
|
||||
|
||||
void pca::set_num_variables(long num_vars) {
|
||||
num_vars_ = num_vars;
|
||||
assert_num_vars_();
|
||||
num_retained_ = num_vars_;
|
||||
data_.resize(record_buffer_, num_vars_);
|
||||
eigval_.resize(num_vars_);
|
||||
eigvec_.resize(num_vars_, num_vars_);
|
||||
mean_.resize(num_vars_);
|
||||
sigma_.resize(num_vars_);
|
||||
initialize_();
|
||||
}
|
||||
|
||||
void pca::add_record(const std::vector<double>& record) {
|
||||
assert_num_vars_();
|
||||
|
||||
if (num_vars_ != long(record.size()))
|
||||
throw std::domain_error(utils::join("Record has the wrong size: ", record.size()));
|
||||
|
||||
resize_data_if_needed_();
|
||||
arma::Row<double> row(&record.front(), record.size());
|
||||
data_.row(num_records_) = std::move(row);
|
||||
++num_records_;
|
||||
}
|
||||
|
||||
std::vector<double> pca::get_record(long record_index) const {
|
||||
return std::move(utils::extract_row_vector(data_, record_index));
|
||||
}
|
||||
|
||||
void pca::set_do_normalize(bool do_normalize) {
|
||||
do_normalize_ = do_normalize;
|
||||
}
|
||||
|
||||
void pca::set_solver(const std::string& solver) {
|
||||
if (solver!="standard" && solver!="dc")
|
||||
throw std::invalid_argument(utils::join("No such solver available: ", solver));
|
||||
solver_ = solver;
|
||||
}
|
||||
|
||||
void pca::solve() {
|
||||
assert_num_vars_();
|
||||
|
||||
if (num_records_ < 2)
|
||||
throw std::logic_error("Number of records smaller than two.");
|
||||
|
||||
data_.resize(num_records_, num_vars_);
|
||||
|
||||
mean_ = utils::compute_column_means(data_);
|
||||
utils::remove_column_means(data_, mean_);
|
||||
|
||||
sigma_ = utils::compute_column_rms(data_);
|
||||
if (do_normalize_) utils::normalize_by_column(data_, sigma_);
|
||||
|
||||
arma::Col<double> eigval(num_vars_);
|
||||
arma::Mat<double> eigvec(num_vars_, num_vars_);
|
||||
|
||||
arma::Mat<double> cov_mat = utils::make_covariance_matrix(data_);
|
||||
arma::eig_sym(eigval, eigvec, cov_mat, solver_.c_str());
|
||||
arma::uvec indices = arma::sort_index(eigval, 1);
|
||||
|
||||
for (long i=0; i<num_vars_; ++i) {
|
||||
eigval_(i) = eigval(indices(i));
|
||||
eigvec_.col(i) = eigvec.col(indices(i));
|
||||
}
|
||||
|
||||
utils::enforce_positive_sign_by_column(eigvec_);
|
||||
proj_eigvec_ = eigvec_;
|
||||
|
||||
princomp_ = data_ * eigvec_;
|
||||
|
||||
energy_(0) = arma::sum(eigval_);
|
||||
eigval_ *= 1./energy_(0);
|
||||
}
|
||||
|
||||
void pca::set_num_retained(long num_retained) {
|
||||
if (num_retained<=0 || num_retained>num_vars_)
|
||||
throw std::range_error(utils::join("Value out of range: ", num_retained));
|
||||
|
||||
num_retained_ = num_retained;
|
||||
proj_eigvec_ = eigvec_.submat(0, 0, eigvec_.n_rows-1, num_retained_-1);
|
||||
}
|
||||
|
||||
std::vector<double> pca::to_principal_space(const std::vector<double>& data) const {
|
||||
arma::Col<double> column(&data.front(), data.size());
|
||||
column -= mean_;
|
||||
if (do_normalize_) column /= sigma_;
|
||||
const arma::Row<double> row(column.t() * proj_eigvec_);
|
||||
return std::move(utils::extract_row_vector(row, 0));
|
||||
}
|
||||
|
||||
std::vector<double> pca::to_variable_space(const std::vector<double>& data) const {
|
||||
const arma::Row<double> row(&data.front(), data.size());
|
||||
arma::Col<double> column(arma::trans(row * proj_eigvec_.t()));
|
||||
if (do_normalize_) column %= sigma_;
|
||||
column += mean_;
|
||||
return std::move(utils::extract_column_vector(column, 0));
|
||||
}
|
||||
|
||||
double pca::get_energy() const {
|
||||
return energy_(0);
|
||||
}
|
||||
|
||||
double pca::get_eigenvalue(long eigen_index) const {
|
||||
if (eigen_index >= num_vars_)
|
||||
throw std::range_error(utils::join("Index out of range: ", eigen_index));
|
||||
return eigval_(eigen_index);
|
||||
}
|
||||
|
||||
std::vector<double> pca::get_eigenvalues() const {
|
||||
return std::move(utils::extract_column_vector(eigval_, 0));
|
||||
}
|
||||
|
||||
std::vector<double> pca::get_eigenvector(long eigen_index) const {
|
||||
return std::move(utils::extract_column_vector(eigvec_, eigen_index));
|
||||
}
|
||||
|
||||
std::vector<double> pca::get_principal(long eigen_index) const {
|
||||
return std::move(utils::extract_column_vector(princomp_, eigen_index));
|
||||
}
|
||||
|
||||
double pca::check_eigenvectors_orthogonal() const {
|
||||
return std::abs(arma::det(eigvec_));
|
||||
}
|
||||
|
||||
double pca::check_projection_accurate() const {
|
||||
if (data_.n_cols!=eigvec_.n_cols || data_.n_rows!=princomp_.n_rows)
|
||||
throw std::runtime_error("No proper data matrix present that the projection could be compared with.");
|
||||
const arma::Mat<double> diff = (princomp_ * arma::trans(eigvec_)) - data_;
|
||||
return 1 - arma::sum(arma::sum( arma::abs(diff) )) / diff.n_elem;
|
||||
}
|
||||
|
||||
bool pca::get_do_normalize() const {
|
||||
return do_normalize_;
|
||||
}
|
||||
|
||||
std::string pca::get_solver() const {
|
||||
return solver_;
|
||||
}
|
||||
|
||||
std::vector<double> pca::get_mean_values() const {
|
||||
return std::move(utils::extract_column_vector(mean_, 0));
|
||||
}
|
||||
|
||||
std::vector<double> pca::get_sigma_values() const {
|
||||
return std::move(utils::extract_column_vector(sigma_, 0));
|
||||
}
|
||||
|
||||
long pca::get_num_variables() const {
|
||||
return num_vars_;
|
||||
}
|
||||
|
||||
long pca::get_num_records() const {
|
||||
return num_records_;
|
||||
}
|
||||
|
||||
long pca::get_num_retained() const {
|
||||
return num_retained_;
|
||||
}
|
||||
|
||||
void pca::save(const std::string& basename) const {
|
||||
const std::string filename = basename + ".pca";
|
||||
std::ofstream file(filename.c_str());
|
||||
utils::assert_file_good(file.good(), filename);
|
||||
utils::write_property(file, "num_variables", num_vars_);
|
||||
utils::write_property(file, "num_records", num_records_);
|
||||
utils::write_property(file, "solver", solver_);
|
||||
utils::write_property(file, "num_retained", num_retained_);
|
||||
utils::write_property(file, "do_normalize", do_normalize_);
|
||||
file.close();
|
||||
|
||||
utils::write_matrix_object(basename + ".eigval", eigval_);
|
||||
utils::write_matrix_object(basename + ".eigvec", eigvec_);
|
||||
utils::write_matrix_object(basename + ".princomp", princomp_);
|
||||
utils::write_matrix_object(basename + ".energy", energy_);
|
||||
utils::write_matrix_object(basename + ".mean", mean_);
|
||||
utils::write_matrix_object(basename + ".sigma", sigma_);
|
||||
}
|
||||
|
||||
void pca::load(const std::string& basename) {
|
||||
const std::string filename = basename + ".pca";
|
||||
std::ifstream file(filename.c_str());
|
||||
utils::assert_file_good(file.good(), filename);
|
||||
utils::read_property(file, "num_variables", num_vars_);
|
||||
utils::read_property(file, "num_records", num_records_);
|
||||
utils::read_property(file, "solver", solver_);
|
||||
utils::read_property(file, "num_retained", num_retained_);
|
||||
utils::read_property(file, "do_normalize", do_normalize_);
|
||||
file.close();
|
||||
|
||||
utils::read_matrix_object(basename + ".eigval", eigval_);
|
||||
utils::read_matrix_object(basename + ".eigvec", eigvec_);
|
||||
utils::read_matrix_object(basename + ".princomp", princomp_);
|
||||
utils::read_matrix_object(basename + ".energy", energy_);
|
||||
utils::read_matrix_object(basename + ".mean", mean_);
|
||||
utils::read_matrix_object(basename + ".sigma", sigma_);
|
||||
|
||||
set_num_retained(num_retained_);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里要注意了,代码清单6中用到了`utils.h`这个文件,它是对部分矩阵和数学计算的封装,内容我放在了代码清单7中。
|
||||
|
||||
代码清单7,`utils.h`:
|
||||
|
||||
```
|
||||
#pragma once
|
||||
|
||||
#include <armadillo>
|
||||
#include <sstream>
|
||||
|
||||
namespace utils {
|
||||
arma::Mat<double> make_covariance_matrix(const arma::Mat<double>& data);
|
||||
arma::Mat<double> make_shuffled_matrix(const arma::Mat<double>& data);
|
||||
arma::Col<double> compute_column_means(const arma::Mat<double>& data);
|
||||
void remove_column_means(arma::Mat<double>& data, const arma::Col<double>& means);
|
||||
arma::Col<double> compute_column_rms(const arma::Mat<double>& data);
|
||||
void normalize_by_column(arma::Mat<double>& data, const arma::Col<double>& rms);
|
||||
void enforce_positive_sign_by_column(arma::Mat<double>& data);
|
||||
std::vector<double> extract_column_vector(const arma::Mat<double>& data, long index);
|
||||
std::vector<double> extract_row_vector(const arma::Mat<double>& data, long index);
|
||||
void assert_file_good(const bool& is_file_good, const std::string& filename);
|
||||
template<typename T>
|
||||
void write_matrix_object(const std::string& filename, const T& matrix) {
|
||||
assert_file_good(matrix.quiet_save(filename, arma::arma_ascii), filename);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void read_matrix_object(const std::string& filename, T& matrix) {
|
||||
assert_file_good(matrix.quiet_load(filename), filename);
|
||||
}
|
||||
template<typename T, typename U, typename V>
|
||||
bool is_approx_equal(const T& value1, const U& value2, const V& eps) {
|
||||
return std::abs(value1-value2)<eps ? true : false;
|
||||
}
|
||||
template<typename T, typename U, typename V>
|
||||
bool is_approx_equal_container(const T& container1, const U& container2, const V& eps) {
|
||||
if (container1.size()==container2.size()) {
|
||||
bool equal = true;
|
||||
for (size_t i=0; i<container1.size(); ++i) {
|
||||
equal = is_approx_equal(container1[i], container2[i], eps);
|
||||
if (!equal) break;
|
||||
}
|
||||
return equal;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
double get_mean(const std::vector<double>& iter);
|
||||
double get_sigma(const std::vector<double>& iter);
|
||||
|
||||
struct join_helper {
|
||||
static void add_to_stream(std::ostream& stream) {}
|
||||
|
||||
template<typename T, typename... Args>
|
||||
static void add_to_stream(std::ostream& stream, const T& arg, const Args&... args) {
|
||||
stream << arg;
|
||||
add_to_stream(stream, args...);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, typename... Args>
|
||||
std::string join(const T& arg, const Args&... args) {
|
||||
std::ostringstream stream;
|
||||
stream << arg;
|
||||
join_helper::add_to_stream(stream, args...);
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void write_property(std::ostream& file, const std::string& key, const T& value) {
|
||||
file << key << "\t" << value << std::endl;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void read_property(std::istream& file, const std::string& key, T& value) {
|
||||
std::string tmp;
|
||||
bool found = false;
|
||||
while (file.good()) {
|
||||
file >> tmp;
|
||||
if (tmp==key) {
|
||||
file >> value;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
throw std::domain_error(join("No such key available: ", key));
|
||||
file.seekg(0);
|
||||
}
|
||||
|
||||
} //utils
|
||||
|
||||
```
|
||||
|
||||
至于具体的实现代码,我放在了在代码清单8`utils.cpp`中。
|
||||
|
||||
代码清单8,`utils.cpp`:
|
||||
|
||||
```
|
||||
#include "utils.h"
|
||||
#include <stdexcept>
|
||||
#include <sstream>
|
||||
#include <numeric>
|
||||
|
||||
namespace utils {
|
||||
|
||||
arma::Mat<double> make_covariance_matrix(const arma::Mat<double>& data) {
|
||||
return std::move( (data.t()*data) * (1./(data.n_rows-1)) );
|
||||
}
|
||||
|
||||
arma::Mat<double> make_shuffled_matrix(const arma::Mat<double>& data) {
|
||||
const long n_rows = data.n_rows;
|
||||
const long n_cols = data.n_cols;
|
||||
arma::Mat<double> shuffle(n_rows, n_cols);
|
||||
for (long j=0; j<n_cols; ++j) {
|
||||
for (long i=0; i<n_rows; ++i) {
|
||||
shuffle(i, j) = data(std::rand()%n_rows, j);
|
||||
}
|
||||
}
|
||||
return std::move(shuffle);
|
||||
}
|
||||
|
||||
arma::Col<double> compute_column_means(const arma::Mat<double>& data) {
|
||||
const long n_cols = data.n_cols;
|
||||
arma::Col<double> means(n_cols);
|
||||
for (long i=0; i<n_cols; ++i)
|
||||
means(i) = arma::mean(data.col(i));
|
||||
return std::move(means);
|
||||
}
|
||||
|
||||
void remove_column_means(arma::Mat<double>& data, const arma::Col<double>& means) {
|
||||
if (data.n_cols != means.n_elem)
|
||||
throw std::range_error("Number of elements of means is not equal to the number of columns of data");
|
||||
for (long i=0; i<long(data.n_cols); ++i)
|
||||
data.col(i) -= means(i);
|
||||
}
|
||||
|
||||
arma::Col<double> compute_column_rms(const arma::Mat<double>& data) {
|
||||
const long n_cols = data.n_cols;
|
||||
arma::Col<double> rms(n_cols);
|
||||
for (long i=0; i<n_cols; ++i) {
|
||||
const double dot = arma::dot(data.col(i), data.col(i));
|
||||
rms(i) = std::sqrt(dot / (data.col(i).n_rows-1));
|
||||
}
|
||||
return std::move(rms);
|
||||
}
|
||||
|
||||
void normalize_by_column(arma::Mat<double>& data, const arma::Col<double>& rms) {
|
||||
if (data.n_cols != rms.n_elem)
|
||||
throw std::range_error("Number of elements of rms is not equal to the number of columns of data");
|
||||
for (long i=0; i<long(data.n_cols); ++i) {
|
||||
if (rms(i)==0)
|
||||
throw std::runtime_error("At least one of the entries of rms equals to zero");
|
||||
data.col(i) *= 1./rms(i);
|
||||
}
|
||||
}
|
||||
|
||||
void enforce_positive_sign_by_column(arma::Mat<double>& data) {
|
||||
for (long i=0; i<long(data.n_cols); ++i) {
|
||||
const double max = arma::max(data.col(i));
|
||||
const double min = arma::min(data.col(i));
|
||||
bool change_sign = false;
|
||||
if (std::abs(max)>=std::abs(min)) {
|
||||
if (max<0) change_sign = true;
|
||||
} else {
|
||||
if (min<0) change_sign = true;
|
||||
}
|
||||
if (change_sign) data.col(i) *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<double> extract_column_vector(const arma::Mat<double>& data, long index) {
|
||||
if (index<0 || index >= long(data.n_cols))
|
||||
throw std::range_error(join("Index out of range: ", index));
|
||||
const long n_rows = data.n_rows;
|
||||
const double* memptr = data.colptr(index);
|
||||
std::vector<double> result(memptr, memptr + n_rows);
|
||||
return std::move(result);
|
||||
}
|
||||
|
||||
std::vector<double> extract_row_vector(const arma::Mat<double>& data, long index) {
|
||||
if (index<0 || index >= long(data.n_rows))
|
||||
throw std::range_error(join("Index out of range: ", index));
|
||||
const arma::Row<double> row(data.row(index));
|
||||
const double* memptr = row.memptr();
|
||||
std::vector<double> result(memptr, memptr + row.n_elem);
|
||||
return std::move(result);
|
||||
}
|
||||
|
||||
void assert_file_good(const bool& is_file_good, const std::string& filename) {
|
||||
if (!is_file_good)
|
||||
throw std::ios_base::failure(join("Cannot open file: ", filename));
|
||||
}
|
||||
|
||||
double get_mean(const std::vector<double>& iter) {
|
||||
const double init = 0;
|
||||
return std::accumulate(iter.begin(), iter.end(), init) / iter.size();
|
||||
}
|
||||
|
||||
double get_sigma(const std::vector<double>& iter) {
|
||||
const double mean = get_mean(iter);
|
||||
double sum = 0;
|
||||
for (auto v=iter.begin(); v!=iter.end(); ++v)
|
||||
sum += std::pow(*v - mean, 2.);
|
||||
return std::sqrt(sum/(iter.size()-1));
|
||||
}
|
||||
|
||||
} //utils
|
||||
|
||||
```
|
||||
|
||||
最后,我们来编写`pca.i`接口文件,也就是代码清单9的内容。
|
||||
|
||||
代码清单9,`pca.i`:
|
||||
|
||||
```
|
||||
%module pca
|
||||
|
||||
%include "std_string.i"
|
||||
%include "std_vector.i"
|
||||
|
||||
namespace std {
|
||||
%template(DoubleVector) vector<double>;
|
||||
}
|
||||
|
||||
%{
|
||||
#include "pca.h"
|
||||
#include "utils.h"
|
||||
%}
|
||||
|
||||
%include "pca.h"
|
||||
%include "utils.h"
|
||||
|
||||
```
|
||||
|
||||
这里需要注意的是,我们在C++代码中使用了熟悉的顺序容器`std::vector`,但由于模板类比较特殊,我们需要用`%template`声明一下。
|
||||
|
||||
一切就绪后,我们执行下面的命令行,生成`_pca.so`库供Python使用:
|
||||
|
||||
```
|
||||
$ swig -c++ -python pca.i # 解释接口定义生成包SWIG装器代码
|
||||
$ g++ -fPIC -c pca.h pca.cpp utils.h utils.cpp pca_wrap.cxx -I/usr/include/python3.7 # 编译源代码
|
||||
$ g++ -shared pca.o pca_wrap.o utils.o -o _pca.so -O2 -Wall -std=c++11 -pthread -shared -fPIC -larmadillo # 链接
|
||||
|
||||
```
|
||||
|
||||
接着,我们使用Python脚本,导入我们创建好的so动态库;然后,调用相应的类的函数。这部分内容,我写在了代码清单10中。
|
||||
|
||||
代码清单10,`testPCA.py`:
|
||||
|
||||
```
|
||||
import pca
|
||||
|
||||
pca_inst = pca.pca(2)
|
||||
pca_inst.add_record([1.0, 1.0])
|
||||
pca_inst.add_record([2.0, 2.0])
|
||||
pca_inst.add_record([4.0, 1.0])
|
||||
|
||||
pca_inst.solve()
|
||||
|
||||
energy = pca_inst.get_energy()
|
||||
eigenvalues = pca_inst.get_eigenvalues()
|
||||
|
||||
print(energy)
|
||||
print(eigenvalues)
|
||||
|
||||
```
|
||||
|
||||
最后,我们分别对纯Python实现的代码,和使用SWIG封装的版本来进行测试,各自都执行1,000,000次,然后对比执行时间。我用一张图表示了我的机器上得到的结果,你可以对比看看。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d4/e2/d4729298aa565d7216720f9d5ededde2.png" alt="">
|
||||
|
||||
虽然这样粗略的比较并不够严谨,比如我们没有认真考虑SWIG接口类型转换的耗时,也没有考虑在不同编程语言下实现算法的逻辑等等。但是,通过这个粗略的结果,你仍然可以看出执行类似运算时,两者性能的巨大差异。
|
||||
|
||||
## SWIG C++常用工具
|
||||
|
||||
到这里,你应该已经可以开始动手操作了,把上面的代码清单当作你的工具进行实践。不过,SWIG本身非常丰富,所以这里我也再给你总结介绍几个常用的工具。
|
||||
|
||||
### **1.全局变量**
|
||||
|
||||
在Python 中,我们可以通过cvar,来访问C++代码中定义的全局变量。
|
||||
|
||||
比如说,我们在头文件 `sample.h`中定义了一个全局变量,并在`sample.i`中对其进行引用,也就是代码清单 11和12的内容。
|
||||
|
||||
代码清单11,`sample.h`:
|
||||
|
||||
```
|
||||
#include <cstdint>
|
||||
int32_t score = 100;
|
||||
|
||||
```
|
||||
|
||||
代码清单12,`sample.i`:
|
||||
|
||||
```
|
||||
%module sample
|
||||
%{
|
||||
#include "sample.h"
|
||||
%}
|
||||
|
||||
%include "sample.h"
|
||||
|
||||
```
|
||||
|
||||
这样,我们就可以直接在Python脚本中,通过cvar来访问对应的全局变量,如代码清单13所示,输出结果为100。
|
||||
|
||||
代码清单13,`sample.py`:
|
||||
|
||||
```
|
||||
import sample
|
||||
print sample.cvar.score
|
||||
|
||||
```
|
||||
|
||||
### **2.常量**
|
||||
|
||||
我们可以在接口定义文件中,使用 `%constant`来设定常量,如代码清单14所示。
|
||||
|
||||
代码清单14,`sample.i`:
|
||||
|
||||
```
|
||||
%constant int foo = 100;
|
||||
%constant const char* bar = "foobar2000";
|
||||
|
||||
```
|
||||
|
||||
### **3.Enumeration**
|
||||
|
||||
我们可以在接口文件中,使用enum关键字来定义enum。
|
||||
|
||||
### **4.指针和引用**
|
||||
|
||||
在C++世界中,指针是永远也绕不开的一个概念。它无处不在,我们也无时无刻不需要使用它。因此,在这里,我认为很有必要介绍一下,如何对C++中的指针和引用进行操作。
|
||||
|
||||
SWIG对指针有着较为不错的支持,对智能指针也有一定的支持,而且在近期的更新日志中,我发现它对智能指针的支持一直在更新。下面的代码清单15和16,就展示了针对指针和引用的使用方法。
|
||||
|
||||
代码清单15,`sample.h`:
|
||||
|
||||
```
|
||||
#include <cstdint>
|
||||
|
||||
void passPointer(ClassA* ptr) {
|
||||
printf("result= %d", ptr->result);
|
||||
}
|
||||
|
||||
void passReference(const ClassA& ref) {
|
||||
printf("result= %d", ref.result);
|
||||
}
|
||||
|
||||
void passValue(ClassA obj) {
|
||||
printf("result= %d", obj.result);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
代码清单16,`sample.py`:
|
||||
|
||||
```
|
||||
import sample
|
||||
|
||||
a = ClassA() # 创建 ClassA实例
|
||||
passPointer(a)
|
||||
passReference(a)
|
||||
passValue(a)
|
||||
|
||||
```
|
||||
|
||||
### **5.字符串**
|
||||
|
||||
我们在工业级代码中,时常使用`std::string`。而在SWIG的环境下,使用标准库中的字符串,需要你在接口文件中声明`%include “std_stirng.i”`,来确保实现C++ `std::string`到Python `str`的自动转换。具体内容我放在了代码清单17中。
|
||||
|
||||
代码清单17,`sample.i`:
|
||||
|
||||
```
|
||||
%module sample
|
||||
|
||||
%include "std_string.i"
|
||||
|
||||
|
||||
```
|
||||
|
||||
### **6.向量**
|
||||
|
||||
`std::vector`是STL中最常见也是使用最频繁的顺序容器,模板类比较特殊,因此,它的使用也比字符串稍微复杂一些,需要使用`%template`进行声明。详细内容我放在了代码清单18中。
|
||||
|
||||
代码清单18,`sample.i`:
|
||||
|
||||
```
|
||||
%module sample
|
||||
|
||||
%include "std_string.i"
|
||||
%include "std_vector.i"
|
||||
|
||||
namespace std {
|
||||
%template(DoubleVector) vector<double>;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
### **7. 映射**
|
||||
|
||||
`std::map` 同样是STL中最常见也是使用最频繁的容器。同样的,它的模板类也比较特殊,需要使用`%template`进行声明,详细内容可见代码清单19。
|
||||
|
||||
代码清单19,`sample.i`:
|
||||
|
||||
```
|
||||
%module sample
|
||||
|
||||
%include "std_string.i"
|
||||
%include "std_map.i"
|
||||
|
||||
namespace std {
|
||||
%template(Int2strMap) map<int, string>;
|
||||
%template(Str2intMap) map<string, int>;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
## 学习路径
|
||||
|
||||
到此,SWIG入门这个小目标,我们就已经实现了。今天内容可以当作一份SWIG的编程实践指南,我给你提供了19个代码清单,利用它们,你就可以上手操作了。当然,如果在这方面你还想继续精进,该怎么办呢?别着急,今天这节课的最后,我再和你分享下,我觉得比较高效的一条SWIG学习路径。
|
||||
|
||||
首先,任何技术的学习不要脱离官方文档。SWIG网站上提供了难以置信的详尽文档,通过文档掌握SWIG的用法,显然是最好的一个途径。
|
||||
|
||||
其次,要深入SWIG,对C++有一个较为全面的掌握,就显得至关重要了。对于高性能计算来说,C++总是绕不开的一个主题,特别是对内存管理、指针和虚函数的应用,需要你实际上手编写C++代码后,才能逐渐掌握。退一步讲,即便你只是为了封装其他C++库供Python调用,也需要对C++有一个基本了解,以便未来遇到编译或链接错误时,可以找到方向来解决问题。
|
||||
|
||||
最后,我再罗列一些学习素材,供你进一步学习参考。
|
||||
|
||||
第一便是SWIG文档。
|
||||
|
||||
- a. [http://www.swig.org/doc.html](http://www.swig.org/doc.html)
|
||||
- b. [http://www.swig.org/Doc4.0/SWIGPlus.html](http://www.swig.org/Doc4.0/SWIGPlus.html)
|
||||
- c. PDF版本:[http://www.swig.org/Doc4.0/SWIGDocumentation.pdf](http://www.swig.org/Doc4.0/SWIGDocumentation.pdf)
|
||||
|
||||
第二是《C++ Primer》这本书。作为C++领域的经典书籍,这本书对你全面了解C++有极大帮助。
|
||||
|
||||
第三则是《高级C/C++编译技术》这本书。这本书的内容更为进阶,你可以把它作为学习C++的提高和了解。
|
||||
|
||||
好了,今天的内容就到此结束了。关于SWIG,你有哪些收获,或者还有哪些问题,都欢迎你留言和我分享讨论。也欢迎你把这篇文章分享给你的同事、朋友,我们一起学习和进步。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user