直到最后,我们也没有去看那片海

我决意要写一篇小说。

幕一

冒出这个想法是在暑假结束前的第十天,其实在此之前,假期伊始,我曾规划好整个暑假的时间,去完成一部对我来说称得上“宏大”的中篇小说,奈何勉强拼凑出数万字的情节,才发现全篇都是别人的影子,所谓辛苦创作到头来不过是拾人牙慧。唏嘘之余,我并未放弃创作的想法,暑假行将结束的日子,跨过群山所见的海、夜奔的少年少女一并涌入我的梦境,我决意要写一篇小说,而且想好了标题——《直到最后,我们也没有去看那片海》,小说结尾处,主角将亲口道出这句话。

下笔之前,构建出一个完整的大纲、理清故事脉络是不可或缺的步骤。所以我联系了旧时好友,希望和他能先商量着捣鼓出框架,接下来的日子慢慢填补小说内容。

见面寒暄过后,我道明了来意,解释了目前的想法:“简而言之,就是在青春终末,少年少女相约去看海,却因为各种原因未能如愿的故事。”

“你想写便写罢,毕竟我也说过,你写不下去的话,我会帮你完成一部分的。”

“多谢。”

接着我们讨论创作的细节,讨论完角色的大体设定和故事脉络,他开始完善大纲内容,而我提笔,开始描绘起方才在眼前浮现的情景。

舷窗外飘起小雨,远处推着小车的保洁工身影逐渐模糊,直到融化成背景里的群青色斑点。机翼缓慢加速穿过雨幕,涡轮引擎呼啸悲鸣,要将他推离这片陌生的土地。

或许是为了弥补彼时的遗憾,高考结束后A选择了这座临海城市,在这里度过短暂的大学时光。不只是太多琐事消耗掉他满腔的热情,或是自己选择了沉沦,溺死在一无所成的日子;隔着流逝的雨幕,他看着形形色色的人影走过,无数憧憬泛着光越飘越远,而那个看海的愿望,早不知飘向了何方。

总而言之,直到他行将离开这座埋葬梦想的城市,座椅上倾脱离地面的一瞬,他才蓦地想起,那个为何至此的原因。

其实机场离海不远,爬升到特定高度,透过舷窗也能望见海洋一角。只是靠海侧在相反方向,而当他想从座位站起,立即被空乘人员微笑着制止,说是爬升期间禁止离座,而他也讪讪笑了笑,又坐了回去。

数年后,他回到故乡,与彼时好友相聚。席间觥筹交错,旧友相聊甚欢,谈笑间他无意问起最后那位坚持去看海的同伴:“所以最后你看到海了?”

秃顶男人轻哼一声,把嘴里烧一半的华子直接掐灭到酒杯里,答道:“没呢,当时我在山里迷了路,遇上个好心人帮忙送了我回去。”

“直到最后,我们还是没能去看那片海。”

</td></tr></table>

幕二

“所以这个秃顶的是B?”

“嗯呢。”

“嗯——这是结尾部分A的自述?感觉最后的转折也太生硬了吧,数年后的部分给人一种很赶的感觉,最后一句话就像是作者写不下去了硬生生凑出来点题的。”

“好像……确实是这样。”

“你来看看我写的大纲。”友人招呼着我凑近笔记本屏幕。

大纲开头处写着类似卷首语的东西:
与其说想去看海,不如说是主人公们对于将要破笼而出,脱离一直束缚自己的小环境的迷茫。与其说梦想是什么,他们更想知道试着去做什么异想天开的事情——真正的幻想实现到底是什么样的。

“你写的什么寄吧。”

“凑合着看吧。”

这段文字过后是人物设定以及剧情大纲。最初的讨论中,我们敲定了A、B、C三位主要人物的形象和关系,友人在方才填充了具体内容。

A(男):成绩比较好。家里穷得叮当响,贼走一遭都得看可怜留点吃饭钱。性格较为木讷。(有时候也会想像自己这样条件的孩子,可能早就应该学坏了,父母收入不多,勉强够不饿死,而自己读书的机会是通过借钱争取来的,一到开学的时候就是挨家挨户借钱。学校里面的贷款只能勉强够补课费用,不过自己也曾幻想过自己沐浴在和风春日中,享受着大学美好时光。

可是每当又因为吃校园里的饭菜的时候,内心深感麻木,每天都要顶着只能稍微饱腹的肚子,在同学们和老师面前强忍着对生活的不满。但是如果自己一意孤行,没有选择好的学校,势必会让家里人付出过分的努力。所以上大学和花费无用的钱去看海,简直就是同一种奢求。)

B(男):成绩比较差。纯纯富哥,奈何家教严格,总有一些不切实际的幻想。 (家教严苛,父母控制欲极强,生活环境压抑;读书的爱好给予了B度过这段晦暗光阴的力量,也让他拥有了许多憧憬与期许)

