又是一个新坑

我不管, 好玩玩它就是了.

又, 这本书里面现在看起来故事挺多的, 感觉挺有意思的.

Finite-State Techniques

Finite-state automata (FSAs) are among the simplest computing machines that can be envisaged. They are well understood mathematically, easy to implement and efficient at doing what they do. If an NLP problem can be conveniently solved with a finite-state automaton, then it is probably a good idea to solve it that way. However. FSAs are subject to certain formal limitations that render them ill suited to certain computational linguistic tasks. This chapter provides a fairly comprehensive introduction to these machines and their implementation, indicates possible areas of application and gives some concrete examples of their use, and examines their limitations.

Finite-state transition networks

如何判断输入的文本的语言类型?

  • 词频分析:

    比如对于不同的语言文本, 其字母出现的频率不同, 所以可以通过这样的方式来判断.

  • 暴力枚举:

    假设所有的语言都存在对应的处理程序, 对所有的处理程序, 都进行一次匹配, 如果不成功的话就使用下一个语言, 这样的坏处是比较慢.

简单的自动机的例子

一个开怀大笑的例子:

/_img/reading/lisp-nlp/laughing-machine.svg

上图即为一个简单的 (只有四个状态的) 描述 FSA (有限自动机) 的 FSTN (有限状态转移图). 其中在第三个状态 3 处, 在两条道路 ”h” 和 ”!” 之间会有一定的机率发生转换. 这个转换的依据可以是外部的一个随机数生成器这样之类的东西.

使用上述自动机, 还能够用来处理输入, 比如将输入的符号扔到里面进行匹配. (那么说起来, 这个的感觉有点像是一种正则表达式进行匹配的感觉. )

当然, 描述一个自动机可以有多种可能:

/_img/reading/lisp-nlp/laughing-machine-reading.svg

不过和前一种自动机不同的是, 这个自动机是 non-deterministic (非确定的) 的自动机. 从 2 状态的两个 a 转换的规则即可看出.

除了上面的未定分支, 还可以出现跳转分支 (jump arc, 或者说, 无标签的跳转 unlabelled arcs):

/_img/reading/lisp-nlp/laughing-machine-with-jump-arc.svg

注: 这一段是想要说明 FSTN 和 FSA 的一些细微的区别, 即对于 FSTN 来说, 上面的网络都是可行的, 但是对于 FSA 来说, 这样的网络并不都是可行的. 但是一般为了方便, 所以会在不出现歧义的情况下将二者等同.

于是上面就给出了一个如何处理语言 (或者说, 如何识别特定语言) 的思路: 即对一种语言, 构建出关于其的自动机网络 FSTN, 然后对其识别即可.

A notation for networks

为了达到使用 FSTN 对语言进行识别的目的, 首先需要想一种用来描述 FSTN 的形式标记. 而对于一个 FSTN, 其应该由如下组成: (显然, 这就是一个有向图)

  1. 网络需要有一个名称

    尽管在描述 FSTN 的时候, 一个名字仅仅只是助记用途 (mnemonic), 但是在 RTN 种, the name of network plays an absolutely crucial role in the definition of RTNs.

    在这里用:

    Name TO-LAUGH:
    

    来标记之前的 laughing 机.

  2. 网络的节点的集合

    对于节点的集合, 其必须包含一个初始节点 Initial, 以及一个终止节点 Final. 于是一个图的节点集合可以记为:

    Initial 1
    Final 4
    

    对于这样的一个集合, 一个更加方便的方法则是对集合进行划分 (引入子集和缩写 abbreviates 的方式来进行描述):

    V abbreviates:
      a, e, i, o, u.
    C abbreviates:
      b, c, d, f, g, h, j, k, l, m, n, p, q, r, s, t, v, w, x, y, z.
    

    对于这样的缩写, 在绘图的时候通过用缩写名字来代替各种线的方式来简化图的结构.

  3. 连接网络中节点的有向线段

    对于有向线段的描述, 通过 From <node> to <node> by <label> 的方式进行定义. 其中在用 # 来表述 <label> 的时候, 认为其为无 label 的跳转.

    于是一个完整的描述如下: (为上面的最后一个自动机的图)

    Name TO-LAUGH:
      Initial 1
      Final 4
      From 1 to 2 by h
      From 2 to 3 by a
      From 3 to 1 by #
      From 3 to 4 by !.
    

    其中, <label> 可以是多个字符的子集.

    并且还能够通过这样的方式来对其进行描述: (通过一个转移矩阵来描述和储存)

    ha!
    1200
    2030
    3204
    4000

    其中用 0 表示没有线段, 用其他的数字来表示到达的目的地.

