Mod of Zach Muller's WWF 01_Custom.ipynb.

Here we'll take cropped images of antinodes and try to count the rings, by fashioning a regression model out of a one-class classification model and scaling the output sigmoid (via fastai's y_range parameter) so that our fitted values stay within the linear regime of the sigmoid.

And we also want to "clamp" our output between a min of about 0.2 rings and a max of 11 rings, because that's how the dataset was created; so sigmoid makes a good choice for this "clamping" too.

 

Installs & Imports

import espiownage
from espiownage.core import *
sysinfo()
print(f"espiownage version {espiownage.__version__}")
TORCH_VERSION=torch1.9.0; CUDA_VERSION=cu102
CUDA available = True, Device count = 1, Current device = 0
Device name = NVIDIA GeForce RTX 2070 with Max-Q Design
hostname: oryxpro
espiownage version 0.0.47

And import our libraries

from fastai.vision.all import *
from espiownage.core import *

Below you will find the exact imports for everything we use today

from fastcore.foundation import L
from fastcore.xtras import Path # @patch'd properties to the Pathlib module

from fastai.callback.fp16 import to_fp16
from fastai.callback.schedule import fit_one_cycle, lr_find 
from fastai.data.external import untar_data, URLs

from fastai.data.block import RegressionBlock, DataBlock
from fastai.data.transforms import get_image_files, Normalize, RandomSplitter, parent_label

from fastai.interpret import ClassificationInterpretation
from fastai.learner import Learner # imports @patch'd properties to Learner including `save`, `load`, `freeze`, and `unfreeze`
from fastai.optimizer import ranger

from fastai.vision.augment import aug_transforms, RandomResizedCrop, Resize
from fastai.vision.core import imagenet_stats
from fastai.vision.data import ImageBlock
from fastai.vision.learner import cnn_learner
from fastai.vision.utils import download_images, verify_images

Run parameters

dataset_name = 'cyclegan' # choose from: 
                            # - cleaner (*real* data that's clean-er than "preclean"), 
                            # - preclean (unedited aggregates of 15-or-more volunteers)
                            # - spnet    (original SPNet Real dataset)
                            # - cyclegan (original SPNet CGSmall dataset)
                            # - fake (newer than SPNet fake, this includes non-int ring #s)
use_wandb = False        # WandB.ai logging
project = 'count_in_crops' # project name for wandb
if use_wandb: 
    !pip install wandb -qqq
    import wandb
    from fastai.callback.wandb import *
    from fastai.callback.tracker import SaveModelCallback
    wandb.login()

Download and prepare data

path = get_data(dataset_name) / 'crops'; path
curl -L -o /home/drscotthawley/.espiownage/data/espiownage-cyclegan.tgz https://www.dropbox.com/sh/24eqya4frqg5i7c/AABf0H1J4ama2s3WGGFHv3MFa/data/espiownage-cyclegan.tgz; tar xfz /home/drscotthawley/.espiownage/data/espiownage-cyclegan.tgz -C /home/drscotthawley/.espiownage/data/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   180    0   180    0     0    642      0 --:--:-- --:--:-- --:--:--   642
100   318  100   318    0     0    464      0 --:--:-- --:--:-- --:--:--     0
100  1076    0  1076    0     0   1034      0 --:--:--  0:00:01 --:--:--  1034
100  690M  100  690M    0     0  13.6M      0  0:00:50  0:00:50 --:--:-- 26.0M
Path('/home/drscotthawley/.espiownage/data/espiownage-cyclegan/crops')
fnames = get_image_files(path)
print(f"{len(fnames)} total cropped images")
ind = 1  # pick one cropped image
fnames[ind]
3676 total cropped images
Path('/home/drscotthawley/.espiownage/data/espiownage-cyclegan/crops/steelpan_0000242_261_172_500_359_10.0.png')

For labels, we want the ring count which is the number between the last '_' and the '.png'

def label_func(x):  
    return round(float(x.stem.split('_')[-1]),2)

print(label_func(fnames[ind]))
10.0
cropsize = (300,300) # pixels
croppedrings = DataBlock(blocks=(ImageBlock, RegressionBlock(n_out=1)),
                    get_items=get_image_files,
                    splitter=RandomSplitter(),
                    get_y=label_func,
                    item_tfms=Resize(cropsize, ResizeMethod.Squish),
                    batch_tfms=[*aug_transforms(size=cropsize, flip_vert=True, max_rotate=360.0), 
                    Normalize.from_stats(*imagenet_stats)])
