在SAE上部署及使用标注系统

现在用的这个在线标注系统部署使用起来有些麻烦,过段时间就忘了,所以这里记录下来。

本文包括三部分,第一部分介绍如何在SAE上部署标注系统,包括代码部分和数据库部分。第二部分介绍预处理和后处理,包括主要php文件概述,CoNLL文件与数据库中保存格式的互相转换。第三部分介绍标注系统使用流程,标注者直接看这里就好

部署

代码管理

登录新浪云,在云应用 SAE中创建新应用,全部按照默认配置即可,假设创建的应用名为appname

创建成功后进入应用管理,在左侧的应用菜单中选择代码管理,在右侧出现界面中选择上传代码包,上传标注系统的代码压缩包。这时点击下方的应用链接访问刚才注册的应用网址会提示无法连接数据库,因此接下来需要搭建数据库。

创建数据库

仍然在左侧的数据库与缓存服务菜单中选择共享型MySQL,在右侧出现界面中选择创建MySQL,在下面会出现名为app_appname的数据库。点击数据库对应的管理进入管理界面,在最上面一排选项中选择导入,用以下代码对数据库进行初始化。

注意这里的数据库命名规则是app_+刚刚注册的appname,所以第一句的数据库名需要根据应用名不同修改。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
USE app_sdpannotate;
CREATE TABLE IF NOT EXISTS `charge` (
`sent` varchar(300) NOT NULL,
`tagsent` varchar(300) NOT NULL,
`annoter` varchar(50) NOT NULL,
`tagtime` varchar(30) NOT NULL,
`comment` varchar(300) NOT NULL,
`chargetime` varchar(30) NOT NULL,
`result` int(11) NOT NULL,
`adminname` varchar(50) NOT NULL,
`chargecomment` varchar(300) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=gbk;
CREATE TABLE IF NOT EXISTS `dependancy` (
`sentid` int(11) NOT NULL,
`sent` varchar(1500) CHARACTER SET gbk NOT NULL,
`dep_sent` varchar(1500) CHARACTER SET gbk NOT NULL,
`res_sent` varchar(1500) CHARACTER SET gbk NOT NULL,
`tag` char(2) CHARACTER SET gbk DEFAULT NULL,
`annoter` varchar(50) CHARACTER SET gbk NOT NULL,
`time` varchar(30) CHARACTER SET gbk NOT NULL,
`skip` int(11) NOT NULL,
`comment` varchar(50) CHARACTER SET gbk NOT NULL,
PRIMARY KEY (`sentid`,`annoter`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `dep_charge` (
`sentid` int(11) NOT NULL,
`sent` varchar(300) CHARACTER SET gbk NOT NULL,
`res_sent` varchar(1000) CHARACTER SET gbk NOT NULL,
`annoter` varchar(50) CHARACTER SET gbk NOT NULL,
`res_time` varchar(50) CHARACTER SET gbk NOT NULL,
`comment` varchar(500) CHARACTER SET gbk NOT NULL,
`chargetime` varchar(50) CHARACTER SET gbk NOT NULL,
`result` int(11) NOT NULL,
`adminname` varchar(30) CHARACTER SET gbk NOT NULL,
`chargecomment` varchar(500) CHARACTER SET gbk NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `dep_sentence` (
`sentid` int(11) NOT NULL AUTO_INCREMENT,
`sent` varchar(1500) CHARACTER SET gbk NOT NULL,
`proto_depsent` varchar(1500) CHARACTER SET gbk NOT NULL,
`done` int(11) NOT NULL,
PRIMARY KEY (`sentid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=102904 ;
CREATE TABLE IF NOT EXISTS `signup` (
`uid` varchar(20) NOT NULL,
`username` varchar(40) NOT NULL,
`password` varchar(40) NOT NULL,
`times` int(11) NOT NULL,
`regtime` char(40) DEFAULT NULL,
`complete` int(11) NOT NULL,
`firstweek` int(11) DEFAULT NULL,
`secondweek` int(11) DEFAULT NULL,
`fourweek` int(11) DEFAULT NULL,
`dep_compelete` int(11) DEFAULT NULL,
PRIMARY KEY (`uid`,`username`)
) ENGINE=MyISAM DEFAULT CHARSET=gbk;

至此应用网站已经可以正常访问,用户注册、登录等功能也已经正常,但是还没有待标注数据,因此接下来需要对CoNLL文件进行转化,之后输入数据库中,才能开始标注。

使用流程

  • 在SAE上按照上一节的方法部署代码
  • 用上一节的sql语句初始化数据库
  • 将已经自动标注好的CoNLL文件用下一节中的conll2sql脚本转换为sql语句,用得到的sql语句将句子信息导入数据库(例子中CoNLL文件一共1000句,分成10份,因此会输出10个文件,每个文件对应100句话的sql语句,每份对应一个标注者)
  • 根据上一步的个数以及标注者的annoter这一项的值在系统中注册对应的用户名(例如,0-99句分给annoter user0,对应需要注册一个用户名为user0的账号,用该账号登陆之后需要标注的就是这100句话)
  • 在系统中标注数据,主要是对自动预测结果的修正
  • 从数据库中将dependency表的数据导出
  • 用下一节中的csv2conll脚本将导出的数据转化为CoNLL格式

注:这里使用的CoNLL格式如下所示,其中每句话用一行表示,第一列表示词在句中的位置,第二列表示,第四列表示词性,第七列表示父节点位置,第八列表示该词与当前父节点之间弧上的标签

1       我      我      n       n       _       2       Exp     _       _
2       是      是      v       v       _       5       Root    _       _
3       中国    中国    a       a       _       4       Desc    _       _
4       人    人    n       n       _       2       Clas    _       _

1       早起    早起    n       n       _       2       Exp     _       _
2       使      使      v       v       _       5       Root    _       _
3       人      人      n       n       _       2       Datv    _       _
3       人      人      n       n       _       4       Agt     _       _
4       健康    健康    v       v       _       2       eResu   _       _

预处理及后处理

主要php文件概述及标注流程

index.php

登录界面,在数据库中signup表中查询用户信息。

register.php/registerc.php

注册界面,在数据库中signup表中先查询用户名是否重复,如果不重复则将用户信息插入signup表中。

label_dep.php

标注界面,从数据库中dependancy表中选择annotater项为当前用户的,且还未标注的句子显示。

先后单击词A和B会生成一条由A指向B的弧,右键单击弧标签可以选中该弧(由蓝色变红),此时可以再次右键单击标签进行修改弧标签。在选中一条弧的情况下双击该弧可以删除该弧。标注完毕后点击保存将结果保存到数据库中dependancy表中的res_sent项。(弧标签类别也是在这里定义的)

单击下方的切换修改分词可以修改分词和词性。在最后一列词性上单击右键会出现增加/删除一列的选项,用于修改分词。在每个词性上单击右键都会出现可以替换的词性列表,用于修改词性。(修改的词性也是在这里定义的)

historyc_dep.php

标注历史界面,从数据库中dependancy表中选择annotater项为当前用户且已经标注的句子显示。单击一个句子可以对其重新进行标注。

CoNLL文件转换为sql文件

用如下python脚本将CoNLL文件转换为sql语句

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
26
27
28
29
30
31
32
33
34
35
36
37
import sys
def output(sents, begin, end, outfile, user):
annotator = user
sql = "insert into dependancy(sentid,sent,dep_sent,annoter) values"
i = begin
raw_sent = ' '.join(['[' + str(id) + ']' + tok[0] + '/' + tok[1] for id, tok in enumerate(sents[begin])])
sql += "('" + str(i) + "','" + raw_sent + "','"
rels = "\t\t".join(['['+str(tok[2])+']'+sents[begin][tok[2]][0]+'_['+str(id+1)+']'+tok[0]+'('+tok[3]+')' for id, tok in enumerate(sents[begin][1:]) ])
sql += rels + "','"+annotator+"')"
i += 1
for sent in sents[begin+1:end+1]:
raw_sent = ' '.join(['[' + str(id) + ']' + tok[0] + '/' + tok[1] for id, tok in enumerate(sent)])
sql += ",('" + str(i) + "','" + raw_sent + "','"
rels = "\t\t".join(['['+str(tok[2])+']'+sent[tok[2]][0]+'_['+str(id+1)+']'+tok[0]+'('+tok[3]+')' for id, tok in enumerate(sent[1:]) ])
sql += rels + "','"+annotator+"')"
i += 1
fo = file(outfile,"w")
fo.write(sql)
if __name__=="__main__":
fi = file("biaozhu.txt","r")
fi = fi.read()
sents = fi.strip().split("\n\n")
print "total sents: ",len(sents)
sentences = []
n = 0
for sent in sents:
sentences.append([])
sentences[n].append(("Root", "Root", -1, "-NULL-"))
lines = sent.strip().split("\n")
for line in lines:
items = line.strip().split("\t")
sentences[n].append((items[1], items[3], int(items[6]), items[7]))
n += 1
for i in xrange(10):
output(sentences, i*100, i*100+99, "huawei"+str(i*100)+"-"+str(i*100+99)+"-user"+str(i)+".sql", "user"+str(i))

这里把1000句话拆成10份,每份annotator不同,这样用不同的账号登陆时将被分配给对应任务。此外也是因为1000句sql语句同时上传的时候太卡,拆分之后就快很多。

输入CoNLL文件格式在上一节中给出了例子,如果输入的句子数量不是1000句或者想要采用别的分组方式,可以修改一下主函数里的调用。

csv文件转换为CoNLL文件

在数据库中将标注好的句子导出到csv文件,用如下Python脚本将csv文件转换为CoNLL文件

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
26
27
28
29
30
31
32
33
34
35
36
37
38
def output(outfile, sents):
fo = file(outfile, "w")
for sent in sents:
for idx, tok in enumerate(sent[1:]):
line = str(idx+1)+"\t"+tok["tok"]+"\t"+tok["tok"]+"\t"+tok["pos"]+"\t"+tok["pos"]+"\t_\t"+str(tok["hed"])+"\t"+tok["rel"]+"\t_\t_\n"
fo.write(line)
fo.write("\n")
def process(infos):
sents = []
for info in infos:
sent = [{"tok":token[0], "pos":token[1]} for token in [tok.split("]")[1].split("/") for tok in info["tokens"]]]
for rel in info["result"]:
items = rel.split("_")
index = int(items[1].split("]")[0].strip("["))
head = int(items[0].split("]")[0].strip("["))
relation = items[1].split("(")[1].strip(")")
#print index, head, relation
sent[index]["hed"] = head
sent[index]["rel"] = relation
sents.append(sent)
return sents
if __name__ == "__main__":
fi = file("dependancy.csv", "r")
fi = fi.read()
sents = fi.strip().split("\n")
infos = []
for sent in sents:
info = {}
items = [it.strip("\"") for it in sent.strip().split(";")]
if items[7] == "1": continue #skip the skipped sents
info["tokens"] = items[1].strip().split()
info["origin"] = items[2].strip().split("\t\t")
info["result"] = items[3].strip().split("\t")
infos.append(info)
sents = process(infos)
output("annotated_test.conll", sents)

数据库中dependancy表包括sentid, sent, dep_sent, res_sent, tag, annoter, time, skipcomment等几个column,这里用到的是sent(保存句子的词项和词性信息),res_sent(保存标注的句子弧和标签信息)以及skip(记录该句子是否被跳过,跳过为1,否则为0)。

标注系统介绍

登录界面

进入标注系统主页会看到以下登录界面,用户名为user0~user9,每个用户名下分配了100句话。

登录界面

标注界面

登录之后进入标注界面,待标注句子可能在比较下面的位置,如果找不到就往下拉。

标注界面-修改弧标签

先后单击词A和B会生成一条由A指向B的弧,右键单击弧标签可以选中该弧(由蓝色变红),此时可以再次右键单击标签进行修改弧标签(如上图所示)。在选中一条弧的情况下双击该弧可以删除该弧。

标注界面-修改分词/词性

单击下方的切换修改分词可以修改分词和词性。在最后一列词性上单击右键会出现增加/删除一列的选项,用于修改分词。在每个词性上单击右键都会出现可以替换的词性列表,用于修改词性。修改完毕之后单击下方分词修改完毕保存修改。

标注完成后单击保存将对该句子的标注存入数据库。

此外,最下面还有跳过选项,需要先选择一个跳过理由(如果选择‘其他’则需要填写跳过理由),然后单击跳过。跳过的句子在数据库中会有标记,并不会再次出现。

历史记录

单击标注界面右上角历史记录进入历史记录界面,显示所有当前用户标注过的句子。左键单击一个句子会进入标注界面,可以修改该句子标注结果

历史记录界面

单击新任务可以回到标注界面,进行下一个句子的标注。

代码修改

绝对路径改为相对路径

原来的代码中存在许多绝对路径链接,例如label_dep.php中的:

1
2
$url = "http://semanticannotate.sinaapp.com/index.php";
echo "window.location.href='$url'";

如果注册域名不是semanticannotate的话,就会重定向到错误网址,解决方法是将这些都改成相对路径,例如这里改为:

1
2
$url = "index.php";
echo "window.location.href='$url'";

主要php文件概述及标注流程里说明的几个文件里应该都存在这种情况,直接Ctrl+F搜索semantic找绝对路径然后改成相对路径就行。

当前用户已标注数据统计

label_dep.php中左上角显示本人已标注数据的地方原来使用的是从COOKIE中获得记录(43行开始),如下:

1
2
3
4
5
6
7
<p class="logo" >
您已完成<label class='finish' id="finish">
<?php
echo $_COOKIE['dep_finish'];
?> </label>句标注任务
</p>

实测很多时候不好用,因此改为直接从数据库中的signup表中读取当前用户已完成标注数目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<p class="logo" >
您已完成<label class='finish' id="finish">
<?php
$sql = "select dep_compelete from signup where username = '".$_COOKIE['user']."'";
mysql_query("SET NAMES 'UTF8'");
$res = mysql_query($sql);
$dep_finish = "";
while ($words = mysql_fetch_assoc($res)){
$result = $words['dep_compelete'];
}
echo $result;
?>
</label>句标注任务 </p>

标注界面保存后刷新问题

label_dep.php中保存标注句子后原来使用的是window.location.href重新打开页面(728行开始),这样需要用户重新把网页下拉到标注句子的地方:

1
2
3
4
5
6
7
8
9
10
var url = "write_dep.php";
$.post(
url,
{res_sent:str, psent : protosent, skip: "0", comment:comment},
function (data){
$.cookie('sent_modify', null);
window.location.href="label_dep.php";
}
);

这里改为用window.location.reload()刷新,避免用户手动下拉:

1
2
3
4
5
6
7
8
9
10
11
var url = "write_dep.php";
$.post(
url,
{res_sent:str, psent : protosent, skip: "0", comment:comment, sent:newwordpos},
function (data){
$.cookie('sent_modify', null);
if (data == "写入成功!")
window.location.reload();
}
);