Sklearn-源码解析-书-v1-0-三十-

Sklearn 源码解析(书)v1.0(三十)

这段代码实现了一个Sphinx后处理转换,为sphinx-design下拉菜单生成稳定的锚点ID,确保深度链接在文档重建后仍然有效。[/content]

[content]

import inspect

import os

import subprocess

import sys

from functools import partial

from operator import attrgetter

REVISION_CMD = "git rev-parse --short HEAD"

def _get_git_revision():

try:

revision = subprocess.check_output(REVISION_CMD.split()).strip()

except (subprocess.CalledProcessError, OSError):

print("Failed to execute git to get revision")

return None

return revision.decode("utf-8")

def _linkcode_resolve(domain, info, package, url_fmt, revision):

"""Determine a link to online source for a class/method/function

This is called by sphinx.ext.linkcode

An example with a long-untouched module that everyone has

>>> _linkcode_resolve('py', {'module': 'tty',

... 'fullname': 'setraw'},

... package='tty',

... url_fmt='https://hg.python.org/cpython/file/'

... '{revision}/Lib/{package}/{path}#L{lineno}',

... revision='xxxx')

'https://hg.python.org/cpython/file/xxxx/Lib/tty/tty.py#L18'

"""

if revision is None:

return

if domain not in ("py", "pyx"):

return

if not info.get("module") or not info.get("fullname"):

return

class_name = info["fullname"].split(".")[0]

module = import(info["module"], fromlist=[class_name])

obj = attrgetter(info["fullname"])(module)

Unwrap the object to get the correct source

file in case that is wrapped by a decorator

obj = inspect.unwrap(obj)

try:

fn = inspect.getsourcefile(obj)

except Exception:

fn = None

if not fn:

try:

fn = inspect.getsourcefile(sys.modules[obj.module])

except Exception:

fn = None

if not fn:

return

try:

fn = os.path.relpath(fn, start=os.path.dirname(import(package).file))

except ValueError:

return None

try:

lineno = inspect.getsourcelines(obj)[1]

except Exception:

lineno = ""

return url_fmt.format(revision=revision, package=package, path=fn, lineno=lineno)

def make_linkcode_resolve(package, url_fmt):

"""Returns a linkcode_resolve function for the given URL format

revision is a git commit reference (hash or name)

package is the name of the root module of the package

url_fmt is along the lines of ('https://github.com/USER/PROJECT/'

'blob/{revision}/{package}/'

'{path}#L{lineno}')

"""

revision = _get_git_revision()

return partial(

_linkcode_resolve, revision=revision, package=package, url_fmt=url_fmt

)


这段代码实现了一个用于Sphinx的linkcode解析函数,它能够将Python对象链接到GitHub上的对应源码行,通过获取Git修订版和解析对象的源文件位置来生成精确的URL。[/content]

[content]
from functools import cache

from sphinx.util.logging import getLogger

logger = getLogger(__name__)


def override_pst_pagetoc(app, pagename, templatename, context, doctree):
    """Overrides the `generate_toc_html` function of pydata-sphinx-theme for API."""

    @cache
    def generate_api_toc_html(kind="html"):
        """Generate the in-page toc for an API page.

        This relies on the `generate_toc_html` function added by pydata-sphinx-theme
        into the context. We save the original function into `pst_generate_toc_html`
        and override `generate_toc_html` with this function for generated API pages.

        The pagetoc of an API page would look like the following:

        <ul class="visible ...">               <-- Unwrap
         <li class="toc-h1 ...">               <-- Unwrap
          <a class="..." href="#">{{obj}}</a>  <-- Decompose

          <ul class="visible ...">
           <li class="toc-h2 ...">
            ...object
            <ul class="...">                          <-- Set visible if exists
             <li class="toc-h3 ...">...method 1</li>  <-- Shorten
             <li class="toc-h3 ...">...method 2</li>  <-- Shorten
             ...more methods                          <-- Shorten
            </ul>
           </li>
           <li class="toc-h2 ...">...gallery examples</li>
          </ul>

         </li>                                 <-- Unwrapped
        </ul>                                  <-- Unwrapped
        """
        soup = context["pst_generate_toc_html"](kind="soup")

        try:
            # Unwrap the outermost level
            soup.ul.unwrap()
            soup.li.unwrap()
            soup.a.decompose()

            # Get all toc-h2 level entries, where the first one should be the function
            # or class, and the second one, if exists, should be the examples; there
            # should be no more than two entries at this level for generated API pages
            lis = soup.ul.select("li.toc-h2")
            main_li = lis[0]
            meth_list = main_li.ul

            if meth_list is not None:
                # This is a class API page, we remove the class name from the method
                # names to make them better fit into the secondary sidebar; also we
                # make the toc-h3 level entries always visible to more easily navigate
                # through the methods
                meth_list["class"].append("visible")
                for meth in meth_list.find_all("li", {"class": "toc-h3"}):
                    target = meth.a.code.span
                    target.string = target.string.split(".", 1)[1]

            # This corresponds to the behavior of `generate_toc_html`
            return str(soup) if kind == "html" else soup

        except Exception as e:
            # Upon any failure we return the original pagetoc
            logger.warning(
                f"Failed to generate API pagetoc for {pagename}: {e}; falling back"
            )
            return context["pst_generate_toc_html"](kind=kind)

    # Override the pydata-sphinx-theme implementation for generate API pages
    if pagename.startswith("modules/generated/"):
        context["pst_generate_toc_html"] = context["generate_toc_html"]
        context["generate_toc_html"] = generate_api_toc_html


def setup(app):
    # Need to be triggered after `pydata_sphinx_theme.toctree.add_toctree_functions`,
    # and since default priority is 500 we set 900 for safety
    app.connect("html-page-context", override_pst_pagetoc, priority=900)

这段代码实现了一个Sphinx扩展,用于覆盖pydata-sphinx-theme的页面目录生成功能,为API参考页面定制目录结构,使得方法列表能够更好地适应侧边栏显示。[/content]

[content]

"""A Sphinx extension for linking to your project's issue tracker.

Copyright 2014 Steven Loria

Permission is hereby granted, free of charge, to any person obtaining a copy

of this software and associated documentation files (the "Software"), to deal

in the Software without restriction, including without limitation the rights

to use, copy, modify, merge, publish, distribute, sublicense, and/or sell

copies of the Software, and to permit persons to whom the Software is

furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in

all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN

THE SOFTWARE.

"""

import re

from docutils import nodes, utils

from sphinx.util.nodes import split_explicit_title

version = "1.2.0"

author = "Steven Loria"

license = "MIT"

def user_role(name, rawtext, text, lineno, inliner, options=None, content=None):

"""Sphinx role for linking to a user profile. Defaults to linking to

Github profiles, but the profile URIS can be configured via the

issues_user_uri config value.

Examples: ::

:user:sloria

Anchor text also works: ::

:user:Steven Loria <sloria>

"""

options = options or {}

content = content or []

has_explicit_title, title, target = split_explicit_title(text)

target = utils.unescape(target).strip()

title = utils.unescape(title).strip()

config = inliner.document.settings.env.app.config

if config.issues_user_uri:

ref = config.issues_user_uri.format(user=target)

else:

ref = "https://github.com/{0}".format(target)

if has_explicit_title:

text = title

else:

text = "@{0}".format(target)

link = nodes.reference(text=text, refuri=ref, **options)

return [link], []

def cve_role(name, rawtext, text, lineno, inliner, options=None, content=None):

"""Sphinx role for linking to a CVE on https://cve.mitre.org.

Examples: :::

:cve:CVE-2018-17175

"""

options = options or {}

content = content or []

has_explicit_title, title, target = split_explicit_title(text)

target = utils.unescape(target).strip()

title = utils.unescape(title).strip()

ref = "https://cve.mitre.org/cgi-bin/cvename.cgi?name={0}".format(target)

text = title if has_explicit_title else target

link = nodes.reference(text=text, refuri=ref, **options)

return [link], []

class IssueRole(object):

EXTERNAL_REPO_REGEX = re.compile(r"^(\w+)/(.+)([#@])([\w]+)$")

def init(

self, uri_config_option, format_kwarg, github_uri_template, format_text=None

):

self.uri_config_option = uri_config_option

self.format_kwarg = format_kwarg

self.github_uri_template = github_uri_template

self.format_text = format_text or self.default_format_text

@staticmethod

def default_format_text(issue_no):

return "#{0}".format(issue_no)

def make_node(self, name, issue_no, config, options=None):

name_map = {"pr": "pull", "issue": "issues", "commit": "commit"}

options = options or {}

repo_match = self.EXTERNAL_REPO_REGEX.match(issue_no)

if repo_match: # External repo

username, repo, symbol, issue = repo_match.groups()

if name not in name_map:

raise ValueError(

"External repo linking not supported for :{}:".format(name)

)

path = name_map.get(name)

ref = "https://github.com/{issues_github_path}/{path}/{n}".format(

issues_github_path="{}/{}".format(username, repo), path=path, n=issue

)

formatted_issue = self.format_text(issue).lstrip("#")

text = "{username}/{repo}{symbol}{formatted_issue}".format(**locals())

link = nodes.reference(text=text, refuri=ref, **options)

return link

if issue_no not in ("-", "0"):

uri_template = getattr(config, self.uri_config_option, None)

if uri_template:

ref = uri_template.format(**{self.format_kwarg: issue_no})

elif config.issues_github_path:

ref = self.github_uri_template.format(

issues_github_path=config.issues_github_path, n=issue_no

)

else:

raise ValueError(

"Neither {} nor issues_github_path is set".format(

self.uri_config_option

)

)

issue_text = self.format_text(issue_no)

link = nodes.reference(text=issue_text, refuri=ref, **options)

else:

link = None

return link

def call(

self, name, rawtext, text, lineno, inliner, options=None, content=None

):

options = options or {}

content = content or []

issue_nos = [each.strip() for each in utils.unescape(text).split(",")]

config = inliner.document.settings.env.app.config

ret = []

for i, issue_no in enumerate(issue_nos):

node = self.make_node(name, issue_no, config, options=options)

ret.append(node)

if i != len(issue_nos) - 1:

sep = nodes.raw(text=", ", format="html")

ret.append(sep)

return ret, []

"""Sphinx role for linking to an issue. Must have

issues_uri or issues_github_path configured in conf.py.

Examples: :::

:issue:123

:issue:42,45

:issue:sloria/konch#123

"""

issue_role = IssueRole(

uri_config_option="issues_uri",

format_kwarg="issue",

github_uri_template="https://github.com/{issues_github_path}/issues/{n}",

)

"""Sphinx role for linking to a pull request. Must have

issues_pr_uri or issues_github_path configured in conf.py.

Examples: ::

:pr:123

:pr:42,45

:pr:sloria/konch#43

"""

pr_role = IssueRole(

uri_config_option="issues_pr_uri",

format_kwarg="pr",

github_uri_template="https://github.com/{issues_github_path}/pull/{n}",

)

def format_commit_text(sha):

return sha[:7]

"""Sphinx role for linking to a commit. Must have

issues_pr_uri or issues_github_path configured in conf.py.

Examples: :::

:commit:123abc456def

:commit:sloria/konch@123abc456def

"""

commit_role = IssueRole(

uri_config_option="issues_commit_uri",

format_kwarg="commit",

github_uri_template="https://github.com/{issues_github_path}/commit/{n}",

format_text=format_commit_text,

)

def setup(app):

Format template for issues URI

e.g. 'https://github.com/sloria/marshmallow/issues/

app.add_config_value("issues_uri", default=None, rebuild="html")

Format template for PR URI

e.g. 'https://github.com/sloria/marshmallow/pull/

app.add_config_value("issues_pr_uri", default=None, rebuild="html")

Format template for commit URI

e.g. 'https://github.com/sloria/marshmallow/commits/

app.add_config_value("issues_commit_uri", default=None, rebuild="html")

Shortcut for Github, e.g. 'sloria/marshmallow'

app.add_config_value("issues_github_path", default=None, rebuild="html")

Format template for user profile URI

e.g. 'https://github.com/{user}'

app.add_config_value("issues_user_uri", default=None, rebuild="html")

app.add_role("issue", issue_role)

app.add_role("pr", pr_role)

app.add_role("user", user_role)

app.add_role("commit", commit_role)

app.add_role("cve", cve_role)

return {

"version": version,

"parallel_read_safe": True,

"parallel_write_safe": True,

}


这段代码实现了一个Sphinx扩展,提供了链接到GitHub问题、拉取请求、提交和用户资料的角色,同时也支持链接到CVE条目。它通过配置值定义URL模板,并在不同的上下文中解析和生成相应的超链接。[/content]

[content]
## 68.7 前端交互层 —— 文档站点的"用户体验引擎"

**核心前端脚本功能**
- `api-search.js`:实现客户端 API 搜索,基于预生成的 JSON 索引(`api-search.json`),支持模糊匹配、分类筛选、键盘导航
- `dropdown.js`:控制导航栏下拉菜单、版本切换器、主题切换器的展开/折叠、点击外部关闭、键盘无障碍访问
- `sg_plotly_resize.js`:监听窗口 resize 事件,自动调用 `Plotly.Plots.resize()` 使示例图库中的 Plotly 图表响应式适配
- `theme-observer.js`:监听 `color-scheme` 媒体查询变化,同步更新 `html` 元素的 `data-theme` 属性,实现系统级深色/浅色模式跟随
- `version-switcher.js`:从 `versions.json` 加载版本列表,渲染版本切换器下拉菜单,处理版本跳转与 URL 映射
- `vendor/svg-pan-zoom.min.js`:第三方库,为 SVG 图表(如决策树可视化)提供平移缩拉交互能力

源码路径:`doc/js/scripts/api-search.js` - `*`(1-300行)
源码路径:`doc/js/scripts/dropdown.js` - `*`(1-200行)
源码路径:`doc/js/scripts/sg_plotly_resize.js` - `*`(1-100行)
源码路径:`doc/js/scripts/theme-observer.js` - `*`(1-100行)
源码路径:`doc/js/scripts/version-switcher.js` - `*`(1-150行)
源码路径:`doc/js/scripts/vendor/svg-pan-zoom.min.js` - `*`(1-500行)

[content]
/**
 * This script is for initializing the search table on the API index page. See
 * DataTables documentation for more information: https://datatables.net/
 */

document.addEventListener("DOMContentLoaded", function () {
  new DataTable("table.apisearch-table", {
    order: [], // Keep original order
    lengthMenu: [10, 25, 50, 100, { label: "All", value: -1 }],
    pageLength: -1, // Show all entries by default
  });
});

这段代码实现了API索引页面的搜索表格初始化,使用DataTables库提供数据过滤、排序和分页功能,默认显示所有条目。[/content]

[content]

/**

  • This script is used to add the functionality of collapsing/expanding all dropdowns

  • on the page to the sphinx-design dropdowns. This is because some browsers cannot

  • search into collapsed

    (such as Firefox).

  • The reason why the buttons are added to the page with JS (dynamic) instead of with

  • sphinx (static) is that the button will not work without JS activated, so we do not

  • want them to show up in that case.

*/