dls = croppedrings.dataloaders(path, bs=32)
/home/drscotthawley/envs/espi/lib/python3.9/site-packages/torch/_tensor.py:1023: UserWarning: torch.solve is deprecated in favor of torch.linalg.solveand will be removed in a future PyTorch release.
torch.linalg.solve has its arguments reversed and does not return the LU factorization.
To get the LU factorization see torch.lu, which can be used with torch.lu_solve or torch.lu_unpack.
X = torch.solve(B, A).solution
should be replaced with
X = torch.linalg.solve(A, B) (Triggered internally at  /pytorch/aten/src/ATen/native/BatchLinearAlgebra.cpp:760.)
  ret = func(*args, **kwargs)
dls.show_batch(max_n=9)

Train model

opt = ranger
y_range=(0.2,13)  # balance between "clamping" to range of real data vs too much "compression" from sigmoid nonlinearity
learn = cnn_learner(dls, resnet34, n_out=1, y_range=y_range, metrics=[mae, acc_reg05,acc_reg1,acc_reg15,acc_reg2], loss_func=MSELossFlat(), opt_func=opt)
/home/drscotthawley/envs/espi/lib/python3.9/site-packages/torch/nn/functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at  /pytorch/c10/core/TensorImpl.h:1156.)
  return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)
learn.lr_find()
/home/drscotthawley/envs/espi/lib/python3.9/site-packages/fastai/callback/schedule.py:269: UserWarning: color is redundantly defined by the 'color' keyword argument and the fmt string "ro" (-> color='r'). The keyword argument will take precedence.
  ax.plot(val, idx, 'ro', label=nm, c=color)
SuggestedLRs(valley=0.007585775572806597)
lr = 5e-3
learn.fine_tune(30, lr, freeze_epochs=2)  # accidentally ran this twice in a row :-O
epoch train_loss valid_loss mae acc_reg05 acc_reg1 acc_reg15 acc_reg2 time
0 23.690529 11.746309 2.431785 0.201361 0.374150 0.485714 0.612245 00:21
1 13.347509 6.371914 1.836660 0.202721 0.420408 0.563265 0.695238 00:21
epoch train_loss valid_loss mae acc_reg05 acc_reg1 acc_reg15 acc_reg2 time
0 8.084689 3.744010 1.464824 0.240816 0.459864 0.612245 0.737415 00:28
1 6.740868 2.491430 1.237141 0.246258 0.495238 0.680272 0.819048 00:28
2 5.408371 2.613919 1.286922 0.231293 0.466667 0.661224 0.794558 00:28
3 4.580756 1.583153 0.973145 0.336054 0.606803 0.763265 0.892517 00:27
4 3.184842 1.040220 0.815796 0.349660 0.704762 0.876190 0.946939 00:28
5 2.036166 0.566349 0.598989 0.504762 0.828571 0.955102 0.990476 00:28
6 1.409071 0.353474 0.425908 0.691156 0.922449 0.971429 0.991837 00:28
7 1.063559 0.517475 0.481554 0.657143 0.892517 0.938776 0.983673 00:28
8 1.012298 1.043140 0.790507 0.394558 0.706122 0.874830 0.946939 00:28
9 0.811525 0.347281 0.397709 0.746939 0.919728 0.965986 0.983673 00:28
10 0.641689 0.624848 0.432343 0.755102 0.921088 0.936054 0.978231 00:28
11 0.671466 0.287476 0.387851 0.738775 0.933333 0.980952 0.991837 00:28
12 0.612996 0.340356 0.431009 0.670748 0.903401 0.982313 0.998639 00:28
13 0.548205 0.142453 0.262328 0.877551 0.979592 0.991837 0.997279 00:28
14 0.537693 0.150802 0.252080 0.865306 0.978231 0.987755 0.997279 00:29
15 0.497725 0.113487 0.221978 0.900680 0.986395 0.994558 0.995918 00:29
16 0.434549 0.406680 0.326680 0.840816 0.956463 0.964626 0.987755 00:28
17 0.481495 0.091545 0.202630 0.938776 0.985034 0.993197 1.000000 00:28
18 0.363128 0.131406 0.247828 0.904762 0.985034 0.989116 0.998639 00:28
19 0.371009 0.117869 0.257246 0.923810 0.985034 0.995918 0.998639 00:28
20 0.355878 0.076294 0.205454 0.940136 0.991837 1.000000 1.000000 00:28
21 0.265829 0.064911 0.190835 0.949660 0.995918 1.000000 1.000000 00:28
22 0.251925 0.071638 0.180771 0.940136 0.994558 0.995918 1.000000 00:28
23 0.250220 0.061222 0.169487 0.957823 0.995918 0.997279 1.000000 00:28
24 0.270462 0.065909 0.167894 0.953741 0.993197 0.995918 1.000000 00:28
25 0.263908 0.063861 0.165206 0.959184 0.993197 0.995918 1.000000 00:28
26 0.287362 0.074290 0.193590 0.964626 0.991837 0.995918 1.000000 00:28
27 0.244050 0.056684 0.154601 0.960544 0.995918 0.995918 1.000000 00:28
28 0.278361 0.060150 0.162212 0.959184 0.994558 0.995918 1.000000 00:28
29 0.267751 0.058120 0.155898 0.961905 0.995918 0.995918 1.000000 00:28
learn.save(f'crop-rings-{dataset_name}')
Path('models/crop-rings-cyclegan.pth')

