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}