Python自然语言处理实践: 在NLTK中使用斯坦福中文分词器

斯坦福大学自然语言处理组是世界知名的NLP研究小组,他们提供了一系列开源的Java文本分析工具,包括分词器(Word Segmenter),词性标注工具(Part-Of-Speech Tagger),命名实体识别工具(Named Entity Recognizer),句法分析器(Parser)等,可喜的事,他们还为这些工具训练了相应的中文模型,支持中文文本处理。在使用NLTK的过程中,发现当前版本的NLTK已经提供了相应的斯坦福文本处理工具接口,包括词性标注,命名实体识别和句法分析器的接口,不过可惜的是,没有提供分词器的接口。在google无果和阅读了相应的代码后,我决定照猫画虎为NLTK写一个斯坦福中文分词器接口,这样可以方便的在Python中调用斯坦福文本处理工具。

首先需要做一些准备工作,第一步当然是安装NLTK,这个可以参考我们在gensim的相关文章中的介绍《如何计算两个文档的相似度》,不过这里建议check github上最新的NLTK源代码并用“python setup.py install”的方式安装这个版本:https://github.com/nltk/nltk。这个版本新增了对于斯坦福句法分析器的接口,一些老的版本并没有,这个之后我们也许还会用来介绍。而我们也是在这个版本中添加的斯坦福分词器接口,其他版本也许会存在一些小问题。其次是安装Java运行环境,以Ubuntu 12.04为例,安装Java运行环境仅需要两步:

sudo apt-get install default-jre
sudo apt-get install default-jdk

最后,当然是最重要的,你需要下载斯坦福分词器的相应文件,包括源代码,模型文件,词典文件等。注意斯坦福分词器并不仅仅支持中文分词,还支持阿拉伯语的分词,需要下载的zip打包文件是这个: Download Stanford Word Segmenter version 2014-08-27,下载后解压。

准备工作就绪后,我们首先考虑的是在nltk源代码里的什么地方来添加这个接口文件。在nltk源代码包下,斯坦福词性标注器和命名实体识别工具的接口文件是这个:nltk/tag/stanford.py ,而句法分析器的接口文件是这个:nltk/parse/stanford.py , 虽然在nltk/tokenize/目录下有一个stanford.py文件,但是仅仅提供了一个针对英文的tokenizer工具PTBTokenizer的接口,没有针对斯坦福分词器的接口,于是我决定在nltk/tokenize下添加一个stanford_segmenter.py文件,作为nltk斯坦福中文分词器的接口文件。NLTK中的这些接口利用了Linux 下的管道(PIPE)机制和subprocess模块,这里直接贴源代码了,感兴趣的同学可以自行阅读:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Natural Language Toolkit: Interface to the Stanford Chinese Segmenter
#
# Copyright (C) 2001-2014 NLTK Project
# Author: 52nlp <52nlpcn@gmail.com>
#
# URL: <http://nltk.org/>
# For license information, see LICENSE.TXT

from __future__ import unicode_literals, print_function

import tempfile
import os
import json
from subprocess import PIPE

from nltk import compat
from nltk.internals import find_jar, config_java, java, _java_options

from nltk.tokenize.api import TokenizerI

