使用Python生成LaTeX 數(shù)學(xué)公式

原文地址: https://www.zhouwenzhen.top/archives/48/
使用Python生成LaTeX 數(shù)學(xué)公式

在閱讀算法文獻或者數(shù)學(xué)相關(guān)的文章中經(jīng)常會看到一些簡單或復(fù)雜的數(shù)學(xué)公式,最近在分享此類文章時,想使用LaTex鍵入數(shù)學(xué)公式以美化閱讀,發(fā)現(xiàn)需要反復(fù)去查詢LaTex相關(guān)的語法嗜傅,效率較低且容易出錯千埃。

最近 GitHub 上出現(xiàn)了一個開源項目 latexify_py春叫,它使用 Python 就能生成 LaTeX 數(shù)學(xué)公式亭螟。打開Google Colaboratory示例列舉了幾個案例:

image-20200910103526562

先試試看

在本地安裝相應(yīng)的Python包残家,Python版本 >= 3.6

pip install latexify-py

參考官方示例進行測試:

import math
import latexify


@latexify.with_latex
def solve(a, b, c):
    return (-b + math.sqrt(b ** 2 - 4 * a * c)) / (2 * a)


if __name__ == '__main__':
    print(solve)

終端打印結(jié)果為:

\mathrm{solve}(a, b, c)\triangleq \frac{-b + \sqrt{b^{2} - 4ac}}{2a}

將打印結(jié)果輸入到支持LaTeX的編輯器中榆俺,以Typora為例。選擇插入公式塊:
\mathrm{solve}(a, b, c)\triangleq \frac{-b + \sqrt{b^{2} - 4ac}}{2a}
于是,把最近閱讀的facebook開源的prophet時間序列預(yù)測算法提到的飽和增長模型公式進行測試茴晋,原文中為

image-20200910115235060

開始在python中鍵入代碼:

@latexify.with_latex
def g(t):
    return C(t) / (1 + exp(1-(k + alpha(t) ** T * delta) * (t -(m + alpha(t) ** T * gamma))))

終端打印結(jié)果并輸入Typora為:

\mathrm{g}(t)\triangleq \frac{\mathrm{C}\left(t\right)}{1 + \mathrm{exp}\left(1 - (k + \mathrm{{\alpha}}\left(t\right)^{t}{\delta})(t - m + \mathrm{{\alpha}}\left(t\right)^{T}{\gamma})\right)}

\mathrm{g}(t)\triangleq \frac{\mathrm{C}\left(t\right)}{1 + \mathrm{exp}\left(1 - (k + \mathrm{{\alpha}}\left(t\right)^{T}{\delta})(t - m + \mathrm{{\alpha}}\left(t\right)^{T}{\gamma})\right)}

對比發(fā)現(xiàn)python輸出的公式中有一個錯誤:刪除了一個括號陪捷,而python代碼中是包含的,由
t - (m + \mathrm{{\alpha}}\left(t\right)^{T}{\gamma})
變成了:
t - m + \mathrm{{\alpha}}\left(t\right)^{T}{\gamma}
為了進一步驗證上面出現(xiàn)的問題诺擅,輸入一段很簡單的代碼:

@latexify.with_latex
def test(a, b):
    return  - (a + b)

輸出的公式和預(yù)想的一致:
\mathrm{test}(a, b)\triangleq -\left(a + b\right)
這時市袖,小小的修改一下代碼:

@latexify.with_latex
def test(a, b):
    return  1 - (a + b)

預(yù)想的公式應(yīng)該為:
\mathrm{test}(a, b)\triangleq 1 - (a + b)
而實際卻是:
\mathrm{test}(a, b)\triangleq 1 - a + b
猜想,這可能是一個bug或者是輸入的方式不對烁涌,雖然這個問題很好解決苍碟,但是一直很疑惑。撮执。微峰。。抒钱。

latexify_py做了什么蜓肆?

為了一探究竟,嘗試去閱讀其源碼谋币,看看它都做了哪些事情仗扬?

