如何在没有文档上下文的情况下对空文本进行去标记?

How to detokenize spacy text without doc context?

我有一个序列到序列模型,该模型是在 spacy 的标记化形成的标记上训练的。这既是编码器又是解码器。

输出是来自 seq2seq 模型的标记流。我想对文本进行去标记以形成自然文本。

示例:

Seq2Seq 的输入: 一些文本

Seq2Seq的输出:这行不通。

spacy 中是否有任何 API 可以根据其分词器中的规则来反向分词?

spaCy 在内部跟踪一个布尔数组以判断标记是否有尾随白色space。您需要此数组将字符串重新组合在一起。如果您使用的是 seq2seq 模型,则可以分别预测 space。

James Bradbury(TorchText 的作者)就此向我抱怨。他是对的,我在 spaCy 中设计标记化系统时没有考虑 seq2seq 模型。他开发了revtok来解决他的问题。

基本上,revtok 所做的(如果我理解正确的话)是将两个额外的位打包到词素 ID 上:词素是否对前面的 space 有亲和力,以及它是否对后面的 space。在词位都具有 space 亲和性的标记之间插入空格。

这是为 spaCy Doc 查找这些位的代码:

def has_pre_space(token):
    if token.i == 0:
        return False
    if token.nbor(-1).whitespace_:
        return True
    else:
        return False

def has_space(token):
    return token.whitespace_

诀窍是当 当前词素表示 "no trailing space" 时,你删除 space下一个词位表示 "no leading space"。这意味着您可以使用频率统计来决定这两个词位中的哪一个 "blame" 缺少 space。

James 的观点是,该策略为单词预测决策增加了很少的熵。替代方案将使用 hello."Hello 等条目扩展词典。他的方法两者都没有,因为您可以将字符串 hello. 编码为 (hello, 1, 0), (., 1, 1)(hello, 1, 0), (., 0, 1)。这个选择很简单:我们肯定要 "blame" 缺少 space 的时期。

长话短说;博士 我已经编写了一个尝试执行此操作的代码,代码段如下。


另一种计算复杂度为 O(n^2) * 的方法是使用我刚刚编写的函数。 主要想法是 “spaCy 分裂的东西,应该重新加入一次!”

代码:

#!/usr/bin/env python                     
import spacy     
import string

                    
                                                                                              
                                               
class detokenizer:                                                                            
    """ This class is an attempt to detokenize spaCy tokenized sentence """
    def __init__(self, model="en_core_web_sm"):             
        self.nlp = spacy.load(model)
     
    def __call__(self, tokens : list):
        """ Call this method to get list of detokenized words """                     
        while self._connect_next_token_pair(tokens):
            pass              
        return tokens                                                                         
                                               
    def get_sentence(self, tokens : list) -> str:                                                                                                                                            
        """ call this method to get detokenized sentence """            
        return " ".join(self(tokens))
                                               
    def _connect_next_token_pair(self, tokens : list):                  
        i = self._find_first_pair(tokens)
        if i == -1:                                                                                                                                                                          
            return False                                                                                                                 
        tokens[i] = tokens[i] + tokens[i+1]                                                   
        tokens.pop(i+1)                                                                                                                                                                       
        return True                                                                                                                                                                          
                                                                                                                                                                                             
                                                                                                                                                                                             
    def _find_first_pair(self,tokens):                                                                                                                                                       
        if len(tokens) <= 1:                                                                                                                                                                 
            return -1                                                                         
        for i in range(len(tokens)-1):
            if self._would_spaCy_join(tokens,i):                                
                return i
        return -1                                                                             
                                               
    def _would_spaCy_join(self, tokens, index):                                       
        """             
        Check whether the sum of lengths of spaCy tokenized words is equal to the length of joined and then spaCy tokenized words...                                                                  
                        
        In other words, we say we should join only if the join is reversible.          
        eg.:             
            for the text ["The","man","."]
            we would joins "man" with "."
            but wouldn't join "The" with "man."                                               
        """                                    
    left_part = tokens[index]
    right_part = tokens[index+1]
    length_before_join = len(self.nlp(left_part)) + len(self.nlp(right_part))
    length_after_join = len(self.nlp(left_part + right_part))
    if self.nlp(left_part)[-1].text in string.punctuation:
        return False
    return length_before_join == length_after_join 

用法:

import spacy                           
dt = detokenizer()                     

sentence = "I am the man, who dont dont know. And who won't. be doing"
nlp = spacy.load("en_core_web_sm")      
spaCy_tokenized = nlp(sentence)                      

string_tokens = [a.text for a in spaCy_tokenized]           

detokenized_sentence = dt.get_sentence(string_tokens)
list_of_words = dt(string_tokens)

print(sentence)    
print(detokenized_sentence)
print(string_tokens)
print(list_of_words)

输出:

I am the man, who dont dont know. And who won't. be doing
I am the man, who dont dont know. And who won't . be doing
['I', 'am', 'the', 'man', ',', 'who', 'do', 'nt', 'do', 'nt', 'know', '.', 'And', 'who', 'wo', "n't", '.', 'be', 'doing']
['I', 'am', 'the', 'man,', 'who', 'dont', 'dont', 'know.', 'And', 'who', "won't", '.', 'be', 'doing']

缺点:

在这种方法中,您可以轻松合并“do”和“nt”,以及在点“.”之间去掉 space。和前面的词。 这种方法并不完美,因为有多种可能的句子组合会导致特定的 spaCy 标记化。

我不确定当你只有 spaCy 分隔文本时是否有一种方法可以完全取消标记一个句子,但这是我所拥有的最好的方法。


在 Google 上搜索了几个小时后,只出现了几个答案,这个堆栈问题在 chrome 的 3 个选项卡上打开了 ;),它只写了基本上 “不要使用 spaCy,使用 revtok”。由于我无法更改其他研究人员选择的标记化,我不得不开发自己的解决方案。希望对某人有所帮助 ;)