0%

永不褪色的少年心——《女神异闻录5》(Persona 5)玩后感

“不过是回合制RPG(角色扮演游戏),有什么可玩的呢?”

在好友向我极力推荐《女神异闻录5》(英文名Persona 5,后文简称P5)时,从一岁开始玩日式回合制RPG,天天看女主角领便当剧情的我,曾露出过不屑的神情。

没想到,之后的一个多月里,在工作的空闲之余,我花100多个小时玩通了这款游戏。

我一直不肯写游戏评测,因为我想建立一个系统的游戏评价框架,之后用统一的方式来评价所有游戏。开博客到现在,我一篇谈游戏的文章都没写。但是,今天玩通了P5后,无数的情愫从我的心中涌出,我实在按耐不住想发表看法的冲动了。我打算从个人的体验出发,谈一下这款游戏,而不去理性地分析游戏的优劣在哪。

这篇文章会有一定的剧透,但应该不影响没有玩过游戏的人。

老树新花——最古老的游戏与最新潮的表现形式

游戏一开始突然放了一段画风亲切的2D日式动画。穿着帅气风衣的主角,穿梭于热闹的赌场中。我这才反应过来,这是个日本公司制作(日式动画),刚出了没几年的游戏(背景是现代)。我脑中瞬间萌生了两个问题:2D动画这么费钱,整部游戏的剧情都是用2D动画表现的?主角生活在现代,怎么进行战斗呢?

很快,这两个问题就得到了解答。动画结束,进入操作模式后,游戏就变成3D的了,大部分剧情也都是用3D表示的。很快,只见一个警卫突然变成了怪物,第一场战斗开始了。我立马反应过来:这应该是幻想世界。在现代背景中加入幻想世界的设定,就可以触发战斗了。

而看到了游戏的战斗方式后,我又露出了怀念的微笑:主角和敌人轮流行动,每人一回合只能行动一次;技能有不同的属性,击中弱点属性可以把敌人击倒;物理技能消耗HP,魔法技能消耗SP……。打完怪物后,主角要进入迷宫探索。

自诞生于示波器后,电子游戏的画质就在不断提升。在3D技术尚未发展成熟时,2D游戏都在想方设法加入一些3D动画,来提高游戏的整体观赏性。没想到到了3D技术日益成熟的今天,加入2D的手绘动画反而成为了一种时尚。在这个拥有智能手机的现代世界观里,拉风的服装、嚣张的音乐、不羁的UI……每一个游戏元素都在极力地证明这款游戏是十分”现代化的“。

但是,仔细一看,这个游戏的核心玩法和最早期的游戏一模一样。一人行动一次的公平回合制战斗,在迷宫里刷小怪升级,购买攻击力更高的装备后打boss看剧情。

这款游戏实在太矛盾了,披着最新潮的外套,却有着最古老的内核。不仅仅是新与旧的结合,这款游戏同样是”游戏“(交互艺术)与”剧情、音乐、美术、演出“(传统艺术)的结合。再准确点来说,这款游戏中,属于游戏的那些部分,全是旧的;不属于游戏的那些部分,却有很多新鲜之处。

这些看上去矛盾的结合,却产生了奇妙的反应:我在玩游戏的时候,不会像最早的那些玩家一样沉醉于回合制战斗系统,享受攒装备打boss的乐趣。更多的时候,我跟随背景音乐,调动自己的情绪,享受击中敌人弱点时音效与动画共同渲染出的激动瞬间,像旅行一样在3D迷宫里的遨游。

这些体验,一般只有在看电影才能获得。一般情况下,如果一款游戏不能带来独特于其他艺术形式(如小说、音乐、电影)的体验,无论体验多好,我都会认为这是一款失败、”退化“的游戏(如《隐形守护者》)。但是,P5带来的这些体验是任何其他艺术形式无法提供的。和另一款游戏性更少的作品《十三机兵防卫圈》一样,哪怕剧情、音乐、演出再多,交互的部分再少,只要一款游戏带来了极佳的体验,并且这些体验仅能用游戏来表达,非得玩家亲自动手去玩,用五感去体会游戏的每一分钟,那么这款游戏就是一个好游戏。

与传统艺术结合,却更好地表达了传统艺术带来的体验,这恐怕是玩完P5后多数玩家的共识。而更令我赞叹的是,P5给我带来了另一种从未有过的游戏体验——“沉浸感”。现在很多公司把VR包装成“元宇宙”(metaverse)在胡乱炒作,殊不知日本ACG里“异世界”的英文就是metaverse。真正具有沉浸感的世界,不需要先进的技术,不需要为了敛财到处炒作的“科技公司”,而需要用户有一定的艺术欣赏能力,需要设计者有先进的设计理念。从激进的角度讲,优秀的小说就可以看成一个全新的世界。但读小说时,读者永远只是旁观者。而游戏是唯一一种能够让用户参与进内容创造的艺术形式。好的游戏就能创造一个“异世界”,供玩家游玩、享受。在我看来P5某种程度实现了这一点。

我就是男主角

除了传统RPG的战斗,P5还有一半的游戏系统属于文字冒险游戏。主角要提升自己的个人能力(知识、勇气、灵巧、魅力、体贴),加深与其他人物的关系。在文字冒险游戏中,为了提升玩家的沉浸感,一般会让主角一言不发,由玩家决定主角的所有发言。P5保留了传统文字冒险游戏的这些特性。

文字冒险游戏的一个特例是galgame(美少女游戏),这类主打和美少女谈恋爱的作品虽然脱胎于游戏,却越来越向视觉小说靠拢,只勉强保留了选择关键对话这一交互选项。P5整体来说是galgame的超集,因为玩P5时经常需要选择发言,且主角也可以和女性角色提高好感度。

玩galgame时,我怎么也无法代入男主角。如果男主角在剧情中戏份比较多,我就会觉得男主角的行动和我的想法存在差异,很自然的以旁观者的身份来对待游戏;如果男主角配音也没有、立绘也没有、性格也没有,剧情还是莫名其妙地发展起来了,我就会开始疑惑:为什么要和这样的女主角发展剧情,为什么剧情还能进行下去?最后也会失去代入感。事实上,让玩家代入主角是非常难办到的,很多galgame已经改变了策略,忽略了代入感,默认玩家就是看客,完全把男主角当成剧情中的人物来看待。

P5选择了较传统的方式,男主角大部分时间都不会发言,剧情甚至基本都是配角推动的。让玩家选择的发言几乎改变不了剧情,比如在开始战斗之前让主角喊一句“出发吧”,队友失去斗志的时候说一句“加油”。除了在文字冒险部分和其他角色培养感情时,恰当的发言能提供更多好感度,其他地方的对话选择系统就是个摆设。一开始,我觉得P5的文字冒险系统设计得很差。

玩着玩着,我觉得有点不对劲了。剧情进行了一段时间后,沉着冷静、勇敢坚强、深谋远虑的女主角新岛真加入了队伍,我一下就被她的声音和外貌吸引住了(题外话:新岛真的声优是佐藤利奈,玩P5后我对炮姐的好感度也有所提升)。在和新岛真的关系发展到一定程度后,需要把魅力升满才能解锁后续剧情。我瞬间改变了之前的所有计划,开始全力提升魅力。周四,澡堂里是药浴,我冒着把身体烫坏的风险泡澡,就为了多加1点魅力;去家庭餐厅看书,我只点美化皮肤的饮料;睡觉之前,我认真阅读时尚杂志。总算,经过一段时间的努力,我把魅力升满了,顺理成章地过完了后面的剧情,在关键处选择了正确的发言,和新岛真达成情侣关系。没想到,过了几天后,我和另一个女性角色的关系也发展到了恋爱分歧点,我想也不想地避开了恋爱的选项。如果开启恋爱路线,就能把和角色的好感度升满,解锁对游戏有利的强力辅助技能。一般情况下,作为一个贪心的玩家,我是一定要把所有能获得的能力都获得的,管他什么脚踏几条船呢。但是,在选择这次的恋爱分歧点时,我竟然为和其他人发展恋爱关系产生了负罪感,毅然决然地放弃了能够获得特殊能力的选项。我总算意识到,我已经开始把自己代入到游戏里了。开始恋爱关系后,和这个角色再发生剧情就没有任何收益了,游戏也不会主动提示你去找这个角色。但是,我已经忘掉了这是在玩游戏。刚开始恋爱,怎么能就这样停下来呢?我奋然对抗着游戏系统,在图书馆门口、校门口、地铁口寻找着新岛真,和她一起去各个景点游玩,即使我明白再提升好感度也触发不了任何有效的新剧情。好在我还有作为玩家的最后一点理性,提前存了个档,在同一天里尝试了去不同景点谈恋爱的剧情,最后哀伤地和这不断重复却终将逝去的一天告别。

游戏发生在现实生活和幻想世界之间。在紧张的氛围中走出迷宫、打完boss后,玩家就可以在现实生活中开启文字冒险部分,和上完班后回家休息非常相似。走在涩谷的大街上,望着来往的行人,走进便利店购物,我享受着从战斗中解放的日常生活中。刚刚在游戏里战斗完的男主角,和忘记工作、沉浸游戏的现实中的我达成了巧妙的一致。这种现实与虚拟的一致让我真正融入到了日本东京的生活中。甚至有的时候会反过来,我把生活当成了游戏。夜晚,我一边散步,一边哼着城市里的BGM “Beneath the Mask”,仿佛自己是这个世界中唯一有智能的男主角;做了一场精彩的演讲,得到周围人的赞赏时,我感觉自己的魅力值得到了提升;拒绝男生的天文馆旅游邀请,接受另一个男生的公园乘船邀请后,我忍不住笑了出来——我在游戏里竟然做过一模一样的事情。

游戏本身的文字冒险系统无疑是十分简陋的。但正是在这样一个男主说话完全靠玩家的游戏中,配上RPG战斗、现代城市、女主角等要素,加上我个人的生活经历,我竟然在游戏中感到了前所未有的沉浸感。这太神奇了,我玩其他各种类型的游戏从来没有过这样的体验。

我开始习惯把自己代入游戏里的男主角了。在战斗中使用技能时,我会和主角一起喊出“pe-ru-so-na”。我甚至把自己代入进了主线剧情中。当男主角与其他主角久别重逢时,大家都热烈欢迎着男主。虽然每个人只有一两句话,我也搞不清楚他们为什么和一个“哑巴”的感情那么好,但我还是感到了男主角与同伴之间牢固的羁绊。剧情越进行到后面,我的这些感受就越深刻。仔细分析过后,我发现,不仅是游戏中的种种游戏元素令我感到了沉浸,游戏的剧情也很符我的胃口,让我感到“如果是我,也一定会这样做”的感觉。

给永远的少年的赞歌

本节剧透较多

前面也说到,由于游戏机制的原因,游戏无法对男主角进行刻画。而其他角色加入队伍后,很多时候都是例行发言。每个角色的个人剧情也只是稍微扩充了故事背景,没能做到对角色的深刻、多面的刻画。相比很多剧情优秀的作品,P5并没有把角色刻画好,几个标签就可以轻松描述一个角色。没办法,这毕竟是一个RPG,而且游戏想面向的玩家也不是有年纪有阅历的人,和其他作品比角色塑造还是太难了一些。

游戏的剧情上有亮点,但和人设一样,很多地方缺乏深度,导致玩家在剧情高潮时很难在情绪上感到同样的激动。游戏的结尾平稳而平常,没有特别大的高潮。我看的作品不多,不知道这样的结局在所有作品中排名如何,只能说结局不至于太差。

虽然P5在剧情上很一般,但我并不认为编剧没有水平。我能感到,剧情中留有很多隐喻。我最近写文章时慢慢领悟了“隐喻”这一技能。有的时候,我想表达很多事情,但是表达出来篇幅就很长,而且没有什么感染力。如果把这些情感、思绪藏于文字之后,让读者自己去体会,甚至根据经历的不同产生不同的体会,文章的表达能力就会更强。不管编剧本身是怎么想的,我从游戏剧情读出了许多价值观,其中很多想法我都是非常赞同的。通过我自己留隐喻时的经验,我推理出编剧是一个有一定深度的人,他把自己的想法藏在种种细节之后。那为什么一个有能力的编剧写出这样相对较为平庸的剧情呢?我想,肯定不是他不会写,而是他认为这部作品是给年轻人玩的,写一些给年轻人的剧情就行了。

为什么我觉得这个游戏是给年轻人玩的呢?很简单,看游戏的剧情梗概就知道了。P5讲了什么故事?一群穿着帅气服装的少年,以“怪盗”自诩,打败邪恶的大人,最后证明了真正的正义。故事看上去就是这么简单幼稚,只是游戏的剧情非常充实,让玩家觉得整个剧情确实是合理的而已。年纪小一点的玩家,一定会对剧情里的人物憧憬不已,幻想成为和他们一样的人;但年龄稍微大一点,经历了一些挫折后就知道,这只是一个幻想的故事而已,现实没有那么简单而美好。

但是,编剧显然不是一个年轻人。游戏里提到了许多不公的事情,这些事情很可能在现实中有原型,来自于编剧自己的听闻。如果费心思去刻画的话,一定会提高整个游戏的深度的。面对不公的事情,游戏里的解决方式是怎样的呢?很简单,用正义让坏人“醒悟”就行了。这是多么潦草的解决方法啊。这又是多么浪漫的解决方法啊!编剧知道现实中的事情很复杂,但是出于向游戏机制的靠齐,也出于对年轻玩家的考虑,写了这么一个浪漫的故事。

根据上述这些线索,我推理编剧明知世界很复杂,却要把故事往简单的方向去写,往年轻人能接受的方向写——只要坚持正义,就终能得到正义。如果这样看的话,P5就是给这种“少年思维”的赞歌,为少年们追求正义的梦想披上一层屏障,以使这些梦想晚一点被无情的现实戳破。

作为有阅历的成年人,还能写出这样的剧情,不知道这位编剧是确实还有一颗少年的心,还是说写出这种主旨简单的剧情没有什么难度。但不管如何,我也被剧情中的只言片语触动到了。我想,不仅是我,应该还会有很多其他成年人,会被P5的剧情所触动吧。人对事情的认知是螺旋上升的:最初可能是初生牛犊不怕虎,一旦在黑暗中触碰到了岩浆后,才能知道周围的环境不是天堂,而是地狱,因而感到恐惧。而只有接触过永恒的黑暗,经历过无尽的苦痛后,依然对美好有追求的人,才能称为勇敢的人。他们是永远的少年,永远忘不了少年时对美好的世界的向往。

我觉得我之所以这么喜欢这款游戏,之所以沉浸于游戏中,不仅是因为游戏的整体机制或者种种细节设定,更是因为我欣赏、羡慕主角们的经历。在我的心中,或许永远活着一个“怪盗”,在大功告成时,可以毫不羞耻地对全世界的人发出宣告:“这个世界由我收下了。”

