词性检测

目的

词性检测的主要原因是:作者觉得自己手上目前没有比较靠谱的词性标注原料。来自互联网的资料五花八门,数量庞大,想逐一挑选和纠正,太消耗个人精力。因此,作者生出了让程序协助词性检测的想法。

词性检测和词性标注不是同一件事情。词性检测是单独检测一个词汇可能具有的词性;而词性标注是确定一个词汇在某个句子中所表现出的词性。虽都与词性相关,但两者目的不同。

词性

从程序的角度来看,汉语只不过是标记语言中的一种。那么这个标记具有什么样的语法属性,是均有可能,仅是概率大小不同而已。
实际生活当中,一个词汇的使用方式也是在不断变化的。只要使用的人多了,某个词汇就会多出一些属性;使用的人少了,某些属性也就逐渐消失了。之前的文章之中,作者举了一个“车”这个字的例子。大家可以翻看之前的文章《我的NLP(自然语言处理)历程(8)——频次统计》。

在诸多的词性之中,最容易确定的就是虚词。目前大家经常使用的虚词大约有1000多个。这个部分可以参考《现代汉语虚词词典》。这些虚词包括:副词、连词、介词、感叹词、助词、拟声词【简记为:妇(副)联(连)借(介)炭(叹)助(助)你(拟)】。

数量词是实词中最容易确定的词性,主要因为其特征实在过于明显。有关数量词的提取可以参考:《我的NLP(自然语言处理)历程(16)——提取数量词》。汉语中的量词非常多,且很多量词存在兼类的情况,不过数量上总体有限。读者可以参考《现代汉语虚词词典》中的量词与名词搭配表。从其他角度来看,通过名词所对应的量词不同,可以将名词细分成不同类型。

形容词和动词虽然数量也不少,但是总体来说数量依然有限。而且很多形容词和动词有一套造词方法,反之则可以通过词根来确定其词性。

所有词汇当中,最庞大,也是最复杂的非名词莫属。其涉及的范围和数量都是其他几类词汇所不能比的。在做短语或者句子分析时,最常见的就是人名、地名、团体名、机构名、专用名等等名词的识别。再加上缩写、简写和习惯用语,可以说名词家族整个体系的繁杂。所以词性识别的重点在于识别名词,特别是一系列常用的名词。

可能有读者会问:有一个词典库不就可以识别各种名词了,没有作者所说的难度这么大。其实作者最初的想法也是如此。在实际进行了无数的分析之后,才发现事情并非想象的那么简单。
以姓名为例,作者的词典库中有预先内置的姓名大约114万5千多个,另外还有各种外文名字。即使有这么庞大的库做支撑,但是语料中依然有很多姓名不在词典库的预置范围内。在阅读小说时,人名的判定就是个大问题。
除了人名,地名、团体名、机构名、公司名、专有名都是如此。

理论基础

词性检测的理论基础依然是基于相关系数。这里需要使用到集合对个体的相关系数:

设\( S^* = \{ {{s_i}|i \in {N^*}} \} \)是某个字符串集合。对于指定的语料,按照一定的统计方式进行统计,得到每个对应字符串的词频集合\( F = \{ {({s_i},{f_i})|i \in {N^*}} \} \),其中\({f_i}\)是字符串\({s_i}\)在语料中出现的频次。

现有字符串\(s\),其在语料中的频次为 \(f\) 。那么定义这个集合 \(S^*\)与字符串\(s\)之间的相关系数\(\gamma\):

\[ \gamma ({S^*} \cdot s) = \frac{{\sum\limits_{i = 1}^N {{f_{{s_i} \cdot s}}} }}{2}\left( {\frac{1}{{\sum\limits_{i = 1}^N {{f_{{s_i}}}} }} + \frac{1}{f}} \right) = \frac{{\sum\limits_{i = 1}^N {{f_{{s_i} \cdot s}}} }}{2}\left( {\frac{1}{{\sum\limits_{i = 1}^N {{f_i}} }} + \frac{1}{f}} \right) \]

其中\({f_{{s_i} \cdot s}}\)表明是以集合\(S^*\)中的字符串元素做前缀,与字符串\(s\)拼接后所得字符串在语料中出现的频次。

例如:以检测量词和字符串“苹果”的相关系数为例。

