rmds 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,150 @@
1
+ #
2
+ # RMDS - Ruby Multidimensional Scaling Library
3
+ # Copyright (c) Christoph Heindl, 2010
4
+ # http://github.com/cheind/rmds
5
+ #
6
+
7
+ module MDS
8
+
9
+ #
10
+ # In metric MDS dissimilarities are interpreted as distances und
11
+ # the goal is to find an embedding in an Euclidean space that best preserves
12
+ # the given distances.
13
+ #
14
+ # This implementation gives an analytical solution and avoids iterative
15
+ # optimization. The performance of the algorithm highly depends on the
16
+ # implementation of the eigen-decomposition provided via {MDS::MatrixInterface}.
17
+ #
18
+ # *Examples*
19
+ # - {Examples.minimal_metric}
20
+ # - {Examples.extended_metric}
21
+ #
22
+ class Metric
23
+
24
+ #
25
+ # Find a Cartesian embedding for the given distances.
26
+ #
27
+ # Instead of a fixed dimensionality for the resulting embedding, this
28
+ # method determines the dimensionality based on the variances of distances
29
+ # in its input matrix and the parameter passed. The parameter specifies
30
+ # the percent of variance of distance to preserve in the Cartesian embedding.
31
+ #
32
+ # @param [MDS::Matrix] d squared Eulcidean distance matrix of observations
33
+ # @param [Float] k the percent of variance of distances
34
+ # to preserve in embedding in the range [0..1].
35
+ # @return [MDS::Matrix] the matrix containing the cartesian embedding.
36
+ #
37
+ def Metric.projectk(d, k)
38
+ b = Metric.shift(d)
39
+ eval, evec = b.ed
40
+ dims = Metric.find_dimensionality(eval, k)
41
+ Metric.project(eval, evec, dims)
42
+ end
43
+
44
+ #
45
+ # Find a Cartesian embedding for the given distances.
46
+ #
47
+ # @param [MDS::Matrix] d squared Eulcidean distance matrix of observations
48
+ # @param [Integer] dims the number of dimensions of the embedding.
49
+ # @return [MDS::Matrix] the matrix containing the cartesian embedding.
50
+ #
51
+ def Metric.projectd(d, dims)
52
+ b = Metric.shift(d)
53
+ eval, evec = b.ed
54
+ Metric.project(eval, evec, dims)
55
+ end
56
+
57
+ #
58
+ # Calculates the squared Euclidean distances for all
59
+ # pairwise observations in the given matrix. Each observation
60
+ # corresponds to a matrix row and is provided in Cartesian coordinates.
61
+ #
62
+ # The result is a real symmetric matrix of size +NxN+, where +N+ is the
63
+ # number of observations in the input. Each element +(i,j)+ in this matrix
64
+ # corresponds to the squared distance between the i-th and j-th observation
65
+ # in the input matrix.
66
+ #
67
+ # @param [MDS::Matrix] x the matrix of observations.
68
+ # @return [MDS::Matrix] the squared Euclidean distance matrix
69
+ #
70
+ def Metric.squared_distances(x)
71
+ # Product of x with transpose of x
72
+ xxt = x * x.t
73
+ # 1xN matrix of ones, where N size of xxt
74
+ ones = Matrix.create(1, xxt.nrows, 1.0)
75
+ # Nx1 matrix containing diagonal elements of x
76
+ diagonals = xxt.diagonals
77
+ c = Matrix.create_block(xxt.nrows, 1) do |i, j|
78
+ diagonals[i]
79
+ end
80
+ c * ones + (c * ones).t - xxt * 2
81
+ end
82
+
83
+
84
+ protected
85
+
86
+ #
87
+ # Transform the squared distance matrix into a matrix of Cartesian
88
+ # coordinates by projecting the distances onto the basis formed by
89
+ # the eigen-decomposition of the initial matrix.
90
+ #
91
+ # @param [MDS::Matrix] d squared Euclidean distance matrix
92
+ # @param [Float] k percent [0..1] of variances in distances to keep in embedding.
93
+ # @return [MDS::Matrix] containing the Cartesian embedding.
94
+ #
95
+ def Metric.project(eval, evec, dims)
96
+ lam = eval.minor(0..eval.nrows-1, 0..dims-1)
97
+ for i in 0..lam.ncols-1
98
+ v = lam[i,i]
99
+ if v > 0.0
100
+ lam[i,i] = Math.sqrt(v)
101
+ end
102
+ end
103
+ evec * lam
104
+ end
105
+
106
+ #
107
+ # Transforms the distance matrix into an equivalent scalar product
108
+ # matrix.
109
+ #
110
+ # @param [MDS::Matrix] d the squared Euclidean distance matrix
111
+ # @return [MDS::Matrix] the equivalent scalar product matrix.
112
+ #
113
+ def Metric.shift(d)
114
+ # Nx1 matrix of ones
115
+ ones = Matrix.create(d.nrows, 1, 1.0)
116
+ # 1xN weight vector
117
+ m = Matrix.create(1, d.nrows, 1.0/d.nrows)
118
+ # NxN centering matrix
119
+ xi = Matrix.create_identity(d.nrows) - ones * m
120
+ # NxN shifted distances matrix, B
121
+ (xi * d * xi.t) * -0.5
122
+ end
123
+
124
+
125
+ #
126
+ # Find the dimensionality of the resulting coordinate space so that
127
+ # at least +k+ percent of the variance of the distances is preserved.
128
+ #
129
+ # @param [MDS::Matrix] eval the diagonal matrix of sorted eigen-values.
130
+ # @param [Float] k percent of variance to keep.
131
+ # @return [Integer] minimum number of dimensions to use, to keep k-percent
132
+ # of variances of distances in embedding.
133
+ #
134
+ def Metric.find_dimensionality(eval, k)
135
+ sum_ev = eval.trace
136
+ n = eval.nrows
137
+
138
+ i = 0
139
+ sum = 0
140
+ while ((i < n) &&
141
+ (eval[i,i] > 0.0) &&
142
+ (sum / sum_ev) < k)
143
+ sum += eval[i,i]
144
+ i += 1
145
+ end
146
+ i
147
+ end
148
+ end
149
+
150
+ end
@@ -0,0 +1,199 @@
1
+ #
2
+ # RMDS - Ruby Multidimensional Scaling Library
3
+ # Copyright (c) Christoph Heindl, 2010
4
+ # http://github.com/cheind/rmds
5
+ #
6
+
7
+ require 'mds/test/matrix_assertions.rb'
8
+
9
+ module MDS
10
+ module Test
11
+
12
+ #
13
+ # Module containing standarized tests for matrix interfaces.
14
+ #
15
+ module BundleMatrixInterface
16
+ include MDS::Test::MatrixAssertions
17
+
18
+ #----------------------------
19
+ # Matrix creators
20
+ #----------------------------
21
+
22
+ def test_create
23
+ m = MDS::Matrix.create(2, 3, 0.0)
24
+ assert_equal(2, m.nrows)
25
+ assert_equal(3, m.ncols)
26
+
27
+ for i in 0..m.nrows-1 do
28
+ for j in 0..m.ncols-1 do
29
+ assert_instance_of(Float, m[i,j])
30
+ assert_equal(0.0, m[i,j])
31
+ end
32
+ end
33
+ end
34
+
35
+ def test_create_identity
36
+ m = MDS::Matrix.create_identity(3)
37
+ r = MDS::Matrix.create(3, 3, 0.0)
38
+ r[0,0] = 1.0; r[1,1] = 1.0; r[2,2] = 1.0;
39
+ assert_equal_matrices(m, r)
40
+ end
41
+
42
+ def test_create_diagonal
43
+ m = MDS::Matrix.create_diagonal(1.0, 2.0, 3.0)
44
+ r = MDS::Matrix.create(3, 3, 0.0)
45
+ r[0,0] = 1.0; r[1,1] = 2.0; r[2,2] = 3.0;
46
+ assert_equal_matrices(m, r)
47
+ end
48
+
49
+ def test_create_block
50
+ m = MDS::Matrix.create_block(2,2) do |i,j|
51
+ i == j ? 0.0 : 1.0
52
+ end
53
+ r = MDS::Matrix.create(3, 3, 1.0)
54
+ r[0,0] = 0.0; r[1,1] = 0.0;
55
+ assert_equal_matrices(m, r)
56
+ end
57
+
58
+ def test_create_rows
59
+ m = MDS::Matrix.create_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0])
60
+ r = MDS::Matrix.create_block(2,3) do |i,j|
61
+ (i*3 + j + 1).to_f
62
+ end
63
+ assert_equal_matrices(m, r)
64
+ end
65
+
66
+ #----------------------------
67
+ # Matrix element accessors
68
+ #----------------------------
69
+
70
+ def test_get_set
71
+ a = MDS::Matrix.create(2, 2, 1.0)
72
+
73
+ a[0,0] = 1.0
74
+ a[0,1] = 2.0
75
+ a[1,0] = 3.0
76
+ a[1,1] = 4.0
77
+
78
+ assert_equal(1.0, a[0,0])
79
+ assert_equal(2.0, a[0,1])
80
+ assert_equal(3.0, a[1,0])
81
+ assert_equal(4.0, a[1,1])
82
+ end
83
+
84
+ #----------------------------
85
+ # Matrix views
86
+ #----------------------------
87
+
88
+ def test_transpose
89
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 2.0, 3.0])
90
+ r = MDS::Matrix.create_rows([2.0, 1.0], [3.0, 2.0], [4.0, 3.0])
91
+ m = a.t
92
+ assert_delta_matrices(r, m)
93
+ end
94
+
95
+ def test_diagonals
96
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 4.0, 3.0])
97
+ r = MDS::Matrix.create_rows([2.0, 4.0])
98
+ m = MDS::Matrix.create_rows(a.diagonals)
99
+ assert_delta_matrices(r, m)
100
+ end
101
+
102
+ def test_minor
103
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 4.0, 3.0])
104
+ r = MDS::Matrix.create_rows([4.0],[3.0])
105
+ m = a.minor(0..1, 2..2)
106
+ assert_delta_matrices(r, m)
107
+ end
108
+
109
+ def test_trace
110
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 4.0, 3.0])
111
+ assert_equal(6.0, a.trace)
112
+ end
113
+
114
+ def test_columns
115
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 4.0, 3.0])
116
+ assert_equal([[2.0, 1.0], [3.0, 4.0], [4.0, 3.0]], a.columns)
117
+ end
118
+
119
+ def test_rows
120
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 4.0, 3.0])
121
+ assert_equal([[2.0, 3.0, 4.0], [1.0, 4.0, 3.0]], a.rows)
122
+ end
123
+
124
+ #----------------------------
125
+ # Matrix operations
126
+ #----------------------------
127
+
128
+ def test_product
129
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 2.0, 3.0])
130
+ b = MDS::Matrix.create_rows([3.0, 1.0], [1.0, 2.0], [3.0, -4.0])
131
+ r = MDS::Matrix.create_rows([21.0, -8.0], [14.0, -7.0])
132
+
133
+ m = a * b
134
+ assert_delta_matrices(r, m)
135
+ end
136
+
137
+ def test_product_scalar
138
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 2.0, 3.0])
139
+ r = MDS::Matrix.create_rows([4.0, 6.0, 8.0], [2.0, 4.0, 6.0])
140
+ m = a * 2.0
141
+ assert_delta_matrices(r, m)
142
+ end
143
+
144
+ def test_add
145
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 2.0, 3.0])
146
+ b = MDS::Matrix.create_rows([1.0, 2.0, 3.0], [0.0, 0.0, 2.0])
147
+ r = MDS::Matrix.create_rows([3.0, 5.0, 7.0], [1.0, 2.0, 5.0])
148
+ m = a + b
149
+ assert_delta_matrices(r, m)
150
+ end
151
+
152
+ def test_sub
153
+ a = MDS::Matrix.create_rows([2.0, 3.0, 4.0], [1.0, 2.0, 3.0])
154
+ b = MDS::Matrix.create_rows([1.0, 2.0, 2.0], [0.0, 0.0, 2.0])
155
+ r = MDS::Matrix.create_rows([1.0, 1.0, 2.0], [1.0, 2.0, 1.0])
156
+ m = a - b
157
+ assert_delta_matrices(r, m)
158
+ end
159
+
160
+ def test_ed
161
+ a = MDS::Matrix.create_rows(
162
+ [0.0, 10.0, 2.0],
163
+ [10.0, 0.0, 20.0],
164
+ [2.0, 20.0, 0.0]
165
+ )
166
+
167
+ eval_a, evec_a = a.ed
168
+
169
+ assert_in_delta(23.2051, eval_a[0,0], 1e-3)
170
+ assert_in_delta(-1.5954, eval_a[1,1], 1e-3)
171
+ assert_in_delta(-21.6097, eval_a[2,2], 1e-3)
172
+
173
+ r0 = MDS::Matrix.create_rows([0.3529, 0.6934, 0.6281])
174
+ r1 = MDS::Matrix.create_rows([0.8948, -0.0541, -0.4430])
175
+ r2 = MDS::Matrix.create_rows([-0.2732, 0.7184, -0.6396])
176
+
177
+ v0 = evec_a.minor(0..2, 0..0)
178
+ v1 = evec_a.minor(0..2, 1..1)
179
+ v2 = evec_a.minor(0..2, 2..2)
180
+
181
+ # Norm of vectors
182
+ assert_in_delta(1.0, (v0.t * v0)[0,0], 1e-3)
183
+ assert_in_delta(1.0, (v1.t * v1)[0,0], 1e-3)
184
+ assert_in_delta(1.0, (v2.t * v2)[0,0], 1e-3)
185
+
186
+ # Orthogonality of basis vectors
187
+ assert_in_delta(0.0, (v0.t * v1)[0,0], 1e-3)
188
+ assert_in_delta(0.0, (v0.t * v2)[0,0], 1e-3)
189
+ assert_in_delta(0.0, (v1.t * v2)[0,0], 1e-3)
190
+
191
+ # Orientation of basis vectors (up to 180° ambiguity)
192
+ assert_in_delta(1.0, (r0 * v0)[0,0].abs, 1e-3)
193
+ assert_in_delta(1.0, (r1 * v1)[0,0].abs, 1e-3)
194
+ assert_in_delta(1.0, (r2 * v2)[0,0].abs, 1e-3)
195
+ end
196
+ end
197
+
198
+ end
199
+ end
@@ -0,0 +1,82 @@
1
+ #
2
+ # RMDS - Ruby Multidimensional Scaling Library
3
+ # Copyright (c) Christoph Heindl, 2010
4
+ # http://github.com/cheind/rmds
5
+ #
6
+
7
+ require 'mds/test/matrix_assertions.rb'
8
+
9
+ module MDS
10
+ module Test
11
+
12
+ #
13
+ # Module containing standarized tests for MDS::Metric.
14
+ #
15
+ module BundleMetric
16
+ include MDS::Test::MatrixAssertions
17
+
18
+ def test_squared_distances
19
+ input = MDS::Matrix.create_rows(
20
+ [1.0, 2.0],
21
+ [4.0, 3.0],
22
+ [0.0, 1.0]
23
+ )
24
+
25
+ output = MDS::Matrix.create_rows(
26
+ [0.0, 10.0, 2.0],
27
+ [10.0, 0.0, 20.0],
28
+ [2.0, 20.0, 0.0]
29
+ )
30
+
31
+ d = MDS::Metric.squared_distances(input)
32
+ assert_equal_matrices(output, d)
33
+ end
34
+
35
+ def test_k_2d
36
+ x = MDS::Matrix.create_rows(
37
+ [1.0, 2.0],
38
+ [4.0, 3.0],
39
+ [0.0, 1.0]
40
+ )
41
+ d = MDS::Metric::squared_distances(x)
42
+ proj = MDS::Metric.projectk(d, 0.99)
43
+ dd = MDS::Metric.squared_distances(proj)
44
+ assert_delta_matrices(d, dd, 1e-5)
45
+ end
46
+
47
+ def test_d_2d
48
+ x = MDS::Matrix.create_rows(
49
+ [1.0, 2.0],
50
+ [4.0, 3.0],
51
+ [0.0, 1.0]
52
+ )
53
+ d = MDS::Metric.squared_distances(x)
54
+
55
+ proj = MDS::Metric.projectd(d, 2)
56
+ dd = MDS::Metric.squared_distances(proj)
57
+ assert_delta_matrices(d, dd, 1e-5)
58
+ end
59
+
60
+ def test_k_5d
61
+ x = MDS::Matrix.create_random(10, 5, -10, 10)
62
+ d = MDS::Metric.squared_distances(x)
63
+
64
+ proj = MDS::Metric.projectk(d, 1.0)
65
+ dd = MDS::Metric.squared_distances(proj)
66
+ assert_delta_matrices(d, dd, 1e-5)
67
+
68
+ proj = MDS::Metric.projectk(d, 0.8)
69
+ dd = MDS::Metric.squared_distances(proj)
70
+ assert_delta_matrices(d, dd, 5e2)
71
+ end
72
+
73
+ def test_d_5d
74
+ x = MDS::Matrix.create_random(10, 5, -10, 10)
75
+ d = MDS::Metric.squared_distances(x)
76
+ proj = MDS::Metric.projectd(d, 10)
77
+ dd = MDS::Metric.squared_distances(proj)
78
+ assert_delta_matrices(d, dd, 1e-5)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,54 @@
1
+ #
2
+ # RMDS - Ruby Multidimensional Scaling Library
3
+ # Copyright (c) Christoph Heindl, 2010
4
+ # http://github.com/cheind/rmds
5
+ #
6
+
7
+ module MDS
8
+ module Test
9
+
10
+ #
11
+ # Addition assertions for comparing matrices.
12
+ #
13
+ module MatrixAssertions
14
+
15
+ #
16
+ # Assert that two matrices are equal
17
+ #
18
+ # @param [MDS::Matrix] a first matrix
19
+ # @param [MDS::Matrix] b second matrix
20
+ #
21
+ def assert_equal_matrices(a, b)
22
+ assert_instance_of(a.matrix.class, b.matrix)
23
+ assert(a.nrows, b.nrows)
24
+ assert(a.ncols, b.ncols)
25
+
26
+ for i in 0..a.nrows-1 do
27
+ for j in 0..a.ncols-1 do
28
+ assert_equal(a[i,j], b[i,j])
29
+ end
30
+ end
31
+ end
32
+
33
+ #
34
+ # Assert that two matrices are equal up to delta
35
+ #
36
+ # @param [MDS::Matrix] first matrix
37
+ # @param [MDS::Matrix] second matrix
38
+ # @param [Float] delta delta
39
+ #
40
+ def assert_delta_matrices(a, b, delta = 1e-10)
41
+ assert_instance_of(a.matrix.class, b.matrix)
42
+ assert(a.nrows, b.nrows)
43
+ assert(a.ncols, b.ncols)
44
+
45
+ for i in 0..a.nrows-1 do
46
+ for j in 0..a.ncols-1 do
47
+ assert_in_delta(a[i,j], b[i,j], delta)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ end