This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View 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 -&gt; Engineering Manager -&gt; Director -&gt; VP但是我们大家的思想中并没有严格上下级这样的概念。
公司鼓励每个人积极发表自己的观点。比如,一个应届毕业生,因为一个问题和自己的老板,乃至老板的老板据理力争,这样的场景也是很常见的。
另外公司每隔一段时间便会组织一次Q&amp;A我们大家可以向CEO、CTO等提问。比如你想了解某个产品的发展方向公司目前的侧重点甚至是一些敏感的问题都可以提问。
同时公司的领导哪怕是上到CEO、CTO、COO这样的高层都没有自己的单独办公室都是和我们一起坐在开放的区域内办公这样即拉近了距离也是为了方便交流和讨论。
## 开放式的讨论平台
第二点是开放式的讨论平台。我一直觉得这个方式非常好也很喜欢。FB用的是自己开发的workplace相当于一个开放的社区里面会有不同的群组无论你有什么问题都可以去相应的群组提问那里会有各个领域的高手来帮你解答。
举个例子如果你有Python相关的问题便可以去Python的群组问你如果有Spark的问题就去Spark 群组问。
很多时候各个组开发的产品都会涉及很多的跨组合作要用到其他组开发的一些API、算法、框架等等。这样在使用的时候就难免会遇到一些问题这个时候我们大家通常便会在对应的群组中提问。问题解决后也保存了下来之后再有人遇到相同的问题时便能直接搜索到对应的帖子及答案大大提高了办公的效率。
除了上述Q&amp;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工作的主要工作体验。其实在硅谷工作不仅仅有技术上的收获比如你可以直接接触到业内顶级大牛了解到最新最前沿的技术还有很多认知和思维方式上的影响比如对于流程、合作、开源等的思考。
接下来的几篇文章,我会继续讲述,关于技术研发我这些年的工作经验和总结,以及对于职业方向的认识和思考。欢迎你在留言区和我一起讨论交流这些问题,经验分享和交流,是每个技术人成长必不可少的环节。

View 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这也就告诉了开发人员他们所写的代码可能存在问题需要再次检查。
## 写在最后
关于技术研发的注意事项,我主要强调这些内容。事实上,日常开发工作中,很多的细节都值得特别关注,而对于易错的地方,用系统化的流程解决不失为一个高效的方案。那么,在你的日常工作中,有哪些特别留心的地方值得分享,或者有哪些疑惑的地方想要交流吗?欢迎在留言区写下你的想法。

View 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++为主。
从薪酬的角度来看,普遍来说:算法 &gt; 架构 &gt; 后端 &gt; 前端。当然,这主要是由市场的供需关系决定的。
就拿算法岗来说国内市场普遍缺少算法人才也是因为这个岗位的培养难度更大需要投入更大的精力。在顶尖互联网公司参与核心产品研发的算法工程师们工作三年年收入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呢
总之,在某一领域到了进阶的阶段,你需要关注的,绝不仅仅只是某些功能的实现,更需要你考虑所写代码的性能、质量,甚至于整个系统的设计等等。
虽然讲了这么多东西,但最后我想说的是,三百六十行,行行出状元。对于计算机行业,乃至整个职场来说,每一个领域都没有优劣之分,每个领域你都可以做得很牛逼,前提是你不懈地学习、实践和思考。
那么,对于职业选择和发展,你又是如何看待和理解的呢?欢迎留言和我一起交流探讨,也希望屏幕前的一直不懈学习的你,能找到属于自己的方向,不断前进和创新,实现自己的人生理想。

View 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编写的PCAPrincipal 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 &lt;time.h&gt;
double My_variable = 3.0;
int fact(int n) {
if (n &lt;= 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(&amp;ltime);
return ctime(&amp;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 &lt;vector&gt;
#include &lt;string&gt;
#include &lt;armadillo&gt;
class pca {
public:
pca();
explicit pca(long num_vars);
virtual ~pca();
bool operator==(const pca&amp; other);
void set_num_variables(long num_vars);
long get_num_variables() const;
void add_record(const std::vector&lt;double&gt;&amp; record);
std::vector&lt;double&gt; 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&amp; solver);
std::string get_solver() const;
void solve();
double check_eigenvectors_orthogonal() const;
double check_projection_accurate() const;
void save(const std::string&amp; basename) const;
void load(const std::string&amp; basename);
void set_num_retained(long num_retained);
long get_num_retained() const;
std::vector&lt;double&gt; to_principal_space(const std::vector&lt;double&gt;&amp; record) const;
std::vector&lt;double&gt; to_variable_space(const std::vector&lt;double&gt;&amp; data) const;
double get_energy() const;
double get_eigenvalue(long eigen_index) const;
std::vector&lt;double&gt; get_eigenvalues() const;
std::vector&lt;double&gt; get_eigenvector(long eigen_index) const;
std::vector&lt;double&gt; get_principal(long eigen_index) const;
std::vector&lt;double&gt; get_mean_values() const;
std::vector&lt;double&gt; 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&lt;double&gt; data_;
arma::Col&lt;double&gt; energy_;
arma::Col&lt;double&gt; eigval_;
arma::Mat&lt;double&gt; eigvec_;
arma::Mat&lt;double&gt; proj_eigvec_;
arma::Mat&lt;double&gt; princomp_;
arma::Col&lt;double&gt; mean_;
arma::Col&lt;double&gt; sigma_;
void initialize_();
void assert_num_vars_();
void resize_data_if_needed_();
};
```
接着,我们再来编写具体实现`pca.cpp`也就是代码清单6的内容。
代码清单6`pca.cpp`
```
#include &quot;pca.h&quot;
#include &quot;utils.h&quot;
#include &lt;stdexcept&gt;
#include &lt;random&gt;
pca::pca()
: num_vars_(0),
num_records_(0),
record_buffer_(1000),
solver_(&quot;dc&quot;),
do_normalize_(false),
num_retained_(1),
energy_(1)
{}
pca::pca(long num_vars)
: num_vars_(num_vars),
num_records_(0),
record_buffer_(1000),
solver_(&quot;dc&quot;),
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&amp; other) {
const double eps = 1e-5;
if (num_vars_ == other.num_vars_ &amp;&amp;
num_records_ == other.num_records_ &amp;&amp;
record_buffer_ == other.record_buffer_ &amp;&amp;
solver_ == other.solver_ &amp;&amp;
do_normalize_ == other.do_normalize_ &amp;&amp;
num_retained_ == other.num_retained_ &amp;&amp;
utils::is_approx_equal_container(eigval_, other.eigval_, eps) &amp;&amp;
utils::is_approx_equal_container(eigvec_, other.eigvec_, eps) &amp;&amp;
utils::is_approx_equal_container(princomp_, other.princomp_, eps) &amp;&amp;
utils::is_approx_equal_container(energy_, other.energy_, eps) &amp;&amp;
utils::is_approx_equal_container(mean_, other.mean_, eps) &amp;&amp;
utils::is_approx_equal_container(sigma_, other.sigma_, eps) &amp;&amp;
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_ &lt; 2)
throw std::invalid_argument(&quot;Number of variables smaller than two.&quot;);
}
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&lt;double&gt;&amp; record) {
assert_num_vars_();
if (num_vars_ != long(record.size()))
throw std::domain_error(utils::join(&quot;Record has the wrong size: &quot;, record.size()));
resize_data_if_needed_();
arma::Row&lt;double&gt; row(&amp;record.front(), record.size());
data_.row(num_records_) = std::move(row);
++num_records_;
}
std::vector&lt;double&gt; 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&amp; solver) {
if (solver!=&quot;standard&quot; &amp;&amp; solver!=&quot;dc&quot;)
throw std::invalid_argument(utils::join(&quot;No such solver available: &quot;, solver));
solver_ = solver;
}
void pca::solve() {
assert_num_vars_();
if (num_records_ &lt; 2)
throw std::logic_error(&quot;Number of records smaller than two.&quot;);
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&lt;double&gt; eigval(num_vars_);
arma::Mat&lt;double&gt; eigvec(num_vars_, num_vars_);
arma::Mat&lt;double&gt; 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&lt;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&lt;=0 || num_retained&gt;num_vars_)
throw std::range_error(utils::join(&quot;Value out of range: &quot;, num_retained));
num_retained_ = num_retained;
proj_eigvec_ = eigvec_.submat(0, 0, eigvec_.n_rows-1, num_retained_-1);
}
std::vector&lt;double&gt; pca::to_principal_space(const std::vector&lt;double&gt;&amp; data) const {
arma::Col&lt;double&gt; column(&amp;data.front(), data.size());
column -= mean_;
if (do_normalize_) column /= sigma_;
const arma::Row&lt;double&gt; row(column.t() * proj_eigvec_);
return std::move(utils::extract_row_vector(row, 0));
}
std::vector&lt;double&gt; pca::to_variable_space(const std::vector&lt;double&gt;&amp; data) const {
const arma::Row&lt;double&gt; row(&amp;data.front(), data.size());
arma::Col&lt;double&gt; 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 &gt;= num_vars_)
throw std::range_error(utils::join(&quot;Index out of range: &quot;, eigen_index));
return eigval_(eigen_index);
}
std::vector&lt;double&gt; pca::get_eigenvalues() const {
return std::move(utils::extract_column_vector(eigval_, 0));
}
std::vector&lt;double&gt; pca::get_eigenvector(long eigen_index) const {
return std::move(utils::extract_column_vector(eigvec_, eigen_index));
}
std::vector&lt;double&gt; 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(&quot;No proper data matrix present that the projection could be compared with.&quot;);
const arma::Mat&lt;double&gt; 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&lt;double&gt; pca::get_mean_values() const {
return std::move(utils::extract_column_vector(mean_, 0));
}
std::vector&lt;double&gt; 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&amp; basename) const {
const std::string filename = basename + &quot;.pca&quot;;
std::ofstream file(filename.c_str());
utils::assert_file_good(file.good(), filename);
utils::write_property(file, &quot;num_variables&quot;, num_vars_);
utils::write_property(file, &quot;num_records&quot;, num_records_);
utils::write_property(file, &quot;solver&quot;, solver_);
utils::write_property(file, &quot;num_retained&quot;, num_retained_);
utils::write_property(file, &quot;do_normalize&quot;, do_normalize_);
file.close();
utils::write_matrix_object(basename + &quot;.eigval&quot;, eigval_);
utils::write_matrix_object(basename + &quot;.eigvec&quot;, eigvec_);
utils::write_matrix_object(basename + &quot;.princomp&quot;, princomp_);
utils::write_matrix_object(basename + &quot;.energy&quot;, energy_);
utils::write_matrix_object(basename + &quot;.mean&quot;, mean_);
utils::write_matrix_object(basename + &quot;.sigma&quot;, sigma_);
}
void pca::load(const std::string&amp; basename) {
const std::string filename = basename + &quot;.pca&quot;;
std::ifstream file(filename.c_str());
utils::assert_file_good(file.good(), filename);
utils::read_property(file, &quot;num_variables&quot;, num_vars_);
utils::read_property(file, &quot;num_records&quot;, num_records_);
utils::read_property(file, &quot;solver&quot;, solver_);
utils::read_property(file, &quot;num_retained&quot;, num_retained_);
utils::read_property(file, &quot;do_normalize&quot;, do_normalize_);
file.close();
utils::read_matrix_object(basename + &quot;.eigval&quot;, eigval_);
utils::read_matrix_object(basename + &quot;.eigvec&quot;, eigvec_);
utils::read_matrix_object(basename + &quot;.princomp&quot;, princomp_);
utils::read_matrix_object(basename + &quot;.energy&quot;, energy_);
utils::read_matrix_object(basename + &quot;.mean&quot;, mean_);
utils::read_matrix_object(basename + &quot;.sigma&quot;, sigma_);
set_num_retained(num_retained_);
}
```
这里要注意了代码清单6中用到了`utils.h`这个文件它是对部分矩阵和数学计算的封装内容我放在了代码清单7中。
代码清单7`utils.h`
```
#pragma once
#include &lt;armadillo&gt;
#include &lt;sstream&gt;
namespace utils {
arma::Mat&lt;double&gt; make_covariance_matrix(const arma::Mat&lt;double&gt;&amp; data);
arma::Mat&lt;double&gt; make_shuffled_matrix(const arma::Mat&lt;double&gt;&amp; data);
arma::Col&lt;double&gt; compute_column_means(const arma::Mat&lt;double&gt;&amp; data);
void remove_column_means(arma::Mat&lt;double&gt;&amp; data, const arma::Col&lt;double&gt;&amp; means);
arma::Col&lt;double&gt; compute_column_rms(const arma::Mat&lt;double&gt;&amp; data);
void normalize_by_column(arma::Mat&lt;double&gt;&amp; data, const arma::Col&lt;double&gt;&amp; rms);
void enforce_positive_sign_by_column(arma::Mat&lt;double&gt;&amp; data);
std::vector&lt;double&gt; extract_column_vector(const arma::Mat&lt;double&gt;&amp; data, long index);
std::vector&lt;double&gt; extract_row_vector(const arma::Mat&lt;double&gt;&amp; data, long index);
void assert_file_good(const bool&amp; is_file_good, const std::string&amp; filename);
template&lt;typename T&gt;
void write_matrix_object(const std::string&amp; filename, const T&amp; matrix) {
assert_file_good(matrix.quiet_save(filename, arma::arma_ascii), filename);
}
template&lt;typename T&gt;
void read_matrix_object(const std::string&amp; filename, T&amp; matrix) {
assert_file_good(matrix.quiet_load(filename), filename);
}
template&lt;typename T, typename U, typename V&gt;
bool is_approx_equal(const T&amp; value1, const U&amp; value2, const V&amp; eps) {
return std::abs(value1-value2)&lt;eps ? true : false;
}
template&lt;typename T, typename U, typename V&gt;
bool is_approx_equal_container(const T&amp; container1, const U&amp; container2, const V&amp; eps) {
if (container1.size()==container2.size()) {
bool equal = true;
for (size_t i=0; i&lt;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&lt;double&gt;&amp; iter);
double get_sigma(const std::vector&lt;double&gt;&amp; iter);
struct join_helper {
static void add_to_stream(std::ostream&amp; stream) {}
template&lt;typename T, typename... Args&gt;
static void add_to_stream(std::ostream&amp; stream, const T&amp; arg, const Args&amp;... args) {
stream &lt;&lt; arg;
add_to_stream(stream, args...);
}
};
template&lt;typename T, typename... Args&gt;
std::string join(const T&amp; arg, const Args&amp;... args) {
std::ostringstream stream;
stream &lt;&lt; arg;
join_helper::add_to_stream(stream, args...);
return stream.str();
}
template&lt;typename T&gt;
void write_property(std::ostream&amp; file, const std::string&amp; key, const T&amp; value) {
file &lt;&lt; key &lt;&lt; &quot;\t&quot; &lt;&lt; value &lt;&lt; std::endl;
}
template&lt;typename T&gt;
void read_property(std::istream&amp; file, const std::string&amp; key, T&amp; value) {
std::string tmp;
bool found = false;
while (file.good()) {
file &gt;&gt; tmp;
if (tmp==key) {
file &gt;&gt; value;
found = true;
break;
}
}
if (!found)
throw std::domain_error(join(&quot;No such key available: &quot;, key));
file.seekg(0);
}
} //utils
```
至于具体的实现代码我放在了在代码清单8`utils.cpp`中。
代码清单8`utils.cpp`
```
#include &quot;utils.h&quot;
#include &lt;stdexcept&gt;
#include &lt;sstream&gt;
#include &lt;numeric&gt;
namespace utils {
arma::Mat&lt;double&gt; make_covariance_matrix(const arma::Mat&lt;double&gt;&amp; data) {
return std::move( (data.t()*data) * (1./(data.n_rows-1)) );
}
arma::Mat&lt;double&gt; make_shuffled_matrix(const arma::Mat&lt;double&gt;&amp; data) {
const long n_rows = data.n_rows;
const long n_cols = data.n_cols;
arma::Mat&lt;double&gt; shuffle(n_rows, n_cols);
for (long j=0; j&lt;n_cols; ++j) {
for (long i=0; i&lt;n_rows; ++i) {
shuffle(i, j) = data(std::rand()%n_rows, j);
}
}
return std::move(shuffle);
}
arma::Col&lt;double&gt; compute_column_means(const arma::Mat&lt;double&gt;&amp; data) {
const long n_cols = data.n_cols;
arma::Col&lt;double&gt; means(n_cols);
for (long i=0; i&lt;n_cols; ++i)
means(i) = arma::mean(data.col(i));
return std::move(means);
}
void remove_column_means(arma::Mat&lt;double&gt;&amp; data, const arma::Col&lt;double&gt;&amp; means) {
if (data.n_cols != means.n_elem)
throw std::range_error(&quot;Number of elements of means is not equal to the number of columns of data&quot;);
for (long i=0; i&lt;long(data.n_cols); ++i)
data.col(i) -= means(i);
}
arma::Col&lt;double&gt; compute_column_rms(const arma::Mat&lt;double&gt;&amp; data) {
const long n_cols = data.n_cols;
arma::Col&lt;double&gt; rms(n_cols);
for (long i=0; i&lt;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&lt;double&gt;&amp; data, const arma::Col&lt;double&gt;&amp; rms) {
if (data.n_cols != rms.n_elem)
throw std::range_error(&quot;Number of elements of rms is not equal to the number of columns of data&quot;);
for (long i=0; i&lt;long(data.n_cols); ++i) {
if (rms(i)==0)
throw std::runtime_error(&quot;At least one of the entries of rms equals to zero&quot;);
data.col(i) *= 1./rms(i);
}
}
void enforce_positive_sign_by_column(arma::Mat&lt;double&gt;&amp; data) {
for (long i=0; i&lt;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)&gt;=std::abs(min)) {
if (max&lt;0) change_sign = true;
} else {
if (min&lt;0) change_sign = true;
}
if (change_sign) data.col(i) *= -1;
}
}
std::vector&lt;double&gt; extract_column_vector(const arma::Mat&lt;double&gt;&amp; data, long index) {
if (index&lt;0 || index &gt;= long(data.n_cols))
throw std::range_error(join(&quot;Index out of range: &quot;, index));
const long n_rows = data.n_rows;
const double* memptr = data.colptr(index);
std::vector&lt;double&gt; result(memptr, memptr + n_rows);
return std::move(result);
}
std::vector&lt;double&gt; extract_row_vector(const arma::Mat&lt;double&gt;&amp; data, long index) {
if (index&lt;0 || index &gt;= long(data.n_rows))
throw std::range_error(join(&quot;Index out of range: &quot;, index));
const arma::Row&lt;double&gt; row(data.row(index));
const double* memptr = row.memptr();
std::vector&lt;double&gt; result(memptr, memptr + row.n_elem);
return std::move(result);
}
void assert_file_good(const bool&amp; is_file_good, const std::string&amp; filename) {
if (!is_file_good)
throw std::ios_base::failure(join(&quot;Cannot open file: &quot;, filename));
}
double get_mean(const std::vector&lt;double&gt;&amp; iter) {
const double init = 0;
return std::accumulate(iter.begin(), iter.end(), init) / iter.size();
}
double get_sigma(const std::vector&lt;double&gt;&amp; 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 &quot;std_string.i&quot;
%include &quot;std_vector.i&quot;
namespace std {
%template(DoubleVector) vector&lt;double&gt;;
}
%{
#include &quot;pca.h&quot;
#include &quot;utils.h&quot;
%}
%include &quot;pca.h&quot;
%include &quot;utils.h&quot;
```
这里需要注意的是我们在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 &lt;cstdint&gt;
int32_t score = 100;
```
代码清单12`sample.i`
```
%module sample
%{
#include &quot;sample.h&quot;
%}
%include &quot;sample.h&quot;
```
这样我们就可以直接在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 = &quot;foobar2000&quot;;
```
### **3.Enumeration**
我们可以在接口文件中使用enum关键字来定义enum。
### **4.指针和引用**
在C++世界中指针是永远也绕不开的一个概念。它无处不在我们也无时无刻不需要使用它。因此在这里我认为很有必要介绍一下如何对C++中的指针和引用进行操作。
SWIG对指针有着较为不错的支持对智能指针也有一定的支持而且在近期的更新日志中我发现它对智能指针的支持一直在更新。下面的代码清单15和16就展示了针对指针和引用的使用方法。
代码清单15`sample.h`
```
#include &lt;cstdint&gt;
void passPointer(ClassA* ptr) {
printf(&quot;result= %d&quot;, ptr-&gt;result);
}
void passReference(const ClassA&amp; ref) {
printf(&quot;result= %d&quot;, ref.result);
}
void passValue(ClassA obj) {
printf(&quot;result= %d&quot;, 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 &quot;std_string.i&quot;
```
### **6.向量**
`std::vector`是STL中最常见也是使用最频繁的顺序容器模板类比较特殊因此它的使用也比字符串稍微复杂一些需要使用`%template`进行声明。详细内容我放在了代码清单18中。
代码清单18`sample.i`
```
%module sample
%include &quot;std_string.i&quot;
%include &quot;std_vector.i&quot;
namespace std {
%template(DoubleVector) vector&lt;double&gt;;
}
```
### **7. 映射**
`std::map` 同样是STL中最常见也是使用最频繁的容器。同样的它的模板类也比较特殊需要使用`%template`进行声明详细内容可见代码清单19。
代码清单19`sample.i`
```
%module sample
%include &quot;std_string.i&quot;
%include &quot;std_map.i&quot;
namespace std {
%template(Int2strMap) map&lt;int, string&gt;;
%template(Str2intMap) map&lt;string, int&gt;;
}
```
## 学习路径
到此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你有哪些收获或者还有哪些问题都欢迎你留言和我分享讨论。也欢迎你把这篇文章分享给你的同事、朋友我们一起学习和进步。