数量词字符串集合:\(S=\{界,日,天,…,个,对,…,辈子\}\)

\(f_{量词}\)是所有量词的词频之和,即:

\[f_{量词}=f_{界}+f_{日}+f_{天}+…+f_{个}+…+f_{对}+…+f_{辈子}\]

\(f_{{量词} \cdot {苹果}}\)是所有人工合成词的词频之和,即:

\[f_{{量词}⋅{苹果}}=f_{{界}⋅{苹果}}+f_{{日}⋅{苹果}}+f_{{天}⋅{苹果}}+…+f_{{个}⋅{苹果}}+…+f_{{对}⋅{苹果}}+…+f_{{辈子}⋅{苹果}}\]

量词和字符串“苹果”的相关系数如下:

\[\gamma_{{量词}⋅{苹果}}=\frac {f_{{量词}⋅{苹果}}}{2}(\frac {1}{f_{量词}} + \frac {1}{f_{苹果}}) \cong 0.05319837 \]

准备工作

由于语料库十分庞大,因此如果采用逐一扫描的方式来统计\(f_{s_i⋅s}\)将是一个十分耗时的过程。为了能加快相关的统计工作,需要对语料进行预处理。即,将语料按照可能存在的检索需求,进行完全拆分。这个过程十分耗时,而且需要有一个庞大的数据库做支持。

所谓按照可能存在的检索需求,就是要符合人工合成词的检索需求。因此,拆分出的字符串长度不少于两个字符。最长字符串,按理论应该没有限制。但是实际上过长的字符串已经没有实际意义。
常见的词性检测应该集中在对2~3个字符长度字符串做词性检测。再长的字符串,要么可以通过分词拆分成短字符串,要么是特殊的名字和专有名词。因此,可以不用过多关注超长字符串的问题。
所以“完全展开过程”应该是:字符串长度从2起,至6终结。这样可以保证在被检测字符串之前加上其他的检测用字符串的数据应该完全被数据库覆盖。

这个数据库十分庞大,数据记录总数将近3亿条。

字符串长度数量
23,560,196
335,620,269
476,207,780
592,973,787
689,034,672
总计297,396,704

为了缩减数据表中的记录数量,已采取了如下两个方法:
(1)凡是在词典库中出现的数据,在该数据库中将不再重复记录。
(2)凡是统计频次仅为1次的数据,一律不予保存,就当其未出现过。

检测函数

以上述数据库为基础,可以快速检测相关的数据。

以量词检测函数为例:

        public static double GetQtyGamma(string value)
        {
#if DEBUG
            Debug.Assert(!string.IsNullOrEmpty(value));
#endif
            // 获得量词
            string[] quantifiers = Quantity.GetQuantifiers();
#if DEBUG
            // 获得计数
            Console.WriteLine(string.Format(
                "\t[数量词]频次统计:{0}",
                CounterTool.GetCount(quantifiers)));
            // 单词计数
            Console.WriteLine(string.Format(
                "\t[\"{0}\"]频次统计:{1}", value,
                CounterTool.GetCount(value, true)));
            // 后缀计数
            Console.WriteLine(string.Format(
                "\t[数量词]+[\"{0}\"]频次统计:{1}", value,
                CounterTool.GetCount(false, value, quantifiers)));
            // 相关系数
            Console.WriteLine(string.Format(
                "\t[数量词]+[\"{0}\"]相关系数:{1}", value,
                GammaTool.GetGamma(false, value, quantifiers)));
#endif
            // 返回结果
            return GammaTool.GetGamma(false, value, quantifiers);
        }

        // 使用全局计数
        public static double GetGamma(bool prefix, string single, string[] groups)
        {
#if DEBUG
            Debug.Assert(!string.IsNullOrEmpty(single));
            Debug.Assert(groups != null && groups.Length > 0);
#endif
            // 频次参数
            double fSingle = CounterTool.GetCount(single, true);
            // 检查结果
            if (fSingle <= 0) return -1.0f;

            // 频次参数
            double fCombination = 0; double fGroup = 0;
            // 增加数据
            foreach (string group in groups)
            {
                // 获得计数
                int fg = CounterTool.GetCount(group, true); if (fg > 0) fGroup += fg;
                // 获得新值
                string newValue = prefix ? (single + group) : (group + single);
                // 获得计数
                int count = CounterTool.GetCount(newValue, true); if (count > 0) fCombination += count;
            }
            // 检查结果
            if (fGroup <= 0.0f || fSingle <= 0.0f) return -1.0f;
            //返回结果
            return fCombination * 0.5f * (1.0f / fGroup + 1.0f / fSingle);
        }

