从作为预测的模型到作为概率分布的模型 (Model: From Prediction to Distribution)

作为机器学习的实践者,自己对于模型的理解就是 Predict:从一组输入信号来计算目标变量,也就是: (1)   但是在机器学习的理论学习中,我常常看到教材上提到模型也描述了概率分布,并从这个角度有很多理论上的推导,比如在分类模型中,Maximum Likelihood Estimation 和 Cross Entropy 是等价的,再比如如果假定观测误差符合正态分布的话,那么Maximum Likelihood Estimation 和 Mean Square Error 在回归模型中也是等价的,等等。在我看来,如果我们已经有了m个训练数据: (2)   那么训练数据的概率分布已经可以使用计数的方法(也就是 Empirical Distribution)直接计算了,为什么还需要一个模型呢? 直到最近,我才对于这个问题有了一些更深刻的认识:所谓模型描述了概率分布,指的是在给定输入条件 的情况下,模型给出预测值 ,以及确认该值是 的概率。也就是下面的条件概率密度函数: (3)   对于分类模型,上面的描述比较好理解:假定我们有 C 个 class,那么在最后一层网络中一般有 C 个输出,经过 Softmax 归一化处理以后,我们把 就当做当前用例属于每一类的概率,这确实是一个概率分布。只不过在实践中我们需要取出概率最大的一个作为模型的输出值(Predict a Class)而已。 对于回归模型,上面的描述的意义就不是那么明显了,下面一段分析引用自 Maximum Likelihood Estimation 和 Mean Square Error: 假定我们根据实验观察,得到了大量的训练数据。因为总是有不可知因素能够影响 的最终取值,但是却没有被我们包含在 当中,不难想象,在这种情况下,我们会发现某些训练数据的输入信号部分是完全一样的,但是 却不一样。同时我们的模型只能够预测一个值,这个时候模型应该预测那一个值呢?如果我们把模型看做对于目标数据的概率密度函数,那么对于和模型预测值相同的那些值,它们的条件概率是1吗?和模型预测值不同的那些值,它们的条件概率是0吗? 对于模型应该预测的值,训练数据的平均值是一个明显正确的选择。但是后面两个问题,我们不应该采用非0即1的答案。统计学指出,在上面的情况下,我们应当假定在相同的输入信号下所有的目标变量的概率分布符合正态分布(Normal Distribution),其分布的中心就是所有目标变量的平均值,这个值也是我们的模型的预测值(Predict)。但是对于和这个预测值相同的值,其概率并不是1,和这个预测值不同的值,其概率也不是0,而是符合正态分布。在这个前提下,我们就可以说模型描述了给定了条件 , 的概率分布: (4)   […]

不用f前缀但是像fstring一样Print (fstring print without f-prefix)