document.addEventListener("DOMContentLOAD", () => {

// Get all sphinx-design dropdowns

const allDropdowns = document.querySelectorAll("details.sd-dropdown");

allDropdowns.forEach((dropdown) => {

// Get the summary element of the dropdown, where we will place the buttons

const summaryTitle = dropdown.querySelector("summary.sd-summary-title");

// The state marker with the toggle all icon inside

const newStateMarker = document.createElement("span");

const newIcon = document.createElement("i");

newIcon.classList.add("fa-solid", "fa-angles-right");

newStateMarker.appendChild(newIcon);

// Classes for styling; sd-summary-state-marker and sd-summary-chevron-right are

implemented by sphinx-design; sk-toggle-all is implemented by us

newStateMarker.classList.add(

"sd-summary-state-marker",

"sd-summary-chevron-right",

"sk-toggle-all"

);

Bootstrap tooltip configurations

newStateMarker.setAttribute("data-bs-toggle", "tooltip");

newStateMarker.setAttribute("data-bs-placement", "top");

newStateMarker.setAttribute("data-bs-offset", "0,10");

newStateMarker.setAttribute("data-bs-title", "Toggle all dropdowns");

Enable the tooltip

new bootstrap.Tooltip(newStateMarker);

Assign the collapse/expand action to the state marker

newStateMarker.addEventListener("click", () => {

if (dropdown.open) {

console.log("[SK] Collapsing all dropdowns...");

allDropdowns.forEach((node) => {

if (node !== dropdown) {

node.removeAttribute("open");

}

});

} else {

console.log("[SK] Expanding all dropdowns...");

allDropdowns.forEach((node) => {

if (node !== dropdown) {

node.setAttribute("open", "");

}

});

});

Append the state marker to the summary element

summaryTitle.insertBefore(newStateMarker, summaryTitle.lastElementChild);

});

});


这段代码实现了一个功能,通过在sphinx-design下拉菜单的标题元素中添加一个状态标记,实现了在页面上展开或折叠所有下拉菜单的功能,提高了文档的可用性和可访问性。[/content]

[content]
// Related to https://github.com/scikit-learn/scikit-learn/issues/30279
// There an interaction between plotly and bootstrap/pydata-sphinx-theme
// that causes plotly figures to not detect the right-hand sidebar width

// Plotly figures are responsive, this triggers a resize event once the DOM has
// finished loading so that they resize themselves.

document.addEventListener("DOMContentLoaded", () => {
  window.dispatchEvent(new Event("resize"));
});

这段代码在DOM加载完成后触发一个窗口大小变化事件,以解决Plotly图表与PyData Sphinx主题之间的交互问题,确保图表能够正确响应式地调整大小以适应侧边栏的宽度。[/content]

[content]

(function () {

const observer = new MutationObserver((mutationsList) => {

for (const mutation of mutationsList) {

if (

mutation.type === "attributes" &&

mutation.attributeName === "data-theme"

) {

document

.querySelectorAll(".sk-top-container")

.forEach((estimatorElement) => {

const newTheme = detectTheme(estimatorElement);

estimatorElement.classList.remove("light", "dark");

estimatorElement.classList.add(newTheme);

});

}

}

});

observer.observe(document.documentElement, {

attributes: true,

attributeFilter: ["data-theme"],

});

})();


这段代码实现了一个主题观察器,用于监测文档的色彩主题变化(如从浅色切换到深色),并相应地更新所有估计器显示元素的主题类,以确保在系统主题切换时文档界面能够保持视觉一致性。[/content]

[content]
/**
 * Adds the link to available documentation page as the last entry in the version
 * switcher dropdown. Since other entries in the dropdown are also added dynamically,
* we only add the link when the user clicks on some version switcher button to make
* sure that this entry is the last one.
 */

function addVersionSwitcherAvailDocsLink() {
  var availDocsLinkAdded = false;

  // There can be multiple version switcher buttons because there is at least one for
  // laptop size and one for mobile size (in the sidebar)
  document
    .querySelectorAll(".version-switcher__button")
    .forEach(function (btn) {
      btn.addEventListener("click", function () {
        if (!availDocsLinkAdded) {
          # All version switcher dropdowns are updated once any button is clicked
          document
            .querySelectorAll(".version-switcher__menu")
            .forEach(function (menu) {
              var availDocsLink = document.createElement("a");
              availDocsLink.setAttribute(
                "href",
                "https://scikit-learn.org/dev/versions.html"
              );
              availDocsLink.innerHTML = "More";
              # We use the same class as the last entry to be safe
              availDocsLink.className = menu.lastChild.className;
              availDocsLink.classList.add("sk-avail-docs-link");
              menu.appendChild(availDocsLink);
            });
          # Set the flag so we do not add again
          availDocsLinkAdded = true;
        }
      });
    });
}

document.addEventListener("DOMContentLoaded", addVersionSwitcherAvailDocsLink);

这段代码实现了一个功能,在用户点击版本切换器按钮时,动态地在版本切换器菜单的末尾添加一个指向开发版文档的链接(“More”),确保用户可以轻松访问最新的开发版文档。

[/content]

[content]

68.3 生活类比

想象 scikit-learn 的文档系统是一座智能化的「知识出版工厂」conf.py = 总控台:统筹项目元数据、扩展注册、主题定制与构建事件钩子 api_reference.py = 目录编纂室:通过配置字典自动生成结构化 API 文档页面 conftest.py = 环境体检站:基于 pytest 的条件跳过机制确保示例在不同环境稳健运行 sphinxext/ = 专属工具箱:7 个定制扩展解决 NaN 估计器列表、短摘要渲染、DOI 角色、GitHub 源码链接等专项需求 js/scripts/ = 用户体验引擎:API 搜索、下拉菜单、Plotly 响应式、主题切换、版本切换器等前端交互增强 examples/ = 实战演练场:按主题组织的示例脚本,作为算法使用方法的「教学样本」 release_highlights/ = 版本史册:各版本新特性的可运行演示,记录功能演进历程 asv_benchmarks/ = 性能裁判所:标准化的基准测试套件,通过抽象基类、数据集工厂与评分器构建可复现的性能评测体系 benchmarks/ = 专项擂台赛:针对梯度提升、PCA、聚类、线性模型等热点算法的独立性能对决脚本 就像现代化出版工厂需要总控调度、自动排版、质检把关、专用工具与精美装帧,scikit-learn 文档系统将源码、示例、测试与前端交互整合为一套可扩展、可维护的知识生产流水线;而基准测试体系则像专业的赛车测试场,通过标准化赛道(数据集)、计时器(评分器)与对照组(基线模型)量化算法性能。

68.4 设计中的取舍

为什么采用当前方案,而不是更复杂的替代方案? 本章源码优先选择清晰、可维护且与既有 API 兼容的实现;这降低了使用和调试成本,但也意味着部分极端场景需要调用者自行权衡性能、灵活性与实现复杂度。

68.5 动手练习

68.5.1 阅读 Sphinx 配置核心逻辑

阅读 doc/conf.py,理解以下内容:

  1. 项目元数据(版权、版本、作者)是如何自动提取的

  2. extensions 列表中核心扩展(autodoc、numpydoc、sphinx_gallery 等)的作用

  3. html_theme_optionshtml_context 如何定制 PyData Sphinx 主题

  4. sphinx_gallery_confexamples_dirssubsection_order 控制示例图库生成顺序

  5. setup(app) 中连接的事件钩子(builder-initeddoctree-resolved 等)的作用

回答问题:

  • sphinx_gallery 如何通过 filename_pattern 筛选示例文件?

  • numpydoc_show_class_members = False 对文档输出有何影响?

  • github_usergithub_repo 变量在生成 GitHub 链接时如何使用?

68.5.2 分析 API 引用自动生成机制

阅读 doc/api_reference.py,对比以下配置结构:

  1. API_REFERENCE 字典中模块名到文档页面的映射规则

  2. DEPRECATED_API_REFERENCE 如何处理已弃用 API 的文档生成

  3. _write_api_reference_write_deprecated_api_reference 辅助函数的实现差异

  4. generate_api_reference 函数如何遍历配置生成 .rst 文件

回答问题:

  • 为什么需要区分 API_REFERENCEDEPRECATED_API_REFERENCE 两套配置?

  • autosummary 指令在生成的 .rst 文件中起什么作用?

  • 如何向 API_REFERENCE 添加新模块的文档生成规则?

68.5.3 实现自定义 Sphinx 扩展原型

参考 doc/sphinxext/github_link.pydoc/sphinxext/doi_role.py,实现一个简单的自定义角色扩展 custom_ref_role.py

  1. 定义 custom_ref_role 函数,接收 namerawtexttextlinenoinliner 参数

  2. 解析 text 中的 label <target> 格式,生成指向自定义 URL 模式的引用节点

  3. setup(app) 中使用 app.add_role('custom', custom_ref_role) 注册角色

  4. conf.pyextensions 列表中添加 sphinxext.custom_ref_role 并测试

回答问题:

  • Sphinx 角色函数的返回值格式是什么?

  • inliner.reporter.error 如何在文档构建时报告错误?

  • 如何在角色中访问 Sphinx 环境变量(如 app.config)?

68.5.4 探索示例图库生成流程

阅读 doc/conf.py 中的 sphinx_gallery_conf 配置,并查看 examples/applications/plot_cyclical_feature_engineering.py

  1. 示例脚本的标准结构(文档字符串、代码分块、绘图调用)

  2. sphinx_gallery 如何将 .py 脚本转换为 .rst 文档页面

  3. 缩略图生成与 plot_directive 的交互机制

  4. 交叉引用(.. currentmodule:::class: 等)的自动解析

回答问题:

  • 示例脚本中的 # %% 标记有什么作用?

  • sphinx_gallery 如何处理示例脚本中的异常?

  • 如何在示例中控制生成的缩略图?

68.5.5 分析发布亮点示例的编写模式

对比 examples/release_highlights/plot_release_highlights_1_5_0.pyplot_release_highlights_1_8_0.py

  1. 版本亮点示例的标准结构(模块文档字符串、分节标题、代码演示)

  2. 新特性演示代码的组织方式(导入、数据准备、模型训练、可视化)

  3. .. currentmodule::.. _anchor: 指令的使用规范

  4. 如何通过 plt.show() 控制图表在文档中的显示

回答问题:

  • 为什么发布亮点示例需要显式导入 sklearn.experimental.enable_*

  • 示例中的 # %: 单元格分隔符如何影响文档渲染?

  • 如何确保示例在文档构建时不因缺失依赖而失败?

68.5.6 解析基准测试抽象基类设计

阅读 asv_benchmarks/benchmarks/common.py,理解以下核心抽象:

  1. Benchmark 类如何通过 config.json 统一加载配置并提供公共状态

  2. Estimator 基类定义的 make_data/make_estimator 抽象契约与 setup_cache 机制

  3. PredictorTransformer 基于配置条件式展开的 time_predict/time_transform 方法

  4. track_same_prediction/track_same_transform 回归检测机制的实现

回答问题:

  • get_from_config 如何实现环境变量覆盖配置文件?

  • clear_tmp 在基准测试生命周期中扮演什么角色?

  • 为什么 Estimator 需要同时实现 setup_cachesetup 两个阶段?

68.5.7 剖析独立基准脚本的性能对比范式

选择 benchmarks/bench_hist_gradient_boosting.pybenchmarks/bench_pca_solvers.py

  1. get_equivalent_estimator 如何实现 HistGB 与 LightGBM/XGBoost/CatBoost 的参数映射转换

  2. measure_one 中位计时策略如何减少抖动噪声

  3. bench_plot_svd.py 如何生成 3D 曲面图对比 SciPy 标准 SVD 与随机 SVD

  4. 线程扩展性基准中 threadpoolctl 如何控制并行度

回答问题:

  • 为什么基准测试需要同时报告训练耗时与预测吞吐率?

  • bench_hist_gradient_boosting_adult.py 如何处理分类特征与缺失值?

  • 3D 曲面图的坐标轴分别代表什么维度的性能指标?

68.6 本章小结

本章围绕源码实现梳理了核心数据结构、关键调用流程与设计权衡。

以下是本章概念速查表:

| 概念 | 解释 |

|---|---|

| doc/conf.py | Sphinx 配置中枢:项目元数据、扩展注册、主题定制、示例排序逻辑与事件钩子,文档构建的全局调度机制 |

| doc/api_reference.py | API 引用生成器:API_REFERENCE 与 DEPRECATED_API_REFERENCE 配置字典及辅助函数,自动生成结构化 API 文档页面 |

| doc/conftest.py | 文档测试守卫:基于 pytest 的条件跳过机制,动态依赖检查确保文档示例在不同环境下稳健运行 |

| doc/sphinxext/ | 自定义 Sphinx 扩展套件:7 个扩展分别实现 NaN 估计器列表、短摘要渲染、DOI 角色、下拉锚点、GitHub 链接、页面目录覆盖、Issue 关联 |

| doc/js/scripts/ | 前端交互层:API 搜索、下拉菜单、Plotly 响应式、主题切换、版本切换器等脚本提升文档站点用户体验 |

| examples/applications/ | 应用案例集:跨领域综合案例,展示 scikit-learn 在真实世界问题中的组合式应用 |

| examples/release_highlights/ | 版本亮点示例:0.22 至 1.8 各版本新特性的可运行演示,作为功能演进的直观教学工具 |

| sphinx-gallery | 示例图库生成器:自动执行示例脚本、捕获输出、生成缩略图与交叉引用,构建可执行的文档画廊 |

| asv_benchmarks/benchmarks/common.py | 基准测试抽象基类:Benchmark/Estimator/Predictor/Transformer 定义统一接口与缓存机制 |

| asv_benchmarks/benchmarks/datasets.py | 数据集工厂:合成与真实数据集的生成/加载,joblib.Memory 缓存加速重复实验 |

| asv_benchmarks/benchmarks/utils.py | 评分函数集:neg_mean_inertia、make_gen_classif_scorers 等针对不同任务的性能度量 |

| benchmarks/ | 独立基准脚本:针对 HistGB、PCA、KMeans、线性模型等专题的深度性能剖析与对比实验 |

下一章将继续沿着相关模块的调用链深入分析。

68.7 架构与数据流图

graph TD A[conf] --> B[api_reference] B --> C[conftest]
sequenceDiagram participant U as 调用者 participant E as conf participant C as api_reference U->>E: 调用入口 E->>C: 传递参数 C-->>U: 返回结果
graph LR I[输入] --> P[参数校验] P --> T[核心处理] T --> O[输出]
graph TD L1[用户 API 层] --> L2[算法/服务层] L2 --> L3[数据结构层] L3 --> L4[运行时与依赖层]

上述图分别展示模块依赖、调用时序、数据流和架构分层。

第 69 章 —— 聚类分析实战 —— 数据分群的“地形测绘仪”

69.1 学习目标

  • 难度:★★★☆☆(3/5)

  • 预备知识:Python 基础、面向对象编程与 Markdown/代码阅读基础

  • 理解 scikit‑learn 示例库的目录结构与主题划分

  • 掌握聚类、分类、回归、降维、预处理等核心模块的典型使用范式

  • 了解如何通过示例快速上手特定算法或解决特定问题

  • 学会阅读示例代码以提取最佳实践模式

  • 理解聚类算法的假设、局限性与评估指标(轮廓系数、ARI、AMI、V‑measure)

  • 掌握 KMeans、MiniBatchKMeans、DBSCAN、OPTICS、HDBSCAN、层次聚类、谱聚类、双聚类等算法的典型使用场景

  • 理解如何通过可视化分析聚类结果、决策边界、稳定性与超参数敏感性

69.2 生活类比

想象示例图库是一座“机器学习实战超市”

  • 按模块分区 = 货架分类(聚类区、分类区、降维区、集成区等)

  • 每个示例文件 = 一款“即买即用”的成品套餐,涵盖数据准备、建模、评估、可视化全流程

  • 应用案例集 = “场景定制餐”,比如图像去噪、人脸识别、股票预测、异常检测等真实业务场景

  • 版本演进展示 = “新品发布会”,每个版本新增特性都用可运行代码演示

  • 开发工具与扩展 = “后厨工具间”,提供自定义估计器、缺失值处理、模型解释等进阶技艺

  • 聚类实验室 = “地形测绘中心”,KMeans 等中心法像“网格划分”,DBSCAN/OPTICS/HDBSCAN 像“地貌识别”,层次聚类像“系谱构建”,谱/双聚类像“图分割与棋盘重组”。

逛超市时,你不必细读每款套餐的配料表(源码),只要挑中符合需求的那一款(示例),复制代码、微调参数/数据,即可直接上桌(跑通业务)。

69.3 源码地图

examples/cluster/plot_kmeans_assumptions.py
├── __main__ (1-150行)  # KMeans 假设演示:各种数据分布下的失效模式与替代方案
examples/cluster/plot_kmeans_digits.py
├── __main__ (1-150行)  # 手写数字聚类:初始化策略对比与聚类质量指标评估
├── bench_k_means (1-150行)  # KMeans 基准测试函数:计时、惯性与多指标评估
examples/cluster/plot_kmeans_plusplus.py
├── __main__ (1-150行)  # KMeans++ 初始化可视化:种子选择过程演示
examples/cluster/plot_kmeans_silhouette_analysis.py
├── __main__ (1-150行)  # 轮廓分析选取聚类数:轮廓图与聚类可视化联动
examples/cluster/plot_kmeans_stability_low_dim_dense.py
├── __main__ (1-150行)  # KMeans 初始化稳定性评估:n_init 对惯性方差的影响
├── make_data (1-150行)  # 生成网格分布的各向同性高斯簇数据
examples/cluster/plot_mini_batch_kmeans.py
├── __main__ (1-150行)  # KMeans 与 MiniBatchKMeans 对比:训练时间、惯性与标签差异可视化
examples/cluster/plot_dbscan.py
├── __main__ (1-150行)  # DBSCAN 密度聚类演示:核心样本、噪声点与监督评估指标
examples/cluster/plot_optics.py
├── __main__ (1-150行)  # OPTICS 演示:可达度图、Xi 方法与 DBSCAN 等价切分
examples/cluster/plot_hdbscan.py
├── __main__ (1-150行)  # HDBSCAN 演示:尺度不变性、多尺度聚类与超参数鲁棒性
│   └── plot (1-150行)  # 通用聚类可视化函数:支持概率权重的散点绘制
examples/cluster/plot_agglomerative_clustering_metrics.py
├── __main__ (1-150行)  # 层次聚类度量对比:余弦/欧氏/城市块距离对波形聚类的影响
├── sqr (1-150行)          # 方波生成函数:用于构造合成波形数据
examples/cluster/plot_agglomerative_dendrogram.py
├── __main__ (1-150行)  # 层次聚类树状图绘制:基于 Iris 数据完整树与截断展示
├── plot_dendrogram (1-150行)  # 将 AgglomerativeClustering 转换为 scipy linkage 并绘制
examples/cluster/plot_birch_vs_minibatchkmeans.py
├── __main__ (1-150行)  # BIRCH 与 MiniBatchKMeans 规模性能对比:全局聚类步骤影响
examples/cluster/plot_bisect_kmeans.py
├── __main__ (1-150行)  # BisectingKMeans 与普通 KMeans 性能比较:层次分裂结构 vs 扁平结构
examples/cluster/plot_linkage_comparison.py
├── __main__ (1-150行)  # 四种链接策略在玩具数据上的行为对比:单/平均/完全/Ward 链接
examples/cluster/plot_ward_structured_vs_unstructured.py
├── __main__ (1-150行)  # 结构化 vs 非结构化 Ward 聚类:Swiss Roll 与螺旋数据的连通约束影响
examples/cluster/plot_affinity_propagation.py
├── __main__ (1-150行)  # 亲和传播聚类:消息传递机制与簇中心自动确定
examples/cluster/plot_mean_shift.py
├── __main__ (1-150行)  # 均值漂移聚类:带宽自动估计与种子箱策略
examples/cluster/plot_coin_segmentation.py
├── __main__ (1-150行)  # 谱聚类图像分割:三种标签分配策略对硬币图像的影响
examples/cluster/plot_coin_ward_segmentation.py
├── __main__ (1-150行)  # 结构化 Ward 层次聚类图像分割:空间连通约束确保区域连通
examples/cluster/plot_segmentation_toy.py
├── __main__ (1-150行)  # 谱聚类分割圆环玩具数据:mask 限制前景与 Voronoi 式分割
examples/bicluster/plot_bicluster_newsgroups.py
├── __main__ (1-150行)  # 20 Newsgroups 双聚类:SpectralCoclustering 与 MiniBatchKMeans 对比
├── number_normalizer (1-150行)  # 数字标记归一化器:将数字 token 映射为 #NUMBER
├── NumberNormalizingVectorizer.build_tokenizer (1-150行)  # 自定义 TF‑IDF 向量化器:集成数字归一化
├── bicluster_ncut (1-150行)  # 双聚类归一化割评价:基于文档‑词矩阵的切割代价计算
examples/bicluster/plot_spectral_biclustering.py
├── __main__ (1-150行)  # 谱双聚类棋盘数据重构:行列重排后的双聚类可视化与共识得分
examples/bicluster/plot_spectral_coclustering.py
├── __main__ (1-150行)  # 谱共聚类植入模式恢复:共识得分评估重排矩阵的恢复质量
examples/cluster/plot_cluster_comparison.py
├── __main__ (1-150行)  # 11 种聚类算法在 6 种玩具数据上的综合对比:参数调优、运行时、标签可视化

69.4 K‑Means 家族与初始化策略 —— 聚类基石的“锚点选择术”

核心概念

KMeans 假设簇为球形、方差相等、规模相近。当任一假设被破坏时,算法会产生异常聚类。我们通过四个失效示例演示:① 聚类数不匹配、② 各向异性、③ 方差不等、④ 簇大小不均。随后展示轮廓分析帮助选取合适的 n_clusters,以及 KMeans++ 与随机、PCA 初始化的差异。

核心类型定义

# 第 69 章 —— 文件: examples/cluster/plot_kmeans_assumptions.py
# 第 69 章 —— 核心类/函数: KMeans

源码路径:examples/cluster/plot_kmeans_assumptions.py - __main__(1‑150 行)

from sklearn.cluster import KMeans

common_params = {
    "n_init": "auto",
    "random_state": random_state,
}

# 第 69 章 —— 非最优簇数量(K=2 而真实为 3)
y_pred = KMeans(n_clusters=2, **common_params).fit_predict(X)
axs[0, 0].scatter(X[:, 0], X[:, 1], c=y_pred)
axs[0, 0].set_title("Non-optimal Number of Clusters")

# 第 69 章 —— 各向异性分布的簇
y_pred = KMeans(n_clusters=3, **common_params).fit_predict(X_aniso)
axs[0, 1].scatter(X_aniso[:, 0], X_aniso[:, 1], c=y_pred)
axs[0, 1].set_title("Anisotropically Distributed Blobs")

代码解释(逐行注释)

| 行号 | 注释 |

|------|------|

| 1‑3 | 从 sklearn.cluster 导入 KMeans 类。 |

| 5‑8 | 定义 common_params,统一使用 n_init="auto"(自动选择初始化次数)和 random_state,保证可复现。 |

| 11‑13 | 对原始球形数据使用 错误的簇数(2),调用 fit_predict 返回预测标签 y_pred。 |

| 14‑16 | 将预测标签映射为颜色,散点图展示聚类结果,并标注子图标题。 |

| 19‑21 | 对 各向异性 数据 X_aniso 使用正确的簇数(3),同上绘图。 |

| … | 类似的代码块分别演示方差不等与簇大小不均的情形。 |

这段代码展示了 KMeans 在不同假设破坏下的失效,并提供了 可视化对比,帮助读者直观感知何时需要更换聚类模型。

完整数据流图(展示从数据生成到聚类结果的路径):

graph TD A[make_blobs] --> B[数据变形 (线性 transformation)] B --> C[KMeans (不同 n_clusters/初始化)] C --> D[matplotlib 散点绘图] D --> E[观察失效模式]

69.4.1 轮廓系数分析选 K

源码路径:examples/cluster/plot_kmeans_silhouette_analysis.py - __main__(1‑150 行)

from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_samples, silhouette_score

range_n_clusters = [2, 3, 4, 5, 6]

for n_clusters in range_n_clusters:
    # 1️⃣ 初始化 KMeans
    clusterer = KMeans(n_clusters=n_clusters, random_state=10)
    cluster_labels = clusterer.fit_predict(X)

    # 2️⃣ 计算整体轮廓均值
    silhouette_avg = silhouette_score(X, cluster_labels)

    # 3️⃣ 计算每个样本的轮廓值
    sample_silhouette_values = silhouette_samples(X, cluster_labels)

代码解释(逐行注释)

| 行号 | 注释 |

|------|------|

| 1‑2 | 导入 KMeans 与轮廓评估函数。 |

| 4‑5 | 定义待评估的 n_clusters 集合。 |

| 7‑9 | 对每个候选簇数创建 KMeans 实例并拟合数据,得到 cluster_labels。 |

| 11‑13 | 使用 silhouette_score 计算所有样本的 平均轮廓系数,用于全局质量评估。 |

| 15‑17 | 调用 silhouette_samples 获取每个样本的 局部轮廓值,后续用于绘制轮廓条形图。 |

| 20‑45 | 循环绘制每个簇的轮廓条形图、基准线(平均轮廓)以及聚类散点图。 |

此段代码展示 从数值指标到可视化 的完整流程,帮助判断 最佳聚类数(轮廓最高且分布均匀时)。

可视化流程(Mermaid)

graph LR X[生成数据] --> K[KMeans (vary n_clusters)] K --> S[Silhouette Score] S --> V[可视化: 条形图 + 散点]

69.4.2 K‑Means++ 初始化可视化

源码路径:examples/cluster/plot_kmeans_plusplus.py - __main__(1‑150 行)

from sklearn.cluster import kmeans_plusplus
from sklearn.datasets import make_blobs

# 第 69 章 —— 生成 4000 条样本,4 个中心
X, y_true = make_blobs(n_samples=n_samples, centers=n_components,
                       cluster_std=0.60, random_state=0)
X = X[:, ::-1]  # 交换坐标轴,仅作可视化区别

# 第 69 章 —— 使用 kmeans_plusplus 计算初始化种子
centers_init, indices = kmeans_plusplus(X, n_clusters=4, random_state=0)

# 第 69 章 —— 绘制原始点与初始化质心
plt.scatter(X[cluster_data, 0], X[cluster_data, 1], c=col, marker='.', s=10)
plt.scatter(centers_init[:, 0], centers_init[:, 1], c='b', s=50)

逐行注释

| 行号 | 注释 |

|------|------|

| 1‑2 | 导入 kmeans_plusplusmake_blobs。 |

| 5‑9 | 生成四簇数据,每簇标准差 0.6,随机种子确保可复现。 |

| 10 | 将特征轴调换,仅用于展示不同投影。 |

| 13‑14 | 调用 kmeans_plusplus 计算 k‑means++ 初始化的质心坐标及对应索引。 |

| 18‑22 | 使用不同颜色绘制每个真实簇的样本点,随后绘制初始化质心(蓝色大点)。 |

这段代码帮助直观理解 K‑Means++ 如何通过距离加权避免质心重叠,为后续大规模 KMeans 做好“种子准备”。

种子选取示意图(Mermaid):

graph TD Data[原始样本] --> SeedSelection[kmeans_plusplus] SeedSelection --> Seeds[初始化质心] Seeds --> KMeans[后续 KMeans 迭代]

69.4.3 初始化稳健性与 MiniBatchKMeans 对比

源码路径:examples/cluster/plot_kmeans_stability_low_dim_dense.py - __main__(1‑150 行)

以及 make_data(1‑150 行)

def make_data(random_state, n_samples_per_center, grid_size, scale):
    random_state = check_random_state(random_state)
    # 生成 3x3 网格中心
    centers = np.array([[i, j] for i in range(grid_size) for j in range(grid_size)])
    noise = random_state.normal(scale=scale, size=(n_samples_per_center,
                                                   centers.shape[1]))
    X = np.concatenate([c + noise for c in centers])
    y = np.concatenate([[i] * n_samples_per_center for i in range(n_clusters_true)])
    return shuffle(X, y, random_state=random_state)

实验代码(核心循环):

for factory, init, params, format in cases:
    inertia = np.empty((len(n_init_range), n_runs))
    for run_id in range(n_runs):
        X, y = make_data(run_id, n_samples_per_center, grid_size, scale)
        for i, n_init in enumerate(n_init_range):
            km = factory(
                n_clusters=n_clusters,
                init=init,
                random_state=run_id,
                n_init=n_init,
                **params,
            ).fit(X)
            inertia[i, run_id] = km.inertia_
    # 绘制误差棒:横坐标 n_init,纵坐标 mean(inertia)±std
    plt.errorbar(n_init_range, inertia.mean(axis=1), inertia.std(axis=1), fmt=format)

逐行解释

| 行号 | 注释 |

|------|------|

| 1‑14 | make_data 生成 网格分布 的 isotropic 高斯簇,带噪声 scale。 |

| 22‑27 | 定义实验配置:模型工厂(KMeansMiniBatchKMeans)、初始化方式(k‑means++/random)、额外参数(如 max_no_improvement)和绘图符号。 |

| 29‑33 | 为每个配置执行 n_runs 次随机种子实验,以估计 惯性 的均值与方差。 |

| 35‑40 | 对每个 n_init(初始化次数)训练模型,记录 inertia_(簇内平方和)。 |

| 45‑49 | 使用 plt.errorbar 绘制 平均惯性标准差,直观比较 n_init 对收敛稳健性的影响。 |

| 58‑76 | 额外演示单次随机初始化导致的 局部最小(MiniBatchKMeans),通过颜色标记每个簇的中心位置。 |

实验结论

  • 增大 n_init 能显著降低惯性的方差,尤其对 随机初始化 效果更明显。

  • MiniBatchKMeans 在单次随机初始化时容易陷入局部最优(质心位于簇之间),但 n_init 多次可以缓解。

实验数据流图

graph TD Data[make_data] --> Init[不同 n_init] Init --> Model[KMeans / MiniBatchKMeans] Model --> Inertia[记录 inertia_] Inertia --> Plot[误差棒可视化]

69.5 密度与层次聚类 —— 从“邻域密度”到“系谱树构建”的谱系演进

69.5.1 DBSCAN 基础示例

源码路径:examples/cluster/plot_dbscan.py - __main__(1‑150 行)

from sklearn.cluster import DBSCAN
from sklearn import metrics

db = DBSCAN(eps=0.3, min_samples=10).fit(X)
labels = db.labels_

n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0)
n_noise_ = list(labels).count(-1)

