编程语言研究书库

第一个神经元 — 感知机如何在三十行代码里学会 AND 和 OR

第一个神经元 — 感知机如何在三十行代码里学会 AND 和 OR

这一章讲三件事: 一个 1957 年的算法(感知机)怎么用三十行 Rust 代码「学会」 AND 和 OR 这两个函数;它内部的三个动作(前向、算差值、更新)各自在干什么、 为什么这套动作保证越错越少;以及它的能力边界画在哪里——哪些数据它永远学不会。 读完你会拿到全书的第一条机制推导链,本组 03 章直接接在它的边界上。 不需要任何基础,遇到的生词都在当场解释。

1. 先看现象:同一个算法,学会两个不同的函数

第 1 章结尾的测试里发生了这样一件事:

作者把 OR 函数的四个真值行喂给感知机,训练——拿数据把模型里的数调对——, 测试通过——它学会了 OR。 然后他一行代码不改,把 AND 函数的四个真值行喂给同一个算法, 测试又通过了——它学会了 AND1。

这两次训练之间,变的只有数据。同一套代码自己从数据里摸出了两个完全不同的函数, 靠的是同一套代码摸出两组不同的内部数值。

这个现象的份量,原作者引用 Rosenblatt 自己的话来标定: 他称感知机为「第一台能有原创想法的机器」——意思是它能学习和推广到新情况, 而不只是执行预先写死的规则1。

本章的主线例子就是这条训练轨迹。 下面每一节都会回到它: 第 4–6 节搭出代码和数据,第 7 节把训练全程走一遍(每步都有具体数字), 第 8 节解释为什么这套更新一定有用。

顺便交代本章的组织:原书第 1 章分四节(直觉建立 → 从数据学 → 实现 → 为什么有效), 我们的拆解按同样的顺序走,只在 Rust 教学密集处放慢脚步。 如果你只想抓主线,读第 2、7、8 节即可;第 4–6 节是给动手读者的慢速通道。

2. 顶层全景:感知机的全部身家

先别管 Rust,先看清这个算法长什么样。读之前先回答一个直觉问题: 既然 AND/OR 用 if-else 两行就写完了,为什么要造一台「学」的机器? 因为 if-else 方案里,行为是你给的;而我们要的行为(比如「下一个该接什么词」) 没人给得出。感知机是通用的:它不在乎学的是 AND 还是别的什么, 只要数据能画进坐标系、答案能分成两类,同一台机器就能学—— 用一行 if-else 换一个可以装任何函数的容器,这就是下面全部零件的意义。

它身上的零件一只手数得完:

  输入 [x₁, x₂]          两个数,比如 A、B 两个开关的取值
      │
      ├─ × w₁, × w₂      各自乘以一个「权重」(weight)——学到的数
      ├─ 求和 + b         加起来,再加一个「偏置」(bias)——学到的数
      └─ 过一道开关 σ     大于 0 输出 1(是),否则输出 -1(否)

  图说:从左到右跑一遍叫「前向」(forward)。学习 = 把 w₁、w₂、b 调对。

写成一行公式(原书的写法,照抄):

  y = σ(wᵀx + b)

  wᵀx 的意思是:w 和 x 是对应位置相乘再全部加起来(两个数乘积的和)。
  比如 w = [2.1, 2.7]、x = [1, -1],wᵀx = 2.1×1 + 2.7×(−1) = −0.6。
  σ 是那道开关——书里的正式名叫**阶跃函数**(step function):输入大于 0 出 1,否则出 −1。

每个零件解决一个问题:

零件解决什么问题没它会怎样
权重 w两个输入的重要性不同(比如「两个都真才算真」的题里,B 的影响和 A 不一样)所有输入只能一视同仁
偏置 b阶跃函数的触发门槛可以整体平移(没它,过原点的线不够用)有些分界线画不出来
阶跃函数 σ要的是是非判断,不是连续数值输出没法对应「真/假」两类

学习在这台机器里就是一件事:找到一组 w 和 b,让所有训练例子都判对。 训练开始前它们是多少?不知道——这正是「学习」存在的意义2。

记号就地展开:wᵀx 到底是什么

wᵀx 这个记号后面会成千上万次出现,第一次见就拆开(原书在前言约定过记号, 这里提前兑现):w 和 x 都是向量(一串数),wᵀx 的意思是—— 对应位置相乘,再全部加起来。拿一组具体的数(为演示编的):

  w = [0.5, 2.0], x = [3.0, −1.0]
  wᵀx = 0.5×3.0 + 2.0×(−1.0) = 1.5 − 2.0 = −0.5

上标 ᵀ(转置)在这里不用深究:它只是一种约定,提醒你「w 和 x 是两个向量, 这一串运算叫加权求和」。第 6 节的感知机代码里那个 for 循环, 干的就是这件事——乘、加,循环到底。

一段历史:1957 年的起点,和一句给本章定调的引语

本章题头引了 Helen Keller 的一句话:

我渴望成就伟大而崇高的事业;但我的首要职责,是把小事当作伟大而崇高的事业来完成3。

这句引语选得用心:本章的「小事」就是 AND 和 OR——简单到每个程序员都懂, 但原书会认真对待它们,就像对待伟大任务一样。 因为从这两个小函数里长出来的机制,会一直活到最终的神经网络里。

主角感知机,1957 年由 Frank Rosenblatt 做出,1958 年发在论文 《The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain》—— 标题直译:「感知机:大脑中信息存储与组织的概率模型」4。 两个信息都在标题里:它的灵感来自神经科学和心理学(模拟生物神经元如何产生学习和认知), 以及它从一开始就自视为「学习机制」的模型,不只是电路玩具。

原书从 Rosenblatt 的遗产里挑出两个到今天仍是机器学习地基的思想5:

  1. 线性分类器:用一条直线(高维时是平面、超平面)把两类分开;
  2. 通过参数更新来学习:不直接写答案,而是反复小步调整内部数值。

本章的路线也预先交代了,分四步6:

  ① 用 AND/OR 建立直觉(第 3–4 节)
  ② 把视角从「写代码」换成「从数据学」(第 4 节)
  ③ 实现感知机,看它在数据上训练(第 5–6 节)
  ④ 退后一步,搞懂它为什么有效,顺带立起一批基础概念(第 7–8 节)

4. 热身一:AND/OR 长什么样,以及第一段 Rust

原书不直接从算法讲起,而是先让你用最笨的办法把 AND/OR 写死, 然后问:这种「写死」能不能换成「学出来」? 这个对照是本章全部内容的地基。

AND 和 OR 的真值表

两个布尔值(只能是真或假)进去,一个布尔值出来。这张表还有个值得点破的性质: 真值表是「数据即函数」的特例——逻辑课里,AND/OR 干脆就是靠真值表定义的, 表里的每一行都是完整的知识,没有隐藏的未知7。后面会反复用到这个对照: 真值表数据没有任何未知,真实数据全是未知(本组 03 章的核心话题)。

ABA AND BA OR B
真真真真
真假假真
假真假真
假假假假

AND 要两个都真才真;OR 只要有一个真就真8。 写程序时这俩就是语言自带的运算符:Rust 里 AND 是 &&,OR 是 ||9。

最小的 Rust 程序长什么样

原书的第一段代码就是上面两个函数加一个 main。 逐行说清这里第一次出现的语言零件(后面每一章都要用):

  • fn 定义函数。 名字后面括号里写参数,参数要有名字和类型(a: bool)。 Rust 的命名习惯是全小写,多个词用下划线连(叫 snake case),比如 predict_one10。
  • 返回值用 -> 类型 声明。 Rust 不用写 return:函数里最后一个不带分号的表达式 就是返回值11。
  • let 声明变量,默认不可改。 想改必须加 mut(mutable 的缩写)—— 「这个变量会被重新赋值」在 Rust 里是显式声明的,不是默认值12。
  • 同名再声明一次可以「覆盖」旧值,叫 shadowing。 不加 mut 也能「改」, 实际是造了个新变量把旧的盖住了13。
  • println! 末尾带感叹号,是宏调用——一种长得和普通调用很像的特殊代码形态, 现阶段只要知道它负责往屏幕印东西14。

把程序存成 main.rs,rustc main.rs 编译,./main 运行,屏幕上印出 true AND false = false 这类结果——行为被代码写得明明白白15。

补一层背景:Rust 程序的两辈子

对完全没碰过编译型语言的读者,补一层必要的背景。

你写的 main.rs 是给人看的源代码;rustc 把它整个翻译成机器语言, 产出一个可执行文件——从此这个程序可以离开 rustc、在任何同类机器上直接跑。 这和 Python 那种「边读边执行」的活法不同: 编译期(翻译那一刻)就是 Rust 的检查全部生效的地方——类型不对、所有权违规、 函数不存在,全部在这一刻被拦下,而不是等程序跑到那一行才炸16。

记住这个分工,后面所有「编译器替你把关」的说法都有着落: Rust 的安全性不是运行时巡逻出来的,是翻译时一次性验完的。

作者的提问就是本章的发动机: 这种明明白白写出来的行为, 能不能不写出来、改成从数据里学出来?17

5. 热身二:把真值表变成程序读得懂的数据集

要「从数据学」,先得有数据。真值表天然就是一张微型数据集:

  • 特征(feature,也叫属性、输入变量):每一行前面的列——这里的 A、B;
  • 标签(label,也叫输出变量):每一行的答案列;
  • 每一行是一个完整的例子(example);每个标签值定义一个类别(class), 本题两类:真、假18。

但程序不认「真/假」这两个字。原书做了一个选择:用 1.0 代表真,用 −1.0 代表假19。 (为什么不用 0 和 1?先卖个关子——第 6 节会看到,−1/1 让「更新方向」的数学干净得多。)

于是 OR 函数的数据集变成四行文本:

  1,1,1
  1,-1,1
  -1,1,1
  -1,-1,-1

前两列是特征,最后一列是标签:前三种情况 OR 为真,只有「假 OR 假」为假。

cargo init 与 cargo test:两条命令走天下

