https://d2l.ai/chapter_linear-regression/linear-regression-concise.html

When I try to use Huber Loss, I am getting some error which i am unable to decode it.Can you please help me out

Hi @Rosetta, thanks for raising the issue! `HuberLoss`

may have some implementation issues, we will tackle it.

The Gluon model weight matrix W.shape is (1, 2), is different from the W.shape (2, 1) in the last section, I guess that the Gluon model use XW^T + b equation, so in this code example,

‘’

w = net[0].weight.data()

print(‘Error in estimating w’, true_w.reshape(w.shape) - w)

b = net[0].bias.data()

print(‘Error in estimating b’, true_b - b)

‘’

w.T is needed or not?

Hi @liwei, you are correct! Gluon uses $XW^T + b$. Even though the shape are flipped, it doesn’t affect the final value.

trainer.step normalizes the loss by 1/batch_size. when we already take the loss.mean(), loss get normalized already so there is no need to normalize again in step.

Hi @sahu.vaibhav, trainer.step() will update the weights, while loss measures the difference between truth and predictions. That’s why we use loss.mean().

I am getting this error:

`TypeError: Operator `abs` registered in backend is known as `abs` in Python. This is a legacy operator which can only accept legacy ndarrays, while received an MXNet numpy ndarray. Please call `as_nd_ndarray()` upon the numpy ndarray to convert it to a legacy ndarray, and then feed the converted array to this operator.`

What can be the cause? It was working fine when I used L2loss , but this error popped up when I used Huber’s Loss.

I’m getting the same error. Any way to solve this? Any help would be much appreciated.

In order to solve the issue of **TypeError: Operator abs registered in backend is known as abs …**

I’ve edited python file

**mxnet/gluon/loss.py**installed in my package and changed class

**HuberLoss**with

**MyHuberLoss**.

Reason of failure, as far as I understand, is described into docstring of function **hybrid_forward** implemented into class **MyHuberLoss**.

Then I used **MyHuberLoss** in my notebook in order to experiment Huber loss function

Here is the hack and the modified class :

```
import mxnet
from mxnet.gluon import loss
from mxnet.gluon.loss import _reshape_like
from mxnet.gluon.loss import _apply_weighting
import mxnet.numpy as np
class MyHuberLoss(loss.Loss):
r"""Calculates smoothed L1 loss that is equal to L1 loss if absolute error
exceeds rho but is equal to L2 loss otherwise. Also called SmoothedL1 loss.
.. math::
L = \sum_i \begin{cases} \frac{1}{2 {rho}} ({label}_i - {pred}_i)^2 &
\text{ if } |{label}_i - {pred}_i| < {rho} \\
|{label}_i - {pred}_i| - \frac{{rho}}{2} &
\text{ otherwise }
\end{cases}
`label` and `pred` can have arbitrary shape as long as they have the same
number of elements.
Parameters
----------
rho : float, default 1
Threshold for trimmed mean estimator.
weight : float or None
Global scalar weight for loss.
batch_axis : int, default 0
The axis that represents mini-batch.
Inputs:
- **pred**: prediction tensor with arbitrary shape
- **label**: target tensor with the same size as pred.
- **sample_weight**: element-wise weighting tensor. Must be broadcastable
to the same shape as pred. For example, if pred has shape (64, 10)
and you want to weigh each sample in the batch separately,
sample_weight should have shape (64, 1).
Outputs:
- **loss**: loss tensor with shape (batch_size,). Dimenions other than
batch_axis are averaged out.
"""
def __init__(self, rho=1, weight=None, batch_axis=0, **kwargs):
super(MyHuberLoss, self).__init__(weight, batch_axis, **kwargs)
self._rho = rho
def hybrid_forward(self, F, pred, label, sample_weight=None):
'''This function uses array operator from module mxnet.ndarray
This module is recorded in variable F, that is given as parameter function.
Parameters pred and label, in the context of this exercise, both use array type
mxnet.numpy.ndarray
Triggered error show that module mxnet.ndarray does not support operators
such as abs and sqrt to be applied to mxnet.ndarray type.
Then, in order to keep direct graph safe, rather then changing array types in order to
support operators from module mxnet.ndarray, I changed module into F to module
mxnet.numpy, that is compliant with array type mxnet.numpy.ndarray.
'''
is_mxnet_numpy = False
if type(label) == mxnet.numpy.ndarray or type(pred) == mxnet.numpy.ndarray:
is_mxnet_numpy = False
label = _reshape_like(F, label, pred)
if is_mxnet_numpy:
F = mxnet.numpy
else :
pass
loss = F.abs(label - pred)
loss = F.where(loss > self._rho, loss - 0.5 * self._rho,
(0.5 / self._rho) * F.square(loss))
loss = _apply_weighting(F, loss, self._weight, sample_weight)
if is_mxnet_numpy :
loss_mean = F.mean(loss, axis=self._batch_axis)
else :
loss_mean = F.mean(loss, axis=self._batch_axis, exclude=True)
return loss_mean
```

```
import mxnet
from mxnet.gluon import loss
from mxnet.gluon.loss import _reshape_like
from mxnet.gluon.loss import _apply_weighting
import mxnet.numpy as np
class MyHuberLoss(loss.Loss):
r"""Calculates smoothed L1 loss that is equal to L1 loss if absolute error
exceeds rho but is equal to L2 loss otherwise. Also called SmoothedL1 loss.
.. math::
L = \sum_i \begin{cases} \frac{1}{2 {rho}} ({label}_i - {pred}_i)^2 &
\text{ if } |{label}_i - {pred}_i| < {rho} \\
|{label}_i - {pred}_i| - \frac{{rho}}{2} &
\text{ otherwise }
\end{cases}
`label` and `pred` can have arbitrary shape as long as they have the same
number of elements.
Parameters
----------
rho : float, default 1
Threshold for trimmed mean estimator.
weight : float or None
Global scalar weight for loss.
batch_axis : int, default 0
The axis that represents mini-batch.
Inputs:
- **pred**: prediction tensor with arbitrary shape
- **label**: target tensor with the same size as pred.
- **sample_weight**: element-wise weighting tensor. Must be broadcastable
to the same shape as pred. For example, if pred has shape (64, 10)
and you want to weigh each sample in the batch separately,
sample_weight should have shape (64, 1).
Outputs:
- **loss**: loss tensor with shape (batch_size,). Dimenions other than
batch_axis are averaged out.
"""
def __init__(self, rho=1, weight=None, batch_axis=0, **kwargs):
super(MyHuberLoss, self).__init__(weight, batch_axis, **kwargs)
self._rho = rho
def hybrid_forward(self, F, pred, label, sample_weight=None):
'''This function uses array operator from module mxnet.ndarray
This module is recorded in variable F, that is given as parameter function.
Parameters pred and label, in the context of this exercise, both use array type
mxnet.numpy.ndarray
Triggered error show that module mxnet.ndarray does not support operators
such as abs and sqrt to be applied to mxnet.ndarray type.
Then, in order to keep direct graph safe, rather then changing array types in order to
support operators from module mxnet.ndarray, I changed module into F to module
mxnet.numpy, that is compliant with array type mxnet.numpy.ndarray.
'''
is_mxnet_numpy = False
if type(label) == mxnet.numpy.ndarray or type(pred) == mxnet.numpy.ndarray:
is_mxnet_numpy = True
label = _reshape_like(F, label, pred)
if is_mxnet_numpy:
F = mxnet.numpy
else :
pass
loss = F.abs(label - pred)
loss = F.where(loss > self._rho, loss - 0.5 * self._rho,
(0.5 / self._rho) * F.square(loss))
loss = _apply_weighting(F, loss, self._weight, sample_weight)
if is_mxnet_numpy :
loss_mean = F.mean(loss, axis=self._batch_axis)
else :
loss_mean = F.mean(loss, axis=self._batch_axis, exclude=True)
return loss_mean
```

As a result, using Huber loss tooks twice number of epochs then L2 loss for getting the same precision for average error over the whole dataset.

I think this is due to mixted slopes provided in Huber loss function. Checking documentation, it is showed that when when error is less or equal Ro parameter in between prediction and true value, then loss function comes as L2 loss function. When error is greater then Ro parameter, then loss function comes as a straight line from which, slope (then gradient) has a lower magnitude then L2 slope.

I’ve no other idea…

@akpd2l @rafribeiro @FBT, the current stable version of `mxnet.gluon.loss.HuberLoss`

does not support operations on numpy arrays, but one can implement loss functions from scratch as it was done in 3.2.6.

First, recall that the Huber loss function is defined as

```
huber_loss(y_hat, y; rho) = 0.5/rho * (y_hat - y)**2 if abs(y_hat - y) <= rho
else abs(y_hat - y) - 0.5*rho
```

It becomes L2 loss for relatively small absolute residuals, and L1 loss, for large ones. So, it is smooth and less influenced by outliers. A switch is controlled by the (hyper)parameter `rho`

. One can choose it experimentally or by measuring the variability of the inliers (e.g. `rho=median(y - median(y))`

).

Now, define a new python function

```
def huber_loss(y_hat, y, rho=1.):
loss = np.abs(y_hat - y)
loss = np.where(loss <= rho, 0.5/rho * loss**2, loss - 0.5*rho)
return loss
```

and in 3.3.7 replace `l = loss(net(X), y)`

with `l = huber_loss(net(X), y)`

.

@FBT, when comparing two different loss functions used in training, it is more reasonable to choose one common measure for evaluation. In 3.3.7, `loss`

is used for both optimization (line `l = loss(net(X), y)`

) and evaluation (line `l = loss(net(features), labels)`

). Instead, choose a new loss function for evaluation (e.g. L1 loss as it is more descriptive) and compare performances of `MyHuberLoss`

and `gluon.loss.L2Loss`

during training:

```
loss_eval = gluon.loss.L1Loss()
l2loss = gluon.loss.L2Loss()
huber_loss = MyHuberLoss(rho=0.5) # don't forget to adjust `rho`
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
with autograd.record():
l = huber_loss(net(X), y) # or l = l2loss(net(X), y)
l.backward()
trainer.step(batch_size)
l = loss_eval(net(features), labels)
print(f'epoch {epoch + 1}, loss {l.mean().asnumpy():f}')
```

In our case, you will observe that the results are closely the same (why?).

For sure simplicity is a far more preferable option for implementation such as for proposed `huber_loss`

. Thanks for this point.

But how such implementation deals with ascendant-compatibility with legacy code? This was the reason of class hacking supporting both mxnet.numpy.ndarray and mxnet.ndarray array types (see docstring from hybrid_forward function)

Concerning usage of different loss functions, I understand the use of L1 loss for interpretability. But at this stage, data is random toy dataset, then, it is diffcult (for me) to find itnerpretability.

Thanks for this information concerning the usage of Huber loss taking into account outliers into data model.

For results from usage of either Huber or L2 loss functions, results on my side are quite differents.

Here below are results and parameters I used.

**Reasons for differences** : please note that into `huber_loss`

implementation function, loss is returned as an **array**, while into `MyHuberLoss`

loss is returned as a **scalar**, because of mean applied on array before return.

This leads to change usage of `trainer.step()`

function. When mean is returned from inside the loss function, then we should use `trainer.step(1)`

; when array is returned such as in `huber_loss`

, then `trainer.step(batch_size)`

has to be used for gradients calculation from average batch loss.

I guess this last point is related with question 1.

**A question** : suppose you use batch_size = 90. What happens then for the last batch, sized with 10, when using `train_step(batch_size)`

?

```
# L1 loss
epoch 1, loss 4.149518
epoch 2, loss 3.457200
epoch 3, loss 2.769157
# Huber loss from MyHuberLoss
epoch 1, loss 4.144689
epoch 2, loss 3.453153
epoch 3, loss 2.767166
# Huber loss from huber_loss
epoch 1, loss 3.938834
epoch 2, loss 3.009948
epoch 3, loss 3.315474
# L2 loss
epoch 1, loss 1.691334
epoch 2, loss 0.590921
epoch 3, loss 0.206460
```