print("Estimated number of clusters:", n_clusters_)
print("Estimated number of noise points:", n_noise_)
print("Homogeneity:", metrics.homogeneity_score(labels_true, labels))
# 第 69 章 —— … 其他 supervised 指标

关键点

  • eps 决定邻域半径,min_samples 决定核心点阈值。

  • 噪声点标记为 -1,便于后续可视化。

  • 可使用 监督指标(当真值已知)评估聚类质量。

可视化简要示意(Mermaid):

graph LR X[输入数据] --> DBSCAN[密度聚类] DBSCAN --> L[标签 (包括 -1 噪声)] L --> V[可视化: 核心/边界/噪声]

69.5.2 OPTICS 与可达度图

源码路径:examples/cluster/plot_optics.py - __main__(1‑150 行)

from sklearn.cluster import OPTICS, cluster_optics_dbscan

clust = OPTICS(min_samples=50, xi=0.05, min_cluster_size=0.05)
clust.fit(X)

# 第 69 章 —— 使用 OPTICS 生成的 reachability 图,用 DBSCAN 的不同 eps 进行切分
labels_050 = cluster_optics_dbscan(
    reachability=clust.reachability_,
    core_distances=clust.core_distances_,
    ordering=clust.ordering_,
    eps=0.5,
)

代码要点

  • OPTICS 计算 可达度序列,不需要预设 eps

  • cluster_optics_dbscan 通过给定 eps 在同一可达度图上实现 多阈值切分(相当于多次 DBSCAN)。

  • 可达度图 (reachability_) 能直观展示 密度变化,帮助手动挑选合适 eps