数据分析

数据分析的时候,要时刻注意一点:汉语中,词的兼类问题是十分普遍的现象。

1 量词前缀

名词的特点之一,就是可以在其前面使用量词。

E:\……\net6.0>NLPConsole -scan /qty 苹果
        [数量词]频次统计:184035090
        ["苹果"]频次统计:61041
        [数量词]+["苹果"]频次统计:6169
        [数量词]+["苹果"]相关系数:0.05054837029218231
Time elapsed : 1257 ms

此例中可以看出量词的统计频次非常大。

再测试下其他的词汇,例如:形容词、动词。

E:\……\net6.0>NLPConsole -scan /qty 美丽
        [数量词]频次统计:184035090
        ["美丽"]频次统计:56219
        [数量词]+["美丽"]频次统计:9083
        [数量词]+["美丽"]相关系数:0.08080697515876034
Time elapsed : 624 ms

E:\……\net6.0>NLPConsole -scan /qty 行走
        [数量词]频次统计:184035090
        ["行走"]频次统计:14513
        [数量词]+["行走"]频次统计:2531
        [数量词]+["行走"]相关系数:0.08720456124030233
Time elapsed : 817 ms

从实际结果中,可以看出:形容词、动词所得的相关系数和名词所得的相关系数差别并不大。
究其主要原因有以下几点:
(1)汉语语法中,仅说名词之前可以加数量词。这种说法并不是一个断言。
(2)动词和名词某种意义上处于一种等价地位。
或者说,有兼类的情况。如上面所说的“行走”一词,可以是动词,也可以是名词。
(3)量词之中也存在兼类的情况。例如:“对”。
(4)形容词的相关系数也不低,主要原因是被修饰词后置引起的。
例如:“一个美丽的姑娘”。其实,“一个”是修饰的“姑娘”,而不是“美丽”。但是因为都同时修饰姑娘这个词,所以“美丽”的位置恰好与量词在一起。而这种情况在句子中是很普遍的。

2 助词后缀

以助词为后缀,求得相关系数:

E:\……>NLPConsole -scan /aux 苹果
        [助词]频次统计:94065267
        ["苹果"]频次统计:61041
        ["苹果"]+[助词]频次统计:5117
        ["苹果"]+[助词]相关系数:0.04194165014270064
Time elapsed : 135 ms

E:\……>NLPConsole -scan /aux 美丽
        [助词]频次统计:94065267
        ["美丽"]频次统计:56219
        ["美丽"]+[助词]频次统计:27127
        ["美丽"]+[助词]相关系数:0.24140604341173683
Time elapsed : 126 ms

E:\……>NLPConsole -scan /aux 行走
        [助词]频次统计:94065267
        ["行走"]频次统计:14513
        ["行走"]+[助词]频次统计:2546
        ["行走"]+[助词]相关系数:0.08772799605205288
Time elapsed : 122 ms

从实际结果中可以看出,形容词的相关系数偏高。名词和动词的相关系数相差也并不是很大。
形容词的相关系数偏高,主要是形容词后面可以加助词“的”形成修饰语。而且这种情况的频次比较高。

3 副词前缀

以副词为前缀,求得相关系数:

E:\……>NLPConsole -scan /adv 苹果
        [副词]频次统计:224093234
        ["苹果"]频次统计:61041
        [副词]+["苹果"]频次统计:6809
        [副词]+["苹果"]相关系数:0.055789180312606115
Time elapsed : 1954 ms

E:\……>NLPConsole -scan /adv 美丽
        [副词]频次统计:224093234
        ["美丽"]频次统计:56219
        [副词]+["美丽"]频次统计:11707
        [副词]+["美丽"]相关系数:0.10414572451703687
Time elapsed : 1120 ms

E:\……>NLPConsole -scan /adv 行走
        [副词]频次统计:224093234
        ["行走"]频次统计:14513
        [副词]+["行走"]频次统计:1900
        [副词]+["行走"]相关系数:0.0654627937064345
Time elapsed : 1403 ms

