Skip to content

Commit

Permalink
Removing DLPipelines.jl; Learning method -> Learning task (#198)
Browse files Browse the repository at this point in the history
* Move DLPipelines into package

* rename method -> task

* Change decode^y -> decodeypred

* update changelog
  • Loading branch information
lorenzoh authored Mar 18, 2022
1 parent d6d03b6 commit 294f367
Show file tree
Hide file tree
Showing 56 changed files with 1,428 additions and 1,081 deletions.
17 changes: 14 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Made block-based learning method more modular. `SupervisedMethod` now supplants `BlockMethod`. [PR](https://github.com/FluxML/FastAI.jl/pull/188)
- `getencodings` and `getblocks` should now be used to get block information and encodings from a method
- See the [new tutorial training a Variational Autoencoder].
- See also the docstrings for `AbstractBlockMethod` and `SupervisedMethod`
- `getencodings` and `getblocks` should now be used to get block information and encodings from a method
- See the [new tutorial training a Variational Autoencoder].
- See also the docstrings for `AbstractBlockTask` and `SupervisedTask`

### Changed

- (BREAKING): all learning method names have been renamed to task, i.e `method*` -> `task*` and `Method*` -> `Task*`. Specifically, these exported symbols are affected:
- `BlockMethod` -> `BlockTask`,
- `describemethod` -> `describetask`,
- `methodmodel` -> `taskmodel`,
- `methoddataset` -> `taskdataset`,
- `methoddataloaders` -> `taskdataloaders`,
- `methodlossfn` -> `tasklossfn`,
- `findlearningmethods` -> `findlearningtasks`,
- `methodlearner` -> `tasklearner`,
- `savemethodmodel` -> `savetaskmodel`,
- `loadmethodmodel` -> `loadtaskmodel`
- `BlockMethod` now deprecated in favor of `SupervisedMethod`
- (INTERNAL) domain-specific functionality has moved to submodules `FastAI.Vision` (computer vision) and `FastAI.Tabular` (tabular data). Exports of `FastAI` are not affected.
- (INTERNAL) test suite now runs on InlineTest.jl
Expand Down
2 changes: 0 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340"
BSON = "fbb218c0-5317-5bc6-957e-2ee96dd4b1f0"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
DLPipelines = "e6530d7c-7faa-4ede-a0d6-9eff9baad396"
DataAugmentation = "88a5189c-e7ff-4f85-ac6b-e6158070f02e"
DataDeps = "124859b0-ceae-595e-8997-d05f6a7a8dfe"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Expand Down Expand Up @@ -45,7 +44,6 @@ Animations = "0.4"
BSON = "0.3"
CSV = "0.8, 0.9, 0.10"
Colors = "0.12"
DLPipelines = "0.3"
DataAugmentation = "0.2.4"
DataDeps = "0.7"
DataFrames = "1"
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ Pkg.add("FastAI")

or try it out with this [Google Colab template](https://colab.research.google.com/gist/lorenzoh/2fdc91f9e42a15e633861c640c68e5e8).


As an example, here is how to train an image classification model:

```julia
using FastAI
data, blocks = loaddataset("imagenette2-160", (Image, Label))
method = ImageClassificationSingle(blocks)
learner = methodlearner(method, data, callbacks=[ToGPU()])
task = ImageClassificationSingle(blocks)
learner = tasklearner(task, data, callbacks=[ToGPU()])
fitonecycle!(learner, 10)
showoutputs(method, learner)
showoutputs(task, learner)
```

Please read [the documentation](https://fluxml.github.io/FastAI.jl/dev) for more information and see the [setup instructions](docs/setup.md).
12 changes: 6 additions & 6 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@

### High-level

Quickly get started training and finetuning models using already implemented learning methods and callbacks.
Quickly get started training and finetuning models using already implemented learning tasks and callbacks.

{.tight}
- [`methodlearner`](#)
- [`tasklearner`](#)
- [`fit!`](#)
- [`fitonecycle!`](#)
- [`finetune!`](#)
- [`BlockMethod`](#)
- [`BlockTask`](#)
- callbacks

### Mid-level

{.tight}
- [`Learner`](#)
- [`methodmodel`](#)
- [`methodlossfn`](#)
- [`taskmodel`](#)
- [`tasklossfn`](#)

### Low-level

{.tight}
- [`LearningMethod`](#)
- [`LearningTask`](#)
- [`encode`](#)
- [`encodeinput`](#)
- `decodey`
Expand Down
27 changes: 14 additions & 13 deletions docs/background/blocksencodings.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ _Unstructured notes on blocks and encodings_
- For example, for supervised learning tasks, there is an input block and a target block and we want to learn to predict targets from inputs. Learning to predict a cat/dog label (`Label(["cat", "dog"])`) from 2D images (`Image{2}()`) is a supervised image classification task.
- A block is not a piece of data itself. Instead it describes the meaning of a piece of data in a context. That a piece of data is a block can be checked using [`checkblock`]`(block, data)`. A piece of data for the `Label` block above needs to be one of the labels, so `checkblock(Label(["cat", "dog"]), "cat") == true`, but `checkblock(Label(["cat", "dog"]), "cat") == false`.
- We can say that a data container is compatible with a learning method if every observation in it is a valid sample of the sample block of the learning method. The sample block for supervised tasks is `sampleblock = (inputblock, targetblock)` so `sample = getobs(data, i)` from a compatible data container implies that `checkblock(sampleblock, sample)`. This also means that any data stored in blocks must not depend on individual samples; we can store the names of possible classes inside the `Label` block because they are the same across the whole dataset.
- We can say that a data container is compatible with a learning task if every observation in it is a valid sample of the sample block of the learning task. The sample block for supervised tasks is `sampleblock = (inputblock, targetblock)` so `sample = getobs(data, i)` from a compatible data container implies that `checkblock(sampleblock, sample)`. This also means that any data stored in blocks must not depend on individual samples; we can store the names of possible classes inside the `Label` block because they are the same across the whole dataset.


## Data pipelines
Expand Down Expand Up @@ -40,7 +40,7 @@ Where do we draw the line between model and data processing? In general, the enc
{cell=main}
```julia
using FastAI, Colors
using FastAI: ImageTensor
using FastAI.Vision: ImageTensor
enc = ImagePreprocessing()
data = rand(RGB, 100, 100)
@show summary(data)
Expand Down Expand Up @@ -70,6 +70,7 @@ Where do we draw the line between model and data processing? In general, the enc
```
{cell=main}
```julia
using FastAI: encodedblockfilled
encodedblockfilled(enc, Label(1:10)) == Label(1:10)
```
- Encodings can be applied to tuples of blocks. The default behavior is to apply the encoding to each block separately.
Expand All @@ -80,15 +81,15 @@ Where do we draw the line between model and data processing? In general, the enc

- Applying a tuple of encodings will encode the data by applying one encoding after the other. When decoding, the order is reversed.

## Block learning methods
## Block learning tasks

[`BlockMethod`](#) creates a learning method from blocks and encodings. You define the sample block (recall for supervised tasks this is a tuple of input and target) and a sequence of encodings that are applied to all blocks.
[`BlockTask`](#) creates a learning task from blocks and encodings. You define the sample block (recall for supervised tasks this is a tuple of input and target) and a sequence of encodings that are applied to all blocks.

The below example defines the same learning method as [`ImageClassificationSingle`](#) does. The first two encodings only change `Image`, and the last changes only `Label`, so it's simple to understand.
The below example defines the same learning task as [`ImageClassificationSingle`](#) does. The first two encodings only change `Image`, and the last changes only `Label`, so it's simple to understand.

{cell=main}
```julia
method = BlockMethod(
task = BlockTask(
(Image{2}(), Label(["cats", "dogs"])),
(
ProjectiveTransforms((128, 128)),
Expand All @@ -104,23 +105,23 @@ Now `encode` expects a sample and just runs the encodings over that, giving us a
```julia
data = loadfolderdata(joinpath(datasetpath("dogscats"), "train"), filterfn=isimagefile, loadfn=(loadfile, parentname))
sample = getobs(data, 1)
x, y = encode(method, Training(), sample)
x, y = encodesample(task, Training(), sample)
summary(x), summary(y)
```

This is equivalent to:

{cell=main}
```julia
x, y = encode(method.encodings, Training(), method.blocks, sample)
x, y = encode(task.encodings, Training(), FastAI.getblocks(task).sample, sample)
summary(x), summary(y)
```

Image segmentation looks almost the same except we use a `Mask` block as target. We're also using `OneHot` here, because it also has an `encode` method for `Mask`s. For this method, `ProjectiveTransforms` will be applied to both the `Image` and the `Mask`, using the same random state for cropping and augmentation.
Image segmentation looks almost the same except we use a `Mask` block as target. We're also using `OneHot` here, because it also has an `encode` task for `Mask`s. For this task, `ProjectiveTransforms` will be applied to both the `Image` and the `Mask`, using the same random state for cropping and augmentation.

{cell=main}
```julia
method = BlockMethod(
task = BlockTask(
(Image{2}(), Mask{2}(1:10)),
(
ProjectiveTransforms((128, 128)),
Expand All @@ -130,18 +131,18 @@ method = BlockMethod(
)
```

The easiest way to understand how encodings are applied to each block is to use [`describemethod`](#) and [`describeencodings`](#) which print a table of how each encoding is applied successively to each block. Rows where a block is **bolded** indicate that the data was transformed by that encoding.
The easiest way to understand how encodings are applied to each block is to use [`describetask`](#) and [`describeencodings`](#) which print a table of how each encoding is applied successively to each block. Rows where a block is **bolded** indicate that the data was transformed by that encoding.

{cell=main}
```julia
describemethod(method)
describetask(task)
```

The above tables make it clear what happens during training ("encoding a sample") and inference (encoding an input and "decoding an output"). The more general form [`describeencodings`](#) takes in encodings and blocks directly and can be useful for building an understanding of how encodings apply to some blocks.

{cell=main}
```julia
FastAI.describeencodings(method.encodings, (Image{2}(),))
FastAI.describeencodings(task.encodings, (Image{2}(),))
```

{cell=main}
Expand Down
20 changes: 10 additions & 10 deletions docs/background/datapipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ using FastAI
using FastAI.Datasets

data = loadtaskdata(datasetpath("imagenette2-320"), ImageClassification)
method = ImageClassification(Datasets.getclassesclassification("imagenette2-320"), (224, 224))
task = ImageClassification(Datasets.getclassesclassification("imagenette2-320"), (224, 224))

# maps data processing over `data`
methoddata = methoddataset(data, method, Training())
taskdata = taskdataset(data, task, Training())

# creates a data container of collated batches
batchdata = batchviewcollated(methoddata, 16)
batchdata = batchviewcollated(taskdata, 16)

NBATCHES = 200

Expand All @@ -50,7 +50,7 @@ end

Running each timer twice to forego compilation time, the sequential iterator takes 20 seconds while the parallel iterator using 11 background threads only takes 2.5 seconds. This certainly isn't a proper benchmark, but it shows the performance can be improved by an order of magnitude with no effort.

Beside increasing the amount of compute available with worker threads as above, the data loading performance can also be improved by reducing the time it takes to load a single batch. Since a batch is made up of some number of observations, this usually boils down to reducing the loading time of a single observation. If you're using the `LearningMethod` API, this can be further broken down into the loading and encoding part.
Beside increasing the amount of compute available with worker threads as above, the data loading performance can also be improved by reducing the time it takes to load a single batch. Since a batch is made up of some number of observations, this usually boils down to reducing the loading time of a single observation. If you're using the `LearningTask` API, this can be further broken down into the loading and encoding part.

## Measuring performance

Expand All @@ -69,8 +69,8 @@ using FastAI.Datasets
using FluxTraining: step!

data = loaddataset("imagenette2-320", (Image, Label))
method = ImageClassificationSingle(blocks)
learner = methodlearner(method, data)
task = ImageClassificationSingle(blocks)
learner = tasklearner(task, data)

NBATCHES = 100

Expand Down Expand Up @@ -110,17 +110,17 @@ end
# Time it takes to encode an `(image, class)` observation into `(x, y)`
obss = [getobs(data, i) for i in 1:N]
@btime for i in 1:N
encode(method, Training(), obss[i])
encodesample(task, Training(), obss[i])
end
```

This will give you a pretty good idea of where the performance bottleneck is. Note that the encoding performance is often dependent of the method configuration. If we used `ImageClassification` with input size `(64, 64)` it would be much faster.
This will give you a pretty good idea of where the performance bottleneck is. Note that the encoding performance is often dependent of the task configuration. If we used `ImageClassification` with input size `(64, 64)` it would be much faster.

## Improving performance

So, you've identified the data pipeline as a performance bottleneck. What now? Before anything else, make sure you're doing the following:

- Use `DataLoaders.DataLoader` as a data iterator. If you're using [`methoddataloaders`](#) or [`methodlearner`](#), this is already the case.
- Use `DataLoaders.DataLoader` as a data iterator. If you're using [`taskdataloaders`](#) or [`tasklearner`](#), this is already the case.
- Start Julia with multiple threads by specifying the `-t n`/`-t auto` flag when starting Julia. If it is successful, `Threads.nthreads()` should be larger than `1`.

If the data loading is still slowing down training, you'll probably have to speed up the loading of each observation. As mentioned above, this can be broken down into observation loading and encoding. The exact strategy will depend on your use case, but here are some examples.
Expand All @@ -142,7 +142,7 @@ data_160px, _ = loaddataset("imagenette2-160", (Image, Label))

### Reducing allocations with inplace operations

When implementing the `LearningMethod` interface, you have the option to implement `encode!(buf, method, context, sample)`, an inplace version of `encode` that reuses a buffer to avoid allocations. Reducing allocations often speeds up the encoding step and can also reduce the frequency of garbage collector pauses during training which can reduce GPU utilization.
When implementing the `LearningTask` interface, you have the option to implement `encode!(buf, task, context, sample)`, an inplace version of `encode` that reuses a buffer to avoid allocations. Reducing allocations often speeds up the encoding step and can also reduce the frequency of garbage collector pauses during training which can reduce GPU utilization.

### Using efficient data augmentation

Expand Down
14 changes: 7 additions & 7 deletions docs/discovery.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Discovery

As you may have seen in [the introduction](./introduction.md), FastAI.jl makes it possible to train models in just 5 lines of code. However, if you have a task in mind, you need to know what datasets you can train on and if there are convenience learning method constructors. For example, the introduction loads the `"imagenette2-160"` dataset and uses [`ImageClassificationSingle`](#) to construct a learning method. Now what if, instead of classifying an image into one class, we want to classify every single pixel into a class (semantic segmentation)? Now we need a dataset with pixel-level annotations and a learning method that can process those segmentation masks.
As you may have seen in [the introduction](./introduction.md), FastAI.jl makes it possible to train models in just 5 lines of code. However, if you have a task in mind, you need to know what datasets you can train on and if there are convenience learning task constructors. For example, the introduction loads the `"imagenette2-160"` dataset and uses [`ImageClassificationSingle`](#) to construct a learning task. Now what if, instead of classifying an image into one class, we want to classify every single pixel into a class (semantic segmentation)? Now we need a dataset with pixel-level annotations and a learning task that can process those segmentation masks.

For finding both, we can make use of `Block`s. A `Block` represents a kind of data, for example images, labels or keypoints. For supervised learning tasks, we have an input block and a target block. If we wanted to classify whether 2D images contain a cat or a dog, we could use the blocks `(Image{2}(), Label(["cat", "dog"]))`, while for semantic segmentation, we'll have an input `Image` block and a target [`Mask`](#) block.

Expand Down Expand Up @@ -59,24 +59,24 @@ In short, if you have a learning task in mind and want to load a dataset for tha
2. List all datasets with `Image` as input block and any target block. (Hint: the supertype of all types is `Any`)


## Finding a learning method
## Finding a learning task

Armed with a dataset, we can go to the next step: creating a learning method. Since we already have blocks defined, this amounts to defining the encodings that are applied to the data before it is used in training. Here, FastAI.jl already defines some convenient constructors for learning methods and you can find them with [`findlearningmethods`](#). Here we can pass in either block types as above or the block instances we got from `loaddataset`.
Armed with a dataset, we can go to the next step: creating a learning task. Since we already have blocks defined, this amounts to defining the encodings that are applied to the data before it is used in training. Here, FastAI.jl already defines some convenient constructors for learning tasks and you can find them with [`findlearningtasks`](#). Here we can pass in either block types as above or the block instances we got from `loaddataset`.

{cell=main}
```julia
findlearningmethods(blocks)
findlearningtasks(blocks)
```

Looks like we can use the [`ImageSegmentation`](#) function to create a learning method for our learning task. Every function returned can be called with `blocks` and, optionally, some keyword arguments for customization.
Looks like we can use the [`ImageSegmentation`](#) function to create a learning task for our learning task. Every function returned can be called with `blocks` and, optionally, some keyword arguments for customization.

{cell=main}
```julia
method = ImageSegmentation(blocks; size = (64, 64))
task = ImageSegmentation(blocks; size = (64, 64))
```

And that's the basic workflow for getting started with a supervised task.

### Exercises

1. Find all learning method functions with images as inputs.
1. Find all learning task functions with images as inputs.
Loading

0 comments on commit 294f367

Please sign in to comment.