textslinger 0.1.0__tar.gz
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.
- textslinger-0.1.0/LICENSE.md +21 -0
- textslinger-0.1.0/PKG-INFO +84 -0
- textslinger-0.1.0/README.md +17 -0
- textslinger-0.1.0/pyproject.toml +73 -0
- textslinger-0.1.0/setup.cfg +4 -0
- textslinger-0.1.0/textslinger/__init__.py +0 -0
- textslinger-0.1.0/textslinger/causal.py +437 -0
- textslinger-0.1.0/textslinger/classifier.py +227 -0
- textslinger-0.1.0/textslinger/exceptions.py +25 -0
- textslinger-0.1.0/textslinger/language_model.py +52 -0
- textslinger-0.1.0/textslinger/mixture.py +126 -0
- textslinger-0.1.0/textslinger/ngram.py +110 -0
- textslinger-0.1.0/textslinger/uniform.py +65 -0
- textslinger-0.1.0/textslinger.egg-info/PKG-INFO +84 -0
- textslinger-0.1.0/textslinger.egg-info/SOURCES.txt +17 -0
- textslinger-0.1.0/textslinger.egg-info/dependency_links.txt +1 -0
- textslinger-0.1.0/textslinger.egg-info/requires.txt +23 -0
- textslinger-0.1.0/textslinger.egg-info/top_level.txt +3 -0
- textslinger-0.1.0/textslinger.egg-info/zip-safe +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Keith Vertanen, Dylan Gaines
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: textslinger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TextSlinger text prediction toolkit.
|
|
5
|
+
Author-email: Dylan Gaines <dcgaines@mtu.edu>, Keith Vertanen <vertanen@mtu.edu>, CAMBI <cambi_support@googlegroups.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Keith Vertanen, Dylan Gaines
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://www.cambi.tech/
|
|
28
|
+
Project-URL: Source, https://github.com/kdv123/textpredict
|
|
29
|
+
Platform: Linux
|
|
30
|
+
Platform: Windows
|
|
31
|
+
Platform: Mac OS-X
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
34
|
+
Classifier: Natural Language :: English
|
|
35
|
+
Classifier: Intended Audience :: Science/Research
|
|
36
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
37
|
+
Classifier: Intended Audience :: Developers
|
|
38
|
+
Classifier: Programming Language :: Python
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Requires-Python: <3.11,>=3.9
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE.md
|
|
44
|
+
Requires-Dist: torch==2.2.0
|
|
45
|
+
Requires-Dist: torchvision==0.17.0
|
|
46
|
+
Requires-Dist: torchaudio==2.2.0
|
|
47
|
+
Requires-Dist: datasets==2.0.0
|
|
48
|
+
Requires-Dist: bitsandbytes==0.42.0
|
|
49
|
+
Requires-Dist: requests==2.32.3
|
|
50
|
+
Requires-Dist: kenlm==0.1
|
|
51
|
+
Requires-Dist: nlpaug==1.1.11
|
|
52
|
+
Requires-Dist: psutil==5.7.2
|
|
53
|
+
Requires-Dist: ipywidgets==8.1.3
|
|
54
|
+
Requires-Dist: sentencepiece==0.2.0
|
|
55
|
+
Requires-Dist: protobuf==4.25.3
|
|
56
|
+
Requires-Dist: evaluate==0.4.0
|
|
57
|
+
Requires-Dist: scikit-learn==1.2.2
|
|
58
|
+
Requires-Dist: accelerate==0.33.0
|
|
59
|
+
Requires-Dist: transformers==4.47.0
|
|
60
|
+
Requires-Dist: numpy==1.24.4
|
|
61
|
+
Requires-Dist: tqdm==4.62.2
|
|
62
|
+
Provides-Extra: release
|
|
63
|
+
Requires-Dist: twine==5.0.0; extra == "release"
|
|
64
|
+
Requires-Dist: build==1.2.2.post1; extra == "release"
|
|
65
|
+
Requires-Dist: wheel==0.43.0; extra == "release"
|
|
66
|
+
Dynamic: license-file
|
|
67
|
+
|
|
68
|
+
Python library for making text predictions using a language model.
|
|
69
|
+
|
|
70
|
+
**** Setting up a Python environment ****
|
|
71
|
+
These instructions are the same as those in the classify-aac which actually does training of models.
|
|
72
|
+
But the same environment should also work for evaluation using the models (though you may only need a subset of the packages).
|
|
73
|
+
This was tested on cheetah on 9/14/24.
|
|
74
|
+
|
|
75
|
+
If you don't have anaconda installed in your user account you'll first need to do that.
|
|
76
|
+
See: https://docs.anaconda.com/anaconda/install/linux/
|
|
77
|
+
|
|
78
|
+
% conda create -n aacllm python=3.10
|
|
79
|
+
% conda activate aacllm
|
|
80
|
+
% conda install pytorch torchvision torchaudio pytorch-cuda cuda mpi4py -c pytorch -c nvidia
|
|
81
|
+
% pip install 'git+https://github.com/potamides/uniformers.git#egg=uniformers'
|
|
82
|
+
% pip install --upgrade transformers
|
|
83
|
+
% pip install kenlm==0.1 --global-option="--max_order=12"
|
|
84
|
+
% pip install rbloom bitsandbytes requests nlpaug ipywidgets psutil datasets sentencepiece protobuf evaluate scikit-learn deepspeed accelerate
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Python library for making text predictions using a language model.
|
|
2
|
+
|
|
3
|
+
**** Setting up a Python environment ****
|
|
4
|
+
These instructions are the same as those in the classify-aac which actually does training of models.
|
|
5
|
+
But the same environment should also work for evaluation using the models (though you may only need a subset of the packages).
|
|
6
|
+
This was tested on cheetah on 9/14/24.
|
|
7
|
+
|
|
8
|
+
If you don't have anaconda installed in your user account you'll first need to do that.
|
|
9
|
+
See: https://docs.anaconda.com/anaconda/install/linux/
|
|
10
|
+
|
|
11
|
+
% conda create -n aacllm python=3.10
|
|
12
|
+
% conda activate aacllm
|
|
13
|
+
% conda install pytorch torchvision torchaudio pytorch-cuda cuda mpi4py -c pytorch -c nvidia
|
|
14
|
+
% pip install 'git+https://github.com/potamides/uniformers.git#egg=uniformers'
|
|
15
|
+
% pip install --upgrade transformers
|
|
16
|
+
% pip install kenlm==0.1 --global-option="--max_order=12"
|
|
17
|
+
% pip install rbloom bitsandbytes requests nlpaug ipywidgets psutil datasets sentencepiece protobuf evaluate scikit-learn deepspeed accelerate
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "setuptools.build_meta"
|
|
3
|
+
requires = ["setuptools>=61.0", "wheel>=0.37.1"]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "textslinger"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{name="Dylan Gaines", email="dcgaines@mtu.edu"},
|
|
10
|
+
{name="Keith Vertanen", email="vertanen@mtu.edu"},
|
|
11
|
+
{name="CAMBI", email="cambi_support@googlegroups.com"},
|
|
12
|
+
]
|
|
13
|
+
description = "TextSlinger text prediction toolkit."
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
requires-python = ">=3.9,<3.11"
|
|
16
|
+
classifiers = [
|
|
17
|
+
'License :: OSI Approved :: MIT License',
|
|
18
|
+
'Topic :: Scientific/Engineering :: Human Machine Interfaces',
|
|
19
|
+
'Natural Language :: English',
|
|
20
|
+
'Intended Audience :: Science/Research',
|
|
21
|
+
'Intended Audience :: End Users/Desktop',
|
|
22
|
+
'Intended Audience :: Developers',
|
|
23
|
+
'Programming Language :: Python',
|
|
24
|
+
'Programming Language :: Python :: 3.9',
|
|
25
|
+
'Programming Language :: Python :: 3.10',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
dependencies = [
|
|
29
|
+
"torch==2.2.0",
|
|
30
|
+
"torchvision==0.17.0",
|
|
31
|
+
"torchaudio==2.2.0",
|
|
32
|
+
"datasets==2.0.0",
|
|
33
|
+
"bitsandbytes==0.42.0",
|
|
34
|
+
"requests==2.32.3",
|
|
35
|
+
"kenlm==0.1",
|
|
36
|
+
"nlpaug==1.1.11",
|
|
37
|
+
"psutil==5.7.2",
|
|
38
|
+
"ipywidgets==8.1.3",
|
|
39
|
+
"sentencepiece==0.2.0",
|
|
40
|
+
"protobuf==4.25.3",
|
|
41
|
+
"evaluate==0.4.0",
|
|
42
|
+
"scikit-learn==1.2.2",
|
|
43
|
+
"accelerate==0.33.0",
|
|
44
|
+
"transformers==4.47.0",
|
|
45
|
+
"numpy==1.24.4",
|
|
46
|
+
"tqdm==4.62.2",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.license]
|
|
50
|
+
file = "LICENSE.md"
|
|
51
|
+
license = "MIT"
|
|
52
|
+
|
|
53
|
+
[project.optional-dependencies]
|
|
54
|
+
release = [
|
|
55
|
+
"twine==5.0.0",
|
|
56
|
+
"build==1.2.2.post1",
|
|
57
|
+
"wheel==0.43.0",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[project.urls]
|
|
61
|
+
"Homepage" = "https://www.cambi.tech/"
|
|
62
|
+
"Source" = "https://github.com/kdv123/textpredict"
|
|
63
|
+
|
|
64
|
+
[tool.setuptools]
|
|
65
|
+
# https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
|
|
66
|
+
platforms = ["Linux", "Windows", "Mac OS-X"]
|
|
67
|
+
include-package-data = true
|
|
68
|
+
zip-safe = true
|
|
69
|
+
|
|
70
|
+
# Python modules and packages that are included in the
|
|
71
|
+
# distribution package (and therefore become importable)
|
|
72
|
+
[tool.setuptools.packages.find]
|
|
73
|
+
exclude = ["examples", "scripts"]
|
|
File without changes
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
4
|
+
import itertools
|
|
5
|
+
import heapq
|
|
6
|
+
from textslinger.language_model import LanguageModel, DEFAULT_SYMBOL_SET
|
|
7
|
+
from textslinger.exceptions import InvalidLanguageModelException
|
|
8
|
+
from scipy.special import logsumexp
|
|
9
|
+
from scipy.special import softmax
|
|
10
|
+
import time
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from typing import Final
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CausalLanguageModel(LanguageModel):
|
|
16
|
+
"""Character language model based on a pre-trained causal model, GPT-2 by default."""
|
|
17
|
+
|
|
18
|
+
def __init__(self,
|
|
19
|
+
symbol_set: List[str],
|
|
20
|
+
lang_model_name: str,
|
|
21
|
+
lm_path: str = None,
|
|
22
|
+
lm_device: str = "cpu",
|
|
23
|
+
lm_left_context: str = "",
|
|
24
|
+
beam_width: int = None,
|
|
25
|
+
fp16: bool = False,
|
|
26
|
+
mixed_case_context: bool = False,
|
|
27
|
+
case_simple: bool = False,
|
|
28
|
+
max_completed: int = None,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize instance variables and load the language model with given path
|
|
32
|
+
Args:
|
|
33
|
+
symbol_set - list of symbol strings
|
|
34
|
+
lang_model_name - name of the Hugging Face casual language model to load
|
|
35
|
+
lm_path - load fine-tuned model from specified directory
|
|
36
|
+
lm_device - device to use for making predictions (cpu, mps, or cuda)
|
|
37
|
+
lm_left_context - text to condition start of sentence on
|
|
38
|
+
beam_width - how many hypotheses to keep during the search, None=off
|
|
39
|
+
fp16 - convert model to fp16 to save memory/compute on CUDA
|
|
40
|
+
mixed_case_context - use mixed case for language model left context
|
|
41
|
+
case_simple - simple fixing of left context case
|
|
42
|
+
max_completed - stop search once we reach this many completed hypotheses, None=don't prune
|
|
43
|
+
"""
|
|
44
|
+
super().__init__(symbol_set=symbol_set)
|
|
45
|
+
self.model = None
|
|
46
|
+
self.tokenizer = None
|
|
47
|
+
self.vocab_size = 0
|
|
48
|
+
self.valid_vocab = []
|
|
49
|
+
self.vocab = defaultdict(list)
|
|
50
|
+
# Since subword token ids are integers, use a list instead of a dictionary
|
|
51
|
+
self.index_to_word = []
|
|
52
|
+
self.index_to_word_lower = []
|
|
53
|
+
self.symbol_set_lower = None
|
|
54
|
+
self.device = lm_device
|
|
55
|
+
self.left_context = lm_left_context
|
|
56
|
+
self.fp16 = fp16
|
|
57
|
+
self.mixed_case_context = mixed_case_context
|
|
58
|
+
self.case_simple = case_simple
|
|
59
|
+
self.max_completed = max_completed
|
|
60
|
+
|
|
61
|
+
if not max_completed and not beam_width:
|
|
62
|
+
print(f"WARNING: using causal language model without any pruning, this can be slow!")
|
|
63
|
+
else:
|
|
64
|
+
print(f"Causal language model, beam_width {beam_width}, max_completed {max_completed}")
|
|
65
|
+
|
|
66
|
+
# We optionally load the model from a local directory, but if this is not
|
|
67
|
+
# specified, we load a Hugging Face model
|
|
68
|
+
self.model_name = lang_model_name
|
|
69
|
+
self.model_dir = lm_path if lm_path else self.model_name
|
|
70
|
+
|
|
71
|
+
# Parameters for the search
|
|
72
|
+
self.beam_width = beam_width
|
|
73
|
+
|
|
74
|
+
# Simple heuristic to correct case in the LM context
|
|
75
|
+
self.simple_upper_words = {"i": "I",
|
|
76
|
+
"i'll": "I'll",
|
|
77
|
+
"i've": "I've",
|
|
78
|
+
"i'd": "I'd",
|
|
79
|
+
"i'm": "I'm"}
|
|
80
|
+
|
|
81
|
+
# Track how much time spent in different parts of the predict function
|
|
82
|
+
self.predict_total_ns = 0
|
|
83
|
+
self.predict_inference_ns = 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Are we a model that automatically inserts a start token that we need to get rid of
|
|
87
|
+
self.drop_first_token = False
|
|
88
|
+
|
|
89
|
+
self.load()
|
|
90
|
+
|
|
91
|
+
def _build_vocab(self) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Build a vocabulary table mapping token index to word strings
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# Loop over all the subword tokens in the LLM
|
|
97
|
+
for i in range(self.vocab_size):
|
|
98
|
+
# Create a map from the subword token integer ID to the mixed and lowercase string versions
|
|
99
|
+
word = self.tokenizer.decode([i])
|
|
100
|
+
word_lower = word.lower()
|
|
101
|
+
self.index_to_word += word,
|
|
102
|
+
self.index_to_word_lower += word_lower,
|
|
103
|
+
|
|
104
|
+
# Check if all the characters in the subword token are in our valid symbol set
|
|
105
|
+
valid = True
|
|
106
|
+
for ch in word_lower:
|
|
107
|
+
if ch == ' ':
|
|
108
|
+
continue
|
|
109
|
+
elif ch not in self.symbol_set_lower:
|
|
110
|
+
valid = False
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
# If the subword token symbols are all valid, then add it to the list of valid token IDs
|
|
114
|
+
if valid:
|
|
115
|
+
self.valid_vocab += i,
|
|
116
|
+
# Add this token ID to all lists for its valid text prefixes
|
|
117
|
+
for j in range(len(word)):
|
|
118
|
+
key = word_lower[0:j + 1]
|
|
119
|
+
self.vocab[key] += i,
|
|
120
|
+
|
|
121
|
+
# When done, self.vocab can be used to map to possible following subword tokens given some text, e.g.:
|
|
122
|
+
# self.vocab["cyclo"] = [47495, 49484]
|
|
123
|
+
# self.index_to_word[self.vocab["cyclo"][0]] = cyclop
|
|
124
|
+
# self.index_to_word[self.vocab["cyclo"][1]] = cyclopedia
|
|
125
|
+
|
|
126
|
+
(self.model_name.startswith("facebook/opt") or self.model_name.startswith("figmtu/opt") or "Llama-3.1" in self.model_name)
|
|
127
|
+
|
|
128
|
+
# Get the index we use for the start or end pseudo-word
|
|
129
|
+
if self.left_context == "":
|
|
130
|
+
if "gpt2" in self.model_name:
|
|
131
|
+
self.left_context = "<|endoftext|>"
|
|
132
|
+
elif "Llama-3.1" in self.model_name:
|
|
133
|
+
self.left_context = "<|begin_of_text|>"
|
|
134
|
+
# Seems to have both sentence start and end tokens: https://docs.mistral.ai/guides/tokenization/
|
|
135
|
+
elif "Mistral" in self.model_name:
|
|
136
|
+
self.left_context = "<s>"
|
|
137
|
+
else:
|
|
138
|
+
self.left_context = "</s>"
|
|
139
|
+
|
|
140
|
+
# OPT, Llama and Mistral all insert start token
|
|
141
|
+
self.drop_first_token = (self.model_name.startswith("facebook/opt") or
|
|
142
|
+
self.model_name.startswith("figmtu/opt") or
|
|
143
|
+
"Llama-3.1" in self.model_name or
|
|
144
|
+
"Mistral" in self.model_name)
|
|
145
|
+
|
|
146
|
+
# Get token id(s) for the left context we condition all sentences on
|
|
147
|
+
self.left_context_tokens = self._encode(self.left_context)
|
|
148
|
+
print(f"Causal: left_context = '{self.left_context}', left_context_tokens = {self.left_context_tokens}")
|
|
149
|
+
|
|
150
|
+
def _encode(self, text: str) -> List[int]:
|
|
151
|
+
tokens = self.tokenizer.encode(text)
|
|
152
|
+
# Both OPT and Llama automatically insert a start token which we want to control ourselves
|
|
153
|
+
if len(tokens) > 1 and self.drop_first_token:
|
|
154
|
+
tokens = tokens[1:]
|
|
155
|
+
|
|
156
|
+
return tokens
|
|
157
|
+
|
|
158
|
+
def _sequence_string(self, sequence: List[int]) -> str:
|
|
159
|
+
"""
|
|
160
|
+
Convert a sequence of subword token IDs into a string with each token in ()'s
|
|
161
|
+
:param sequence: List of subword token IDs
|
|
162
|
+
:return: String
|
|
163
|
+
"""
|
|
164
|
+
return "".join([f"({self.index_to_word[x]})" for x in sequence])
|
|
165
|
+
|
|
166
|
+
def get_all_tokens_text(self):
|
|
167
|
+
"""
|
|
168
|
+
Return an array with the text of all subword tokens.
|
|
169
|
+
The array is in order by the integer index into the vocabulary.
|
|
170
|
+
This is mostly just for exploring the tokens in different LLMs.
|
|
171
|
+
:return: Array of subword token text strings.
|
|
172
|
+
"""
|
|
173
|
+
result = []
|
|
174
|
+
for i in range(self.vocab_size):
|
|
175
|
+
result.append(self.tokenizer.decode([i]))
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
def predict(self, evidence: List[str]) -> List[Tuple]:
|
|
179
|
+
"""
|
|
180
|
+
Given an evidence of typed string, predict the probability distribution of
|
|
181
|
+
the next symbol
|
|
182
|
+
Args:
|
|
183
|
+
evidence - a list of characters (typed by the user)
|
|
184
|
+
Response:
|
|
185
|
+
A list of symbols with probability
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
assert self.model is not None, "language model does not exist!"
|
|
189
|
+
start_ns = time.time_ns()
|
|
190
|
+
|
|
191
|
+
context = "".join(evidence)
|
|
192
|
+
|
|
193
|
+
# If using the simple case feature, we need to go through the actual
|
|
194
|
+
# left context and capitalize the first letter in the sentence as
|
|
195
|
+
# well as any word in our list of words that should be capitalized.
|
|
196
|
+
if self.case_simple and len(context) > 0:
|
|
197
|
+
cased_context = ""
|
|
198
|
+
words = context.split()
|
|
199
|
+
for i, word in enumerate(words):
|
|
200
|
+
if i == 0 and word[0] >= 'a' and word[0] <= 'z':
|
|
201
|
+
word = word[0].upper() + word[1:]
|
|
202
|
+
if i > 0:
|
|
203
|
+
if word in self.simple_upper_words:
|
|
204
|
+
word = self.simple_upper_words[word]
|
|
205
|
+
cased_context += " "
|
|
206
|
+
cased_context += word
|
|
207
|
+
# Handle ending space in the context
|
|
208
|
+
if context[-1] == ' ':
|
|
209
|
+
cased_context += " "
|
|
210
|
+
context = cased_context
|
|
211
|
+
|
|
212
|
+
context_lower = context.lower()
|
|
213
|
+
|
|
214
|
+
# Index in the hypothesis string that is the next character after our context
|
|
215
|
+
target_pos = len(context_lower)
|
|
216
|
+
|
|
217
|
+
# For stats purposes track length of the prefix we are extending from space to match
|
|
218
|
+
# prefix_len = target_pos
|
|
219
|
+
|
|
220
|
+
# Look for the last space in the context, or -1 if no begin_text in context yet
|
|
221
|
+
pos = context_lower.rfind(" ")
|
|
222
|
+
tokens = []
|
|
223
|
+
tokens.extend(self.left_context_tokens)
|
|
224
|
+
if pos >= 0:
|
|
225
|
+
# Optionally, we condition on upper and lower case left context
|
|
226
|
+
if self.mixed_case_context:
|
|
227
|
+
truncated_context = context[0:pos]
|
|
228
|
+
else:
|
|
229
|
+
truncated_context = context_lower[0:pos]
|
|
230
|
+
tokens.extend(self._encode(truncated_context))
|
|
231
|
+
#prefix_len -= pos
|
|
232
|
+
|
|
233
|
+
#print(f"DEBUG, {context_lower} pos {pos}, prefix_len {prefix_len}")
|
|
234
|
+
|
|
235
|
+
# Constant indexes for use with the hypotheses tuples
|
|
236
|
+
LOGP: Final[int] = 0
|
|
237
|
+
SEQ: Final[int] = 1
|
|
238
|
+
LEN: Final[int] = 2
|
|
239
|
+
|
|
240
|
+
# Our starting hypothesis that we'll be extending.
|
|
241
|
+
# Format is (log likelihood, token id sequence, text length).
|
|
242
|
+
# Note: we only include tokens after any in left context.
|
|
243
|
+
start_length = 0
|
|
244
|
+
for x in tokens[len(self.left_context_tokens):]:
|
|
245
|
+
start_length += len(self.index_to_word_lower[x])
|
|
246
|
+
current_hypos = [(0.0, tokens, start_length)]
|
|
247
|
+
|
|
248
|
+
# We use a priority queue to track the top hypotheses during the beam search.
|
|
249
|
+
# For a beam of 8, empirical testing showed this was about the same amount
|
|
250
|
+
# of time as a simpler list that used a linear search to replace when full.
|
|
251
|
+
heapq.heapify(current_hypos)
|
|
252
|
+
|
|
253
|
+
# Create a hash mapping each valid following character to a list of log probabilities
|
|
254
|
+
char_to_log_probs = defaultdict(list)
|
|
255
|
+
|
|
256
|
+
# Add new extended hypotheses to this heap
|
|
257
|
+
next_hypos = []
|
|
258
|
+
|
|
259
|
+
# Tracks count of completed hypotheses
|
|
260
|
+
completed = 0
|
|
261
|
+
|
|
262
|
+
# Used to signal to while loop to stop the search
|
|
263
|
+
done = False
|
|
264
|
+
|
|
265
|
+
# Start a beam search forward from the backed off token sequence.
|
|
266
|
+
# Each iteration of this while loop extends hypotheses by all valid tokens.
|
|
267
|
+
# We only keep at most self.beam_width hypotheses in the valid heap.
|
|
268
|
+
# Stop extending search once we reach our max completed target.
|
|
269
|
+
while len(current_hypos) > 0 and not done:
|
|
270
|
+
# We'll explore hypothesis in order from most probable to least.
|
|
271
|
+
# This has little impact on how long it takes since this is only sorting a small number of things.
|
|
272
|
+
# But it is important with max_completed pruning since we want to bias for completing high probability things.
|
|
273
|
+
current_hypos.sort(reverse=True)
|
|
274
|
+
|
|
275
|
+
# Work on the hypotheses from the last round of extension.
|
|
276
|
+
# Create the torch tensor for the inference with a row for each hypothesis.
|
|
277
|
+
tokens_tensor = torch.tensor([x[SEQ] for x in current_hypos]).reshape(len(current_hypos), -1).to(self.device)
|
|
278
|
+
|
|
279
|
+
before_inference_ns = time.time_ns()
|
|
280
|
+
# Ask the LLM to predict tokens that come after our current set of hypotheses
|
|
281
|
+
with torch.no_grad():
|
|
282
|
+
# Compute the probabilities from the logits
|
|
283
|
+
log_probs = torch.log_softmax(self.model(tokens_tensor).logits[:, -1, :], dim=1)
|
|
284
|
+
|
|
285
|
+
# Create a big 2D tensor where each row is that hypothesis' current likelihood.
|
|
286
|
+
# First create a list of just the hypotheses' likelihoods.
|
|
287
|
+
# Then reshape to be a column vector.
|
|
288
|
+
# Then duplicate the column based on the number of subword tokens in the LLM.
|
|
289
|
+
add_tensor = torch.tensor([x[LOGP] for x in current_hypos]).reshape((log_probs.size()[0], 1)).repeat(1, log_probs.size()[1]).to(self.device)
|
|
290
|
+
|
|
291
|
+
# Add the current likelihoods with each subtoken's probability.
|
|
292
|
+
# Move it back to the CPU and convert to numpy since this makes it a lot faster to access for some reason.
|
|
293
|
+
new_log_probs = torch.add(log_probs, add_tensor).detach().cpu().numpy()
|
|
294
|
+
self.predict_inference_ns += time.time_ns() - before_inference_ns
|
|
295
|
+
|
|
296
|
+
for current_index, current in enumerate(current_hypos):
|
|
297
|
+
vocab = []
|
|
298
|
+
extra_vocab = []
|
|
299
|
+
# Extending this hypothesis must match the remaining text
|
|
300
|
+
remaining_context = context_lower[current[LEN]:]
|
|
301
|
+
if len(remaining_context) == 0:
|
|
302
|
+
# There is no remaining context thus all subword tokens that are valid under our symbol set
|
|
303
|
+
# should be considered when computing the probability of the next character.
|
|
304
|
+
vocab = self.valid_vocab
|
|
305
|
+
else:
|
|
306
|
+
if remaining_context in self.vocab:
|
|
307
|
+
# We have a list of subword tokens that match the remaining text.
|
|
308
|
+
# They could be the same length as the remaining text or longer and have the remaining text as a prefix.
|
|
309
|
+
vocab = self.vocab[remaining_context]
|
|
310
|
+
|
|
311
|
+
# We may need to use a subword token that doesn't completely consume the remaining text.
|
|
312
|
+
# Find these by tokenizing all possible lengths of text starting from the current position.
|
|
313
|
+
for i in range(1, len(remaining_context)):
|
|
314
|
+
tokenization = self._encode(context_lower[current[LEN]:current[LEN] + i])
|
|
315
|
+
# Ignore tokenizations involving multiple tokens since they involve an ID we would have already added.
|
|
316
|
+
if len(tokenization) == 1:
|
|
317
|
+
extra_vocab += tokenization[0],
|
|
318
|
+
|
|
319
|
+
# The below code takes the most time, results from pprofile on 5 phrases on an 2080 GPU:
|
|
320
|
+
# 299| 22484582| 89.5763| 3.9839e-06| 14.24%| for token_id in itertools.chain(vocab, extra_vocab):
|
|
321
|
+
# 300| 0| 0| 0| 0.00%| # For a hypothesis to finish it must extend beyond the existing typed context
|
|
322
|
+
# 301| 22483271| 93.7939| 4.17172e-06| 14.91%| subword_len = len(self.index_to_word_lower[token_id])
|
|
323
|
+
# 302| 22483271| 92.8608| 4.13022e-06| 14.76%| if (current[LEN] + subword_len) > len(context):
|
|
324
|
+
# 303| 0| 0| 0| 0.00%| # Add this likelihood to the list for the character at the prediction position.
|
|
325
|
+
# 304| 0| 0| 0| 0.00%| # Tracking the list and doing logsumpexp later was faster than doing it for each add.
|
|
326
|
+
# 305| 22480431| 106.353| 4.73094e-06| 16.90%| char_to_log_probs[self.index_to_word_lower[token_id][target_pos - current[LEN]]] += new_log_probs[current_index][token_id],
|
|
327
|
+
# 306| 22480431| 92.689| 4.1231e-06| 14.73%| completed += 1
|
|
328
|
+
# 307| 2840| 0.0124488| 4.38338e-06| 0.00%| elif not self.beam_width or len(next_hypos) <
|
|
329
|
+
#
|
|
330
|
+
# Tuning notes:
|
|
331
|
+
# - With a beam of 8 and max completed of 32,000, getting around 5x speedup on written dev set.
|
|
332
|
+
# - This results in a PPL increase of 0.0025 versus old results using only beam of >= 8.
|
|
333
|
+
# - Pruning based on log probability difference and based on minimum number of hypotheses per symbol in alphabet did worse.
|
|
334
|
+
# - Code for these other pruning methods was removed.
|
|
335
|
+
# Possible ways to make it faster:
|
|
336
|
+
# - Stop part way through the below for loop over (vocab, extra_vocab). But this seems weird since the token IDs are in
|
|
337
|
+
# no particular order, we'd be just stopping early on the last hypothesis being explored by the enclosing loop.
|
|
338
|
+
# - Sort the rows in the log prob results on the GPU. Use these to limit which token IDs we explore in the below
|
|
339
|
+
# for loop. Is it possible to do this without introducing too much extra work to limit to the high probability ones?
|
|
340
|
+
|
|
341
|
+
# Create a list of token indexes that are a prefix of the target text.
|
|
342
|
+
# We go over all the integer IDs in the vocab and extra_vocab lists.
|
|
343
|
+
for token_id in itertools.chain(vocab, extra_vocab):
|
|
344
|
+
# For a hypothesis to finish it must extend beyond the existing typed context
|
|
345
|
+
subword_len = len(self.index_to_word_lower[token_id])
|
|
346
|
+
if (current[LEN] + subword_len) > len(context):
|
|
347
|
+
# Add this likelihood to the list for the character at the prediction position.
|
|
348
|
+
# Tracking the list and doing logsumpexp later was faster than doing it for each add.
|
|
349
|
+
char_to_log_probs[self.index_to_word_lower[token_id][target_pos - current[LEN]]] += new_log_probs[current_index][token_id],
|
|
350
|
+
completed += 1
|
|
351
|
+
elif not self.beam_width or len(next_hypos) < self.beam_width:
|
|
352
|
+
# If we are under the beam limit then just add it
|
|
353
|
+
heapq.heappush(next_hypos,
|
|
354
|
+
(new_log_probs[current_index][token_id],
|
|
355
|
+
current[SEQ] + [token_id],
|
|
356
|
+
current[LEN] + subword_len))
|
|
357
|
+
elif new_log_probs[current_index][token_id] > next_hypos[0][LOGP]:
|
|
358
|
+
# Or replace the worst hypotheses with the new one
|
|
359
|
+
heapq.heappushpop(next_hypos,
|
|
360
|
+
(new_log_probs[current_index][token_id],
|
|
361
|
+
current[SEQ] + [token_id],
|
|
362
|
+
current[LEN] + subword_len))
|
|
363
|
+
|
|
364
|
+
# Break out of the for loop over hypotheses and while loop if we reach our max completed goal
|
|
365
|
+
if self.max_completed and completed >= self.max_completed:
|
|
366
|
+
done = True
|
|
367
|
+
break
|
|
368
|
+
|
|
369
|
+
# Swap in the extended set as the new current working set
|
|
370
|
+
current_hypos = next_hypos
|
|
371
|
+
next_hypos = []
|
|
372
|
+
|
|
373
|
+
# Parallel array to symbol_set for storing the marginals
|
|
374
|
+
char_probs = []
|
|
375
|
+
for ch in self.symbol_set_lower:
|
|
376
|
+
# Handle cases when symbols are never seen
|
|
377
|
+
if ch in char_to_log_probs:
|
|
378
|
+
char_probs += logsumexp(char_to_log_probs[ch]),
|
|
379
|
+
else:
|
|
380
|
+
char_probs += float("-inf"),
|
|
381
|
+
|
|
382
|
+
# Normalize to a distribution that sums to 1
|
|
383
|
+
char_probs = softmax(char_probs)
|
|
384
|
+
|
|
385
|
+
next_char_pred = {}
|
|
386
|
+
for i, ch in enumerate(self.symbol_set_lower):
|
|
387
|
+
# This works even if ch is a space
|
|
388
|
+
next_char_pred[ch.upper()] = char_probs[i]
|
|
389
|
+
|
|
390
|
+
end_ns = time.time_ns()
|
|
391
|
+
self.predict_total_ns += end_ns - start_ns
|
|
392
|
+
|
|
393
|
+
return list(sorted(next_char_pred.items(), key=lambda item: item[1], reverse=True))
|
|
394
|
+
|
|
395
|
+
def dump_predict_times(self) -> None:
|
|
396
|
+
"""Print some stats about the prediction timing"""
|
|
397
|
+
if self.predict_total_ns > 0:
|
|
398
|
+
print(f"Predict %: "
|
|
399
|
+
f"inference {self.predict_inference_ns / self.predict_total_ns * 100.0:.3f}")
|
|
400
|
+
|
|
401
|
+
def load(self) -> None:
|
|
402
|
+
"""
|
|
403
|
+
Load the language model and tokenizer, initialize class variables
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name, use_fast=False)
|
|
407
|
+
except BaseException:
|
|
408
|
+
raise InvalidLanguageModelException(f"{self.model_name} is not a valid model identifier on HuggingFace.")
|
|
409
|
+
self.vocab_size = self.tokenizer.vocab_size
|
|
410
|
+
try:
|
|
411
|
+
self.model = AutoModelForCausalLM.from_pretrained(self.model_dir)
|
|
412
|
+
if self.fp16 and self.device == "cuda":
|
|
413
|
+
self.model = self.model.half()
|
|
414
|
+
except:
|
|
415
|
+
raise InvalidLanguageModelException(f"{self.model_dir} is not a valid local folder or model identifier on HuggingFace.")
|
|
416
|
+
|
|
417
|
+
self.model.eval()
|
|
418
|
+
|
|
419
|
+
self.model.to(self.device)
|
|
420
|
+
|
|
421
|
+
self.symbol_set_lower = []
|
|
422
|
+
|
|
423
|
+
for ch in self.symbol_set:
|
|
424
|
+
# This works even if ch is a space
|
|
425
|
+
self.symbol_set_lower.append(ch.lower())
|
|
426
|
+
|
|
427
|
+
self._build_vocab()
|
|
428
|
+
|
|
429
|
+
def get_num_parameters(self) -> int:
|
|
430
|
+
"""
|
|
431
|
+
Find out how many parameters the loaded model has
|
|
432
|
+
Args:
|
|
433
|
+
Response:
|
|
434
|
+
Integer number of parameters in the transformer model
|
|
435
|
+
"""
|
|
436
|
+
return sum(p.numel() for p in self.model.parameters())
|
|
437
|
+
|