📕 第3章 深度学习的体验
📄 一句话概括

Jupyter Notebook 是一个网页界面,把代码分成一格一格的,写一格跑一格,结果就留在下面。 它存下来的是一个 JSON 文件(.ipynb),里面只有「格子内容」和「最后一次的输出」, 不存「你按了什么顺序跑」。这是所有怪现象的根源。 这一节的 Demo 真写了一个三格的 notebook 并读回来: 588 字节、39 行、两个 code 格一个 markdown 格;再演示同一份代码换顺序跑出两个结果。

📝 本节和前面几节的关系

上一节装了框架,这一节是写代码的地方。 书里第 3-2 节专门讲它,因为后面每一步实验都在 Notebook 里做。 这一节和算法无关,但踩过坑的人都知道:notebook 的隐状态问题浪费的时间,比学一个算法还多。

一、装与起

pip install notebook,然后在一个目录里敲 jupyter notebook,浏览器会自动打开一个页面, 地址是 localhost 加一个端口。 它是个本地服务,跑在你自己的机器上,关掉命令行窗口服务就停了。 所以别把它当成「网页版 Python」,它没有云端。 书里还提了 JupyterLab,那是新一代界面,功能更多(能开终端、能拖文件)。 两者读的是同一批 .ipynb 文件,用哪个都行。

二、格子有两种

Code 格写代码,Shift+Enter 运行,结果显示在格子下面。 Markdown 格写说明,同样 Shift+Enter 渲染成标题、列表、公式。 在命令模式(格子左边蓝色)下按 A 在上面插一格、B 在下面插一格、DD 删掉当前格。 这些快捷键用熟了比点菜单快得多。 输出是跟着格子存的。所以你把 notebook 发给别人,别人能看到你的输出, 但看不到你是怎么跑出来的——这就是下一节要讲的坑。

三、它就是一个 JSON

把 .ipynb 用文本编辑器打开,会看到一个 JSON:顶层四个键——cells、metadata、nbformat、 nbformat_minor。 cells 是个列表,每一项是一格:cell_type 说明是 code 还是 markdown, source 是内容(按行拆成字符串列表),code 格还多出 outputs 和 execution_count。 知道这件事有三个好处:能在 git 里 diff(它是纯文本)、能用脚本批量改(比如清空所有输出)、 能自己程序化生成 notebook。

四、执行顺序的坑

Notebook 的每个格子共享同一个 Python 进程,变量在格子之间是活的。这很方便,也很危险。 典型场景:第 1 格写 a = 1,第 2 格写 a = a + 1。按 1→2 跑一遍,a 是 2;按 2→1 跑一遍,a 是 1。 同一份文件,两个结果。 更隐蔽的是循环引用:你在第 5 格改了第 2 格用过的一个函数,但只重跑了第 5 格, 于是后面的结果一半用旧函数一半用新函数。这种错误看代码看不出来,只有重跑才能发现。 所以规矩只有一条:交结果之前,Restart Kernel and Run All。从头按顺序跑一遍,跑得通、结果对, 才算数。

五、两个实用技巧

%matplotlib inline:让画出来的图直接嵌在输出里,而不是弹一个新窗口。 新版 notebook 默认就是内嵌,不加也多半没问题。 %%time:放在格子第一行,跑完会告诉你这一格花了多久。 测训练速度时很有用,比自己在代码里插 time 干净。 书里那个 POINT 讲的是 Mac 上的执行方式——Mac 上装完 Jupyter 可能需要python3 -m jupyter notebook, 因为 jupyter 这个命令可能没进 PATH。

六、动手 Demo:它是一个 JSON 文件

用代码生成一个最小 notebook(两格),存盘、读回来、数数它有几个 cell、多少行代码; 然后用 exec 模拟「同样的两格、换顺序跑」会发生什么。

import json

