问题

世界上有哪些代码量很少,但很牛逼很经典的算法或项目案例?

回答
当然,这倒是个有趣的话题。很多人一提到“算法”或者“牛逼项目”,脑子里就涌现出各种复杂的数学模型、庞大的代码库,动辄几万行起步。但其实不然,很多时候,最简洁、最精妙的设计,反而是最能穿透人心的。它们就像武侠小说里那种,看起来轻描淡写的一招,却能以柔克刚,化解千斤重担。

今天咱们就来聊聊那些代码量不多,但“内功”深厚,让人拍案叫绝的经典算法和项目案例。咱们尽量用大白话讲讲,别那些拗口的专业术语,让你听着也像是在品鉴一段古老的故事。

1. 快速排序(Quicksort):一招鲜,吃遍天

说到代码量少又牛逼,快速排序绝对是绕不开的经典。你可能听过它的名字,甚至在各种编程教程里见过它的样子。它最让人惊艳的地方在于,用极少的代码,就能实现一个相当高效的排序算法。

想象一下,你有一堆乱七八糟的扑克牌,要按顺序排好。快速排序的做法,就像是找一个“标杆”,然后把牌分成两拨:比标杆小的放一边,比标杆大的放另一边。然后,再分别对这两拨牌重复这个过程,直到所有的牌都排好。

用代码来描述这个过程,其实就是这么简单:

选择一个“基准”(pivot)元素:通常选第一个,最后一个,或者中间的都可以。
分区(partition):遍历数组,把小于基准的元素放到它左边,大于基准的放到它右边。最后,基准就到了它最终该在的位置。
递归(recursion):对基准左边的子数组和右边的子数组,重复上面的步骤。

你看,整个逻辑就这么几个步骤。实现起来,往往就是几十行代码的事儿(当然,这里指的是核心的递归函数,如果加上各种错误检查和优化,代码量会多点)。但它在平均情况下的效率极高,时间复杂度是 O(n log n),这在排序算法里已经是顶尖水平了。

它的“牛逼”之处在于,它是一种“分而治之”的典范。把一个大问题拆解成若干个小问题,逐个击破,最后汇集成一个完美的解决方案。而且,它的思想可以延伸到很多其他领域,不仅仅是排序。比如在某些数据结构的设计中,也常常能看到它的影子。

为什么它不显得“AI”? 因为它的核心思想是人类在实践中摸索出来的,一种直观的、基于逻辑的拆解和重组。没有海量的数据训练,没有复杂的神经网络结构。它就是纯粹的智慧结晶。

2. 傅里叶变换(Fourier Transform):让声音和图像“听话”

这玩意听起来可能有点玄乎,但它真的太重要了!它有一个形象的比喻:万物皆可分解为“纯粹的音波”。

你听到的任何声音,比如一首交响乐,或者你的说话声,其实都是由无数个不同频率、不同振幅的正弦波叠加而成的。傅里叶变换就是一种数学工具,它能把一个复杂的信号(比如声音、图像,甚至是股票价格波动),分解成它最基本、最纯粹的“组成部分”——也就是这些不同频率的正弦波。

你可以想象成一个调音台,把一首混合了各种乐器的音乐,分解成每一轨单独的乐器声音。傅里叶变换做的就是类似的事情,只不过它处理的是数学信号。

正向傅里叶变换:将时域信号(比如一段时间内的声音波形)转换到频域(即分析它包含哪些频率的成分)。
逆向傅里叶变换:将频域信息再还原回时域信号。

它的代码量其实也还好,核心算法的实现并不算特别庞大。但它的应用范围,简直是无边无际!

音频处理:MP3压缩就是用了傅里叶变换的思想,去掉人耳不敏感的频率成分,让文件变小。
图像处理:人脸识别、图像去噪、边缘检测,很多都离不开它。我们看到的很多特效,比如模糊、锐化,都是在频域里操作的。
通信工程:手机信号的传输、无线电通信,本质上都是在处理不同频率的信号。
科学研究:从天体物理到量子力学,很多领域都用它来分析数据。

它之所以经典,是因为它揭示了“局部(时间域)”和“整体(频率域)”之间的深刻联系。它提供了一种全新的视角去看待和处理信息。它不依赖于什么复杂的深度学习框架,它的数学原理是扎实而普适的。

为什么它不显得“AI”? 这是纯粹的数学工具,是人类对自然现象的深刻洞察。它的逻辑是清晰的数学推理,不是概率统计模型的迭代优化。

3. DiffieHellman密钥交换协议:在不安全的网络里“秘密通信”

这听起来有点像科幻小说里的情节,但在互联网上,这却是保障我们信息安全的基础之一。DiffieHellman协议,用极少的数学原理,解决了一个看似无解的问题:如何在完全不安全的网络上,安全地交换一个只有双方知道的秘密密钥?

想象一下,你和你的朋友,在完全陌生、充满窃听者的情况下,想要约定一个只有你们俩才懂的密码。这怎么可能?

DiffieHellman的聪明之处在于它利用了数学上的一个特性:有些计算正向做很容易,反向做却极其困难。 具体来说,就是“离散对数问题”。

咱们用一个简单的比喻来说明:

1. 公开颜料(大A,大B):我和我朋友在网上(公开场合)约定好,用一种公开的颜料,比如“黄色”。每个人都知道这个颜色。
2. 秘密颜料(小a,小b):我秘密地选了一种自己的颜色,比如“蓝色”(小a)。我朋友也秘密地选了一种自己的颜色,比如“红色”(小b)。
3. 混合后公开(混合A,混合B):我把我公开的“黄色”颜料和我秘密的“蓝色”颜料混合,得到一种新的颜色“绿”(混合A),然后把这个“绿色”公布到网上。我朋友也用公开的“黄色”和他的秘密“红色”混合,得到“橙色”(混合B),然后公布到网上。
4. 私下混合,秘密达成(最终密码):
我拿到我朋友公布的“橙色”(混合B),然后用我秘密的“蓝色”(小a)去混合它。
我朋友拿到我公布的“绿色”(混合A),然后用他秘密的“红色”(小b)去混合它。

奇妙的事情发生了!无论我们用什么方式混合颜色(这个混合过程可以类比数学上的指数运算),最终我们两个都会得到完全相同的“最终颜色”,而其他偷听者只知道公开的“黄色”以及我俩公布的“绿色”和“橙色”,但他们无法通过这些颜色推算出我们各自秘密使用的“蓝色”和“红色”,更别提最终的那个混合颜色了。

DiffieHellman协议就是用数字来代替颜色,用模幂运算(一个数学运算)来代替混合。核心代码实现,就是实现这么一个模幂运算和一些公开参数的设置。虽然它不是直接用来加密通信的,但它生成的那把“秘密密钥”,就是用来加密后续通信的。

它的“牛逼”之处在于,它用非常优雅的数学理论,解决了网络安全中最基本也最核心的问题——安全地建立一个共享的秘密。 它的代码量极少,但其影响力却辐射了整个互联网的安全领域。

为什么它不显得“AI”? 这是基于数论和密码学的纯粹数学原理,是一种逻辑上的巧妙设计,不是机器学习模型。

这些案例的共同点

你看,这些经典的算法和项目,虽然在某些情况下可能有更复杂的变种,但它们的核心思想往往非常简洁,并且能用相对少的代码实现。它们之所以“牛逼”,是因为:

洞察深刻:它们抓住了问题的本质,找到了解决问题的关键点。
思想普适:其核心思想可以迁移到其他问题和领域。
效率高:在效率上,它们通常能做到很优。
优雅简洁:用最少的代码,实现最强大的功能。

它们不像现在很多“AI项目”那样,动辄需要几千行甚至几万行的代码来训练一个庞大的模型。它们更像是精雕细琢的工艺品,每一行代码、每一个逻辑都充满了智慧的闪光。它们是人类对逻辑、数学和抽象思维能力淋漓尽致的体现。

希望这些例子能让你对“少即是多”的编程智慧,有更直观的感受。这些东西的魅力,在于理解它们背后的那份“聪明劲儿”,而不是仅仅看到代码的长度。

网友意见

user avatar

居然没人说这个在GitHub上拿到34k星的项目?难道知乎上的程序员都只逛百度,不逛GitHub?

这个项目堪称传奇,在GitHub上被无数人称赞为新手必学项目,却偏偏又可以无需任何修改,就在任何平台、版本的IDE上运用,真正的0成本接入,堪称有史以来最伟大的工程,没有之一

为了尊重原作者,请大家去项目中查看,绝对是计算机历史上的一座丰碑。

我上面的话,可不是我自己评价的,都是网友们的留言:


这也是我见过第一款,被中外老哥用花式语言夸奖的项目,比如Book思议(不可思议),甚至很多人用:道可道,非常道、无极、凝视深渊这里很有哲理的话来夸奖。

