Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.0.10 #47

Merged
merged 61 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
cad55a3
Add dotenv dep
Eve-ning Feb 14, 2024
f87471c
Add loading of .env via conf
Eve-ning Feb 14, 2024
b5b9e77
Rename polycrop and allow of value setting
Eve-ning Feb 14, 2024
b448cab
Add test for polycrop
Eve-ning Feb 14, 2024
7892492
Add rot as aug
Eve-ning Feb 14, 2024
416ec20
Merge pull request #46 from FR-DC/FRML-114
Eve-ning Feb 14, 2024
75e4588
Merge branch 'main' into 0.0.10
Eve-ning Feb 14, 2024
7dbc965
Fix issue with InceptionV3 only using RGB
Eve-ning Feb 14, 2024
d73c090
Merge pull request #48 from FR-DC/FRML-115
Eve-ning Feb 14, 2024
c5dc148
Fix issue with casuarina dataset incorrect dir
Eve-ning Feb 14, 2024
66df406
Merge pull request #49 from FR-DC/FRML-116
Eve-ning Feb 14, 2024
745c7a7
Fix issue with missing in_channels
Eve-ning Feb 15, 2024
d11452d
Adapt InceptionV3 to use multi-channels
Eve-ning Feb 15, 2024
1fa29e1
Disallow arbitrary channels with in_channel specified
Eve-ning Feb 15, 2024
f205a25
Bump gitpython from 3.1.40 to 3.1.41
dependabot[bot] Feb 15, 2024
d1ff70e
Bump pillow from 10.1.0 to 10.2.0
dependabot[bot] Feb 15, 2024
4fd81fe
Add train accuracy tracking
Eve-ning Feb 15, 2024
f02e4a7
Merge branch '0.0.10' into FRML-117
Eve-ning Feb 15, 2024
1c43769
Merge branch '0.0.10' into FRML-118
Eve-ning Feb 15, 2024
46adb35
Merge pull request #52 from FR-DC/FRML-117
Eve-ning Feb 15, 2024
6ac2841
Merge pull request #53 from FR-DC/FRML-118
Eve-ning Feb 15, 2024
37a5649
Improve explainability of adapter
Eve-ning Feb 15, 2024
74244f7
Merge branch '0.0.10' into FRML-117
Eve-ning Feb 15, 2024
0e080ee
Fix missing kernel size and stride spec
Eve-ning Feb 15, 2024
91a4e34
Dynamically fetch the kernel and stride
Eve-ning Feb 15, 2024
5493815
Merge branch 'FRML-117' of https://github.com/FR-DC/FRDC-ML into FRML…
Eve-ning Feb 15, 2024
48fb402
Fix bad access to conv layer
Eve-ning Feb 15, 2024
d5c4e67
Lint
Eve-ning Feb 15, 2024
ef8daff
Merge pull request #54 from FR-DC/FRML-117
Eve-ning Feb 15, 2024
0db79b9
Remove ReLU from first layer
Eve-ning Feb 15, 2024
eef89bf
Merge branch '0.0.10' into dependabot/pip/pillow-10.2.0
Eve-ning Feb 15, 2024
cde52a5
Revert "Remove ReLU from first layer"
Eve-ning Feb 15, 2024
6b44677
Merge branch '0.0.10' into dependabot/pip/gitpython-3.1.41
Eve-ning Feb 15, 2024
803ff9a
Merge pull request #50 from FR-DC/dependabot/pip/gitpython-3.1.41
Eve-ning Feb 15, 2024
2873d2f
Merge pull request #51 from FR-DC/dependabot/pip/pillow-10.2.0
Eve-ning Feb 15, 2024
942e7d5
Create consistency.ipynb
Eve-ning Feb 20, 2024
b2df562
Update consistency.ipynb
Eve-ning Feb 21, 2024
4a179c0
Make Standard Scaler fit on segments only
Eve-ning Feb 21, 2024
2765a59
Expose ImageNet Scaling option
Eve-ning Feb 21, 2024
b5783da
Format variable names
Eve-ning Feb 21, 2024
594f2a9
Deprecate verbose wandb helper class
Eve-ning Feb 21, 2024
33286c0
Remove unused val_iters arg
Eve-ning Feb 21, 2024
0afe80c
Remove unused val iters args
Eve-ning Feb 21, 2024
a5a008b
Unfreeze inception for better performance
Eve-ning Feb 21, 2024
6d4ba32
Disable wandb during tests
Eve-ning Feb 21, 2024
33b9108
Fix incorrect loss name index
Eve-ning Feb 21, 2024
b74c406
Fix uninitialized unl for supervised learning
Eve-ning Feb 21, 2024
9d95948
Fix imagenet_scaling not used
Eve-ning Feb 21, 2024
66bb6ca
Simplify prediction function
Eve-ning Feb 21, 2024
555d812
Make augmentation size customizable
Eve-ning Feb 21, 2024
b3e805c
Swap to EfficientNet & Update aug params
Eve-ning Feb 21, 2024
b616d42
Create efficientnetb1.py
Eve-ning Feb 21, 2024
b577933
Reduce to 1 Layer
Eve-ning Feb 21, 2024
6cdfb4f
Merge pull request #55 from FR-DC/FRML-119
Eve-ning Feb 21, 2024
c20c67e
Merge branch '0.0.10' into FRRD-60
Eve-ning Feb 21, 2024
f5724d4
Merge pull request #57 from FR-DC/FRML-120
Eve-ning Feb 21, 2024
f47c70d
Merge branch '0.0.10' into FRRD-60
Eve-ning Feb 21, 2024
0755316
Update consistency
Eve-ning Feb 22, 2024
37fe07c
Update consistency.ipynb
Eve-ning Feb 22, 2024
3e8382b
Add section about a const dataset
Eve-ning Feb 22, 2024
b1e5f22
Merge pull request #56 from FR-DC/FRRD-60
Eve-ning Feb 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 94 additions & 60 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ label-studio-sdk = "^0.0.32"
#torchvision = {version="^0.16.0", source="pytorch"}
#torchaudio = {version="^2.1.0", source="pytorch"}
#lightning = "^2.0.9.post0"
python-dotenv = "^1.0.1"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.2"
Expand Down
15 changes: 9 additions & 6 deletions src/frdc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

import label_studio_sdk as label_studio
import requests
from dotenv import load_dotenv
from google.cloud import storage as gcs

logger = logging.getLogger(__name__)

ROOT_DIR = Path(__file__).parents[2]

load_dotenv(ROOT_DIR / ".env")
LOCAL_DATASET_ROOT_DIR = ROOT_DIR / "rsc"
os.environ["GOOGLE_CLOUD_PROJECT"] = "frmodel"
GCS_PROJECT_ID = "frmodel"
Expand Down Expand Up @@ -53,7 +56,7 @@
)
GCS_BUCKET = GCS_CLIENT.bucket(GCS_BUCKET_NAME)
logger.info("Connected to GCS.")
except Exception as e:
except Exception:
logger.warning(
"Could not connect to GCS. Will not be able to download files. "
"Check that you've (1) Installed the GCS CLI and (2) Set up the"
Expand All @@ -76,11 +79,11 @@
LABEL_STUDIO_CLIENT.get_project(1)
except requests.exceptions.HTTPError:
logger.warning(
f"Could not get main annotation project. "
f"Pulling annotations may not work. "
f"It's possible that your API Key is incorrect, "
f"or somehow your .netrc is preventing you from "
f"accessing the project. "
"Could not get main annotation project. "
"Pulling annotations may not work. "
"It's possible that your API Key is incorrect, "
"or somehow your .netrc is preventing you from "
"accessing the project. "
)
except requests.exceptions.ConnectionError:
logger.warning(
Expand Down
1 change: 0 additions & 1 deletion src/frdc/evaluate/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@

4 changes: 2 additions & 2 deletions src/frdc/load/preset.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,10 @@ class FRDCDatasetPreset:
"chestnut_nature_park", "20210510", "90deg60m84.5pct255deg"
)
casuarina_20220418_183deg = FRDCDatasetPartial(
"casuarina_nature_park", "20220418", "183deg"
"casuarina", "20220418", "183deg"
)
casuarina_20220418_93deg = FRDCDatasetPartial(
"casuarina_nature_park", "20220418", "93deg"
"casuarina", "20220418", "93deg"
)
DEBUG = lambda resize=299: FRDCDatasetPartial(
site="DEBUG", date="0", version=None
Expand Down
136 changes: 136 additions & 0 deletions src/frdc/models/efficientnetb1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from copy import deepcopy

import torch
from sklearn.preprocessing import OrdinalEncoder, StandardScaler
from torch import nn
from torchvision.models import (
EfficientNet,
efficientnet_b1,
EfficientNet_B1_Weights,
)

from frdc.train.mixmatch_module import MixMatchModule
from frdc.utils.ema import EMA


class EfficientNetB1MixMatchModule(MixMatchModule):
MIN_SIZE = 320
EFF_OUT_DIMS = 1280

def __init__(
self,
*,
in_channels: int,
n_classes: int,
lr: float,
x_scaler: StandardScaler,
y_encoder: OrdinalEncoder,
ema_lr: float = 0.001,
weight_decay: float = 1e-5,
frozen: bool = True,
):
"""Initialize the EfficientNet model.

Args:
in_channels: The number of input channels.
n_classes: The number of classes.
lr: The learning rate.
x_scaler: The X input StandardScaler.
y_encoder: The Y input OrdinalEncoder.
ema_lr: The learning rate for the EMA model.
weight_decay: The weight decay.
frozen: Whether to freeze the base model.

Notes:
- Min input size: 320 x 320
"""
self.lr = lr
self.weight_decay = weight_decay

super().__init__(
n_classes=n_classes,
x_scaler=x_scaler,
y_encoder=y_encoder,
sharpen_temp=0.5,
mix_beta_alpha=0.75,
)

self.eff = efficientnet_b1(
weights=EfficientNet_B1_Weights.IMAGENET1K_V2
)

# Remove the final layer
self.eff.classifier = nn.Identity()

if frozen:
for param in self.eff.parameters():
param.requires_grad = False

# Adapt the first layer to accept the number of channels
self.eff = self.adapt_efficient_multi_channel(self.eff, in_channels)

self.fc = nn.Sequential(
nn.Linear(self.EFF_OUT_DIMS, n_classes),
nn.Softmax(dim=1),
)

# The problem is that the deep copy runs even before the module is
# initialized, which means ema_model is empty.
ema_model = deepcopy(self)
for param in ema_model.parameters():
param.detach_()

self._ema_model = ema_model
self.ema_updater = EMA(model=self, ema_model=self.ema_model)
self.ema_lr = ema_lr

@staticmethod
def adapt_efficient_multi_channel(
eff: EfficientNet,
in_channels: int,
) -> EfficientNet:
"""Adapt the EfficientNet model to accept a different number of
input channels.

Notes:
This operation is in-place, however will still return the model

Args:
eff: The EfficientNet model
in_channels: The number of input channels

Returns:
The adapted EfficientNet model.
"""
old_conv = eff.features[0][0]
new_conv = nn.Conv2d(
in_channels=in_channels,
out_channels=old_conv.out_channels,
kernel_size=old_conv.kernel_size,
stride=old_conv.stride,
padding=old_conv.padding,
bias=old_conv.bias,
)
new_conv.weight.data[:, :3] = old_conv.weight.data
new_conv.weight.data[:, 3:] = old_conv.weight.data[:, 1:2].repeat(
1, 5, 1, 1
)
eff.features[0][0] = new_conv

return eff

@property
def ema_model(self):
return self._ema_model

def update_ema(self):
self.ema_updater.update(self.ema_lr)

def forward(self, x: torch.Tensor):
"""Forward pass."""
return self.fc(self.eff(x))

def configure_optimizers(self):
return torch.optim.Adam(
self.parameters(), lr=self.lr, weight_decay=self.weight_decay
)
124 changes: 109 additions & 15 deletions src/frdc/models/inceptionv3.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from sklearn.preprocessing import OrdinalEncoder, StandardScaler
from torch import nn
from torchvision.models import Inception_V3_Weights, inception_v3
from torchvision.models.inception import BasicConv2d, Inception3

from frdc.train.mixmatch_module import MixMatchModule
from frdc.utils.ema import EMA
Expand All @@ -18,16 +19,24 @@ class InceptionV3MixMatchModule(MixMatchModule):
def __init__(
self,
*,
in_channels: int,
n_classes: int,
lr: float,
x_scaler: StandardScaler,
y_encoder: OrdinalEncoder,
ema_lr: float = 0.001,
imagenet_scaling: bool = False,
):
"""Initialize the InceptionV3 model.

Args:
n_classes: The number of output classes
in_channels: The number of input channels.
n_classes: The number of classes.
lr: The learning rate.
x_scaler: The X input StandardScaler.
y_encoder: The Y input OrdinalEncoder.
ema_lr: The learning rate for the EMA model.
imagenet_scaling: Whether to use the adapted ImageNet scaling.

Notes:
- Min input size: 299 x 299.
Expand All @@ -44,32 +53,118 @@ def __init__(
sharpen_temp=0.5,
mix_beta_alpha=0.75,
)
self.imagenet_scaling = imagenet_scaling

self.inception = inception_v3(
weights=Inception_V3_Weights.IMAGENET1K_V1,
transform_input=False,
)

# Remove the final layer
self.inception.fc = nn.Identity()

# Freeze base model
# Freeze inception weights
for param in self.inception.parameters():
param.requires_grad = False

# Adapt the first layer to accept the number of channels
self.inception = self.adapt_inception_multi_channel(
self.inception, in_channels
)

self.fc = nn.Sequential(
nn.BatchNorm1d(self.INCEPTION_OUT_DIMS),
nn.Linear(self.INCEPTION_OUT_DIMS, self.INCEPTION_OUT_DIMS // 2),
nn.BatchNorm1d(self.INCEPTION_OUT_DIMS // 2),
nn.Linear(self.INCEPTION_OUT_DIMS // 2, n_classes),
nn.Softmax(dim=1),
)

# The problem is that the deep copy runs even before the module is
# initialized, which means ema_model is empty.
ema_model = deepcopy(self)
for param in ema_model.parameters():
param.detach_()
# for param in ema_model.parameters():
# param.detach_()

self._ema_model = ema_model
self.ema_updater = EMA(model=self, ema_model=self.ema_model)
self.ema_lr = ema_lr

@staticmethod
def adapt_inception_multi_channel(
inception: Inception3,
in_channels: int,
) -> Inception3:
"""Adapt the 1st layer of the InceptionV3 model to accept n-channels.

Notes:
This operation is in-place, however will still return the model

Args:
inception: The InceptionV3 model
in_channels: The number of input channels

Returns:
The adapted InceptionV3 model.
"""

original_in_channels = inception.Conv2d_1a_3x3.conv.in_channels

# Replicate the first layer, but with a different number of channels
conv2d_1a_3x3 = BasicConv2d(
in_channels=in_channels,
out_channels=inception.Conv2d_1a_3x3.conv.out_channels,
kernel_size=inception.Conv2d_1a_3x3.conv.kernel_size,
stride=inception.Conv2d_1a_3x3.conv.stride,
)

# Copy the BGR weights from the first layer of the original model
conv2d_1a_3x3.conv.weight.data[
:, :original_in_channels
] = inception.Conv2d_1a_3x3.conv.weight.data

# We'll repeat the G weights to the other channels as an initial
# approximation
# We use [1:2] instead of [1] so it doesn't lose the dimension
conv2d_1a_3x3.conv.weight.data[
:, original_in_channels:
] = inception.Conv2d_1a_3x3.conv.weight.data[:, 1:2].tile(
(in_channels - original_in_channels, 1, 1)
)

# Finally, set the new layer back
inception.Conv2d_1a_3x3 = conv2d_1a_3x3

return inception

@staticmethod
def _imagenet_scaling(x: torch.Tensor) -> torch.Tensor:
"""Perform adapted ImageNet normalization on the input tensor.

See Also:
torchvision.models.inception.Inception3._transform_input

Notes:
This is adapted from the original InceptionV3 model, which
uses an RGB transformation. We have adapted it to accept
any number of channels.

Additional channels will use the same mean and std as the
green channel. This is because our task-domain is green-dominant.

"""
x_ch0 = (
torch.unsqueeze(x[:, 0], 1) * (0.229 / 0.5) + (0.485 - 0.5) / 0.5
)
x_ch1 = (
torch.unsqueeze(x[:, 1], 1) * (0.224 / 0.5) + (0.456 - 0.5) / 0.5
)
x_ch2 = (
torch.unsqueeze(x[:, 2], 1) * (0.225 / 0.5) + (0.406 - 0.5) / 0.5
)
x_chk = x[:, 3:] * (0.224 / 0.5) + (0.456 - 0.5) / 0.5
x = torch.cat((x_ch0, x_ch1, x_ch2, x_chk), 1)
return x

@property
def ema_model(self):
return self._ema_model
Expand All @@ -81,24 +176,22 @@ def forward(self, x: torch.Tensor):
"""Forward pass.

Notes:
- Min input size: 299 x 299.
- Batch size: >= 2.
Min input size: 299 x 299.

Args:
x: Input tensor of shape (batch_size, channels, height, width).
"""

if (
any(s == 1 for s in x.shape)
or x.shape[2] < self.MIN_SIZE
or x.shape[3] < self.MIN_SIZE
):
if x.shape[2] < self.MIN_SIZE or x.shape[3] < self.MIN_SIZE:
raise RuntimeError(
f"Input shape {x.shape} must adhere to the following:\n"
f" - No singleton dimensions\n"
f" - Size >= {self.MIN_SIZE}\n"
f"Input shape {x.shape} is too small for InceptionV3.\n"
f"Minimum size: {self.MIN_SIZE} x {self.MIN_SIZE}.\n"
f"Got: {x.shape[2]} x {x.shape[3]}."
)

if self.imagenet_scaling:
x = self._imagenet_scaling(x)

# During training, the auxiliary outputs are used for auxiliary loss,
# but during testing, only the main output is used.
if self.training:
Expand All @@ -112,4 +205,5 @@ def configure_optimizers(self):
return torch.optim.Adam(
self.parameters(),
lr=self.lr,
weight_decay=1e-5,
)
Loading
Loading