Rust 项目的日常由两条命令撑起20:

  • cargo init:把一个普通文件夹变成 Rust 项目——它生成 Cargo.toml (项目的清单:名字、版本、依赖列表)和目录结构;
  • cargo test:编译并运行所有测试,红绿一目了然。

后面还会陆续遇到 cargo run(编译并运行程序)、cargo build(只编译)。 这四条命令覆盖了本书的全部日常;遇到新的再就地解释。

测试先行:先写「验收标准」,再写实现

作者接下来的做法有个名字,叫测试驱动开发(Test-Driven Development): 先写一个会自动检查的测试,描述「数据加载正确应该是什么样」,再去实现加载21。 好处有两个:动手前先逼你想清输入输出长什么样;改代码后随时确认没改坏。

第一个测试做的事:把上面那段文本喂给一个叫 load_data_from_csv 的函数, 断言(用 assert_eq! 宏)返回的矩阵每一格都是对的22。 第一次跑必然失败——函数还不存在,编译器直接报错 cannot find function23。 补一个只有一行 todo!() 的空函数,编译过了,测试运行,红(FAILED,「not yet implemented」)24。 最后写出真实现,测试转绿(ok)25。

  红 → 绿 → 重构   这个循环后面每一章都在用。

数据加载器的六行,逐个零件点名

加载器的完整实现只有六行,把它和零件表对起来看(代码照原书):

fn load_data_from_csv(content: String) -> Vec<Vec<f32>> {
    content
        .lines()
        .map(|line| {
            line.split(',')
                .map(|field| field.trim().parse().unwrap())
                .collect()
        })
        .collect()
}
  content
    .lines()              按行切:得到 4 行的序列
    .map(|line| ...)     对每一行再切一次:
        split(',')        按逗号切成字段
        .map(...)         每个字段:去空白(trim) → 解析成 f32(parse) → 担保成功(unwrap)
        .collect()        收成一行 Vec<f32>
    .collect()            收成 Vec<Vec<f32>>:4 行 × 3 列

数据的流向是「从外到内再收回来」:lines 先切行,内层 map 处理每行内部, 两个 collect 各收一层26。类型写明 -> Vec<Vec<f32>> 让编译器知道 最终要收成什么——collect 自己猜不出目标形状。 回看这两类方法:.map 属于「返回一列结果、继续链式」的方法, 两个 .collect 属于「吃掉整列、给出结果」的方法,分类严丝合缝。

属性、mod tests 与 #[cfg(test)]:测试为什么住在代码文件里

第一个测试的完整模样里还有两个没解释的东西:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_loads_or_data() { /* ... */ }
}
  • mod tests { ... } 声明了一个叫 tests 的模块(module)—— 给一坨代码围起来的命名区域。Rust 的惯例是把单元测试放进被测文件里的这个模块, 和被测代码住同一屋檐下(模块的细节第 4 章再展开)27;
  • #[cfg(test)] 是一种属性(attribute):贴在代码块上的标签信息, 不改变逻辑,但影响编译。cfg(test) 的意思是「只有在跑测试时才编译这一块」—— 正常构建程序时,测试代码根本不进可执行文件28;
  • use super::*; 把外层文件的东西引进来:测试要调用 load_data_from_csv, 得先「看见」它。

把这套机制和第 4 节测试驱动开发的流程合起来看:Rust 把「测试是代码的一等居民」 做成了语言级设施——测试就在被测文件里、有编译条件、有独立运行入口(cargo test)。 写测试的摩擦小到几乎为零,这本身就是对「测试驱动」这种工作法的语言级支持。

Result 的一手实验:失败被写进了类型

parse() 的完整实验原书给过,照抄思路走一遍:

let number_string = "1.0";
let result : Result<f32, ParseFloatError> = number_string.parse();
let number: f32 = result.unwrap();

三行的意思:第一行是个文本;第二行试着做解析——把文本转成数——目标是 f32—— 返回的不是数,而是一个 Result,它要么包着「成功值」、要么包着「错误值」, 尖括号里的 f32, ParseFloatError 就是这两样东西各自的类型29; 第三行 unwrap() 是说「我担保成功,把数给我;万一错了就当场 panic」。

把 "1.0" 换成 "abc" 再跑,panic 的消息会告诉你解析失败。 注意全程没有 try/catch 那种「绕着失败走」的语法——Rust 的哲学是: 失败是返回值的一种常态,必须被看见、被处理、或者被显式担保掉(unwrap 就是那纸担保书)。

作者也交了底:教学代码里这么写,是因为「我们控制自己解析的文本」; 生产用法应该改进30。这句交代很值得学——每个 unwrap 都是一处 TODO, 意思是「这里可能 panic,我暂且担保它不会」。

测试数据串里的一个小机关

测试里那串数据的写法有个值得认的小机关:

let data = String::from(
    "1,1,1\n\
     1,-1,1\n\
     -1,1,1\n\
     -1,-1,-1\n",
);

行尾的反斜杠 \ 是续行:它告诉编译器「这串字符还没完,下一行接着来」, 于是四行文本在源码里排得整齐,运行时仍是同一个带换行符的文本块31。 \n 是换行符本身。原书还交代了一个测试惯例:测试数据尽量摆在明处—— 写测试时,让数据完整躺在读者眼前,比藏在文件里更清楚32。

cargo test 的输出里有什么

第一次成功的 cargo test,输出值得逐段认一遍33:

  Compiling play v0.1.0 (...)     ← 编译阶段:这一步过了,类型与所有权检查全过
  Running unittests main.rs (...) ← 运行阶段
  running 1 test
  test tests::it_loads_or_data ... ok   ← 绿
  test result: ok. 1 passed; 0 failed; 0 ignored; ...

三个分段对应三件事:编译(语言级检查)、运行(逻辑执行)、结果(通过数)。 前面那个红阶段(FAILED + not yet implemented)同样三段都在,只是结果段报红。 原书说:之后的章节不再贴完整测试输出,但测试会一直陪着—— 经常写在代码之前,有时紧跟其后,用来验证刚造出来的东西34。 「测试先行」从这一章的演示,变成全书的默认工作方式。

元组、for 循环与 split_at:分离输入和标签

数据加载完,下一步是把每行**最后一列(标签)和前两列(输入)**分开。 原书用一段新代码同时教了三样东西:

fn split_columns(matrix: Vec<Vec<f32>>) -> (Vec<Vec<f32>>, Vec<f32>) {
    let mut labels = Vec::new();
    let mut inputs = Vec::new();

    for row in matrix {
        let (rest, last) = row.split_at(row.len() - 1);
        labels.push(last[0]);
        inputs.push(rest.to_vec());
    }

    (inputs, labels)
}
  • for row in matrix:对这一行行数据循环,每次拿到一行;
  • let (rest, last) = ...:这叫元组(tuple)——把几个值打包成一个, 左边同样形状的括号把它拆开,一份给 rest、一份给 last。 函数签名末尾的 -> (Vec<Vec<f32>>, Vec<f32>) 说的是: 这个函数返回两个值,一对;
  • split_at(n):把向量在位置 n 切成两段——这里取「除最后一格以外」和「最后一格」, 正好就是输入和标签的分离35。

push 是往向量尾部追加。循环走完,inputs 和 labels 两个向量一一对应: 第 i 个输入的标签就是 labels[i]。从这张表到这两列,数据才算真的准备好了。

数据加载里的 Rust,以及第一个「语言即机制」时刻

真正实现只有六行,但它把 Rust 的几样主力全用上了:

  • 字符串——程序里的文本形态——按行切开,再按逗号切开,再把每格从文字转成数字, 一列操作串起来做完;

  • 这种「逐个处理一列元素」的做法叫迭代,工具本身叫迭代器(iterator), 可以链式地 .map(...).collect() 起来36。

  • 转数字可能失败(格子里万一不是数字呢?),Rust 不让失败悄悄溜过: parse() 返回一个叫 Result 的类型,它要么是「成功值」、要么是「错误值」37。

  • 代码写了 .unwrap():「我保证这里是成功值,直接给我结果;万一错了就让程序当场停掉。」 这种当场停掉叫 panic。教学代码可以这么干,生产代码不该30。

「语言即机制」时刻在这里: Result 的存在意味着「解析可能失败」这件事 被写进了类型里,编译器会盯着你处理它。Python 里你很可能忘掉这茬,直到线上炸了才想起来。 Rust 的类型系统在替你把关——这就是本章前文说过的「把别处隐含的细节摆上台面」, 这里是它第一次具体兑现。

Rust 的类型全家福:一张先混个脸熟的表

原书在第一个测试旁插了一段类型导览,后面会一个个用熟,这里先整张脸熟38:

类别成员一句话
标量(单个值)布尔 bool、整数 i32/u32 等、浮点 f32/f64、字符 char数字、真假、字母这些原子
复合(打包)元组 tuple、数组 array元组各格类型可不同;数组各格同类型、定长
切片 slice&[f32]引用一个集合的一段,不拥有数据
标准库常用String、Vec、HashMap文本、可变长数列、键值表

两个马上用得上的细节:

  • String 在 Rust 里其实是一串字节(数据存储的最小单位,8 个二进制位)加一批方法, trim()(去空白)和 parse()(转数字)就是那些方法里的两个39;

  • f64 是默认浮点(f64 的精度——小数点后的可靠位数——更高), 本书用 f32 是为了省内存、贴近机器学习的真实约束40。

记不住全表没关系——每一样第一次真正用到时,我们都会就地再解释一次。

String 在 Rust 里有两个常用形态,原书只选了其中一个:String 类型 (字节向量加一串方法);另一个是字符串切片(&str,只是借用一段文本、 不拥有它)41。眼下不用区分两者的使用场景,只要知道存在两种、本书用 String。

Vec 可变、可追加、长度随用随长;Vec::new() 造一个空的, push 往里添。HashMap 是键值对照表,后面章节用到时再认识41。

trim() 和 parse() 是两个马上要用的方法:trim 去掉文本首尾空白 (数据里「1, 1」的空格就靠它),parse 把文本转成数字42。

顺手认识两个类型

  • Vec<f32>:可变长度的数列,f32 是 32 位浮点数。 Vec<Vec<f32>> 就是「数列的数列」——按原书的约定,内层每个向量是一行, 同下标的位置构成列43。这就是后面所有矩阵运算的毛坯。
  • f32 还是 f64? Rust 默认 f64(64 位,更精确),但全书多数例子用 f32: 省内存,也更贴近机器学习实现里「内存吃紧」的真实约束40。

编译器是对手还是教练:这一章的报错文化

读这一章时,你会注意到原书对编译错误的态度很特别:不躲、不恼,一路展示。

  • 第一次 cargo test,红字 cannot find function——作者平静地解释: 我们还没写这个函数呢44;
  • todo!() 让测试从「编译失败」进级为「运行失败」(not yet implemented panic)—— 这是有意的中间状态,证明测试框架已经能跑45;
  • 第 3 章的 E0382 报错,作者干脆整段印出来,一行行解读编译器给出的建议。

这套态度值得单独点名:Rust 的报错密度高,但每一条都是编译器在替你做检查清单。 把报错当教练而不是对手,是这门语言唯一正确的打开方式—— 也和这本书「让隐藏的东西显式」的主旨完全同构。

6. 感知机的代码形状:trait、struct 与两个方法

数据能读了,现在造学习器。原书在这里教了 Rust 里最重要的两个组织工具。

用 trait 定义「一个分类器该有的行为」

作者先不急着写感知机,而是先写所有分类器都该遵守的契约:

pub trait Classifier {
    fn train(&mut self, input: Vec<Vec<f32>>, labels: Vec<f32>);
    fn classify(&self, input: Vec<f32>) -> f32;
}

trait 是 Rust 里定义「一组行为」的方式。 它的意思是: 谁声称自己是个分类器,谁就必须提供这两个方法——train(拿数据练自己,会改内部状态) 和 classify(拿一个新样本,吐出一个类别)46。

两个方法的「伙食」也值得记住:

  • train 吃两样:全体输入(Vec<Vec<f32>>,例子们的矩阵)和全体标签(Vec<f32>)—— 整批进,消化完才算练完一遍;
  • classify 只吃一样:单个输入(Vec<f32>),吐一个数——一次一问,即问即答。

训练是「批处理」,推断是「交互式」——这个不对称不是 Rust 的设计, 是机器学习本身的形状:学习必须看够例子才发生,推断对每个新样本独立成立。 trait 的签名把这个形状原样固定了下来47。

这个设计的眼光要放大看:本书终点是神经网络,但神经网络的「行为」同样是 train 和 classify 这两个动作。现在写下的 trait,到第 8 章(神经网络)还在用—— 契约先于实现,模型可以换,行为约定不变48。

train 的第一个参数是 &mut self(classify 是 &self): training 要改自己,classify 只读自己。「这个方法会不会修改对象」 被写在了方法签名上,这也是 mut 哲学的延续。

泛型:Vec<T> 里的 T 是什么

Vec<f32> 中间尖括号里的东西值得单独说破:它叫泛型(generics), 意思是「这个容器对任意类型都可用,用的时候把类型填进去」。 Vec<f32> 是「装 f32 的向量」,Vec<Vec<f32>> 就是「装『装 f32 的向量』的向量」49。

泛型是 Rust 类型系统的核心能力之一,也是本书第 4 章要自己造线性代数库的前提—— 到时候你会看到 Matrix<T> 这类写法。现在只需记住:尖括号里是类型占位, 填进去什么,整个结构就只为那种类型工作,填错了,编译器当场拦住。

用 struct 装下「学到的知识」

感知机这个类型本身朴素得可爱:

pub struct PerceptronClassifier {
    weights: Vec<f32>,
    bias: f32,
    iterations: usize,
}

struct 是把几个相关的值打包成一个类型的容器。 三个成员分工明确:

成员是什么类别
weights: Vec<f32>每个特征一个权重学到的(训练填数)
bias: f32门槛学到的
iterations: usize训练循环几遍人定的(配置)

注意那个 usize——Rust 里专管「个数、下标」的整数类型50。 最后一行把「参数」这个重要概念引出来:模型内部的数分两类, 一类是训练学出来的(权重、偏置),一类是人提前定的(迭代次数)。 人定的这类后文叫超参数(hyperparameter)——带个「参」字, 听着像更高级的参数,其实是两码事:前者从数据里学,后者学不到,只能人调。

初始值设成全零。感知机对此不敏感,零起步没问题51; 但作者特意记了一笔:权重初始化是个大题目,第 4 章起要专门对待51—— 很多算法对初始值极其敏感,感知机只是运气好。

impl 块、self 与 ::new:方法从哪来

Rust 的方法不写在 struct 里,而是写在独立的 impl 块里:

impl PerceptronClassifier {
    pub fn new(iterations: usize) -> PerceptronClassifier {
        PerceptronClassifier {
            weights: vec![],
            bias: 0.0,
            iterations,
        }
    }
}

三个要点:

  • self 是方法身处的那个对象。 train(&mut self, ...) 里的 self 就是「调用这个方法的具体分类器」;&mut 表示这个方法会改它, & 表示只读它。方法签名就是「这个方法动我不动我」的声明书;
  • ::new 用双冒号,不用点号。 点号调的是「作用在某个对象上」的方法, 双冒号调的是「挂在类型上」的方法——不需要 self,等价于别的语言里的静态方法52;
  • vec![] 是宏:一个创建向量的简写,方括号里直接填初始内容。

测试里的调用串起来就是全貌: let mut classifier = PerceptronClassifier::new(100); 造一个分类器, classifier.train(input, labels); 训练它, classifier.classify(vec![-1.0, -1.0]) 问它新样本53。 测试本身写的也是「验收标准」:四个输入各自的预期输出, 和真值表逐格对应54。

train 的三步骨架

trait 的两个方法落进 struct,train 的骨架就这几行:

for _ in 0..self.iterations {          // 外层:超参数定好的遍数
    for (input, label) in zip(&x_train, &y_train) {   // 内层:每个例子
        let prediction = self.forward(input);          // ① 前向
        let loss = self.loss(*label, prediction);      // ② 算误差
        if loss.abs() > 0.0 {
            self.update(input, loss);                  // ③ 更新(只在错了时)
        }
    }
}

zip 把输入列和标签列配成一对一对,挨个走55。

三个函数的逐行走查

骨架里调用的三个函数,逐个拆开看(代码照原书,注释是我们的):

fn forward(&self, input: &Vec<f32>) -> f32 {
    let mut weighted_sum = 0.0;
    for feat in 0..input.len() {
        weighted_sum += input[feat] * self.weights[feat];
    }
    weighted_sum += self.bias;

    if weighted_sum > 0.0 { 1.0 } else { -1.0 }
}

前向(「forward」是机器学习的通行叫法:数据从输入端流向输出端,从左到右)56。 两个细节:输入是 &Vec<f32>(借用,看一眼就还);返回值不是加权和,是过了阶跃函数的 ±1—— 模型对外只说「是/否」。

fn loss(&mut self, label: f32, prediction: f32) -> f32 {
    label - prediction
}

fn update(&mut self, input: &Vec<f32>, error: f32) {
    for feat in 0..input.len() {
        self.weights[feat] += error * input[feat];
    }
    self.bias += error;
}

loss 只做一个减法:e = 标签 − 预测。 e = 0 判对了;e = 2 表示正类被判成负;e = −2 表示负类被判成正57。 e 的符号天然带着「往哪边修」的信息——这是 −1/1 编码的回报: 若当初用 0/1,「错」只有 0 和 1 两种,分不出方向。 update 把这个方向信息花掉:每个权重按「误差——答错的量——乘以对应输入」挪动, 偏置按误差挪动58。

配合骨架里那行 if loss.abs() > 0.0 { self.update(...) }—— 只在犯错时更新,更新量正比于错多少。 三个函数加起来不到三十行, 就是感知机算法的全部实现。

三步循环,就是感知机的全部训练。下一节把三步逐个拆开,带着真数字走。

7. 主线例子:AND 数据集,五步收敛全程

现在把第 1 节的悬念走完。用 AND 数据集(注意行的顺序:先三个非全假的,再全假那行), 训练前作者故意把初始值设成差的:w = [0.1, 0.7], b = 10.0—— 教材式的不良初值,纯粹为了让你「看见」边界被一点点修好59。

OR 的五遍:相似但不相同

原书展示的是 AND 的五遍轨迹;同一台机器喂 OR 的表,走势相似—— 同样的先平移、后转角度、最后干净一遍——但具体的权重路径不同, 收敛遍数也不同60。这不奇怪:OR 的解空间和 AND 的解空间本来就不是一个形状 (OR 的负类只有 (−1,−1) 一个点,边界的「压力分布」完全不同)。

「同一算法、不同轨迹」这个观察,本身就是下一个认知台阶的入口: 既然收敛到的是「某一条能分开的线」,而不是「那条最好的线」, 就难怪原书第 2 章(本组 03 章)要追问「该要哪一条」了。

开局:一片「全是 −1」的世界

先体会一下起点。权重全零、偏置为零时:加权和永远是 0, 阶跃函数永远输出 −1——不管输入什么,模型的答案都是「否」61。 前几遍训练,就是在「全错或几乎全错」的地基上开始的: 对 AND 来说,四个例子里只有一个 (−1,−1) 的答案本来就是 −1,其余三个全错。

这就是「从数据学」的朴素起点:它不是带着一点先见起步,而是带着满盘错误起步。 学习算法的全部工作,就是把这一片 −1 的世界改成对的形状。

第 0 遍:错 3 次,权重被推着走

下面每个数字都是原书运行时打印记录里的真实输出62, 每步旁边的中间结果是我们按更新规则当场算的。

初始 w=[0.1, 0.7], b=10.0。阶跃函数的规则:加权和 > 0 判 1,否则判 −1。

例子 (x₁, x₂)标签加权和 wᵀx+b预测对错更新后 w / b
(1, 1)10.1+0.7+10 = 10.81✓不变 / 10.0
(1, −1)−10.1−0.7+10 = 9.41✗w += (−2)·(1,−1) → [−1.9, 2.7] / 8.0
(−1, 1)−11.9+2.7+8 = 12.61✗w += (−2)·(−1,1) → [0.1, 0.7] / 6.0
(−1, −1)−1−0.1−0.7+6 = 5.21✗w += (−2)·(−1,−1) → [2.1, 2.7] / 4.0

三遍错误一模一样:该判 −1 的全被判成了 1——因为偏置 10 太大,输出整体偏向「是」。 每错一次,误差 e = 标签 − 预测 = −1 − 1 = −2,更新规则 w ← w + e·x, b ← b + e 把权重往「压低加权和」的方向推一格。

日志:Iteration: 0, Error: 3, Weights: [2.1, 2.7], Bias: 462。对上了。

第 1 遍:错误搬家,权重来回翻

从 w=[2.1,2.7], b=4 出发,四个例子挨个过:

例子 (x₁, x₂)标签加权和预测对错更新后 w / b
(1, 1)12.1+2.7+4 = 8.81✓不变 / 4.0
(1, −1)−12.1−2.7+4 = 3.41✗w += (−2)·(1,−1) → [0.1, 4.7] / 2.0
(−1, 1)−1−0.1+4.7+2 = 6.61✗w += (−2)·(−1,1) → [2.1, 2.7] / 0.0
(−1, −1)−1−2.1−2.7+0 = −4.8−1✓不变 / 0.0

错 2 次,最后停在 w=[2.1, 2.7], b=0——日志 Error: 2, Weights: [2.1, 2.7], Bias: 062。 这一遍值得盯的是中间状态:更新把权重推出去、下一个错误又把它拉回来, w 在这一遍里来回翻了一次——拔河的雏形,单个点的改善会打乱别的点。

第 2 遍:斜率动了

b=0 之后,两个「恰好相反」的例子都判对了,只剩 (−1, 1):

例子 (x₁, x₂)标签加权和预测对错更新后 w / b
(1, 1)12.1+2.7+0 = 4.81✓不变 / 0.0
(1, −1)−12.1−2.7 = −0.6−1✓不变 / 0.0
(−1, 1)−1−2.1+2.7 = 0.61✗w += (−2)·(−1,1) → [4.1, 0.7] / −2.0
(−1, −1)−1−4.1−0.7−2 = −6.8−1✓不变 / −2.0

错 1 次,w 从 [2.1, 2.7] 变成 [4.1, 0.7]、b=−2——日志 Error: 162。 一个权重往下掉(2.7→0.7)、一个往上走(2.1→4.1)——线的斜率动了。 这就是原书说的:权重的变化对应边界斜率的变化63。

第 3 遍:最后一错;第 4 遍:干净通过

第 3 遍从 w=[4.1,0.7], b=−2 出发:

例子 (x₁, x₂)标签加权和预测对错更新后 w / b
(1, 1)14.1+0.7−2 = 2.81✓不变 / −2.0
(1, −1)−14.1−0.7−2 = 1.41✗w += (−2)·(1,−1) → [2.1, 2.7] / −4.0
(−1, 1)−1−2.1+2.7−4 = −3.4−1✓不变 / −4.0
(−1, −1)−1−2.1−2.7−4 = −8.8−1✓不变 / −4.0

错 1 次((1,−1) 加权和 1.4 > 0 被判成 1),更新把 w 翻回 [2.1, 2.7]、b=−462。

第 4 遍:四个例子全对,一遍下来一次更新都没发生——Error: 0。 日志打出的却是 Converged after 5 iterations(收敛于第 5 次),不是 462。 为什么?因为它没法提前知道已经对了——必须完整走过一整遍、 确认一遍里一个错都没犯,才敢宣布收敛64。这个「干净的一遍」是收敛的判定标准。

更新规则的符号直觉:三种错法三种修法

w ← w + e·x, b ← b + e 这两行,对四种「错的情形」各意味着什么(数为演示编的):

  例:某点 x = (2, 1),标签 y = 1(该判正),现在判负,e = 2。
    w₁ ← w₁ + 2×2 = +4    与 x₁ 同号且放大 → 抬高这项对加权和的贡献
    w₂ ← w₂ + 2×1 = +2    同理
    b  ← b + 2            整体门槛向「更容易判正」移
  效果:这个点(和同类点)的加权和整体抬高,朝「判正」那边推。

  若同一点标签是 −1(该判负),e = −2:三个更新全部反向,
  把这个方向对加权和的贡献往下压。

一句话:更新方向 = 「让这类点下次更容易被判对」的方向, 步长正比于输入本身的大小(所以第 8 章会说:离原点远的点被推得狠)65。 「特征和误差同号权重就增大、异号就减小」——原书这句正文, 说的就是这个表的压缩版66。

把五步摆在一起看

  遍数   犯错数   权重            偏置   这条线在干什么
  0      3       [2.1, 2.7]      4     全偏向「是」,被三个「否」往回推
  1      2       [2.1, 2.7]      0     平移(偏置从 4 到 0,斜率没变)
  2      1       [4.1, 0.7]     -2     斜率动了,转了个角度
  3      1       [2.1, 2.7]     -4     再修一次,位置基本到位
  4      0       [2.1, 2.7]     -4     干净的一遍 → 宣布收敛

三句话收拢这一节:

  1. 更新永远只在犯错时发生,更新量 = 误差 × 输入;
  2. 偏置管线的平移,权重管线的角度——日志和图里两者交替变化不是巧合, 原书专门点破:权重的变化对应边界斜率的变化63;
  3. 「收敛」不是「对了」,而是「完整走过一遍都没错」。

同一个算法,换个数据集

第 1 节说的现象现在可以解释一半了:OR 的测试把数据换成 OR 的表, 同样的循环摸出另一组 w 和 b,那组数恰好编码了 OR 的行为67。 算法不变,变的是数据;函数不写在代码里,写在学出来的数里。

第二个测试的诞生:OR 学会了,AND 跟上

测试驱动开发的循环在感知机身上又跑了一圈。 先写「验收标准」——创建一个分类器、训练、断言四个输入各自的输出68:

let mut classifier = PerceptronClassifier::new(100);
classifier.train(input, labels);

assert_eq!(classifier.classify(vec![-1.0, -1.0]), -1.0);
assert_eq!(classifier.classify(vec![ 1.0, -1.0]),  1.0);
assert_eq!(classifier.classify(vec![-1.0,  1.0]),  1.0);
assert_eq!(classifier.classify(vec![ 1.0,  1.0]),  1.0);

new(100) 里的 100 是迭代遍数——超参数,人定的。 OR 的测试转绿之后,作者顺手加了一道练习题:给 AND 也写一个测试69。 AND 的表和 OR 只差中间两行(AND 在「一真一假」时输出假,OR 输出真), 测试代码结构一模一样,数据一换就行。

结果:AND 测试一次通过,实现一行没改。67

这一节反复出现的奇迹,至此可以拆开解释了:

  函数(AND/OR)没有写进任何代码;
  代码里只有「从数据调参数」的通用机器;
  数据不同 → 调出的参数不同 → 同一台机器呈现不同的函数。

「学出来的行为住在权重和偏置里」——这句话从口号变成了你能指认的事实: OR 训练完 w=[2.1, 2.7], b=−4 这样的数;AND 训练完是另一组数。 代码是机器,参数是灵魂。

8. 为什么这套更新一定有用:决策边界与收敛定理

现象走完了,该回答「凭什么」。这一节是本章的承重墙,原书用了三层论证,我们照它的顺序接住。

第一层:把数据画出来,答案就在眼前

把每个例子画在平面坐标系上:横轴 A,纵轴 B,真值的点画圈,假值的点画三角 (这是原书图 1-2 的画法)70。AND 的图:三个圈挤在右上角,一个三角在左下角。 画一条线把圈和三角分开——分类完成。 这条线在机器学习里叫决策边界(decision boundary):模型从「预测这个」换成「预测那个」的分界面71。 分类问题的本质,就是找到那条线的位置和角度。

第二层:权重和偏置,就是那条线的参数

原书给了推导,照抄过来。二维时,决策边界就是加权和等于 0 的地方:

  wᵀx + b = w₁x₁ + w₂x₂ + b = 0

  解出 x₂:  x₂ = −(w₁/w₂)·x₁ − b/w₂

这是一条直线的标准样子:−w₁/w₂ 是斜率,−b/w₂ 是截距(线与纵轴的交点)72。

这一层把第 6 节的日志翻译成了几何:

  • 第 0→1 遍,权重 [2.1, 2.7] 没变、偏置从 4 变 0 → 斜率不变、线整体平移;
  • 第 2 遍,权重变成 [4.1, 0.7] → 斜率 −4.1/0.7 变成 −2.1/2.7,线转了个角度63。

「学习」就是这条线在平面里一边转一边挪,直到站对位置。 原书给这个过程起了个名字: 这已经是优化的雏形——搜索能消灭分类错误的参数73(第 7 章会展开成真正的优化理论)。

把日志翻译成线:斜率和截距的数值对账

第 6 节的五行日志,用上面的公式 x₂ = −(w₁/w₂)·x₁ − b/w₂ 逐遍换算, 就得到那条线每一遍的「证件照」(斜率、截距由书中日志的 w、b 算出):

遍数wb斜率 −w₁/w₂截距 −b/w₂线的动作
0[2.1, 2.7]4−0.78−1.48斜率到位,位置偏高
1[2.1, 2.7]0−0.780斜率不动,平移穿过原点
2[4.1, 0.7]−2−5.862.86陡转一个角度
3[2.1, 2.7]−4−0.781.48转回,平移到最终位置
4[2.1, 2.7]−4−0.781.48不动(干净一遍)

对照书里的图(图 1-4),每一遍的线确实如此:第 0→1 遍平移、第 2 遍陡转、第 3 遍归位74。 原书还专门点出一个细节:第 1 到第 2 遍之间、第 2 到第 3 遍之间,权重变了—— 图里斜率发生变化的,恰好就是这两步;其余步骤只有截距动75。 「权重决定边界的朝向」从一句陈述变成了可以逐遍对账的事实。

第三层:每次更新,都把这个点往对的一边推

为什么「挪一格」是朝对的方向挪?原书的证明这样走(记号照原书):

设某个点 x 的标签是 y(只能是 1 或 −1),现在被判错了—— 也就是说 y 和加权和 wᵀx + b 符号相反,它们的乘积 y(wᵀx + b) ≤ 076。

代码里的更新是 w ← w + e·x, b ← b + e,其中误差 e = y − ŷ。 对判错的点:ŷ 和 y 相反,e = y − Ŷ = 2y(两个相反数的差是 ±2)。 所以代码实际做的是 w ← w + 2y·x, b ← b + 2y—— 比教材里的标准形式 w ← w + y·x, b ← b + y 大 2 倍77。 乘个常数无所谓:每次更新都乘同一个 2,找到的分界线不变77。

看更新之后,同一个点的新加权和变成多少(把新 w、新 b 代回去展开):

  (w + yx)ᵀx + (b + y) = wᵀx + b + y(xᵀx + 1)
                          └─────┘     └──────┘
                          原来的加权和   新增的项

新增的项 y(xᵀx + 1):xᵀx + 1 恒为正(平方和加 1), 所以新项的符号和 y 一致——加权和朝 y 那一边移动了。 离原点越远的点 xᵀx 越大,推得越狠78。

更精确的说法:因为 y² = 1,更新后 y(wᵀx + b) 恰好增加了 xᵀx + 1—— 一个恒正的数79。而这个量 y(wᵀx+b) 衡量的是「这个点被判对的把握」: 它越正,点离自己的一边越远。所以:

每次犯错,算法都把这个点的把握抬高了一个确定的正数。

几何上,你可以想象成把分界线朝那个分错的点推近一格; 点对 y 同号就把它的地盘扩一点,异号就压回去80。

零点归哪边:一个任意的选择

阶跃函数有一个边界情形书里专门交代了:加权和恰好等于 0 怎么办? 规则必须二选一,书里选了 −1(判负)——理由是这纯粹是任意选择: 加权为零的点正好骑在决策边界上,而训练好的模型几乎不会恰好停在那里81。

这个「任意但要说清」的细节,是本书教法的又一例: 规范里每一处模糊的角落,都值得花一句话钉死,不然读者会自己脑补出各种规则。

决策边界在高维长什么样

本章的所有图都是二维:两个特征,一条直线。但如果特征是三个呢?一百个呢? 原书一句话带过、后面天天要用的推广是:边界这个词在更高维依然成立—— 三维时它是一张平面,d 维时它是一个 d−1 维的超平面82。 「超」没有玄学,就是「比所在空间少一维」的统称:直线之于平面是超平面, 平面之于三维空间也是。

「特征空间」(feature space)这个词也顺手认识:把所有特征当坐标轴 搭出来的空间,每个例子是其中的一个点83。本章的平面上猫狗各据一方, 就是「两个特征、二维特征空间」的样子;第 10 章的图像分类, 每个像素是一个特征,特征空间有几万维——那里没有图可画, 但「找分界」这件事的结构一模一样。

第三层论证还藏着一个对下文的预告。把「点离边界多近」这个概念单独拎出来, 它有个名字——间隔(margin):点就是站在边界两边、按 y(wᵀx+b) 量出来的距离。 感知机的更新只关心「站错边没有」,完全不关心「站得多开」; 把「站得开」变成优化目标本身,正是下一章支持向量分类器的出发点84。 可以说,下一章的全部内容,都是从「拔河只分对错、不分远近」这个缺口长出来的。

收敛定理:这场拔河一定结束

最后一块拼图:更新会改善触发它的那个点,但可能恶化别的点—— 所以这不是「每次全体变好」的单调故事,而是一场点与点之间的拔河80。

感知机收敛定理说的就是这个拔河的结局: 只要数据能被一条直线分开(线性可分),这场拔河保证在有限次更新内结束, 算法一定能找到一条分界线80。

这句话的边界和本组 03 章直接相关:数据不能被直线分开时,定理不适用, 拔河永远打不完——感知机会在几个错误之间震荡,永远停不下来。 XOR 就是最小的反例(那里详讲)。

9. 感知机的天花板:章末自己交代的两个伏笔

第 1 章在结尾自己划了边界,留给下一章:

  1. 它学不会非线性可分的数据。 章末明说:数据不能被直线分开时,感知机失败; 「非线性」——数据和答案之间的关系画不成直线——说的就是这个85。

  2. 它的解「能用」但未必「好用」。 这一条章末没展开,但日志里已经埋了线索: 第 4 遍之后 w=[2.1, 2.7], b=−4,AND 的训练数据其实还能被无数条线分开, 感知机找到哪条算哪条——对新数据的泛化——学来的本领用在没见过的新数据上——好坏, 它一概不管。本组 03 章的全部内容,就是从这条线索长出来的。

Rosenblatt 当年的豪情(「第一台能有原创想法的机器」)和他身后的历史, 原书用一句话收束:正是这些局限,推着后世的科学家走向更强的技术, 最终通向现代机器学习和今天的 AI85。

下一章的两个预告(第 1 章自己埋的)

本章结尾,原书预告了第 2 章的两件事86:

  1. 把感知机用到「没见过的数据」上。 AND/OR 是数据即函数的特例; 下一章换真实样本,看感知机的分类能不能推广到训练集之外;
  2. 亲眼看它的两个局限发作。 不可分数据上的失败、以及「勉强可分」时的挣扎, 都会实跑出来。

这两个预告都兑现了:本组 03 章的前两节,就是它们的完整展开。

10. 作者的判断与证据

书里有明确证据的:

  • 五步训练日志与更新规则逐点吻合(我们上面每步验算过)—— 「偏置平移、权重转角度」「干净一遍才算收敛」这些说法,书里都有原文对应626463;
  • 收敛定理的增量论证 y(wᵀx+b) 增加 xᵀx+1,书里有完整推导79;
  • 「严格来说我们算的不是正式损失函数,只是误差项」——作者自己声明87, 正式的、光滑的损失函数第 2 章才登场(那就是 hinge loss)。
  • 全零初始化的安全性有前提:只对感知机成立,作者明确把「权重初始化是大题目、 第 4 章专门对待」写进了正文51——没有把特例当成普适规律,这种分寸感值得学。

作者没展开、我们替读者标出来的:

  • 为什么用 −1/1 而不是 0/1 编码标签?书里没有明说。我们的补充(不在书里,来自通用知识): 0/1 会让「更新方向」的符号分析变别扭(误差 e 只有 0/1 两种,无法区分「错在多判了」还是「错在少判了」), −1/1 下 e = ±2 天然带方向。这个选择是后续一切推导的前提,读者最好心里有数。
  • 感知机「在哪个例子顺序下先收敛到哪个解」受数据顺序影响——书没提; 第 2 章的「打乱数据」会回来补这一课。

判断(我们的,不是书里的): 第 1 章的教学设计里,最值钱的是那个 「故意不良初值」的安排——从零出发两趟就收敛,什么都看不见; 从 [0.1, 0.7], b=10 出发,五趟里平移和旋转交替出现,决策边界的每个动作 都被迫显形。讲机制时,「让错误慢慢发生」比「演示正确答案」有效得多, 这个手法值得任何写教程的人抄走。 如果错,会错在: 如果学习者是「只想跑通」型,这种刻意减速的安排会显得拖沓—— 但对以「读懂机制」为目标的读者,它恰是最短路径。这本书的目标读者是后者。

判断(我们的,不是书里的): 第 1 章的教学顺序(直觉 → 数据 → 实现 → 理论) 是「机制类课程」的优等生排法:每一步都踩在前一步的看得见成果上, 理论最后登场时,读者手里已经有了一批亲手跑出来的现象可以对账。 反面教材是「先三个月数学再写代码」的传统排法——理论先行对自学者是劝退率最高的安排。 如果错,会错在: 这个顺序对「目标是从零实现」的读者最优; 如果读者的目标是「先建立理论框架再动手」,理论殿后可能显得拖沓—— 但这本书的读者画像(写代码的人)恰好站在前一阵营。

教材用词之辨:它叫 loss,其实还不是 loss

一个容易被滑过去的较真:代码里那个函数名叫 loss,但原书主动承认—— 它算的严格说是误差项(error),不是正式的损失函数: 真正的损失函数是「预测和目标之间光滑的差异度量」,感知机这一步只用 「错了没有、错在哪边」88。

为什么这个较真值得写进拆解?因为「损失函数」是后面每一章的中心物件, 第 2 章 hinge loss 一出场就是真损失——它是连续的、有梯度的、能衡量「差多少」的。 回头看感知机的「误差项」,它既不连续(对了是 0,错了跳到 ±2), 也没有「差多少」的细腻度。两步之差,恰好是「拍脑袋更新」和「梯度下降更新」的分界线。

记这个演化的轨迹:

  第 1 章:误差项 e = y − ŷ     只在错时非零,不连续
  第 2 章:hinge loss            连续、分段、有梯度
  (未放出章节:更光滑的损失,如交叉熵)

11. 边界与局限

  • 只对线性可分数据有效。 收敛定理的前提写死在定理里; XOR 这类数据上它永远震荡,这不是 bug,是模型类的表达力上限85。
  • 找到哪个解有随机性。 解不唯一,感知机对「要哪个」没有偏好—— 泛化能力参差不齐,这是第 2 章「最大间隔」要修的第一个缺陷。
  • 阶跃函数太硬。 它(大于 0 出 1,否则出 −1)不可微分、不光滑, 没法用「往下降最快的方向挪」这种高级优化——这逼着第 2 章换上更光滑的 loss, 也是后面神经网络要换激活函数——加权和之后那道把结果压弯的变换——的伏笔。
  • 教学代码不是生产代码。 书里的 unwrap()(解析失败就 panic)对受控的教学数据可以接受, 作者自己注明生产环境要改进30。照抄到真实项目前记得换掉。
  • Early release 痕迹。 章首照例申明「这是未编辑稿、配书仓库稍后开放」89; 引用时锚定的是 2026-07-23 这一版。
  • 迭代遍数 100 的来历没有交代。 new(100) 里的 100 是「够用」的经验值: AND/OR 五步就收敛,100 只是保险上限。书里没讨论「遍数设多少」的方法论, 这个空白要等未放出的第 3 章(验证与测试)来填。
  • 例子的规模极小(每类 4 个例子)。 这是教学上的清醒选择——小到能手算对账, 但也意味着本章所有「它学会了」的结论都不涉及统计意义上的泛化: 四行数据的 AND 与四万张图的猫狗,之间隔着整本书还没走的路程。
  • 顺序敏感没有给出衡量。 本章的例子顺序固定且有利(错误均匀分布在前几行); 换个顺序,收敛遍数会变——书里没演示,只把它留给第 2 章「打乱」那一课。

12. 可带走的(合上这章,你应该带走了)

  1. 感知机 = 一行公式 y = σ(wᵀx + b) + 训练三步:前向(算加权和过阶跃函数)、 误差(e = y − ŷ)、更新(w ← w + e·x,b ← b + e,只在犯错时)。
  2. −1/1 编码标签;权重学重要性、偏置定门槛;阶跃函数 σ 负责把连续值切成两类。
  3. 决策边界 = wᵀx + b = 0;偏置管平移、权重管角度——看训练日志就能读出线的动作。
  4. 收敛 = 完整走过一遍数据零更新;收敛定理保证线性可分时有限步内结束。
  5. 更新的方向是对的:每次犯错,该点的 y(wᵀx+b) 增加 xᵀx + 1,把握抬高一个正数; 代价是可能拉低别的点——所以是拔河,不是单调变好。
  6. 代码里学到的数(权重、偏置)和人定的数(迭代次数)是两类:后者叫超参数。
  7. Rust 侧:trait 定义行为契约(train/classify),struct 装学到的数, mut/&/&mut 把「改不改、谁负责」写进类型;Result 让失败无法被忽略。
  8. 测试驱动开发的红→绿循环:先写验收标准,再写实现。
  9. 感知机的两个天花板:非线性数据学不会;会学的解不挑好坏。下一章全是为了修它们。
  10. 报错是教练不是对手:Rust 的每个编译错误都是一次免费检查, 把红字读完再动手改,是这门语言唯一正确的节奏。

13. 原文地图

主题原书章原文位置
感知机的历史(1957/1958,Rosenblatt)Chapter 1. The First Neurontext/05-ch01-chapter-1-the-first-neuron.txt:15(搜「1957」)
Helen Keller 引语同上text/05-ch01-chapter-1-the-first-neuron.txt:12(搜「great and noble task」)
测试数据串的续行写法同上text/05-ch01-chapter-1-the-first-neuron.txt:185(搜「x[3], [-1.0, -1.0, -1.0]」)
cargo test 输出的三段同上text/05-ch01-chapter-1-the-first-neuron.txt:310(搜「test result: ok」)
更新规则的符号说明同上text/05-ch01-chapter-1-the-first-neuron.txt:545(搜「same sign」)
OR 测试的相似模式同上text/05-ch01-chapter-1-the-first-neuron.txt:612(搜「similar pattern for the OR test」)
本章聚焦的两个思想同上text/05-ch01-chapter-1-the-first-neuron.txt:17(搜「linear classifiers and learning via parameter updates」)
AND/OR 定义同上text/05-ch01-chapter-1-the-first-neuron.txt:25(搜「returns True if both」)
Rust 命名约定同上text/05-ch01-chapter-1-the-first-neuron.txt:53(搜「snake case」)
最后一个表达式即返回值同上text/05-ch01-chapter-1-the-first-neuron.txt:57(搜「last expression」)
变量默认不可变 / mut同上text/05-ch01-chapter-1-the-first-neuron.txt:63(搜「immutable」)
shadowing同上text/05-ch01-chapter-1-the-first-neuron.txt:73(搜「shadowing」)
宏调用 println!同上text/05-ch01-chapter-1-the-first-neuron.txt:82(搜「macro calls」)
「能不能从数据学」之问同上text/05-ch01-chapter-1-the-first-neuron.txt:88(搜「without having to specify that behavior ourselves」)
特征 / 标签 / 例子 / 类别同上text/05-ch01-chapter-1-the-first-neuron.txt:154(搜「complete example or instance」)
测试驱动开发同上text/05-ch01-chapter-1-the-first-neuron.txt:158(搜「Test-Driven Development」)
−1.0 / 1.0 编码真与假同上text/05-ch01-chapter-1-the-first-neuron.txt:202(搜「represent the values False and True」)
行 = 例子的约定同上text/05-ch01-chapter-1-the-first-neuron.txt:208(搜「represents a row in the matrix」)
f32 的选择同上text/05-ch01-chapter-1-the-first-neuron.txt:206(搜「32 bits」)
编译错误:函数不存在同上text/05-ch01-chapter-1-the-first-neuron.txt:225(搜「cannot find function」)
todo!() 与红→绿同上text/05-ch01-chapter-1-the-first-neuron.txt:269(搜「not yet implemented」)
迭代器与链式变换同上text/05-ch01-chapter-1-the-first-neuron.txt:286(搜「two main types of methods」)
Result 与 unwrap / panic同上text/05-ch01-chapter-1-the-first-neuron.txt:290(搜「Result<T,E>」);:292(搜「called a panic」)
感知机是线性二分类器同上text/05-ch01-chapter-1-the-first-neuron.txt:319(搜「linear binary classifier」)
权重与偏置的角色同上text/05-ch01-chapter-1-the-first-neuron.txt:323(搜「These are the weights」);:327(搜「step function」)
「学习 = 找到对的权重和偏置」同上text/05-ch01-chapter-1-the-first-neuron.txt:333(搜「getting to the values of weights and biases」)
Classifier trait 的设计意图同上text/05-ch01-chapter-1-the-first-neuron.txt:337(搜「the same two behaviors」)
超参数概念(iterations)同上text/05-ch01-chapter-1-the-first-neuron.txt:430(搜「play a crucial role」)
零初始化对感知机无害同上text/05-ch01-chapter-1-the-first-neuron.txt:434(搜「set everything to zero」)
训练三步同上text/05-ch01-chapter-1-the-first-neuron.txt:513(搜「composed of three steps」)
前向的定义同上text/05-ch01-chapter-1-the-first-neuron.txt:519(搜「if the weighted sum is bigger than zero」)
零点判负的选择同上text/05-ch01-chapter-1-the-first-neuron.txt:529(搜「At exactly zero」)
误差项 e = y − ŷ同上text/05-ch01-chapter-1-the-first-neuron.txt:533(搜「always produces −1.0」)
不是正式损失函数同上text/05-ch01-chapter-1-the-first-neuron.txt:539(搜「formal loss function」)
更新规则的方向性同上text/05-ch01-chapter-1-the-first-neuron.txt:545(搜「same sign」)
同一个算法学会 AND同上text/05-ch01-chapter-1-the-first-neuron.txt:578(搜「The SAME algorithm」)
决策边界的定义同上text/05-ch01-chapter-1-the-first-neuron.txt:590(搜「separates different classes」)
故意的不良初值同上text/05-ch01-chapter-1-the-first-neuron.txt:598(搜「0.1, 0.7」)
训练日志(五个迭代)同上text/05-ch01-chapter-1-the-first-neuron.txt:622(搜「Iteration: 0, Error: 3」)
属性、#[cfg(test)] 与 mod tests同上text/05-ch01-chapter-1-the-first-neuron.txt:190(搜「mod tests」);:192(搜「metadata annotations」)
类型全家福同上text/05-ch01-chapter-1-the-first-neuron.txt:200(搜「four primary scalar types」)
迭代器的两类方法同上text/05-ch01-chapter-1-the-first-neuron.txt:286(搜「two main types of methods」)
forward 的逐行实现同上text/05-ch01-chapter-1-the-first-neuron.txt:467(搜「fn forward」)
更新函数的实现同上text/05-ch01-chapter-1-the-first-neuron.txt:488(搜「fn update」)
Classifier trait 的签名同上text/05-ch01-chapter-1-the-first-neuron.txt:341(搜「pub trait Classifier」)
PerceptronClassifier 结构体同上text/05-ch01-chapter-1-the-first-neuron.txt:410(搜「pub struct PerceptronClassifier」)
本章的四个小节路线同上text/05-ch01-chapter-1-the-first-neuron.txt:19(搜「step by step approach」)
usize 类型同上text/05-ch01-chapter-1-the-first-neuron.txt:413(搜「iterations: usize」)
训练数据非空的防御同上text/05-ch01-chapter-1-the-first-neuron.txt:443(搜「Training data cannot be empty」)
zip 对齐输入与标签同上text/05-ch01-chapter-1-the-first-neuron.txt:449(搜「zip(&x_train, &y_train)」)
干净一遍才算收敛同上text/05-ch01-chapter-1-the-first-neuron.txt:631(搜「completes one full pass」)
Result 的显式实验同上text/05-ch01-chapter-1-the-first-neuron.txt:296(搜「let number_string」)
split_columns 与元组同上text/05-ch01-chapter-1-the-first-neuron.txt:389(搜「split_columns」)
斜率与截距的公式同上text/05-ch01-chapter-1-the-first-neuron.txt:641(搜「defines the slope」)
权重变化对应斜率变化同上text/05-ch01-chapter-1-the-first-neuron.txt:629(搜「determine the orientation」)
已经是优化的雏形同上text/05-ch01-chapter-1-the-first-neuron.txt:645(搜「already a form of optimization」)
判错点的符号分析同上text/05-ch01-chapter-1-the-first-neuron.txt:647(搜「sign disagrees」)
代码更新是标准形式的 2 倍同上text/05-ch01-chapter-1-the-first-neuron.txt:655(搜「scaled by a factor of two」)
更新后加权和的展开式同上text/05-ch01-chapter-1-the-first-neuron.txt:653(搜「moves the weight vector」);:661(搜「increases by exactly」)
拔河与收敛定理同上text/05-ch01-chapter-1-the-first-neuron.txt:663(搜「perceptron convergence theorem」)
「原创想法的机器」与局限伏笔同上text/05-ch01-chapter-1-the-first-neuron.txt:671(搜「original idea」);:673(搜「important limitations」)
early release 声明同上text/05-ch01-chapter-1-the-first-neuron.txt:8(搜「GitHub repo will be made active later on」)

Footnotes

  1. 出处:「Chapter 1. The First Neuron」第 578 段(text/05-ch01-chapter-1-the-first-neuron.txt:578,搜「The SAME algorithm」)与第 671 段(搜「original idea」)。 ↩ ↩2

  2. 出处:「Chapter 1. The First Neuron」第 333 段(text/05-ch01-chapter-1-the-first-neuron.txt:333,搜「getting to the values of weights and biases」)。 ↩

  3. 出处:「Chapter 1. The First Neuron」第 12 段(text/05-ch01-chapter-1-the-first-neuron.txt:12,搜「great and noble task」)。 ↩

  4. 出处:「Chapter 1. The First Neuron」第 15 段(text/05-ch01-chapter-1-the-first-neuron.txt:15,搜「1957」)。 ↩

  5. 出处:「Chapter 1. The First Neuron」第 17 段(text/05-ch01-chapter-1-the-first-neuron.txt:17,搜「linear classifiers and learning via parameter updates」)。 ↩

  6. 出处:「Chapter 1. The First Neuron」第 19 段(text/05-ch01-chapter-1-the-first-neuron.txt:19,搜「step by step approach」)。 ↩

  7. 出处:「Chapter 2. The Search for Structure」第 17 段(text/06-ch02-chapter-2-the-search-for-structure.txt:17,搜「data and function coincide」)。 ↩

  8. 出处:「Chapter 1. The First Neuron」第 25 段(text/05-ch01-chapter-1-the-first-neuron.txt:25,搜「returns True if both」)。 ↩

  9. 出处:「Chapter 1. The First Neuron」第 59 段(text/05-ch01-chapter-1-the-first-neuron.txt:59,搜「The boolean operator for AND is &&」)。 ↩

  10. 出处:「Chapter 1. The First Neuron」第 53 段(text/05-ch01-chapter-1-the-first-neuron.txt:53,搜「snake case」)。 ↩

  11. 出处:「Chapter 1. The First Neuron」第 57 段(text/05-ch01-chapter-1-the-first-neuron.txt:57,搜「last expression」)。 ↩

  12. 出处:「Chapter 1. The First Neuron」第 63 段(text/05-ch01-chapter-1-the-first-neuron.txt:63,搜「variables in Rust are immutable」)。 ↩

  13. 出处:「Chapter 1. The First Neuron」第 73 段(text/05-ch01-chapter-1-the-first-neuron.txt:73,搜「shadowing」)。 ↩

  14. 出处:「Chapter 1. The First Neuron」第 82 段(text/05-ch01-chapter-1-the-first-neuron.txt:82,搜「macro calls」)。 ↩

  15. 出处:「Chapter 1. The First Neuron」第 44 段(text/05-ch01-chapter-1-the-first-neuron.txt:44,搜「rustc main.rs」)。 ↩

  16. 出处:「Chapter 1. The First Neuron」第 51 段(text/05-ch01-chapter-1-the-first-neuron.txt:51,搜「compiled to a machine language」)。原文:「These files are then compiled to a machine language and can be executed by the machine.」 ↩

  17. 出处:「Chapter 1. The First Neuron」第 88 段(text/05-ch01-chapter-1-the-first-neuron.txt:88,搜「without having to specify that behavior ourselves」)。 ↩

  18. 出处:「Chapter 1. The First Neuron」第 154 段(text/05-ch01-chapter-1-the-first-neuron.txt:154,搜「complete example or instance」)与第 156 段(搜「Each output value defines a class」)。 ↩

  19. 出处:「Chapter 1. The First Neuron」第 202 段(text/05-ch01-chapter-1-the-first-neuron.txt:202,搜「represent the values False and True」)。 ↩

  20. 出处:「Chapter 1. The First Neuron」第 214 段(text/05-ch01-chapter-1-the-first-neuron.txt:214,搜「cargo init」)。 ↩

  21. 出处:「Chapter 1. The First Neuron」第 158 段(text/05-ch01-chapter-1-the-first-neuron.txt:158,搜「Test-Driven Development」)。 ↩

  22. 出处:「Chapter 1. The First Neuron」第 170 段(text/05-ch01-chapter-1-the-first-neuron.txt:170,搜「fn it_loads_or_data」)。 ↩

  23. 出处:「Chapter 1. The First Neuron」第 225 段(text/05-ch01-chapter-1-the-first-neuron.txt:225,搜「cannot find function」)。 ↩

  24. 出处:「Chapter 1. The First Neuron」第 257 段(text/05-ch01-chapter-1-the-first-neuron.txt:257,搜「panicked at main.rs」)。 ↩

  25. 出处:「Chapter 1. The First Neuron」第 301 段(text/05-ch01-chapter-1-the-first-neuron.txt:301,搜「cargo test again」)。 ↩

  26. 出处:「Chapter 1. The First Neuron」第 275 段(text/05-ch01-chapter-1-the-first-neuron.txt:275,搜「fn load_data_from_csv」)。 ↩

  27. 出处:「Chapter 1. The First Neuron」第 190 段(text/05-ch01-chapter-1-the-first-neuron.txt:190,搜「mod tests」)。 ↩

  28. 出处:「Chapter 1. The First Neuron」第 192 段(text/05-ch01-chapter-1-the-first-neuron.txt:192,搜「metadata annotations」)。 ↩

  29. 出处:「Chapter 1. The First Neuron」第 296 段(text/05-ch01-chapter-1-the-first-neuron.txt:296,搜「let number_string」)。原书用三行显式代码展示 parse → Result → unwrap 的完整链路。 ↩

  30. 出处:「Chapter 1. The First Neuron」第 292 段(text/05-ch01-chapter-1-the-first-neuron.txt:292,搜「called a panic in Rust」)。 ↩ ↩2 ↩3

  31. 出处:「Chapter 1. The First Neuron」第 185 段(text/05-ch01-chapter-1-the-first-neuron.txt:185,搜「x[3], [-1.0, -1.0, -1.0]」)。 ↩

  32. 出处:「Chapter 1. The First Neuron」第 379 段(text/05-ch01-chapter-1-the-first-neuron.txt:379,搜「explicitly in front of us」)。 ↩

  33. 出处:「Chapter 1. The First Neuron」第 310 段(text/05-ch01-chapter-1-the-first-neuron.txt:310,搜「test result: ok」)。 ↩

  34. 出处:「Chapter 1. The First Neuron」第 315 段(text/05-ch01-chapter-1-the-first-neuron.txt:315,搜「won’t be pasting full test outputs」)。 ↩

  35. 出处:「Chapter 1. The First Neuron」第 389 段(text/05-ch01-chapter-1-the-first-neuron.txt:389,搜「fn split_columns」);元组的说明见第 404 段(搜「collection of values that can have different types」)。 ↩

  36. 出处:「Chapter 1. The First Neuron」第 286 段(text/05-ch01-chapter-1-the-first-neuron.txt:286,搜「two main types of methods」)。 ↩

  37. 出处:「Chapter 1. The First Neuron」第 290 段(text/05-ch01-chapter-1-the-first-neuron.txt:290,搜「Result<T,E>」)。 ↩

  38. 出处:「Chapter 1. The First Neuron」第 200 段(text/05-ch01-chapter-1-the-first-neuron.txt:200,搜「four primary scalar types」)。 ↩

  39. 出处:「Chapter 1. The First Neuron」第 202 段(text/05-ch01-chapter-1-the-first-neuron.txt:202,搜「trim() and parse()」)。 ↩

  40. 出处:「Chapter 1. The First Neuron」第 206 段(text/05-ch01-chapter-1-the-first-neuron.txt:206,搜「32 bits」)。 ↩ ↩2

  41. 出处:「Chapter 1. The First Neuron」第 200 段(text/05-ch01-chapter-1-the-first-neuron.txt:200,搜「HashMaps」)。 ↩ ↩2

  42. 出处:「Chapter 1. The First Neuron」第 202 段(text/05-ch01-chapter-1-the-first-neuron.txt:202,搜「trim() and parse()」)。 ↩

  43. 出处:「Chapter 1. The First Neuron」第 208 段(text/05-ch01-chapter-1-the-first-neuron.txt:208,搜「represents a row in the matrix」)。 ↩

  44. 出处:「Chapter 1. The First Neuron」第 225 段(text/05-ch01-chapter-1-the-first-neuron.txt:225,搜「cannot find function」)。 ↩

  45. 出处:「Chapter 1. The First Neuron」第 269 段(text/05-ch01-chapter-1-the-first-neuron.txt:269,搜「not yet implemented」)。 ↩

  46. 出处:「Chapter 1. The First Neuron」第 339 段(text/05-ch01-chapter-1-the-first-neuron.txt:339,搜「using Traits」)。 ↩

  47. 出处:「Chapter 1. The First Neuron」第 341 段(text/05-ch01-chapter-1-the-first-neuron.txt:341,搜「pub trait Classifier」)。 ↩

  48. 出处:「Chapter 1. The First Neuron」第 337 段(text/05-ch01-chapter-1-the-first-neuron.txt:337,搜「the same two behaviors」)——「后来训练神经网络分类图像时,同样是这两个行为」。 ↩

  49. 出处:「Chapter 1. The First Neuron」第 204 段(text/05-ch01-chapter-1-the-first-neuron.txt:204,搜「generics notation」)。 ↩

  50. 出处:「Chapter 1. The First Neuron」第 413 段(text/05-ch01-chapter-1-the-first-neuron.txt:413,搜「iterations: usize」)。 ↩

  51. 出处:「Chapter 1. The First Neuron」第 434 段(text/05-ch01-chapter-1-the-first-neuron.txt:434,搜「set everything to zero」)。 ↩ ↩2 ↩3

  52. 出处:「Chapter 1. The First Neuron」第 385 段(text/05-ch01-chapter-1-the-first-neuron.txt:385,搜「static method」)。 ↩

  53. 出处:「Chapter 1. The First Neuron」第 370 段(text/05-ch01-chapter-1-the-first-neuron.txt:370,搜「PerceptronClassifier::new(100)」)。 ↩

  54. 出处:「Chapter 1. The First Neuron」第 359 段(text/05-ch01-chapter-1-the-first-neuron.txt:359,搜「it_classifies_or_inputs」)。 ↩

  55. 出处:「Chapter 1. The First Neuron」第 509 段(text/05-ch01-chapter-1-the-first-neuron.txt:509,搜「vector of tuples」)。 ↩

  56. 出处:「Chapter 1. The First Neuron」第 517 段(text/05-ch01-chapter-1-the-first-neuron.txt:517,搜「going from left to right」)。 ↩

  57. 出处:「Chapter 1. The First Neuron」第 533 段(text/05-ch01-chapter-1-the-first-neuron.txt:533,搜「regardless of the input」)。 ↩

  58. 出处:「Chapter 1. The First Neuron」第 492 段(text/05-ch01-chapter-1-the-first-neuron.txt:492,搜「self.weights[feat] += error * input[feat]」)。 ↩

  59. 出处:「Chapter 1. The First Neuron」第 598 段(text/05-ch01-chapter-1-the-first-neuron.txt:598,搜「0.1, 0.7」)与第 612 段(搜「deliberately bad」)。 ↩

  60. 出处:「Chapter 1. The First Neuron」第 612 段(text/05-ch01-chapter-1-the-first-neuron.txt:612,搜「similar pattern for the OR test」)。 ↩

  61. 出处:「Chapter 1. The First Neuron」第 533 段(text/05-ch01-chapter-1-the-first-neuron.txt:533,搜「always produces −1.0」)。 ↩

  62. 出处:「Chapter 1. The First Neuron」第 622 段(text/05-ch01-chapter-1-the-first-neuron.txt:622,搜「Iteration: 0, Error: 3」)。五行日志在第 622–626 段。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  63. 出处:「Chapter 1. The First Neuron」第 629 段(text/05-ch01-chapter-1-the-first-neuron.txt:629,搜「determine the orientation of the boundary」)。 ↩ ↩2 ↩3 ↩4

  64. 出处:「Chapter 1. The First Neuron」第 631 段(text/05-ch01-chapter-1-the-first-neuron.txt:631,搜「completes one full pass」)。 ↩ ↩2

  65. 出处:「Chapter 1. The First Neuron」第 661 段(text/05-ch01-chapter-1-the-first-neuron.txt:661,搜「moves further for points far from the origin」)。 ↩

  66. 出处:「Chapter 1. The First Neuron」第 545 段(text/05-ch01-chapter-1-the-first-neuron.txt:545,搜「same sign」)。 ↩

  67. 出处:「Chapter 1. The First Neuron」第 578 段(text/05-ch01-chapter-1-the-first-neuron.txt:578,搜「encapsulating the behavior of AND」)。 ↩ ↩2

  68. 出处:「Chapter 1. The First Neuron」第 359 段(text/05-ch01-chapter-1-the-first-neuron.txt:359,搜「it_classifies_or_inputs」)。 ↩

  69. 出处:「Chapter 1. The First Neuron」第 558 段(text/05-ch01-chapter-1-the-first-neuron.txt:558,搜「it_classifies_and_inputs」)。 ↩

  70. 出处:「Chapter 1. The First Neuron」第 588 段(text/05-ch01-chapter-1-the-first-neuron.txt:588,搜「Circles correspond to True and triangles to False」)。 ↩

  71. 出处:「Chapter 1. The First Neuron」第 588 段(text/05-ch01-chapter-1-the-first-neuron.txt:588,搜「drawing a line」)与第 590 段(搜「decision boundary」)。 ↩

  72. 出处:「Chapter 1. The First Neuron」第 633–641 段(text/05-ch01-chapter-1-the-first-neuron.txt:633,搜「decision boundary is given by」)。 ↩

  73. 出处:「Chapter 1. The First Neuron」第 645 段(text/05-ch01-chapter-1-the-first-neuron.txt:645,搜「already a form of optimization」)。 ↩

  74. 出处:「Chapter 1. The First Neuron」第 612 段(text/05-ch01-chapter-1-the-first-neuron.txt:612,搜「decision boundary evolves」)。 ↩

  75. 出处:「Chapter 1. The First Neuron」第 616–618 段(text/05-ch01-chapter-1-the-first-neuron.txt:616,搜「the slope of the lines remains unchanged」)与第 629 段(搜「correspondence is not accidental」)。 ↩

  76. 出处:「Chapter 1. The First Neuron」第 647 段(text/05-ch01-chapter-1-the-first-neuron.txt:647,搜「sign disagrees with y」)。 ↩

  77. 出处:「Chapter 1. The First Neuron」第 655 段(text/05-ch01-chapter-1-the-first-neuron.txt:655,搜「scaled by a factor of two」)。 ↩ ↩2

  78. 出处:「Chapter 1. The First Neuron」第 653 段(text/05-ch01-chapter-1-the-first-neuron.txt:653,搜「moves the weight vector」)。 ↩

  79. 出处:「Chapter 1. The First Neuron」第 661 段(text/05-ch01-chapter-1-the-first-neuron.txt:661,搜「increases by exactly」)。 ↩ ↩2

  80. 出处:「Chapter 1. The First Neuron」第 663 段(text/05-ch01-chapter-1-the-first-neuron.txt:663,搜「tug-of-war」)。 ↩ ↩2 ↩3

  81. 出处:「Chapter 1. The First Neuron」第 529 段(text/05-ch01-chapter-1-the-first-neuron.txt:529,搜「At exactly zero」)。 ↩

  82. 出处:「Chapter 1. The First Neuron」第 590 段(text/05-ch01-chapter-1-the-first-neuron.txt:590,搜「surface」)。 ↩

  83. 出处:「Chapter 1. The First Neuron」第 590 段(text/05-ch01-chapter-1-the-first-neuron.txt:590,搜「feature space」)。 ↩

  84. 出处:「Chapter 2. The Search for Structure」第 31 段(text/06-ch02-chapter-2-the-search-for-structure.txt:31,搜「distance between a decision boundary and the closest data points」)。 ↩

  85. 出处:「Chapter 1. The First Neuron」第 673 段(text/05-ch01-chapter-1-the-first-neuron.txt:673,搜「important limitations」)。 ↩ ↩2 ↩3

  86. 出处:「Chapter 1. The First Neuron」第 671 段(text/05-ch01-chapter-1-the-first-neuron.txt:671,搜「never seen before」)与第 673 段(搜「slightly violated」)。 ↩

  87. 出处:「Chapter 1. The First Neuron」第 539 段(text/05-ch01-chapter-1-the-first-neuron.txt:539,搜「formal loss function」)。 ↩

  88. 出处:「Chapter 1. The First Neuron」第 539 段(text/05-ch01-chapter-1-the-first-neuron.txt:539,搜「smooth measures of discrepancy」)。 ↩

  89. 出处:「Chapter 1. The First Neuron」第 8 段(text/05-ch01-chapter-1-the-first-neuron.txt:8,搜「GitHub repo will be made active later on」)。 ↩

On this page

第一个神经元 — 感知机如何在三十行代码里学会 AND 和 OR1. 先看现象:同一个算法,学会两个不同的函数2. 顶层全景:感知机的全部身家记号就地展开:wᵀx 到底是什么一段历史:1957 年的起点,和一句给本章定调的引语4. 热身一:AND/OR 长什么样,以及第一段 RustAND 和 OR 的真值表最小的 Rust 程序长什么样补一层背景:Rust 程序的两辈子5. 热身二:把真值表变成程序读得懂的数据集cargo init 与 cargo test:两条命令走天下测试先行:先写「验收标准」,再写实现数据加载器的六行,逐个零件点名属性、mod tests 与 #[cfg(test)]:测试为什么住在代码文件里Result 的一手实验:失败被写进了类型测试数据串里的一个小机关cargo test 的输出里有什么元组、for 循环与 split_at:分离输入和标签数据加载里的 Rust,以及第一个「语言即机制」时刻Rust 的类型全家福:一张先混个脸熟的表顺手认识两个类型编译器是对手还是教练:这一章的报错文化6. 感知机的代码形状:trait、struct 与两个方法用 trait 定义「一个分类器该有的行为」泛型:Vec<T> 里的 T 是什么用 struct 装下「学到的知识」impl 块、self 与 ::new:方法从哪来train 的三步骨架三个函数的逐行走查7. 主线例子:AND 数据集,五步收敛全程OR 的五遍:相似但不相同开局:一片「全是 −1」的世界第 0 遍:错 3 次,权重被推着走第 1 遍:错误搬家,权重来回翻第 2 遍:斜率动了第 3 遍:最后一错;第 4 遍:干净通过更新规则的符号直觉:三种错法三种修法把五步摆在一起看同一个算法,换个数据集第二个测试的诞生:OR 学会了,AND 跟上8. 为什么这套更新一定有用:决策边界与收敛定理第一层:把数据画出来,答案就在眼前第二层:权重和偏置,就是那条线的参数把日志翻译成线:斜率和截距的数值对账第三层:每次更新,都把这个点往对的一边推零点归哪边:一个任意的选择决策边界在高维长什么样收敛定理:这场拔河一定结束9. 感知机的天花板:章末自己交代的两个伏笔下一章的两个预告(第 1 章自己埋的)10. 作者的判断与证据教材用词之辨:它叫 loss,其实还不是 loss11. 边界与局限12. 可带走的(合上这章,你应该带走了)13. 原文地图Footnotes