tokenization_marian.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. # Copyright 2020 The HuggingFace Team. All rights reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import json
  15. import os
  16. import warnings
  17. from pathlib import Path
  18. from shutil import copyfile
  19. from typing import Any
  20. import sentencepiece
  21. from ...tokenization_python import PreTrainedTokenizer
  22. from ...utils import logging
  23. from ...utils.import_utils import requires
  24. logger = logging.get_logger(__name__)
  25. VOCAB_FILES_NAMES = {
  26. "source_spm": "source.spm",
  27. "target_spm": "target.spm",
  28. "vocab": "vocab.json",
  29. "target_vocab_file": "target_vocab.json",
  30. "tokenizer_config_file": "tokenizer_config.json",
  31. }
  32. SPIECE_UNDERLINE = "▁"
  33. # Example URL https://huggingface.co/Helsinki-NLP/opus-mt-en-de/resolve/main/vocab.json
  34. @requires(backends=("sentencepiece",))
  35. class MarianTokenizer(PreTrainedTokenizer):
  36. r"""
  37. Construct a Marian tokenizer. Based on [SentencePiece](https://github.com/google/sentencepiece).
  38. This tokenizer inherits from [`PreTrainedTokenizer`] which contains most of the main methods. Users should refer to
  39. this superclass for more information regarding those methods.
  40. Args:
  41. source_spm (`str`):
  42. [SentencePiece](https://github.com/google/sentencepiece) file (generally has a .spm extension) that
  43. contains the vocabulary for the source language.
  44. target_spm (`str`):
  45. [SentencePiece](https://github.com/google/sentencepiece) file (generally has a .spm extension) that
  46. contains the vocabulary for the target language.
  47. source_lang (`str`, *optional*):
  48. A string representing the source language.
  49. target_lang (`str`, *optional*):
  50. A string representing the target language.
  51. unk_token (`str`, *optional*, defaults to `"<unk>"`):
  52. The unknown token. A token that is not in the vocabulary cannot be converted to an ID and is set to be this
  53. token instead.
  54. eos_token (`str`, *optional*, defaults to `"</s>"`):
  55. The end of sequence token.
  56. pad_token (`str`, *optional*, defaults to `"<pad>"`):
  57. The token used for padding, for example when batching sequences of different lengths.
  58. model_max_length (`int`, *optional*, defaults to 512):
  59. The maximum sentence length the model accepts.
  60. additional_special_tokens (`list[str]`, *optional*, defaults to `["<eop>", "<eod>"]`):
  61. Additional special tokens used by the tokenizer.
  62. sp_model_kwargs (`dict`, *optional*):
  63. Will be passed to the `SentencePieceProcessor.__init__()` method. The [Python wrapper for
  64. SentencePiece](https://github.com/google/sentencepiece/tree/master/python) can be used, among other things,
  65. to set:
  66. - `enable_sampling`: Enable subword regularization.
  67. - `nbest_size`: Sampling parameters for unigram. Invalid for BPE-Dropout.
  68. - `nbest_size = {0,1}`: No sampling is performed.
  69. - `nbest_size > 1`: samples from the nbest_size results.
  70. - `nbest_size < 0`: assuming that nbest_size is infinite and samples from the all hypothesis (lattice)
  71. using forward-filtering-and-backward-sampling algorithm.
  72. - `alpha`: Smoothing parameter for unigram sampling, and dropout probability of merge operations for
  73. BPE-dropout.
  74. Examples:
  75. ```python
  76. >>> from transformers import MarianForCausalLM, MarianTokenizer
  77. >>> model = MarianForCausalLM.from_pretrained("Helsinki-NLP/opus-mt-en-de")
  78. >>> tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
  79. >>> src_texts = ["I am a small frog.", "Tom asked his teacher for advice."]
  80. >>> tgt_texts = ["Ich bin ein kleiner Frosch.", "Tom bat seinen Lehrer um Rat."] # optional
  81. >>> inputs = tokenizer(src_texts, text_target=tgt_texts, return_tensors="pt", padding=True)
  82. >>> outputs = model(**inputs) # should work
  83. ```"""
  84. vocab_files_names = VOCAB_FILES_NAMES
  85. model_input_names = ["input_ids", "attention_mask"]
  86. def __init__(
  87. self,
  88. source_spm,
  89. target_spm,
  90. vocab,
  91. target_vocab_file=None,
  92. source_lang=None,
  93. target_lang=None,
  94. unk_token="<unk>",
  95. eos_token="</s>",
  96. pad_token="<pad>",
  97. model_max_length=512,
  98. sp_model_kwargs: dict[str, Any] | None = None,
  99. separate_vocabs=False,
  100. **kwargs,
  101. ) -> None:
  102. self.sp_model_kwargs = {} if sp_model_kwargs is None else sp_model_kwargs
  103. assert Path(source_spm).exists(), f"cannot find spm source {source_spm}"
  104. self.separate_vocabs = separate_vocabs
  105. self.encoder = load_json(vocab)
  106. if str(unk_token) not in self.encoder:
  107. raise KeyError("<unk> token must be in the vocab")
  108. if separate_vocabs:
  109. self.target_encoder = load_json(target_vocab_file)
  110. self.decoder = {v: k for k, v in self.target_encoder.items()}
  111. self.supported_language_codes = []
  112. else:
  113. self.decoder = {v: k for k, v in self.encoder.items()}
  114. self.supported_language_codes: list = [k for k in self.encoder if k.startswith(">>") and k.endswith("<<")]
  115. self.source_lang = source_lang
  116. self.target_lang = target_lang
  117. self.spm_files = [source_spm, target_spm]
  118. # load SentencePiece model for pre-processing
  119. self.spm_source = load_spm(source_spm, self.sp_model_kwargs)
  120. self.spm_target = load_spm(target_spm, self.sp_model_kwargs)
  121. self.current_spm = self.spm_source
  122. self.current_encoder = self.encoder
  123. # Multilingual target side: default to using first supported language code.
  124. self._setup_normalizer()
  125. self._decode_use_source_tokenizer = False
  126. super().__init__(
  127. # bos_token=bos_token, unused. Start decoding with config.decoder_start_token_id
  128. source_lang=source_lang,
  129. target_lang=target_lang,
  130. unk_token=unk_token,
  131. eos_token=eos_token,
  132. pad_token=pad_token,
  133. model_max_length=model_max_length,
  134. sp_model_kwargs=self.sp_model_kwargs,
  135. target_vocab_file=target_vocab_file,
  136. separate_vocabs=separate_vocabs,
  137. **kwargs,
  138. )
  139. def _setup_normalizer(self):
  140. try:
  141. from sacremoses import MosesPunctNormalizer
  142. self.punc_normalizer = MosesPunctNormalizer(self.source_lang).normalize
  143. except (ImportError, FileNotFoundError):
  144. warnings.warn("Recommended: pip install sacremoses.")
  145. self.punc_normalizer = lambda x: x
  146. def normalize(self, x: str) -> str:
  147. """Cover moses empty string edge case. They return empty list for '' input!"""
  148. return self.punc_normalizer(x) if x else ""
  149. def _convert_token_to_id(self, token):
  150. if token in self.current_encoder:
  151. return self.current_encoder[token]
  152. # The Marian vocab is not aligned with the SentencePiece IDs, so falling back to raw
  153. # SentencePiece indices would map to unrelated tokens. Treat such pieces as unknown.
  154. return self.current_encoder[self.unk_token]
  155. def remove_language_code(self, text: str):
  156. """Remove language codes like >>fr<< before sentencepiece"""
  157. code = []
  158. if text.startswith(">>") and (end_loc := text.find("<<")) != -1:
  159. code.append(text[: end_loc + 2])
  160. text = text[end_loc + 2 :]
  161. return code, text
  162. def _tokenize(self, text: str) -> list[str]:
  163. code, text = self.remove_language_code(text)
  164. pieces = self.current_spm.encode(text, out_type=str)
  165. return code + pieces
  166. def _convert_id_to_token(self, index: int) -> str:
  167. """Converts an index (integer) in a token (str) using the decoder."""
  168. if index in self.decoder:
  169. return self.decoder[index]
  170. # Fall back to SPM model for IDs not in external vocab
  171. spm_model = self.spm_source if self._decode_use_source_tokenizer else self.spm_target
  172. piece = spm_model.IdToPiece(index)
  173. return piece if piece else self.unk_token
  174. def batch_decode(self, sequences, **kwargs):
  175. """
  176. Convert a list of lists of token ids into a list of strings by calling decode.
  177. Args:
  178. sequences (`Union[list[int], list[list[int]], np.ndarray, torch.Tensor]`):
  179. List of tokenized input ids. Can be obtained using the `__call__` method.
  180. skip_special_tokens (`bool`, *optional*, defaults to `False`):
  181. Whether or not to remove special tokens in the decoding.
  182. clean_up_tokenization_spaces (`bool`, *optional*):
  183. Whether or not to clean up the tokenization spaces. If `None`, will default to
  184. `self.clean_up_tokenization_spaces` (available in the `tokenizer_config`).
  185. use_source_tokenizer (`bool`, *optional*, defaults to `False`):
  186. Whether or not to use the source tokenizer to decode sequences (only applicable in sequence-to-sequence
  187. problems).
  188. kwargs (additional keyword arguments, *optional*):
  189. Will be passed to the underlying model specific decode method.
  190. Returns:
  191. `list[str]`: The list of decoded sentences.
  192. """
  193. return super().batch_decode(sequences, **kwargs)
  194. def decode(self, token_ids, **kwargs):
  195. """
  196. Converts a sequence of ids in a string, using the tokenizer and vocabulary with options to remove special
  197. tokens and clean up tokenization spaces.
  198. Similar to doing `self.convert_tokens_to_string(self.convert_ids_to_tokens(token_ids))`.
  199. Args:
  200. token_ids (`Union[int, list[int], np.ndarray, torch.Tensor]`):
  201. List of tokenized input ids. Can be obtained using the `__call__` method.
  202. skip_special_tokens (`bool`, *optional*, defaults to `False`):
  203. Whether or not to remove special tokens in the decoding.
  204. clean_up_tokenization_spaces (`bool`, *optional*):
  205. Whether or not to clean up the tokenization spaces. If `None`, will default to
  206. `self.clean_up_tokenization_spaces` (available in the `tokenizer_config`).
  207. use_source_tokenizer (`bool`, *optional*, defaults to `False`):
  208. Whether or not to use the source tokenizer to decode sequences (only applicable in sequence-to-sequence
  209. problems).
  210. kwargs (additional keyword arguments, *optional*):
  211. Will be passed to the underlying model specific decode method.
  212. Returns:
  213. `str`: The decoded sentence.
  214. """
  215. return super().decode(token_ids, **kwargs)
  216. def _decode(
  217. self,
  218. token_ids,
  219. skip_special_tokens: bool = False,
  220. clean_up_tokenization_spaces: bool | None = None,
  221. **kwargs,
  222. ) -> str:
  223. """Internal decode method that handles use_source_tokenizer parameter."""
  224. default_use_source = not self.separate_vocabs
  225. self._decode_use_source_tokenizer = kwargs.pop("use_source_tokenizer", default_use_source)
  226. return super()._decode(
  227. token_ids=token_ids,
  228. skip_special_tokens=skip_special_tokens,
  229. clean_up_tokenization_spaces=clean_up_tokenization_spaces,
  230. **kwargs,
  231. )
  232. def convert_tokens_to_string(self, tokens: list[str]) -> str:
  233. """Uses source spm if _decode_use_source_tokenizer is True, and target spm otherwise"""
  234. sp_model = self.spm_source if self._decode_use_source_tokenizer else self.spm_target
  235. current_sub_tokens = []
  236. out_string = ""
  237. for token in tokens:
  238. # make sure that special tokens are not decoded using sentencepiece model
  239. if token in self.all_special_tokens:
  240. out_string += sp_model.decode_pieces(current_sub_tokens) + token + " "
  241. current_sub_tokens = []
  242. else:
  243. current_sub_tokens.append(token)
  244. out_string += sp_model.decode_pieces(current_sub_tokens)
  245. out_string = out_string.replace(SPIECE_UNDERLINE, " ")
  246. return out_string.strip()
  247. def build_inputs_with_special_tokens(self, token_ids_0, token_ids_1=None) -> list[int]:
  248. """Build model inputs from a sequence by appending eos_token_id."""
  249. if token_ids_1 is None:
  250. return token_ids_0 + [self.eos_token_id]
  251. # We don't expect to process pairs, but leave the pair logic for API consistency
  252. return token_ids_0 + token_ids_1 + [self.eos_token_id]
  253. def _switch_to_input_mode(self):
  254. self.current_spm = self.spm_source
  255. self.current_encoder = self.encoder
  256. def _switch_to_target_mode(self):
  257. self.current_spm = self.spm_target
  258. if self.separate_vocabs:
  259. self.current_encoder = self.target_encoder
  260. @property
  261. def vocab_size(self) -> int:
  262. return len(self.encoder)
  263. def save_vocabulary(self, save_directory: str, filename_prefix: str | None = None) -> tuple[str]:
  264. if not os.path.isdir(save_directory):
  265. logger.error(f"Vocabulary path ({save_directory}) should be a directory")
  266. return
  267. saved_files = []
  268. if self.separate_vocabs:
  269. out_src_vocab_file = os.path.join(
  270. save_directory,
  271. (filename_prefix + "-" if filename_prefix else "") + VOCAB_FILES_NAMES["vocab"],
  272. )
  273. out_tgt_vocab_file = os.path.join(
  274. save_directory,
  275. (filename_prefix + "-" if filename_prefix else "") + VOCAB_FILES_NAMES["target_vocab_file"],
  276. )
  277. save_json(self.encoder, out_src_vocab_file)
  278. save_json(self.target_encoder, out_tgt_vocab_file)
  279. saved_files.append(out_src_vocab_file)
  280. saved_files.append(out_tgt_vocab_file)
  281. else:
  282. out_vocab_file = os.path.join(
  283. save_directory, (filename_prefix + "-" if filename_prefix else "") + VOCAB_FILES_NAMES["vocab"]
  284. )
  285. save_json(self.encoder, out_vocab_file)
  286. saved_files.append(out_vocab_file)
  287. for spm_save_filename, spm_orig_path, spm_model in zip(
  288. [VOCAB_FILES_NAMES["source_spm"], VOCAB_FILES_NAMES["target_spm"]],
  289. self.spm_files,
  290. [self.spm_source, self.spm_target],
  291. ):
  292. spm_save_path = os.path.join(
  293. save_directory, (filename_prefix + "-" if filename_prefix else "") + spm_save_filename
  294. )
  295. if os.path.abspath(spm_orig_path) != os.path.abspath(spm_save_path) and os.path.isfile(spm_orig_path):
  296. copyfile(spm_orig_path, spm_save_path)
  297. saved_files.append(spm_save_path)
  298. elif not os.path.isfile(spm_orig_path):
  299. with open(spm_save_path, "wb") as fi:
  300. content_spiece_model = spm_model.serialized_model_proto()
  301. fi.write(content_spiece_model)
  302. saved_files.append(spm_save_path)
  303. return tuple(saved_files)
  304. def get_vocab(self) -> dict:
  305. return self.get_src_vocab()
  306. def get_src_vocab(self):
  307. return dict(self.encoder, **self.added_tokens_encoder)
  308. def get_tgt_vocab(self):
  309. return dict(self.target_encoder, **self.added_tokens_decoder)
  310. def __getstate__(self) -> dict:
  311. state = self.__dict__.copy()
  312. state.update(
  313. dict.fromkeys(["spm_source", "spm_target", "current_spm", "punc_normalizer", "target_vocab_file"])
  314. )
  315. return state
  316. def __setstate__(self, d: dict) -> None:
  317. self.__dict__ = d
  318. # for backward compatibility
  319. if not hasattr(self, "sp_model_kwargs"):
  320. self.sp_model_kwargs = {}
  321. if not hasattr(self, "_decode_use_source_tokenizer"):
  322. self._decode_use_source_tokenizer = False
  323. self.spm_source, self.spm_target = (load_spm(f, self.sp_model_kwargs) for f in self.spm_files)
  324. self.current_spm = self.spm_source
  325. self._setup_normalizer()
  326. def num_special_tokens_to_add(self, *args, **kwargs):
  327. """Just EOS"""
  328. return 1
  329. def _special_token_mask(self, seq):
  330. all_special_ids = set(self.all_special_ids) # call it once instead of inside list comp
  331. all_special_ids.remove(self.unk_token_id) # <unk> is only sometimes special
  332. return [1 if x in all_special_ids else 0 for x in seq]
  333. def get_special_tokens_mask(
  334. self, token_ids_0: list, token_ids_1: list | None = None, already_has_special_tokens: bool = False
  335. ) -> list[int]:
  336. """Get list where entries are [1] if a token is [eos] or [pad] else 0."""
  337. if already_has_special_tokens:
  338. return self._special_token_mask(token_ids_0)
  339. elif token_ids_1 is None:
  340. return self._special_token_mask(token_ids_0) + [1]
  341. else:
  342. return self._special_token_mask(token_ids_0 + token_ids_1) + [1]
  343. def load_spm(path: str, sp_model_kwargs: dict[str, Any]) -> sentencepiece.SentencePieceProcessor:
  344. spm = sentencepiece.SentencePieceProcessor(**sp_model_kwargs)
  345. spm.Load(path)
  346. return spm
  347. def save_json(data, path: str) -> None:
  348. with open(path, "w") as f:
  349. json.dump(data, f, indent=2)
  350. def load_json(path: str) -> dict | list:
  351. with open(path, "r") as f:
  352. return json.load(f)
  353. __all__ = ["MarianTokenizer"]