机器学习-英文文本预处理

转载自:http://www.cnblogs.com/pinard/p/6756534.html

英文文本挖掘预处理特点 

英文文本的预处理方法和中文的有部分区别。首先,英文文本挖掘预处理一般可以不做分词(特殊需求除外),而中文预处理分词是必不可少的一步。第二点,大部分英文文本都是uft-8的编码,这样在大多数时候处理的时候不用考虑编码转换的问题,而中文文本处理必须要处理unicode的编码问题。

而英文文本的预处理也有自己特殊的地方,第三点就是拼写问题,很多时候,我们的预处理要包括拼写检查,比如“Helo World”这样的错误,我们不能在分析的时候讲错纠错。所以需要在预处理前加以纠正。第四点就是词干提取(stemming)和词形还原(lemmatization)。这个东西主要是英文有单数,复数和各种时态,导致一个词会有不同的形式。比如“countries”和"country","wolf"和"wolves",我们期望是有一个词。

英文文本挖掘预处理一:数据收集

这部分英文和中文类似。获取方法一般有两种:使用别人做好的语料库和自己用爬虫去在网上去爬自己的语料数据。

对于第一种方法,常用的文本语料库在网上有很多,如果大家只是学习,则可以直接下载下来使用,但如果是某些特殊主题的语料库,比如“deep learning”相关的语料库,则这种方法行不通,需要我们自己用第二种方法去获取。

对于第二种使用爬虫的方法,开源工具有很多,通用的爬虫我一般使用beautifulsoup。但是我们我们需要某些特殊的语料数据,比如上面提到的“deep learning”相关的语料库,则需要用主题爬虫(也叫聚焦爬虫)来完成。这个我一般使用ache。 ache允许我们用关键字或者一个分类算法模型来过滤出我们需要的主题语料,比较强大。

英文文本挖掘预处理二:除去数据中非文本部分

这一步主要是针对我们用爬虫收集的语料数据,由于爬下来的内容中有很多html的一些标签,需要去掉。少量的非文本内容的可以直接用Python的正则表达式(re)删除, 复杂的则可以用beautifulsoup来去除。另外还有一些特殊的非英文字符(non-alpha),也可以用Python的正则表达式(re)删除。

re 模块

参考 blog
正则表达式(Regular Expression)是字符串处理的常用工具,通常被用来检索、替换那些符合某个模式(Pattern)的文本。很多程序设计语言都支持正则表达式,像Perl、Java、C/C++。在 Python 中是通过标准库中的 re 模块 提供对正则的支持。

关于正则表达式的语法可以看 - speech and language processing chapter2 - 正则表达式中的*,+,?以及

编译正则表达式

re 模块提供了 re.compile() 函数将一个字符串编译成 pattern object,用于匹配或搜索。函数原型如下:

1
re.compile(pattern, flags=0)

re.compile() 还接受一个可选的参数 flag,用于指定正则匹配的模式。关于匹配模式,后面将会讲到。

反斜杠的困扰

在 python 的字符串中, 是被当做转义字符的。在正则表达式中, 也是被当做转义字符。这就导致了一个问题:如果你要匹配 \ 字符串,那么传递给 re.compile() 的字符串必须是

1
2
3
4
5
6
7
8
9
10
11

由于字符串的转义,所以实际传递给 re.compile() 的是 ```”\\“```,然后再通过正则表达式的转义,```”\\“``` 会匹配到字符”\“。这样虽然可以正确匹配到字符 \,但是很麻烦,而且容易漏写反斜杠而导致 Bug。那么有什么好的解决方案呢?

原始字符串很好的解决了这个问题,通过在字符串前面添加一个r,表示原始字符串,不让字符串的反斜杠发生转义。那么就可以使用```r"\\\\"```来匹配字符 ```\```了。

### patern object 执行匹配
一旦你编译得到了一个 pattern object,你就可以使用 pattern object 的方法或属性进行匹配了,下面列举几个常用的方法,更多请看[这里](https://docs.python.org/3/library/re.html#regular-expression-objects)。



```Pattern.match(string[, pos[, endpos]])

  • 匹配从 pos 到 endpos 的字符子串的开头。匹配成功返回一个 match object,不匹配返回 None。
  • pos 的默认值是0,endpos 的默认值是 len(string),所以默认情况下是匹配整个字符串的开头。
