Skip to main content

burn_backend/backend/ops/
activation.rs

1use crate::tensor::FloatTensor;
2use crate::{Backend, Scalar, TensorMetadata, get_or_init_device_settings};
3use core::f64::consts::SQRT_2;
4
5/// Activation function operations.
6///
7/// This trait let backend implementations override activation functions for better performance.
8pub trait ActivationOps<B: Backend> {
9    /// Applies the LeakyReLU activation function.
10    ///
11    /// # Arguments
12    ///
13    /// * `tensor` - The tensor.
14    /// * `negative_slope` - The negative_slope value that values smaller than 0 are multiplied with.
15    ///
16    /// # Returns
17    ///
18    /// The output tensor.
19    fn leaky_relu(tensor: FloatTensor<B>, negative_slope: Scalar) -> FloatTensor<B> {
20        let bool_dtype = get_or_init_device_settings::<B>(&tensor.device()).bool_dtype;
21        let mask = B::float_lower_elem(tensor.clone(), 0f32.into(), bool_dtype);
22        let scaled_tensor = B::float_mul_scalar(tensor.clone(), negative_slope);
23
24        // Update the tensor where the values are `< 0` by `tensor * negative_slope`.
25        B::float_mask_where(tensor, mask, scaled_tensor)
26    }
27
28    /// Applies the ReLU activation function.
29    ///
30    /// # Arguments
31    ///
32    /// * `tensor` - The tensor.
33    ///
34    /// # Returns
35    ///
36    /// The output tensor.
37    fn relu(tensor: FloatTensor<B>) -> FloatTensor<B> {
38        let bool_dtype = get_or_init_device_settings::<B>(&tensor.device()).bool_dtype;
39        let mask = B::float_lower_equal_elem(tensor.clone(), 0f32.into(), bool_dtype);
40
41        B::float_mask_fill(tensor, mask, 0f32.into())
42    }
43
44    /// Applies the ReLU activation function backward.
45    ///
46    /// # Arguments
47    ///
48    /// * `output` - The output tensor.
49    ///
50    /// # Returns
51    ///
52    /// The gradient.
53    fn relu_backward(output: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B> {
54        let bool_dtype = get_or_init_device_settings::<B>(&output.device()).bool_dtype;
55        let mask = B::float_lower_equal_elem(output, 0f32.into(), bool_dtype);
56
57        B::float_mask_fill(grad, mask, 0.into())
58    }
59
60    /// Applies the Gelu activation function.
61    ///
62    /// # Arguments
63    ///
64    /// * `tensor` - The tensor.
65    ///
66    /// # Returns
67    ///
68    /// The output tensor.
69    fn gelu(tensor: FloatTensor<B>) -> FloatTensor<B> {
70        let x = B::float_div_scalar(tensor.clone(), SQRT_2.into());
71        let x = B::float_erf(x);
72        let x = B::float_add_scalar(x, 1f32.into());
73        let x = B::float_mul(tensor, x);
74
75        B::float_div_scalar(x, 2f32.into())
76    }
77    /// Applies the PReLu activation function.
78    /// # Arguments
79    /// * `tensor` - The input tensor
80    /// * `alpha` - The weight tensor
81    fn prelu(tensor: FloatTensor<B>, alpha: FloatTensor<B>) -> FloatTensor<B> {
82        let bool_dtype = get_or_init_device_settings::<B>(&tensor.device()).bool_dtype;
83        let mask = B::float_lower_elem(tensor.clone(), 0f32.into(), bool_dtype);
84        let scaled_tensor = B::float_mul(tensor.clone(), alpha);
85        B::float_mask_where(tensor, mask, scaled_tensor)
86    }
87
88    /// Applies the Gelu activation function backward.
89    ///
90    /// # Arguments
91    ///
92    /// * `x` - The tensor.
93    /// * `grad` - The gradient.
94    ///
95    /// # Returns
96    ///
97    /// The output tensor.
98    fn gelu_backward(x: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B> {
99        // Exact analytical derivative of erf-based GELU:
100        // d/dx [0.5 * x * (1 + erf(x / sqrt(2)))]
101        // = 0.5 * (1 + erf(x / sqrt(2))) + x * (1 / sqrt(2 * pi)) * exp(-0.5 * x^2)
102        const FRAC_1_SQRT_2PI: f64 = 0.3989422804014327;
103
104        let erf_term = B::float_erf(B::float_div_scalar(x.clone(), SQRT_2.into()));
105        let cdf = B::float_mul_scalar(B::float_add_scalar(erf_term, 1.0.into()), 0.5.into());
106
107        let x2 = B::float_mul(x.clone(), x.clone());
108        let exp_term = B::float_exp(B::float_mul_scalar(x2, (-0.5).into()));
109        let pdf_term = B::float_mul(B::float_mul_scalar(x, FRAC_1_SQRT_2PI.into()), exp_term);
110
111        let dgelu = B::float_add(cdf, pdf_term);
112        B::float_mul(dgelu, grad)
113    }
114
115    /// Applies the Sigmoid activation function.
116    ///
117    /// # Arguments
118    ///
119    /// * `tensor` - The tensor.
120    ///
121    /// # Returns
122    ///
123    /// The output tensor.
124    fn sigmoid(tensor: FloatTensor<B>) -> FloatTensor<B> {
125        let dtype = tensor.dtype();
126        let tensor_full = B::float_cast(tensor, burn_std::FloatDType::F32);
127        let tensor_tmp = B::float_exp(B::float_neg(B::float_log(B::float_add_scalar(
128            B::float_exp(B::float_neg(tensor_full)),
129            1.0.into(),
130        ))));
131
132        B::float_cast(tensor_tmp, dtype.into())
133    }
134
135    /// Applies the Sigmoid activation function backward.
136    ///
137    /// # Arguments
138    ///
139    /// * `output` - The output tensor of the sigmoid function.
140    /// * `grad` - The gradient.
141    ///
142    /// # Returns
143    ///
144    /// The output tensor.
145    fn sigmoid_backward(output: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B> {
146        let value = B::float_mul(
147            output.clone(),
148            B::float_add_scalar(B::float_neg(output), 1.0.into()),
149        );
150        B::float_mul(value, grad)
151    }
152
153    /// Applies the hard Sigmoid activation function.
154    ///
155    /// # Arguments
156    ///
157    /// * `tensor` - The tensor.
158    /// * `alpha` - The alpha value that the tensor is multiplied with.
159    /// * `beta` - The beta value that is added to the tensor
160    ///
161    /// # Returns
162    ///
163    /// The output tensor.
164    fn hard_sigmoid(tensor: FloatTensor<B>, alpha: Scalar, beta: Scalar) -> FloatTensor<B> {
165        let dtype = tensor.dtype();
166        let tensor_full = B::float_cast(tensor, burn_std::FloatDType::F32);
167
168        let tensor_tmp = B::float_clamp(
169            B::float_add_scalar(B::float_mul_scalar(tensor_full, alpha), beta),
170            0.0.into(),
171            1.0.into(),
172        );
173
174        B::float_cast(tensor_tmp, dtype.into())
175    }
176
177    /// Applies the LogSigmoid activation function.
178    ///
179    /// # Arguments
180    ///
181    /// * `tensor` - The tensor.
182    ///
183    /// # Returns
184    ///
185    /// The output tensor.
186    fn log_sigmoid(tensor: FloatTensor<B>) -> FloatTensor<B> {
187        // To avoid overflow, we use the log-sum-exp trick.
188        //
189        // ```ignore
190        // log(sigmoid(x)) = log(1/(1 + exp(-x)))
191        //                 = log(1) - log(1 + exp(-x))
192        //                 = -log(1 + exp(-x))
193        //                 = -log(exp(0) + exp(-x))
194        // ```
195        // The `exp(t)` of even a moderate-magnitude positive number can be astronomically huge, so we
196        // subtract the `max(t, 0)` of each value (where `t = -x` in this case). This results in the
197        // following equivalence:
198        // ```ignore
199        // log(sigmoid(x)) = -(max(-x, 0) + log(exp(-max(-x, 0)) + exp(-x - max(-x, 0))))
200        // ```
201        //
202        // This extends the range of values for which we obtain accurate results.
203
204        // max(-x, 0)
205        let bool_dtype = get_or_init_device_settings::<B>(&tensor.device()).bool_dtype;
206        let tensor_neg = B::float_neg(tensor);
207        let mask = B::float_lower_elem(tensor_neg.clone(), 0f32.into(), bool_dtype);
208        let max_elem = B::float_mask_fill(tensor_neg.clone(), mask, 0f32.into());
209        let max_elem_neg = B::float_neg(max_elem.clone());
210
211        // z = exp(-max(-x, 0)) + exp(-x - max(-x, 0))
212        let z = B::float_add(
213            B::float_exp(max_elem_neg.clone()),
214            B::float_exp(B::float_sub(tensor_neg, max_elem.clone())),
215        );
216
217        // -max(-x, 0) - log(-z)
218        B::float_sub(max_elem_neg, B::float_log(z))
219    }
220
221    /// Applies the softmax function along the given dimension.
222    ///
223    /// Uses the max-shift trick for numerical stability: the per-row `max` is detached
224    /// so no gradient flows back through it (the shift is a numerical-stability
225    /// transformation, not part of the function).
226    ///
227    /// # Arguments
228    ///
229    /// * `tensor` - The tensor.
230    /// * `dim` - The dimension along which softmax is computed.
231    ///
232    /// # Returns
233    ///
234    /// The output tensor.
235    fn softmax(tensor: FloatTensor<B>, dim: usize) -> FloatTensor<B> {
236        let max = B::float_max_dim(B::float_detach(tensor.clone()), dim);
237        let shifted = B::float_sub(tensor, max);
238        let exp = B::float_exp(shifted);
239        let sum = B::float_sum_dim(exp.clone(), dim);
240        B::float_div(exp, sum)
241    }
242
243    /// Applies the log-softmax function along the given dimension.
244    ///
245    /// Computed via the log-sum-exp trick with a detached max-shift for numerical
246    /// stability.
247    ///
248    /// # Arguments
249    ///
250    /// * `tensor` - The tensor.
251    /// * `dim` - The dimension along which log-softmax is computed.
252    ///
253    /// # Returns
254    ///
255    /// The output tensor.
256    fn log_softmax(tensor: FloatTensor<B>, dim: usize) -> FloatTensor<B> {
257        let max = B::float_max_dim(B::float_detach(tensor.clone()), dim);
258        let shifted = B::float_sub(tensor, max);
259        let log_sum_exp = B::float_log(B::float_sum_dim(B::float_exp(shifted.clone()), dim));
260        B::float_sub(shifted, log_sum_exp)
261    }
262
263    /// Applies the softmin function along the given dimension.
264    ///
265    /// Equivalent to `softmax(-tensor, dim)`.
266    ///
267    /// # Arguments
268    ///
269    /// * `tensor` - The tensor.
270    /// * `dim` - The dimension along which softmin is computed.
271    ///
272    /// # Returns
273    ///
274    /// The output tensor.
275    fn softmin(tensor: FloatTensor<B>, dim: usize) -> FloatTensor<B> {
276        Self::softmax(B::float_neg(tensor), dim)
277    }
278
279    /// Applies the LogSigmoid activation function backward.
280    ///
281    /// # Arguments
282    ///
283    /// * `x` - The input tensor.
284    /// * `grad` - The gradient.
285    ///
286    /// # Returns
287    ///
288    /// The output gradient.
289    fn log_sigmoid_backward(x: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B> {
290        // Derivative of -max(-x, 0) - log(exp(-max(-x, 0)) - exp(-x - max(-x, 0)))) is
291        // -max_derive - (-max_derive * exp(-max(-x, 0)) + (-1 - max_derive) * exp(-x - max(-x, 0))) / z
292        // where z = exp(-max(-x, 0)) + exp(-x - max(-x, 0))
293        //
294        // This simplifies to:
295        // -max_derive - (z-1)/z if x is >= 0
296        // -max_derive + (z-1)/z if x is < 0
297
298        let shape = x.shape();
299        let dtype = x.dtype();
300        let device = x.device();
301        let bool_dtype = get_or_init_device_settings::<B>(&device).bool_dtype;
302
303        // max(-x, 0)
304        let x_neg = B::float_neg(x);
305        let mask = B::float_lower_elem(x_neg.clone(), 0f32.into(), bool_dtype); // -x < 0 or x >= 0
306        let max_elem = B::float_mask_fill(x_neg.clone(), mask.clone(), 0f32.into());
307
308        // z = exp(-max(-x, 0)) + exp(-x - max(-x, 0))
309        let z = B::float_add(
310            B::float_exp(B::float_neg(max_elem.clone())),
311            B::float_exp(B::float_sub(x_neg, max_elem)),
312        );
313
314        // Derivative of max(-x, 0) is 1 if x < 0 or 0 if x >= 0
315        let ones = B::float_ones(shape, &device, dtype.into());
316        let max_derive = B::float_mask_fill(ones.clone(), mask.clone(), 0f32.into());
317        let sign = B::float_mask_fill(ones.clone(), mask, (-1f32).into());
318
319        // grad * (max_derive - sign * (1 - (1 / z)))
320        B::float_mul(
321            grad,
322            B::float_sub(
323                max_derive,
324                B::float_mul(sign, B::float_sub(ones, B::float_recip(z))),
325            ),
326        )
327    }
328}