**Parameters, training and evaluation :**

```
batch_size = 100
# Feed forward network
net = nn.Sequential()
# One unique layer, connected with all nodes from input layer
net.add(nn.Dense(1))
# Parameters initilization
net.initialize(init.Normal(sigma=0.01))
# Optimizer
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1})
lossInstance = gluon.loss.L1Loss()
lossInstance = gluon.loss.L2Loss()
rho=0.5
#lossInstance = MyHuberLoss(rho=rho) # don't forget to adjust `rho`
#lossInstance = huber_loss
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
# Compute Error from feed-forward step
with autograd.record():
if type(lossInstance) == huber_loss :
l = lossInstance(net(X), y, rho=rho)
else :
l = lossInstance(net(X), y)
# Back propagation of gradient
l.backward()
# Learning stage : update weights, biais
if type(lossInstance) == MyHuberLoss :
trainer.step(1)
else :
trainer.step(batch_size)
# Compute error over the full dataset
l = l1loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l.mean().asnumpy():f}')
```

Since this chapter is for beginners, I didn’t consider inheriting base `mxnet.gluon.loss.Loss`

class to implement a general loss functor for every backend and followed the previous paragraph’s instructions. If my memory serves me correctly, the book introduces the first custom loss with Gluon API in chapter 9.

Following Gluon API, loss functors are expected to return arrays of shape `(batch_size,)`

or `(..., batch_size)`

(in case of multiple tasks) for Numpy NDArrays. `mean`

is applied over *axis 1*, which averages loss values over *tasks* not batch entries. In our case it means that `loss(net(X), y)`

is of shape `(batch_size,)`

as `y`

is of shape `(batch_size,1)`

.

For example,

```
>>> y = np.array([[1, -1, 4, 3]]).reshape(4, 1) # shape = (batch_size, 1) as in `load_array`
>>> y_hat = np.array([[1, 0, 5, 4]]).reshape(4, 1)
>>> loss = gluon.loss.L1Loss()
>>> loss(y_hat, y)
array([0., 1., 3., 2.])
```

As you can see, averaging was not performed because we have only one regression task, therefore we need to pass `batch_size`

steps into `trainer.step`

and call `mean`

in

`print(f'epoch {epoch + 1}, loss {l.mean().asnumpy():f}')`

manually.

Accordingly, I think you should reimplement `MyHuberLoss`

so that you don’t have to check loss types to determine trainer steps. I believe this will also shed some light on your issues.

`load_array`

function from 3.3.2 is based on `mxnet.gluon.data.DataLoader`

, which has option `last_batch`

. The function calls `DataLoader`

with the default `last_batch`

, therefore the last batch will be of shape `(10, number_of_features)`

.