C(女):成绩一般。寡言少语。(对于A有模糊但却强烈的爱恋,平时在大家面前不爱说话,但是却愿意同A与B多聊上几句,甚至会特意打招呼。在看海的事情上,与其说是想去看海,其实只是不想被A、B丢下,至少能跟其中一个人呆在一块就好。

父亲是货车司机,母亲则在乡下种地或者找零活,跟A一样都是长期在学校吃饭的类型,平时自己住在姑妈家里面。)

“富哥成绩较好,A成绩较差会更好一些。”

“富哥骄奢淫逸成绩垫底才对吧。”

“但是家境好的成绩好才更合理一些吧。”

“……行吧。”

“而且A后面补充的设定也太离谱了,现在谁还读高中靠挨家挨户借钱?首先高中学费本来就很少,而且还有各类助学金。这样写也太刻意了。我觉得除非你写A家里有什么变故,不然根本立不住脚。”

“那我再改改。”

“按我们之前的想法,A家里没必要设定这么困难,倒是家里突生变故的情节可以写到C上面……还有我们加上些生活细节会更好吧,像是A和B喜欢在晚饭后坐在操场边上边看人们踢球边聊天,C喜欢在课间折纸鹤之类的。”

“明白你意思了,那我之后再改改。”

“我写个开头先。”

“呐,要不要去看海。”

昏黄与惨绿交织的背景里,模糊的人影倚在窗前,摆弄着奇怪的手势,不时做捂嘴或捧腹状;依稀能听到它与某人的谈笑声,另一人应该藏在墙壁转角处,恰巧在我视野之外。

“你有没有听啊。”

肩上突然被托付了重量,接着是剧烈的摇晃,强迫着我面对那个声音。

“停停停,你刚才说啥。”

“ 我说,要不要去看海。”

“海?”

“离我们这两百公里不是有一片内海嘛。下个月就高三分班了,周末要不要一起去看。”

“嗯......我再考虑考虑。你现在找了哪些人?”

“我现在只找了你,要不你问问C?她应该会愿意和我们一起去吧。”

我侧过头,往C的方向望去。我和她的座位都靠窗,唯一的区别在于,我在教室这头,她在教室那头。她坐在黄昏的暮光中,认真折着纸鹤——不知是何缘故,最近她卖了一大沓便利贴放在桌角,一有空闲便揭下一张开始折纸鹤,折完便随手丢入脚边的玻璃罐里。前几天她给我看过,玻璃罐里堆着五颜六色的纸鹤,大概占据了2/3的空间,她低着头说再过几天应该就填满了,言语里藏着莫名的笑意。

“那我等会问问她吧。你有什么具体的计划吗?”

“分班考试后我们有一天的假,考试最后一天晚上我们做直通班车到那,第二天玩到十点再坐最后一天班车回家。”

“我家里管得严,但空一天还是能圆过去的。”他又补充了一句。

“好的,快上课了,这事过几天再说吧。”

在我尝试完成意义不明的开头期间,友人整理了剧情大纲的部分。按照最初的想法,应该分为“打算去看海->计划坐班车,结果受到阻碍->解决交通工具的问题,出发去看海->没能看到海”这四个部分,友人完善了每部分的大致内容,准确来说,是将我们讨论的内容付诸笔墨。

“我觉得交通工具可以写成A通过撒谎等方式从叔叔或者舅舅家套来了一辆宝马摩托车。不过说是宝马,其实是他叔叔从网上买了个车标然后贴上去的。”

“所以为什么是宝马?”

“不知道……随便想的。”

之后,我们……嗯,抱歉,由于这篇文章本质上是回忆往事,而我现在急着跑去下云顶之弈,因此减弱了几分回忆的能力。稍等……哦,我想起来了,在那个漫长的夏日午后余下的时光里,我下了两把云顶之弈。

幕三

最后他们准备好了一切,可惜一切并不顺利,首先是打工消磨了时间,其次是司机并不熟悉路线,所以在很多陌生的小乡村里面乱逛,在乡间小路里面穿行。经过几天,本地人恶意加价的油量,出租车上度过的廉价夜晚,让其中一个少年在突出重围的征程中选择了逃避,趁车辆维修,泡在了网吧里面,嘴里说着“打最后一把云顶之奕,我马上就铂金段位,打完就精力饱满地去看海。”

结果另一个少年,在朋友圈发布高中最后的暑假看了海,而网瘾少年连泡两夜网吧后,顺利地抵达钻石段位。当少年从网吧取下耳机,感叹高中无憾时,这时才发现自己被父母围住,突然才发现自己好像还没去看海。

至此,俩人因为高考被强行困在家中。

多年后,当年的网瘾少年问起:“你当年看到了海吗?”

另一个少年掏出手机,让他看看手机里面的图片,只听对方一句:“这不是网图吗?”

“你发现了呀,当年滥竽充数罢了。”

“可惜了......”

“直到最后,我们也没有去看那片海。”

