最近工作中文本处理任务特别多,今天特地看一下大小写转换。大小写转换对于文本的后处理很重要,如果做不好,句子看起来很ugly。一开始想通过端到端的方法做,后来想一想感觉不需要上神经网络模型。大小写本身就是跟“是否句子开头”、“是否命名实体”、“是否缩略词”等有关系。因此认为大小写转换过程应该走一个pipeline的流程,看了一些资料发现确实如此。
参考:https://www.cs.cmu.edu/~llita/papers/lita.truecasing-acl2003.pdf https://towardsdatascience.com/truecasing-in-natural-language-processing-12c4df086c21
论文tRuEcasIng 大小写对于NER、ASR后处理等任务很重要,比如“the president”和“the President”表达的是不同的含义。大小写跟文本的来源、作者的写作风格都有关系,为了对不同语料库中的文本进行统一归一化,可以使用Truecasing将大小写变为统一规范形式。
在以往相关工作中,关注到的是句首、引号后面和命名实体的大写。论文将词的大小写分为4类:全部小写(LC)、首字母大写(UC)、全部字母大写(CA)、混合大小写(MC)。论文中使用的方法可以关注到word的上下文信息。因此即便遇到UNK(如lenon),也会因为跟lenon有相同上下文的词常采用UC,而把lenon也记为UC类别。
如果我们使用Unigram语言模型的话,可能发现这样的问题:只有12%的词是有多种大小写表示的,那对于new这个词,new后面跟着的词中只有少部分(York、Zealand)是需要大写的,那么new这个词就很容易改写为小写的,使得后面是York时出错了(可以使用贪心法解码Unigram Model)。
为了避免Unigram模型的缺点,需要不仅仅考虑local context,还要考虑整个句子的语义。因此论文使用了HMM对整个句子进行建模和推理:
其中每个节点包含了如下信息:词的可能的truecase、词的语法信息、词在句子中位置信息、前面两个词的语法信息、前面两个词的truecase信息。也就是说,HMM的隐藏状态$\left(q_{1}, q_{2}, \cdots, q_{n}\right)$是词的大小写和上下文信息组合,观察状态$O_{1} O_{2} \cdots O_{t}$是词的lexical item,使用维特比进行求解:$q_{\tau}^{*}=\operatorname{argmax}_{q_{i 1} q_{i 2} \cdots q_{i t}} P\left(q_{i 1} q_{i 2} \cdots q_{i t} \mid O_{1} O_{2} \cdots O_{t}, \lambda\right)$转移概率lambda是语言模型特征的打分:
$\begin{aligned} P_{\text {model}}\left(w_{3} \mid w_{2}, w_{1}\right) &=\lambda_{\text {trigram}} P\left(w_{3} \mid w_{2}, w_{1}\right) \\ &+\lambda_{\text {bigram}} P\left(w_{3} \mid w_{2}\right) \\ &+\lambda_{\text {unigram}} P\left(w_{3}\right) \\ &+\lambda_{\text {uniform}} P_{0} \end{aligned}$
对于UNK单词,比如‘mispeling’,在训练时被替换为UNKNOWN LC,‘Lenon’被替换为UNKNOWN_UC,这样做的目的是当一个unknown word出现同样上下文的context时,就知道怎样大小写了。
这篇论文有一个简单版的实现:https://github.com/daltonfury42/truecase ,我们也阅读一下这个代码。下面是它的训练过程的主函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def train(self, corpus): for sentence in corpus: # 首先检查句子合法性,去掉全部是大写的句子 if not self.check_sentence_sanity(sentence): continue for word_idx, word in enumerate(sentence): self.uni_dist[word] += 1 word_lower = word.lower() # 把单词的所有大小写可能放到word_casing_lookup中 if word_lower not in self.word_casing_lookup: self.word_casing_lookup[word_lower] = set() self.word_casing_lookup[word_lower].add(word) # 将word的上下文加入bi-gram语言模型统计中 self.__function_one(sentence, word, word_idx, word_lower) # 将word的上下文加入tri-gram语言模型统计中 self.__function_two(sentence, word, word_idx)
训练好的模型用下面代码存放到pickle中:
1 2 3 4 5 6 7 8 9 10 11 def save_to_file(self, file_path): pickle_dict = { "uni_dist": self.uni_dist, "backward_bi_dist": self.backward_bi_dist, "forward_bi_dist": self.forward_bi_dist, "trigram_dist": self.trigram_dist, "word_casing_lookup": self.word_casing_lookup, } with open(file_path, "wb") as fp: pickle.dump(pickle_dict, fp) print("Model saved to " + file_path)
模型推理的主函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 def get_true_case(self, sentence, out_of_vocabulary_token_option="title"): # outOfVocabulariyTokenOption=title将OOV词以大写格式输出 # outOfVocabulariyTokenOption=lower将OOV词以小写格式输出 # outOfVocabulariyTokenOption=as-is将OOV词以原先格式输出 tokens = self.tknzr.tokenize(sentence) tokens_true_case = [] for token_idx, token in enumerate(tokens): # 标点和数字原样输出 if token in string.punctuation or token.isdigit(): tokens_true_case.append(token) else: token = token.lower() # 在词表中 if token in self.word_casing_lookup: # 只有一种形式的直接返回 if len(self.word_casing_lookup[token]) == 1: tokens_true_case.append( list(self.word_casing_lookup[token])[0]) else: prev_token = (tokens_true_case[token_idx - 1] if token_idx > 0 else None) next_token = (tokens[token_idx + 1] if token_idx < len(tokens) - 1 else None) best_token = None highest_score = float("-inf") # 找到语言模型得分最高的得分组合 for possible_token in self.word_casing_lookup[token]: score = self.get_score(prev_token, possible_token, next_token) if score > highest_score: best_token = possible_token highest_score = score tokens_true_case.append(best_token) if token_idx == 0: tokens_true_case[0] = tokens_true_case[0].title() else: # OOV if out_of_vocabulary_token_option == "title": tokens_true_case.append(token.title()) elif out_of_vocabulary_token_option == "lower": tokens_true_case.append(token.lower()) else: tokens_true_case.append(token) return "".join([ " " + i if not i.startswith("'") and i not in string.punctuation else i for i in tokens_true_case
一个pipeline的设计 我们可以设计这样一个pipeline以满足大部分大小写转换的需求:分句 + 命名实体大写 + n-gram语言模型处理其他。分句使用spacy,它利用dependency parse tree和一些规则判断句子的边界。在处理时有一些情况要注意:
注意句子开始的标志,一些不以字母为开始的句子,它的第一个字母应该大写:”non-digital items(t-shirts, printed posters, mugs, and cd-roms) require physical shipping.”
注意句子切分,如省略号、双引号后面是否是独立的一句话
注意缩略词的分词