active-vision 0.0.2__tar.gz → 0.0.3__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.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: active-vision
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Active learning for edge vision.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
8
  Requires-Dist: datasets>=3.2.0
9
9
  Requires-Dist: fastai>=2.7.18
10
+ Requires-Dist: gradio>=5.12.0
10
11
  Requires-Dist: ipykernel>=6.29.5
11
12
  Requires-Dist: ipywidgets>=8.1.5
12
13
  Requires-Dist: loguru>=0.7.3
@@ -14,40 +15,53 @@ Requires-Dist: seaborn>=0.13.2
14
15
 
15
16
  ![Python Version](https://img.shields.io/badge/python-3.10%2B-blue?style=for-the-badge)
16
17
  ![License](https://img.shields.io/badge/License-Apache%202.0-green.svg?style=for-the-badge)
17
- ![PyPI](https://img.shields.io/pypi/v/active-vision?style=for-the-badge)
18
+ [![PyPI](https://img.shields.io/pypi/v/active-vision?style=for-the-badge)](https://pypi.org/project/active-vision/)
18
19
  ![Downloads](https://img.shields.io/pepy/dt/active-vision?style=for-the-badge&logo=pypi&logoColor=white&label=Downloads&color=purple)
19
20
 
20
21
  <p align="center">
21
- <img src="https://github.com/dnth/active-vision/blob/main/assets/logo.png" alt="active-vision">
22
+ <img src="https://raw.githubusercontent.com/dnth/active-vision/main/assets/logo.png" alt="active-vision">
22
23
  </p>
23
24
 
24
25
  Active learning at the edge for computer vision.
25
26
 
26
- The goal of this project is to create a framework for active learning at the edge for computer vision. We should be able to train a model on a small dataset and then use active learning to iteratively improve the model all on a local machine.
27
+ The goal of this project is to create a framework for the active learning loop for computer vision deployed on edge devices.
27
28
 
28
- ## Tech Stack
29
+ ## Installation
30
+ I recommend using [uv](https://docs.astral.sh/uv/) to set up a virtual environment and install the package. You can also use other virtual env of your choice.
29
31
 
30
- - Training framework: fastai
31
- - User interface: streamlit
32
- - Database: sqlite
33
- - Experiment tracking: wandb
32
+ If you're using uv:
34
33
 
35
- ## Installation
34
+ ```bash
35
+ uv venv
36
+ uv sync
37
+ ```
38
+ Once the virtual environment is created, you can install the package using pip.
36
39
 
37
- PyPI
40
+ Get a release from PyPI
38
41
  ```bash
39
42
  pip install active-vision
40
43
  ```
41
44
 
42
- Local install
45
+ Install from source
43
46
  ```bash
44
47
  git clone https://github.com/dnth/active-vision.git
45
48
  cd active-vision
46
49
  pip install -e .
47
50
  ```
48
51
 
52
+ > [!TIP]
53
+ > If you're using uv add a uv before the pip install command to install into your virtual environment. Eg:
54
+ > ```bash
55
+ > uv pip install active-vision
56
+ > ```
57
+
49
58
  ## Usage
50
- See the [notebook](./nbs/end-to-end.ipynb) for a complete example.
59
+ See the [notebook](./nbs/04_relabel_loop.ipynb) for a complete example.
60
+
61
+ Be sure to prepared 3 datasets:
62
+ - train: A dataframe of an existing labeled training dataset.
63
+ - unlabeled: A dataframe of unlabeled data which we will sample from using active learning.
64
+ - eval: A dataframe of labeled data which we will use to evaluate the performance of the model. (Optional)
51
65
 
52
66
  ```python
53
67
  from active_vision import ActiveLearner
@@ -56,29 +70,38 @@ import pandas as pd
56
70
  # Create an active learner instance with a model
57
71
  al = ActiveLearner("resnet18")
58
72
 
59
- # Load the dataset into the active learner
73
+ # Load dataset
60
74
  train_df = pd.read_parquet("training_samples.parquet")
61
- al.load_dataset(train_df, "filepath", "label")
75
+ al.load_dataset(df, filepath_col="filepath", label_col="label")
62
76
 
63
- # Train the model
77
+ # Train model
64
78
  al.train(epochs=3, lr=1e-3)
65
79
 
66
- # Load evaluation data
67
- eval_df = pd.read_parquet("evaluation_samples.parquet")
80
+ # Evaluate the model on a *labeled* evaluation set
81
+ accuracy = al.evaluate(eval_df, filepath_col="filepath", label_col="label")
68
82
 
69
- # Evaluate the model on a labeled evaluation set
70
- accuracy = al.evaluate(eval_df, "filepath", "label")
71
-
72
- # Get predictions from an unlabeled set
83
+ # Get predictions from an *unlabeled* set
73
84
  pred_df = al.predict(filepaths)
74
85
 
75
- # Sample low confidence predictions
86
+ # Sample low confidence predictions from unlabeled set
76
87
  uncertain_df = al.sample_uncertain(pred_df, num_samples=10)
77
88
 
78
- # Add newly labeled data to training set
79
- al.add_to_train_set(uncertain_df)
89
+ # Launch a Gradio UI to label the low confidence samples
90
+ al.label(uncertain_df, output_filename="uncertain")
80
91
  ```
81
92
 
93
+ ![Gradio UI](./assets/labeling_ui.png)
94
+
95
+ Once complete, the labeled samples will be save into a new df.
96
+ We can now add the newly labeled data to the training set.
97
+
98
+ ```python
99
+ # Add newly labeled data to training set and save as a new file active_labeled
100
+ al.add_to_train_set(labeled_df, output_filename="active_labeled")
101
+ ```
102
+
103
+ Repeat the process until the model is good enough. Use the dataset to train a larger model and deploy.
104
+
82
105
  ## Workflow
83
106
  There are two workflows for active learning at the edge that we can use depending on the availability of labeled data.
84
107
 
@@ -1,39 +1,52 @@
1
1
  ![Python Version](https://img.shields.io/badge/python-3.10%2B-blue?style=for-the-badge)
2
2
  ![License](https://img.shields.io/badge/License-Apache%202.0-green.svg?style=for-the-badge)
3
- ![PyPI](https://img.shields.io/pypi/v/active-vision?style=for-the-badge)
3
+ [![PyPI](https://img.shields.io/pypi/v/active-vision?style=for-the-badge)](https://pypi.org/project/active-vision/)
4
4
  ![Downloads](https://img.shields.io/pepy/dt/active-vision?style=for-the-badge&logo=pypi&logoColor=white&label=Downloads&color=purple)
5
5
 
6
6
  <p align="center">
7
- <img src="https://github.com/dnth/active-vision/blob/main/assets/logo.png" alt="active-vision">
7
+ <img src="https://raw.githubusercontent.com/dnth/active-vision/main/assets/logo.png" alt="active-vision">
8
8
  </p>
9
9
 
10
10
  Active learning at the edge for computer vision.
11
11
 
12
- The goal of this project is to create a framework for active learning at the edge for computer vision. We should be able to train a model on a small dataset and then use active learning to iteratively improve the model all on a local machine.
12
+ The goal of this project is to create a framework for the active learning loop for computer vision deployed on edge devices.
13
13
 
14
- ## Tech Stack
14
+ ## Installation
15
+ I recommend using [uv](https://docs.astral.sh/uv/) to set up a virtual environment and install the package. You can also use other virtual env of your choice.
15
16
 
16
- - Training framework: fastai
17
- - User interface: streamlit
18
- - Database: sqlite
19
- - Experiment tracking: wandb
17
+ If you're using uv:
20
18
 
21
- ## Installation
19
+ ```bash
20
+ uv venv
21
+ uv sync
22
+ ```
23
+ Once the virtual environment is created, you can install the package using pip.
22
24
 
23
- PyPI
25
+ Get a release from PyPI
24
26
  ```bash
25
27
  pip install active-vision
26
28
  ```
27
29
 
28
- Local install
30
+ Install from source
29
31
  ```bash
30
32
  git clone https://github.com/dnth/active-vision.git
31
33
  cd active-vision
32
34
  pip install -e .
33
35
  ```
34
36
 
37
+ > [!TIP]
38
+ > If you're using uv add a uv before the pip install command to install into your virtual environment. Eg:
39
+ > ```bash
40
+ > uv pip install active-vision
41
+ > ```
42
+
35
43
  ## Usage
36
- See the [notebook](./nbs/end-to-end.ipynb) for a complete example.
44
+ See the [notebook](./nbs/04_relabel_loop.ipynb) for a complete example.
45
+
46
+ Be sure to prepared 3 datasets:
47
+ - train: A dataframe of an existing labeled training dataset.
48
+ - unlabeled: A dataframe of unlabeled data which we will sample from using active learning.
49
+ - eval: A dataframe of labeled data which we will use to evaluate the performance of the model. (Optional)
37
50
 
38
51
  ```python
39
52
  from active_vision import ActiveLearner
@@ -42,29 +55,38 @@ import pandas as pd
42
55
  # Create an active learner instance with a model
43
56
  al = ActiveLearner("resnet18")
44
57
 
45
- # Load the dataset into the active learner
58
+ # Load dataset
46
59
  train_df = pd.read_parquet("training_samples.parquet")
47
- al.load_dataset(train_df, "filepath", "label")
60
+ al.load_dataset(df, filepath_col="filepath", label_col="label")
48
61
 
49
- # Train the model
62
+ # Train model
50
63
  al.train(epochs=3, lr=1e-3)
51
64
 
52
- # Load evaluation data
53
- eval_df = pd.read_parquet("evaluation_samples.parquet")
65
+ # Evaluate the model on a *labeled* evaluation set
66
+ accuracy = al.evaluate(eval_df, filepath_col="filepath", label_col="label")
54
67
 
55
- # Evaluate the model on a labeled evaluation set
56
- accuracy = al.evaluate(eval_df, "filepath", "label")
57
-
58
- # Get predictions from an unlabeled set
68
+ # Get predictions from an *unlabeled* set
59
69
  pred_df = al.predict(filepaths)
60
70
 
61
- # Sample low confidence predictions
71
+ # Sample low confidence predictions from unlabeled set
62
72
  uncertain_df = al.sample_uncertain(pred_df, num_samples=10)
63
73
 
64
- # Add newly labeled data to training set
65
- al.add_to_train_set(uncertain_df)
74
+ # Launch a Gradio UI to label the low confidence samples
75
+ al.label(uncertain_df, output_filename="uncertain")
66
76
  ```
67
77
 
78
+ ![Gradio UI](./assets/labeling_ui.png)
79
+
80
+ Once complete, the labeled samples will be save into a new df.
81
+ We can now add the newly labeled data to the training set.
82
+
83
+ ```python
84
+ # Add newly labeled data to training set and save as a new file active_labeled
85
+ al.add_to_train_set(labeled_df, output_filename="active_labeled")
86
+ ```
87
+
88
+ Repeat the process until the model is good enough. Use the dataset to train a larger model and deploy.
89
+
68
90
  ## Workflow
69
91
  There are two workflows for active learning at the edge that we can use depending on the availability of labeled data.
70
92
 
@@ -1,14 +1,15 @@
1
1
  [project]
2
2
  name = "active-vision"
3
- version = "0.0.2"
3
+ version = "0.0.3"
4
4
  description = "Active learning for edge vision."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
8
  "datasets>=3.2.0",
9
9
  "fastai>=2.7.18",
10
+ "gradio>=5.12.0",
10
11
  "ipykernel>=6.29.5",
11
12
  "ipywidgets>=8.1.5",
12
13
  "loguru>=0.7.3",
13
14
  "seaborn>=0.13.2",
14
- ]
15
+ ]
@@ -0,0 +1,3 @@
1
+ __version__ = "0.0.3"
2
+
3
+ from .core import *
@@ -0,0 +1,291 @@
1
+ import pandas as pd
2
+ from loguru import logger
3
+ from fastai.vision.models import resnet18, resnet34
4
+ from fastai.callback.all import ShowGraphCallback
5
+ from fastai.vision.all import (
6
+ ImageDataLoaders,
7
+ aug_transforms,
8
+ Resize,
9
+ vision_learner,
10
+ accuracy,
11
+ valley,
12
+ slide,
13
+ minimum,
14
+ steep,
15
+ )
16
+ import torch
17
+ import torch.nn.functional as F
18
+
19
+ import warnings
20
+
21
+ warnings.filterwarnings("ignore", category=FutureWarning)
22
+
23
+
24
+ class ActiveLearner:
25
+ def __init__(self, model_name: str):
26
+ self.model = self.load_model(model_name)
27
+
28
+ def load_model(self, model_name: str):
29
+ models = {"resnet18": resnet18, "resnet34": resnet34}
30
+ logger.info(f"Loading model {model_name}")
31
+ if model_name not in models:
32
+ logger.error(f"Model {model_name} not found")
33
+ raise ValueError(f"Model {model_name} not found")
34
+ return models[model_name]
35
+
36
+ def load_dataset(
37
+ self,
38
+ df: pd.DataFrame,
39
+ filepath_col: str,
40
+ label_col: str,
41
+ valid_pct: float = 0.2,
42
+ batch_size: int = 16,
43
+ image_size: int = 224,
44
+ ):
45
+ logger.info(f"Loading dataset from {filepath_col} and {label_col}")
46
+ self.train_set = df.copy()
47
+
48
+ logger.info("Creating dataloaders")
49
+ self.dls = ImageDataLoaders.from_df(
50
+ df,
51
+ path=".",
52
+ valid_pct=valid_pct,
53
+ fn_col=filepath_col,
54
+ label_col=label_col,
55
+ bs=batch_size,
56
+ item_tfms=Resize(image_size),
57
+ batch_tfms=aug_transforms(size=image_size, min_scale=0.75),
58
+ )
59
+ logger.info("Creating learner")
60
+ self.learn = vision_learner(self.dls, self.model, metrics=accuracy).to_fp16()
61
+ self.class_names = self.dls.vocab
62
+ logger.info("Done. Ready to train.")
63
+
64
+ def lr_find(self):
65
+ logger.info("Finding optimal learning rate")
66
+ self.lrs = self.learn.lr_find(suggest_funcs=(minimum, steep, valley, slide))
67
+ logger.info(f"Optimal learning rate: {self.lrs.valley}")
68
+
69
+ def train(self, epochs: int, lr: float):
70
+ logger.info(f"Training for {epochs} epochs with learning rate: {lr}")
71
+ self.learn.fine_tune(epochs, lr, cbs=[ShowGraphCallback()])
72
+
73
+ def predict(self, filepaths: list[str], batch_size: int = 16):
74
+ """
75
+ Run inference on an unlabeled dataset. Returns a df with filepaths and predicted labels, and confidence scores.
76
+ """
77
+ logger.info(f"Running inference on {len(filepaths)} samples")
78
+ test_dl = self.dls.test_dl(filepaths, bs=batch_size)
79
+ preds, _, cls_preds = self.learn.get_preds(dl=test_dl, with_decoded=True)
80
+
81
+ self.pred_df = pd.DataFrame(
82
+ {
83
+ "filepath": filepaths,
84
+ "pred_label": [self.learn.dls.vocab[i] for i in cls_preds.numpy()],
85
+ "pred_conf": torch.max(F.softmax(preds, dim=1), dim=1)[0].numpy(),
86
+ }
87
+ )
88
+ return self.pred_df
89
+
90
+ def evaluate(
91
+ self, df: pd.DataFrame, filepath_col: str, label_col: str, batch_size: int = 16
92
+ ):
93
+ """
94
+ Evaluate on a labeled dataset. Returns a score.
95
+ """
96
+ self.eval_set = df.copy()
97
+
98
+ filepaths = self.eval_set[filepath_col].tolist()
99
+ labels = self.eval_set[label_col].tolist()
100
+ test_dl = self.dls.test_dl(filepaths, bs=batch_size)
101
+ preds, _, cls_preds = self.learn.get_preds(dl=test_dl, with_decoded=True)
102
+
103
+ self.eval_df = pd.DataFrame(
104
+ {
105
+ "filepath": filepaths,
106
+ "label": labels,
107
+ "pred_label": [self.learn.dls.vocab[i] for i in cls_preds.numpy()],
108
+ }
109
+ )
110
+
111
+ accuracy = float((self.eval_df["label"] == self.eval_df["pred_label"]).mean())
112
+ logger.info(f"Accuracy: {accuracy:.2%}")
113
+ return accuracy
114
+
115
+ def sample_uncertain(self, df: pd.DataFrame, num_samples: int):
116
+ """
117
+ Sample top `num_samples` low confidence samples. Returns a df with filepaths and predicted labels, and confidence scores.
118
+ """
119
+ logger.info(f"Getting top {num_samples} low confidence samples")
120
+ uncertain_df = df.sort_values(by="pred_conf", ascending=True).head(num_samples)
121
+ return uncertain_df
122
+
123
+ def label(self, df: pd.DataFrame, output_filename: str = "labeled"):
124
+ """
125
+ Launch a labeling interface for the user to label the samples.
126
+ Input is a df with filepaths listing the files to be labeled. Output is a df with filepaths and labels.
127
+ """
128
+ import gradio as gr
129
+
130
+ shortcut_js = """
131
+ <script>
132
+ function shortcuts(e) {
133
+ // Only block shortcuts if we're in a text input or textarea
134
+ if (e.target.tagName.toLowerCase() === "textarea" ||
135
+ (e.target.tagName.toLowerCase() === "input" && e.target.type.toLowerCase() === "text")) {
136
+ return;
137
+ }
138
+
139
+ if (e.key.toLowerCase() == "w") {
140
+ document.getElementById("submit_btn").click();
141
+ } else if (e.key.toLowerCase() == "d") {
142
+ document.getElementById("next_btn").click();
143
+ } else if (e.key.toLowerCase() == "a") {
144
+ document.getElementById("back_btn").click();
145
+ }
146
+ }
147
+ document.addEventListener('keypress', shortcuts, false);
148
+ </script>
149
+ """
150
+
151
+ logger.info(f"Launching labeling interface for {len(df)} samples")
152
+
153
+ filepaths = df["filepath"].tolist()
154
+
155
+ with gr.Blocks(head=shortcut_js) as demo:
156
+ current_index = gr.State(value=0)
157
+
158
+ filename = gr.Textbox(
159
+ label="Filename", value=filepaths[0], interactive=False
160
+ )
161
+
162
+ image = gr.Image(
163
+ type="filepath", label="Image", value=filepaths[0], height=500
164
+ )
165
+ category = gr.Radio(choices=self.class_names, label="Select Category")
166
+
167
+ with gr.Row():
168
+ back_btn = gr.Button("← Previous (A)", elem_id="back_btn")
169
+ submit_btn = gr.Button(
170
+ "Submit (W)",
171
+ variant="primary",
172
+ elem_id="submit_btn",
173
+ interactive=False,
174
+ )
175
+ next_btn = gr.Button("Next → (D)", elem_id="next_btn")
176
+
177
+ progress = gr.Slider(
178
+ minimum=0,
179
+ maximum=len(filepaths) - 1,
180
+ value=0,
181
+ label="Progress",
182
+ interactive=False,
183
+ )
184
+
185
+ finish_btn = gr.Button("Finish Labeling", variant="primary")
186
+
187
+ def update_submit_btn(choice):
188
+ return gr.Button(interactive=choice is not None)
189
+
190
+ category.change(
191
+ fn=update_submit_btn, inputs=[category], outputs=[submit_btn]
192
+ )
193
+
194
+ def navigate(current_idx, direction):
195
+ next_idx = current_idx + direction
196
+ if 0 <= next_idx < len(filepaths):
197
+ return filepaths[next_idx], filepaths[next_idx], next_idx, next_idx
198
+ return (
199
+ filepaths[current_idx],
200
+ filepaths[current_idx],
201
+ current_idx,
202
+ current_idx,
203
+ )
204
+
205
+ def save_and_next(current_idx, selected_category):
206
+ if selected_category is None:
207
+ return (
208
+ filepaths[current_idx],
209
+ filepaths[current_idx],
210
+ current_idx,
211
+ current_idx,
212
+ )
213
+
214
+ # Save the current annotation
215
+ with open(f"{output_filename}.csv", "a") as f:
216
+ f.write(f"{filepaths[current_idx]},{selected_category}\n")
217
+
218
+ # Move to next image if not at the end
219
+ next_idx = current_idx + 1
220
+ if next_idx >= len(filepaths):
221
+ return (
222
+ filepaths[current_idx],
223
+ filepaths[current_idx],
224
+ current_idx,
225
+ current_idx,
226
+ )
227
+ return filepaths[next_idx], filepaths[next_idx], next_idx, next_idx
228
+
229
+ def convert_csv_to_parquet():
230
+ try:
231
+ df = pd.read_csv(f"{output_filename}.csv", header=None)
232
+ df.columns = ["filepath", "label"]
233
+ df = df.drop_duplicates(subset=["filepath"], keep="last")
234
+ df.to_parquet(f"{output_filename}.parquet")
235
+ gr.Info(f"Annotation saved to {output_filename}.parquet")
236
+ except Exception as e:
237
+ logger.error(e)
238
+ return
239
+
240
+ back_btn.click(
241
+ fn=lambda idx: navigate(idx, -1),
242
+ inputs=[current_index],
243
+ outputs=[filename, image, current_index, progress],
244
+ )
245
+
246
+ next_btn.click(
247
+ fn=lambda idx: navigate(idx, 1),
248
+ inputs=[current_index],
249
+ outputs=[filename, image, current_index, progress],
250
+ )
251
+
252
+ submit_btn.click(
253
+ fn=save_and_next,
254
+ inputs=[current_index, category],
255
+ outputs=[filename, image, current_index, progress],
256
+ )
257
+
258
+ finish_btn.click(fn=convert_csv_to_parquet)
259
+
260
+ demo.launch(height=1000)
261
+
262
+ def add_to_train_set(self, df: pd.DataFrame, output_filename: str):
263
+ """
264
+ Add samples to the training set.
265
+ """
266
+ new_train_set = df.copy()
267
+ # new_train_set.drop(columns=["pred_conf"], inplace=True)
268
+ # new_train_set.rename(columns={"pred_label": "label"}, inplace=True)
269
+
270
+ # len_old = len(self.train_set)
271
+
272
+ logger.info(f"Adding {len(new_train_set)} samples to training set")
273
+ self.train_set = pd.concat([self.train_set, new_train_set])
274
+
275
+ self.train_set = self.train_set.drop_duplicates(
276
+ subset=["filepath"], keep="last"
277
+ )
278
+ self.train_set.reset_index(drop=True, inplace=True)
279
+
280
+ self.train_set.to_parquet(f"{output_filename}.parquet")
281
+ logger.info(f"Saved training set to {output_filename}.parquet")
282
+
283
+ # if len(self.train_set) == len_old:
284
+ # logger.warning("No new samples added to training set")
285
+
286
+ # elif len_old + len(new_train_set) < len(self.train_set):
287
+ # logger.warning("Some samples were duplicates and removed from training set")
288
+
289
+ # else:
290
+ # logger.info("All new samples added to training set")
291
+ # logger.info(f"Training set now has {len(self.train_set)} samples")
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: active-vision
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Active learning for edge vision.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
8
  Requires-Dist: datasets>=3.2.0
9
9
  Requires-Dist: fastai>=2.7.18
10
+ Requires-Dist: gradio>=5.12.0
10
11
  Requires-Dist: ipykernel>=6.29.5
11
12
  Requires-Dist: ipywidgets>=8.1.5
12
13
  Requires-Dist: loguru>=0.7.3
@@ -14,40 +15,53 @@ Requires-Dist: seaborn>=0.13.2
14
15
 
15
16
  ![Python Version](https://img.shields.io/badge/python-3.10%2B-blue?style=for-the-badge)
16
17
  ![License](https://img.shields.io/badge/License-Apache%202.0-green.svg?style=for-the-badge)
17
- ![PyPI](https://img.shields.io/pypi/v/active-vision?style=for-the-badge)
18
+ [![PyPI](https://img.shields.io/pypi/v/active-vision?style=for-the-badge)](https://pypi.org/project/active-vision/)
18
19
  ![Downloads](https://img.shields.io/pepy/dt/active-vision?style=for-the-badge&logo=pypi&logoColor=white&label=Downloads&color=purple)
19
20
 
20
21
  <p align="center">
21
- <img src="https://github.com/dnth/active-vision/blob/main/assets/logo.png" alt="active-vision">
22
+ <img src="https://raw.githubusercontent.com/dnth/active-vision/main/assets/logo.png" alt="active-vision">
22
23
  </p>
23
24
 
24
25
  Active learning at the edge for computer vision.
25
26
 
26
- The goal of this project is to create a framework for active learning at the edge for computer vision. We should be able to train a model on a small dataset and then use active learning to iteratively improve the model all on a local machine.
27
+ The goal of this project is to create a framework for the active learning loop for computer vision deployed on edge devices.
27
28
 
28
- ## Tech Stack
29
+ ## Installation
30
+ I recommend using [uv](https://docs.astral.sh/uv/) to set up a virtual environment and install the package. You can also use other virtual env of your choice.
29
31
 
30
- - Training framework: fastai
31
- - User interface: streamlit
32
- - Database: sqlite
33
- - Experiment tracking: wandb
32
+ If you're using uv:
34
33
 
35
- ## Installation
34
+ ```bash
35
+ uv venv
36
+ uv sync
37
+ ```
38
+ Once the virtual environment is created, you can install the package using pip.
36
39
 
37
- PyPI
40
+ Get a release from PyPI
38
41
  ```bash
39
42
  pip install active-vision
40
43
  ```
41
44
 
42
- Local install
45
+ Install from source
43
46
  ```bash
44
47
  git clone https://github.com/dnth/active-vision.git
45
48
  cd active-vision
46
49
  pip install -e .
47
50
  ```
48
51
 
52
+ > [!TIP]
53
+ > If you're using uv add a uv before the pip install command to install into your virtual environment. Eg:
54
+ > ```bash
55
+ > uv pip install active-vision
56
+ > ```
57
+
49
58
  ## Usage
50
- See the [notebook](./nbs/end-to-end.ipynb) for a complete example.
59
+ See the [notebook](./nbs/04_relabel_loop.ipynb) for a complete example.
60
+
61
+ Be sure to prepared 3 datasets:
62
+ - train: A dataframe of an existing labeled training dataset.
63
+ - unlabeled: A dataframe of unlabeled data which we will sample from using active learning.
64
+ - eval: A dataframe of labeled data which we will use to evaluate the performance of the model. (Optional)
51
65
 
52
66
  ```python
53
67
  from active_vision import ActiveLearner
@@ -56,29 +70,38 @@ import pandas as pd
56
70
  # Create an active learner instance with a model
57
71
  al = ActiveLearner("resnet18")
58
72
 
59
- # Load the dataset into the active learner
73
+ # Load dataset
60
74
  train_df = pd.read_parquet("training_samples.parquet")
61
- al.load_dataset(train_df, "filepath", "label")
75
+ al.load_dataset(df, filepath_col="filepath", label_col="label")
62
76
 
63
- # Train the model
77
+ # Train model
64
78
  al.train(epochs=3, lr=1e-3)
65
79
 
66
- # Load evaluation data
67
- eval_df = pd.read_parquet("evaluation_samples.parquet")
80
+ # Evaluate the model on a *labeled* evaluation set
81
+ accuracy = al.evaluate(eval_df, filepath_col="filepath", label_col="label")
68
82
 
69
- # Evaluate the model on a labeled evaluation set
70
- accuracy = al.evaluate(eval_df, "filepath", "label")
71
-
72
- # Get predictions from an unlabeled set
83
+ # Get predictions from an *unlabeled* set
73
84
  pred_df = al.predict(filepaths)
74
85
 
75
- # Sample low confidence predictions
86
+ # Sample low confidence predictions from unlabeled set
76
87
  uncertain_df = al.sample_uncertain(pred_df, num_samples=10)
77
88
 
78
- # Add newly labeled data to training set
79
- al.add_to_train_set(uncertain_df)
89
+ # Launch a Gradio UI to label the low confidence samples
90
+ al.label(uncertain_df, output_filename="uncertain")
80
91
  ```
81
92
 
93
+ ![Gradio UI](./assets/labeling_ui.png)
94
+
95
+ Once complete, the labeled samples will be save into a new df.
96
+ We can now add the newly labeled data to the training set.
97
+
98
+ ```python
99
+ # Add newly labeled data to training set and save as a new file active_labeled
100
+ al.add_to_train_set(labeled_df, output_filename="active_labeled")
101
+ ```
102
+
103
+ Repeat the process until the model is good enough. Use the dataset to train a larger model and deploy.
104
+
82
105
  ## Workflow
83
106
  There are two workflows for active learning at the edge that we can use depending on the availability of labeled data.
84
107
 
@@ -1,5 +1,6 @@
1
1
  datasets>=3.2.0
2
2
  fastai>=2.7.18
3
+ gradio>=5.12.0
3
4
  ipykernel>=6.29.5
4
5
  ipywidgets>=8.1.5
5
6
  loguru>=0.7.3
@@ -1,3 +0,0 @@
1
- __version__ = "0.0.2"
2
-
3
- from .core import *
@@ -1,149 +0,0 @@
1
- import pandas as pd
2
- from loguru import logger
3
- from fastai.vision.models import resnet18, resnet34
4
- from fastai.callback.all import ShowGraphCallback
5
- from fastai.vision.all import (
6
- ImageDataLoaders,
7
- aug_transforms,
8
- Resize,
9
- vision_learner,
10
- accuracy,
11
- valley,
12
- slide,
13
- minimum,
14
- steep,
15
- )
16
- import torch
17
- import torch.nn.functional as F
18
-
19
- import warnings
20
-
21
- warnings.filterwarnings("ignore", category=FutureWarning)
22
-
23
-
24
- class ActiveLearner:
25
- def __init__(self, model_name: str):
26
- self.model = self.load_model(model_name)
27
-
28
- def load_model(self, model_name: str):
29
- models = {"resnet18": resnet18, "resnet34": resnet34}
30
- logger.info(f"Loading model {model_name}")
31
- if model_name not in models:
32
- logger.error(f"Model {model_name} not found")
33
- raise ValueError(f"Model {model_name} not found")
34
- return models[model_name]
35
-
36
- def load_dataset(
37
- self,
38
- df: pd.DataFrame,
39
- filepath_col: str,
40
- label_col: str,
41
- valid_pct: float = 0.2,
42
- batch_size: int = 16,
43
- image_size: int = 224,
44
- ):
45
- logger.info(f"Loading dataset from {filepath_col} and {label_col}")
46
- self.train_set = df.copy()
47
-
48
- logger.info("Creating dataloaders")
49
- self.dls = ImageDataLoaders.from_df(
50
- df,
51
- path=".",
52
- valid_pct=valid_pct,
53
- fn_col=filepath_col,
54
- label_col=label_col,
55
- bs=batch_size,
56
- item_tfms=Resize(image_size),
57
- batch_tfms=aug_transforms(size=image_size, min_scale=0.75),
58
- )
59
- logger.info("Creating learner")
60
- self.learn = vision_learner(self.dls, self.model, metrics=accuracy).to_fp16()
61
- self.class_names = self.dls.vocab
62
- logger.info("Done. Ready to train.")
63
-
64
- def lr_find(self):
65
- logger.info("Finding optimal learning rate")
66
- self.lrs = self.learn.lr_find(suggest_funcs=(minimum, steep, valley, slide))
67
- logger.info(f"Optimal learning rate: {self.lrs.valley}")
68
-
69
- def train(self, epochs: int, lr: float):
70
- logger.info(f"Training for {epochs} epochs with learning rate: {lr}")
71
- self.learn.fine_tune(epochs, lr, cbs=[ShowGraphCallback()])
72
-
73
- def predict(self, filepaths: list[str], batch_size: int = 16):
74
- """
75
- Run inference on an unlabeled dataset. Returns a df with filepaths and predicted labels, and confidence scores.
76
- """
77
- logger.info(f"Running inference on {len(filepaths)} samples")
78
- test_dl = self.dls.test_dl(filepaths, bs=batch_size)
79
- preds, _, cls_preds = self.learn.get_preds(dl=test_dl, with_decoded=True)
80
-
81
- self.pred_df = pd.DataFrame(
82
- {
83
- "filepath": filepaths,
84
- "pred_label": [self.learn.dls.vocab[i] for i in cls_preds.numpy()],
85
- "pred_conf": torch.max(F.softmax(preds, dim=1), dim=1)[0].numpy(),
86
- }
87
- )
88
- return self.pred_df
89
-
90
- def evaluate(self, df: pd.DataFrame, filepath_col: str, label_col: str, batch_size: int = 16):
91
- """
92
- Evaluate on a labeled dataset. Returns a score.
93
- """
94
- self.eval_set = df.copy()
95
-
96
- filepaths = self.eval_set[filepath_col].tolist()
97
- labels = self.eval_set[label_col].tolist()
98
- test_dl = self.dls.test_dl(filepaths, bs=batch_size)
99
- preds, _, cls_preds = self.learn.get_preds(dl=test_dl, with_decoded=True)
100
-
101
- self.eval_df = pd.DataFrame(
102
- {
103
- "filepath": filepaths,
104
- "label": labels,
105
- "pred_label": [self.learn.dls.vocab[i] for i in cls_preds.numpy()],
106
- }
107
- )
108
-
109
- accuracy = float((self.eval_df["label"] == self.eval_df["pred_label"]).mean())
110
- logger.info(f"Accuracy: {accuracy:.2%}")
111
- return accuracy
112
-
113
- def sample_uncertain(self, df: pd.DataFrame, num_samples: int):
114
- """
115
- Sample top `num_samples` low confidence samples. Returns a df with filepaths and predicted labels, and confidence scores.
116
- """
117
- uncertain_df = df.sort_values(
118
- by="pred_conf", ascending=True
119
- ).head(num_samples)
120
- return uncertain_df
121
-
122
- def add_to_train_set(self, df: pd.DataFrame):
123
- """
124
- Add samples to the training set.
125
- """
126
- new_train_set = df.copy()
127
- new_train_set.drop(columns=["pred_conf"], inplace=True)
128
- new_train_set.rename(columns={"pred_label": "label"}, inplace=True)
129
-
130
- len_old = len(self.train_set)
131
-
132
- logger.info(f"Adding {len(new_train_set)} samples to training set")
133
- self.train_set = pd.concat([self.train_set, new_train_set])
134
-
135
- self.train_set = self.train_set.drop_duplicates(
136
- subset=["filepath"], keep="last"
137
- )
138
- self.train_set.reset_index(drop=True, inplace=True)
139
-
140
-
141
- if len(self.train_set) == len_old:
142
- logger.warning("No new samples added to training set")
143
-
144
- elif len_old + len(new_train_set) < len(self.train_set):
145
- logger.warning("Some samples were duplicates and removed from training set")
146
-
147
- else:
148
- logger.info("All new samples added to training set")
149
- logger.info(f"Training set now has {len(self.train_set)} samples")
File without changes
File without changes