“我要把你下云顶写进小说里。”

没错,此时这篇小说已经完全变成了两人共同创作的产物,一个人写根本没有写作的动力,毕竟赚不了钱。

“哎呀,磨刀不误砍柴工嘛。我本来灵感枯竭,这多下几把云顶这不就文思泉涌了。”

“那让我看看你写了啥。”

“我给你发过去了,主要是一些人物背景的补充和大纲内容的完善,比如为了表现B的背景我打算这么写。”

母亲夹一块鱼肚皮放在B碗里。 

“你看,妈妈每次都把最好吃的夹给你,鱼肚皮我们都舍不得吃的。” 

“嗯嗯” 

“我怕就怕你以后出息了就把你爹娘忘了。” 

“怎么会呢....” 

“哼,之前我们单位里有家孩子读完清华本科就去美国了,去年他爸死了尸体在老家晾一周,他才回来,回来呆三天就又回美国了。你说养这种孩子是不是不如不养,我怕就怕你变成这种白眼狼。”(父亲撑起手肘点烟,故作高深的嘴脸)

“啊嗯......” 

“分班考试准备的咋样?我看你上次排名又退步了,高三了还整天偷偷看那些闲书……” 

“乔治奥威尔怎么算闲书……”(声音逐渐低下去)

“你说什么?” 

“没什么。”

“你但凡把你看那些鬼书的时间花一般在学习上,成绩不得好很多?还是你懒、花的功夫少。”

“其实妈妈相信你的,我和你爸小时候读书都那么聪明,你肯定会比我们更好;我初中本来成绩一般,但我就一直不服输,然后考了全校第二,当时……”

“你再怎么说不也就是个中专,即使按全县的排名我不比你们当时好?”

“你怎么说话的……”

“我们当时才多少人读书;你读书的条件,比我们好得多啊,我就记得当时每周从学校回来还得顺便劈框柴回家……”

“不是,我对比的是全县排名,我们人多不说明我这含金量更高吗?”

“你读这么多书啥什么都没学到,就学会顶嘴了?那这书不如不读。”

“你怎么这么不谦虚。”

“……我去放碗。”

“什么照镜子环节。”

“确实有从生活实际改编的部分,嗯不过我感觉还是不太到位,好像不够压抑吧。”

“那么,你现在打算怎么办。”友人往后一仰,像海星一般瘫坐到床上:“啊——一点都不想动了。”

“别啊,你这样我都开始想打云顶之弈了。”

……

幕四

故事发展最终敲定为:邻近故事结局,由于某些突发情况(与之前折千纸鹤的原因相呼应),C不得不提前回家,但B坚持想去看海,于是三人间出现分歧,A踌躇片刻后决定先送C回家,三人分道扬镳。

她偎依在我身后,臂弯环绕在胸前。我能感受到她手臂的温度,和耳边若隐若现的呼吸声。拂面冷风带走夏夜的燥热,路灯的影子延伸到道路尽头。 那个瞬间,我感觉自己就是世界的皇帝,架起车辇纵横捭阖,身后是千军万马和挚爱的女人;我挥起长剑,要踏平世界的角落。群山在我脚下臣服,夜幕燃成银河,照亮征伐的道路。我听见她说,想要就一直、一直这样下去,直到世界尽头。

但我没有千军万马,也没有挥斥宇内的宝剑,一切少年的臆想都如同叔叔为附庸风雅而贴上的宝马车标,在不经意间被风吹剥落,暴露出原本的穷酸模样。在离家大概5公里处,我的车辇终是油尽灯枯,再也点不上火;我和C推着摩托车不知走了多久,一路上相对无言。在途径学校门口处,我向她挥手告别,我们不再顺路,要回到不同的地方。她迟疑了一会,把手上的红绳解下,系在我的手腕上;她向我告别,消失在小巷的阴影里。

多年后我曾质问自己,到底有没有喜欢过那个爱在窗边默默折千纸鹤的女孩,奈何过往的记忆早已支离破碎,再难拼凑出事件的原貌,我找不到问题的答案。但我可以肯定的是,至少在那个叛逃的夜晚,我只想牵起那女孩的手,和她一起逃离,直到世界和时间尽头。

“那么,其他部分呢?”

“其他部分?”

“比如A是怎么骗到叔叔的摩托车的,以及C接到的电话里到底说了什么,还有……哎呀就中间发生的所有情节你压根就没写嘛。”

“我不知道。”

“不知道?”

秋风敲打着窗棂灌了进来,捎来泛黄的梧桐叶。

“因为实际上故事尚未发生就结束了。在那个夏天结束之前,两位作者并没能把故事写完。这场对话,也只是过后虚构的产物。”

“……”

“直到最后,我们也没有去看那片海”

By ivel, nas

2023/4/28

端口是什么

网络端口

端口标识一台计算机特定进程提供的服务,让IP相同的情况下(一台计算机)可以分辨不同进程。