可达度图示意(Mermaid):

graph TD X --> OPTICS[计算可达度 & 核心距离] OPTICS --> RD[可达度曲线] RD --> DBSCAN[eps 切分 (0.5, 2.0, …)] DBSCAN --> Labels[不同聚类结果]

69.5.3 HDBSCAN:尺度不变的多尺度聚类

源码路径:examples/cluster/plot_hdbscan.py - __main__(1‑150 行)以及 plot(1‑150 行)

from sklearn.cluster import HDBSCAN

hdb = HDBSCAN(copy=True)
hdb.fit(X)

# 第 69 章 —— 可视化带概率权重的点
plot(X, hdb.labels_, hdb.probabilities_, parameters={"min_cluster_size":5})

核心优势

  • 无需 eps:内部遍历所有可能的密度阈值。

  • scale‑invariant:对数据的尺度变换(乘以常数)保持聚类不变。

  • min_cluster_sizemin_samples 控制 噪声容忍度最小簇规模,但对结果影响相对更平稳。

对比示意(Mermaid):

graph LR X --> DBSCAN[单一 eps] --> Clust1[易受 eps 影响] X --> HDBSCAN[自动遍历 eps] --> Clust2[鲁棒多尺度]

69.5.4 层次聚类链接策略比较

源码路径:examples/cluster/plot_agglomerative_clustering_metrics.py(聚类指标对比)和 plot_agglomerative_dendrogram.py(树状图)以及 plot_linkage_comparison.py(链接策略)

from sklearn.cluster import AgglomerativeClustering

model = AgglomerativeClustering(
    n_clusters=n_clusters, linkage="average", metric=metric
)
model.fit(X)

关键点

  • metric 可选 cosine、euclidean、cityblock,对高维数据差异显著。

  • linkagesingle、average、complete、ward,每种在噪声、形状、簇大小上表现不同。

  • plot_dendrogramchildren_distances_ 转换为 scipy linkage 矩阵,绘制层次树状图,帮助直观看到 合并顺序

链接策略对比图(Mermaid):

graph TD Data --> Single[Single Linkage] --> Result1 Data --> Average[Average Linkage] --> Result2 Data --> Complete[Complete Linkage] --> Result3 Data --> Ward[Ward Linkage] --> Result4

69.6 谱聚类、双聚类与综合对比 —— 图视角与矩阵视角的“结构挖掘”

69.6.1 谱聚类图像分割(硬币)

源码路径:examples/cluster/plot_coin_segmentation.py - __main__(1‑150 行)

from sklearn.cluster import spectral_clustering
from sklearn.feature_extraction import image

graph = image.img_to_graph(rescaled_coins)
graph.data = np.exp(-beta * graph.data / graph.data.std()) + eps

labels = spectral_clustering(
    graph,
    n_clusters=n_regions + n_regions_plus,
    assign_labels=assign_labels,
    random_state=42,
)

核心要素

  • 将图像转为 稀疏图(像素‑像素梯度),graph.data 经过 指数平滑(beta 控制平滑程度)。

  • assign_labels 有三种策略:kmeansdiscretizecluster_qr,影响最终 分割边界的平滑度

  • beta→0 时图像信息被抹平,聚类退化为 Voronoi 分割;beta 越大越依赖梯度信息。

69.6.2 结构化 Ward 图像分割

源码路径:examples/cluster/plot_coin_ward_segmentation.py - __main__(1‑150 行)

from sklearn.feature_extraction.image import grid_to_graph
connectivity = grid_to_graph(*rescaled_coins.shape)

ward = AgglomerativeClustering(
    n_clusters=n_clusters, linkage="ward", connectivity=connectivity
)
ward.fit(X)

关键点

  • grid_to_graph 生成 像素的 4‑邻接图,作为 connectivity 强制聚类结果 空间连通

  • Ward 在此约束下会 合并相邻像素,防止产生碎片化区域,提升分割的 形状完整性

69.6.3 双聚类:文档‑词矩阵的行列共同聚类

69.6.3.1 Newsgroups 双聚类

源码路径:examples/bicluster/plot_bicluster_newsgroups.py - __main__(1‑150 行)

cocluster = SpectralCoclustering(
    n_clusters=len(categories), svd_method="arpack", random_state=0
)
cocluster.fit(X)   # X 为 TF‑IDF 稀疏矩阵
row_labels = cocluster.row_labels_
col_labels = cocluster.column_labels_
  • 行标签对应 文档簇,列标签对应 词簇,共同发现 主题‑关键词 结构。

  • 使用 Ncutbicluster_ncut)评估每个双簇的 切割质量,选择最优双簇。

69.6.3.2 棋盘数据 Spectral Biclustering

源码路径:examples/bicluster/plot_spectral_biclustering.py - __main__(1‑150 行)

model = SpectralBiclustering(
    n_clusters=n_clusters, method="log", random_state=0
)
model.fit(data)   # data 为 generate_checkerboard(...)
score = consensus_score(
    model.biclusters_, (rows[:, row_idx_shuffled], columns[:, col_idx_shuffled])
)
  • Spectral Biclustering 同时对 行、列 进行聚类,重排后可视化为 棋盘块

  • Consensus score 量化恢复的行列结构与原始结构的一致性(最高 1.0)。

69.6.4 全算法综合基准

源码路径:examples/cluster/plot_cluster_comparison.py - __main__(1‑150 行)

  • 通过 统一的标准化StandardScaler)与 统一的绘图配色,在六个玩具数据集上比较 11 种聚类算法(KMeans、MiniBatchKMeans、DBSCAN、OPTICS、HDBSCAN、谱聚类、层次聚类、BIRCH、GaussianMixture 等)。

  • 每种算法均记录 运行时间聚类标签可视化散点,帮助读者快速感知 不同算法对不同数据形状的适配程度


69.7 聚类评估、归纳式推广与应用拓展 —— 从“指标校准”到“生产落地”的闭环

69.7.1 评估指标的机会校正

源码路径:examples/cluster/plot_adjusted_for_chance_measures.py - random_labels(1‑150 行)等

  • 实验展示 随机标签V‑measure、Rand、ARI、AMI 的影响。

  • 调整后指标(ARI、AMI) 在随机标签下围绕 0 波动,说明它们已 剔除机会因素;而未调整指标随簇数线性上升,误导性强。

69.7.2 归纳式聚类(Inductive Clustering)

源码路径:examples/cluster/plot_inductive_clustering.py - InductiveClusterer(多行)

class InductiveClusterer(ClusterMixin, BaseEstimator):
    def __init__(self, clusterer, classifier):
        self.clusterer = clusterer
        self.classifier = classifier

    def fit(self, X, y=None):
        self.clusterer_ = clone(self.clusterer)
        self.classifier_ = clone(self.classifier)
        y = self.clusterer_.fit_predict(X)
        self.classifier_.fit(X, y)
        self.labels_ = y
        return self

    @available_if(_classifier_has("predict"))
    def predict(self, X):
        check_is_fitted(self)
        return self.classifier_.predict(X)
  • 聚类 获取伪标签,再 训练分类器 进行 归纳推断

  • 通过 available_if 动态暴露 predict / decision_function,只要底层分类器支持即可使用。

  • 适用于 大规模、增量 数据场景,避免每次重新聚类。

69.7.3 特征压缩与字典学习

  • FeatureAgglomerationplot_digits_agglomeration.py)在特征空间进行层次合并,随后 逆变换 恢复图像。

  • MiniBatchKMeans 在线学习 人脸局部字典plot_dict_face_patches.py),展示 partial_fit 在大规模数据流上的增量学习能力。


69.8 设计中的取舍

问:为什么不直接使用 GaussianMixture 代替 KMeans?

  • GaussianMixture(GMM) 通过 EM 对每个簇建模为 高斯分布,能够捕获 不同协方差(各向异性、方差不等)的结构。

  • 代价是 计算量更大(每次 E‑步骤需要计算所有样本对所有簇的概率),在 大规模 2D/3D 数据 上显著慢于 KMeans。

  • 当数据满足 KMeans 的假设(球形、等方差)时,KMeans 更快且足够准确;若假设破坏(如 plot_kmeans_assumptions.py 所示),GMM 才成为更可靠的选择。

问:KMeans 与 MiniBatchKMeans 在大数据上该选哪个?

  • MiniBatchKMeans 采用 小批量更新,显著降低内存占用与计算时间,适合 百万级样本

  • 但批量更新会导致 微小的聚类漂移,惯性稍高,且对 init 更敏感。

  • 若对 聚类质量 要求高且数据规模可放入内存,优先使用 KMeans;若 实时性或资源受限,则选 MiniBatchKMeans 并适当增大 n_init

问:为何在图像分割里仍保留 assign_labels='kmeans' 选项?

  • kmeans 速度最快,适合 大规模 图像。

  • discretizecluster_qr高质量分割(边界更光滑)时优势明显,但计算开销更大。

  • 实际使用中常先用 kmeans 快速预览,若对细节要求高再切换到 cluster_qr


69.9 动手练习

  1. 复现一个应用案例并替换核心算法

    • 选择 examples/applications/plot_face_recognition.pyplot_stock_market.py

    • 完整运行原示例,掌握数据流与评估指标。

    • 将核心估计器(如 SVM → LogisticRegression,GBDT → RandomForest)替换后再次跑通。

    • 思考:替换后精度为何提升/下降?是否涉及数据预处理不匹配?

  2. 追踪某功能的版本演进历史

    • 选定功能(如 HistGradientBoostingTargetEncoderArray API 兼容)。

    • examples/release_highlights/ 中找到该功能首次出现的版本示例,依次运行后续版本。

    • 记录 API 变化(参数名、默认值、返回值、新增方法)。

    • 思考:哪个版本引入了破坏性变更?实验性功能启用机制 (sklearn.experimental) 在哪个版本退役?

  3. 设计一个最小化自定义估计器示例

    • 参考 examples/developing_estimators/sklearn_is_fitted.pyexamples/frozen/plot_frozen_examples.py

    • 实现 MyTransformer(继承 BaseEstimator, TransformerMixin),实现 fit/transform/get_feature_names_out

    • 实现 MyClassifier(继承 BaseEstimator, ClassifierMixin),实现 predict_probadecision_function

    • 将两者放入 Pipeline,验证 set_params、HTML 表示、元数据路由。

    • 思考:必须实现哪些 _required_parameters 才能通过 check_estimator

    • 使用 available_if 动态暴露 predict_proba;探究 FrozenEstimator 如何防止误调用 fit

  4. 聚类算法假设失效诊断与替代方案实验

    • 运行 examples/cluster/plot_kmeans_assumptions.py 观察四种失效场景。

    • 为每种场景解释 KMeans 失效原因。

    • 使用 examples/cluster/plot_kmeans_silhouette_analysis.py 通过轮廓分析辅助选 K。

    • 再运行 examples/cluster/plot_cluster_comparison.py,比较 GaussianMixture、HDBSCAN、SpectralClustering 在同一数据上的表现。

    • 思考:GMM 如何通过协方差类型(full、diag、tied)处理各向异性与方差不等?

    • HDBSCAN 如何在不指定 eps 的情况下自动适应多尺度密度?

    • SpectralClustering 的 affinity 参数(nearest_neighbors vs rbf)如何影响图构建?

  5. 聚类稳定性与初始化策略深度对比

    • 运行 examples/cluster/plot_kmeans_stability_low_dim_dense.py,记录 n_init 对惯性方差的影响。

    • n_init 增大至 100,观察收敛是否饱和。

    • 在真实手写数字数据上运行 examples/cluster/plot_kmeans_digits.py,比较 k-means++、random、PCA 初始化 的指标差异(惯性、Silhouette、V‑measure 等)。

    • 思考:为何 MiniBatchKMeans 随机初始化的方差下降更慢?

    • 在高维稀疏数据上,n_init='auto' 的启发式规则是什么?

    • 如何联合 inertia_silhouette_score 判断是否陷入局部最优?

  6. 图像分割中的谱聚类与层次聚类实战

    • 运行 examples/cluster/plot_coin_segmentation.pyplot_coin_ward_segmentation.py

    • 对比三种 assign_labels(kmeans、discretize、cluster_qr)在分割边界平滑度上的差异。

    • 观察结构化 Ward 中 connectivity=grid_to_graph 如何强制区域连通。

    • 使用 examples/cluster/plot_segmentation_toy.py 探索 mask 对分割范围的限制。

    • 思考:β 参数如何控制“图像依赖程度”?β→0 时退化为什么?

    • 若将 connectivity 替换为 kneighbors_graph(n_neighbors=4),会出现什么变化?

    • cluster_qr 如何避免 kmeans 标签随机性导致的不稳定分割?

69.10 本章小结

本章我们系统地走完了 scikit‑learn 示例库的聚类实验室

  • 先从 K‑Means 核心假设失效场景 入手,学会使用 轮廓分析初始化策略 提升稳健性。

  • 随后探索 密度族算法(DBSCAN、OPTICS、HDBSCAN)如何通过 邻域密度可达度 捕捉非球形簇,并理解它们的 参数敏感性

  • 再对 层次聚类链接策略连通约束 进行细致比较,展示 树状结构结构化聚类 的实际效果。

  • 随后进入 谱聚类与双聚类,把 图视角矩阵视角 融合,用图像分割与文本共聚类案例直观呈现 结构挖掘

  • 最后通过 评估指标的机会校正归纳式聚类特征压缩,形成从 指标校准生产落地 的完整闭环。

这一系列 源码驱动、逐行解释、可视化交织 的学习方式,让你不仅能快速复制运行示例,还能抽取最佳实践并迁移到自己的项目中。

69.10.1 本章概念表

| 概念 | 解释 |

|------|------|

| KMeans 假设 | 簇为球形、方差相等、规模相近;违背时易失效 |

| 初始化策略 | kmeans++randomPCA;影响收敛速度与局部最优概率 |

| 轮廓系数 | [-1,1] 区间,衡量样本在簇内凝聚度与簇间分离度 |

| DBSCAN / OPTICS / HDBSCAN | 基于密度的聚类族,分别使用固定 eps、可变 eps、全 eps 扫描 |

| 链接策略 | singleaveragecompleteward,决定层次聚类合并方式 |

| 结构化 Ward | 使用 connectivity 图约束合并,保持空间连通性 |

| Spectral Clustering | 在图上做 Laplacian 特征分解,适用于非凸形状 |

| 双聚类 | 同时聚类行列(文档‑词、像素‑颜色),如 SpectralCoclustering |

| 调整后指标(ARI/AMI) | 消除随机标签的机会效应,基准评估聚类质量 |

| 归纳式聚类 | 用聚类标签训练分类器,实现新样本的快速推断 |

69.11 下一章预告

在下一章 “集成学习纵深 —— 模型集成的‘联邦议会’” 中,我们将打开 Bagging、Boosting、Stacking 与 Voting 四大集成范式的大门,比较它们在 多样性、偏差‑方差权衡、并行化 等维度的表现,并通过 实际案例 展示如何利用 scikit‑learn 的 Pipeline交叉验证 统一管理多个弱学习器,构建强大的预测模型。