英语的一些例子

英语语句

一个英语的例子 (代码以及解释)

对于一个英语的语句结构, 可以用如下的 FSTN 来进行表述:

Name ENGLISH-1:
  Initial 1
  Final 9
  From 1 to 3 by NP
  From 1 to 2 by DET
  From 2 to 3 by N
  From 3 to 4 by BV
  From 4 to 5 by ADV
  From 4 to 5 by #
  From 5 to 6 by DET
  From 5 to 7 by DET
  From 5 to 8 by #
  From 6 to 6 by MOD
  From 6 to 7 by ADJ
  From 7 to 9 by N
  From 8 to 8 by MOD
  From 8 to 9 by ADJ
  From 9 to 4 by CNJ
  From 9 to 1 by CNJ

  NP abbreviates:
    kim, sandy, lee.
  DET abbreviates:
    a, the, her.
  N abbreviates:
    consumer, man, woman.
  BV abbreviates:
    is, was.
  CNJ abbreviates:
    and, or.
  ADJ abbreviates:
    happy, stupid.
  MOD abbreviates:
    very.
  ADV abbreviates:
    often, always, sometimes.

其中的缩写分别对应:

  • N: noun 名词
  • NP: noun phrase 名词性短语
  • DET: determiners 定冠词 (words that can come before (common) nouns)
  • ADJ: adjectives 形容词
  • MOD: modify adjecitves 形容词修饰短语, 如 “stupid” 和 “very stupid”
  • V: verb 动词
  • VP: verb phrase 动词短语
  • BV: be 动词
  • CNJ: conjuction 连接词

说明:

  • 尽管这样的 FSTN 看起来并不是很容易理解, 但是却很容易实现.
  • 感觉可以用类似于正则表达式或者是 ENBF 的方式来表述上面的语句:
    <sentence> ::= <object> <BV> <description>
    <object> ::= <DET> <N> | <NP>
    <description> ::= (<adj_pharse> | <noun_pharse>) <more>*
    <adj_pharse> ::= <ADV> <MOD>* <ADJ>
    <noun_pharse> ::= (<ADV> | " ") (<DET> (<MOD>* <ADJ> | " ") <N> | <MOD>* <ADJ>)
    <more> ::= <CNJ> (<description> | <sentence>)
    

    (差不多这样的感觉, 写得不是很干净… )

  • 不过上面的 FSTN 仅仅实现了 A is B 这样的语句. 因为语句比较简单, 所以可以通过一些非常直接的方式来进行构造:
    1. 主体肯定是 <A> <BV> <B> 这样的构造
    2. 对于 <A> 的部分, 需要构造名词性短语, 即 <DET> <N> 或者 <NP> 短语
    3. 对于 <B> 的部分, 需要构造的是一个形容词性短语或者是名词性短语.

      对于形容词性短语, 简单的方式就是 <ADV> <MOD>* <ADJ> 这样构造

      对于名词性短语, 通过形容词性短语修饰的名词即可得到

    并且通过拓展语料库的方式, 应该就能够实现更多复杂的语句. 不过这个目前并没有上下文一致性的判断.

/_img/reading/lisp-nlp/english-1-fstn.svg

一些其他的例子

英语的单音节词

其中对于英语的单音节的单词:

/_img/reading/lisp-nlp/eng-monosyl-fstn.svg

代码以及解释
Name ENG-MONOSYL:
  Initial 1, 2
  Final 3, 4, 5
  From 1 to 2 by C0
  From 2 to 3 by V
  From 3 to 4 by C8
  From 4 to 5 by s
  From 1 to 7 by C3
  From 7 to 2 by w
  From 1 to 6 by C2
  From 6 to 2 by l
  From 6 to 5 by #
  From 1 to 5 by C1
  From 5 to 2 by r
  From 1 to 8 by s
  From 8 to 5 by C4
  From 8 to 2 by C5
  From 3 to 9 by l
  From 3 to 10 by s
  From 3 to 11 by C7
  From 9 to 4 by C6
  From 10 to 4 by C4
  From 11 to 4 by th.

  V abbreviates:
    a, ae, ai, au, e, ea, ee, ei, eu, i, ia, ie, o, oa, oe, oi, oo, ou, ue, ui.
  CO abbreviates:
    b, c, ch, d, f, g, h, j, k, I, m, n, p, qu, r, s, sh, t, th, v, w, x.
  C1 abbreviates:
    d, sh, th.
  C2 abbreviates:
    b, c, f, g, k.
  C3 abbreviates:
    d, g, h, t, th.
  C4 abbreviates:
    c, k, p, t.
  C5 abbreviates:
    c, k, l, m, n, p, pl, qu, t, w.
  C6 abbreviates:
    b, t, m.
  C7 abbreviates:
    d, f, i, n, x.
  C8 abbreviates:
    b, c, ch, ck, d, f, g, h, k, l, m, mp, mph, n, ng, p, que, r, s, sh, th, v, w, x, y, z.
题外话
随机的英文单词

突然很好奇一个想法, 如果我有一个足够大的英语单词库 (比如 这个), 然后对其进行统计, 比如说将 26 个字母每个字母都做一遍统计, 统计其从前一个字母, 或者说前几个字母向下一个字母变化的概率.

比如说用这样的一个程序来统计: (请忽略我丑陋的四不像代码)

这里 下载一个 10000 词的词典:

require 'open-uri'
words = URI.open(URI("https://www.mit.edu/~ecprice/wordlist.10000"))\
	    .read.split(/\s+/)\
	    .map { |word| word.downcase.split("").map { |c| c.ord - 97 } }

words.length # => 10000

其中统计的方法是这样的:

  • 对于一个 arc, 用 From A to B by P 的形式来记录
  • 通过使用一个矩阵 trans 来记录数据. 规定第 A 行为 From, 第 B 列为 to, 即第 A 行的第 B 列的元素记录了 From A to B 的 arc 次数.
    trans = 27.times.map { 27.times.map { 0 } }
    
    puts "You got a 27x27 matrix with default value of 0."
    
  • count = ->(word) {
      pre = word[0] # input as C style word
      (1...word.length).each do |i| # start from second character
        trans[pre][word[i]] += 1
        pre = word[i]
      end
      trans[word[-1]][26] += 1 # end of word
    }
    
    words.each { |word| count.call(word) }
    
    puts "Count each arcs to the matrix."
    
    归一化前
    6132323237840147212183596532157276170106783117818379341710120299
    1601584170012128811501131283011651710123015153
    3622639388223141921153125365913513141303120210352169
    148693648942653629131159115517111798528111340880
    37748322685176949418443163702248443812636111494127628120731754541310
    101142121763119200611012610617306202111063
    12463327422310410301338506350133621567000241613
    248436294001188011610162313244146954150303186
    2588745617428293158110625270160132560311081704414551918412215092
    33021400009100104330110371000010
    27602156432962111321122046826010100153
    37214148457221914690193021572901603106889724302170488
    33864624305222541066871931850361447131210208
    276925735841446706112741048251398135458454661605362465838
    626312411621439010396562822841067154149061317518627510013120348144
    2493793082895124121781152321000307669074100121142
    101000001003000000121030000011
    5933486130999257775591615310910041036111432023198511211521599
    14012143104731272043600375338131841617726264620822803502043
    3799405798642569950267281330610035327812216661811371760
    101729358871665186210142113232770231721718413258837
    11625341502120500000592014220014025
    129433104303110421923161301832611315075
    1812402410625001212530003280044058
    181014848021240016231621150114910507002727
    22102440001400200160011050004717
    000000000000000000000000000

    归一化代码: (保留百分号后两位小数)

    26.times do |alphabet|
      total = trans[alphabet].sum
      trans[alphabet].map! { |count| (count * 100.0 / total).round(2) }
    end
    
    归一化后 (单位 %)
    0.112.456.014.410.150.742.730.394.050.061.112.144.013.520.113.160.1912.615.7814.521.541.470.630.321.880.375.56
    14.021.310.70.3514.90.00.090.1811.220.70.0913.150.960.2611.220.260.010.174.470.618.850.180.260.01.310.094.65
    11.970.072.080.312.830.070.0710.386.350.035.064.130.10.219.540.10.174.331.3610.023.970.070.030.01.160.075.59
    5.90.240.361.4419.510.161.040.214.440.360.041.240.60.364.590.20.042.834.670.363.391.120.440.041.360.035.1
    4.960.634.249.012.321.241.240.240.580.040.214.872.9511.10.51.660.4714.6612.383.630.371.580.962.30.590.0517.23
    10.90.110.430.2213.058.20.320.1120.710.00.06.580.110.013.590.110.06.580.763.246.690.00.220.111.190.06.8
    7.220.350.170.1715.960.121.346.066.00.00.061.920.472.913.670.290.07.753.610.873.90.00.00.01.40.0635.7
    17.350.280.210.4220.570.00.00.0713.160.00.071.120.71.1216.170.210.143.080.984.833.780.070.350.02.10.2113.02
    4.721.598.353.195.161.72.890.020.180.110.464.942.9324.2611.042.010.153.118.088.330.353.370.020.40.020.921.68
    18.030.01.090.5521.860.00.00.04.920.550.00.00.550.023.51.640.00.550.550.020.220.550.00.00.00.05.46
    4.561.010.00.3426.350.680.510.3416.220.340.171.860.513.552.030.340.00.6811.490.341.010.00.170.01.690.025.84
    11.510.430.432.617.70.650.280.0314.520.00.599.350.460.228.980.50.00.093.282.723.00.740.090.06.720.015.1
    17.683.350.310.122.490.260.10.113.280.050.00.313.560.3710.099.680.00.163.190.212.460.050.160.051.10.010.88
    5.720.195.337.428.590.9514.640.235.680.211.00.520.272.032.80.080.10.179.4213.711.241.10.120.040.950.117.38
    1.461.482.922.730.491.012.120.240.920.141.326.636.6825.093.623.50.014.424.124.376.472.353.080.470.80.193.39
    12.280.150.350.4415.190.10.394.696.120.050.18.780.540.2511.454.930.015.153.264.443.650.050.00.00.590.057.01
    0.810.00.810.00.00.00.00.00.810.00.02.440.00.00.00.00.00.00.811.6383.740.00.00.00.00.08.94
    12.20.71.772.6720.560.511.580.1411.50.021.261.092.242.068.440.740.022.356.584.752.021.050.250.023.130.0212.33
    2.750.242.810.29.30.240.144.017.080.00.731.040.750.263.623.170.140.145.1512.74.090.040.550.00.690.040.18
    7.960.190.840.1116.760.130.085.3820.90.00.041.410.590.276.430.210.07.425.842.563.490.130.380.022.880.0215.97
    5.213.714.82.994.490.833.350.054.440.10.527.325.8311.960.363.610.116.3511.199.490.050.150.10.260.410.411.91
    13.660.240.590.3548.880.00.240.1224.150.00.00.00.00.06.950.240.00.120.470.240.240.00.00.120.470.02.94
    20.410.630.470.4716.460.470.04.9116.460.320.161.420.324.919.650.470.02.855.060.950.160.160.470.160.790.011.87
    6.820.389.090.09.090.380.02.279.470.00.00.380.760.380.7620.080.00.00.012.123.030.00.01.521.520.021.97
    1.750.971.360.784.670.00.190.12.340.00.01.562.241.562.041.460.01.074.770.970.490.00.680.00.00.1970.79
    16.180.740.01.4732.350.00.00.010.290.00.01.470.00.011.760.00.00.740.740.03.680.00.00.02.945.1512.5
    000000000000000000000000000
  • 其中关于绘图的代码: 在输出的时候为了美观起见, 最多只输出前 3 (n = 3) 个可能的 arc. 且并不输出休止符.
    n = 3
    res = ""
    
    26.times do |alphabet|
      index = -1
      res << (trans[alphabet][0...-1]
                .map { |percent| [percent, (index += 1)] }
                .sort_by { |x| x[0] }
                .reverse)[0...n]
               .map { |item| "#{(alphabet + 97).chr} -> #{(item[1] + 97).chr} [label = \"#{item[0]}\"];\n" }
               .join("")
    end
    
    res
    

    最终得到的图的结果:

    /_img/reading/lisp-nlp/cout-of-english-words.svg

    没什么想法美化了, 只能说真的丑啊… 看来之后还要想想办法钻研一下如何出漂亮的图.

  • 造词部分则通过如下的方式来实现
    准备的代码
    require 'pickup' # https://github.com/fl00r/pickup
    
    hashed_trans = {}
    
    trans.each_with_index do |tos, from_index|
      hashed_tos = {}
    
      tos.each_with_index do |possibility, to_index|
        hashed_tos[(to_index + 97).chr] = possibility
      end
    
      hashed_trans[(from_index + 97).chr] = Pickup.new(hashed_tos)
    end
    
    word = -> (iter = 0, char = '') {
      c = (rand(26) + 97).chr
      if iter == 0
        c + word.call(1, c)
      elsif iter > 10
        c
      else
        c = hashed_trans[char].pick
        c == '{' ? "" : c + word.call(iter + 1, c)
      end
    }
    

    注: 其中使用了一个叫做 pickup 的 gem, 原因是我懒得写随机生成的算法了.

    来输出一段话吧:

    10.times.map { word.call }.join(" ")
    
    thice baddicana b ran gug on f ronstintotag veacadonamaw erarivore
        

    感觉有点样子了.

