mirror of
https://github.com/wassname/multifit.git
synced 2026-09-22 13:20:28 +08:00
183 lines
6.6 KiB
Python
183 lines
6.6 KiB
Python
"""
|
|
Utility methods for data processing.
|
|
"""
|
|
import pandas as pd
|
|
import numpy as np
|
|
from fastai import F, to_device
|
|
import torch
|
|
from tqdm import tqdm
|
|
import re
|
|
import csv
|
|
from functools import reduce
|
|
from fastai.text.data import TextDataset
|
|
from fastai.text.transform import Tokenizer, BaseTokenizer, Vocab, default_rules
|
|
from fastai.torch_core import *
|
|
from pathlib import Path
|
|
|
|
EOS = '<eos>'
|
|
UNK = '<unk>'
|
|
PAD = '<pad>'
|
|
PAD_TOKEN_ID = 1
|
|
|
|
number_match_re = re.compile(r'^([0-9]+[,.]?)+$')
|
|
number_split_re = re.compile(r'([,.])')
|
|
|
|
class SentencepieceTokenizer(BaseTokenizer):
|
|
def __init__(self, path:PathOrStr, cache_name:str='tmp'):
|
|
try:
|
|
import sentencepiece as spm
|
|
except ImportError:
|
|
raise Exception('sentencepiece module is missing: run `pip install sentencepiece`')
|
|
self.tok = spm.SentencePieceProcessor()
|
|
self.tok.Load(str(Path(path) / cache_name / 'm.model'))
|
|
def tokenizer(self, t:str) -> List[str]:
|
|
return self.tok.EncodeAsPieces(t)
|
|
def add_special_cases(self, toks:Collection[str]):
|
|
pass
|
|
|
|
def get_sentencepiece(path:PathOrStr, dataset:TextDataset, rules:ListRules=None,
|
|
cache_name:str='tmp', vocab_size:int=30000,
|
|
model_type:str='unigram', input_sentence_size:int=1E7,
|
|
pad_idx:int=PAD_TOKEN_ID):
|
|
try:
|
|
import sentencepiece as spm
|
|
except ImportError:
|
|
raise Exception('sentencepiece module is missing: run `pip install sentencepiece`')
|
|
|
|
path = Path(path)
|
|
os.makedirs(path / cache_name, exist_ok=True)
|
|
rules = rules if rules else default_rules
|
|
|
|
if not os.path.isfile(path / cache_name / 'm.model') or not os.path.isfile(path / 'itos.pkl'):
|
|
raw_text = reduce(lambda t, rule: rule(t), rules, '\n'.join(dataset.x))
|
|
raw_text_path = path / cache_name / 'all_text.txt'
|
|
with open(raw_text_path, 'w') as f:
|
|
f.write(raw_text)
|
|
|
|
sp_params = f'--input={raw_text_path} --pad_id={pad_idx} --unk_id=0' \
|
|
f'--character_coverage=1.0 --bos_id=-1 --eos_id=-1 ' \
|
|
f'--input_sentence_size={int(input_sentence_size)} ' \
|
|
f'--model_prefix={path / cache_name / "m"} ' \
|
|
f'--vocab_size={vocab_size} --model_type={model_type} '
|
|
spm.SentencePieceTrainer.Train(sp_params)
|
|
|
|
with open(path / cache_name / 'm.vocab', 'r') as f:
|
|
vocab = [line.split('\t')[0] for line in f.readlines()]
|
|
vocab[0] = UNK
|
|
vocab[pad_idx] = PAD
|
|
|
|
pickle.dump(vocab, open(path / 'itos.pkl', 'wb'))
|
|
|
|
vocab = Vocab(pickle.load(open(path / 'itos.pkl', 'rb')))
|
|
spt = SentencepieceTokenizer(path, cache_name)
|
|
tokenizer = Tokenizer(tok_func=lambda lang: spt, rules=rules)
|
|
|
|
return {'tokenizer': tokenizer, 'vocab': vocab}
|
|
|
|
def replace_number(token):
|
|
"""Replaces a number and returns a list of one or multiple tokens."""
|
|
if number_match_re.match(token):
|
|
return number_split_re.sub(r' @\1@ ', token)
|
|
return token
|
|
|
|
|
|
def read_file(file_path, outname):
|
|
"""Reads a text file and writes it to a .csv."""
|
|
with open(file_path, encoding='utf8') as f:
|
|
text = f.readlines()
|
|
df = pd.DataFrame(
|
|
{'text': np.array(text), 'labels': np.zeros(len(text))},
|
|
columns=['labels', 'text'])
|
|
df.to_csv(file_path.parent / f'{outname}.csv', header=False, index=False)
|
|
|
|
|
|
def read_whitespace_file(filepath):
|
|
"""Reads a file and prepares the tokens."""
|
|
tokens = []
|
|
with open(filepath, encoding='utf-8') as f:
|
|
for line in f:
|
|
# newlines are replaced with EOS
|
|
tokens.append(line.split() + [EOS])
|
|
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:
|
|
"""Placeholder class as LanguageModelLoader requires object with ids attribute."""
|
|
def __init__(self, ids):
|
|
self.ids = ids
|
|
self.loss_func = F.cross_entropy
|
|
|
|
|
|
def validate(model, ids, bptt=2000):
|
|
"""
|
|
Return the validation loss and perplexity of a model
|
|
:param model: model to test
|
|
:param ids: data on which to evaluate the model
|
|
:param bptt: bptt for this evaluation (doesn't change the result, only the speed)
|
|
From https://github.com/sgugger/Adam-experiments/blob/master/lm_val_fns.py#L34
|
|
"""
|
|
data = TextReader(np.concatenate(ids), bptt)
|
|
model.eval()
|
|
model.reset()
|
|
total_loss, num_examples = 0., 0
|
|
for inputs, targets in tqdm(data):
|
|
outputs, raws, outs = model(to_device(inputs, None))
|
|
p_vocab = F.softmax(outputs, 1)
|
|
for i, pv in enumerate(p_vocab):
|
|
targ_pred = pv[targets[i]]
|
|
total_loss -= torch.log(targ_pred.detach())
|
|
num_examples += len(inputs)
|
|
mean = total_loss / num_examples # divide by total number of tokens
|
|
return mean, np.exp(mean)
|
|
|
|
|
|
class TextReader():
|
|
""" Returns a language model iterator that iterates through batches that are of length N(bptt,5)
|
|
The first batch returned is always bptt+25; the max possible width. This is done because of they way that pytorch
|
|
allocates cuda memory in order to prevent multiple buffers from being created as the batch width grows.
|
|
From: https://github.com/sgugger/Adam-experiments/blob/master/lm_val_fns.py#L3
|
|
"""
|
|
def __init__(self, nums, bptt, backwards=False):
|
|
self.bptt,self.backwards = bptt,backwards
|
|
self.data = self.batchify(nums)
|
|
self.i,self.iter = 0,0
|
|
self.n = len(self.data)
|
|
|
|
def __iter__(self):
|
|
self.i,self.iter = 0,0
|
|
while self.i < self.n-1 and self.iter<len(self):
|
|
res = self.get_batch(self.i, self.bptt)
|
|
self.i += self.bptt
|
|
self.iter += 1
|
|
yield res
|
|
|
|
def __len__(self): return self.n // self.bptt
|
|
|
|
def batchify(self, data):
|
|
data = np.array(data)[:,None]
|
|
if self.backwards: data=data[::-1]
|
|
return torch.LongTensor(data)
|
|
|
|
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) |