祝你在聚类实验室的探索之旅收获满满,期待在集成学习的“议会”里再会!

69.12 生活类比

想象示例图库是一座'机器学习实战超市': 按模块分区 = 货架分类(聚类区、分类区、降维区、集成区...) 每个示例文件 = 一款'即买即用'的成品套餐(含数据准备、建模、评估、可视化全流程) 应用案例集 = '场景定制餐':图像去噪、人脸识别、股票预测、异常检测等真实业务场景 版本演进展示 = '新品发布会':每个版本的新特性用可运行代码演示 开发工具与扩展 = '后厨工具间':自定义估计器、缺失值处理、模型解释等进阶技艺 聚类实验室 = '地形测绘中心':KMeans 等中心法像'网格划分'、DBSCAN/OPTICS/HDBSCAN 像'地貌识别'、层次聚类像'系谱构建'、谱/双聚类像'图分割与棋盘重组' 逛超市时,你不必读懂每款套餐的配料表(源码),只需挑中看的(需求匹配),拿回家(复制代码)微调配料(参数/数据)即可开吃(跑通业务)

69.13 源码地图

examples/cluster/plot_kmeans_assumptions.py
├── __main__ (1-150行)  # KMeans 假设演示:各种数据分布下的失效模式与替代方案
examples/cluster/plot_kmeans_digits.py
├── __main__ (1-150行)  # 手写数字聚类:初始化策略对比与聚类质量指标评估
├── bench_k_means (1-150行)  # KMeans 基准测试函数:计时、惯性与多指标评估
examples/cluster/plot_kmeans_plusplus.py
├── __main__ (1-150行)  # KMeans++ 初始化可视化:种子选择过程演示
examples/cluster/plot_kmeans_silhouette_analysis.py
├── __main__ (1-150行)  # 轮廓分析选取聚类数:轮廓图与聚类可视化联动
examples/cluster/plot_kmeans_stability_low_dim_dense.py
├── __main__ (1-150行)  # KMeans 初始化稳定性评估:多次运行惯性统计与可视化
├── make_data (1-150行)  # 生成网格分布的各向同性高斯簇数据
examples/cluster/plot_mini_batch_kmeans.py
├── __main__ (1-150行)  # KMeans 与 MiniBatchKMeans 对比:训练时间、惯性与标签差异可视化
examples/cluster/plot_dbscan.py
├── __main__ (1-150行)  # DBSCAN 密度聚类演示:核心样本、噪声点与监督评估指标
examples/cluster/plot_optics.py
├── __main__ (1-150行)  # OPTICS 算法演示:可达度图、Xi 方法与 DBSCAN 等价切分
examples/cluster/plot_hdbscan.py
├── __main__ (1-150行)  # HDBSCAN 算法演示:尺度不变性、多尺度聚类与超参数鲁棒性
├── plot (1-150行)  # 通用聚类可视化函数:支持概率权重的散点绘制
examples/cluster/plot_agglomerative_clustering_metrics.py
├── __main__ (1-150行)  # 层次聚类度量对比:余弦/欧氏/城市块距离对波形聚类的影响
├── sqr (1-150行)  # 方波生成函数:用于构造合成波形数据
examples/cluster/plot_agglomerative_dendrogram.py
├── __main__ (1-150行)  # 层次聚类树状图绘制:基于 Iris 数据的完整树与截断展示
├── plot_dendrogram (1-150行)  # 将 AgglomerativeClustering 转换为 scipy linkage 矩阵并绘图
examples/cluster/plot_birch_vs_minibatchkmeans.py
├── __main__ (1-150行)  # BIRCH 与 MiniBatchKMeans 规模性能对比:全局聚类步骤影响
examples/cluster/plot_bisect_kmeans.py
├── __main__ (1-150行)  # BisectingKMeans 与 KMeans 结构对比:层次分裂产生更规则大尺度结构
examples/cluster/plot_linkage_comparison.py
├── __main__ (1-150行)  # 四种链接策略在玩具数据上的行为对比:单/平均/完全/沃德链接
examples/cluster/plot_cluster_comparison.py
├── __main__ (1-150行)  # 11 种聚类算法在 6 种玩具数据上的综合对比:参数调优与运行时间
examples/cluster/plot_affinity_propagation.py
├── __main__ (1-150行)  # 亲和传播聚类:消息传递机制与聚类中心自动确定
examples/cluster/plot_mean_shift.py
├── __main__ (1-150行)  # 均值漂移聚类:带宽自动估计与种子箱策略
examples/cluster/plot_coin_segmentation.py
├── __main__ (1-150行)  # 谱聚类图像分割:硬币图像的三种标签分配策略对比
examples/cluster/plot_coin_ward_segmentation.py
├── __main__ (1-150行)  # 结构化 Ward 层次聚类图像分割:空间连通约束下的区域分割
examples/cluster/plot_segmentation_toy.py
├── __main__ (1-150行)  # 谱聚类分离圆环:Voronoi 风格分区与掩码限制
examples/bicluster/plot_bicluster_newsgroups.py
├── __main__ (1-150行)  # 20 Newsgroups 双聚类:SpectralCoclustering 与 MiniBatchKMeans 对比
├── number_normalizer (1-150行)  # 数字标记归一化器:将数字 token 映射为 #NUMBER
├── NumberNormalizingVectorizer.build_tokenizer (1-150行)  # 自定义 TF-IDF 向量化器:集成数字归一化
├── bicluster_ncut (1-150行)  # 双聚类归一化割评价:基于文档-词矩阵的切割代价计算
examples/bicluster/plot_spectral_biclustering.py
├── __main__ (1-150行)  # 谱双聚类棋盘数据重构:行列重排后的双聚类可视化与共识得分
examples/bicluster/plot_spectral_coclustering.py
├── __main__ (1-150行)  # 谱共聚类植入模式恢复:共识得分评估重排矩阵的恢复质量
examples/cluster/plot_adjusted_for_chance_measures.py
├── random_labels (1-150行)  # 生成均匀随机标签
├── fixed_classes_uniform_labelings_scores (1-150行)  # 固定真实类别下随机预测标签的指标统计
├── uniform_labelings_scores (1-150行)  # 类别与聚类数匹配的双随机标签指标统计
examples/cluster/plot_face_compress.py
├── __main__ (1-150行)  # 向量量化图像压缩:KBinsDiscretizer 均匀/分位数/KMeans 策略对比
examples/cluster/plot_dict_face_patches.py
├── __main__ (1-150行)  # 在线字典学习:MiniBatchKMeans partial_fit 学习人脸图像块
examples/cluster/plot_digits_agglomeration.py
├── __main__ (1-150行)  # 特征团聚降维:手写数字图像的空间结构约束聚合与还原
examples/cluster/plot_digits_linkage.py
├── __main__ (1-150行)  # 数字数据 2D 嵌入上的四种链接策略:聚类大小不均现象观察
├── plot_clustering (1-150行)  # 聚类结果可视化:数字标记散点图与颜色映射
examples/cluster/plot_feature_agglomeration_vs_univariate_selection.py
├── __main__ (1-150行)  # 特征团聚 vs 单变量选择:贝叶斯回归下的降维效果对比
examples/cluster/plot_inductive_clustering.py
├── __main__ (1-150行)  # 归纳式聚类元估计器:聚类标签训练分类器实现新样本推断
├── _classifier_has (1-150行)  # 检查分类器是否拥有特定属性的描述符工厂
├── InductiveClusterer.__init__ (1-150行)  # 初始化聚类器与分类器
├── InductiveClusterer.fit (1-150行)  # 拟合聚类器获取伪标签并训练分类器
├── InductiveClusterer.predict (1-150行)  # 委托分类器预测新样本簇归属
├── InductiveClusterer.decision_function (1-150行)  # 委托分类器决策函数
├── plot_scatter (1-150行)  # 散点绘图辅助函数:支持透明度与边框
examples/cluster/plot_ward_structured_vs_unstructured.py
├── __main__ (1-150行)  # 结构化 vs 非结构化 Ward 聚类:瑞士卷与螺旋数据上的连通性约束影响

69.14 设计中的取舍

为什么采用当前方案而不是更复杂的替代方案? 本章实现优先保证与既有 API 的一致性、可维护性与运行效率。这意味着在少数极端场景下,调用者需要自行在灵活性、内存与速度之间做取舍,换取默认路径的清晰与稳定。

69.15 动手练习

69.15.1 复现一个应用案例并替换核心算法

选择 examples/applications/plot_face_recognition.pyplot_stock_market.py

  1. 完整运行原示例,理解数据流与评估指标

  2. 将核心估计器替换为同类算法(如 SVM→LogisticRegression、GBDT→RandomForest)

  3. 对比训练时间、预测精度、模型大小

回答问题:

  • 替换后为何精度提升/下降?是否涉及数据预处理不匹配?

  • 如何用 set_output(transform="pandas") 让中间特征可解释?

  • 若部署到生产,哪些步骤需封装为自定义 Transformer?

69.15.2 追踪某功能的版本演进历史

选定一个核心功能(如:HistGradientBoosting、元数据路由、TargetEncoder、Array API 兼容)

  1. examples/release_highlights/ 中找到该功能首次出现的版本

  2. 依次运行后续版本的同功能示例,记录 API 变化(参数名、默认值、返回值、新增方法)

  3. 总结该功能从实验性→稳定化→增强的完整演进路径

回答问题:

  • 哪个版本引入了破坏性变更?如何通过示例代码感知?

  • 实验性功能启用机制 (sklearn.experimental) 在哪个版本退役?

  • 如何利用版本演进示例编写版本兼容的工厂函数?

69.15.3 设计一个最小化自定义估计器示例

参考 examples/developing_estimators/sklearn_is_fitted.pyexamples/frozen/plot_frozen_examples.py

  1. 实现一个 MyTransformer 继承 BaseEstimator, TransformerMixin,实现 fit/transform/get_feature_names_out

  2. 实现一个 MyClassifier 继承 BaseEstimator, ClassifierMixin,支持 predict_probadecision_function

  3. 将两者组合进 Pipeline,验证 set_params 深层参数访问、HTML 表示、元数据路由

回答问题:

  • 必须实现哪些 _required_parameters 才能通过 check_estimator

  • 如何用 available_if 动态暴露 predict_proba(仅当底层支持时)?

  • FrozenEstimator 如何防止误调用 fit?其 __sklearn_tags__ 如何设置?

69.15.4 聚类算法假设失效诊断与替代方案实验

运行 examples/cluster/plot_kmeans_assumptions.py 并仔细观察四种失效场景

  1. 为每种场景(非最优 K、各向异性、方差不等、规模不均)解释 KMeans 为何失效

  2. 运行 examples/cluster/plot_kmeans_silhouette_analysis.py 理解轮廓分析如何辅助选 K

  3. 运行 examples/cluster/plot_cluster_comparison.py 对比 GaussianMixture、HDBSCAN、SpectralClustering 在同样数据上的表现

回答问题:

  • GaussianMixture 为何能处理各向异性与方差不等?其协方差类型参数如何选择?

  • HDBSCAN 如何在不指定 eps 的情况下自动适应多尺度密度?

  • SpectralClustering 的 affinity 參數(nearest_neighbors vs rbf)如何影响图构建?

69.15.5 聚类稳定性与初始化策略深度对比

运行 examples/cluster/plot_kmeans_stability_low_dim_dense.py 观察 n_init 对惯性方差的影响

  1. 记录 KMeans/MiniBatchKMeans 在 k-means++ 与 random 初始化下的均值/标准差曲线

  2. 运行 examples/cluster/plot_kmeans_digits.py 对比三种初始化在真实数据上的指标差异

  3. 修改 plot_kmeans_stability_low_dim_dense.py 增加 n_init=100 实验,观察收敛是否饱和

回答问题:

  • 为何 MiniBatchKMeans 随机初始化的方差随 n_init 下降更慢?

  • 在高维稀疏数据上,n_init='auto' 的启发式规则是什么?

  • 如何用 inertia_silhouette_score 联合判断是否陷入局部最优?

69.15.6 图像分割中的谱聚类与层次聚类实战

运行 examples/cluster/plot_coin_segmentation.pyplot_coin_ward_segmentation.py

  1. 对比三种 assign_labels(kmeans/discretize/cluster_qr)在分割边界平滑度上的差异

  2. 观察结构化 Ward 中 connectivity=grid_to_graph 如何强制区域连通

  3. 运行 examples/cluster/plot_segmentation_toy.py 理解 mask 如何限制分割范围

回答问题:

  • 为何谱聚类的 beta 参数控制'图像依赖程度'?beta→0 时退化为什么?

  • 结构化 Ward 的连通图若改为 kneighbors_graph(n_neighbors=4) 会怎样?

  • 如何用 cluster_qr 避免 kmeans 标签分配的随机性?

69.16 本章小结

本章围绕源码梳理了核心数据结构、调用流程与设计权衡。

以下是本章概念速查表:

| 概念 | 解释 |

|---|---|

| 示例图库分层组织 | applications(场景实战) / release_highlights(版本演进) / developing_estimators等开发工具 / 算法专题目录(cluster/classification等) |

| 即拿即用的完整流水线 | 每个示例覆盖 数据加载→预处理→建模→评估→可视化 全链路,可直接复制修改数据投入生产 |

| 版本演进活文档 | release_highlights 目录用可运行代码记录每个版本核心新特性,是 API 迁移学习的最佳教材 |

| 最佳实践模式提取 | Pipeline 组合、元数据路由、交叉验证嵌套、显示对象可视化、自定义估计器开发等工程化范式 |

| 跨领域迁移参考 | 图像/文本/时序/表格/图数据等不同模态数据的典型预处理与建模套路对比 |

| KMeans 假设与局限 | 球形簇、等方差、均衡规模;违背时退化,可用轮廓分析选 K、增大 n_init 或改用 GaussianMixture |

| 初始化策略影响 | k-means++ > random;PCA 初始化确定性强;MiniBatchKMeans 速度快但结果略异 |

| 密度聚类家族 | DBSCAN 需手动调 eps;OPTICS 给出可达度图可后切分;HDBSCAN 尺度不变、多尺度、超参语义更清晰 |

| 层次聚类链接策略 | Ward 最稳健抗噪;Single 快但怕噪;Average/Complete 适合球形簇;连通性约束能加速并改变合并行为 |

| 谱/双聚类 | 谱聚类解决非凸形状(图割近似);双聚类同时聚类行列(文档-词、基因-样本、图像块) |

| 聚类评估指标体系 | 外部指标:ARI/AMI 经机会校正为 0 基线;内部指标:轮廓系数无监督评估;非调整指标随簇数膨胀误导 |

| 归纳式聚类模式 | 聚类器产出伪标签 → 训练分类器 → 推断新样本;解决大规模/流式/一致性标注问题 |

| 向量量化与字典学习 | KBinsDiscretizer(kmeans 策略)做图像压缩;MiniBatchKMeans partial_fit 在线学习图像块字典 |

下一章将继续沿相关模块的调用链深入分析。

第 70 章 —— 集成学章 集成学习纵深 —— 模型集成的“联邦议会”