Python 早期的Print语句受到 C 的直接影响,在把变量嵌入到输出的字符串时采用了占位符语法。使用这种打印语句,程序员需要在脑子里把所有的变量处理两次,一次是它们在字符串中的位置,一次是它们的具体名称。这样处理实际上比较繁琐而且容易出错的: name = ‘Bob’ age = ’60’ print(‘%s is % years old.’ % (name, age)) 新的f-string 语法可以直接把变量嵌入到字符串中,算是朝正确的方向前进了一大步: print(f'{name} is {age} years old.’) 现在的问题是,我们总是要加上 f 这个恼人的前缀,特别是如果输出的消息是多行的话,每一行都要写上这个前缀,更是麻烦。我琢磨了很久,终于搞明白怎样在正常的字符串中嵌入变量名处理了,真是不错: def Print(message): import inspect frame = inspect.stack()[1][0] print(message.format_map({**frame.f_globals, **frame.f_locals})) name = ‘Bob’ age = ’60’ Print(‘{name} is {age} years old. – Print magic’) 长久以来的一个心病终于解决了。

C++: Variadic Template and Fold Expression are Powerful.

这两天做一个Pipeline的Framework,见识了现在C++ Generic Programming 的强大功能,比原来的 Object Oriented Programming 看上去 Cool 多了:完全没有关系的一堆类型,一样可以放在框架里操作,写出来的程序也没有任何效率的损失,看来这些年C++又进步了很多。下面简要说说我的设计。 Class BaseData 表示 Pipeline 中的一个数据节点。 Class Phase 表示针对数据的某个操作。 Phase 接受一些 BaseData 作为输入,并输出到另外一些BaseData。为了灵活和简单起见,Phase 不设任何 Virtual Method 作为接口,只要就实现一个 Run Method 即可。程序概要如下: class BaseData { public: get() … set() … }; class DataA : public BaseData {…}; class DataB : public BaseData {…}; class DataC : public BaseData {…}; […]

C++ 的一些新感想

因为在工作中C++用的比较多,听说 C++ 17 最近又引入了很多新特性,就买了几本 C++ 的书回来看。看了一阵,感慨颇多,罗列如下: C++ 在 Library 开发人员和普通使用人员之间的鸿沟是越来越大了。新增加的很多特性都是给Library作者提供的,普通用户平时根本用不到。特别的,我以前总是以为所谓 Library 就是在语言之上提供一些包装好的功能模块,现在看到标准库中很多 Type Traits (比如 is_trivially_destructible) 都需要编译器的特殊支持才能实现,远远地超出了我以前的理解范畴。一般的 C++ 用户,如果打开标准库里面的程序想要研究一下某个特性是如何实现的,多半和天书一样看不懂。 C++ 现在的救命稻草就是运行时刻的性能了,这集中体现在C++为程序员提供的内存控制上。为此 C++ 几乎牺牲了其他可能的前进方向,并且不惜在语言上增加了巨大的复杂性。C++ 的创始人 Bjarne Stroustrup 前些年还提要在 C++ 中引入自动内存管理,现在是绝口不提了,估计搞出来也没有人用;为了更有效的支持大型对象,C++ 搞出来 Rvalue Reference 和 Move Constructor,让我这样的 C++ 的老鸟程序员也感到太过复杂;更不用提新的 std::string_view,本身大概唯一的作用就是在大字符串上搞一些子串操作比较有效率,结果强行塞进标准库,连 C++ 的资深作者 Nicolai M. Josuttis 都看不下去,在他的书中大声疾呼:string_view is considered harmful! 即便如此追求性能,看着 Programming Language Popularity Trend 上面 C++ 日渐下滑的曲线,我估计日后 […]

Vim Window Management (Vim 多窗口管理)

我一直使用Vim作为编辑器。现在大屏幕显示器越来越流行,使用Vim的时候也越来越多地同时用到多个窗口。为了能够更有效地利用多个窗口,我琢磨着写了一些脚本来更好地在多个窗口之间切换,下面把我学到的知识说一说。 首先我不会写 Vim Script,所以我的脚本都是用 Python写的,不过 Vim的 Python Interface 也很方便,就把脚本写在一个单独的 window.py 文件中,然后在 vimrc 里面加上: py3file window.py 就可以了。 要想有效的管理窗口,首先需要能够了解当前的窗口分布情况。Vim 的 Python Interface 提供了一个 vim.windows 对象,不过更好的办法是调用 winlayout 这个函数。因为 Vim 中所有的窗口都是从一个主窗口水平或者垂直切分得到的,所以所有的窗口可以表示为一棵递归二叉树。遍历这棵树就可以得到所有窗口的信息,包括其编号、大小,当前位置,父窗口以及其中显示的数据。具体的调用方法如下: winlayout = vim.eval(‘winlayout()’) 第二个非常有用的命令是 wincmd。Vim提供的绝大部分窗口管理命令都从快捷键 CTRL+W 开始,但是这个快捷键在脚本中使用起来并不方便,这个时候就可以使用wincmd。比如我们要把输入焦点切换到左面的窗口,可以使用: vim.command(‘wincmd l’) 最后就是创建新窗口和关闭当前窗口的命令,这方面Vim提供了非常繁多的命令,但是对于脚本作者来讲最实用的也就是下面几个: 关闭当前窗口:vim.command(‘close’) 关闭所有其他窗口,只留下当前窗口:vim.command(‘only’) 垂直切分在右面打开新窗口:vim.command(‘rightbelow vnew’) 垂直切分在左面打开新窗口:vim.command(‘leftabove vnew’) 水平切分在下面打开新窗口:vim.command(‘rightbelow new’) 水平切分在上面打开新窗口:vim.command(‘leftabove new’) 有了这些命令以后,我们就可以编写脚本来控制Vim的多个窗口了。我自己写了一个脚本用于管理两个Side by Side 放置的窗口,再加上一些水平切分的支持,结果挺方便的,以后有机会放到 Github 上吧。

等位基因的组合数目

假定某种形状(比如豌豆花的颜色)由一对等位基因控制,其中显性基因用字母表示,隐性基因用字母表示。显而易见,我们一共有三种可能的基因型:,分别对应红色、粉色和白色。如果某个等位基因一共有种可能的变异,即: (1)   那么一共有多少种可能的基因型呢(注意基因型 和 是一样的)? 这个问题看上去很简单,但是我自己却迷惑了很长一段时间。最后我终于认识到:在阅读 William Feller 老爷爷的概率书的这段时间里,自己在经过了相当多的训练之后,已经习惯性的去考虑样本空间内每一个样本的概率是相等的情形了,从这一点讲,那么很明显我们一共有 种基因组合的结果。但是问题是这些组合虽然每一个出现的概率相等,但是其中却有一些重复;等到我们把重复的合并以后,剩下的样本出现的概率却不相等了。具体来讲,纯合的基因型: (2)   要比杂合的基因型 (3)   出现的概率要低。具体到豌豆花的例子,也就是白色和红色出现的概率分别是 25%,而粉色出现的概率是50%。在领悟到这一点之后,前面给出的题目就很容易计算了:当某个等位基因一共有种可能的变异的时候,我们可以把所有的可能的组合排成一个 的表格,其中对角线上的基因型是纯合的,一共有个,而非对角线上的基因型是杂合的,一共有 种。考虑到杂合的基因型每种都会出现两次,那么所有不同的基因型一共有: (4)   这和William Feller 老爷爷书中给出的答案完全一致。

排列组合的新思路

排列就是从一个Population 中有序的采样(Sample),组合就是从一个Population中 无序的采样。无论哪种采样,基本的排列组合公式 和 都假定Population 中的每一个对象是可以区分的。但是对于Population 中存在不可区分对象的情况(比如 Sampling with Replacement),我们就需要新的思路。我自己对于这个关键的区别一直没有很深刻的理解,直到最近读 William Feller 老爷爷的概率书,才有了一些进展,下面来说一说。 假定一个长度为 的二进制字符串,其中有个1的字符串有多少? 这个问题中,Population就是 0 和 1,但是它们可以重复出现在最终的采样中,因此简单的排列组合思路并不适用。解决这个问题有两种传统的思路: 思路一:假定我们在这 个字符串中 挑选 个 1,然后剩下的全部是0,那么可供挑选的总共方案是: (1)   思路二:假定所有的0和1都是可以区分的,那么所有总共可能的方案就是 种,其中有 个1 和 个0。如果这个1互相可以区分,那么它们总共有 的不同的排列方式,相应的对于这些可以区分的0我们也有 种不同的排列方式。现在这些0和1都变成不可区分的了,所以这些不同的排列方式就变成了一种,那么长度为 的二进制字符串,其中有个1的字符串就有: (2)   这正好就是思路一中的组合数公式,因此两种思路的结果是一样的。 上面的第二种思路讲述起来要比第一种长很多,但是我反而觉得更容易理解,其主要原因是因为它和组合数的公式推导的思路本身就是一致的。组合基本问题:从 个可以互相区分的对象中,取出个对象的不同组合(也就是不考虑这个对象的顺序)有多少?很明显如果我们考虑这个对象的顺序,不同的顺序代表不同的组合的话,那么我们一共有: (3)   种不同的方案,这是一个基本的排列问题。现在考虑 个不同的对象有多少种不同的排列方式,很明显这也是个基本的排列问题,答案是 种。现在我们看到,如果相同的个对象的不同排列方式只能算作同一种组合的话,那么 3 对于所有可能的组合就高估了 倍,因此从 个可以互相区分的对象中,取出个对象的不同组合(也就是不考虑这个对象的顺序)就有: (4)   沿着这个新思路,我们可以解决一些新的排列组合题目,下面是一些例子。 假定有面不同的旗帜,要挂在 个旗杆上,一共有多少悬挂的方法? 很明显第一面旗帜有 种悬挂的方法,注意到有旗帜的那个旗杆现在如果我们要在上面再挂新的旗帜的话,有上面和下面两种选择,所以第二面旗帜有 […]

家里有两个男孩的概率是多大?

这道概率的题目我以前在知乎上看见人讨论过,当时的背景是量子力学中概率问题。最近又在 William Feller 老爷爷的书中看到了这道题目。经典的概率已经很难了,量子力学中的概率就更是违反直觉。但是无论如何,我还是反对很多人故弄玄虚,把本来很清楚的问题搞得越来越糊涂,相比之下,老爷爷的书讲得很清楚。下面讲一讲这道问题和我的理解。 考虑所有有两个孩子的家庭,假定我们已经知道家里至少有一个男孩,那么这个家里有两个男孩的概率是多大? 如果每个孩子是男是女的概率相等,那么两个孩子的家庭一共有以下四种可能的基本情况: 男孩,男孩 男孩,女孩 女孩,男孩 女孩,女孩 这四种基本情况的概率是相等的,都是 0.25,也就是说家里有两个男孩的概率是0.25。现在我们给出了条件:家里至少有一个男孩。这个条件过滤掉了两个女孩这种基本情况,只剩下三种基本情况,因此家里有两个男孩的概率变为。用概率论的术语来描述的话,两个孩子都是男孩这个事件(A)的概率是: (1)   家里至少有一个男孩这句话给出了一个条件(Hypothesis,H),这个条件(或者假设)发生的概率是: (2)   题目所问的问题实际上就是一个条件概率:在家里至少有一个男孩这个条件(H)之下,家里有两个男孩的这个事件(A)发生的概率是多少?这个概率可以用条件概率公式表达如下: (3)   顺便说一下,对于条件概率公式,我觉得下面的表达方式更自然一些: (4)   我个人常常把它读作:事件A和H同时发生的概率,就是事件H发生的概率 乘以 当事件H发生时事件A将会发生的条件概率。这样的表达式也很容易拓展到多个事件的情况,比如下面: (5)   现在回到我们的条件概率公式 (3)。 我们已经知道是 0.75,但是 是多少呢?考虑事件 AH的具体含义,它表示“两个孩子都是男孩”而且“至少有一个男孩”这两个事件的交集(即同时发生)。参考上面枚举所有情况,很明显这种事件在所有四个事件中只发生了一次,因此 。把这个数据代入上式,我们就可以计算得出在家里至少有一个男孩这个条件(H)之下,家里有两个男孩的这个事件(A)发生的概率是: (6)   和我们直觉推理的结果一致。 下面有意思的事情来了,前面的问题可以被改写为下面的形式:假定你有一个朋友,你知道她有两个小孩,其中有一个是男孩。你可以猜测她的家里两个孩子都是男孩的概率是,这个结果正如我们前面分析的一样。下面假定有一天,你在公园散步 ,遇到你的这位朋友带着一位男孩也在散步,而这个男孩正是你这位朋友的小孩。现在我们要问一个新问题,你这位朋友家里两个孩子都是男孩的概率是多少? 这个新问题可以按照下面的简单思路来回答:你没有看见的那个孩子是男是女的概率是相等的,都是0.5,如果那个没有看见的孩子是男孩,你的朋友就有两个男孩,如果那个没有看见的孩子是女孩,你的朋友就有一男一女。所以对于这个新问题,你这位朋友家里两个孩子都是男孩的概率是0.5。 很明显,这个新问题的答案和我们原来问题的答案不一样,一个是0.5,一个是。为什么会是这样子呢?会不会是某一个答案搞错了?我们原来已经知道朋友家里有一个男孩,现在我们只是实际看到了这个男孩。难道看一眼这个男孩确认一下我们已经知道的事实就会导致概率的变化吗? 上面的问题确实很令人迷惑,这也突出体现了概率论这门学科的众多违反直觉之处。实际上,这个新问题和我们前面讨论的问题确实是不同的问题,就像著名的 Monty Hall Problem 一样,某些被呈现的事件实际上被Over Represented。 原来的问题可以看做采样基本对象是家庭:在所有有两个孩子的家庭中,我们把至少有一个男孩的家庭抽取出来排成一队,队列中的每一个项目都是一个家庭;现在我们从这个队列中随机抽取一个家庭,那么抽到有两个男孩的家庭的概率是 。 而新问题采样的基本对象是男孩:对于所有的有两个孩子的家庭,把所有的男孩都排成一队,队列中的每一个项目都是一个男孩;下来我们从队伍中随机抽取一个男孩,问他来自于两个男孩的家庭的概率是多少。这种情况下他来自于有两个男孩的家庭的概率是0.5。这是因为两个男孩的家庭男孩比其他家庭要多,所以在这个队列中抽到来自两个男孩家庭的男孩的概率也要比其他家庭大。不易察觉的是,对于两个女孩的家庭,由于这些家庭根本没有办法参与到这个队列中来,如果我们加上限制条件,参加队列的家庭是那些至少有一个男孩的家庭(Hypothesis, H),那么抽到来自两个男孩家庭的男孩的概率依然是0.5。这个过程参见下面的图片。 现在我们言归正传,那么前面那个朋友家庭有两个男孩的概率到底是0.5 还是 呢?对于这个问题,我的回答是概率只能应用于统计,而不能应用于个体,所以这个问题没有意义呀。 […]

字母表(Alphabet)的长度为m,单词(Word)的长度为n,字母表中的每一个字母都用到的单词有多少?

字母表(Alphabet)的长度为,单词(Word)的长度为,字母表中的每一个字母都用到的单词有多少?这个问题又是一个可以通过递归分析来解决的组合数学题目。 我们先假定这样的单词总数是 。当字母表的长度为,单词的长度为的时候,所有可能的单词总数是: (1)   在上面这么多单词中,只含有一个字母的单词总数是: (2)   只含有两个字母,并且这两个字母都用到了的单词总数是多少呢?首先从个字母中选取两个字母的组合数目是: (3)   而这两个字母组成的长度为 ,而且这两个字母都用到了的单词,按照我们上面的递归定义则是 。因为每个组合中的字母都不一样,而每个单词用到了所有的字母,所以所有这些组合代表的字母表形成的单词互相既不重复,又涵盖了所有长度为而且用到了两个不同字母的单词。综上所述,所有两字母而且这两个字母都被用到的单词总数是: (4)   以此类推,所有三个字母而且这三个字母都被用到的单词总数是: (5)   这样可以一直推到 。另外我们注意到在单词表只有一个字母的情况下,用到这个字母的长度为 的单词为 1,也就是 。那么我们把上面讨论过的只含有一个字母的单词总数可以表示为: (6)   我们从所有可能的 的单词中去除掉那些只有一个字母的单词、二个字母而且每个字母都被用到的单词、三个字母而且每个字母都被用到的单词、…、一直到 个字母而且每个字母都被用到的单词,就得到了 个字母都被用到的单词,也就是: (7)   上面就是 的一个递归定义,具体的实现在这里。 from math import factorial # combination def C(n,m): return factorial(n) / (factorial(m) * factorial(n-m)) # Cache table for X, see below. […]

An informal (picture based) proof of De Morgan’s laws

很多组合数学的问题正向考虑比较困难,反向考虑就容易很多。这种时候,Morgan 定律就排上了用场,下面是该定律的形象化的证明和一个具体应用的例子。 假定有两个集合 A 和 B,Morgan 定律把它们的交集通过补集和并集操作表示如下: (1)   这个定律一个形象化的证明如下: 下面是 Morgan 定律具体应用的例子,假定我们有一个 位的数字,每一位上可以是0-9这10个数字,那么既有0又有1的数字一共有多少? 所有总共可能的数字是 (World): (2)   其中没有0的数字 (定义为集合 ) : (3)   其中没有1的数字 (定义为集合 ) : (4)   所有有0的数字(定义为集合A): (5)   所有有1的数字(定义为集合B): (6)   那么根据 Morgan 定律,又有0又有1的数字就是: (7)   我们已经知道 , ,关键是 如何计算,可以看出,既没有0也没有1的数字是: (8)   所以我们有: (9)   最后对于又有0又有1的数字就是: (10)   对于 的情况,又有0又有1的数字只有两个: […]