Interpretation

learn.load(f'crop-rings-{dataset_name}');
preds, targs, losses = learn.get_preds(with_loss=True) # validation set only
len(preds)
735

I'll define a method to show a single prediction

def showpred(ind, preds, targs, losses, dls): # show prediction at this index
    print(f"preds[{ind}] = {preds[ind]}, targs[{ind}] = {targs[ind]}, loss = {losses[ind]}")
    print(f"file = {os.path.basename(dls.valid.items[ind])}")
    print("Image:")
    dls.valid.dataset[ind][0].show()
showpred(0, preds, targs, losses, dls)
preds[0] = tensor([2.0514]), targs[0] = 2.0, loss = 0.0026428368873894215
file = steelpan_0000553_175_207_264_262_2.0.png
Image:

And now we'll run through predictions for the whole validation set:

results = []
for i in range(len(preds)):
    line_list = [dls.valid.items[i].stem]+[round(targs[i].cpu().numpy().item(),2), round(preds[i][0].cpu().numpy().item(),2), losses[i].cpu().numpy(), i]
    results.append(line_list)

# store as pandas dataframe
res_df = pd.DataFrame(results, columns=['filename', 'target', 'prediction', 'loss','i'])

We can do our own version of printing top_losses:

res_df = res_df.sort_values('loss', ascending=False)
res_df.head()
filename target prediction loss i
81 steelpan_0000527_277_45_312_86_2.0 2.0 0.20 3.2399998 81
265 steelpan_0000225_278_344_353_377_2.0 2.0 0.20 3.2399998 265
22 steelpan_0000132_208_9_331_44_2.0 2.0 0.20 3.2386274 22
667 steelpan_0000918_46_11_261_234_2.0 2.0 2.97 0.9365522 667
553 steelpan_0000654_47_152_212_327_8.0 8.0 7.10 0.8154157 553
def show_top_losses(res_df, preds, targs, losses, dls, n=5):
    for j in range(n):
        showpred(res_df.iloc[j]['i'], preds, targs, losses, dls)
        
show_top_losses(res_df, preds, targs, losses, dls)
preds[81] = tensor([0.2000]), targs[81] = 2.0, loss = 3.239999771118164
file = steelpan_0000527_277_45_312_86_2.0.png
Image:
preds[265] = tensor([0.2000]), targs[265] = 2.0, loss = 3.239999771118164
file = steelpan_0000225_278_344_353_377_2.0.png
Image:
preds[22] = tensor([0.2004]), targs[22] = 2.0, loss = 3.2386274337768555
file = steelpan_0000132_208_9_331_44_2.0.png
Image:
preds[667] = tensor([2.9678]), targs[667] = 2.0, loss = 0.9365522265434265
file = steelpan_0000918_46_11_261_234_2.0.png
Image:
preds[553] = tensor([7.0970]), targs[553] = 8.0, loss = 0.8154156804084778
file = steelpan_0000654_47_152_212_327_8.0.png
Image:

So then we can these results output into a CSV file, and use it to direct our data-cleaning efforts, i.e. look at the top-loss images first!

res_df.to_csv(f'ring_count_top_losses_{dataset_name}.csv', index=False)

When in doubt, look at the data...

Let's take a look at plots of this data

df2 = res_df.reset_index(drop=True)
plt.plot(df2["target"],'o',label='target')
plt.plot(df2["prediction"],'s', label='prediction')
plt.xlabel('Top-loss order (left=worse, right=better)')
plt.legend(loc='lower right')
plt.ylabel('Ring count')
Text(0, 0.5, 'Ring count')
plt.plot(df2["target"],df2["prediction"],'o')
plt.xlabel('Target ring count')
plt.ylabel('Predicted ring count')
plt.axis('square')
(0.5, 11.731, -0.31050000000000005, 10.9205)
print(f"Target ring count range: ({df2['target'].min()}, {df2['target'].max()})")
print(f"Predicted ring count range: ({df2['prediction'].min()}, {df2['prediction'].max()})")
Target ring count range: (1.0, 11.0)
Predicted ring count range: (0.2, 10.41)