在本章我们围绕联邦议会的比喻贯穿始终,用完整段落描述集成学习的核心思想。想象一个联邦议会,由来自不同领域、背景各异的议员组成。每位议员代表一种相对简单、弱小的模型(如一棵小决策树、线性回归或样条回归)。他们在议会中共同制定决策,但发言权不是固定的,而是依据各自的表现动态调整。当某位议员错误率较高时,议会会削弱他的投票权重,使其对最终决策的影响减小;相反,表现优秀的议员会获得更大的权重,从而在后续讨论中拥有更大的发言空间。AdaBoost 类似于加权投票制:每轮新加入的议员专注于前几轮未能正确分类的难分样本,通过提升这些样本的权重来迫使后续议员更精细地纠正错误。Gradient Boosting 则像接力修正赛:每位新议员的任务是纠正前几轮累计的残差错误,学习率决定了修正步长。早停和 OOB 好比议会内部设立的自动叫停机制:当验证集(或未被抽中的样本)表现连续若干轮不再提升时,议会便会暂停继续引入新议员,以防止过度拟合。HistGradientBoosting 对分类特征的原生支持相当于免预处理直通车:类别特征无需 One-Hot 编码,分裂时直接依据目标统计量对类别进行划分,从而避免高基数特征导致的特征空间爆炸。随机森林 / ExtraTrees 可视为并行众包:每棵树都在自助抽样得到的子样本上独立训练,特征子采样进一步提升多样性;OOB 误差就像旁观者清的免费体检,帮助评估模型容量。将树的叶子索引做 One-Hot 编码并喂给线性模型,就像把高维稀疏哈希映射交给了线性模型进行二次组合,使得非线性特征被自动工程化。Stacking 如同两院制议会:下院(基学习器)各抒己见,上院(元学习器)学习如何加权裁决;Voting 则像联合执政,软投票通过概率加权实现,硬投票则采用少数服从多数原则。Isolation Forest 的随机分区过程可以看作随机分区猎手:异常点像稀有动物,少数随机砍树就能将其隔离(路径短),而正常样本需要更多砍树才能被隔离(路径长)。单约束则是业务知识的硬约束:强制模型只许涨不许跌(或相反),通过 PartialDependenceDisplay 验证约束后偏依赖曲线的平滑性,降低对噪声的敏感度。

想象一个联邦议会,由来自不同领域、背景各异的议员组成。每位议员代表一种相对简单、弱小的模型(如一棵小决策树、线性回归或样条回归)。他们在议会中共同制定决策,但发言权不是固定的,而是依据各自的表现动态调整。当某位议员错误率较高时,议会会削弱他的投票权重,使其对最终决策的影响减小;相反,表现优秀的议员会获得更大的权重,从而在后续讨论中拥有更大的发言空间。AdaBoost 类似于加权投票制:每轮新加入的议员专注于前几轮未能正确分类的难分样本,通过提升这些样本的权重来迫使后续议员更精细地纠正错误。Gradient Boosting 则像接力修正赛:每位新议员的任务是纠正前几轮累计的残差错误,学习率决定了修正步长。早停和 OOB 好比议会内部设立的自动叫停机制:当验证集(或未被抽中的样本)表现连续若干轮不再提升时,议会便会暂停继续引入新议员,以防止过度拟合。HistGradientBoosting 对分类特征的原生支持相当于免预处理直通车:类别特征无需 One-Hot 编码,分裂时直接依据目标统计量对类别进行划分,从而避免高基数特征导致的特征空间爆炸。随机森林 / ExtraTrees 可视为并行众包:每棵树都在自助抽样得到的子样本上独立训练,特征子采样进一步提升多样性;OOB 误差就像旁观者清的免费体检,帮助评估模型容量。将树的叶子索引做 One-Hot 编码并喂给线性模型,就像把高维稀疏哈希映射交给了线性模型进行二次组合,使得非线性特征被自动工程化。Stacking 如同两院制议会:下院(基学习器)各抒己见,上院(元学习器)学习如何加权裁决;Voting 则像联合执政,软投票通过概率加权实现,硬投票则采用少数服从多数原则。Isolation Forest 的随机分区过程可以看作随机分区猎手:异常点像稀有动物,少数随机砍树就能将其隔离(路径短),而正常样本需要更多砍树才能被隔离(路径长)。单约束则是业务知识的硬约束:强制模型只许涨不许跌(或相反),通过 PartialDependenceDisplay 验证约束后偏依赖曲线的平滑性,降低对噪声的敏感度。

70.1 学习目标

  • 难度:★★★☆☆(3/5)

  • 预备知识:Python 基础、面向对象编程与 Markdown/代码阅读基础

理解 AdaBoost 分类与回归算法的核心原理(SAMME、AdaBoost.R2)及弱学习器权重更新机制

掌握梯度提升树(GBT)的正则化策略:学习率、子采样、特征子采样对偏差‑方差的协同效应

理解梯度提升的早停机制、OOB 估计与分位数回归预测区间构建方法

掌握 HistGradientBoosting 原生分类特征支持、缺失值处理、单调约束等高级特性

理解随机森林与 ExtraTrees 的 OOB 误差监控、特征重要性双视角(MDI 与置换重要性)对比

掌握树集成特征变换技术:RandomTreesEmbedding、RF/GBDT 叶子索引 One‑Hot 编码喂入线性模型

理解堆叠集成与投票集成的异构模型融合机制、元学习器权重解读与 SuperLearner 约束近似

掌握 IsolationForest 异常检测的路径长度决策函数与双模式可视化,以及单约束梯度提升的领域知识注入与偏依赖验证

70.2 生活类比

想象一个联邦议会,由来自不同领域、背景各异的议员组成。每位议员代表一种相对简单、弱小的模型(如一棵小决策树、线性回归或样条回归)。他们在议会中共同制定决策,但发言权不是固定的,而是依据各自的表现动态调整。当某位议员错误率较高时,议会会削弱他的投票权重,使其对最终决策的影响减小;相反,表现优秀的议员会获得更大的权重,从而在后续讨论中拥有更大的发言空间。AdaBoost 类似于加权投票制:每轮新加入的议员专注于前几轮未能正确分类的难分样本,通过提升这些样本的权重来迫使后续议员更精细地纠正错误。Gradient Boosting 则像接力修正赛:每位新议员的任务是纠正前几轮累计的残差错误,学习率决定了修正步长。早停和 OOB 好比议会内部设立的自动叫停机制:当验证集(或未被抽中的样本)表现连续若干轮不再提升时,议会便会暂停继续引入新议员,以防止过度拟合。HistGradientBoosting 对分类特征的原生支持相当于免预处理直通车:类别特征无需 One-Hot 编码,分裂时直接依据目标统计量对类别进行划分,从而避免高基数特征导致的特征空间爆炸。随机森林 / ExtraTrees 可视为并行众包:每棵树都在自助抽样得到的子样本上独立训练,特征子采样进一步提升多样性;OOB 误差就像旁观者清的免费体检,帮助评估模型容量。将树的叶子索引做 One-Hot 编码并喂给线性模型,就像把高维稀疏哈希映射交给了线性模型进行二次组合,使得非线性特征被自动工程化。Stacking 如同两院制议会:下院(基学习器)各抒己见,上院(元学习器)学习如何加权裁决;Voting 则像联合执政,软投票通过概率加权实现,硬投票则采用少数服从多数原则。Isolation Forest 的随机分区过程可以看作随机分区猎手:异常点像稀有动物,少数随机砍树就能将其隔离(路径短),而正常样本需要更多砍树才能被隔离(路径长)。单约束则是业务知识的硬约束:强制模型只许涨不许跌(或相反),通过 PartialDependenceDisplay 验证约束后偏依赖曲线的平滑性,降低对噪声的敏感度。

70.3 源码地图

以下是本章对应的源码示例文件及其所在章节:

examples/ensemble/plot_adaboost_multiclass.py

  • AdaBoost 分类核心(SAMME)

examples/ensemble/plot_adaboost_regression.py

  • AdaBoost 回归核心(AdaBoost.R2)

examples/ensemble/plot_adaboost_twoclass.py

  • AdaBoost 二分类决策边界与分数分布

examples/ensemble/plot_gradient_boosting_categorical.py

  • 梯度提升分类特征原生支持(HistGradientBoosting)

examples/ensemble/plot_gradient_boosting_early_stopping.py

  • 梯度提升早停机制

examples/ensemble/plot_gradient_boosting_oob.py

  • 梯度提升 OOB 估计(Stochastic Gradient Boosting)

examples/ensemble/plot_gradient_boosting_quantile.py

  • 梯度提升分位数回归

examples/ensemble/plot_gradient_boosting_regression.py

  • 梯度提升基础回归示例

examples/ensemble/plot_gradient_boosting_regularization.py

  • 梯度提升正则化策略

examples/ensemble/plot_hgbt_regression.py

  • HistGradientBoosting 全面特性(早停、缺失值等)

examples/ensemble/plot_forest_hist_grad_boosting_comparison.py

  • 随机森林 vs HistGradientBoosting 基准

examples/ensemble/plot_forest_importances.py

  • 随机森林特征重要性双视角(MDI 与置换重要性)

examples/ensemble/plot_feature_transformation.py

  • 树集成特征变换:叶子索引 One‑Hot + 线性模型

examples/ensemble/plot_stack_predictors.py

  • 堆叠集成:多元学习器的协同增益

examples/ensemble/plot_voting_decision_regions.py

  • 投票集成:软投票、硬投票与阈值调节

examples/ensemble/plot_voting_regressor.py

  • 投票回归:简单平均的稳健基线

examples/ensemble/plot_isolation_forest.py

  • IsolationForest 异常检测:路径长度决策函数

examples/ensemble/plot_monotonic_constraints.py

  • 单约束:将业务规则注入 HGBT

70.4 AdaBoost 分类核心 —— 多类别 SAMME 算法与弱学习器权重博弈

以下代码演示了如何在多类别分类问题上使用 AdaBoost(SAMME 算法)提升弱学习器的表现。我们先构造一个十维高斯分布数据集,使其由三个同心球面分离的类别组成,随后划分训练集和测试集。使用决策树(最大叶子节点数为 8)作为弱学习器,训练 AdaBoost 分类器,并观察其在测试集上的误分类错误随迭代次数的变化。此外,我们还检查了每个弱学习器在重加权训练集上的错误率及其对应的模型权重,以了解 AdaBoost 是如何根据弱学习器的表现动态调整其在最终集成中的影响力的。

# 第 70 章 —— Authors: The scikit-learn developers
# 第 70 章 —— SPDX-License-Identifier: BSD-3-Clause

# 第 70 章 —— %%
# 第 70 章 —— Creating the dataset
# 第 70 章 —— --------------------
# 第 70 章 —— The classification dataset is constructed by taking a ten-dimensional standard
# 第 70 章 —— normal distribution (:math:`x` in :math:`R^{10}`) and defining three classes
# 第 70 章 —— separated by nested concentric ten-dimensional spheres such that roughly equal
# 第 70 章 —— numbers of samples are in each class (quantiles of the :math:`\chi^2`
# 第 70 章 —— distribution).
from sklearn.datasets import make_gaussian_quantiles

X, y = make_gaussian_quantiles(
    n_samples=2_000, n_features=10, n_classes=3, random_state=1
)

# 第 70 章 —— %%
# 第 70 章 —— We split the dataset into 2 sets: 70 percent of the samples are used for
# 第 70 章 —— training and the remaining 30 percent for testing.
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, train_size=0.7, random_state=42
)

# 第 70 章 —— %%
# 第 70 章 —— Training the `AdaBoostClassifier`
# 第 70 章 —— ---------------------------------
# 第 70 章 —— We train the :class:`~sklearn.ensemble.AdaBoostClassifier`. The estimator
# 第 70 章 —— utilizes boosting to improve the classification accuracy. Boosting is a method
# 第 70 章 —— designed to train weak learners (i.e. `estimator`) that learn from their
# 第 70 章 —— predecessor's mistakes.
#
# 第 70 章 —— Here, we define the weak learner as a
# 第 70 章 —— :class:`~sklearn.tree.DecisionTreeClassifier` and set the maximum number of
# 第 70 章 —— leaves to 8. In a real setting, this parameter should be tuned. We set it to a
# 第 70 章 —— rather low value to limit the runtime of the example.
#
# 第 70 章 —— The `SAMME` algorithm build into the
# 第 70 章 —— :class:`~sklearn.ensemble.AdaBoostClassifier` then uses the correct or
# 第 70 章 —— incorrect predictions made be the current weak learner to update the sample
# 第 70 章 —— weights used for training the consecutive weak learners. Also, the weight of
# 第 70 章 —— the weak learner itself is calculated based on its accuracy in classifying the
# 第 70 章 —— training examples. The weight of the weak learner determines its influence on
# 第 70 章 —— the final ensemble prediction.
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier

weak_learner = DecisionTreeClassifier(max_leaf_nodes=8)
n_estimators = 300

adaboost_clf = AdaBoostClassifier(
    estimator=weak_learner,
    n_estimators=n_estimators,
    random_state=42,
).fit(X_train, y_train)

# 第 70 章 —— %%
# 第 70 章 —— Analysis
# 第 70 章 —— --------
# 第 70 章 —— Convergence of the `AdaBoostClassifier`
# 第 70 章 —— ***************************************
# 第 70 章 —— To demonstrate the effectiveness of boosting in improving accuracy, we
# 第 70 章 —— evaluate the misclassification error of the boosted trees in comparison to two
# 第 70 章 —— baseline scores. The first baseline score is the `misclassification_error`
# 第 70 章 —— obtained from a single weak-learner (i.e.
# 第 70 章 —— :class:`~sklearn.tree.DecisionTreeClassifier`), which serves as a reference
# 第 70 章 —— point. The second baseline score is obtained from the
# 第 70 章 —— :class:`~sklearn.dummy.DummyClassifier`, which predicts the most prevalent
# 第 70 章 —— class in a dataset.
from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score

dummy_clf = DummyClassifier()


def misclassification_error(y_true, y_pred):
    return 1 - accuracy_score(y_true, y_pred)


weak_learners_misclassification_error = misclassification_error(
    y_test, weak_learner.fit(X_train, y_train).predict(X_test)
)

dummy_classifiers_misclassification_error = misclassification_error(
    y_test, dummy_clf.fit(X_train, y_train).predict(X_test)
)

print(
    "DecisionTreeClassifier's misclassification_error: "
    f"{weak_learners_misclassification_error:.3f}"
)
print(
    "DummyClassifier's misclassification_error: "
    f"{dummy_classifiers_misclassification_error:.3f}"
)

# 第 70 章 —— %%
# 第 70 章 —— After training the :class:`~sklearn.tree.DecisionTreeClassifier` model, the
# 第 70 章 —— achieved error surpasses the expected value that would have been obtained by
# 第 70 章 —— guessing the most frequent class label, as the
# 第 70 章 —— :class:`~sklearn.dummy.DummyClassifier` does.
#
# 第 70 章 —— Now, we calculate the `misclassification_error`, i.e. `1 - accuracy`, of the
# 第 70 章 —— additive model (:class:`~sklearn.tree.DecisionTreeClassifier`) at each
# 第 70 章 —— boosting iteration on the test set to assess its performance.
#
# 第 70 章 —— We use :meth:`~sklearn.ensemble.AdaBoostClassifier.staged_predict` that makes
# 第 70 章 —— as many iterations as the number of fitted estimator (i.e. corresponding to
# 第 70 章 —— `n_estimators`). At iteration `n`, the predictions of AdaBoost only use the
# 第 70 章 —— `n` first weak learners. We compare these predictions with the true
# 第 70 章 —— predictions `y_test` and we, therefore, conclude on the benefit (or not) of adding a
# 第 70 章 —— new weak learner into the chain.
#
# 第 70 章 —— We plot the misclassification error for the different stages:
import matplotlib.pyplot as plt
import pandas as pd

boosting_errors = pd.DataFrame(
    {
        "Number of trees": range(1, n_estimators + 1),
        "AdaBoost": [
            misclassification_error(y_test, y_pred)
            for y_pred in adaboost_clf.staged_predict(X_test)
        ],
    }
).set_index("Number of trees")
ax = boosting_errors.plot()
ax.set_ylabel("Misclassification error on test set")
ax.set_title("Convergence of AdaBoost algorithm")

