fairseq笔记

训练新模型

以机器翻译为例子开始

Fairseq 包含多个翻译数据集的示例预处理脚本:IWSLT 2014(德语-英语)、WMT 2014(英语-法语)和 WMT 2014(英语-德语)。预处理和二值化 IWSLT 数据集:

> cd examples/translation/
> bash prepare-iwslt14.sh
> cd ../..

> TEXT=examples/translation/iwslt14.tokenized.de-en

> fairseq-preprocess --source-lang de --target-lang en \
    --trainpref $TEXT/train --validpref $TEXT/valid --testpref $TEXT/test \
    --destdir data-bin/iwslt14.tokenized.de-en

我在脚本中写了注释,方便对脚本语言不熟练的同学入门,如果熟练的同学可以回忆一下这个预处理做了什么,然后跳转到关于fairseq-preprocess的源码讲解部分

  • 下载数据集
  • 下载subword-nmt和moses
  • 清洗训练集
  • 去掉包含
  • 用moses分词,把标点符号和英文单词分开
  • 保留1-175长度和源语言目标语言长度1.5比例内的句子
  • 全部单词变小写
  • 把训练数据集(清洗后的)按照22:1划分成训练集和验证集
  • 把原验证集测试集全划分成测试集
  • bpe算法
#!/usr/bin/env bash

echo 'Cloning Moses github repository (for tokenization scripts)...'
git clone https://github.com/moses-smt/mosesdecoder.git

echo 'Cloning Subword NMT repository (for BPE pre-processing)...'
git clone https://github.com/rsennrich/subword-nmt.git

SCRIPTS=mosesdecoder/scripts
TOKENIZER=$SCRIPTS/tokenizer/tokenizer.perl
LC=$SCRIPTS/tokenizer/lowercase.perl
CLEAN=$SCRIPTS/training/clean-corpus-n.perl
BPEROOT=subword-nmt/subword_nmt
BPE_TOKENS=10000

URL="http://dl.fbaipublicfiles.com/fairseq/data/iwslt14/de-en.tgz"
GZ=de-en.tgz

if [ ! -d "$SCRIPTS" ]; then
    echo "Please set SCRIPTS variable correctly to point to Moses scripts."
    exit
fi

src=de
tgt=en
lang=de-en
prep=iwslt14.tokenized.de-en
tmp=$prep/tmp
orig=orig

mkdir -p $orig $tmp $prep

echo "Downloading data from ${URL}..."
cd $orig

wget "$URL"

if [ -f $GZ ]; then
    echo "Data successfully downloaded."
else
    echo "Data not successfully downloaded."
    exit
fi

tar zxvf $GZ

cd ..

echo "pre-processing train data..."
for l in $src $tgt; do
    f=train.tags.$lang.$l
    tok=train.tags.$lang.tok.$l

    cat $orig/$lang/$f | \
    grep -v '' | \
    grep -v '' | \
    grep -v '' | \

    sed -e 's///g' | \
    sed -e 's///g' | \
    sed -e 's///g' | \
    sed -e 's///g' | \

    perl $TOKENIZER -threads 8 -l $l > $tmp/$tok

    echo ""
done

perl $CLEAN -ratio 1.5 $tmp/train.tags.$lang.tok $src $tgt $tmp/train.tags.$lang.clean 1 175

for l in $src $tgt; do
    perl $LC < $tmp/train.tags.$lang.clean.$l > $tmp/train.tags.$lang.$l
done

