mirror of
https://github.com/TheAlgorithms/Python.git
synced 2026-09-28 21:45:27 +08:00
Closes the remaining items in #13171: friendly ValueError for non-numeric inputs (str/dict) instead of raw numpy messages, optional axis=None for a whole-array distribution, documented NaN/inf behavior, and doctests covering identical logits, large (overflow-prone) logits, 2-D axis selection, empty input, non-numeric input, and out-of-bounds axis.
140 lines
4.8 KiB
Python
140 lines
4.8 KiB
Python
"""
|
|
This script demonstrates the implementation of the Softmax function.
|
|
|
|
It takes as input a vector of K real numbers and normalizes it into a
|
|
probability distribution consisting of K probabilities proportional
|
|
to the exponentials of the input numbers. After applying softmax,
|
|
the elements of the vector always sum up to 1.
|
|
|
|
Script inspired by its corresponding Wikipedia article:
|
|
https://en.wikipedia.org/wiki/Softmax_function
|
|
"""
|
|
|
|
import numpy as np
|
|
from numpy.exceptions import AxisError
|
|
|
|
|
|
def softmax(vector: np.ndarray, axis: int | None = -1) -> np.ndarray:
|
|
"""
|
|
Compute the softmax of ``vector`` along ``axis`` in a numerically-stable way.
|
|
|
|
Parameters:
|
|
vector (np.ndarray | list | tuple): Input data (vector, matrix or
|
|
higher-rank tensor). It is converted to a float ``np.ndarray``,
|
|
so lists, tuples and integers are accepted too.
|
|
axis (int | None, optional): Axis along which softmax is computed so
|
|
that the probabilities sum to 1 along that axis. If ``None``, the
|
|
softmax is computed over the flattened array (a single
|
|
distribution). Default is ``-1`` (the last axis).
|
|
|
|
Returns:
|
|
np.ndarray: An array with the same shape as ``vector`` whose values
|
|
along ``axis`` (or over the whole array when ``axis is None``) form a
|
|
probability distribution that sums to 1.
|
|
|
|
Raises:
|
|
ValueError: If ``vector`` is empty or cannot be converted to a numeric
|
|
float array (for example a string or a dict).
|
|
numpy.exceptions.AxisError: If ``axis`` is out of bounds for the input.
|
|
|
|
Note:
|
|
If the input contains ``NaN`` or ``inf`` the result will contain
|
|
``NaN`` along the affected axis; softmax is only meaningful for finite
|
|
real inputs.
|
|
|
|
The softmax vector adds up to one. We need to ceil to mitigate precision.
|
|
|
|
>>> float(np.ceil(np.sum(softmax([1, 2, 3, 4]))))
|
|
1.0
|
|
|
|
Identical logits map to a uniform distribution:
|
|
|
|
>>> softmax(np.array([5, 5]))
|
|
array([0.5, 0.5])
|
|
|
|
A single element always maps to 1:
|
|
|
|
>>> softmax([0])
|
|
array([1.])
|
|
|
|
It is numerically stable for large logits (no overflow):
|
|
|
|
>>> softmax([1000.0, 1001.0, 1002.0])
|
|
array([0.09003057, 0.24472847, 0.66524096])
|
|
|
|
For a 2-D array the ``axis`` selects where probabilities sum to 1:
|
|
|
|
>>> mat = np.array([[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]])
|
|
>>> np.round(softmax(mat, axis=-1), 3)
|
|
array([[0.09 , 0.245, 0.665],
|
|
[0.09 , 0.245, 0.665]])
|
|
>>> np.round(softmax(mat, axis=0), 3)
|
|
array([[0.5, 0.5, 0.5],
|
|
[0.5, 0.5, 0.5]])
|
|
|
|
With ``axis=None`` the whole array becomes one distribution that sums to 1:
|
|
|
|
>>> float(np.round(np.sum(softmax(mat, axis=None)), 6))
|
|
1.0
|
|
|
|
Empty, non-numeric and out-of-bounds inputs raise clear errors:
|
|
|
|
>>> softmax([])
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: softmax input must be non-empty
|
|
>>> softmax("not a number")
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: softmax input must be numeric, got str
|
|
>>> softmax([1, 2, 3], axis=3)
|
|
Traceback (most recent call last):
|
|
...
|
|
numpy.exceptions.AxisError: axis 3 is out of bounds for array of dimension 1
|
|
"""
|
|
# Convert input to a float numpy array, turning numpy's terse conversion
|
|
# errors into a clear message about the unsupported input type.
|
|
try:
|
|
vector = np.asarray(vector, dtype=float)
|
|
except (ValueError, TypeError) as exc:
|
|
error_message = f"softmax input must be numeric, got {type(vector).__name__}"
|
|
raise ValueError(error_message) from exc
|
|
|
|
# Handle empty input
|
|
if vector.size == 0:
|
|
raise ValueError("softmax input must be non-empty")
|
|
|
|
# Validate axis (None means "treat the whole array as one distribution")
|
|
if axis is not None:
|
|
ndim = vector.ndim
|
|
if axis >= ndim or axis < -ndim:
|
|
error_message = (
|
|
f"axis {axis} is out of bounds for array of dimension {ndim}"
|
|
)
|
|
raise AxisError(error_message)
|
|
|
|
# Subtract max for numerical stability
|
|
vector_max = np.max(vector, axis=axis, keepdims=True)
|
|
exponent_vector = np.exp(vector - vector_max)
|
|
|
|
# Sum of exponentials along the axis
|
|
sum_of_exponents = np.sum(exponent_vector, axis=axis, keepdims=True)
|
|
|
|
# Divide each exponent by the sum along the axis
|
|
softmax_vector = exponent_vector / sum_of_exponents
|
|
return softmax_vector
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Single value
|
|
print(softmax((0,)))
|
|
# Vector
|
|
print(softmax([1, 2, 3]))
|
|
# Matrix along last axis
|
|
mat = np.array([[1, 2, 3], [4, 5, 6]])
|
|
print("Softmax along last axis:\n", softmax(mat))
|
|
# Matrix along axis 0
|
|
print("Softmax along axis 0:\n", softmax(mat, axis=0))
|
|
# Whole-matrix distribution
|
|
print("Softmax over the whole matrix:\n", softmax(mat, axis=None))
|