1
2
3
4
5
pattern = re.compile("d")
print(pattern.match('dog')) # 在字串开头,匹配成功
print(pattern.match('god')) # 不再子串开头,匹配不成功
print(pattern.match('ddaa', 1,5)) # 在子串开头,匹配成功
print(pattern.match('monday', 3))
1
2
3
4
<_sre.SRE_Match object; span=(0, 1), match='d'>
<_sre.SRE_Match object; span=(0, 1), match='g'>
<_sre.SRE_Match object; span=(1, 2), match='d'>
<_sre.SRE_Match object; span=(3, 4), match='d'>

regex.search(string[, pos[, endpos]])

  • 扫描整个字符串,并返回它找到的第一个匹配
  • 和 regex.match() 一样,可以通过 pos 和 endpos 指定范围
1
2
3
4
5
pattern = re.compile("ar{1}")
match = pattern.search("marray")
print(match)

<_sre.SRE_Match object; span=(1, 3), match='ar'>

regex.findall(string[, pos[, endpos]])

  • 找到所有匹配的子串,并返回一个 list
  • 可选参数 pos 和 endpos 和上面一样
1
2
3
4
5
pattern = re.compile(r"\d+") # 匹配字符串中的数字
lst = pattern.findall("abc1def2rst3xyz")
print(lst)

['1', '2', '3']

regex.finditer(string[, pos[, endpos]])

  • 找到所有匹配的子串,并返回由这些匹配结果(match object)组成的迭代器
  • 可选参数 pos 和 endpos 和上面一样。
1
2
3
4
5
6
7
8
pattern = re.compile(r"\d+")
p = pattern.finditer("abc1def2rst3xyz")
for i in p:
print(i)

<_sre.SRE_Match object; span=(3, 4), match='1'>
<_sre.SRE_Match object; span=(7, 8), match='2'>
<_sre.SRE_Match object; span=(11, 12), match='3'>

match object 获取结果

在上面讲到,通过 pattern object 的方法(除 findall 外)进行匹配得到的返回结果都是 match object。每一个 match object 都包含了匹配到的相关信息,比如,起始位置、匹配到的子串。那么,我们如何从 match object 中提取这些信息呢?

match.group([group1, ...]):

  • 返回 match object 中的字符串。
  • 每一个 ( ) 都是一个分组,分组编号从1开始,从左往右,每遇到一个左括号,分组编号+1。
  • 组 0 总是存在的,它就是整个表达式
  • 没有参数时,group1默认为0,这时返回整个匹配到的字符串。
  • 指定一个参数(整数)时,返回该分组匹配到的字符串。
  • 指定多个参数时,返回由那几个分组匹配到的字符串组成的 tuple。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pattern = re.compile(r"(\w+) (\w+)") # \w 匹配任意字母,数字,下划线
m = pattern.match("He _ Kobe Bryant, Lakers player")
print(m)
print(m.group())
print(m.group(1))
print(m.group(2))
print(m.group(1,2))


<_sre.SRE_Match object; span=(0, 4), match='He _'>
He _
He
_
('He', '_')

match.groups()

  • 返回由所有分组匹配到的字符串组成的 tuple。
1
2
3
4
5
m = re.match(r"(\d+)\.(\d+)", '24.163')
m.groups()


('24', '163')

match.start([group])

  • 没有参数时,返回匹配到的字符串的起始位置。
  • 指定参数(整数)时,返回该分组匹配到的字符串的起始位置。
1
2
3
4
pattern = re.compile(r"(\w+) (\w+)")
m = pattern.match("Kobe Bryant, Lakers")
print(m.start()) # 0
print(m.start(2)) # 5

match.end([group]):

  • 没有参数时,返回匹配到的字符串的结束位置。
  • 指定参数(整数)时,返回该分组匹配到的字符串的结束位置。