游戏整体评价

发表了这么多看法,最后还是来整理一下我对P5这款游戏的评价。

从客观上看,P5的游戏机制老旧。不是说老的玩法就不好,而是这些玩法本身就不够好。RPG部分,玩法几乎和最早的RPG一样,稍微加入了一点点精灵宝可梦的属性克制和怪物捕捉系统,看得出来这些系统很多年都没有更新了。纯粹的回合制游戏就是不好玩的。回合制游戏的出现,本来就是早期机器性能、玩家欣赏水平、设计师设计水平共同导致的。现在还做出这种机制是很丢脸的。让我去做,随便加一点机制都会让游戏深度提高很多。迷宫中有不少谜题,但解密性不强,看得出来是为了照顾玩家,不然卡关会让玩家的体验很差,这一点给一个中评。和《黑暗之魂》一样,搞一个“巧妙”的电梯或密道,其实是一个看起来花里胡哨其实没什么用的设定。躲藏在掩体后面是迷宫中唯一一个有趣的通用设定,但做得不够好。RPG的整体养成思路其实不是JRPG(日式RPG),是DRPG(地下城RPG)。我一玩游戏就感到了和玩《世界树迷宫》(同公司的作品)一样的感觉:药品不够,玩家要评估自己的补给量,探索完迷宫后要回城补给。但这个游戏中剧情占比较多,不可能像一个真正的DRPG一样让玩家花时间在“探索迷宫-回城”这一循环上。结果就是这个游戏的RPG养成系统做得非常差劲。文字冒险部分没什么可以评价的,因为文字冒险本身的重心就不在游戏体验上,一般是在剧情上。文字冒险部分就是一个再普通不过的上个世纪就有的系统。

音乐、美术我没有资格评价。可能一些挑剔的玩家觉得3D建模不够好,不过这方面不是很敏感的我没什么看法(除了新岛真的脸部模型没有立绘好看)。音乐方面我只能用“好”这一个字来形容。在恰当的场合,有恰当的调节气氛的音乐音效,给玩家近乎电影级的体验,这是线性剧情RPG游戏的天生优势。P5在这一点上做得非常好,一般的游戏绝对没有这种演出效果。同时,这也是我第一次见游戏的日常音乐中出现歌曲,每一首歌的效果都很好,它们能够表达纯音乐表达不了的情感。

剧情评价了很多了。但我不是专业人士,修养不够,给不了客观评价。从以上分析也可以看出,我目前仅能,我也仅允许自己在游戏玩法上进行深入的评价,这方面我有充足的自信。

从主观上看,我在玩这款游戏时十分投入。经分析后,我发现我主要是在游戏中感到了代入感。对于我来说,这是一次全新的游戏体验。我喜欢,且感激这款游戏。除了代入感,游戏剧情中蕴含的主旨也让我备受感动。不管作者创作的本意如何,我对游戏剧情有了自己的一份解读与体悟。

我的收获

玩完这款游戏后,我有许多收获,主要集中在游戏设计上。

  • 如前文所讲,我认识到游戏与非游戏的边界不是游戏玩法与传统艺术形式占比多少,而是要看一款游戏的内容能否仅由游戏表达出来。如果写篇小说、拍部电影能有更好的效果,那这个作品就不配称作游戏了。
  • 续上点。游戏中的传统艺术形式(美术、音乐)非常重要,但所有这些形式都应该围绕游戏而做。P5就是一个例子,音乐做得很好,完美反映了各种情况下玩家所处的氛围。而游戏的剧情很多时候必须为游戏机制服务,不然只看剧情这一点的话游戏不一定比得过文学和电影。
  • 在游戏里加入2D日式动画是一个很好的主意。只要有足够的钱)。
  • 在游戏里加入歌曲是一个很好的主意。
  • UI不一定要规规矩矩,可以像P5一样有自己的风格。这对美术有一点的要求。
  • 让男主角当哑巴,加入一些不影响的对话选择,真的能提升玩家的代入感。
  • 把背景放到现代也很有趣,可以让玩家感到更加亲切。

差不多就这些了。我觉得P5应该是游戏史上有里程牌意义的游戏,它让日式RPG从棺材里伸出一只手,告诉世界它还没有死。它同样是我玩游戏经历中具有里程牌意义的游戏——我没想到一个不好玩的游戏也能这么吸引人。

我从小到大的一大梦想就是做国产日式RPG。现在这个梦想可以修订一下了。给我20年时间,等我有了资源后,我要做一款日式RPG。故事发生2010年代(我是中学生的年代)。游戏在具有优秀玩法、有足够的深度供玩家思考探索的同时,有着过得去的剧情、恰到好处的音乐、生动而不是华而不实的建模、契合主题的美术。简而言之,游戏玩法吊打P5,除了游戏玩法之外的地方力争做到比P5出色。

作为制作人,我要保障游戏每一环节的质量。程序不用说,我能让游戏代码质量达到可以开源的程度。我要提升音乐素养,不说自己负责部分音乐,起码要让我的音乐素养达到游戏素养的1/2(比如我现在的游戏素养属于世界前0.00001%,那么我音乐素养要达到世界前0.00002%)。美术相比之下比较好弄,因为这个东西是眼睛能看得见的东西。最后是游戏玩法,这自然不用说,不可能做得不够好。做得不让我自己满意,就不会拿出去卖。

游戏大获成功后,我要把游戏动画化。我要用实际行动告诉那些制作者:1)2D动画画风的游戏不能改编成真人电视剧;2)不能不加修改地把游戏改成动画,也不能改编得面目全非。

最后,感谢P5。在自己的博客写博文的一大好处就是能在无拘无束的情况下乱写,想到什么写什么。今天这篇文章就是这样,该认真组织语言的地方就认真写,该抒情抒情,想乱写的地方就乱写。今天我不仅玩完了游戏,还把很多感想一次写下来,可谓是双倍的快乐。这篇文章必然是要修改的,肯定有很多错别字。但我写了5小时,实在太累了,等以后再改吧。


更新

2022.8.9:修改错别字若干,修改不通的语句若干。没想到,过了大半年,我的文字编辑水平有了飞一般的提升。

苟活于世间 —— 初入社会有感 ~ Life Outside Campus

最近我获得了一份极佳的临时工作机会。薪资媲美互联网大厂,工作时间965,工作内容是我不算讨厌的开源项目开发。我独居于市区中的小房间里,骑车一刻钟就能到公司。公司内氛围良好,大家全是搞技术的出身,穿T恤凉鞋、和上司交谈时插科打诨都是正常现象。同事个个都很年轻,面孔就像学校里见到的同辈同学一样。

但是,在同事们聊天时,我只能当一个听众——他们的生活经历超出了我之前的认知。

有人谈自己被高薪诱去某个大厂工作了两年,工作强度大,上司欺压员工,经历种种不公才得以跳槽到现在的公司。大家纷纷赞赏他的忍耐能力,这样差的工作环境,竟然忍了两年。

有人说在其他城市能租一个两居室,自己住一间房,狗住一间房。来了这里,只能让狗屈居于客厅。另一个同事打趣道:“我现在住的还没有你家狗以前住的地方好。”

有新来的员工问关于落户的政策。大家纷纷提出自己的见解。话题渐渐从落户,到租公租房,到租一个郊区的房子要买车,到汽车每天要收高额的停车费。

有人在经验分享会上讲述买房流程,介绍买房时要考虑的注意事项、和租房有哪些不同。分享之后,同事提的第一个问题是怎么凑到买房的钱。演讲者不好意思地笑着说:“有6个钱包就可以了。”

有同事已经结了婚,抱怨生活中种种琐事带来的烦恼。我好奇地询问彩礼的事情。他认真地分析了各地的习俗,总结说只要彩礼的钱最后是给小两口用就没关系。他回头问我是不是在恐婚,我只好笑一笑岔开了话题。

有人早上到岗时间时早时晚,据了解他和女朋友一同起床。女朋友要早起上班时,他也跟着早起。周五下午,多数人都早早地回家,我问了后才知道,大家都要赶紧回家陪陪家人。

有一位同事刚和女朋友租了一间更好的房子,突然向他人谈起了自己情感上的问题。二人相识于校园,感情融洽。毕业后二人在开始工作,经济上主要由男方负责。平时小矛盾虽有,但这次的矛盾似乎难以缓和:男方平时想买个游戏都犹豫不决,女方却以借钱的名义想买一个贵重的装饰品。一石激起千层浪,过去忍忍就算了的小事全部变成了引爆矛盾的燃料。另一位同事以自己和前女友和现女友的生活经历,半安慰半抱怨地提供着建议。还有一位刚实习没多久的同事竟然也感到了共情,讲述自己工作后劳累无比,懒于在七夕回复在校女友的消息,对方因此断绝了联系。大家都感叹着单身的美好,劝我好好珍惜。我笑了笑,他们嘲弄道:“弈帆,不要笑,你以后也是这样的。”我不是在嘲笑他们,我是在苦笑自己对现实的切换感到无所适从啊!前几个月,单纯的本科好友还在向我炫耀着他女人缘有多广,形象地描述他是怎么如痴如醉地喜欢上了某个人。

工作、住房、情感,这些都是在学校里难以见到的话题。我淹没于一个个陌生的名词中,勉强地记下生活中的种种见闻。但是,有一件事是我不需要特别去记忆的:哪怕有舒适的工作,靠父母付了房子的首付,有了稳定的婚后生活,大家都不想生孩子,都嘲笑着三孩政策。

在三、四十岁的人口中听到这些话题,一点也不令人意外。问题是,我的同事们实在是太年轻了——放在大学里,他们就是路过的同学,就是普通的同龄人。我们的好朋友,基本上都是小学、中学、大学时同龄的同学。可以预见,我们这些同龄的好友,在进入社会三五年后,就会和我现在的同事一样,把重心放到无穷无尽的生活琐事上,变成既熟悉又陌生的样子。

傍晚时分,路上都是悠然地骑电动车回家的上班族。我一边想着上面这些闲事,感叹生活的不易,一边把劳累全部释放到自行车的踏板上,得意地赶超一辆又一辆电动车。忽然,伴随着由远到近响起的几阵急促的喇叭声,一辆电动车倏然从我身边掠过。不用看那颜色统一的安全帽、衣服、后备箱也知道,在这个时间还在马路上飞奔的,只会是一位送餐的外卖员。我们这些“名校”毕业,干着”体面工作“的程序员,好像还没有抱怨生活的资格啊。

这些都是在我视野内的见闻。在我看不到的地方,还有多少人机械地进行着重复性劳动?有多少人提心吊胆地游走在法律边缘的灰色产业?有多少中年人战战兢兢,生怕明天就被裁员?又或是有多少人已经在一声令下后失业,正为生计四处奔波?

再仔细一想,我们这些程序员,谈不上高贵,却也不能说没有抱怨生活的资格。对于世界上的大多数人来说,大家都是一样的——我们不过是苟活于世间罢了。古今中外,人类的目的都是一样的:活下去,好好地活着,幸福地活着。不管是在众人的期待下出生,立下过扬名立万的理想;在盛世时享受着家人的呵护,畅想过未来幸福美好的生活;在遭遇挫折时心有不甘,发出过改变一切的豪言;在众人中脱颖而出,妄想过自己是天之骄子;在初窥知识的边界后,为拓宽人类见闻而奋发图强过,都会在某时某刻,被迫离开心里的象牙塔,破开”自己永远会被他人照顾“的茧房,踏上普通平淡的生活之路。多数人都没有躺着活的资格,要靠自己的双手去挣钱,用钱换取住处,换取食物,在世间守住属于自己的一隅,普通地活下去。

不走出学校,自己看一看这个世界,是无法摆脱幼稚的思想的。感谢现在这段经历,让我知道了自己会活得怎么样,因此应该为了什么而活。

之前学了不少东西,今天以学习笔记的形式分享出来。学习笔记不适合作为入门的教程。

Git/Github使用方法学习笔记

从例子直观理解git

git是一个开源的分布式版本控制系统。如果你去网上搜git的定义,一定会搜到不少这样精确又令人看不懂的定义。今天,我准备根据我的笔记,来友好地介绍一下git相关的知识。

假设你要玩一个流程很长的RPG,你打了一天,打到了半夜。这个时候该做什么呢?没错,存档。不然下次就要从头开始玩游戏了。

除了继承之前的游戏进度,存档还有一个好处:如果你不小心输了,可以从上次存档的地方开始重新玩。甚至你玩着玩着发现进错了结局,可以从好早的档重新开始玩。存档,除了保存进度,还能记录下游戏发展向不同分支的关键节点。

现在再做一个很不现实的假设:你要玩一个无聊又冗长的RPG游戏,但是通关了之后你会得到一大笔钱。为此,你只开始关注能否尽快通关,而不去体验游戏的内容了。你在单位的电脑玩,在网吧的电脑玩,还在家里的电脑玩。你把所有好友都动员起来一起帮你玩游戏。但是每个人的进度不一样就没意义了。于是,你们想了个办法:大家玩完后,把存档放到网络上,下一个人就可以用最新的文档来了。

大家决定按贡献度分钱。同时,大家还想方便地交流每个人玩的具体情况,碰到了什么问题。于是,大家扩展了存档功能,不仅让存档保存游戏内容,还保存了每个人的保存时间、进行了多少游戏进度、想分享的游戏攻略等有意义的信息。

这就是git用法的一个形象的解释。在我的理解里,能够保存项目进度关键节点,并记录有意义的日志信息,就是一个版本控制,不同的人能在不同的设备上同步所有其他人的进度,就是分布式。源代码给你看,就叫开源。合起来呢,就变成了开源的分布式版本控制系统。

这个例子基本上是我自己想出来的,我在想这个例子的时候会主动思考git有哪些性质,这些性质与另一个通俗易懂的事情的哪些性质比较类似。通过这个过程,我反而加深了自己对git的理解。如果读者在学完git后,能够真正看懂这个例子,并且举出一个自己的例子,那就说明你基本理解了git。

这里推荐个比较有名的博客,上面有git的教程。我是跟着这篇博客上的教程入门git的。该教程举了个日志的例子(如下图)。该日志记录的内容,代表了git的大部分操作。可以暂且认为git就是一个自动生成这样格式的版本日志的程序。说得那么好听,git就是个自动记事本。

git的操作

一定要自己开一个项目去学习git,光看不动手很难学会

为了节约写作时间,让我能够写更多的学习笔记,最大化造福他人,我决定牺牲博文质量,偷懒地放上之前做的ppt。

Hello git!

完成上图中的操作,你就可以像写完了一门新语言的hello world程序一样,说自己已经掌握git了。

