Merge branch 'master' into bilm

This commit is contained in:
Piotr Czapla
2018-11-16 20:09:41 +01:00
13 changed files with 601 additions and 163 deletions
+43 -4
View File
@@ -1,6 +1,44 @@
# ulmfit-multilingual
Temporary repository used for collaboration on application of for multiple languages.
## data directory strucutre
Directory structure after changes to the way we process wiki dumps.
```
data
├── imdb
│   ├── aclImdb
│   ├── imdb_lm
│   └── tmp
├── wiki
│   ├── de-100
│   │   └── models
│   ├── de-100-unk
│   │   └── models
│   ├── de-2
│   │   └── models
│   ├── de-2-unk
│   │   └── models
│   ├── de-all
│   │   └── models
│   ├── wikitext-103
│   │   └── models
│   └── wikitext-2
│      └── models
├── wiki_dumps
├── wiki_extr
│   └── de
│   ├── AA
│   ├── AB
...
└── CC
└── xnli
├── XNLI-1.0
└── XNLI-MT-1.0
├── multinli
└── xnli
```
## how to contribute
We have a fork of fastai to propose changes to fastai.text, with a branch for this project:
https://github.com/n-waves/fastai/tree/ulmfit_multilingual
@@ -8,11 +46,12 @@ We have a fork of fastai to propose changes to fastai.text, with a branch for th
Let us know that you want to start collaboration on fastai forum thread: [Multilingual ULMFIT](https://forums.fast.ai/t/multilingual-ulmfit/28117)
and you will get access to both repositories.
Follow the developer [installation of fastai] (https://github.com/fastai/fastai#developer-install)
Add n-waves/fastai as additional remote as described here: https://help.github.com/articles/adding-a-remote/
Here is what I've did:
```bash
- Follow the [developer installation of fastai](https://github.com/fastai/fastai#developer-install)
- Add n-waves/fastai as additional remote as described here: https://help.github.com/articles/adding-a-remote/
Here is what I did:
```bash
$ cd fastai
$ git remote add n-waves https://github.com/n-waves/fastai.git
$ git remote -v
n-waves https://github.com/n-waves/fastai.git (fetch)
+213 -18
View File
@@ -3,20 +3,228 @@ Utility methods for data processing.
"""
import pandas as pd
import numpy as np
import fire
from fastai import F, to_device
import torch
from tqdm import tqdm
import re
import csv
import pathlib
import tarfile
from sklearn import model_selection
from sacremoses import MosesTokenizer
from typing import Dict, Tuple, List
EOS = '<eos>'
UNK = '<unk>'
PAD = '<pad>'
SEP = '<sep>' # special separator token for NLI
PAD_TOKEN_ID = 1
IMDB, XNLI, TRN, VAL, TST, EN = 'imdb', 'xnli', 'train', 'val', 'test', 'en'
DATASETS = ['imdb', 'xnli']
XNLI_PATHS = {
TRN: 'XNLI-MT-1.0/multinli/multinli.train.%s.tsv',
VAL: 'XNLI-1.0/xnli.dev.tsv',
TST: 'XNLI-1.0/xnli.test.tsv'
}
number_match_re = re.compile(r'^([0-9]+[,.]?)+$')
number_split_re = re.compile(r'([,.])')
CLASSES = ['neg', 'pos', 'unsup']
def get_texts(path):
texts, labels = [],[]
for idx, label in enumerate(CLASSES):
for fname in (path/label).glob('*.*'):
texts.append(fname.open('r', encoding='utf-8').read())
labels.append(idx)
return np.array(texts), np.array(labels)
def ensure_paths_exists(*paths, message="One or more required files cannot be found."):
error = False
for path in paths:
if not path.exists():
print(f'Error: {path} does not exist.')
error = True
if error:
raise FileNotFoundError(message)
def get_data_folder():
"""
return data folder to use for future processing
"""
return (pathlib.Path(__file__).parent.parent / "data")
def get_scripts_folder():
"""
return data folder to use for future processing
"""
return (pathlib.Path(__file__).parent.parent)
def prepare_imdb(file_path: str, prepare_lm = False):
"""
function to extract aclImdb and combine into fastai standard format of labels and then text
columns
Args:
file_path: path to the aclImdb.tgz
prepare_lm (bool): prepare file for language model finetuning
Returns:
None
"""
file_path = pathlib.Path(file_path)
dir_path = pathlib.Path(file_path.parent / 'aclImdb').resolve()
assert tarfile.is_tarfile(file_path), "this is not a valid targz file"
if not dir_path.exists():
print(f"Extracting {file_path} to {dir_path}. This may take a long time...")
tgz_file = tarfile.open(file_path)
tgz_file.extractall(path=dir_path.parent) # the aclImdb.tgz has aclImdb dir packed
assert dir_path.exists()
print(f"Extracted to {dir_path}")
CLAS_PATH = dir_path.parent
CLAS_PATH.mkdir(exist_ok=True)
LM_PATH = dir_path.parent /'imdb_lm'
LM_PATH.mkdir(exist_ok=True)
# processing the split files to create train.csv and test.csv in fastai format
col_names = ['labels', 'text']
trn_texts, trn_labels = get_texts(dir_path/ 'train')
val_texts, val_labels = get_texts(dir_path / 'test')
np.random.seed(42)
trn_idx = np.random.permutation(len(trn_texts))
val_idx = np.random.permutation(len(val_texts))
trn_texts = trn_texts[trn_idx]
val_texts = val_texts[val_idx]
trn_labels = trn_labels[trn_idx]
val_labels = val_labels[val_idx]
df_trn = pd.DataFrame({'text': trn_texts, 'labels': trn_labels}, columns=col_names)
df_val = pd.DataFrame({'text': val_texts, 'labels': val_labels}, columns=col_names)
print(f"df_trn has {len(df_trn)} rows, while df_val has {len(df_val)} rows")
print(f"Writing them to {CLAS_PATH}")
df_trn[df_trn['labels'] != 2].to_csv(CLAS_PATH / 'train.csv', header=False, index=False)
df_val.to_csv(CLAS_PATH / 'test.csv', header=False, index=False)
(CLAS_PATH / 'classes.txt').open('w', encoding='utf-8').writelines(f'{o}\n' for o in CLASSES)
if prepare_lm:
print("Preparing LM data")
trn_texts, val_texts = model_selection.train_test_split(
np.concatenate([trn_texts, val_texts]), test_size=0.1)
print(f"trn_texts has {len(trn_texts)} samples, while val_texts has {len(val_texts)} rows")
print(f"Writing them to {LM_PATH}")
df_trn = pd.DataFrame({'text': trn_texts, 'labels': [0] * len(trn_texts)}, columns=col_names)
df_val = pd.DataFrame({'text': val_texts, 'labels': [0] * len(val_texts)}, columns=col_names)
df_trn.to_csv(LM_PATH / 'train.csv', header=False, index=False)
df_val.to_csv(LM_PATH / 'test.csv', header=False, index=False)
def read_imdb(dir_path, lang, split) -> Tuple[List[List[str]], List[str]]:
"""
Reads IMDb data.
:param dir_path: the path to the imdb folder
:param lang: the language (not used here as IMDb is only available in English)
:param split: the split of the data that should be read (train, test, val)
:return: a tuple consisting of a list of lists of tokens and a list of labels
"""
file_path = dir_path / 'train.csv' if split == TRN else dir_path / 'test.csv'
toks, lbls = [], []
mt = MosesTokenizer('en')
print(f'Reading {file_path}...')
with open(file_path, encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
label, text = row
lbls.append(label)
raw_tokens = mt.tokenize(text, return_str=True).split(' ') + [EOS]
tokens = []
for token in raw_tokens:
if number_match_re.match(token):
tokens += number_split_re.sub(r' @\1@ ', token).split()
else:
tokens.append(token)
toks.append(tokens)
return toks, lbls
def read_xnli(dir_path, lang, split) -> Tuple[List[List[str]], List[str]]:
"""
Reads XNLI data.
:param dir_path: the path to the xnli folder
:param lang: the language
:param split: the split of the data that should be read (train, test, val)
:return: a tuple consisting of a list of lists of tokens and a list of labels
"""
file_path = XNLI_PATHS[split]
if split == TRN:
file_path = file_path % lang
elif lang == EN:
file_name = 'xnli.dev.en.tsv' if split == VAL else 'xnli.test.en.tsv'
file_path = f'XNLI-MT-1.0/xnli/{file_name}'
file_path = dir_path / file_path
toks, lbls = [], []
print(f'Reading {file_path}...')
with open(file_path, encoding='utf-8') as f:
reader = csv.reader(f, delimiter='\t', quoting=csv.QUOTE_NONE)
for i, row in enumerate(reader):
if i == 0: # skip the header
continue
# the examples are already tokenized with Moses
if split == TRN:
premise, hypo, label = row
else:
ex_lang = row[0]
if ex_lang != lang:
continue
premise, hypo, label = row[-3], row[-2], row[1]
# TODO add BOS
premise_toks = premise.split(' ') + [EOS]
hypo_toks = hypo.split(' ') + [EOS]
toks.append(premise_toks + [SEP] + hypo_toks)
lbls.append(label)
return toks, lbls
def read_clas_data(dir_path, dataset, lang) -> Tuple[Dict[str, List[List[str]]], Dict[str, List[str]]]:
"""
Read the dataset from the classification datasets and tokenize them.
:param dir_path: the path to the dataset
:param dataset: the name of the dataset
:param lang: the language
:return: a tuple consisting of:
1. a dictionary mapping splits to a list of lists of tokens
2. a dictionary mapping splits to a list of labels
"""
processors = {
'imdb': read_imdb,
'xnli': read_xnli
}
processor = processors[dataset]
toks, lbls = {}, {}
toks[TRN], lbls[TRN] = processor(dir_path, lang, TRN)
toks[TST], lbls[TST] = processor(dir_path, lang, TST)
if dataset == IMDB:
# for IMDb, we need to split off a separate validation set
# note that we train and fine-tune ULMFiT on the full training set in the paper
# to do this, we can just keep the training set the same
trn_len = int(len(toks[TRN]) * 0.9)
toks[TRN], toks[VAL] = toks[TRN][:trn_len], toks[TRN][trn_len:]
lbls[TRN], lbls[VAL] = lbls[TRN][:trn_len], lbls[TRN][trn_len:]
else:
toks[VAL], lbls[VAL] = processor(dir_path, lang, VAL)
return toks, lbls
def replace_number(token):
"""Replaces a number and returns a list of one or multiple tokens."""
@@ -45,23 +253,6 @@ def read_whitespace_file(filepath):
return np.array(tokens)
def read_imdb(file_path, mt):
toks, lbls = [], []
print(f'Reading {file_path}...')
with open(file_path, encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
label, text = row
lbls.append(label)
raw_tokens = mt.tokenize(text, return_str=True).split(' ') + [EOS]
tokens = []
for token in raw_tokens:
if number_match_re.match(token):
tokens += number_split_re.sub(r' @\1@ ', token).split()
else:
tokens.append(token)
toks.append(tokens)
return np.array(toks), np.array(lbls)
class DataStump:
@@ -124,4 +315,8 @@ class TextReader():
def get_batch(self, i, seq_len):
source = self.data
seq_len = min(seq_len, len(source) - 1 - i)
return source[i:i+seq_len], source[i+1:i+1+seq_len].view(-1)
return source[i:i+seq_len], source[i+1:i+1+seq_len].view(-1)
if __name__ == "__main__":
fire.Fire() # allows using all functions via CLI e.g. python utils.py prepare_imdb aclImdb.tgz
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
ROOT="data"
DATA_DIR="${ROOT}/imdb"
mkdir -p "${DATA_DIR}"
echo "Saving data in $DATA_DIR"
wget -c "http://files.fast.ai/data/aclImdb.tgz" -P "${DATA_DIR}"
echo "Imdb is raw text so we are tokenizing it with Moses"
python -m fastai_contrib.utils prepare_imdb "${DATA_DIR}/aclImdb.tgz" --prepare_lm==False
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
ROOT="data"
DATA_DIR="${ROOT}/wiki/"
mkdir -p "${DATA_DIR}"
echo "Saving data in ""$DATA_DIR"
wget -c "https://s3.amazonaws.com/research.metamind.io/wikitext/wikitext-2-v1.zip" -P "${DATA_DIR}"
wget -c "https://s3.amazonaws.com/research.metamind.io/wikitext/wikitext-103-v1.zip" -P "${DATA_DIR}"
unzip "${DATA_DIR}/wikitext-2-v1.zip" -d "${DATA_DIR}"
unzip "${DATA_DIR}/wikitext-103-v1.zip" -d "${DATA_DIR}"
for f in ${DATA_DIR}/wikitext-*/wiki.*.tokens; do
nf=$(dirname $f)/en.$(basename $f)
echo "Renaming $f to $nf"
mv $f $(dirname $f)/en.$(basename $f)
done
echo "Please note wikitext en is already tokenized with Moses"
+17 -16
View File
@@ -3,6 +3,16 @@
# Script is partially based on https://github.com/facebookresearch/fastText/blob/master/get-wikimedia.sh
ROOT="data"
echo "Saving data in ""$ROOT"
if [ "$1" == "" ] ; then
read -r -p "Choose a language (e.g. en, bh, fr, etc.): " choice
LANG="$choice"
else
LANG="$1"
fi
echo "Chosen language: ""$LANG"
DUMP_DIR="${ROOT}/wiki_dumps"
EXTR_DIR="${ROOT}/wiki_extr"
WIKI_DIR="${ROOT}/wiki"
@@ -12,20 +22,10 @@ mkdir -p "${DUMP_DIR}"
mkdir -p "${EXTR_DIR}"
mkdir -p "${WIKI_DIR}"
echo "Saving data in ""$ROOT"
read -r -p "Choose a language (e.g. en, bh, fr, etc.): " choice
LANG="$choice"
echo "Chosen language: ""$LANG"
DUMP_FILE="${LANG}wiki-latest-pages-articles.xml.bz2"
DUMP_PATH="${DUMP_DIR}/${DUMP_FILE}"
if [ ! -f "${DUMP_PATH}" ]; then
read -r -p "Continue to download (WARNING: This might be big and can take a long time!) (y/n)? " choice
case "$choice" in
y|Y ) echo "Starting download...";;
n|N ) echo "Exiting";exit 1;;
* ) echo "Invalid answer";exit 1;;
esac
wget -c "https://dumps.wikimedia.org/""${LANG}""wiki/latest/""${DUMP_FILE}""" -P "${DUMP_DIR}"
else
echo "${DUMP_PATH} already exists. Skipping download."
@@ -36,17 +36,18 @@ if [ ! -d "${EXTR}" ]; then
git clone https://github.com/attardi/wikiextractor.git
cd "${EXTR}"
python setup.py install
cd ..
fi
EXTR_PATH="${EXTR_DIR}/${LANG}"
if [ ! -d "${EXTR_PATH}" ]; then
read -r -p "Continue to extract Wikipedia (WARNING: This might take a long time!) (y/n)? " choice
case "$choice" in
y|Y ) echo "Extracting ${DUMP_PATH} to ${EXTR_PATH}...";;
n|N ) echo "Exiting";exit 1;;
* ) echo "Invalid answer";exit 1;;
esac
python wikiextractor/WikiExtractor.py -s --json -o "${EXTR_PATH}" "${DUMP_PATH}"
else
echo "${EXTR_PATH} already exists. Skipping extraction."
fi
python -m ulmfit.create_wikitext -i "${EXTR_PATH}" -l "${LANG}" -o "${WIKI_DIR}"
python -m ulmfit.postprocess_wikitext "${WIKI_DIR}/${LANG}-2" $LANG
python -m ulmfit.postprocess_wikitext "${WIKI_DIR}/${LANG}-100" $LANG
#python -m ulmfit.postprocess_wikitext "${WIKI_DIR}/${LANG}-all" $LANG
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Script to download a Wikipedia dump
# Script is partially based on https://github.com/facebookresearch/fastText/blob/master/get-wikimedia.sh
ROOT="data"
XNLI_DIR="${ROOT}/xnli"
mkdir -p "${ROOT}"
mkdir -p "${XNLI_DIR}"
echo "Saving data in ""$ROOT"
MT_FILE="XNLI-MT-1.0.zip"
XNLI_FILE="XNLI-1.0.zip"
MT_PATH="${XNLI_DIR}/${MT_FILE}"
XNLI_PATH="${XNLI_DIR}/${XNLI_FILE}"
if [ ! -f "${MT_PATH}" ]; then
wget -c "https://s3.amazonaws.com/xnli/XNLI-MT-1.0.zip" -P "${XNLI_DIR}"
wget -c "https://s3.amazonaws.com/xnli/XNLI-1.0.zip" -P "${XNLI_DIR}"
else
echo "${MT_PATH} already exists. Skipping download."
fi
unzip "${MT_PATH}" -d "${XNLI_DIR}"
unzip "${XNLI_PATH}" -d "${XNLI_DIR}"
echo "Please note xnli en is already tokenized with Moses"
+3 -1
View File
@@ -1,2 +1,4 @@
fire>=0.1.3
cupy>=5.0.0
cupy>=5.0.0
scikit-learn>=0.20
sacremoses>=0.0.5
+47
View File
@@ -0,0 +1,47 @@
import fire
import pytest
import ulmfit.pretrain_lm
import ulmfit.train_clas
from fastai import *
from fastai.text import *
from fastai_contrib.utils import *
"""
It is a mixture of a pytest unit test and woven together to compose an end to end functional test.
"""
def check_data_exists():
data = get_data_folder()
wt2 = data / "wiki" / "wikitext-2"
imdb = data / "imdb"
ensure_paths_exists(wt2 / "en.wiki.train.tokens",
imdb / "train.csv",
message="We don't run data preparation scripts automatically as it takes ages, run prepare_wiki-en.sh & prepare_imdb.sh")
return imdb, wt2
def test_pretrain_lm():
imdb,wt2 = check_data_exists()
lm_name="end-to-end-test-quick"
results = ulmfit.pretrain_lm.pretrain_lm(
dir_path=wt2,
lang='en',
qrnn=True,
clean=True,
max_vocab=1000,
bs=80,
num_epochs=1,
name=lm_name,
ds_pct=0.03
)
assert results['accuracy'] > 0.30
results = ulmfit.train_clas.new_train_clas(
data_dir=get_data_folder(),
lang='en', pretrain_name=lm_name, model_dir=wt2/'models',
qrnn=True,
fine_tune=True,
max_vocab=1000,
bs=20, bptt=70, name=lm_name+'-imdb-clas',
dataset='imdb',
ds_pct=0.03)
+51 -2
View File
@@ -1,2 +1,51 @@
# todo
- [] create new docs
# Todo
- [ ] Update these docs
Getting Started
---
## Download and Extract the Wikipedia corpus
In Linux, you can do all the following steps automatically with [prepare_wiki.sh](./prepare_wiki.sh)
**Manual Instructions**
We use the [WikiExtractor.py](http://medialab.di.unipi.it/wiki/Wikipedia_Extractor). It is a Python script that extracts and cleans text from a [Wikipedia database dump](http://download.wikimedia.org/).
At the end of this step, you should have the following directory structure inside ulmfit:
```bash
|- data
|- wiki
|- wiki_dumps
|- wiki_extr
|- wikiextractor
```
The extracted data should be in the folder `wiki_extr` -> language name e.g.`en` (english), `fr` (french) `hi` (hindi) and so on.
## Create and Post Process WikiText
### Get the Dependencies
**Python Fire**: To install Python Fire with pip, run: `pip install fire`
To install Python Fire with conda, run: `conda install fire -c conda-forge`
**Moses Tokenizer**: To install Moses Tokenizer: `pip install -U sacremoses`
Use the Python script [create_wikitext.py](./create_wikitext.py) to process the extracted Wikipedia documents.
### Create and Post-Process
If you used the automated shell script from previous step, this might look something like
```bash
python create_wikitext.py -i data/wiki_extr/hi -o data/wiki/hi -l hi
```
for hindi (unicode: 'hi')
This should create two splits of your Wikimedia Dumps: a small and large one.
_**Then**_, use the [postprocess_wikitext.py](./postprocess_wikitext.py) script to finish post processing. This processes numbers, builds a vocab, and limits the vocabulary size. This might look following for Hindi (`hi`)
```bash
python postprocess_wikitext.py data/wiki/hi-2 hi
python postprocess_wikitext.py data/wiki/hi-100 hi
```
+9 -4
View File
@@ -57,7 +57,7 @@ def write_wikitext(file_path, text_iter, mt, num_tokens, mode='w'):
f_out.write(tokenized + '\n')
total_num_tokens += num_tokens_article + 1
if total_num_tokens > num_tokens:
if num_tokens is not None and total_num_tokens > num_tokens:
break
if i % 10000 == 0 and i > 0:
print('Processed {:,} documents. Total # tokens: {:,}.'.format(i, total_num_tokens))
@@ -76,8 +76,10 @@ def main(args):
sml_wiki = output / f'{args.lang}-2'
lrg_wiki = output / f'{args.lang}-100'
all_wiki = output / f'{args.lang}-all'
sml_wiki.mkdir(exist_ok=True)
lrg_wiki.mkdir(exist_ok=True)
all_wiki.mkdir(exist_ok=True)
text_iter = get_texts(input_path)
@@ -87,15 +89,18 @@ def main(args):
sml_file_path = sml_wiki / f'{args.lang}.wiki.{split}.tokens'
write_wikitext(sml_file_path, text_iter, mt, token_num)
lrg_file_path = lrg_wiki / f'{args.lang}.wiki.{split}.tokens'
all_file_path = all_wiki / f'{args.lang}.wiki.{split}.tokens'
# copy the content of the small file to the large file
print(f'Copying {sml_file_path} to {lrg_file_path}.')
print(f'Copying {sml_file_path} to {lrg_file_path} & {all_file_path}.')
copyfile(sml_file_path, lrg_file_path)
copyfile(sml_file_path, all_file_path)
# add the new articles to the existing ones
lrg_wiki_train = lrg_wiki / f'{args.lang}.wiki.train.tokens'
write_wikitext(lrg_wiki_train, text_iter, mt, 98000000, mode='a')
all_wiki_train = all_wiki / f'{args.lang}.wiki.train.tokens'
copyfile(lrg_wiki_train, all_wiki_train)
write_wikitext(lrg_wiki_train, text_iter, mt, None, mode='a') # TODO fix it (change lrg to all)
if __name__ == '__main__':
+21 -37
View File
@@ -7,6 +7,9 @@ import argparse
from collections import Counter
from pathlib import Path
import fire
from fastai_contrib.utils import replace_number, UNK
@@ -76,46 +79,27 @@ def replace_numbers(file_path, unk_path):
f_out.write(line)
def main(args):
input_path = Path(args.input)
assert input_path.exists(), f'Error: {input_path} does not exist.'
sml_wiki = input_path / f'{args.lang}-2'
lrg_wiki = input_path / f'{args.lang}-100'
assert sml_wiki.exists(), f'Error: {sml_wiki} does not exist.'
assert lrg_wiki.exists(), f'Error: {lrg_wiki} does not exist.'
def postprocess_wikitext(path, lang):
wiki_path = Path(path)
assert wiki_path.exists(), f'Error: {wiki_path} does not exist.'
dest_path = wiki_path.parent / (wiki_path.name + "-unk")
dest_path.mkdir(exist_ok=True)
splits = ['train', 'valid', 'test']
for wiki in [sml_wiki, lrg_wiki]:
for split in splits:
# replace numbers with placeholders
file_path = wiki / f'{args.lang}.wiki.{split}.tokens'
unk_path = wiki / f'{args.lang}.wiki.{split}.tokens.unk'
replace_numbers(file_path, unk_path)
sml_wiki_train = sml_wiki / f'{args.lang}.wiki.train.tokens'
lrg_wiki_train = lrg_wiki / f'{args.lang}.wiki.train.tokens'
sml_vocab = build_vocab(sml_wiki_train)
print(f'{args.lang}-2 vocab size: {len(sml_vocab)}')
lrg_vocab = build_vocab(lrg_wiki_train)
print(f'{args.lang}-100 vocab size: {len(lrg_vocab)}')
for split in splits:
# replace numbers with placeholders
file_path = wiki_path / f'{lang}.wiki.{split}.tokens'
assert file_path.exists(), f"Error: {file_path} does not exist."
unk_path = dest_path / file_path.name
replace_numbers(file_path, unk_path)
# replace words not in the vocab with <unk>
for wiki, vocab in zip([sml_wiki, lrg_wiki], [sml_vocab, lrg_vocab]):
for split in splits:
unk_path = wiki / f'{args.lang}.wiki.{split}.tokens.unk'
limit_vocab(unk_path, vocab)
wiki_train = dest_path / f'{lang}.wiki.train.tokens'
vocab = build_vocab(wiki_train)
print(f'{wiki_path} vocab size: {len(vocab)}')
for split in splits:
unk_path = dest_path / f'{lang}.wiki.{split}.tokens'
limit_vocab(unk_path, vocab)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', required=True,
help='the directory of the wikitext files')
parser.add_argument('-l', '--lang', required=True,
help='the iso code of the language of the Wikipedia '
'documents, e.g. en, fr, de, etc.')
args = parser.parse_args()
main(args)
fire.Fire(postprocess_wikitext)
+30 -16
View File
@@ -32,11 +32,11 @@ def accuracy_bwd(input, targs):
return accuracy(input[...,1], targs[...,1])
def pretrain_lm(dir_path, cuda_id=0, qrnn=True, clean=True, max_vocab=60000,
bs=70, bptt=70, name='wt-103', model_dir='models', num_epochs=10,
bidir=False, ds_pct=1.0):
def pretrain_lm(dir_path, lang='en', cuda_id=0, qrnn=True, clean=True, max_vocab=60000,
bs=70, bptt=70, name='wt-103', num_epochs=10, bidir=False, ds_pct=1.0):
"""
:param dir_path: The path to the directory of the file.
:param dir_path: The path to the directory that contains wiki text
:param lang: the language unicode
:param cuda_id: The id of the GPU. Uses GPU 0 by default or no GPU when
run on CPU.
:param qrrn: Use a QRNN. Requires installing cupy.
@@ -48,6 +48,8 @@ def pretrain_lm(dir_path, cuda_id=0, qrnn=True, clean=True, max_vocab=60000,
:param model_dir: The path to the directory where the models should be saved
:param bidir: whether the language model is bidirectional
"""
results = {}
model_dir = 'models' # removed from params, as it is absolute models location in train_clas and here it is relative
if not torch.cuda.is_available():
print('CUDA not available. Setting device=-1.')
cuda_id = -1
@@ -63,9 +65,9 @@ def pretrain_lm(dir_path, cuda_id=0, qrnn=True, clean=True, max_vocab=60000,
if qrnn:
print('Using QRNNs...')
trn_path = dir_path / 'wiki.train.tokens'
val_path = dir_path / 'wiki.valid.tokens'
tst_path = dir_path / 'wiki.test.tokens'
trn_path = dir_path / f'{lang}.wiki.train.tokens'
val_path = dir_path / f'{lang}.wiki.valid.tokens'
tst_path = dir_path / f'{lang}.wiki.test.tokens'
for path_ in [trn_path, val_path, tst_path]:
assert path_.exists(), f'Error: {path_} does not exist.'
@@ -74,8 +76,9 @@ def pretrain_lm(dir_path, cuda_id=0, qrnn=True, clean=True, max_vocab=60000,
trn_tok = read_whitespace_file(trn_path)
val_tok = read_whitespace_file(val_path)
if ds_pct < 1.0:
trn_tok = trn_tok[:int(len(trn_tok) * ds_pct)]
val_tok = val_tok[:int(len(val_tok) * ds_pct)]
trn_tok = trn_tok[:max(20, int(len(trn_tok) * ds_pct))]
val_tok = val_tok[:max(20, int(len(val_tok) * ds_pct))]
print(f"Limiting data sets to {ds_pct*100}%, trn {len(trn_tok)}, val: {len(val_tok)}")
# create the vocabulary
cnt = Counter(word for sent in trn_tok for word in sent)
@@ -113,7 +116,7 @@ def pretrain_lm(dir_path, cuda_id=0, qrnn=True, clean=True, max_vocab=60000,
if qrnn:
emb_sz, nh, nl = 400, 1550, 3
#dps = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
dps = np.array([0.25, 0.1, 0.2, 0.02, 0.15]) * 0.1
dps = np.array([0.25, 0.1, 0.2, 0.02, 0.15])
drop_mult = 0.1
else:
emb_sz, nh, nl = 400, 1150, 3
@@ -121,11 +124,11 @@ def pretrain_lm(dir_path, cuda_id=0, qrnn=True, clean=True, max_vocab=60000,
dps = np.array([0.25, 0.1, 0.2, 0.02, 0.15])
drop_mult = 0.1
fastai.text.learner.default_dropout['language'] = dps
fastai.text.learner.default_dropout['language'] = dps * drop_mult
lm_learner = bilm_learner if bidir else language_model_learner
learn = lm_learner(data_lm, bptt=bptt, emb_sz=emb_sz, nh=nh, nl=nl, pad_token=1,
drop_mult=drop_mult, tie_weights=True,
drop_mult=drop_mult, tie_weights=True, model_dir=model_dir,
bias=True, qrnn=qrnn, clip=0.12)
# compared to standard Adam, we set beta_1 to 0.8
learn.opt_fn = partial(optim.Adam, betas=(0.8, 0.99))
@@ -136,23 +139,34 @@ def pretrain_lm(dir_path, cuda_id=0, qrnn=True, clean=True, max_vocab=60000,
else:
learn.metrics = [accuracy]
# save vocabulary
print('Saving vocabulary...')
with open(model_dir / f'itos_{name}.pkl', 'wb') as f:
print(f"Saving vocabulary as {dir_path / model_dir}")
results['itos_fname'] = dir_path / model_dir / f'itos_{name}.pkl'
with open(results['itos_fname'], 'wb') as f:
pickle.dump(itos, f)
learn.fit_one_cycle(num_epochs, 5e-3, (0.8, 0.7), wd=1e-7)
if clean and max_vocab is None:
# only if we use the unpreprocessed version and the full vocabulary
# are the perplexity results comparable to previous work
print(f"Validating model performance with test tokens from: {trn_path}")
tst_tok = read_whitespace_file(trn_path)
tst_ids = np.array([([stoi.get(w, stoi[UNK]) for w in s]) for s in tst_tok])
logloss, perplexity = validate(model, tst_ids, bptt)
logloss, perplexity = validate(learn.model, tst_ids, bptt)
print('Test logloss:', logloss.item(), 'perplexity:', perplexity.item())
print(f"Saving models at {learn.path / learn.model_dir}")
learn.save(f'{model_name}_{name}')
torch.save(learn.opt.opt.state_dict(), learn.path / learn.model_dir / f'{model_name}3_{name}_state.pth')
opt_state_path = learn.path / learn.model_dir / f'{model_name}3_{name}_state.pth'
print(f"Saving optimiser state at {opt_state_path}")
torch.save(learn.opt.opt.state_dict(), opt_state_path)
results['accuracy'] = learn.validate()[1]
return results
if __name__ == '__main__':
fire.Fire(pretrain_lm)
+111 -65
View File
@@ -5,81 +5,118 @@ Optionally fine-tune LM before.
import numpy as np
import pickle
import torch
from fastai.text import TextLMDataBunch, TextClasDataBunch, language_model_learner, text_classifier_learner
from fastai import fit_one_cycle
from fastai_contrib.utils import PAD, UNK, read_imdb, PAD_TOKEN_ID
from fastai_contrib.utils import PAD, UNK, read_clas_data, PAD_TOKEN_ID, DATASETS, TRN, VAL, TST, ensure_paths_exists
from fastai.text.transform import Vocab
from sacremoses import MosesTokenizer
import fire
from collections import Counter
from pathlib import Path
def new_train_clas(dir_path, lang='en', pretrain_name='wt-103', model_dir='models', qrnn=True,
fine_tune=True, clean=True, max_vocab=30000, bs=70, bptt=70):
dir_path = Path(dir_path)
def new_train_clas(data_dir, lang='en', cuda_id=0, pretrain_name='wt103', model_dir='models',
qrnn=False,
fine_tune=True, max_vocab=30000, bs=20, bptt=70, name='imdb-clas',
dataset='imdb', ds_pct=1.0):
"""
:param data_dir: The path to the `data` directory
:param lang: the language unicode
:param cuda_id: The id of the GPU. Uses GPU 0 by default or no GPU when
run on CPU.
:param pretrain_name: name of the pretrained model
:param model_dir: The path to the directory where the pretrained model is saved
:param qrrn: Use a QRNN. Requires installing cupy.
:param fine_tune: Fine-tune the pretrained language model
:param max_vocab: The maximum size of the vocabulary.
:param bs: The batch size.
:param bptt: The back-propagation-through-time sequence length.
:param name: The name used for both the model and the vocabulary.
:param dataset: The dataset used for evaluation. Currently only IMDb and
XNLI are implemented. Assumes dataset is located in `data`
folder and that name of folder is the same as dataset name.
"""
results={}
if not torch.cuda.is_available():
print('CUDA not available. Setting device=-1.')
cuda_id = -1
torch.cuda.set_device(cuda_id)
print(f'Dataset: {dataset}. Language: {lang}.')
assert dataset in DATASETS, f'Error: {dataset} processing is not implemented.'
assert (dataset == 'imdb' and lang == 'en') or not dataset == 'imdb',\
'Error: IMDb is only available in English.'
data_dir = Path(data_dir)
assert data_dir.name == 'data',\
f'Error: Name of data directory should be data, not {data_dir.name}.'
dataset_dir = data_dir / dataset
model_dir = Path(model_dir)
assert dir_path.exists(), f'Error: {dir_path} does not exist.'
assert model_dir.exists(), f'Error: {model_dir} does not exist.'
if qrnn:
print('Using QRNNs...')
model_name = 'qrnn' if qrnn else 'lstm'
lm_name = f'{model_name}_{pretrain_name}'
pretrained_fname = (lm_name, f'itos_{pretrain_name}')
if clean:
# use no preprocessing besides MosesTokenizer
tmp_dir = dir_path / 'tmp'
tmp_dir.mkdir(exist_ok=True)
if not (tmp_dir / 'train_ids.npy').exists():
trn_path = dir_path / 'train.csv'
tst_path = dir_path / 'test.csv'
assert trn_path.exists(), f'Error: {trn_path} does not exist.'
assert tst_path.exists(), f'Error: {tst_path} does not exist.'
trn_toks, trn_lbls = read_imdb(trn_path, MosesTokenizer(lang))
tst_toks, tst_lbls = read_imdb(tst_path, MosesTokenizer(lang))
ensure_paths_exists(data_dir,
dataset_dir,
model_dir,
model_dir/f"{pretrained_fname[0]}.pth",
model_dir/f"{pretrained_fname[1]}.pkl")
# split off validation set if it does not exist
val_path = dir_path / 'valid.csv'
if not val_path.exists():
trn_len = int(len(trn_toks) * 0.9)
trn_toks, val_toks = trn_toks[:trn_len], trn_toks[trn_len:]
trn_lbls, val_lbls = trn_lbls[:trn_len], trn_lbls[trn_len:]
else:
val_toks, val_lbls = read_imdb(val_path, MosesTokenizer(lang))
# create the vocabulary
cnt = Counter(word for example in trn_toks for word in example)
itos = [o for o, c in cnt.most_common(n=max_vocab)]
itos.insert(0, PAD)
itos.insert(0, UNK)
stoi = {w: i for i, w in enumerate(itos)}
with open(tmp_dir / 'itos.pkl', 'wb') as f:
pickle.dump(itos, f)
tmp_dir = dataset_dir / 'tmp'
tmp_dir.mkdir(exist_ok=True)
vocab_file = tmp_dir / f'vocab_{lang}.pkl'
trn_ids = np.array([([stoi.get(w, stoi[UNK]) for w in s]) for s in trn_toks])
val_ids = np.array([([stoi.get(w, stoi[UNK]) for w in s]) for s in val_toks])
tst_ids = np.array([([stoi.get(w, stoi[UNK]) for w in s]) for s in tst_toks])
if not (tmp_dir / f'{TRN}_{lang}_ids.npy').exists():
print('Reading the data...')
toks, lbls = read_clas_data(dataset_dir, dataset, lang)
print(f'Train size: {len(trn_ids)}. Valid size: {len(val_ids)}. '
f'Test size: {len(tst_ids)}.')
# create the vocabulary
counter = Counter(word for example in toks[TRN] for word in example)
itos = [word for word, count in counter.most_common(n=max_vocab)]
itos.insert(0, PAD)
itos.insert(0, UNK)
vocab = Vocab(itos)
stoi = vocab.stoi
with open(vocab_file, 'wb') as f:
pickle.dump(vocab, f)
for split, ids, lbl in zip(['train', 'valid', 'test'],
[trn_ids, val_ids, tst_ids],
[trn_lbls, val_lbls, tst_lbls]):
np.save(tmp_dir / f'{split}_ids.npy', ids)
np.save(tmp_dir / f'{split}_lbl.npy', lbl)
data_lm = TextLMDataBunch.from_id_files(tmp_dir, test='test')
data_clas = TextClasDataBunch.from_id_files(tmp_dir, test='test')
ids = {}
for split in [TRN, VAL, TST]:
ids[split] = np.array([([stoi.get(w, stoi[UNK]) for w in s])
for s in toks[split]])
np.save(tmp_dir / f'{split}_{lang}_ids.npy', ids[split])
np.save(tmp_dir / f'{split}_{lang}_lbl.npy', lbls[split])
else:
# use fastai peprocessing and tokenization
data_lm = TextLMDataBunch.from_csv(dir_path, bs=bs)
data_clas = TextClasDataBunch.from_csv(dir_path, vocab=data_lm.train_ds.vocab, bs=bs)
print('Loading the pickled data...')
ids, lbls = {}, {}
for split in [TRN, VAL, TST]:
ids[split] = np.load(tmp_dir / f'{split}_{lang}_ids.npy')
lbls[split] = np.load(tmp_dir / f'{split}_{lang}_lbl.npy')
with open(vocab_file, 'rb') as f:
vocab = pickle.load(f)
# todo implemend save and load
#data_lm.save()
#data_clas.save()
#data_lm = TextLMDataBunch.load(path)
#data_clas = TextClasDataBunch.load(path, bs=bs)
print(f'Train size: {len(ids[TRN])}. Valid size: {len(ids[VAL])}. '
f'Test size: {len(ids[TST])}.')
if ds_pct < 1.0:
print(f"Makeing the dataset smaller {ds_pct}")
for split in [TRN, VAL, TST]:
ids[split] = ids[split][:int(len(ids[split])*ds_pct)]
data_lm = TextLMDataBunch.from_ids(path=tmp_dir, vocab=vocab, train_ids=ids[TRN],
valid_ids=ids[VAL], bs=bs, bptt=bptt)
# TODO TextClasDataBunch allows tst_ids as input, but not tst_lbls?
data_clas = TextClasDataBunch.from_ids(
path=tmp_dir, vocab=vocab, train_ids=ids[TRN], valid_ids=ids[VAL],
train_lbls=lbls[TRN], valid_lbls=lbls[VAL], bs=bs)
if qrnn:
emb_sz, nh, nl = 400, 1550, 3
@@ -88,29 +125,38 @@ def new_train_clas(dir_path, lang='en', pretrain_name='wt-103', model_dir='model
learn = language_model_learner(
data_lm, bptt=bptt, emb_sz=emb_sz, nh=nh, nl=nl, qrnn=qrnn,
pad_token=PAD_TOKEN_ID,
pretrained_fnames=(f'lstm_{pretrain_name}', f'itos_{pretrain_name}'),
pretrained_fnames=pretrained_fname,
path=model_dir.parent, model_dir=model_dir.name)
if fine_tune:
lm_enc_finetuned = f"{lm_name}_{dataset}_enc"
if fine_tune and not (model_dir / f"lm_enc_finetuned.pth").exists():
print('Fine-tuning the language model...')
learn.unfreeze()
learn.fit(2, slice(1e-4, 1e-2))
# save encoder
learn.save_encoder('enc')
learn.save_encoder(lm_enc_finetuned)
print("Starting classifier training")
learn = text_classifier_learner(data_clas, bptt=bptt, pad_token=PAD_TOKEN_ID,
path=model_dir.parent, model_dir=model_dir.name,
qrnn=qrnn, emb_sz=emb_sz, nh=nh, nl=nl)
learn.load_encoder('enc')
fit_one_cycle(learn, 1, 5e-3, (0.8, 0.7), wd=1e-7)
learn.load_encoder(lm_enc_finetuned)
learn.fit_one_cycle(1, 2e-2, moms=(0.8, 0.7), wd=1e-7)
learn.freeze_to(-2)
fit_one_cycle(learn, 1, 5e-3, (0.8, 0.7), wd=1e-7)
learn.fit_one_cycle(1, slice(1e-2 / (2.6 ** 4), 1e-2), moms=(0.8, 0.7), wd=1e-7)
learn.freeze_to(-3)
learn.fit_one_cycle(1, slice(5e-3 / (2.6 ** 4), 5e-3), moms=(0.8, 0.7), wd=1e-7)
learn.unfreeze()
fit_one_cycle(learn, 10, 5e-3, (0.8, 0.7), wd=1e-7)
learn.fit_one_cycle(2, slice(1e-3 / (2.6 ** 4), 1e-3), moms=(0.8, 0.7), wd=1e-7)
results['accuracy'] = learn.validate()[1]
print(f"Saving models at {learn.path / learn.model_dir}")
learn.save(f'{model_name}_{name}')
return results
if __name__ == '__main__': fire.Fire(new_train_clas)
if __name__ == '__main__':
fire.Fire(new_train_clas)