从实际情况可以看出,基本如前面的差不多。
按汉语语法理论,名词前面是不可以加副词的。其相关系数较高的原因主要源于副词中存在兼类词。
形容词的相关系数偏高,主要是形容词前面可以加“很”,“太”这类副词,而且频次比较高。

4 方位词后缀

以方位词为后缀,求得相关系数:

E:\……>NLPConsole -scan /loc 苹果
        [方位词]频次统计:35232230
        ["苹果"]频次统计:61041
        ["苹果"]+[方位词]频次统计:1112
        ["苹果"]+[方位词]相关系数:0.00912441290820976
Time elapsed : 77 ms

E:\……>NLPConsole -scan /loc 美丽
        [方位词]频次统计:35232230
        ["美丽"]频次统计:56219
        ["美丽"]+[方位词]频次统计:337
        ["美丽"]+[方位词]相关系数:0.0030019899026216307
Time elapsed : 66 ms

E:\……>NLPConsole -scan /loc 行走
        [方位词]频次统计:35232230
        ["行走"]频次统计:14513
        ["行走"]+[方位词]频次统计:437
        ["行走"]+[方位词]相关系数:0.015061669219810713
Time elapsed : 66 ms

可以看到,相关系数比之前的数值均有所下降。

5 特殊检测词

由上面的例子可以看出,全盘检测都看不出太大的区别。毕竟兼类以及语料的情况太复杂,都“一锅烩”的结果就是数据完全看不出有任何差异。因此需要选择出针对性比较强的语法约束条件。

这里选择了五组检测词:
(1)第一组:“不”、“太”、“很”;
该组词做前缀,主要针对名词。
(2)第二组:“着”、“了”、“过”;
该组词做后缀,主要针对动词。
(3)第三组:“用”、“把”、“被”;
该组词做前缀,主要针对名词。
(4)第四组:“的”、“地”、“得”;
该组词做后缀。
(5)第五组:“应该”、“可以”、“必须”;
该组词做前缀。

E:\……>NLPConsole -scan /spec 苹果
        ("不苹果").gamma = -1
        ("太苹果").gamma = 1.74279043217096E-05
        ("很苹果").gamma = -1
        ("苹果着").gamma = 7.490252223886735E-05
        ("苹果了").gamma = 0.0003869355618264652
        ("苹果过").gamma = 0.00018352561538365555
        ("用苹果").gamma = 0.0023385976879444965
        ("把苹果").gamma = 0.0017751175108041606
        ("被苹果").gamma = 0.0021316122283297784
        ("苹果的").gamma = 0.030490014078203818
        ("苹果地").gamma = 0.0015586106537509995
        ("苹果得").gamma = 9.192530120629668E-05
        ("应该苹果").gamma = -1
        ("可以苹果").gamma = -1
        ("必须苹果").gamma = -1
Time elapsed : 87 ms

E:\……>NLPConsole -scan /spec 美丽
        ("不美丽").gamma = 0.0007599547106898916
        ("太美丽").gamma = 0.0007627386688712581
        ("很美丽").gamma = 0.0038032729024592904
        ("美丽着").gamma = 6.317553960423184E-05
        ("美丽了").gamma = 0.0013492221676769916
        ("美丽过").gamma = 0.00022611620104658425
        ("用美丽").gamma = 0.0005885560989837975
        ("把美丽").gamma = 0.0006616951436454574
        ("被美丽").gamma = 0.0002206812867039208
        ("美丽的").gamma = 0.2304138619884229
        ("美丽地").gamma = 0.0014568752042141554
        ("美丽得").gamma = 0.0021652014229106243
        ("应该美丽").gamma = -1
        ("可以美丽").gamma = 2.8650606360143836E-05
        ("必须美丽").gamma = -1
Time elapsed : 47 ms

E:\……>NLPConsole -scan /spec 行走
        ("不行走").gamma = 0.00031048851632564206
        ("太行走").gamma = 0.00017487303578597813
        ("很行走").gamma = -1
        ("行走着").gamma = 0.008023292532419653
        ("行走了").gamma = 0.010796411202188374
        ("行走过").gamma = 0.0029758346443668738
        ("用行走").gamma = 0.0002076767375768556
        ("把行走").gamma = 6.975546058301331E-05
        ("被行走").gamma = -1
        ("行走的").gamma = 0.05714416042463867
        ("行走地").gamma = 0.0006910227439428804
        ("行走得").gamma = 0.0006923500112109924
        ("应该行走").gamma = -1
        ("可以行走").gamma = 0.0013341147350400978
        ("必须行走").gamma = -1
