神经翻译笔记3扩展e第2部分. Subword

NMT Tutorial 3扩展e第2部分. Subword

序言

按照布隆菲尔德的理论,词被认为是人类语言中能自行独立存在的最小单位,是“最小自由形式”。因此,对西方语言做NLP时,以词为基石是一个很自然的想法(甚至对于汉语这种没有明显词界限的语言来说,分词也成为了一个重要的工作)

但是,将某个语言的词穷举出来是不太现实的。首先,名词、动词、形容词、副词这四种属于开放词类,总会有新的词加入进来。其次,网络用语会创造出更多新词,或者为某个词给出不规则的变形。最后,以德语为代表的语言通常会将几个基本词组合起来,形成一个复合词,例如Abwasserbehandlungsanlage “污水处理厂”可以被细分为Abwasser、behandlungs和Anlage三个部分。韩语、日语等黏着语里这种现象更加明显(需要说明的是,尽管德语里存在着这种黏着语构词的方法,但是传统意义上不把德语看作是黏着语的一种)。即便是存在某个语言能获得其完整词表,词表的数量也会非常庞大,使得模型复杂度很高,训练起来很难。对于以德语、西班牙语、俄语为代表的屈折语,也会存在类似的问题(例如西班牙语动词可能有80种变化)。因此,在机器翻译等任务中,从训练语料构造词表时,通常会过滤掉出现频率很低的单词,并将这些单词统一标记为UNK。根据Zipf定律,这种做法能筛掉很多不常见词,简化模型结构,而且可以起到部分防止过拟合的作用。此外,模型上线做推断时,也有很大概率会遇到在训练语料里没见过的词,这些词也会被标为UNK。所有不在词表里被标记为UNK的词,通常被称作集外词(OOV)或者未登录词

对未登录词的处理是机器翻译领域里一个十分重要的问题。[Sennrich2016]认为,对于某些未登录词的翻译可能是”透明“的,包括

  • 命名实体,例如人名、地名等。对于这些词,如果目标语言和源语言的字母体系相同,可能可以直接抄写;如果不同,需要做些转写。例如将英语的Barack Obama转写成俄语的Барак Обама
  • 借词,可以通过字母级别的翻译做到,例如将claustrophobia翻译成德语的Klaustrophobie和俄语的Клаустрофобия
  • 词素复杂的词,例如通过组合或者屈折变化得到的词,可以将其进一步拆分为词素,通过分别翻译各个词素的得到结果。例如将英语的solar system翻译成德语的Sonnensystem或者匈牙利语的Naprendszer

因此,将词拆分为更细粒度的subword(有时被译为“子词”),可以有助于机器翻译效果的提升。文章同时指出使用一种称为“比特对编码”(Byte Pair Encoding——BPE)的算法可以将词进一步划分,但是BPE对单词的划分是纯基于统计的,得到的subword所蕴含的词素,或者说形态学信息,并不明显。因此,本文对一种基于形态学的分词器Morfessor也做了一个介绍。Morfessor使用的是无监督学习的方法,能达到不错的准确率。最后,本文会重点介绍2016年FAIR提出的一种基于subword的词嵌入表示方法fastText

除去subword方法以外,还可以将词拆成字符,为每个字符训练一个字符向量。这种方法很直观,也很有效,不过无需太费笔墨来描述。关于字符向量的优秀工作,可以参考[Bojanowski2015]的“相关工作”部分。

分词方法介绍

BPE

原理与算法

BPE算法[Gage1994]的本质实际上是一种数据压缩算法。数据压缩的一般做法都是将常见比特串替换为更短的表示方法,而BPE也不例外。更具体地说,BPE是找出最常出现的相邻字节对,将其替换成一个在原始数据里没有出现的字节,一直循环下去,直到找不到最常出现的字节对或者所有字节都用光了为止。打包数据之前,算法会写出字节对的替换表。例如,对"lwlwlwlwrr"使用BPE算法,会先把lw替换为a,得到"aaaarr",然后把"aa"替换为"b",得到"bbrr"。此时所有相邻字节对"bb"、“br”、"rr"的出现次数相等,迭代结束,输出替换表{“b” -> “aa”, “a” -> “lw”}