整个GitHub,甚至往大了说,全网你都找不到第二个这样的项目了!

项目名称:nocode

项目地址:github.com/kelseyhighto

等你心平气和,仔细看完项目就会明白,这个项目贯彻了git这个词的本意。


而且程序员其实有三重境界:

1.眼中有码,敲键盘如有神助。

2.心中有码,无码也有码,对于代码的理解近乎于道。

3.心中无码,有码也无码,他就是道。

只有到达第三重境界才能迈过35岁这个坎


我看完这个项目之后的感想:





再给大家来个硬核项目:99行代码实现《冰雪奇缘》!

《冰雪奇缘》没有真人出演,预算却高达1.5亿美元,每一秒的镜头都是经费在燃烧。一般人想用电脑做出CG特效简直不可想象。

然而,最近一位来自中国的MIT博士,开发了物理模拟编程语言:Taichi

大大降低了成本。

计算机图形学知名学者、北大教授陈宝权给出很高的评价:

       import taichi as ti quality = 1 # Use a larger value for higher-res simulations n_particles, n_grid = 9000 * quality ** 2, 128 * quality dx, inv_dx = 1 / n_grid, float(n_grid) dt = 1e-4 / quality p_vol, p_rho = (dx * 0.5)**2, 1 p_mass = p_vol * p_rho E, nu = 0.1e4, 0.2 # Young's modulus and Poisson's ratio mu_0, lambda_0 = E / (2 * (1 + nu)), E * nu / ((1+nu) * (1 - 2 * nu)) # Lame parameters  x = ti.Vector(2, dt=ti.f32, shape=n_particles) # position v = ti.Vector(2, dt=ti.f32, shape=n_particles) # velocity C = ti.Matrix(2, 2, dt=ti.f32, shape=n_particles) # affine velocity field F = ti.Matrix(2, 2, dt=ti.f32, shape=n_particles) # deformation gradient material = ti.var(dt=ti.i32, shape=n_particles) # material id Jp = ti.var(dt=ti.f32, shape=n_particles) # plastic deformation grid_v = ti.Vector(2, dt=ti.f32, shape=(n_grid, n_grid)) # grid node momemtum/velocity grid_m = ti.var(dt=ti.f32, shape=(n_grid, n_grid)) # grid node mass ti.cfg.arch = ti.cuda # Try to run on GPU  @ti.kernel def substep():   for i, j in ti.ndrange(n_grid, n_grid):     grid_v[i, j] = [0, 0]     grid_m[i, j] = 0   for p in range(n_particles): # Particle state update and scatter to grid (P2G)     base = (x[p] * inv_dx - 0.5).cast(int)     fx = x[p] * inv_dx - base.cast(float)     # Quadratic kernels  [http://mpm.graphics   Eqn. 123, with x=fx, fx-1,fx-2]     w = [0.5 * ti.sqr(1.5 - fx), 0.75 - ti.sqr(fx - 1), 0.5 * ti.sqr(fx - 0.5)]     F[p] = (ti.Matrix.identity(ti.f32, 2) + dt * C[p]) @ F[p] # deformation gradient update     h = ti.exp(10 * (1.0 - Jp[p])) # Hardening coefficient: snow gets harder when compressed     if material[p] == 1: # jelly, make it softer       h = 0.3     mu, la = mu_0 * h, lambda_0 * h     if material[p] == 0: # liquid       mu = 0.0     U, sig, V = ti.svd(F[p])     J = 1.0     for d in ti.static(range(2)):       new_sig = sig[d, d]       if material[p] == 2:  # Snow         new_sig = min(max(sig[d, d], 1 - 2.5e-2), 1 + 4.5e-3)  # Plasticity       Jp[p] *= sig[d, d] / new_sig       sig[d, d] = new_sig       J *= new_sig     if material[p] == 0:  # Reset deformation gradient to avoid numerical instability       F[p] = ti.Matrix.identity(ti.f32, 2) * ti.sqrt(J)     elif material[p] == 2:       F[p] = U @ sig @ V.T() # Reconstruct elastic deformation gradient after plasticity     stress = 2 * mu * (F[p] - U @ V.T()) @ F[p].T() + ti.Matrix.identity(ti.f32, 2) * la * J * (J - 1)     stress = (-dt * p_vol * 4 * inv_dx * inv_dx) * stress     affine = stress + p_mass * C[p]     for i, j in ti.static(ti.ndrange(3, 3)): # Loop over 3x3 grid node neighborhood       offset = ti.Vector([i, j])       dpos = (offset.cast(float) - fx) * dx       weight = w[i][0] * w[j][1]       grid_v[base + offset] += weight * (p_mass * v[p] + affine @ dpos)       grid_m[base + offset] += weight * p_mass   for i, j in ti.ndrange(n_grid, n_grid):     if grid_m[i, j] > 0: # No need for epsilon here       grid_v[i, j] = (1 / grid_m[i, j]) * grid_v[i, j] # Momentum to velocity       grid_v[i, j][1] -= dt * 50 # gravity       if i < 3 and grid_v[i, j][0] < 0:          grid_v[i, j][0] = 0 # Boundary conditions       if i > n_grid - 3 and grid_v[i, j][0] > 0: grid_v[i, j][0] = 0       if j < 3 and grid_v[i, j][1] < 0:          grid_v[i, j][1] = 0       if j > n_grid - 3 and grid_v[i, j][1] > 0: grid_v[i, j][1] = 0   for p in range(n_particles): # grid to particle (G2P)     base = (x[p] * inv_dx - 0.5).cast(int)     fx = x[p] * inv_dx - base.cast(float)     w = [0.5 * ti.sqr(1.5 - fx), 0.75 - ti.sqr(fx - 1.0), 0.5 * ti.sqr(fx - 0.5)]     new_v = ti.Vector.zero(ti.f32, 2)     new_C = ti.Matrix.zero(ti.f32, 2, 2)     for i, j in ti.static(ti.ndrange(3, 3)): # loop over 3x3 grid node neighborhood       dpos = ti.Vector([i, j]).cast(float) - fx       g_v = grid_v[base + ti.Vector([i, j])]       weight = w[i][0] * w[j][1]       new_v += weight * g_v       new_C += 4 * inv_dx * weight * ti.outer_product(g_v, dpos)     v[p], C[p] = new_v, new_C     x[p] += dt * v[p] # advection  import random group_size = n_particles // 3 for i in range(n_particles):   x[i] = [random.random() * 0.2 + 0.3 + 0.10 * (i // group_size), random.random() * 0.2 + 0.05 + 0.32 * (i // group_size)]   material[i] = i // group_size # 0: fluid 1: jelly 2: snow   v[i] = [0, 0]   F[i] = [[1, 0], [0, 1]]   Jp[i] = 1  import numpy as np gui = ti.GUI("Taichi MLS-MPM-99", res=512, background_color=0x112F41) for frame in range(20000):   for s in range(int(2e-3 // dt)):     substep()   colors = np.array([0x068587, 0xED553B, 0xEEEEF0], dtype=np.uint32)   gui.circles(x.to_numpy(), radius=1.5, color=colors[material.to_numpy()])   gui.show() # Change to gui.show(f'{frame:06d}.png') to write images to disk     

这段代码用Python 3运行。运行前要根据操作系统和CUDA版本(如果有CUDA的话)安装taichi

       # CPU 版本 (支持Linux, OS X and Windows) python3 -m pip install taichi-nightly  # GPU (CUDA 10.0) (只支持Linux) python3 -m pip install taichi-nightly-cuda-10-0  # GPU (CUDA 10.1) (只支持Linux) python3 -m pip install taichi-nightly-cuda-10-1     

这99行代码虽然很短,其背后的故事却很长。

虽然语法看起来是Python,其计算部分却会被一整套编译系统接管,最后输出可执行的x86_64或者PTX instructions,能够在CPU/GPU上高效运行。

https://www.zhihu.com/video/1200720718734069760

项目地址:github.com/yuanming-hu/


延伸阅读:

如果你想了解更多有趣、有观点、有知识的内容,别忘记点击 @东来 关注我,一个有趣的人在默默的等着你!

我还写了不少“宝藏”内容, 可以来看看我19年的高赞内容,横跨编程、旅行、体育三大板块,好几个是单篇数百万阅读的硬核科普内容。

user avatar

前面的回答有提到FFT,这个算法也很牛逼,另外我觉得的PID算法也很强,特别是在工业控制中,使用非常广泛,下面是我之前学习PID写的笔记,可以参考一下;

1 前言

控制系统通常根据有没有反馈会分为开环系统和闭环系统,在闭环系统的控制中,PID算法非常强大,其三个部分分别为;

  • P:比例环节;
  • I:积分环节;
  • D:微分环节;

