Fill-Mask
Transformers
Safetensors
English
modernlayoutlm
document-understanding
layoutlm
modernbert
custom_code
Instructions to use kkuroma/ModernLayoutLM with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use kkuroma/ModernLayoutLM with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("fill-mask", model="kkuroma/ModernLayoutLM", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForMaskedLM model = AutoModelForMaskedLM.from_pretrained("kkuroma/ModernLayoutLM", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download processing_modernlayoutlm.py from kkuroma/ModernLayoutLM: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/kkuroma/ModernLayoutLM/resolve/main/processing_modernlayoutlm.py
- Command line
-
hf download hf://kkuroma/ModernLayoutLM/processing_modernlayoutlm.py
-
curl -L -o processing_modernlayoutlm.py https://huggingface.co/kkuroma/ModernLayoutLM/resolve/main/processing_modernlayoutlm.py
10.1 kB
| """ | |
| Turning words and boxes into model inputs | |
| There is no image tower, so this only tokenizes words and broadcasts each box across its subwords | |
| This module is copied into exported model directories and may import only torch and transformers | |
| """ | |
| from __future__ import annotations | |
| from typing import Any | |
| from transformers.processing_utils import ProcessorMixin | |
| from transformers.tokenization_utils_base import BatchEncoding | |
| # Follow LayoutLM: CLS at the origin, SEP at the far corner, padding masked out | |
| CLS_BOX = (0, 0, 0, 0) | |
| SEP_BOX = (1000, 1000, 1000, 1000) | |
| PAD_BOX = (0, 0, 0, 0) | |
| # Label id meaning "do not compute loss here", the torch cross-entropy default | |
| IGNORE_INDEX = -100 | |
| class ModernLayoutLmProcessor(ProcessorMixin): | |
| """ | |
| Pairs a fast tokenizer with box and label expansion over subwords | |
| Args: | |
| tokenizer: Any fast tokenizer, so the LayoutLM baseline reuses this with its own one. | |
| coordinate_scale: The scale boxes are expected to already be normalized to. | |
| label_all_subwords: Label every subword, not only the first as published FUNSD and CORD do. | |
| """ | |
| attributes = ["tokenizer"] | |
| tokenizer_class = "AutoTokenizer" | |
| def __init__( | |
| self, | |
| tokenizer: Any, | |
| coordinate_scale: int = 1000, | |
| label_all_subwords: bool = False, | |
| ) -> None: | |
| self.coordinate_scale = coordinate_scale | |
| self.label_all_subwords = label_all_subwords | |
| super().__init__(tokenizer) | |
| # Diverge as ProcessorMixin.__call__ is typed for image and audio processors | |
| def __call__( # pyright: ignore[reportIncompatibleMethodOverride] | |
| self, | |
| words: list[str] | list[list[str]], | |
| boxes: list[list[int]] | list[list[list[int]]], | |
| word_labels: list[int] | list[list[int]] | None = None, | |
| max_length: int | None = None, | |
| padding: bool | str = "max_length", | |
| truncation: bool = True, | |
| return_overflowing_tokens: bool = False, | |
| stride: int = 0, | |
| return_tensors: str | None = None, | |
| **kwargs: Any, | |
| ) -> BatchEncoding: | |
| """ | |
| Tokenizes words and aligns boxes, and optionally labels, to the resulting subwords | |
| Accepts one document or a batch, told apart by whether `words` holds strings or lists. | |
| Special tokens receive the sentinel boxes defined in this module. | |
| With `return_overflowing_tokens` a long page becomes several chunks, each with its boxes. | |
| Args: | |
| words: One document's words, or a batch of documents' words. | |
| boxes: One (x0, y0, x1, y1) box per word on `coordinate_scale`, batched like `words`. | |
| word_labels: One class index per word, batched like `words`, or None for no labels. | |
| max_length: Truncation length, by default the tokenizer's own. | |
| padding: Passed to the tokenizer. | |
| truncation: Passed to the tokenizer. | |
| return_overflowing_tokens: Emit every chunk of an over-length page, not only the first. | |
| stride: Overlap in tokens between consecutive chunks. | |
| return_tensors: "pt" for torch tensors, None for lists. | |
| **kwargs: Forwarded to the tokenizer. | |
| Returns: | |
| A BatchEncoding of `input_ids`, `attention_mask`, `bbox`, plus `labels` if given any. | |
| Raises: | |
| ValueError: If `words` and `boxes` differ in length, or a coordinate leaves the grid. | |
| """ | |
| batched = bool(words) and isinstance(words[0], list) | |
| batch_words: list[list[str]] = words if batched else [words] # type: ignore[assignment] | |
| batch_boxes: list[list[list[int]]] = boxes if batched else [boxes] # type: ignore[assignment] | |
| batch_labels: list[list[int]] | None = None | |
| if word_labels is not None: | |
| batch_labels = word_labels if batched else [word_labels] # type: ignore[assignment] | |
| self._validate(batch_words, batch_boxes, batch_labels) | |
| encoding = self.tokenizer( | |
| text=batch_words, | |
| is_split_into_words=True, | |
| max_length=max_length, | |
| padding=padding, | |
| truncation=truncation, | |
| return_overflowing_tokens=return_overflowing_tokens, | |
| stride=stride, | |
| return_tensors=None, | |
| **kwargs, | |
| ) | |
| sample_map = encoding.get("overflow_to_sample_mapping") | |
| if sample_map is None: | |
| sample_map = list(range(len(encoding["input_ids"]))) | |
| aligned_boxes: list[list[list[int]]] = [] | |
| aligned_labels: list[list[int]] = [] | |
| for chunk_index, sample_index in enumerate(sample_map): | |
| word_ids = encoding.word_ids(chunk_index) | |
| source_boxes = batch_boxes[sample_index] | |
| aligned_boxes.append(self._expand_boxes(word_ids, source_boxes)) | |
| if batch_labels is not None: | |
| aligned_labels.append(self._expand_labels(word_ids, batch_labels[sample_index])) | |
| encoding["bbox"] = aligned_boxes | |
| if batch_labels is not None: | |
| encoding["labels"] = aligned_labels | |
| if return_tensors is not None: | |
| encoding.convert_to_tensors(tensor_type=return_tensors) | |
| return encoding | |
| def _validate( | |
| self, | |
| batch_words: list[list[str]], | |
| batch_boxes: list[list[list[int]]], | |
| batch_labels: list[list[int]] | None, | |
| ) -> None: | |
| """ | |
| Checks that the parallel inputs line up and the coordinates are in range | |
| A misalignment would otherwise train a model on wrong boxes without ever erroring. | |
| Args: | |
| batch_words: Words per document. | |
| batch_boxes: Boxes per document. | |
| batch_labels: Labels per document, or None. | |
| Raises: | |
| ValueError: On a length mismatch or an out of range coordinate. | |
| """ | |
| if len(batch_words) != len(batch_boxes): | |
| raise ValueError(f"Got {len(batch_words)} word lists but {len(batch_boxes)} box lists") | |
| if batch_labels is not None and len(batch_labels) != len(batch_words): | |
| raise ValueError( | |
| f"Got {len(batch_words)} word lists but {len(batch_labels)} label lists" | |
| ) | |
| for index, (doc_words, doc_boxes) in enumerate(zip(batch_words, batch_boxes, strict=True)): | |
| if len(doc_words) != len(doc_boxes): | |
| raise ValueError( | |
| f"Document {index} has {len(doc_words)} words but {len(doc_boxes)} boxes" | |
| ) | |
| if batch_labels is not None and len(batch_labels[index]) != len(doc_words): | |
| raise ValueError( | |
| f"Document {index} has {len(doc_words)} words " | |
| f"but {len(batch_labels[index])} labels" | |
| ) | |
| for box in doc_boxes: | |
| if len(box) != 4: | |
| raise ValueError(f"Document {index} has a box with {len(box)} coordinates") | |
| if any(not 0 <= value <= self.coordinate_scale for value in box): | |
| raise ValueError( | |
| f"Document {index} has box {box} outside " | |
| f"[0, {self.coordinate_scale}]. Normalize before calling the processor." | |
| ) | |
| def _expand_boxes( | |
| self, | |
| word_ids: list[int | None], | |
| source_boxes: list[list[int]], | |
| ) -> list[list[int]]: | |
| """ | |
| Assigns a box to every token position in one chunk | |
| Args: | |
| word_ids: Per-token index into `source_boxes`, None for special and padding tokens. | |
| source_boxes: One box per word of the source document. | |
| Returns: | |
| One box per token position. | |
| """ | |
| special_boxes = self._special_boxes(word_ids) | |
| return [ | |
| list(source_boxes[word_id]) if word_id is not None else special_boxes[position] | |
| for position, word_id in enumerate(word_ids) | |
| ] | |
| def _special_boxes(self, word_ids: list[int | None]) -> dict[int, list[int]]: | |
| """ | |
| Chooses a sentinel box for each token that came from no word | |
| Leading specials sit at the origin like CLS, and the first trailing one at the far corner. | |
| Everything past it is padding at the origin. | |
| Args: | |
| word_ids: Per-token index into the source words, None for non-word tokens. | |
| Returns: | |
| A mapping from token position to sentinel box. | |
| """ | |
| word_positions = [i for i, word_id in enumerate(word_ids) if word_id is not None] | |
| last_word = word_positions[-1] if word_positions else -1 | |
| boxes: dict[int, list[int]] = {} | |
| seen_separator = False | |
| for position, word_id in enumerate(word_ids): | |
| if word_id is not None: | |
| continue | |
| if position < last_word: | |
| boxes[position] = list(CLS_BOX) | |
| elif not seen_separator: | |
| boxes[position] = list(SEP_BOX) | |
| seen_separator = True | |
| else: | |
| boxes[position] = list(PAD_BOX) | |
| return boxes | |
| def _expand_labels( | |
| self, | |
| word_ids: list[int | None], | |
| source_labels: list[int], | |
| ) -> list[int]: | |
| """ | |
| Assigns a label to every token position in one chunk | |
| Special and padding tokens always get `IGNORE_INDEX`. | |
| Whether the later subwords of a word inherit its label follows `label_all_subwords`. | |
| Args: | |
| word_ids: Per-token index into `source_labels`, None for non-word tokens. | |
| source_labels: One class index per word of the source document. | |
| Returns: | |
| One label per token position. | |
| """ | |
| labels: list[int] = [] | |
| previous = None | |
| for word_id in word_ids: | |
| if word_id is None: | |
| labels.append(IGNORE_INDEX) | |
| elif word_id != previous or self.label_all_subwords: | |
| labels.append(source_labels[word_id]) | |
| else: | |
| labels.append(IGNORE_INDEX) | |
| previous = word_id | |
| return labels | |
| __all__ = ["ModernLayoutLmProcessor"] | |