The path twists, and the future is uncertain.

作者: twisted Page 2 of 3

我们去2020

有段时间没写这种文章了。根据姜文电影的名场面,我决定将博客的这个栏目命名为——「正经人谁写日记呀」,哈哈。

2019年像之前的每一年一样,过得很快。对它的印象只剩加班加班、辞职家里蹲、旅行,然后,就是现在:手机在放歌,我坐在床上,敲打老旧的 MacBook,现在是 12.31 02:02。

又是不太长进的一年。原本没有值得写的东西。但今年有两个瞬间让我想写点什么:
一是前女友在微信上再次联络我之后,我觉得人和人也许是可以相互原谅的,心里稍微好受一点,当时感激地有写东西的冲动。
二是前几天,见了一些老朋友。看到大家都过得很好,也许说不上很好但也是「稳步前行」。似乎让我对自己也稍稍有了一点信心。

也谈我的前东家 (一)

毕业后我在两个公司工作过,其中一家是台湾的电子制造公司LiteOn,第二家是华为。

这是一篇LiteOn领导拜托我写给他们介绍华为工程师生活的文章。我把它贴出来,作为对前东家讨论系列的第一篇,之后可能还会有别的分析文章。

a < b < c 表达式在各种编程语言中的不同「表达」

表妹在大一的C语言课上写了个bug程序,发到群里让大家帮忙debug。我一眼看出其中存在一处“语法”错误:

if (0 < a < 10) {
…
}

一个很有意思的事是,我把它当成了“语法错误”,认为这样写根本编译不过。

但事实是,从之后的讨论中看到,这个程序没有编译失败。只是逻辑上有错而已——这个if后面的表达式永远为true

我用gcc编译了一遍试试,报了这个warning:

warning: comparisons like ‘X<=Y<=Z’ do not have their mathematical meaning [-Wparentheses]
if (0 < a < 10) {

意味着我们一开始的判断是对的,0 < a < 10 确实不是正确的C语言写法,应该写作(0 < a) && (a < 10)。但编译器还是允许它通过了。程序运行时真正发生的是什么呢?:

(0 < a) < 10

相当于先执行了括号内的运算,返回 truefalse。在C语言中 true == 1false == 0。这两个值再去与10做比较。——当然是恒 <10 的,所以 (0 < a < 10) 这个表达式恒为 true,相关 if 语句永远不会走进 else 分支。

这方面的语法差异还挺有趣的,这篇文章里说,如果你没学过C,你可能会以为a < b < c就是a < b < c,如果你学过C,你会以为这里无法编译通过。

关于 a < b < c 的事实是

  • 在 Python 里,a < b < c 的意思就是 a < b < c
  • C语言里,编译能过,但有告警 comparisons like ‘X<=Y<=Z’ do not have their mathematical meaning
  • C++里,表现与C中相同,但还有个额外的告警,警告你在这个表达式中发生了布尔型与整型间的隐式转换
  • Haskell里,这里会发生类型错误,因为 bool 和 int 之间没有隐式转换
  • Fortran里,这是语法错误,因为 < 符号没有关联性(non-associative (meaning operations cannot be chained, often because the output type is incompatible with the input types))。

经验总结

在编译时开启所有编译告警,并尽可能地将它们清零,是一个好的习惯。
当然,也不是一概而论的。取决于你的项目性质,某些告警(未使用的函数、未使用的变量)还是可以选择关闭的。最好是开启所有告警,然后明确声明关闭特定的某几个告警;而不是直接关闭所有告警。

【转】哈希碰撞与生日攻击

最近在思考关于哈希表二次探测再散列后如何查找的问题。暂时没找到解释。先转一篇有趣的科普文,以供后续研究。

原文来自: http://www.ruanyifeng.com/blog/2018/09/hash-collision-and-birthday-attack.html

一、哈希碰撞是什么?

所谓哈希(hash),就是将不同的输入映射成独一无二的、固定长度的值(又称”哈希值”)。它是最常见的软件运算之一。

如果不同的输入得到了同一个哈希值,就发生了”哈希碰撞”(collision)。

举例来说,很多网络服务会使用哈希函数,产生一个 token,标识用户的身份和权限。


AFGG2piXh0ht6dmXUxqv4nA1PU120r0yMAQhuc13i8

上面这个字符串就是一个哈希值。如果两个不同的用户,得到了同样的 token,就发生了哈希碰撞。服务器将把这两个用户视为同一个人,这意味着,用户 B 可以读取和更改用户 A 的信息,这无疑带来了很大的安全隐患。

黑客攻击的一种方法,就是设法制造”哈希碰撞”,然后入侵系统,窃取信息。

二、如何防止哈希碰撞?

防止哈希碰撞的最有效方法,就是扩大哈希值的取值空间。

16个二进制位的哈希值,产生碰撞的可能性是 65536 分之一。也就是说,如果有65537个用户,就一定会产生碰撞。哈希值的长度扩大到32个二进制位,碰撞的可能性就会下降到 4,294,967,296 分之一。

更长的哈希值意味着更大的存储空间、更多的计算,将影响性能和成本。开发者必须做出抉择,在安全与成本之间找到平衡。

下面就介绍,如何在满足安全要求的前提下,找出哈希值的最短长度。

三、生日攻击

哈希碰撞的概率取决于两个因素(假设哈希函数是可靠的,每个值的生成概率都相同)。

  • 取值空间的大小(即哈希值的长度)
  • 整个生命周期中,哈希值的计算次数

这个问题在数学上早有原型,叫做”生日问题“(birthday problem):一个班级需要有多少人,才能保证每个同学的生日都不一样?

答案很出人意料。如果至少两个同学生日相同的概率不超过5%,那么这个班只能有7个人。事实上,一个23人的班级有50%的概率,至少两个同学生日相同;50人班级有97%的概率,70人的班级则是99.9%的概率(计算方法见后文)。

这意味着,如果哈希值的取值空间是365,只要计算23个哈希值,就有50%的可能产生碰撞。也就是说,哈希碰撞的可能性,远比想象的高。实际上,有一个近似的公式。

上面公式可以算出,50% 的哈希碰撞概率所需要的计算次数,N 表示哈希的取值空间。生日问题的 N 就是365,算出来是 23.9。这个公式告诉我们,哈希碰撞所需耗费的计算次数,跟取值空间的平方根是一个数量级。

这种利用哈希空间不足够大,而制造碰撞的攻击方法,就被称为生日攻击(birthday attack)。

四、数学推导

这一节给出生日攻击的数学推导。

至少两个人生日相同的概率,可以先算出所有人生日互不相同的概率,再用 1 减去这个概率。

我们把这个问题设想成,每个人排队依次进入一个房间。第一个进入房间的人,与房间里已有的人(0人),生日都不相同的概率是365/365;第二个进入房间的人,生日独一无二的概率是364/365;第三个人是363/365,以此类推。

因此,所有人的生日都不相同的概率,就是下面的公式。

上面公式的 n 表示进入房间的人数。可以看出,进入房间的人越多,生日互不相同的概率就越小。

这个公式可以推导成下面的形式。

那么,至少有两个人生日相同的概率,就是 1 减去上面的公式。

五、哈希碰撞的公式

上面的公式,可以进一步推导成一般性的、便于计算的形式。

根据泰勒公式,指数函数 ex 可以用多项式展开。

如果 x 是一个极小的值,那么上面的公式近似等于下面的形式。

现在把生日问题的1/365代入。

因此,生日问题的概率公式,变成下面这样。

假设 d 为取值空间(生日问题里是 365),就得到了一般化公式。

上面就是哈希碰撞概率的公式。

六、应用

上面的公式写成函数。


const calculate = (d, n) => {
  const exponent = (-n * (n - 1)) / (2 * d)
  return 1 - Math.E ** exponent;
}

calculate(365, 23) // 0.5000017521827107
calculate(365, 50) // 0.9651312540863107
calculate(365, 70) // 0.9986618113807388

一般来说,哈希值由大小写字母和阿拉伯数字构成,一共62个字符(10 + 26 + 26)。如果哈希值只有三个字符的长度(比如abc),取值空间就是 62 ^ 3 = 238,328,那么10000次计算导致的哈希碰撞概率是100%。


calculate(62 ** 3, 10000) // 1

哈希值的长度增加到5个字符(比如abcde),碰撞的概率就下降到5.3%。


calculate(62 ** 5, 10000) // 0.05310946204730993

现在有一家公司,它的 API 每秒会收到100万个请求,每个请求都会生成一个哈希值,假定这个 API 会使用10年。那么,大约一共会计算300万亿次哈希。能够接受的哈希碰撞概率是1000亿分之一(即每天发生一次哈希碰撞),请问哈希字符串最少需要多少个字符?

根据上面的公式倒推,就会知道哈希值的最短长度是22个字符(比如BwQ1W6soXkA1PU120r0yMA),计算过程略。

22个字符的哈希值,就能保证300万亿次计算里面,只有1000亿分之一的概率发生碰撞。常用的 SHA256 哈希函数产生的是64个字符的哈希值,每个字符的取值范围是0~9和a~f,发生碰撞的概率还要低得多。

七、参考链接

给网站添加https安全证书

众所周知,Chrome的68版会将未使用 https 加密的网站标记为「不安全」。可以说 Google 是在不遗余力地推广传输层安全协议。 响应 Google 的号召。当我得知 https 证书也有一些免费版本可用时,就决定给自己的博客加上 https。 略过各种方案的比较和选择,这里直接记录(我认为)个人博客网站添加 https 证书的最佳实践:

服务器添加安全证书

我参考了这篇:Let’s Encrypt 给网站加 HTTPS 完全指南 但你不用按那篇文章里的步骤来,因为它已经是2年前的文章了,现在可是8102年,事情变得很简单: 只需在服务器上安装 certbot, 它自动帮你完成一系列的证书配置和验证操作。(需要对服务器有足够权限) 网址在:https://certbot.eff.org/ 我选择了 Apache on Ubuntu 16.04 (xenial) ,页面的提示如下: 不一定跟我一样,选择你自己的操作系统服务器软件,certbot 页面上会提供对应的安装方法。 安装后继续按照 certbot 页面的说明 get start ,程序的交互式界面会引导你进行选择和配置。 跟随引导进行操作,完成后网站就被 Let’s Encrypt 提供的 SSL/TLS 证书保护起来了。是不是很有安全感!
(完成证书配置后,certbot 会提示你到 https://www.ssllabs.com/ssltest/index.html 网站去检测 SSL web server 的安全性。最终的评价等级其实取决于你的证书授权机构、它使用的加密协议、能被哪些平台兼容… 作为安全通信方面的小白,我其实不太清楚怎么去确认自己的网站足够安全。这个检测至少提供了一些有效参考信息。) 由于 Let’s Encrypt 提供的免费证书只有3个月有效期,每次到期前需要 renew。certbot 支持自动 renew。 关于 renew 的 config ,你能在 /etc/letsencrypt/renew/ 路径下找到。用下面这个命令测试自己的 renew 设置是否有效:
sudo certbot renew --dry-run

一点清扫工作

现在你应该能通过前缀 https:// 的网址来访问自己的网站了。再回头试一下 http:// 访问,看会不会自动跳转到 https:// 。(如果没有,就要检查网站的重定向设置了) 不过,你也许会遇到,其他的 https 网站在浏览器里都有一把小绿锁显示,为什么我的网站只有小灰锁呢? 在浏览器的小锁(或灰色叹号)上点击,可以看到它对网站潜在隐患的提示。 对于个人博客来说,问题通常出现在站内的图片和链接上。它们的地址可能不是 https 的,没有被 SSL/TLS 保护,所以会被中间人截获。
参考这篇这篇文章,可以编辑当前 WordPress 主题下的 function.php 文件,强制将站内图片的地址转换为 https : 在 function.php 文件的尾部添加以下代码(修改其中两处 YOUR_DOMAIN 为你自己的域名)
/* 替换图片链接为 https */
function my_content_manipulator($content){
    if( is_ssl() ){
        $content = str_replace('http://www.YOUR_DOMAIN.com/wp-content/uploads', 'https://www.YOUR_DOMAIN.com/wp-content/uploads', $content);
    }
    return $content;
}
add_filter('the_content', 'my_content_manipulator');
若还有错,参照这篇文章,利用浏览器的功能来排查错误。 找到错误原因后:如果是通向站外的链接,直接把它们改成 https 就行了(现在几乎所有正经网站都支持https了)。 如果是 WordPress 插件的问题,找找插件设置里能否修改相关地址。插件不支持的话,就看你愿不愿意忍痛寻找替代品了。

Python Tkinter 进阶实践

之前简单介绍过Tkinter:Python 可视化图形界面简单实践
这次来记点进阶心得。


Tkinter实现标签页效果

用 Python 和 Tkinter 写过一些小工具。现在想把它们整合到一起,以类似“标签页”的形式来切换。

具体参考:Tkinter 8.5 reference: a GUI for Python
用到的是 Notebook 控件,这方面网上比较少提,所以记录一下。

举例:

MyNotebook1 = Notebook(top)
MyNotebook1.place(relx=0.022, rely=0.062, relwidth=0.956, relheight=0.876)

之后你就可以将 Notebook 作为 parent 来构建 Frame,承载你的标签页:

Tab1 = Frame(MyNotebook1)
....
MyNotebook1.add(Tab1, text='First tab name')

与普通的 Frame 不同,完成构建的 Tab1 不是通过 pack、grid、place 来布局,而是用 Notebook 的 add 方法,并指定标签名。

再来一个例子:

#about tab
self.TabX = Frame(self.Notebook1)
self.TabXLbl = Label(self.TabX, text="Listen's Swiss Army knife")
self.TabXLbl.pack()
self.TabXLb2 = Label(self.TabX, text="ver 1.3.0")
self.TabXLb2.pack()
self.Notebook1.add(self.TabX, text='About')
#about tab end

上面这个标签页实际效果大致是:

简单来说,你只要先建立了 Notebook 控件,然后往里面 add Frame 就行了,每个 Frame 都会呈现为1个独立的标签页,名字在 add 时指定。


GUI 界面与逻辑分离

如果只是一个功能简单的小工具,随便怎么写都没问题。但我在把多个小工具整合成一套时发现,东西一多了会很难管理。
在网上看到别人的做法,是将界面和逻辑分离开:

class Application_ui(Frame):
#这个类仅实现界面生成功能,具体事件处理代码在子类Application中。
def __init__(self, master=None):
Frame.__init__(self, master)
self.master.title("Listen's Victorinox")
self.master.geometry('880x380')
self.createWidgets()

def createWidgets(self):
....

class Application(Application_ui):
#这个类实现具体的事件处理回调函数。界面生成代码在Application_ui中。
def __init__(self, master=None):
Application_ui.__init__(self, master)
....

上面的做法是从 TK 继承了 Frame,然后重写成自己的 UI (Application_ui)。这个 Application_ui 只生成界面,之后再用 Application 类去继承它,在 Application 里面进行逻辑处理。

单独调试 UI :

if __name__ == "__main__":
top = Tk()
Application_ui(top).mainloop()

这样的好处是,在设计 UI 时,可以只考虑控件和布局。若画面呈现不如预期,就可以直接调整。

而 UI 里的每个控件其实都是空的,它们不实现任何功能,功能都在 Application 类里去添加。
例如:

self.Tab1varWL = StringVar()
self.Tab1varWL.set("WL")
self.TabStrip1__Tab1WLNum['textvariable'] = self.Tab1varWL
self.TabStrip1__Tab1WLNum.bind("Return",self.WL2Page)

self.TabStrip1__Tab1WLNum 这个控件之前是空的,现在我给它设置了 StringVar() ,并且绑定了一个事件是 Return,这个控件收到回车键时会触发 self.WL2Page 方法。
另一个 tab 的实例:
 # Tab4 var
self.Tab4path = StringVar()
self.Tab4pathEntry['textvariable'] = self.Tab4path
self.Tab4ButtonSelect['command'] = self.Tab4selectPath
self.Tab4ButtonConfirm['command'] = self.Tab4ReadFile

相比于把 GUI 各种按钮的函数摆得到处都是,现在则可以按tab分割,都放在 Application 类的私有方法里。

对这部分进行调试:

if __name__ == "__main__":
top = Tk()
Application(top).mainloop()

开发时,可以先在 Application_ui 类里面调试界面,完成后,再到 Application 类给控件们分配变量、绑定方法,验证逻辑功能。
两部分工作分割开,这样就清晰很多。制作复杂界面的 GUI 程序时,界面和逻辑的分离很有必要。


将编译好的程序打包成安装程序来发布

我在上一篇Python 可视化图形界面简单实践里介绍过,用 pyinstaller 简单快速地打包 python 程序,生成不依赖 python 的可执行文件 exe。

当时有说 -F 参数是将程序打包成一个单独的exe文件,不加则会生成一整个目录的文件。 同时也因为速度原因不建议添加这个参数。
可是程序发布时,这么多零散的文件是很不方便的,你只能将目录压缩成zip、发给别人,让别人自己去找里面的 .exe 文件。

一个合理的发布方式是提供安装文件,收到的人执行安装文件就能完成部署。
这里我们用到的工具是 NSIS(Nullsoft Scriptable Install System)。具体步骤参照这篇教程,写的很详细,我就不搬过来了:程序打包成exe文件

原则上任何语言的程序都可以这样发布。NSIS用压缩算法把你的程序那一大堆文件压成一个包,套了个安装向导的壳。
用户拿到后一运行,安装向导像所有程序一样,引导他们选择安装路径等等,然后把压缩包解压到用户路径,再为这个路径创建桌面/开始菜单的快捷方式。

NSIS 可以提供不同语言的安装向导,你甚至可以给它添加奇怪的用户协议~


这篇文章涉及的内容都是我在开发 Victorinox 小工具时遇到的。这是一把全世界只对我1个人有用的瑞士军刀。。。。。。
源码已push到GitHub:https://github.com/MamaShip/Victorinox

Git Flow实践

工作上一直使用git作为代码版本管理工具,但从来没掌握好它。最近我负责的这部分代码管理越来越混乱,掌握一套正确成熟的git工作流程几乎是迫在眉睫的事情。

git 的基础使用就不赘述了。大概每个程序员都会花几个小时掌握 git 的基础操作,再花几周去实践和熟悉吧。
首先推荐两篇文章:

如果你没有遇到过多人协作下代码管理混乱、想要 release 新版时却交不出一份能 work 的代码,读这两篇文章可能不会很有 sense。但你只要遇过一次版本危机,再硬着头皮连续加班到凌晨一两点,回头来看就会很有感受。

上面两篇文章其实讲了2件重要的事:

  • 根据你的项目性质来决定工作流(git flow / github flow / gitlab flow)
  • 在一个固定模式下,git如何规范地工作


由于我在一家制造业企业,RD 只需隔一段时间 Release 一个 FW 版本给产线即可。比起敏捷地迭代,版本的稳定性更重要。传统的 git flow 是适合我们的。

在 git flow 框架下,有2个长期分支:

  • 主分支master
  • 开发分支develop

master名为master,却并不是日常工作的基础分支,而是一个经过验证的稳定分支。随时可以用来发布。
与master平行存在,内容几乎相同但又略有超前的,就是develop分支。平日的开发必须基于develop分支进行。在git flow框架下,当一个新的需求产生时,你基于最新的develop拉出一个 feature/xxx 分支,进行开发,功能完成后,再合并回 develop。(合并后 feature/xxx 分支自然消亡)

3种短期分支(完成开发后,合并进长期分支,然后删除):

  • 功能分支(feature branch)
  • 补丁分支(hotfix branch)
  • 预发分支(release branch)

feature 只能从 develop 拉出,然后合并进 develop。
hotfix 只能从 master 拉出,然后合并进 master 和 develop。
release 则是从 develop 拉出,进行测试,完成验证后合并进 master。

当 git flow 进行 release 时,它实际做了2件事:把从上一次 release 至今的 develop 改动经过验证后合入 master,再反向把 master 合并回 develop 确保两边同步。
为什么要反向 merge 一遍,因为 release 过程中若测试出一些问题,就可以直接在 release/xxx 分支上进行修改,而这个改动是没有发生在 develop 上的。所以 release 分支在删除时会保证这些改动也被同步到develop。

在这样的设计下,master 是稳定的,它受到 release 规则的保护,除非发生 hotfix,就只有经过测验的 release 分支才能合进 master。
而 develop 分支则相对比较敏捷,根据需求拉出 feature branch 后,开发者可以专注于自己的子任务,把兼容性等全局测试交给 release 分支去负责。

使用 git flow 相关工具,可以帮助规范这个流程。例如,工具会禁止你基于 master 拉出一个新的 feature 分支,因为新 feature 总是应该基于 develop 分支来开发,若要紧急修补某个bug,你应该使用 hotfix。
一些git flow工具(如gitflow-toolkit),还可以规范你的commit,确保commit信息清晰有效,能被script解析。这样你就能使用现成的工具(git-chglogconventional-changelog),自动生成change log。这在 release 正式版本的时候是很有用的:


以上是我理解的 git flow 实践。在应用中发现它也对使用者提出了一些要求:

  • 无论是否使用工具辅助,所有人都需要按规范格式进行commit(参考: Angular 社区规范
  • 每一项逻辑上独立的改动,应归属于一次单独的commit
    (不要在一个feature分支下进行多项任务的开发;若有多次commit都是关于同一个修改项目,应先squash再合进develop;尽量让每个 commit 都是一项独立、完整的改动,可以方便debug/release时进行cherry-pick和rebase)
  • git flow tool 大多是基于 Linux 系统。win下的开发者需要自我约束。

git flow 仍在实践中学习。有新的理解会再更新。

为什么神经网络对我有用

我对机器学习的兴趣完全是出于好奇。但神经网络在这方面不太吸引我,也许是因为高中生物已经学过很多。
但机器学习里的神经网络——尤其深度神经网络——对我来说是「有用」的。这里我将介绍它在哲学层面给我的一点启发。(其中一些想法未必是对的,仅作记录)

世界是基于通感的

是的,结论就是,世界是基于通感的。我先把结论放在这里。
这种说法会很confusing。因为从唯物主义的角度来说,外部世界是「客观存在」。但当我们说「世界是基于通感的」,指的是:你所认知的世界。
也就是说,在你的心里,在你的概念里,在你的思想里的这个世界。是基于通感而存在的。

让我们从头开始。
首先确保我们知道什么是感知。
生物学上的每一个神经元,在机器学习里都被用一种类似的运算节点来模拟。这种模拟神经元的运算节点,被称为感知机(perceptron)。无数个感知机交织在一起,搭建出了神经网络——这是我们对生物的模拟。
生物所能获得的感知是什么?——基于各种器官与外部交互时获得的信号:眼睛获取光学信号,耳膜获取声音,皮肤上的感受器获取触觉和温度,位听神经获取平衡感……
这是我们对外界的感知。:器官收集客观的物理化学信息,将其处理成能在生物体内传输的信号。——就目前所知,这种信号类似于一种电信号,即使它们在突触之间也许是利用化学方式传播。

那意识又是什么。
《失控》这本书,有一种很打动我的讨论:意识依赖于感知。
很多人以为把大脑单独拿出来,它里面仍然保存了这个人的意识(缸中之脑)。但KK认为,是你的躯体接收到的这些「感知」赋予了你意识。
1954年,一群加拿大心理学家做了个很有趣的实验。他们搭建了一间避光隔音的实验室,志愿者们呆在这个狭小房间里,头上戴着半透明的防护眼镜,手臂裹着纸板,手上戴着棉手套,耳朵里塞着耳机(里面持续播放低沉的噪音),在床上静躺2-3天。
他们起初还能听到持续的嗡嗡声,不久就融入一片死寂。只看得到暗淡的灰色,与生俱来的五色百感渐渐蒸发殆尽。各种意识挣脱身体的羁绊开始旋转。
半数的受测者都产生了幻觉,声称进入一种“醒时梦”的状态。由于志愿者们的描述太不可靠,有一位研究员以受测者的身份进行了观察:“现实感没了,体像变了,说话困难,尘封的往事历历在目,满脑子性欲,思维迟钝,梦境复杂,以及由忧虑和惊恐引起的目眩神迷”。
在这个与世隔绝的寂静棺材里呆了两天后,几乎所有被试都没有了正常的思维。注意力土崩瓦解,取而代之的是虚幻丛生的白日梦。

这个实验如何解释?我不是在写一本教科书,所以无法给出详细的论证。个人看法:人的神经网络是高度复杂交错构建的。其中不可避免的存在大量的循环。如果没有外部的强刺激来触发某些响应机制,这个网络就会自我运转至一种疯狂状态——所有存在正反馈的环都会无限循环地放大其中信号。你脑海深处产生的一点点小想法,都有可能被强化成一场巨大的无法停止的风暴。

身体是意识乃至生命停泊的港湾,是阻止意识被自酿的风暴吞噬的机器。神经线路天生就有玩火自焚的倾向。如果放任不管,不让它直接连接“外部世界”,聪明的网络就会把自己的构想当作现实。
……
而身体,或者说任何由感觉和催化剂汇集起来的实体,通过加载需要立即处理的紧急事务,打断了神智的胡思乱想!

这个观点对我的启发非常强。Everything you feel is shaping you。你以为是「大脑」在思考,但其实你的所有器官也都在参与「思考」。它们采集的信息不只是被「采集」而已,这些信息是意识运转的基础,它们进入感受器时就已经组成了你的一部分意识。
——你之所以知道自己能控制自己的手指,是因为你驱动它时能感受到相应的触觉、你的眼睛能捕捉到它运动。——若没有这些信息反馈,这一套行动就无法形成闭环——你仅靠想象来决定自己的动作,你的想象就会最终演变为风暴而丧失其应有的意义,因为你的逻辑系统是孤立地运转的。

深度神经网络继续推进我对这个话题的认知,它让我几乎可以从哲学层面来看待人类的「运行」。
『殊途同归』。当初Geoffrey Hinton坚持认为神经网络是才是机器学习的正确方向。他坚持要把自己那篇论文发在Nature上,但在那个年代,所有人都觉得神经网络这个主意已经没救了。他想到其中一个审稿人是Stuart Sutherland,一位很有名的心理学家,就跑去给他解释机器学习里的神经网络以及反向传播算法。这位心理学家 was shocked,因为他发现这群计算机科学家用不同的方法验证了与他们对脑神经科学的研究相似的结论(关于概念如何传播和组织、它们在大脑内的图形结构…)。

这个故事想指出的是,计算机科学家往往从工程的角度来思考如何实现对一些东西的计算,但它们的努力有时同样反映出生物进化的方向。当他们的深度神经网络模型越来越完善的时候,这种模拟,很有可能就是在越来越逼近生物学上的「现实」。

好了,该来具体谈谈机器学习里的神经网络了。如果学习它,你会了解到些什么?

我不列举所有知识。只举对我来说重要的。

至少目前,卷积神经网络在图像识别领域已经获得超出人类的正确率。这证明图像识别应用中的一些技术是「先进」的。
来看:计算机科学家已经在探究深度神经网络工作的「原理」,在图像识别上,输入的图像传给网络第一层时,第一层的所有感知机都只处理简单特征:某些区域的直线、斜线、纯色…
第二层开始,感知机接受前一层传来的处理结果并将它们抽象和组合,这时候它们开始能理解类似:圆形、倒角、长直线……
第三层也许能将信息组织得更抽象,你能看到一些感知机似乎在找鼻子,另一些在找眼睛……(如果你是在训练一个人脸识别的网络)
再往后,后面的层级就会越来越抽象,处理的信息达到人类无法理解的维度。

抱歉我没有找到恰当的配图,我在课上看到过,但当时没有存,现在一时找不到了。总的来说就是:神经网络的浅层如我们预期一样在处理输入信号,将其翻译为一些「有意义」的特征;但随着深度的增加,越靠后的网络层越倾向于把信息往更高维度、更综合的方向抽象,最终形成一个「结果」向外部输出。

它当然跟人脑不像,因为人脑不是这样线性的「输入-输出」模型。但我愿意相信的是:在人脑最重要的核心区域,神经元全部基于高度抽象的“概念”进行运算,你收到的信息,无差别地传入网络(它们也许被预处理过,如视神经已经把光学信号进行了转译),被一套通用的模型处理,输出一些相对稳定的结果(变成你的想法和行动)。

大脑的不同区域看起来是分工的。但它们的功能如此「通用」,以至于你给它输入别的信号它也能慢慢学会处理。为了验证所有大脑神经都是用的同一个「算法」,有人把小鼠的听觉输入切断,然后给负责听觉的大脑皮层接入视觉感官,让动物的auditory cortex学会了看东西……

然后他们给盲人做了个头戴式摄像头,采集低清图像转化成灰度图像,将每个像素点的灰度值转换成不同电压传到盲人舌头上。盲人就靠舌头获得了视觉……

这意味着,讯号是可以跨界产生影响的。就像「辣」其实是一种痛觉,但你吃东西的时候会把它判断为这道菜的「味道」。人体是如此复杂的存在,它既是由大脑统控的集中式管理方案,又是器官各自运转的分布式处理系统。你所看到的东西在被视神经收集到的瞬间,视神经已经开始对它做处理。信息层层传递,传到大脑皮层的已经是一些足够抽象的概念——它们甚至已经不必与「视觉」有关。
一束信号如果在传递中串了线,就有可能从「视觉频道」跳到「听觉频道」,当这个概念错误地被听觉大脑皮层处理时,严重时就会让你产生幻听,不严重时…也许你脑内就莫名地产生了一丝忧伤、或者一丝欢愉。

讯号是可以跨界产生影响的,即所谓的“通感”。如果我们认为大脑内有一个核心区域是所有意识交汇的「总成」。就能解释文艺作品中,以下技术有助于推动情绪的原因:环境描写、画面色调、背景音乐……
它们在某种程度上给你传递了“通用”的感受,因为这些不同形式的信号在输入网络的时候都“触动”了一些共同的关键节点。于是你可以从某些颜色、某些旋律、某些文字中感受到近似的悲伤。

刚才提到Geoffrey Hinton和他的反向传播算法,「反向传播算法」指的是,神经网络在进行一次运算之后,会根据输出的结果正确与否接收外界的「惩罚」,这个惩罚会被反向传播回整个路径,使得每个感知机自我修正(下次我就不会再这样了!)。

神经网络是在不断“学习”中变化的。当你被某段感情伤害时,状况就类似于机器学习模型算出了错误的答案,身体的某种机制会去“惩罚”这个错误答案。从而,让整个网络里的每个神经元自我调整。它们产生一种“抑制”,会去回溯在这段感情里你的所有行为和情绪,控制情绪的、控制运动的每个神经元都会根据权重抑制自身。这个“学习”过程让神经网络产生变化:即使跟同一个人发生一段全新的恋情,你的感受和行为也会不一样了,因为你身体的每一个神经元都在学着活得更“正确”。

世界是基于通感的

回到这个结论上来,我认为,从出生起(甚至出生前起),人的感知就在塑造自己(的意识)。气候冷暖在塑造你,你看到的事物在塑造你,你受到的夸奖和委屈也在塑造你。
当你在谈论审美时,你真正指的是什么?
(如果你认同「Everything you feel is shaping you」)也许「审美」只是你从小到大所有感知的加权总和——所有这些感知中让你感到「舒服」的部分,构成了你的审美。

人的大脑是在高度抽象的层面被「训练」的。它不仅仅是学会了一件「具体」的事,而且会跨界产生影响。
就像你学习知识时会用「类比」这种方法。如果所有抽象概念都是在核心区域被处理,那么它学到的经验就可以在毫不相干(却又隐含相同抽象规律)的领域间迁移。即使那些抽象规律并不真正适用于所有领域,若假设人脑的运算能力总是不足的,It’s underfitting,它会用并不普适的经验去处理未知事件就一点也不奇怪了。——就像你生命里的每一个「第一次」尝试。(而且你会在错误尝试后得到「惩罚」,这促使了一次「反向传播」,从而「训练」了你的神经网络,从这个方面来说,你就「学习」了。)

也许你从小生活在局促的空间里,家教很严、做事处世处处受制,长大后想问题的格局也很容易「小器」。

也许你谈恋爱从未失败过,只要努力,想追的女孩就总是能追到手,后来你成了一支基金的经理,潜意识里觉得这种「投资->回报」模型跟追女孩很像,你的投资风格就会异常奔放自信,自以为每次操作都能带来预期中的高收益。


神经网络对我是有用的。我借助它来认识自己。

如果你把人体比做一套高度复杂的神经网络系统,我的建议是:试着了解自己的“网络特性”。——这似乎是与基因有关、取决于成长环境和历程的巨大模型。
然后基于特性去做能够改善网络自身的选择和决定。

以我自己为例:
我认为我的大脑是学习率较高、对错误的punishment超强的类型,同时我会倾向于过早泛化。 这意味着我对未知情况的试探会比较快速和大胆,但一旦得到坏的结果,就会获得很强的负反馈,使之前的所有相关行为都被严重“否定”,这使我很不愿意去做完全没接触过的事情。同时我有很强的倾向去给事物做“抽象”,寻求它们的内在一致性,以尽量总结出可以覆盖更多实例的简单经验。

那我会如何修正自己的选择和决定呢?
——我仍然会鼓励自己去尝试未知,而不是靠既有经验去推断。
——在一些重要的体验发生时,我不会去强化它(让刺激的更刺激),我会去做一些差异化,让正在发生的这件事带上一些不同的特征。这样我在下次遇到类似情况时,就能够因为「差异」而跳出之前的经验,不至于落入一种错误的响应中而丢失宝贵体验。
——不过如果我想活得感性一点的话,在某次重要事件发生时,我会究极强化我的体验,超速飙车、做爱、吸毒一条龙,全方位分泌所有激素去促进神经网络「深刻记住」这一刻。

对 ML 中 Normal Equation 的理解

机器学习中的回归问题,类似于以往各种工程上的优化问题。定义一个 Cost Function:

(1)   \begin{equation*}  J(\theta) = \frac{1}{2m}\sum_{i=1}^m(h_\theta(x^i)-y^i)^2 \end{equation*}

对这个函数求解最优化问题。

由于J是一个关于\theta的函数,所以目标是求一组最优的\theta,来使J取得最小值。

一个符合直觉的做法是Gradient Descent。也就是迭代地用 J\theta 求导,顺着梯度最大的方向移动,直到收敛于最低点。

另一个方法则是Normal Equation,对我来说这是一个非常强的公式,可以把Gradient Descent花了那么多次迭代才解决的事用一次运算就搞定:

(2)   \begin{equation*}  best \theta = (\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top \overrightarrow{y} \end{equation*}

这里面的\mathbf{X}是一个矩阵,\overrightarrow{y}是列向量,\theta其实也是列向量。
\mathbf{X}的每一行是一个样本,第i行其实就是 x_i^1, x_i^2, x_i^3, \dots 这样的一组特征(feature),其中的每个值都可以视作特征空间里的一个维度。
对于第i个样本,它对应的(标准)结果就是 y_i,总共m个样本结果组成了向量\overrightarrow{y}
(回顾一下Cost Function:损失函数其实是定义了我们的Hypothesis h_\theta(\mathbf{X})与真实结果\overrightarrow{y}之间的距离。以此衡量训练结果。)
再补充Hypothesis:

(3)   \begin{equation*}  h_\theta(x) = \theta^\top \overrightarrow{x} = \theta_1 x_1 + \theta_2 x_2 + \dots + \theta_n x_n \end{equation*}

Normal Equation 的思路类似于中学数学里求极值:对某个方程求导,然后让式子等于 0,等于 0 的点即为极值点。
但为什么上述思路能被(2)这样一个式子实现呢。


这个问题是我去年初遇到的。当时就想把阳哥哥给我的解答记录下来。拖了这么久,终于给Wordpress装了LaTeX插件,所以来记录一下:

(2)的推导方法有两种,第一种是 cost function J (1)对 \theta 这个向量的每一个分量求偏导

    \[ \begin{cases} \frac{\partial J}{\partial \theta_1} = \frac{1}{m} (h_\theta(x^1)-y^1) x_1\\ \frac{\partial J}{\partial \theta_2} = \frac{1}{m} (h_\theta(x^2)-y^2) x_2\\ \dots \\ \frac{\partial J}{\partial \theta_n} = \frac{1}{m} (h_\theta(x^n)-y^n) x_n \end{cases} \]

假设 \theta 是个n维向量,就得到了n个式子,令这n个式子都等于0,我们就得到了一个线性方程组:

    \[ \begin{cases} \frac{1}{m} (h_\theta(x^1)-y^1) x_1 = 0\\ \frac{1}{m} (h_\theta(x^2)-y^2) x_2 = 0\\ \dots \\ \frac{1}{m} (h_\theta(x^n)-y^n) x_n = 0 \end{cases} \]

这个线性方程组可以写成矩阵相乘的形式,即

(4)   \begin{equation*}  \mathbf{X}^\top\mathbf{X}\theta = \mathbf{X}^\top \overrightarrow{y} \end{equation*}

(4)这个式子就是所谓的Normal Equation。它跟式(2)是等价的。


第二种方法要稍微高级一点儿,写出来会简洁得多。就是直接把cost function写成矩阵的形式,即

    \[ J = (\overrightarrow{y} - \mathbf{X}\theta)^\top (\overrightarrow{y} - \mathbf{X}\theta) \]

然后直接以这个式子对向量 \theta 求导,也会直接得出normal equation。
至于如何直接对向量求导,你可以参考wikipedia的matrix calculus。其实本质上对向量求导就是一种记号,和对向量的每个分量求导没的本质区别。


再回头看一次这个好用的Normal Equation:

    \[ best \theta = (\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top \overrightarrow{y} \]

对于有 n 个特征维度的 \mathbf{X} 来说,用 Normal Equation 求 \theta 的复杂度是 O(n^3),而 Gradient Descent 的复杂度为 O(kn^2)
所以,当特征维度不多时,都可以用 Normal Equation 快速求解。当 features n > 10000 时,才开始考虑使用 Gradient Descent 。

但是,这个公式显然也不是能够无条件使用的。你可以看到其中有 (\mathbf{X}^\top\mathbf{X})^{-1} ,意味着括号内的矩阵必须是可逆的。
真的是这样吗?——老师说,此处的矩阵并不必须是可逆的。在代码中,对这个矩阵的求逆直接用pinv()函数来做(也就是求伪逆、广义逆)。阳哥哥说,使用广义逆来算 \theta(\mathbf{X}^\top\mathbf{X}) 不可逆时的一种传统的处理方式,可以用的原因是广义逆得出的 \theta 符合一些良好的统计性质。(具体参考:高惠璇的《统计计算》)
不过, (\mathbf{X}^\top\mathbf{X}) 不可逆往往意味着数据模型有问题(Features数量比样本数量还大,或是非常强的collinearity),遇到不可逆的情况还是返回去检查模型比较好。

Python 可视化图形界面简单实践

最近工作上常用的一个操作需要进行二进制数bit翻转。由于不知道windows系统计算器就有这个功能,自己用python写了一个。简单记录下实践过程。

Python进行GUI编程,可用的库并不多。功能也不算强大,这篇文章简单列举了几个库的特点。
我呢,没有被它们任何一个吸引。所以直截了当地选择了Python标准库自带的Tkinter
( Python Tkinter 官方文档:https://docs.python.org/2/library/tkinter.html

完成品大致是这样的:

基本框架是:

from Tkinter import *
top = Tk()
top.title("Listen's Option Checker")
# Your code here
top.mainloop()

在这个基础的窗口里,用Frame控件分割空间(类似于html和CSS那种方式)实现布局。
然后在各个分区的frame里,根据与用户的交互方式,选择对应的控件:Button、Entry、Label……

控件由类似这样的句子生成:

Confirm_Button = Button(frm2Sub1,text="确认",command=hex2bin)
Confirm_Button.pack(side=LEFT)

第一句创建了一个Button实例,这个按钮的父容器是frm2Sub1,按钮上文字为「确认」,当用户按下这个按钮时,就会立即触发一个名为hex2bin的函数,来对用户的操作进行响应。
现在命名空间内的Confirm_Button指向了这个Button实例。用.pack()方法来完成对它的部署。参数side=LEFT是要让它在所属Frame容器内靠左放置,若不指定位置,每个新的控件都会被部署在前一个控件下方。

当然,包括Frame在内的所有控件都需要通过.pack()来呈现。如果漏了这一句,程序界面内就看不到对应控件。

去查一下Tkinter支持控件列表,再配合以上基本信息,已经足以制作一些功能极其简单的可视化程序界面。其实网络上大多数介绍文章也没有讲的更多。
但还有一些值得注意的地方。

Command传参

Button控件的command参数,是不允许传参的
我理解此处的command参数就类似于一个函数指针,指向目标函数。
网上比较常见的传参方法是用lambda函数

Button(frm, textvariable=strvar31, command = lambda : FlipBit(31,strvar31))

它其实是定义了一个匿名函数,在Button被按下时执行的是lambda,再由lambda将参数交给你的执行函数。类似这样的用法,在只有一两个同类控件的时候是可行的。也很方便。

但对我这个程序来说,需要32个按钮充当32位二进制bit,不可能手动复制32个button的创建过程。
这里的Lambda函数,如果用在for循环创建的button内,就会导致它传进去的参数固定为for循环的控制变量i(的最大值,也就是最后一次循环的值)。
原因也很简单,lambda函数在未被执行时并不会被解释(编译成固定的函数)。它只有被真正执行时才生效。而真正执行时是Button按下时,不是For循环当初loop经过它时。所以i始终为定值(终值)。

最终的办法是用一个Event Handler。定义如下两个function:

def handler(bit):
num[bit] = num[bit] ^ 1
strvarlist[bit].set(str(num[bit]))
binary = "".join(map(str,num))
decimal = int(binary,2)
var16.set(hex(decimal))

def handlerAdaptor(fun,v):
return lambda event, fun=fun, v=v: fun(v)

由handlerAdaptor作中介,将参数传进去。方法是将handler与鼠标点击event绑定。
绑定鼠标事件的Button写法如下:

ButtonList.append(Button(SubSubFrameList[i], textvariable=strvarlist[i]))
ButtonList[i].bind('', handlerAdaptor(handler, i))
ButtonList[i].pack()

这里跟前面创建控件的方式不同,有一个小变化:
为了使我们这32个bit的控件(包括button和其上的textvariable)可以被方便地遍历,我不再是给它们赋予一个固定的命名,而是将每个实例直接.append()到它们各自的list里
这样我在另外的函数里处理它们时,可以直接用下标来控制和选择。——包括这里创建完成后的.bind()和.pack(),也都是用list加下标来做选择。


一些参考资料:
tkinter官方文档学习笔记
Tkinter 控件简单计算器示例
Python Tkinter参考资料之(通用控件属性)
tkinter模块常用参数(python3)
Python Tkinter GUI(三)显示图片
Python GUI进阶(ttk)—让界面变得更美


打包exe程序


上表来自《Python程序打包成exe可执行文件》,文章很有用。我此前用过的py2exe不好用,这次选择pyinstaller,简单打包方法:

pyinstaller -F -w main.py

生成文件在dist目录。build仅为中间文件。

-F 是打包成一个单独的exe文件,不加则会生成一整个目录的文件。(加了该参数会导致程序调用外部image失败,在spec配置内添加DATA也没用)

-w 不需要查看命令行时,用这个参数隐藏cmd画面。

亲测-F之后程序启动速度慢了几十倍不止。所以我不建议-F

更复杂的配置,需要使用脚本相同目录下,由pyinstaller生成的一个.spec文件。我没有深入研究,需要时可以在网上查。(参考:pyinstaller打包工具的使用说明将自己的python程序打包成.exe/.app)

Page 2 of 3

Powered by WordPress & Theme by Anders Norén