PID算法可以自动对控制系统进行准确且迅速的校正,因此被广泛地应用于工业控制系统。

2 开环控制

首先来看开环控制系统,如下图所示,隆哥蒙着眼,需要走到虚线旗帜所表示的目标位置,由于缺少反馈(眼睛可以感知当前距离和位置,由于眼睛被蒙上没有反馈,所以这也是一个开环系统),最终隆哥会较大概率偏离预期的目标,可能会运行到途中实线旗帜所表示的位置。

开环系统的整体结构如下所示;


这里做一个不是很恰当的比喻;

  • Input:告诉隆哥目标距离的直线位置(10米);
  • Controller:隆哥大脑中计算出到达目标所需要走多少步
  • Process:双腿作为执行机构,输出了相应的步数,但是最终仍然偏离了目标;

看来没有反馈的存在,很难准确到达目标位置。

3 闭环控制

所以为了准确到达目标位置,这里就需要引入反馈,具体如下图所示;


在这里继续举个不怎么恰当的比喻;隆哥重获光明之后,基本可以看到目标位置了;

  • 第一步Input:告诉隆哥目标距离的直线位置(10米);
  • 第二步Controller:隆哥大脑中计算出到达目标所需要走多少步
  • 第三步Process:双腿作为执行机构,输出了相应的步数,但是最终仍然偏离了目标;
  • 第四步Feedback:通过视觉获取到目前已经前进的距离,(比如前进了2米,那么还有8米的偏差);
  • 第五步err:根据偏差重新计算所需要的步数,然后重复上述四个步骤,最终隆哥达到最终的目标位置。

4 PID

4.1 系统架构

虽然在反馈系统下,隆哥最终到达目标位置,但是现在又来了新的任务,就是又地到达目标位置。所以这里隆哥开始采用PID Controller,只要适当调整P,I和D的参数,就可以到达目标位置,具体如下图所示;

隆哥为了最短时间内到达目标位置,进行了不断的尝试,分别出现了以下几种情况;

  • 跑得太快,最终导致冲过了目标位置还得往回跑
  • 跑得太慢,最终导致到达目标位置所用时间太长

经过不断的尝试,终于找到了最佳的方式,其过程大概如下图所示;

这里依然举一个不是很恰当的比喻;

  • 第一步:得到与目标位置的距离偏差(比如最开始是10米,后面会逐渐变小);
  • 第二步:根据误差,预估需要多少速度,如何估算呢,看下面几步;

P比例则是给定一个速度的大致范围,满足下面这个公式;

因此比例作用相当于某一时刻的偏差(err)与比例系数的乘积,具体如下所示;

绿色线为上述例子中从初始位置到目标位置的距离变化; 红色线为上述例子中从初始位置到目标位置的偏差变化,两者为互补的关系;




I积分则是误差在一定时间内的和,满足以下公式;

如下图所示;

红色曲线阴影部分面积即为积分作用的结果,其不断累积的误差,最终乘以积分系数就得到了积分部分的输出;




D微分则是误差变化曲线某处的导数,或者说是某一点的斜率,因此这里需要引入微分;


从图中可知,当偏差变化过快,微分环节会输出较大的负数,作为抑制输出继续上升,从而抑制过冲。




综上,,分别增加其中一项参数会对系统造成的影响总结如下表所示;

4.2 理论基础

上面扯了这么多,无非是为了初步理解PID在负反馈系统中的调节作用,下面开始推导一下算法实现的具体过程;PID控制器的系统框图如下所示;

因此不难得出输入和输出的关系;

是比例增益; 是积分增益; 是微分增益;

4.3 离散化

在数字系统中进行PID算法控制,需要对上述算法进行离散化;假设系统采样时间为 则将输入序列化得到;

将输出序列化得到;

  • 比例项:
  • 积分项:
  • 微分项:

所以最终可以得到式①,也就是网上所说的位置式PID

将式①再做一下简化;

最终得到增量式PID的离散公式如下:

4.4 伪算法

这里简单总结一下位置式PID实现的伪算法;

       previous_error := 0  //上一次偏差 integral := 0   //积分和  //循环  //采样周期为dt loop:  //setpoint 设定值  //measured_value 反馈值     error := setpoint  measured_value //计算得到偏差     integral := integral + error × dt //计算得到积分累加和     derivative := (error  previous_error) / dt //计算得到微分     output := Kp × error + Ki × integral + Kd × derivative //计算得到PID输出     previous_error := error //保存当前偏差为下一次采样时所需要的历史偏差     wait(dt) //等待下一次采用     goto loop        

5 C++实现

这里是位置式PID算法的C语言实现;

pid.cpp

       #ifndef _PID_SOURCE_ #define _PID_SOURCE_  #include <iostream> #include <cmath> #include "pid.h"  using namespace std;  class PIDImpl {     public:         PIDImpl( double dt, double max, double min, double Kp, double Kd, double Ki );         ~PIDImpl();         double calculate( double setpoint, double pv );      private:         double _dt;         double _max;         double _min;         double _Kp;         double _Kd;         double _Ki;         double _pre_error;         double _integral; };   PID::PID( double dt, double max, double min, double Kp, double Kd, double Ki ) {     pimpl = new PIDImpl(dt,max,min,Kp,Kd,Ki); } double PID::calculate( double setpoint, double pv ) {     return pimpl->calculate(setpoint,pv); } PID::~PID()  {     delete pimpl; }   /**  * Implementation  */ PIDImpl::PIDImpl( double dt, double max, double min, double Kp, double Kd, double Ki ) :     _dt(dt),     _max(max),     _min(min),     _Kp(Kp),     _Kd(Kd),     _Ki(Ki),     _pre_error(0),     _integral(0) { }  double PIDImpl::calculate( double setpoint, double pv ) {          // Calculate error     double error = setpoint - pv;      // Proportional term     double Pout = _Kp * error;      // Integral term     _integral += error * _dt;     double Iout = _Ki * _integral;      // Derivative term     double derivative = (error - _pre_error) / _dt;     double Dout = _Kd * derivative;      // Calculate total output     double output = Pout + Iout + Dout;      // Restrict to max/min     if( output > _max )         output = _max;     else if( output < _min )         output = _min;      // Save error to previous error     _pre_error = error;      return output; }  PIDImpl::~PIDImpl() { }  #endif       

pid.h

       #ifndef _PID_H_ #define _PID_H_  class PIDImpl; class PID {     public:         // Kp -  proportional gain         // Ki -  Integral gain         // Kd -  derivative gain         // dt -  loop interval time         // max - maximum value of manipulated variable         // min - minimum value of manipulated variable         PID( double dt, double max, double min, double Kp, double Kd, double Ki );          // Returns the manipulated variable given a setpoint and current process value         double calculate( double setpoint, double pv );         ~PID();      private:         PIDImpl *pimpl; };  #endif       

pid_example.cpp

       #include "pid.h" #include <stdio.h>  int main() {      PID pid = PID(0.1, 100, -100, 0.1, 0.01, 0.5);      double val = 20;     for (int i = 0; i < 100; i++) {         double inc = pid.calculate(0, val);         printf("val:% 7.3f inc:% 7.3f
", val, inc);         val += inc;     }      return 0; }       

编译并测试;

       g++ -c pid.cpp -o pid.o # To compile example code: g++ pid_example.cpp pid.o -o pid_example       

6 总结

本文总结了PID控制器算法在闭环系统中根据偏差变化的具体调节作用,每个环节可能对系统输出造成什么样的变化,给出了位置式和离散PID算法的推导过程,并给出了位置式算法的C++程序实现。文章中只是最简易的PID算法,并没做优化,在实际的应用中,还需要做积分抗饱和,微分环节的优化,具体需要参考实际系统的情况来定。

由于作者能力和水平有限,文中难免存在错误和纰漏,请不吝赐教。



手动码字,感觉走心的话,点赞,关注支持一下吧; @极客小麦

user avatar

快速傅里叶变换(FFT),核心算法用递归法几十行搞定。

计算机应用最为重要的算法之一,广泛应用于各种各样的工业和科学实践中。第一次看到递归算出快速傅里叶的时候,惊了很久。后来经过数学家的很多努力,有了新算法将复杂度从N^2,降到N*log(N),觉得可以称之为人类智慧的精华了。

有空可以贴下源代码。

2020.1.15 科研狗这几天忙着科研,大家别急,今天一定抽空把算法和源代码贴出来。

第一次更新(1.15):

众所周知,数学中有一个傅里叶变换,是将函数在实空间的定义域变换到倒空间的定义域上,这个变换非常重要,因为一些实空间不易发现的信息,在倒空间中会表现的非常明显,所以傅里叶变换是很多信号、物理的分析基础。它的形式如下:

以及其逆变换:

但是如果在计算机想要编程实现傅里叶变换就要不可避免地面临两个近似:第一,现实中我们无法将积分的上下界延伸到无穷,所以这里一定是一个有界积分;第二,无论什么连续的公式,只要是想要被计算机实现,就必须进行离散化,所以这里的连续积分( )就近似为离散求和( )。而基于这两种朴素近似下的,就是所谓的“离散傅里叶变换”(Discrete Fourier Transform,简称DFT),它的形式也很直观:

看到这里,其实你如果利用DFT简单地编程,其实已经可以实现傅里叶变换了。但是我们为什么不用这个算法呢?是因为复杂度。

复杂度是算法中的一个概念,简单来说就是复杂度越高,那么这个算法计算大体系就越困难,那有多困难呢?我举个例子,现在算法基础库中对于矩阵的对角化应该是最基础和重要的了,矩阵的对角化的计算复杂度是 ,就是说对角化的时间就随着矩阵维数的三次方增加,这是十分恐怖的增长。目前市面的通用的对角化库,比如 lapcak,对角化的极限维数一般也就在 10000, 这个量级。那DFT的复杂度是多少呢?你可以从公式上看到,如果你现在要对一个包含N个数的数组进行DFT,那你每算其中一个变换后的数,都要进行N次的运算,那这个算法的复杂度也就是 ,也就是说,用DFT最多也就只能处理大概一个拥有大概 个数的数组。但是在生产科研实践中,这实在不是一个大的上限,所以这个复杂度的硬伤一直制约着DFT的发展,而这就是属于当时制约整个人类文明进步的“卡脖子”的难题,如果FFT没有出现,那我们的世界现在绝对不是现在这个样子。

时间来到1965年,James Cooley 和 John Tukey 发现了一种算法,这种的算法的复杂度降低为了 ,而这种算法就称之为“快速傅里叶变换”(Fast Fourier transform, 简称FFT)。但其实,就这个问题,Danielson 和 Lanczos 在1942年就已经开始了相关的讨论。他们发现任何一个DFT其实可以重新改写成两个DFT的和:

数学家进一步发现这一分为二的DFT组,一个是原来是偶数项(even,第0,2,4,6...项)组成的,而另一个是由原来的奇数项(odd)组成的。如果你只是看到这一步,你已经发现了一个大事情,就是原来用这个公式来算的话,求一个傅里叶变换不再需要进行N次相乘,而只需要odd组的N/2次运算,even组的直接不用做任何运算直接拉下来就好了。那这样计算量不就减少一半了吗?但是其实事情并没有那么简单,我们刚才证明了任何一个DFT组都能被一分为二,而且计算量减半。但是我们现在一分为二得到的even组和odd组,他们不又是DFT组吗?我们可以继续之前的操作,将他们分别再次分为两个数组的求和!

一分为二,二分为四,四分为八,一直分到每个数组只剩一个数字为止!而且每进行一次操作,就能使得其中一半的计算量被减少。这样我们就把计算量从 降到了 。你可能现在对这个算法的突破性还没有概念,我现在来给你构建下概念,

随着N的增加,FFT算法对于DFT的优势会不断显现,一开始1024个数,FFT比DFT快100倍;32768个数,就快了2000多倍,到了我们之前提到的极限1百万个数,快了近5万倍!FFT将我们的计算分析能力成万倍的提升。提一句,现在FFT最稳定最快的库是FFTW,这个W是“in West”,就是说“西方的FFT”。我当时刚看懂FFT时,不服气,一心想写个东方版,FFTE(FFT in East),后来和我自己的程序和FFTW性能一比,我人都傻了,果然好的FFT程序还是超级难写的。

这个算法关键的关键就是将DFT组不断奇偶细分,细分到最后时,如何确定每个数组前面的相位系数。我希望今天可以在第二次更新中把如何确定细分后的序列,以及蝶形算法更完。

我先把我当时研一第一次接触到FFT编写的Fortran源码贴出来吧。你品,细品。短短几十行,如果你能花几天的时间理解这个递归程序,那么这将是2020年获得的第一个受益终身的知识。

       ! Cooley-Tukey FFT recursive subroutine fft(x,sgn)      implicit none     integer, intent(in) :: sgn     complex(8), dimension(:), intent(inout) :: x     complex(8) :: t     integer :: N     integer :: i     complex(8), dimension(:), allocatable :: even, odd      N=size(x)      if(N .le. 1) return      allocate(odd((N+1)/2))     allocate(even(N/2))      ! divide     odd =x(1:N:2)     even=x(2:N:2)      ! conquer     call fft(odd, sgn)     call fft(even, sgn)      ! combine     do i=1,N/2         t=exp(cmplx(0.0d0,sgn*2.0d0*pi*(i-1)/N))*even(i)         x(i)     = odd(i) + t         x(i+N/2) = odd(i) - t     end do      deallocate(odd)     deallocate(even)  end subroutine fft     

user avatar

有的,比如 洗牌算法:这个算法真的很牛逼很经典,而且代码很少。

评论区好多大佬,膜拜ing。。


先来思考一个问题:有一个大小为 100 的数组,里面的元素是从 1 到 100 按顺序排列,怎样随机的从里面选择 1 个数?

最简单的方法是利用系统的方法 Math.random() * 100 ,这样就可以拿到一个 0 到 99 的随机数,然后去数组找对应的位置就即可。

接下来在思考一个问题: 有一个大小为100的数组,里面的元素是从 1 到 100 按顺序排列,怎样随机的从里面选择 50 个数?

注意数字不能重复!

注意数字不能重复!

注意数字不能重复!

如果根据上面的思路,你第一想法是:随机 50 次不就行了?

但是,这样做有个很明显的 bug :数字是会重复的。

修改一下?

弄一个数组,把每一次随机的数都放到数组里,下一次随机就看这个数组里面有没有这数,有的话就继续随机,直到这个数组里面有 50 个数字就停止。

这样是可以的!

但,还是有个小问题,考虑一下极端情况:有一个大小为100的数组,里面的元素是从 1 到 100 按顺序排列,怎样随机的从里面选择 99 个数

如果按照上面的方法操作,越往后选择的数字跟前面已经挑选的数字重复的概率越高,这就会造成如果数组很大,选择的数字数目也很大的话,重复次数在量级上会很大。

这个时候就需要换一个思路,如果先将数组里面的元素打乱,那么按顺序选择前 50 个不就可以了?

是的!

但我们得注意什么叫乱?

一副扑克有 54 张牌,有 54! 种排列方式。所谓的打乱指的是,你所执行的操作,应该能够 等概率地生成 这 54! 种结果中的一种。

洗牌算法就能做到这一点。

洗牌算法

Fisher–Yates shuffle 算法由 Ronald Fisher 和 Frank Yates 于 1938 年提出,在 1964 年由 Richard Durstenfeld 改编为适用于电脑编程的版本。

这个算法很牛逼却很好理解,通俗的解释就是:将最后一个数和前面任意 n-1 个数中的一个数进行交换,然后倒数第二个数和前面任意 n-2 个数中的一个数进行交换。。。



小程序实现代码

       for (var i = this.rowCount * this.colCount - 1; i >= 0 ; i--){   var iX = parseInt(i / this.colCount);   var iY = i % this.colCount;    var randNumber = this.rangeRandom(0, i + 1);    var randX = parseInt(randNumber / this.colCount);   var randY = randNumber % this.colCount;   //交换两个位置   var temp = tmpMineMap[iX][iY];   tmpMineMap[iX][iY] = tmpMineMap[randX][randY];   tmpMineMap[randX][randY] = temp; }     

在整个过程中,这个算法保证了每一个元素出现在每一个位置的概率是相等的

这个算法真的很牛逼很经典,而且代码很少。


我做了一个视频进行辅助理解。

洗牌算法:) https://www.zhihu.com/video/1220804449213698048



为了避免知乎大佬觉得我吹逼,先贴一下自己的 GitHub 地址,目前 70,000 star,全球排名 51 名。

github.com/MisterBooo

算法是一种技能,是可以通过科学合理的方式训练出来的能力。

在想刷题之前,得从心里认识到接受刷题很重要,才能坚持去刷题。

江湖有个传言:国内刷 LeetCode,最多够你吃 1 年老本;湾区刷 LeetCode ,够你吃 10 年老本了。

为什么湾区的刷题性价比这么高呢?

你想想,电面考 4 道题,一道题值 5 万!单位是 Dollar !

刷到就是赚到!!

想想是不是很刺激,有没有动力开始刷题了!可以提速刷题了!

就目前互联网的情况来说,无论是面国外大厂还是面国内大厂,如果想换工作都要去刷题,一面二面不丢你几道 Hard 题,都对不住你偷偷摸摸找个会议室假装开会实则面试的鸡贼。

