欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ROUGE与PyROUGE的安装:虚拟机上从头再来

程序员文章站 2022-07-01 18:09:15
...

1 写在前面

上一篇文章:ROUGE与PyROUGE的安装:非root权限的尝试与失败,烂尾了。由于没有服务器的root权限,最终也没能成功。但我是不会就这样放弃的,经老师建议,我可以在本地安装虚拟机,在虚拟机中使用ROUGE评价模型。

我使用的软件是VMware Workstation Pro。还记得大一的暑假参加科技夏令营的时候,我们学习在服务器上部署LNMP框架,搭建WordPress的博客。当时为了练习linux命令,在本地搭了一台Ubuntu和一台CentOS7,距离现在已经一年半了。所以说,哈哈,我不记得密码了。我蒙对了CentOS7的密码,所以下文将基于本地的虚拟机:CentOS7

2 前期准备

关于下载和安装虚拟机的教程在此不进行详述,改日再成博客。我的这台虚拟机内存分配了2GB,硬盘20GB。我所说的前期准备主要是网络设置。

在我wget的时候发现网络不通,修改网络配置可以参考这篇博客:VMware 虚拟机无法连接网络解决办法。这篇博客写的很详细,但唯一没有提到网络连接模式应该选择WMnet8 NAT模式

另外为了能够更好地使用yum命令,最好对它的安装源进行升级。这个过程往往很缓慢,可以在睡觉前更新,让它安装一夜。

$ yum -y upgrade

3 注意事项

具体的安装过程完全可以参考上一篇文章,有了root权限,安装起来会快很多很多,基本没遇到太复杂的问题。如下是一些需要注意的事项。

3.1 设置perl的执行路径

首先,既然我们已经有了root权限,就要把perl的路径变量设在root的配置文件中。这里首先需要用su命令进入到root中,然后:

# vim /etc/profile

在文件的最后加上如下内容,其中路径可以在安装ActivePerl的时候得到:

export PATH=$PATH:/opt/ActivePerl-5.26/site/bin:/opt/ActivePerl-5.26/bin
export MANPATH=$MANPATH:/opt/ActivePerl-5.26/site/man:/opt/ActivePerl-5.26/man

然后**当前环境变量:

# source /etc/profile

但是在这时我们使用perl -v命令查看perl,仍然不是我们新安装的。我又不知道如何卸载系统原有的perl(网上有人问过这个问题,一般无法卸载)。于是破釜沉舟:

whereis perl

得到系统原装的perl的可执行文件路径后,我们删掉它。这时再查看perl -v,成功的覆盖掉了原始perl,得到了5.26.0版本。关于linux查找文件的命令,findwhereislocate有什么区别,可以参考:Linux下怎样搜索文件

3.2 ROUGE-RELEASE-1.5.5

在出现了很多File not found问题之后,我开始反思我的项目是不是不完整。在网上查到的博客多数都是互相抄袭,但这篇博客:Ubuntu安装配置ROUGE教程是原创。其中给出了ROUGE-1.5.5的源码地址:GitHub,我才知道我的项目缺了很多东西,这是它应有的模样:

data  docs  README.txt  RELEASE-NOTE.txt  ROUGE-1.5.5.pl  runROUGE-test.pl  sample-output  sample-test

而网上绝大多数博客给出的ROUGE的目录都没有sample-outputsample-test这两个文件夹,我都不知道他们明知道结果不对还互相抄袭是什么心理,或者说他们都没做过这个实验?

我们照上节提到的方法将环境变量添加到/etc/profile中:

export ROUGE_EVAL_HOME=$ROUGE_EVAL_HOME:/home/thomas_atlantis/environments/ROUGE-RELEASE-1.5.5/data

3.3 修改*.pl文件开头声明

一般来说我们可能会遇到这个问题:

Can't locate DB_File.pm in @INC

这我在上篇博客中已经强调过了。由于我们的perl路径已经不是默认的路径了,我们需要将所有的*.pl文件的首行修改为:

#!/opt/ActivePerl-5.26/bin/perl -w

我们可以使用以下命令查找出ROUGE-RELEASE-1.5.5下的所有*.pl文件,以免有遗漏:

$ find . -name '*.pl'

得到结果:

./data/WordNet-2.0-Exceptions/buildExeptionDB.pl
./data/WordNet-1.6-Exceptions/buildExeptionDB.pl
./runROUGE-test.pl
./test/runROUGE-test.pl
./ROUGE-1.5.5.pl

3.4 修改*.pl文件执行权限

这个版本的ROUGE中不存在执行权限问题,但还是要检查以下,查看上节中*.pl文件列表,如果某个文件不是绿色的可执行状态,我们要:

$ chmod +x *.pl

3.5 WordNet-2.0.exc.db问题

Cannot open exception db file for reading: data/WordNet-2.0.exc.db

这是一个常见问题,解决方法:

$ cd ROUGE-RELEASE-1.5.5/data
$ rm WordNet-2.0.exc.db
$ ./WordNet-2.0-Exceptions/buildExeptionDB.pl ./WordNet-2.0-Exceptions $ ./smart_common_words.txt ./WordNet-2.0.exc.db

解决完以上问题,ROUGE就基本配好了,测试命令:

perl runROUGE-test.pl

的结果正确无误输出如下:
ROUGE与PyROUGE的安装:虚拟机上从头再来
我们可以查看其中的一个样例DUC2002-ROUGE.in.26.spl.out

---------------------------------------------
26 ROUGE-1 Average_R: 0.33939 (95%-conf.int. 0.32215 - 0.35572)
26 ROUGE-1 Average_P: 0.28013 (95%-conf.int. 0.26712 - 0.29348)
26 ROUGE-1 Average_F: 0.30654 (95%-conf.int. 0.29199 - 0.32107)
---------------------------------------------
26 ROUGE-2 Average_R: 0.07727 (95%-conf.int. 0.06642 - 0.08925)
26 ROUGE-2 Average_P: 0.06356 (95%-conf.int. 0.05484 - 0.07321)
26 ROUGE-2 Average_F: 0.06967 (95%-conf.int. 0.06017 - 0.08050)
---------------------------------------------
26 ROUGE-3 Average_R: 0.02587 (95%-conf.int. 0.01928 - 0.03279)
26 ROUGE-3 Average_P: 0.02119 (95%-conf.int. 0.01587 - 0.02674)
26 ROUGE-3 Average_F: 0.02327 (95%-conf.int. 0.01749 - 0.02950)
---------------------------------------------
26 ROUGE-4 Average_R: 0.01185 (95%-conf.int. 0.00768 - 0.01608)
26 ROUGE-4 Average_P: 0.00970 (95%-conf.int. 0.00625 - 0.01315)
26 ROUGE-4 Average_F: 0.01065 (95%-conf.int. 0.00689 - 0.01445)
---------------------------------------------
26 ROUGE-L Average_R: 0.30064 (95%-conf.int. 0.28601 - 0.31506)
26 ROUGE-L Average_P: 0.24797 (95%-conf.int. 0.23702 - 0.25990)
26 ROUGE-L Average_F: 0.27143 (95%-conf.int. 0.25923 - 0.28387)

4 pyrouge的安装

$ git clone https://github.com/bheinzerling/pyrouge
$ cd pyrouge
$ sudo python setup.py install
$ python -m pyrouge.test

这里注意,一定不要使用pip install pyrouge的方法安装,那样安装的文件的源码是有BUG在里面的。如有其它问题可以参考以上的GitHub:README。如果测试成功最后可以看到:

----------------------------------------------------------------------
Ran 10 tests in 8.987s

OK

5 pyrouge的使用