```
import mxnet
from mxnet.gluon import loss
from mxnet.gluon.loss import _reshape_like
from mxnet.gluon.loss import _apply_weighting
import mxnet.numpy as np
class MyHuberLoss(loss.Loss):
r"""Calculates smoothed L1 loss that is equal to L1 loss if absolute error
exceeds rho but is equal to L2 loss otherwise. Also called SmoothedL1 loss.
.. math::
L = \sum_i \begin{cases} \frac{1}{2 {rho}} ({label}_i - {pred}_i)^2 &
\text{ if } |{label}_i - {pred}_i| < {rho} \\
|{label}_i - {pred}_i| - \frac{{rho}}{2} &
\text{ otherwise }
\end{cases}
`label` and `pred` can have arbitrary shape as long as they have the same
number of elements.
Parameters
----------
rho : float, default 1
Threshold for trimmed mean estimator.
weight : float or None
Global scalar weight for loss.
batch_axis : int, default 0
The axis that represents mini-batch.
Inputs:
- **pred**: prediction tensor with arbitrary shape
- **label**: target tensor with the same size as pred.
- **sample_weight**: element-wise weighting tensor. Must be broadcastable
to the same shape as pred. For example, if pred has shape (64, 10)
and you want to weigh each sample in the batch separately,
sample_weight should have shape (64, 1).
Outputs:
- **loss**: loss tensor with shape (batch_size,). Dimenions other than
batch_axis are averaged out.
"""
def __init__(self, rho=1, weight=None, batch_axis=0, **kwargs):
super(MyHuberLoss, self).__init__(weight, batch_axis, **kwargs)
self._rho = rho
def hybrid_forward(self, F, pred, label, sample_weight=None):
'''This function uses array operator from module mxnet.ndarray
This module is recorded in variable F, that is given as parameter function.
Parameters pred and label, in the context of this exercise, both use array type
mxnet.numpy.ndarray
Triggered error show that module mxnet.ndarray does not support operators
such as abs and sqrt to be applied to mxnet.ndarray type.
Then, in order to keep direct graph safe, rather then changing array types in order to
support operators from module mxnet.ndarray, I changed module into F to module
mxnet.numpy, that is compliant with array type mxnet.numpy.ndarray.
'''
is_mxnet_numpy = False
#if type(label) == mxnet.numpy.ndarray or type(pred) == mxnet.numpy.ndarray:
if mxnet.util.is_np_array() :
# Type is mxnet.numpy.ndarray
is_mxnet_numpy = True
# Use appropriate functions from module mxnet.numpy
F = mxnet.numpy
label = label.reshape(pred.shape)
else :
# This is a legacy mx.nd.NDArray; default module (mxnet.ndarray)
# given in parameter is used
label = _reshape_like(F, label, pred)
loss = F.abs(label - pred)
loss = F.where(loss > self._rho, loss - 0.5 * self._rho,
(0.5 / self._rho) * F.square(loss))
loss = _apply_weighting(F, loss, self._weight, sample_weight)
if is_mxnet_numpy :
# Mean does apply on batch axis only; this avoid 1st axis.
loss_mean = F.mean(loss, axis=tuple(range(1, loss.ndim)))
else :
loss_mean = F.mean(loss, axis=self._batch_axis, exclude=True)
return loss_mean
```

Concerning return from Loss class, you’ve riht, thanks. `MyHuberLoss`

has been changed accordingly. Return format of loss is an array shaped as `(batch_size,)`

, according to documentation.

But this does not change deeply the results and so, does not explain why there is a strong difference while using `huber_loss`

function and `MyHuberLoss`

class.

Computing gradient with the average of loss is equivalent (on the paper) to compute the average gradient using loss array. Gradient operator is a linear operator for addition and multiplication with scalar, and that’s what `mean`

operator do.

Concerning the case where `batch_size=90`

(case where last batch is lesser size then all others) then `last_batch`

option does not seems to solve this issue. Updating parameters with `trainer.step(batch_size)`

on loss array with size 10 will not provide expected result when `batch_size=90`

.

Also, using `trainer.step(1)`

after calculation of mean for each batch is valiid option in this case and is more relevant.

Better solution is to feed `trainer.step`

with size of loss array, rather then `batch_size`

.

Difference of results while using `huber_loss`

is due to the `y.shape`

and `y_hat.shape`

that are not same !

Following implementation lead to same results :

```
def huber_loss(y_hat, y, rho=1.):
loss = np.abs(y_hat - y.reshape(y_hat.shape))
loss = np.where(loss <= rho, 0.5/rho * loss**2, loss - 0.5*rho)
return loss
```

As described below :

```
# L1 loss
epoch 1, loss 4.147878
epoch 2, loss 3.456719
epoch 3, loss 2.767898
# Huber loss from MyHuberLoss
epoch 1, loss 4.146686
epoch 2, loss 3.455244
epoch 3, loss 2.767972
# Huber loss from huber_loss
epoch 1, loss 4.147504
epoch 2, loss 3.459582
epoch 3, loss 2.778563
```

It appears that the problem has yet to be resolved.