python代码解析器_500 行 Python 代码做一个英文解析器

They ate the pizza with anchovies

正确的解析是连接“with”和“pizza”,而错误的解析将“with”和“eat”联系在了一起:

过去的一些年,自然语言处理(NLP)社区在语法分析方面取得了很大的进展。现在,小小的 Python 实现可能比广泛应用的 Stanford 解析器表现得更出色。

解析器      准确度     速度(词/秒)      语言      代码行数

Stanford    89.6%     19                    Java      > 50,000[1]

parser.py  89.8%     2,020               Python    ~500

Redshift     93.6%

2,580

Cython   ~4,000

文章剩下的部分首先设置了问题,接着带你了解为此准备的 简洁实现

。parser.py 代码中的前 200 行描述了词性的标注者和学习者( 这里

)。除非你非常熟悉 NLP 方向的研究,否则在研究这篇文章之前至少应该略读。

Cython 系统和 Redshift 是为我目前的研究而写的。和麦考瑞大学的合同到期后,我计划六月份对它进行改进,用于一般用途。目前的版本托管在  GitHub

上。

问题描述

在你的手机中输入这样一条指令是非常友善的:

Set volume to zero when I’m in a meeting, unless John’s school calls.

接着进行适当的策略配置。在 Android 系统上,你可以应用 Tasker 做这样的事情,而 NL 接口会更好一些。接收可以编辑的语义表示,你就能了解到它认为你表达的意思,并且可以修正他的想法,这样是特别友善的。

这项工作有很多问题需要解决,但一些种类的句法形态绝对是必要的。我们需要知道:

Unless John’s school calls, when I’m in a meeting, set volume to zero

是解析指令的又一种方式,而

Unless John’s school, call when I’m in a meeting

表达了完全不同的意思。

依赖解析器返回一个单词与单词间的关系图,使推理变得更容易。关系图是树形结构,有向边,每个节点(单词)有且仅有一个入弧(头部依赖)。

用法示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

>>> parser

=

parser.Parser()

>>> tokens

=

"Set the volume to zero when I 'm in a meeting unless John 's school calls"

.split()

>>> tags, heads

=

parser.parse(tokens)

>>> heads

[

-

,

,

,

,

,

,

,

,

,

,

,

,

,

,

,

]

>>>

for

i, h

in

enumerate

(heads):

... head

=

tokens[heads[h]]

if

h >

=

else

'None'

...

print

(tokens[i]

+

'

+

head])

Set

<

-

-

None

the <

-

-

volume

volume <

-

-

Set

to <

-

-

Set

zero <

-

-

to

when <

-

-

Set

I <

-

-

'm

'm <

-

-

when

in

<

-

-

'm

a <

-

-

meeting

meeting <

-

-

in

unless <

-

-

Set

John <

-

-

's

's <

-

-

calls

school <

-

-

calls

calls <

-

-

unless

一种观点是通过语法分析进行推导比字符串应该稍稍容易一些。语义分析映射有望比字面意义映射更简单。

这个问题最让人困惑的是正确性是由惯例,即注释指南决定的。如果你没有阅读指南并且不是一个语言学家,就不能判断解析是否正确,这使整个任务显得奇怪和虚假。

例如,在上面的解析中存在一个错误:根据 Stanford 的注释指南规定,“John’s school calls” 存在结构错误。而句子这部分的结构是指导注释器如何解析一个类似于“John’s school clothes”的例子。

这一点值得深入考虑。理论上讲,我们已经制定了准则,所以“正确”的解析应该相反。如果我们违反约定,有充分的理由相信解析任务会变得更加困难,因为任务和其他语>法的一致性会降低。【2】但是我们可以测试经验,并且我们很高兴通过反转策略获得优势。

我们确实需要惯例中的差异——我们不希望接收相同的结构,否则结果不会很有用。注释指南在哪些区别使下游应用有效和哪些解析器可以轻松预测之间取得平衡。

映射树

在决定构建什么样子的关系图时,我们可以进行一项特别有效的简化:对将要处理的关系图结构进行限制。它不仅在易学性方面有优势,在加深算法理解方面也有作用。大部分的>英文解析工作中,我们遵循约束的依赖关系图就是映射树:

树。除了根外,每个单词都有一个弧头。

映射关系。针对每对依赖关系 (a1, a2)和 (b1, b2),如果 a1 < b2, 那么 a2 >= b2。换句话说,依赖关系不能交叉。不可能存在一对 a1 b1 a2 b2 或者 b1 a1 b2 a2 形式的依赖关系。

在解析非映射树方面有丰富的文献,解析无环有向图方面的文献相对而言少一些。我将要阐述的解析算法用于映射树领域。

贪婪的基于转换的解析

我们的语法分析器以字符串符号列表作为输入,输出代表关系图中边的弧头索引列表。如果第 i 个弧头元素是 j, 依赖关系包括一条边 (j, i)。基于转换的语法分析器>是有限状态转换器;它将 N 个单词的数组映射到 N 个弧头索引的输出数组。

start  MSNBC  reported  that  Facebook  bought  WhatsApp  for  $16bn  root