对于压缩算法而言,这种做法还不够,需要考虑更多细节。但是对于分词而言,上述流程已经足够了(实际上,真正使用时也没有什么替换表之类的)。将BPE用作分词时,先将词表里的所有单词展开成字符序列,并在末尾加一个特殊标记</w>,以恢复成原有的标识符。然后,也是对所有字符对做计数,找到出现最频繁的(例如(“A”, “B”)),将其合并得到新的符号"AB",看做一个“字符”,如此往复。因此,最后词表大小是原始词表大小+合并操作的次数。为了效率起见,BPE不考虑跨词的字符组合。BPE算法的核心学习过程可以写做如下Python代码

import re
import collections


def get_stats(vocab):
    pairs = collections.defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols) - 1):
            pairs[symbols[i], symbols[i + 1]] += freq
    return pairs


def merge_vocab(pair, v_in):
    v_out = {
   }
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out


def main():
    vocab = {
   'l o w</w>': 5, 'l o w e r</w>': 2,
             'n e w e s t</w>': 6, 'w i d e s t</w>': 3}
    num_merges = 10
    for i in range(num_merges):
        pairs = get_stats(vocab)
        best = max(pairs, key=pairs.get)
        vocab = merge_vocab(best, vocab)
        print(best)


if __name__ == '__main__':
    main()

测试阶段,对未登录词,可以将其先拆分成字符序列,然后用前面学到的组合方式将字符组成更大的,已知的符号。核心算法如下:

def encode(word, bpe_codes):
    pairs = get_pairs(word)  # went -> {('w', 'e'), ('e', 'n'), ('n', 't</w>')}
    while True:
        获取频率最低的二元组。如果所有二元组都不在bpe_codes里,跳出循环
        将pairs中对应的二元组合并,得到new_word # 如果('e', 'n') -> 'en',则得到('w', 'en', 't</w>')
        如果new_word是一元组(所有单词合并到一起),跳出循环。否则更新pairs

由于未登录词通常会被这种方法拆成若干个subword,因此通常会向不在原来词尾的subword后面写明一个分隔符,通常是@@。例如,假如said这个词不在词表里,encode以后得到('sa', 'i', 'd'),那么输出会是sa@@ i@@ d

使用

可以用命令pip install subword-nmt安装包subword-nmt以后,可以使用如下代码得到BPE的分词结果,以及将BPE的分词方法用到测试语料上

from subword_nmt import apply_bpe, learn_bpe


with open('../data/toy_vocab.txt', 'r', encoding='utf-8') as in_file, \
        open('../data/toy_bpe.txt', 'w+', encoding='utf-8') as out_file:
    # 得到分词结果,写到../data/toy_bpe.txt这个文件中
    # 1500是最后BPE词表大小,is_dict说明输入文件是个词表文件,格式为"<单词> <次数>"
    learn_bpe.learn_bpe(in_file, out_file, 1500, verbose=True, is_dict=True)

with open('../data/bpe_test_raw.txt', 'r', encoding='utf-8') as in_file, \
        open('../data/bpe_test_processed.txt', 'w+', encoding='utf-8') as out_file, \
        open('../data/toy_bpe.txt', 'r', encoding='utf-8') as code_file:
    # 构造BPE词表
    bpe = apply_bpe.BPE(code_file)
    for line in in_file:
        # 使用BPE分词
        out_file.write(bpe.process_line(line))

Morfessor

BPE是一套很经典也很常用的获取subword的方法,但是美中不足的是该方法完全基于频率统计,因此分出来的subword可能不太包含语法含义。可以考虑将词从语法角度做进一步划分,拆成语素。由于语素是最小的语法单位,是最小的语音语义结合体,因此它们本身比字符承载了更多的语法含义,同时比词更灵活。很多工作,例如[Botha2014] [Cui2015] [Luong2013] [Qiu2014]都采取了这样的思路,这些工作在划分语素时都使用了Morfessor [Creutz2007] 这个工具,因此本节主要讨论Morfessor的原理。本文主要遵从2.0版本文档[Virpioja2013]的脉络,而非原始论文,因此这里主要讨论Morfessor Baseline的2.0实现,没有涵盖Morfessor Categories-MAP的内容。

术语

原文提了三个基本概念,分别是compounds, constructions和atoms。考虑到这三个词比较抽象,不太好找到中文里对应的词,本文所关注的又只是词语划分到词素的过程,因此下文使用的是原文里提供的对应关系:

  • compounds,对应于原始词
  • constructions,对应于划分得到的词素
  • atoms,对应于字母

需要注意的是,Morfessor能做的不只是将词划分为词素,还可以从句子中得到短语,所以compounds等概念在别的任务中,会有不同的具体含义

方法

所有Morfessor的模型包括两个部分,有词典(lexicon)和语法(grammar)。前者存储词素的属性,后者确定如何将词素组成词。Morfessor有两个基本假设

  • 词由一个或多个词素构成,单个词中所含词素的上限是该词包含的字母个数
  • 组成词的词素是独立出现的。这个假设虽然与事实相悖而且是可能的改进点,但是会使算法变得简单

模型的损失函数来自于对模型的最大后验概率估计(MAP估计),分为两个部分:模型似然(来自于前面的基本假设)和先验概率(决定模型中词典的概率)。训练算法总体上来讲是使用贪婪算法和局部搜索策略,而解码使用了维特比(Viterbi)算法

模型与损失函数

Morfessor使用的是生成概率模型,对给定的参数 θ ​ \boldsymbol{\theta}​ θ,产生词 W ​ W​ W和分解结果 A ​ A​ A(原文称为analysis)。对词 w ​ w​ w,其分解结果 a ​ \boldsymbol{a}​ a可以通过分词函数 ϕ ​ \phi​ ϕ获得
a = ϕ ( w ; θ ) \boldsymbol{a} = \phi(w;\boldsymbol{\theta}) a=ϕ(w;θ)
此时 a \boldsymbol{a} a可以看作是一串词素组成的序列 a = ( m 1 , ⋯ &ThinSpace; , m n ) \boldsymbol{a} = (m_1, \cdots, m_n) a=(m1,,mn) w w w可以通过对 a \boldsymbol{a} a做合词操作 ϕ − 1 \phi^{-1} ϕ1重新获得。一般情况下, ϕ − 1 \phi^{-1} ϕ1只是一个简单的连接操作,不需要模型

模型的目标是要对观察到的训练数据 D W \boldsymbol{D}_W DW寻找最可能的参数 θ \boldsymbol{\theta} θ
θ M A P = a r g max ⁡ θ p ( θ ∣ D W ) = a r g max ⁡ θ p ( θ ) p ( D W ∣ θ ) \boldsymbol{\theta}_{\rm MAP} = \mathop{ {\rm arg}\max}_{\boldsymbol{\theta}} p(\boldsymbol{\theta}|\boldsymbol{D}_W) = \mathop{ {\rm arg}\max}_{\boldsymbol{\theta}} p(\boldsymbol{\theta})p(\boldsymbol{D}_W|\boldsymbol{\theta}) θMAP=argmaxθp(θDW)=argmaxθp(θ)p(DWθ)

似然

假设 D W \boldsymbol{D}_W DW 是训练数据,包含 N N N个单词和单词之间的分界( # w \#w #w)。假设每个词出现的概率是独立的,则对数似然为
log ⁡ p ( D W ∣ θ ) = ∑ j = 1 N log ⁡ p ( W = w j ∣ θ ) = ∑ j = 1 N log ⁡ ∑ a ∈ Φ ( w j ) p ( A = a ∣ θ ) \begin{aligned} \log p(\boldsymbol{D}_W|\boldsymbol{\theta}) &amp;= \sum_{j=1}^N \log p(W=w_j|\boldsymbol{\theta}) \\ &amp;= \sum_{j=1}^N \log \sum_{\boldsymbol{a} \in \Phi(w_j)} p(A=\boldsymbol{a}|\boldsymbol{\theta}) \end{aligned} logp(DWθ)=j=1Nlogp(W=wjθ)=j=1NlogaΦ(wj)p(A=aθ)
其中 Φ ( w ) = { a : ϕ − 1 ( a ) = w } \Phi(w) = \{\boldsymbol{a} : \phi^{-1}(\boldsymbol{a}) = w\} Φ(w)={ a:ϕ1(a)=w}。假设变量 Y \boldsymbol{Y} Y为训练数据中的每个单词 w j w_j wj分配一个 Φ ( w j ) \Phi(w_j) Φ(wj)中的分词序列,即 Y = ( y 1 , ⋯ &ThinSpace; , y N ) \boldsymbol{Y}= (\boldsymbol{y}_1, \cdots, \boldsymbol{y}_N) Y=(y1,,yN),则目标函数变为
log ⁡ p ( D W ∣ θ , Y ) = ∑ j = 1 N log ⁡ p ( y j ∣ θ ) = ∑ j = 1 N log ⁡ p ( m j 1 , ⋯ &ThinSpace; , m j ∣ y j ∣ , # w ∣ θ ) \log p(\boldsymbol{D}_W|\boldsymbol{\theta}, \boldsymbol{Y}) = \sum_{j=1}^N \log p(\boldsymbol{y}_j|\boldsymbol{\theta}) = \sum_{j=1}^N \log p(m_{j1}, \cdots, m_{j|\boldsymbol{y}_j|}, \#_w|\boldsymbol{\theta}) logp(DWθ,Y)=j=1Nlogp(yjθ)=j=1Nlogp(mj1,,mjyj,#wθ)
由前面的基本假设2,上式可以简化为
log ⁡ p ( D W ∣ θ , Y ) = ∑ j = 1 N ( log ⁡ p ( # w ∣ θ ) + ∑ i = 1 ∣ y j ∣ log ⁡ p ( m j i ∣ θ ) ) \log p(\boldsymbol{D}_W|\boldsymbol{\theta},\boldsymbol{Y}) = \sum_{j=1}^N \left(\log p(\#_w|\boldsymbol{\theta}) + \sum_{i=1}^{|\boldsymbol{y}_j|}\log p(m_{ji}|\boldsymbol{\theta})\right) logp(DWθ,Y)=j=1Nlogp(#wθ)+i=1yjlogp(mjiθ)

先验

原始的Morfessor实现将模型参数分为两部分,就是前面说的词典 L ​ \mathcal{L}​ L和语法 G ​ \mathcal{G}​ G。本文介绍的Morfessor Baseline没有语法参数,所以参数先验 p ( θ ) = p ( L ) ​ p(\boldsymbol{\theta}) = p(\mathcal{L})​ p(θ)=p(L)。先验概率影响的结果是,对于某个词典,它存储的词素越短越少,则概率越高。对于词素 m i ​ m_i​ mi,如果 p ( m i ∣ θ ) &gt; 0 ​ p(m_i|\boldsymbol{\theta}) &gt; 0​ p(miθ)>0,那么说它是被存储了的。有 μ ​ \mu​ μ个词素的词典的概率是
p ( L ) = p ( μ ) × p ( p r o p e r t i e s ( m 1 ) , … , p r o p e r t i e s ( m μ ) ) × μ ! p(\mathcal{L}) = p(\mu) \times p({\rm properties}(m_1), \ldots, {\rm properties}(m_\mu)) \times \mu ! p(L)=p(μ)×p(properties(m1),,properties(mμ))×μ!
使用阶乘项的原因是 μ \mu μ个词素有 μ ! \mu ! μ!种排列方式,而对词典来说相同内容的不同排列都是等价的。 p ( μ ) p(\mu) p(μ)可以忽略掉

上式中的“属性”properties可以进一步划分为“形式”form和用法usage

  • “形式”指的是词素由哪些字母组成,被认为是独立的。词素 m i m_i mi的“形式”的概率分为两个部分,一个是长度分布 p ( L ) p(L) p(L),一个是字母序列 σ i \boldsymbol{\sigma}_i σi的类别分布 p ( C ) p(C) p(C)
    p ( σ i ) = p ( L = ∣ σ i ∣ ) ∏ j = 1 ∣ σ i ∣ p ( C = σ i j ) p(\boldsymbol{\sigma}_i) = p(L=|\boldsymbol{\sigma}_i|)\prod_{j=1}^{|\boldsymbol{\sigma}_i|}p(C=\boldsymbol{\sigma}_{ij}) p(σi)=p(L=σi)j=1σip(C=σij)

  • “用法”指的是每个词素的数量 τ i \tau_i τi ν = ∑ i τ i \nu = \sum_i \tau_i ν=iτi是所有词素的总数。可以通过 ν \nu ν来计算每个词素的最大似然估计: p ( m i ∣ θ ) = τ i / ( N + ν ) p(m_i|\boldsymbol{\theta}) = \tau_i / (N+\nu) p(miθ)=τi/(N+ν)。给定 μ \mu μ ν \nu ν,词素数量的先验为
    p ( τ 1 , … , τ μ ∣ μ , ν ) = 1 / ( ν − 1 μ − 1 ) p(\tau_1, \ldots, \tau_\mu | \mu, \nu) = 1/{\nu-1 \choose \mu-1} p(τ1,,τμμ,ν)=1/(μ1ν1)
    也就是说,每个词素序列的先验都相同

训练与解码算法

Morfessor使用了无监督/半监督学习方法来训练模型,支持批量训练和在线训练。批量训练可以使用全局搜索算法和局部搜索算法,而在线训练只能使用局部搜索算法

参数初始化

Morfessor 2.0的初始化方法是对每个单词的字母之间随机插入空格,将得到的词素放入词典,并对应地初始化 Y \boldsymbol{Y} Y

全局维特比算法

Morfessor没有使用HMM常用的前向-后向算法来求解,主要是因为可能的划分方式太多,计算量太大,而且没有封闭解析解。因此工具首先为每个单词找到最可能的分解结果 ϕ b e s t ( w ; θ ( t − 1 ) ) ​ \phi_{\rm best}(w;\boldsymbol{\theta}^{(t-1)})​ ϕbest(w;θ(t1)),然后更新参数来最小化损失函数
θ ( t ) = a r g min ⁡ θ { − log ⁡ p ( θ ) − log ⁡ ∏ j = 1 ∣ D W ∣ p ( ϕ b e s t ( w j ; θ ( t − 1 ) ) ∣ θ ) } \boldsymbol{\theta}^{(t)} = \mathop{ {\rm arg}\min}_{\boldsymbol{\theta}}\left\{-\log p(\boldsymbol{\theta}) - \log \prod_{j=1}^{|\boldsymbol{D}_W|}p\left(\phi_{\rm best}(w_j;\boldsymbol{\theta}^{(t-1)})|\boldsymbol{\theta}\right)\right\} θ(t)=argminθlogp(θ)logj=1DWp(ϕbest(wj;θ(t1))θ)
这里
ϕ b e s t ( w ; θ ) = a r g max ⁡ a p ( a ∣ w , θ ) = a r g max ⁡ m 1 , … , m n : w = m 1 … m n p ( m 1 , … , m n , # w ∣ θ ) \phi_{\rm best}(w;\boldsymbol{\theta}) = \mathop{ {\rm arg}\max}_{\boldsymbol{a}}p(\boldsymbol{a}|w,\boldsymbol{\theta}) = \mathop{ {\rm arg}\max}_{m1,\ldots,m_n: \\ w=m1\ldots m_n}p(m_1,\ldots,m_n,\#_w|\boldsymbol{\theta}) ϕbest(w;θ)=argmaxap(aw,θ)=argmaxm1,,mn:w=m1mnp(m1,,mn,#wθ)
可以进一步使用HMM的维特比算法求解,这里可观察序列是组成单词w的字母序列,隐藏状态是单词的词素。与传统维特比算法的不同在于,一个状态(即一个词素)可以覆盖多个观察值(多个字母)。选出到第 i i i个观察值的最优路径时,这条路径可能来自于第一到第 i − 1 i-1 i1个观察值中的任意一个,因此算法时间复杂度是 O ( ∣ w ∣

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值