echo "pre-processing valid/test data..."
for l in $src $tgt; do

    for o in ls $orig/$lang/IWSLT14.TED*.$l.xml; do

    fname=${o##*/}

    f=$tmp/${fname%.*}
    echo $o $f

    grep ' $o | \
        sed -e 's/\s*//g' | \
        sed -e 's/\s*\s*//g' | \
        sed -e "s/\'/\'/g" | \

    perl $TOKENIZER -threads 8 -l $l | \
    perl $LC > $f
    echo ""
    done
done

echo "creating train, valid, test..."

for l in $src $tgt; do
    awk '{if (NR%23 == 0)  print $0; }' $tmp/train.tags.de-en.$l > $tmp/valid.$l
    awk '{if (NR%23 != 0)  print $0; }' $tmp/train.tags.de-en.$l > $tmp/train.$l

    cat $tmp/IWSLT14.TED.dev2010.de-en.$l \
        $tmp/IWSLT14.TEDX.dev2012.de-en.$l \
        $tmp/IWSLT14.TED.tst2010.de-en.$l \
        $tmp/IWSLT14.TED.tst2011.de-en.$l \
        $tmp/IWSLT14.TED.tst2012.de-en.$l \
        > $tmp/test.$l
done

TRAIN=$tmp/train.en-de
BPE_CODE=$prep/code
rm -f $TRAIN
for l in $src $tgt; do
    cat $tmp/train.$l >> $TRAIN
done

echo "learn_bpe.py on ${TRAIN}..."
python $BPEROOT/learn_bpe.py -s $BPE_TOKENS < $TRAIN > $BPE_CODE

for L in $src $tgt; do
    for f in train.$L valid.$L test.$L; do
        echo "apply_bpe.py to ${f}..."
        python $BPEROOT/apply_bpe.py -c $BPE_CODE < $tmp/$f > $prep/$f
    done
done

preprocess的源码分析

先讲怎么从命令行中截获参数,fairseq用的是python 标准库里的argparser,顾名思义,是参数解析器。

在python中,有如下四种类型的参数,分别是位置参数、关键字参数、默认参数和可变参数。其中位置参数和关键字参数讲的是调用的方式,比如以下例子:

def print_hello(name, sex)

print_hello('小明','male')

print_hello(sex='male',name='小明')

位置参数通过参数定义的位置来传递参数;关键字参数通过键值对的方式来传递参数,不需要考虑位置关系。当关键字参数和位置参数混用的时候,需要特别注意, 位置参数必须在关键字参数之前,所以解析的方式就是逐个把位置参数送入形参,再把关键字和形参结合,一旦关键字参数和位置参数相同,比如 print_hello(1, name='&#x5C0F;&#x660E;')就会因为name有两个实参报错。

默认参数就是在形参定义的时候,带上的默认值,比如

def print_hello(sex,name='male')

这时候调用的时候就可以不传name的实参进去,当然需要注意的是,默认参数也必须在位置参数之后。

可变参数就是有时候我们不确定调用的时候会传递多少个参数,此时可以用packing包裹位置参数或者关键字参数。
比如包裹位置参数的例子


def func(*args):
    ....

func()
func(a)
func(a, b, c)

所有传进去的参数都会被args收集,他是一个tuple类型的变量。

def func(**kargs):
    ....

func(a=1)
func(a=1, b=2, c=3)

kargs是一个dict类型的变量。
需要注意的是,args和kargs并不是必须的命名,只是一种习惯,区别是元组还是字典,靠的是**的数量。

我们什么时候需要argparse,比如我们写好了一个python脚本hello.py
你可以直接使用

python hello.py

来运行这个脚本,但有时候你并不满足只是运行,可能还需要从外界获取一点额外的信息,比如说,使用者的名字,运行的次数,等等。这时你就可以使用argparse,达到下面的效果:

python hello.py --name 小明 --time 3

这时小明和3就会被传入hello.py中,并且可以被获取,我们不会讲的特别深入,只保证你能明白fairseq用这个做什么,感兴趣可以自行从python的官方文档中阅读。

讲完argparse的用途,我们讲怎么做。
基本上是三部曲,第一是创建解析器,第二步是往解析器里添加需要解析的参数,第三步是开始解析参数。这就好像开辅导班,第一步是租店面,第二步是确定教什么科目,第三步是招收对应的老师为学生授课这样(奇怪的例子orz)

argparse中,有一个命令解析类,叫做 ArgumentParser,他的构造函数中的所有参数都是关键字参数,也就是要用键值对的方式传进去,他有非常多的成员,我们只讲fairseq用的部分。
bool类型的add_help,当这个参数是true的时候,你可以通过-h或者–help读到这个 hello.py所有参数的帮助(当然这得是你写了才有东西输出)。
bool类型的 allow_abbrev,这个是允许使用缩写的意思,在python3.5以后默认开启,比如我们定义了–time 这个参数,当你实际使用时采用–ti 3,也可以被识别到time参数上,当然,一旦你输入的缩写是多个参数的共同前缀,产生了歧义时,这个选项就无法使用了。

在定义完一个解析器之后,我们需要为里面加上需要解析的内容,这个是通过解析器类的 add_argument方法进行的,比如我们需要让这个参数解析器接受 --time,就是通过这个方法加的。这个方法常用的参数如下:
default – 说白了如果定义了这个,就允许这个参数被当成默认参数处理,如果没写,或者把这一个参数定义成None,那这个参数就无法在命令行缺省该参数时使用。
name or flags – 这个参数就是用来写–time的,为这个解析器类加上需要解析的参数,需要注意,如果在同一个add_argument里面写多个flags ('-f', '--foo'),也可以只写一个name比如 (bar),需要注意,不加-的会被解析成位置参数,是不允许缺省的,一旦缺省了会报错,而加了-的会被认为是可选参数,同时如果flags不是一个字母,前面要加–,如果是一个字母,只用一个-。
dest – 这个说起来有点绕啊,其实他和name需要区分一下,他是parser创建完后,解析了参数之后(后面会说的parse_args()方法),你用什么变量名来获取刚刚的参数,比如说啊
parser.add_argument(‘-f’, ‘–foo-bar’, ‘–foo’),这里面所有的参数都是flags对吧,那之后我们要调用这个参数,就是通过parse.foo_bar,因为如果有–的,会选择第一个–的names去掉杠杠,而且把里面的-变成_(这是因为变量名的规范要求),如果只有-的,就取第一个-的name作为内容。

讲完参数的类型后,我们开始看源码部分的argparse内容。 fairseq-preprocess调用的是 fairseq_cli/preprocess.pycli-main()

def cli_main():
    parser = options.get_preprocessing_parser()
    args = parser.parse_args()
    main(args)

其中options的路径是 fairseq/options.py,我们看看预处理的解析器函数内容是什么。

def get_preprocessing_parser(default_task="translation"):
    parser = get_parser("Preprocessing", default_task)
    add_preprocess_args(parser)
    return parser

上面这三行里面,get_parser创建了一个parser,他的两个实参中,第一个字符串类的desc并没有用上,更像是一个和不同cli指令区分开的标记,只是为了增加可读性。这个函数的具体注释写在代码体里面了。

def get_parser(desc, default_task="translation"):
"""

    Args:
        desc: 没用上,这里像是一个信息标记
        default_task:  默认任务

    Returns:

"""

    usr_parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
    usr_parser.add_argument("--user-dir", default=None)

    usr_args, _ = usr_parser.parse_known_args()

    utils.import_user_module(usr_args)

    parser = argparse.ArgumentParser(allow_abbrev=False)
    gen_parser_from_dataclass(parser, CommonConfig())

    from fairseq.registry import REGISTRIES

    for registry_name, REGISTRY in REGISTRIES.items():
        parser.add_argument(
            "--" + registry_name.replace("_", "-"),
            default=REGISTRY["default"],
            choices=REGISTRY["registry"].keys(),
        )

    from fairseq.tasks import TASK_REGISTRY

    parser.add_argument(
        "--task",
        metavar="TASK",
        default=default_task,
        choices=TASK_REGISTRY.keys(),
        help="task",
    )

    return parser

Original: https://blog.csdn.net/koala_cola/article/details/122482738
Author: koala_cola
Title: fairseq笔记

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/530398/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球