同时,还得认识到一点,面试能力和你平时的工作能力其实差别挺大的。

有些人技术挺厉害的,但没有刷题,一面二面都过不了,而某些小镇刷题家,还真就靠刷题拿下了 Google、微软、脸书等大厂offer。

国内大厂也有这种趋势,比如字节,一大半都是面试题。

要不是他提前先看视频刷题,妥妥得凉凉。

所以,刷题很重要。

(PS:感谢大家耐心的阅读,算法是程序员的重中之重,必须攻克,大厂面试必考,顺便送一份阿里大佬刷Leetcode总结的算法笔记,如果你能吃透,那我相信80%的技术面试都会不在话下:

BAT大佬写的Leetcode刷题笔记,看完秒杀80%的算法题!

这本书的目录,非常经典:

刷题大概可以分为 4 个阶段。

1、纯小白,不知道怎么刷题,对很多概念都很陌生,各种数据结构和知识点几乎完全不懂,打开 LeetCode 第一题,满头问号。

有人相爱、有人夜里开车看海、有人 LeetCode 第一题都做不出来。

2、算法上基本已经入门,Easy 可以做出来,Medium 纠结半天也能有头绪,但基础不牢,比如字符转字符串还得 Google 一下。

3、刷了几百道题后,总结了自己的解题模板,参加周赛有时候甚至可以全部完成。

4、开始以 beat 100% 作为 AC 的目标了。

就目前的算法面试大环境来说,能达到第二阶段,中小公司可以应付过去了,到达第三阶段,字节、腾讯算法面试环节妥妥没问题了。

怎么样到达第三阶段?

给一下我的一些小建议吧。

1、如果目标是国内大厂,那么一定要刷足够的题,不需要把 LeetCode 上 2500 道算法题都刷完,但至少刷 200 道算法高频题,这些高频题我都写了题解同时也录制了视频,

在这个链接总结了:algomooc.com/1659.html

2、面试前一周以看题为主,因为刷题也刷不了几题,多看看自己总结或者别人总结的模板,比如回溯算法模板,掌握后,几十道回溯题都不在话下。

一些模板:

3、刷题过程需要注意难度要循序渐进,算法训练是一个系统工程,需要循序渐进,太过于急功近利,反而容易因做不出难题而产生挫败感,带来反效果。

如果你本身有基础,熟练度高,那你刷简单的 LeetCode 应该是几分钟一题,几分钟一题的,花不了你多少时间。

如果你刷简单都花费很长时间,说明熟练度不够,就更应该从简单开始,然后过度到中等,再过度到困难。

并且,目前国内大厂的算法考察,基本不会超过 LeetCode 中等难度,上限难度基本都是 LeetCode 中等题里面的中等难度,所以不要太去纠结难题怪题偏题。

把高频题掌握就行了:algomooc.com/1659.html

再退一步,如果你觉得 LeetCode 的题目太难,可以先从《剑指 Offer》上的算法题开始学起。

为了帮助大家更好的入门学习算法,经过半年的积累,我给大家了《剑指 Offer》系列的三十道题目,结合动画的形式录制了视频,相信能帮助你更好的刷题。

领取地址:

4、按算法分类来选题,比如一个时间段,只刷链表题,刷得差不多的时候,接下来再刷二叉树的题。

这样做有几个很明显的好处。

一、持续地刷同个类型的题目,可以不断地巩固和加深理解,可以总结出自己的思考路径或者解题模板。

比如链表题目,就会去思考虚拟头节点、双指针、快慢指针。

二、可以更全面地接触这个数据结构,算法的各个变种,这会促使你对这个数据结构,算法的理解更加全面和深刻,学习的效率会更高。

我一直认为读书是世界上性价比最高的成长方式,书很便宜但分量很重,是让我们摆脱平庸走向卓越的方式之一。

对于计算机专业的学生而言,读计算机经典书籍不光能让你快速提升知识和能力,更会让你在校招之际如虎添翼。

书籍下载:计算机必看经典书籍(含下载方式)

最后,再给大家送上点干货!

下面这是一个高赞回答合集,建议大家点赞&收藏,Mark住别丢了,大学期间绝对用得上

1、怎么学好数据结构,看下面这个回答,已经获得了 21000+ 的赞和 50000+的收藏。

2、如何系统地学习算法,看下面这个回答,已经获得了 11000+ 的赞和 26000+的收藏。

3、新手该如何使用 GitHub,看下面这个回答,如果在大学期间就知道使用 GitHub ,那么能力远超同龄人。

4、想成为一名优秀的程序员,那么这些程序员平时都喜欢逛的论坛怎么说你也得收藏一些吧。

5、无论别人怎么说,我都是坚定不移的选择计算机专业。

6、如何系统地学习 C++ ,这个回答能帮你找到路线。

7、想要准备 Java 面试,那么这些面试题必须掌握。

赶紧点赞和收藏吧~

user avatar

没有人提王垠的40行代码吗?



简单地说,这段代码做了两件事,一件事是CPS,也就是自动尾递归,第二件事则是用Scheme语言写了一个Scheme的解释器。通过他给出的cps函数,我可以用Scheme这个语言的符号系统重新定义所有Scheme的关键字,并执行正确的程序语义。换言之,它可以让这个语言自己解释自己。本质上,他的代码是在模仿当初 John McCarthy 发明 Lisp 语言时给出的代码,但用了Scheme风格重写了一遍。

-----------------------------------

关于这段代码的内容大家可以到这个问题下去看


王垠的「40 行代码」真如他说的那么厉害吗? - 知乎 zhihu.com/question/2082


也欢迎大家到我的公众号不恰柠檬来玩呀~

user avatar

机器学习十大算法之一:EM算法。

能评得上十大之一,让人听起来觉得挺NB的。什么是NB啊,我们一般说某个人很NB,是因为他能解决一些别人解决不了的问题。那么EM算法能解决什么问题呢?或者说EM算法是因为什么而来到这个世界上,还吸引了那么多世人的目光。

进入正题之前先给大家推荐一本书--《Python机器学习及实践-从零开始通往KAGGLE竞赛之路》,对于想系统学习机器学习的同学,推荐去看一下,电子版下载链接如下:

链接:pan.baidu.com/s/1rzaFEr

提取码:5gou

进入正题:

一、最大似然

  • 假设我们遇到的是下面这样的问题:
    • 假设我们需要调查我们学校的男生和女生的身高分布。你怎么做啊?你说那么多人不可能一个一个去问吧,肯定是抽样了。假设你在校园里随便地活捉了100个男生和100个女生。他们共200个人(也就是200个身高的样本数据,为了方便表示,下面,我说“人”的意思就是对应的身高)都在教室里面了。那下一步怎么办啊?你开始喊:“男的左边,女的右边,其他的站中间!”。然后你就先统计抽样得到的100个男生的身高。假设他们的身高是服从高斯分布的。但是这个分布的均值u和方差∂2我们不知道,这两个参数就是我们要估计的。记作θ=[u, ∂]T。
    • 用数学的语言来说就是:在学校那么多男生(身高)中,我们独立地按照概率密度p(x|θ)抽取100了个(身高),组成样本集X,我们想通过样本集X来估计出未知参数θ。这里概率密度p(x|θ)我们知道了是高斯分布N(u,∂)的形式,其中的未知参数是θ=[u, ∂]T。抽到的样本集是X={x1,x2,…,xN},其中xi表示抽到的第i个人的身高,这里N就是100,表示抽到的样本个数。
    • 由于每个样本都是独立地从p(x|θ)中抽取的,换句话说这100个男生中的任何一个,都是我随便捉的,从我的角度来看这些男生之间是没有关系的。那么,我从学校那么多男生中为什么就恰好抽到了这100个人呢?抽到这100个人的概率是多少呢?因为这些男生(的身高)是服从同一个高斯分布p(x|θ)的。那么我抽到男生A(的身高)的概率是p(xA|θ),抽到男生B的概率是p(xB|θ),那因为他们是独立的,所以很明显,我同时抽到男生A和男生B的概率是p(xA|θ)* p(xB|θ),同理,我同时抽到这100个男生的概率就是他们各自概率的乘积了。用数学家的口吻说就是从分布是p(x|θ)的总体样本中抽取到这100个样本的概率,也就是样本集X中各个样本的联合概率,用下式表示:
  • 这个概率反映了,在概率密度函数的参数是θ时,得到X这组样本的概率。因为这里X是已知的,也就是说我抽取到的这100个人的身高可以测出来,也就是已知的了。而θ是未知了,则上面这个公式只有θ是未知数,所以它是θ的函数。这个函数放映的是在不同的参数θ取值下,取得当前这个样本集的可能性,因此称为参数θ相对于样本集X的似然函数(likehood function)。记为L(θ)。
  • 这里出现了一个概念,似然函数。还记得我们的目标吗?我们需要在已经抽到这一组样本X的条件下,估计参数θ的值。怎么估计呢?似然函数有啥用呢?那咱们先来了解下似然的概念。

直接举个例子:

  • 某位同学与一位猎人一起外出打猎,一只野兔从前方窜过。只听一声枪响,野兔应声到下,如果要你推测,这一发命中的子弹是谁打的?你就会想,只发一枪便打中,由于猎人命中的概率一般大于这位同学命中的概率,看来这一枪是猎人射中的。
  • 这个例子所作的推断就体现了极大似然法的基本思想。
  • 再例如:下课了,一群男女同学分别去厕所了。然后,你闲着无聊,想知道课间是男生上厕所的人多还是女生上厕所的人比较多,然后你就跑去蹲在男厕和女厕的门口。蹲了五分钟,突然一个美女走出来,你狂喜,跑过来告诉我,课间女生上厕所的人比较多,你要不相信你可以进去数数。呵呵,我才没那么蠢跑进去数呢,到时还不得上头条。我问你是怎么知道的。你说:“5分钟了,出来的是女生,女生啊,那么女生出来的概率肯定是最大的了,或者说比男生要大,那么女厕所的人肯定比男厕所的人多”。看到了没,你已经运用最大似然估计了。你通过观察到女生先出来,那么什么情况下,女生会先出来呢?肯定是女生出来的概率最大的时候了,那什么时候女生出来的概率最大啊,那肯定是女厕所比男厕所多人的时候了,这个就是你估计到的参数了。
  • 从上面这两个例子,你得到了什么结论?
  • 回到男生身高那个例子。在学校那么男生中,我一抽就抽到这100个男生(表示身高),而不是其他人,那是不是表示在整个学校中,这100个人(的身高)出现的概率最大啊。那么这个概率怎么表示?哦,就是上面那个似然函数L(θ)。所以,我们就只需要找到一个参数θ,其对应的似然函数L(θ)最大,也就是说抽到这100个男生(的身高)概率最大。这个叫做θ的最大似然估计量,记为:
  • 有时,可以看到L(θ)是连乘的,所以为了便于分析,还可以定义对数似然函数,将其变成连加的:
  • 好了,现在我们知道了,要求θ,只需要使θ的似然函数L(θ)极大化,然后极大值对应的θ就是我们的估计。这里就回到了求最值的问题了。怎么求一个函数的最值?当然是求导,然后让导数为0,那么解这个方程得到的θ就是了(当然,前提是函数L(θ)连续可微)。那如果θ是包含多个参数的向量那怎么处理啊?当然是求L(θ)对所有参数的偏导数,也就是梯度了,那么n个未知的参数,就有n个方程,方程组的解就是似然函数的极值点了,当然就得到这n个参数了。
  • 最大似然估计你可以把它看作是一个反推。多数情况下我们是根据已知条件来推算结果,而最大似然估计是已经知道了结果,然后寻求使该结果出现的可能性最大的条件,以此作为估计值。比如,如果其他条件一定的话,抽烟者发生肺癌的危险时不抽烟者的5倍,那么如果现在我已经知道有个人是肺癌,我想问你这个人抽烟还是不抽烟。你怎么判断?你可能对这个人一无所知,你所知道的只有一件事,那就是抽烟更容易发生肺癌,那么你会猜测这个人不抽烟吗?我相信你更有可能会说,这个人抽烟。为什么?这就是“最大可能”,我只能说他“最有可能”是抽烟的,“他是抽烟的”这一估计值才是“最有可能”得到“肺癌”这样的结果。这就是最大似然估计。
  • 好了,极大似然估计就讲到这,总结一下:
    • 极大似然估计,只是一种概率论在统计学的应用,它是参数估计的方法之一。说的是已知某个随机样本满足某种概率分布,但是其中具体的参数不清楚,参数估计就是通过若干次试验,观察其结果,利用结果推出参数的大概值。最大似然估计是建立在这样的思想上:已知某个参数能使这个样本出现的概率最大,我们当然不会再去选择其他小概率的样本,所以干脆就把这个参数作为估计的真实值。


  • 求最大似然函数估计值的一般步骤:

(1)写出似然函数;

(2)对似然函数取对数,并整理;

(3)求导数,令导数为0,得到似然方程;

(4)解似然方程,得到的参数即为所求;

二、EM算法

  • 好了,重新回到上面那个身高分布估计的问题。现在,通过抽取得到的那100个男生的身高和已知的其身高服从高斯分布,我们通过最大化其似然函数,就可以得到了对应高斯分布的参数θ=[u, ∂]T了。那么,对于我们学校的女生的身高分布也可以用同样的方法得到了。
  • 再回到例子本身,如果没有“男的左边,女的右边,其他的站中间!”这个步骤,或者说我抽到这200个人中,某些男生和某些女生一见钟情,已经好上了,纠缠起来了。咱们也不想那么残忍,硬把他们拉扯开。那现在这200个人已经混到一起了,这时候,你从这200个人(的身高)里面随便给我指一个人(的身高),我都无法确定这个人(的身高)是男生(的身高)还是女生(的身高)。也就是说你不知道抽取的那200个人里面的每一个人到底是从男生的那个身高分布里面抽取的,还是女生的那个身高分布抽取的。用数学的语言就是,抽取得到的每个样本都不知道是从哪个分布抽取的。
  • 这个时候,对于每一个样本或者你抽取到的人,就有两个东西需要猜测或者估计的了,一是这个人是男的还是女的?二是男生和女生对应的身高的高斯分布的参数是多少?
  • 只有当我们知道了哪些人属于同一个高斯分布的时候,我们才能够对这个分布的参数作出靠谱的预测,例如刚开始的最大似然所说的,但现在两种高斯分布的人混在一块了,我们又不知道哪些人属于第一个高斯分布,哪些属于第二个,所以就没法估计这两个分布的参数。反过来,只有当我们对这两个分布的参数作出了准确的估计的时候,才能知道到底哪些人属于第一个分布,那些人属于第二个分布。
  • 这就成了一个先有鸡还是先有蛋的问题了。鸡说,没有我,谁把你生出来的啊。蛋不服,说,没有我,你从哪蹦出来啊。(呵呵,这是一个哲学问题。当然了,后来科学家说先有蛋,因为鸡蛋是鸟蛋进化的)。为了解决这个你依赖我,我依赖你的循环依赖问题,总得有一方要先打破僵局,说,不管了,我先随便整一个值出来,看你怎么变,然后我再根据你的变化调整我的变化,然后如此迭代着不断互相推导,最终就会收敛到一个解。这就是EM算法的基本思想了。
  • 不知道大家能否理解其中的思想,我再来啰嗦一下。其实这个思想无处在不啊。
    • 例如,小时候,老妈给一大袋糖果给你,叫你和你姐姐等分,然后你懒得去点糖果的个数,所以你也就不知道每个人到底该分多少个。咱们一般怎么做呢?先把一袋糖果目测的分为两袋,然后把两袋糖果拿在左右手,看哪个重,如果右手重,那很明显右手这代糖果多了,然后你再在右手这袋糖果中抓一把放到左手这袋,然后再感受下哪个重,然后再从重的那袋抓一小把放进轻的那一袋,继续下去,直到你感觉两袋糖果差不多相等了为止。呵呵,然后为了体现公平,你还让你姐姐先选了。
  • EM算法就是这样,假设我们想估计知道A和B两个参数,在开始状态下二者都是未知的,但如果知道了A的信息就可以得到B的信息,反过来知道了B也就得到了A。可以考虑首先赋予A某种初值,以此得到B的估计值,然后从B的当前值出发,重新估计A的取值,这个过程一直持续到收敛为止。
  • EM的意思是“Expectation Maximization”,在我们上面这个问题里面,我们是先随便猜一下男生(身高)的正态分布的参数:如均值和方差是多少。例如男生的均值是1米7,方差是0.1米(当然了,刚开始肯定没那么准),然后计算出每个人更可能属于第一个还是第二个正态分布中的(例如,这个人的身高是1米8,那很明显,他最大可能属于男生的那个分布),这个是属于Expectation一步。有了每个人的归属,或者说我们已经大概地按上面的方法将这200个人分为男生和女生两部分,我们就可以根据之前说的最大似然那样,通过这些被大概分为男生的n个人来重新估计第一个分布的参数,女生的那个分布同样方法重新估计。这个是Maximization。然后,当我们更新了这两个分布的时候,每一个属于这两个分布的概率又变了,那么我们就再需要调整E步……如此往复,直到参数基本不再发生变化为止。
  • 这里把每个人(样本)的完整描述看做是三元组yi={xi,zi1,zi2},其中,xi是第i个样本的观测值,也就是对应的这个人的身高,是可以观测到的值。zi1和zi2表示男生和女生这两个高斯分布中哪个被用来产生值xi,就是说这两个值标记这个人到底是男生还是女生(的身高分布产生的)。这两个值我们是不知道的,是隐含变量。确切的说,zij在xi由第j个高斯分布产生时值为1,否则为0。例如一个样本的观测值为1.8,然后他来自男生的那个高斯分布,那么我们可以将这个样本表示为{1.8, 1, 0}。如果zi1和zi2的值已知,也就是说每个人我已经标记为男生或者女生了,那么我们就可以利用上面说的最大似然算法来估计他们各自高斯分布的参数。但是它们未知,因此我们只能用EM算法。
  • 咱们现在不是因为那个恶心的隐含变量(抽取得到的每个样本都不知道是从哪个分布抽取的)使得本来简单的可以求解的问题变复杂了,求解不了吗。那怎么办呢?人类解决问题的思路都是想能否把复杂的问题简单化。好,那么现在把这个复杂的问题逆回来,我假设已经知道这个隐含变量了,哎,那么求解那个分布的参数是不是很容易了,直接按上面说的最大似然估计就好了。那你就问我了,这个隐含变量是未知的,你怎么就来一个假设说已知呢?你这种假设是没有根据的。呵呵,我知道,所以我们可以先给这个给分布弄一个初始值,然后求这个隐含变量的期望,当成是这个隐含变量的已知值,那么现在就可以用最大似然求解那个分布的参数了吧,那假设这个参数比之前的那个随机的参数要好,它更能表达真实的分布,那么我们再通过这个参数确定的分布去求这个隐含变量的期望,然后再最大化,得到另一个更优的参数,……迭代,就能得到一个皆大欢喜的结果了。
  • 这时候你就不服了,说你老迭代迭代的,你咋知道新的参数的估计就比原来的好啊?为什么这种方法行得通呢?有没有失效的时候呢?什么时候失效呢?用到这个方法需要注意什么问题呢?呵呵,一下子抛出那么多问题,搞得我适应不过来了,不过这证明了你有很好的搞研究的潜质啊。呵呵,其实这些问题就是数学家需要解决的问题。在数学上是可以稳当的证明的或者得出结论的。那咱们用数学来把上面的问题重新描述下。(在这里可以知道,不管多么复杂或者简单的物理世界的思想,都需要通过数学工具进行建模抽象才得以使用并发挥其强大的作用,而且,这里面蕴含的数学往往能带给你更多想象不到的东西,这就是数学的精妙所在啊)

三、EM算法推导

  • 假设我们有一个样本集{x(1),…,x(m)},包含m个独立的样本。但每个样本i对应的类别z(i)是未知的(相当于聚类),也即隐含变量。故我们需要估计概率模型p(x,z)的参数θ,但是由于里面包含隐含变量z,所以很难用最大似然求解,但如果z知道了,那我们就很容易求解了。
  • 对于参数估计,我们本质上还是想获得一个使似然函数最大化的那个参数θ,现在与最大似然不同的只是似然函数式中多了一个未知的变量z,见下式(1)。也就是说我们的目标是找到适合的θ和z让L(θ)最大。那我们也许会想,你就是多了一个未知的变量而已啊,我也可以分别对未知的θ和z分别求偏导,再令其等于0,求解出来不也一样吗?



  • 本质上我们是需要最大化(1)式(对(1)式,我们回忆下联合概率密度下某个变量的边缘概率密度函数的求解,注意这里z也是随机变量。对每一个样本i的所有可能类别z求等式右边的联合概率密度函数和,也就得到等式左边为随机变量x的边缘概率密度),也就是似然函数,但是可以看到里面有“和的对数”,求导后形式会非常复杂(自己可以想象下log(f1(x)+ f2(x)+ f3(x)+…)复合函数的求导),所以很难求解得到未知参数z和θ。那OK,我们可否对(1)式做一些改变呢?我们看(2)式,(2)式只是分子分母同乘以一个相等的函数,还是有“和的对数”啊,还是求解不了,那为什么要这么做呢?咱们先不管,看(3)式,发现(3)式变成了“对数的和”,那这样求导就容易了。我们注意点,还发现等号变成了不等号,为什么能这么变呢?这就是Jensen不等式的大显神威的地方。

Jensen不等式:

  • 设f是定义域为实数的函数,如果对于所有的实数x。如果对于所有的实数x,f(x)的二次导数大于等于0,那么f是凸函数。当x是向量时,如果其hessian矩阵H是半正定的,那么f是凸函数。如果只大于0,不等于0,那么称f是严格凸函数。
  • Jensen不等式表述如下:
  • 如果f是凸函数,X是随机变量,那么:E[f(X)]>=f(E[X])
  • 特别地,如果f是严格凸函数,当且仅当X是常量时,上式取等号。
  • 如果用图表示会很清晰:


  • 图中,实线f是凸函数,X是随机变量,有0.5的概率是a,有0.5的概率是b。(就像掷硬币一样)。X的期望值就是a和b的中值了,图中可以看到E[f(X)]>=f(E[X])成立。
  • 当f是(严格)凹函数当且仅当-f是(严格)凸函数。
    • Jensen不等式应用于凹函数时,不等号方向反向。


  • 回到公式(2),因为f(x)=log x为凹函数(其二次导数为-1/x2<0)。
  • (2)式中

的期望,(考虑到E(X)=∑x*p(x),f(X)是X的函数,则E(f(X))=∑f(x)*p(x)),又,所以就可以得到公式(3)的不等式了(若不明白,请拿起笔,呵呵):



  • OK,到这里,现在式(3)就容易地求导了,但是式(2)和式(3)是不等号啊,式(2)的最大值不是式(3)的最大值啊,而我们想得到式(2)的最大值,那怎么办呢?
  • 现在我们就需要一点想象力了,上面的式(2)和式(3)不等式可以写成:似然函数L(θ)>=J(z,Q),那么我们可以通过不断的最大化这个下界J,来使得L(θ)不断提高,最终达到它的最大值。



  • 见上图,我们固定θ,调整Q(z)使下界J(z,Q)上升至与L(θ)在此点θ处相等(绿色曲线到蓝色曲线),然后固定Q(z),调整θ使下界J(z,Q)达到最大值(θt到θt+1),然后再固定θ,调整Q(z)……直到收敛到似然函数L(θ)的最大值处的θ*。这里有两个问题:什么时候下界J(z,Q)与L(θ)在此点θ处相等?为什么一定会收敛?
  • 首先第一个问题,在Jensen不等式中说到,当自变量X是常数的时候,等式成立。而在这里,即:



  • 再推导下,由于

(因为Q是随机变量z(i)的概率密度函数),则可以得到:分子的和等于c(分子分母都对所有z(i)求和:多个等式分子分母相加不变,这个认为每个样例的两个概率比值都是c),则:



  • 至此,我们推出了在固定参数θ后,使下界拉升的Q(z)的计算公式就是后验概率,解决了Q(z)如何选择的问题。这一步就是E步,建立L(θ)的下界。接下来的M步,就是在给定Q(z)后,调整θ,去极大化L(θ)的下界J(在固定Q(z)后,下界还可以调整的更大)。那么一般的EM算法的步骤如下:

EM算法(Expectation-maximization):

  • 期望最大算法是一种从不完全数据或有数据丢失的数据集(存在隐含变量)中求解概率模型参数的最大似然估计方法。

EM的算法流程:

  • 初始化分布参数θ;

重复以下步骤直到收敛:

  • E步骤:根据参数初始值或上一次迭代的模型参数来计算出隐性变量的后验概率,其实就是隐性变量的期望。作为隐藏变量的现估计值:



  • M步骤:将似然函数最大化以获得新的参数值:



  • 这个不断的迭代,就可以得到使似然函数L(θ)最大化的参数θ了。那就得回答刚才的第二个问题了,它会收敛吗?
  • 感性的说,因为下界不断提高,所以极大似然估计单调增加,那么最终我们会到达最大似然估计的最大值。理性分析的话,就会得到下面的东西:



四、EM算法另一种理解

  • 坐标上升法(Coordinate ascent):



  • 图中的直线式迭代优化的路径,可以看到每一步都会向最优值前进一步,而且前进路线是平行于坐标轴的,因为每一步只优化一个变量。
  • 这犹如在x-y坐标系中找一个曲线的极值,然而曲线函数不能直接求导,因此什么梯度下降方法就不适用了。但固定一个变量后,另外一个可以通过求导得到,因此可以使用坐标上升法,一次固定一个变量,对另外的求极值,最后逐步逼近极值。对应到EM上,E步:固定θ,优化Q;M步:固定Q,优化θ;交替将极值推向最大。

五、EM的应用


更多计算机相关学习资料可以去我的个人网站:tanqingbo.cn/CSBook001/

推荐阅读:

类似的话题

  • 回答
    当然,这倒是个有趣的话题。很多人一提到“算法”或者“牛逼项目”,脑子里就涌现出各种复杂的数学模型、庞大的代码库,动辄几万行起步。但其实不然,很多时候,最简洁、最精妙的设计,反而是最能穿透人心的。它们就像武侠小说里那种,看起来轻描淡写的一招,却能以柔克刚,化解千斤重担。今天咱们就来聊聊那些代码量不多,.............
  • 回答
    《阴阳师》IP 的全新篇章,《代号 · 世界》的最新官方爆料,无疑在玩家群体中掀起了一阵不小的波澜。这次释出的信息量不小,细细品味之下,确实有许多值得深入探讨的亮点,它们不仅预示着《阴阳师》宇宙的进一步拓展,也展现了网易在探索新玩法、新体验上的野心。首先,从世界观和时代背景上看,《代号 · 世界》将.............
  • 回答
    世界上有无数令人惊叹的潜水胜地,每个地方都有其独特的魅力和水下奇观。选择“最值得推荐”是一个非常主观的问题,因为这取决于个人的潜水经验、偏好(例如,是喜欢色彩斑斓的珊瑚礁、巨大的海洋生物,还是沉船探险),以及预算和旅行时间等因素。不过,我可以为您详细介绍一些在全球范围内享有盛誉、被广泛推荐的潜水胜地.............
  • 回答
    世界上确实存在不少“两地直线距离不远,但互通必须绕一大圈”的例子,这通常是由于地理、政治、历史、经济等多种因素造成的复杂局面。以下是一些详细的例子:1. 帕米尔高原的尴尬:中国与吉尔吉斯斯坦、塔吉克斯坦(以及更广泛的中亚地区)的陆路交通 地理直线距离: 从中国新疆的伊犁哈萨克自治州西部边境,到吉.............
  • 回答
    世界上有很多大型科研项目因各种原因而失败,这些失败项目通常规模庞大、耗资巨大、目标远大,但最终未能达到预期目标。它们的失败原因也多种多样,包括技术瓶颈、预算超支、政治干预、战略失误等等。下面我将详细讲述一些比较有代表性的失败大型科研项目:1. 超级高铁(Supersonic Transport S.............
  • 回答
    到目前为止,世界上 没有一个主权国家将政党直接、正式地体现在其国旗上。国旗通常是国家主权、国家象征、历史、民族和理想的代表,而不是某个特定政党的标志。将政党标志放在国旗上,在很大程度上会与国家的统一性、超越党派的代表性原则相悖。为什么会产生这样的疑问?或者为什么人们会有这样的联想?产生这种疑问的原因.............
  • 回答
    世界上存在着许多不为人熟知,却又非常独特且重要的职业。这些职业往往服务于特定的需求或领域,其专业性和所需的技能也可能超乎寻常。下面我将为你详细介绍一些这样的职业,力求展现它们的独特性和价值:一、 专注于“声音”的职业: 电影对白剪辑师 (Dialogue Editor/ADR Editor): .............
  • 回答
    世界上有无数令人惊叹的人和事,它们以各种形式挑战着我们的认知,激发着我们的好奇心。以下我将从不同角度,尽量详细地讲述一些让我觉得不可思议的例子:一、 人类自身的潜力与成就: 极限生存者: 南极探险家们: 想象一下,在零下几十度的严寒中,在永恒的黑暗和极端的孤独中,为了科学探索而前行的.............
  • 回答
    世界上有很多极其厉害但又鲜为人知的公司,它们可能不像苹果、谷歌那样家喻户晓,但却在各自的领域拥有深厚的技术实力、庞大的市场份额或关键的行业影响力。以下列举一些,并尝试详细阐述:1. ASML(阿斯麦):半导体制造的绝对霸主 公司概况: ASML Holding N.V. 是一家荷兰公司,是全球唯.............
  • 回答
    火车站,在很多人心里或许只是一个功能性的场所,承载着旅途的开始与结束。但殊不知,在世界的某个角落,有些火车站早已超越了其本职,蜕变成了独一无二的风景,让人驻足赞叹,甚至成为旅行的目的地本身。今天,就让我们一起走进这些充满惊喜的“火车王国”,探寻那些令人难忘的有趣火车站。1. 加拿大:班夫火车站 (B.............
  • 回答
    当然,我们来聊聊世界上那些袖珍王国,它们虽然小巧,却各有千秋,历史悠久,文化独特。一、 Vatican City (梵蒂冈)说到迷你小国,怎么能不提梵蒂冈?它可是世界上面积最小的主权国家,位于意大利首都罗马的西北角,三面被罗马城环绕,一面与意大利接壤。总面积不过0.44平方公里,比咱家小区还小一点.............
  • 回答
    在世界各国的货币中,许多都因其精美的设计、丰富的文化象征以及高质量的印刷工艺而备受赞誉,被称为“高颜值”货币。这些货币不仅仅是交换的媒介,更是一张张承载着国家历史、文化、艺术和自然风光的微型画布。以下是一些被广泛认为颜值很高的货币,并会尽量详细地讲述:1. 加拿大元(CAD) 原因: 加拿大元以.............
  • 回答
    世界上有无数令人惊叹的毕业设计、毕业作品和毕业论文,它们不仅是学生们学术旅程的顶点,更是创新、才华和对未来社会贡献的有力证明。挑选“惊人”的标准可能因人而异,但通常包括其独创性、技术难度、艺术感染力、社会价值、解决问题的能力以及对现有领域的突破。以下我将列举一些在不同领域具有代表性的“惊人”毕业作品.............
  • 回答
    目前世界上绝大多数国家的国旗都带有某种图案、颜色组合或象征意义,纯色国旗的情况非常罕见,而且常常会引起一些历史或政治上的讨论。严格意义上讲,目前世界上的主权国家,其国旗并不是纯色的。不过,我们可以从历史的角度或者非常规的角度来看待这个问题,这涉及到对“纯色”的定义以及一些国家在特定时期的旗帜。为什么.............
  • 回答
    世界上不乏宏伟的工程项目,它们的设计初衷是为了改变世界、解决重大问题或展示人类的工程实力。然而,并非所有这些项目都能达到预期的目标,有些甚至以彻底失败告终。这些失败的大型工程项目通常伴随着巨额的成本超支、严重的延误、未能实现预期的功能、对环境造成破坏,甚至导致人身伤亡。以下是一些世界上著名的、失败的.............
  • 回答
    世界上至今仍有许多令人费解的悬案,它们像暗夜里的星辰,虽然遥远却从未真正熄灭,持续吸引着人们的好奇心和探究欲。这些案件往往因为信息缺失、证据模糊、时间久远而变得扑朔迷离,成为了历史长河中无法解开的谜团。一、开膛手杰克(Jack the Ripper):伦敦阴影下的幽灵提到未解悬案,开膛手杰克的名字几.............
  • 回答
    放眼世界各地,我们不难发现许多地名中巧妙地融入了表示“老”或“旧”的字眼,它们或诉说着悠久的历史,或描绘着古老的风貌,为这些地方增添了独特的韵味。这些名字背后往往承载着一段段鲜活的记忆和故事,值得我们细细品味。欧洲:历史的印记,古老的回响在欧洲这片古老而充满故事的大陆上,带有“老”或“旧”字样的地名.............
  • 回答
    在人类对食物的探索和追求中,总有一些奇特的、挑战我们日常认知的美食,它们在制作过程或食用方式上,蕴含着令人拍案叫绝的巧思与传统,甚至带着几分哲学意味。这些食物,就像是隐藏在味蕾深处的谜语,一旦解开,便会让你对“吃”这件事有了全新的理解。1. 挪威的鲨鱼肉干(Hákarl):时间与发酵的艺术这玩意儿可.............
  • 回答
    好的,让我们一起踏上一段令人惊叹的机场之旅吧!我们都会惊叹于这些不仅仅是起降飞机的地方,更是充满惊喜、历史、甚至艺术的独特空间。抛开那些冷冰冰的条条框框,我会用最鲜活的笔触,带你领略这些世界各地最不一般的机场。1. 尼泊尔 卢卡拉机场(TenzingHillary Airport):“珠峰的门户”.............
  • 回答
    好的,咱们就来聊聊那些有点让人扼腕叹息、没能把“看长远”这事儿儿做得特别到位的轨道交通规划。这些案例,往往不是出于恶意,而是当时种种现实考量下的结果,但回过头来看,确实留下了一些遗憾。1. 曾经的“一线天”与被牺牲的地下空间:以北京为例北京的轨道交通发展速度在全球都是数一数二的,但早期规划中,我们能.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有