poemai-utils 0.0.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.
- poemai-utils-0.0.0/LICENSE.txt +21 -0
- poemai-utils-0.0.0/PKG-INFO +19 -0
- poemai-utils-0.0.0/README.md +3 -0
- poemai-utils-0.0.0/pyproject.toml +12 -0
- poemai-utils-0.0.0/setup.cfg +74 -0
- poemai-utils-0.0.0/setup.py +21 -0
- poemai-utils-0.0.0/src/poemai_utils/__init__.py +16 -0
- poemai-utils-0.0.0/src/poemai_utils/basic_types_utils.py +52 -0
- poemai-utils-0.0.0/src/poemai_utils/configuration_api.py +2 -0
- poemai-utils-0.0.0/src/poemai_utils/embeddings/__init__.py +0 -0
- poemai-utils-0.0.0/src/poemai_utils/embeddings/cross_similarity_analyzer.py +121 -0
- poemai-utils-0.0.0/src/poemai_utils/embeddings/embedder_base.py +3 -0
- poemai-utils-0.0.0/src/poemai_utils/embeddings/embedding_cache.py +34 -0
- poemai-utils-0.0.0/src/poemai_utils/embeddings/embedding_store.py +60 -0
- poemai-utils-0.0.0/src/poemai_utils/embeddings/openai_embedder.py +16 -0
- poemai-utils-0.0.0/src/poemai_utils/embeddings/self_similarity_analyzer.py +70 -0
- poemai-utils-0.0.0/src/poemai_utils/embeddings/sentence_transformer_embedder.py +46 -0
- poemai-utils-0.0.0/src/poemai_utils/embeddings/sgpt_embedder.py +85 -0
- poemai-utils-0.0.0/src/poemai_utils/enum_utils.py +38 -0
- poemai-utils-0.0.0/src/poemai_utils/openai/ask.py +452 -0
- poemai-utils-0.0.0/src/poemai_utils/openai/llm_answer_cache.py +102 -0
- poemai-utils-0.0.0/src/poemai_utils/openai/llm_answer_cache_dao.py +46 -0
- poemai-utils-0.0.0/src/poemai_utils/openai/openai_model.py +43 -0
- poemai-utils-0.0.0/src/poemai_utils/utils_config.py +9 -0
- poemai-utils-0.0.0/src/poemai_utils.egg-info/PKG-INFO +19 -0
- poemai-utils-0.0.0/src/poemai_utils.egg-info/SOURCES.txt +29 -0
- poemai-utils-0.0.0/src/poemai_utils.egg-info/dependency_links.txt +1 -0
- poemai-utils-0.0.0/src/poemai_utils.egg-info/not-zip-safe +1 -0
- poemai-utils-0.0.0/src/poemai_utils.egg-info/requires.txt +12 -0
- poemai-utils-0.0.0/src/poemai_utils.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Markus Emmenegger, poemAI GmbH, Horw, Switzerland
|
|
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,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: poemai-utils
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Add a short description here!
|
|
5
|
+
Home-page: https://github.com/pyscaffold/pyscaffold/
|
|
6
|
+
Author: Markus Emmenegger
|
|
7
|
+
Author-email: markus.emmenegger@poemai.ch
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Documentation, https://pyscaffold.org/
|
|
10
|
+
Platform: any
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Description-Content-Type: text/markdown; charset=UTF-8
|
|
14
|
+
Provides-Extra: testing
|
|
15
|
+
License-File: LICENSE.txt
|
|
16
|
+
|
|
17
|
+
# poemai-utils
|
|
18
|
+
|
|
19
|
+
This package is a collection of utilities for AI projects.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD!
|
|
3
|
+
requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5"]
|
|
4
|
+
build-backend = "setuptools.build_meta"
|
|
5
|
+
|
|
6
|
+
[tool.setuptools_scm]
|
|
7
|
+
# For smarter version schemes and other configuration options,
|
|
8
|
+
# check out https://github.com/pypa/setuptools_scm
|
|
9
|
+
version_scheme = "no-guess-dev"
|
|
10
|
+
[tool.black]
|
|
11
|
+
line-length = 88
|
|
12
|
+
target-version = ['py311']
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = poemai-utils
|
|
3
|
+
description = Add a short description here!
|
|
4
|
+
author = Markus Emmenegger
|
|
5
|
+
author_email = markus.emmenegger@poemai.ch
|
|
6
|
+
license = MIT
|
|
7
|
+
license_files = LICENSE.txt
|
|
8
|
+
long_description = file: README.md
|
|
9
|
+
long_description_content_type = text/markdown; charset=UTF-8
|
|
10
|
+
url = https://github.com/pyscaffold/pyscaffold/
|
|
11
|
+
project_urls =
|
|
12
|
+
Documentation = https://pyscaffold.org/
|
|
13
|
+
platforms = any
|
|
14
|
+
classifiers =
|
|
15
|
+
Development Status :: 4 - Beta
|
|
16
|
+
Programming Language :: Python
|
|
17
|
+
|
|
18
|
+
[options]
|
|
19
|
+
zip_safe = False
|
|
20
|
+
packages = find_namespace:
|
|
21
|
+
include_package_data = True
|
|
22
|
+
package_dir =
|
|
23
|
+
=src
|
|
24
|
+
install_requires =
|
|
25
|
+
importlib-metadata; python_version<"3.8"
|
|
26
|
+
sqlitedict
|
|
27
|
+
httpx
|
|
28
|
+
numpy
|
|
29
|
+
|
|
30
|
+
[options.packages.find]
|
|
31
|
+
where = src
|
|
32
|
+
exclude =
|
|
33
|
+
tests
|
|
34
|
+
|
|
35
|
+
[options.extras_require]
|
|
36
|
+
testing =
|
|
37
|
+
setuptools
|
|
38
|
+
pytest
|
|
39
|
+
pytest-cov
|
|
40
|
+
transformers
|
|
41
|
+
|
|
42
|
+
[options.entry_points]
|
|
43
|
+
|
|
44
|
+
[tool:pytest]
|
|
45
|
+
norecursedirs =
|
|
46
|
+
dist
|
|
47
|
+
build
|
|
48
|
+
.tox
|
|
49
|
+
testpaths = tests
|
|
50
|
+
|
|
51
|
+
[devpi:upload]
|
|
52
|
+
no_vcs = 1
|
|
53
|
+
formats = bdist_wheel
|
|
54
|
+
|
|
55
|
+
[flake8]
|
|
56
|
+
max_line_length = 88
|
|
57
|
+
extend_ignore = E203, W503
|
|
58
|
+
exclude =
|
|
59
|
+
.tox
|
|
60
|
+
build
|
|
61
|
+
dist
|
|
62
|
+
.eggs
|
|
63
|
+
docs/conf.py
|
|
64
|
+
|
|
65
|
+
[pyscaffold]
|
|
66
|
+
version = 4.4.1
|
|
67
|
+
package = poemai_utils
|
|
68
|
+
|
|
69
|
+
[options.package_data]
|
|
70
|
+
|
|
71
|
+
[egg_info]
|
|
72
|
+
tag_build =
|
|
73
|
+
tag_date = 0
|
|
74
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Setup file for poemai-utils.
|
|
3
|
+
Use setup.cfg to configure your project.
|
|
4
|
+
|
|
5
|
+
This file was generated with PyScaffold 4.4.1.
|
|
6
|
+
PyScaffold helps you to put up the scaffold of your new Python project.
|
|
7
|
+
Learn more under: https://pyscaffold.org/
|
|
8
|
+
"""
|
|
9
|
+
from setuptools import setup
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
try:
|
|
13
|
+
setup(use_scm_version={"version_scheme": "no-guess-dev"})
|
|
14
|
+
except: # noqa
|
|
15
|
+
print(
|
|
16
|
+
"\n\nAn error occurred while building the project, "
|
|
17
|
+
"please ensure you have the most updated version of setuptools, "
|
|
18
|
+
"setuptools_scm and wheel with:\n"
|
|
19
|
+
" pip install -U setuptools setuptools_scm wheel\n\n"
|
|
20
|
+
)
|
|
21
|
+
raise
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if sys.version_info[:2] >= (3, 8):
|
|
4
|
+
# TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version # pragma: no cover
|
|
6
|
+
else:
|
|
7
|
+
from importlib_metadata import PackageNotFoundError, version # pragma: no cover
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
# Change here if project is renamed and does not equal the package name
|
|
11
|
+
dist_name = "poemai-utils"
|
|
12
|
+
__version__ = version(dist_name)
|
|
13
|
+
except PackageNotFoundError: # pragma: no cover
|
|
14
|
+
__version__ = "unknown"
|
|
15
|
+
finally:
|
|
16
|
+
del version, PackageNotFoundError
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
def linebreak(text, max_length=80):
|
|
2
|
+
"""Add linebreaks to text to make it more readable"""
|
|
3
|
+
|
|
4
|
+
out_lines = []
|
|
5
|
+
for line in text.split("\n"):
|
|
6
|
+
if len(line) <= max_length:
|
|
7
|
+
out_lines.append(line)
|
|
8
|
+
else:
|
|
9
|
+
# split line into words
|
|
10
|
+
words = line.split(" ")
|
|
11
|
+
out_line = ""
|
|
12
|
+
for word in words:
|
|
13
|
+
if len(out_line) + len(word) > max_length:
|
|
14
|
+
out_lines.append(out_line)
|
|
15
|
+
out_line = ""
|
|
16
|
+
out_line += word + " "
|
|
17
|
+
out_lines.append(out_line)
|
|
18
|
+
return "\n".join(out_lines)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def short_display(text, max_lenght=100):
|
|
22
|
+
if len(text) > max_lenght:
|
|
23
|
+
return text[:max_lenght] + "..."
|
|
24
|
+
return text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def remove_none_from_dict(
|
|
28
|
+
dictionary, remove_empty_lists=False, remove_empty_dicts=False
|
|
29
|
+
):
|
|
30
|
+
retval = {}
|
|
31
|
+
|
|
32
|
+
for key, value in list(dictionary.items()):
|
|
33
|
+
if value is None:
|
|
34
|
+
continue
|
|
35
|
+
elif isinstance(value, dict):
|
|
36
|
+
new_value = remove_none_from_dict(value)
|
|
37
|
+
if not new_value and remove_empty_dicts:
|
|
38
|
+
continue
|
|
39
|
+
else:
|
|
40
|
+
retval[key] = new_value
|
|
41
|
+
elif isinstance(value, list):
|
|
42
|
+
if len(value) == 0 and remove_empty_lists:
|
|
43
|
+
continue
|
|
44
|
+
else:
|
|
45
|
+
retval[key] = [
|
|
46
|
+
remove_none_from_dict(item) if isinstance(item, dict) else item
|
|
47
|
+
for item in value
|
|
48
|
+
]
|
|
49
|
+
else:
|
|
50
|
+
retval[key] = value
|
|
51
|
+
|
|
52
|
+
return retval
|
|
File without changes
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from poemai_utils.embeddings.self_similarity_analyzer import SelfSimilarityAnalyzer
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CrossSimilarityAnalyzer:
|
|
6
|
+
"""A class to analyze the similarity between two lists of embeddings."""
|
|
7
|
+
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
embedding_cache=None,
|
|
11
|
+
similarity_analyzer_1=None,
|
|
12
|
+
similarity_analyzer_2=None,
|
|
13
|
+
embedder=None,
|
|
14
|
+
):
|
|
15
|
+
if similarity_analyzer_1 is None:
|
|
16
|
+
self.analyzer_1 = SelfSimilarityAnalyzer(
|
|
17
|
+
embedder,
|
|
18
|
+
embedding_cache=embedding_cache,
|
|
19
|
+
)
|
|
20
|
+
else:
|
|
21
|
+
self.analyzer_1 = similarity_analyzer_1
|
|
22
|
+
if similarity_analyzer_2 is None:
|
|
23
|
+
self.analyzer_2 = SelfSimilarityAnalyzer(
|
|
24
|
+
embedder, embedding_cache=embedding_cache
|
|
25
|
+
)
|
|
26
|
+
else:
|
|
27
|
+
self.analyzer_2 = similarity_analyzer_2
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
self.analyzer_1.use_cosine_similarity
|
|
31
|
+
!= self.analyzer_2.use_cosine_similarity
|
|
32
|
+
):
|
|
33
|
+
raise ValueError("Both analyzers must have the same use_cosine_similarity")
|
|
34
|
+
|
|
35
|
+
self.use_cosine_similarity = self.analyzer_1.use_cosine_similarity
|
|
36
|
+
|
|
37
|
+
def add_text_for_1(self, text, metadata):
|
|
38
|
+
self.analyzer_1.add_text(text, metadata)
|
|
39
|
+
|
|
40
|
+
def add_embedding_for_1(self, text, embedding, metadata):
|
|
41
|
+
self.analyzer_1.add_embedding(text, embedding, metadata)
|
|
42
|
+
|
|
43
|
+
def add_text_for_2(self, text, metadata):
|
|
44
|
+
self.analyzer_2.add_text(text, metadata)
|
|
45
|
+
|
|
46
|
+
def add_embedding_for_2(self, text, embedding, metadata):
|
|
47
|
+
self.analyzer_2.add_embedding(text, embedding, metadata)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def normalize_rows(x):
|
|
51
|
+
return x / np.linalg.norm(x, ord=2, axis=1, keepdims=True)
|
|
52
|
+
|
|
53
|
+
def cross_similarity_matrix(self):
|
|
54
|
+
m1 = self.analyzer_1.embedding_store.embedding_matrix
|
|
55
|
+
m2 = self.analyzer_2.embedding_store.embedding_matrix
|
|
56
|
+
|
|
57
|
+
if self.use_cosine_similarity:
|
|
58
|
+
m1 = self.normalize_rows(m1)
|
|
59
|
+
m2 = self.normalize_rows(m2)
|
|
60
|
+
|
|
61
|
+
return np.matmul(
|
|
62
|
+
m1,
|
|
63
|
+
m2.T,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def find_top_k_similar_pairs(self, k=5):
|
|
67
|
+
similarity_matrix = self.cross_similarity_matrix()
|
|
68
|
+
|
|
69
|
+
top_k_idxs = np.argsort(similarity_matrix, axis=None)[-k:]
|
|
70
|
+
rows, cols = np.unravel_index(top_k_idxs, similarity_matrix.shape)
|
|
71
|
+
values = similarity_matrix[rows, cols]
|
|
72
|
+
|
|
73
|
+
retval = []
|
|
74
|
+
for row, col, value in zip(rows, cols, values):
|
|
75
|
+
row_metadata = self.analyzer_1.metadata.get(row)
|
|
76
|
+
col_metadata = self.analyzer_2.metadata.get(col)
|
|
77
|
+
|
|
78
|
+
retval.append(
|
|
79
|
+
(
|
|
80
|
+
row_metadata,
|
|
81
|
+
col_metadata,
|
|
82
|
+
value,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return retval
|
|
87
|
+
|
|
88
|
+
def map_1_to_2(self, k=1):
|
|
89
|
+
similarity_matrix = self.cross_similarity_matrix()
|
|
90
|
+
best = np.argsort(similarity_matrix, axis=1)[:, ::-1][:, :k]
|
|
91
|
+
|
|
92
|
+
retval = []
|
|
93
|
+
for i, row in enumerate(best):
|
|
94
|
+
row_metadata = self.analyzer_1.metadata.get(i)
|
|
95
|
+
col_metadata = [self.analyzer_2.metadata.get(b) for b in row]
|
|
96
|
+
|
|
97
|
+
retval.append(
|
|
98
|
+
(
|
|
99
|
+
row_metadata,
|
|
100
|
+
col_metadata,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
return retval
|
|
104
|
+
|
|
105
|
+
def map_1_to_2_with_scores(self, k=1):
|
|
106
|
+
similarity_matrix = self.cross_similarity_matrix()
|
|
107
|
+
best = np.argsort(similarity_matrix, axis=1)[:, ::-1][:, :k]
|
|
108
|
+
|
|
109
|
+
retval = []
|
|
110
|
+
for i, row in enumerate(best):
|
|
111
|
+
row_metadata = self.analyzer_1.metadata.get(i)
|
|
112
|
+
col_metadata = [self.analyzer_2.metadata.get(b) for b in row]
|
|
113
|
+
|
|
114
|
+
col_result = list(zip(col_metadata, similarity_matrix[i, row]))
|
|
115
|
+
retval.append(
|
|
116
|
+
(
|
|
117
|
+
row_metadata,
|
|
118
|
+
col_result,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
return retval
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from hashlib import sha256
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from sqlitedict import SqliteDict
|
|
6
|
+
|
|
7
|
+
_logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EmbeddingCache:
|
|
11
|
+
def __init__(self, cache_file):
|
|
12
|
+
self.cache = SqliteDict(cache_file, autocommit=True)
|
|
13
|
+
|
|
14
|
+
def get(self, text, model_name):
|
|
15
|
+
text_hash = self.text_hash(text)
|
|
16
|
+
key = self.calc_key(model_name, text_hash)
|
|
17
|
+
short_key = (model_name, text_hash[:6])
|
|
18
|
+
if key in self.cache:
|
|
19
|
+
_logger.debug(f"Cache hit for {short_key}")
|
|
20
|
+
return np.array(self.cache[key])
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
def put(self, text, embedding, model_name):
|
|
24
|
+
text_hash = self.text_hash(text)
|
|
25
|
+
key = self.calc_key(model_name, text_hash)
|
|
26
|
+
self.cache[key] = embedding.tolist()
|
|
27
|
+
|
|
28
|
+
def text_hash(self, text):
|
|
29
|
+
# calculate sha256
|
|
30
|
+
return sha256(text.encode("utf-8")).hexdigest()
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def calc_key(model_name, text_hash):
|
|
34
|
+
return model_name + "_" + text_hash
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from hashlib import sha256
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EmbeddingStore:
|
|
7
|
+
def __init__(self, embedding_cache=None, embedder=None):
|
|
8
|
+
self.embedder = embedder
|
|
9
|
+
|
|
10
|
+
self.texts = []
|
|
11
|
+
self.embedding_matrix = None
|
|
12
|
+
self.embedding_cache = embedding_cache
|
|
13
|
+
|
|
14
|
+
def _store_embedding(self, embedding):
|
|
15
|
+
if self.embedding_matrix is None:
|
|
16
|
+
self.embedding_matrix = np.expand_dims(embedding, 0)
|
|
17
|
+
else:
|
|
18
|
+
self.embedding_matrix = np.concatenate(
|
|
19
|
+
[self.embedding_matrix, [embedding]], 0
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def add_text(self, text, is_query: bool = False):
|
|
23
|
+
self.texts.append(text)
|
|
24
|
+
self._store_embedding(self._create_embedding(text, is_query=is_query))
|
|
25
|
+
return len(self.texts) - 1
|
|
26
|
+
|
|
27
|
+
def add_embedding(self, text, embedding):
|
|
28
|
+
self.texts.append(text)
|
|
29
|
+
self._store_embedding(embedding)
|
|
30
|
+
return len(self.texts) - 1
|
|
31
|
+
|
|
32
|
+
def query_by_text(self, query, k=5):
|
|
33
|
+
if self.embedding_matrix is None:
|
|
34
|
+
return []
|
|
35
|
+
query_embedding = self._create_embedding(query)
|
|
36
|
+
scores = self.embedding_matrix.dot(query_embedding)
|
|
37
|
+
idxs = np.argsort(scores)[-k:][::-1]
|
|
38
|
+
|
|
39
|
+
return [(self.texts[i], scores[i], i) for i in idxs]
|
|
40
|
+
|
|
41
|
+
def query_by_embedding(self, query_embedding, k=5):
|
|
42
|
+
if self.embedding_matrix is None:
|
|
43
|
+
return []
|
|
44
|
+
scores = self.embedding_matrix.dot(query_embedding)
|
|
45
|
+
idxs = np.argsort(scores)[-k:][::-1]
|
|
46
|
+
|
|
47
|
+
return [(self.texts[i], scores[i], i) for i in idxs]
|
|
48
|
+
|
|
49
|
+
def _create_embedding(self, text, is_query):
|
|
50
|
+
if self.embedding_cache is not None:
|
|
51
|
+
embedding = self.embedding_cache.get(text, self.embedder.model_name)
|
|
52
|
+
if embedding is not None:
|
|
53
|
+
return embedding
|
|
54
|
+
|
|
55
|
+
embedding = self.embedder.calc_embedding(text, is_query=is_query)
|
|
56
|
+
|
|
57
|
+
if self.embedding_cache is not None:
|
|
58
|
+
self.embedding_cache.put(text, embedding, self.embedder.model_name)
|
|
59
|
+
|
|
60
|
+
return embedding
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import openai
|
|
3
|
+
from poemai_utils.embeddings.embedder_base import EbedderBase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OpenAIEmbedder(EbedderBase):
|
|
7
|
+
def __init__(self, model_name="text-embedding-ada-002"):
|
|
8
|
+
super().__init__()
|
|
9
|
+
|
|
10
|
+
self.model_name = model_name
|
|
11
|
+
|
|
12
|
+
def calc_embedding(self, text, is_query: bool = False):
|
|
13
|
+
response = openai.Embedding.create(input=text, model=self.model_name)
|
|
14
|
+
embedding = response["data"][0]["embedding"]
|
|
15
|
+
embedding = np.array(embedding, dtype=np.float32)
|
|
16
|
+
return embedding
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from poemai_utils.embeddings.embedding_store import EmbeddingStore
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SelfSimilarityAnalyzer:
|
|
6
|
+
def __init__(self, embedder, embedding_cache=None):
|
|
7
|
+
if embedder is None:
|
|
8
|
+
raise ValueError("embedder must be provided")
|
|
9
|
+
self.embedding_store = EmbeddingStore(embedding_cache, embedder=embedder)
|
|
10
|
+
self.use_cosine_similarity = embedder.use_cosine_similarity
|
|
11
|
+
self.metadata = {}
|
|
12
|
+
|
|
13
|
+
def add_text(self, text, metadata, is_query=False):
|
|
14
|
+
if text is None:
|
|
15
|
+
return
|
|
16
|
+
text_id = self.embedding_store.add_text(text, is_query=is_query)
|
|
17
|
+
self.metadata[text_id] = metadata
|
|
18
|
+
|
|
19
|
+
def add_embedding(self, text, embedding, metadata):
|
|
20
|
+
text_id = self.embedding_store.add_embedding(text, embedding)
|
|
21
|
+
self.metadata[text_id] = metadata
|
|
22
|
+
|
|
23
|
+
def query(self, query, k=5):
|
|
24
|
+
top_k = self.embedding_store.query_by_text(query, k)
|
|
25
|
+
return [
|
|
26
|
+
(text_id, score, self.metadata.get(text_id)) for _, score, text_id in top_k
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def normalize_rows(x):
|
|
31
|
+
return x / np.linalg.norm(x, ord=2, axis=1, keepdims=True)
|
|
32
|
+
|
|
33
|
+
def similarity_matrix(self):
|
|
34
|
+
m1 = self.embedding_store.embedding_matrix
|
|
35
|
+
if self.use_cosine_similarity:
|
|
36
|
+
m1 = self.normalize_rows(m1)
|
|
37
|
+
|
|
38
|
+
return np.matmul(
|
|
39
|
+
m1,
|
|
40
|
+
m1.T,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def find_top_k_similar_pairs(self, k=5):
|
|
44
|
+
similarity_matrix = self.similarity_matrix()
|
|
45
|
+
|
|
46
|
+
similarity_matrix = np.tril(similarity_matrix)
|
|
47
|
+
np.fill_diagonal(similarity_matrix, 0)
|
|
48
|
+
|
|
49
|
+
# find the top k most similar sections
|
|
50
|
+
top_k_idxs = np.argsort(similarity_matrix, axis=None)[-k:]
|
|
51
|
+
rows, cols = np.unravel_index(top_k_idxs, similarity_matrix.shape)
|
|
52
|
+
values = similarity_matrix[rows, cols]
|
|
53
|
+
|
|
54
|
+
retval = []
|
|
55
|
+
for row, col, value in zip(rows, cols, values):
|
|
56
|
+
row_metadata = self.metadata.get(row)
|
|
57
|
+
col_metadata = self.metadata.get(col)
|
|
58
|
+
|
|
59
|
+
retval.append(
|
|
60
|
+
(
|
|
61
|
+
row_metadata,
|
|
62
|
+
col_metadata,
|
|
63
|
+
value,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return retval
|
|
68
|
+
|
|
69
|
+
def metadata(self, text_id):
|
|
70
|
+
return self.metadata.get(text_id)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from poemai_utils.embeddings.embedder_base import EbedderBase
|
|
4
|
+
from poemai_utils.enum_utils import add_enum_attrs, add_enum_repr
|
|
5
|
+
from sentence_transformers import SentenceTransformer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SentenceTransformerEmbeddingModel(Enum):
|
|
9
|
+
LABSE = "sentence-transformers/LaBSE"
|
|
10
|
+
DISTILUSE = "distiluse-base-multilingual-cased-v1"
|
|
11
|
+
BI_ELECTRA_GERMAN = "svalabs/bi-electra-ms-marco-german-uncased"
|
|
12
|
+
DISTILBERT = "msmarco-distilbert-base-tas-b"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
add_enum_attrs(
|
|
16
|
+
{
|
|
17
|
+
SentenceTransformerEmbeddingModel.LABSE: {
|
|
18
|
+
"use_cosine_similarity": False,
|
|
19
|
+
},
|
|
20
|
+
SentenceTransformerEmbeddingModel.DISTILUSE: {
|
|
21
|
+
"use_cosine_similarity": False,
|
|
22
|
+
},
|
|
23
|
+
SentenceTransformerEmbeddingModel.BI_ELECTRA_GERMAN: {
|
|
24
|
+
"use_cosine_similarity": True,
|
|
25
|
+
},
|
|
26
|
+
SentenceTransformerEmbeddingModel.DISTILBERT: {
|
|
27
|
+
"use_cosine_similarity": False,
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
add_enum_repr(SentenceTransformerEmbeddingModel)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SentenceTransformerEmbedder(EbedderBase):
|
|
35
|
+
# msmarco-distilbert-base-tas-b
|
|
36
|
+
# distiluse-base-multilingual-cased-v1
|
|
37
|
+
# sentence-transformers/LaBSE'
|
|
38
|
+
# svalabs/bi-electra-ms-marco-german-uncased : use_cosine_similarity=True
|
|
39
|
+
def __init__(self, model_id, use_cosine_similarity=False):
|
|
40
|
+
super().__init__(use_cosine_similarity=model_id.use_cosine_similarity)
|
|
41
|
+
|
|
42
|
+
self.model_name = model_id.value
|
|
43
|
+
self.model = SentenceTransformer(model_id.value)
|
|
44
|
+
|
|
45
|
+
def calc_embedding(self, text, is_query: bool = False):
|
|
46
|
+
return self.model.encode(text, show_progress_bar=False)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from poemai_utils.embeddings.embedder_base import EbedderBase
|
|
3
|
+
from transformers import AutoModel, AutoTokenizer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SGPTEmbedder(EbedderBase):
|
|
7
|
+
def __init__(
|
|
8
|
+
self, model_name="Muennighoff/SGPT-125M-weightedmean-msmarco-specb-bitfit"
|
|
9
|
+
):
|
|
10
|
+
super().__init__()
|
|
11
|
+
|
|
12
|
+
self.use_cosine_similarity = True
|
|
13
|
+
self.model_name = model_name
|
|
14
|
+
# Get our models - The package will take care of downloading the models automatically
|
|
15
|
+
# For best performance: Muennighoff/SGPT-5.8B-weightedmean-msmarco-specb-bitfit
|
|
16
|
+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
|
17
|
+
|
|
18
|
+
self.SPECB_QUE_BOS = self.tokenizer.encode("[", add_special_tokens=False)[0]
|
|
19
|
+
self.SPECB_QUE_EOS = self.tokenizer.encode("]", add_special_tokens=False)[0]
|
|
20
|
+
|
|
21
|
+
self.SPECB_DOC_BOS = self.tokenizer.encode("{", add_special_tokens=False)[0]
|
|
22
|
+
self.SPECB_DOC_EOS = self.tokenizer.encode("}", add_special_tokens=False)[0]
|
|
23
|
+
|
|
24
|
+
self.model = AutoModel.from_pretrained(self.model_name)
|
|
25
|
+
# Deactivate Dropout (There is no dropout in the above models so it makes no difference here but other SGPT models may have dropout)
|
|
26
|
+
self.model.eval()
|
|
27
|
+
|
|
28
|
+
def tokenize_with_specb(self, texts, is_query):
|
|
29
|
+
# Tokenize without padding
|
|
30
|
+
batch_tokens = self.tokenizer(texts, padding=False, truncation=True)
|
|
31
|
+
# Add special brackets & pay attention to them
|
|
32
|
+
for seq, att in zip(batch_tokens["input_ids"], batch_tokens["attention_mask"]):
|
|
33
|
+
if is_query:
|
|
34
|
+
seq.insert(0, self.SPECB_QUE_BOS)
|
|
35
|
+
seq.append(self.SPECB_QUE_EOS)
|
|
36
|
+
else:
|
|
37
|
+
seq.insert(0, self.SPECB_DOC_BOS)
|
|
38
|
+
seq.append(self.SPECB_DOC_EOS)
|
|
39
|
+
att.insert(0, 1)
|
|
40
|
+
att.append(1)
|
|
41
|
+
# Add padding
|
|
42
|
+
batch_tokens = self.tokenizer.pad(
|
|
43
|
+
batch_tokens, padding=True, return_tensors="pt"
|
|
44
|
+
)
|
|
45
|
+
return batch_tokens
|
|
46
|
+
|
|
47
|
+
def get_weightedmean_embedding(self, batch_tokens):
|
|
48
|
+
# Get the embeddings
|
|
49
|
+
with torch.no_grad():
|
|
50
|
+
# Get hidden state of shape [bs, seq_len, hid_dim]
|
|
51
|
+
last_hidden_state = self.model(
|
|
52
|
+
**batch_tokens, output_hidden_states=True, return_dict=True
|
|
53
|
+
).last_hidden_state
|
|
54
|
+
|
|
55
|
+
# Get weights of shape [bs, seq_len, hid_dim]
|
|
56
|
+
weights = (
|
|
57
|
+
torch.arange(start=1, end=last_hidden_state.shape[1] + 1)
|
|
58
|
+
.unsqueeze(0)
|
|
59
|
+
.unsqueeze(-1)
|
|
60
|
+
.expand(last_hidden_state.size())
|
|
61
|
+
.float()
|
|
62
|
+
.to(last_hidden_state.device)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Get attn mask of shape [bs, seq_len, hid_dim]
|
|
66
|
+
input_mask_expanded = (
|
|
67
|
+
batch_tokens["attention_mask"]
|
|
68
|
+
.unsqueeze(-1)
|
|
69
|
+
.expand(last_hidden_state.size())
|
|
70
|
+
.float()
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Perform weighted mean pooling across seq_len: bs, seq_len, hidden_dim -> bs, hidden_dim
|
|
74
|
+
sum_embeddings = torch.sum(
|
|
75
|
+
last_hidden_state * input_mask_expanded * weights, dim=1
|
|
76
|
+
)
|
|
77
|
+
sum_mask = torch.sum(input_mask_expanded * weights, dim=1)
|
|
78
|
+
|
|
79
|
+
embeddings = sum_embeddings / sum_mask
|
|
80
|
+
|
|
81
|
+
return embeddings
|
|
82
|
+
|
|
83
|
+
def calc_embedding(self, text, is_query: bool = False):
|
|
84
|
+
tokens = self.tokenize_with_specb([text], is_query=is_query)
|
|
85
|
+
return self.get_weightedmean_embedding(tokens)[0]
|