Gilles Castel作者

世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的

一般你是用手写还是 MarkDown 做数学笔记?在这篇文章中,作者介绍了如何用 LaTex 和 Vim 实时做数学笔记,通过一系列炫酷的技巧,不论是表达式板书还是图像绘制,我们都能实时跟得上。

机器学习的学习过程中,很多时候都需要手动推导目标函数或最优化过程。这些推导很可能是老师在黑板上一边写一边解释,有时候我们会仔细听解释,并顾不上记笔记与注解。因此课程音频与视频就很重要了。但如果我们能跟得上老师的手写速度,那么以后回忆这些笔记就方便很多。

例如在最近结课的 CS224n 2019 中,Christopher Manning 在第一节课就手动推 Word2Vec 的最优化与权重更新过程。由此可见,飞一般地记数学笔记还是很有必要的。
图片截图来自 CS224n 2019

在这篇文章中,作者在攻读数学专业学士学位的第二个学期开始用 LaTex 做课堂笔记,自此以后一直在使用,总共记下了 1700 多页的笔记。以下是一些例子,你可以看看用 LaTex 做出笔记是什么样子的。

这些包括图表在内的笔记,都是在上课期间完成的,之后没有修订过。为了使 LaTex 做笔记可行,作者在头脑中设下以下四个目标:

  • 利用 LaTex 记文本和数学公式应与讲师在黑板上写字的速度保持一致:不允许延误。

  • 画图的速度应尽量与讲师保持一致。

  • 做笔记,如添加注解、编辑所有注解、整合最后两堂课的内容、搜索注解等,应方便快捷。

  • 当我们想在 pdf 文件旁边添加注释时,利用 LaTex 应能够实现这一目的。

以下从 Vim+LaTex 到 Snip­pets,作者介绍了如何科学地记数学笔记。

Vim 和 LaTex

我使用 Vim 在 LaTex 中记文本和数学公式。Vim 是一个功能强大的通用文本编辑器,可扩展性很强。我使用 Vim 写代码、LaTex、mark­down 等一切基于文本的东西。Vim 具有一个非常陡峭的学习曲线,一旦你弄清楚了基本原理,则很难再使用那些缺少 Vim 快捷键绑定的编辑器。以下是我编辑 LaTex 时屏幕的样子:

左边是 Vim,右边是我的 pdf 查看器 Zathura(也有类似于 Vim 的快捷键绑定)。我正使用带有 bspwm 的 Ubuntu 作为自己的窗口管理器。我在 Vim 使用的 LaTex 插件是 vimtex。该插件提供句法高亮显示、内容图表以及 synctex 等。使用 vim 插件的设置如下所示:

Plug 'lervag/vimtex'
let g:tex_flavor='latex'
let g:vimtex_view_method='zathura'
let g:vimtex_quickfix_mode=0
set conceallevel=1
let g:tex_conceal='abdmg'

最后两行的设置隐藏属性。这是一个特征,当你的光标不在那一行时,LaTex 代码会被替代或隐藏。通过隐藏 \[、\] 和$等标志符,你可以更舒服地浏览文件。这一特征也以∩替代\bigcap,以∈替代\in 等。以下动画应能够使这一过程更清楚。

这篇博客要解决的核心问题是:用 LaTex 做笔记如何与讲师在黑板上写字的速度保持一致。这都要归功于 snippet。

Snippets

一个 snippet 是一段可重复使用的短文本,可用来编辑其他文本。例如,当我键入 sign 并按下 Tab 时,单词 sign 将会补全为一个自定义的签名。

Snippet 也可以是动态的:当我键入 today 并按下 Tab 时,单词 today 将会被当前日期替代;键入 box Tab 变成一个可以自动增大的框。

你甚至可以在另一个 snippet 中使用 snippet。

利用 UltiSnip 创建 Snippet

我使用插件 UltiSnip 来管理我的 snippet,其设置如下:

lug 'sirver/ultisnips'
let g:UltiSnipsExpandTrigger = '<tab>'
let g:UltiSnipsJumpForwardTrigger = '<tab>'
let g:UltiSnipsJumpBackwardTrigger = '<s-tab>'

定义 sign 的 snippet 的代码如下所示:

snippet sign "Signature"
Yours sincerely,

Gilles Castel
endsnippet

对于动态 snippet,你可以在倒引号``之间输入代码,并在 snippet 扩展时运行。我在这里使用 bash 编译器格式化当前日期:date + %F。

snippet today "Date"
`date +%F`
endsnippet

你也可以在`!p ... `块内部使用 Python。你可以看一下定义 box 的 snippet 代码:

snippet box "Box"
`!p snip.rv = '┌' + '─' * (len(t[1]) + 2) + '┐'`
│ $1 │
`!p snip.rv = '└' + '─' * (len(t[1]) + 2) + '┘'`
$0
endsnippet

这些 Python 代码块将会被变量 snip.rv 的值替代。在这些代码块内部,你可以访问 snippet 的当前状态,如 t[1] 包含第一个制表位,fn 表示当前文档名称。

LaTeX snippet

借助于 snippet,利用 LaTeX 做笔记要比手写快得多。一些复杂的 snippet 可以为你节省大量时间并消除做笔记的挫败感。让我们开始了解一些简单的 snippet。

环境

为了嵌入环境,我必须在一行开端键入 beg。之后我键入环境名称,后者会直接在\end{} 指令中映出。按下 Tab 使光标位于新创建的环境中。

该 snippet 的代码如下所示:

snippet beg "begin{} / end{}" bA
\begin{$1}
    $0
\end{$1}
endsnippet

b 意味着该 snippet 将只能在一行开端扩展,并且 A 代表自动扩展,这意味着我不需要键入 Tab 就可以扩展 snippet。制表位--即可以通过按下 Tab 和 Shift+Tab 跳转到的地方--以$1、$2 等表示,同时最后一个为$0。

inline math 和 display math

我最常使用的两个 snippet 是 mk 和 dm。这些 snippet 负责数学代码的开始。第一个是 inline math snippet,第二个是 display math snippet。

in­line math snippet 是「智能的」:它知道何时在$符号后嵌入一个位置。当我在结尾$的正后方开始键入一个单词时,它添加一个位置。但是,当我键入一个非单词字符时,它不添加一个位置,例如下图的$p$-value。

该 snippet 的代码如下所示:

snippet mk "Math" wA
$${1}$`!pif t[2] and t[2][0] not in [',', '.', '?', '-', ' ']:
    snip.rv = ' 'else:
    snip.rv = ''
`$2endsnippet

第一行结尾处的 w 意味着该 snippet 将在词边界扩展,所以举例而言,hellomk 不会扩展,而 hello mk 会扩展。

diaplay math snippet 更简单,但同时也相当方便;该 snippet 使我在一段时间内不会忘记结束方程式。

snippet dm "Math" wA
\[
$1
.\] $0
endsnippet

上下标

另一个有用的 snippet 主要针对下标,该 snippet 自动将 a1 更改为 a_1 以及 a_12 更改为 a_{12}。

该 snippet 代码使用正则表达式作为触发器。当你在 [A-Za-z]\d 编码的数字后面键入一个字符,或者在 _以及两个数字 [A-Za-z]_\d\d 后面键入一个字符时,触发器会扩展该 snippet。

snippet '([A-Za-z])(\d)' "auto subscript" wrA
`!p snip.rv = match.group(1)`_`!p snip.rv = match.group(2)`
endsnippet

snippet '([A-Za-z])_(\d\d)' "auto subscript2" wrA
`!p snip.rv = match.group(1)`_{`!p snip.rv = match.group(2)`}
endsnippet

当你在使用圆括弧包装部分正则表达式时,如 (\d\d),你可以通过 Python 中的 match.group(i) 在扩展 snippet 中使用它们。

对于上标而言,我使用 td 自动扩展为 ^{} 的。但是,对于平方、立方、以及一小部分其他常见上标,我使用 sr、cb、comp 等专门的 snippet。

snippet sr "^2" iA
^2
endsnippet

snippet cb "^3" iA
^3
endsnippet

snippet compl "complement" iA
^{c}
endsnippet

snippet td "superscript" iA
^{$1}$0
endsnippet

分数 

用于分数的 snippet 是我使用最方便的自动扩展方法之一,它可以进行以下扩展:

第一个的代码非常简单:

snippet // "Fraction" iA
\\frac{$1}{$2}$0
endsnippet

第二和第三个例子使用正则表达式匹配 3/、4ac、 6\pi^2/、a_2/等表达式。

snippet '((\d+)|(\d*)(\\)?([A-Za-z]+)((\^|_)(\{\d+\}|\d))*)/' "Fraction" wrA
\\frac{`!p snip.rv = match.group(1)`}{$1}$0
endsnippet

如你所见,正则表达式可能非常复杂,这里有个图表可以解释:

在第四、第五个例子中,它试图找到匹配圆括弧。由于不能使用 Ul­tiSnips 的正则表达式引擎,我转向了 Python:

priority 1000
snippet '^.*\)/' "() Fraction" wrA
`!p
stripped = match.string[:-1]
depth = 0
i = len(stripped) - 1
while True:
    if stripped[i] == ')': depth += 1
    if stripped[i] == '(': depth -= 1
    if depth == 0: break;
    i -= 1
snip.rv = stripped[0:i] + "\\frac{" + stripped[i+1:-1] + "}"
`{$1}$0
endsnippet

最后一个与分数有关的 snippet 即使用你的选择来制作分数。你可以先用它选择一些文本,然后按下 Tab,打出/,然后再按下 Tab。

这些代码用到了 ${VISUAL} 变量表示你的选择。

snippet / "Fraction" iA
\\frac{${VISUAL}}{$1}$0
endsnippet

sympy 和 Math­e­mat­i­ca

另一个很酷但不太常用的 snippet 是利用 sympy 评估数学表达式。例如,sympy Tab 键扩展为 sympy | sympy,sympy 1 + 1 sympy Tab 键扩展为 2。

snippet sympy "sympy block " w
sympy $1 sympy$0
endsnippet

priority 10000
snippet 'sympy(.*)sympy' "evaluate sympy" wr
`!p
from sympy import *
x, y, z, t = symbols('x y z t')
k, m, n = symbols('k m n', integer=True)
f, g, h = symbols('f g h', cls=Function)
init_printing()
snip.rv = eval('latex(' + match.group(1).replace('\\', '') \
    .replace('^', '**') \
    .replace('{', '(') \
    .replace('}', ')') + ')')
`
endsnippet

Math­e­mat­i­ca 用户也可以进行类似的操作:

priority 1000
snippet math "mathematica block" w
math $1 math$0
endsnippet

priority 10000
snippet 'math(.*)math' "evaluate mathematica" wr
`!p
import subprocess
code = 'ToString[' + match.group(1) + ', TeXForm]'
snip.rv = subprocess.check_output(['wolframscript', '-code', code])
`
endsnippet

后缀 snippet

值得分享的还有后缀 snippet,如 phat → \hat{p},zbar → \overline{z} 等。一个类似的 snippet 是后缀向量,如 v → \vec{v}。这些 snippet 真的非常节省时间,因为你可以借此跟上老师板书的节奏。

注意,我还是可以用 bar 和 hat 前缀,我以较低的优先级将它们添了进去。那些 snippet 的代码如下:

priority 10
snippet "bar" "bar" riA
\overline{$1}$0
endsnippet

priority 100
snippet "([a-zA-Z])bar" "bar" riA
\overline{`!p snip.rv=match.group(1)`}
endsnippet


priority 10
snippet "hat" "hat" riA
\hat{$1}$0
endsnippet

priority 100
snippet "([a-zA-Z])hat" "hat" riA
\hat{`!p snip.rv=match.group(1)`}
endsnippet


snippet "(\\?\w+)(,\.|\.,)" "Vector postfix" riA
\vec{`!p snip.rv=match.group(1)`}
endsnippet 

其他 snippet

我还有 100 多个其他常用的 snippet,其中多数非常简单。例如,!>变成 \mapsto,->变成\to 等。

下载地址:https://castel.dev/tex-e0c2e8b64036f77db00411d562750c12.snippets

上下文管理

写这些 snip­pet 的时候需要考虑:这些 snippet 和普通文本有冲突吗?例如,我的词典里有大约 72 个英文单词、2000 个包含 sr 的荷兰语单词,也就是说,如果我打出 disregard,sr 就会变成^2,我会得到 di^2egard。