参考这篇博客:rouge与pyrouge使用事项。博客中给出的代码需要稍作改动。我假设输入的文件的格式是:reference:每行一段参考标准摘要,一段可以包含多个句子;candidate:与前者相对应,每行一段模型预测的摘要,一段可以包含多个句子。这是在考虑模型输出为多句摘要且未进行分句的情况,属于默认情况。如果模型给出多句且已分好句,那么可以将多句分别写在一行,段与段之间隔一个空行。我将加入一个参数来控制这两种输入格式。

原博主给出的代码中有一句:" ".join(ref[i]).replace(' ', '') + '\n',让人很莫名其妙,为什么要先用空格连接然后又去掉空格?我猜想她是想用\n连接来着,但是空格是真的不应该去。**ROUGE要求每行一句话,单词用空格隔开。**而且最好在评价结束后将程序生成的中间文件删除掉,否则会影响下一次评价的输入。我使用argparse命令行参数解析工具包,将修改后的程序封装了一下,下面是源代码:

# -*- coding: utf-8
import pyrouge, codecs, os, sys, logging, argparse
from nltk.tokenize import sent_tokenize

def remove_files(path):
    for file in os.listdir(path):
        path_file = os.path.join(path, file)
        os.remove(path_file)

def rouge(ref, hyp, log_path):
    assert len(ref) == len(hyp)
    ref_dir = log_path + 'reference/'
    cand_dir = log_path + 'candidate/'
    if not os.path.exists(ref_dir):
        os.mkdir(ref_dir)
    if not os.path.exists(cand_dir):
        os.mkdir(cand_dir)
    for i in range(len(ref)):
        with codecs.open(ref_dir+"%06d_reference.txt" % i, 'w', 'utf-8') as f:
            f.write("\n".join(ref[i]) + '\n')
        with codecs.open(cand_dir+"%06d_candidate.txt" % i, 'w', 'utf-8') as f:
            f.write("\n".join(hyp[i]).replace('<unk>', 'UNK') + '\n')

    r = pyrouge.Rouge155()
    r.model_filename_pattern = '#ID#_reference.txt'
    r.system_filename_pattern = '(\d+)_candidate.txt'
    r.model_dir = ref_dir
    r.system_dir = cand_dir
    logging.getLogger('global').setLevel(logging.WARNING)
    rouge_results = r.convert_and_evaluate()
    scores = r.output_to_dict(rouge_results)
    recall = [round(scores["rouge_1_recall"] * 100, 2),
              round(scores["rouge_2_recall"] * 100, 2),
              round(scores["rouge_l_recall"] * 100, 2)]
    precision = [round(scores["rouge_1_precision"] * 100, 2),
                 round(scores["rouge_2_precision"] * 100, 2),
                 round(scores["rouge_l_precision"] * 100, 2)]
    f_score = [round(scores["rouge_1_f_score"] * 100, 2),
               round(scores["rouge_2_f_score"] * 100, 2),
               round(scores["rouge_l_f_score"] * 100, 2)]
    print("F_measure: %s Recall: %s Precision: %s\n"
              % (str(f_score), str(recall), str(precision)))
    remove_files(ref_dir)
    remove_files(cand_dir)
    with codecs.open(log_path + "rouge_score", 'a+', 'utf-8') as f:
        f.write("F_measure: %s Recall: %s Precision: %s\n"
              % (str(f_score), str(recall), str(precision)))
    return f_score[:], recall[:], precision[:]

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('-r', '--reference', help='reference input file')
    parser.add_argument('-c', '--candidate', help='candidate input file')
    parser.add_argument('-a', '--auto_tokenize', choices=['true', 'false'], default='true',
    help='if true, every paragraph takes one line;'
    'if false, every sentence takes one line, paragraphs are split by one blank line')
    parser.add_argument('-l', '--log_path', help="log path to extract files and store rouge scores")
    args = parser.parse_args()
    assert args.reference
    assert args.candidate
    assert args.log_path
    if args.auto_tokenize == 'false':
        ref = [para.strip().split('\n') for para in codecs.open(args.reference, 'r', encoding='utf-8').read().strip().split('\n\n')]
        can = [para.strip().split('\n') for para in codecs.open(args.candidate, 'r', encoding='utf-8').read().strip().split('\n\n')]
    else:
        ref = [sent_tokenize(line.strip()) for line in codecs.open(args.reference, 'r', encoding='utf-8').readlines() if line.strip()]
        can = [sent_tokenize(line.strip()) for line in codecs.open(args.candidate, 'r', encoding='utf-8').readlines() if line.strip()]
    assert len(ref) == len(can)
    rouge(ref, can, args.log_path)