首先入口是@latexify.with_latex這個注解。latexify提供with_latex和get_latex兩個注解蕾额,with_latex只是先做一些初始化早芭,實際也是調(diào)用get_latex。重點看一下get_latex凡简,其源碼:

def get_latex(fn, math_symbol=True):
  try:
    source = inspect.getsource(fn)##獲取整個模塊的源代碼
  except Exception:
    # Maybe running on console.
    source = dill.source.getsource(fn)

  return LatexifyVisitor(math_symbol=math_symbol).visit(ast.parse(source)) ##ast.parse把源碼解析為AST節(jié)點逼友,AST是抽象語法樹,不依賴于具體的文法秤涩,不依賴于語言的細節(jié)帜乞,我們將源代碼轉(zhuǎn)化為AST后,可以對AST做很多的操作

LatexifyVisitor繼承ast的NodeVisitor筐眷,ast.NodeVisitor是一個專門用來遍歷語法樹的工具黎烈,可以通過繼承這個類來完成對語法樹的遍歷以及遍歷過程中的處理。

LatexifyVisitor首先從根節(jié)點root進行遍歷匀谣,在遍歷的過程中照棋,每個節(jié)點類型都有專用的類型處理函數(shù),以"visit_" + "Node類型"為名稱武翎,如果不存在烈炭,則調(diào)用通用的的處理函數(shù)generic_visit。

在latexify的core.py直接引入astunparse宝恶,將生成的ast打印出來:

def get_latex(fn, math_symbol=True):
  try:
    source = inspect.getsource(fn)
    
    print(astunparse.dump(ast.parse(source)))
    
  except Exception:
    # Maybe running on console.
    source = dill.source.getsource(fn)

  return LatexifyVisitor(math_symbol=math_symbol).visit(ast.parse(source))

下面是test對應(yīng)的ast結(jié)構(gòu):

Module(
  body=[FunctionDef(
    name='test',
    args=arguments(
      posonlyargs=[],
      args=[
        arg(
          arg='a',
          annotation=None,
          type_comment=None),
        arg(
          arg='b',
          annotation=None,
          type_comment=None)],
      vararg=None,
      kwonlyargs=[],
      kw_defaults=[],
      kwarg=None,
      defaults=[]),
    body=[Return(value=BinOp(
      left=Constant(
        value=1,
        kind=None),
      op=Sub(),
      right=BinOp(
        left=Name(
          id='a',
          ctx=Load()),
        op=Add(),
        right=Name(
          id='b',
          ctx=Load()))))],
    decorator_list=[Attribute(
      value=Name(
        id='latexify',
        ctx=Load()),
      attr='with_latex',
      ctx=Load())],
    returns=None,
    type_comment=None)],
  type_ignores=[])

首先訪問根節(jié)點root符隙,root為Moudle類型趴捅,會調(diào)用visit_Moudle函數(shù),以此始遍歷子節(jié)點FunctionDef霹疫、Return和BinOp拱绑,調(diào)用對應(yīng)的visit_FunctionDef、visit_Return和vist_BinOp丽蝎。

參照打印出來的python公式代碼和ast結(jié)構(gòu)猎拨,來分析一下整體邏輯:

vist_FunctionDef

def visit_FunctionDef(self, node):
  name_str = r'\mathrm{' + str(node.name) + '}'
  arg_strs = [self._parse_math_symbols(str(arg.arg)) for arg in node.args.args]
  body_str = self.visit(node.body[0])
  return name_str + '(' + ', '.join(arg_strs) + r')\triangleq ' + body_str

遍歷FunctionDef節(jié)點后,輸出為:

\mathrm{test}(a屠阻,b)\triangleq

visit_Return

def visit_Return(self, node):
  return self.visit(node.value)

Return節(jié)點的值為子節(jié)點红省,類型為BinOp。ast將輸入的代碼分為left和right国觉,test例子中类腮,left為常數(shù)1,right是下一個子節(jié)點蛉加,類型為BinOp蚜枢,op為運算符,這里為Sub減法针饥〕С椋看看visit_BinOp:

visit_BinOp

