Download models/decoding/spans.py from sysy9292/kf-deberta-base-kg-extractor: direct link, hf CLI and curl.
- Browser
- Download file 12.5 kB
-
https://huggingface.co/sysy9292/kf-deberta-base-kg-extractor/resolve/main/models/decoding/spans.py
- Command line
-
hf download hf://sysy9292/kf-deberta-base-kg-extractor/models/decoding/spans.py
-
curl -L -o spans.py https://huggingface.co/sysy9292/kf-deberta-base-kg-extractor/resolve/main/models/decoding/spans.py
12.5 kB
| """trainable span Head와 분리된 BIO/boundary/constrained decoding.""" | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from itertools import product | |
| from typing import Iterable, Sequence | |
| import torch | |
| class DecodedSpan: | |
| sentence_index: int | |
| token_start: int | |
| token_end: int | |
| label: str | |
| score: float | |
| character_start: int | None = None | |
| character_end: int | None = None | |
| def boundary(self) -> tuple[int, int, int]: | |
| return self.sentence_index, self.token_start, self.token_end | |
| class BoundaryDecodeResult: | |
| """Boundary threshold를 통과한 pair 수와 deterministic 선택 결과.""" | |
| spans: tuple[DecodedSpan, ...] | |
| candidate_count: int | |
| class BIOFlatDecoder: | |
| """BIO argmax를 flat/non-overlap span으로 복원한다.""" | |
| def __init__(self, labels: Sequence[str]) -> None: | |
| if not labels or labels[0] != "O": | |
| raise ValueError("BIO labels must start with O") | |
| self.labels = tuple(labels) | |
| def decode( | |
| self, | |
| logits: torch.Tensor, | |
| token_mask: torch.BoolTensor, | |
| ) -> list[list[list[DecodedSpan]]]: | |
| if logits.ndim != 4 or token_mask.shape != logits.shape[:3]: | |
| raise ValueError("BIO decoder expects logits [B,S,T,C]") | |
| probabilities = torch.softmax(logits, dim=-1) | |
| ids = probabilities.argmax(dim=-1) | |
| output: list[list[list[DecodedSpan]]] = [] | |
| for batch_index in range(logits.shape[0]): | |
| article = [] | |
| for sentence_index in range(logits.shape[1]): | |
| spans: list[DecodedSpan] = [] | |
| active_label: str | None = None | |
| start = 0 | |
| scores: list[float] = [] | |
| valid_positions = torch.nonzero( | |
| token_mask[batch_index, sentence_index], as_tuple=False | |
| ).flatten().tolist() | |
| for offset in range(len(valid_positions) + 1): | |
| token_index = ( | |
| valid_positions[-1] + 1 | |
| if offset == len(valid_positions) and valid_positions | |
| else (0 if offset == len(valid_positions) else valid_positions[offset]) | |
| ) | |
| label = "O" if offset == len(valid_positions) else self.labels[int(ids[batch_index, sentence_index, token_index])] | |
| prefix, _, kind = label.partition("-") | |
| if active_label is not None and (prefix != "I" or kind != active_label): | |
| spans.append(DecodedSpan(sentence_index, start, token_index, active_label, sum(scores) / len(scores))) | |
| active_label, scores = None, [] | |
| if prefix == "B" or (prefix == "I" and active_label is None): | |
| active_label, start = kind, token_index | |
| scores = [float(probabilities[batch_index, sentence_index, token_index, int(ids[batch_index, sentence_index, token_index])])] | |
| elif prefix == "I" and kind == active_label: | |
| scores.append(float(probabilities[batch_index, sentence_index, token_index, int(ids[batch_index, sentence_index, token_index])])) | |
| article.append(spans) | |
| output.append(article) | |
| return output | |
| class BoundaryProposalDecoder: | |
| """독립 start/end probability에서 폭 제한 proposal을 만든다.""" | |
| def __init__(self, labels: Sequence[str], threshold: float = 0.5, max_width: int = 96) -> None: | |
| self.labels = tuple(labels) | |
| self.threshold = threshold | |
| self.max_width = max_width | |
| def decode(self, start_logits: torch.Tensor, end_logits: torch.Tensor, mask: torch.BoolTensor) -> list[DecodedSpan]: | |
| return list(self.decode_with_diagnostics(start_logits, end_logits, mask).spans) | |
| def decode_with_diagnostics( | |
| self, | |
| start_logits: torch.Tensor, | |
| end_logits: torch.Tensor, | |
| mask: torch.BoolTensor, | |
| ) -> BoundaryDecodeResult: | |
| starts = torch.sigmoid(start_logits).detach() | |
| ends = torch.sigmoid(end_logits).detach() | |
| proposals = [] | |
| for sentence in range(start_logits.shape[0]): | |
| for label_index, label in enumerate(self.labels): | |
| start_ids = torch.nonzero((starts[sentence, :, label_index] >= self.threshold) & mask[sentence], as_tuple=False).flatten().tolist() | |
| end_ids = torch.nonzero((ends[sentence, :, label_index] >= self.threshold) & mask[sentence], as_tuple=False).flatten().tolist() | |
| for start, end_inclusive in product(start_ids, end_ids): | |
| if start <= end_inclusive and end_inclusive - start + 1 <= self.max_width: | |
| score = float((starts[sentence, start, label_index] * ends[sentence, end_inclusive, label_index]).sqrt()) | |
| proposals.append(DecodedSpan(sentence, start, end_inclusive + 1, label, score)) | |
| return BoundaryDecodeResult(tuple(proposals), len(proposals)) | |
| class GreedyOneToOneBoundaryDecoder: | |
| """Confidence 순으로 start/end를 한 번씩만 쓰는 historical-style decoder. | |
| Exact duplicate는 endpoint 1:1 제약으로 제거한다. 서로 다른 endpoint를 사용하는 span의 | |
| overlap/nesting은 허용하며, label·문장별 출력 cap으로 무제한 조합을 막는다. | |
| """ | |
| def __init__( | |
| self, | |
| labels: Sequence[str], | |
| threshold: float = 0.5, | |
| max_width: int = 64, | |
| max_outputs_per_label_sentence: int = 4, | |
| ) -> None: | |
| if max_width <= 0 or max_outputs_per_label_sentence <= 0: | |
| raise ValueError("Greedy boundary decoder limits must be positive") | |
| self.labels = tuple(labels) | |
| self.threshold = threshold | |
| self.max_width = max_width | |
| self.max_outputs_per_label_sentence = max_outputs_per_label_sentence | |
| def decode( | |
| self, | |
| start_logits: torch.Tensor, | |
| end_logits: torch.Tensor, | |
| mask: torch.BoolTensor, | |
| ) -> list[DecodedSpan]: | |
| return list(self.decode_with_diagnostics(start_logits, end_logits, mask).spans) | |
| def decode_with_diagnostics( | |
| self, | |
| start_logits: torch.Tensor, | |
| end_logits: torch.Tensor, | |
| mask: torch.BoolTensor, | |
| ) -> BoundaryDecodeResult: | |
| starts = torch.sigmoid(start_logits).detach() | |
| ends = torch.sigmoid(end_logits).detach() | |
| selected: list[DecodedSpan] = [] | |
| candidate_count = 0 | |
| for sentence in range(start_logits.shape[0]): | |
| for label_index, label in enumerate(self.labels): | |
| start_ids = torch.nonzero( | |
| (starts[sentence, :, label_index] >= self.threshold) | |
| & mask[sentence], | |
| as_tuple=False, | |
| ).flatten().tolist() | |
| end_ids = torch.nonzero( | |
| (ends[sentence, :, label_index] >= self.threshold) | |
| & mask[sentence], | |
| as_tuple=False, | |
| ).flatten().tolist() | |
| candidates: list[DecodedSpan] = [] | |
| for start in start_ids: | |
| for end_inclusive in end_ids: | |
| width = end_inclusive - start + 1 | |
| if 1 <= width <= self.max_width: | |
| score = float( | |
| ( | |
| starts[sentence, start, label_index] | |
| * ends[sentence, end_inclusive, label_index] | |
| ).sqrt() | |
| ) | |
| candidates.append( | |
| DecodedSpan( | |
| sentence, | |
| start, | |
| end_inclusive + 1, | |
| label, | |
| score, | |
| ) | |
| ) | |
| candidate_count += len(candidates) | |
| used_starts: set[int] = set() | |
| used_ends: set[int] = set() | |
| for candidate in sorted( | |
| candidates, | |
| key=lambda item: ( | |
| -item.score, | |
| item.token_start, | |
| item.token_end, | |
| ), | |
| ): | |
| end_inclusive = candidate.token_end - 1 | |
| if ( | |
| candidate.token_start in used_starts | |
| or end_inclusive in used_ends | |
| ): | |
| continue | |
| selected.append(candidate) | |
| used_starts.add(candidate.token_start) | |
| used_ends.add(end_inclusive) | |
| if ( | |
| len(used_starts) | |
| >= self.max_outputs_per_label_sentence | |
| ): | |
| break | |
| return BoundaryDecodeResult(tuple(selected), candidate_count) | |
| class SemanticConstraintConfig: | |
| threshold: float = 0.5 | |
| max_outputs_per_sentence: int = 12 | |
| allow_cross_label_exact_boundary: bool = True | |
| allow_nested: bool = True | |
| containment_margin: float = 0.08 | |
| overlap_margin: float = 0.05 | |
| class SemanticConstraintDecoder: | |
| """confidence 순으로 prefix/suffix/merged proposition 중복을 제한한다.""" | |
| def __init__(self, config: SemanticConstraintConfig | None = None) -> None: | |
| self.config = config or SemanticConstraintConfig() | |
| def decode(self, candidates: Iterable[DecodedSpan]) -> list[DecodedSpan]: | |
| selected: list[DecodedSpan] = [] | |
| counts: dict[int, int] = {} | |
| for candidate in sorted(candidates, key=lambda item: (-item.score, item.token_start, item.token_end, item.label)): | |
| if candidate.score < self.config.threshold: | |
| continue | |
| if counts.get(candidate.sentence_index, 0) >= self.config.max_outputs_per_sentence: | |
| continue | |
| if any(self._conflicts(candidate, accepted) for accepted in selected): | |
| continue | |
| selected.append(candidate) | |
| counts[candidate.sentence_index] = counts.get(candidate.sentence_index, 0) + 1 | |
| return sorted(selected, key=lambda item: (item.sentence_index, item.token_start, item.token_end, item.label)) | |
| def _conflicts(self, candidate: DecodedSpan, accepted: DecodedSpan) -> bool: | |
| if candidate.sentence_index != accepted.sentence_index: | |
| return False | |
| if candidate.boundary == accepted.boundary: | |
| return not ( | |
| self.config.allow_cross_label_exact_boundary and candidate.label != accepted.label | |
| ) | |
| overlap = max(candidate.token_start, accepted.token_start) < min(candidate.token_end, accepted.token_end) | |
| if not overlap: | |
| return False | |
| candidate_contains = candidate.token_start <= accepted.token_start and accepted.token_end <= candidate.token_end | |
| accepted_contains = accepted.token_start <= candidate.token_start and candidate.token_end <= accepted.token_end | |
| if candidate_contains or accepted_contains: | |
| if self.config.allow_nested and candidate.label != accepted.label: | |
| return False | |
| return candidate.score + self.config.containment_margin < accepted.score | |
| return candidate.score <= accepted.score + self.config.overlap_margin | |
| def attach_character_offsets( | |
| spans: Iterable[DecodedSpan], | |
| token_offsets: torch.LongTensor, | |
| sentence_offsets: torch.LongTensor, | |
| ) -> list[DecodedSpan]: | |
| output = [] | |
| for span in spans: | |
| local_start = int(token_offsets[span.sentence_index, span.token_start, 0]) | |
| local_end = int(token_offsets[span.sentence_index, span.token_end - 1, 1]) | |
| article_start = int(sentence_offsets[span.sentence_index, 0]) + local_start | |
| article_end = int(sentence_offsets[span.sentence_index, 0]) + local_end | |
| output.append( | |
| DecodedSpan( | |
| span.sentence_index, | |
| span.token_start, | |
| span.token_end, | |
| span.label, | |
| span.score, | |
| article_start, | |
| article_end, | |
| ) | |
| ) | |
| return output | |