文本预处理对于NLP领域的从业者或研究者来说并不陌生,也是很多人刚接触NLP领域面临的第一个问题。今天我想对文本预处理所涉及的知识进行一个整理,作为个人的笔记,同时分享一些个人思考与心得。文章以我对文本预处理认知顺序展开,如果文中有错误,欢迎指正。
为什么要有文本预处理?
1. 计算机无法直接理解文字其实对于程序员来说,文本预处理不应该感到陌生。因为在我看来,程序编译器就是一种“语言预处理”的过程。比如gcc把C语言的代码转化为机器语言,计算机才能执行程序。
所以将“自然语言”转化为“符号语言”是文本预处理最本质最核心的一个目的。
2. 在下游任务中,发现转化的“符号语言”不够好无论是个人的一次项目还是人类历史上的研究,文本预处理中所涉及的操作绝对不是一蹴而就的,而是在做下游任务的时候,发现了一些问题,然后我们再回过头来,通过预处理的方式解决。
我猜测,人类在第一次做文本预处理的时候,只做了一件事,把所有“文字”转成了“ASCII码”(甚至那时候可能ASCII码都没有),丢进计算机去处理。然后,程序员们在下游任务中发现了种种不满意,而回过头来优化。
比如我们觉得有些单词对我的任务没有作用,浪费计算力和空间,就定义了“stop word”。
比如我们觉得“one-hot”的表示方式太过于“庞大”而且没有任何词义表示,所以我们想办法把庞大的“one-hot”转化为稠密的“词向量”。
所以,这一部分预处理的原因是“人工智能还不够智能”,我们通过“人工”去弥补“智能”。和我们教儿童识字会将很多生僻词拿走是一个道理。希望有朝一日,我们可以少一点人工帮助,把最原始的文字丢给计算机,让他去处理。
根据上面的两个原因,我得到了两个结论。第一,文本预处理必不可少。第二,文本预处理没有绝对的“必备操作”,我们应该根据自己下游任务来思考需要哪些操作。而不是把文本预处理与下游任务孤立开,不能所有情况都用一套流程来处理。
所以,我下面整理思路也是从“为什么做”到“怎么做”最后“反思”来梳理。反思部分有很多个人思考,仅供交流,抛砖引玉。
分词 —— Tokenization
分词这个称呼其实是不准确,容易引起误导。Tokenization这里更加准确的翻译应该是标记化。词语和符号都是标记,标记化就是对句子中的词语和标点进行合理的分割。
一般NLP处理都是以词语为粒度的切割为前提。例如传统的bow是对词频的统计形成文本向量。RNN是一个词一个词的输入。
大概是由于:汉字或者字母的粒度,无法表达语义,例如给你一个‘s’,完全不知所云。而以句子为粒度分析,句子千变万化,两篇同样描述大熊猫的文章,甚至可能没有任何两个句子相同。不利于统计模型或者机器学习模型的分析。所以,词语粒度的切割是最为合适的。
分词在英语和中文的考虑是不同的,分开来说。
1. 英文分词英文单词相对分词会容易一些,但还是有一些细节需要注意。
英文单词天生会用空格间隔开,所以很多人会采用以下方式分词。
sentence.split() # split()默认以空格分词
考虑分词“It's your cat!”这句话。这样分词的最后一个词会是“cat!”。就会把“cat”和“cat!”当作两个词来看待。于是有的人选择先去掉标点符号再做分词,那样“it's”和“its”就都会成为“its”。
这就是为什么即使是简单的英文分词,nltk还是出了word_tokenize的工具。
from nltk import word_tokenize
print(word_tokenize("it's your cat!"))
# 打印结果:['it', "'s", 'your', 'cat', '!']
可以看到nltk很好的兼顾了英文中标点符号的情况。值得让人高兴的是,GloVe等预训练词向量是有"'s"、"'re"等标记的向量的。
2. 中文分词先说一下结论:中文分词可以当做是一个已解决的问题,也就是我们可以通过调用jieba等分词库来实现。
import jieba
seg_list = jieba.cut("我来到南京长江大桥")
print(list(seg_list)) # jieba.cut() 返回的是一个生成器
# 打印结果:['我', '来到', '南京长江大桥']
深究一下分词算法大抵有以下几个方面,篇幅关系不做展开。
1. 基于规则分词。简单的有正向最大匹配法和逆向最大匹配法。据有外国学者1995年的研究表明,这两种方法分词完全一致且正确的句子占90%左右,这两种方法分词不一样但会至少有一种是正确的句子占9%。也就是只有1%左右的句子,这两种方法是分不出来的。
2. 基于统计分词。这种分词方法需要建立语言模型,然后通过Viterbi算法进行规划寻找概率最大的分词方法。
3. 基于深度学习分词。
对于这3种分词方法,前2种我进行过编程实现都不算复杂,但第3种还没有接触过,这里存疑,找时间再研究下。
停用词 —— stop word
其实为什么要有停用词,我觉得主要是因为传统的NLP是基于统计的。我们统计到了大量“is”、“a”、“what”这样的单词,但是这样的词又不能帮助我辨别文本与文本的区别。可能TF-IDF可以一定程度上解决高频词的重要度问题。但是既然没用,为什么不去掉。
操作思路就是:生成停用词表,然后去掉数据里所有的停用词。
1. 生成停用词表可以自己对数据集做词频统计选出高频词,也可以网上下载,方便一点就直接使用nltk提供的停用词。
from nltk.corpus import stopwords
# 需要提前调用 nltk.download(‘stopwords’) 下载
my_stopwords = set(stopwords.words('english'))
但无论如何生成这个停用词表,都最好加一步筛选过程。比如“what”被nltk当做了停用词,但假如你的任务恰好就是问答系统,那是不是“what”里包含了很多重要的信息呢。
my_stopwords .remove('what')
2. 去掉停用词 从序列里去掉特定的单词,是一个简单的编程问题,但仍然有一些细节可以注意。
(1)步骤1中生成的停用词表变量使用set类型,就是因为这里要判断 in 的操作。set的in操作平均时间复杂度是O(1),list的in操作平均时间复杂度是O(n)。
(2)列表生成式是一种更加Pythonic的写法,对代码规范有点轻微强迫症是一种自然而然的好习惯。
# words 是已经分词好的一句话
words = [ w for w in words if w not in my_stopwords ]
那么反思一下停用词这件事,既然是由词的频率进行判断的。那和词频无关的表示方式,是否还有必要去掉停用词呢?比如skip gram是由一个神经网络训练出来的词向量。
kaggle上一个专家分享了他的经验,推荐去看一下全文。
引用自:How to: Preprocessing when using embeddings Don't use standard preprocessing steps like stemming or stopword removal when you have pre-trained embeddings
他指出很多人在Embedding之前使用预处理,是想当然的觉得提出(他们认为的)重要信息,可以帮助神经网络更好的工作。但可能往往结果并不是想象的那样。(感谢零顾酱的指正!原稿我根据上下文阅读”过拟合了“)
You loose valuable information, which would help your NN to figure things out.
在看到这篇文章前,我是任何任务无脑去停用词的。但在这之后,会每次任务,分别对“去停用词”版本和“不去停用词”版本作比较。不过一般基于预处理词向量的词表示方式的任务,确实是不去停用词版本会更好。
所以这也印证了我文章开头提到的第二个原因。我们有的预处理步骤,是因为我们处理任务的方法处理不了某些信息,而并不是这些信息真的毫无作用。比如我们用统计的思路去表达词信息,确实无法处理高频词的作用。
文本标准化 —— Text Normalization
需要做文本的标准化,主要是由于英语中同一个单词可能有不同的形态。
名词以apple为例,有apple,apples的形态。
动词以take为例,有take,took,token的形态。
文本标准化通常的做法有两种,Stemming和Lemmatization。
1. Stemming —— 词干分析Stemming是一种基于规则的标准化方式。是早期语言学家总结了英语中的单词变形方式,如词尾增加后缀等方式。然后根据这些变形规则尝试去反向还原单词词干。
from nltk.stem import LancasterStemmer
lancaster=LancasterStemmer()
lancaster.stem('prestudy')
# 输出:prestudi
# study:study studies:studi
根据上图的例子,我们可以看到Stemming的几个问题:
- 还原出来的词干,未必是一个单词。例子中prestudy已经是单词原型了,但是还原成了prestudi。假如使用了预训练的词向量,比如prestudy在GloVe中是存在的,但是prestudi却是没有的,这样就有点弄巧成拙的感觉。
- 有的单词是一样的,但是规则却不能将他们还原成一样。
- 有的单词是不一样的,但是规则会把这两个单词还原成一个。比如fli和flying变换过都是fli。
2. Lemmatization —— 词元分析
词元分析是基于词典进行的标准化,所以好处就是还原出来的单词都是现实存在的单词,但对于词典中没有的单词,就无法还原了。
from nltk.stem import WordNetLemmatizer
wordnet_lemmatizer = WordNetLemmatizer()
wordnet_lemmatizer.lemmatize('studies')
# 输出是study
感谢:
没有鱼丸木有粗面 同学,零顾酱同学的指正!