0       2              9                 2      4                    2           4                 4      7          0

弧头数组表示了 MSNBC 的弧头:MSNBC 的单词索引是1,reported 的单词索引是2, head[1] == 2。你应该已经发现为什么树形结构如此方便——如果我们输出一个 DAG 结构,这种结构中的单词可能包含多个弧头,树形结构将不再工作。

虽然 heads 可以表示为一个数组,我们确实喜欢保持一定的替代方式来访问解析,以方便高效的提取特征。Parse 类就是这样:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

class

Parse(

object

):

def

__init__(

self

, n):

self

.n

=

n

self

.heads

=

[

None

]

*

(n

-

)

self

.lefts

=

[]

self

.rights

=

[]

for

i

in

range

(n

+

):

self

.lefts.append(DefaultList(

))

self

.rights.append(DefaultList(

))

def

add_arc(

self

, head, child):

self

.heads[child]

=

head

if

child < head:

self

.lefts[head].append(child)

else

:

self

.rights[head].append(child)

和语法解析一样,我们也需要跟踪句子中的位置。我们通过在 words 数组中置入一个索引和引入栈机制实现,栈中可以压入单词,设置单词的弧头时,弹出单词。所以我们的状态数据结构是基础。

一个索引 i, 活动于符号列表中

到现在为止语法解析器中的加入的依赖关系

一个包含索引 i 之前产生的单词的栈,我们已为这些单词声明了弧头。

解析过程的每一步都应用了三种操作之一:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

SHIFT

=

; RIGHT

=

; LEFT

=

MOVES

=

[SHIFT, RIGHT, LEFT]

def

transition(move, i, stack, parse):

global

SHIFT, RIGHT, LEFT

if

move

=

=

SHIFT:

stack.append(i)

return

i

+

elif

move

=

=

RIGHT:

parse.add_arc(stack[

-

], stack.pop())

return

i

elif

move

=

=

LEFT:

parse.add_arc(i, stack.pop())

return

i

raise

GrammarError(

"Unknown move: %d"

%

move)

LEFT 和 RIGHT 操作添加依赖关系并弹栈,而 SHIFT 压栈并增加缓存中 i 值。

因此,语法解析器以一个空栈开始,缓存索引为0,没有依赖关系记录。选择一个有效的操作,应用到当前状态。继续选择操作并应用直到栈为空且缓存索引到达输入数组的终点。(没有逐步跟踪是很难理解这种算法的。尝试准备一个句子,画出映射解析树,接着通过选择正确的转换序列遍历完解析树。)

下面是代码中的解析循环:

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

class

Parser(

object

):

...

def

parse(

self

, words):

tags

=

self

.tagger(words)

n

=

len

(words)

idx

=

stack

=

[

]

deps

=

Parse(n)

while

stack

or

idx < n:

features

=

extract_features(words, tags, idx, n, stack, deps)

scores

=

self

.model.score(features)

valid_moves

=

get_valid_moves(i, n,

len

(stack))

next_move

=

max

(valid_moves, key

=

lambda

move: scores[move])

idx

=

transition(next_move, idx, stack, parse)

return

tags, parse

def

get_valid_moves(i, n, stack_depth):

moves

=

[]

if

i < n:

moves.append(SHIFT)

if

stack_depth >

=

:

moves.append(RIGHT)

if

stack_depth >

=

:

moves.append(LEFT)

return

moves

我们以标记的句子开始,进行状态初始化。然后将状态映射到一个采用线性模型评分的特征集合。接着寻找得分最高的有效操作,应用到状态中。

这里的评分模型和 词性标注

中的一样工作。如果对提取特征和使用线性模型评分的观点感到困惑,你应该复习这篇文章。下面是评分模型如何工作的提示:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

class

Perceptron(

object

)

...

def

score(

self

, features):

all_weights

=

self

.weights

scores

=

dict

((clas,

)

for

clas

in

self

.classes)

for

feat, value

in

features.items():

if

value

=

=

:

continue

if

feat

not

in

all_weights:

continue

weights

=

all_weights[feat]

for

clas, weight

in

weights.items():

scores[clas]

+

=

value

*

weight

return

scores

这里仅仅对每个特征的类权重求和。这通常被表示为一个点积,然而我发现处理很多类时就不太适合了。

定向解析器(RedShift)遍历多个候选元素,但最终只会选择最好的一个。我们将关注效率和简便而忽略其准确性。我们只进行了单一的分析。我们的搜索策略将是完全贪婪的,就像词性标记一样。我们将锁定在选择的每一步。

