mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Vector: Reorganize package into topic-based files #4669
Split the catch-all values.go into one file per concept, each mirrored by its test: distance.go, norm.go, stats.go, product.go, centroid.go, plus the mean methods folded into mean.go and Copy/Dim/Sum into vector.go. Remove values.go, values_test.go, and values_more_test.go so functionality and tests live where developers expect them. Hoist the two 512-dimensional face embeddings shared by the distance, norm, and cosine tests into fixtures_test.go, removing the previous triplication, and decompose the monolithic TestVector into per-concept tests. Close pre-existing coverage gaps in the integer converters and the GeometricMean/HarmonicMean method wrappers, bringing the package to 100% statement coverage. Pure code movement; no behavior change.
This commit is contained in:
parent
40b170ecb0
commit
03129c9129
20 changed files with 700 additions and 611 deletions
40
pkg/vector/centroid.go
Normal file
40
pkg/vector/centroid.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package vector
|
||||
|
||||
// Centroid returns the element-wise mean (centroid) of the given vectors as a
|
||||
// new, independent vector. Vectors whose length differs from the first vector
|
||||
// are ignored, and the mean is taken over the vectors actually included. It
|
||||
// returns nil when vs is empty or the first vector has no elements.
|
||||
func Centroid(vs Vectors) Vector {
|
||||
if len(vs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dim := len(vs[0])
|
||||
|
||||
if dim == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(Vector, dim)
|
||||
n := 0
|
||||
|
||||
for _, v := range vs {
|
||||
if len(v) != dim {
|
||||
continue
|
||||
}
|
||||
|
||||
for j := range dim {
|
||||
result[j] += v[j]
|
||||
}
|
||||
|
||||
n++
|
||||
}
|
||||
|
||||
inv := 1 / float64(n)
|
||||
|
||||
for j := range result {
|
||||
result[j] *= inv
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
39
pkg/vector/centroid_test.go
Normal file
39
pkg/vector/centroid_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package vector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCentroid(t *testing.T) {
|
||||
t.Run("Mean", func(t *testing.T) {
|
||||
got := Centroid(Vectors{{1, 2}, {3, 4}})
|
||||
assert.Equal(t, Vector{2, 3}, got)
|
||||
})
|
||||
t.Run("Single", func(t *testing.T) {
|
||||
got := Centroid(Vectors{{2, 4, 6}})
|
||||
assert.Equal(t, Vector{2, 4, 6}, got)
|
||||
})
|
||||
t.Run("SkipsMismatchedDimensions", func(t *testing.T) {
|
||||
// The 3-D vector is ignored; the mean is taken over the two 2-D vectors.
|
||||
got := Centroid(Vectors{{1, 2}, {1, 2, 3}, {3, 4}})
|
||||
assert.Equal(t, Vector{2, 3}, got)
|
||||
})
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.Nil(t, Centroid(Vectors{}))
|
||||
assert.Nil(t, Centroid(nil))
|
||||
})
|
||||
t.Run("FirstVectorEmpty", func(t *testing.T) {
|
||||
assert.Nil(t, Centroid(Vectors{{}, {1, 2}}))
|
||||
})
|
||||
t.Run("Independent", func(t *testing.T) {
|
||||
a := Vector{1, 2}
|
||||
b := Vector{3, 4}
|
||||
got := Centroid(Vectors{a, b})
|
||||
got[0] = 100
|
||||
// Mutating the result must not change the inputs.
|
||||
assert.Equal(t, Vector{1, 2}, a)
|
||||
assert.Equal(t, Vector{3, 4}, b)
|
||||
})
|
||||
}
|
||||
82
pkg/vector/distance.go
Normal file
82
pkg/vector/distance.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package vector
|
||||
|
||||
import "math"
|
||||
|
||||
// EuclideanDist returns the Euclidean distance between the vectors,
|
||||
func (v Vector) EuclideanDist(w Vector) float64 {
|
||||
return EuclideanDist(v, w)
|
||||
}
|
||||
|
||||
// CosineSimilarity returns the cosine similarity between two vectors,
|
||||
// ranging from -1 (opposite) to 1 (identical).
|
||||
func (v Vector) CosineSimilarity(w Vector) float64 {
|
||||
return CosineSimilarity(v, w)
|
||||
}
|
||||
|
||||
// CosineDist returns the cosine distance between two vectors (1 - cosine similarity).
|
||||
func (v Vector) CosineDist(w Vector) float64 {
|
||||
return CosineDist(v, w)
|
||||
}
|
||||
|
||||
// EuclideanDist returns the Euclidean distance between multiple vectors.
|
||||
func EuclideanDist(a, b Vector) float64 {
|
||||
if a.Dim() != b.Dim() {
|
||||
return NaN()
|
||||
}
|
||||
|
||||
var (
|
||||
s, t float64
|
||||
)
|
||||
|
||||
for i := range a {
|
||||
t = a[i] - b[i]
|
||||
s += t * t
|
||||
}
|
||||
|
||||
return math.Sqrt(s)
|
||||
}
|
||||
|
||||
// CosineSimilarity returns the cosine similarity between two vectors, ranging
|
||||
// from -1 (opposite) to 1 (identical). It returns NaN when the dimensions
|
||||
// differ and 0 when either operand is a zero vector.
|
||||
func CosineSimilarity(a, b Vector) float64 {
|
||||
if a.Dim() != b.Dim() {
|
||||
return NaN()
|
||||
}
|
||||
|
||||
var sum, s1, s2 float64
|
||||
|
||||
for i := range a {
|
||||
sum += a[i] * b[i]
|
||||
s1 += a[i] * a[i]
|
||||
s2 += b[i] * b[i]
|
||||
}
|
||||
|
||||
if s1 == 0 || s2 == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
return sum / (math.Sqrt(s1) * math.Sqrt(s2))
|
||||
}
|
||||
|
||||
// CosineDist returns the cosine distance between two vectors, defined as
|
||||
// 1 - CosineSimilarity. Identical vectors yield 0; it returns NaN when the
|
||||
// dimensions differ.
|
||||
func CosineDist(a, b Vector) float64 {
|
||||
return 1.0 - CosineSimilarity(a, b)
|
||||
}
|
||||
|
||||
// CosineDists returns the cosine distances between two sets of vectors.
|
||||
func CosineDists(x, y Vectors) Vectors {
|
||||
result := make(Vectors, len(x))
|
||||
|
||||
for i, a := range x {
|
||||
result[i] = make([]float64, len(y))
|
||||
|
||||
for j, b := range y {
|
||||
result[i][j] = CosineDist(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
116
pkg/vector/distance_test.go
Normal file
116
pkg/vector/distance_test.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package vector
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEuclideanDist(t *testing.T) {
|
||||
a := Vector{1, 2, 3, 4, 6, 5}
|
||||
b := Vector{2, 1, 3, 4, 5, 6}
|
||||
d := Vector{0, 0, 0, 0, 0, 0}
|
||||
e := Vector{}
|
||||
n := make(Vector, 512)
|
||||
|
||||
t.Run("Method", func(t *testing.T) {
|
||||
assert.InDelta(t, 2, a.EuclideanDist(b), 0.01)
|
||||
assert.InDelta(t, a.EuclideanDist(b), b.EuclideanDist(a), 0.01)
|
||||
assert.True(t, math.IsNaN(faceEmbeddingB.EuclideanDist(d)))
|
||||
assert.InDelta(t, 0, d.EuclideanDist(d), 0.01)
|
||||
assert.True(t, math.IsNaN(e.EuclideanDist(d)))
|
||||
assert.InDelta(t, 0.9999999779072661, faceEmbeddingB.EuclideanDist(n), 0.01)
|
||||
})
|
||||
t.Run("Func", func(t *testing.T) {
|
||||
assert.InDelta(t, 2.0, EuclideanDist(a, b), 0.01)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCosineSimilarity(t *testing.T) {
|
||||
a := Vector{1, 2, 3, 4, 6, 5}
|
||||
b := Vector{2, 1, 3, 4, 5, 6}
|
||||
d := Vector{0, 0, 0, 0, 0, 0}
|
||||
e := Vector{}
|
||||
n := make(Vector, 512)
|
||||
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
assert.InDelta(t, 0.978021978021978, a.CosineSimilarity(b), 0.01)
|
||||
assert.True(t, math.IsNaN(faceEmbeddingB.CosineSimilarity(d)))
|
||||
assert.InDelta(t, 0, d.CosineSimilarity(d), 0.01)
|
||||
assert.True(t, math.IsNaN(e.CosineSimilarity(d)))
|
||||
assert.InDelta(t, 0, faceEmbeddingB.CosineSimilarity(n), 0.01)
|
||||
assert.InDelta(t, 0, n.CosineSimilarity(n), 0.01)
|
||||
assert.InDelta(t, 1.0, faceEmbeddingB.CosineSimilarity(faceEmbeddingB), 0.01)
|
||||
assert.InDelta(t, 1.0, a.CosineSimilarity(a), 0.01)
|
||||
assert.InDelta(t, 1.0, b.CosineSimilarity(b), 0.01)
|
||||
})
|
||||
t.Run("Func", func(t *testing.T) {
|
||||
assert.InDelta(t, 0.978021978021978, CosineSimilarity(a, b), 0.01)
|
||||
})
|
||||
t.Run("Orthogonal", func(t *testing.T) {
|
||||
assert.InDelta(t, 0.0, CosineSimilarity(Vector{1, 0}, Vector{0, 1}), 0.00001)
|
||||
assert.InDelta(t, 1.0, CosineDist(Vector{1, 0}, Vector{0, 1}), 0.00001)
|
||||
})
|
||||
t.Run("Opposite", func(t *testing.T) {
|
||||
assert.InDelta(t, -1.0, CosineSimilarity(Vector{1, 0}, Vector{-1, 0}), 0.00001)
|
||||
assert.InDelta(t, 2.0, CosineDist(Vector{1, 0}, Vector{-1, 0}), 0.00001)
|
||||
})
|
||||
t.Run("DimensionMismatch", func(t *testing.T) {
|
||||
assert.True(t, math.IsNaN(CosineSimilarity(Vector{1, 0}, Vector{1, 0, 0})))
|
||||
assert.True(t, math.IsNaN(CosineDist(Vector{1, 0}, Vector{1, 0, 0})))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCosineDist(t *testing.T) {
|
||||
a := Vector{1, 2, 3, 4, 6, 5}
|
||||
b := Vector{2, 1, 3, 4, 5, 6}
|
||||
d := Vector{0, 0, 0, 0, 0, 0}
|
||||
e := Vector{}
|
||||
n := make(Vector, 512)
|
||||
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
// Distance is 1 - similarity: 0 for identical, 1 for a zero vector, NaN on dim mismatch.
|
||||
assert.InDelta(t, 0.021978021978022, a.CosineDist(b), 0.01)
|
||||
assert.True(t, math.IsNaN(faceEmbeddingB.CosineDist(d)))
|
||||
assert.InDelta(t, 1.0, d.CosineDist(d), 0.01)
|
||||
assert.True(t, math.IsNaN(e.CosineDist(d)))
|
||||
assert.InDelta(t, 1.0, faceEmbeddingB.CosineDist(n), 0.01)
|
||||
assert.InDelta(t, 1.0, n.CosineDist(n), 0.01)
|
||||
assert.InDelta(t, 0, faceEmbeddingB.CosineDist(faceEmbeddingB), 0.01)
|
||||
assert.InDelta(t, 0, a.CosineDist(a), 0.01)
|
||||
assert.InDelta(t, 0, b.CosineDist(b), 0.01)
|
||||
})
|
||||
t.Run("Func", func(t *testing.T) {
|
||||
assert.InDelta(t, 0.021978021978022, CosineDist(a, b), 0.01)
|
||||
})
|
||||
t.Run("Equal", func(t *testing.T) {
|
||||
x := Vector{1, 0, 0, 1, 0, 0}
|
||||
y := Vector{1, 0, 0, 1, 0, 0}
|
||||
// Identical vectors: similarity 1, distance 0.
|
||||
assert.InDelta(t, 1.0, CosineSimilarity(x, y), 0.00001)
|
||||
assert.InDelta(t, 0.0, CosineDist(x, y), 0.00001)
|
||||
})
|
||||
t.Run("Faces", func(t *testing.T) {
|
||||
// Real face embeddings: near-orthogonal, so distance is ~1.
|
||||
assert.InDelta(t, -0.003275301858301365, CosineSimilarity(faceEmbeddingA, faceEmbeddingB), 0.00001)
|
||||
assert.InDelta(t, 1.003275301858301365, CosineDist(faceEmbeddingA, faceEmbeddingB), 0.00001)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCosineDists(t *testing.T) {
|
||||
x := Vectors{{1, 0}, {0, 1}}
|
||||
y := Vectors{{1, 0}, {-1, 0}}
|
||||
got := CosineDists(x, y)
|
||||
assert.Len(t, got, 2)
|
||||
assert.InDelta(t, 0.0, got[0][0], 0.00001) // identical
|
||||
assert.InDelta(t, 2.0, got[0][1], 0.00001) // opposite
|
||||
assert.InDelta(t, 1.0, got[1][0], 0.00001) // orthogonal
|
||||
assert.InDelta(t, 1.0, got[1][1], 0.00001) // orthogonal
|
||||
}
|
||||
|
||||
func BenchmarkCosineDist(b *testing.B) {
|
||||
for b.Loop() {
|
||||
CosineDist(faceEmbeddingA, faceEmbeddingB)
|
||||
}
|
||||
}
|
||||
8
pkg/vector/fixtures_test.go
Normal file
8
pkg/vector/fixtures_test.go
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,9 @@
|
|||
package vector
|
||||
|
||||
import "math"
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Mean gets the average of a slice of numbers
|
||||
func Mean(v Vector) float64 {
|
||||
|
|
@ -56,3 +59,47 @@ func HarmonicMean(v Vector) float64 {
|
|||
|
||||
return float64(l) / p
|
||||
}
|
||||
|
||||
// Mean returns the vector's mean value.
|
||||
func (v Vector) Mean() float64 {
|
||||
return Mean(v)
|
||||
}
|
||||
|
||||
// GeometricMean returns the vector's geometric mean value.
|
||||
func (v Vector) GeometricMean() float64 {
|
||||
return GeometricMean(v)
|
||||
}
|
||||
|
||||
// HarmonicMean returns the vector's harmonic mean value.
|
||||
func (v Vector) HarmonicMean() float64 {
|
||||
return HarmonicMean(v)
|
||||
}
|
||||
|
||||
// weightedSum returns the weighted sum of the vector. This is really only useful in
|
||||
// calculating the weighted mean.
|
||||
func (v Vector) weightedSum(w Vector) (float64, error) {
|
||||
if len(v) != len(w) {
|
||||
return NaN(), fmt.Errorf("length of weights unequal to vector length")
|
||||
}
|
||||
|
||||
ws := 0.0
|
||||
|
||||
for i := range v {
|
||||
ws += v[i] * w[i]
|
||||
}
|
||||
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
// WeightedMean returns the vector's weighted mean value based of the specified weights.
|
||||
func (v Vector) WeightedMean(w Vector) (float64, error) {
|
||||
ws, err := v.weightedSum(w)
|
||||
|
||||
if err != nil {
|
||||
return NaN(), err
|
||||
}
|
||||
|
||||
sw := w.Sum()
|
||||
|
||||
return ws / sw, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ func TestGeometricMean(t *testing.T) {
|
|||
t.Run("PowersOfThree", func(t *testing.T) {
|
||||
assert.InDelta(t, math.Pow(3, 1.5), GeometricMean(Vector{1, 3, 9, 27}), 0.00001)
|
||||
})
|
||||
t.Run("Method", func(t *testing.T) {
|
||||
assert.InDelta(t, 4.0, Vector{2, 8}.GeometricMean(), 0.00001)
|
||||
})
|
||||
t.Run("ContainsZero", func(t *testing.T) {
|
||||
// A zero element drives the product to 0, so the geometric mean is 0.
|
||||
assert.InDelta(t, 0.0, GeometricMean(Vector{2, 0, 8}), 0.00001)
|
||||
|
|
@ -48,6 +51,9 @@ func TestHarmonicMean(t *testing.T) {
|
|||
t.Run("Equal", func(t *testing.T) {
|
||||
assert.InDelta(t, 2.0, HarmonicMean(Vector{2, 2}), 0.00001)
|
||||
})
|
||||
t.Run("Method", func(t *testing.T) {
|
||||
assert.InDelta(t, 3.0/1.75, Vector{1, 2, 4}.HarmonicMean(), 0.00001)
|
||||
})
|
||||
t.Run("ContainsZero", func(t *testing.T) {
|
||||
assert.True(t, math.IsNaN(HarmonicMean(Vector{1, 0, 2})))
|
||||
})
|
||||
|
|
@ -58,3 +64,16 @@ func TestHarmonicMean(t *testing.T) {
|
|||
assert.True(t, math.IsNaN(HarmonicMean(Vector{})))
|
||||
})
|
||||
}
|
||||
|
||||
func TestVector_WeightedMean(t *testing.T) {
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
r, err := Vector{1, 2, 4}.WeightedMean(Vector{1, 0, 1})
|
||||
assert.NoError(t, err)
|
||||
assert.InDelta(t, 2.5, r, 0.00001)
|
||||
})
|
||||
t.Run("LengthMismatch", func(t *testing.T) {
|
||||
r, err := Vector{1, 2, 4}.WeightedMean(Vector{1, 1})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, math.IsNaN(r))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
28
pkg/vector/norm.go
Normal file
28
pkg/vector/norm.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package vector
|
||||
|
||||
import "math"
|
||||
|
||||
// Norm returns the vector size (magnitude),
|
||||
// see https://builtin.com/data-science/vector-norms.
|
||||
func (v Vector) Norm(pow float64) float64 {
|
||||
return Norm(v, pow)
|
||||
}
|
||||
|
||||
// EuclideanNorm returns the Euclidean vector size (magnitude),
|
||||
// see https://builtin.com/data-science/vector-norms.
|
||||
func (v Vector) EuclideanNorm() float64 {
|
||||
return v.Norm(2.0)
|
||||
}
|
||||
|
||||
// Norm returns the size of the vector (use pow = 2.0 for the Euclidean distance),
|
||||
// see https://builtin.com/data-science/vector-norms. Absolute values are used so
|
||||
// that odd powers (e.g. the L1 norm) stay well-defined for negative components.
|
||||
func Norm(v Vector, pow float64) float64 {
|
||||
s := 0.0
|
||||
|
||||
for _, f := range v {
|
||||
s += math.Pow(math.Abs(f), pow)
|
||||
}
|
||||
|
||||
return math.Pow(s, 1/pow)
|
||||
}
|
||||
32
pkg/vector/norm_test.go
Normal file
32
pkg/vector/norm_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package vector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVector_EuclideanNorm(t *testing.T) {
|
||||
a := Vector{1, 2, 3, 4, 6, 5}
|
||||
b := Vector{2, 1, 3, 4, 5, 6}
|
||||
d := Vector{0, 0, 0, 0, 0, 0}
|
||||
e := Vector{}
|
||||
|
||||
assert.InDelta(t, 9.539392014169456, a.EuclideanNorm(), 0.01)
|
||||
assert.InDelta(t, 9.539392014169456, b.EuclideanNorm(), 0.01)
|
||||
assert.Equal(t, a.EuclideanNorm(), b.EuclideanNorm())
|
||||
assert.InDelta(t, 0.9999999779072661, faceEmbeddingB.EuclideanNorm(), 0.01)
|
||||
assert.InDelta(t, 0, d.EuclideanNorm(), 0.01)
|
||||
assert.InDelta(t, 0, e.EuclideanNorm(), 0.01)
|
||||
assert.Equal(t, d.EuclideanNorm(), e.EuclideanNorm())
|
||||
}
|
||||
|
||||
func TestNorm(t *testing.T) {
|
||||
t.Run("Euclidean", func(t *testing.T) {
|
||||
assert.InDelta(t, 5.0, Norm(Vector{3, -4}, 2.0), 0.00001)
|
||||
})
|
||||
t.Run("Manhattan", func(t *testing.T) {
|
||||
// L1 norm uses absolute values, so negatives contribute their magnitude.
|
||||
assert.InDelta(t, 5.0, Norm(Vector{1, -2, 2}, 1.0), 0.00001)
|
||||
})
|
||||
}
|
||||
32
pkg/vector/normalize.go
Normal file
32
pkg/vector/normalize.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package vector
|
||||
|
||||
import "math"
|
||||
|
||||
// Normalize scales the vector to unit length (L2 norm) in place.
|
||||
// A zero vector (including an empty one) is left unchanged to avoid
|
||||
// a division by zero.
|
||||
func (v Vector) Normalize() {
|
||||
var sum float64
|
||||
|
||||
for _, f := range v {
|
||||
sum += f * f
|
||||
}
|
||||
|
||||
if sum == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
inv := 1 / math.Sqrt(sum)
|
||||
|
||||
for i := range v {
|
||||
v[i] *= inv
|
||||
}
|
||||
}
|
||||
|
||||
// Normalized returns an L2-normalized copy of the vector,
|
||||
// leaving the receiver unchanged.
|
||||
func (v Vector) Normalized() Vector {
|
||||
c := v.Copy()
|
||||
c.Normalize()
|
||||
return c
|
||||
}
|
||||
|
|
@ -58,35 +58,3 @@ func TestVector_Normalized(t *testing.T) {
|
|||
assert.Equal(t, Vector{}, Vector{}.Normalized())
|
||||
})
|
||||
}
|
||||
|
||||
func TestCentroid(t *testing.T) {
|
||||
t.Run("Mean", func(t *testing.T) {
|
||||
got := Centroid(Vectors{{1, 2}, {3, 4}})
|
||||
assert.Equal(t, Vector{2, 3}, got)
|
||||
})
|
||||
t.Run("Single", func(t *testing.T) {
|
||||
got := Centroid(Vectors{{2, 4, 6}})
|
||||
assert.Equal(t, Vector{2, 4, 6}, got)
|
||||
})
|
||||
t.Run("SkipsMismatchedDimensions", func(t *testing.T) {
|
||||
// The 3-D vector is ignored; the mean is taken over the two 2-D vectors.
|
||||
got := Centroid(Vectors{{1, 2}, {1, 2, 3}, {3, 4}})
|
||||
assert.Equal(t, Vector{2, 3}, got)
|
||||
})
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.Nil(t, Centroid(Vectors{}))
|
||||
assert.Nil(t, Centroid(nil))
|
||||
})
|
||||
t.Run("FirstVectorEmpty", func(t *testing.T) {
|
||||
assert.Nil(t, Centroid(Vectors{{}, {1, 2}}))
|
||||
})
|
||||
t.Run("Independent", func(t *testing.T) {
|
||||
a := Vector{1, 2}
|
||||
b := Vector{3, 4}
|
||||
got := Centroid(Vectors{a, b})
|
||||
got[0] = 100
|
||||
// Mutating the result must not change the inputs.
|
||||
assert.Equal(t, Vector{1, 2}, a)
|
||||
assert.Equal(t, Vector{3, 4}, b)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
29
pkg/vector/product.go
Normal file
29
pkg/vector/product.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package vector
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Product returns a vector of element-wise products of two input vectors.
|
||||
func Product(a, b Vector) (Vector, error) {
|
||||
if len(a) != len(b) {
|
||||
return nil, fmt.Errorf("vector dimensions do not match (%d, %d)", len(a), len(b))
|
||||
}
|
||||
|
||||
p := make(Vector, len(a))
|
||||
|
||||
for i := range a {
|
||||
p[i] = a[i] * b[i]
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// DotProduct returns the dot product of two vectors.
|
||||
func DotProduct(a, b Vector) (float64, error) {
|
||||
p, err := Product(a, b)
|
||||
|
||||
if err != nil {
|
||||
return NaN(), err
|
||||
}
|
||||
|
||||
return p.Sum(), nil
|
||||
}
|
||||
34
pkg/vector/product_test.go
Normal file
34
pkg/vector/product_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package vector
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProduct(t *testing.T) {
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
p, err := Product(Vector{1, 2, 3}, Vector{4, 5, 6})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Vector{4, 10, 18}, p)
|
||||
})
|
||||
t.Run("LengthMismatch", func(t *testing.T) {
|
||||
p, err := Product(Vector{1, 2, 3}, Vector{4, 5})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, p)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDotProduct(t *testing.T) {
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
r, err := DotProduct(Vector{1, 2, 3}, Vector{4, 5, 6})
|
||||
assert.NoError(t, err)
|
||||
assert.InDelta(t, 32.0, r, 0.00001)
|
||||
})
|
||||
t.Run("LengthMismatch", func(t *testing.T) {
|
||||
r, err := DotProduct(Vector{1, 2, 3}, Vector{4, 5})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, math.IsNaN(r))
|
||||
})
|
||||
}
|
||||
53
pkg/vector/stats.go
Normal file
53
pkg/vector/stats.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package vector
|
||||
|
||||
import "math"
|
||||
|
||||
// Sd calculates the vector's standard deviation.
|
||||
func (v Vector) Sd() float64 {
|
||||
return math.Sqrt(v.Variance())
|
||||
}
|
||||
|
||||
// Variance calculates the vector's variance.
|
||||
func (v Vector) Variance() float64 {
|
||||
return v.variance(v.Mean())
|
||||
}
|
||||
|
||||
// variance returns the sample variance around the given mean.
|
||||
// Empty and single-element vectors have zero variance by convention,
|
||||
// which also avoids a division by zero in the n-1 denominator.
|
||||
func (v Vector) variance(mean float64) float64 {
|
||||
n := float64(len(v))
|
||||
|
||||
if n < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
ss := 0.0
|
||||
|
||||
for _, f := range v {
|
||||
d := f - mean
|
||||
ss += d * d
|
||||
}
|
||||
|
||||
return ss / (n - 1)
|
||||
}
|
||||
|
||||
// Cor returns the Pearson correlation between two vectors.
|
||||
func Cor(a, b Vector) (float64, error) {
|
||||
n := float64(len(a))
|
||||
xy, err := Product(a, b)
|
||||
|
||||
if err != nil {
|
||||
return NaN(), err
|
||||
}
|
||||
|
||||
sx := a.Sd()
|
||||
sy := b.Sd()
|
||||
|
||||
mx := a.Mean()
|
||||
my := b.Mean()
|
||||
|
||||
r := (xy.Sum() - n*mx*my) / ((n - 1) * sx * sy)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
38
pkg/vector/stats_test.go
Normal file
38
pkg/vector/stats_test.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package vector
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVector_Variance(t *testing.T) {
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
assert.InDelta(t, 32.0/7.0, Vector{2, 4, 4, 4, 5, 5, 7, 9}.Variance(), 0.00001)
|
||||
})
|
||||
t.Run("Single", func(t *testing.T) {
|
||||
assert.InDelta(t, 0.0, Vector{5}.Variance(), 0.00001)
|
||||
})
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.InDelta(t, 0.0, Vector{}.Variance(), 0.00001)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVector_Sd(t *testing.T) {
|
||||
assert.InDelta(t, math.Sqrt(32.0/7.0), Vector{2, 4, 4, 4, 5, 5, 7, 9}.Sd(), 0.00001)
|
||||
assert.InDelta(t, 0.0, Vector{5}.Sd(), 0.00001)
|
||||
}
|
||||
|
||||
func TestCor(t *testing.T) {
|
||||
t.Run("PerfectPositive", func(t *testing.T) {
|
||||
r, err := Cor(Vector{1, 2, 3}, Vector{2, 4, 6})
|
||||
assert.NoError(t, err)
|
||||
assert.InDelta(t, 1.0, r, 0.00001)
|
||||
})
|
||||
t.Run("LengthMismatch", func(t *testing.T) {
|
||||
r, err := Cor(Vector{1, 2, 3}, Vector{1, 2})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, math.IsNaN(r))
|
||||
})
|
||||
}
|
||||
|
|
@ -1,321 +0,0 @@
|
|||
package vector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Copy returns a copy of the vector.
|
||||
func (v Vector) Copy() Vector {
|
||||
y := make(Vector, len(v))
|
||||
copy(y, v)
|
||||
return y
|
||||
}
|
||||
|
||||
// Dim returns the number of values (dimension).
|
||||
func (v Vector) Dim() int {
|
||||
return len(v)
|
||||
}
|
||||
|
||||
// Sum returns the sum of the vector values.
|
||||
func (v Vector) Sum() float64 {
|
||||
s := 0.0
|
||||
|
||||
for _, f := range v {
|
||||
s += f
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// weightedSum returns the weighted sum of the vector. This is really only useful in
|
||||
// calculating the weighted mean.
|
||||
func (v Vector) weightedSum(w Vector) (float64, error) {
|
||||
if len(v) != len(w) {
|
||||
return NaN(), fmt.Errorf("length of weights unequal to vector length")
|
||||
}
|
||||
|
||||
ws := 0.0
|
||||
|
||||
for i := range v {
|
||||
ws += v[i] * w[i]
|
||||
}
|
||||
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
// Mean returns the vector's mean value.
|
||||
func (v Vector) Mean() float64 {
|
||||
return Mean(v)
|
||||
}
|
||||
|
||||
// GeometricMean returns the vector's geometric mean value.
|
||||
func (v Vector) GeometricMean() float64 {
|
||||
return GeometricMean(v)
|
||||
}
|
||||
|
||||
// HarmonicMean returns the vector's harmonic mean value.
|
||||
func (v Vector) HarmonicMean() float64 {
|
||||
return HarmonicMean(v)
|
||||
}
|
||||
|
||||
// WeightedMean returns the vector's weighted mean value based of the specified weights.
|
||||
func (v Vector) WeightedMean(w Vector) (float64, error) {
|
||||
ws, err := v.weightedSum(w)
|
||||
|
||||
if err != nil {
|
||||
return NaN(), err
|
||||
}
|
||||
|
||||
sw := w.Sum()
|
||||
|
||||
return ws / sw, nil
|
||||
}
|
||||
|
||||
// Sd calculates the vector's standard deviation.
|
||||
func (v Vector) Sd() float64 {
|
||||
return math.Sqrt(v.Variance())
|
||||
}
|
||||
|
||||
// Variance calculates the vector's variance.
|
||||
func (v Vector) Variance() float64 {
|
||||
return v.variance(v.Mean())
|
||||
}
|
||||
|
||||
// EuclideanDist returns the Euclidean distance between the vectors,
|
||||
func (v Vector) EuclideanDist(w Vector) float64 {
|
||||
return EuclideanDist(v, w)
|
||||
}
|
||||
|
||||
// CosineSimilarity returns the cosine similarity between two vectors,
|
||||
// ranging from -1 (opposite) to 1 (identical).
|
||||
func (v Vector) CosineSimilarity(w Vector) float64 {
|
||||
return CosineSimilarity(v, w)
|
||||
}
|
||||
|
||||
// CosineDist returns the cosine distance between two vectors (1 - cosine similarity).
|
||||
func (v Vector) CosineDist(w Vector) float64 {
|
||||
return CosineDist(v, w)
|
||||
}
|
||||
|
||||
// Norm returns the vector size (magnitude),
|
||||
// see https://builtin.com/data-science/vector-norms.
|
||||
func (v Vector) Norm(pow float64) float64 {
|
||||
return Norm(v, pow)
|
||||
}
|
||||
|
||||
// EuclideanNorm returns the Euclidean vector size (magnitude),
|
||||
// see https://builtin.com/data-science/vector-norms.
|
||||
func (v Vector) EuclideanNorm() float64 {
|
||||
return v.Norm(2.0)
|
||||
}
|
||||
|
||||
// Normalize scales the vector to unit length (L2 norm) in place.
|
||||
// A zero vector (including an empty one) is left unchanged to avoid
|
||||
// a division by zero.
|
||||
func (v Vector) Normalize() {
|
||||
var sum float64
|
||||
|
||||
for _, f := range v {
|
||||
sum += f * f
|
||||
}
|
||||
|
||||
if sum == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
inv := 1 / math.Sqrt(sum)
|
||||
|
||||
for i := range v {
|
||||
v[i] *= inv
|
||||
}
|
||||
}
|
||||
|
||||
// Normalized returns an L2-normalized copy of the vector,
|
||||
// leaving the receiver unchanged.
|
||||
func (v Vector) Normalized() Vector {
|
||||
c := v.Copy()
|
||||
c.Normalize()
|
||||
return c
|
||||
}
|
||||
|
||||
// variance returns the sample variance around the given mean.
|
||||
// Empty and single-element vectors have zero variance by convention,
|
||||
// which also avoids a division by zero in the n-1 denominator.
|
||||
func (v Vector) variance(mean float64) float64 {
|
||||
n := float64(len(v))
|
||||
|
||||
if n < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
ss := 0.0
|
||||
|
||||
for _, f := range v {
|
||||
d := f - mean
|
||||
ss += d * d
|
||||
}
|
||||
|
||||
return ss / (n - 1)
|
||||
}
|
||||
|
||||
// Product returns a vector of element-wise products of two input vectors.
|
||||
func Product(a, b Vector) (Vector, error) {
|
||||
if len(a) != len(b) {
|
||||
return nil, fmt.Errorf("vector dimensions do not match (%d, %d)", len(a), len(b))
|
||||
}
|
||||
|
||||
p := make(Vector, len(a))
|
||||
|
||||
for i := range a {
|
||||
p[i] = a[i] * b[i]
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// DotProduct returns the dot product of two vectors.
|
||||
func DotProduct(a, b Vector) (float64, error) {
|
||||
p, err := Product(a, b)
|
||||
|
||||
if err != nil {
|
||||
return NaN(), err
|
||||
}
|
||||
|
||||
return p.Sum(), nil
|
||||
}
|
||||
|
||||
// Norm returns the size of the vector (use pow = 2.0 for the Euclidean distance),
|
||||
// see https://builtin.com/data-science/vector-norms. Absolute values are used so
|
||||
// that odd powers (e.g. the L1 norm) stay well-defined for negative components.
|
||||
func Norm(v Vector, pow float64) float64 {
|
||||
s := 0.0
|
||||
|
||||
for _, f := range v {
|
||||
s += math.Pow(math.Abs(f), pow)
|
||||
}
|
||||
|
||||
return math.Pow(s, 1/pow)
|
||||
}
|
||||
|
||||
// EuclideanDist returns the Euclidean distance between multiple vectors.
|
||||
func EuclideanDist(a, b Vector) float64 {
|
||||
if a.Dim() != b.Dim() {
|
||||
return NaN()
|
||||
}
|
||||
|
||||
var (
|
||||
s, t float64
|
||||
)
|
||||
|
||||
for i := range a {
|
||||
t = a[i] - b[i]
|
||||
s += t * t
|
||||
}
|
||||
|
||||
return math.Sqrt(s)
|
||||
}
|
||||
|
||||
// CosineSimilarity returns the cosine similarity between two vectors, ranging
|
||||
// from -1 (opposite) to 1 (identical). It returns NaN when the dimensions
|
||||
// differ and 0 when either operand is a zero vector.
|
||||
func CosineSimilarity(a, b Vector) float64 {
|
||||
if a.Dim() != b.Dim() {
|
||||
return NaN()
|
||||
}
|
||||
|
||||
var sum, s1, s2 float64
|
||||
|
||||
for i := range a {
|
||||
sum += a[i] * b[i]
|
||||
s1 += a[i] * a[i]
|
||||
s2 += b[i] * b[i]
|
||||
}
|
||||
|
||||
if s1 == 0 || s2 == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
return sum / (math.Sqrt(s1) * math.Sqrt(s2))
|
||||
}
|
||||
|
||||
// CosineDist returns the cosine distance between two vectors, defined as
|
||||
// 1 - CosineSimilarity. Identical vectors yield 0; it returns NaN when the
|
||||
// dimensions differ.
|
||||
func CosineDist(a, b Vector) float64 {
|
||||
return 1.0 - CosineSimilarity(a, b)
|
||||
}
|
||||
|
||||
// CosineDists returns the cosine distances between two sets of vectors.
|
||||
func CosineDists(x, y Vectors) Vectors {
|
||||
result := make(Vectors, len(x))
|
||||
|
||||
for i, a := range x {
|
||||
result[i] = make([]float64, len(y))
|
||||
|
||||
for j, b := range y {
|
||||
result[i][j] = CosineDist(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Centroid returns the element-wise mean (centroid) of the given vectors as a
|
||||
// new, independent vector. Vectors whose length differs from the first vector
|
||||
// are ignored, and the mean is taken over the vectors actually included. It
|
||||
// returns nil when vs is empty or the first vector has no elements.
|
||||
func Centroid(vs Vectors) Vector {
|
||||
if len(vs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dim := len(vs[0])
|
||||
|
||||
if dim == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(Vector, dim)
|
||||
n := 0
|
||||
|
||||
for _, v := range vs {
|
||||
if len(v) != dim {
|
||||
continue
|
||||
}
|
||||
|
||||
for j := range dim {
|
||||
result[j] += v[j]
|
||||
}
|
||||
|
||||
n++
|
||||
}
|
||||
|
||||
inv := 1 / float64(n)
|
||||
|
||||
for j := range result {
|
||||
result[j] *= inv
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Cor returns the Pearson correlation between two vectors.
|
||||
func Cor(a, b Vector) (float64, error) {
|
||||
n := float64(len(a))
|
||||
xy, err := Product(a, b)
|
||||
|
||||
if err != nil {
|
||||
return NaN(), err
|
||||
}
|
||||
|
||||
sx := a.Sd()
|
||||
sy := b.Sd()
|
||||
|
||||
mx := a.Mean()
|
||||
my := b.Mean()
|
||||
|
||||
r := (xy.Sum() - n*mx*my) / ((n - 1) * sx * sy)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
package vector
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVector_Sum(t *testing.T) {
|
||||
assert.InDelta(t, 6.0, Vector{1, 2, 3}.Sum(), 0.00001)
|
||||
assert.InDelta(t, 0.0, Vector{}.Sum(), 0.00001)
|
||||
}
|
||||
|
||||
func TestVector_Dim(t *testing.T) {
|
||||
assert.Equal(t, 3, Vector{1, 2, 3}.Dim())
|
||||
assert.Equal(t, 0, Vector{}.Dim())
|
||||
}
|
||||
|
||||
func TestVector_Copy(t *testing.T) {
|
||||
a := Vector{1, 2, 3}
|
||||
b := a.Copy()
|
||||
b[0] = 99
|
||||
assert.Equal(t, Vector{1, 2, 3}, a, "modifying the copy must not change the original")
|
||||
assert.Equal(t, Vector{99, 2, 3}, b)
|
||||
}
|
||||
|
||||
func TestNullVector(t *testing.T) {
|
||||
v := NullVector(3)
|
||||
assert.Equal(t, 3, v.Dim())
|
||||
assert.Equal(t, Vector{0, 0, 0}, v)
|
||||
}
|
||||
|
||||
func TestNewVector_Float64Copy(t *testing.T) {
|
||||
// NewVector must return a vector independent of the source slice.
|
||||
src := []float64{1, 2, 3}
|
||||
v, err := NewVector(src)
|
||||
assert.NoError(t, err)
|
||||
src[0] = 99
|
||||
assert.Equal(t, Vector{1, 2, 3}, v)
|
||||
}
|
||||
|
||||
func TestVector_Variance(t *testing.T) {
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
assert.InDelta(t, 32.0/7.0, Vector{2, 4, 4, 4, 5, 5, 7, 9}.Variance(), 0.00001)
|
||||
})
|
||||
t.Run("Single", func(t *testing.T) {
|
||||
assert.InDelta(t, 0.0, Vector{5}.Variance(), 0.00001)
|
||||
})
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.InDelta(t, 0.0, Vector{}.Variance(), 0.00001)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVector_Sd(t *testing.T) {
|
||||
assert.InDelta(t, math.Sqrt(32.0/7.0), Vector{2, 4, 4, 4, 5, 5, 7, 9}.Sd(), 0.00001)
|
||||
assert.InDelta(t, 0.0, Vector{5}.Sd(), 0.00001)
|
||||
}
|
||||
|
||||
func TestVector_WeightedMean(t *testing.T) {
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
r, err := Vector{1, 2, 4}.WeightedMean(Vector{1, 0, 1})
|
||||
assert.NoError(t, err)
|
||||
assert.InDelta(t, 2.5, r, 0.00001)
|
||||
})
|
||||
t.Run("LengthMismatch", func(t *testing.T) {
|
||||
r, err := Vector{1, 2, 4}.WeightedMean(Vector{1, 1})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, math.IsNaN(r))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCor(t *testing.T) {
|
||||
t.Run("PerfectPositive", func(t *testing.T) {
|
||||
r, err := Cor(Vector{1, 2, 3}, Vector{2, 4, 6})
|
||||
assert.NoError(t, err)
|
||||
assert.InDelta(t, 1.0, r, 0.00001)
|
||||
})
|
||||
t.Run("LengthMismatch", func(t *testing.T) {
|
||||
r, err := Cor(Vector{1, 2, 3}, Vector{1, 2})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, math.IsNaN(r))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNorm(t *testing.T) {
|
||||
t.Run("Euclidean", func(t *testing.T) {
|
||||
assert.InDelta(t, 5.0, Norm(Vector{3, -4}, 2.0), 0.00001)
|
||||
})
|
||||
t.Run("Manhattan", func(t *testing.T) {
|
||||
// L1 norm uses absolute values, so negatives contribute their magnitude.
|
||||
assert.InDelta(t, 5.0, Norm(Vector{1, -2, 2}, 1.0), 0.00001)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProduct(t *testing.T) {
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
p, err := Product(Vector{1, 2, 3}, Vector{4, 5, 6})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Vector{4, 10, 18}, p)
|
||||
})
|
||||
t.Run("LengthMismatch", func(t *testing.T) {
|
||||
p, err := Product(Vector{1, 2, 3}, Vector{4, 5})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, p)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDotProduct(t *testing.T) {
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
r, err := DotProduct(Vector{1, 2, 3}, Vector{4, 5, 6})
|
||||
assert.NoError(t, err)
|
||||
assert.InDelta(t, 32.0, r, 0.00001)
|
||||
})
|
||||
t.Run("LengthMismatch", func(t *testing.T) {
|
||||
r, err := DotProduct(Vector{1, 2, 3}, Vector{4, 5})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, math.IsNaN(r))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCosineSimilarity(t *testing.T) {
|
||||
t.Run("Orthogonal", func(t *testing.T) {
|
||||
assert.InDelta(t, 0.0, CosineSimilarity(Vector{1, 0}, Vector{0, 1}), 0.00001)
|
||||
assert.InDelta(t, 1.0, CosineDist(Vector{1, 0}, Vector{0, 1}), 0.00001)
|
||||
})
|
||||
t.Run("Opposite", func(t *testing.T) {
|
||||
assert.InDelta(t, -1.0, CosineSimilarity(Vector{1, 0}, Vector{-1, 0}), 0.00001)
|
||||
assert.InDelta(t, 2.0, CosineDist(Vector{1, 0}, Vector{-1, 0}), 0.00001)
|
||||
})
|
||||
t.Run("DimensionMismatch", func(t *testing.T) {
|
||||
assert.True(t, math.IsNaN(CosineSimilarity(Vector{1, 0}, Vector{1, 0, 0})))
|
||||
assert.True(t, math.IsNaN(CosineDist(Vector{1, 0}, Vector{1, 0, 0})))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCosineDists(t *testing.T) {
|
||||
x := Vectors{{1, 0}, {0, 1}}
|
||||
y := Vectors{{1, 0}, {-1, 0}}
|
||||
got := CosineDists(x, y)
|
||||
assert.Len(t, got, 2)
|
||||
assert.InDelta(t, 0.0, got[0][0], 0.00001) // identical
|
||||
assert.InDelta(t, 2.0, got[0][1], 0.00001) // opposite
|
||||
assert.InDelta(t, 1.0, got[1][0], 0.00001) // orthogonal
|
||||
assert.InDelta(t, 1.0, got[1][1], 0.00001) // orthogonal
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -72,6 +72,29 @@ func NullVector(dim int) Vector {
|
|||
return make(Vector, dim)
|
||||
}
|
||||
|
||||
// Copy returns a copy of the vector.
|
||||
func (v Vector) Copy() Vector {
|
||||
y := make(Vector, len(v))
|
||||
copy(y, v)
|
||||
return y
|
||||
}
|
||||
|
||||
// Dim returns the number of values (dimension).
|
||||
func (v Vector) Dim() int {
|
||||
return len(v)
|
||||
}
|
||||
|
||||
// Sum returns the sum of the vector values.
|
||||
func (v Vector) Sum() float64 {
|
||||
s := 0.0
|
||||
|
||||
for _, f := range v {
|
||||
s += f
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// uint8ToVector creates a new vector from a uint8 slice.
|
||||
func uint8ToVector(values []uint8) Vector {
|
||||
v := make(Vector, len(values))
|
||||
|
|
|
|||
|
|
@ -22,9 +22,88 @@ func TestNewVector(t *testing.T) {
|
|||
assert.IsType(t, Vector{}, v)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("Uint8", func(t *testing.T) {
|
||||
v, err := NewVector([]uint8{1, 2, 3})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Vector{1, 2, 3}, v)
|
||||
})
|
||||
t.Run("Uint16", func(t *testing.T) {
|
||||
v, err := NewVector([]uint16{1, 2, 3})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Vector{1, 2, 3}, v)
|
||||
})
|
||||
t.Run("Uint32", func(t *testing.T) {
|
||||
v, err := NewVector([]uint32{1, 2, 3})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Vector{1, 2, 3}, v)
|
||||
})
|
||||
t.Run("Uint64", func(t *testing.T) {
|
||||
v, err := NewVector([]uint64{1, 2, 3})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Vector{1, 2, 3}, v)
|
||||
})
|
||||
t.Run("Int8", func(t *testing.T) {
|
||||
v, err := NewVector([]int8{-1, 2, 3})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Vector{-1, 2, 3}, v)
|
||||
})
|
||||
t.Run("Int16", func(t *testing.T) {
|
||||
v, err := NewVector([]int16{-1, 2, 3})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Vector{-1, 2, 3}, v)
|
||||
})
|
||||
t.Run("Int32", func(t *testing.T) {
|
||||
v, err := NewVector([]int32{-1, 2, 3})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Vector{-1, 2, 3}, v)
|
||||
})
|
||||
t.Run("Int64", func(t *testing.T) {
|
||||
v, err := NewVector([]int64{-1, 2, 3})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Vector{-1, 2, 3}, v)
|
||||
})
|
||||
t.Run("Vector", func(t *testing.T) {
|
||||
src := Vector{1, 2, 3}
|
||||
v, err := NewVector(src)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, src, v)
|
||||
})
|
||||
t.Run("String", func(t *testing.T) {
|
||||
v, err := NewVector([]string{"a", "b", "c"})
|
||||
assert.IsType(t, Vector{}, v)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewVector_Float64Copy(t *testing.T) {
|
||||
// NewVector must return a vector independent of the source slice.
|
||||
src := []float64{1, 2, 3}
|
||||
v, err := NewVector(src)
|
||||
assert.NoError(t, err)
|
||||
src[0] = 99
|
||||
assert.Equal(t, Vector{1, 2, 3}, v)
|
||||
}
|
||||
|
||||
func TestNullVector(t *testing.T) {
|
||||
v := NullVector(3)
|
||||
assert.Equal(t, 3, v.Dim())
|
||||
assert.Equal(t, Vector{0, 0, 0}, v)
|
||||
}
|
||||
|
||||
func TestVector_Copy(t *testing.T) {
|
||||
a := Vector{1, 2, 3}
|
||||
b := a.Copy()
|
||||
b[0] = 99
|
||||
assert.Equal(t, Vector{1, 2, 3}, a, "modifying the copy must not change the original")
|
||||
assert.Equal(t, Vector{99, 2, 3}, b)
|
||||
}
|
||||
|
||||
func TestVector_Dim(t *testing.T) {
|
||||
assert.Equal(t, 3, Vector{1, 2, 3}.Dim())
|
||||
assert.Equal(t, 0, Vector{}.Dim())
|
||||
}
|
||||
|
||||
func TestVector_Sum(t *testing.T) {
|
||||
assert.InDelta(t, 6.0, Vector{1, 2, 3}.Sum(), 0.00001)
|
||||
assert.InDelta(t, 0.0, Vector{}.Sum(), 0.00001)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue