diff --git a/components/knowledge/KatexFormula.jsx b/components/knowledge/KatexFormula.jsx new file mode 100644 index 00000000..45ed764a --- /dev/null +++ b/components/knowledge/KatexFormula.jsx @@ -0,0 +1,50 @@ +import katex from "katex"; + +/* + * Split out from KnowledgeLayout.jsx so pages that never render maths don't + * pull the katex JS (and its CSS, imported alongside these components) into + * their bundle. Only the ~39 knowledge topics that use / import + * from this module. + */ + +/** + * Centred display formula, rendered with KaTeX. + * `children` is a LaTeX string (use a template literal; `\\` starts a new line, + * and `\begin{aligned}…\end{aligned}` lays out a multi-step derivation). + * `label` is the accessible description read by screen readers. + * `caption` optionally annotates the formula beneath it. + */ +export function Formula({ children, label, caption }) { + const tex = typeof children === "string" ? children : String(children ?? ""); + const html = katex.renderToString(tex, { + displayMode: true, + throwOnError: false, + strict: false, + }); + return ( +
+
+ {caption && ( +

+ {caption} +

+ )} +
+ ); +} + +/** Inline maths, rendered with KaTeX. `children` is a LaTeX string. */ +export function TeX({ children, label }) { + const tex = typeof children === "string" ? children : String(children ?? ""); + const html = katex.renderToString(tex, { + displayMode: false, + throwOnError: false, + strict: false, + }); + return ; +} diff --git a/components/knowledge/KnowledgeLayout.jsx b/components/knowledge/KnowledgeLayout.jsx index 8826b750..8bd76e1d 100644 --- a/components/knowledge/KnowledgeLayout.jsx +++ b/components/knowledge/KnowledgeLayout.jsx @@ -1,6 +1,5 @@ import Head from "next/head"; import Link from "next/link"; -import katex from "katex"; import SeoHead from "@/components/seo/SeoHead"; import { useI18n } from "@/contexts/I18nContext"; @@ -12,9 +11,10 @@ import { useI18n } from "@/contexts/I18nContext"; * frame — header, provenance strip, table of contents, prose body, refresher, * footer nav, SEO + JSON-LD — so each topic page only writes its content. * - * Companion primitives (KSection, Callout, Formula, Figure, Term, KList) are - * exported from this module and are designed to sit inside safely - * (they opt out with `not-prose` where the typography plugin would interfere). + * Companion primitives (KSection, Callout, Figure, Term) are exported from + * this module and are designed to sit inside safely (they opt out + * with `not-prose` where the typography plugin would interfere). Formula/TeX + * live in ./KatexFormula.jsx so pages without maths don't bundle katex. */ const ACCENT = "#FF3C3C"; @@ -289,48 +289,6 @@ export function Callout({ type = "note", label, children }) { ); } -/** - * Centred display formula, rendered with KaTeX. - * `children` is a LaTeX string (use a template literal; `\\` starts a new line, - * and `\begin{aligned}…\end{aligned}` lays out a multi-step derivation). - * `label` is the accessible description read by screen readers. - * `caption` optionally annotates the formula beneath it. - */ -export function Formula({ children, label, caption }) { - const tex = typeof children === "string" ? children : String(children ?? ""); - const html = katex.renderToString(tex, { - displayMode: true, - throwOnError: false, - strict: false, - }); - return ( -
-
- {caption && ( -

- {caption} -

- )} -
- ); -} - -/** Inline maths, rendered with KaTeX. `children` is a LaTeX string. */ -export function TeX({ children, label }) { - const tex = typeof children === "string" ? children : String(children ?? ""); - const html = katex.renderToString(tex, { - displayMode: false, - throwOnError: false, - strict: false, - }); - return ; -} - /** Figure wrapper for inline SVG diagrams + caption. */ export function Figure({ children, caption }) { return ( diff --git a/components/knowledge/content/artificial-intelligence.jsx b/components/knowledge/content/artificial-intelligence.jsx index 1c9ed64c..802c6e22 100644 --- a/components/knowledge/content/artificial-intelligence.jsx +++ b/components/knowledge/content/artificial-intelligence.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/artificial-intelligence. diff --git a/components/knowledge/content/bayesian-statistics.jsx b/components/knowledge/content/bayesian-statistics.jsx index 7ff12f3c..149cc348 100644 --- a/components/knowledge/content/bayesian-statistics.jsx +++ b/components/knowledge/content/bayesian-statistics.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/bayesian-statistics. diff --git a/components/knowledge/content/calculus-optimisation.jsx b/components/knowledge/content/calculus-optimisation.jsx index 31b03d01..8a7dab8b 100644 --- a/components/knowledge/content/calculus-optimisation.jsx +++ b/components/knowledge/content/calculus-optimisation.jsx @@ -1,5 +1,6 @@ import Link from "next/link"; -import { KSection, Callout, Formula, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/calculus-optimisation. diff --git a/components/knowledge/content/causal-inference.jsx b/components/knowledge/content/causal-inference.jsx index 510b359c..e2d5ceca 100644 --- a/components/knowledge/content/causal-inference.jsx +++ b/components/knowledge/content/causal-inference.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/causal-inference. diff --git a/components/knowledge/content/cluster-cloud-computing.jsx b/components/knowledge/content/cluster-cloud-computing.jsx index 208ecd76..1bf2d837 100644 --- a/components/knowledge/content/cluster-cloud-computing.jsx +++ b/components/knowledge/content/cluster-cloud-computing.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/cluster-cloud-computing. diff --git a/components/knowledge/content/clustering.jsx b/components/knowledge/content/clustering.jsx index 4d022a75..2b8e8a56 100644 --- a/components/knowledge/content/clustering.jsx +++ b/components/knowledge/content/clustering.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/clustering. diff --git a/components/knowledge/content/computational-statistics.jsx b/components/knowledge/content/computational-statistics.jsx index df4fd450..2638597e 100644 --- a/components/knowledge/content/computational-statistics.jsx +++ b/components/knowledge/content/computational-statistics.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/computational-statistics. diff --git a/components/knowledge/content/conformal-prediction.jsx b/components/knowledge/content/conformal-prediction.jsx index a9b9938c..fcaaa7eb 100644 --- a/components/knowledge/content/conformal-prediction.jsx +++ b/components/knowledge/content/conformal-prediction.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/conformal-prediction. diff --git a/components/knowledge/content/deep-learning.jsx b/components/knowledge/content/deep-learning.jsx index f304c9c9..b55fed0d 100644 --- a/components/knowledge/content/deep-learning.jsx +++ b/components/knowledge/content/deep-learning.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/deep-learning. diff --git a/components/knowledge/content/differential-privacy.jsx b/components/knowledge/content/differential-privacy.jsx index aa0b75c6..a251119d 100644 --- a/components/knowledge/content/differential-privacy.jsx +++ b/components/knowledge/content/differential-privacy.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/differential-privacy. diff --git a/components/knowledge/content/ensemble-methods.jsx b/components/knowledge/content/ensemble-methods.jsx index c0ec53dd..f3375e4f 100644 --- a/components/knowledge/content/ensemble-methods.jsx +++ b/components/knowledge/content/ensemble-methods.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/ensemble-methods. diff --git a/components/knowledge/content/extreme-value-theory.jsx b/components/knowledge/content/extreme-value-theory.jsx index 63f0a48a..a5bff7e6 100644 --- a/components/knowledge/content/extreme-value-theory.jsx +++ b/components/knowledge/content/extreme-value-theory.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/extreme-value-theory. diff --git a/components/knowledge/content/feature-engineering.jsx b/components/knowledge/content/feature-engineering.jsx index 686fbdeb..30e5f027 100644 --- a/components/knowledge/content/feature-engineering.jsx +++ b/components/knowledge/content/feature-engineering.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/feature-engineering. diff --git a/components/knowledge/content/gaussian-processes.jsx b/components/knowledge/content/gaussian-processes.jsx index 097af1ae..a6406ce6 100644 --- a/components/knowledge/content/gaussian-processes.jsx +++ b/components/knowledge/content/gaussian-processes.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/gaussian-processes. diff --git a/components/knowledge/content/graph-neural-networks.jsx b/components/knowledge/content/graph-neural-networks.jsx index e794ca56..78ff05ab 100644 --- a/components/knowledge/content/graph-neural-networks.jsx +++ b/components/knowledge/content/graph-neural-networks.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/graph-neural-networks. diff --git a/components/knowledge/content/information-retrieval.jsx b/components/knowledge/content/information-retrieval.jsx index b51d0c05..a6d8b8e7 100644 --- a/components/knowledge/content/information-retrieval.jsx +++ b/components/knowledge/content/information-retrieval.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/information-retrieval. diff --git a/components/knowledge/content/kalman-filter.jsx b/components/knowledge/content/kalman-filter.jsx index 60bc01f8..ce18bcce 100644 --- a/components/knowledge/content/kalman-filter.jsx +++ b/components/knowledge/content/kalman-filter.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/kalman-filter. diff --git a/components/knowledge/content/large-language-models.jsx b/components/knowledge/content/large-language-models.jsx index b5afc866..8ee5e4f5 100644 --- a/components/knowledge/content/large-language-models.jsx +++ b/components/knowledge/content/large-language-models.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/large-language-models. diff --git a/components/knowledge/content/linear-algebra.jsx b/components/knowledge/content/linear-algebra.jsx index 551cb4dd..2f65af01 100644 --- a/components/knowledge/content/linear-algebra.jsx +++ b/components/knowledge/content/linear-algebra.jsx @@ -1,5 +1,6 @@ import Link from "next/link"; -import { KSection, Callout, Formula, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/linear-algebra. diff --git a/components/knowledge/content/linear-statistical-models.jsx b/components/knowledge/content/linear-statistical-models.jsx index e7c12eb4..40787971 100644 --- a/components/knowledge/content/linear-statistical-models.jsx +++ b/components/knowledge/content/linear-statistical-models.jsx @@ -1,5 +1,6 @@ import Link from "next/link"; -import { KSection, Callout, Formula, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/linear-statistical-models. diff --git a/components/knowledge/content/model-evaluation.jsx b/components/knowledge/content/model-evaluation.jsx index cb9c6a93..1efa1316 100644 --- a/components/knowledge/content/model-evaluation.jsx +++ b/components/knowledge/content/model-evaluation.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/model-evaluation. diff --git a/components/knowledge/content/natural-language-processing.jsx b/components/knowledge/content/natural-language-processing.jsx index 88cd79c3..afb58ee0 100644 --- a/components/knowledge/content/natural-language-processing.jsx +++ b/components/knowledge/content/natural-language-processing.jsx @@ -1,4 +1,5 @@ -import { KSection, Callout, Formula, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/natural-language-processing. diff --git a/components/knowledge/content/network-graph-analysis.jsx b/components/knowledge/content/network-graph-analysis.jsx index b50cacd7..3b172e22 100644 --- a/components/knowledge/content/network-graph-analysis.jsx +++ b/components/knowledge/content/network-graph-analysis.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/network-graph-analysis. diff --git a/components/knowledge/content/operations-research.jsx b/components/knowledge/content/operations-research.jsx index dd3f8b8f..a97006ae 100644 --- a/components/knowledge/content/operations-research.jsx +++ b/components/knowledge/content/operations-research.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/operations-research. diff --git a/components/knowledge/content/pca-dimensionality-reduction.jsx b/components/knowledge/content/pca-dimensionality-reduction.jsx index 88f6d3ab..a8ab8b59 100644 --- a/components/knowledge/content/pca-dimensionality-reduction.jsx +++ b/components/knowledge/content/pca-dimensionality-reduction.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/pca-dimensionality-reduction. diff --git a/components/knowledge/content/probabilistic-graphical-models.jsx b/components/knowledge/content/probabilistic-graphical-models.jsx index 0f9989cb..29ae9f8b 100644 --- a/components/knowledge/content/probabilistic-graphical-models.jsx +++ b/components/knowledge/content/probabilistic-graphical-models.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/probabilistic-graphical-models. @@ -26,18 +20,60 @@ function BayesNetFigure({ caption, ariaLabel, rainLabel, sprinklerLabel, grassLa aria-label={ariaLabel} > - {rainLabel} + + {rainLabel} + - {sprinklerLabel} - - {grassLabel} - {wetLabel} + + {sprinklerLabel} + + + + {grassLabel} + + + {wetLabel} + {/* edges */} - - - + + + - + + + @@ -244,33 +280,39 @@ function ZhBody() { return ( <>

- 对一个变量在不确定下进行推理,是贝叶斯的 - 故事。但真实的问题涉及许多相互关联的不确定变量——症状与疾病、原因与结果、信号与状态——而对 - 所有这些变量的完整联合分布,大得像天文数字。概率图模型(PGM)用一个漂亮的 - 洞见让这变得可处理:把变量之间的依赖关系画成一张图,而那张图既把分布压缩成可管理的 + 对一个变量在不确定下进行推理,是 + 贝叶斯的 故事。但真实的问题涉及 + 许多相互关联的不确定变量——症状与疾病、原因与结果、信号与状态——而对 + 所有这些变量的完整联合分布,大得像天文数字。概率图模型 + (PGM)用一个漂亮的 洞见让这变得可处理:把变量之间的依赖关系画成一张图 + ,而那张图既把分布压缩成可管理的 小块,又告诉你如何用它来推理。它是概率论与图论相遇的地方,支撑着大量现代的概率 AI。

它把你已经见过的几条线索系在一起——它是贝叶斯的,它的图就是因果推断里那些同样的 DAG,而学习它的结构就是 - 因果发现。这一页讲核心的想法(把独立性化为图)、 + 因果发现 + 。这一页讲核心的想法(把独立性化为图)、 贝叶斯网络、图如何把联合分布因子分解,以及你如何用证据来推理。

关于一组变量你想知道的一切,原则上都在它们的联合分布里——每一种取值组合的概率。问题在于规模: - 仅仅 30 个是/否变量,联合分布就有超过十亿个条目。你存不下它,更别说从数据中估计它、或用它 - 来计算。必须利用某种结构,而拯救我们的那个结构是独立性——大多数变量并不直接依赖于 - 大多数其他变量。 + 仅仅 30 个是/否变量,联合分布就有超过十亿 + 个条目。你存不下它,更别说从数据中估计它、或用它 + 来计算。必须利用某种结构,而拯救我们的那个结构是独立性 + ——大多数变量并不直接依赖于 大多数其他变量。

- 核心的想法:一张图编码了哪些变量(在条件上)独立于哪些。每个变量是一个节点;两个 - 节点之间的一条边意味着一个直接的依赖;一条缺失的边断言一个 - 条件独立(「一旦我知道了它的父节点,这个变量就再也 + 核心的想法:一张图编码了哪些变量(在条件上)独立于哪些 + 。每个变量是一个节点;两个 节点之间的一条边意味着一个直接的依赖;一条缺失 + 的边断言一个 + 条件独立 + (「一旦我知道了它的父节点,这个变量就再也 告诉不了我关于那个变量的任何东西」)。这张图是依赖结构一张紧凑的、人可读的地图——而关键的是,那些 独立性假设,正是把那个不可能的联合分布缩成某个又小又可计算的东西的原因。

@@ -278,9 +320,10 @@ function ZhBody() {

- 最常见的 PGM 是贝叶斯网络(或称信念网络):一个有向无环图(DAG,与因果图 - 同样的结构),其中每个节点只依赖于它的父节点。每个节点带着一张小表——它在给定父节点下 - 的概率——而那个局部的信息,加上这张图,就完整地指定了整个联合分布。 + 最常见的 PGM 是贝叶斯网络(或称信念网络):一个有向无环图 + (DAG,与因果图 同样的结构),其中每个节点只依赖于它的父节点 + 。每个节点带着一张小表——它在给定父节点下 + 的概率——而那个局部的信息,加上这张图,就完整地指定了整个联合分布。

- 那个经典的玩具例子:和一个洒水器各自都能让草地变湿,而雨还影响洒水器 + 那个经典的玩具例子:和一个洒水器各自都能让草地 + 变湿,而雨还影响洒水器 是否运行。三个节点、几张小表——你就捕捉到了一个可以推理的完整联合分布。

- 数学上的回报是因子分解。一个贝叶斯网络说,完整的联合分布就是每个变量在给定其父节点下 - 概率的乘积: + 数学上的回报是因子分解 + 。一个贝叶斯网络说,完整的联合分布就是每个变量在给定其父节点下 概率的乘积

{String.raw`P(X_1, \dots, X_n) = \prod_{i=1}^{n} P\big(X_i \mid \text{parents}(X_i)\big)`}

- 这就是一行里的全部魔法。你不是存一张覆盖所有变量的巨大的表,而是存许多表(每个节点一张, - 大小只由它那几个父节点决定)。30 个变量那个十亿条目的联合分布,坍缩成了一小撮小表——可存储、可学习、 + 这就是一行里的全部魔法。你不是存一张覆盖所有变量的巨大的表,而是存许多 + 表(每个节点一张, 大小只由它那几个父节点决定)。30 + 个变量那个十亿条目的联合分布,坍缩成了一小撮小表——可存储、可学习、 可计算。图里缺失的边,正是许可这次因子分解的那些独立性假设:结构买来了可处理性。

@@ -314,46 +359,54 @@ function ZhBody() {

建这个模型的意义在于推断——回答形如{" "} - {String.raw`P(\text{query} \mid \text{evidence})`} 的问题:「在草地是湿的、并且天阴的 + {String.raw`P(\text{query} \mid \text{evidence})`}{" "} + 的问题:「在草地是湿的、并且天阴的 情况下,下过雨的概率是多少?」你把观测到的变量钳制到它们的取值上,然后计算你关心的那些变量上更新后 的分布——贝叶斯的信念更新,在网络中传播。

- 陷阱在于,精确推断一般而言在计算上很难(对任意的图是 NP 难的)。对于漂亮的结构 - (树),通过信念传播它是高效的——在相邻节点之间传递「信念」消息,直到它们达成一致 - (注意它与 GNN 的消息传递的亲缘关系)。对于 + 陷阱在于,精确推断一般而言在计算上很难(对任意的图是 NP + 难的)。对于漂亮的结构 (树),通过信念传播 + 它是高效的——在相邻节点之间传递「信念」消息,直到它们达成一致 (注意它与{" "} + GNN 的消息传递的亲缘关系)。对于 有环的、复杂的网络,你就退而求其次用近似推断—— - MCMC 采样,或有环信念传播。所以那个优雅的 - 表示,在图变得纠缠时,伴随着一个真实的计算代价。 + MCMC{" "} + 采样,或有环信念传播。所以那个优雅的 表示,在图变得纠缠时,伴随着一个真实的计算代价。

- 一个 PGM 有两样东西要学:参数(概率表,在给定图的情况下——通常从数据中可以直接得到)和 + 一个 PGM 有两样东西要学:参数 + (概率表,在给定图的情况下——通常从数据中可以直接得到)和 结构(图本身——哪些边存在)。从观测数据中学习结构,正是 - 因果发现,连同它所有的困难(你往往只能恢复出一个 - 等价类)。实践中结构常常来自领域知识——一个专家画出依赖图——而只学习参数,这是一个 - 真正的强项:PGM 让你编码你所知道的,并学习其余的。 + 因果发现 + ,连同它所有的困难(你往往只能恢复出一个 等价类)。实践中结构常常来自 + 领域知识——一个专家画出依赖图——而只学习参数,这是一个 真正的强项:PGM 让你 + 编码你所知道的,并学习其余的。

- PGM 在有许多相互关联的不确定变量、且你需要既有原则又可解释的推理之处 - 最重要。与黑箱模型不同,一个贝叶斯网络展示它的推理——这张图是一份「什么依赖于 + PGM 在有许多相互关联的不确定变量、且你需要既有原则又可解释 + 的推理之处 最重要。与黑箱模型不同,一个贝叶斯网络展示它的推理 + ——这张图是一份「什么依赖于 什么」的可审计地图,而像「在给定这个证据下,这是更新后的概率」这样的一次推断,可以在它上面被追溯。 在一个需要问责的场景里,那种透明度是真正有价值的:你不仅能为答案辩护,还能为推理的结构辩护。

- 它还统一了好几页:它是带许多变量的贝叶斯更新, - 它的 DAG 就是因果推断的图,学习它的结构是 + 它还统一了好几页:它是带许多变量的 + 贝叶斯更新, 它的 DAG 就是 + 因果推断的图,学习它的结构是 因果发现,而它的推断呼应 - GNN 的消息传递。需要的纪律是尊重那个 - 推断代价——在纠缠的图上精确推理很难,所以你依靠近似——以及这个模型的好坏,只取决于 + GNN 的消息传递 + 。需要的纪律是尊重那个 + 推断代价 + ——在纠缠的图上精确推理很难,所以你依靠近似——以及这个模型的好坏,只取决于 你编码进去的依赖结构。

@@ -363,29 +416,30 @@ function ZhBody() {
  • - 许多变量上的完整联合分布大得像天文数字——PGM 靠利用独立性来 - 驯服它。 + 许多变量上的完整联合分布大得像天文数字——PGM 靠利用 + 独立性来 驯服它。
  • - 一张图编码条件独立:节点 = 变量,一条缺失的边 = 一个独立性。一份紧凑、可读的 - 依赖地图。 + 一张图编码条件独立:节点 = 变量,一条缺失的边 = + 一个独立性。一份紧凑、可读的 依赖地图。
  • - 一个贝叶斯网络是一个 DAG,其中每个节点依赖于它的父节点;用 - 每个节点的小表,它指定了整个联合分布。 + 一个贝叶斯网络是一个 DAG,其中每个节点依赖于它的 + 父节点;用 每个节点的小表,它指定了整个联合分布。
  • 因子分解{" "} - {String.raw`P(X_1..X_n)=\prod_i P(X_i\mid \text{parents})`} 把一张巨大的表变成许多 - 小表——可存储、可学习、可计算。 + {String.raw`P(X_1..X_n)=\prod_i P(X_i\mid \text{parents})`}{" "} + 把一张巨大的表变成许多 小表——可存储、可学习、可计算。
  • - 推断 ={" "} - {String.raw`P(\text{query}\mid\text{evidence})`}。在树上容易( + 推断 = {String.raw`P(\text{query}\mid\text{evidence})`} + 。在树上容易( 信念传播);一般而言 NP 难 → MCMC / 近似。
  • - 学习参数(容易)+ 结构(= 因果发现,或来自领域知识)。强项: + 学习参数(容易)+ 结构(= + 因果发现,或来自领域知识)。强项: 透明、可解释的推理。
diff --git a/components/knowledge/content/probability.jsx b/components/knowledge/content/probability.jsx index ef5f14b3..d8632222 100644 --- a/components/knowledge/content/probability.jsx +++ b/components/knowledge/content/probability.jsx @@ -1,5 +1,6 @@ import Link from "next/link"; -import { KSection, Callout, Formula, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/probability. diff --git a/components/knowledge/content/quantile-regression.jsx b/components/knowledge/content/quantile-regression.jsx index 859a7322..f0e51389 100644 --- a/components/knowledge/content/quantile-regression.jsx +++ b/components/knowledge/content/quantile-regression.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/quantile-regression. @@ -48,11 +42,24 @@ function FanFigure({ caption, ariaLabel }) { ))} - τ=.9 + + τ=.9 + - τ=.5 + + τ=.5 + - τ=.1 + + τ=.1 + ); @@ -269,73 +276,83 @@ function ZhBody() { return ( <>

- 普通回归回答一个问题:一个预测变量如何移动 - 结果的平均值?那有用——而它能把几乎一切要紧的东西都藏起来。一项政策的平均效应可能 + 普通回归 + 回答一个问题:一个预测变量如何移动 结果的平均值 + ?那有用——而它能把几乎一切要紧的东西都藏起来。一项政策的平均效应可能 很小,却对顶部帮助极大、对底部毫无帮助;等待时间的离散程度可能随需求拉大,即便均值稳稳不动。 - 分位数回归超越平均值:它建模一个预测变量如何影响分布上任何选定的点——中位数、 - 第 10 百分位、第 90——这样你看到的是对整个响应的影响,而不只是它的质心。 + 分位数回归超越平均值:它建模一个预测变量如何影响分布上任何 + 选定的点——中位数、 第 10 百分位、第 90——这样你看到的是对整个 + 响应的影响,而不只是它的质心。

它是一个独特而出人意料地实用的工具——并干净利落地连到两个邻居:它的损失函数让它天然 稳健,而对尾部建模又连到 极值理论和 - 预测区间。这一页讲这个想法、驱动它的那个巧妙的 - 损失、如何读它的输出,以及它在哪里赢得自己的位置。 + 预测区间 + 。这一页讲这个想法、驱动它的那个巧妙的 损失、如何读它的输出,以及它在哪里赢得自己的位置。

均值回归的局限在于,条件均值是一个单一的概括,而一个单一的概括无法捕捉一种关系如何随分布而变化。 - 两种有着相同均值效应的情形可以完全不同:一个预测变量可能把所有人同等地往上抬,或者抬高 - 顶部而让底部不动,或者在不移动中心的情况下增大离散程度。普通最小二乘对这三者 - 报告同样的平均值,对其间的差别视而不见。 + 两种有着相同均值效应的情形可以完全不同:一个预测变量可能把所有人同等地往上抬, + 或者抬高 顶部而让底部不动,或者在不移动中心的情况下增大离散程度 + 。普通最小二乘对这三者 报告同样的平均值,对其间的差别视而不见。

然而那个差别往往才是全部的重点——在公平上(「这帮的是最弱势的人,还是只帮了已经占优的人?」)、在 - 风险上(「糟糕的情形有多糟,而不是典型情形?」),以及在服务保证上(「第 95 百分位的等待是多少, - 而不是平均等待?」)。分位数回归正是为回答这些而生的。 + 风险上(「糟糕的情形有多糟,而不是典型情形?」),以及在服务保证上(「第 95 + 百分位的等待是多少, 而不是平均等待?」)。分位数回归正是为回答这些而生的。

- 这个想法是一个直接的推广。普通回归建模 {String.raw`Y`} 在给定 {String.raw`X`}{" "} - 下的条件均值,而分位数回归把一个条件分位数 {String.raw`\tau`}—— - 例如 {String.raw`\tau = 0.5`}(中位数),或 {String.raw`0.9`}(第 90 百分 - 位)——建模为预测变量的一个函数。在好几个分位数上拟合它,你就得到一族线,描述结果的底部、 - 中部顶部各自如何对预测变量做出响应。你建模了整个条件分布,而不只是它的均值。 + 这个想法是一个直接的推广。普通回归建模 {String.raw`Y`} 在给定{" "} + {String.raw`X`} 下的条件均值,而分位数回归把一个条件 + 分位数 {String.raw`\tau`}—— 例如{" "} + {String.raw`\tau = 0.5`}(中位数),或 {String.raw`0.9`}(第 90 百分 + 位)——建模为预测变量的一个函数。在好几个分位数上拟合它,你就得到一族线,描述结果的 + 底部中部顶部 + 各自如何对预测变量做出响应。你建模了整个条件分布,而不只是它的均值。

- 其机制是损失函数的一个优雅的改变。普通回归最小化平方误差(它瞄准均值);分位数回归最小化 - 一个不对称的绝对误差——弹球(或检验)损失——它瞄准一个选定的 - 分位数: + 其机制是损失函数的一个优雅的改变。普通回归最小化平方 + 误差(它瞄准均值);分位数回归最小化 一个不对称的绝对误差—— + 弹球(或检验)损失——它瞄准一个选定的 分位数:

{String.raw`L_\tau(r) = \begin{cases} \tau\,r & r \ge 0 \\[3pt] (\tau - 1)\,r & r < 0 \end{cases}`}

- 那个不对称就是全部的诀窍。对 {String.raw`\tau = 0.9`},低估(真值在线之上)受到的惩罚 - 比高估重 9 倍——于是拟合的线被往上推,直到只有约 10% 的点落在它之上:第 90 百分位。 - 调 {String.raw`\tau`},你就能瞄准任何分位数。而因为它建立在绝对(而非平方) - 误差之上,分位数回归对离群值天然稳健——中位数回归 - ({String.raw`\tau = 0.5`})正是最小绝对偏差,最小二乘那个稳健的表亲。 + 那个不对称就是全部的诀窍。对 {String.raw`\tau = 0.9`} + ,低估(真值在线之上)受到的惩罚 比高估重 9 倍 + ——于是拟合的线被往上推,直到只有约 10% 的点落在它之上:第 90 百分位。 调{" "} + {String.raw`\tau`},你就能瞄准任何分位数。而因为它建立在绝对 + (而非平方) 误差之上,分位数回归对离群值天然 + 稳健——中位数回归 ( + {String.raw`\tau = 0.5`})正是最小绝对偏差,最小二乘那个稳健的表亲。

-

真正的洞见来自一次性拟合好几个分位数,并把这些线放在一起看:

+

+ 真正的洞见来自一次性拟合好几个分位数,并把这些线放在一起看: +

- 如果分位数线大致平行,预测变量同等地移动整个分布(离散程度恒定)。如果它们 - 扇形散开,离散程度随预测变量增大——异方差——意味着预测变量 - 影响的不只是水平,还有变异性。那种扇形展开对一条单一的均值线是不可见的,而它常常是最重要的 - 发现:「随着 X 增加,结果不只是上升,它们变得更不平等。」 + 如果分位数线大致平行 + ,预测变量同等地移动整个分布(离散程度恒定)。如果它们 + 扇形散开,离散程度随预测变量增大——异方差 + ——意味着预测变量 影响的不只是水平,还有变异性 + 。那种扇形展开对一条单一的均值线是不可见的,而它常常是最重要的 发现:「随着 X + 增加,结果不只是上升,它们变得更不平等。」

@@ -343,15 +360,17 @@ function ZhBody() {

在离散程度或尾部与中心同等要紧之处,分位数回归赢得自己的位置:

  • - 预测区间——拟合第 5 和第 95 分位数,你就有了一个直接、诚实的区间(「90% 的情形 - 落在这两者之间」)——保形预测的近亲。 + 预测区间——拟合第 5 和第 95 分位数,你就有了一个直接、诚实的区间(「90% + 的情形 落在这两者之间」)——保形预测 + 的近亲。
  • 风险与尾部——直接建模损失或延误的第 99 百分位,在那里 极端才是关切所在,而非平均。
  • - 公平分析——一个效应对分布的底部与顶部是否不同?(一个项目抬高的是最弱势的人,还是 + 公平分析 + ——一个效应对分布的底部与顶部是否不同?(一个项目抬高的是最弱势的人,还是 只是已经过得好的人?)均值告诉不了你;分位数回归能。
@@ -361,10 +380,12 @@ function ZhBody() {

几条告诫让它保持诚实:

- 分别拟合每个分位数可能产生分位数交叉——估计出的第 90 百分位在某些输入处 - 跌到第 50 之下,这在逻辑上不可能,是模型在勉强的一个信号(有一些方法会强制不交叉)。而因为每个 - 分位数是从它附近的数据估计出来的,极端分位数需要更多数据才能可靠地 - 钉住——第 99 百分位天生比中位数更难估计。所以分位数回归在分布的主体里最可信,当你往尾部深处 + 分别拟合每个分位数可能产生分位数交叉——估计出的第 90 + 百分位在某些输入处 跌到第 50 + 之下,这在逻辑上不可能,是模型在勉强的一个信号(有一些方法会强制不交叉)。而因为每个 + 分位数是从它附近的数据估计出来的,极端分位数需要更多数据 + 才能可靠地 钉住——第 99 + 百分位天生比中位数更难估计。所以分位数回归在分布的主体里最可信,当你往尾部深处 推进时,应当与极值理论配对。

@@ -373,17 +394,20 @@ function ZhBody() {

- 政府分析里许多最重要的问题,都不是关于平均值的——它们是关于分布的:最坏情形的 - 等待或延误(第 95 百分位,而非均值)、一项干预帮的是最弱势的人还是只是已经 - 占优的人(公平),以及结果有多不平等、那是否在拉大。分位数回归在均值回归视而不见之处 - 直接回答这些,而那些扇形展开的线是一种有力的方式,去展示一个预测变量增加的 + 政府分析里许多最重要的问题,都不是关于平均值的——它们是关于分布 + 的:最坏情形的 等待或延误(第 95 百分位,而非均值)、一项干预帮的是 + 最弱势的人还是只是已经 占优的人(公平),以及结果有多不平等 + 、那是否在拉大。分位数回归在均值回归视而不见之处 直接回答这些,而那些 + 扇形展开的线是一种有力的方式,去展示一个预测变量增加的 不只是一个结果的水平,还有它的不平等

- 它还与我所倚重的几个邻居相配:它天然稳健 + 它还与我所倚重的几个邻居相配:它天然 + 稳健 (中位数回归 = 最小绝对偏差),它给出诚实的 - 预测区间(拟合两个分位数),而对于真正的 - 尾部它交棒给极值理论。知道平均值很少就是 + 预测区间 + (拟合两个分位数),而对于真正的 尾部它交棒给 + 极值理论。知道平均值很少就是 全部的故事——分位数回归是你询问其余部分的方式。

@@ -393,27 +417,30 @@ function ZhBody() {
  • - 普通回归建模均值——它藏起一个预测变量如何影响底部对顶部。分位数回归建模任何 + 普通回归建模均值 + ——它藏起一个预测变量如何影响底部对顶部。分位数回归建模任何 条件分位数(中位数、第 90……)。
  • - 它最小化弹球 / 检验损失——不对称的绝对误差;对 τ=0.9,低估的代价多 9 倍,于是 - 线落在第 90 百分位。 + 它最小化弹球 / 检验损失——不对称的绝对误差;对 τ=0.9,低估的代价多 9 + 倍,于是 线落在第 90 百分位。
  • - 建立在绝对(而非平方)误差之上 → 天然稳健;中位数回归 = 最小绝对偏差。 + 建立在绝对(而非平方)误差之上 → 天然稳健;中位数回归 = + 最小绝对偏差。
  • 拟合好几个分位数并把它们放在一起读:平行 = 离散程度恒定; - 扇形展开 = 异方差(预测变量改变离散程度/不平等——对一条均值线不可见)。 + 扇形展开 = 异方差 + (预测变量改变离散程度/不平等——对一条均值线不可见)。
  • 非常适合预测区间(拟合两个分位数)、风险/尾部,以及 公平(对最弱势者与顶部的效应)。
  • - 告诫:分位数交叉,以及极端分位数需要更多数据——真正的尾部 - 交给 EVT。 + 告诫:分位数交叉,以及极端分位数需要更多数据 + ——真正的尾部 交给 EVT。
diff --git a/components/knowledge/content/recommender-systems.jsx b/components/knowledge/content/recommender-systems.jsx index e5029bac..2ce3a1f4 100644 --- a/components/knowledge/content/recommender-systems.jsx +++ b/components/knowledge/content/recommender-systems.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/recommender-systems. @@ -16,7 +10,14 @@ import { * Matrix names (R/U/Vᵀ), operators (≈/×), and dims (m × k, k × n) are kept. */ -function MFFigure({ caption, ariaLabel, usersItemsLabel, kFactorsLabel, perUserItemLabel, ratingDotLabel }) { +function MFFigure({ + caption, + ariaLabel, + usersItemsLabel, + kFactorsLabel, + perUserItemLabel, + ratingDotLabel, +}) { return (
{/* R */} - - R - {usersItemsLabel} - + + + R + + + {usersItemsLabel} + + + ≈ + {/* U */} - - U - m × k - × + + + U + + + m × k + + + × + {/* V^T */} - - Vᵀ - k × n + + + Vᵀ + + + k × n + {/* note */} - {kFactorsLabel} - {perUserItemLabel} - {ratingDotLabel} + + {kFactorsLabel} + + + {perUserItemLabel} + + + {ratingDotLabel} +
); @@ -300,13 +419,15 @@ function ZhBody() { return ( <>

- 现代生活中出人意料的一大部分,是被推荐系统塑造的:一个流媒体服务推到你眼前的电影、 + 现代生活中出人意料的一大部分,是被推荐系统 + 塑造的:一个流媒体服务推到你眼前的电影、 一家商店建议的商品、一个信息流接下来给你看的帖子。它们是个性化的引擎,而它们核心的想法真正优雅——从 每个人过去喜欢过什么的模式里,预测一个特定的人会有多喜欢一个特定的东西,并据此排序。

它值得好好了解,有两个原因。它的机制漂亮地连回 - 降维——核心的技术就是同一个潜在因子的 + 降维 + ——核心的技术就是同一个潜在因子的 想法——而它的失败模式(过滤气泡、反馈回路)是整个应用机器学习里社会后果最严重的一些。这一页讲整体的 地形:两大策略、矩阵分解引擎,以及哪里会出问题。

@@ -314,23 +435,24 @@ function ZhBody() {

把问题想成一张巨大的、大部分为空的表:行是用户,列是物品,每个格子是那个用户有多喜欢那个物品—— - 一个评分、一次点击、一次购买。问题在于这张表极度稀疏:任何一个人都只与所有物品里 - 极小的一部分有过交互。推荐器的工作就是把空白填上——预测缺失的格子——然后推荐它预测你会打 - 最高分的物品。 + 一个评分、一次点击、一次购买。问题在于这张表极度稀疏 + :任何一个人都只与所有物品里 极小的一部分有过交互。推荐器的工作就是把空白填上 + ——预测缺失的格子——然后推荐它预测你会打 最高分的物品。

- 做这件事有两种根本不同的方式,区别在于它们去哪里寻找信号:在物品本身,还是在其他用户 - 构成的人群。 + 做这件事有两种根本不同的方式,区别在于它们去哪里寻找信号:在物品 + 本身,还是在其他用户 构成的人群

- 基于内容的过滤看的是物品。它从你过去喜欢过的东西的特征里,建立起一个「你 + 基于内容的过滤看的是物品 + 。它从你过去喜欢过的东西的特征里,建立起一个「你 喜欢什么」的画像,然后推荐有相似特征的物品。喜欢过好几部硬科幻电影?这里又有一部标着科幻的。物品 特征可以是显式的(类型、作者、价格),也可以是描述的学习得到的 - 文本嵌入,而「相似」就是 NLP 页里那个 - 同样的向量距离的想法。 + 文本嵌入,而「相似」就是 NLP + 页里那个 同样的向量距离的想法。

它的强项与弱点是一枚硬币的两面:它能在全新的物品一被录入目录的那一刻就推荐它(它只需要它们的 @@ -339,13 +461,10 @@ function ZhBody() {

- +

- 协同过滤完全忽略物品特征,只用交互的模式——群体的智慧。直觉是:「过去与你意见一致的 + 协同过滤 + 完全忽略物品特征,只用交互的模式——群体的智慧。直觉是:「过去与你意见一致的 人,未来也会与你一致。」两种风味:

    @@ -353,31 +472,31 @@ function ZhBody() { 用户—用户——找到口味与你相似的人,把他们喜欢、而你还没看过的东西推荐给你。
  • - 物品—物品——找到倾向于被同一批人喜欢的物品,把(在那种共同喜欢的意义上)与你打过 + 物品—物品 + ——找到倾向于被同一批人喜欢的物品,把(在那种共同喜欢的意义上)与你打过 高分的物品相似的物品推荐给你。(「购买此商品的顾客也购买了……」)这是大多数大规模系统的驱动力, 因为物品—物品的关系比用户口味更稳定。

- 协同过滤的魔力在于,它不需要知道物品实际上是什么——只需要知道谁与什么交互过。 + 协同过滤的魔力在于,它不需要知道物品实际上是什么 + ——只需要知道谁与什么交互过。 那也是它的弱点,下一节的技术会处理它,而再后面的冷启动问题会把它暴露出来。

- +

- 驱动了现代协同过滤——并著名地赢得了 Netflix 大奖——的那个突破是矩阵分解。想法是:把 - 巨大稀疏的用户—物品评分矩阵 {String.raw`R`} 近似为两个小得多的稠密矩阵的乘积: + 驱动了现代协同过滤——并著名地赢得了 Netflix 大奖——的那个突破是矩阵分解 + 。想法是:把 巨大稀疏的用户—物品评分矩阵 {String.raw`R`}{" "} + 近似为两个小得多的稠密矩阵的乘积:

{String.raw`R_{\,m \times n} \;\approx\; U_{\,m \times k}\, V_{\,n \times k}^{\top}`}

- 每个用户变成一个由 {String.raw`k`}潜在因子组成的短向量,每个物品也是。 + 每个用户变成一个由 {String.raw`k`}潜在因子 + 组成的短向量,每个物品也是。 一个预测的评分,就是一个用户的向量与一个物品的向量的点积。这些因子是学出来的、而非标注的——但它们 往往对应可解释的维度(「有多科幻」「有多轻松」),而一个用户在某个因子上的分数,乘以一个物品在 同一个因子上的分数,就捕捉到了他们的契合度。 @@ -392,36 +511,41 @@ function ZhBody() { />

这与 PCA 是同一个 - 低秩、潜在维度的想法——把一个巨大的矩阵压缩进几个有意义的维度——这正是为什么线性 - 代数的基础在这里直接见效。它还优雅地处理稀疏性:你只在你确实观测到的格子上拟合,而分解会 - 泛化到其余的格子。 + 低秩、潜在维度 + 的想法——把一个巨大的矩阵压缩进几个有意义的维度——这正是为什么线性 + 代数的基础在这里直接见效。它还优雅地处理稀疏性:你只在你确实 + 观测到的格子上拟合,而分解会 泛化到其余的格子。

- 协同过滤的一大弱点是冷启动问题:它需要交互历史,而一个全新的用户或物品一点历史都没有。 + 协同过滤的一大弱点是冷启动问题 + :它需要交互历史,而一个全新的用户或物品一点历史都没有。 你没法给一个你一无所知的人推荐,你也没法把一个刚加进来、还没人碰过的物品推上去。这是每一个新推荐器 核心处那个先有鸡还是先有蛋的难题。

- 标准的修法是走混合路线:对新用户和新物品依靠基于内容的方法(它只需要特征), - 然后随着交互历史的积累转向协同过滤。大多数生产系统正是因为这个原因而是混合的——每种方法都 - 覆盖另一种的盲点。 + 标准的修法是走混合路线:对新用户和新物品依靠基于内容 + 的方法(它只需要特征), 然后随着交互历史的积累转向协同 + 过滤。大多数生产系统正是因为这个原因而是混合的——每种方法都 覆盖另一种的盲点。

- 评估推荐器比一个单一的准确率数字更微妙,因为要紧的是你展示之物的顺序,而非一个精确的 - 评分。指标是以排序为中心的:precision@k - (在前 k 个推荐里,有多少是相关的?)和 NDCG(它还奖励把最好的物品放到最高)。你关心的 - 是列表的顶部——没人会滚动到第 200 条推荐。 + 评估推荐器比一个单一的准确率数字更微妙,因为要紧的是你展示之物的顺序 + ,而非一个精确的 评分。指标是 + 以排序为中心的:precision@k + (在前 k 个推荐里,有多少是相关的?)和 NDCG + (它还奖励把最好的物品放到最高)。你关心的 是列表的顶部——没人会滚动到第 200 条推荐。

- 更深的陷阱是离线—在线鸿沟。一个在历史数据上分数漂亮的推荐器,可能在真实用户那里 - 一败涂地,因为离线数据只记录了人们对系统的推荐做了什么——它没法告诉你他们会对真正新的 + 更深的陷阱是离线—在线鸿沟 + 。一个在历史数据上分数漂亮的推荐器,可能在真实用户那里 + 一败涂地,因为离线数据只记录了人们对 + 系统的推荐做了什么——它没法告诉你他们会对真正新的 建议如何反应。这正是为什么认真的推荐器最终是由实时的 A/B 测试来评判的,而非单凭离线分数。

@@ -434,16 +558,20 @@ function ZhBody() {

  • - 流行度偏差——群体的最爱被推荐得最多,于是它们被交互得最多,于是它们被推荐得更多。 + 流行度偏差 + ——群体的最爱被推荐得最多,于是它们被交互得最多,于是它们被推荐得更多。 富者愈富,而小众的物品始终不可见。
  • - 过滤气泡 / 回音室——通过给你看越来越多你已经参与的东西,系统收窄了你的世界,这对 + 过滤气泡 / 回音室 + ——通过给你看越来越多你已经参与的东西,系统收窄了你的世界,这对 电影是无害的,对新闻和观点却是腐蚀性的。
  • - 反馈回路——模型的推荐成为它接下来训练所用的数据,于是它从自己的影响里学习,并可能 - 失控地盘旋上升。这是奖励塑形问题换了个样子: + 反馈回路 + ——模型的推荐成为它接下来训练所用的数据,于是它从自己的影响里学习,并可能 + 失控地盘旋上升。这是奖励塑形 + 问题换了个样子: 优化原始的参与度,你可能恰恰放大那些最大化点击的耸动或上瘾的内容,不管它对任何人是否有好处。
@@ -452,17 +580,21 @@ function ZhBody() {

- 推荐系统不是政府分析师的核心工具,但它的机制可以推广到任何排序或优先级排定问题—— - 从一长串里把最值得关注的案件、物品或线索浮现出来,这是一种不断重现的形状。矩阵分解 / - 潜在因子的想法就是与 PCA 相同的 - 低秩压缩,而那些排序指标(precision@k)是你评判任何「给我看前 N 个」系统的方式。 + 推荐系统不是政府分析师的核心工具,但它的机制可以推广到任何 + 排序或优先级排定问题—— + 从一长串里把最值得关注的案件、物品或线索浮现出来,这是一种不断重现的形状。 + 矩阵分解 / 潜在因子的想法就是与{" "} + PCA 相同的 低秩压缩,而那些 + 排序指标(precision@k)是你评判任何「给我看前 N 个」系统的方式。

- 不过,最能迁移过来的,是那个警示性的一半。反馈回路——一个在自己过去输出的后果 + 不过,最能迁移过来的,是那个警示性的一半。反馈回路 + ——一个在自己过去输出的后果 之上训练的模型——是一个远超推荐器的陷阱:任何作用于世界、然后又从那个被改变的世界里学习的系统, - 都有把自己的偏差固化下来的风险,这直接连到公平和 - 漂移问题。知道这个失败模式,正是让你能去提防它的 - 东西。 + 都有把自己的偏差固化下来的风险,这直接连到 + 公平和 + 漂移 + 问题。知道这个失败模式,正是让你能去提防它的 东西。

@@ -475,22 +607,25 @@ function ZhBody() {
  • 基于内容(按特征推荐相似物品——能处理新物品,但把你困在你的口味里)对 - 协同(用群体的交互——「像你这样的人喜欢」;物品—物品驱动大多数大系统)。 + 协同 + (用群体的交互——「像你这样的人喜欢」;物品—物品驱动大多数大系统)。
  • - 矩阵分解{String.raw`R \approx U V^\top`})为每个用户/物品学出短的 + 矩阵分解{String.raw`R \approx U V^\top`} + )为每个用户/物品学出短的 潜在因子向量;评分 = 点积。与 PCA 同一个低秩的想法。
  • - 冷启动问题(新用户/物品,无历史)→ 走混合(新的用内容,历史 - 增长后用协同)。 + 冷启动问题(新用户/物品,无历史)→ 走混合 + (新的用内容,历史 增长后用协同)。
  • - 用排序评估(precision@k、NDCG)——但留意离线—在线鸿沟;由实时 - A/B 测试评判。 + 用排序评估(precision@k、NDCG)——但留意离线—在线鸿沟 + ;由实时 A/B 测试评判。
  • - 重大的问题:流行度偏差、过滤气泡、反馈回路——推荐器塑造行为,而不只是预测它。 + 重大的问题:流行度偏差、过滤气泡、反馈回路 + ——推荐器塑造行为,而不只是预测它。
  • diff --git a/components/knowledge/content/reinforcement-learning.jsx b/components/knowledge/content/reinforcement-learning.jsx index a7ca6968..2f592c94 100644 --- a/components/knowledge/content/reinforcement-learning.jsx +++ b/components/knowledge/content/reinforcement-learning.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/reinforcement-learning. diff --git a/components/knowledge/content/robust-statistics.jsx b/components/knowledge/content/robust-statistics.jsx index 3d5b454c..3600aa10 100644 --- a/components/knowledge/content/robust-statistics.jsx +++ b/components/knowledge/content/robust-statistics.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/robust-statistics. @@ -34,15 +28,49 @@ function BreakdownFigure({ role="img" aria-label={ariaLabel} > - {meanLabel} + + {meanLabel} + - {meanNote} - {medianLabel} + + {meanNote} + + + {medianLabel} + - {medianNote} - - {zeroLabel} - {fiftyLabel} + + {medianNote} + + + + {zeroLabel} + + + {fiftyLabel} + ); @@ -284,25 +312,29 @@ function ZhBody() { return ( <>

    - 关于那些最熟悉的统计量,有一个令人不安的事实:均值、标准差 - 和最小二乘回归全都脆弱。单单一个极端的离群值——一个打字错误、一次传感器故障、一个 + 关于那些最熟悉的统计量,有一个令人不安的事实:均值 + 、标准差 和最小二乘回归全都脆弱 + 。单单一个极端的离群值——一个打字错误、一次传感器故障、一个 真实的怪例——就能把均值从数据主体所在之处拖得很远,把标准差吹胀,并把一条回归线从其他所有点都遵循的 - 模式上扳开。而真实数据充满这样的污染。稳健统计是那套正是为抵抗这一点而设计的 - 方法:给出一个反映数据主体的答案,而非被几个坏点挟持。 + 模式上扳开。而真实数据充满这样的污染。稳健统计 + 是那套正是为抵抗这一点而设计的 方法:给出一个反映数据主体 + 的答案,而非被几个坏点挟持。

    它独特而极其实用——与那套假设数据干净的经典工具箱是不同的心态。这一页讲为什么标准方法会崩、我们 - 度量稳健性的精确方式(崩溃点)、抵抗性的替代品(中位数、MAD、M 估计量),以及稳健性所逼出的 - 那个判断:修方法,还是调查离群值?它建立在统计学和 + 度量稳健性的精确方式(崩溃点)、抵抗性的替代品(中位数、MAD、M + 估计量),以及稳健性所逼出的 那个判断:修方法,还是调查离群值?它建立在 + 统计学和 数据准备页之上。

    - 这种脆弱来自平方。均值和最小二乘都最小化平方误差,而平方给了离群值巨大的 + 这种脆弱来自平方。均值和最小二乘都最小化平方 + 误差,而平方给了离群值巨大的 杠杆:一个离十个单位远的点,对损失贡献一百,于是估计值会不惜代价去迁就它。 - [1, 2, 3, 4, 1000] 的均值是 202——一个没有任何数据点靠近的值,是对「数据落在 - 哪里」一个无用的概括。一个点,全面扭曲。 + [1, 2, 3, 4, 1000] 的均值是 202——一个没有 + 任何数据点靠近的值,是对「数据落在 哪里」一个无用的概括。一个点,全面扭曲。

    同样的事发生在回归线上:单单一个高杠杆的离群值,就能把整个拟合旋转过去,产出一条歪曲了其他每个点都 @@ -313,7 +345,8 @@ function ZhBody() {

    - 稳健性有一个精确、漂亮的度量:崩溃点——在估计量给出一个无意义(无界地错误)的答案 + 稳健性有一个精确、漂亮的度量:崩溃点 + ——在估计量给出一个无意义(无界地错误)的答案 之前,数据中可以被任意污染的那个比例。它是这个领域的标志性数字。

    均值的崩溃点是 0%——单单一个被推向无穷的点就把它拖向无穷。 - 中位数的崩溃点是 50%——你可以污染(将近)一半的数据,而中位数仍然合理地坐落在好的 - 那一半之中。50% 是可能的最大值(超过一半,「离群值」就是数据了),这使中位数成为存在的最 + 中位数的崩溃点是 50% + ——你可以污染(将近)一半的数据,而中位数仍然合理地坐落在好的 那一半之中。50% + 是可能的最大值(超过一半,「离群值」就是数据了),这使中位数成为存在的最 稳健的集中趋势估计。那个差距——0% 对 50%——就是稳健统计的全部理由,浓缩在一个对比里。

    @@ -338,35 +372,38 @@ function ZhBody() {

    那些脆弱的经典量的稳健替代品,是你已经半知半解的:

    • - 对集中趋势:用中位数代替均值——不受极端值有多极端的影响,只受每一侧坐落着 - 多少个点的影响。 + 对集中趋势:用中位数 + 代替均值——不受极端值有多极端的影响,只受每一侧坐落着 多少个点的影响。
    • - 对离散程度:用 MAD(中位数绝对偏差)代替标准差。它是到中位数的绝对距离的 - 中位数——一种对中位数的两阶段使用(中心,然后典型偏差),继承了它 50% 的崩溃点。标准差建立在平方 - 偏差之上,会被单单一个离群值吹胀;MAD 则不当回事。 + 对离散程度:用 MAD + (中位数绝对偏差)代替标准差。它是到中位数的绝对距离的 + 中位数——一种对中位数的两阶段使用(中心,然后典型偏差),继承了它 50% + 的崩溃点。标准差建立在平方 偏差之上,会被单单一个离群值吹胀;MAD 则不当回事。

    - 这些不只是替代品——它们是抵抗性的基础,也是为什么一次稳健的分析常常在做别的之前,先悄悄地 - 把均值换成中位数、把标准差换成 MAD。 + 这些不只是替代品——它们是抵抗性的基础 + ,也是为什么一次稳健的分析常常在做别的之前,先悄悄地 把均值换成中位数、把标准差换成 MAD。

    - 中位数稳健,但扔掉了信息(它忽略实际的值,只看它们的顺序),所以当数据确实干净时它效率 - 较低。M 估计量是优雅的中间地带:不是最小化平方误差(它过度加权离群值)或绝对误差 - (稳健但效率较低),而是用一个对小残差表现得像平方误差、对大残差表现得像绝对误差的 - 损失。著名的例子是 Huber 损失: + 中位数稳健,但扔掉了信息(它忽略实际的值,只看它们的顺序),所以当数据确实 + 干净时它效率 较低。M 估计量 + 是优雅的中间地带:不是最小化平方误差(它过度加权离群值)或绝对误差 + (稳健但效率较低),而是用一个对小残差表现得像平方误差、对 + 大残差表现得像绝对误差的 损失。著名的例子是 Huber 损失

    {String.raw`L_\delta(r) = \begin{cases} \tfrac{1}{2}r^2 & |r| \le \delta \\[4pt] \delta\,(|r| - \tfrac{1}{2}\delta) & |r| > \delta \end{cases}`}

    - 在阈值 {String.raw`\delta`} 之下,它是高效的平方损失;之上,损失只线性增长,于是 - 一个遥远的离群值的影响被封顶,而非二次地爆炸。那一个弯就是全部的诀窍——它平滑地降低离群值的 - 权重,同时在表现良好的大多数上保持最小二乘的统计效率。M 估计量给你一个在稳健性与效率之间 + 在阈值 {String.raw`\delta`} 之下,它是高效的平方损失;之上,损失只线性 + 增长,于是 一个遥远的离群值的影响被封顶,而非二次地爆炸。那一个弯就是全部的诀窍——它 + 平滑地降低离群值的 权重 + ,同时在表现良好的大多数上保持最小二乘的统计效率。M 估计量给你一个在稳健性与效率之间 可调的旋钮,这正是为什么 Huber 损失也作为一个损失函数出现在 机器学习里。

    @@ -384,7 +421,8 @@ function ZhBody() { Huber / M 估计量回归——上面那种平滑的降权,应用到拟合上。
  • - RANSAC——在随机子集上拟合,保留最多点认同的那个模型,明确地把离群值当作「非共识」 + RANSAC + ——在随机子集上拟合,保留最多点认同的那个模型,明确地把离群值当作「非共识」 忽略掉。在计算机视觉里常见。
  • @@ -397,18 +435,21 @@ function ZhBody() {

    稳健统计逼出一个容易搞错的判断,而它是最重要的部分:

    - 稳健性与删除或忽略离群值不是一回事。一个稳健的方法降低一个离群值对估计的 - 影响——但那个离群值仍然在那里,而它可能是数据集里最重要的东西。一个极端值可以是一个要去 - 稳健化抵抗的打字错误,也可以是一个真实的信号——那个欺诈、那次入侵、那个突破——而这正是 - 异常检测所要猎取的。所以需要的纪律是:用稳健的 - 方法,让几个坏点不至于悄悄毁掉你的中心估计,并且始终亲自去看那些离群值,去判断它们是该 + 稳健性与删除或忽略离群值不是一回事。一个稳健的方法降低一个离群值 + 对估计的 影响 + ——但那个离群值仍然在那里,而它可能是数据集里最重要的东西。一个极端值可以是一个要去 + 稳健化抵抗的打字错误,也可以 + 是一个真实的信号——那个欺诈、那次入侵、那个突破——而这正是 + 异常检测 + 所要猎取的。所以需要的纪律是:用稳健的 方法,让几个坏点不至于悄悄毁掉你的中心估计, + 并且始终亲自去看那些离群值,去判断它们是该 折扣掉的错误,还是该去追的证据。盲目地稳健化,可能扔掉那个发现;盲目地信任最小二乘,会让一个打字 错误毁掉整个分析。本领在于同时握住两者。

    - (还有那个诚实的代价:当数据真的干净、表现良好的时候,稳健方法比经典方法稍微低效 - 一点——为你通常都有的、防污染的保险所付的一个小价钱。) + (还有那个诚实的代价:当数据真的干净、表现良好的时候,稳健方法比经典方法稍微 + 低效一点——为你通常都有的、防污染的保险所付的一个小价钱。)

    @@ -417,16 +458,19 @@ function ZhBody() {

    真实的政府数据很脏——打字错误、传感器错误、键入错误的条目、真实的极端案例——所以均值和最小二乘的 脆弱是一个活生生的风险,而非教科书的脚注。稳健统计给我的最有价值的本能,是在污染有可能时,用 - 中位数和 MAD而非均值和标准差,这样一小撮坏记录不至于悄悄劫持一个概括或一个拟合的 - 关系。崩溃点(均值 0%,中位数 50%)是记住为什么的那个干脆的方式。 + 中位数和 MAD + 而非均值和标准差,这样一小撮坏记录不至于悄悄劫持一个概括或一个拟合的 关系。 + 崩溃点(均值 0%,中位数 50%)是记住为什么的那个干脆的方式。

    - 但那个判断才是真正的教训:稳健 ≠ 忽略。廉政或情报数据里的一个离群值,可能正是 - 那个要紧的案件——所以我稳健化那个中心估计,调查那个离群值 - (异常检测的心态),而非让一个方法悄悄替我决定。 - 它连到数据准备(处理离群值)、 - 分位数回归(中位数回归是稳健的),以及那个更 - 宏大的主题——不要让几个点在任一方向上把你骗了。 + 但那个判断才是真正的教训:稳健 ≠ 忽略 + 。廉政或情报数据里的一个离群值,可能正是 + 那个要紧的案件——所以我稳健化那个中心估计,调查那个离群值 ( + 异常检测 + 的心态),而非让一个方法悄悄替我决定。 它连到 + 数据准备(处理离群值)、 + 分位数回归 + (中位数回归是稳健的),以及那个更 宏大的主题——不要让几个点在任一方向上把你骗了。

    @@ -435,33 +479,35 @@ function ZhBody() {
    • - 均值、标准差和最小二乘都脆弱——它们最小化平方误差,所以一个离群值 - 获得巨大的杠杆,劫持估计。 + 均值、标准差和最小二乘都脆弱——它们最小化平方 + 误差,所以一个离群值 获得巨大的杠杆,劫持估计。
    • 崩溃点度量稳健性——在估计变得无意义之前你能污染的数据比例。 均值 = 0%,中位数 = 50%(最大值)。
    • - 抵抗性的基础:中位数(对均值)和 MAD(对标准差)——都是 50% - 崩溃。 + 抵抗性的基础:中位数(对均值)和 MAD + (对标准差)——都是 50% 崩溃。
    • - M 估计量(例如 Huber 损失)= 对小残差用平方误差、对大的用 - 线性——封顶一个离群值的影响,同时保持效率。一个可调的稳健性/效率旋钮。 + M 估计量(例如 Huber 损失)= + 对小残差用平方误差、对大的用 线性——封顶一个离群值的影响 + ,同时保持效率。一个可调的稳健性/效率旋钮。
    • - 稳健回归:最小绝对偏差、Huber、RANSAC——拟合主体所支持的那条线。 + 稳健回归:最小绝对偏差、Huber、RANSAC——拟合主体 + 所支持的那条线。
    • - 关键的判断:稳健 ≠ 删除。稳健化估计,调查离群值——它可能就是信号。 - (数据干净时有小的效率代价。) + 关键的判断:稳健 ≠ 删除。稳健化估计, + 调查离群值——它可能就是信号。 (数据干净时有小的效率代价。)

    - 崩溃点、中位数/MAD 的抵抗性、Huber 损失 M 估计量,以及稳健化对调查的判断,反映了当前的稳健统计 - 参考文献以及课程。 + 崩溃点、中位数/MAD 的抵抗性、Huber 损失 M + 估计量,以及稳健化对调查的判断,反映了当前的稳健统计 参考文献以及课程。

    diff --git a/components/knowledge/content/sampling-survey-methodology.jsx b/components/knowledge/content/sampling-survey-methodology.jsx index ca357497..659eed89 100644 --- a/components/knowledge/content/sampling-survey-methodology.jsx +++ b/components/knowledge/content/sampling-survey-methodology.jsx @@ -1,5 +1,6 @@ import Link from "next/link"; -import { KSection, Callout, Formula, TeX, Term } from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/sampling-survey-methodology. diff --git a/components/knowledge/content/spatial-statistics.jsx b/components/knowledge/content/spatial-statistics.jsx index 49925d40..cad5c1f1 100644 --- a/components/knowledge/content/spatial-statistics.jsx +++ b/components/knowledge/content/spatial-statistics.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/spatial-statistics. @@ -25,8 +19,20 @@ function MoranFigure({ caption, ariaLabel, clusteredLabel, randomLabel }) { role="img" aria-label={ariaLabel} > - {clusteredLabel} - {randomLabel} + + {clusteredLabel} + + + {randomLabel} + {/* clustered grid: filled top-left block */} {[0, 1, 2, 3].map((r) => [0, 1, 2, 3].map((c) => { @@ -311,35 +317,39 @@ function ZhBody() { <>

    给数据一个位置,某种微妙的事就会发生:标准的统计工具箱悄悄不再有效。大多数方法假设你的观测是 - 独立的——但彼此邻近的地方明确地独立。相邻的街区收入相似,相邻的地区天气 - 相似,邻近的区域犯罪率相似。空间统计是为地理要紧的数据而建的分支,它的全部目的,是认真 - 对待那种空间依赖,而非假装它不存在。 + 独立的——但彼此邻近的地方明确地 + 独立。相邻的街区收入相似,相邻的地区天气 相似,邻近的区域犯罪率相似。空间统计 + 是为地理要紧的数据而建的分支,它的全部目的,是认真 对待那种空间依赖,而非假装它不存在。

    - 它与 GIS 页不同——那一页是处理空间数据的工具;这 - 一页是位置的推断统计学:如何度量空间模式、检验一个聚集是否真实,以及在空间上做预测。这一页 + 它与 GIS{" "} + 页不同——那一页是处理空间数据的工具;这 一页是位置的推断统计学 + :如何度量空间模式、检验一个聚集是否真实,以及在空间上做预测。这一页 讲核心的想法,而反复出现的主题是:忽略「在哪里」会产出自信而错误的答案。

    - 经典统计里几乎每一种方法,都建立在独立观测 - 的假设之上——知道一个数据点,告诉不了你关于下一个的任何东西。空间数据公然违反这一点:知道 - 一个街区的值,告诉你很多关于它邻居的事。这种空间自相关意味着你的有效样本量比它看上去 - 的要小(邻近的点携带冗余的信息),所以普通的分析报告过度自信的结果——并不存在的 - 显著性、被共同位置吹胀的相关。 + 经典统计里几乎每一种方法,都建立在 + 独立观测 + 的假设之上——知道一个数据点,告诉不了你关于下一个的任何东西。空间数据公然违反这一点:知道 + 一个街区的值,告诉你很多关于它邻居的事。这种空间自相关 + 意味着你的有效样本量比它看上去 的要小(邻近的点携带冗余的信息),所以普通的分析报告 + 过度自信的结果——并不存在的 显著性、被共同位置吹胀的相关。

    - 所以空间统计做两件事:它度量空间依赖(有没有模式,在哪里?),并在模型里考虑它, - 好让结论保持诚实。两者都从一个奠基性的想法出发。 + 所以空间统计做两件事:它度量空间依赖(有没有模式,在哪里?),并在模型里 + 考虑它, 好让结论保持诚实。两者都从一个奠基性的想法出发。

    - 托布勒的地理学第一定律把它说得很直白:「万物彼此相关,但近的东西比远的东西更 - 相关。」那是整个领域的引擎。空间自相关可以是正的(常见情形——相似的值聚在 - 一起,比如财富或温度),或者更罕见地,负的(高值系统性地紧挨着低值,像一张 + 托布勒的地理学第一定律把它说得很直白: + 「万物彼此相关,但近的东西比远的东西更 相关。」 + 那是整个领域的引擎。空间自相关可以是正的(常见情形——相似的值聚在 + 一起,比如财富或温度),或者更罕见地,负的 + (高值系统性地紧挨着低值,像一张 棋盘)。下面这些度量的目标,是检测并量化正在发生的是哪一种、以及在哪里——把「那看起来很聚集」一个 模糊的印象,变成一个可检验的论断。

    @@ -347,8 +357,9 @@ function ZhBody() {

    - 在你能度量空间关系之前,你必须把什么算作「近」形式化。那就是空间权重矩阵——对每一对 - 位置,一个说明它们有多连通的权重。常见的选择:邻接(共享一条边界的地区是邻居)、 + 在你能度量空间关系之前,你必须把什么算作「近」形式化。那就是空间权重矩阵 + ——对每一对 位置,一个说明它们有多连通的权重。常见的选择:邻接 + (共享一条边界的地区是邻居)、 距离(k 公里之内的一切),或 k-最近(每个区域最近的 k 个其他区域)。

    @@ -363,16 +374,18 @@ function ZhBody() {

    空间自相关的标准全局度量是 Moran's I。它本质上是空间的一个 - 相关系数:它问一个位置的值是否倾向于与它邻居的值相匹配, - 在整张地图上。 + 相关 + 系数:它问一个位置的值是否倾向于与它邻居的值相匹配, 在整张地图上。

    {String.raw`I = \frac{N}{\sum_{i}\sum_{j} w_{ij}} \cdot \frac{\sum_{i}\sum_{j} w_{ij}\,(x_i - \bar{x})(x_j - \bar{x})}{\sum_{i} (x_i - \bar{x})^2}`}

    - 你不需要背公式——读它的行为。Moran's I 大致从 −1 跑到 +1:一个远高于 0的值意味着正 - 自相关(聚集——相似的值彼此邻近),接近 0意味着一个随机的空间模式,而 - 低于 0意味着分散。关键的是,你要检验它的显著性(对照「随机排列」的零假设),这样你 + 你不需要背公式——读它的行为。Moran's I 大致从 −1 跑到 +1:一个远高于 0 + 的值意味着正 自相关(聚集——相似的值彼此邻近),接近 0 + 意味着一个随机的空间模式,而 + 低于 0 + 意味着分散。关键的是,你要检验它的显著性(对照「随机排列」的零假设),这样你 就能说一个表面的聚集是真实的、还是很容易出于偶然——这正是发现一个真正的模式,与在云里看见人脸之间 的差别。

    @@ -387,7 +400,8 @@ function ZhBody() {

    Moran's I 对整张地图给出一个数字——但通常有趣的问题是聚集在哪里。 - LISA(空间关联局部指标)把这个全局的统计量分解成每个位置的一个值,揭示出局部的结构。 + LISA + (空间关联局部指标)把这个全局的统计量分解成每个位置的一个值,揭示出局部的结构。 它把每个区域分类为属于一个:

      @@ -398,8 +412,8 @@ function ZhBody() { 低—低聚集——一个冷点(低值之中的低值)。
    • - 高—低 / 低—高——空间离群值:一个与其周遭对着干的值(安全地区里 - 一个高犯罪的口袋),往往是所有情形里最有趣的。 + 高—低 / 低—高——空间离群值 + :一个与其周遭对着干的值(安全地区里 一个高犯罪的口袋),往往是所有情形里最有趣的。

    @@ -411,12 +425,14 @@ function ZhBody() {

    当你建模一个空间的结果(收入能否预测各地区的健康?)时,如果残差有空间自相关,普通 - 回归就无效了——独立性假设被打破,标准误在 - 撒谎。空间回归通过把空间结构建进模型里来修这个: + 回归 + 就无效了——独立性假设被打破,标准误在 撒谎。空间回归 + 通过把空间结构建进模型里来修这个:

    • - 空间滞后模型——把邻居的结果值作为一个预测变量纳入,捕捉真正的溢出(一个地区的值被 + 空间滞后 + 模型——把邻居的结果值作为一个预测变量纳入,捕捉真正的溢出(一个地区的值被 它邻居的所塑造)。
    • @@ -424,20 +440,23 @@ function ZhBody() {

    - 无论哪种方式,要点都与别处的因果纪律一样:尊重数据的 - 结构,否则你的自信是假的。 + 无论哪种方式,要点都与别处的因果 + 纪律一样:尊重数据的 结构,否则你的自信是假的。

    - 最后一个大工具是空间预测。你在散落的位置上测量了一个值(气象站的降雨、采样点的一种 - 污染物),想估计它在中间的每一处克里金做这件事,而它比朴素的插值更聪明:它 - 使用测量出的空间自相关结构——相似性随距离衰减得有多快——在每个未采样的点上做出统计上最优的 + 最后一个大工具是空间预测 + 。你在散落的位置上测量了一个值(气象站的降雨、采样点的一种 污染物),想估计它 + 在中间的每一处克里金做这件事,而它比朴素的插值更聪明:它 使用 + 测量出的 + 空间自相关结构——相似性随距离衰减得有多快——在每个未采样的点上做出统计上最优的 预测,还附带一个不确定性估计。

    - 它是时间序列页上那些预测想法的空间兄弟(在空间上 + 它是时间序列 + 页上那些预测想法的空间兄弟(在空间上 插值,而非在时间上外推),也是你把一小撮采样点变成一个连续、诚实的曲面的方式——带着那个关键的告诫: 你离任何真实的测量越远,不确定性就越大。

    @@ -448,16 +467,16 @@ function ZhBody() {

    大量的政府分析无可逃避地是空间性的——事件、区域模式、把资源往哪里调——而这个学科给我的最有价值的 一样东西,是一个统计上显著的热点,与一个只是噪声的抢眼聚集之间的差别。 - Moran's I 检验到底有没有真实的空间结构,而 LISA 钉住真正的热点 - (以及那些说明问题的离群值)在哪里——这正是你在对「它在哪里最严重?」采取行动之前所 - 需要的。 + Moran's I 检验到底有没有真实的空间结构,而 LISA{" "} + 钉住真正的热点 (以及那些说明问题的离群值)在哪里 + ——这正是你在对「它在哪里最严重?」采取行动之前所 需要的。

    - 它还让建模保持诚实:把空间依赖的数据当作独立的来处理,会产出过度自信的结论, - 所以空间回归是阻止一项区域分析夸大它确定性的东西——与 + 它还让建模保持诚实:把空间依赖的数据当作独立的来处理,会产出过度自信 + 的结论, 所以空间回归是阻止一项区域分析夸大它确定性的东西——与 因果推断同样的「尊重结构」的教训。它补足 - GIS 页(工具对推断),并共享那个关于「面积单元 - 的选择如何塑造一切」的 MAUP 告诫。 + GIS{" "} + 页(工具对推断),并共享那个关于「面积单元 的选择如何塑造一切」的 MAUP 告诫。

    @@ -466,32 +485,32 @@ function ZhBody() {
    • - 空间数据打破独立性假设——邻近的地方相关(空间自相关),所以 - 朴素的统计过度自信。 + 空间数据打破独立性假设——邻近的地方相关(空间自相关 + ),所以 朴素的统计过度自信
    • - 托布勒第一定律:近的东西比远的东西更相关。正自相关(聚集)是常见情形。 + 托布勒第一定律 + :近的东西比远的东西更相关。正自相关(聚集)是常见情形。
    • - 通过空间权重矩阵定义邻居(邻接 / 距离 / k-最近)——一个真正的 - 建模选择。 + 通过空间权重矩阵定义邻居(邻接 / 距离 / + k-最近)——一个真正的 建模选择。
    • - Moran's I = 空间的一个相关(远高于 0 = 聚集;≈0 = 随机),检验显著性。 + Moran's I = 空间的一个相关(远高于 0 = 聚集;≈0 = + 随机),检验显著性。 LISA 找到在哪里——高—高热点、低—低冷点,以及离群值。
    • - 空间回归(滞后/误差模型)把结构建进去,好让推断保持诚实。克里金 - 用自相关在采样点之间预测,附带不确定性。 -
    • -
    • - 贯穿始终的:尊重「在哪里」,否则得到自信而错误的答案(并留意 MAUP)。 + 空间回归(滞后/误差模型)把结构建进去,好让推断保持诚实。 + 克里金用自相关在采样点之间预测,附带不确定性。
    • +
    • 贯穿始终的:尊重「在哪里」,否则得到自信而错误的答案(并留意 MAUP)。

    - 托布勒定律、Moran's I / LISA 热点框架、空间权重,以及克里金,反映了当前的空间统计参考文献以及 - 统计学课程。 + 托布勒定律、Moran's I / LISA + 热点框架、空间权重,以及克里金,反映了当前的空间统计参考文献以及 统计学课程。

    diff --git a/components/knowledge/content/statistical-machine-learning.jsx b/components/knowledge/content/statistical-machine-learning.jsx index 4b90f2b2..6ef5ac4e 100644 --- a/components/knowledge/content/statistical-machine-learning.jsx +++ b/components/knowledge/content/statistical-machine-learning.jsx @@ -1,5 +1,6 @@ import Link from "next/link"; -import { KSection, Callout, Formula, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/statistical-machine-learning. diff --git a/components/knowledge/content/statistical-modelling.jsx b/components/knowledge/content/statistical-modelling.jsx index be7fbf6c..b56c52f5 100644 --- a/components/knowledge/content/statistical-modelling.jsx +++ b/components/knowledge/content/statistical-modelling.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/statistical-modelling. diff --git a/components/knowledge/content/statistical-process-control.jsx b/components/knowledge/content/statistical-process-control.jsx index 3dd2d8d9..f3c5f045 100644 --- a/components/knowledge/content/statistical-process-control.jsx +++ b/components/knowledge/content/statistical-process-control.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/statistical-process-control. @@ -41,25 +35,97 @@ function ControlChartFigure({ caption, ariaLabel, centreLabel, specialCauseLabel aria-label={ariaLabel} > {/* limit lines */} - - UCL - - {centreLabel} - - LCL + + + UCL + + + + {centreLabel} + + + + LCL + {/* points */} {PTS.map(([x, y], i) => { const breach = y < 28; return ( {i > 0 && ( - + )} - + ); })} - {specialCauseLabel} + + {specialCauseLabel} + ); @@ -299,24 +365,29 @@ function ZhBody() { <>

    一个你随时间盯着的指标——一段处理时间、一个错误率、一个每日计数——总在抖动。每次它一动,那个难题 - 就是:真有什么变了,还是这只是这个过程一直都有的正常抖动?对噪声反应过度,你就把 + 就是:真有什么变了,还是这只是这个过程一直都有的正常抖动? + 对噪声反应过度,你就把 力气浪费在追鬼上(而且往往把事情弄得更糟);忽略一个真实的偏移,你就错过一个真正的问题。 - 统计过程控制(SPC)是把这两者分辨开的、有几十年历史的、极为实用的学问——好让你对真实 + 统计过程控制 + (SPC)是把这两者分辨开的、有几十年历史的、极为实用的学问——好让你对真实 的信号做出回应,而把噪声留在那里别动。

    SPC 诞生于制造业(休哈特与戴明),却适用于任何重复的过程,它是 异常检测和 - 流式监控的近亲,有一个锋利的概念内核。这一页讲的 + 流式监控 + 的近亲,有一个锋利的概念内核。这一页讲的 就是那个内核:它核心处的变异区分、把它落地的控制图,以及它那著名界限背后的取舍。

    - 整个领域都建立在一次重新框定之上。在 SPC 之前,人们对每一次起落都做出反应——糟糕的一天的数字会 - 触发一阵手忙脚乱去「修」点什么,即便其实什么都没变。休哈特的洞见是:一个稳定的过程仍然会 - 变化,而把那种固有的变异当作信号来反应——瞎调——通常把过程弄得更糟、 - 而非更好。SPC 的工作是划出一条有原则的线:这么多抖动是正常的,别去动它;那个是一个真正的 + 整个领域都建立在一次重新框定之上。在 SPC + 之前,人们对每一次起落都做出反应——糟糕的一天的数字会 + 触发一阵手忙脚乱去「修」点什么,即便其实什么都没变。休哈特的洞见是: + 一个稳定的过程仍然会 变化,而把那种固有的变异当作信号来反应—— + 瞎调——通常把过程弄得更糟、 而非更好。SPC + 的工作是划出一条有原则的线:这么多抖动是正常的,别去动它;那个是一个真正的 信号,去调查它。

    @@ -325,26 +396,30 @@ function ZhBody() {

    那个奠基性的区分——而且可以说是运营分析里最有用的单一想法:

    • - 常因变异——一个稳定过程自然、固有的抖动。许多微小的、无处不在的影响(条件、材料、 - 时机上的细微差别)。它在范围上是可预测的,即便任何单个值不是,而它不值得去追 - ——这就是这个过程本来的样子。 + 常因变异 + ——一个稳定过程自然、固有的抖动。许多微小的、无处不在的影响(条件、材料、 + 时机上的细微差别)。它在范围上是可预测的,即便任何单个值不是,而它 + 不值得去追——这就是这个过程本来的样子。
    • - 特因变异——一个不寻常的、可归因的原因:真有什么变了(一个新供应商、一件 - 坏掉的工具、一次过程变更)。这产生一个落在正常范围之外的值(或模式),而它确实 + 特因变异——一个不寻常的、可归因的 + 原因:真有什么变了(一个新供应商、一件 + 坏掉的工具、一次过程变更)。这产生一个落在正常范围之外的值(或模式),而它 + 确实 值得调查——有一个真实的原因可找。

    一个只表现出常因变异的过程是受控的(稳定且可预测);一个有特因变异的过程是 - 失控的(有要采取行动的东西)。SPC 的全部机制,都是为了可靠地把这两者分开——好让你只在 - 真有要行动的东西时才行动。 + 失控的(有要采取行动的东西)。SPC + 的全部机制,都是为了可靠地把这两者分开——好让你只在 真有要行动的东西时才行动。

    - 把这一切落地的工具是控制图:随时间画出这个指标,配上三条参考线——一条在过程平均值处的 + 把这一切落地的工具是控制图 + :随时间画出这个指标,配上三条参考线——一条在过程平均值处的 中心线,以及标出正常变异边界的上、下控制限(UCL/LCL)。

    - 只要点在界限内毫无规律地跳来跳去,过程就是受控的——那只是常因噪声,正确的行动是。当一个 + 只要点在界限内毫无规律地跳来跳去,过程就是受控的——那只是常因噪声,正确的行动是 + 。当一个 点越过一条界限,那就是一个值得调查的特因信号。这张图把一个模糊的「那看起来偏高」变成一条有原则、 可重复的决策规则。

    -

    控制限按惯例设在中心线加减过程的三个标准差处:

    +

    + 控制限按惯例设在中心线加减过程的三个标准差处: +

    {String.raw`\text{UCL},\,\text{LCL} = \mu \pm 3\sigma`}

    - 为什么是三?这是一个深思熟虑的成本取舍,而认识到这一点是用好 SPC 的关键。对于 - 大致呈钟形的数据,只有约 0.3% 的点会偶然落在 ±3σ 之外——所以一次越界很可能是一个真实的 + 为什么是三?这是一个深思熟虑的成本取舍,而认识到这一点是用好 SPC + 的关键。对于 大致呈钟形的数据,只有约 0.3% 的点会偶然落在 ±3σ 之外——所以一次越界 + 很可能是一个真实的 信号,而非运气。把界限设得更紧(±2σ),你会更早捕捉到真实的偏移,但你也会得到多得多的 假警报——而对那些做出反应(瞎调)会浪费力气并使过程失稳。

    这正是假阳性对假阴性的取舍——与异常检测里同样的 - 警报疲劳张力。±3σ 是经过长期检验的甜蜜点:假 + 警报疲劳张力。±3σ + 是经过长期检验的甜蜜点:假 警报够罕见,使一个信号值得信任;够敏感,能捕捉一次真实的、大的偏移。三西格玛不是一条自然法则—— 它是关于「狼来了」的成本与错过一次变化的成本之间,一个校准良好的赌注。

    @@ -384,58 +464,69 @@ function ZhBody() {

    一个落在界限之外的点是显而易见的信号——但一个过程可能以没有任何单个点能捕捉的方式漂移。 - 西部电气规则(以及纳尔逊的)增加了一些模式,即便每个点都在界限之内,也会 - 标记出一个特因: + 西部电气规则(以及纳尔逊的)增加了一些模式,即便每个点都在界限之内 + ,也会 标记出一个特因:

      -
    • 连续好几个点全都在中心线同一侧的一连串(过程已经偏移)。
    • -
    • 点稳定地向上或向下行进的一个趋势(逐渐的漂移)。
    • +
    • + 连续好几个点全都在中心线同一侧的一连串(过程已经偏移)。 +
    • +
    • + 点稳定地向上或向下行进的一个趋势(逐渐的漂移)。 +
    • 太多点聚集在远离中心之处,或其他非随机的模式。

    - 贯穿始终的逻辑都一样:一个稳定的过程在中心周围应当看起来随机。任何 - 非随机的模式——一连串、一个趋势、一个周期——都是某种系统性东西的指纹,即一个特因,甚至在 + 贯穿始终的逻辑都一样:一个稳定的过程在中心周围应当看起来随机 + 。任何 + 非随机 + 的模式——一连串、一个趋势、一个周期——都是某种系统性东西的指纹,即一个特因,甚至在 一个点越界之前。SPC 既是关于发现结构,也是关于发现离群值。

    - 经典的 休哈特图只用当前的点,这使它非常擅长捕捉的突然偏移,却迟于注意到 - 一个的持续漂移。为此,有两种图使用历史: + 经典的 休哈特图只用当前的点,这使它非常擅长捕捉 + 的突然偏移,却迟于注意到 一个的持续漂移。为此,有两种图使用历史

    • - CUSUM(累积和)——累加偏离目标的运行中偏差,于是即便一个小的、一致的偏倚也会累加成 + CUSUM + (累积和)——累加偏离目标的运行中偏差,于是即便一个小的、一致的偏倚也会累加成 一个清晰的信号。
    • EWMA(指数加权移动平均)——一个强调近期点的加权平均(与 - 时间序列同样的平滑想法),对逐渐的移动敏感。 + 时间序列 + 同样的平滑想法),对逐渐的移动敏感。

    - 两者都被调来捕捉休哈特图会错过的慢漂移——你为你所担心的那变化挑选图。(还有针对不同 - 数据的图类型:测量值用 X-bar/R 图,比例用 p 图。) + 两者都被调来捕捉休哈特图会错过的慢漂移——你为你所担心的那 + 变化挑选图。(还有针对不同 数据的图类型:测量值用 X-bar/R 图,比例用 p 图。)

    - 任何随时间被盯着的运营指标或 KPI——一段处理时间、一个量、一个错误或合规率——都引出 SPC 所回答的 - 那个问题:这是一次真实的变化,还是只是正常的波动?最有价值的单一想法是 - 常因对特因的区分:大多数月与月之间的抖动是不该触发行动的常因噪声,而对它做出反应 + 任何随时间被盯着的运营指标或 KPI——一段处理时间、一个量、一个错误或合规率——都引出 SPC + 所回答的 那个问题:这是一次真实的变化,还是只是正常的波动? + 最有价值的单一想法是 + 常因对特因 + 的区分:大多数月与月之间的抖动是不该触发行动的常因噪声,而对它做出反应 (瞎调)会把事情弄得更糟——一种省下大量被浪费的力气和过度纠正的纪律。

    - 一张控制图把「那个数字看起来不对劲」变成一条站得住脚的规则,而 ±3σ - 的选择就是贯穿异常检测的那个同样的 + 一张控制图把「那个数字看起来不对劲」变成一条站得住脚的规则,而{" "} + ±3σ 的选择就是贯穿异常检测的那个同样的 假警报取舍(以及 - 精确率/召回率张力)——为「狼来了」的成本对错过一次真实 - 偏移的成本去设定它。它与流式(实时监控)和 - 时间序列(底层的信号)天然相配,是整个工具箱里 - 最真正实用的工具之一。 + 精确率/召回率 + 张力)——为「狼来了」的成本对错过一次真实 偏移的成本去设定它。它与 + 流式(实时监控)和 + 时间序列 + (底层的信号)天然相配,是整个工具箱里 最真正实用的工具之一。

    @@ -444,23 +535,24 @@ function ZhBody() {
    • - SPC 在一个过程随时间里把真实的变化与正常的噪声分辨开——好让你对信号采取行动, - 而不再追逐抖动(也不再瞎调)。 + SPC 在一个过程随时间里把真实的变化与正常的噪声 + 分辨开——好让你对信号采取行动, 而不再追逐抖动(也不再瞎调)。
    • - 核心的区分:常因(固有的抖动,别动它)对特因(可归因的,去 - 调查)。受控对失控。 + 核心的区分:常因(固有的抖动,别动它)对特因 + (可归因的,去 调查)。受控对失控。
    • - 一张控制图画出这个指标,配上一条中心线 + 控制限;一次越界 - 标志着一个特因。 + 一张控制图画出这个指标,配上一条中心线 + 控制限 + ;一次越界 标志着一个特因。
    • - 界限设在 μ ± 3σ——一个深思熟虑的假警报对错过偏移的取舍 - (约 0.3% 的偶然越界概率;警报疲劳的张力)。 + 界限设在 μ ± 3σ——一个深思熟虑的假警报对错过偏移 + 的取舍 (约 0.3% 的偶然越界概率;警报疲劳的张力)。
    • - 西部电气 / 纳尔逊规则捕捉界限内的一连串和趋势——一个稳定的过程应当看起来 + 西部电气 / 纳尔逊规则 + 捕捉界限内的一连串和趋势——一个稳定的过程应当看起来 随机;非随机的模式就是信号。
    • @@ -469,8 +561,8 @@ function ZhBody() {

    - 常因/特因的框架、±3σ 控制图、西部电气规则,以及用于小偏移的 CUSUM/EWMA,反映了当前的 SPC 参考 - 文献以及质量课程。 + 常因/特因的框架、±3σ 控制图、西部电气规则,以及用于小偏移的 CUSUM/EWMA,反映了当前的 SPC + 参考 文献以及质量课程。

    diff --git a/components/knowledge/content/statistics.jsx b/components/knowledge/content/statistics.jsx index 3636382e..7a525c4a 100644 --- a/components/knowledge/content/statistics.jsx +++ b/components/knowledge/content/statistics.jsx @@ -1,5 +1,6 @@ import Link from "next/link"; -import { KSection, Callout, Formula, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/statistics. diff --git a/components/knowledge/content/streaming-analytics.jsx b/components/knowledge/content/streaming-analytics.jsx index 9ecb40fb..4ea0587d 100644 --- a/components/knowledge/content/streaming-analytics.jsx +++ b/components/knowledge/content/streaming-analytics.jsx @@ -36,13 +36,38 @@ function WindowFigure({ caption, ariaLabel, tumblingLabel, slidingLabel }) { {EVENT_X.map((x, i) => ( ))} - {tumblingLabel} + + {tumblingLabel} + {TUMBLING.map(([x0, x1], i) => ( - + ))} - {slidingLabel} + + {slidingLabel} + {SLIDING.map(([x0, x1], i) => ( - + ))} @@ -287,13 +312,15 @@ function ZhBody() { return ( <>

    - 这一节里几乎所有东西都假设批处理:你有一个数据集,你分析它,你得到一个答案。但有一整类 - 问题等不了数据集变完整——你需要在数据到达时就得到答案。在欺诈发生那一刻抓住它、在一个传感器 - 越过阈值时发警报、更新一个实时仪表盘——这些需要流处理:连续地、作为一股无尽的事件流来 - 分析数据,而非分周期地成批分析。 + 这一节里几乎所有东西都假设批处理 + :你有一个数据集,你分析它,你得到一个答案。但有一整类 问题等不了数据集变完整——你需要在 + 数据到达时就得到答案。在欺诈发生那一刻抓住它、在一个传感器 + 越过阈值时发警报、更新一个实时仪表盘——这些需要流处理 + :连续地、作为一股无尽的事件流来 分析数据,而非分周期地成批分析。

    - 它是一门真正不同的学问,而不只是「批处理但更快」,因为数据是无限的——而那一个事实 + 它是一门真正不同的学问,而不只是「批处理但更快」,因为数据是无限的 + ——而那一个事实 逼出一些巧妙的重新思考,尤其是关于时间。这一页讲实用的地形:流式与批处理有何不同、让无限数据可处理的 开窗想法、「一个事件何时发生」这个微妙的问题,以及运行它的技术栈。它建立在 分布式计算页之上。 @@ -303,32 +330,36 @@ function ZhBody() {

    这两种范式回答不同的问题。批处理在一个有界的数据集上运行——昨天所有的 交易——并产出一个完整、正确的答案,但只在数据被收集、作业运行之后(数分钟到数小时的延迟)。 - 流处理在一股无界的流上运行——每笔交易在它发生的那一瞬间——以亚秒级的延迟 - 产出连续更新的答案。 + 流处理在一股无界 + 的流上运行——每笔交易在它发生的那一瞬间——以亚秒级的延迟 产出连续更新的答案。

    取舍是延迟对完整性。批处理更简单、给你完整的图景,但来得晚;流式是即时的,但必须对仍在到达的 - 数据进行推理。当答案的及时性值那份额外的复杂度时——当一个小时后的答案毫无价值时——你才 - 动用流式。 + 数据进行推理。当答案的及时性 + 值那份额外的复杂度时——当一个小时后的答案毫无价值时——你才 动用流式。

    - 那个决定性的挑战是,一股流是无限的。你没法「加载数据集」——没有尽头。你没法计算 + 那个决定性的挑战是,一股流是无限的 + 。你没法「加载数据集」——没有尽头。你没法计算 一个简单的平均值,因为这个平均值所覆盖的数据永不停止增长。每一个熟悉的聚合,都得为一股不会结束的 流重新思考。

    - 一股流是一串事件,每一个都是一条盖了时间戳的、小而不可变的记录——一次点击、一笔交易、 - 一个传感器读数。处理必须是连续有状态的(它在事件流经时记住一个运行中的 + 一股流是一串事件 + ,每一个都是一条盖了时间戳的、小而不可变的记录——一次点击、一笔交易、 + 一个传感器读数。处理必须是连续有状态 + 的(它在事件流经时记住一个运行中的 概括),因为它永远没法回头去重读整段历史。让无限数据变得可处理的关键一步,是把它切成有限的小块。

    - 你没法对一股无限流的「全部」求平均,但你可以对「最近五分钟」求平均。一个窗口是 + 你没法对一股无限流的「全部」求平均,但你可以对「最近五分钟」求平均。一个 + 窗口是 流的一个有限切片,你在它上面做计算——而开窗是流处理的核心想法。主要的种类:

    • - 滚动——固定大小、不重叠(「每 5 分钟」)。每个事件恰好属于一个窗口。适合规则的、 - 离散的聚合。 + 滚动——固定大小、不重叠(「每 5 + 分钟」)。每个事件恰好属于一个窗口。适合规则的、 离散的聚合。
    • - 滑动——固定大小但相互重叠,以一小步推进(「最近 5 分钟,每 30 秒更新」)。每个事件 - 落在好几个窗口里——是平滑滚动平均的理想之选。 + 滑动——固定大小但相互重叠,以一小步推进(「最近 5 分钟,每 30 + 秒更新」)。每个事件 落在好几个窗口里——是平滑滚动平均的理想之选。
    • - 会话——由活动间隙定义的动态窗口(一个用户的一阵点击,在他们安静下来后关闭)。窗口 + 会话 + ——由活动间隙定义的动态窗口(一个用户的一阵点击,在他们安静下来后关闭)。窗口 大小随数据自适应。
    @@ -355,8 +387,8 @@ function ZhBody() {

    - 这里有一个流式独有的微妙问题:每个事件都有两个不同的时间,而把它们搞混会污染你的 - 结果。 + 这里有一个流式独有的微妙问题:每个事件都有两个不同的时间 + ,而把它们搞混会污染你的 结果。

    • @@ -367,18 +399,20 @@ function ZhBody() {

    - 在一个完美的世界里它们会一致。现实中,事件迟到且乱序地到达——一部手机失去信号, - 二十分钟后才上传它的读数,于是一个在 3:00 发生的事件直到 3:20 才到达。如果你按 - 处理时间分桶,那个读数会落进错误的窗口,你的「3:00–3:05」总数就错了。你几乎总是想按事件 - 时间聚合,才能得到正确的答案。 + 在一个完美的世界里它们会一致。现实中,事件迟到且乱序 + 地到达——一部手机失去信号, 二十分钟后才上传它的读数,于是一个在 3:00 发生 + 的事件直到 3:20 才到达。如果你按 + 处理时间分桶,那个读数会落进错误的窗口,你的「3:00–3:05」总数就错了。你几乎总是想按 + 事件 时间聚合,才能得到正确的答案。

    - 但事件时间制造了一个两难:在关闭一个窗口之前,你要等掉队者多久?永远等下去,你就永远 - 产不出结果;关得太早,你就漏掉迟到的数据。水位线是系统的答案——一个移动的标记, - 宣告「我现在有理由相信,我已经看到了截至时刻 T 的所有事件」,这触发窗口关闭并发出它的结果。它是 - 对流式那个根本取舍的一个显式、可调的赌注:延迟对完整性。没有办法把两者都完美地 - 拥有——水位线就是你选择平衡点的地方。 + 但事件时间制造了一个两难:在关闭一个窗口之前,你要等掉队者多久 + ?永远等下去,你就永远 产不出结果;关得太早,你就漏掉迟到的数据。水位线 + 是系统的答案——一个移动的标记, 宣告「我现在有理由相信,我已经看到了截至时刻 T + 的所有事件」,这触发窗口关闭并发出它的结果。它是 + 对流式那个根本取舍的一个显式、可调的赌注:延迟对完整性 + 。没有办法把两者都完美地 拥有——水位线就是你选择平衡点的地方。

    @@ -393,11 +427,12 @@ function ZhBody() { 至多一次——事件可能在失败时被丢弃。快、有损;很少可接受。
  • - 至少一次——没有事件丢失,但有些可能在重试时被处理两次(所以一个计数可能多报)。 - 常见的默认。 + 至少一次 + ——没有事件丢失,但有些可能在重试时被处理两次(所以一个计数可能多报)。 常见的默认。
  • - 恰好一次——黄金标准:每个事件恰好影响结果一次,即便经历失败也是如此。靠检查点和 + 恰好一次 + ——黄金标准:每个事件恰好影响结果一次,即便经历失败也是如此。靠检查点和 细致的协调来实现——更昂贵,但在重复计数会成为真正的问题时(钱、合规)是必不可少的。
  • @@ -409,32 +444,37 @@ function ZhBody() {

    - 一个流式系统通常分成两个角色。一个消息代理——Kafka 是标准——是那条 + 一个流式系统通常分成两个角色。一个消息代理——Kafka{" "} + 是标准——是那条 持久的管道:它摄入事件,把它们存放在有序的日志里,好让生产者和消费者解耦、且什么都不丢失。一个 流处理器——Flink,或 Spark Structured Streaming(绑定到 - Spark 引擎)——做实际的计算:上面讲的开窗、 - 有状态的聚合、事件时间逻辑。 + Spark{" "} + 引擎)——做实际的计算:上面讲的开窗、 有状态的聚合、事件时间逻辑。

    - 你还会听到 LambdaKappa 架构——大体上,是你运行分开的批处理层和流式层 - (Lambda),还是把一切都当作流来处理(Kappa)。还有与机器学习的联系:流式是在线学习和 - MLOps 页里那种实时漂移检测的天然归宿——一个随数据 - 流动而被更新或监控、而非每夜重训的模型。 + 你还会听到 LambdaKappa{" "} + 架构——大体上,是你运行分开的批处理层和流式层 + (Lambda),还是把一切都当作流来处理(Kappa)。还有与机器学习的联系:流式是 + 在线学习MLOps{" "} + 页里那种实时漂移检测的天然归宿——一个随数据 流动而被更新或监控、而非每夜重训的模型。

    - 需要流式的问题,是那些及时性就是全部重点的问题——对进来的数据做实时监控和 - 警报、在一个异常发生那一刻抓住它,而非在明天的 + 需要流式的问题,是那些及时性就是全部重点 + 的问题——对进来的数据做实时监控和 警报、在一个 + 异常发生那一刻抓住它,而非在明天的 报告里。懂得这门学问,正是让我能判断「流式那份额外的复杂度何时比一个简单的批处理作业更划算」 (往往并不划算——当一个小时后的答案可接受时,批处理更简单也够用)。

    - 而在实践中最要紧的概念,是开窗(你如何聚合一股无尽的流)与事件时间对 - 处理时间的区分——因为迟到、乱序的数据悄悄污染一个实时计数,正是那种破坏对实时仪表盘 - 信任的微妙错误。它连到分布式计算(引擎)、 + 而在实践中最要紧的概念,是开窗(你如何聚合一股无尽的流)与 + 事件时间对 处理时间 + 的区分——因为迟到、乱序的数据悄悄污染一个实时计数,正是那种破坏对实时仪表盘 + 信任的微妙错误。它连到分布式计算 + (引擎)、 异常检测(警报),以及 MLOps(实时监控)。

    @@ -445,23 +485,25 @@ function ZhBody() {
    • - 批处理 = 有界数据、完整答案、高延迟。 = 无界流、连续答案、 - 低延迟。当及时性是重点时用流式。 + 批处理 = 有界数据、完整答案、高延迟。 = + 无界流、连续答案、 低延迟。当及时性是重点时用流式。
    • - 一股流是无限的,所以处理是连续且有状态的——你没法重读整段 - 历史。 + 一股流是无限的,所以处理是连续且有状态 + 的——你没法重读整段 历史。
    • - 开窗让它变有限:滚动(固定、不重叠)、滑动 + 开窗让它变有限:滚动(固定、不重叠)、 + 滑动 (重叠的滚动)、会话(基于活动间隙)。
    • - 事件时间(何时发生)对处理时间(何时到达)——按事件时间 - 聚合;数据迟到且乱序。 + 事件时间(何时发生)对处理时间 + (何时到达)——按事件时间 聚合;数据迟到且乱序。
    • - 一条水位线决定何时停止等待掉队者——那个显式的延迟对完整性 + 一条水位线决定何时停止等待掉队者——那个显式的 + 延迟对完整性 取舍。
    • diff --git a/components/knowledge/content/survival-analysis.jsx b/components/knowledge/content/survival-analysis.jsx index 58c13596..7a354f0a 100644 --- a/components/knowledge/content/survival-analysis.jsx +++ b/components/knowledge/content/survival-analysis.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/survival-analysis. @@ -33,13 +27,39 @@ function CensoringFigure({ caption, ariaLabel, studyEndsLabel, legendLabel }) { role="img" aria-label={ariaLabel} > - - {studyEndsLabel} + + + {studyEndsLabel} + {SUBJECTS.map(([end, event], i) => { const y = 22 + i * 24; return ( - + {event ? ( ) : ( @@ -48,7 +68,9 @@ function CensoringFigure({ caption, ariaLabel, studyEndsLabel, legendLabel }) { ); })} - {legendLabel} + + {legendLabel} + ); @@ -279,8 +301,10 @@ function ZhBody() { return ( <>

      - 许多最重要的问题不是「这会不会发生?」而是「还要多久它才发生?」——还要多久一个病人复发、 - 一个客户流失、一台机器故障、一个案件被解决、一个人再犯。生存分析(或称事件时间分析)正是 + 许多最重要的问题不是「这会不会发生?」而是「还要多久 + 它才发生?」——还要多久一个病人复发、 + 一个客户流失、一台机器故障、一个案件被解决、一个人再犯。生存分析 + (或称事件时间分析)正是 为这些问题而建的统计学分支,它之所以作为一门独立的领域存在,是因为数据一个奇特而无可回避的特征:当你 的研究结束时,事件还没有发生在每个人身上——而你拿那些未完成的案例怎么办,改变着一切。

      @@ -293,10 +317,12 @@ function ZhBody() {

      乍一看你可能会动用你已经有的工具,而每一个都以一种有教育意义的方式失败。把它当作对「到事件的时间」 - 的一次回归?你不能——对许多对象来说事件 - 还没发生,所以他们的时间是未知的。把它当作 - 分类(「事件在时刻 T 之前发生了吗?」)?你扔掉了何时这一丰富的信息,而答案任意地 - 取决于你把 T 画在哪里。 + 的一次回归 + ?你不能——对许多对象来说事件 + 还没发生,所以他们的时间是未知的。把它当作 + 分类(「事件在时刻 T + 之前发生了吗?」)?你扔掉了何时这一丰富的信息,而答案任意地 取决于你把 T + 画在哪里。

      数据有一种特殊的结构——一个时长,加上事件到底有没有发生——它需要专门打造的方法。它们所有方法的 @@ -306,7 +332,8 @@ function ZhBody() {

      - 删失是生存分析的核心。一个对象是右删失的,是当你知道他们存活到 + 删失是生存分析的核心。一个对象是右删失的,是当你知道他们 + 存活到 某个点为止,却不知道之后发生了什么——因为在事件发生之前,研究结束了,或者他们退出了。你不知道他们 真实的事件时间;你只知道它比你观测到的更长

      @@ -324,14 +351,18 @@ function ZhBody() {
      -

      生存由两个互补的函数来描述。生存函数

      +

      + 生存由两个互补的函数来描述。生存函数: +

      {String.raw`S(t) = \Pr(T > t)`}

      - ——存活(没有发生事件)超过时刻 {String.raw`t`} 的概率。它从 1 开始,一级一级往 0 下降。 + ——存活(没有发生事件)超过时刻 {String.raw`t`} 的概率。它从 1 开始,一级一级往 + 0 下降。 风险函数 {String.raw`h(t)`} 取了一个不同的角度:它是在时刻{" "} - {String.raw`t`} 事件的瞬时率,你已经存活到那么远的条件下——对那些 + {String.raw`t`} 事件的瞬时率, + 你已经存活到那么远的条件下——对那些 仍处于风险中的人此刻的风险。生存回答「有多大比例撑到了这么久?」;风险回答「对一个存活者来说,此刻 有多危险?」它们是同一个过程的两个视角,不同的方法建模其中之一。

      @@ -339,8 +370,9 @@ function ZhBody() {

      - Kaplan-Meier 估计量是从数据中估计 {String.raw`S(t)`} 的主力,而它优雅地 - 处理删失。它产出那条熟悉的阶梯曲线:生存保持平直,然后在每一次事件实际发生时下降 + Kaplan-Meier 估计量是从数据中估计 {String.raw`S(t)`}{" "} + 的主力,而它优雅地 处理删失。它产出那条熟悉的阶梯曲线 + :生存保持平直,然后在每一次事件实际发生时下降 一级,每一级下降的大小由刚好之前还有多少人处于风险中来设定。删失的对象不会引起下降——他们只是在 自己的删失时刻离开「处于风险」的池子,所以他们正确地贡献到那个点的分母为止、不再更远。

      @@ -353,44 +385,48 @@ function ZhBody() {

      - 真正的问题往往是比较性的:A 组是否比 B 组存活得更久(治疗对对照、一个队列对另一个)?你为每一个画 - 一条 Kaplan-Meier 曲线,用对数秩检验来比较它们——一个关于两条(或更多)生存曲线之间的 - 差异是否大过偶然所能解释的假设检验。它是生存分析里比较组 - 均值的对应物,为尊重删失而建。它告诉你曲线是否不同,却不告诉你差多少、或在调整其他因素的 - 同时——而这正是 Cox 模型登场的地方。 + 真正的问题往往是比较性的:A 组是否比 B + 组存活得更久(治疗对对照、一个队列对另一个)?你为每一个画 一条 Kaplan-Meier 曲线,用 + 对数秩检验来比较它们——一个关于两条(或更多)生存曲线之间的 + 差异是否大过偶然所能解释的假设检验 + 。它是生存分析里比较组 均值的对应物,为尊重删失而建。它告诉你曲线是否不同 + ,却不告诉你差多少、或在调整其他因素的 同时——而这正是 Cox 模型登场的地方。

      要问「在保持其他因素不变的情况下,每个因素如何影响生存?」你需要一个回归——而 - Cox 比例风险模型是占主导地位的那个。它不直接建模生存曲线,而是建模风险,因为 - 风险更稳定、更易处理。它的形式: + Cox 比例风险模型是占主导地位的那个。它不直接建模生存曲线,而是建模 + 风险,因为 风险更稳定、更易处理。它的形式:

      {String.raw`h(t \mid \mathbf{x}) = h_0(t)\, \exp(\beta_1 x_1 + \cdots + \beta_p x_p)`}

      - 美妙之处在于它是半参数的:基线风险 {String.raw`h_0(t)`}——风险总体上如何随 - 时间变化——被留作未指定,所以你对生存曲线的形状不作任何假设。你只估计{" "} - {String.raw`\beta`} 系数,即每个协变量的效应。对一个系数取指数,得到一个 - 风险比{String.raw`e^{\beta} = 2`} 意味着那个因素在任何时刻把瞬时风险 - 加倍;低于 1 则是保护性的。那个单一、可解释的数字——「这个因素把风险乘以 X」——正是为什么 - Cox 模型在医学、可靠性和社会科学里无处不在。 + 美妙之处在于它是半参数的:基线风险 {String.raw`h_0(t)`} + ——风险总体上如何随 时间变化——被留作未指定 + ,所以你对生存曲线的形状不作任何假设。你只估计 {String.raw`\beta`}{" "} + 系数,即每个协变量的效应。对一个系数取指数,得到一个 + 风险比{String.raw`e^{\beta} = 2`}{" "} + 意味着那个因素在任何时刻把瞬时风险 + 加倍;低于 1 则是保护性的。那个单一、可解释的数字——「这个因素把风险乘以 + X」——正是为什么 Cox 模型在医学、可靠性和社会科学里无处不在。

      - Cox 模型用一个关键的假设买来它的灵活性,这个假设藏在名字里:比例风险。它假设一个 - 协变量的效应是对风险在所有时刻的一个恒定的乘数——两组之间的风险比不随时间流逝而 - 改变。 + Cox 模型用一个关键的假设买来它的灵活性,这个假设藏在名字里:比例风险 + 。它假设一个 协变量的效应是对风险在所有时刻的一个恒定的乘数 + ——两组之间的风险比不随时间流逝而 改变。

      那个假设往往是合理的,但并不总是——而当它被违反时,模型会误导人。如果一种治疗早期有帮助、但它的 益处逐渐消退(曲线交叉,或差距随时间缩小),一个单一、恒定的风险比就是一个虚构,它把真实的、随 - 时间变化的故事平均掉了。所以务必检查它(用残差图或一个正式的检验),在它失败时 + 时间变化的故事平均掉了。所以务必检查它 + (用残差图或一个正式的检验),在它失败时 动用扩展(时变系数、分层)。与每个模型一样,假设是信任所栖身——或不栖身——之处。

      @@ -400,15 +436,17 @@ function ZhBody() {

      出人意料地多的分析问题,其实是乔装的事件时间问题:一个案件还要多久被解决、一个问题的复发时间、 - 一个人在一个项目里待多久才退出。生存分析给我的最有价值的东西,是围绕删失的 - 纪律——认识到那些还没完成的案例携带真实的信息,而把它们扔掉(那个诱人的捷径)会系统性地 + 一个人在一个项目里待多久才退出。生存分析给我的最有价值的东西,是围绕 + 删失的 纪律——认识到那些还没 + 完成的案例携带真实的信息,而把它们扔掉(那个诱人的捷径)会系统性地 把答案偏向任何很快就完成的东西。

      - Kaplan-Meier 是展示「随时间有多大比例仍然留存」的诚实方式,对数秩检验 - 恰当地比较两组的时间线,而 Cox 模型在调整混杂因素的同时,给出一个可解释 - 的风险比——「这个因素把风险乘以 X」——这与因果 - 推断的心态天然相配。懂得比例风险假设,正是让那个风险比保持诚实、而非一个 + Kaplan-Meier 是展示「随时间有多大比例仍然留存」的诚实方式, + 对数秩检验恰当地比较两组的时间线,而 Cox 模型 + 在调整混杂因素的同时,给出一个可解释 的风险比——「这个因素把风险乘以 X」——这与 + 因果 推断的心态天然相配。懂得 + 比例风险假设,正是让那个风险比保持诚实、而非一个 变化中故事的便利平均的东西。

      @@ -418,15 +456,17 @@ function ZhBody() {
      • - 生存分析回答「还要多久事件才发生?」——普通回归/分类会失败,因为事件还没发生在 - 每个人身上。 + 生存分析回答「还要多久事件才发生?」 + ——普通回归/分类会失败,因为事件还没发生在 每个人身上。
      • - 删失是关键的想法:一个右删失的对象在某个时刻之前无事件(一个真实的下界)。别把 + 删失 + 是关键的想法:一个右删失的对象在某个时刻之前无事件(一个真实的下界)。别把 它们扔掉,也别把删失当作事件——两者都让结果有偏。
      • - 两个视角:生存 {String.raw`S(t)=\Pr(T>t)`}(撑过 t 的比例)和 + 两个视角:生存 {String.raw`S(t)=\Pr(T>t)`}(撑过 t + 的比例)和 风险 {String.raw`h(t)`}(在迄今存活的条件下的瞬时风险)。
      • @@ -439,14 +479,14 @@ function ZhBody() { {String.raw`e^\beta`} = 风险比(「把风险乘以 X」)。
      • - 检查比例风险假设——一个随时间恒定的风险比;当它被违反时(曲线交叉),那个单一的 - 数字会误导人。 + 检查比例风险假设 + ——一个随时间恒定的风险比;当它被违反时(曲线交叉),那个单一的 数字会误导人。

      - 删失的框架、Kaplan-Meier/对数秩的配对,以及带有比例风险告诫的 Cox 模型,反映了当前的生存分析参考 - 文献以及统计学课程。 + 删失的框架、Kaplan-Meier/对数秩的配对,以及带有比例风险告诫的 Cox + 模型,反映了当前的生存分析参考 文献以及统计学课程。

      diff --git a/components/knowledge/content/time-series-analysis.jsx b/components/knowledge/content/time-series-analysis.jsx index bb842c0a..dd33ea3b 100644 --- a/components/knowledge/content/time-series-analysis.jsx +++ b/components/knowledge/content/time-series-analysis.jsx @@ -1,12 +1,6 @@ import Link from "next/link"; -import { - KSection, - Callout, - Formula, - Figure, - TeX, - Term, -} from "@/components/knowledge/KnowledgeLayout"; +import { KSection, Callout, Figure, Term } from "@/components/knowledge/KnowledgeLayout"; +import { Formula, TeX } from "@/components/knowledge/KatexFormula"; /** * Per-locale content for /knowledge/time-series-analysis. diff --git a/components/knowledge/content/topic-modelling.jsx b/components/knowledge/content/topic-modelling.jsx index 76b29b28..ae5e7470 100644 --- a/components/knowledge/content/topic-modelling.jsx +++ b/components/knowledge/content/topic-modelling.jsx @@ -18,31 +18,99 @@ function LDAFigure({ caption, ariaLabel, documentsLabel, topicsLabel, wordsLabel aria-label={ariaLabel} > {/* documents */} - {documentsLabel} + + {documentsLabel} + {[34, 70, 106].map((y, i) => ( - + ))} {/* topics */} - {topicsLabel} + + {topicsLabel} + {[44, 96].map((y, i) => ( ))} {/* words */} - {wordsLabel} + + {wordsLabel} + {[30, 56, 82, 108].map((y, i) => ( - + ))} {/* doc -> topic links */} {[43, 79, 115].map((y, i) => ( - - + + ))} {/* topic -> word links */} {[44, 96].map((ty, i) => [37, 63, 89, 115].map((wy, j) => ( - + )) )} @@ -274,23 +342,25 @@ function ZhBody() { <>

      想象一个装着一万份文档的文件夹——调查回应、报告、文章、情报笔记——没人有时间读它们。它们 - 关于什么?什么主题贯穿其中,哪些文档共享哪些?主题建模正好回答这个:它是一族 - 无监督的方法,自动发现一个文本集合里潜在的主题(「话题」),不需要任何标签,也不需要 - 事先被告知要找什么。 + 关于什么?什么主题贯穿其中,哪些文档共享哪些?主题建模 + 正好回答这个:它是一族 无监督的方法,自动发现一个文本集合里潜在的主题(「话题」), + 不需要任何标签,也不需要 事先被告知要找什么。

      它是一个真正独特的工具——不是分类(没有类别可预测),也不是一般的 NLP——它是把 - 聚类的想法用到文档上。这一页一步步建起经典的方法(LDA)、 + 聚类 + 的想法用到文档上。这一页一步步建起经典的方法(LDA)、 如何读懂并评判它的输出,以及现代基于嵌入的继任者——连同那个诚实的警告:它找到的主题并不总是有意义的。

      - 决定性的特征是它无监督:你不告诉它有哪些主题,它从文档间词共现的模式中发现 + 决定性的特征是它无监督:你不告诉它有哪些主题,它从文档间词共现的模式中 + 发现 它们。如果一组词——「预算」「赤字」「支出」「税」——在许多文档里不断一起出现,那个反复出现的词簇 - 就是一个主题,一个人随后可以把它认出来是「财政政策」。模型找到统计结构;意义在于词如何 - 成组。 + 就是 + 一个主题,一个人随后可以把它认出来是「财政政策」。模型找到统计结构;意义在于词如何 成组。

      这使它成为面对任何大型、未读文本集合的天然第一步:在你能分析一个语料库之前,你需要知道里面有什么, @@ -301,26 +371,29 @@ function ZhBody() {

      - 经典的主题建模从词袋表示出发(与信息 - 检索同样的起点):一份文档被归约成它所含词的多重集,完全忽略顺序。「The cat - sat」和「sat the cat」看起来一模一样。那听起来有损——确实是——但对于寻找主题,它出奇地 - 好用,因为一份文档的主题内容,主要由哪些词出现、出现多少次来承载,而非它们的顺序。主题 - 建模利用的,正是这种表示所保留的共现模式。 + 经典的主题建模从词袋表示出发(与 + 信息 检索 + 同样的起点):一份文档被归约成它所含词的多重集,完全忽略顺序。「The cat + sat」和「sat the cat」看起来一模一样。那听起来有损——确实是——但对于寻找主题 + ,它出奇地 好用,因为一份文档的主题内容,主要由哪些 + 词出现、出现多少次来承载,而非它们的顺序。主题 建模利用的,正是这种表示所保留的共现模式。

      - 标准的方法是潜在狄利克雷分配(LDA)。它巧妙的一步,是想象一个文档如何被写出来的 + 标准的方法是潜在狄利克雷分配 + (LDA)。它巧妙的一步,是想象一个文档如何被写出来的 生成的故事,然后把它倒过来跑。这个故事有两个简单的想法:

      • - 每个主题是一个在词上的分布(「财政政策」主题给「预算」「税」「赤字」很高的 - 概率)。 + 每个主题 + 是一个在词上的分布(「财政政策」主题给「预算」「税」「赤字」很高的 概率)。
      • - 每份文档是主题的一个混合(一篇新闻文章可能 70% 财政政策、20% 政治、10% 经济)。 + 每份文档是主题的一个混合(一篇新闻文章可能 70% 财政政策、20% 政治、10% + 经济)。

      - LDA 只观测到词——主题和混合是潜在的(隐藏的)。它通过推断倒推,找到最能解释实际所见之词的 - 那组主题以及每份文档的混合。「狄利克雷」那部分只是那个先验,它鼓励每份文档关于少数几个 - 主题、而非全部,这让结果保持可解释。要紧的是那个直觉:文档 = 主题的混合,主题 = 在词上的 - 分布,仅从共现推断而来。 + LDA 只观测到词——主题和混合是潜在的 + (隐藏的)。它通过推断倒推,找到最能解释实际所见之词的 + 那组主题以及每份文档的混合。「狄利克雷」那部分只是那个先验,它鼓励每份文档关于 + 少数几个 + 主题、而非全部,这让结果保持可解释。要紧的是那个直觉: + 文档 = 主题的混合,主题 = 在词上的 分布,仅从共现推断而来。

      @@ -344,8 +419,9 @@ function ZhBody() { {"{"}病人、医院、治疗、临床、护理{"}"} - 。那个关键而常被忽略的要点:模型并不给主题命名。它递给你词组;一个读 - 「病人、医院、治疗……」并把它标为「医疗保健」。主题建模是一个协助人来解读、而非取代人的 + 。那个关键而常被忽略的要点:模型并不给主题命名。它递给你词组;一个 + 读 「病人、医院、治疗……」并把它标为「医疗保健」。主题建模是一个协助 + 人来解读、而非取代人的 工具——它的价值在于快速浮现出结构,由分析师供给意义。在主题之外,你还得到每份文档的混合,这让你能在 整个集合里给主题打标、过滤和追踪。

      @@ -353,31 +429,37 @@ function ZhBody() {

      - LDA 需要你事先指定主题的数量——而没有客观正确的答案,正是聚类 - 里那个「选 k」的问题。太少,不同的主题会被搅在一起;太多,主题会碎裂成噪声和近乎重复的东西。 + LDA 需要你事先指定主题的数量——而没有客观正确的答案,正是 + 聚类里那个「选 + k」的问题。太少,不同的主题会被搅在一起;太多,主题会碎裂成噪声和近乎重复的东西。

      - 标准的指引是一个一致性分数,它度量一个主题的头部词在语义上有多相关——对一个人来说 + 标准的指引是一个一致性分数 + ,它度量一个主题的头部词在语义上有多相关——对一个人来说 它们是否真的「凑在一起」?你在一系列主题数量上计算一致性,找它在哪里达到峰值。但它是一个指引、而非 - 神谕:一致性通常上升、走平、然后下降,而最终的判断仍然落在「主题是否有用」这一人的判断上。 + 神谕:一致性通常上升、走平、然后下降,而最终的判断仍然落在「主题是否有用 + 」这一人的判断上。 与聚类一样,主题的数量是一个你必须自己担起的建模决定,而非数据递给你的一个参数。

      - LDA 不是唯一的选择。非负矩阵分解(NMF)经由一条不同的路径达到相似的结果——把文档—词 - 矩阵分解成主题成分(与 PCA 和推荐器同一个 - 矩阵分解家族),往往更快,在短文本上 - 有时更利落。 + LDA 不是唯一的选择。非负矩阵分解 + (NMF)经由一条不同的路径达到相似的结果——把文档—词 矩阵分解成主题成分(与 PCA + 和推荐器同一个 + 矩阵分解 + 家族),往往更快,在短文本上 有时更利落。

      - 更大的转变是现代的、基于嵌入的方法—— + 更大的转变是现代的、基于嵌入 + 的方法—— BERTopic 及同类——它把文档嵌入为捕捉意义的稠密向量, - 聚类那些向量,再从簇中导出主题。因为它理解意义、 + 聚类那些向量,再从簇中导出主题。因为它理解 + 意义、 而不只是词频,它对同义词和短文本处理得好得多,通常产出更一致的主题——代价是更高的计算成本。这就是 - 贯穿检索和一般 NLP 的那个「词袋 → 嵌入」的同样 - 的演进。 + 贯穿检索和一般 NLP 的那个「词袋 → + 嵌入」的同样 的演进。

      @@ -385,10 +467,12 @@ function ZhBody() {

      那些诚实的告诫,因为主题建模可能华而不实地骗人:

      - 主题不保证有意义。模型找到统计结构,而有时那个结构是垃圾——一个只是常见词大杂烩 - 的「主题」,或者一个格式造成的假象。结果可能不稳定(用不同的种子重跑,主题就变 - 了),词袋方法对上下文视而不见(它们分不清河「岸」的 bank 和「银行」的 bank), - 而整件事需要仔细的预处理(去停用词、合理的分词),否则主题里会塞满「的」和 + 主题不保证有意义。 + 模型找到统计结构,而有时那个结构是垃圾——一个只是常见词大杂烩 + 的「主题」,或者一个格式造成的假象。结果可能不稳定 + (用不同的种子重跑,主题就变 了),词袋方法对上下文视而不见 + (它们分不清河「岸」的 bank 和「银行」的 bank), 而整件事需要 + 仔细的预处理(去停用词、合理的分词),否则主题里会塞满「的」和 「和」。把输出当作一个要去验证的、生成假设的起点,而绝非一个完成了的答案。

      @@ -402,9 +486,10 @@ function ZhBody() { 文档围绕哪些主题聚集」的快速地图,把一个读不完的语料库变成某种可导航的东西。

      - 让它保持诚实的,是同时握住两件事:它是一个人在环的工具(模型找到词组,由我供给 - 意义和标签),而主题在被验证之前可能是垃圾——所以它生成要核查的假设,而非要上报 - 的结论。它直接连到聚类(同样的无监督想法)、 + 让它保持诚实的,是同时握住两件事:它是一个人在环 + 的工具(模型找到词组,由我供给 意义和标签),而主题在被验证之前可能是 + 垃圾——所以它生成要核查的假设,而非要上报 的结论。它直接连到 + 聚类(同样的无监督想法)、 NLP(文本处理),以及与 检索共享的嵌入方法。

      @@ -415,32 +500,33 @@ function ZhBody() {
      • - 主题建模无监督地发现一个文本集合里的主题——无标签。它是给文档的 - 聚类。 + 主题建模无监督地发现一个文本集合里的主题——无标签。它是 + 给文档的 聚类
      • 经典方法从词袋出发(忽略顺序;共现承载主题)。
      • - LDA:文档 = 主题的混合,主题 = 在词上的分布; - 从观测到的词推断隐藏的主题。 + LDA:文档 = 主题的混合,主题 ={" "} + 在词上的分布; 从观测到的词推断隐藏的主题。
      • 模型给出词组——由人给主题命名。外加每份文档的主题混合。
      • - 选择主题的数量是那个「选 k」的问题——用一致性作指引,但判断 - 说了算。NMFBERTopic(基于嵌入)是替代品。 + 选择主题的数量是那个「选 k」的问题——用一致性 + 作指引,但判断 说了算。NMFBERTopic + (基于嵌入)是替代品。
      • - 告诫:主题可能是垃圾、不稳定、对上下文视而不见——一个要去验证的、生成假设的 - 工具,而非一个完成了的答案。 + 告诫:主题可能是垃圾 + 、不稳定、对上下文视而不见——一个要去验证的、生成假设的 工具,而非一个完成了的答案。

      - LDA 的生成框架、基于一致性的主题数量选择,以及 BERTopic 的对比,反映了当前的主题建模参考文献以及 - NLP 课程。 + LDA 的生成框架、基于一致性的主题数量选择,以及 BERTopic + 的对比,反映了当前的主题建模参考文献以及 NLP 课程。

      diff --git a/lib/fonts.js b/lib/fonts.js index f78df0ef..c786c860 100644 --- a/lib/fonts.js +++ b/lib/fonts.js @@ -4,12 +4,17 @@ import localFont from "next/font/local"; // Pixel display — hero h1 brand name // Variable font: single file covers all weights (100–900) and axes. Served as // WOFF2 (lossless brotli-compressed container) — 264KB TTF -> 36KB, identical -// glyphs and axes, loaded on every page. +// glyphs and axes. +// preload: false — only the homepage hero (HeroSection.jsx) needs this above +// the fold; everywhere else it's a 0.025-opacity Footer watermark and a +// once-per-session boot animation. display:swap already avoids FOIT, so +// lazy-loading it saves ~35KB of render-priority preload on every other page. export const bitcount = localFont({ src: "./Bitcount_Prop_Double/BitcountPropDouble-VariableFont.woff2", weight: "100 900", style: "normal", display: "swap", + preload: false, variable: "--font-bitcount", }); diff --git a/lib/posts.js b/lib/posts.js index 6423a8a5..61471b56 100644 --- a/lib/posts.js +++ b/lib/posts.js @@ -170,25 +170,18 @@ function remarkFlowPlugin() { }; } -/** Parse a single .md file and return its metadata + HTML body. */ -async function parsePost(slug) { +/** Read + frontmatter-parse a single .md file. Cheap — no remark pipeline. */ +function readFrontmatter(slug) { const filePath = path.join(POSTS_DIR, `${slug}.md`); const raw = fs.readFileSync(filePath, "utf8"); - const { data, content } = matter(raw); - - // remark-html & remark-gfm are ESM-only — use dynamic import - const { default: remarkHtml } = await import("remark-html"); - const { default: remarkGfm } = await import("remark-gfm"); - const processor = remark() - .use(remarkGfm) - .use(remarkFlowPlugin) - .use(remarkHtml, { sanitize: false }); - const result = processor.processSync(content); + return matter(raw); // { data, content } +} +/** Build the metadata object shared by parseMeta and parsePost. */ +function metaFromFrontmatter(slug, data, content) { // Reading time from the markdown source (~200 wpm), min 1 minute. const words = content.trim().split(/\s+/).filter(Boolean).length; const readingTime = Math.max(1, Math.round(words / 200)); - return { slug, title: data.title || slug, @@ -197,21 +190,43 @@ async function parsePost(slug) { description: data.description || "", ogImage: data.ogImage || null, readingTime, - contentHtml: String(result), }; } +/** + * Metadata only — no remark pipeline. Used by getAllPosts(), which needs + * every post's frontmatter but never renders their bodies. Keeping this + * separate from parsePost() turns /blog/[slug]'s per-page build cost from + * O(N) full-content parses (one per other post, just to get its title) into + * O(1): only the current slug goes through the expensive remark pipeline. + */ +function parseMeta(slug) { + const { data, content } = readFrontmatter(slug); + return metaFromFrontmatter(slug, data, content); +} + +/** Parse a single .md file and return its metadata + HTML body. */ +async function parsePost(slug) { + const { data, content } = readFrontmatter(slug); + const meta = metaFromFrontmatter(slug, data, content); + + // remark-html & remark-gfm are ESM-only — use dynamic import + const { default: remarkHtml } = await import("remark-html"); + const { default: remarkGfm } = await import("remark-gfm"); + const processor = remark() + .use(remarkGfm) + .use(remarkFlowPlugin) + .use(remarkHtml, { sanitize: false }); + const result = processor.processSync(content); + + return { ...meta, contentHtml: String(result) }; +} + /** Return all posts sorted by date descending. */ export async function getAllPosts() { if (!fs.existsSync(POSTS_DIR)) return []; const files = fs.readdirSync(POSTS_DIR).filter((f) => f.endsWith(".md")); - const posts = await Promise.all( - files.map(async (f) => { - const slug = f.replace(/\.md$/, ""); - const { title, date, tags, description, ogImage, readingTime } = await parsePost(slug); - return { slug, title, date, tags, description, ogImage, readingTime }; - }) - ); + const posts = files.map((f) => parseMeta(f.replace(/\.md$/, ""))); return posts.sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0)); } diff --git a/middleware.js b/middleware.js index e2ae8f9f..21fe41f4 100644 --- a/middleware.js +++ b/middleware.js @@ -58,5 +58,12 @@ export function middleware(request) { export const config = { // The "/((?!...).*)" pattern does NOT match the bare root "/", so the curl // rewrite never ran for `curl rin.contact`. List "/" explicitly as well. - matcher: ["/", "/((?!api|_next/static|_next/image|favicon.ico).*)"], + // Also exclude static file extensions (fonts, images, css, etc. under + // /public) — none of them need the CLI-detection or .well-known logic + // below, so skipping the middleware invocation for them removes an Edge + // Function hop from every font/image/stylesheet request on every page. + matcher: [ + "/", + "/((?!api|_next/static|_next/image|.*\\.(?:css|html|ico|jpe?g|js|json|pdf|png|svg|txt|webmanifest|woff2?|xml)$).*)", + ], }; diff --git a/next.config.js b/next.config.js index 07e9f81e..063427ab 100644 --- a/next.config.js +++ b/next.config.js @@ -11,14 +11,9 @@ const nextConfig = { images: { formats: ["image/avif", "image/webp"], - remotePatterns: [ - { protocol: "https", hostname: "upload.wikimedia.org" }, - { protocol: "https", hostname: "yt3.googleusercontent.com" }, - { protocol: "https", hostname: "encrypted-tbn0.gstatic.com" }, - { protocol: "https", hostname: "media.licdn.com" }, - { protocol: "https", hostname: "s3-symbol-logo.tradingview.com" }, - { protocol: "https", hostname: "companieslogo.com" }, - ], + // No remotePatterns: all logos/images are self-hosted under /public/images + // (see components/sections/TimelineSection.jsx). Nothing routes through + // next/image from a remote host today. }, webpack: (config, { isServer }) => { diff --git a/pages/_app.jsx b/pages/_app.jsx index 83a9f96d..39828266 100644 --- a/pages/_app.jsx +++ b/pages/_app.jsx @@ -7,7 +7,10 @@ import { I18nProvider } from "@/contexts/I18nContext"; import { bitcount, dmSans, playfair } from "@/lib/fonts"; import "../public/styles/globals.css"; -import "katex/dist/katex.min.css"; +// -swap variant: font-display:swap instead of the default's font-display:block, +// so the ~39 /knowledge pages that render / don't risk a FOIT +// while KaTeX's math fonts load. Same file size either way. +import "katex/dist/katex-swap.min.css"; const Analytics = dynamic( () => import("@vercel/analytics/react").then((m) => ({ default: m.Analytics })), diff --git a/pages/_document.jsx b/pages/_document.jsx index 704e5158..54812979 100644 --- a/pages/_document.jsx +++ b/pages/_document.jsx @@ -209,370 +209,6 @@ const PROFILE_PAGE_SCHEMA = { }, }; -const PROJECTS_SCHEMA = { - "@context": "https://schema.org", - "@type": "ItemList", - "@id": "https://rin.contact/#projects", - name: "Projects by Rin Huang (Sunchuangyu Huang)", - description: "Software, data science, and analytics projects by Rin Huang", - author: { "@id": "https://rin.contact/#person" }, - itemListElement: [ - { - "@type": "ListItem", - position: 1, - item: { - "@type": "SoftwareApplication", - name: "Mapiva", - description: - "Co-founded a mobile social connection app — full product ownership from architecture through implementation as Dev Lead.", - applicationCategory: "SocialNetworkingApplication", - operatingSystem: "iOS, Android", - author: { "@id": "https://rin.contact/#person" }, - programmingLanguage: ["React Native", "Expo"], - }, - }, - { - "@type": "ListItem", - position: 2, - item: { - "@type": "SoftwareSourceCode", - name: "SA Address Generator", - description: - "Internal tool to generate validated South Australian addresses based on SEIFA indices and remoteness classifications, verified via Mapbox API.", - codeRepository: "https://github.com/rNLKJA/SA-Mock-Address-Generator", - programmingLanguage: "Python", - author: { "@id": "https://rin.contact/#person" }, - }, - }, - { - "@type": "ListItem", - position: 3, - item: { - "@type": "SoftwareSourceCode", - name: "US Political Data Collection System", - description: - "Scraped ~180 presidential debate transcripts and ~25,000 campaign documents from the UC Santa Barbara American Presidency Project with multi-threaded processing.", - codeRepository: "https://github.com/rNLKJA/Political-Data-Collection-System", - programmingLanguage: "Python", - author: { "@id": "https://rin.contact/#person" }, - }, - }, - { - "@type": "ListItem", - position: 4, - item: { - "@type": "SoftwareApplication", - name: "CBS Intelligence Analytics", - description: - "First intelligence analytics capability within the CBS Prevention Team — integrating ABS, SA Health, ACCC, and DataSA data into unified dashboards and GIS maps used by the Minister's Office.", - applicationCategory: "BusinessApplication", - author: { "@id": "https://rin.contact/#person" }, - programmingLanguage: ["Python", "Power BI"], - }, - }, - { - "@type": "ListItem", - position: 5, - item: { - "@type": "SoftwareApplication", - name: "MoodQ", - description: - "Clinician-facing and patient-facing mental health mobile app for the University of Melbourne Psychiatry research group. Migrated from Uniapp to Expo React Native, reducing hosting costs ~$500/month.", - applicationCategory: "HealthApplication", - operatingSystem: "iOS, Android", - author: { "@id": "https://rin.contact/#person" }, - programmingLanguage: ["Expo", "React Native", "Node.js"], - }, - }, - { - "@type": "ListItem", - position: 6, - item: { - "@type": "SoftwareApplication", - name: "SAPOL Intelligence Dashboards", - description: - "Operational intelligence and crime analytics dashboards supporting frontline policing, resource allocation, and command-level decision-making across South Australia.", - applicationCategory: "BusinessApplication", - author: { "@id": "https://rin.contact/#person" }, - programmingLanguage: ["Python", "Power BI", "SQL"], - }, - }, - { - "@type": "ListItem", - position: 7, - item: { - "@type": "SoftwareSourceCode", - name: "Flow Cytometry Analysis Pipeline", - description: - "Automated flow cytometry data analysis using cloud and HPC, with test infrastructure for reproducibility and open-source contributions to celseq2.", - programmingLanguage: "Python", - author: { "@id": "https://rin.contact/#person" }, - }, - }, - { - "@type": "ListItem", - position: 8, - item: { - "@type": "SoftwareSourceCode", - name: "Climate Fact-Checker", - description: - "Two-stage automated fact-checking for climate change claims — TF-IDF evidence retrieval and Transformer-based classification, outperforming LSTM baselines.", - codeRepository: "https://github.com/rNLKJA", - programmingLanguage: "Python", - author: { "@id": "https://rin.contact/#person" }, - }, - }, - { - "@type": "ListItem", - position: 9, - item: { - "@type": "SoftwareSourceCode", - name: "Australia Social Media Analytics on the Cloud", - description: - "Harvested and analysed Twitter and Mastodon data alongside ABS SUDO spatial data to produce a Social Sense Dashboard across Australian regions.", - codeRepository: "https://github.com/rNLKJA/Australia-Social-Media-Analytics-on-the-Cloud", - programmingLanguage: ["Python", "CouchDB"], - author: { "@id": "https://rin.contact/#person" }, - }, - }, - { - "@type": "ListItem", - position: 10, - item: { - "@type": "SoftwareSourceCode", - name: "Twitter HPC Analysis", - description: - "Processed a large-scale Twitter dataset on SPARTAN HPC using MPI and Python, identifying tweet distribution across Australian cities.", - codeRepository: "https://github.com/rNLKJA/Twitter-Data-Analysis-with-HPC", - programmingLanguage: "Python", - author: { "@id": "https://rin.contact/#person" }, - }, - }, - { - "@type": "ListItem", - position: 11, - item: { - "@type": "SoftwareSourceCode", - name: "Cachex AI Game Agent", - description: - "AI agents for Cachex — a two-player connection game — using heuristic A* search and competitive game theory with strategic sabotage logic.", - codeRepository: "https://github.com/rNLKJA/Cachex-AI", - programmingLanguage: "Python", - author: { "@id": "https://rin.contact/#person" }, - }, - }, - { - "@type": "ListItem", - position: 12, - item: { - "@type": "SoftwareSourceCode", - name: "PCRM — Personal Customer Relationship Management", - description: - "Full-stack CRM system with React frontend, Express REST API, and MongoDB backend, built as the COMP30022 IT Project at the University of Melbourne.", - codeRepository: "https://github.com/rNLKJA/Personal-Customer-Relation-Management-PCRM", - programmingLanguage: ["Node.js", "React.js", "MongoDB"], - author: { "@id": "https://rin.contact/#person" }, - }, - }, - ], -}; - -const CREDENTIALS_SCHEMA = { - "@context": "https://schema.org", - "@graph": [ - { - "@type": "EducationalOccupationalCredential", - name: "VETASSESS — Statistician (ANZSCO 224113)", - credentialCategory: "ProfessionalAssessment", - recognizedBy: { "@type": "Organization", name: "VETASSESS" }, - dateCreated: "2026-02-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "IELTS General Training — Band 8", - credentialCategory: "LanguageAssessment", - recognizedBy: { "@type": "Organization", name: "IELTS Official" }, - dateCreated: "2026-02-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Credentialed Community Language — Mandarin", - credentialCategory: "LanguageCredential", - recognizedBy: { "@type": "Organization", name: "NAATI" }, - dateCreated: "2025-12-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Microsoft Certified: Azure Fundamentals (AZ-900)", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "Microsoft", url: "https://microsoft.com" }, - dateCreated: "2024-07-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Neo4j Certified Professional", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "Neo4j", url: "https://neo4j.com" }, - dateCreated: "2025-08-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Neo4j Graph Data Science Certification", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "Neo4j", url: "https://neo4j.com" }, - dateCreated: "2025-08-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Google UX Design Specialisation", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "Google", url: "https://google.com" }, - dateCreated: "2025-12-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Google Business Intelligence Specialisation", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "Google", url: "https://google.com" }, - dateCreated: "2025-12-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Google Project Management Specialisation", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "Google", url: "https://google.com" }, - dateCreated: "2025-12-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Google IT Automation with Python", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "Google", url: "https://google.com" }, - dateCreated: "2022-05-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Google Data Analytics Specialisation", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "Google", url: "https://google.com" }, - dateCreated: "2021-06-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Open-Source Intelligence (OSINT) Fundamentals", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "TCM Security" }, - dateCreated: "2025-10-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Google Analytics Individual Qualification (GAIQ)", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "Google", url: "https://google.com" }, - dateCreated: "2024-05-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Advanced SQL for Data Scientists", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "LinkedIn Learning" }, - dateCreated: "2024-01-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Atlassian Agile Project Management Professional Certificate", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "Atlassian", url: "https://atlassian.com" }, - dateCreated: "2024-04-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Career Essentials in GitHub Professional Certificate", - credentialCategory: "certification", - recognizedBy: { "@type": "Organization", name: "GitHub", url: "https://github.com" }, - dateCreated: "2024-01-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Melbourne Plus: Innovation", - credentialCategory: "microcredential", - recognizedBy: { - "@type": "CollegeOrUniversity", - name: "University of Melbourne", - url: "https://www.unimelb.edu.au", - }, - dateCreated: "2024-05-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - { - "@type": "EducationalOccupationalCredential", - name: "Melbourne Plus: People Leadership", - credentialCategory: "microcredential", - recognizedBy: { - "@type": "CollegeOrUniversity", - name: "University of Melbourne", - url: "https://www.unimelb.edu.au", - }, - dateCreated: "2024-10-01", - holder: { "@id": "https://rin.contact/#person" }, - }, - ], -}; - -const BREADCRUMB_SCHEMA = { - "@context": "https://schema.org", - "@type": "BreadcrumbList", - "@id": "https://rin.contact/#breadcrumb", - itemListElement: [ - { - "@type": "ListItem", - position: 1, - name: "Rin Huang", - item: "https://rin.contact/", - }, - { - "@type": "ListItem", - position: 2, - name: "Career", - item: "https://rin.contact/#timeline", - }, - { - "@type": "ListItem", - position: 3, - name: "Projects", - item: "https://rin.contact/#projects", - }, - { - "@type": "ListItem", - position: 4, - name: "Skills", - item: "https://rin.contact/#skills", - }, - { - "@type": "ListItem", - position: 5, - name: "Contact", - item: "https://rin.contact/#contact", - }, - ], -}; - class MyDocument extends Document { static async getInitialProps(ctx) { const initialProps = await Document.getInitialProps(ctx); @@ -689,11 +325,11 @@ class MyDocument extends Document { href="/opensearch.xml" /> - {/* ── DNS prefetch — third-party image & API domains ── */} - - - - + {/* ── DNS prefetch ── */} + {/* The logo hosts this used to prefetch (LinkedIn, YouTube, Google's + thumbnail cache, TradingView) are no longer contacted — logos are + self-hosted under /public/images now. api.emailjs.com is the only + third-party host actually contacted client-side (contact form). */} {/* @@ -714,18 +350,9 @@ class MyDocument extends Document { type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(PROFILE_PAGE_SCHEMA) }} /> -