1
2
3
4
pattern = re.compile(r"(\w+) (\w+)")
m = pattern.match("Kobe Bryant, Lakers")
print(m.end()) # 11
print(m.end(1)) # 4

match.span([group]):

  • 返回一个二元 tuple 表示匹配到的字符串的范围,即 (start, end)。
  • 指定参数时,返回该分组匹配到的字符串的 (start, end)。
1
2
3
4
pattern = re.compile(r"(\w+) (\w+)")
m = pattern.match("Kobe Bryant, Lakers")
print(m.span()) # (0, 11)
print(m.span(2)) # (5, 11)

模块级别的函数

上面讲到的函数都是对象的方法,要使用它们必须先得到相应的对象。本节将介绍一些Module-Level Functions,比如 match(),search(),findall() 等等。你不需要创建一个 pattern object 就可以直接调用这些函数。

string, flags
1
2
3
4
5
6
7
8
9
10
11
12
13
14


```python
pattern = re.compile(r"(\w+) (\w+)")
m = pattern.match("Kobe Bryant, Lakers")
print(m)

# 相当于

m = re.match(r"(\w+) (\w+)","Kobe Bryant, Lakers")
print(m)

<_sre.SRE_Match object; span=(0, 11), match='Kobe Bryant'>
<_sre.SRE_Match object; span=(0, 11), match='Kobe Bryant'>
1
2
3
4
5
6
7
8
9
10
pattern = re.compile(r"(\w+) (\w+)")
m = pattern.search("Kobe Bryant, Lakers")
print(m)
# 相当于

m = re.search(r"(\w+) (\w+)","Kobe Bryant, Lakers")
print(m)

<_sre.SRE_Match object; span=(0, 11), match='Kobe Bryant'>
<_sre.SRE_Match object; span=(0, 11), match='Kobe Bryant'>

re.findall(pattern, string, flags=0):与上面类似。

re.finditer(pattern, string, flags=0):与上面类似

编译标志(匹配模式)

  • re.IGNORECASE:忽略大小写,同 re.I。

  • re.MULTILINE:多行模式,改变^和$的行为,同 re.M。

  • re.DOTALL:点任意匹配模式,让’.’可以匹配包括’’在内的任意字符,同 re.S。

  • re.LOCALE:使预定字符类 取决于当前区域设定, 同 re.L。

  • re.ASCII:使 只匹配 ASCII 字符,而不是 Unicode 字符,同 re.A。

  • re.VERBOSE:详细模式。这个模式下正则表达式可以是多行,忽略空白字符,并可以加入注释。主要是为了让正则表达式更易读,同 re.X。例如,以下两个正则表达式是等价的:

1
2
3
4
5
a = re.compile(r"\d + \. \d *#re.X") the integral part
b = re.compile(r"\d+\.\d*")
print(b.match("123.45"))

<_sre.SRE_Match object; span=(0, 6), match='123.45'>

修改字符串

第二部分讲的是字符串的匹配和搜索,但是并没有改变字符串。下面就讲一下可以改变字符串的操作。

分割字符串

split()函数在匹配的地方将字符串分割,并返回一个 list。同样的,re 模块提供了两种 split 函数,一个是 pattern object 的方法,一个是模块级的函数。

regex.split(string, maxsplit=0):

  • maxsplit用于指定最大分割次数,不指定将全部分割。
1
2
3
4
5
pattern = re.compile(r"[A-Z]+")
m = pattern.split("abcDefgHijkLmnoPqrs")
print(m)

['abc', 'efg', 'ijk', 'mno', 'qrs']

re.split(pattern, string, maxsplit=0, flags=0):

  • 模块级函数,功能与 regex.split() 相同。
  • flags用于指定匹配模式。
1
2
3
4
5
m = re.split(r"[A-Z]+","abcDefgHijkLmnoPqrs")
print(m)

# 输出结果:
['abc', 'efg', 'ijk', 'mno', 'qrs']

搜索与替换

另一个常用的功能是找到所有的匹配,并把它们用不同的字符串替换。re 模块提供了sub()和subn()来实现替换的功能,而它们也分别有自己两个不同版本的函数。

string, count
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

- 使用 repl 替换 string 中每一个匹配的子串,返回替换后的字符串。若找不到匹配,则返回原字符串。
- repl 可以是一个字符串,也可以是一个函数。
- 当repl是一个字符串时,任何在其中的反斜杠都会被处理。
- 当repl是一个函数时,这个函数应当只接受一个参数(pattern对象),对匹配到的对象进行处理,然后返回一个字符串用于替换。
- count 用于指定最多替换次数,不指定时全部替换。


```python
pattern = re.compile(r"like", re.I)
s1 = pattern.sub(r"love", "I like you, do you like me?")
s2 = pattern.sub(lambda m:m.group().upper(), "I like you, do you like me?") # repl 是函数,其参数是 pattern
print(s1)
print(s2)