Time elapsed : 50 ms
测试词名词形容词动词
不~敏感
太~敏感敏感
很~敏感敏感
~着敏感(分及物和不及物动词)
~了
~过
用~(修饰后置)
把~(修饰后置)
被~(修饰后置)
~的
~地
~得敏感
应该~敏感敏感
可以~敏感敏感
必须~敏感敏感

表中所谓敏感,就是指相关系数\(\gamma≤0.0001\) 。由这十五个检测词就可以初步探测出被检测词的特点。如果没有特点,那就说明被检测词可以兼类。

E:\……>NLPConsole -scan /spec 红
        ("不红").gamma = 0.0005430812757101494
        ("太红").gamma = 0.00013447296598825616
        ("很红").gamma = 0.0004387730098900496
        ("红着").gamma = 0.004553477729989197
        ("红了").gamma = 0.014330391411426226
        ("红过").gamma = 0.00024276108727186435
        ("用红").gamma = 0.0019335491602280033
        ("把红").gamma = 0.0011080546781163205
        ("被红").gamma = 0.0012437309736850009
        ("红的").gamma = 0.019893694786107103
        ("红地").gamma = 0.0020155076840044418
        ("红得").gamma = 0.0016046403200530566
        ("应该红").gamma = 8.326101090907441E-06
        ("可以红").gamma = 1.6545544791676688E-05
        ("必须红").gamma = 1.2285809035512935E-05
Time elapsed : 44 ms

E:\……>NLPConsole -scan /spec 红色
        ("不红色").gamma = -1
        ("太红色").gamma = -1
        ("很红色").gamma = -1
        ("红色着").gamma = 2.448728674206622E-05
        ("红色了").gamma = 0.0002292156482246504
        ("红色过").gamma = 4.913114112848873E-05
        ("用红色").gamma = 0.0027435484011925472
        ("把红色").gamma = 0.0006544722399891065
        ("被红色").gamma = 0.0008934353460842746
        ("红色的").gamma = 0.13311114913618552
        ("红色地").gamma = 0.0030997089204052525
        ("红色得").gamma = 6.156164778740083E-05
        ("应该红色").gamma = -1
        ("可以红色").gamma = -1
        ("必须红色").gamma = -1
Time elapsed : 39 ms

例如:“红”是形容词(有动词兼类),“红色”确是名词。在这里可以明显地区分开。

对于这种特殊词汇的检测方式,就好像在星空中寻找最明亮的新星为定位坐标一样。由这些定位点确定的坐标可以进一步按照矢量进行处理。此时可以使用神经网络算法进行筛选。关键要把一个字符串是否可以做名词、动词和形容词判别出来,后续则可以进行词性标注。

以字符串“苹果”作为训练依据。

14:28:06 > BPCoach.GetVector : 数据记录已加载!
        名词 = 0.9933291826025082
        动词 = 4.4472115983278487E-05
        形容词 = 0
        ("不苹果").gamma = -1
        ("太苹果").gamma = 1.74279043217096E-05
        ("很苹果").gamma = -1
        ("苹果着").gamma = 7.490252223886735E-05
        ("苹果了").gamma = 0.0003869355618264652
        ("苹果过").gamma = 0.00018352561538365555
        ("用苹果").gamma = 0.0023385976879444965
        ("把苹果").gamma = 0.0017751175108041606
        ("被苹果").gamma = 0.0021316122283297784
        ("苹果的").gamma = 0.030490014078203818
        ("苹果地").gamma = 0.0015586106537509995
        ("苹果得").gamma = 9.192530120629668E-05
        ("应该苹果").gamma = -1
        ("可以苹果").gamma = -1
        ("必须苹果").gamma = -1
        ("苹果前").gamma = 0.0008783853071032054
        ("苹果内").gamma = 0.0003621079933493595
        ("苹果间").gamma = -1
……
第693次迭代!
        名词 = 0.9867707534473626
        动词 = 0.00883669943098205
        形容词 = 0.00889068245754069
        误差 = 9.968024435124802E-05