不过这样的方法还是太粗鲁了一些. (有一种概率论里面的无记忆性掷色子的感觉. ) 一些可能可以改进的方向:

  • 增加对前一字母的判断
  • 细化对字母的一些处理, 比如上面的计数部分就做得不是很好
  • 增加对开始标记的处理 (我真的不想再重新写了… )

和上面的题外话类似的, 也能够根据上面的方式来进行处理生成.

[2023-1-25]: 先暂停一下, 去搞一个

(乐: 这个就是一个例题里面的一个东西. )

[2023-2-21]: 回来继续

FSTN in LISP

使用代码来储存 FSTN 的结构

注: 如果有时间的话, 可以考虑直接用 defmarco 来实现类似的操作. 并且为了防止占用 #, 在 clisp 里面用 |#| 的形式来描述.

(defvar haha-fstn "FSTN for haha! ")
(setq haha-fstn
      '((Initial (1))
        (Final (3))
        (From 1 to 2 by h)
        (From 2 to 3 by a)
        (From 3 to 1 by !)))

(defvar swahili-1 "FSTN for Swahili. ")
(setq swahili-1
      '((Initial (1))
        (Final (5))
        (From 1 to 2 by subj)
        (From 2 to 3 by tense)
        (From 3 to 4 by obj)
        (From 4 to 5 by stem)))