如果认真阅读了词性标记,你可能会发现下面的相似性。我们所做的是将解析问题映射到一个使用“扁平化”解决的序列标记问题,或者非结构化的学习算法(通过贪婪搜索)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
python根据需求完成一个TXT解析器的简单开发 一 修改说明: 需求一: 一开始说要解析UECapabilityInfo 消息里的supportedBandCombination-r10 这个IE里的CA组合转化成易阅读的表现形式. 我以为一组CA组合就是一组: bandEUTRA-r10 ca-BandwidthClassUL-r10 ca-BandwidthClassDL-r10 supportedMIMO-CapabilityDL-r10 功能实现: 有效信息筛选:于是就用循环把UECapabilityInformation的数据里每一作为一个元素放到list里面 然后用bandEUTRA-r10作为一组CA的识别信息、在筛选出同组ca-BandwidthClassUL-r10、ca-BandwidthClassDL-r10、supportedMIMO-CapabilityDL-r10的信息,添加保存到字符串中,然后再把字符串作为元素添加到list中去。最后遍历list的元素写入目标文件 需求二: 然后收到反馈CA组合的理解是错误的。一组CA组合应该是以大括号作为识别的,里面可能包含多组: bandEUTRA-r10: ca-BandwidthClassUL-r10 ca-BandwidthClassDL-r10 supportedMIMO-CapabilityDL-r10 CA组合识别原理:在查看UECapabilityInformation内的CA组合后 发现CA组合内第一个 bandEUTRA-r10因为比其他bandEUTRA-r10多了一层的CA组合的大括号,所以如果给每一增加索引的话就会发现除了第一个bandEUTRA-r10,其他bandEUTRA-r10到上一个supportedMIMO-CapabilityDL-r10的距离都是一样的,为了减少复杂度,我删除了所有’{’,这样所有除了所有CA组合第一个bandEUTRA-r10往上第四是’}’其他bandEUTRA-r10的往上第四都是supportedMIMO-CapabilityDL-r10 功能实现: 添加索引:便利时用了for enumerate()循环,这样便利时可以在循环时,自动为每个元素生成索引 CA组合识别:在识别到bandEUTRA-r10时,增加一个判断if datalist1[index-4].startswith(),如果bandEUTRA-r10的往上第四是supportedMIMO-CapabilityDL-r10说明同组CA未结束,把筛选的有效信息强制类型转换后添加在上个元素末尾,反之则说明是个新的CA组合,往列表里添加一个新的元素。 需求三: 之后收到反馈CA组合虽然识别了,但是排序不,需要按照CA组合支持的band进排序 功能实现: 排序:于是我在识别完CA组合后,增加了一个循环和count(),用CA组合里的’-’给它们归类 比如1AA,11A,21AA是一类;1A-1A,2A-1AA,3A-1A是一类 在用一个中间变量保存开头的band的数字,一个类中把开头支持band的数字字母相同的CA组合归为一 比如1A-21A,1A-22A一类1AA-2AA 1AA-3AA为一类 需求四: 之后收到反馈,CA组合分类不能只按照开头比较分类,不然一但数据多了会对查阅带来极大不便,应该按照每组CA组合中bandEUTRA-r10的值进判断,比如1AA-2AA,1A-2AA和1AA-2A应该归在同一 实现原理:首先我想的是按位比较数字,但是因为字母的数量不稳定,数字的位置不一定对应,然后我就想把数字全部提取出来作为索引,在相应的索引后面添加同组元素,用dict来实现排序。难点就在于从字符串中提取数字。后来在python的正则表达式中找到相关的处理函数compile()(设置匹配对象类型)和findall()(找到所有匹配对象并以list返回)。 功能实现: 第二次排序:在上次的排序中我保留了分类和从小到大的排序。方便提取索引时,索引也是从小到大。每遍历一个元素(CA组合有效信息),就compile()和findall(),从该元素中提取数字组合(在compile()的参数中添加()就能够使提取的内容成为一组数据),然后通过dict自带函数setdefault()添加索引,并可以设置索引值为list类型(dict类型的索引的值不可变,但如果类型为list,list的内容可以进改动),避免重复索引,在本次遍历中完成将元素添加到索引值对应的list中去 需求五: 之后对程序进测试,在测试test2时发现layers增加了fourlayers类型后,用来代表layers的数字2和4会影响分类结果。比如1AA(2)-1AA(2)和1A(4)-1A(2)会被归为两类。 test1:当CA组合的格式为xx-xx-xx-xx-xx(最长可识别为五位元素的组合,再长就需要修改代码) test2:当CA组合包含fourLayers test3:当CA组合缺失某种格式比如xx-xx时发现layers增加了fourlayers 功能实现: : 解除layers对排序的影响:用II 和 IV替代2,4来表示layers,测试后不影响阅读与分类 二、整体程序架构: 1.通过循环和自带的startswith()先将每组CA组合的有效信息识别 2.通过sorted()函数将所有CA组合从小到大排列 3.通过count()函数将所有CA组合根据格式不同分类 4.通过循环和正则表达式的split()对所有CAlist数据进处理(用split处理只是防止出现不必要的错误) 5.通过循环和正则表达式compile()和findall()识别所有CA组合中数字,并将同一组合中的数字合为一个元素(在同一循环,用这个数字的元素作为一个dict的索引),用dict自带的setdefault()进Key的添加顺便设置Key的值为list,避免Key重复,在用append把当前Key的字符串,添加到Key对应值的list中去 6.最后对dict整体遍历,将每一个Key的值输出到文本中去。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值