I love you, do you love me?
I LIKE you, do you LIKE me?

re.sub(pattern, repl, string, count=0, flags=0)

  • 模块级函数,与 regex.sub() 函数功能相同。
  • flags 用于指定匹配模式。
1
2
3
4
s1 = re.sub(r"(\w)'s\b", r"\1 is", "She's Xie Pan")
print(s1)

She is Xie Pan

regex.subn(repl, string, count=0)

  • 同 sub(),只不过返回值是一个二元 tuple,即(sub函数返回值, 替换次数)。
1
2
3
4
5
6
7
8
pattern = re.compile(r"like", re.I)
s1 = pattern.subn(r"love", "I like you, do you like me?")
s2 = pattern.subn(lambda m:m.group().upper(), "I like you, do you like me?") # repl 是函数,其参数是 pattern
print(s1)
print(s2)

('I love you, do you love me?', 2)
('I LIKE you, do you LIKE me?', 2)

re.subn(pattern, repl, string, count=0, flags=0): - 同上

英文文本挖掘预处理三:拼写检查

由于英文文本中可能有拼写错误,因此一般需要进行拼写检查。如果确信我们分析的文本没有拼写问题,可以略去此步。 拼写检查,我们一般用pyenchant类库完成。pyenchant的安装很简单:"pip install pyenchant"即可。 对于一段文本,我们可以用下面的方式去找出拼写错误:

1
2
3
# 发现这样安装并不是在虚拟环境下,需要去终端对应的虚拟环境下安装
# source avtivate NLP
!pip install pyenchant
/bin/sh: 1: source: not found
Requirement already satisfied: pyenchant in /home/panxie/anaconda3/lib/python3.6/site-packages (2.0.0)
distributed 1.21.8 requires msgpack, which is not installed.
You are using pip version 10.0.1, however version 18.0 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
1
2
3
4
5
6
7
8
from enchant.checker import SpellChecker
chkr = SpellChecker('en_US')
chkr.set_text("Many peopel like too watch In the Name of people")
for err in chkr:
print("ERROR:", err.word)


ERROR: peopel

发现只能找单词拼写错误的,但 too 这样的是没办法找出的。找出错误后,我们可以自己来决定是否要改正。当然,我们也可以用pyenchant中的wxSpellCheckerDialog类来用对话框的形式来交互决定是忽略,改正还是全部改正文本中的错误拼写。
更多操作可参考:
- https://www.jianshu.com/p/96c01666aeeb - https://pythonhosted.org/pyenchant/tutorial.html

英文文本挖掘预处理四:词干提取(stemming)和词形还原(lemmatization)

词干提取(stemming)和词型还原(lemmatization)是英文文本预处理的特色。两者其实有共同点,即都是要找到词的原始形式。只不过词干提取(stemming)会更加激进一点,它在寻找词干的时候可以会得到不是词的词干。比如"imaging"的词干可能得到的是"imag", 并不是一个词。而词形还原则保守一些,它一般只对能够还原成一个正确的词的词进行处理。个人比较喜欢使用词型还原而不是词干提取。