解决方案是在 snippet 中加入一个上下文管理文(Con­text)管理方法。使用 Vim 的句法高亮,就可以根据你是在写数学还是文本来决定 Ul­tiSnips 是否应该扩展 snippet。我的想法如下:

global !p
texMathZones = ['texMathZone'+x for x in ['A', 'AS', 'B', 'BS', 'C',
'CS', 'D', 'DS', 'E', 'ES', 'F', 'FS', 'G', 'GS', 'H', 'HS', 'I', 'IS',
'J', 'JS', 'K', 'KS', 'L', 'LS', 'DS', 'V', 'W', 'X', 'Y', 'Z']]

texIgnoreMathZones = ['texMathText']

texMathZoneIds = vim.eval('map('+str(texMathZones)+", 'hlID(v:val)')")
texIgnoreMathZoneIds = vim.eval('map('+str(texIgnoreMathZones)+", 'hlID(v:val)')")

ignore = texIgnoreMathZoneIds[0]

def math():
    synstackids = vim.eval("synstack(line('.'), col('.') - (col('.')>=2 ? 1 : 0))")
    try:
        first = next(
            i for i in reversed(synstackids)
            if i in texIgnoreMathZoneIds or i in texMathZoneIds
        )
        return first != ignore
    except StopIteration:
        return False
endglobal

现在你可以将 context "math()"添加到你只想在数学上下文中扩展的 snippet 了。

注意,「数学上下文」这个说法也很微妙。有时候你通过使用\text{...} 将一些文本添加到数学环境中。在那种情况下,你不想让 snip­pet 扩展。然而,在\[ \text{$...$} \] 中,你又需要扩展。所以「数学上下文」这个说法有点不好界定,如下图所示:

实时纠正拼写错误

尽管学习数学是我做笔记的一个重要部分,但大部分时间我都在打英语单词。我的打字技术还不错,每分钟 80 词左右,但我还是会时不时地出错。所以我在 Vim 上添加了快捷键绑定,纠正拼写错误,以免打断我的工作流程。我按下 Ctrl+L 键就可以纠正之前的拼写错误,就像这样:

我的拼写检查设置如下:

setlocal spell
set spelllang=nl,en_gb
inoremap <C-l> <c-g>u<Esc>[s1z=`]a<c-g>u

它跳转到之前的拼写错误 [s,然后选取第一个建议 1z=,接下来跳回 `]a。中间的<c-g>u 使得快速纠正拼写错误成为可能。

结论

使用 Vim 中的 snip­pet 使得书写 LaTeX 不再那么头疼,反而成为一种享受。与实时拼写检查结合之后,记数学笔记变得非常舒服。后续博文将讨论数字绘图及将图嵌入 LaTex 文本等内容。虽然前期学习成本会有一些,但熟悉后板书推导就能飞一般地记载。


原文链接:https://castel.dev/post/lecture-notes-1/

工程LaTeXVim数学
4
相关数据
权重技术

线性模型中特征的系数,或深度网络中的边。训练线性模型的目标是确定每个特征的理想权重。如果权重为 0,则相应的特征对模型来说没有任何贡献。

机器学习技术

机器学习是人工智能的一个分支,是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、计算复杂性理论等多门学科。机器学习理论主要是设计和分析一些让计算机可以自动“学习”的算法。因为学习算法中涉及了大量的统计学理论,机器学习与推断统计学联系尤为密切,也被称为统计学习理论。算法设计方面,机器学习理论关注可以实现的,行之有效的学习算法。

学习曲线技术

在机器学习领域,学习曲线通常是表现学习准确率随着训练次数/时长/数据量的增长而变化的曲线

目标函数技术

目标函数f(x)就是用设计变量来表示的所追求的目标形式,所以目标函数就是设计变量的函数,是一个标量。从工程意义讲,目标函数是系统的性能标准,比如,一个结构的最轻重量、最低造价、最合理形式;一件产品的最短生产时间、最小能量消耗;一个实验的最佳配方等等,建立目标函数的过程就是寻找设计变量与目标的关系的过程,目标函数和设计变量的关系可用曲线、曲面或超曲面表示。

暂无评论
暂无评论~