lmspell 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lmspell/__init__.py +47 -0
- lmspell/core.py +26 -0
- lmspell/data/__init__.py +0 -0
- lmspell/data/lm_data_loader.py +43 -0
- lmspell/data/lm_spell_dataset.py +27 -0
- lmspell/exceptions.py +13 -0
- lmspell/model_enum.py +121 -0
- lmspell/models/__init__.py +0 -0
- lmspell/models/decoder_abstract.py +143 -0
- lmspell/models/encoder_abstract.py +171 -0
- lmspell/models/encoder_decoder_abstract.py +140 -0
- lmspell/models/gemma/__init__.py +0 -0
- lmspell/models/gemma/gemma2_9b_model.py +29 -0
- lmspell/models/gemma/gemma_abstract_model.py +22 -0
- lmspell/models/llama/__init__.py +0 -0
- lmspell/models/llama/llama31_8b_model.py +29 -0
- lmspell/models/llama/llama_abstract_model.py +22 -0
- lmspell/models/mbart/__init__.py +0 -0
- lmspell/models/mbart/language_codes.py +54 -0
- lmspell/models/mbart/mbart50_model.py +8 -0
- lmspell/models/mbart/mbart_abstract_model.py +39 -0
- lmspell/models/model_abstract.py +116 -0
- lmspell/models/mt5/__init__.py +0 -0
- lmspell/models/mt5/mt5_abstract_model.py +34 -0
- lmspell/models/mt5/mt5_base_model.py +8 -0
- lmspell/models/mt5/mt5_large_model.py +8 -0
- lmspell/models/sinbert/__init__.py +0 -0
- lmspell/models/sinbert/sinbert_abstract_model.py +17 -0
- lmspell/models/xlmr/__init__.py +0 -0
- lmspell/models/xlmr/xlmr_abstract_model.py +22 -0
- lmspell/models/xlmr/xlmr_large_model.py +8 -0
- lmspell/trainers/__init__.py +38 -0
- lmspell/trainers/abstract_seq_trainer.py +273 -0
- lmspell/trainers/abstract_trainer.py +12 -0
- lmspell/trainers/multi_gpu_trainer.py +122 -0
- lmspell/trainers/seq2seq_trainer.py +5 -0
- lmspell/trainers/seq_label_trainer.py +5 -0
- lmspell/trainers/unsloth_trainer.py +119 -0
- lmspell/trainers/utils/__init__.py +0 -0
- lmspell/trainers/utils/early_stopping.py +81 -0
- lmspell/trainers/utils/metrics.py +181 -0
- lmspell/utils/__init__.py +0 -0
- lmspell/utils/config_utils.py +40 -0
- lmspell/utils/dataset_utils.py +69 -0
- lmspell/utils/evaluate_utils.py +498 -0
- lmspell/utils/general_utils.py +19 -0
- lmspell/utils/google_utils.py +215 -0
- lmspell/utils/pre_post_processing_utils.py +33 -0
- lmspell-1.0.0.dist-info/METADATA +252 -0
- lmspell-1.0.0.dist-info/RECORD +52 -0
- lmspell-1.0.0.dist-info/WHEEL +4 -0
- lmspell-1.0.0.dist-info/licenses/LICENSE +395 -0
lmspell/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""LMSpell: spell correction with pre-trained language models.
|
|
2
|
+
|
|
3
|
+
Heavy dependencies (torch, transformers, unsloth) are imported lazily, so
|
|
4
|
+
``import lmspell`` stays cheap; they load on first use of ``LMSpell`` or
|
|
5
|
+
``ModelEnum``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib import import_module
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from lmspell.exceptions import (
|
|
12
|
+
EvaluateModelException,
|
|
13
|
+
InvalidModelException,
|
|
14
|
+
LMSpellException,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "1.0.0"
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from lmspell.core import LMSpell
|
|
21
|
+
from lmspell.model_enum import ModelEnum
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"LMSpell",
|
|
25
|
+
"ModelEnum",
|
|
26
|
+
"LMSpellException",
|
|
27
|
+
"InvalidModelException",
|
|
28
|
+
"EvaluateModelException",
|
|
29
|
+
"__version__",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
_LAZY_ATTRS = {
|
|
33
|
+
"LMSpell": "lmspell.core",
|
|
34
|
+
"ModelEnum": "lmspell.model_enum",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __getattr__(name):
|
|
39
|
+
if name in _LAZY_ATTRS:
|
|
40
|
+
value = getattr(import_module(_LAZY_ATTRS[name]), name)
|
|
41
|
+
globals()[name] = value
|
|
42
|
+
return value
|
|
43
|
+
raise AttributeError(f"module 'lmspell' has no attribute {name!r}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def __dir__():
|
|
47
|
+
return sorted(set(globals()) | set(__all__))
|
lmspell/core.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from lmspell.exceptions import InvalidModelException
|
|
2
|
+
from lmspell.model_enum import ModelEnum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LMSpell:
|
|
6
|
+
|
|
7
|
+
def __init__(self, model, language="Sinhala"):
|
|
8
|
+
"""
|
|
9
|
+
Create a spell corrector for a supported Hugging Face model id (see ModelEnum).
|
|
10
|
+
`language` is the language name used in the LLM prompt and for the mBART language code.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
model_enum = ModelEnum(model)
|
|
15
|
+
except ValueError:
|
|
16
|
+
raise InvalidModelException(f"Invalid model: {model}") from None
|
|
17
|
+
|
|
18
|
+
model_class = model_enum.getModelClass()
|
|
19
|
+
self._model = model_class(model_label=model_enum, language=language)
|
|
20
|
+
|
|
21
|
+
def __getattr__(self, name):
|
|
22
|
+
# Only called for attributes LMSpell does not have; guard against recursion
|
|
23
|
+
# when _model itself is missing (e.g. during unpickling or a failed __init__).
|
|
24
|
+
if name == "_model":
|
|
25
|
+
raise AttributeError(name)
|
|
26
|
+
return getattr(self._model, name)
|
lmspell/data/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from torch.utils.data import DataLoader
|
|
2
|
+
|
|
3
|
+
from lmspell.data.lm_spell_dataset import LMSpellDataset
|
|
4
|
+
from lmspell.utils.dataset_utils import DatasetUtils
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LMDataLoader:
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
tokenizer,
|
|
12
|
+
dataset = LMSpellDataset,
|
|
13
|
+
train_path = None,
|
|
14
|
+
val_path = None,
|
|
15
|
+
test_path = None,
|
|
16
|
+
dataset_size = 1,
|
|
17
|
+
train_max_length = 128,
|
|
18
|
+
test_max_length = 128,
|
|
19
|
+
train_batch_size = 16,
|
|
20
|
+
test_batch_size = 16,
|
|
21
|
+
):
|
|
22
|
+
|
|
23
|
+
self.dataset = dataset
|
|
24
|
+
self.train_dataloader = None
|
|
25
|
+
self.val_dataloader = None
|
|
26
|
+
self.test_dataloader = None
|
|
27
|
+
|
|
28
|
+
dataset = DatasetUtils(train_path=train_path, val_path=val_path, test_path=test_path, dataset_size=dataset_size)
|
|
29
|
+
|
|
30
|
+
if dataset.train_dataset is not None:
|
|
31
|
+
train_dataset = self.dataset(dataset.train_dataset, tokenizer, train_max_length)
|
|
32
|
+
train_dataloader = DataLoader(train_dataset, batch_size=train_batch_size, shuffle=False, pin_memory=True)
|
|
33
|
+
self.train_dataloader = train_dataloader
|
|
34
|
+
|
|
35
|
+
if dataset.val_dataset is not None:
|
|
36
|
+
val_dataset = self.dataset(dataset.val_dataset, tokenizer, train_max_length)
|
|
37
|
+
val_dataloader = DataLoader(val_dataset, batch_size=train_batch_size, shuffle=False, pin_memory=True)
|
|
38
|
+
self.val_dataloader = val_dataloader
|
|
39
|
+
|
|
40
|
+
if dataset.test_dataset is not None:
|
|
41
|
+
test_dataset = self.dataset(dataset.test_dataset, tokenizer, test_max_length)
|
|
42
|
+
test_dataloader = DataLoader(test_dataset, batch_size=test_batch_size, shuffle=False, pin_memory=True)
|
|
43
|
+
self.test_dataloader = test_dataloader
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from torch.utils.data import Dataset
|
|
3
|
+
|
|
4
|
+
from lmspell.utils.config_utils import ConfigUtils
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LMSpellDataset(Dataset):
|
|
8
|
+
def __init__(self, dataframe, tokenizer, max_length=128):
|
|
9
|
+
if not isinstance(dataframe, pd.DataFrame):
|
|
10
|
+
dataframe = pd.DataFrame(dataframe)
|
|
11
|
+
self.dataframe = dataframe
|
|
12
|
+
self.tokenizer = tokenizer
|
|
13
|
+
self.max_length = max_length
|
|
14
|
+
|
|
15
|
+
def __len__(self):
|
|
16
|
+
return len(self.dataframe)
|
|
17
|
+
|
|
18
|
+
def __getitem__(self, idx):
|
|
19
|
+
item = self.dataframe.iloc[idx]
|
|
20
|
+
dataset_columns = ConfigUtils.get_dataset_columns()
|
|
21
|
+
inputs = self.tokenizer(item[dataset_columns[0]], return_tensors='pt', padding='max_length', truncation=True, max_length=self.max_length)
|
|
22
|
+
targets = self.tokenizer(item[dataset_columns[1]], return_tensors='pt', padding='max_length', truncation=True, max_length=self.max_length)
|
|
23
|
+
inputs['labels'] = targets['input_ids']
|
|
24
|
+
return {key: val.squeeze() for key, val in inputs.items()}
|
|
25
|
+
|
|
26
|
+
def get(self):
|
|
27
|
+
return len(self.dataframe)
|
lmspell/exceptions.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Exceptions raised by LMSpell."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LMSpellException(Exception):
|
|
5
|
+
"""Base class for all LMSpell errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InvalidModelException(LMSpellException):
|
|
9
|
+
"""Raised when an unsupported model name is requested."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EvaluateModelException(LMSpellException):
|
|
13
|
+
"""Raised when evaluation of a model fails."""
|
lmspell/model_enum.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Dict, Type
|
|
3
|
+
|
|
4
|
+
from lmspell.models.model_abstract import ModelAbstract
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ModelEnum(Enum):
|
|
8
|
+
"""
|
|
9
|
+
Enum class for all the models supported by the
|
|
10
|
+
Neural Spell Checker Library
|
|
11
|
+
"""
|
|
12
|
+
# Fine-tuned models
|
|
13
|
+
# These checkpoints will be released upon the acceptance of the paper
|
|
14
|
+
|
|
15
|
+
# Llama 3.1 models
|
|
16
|
+
LLAMA_31_8B = "unsloth/Meta-Llama-3.1-8B-bnb-4bit"
|
|
17
|
+
LLAMA_31_8B_INSTRUCT = "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit"
|
|
18
|
+
|
|
19
|
+
# Llama 3.2 models
|
|
20
|
+
LLAMA_32_1B = "unsloth/Llama-3.2-1B-bnb-4bit"
|
|
21
|
+
LLAMA_32_1B_INSTRUCT = "unsloth/Llama-3.2-1B-Instruct-bnb-4bit"
|
|
22
|
+
LLAMA_32_3B = "unsloth/Llama-3.2-3B-bnb-4bit"
|
|
23
|
+
LLAMA_32_3B_INSTRUCT = "unsloth/Llama-3.2-3B-Instruct-bnb-4bit"
|
|
24
|
+
|
|
25
|
+
# Gemma 2 models
|
|
26
|
+
GEMMA_2_2B = "unsloth/gemma-2-2b-bnb-4bit"
|
|
27
|
+
GEMMA_2_2B_INSTRUCT = "unsloth/gemma-2-2b-it-bnb-4bit"
|
|
28
|
+
GEMMA_2_9B = "unsloth/gemma-2-9b-bnb-4bit"
|
|
29
|
+
GEMMA_2_9B_INSTRUCT = "unsloth/gemma-2-9b-it-bnb-4bit"
|
|
30
|
+
|
|
31
|
+
# MT5 models
|
|
32
|
+
MT5_SMALL = "google/mt5-small"
|
|
33
|
+
MT5_BASE = "google/mt5-base"
|
|
34
|
+
MT5_LARGE = "google/mt5-large"
|
|
35
|
+
MT5_XL = "google/mt5-xl"
|
|
36
|
+
MT5_XXL = "google/mt5-xxl"
|
|
37
|
+
|
|
38
|
+
# MBART models
|
|
39
|
+
MBART50 = "facebook/mbart-large-50"
|
|
40
|
+
|
|
41
|
+
# SinBERT models
|
|
42
|
+
SINBERT_SMALL = "NLPC-UOM/SinBERT-small"
|
|
43
|
+
SINBERT_LARGE = "NLPC-UOM/SinBERT-large"
|
|
44
|
+
|
|
45
|
+
# XLMR models
|
|
46
|
+
XLMR_BASE = "FacebookAI/xlm-roberta-base"
|
|
47
|
+
XLMR_LARGE = "FacebookAI/xlm-roberta-large"
|
|
48
|
+
|
|
49
|
+
def getModelClass(self) -> Type[ModelAbstract]:
|
|
50
|
+
"""Return the model class associated with the enum value"""
|
|
51
|
+
|
|
52
|
+
from lmspell.models.gemma.gemma2_9b_model import Gemma29BModel
|
|
53
|
+
from lmspell.models.gemma.gemma_abstract_model import GemmaAbstractModel
|
|
54
|
+
from lmspell.models.llama.llama31_8b_model import Llama318BModel
|
|
55
|
+
from lmspell.models.llama.llama_abstract_model import LlamaAbstractModel
|
|
56
|
+
from lmspell.models.mbart.mbart50_model import Mbart50Model
|
|
57
|
+
from lmspell.models.mt5.mt5_abstract_model import Mt5AbstractModel
|
|
58
|
+
from lmspell.models.mt5.mt5_base_model import Mt5BaseModel
|
|
59
|
+
from lmspell.models.mt5.mt5_large_model import Mt5LargeModel
|
|
60
|
+
from lmspell.models.sinbert.sinbert_abstract_model import SinBertAbstractModel
|
|
61
|
+
from lmspell.models.xlmr.xlmr_abstract_model import XlmrAbstractModel
|
|
62
|
+
from lmspell.models.xlmr.xlmr_large_model import XlmrLargeModel
|
|
63
|
+
|
|
64
|
+
# import other model classes as needed
|
|
65
|
+
|
|
66
|
+
model_classes: Dict[ModelEnum, Type[ModelAbstract]] = {
|
|
67
|
+
# Fine-tuned models
|
|
68
|
+
# ModelEnum.LLAMA31_FT_SI: LlamaAbstractModel,
|
|
69
|
+
# ModelEnum.GEMMA2_FT_SI: GemmaAbstractModel,
|
|
70
|
+
# ModelEnum.MT5_FT_SI: Mt5AbstractModel,
|
|
71
|
+
# ModelEnum.MT5_XL_FT_SI: Mt5AbstractModel,
|
|
72
|
+
# ModelEnum.MBART_FT_SI: MbartAbstractModel,
|
|
73
|
+
# ModelEnum.SINBERT_FT_SI: SinBertAbstractModel,
|
|
74
|
+
# ModelEnum.XLMR_FT_SI: XlmrAbstractModel,
|
|
75
|
+
|
|
76
|
+
# Llama 3.1 models
|
|
77
|
+
ModelEnum.LLAMA_31_8B: Llama318BModel,
|
|
78
|
+
ModelEnum.LLAMA_31_8B_INSTRUCT: LlamaAbstractModel,
|
|
79
|
+
|
|
80
|
+
# Llama 3.2 models
|
|
81
|
+
ModelEnum.LLAMA_32_1B: LlamaAbstractModel,
|
|
82
|
+
ModelEnum.LLAMA_32_1B_INSTRUCT: LlamaAbstractModel,
|
|
83
|
+
ModelEnum.LLAMA_32_3B: LlamaAbstractModel,
|
|
84
|
+
ModelEnum.LLAMA_32_3B_INSTRUCT: LlamaAbstractModel,
|
|
85
|
+
|
|
86
|
+
# Gemma 2 models
|
|
87
|
+
ModelEnum.GEMMA_2_2B: GemmaAbstractModel,
|
|
88
|
+
ModelEnum.GEMMA_2_2B_INSTRUCT: GemmaAbstractModel,
|
|
89
|
+
ModelEnum.GEMMA_2_9B: Gemma29BModel,
|
|
90
|
+
ModelEnum.GEMMA_2_9B_INSTRUCT: GemmaAbstractModel,
|
|
91
|
+
|
|
92
|
+
# mT5 models
|
|
93
|
+
ModelEnum.MT5_SMALL: Mt5AbstractModel,
|
|
94
|
+
ModelEnum.MT5_BASE: Mt5BaseModel,
|
|
95
|
+
ModelEnum.MT5_LARGE: Mt5LargeModel,
|
|
96
|
+
ModelEnum.MT5_XL: Mt5AbstractModel,
|
|
97
|
+
ModelEnum.MT5_XXL: Mt5AbstractModel,
|
|
98
|
+
|
|
99
|
+
# mBART models
|
|
100
|
+
ModelEnum.MBART50: Mbart50Model,
|
|
101
|
+
|
|
102
|
+
# SinBERT models
|
|
103
|
+
ModelEnum.SINBERT_SMALL: SinBertAbstractModel,
|
|
104
|
+
ModelEnum.SINBERT_LARGE: SinBertAbstractModel,
|
|
105
|
+
|
|
106
|
+
# XLMR models
|
|
107
|
+
ModelEnum.XLMR_BASE: XlmrAbstractModel,
|
|
108
|
+
ModelEnum.XLMR_LARGE: XlmrLargeModel,
|
|
109
|
+
|
|
110
|
+
# Add other model-class mappings here
|
|
111
|
+
}
|
|
112
|
+
return model_classes[self]
|
|
113
|
+
|
|
114
|
+
def get_model_path(self) -> str:
|
|
115
|
+
"""Return the model path"""
|
|
116
|
+
return self.value
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def get_supported_models():
|
|
120
|
+
"""Return a list of all supported model names"""
|
|
121
|
+
return [model.value for model in ModelEnum]
|
|
File without changes
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from lmspell.exceptions import LMSpellException
|
|
4
|
+
from lmspell.models.model_abstract import ModelAbstract
|
|
5
|
+
from lmspell.utils.config_utils import ConfigUtils
|
|
6
|
+
from lmspell.utils.evaluate_utils import EvaluateUtils
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DecoderAbstract(ModelAbstract):
|
|
10
|
+
do_sample: bool
|
|
11
|
+
|
|
12
|
+
def __init__(self, model_label=None, language="Sinhala"):
|
|
13
|
+
import torch
|
|
14
|
+
from unsloth import FastLanguageModel
|
|
15
|
+
|
|
16
|
+
super().__init__(model_label, language)
|
|
17
|
+
|
|
18
|
+
# Models mapped straight to LlamaAbstractModel/GemmaAbstractModel do not set do_sample;
|
|
19
|
+
# use the same default as the concrete Llama318BModel/Gemma29BModel.
|
|
20
|
+
if not hasattr(self, "do_sample"):
|
|
21
|
+
self.do_sample = True
|
|
22
|
+
|
|
23
|
+
self.max_seq_length = 2048 # WHY!!!
|
|
24
|
+
is_t4 = torch.cuda.is_available() and torch.cuda.get_device_name(0).startswith('Tesla T4')
|
|
25
|
+
self.dtype = torch.float16 if is_t4 else torch.bfloat16
|
|
26
|
+
self.load_in_4bit = True # Get as a parameter
|
|
27
|
+
self.attn_implementation = "flash_attention_2"
|
|
28
|
+
|
|
29
|
+
model, tokenizer = FastLanguageModel.from_pretrained(
|
|
30
|
+
model_name = self.model_label.get_model_path(),
|
|
31
|
+
max_seq_length = self.max_seq_length,
|
|
32
|
+
dtype = self.dtype,
|
|
33
|
+
load_in_4bit = self.load_in_4bit,
|
|
34
|
+
attn_implementation=self.attn_implementation,
|
|
35
|
+
random_state = self.seed,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Need to check pros and cons of loading it like this for both training and inference.
|
|
39
|
+
model = FastLanguageModel.get_peft_model(
|
|
40
|
+
model,
|
|
41
|
+
r = 8, # 8, 16, 32, 64, 128 suggested
|
|
42
|
+
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj","gate_proj", "up_proj", "down_proj"],
|
|
43
|
+
lora_alpha = 16,
|
|
44
|
+
lora_dropout = 0,
|
|
45
|
+
bias = "none",
|
|
46
|
+
use_gradient_checkpointing = "unsloth",
|
|
47
|
+
random_state = self.seed,
|
|
48
|
+
use_rslora = False,
|
|
49
|
+
loftq_config = None,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.model = model
|
|
53
|
+
self.tokenizer = tokenizer
|
|
54
|
+
|
|
55
|
+
# pylint: disable-next=line-too-long
|
|
56
|
+
self.PROMPT = f"""You are an expert {self.language} spell corrector. Below is a sentence in {self.language} language. It may or may not have a spelling mistake. Give the corrected output in {self.language}.
|
|
57
|
+
|
|
58
|
+
### Text:
|
|
59
|
+
{{}}
|
|
60
|
+
|
|
61
|
+
### Output:
|
|
62
|
+
{{}}"""
|
|
63
|
+
|
|
64
|
+
def correct(self, input_set: list[str] | str | pd.DataFrame, target_set: list[str] | str | pd.DataFrame = None, output_dir: str = 'outputs'):
|
|
65
|
+
"""
|
|
66
|
+
Corrects the text. The input set should be a string, a list of strings or a DataFrame.
|
|
67
|
+
Perform the evaluation if the target set is provided.
|
|
68
|
+
"""
|
|
69
|
+
import os
|
|
70
|
+
import re
|
|
71
|
+
|
|
72
|
+
from tqdm import tqdm
|
|
73
|
+
from unsloth import FastLanguageModel
|
|
74
|
+
|
|
75
|
+
os.makedirs(os.path.join(os.getcwd(), output_dir), exist_ok=True)
|
|
76
|
+
|
|
77
|
+
input_set, evaluate_flag = self.process_input(input_set, target_set)
|
|
78
|
+
|
|
79
|
+
dataset_col_names = ConfigUtils.get_dataset_columns()
|
|
80
|
+
|
|
81
|
+
FastLanguageModel.for_inference(self.model)
|
|
82
|
+
pred_texts = []
|
|
83
|
+
generation_mode = self.model.generation_config.get_generation_mode()
|
|
84
|
+
print(f"Generation mode: {generation_mode}")
|
|
85
|
+
|
|
86
|
+
for src in tqdm(input_set[dataset_col_names[0]].to_list(), desc="Generating predictions"):
|
|
87
|
+
inputs = self.tokenizer(
|
|
88
|
+
[self.PROMPT.format(src, "")],
|
|
89
|
+
return_tensors="pt"
|
|
90
|
+
).to(self.device)
|
|
91
|
+
|
|
92
|
+
outputs = self.model.generate(**inputs, max_new_tokens=self.max_seq_length, do_sample=self.do_sample, use_cache=True)
|
|
93
|
+
|
|
94
|
+
pred_text = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
|
|
95
|
+
|
|
96
|
+
pred_texts.append(pred_text)
|
|
97
|
+
|
|
98
|
+
output_texts = []
|
|
99
|
+
for pred in pred_texts:
|
|
100
|
+
match = re.search(r'### Output:\s*([^\n]+)', pred)
|
|
101
|
+
output_texts.append(match.group(1).strip() if match else None)
|
|
102
|
+
|
|
103
|
+
result_col_names = ConfigUtils.get_results_columns()
|
|
104
|
+
|
|
105
|
+
results_data = {
|
|
106
|
+
result_col_names[0]: input_set[dataset_col_names[0]].to_list(),
|
|
107
|
+
result_col_names[1]: output_texts
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
results_df = pd.DataFrame(results_data)
|
|
111
|
+
|
|
112
|
+
if evaluate_flag:
|
|
113
|
+
results_df[result_col_names[2]] = input_set[dataset_col_names[1]].to_list()
|
|
114
|
+
print("Evaluating the outputs...")
|
|
115
|
+
EvaluateUtils.evaluate_from_dataframe(results_df, output_dir)
|
|
116
|
+
|
|
117
|
+
return results_df
|
|
118
|
+
|
|
119
|
+
def correctFromFile(self, src: str, target: str = None, output_dir: str = "outputs"):
|
|
120
|
+
"""
|
|
121
|
+
Corrects the text from a file. The file should be in the format of
|
|
122
|
+
a CSV or TXT file. If the file is a CSV, it should have two columns: 'text' and 'expected'.
|
|
123
|
+
If the file is a TXT, it should have one line per sentence. If target is provided,
|
|
124
|
+
it will be used for evaluation. The output will be saved in the specified output directory.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
if src.endswith('.csv'):
|
|
128
|
+
df = pd.read_csv(src)
|
|
129
|
+
input_col, target_col = ConfigUtils.get_dataset_columns()
|
|
130
|
+
src = df[input_col].tolist()
|
|
131
|
+
if target_col in df.columns:
|
|
132
|
+
target = df[target_col].tolist()
|
|
133
|
+
|
|
134
|
+
elif src.endswith('.txt'):
|
|
135
|
+
with open(src, 'r', encoding='utf-8') as file:
|
|
136
|
+
src = file.readlines()
|
|
137
|
+
if target is not None:
|
|
138
|
+
with open(target, 'r', encoding='utf-8') as file:
|
|
139
|
+
target = file.readlines()
|
|
140
|
+
else:
|
|
141
|
+
raise LMSpellException("Unsupported file format. Only .csv and .txt are supported.") from None
|
|
142
|
+
|
|
143
|
+
return self.correct(src, target, output_dir)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import torch
|
|
6
|
+
from torch.utils.data import DataLoader
|
|
7
|
+
from tqdm import tqdm
|
|
8
|
+
|
|
9
|
+
from lmspell.data.lm_spell_dataset import LMSpellDataset
|
|
10
|
+
from lmspell.exceptions import LMSpellException
|
|
11
|
+
from lmspell.models.model_abstract import ModelAbstract
|
|
12
|
+
from lmspell.utils.config_utils import ConfigUtils
|
|
13
|
+
from lmspell.utils.evaluate_utils import EvaluateUtils
|
|
14
|
+
from lmspell.utils.general_utils import GeneralUtils
|
|
15
|
+
from lmspell.utils.pre_post_processing_utils import PrePostProcessingUtils
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EncoderAbstract(ModelAbstract):
|
|
19
|
+
accelerator: Any
|
|
20
|
+
|
|
21
|
+
def correct(self, input_set: list[str] | str | pd.DataFrame, target_set: list[str] | str | pd.DataFrame = None,
|
|
22
|
+
output_dir: str = "outputs", max_length: int = 128, batch_size: int = 8,
|
|
23
|
+
shuffle: bool = False): # pylint: disable=unused-argument # kept for API parity with EncoderDecoderAbstract
|
|
24
|
+
"""
|
|
25
|
+
Corrects the text using encoder-only models (e.g., XLM-R). Supports string, list, or DataFrame as input.
|
|
26
|
+
If target_set is provided, performs evaluation and saves results.
|
|
27
|
+
"""
|
|
28
|
+
os.makedirs(os.path.join(os.getcwd(), output_dir), exist_ok=True)
|
|
29
|
+
|
|
30
|
+
if max_length > 514:
|
|
31
|
+
print("Warning: max_length is set to 514, as it is the maximum length for XLMR models.")
|
|
32
|
+
max_length = 514
|
|
33
|
+
|
|
34
|
+
input_set, evaluate_flag = self.process_input(input_set, target_set)
|
|
35
|
+
|
|
36
|
+
dataset = LMSpellDataset(input_set, self.tokenizer, max_length)
|
|
37
|
+
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False, pin_memory=True)
|
|
38
|
+
|
|
39
|
+
self.model = self.accelerator.prepare_model(self.model, evaluation_mode=True)
|
|
40
|
+
dataloader = self.accelerator.prepare_data_loader(dataloader)
|
|
41
|
+
self.model.resize_token_embeddings(len(self.tokenizer))
|
|
42
|
+
GeneralUtils.clean_memory()
|
|
43
|
+
|
|
44
|
+
self.model.eval()
|
|
45
|
+
originals, predictions, labels = [], [], []
|
|
46
|
+
|
|
47
|
+
with tqdm(dataloader, leave=True, disable=not self.accelerator.is_local_main_process) as pbar:
|
|
48
|
+
for batch in pbar:
|
|
49
|
+
with torch.no_grad():
|
|
50
|
+
outputs = self.model(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"], labels=batch["labels"])
|
|
51
|
+
|
|
52
|
+
pred = torch.argmax(outputs.logits, dim=-1)
|
|
53
|
+
orig = batch["input_ids"]
|
|
54
|
+
label = batch["labels"]
|
|
55
|
+
|
|
56
|
+
gathered_orig, gathered_pred, gathered_label = self.accelerator.gather_for_metrics((orig, pred, label))
|
|
57
|
+
self.accelerator.wait_for_everyone()
|
|
58
|
+
|
|
59
|
+
originals.extend(gathered_orig)
|
|
60
|
+
predictions.extend(gathered_pred)
|
|
61
|
+
labels.extend(gathered_label)
|
|
62
|
+
|
|
63
|
+
results_df = self.decode(originals, predictions, labels, output_dir)
|
|
64
|
+
|
|
65
|
+
if evaluate_flag and self.accelerator.is_main_process:
|
|
66
|
+
eval_df = results_df.iloc[:, :2].copy()
|
|
67
|
+
if target_set is None:
|
|
68
|
+
# DataFrame input that carries its own target column.
|
|
69
|
+
target_col = input_set[ConfigUtils.get_dataset_columns()[1]]
|
|
70
|
+
elif isinstance(target_set, pd.DataFrame):
|
|
71
|
+
target_col = target_set.iloc[:, 0] if target_set.shape[1] > 0 else pd.Series([])
|
|
72
|
+
elif isinstance(target_set, list):
|
|
73
|
+
target_col = pd.Series(target_set)
|
|
74
|
+
elif isinstance(target_set, str):
|
|
75
|
+
target_col = pd.Series([target_set])
|
|
76
|
+
else:
|
|
77
|
+
raise LMSpellException("Cannot evaluate: target_set must be a string, a list of strings or a DataFrame.")
|
|
78
|
+
|
|
79
|
+
eval_df[ConfigUtils.get_results_columns()[2]] = target_col.reset_index(drop=True)
|
|
80
|
+
|
|
81
|
+
print("Evaluating the outputs...")
|
|
82
|
+
EvaluateUtils.evaluate_from_dataframe(eval_df, output_dir)
|
|
83
|
+
|
|
84
|
+
return results_df
|
|
85
|
+
|
|
86
|
+
def decode(self, originals, predictions, labels, output_dir):
|
|
87
|
+
"""
|
|
88
|
+
Decode a tensor of tensors and save it to a file.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
skip_special_tokens = False
|
|
92
|
+
|
|
93
|
+
# This is different in mbart model. Same for xlmr, sinbert and mt5. We don't need to do this in large models.
|
|
94
|
+
special_token_id_to_keep = self.tokenizer.convert_tokens_to_ids(PrePostProcessingUtils.ZWJ)
|
|
95
|
+
all_special_ids = torch.tensor(self.tokenizer.all_special_ids, dtype=torch.int64).to(self.device)
|
|
96
|
+
|
|
97
|
+
special_token_tensor = torch.tensor([special_token_id_to_keep], dtype=torch.int64).to(self.device)
|
|
98
|
+
|
|
99
|
+
originals, predictions, labels = [
|
|
100
|
+
[PrePostProcessingUtils.remove_special_tokens(tokens.to(self.device), all_special_ids, special_token_tensor) for tokens in data]
|
|
101
|
+
for data in (originals, predictions, labels)
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
originals_decoded, predictions_decoded, labels_decoded = [
|
|
105
|
+
[self.tokenizer.decode(ids, skip_special_tokens=skip_special_tokens).replace('\n', '').strip() for ids in data]
|
|
106
|
+
for data in (originals, predictions, labels)
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
result_col_names = ConfigUtils.get_results_columns()
|
|
110
|
+
|
|
111
|
+
results_data = {
|
|
112
|
+
result_col_names[0]: originals_decoded,
|
|
113
|
+
result_col_names[1]: predictions_decoded,
|
|
114
|
+
result_col_names[2]: labels_decoded,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Write CSV inside the main process condition
|
|
118
|
+
results_df = pd.DataFrame(results_data)
|
|
119
|
+
|
|
120
|
+
# When using BERT models, these character were present in the output. They throws errors when saving in excel format
|
|
121
|
+
results_df = results_df.apply(lambda col: col.map(lambda x: x.replace('.\uffff', '').replace('\x11', '') if isinstance(x, str) else x))
|
|
122
|
+
|
|
123
|
+
print("Replacing ZWJ token")
|
|
124
|
+
for column in result_col_names:
|
|
125
|
+
results_df[column] = PrePostProcessingUtils.clean_zwj(results_df[column])
|
|
126
|
+
|
|
127
|
+
PrePostProcessingUtils.save_dataframe(results_df, output_dir)
|
|
128
|
+
|
|
129
|
+
return results_df
|
|
130
|
+
|
|
131
|
+
def correctFromFile(self, src: str, target: str = None, output_dir: str = "outputs", max_length: int = 128,
|
|
132
|
+
batch_size: int = 8, shuffle: bool = False):
|
|
133
|
+
"""
|
|
134
|
+
Corrects the text from a file. The file should be in the format of
|
|
135
|
+
a CSV or TXT file. If the file is a CSV, it should have two columns: 'text' and 'expected'.
|
|
136
|
+
If the file is a TXT, it should have one line per sentence. If target is provided,
|
|
137
|
+
it will be used for evaluation. The output will be saved in the specified output directory.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
if src.endswith('.csv'):
|
|
141
|
+
df = pd.read_csv(src)
|
|
142
|
+
input_col, target_col = ConfigUtils.get_dataset_columns()
|
|
143
|
+
src = df[input_col].tolist()
|
|
144
|
+
if target_col in df.columns:
|
|
145
|
+
target = df[target_col].tolist()
|
|
146
|
+
|
|
147
|
+
elif src.endswith('.txt'):
|
|
148
|
+
with open(src, 'r', encoding='utf-8') as file:
|
|
149
|
+
src = file.readlines()
|
|
150
|
+
if target is not None:
|
|
151
|
+
with open(target, 'r', encoding='utf-8') as file:
|
|
152
|
+
target = file.readlines()
|
|
153
|
+
else:
|
|
154
|
+
raise LMSpellException("Unsupported file format. Only .csv and .txt are supported.") from None
|
|
155
|
+
|
|
156
|
+
return self.correct(src, target, output_dir, max_length, batch_size, shuffle)
|
|
157
|
+
|
|
158
|
+
def load_model(self):
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
def save_model(self):
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
def get_model_name(self):
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def load_tokenizer(self):
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
def save_tokenizer(self):
|
|
171
|
+
pass
|