这么说有点夸张,但也不假。上图介绍了一次完整的git使用流程。首先,需要使用git init初始化一个git仓库,呼叫git这个记录员来帮你管理版本,不然你的文件夹只是一个普通的、无法进行其他git操作的文件夹。之后,随便新建并写入一个文件(假设它名字叫readne.md。用git add readme.md告诉git你加了这个文件。最后用git commit -m "改动信息"来让git把加的文件给记录下来。

我学到这里的时候十分疑惑:为什么要先add,再commit才能把文件记录下来。为什么不一次性把文件记录下来呢?听说还有个叫github的东西,我’commit’完,还要再进行一步操作才能把文件传到github上面。为什么我想在网上存一份文件要这么多步操作啊!

这里,考虑到知道git的人都知道github,而对git操作有着许多的疑惑,我打算用一张简单的数据流图(该图及后文的称呼并不严谨,仅供学习理解)来介绍git、github的关系,以便于接下来的理解。git与github的具体关系还是放到后面介绍。

其实啊,git init这条语句看上去没干什么,实际上在你的电脑里新建了一个“文件夹”(逻辑上的,而不是实际的)。这个“文件夹”是本地git仓库。而这个仓库呢,又分成两个分区:暂存区和实际的版本库。可以认为git是一个管家,add进缓存区,就是叫管家把事情记在脑子里;commit进版本库,就是叫管家用水笔把事情记在本子上——这样这条记录就确实储存下来,不好销毁了。那么,使用add把文件放入暂存区的好处就显而易见了:有的时候我们要改/加的文件很可能出错,我们再提交之前,需要把文件检查一遍,这样出了错就能及时更改。一旦文件commit了,更改起来就麻烦了,之前的错误记录也难以消除。

而github一类的托管平台,则又可以视为一个文件夹。这些文件夹可以看成是网盘,它们由运营平台提供,以便于你能用互联网在不同设备上访问自己的仓库。目前对于git、github之间的关系,add与commit之间的关系理解到这个程度就行了。

git提供了一个十分使用的命令——git status。该命令不会对git仓库进行任何的修改,而仅仅会输出当前git仓库的状态。如上图所示,修改已有文件,add,commit后,git status都会输出不同的信息。要查看具体的修改细节,可以使用git diff

如最开头所说,我们使用版本控制程序的一大原因,就是希望像RPG读档一样,能够让所有文件回到之前某个保存下来的状态中。这样的操作在git中叫做版本回退。上图给出了一个例子,我们不小心在文件里做出了错误的修改,还commit了。这个时候该如何把文件回退呢?

首先,可以使用git log,查看所有提交记录。之后,可以使用git reset --hard <要回退的版本>指令来进行回退。最新的版本叫做HEAD,上一个版本叫做HEAD^,上上个叫做HEADT^^……依次类推,可以快速地回退到上一个版本。除了用这样的方法指定版本号外,还可以用commit的id来指定版本(commit id在log中查找)。指定id的时候不需要把id输全,只需要输入前几位,保证不冲突就行。

除了commit后发现错误想回退,还有一些程度较轻的版本回退情况:不小心修改了文件而没有add,或者已经add了。对于这两种误修改,都有对应的解决情况:如上图所示,如果文件没有add,使用git checkout -- <filename>就可以把对应文件恢复到暂存区的最新状态(最后一次git commit或者git add的状态)。如果文件add了,使用git reset HEAD <filename>就可以把git暂存区中的文件恢复到没有add之前的状态(最后一次git commit的状态),这等于回到了上个问题,文件没有add该怎么恢复。再一次执行git checkout -- <filename>就可以把文件的内容也彻底恢复回去了。

git add是一个很神奇的指令。它的名字虽然叫add,但实际上不管是新建文件、修改文件、还是删除文件,都需要用git add来更新文件在暂存区中的状态。因此,与其叫做add,不如把这个命令称作update比较好。那么,现在问题来了:如上图所示,有一个文件,我不小心让它进入了git暂存区,我想在文件系统里保留它,但不想让它进入git仓库,该怎么操作呢?这个时候就要使用git rm --cache <filename>。这个叫rm的操作是真正意义是上的remove了,而不像add一样有一点歧义。如果想让文件从文件系统中也消失 ,可以用git rm -f <filename>

刚刚我们碰到了这样一个应用场景:有一个文件,我想在文件系统保留它,却不想让它进入git仓库。这些文件可能是我们为了调试而编写的脚本文件,我们不希望这些乱糟糟的文件上传到公开的仓库中。如果我们不做任何处理,每次输入git status都会看到这些在文件夹中存在却没有进入git仓库的untracked files. 该如何让这些碍眼的提示消失呢?这个时候,可以在git仓库的根目录下创建一个.gitignore文件。该文件用于描述不被git跟踪的文件,描述的形式可以是直接描述文件的全名,也可以用*来模糊匹配,还可以直接忽略一整个文件夹。上图的例子中,我们如果在.gitignore中加入test.py,再输入git status就不会提示有文件没有加入git仓库了。

使用github初始化仓库的话,可以自动创建一个.gitignore文件。这个文件也可以参考别人的仓库里的来写。

讲了这么久的git,终于可以来讲github了。git本身是分布式的,每台电脑都存储了所有的git仓库版本信息。但是,实际上使用的时候,为了在互联网上同步信息,我们一般需要托管平台的帮助。github就是这样一个平台。先直接看一看和github有关的操作吧。

不管怎么样,所有git操作都是基于一个git仓库。这个git仓库可以是我们本地的,也可以是github上面创建的。

如果本地有了一个仓库,那只需去github上创建一个空的同名仓库,再用图中的第一条命令把远程仓库和本地仓库关联起来。

但是,使用https的方法通信,很容易出现超时的问题。如果之前不小心对https格式的地址进行了绑定,可以先remove掉。

与github关联的正确方式是使用ssh,这是一种更安全高效的连接方式。先像上图一样再本地生成ssh密钥。

再把公钥放到github上。

这样就可以正常把本地仓库和github的仓库关联起来了。如果本地没有仓库,而是去克隆github上面的仓库,则可以使用git clone指令。

这里特别要说一句。如果只是用别人的代码,clone仓库本身就行了。如果想要对别人的仓库进行贡献,则最好先去github上面点一下fork,分支出一个完全一样的,属于你自己的仓库,再clone这个属于自己的仓库。因为对别人的仓库是无法进行修改等操作的。

和github的仓库进行关联后,还需要学习一些和分支有关的知识,才能开始使用github。以上图为例,在git里创建分支并在不同的分支开发,就好比在同一台电脑,同一款游戏里,不同的玩家玩到了不同的进度上。当前电脑(HEAD)由玩家dev控制,他玩的进度比较超前。之后轮到玩家master来玩,他看dev的进度很前,于是直接拷贝了dev的游戏存档,这样两个人的游戏进度就完全一样。之后master把dev的存档删了,因为这份存档已经没用了。在这个例子里,两个分支都是线性进行,因此只需要把旧的分支向新分支同步就行。但是,实际开发时,很可能两个分支往不同的方向发展,之后这两个分支还需要进行同步。这个时候就需要对两个分支的信息进行细心的比对,才能把两个分支的代码给融合起来。

开始那副图片描述的内容,可以用上图中的操作来实现。

这页ppt做得不好,它实际包含了两类不太相关的内容:

首先是对开始那些操作的收尾:把副分支向主分支合并后,副分支一般没什么意义了,这个时候可以直接删除。

有了分支的知识,我们就可以理解本地git仓库如何与github上的仓库交互了。我们本地仓库和github上的仓库其实是同一个仓库的两个分支,我们需要不断令这两个仓库同步。

如果本地仓库较新,应该用push,等于是把本地的内容向github上的内容合并。

如果github上的仓库较新,应该用pull,等于把github的内容向本地更新。

这里的命令写得不够具体。理论上两个操作的用法都是git pull/push <remote-repo-name> <branch>这样的格式的。远程仓库名一般叫做origin。但是可以通过--set-upstream可以指定push和pull的默认远程分支。直接git push/pull的时候命令行里会报错,按照提示设置upstream即可。

不管是开发自己还是别人的开源项目,都建议创建一个新的分支开发。开发完一项功能后,把新分支推送到github上,再利用github的pull request把分支合并到主分支上。这种开发方式使得在添加新功能时,所有人都能看到哪些代码进行过修改,有利于多人协作。

更新github上的分支的几分钟内,github会主动提示你要不要进行pull request。或者是点击pull request一栏主动进行”New pull request“,都可以开启一个新的PR。

PR的标题和内容一般都有格式要求。如果是向别人贡献代码,一定要向别人的要求对齐。输入完对PR的描述后,就可以正式创建一个PR了。

(这里没截图)PR的审阅者可以在PR的file changes里面查看有哪些代码被修改,确认无误后可以点击approve,并在整个pr的最后面点击merge。这样,一个带有新功能的分支就正式被并入主分支了。

我之前就接触了github,一直用github gui进行个人项目的同步。但是push/pull/commit等概念一直没有弄明白。通过这次彻底的学习后,我建立起了git及github的知识框架,才算是彻底理解了这些工具的使用方法。这个知识框架可以总结为上图。如果一个人学完git的相关知识后,能在脑中建立起类似上图这样的数据流图,那么就可以说是基本理解了git什么多命令究竟是在干什么。

首先,要理解先得存在本地git仓库,在线github的git仓库才有意义。通过add命令,可以把文件夹里的文件托付给git系统进行管理;通过commit命令,可以让git把当前的内容”存档“。

为了使用github,需要把本地仓库和远程仓库关联。如果是从现有的远程仓库开始构建代码,则用git clone;如果是本地仓库已经有了代码,仅需用git remote add关联起远程仓库。后续所有的git pull/push操作都是在同步本地和远程的git仓库分支。

为了对别人的代码进行贡献,需要fork别人的仓库,形成一个自己的仓库。对自己的仓库进行更新后,再以pr的形式请求更新别人的仓库。但开发个人/私有项目时,也建议使用pr的形式更新代码。

git的笔记差不多就这么多。还有很多知识没有在笔记里提及。比如如果合并存在冲突的代码。其实在有了整体的概念后,这些细枝末节的知识反而都很好理解了。

我反正在学完了git的基础知识后,就再也不用命令行,而是用一些其他工具来进行git仓库的管理了。我现在在使用vscode的git管理系统。里面add、commit都可以方便地进行操作。冲突合并也就是点一点按钮的事情。

我的博客的分类/标签管理方法

之前我把博客里所有博文的分类、标签进行了重构,一直没有提及。这里简要介绍一下重构的内容,及重构后分类和标签的使用依据。

之前的分类和标签貌似是英文。我也不知道自己怎么想的。既然博客的内容全是中文,那么读者也全是会中文的人,用英文的分类/标签没什么意义。当然,我是发现自己想给新分类取名,却怎么也不知道如何用英文来表达某个中文词语的意思时,才准备把所有分类/标签改成中文的。

现在的分类是按照文章的性质进行分类。目前分类仅有两级,直觉上分三级可能更好一点,不知道以后还有没有改进的空间。

目前有学习/杂谈/经验分享/记录这几大类。”学习“就是分享一些客观正确的事情。”工具用法指南“包括软件的安装使用方法,这些是最没有技术含量、原创性,最死板的内容‘;”知识“是我对某一学科的知识的再表达,主要是巩固我自己的学习,内容有一定的原创性,但根本目的还是复述别人的知识,这个名字取得不好,改成”知识分享“或许更合理一点;这篇文章所属的”学习笔记“可以说是劣化版的”知识分享“,原创性稍高一点,但完成度低,内容连贯性差,不适合作为第一手学习资料,适合作为参考,或者用于我口头讲授的素材。

”杂谈“就是中小学生作文。有的文章是有一些深意的,有的文章是随便写的,”趣文“里的就真的是写来玩的。

”经验分享“和”杂谈“一样,是原创性极强的作品,主要是我个人的见解。但提出系统性的个人见解非常困难,还需要保证不与客观事实或参考资料有出入,目前文章较少。”经验分享“这个名字也不是很恰当。说不定以后”经验分享“专门来放一点原创性强,但是不够有深度和完成度,随便写的一些经验分享文章。专门开一个”我的理论“来放最具原创性和深度的文章。

”记录“里包含了专门对我完成的某件事做描述的文章。这里有些东西是为了好玩而写,有些东西是为了丰富个人简历,专门写给别人看的。

分类只能给所有文章从性质这一个维度上进行一个大概的分类,但我感觉用标签检索文章更加合适。标签比分类更加灵活,可以像分类一样描述文章性质(日记),也可以描述文章涉及的内容(数学),还可以标识一类专题(未来可能出现的”博士毕业论文“)。目前标签我是想到什么放什么。

用标签的时候会有一个问题:理论上标签越多越好,但是如果某一天突然想出一个新标签,但以前有些文章可以套入这个标签,却还没有加,我该怎么给以前的文章加标签呢?或者说我想出了一个新标签,却忘了以前想出了类似的标签,取了个不同的名字,这样标签该如何对齐呢?感觉标签使用起来非常麻烦。一个简单的方法是怠惰处理,以前的内容不管了,只管现在和未来的标签是正确的。

写这篇学习笔记的感想

确切来说,要分成对”学习笔记“的感想和对写这篇文章的感想。

我之前一直不肯写学习笔记。一个人真要学会一个东西,不一定要像”学霸“一样记下花花绿绿的笔记,只需要在关键处进行顿悟式的理解,再在脑中建立知识框架即可。在学一个东西的时候(尤其是学习需逻辑思考而非记忆事实的知识),很可能什么都不用记,或者只是对关键知识进行了演算或者分析,留下一些草稿。记录笔记,其实应该是一个向他人表达知识的过程,应该把这个过程用于巩固知识的理解。在这一过程中,把问题讲清楚一般是十分困难的,因为我们的大脑此时都用于如何让脑中的知识变通顺,而无暇顾及他人能否理解我们在讲什么。只有完全学懂了,才可能写出漂亮的笔记或教程;若是对知识的理解更高一层,才可以写教材。我之前一直认为我写的学习笔记没用,因为这些学习笔记属于我个人学习的副产品——正如我们呼出的二氧化碳一样。要把它们转换成能给人看的东西,需要花很多心血。我自己学东西的时候,经常去网上查资料,总能查到这样的一些学习笔记:这些学习笔记缺乏逻辑性,完全不能用来进行自学,看它们就是在浪费时间。我不希望自己的发出来的学习笔记干扰他人的学习。但最近我开始愿意发出自己写的学习笔记了。

这里再次强调一遍学习笔记所属的性质。我之前”知识“(未来将改成”知识分享“)中的文章,是以现有教材为素材,记录我个人分析与总结的文章;而学习笔记,则应该算是自己写出来的学习素材,我加上了一些描述以使得它们勉强可读。这两类文章本质上来说都是我自学的时候用的。

现在开始讲我愿意发学习笔记的原因。首先是因为最近我有分享知识的需要,制造了一些用于讲授的学习素材。这些素材只要加工一下就能发出来让别人看了。有了积累,不发表出来,实在太亏了。我是不愿亏的人,很想有了东西就发表出来。

发学习笔记的最大问题是需要加入一些描述性的文字。写这篇学习笔记之前,我的想法是随便写点文字描述一下就得了。但是,写着写着,我发现不多写点字是不可能把抽象的知识描述清楚的。最后,我只好在文章质量上进行妥协。文字优美与否、通顺与否我已经关心不了了,我只能尽可能让文章可读,并且赶快把文章写完。赶快做出了一个能用的东西,比憋半天什么都做不出来要好。

我之前不想发学习笔记的另一个原因是,我把学习笔记和教材搞混了。教材需要对知识进行面面俱到而精准无误的描述。这样的工作量的巨大的。现在我想通了:学习笔记就是参考资料,我也强烈建议读者不要把学习笔记当成自学的资料来用。学了一会儿,或者知识掌握得已经差不多后,再来以批判的眼光看学习笔记,哪里写得好,哪里有疏漏。这样看待学习笔记才是对的。

再吐槽一下写这篇文章的感想。

我讲这几页ppt的时候感觉十分轻松。我感觉随便写点内容就能把ppt串起来了。但实际写文章的时候却不是这样的,我感觉ppt缺斤少两,逻辑混乱,得加大量的文字才能把话说清楚。最后,我花了比预计多得多的时间才把这篇博文完成。

可见,基于语音的授课的教学效果还是很好的。加上答疑机制的存在,理想情况下教师能够把要讲的知识完全传授给学生。只要学生稍微会学习,老师有强大的表达能力,就能把知识通过上课这种方法传授出去。可惜,大部分人不会教学,逼得学生通过阅读的方式自学。阅读式学习对教授者和学生的要求都更高。真的要感谢那些写出优秀教程/教材的人。

再对我自己和读者强调一句:凡是以实践为目标的技术,一定要在实践中学习,不然学不进去。我们从小到大写那么多作业,就是为了利用实践来加强对知识的理解。对于一种编程技术,学习并验证学习情况的最好方法就是把这个技术用出去。

写这篇学习笔记太累了。如果还要保持产出,我只能进一步降低学习笔记的文章质量了。

星期五的下午

星期五的下午。

教室里的气氛逐渐热闹了起来,老师也放慢了语速。

下课铃一响,就像刚煮开的水一样,教室里顿时充满了吵闹声、桌椅的碰撞声。

几个同学笑嘻嘻地朝我冲了过来。

“去你家玩电脑吧!”

“可是,我爸爸说上周来的人太多了,还骂了我。”

“没事,今天我们不会那么吵的。”

“好吧,那你们可要早点回去啊。”

尽管带着一丝畏惧,我还是急不可耐地带着同学回到了家里。


星期五的下午。

虽然老师不在,教室里依然鸦雀无声,连下课铃也没有打破这份沉静。

我慢悠悠地收拾着卷子,一位好友前来搭话。

“今天你考得怎么样?”

“还好吧,后面大题也错了不少。”

“唉,明天又要考一天的理综。”

“是啊。”

寒暄几句后,好友先行离去。桌面收拾干净后,我一边想着晚自习和周末的计划,一边拖着脚步离开了教室。


星期五的下午。

吃过午饭,宿舍里的四个人都懒洋洋地躺坐在椅子上。

“你们论文写得怎么样了?”

我发起了午后的闲聊。

“马上就要交了,我的导师都不理我,很烦。”

”我还在写。“

“我的论文快写完了。对了,晚上继续打lol吧?”

“额……好吧。”

稍作思考后,我还是勉强答应了下来。


回过神来,现在是星期五的下午。

开完冗长的周会后,楼道里地响起了接连不断的脚步声。

“周五办公室里的气氛都欢快了不少啊!”

“是啊。”

只听一位同事在向旁边的人吐槽着。

“明天就是周末了,今天加班的心情肯定是最差的了。大家都想着早点回去啊……”

未听完他们的谈话,我就关掉电脑,背起书包,急匆匆地离开了办公室。骑上山地车后,我飞快地蹬着踏板,追过了一辆又一辆电动车,赶回了家里。

桌上不是装着XP系统,内存只有1G的老台式机,而是一台崭新的PS5。

今天的班上完了,再也不用想着如何准备考试了。想到这里,我轻快地打开了游戏机。

下个月也没有什么毕业论文要交。下周一别人吩咐什么,我做什么就是了。这个周末是属于我的。我满怀期待地畅想起周末的娱乐计划。

多好啊!没有人会担心我的视力,不让我玩游戏。

多好啊!没有令人压抑的考试,没有做不完的试卷。

多好啊!没有迷茫而焦虑的每一天,没有为远离压力而强作欢笑的逃避。

我过着之前梦寐以求的生活。平时高效率地工作、学习,周末无拘无束地玩耍。我发自内心地享受着现在的时光。

我可以像儿时一样,把全部时间投入到游戏里。

我可以像儿时一样,创造并沉浸于自己的内心世界。

我可以像儿时一样,不去担心明天的到来。

——可惜我不是小孩子了。

写博文的感想

用简练的文字把意思表达清楚,这是写文章必要而又需要花时间练习的事。熟练之后,文章简练了,写文章的时间也会缩短。恰好我最近常常有很多感想,却又担心时间不够,不敢下笔,我有着锻炼写短文的需要。正逢今天时间有多,我可以快速地写一篇小短文,抒发一下心里的感想,练习一下写作。写博客又不是什么特别的任务,没必要非得长篇大论。我争取尽快习惯简明扼要地表达思想,多多记录一些所见所闻、所思所想。

文章很简短,我却边想边写,写了很久才写完。恐怕是因为文章中融入了不少回忆与感情吧。蕴含在文字中的微妙的感悟,只有有经历的人才能看得出来吧。

Pytorch安装教程(包含对齐cuda、cudnn版本,VSCode中Anaconda的设置)

安装一个编程环境/编程库的过程是无比烦人的。安装环境时不可避免地涉及一些和该环境有关的知识,而配环境的人多半是新手,面对未知的知识时总会碰到许多麻烦。比如第一次配和C++有关的库的时候,新手就不知道.dll应该加入环境变量,.h应该加入include。而在写教程的时候,也一般都是新手在写教程,老手们对配环境这一“简单”的事情不屑一顾。因此,我们一般只能在网上看到新手写的配环境教程,上面都是按部就班地介绍一步一步的操作,讲不出每一步操作为什么要这样做,那些操作可以更改或者跳过,碰到了问题又怎么样。教程的不详细同样是配编程环境令人感到烦人的原因。

基于上述情况,配编程环境考察的是人利用搜索引擎的能力。在配环境时,应该多参考几篇文章,有英文阅读能力的应该去参考官方给的配置教程。出了问题把问题的出错信息放到搜索引擎上去查。一般多踩几次坑,多花点时间,环境总能配好。

也正是基于这些原因,我觉得写一篇环境配置的文章很没有意思。没有技术性和原创性、繁琐而无趣。但恰逢我第三次配置pytorch环境,一切都轻车熟路了。我准备在等待文件下载安装之余截几张图,水一篇文章。

准备

去搜索引擎搜索pytorch安装 你的操作系统。比如我这次是在win10上安装,就选择输入”pytorch 安装 win10”.当然,一般会用到Anaconda来安装python上的一些库。在搜索中加入”anaconda”关键字可以搜到更多的教程。

当然,官方网站上也已经给出了不少信息了:https://pytorch.org/get-started/locally/

我的博客是随手写的,看这篇文章可能解决不了所有问题。正如前言中所说,应该多参考几篇教程。

安装Anaconda

Anaconda可以让用户更好地管理python包,配置pytorch需要先安装Anaconda。我说不出它的优点,反正大家都在用,我也一直在用。

https://www.anaconda.com/products/individual#Downloads 这里可以下载Anaconda

如果使用的是Windows,安装以后,点击任务栏中的搜索框,搜索Anaconda,打开Anaconda Powershell Prompt (Anaconda)或者Anaconda Prompt (Anaconda)。Ubuntu貌似进入命令行进入了Anaconda。

在conda命令行中输入conda create --name env_name python=3.7

env_name是虚拟环境的名字。装Python库,就好像在一个新房子里放家具一样,时间久了东西越来越乱。Anaconda给了你无限创造新房子的机会。你可以在一个完全没有“家具”,甚至连Python都没有的虚拟环境里,重新配置库文件。env_name就是虚拟环境的名字,我一般命名为pt,这样输入起来很快。后面的python=3.7是可选的,python版本可选,这句话加不加也可选。没加的话晚点安装python也行。

创建完环境之后,输入conda activate pt就可以进入新的虚拟环境。前面的(base)应该会变为(pt)

搞成上图那样就算配得差不多了。

CPU版本的Pytorch

Pytorch等深度学习框架分CPU版和GPU版。GPU版速度快,但装起来有点复杂。如果只想稍微学习一下编程框架,以后用GPU服务器跑正式代码,可以只装CPU版。

这个时候,为了避免GPU版教程的干扰,应该在搜索引擎中搜索Pytorch CPU 你的操作系统

一般输入conda install pytorch-cpu torchvision-cpu -c pytorch就可以安装好了。但这个时候,一般有一个坑会碰到:下载速度奇慢无比。

这个时候,应该去网上搜索conda 下载慢类似的关键字。网上一般会给出以下几个办法:换源、直接下载安装包、用pip下载。源就是下载的来源网站,在国内的话最好切换到国内的下载网站比较快。我选择换conda源的方法下载。明确了方法后,要继续换搜索的关键字,应搜索conda 换源。我搜索到的办法是:输入conda config --set show_channel_urls yes。之后在用户根目录(比如C:\Users\Yifan Zhou)下编辑.condarc,加入以下内容:

1
2
3
4
5
channels:
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
ssl_verify: true

我发现换了源还是下载得很慢,于是直接挂了个VPN再下载(哈哈哈)

比较好的方式开始用换了源的pip

下好之后稍微验证以下。比如安装之后,继续在Anaconda的虚拟环境中输入以下内容

1
2
3
python
import torch
print(torch.__verion__)

GPU版本的Pytorch

确认显卡驱动

在安装GPU版本Pytorch之前,还要保证电脑中有版本正确的cuda和cudnn。cuda是一套GPU上的编程库,cudnn是基于cuda的深度学习库。Pytorch依赖这两个库,而cudnn版本依赖cuda版本。而由于cuda貌似更新比Pytorch快,出于向下兼容的考虑,应该先在电脑支持的CUDA版本和Pytorch支持的CUDA版本中选择一个较小者,再下载该版本的CUDA和CUDNN。

但是,在安装这些东西之前,还可能碰到一个坑——显卡没装驱动。这里默认电脑使用的是N卡。在控制台中输入nvidia-smi,如果出现了类似下面这种信息:

就说明驱动已经安装好了。如果没有,请搜索显卡驱动,适当加入”Nvidia Cuda Cudnn pytorch tensorflow python”等关键词,多看几篇教程总能把驱动装好。

输出信息中的CUDA Version就是电脑显卡最高支持的CUDA版本。到网上下CUDA也应该不超过这个版本。

CUDA

记录下nvidia-smi命令输出的CUDA版本(比如我的是11.1),再在 https://pytorch.org/get-started/previous-versions/ 查Pytorch是否支持该CUDA版本。如果不支持,就选一个网站上有,且版本小于命令输出的CUDA版本的。之后去 https://developer.nvidia.com/cuda-toolkit-archive 找到合适的CUDA版本,选择版本后选择自己对应的操作系统。下载了可执行文件后直接运行安装。

CUDNN

打开 https://developer.nvidia.com/rdp/cudnn-archive 这个网站。注册账号并登录。根据CUDA版本,找到合适版本在合适操作系统下的CUDNN。(注意是下载cuDNN Library,比如[cuDNN Library for Windows10 (x86)]

正如前面所描述的,CUDNN是个库,不需要安装,只需要把其文件复制到CUDA文件夹下即可使用。要把CUDNN中的bin,include,lib都复制到CUDA的对应目录下。

注意,CUDA的bin文件夹应该被添加进环境变量。用默认设置安装CUDA时一般该文件夹会自动被加入环境变量。

Pytorch

https://pytorch.org/get-started/locally/ 中,选择合适的操作系统和CUDA版本。参考CPU版本的Pytorch这一小节的内容,用conda一键下好pytorch。如果网速慢,请利用搜索引擎解决问题。

一切都下载完后,在环境中输入以下内容,看到True就是胜利。

1
2
3
4
python
import torch
import torchvision
torch.cuda.is_available()

风雨夜行记 ~ Adventure in a Rainy Night

初到上海,老天爷就隆重地请来嘉宾”烟花“迎接我的到来。一场突如其来的台风席卷了城市,大雨倾盆而下,把人们关在了家中,连多家知名餐馆都停止了外卖服务。傍晚,我在生活用品尚不齐全的新房间里,摸着空空如也的肚子暗自叹息。怎么办?是吃点零食,全身脏兮兮地回床上避难?还是一往无前,出门购买食物和所需的生活用品?向来喜欢追求成果上限的我自然是选择后者。

出门之前,我先在脑子里进行了任务计划。我的主要目标是吃饭,次要目标是购买生活物品。既然出了门,就要吃得好一点,并且去大超市买东西。我的住处附近恰好有一个大商场,附近必然有不错的餐馆。我在地图上看好了路线,背上用于运货的书包,带上心爱的遮阳挡雨两用自动伞,就头也不回地出门了。

刚一出门,我就意识到了今晚行动的困难性。屋外风雨交加,骤雨把漆黑的天空和昏暗的街道融为一体,雨点落地声甚至盖过了地铁启动时的轰鸣。今天晚上,我的行动恐怕和在RPG里做主线任务一样充满挑战吧。

果不其然,我很快就碰到了第一道关卡。在我的住处和大马路之间,有一条必经的小道。小道没什么人维护,平日里看起来幽雅宁静,下雨时还因积水形成了一条小溪,真多亏了它糟糕的排水系统。我把第一个场景命名为”积水巷道“。在积水巷道上,电动车驾驶员都得小心翼翼地行驶着,生怕把水溅到身体上,我一个撑着伞的行人又该怎么通过呢?我倒是不慌,想着:凡是游戏里的关卡,必然设置了通关的方法。很幸运,我发现路对面有一条较窄的人行道,上面没有积水,可以安稳地通过这条小道。想来这条路的建设者并不是没有注意到路面会有积水的现象,还非常贴心地建了一条下雨专用人行道。看得出这个建设者以前应该是个程序员,碰到解决不了的BUG,就给用户另一个更复杂的操作步骤来绕过BUG。凭借着细心的观察和对程序员的理解,我顺利利用人行道离开了积水巷道关卡。

离开了小道,来到大马路上,我立刻见识到了台风的威力。本来几阵夹杂细雨的微风就让我撑伞撑得十分吃力了,忽然的一阵狂风让雨伞飞舞起来,整个伞面都翻转了过去。不仅是我,街上其他撑伞的行人都受到了不小的影响。只见几个行人万般无奈地收起了伞,在雨点的敲打下叹息着。我碰到了新的挑战:“狂风事件”。狂风事件会在行动时随机发生,令行人失去撑伞能力,被迫淋雨,降低行动能力与士气。如果不是有不得不去做的事情,一般人遇到狂风事件都会退缩,立马跑回家去了。然而,我却十分冷静地面对着突然刮起的大风,使用我独创的“相对论遮风挡雨法”消除了狂风事件对我的影响。

什么是“相对论遮风挡雨法”呢?这里就要介绍一下背景信息了。之前出去旅游的时候,一向不喜欢撑伞的我,被太阳晒得没办法,买了现在这把遮阳挡雨两用自动伞。我发现遮阳的时候如果和遮雨一样正着撑伞,有的时候遮阳效果并不好,因为太阳的光往往是斜着照过来的。然而,不管你人朝着哪个方向走路,都把伞正对着太阳,就能起到非常好的遮阳效果。因此,我独创了一种“相对论遮阳法”,根据太阳的方位,在走路的时候固定手和伞的朝向,而不管自己朝着哪个方向走。今天碰到了台风后,我瞬间领悟了“相对论遮风挡雨法”,以应对狂风事件。这种撑伞法相比之下更加复杂,需要分两个步骤进行。在第一步中,需要用手无力地把伞举起,通过伞的运动方向感受风的方向;在第二步中,用类似相对论遮阳法的手法,把撑伞朝向固定为风吹来的方向。凭借这种方法,无论风有多大,只要握紧伞把,伞就不会翻转,还能把雨滴全部挡下来。使用相对论遮风挡雨法后,我自由自在地在风雨中走着。

由于我肚子实在太饿,我决定改变计划,立刻吃饭。马路对面有几家店还开着,我满怀期待地过了马路,希望能找到一家合适的餐馆。结果这边还在开门的只有几家装修平庸、看上去就不靠谱的餐馆,剩下还在开门的店就只有日式便利店、药店,还有一家写着“内有被褥”的杂货便利店。这些店都不能解决我的饮食问题。我有些低落地向前走了几步,突然眼前一亮,看到一家装修精美的餐厅。原来是一家看起来质量还不错的咖啡馆。但是这种店又过于正式了一些,不适合一个人随便解决一餐。一个人吃顿饭还是有些难啊,这样的店就适合几个人一起去,尤其是情侣。情侣?哼!这种店就是为了提供这样的氛围才装修得这么好的,太不为单身的人考虑了。一想到这,我就气饱了,也不管肚子饿不饿了,义愤填膺地从店门口晃了过去。

又走了一段时间,我发现店面越来越少,路也越来越窄,最后竟被一棵吹倒的大树拦住了去路。这感觉不对啊,是不是走错了路?我破天荒地打开手机看了下地图,反复确认了我走的路是正确的。这棵树是怎么回事?我很快给了自己一个合理的答案:RPG游戏里经常会出现第一次能走通的路在游戏后期被拦住的情况,玩家需要进入一个新的场景,通过更难的关卡,来绕过障碍物。想到这里,我就摩拳擦掌,对即将面临的新事件期待起来:我是不是要自己找一条路出来?结果,我又看了一眼地图,发现地图导航提示我在此处应下台阶。我这才发现,这条路本来就是死路,我应该走地下通道绕过这里。我刚刚提起的兴致一下就被浇灭了。

终于,商场的大楼近在眼前了,我只需要过一条比较宽的马路就行了。可是,我又碰到了新挑战:马路上有不浅的积水,穿凉鞋倒是可以不管不顾地走过去,可我穿了运动鞋出来。看来我不得不面对这“无伤通过积水公路”的挑战了。细心的我发现斑马线是有一定厚度的,踩着斑马线走几乎不会沾到水。绿灯一亮,我就使出”轻功水上漂”绝技,大踏步地踩着斑马线走。最后一条斑马线离人行道还有一段距离,这之间还有不少积水。为了保证自己是“无伤通关”,我一扭头,令积水恰好能反射路灯的灯光。这样,我就掌握了所有的危险地带和安全地带的位置。踏过几个较干的路面,我成功抵达了马路对面。

地图上显示商场的关门是十点,而现在是八点多,时间还算充裕。我看商场对面开着的店铺很多,决定先找个地方吃饭。有好几家店看起来都不错,该去哪一家呢?正当我准备开始分析时,突然发现一个外卖员从一家面馆走了出来。能做外卖,这个点还有人点餐,说明这家店应该还不错。特别是我发现这家店里还有一个正在用餐的人,这让我对这家店的质量更加放心了。进店之后,我直接点了最贵的红烧牛肉面。面端上来用了七、八分钟,这对饥肠辘辘的我来说就像45分钟的语文课一样漫长。加几滴醋,用筷子微微搅拌,捞出一大把筋道的面条,配上大块牛肉,我享受这绝妙的用餐体验。一想到吃一辈子红烧牛肉方便面也吃不到这么多牛肉,我就吃得更香了。我吃得过于投入,以至于身上的雨水渐渐变成了汗水。吃完面条,我没有休息,立刻走出店门,吹了下风才发现身上出汗了。

马上,我的旅途就要抵达终点了。在明亮的商场里挑选商品,而不必四处躲雨,这就是我即将获得的通关奖励。买完东西之后,我还要打出租车回家,结束这冒险之旅。一边想着这些幸福的事情,我很快来走到了商场门口。商场里面倒确实是十分明亮,可大门却是锁着的。十点没到,商场怎么关门了呢?看来今天台风的影响确实很大,出于人道主义的考虑,商场的工作人员都早早下班避雨了。这么大的雨,也没人来买东西,商场也赚不到钱啊。想到商场关门的种种原因,我顿时觉得自己十分草率,竟然出门之前不去从商场的角度考虑,忽视了商场有提早关门的可能。但也不亏,我的主要任务——吃饭已经完成,买生活用品的支线任务就放一边吧。站在商场前的路灯下,我偶然抬头一望,针状的雨在光线下清晰可见,猛烈地扎在我伞上。看见无色的雨本身算是一件有趣的事,现在的我却只能感叹今天雨真大。

忽然,我发现自己似乎错过了什么东西。购买生活用品这一任务还不能宣告失败!我又再次充满了动力,沿回家的路走去。

回程时,我走在马路的另一侧。非常烦人,我又碰到了新的挑战:马路这侧的人行道中间的路砖因修地铁而全部挖掉了,人行道外的马路又全是积水,马路上时不时有汽车经过,走马路中间也不太好。我唯一的选择,就是走人行道外侧残留的石砖。石砖非常狭窄,仅能恰好放下我的鞋子。石砖的一边是积水的马路,一边是坑坑洼洼的泥土——还好不是掉下去就死的万丈深渊,起码给我留了一些容错率。于是,我开始挑战“平衡木”关卡了。虽然我天生缺乏运动细胞,但我使用了极佳的前进策略:我一只手使用相对论遮风挡雨法撑伞,为了平衡,另一手略微向外伸出。我出乎意料地顺利通过了平衡木挑战,虽然中间有几次失去平衡踩到了泥土上。

路过“倾倒之树“景点,我再次感叹起了今天行程的不易。仔细想来,今天好像遇到了不少有趣的挑战。于是,我一边走路,一边构思起了这篇文章。

走到了”积水巷道“的入口,我没有进去,而是又过了一次马路,走到了那一排仅有少数亮光的店面前。原来,在发现大商场关门后,我的潜意识一直没有停止思考,一直寻找着购买生活物品的方法。那时,我忽然想起这里有一家便利店写着”内有被褥“。为什么一家便利店要在牌子写这些东西?是真的提示顾客里面卖被子吗?不是这样的,真相只有一个!其实”内有被褥“的意思是,这家店不仅是一个卖饮料零食的普通便利店,还是一个卖生活用品的杂货店!!虽然我没有进店确认,但我利用心理学思考推理出了这一结论。有了这一发现,我才满怀希望地踏上了回程的路,觉得宣告任务失败还为时尚早。

那么结果如何呢?我走进杂货便利店一看,果然,在被子旁边,还有洗衣液、碗筷、垃圾桶这些生活用品,甚至还有电饭锅。我的推理完全正确!我最近一直在看柯南,感觉学到的推理能力一直没有用武之地。但非常巧,今天恰好有一个舞台,让我把推理能力淋漓尽致地展现了出来。不仅是看柯南学到的推理能力,我在之前参与的很多项目、经历中已经掌握了极强的逻辑思考和求解问题的推理能力,并且在玩LOL时锻炼了从对手的角度反向思考的”千层饼“心理分析能力。今天我把这些能力都用了出来,实在值得吹嘘一番。

考虑到台风可能一时半会儿不会走,我不仅把日常要用的清洁用品买了,还买了个电饭锅准备回去自己做东西吃,买了一大堆东西。最后,我一只手撑伞和拎锅,另一只手抱着刚买的装得满满当当的垃圾桶,吃力地走出了杂货店。虽然拎这么多东西很麻烦,但我看着这些”战利品“,感到十分充实与开心。

雨势渐小,积水巷道里的水却依然没有消退。和出来的时候不同,我熟练地走上了人行道,回忆起了今天的奇遇。今天我的行动称不上旅行,用”冒险“来形容恐怕更恰当一些。正如我以前写道的,旅行的意义在于旅行的体验。其实今天吃饭、买东西的任务都在其次,最令我难忘的是一路的见闻、经历。虽说老天不是很给面子,让台风来迎接我的到来,但也多亏了这风雨交加的夜晚,给了我这独一无二的体验。这换成其他任何人,或者其他任何时候,都很难有这样的情况出现吧。再往大了想,人生的快乐也不是那么奢侈的事情。都说”偷得浮生半日闲“,再平淡的生活,亦或是再艰苦的生活,也是能通过一些方法找到乐趣的吧。

满载着战利品,我回到了家中。今天采取了这么多应对下雨的策略,应该取得了不错的效果。结果我一坐下,屁股一凉,才发现我的短袖裤子和短袖T恤都湿了,鞋子也湿了个透心凉。不能说我的撑伞法、过街法不好,只能说雨下得太大,要挑战的难度太大。今天我的主要任务和次要任务都完成了,虽然没有做到无伤,但也是不小的成就了。今天过得非常好,希望以后我在自我挑战中能够再接再厉。

初夏,一片绿叶悄然飘落

一片绿叶

六月,北京的气温开始升高。风和日丽的春天突然离去,闷热的空气充斥了整栋宿舍楼。傍晚,热气依然没有离开宿舍的意思,我只好先它一步,跑到宿舍外散步乘凉。

散步时无事可做,我只好欣赏起校园里那些不起眼的花草树木起来。经历了漫漫长冬后,在第一阵春风的引导下,植物开始竞相生长。大树又一次长满了树叶,和草坪一起,构成了一幅放眼望去只有绿色的单调画面。好在有各色的花朵在路旁点缀,给枯燥的以理工科为主的大学勉强增添上了一分色彩。

经历了一个春天的滋补,初夏的植物都茂盛地生长着。我偶然低头看去,突然发现树下竟然有几片落叶。与秋天落木萧萧的美景不同,此时地上的落叶只有零星的几片,树上也看不到有任何树叶要落下的迹象。想必这几片落叶只是不成器的失败者,看它们枯黄的颜色就知道,和正在生长的绿叶相比,它们并没有得到足够的养分,最后体力不支被风吹到地上。

忽然,一阵风吹过,又一片树叶从树上落下。我的内心一阵欣喜,因为自己的猜想算是得到了证明:哪怕在夏天,依然会有些发育不良的树叶被吹落。但我定睛一看,却又是一阵震惊:树叶从大树深处的阴影中落下,却在灯光下显得绿得发亮;明明是从高处跌落,最终必将驻留在地上,却在风中迟迟不肯落地。

这片落叶是与众不同的吧。和地上那些已经一动不动,几近腐烂的落叶相比,它绿得那么鲜艳,在空中飞舞得那么轻盈。明明马上就会落地,失去活动的机会,却依然奋力地展示着自己的生命力。

和树上那些长得正旺的树叶相比,它也是不同的。它本来应该可以继续在树上生长,用最好的姿态去迎接盛夏的骄阳的吧。那它是不小心从树上跌落,还是自己想离开大树的呢?它是见惯了花开花落,厌恶了大多数植物同样的命运?还是只是想随风飘摇,看一看树外不同的景色?它最好的归宿是哪里呢?

还未等那片绿叶落地,又刮起了一阵更强的风。眨眼间,绿叶就从我的视野里消失了。我想,大自然已经给了我最好的答案:那片绿叶心向远方。我的视野之外,就是它最好的去处。

迷茫与挣扎

说来丢人,马上就要毕业了,我还没有一个明确的去向。

过去的一年半里,我都是在同样一种不安的心情下度过的。我没有踏上迈向未来的“正轨”,我担心自己的未来会陷入迷茫、找不到方向的境地。事实上,到了即将毕业的今天,我的一切担忧变成了现实。

在这种担忧之下,我很难完全集中于眼前的事情,无论它是否会对我的未来产生功利的影响。我在各种事情上犯了很多错误,或者说有很多做得不好的地方。这些错误或失误长久地驻留在我的脑中,自责不断地刺痛着我,让我在做类似事情回忆起这些错误时都倍加折磨。在正反馈调节下,自责引发进一步的错误,事情向越来越糟糕的方向发展。

我很早就意识到了我的所有问题的原因,并尝试去解决。我通过一系列思考分析清了自己的现状,一直保持着十分自信的心态。但是,我只是发现了问题并分析了问题,却没有解决问题,并付出行动。

我一直在迷茫中挣扎,明白了自责不是解决错误的方法。只有从错误中学习,并提取经验为未来服务,才是利用错误的最好方法。在找寻未来方向的这段经历中,我学到了什么呢?经过一段时间的发现和思考,我得到了这样的结论:人生本身就是混沌的,需要自己去寻找方向。我能找到大致的方向,却缺乏主动为方向铺路并沿着道路前进的能力。

温室里的流水线

两年半前,我参加了某个留学中介的讲座。主讲人提到了一个案例:有一个计算机专业的学生,大学前三年都对专业知识一窍不通,但是在大三暑假依然找到了一个和艺术、信息技术相关的暑研,最后大四申请到了该方向上的硕士。主讲人想通过这个例子说明,留学中介可以极大地帮助想留学的同学,哪怕专业能力再差,也能申请到学校。主讲人把这个故事当成了宣传中介的绝佳示例,却让我彻底断绝了向留学中介寻求哪怕一丁点帮助的想法:一个这样的人都能成功留学,那这种形式的留学的意义在哪,又有何含金量可言呢?这样是“没有意义”的。我宁可自己申请不到学校,也不想通过留学中介“成功”申请到某所学校。

我一直坚持着自己的观念,由于种种原因我也确实没有申请到学校。一开始,我还没有相同自己潜意识里想表达的“没有意义”是什么意思。现在,我总算想通了自己申请留学的意义所在:出国留学,是对自己未来的一次主动选择。这其中有许多信息、方向上的困难,而不是思考、智力上的困难。这个过程考验人对于方向的掌握,并为某个方向努力的能力。留学中介帮你完成了许多中间步骤,让你失去了这次锻炼自己掌控自己未来的机会。将来,或许是留学毕业后,肯定还会再次碰到未来方向的选择问题。侥幸绕开了一次考验,只会把问题全部积攒起来滞留到下一次考验上。因此,当时的我就在潜意识中认识到了:选择未来,掌控未来是一种重要的能力,比能否在毕业那年成功留学重要得多。我要利用这次机会主动选择未来的方向并朝着方向迈进。

我在一所还不错的大学里上学。仔细回想,我和周围的人一样,从小到大不过是在温室里的流水线上,在他人的呵护下推动着移动罢了。绝大多数出生普通的孩子都被灌输“万般皆下品,唯有读书高”的信念,巴不得鲤鱼跃龙门,一朝金榜题名改变命运。我和周围这些幸运的同学,在家长和老师的照料下,十二年考场厮杀搏斗,千军万马中闯到名牌大学。我们人生的轨迹是早早地就决定好了的,人生的价值甚至是可以被量化的:用那光彩照人,却又污秽不堪的考试成绩来表示。考试越来越难,周围的人越来越强,流水线上的人越来越少,我们嬉笑地看着跌落出流水线的人,暗自骄傲。

到了大学,父母的呵护少了些,世界那狰狞的本性逐渐向我们展露出来。我们慢慢离开了温室,却依然没有离开流水线,而是逐渐被机器划分到了不同的支流上。有的支流叫做“保研”,有的叫“出国”,还有叫“工作”、“考研”和“创业”什么的。有人会跟你说,每条支流上的风景都很好,有哪些优秀的零件获得了许多奖项,保持着顶尖的成绩,收获了“精彩”的人生。大家趋之若鹜,纷纷在流水线上赛跑着,继续争夺着那几条看起来更体面的支流。

但是,人生绝对不是只有铺好的道路,或是说那些自动运行的流水线的。人生的大部分地方是混沌、黑暗、未经探索而充满危险的。在混沌中前行,会迷茫、犹豫;会陷入深坑,难以挣脱;会因害怕和痛苦而回头逃避;会因找不到同行的人而寂寞惆怅;会对着无人的夜空哀嚎。但是,正是在无人探索的深处,有光明,有宝藏,有能让你忘却路上所有的苦难,并且骄傲地向全世界炫耀的事物。危险而困难,却充满了令人向往的事物,这才是人生的全貌。流水线,只是人生的一隅,一个被前人故意建好的设施而已。

每个人在人生中的某个阶段,总会走下这流水线,被迫去用自己的双脚探索这个世界。亦或是流水线的终点已到,仅需在附近寻找光照,就可以停滞不前却又安稳地生活着;亦或是从某处落下,无法立刻安定,在担惊受怕地瞎逛一阵后,建立起一个勉强供自己生活的根据地;亦或是凭自己的意志离开,不断前行,根据自己脑中的印象,去寻找世界上最危险、最美丽的宝藏。

正是意识到了这些,我对流水线积怨已久。我想凭自己的双脚走路,却又对远处的黑暗感到恐惧,只好暂且跟随有光照的流水线行走。我畅想有一天能逐渐远离流水线,去往更有趣的地方。但十分不巧,一座高山出现了。我的面前是一座垂直的悬崖,高不见顶,给人高山仰止之感。我屡次伸手想借突出的石块攀岩上去,却又屡屡跌落。摔伤的次数越多,我越恐惧,离下次尝试的时间就越久。我看到附近有不少人登上了山:有人乘着流水线,缓缓上了山;有人沿着铺好的、略有陡峭的台阶,很快跑上了山;哪怕是荒凉的周围,也有人攀登上了较矮的山峰,踏上了登山之旅。我开始责问自己为什么没有长得更高一点,手脚为什么没有更有力气一点,上次攀登的时候为什么没有再小心一点。最终,我开始驻足不前。

在温室的流水线里呆久了,我不过是一个大号的婴儿而已。凭借叛逆的本能,逃离了流水线,却发现自己根本没有掌握足够的运动能力。不肯低下头,看着路前行;倒下后,不肯治疗伤口,躺着逃避下一次挑战;不肯向周围张望,挪动双脚,换一条上山的道路。周围的一切都看得清清楚楚,却就是因为种种原因毫无行动。

现在,我不仅看清了周围,还看清了自己。既然向往远处的宝藏,那么再高的山、再陡的崖也无法阻止的我前行。体能不行,就去锻炼身体;一个石块被我抓断了,就去抓旁边那块;跌倒了,就尽快投入下一次尝试。凭借着正确的战略,征服眼前的山只是时间问题。

后记

在外面转了一圈后,我不仅身上凉快了,脑子也清醒了,回宿舍后立刻把所见所想记录了下来。

我又想起那片树叶了。它的颜色那么鲜艳,不是春风赏赐给它的,而是它从树的枝干里汲取营养,自己长出来的。既然它心向远方,哪怕暂时落到地上无法移动,风儿或许也会因欣赏它的美丽而响应它的呼唤,载着它继续飞翔。

人在前进时需要动力,谨以此文自勉。

真·后记

写完这篇文章后,我觉得内容有些羞耻,就立刻把文档关掉,再也没打开过。一个多月后重读这篇文章,还是觉得内容有点不太适合发表出来。不过仔细分析了一下后,我觉得这篇文章里并没有过多地反映我个人的信息,而是很好地刻画了一个人的心理活动,并且表达出了一些思想,对于他人来说还是有一些阅读价值的。照相记录画面,录音记录声音,那么文字则是能把思想、情感保存下来,供愿意欣赏的人品味。我最终还是准备把这篇文章发表出来。

如今我已经有了毕业的去向。我不会嘲笑自己之前的焦虑,毕竟这些都是宝贵的人生体验;我也不会因有了去处就得意忘形,因为这本来就是我应得的。这件事其实不是本文的重点。最重要的是文章中提及的对人生的态度,即如何决定自己未来的发展。不论何时,我都会坚持这样的态度。

“悬梁刺股”不如“乐此不疲”

正文

大学四年中,我有了许多新的感悟。其中,我认为最重要的一项收获是:与其“悬梁刺股”般督促自己艰苦地奋斗,不如在任务中寻找令自己快乐的成分,从而乐此不疲地达成目标。我在大学中印象最深刻的两段经历印证了这一道理。

一、因为热爱,所以成功

第一段经历是我在大一暑假参加的算法竞赛集训。当时,我有幸受到了学长学姐的邀请,提前加入了集训队。我既要和其他大一同学一样,学习新的竞赛知识,又要和其他高年级集训队的同学一起参加模拟赛。作为大一的学生,我要每两天上一次课,每次完成数十道习题;作为集训队的一员,我每周要参加四场训练赛,每场比赛持续五小时,赛后还需要回顾比赛中没写出来的题目。在旁人眼中,我每天都要完成大量的题目,日子过得十分辛苦。然而,我自己知道,我从小就不是一个刻苦的人。我之所以能每天坚持写完那么多题目,是因为我乐在其中。当我思路深陷泥潭,绞尽脑汁也想不出解题之道时,屏幕上一行行的“答案错误”就仿佛是对我的付出所发出的一声声嘲笑;而当我柳暗花明,总算把题目解出来时,屏幕上那一行“答案正确”就是对我最好的盛赞。这句对我的赞美,胜过了之前所受的所有嘲笑,让我收获到了极大的成就感。我享受学习新算法时的恍然大悟,享受想题时的冥思苦想,享受解题失败后的心烦意乱,享受解题时的灵光一闪,享受题目最终通过时的开怀大笑。我热爱算法竞赛的一切。

正是因为我对算法竞赛的这份热爱,让我不知不觉中完成了大量的习题,竞赛水平突飞猛进。在暑假集训前,由于我在高中没有竞赛基础,我的水平在同年级中算不上拔尖。而集训结束后,我的水平得到了质的飞跃,在大大小小的比赛中都取得了不错的成绩。暑假结束后,在程序实践课的期末考试中,所有同学要求组队在两个小时里完成五道题目。我单人参赛,50分钟不到就以全年级最快速度写完了所有题目。之后,我首次参加正式的算法比赛,我们团队获得了银牌。一个月后,我参加国际大学生程序设计竞赛亚洲区域赛。凭借着我通过的一道中等难度的题目,我们队伍拿下了北理2018年第一块算法竞赛金牌。

我在算法竞赛中获得的金牌,为我带来了许多利益上的收获。但是,在参与算法竞赛中最令我难忘的经历,不是登上金牌领奖台的那一刻,而是我大一暑假集训时,那乐在其中的每一天。我的热爱为我带来了成功,但我不是为了获得成就而去热爱,而只是单纯觉得参加竞赛很有趣而已。在参加算法竞赛的过程中,我不仅每天都过得十分开心,更是学会了一个道理:热爱自己的事业,能够取得成功。

二、抛开压力,寻找目标

第二段经历是我在大三暑假开始参与的研究项目。我和另一位同学一起,在本校明振军老师和与本校有合作关系的Farrokh,Janet教授夫妇的指导下进行科研。两位外国教授不仅学术成果斐然,更是善于教学,他们的许多学生已经成为了教授。现在,这两位教授年事已高,却依旧思路清晰、逻辑缜密,还心胸宽阔,毫无保留地指导着各个国家的学生。他们不仅在科研上指导我们,还常常分享生活和学习的心得。有一次,我们向他们请教,在和同龄人的竞争中感到压力太大怎么办。Farrokh教授回答道:“时常保持快乐。成功的人不一定快乐,但快乐的人总能成功。我喜欢我的事业,我做着我喜欢的事情,别人的成就又与我何干呢?我每天勤恳地工作,是因为我热爱我做的事情啊!”

教授的一番话点醒了我,让我开始重新审视自己的行为。在遇到这两位教授之前,我就已经参与了一些科研项目。但是,我感觉自己付出了很多时间与心血,却一直没有什么收获。回过头来一想,我之前参与科研,只不过是为了发论文,提升自己的背景,获取利益而已。我根本没想清楚为什么要去参与科研,只会关注有没有产出,所以参与科研时感到十分辛苦,却又毫无收获。我根本不喜欢我正在做的事情。

我参与的暑研课题离我的专业比较远,导致我对这个课题并不是很感兴趣。但是,在这次的科研项目中,我开始转换思维,试图去发掘这个科研项目中令我感兴趣、能让我有动力去奋斗的目标。结果,还是两位教授的教诲帮助了我。他们告诉我,科研不一定是要做出什么惊天动地的成就。发掘一些新的知识,能给其他研究者带来启发,这就够了。因此,我确立了我在这次科研中要达到的目标:做为本科生,想做出太大的创新十分困难。不如提出一些简单的创新点,并尽可能给别人的研究提供一些灵感。确立了能让我获得成就感的目标后,我在毫无压力的心态下投入到了这个科研项目中,最终完成了一篇论文,该论文目前正在投稿中。在这段经历中,我并没有像算法竞赛一样那么热爱我正在从事的工作,但我依旧去挖掘任务中能让我感到兴奋、愿意为之奋斗的因素。最终的结果证明,在压力下为了功利匍匐而行,不如看向远方,慷慨高歌着踏步而行。

三、不再“努力”,快乐人生

我的算法竞赛经历,让我明白从事自己所热爱的工作,能够既享受过程,又收获成功。我的暑研经历,让我明白哪怕是面对自己不是很喜欢的事情,艰苦地奋斗依旧比不过为了任务中某个热爱的成分而奋斗。但是,我的收获和大部分人的认知有所出入。从小到大,我们都被灌输了要努力学习的概念,也听过种种因努力而成果的例子。诚然,是有很多人凭借着过人的意志,在艰苦的奋斗后取得了成功,比如“头悬梁,锥刺股”的故事。但是,又有多少人倒在了正在努力的路上呢?我们不得而知。从另一个角度来讲,什么是成功呢?在人生的一站中凭借辛苦的努力取得成功,是否又能在下一站成功呢?什么时候能停下来,欣赏人生的大好风景呢?努力,本来是表示尽力去完成一件事,引申为一种积极的工作态度。然而,在诸多艰苦奋斗事迹的渲染下,努力成了抑制自己的欲望、让自己在一种十分不情愿的态度下,依然拼尽全力去做事的代名词。这种对于“努力”的阐释,完全是自相矛盾的:自己都感到辛苦,说明自己的内心根本不认同自己正在做的事情,那哪里来的动力去把事情做好,取得更高的成就呢?因此,在目前从事的事情中感到辛苦的人,要么是因为无法失败而勉强自己

,把任务以最低的标准达成,要么是为了诸如功名的其他目标而强迫自己奋斗,在达到短期目标后就会立刻抛弃当前这项令自己辛苦的工作。正是因为这种对努力的错误阐释,导致有些人盲目努力,倒在了半路上;有些人通过“努力”获得了一时的成功,却在下一件事情中顺从自己远离压力的本能,放弃了努力;有些人持续地“努力”,一直小有成就,却发现自己的努力是没有尽头的;有些人深谙事故,“努力”一时得到了自己想要的事情后,开始鼓吹“努力”,通过他人的努力获利。不管每个人经历如何、结果如何,这些所谓压制自己的欲望进行艰苦奋斗的人,在“努力”时是不幸福的。

“努力”的反面,是快乐地从事一件事。保持快乐的心态能否取得世俗上的成功,这难以下一个定论。但是,保持这种心态的人,有着其他层面上的优势。这种优势体现在整个人生的意义上。人生的意义是什么呢?简单来讲,就是要让自己过得开心。与“努力”相比,热爱自己所从事的事情,能让人持久地获得动力,同时获得享受。更进一步地,不管当前从事的工作是否热爱,都应该从中寻找一些值得奋斗的目标。人生中,除了成绩、背景、金钱、地位、粉丝、异性缘这些世俗、公认的目标,还有着更多精彩的事物。视频游戏胜利时的一幅画面、沙滩上的一块贝壳、茂密草丛中的一束红花、父母的一声叮咛、久别重逢时的一阵寒暄、对视时躲闪的一缕目光、半夜的一段深思、聚光灯下的一场演讲、求知者抛来的一句提问、万千人幸福的笑脸。这些事物,远比世俗的目标重要得多,更能让人士气高昂地前行。“乐此不疲”的人,不需要在人生的路上休息,因为他们一直是快乐的,更是有意义、无悔的;甚至因为他们对于事情有着更多的投入,反而更容易收获其他人眼中难以企及的成功。

“悬梁刺股”不如“乐此不疲”,这是我从大学中印象最深刻的两段经历中得到的收获。未来,我依然会保持这份信念,在自己热爱的领域中取得令自己满意的成就。“悬梁刺股”不如“乐此不疲”

吐槽与评价

其实,现在我有很多时间了,应该静下心来反思一下,并发一些博文了。我有好多东西想总结,并且表达、分享出来的。可惜自己太懒了。

这篇文章是为了学校最终的德育答辩而写的。正常来说,我随便写一下糊弄过去就完事了。但是,这篇文章可能可以发表到学校的”前辈经验分享“书刊上,供新生阅读。我十分想向别人分享一些想法,尤其是对于比我年轻、可能会重新走我的一些弯路的人。考虑到我的文章可能会帮助别人,甚至改变他人的人生,我决定认真写一篇文章。

写文章之前,我的潜意识里就已经制定了一些写文章的注意事项(我的大脑的思考能力实在太强,以至于我不需要刻意地思考,就已经制定了完成事情的策略)。一些策略是为了能让文章被顺利录用,一些策略是为了更好地帮助后辈们。

  • 立意不能过于反常理。显然,抨击学校的不合理制度、告诉大家在大学不应该做些什么的是不允许发表的。但是,如果写一些常见甚至错误的立意,就对他人没什么帮助,我写起来也没劲。因此,我选择了我演讲时最常用的,有点反常理却能让人接受的一个立意:努力学习不如快乐学习。
  • 让审稿的人知道你很厉害。我必须吹一下我取得的成就,告诉审稿人我很强,这篇文章很有参考性。这是我擅长的领域,我在文章中也花五成功力吹嘘了自己一番。
  • 告诉后辈们一些重要的信息。讲大道理谁都会讲,但我特意在文章中写了一些十分具体的东西。聪明的新生会利用文章中提到的信息来提升自己。

同时,文章的字数要求在3000字左右。为了不让自己受到字数限制的影响,我特意没有用word和其他软件,用了看不见字数的记事本来写这篇文章。可谓是殚精竭虑,面面俱到。

制定完了策略后,我就开始写文章了。一开始,我写得非常不顺利,一方面是因为这是篇正式文章,虽然没有题材要求,但还是不能写起来太奔放的;另一方面,文章要花一些笔墨来写事,我只擅长写理和对事情客观而准确的描述,单纯写事对我来说有点不熟悉。结果文章写完,导入word一看,只有2200多字。没办法,我要说的话就这么多,该怎么办呢?

没办法,我开始顺从本心,在文章的总结阶段加入自己擅长的分析说理的文字,瞬间提升了文章的深度,也让文章从各种意义上看起来不够自然了。写完之后,导入word一看,3000字,刚好。我把想说的话说完了,发到博客上了。最终能不能被学校录用我已经不在意了,我已经把我在这几件事上想说的东西都说出来了,我的目的已经达到了。这篇文章中,我已经对我的这种态度进行了一定的说明。我讨厌无意义的结果,讨厌为了不纯粹的目的花费时间。我的目标是分享有用的经验,我认为这篇文章对后辈有着非常大的帮助。如果因为文章的某些观点不太能够被理解,导致学校不发出来,那我也不愿意去为了迎合审稿人而修改内容。

讲完了我写文章的心路历程,现在来评价一下这篇文章。评价之前吐槽一句,我发现自己的文学素养实在太差了。我有着灵活的思维,天马行空的创意。这不仅仅体现在其他方面,还体现在文学创作上。可惜,我没有足够的文学积累,无法做到用巧妙的方式来表达我的意思。但我清楚地知道,只要我有一定的文学积累,我一定写出具有文学性的文章,即用词优美华丽,却不失内涵,有深意的文字。有时间我会去尝试养成阅读文学书籍的习惯。

文章中前几段描写的是我的两段经历。其中第一段经历确实是我印象最深刻的经历,我也时常与别人分享,两次演讲中都分享了这个经历。第二段经历有点生搬硬凑的感觉,与实际情况有些出入,但为了写文章举例子不得不歪曲了一下真实的情况。这几段没什么出彩的地方,不过没有这些经历的人看完这些经历,应该能体会到一些东西。

后面三段总结段本来是很短的,最后我在放开手脚后往里面加入了很多内容。倒数第三段批判努力的内容我本来想单独用一篇文章来写,从本质上分析原因。可惜我实在太懒了。这篇文章浅谈了努力的部分内容,有一点点深度。当然,写这一段时我非常爽,因为我讽刺了非常多的东西。如果稍微对生活有一些观察,就能看懂这一段具体在讲生活中的哪些现象。

倒数第二段虽然比前一段短,却包含了更多的深意。这里的内容其实已经和本文没关了,应该在另一个主题里提及。但为了让自己写得爽一些,我直接就开始把想写的东西全写了。这一段的内容不知道读起来是不是很奇怪,但内容绝对不是乱写的,而是有逻辑,有一些文字背后的深意的。

总结来看,这篇文章是一篇要提交上去的文章,虽然我认为质量已经足够,但缺点还是很多。文笔不够好,这是自然的。文章后面有点违和,这是由于我为了凑字数,转换了写作思路导致的。评价起任何一种艺术作品来何其容易,但真做起来还是很难的。比如这篇文章,我知道有哪些地方不够好,但就是不好改,甚至我没有能力去改。不管怎么说,我认为这篇文章还算是合格,可以提交上去,也可以发到博客上分享一下。

“Poisson Image Editing”论文方法实现

上上篇博文中(现在是上上上了),我介绍了一下这篇图像融合的经典论文。今天,我将记录一下这篇论文的C++代码实现。我已经三个多月没碰C++了,手很生。但是,我会凭借着我高超的编程底力和过人的天赋,三小时内完成代码实现。稍微对工作量有一点了解的人都会产生疑问:“你在开玩笑吧?论文实现不比写算法题,你要先去看懂论文,再调一调别人的库函数,写代码之前有很多准备工作要做。你这不可能完成。”肯定有人这样想。但是,我可以说,包括学习前置知识在内,我可以三小时完成实现。这是一个七年编程王者应该有的自信。这篇博文的正文和会按之前的格式对项目的实现做一个比较全面的记录,最后一个章节会写下我完成此项目的实况。

代码仓库:https://github.com/SingleZombie/Gradient-Domain-Image-Processing-Cpp

由于种种原因,在记录了编程的实况后,我就把这篇博文搁置下来了。但马上要写毕业论文了,这篇博文和论文的内容有很多重合之处,我打算先把博文写完。

知识准备

在代码实现前,我们先整理一下实现方法的整体思路,再提取出实现中涉及的知识点,对每个知识点有关的实现方法和具体的实现技术进行介绍,最后对方法的核心——结果图像求解进行一个详细的介绍。

方法思想

整个方法的总流程图如上。和普通的图像复制类似,方法需要输入一幅源图像、一幅目标图像及复制区域(步骤1),输出一幅融合好的图像(步骤4)。为了得到最终的图像,需要先计算出源图像的梯度(步骤2),再根据目标图像在复制区域边缘的像素值和源图像复制区域的梯度值对结果图像求解(步骤3)。实际上,其他步骤都十分简单,方法的关键就在于第三步结果图像求解。

方法中设计的知识点有:

  1. 图像输入/输出:这项操作的内容很显然:把操作系统中的图像文件读入到程序的一个数组中,再把一个数组输出成一个图像文件。这个可以通过OpenCV库来轻松实现。图像输入输出是OpenCV最基本的操作,网上随便搜一搜OpenCV的教程就能找到。我自己也有一篇博文讲了这项知识。
  2. 图像梯度计算:所谓图像的梯度,就是每个像素与它左边和上边像素的颜色值之差,某处的梯度值表示此处颜色变化速度的快慢。由于后续操作需要源图像的梯度,在实际进行结果图像求解前需要先预处理出图像梯度。实现的时候用一个滤波器对图像做一次滤波即可,OpenCV包括了滤波功能。我的这篇博客简单介绍了OpenCV滤波函数fillter2D的用法。
  3. 结果图像求解:图像融合方法的思想是在融合区域边缘的颜色和目标图像一样的前提下,让源图像的梯度尽可能不变。这是一个最优化问题,最后问题可以转换成求解线性方程组。因此,这一步的目的就是通过求解线性方程组来得到我们想要的结果图像。这是本程序最关键、困难的一步,C++的Eigen库提供了高性能的矩阵运算函数。
  4. 图像拼接:最后输出前,要把处理过的图像区域和整幅目标图像拼起来。OpenCV提供了方便的图像区域覆盖函数。

本程序设计的图像处理操作都十分基础,用一些简单的OpenCV库函数即可。如前面多次强调的,该程序的关键是第三步。下一节将对第三步进行详细介绍。

结果图像的问题建立与求解

(导向插值标记,来自论文[1])

一般来说,用公式描述一件事是方便描述者,而折磨倾听者的。但是,为了把问题准确无误的描述出来,有时不得不使用严谨的公式标记。

在此问题中,我们把图像看成一个函数。如图所示,$S$是一个二维点的集合,即一堆可以用$(x,y)$这样的坐标描述的点。集合$S$就是图像函数的定义域。图像的值域呢?自然是图像的像素值了,这取决于实际情况,比如RGB像素值的范围是$[0, 2^{24})$。

现在,理解了图像其实可以表达为定义域是二维点,值域是颜色空间的值域的一个函数后,就可以继续理解符号标记了。令$g$是源图像,$f^*$是目标图像,$f$是我们把源图像复制到目标区域后,经过图像融合,得到的结果图像。按上一节的话说,$g$和$f^*$是输入,$f$是输出的结果图像。我们还有一个输入,就是进行图像融合的区域$\Omega$。既然我们把图像看成了函数,那么区域$\Omega$就是整个定义域$S$的一个子集。

$v$是$g$的梯度,$\partial\Omega$是区域$\Omega$的梯度,这两个量都是计算得到的。

我们还要引入一个符号——梯度算子$\triangledown$。$v$是$g$的梯度,可以写成$v=\triangledown g$。

重申一下结果图像求解的思想:在融合区域边缘的颜色和目标图像一样的前提下,让源图像的梯度尽可能不变(最小化差值)。用数学语言就是:

如果理解了求解的思想,那么看懂,或者说推导出这个公式是十分简单的。

这个公式是理想情况:图像在一个连续的二维数集上定义。但实际上,我们图像是离散是,只有在整数的位置处有值。我们要把问题用离散的形式表达出来。这里又要提出一些标记,对于$S$中的每个像素$p$,令 $N_p$ 为其4邻域中仍在 $S$ 的像素集合。也就是说,这个符号表示的某个像素上下左右这四个像素的集合,在这些像素没有跑出整幅图像的前提下。令 $(p,q)$ 为一个像素对,其中 $q \in N_p$ 。

再令 $v_{pq}$ 表示 $pq$ 两点间的像素值之差,即p到q方向的梯度值。则上面那个离散化的式子可以被转换成:

能看懂之前的式子,看懂新的式子也不难。

经过一系列数学推导,上面这个离散域的最优化问题可以被转换成一个线性方程组:

这个方程组就比较难懂了。不过,可以用一个直观的方法描述出来:待求解的结果图像的每个像素与周围四个像素的像素值之差(梯度)的求和等于源图像与周围四个点的像素值之差(梯度)的求和。公式中结果图像的每个像素与周围的梯度的求和分了两部分,是因为如果这个像素来自图像边界,则这个值是指定的(来自约束条件,边界像素值等于目标图像的像素值),在等式右边;如果像素不来自边界,则这个值是未知的,在等式左边。

有了联立的线性方程组,问题就变成了纯粹的数学问题,有很多的工具和方法来求解。

程序实现

程序结构图

在理想情况下,程序的结构图如下:(目前的代码很乱,没有严格按照这个图来)

整体分成输入、输出、和图像处理模块,非常简明。

如方法思想中所介绍的,图像处理可以分成图像梯度计算和目标图像求解。而求解又可以分成方程组建立和方程组求解。

这里仅介绍图像处理模块的实现细节。该模块整体的伪代码如下:

这份伪代码只能算是把整个流程用英文表达出来了而已,前文的梯度计算、方程组建立、方程组求解等内容均有体现。接下来几节介绍这三个重要步骤的细节。其他诸如获取图像的一个区域、填充图形一个区域的操作十分简单,可以用OpenCV轻松实现。OpenCV用Rect表示一个矩形区域,假设其有实例rect。令mat为任意一个图像,则mat[rect]就是图像的一个区域,可以执行读写操作。

图像梯度计算

梯度计算是学数字图像处理时的一个基本作业,由于有库函数的帮助,实现起来很方便,其伪代码如下:

拉普拉斯滤波器就是

1
2
3
[[0, -1, 0],
[-1, 4, -1],
[0, -1, 0]]

这样一个3X3的滤波器。手动建立一个滤波器后,调用OpenCV的filter2D就可以完成滤波了。

方程组建立

问题的方程组就是“结果图像的问题建立与求解”一节中最后一个公式所表示的方程组。只有彻底理解了那个公式中每一项的由来,才能把方程组建好。方程组求解时用到了矩阵,这其实就是把方程组的所有有关参数塞入了一个二维的存储结构中,没有太深奥的东西。

方程组主要有3项:系数矩阵项,边缘像素值项和梯度项。系数矩阵是方程组的左端项,后两者之和就是方程组的右端项。这一步的伪代码如下:

系数矩阵项lhs只由融合区域的大小决定。设矩阵的第i行表示第i个像素有关的方程,则第i列就是表示i像素自己的系数,由公式可知系数值是$|N_p|$;而其他至多4个在这个像素上下左右,且在融合区域内部的像素的系数是$-1$。

边缘像素值来自原问题的限制条件:融合区域的边缘像素值等于目标图像在此处的像素值。因此,这一部分的值要根据目标图像及融合区域得到。由于边缘项的出现没有什么规律,实际实现时可以在计算系数矩阵的同时计算边缘项:对每个像素四周判断,如果这一项在边缘上,则更新返回的边缘项;否则更新系数矩阵。

梯度项则完全来自源图像区域的梯度,甚至每个元素的位置都没有变,只要把它的形状改成列数为1的矩阵。

方程组求解

有了所有参数,调库求解方程组就是几行的事情了。下面是Eigen求解Ax=B方程组的代码:

1
2
3
4
5
6
7
Eigen::SparseLU<Eigen::SparseMatrix<float>> solver;
solver.analyzePattern(A);
solver.factorize(A);
Eigen::VectorXf tmpRes = solver.solve(b);Eigen::SparseLU<Eigen::SparseMatrix<float>> solver;
solver.analyzePattern(A);
solver.factorize(A);
Eigen::VectorXf tmpRes = solver.solve(b);

我只有在这一部分贴了代码,因为其他步骤都是比较灵活的,每个人都有自己的实现方式。只有这一步调库的写法是固定的。

求解完方程组,程序其实就基本写完了。

UPD:SparseLU并不是求解此问题的最佳方法。SimplicialLDLT适用于正定对称矩阵,在此问题中有更高的性能(我并不知道其中的原因,但是官网上明确写出这个方法适用于2D泊松问题,我就把结论拿来用了,哈哈)。只要把上述代码的SparseLU替换掉即可。

1
2
3
4
5
6
7
Eigen::SimplicialLDLT<Eigen::SparseMatrix<float>> solver;
solver.analyzePattern(A);
solver.factorize(A);
Eigen::VectorXf tmpRes = solver.solve(b);Eigen::SparseLU<Eigen::SparseMatrix<float>> solver;
solver.analyzePattern(A);
solver.factorize(A);
Eigen::VectorXf tmpRes = solver.solve(b);

结果展示

如图,(a)是源图像,(b)是目标图像,(c)是直接复制的结果,(d)是图像融合的结果。从视觉上来看,图像融合的效果还是不错的。如果能够用套索工具代替方框工具选择区域。用套索工具的话,难点在于实现套索工具本身,这已经脱离了本程序的主要任务了。有了套索的选定区域后,就是获取边缘项烦了一点,大部分的步骤还是一样的。

UPD:

程序里发现了一个BUG!!!方程右端的边缘项应该是相加,我写成了直接赋值。上面的结果图片是错的,正确的图片如下:

后续工作

在下面的”实况记录“一节中,你会发现,我5个小时不到就完成了没有bug的程序,基本完成了整个毕业设计。得知了这一事实,你能得到哪些结论?

首先,你会说:”哇,你好强啊,这么快就把一个看上去那么复杂的项目写完了。“确实,这说得很正确。但这对于我来说是基本操作,甚至我还对自己有些不够满意,写代码的时候急躁了一点,不然可以更快写完。比起学东西,写论文和博客等文档的时间,实际编程的时间确实占比很小。

层次高一点的人,会评论道:”你这项目太水了吧!你干了什么?不久求个线性方程组?高中生都会解这种问题,叫一个大一新生学一学也能写出求解线性方程组的程序。“这说得不错。确实这个项目实现起来很简单。但是,你要注意到,这是一个科研性质的毕业设计。什么是科研?写代码是科研吗?不是。科研是要针对一个问题,提出某些解法,或者说改进现有的解法。重点是思路。我刚刚也说了,我的大多数时间其实花在了看文章、学习上面。这十分合理。

对科研有一点经验的人,又能找到我的漏洞了:”你刚刚说科研的定义,说得很好。但你做了什么呢?你复现了别人的方法。这是科研吗?这有任何创新吗?有任何改进吗?“能说出这种话的人,一定是高手。确实,本文的内容是不太够的。我的创新点在其他地方。这篇文章,只是我毕业设计的核心部分,还有一些内容没有展示出来。敬请期待以后的博客。

真正善于见微知著的人,还能得到其他的结论:”这就是本科教育吗?本科论文就这么水吗?“我只能说:“是的。”本科教育基本没有给学生们提供接触科研的机会,而学校为了方便大家毕业,不会在毕业设计上为难大家。没有了GPA,奖学金这些功利的东西,大家也没有认真做这个项目的理由了。本科毕业设计,就是一个从学校、导师到学生,基本没有人会认真对待的东西,从动机的角度来说,十分合理。

要是你再想得透彻一点,会发现这一切都是不是事。还是我最喜欢强调的,重点是你学到什么,你能得到什么。对于我来说,本科毕设强化了我的科研能力,让我找回了一点写代码的熟练度,锻炼了逻辑表达能力。虽然我花费的很多心思没有任何功利的作用,没有人会注意到我做的那些东西,但我知道我能得到什么就够了,不需要得到别人的认可。本科教育也是这样。本科究竟是为了什么呢?说来可笑,一部分去就业,一部分人继续科研。无论是工业界的知识,还是科研能力,本科一概不教。对于大学来说,能通过高考把优秀的人才招进来,本科设立的意义就已经达到了。既然如此,毕业设计怎么搞不都一样了吗。

实况记录

3.22 18:13

现在,游戏开始。先稍微讲一下我已经做好了哪些准备:我已经建立好了VS项目,导入了之前了OpenCV环境,并成功实现了普通的图像复制。我下好了别人实现论文的代码,以及代码中将用到的Eigen数学库。我还没有学过Eigen的用法,也没有把整个方法的实现流程完全想好。

18:26

理解了此方法中如何构建最后的求解矩阵。现在学习如何用Eigen求解稀疏矩阵。

18:36

知道如何用Eigen创建矩阵,求解矩阵了。可以考虑写代码了。先想办法把Eigen导入项目,并且测试一下Eigen解方程代码。

18:39

很烦!Eigen还要编译,计划中断。我准备一边编译一边盲写代码(写的代码无法进行测试)。

18:57

一边复习OpenCV,一边写完了拉普拉斯滤波代码。同时Eigen似乎编译完了。

19:09

Eigen编译到一半,出了问题,开始向搜索引擎求教。最烦人的配环境时间开始了。

19:21

气死了,这个库可以不编译直接使用。重新开始Eigen的使用测试。

19:36

写到一半,OpenCV配置又出错了。原来是之前写的OpenCV配置文件少包含了一个库文件。批评一下过去的自己,太坑了吧。

19:47

OK,Eigen测试完了,游戏结束。现在所有不确定的东西都搞定了,前置知识学习完毕。接下来我可以展示王者级别的写代码能力了。

20:26

潇潇洒洒写了几十行,突然发现我不会对OpenCV和Eigen两个库的矩阵进行互相转化,又开始查东西了。时间还很充裕,问题不大。

20:30

原来OpenCV提供了转换函数。好,工作继续。

20:37

虽然我没有花半秒钟在程序的软件工程设计上,但我潜意识里已经把代码的结构、实现顺序给想好了。凭借着强大的功底,我本能地使用了自底向上的思想,先把每个细节的实现函数写好了。现在我准备把整个程序拼起来。

20:46

好,程序拼完了,已经开始编译了。说实话,第一遍能不能过(没有BUG)?肯定过不了。无论多强的高手,都会写BUG,都有大意的时候。BUG本身并不可怕,可怕的是不会调试BUG。编程能力展示结束,现在展示我发现BUG的洞察力和解决BUG的决断力。

20:54

代码还是倒在了OpenCV和Eigen两个库的互相兼容上。三个小时快到了,你会不会觉得我很慌?你会不会觉得我已经写不完了?不好意思,现在我才刚刚登上展示实力的舞台。只有在逆境中绝处逢生,才能体现出一个人的过人之处。

20:58

突然发现代码漏写了!图像有RGB三个颜色通道,该方法要对每个颜色通道单独处理,我忘了拆开颜色通道了!

21:03

OpenCV和Eigen相互转换的库函数不能用,我就自己动手!总算,稍加调试之下,代码可以运行了!只不过代码的运行效果明显不对……这下调试起来就麻烦了。很好,最尴尬的情况出现了。我不会放弃的!

21:17

时间已经过了。但我有大约10分27秒花费在了写博文上,这些时间应该不算。继续计时。

21:45

我发现我对论文方法的理解出现了问题。

22:25

更改对论文理解上的逻辑BUG后,又发现8位颜色通道溢出了。

22:34

历经千辛万苦,程序总算是成功实现了。总耗时约260分钟。多花费了约50%的时间,你肯定说我很菜,说我写代码写得烂。但是这种说法并不对。仔细看一看这份文字实况,你能看出什么?没错,我一直在强调我纯粹的编程实力,并没有强调我有快速把论文转换成代码的能力,也没有强调我有快速学会调用那些坑爹的公共库的能力。我写代码写了多久?19:47~20:26,39分钟;20:30~20:46,16分钟,一共55分钟。第一版本的代码,我花55分钟就写完了。后面又调试了一下,20:54~22:34,100分钟,减去重看论文的21:17~21:45这28分钟,又花了72分钟调试,最后也就改了5%不到的代码。我说这么多数据是为了什么?是为了说明,恰恰是这份实况记录,强调了我编程能力之强,强调了时间其实都耗费在一些编程之外的时间上。虽然没有在规定时间内写完,但我感觉虽败犹荣。

当然,你要说我每个方面的表现都很好吗?也不是这样的。我自己也承认,理解论文的时候急躁了一些,学库函数的时候学的不仔细了一些,导致我这个第一版的程序,出现了一点小小的偏差。不过真要说起来,我今天确实有点急于求成。因为立下了3小时完成的flag,所以我做事火急火燎,忽略了工作质量,结果败给了自己。如果一开始没有这个flag,我能不能3小时内完成呢?你可能会认为我会说:“会”。好,实际情况是,我不立这个flag,我今天会玩一个晚上,不会来做这个项目,哈哈。

参考文献

[1]. Pérez P, Gangnet M, Blake A. Poisson image editing[M]//ACM SIGGRAPH 2003 Papers. 2003: 313-318.

最近买了台新电脑,所有编程都需要重新配置,可以写很多水博文了。

Visual Studio 2019(VS2019)安装教程(解决VS scanf问题)

准确来说,这是VS2019在Windows 10下为了配置C++编程环境而写的教程。

在下载前,请务必保证C盘的空间比较充裕,因为不论安装在哪,VS都会挤占C盘的很多空间。

下载

  1. 打开官方网站https://www.visualstudio.com/vs,点击网站中间那个醒目的“下载 Visual Studio”按钮,再选择”Community 2019”(即社区版)。社区版是免费的,其他几个版本都要付费。作为普通的程序员,使用社区版就够了。1

  2. 由于我们点击了下载按钮,为VS贡献了宝贵的+1下载量,我们得到了来自微软公司的感谢。浏览器此时会自动开始下载。

    2下载很快就会结束。规模这么庞大的编程软件,几分钟就下载完了?其实我们现在只下载了一个下载器,待会才会正式开始下载VS的本体。

安装

  1. 下载完成后,在文件夹里找到下载好的可执行文件并点击执行。处理掉操作系统弹出的烦人的提示后,我们可以看到如下界面:

    3

    这说明安装正式开始了。

  2. 点击继续后,下载器会读一个进度条。读条结束后,可以看到如下的安装配置界面:

    4刚看到这个界面,第一次用VS的人肯定会有点懵:哎呀!怎么这么多选项啊!该点哪个呀?

    但如果你是一个有推理能力的人,你看到这个界面,哪怕不去网上查询VS的使用方法,也能猜出这个页面的意思:VS是一个集成开发环境(IDE),可以进行不同语言的开发。这个界面是用来让用户选择要开发的语言的。

    5所以,我们应该根据自己的需要选择要开发的语言。只进行C++的开发的话,只点击”使用C++的桌面开发“即可。上图中,我多点了几个选项。

    6

    下载之前,如果C盘空间吃紧的话,可以点击“安装位置”选项卡,把VS的部分文件装在其他盘上。不过在右下角可以看到,VS的大部分依赖文件还是得装在C盘的。

    点击安装按钮后,VS就会开始下载安装了。

  3. 7

    下载结束后,就可以正式使用VS了。使用之前,会弹出如上的登录界面。可以点击“以后再说”不登录直接使用程序,也可以创建账户并登录。创建账户的好处是,你在一个账户上配置了VS的界面布局后,无论在哪台电脑上都可以直接使用自己这套界面布局。

    通过了登录界面后,还有一个主题选择界面,可以按自己的喜好定义VS的“外貌”。

使用VS写C++的Hello World

第一次打开VS2019,应该会看到如下的界面:

8

又是琳琅满目的按钮。我们不必感到慌乱,只需点击最后一个选项“创建新项目”即可。

9

10

要在一堆选项中选择项目模板。作为一个初学者,我们要从零开始学习。因此,我们先在所有语言下拉栏中选择”C++”,再选择“空项目”.

11

终于,激动人心的编程就要开始了。再次之前,我们要给项目取一个好听的名字。这里我们取名叫做”Hello World”。

12

好了,总算正式进入VS了。代码需要写到文件里,而我们现在还没有创建文件呢。所以,我们要创建一个源代码文件。右键点击右边的“源文件”文件夹,点击添加->新建项。

13

在新窗口里点击”C++文件(.cpp)”,然后在下面的“名称”输入框中输入一个响亮的名字。按照传统,我们给第一个源代码的文件取名为”main.cpp”。

14

接下来,第一份源代码文件就打开了,我们可以编辑这份文件了。无论你是否学过C++,可以往文本编辑器中输入以下代码,达到和上图一样的效果:

1
2
3
4
5
6
7
8
9
#include <iostream>

using namespace std;

int main()
{
cout << "Hello World" << endl;
return 0;
}

按下F5就可以编译并调试这份代码,如果得到了下图的结果,就说明VS能成功编译运行C++程序了。

15

当然,如果使用老的VS版本,或者因为其他一些原因,按下F5后控制台会一闪而过。这个时候。你需要在return 0这一行左边的灰色部分点一下,加一个断点,让程序运行到这行的时候停下来。这样就可以看清控制台的输入了。

16

在VS中使用scanf

理论上,这篇教程已经写完了,大家可以快乐地用VS写C/C++程序了。但是,有些人会用VS写纯C语言代码。为了保险起见,我们不妨测试一下一份C语言的“A + B“程序。输入以下代码并按F5调试运行:

17

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main()
{
int a, b;
scanf("%d %d", &a, &b);
printf("%d\n", a + b);
return 0;
}

结果编译器会报错,不让这份代码通过。

18

这是为什么呢?事实上,VS不支持纯C语言的编写,只支持C++的编写。很多早期C标准库有安全问题,VS不允许使用这些库函数。但在学习C语言,尤其是要把C语言代码提交到在线评测平台(Online Judge)的时候,不得不去使用这些C标准库的函数。为了解决这个矛盾,我们需要对VS的设置进行一些更改。

19

在右边”解决方案资源管理器“窗口中,在项目名”Hello World”上点击右键,再点击”属性”。

20

在左侧点击C/C++,把右边的SDL检查改成“否”。这样就可以使用C语言中scanf等不太安全的标准库函数了。