nb = {
    "cells": [
        {"cell_type": "markdown", "metadata": {},
         "source": ["# 第一个 Notebook\n"]},
        {"cell_type": "code", "execution_count": 1, "metadata": {},
         "outputs": [], "source": ["a = 1\n"]},
    ],
    "metadata": {"kernelspec": {"name": "python3"}},
    "nbformat": 4,
    "nbformat_minor": 5,
}
json.dump(nb, open("demo.ipynb", "w", encoding="utf-8"),
          ensure_ascii=False, indent=1)

# 执行顺序:同一份代码,两种顺序两个结果
ns = {}
exec("a = 1", ns)          # 第 1 格
exec("a = a + 1", ns)      # 第 2 格
print(ns["a"])              # 2

实跑输出:

--------------------------------------------------------------------
使用 Jupyter Notebook:它是一个 JSON 文件
--------------------------------------------------------------------

  写了一个三格的 notebook:588 字节,39 行。
  它的顶层只有四个键:cells、metadata、nbformat、nbformat_minor

  cell 类型     个数
  --------------------
  code          2
  markdown      1

  代码行合计 6 行。**整个 notebook 就是这样一个 JSON**——
  所以它能在 git 里 diff、能被脚本读写、也能被 nbconvert 转成 html。

  真正要小心的是「执行顺序」。同样两格代码,按不同顺序跑,结果不一样:

  第 1 格:a = 1
  第 2 格:a = 2
  按 1 → 2 的顺序跑完,a = 2

  (先跑第 2 格)a = 100
  (再跑第 1 格)a = 1
  按 2 → 1 的顺序跑完,a = 1

  **同一个 notebook,两次跑出两个结果。**笔记本保存的是「格子」和「最后一次的输出」,
  不保存「执行顺序」。所以看到别人的 notebook 结果对不上,第一件事是
  Restart & Run All——从头按顺序跑一遍,才能确认结果是真的。

588 字节、39 行、2 个 code 格加 1 个 markdown 格。这就是一个 notebook 的全部—— 没有任何隐藏状态存在文件里。 顶层四个键里,nbformat 和 nbformat_minor 是格式版本号(4 和 5)。 别人打不开你的 notebook,很多时候就是版本号对不上。 后半段那个实验是关键:按 1→2 的顺序跑,a 最后是 2;按 2→1 的顺序跑,a 最后是 1。 代码一个字没变,结果变了。 现实里更糟:你不会只跑两格,而是几十格来回跳。 所以看到「书上的代码我抄了但结果不对」,第一件事不是怀疑书,是 Restart & Run All。

30 秒实验

  1. 把生成出来的 .ipynb 用文本编辑器打开,找到 execution_count 这个键,看它是什么。
  2. 在真实 notebook 里故意乱序跑一次,再 Restart & Run All,对比两次的输出。
  3. 用 jupyter nbconvert --to script demo.ipynb 把它转成 .py,看转换成什么样。

常见误区

  1. 把 notebook 当成脚本用。它是探索工具,不是程序。 要交付、要定时跑、要给别人调用,就写成 .py。
  2. 不重启就一直往下跑。跑上几个小时之后,内存里堆着一堆早就删掉的东西, 结果谁也说不清是怎么来的。
  3. 把大数据集读进 notebook 变量然后保存。输出里嵌着图片和表格,文件能涨到几十兆, git 一提交就再也拉不动。
自测三问(点开对答案) 1. notebook 文件里存了什么、不存什么? —— 存了每个格子的内容和最后一次的输出,不存执行顺序、不存运行时内存。 所以「跑出这个结果的过程」是不可复现的。 2. 为什么会出现「同样的代码两次结果不同」? —— 因为所有格子共享一个进程,变量是活的。换一个执行顺序,同一个变量在不同时刻的值就不一样。 3. 交作业前必须做哪一步? —— Restart Kernel and Run All。从头按顺序跑一遍,确认在干净状态下结果成立。

工具就绪,开始看数据。下一节是 NumPy——这一节要盯住的只有一件事:形状。

相关笔记