| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- # -*- coding: utf-8 -*-
- # Natural Language Toolkit: Machine Translation
- #
- # Copyright (C) 2001-2020 NLTK Project
- # Author: Uday Krishna <udaykrishna5@gmail.com>
- # URL: <http://nltk.org/>
- # For license information, see LICENSE.TXT
- from nltk.stem.porter import PorterStemmer
- from nltk.corpus import wordnet
- from itertools import chain, product
- def _generate_enums(hypothesis, reference, preprocess=str.lower):
- """
- Takes in string inputs for hypothesis and reference and returns
- enumerated word lists for each of them
- :param hypothesis: hypothesis string
- :type hypothesis: str
- :param reference: reference string
- :type reference: str
- :preprocess: preprocessing method (default str.lower)
- :type preprocess: method
- :return: enumerated words list
- :rtype: list of 2D tuples, list of 2D tuples
- """
- hypothesis_list = list(enumerate(preprocess(hypothesis).split()))
- reference_list = list(enumerate(preprocess(reference).split()))
- return hypothesis_list, reference_list
- def exact_match(hypothesis, reference):
- """
- matches exact words in hypothesis and reference
- and returns a word mapping based on the enumerated
- word id between hypothesis and reference
- :param hypothesis: hypothesis string
- :type hypothesis: str
- :param reference: reference string
- :type reference: str
- :return: enumerated matched tuples, enumerated unmatched hypothesis tuples,
- enumerated unmatched reference tuples
- :rtype: list of 2D tuples, list of 2D tuples, list of 2D tuples
- """
- hypothesis_list, reference_list = _generate_enums(hypothesis, reference)
- return _match_enums(hypothesis_list, reference_list)
- def _match_enums(enum_hypothesis_list, enum_reference_list):
- """
- matches exact words in hypothesis and reference and returns
- a word mapping between enum_hypothesis_list and enum_reference_list
- based on the enumerated word id.
- :param enum_hypothesis_list: enumerated hypothesis list
- :type enum_hypothesis_list: list of tuples
- :param enum_reference_list: enumerated reference list
- :type enum_reference_list: list of 2D tuples
- :return: enumerated matched tuples, enumerated unmatched hypothesis tuples,
- enumerated unmatched reference tuples
- :rtype: list of 2D tuples, list of 2D tuples, list of 2D tuples
- """
- word_match = []
- for i in range(len(enum_hypothesis_list))[::-1]:
- for j in range(len(enum_reference_list))[::-1]:
- if enum_hypothesis_list[i][1] == enum_reference_list[j][1]:
- word_match.append(
- (enum_hypothesis_list[i][0], enum_reference_list[j][0])
- )
- (enum_hypothesis_list.pop(i)[1], enum_reference_list.pop(j)[1])
- break
- return word_match, enum_hypothesis_list, enum_reference_list
- def _enum_stem_match(
- enum_hypothesis_list, enum_reference_list, stemmer=PorterStemmer()
- ):
- """
- Stems each word and matches them in hypothesis and reference
- and returns a word mapping between enum_hypothesis_list and
- enum_reference_list based on the enumerated word id. The function also
- returns a enumerated list of unmatched words for hypothesis and reference.
- :param enum_hypothesis_list:
- :type enum_hypothesis_list:
- :param enum_reference_list:
- :type enum_reference_list:
- :param stemmer: nltk.stem.api.StemmerI object (default PorterStemmer())
- :type stemmer: nltk.stem.api.StemmerI or any class that implements a stem method
- :return: enumerated matched tuples, enumerated unmatched hypothesis tuples,
- enumerated unmatched reference tuples
- :rtype: list of 2D tuples, list of 2D tuples, list of 2D tuples
- """
- stemmed_enum_list1 = [
- (word_pair[0], stemmer.stem(word_pair[1])) for word_pair in enum_hypothesis_list
- ]
- stemmed_enum_list2 = [
- (word_pair[0], stemmer.stem(word_pair[1])) for word_pair in enum_reference_list
- ]
- word_match, enum_unmat_hypo_list, enum_unmat_ref_list = _match_enums(
- stemmed_enum_list1, stemmed_enum_list2
- )
- enum_unmat_hypo_list = (
- list(zip(*enum_unmat_hypo_list)) if len(enum_unmat_hypo_list) > 0 else []
- )
- enum_unmat_ref_list = (
- list(zip(*enum_unmat_ref_list)) if len(enum_unmat_ref_list) > 0 else []
- )
- enum_hypothesis_list = list(
- filter(lambda x: x[0] not in enum_unmat_hypo_list, enum_hypothesis_list)
- )
- enum_reference_list = list(
- filter(lambda x: x[0] not in enum_unmat_ref_list, enum_reference_list)
- )
- return word_match, enum_hypothesis_list, enum_reference_list
- def stem_match(hypothesis, reference, stemmer=PorterStemmer()):
- """
- Stems each word and matches them in hypothesis and reference
- and returns a word mapping between hypothesis and reference
- :param hypothesis:
- :type hypothesis:
- :param reference:
- :type reference:
- :param stemmer: nltk.stem.api.StemmerI object (default PorterStemmer())
- :type stemmer: nltk.stem.api.StemmerI or any class that
- implements a stem method
- :return: enumerated matched tuples, enumerated unmatched hypothesis tuples,
- enumerated unmatched reference tuples
- :rtype: list of 2D tuples, list of 2D tuples, list of 2D tuples
- """
- enum_hypothesis_list, enum_reference_list = _generate_enums(hypothesis, reference)
- return _enum_stem_match(enum_hypothesis_list, enum_reference_list, stemmer=stemmer)
- def _enum_wordnetsyn_match(enum_hypothesis_list, enum_reference_list, wordnet=wordnet):
- """
- Matches each word in reference to a word in hypothesis
- if any synonym of a hypothesis word is the exact match
- to the reference word.
- :param enum_hypothesis_list: enumerated hypothesis list
- :param enum_reference_list: enumerated reference list
- :param wordnet: a wordnet corpus reader object (default nltk.corpus.wordnet)
- :type wordnet: WordNetCorpusReader
- :return: list of matched tuples, unmatched hypothesis list, unmatched reference list
- :rtype: list of tuples, list of tuples, list of tuples
- """
- word_match = []
- for i in range(len(enum_hypothesis_list))[::-1]:
- hypothesis_syns = set(
- chain(
- *[
- [
- lemma.name()
- for lemma in synset.lemmas()
- if lemma.name().find("_") < 0
- ]
- for synset in wordnet.synsets(enum_hypothesis_list[i][1])
- ]
- )
- ).union({enum_hypothesis_list[i][1]})
- for j in range(len(enum_reference_list))[::-1]:
- if enum_reference_list[j][1] in hypothesis_syns:
- word_match.append(
- (enum_hypothesis_list[i][0], enum_reference_list[j][0])
- )
- enum_hypothesis_list.pop(i), enum_reference_list.pop(j)
- break
- return word_match, enum_hypothesis_list, enum_reference_list
- def wordnetsyn_match(hypothesis, reference, wordnet=wordnet):
- """
- Matches each word in reference to a word in hypothesis if any synonym
- of a hypothesis word is the exact match to the reference word.
- :param hypothesis: hypothesis string
- :param reference: reference string
- :param wordnet: a wordnet corpus reader object (default nltk.corpus.wordnet)
- :type wordnet: WordNetCorpusReader
- :return: list of mapped tuples
- :rtype: list of tuples
- """
- enum_hypothesis_list, enum_reference_list = _generate_enums(hypothesis, reference)
- return _enum_wordnetsyn_match(
- enum_hypothesis_list, enum_reference_list, wordnet=wordnet
- )
- def _enum_allign_words(
- enum_hypothesis_list, enum_reference_list, stemmer=PorterStemmer(), wordnet=wordnet
- ):
- """
- Aligns/matches words in the hypothesis to reference by sequentially
- applying exact match, stemmed match and wordnet based synonym match.
- in case there are multiple matches the match which has the least number
- of crossing is chosen. Takes enumerated list as input instead of
- string input
- :param enum_hypothesis_list: enumerated hypothesis list
- :param enum_reference_list: enumerated reference list
- :param stemmer: nltk.stem.api.StemmerI object (default PorterStemmer())
- :type stemmer: nltk.stem.api.StemmerI or any class that implements a stem method
- :param wordnet: a wordnet corpus reader object (default nltk.corpus.wordnet)
- :type wordnet: WordNetCorpusReader
- :return: sorted list of matched tuples, unmatched hypothesis list,
- unmatched reference list
- :rtype: list of tuples, list of tuples, list of tuples
- """
- exact_matches, enum_hypothesis_list, enum_reference_list = _match_enums(
- enum_hypothesis_list, enum_reference_list
- )
- stem_matches, enum_hypothesis_list, enum_reference_list = _enum_stem_match(
- enum_hypothesis_list, enum_reference_list, stemmer=stemmer
- )
- wns_matches, enum_hypothesis_list, enum_reference_list = _enum_wordnetsyn_match(
- enum_hypothesis_list, enum_reference_list, wordnet=wordnet
- )
- return (
- sorted(
- exact_matches + stem_matches + wns_matches, key=lambda wordpair: wordpair[0]
- ),
- enum_hypothesis_list,
- enum_reference_list,
- )
- def allign_words(hypothesis, reference, stemmer=PorterStemmer(), wordnet=wordnet):
- """
- Aligns/matches words in the hypothesis to reference by sequentially
- applying exact match, stemmed match and wordnet based synonym match.
- In case there are multiple matches the match which has the least number
- of crossing is chosen.
- :param hypothesis: hypothesis string
- :param reference: reference string
- :param stemmer: nltk.stem.api.StemmerI object (default PorterStemmer())
- :type stemmer: nltk.stem.api.StemmerI or any class that implements a stem method
- :param wordnet: a wordnet corpus reader object (default nltk.corpus.wordnet)
- :type wordnet: WordNetCorpusReader
- :return: sorted list of matched tuples, unmatched hypothesis list, unmatched reference list
- :rtype: list of tuples, list of tuples, list of tuples
- """
- enum_hypothesis_list, enum_reference_list = _generate_enums(hypothesis, reference)
- return _enum_allign_words(
- enum_hypothesis_list, enum_reference_list, stemmer=stemmer, wordnet=wordnet
- )
- def _count_chunks(matches):
- """
- Counts the fewest possible number of chunks such that matched unigrams
- of each chunk are adjacent to each other. This is used to caluclate the
- fragmentation part of the metric.
- :param matches: list containing a mapping of matched words (output of allign_words)
- :return: Number of chunks a sentence is divided into post allignment
- :rtype: int
- """
- i = 0
- chunks = 1
- while i < len(matches) - 1:
- if (matches[i + 1][0] == matches[i][0] + 1) and (
- matches[i + 1][1] == matches[i][1] + 1
- ):
- i += 1
- continue
- i += 1
- chunks += 1
- return chunks
- def single_meteor_score(
- reference,
- hypothesis,
- preprocess=str.lower,
- stemmer=PorterStemmer(),
- wordnet=wordnet,
- alpha=0.9,
- beta=3,
- gamma=0.5,
- ):
- """
- Calculates METEOR score for single hypothesis and reference as per
- "Meteor: An Automatic Metric for MT Evaluation with HighLevels of
- Correlation with Human Judgments" by Alon Lavie and Abhaya Agarwal,
- in Proceedings of ACL.
- http://www.cs.cmu.edu/~alavie/METEOR/pdf/Lavie-Agarwal-2007-METEOR.pdf
- >>> hypothesis1 = 'It is a guide to action which ensures that the military always obeys the commands of the party'
- >>> reference1 = 'It is a guide to action that ensures that the military will forever heed Party commands'
- >>> round(single_meteor_score(reference1, hypothesis1),4)
- 0.7398
- If there is no words match during the alignment the method returns the
- score as 0. We can safely return a zero instead of raising a
- division by zero error as no match usually implies a bad translation.
- >>> round(meteor_score('this is a cat', 'non matching hypothesis'),4)
- 0.0
- :param references: reference sentences
- :type references: list(str)
- :param hypothesis: a hypothesis sentence
- :type hypothesis: str
- :param preprocess: preprocessing function (default str.lower)
- :type preprocess: method
- :param stemmer: nltk.stem.api.StemmerI object (default PorterStemmer())
- :type stemmer: nltk.stem.api.StemmerI or any class that implements a stem method
- :param wordnet: a wordnet corpus reader object (default nltk.corpus.wordnet)
- :type wordnet: WordNetCorpusReader
- :param alpha: parameter for controlling relative weights of precision and recall.
- :type alpha: float
- :param beta: parameter for controlling shape of penalty as a
- function of as a function of fragmentation.
- :type beta: float
- :param gamma: relative weight assigned to fragmentation penality.
- :type gamma: float
- :return: The sentence-level METEOR score.
- :rtype: float
- """
- enum_hypothesis, enum_reference = _generate_enums(
- hypothesis, reference, preprocess=preprocess
- )
- translation_length = len(enum_hypothesis)
- reference_length = len(enum_reference)
- matches, _, _ = _enum_allign_words(enum_hypothesis, enum_reference, stemmer=stemmer)
- matches_count = len(matches)
- try:
- precision = float(matches_count) / translation_length
- recall = float(matches_count) / reference_length
- fmean = (precision * recall) / (alpha * precision + (1 - alpha) * recall)
- chunk_count = float(_count_chunks(matches))
- frag_frac = chunk_count / matches_count
- except ZeroDivisionError:
- return 0.0
- penalty = gamma * frag_frac ** beta
- return (1 - penalty) * fmean
- def meteor_score(
- references,
- hypothesis,
- preprocess=str.lower,
- stemmer=PorterStemmer(),
- wordnet=wordnet,
- alpha=0.9,
- beta=3,
- gamma=0.5,
- ):
- """
- Calculates METEOR score for hypothesis with multiple references as
- described in "Meteor: An Automatic Metric for MT Evaluation with
- HighLevels of Correlation with Human Judgments" by Alon Lavie and
- Abhaya Agarwal, in Proceedings of ACL.
- http://www.cs.cmu.edu/~alavie/METEOR/pdf/Lavie-Agarwal-2007-METEOR.pdf
- In case of multiple references the best score is chosen. This method
- iterates over single_meteor_score and picks the best pair among all
- the references for a given hypothesis
- >>> hypothesis1 = 'It is a guide to action which ensures that the military always obeys the commands of the party'
- >>> hypothesis2 = 'It is to insure the troops forever hearing the activity guidebook that party direct'
- >>> reference1 = 'It is a guide to action that ensures that the military will forever heed Party commands'
- >>> reference2 = 'It is the guiding principle which guarantees the military forces always being under the command of the Party'
- >>> reference3 = 'It is the practical guide for the army always to heed the directions of the party'
- >>> round(meteor_score([reference1, reference2, reference3], hypothesis1),4)
- 0.7398
- If there is no words match during the alignment the method returns the
- score as 0. We can safely return a zero instead of raising a
- division by zero error as no match usually implies a bad translation.
- >>> round(meteor_score(['this is a cat'], 'non matching hypothesis'),4)
- 0.0
- :param references: reference sentences
- :type references: list(str)
- :param hypothesis: a hypothesis sentence
- :type hypothesis: str
- :param preprocess: preprocessing function (default str.lower)
- :type preprocess: method
- :param stemmer: nltk.stem.api.StemmerI object (default PorterStemmer())
- :type stemmer: nltk.stem.api.StemmerI or any class that implements a stem method
- :param wordnet: a wordnet corpus reader object (default nltk.corpus.wordnet)
- :type wordnet: WordNetCorpusReader
- :param alpha: parameter for controlling relative weights of precision and recall.
- :type alpha: float
- :param beta: parameter for controlling shape of penalty as a function
- of as a function of fragmentation.
- :type beta: float
- :param gamma: relative weight assigned to fragmentation penality.
- :type gamma: float
- :return: The sentence-level METEOR score.
- :rtype: float
- """
- return max(
- [
- single_meteor_score(
- reference,
- hypothesis,
- stemmer=stemmer,
- wordnet=wordnet,
- alpha=alpha,
- beta=beta,
- gamma=gamma,
- )
- for reference in references
- ]
- )
|