Pipelines¶
Pipelines are an integral part of river. We encourage their usage and apply them in many of their examples.
The compose.Pipeline
contains all the logic for building and applying pipelines. A pipeline is essentially a list of estimators that are applied in sequence. The only requirement is that the first n - 1
steps be transformers. The last step can be a regressor, a classifier, a clusterer, a transformer, etc. Here is an example:
from river import compose
from river import linear_model
from river import preprocessing
from river import feature_extraction
model = compose.Pipeline(
preprocessing.StandardScaler(),
feature_extraction.PolynomialExtender(),
linear_model.LinearRegression()
)
You can also use the |
operator, as so:
model = (
preprocessing.StandardScaler() |
feature_extraction.PolynomialExtender() |
linear_model.LinearRegression()
)
Or, equally:
model = preprocessing.StandardScaler()
model |= feature_extraction.PolynomialExtender()
model |= linear_model.LinearRegression()
A pipeline has a draw
method that can be used to visualize it:
model
StandardScaler
{'counts': Counter(),
'means': defaultdict(<class 'float'>, {}),
'vars': defaultdict(<class 'float'>, {}),
'with_std': True}
PolynomialExtender
{'bias_name': 'bias',
'degree': 2,
'include_bias': False,
'interaction_only': False}
LinearRegression
{'_weights': {},
'_y_name': None,
'clip_gradient': 1000000000000.0,
'initializer': Zeros (),
'intercept': 0.0,
'intercept_init': 0.0,
'intercept_lr': Constant({'learning_rate': 0.01}),
'l2': 0.0,
'loss': Squared({}),
'optimizer': SGD({'lr': Constant({'learning_rate': 0.01}), 'n_iterations': 0})}
compose.Pipeline
inherits from base.Estimator
, which means that it has a learn_one
method. You would expect learn_one
to update each estimator, but that's not actually what happens. Instead, the transformers are updated when predict_one
(or predict_proba_one
for that matter) is called. Indeed, in online machine learning, we can update the unsupervised parts of our model when a sample arrives. We don't have to wait for the ground truth to arrive in order to update unsupervised estimators that don't depend on it. In other words, in a pipeline, learn_one
updates the supervised parts, whilst predict_one
updates the unsupervised parts. It's important to be aware of this behavior, as it is quite different to what is done in other libraries that rely on batch machine learning.
Here is a small example to illustrate the previous point:
from river import datasets
dataset = datasets.TrumpApproval()
x, y = next(iter(dataset))
x, y
({'ordinal_date': 736389,
'gallup': 43.843213,
'ipsos': 46.19925042857143,
'morning_consult': 48.318749,
'rasmussen': 44.104692,
'you_gov': 43.636914000000004},
43.75505)
Let us call predict_one
, which will update each transformer, but won't update the linear regression.
model.predict_one(x)
0.0
The prediction is nil because each weight of the linear regression is equal to 0.
model['StandardScaler'].means
defaultdict(float,
{'ordinal_date': 736389.0,
'gallup': 43.843213,
'ipsos': 46.19925042857143,
'morning_consult': 48.318749,
'rasmussen': 44.104692,
'you_gov': 43.636914000000004})
As we can see, the means of each feature have been updated, even though we called predict_one
and not learn_one
.
Note that if you call transform_one
with a pipeline who's last step is not a transformer, then the output from the last transformer (which is thus the penultimate step) will be returned:
model.transform_one(x)
{'ordinal_date': 0.0,
'gallup': 0.0,
'ipsos': 0.0,
'morning_consult': 0.0,
'rasmussen': 0.0,
'you_gov': 0.0,
'ordinal_date*ordinal_date': 0.0,
'gallup*ordinal_date': 0.0,
'ipsos*ordinal_date': 0.0,
'morning_consult*ordinal_date': 0.0,
'ordinal_date*rasmussen': 0.0,
'ordinal_date*you_gov': 0.0,
'gallup*gallup': 0.0,
'gallup*ipsos': 0.0,
'gallup*morning_consult': 0.0,
'gallup*rasmussen': 0.0,
'gallup*you_gov': 0.0,
'ipsos*ipsos': 0.0,
'ipsos*morning_consult': 0.0,
'ipsos*rasmussen': 0.0,
'ipsos*you_gov': 0.0,
'morning_consult*morning_consult': 0.0,
'morning_consult*rasmussen': 0.0,
'morning_consult*you_gov': 0.0,
'rasmussen*rasmussen': 0.0,
'rasmussen*you_gov': 0.0,
'you_gov*you_gov': 0.0}
In many cases, you might want to connect a step to multiple steps. For instance, you might to extract different kinds of features from a single input. An elegant way to do this is to use a compose.TransformerUnion
. Essentially, the latter is a list of transformers who's results will be merged into a single dict
when transform_one
is called. As an example let's say that we want to apply a feature_extraction.RBFSampler
as well as the feature_extraction.PolynomialExtender
. This may be done as so:
model = (
preprocessing.StandardScaler() |
(feature_extraction.PolynomialExtender() + feature_extraction.RBFSampler()) |
linear_model.LinearRegression()
)
model
StandardScaler
{'counts': Counter(),
'means': defaultdict(<class 'float'>, {}),
'vars': defaultdict(<class 'float'>, {}),
'with_std': True}
PolynomialExtender
{'bias_name': 'bias',
'degree': 2,
'include_bias': False,
'interaction_only': False}
RBFSampler
{'gamma': 1.0,
'n_components': 100,
'offsets': [5.348874741614939,
4.735909930156044,
5.971092019602209,
4.367066015412262,
3.127920534845864,
0.37392983130489404,
3.64824799417614,
0.9467523114609028,
5.8460482007890056,
0.21307093951510905,
4.296352761978815,
4.885769519990044,
4.4974275169291085,
2.3157102224489563,
3.749833378142358,
2.650446916492836,
6.179524681887679,
0.6428056908424152,
6.207915839437681,
3.6270421065763347,
4.9375786872844785,
2.9626426323035275,
5.5300971896639135,
5.501246300206098,
1.4518311724518067,
5.64367712495883,
5.803773079669503,
3.6551342774863174,
2.9461038799076684,
4.985666339020469,
2.829278375264934,
5.928601203848413,
1.7039114011364669,
4.178211376970135,
6.154630345212183,
2.5295801347270843,
5.486208799602425,
5.267393102901959,
4.777894616956858,
1.7786228575507839,
4.578471314736089,
4.484786739021398,
0.24868080592290748,
3.2753589193592405,
4.88070037041504,
1.466571448234209,
3.3264601048294318,
1.6590733434016245,
1.1985801914557876,
0.8015729494141526,
1.138657196539614,
0.9764996056622854,
3.9475109775417643,
5.205143102507694,
1.4834399693366571,
4.644479900966495,
3.0155675114154277,
5.877260181566818,
2.3093714765172066,
3.7947136561550274,
0.15920684950500258,
2.9466639019228342,
3.1057280084094794,
1.7443894329049054,
0.3875973350518237,
3.5347225214041136,
0.8504350133150541,
0.698485808984114,
6.087184152921054,
5.79948683389869,
3.0803641718998853,
3.8924904929785678,
3.4454558277232183,
5.26885835787588,
0.09782171801688797,
4.689137884938644,
3.669006644937924,
0.5980361662723929,
1.1154430857926654,
6.194009694846217,
3.661618895046223,
4.608763720258219,
1.836124107933639,
0.5336374459154921,
0.8497883101114175,
5.176565671923488,
1.1821322482060077,
3.984283408892691,
0.7075194478910113,
3.8860793510593235,
4.9542207502329685,
2.148945911436336,
5.838103273908845,
4.67313705294741,
4.285749664663917,
3.3484438176069045,
4.6510149715919615,
5.148716656031428,
5.199652712940212,
5.967069610033551],
'rng': <random.Random object at 0x560b2ed68b70>,
'seed': None,
'weights': defaultdict(<bound method RBFSampler._random_weights of RBFSampler (
gamma=1.
n_components=100
seed=None
)>, {})}
LinearRegression
{'_weights': {},
'_y_name': None,
'clip_gradient': 1000000000000.0,
'initializer': Zeros (),
'intercept': 0.0,
'intercept_init': 0.0,
'intercept_lr': Constant({'learning_rate': 0.01}),
'l2': 0.0,
'loss': Squared({}),
'optimizer': SGD({'lr': Constant({'learning_rate': 0.01}), 'n_iterations': 0})}
Note that the +
symbol acts as a shorthand notation for creating a compose.TransformerUnion
, which means that we could have declared the above pipeline as so:
model = (
preprocessing.StandardScaler() |
compose.TransformerUnion(
feature_extraction.PolynomialExtender(),
feature_extraction.RBFSampler()
) |
linear_model.LinearRegression()
)
Pipelines provide the benefit of removing a lot of cruft by taking care of tedious details for you. They also enable to clearly define what steps your model is made of. Finally, having your model in a single object means that you can move it around more easily. Note that you can include user-defined functions in a pipeline by using a compose.FuncTransformer
.