plt.plot(
    [boosting_errors.index.min(), boosting_errors.index.max()],
    [weak_learners_misclassification_error, weak_learners_misclassification_error],
    color="tab:orange",
    linestyle="dashed",
)
plt.plot(
    [boosting_errors.index.min(), boosting_errors.index.max()],
    [
        dummy_classifiers_misclassification_error,
        dummy_classifiers_misclassification_error,
    ],
    color="c",
    linestyle="dotted",
)
plt.legend(["AdaBoost", "DecisionTreeClassifier", "DummyClassifier"], loc=1)
plt.show()

# 第 70 章 —— %%
# 第 70 章 —— The plot shows the missclassification error on the test set after each
# 第 70 章 —— boosting iteration. We see that the error of the boosted trees converges to an
# 第 70 章 —— error of around 0.3 after 50 iterations, indicating a significantly higher
# 第 70 章 —— accuracy compared to a single tree, as illustrated by the dashed line in the
# 第 70 章 —— plot.
#
# 第 70 章 —— The misclassification error jitters because the `SAMME` algorithm uses the
# 第 70 章 —— discrete outputs of the weak learners to train the boosted model.
#
# 第 70 章 —— The convergence of :class:`~sklearn.ensemble.AdaBoostClassifier` is mainly
# 第 70 章 —— influenced by the learning rate (i.e. `learning_rate`), the number of weak
# 第 70 章 —— learners used (`n_estimators`), and the expressivity of the weak learners
# 第 70 章 —— (e.g. `max_leaf_nodes`).

# 第 70 章 —— %%
# 第 70 章 —— Errors and weights of the Weak Learners
# 第 70 章 —— ***************************************
# 第 70 章 —— As previously mentioned, AdaBoost is a forward stagewise additive model. We
# 第 70 章 —— now focus on understanding the relationship between the attributed weights of
# 第 70 章 —— the weak learners and their statistical performance.
#
# 第 70 章 —— We use the fitted :class:`~sklearn.ensemble.AdaBoostClassifier`'s attributes
# 第 70 章 —— `estimator_errors_` and `estimator_weights_` to investigate this link.
weak_learners_info = pd.DataFrame(
    {
        "Number of trees": range(1, n_estimators + 1),
        "Errors": adaboost_clf.estimator_errors_,
        "Weights": adaboost_clf.estimator_weights_,
    }
).set_index("Number of trees")

axs = weak_learners_info.plot(
    subplots=True, layout=(1, 2), figsize=(10, 4), legend=False, color="tab:blue"
)
axs[0, 0].set_ylabel("Train error")
axs[0, 0].set_title("Weak learner's training error")
axs[0, 1].set_ylabel("Weight")
axs[0, 1].set_title("Weak learner's weight")
fig = axs[0, 0].get_figure()
fig.suptitle("Weak learner's errors and weights for the AdaBoostClassifier")
fig.tight_layout()

# 第 70 章 —— %%
# 第 70 章 —— On the left plot, we show the weighted error of each weak learner on the
# 第 70 章 —— reweighted training set at each boosting iteration. On the right plot, we show
# 第 70 章 —— the weights associated with each weak learner later used to make the
# 第 70 章 —— predictions of the final additive model.
#
# 第 70 章 —— We see that the error of the weak learner is the inverse of the weights. It
# 第 70 章 —— means that our additive model will trust more a weak learner that makes
# 第 70 章 —— smaller errors (on the training set) by increasing its impact on the final
# 第 70 章 —— decision. Indeed, this exactly is the formulation of updating the base
# 第 70 章 —— estimators' weights after each iteration in AdaBoost.
#
# 第 70 章 —— .. dropdown:: Mathematical details
#
# 第 70 章 —— The weight associated with a weak learner trained at the stage :math:`m` is
# 第 70 章 —— inversely associated with its misclassification error such that:
#
# 第 70 章 —— .. math:: \alpha^{(m)} = \log \frac{1 - err^{(m)}}{err^{(m)}} + \log (K - 1),
#
# 第 70 章 —— where :math:`\alpha^{(m)}` and :math:`err^{(m)}` are the weight and the error
# 第 70 章 —— of the :math:`m` th weak learner, respectively, and :math:`K` is the number of
# 第 70 章 —— classes in our classification problem.
#
# 第 70 章 —— Another interesting observation boils down to the fact that the first weak
# 第 70 章 —— learners of the model make fewer errors than later weak learners of the
# 第 70 章 —— boosting chain.
#
# 第 70 章 —— The intuition behind this observation is the following: due to the sample
# 第 70 章 —— reweighting, later classifiers are forced to try to classify more difficult or
# 第 70 章 —— noisy samples and to ignore already well classified samples. Therefore, the
# 第 70 章 —— overall error on the training set will increase. That's why the weak learner's
# 第 70 章 —— weights are built to counter-balance the worse performing weak learners.

代码作用:以上代码完整展示了 AdaBoost 在多类别分类上的工作机制。它首先生成符合预设分布的数据集,接着训练基于决策树的弱学习器的 AdaBoost 集成模型。通过绘制误分类错误随迭代的变化曲线,我们可以看到模型性能如何随着弱学习器数量的增加而提升并趋于稳定。此外,通过查看每个弱学习器的训练错误及其对应的模型权重,我们能够直观理解 AdaBoost 是如何根据弱学习器的表现动态调整其在最终集成中的影响力的——错误越小的弱学习器获得越高的权重,从而在最终预测中发挥更大作用。这验证了 AdaBoost 的核心思想:通过加权投票和样本重加权,使得后续模型专注于前一轮模型误分类的样本,从而逐步改进集合的整体表现。

流程图

flowchart TD A[生成高斯分布数据] --> B[划分训练/测试集] B --> C[训练 AdaBoost 分类器] C --> D[计算基线误差] D --> E[绘制误分类错误收敛曲线] E --> F[分析弱学习器错误率和权重]

架构图

flowchart LR A[原始特征 X] --> B[弱学习器 DecisionTreeClassifier] B --> C[AdaBoost 迭代更新样本权重] C --> D[弱学习器训练错误 estimator_errors_] D --> E[弱学习器模型权重 estimator_weights_] E --> F[最终集成预测]

设计取舍

问:为什么在 AdaBoost 中使用决策树桩(max_depth=1)作为弱学习器而不是更深的树?

答:决策树桩作为弱学习器能够保持模型的简洁性,避免单个学习器过于强导致 boosting 过程中的样本权重更新失效。较深的树可能在早期就达到较低训练误差,削弱后续迭代的效果,而浅树确保每轮都聚焦于难以分类的样本,从而逐步提升集合性能。

问:SAMME 算法中弱学习器的权重公式为何包含 log(K-1) 项?

答:在多分类设置中,log(K-1) 项用于校正随机猜测的基线。当只有两个类别时,该项为 log(1) = 0,退化为经典 AdaBoost;当类别增加时,该项确保即使弱学习器略优于随机猜测也能获得正权重,防止权重因类别数增加而被稀释。

70.5 AdaBoost 回归核心 —— AdaBoost.R2 对正弦波的拟合

以下代码演示了 AdaBoost 回归(AdaBoost.R2)如何在一维正弦波数据上迭代改进预测。我们首先生成包含两个正弦项及高斯噪声的合成数据,然后分别使用单棵决策树回归器和基于同样决策树的 AdaBoost 回归器进行拟合。最后,我们将它们的预测结果在同一图中可视化,以直观展示随着提升轮数的增加,模型如何逐步捕捉数据中的细节。

# 第 70 章 —— %%
# 第 70 章 —— Preparing the data
# 第 70 章 —— ------------------
# 第 70 章 —— First, we prepare dummy data with a sinusoidal relationship and some gaussian noise.

# 第 70 章 —— Authors: The scikit-learn developers
# 第 70 章 —— SPDX-License-Identifier: BSD-3-Clause

import numpy as np

rng = np.random.RandomState(1)
X = np.linspace(0, 6, 100)[:, np.newaxis]
y = np.sin(X).ravel() + np.sin(6 * X).ravel() + rng.normal(0, 0.1, X.shape[0])

# 第 70 章 —— %%
# 第 70 章 —— Training and prediction with DecisionTree and AdaBoost Regressors
# 第 70 章 —— -----------------------------------------------------------------
# 第 70 章 —— Now, we define the classifiers and fit them to the data.
# 第 70 章 —— Then we predict on that same data to see how well they could fit it.
# 第 70 章 —— The first regressor is a `DecisionTreeRegressor` with `max_depth=4`.
# 第 70 章 —— The second regressor is an `AdaBoostRegressor` with a `DecisionTreeRegressor`
# 第 70 章 —— of `max_depth=4` as base learner and will be built with `n_estimators=300`
# 第 70 章 —— of those base learners.

from sklearn.ensemble import AdaBoostRegressor
from sklearn.tree import DecisionTreeRegressor

regr_1 = DecisionTreeRegressor(max_depth=4)

regr_2 = AdaBoostRegressor(
    DecisionTreeRegressor(max_depth=4), n_estimators=300, random_state=rng
)

regr_1.fit(X, y)
regr_2.fit(X, y)

y_1 = regr_1.predict(X)
y_2 = regr_2.predict(X)

# 第 70 章 —— %%
# 第 70 章 —— Plotting the results
# 第 70 章 —— --------------------
# 第 70 章 —— Finally, we plot how well our two regressors,
# 第 70 章 —— single decision tree regressor and AdaBoost regressor, could fit the data.

import matplotlib.pyplot as plt
import seaborn as sns

colors = sns.color_palette("colorblind")

plt.figure()
plt.scatter(X, y, color=colors[0], label="training samples")
plt.plot(X, y_1, color=colors[1], label="n_estimators=1", linewidth=2)
plt.plot(X, y_2, color=colors[2], label="n_estimators=300", linewidth=2)
plt.xlabel("data")
plt.ylabel("target")
plt.title("Boosted Decision Tree Regression")
plt.legend()
plt.show()

代码作用:此段代码展示了 AdaBoost 回归(AdaBoost.R2)在函数逼近任务中的表现。通过将单棵决策树的预测与经过 300 次提升的 AdaBoost 回归器的预测进行对比,可以明显看到后者在捕捉数据中的复杂波形方面表现得更好。单棵树由于其结构简单,只能学习到数据的粗略趋势,而 AdaBoost 通过迭代地在之前模型的残差上训练新的弱学习器,逐步弥补误差,最终产生更为平滑且贴合真实函数的预测曲线。这验证了 boosting 的核心思想:通过线性组合很多弱学习器,每个学习器专注于修正前者的错误,从而构建出强大的预测模型。

流程图

flowchart TD A[生成正弦波数据] --> B[训练单棵决策树回归器] A --> C[训练 AdaBoost 回归器] B --> D[单棵树预测] C --> E[AdaBoost 预测] D --> F[结果可视化] E --> F

架构图

flowchart LR A[原始特征 X] --> B[弱学习器 DecisionTreeRegressor] B --> C[AdaBoost 迭代训练弱学习器] C --> D[残差计算] D --> E[弱学习器权重更新] E --> F[最终集成预测]

设计取舍

问:为什么在 AdaBoost 回归中使用决策树作为弱学习器?

答:决策树作为弱学习器能够捕捉非线性关系,且其结构简单便于在 boosting 框架中进行快速迭代。单棵树虽然表达能力有限,但通过加权组合多棵树,AdaBoost 能够逐步逼近复杂函数,同时避免单个模型过拟合。

问:AdaBoost.R2 如何处理回归中的样本权重更新?

答:AdaBoost.R2 基于每个样本的相对误差(即实际值与预测值的差异除以当前最大误差)来更新样本权重,误差大的样本获得更高权重,从而迫使后续弱学习器更关注难以预测的样本。

70.6 AdaBoost 二分类决策边界与分数分布

接下来的例子展示了 AdaBoost 在二分类任务中的决策边界和分数分布。我们构造了一个由两个高斯 quantiles 聚类组成的非线性可分数据集,训练一个由决策树桩(max_depth=1)构成的 AdaBoost 分类器,并可视化其决策边界以及两类样本的决策得分分布。这有助于理解 AdaBoost 如何通过线性组合弱学习器的输出来形成最终的分类决策,以及得分的符号和大小如何对应类别预测和置信度。

# 第 70 章 —— Authors: The scikit-learn developers
# 第 70 章 —— SPDX-License-Identifier: BSD-3-Clause

import matplotlib.pyplot as plt
import numpy as np

from sklearn.datasets import make_gaussian_quantiles
from sklearn.ensemble import AdaBoostClassifier
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.tree import DecisionTreeClassifier

# 第 70 章 —— Construct dataset
X1, y1 = make_gaussian_quantiles(
    cov=2.0, n_samples=200, n_features=2, n_classes=2, random_state=1
)
X2, y2 = make_gaussian_quantiles(
    mean=(3, 3), cov=1.5, n_samples=300, n_features=2, n_classes=2, random_state=1
)
X = np.concatenate((X1, X2))
y = np.concatenate((y1, -y2 + 1))

# 第 70 章 —— Create and fit an AdaBoosted decision tree
bdt = AdaBoostClassifier(DecisionTreeClassifier(max_depth=1), n_estimators=200)
bdt.fit(X, y)

plot_colors = "br"
plot_step = 0.02
class_names = "AB"

plt.figure(figsize=(10, 5))

# 第 70 章 —— Plot the decision boundaries
ax = plt.subplot(121)
disp = DecisionBoundaryDisplay.from_estimator(
    bdt,
    X,
    cmap=plt.cm.Paired,
    response_method="predict",
    ax=ax,
    xlabel="x",
    ylabel="y",
)
x_min, x_max = disp.xx0.min(), disp.xx0.max()
y_min, y_max = disp.xx1.min(), disp.xx1.max()
plt.axis("tight")

# 第 70 章 —— Plot the training points
for i, n, c in zip(range(2), class_names, plot_colors):
    idx = (y == i).nonzero()
    plt.scatter(
        X[idx, 0],
        X[idx, 1],
        c=c,
        s=20,
        edgecolor="k",
        label="Class %s" % n,
    )
plt.xlim(x_min, x_max)
plt.ylim(y_min, y_max)
plt.legend(loc="upper right")

plt.title("Decision Boundary")

# 第 70 章 —— Plot the two-class decision scores
twoclass_output = bdt.decision_function(X)
plot_range = (twoclass_output.min(), twoclass_output.max())
plt.subplot(122)
for i, n, c in zip(range(2), class_names, plot_colors):
    plt.hist(
        twoclass_output[y == i],
        bins=10,
        range=plot_range,
        facecolor=c,
        label="Class %s" % n,
        alpha=0.5,
        edgecolor="k",
    )
x1, x2, y1, y2 = plt.axis()
plt.axis((x1, x2, y1, y2 * 1.2))
plt.legend(loc="upper right")
plt.ylabel("Samples")
plt.xlabel("Score")
plt.title("Decision Scores")

plt.tight_layout()
plt.subplots_adjust(wspace=0.35)
plt.show()

代码作用:此代码展示了 AdaBoost 在二分类问题上的表现。左图显示了学习到的决策边界:尽管基学习器仅是决策树桩(单次分割),但通过提升,最终的集成模型能够逼近数据的真实分布。右图展示了两类样本的决策得分分布:得分大于零的样本被预测为类别 B,小于零的为类别 A;得分的绝对值衡量了模型对预测的信心。可以看出,AdaBoost 成功地将两类分开,且得分分布之间存在明显间隔,说明模型不仅实现了分类,还提供了具有实际意义的置信度分数。

流程图

