vislearnlabpy 0.0.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.
- vislearnlabpy-0.0.1/LICENSE +21 -0
- vislearnlabpy-0.0.1/PKG-INFO +38 -0
- vislearnlabpy-0.0.1/README.md +20 -0
- vislearnlabpy-0.0.1/pyproject.toml +28 -0
- vislearnlabpy-0.0.1/setup.cfg +4 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy/__init__.py +0 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy/embeddings/embedding_utils.py +17 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy/embeddings/generate_embeddings.py +40 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy/embeddings/stimuli_loader.py +79 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy/models/clip_model.py +12 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy/models/feature_generator.py +105 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy/models/multimodal_model.py +190 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy.egg-info/PKG-INFO +38 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy.egg-info/SOURCES.txt +15 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy.egg-info/dependency_links.txt +1 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy.egg-info/requires.txt +3 -0
- vislearnlabpy-0.0.1/src/vislearnlabpy.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Visual Learning Lab
|
|
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,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vislearnlabpy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Visual Learning Lab utility files and pipelines
|
|
5
|
+
Author-email: Tarun Sepuri <tarunsepuri@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vislearnlab/vllpy
|
|
8
|
+
Project-URL: Issues, https://github.com/vislearnlab/vllpy/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pandas>=2.2.3
|
|
15
|
+
Requires-Dist: tqdm>=4.67.1
|
|
16
|
+
Requires-Dist: pillow>=11.0.0
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# vllpy
|
|
20
|
+
|
|
21
|
+
This is a package with common utility functions, files and pipelines for the Visual Learning Lab. Creating a conda environment is recommended but optional. This package uses python=3.12.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
conda create -n vislearnlabpy python=3.12
|
|
25
|
+
conda activate vislearnlabpy
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then, activate the environment and simply install vislearnlabpy via running the following pip command in your terminal. You will also have to install [PyTorch](https://pytorch.org/) and CLIP manually.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
pip install git+https://github.com/openai/CLIP.git
|
|
32
|
+
pip install --upgrade vislearnlabpy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
To install PyTorch on the Tversky server, run:
|
|
36
|
+
```
|
|
37
|
+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
|
|
38
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# vllpy
|
|
2
|
+
|
|
3
|
+
This is a package with common utility functions, files and pipelines for the Visual Learning Lab. Creating a conda environment is recommended but optional. This package uses python=3.12.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
conda create -n vislearnlabpy python=3.12
|
|
7
|
+
conda activate vislearnlabpy
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Then, activate the environment and simply install vislearnlabpy via running the following pip command in your terminal. You will also have to install [PyTorch](https://pytorch.org/) and CLIP manually.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
pip install git+https://github.com/openai/CLIP.git
|
|
14
|
+
pip install --upgrade vislearnlabpy
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
To install PyTorch on the Tversky server, run:
|
|
18
|
+
```
|
|
19
|
+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
|
|
20
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vislearnlabpy"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Tarun Sepuri", email="tarunsepuri@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pandas>=2.2.3",
|
|
13
|
+
"tqdm>=4.67.1",
|
|
14
|
+
"pillow>=11.0.0"
|
|
15
|
+
]
|
|
16
|
+
description = "Visual Learning Lab utility files and pipelines"
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
requires-python = ">=3.12"
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
]
|
|
23
|
+
license = "MIT"
|
|
24
|
+
license-files = ["LICEN[CS]E*"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/vislearnlab/vllpy"
|
|
28
|
+
Issues = "https://github.com/vislearnlab/vllpy/issues"
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
def rename_csv_column(file, cols, new_name):
|
|
4
|
+
for col in cols:
|
|
5
|
+
if col in file.columns:
|
|
6
|
+
file = file.rename(columns={col: new_name})
|
|
7
|
+
return file # Rename only the first match
|
|
8
|
+
|
|
9
|
+
def process_csv(input_csv):
|
|
10
|
+
images_df = pd.read_csv(input_csv)
|
|
11
|
+
text_cols = ['class_name', 'word', 'text']
|
|
12
|
+
image_cols = ['input_path', 'image_path']
|
|
13
|
+
if 'text1' not in images_df.columns:
|
|
14
|
+
images_df = rename_csv_column(images_df, text_cols, 'text1')
|
|
15
|
+
if 'image1' not in images_df.columns:
|
|
16
|
+
images_df = rename_csv_column(images_df, image_cols, 'image1')
|
|
17
|
+
return images_df
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from vislearnlabpy.models.clip_model import CLIPGenerator
|
|
2
|
+
import torch
|
|
3
|
+
import os
|
|
4
|
+
from vislearnlabpy.embeddings.stimuli_loader import StimuliLoader
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
torch.set_num_threads(32)
|
|
8
|
+
|
|
9
|
+
class EmbeddingGenerator():
|
|
10
|
+
def __init__(self, device=None, model_type="clip"):
|
|
11
|
+
if device is None:
|
|
12
|
+
self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
13
|
+
else:
|
|
14
|
+
self.device = device
|
|
15
|
+
self.model_type = model_type
|
|
16
|
+
|
|
17
|
+
def generate_image_embeddings(self, output_path=os.getcwd(), overwrite=False,
|
|
18
|
+
normalize=True, output_type="csv", input_csv=None,
|
|
19
|
+
input_dir=None, batch_size=1):
|
|
20
|
+
if input_csv is None:
|
|
21
|
+
if input_dir is None:
|
|
22
|
+
raise Exception("Either input CSV or input image path needs to be provided")
|
|
23
|
+
images_dataloader = StimuliLoader(
|
|
24
|
+
image_folder=input_dir,
|
|
25
|
+
batch_size=batch_size,
|
|
26
|
+
stimuli_type="images"
|
|
27
|
+
).dataloader()
|
|
28
|
+
else:
|
|
29
|
+
images_dataloader = StimuliLoader(
|
|
30
|
+
image_folder=input_dir,
|
|
31
|
+
dataset_file=input_csv,
|
|
32
|
+
batch_size=batch_size,
|
|
33
|
+
stimuli_type="images",
|
|
34
|
+
id_column="image1"
|
|
35
|
+
).dataloader()
|
|
36
|
+
CLIPGenerator(device=self.device, dataloader=images_dataloader).save_image_embeddings(save_path=output_path,
|
|
37
|
+
output_type=output_type,
|
|
38
|
+
normalize_embeddings=normalize,
|
|
39
|
+
overwrite=overwrite,
|
|
40
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import re
|
|
4
|
+
from PIL import Image
|
|
5
|
+
from torch.utils.data import Dataset, DataLoader
|
|
6
|
+
from vislearnlabpy.embeddings.embedding_utils import process_csv
|
|
7
|
+
import random
|
|
8
|
+
|
|
9
|
+
random.seed(2)
|
|
10
|
+
|
|
11
|
+
class StimuliDataset(Dataset):
|
|
12
|
+
def __init__(self, manifest, images_folder=None, id_column=None):
|
|
13
|
+
self.manifest = manifest
|
|
14
|
+
self.images_folder = images_folder
|
|
15
|
+
self.num_text_cols = sum(1 for c in self.manifest.columns if re.compile("text[0-9]").match(c))
|
|
16
|
+
self.num_image_cols = len([c for c in self.manifest.columns if re.compile("image[0-9]").match(c)])
|
|
17
|
+
self.id_column = id_column
|
|
18
|
+
|
|
19
|
+
def __len__(self):
|
|
20
|
+
return len(self.manifest)
|
|
21
|
+
|
|
22
|
+
def __getitem__(self, idx):
|
|
23
|
+
row = self.manifest.iloc[idx]
|
|
24
|
+
texts = [str(row[f"text{i}"]) for i in range(1, self.num_text_cols + 1)]
|
|
25
|
+
images = []
|
|
26
|
+
image_paths = self._get_image_paths(row)
|
|
27
|
+
for image_path in image_paths:
|
|
28
|
+
if image_path is None or not os.path.exists(image_path):
|
|
29
|
+
images.append(None)
|
|
30
|
+
else:
|
|
31
|
+
with Image.open(image_path).convert('RGB') as img:
|
|
32
|
+
images.append(img.copy()) # Copy the image data to memory
|
|
33
|
+
return {"images": images, "text": texts, "id": [row[self.id_column] if self.id_column is not None else random.randint(0, 1000000)]}
|
|
34
|
+
|
|
35
|
+
def _get_image_paths(self, row):
|
|
36
|
+
# If this is a text only dataset
|
|
37
|
+
if self.images_folder is None and self.num_image_cols == 0:
|
|
38
|
+
return [None] * self.num_text_cols
|
|
39
|
+
# If images are not in the manifest
|
|
40
|
+
elif self.num_image_cols == 0:
|
|
41
|
+
return [os.path.join(self.images_folder, row[f"text{i}"] + ".jpg") for i in range(1, self.num_text_cols + 1)]
|
|
42
|
+
else:
|
|
43
|
+
# allowing for a secondary image path to be provided with different images in the same stimuli sets stored in different subpaths
|
|
44
|
+
return [
|
|
45
|
+
os.path.join(*(self.images_folder,) if self.images_folder is not None else (), *(row["image_path"],) if "image_path" in row else (), row[f"image{i}"])
|
|
46
|
+
for i in range(1, self.num_image_cols + 1)]
|
|
47
|
+
|
|
48
|
+
class StimuliLoader():
|
|
49
|
+
def __init__(self, dataset_file=None, batch_size=1, image_folder=None, id_column=None, stimuli_type='lookit', pairwise=False):
|
|
50
|
+
self.id_column = id_column
|
|
51
|
+
if dataset_file is None:
|
|
52
|
+
if image_folder is not None:
|
|
53
|
+
image_files = [
|
|
54
|
+
f for f in os.listdir(image_folder)
|
|
55
|
+
if (
|
|
56
|
+
os.path.isfile(os.path.join(image_folder, f)) and
|
|
57
|
+
not f.startswith("._") and
|
|
58
|
+
f.lower().endswith((".png", ".jpg", ".jpeg"))
|
|
59
|
+
)
|
|
60
|
+
]
|
|
61
|
+
self.manifest = pd.DataFrame({'image1': image_files})
|
|
62
|
+
# assuming that the image path is a unique identifier if a specific ID column is not provided
|
|
63
|
+
self.id_column = "image1" if id_column is None else id_column
|
|
64
|
+
else:
|
|
65
|
+
raise ValueError("Either image folder or dataset file needs to be provided")
|
|
66
|
+
else:
|
|
67
|
+
self.manifest = process_csv(dataset_file)
|
|
68
|
+
self.stimuli_type = stimuli_type
|
|
69
|
+
self.image_folder = image_folder
|
|
70
|
+
self.batch_size = batch_size
|
|
71
|
+
self.pairwise = pairwise
|
|
72
|
+
|
|
73
|
+
def collator(self, batch):
|
|
74
|
+
return {key: [item for ex in batch for item in ex[key]] for key in batch[0]}
|
|
75
|
+
|
|
76
|
+
def dataloader(self):
|
|
77
|
+
dataset = StimuliDataset(self.manifest, self.image_folder, self.id_column)
|
|
78
|
+
return DataLoader(dataset, batch_size=self.batch_size, collate_fn=self.collator)
|
|
79
|
+
# TODO: allow for calculating all possible pairwise similarities in a space to do RSA etc.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import clip
|
|
2
|
+
from vislearnlabpy.models.multimodal_model import MultimodalModel
|
|
3
|
+
|
|
4
|
+
class CLIPGenerator(MultimodalModel):
|
|
5
|
+
def __init__(self, dataloader=None, device=None):
|
|
6
|
+
self.model, self.preprocess = clip.load("ViT-B/32")
|
|
7
|
+
super().__init__(self.model, self.preprocess, dataloader, device)
|
|
8
|
+
self.name = "clip"
|
|
9
|
+
|
|
10
|
+
def preprocess_text(self, text):
|
|
11
|
+
return clip.tokenize(f"a photo of a {text}").to(self.device)
|
|
12
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import itertools
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import os
|
|
5
|
+
import torch
|
|
6
|
+
import random
|
|
7
|
+
|
|
8
|
+
from tqdm import tqdm
|
|
9
|
+
from torch.nn.functional import cosine_similarity
|
|
10
|
+
random.seed(4)
|
|
11
|
+
|
|
12
|
+
class FeatureGenerator(ABC):
|
|
13
|
+
"""Abstract base class for generating similarity scores between pairs of concepts."""
|
|
14
|
+
def __init__(self, model, preprocess, dataloader, device=None, name="feature_generator"):
|
|
15
|
+
# working with tversky CPU capacity
|
|
16
|
+
torch.set_num_threads(1)
|
|
17
|
+
if device is None:
|
|
18
|
+
self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
19
|
+
else:
|
|
20
|
+
self.device = device
|
|
21
|
+
#model = torch.nn.DataParallel(model)
|
|
22
|
+
self.model = model.to(self.device)
|
|
23
|
+
self.model.eval()
|
|
24
|
+
self.preprocess = preprocess
|
|
25
|
+
self.name = name
|
|
26
|
+
self.dataloader = dataloader
|
|
27
|
+
|
|
28
|
+
def similarity(self, embeddings1, embeddings2):
|
|
29
|
+
# Compute cosine similarity
|
|
30
|
+
similarity_score = cosine_similarity(embeddings1, embeddings2)
|
|
31
|
+
return similarity_score.item()
|
|
32
|
+
|
|
33
|
+
# Normalize each embedding to have a unit L2 norm
|
|
34
|
+
def normalize_embeddings(self, embeddings):
|
|
35
|
+
return [embedding / embedding.norm(dim=-1, keepdim=True) for embedding in embeddings]
|
|
36
|
+
|
|
37
|
+
def save_similarities(self, sim_df: pd.DataFrame, save_path=None):
|
|
38
|
+
"""Save similarity scores to CSV."""
|
|
39
|
+
self.save_df(sim_df, f'similarities-{self.name}_data.csv', save_path)
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def similarities(self, stimulus1, stimulus2, dataloader_row):
|
|
43
|
+
"""Compute similarity scores between two stimuli"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def format_similarity_row(self, word1, word2, similarity_score):
|
|
47
|
+
return {'target': word1, 'distractor': word2, **similarity_score}
|
|
48
|
+
|
|
49
|
+
def lookit_similarities(self, save_path=None):
|
|
50
|
+
"""Calculate cosine similarities between all word pairs in the Lookit dataset"""
|
|
51
|
+
full_similarity_df = pd.DataFrame()
|
|
52
|
+
with torch.no_grad():
|
|
53
|
+
for d in tqdm(self.dataloader, desc=f"Calculating {self.name} similarities"):
|
|
54
|
+
similarity_data = []
|
|
55
|
+
# need to fix placing based on the stimuli set
|
|
56
|
+
text_set = set()
|
|
57
|
+
for count, (text1, text2) in enumerate(itertools.permutations(d['text'], 2)):
|
|
58
|
+
images = [d['images'][d['text'].index(text1)], d['images'][d['text'].index(text2)]]
|
|
59
|
+
pair = tuple(([text1, text2]))
|
|
60
|
+
if pair not in text_set:
|
|
61
|
+
curr_similarities = self.similarities(text1, text2, images)[0]
|
|
62
|
+
curr_similarities["stimuli_id"] = d['id'][0]
|
|
63
|
+
curr_similarities["row_id"] = f"{curr_similarities["stimuli_id"]}_{count}"
|
|
64
|
+
similarity_data.append(self.format_similarity_row(text1, text2, curr_similarities))
|
|
65
|
+
text_set.add(pair)
|
|
66
|
+
if len(similarity_data) > 0:
|
|
67
|
+
similarity_df = pd.DataFrame(similarity_data)
|
|
68
|
+
# Fix concatenation to handle empty DataFrames properly
|
|
69
|
+
if full_similarity_df.empty:
|
|
70
|
+
full_similarity_df = similarity_df
|
|
71
|
+
else:
|
|
72
|
+
full_similarity_df = pd.concat([full_similarity_df, similarity_df], ignore_index=True)
|
|
73
|
+
self.save_similarities(similarity_df, save_path)
|
|
74
|
+
return full_similarity_df
|
|
75
|
+
|
|
76
|
+
def image_word_alignment(self, images, words):
|
|
77
|
+
"""Compute alignment between a set of images and a list of words"""
|
|
78
|
+
inputs = self.preprocess(images=images, text=words, return_tensors="pt", padding=True)
|
|
79
|
+
return self.image_word_alignment(**inputs)
|
|
80
|
+
|
|
81
|
+
def save_df(self, df, filename, save_path=None, overwrite=False):
|
|
82
|
+
"""
|
|
83
|
+
Save dataframe to CSV, appending to existing file if it exists.
|
|
84
|
+
Avoids duplicate row_ids and handles new directory creation.
|
|
85
|
+
"""
|
|
86
|
+
filepath = os.path.join(save_path, filename)
|
|
87
|
+
# create directory if it does not exist
|
|
88
|
+
if not os.path.exists(save_path):
|
|
89
|
+
os.makedirs(save_path)
|
|
90
|
+
if not os.path.exists(filepath):
|
|
91
|
+
df.to_csv(filepath, index=False)
|
|
92
|
+
return
|
|
93
|
+
try:
|
|
94
|
+
existing_df = pd.read_csv(filepath)
|
|
95
|
+
row_ids = df['row_id'].values
|
|
96
|
+
if overwrite:
|
|
97
|
+
# Remove rows from existing data that would be overwritten by new data
|
|
98
|
+
existing_df = existing_df[~existing_df['row_id'].isin(row_ids)]
|
|
99
|
+
existing_df.to_csv(filepath, index=False)
|
|
100
|
+
else:
|
|
101
|
+
df = df[~df['row_id'].isin(existing_df['row_id'].values)]
|
|
102
|
+
df.to_csv(filepath, mode='a', header=False, index=False)
|
|
103
|
+
except pd.errors.EmptyDataError:
|
|
104
|
+
# Handle case where existing file is empty
|
|
105
|
+
df.to_csv(filepath, index=False)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from vislearnlabpy.models.feature_generator import FeatureGenerator
|
|
3
|
+
import torch
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from tqdm import tqdm
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
class MultimodalModel(FeatureGenerator):
|
|
11
|
+
"""Abstract base class for multimodal models like CLIP and CVCL that extends FeatureGenerator"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, model, preprocess, dataloader=None, device=None):
|
|
14
|
+
super().__init__(model, preprocess, dataloader, device)
|
|
15
|
+
print(self.dataloader)
|
|
16
|
+
self.image_word_alignment = lambda **x: self.model(**x).logits_per_image.softmax(dim=-1).detach().cpu().numpy()
|
|
17
|
+
|
|
18
|
+
# Load and preprocess images
|
|
19
|
+
def preprocess_image(self, image):
|
|
20
|
+
return self.preprocess(image).unsqueeze(0).to(self.device)
|
|
21
|
+
|
|
22
|
+
def preprocess_text(self, text):
|
|
23
|
+
return self.model.tokenize(text).to(self.device)
|
|
24
|
+
|
|
25
|
+
def encode_image(self, image):
|
|
26
|
+
return self.model.encode_image(image)
|
|
27
|
+
|
|
28
|
+
def encode_text(self, text):
|
|
29
|
+
return self.model.encode_text(text)
|
|
30
|
+
|
|
31
|
+
def image_embeddings(self, images, normalize_embeddings=True):
|
|
32
|
+
"""Get image embeddings"""
|
|
33
|
+
images = [self.preprocess_image(image) for image in images]
|
|
34
|
+
with torch.no_grad():
|
|
35
|
+
embeddings = [self.encode_image(image) for image in images]
|
|
36
|
+
if normalize_embeddings:
|
|
37
|
+
return self.normalize_embeddings(embeddings)
|
|
38
|
+
else:
|
|
39
|
+
return embeddings
|
|
40
|
+
|
|
41
|
+
def text_embeddings(self, words, normalize_embeddings=True):
|
|
42
|
+
"""Get text embeddings"""
|
|
43
|
+
all_text_features = [self.preprocess_text(word) for word in words]
|
|
44
|
+
with torch.no_grad():
|
|
45
|
+
embeddings = [self.encode_text(text_features) for text_features in all_text_features]
|
|
46
|
+
if normalize_embeddings:
|
|
47
|
+
return self.normalize_embeddings(embeddings)
|
|
48
|
+
else:
|
|
49
|
+
return embeddings
|
|
50
|
+
|
|
51
|
+
def multimodal_embeddings(self, image_embeddings, text_embeddings):
|
|
52
|
+
"""Get multimodal embeddings: by default, averages image and text embeddings"""
|
|
53
|
+
return [(a + b) / 2 for a, b in zip(image_embeddings, text_embeddings)]
|
|
54
|
+
|
|
55
|
+
def text_to_images_logits(self, image_embeddings, text_embeddings, logit_scale=100):
|
|
56
|
+
"""Get logits of text to image embedding dot products"""
|
|
57
|
+
return logit_scale * image_embeddings @ text_embeddings.t()
|
|
58
|
+
|
|
59
|
+
def text_to_images_similarity(self, image_embeddings, text_embedding, logit_scale=100):
|
|
60
|
+
# Convert image_embeddings list to tensor if needed
|
|
61
|
+
if isinstance(image_embeddings, list):
|
|
62
|
+
image_embeddings = torch.stack(image_embeddings)
|
|
63
|
+
logits = self.text_to_images_logits(image_embeddings, text_embedding, logit_scale).to(self.device)
|
|
64
|
+
softmaxes = torch.nn.functional.softmax(logits, dim=0)
|
|
65
|
+
return softmaxes[1][0].item()
|
|
66
|
+
|
|
67
|
+
def multimodal_luce(self, image_embeddings, text_embedding):
|
|
68
|
+
target_similarity = self.similarity(image_embeddings[0], text_embedding)
|
|
69
|
+
distractor_similarity = self.similarity(image_embeddings[1], text_embedding)
|
|
70
|
+
luce = distractor_similarity / (distractor_similarity + target_similarity)
|
|
71
|
+
return luce
|
|
72
|
+
|
|
73
|
+
def similarities(self, word1, word2, images):
|
|
74
|
+
valid_images = [img for img in images if img is not None]
|
|
75
|
+
similarity_scores = []
|
|
76
|
+
# TODO: this only returns the similarity scores for the first pair of images: need to separate out, indexing is weird
|
|
77
|
+
for image1, image2 in itertools.combinations(valid_images, 2):
|
|
78
|
+
curr_image_embeddings = self.image_embeddings([image1, image2])
|
|
79
|
+
curr_text_embeddings = self.text_embeddings([word1, word2])
|
|
80
|
+
# TODO: need to fix how each row is labeled in lookit_similarities,
|
|
81
|
+
similarity_scores.append({
|
|
82
|
+
'image_similarity': self.similarity(curr_image_embeddings[0], curr_image_embeddings[1]),
|
|
83
|
+
'text_similarity': self.similarity(curr_text_embeddings[0], curr_text_embeddings[1]),
|
|
84
|
+
# finding distractor image to target word similarity
|
|
85
|
+
'multimodal_similarity': self.text_to_images_similarity(curr_image_embeddings, curr_text_embeddings[0], logit_scale=10),
|
|
86
|
+
})
|
|
87
|
+
if similarity_scores == []:
|
|
88
|
+
print(f"skipping {word1} and {word2} since they do not have valid images")
|
|
89
|
+
return [{
|
|
90
|
+
'image_similarity': None,
|
|
91
|
+
'text_similarity': None,
|
|
92
|
+
'multimodal_similarity': None
|
|
93
|
+
}]
|
|
94
|
+
else:
|
|
95
|
+
return similarity_scores
|
|
96
|
+
|
|
97
|
+
# TODO: probably move this to the dataloader row level instead of to a pair of words within a dataloader row
|
|
98
|
+
# TODO: words or texts? what is my parameter
|
|
99
|
+
def embeddings(self, word1, word2, dataloader_row):
|
|
100
|
+
valid_images = [img for img in dataloader_row['images'] if img is not None]
|
|
101
|
+
output_embeddings = []
|
|
102
|
+
for image1, image2 in itertools.combinations(valid_images, 2):
|
|
103
|
+
curr_image_embeddings = self.image_embeddings([image1, image2])
|
|
104
|
+
curr_text_embeddings = self.text_embeddings([word1, word2])
|
|
105
|
+
output_embeddings.append({
|
|
106
|
+
'image_embeddings': curr_image_embeddings,
|
|
107
|
+
'text_embeddings': curr_text_embeddings,
|
|
108
|
+
'multimodal_embeddings': self.multimodal_embeddings(curr_image_embeddings, curr_text_embeddings)
|
|
109
|
+
})
|
|
110
|
+
return output_embeddings
|
|
111
|
+
|
|
112
|
+
def save_embedding(self, embedding, curr_id, text, save_path):
|
|
113
|
+
# if curr_id represents the full path of the image, portion off only the last part
|
|
114
|
+
if os.path.exists(curr_id):
|
|
115
|
+
sub_save_path = f"{text}/{Path(curr_id).stem}"
|
|
116
|
+
else:
|
|
117
|
+
sub_save_path = curr_id
|
|
118
|
+
embedding_output_path = Path(f"{os.getcwd()}/{save_path}/{sub_save_path}").with_suffix('.npy')
|
|
119
|
+
os.makedirs(embedding_output_path.parent, exist_ok=True)
|
|
120
|
+
np_embedding = embedding.cpu().numpy()
|
|
121
|
+
np.save(str(embedding_output_path), np_embedding)
|
|
122
|
+
return str(embedding_output_path)
|
|
123
|
+
|
|
124
|
+
def save_text_embeddings(self, texts, save_path, normalize_embeddings=True, output_type="csv", overwrite=False):
|
|
125
|
+
filename = f"{self.name}_text_embeddings_{output_type}.csv"
|
|
126
|
+
text_save_path = os.path.join(str(save_path), "text_embeddings")
|
|
127
|
+
os.makedirs(text_save_path, exist_ok=True)
|
|
128
|
+
filepath = os.path.join(text_save_path, filename)
|
|
129
|
+
existing_row_ids = []
|
|
130
|
+
if os.path.exists(filepath):
|
|
131
|
+
existing_df = pd.read_csv(filepath)
|
|
132
|
+
existing_row_ids = existing_df['row_id'].values
|
|
133
|
+
# TODO: repeated code -- move embedding code to embeddings folder
|
|
134
|
+
with torch.no_grad():
|
|
135
|
+
row_data = []
|
|
136
|
+
for text in tqdm(texts, desc="Calculating text embeddings"):
|
|
137
|
+
if text not in existing_row_ids or overwrite:
|
|
138
|
+
curr_text_embeddings = self.text_embeddings([text], normalize_embeddings)[0]
|
|
139
|
+
curr_row_data = {'row_id': text}
|
|
140
|
+
if output_type == "csv":
|
|
141
|
+
# new row with a separate column for each number in the 512 dimensions and one for the image_path as the row_id
|
|
142
|
+
curr_image_embeddings = curr_image_embeddings.squeeze(0).tolist() if isinstance(curr_image_embeddings, torch.Tensor) else curr_image_embeddings
|
|
143
|
+
for i, value in enumerate(curr_image_embeddings):
|
|
144
|
+
curr_row_data[f"{i}"] = value.item() if isinstance(value, torch.Tensor) else value
|
|
145
|
+
elif output_type == "npy":
|
|
146
|
+
curr_row_data["embedding_path"] = self.save_embedding(curr_text_embeddings, text, text, text_save_path)
|
|
147
|
+
row_data.append(curr_row_data)
|
|
148
|
+
if len(row_data) > 0:
|
|
149
|
+
self.save_df(pd.DataFrame(row_data), filename, text_save_path, overwrite=overwrite)
|
|
150
|
+
|
|
151
|
+
def save_image_embeddings(self, save_path=None, normalize_embeddings=True, output_type="csv", overwrite=False):
|
|
152
|
+
filename = f"{self.name}_image_embeddings_{output_type}.csv"
|
|
153
|
+
save_path = str(save_path) or os.path.join(os.getcwd(), "output")
|
|
154
|
+
img_save_path = os.path.join(save_path, "image_embeddings")
|
|
155
|
+
os.makedirs(img_save_path, exist_ok=True)
|
|
156
|
+
filepath = os.path.join(img_save_path, filename)
|
|
157
|
+
existing_row_ids = []
|
|
158
|
+
if os.path.exists(filepath):
|
|
159
|
+
existing_df = pd.read_csv(filepath)
|
|
160
|
+
existing_row_ids = existing_df['row_id'].values
|
|
161
|
+
all_text = set()
|
|
162
|
+
with torch.no_grad():
|
|
163
|
+
for d in tqdm(self.dataloader, desc=f"Calculating {self.name} embeddings", position=tqdm._get_free_pos()):
|
|
164
|
+
print(d)
|
|
165
|
+
# TODO: only works for single images in a row
|
|
166
|
+
row_data = []
|
|
167
|
+
for count, (image, curr_id, text) in tqdm(enumerate(zip(d['images'], d['id'], d['text'] if d['text'] else itertools.repeat(None, len(d['images'])))), total=len(d['images']), desc="Image embedding progress in current batch", position=tqdm._get_free_pos()):
|
|
168
|
+
if curr_id not in existing_row_ids or overwrite:
|
|
169
|
+
curr_image_embeddings = self.image_embeddings([image], normalize_embeddings)[0]
|
|
170
|
+
curr_row_data = {'row_id': curr_id}
|
|
171
|
+
if text is not None:
|
|
172
|
+
curr_row_data['text'] = text
|
|
173
|
+
if output_type == "csv":
|
|
174
|
+
# new row with a separate column for each number in the 512 dimensions and one for the image_path as the row_id
|
|
175
|
+
curr_image_embeddings = curr_image_embeddings.squeeze(0).tolist() if isinstance(curr_image_embeddings, torch.Tensor) else curr_image_embeddings
|
|
176
|
+
for i, value in enumerate(curr_image_embeddings):
|
|
177
|
+
curr_row_data[f"{i}"] = value.item() if isinstance(value, torch.Tensor) else value
|
|
178
|
+
elif output_type == "npy":
|
|
179
|
+
curr_row_data["embedding_path"] = self.save_embedding(curr_image_embeddings, curr_id, text, img_save_path)
|
|
180
|
+
row_data.append(curr_row_data)
|
|
181
|
+
if text is not None:
|
|
182
|
+
all_text.add(text)
|
|
183
|
+
if len(row_data) > 0:
|
|
184
|
+
self.save_df(pd.DataFrame(row_data), filename, img_save_path, overwrite=overwrite)
|
|
185
|
+
self.save_text_embeddings(all_text, save_path, normalize_embeddings, output_type, overwrite)
|
|
186
|
+
|
|
187
|
+
def normalize_embeddings(self, embeddings):
|
|
188
|
+
"""Normalize embeddings to unit L2 norm"""
|
|
189
|
+
return [embedding / embedding.norm(dim=-1, keepdim=True) for embedding in embeddings]
|
|
190
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vislearnlabpy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Visual Learning Lab utility files and pipelines
|
|
5
|
+
Author-email: Tarun Sepuri <tarunsepuri@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vislearnlab/vllpy
|
|
8
|
+
Project-URL: Issues, https://github.com/vislearnlab/vllpy/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pandas>=2.2.3
|
|
15
|
+
Requires-Dist: tqdm>=4.67.1
|
|
16
|
+
Requires-Dist: pillow>=11.0.0
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# vllpy
|
|
20
|
+
|
|
21
|
+
This is a package with common utility functions, files and pipelines for the Visual Learning Lab. Creating a conda environment is recommended but optional. This package uses python=3.12.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
conda create -n vislearnlabpy python=3.12
|
|
25
|
+
conda activate vislearnlabpy
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then, activate the environment and simply install vislearnlabpy via running the following pip command in your terminal. You will also have to install [PyTorch](https://pytorch.org/) and CLIP manually.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
pip install git+https://github.com/openai/CLIP.git
|
|
32
|
+
pip install --upgrade vislearnlabpy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
To install PyTorch on the Tversky server, run:
|
|
36
|
+
```
|
|
37
|
+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
|
|
38
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/vislearnlabpy/__init__.py
|
|
5
|
+
src/vislearnlabpy.egg-info/PKG-INFO
|
|
6
|
+
src/vislearnlabpy.egg-info/SOURCES.txt
|
|
7
|
+
src/vislearnlabpy.egg-info/dependency_links.txt
|
|
8
|
+
src/vislearnlabpy.egg-info/requires.txt
|
|
9
|
+
src/vislearnlabpy.egg-info/top_level.txt
|
|
10
|
+
src/vislearnlabpy/embeddings/embedding_utils.py
|
|
11
|
+
src/vislearnlabpy/embeddings/generate_embeddings.py
|
|
12
|
+
src/vislearnlabpy/embeddings/stimuli_loader.py
|
|
13
|
+
src/vislearnlabpy/models/clip_model.py
|
|
14
|
+
src/vislearnlabpy/models/feature_generator.py
|
|
15
|
+
src/vislearnlabpy/models/multimodal_model.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vislearnlabpy
|