if __name__ == '__main__':
    main()

可以通过-h--help参数查看帮助文档:

[aaa@qq.com workspace]# rouge -h
usage: rouge_tool.py [-h] [-r REFERENCE] [-c CANDIDATE] [-a {true,false}]
                     [-l LOG_PATH]

optional arguments:
  -h, --help            show this help message and exit
  -r REFERENCE, --reference REFERENCE
                        reference input file
  -c CANDIDATE, --candidate CANDIDATE
                        candidate input file
  -a {true,false}, --auto_tokenize {true,false}
                        if true, every paragraph takes one line;if false,
                        every sentence takes one line, paragraphs are split by
                        one blank line
  -l LOG_PATH, --log_path LOG_PATH
                        log path to extract files and store rouge scores

两种格式下的使用例子,其中rouge_log是暂存中间文件和存储最终score的路径。

$ python rouge_tool.py -r sent_tokenized_ref -c sent_tokenized_can -l rouge_log/ -a false
$ python rouge_tool.py -r reference -c candidate -l rouge_log/

如果有Assertion Error那多半是文件格式或命令格式不对,如果出现

subprocess.CalledProcessError: Command ... returned non-zero exit status 255

可以考虑将错误指出的命令运行一下,多半是log_path/referencehuolog_path/candidate中有其他的文件,删除一下重新运行。我使用两篇论文的摘要作为参考,将其中一部分词替换为<unk>作为候选,运行结果的正确示例:

F_measure: [95.94, 91.8, 95.94] Recall: [95.94, 91.8, 95.94] Precision: [95.94, 91.8, 95.94]

其中方括号列表内的三个值分别代表ROUGE-1,ROUGE-2和ROUGE-L,单位为%。如果想要更方便的使用脚本,把ROUGE变为一个命令,建议不要使用pyinstaller打包成可执行文件,那样不仅会速度很慢,还会出很多第三方库不兼容的问题。我们只需要为命令起一个别名,在~/.bashrc中加入alias rouge=python /home/username/some_path/rouge_tool.py

关于中文的处理

这部分以及以下第6小节参考了这篇文章:ROUGE脚本对中文的支持。顺便拜访了以下博主鱼虾一整碗(雨下一整晚)的网站,做的很棒。

ROUGE perl 脚本不能直接用在中文上,需要把中文转成英文或数字。具体的,应该把每个字都转成一个唯一对应的英文或数字,同时输出的时候符号与符号之间用空格分隔。 还是要注意中英混合时的tokenize!评估时,英文是词为单位,而中文是字为单位!

训练中,一般自己实现一个简单的ROUGE-N或者ROUGE-L。正确的实现,应该是取COUNT(当共现的NGRAM中有多个相同的时候),可以直接使用已有的rouge re-implementation脚本。

而在最终写论文的时候才用ROUGE perl脚本,较为正式。

6 其他ROUGE工具

re-implementation是指用原生Python实现的,wrapper是指在PERL脚本上用Python封装了一下;use SET for multi-NGRAMuse COUNT for mult-NGRAM 是指当 goldpredict 共现的NGRAM里,某些NGRAM出现了多次,有的算的是uniq个数(SET),有的是COUNT。我看论文是COUNT,究竟如何不确定,毕竟google的seq2seq都是set,而我有没看到PERL里相关部分