class StanfordSegmenter(TokenizerI):
    r"""
    Interface to the Stanford Segmenter

    >>> from nltk.tokenize.stanford_segmenter import StanfordSegmenter
    >>> segmenter = StanfordSegmenter(path_to_jar="stanford-segmenter-3.4.1.jar", path_to_sihan_corpora_dict="./data", path_to_model="./data/pku.gz", path_to_dict="./data/dict-chris6.ser.gz")
    >>> sentence = u"这是斯坦福中文分词器测试"
    >>> segmenter.segment(sentence)
    >>> u'\u8fd9 \u662f \u65af\u5766\u798f \u4e2d\u6587 \u5206\u8bcd\u5668 \u6d4b\u8bd5\n'
    >>> segmenter.segment_file("test.simp.utf8")
    >>> u'\u9762\u5bf9 \u65b0 \u4e16\u7eaa \uff0c \u4e16\u754c \u5404\u56fd ...
    """

    _JAR = 'stanford-segmenter.jar'

    def __init__(self, path_to_jar=None,
            path_to_sihan_corpora_dict=None,
            path_to_model=None, path_to_dict=None,
            encoding='UTF-8', options=None,
            verbose=False, java_options='-mx2g'):
        self._stanford_jar = find_jar(
            self._JAR, path_to_jar,
            env_vars=('STANFORD_SEGMENTER',),
            searchpath=(),
            verbose=verbose
        )
        self._sihan_corpora_dict = path_to_sihan_corpora_dict
        self._model = path_to_model
        self._dict = path_to_dict

        self._encoding = encoding
        self.java_options = java_options
        options = {} if options is None else options
        self._options_cmd = ','.join('{0}={1}'.format(key, json.dumps(val)) for key, val in options.items())

    def segment_file(self, input_file_path):
        """
        """
        cmd = [
            'edu.stanford.nlp.ie.crf.CRFClassifier',
            '-sighanCorporaDict', self._sihan_corpora_dict,
            '-textFile', input_file_path,
            '-sighanPostProcessing', 'true',
            '-keepAllWhitespaces', 'false',
            '-loadClassifier', self._model,
            '-serDictionary', self._dict
        ]

        stdout = self._execute(cmd)

        return stdout

    def segment(self, tokens):
        return self.segment_sents([tokens])

    def segment_sents(self, sentences):
        """
        """
        encoding = self._encoding
        # Create a temporary input file
        _input_fh, self._input_file_path = tempfile.mkstemp(text=True)

        # Write the actural sentences to the temporary input file
        _input_fh = os.fdopen(_input_fh, 'wb')
        _input = '\n'.join((' '.join(x) for x in sentences))
        if isinstance(_input, compat.text_type) and encoding:
            _input = _input.encode(encoding)
        _input_fh.write(_input)
        _input_fh.close()

        cmd = [
            'edu.stanford.nlp.ie.crf.CRFClassifier',
            '-sighanCorporaDict', self._sihan_corpora_dict,
            '-textFile', self._input_file_path,
            '-sighanPostProcessing', 'true',
            '-keepAllWhitespaces', 'false',
            '-loadClassifier', self._model,
            '-serDictionary', self._dict
        ]

        stdout = self._execute(cmd)

        # Delete the temporary file
        os.unlink(self._input_file_path)

        return stdout

    def _execute(self, cmd, verbose=False):
        encoding = self._encoding
        cmd.extend(['-inputEncoding', encoding])
        _options_cmd = self._options_cmd
        if _options_cmd:
            cmd.extend(['-options', self._options_cmd])

        default_options = ' '.join(_java_options)

        # Configure java.
        config_java(options=self.java_options, verbose=verbose)

        stdout, _stderr = java(cmd,classpath=self._stanford_jar, stdout=PIPE, stderr=PIPE)
        stdout = stdout.decode(encoding)

        # Return java configurations to their default values.
        config_java(options=default_options, verbose=False)

        return stdout

def setup_module(module):
    from nose import SkipTest

    try:
        StanfordSegmenter()
    except LookupError:
        raise SkipTest('doctests from nltk.tokenize.stanford_segmenter are skipped because the stanford segmenter jar doesn\'t exist')

Update: 自NLTK 3.2版本起,这个接口已经被正式加进去了,所以大家可以直接下载安装官方版本,以下这段话可以忽略: https://github.com/nltk/nltk/blob/develop/nltk/tokenize/stanford_segmenter.py

我在github上fork了一个最新的NLTK版本,然后在这个版本中添加了stanford_segmenter.py,感兴趣的同学可以自行下载这个代码,放到nltk/tokenize/目录下,然后重新安装NLTK:sudo python setpy.py install. 或者直接clone我们的这个nltk版本,安装后就可以使用斯坦福中文分词器了。

现在就可以在Python NLTK中调用这个斯坦福中文分词接口了。为了方便起见,建议首先进入到解压后的斯坦福分词工具目录下:cd stanford-segmenter-2014-08-27,然后在这个目录下启用ipython,当然默认python解释器也可:

# 初始化斯坦福中文分词器
In [1]: from nltk.tokenize.stanford_segmenter import StanfordSegmenter