比如SSH的默认端口为22,jupyter notebook默认端口为8888。

注意事项

  1. 同一台计算机上端口不应重复。 否则会发生端口号冲突
  2. 使用的端口号应大于1024。 0~1023端口为公认端口,明确绑定了特定的服务协议。
  3. 临时端口。 客户端端口号存在时间短暂,大多TCP/IP实现会临时分配1024~5000的端口号。

IP是什么

IP 简介

IP(Internet Protocol Address)是互联网中用于标识和定位设备的唯一标识符,一般有IPv4和IPv6两个版本。

子网掩码

子网掩码用于划分IP主机部分与网络部分。网络部分标识了所属的网络,主机部分标识了具体主机。

例如IPv4地址192.168.0.1和子网掩码255.255.255.0,逐位逻辑运算得到网络地址192.168.0.0(网络地址)和0.0.0.1(主机标识)。

网关

连接两个或多个不同网络设备或节点,实现网络间通信。

网关可以是硬件设备(如路由器)或网络实现(如网络中某个服务器)。完成数据转发、NAT、DNS解析、协议转换等功能。

tips:网关与默认网关不同,后者通常是本地网络中设置的主机,处理与其他网络数据包的转发与出站数据包目标地址,前者则用于任意设备连接不同网络。

MAC地址

MAC地址是网卡制造商预先分配并烧录的全球唯一地址。仅在局域网内传播,不会在不同局域网间传播。

NAT地址中转换技术

NAT将内部私有的网络地址(IP地址)转化合法的网络IP使用,即内部节点与外部网络通信时,在网关处将内部地址替换为共有地址,从而在外部网络中使用。

图示

同一台机器なのに,何故6个IPが有るの?

在终端中输入:

1
ifconfig | grep inet

终端将得到如下输出:

终端上输出的6个IP

第一行输出192.168.1.24为本机在局域网中(比如同一路由器下)的IPv4格式地址,用于局域网络内的通信,第二行为对应IPv6地址。

第三行输出127.0.0.1为环回地址的IPv4格式,用于本机与本机通信(自己和自己通信),第四行为对应IPv6地址。

(第五行输出为本机在广域网下的IP【不对,是虚拟网卡的ip】,在较大范围内可以用这个ip进行远程通信,第二行为对应IPv6地址。)

公网ip用:

1
curl ifconfig.me

ssh远程操作非同一局域网下的服务器用这个ip。

(tips: 广域网IP不等于互联网的IP)

Github使用与背景知识(Git与建立自己的Github仓库)

Git是什么

Git是一个开源的分布式版本控制系统,由Linus Torvalds为更好管理Linux内核开发而设计,让开发者与项目团队能更好地协同工作,在github上提供了一个Git代码的托管服务。1

Git的特点:2

  1. 保存文件修改记录。进行开发的时候,在本地可以保存我们代码,然后上传到服务器中。

  2. 标记版本号,便于区分。每次和服务器交互时都会提交一些修改的代码,git会为每一次提交生成版本号,用这个版本号来进行区分每一次的提交。这个版本号在git当中会使用一个hash值进行唯一区分;这个hash函数使用的是sha1(不仅git使用这个sha1生成hash值,一些著名的软件,如redis、lua等也是使用sha1产生hash值)。

  3. 提供历史版本记录。协同开发的时候,产生的每次提交都会在git上保存有历史版本信息,根据历史版本信息可以追溯到具体代码提交;以及当代码出现bug时可以根据历史版本锁定bug位置。
    可还原到历史指定版本。当代码出现bug时,可以还原到历史指定的版本。

  4. 对比版本差异。使用git diff工具进行比较文件差异

Github是什么

Github是基于Git的在线托管平台,现在是微软旗下公司。Github为开发者们的Git仓库提供托管服务,同时提供社区功能,让用户们可关注其他用户、点赞项目(star)、下载克隆项目(clone)、对项目代码提出改进建议(fork)。

Github使用

