active-vision 0.0.2__tar.gz → 0.0.3__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -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