wordviz 0.1.1__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.
- wordviz-0.1.1/LICENSE +21 -0
- wordviz-0.1.1/PKG-INFO +82 -0
- wordviz-0.1.1/README.md +56 -0
- wordviz-0.1.1/pyproject.toml +31 -0
- wordviz-0.1.1/src/wordviz/__init__.py +0 -0
- wordviz-0.1.1/src/wordviz/clustering.py +44 -0
- wordviz-0.1.1/src/wordviz/dim_reduction.py +76 -0
- wordviz-0.1.1/src/wordviz/loading.py +269 -0
- wordviz-0.1.1/src/wordviz/plotting.py +488 -0
- wordviz-0.1.1/src/wordviz/similarity.py +193 -0
wordviz-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Elena Zen
|
|
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.
|
wordviz-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: wordviz
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A lightweight Python package for loading, analyzing, and visualizing word embeddings.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Elena Zen
|
|
7
|
+
Author-email: elenazen563@gmail.com
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: adjusttext (>=1.3.0,<2.0.0)
|
|
15
|
+
Requires-Dist: gensim (>=4.3.3,<5.0.0)
|
|
16
|
+
Requires-Dist: ipykernel (>=6.29.5,<7.0.0)
|
|
17
|
+
Requires-Dist: jupyter (>=1.1.1,<2.0.0)
|
|
18
|
+
Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
|
|
19
|
+
Requires-Dist: numpy (>=1.18.5,<2.0)
|
|
20
|
+
Requires-Dist: plotly (>=6.0.1,<7.0.0)
|
|
21
|
+
Requires-Dist: scikit-learn (>=1.6.1,<2.0.0)
|
|
22
|
+
Requires-Dist: scipy (>=1.7.0,<1.14.0)
|
|
23
|
+
Requires-Dist: seaborn (>=0.13.2,<0.14.0)
|
|
24
|
+
Requires-Dist: umap-learn (>=0.5.7,<0.6.0)
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# WordViz
|
|
28
|
+
|
|
29
|
+
**WordViz** is a Python visualization library designed for exploring and visualizing word embeddings. Built on top of popular libraries such as `matplotlib`, `plotly`, and `gensim`, WordViz provides intuitive tools for analyzing embeddings through clustering, similarity exploration, and dimensionality reduction, all wrapped in interactive and customizable plots.
|
|
30
|
+
With WordViz, users can gain insights into the structure of their word embeddings, making it a valuable tool for researchers and practitioners in natural language processing.
|
|
31
|
+
|
|
32
|
+
## Main Features
|
|
33
|
+
|
|
34
|
+
- Load and explore pretrained embeddings (e.g., GloVe, FastText)
|
|
35
|
+
- Select from a variety of available embeddings
|
|
36
|
+
- Visualize embeddings in 2D with flexible dimensionality reduction options
|
|
37
|
+
- Identify and plot the most similar words to a given token
|
|
38
|
+
- Visualize clusters of related words
|
|
39
|
+
- Interactive plots powered by `plotly`
|
|
40
|
+
- Support for both light and dark themes
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Install the latest version from PyPI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install wordviz
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
You can load and manage embeddings though the `EmbeddingLoader` class, and then visualize them with the `Visualizer` class.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from wordviz.loading import EmbeddingLoader
|
|
58
|
+
from wordviz.plotting import Visualizer
|
|
59
|
+
|
|
60
|
+
loader = EmbeddingLoader()
|
|
61
|
+
loader.load_from_file('path/to/your/embedding/file', 'word2vec')
|
|
62
|
+
|
|
63
|
+
vis = Visualizer(loader)
|
|
64
|
+
vis.plot_embeddings()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can explore all functionalities through the example notebook provided in the `docs/` folder:
|
|
68
|
+
|
|
69
|
+
👉 [View example notebook](docs/example.ipynb)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## Contributing
|
|
73
|
+
|
|
74
|
+
This project was created as part of my Bachelor's Degree thesis. For now, it remains a personal project and is not yet open to public collaboration.
|
|
75
|
+
However, it will be further developed and eventually opened to contributions.
|
|
76
|
+
|
|
77
|
+
In the meantime, if you want to suggest features or report bugs, feel free to contact me directly.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
This project is licensed under the MIT License.
|
wordviz-0.1.1/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# WordViz
|
|
2
|
+
|
|
3
|
+
**WordViz** is a Python visualization library designed for exploring and visualizing word embeddings. Built on top of popular libraries such as `matplotlib`, `plotly`, and `gensim`, WordViz provides intuitive tools for analyzing embeddings through clustering, similarity exploration, and dimensionality reduction, all wrapped in interactive and customizable plots.
|
|
4
|
+
With WordViz, users can gain insights into the structure of their word embeddings, making it a valuable tool for researchers and practitioners in natural language processing.
|
|
5
|
+
|
|
6
|
+
## Main Features
|
|
7
|
+
|
|
8
|
+
- Load and explore pretrained embeddings (e.g., GloVe, FastText)
|
|
9
|
+
- Select from a variety of available embeddings
|
|
10
|
+
- Visualize embeddings in 2D with flexible dimensionality reduction options
|
|
11
|
+
- Identify and plot the most similar words to a given token
|
|
12
|
+
- Visualize clusters of related words
|
|
13
|
+
- Interactive plots powered by `plotly`
|
|
14
|
+
- Support for both light and dark themes
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Install the latest version from PyPI:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install wordviz
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
You can load and manage embeddings though the `EmbeddingLoader` class, and then visualize them with the `Visualizer` class.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from wordviz.loading import EmbeddingLoader
|
|
32
|
+
from wordviz.plotting import Visualizer
|
|
33
|
+
|
|
34
|
+
loader = EmbeddingLoader()
|
|
35
|
+
loader.load_from_file('path/to/your/embedding/file', 'word2vec')
|
|
36
|
+
|
|
37
|
+
vis = Visualizer(loader)
|
|
38
|
+
vis.plot_embeddings()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You can explore all functionalities through the example notebook provided in the `docs/` folder:
|
|
42
|
+
|
|
43
|
+
👉 [View example notebook](docs/example.ipynb)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Contributing
|
|
47
|
+
|
|
48
|
+
This project was created as part of my Bachelor's Degree thesis. For now, it remains a personal project and is not yet open to public collaboration.
|
|
49
|
+
However, it will be further developed and eventually opened to contributions.
|
|
50
|
+
|
|
51
|
+
In the meantime, if you want to suggest features or report bugs, feel free to contact me directly.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "wordviz"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "A lightweight Python package for loading, analyzing, and visualizing word embeddings."
|
|
5
|
+
authors = ["Elena Zen <elenazen563@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = ">=3.11"
|
|
11
|
+
matplotlib = ">=3.10.1,<4.0.0"
|
|
12
|
+
numpy = ">=1.18.5,<2.0"
|
|
13
|
+
gensim = ">=4.3.3,<5.0.0"
|
|
14
|
+
scipy = ">=1.7.0,<1.14.0"
|
|
15
|
+
scikit-learn = ">=1.6.1,<2.0.0"
|
|
16
|
+
plotly = ">=6.0.1,<7.0.0"
|
|
17
|
+
seaborn = ">=0.13.2,<0.14.0"
|
|
18
|
+
adjusttext = "^1.3.0"
|
|
19
|
+
umap-learn = ">=0.5.7,<0.6.0"
|
|
20
|
+
ipykernel = ">=6.29.5,<7.0.0"
|
|
21
|
+
jupyter = ">=1.1.1,<2.0.0"
|
|
22
|
+
|
|
23
|
+
[tool.poetry.group.dev.dependencies]
|
|
24
|
+
pytest = "^8.3.5"
|
|
25
|
+
black = "^25.1.0"
|
|
26
|
+
isort = "^6.0.1"
|
|
27
|
+
sphinx = "^8.2.3"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
31
|
+
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from sklearn.cluster import KMeans, DBSCAN
|
|
3
|
+
from typing import Tuple, Optional
|
|
4
|
+
from .dim_reduction import reduce_dim
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_clusters(vectors: np.ndarray, n_clusters: int = 5, method: str = 'kmeans') -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]:
|
|
8
|
+
'''
|
|
9
|
+
Performs clustering on embeddings for visualization purposes.
|
|
10
|
+
If the input vectors have more than 2 dimensions, dimensionality reduction is applied first.
|
|
11
|
+
|
|
12
|
+
Parameters:
|
|
13
|
+
-----------
|
|
14
|
+
vectors : np.ndarray
|
|
15
|
+
Array of embeddings to cluster.
|
|
16
|
+
n_clusters : int, default=5
|
|
17
|
+
Number of clusters to generate (used only for k-means).
|
|
18
|
+
method : str, default='kmeans'
|
|
19
|
+
Clustering method to use ('kmeans' or 'dbscan').
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
--------
|
|
23
|
+
labels : np.ndarray
|
|
24
|
+
Cluster labels assigned to each vector.
|
|
25
|
+
centers : np.ndarray or None
|
|
26
|
+
Coordinates of cluster centers (only for k-means; None for dbscan).
|
|
27
|
+
reduced_emb : np.ndarray
|
|
28
|
+
2D reduced embeddings used for clustering and plotting.
|
|
29
|
+
'''
|
|
30
|
+
|
|
31
|
+
if vectors.shape[1] > 2:
|
|
32
|
+
reduced_emb = reduce_dim(vectors)
|
|
33
|
+
else:
|
|
34
|
+
reduced_emb = vectors
|
|
35
|
+
|
|
36
|
+
match method:
|
|
37
|
+
case 'kmeans':
|
|
38
|
+
clustering = KMeans(n_clusters=n_clusters, random_state=0, n_init="auto").fit(reduced_emb)
|
|
39
|
+
centers = clustering.cluster_centers_
|
|
40
|
+
case 'dbscan':
|
|
41
|
+
clustering = DBSCAN(eps=0.5, min_samples=n_clusters).fit(reduced_emb)
|
|
42
|
+
centers = None
|
|
43
|
+
|
|
44
|
+
return clustering.labels_, centers, reduced_emb
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import umap
|
|
2
|
+
import numpy as np
|
|
3
|
+
from sklearn.decomposition import PCA
|
|
4
|
+
from sklearn.manifold import TSNE
|
|
5
|
+
from sklearn.manifold import Isomap
|
|
6
|
+
from sklearn.manifold import MDS
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
def reduce_dim(vectors: np.ndarray, method: str = 'pca', n_dimensions: int = 2, dist: str = 'euclidean', **kwargs) -> np.ndarray:
|
|
10
|
+
'''
|
|
11
|
+
Applies dimensionality reduction for visualization to input vectors using a specified method.
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
-----------
|
|
15
|
+
vectors: array
|
|
16
|
+
Input high-dimensional data to reduce.
|
|
17
|
+
method: str, default:'pca'
|
|
18
|
+
Dimensionality reduction algorithm to apply. Options are:
|
|
19
|
+
- 'pca': Principal Component Analysis
|
|
20
|
+
- 'tsne': t-Distributed Stochastic Neighbor Embedding
|
|
21
|
+
- 'umap': Uniformed Manifold Approximation and Projection
|
|
22
|
+
- 'isomap': Isometric Mapping
|
|
23
|
+
- 'mds': Multidimensional Scaling
|
|
24
|
+
n_dimensions: int, default:2
|
|
25
|
+
Number of dimensions for output. Must be 2 or 3
|
|
26
|
+
dist : str, default='euclidean'
|
|
27
|
+
Distance metric for methods that require a distance matrix (MDS). Ignored for other methods.
|
|
28
|
+
**kwargs : dict
|
|
29
|
+
Additional parameters passed to the selected dimensionality reduction algorithm.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
--------
|
|
33
|
+
np.ndarray
|
|
34
|
+
Embedding reduced at specified dimensions.
|
|
35
|
+
'''
|
|
36
|
+
|
|
37
|
+
if n_dimensions not in [2, 3]:
|
|
38
|
+
raise ValueError('n_dimensions has to be 2 or 3')
|
|
39
|
+
|
|
40
|
+
if (method in ['isomap', 'tsne', 'mds']) and vectors.shape[0] > 5000:
|
|
41
|
+
warnings.warn(f"Warning: {method} is a computational heavy method, loading very large embeddings without subsetting may result in execution times so long that the process may never complete.")
|
|
42
|
+
|
|
43
|
+
default_params = {
|
|
44
|
+
'pca': {},
|
|
45
|
+
'tsne': {'random_state': 42, 'perplexity': 30},
|
|
46
|
+
'umap': {'n_neighbors': 15, 'min_dist': 0.1, 'random_state': 42},
|
|
47
|
+
'isomap': {'n_neighbors': 5},
|
|
48
|
+
'mds': {'metric':True, 'n_init':3, 'max_iter':300, 'random_state':42, 'dissimilarity': 'euclidean'}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if method in default_params:
|
|
52
|
+
params = {**default_params[method.lower()], **kwargs}
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError(f"Method {method} not supported. Choose between 'pca', 'tsne', 'umap' or 'isomap'")
|
|
55
|
+
|
|
56
|
+
match method.lower():
|
|
57
|
+
case 'pca':
|
|
58
|
+
reducer = PCA(n_components=n_dimensions, **params)
|
|
59
|
+
case 'tsne':
|
|
60
|
+
reducer = TSNE(n_components=n_dimensions, **params)
|
|
61
|
+
case 'umap':
|
|
62
|
+
reducer = umap.UMAP(n_components=n_dimensions, **params)
|
|
63
|
+
case 'isomap':
|
|
64
|
+
reducer = Isomap(n_components=n_dimensions, **params)
|
|
65
|
+
case 'mds':
|
|
66
|
+
reducer = MDS(n_components=n_dimensions,
|
|
67
|
+
dissimilarity='euclidean' if dist == 'euclidean' else 'precomputed', **params)
|
|
68
|
+
if dist != 'precomputed' and dist != 'euclidean':
|
|
69
|
+
from sklearn.metrics import pairwise_distances
|
|
70
|
+
vectors = pairwise_distances(vectors, metric=dist)
|
|
71
|
+
|
|
72
|
+
reduced_emb = reducer.fit_transform(vectors)
|
|
73
|
+
|
|
74
|
+
return reduced_emb
|
|
75
|
+
|
|
76
|
+
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from gensim.models import KeyedVectors
|
|
4
|
+
from gensim.scripts.glove2word2vec import glove2word2vec
|
|
5
|
+
from gensim.models.fasttext import load_facebook_model
|
|
6
|
+
import numpy as np
|
|
7
|
+
import zipfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EmbeddingLoader:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.embeddings_raw = None
|
|
15
|
+
self.embeddings = None
|
|
16
|
+
self.tokens = None
|
|
17
|
+
self.dimension = None
|
|
18
|
+
self.embeddings_subset = None
|
|
19
|
+
self.tokens_subset = None
|
|
20
|
+
|
|
21
|
+
self.available_pretrained = {
|
|
22
|
+
'glove': {
|
|
23
|
+
'en': {
|
|
24
|
+
'wiki': {
|
|
25
|
+
'100d': {
|
|
26
|
+
'url': 'https://nlp.stanford.edu/data/glove.6B.zip',
|
|
27
|
+
'filename': 'glove.6B.100d.txt'
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
'cc': {
|
|
31
|
+
'300d': {
|
|
32
|
+
'url': 'https://nlp.stanford.edu/data/glove.42B.300d.zip',
|
|
33
|
+
'filename': 'glove.42B.300d.txt'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
'twitter': {
|
|
37
|
+
'100d': {
|
|
38
|
+
'url': 'https://nlp.stanford.edu/data/glove.twitter.27B.zip',
|
|
39
|
+
'filename': 'glove.twitter.27B.100d.txt'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
'fasttext': {
|
|
45
|
+
'it': {
|
|
46
|
+
'cc': {
|
|
47
|
+
'300d': {
|
|
48
|
+
'url': 'https://dl.fbaipublicfiles.com/fasttext/vectors-crawl/cc.it.300.bin.gz'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def get_cache_dir(self):
|
|
56
|
+
cache_dir = Path.home() / ".wordviz_cache"
|
|
57
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
return cache_dir
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _validate_file(self, path):
|
|
62
|
+
'''checks if path argument leads to a valid file name and returns if it is binary'''
|
|
63
|
+
valid_ext = ['.bin', '.txt', '.vec']
|
|
64
|
+
|
|
65
|
+
if not isinstance(path, str):
|
|
66
|
+
path = str(path)
|
|
67
|
+
_, ext = os.path.splitext(path.lower())
|
|
68
|
+
|
|
69
|
+
if path is None:
|
|
70
|
+
raise ValueError('File path is required')
|
|
71
|
+
|
|
72
|
+
if not isinstance(path, str):
|
|
73
|
+
raise TypeError('The file path must be a string')
|
|
74
|
+
|
|
75
|
+
if not os.path.exists(path):
|
|
76
|
+
raise FileNotFoundError(f"Invalid file path {path}: the file does not exist")
|
|
77
|
+
|
|
78
|
+
if ext not in valid_ext:
|
|
79
|
+
raise ValueError(f'Invalid file extension {ext}. Valid extensions are: {','.join(valid_ext)}')
|
|
80
|
+
|
|
81
|
+
binary = True if ext == '.bin' else False
|
|
82
|
+
|
|
83
|
+
return binary
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def load_from_file(self, path: str, format: str) -> np.ndarray:
|
|
87
|
+
'''
|
|
88
|
+
Loads word embeddings from a file in .txt, .vec, or .bin format.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
-----------
|
|
92
|
+
path : str
|
|
93
|
+
Path to the embedding file.
|
|
94
|
+
format : str
|
|
95
|
+
Format of the embedding model: 'word2vec', 'fasttext', or 'glove'.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
--------
|
|
99
|
+
np.ndarray
|
|
100
|
+
Loaded embedding matrix.
|
|
101
|
+
|
|
102
|
+
Notes:
|
|
103
|
+
------
|
|
104
|
+
- For GloVe files, they are first converted to word2vec format.
|
|
105
|
+
- FastText binary files are supported via Facebook's native loader.
|
|
106
|
+
- Loaded tokens are stored in self.tokens.
|
|
107
|
+
- Embedding matrix is stored in self.embeddings.
|
|
108
|
+
'''
|
|
109
|
+
|
|
110
|
+
binary = self._validate_file(path)
|
|
111
|
+
|
|
112
|
+
match format:
|
|
113
|
+
case 'word2vec':
|
|
114
|
+
self.embeddings_raw = KeyedVectors.load_word2vec_format(path, binary=binary)
|
|
115
|
+
case 'fasttext':
|
|
116
|
+
if binary:
|
|
117
|
+
self.embeddings_raw = load_facebook_model(path)
|
|
118
|
+
else:
|
|
119
|
+
self.embeddings_raw = KeyedVectors.load_word2vec_format(path, binary=False)
|
|
120
|
+
case 'glove':
|
|
121
|
+
glove2word2vec(path, "glove_w2v.txt")
|
|
122
|
+
if not os.path.exists("glove_w2v.txt"):
|
|
123
|
+
raise RuntimeError("GloVe to Word2Vec conversion failed.")
|
|
124
|
+
|
|
125
|
+
self.embeddings_raw = KeyedVectors.load_word2vec_format("glove_w2v.txt")
|
|
126
|
+
|
|
127
|
+
self.tokens = list(self.embeddings_raw.index_to_key)
|
|
128
|
+
self.dimension = self.embeddings_raw.vector_size
|
|
129
|
+
|
|
130
|
+
words = self.embeddings_raw.index_to_key
|
|
131
|
+
self.embeddings = np.array([self.embeddings_raw.get_vector(word) for word in words])
|
|
132
|
+
print("Embedding loaded from file")
|
|
133
|
+
|
|
134
|
+
return self.embeddings
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def download_zip(self, url, filename):
|
|
138
|
+
'''downloads zip file from url'''
|
|
139
|
+
zip_path = self.get_cache_dir() / filename
|
|
140
|
+
if not zip_path.exists():
|
|
141
|
+
print(f"Downloading {filename}...")
|
|
142
|
+
urllib.request.urlretrieve(url, zip_path)
|
|
143
|
+
else:
|
|
144
|
+
print(f"{filename} already exists in cache.")
|
|
145
|
+
return zip_path
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def export_embedding(self, source_path, dest_folder):
|
|
149
|
+
'''saves locally pretrained embeddings file'''
|
|
150
|
+
os.makedirs(dest_folder, exist_ok=True)
|
|
151
|
+
filename = os.path.basename(source_path)
|
|
152
|
+
dest_path = os.path.join(dest_folder, filename)
|
|
153
|
+
shutil.copy(source_path, dest_path)
|
|
154
|
+
print(f"File saved in {dest_path}.")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def load_pretrained(self, model: str, lang: str, source: str, dimension: str, save_file: bool = False, export_dir: str = None) -> np.ndarray:
|
|
158
|
+
'''
|
|
159
|
+
Downloads and loads a pretrained embedding model from an online source.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
-----------
|
|
163
|
+
model : str
|
|
164
|
+
Name of the embedding model ('word2vec', 'fasttext', etc.).
|
|
165
|
+
lang : str
|
|
166
|
+
Language code of the embedding ('en', 'it').
|
|
167
|
+
source : str
|
|
168
|
+
Data source ('wiki', 'cc').
|
|
169
|
+
dimension : str or int
|
|
170
|
+
Embedding dimensionality (e.g., '300').
|
|
171
|
+
save_file : bool, default=False
|
|
172
|
+
If True, saves the embedding to the specified export directory.
|
|
173
|
+
export_dir : str, optional
|
|
174
|
+
Path to the directory where the file will be exported (used if save_file=True).
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
--------
|
|
178
|
+
np.ndarray
|
|
179
|
+
Loaded embedding matrix (n_words x dimension).
|
|
180
|
+
'''
|
|
181
|
+
|
|
182
|
+
option = self.available_pretrained[model][lang][source][dimension]
|
|
183
|
+
url = option['url']
|
|
184
|
+
filename = option['filename']
|
|
185
|
+
zip_filename = url.split("/")[-1]
|
|
186
|
+
|
|
187
|
+
zip_path = self.download_zip(url, zip_filename)
|
|
188
|
+
|
|
189
|
+
dest_dir = self.get_cache_dir() / model / lang / source / dimension
|
|
190
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
file_path = dest_dir / filename
|
|
192
|
+
|
|
193
|
+
if not file_path.exists():
|
|
194
|
+
with zipfile.ZipFile(zip_path, 'r') as z:
|
|
195
|
+
print(f"Extracting {filename}...")
|
|
196
|
+
z.extract(filename, path=dest_dir)
|
|
197
|
+
|
|
198
|
+
self.embeddings = self.load_from_file(file_path, model)
|
|
199
|
+
|
|
200
|
+
if save_file:
|
|
201
|
+
if export_dir is None:
|
|
202
|
+
raise ValueError("Must specify export_dir to save file.")
|
|
203
|
+
self.export_embedding(file_path, export_dir)
|
|
204
|
+
|
|
205
|
+
return self.embeddings
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def list_available_pretrained(self):
|
|
209
|
+
'''prints a list of pretrained embeddings provided by the package'''
|
|
210
|
+
print('model | lang | source | dim')
|
|
211
|
+
for model, langs in self.available_pretrained.items():
|
|
212
|
+
for lang, sources in langs.items():
|
|
213
|
+
for source, dims in sources.items():
|
|
214
|
+
for dim in dims:
|
|
215
|
+
print(f"{model} | {lang} | {source} | {dim}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_embedding(self, word):
|
|
219
|
+
'''returns corresponding embeddings using KeyedVectors object for a string given by the user'''
|
|
220
|
+
if isinstance(word, str):
|
|
221
|
+
if word in self.embeddings_raw:
|
|
222
|
+
return self.embeddings_raw[word]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def subset(self, n: int = 1000, strategy: str = 'first', random_seed: int = None):
|
|
226
|
+
'''
|
|
227
|
+
Create a subset of the current embeddings and tokens. Useful for speeding up visualizations or
|
|
228
|
+
managing memory with large embedding spaces.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
-----------
|
|
232
|
+
n : int, default=1000
|
|
233
|
+
Number of embeddings to retain. If n exceeds the total number of available embeddings, all are retained.
|
|
234
|
+
strategy : str, default='first'
|
|
235
|
+
Selection strategy:
|
|
236
|
+
- 'first': select the first n embeddings in original order.
|
|
237
|
+
- 'random': select n random embeddings.
|
|
238
|
+
random_seed : int, optional
|
|
239
|
+
Seed for reproducible random sampling (only used if strategy is 'random').
|
|
240
|
+
|
|
241
|
+
Updates
|
|
242
|
+
--------
|
|
243
|
+
self.tokens_subset : list of str
|
|
244
|
+
List of selected token strings.
|
|
245
|
+
self.embeddings_subset : np.ndarray
|
|
246
|
+
Corresponding selected embedding vectors.
|
|
247
|
+
'''
|
|
248
|
+
if n > len(self.tokens):
|
|
249
|
+
print('n is larger than the embedding size, the subset size will be equal to the full size')
|
|
250
|
+
|
|
251
|
+
if strategy == 'first':
|
|
252
|
+
indices = list(range(min(n, len(self.tokens))))
|
|
253
|
+
elif strategy == 'random':
|
|
254
|
+
rng = np.random.default_rng(random_seed)
|
|
255
|
+
indices = rng.choice(len(self.tokens), size=min(n, len(self.tokens)), replace=False).tolist()
|
|
256
|
+
else:
|
|
257
|
+
raise ValueError("strategy has to be 'first' o 'random'")
|
|
258
|
+
|
|
259
|
+
self.tokens_subset = [self.tokens[i] for i in indices]
|
|
260
|
+
self.embeddings_subset = self.embeddings[indices]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def use_subset(self, n: int = 1000):
|
|
264
|
+
'''returns embedding subset. If None, creates 1000 words subset and returns it.'''
|
|
265
|
+
|
|
266
|
+
if self.embeddings_subset is None:
|
|
267
|
+
self.subset(n)
|
|
268
|
+
|
|
269
|
+
return self.embeddings_subset, self.tokens_subset
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
from adjustText import adjust_text
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import numpy as np
|
|
4
|
+
import plotly.express as px
|
|
5
|
+
import plotly.graph_objects as go
|
|
6
|
+
import seaborn as sns
|
|
7
|
+
from sklearn.cluster import KMeans
|
|
8
|
+
from sklearn.metrics import pairwise_distances
|
|
9
|
+
from scipy.stats import gaussian_kde
|
|
10
|
+
import warnings
|
|
11
|
+
from .clustering import create_clusters
|
|
12
|
+
from .dim_reduction import reduce_dim
|
|
13
|
+
from .similarity import n_most_similar
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Visualizer:
|
|
17
|
+
def __init__(self, loader):
|
|
18
|
+
self.loader = loader
|
|
19
|
+
self.tokens = loader.tokens
|
|
20
|
+
self.embeddings = loader.embeddings
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_theme(self, theme='light1'):
|
|
24
|
+
themes = {
|
|
25
|
+
'light1': {
|
|
26
|
+
'bg' : '#f5f5fa',
|
|
27
|
+
'points' : '#66c2a5',
|
|
28
|
+
'target' : '#e78ac3',
|
|
29
|
+
'grid' : '#cccccc',
|
|
30
|
+
'text' : '#1a1a1a',
|
|
31
|
+
'scale': 'Viridis'
|
|
32
|
+
},
|
|
33
|
+
'dark1': {
|
|
34
|
+
'bg' : '#1a1a1a',
|
|
35
|
+
'points' : '#fc8d62',
|
|
36
|
+
'target' : '#a6d854',
|
|
37
|
+
'grid' : '#444444',
|
|
38
|
+
'text' : '#f0f0f0',
|
|
39
|
+
'scale': 'Plasma'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return themes.get(theme, themes['light1'])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _setup_plot(self, theme, grid, title):
|
|
46
|
+
'''base private function to config matplotlib plot'''
|
|
47
|
+
colors = self.get_theme(theme)
|
|
48
|
+
|
|
49
|
+
fig, ax = plt.subplots(figsize=(10, 8))
|
|
50
|
+
|
|
51
|
+
fig.patch.set_facecolor(colors['bg'])
|
|
52
|
+
ax.set_facecolor(colors['bg'])
|
|
53
|
+
|
|
54
|
+
if grid:
|
|
55
|
+
ax.grid(True, linestyle='--', color=colors['grid'], alpha=0.6)
|
|
56
|
+
ax.set_axisbelow(True)
|
|
57
|
+
ax.tick_params(left=True, bottom=True, labelleft=False, labelbottom=False, color=(0.5, 0.5, 0.5, 0.4))
|
|
58
|
+
else:
|
|
59
|
+
plt.xticks([])
|
|
60
|
+
plt.yticks([])
|
|
61
|
+
|
|
62
|
+
for spine in ax.spines.values():
|
|
63
|
+
spine.set_visible(False)
|
|
64
|
+
|
|
65
|
+
if title is not None:
|
|
66
|
+
plt.title(title, fontsize=12, fontweight='bold', color=colors['text'])
|
|
67
|
+
|
|
68
|
+
return fig, ax, colors
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def map_colors(self, labels):
|
|
72
|
+
'''automatizes color and legend label mapping for clustering applied to embeddings'''
|
|
73
|
+
unique_classes = list(set(labels))
|
|
74
|
+
palette = sns.color_palette("Set2", n_colors=len(unique_classes))
|
|
75
|
+
class_to_color = dict(zip(unique_classes, palette))
|
|
76
|
+
colors = [class_to_color[label] for label in labels]
|
|
77
|
+
legend_labels = {label: (class_to_color[label], f'Cluster {label+1}') for label in unique_classes}
|
|
78
|
+
|
|
79
|
+
return colors, legend_labels
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def select_sparse_labels(self, embeddings, n):
|
|
83
|
+
'''uses clustering to select n distributed labels to visualize'''
|
|
84
|
+
kmeans = KMeans(n_clusters=n, random_state=0).fit(embeddings)
|
|
85
|
+
centers = kmeans.cluster_centers_
|
|
86
|
+
indices = []
|
|
87
|
+
|
|
88
|
+
for center in centers:
|
|
89
|
+
idx = np.argmin(np.linalg.norm(embeddings - center, axis=1))
|
|
90
|
+
indices.append(idx)
|
|
91
|
+
|
|
92
|
+
return indices
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def plot_embeddings(self, red_method: str = 'pca', grid: bool = True, theme: str = 'light1', title: str = None, nlabels: int = 0, use_subset: bool = False):
|
|
96
|
+
'''
|
|
97
|
+
Creates a simple static 2D scatterplot of the embeddings.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
-----------
|
|
101
|
+
red_method : str, default='pca'
|
|
102
|
+
Dimensionality reduction method to apply ('pca', 'tsne', 'umap', etc.).
|
|
103
|
+
grid : bool, default=True
|
|
104
|
+
If True, displays a background grid on the plot.
|
|
105
|
+
theme : str, default='light1'
|
|
106
|
+
Color theme to apply.
|
|
107
|
+
title : str, optional
|
|
108
|
+
Title to display on the plot.
|
|
109
|
+
nlabels : int, default=0
|
|
110
|
+
Number of word labels to display. If 0, no labels are shown.
|
|
111
|
+
use_subset : bool, default=False
|
|
112
|
+
If True, uses the embedding subset instead of the full embeddings.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
--------
|
|
116
|
+
fig : matplotlib.figure.Figure
|
|
117
|
+
ax : matplotlib.axes.Axes
|
|
118
|
+
'''
|
|
119
|
+
|
|
120
|
+
if use_subset:
|
|
121
|
+
emb, tokens = self.loader.use_subset()
|
|
122
|
+
else:
|
|
123
|
+
emb = self.embeddings
|
|
124
|
+
tokens = self.tokens
|
|
125
|
+
|
|
126
|
+
reduced_emb = reduce_dim(emb, method=red_method)
|
|
127
|
+
|
|
128
|
+
fig, ax, colors = self._setup_plot(theme, grid, title)
|
|
129
|
+
ax.scatter(reduced_emb[:, 0], reduced_emb[:, 1], c=colors['points'], alpha=0.5, s=14, marker='o')
|
|
130
|
+
|
|
131
|
+
texts = []
|
|
132
|
+
if nlabels > 0:
|
|
133
|
+
sparse_indices = self.select_sparse_labels(reduced_emb, nlabels)
|
|
134
|
+
for i in sparse_indices:
|
|
135
|
+
texts.append(ax.text(reduced_emb[i, 0], reduced_emb[i, 1], tokens[i],
|
|
136
|
+
color=colors['text'], fontsize=9, alpha=1, ha='center', va='bottom'))
|
|
137
|
+
|
|
138
|
+
adjust_text(texts, ax=ax, expand=(1.2, 2), arrowprops=dict(arrowstyle='-', color='k'))
|
|
139
|
+
plt.rcParams['figure.dpi'] = 600
|
|
140
|
+
plt.show()
|
|
141
|
+
return fig, ax
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def plot_similarity(self, target_word: str, dist: str = 'cosine', n: int = 10, red_method: str = 'umap', grid: bool = True, theme: str = 'light1', title: str = None):
|
|
145
|
+
'''
|
|
146
|
+
Creates a scatterplot showing the most similar words to a target word.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
-----------
|
|
150
|
+
target_word : str
|
|
151
|
+
The word for which to find and plot the most similar words.
|
|
152
|
+
dist : str, default='cosine'
|
|
153
|
+
Distance metric to use when computing word similarity.
|
|
154
|
+
n : int, default=10
|
|
155
|
+
Number of similar words to display.
|
|
156
|
+
red_method : str, default='umap'
|
|
157
|
+
Dimensionality reduction method to apply ('pca', 'tsne', 'umap', etc.).
|
|
158
|
+
grid : bool, default=True
|
|
159
|
+
If True, displays a background grid on the plot.
|
|
160
|
+
theme : str, default='light1'
|
|
161
|
+
Color theme to apply to the plot.
|
|
162
|
+
title : str, optional
|
|
163
|
+
Title to display. If None, a default title will be generated.
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
--------
|
|
167
|
+
fig : matplotlib.figure.Figure
|
|
168
|
+
ax : matplotlib.axes.Axes
|
|
169
|
+
'''
|
|
170
|
+
similar_words, similar_vecs, _ = n_most_similar(self.loader, target_word, dist, n)
|
|
171
|
+
target_vec = self.loader.get_embedding(target_word)
|
|
172
|
+
vectors = np.vstack([target_vec.reshape(1, -1), similar_vecs])
|
|
173
|
+
words = [target_word] + similar_words
|
|
174
|
+
|
|
175
|
+
reduced_emb = reduce_dim(vectors, method=red_method)
|
|
176
|
+
|
|
177
|
+
if title is None:
|
|
178
|
+
title = f"Top {n} words similar to '{target_word}'"
|
|
179
|
+
|
|
180
|
+
fig, ax, colors = self._setup_plot(theme, grid, title)
|
|
181
|
+
|
|
182
|
+
texts = []
|
|
183
|
+
ax.scatter(reduced_emb[0, 0], reduced_emb[0, 1], c=colors['target'], alpha=0.5, s=20, marker='o')
|
|
184
|
+
texts.append(ax.text(reduced_emb[0, 0], reduced_emb[0, 1], target_word,
|
|
185
|
+
color=colors['text'], fontsize=9, fontweight='bold', alpha=1, ha='center', va='bottom'))
|
|
186
|
+
|
|
187
|
+
ax.scatter(reduced_emb[1:, 0], reduced_emb[1:, 1], c=colors['points'], alpha=0.5, s=20, marker='o')
|
|
188
|
+
for i, word in enumerate(similar_words):
|
|
189
|
+
texts.append(ax.text(reduced_emb[i+1, 0], reduced_emb[i+1, 1], word,
|
|
190
|
+
color=colors['text'], fontsize=9, alpha=1, ha='center', va='bottom'))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
plt.rcParams['figure.dpi'] = 600
|
|
194
|
+
plt.show()
|
|
195
|
+
|
|
196
|
+
return fig, ax
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def plot_topography(self, dist: str = 'cosine', red_method: str = 'isomap', use_subset: bool = True, grid: bool = True, theme: str = 'light1', title: str = None):
|
|
200
|
+
'''
|
|
201
|
+
Plots word embeddings in a topographical map using dimensionality reduction to maintain word distances in the representation. Allows to visualize word density in the space.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
-----------
|
|
205
|
+
dist : str, default='cosine'
|
|
206
|
+
The distance metric to use for word similarity. Options include 'cosine', 'euclidean', etc.
|
|
207
|
+
red_method : str, default='isomap'
|
|
208
|
+
The dimensionality reduction method to use for visualizing the word embeddings, maintaining word distances. Options include 'umap', 'isomap', and 'mds'.
|
|
209
|
+
use_subset : bool, default=True
|
|
210
|
+
If True, uses a subset of the embeddings for visualization. This is recommended in this plot for larger embeddings.
|
|
211
|
+
grid : bool, default=True
|
|
212
|
+
If True, shows grid lines on the plot.
|
|
213
|
+
theme : str, default='light1'
|
|
214
|
+
The plot theme to use, which controls the colors of the plot.
|
|
215
|
+
title : str, optional
|
|
216
|
+
Title of the plot. If not provided, a default title is used.
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
--------
|
|
220
|
+
fig : plotly.graph_objs.Figure
|
|
221
|
+
'''
|
|
222
|
+
|
|
223
|
+
if use_subset:
|
|
224
|
+
emb, tokens = self.loader.use_subset()
|
|
225
|
+
else:
|
|
226
|
+
emb = self.embeddings
|
|
227
|
+
tokens = self.tokens
|
|
228
|
+
|
|
229
|
+
reduced_emb = reduce_dim(emb, red_method, dist=dist)
|
|
230
|
+
|
|
231
|
+
x = reduced_emb[:, 0]
|
|
232
|
+
y = reduced_emb[:, 1]
|
|
233
|
+
fig = go.Figure()
|
|
234
|
+
|
|
235
|
+
colors = self.get_theme(theme)
|
|
236
|
+
|
|
237
|
+
# calculate coordinates for contour plot
|
|
238
|
+
x_grid, y_grid = np.meshgrid(np.linspace(x.min() - 0.5, x.max() + 0.5, 100),
|
|
239
|
+
np.linspace(y.min() - 0.5, y.max() + 0.5, 100))
|
|
240
|
+
|
|
241
|
+
kde = gaussian_kde([x, y], bw_method=0.2)
|
|
242
|
+
z_grid = kde([x_grid.flatten(), y_grid.flatten()]).reshape(x_grid.shape)
|
|
243
|
+
z_grid = np.log1p(z_grid)
|
|
244
|
+
|
|
245
|
+
# add contour
|
|
246
|
+
fig.add_trace(go.Contour(
|
|
247
|
+
z=z_grid,
|
|
248
|
+
x=x_grid[0],
|
|
249
|
+
y=y_grid[:, 0],
|
|
250
|
+
colorscale=colors['scale'],
|
|
251
|
+
opacity=0.8,
|
|
252
|
+
contours=dict(
|
|
253
|
+
showlabels=False,
|
|
254
|
+
start=z_grid.min(),
|
|
255
|
+
end=z_grid.max(),
|
|
256
|
+
size=(z_grid.max() - z_grid.min()) / 15),
|
|
257
|
+
colorbar=dict(title="Density"),
|
|
258
|
+
hoverinfo='skip'
|
|
259
|
+
))
|
|
260
|
+
|
|
261
|
+
# add points
|
|
262
|
+
fig.add_trace(go.Scatter(
|
|
263
|
+
x=x, y=y,
|
|
264
|
+
mode='markers',
|
|
265
|
+
marker=dict(
|
|
266
|
+
size=5,
|
|
267
|
+
color='rgba(255, 255, 255, 0.5)',
|
|
268
|
+
line=dict(width=1, color='rgba(0, 0, 0, 0.8)')
|
|
269
|
+
),
|
|
270
|
+
text=tokens,
|
|
271
|
+
hovertemplate='%{text}<extra></extra>',
|
|
272
|
+
showlegend=False
|
|
273
|
+
))
|
|
274
|
+
|
|
275
|
+
fig.update_traces(
|
|
276
|
+
hoverlabel=dict(
|
|
277
|
+
bgcolor=colors['bg'],
|
|
278
|
+
font=dict(color=colors['text'])
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
fig.update_layout(
|
|
283
|
+
width=900,
|
|
284
|
+
height=700,
|
|
285
|
+
title=title if title else "Word Embedding Topography",
|
|
286
|
+
title_x=0.5,
|
|
287
|
+
title_xanchor='center',
|
|
288
|
+
plot_bgcolor=colors['bg'],
|
|
289
|
+
paper_bgcolor=colors['bg'],
|
|
290
|
+
font=dict(color=colors['text']),
|
|
291
|
+
xaxis=dict(showgrid=grid, zeroline=False, showticklabels=False, title=""),
|
|
292
|
+
yaxis=dict(showgrid=grid, zeroline=False, showticklabels=False, title="")
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return fig
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def similarity_heatmap(self, dist: str = 'cosine', use_subset: bool = True, n: int = 500, theme: str = 'light1', title: bool = None):
|
|
299
|
+
'''
|
|
300
|
+
Creates a heatmap showing pairwise distances between word embeddings.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
-----------
|
|
304
|
+
dist : str, default='cosine'
|
|
305
|
+
Distance metric to use for computing similarity between embeddings.
|
|
306
|
+
use_subset : bool, default=True
|
|
307
|
+
If True, uses a subset of the embeddings. Otherwise, uses the full set.
|
|
308
|
+
n : int, optional
|
|
309
|
+
Number of embeddings to subset. Ignored if a subset already exists and use_subset is True.
|
|
310
|
+
theme : str, default='light1'
|
|
311
|
+
Plot color theme to use.
|
|
312
|
+
title : str, optional
|
|
313
|
+
Title for the heatmap. If None, a default title is assigned.
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
--------
|
|
317
|
+
fig : plotly.graph_objects.Figure
|
|
318
|
+
'''
|
|
319
|
+
|
|
320
|
+
if use_subset:
|
|
321
|
+
if n:
|
|
322
|
+
self.loader.subset(n)
|
|
323
|
+
emb = self.loader.embeddings_subset
|
|
324
|
+
tokens = self.loader.tokens_subset
|
|
325
|
+
else:
|
|
326
|
+
emb, tokens = self.loader.use_subset()
|
|
327
|
+
else:
|
|
328
|
+
emb = self.embeddings
|
|
329
|
+
tokens = self.tokens
|
|
330
|
+
|
|
331
|
+
if emb.shape[0] > 500:
|
|
332
|
+
warnings.warn(f"Warning: loading more than 500 embeddings without subsetting will generate more than one heatmap and may result in longer execution times. Consider subsetting before or setting n < 500.")
|
|
333
|
+
|
|
334
|
+
distances = pairwise_distances(emb, metric=dist)
|
|
335
|
+
|
|
336
|
+
colors = self.get_theme(theme)
|
|
337
|
+
|
|
338
|
+
fig = px.imshow(distances, x=tokens, y=tokens, text_auto=True, color_continuous_scale=colors['scale'])
|
|
339
|
+
fig.update_layout(
|
|
340
|
+
width=800, height=800,
|
|
341
|
+
title=title if title else "Word Embedding Similarity Heatmap",
|
|
342
|
+
title_x=0.5,
|
|
343
|
+
title_xanchor='center',
|
|
344
|
+
plot_bgcolor=colors['bg'],
|
|
345
|
+
paper_bgcolor=colors['bg'],
|
|
346
|
+
font=dict(color=colors['text']))
|
|
347
|
+
fig.update_coloraxes(colorbar_title='Distance')
|
|
348
|
+
fig.update_traces(
|
|
349
|
+
hovertemplate="Word 1: %{x}<br>Word 2: %{y}<br>Distance: %{z}<extra></extra>",
|
|
350
|
+
hoverlabel=dict(
|
|
351
|
+
bgcolor=colors['bg'],
|
|
352
|
+
font=dict(color=colors['text']) ))
|
|
353
|
+
|
|
354
|
+
return fig
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def plot_clusters(self, n_clusters=5, method='kmeans', red_method='pca', show_centers=False, grid=True, theme='light1', title=None, nlabels=0, use_subset=False):
|
|
358
|
+
'''
|
|
359
|
+
Creates a 2D scatterplot of clustered embeddings using a clustering algorithm.
|
|
360
|
+
|
|
361
|
+
Parameters:
|
|
362
|
+
-----------
|
|
363
|
+
n_clusters : int, default=5
|
|
364
|
+
Number of clusters to generate.
|
|
365
|
+
method : str, default='kmeans'
|
|
366
|
+
Clustering method to use ('kmeans' or others supported by create_clusters).
|
|
367
|
+
red_method : str, default='pca'
|
|
368
|
+
Dimensionality reduction method to apply before plotting.
|
|
369
|
+
show_centers : bool, default=False
|
|
370
|
+
If True, displays cluster centers on the plot.
|
|
371
|
+
grid : bool, default=True
|
|
372
|
+
Whether to display grid lines.
|
|
373
|
+
theme : str, default='light1'
|
|
374
|
+
Plot color theme.
|
|
375
|
+
title : str, optional
|
|
376
|
+
Title of the plot. If None, no title is shown.
|
|
377
|
+
nlabels : int, default=0
|
|
378
|
+
Number of token labels to display on the plot.
|
|
379
|
+
use_subset : bool, default=False
|
|
380
|
+
If True, uses the embedding subset instead of the full embeddings.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
--------
|
|
384
|
+
fig : matplotlib.figure.Figure
|
|
385
|
+
ax : matplotlib.axes.Axes
|
|
386
|
+
'''
|
|
387
|
+
|
|
388
|
+
if use_subset:
|
|
389
|
+
emb, tokens = self.loader.use_subset()
|
|
390
|
+
else:
|
|
391
|
+
emb = self.embeddings
|
|
392
|
+
tokens = self.tokens
|
|
393
|
+
|
|
394
|
+
reduced_emb = reduce_dim(emb, method=red_method)
|
|
395
|
+
|
|
396
|
+
clusters, centers, reduced_emb = create_clusters(reduced_emb, n_clusters=n_clusters, method=method)
|
|
397
|
+
clusters_colors, legend_labels = self.map_colors(clusters)
|
|
398
|
+
|
|
399
|
+
fig, ax, colors = self._setup_plot(theme, grid, title)
|
|
400
|
+
ax.scatter(reduced_emb[:, 0], reduced_emb[:, 1], c=clusters_colors, alpha=0.5, s=14, marker='o')
|
|
401
|
+
|
|
402
|
+
if show_centers and centers is not None:
|
|
403
|
+
for i in range(n_clusters):
|
|
404
|
+
ax.scatter(centers[i, 0], centers[i, 1], edgecolors="grey", color=colors['text'], s=40, alpha=0.8, marker='o')
|
|
405
|
+
|
|
406
|
+
legend_elements = [plt.Line2D([0], [0], marker='o',
|
|
407
|
+
color=color,
|
|
408
|
+
label=label_text,
|
|
409
|
+
markerfacecolor=color,
|
|
410
|
+
markersize=8,
|
|
411
|
+
linestyle='None')
|
|
412
|
+
for label, (color, label_text) in legend_labels.items()]
|
|
413
|
+
|
|
414
|
+
texts=[]
|
|
415
|
+
if nlabels > 0:
|
|
416
|
+
sparse_indices = self.select_sparse_labels(reduced_emb, nlabels)
|
|
417
|
+
for i in sparse_indices:
|
|
418
|
+
texts.append(ax.text(reduced_emb[i, 0], reduced_emb[i, 1], tokens[i],
|
|
419
|
+
color=colors['text'], fontsize=9, alpha=1, ha='center', va='bottom'))
|
|
420
|
+
|
|
421
|
+
ax.legend(handles=legend_elements, facecolor=colors['bg'], labelcolor=colors['text'])
|
|
422
|
+
adjust_text(texts, ax=ax, expand=(1.2, 2), arrowprops=dict(arrowstyle='-', color=colors['text']))
|
|
423
|
+
plt.rcParams['figure.dpi'] = 600
|
|
424
|
+
plt.show()
|
|
425
|
+
return fig, ax
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def interactive_embeddings(self, red_method='pca', grid=True, theme='light1', title=None, use_subset=False):
|
|
429
|
+
'''
|
|
430
|
+
Creates an interactive 2D scatterplot of embeddings using Plotly.
|
|
431
|
+
|
|
432
|
+
Parameters:
|
|
433
|
+
-----------
|
|
434
|
+
red_method : str, default='pca'
|
|
435
|
+
Dimensionality reduction method to apply before plotting.
|
|
436
|
+
grid : bool, default=True
|
|
437
|
+
Whether to display grid lines.
|
|
438
|
+
theme : str, default='light1'
|
|
439
|
+
Plot color theme.
|
|
440
|
+
title : str, optional
|
|
441
|
+
Title of the plot. If None, no title is shown.
|
|
442
|
+
use_subset : bool, default=False
|
|
443
|
+
If True, uses the embedding subset instead of the full embeddings.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
--------
|
|
447
|
+
fig : plotly.graph_objects.Figure
|
|
448
|
+
'''
|
|
449
|
+
|
|
450
|
+
if use_subset:
|
|
451
|
+
emb, tokens = self.loader.use_subset()
|
|
452
|
+
else:
|
|
453
|
+
emb = self.embeddings
|
|
454
|
+
tokens = self.tokens
|
|
455
|
+
|
|
456
|
+
reduced_emb = reduce_dim(emb, method=red_method)
|
|
457
|
+
|
|
458
|
+
colors = self.get_theme(theme)
|
|
459
|
+
|
|
460
|
+
fig = px.scatter(reduced_emb, reduced_emb[:, 0], reduced_emb[:, 1], color_discrete_sequence=[colors['points']])
|
|
461
|
+
fig.update_traces(
|
|
462
|
+
text=tokens,
|
|
463
|
+
textposition='top center',
|
|
464
|
+
hovertemplate='%{text}<extra></extra>',
|
|
465
|
+
hoverlabel=dict(
|
|
466
|
+
bgcolor=colors['bg'],
|
|
467
|
+
font=dict(color=colors['text'])),
|
|
468
|
+
marker=dict(size=6, opacity=0.6, line=dict(width=0))
|
|
469
|
+
)
|
|
470
|
+
fig.update_layout(
|
|
471
|
+
height=600,
|
|
472
|
+
title=title if title else "Word Embedding Interactive Plot",
|
|
473
|
+
title_x=0.5,
|
|
474
|
+
title_xanchor='center',
|
|
475
|
+
plot_bgcolor=colors['bg'],
|
|
476
|
+
paper_bgcolor=colors['bg'],
|
|
477
|
+
font=dict(color=colors['text']),
|
|
478
|
+
xaxis=dict(showticklabels=False, showgrid=grid, gridcolor=colors['grid'], zeroline=False),
|
|
479
|
+
yaxis=dict(showticklabels=False, showgrid=grid, gridcolor=colors['grid'], zeroline=False),
|
|
480
|
+
xaxis_title=None,
|
|
481
|
+
yaxis_title=None
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
return fig
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.spatial.distance import cityblock, euclidean, cosine, chebyshev, canberra, braycurtis
|
|
3
|
+
from scipy.stats import pearsonr, spearmanr
|
|
4
|
+
from sklearn.metrics import pairwise_distances
|
|
5
|
+
from typing import List, Tuple
|
|
6
|
+
import warnings
|
|
7
|
+
from wordviz.loading import EmbeddingLoader
|
|
8
|
+
|
|
9
|
+
def word_distance(loader: EmbeddingLoader, word1: str, word2: str, dist: str = 'cosine') -> float:
|
|
10
|
+
'''
|
|
11
|
+
Computes distance between two words given by user.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
-----------
|
|
15
|
+
loader: EmbeddingLoader
|
|
16
|
+
Object used to load embeddings
|
|
17
|
+
word1, word2: str
|
|
18
|
+
Word to compute distance between
|
|
19
|
+
dist: str, default='cosine'
|
|
20
|
+
Type of distance to use:
|
|
21
|
+
- 'braycurtis'
|
|
22
|
+
- 'canberra'
|
|
23
|
+
- 'chebyshev'
|
|
24
|
+
- 'cosine'
|
|
25
|
+
- 'dot'
|
|
26
|
+
- 'euclidean'
|
|
27
|
+
- 'manhattan'
|
|
28
|
+
- 'pearson'
|
|
29
|
+
- 'pearson'
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
--------
|
|
33
|
+
distance: float
|
|
34
|
+
'''
|
|
35
|
+
words = loader.tokens
|
|
36
|
+
|
|
37
|
+
missing = [w for w in (word1, word2) if w not in words]
|
|
38
|
+
if missing:
|
|
39
|
+
raise ValueError(f"Word(s) not in vocabulary: {', '.join(missing)}")
|
|
40
|
+
|
|
41
|
+
vec1 = loader.get_embedding(word1)
|
|
42
|
+
vec2 = loader.get_embedding(word2)
|
|
43
|
+
emb_matrix = loader.embeddings
|
|
44
|
+
|
|
45
|
+
match dist:
|
|
46
|
+
case 'braycurtis':
|
|
47
|
+
distance = braycurtis(vec1, vec2)
|
|
48
|
+
case 'canberra':
|
|
49
|
+
distance = canberra(vec1, vec2)
|
|
50
|
+
case 'chebyshev':
|
|
51
|
+
distance = chebyshev(vec1, vec2)
|
|
52
|
+
case 'cosine':
|
|
53
|
+
distance = cosine(vec1, vec2)
|
|
54
|
+
case 'dot':
|
|
55
|
+
distance = -np.dot(vec1, vec2)
|
|
56
|
+
case 'euclidean':
|
|
57
|
+
distance = euclidean(vec1, vec2)
|
|
58
|
+
case 'manhattan':
|
|
59
|
+
distance = cityblock(vec1, vec2)
|
|
60
|
+
case 'pearson':
|
|
61
|
+
pearson_corr, _ = pearsonr(vec1, vec2)
|
|
62
|
+
distance = 1 - pearson_corr
|
|
63
|
+
case 'spearman':
|
|
64
|
+
spearman_corr, _ = spearmanr(vec1, vec2)
|
|
65
|
+
distance = 1 - spearman_corr
|
|
66
|
+
|
|
67
|
+
return distance
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def n_most_similar2(loader: EmbeddingLoader, target_word: str, dist: str = 'cosine', n: int = 10) -> Tuple[List[str], np.ndarray, List[float]]:
|
|
71
|
+
'''
|
|
72
|
+
Finds the n most similar words to a given target word using a specified distance metric.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
-----------
|
|
76
|
+
loader : EmbeddingLoader
|
|
77
|
+
An instance of the embedding loader containing word vectors.
|
|
78
|
+
target_word : str
|
|
79
|
+
The word for which to find the most similar neighbors.
|
|
80
|
+
dist : str, default='cosine'
|
|
81
|
+
The distance metric to use. Options include 'cosine', 'euclidean', etc.
|
|
82
|
+
n : int, default=10
|
|
83
|
+
The number of most similar words to retrieve.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
--------
|
|
87
|
+
words : list of str
|
|
88
|
+
The most similar words found.
|
|
89
|
+
vectors : np.ndarray
|
|
90
|
+
Embedding vectors corresponding to the most similar words.
|
|
91
|
+
distances : list of float
|
|
92
|
+
Distances from the target word to each of the most similar words.
|
|
93
|
+
'''
|
|
94
|
+
words = loader.tokens
|
|
95
|
+
|
|
96
|
+
if target_word not in words:
|
|
97
|
+
raise ValueError(f'{target_word} is not in vocabulary')
|
|
98
|
+
|
|
99
|
+
topn_vectors = {}
|
|
100
|
+
|
|
101
|
+
for word in words:
|
|
102
|
+
if word == target_word:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
distance = word_distance(loader, word, target_word, dist)
|
|
106
|
+
if len(topn_vectors) < n:
|
|
107
|
+
topn_vectors[word] = distance
|
|
108
|
+
else:
|
|
109
|
+
max_dist_word = max(topn_vectors, key=topn_vectors.get)
|
|
110
|
+
if distance < topn_vectors[max_dist_word]:
|
|
111
|
+
del topn_vectors[max_dist_word]
|
|
112
|
+
topn_vectors[word] = distance
|
|
113
|
+
|
|
114
|
+
vectors = np.array([loader.get_embedding(word) for word in topn_vectors])
|
|
115
|
+
words = list(topn_vectors.keys())
|
|
116
|
+
distances = list(topn_vectors.values())
|
|
117
|
+
|
|
118
|
+
return words, vectors, distances
|
|
119
|
+
|
|
120
|
+
def n_most_similar(loader: EmbeddingLoader, target_word: str, dist: str = 'cosine', n: int = 10) -> Tuple[List[str], np.ndarray, List[float]]:
|
|
121
|
+
'''
|
|
122
|
+
Findsspairwise the n most similar words to a given target word using a specified distance metric.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
-----------
|
|
126
|
+
loader : EmbeddingLoader
|
|
127
|
+
An instance of the embedding loader containing word vectors.
|
|
128
|
+
target_word : str
|
|
129
|
+
The word for which to find the most similar neighbors.
|
|
130
|
+
dist : str, default='cosine'
|
|
131
|
+
The distance metric to use. Options include 'cosine', 'euclidean', etc.
|
|
132
|
+
n : int, default=10
|
|
133
|
+
The number of most similar words to retrieve.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
--------
|
|
137
|
+
words : list of str
|
|
138
|
+
The most similar words found.
|
|
139
|
+
vectors : np.ndarray
|
|
140
|
+
Embedding vectors corresponding to the most similar words.
|
|
141
|
+
distances : list of float
|
|
142
|
+
Distances from the target word to each of the most similar words.
|
|
143
|
+
'''
|
|
144
|
+
words = loader.tokens
|
|
145
|
+
|
|
146
|
+
if target_word not in words:
|
|
147
|
+
raise ValueError(f'{target_word} is not in vocabulary')
|
|
148
|
+
|
|
149
|
+
# Ottieni il vettore target
|
|
150
|
+
target_vector = loader.get_embedding(target_word)
|
|
151
|
+
target_index = words.index(target_word)
|
|
152
|
+
|
|
153
|
+
# Prepara gli indici di tutte le parole tranne la parola target
|
|
154
|
+
word_indices = list(range(len(words)))
|
|
155
|
+
word_indices.remove(target_index)
|
|
156
|
+
|
|
157
|
+
# Sottoinsiemi di parole e vettori corrispondenti
|
|
158
|
+
filtered_words = [words[i] for i in word_indices]
|
|
159
|
+
|
|
160
|
+
# Per gestire vocabolari molto grandi, possiamo elaborare per batch
|
|
161
|
+
batch_size = 10000
|
|
162
|
+
all_distances = []
|
|
163
|
+
all_indices = []
|
|
164
|
+
|
|
165
|
+
for i in range(0, len(filtered_words), batch_size):
|
|
166
|
+
batch_words = filtered_words[i:i+batch_size]
|
|
167
|
+
batch_vectors = np.array([loader.get_embedding(word) for word in batch_words])
|
|
168
|
+
|
|
169
|
+
# Calcola le distanze pairwise tra il vettore target e tutti i vettori batch
|
|
170
|
+
# Reshape target_vector per ottenere una matrice 1 x dimensione
|
|
171
|
+
distances = pairwise_distances(
|
|
172
|
+
target_vector.reshape(1, -1),
|
|
173
|
+
batch_vectors,
|
|
174
|
+
metric=dist
|
|
175
|
+
).flatten()
|
|
176
|
+
|
|
177
|
+
all_distances.extend(distances)
|
|
178
|
+
all_indices.extend(range(i, min(i+batch_size, len(filtered_words))))
|
|
179
|
+
|
|
180
|
+
# Seleziona gli indici delle n distanze minori
|
|
181
|
+
if len(all_distances) <= n:
|
|
182
|
+
top_n_indices = np.argsort(all_distances)
|
|
183
|
+
else:
|
|
184
|
+
top_n_indices = np.argpartition(all_distances, n-1)[:n]
|
|
185
|
+
# Ordina questi n indici per distanza
|
|
186
|
+
top_n_indices = top_n_indices[np.argsort(np.array(all_distances)[top_n_indices])]
|
|
187
|
+
|
|
188
|
+
# Ottieni le parole, le distanze e i vettori corrispondenti
|
|
189
|
+
result_words = [filtered_words[all_indices[i]] for i in top_n_indices]
|
|
190
|
+
result_distances = [all_distances[i] for i in top_n_indices]
|
|
191
|
+
result_vectors = np.array([loader.get_embedding(word) for word in result_words])
|
|
192
|
+
|
|
193
|
+
return result_words, result_vectors, result_distances
|