进入Github官网(https://github.com),选择Sign in(注册),完成个人信息注册后登录。

(tips: 1. Github与国内连接不稳定,有时需使用科学上网工具,2. 个人信息中username请谨慎填写<可能会陪伴你很久……>)

Github仓库创建:1

登录Github账户后,选择页面左上角菜单栏,然后选择 New (新建仓库)。

可参考:

建立仓库

选择填写以下内容:
建立仓库时的配置

(tips:README文件一般用来让用户介绍该仓库的内容与使用方法)

接着选择右下角Create repository(创建仓库)。

Git与Github绑定

创建仓库后,需要自己本地的电脑克隆一个库,方便将代码同步到Github上的仓库,为此需要先安装git工具。

在Linux上安装 git:

如果在ubuntu或者Debian系统:

1
sudo apt-get install git

如果在CentOS或者Fedora系统:

1
sudo yum install git

在Windows上,则需要使用Git fow windows软件,来帮助用户使用命令行工具(Git Bash)来执行Windows应用于访问Winodows文件。此外,Git Bash还拥有着使用SSH进行远程操作的能力。

在Git官网(https://git-scm.com/downloads
下载对应的Windows版本,依次点击下一步,最后弹出页面:

恭喜你安装完成!

页面中两项请打钩。

接着会打开一个Git Bash终端

GitBash窗口

Git常用命令:

一些常用命令

获取到Git工具后,需要完成Git与Github的绑定

获取ssh密钥

关于ssh的介绍可以参考:https://ivel-li.github.io/2024/11/25/ssh概述

在windows中刚打开的Git Bash中(或者linux的终端中):

1
cd ~/.ssh

如返回 “no such file or directory” 表明电脑没有ssh key,需要创建ssh key;

在终端输入

1
ssh-keygen -t rsa -C "在这里输入你的GitHub账户"

连续三次回车(Enter)后,终端会显示:1

你的ssh key保存位置

找到上述路径的文件夹,里面存有ssh的两个密钥,id_rsa为私钥,id_rsa.pub为公钥。

用记事本打开id_rsa.pub文件,复制里面的全部内容。

你的ssh密钥

与Github仓库绑定

找到你的个人账户的Settings,接着进入SSH and GPG keys项目。

你的ssh密钥

选择新建密钥。

新建ssh密钥

填写title并粘贴刚才复制的公钥,接着点击下方的Add SSH key,完成绑定。

输入密钥

在Git bash中输入

1
ssh -T git@github.com

接着输入yes,如果输出如下则表示绑定成功。

输入密钥

接下来是简单的配置环节:

gitname最好与github账户名字相同,git邮箱有github账户的邮箱。

1
2
git config --global user.name “gitname”
git config --global user.email “git邮箱”

至此,完成与Github仓库的绑定。

Github简单使用

  1. Git clone:将库克隆到本地,方便上传代码。

进入自己刚才新建的库的我也,点击 <> Code,复制网址。

输入密钥

在本地选择的存储位置,右键选择Git Bash Here:

本地目录下打开GitBash

输入:

1
git clone 刚才复制的网址

接着发现本地选择的目录下已经保存了我们的库。

  1. 上传代码

我们尝试在本地新建一个For_test.txt文件,并上传到Github仓库,并为其添加备注

输入密钥

可以观测到github上我们的文件已经上传成功:

Github上传成功

Git push常用语法为:

1
git push <远程仓库名> <分支名>

此处的远程仓库名是什么,分支是什么

分支一般为协同开发时不同开发者分别上传的内容,在初始化Git仓库时,Git默认已创建一个master(也叫main)的分支。

远程仓库一般直接写为origin,直接代表本地文件夹相对应的远程仓库

  1. 拉取文件

常用命令为

1
git pull <远程仓库名> <分支名>

从远程仓库获取代码并合并本地版本。

1
git pull origin master:brantest

将远程仓库origin的master分支拉取过来,与本地的brandtest分支合并。

参考:

- [1] https://blog.csdn.net/black_sneak/article/details/139600633
- [2] [https://blog.csdn.net/Long_xu/article/details/139724137]

ssh概述

SSH 简述

SSH是一种网络协议,用于计算机间的加密登录。在1995年,芬兰学者Tatu Ylonen设计了SSH协议,加密登录信息;该协议终结了互联网间明文通信,被截获后内容暴露无遗的现象,迅速在全球推广,成为Linux系统的标准配置。

ssh原理如下:1

ssh原理图

SSH 常见用法

1
ssh -p 22 user@host

-p 22:指定port(端口)为22。(一般默认端口为22)

关于ip的介绍,可以参考

user: 登录的用户名。

host:登录的主机的ip。可参考 https://ivel-li.github.io/2024/11/25/IP是什么/

参考:

- [1] https://blog.csdn.net/wang_qiu_hao/article/details/127902007

神经网络初始化问题

log:

2024/11/22:部分图片

1
2
3
4
import numpy
from numpy import random
import matplotlib.pyplot as plt
import math

初始化权重预实验:

【输入】常见做法:将输入值缩放到均值为0,标准差为1的正态分布中。

【权值】初始化:若采用相同的标准正态分布N(0,1)N(0,1)

1
2
3
4
5
x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=random.normal(loc=0, scale=1, size=(512,512))#a对应每层神经网络中的W:512*512
x=a@x
x.mean(), x.std()###向量.mean()输出所有元素的均值;.std()输出所有元素标准差
(7.943177084708433e+133, 2.849247295078971e+135)

如上可观察到,在100层的神经网络中,对初值和权重取N(0,1)N(0,1)分布,最终输出极大。

1
2
3
4
5
x=random.normal(loc=0, scale=0.01, size=512)#均值为0,方差为0.01的正态分布,512个数据
for i in range(100):
a=random.normal(loc=0, scale=0.01, size=(512,512))
x=a@x
x.mean(), x.std()
(3.0804500580946894e-69, 2.214484543669681e-67)

如上可观察到,在100层的神经网络中,对初值和权重取N(0,0.01)N(0,0.01)分布,最终输出极小。(ps: 在取N(0,0.1)N(0,0.1)时输出也很大)

即:初始化权重过大过小均会影响学习效果

1

上式中y[i]表达式可描述一层神经元的映射关系

1
2
3
4
5
6
7
8
9
mean, var=0.,0.
#i的大小与神经网络层数无关,意义只在于i够大时结果更精确
for i in range(1000):
x=random.normal(loc=0, scale=1, size=(512))
a=random.normal(loc=0, scale=1, size=(512,512))
y=a@x
mean+=y.mean().item()#item()函数取值比索引取值精度更高
var+=numpy.multiply(y,y).mean().item()
mean/1000,math.sqrt(var/1000)
(-0.03692881605069143, 22.68767143694423)
1
math.sqrt(512)
22.627416997969522

此时我们注意到Y的标准差Σjmmean(yi2)m\frac{\Sigma_j^m mean(y_i^2)}{m}【y平均值为0】, 其逼近x维数(神经元个数)的平方根。

1
2
3
4
5
6
7
8
9
#它教会了我如何计算均值为0的一组数据的标准差(通过D(X)=E(X^2))。
mean, var=0.,0.
for i in range(1000):
x=random.normal(loc=0, scale=1, size=(512))
a=random.normal(loc=0, scale=1, size=(512,512))
y=a@x
mean+=y.mean().item()#item()函数取值比索引取值精度更高(不知道为什么)
var+=numpy.multiply(y,y).mean().item()
mean/1000,math.sqrt(var/1000)
(-0.02495151144918943, 22.6154885506345)

一些数学推导:

方差D(X)=E[XE(X)]2D(X)=E([X-E(X)]^2)

D(XY)=E([XYE(XY)]2)D(XY)=E([XY-E(XY)]^2)

又当X,Y相互独立时,有E(XY)=E(X)E(Y)E(XY)=E(X)E(Y)

故有D(XY)=E(X2Y2+E2(XY)2XYE(XY))D(XY)=E(X^2Y^2+E^2(XY)-2XYE(XY))

当X,Y满足正态分布N(0,1)N(0,1),有D(XY)=E(X2Y2)=E(X2)E(Y2)=D(X)D(Y)D(XY)=E(X^2Y^2)=E(X^2)E(Y^2)=D(X)D(Y)

D(X+Y)=E[X+YE(X+Y)]2=E[X+Y]2)=E(X2)+E(Y2)+2E(XY)=E(X2)+E(Y2)=D(X)+D(Y)D(X+Y)=E([X+Y-E(X+Y)]^2)=E([X+Y]^2)=E(X^2)+E(Y^2)+2E(XY)=E(X^2)+E(Y^2)=D(X)+D(Y)

有了上述结论,对N(0,1)N(0,1)分布的X,YX,Y而言,有D(XY)=D(X)D(Y),D(X+Y)=D(X)+D(Y)D(XY)=D(X)D(Y),D(X+Y)=D(X)+D(Y)

因此上述代码中,Y的每个元素方差为512。

上面代码块中var中右式实际上为EY2)E(Y^2),当i迭代次数够大求和平均等价于D(Y)D(Y)

1
2
3
4
5
6
7
8
mean, var=0.,0.
for i in range(1000):
x=random.normal(loc=0, scale=1, size=(512))
a=random.normal(loc=0, scale=1, size=(512,512))*math.sqrt(1./512)#D(cX)=c^2 D(X)
y=a@x
mean+=y.mean().item()
var+=numpy.multiply(y,y).mean().item()
mean/1000,math.sqrt(var/1000)
(-0.0009292457617292619, 0.9998508585319182)

由上可见,把N(0,1)N(0,1)的初始化权值缩放至1/n1/\sqrt{n}后输出层不再出现爆炸。

1
2
3
4
5
x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=random.normal(loc=0, scale=1, size=(512,512))*math.sqrt(1./512)
x=a@x
x.mean(), x.std()
(-0.013509481923589781, 0.669791021961652)

由上可见,把N(0,1)N(0,1)的初始化权值缩放至1/n1/\sqrt{n}后输出层(均值和方差)不再出现爆炸。即便在100层后亦是如此。

当我们考虑激活函数

饱和激活函数F:limx>F>0lim_{x->\infty}F'->0

非饱和激活函数F:limx>F不趋于0lim_{x->\infty}F'不趋于0

典型的饱和函数有Sigmoid,Tanh函数。

不满足饱和函数条件的函数则称为非饱和激活函数

ReLU及其变体则是“非饱和激活函数”。

使用“非饱和激活函数”的优势在于两点:

  1. "非饱和激活函数”能解决所谓的“梯度消失”问题。

  2. 它能加快收敛速度。

Xavier初始化(适用于饱和激活函数)

image

1
2
3
4
5
x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=random.normal(loc=0, scale=1, size=(512,512))*math.sqrt(1./512)
x=numpy.array([math.tanh(c)for c in a@x])
x.mean(), x.std()
(0.0008083661917762789, 0.05757114952184833)

可以观察到,在一百层神经网络后,x的均值与标准差值仍处于有效范围,激活未完全消失。

1
2
3
4
5
x=numpy.random.uniform(low=-1,high=1,size=512)#均值为0,方差为1/3的均匀分布(从-1到1取值),512个数据
for i in range(100):
a=random.normal(loc=0, scale=1, size=(512,512))*math.sqrt(1./512)
x=numpy.array([math.tanh(c)for c in a@x])
x.mean(), x.std()
(0.0013361727805947863, 0.0572257326796936)
1
2
3
4
5
6
x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
#权值取均值为0,方差为1/3的均匀分布(从-1到1取值),512*512个数据*1/n
a=numpy.random.uniform(low=-1,high=1,size=(512,512))*math.sqrt(1./512)
x=numpy.array([math.tanh(c)for c in a@x])
x.mean(), x.std()
(6.753492971342052e-26, 1.2087455023485755e-24)

注意到两种算法区别在于,上述x的1/3方差只出现一次,大量迭代后不会对后续结果造成影响,后者令a的权值初始化为1/3,a会出现100次,导致了最终激活梯度无穷小。

1
2
3
4
5
6
x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
#权值取均值为0,方差为1的均匀分布(从-sqrt3到sqrt3取值),512*512个数据*1/n
a=numpy.random.uniform(low=-1.73205081,high=1.73205081,size=(512,512))*math.sqrt(1./512)
x=numpy.array([math.tanh(c)for c in a@x])
x.mean(), x.std()
(0.0018487857398481779, 0.05713394734193917)

如上所示,当a取均匀分布但方差为1时,100次迭代后x的均值与标准差值仍处于有效范围,激活未完全消失。

因此笔者猜想,或者核心在于使a@x(或者说 WTXW^TX )的方差为1

对于Xavier初始化方法,其将每层的权重设置为:

±6ni+ni+1\pm\frac{\sqrt{6}}{\sqrt{n_i+n_{i+1}}}

上式中nin_i为传入该层的连接的数量(扇入),ni+1n_{i+1}为传出该层的连接的数量,也被称为(扇出)。

1
2
3
4
def xavier(m,h):
return numpy.random.uniform(low=-1,high=1,size=(m,h))*math.sqrt(6./(m+h))
def xbvier(m,h):
return numpy.random.normal(loc=0, scale=1, size=(m,h))*math.sqrt(6./(m+h))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#xavier初始化
x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=xavier(512,512)
x=numpy.array([math.tanh(c)for c in a@x])
print(x.mean(), x.std())
#xavier初始化,但是权重并非U(-1,1)而是N(0,1)分布
x1=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=xbvier(512,512)
x1=numpy.array([math.tanh(c)for c in a@x1])
print(x1.mean(), x1.std())
#凑方差为1初始化,权重U(-1,1)分布
x2=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=numpy.random.uniform(low=-1,high=1,size=(512,512))*math.sqrt(3./512)
x2=numpy.array([math.tanh(c)for c in a@x2])
print(x2.mean(), x2.std())

#以下为无激活函数的情况
#xavier初始化
x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=xavier(512,512)
x=a@x
print(x.mean(), x.std())
#xavier初始化,但是权重并非U(-1,1)而是N(0,1)分布
x1=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=xbvier(512,512)
x1=a@x
print(x1.mean(), x1.std())
#凑方差为1初始化,权重U(-1,1)分布
x2=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=numpy.random.uniform(low=-1,high=1,size=(512,512))*math.sqrt(3./512)
x2=numpy.array([math.tanh(c)for c in a@x2])
print(x2.mean(), x2.std())
-0.0011354134828090012 0.06322324251617281
-0.04347840333790784 0.6898032256032508
-0.0025500434343385574 0.04863383928260507
-0.016140350889364705 0.7301846105693588
-0.08559922112658865 1.341156944469779
0.000651830089541752 0.09598924410829489

此时对于权值初始化U(1,1)U(-1,1)的情况,Xavier初始化表现良好。
数学上很简单,此时±6ni+ni+1=±325122\pm\frac{\sqrt{6}}{\sqrt{n_i+n_{i+1}}}=\pm\frac{\sqrt{3*2}}{\sqrt{512*2}},即3512\frac{\sqrt{3}}{\sqrt{512}},等价于令方差为1/3,均值为0,从(-1,1)均匀取值,512个数据的X对应的a@x(或者说 𝑊𝑇𝑋 )的方差为1。

以上内容对有无激活函数情况下:xavier初始化对权重U(-1,1)分布、N(0,1)分布、凑方差1初始化进行了比较。

小结

  1. 有激活函数时方差传递效果比无激活函数时较弱,推测原因在于tanhx这一激活函数本身特性限制了方差传递
  2. xavier初始化对权重N(0,1)分布相较于U(-1,1)分布,理论上每层方差变化分别为3、1,结果上确实可以观测到最终权重N(0,1)分布引出的x1的方差较大,但并未在100层传递下出现爆炸(10000层也不会)。
    方差凑1法仍需继续讨论

Xavier方法的适用条件:

  1. 权重U(-1,1)分布。
    2.激活函数为饱和激活函数。
  2. 网络层为前馈全连接神经网络层。(应该吧)

image

为了说明这一点,Glorot和Bengio证明,使用Xavier初始化的网络在CIFAR-10图像分类任务上实现了更快的收敛速度和更高的准确性。

Kaiming初始化(he初始化)

从概念上讲,当使用关于0对称且在[-1,1]内部有输出(如softsign和tanh)的激活函数时,我们希望每个层的激活输出的平均值为0,平均标准偏差为1,这是有意义的,这也正是Xavier所支持的。

但是如果我们使用ReLU激活函数呢?以同样的方式缩放随机初始权重值是否仍然有意义?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def relu(x):
for i in range(len(x)):
temp=numpy.array([x[i],0])
x[i]=temp.max()
return x

mean, var=0.,0.
for i in range(1000):
x=random.normal(loc=0, scale=1, size=(512))
a=random.normal(loc=0, scale=1, size=(512,512))
y=relu(a@x)
mean+=y.mean().item()
var+=numpy.multiply(y,y).mean().item()
mean/1000,math.sqrt(var/1000)
(9.048598009365435, 16.032879062442056)
1
math.sqrt(512/2)
16.0

我们注意到,RELU激活时y的标准差可直接计算,D(Y)=5122D(Y)=\frac{\sqrt{512}}{\sqrt{2}},即激活函数RELU将使标准差缩小12\frac{1}{\sqrt{2}}

1
2
3
4
5
6
7
8
mean, var=0.,0.
for i in range(1000):
x=random.normal(loc=0, scale=1, size=(512))
a=random.normal(loc=0, scale=1, size=(512,512))*math.sqrt(2/512)
y=relu(a@x)
mean+=y.mean().item()
var+=numpy.multiply(y,y).mean().item()
mean/1000,math.sqrt(var/1000)

使每个初始化权重乘2512\frac{\sqrt{2}}{\sqrt{512}},保持激活层标准差在1左右。

image

个人理解:n\sqrt{n}代表正态分布权值矩阵维度的影响,2\sqrt{2}代表Relu激活函数的影响

1
2
3
4
5
6
7
### 表示扇入数
def kaiming(m,h):
return numpy.random.normal(loc=0,scale=1,size=(m,h))*math.sqrt(2./h)
def kaiming1(m,h):
return numpy.random.normal(loc=0,scale=2,size=(m,h))*math.sqrt(2./h)
def kaiming2(m,h):
return numpy.random.uniform(low=-1,high=1,size=(m,h))*math.sqrt(2./h)
1
2
3
4
5
x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=kaiming(512,512)
x=relu(a@x)
print(x.mean(), x.std())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=xavier(512,512)
x=relu(a@x)
print(x.mean(), x.std())

x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=kaiming1(512,512)
x=relu(a@x)
print(x.mean(), x.std())


x=random.normal(loc=0, scale=1, size=512)#均值为0,方差为1的正态分布,512个数据
for i in range(100):
a=kaiming2(512,512)
x=relu(a@x)
print(x.mean(), x.std())

可以观察到,对于ReLU激活函数,使用Xavier初始化将导致激活输出在100层几乎消失。

对于不能使初始权重方差为1的情况,如U(1,1)N(0,2)U(-1,1)、N(0,2),如此方法表现并不佳,即方差凑1依旧十分重要

image

Kaiming方法的适用条件:

  1. 权重N(0,1)分布(方差为1)。
    2.激活函数为非饱和激活函数ReLU。
  2. 网络层为前馈全连接神经网络层。(应该吧)

总结

  1. 核心:方差凑1

  2. 激活函数(复杂的激活函数无法计算):

  2.1 tanh()&Sigmoid() => Xavier初始化方法:权重U(-1,1)分布6ni+ni+1*\frac{\sqrt{6}}{\sqrt{n_i+n_{i+1}}}

  2.2 ReLU() => kaiming初始化方法:权重N(-1,1)分布2n*\frac{\sqrt{2}}{\sqrt{n}}

此外上述方法理论上在前馈全连接层较为适用

实用语法:

  1. x.item():取出较x直接索引、精度更高的值
  2. numpy.random.normal(loc=0,scale=1,size=(m,h)): m*h矩阵, 元素为N(0,1)正态分布。
  3. numpy.random.uniform(low=-1,high=1,size=(m,h)): m*h矩阵, 元素为U(-1,1)均匀分布。