datastats / datastats.py
yangwang825's picture
Update datastats.py
f56951b verified
raw
history blame contribute delete
No virus
10.7 kB
""" DataStats metric. """
import logging
import functools
from collections import Counter
from multiprocessing import Pool
from contextlib import contextmanager
from typing import List, Any, Dict, Optional
from collections import namedtuple as _namedtuple
import spacy
import datasets
import evaluate
from packaging import version
logger = logging.getLogger(__name__)
try:
_en = spacy.load('en_core_web_sm')
except OSError as stderr:
spacy.cli.download('en_core_web_sm')
_en = spacy.load('en_core_web_sm')
@contextmanager
def filter_logging_context():
def filter_log(record):
return False if "This is expected if you are initialising" in record.msg else True
logger = datasets.utils.logging.get_logger("transformers.modeling_utils")
logger.addFilter(filter_log)
try:
yield
finally:
logger.removeFilter(filter_log)
_CITATION = """\
@article{grusky2018newsroom,
title={Newsroom: A dataset of 1.3 million summaries with diverse extractive strategies},
author={Grusky, Max and Naaman, Mor and Artzi, Yoav},
journal={arXiv preprint arXiv:1804.11283},
year={2018}
}
"""
_DESCRIPTION = """\
DataStats examines summarisation strategies using three measures that capture the degree of text overlap between the summary and article, and the rate of compression of the information conveyed.
"""
_KWARGS_DESCRIPTION = """
DataStats metric for text summarisation.
Args:
summaries (list of str): model-generated summries.
articles (list of str or list of list of str): Original articles.
Returns:
coverage: Percentage of words in the summary that are from the source article, measuring the extent to which a summary is a derivative of a text.
density: It is defined as the average length of the extractive fragment to which each summary word belongs.
compression: It is defined as the word ratio between the articles and its summaries.
Examples:
>>> predictions = ["hello there", "general kenobi"]
>>> references = ["hello there", "general kenobi"]
>>> bertscore = evaluate.load("datastats")
>>> results = bertscore.compute(predictions=predictions, references=references)
"""
def find_ngrams(input_list: List[Any], n: int):
return zip(*[input_list[i:] for i in range(n)])
def normalize(tokens: List[str], lowercase: bool = False):
"""
Lowercases and turns tokens into distinct words.
"""
return [str(t).lower() if not lowercase else str(t) for t in tokens]
class Fragments:
Match = _namedtuple("Match", ("summary", "text", "length"))
def __init__(self, summary, text, lowercase: bool = False):
if isinstance(summary, str):
self.summary = summary.split()
else:
self.summary = summary
if isinstance(text, str):
self.text = text.split()
else:
self.text = text
self._norm_summary = normalize(self.summary, lowercase)
self._norm_text = normalize(self.text, lowercase)
self._match(self._norm_summary, self._norm_text)
def overlaps(self):
"""
Return a list of Fragments.Match objects between summary and text.
This is a list of named tuples of the form (summary, text, length):
"""
return self._matches
def strings(self, min_length=0, summary_base=True):
# Compute the strings against the summary or the text?
base = self.summary if summary_base else self.text
# Generate strings, filtering out strings below the minimum length.
strings = [base[i : i + length] for i, j, length in self.overlaps() if length > min_length]
return strings
def coverage(self, summary_base=True):
"""
Return the COVERAGE score of the summary and text.
"""
numerator = sum(o.length for o in self.overlaps())
if summary_base:
denominator = len(self.summary)
else:
denominator = len(self.text)
if denominator == 0:
return 0
else:
return numerator / denominator
def density(self, summary_base=True):
"""
Return the DENSITY score of summary and text.
"""
numerator = sum(o.length ** 2 for o in self.overlaps())
if summary_base:
denominator = len(self.summary)
else:
denominator = len(self.text)
if denominator == 0:
return 0
else:
return numerator / denominator
def compression(self, text_to_summary=True):
"""
Return compression ratio between summary and text.
"""
ratio = [len(self.text), len(self.summary)]
try:
if text_to_summary:
return ratio[0] / ratio[1]
else:
return ratio[1] / ratio[0]
except ZeroDivisionError:
return 0
def _match(self, a, b):
"""
Raw procedure for matching summary in text, described in paper.
"""
self._matches = []
a_start = b_start = 0
while a_start < len(a):
best_match = None
best_match_length = 0
while b_start < len(b):
if a[a_start] == b[b_start]:
a_end = a_start
b_end = b_start
while a_end < len(a) and b_end < len(b) \
and b[b_end] == a[a_end]:
b_end += 1
a_end += 1
length = a_end - a_start
if length > best_match_length:
best_match = Fragments.Match(a_start, b_start, length)
best_match_length = length
b_start = b_end
else:
b_start += 1
b_start = 0
if best_match:
if best_match_length > 0:
self._matches.append(best_match)
a_start += best_match_length
else:
a_start += 1
class DataStatsMetric(object):
def __init__(
self,
n_gram: int = 3,
n_workers: int = 24,
lowercase: bool = False,
tokenize: bool = True
):
"""
Data Statistics metric
Args:
n_gram (int): Compute statistics for n-grams up to and including this length.
n_workers (int): Number of processes to use if using multiprocessing.
case (bool): Whether to lowercase input before calculating statistics.
tokenize (bool): Whether to tokenize the input.
"""
self.n_gram = n_gram
self.n_workers = n_workers
self.lowercase = lowercase
self.tokenize = tokenize
def evaluate_example(self, summary, input_text):
if self.tokenize:
input_text = _en(input_text, disable=["tagger", "parser", "ner", "textcat"])
input_text = [tok.text for tok in input_text]
summary = _en(summary, disable=["tagger", "parser", "ner", "textcat"])
summary = [tok.text for tok in summary]
fragments = Fragments(summary, input_text, lowercase=self.lowercase)
coverage = fragments.coverage()
density = fragments.density()
compression = fragments.compression()
score_dict = {"coverage": coverage, "density": density, "compression": compression}
tokenized_summary = fragments._norm_summary
tokenized_text = fragments._norm_text
score_dict["summary_length"] = len(tokenized_summary)
for i in range(1, self.n_gram + 1):
input_ngrams = list(find_ngrams(tokenized_text, i))
summ_ngrams = list(find_ngrams(tokenized_summary, i))
input_ngrams_set = set(input_ngrams)
summ_ngrams_set = set(summ_ngrams)
intersect = summ_ngrams_set.intersection(input_ngrams_set)
try:
score_dict[f"percentage_novel_{i}-gram"] = (len(summ_ngrams_set) \
- len(intersect))/float(len(summ_ngrams_set))
ngramCounter = Counter()
ngramCounter.update(summ_ngrams)
repeated = [key for key, val in ngramCounter.items() if val > 1]
score_dict[f"percentage_repeated_{i}-gram_in_summ"] = len(repeated)/float(len(summ_ngrams_set))
except ZeroDivisionError:
continue
return score_dict
def evaluate_batch(self, summaries, input_texts, aggregate=True):
corpus_score_dict = Counter()
p = Pool(processes=self.n_workers)
results = p.starmap(self.evaluate_example, zip(summaries, input_texts))
p.close()
if aggregate:
[corpus_score_dict.update(x) for x in results]
for key in corpus_score_dict.keys():
corpus_score_dict[key] /= float(len(input_texts))
return corpus_score_dict
else:
return results
@property
def supports_multi_ref(self):
return False
@evaluate.utils.file_utils.add_start_docstrings(_DESCRIPTION, _KWARGS_DESCRIPTION)
class DataStats(evaluate.Metric):
name = 'DataStats'
def _info(self):
return evaluate.MetricInfo(
description=_DESCRIPTION,
citation=_CITATION,
homepage="",
inputs_description=_KWARGS_DESCRIPTION,
features=[
datasets.Features(
{
"predictions": datasets.Value("string", id="sequence"),
"references": datasets.Value("string", id="sequence"),
}
),
],
codebase_urls=["https://github.com/Tiiiger/bert_score"],
reference_urls=[
"https://github.com/lil-lab/newsroom",
"https://arxiv.org/pdf/2007.12626",
],
)
def _compute(
self,
predictions,
references,
n_gram: int = 3,
n_workers: int = 4,
lowercase: bool = False,
tokenize: bool = True,
**kwargs,
):
# logger.info(predictions)
# logger.info(references)
datastats = DataStatsMetric(n_gram, n_workers, lowercase, tokenize)
results = datastats.evaluate_batch(predictions, references)
coverage = float(results['coverage'])
density = float(results['density'])
compression = float(results['compression'])
# logger.info(coverage, density, compression)
return {
"coverage": coverage,
"density": density,
"compression": compression
}