在实际应用中,一般使用nltk来进行词干提取和词型还原。安装nltk也很简单,"pip install nltk"即可。只不过我们一般需要下载nltk的语料库,可以用下面的代码完成,nltk会弹出对话框选择要下载的内容。选择下载语料库就可以了。

1
2
3
4
5
6
7
import nltk
nltk.download('wordnet')

[nltk_data] Downloading package wordnet to /home/panxie/nltk_data...
[nltk_data] Unzipping corpora/wordnet.zip.

True

在nltk中,做词干提取的方法有PorterStemmer,LancasterStemmer和SnowballStemmer。个人推荐使用SnowballStemmer。这个类可以处理很多种语言,当然,除了中文。

1
2
3
4
5
from nltk.stem import SnowballStemmer
stemmer = SnowballStemmer("english")
stemmer.stem("countries")

'countri'

输出是"countri",这个词干并不是一个词。
而如果是做词型还原,则一般可以使用WordNetLemmatizer类,即wordnet词形还原方法。

1
2
3
4
5
6
from nltk.stem import WordNetLemmatizer
wnl = WordNetLemmatizer()
print(wnl.lemmatize('countries'))


country

输出是"country",比较符合需求。
在实际的英文文本挖掘预处理的时候,建议使用基于wordnet的词形还原就可以了。
这里有个词干提取和词型还原的demo,如果是这块的新手可以去看看,上手很合适。

英文文本挖掘预处理五:转化为小写

1
2
3
4
text = 'XiePan'
print(text.lower())

xiepan

英文文本挖掘预处理六:引入停用词

在英文文本中有很多无效的词,比如“a”,“to”,一些短词,还有一些标点符号,这些我们不想在文本分析的时候引入,因此需要去掉,这些词就是停用词。个人常用的英文停用词表下载地址在这。当然也有其他版本的停用词表,不过这个版本是我常用的。

在我们用scikit-learn做特征处理的时候,可以通过参数stop_words来引入一个数组作为停用词表。这个方法和前文讲中文停用词的方法相同,这里就不写出代码,大家参考前文即可。

1
2
3
4
5
6
7
from nltk import word_tokenize
from nltk.corpus import stopwords
stop = set(stopwords.words('english')) # 停用词
stop.add("foo") # 增加一个词
stop.remove("is") # 去掉一个词
sentence = "this is a foo bar sentence"
[i for i in word_tokenize(sentence.lower()) if i not in stop]
['is', 'bar', 'sentence']

英文文本挖掘预处理七:特征处理

现在我们就可以用scikit-learn来对我们的文本特征进行处理了,在文本挖掘预处理之向量化与Hash Trick中,我们讲到了两种特征处理的方法,向量化与Hash Trick。而向量化是最常用的方法,因为它可以接着进行TF-IDF的特征处理。在文本挖掘预处理之TF-IDF中,我们也讲到了TF-IDF特征处理的方法

TfidfVectorizer类可以帮助我们完成向量化,TF-IDF和标准化三步。当然,还可以帮我们处理停用词。这部分工作和中文的特征处理也是完全相同的,大家参考前文即可。

英文文本挖掘预处理八:建立分析模型

有了每段文本的TF-IDF的特征向量,我们就可以利用这些数据建立分类模型,或者聚类模型了,或者进行主题模型的分析。此时的分类聚类模型和之前讲的非自然语言处理的数据分析没有什么两样。因此对应的算法都可以直接使用。而主题模型是自然语言处理比较特殊的一块,这个我们后面再单独讲。

英文文本挖掘预处理总结

上面我们对英文文本挖掘预处理的过程做了一个总结,希望可以帮助到大家。需要注意的是这个流程主要针对一些常用的文本挖掘,并使用了词袋模型,对于某一些自然语言处理的需求则流程需要修改。比如有时候需要做词性标注,而有时候我们也需要英文分词,比如得到"New York"而不是“New”和“York”,因此这个流程仅供自然语言处理入门者参考,我们可以根据我们的数据分析目的选择合适的预处理方法。