flowchart TD A[生成二分类高斯数据] --> B[训练 AdaBoost 分类器] B --> C[绘制决策边界] B --> D[计算决策函数输出] D --> E[绘制决策得分分布直方图]

架构图

flowchart LR A[原始特征 X] --> B[弱学习器 DecisionTreeClassifier(max_depth=1)] B --> C[AdaBoost 迭代更新样本权重] C --> D[弱学习器加权投票] D --> E[最终决策函数输出] E --> F[决策边界可视化] E --> G[决策得分分布直方图]

设计取舍

问:为什么在二分类 AdaBoost 中使用决策树桩(max_depth=1)作为弱学习器?

答:决策树桩作为弱学习器能够保持模型的高偏低方差特征,使得 boosting 过程中的样本权重更新更具意义。若使用更深的树,早期模型可能就能达到较低训练误差,导致后续迭代聚焦于噪声样本而非真正难分的样本,削弱 boosting 的纠错效果。

问:决策得分的符号如何决定类别预测?

答:在二分类 AdaBoost 中,决策函数的符号直接决定类别:正值预测为类别 B,负值预测为类别 A。得分的绝对值越大,表示模型对该预测越有信心,因为它反映了多个弱学习器在这一方向上的累积一致投票。

70.7 梯度提升分类特征原生支持 —— HistGradientBoosting 的类别处理

以下代码比较了不同编码策略对 Ames Iowa Housing 数据集上梯度提升模型的影响。我们评估了五种处理类别特征的方法:删除、独热编码、序数编码、目标编码以及 HistGradientBoostingRegressor 的原生类别支持。通过交叉验证比较它们的平均绝对百分比误差和训练时间,我们可以看到原生类别支持在保持竞争力预测性能的同时,显著减少了 preprocessing 开销,尤其在基数较高时表现更优。这验证了 HistGradientBoosting 在处理类别特征时的效率与效果优势。

# 第 70 章 —— Authors: The scikit-learn developers
# 第 70 章 —— SPDX-License-Identifier: BSD-3-Clause

# 第 70 章 —— %%
# 第 70 章 —— Load Ames Housing dataset
# 第 70 章 —— -------------------------
# 第 70 章 —— First, we load the Ames Housing data as a pandas dataframe. The features
# 第 70 章 —— are either categorical or numerical:
from sklearn.datasets import fetch_openml

X, y = fetch_openml(data_id=42165, as_frame=True, return_X_y=True)

# 第 70 章 —— Select only a subset of features of X to make the example faster to run
categorical_columns_subset = [
    "BldgType",
    "GarageFinish",
    "LotConfig",
    "Functional",
    "MasVnrType",
    "HouseStyle",
    "FireplaceQu",
    "ExterCond",
    "ExterQual",
    "PoolQC",
]

numerical_columns_subset = [
    "3SsnPorch",
    "Fireplaces",
    "BsmtHalfBath",
    "HalfBath",
    "GarageCars",
    "TotRmsAbvGrd",
    "BsmtFinSF1",
    "BsmtFinSF2",
    "GrLivArea",
    "ScreenPorch",
]

X = X[categorical_columns_subset + numerical_columns_subset]
X[categorical_columns_subset] = X[categorical_columns_subset].astype("category")

categorical_columns = X.select_dtypes(include="category").columns
n_categorical_features = len(categorical_columns)
n_numerical_features = X.select_dtypes(include="number").shape[1]

print(f"Number of samples: {X.shape[0]}")
print(f"Number of features: {X.shape[1]}")
print(f"Number of categorical features: {n_categorical_features}")
print(f"Number of numerical features: {n_numerical_features}")

# 第 70 章 —— %%
# 第 70 章 —— Gradient boosting estimator with dropped categorical features
# 第 70 章 —— -------------------------------------------------------------
# 第 70 章 —— As a baseline, we create an estimator where the categorical features are
# 第 70 章 —— dropped:

from sklearn.compose import make_column_selector, make_column_transformer
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.pipeline import make_pipeline

dropper = make_column_transformer(
    ("drop", make_column_selector(dtype_include="category")), remainder="passthrough"
)
hist_dropped = make_pipeline(dropper, HistGradientBoostingRegressor(random_state=42))
hist_dropped

# 第 70 章 —— %%
# 第 70 章 —— Gradient boosting estimator with one-hot encoding
# 第 70 章 —— -------------------------------------------------
# 第 70 章 —— Next, we create a pipeline to one-hot encode the categorical features,
# 第 70 章 —— while letting the remaining features `"passthrough"` unchanged:

from sklearn.preprocessing import OneHotEncoder

one_hot_encoder = make_column_transformer(
    (
        OneHotEncoder(sparse_output=False, handle_unknown="ignore"),
        make_column_selector(dtype_include="category"),
    ),
    remainder="passthrough",
)

hist_one_hot = make_pipeline(
    one_hot_encoder, HistGradientBoostingRegressor(random_state=42)
)
hist_one_hot

# 第 70 章 —— %%
# 第 70 章 —— Gradient boosting estimator with ordinal encoding
# 第 70 章 —— -------------------------------------------------
# 第 70 章 —— Next, we create a pipeline that treats categorical features as ordered
# 第 70 章 —— quantities, i.e. the categories are encoded as 0, 1, 2, etc., and treated as
# 第 70 章 —— continuous features.

import numpy as np

from sklearn.preprocessing import OrdinalEncoder

ordinal_encoder = make_column_transformer(
    (
        OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=np.nan),
        make_column_selector(dtype_include="category"),
    ),
    remainder="passthrough",
)

hist_ordinal = make_pipeline(
    ordinal_encoder, HistGradientBoostingRegressor(random_state=42)
)
hist_ordinal

# 第 70 章 —— %%
# 第 70 章 —— Gradient boosting estimator with target encoding
# 第 70 章 —— ------------------------------------------------
# 第 70 章 —— Another possibility is to use the :class:`~preprocessing.TargetEncoder`, which
# 第 70 章 —— encodes the categories computed from the mean of the (training) target
# 第 70 章 —— variable, as computed using a smoothed `np.mean(y, axis=0)` i.e.:
#
# 第 70 章 —— - in regression it uses the mean of `y`;
# 第 70 章 —— - in binary classification, the positive-class rate;
# 第 70 章 —— - in multiclass, a vector of class rates (one per class).
#
# 第 70 章 —— For each category, it computes these target averages using :term:`cross
# 第 70 章 —— fitting`, meaning that the training data are split into folds: in each fold
# 第 70 章 —— the averages are calculated only on a subset of data and then applied to the
# 第 70 章 —— held-out part. This way, each sample is encoded using statistics from data it
# 第 70 章 —— was not part of, preventing information leakage from the target.

from sklearn.preprocessing import TargetEncoder

target_encoder = make_column_transformer(
    (
        TargetEncoder(target_type="continuous", random_state=42),
        make_column_selector(dtype_include="category"),
    ),
    remainder="passthrough",
)

hist_target = make_pipeline(
    target_encoder, HistGradientBoostingRegressor(random_state=42)
)
hist_target

# 第 70 章 —— %%
# 第 70 章 —— Gradient boosting estimator with native categorical support
# 第 70 章 —— -----------------------------------------------------------
# 第 70 章 —— We now create a :class:`~ensemble.HistGradientBoostingRegressor` estimator
# 第 70 章 —— that can natively handle categorical features without explicit encoding. Such
# 第 70 章 —— functionality can be enabled by setting `categorical_features="from_dtype"`,
# 第 70 章 —— which automatically detects features with categorical dtypes, or more explicitly
# 第 70 章 —— by `categorical_features=categorical_columns_subset`.
#
# 第 70 章 —— Unlike previous encoding approaches, the estimator natively deals with the
# 第 70 章 —— categorical features. At each split, it partitions the categories of such a
# 第 70 章 —— feature into disjoint sets using a heuristic that sorts them by their effect
# 第 70 章 —— on the target variable, see `Split finding with categorical features
# 第 70 章 —— <https://scikit-learn.org/stable/modules/ensemble.html#split-finding-with-categorical-features>`_
# 第 70 章 —— for details.
#
# 第 70 章 —— While ordinal encoding may work well for low-cardinality features even if
# 第 70 章 —— categories have no natural order, reaching meaningful splits requires deeper
# 第 70 章 —— trees as the cardinality increases. The native categorical support avoids this
# 第 70 章 —— by directly working with unordered categories. The advantage over one-hot
# 第 70 章 —— encoding is the omitted preprocessing and faster fit and predict time.

hist_native = HistGradientBoostingRegressor(
    random_state=42, categorical_features="from_dtype"
)
hist_native

# 第 70 章 —— %%
# 第 70 章 —— Model comparison
# 第 70 章 —— ----------------
# 第 70 章 —— Here we use :term:`cross validation` to compare the models performance in
# 第 70 章 —— terms of :func:`~metrics.mean_absolute_percentage_error` and fit times. In the
# 第 70 章 —— upcoming plots, error bars represent 1 standard deviation as computed across
# 第 70 章 —— cross-validation splits.

from sklearn.model_selection import cross_validate

common_params = {"cv": 5, "scoring": "neg_mean_absolute_percentage_error", "n_jobs": -1}

dropped_result = cross_validate(hist_dropped, X, y, **common_params)
one_hot_result = cross_validate(hist_one_hot, X, y, **common_params)
ordinal_result = cross_validate(hist_ordinal, X, y, **common_params)
target_result = cross_validate(hist_target, X, y, **common_params)
native_result = cross_validate(hist_native, X, y, **common_params)
results = [
    ("Dropped", dropped_result),
    ("One Hot", one_hot_result),
    ("Ordinal", ordinal_result),
    ("Target", target_result),
    ("Native", native_result),
]

# 第 70 章 —— %%
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker


def plot_performance_tradeoff(results, title):
    fig, ax = plt.subplots()
    markers = ["s", "o", "^", "x", "D"]

    for idx, (name, result) in enumerate(results):
        test_error = -result["test_score"]
        mean_fit_time = np.mean(result["fit_time"])
        mean_score = np.mean(test_error)
        std_fit_time = np.std(result["fit_time"])
        std_score = np.std(test_error)

        ax.scatter(
            result["fit_time"],
            test_error,
            label=name,
            marker=markers[idx],
        )
        ax.scatter(
            mean_fit_time,
            mean_score,
            color="k",
            marker=markers[idx],
        )
        ax.errorbar(
            x=mean_fit_time,
            y=mean_score,
            yerr=std_score,
            c="k",
            capsize=2,
        )
        ax.errorbar(
            x=mean_fit_time,
            y=mean_score,
            xerr=std_fit_time,
            c="k",
            capsize=2,
        )

    ax.set_xscale("log")

    nticks = 7
    x0, x1 = np.log10(ax.get_xlim())
    ticks = np.logspace(x0, x1, nticks)
    ax.set_xticks(ticks)
    ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%1.1e"))
    ax.minorticks_off()

    ax.annotate(
        "  best\nmodels",
        xy=(0.04, 0.04),
        xycoords="axes fraction",
        xytext=(0.09, 0.14),
        textcoords="axes fraction",
        arrowprops=dict(arrowstyle="->", lw=1.5),
    )
    ax.set_xlabel("Time to fit (seconds)")
    ax.set_ylabel("Mean Absolute Percentage Error")
    ax.set_title(title)
    ax.legend()
    plt.show()


plot_performance_tradeoff(results, "Gradient Boosting on Ames Housing")

# 第 70 章 —— %%
# 第 70 章 —— In the plot above, the "best models" are those that are closer to the
# 第 70 章 —— down-left corner, as indicated by the arrow. Those models would indeed
# 第 70 章 —— correspond to faster fitting and lower error.
#
# 第 70 章 —— The model using one-hot encoded data is the slowest. This is to be expected,
# 第 70 章 —— as one-hot encoding creates an additional feature for each category value of
# 第 70 章 —— every categorical feature, greatly increasing the number of split candidates
# 第 70 章 —— during training. In theory, we expect the native handling of categorical
# 第 70 章 —— features to be slightly slower than treating categories as ordered quantities
# 第 70 章 —— ('Ordinal'), since native handling requires :ref:`sorting categories
# 第 70 章 —— <categorical_support_gbdt>`. Fitting times should however be close when the
# 第 70 章 —— number of categories is small, and this may not always be reflected in
# 第 70 章 —— practice.
#
# 第 70 章 —— The time required to fit when using the `TargetEncoder` depends on the
# 第 70 章 —— cross fitting parameter `cv`, as adding splits come at a computational cost.
#
# 第 70 章 —— In terms of prediction performance, dropping the categorical features leads to
# 第 70 章 —— the worst performance. The four models that make use of the categorical
# 第 70 章 —— features have comparable error rates, with a slight edge for the native
# 第 70 章 —— handling.

# 第 70 章 —— %%
# 第 70 章 —— Limiting the number of splits
# 第 70 章 —— -----------------------------
# 第 70 章 —— In general, one can expect poorer predictions from one-hot-encoded data,
# 第 70 章 —— especially when the tree depths or the number of nodes are limited: with
# 第 70 章 —— one-hot-encoded data, one needs more split points, i.e. more depth, in order
# 第 70 章 —— to recover an equivalent split that could be obtained in one single split
# 第 70 章 —— point with native handling.
#
# 第 70 章 —— This is also true when categories are treated as ordinal quantities: if
# 第 70 章 —— categories are `A..F` and the best split is `ACF - BDE` the one-hot-encoder
# 第 70 章 —— model would need 3 split points (one per category in the left node), and the
# 第 70 章 —— ordinal non-native model would need 4 splits: 1 split to isolate `A`, 1 split
# 第 70 章 —— to isolate `F`, and 2 splits to isolate `C` from `BCDE`.
#
# 第 70 章 —— How strongly the models' performances differ in practice depends on the
# 第 70 章 —— dataset and on the flexibility of the trees.
#
# 第 70 章 —— To see this, let us re-run the same analysis with under-fitting models where
# 第 70 章 —— we artificially limit the total number of splits by both limiting the number
# 第 70 章 —— of trees and the depth of each tree.

for pipe in (hist_dropped, hist_one_hot, hist_ordinal, hist_target, hist_native):
    if pipe is hist_native:
        # The native model does not use a pipeline so, we can set the parameters
        # directly.
        pipe.set_params(max_depth=3, max_iter=15)
    else:
        pipe.set_params(
            histgradientboostingregressor__max_depth=3,
            histgradientboostingregressor__max_iter=15,
        )

dropped_result = cross_validate(hist_dropped, X, y, **common_params)
one_hot_result = cross_validate(hist_one_hot, X, y, **common_params)
ordinal_result = cross_validate(hist_ordinal, X, y, **common_params)
target_result = cross_validate(hist_target, X, y, **common_params)
native_result = cross_validate(hist_native, X, y, **common_params)
results_underfit = [
    ("Dropped", dropped_result),
    ("One Hot", one_hot_result),
    ("Ordinal", ordinal_result),
    ("Target", target_result),
    ("Native", native_result),
]

# 第 70 章 —— %%
plot_performance_tradeoff(
    results_underfit, "Gradient Boosting on Ames Housing (few and shallow trees)"
)

# 第 70 章 —— %%
# 第 70 章 —— The results for these underfitting models confirm our previous intuition: the
# 第 70 章 —— native category handling strategy performs the best when the splitting budget
# 第 70 章 —— is constrained. The three explicit encoding strategies (one-hot, ordinal and
# 第 70 章 —— target encoding) lead to slightly larger errors than the estimator's native
# 第 70 章 —— handling, but still perform better than the baseline model that just dropped
# 第 70 章 —— the categorical features altogether.
posted @ 2026-09-04 04:08  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报