# 注意分词模型,词典等资源在data目录下,使用的是相对路径,
# 在stanford-segmenter-2014-08-27的目录下执行有效
# 注意,斯坦福中文分词器提供了两个中文分词模型:
# ctb.gz是基于宾州中文树库训练的模型
# pku.gz是基于北大在2005backoof上提供的人名日报语料库
# 这里选用了pku.gz,方便最后的测试
In [2]: segmenter = StanfordSegmenter(path_to_jar="stanford-segmenter-3.4.1.jar", path_to_sihan_corpora_dict="./data", path_to_model="./data/pku.gz", path_to_dict="./data/dict-chris6.ser.gz")

# 测试一个中文句子,注意u
In [3]: sentence = u"这是斯坦福中文分词器测试"

# 调用segment方法来切分中文句子,这里隐藏了一个问题,我们最后来说明
In [4]: segmenter.segment(sentence)
Out[4]: u'\u8fd9 \u662f \u65af\u5766\u798f \u4e2d\u6587 \u5206\u8bcd\u5668 \u6d4b\u8bd5\n'

# 由于分词后显示的是unicode编码,我们把这个结果输出到文件中
In [5]: outfile = open('outfile', 'w')

In [6]: result = segmenter.segment(sentence)

# 注意写入到文件的时候要encode 为 UTF-8编码
In [7]: outfile.write(result.encode('UTF-8'))

In [8]: outfile.close()

打开这个outfile文件:

这 是 斯坦福 中文 分词器 测试

这里同时提供了一个segment_file的调用方法,方便直接对文件进行切分,让我们来测试《中文分词入门之资源》中介绍的backoff2005的测试集pku_test.utf8,来看看斯坦福分词器的效果:

In [9]: result = segmenter.segment_file('pku_test.utf8')

In [10]: outfile = open('pku_outfile', 'w')

In [11]: outfile.write(result.encode('UTF-8'))

In [12]: outfile.close()

打开结果文件pku_outfile:

共同 创造 美好 的 新 世纪 ——二○○一年 新年 贺词
( 二○○○年 十二月 三十一日 ) ( 附 图片 1 张 )
女士 们 , 先生 们 , 同志 们 , 朋友 们 :
2001年 新年 钟声 即将 敲响 。 人类 社会 前进 的 航船 就要 驶入 21 世纪 的 新 航程 。 中国 人民 进入 了 向 现代化 建设 第三 步 战略 目标 迈进 的 新 征程 。
在 这个 激动人心 的 时刻 , 我 很 高兴 通过 中国 国际 广播 电台 、 中央 人民 广播 电台 和 中央 电视台 , 向 全国 各族 人民 , 向 香港 特别 行政区 同胞 、 澳门 特别 行政区 同胞 和 台湾 同胞 、 海外 侨胞 , 向 世界 各国 的 朋友 们 , 致以 新 世纪 第一 个 新年 的 祝贺 !
....

我们用backoff2005的测试脚本来测试一下斯坦福中文分词器在这份测试语料上的效果:

./icwb2-data/scripts/score ./icwb2-data/gold/pku_training_words.utf8 ./icwb2-data/gold/pku_test_gold.utf8 pku_outfile > stanford_pku_test.score

结果如下:
=== SUMMARY:
=== TOTAL INSERTIONS: 1479
=== TOTAL DELETIONS: 1974
=== TOTAL SUBSTITUTIONS: 3638
=== TOTAL NCHANGE: 7091
=== TOTAL TRUE WORD COUNT: 104372
=== TOTAL TEST WORD COUNT: 103877
=== TOTAL TRUE WORDS RECALL: 0.946
=== TOTAL TEST WORDS PRECISION: 0.951
=== F MEASURE: 0.948
=== OOV Rate: 0.058
=== OOV Recall Rate: 0.769
=== IV Recall Rate: 0.957
### pku_outfile 1479 1974 3638 7091 104372 103877 0.946 0.951 0.948 0.058 0.769 0.957

准确率是95.1%, 召回率是94.6%, F值是94.8%, 相当不错。感兴趣的同学可以测试一下其他测试集,或者用宾州中文树库的模型来测试一下结果。