def visit_BinOp(self, node):
  priority = {
      ast.Add: 10,
      ast.Sub: 10,
      ast.Mult: 20,
      ast.MatMult: 20,
      ast.Div: 20,
      ast.FloorDiv: 20,
      ast.Mod: 20,
      ast.Pow: 30,
  }

  def _unwrap(child):
    return self.visit(child)

  def _wrap(child):
    latex = _unwrap(child)
    if isinstance(child, ast.BinOp):
      cp = priority[type(child.op)] if type(child.op) in priority else 100
      pp = priority[type(node.op)] if type(node.op) in priority else 100

      if cp < pp:
        return '(' + latex + ')'
    return latex

  l = node.left
  r = node.right
  reprs = {
      ast.Add: (lambda: _wrap(l) + ' + ' + _wrap(r)),
      ast.Sub: (lambda: _wrap(l) + ' - ' + _wrap(r)),
      ast.Mult: (lambda: _wrap(l) + _wrap(r)),
      ast.MatMult: (lambda: _wrap(l) + _wrap(r)),
      ast.Div: (lambda: r'\frac{' + _unwrap(l) + '}{' + _unwrap(r) + '}'),
      ast.FloorDiv: (lambda: r'\left\lfloor\frac{' + _unwrap(l) + '}{' + _unwrap(r) + r'}\right\rfloor'),
      ast.Mod: (lambda: _wrap(l) + r' \bmod ' + _wrap(r)),
      ast.Pow: (lambda: _wrap(l) + '^{' + _unwrap(r) + '}'),
  }

  if type(node.op) in reprs:
    return reprs[type(node.op)]()
  else:
    return r'\mathrm{unknown\_binop}(' + _unwrap(l) + ', ' + _unwrap(r) + ')'

ast.Add和ast.Sub設(shè)置的優(yōu)先級都為10,_wrap方法通過優(yōu)先級來判斷是否添加括號丁眼,即:

  cp = priority[type(child.op)] if type(child.op) in priority else 100
  pp = priority[type(node.op)] if type(node.op) in priority else 100
  if cp < pp:
    return '(' + latex + ')'

test例子中child.op為Sub筷凤,node.op是right中的op為Add,優(yōu)先級相同不添加括號苞七,所以輸出:

1 - a + b

遍歷結(jié)束后輸出:

\mathrm{test}(a, b)\triangleq 1 - a + b

這和公式實際上表達的意思南轅北轍藐守,解決方法就是將小于改為小于等于,即

 if cp <= pp:
    return '(' + latex + ')'
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蹂风,一起剝皮案震驚了整個濱河市卢厂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惠啄,老刑警劉巖慎恒,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異撵渡,居然都是意外死亡融柬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門趋距,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粒氧,“玉大人,你說我怎么就攤上這事节腐⊥舛ⅲ” “怎么了廊敌?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長门怪。 經(jīng)常有香客問我,道長锅纺,這世上最難降的妖魔是什么掷空? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮囤锉,結(jié)果婚禮上坦弟,老公的妹妹穿的比我還像新娘。我一直安慰自己官地,他們只是感情好酿傍,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著驱入,像睡著了一般赤炒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上亏较,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天莺褒,我揣著相機與錄音,去河邊找鬼雪情。 笑死遵岩,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的巡通。 我是一名探鬼主播尘执,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼宴凉!你這毒婦竟也來了誊锭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤弥锄,失蹤者是張志新(化名)和其女友劉穎炉旷,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叉讥,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡窘行,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了图仓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片罐盔。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖救崔,靈堂內(nèi)的尸體忽然破棺而出惶看,到底是詐尸還是另有隱情捏顺,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布纬黎,位于F島的核電站幅骄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏本今。R本人自食惡果不足惜拆座,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望冠息。 院中可真熱鬧挪凑,春花似錦、人聲如沸逛艰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽散怖。三九已至菇绵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間镇眷,已是汗流浹背脸甘。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留偏灿,地道東北人丹诀。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像翁垂,于是被迫代替她去往敵國和親铆遭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345