<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【BERT】BERT 是如何分詞的

          共 7178字,需瀏覽 15分鐘

           ·

          2021-01-15 16:34

          來自 |?知乎

          地址 |?https://zhuanlan.zhihu.com/p/132361501

          作者 |?Alan Lee

          BERT 表示 Bidirectional Encoder Representations from Transformers,是 Google 于 2018 年發(fā)布的一種語言表示模型。該模型一經(jīng)發(fā)布便成為爭相效仿的對象,相信大家也都多少聽說過研究過了。本文主要聚焦于 BERT 的分詞方法,后續(xù)再談模型實(shí)現(xiàn)細(xì)節(jié)。

          BERT 源碼中?tokenization.py?就是預(yù)處理進(jìn)行分詞的程序,主要有兩個分詞器:BasicTokenizer?和?WordpieceTokenizer,另外一個?FullTokenizer?是這兩個的結(jié)合:先進(jìn)行?BasicTokenizer?得到一個分得比較粗的 token 列表,然后再對每個 token 進(jìn)行一次WordpieceTokenizer,得到最終的分詞結(jié)果。

          為了能直觀看到每一步處理效果,我會用下面這個貫穿始終的例子來說明,該句修改自 Keras 的維基百科介紹:

          example = "Keras是ONEIROS(Open-ended Neuro-Electronic Intelligent Robot Operating System,開放式神經(jīng)電子智能機(jī)器人操作系統(tǒng))項(xiàng)目研究工作的部分產(chǎn)物[3],主要作者和維護(hù)者是Google工程師Fran?ois Chollet。\r\n"
          對于中文來說,一句話概括:BERT 采取的是「分字」,即每一個漢字都切開。

          BasicTokenizer

          BasicTokenizer(以下簡稱 BT)是一個初步的分詞器。對于一個待分詞字符串,流程大致就是轉(zhuǎn)成 unicode -> 去除各種奇怪字符 -> 處理中文 -> 空格分詞 -> 去除多余字符和標(biāo)點(diǎn)分詞 -> 再次空格分詞,結(jié)束。

          大致流程就是這樣,還有很多細(xì)節(jié),下面我依次說下。

          轉(zhuǎn)成 unicode

          轉(zhuǎn)成 unicode?這步對應(yīng)于?convert_to_unicode(text)?函數(shù),很好理解,就是將輸入轉(zhuǎn)成 unicode 字符串,如果你用的 Python 3 而且輸入是?str?類型,那么這點(diǎn)無需擔(dān)心,輸入和輸出一樣;如果是 Python 3 而且輸入類型是?bytes,那么該函數(shù)會使用?text.decode("utf-8", "ignore")?來轉(zhuǎn)成 unicode 類型。如果你用的是 Python 2,那么請看 Sunsetting Python 2 support 和 Python 2.7 Countdown ,Just drop it。

          經(jīng)過這步后,example?和原來相同:

          >>> example = convert_to_unicode(example)
          >>> example
          'Keras是ONEIROS(Open-ended Neuro-Electronic Intelligent Robot Operating System,開放式神經(jīng)電子智能機(jī)器人操作系統(tǒng))項(xiàng)目研究工作的部分產(chǎn)物[3],主要作者和維護(hù)者是Google工程師Fran?ois Chollet。\r\n'

          去除各種奇怪字符

          去除各種奇怪字符對應(yīng)于 BT 類的?_clean_text(text)?方法,通過 Unicode 碼位(Unicode code point,以下碼位均指 Unicode 碼位)來去除各種不合法字符和多余空格,包括:

          Python 中可以通過?ord(c)?來獲取字符?c?的碼位,使用?chr(i)?來獲取碼位為?i?的 Unicode 字符,$0 \leq i \leq \text{0x10ffff}$,即十進(jìn)制的 $[0, 1114111]$。
          • 碼位為 0 的?\x00,即空字符(Null character),或叫結(jié)束符,肉眼不可見,屬于控制字符,一般在字符串末尾。注意不是空格,空格的碼位是 32

          • 碼位為 0xfffd(十進(jìn)制 65533)的??,即替換字符(REPLACEMENT CHARACTER),通常用來替換未知、無法識別或者無法表示的字符

          • 除?\t、\r?和?\n?以外的控制字符(Control character),即 Unicode 類別是?Cc?和?Cf的字符??梢允褂?unicodedata.category(c)?來查看?c?的 Unicode 類別。代碼中用_is_control(char)?來判斷?char?是不是控制字符

          • 將所有空白字符轉(zhuǎn)換為一個空格,包括標(biāo)準(zhǔn)空格、\t\r、\n?以及 Unicode 類別為?Zs的字符。代碼中用?_is_whitespace(char)?來判斷?char?是不是空白字符

          經(jīng)過這步后,example?中的?\r\n?被替換成兩個空格:

          >>> example = _clean_text(example)
          >>> example
          'Keras是ONEIROS(Open-ended Neuro-Electronic Intelligent Robot Operating System,開放式神經(jīng)電子智能機(jī)器人操作系統(tǒng))項(xiàng)目研究工作的部分產(chǎn)物[3],主要作者和維護(hù)者是Google工程師Fran?ois Chollet。'

          處理中文

          處理中文對應(yīng)于 BT 類的?_tokenize_chinese_chars(text)?方法。對于?text?中的字符,首先判斷其是不是「中文字符」(關(guān)于中文字符的說明見下方引用塊說明),是的話在其前后加上一個空格,否則原樣輸出。那么有一個問題,如何判斷一個字符是不是「中文」呢?

          _is_chinese_char(cp)?方法,cp?就是剛才說的碼位,通過碼位來判斷,總共有 81520 個字,詳細(xì)的碼位范圍如下(都是閉區(qū)間):

          • [0x4E00, 0x9FFF]:十進(jìn)制 [19968, 40959]

          • [0x3400, 0x4DBF]:十進(jìn)制 [13312, 19903]

          • [0x20000, 0x2A6DF]:十進(jìn)制 [131072, 173791]

          • [0x2A700, 0x2B73F]:十進(jìn)制 [173824, 177983]

          • [0x2B740, 0x2B81F]:十進(jìn)制 [177984, 178207]

          • [0x2B820, 0x2CEAF]:十進(jìn)制 [178208, 183983]

          • [0xF900, 0xFAFF]:十進(jìn)制 [63744, 64255]

          • [0x2F800, 0x2FA1F]:十進(jìn)制 [194560, 195103]

          其實(shí)我覺得這個范文可以再精簡下,因?yàn)橛袔讉€區(qū)間是相鄰的,下面三個區(qū)間:

          • [0x2A700, 0x2B73F]:十進(jìn)制 [173824, 177983]

          • [0x2B740, 0x2B81F]:十進(jìn)制 [177984,?178207]

          • [0x2B820, 0x2CEAF]:十進(jìn)制 [178208, 183983]

          可以精簡成一個:

          • [0x2A700, 0x2CEAF]:十進(jìn)制 [173824, 183983]

          原來的 8 個區(qū)間精簡成 6 個,至于原來為什么寫成 8 個,I don't know 啊

          關(guān)于「中文字符」的說明:按照代碼中的定義,這里說的「中文字符」指的是 CJK Unicode block 中的字符,包括現(xiàn)代漢語、部分日語、部分韓語和越南語。但是根據(jù) CJK Unicode block 中的定義,這些字符只包括第一個碼位區(qū)間([0x4E00, 0x9FFF])內(nèi)的字符,也就是說代碼中的字符要遠(yuǎn)遠(yuǎn)多于 CJK Unicode block 中包括的字符,這一點(diǎn)暫時有些疑問。我把源碼關(guān)于這塊的注釋引用過來如下:
          def _is_chinese_char(self, cp):
          """Checks whether CP is the codepoint of a CJK character."""
          # This defines a "chinese character" as anything in the CJK Unicode block:
          # https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block)
          #
          # Note that the CJK Unicode block is NOT all Japanese and Korean characters,
          # despite its name. The modern Korean Hangul alphabet is a different block,
          # as is Japanese Hiragana and Katakana. Those alphabets are used to write
          # space-separated words, so they are not treated specially and handled
          # like the all of the other languages.

          pass

          經(jīng)過這步后,中文被按字分開,用空格分隔,但英文數(shù)字等仍然保持原狀:

          >>> example = _tokenize_chinese_chars(example)
          >>> example
          'Keras 是 ONEIROS(Open-ended Neuro-Electronic Intelligent Robot Operating System, 開 放 式 神 經(jīng) 電 子 智 能 機(jī) 器 人 操 作 系 統(tǒng) ) 項(xiàng) 目 研 究 工 作 的 部 分 產(chǎn) 物 [3], 主 要 作 者 和 維 護(hù) 者 是 Google 工 程 師 Fran?ois Chollet。'

          空格分詞

          空格分詞對應(yīng)于?whitespace_tokenize(text)?函數(shù)。首先對?text?進(jìn)行?strip()?操作,去掉兩邊多余空白字符,然后如果剩下的是一個空字符串,則直接返回空列表,否則進(jìn)行?split()?操作,得到最初的分詞結(jié)果?orig_tokens。

          經(jīng)過這步后,example?變成一個列表:

          >>> example = whitespace_tokenize(example)
          >>> example
          ['Keras', '是', 'ONEIROS(Open-ended', 'Neuro-Electronic', 'Intelligent', 'Robot', 'Operating', 'System,', '開', '放', '式', '神', '經(jīng)', '電', '子', '智', '能', '機(jī)', '器', '人', '操', '作', '系', '統(tǒng)', ')', '項(xiàng)', '目', '研', '究', '工', '作', '的', '部', '分', '產(chǎn)', '物', '[3],', '主', '要', '作', '者', '和', '維', '護(hù)', '者', '是', 'Google', '工', '程', '師', 'Fran?ois', 'Chollet。']

          去除多余字符和標(biāo)點(diǎn)分詞

          接下來是針對?orig_tokens?的分詞結(jié)果進(jìn)一步處理,代碼如下:

          for token in orig_tokens:
          if self.do_lower_case:
          token = token.lower()
          token = self._run_strip_accents(token)
          split_tokens.extend(self._run_split_on_punc(token))

          邏輯不復(fù)雜,我在這里主要說下?_run_strip_accents?和?_run_split_on_punc。

          _run_strip_accents(text)?方法用于去除 accents,即變音符號,那么什么是變音符號呢?像 Keras 作者 Fran?ois Chollet 名字中些許奇怪的字符??、簡歷的英文 résumé 中的?é?和中文拼音聲調(diào)?á?等,這些都是變音符號 accents,維基百科中描述如下:

          附加符號或稱變音符號(diacritic、diacritical mark、diacritical point、diacritical sign),是指添加在字母上面的符號,以更改字母的發(fā)音或者以區(qū)分拼寫相似詞語。例如漢語拼音字母“ü”上面的兩個小點(diǎn),或“á”、“à”字母上面的標(biāo)調(diào)符。

          常見 accents 可參見 Common accented characters。

          _run_strip_accents(text)?方法就是要把這些 accents 去掉,例如?Fran?ois Chollet?變成Francois Chollet,résumé?變成?resume,á?變成?a。該方法代碼不長,如下:

          def _run_strip_accents(self, text):
          """Strips accents from a piece of text."""
          text = unicodedata.normalize("NFD", text)
          output = []
          for char in text:
          cat = unicodedata.category(char)
          if cat == "Mn":
          continue
          output.append(char)
          return "".join(output)

          使用列表推導(dǎo)式代碼還可以進(jìn)一步精簡為:

          def _run_strip_accents(self, text):
          """Strips accents from a piece of text."""
          text = unicodedata.normalize("NFD", text)
          output = [char for char in text if unicodedata.category(char) != 'Mn']
          return "".join(output)

          這段代碼核心就是?unicodedata.normalize?和?unicodedata.category?兩個函數(shù)。前者返回輸入字符串?text?的規(guī)范分解形式(Unicode 字符有多種規(guī)范形式,本文默認(rèn)指?NFD?形式,即規(guī)范分解),后者返回輸入字符?char?的 Unicode 類別。下面我舉例說明一下兩個函數(shù)的作用。

          假如我們要處理?āóǔè,其中含有變音符號,這種字符其實(shí)是由兩個字符組成的,比如?ā(碼位 0x101)是由?a(碼位 0x61)和 上面那一橫(碼位 0x304)組成的,通過unicodedata.normalize?就可以把這兩者拆分出來:

          >>> import unicodedata  # unicodedata 是內(nèi)置庫
          >>> s = 'āóǔè'
          >>> s_norm = unicodedata.normalize('NFD', s)
          >>> s_norm, len(s_norm)

          ('āóǔè', 8) # 看起來和原來的一摸一樣,但是長度已經(jīng)變了


          unicodedata.category

          用來返回各個字符的類別:

          >>> ' '.join(unicodedata.category(c) for c in s_norm)


          'Ll Mn Ll Mn Ll Mn Ll Mn'

          >Ll 類別 表示 Lowercase Letter,小寫字母。>Mn 類別 表示的是 Nonspacing Mark,非間距標(biāo)記,變音字符就屬于這類,所以我們可以根據(jù)類別直接去掉變音字符:

          >>> ''.join(c for c in s_norm if unicodedata.category(c) != 'Mn')
          'aoue'

          _run_split_on_punc(text)?是標(biāo)點(diǎn)分詞,按照標(biāo)點(diǎn)符號分詞。

          _run_split_on_punc(text)?方法是針對上一步空格分詞后的每個 token 的。

          在說這個方法之前,先說一下判斷一個字符是否是標(biāo)點(diǎn)符號的函數(shù):_is_punctuation(char)。該函數(shù)代碼不長,我放到下面:

          def _is_punctuation(char):
          """Checks whether `chars` is a punctuation character."""
          cp = ord(char)
          if ((cp >= 33 and cp <= 47) or (cp >= 58 and cp <= 64) or
          (cp >= 91 and cp <= 96) or (cp >= 123 and cp <= 126)):
          return True
          cat = unicodedata.category(char)
          if cat.startswith("P"):
          return True
          return False

          通常我們會用一個類似詞庫的文件來存放所有的標(biāo)點(diǎn)符號,而?_is_punctuation?函數(shù)是通過碼位來判斷的,這樣更靈活,也不必保留一個額外的詞庫文件。具體是有兩種情況會視為標(biāo)點(diǎn):ASCII中除了字母和數(shù)字意外的字符和以 P 開頭的 Unicode 類別中的字符。第一種情況總共有 32 個字符,如下:

          !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

          _run_split_on_punc?的總體過程就是:

          1. 1. 首先設(shè)置?start_new_word=True?和?output=[],output?就是最終的輸出

          2. 2. 對?text?中每個字符進(jìn)行判斷,如果該字符是標(biāo)點(diǎn),則?output.append([char]),并設(shè)置start_new_word=True

          3. 3. 如果不是標(biāo)點(diǎn)且?start_new_word=True,那么意味著這是新一段的開始,直接output.append([]),然后再設(shè)置?start_new_word = False,并在剛才 append 的空列表上加上當(dāng)前字符:output[-1].append(char)

          現(xiàn)在得到的?output?是一個嵌套列表,其中每一個列表都是被標(biāo)點(diǎn)分開的一段,最后把每個列表 join 拼接一下,拉平?output?即可。

          經(jīng)過這步后,原先沒有被分開的字詞標(biāo)點(diǎn)(例如?ONEIROS(Open-ended)、沒有去掉的變音符號(例如??)都被相應(yīng)處理:

          >>> example
          ['keras', '是', 'oneiros', '(', 'open', '-', 'ended', 'neuro', '-', 'electronic', 'intelligent', 'robot', 'operating', 'system', ',', '開', '放', '式', '神', '經(jīng)', '電', '子', '智', '能', '機(jī)', '器', '人', '操', '作', '系', '統(tǒng)', ')', '項(xiàng)', '目', '研', '究', '工', '作', '的', '部', '分', '產(chǎn)', '物', '[', '3', ']', ',', '主', '要', '作', '者', '和', '維', '護(hù)', '者', '是', 'google', '工', '程', '師', 'francois', 'chollet', '。']

          再次空格分詞

          這句對應(yīng)于如下代碼:

          output_tokens = whitespace_tokenize(" ".join(split_tokens))

          很簡單,就是先用標(biāo)準(zhǔn)空格拼接上一步的處理結(jié)果,再執(zhí)行空格分詞。(But WHY?)

          經(jīng)過這步后,和上步結(jié)果一樣:

          >>> example
          ['keras', '是', 'oneiros', '(', 'open', '-', 'ended', 'neuro', '-', 'electronic', 'intelligent', 'robot', 'operating', 'system', ',', '開', '放', '式', '神', '經(jīng)', '電', '子', '智', '能', '機(jī)', '器', '人', '操', '作', '系', '統(tǒng)', ')', '項(xiàng)', '目', '研', '究', '工', '作', '的', '部', '分', '產(chǎn)', '物', '[', '3', ']', ',', '主', '要', '作', '者', '和', '維', '護(hù)', '者', '是', 'google', '工', '程', '師', 'francois', 'chollet', '。']

          這就是 BT 最終的輸出了。

          WordpieceTokenizer

          WordpieceTokenizer(以下簡稱 WPT)是在 BT 結(jié)果的基礎(chǔ)上進(jìn)行再一次切分,得到子詞(subword,以?##?開頭),詞匯表就是在此時引入的。該類只有兩個方法:一個初始化方法__init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=200),一個分詞方法tokenize(self, text)。

          對于中文來說,使不使用 WPT 都一樣,因?yàn)橹形慕?jīng)過 BasicTokenizer 后已經(jīng)變成一個字一個字了,沒法再「子」了?

          __init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=200)vocab?就是詞匯表,collections.OrderedDict()?類型,由?load_vocab(vocab_file)?讀入,key 為詞匯,value 為對應(yīng)索引,順序依照?vocab_file?中的順序。有一點(diǎn)需要注意的是,詞匯表中已包含所有可能的子詞。unk_token?為未登錄詞的標(biāo)記,默認(rèn)為?[UNK]max_input_chars_per_word?為單個詞的最大長度,如果一個詞的長度超過這個最大長度,那么直接將其設(shè)為?unk_token。

          tokenize(self, text):該方法就是主要的分詞方法了,大致分詞思路是按照從左到右的順序,將一個詞拆分成多個子詞,每個子詞盡可能長。按照源碼中的說法,該方法稱之為 greedy longest-match-first algorithm,貪婪最長優(yōu)先匹配算法。

          開始時首先將?text?轉(zhuǎn)成 unicode,并進(jìn)行空格分詞,然后依次遍歷每個詞。為了能夠清楚直觀地理解遍歷流程,我特地制作了一個 GIF 來解釋,以?unaffable?為例:

          注:

          • 藍(lán)色底色表示當(dāng)前子字符串,對應(yīng)于代碼中的?cur_substr

          • 當(dāng)從第一個位置開始遍歷時,不需要在當(dāng)前字串前面加?##,否則需要

          大致流程說明(雖然我相信上面那個 GIF 夠清楚了):

          1. 1.? 從第一個位置開始,由于是最長匹配,結(jié)束位置需要從最右端依次遞減,所以遍歷的第一個子詞是其本身?unaffable,該子詞不在詞匯表中

          2. 2.? 結(jié)束位置左移一位得到子詞?unaffabl,同樣不在詞匯表中

          3. 3.? 重復(fù)這個操作,直到?un,該子詞在詞匯表中,將其加入?output_tokens,以第一個位置開始的遍歷結(jié)束

          4. 4.? 跳過?un,從其后的?a?開始新一輪遍歷,結(jié)束位置依然是從最右端依次遞減,但此時需要在前面加上?##?標(biāo)記,得到?##affable?不在詞匯表中

          5. 5.? 結(jié)束位置左移一位得到子詞?##affabl,同樣不在詞匯表中

          6. 6.? 重復(fù)這個操作,直到?##aff,該字詞在詞匯表中,將其加入?output_tokens,此輪遍歷結(jié)束

          7. 7.? 跳過?aff,從其后的?a?開始新一輪遍歷,結(jié)束位置依然是從最右端依次遞減。##able?在詞匯表中,將其加入?output_tokens

          8. able?后沒有字符了,整個遍歷結(jié)束

          將 BT 的結(jié)果輸入給 WPT,那么?example?的最終分詞結(jié)果就是

          ['keras', '是', 'one', '##iros', '(', 'open', '-', 'ended', 'neu', '##ro', '-', 'electronic', 'intelligent', 'robot', 'operating', 'system', ',', '開', '放', '式', '神', '經(jīng)', '電', '子', '智', '能', '機(jī)', '器', '人', '操', '作', '系', '統(tǒng)', ')', '項(xiàng)', '目', '研', '究', '工', '作', '的', '部', '分', '產(chǎn)', '物', '[', '3', ']', ',', '主', '要', '作', '者', '和', '維', '護(hù)', '者', '是', 'google', '工', '程', '師', 'franco', '##is', 'cho', '##llet', '。']

          至此,BERT 分詞部分結(jié)束。

          Reference

          • [1810.04805] BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

          • bert/tokenization.py at master · google-research/bert

          • How to replace accented characters in python? - Stack Overflow

          • What is the best way to remove accents in a Python unicode string? - Stack Overflow

          • Accents & Accented Characters - Fonts.com | Fonts.com

          • Common accented characters | Butterick’s Practical Typography

          掃描關(guān)注本號↓

          瀏覽 66
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  小美女操逼 | 欧美插穴网 | www一区二区处女 | 九草天堂在线视频 | 人人草人人玩 |