21

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

 5 years ago
source link: https://www.jiqizhixin.com/articles/19040101?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

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

3mYvamN.png!web 图片截图来自 CS224n 2019

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

6Vn6Jri.png!web

fYnAf27.png!web

RFnaayZ.png!web

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

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

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

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

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

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

Vim 和 LaTex

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

i6NnEfJ.png!web

左边是 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 等。以下动画应能够使这一过程更清楚。

YbMZreF.gif

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

Snippets

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

rEjqYjB.gif

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

iaINreu.gif

3IZFjqy.gif

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

bUrA7v3.gif

利用 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 使光标位于新创建的环境中。

6ZZVR3A.gif

该 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。

jAV7Bzj.gif

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

E3euqey.gif

该 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 使我在一段时间内不会忘记结束方程式。

viIrA3v.gif

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

上下标

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

bi2MfmA.gif

该 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。

i67Zfiz.gif

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 是我使用最方便的自动扩展方法之一,它可以进行以下扩展:

UvQNreu.png!web

muIVV3Z.gif

第一个的代码非常简单:

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

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

NjmE3yU.png!web

在第四、第五个例子中,它试图找到匹配圆括弧。由于不能使用 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。

YriMjam.gif

这些代码用到了 ${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。

Avuquu3.gif

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 用户也可以进行类似的操作:

byMrEzz.gif

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 真的非常节省时间,因为你可以借此跟上老师板书的节奏。

AjAVZv2.gif

注意,我还是可以用 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

ArE3Ajn.gif

上下文管理

写这些 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{$...$} \] 中,你又需要扩展。所以「数学上下文」这个说法有点不好界定,如下图所示:

aErqEnj.gif

实时纠正拼写错误

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

zueEz2I.gif

我的拼写检查设置如下:

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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK