192 lines
5.5 KiB
Go
192 lines
5.5 KiB
Go
package mathkit
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// Sigmoid is the logistic function 1 / (1 + e^-x), computed in the
|
|
// numerically stable branch-on-sign form so it doesn't overflow
|
|
// math.Exp for very negative x (a plain 1/(1+exp(-x)) blows up
|
|
// exp(-x) to +Inf for x below about -745 and silently returns 0
|
|
// instead of propagating the overflow visibly).
|
|
func Sigmoid(x float64) float64 {
|
|
if x >= 0 {
|
|
z := math.Exp(-x)
|
|
return 1 / (1 + z)
|
|
}
|
|
z := math.Exp(x)
|
|
return z / (1 + z)
|
|
}
|
|
|
|
// ReLU is max(0, x).
|
|
func ReLU(x float64) float64 {
|
|
if x > 0 {
|
|
return x
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Softmax turns a vector of raw scores ("logits") into a probability
|
|
// distribution: non-negative entries that sum to 1. Subtracts the max
|
|
// before exponentiating (the standard numerically-stable formulation)
|
|
// so large logits don't overflow math.Exp.
|
|
func Softmax(x []float64) []float64 {
|
|
if len(x) == 0 {
|
|
return nil
|
|
}
|
|
max := x[0]
|
|
for _, v := range x[1:] {
|
|
if v > max {
|
|
max = v
|
|
}
|
|
}
|
|
out := make([]float64, len(x))
|
|
var sum float64
|
|
for i, v := range x {
|
|
e := math.Exp(v - max)
|
|
out[i] = e
|
|
sum += e
|
|
}
|
|
for i := range out {
|
|
out[i] /= sum
|
|
}
|
|
return out
|
|
}
|
|
|
|
// MSELoss returns the mean squared error between predictions and
|
|
// targets: mean((pred - target)^2), the default regression loss.
|
|
func MSELoss(predictions, targets []float64) (float64, error) {
|
|
if len(predictions) != len(targets) {
|
|
return 0, fmt.Errorf("mse_loss: predictions and targets have different lengths (%d vs %d)", len(predictions), len(targets))
|
|
}
|
|
if len(predictions) == 0 {
|
|
return 0, fmt.Errorf("mse_loss: empty input")
|
|
}
|
|
var sum float64
|
|
for i := range predictions {
|
|
d := predictions[i] - targets[i]
|
|
sum += d * d
|
|
}
|
|
return sum / float64(len(predictions)), nil
|
|
}
|
|
|
|
// crossEntropyEps clamps predicted probabilities away from exactly 0
|
|
// or 1 before taking a log — ln(0) is -Inf, which would make a single
|
|
// confidently-wrong prediction blow up the whole averaged loss to
|
|
// +Inf instead of just contributing a large-but-finite penalty. Every
|
|
// production cross-entropy implementation (e.g. scikit-learn's
|
|
// log_loss) clips for exactly this reason.
|
|
const crossEntropyEps = 1e-12
|
|
|
|
// CrossEntropyLoss returns the mean binary cross-entropy between
|
|
// predicted probabilities and 0/1 (or soft, in [0,1]) targets:
|
|
// mean(-(y*ln(p) + (1-y)*ln(1-p))), the default classification loss.
|
|
func CrossEntropyLoss(predictions, targets []float64) (float64, error) {
|
|
if len(predictions) != len(targets) {
|
|
return 0, fmt.Errorf("cross_entropy_loss: predictions and targets have different lengths (%d vs %d)", len(predictions), len(targets))
|
|
}
|
|
if len(predictions) == 0 {
|
|
return 0, fmt.Errorf("cross_entropy_loss: empty input")
|
|
}
|
|
var sum float64
|
|
for i, p := range predictions {
|
|
if p < 0 || p > 1 {
|
|
return 0, fmt.Errorf("cross_entropy_loss: predictions must be probabilities in [0, 1], got %g at index %d", p, i)
|
|
}
|
|
p = math.Min(math.Max(p, crossEntropyEps), 1-crossEntropyEps)
|
|
y := targets[i]
|
|
sum += -(y*math.Log(p) + (1-y)*math.Log(1-p))
|
|
}
|
|
return sum / float64(len(predictions)), nil
|
|
}
|
|
|
|
// LinearRegressionResult is a fitted ordinary-least-squares linear
|
|
// model.
|
|
type LinearRegressionResult struct {
|
|
// Coefficients[0] is the intercept, Coefficients[1:] line up with
|
|
// the feature columns of X in order.
|
|
Coefficients []float64
|
|
Predictions []float64
|
|
Residuals []float64
|
|
RSquared float64
|
|
}
|
|
|
|
// LinearRegression fits y ≈ X·β + intercept by ordinary least squares
|
|
// via the normal equations β = (XᵀX)^-1 Xᵀy, with an intercept column
|
|
// of 1s prepended automatically (the caller supplies only the real
|
|
// feature columns in X). The normal-equation approach is the direct,
|
|
// closed-form solution to minimizing squared error — the textbook
|
|
// ML-101 regression fit, as opposed to reaching it iteratively via
|
|
// GradientDescent on the MSE loss.
|
|
func LinearRegression(X Matrix, y []float64) (*LinearRegressionResult, error) {
|
|
rows, cols, err := X.Dims()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("linear_regression: %w", err)
|
|
}
|
|
if rows != len(y) {
|
|
return nil, fmt.Errorf("linear_regression: X has %d rows but y has %d entries", rows, len(y))
|
|
}
|
|
if rows <= cols {
|
|
return nil, fmt.Errorf("linear_regression: need more observations (%d) than features+1 (%d) to fit uniquely", rows, cols+1)
|
|
}
|
|
|
|
// Prepend the intercept column of 1s.
|
|
design := make(Matrix, rows)
|
|
for i := 0; i < rows; i++ {
|
|
row := make([]float64, cols+1)
|
|
row[0] = 1
|
|
copy(row[1:], X[i])
|
|
design[i] = row
|
|
}
|
|
|
|
designT, err := Transpose(design)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("linear_regression: %w", err)
|
|
}
|
|
xtx, err := MatMul(designT, design)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("linear_regression: %w", err)
|
|
}
|
|
xtxInv, err := Inverse(xtx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("linear_regression: features are collinear (XᵀX is singular): %w", err)
|
|
}
|
|
xty, err := MatVecMul(designT, y)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("linear_regression: %w", err)
|
|
}
|
|
beta, err := MatVecMul(xtxInv, xty)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("linear_regression: %w", err)
|
|
}
|
|
|
|
preds, err := MatVecMul(design, beta)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("linear_regression: %w", err)
|
|
}
|
|
residuals := make([]float64, rows)
|
|
var ssRes float64
|
|
for i := range preds {
|
|
residuals[i] = y[i] - preds[i]
|
|
ssRes += residuals[i] * residuals[i]
|
|
}
|
|
meanY, _ := Mean(y)
|
|
var ssTot float64
|
|
for _, v := range y {
|
|
d := v - meanY
|
|
ssTot += d * d
|
|
}
|
|
rSquared := 1.0
|
|
if ssTot > 0 {
|
|
rSquared = 1 - ssRes/ssTot
|
|
}
|
|
|
|
return &LinearRegressionResult{
|
|
Coefficients: beta,
|
|
Predictions: preds,
|
|
Residuals: residuals,
|
|
RSquared: rSquared,
|
|
}, nil
|
|
}
|