计算机原理基础:编程语言、硬件与数学
写在前面
这篇文档是从《人工智能训练师基础》系列里拆出来的”计算机原理基础”合集,把原来散落在第五章和第七章的两块内容合并到一起讲。它们俩本来就是一个祖宗——都属于”计算机基础”范畴,编程语言那一块讲的是用 Python 写代码的语法和套路,硬件数学那一块讲的是计算机怎么组成的、底层数学是怎么回事,合在一篇里读着更连贯,也方便你集中梳理一遍。
按系列里的内容安排,编程语言是中游分量、不能放;计算机硬件和数学基础都属于”了解即可,抓关键知识点带过”的部分,不深挖,但概念点密集、容易混淆,遗忘率又高,所以也得认真过一遍,目标是”听明白、能上手”。
讲解还是老规矩——博客串讲风格,像学长给学弟讲知识,全程用”你”来称呼;不用表格、不用图表,纯文字加代码块;遇到容易记混的概念就用生活化类比,比如原文里”冯·诺依曼架构五大部件””CISC 像大菜谱、RISC 像小菜单”这种;凡是涉及具体年份、版本号这类可能记不准的信息,一律用【需网络查询确认:xxx】标出来,提示你学习时再核对。每个补充的代码和计算都给具体输入输出,让你能跟着走一遍。
下面正式开始,先讲编程语言这条主线。
第一部分 编程语言:以Python为主线
本系列以 Python 为主线,但内容里会混 Java、C++ 的基本语法,所以 Java、C++ 跟 Python 不同的地方也得知道。
Python语言特性与基础语法
Python 的几个语言特性先钉死:动态类型(变量不用声明类型,运行时确定)、强类型(不会偷偷做隐式类型转换,比如 "1" + 1 在 Python 里会直接报 TypeError,不像 JavaScript 给你偷偷转)、缩进表示代码块(不用大括号,缩进错了语法就过不去)、解释执行(不需要编译成机器码,逐行解释跑)。
基本数据类型:int(任意精度,不会溢出,不像 C/Java 的 int 有 32 位范围限制)、float、bool(True/False,注意首字母大写)、str(不可变,修改字符串其实是新建一个)、None(表示”没有值”,类似 Java 的 null 但类型不同)。容器类型:list(可变、有序)、tuple(不可变、有序)、dict(键值对,Python 3.7+ 保证插入顺序)、set(无序、不重复,常用来去重)。
is 和 == 的区别
is 比较的是身份(对象 id,也就是内存地址),== 比较的是值。这个区别很常见,光说记不住,看代码:
1 | a = [1, 2, 3] |
这里有个坑要小心——CPython 的小整数缓存机制,把 -5 到 256 这个范围的整数预先创建好复用,所以这个范围内的整数 is 比较会”巧合地”返回 True,但这不是语言保证的,是实现细节,跨实现或大数就不一定了:
1 | x = 256 |
所以判断值相等永远用 ==,判断是不是同一个对象(比如单例 None)才用 is,比如 if x is None。
可变默认参数的坑
这是 Python 最经典的坑之一,几乎是重点。函数的默认参数在函数定义时就被创建一次,之后每次调用都共用同一个对象。如果默认参数是可变类型(list、dict、set),多次调用之间会”共享状态”,结果越积越多:
1 | # 错误写法 |
正确做法是把默认值设成 None,在函数内部判断再初始化:
1 | def add_item(item, lst=None): |
记住口诀:可变默认参数要不得,用 None 兜底再初始化。
列表推导式
列表推导式是 Python 重点知识,写法是 [表达式 for 变量 in 可迭代对象 if 条件],本质是把 for 循环压成一行,可读性高、速度比显式 for+append 快。看几个常见写法:
1 | # 基本形式:求平方 |
记住一句话:能用列表推导式就别写 for+append,但要保证可读,嵌套超过两层就老老实实写循环。
面向对象三件套:封装、继承、多态
面向对象三个核心特性必须会区分、会写代码。
封装:把数据和操作数据的方法绑在一起,对外隐藏内部细节。Python 里靠下划线约定:单下划线 _name 表示”内部用,别从外面碰我”(只是约定,Python 不会真的拦你,外部照样能访问);双下划线 __name 触发名称改写(name mangling),会自动改成 _类名__name,从外部直接访问会报错,算是一种”伪私有”。Java 和 C++ 有真正的 private/protected/public 关键字,Python 没有真正的访问控制,全靠约定。
继承:子类获得父类的属性和方法。Python 写法是 class Dog(Animal):,支持多继承。Java 是单继承(一个类只能 extends 一个父类),但可以实现多个接口;C++ 支持多继承,有著名的”菱形继承”问题——D 继承 B 和 C,B、C 都继承 A,A 的成员在 D 里会有两份,靠虚继承(virtual)解决。
多态:同一个方法调用,不同对象表现不同行为。Python 是动态语言,多态是”鸭子类型”——只要对象有这个方法就能调用,不管它是什么类。Java/C++ 靠方法重写(override)和父类引用指向子类对象实现运行时多态,C++ 还需要把方法声明为 virtual 才能动态绑定,不然就是静态绑定(编译时确定调哪个)。
构造方法:Python 是 __init__(注意是初始化,不是创建,对象创建是 __new__ 干的),Java 和 C++ 是与类同名的构造函数。Python 子类调父类构造要用 super().__init__()。
一个完整类示例:封装、继承、多态一起演示
光讲概念容易混,下面这个例子把三件套一次过完:
1 | class Animal: |
名称重整示例
双下划线开头的属性会被 Python 自动改名,这个点很常见,单独拎出来看清楚:
1 | class Person: |
记忆要点:单下划线是”君子约定”挡不住,双下划线是”改名加锁”也挡不住,但能挡住无意的访问;真要私有就老老实实提供 getter/setter,靠约定遵守。
类方法、静态方法、实例方法
这三个方法要分清,装饰器是重点知识:
1 | class Counter: |
三句话区分:实例方法要 self(操作实例状态)、类方法要 cls(操作类级状态,常用作”工厂方法”)、静态方法啥都不要(就是个工具函数,逻辑上属于这个类但不需要访问实例或类)。
异常处理
Python 异常用 try/except/else/finally 结构。这块代码示例必须看一遍,把执行顺序搞清楚,因为关键就在于”哪段会跑、哪段不会跑”。
关键知识点有几条:except 的捕获顺序要从具体到一般(子类异常在前,父类 Exception 在后,否则前面的 except 永远捕不到);else 是没异常才跑(注意它和 try 后面直接写代码的区别——else 里的代码不会被自己 try 进去,更安全);finally 一定跑,哪怕 try 里有 return,finally 也会在 return 之前执行(这个容易混);抛异常用 raise ValueError("xxx"),自定义异常继承 Exception 类。
try/except/else/finally 完整示例
1 | def divide(a, b): |
finally 在 return 之前执行
这是重点知识,单独演示一遍:
1 | def test(): |
注意 Python 解释器看到 try 里有 return 时,会先把返回值算好压栈,然后跳到 finally 执行,finally 跑完才真正返回。如果 finally 里也有 return,会覆盖 try 里的 return——但这写法是反人类,别这么用。
自定义异常
1 | class MyError(Exception): |
Java 的异常分受检异常(checked,必须 try-catch 或 throws 声明,编译器强制)和非受检异常(unchecked,RuntimeException 子类,编译器不管),这是 Java 特有的概念,Python 没有这种区分,所有异常都是运行时抛出。
第二部分 计算机硬件:了解即可
硬件这块”了解即可”,抓关键知识点带过,不深挖。
冯·诺依曼架构与五大部件协作
冯·诺依曼架构是整个现代计算机的基石。它的核心思想就一句话——“存储程序”:程序和数据一起存放在存储器里,机器按地址顺序取指令、执行指令,不需要人去拨开关改线路(早期计算机真是这么干的,所以以前的”编程”叫”接线”)。
计算机由五大部分组成:运算器、控制器、存储器、输入设备、输出设备。现代 CPU 把运算器和控制器集成在一起,叫中央处理器(CPU),再加上寄存器和缓存。可以用”工厂流水线”类比这五块:
- 输入设备是”原材料入口”,键盘、鼠标、扫描仪,把外部的程序和数据送进存储器;
- 存储器是”仓库”和”配方本”,程序指令和数据都存在这里,按地址取用;
- 控制器是”车间主任”,负责从存储器取指令、译码、发出控制信号指挥其他部件干活,它本身不运算;
- 运算器(ALU)是”工人”,听控制器的指挥对数据进行算术和逻辑运算;
- 输出设备是”成品出口”,显示器、打印机,把结果送出来给人看。
五大部件的协作流程是这样的:第一步,输入设备把程序和数据送进存储器;第二步,控制器从存储器里取出一条指令(这叫”取指”);第三步,控制器译码,搞清楚这条指令要干啥(这叫”译码”);第四步,控制器发出控制信号,让运算器从存储器取数据进行运算(这叫”执行”);第五步,运算结果写回存储器;第六步,如果需要给人看,输出设备把结果送出来。然后控制器自动取下一条指令,循环往复,这就是”取指—译码—执行”的指令周期,计算机就是这么一行一行往下跑的。
这里有个重点:冯·诺依曼架构的瓶颈叫”冯·诺依曼瓶颈”——因为指令和数据共用一条总线、共用一个存储器,CPU 取指令和取数据不能同时进行,得排队。后来的哈佛架构就是把指令和数据分开存储、分开总线,能并行访问,缓解了这个瓶颈,DSP 和很多嵌入式芯片用的就是哈佛架构。
指令集架构:CISC vs RISC
指令集架构(ISA)是 CPU 能听懂的”指令字典”,分两大流派。
CISC(复杂指令集):指令多、长度可变、单条指令能干复杂的事、靠微代码实现。可以用”大菜谱”来类比——菜谱里什么菜都有,从炒青菜到佛跳墙,一道菜一道工序复杂,但厨子(CPU)看着菜谱照做就行。典型代表是 x86,由 Intel 主导,统治桌面和服务器,性能强但功耗高。
RISC(精简指令集):指令少、长度固定、每条指令只干一件简单事、单周期执行、用 Load-Store 架构(只有加载/存储指令访问内存,运算只在寄存器之间)、流水线高效、功耗低。可以用”小菜单”来类比——菜单里只有几道家常菜,每道都简单快速,但要做复杂菜得组合好几道指令,靠流水线高速叠加来弥补。典型代表是 ARM,统治移动设备和嵌入式,手机平板几乎全是 ARM。
更详细的对比可以这样记:CISC 指令数量多达几百上千条,指令长度从 1 字节到 15 字节不等,复杂指令一条能干很多活但执行周期长;RISC 指令数量通常一百多条,指令长度固定(比如 ARM 都是 4 字节),每条指令一个周期左右,靠流水线把多条简单指令快速叠加执行。CISC 的优势是程序员写汇编省事、代码密度高(同样功能指令条数少);RISC 的优势是硬件设计简单、流水线效率高、功耗低、容易做高主频。一句话区分:x86 是 CISC,性能强功耗高,电脑服务器用;ARM 是 RISC,功耗低效率高,手机嵌入式用。
现代 x86 内部其实把 CISC 指令解码成类似 RISC 的微操作(micro-ops)再执行,是”外表 CISC、内核 RISC”的混合设计,但 ISA 层面仍属 CISC。两家指令集不兼容,给 x86 编译的程序不能直接在 ARM 上跑,需要重新编译或转译(比如苹果从 Intel 换到自研 M 系列芯片时用的 Rosetta 2 就是干这个的)。【需网络查询确认:苹果 M 系列芯片具体型号与发布时间线】
存储器层次与局部性原理
存储器层次结构从快到慢、从小到大、从贵到便宜,金字塔结构层层堆叠:
最顶上是寄存器,在 CPU 内部,速度最快,纳秒级以下(大约 0.3 纳秒),容量极小,一般就几十到几百个,每个几十字节,存的是当前正在算的数据;下面是高速缓存 Cache,分 L1、L2、L3 三层——L1 在每个 CPU 核内部,几十 KB 容量,访问约 1 纳秒;L2 也常在核内但稍远一点,几百 KB,访问约 3 到 10 纳秒;L3 是所有核共享的,几 MB 到几十 MB,访问约 10 到 30 纳秒;再下面是主存内存(DRAM),几个 GB 到几十 GB,访问约 100 纳秒;最底下是外存硬盘,SSD 固态盘几百 GB 到几 TB,访问约 100 微秒(0.1 毫秒),HDD 机械盘 TB 级,访问约 10 毫秒。
把这套数字串起来记个量级:寄存器和 Cache 是”纳秒”级,主存是”百纳秒”级,SSD 是”百微秒”级,HDD 是”毫秒”级。从寄存器到 HDD,速度差了大约 10 的 7 次方倍(一千万倍),但容量也差了 10 的 9 次方倍。这就是为什么不能所有数据都放最快的地方——快的东西贵、容量小,慢的东西便宜、容量大,存储器层次结构就是在速度、容量、成本三者之间找平衡。
核心思想是”局部性原理”:时间局部性(刚用过的数据还会再用,所以最近用过的留 Cache 里)和空间局部性(用了一个地址,附近地址很可能也会用,所以一次从内存搬一块到 Cache 而不是只搬一个字节)。所以加缓存能极大提升平均访问速度。
局部性的代码示例
时间局部性看循环变量——同一段代码里同一个变量被反复读写,它就会一直留在寄存器或 L1 Cache 里,不用每次都去内存取:
1 | sum = 0 |
空间局部性看连续访问——数组在内存里是连续存放的,按顺序访问时,第一次访问某个地址会把它周围一整块都搬到 Cache,后面访问邻居就直接命中 Cache:
1 | arr = list(range(1000)) |
这就是为什么写代码访问二维数组要”按行遍历”而不是”按列遍历”——C/Java 这类行主序语言里,按行遍历是连续访问,按列遍历是跳跃访问,性能差好几个数量级。
Cache 命中率计算示例
Cache 命中率是重点知识,公式很简单:平均访问时间 = 命中率 × 缓存时间 + (1 - 命中率) × 内存时间。给个具体数字算一遍:
假设某程序运行时 Cache 命中率是 90%,Cache 访问时间 10 纳秒,未命中时要去主存取,主存访问时间 100 纳秒。那平均访问时间 = 0.9 × 10 + 0.1 × 100 = 9 + 10 = 19 纳秒。
虽然 90% 的访问都很快(10 纳秒),但那 10% 的未命中把平均时间从 10 纳秒拉到 19 纳秒,几乎翻倍。这就是为什么提升命中率这么重要——命中率从 90% 提到 99%,平均时间就从 19 纳秒降到 10.9 纳秒(0.99×10 + 0.01×100 = 9.9 + 1 = 10.9),降幅接近一半。
第三部分 数学基础:了解即可
数学也属于”了解即可”,但概念点密、容易混,且是机器学习理论的底子,不能完全放掉。
线性代数:矩阵运算与分解
矩阵运算要熟。加法要求两个矩阵同形(行数列数都一样),对应位置相加;乘法要求 A 是 m×n、B 是 n×p,结果才是 m×p,注意不满足交换律(AB ≠ BA),但满足结合律和分配律;转置是行列互换,A 是 m×n,Aᵀ 就是 n×m;逆矩阵只有方阵且行列式不为 0 才有,A·A⁻¹ = I。
矩阵乘法示例
光记公式容易混,拿个具体数字走一遍。设 A 是 2×3 矩阵、B 是 3×2 矩阵,A 乘 B 结果是 2×2:
1 | A = | 1 2 3 | B = | 7 8 | |
C = A·B 是 2×2,每个元素 C[i][j] 等于 A 的第 i 行和 B 的第 j 列对应相乘再相加:
- C[0][0] = 1×7 + 2×9 + 3×11 = 7 + 18 + 33 = 58
- C[0][1] = 1×8 + 2×10 + 3×12 = 8 + 20 + 36 = 64
- C[1][0] = 4×7 + 5×9 + 6×11 = 28 + 45 + 66 = 139
- C[1][1] = 4×8 + 5×10 + 6×12 = 32 + 50 + 72 = 154
最终结果:
1 | C = | 58 64 | |
记忆要点:A 的列数必须等于 B 的行数才能乘;结果是 A 的行数 × B 的列数;每个元素是”行乘列”的点积。注意 AB ≠ BA,你把上面 A 和 B 顺序换过来乘,结果完全不一样。
特征值分解(EVD)
对方阵 A,如果能找到特征值 λ 和特征向量 v 满足 Av = λv(意思是 A 作用在 v 上只是把它拉长或缩短 λ 倍,方向不变),那么 A 可以分解成 PDP⁻¹ 的形式,其中 D 是对角阵(对角线上是特征值)、P 的列是特征向量。
这个有什么用?最经典的应用就是主成分分析(PCA)降维。PCA 的思路是找数据方差最大的几个方向,把高维数据投到这几个方向上达到降维。具体做法是对数据的协方差矩阵做特征值分解,特征向量就是主成分方向(每个特征向量代表一个方向),特征值大小代表该方向上的方差大小——特征值越大的方向,数据散得越开,包含的信息越多。所以做 PCA 时把特征值从大到小排序,取前 K 个特征值对应的特征向量,就把数据从 n 维降到了 K 维,且保留了最多的信息。
直观理解:协方差矩阵的特征向量告诉你”数据在哪个方向上变化最剧烈”,特征值告诉你”这个方向上变化有多剧烈”。第一主成分就是变化最剧烈的方向,第二主成分次之,依此类推。比如你有一组二维数据点散布成一个椭圆形,特征值分解后两个特征向量就是椭圆的长轴和短轴方向,长轴对应的特征值大(这个方向上数据散得开)、短轴对应的特征值小,PCA 取长轴方向就把二维数据降到一维,且保留了大部分信息。
奇异值分解(SVD)
SVD 是特征值分解的”加强版”——EVD 只能对方阵做,SVD 对任意矩阵都能做。把任意 m×n 矩阵 A 分解成 UΣVᵀ 的形式,其中 U 是 m×m 正交阵(左奇异向量)、V 是 n×n 正交阵(右奇异向量)、Σ 是 m×n 对角阵(对角线上是奇异值,从大到小排列)。
应用场景有三个最常见:第一,推荐系统里的协同过滤——把”用户-物品”评分矩阵做 SVD 分解,能学到用户和物品的潜在特征,用来预测缺失的评分,比如 Netflix 那种”用户给电影打分”的稀疏矩阵就靠 SVD 补全;第二,图像压缩——把图像矩阵做 SVD,只保留前 K 个最大的奇异值,能用很少的存储近似还原图像,奇异值越大代表包含的信息越多,前几个奇异值往往就包含了图像大部分能量;第三,自然语言处理里的潜在语义分析(LSA),对”词-文档”矩阵做 SVD 找出潜在主题。PCA 在数据不是方阵时也常通过 SVD 实现,因为 SVD 比直接算特征值更稳定、更通用,数值计算库里 sklearn 的 PCA 内部其实就是用 SVD 实现的。
微积分:导数、梯度与链式法则
微积分核心是导数和梯度。导数是函数在某点的瞬时变化率,一元函数 f(x) 在 x 处的导数 f’(x) 就是切线斜率;梯度是多元函数对各变量偏导数组成的向量,方向指向函数值上升最快的方向,大小是这个最快上升率。所以梯度下降要往梯度的反方向走——你要找最小值,自然是往下降最快的方向走。
链式法则要会——复合函数求导一层层乘,比如 f(g(x)) 的导数是 f’(g(x))·g’(x)。神经网络反向传播就是靠链式法则算梯度,从输出层往输入层逐层算偏导,每层都乘上后面那层的导数。
概率论与贝叶斯定理
贝叶斯定理是重点中的重点,公式:P(A|B) = P(B|A) × P(A) / P(B)。含义:在观察到 B 发生后,A 发生的后验概率,等于(A 发生时 B 的似然 × A 的先验概率)/ B 的证据因子。
贝叶斯定理的经典计算过程
举例:某病发病率 1%(先验),检测准确率 99%(真阳性率和真阴性率都是 99%),你检测阳性,真正患病的概率是多少?这题把计算过程拆细给你看:
- 设 A = “真患病”,B = “检测阳性”
- 先验 P(A) = 0.01(发病率 1%)
- 似然 P(B|A) = 0.99(真有病的人里 99% 检测阳性)
- 还要算 P(B) 即”不管有没有病,检测阳性”的总概率,要分两种情况:
- 真有病且阳性:P(A) × P(B|A) = 0.01 × 0.99 = 0.0099
- 没病但误诊阳性:P(¬A) × P(B|¬A) = 0.99 × 0.01 = 0.0099
- 所以 P(B) = 0.0099 + 0.0099 = 0.0198
- 套贝叶斯公式:P(A|B) = 0.99 × 0.01 / 0.0198 = 0.0099 / 0.0198 = 0.5
结果只有 50%,这跟直觉差很远(直觉觉得”准确率 99% 检测阳性,那基本就是有病了”),是经典的”基础率忽视”案例,很容易踩坑。原因就是发病率太低(先验只有 1%),即使误诊率只有 1%,没病的人基数太大,误诊的绝对人数照样和真患者差不多,所以阳性里一半真一半假。这就是为什么筛查罕见病要做二次确诊——一次阳性不可靠。
正态分布与 3σ 原则
正态分布(高斯分布)是钟形曲线,由均值 μ 和方差 σ² 决定,记作 N(μ, σ²)。性质:约 68% 的数据落在 μ±σ 内,约 95% 落在 μ±2σ 内,约 99.7% 落在 μ±3σ 内,这就是 3σ 原则。
给个具体数字例子感受一下。假设某省高考数学成绩服从正态分布 N(90, 100),意思是平均分 90 分、标准差 σ=10 分(方差 100)。按 3σ 原则:
- 约 68% 的考生成绩在 90±10 = 80 到 100 分之间;
- 约 95% 的考生成绩在 90±20 = 70 到 110 分之间;
- 约 99.7% 的考生成绩在 90±30 = 60 到 120 分之间。
也就是说,成绩低于 60 或高于 120 的人是极少数(不到 0.3%),考 130 分以上的凤毛麟角。质量管理里著名的”六西格玛”就是要求缺陷率控制在 μ±6σ 之外,相当于百万分之 0.002 的缺陷率。
中心极限定理也要记:大量独立同分布随机变量之和近似服从正态分布,不管原来是什么分布。这就是为什么正态分布在统计里这么常见——很多统计量本质是大量独立小因素叠加的结果,自然就趋近正态了。
期望值 E(X) 是随机变量取值的加权平均。线性性质要记牢:E(aX + b) = aE(X) + b,Var(aX + b) = a²Var(X)(别忘了平方,这是容易混的坑——加常数 b 不影响方差,但乘常数 a 会让方差变成 a² 倍)。
梯度下降与牛顿法
梯度下降是机器学习最常用的优化方法:从初始点出发,每步沿负梯度方向走一段,步长由学习率控制。公式 θ = θ - η ∇J(θ)。学习率太大震荡发散,太小收敛慢。变体有批量梯度下降(BGD,用全部数据)、随机梯度下降(SGD,每次用一个样本)、小批量梯度下降(MBGD,每次用一小批,最常用)。改进方法有动量法、AdaGrad/RMSProp/Adam(自适应调整每个参数的学习率),Adam 是目前最常用的。
梯度下降的迭代示例
光记公式没感觉,拿一个最简单的二次函数走 3 次迭代。设目标函数 f(x) = x²,要找最小值(显然在 x=0 处)。梯度(这里就是导数)是 f’(x) = 2x。设初始点 x₀ = 2,学习率 η = 0.1:
- 初始:x₀ = 2,f(x₀) = 4
- 第一次迭代:梯度 = 2×2 = 4,x₁ = x₀ - η×梯度 = 2 - 0.1×4 = 2 - 0.4 = 1.6,f(x₁) = 2.56
- 第二次迭代:梯度 = 2×1.6 = 3.2,x₂ = 1.6 - 0.1×3.2 = 1.6 - 0.32 = 1.28,f(x₂) = 1.6384
- 第三次迭代:梯度 = 2×1.28 = 2.56,x₃ = 1.28 - 0.1×2.56 = 1.28 - 0.256 = 1.024,f(x₃) = 1.048576
可以看到每步 x 都在向 0 靠拢,f(x) 也在下降,但下降速度越来越慢——因为越接近最小值,梯度越小,步子越小,这就是梯度下降在最小值附近”龟速前进”的特性。如果学习率改成 1(太大),x₁ = 2 - 1×4 = -2,x₂ = -2 - 1×(-4) = 2,在 -2 和 2 之间来回震荡发散;如果学习率改成 1.1(超过临界值),还会越震越大。
牛顿法:为什么比梯度下降快
牛顿法用二阶导数信息(多元情况下是海森矩阵 H),迭代公式 θ = θ - H⁻¹∇J。为什么它比梯度下降快?直观理解可以这样想:
梯度下降只看”坡面往哪边斜”(一阶导数),它不知道坡面有多陡、什么时候该减速,所以只能小心翼翼地小步走,越接近谷底走得越慢;牛顿法不仅看坡面斜不斜,还看”坡面弯不弯”(二阶导数),相当于能用一个二次曲面去近似真实的损失函数,直接算出这个二次曲面的最低点跳过去,所以叫”二次收敛”——理论上一元二次函数一步就能到最低点。对一般的凸函数,牛顿法是二次收敛,而梯度下降只是一次收敛(线性收敛),所以同样精度下牛顿法需要的迭代步数少得多。
但牛顿法的代价很大:每次迭代要算海森矩阵的逆(或解一个线性方程组 H·Δθ = ∇J),海森矩阵是 n×n 的,n 是参数个数,机器学习里参数动辄上万上百万,算逆矩阵的存储和计算量都是 O(n³),根本吃不消。所以实际工程中要么用拟牛顿法(BFGS、L-BFGS)近似海森矩阵,要么干脆放弃二阶方法用 Adam 这类一阶自适应方法。需要注意:梯度下降是一阶方法(只用一阶导数),牛顿法是二阶方法;牛顿法不一定总比梯度下降好——目标函数非凸时可能收敛到鞍点,海森矩阵不可逆时还会直接挂掉。
计算机原理基础重点回顾
Python 基础语法里最容易混的:is 比 id、== 比值,判断值相等用 ==,判断 None 用 is None,别被 CPython 的小整数缓存(-5 到 256)骗了以为小整数 is 总是 True。可变默认参数是经典坑,def f(lst=[]) 会让多次调用共享同一个列表,正确写法是 lst=None 内部判断初始化。列表推导式 [x for x in ... if ...] 别写成 for+append 一长串。
面向对象三件套:封装靠下划线(单下划线约定、双下划线名称重整成 _类名__name)、继承写 class Dog(Animal):、多态在 Python 是鸭子类型。Java 单继承多接口、C++ 多继承有菱形问题靠虚继承。构造方法 Python 是 __init__(注意不是创建,创建是 __new__),子类调父类构造用 super().__init__()。三种方法别混:实例方法要 self、类方法 @classmethod 要 cls、静态方法 @staticmethod 啥都不要。
异常处理:try/except/else/finally 四段,except 顺序从具体到一般(子类在前父类在后),else 是没异常才跑(不是 try 的延续而是独立的)、finally 一定跑(哪怕 try 里有 return 也先跑 finally 再 return)。raise 抛异常、自定义异常继承 Exception。
冯·诺依曼架构五大部件:运算器、控制器、存储器、输入、输出,核心思想是”存储程序”,CPU 取指—译码—执行循环。冯·诺依曼瓶颈是指令和数据共用总线,哈佛架构分开。CISC 像”大菜谱”指令多复杂、x86 代表、桌面服务器;RISC 像”小菜单”指令少简单、ARM 代表、移动嵌入式;现代 x86 外表 CISC 内核 RISC,两家指令集不兼容。
存储器层次:寄存器(纳秒以下)→ Cache L1/L2/L3(1 到几十纳秒)→ 主存 DRAM(百纳秒级)→ SSD(百微秒级)→ HDD(毫秒级),越往下越慢越便宜越大。局部性原理:时间局部性是”刚用过的还会再用”、空间局部性是”用了附近地址也会用”,写代码按行遍历比按列遍历快就是这个道理。Cache 平均访问时间 = 命中率 × 缓存时间 + (1-命中率) × 内存时间,命中率从 90% 升到 99% 能把平均时间砍掉接近一半。
矩阵乘法:A 是 m×n、B 是 n×p,结果 m×p,注意 AB ≠ BA 不满足交换律。EVD 只能对方阵做、SVD 对任意矩阵都能做;PCA 用协方差矩阵的特征向量找主成分方向、特征值大小代表方差大小;SVD 用于推荐系统、图像压缩、LSA。贝叶斯公式 P(A|B) = P(B|A)P(A)/P(B),罕见病检测阳性真正患病率那个例子的 50% 是”基础率忽视”案例,很重要。正态分布 3σ:68% 在 μ±σ、95% 在 μ±2σ、99.7% 在 μ±3σ;中心极限定理是大量独立同分布之和近似正态。期望线性性 E(aX+b) = aE(X)+b,方差 Var(aX+b) = a²Var(X) 别忘了平方。
梯度下降是一阶方法,公式 θ = θ - η∇J,学习率太大震荡发散、太小龟速;变体 BGD 用全部、SGD 用一个、MBGD 用一小批;改进有 Momentum、RMSProp、Adam(自适应学习率,最常用)。牛顿法是二阶方法,公式 θ = θ - H⁻¹∇J,二次收敛比梯度下降快(梯度下降一次收敛),但算海森矩阵逆开销 O(n³) 不可行;非凸时可能收敛到鞍点,海森矩阵不可逆会挂;工程上用 L-BFGS 近似。一个完整计算示例记牢:f(x)=x²、x₀=2、η=0.1,三次迭代 x 从 2→1.6→1.28→1.024,每步向 0 逼近但越来越慢。