(defvar abbreviations)
(setq abbreviations
      '((subj ni u a tu wa)
        (obj ni ku m tu wa)
        (tense ta na me li)
        (stem penda piga sumbua lipa)))
一些数据结构的其他代码
;;; The Network Rules
(defun initial-nodes (network)
  "Find Initial nodes in NETWORK. "
  (nth 1 (assoc 'Initial network)))

(defun final-nodes (network)
  "Find Final nodes in NETWORK. "
  (nth 1 (assoc 'Final network)))

(defun transitions (network)
  "Find Transitions for NETWORK. "
  (cddr network))

;;; The Transistion Rules
(defun trans-node (transition)
  "Get From node from the TRANSITION. "
  (getf transition 'From))

(defun trans-newnode (transition)
  "Get To node from the TRANSISTION. "
  (getf transition 'To))

(defun trans-label (transition)
  "Get By node from the TRANSITION. "
  (getf transition 'by))

注: 这里有一个小小的吐槽. 这样的话就相当于把一个 FSTN 的形式给写死了. (指 transitions 的代码, 相当于是认为 FSTN 的前两行就是 Initial 和 Final, 然后后面的全都是转移关系.

根据 FSTN 的历遍机器 - Recognizer [低端版本]

先定义算法的过程:

  1. 根据可能的 Initial nodes 建立一组可能的状态 (initial pool)
  2. 执行如下操作:
    1. 选择其中的一个可能的状态 (将其从 pool 中移除)
    2. 如果它处于 Final 状态, 则停止并说明识别成功, 否则执行下面的操作:
      1. 计算它下一步可能的状态
      2. 将那些可能的状态放到 initial pool 里面
  3. 如果可选的 pool 被清空了, 那么就说明识别不成功

那么一个可能的状态长什么样呢? 即希望通过: (state , remaining input) 的形式来记录. 这个计算下一步可能的状态的话, 分为三种情况:

  • 若 label 和 remaining input 的符合
  • 若 abbreviate 和 remaining input 符合
  • 若 label 是 #

于是代码如下:

(defun recognize (network tape)
  "Test if the TAPE matches NETWORK, return t if true, nil otherwise. "
  (catch 'stop
    (dolist (initial (initial-nodes network))
      ;; Iterating the inital pool
      (recognize-next initial tape network))
    ;; return nil if fails
    nil))

(defun recognize-next (state tape network)
  "Iterate the tape by shorten the TAPE, move to next state by NETWORK. 
If TAPE is empties and in final STATE, meaning case matched, it will throw 'stop."
  (if (and (member state (final-nodes network))
           (null tape))                 ; final state and empty tape
      (throw 'stop t)                   ; success and abort
      (dolist (transition (transitions network))
        (if (equal state (trans-node transition)) ; select start
            ;; for every possible next-nodes
            (dolist (newtape (recognize-move (trans-label transition) tape))
              (recognize-next (trans-newnode transition) newtape network))))))

(defun recognize-move (label tape)
  "Return a list of possible new tape by label. "
  (if (equal (car tape) label)          ; first of tape matches label 
      (list (cdr tape))
      (if (member (car tape) (assoc label abbreviations))
          (list (cdr tape))             ; if matches abbreviations
          (if (equal label '|#|)        ; jump if #
              (list tape)
              '()))))                   ; return nil if all fails
注: 关于这个代码的一些注释

在计科导里面我们实现了一个图灵机, 这里的状态转移基本上就非常像图灵机了, 只是规则已经固定且形式上是一个变短的形式.

不过现在还没有搞定如何处理位置和无限长度纸带的一个写法, 之后有时间再试试看用 Common Lisp 来写一个图灵机.

不过这个我觉得实在有点不太合理, 因为如果要按照上面的算法来进行的话, 我们输入的文本就有很多的要求, 比如我们对于一个 Swahili 的语言, 就需要提前分词: '(wa me tu lipa) 否则就无法处理. 这显然是和一开始的想法是违背的吧, 毕竟一般可能会希望通过 'wametulip 这样直接的形式来.

一个可能的想法就是将 Abbreviations 通过展开的方式, 来替代提前分割的一个做法.

并且还有一个问题就是这个问题里面的算法, 如果对于很多分支的话, 就会因为搜索空间过大而搜索无力而死机了. 这样就非常痛苦. 现实世界中这样的单词空间可以说是成百上千的量级了, 如果不能够解决的话, 就会有一个搜索爆炸的问题.

关于这个的话, 我觉得 Steven Wolfram 的一个 视频 应该讲得挺好的, 大致的一个思路就是通过概率的方式来减少历遍所有的可能域. 而通过谈话模型来缩小可能的空间或者改变倾向. 有点像是蚁群算法, 或者和我之前整的那个完全随机的那个单词生成的感觉很像, 但是需要更加精细的操作.

而生成的代码就变得更加简单一点了:

(defun generate (network)
  "Print all possible tapes by NETWORK. "
  (dolist (initial (initial-nodes network))
    (generate-next initial '() network)))

(defun generate-next (node tape network)
  "Append the TAPE and change NODE by NETWORK. "
  (if (member node (final-nodes network))
      (print tape)
      (dolist (transition (transitions network))
        (if (equal node (trans-node transition))
            (generate-move transition tape network))
        '())))

(defun generate-move (transition tape network)
  (let ((by (trans-label transition))
        (to (trans-newnode transition)))
    (cond ((equal by '|#|)
           (generate-next to tape network))
          ((assoc by abbreviations)
           (dolist (pattern (rest (assoc by abbreviations)))
             (generate-next to (generate-append tape pattern) network)))
          (t
           (generate-next to (generate-append tape by) network)))))

(defun generate-append (list value)
  (append list (list value)))

注: 这样的话如果有那种死循环的 FSTN. 那么你运行的时候就会非常痛苦.

(setq deadly-fstn
      '((Initial 1)
        (Final 3)
        (From 1 to 2 by h)
        (From 2 to 1 by a)
        (From 2 to 3 by -)))

一个可能的做法就是加入栈深度检查, 只要过深就退出, 或者别的什么之类的. 并且这个程序也不能够让 Final node 上的节点去选择其他可能的点.

对上面网络的一个简单解释 (无 # 分支版本):

  1. 若当前状态不是 Final State:
    • 若纸带为空, 则退出 (nil)
    • 若纸带非空, 则:
      • 若满足纸带匹配缩写规则或者缩写规则, 那么就按照规则缩短纸带
      • 若不满足任何的以上两种规则, 就退出 (nil)
  2. 若当前状态是 Final State
    • 若纸带为空, 则停止查找 (stop)
    • 若纸带非空, 则退出 (nil)
  3. 因为纸带长度有限, 且因为不存在 #, 即纸带长度总会减少, 所以纸带总会为空, 若纸带为空: 若当前状态是 Final State, 则停止 (stop); 若当前状态不是 Final State, 则退出 (nil).
  4. 综上, 是成立的.

Finite-state Transducer

An FST is a more interesting kind of FSA that allows a string of output symbols to be produced as an input string is recognized.

就是什么呢, 如果看过上面的一些注释或者看过代码的话, 肯定会对这个代码感到十分不爽, 因为只能认证, 或者说识别, 没法做更多的事情.

于是就有了 Finite-State Transducer 的一个需求, 即通过 FSTN 的规则来将将输入转换为输出. (有点像是带有写功能的图灵机? 很像啊. )

于是有一个简单的记法:

Name ENG_FRE:
  Initial 1
  Final 5
  From 1 to 2 by WHERE
  From 2 to 3 by BV
  From 3 to 4 by DET-FEMN
  From 4 to 5 by N-FEMN
  From 3 to 6 by DET-MASC
  From 6 to 5 by N-MASC.

WHERE abbreviates:
  where_ou.
BV abbreviates:
  is_est.
DET-FEMN abbreviates:
  the_la.
DET-MASC abbreviates:
  the_le.
N-FEMN abbreviates:
  exit_sortie, shop_boutique, toliet_toilette.
N-MASC abbreviates:
  policeman_gendarme. 

上面的例子实际上就是一个简单的 English-French 的翻译 FSTN. 其中通过 from_to 的形式来记录 arc, 表示将读到的 from 转换为 to.

于是在 Lisp 表述里面, 将 from_to(from to) 的 List 形式来记录.

大概类似这样吧
(defvar eng-fre "English-French FSTN. ")
(setq eng-fre
      '((Initial (1))
        (Final (5))
        (From 1 to 2 by WHERE)
        (From 2 to 3 by BV)
        (From 3 to 4 by DET-FEMN)
        (From 4 to 5 by DET-MASC)
        (From 3 to 6 by DET-MASC)
        (From 6 to 5 by N-MASC)))

(setq abbreviations
      '((WHERE (where ou))
        (BE (is est))
        (DET-FEMN (the la))
        (DET-MASC (the le))
        (N-FEMN (exit sortie) (shop boutique) (toliet toilette))
        (N-MASC (policeman gendarme))))

那么前面的 generaterecognize 的函数实际上现在就是被重新合在一起了. 通过 recognize 来读取, 再通过 generate 来转换并生成新的结果.

[2023/02/24]: 先滚去学一点 Lisp, 不然写代码太难受了.

附录

Common Lisp 补注

可以参考的一个 在线文档.

Common Lisp 补注

简单的数据结构和类型

嘿嘿, 想不到吧, 在 Lisp (Common Lisp) 里面, 只要用 List 就能够干翻一切乐.

是否厌倦了在 List 里面使用 car, cdr? 试试 nth?

(nth 1 '(0 1 2 3 4))

比如要一个 Hash 表?

(assoc 'key
       '((key value list)
         (other-key value list and so on)))

Debugging and Testing

平时写代码的时候我都忽略了一点, 就是如何调试和测试. (尤其是后者)

在 Lisp 里面有 各种调试的方法, 其中我认为比较好用的 (我会的):

  • trace 以及 untrace, 可以记录 (或取消) 一个方法的调用的参数

黑话表

ATNaugmented transition network
CF-PSGcontext-free phrase structure grammar
CFLcontext-free language
DAGdirected acyclic graph
DBQdatabase query (language)
DCGdefinite clause grammar
FSAfinite-state automaton
FSTfinite-state transducer
FSTNfinite-state transition network
MRLmeaning representation language
MTmachine translation
NATRnetwork and transducer representation
PApushdown automation
PTpushdown transducer
RTNrecursive transition network
WFCword form clause
WFSTwell-formed substring table

一些无关的代码

简单的自动机绘图代码

简单的自动机绘图代码

nodes = /(Initial|Final)\s+((((\w+),\s*)|\w+)+)/
arcs = /From\s+(\w+)\sto\s+(\w+)\s+by\s+(\w+|#)/

res = "  rankdir = LR;\n" + \
      "  node [shape = point]; qi qa;\n" + \
      "  node [shape = circle];\n"

raw.each_line do |line|
  if line.match(nodes)
    m = Regexp.last_match
    type, node_names = m[1], m[2]

    if type == "Initial"
      node_names.split(/,\s*/).each do |node_name|
        res << "  qi -> #{node_name};\n" unless node_name.empty?
      end
    else # type == "Final"
      node_names.split(/,\s*/).each do |node_name|
        res << "  #{node_name} -> qa;\n" unless node_name.empty?
      end
    end
  elsif line.match(arcs)
    m = Regexp.last_match
    from, to = m[1], m[2]

    if m[3] == '#'
      label = ";\n"
    else
      label = " [label = \"#{m[3]}\"];\n"
    end
    res << "  \"#{from}\" -> \"#{to}\"#{label}"
  end
end

return res
FSTN Parser

FSTN Parser

一个简单的 FSTN Parser 在 Ruby and EBNF 里面介绍了.