最后我们再说明一下这个接口存在的问题,因为使用了Linux PIPE模式来调用斯坦福中文分词器,相当于在Python中执行相应的Java命令,导致每次在执行分词时会加载一遍分词所需的模型和词典,这个对文件操作时(segment_file)没有多大的问题,但是在对句子执行分词(segment)的时候会存在很大的问题,每次都加载数据,在实际产品中基本是不可用的。虽然发现斯坦福分词器提供了一个 --readStdin 的读入标准输入的参数,也尝试通过python subprocess中先load 文件,再用的communicate方法来读入标准输入,但是仍然没有解决问题,发现还是一次执行,一次结束。这个问题困扰了我很久,google了很多资料也没有解决问题,欢迎懂行的同学告知或者来解决这个问题,再此先谢过了。That's all!

注:原创文章,转载请注明出处“我爱自然语言处理”:www.52nlp.cn

本文链接地址:http://www.52nlp.cn/python自然语言处理实践-在nltk中使用斯坦福中文分词器

Python自然语言处理实践: 在NLTK中使用斯坦福中文分词器》上有81条评论

  1. cn

    您好:windows 10下分词时,报这么个错误,UnicodeDecodeError: 'utf8' codec can't decode byte 0xb4 in position 0: invalid start byte

    [回复]

    52nlp 回复:

    这个是python 编码问题,和你处理的文本有关,windows下不太熟悉,你可以参考一下这篇文章,有相似的错误:

    http://www.crifan.com/summary_python_2_x_common_string_encode_decode_error_reason_and_solution/

    [回复]

    沈卓 回复:

    您好,您这个问题解决了吗?我也是这个问题,还没解决

    [回复]

  2. 李华阳

    您好,我看到一个篇英文博客与本文大量相似,我不太清楚您和该博主是否是同一人,那篇博客没有标注出自于您这篇文章,如果是您大量借用他人博客却声名原创是不礼貌的,如果是他抄袭您可以联系他要求他注明出处。
    http://textminingonline.com/dive-into-nltk-part-vi-add-stanford-word-segmenter-interface-for-python-nltk

    [回复]

    52nlp 回复:

    谢谢提醒,其实这个英文博客是我用来练习的。

    [回复]

  3. 李华阳

    先看到一骗英文的博客基本直接照搬本文了,我都已经以为他是原创了,然后发现代码部分留的邮箱是您的,而且日期在此之后。。。

    [回复]

  4. hyj

    很赞。。。
    可以跟"stanford-segmenter-3.4.1.jar"
    但是3.6.0好像有异常:
    Exception in thread "main" java.lang.UnsupportedClassVersionError: edu/stanford/nlp/ie/crf/CRFClassifier : Unsupported major.minor version 52.0
    at java.lang.ClassLoader.defineClass1(Native Method)
    应该是版本问题。。。

    我用过3.6.0 版本的stanford-segmenter在java下面进行分词; 除了第一次需要load dict比较慢,后面都会很快。但是python版本的3.4.1没有对后面进行优化。 每次都比较慢....

    [回复]

    52nlp 回复:

    谢谢,回头试一下3.6.0

    [回复]

  5. 52nlp用户

    您好,我这边python调3.6的也慢啊,怎么办?

    [回复]

    52nlp 回复:

    慢没办法,它本身java写得,又多了一层Python调用,实在不行,先用原生的吧

    [回复]

  6. lls

    这里遇到了一个问题,能帮我解答一下吗?万分感谢!

    >>> segmenter = StanfordSegmenter(path_to_jar="stanford-segmenter-3.4.1.jar",
    ... path_to_sihan_corpora_dict="./data",path_to_model="./data/pku.gz",
    ... path_to_dict="./data/dict-chris6.ser.gz")
    Traceback (most recent call last):
    File "", line 3, in
    File "/usr/local/lib/python3.4/dist-packages/nltk/tokenize/stanford_segmenter.py", line 60, in __init__
    verbose=verbose)
    File "/usr/local/lib/python3.4/dist-packages/nltk/internals.py", line 719, in find_jar
    searchpath, url, verbose, is_regex))
    File "/usr/local/lib/python3.4/dist-packages/nltk/internals.py", line 714, in find_jar_iter
    raise LookupError('\n\n%s\n%s\n%s' % (div, msg, div))
    LookupError:

    ===========================================================================
    NLTK was unable to find slf4j-api.jar! Set the CLASSPATH environment
    variable.

    [回复]

    52nlp 回复:

    报错提示的是Jar环境变量问题,可以参考一下这个issue:

    https://github.com/nltk/nltk/issues/1239

    [回复]

    prigioni 回复:

    没有看懂这个网页下面的

    [回复]

  7. 我是使用jpype库,在python中调用jar,解决了每次都需要load的问题。主要代码如下:
    jvm_path = jpype.getDefaultJVMPath()
    jvm_args = ["-Xmx1g", "-Djava.ext.dirs=%s" % ('/usr/local/share/stanford_nltk/stanford-segmenter-2015-12-09/')]
    jpype.startJVM(jvm_path, jvm_args[0], jvm_args[1])

    AR_Segmenter = jpype.JClass('edu.stanford.nlp.international.arabic.process.ArabicSegmenter')
    Properties = jpype.JClass('java.util.Properties')

    options = Properties()
    options.setProperty('orthoOptions','removeProMarker')
    options.setProperty('loadClassifier',ARClassifierData)
    ARSegmenter = AR_Segmenter(options)
    if ARSegmenter.flags.loadClassifier:
    ARSegmenter.loadSegmenter(ARSegmenter.flags.loadClassifier, options)

    [回复]

    52nlp 回复:

    谢谢

    [回复]

    春越 回复:

    您好,请问您的jpype的安装环境是什么呢?我在JDK1.8,python2.6 Ubuntu14 64bit机器安装成功了,但import jpype时,出现了“_jpype.so: undefined symbol _ZTVNSt7__cxx1118basic_stringstreamIcSt11char_traitsIcESaIcEEE”的错误

    [回复]

  8. 小黄鸭

    您好,用了您的教程,成功配置好了~非常感谢
    另外有个小问题,我在处理带有英文单词的中文文本时,英文单词会被切割成一个个字母,有什么好的解决方法吗?

    [回复]

    52nlp 回复:

    这个是斯坦福分词器的问题吧?或者你可以试一下mecab中文分词,这类问题已经处理掉了。

    [回复]

    小黄鸭 回复:

    是的,这是斯坦福分词器会这样。想问您知不知道有没有什么参数可以修改后,英文单词就不会产生这种问题

    [回复]

    52nlp 回复:

    抱歉,这个我不太清楚,按说一个成熟的中文分词器应该这个地方应该可控的。

  9. LjessonS

    老师,您好!我也遇到了楼上的问题,在linux上使用没有问题,在windows上报错,就是无法找到那个坑爹的主类。
    错误: 找不到或无法加载主类 edu.stanford.nlp.ie.crf.CRFClassifier
    后面的错误信息和前面的同学一样。
    环境变量我也设置了,好纠结,不知道该怎么解决这个问题,无法分词寸步难行,主要还是想用nltk这个包。麻烦老师帮忙解决一下,万分感谢!

    [回复]

    52nlp 回复:

    抱歉,windows下的情况我不太熟悉

    [回复]

    nlpxsk 回复:

    这个问题主要是nltk/tokenize/stanford_segmenter.py第63行的分隔符强制写成了“:”,在windows下面不行,应该是";",可以用os.pathsep代替。

    [回复]

    Zero 回复:

    确实是这样,改一下就可以了,赞一个!

    [回复]

  10. dbli

    >>> segmenter = StanfordSegmenter(path_to_jar="stanford-segmenter-3.4.1.jar", path_to_sihan_corpora_dict="./data", path_to_model="./data/pku.gz", path_to_dict="./data/dict-chris6.ser.gz")
    Traceback (most recent call last):
    File "", line 1, in
    File "C:\ProgramData\Anaconda2\lib\site-packages\nltk\tokenize\stanford_segmenter.py", line 55, in __init__
    verbose=verbose)
    File "C:\ProgramData\Anaconda2\lib\site-packages\nltk\internals.py", line 719, in find_jar
    searchpath, url, verbose, is_regex))
    File "C:\ProgramData\Anaconda2\lib\site-packages\nltk\internals.py", line 635, in find_jar_iter
    (name_pattern, path_to_jar))
    LookupError: Could not find stanford-segmenter.jar jar file at stanford-segmenter-3.4.1.jar
    有人知道这个错误吗?

    [回复]

  11. dbli

    >>> segmenter = StanfordSegmenter(path_to_jar="stanford-segmenter-3.4.1.jar", path_to_sihan_corpora_dict="./data", path_to_model="./data/pku.gz", path_to_dict="./data/dict-chris6.ser.gz")
    Traceback (most recent call last):
    File "", line 1, in
    File "C:\ProgramData\Anaconda2\lib\site-packages\nltk\tokenize\stanford_segmenter.py", line 55, in __init__
    verbose=verbose)
    File "C:\ProgramData\Anaconda2\lib\site-packages\nltk\internals.py", line 719, in find_jar
    searchpath, url, verbose, is_regex))
    File "C:\ProgramData\Anaconda2\lib\site-packages\nltk\internals.py", line 635, in find_jar_iter
    (name_pattern, path_to_jar))
    LookupError: Could not find stanford-segmenter.jar jar file at stanford-segmenter-3.4.1.jar
    有人遇到过这种错误吗?

    [回复]

  12. km

    您好 我在使用过程中遇到了如下问题:

    Traceback (most recent call last):
    File "./test.py", line 13, in
    res = segmenter.segment(sentence)
    File "/usr/local/lib/python2.7/dist-packages/nltk/tokenize/stanford_segmenter.py", line 164, in segment
    return self.segment_sents([tokens])
    File "/usr/local/lib/python2.7/dist-packages/nltk/tokenize/stanford_segmenter.py", line 192, in segment_sents
    stdout = self._execute(cmd)
    File "/usr/local/lib/python2.7/dist-packages/nltk/tokenize/stanford_segmenter.py", line 211, in _execute
    stdout, _stderr = java(cmd, classpath=self._stanford_jar, stdout=PIPE, stderr=PIPE)
    File "/usr/local/lib/python2.7/dist-packages/nltk/internals.py", line 129, in java
    p = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr)
    File "/usr/lib/python2.7/subprocess.py", line 711, in __init__
    errread, errwrite)
    File "/usr/lib/python2.7/subprocess.py", line 1343, in _execute_child
    raise child_exception
    TypeError: execv() arg 2 must contain only strings

    我使用的segmenter是最新的3.7.0版本。请问这是什么问题呢?

    谢谢

    [回复]

  13. km

    您好。我在按照您所述的方法尝试时,在执行segmenter.segment(sentence)时发生了如下错误

    Traceback (most recent call last):
    File "./test.py", line 13, in
    res = segmenter.segment(sentence)
    File "/usr/local/lib/python2.7/dist-packages/nltk/tokenize/stanford_segmenter.py", line 164, in segment
    return self.segment_sents([tokens])
    File "/usr/local/lib/python2.7/dist-packages/nltk/tokenize/stanford_segmenter.py", line 192, in segment_sents
    stdout = self._execute(cmd)
    File "/usr/local/lib/python2.7/dist-packages/nltk/tokenize/stanford_segmenter.py", line 211, in _execute
    stdout, _stderr = java(cmd, classpath=self._stanford_jar, stdout=PIPE, stderr=PIPE)
    File "/usr/local/lib/python2.7/dist-packages/nltk/internals.py", line 129, in java
    p = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr)
    File "/usr/lib/python2.7/subprocess.py", line 711, in __init__
    errread, errwrite)
    File "/usr/lib/python2.7/subprocess.py", line 1343, in _execute_child
    raise child_exception
    TypeError: execv() arg 2 must contain only strings

    我安装的segmenter是最新的3.7.0版本,执行环境是64位ubuntu。想请问一下是什么问题呢?

    [回复]

    52nlp 回复:

    抱歉,不清楚

    [回复]

发表评论

电子邮件地址不会被公开。 必填项已用*标注