I just realized I can use the Jacobian calculation
from Stan's auto-dif to check my work. I'll add that
test later tonight.
On 4/24/12 3:22 PM, Ben Goodrich wrote:
> Also, it might be good to mention in the documentation that a precision
> matrix can be composed in exactly the same way.
I agree. Should I change the data type form
cov_matrix to pos_def_matrix? I wasn't even sure
these were the same concept when I added the first
time (though we're really looking at only symmetric
pos def matrices).
Also, we can mention that users can rescale
a correlation matrix. I'll eventually replace
that transform with the hyperspherical transform
if it looks like it'll be more efficient.
> This is better
> computationally because it avoids the need to do any matrix inversions
> for most models involving elliptical distributions. But, at the moment,
> users would have to do everything themselves in the .stan file because
> the C++ code does not have the necessary parameterizations for this.
So maybe it can wait.
Or feel free to add the parameterizations.
I think the dust has settled enough around the
transforms and error checking that it's safe
to add again.
- Bob
> Also, it might be good to mention in the documentation that a precision> matrix can be composed in exactly the same way.
I agree. Should I change the data type form
cov_matrix to pos_def_matrix? I wasn't even sure
these were the same concept when I added the first
time (though we're really looking at only symmetric
pos def matrices).
Also, we can mention that users can rescale
a correlation matrix. I'll eventually replace
that transform with the hyperspherical transform
if it looks like it'll be more efficient.
By the way, Matt found this book:
http://cs.nenu.edu.cn/Lunwen/2010050513555973499.pdf
on matrix differential calc -- it has all sorts of
great material on Jacobians and derivatives of
matrix operations.
> We could have
> sym_pos_def_matrix but that is a bit long-winded. I'd be happy to stick
> with cov_matrix in the .stan file if we had a cov_matrix class in the
> C++ file that could represent a covariance matrix in a myriad of
> different ways. Then we could parameterize it via the Cholesky factor of
> the precision matrix without the user really knowing.
I agree that having something like a covariance matrix
to cache factorizations makes sense. I'm still not sure
how to do it, though in terms of when things like Cholesky
factors get computed.
If we allow general A[m,n] = x;
assignments, then we can't guarantee positive definiteness.
> I worked out that the LKJ and the onion are particular hyperspherical
> transforms.
Interesting. I have to go into Eigen and figure out
what all the pull() and other ops you used were so I
can reverse engineer the transform!
> It is just that they are the only hyperspherical transforms
> that imply a known joint density for the correlation matrix. You could
> use another density on the hypersphere(s) and get some correlation
> matrix, but its joint density would just be (proportional to) the
> product of the (independent) hypersphere densities times a Jacobian
> determinant.
> Hard to see what would be the use of implementing that in
> stan until someone does it in such a way that the joint density becomes
> analytically attractive.
I'm just thinking about supporting the corr_matrix data type
most efficiently in terms of inverse transforms and their
Jacobians. The transform only gets applied for initial values.
All that pull stuff in transform.hpp basically just reflects that fact that we get "K choose 2" CPCs in an Array and need to stuff them into the lower triangle of a Cholesky factor. As in,
L =
1
a x
b e x
c f h x
d g i j x
where x is the square root of the sum-of-squares in its row. So, first we pull a:d from the CPCs, and put them in L. Then remark the spot in CPCs that we pull from, pull e:g, and put them in L, etc. And calculate the diagonal values along the way.
> eigen. What sort of A[m,n] = x assignments are you contemplating besides
> x being a block of zeros?
Nothing in particular. I'm just imagining someone
doing something like rescaling one entry at a time.
Or just having the covariance matrix be on the left-hand
side of an assignment.
This is all related to whether we only allow operations
that maintain positive definiteness.
Am I right in thinking we could also use a basis of the
form:
L =
0
a 0
b e 0
c f h 0
d g i j 0
where each of the row vectors is of length 1 (i.e., on
the hypersphere)? You still get 1s on the diagonal, a positive
definite result, and all values constrained between -1 and 1.
I'm just not sure it's an onto mapping given the way you did it.
A more efficient way to do this for stan::agrad::var
(as things stand now) would be to use our own dot_product
ops to produce the result so the gradients are more efficient.
I'm going to roll that into the covariance matrix case
now that it's unit tested.
> We already have one function for transforming initial values in
> transform.hpp called factor_cov_matrix(). It could be based on that.
Isn't that what's already going on now?
You can build a Cholesky factor up from scratch using
the built in positive-constraining inverse transform,
but it's very clunky.
I'd rather not call it "hacks", but I take your point.
When you say "limitations", you mean things like
not having vectorized forms of the distributions, using
covariance matrices, etc.?
x_1 y_1 y_2 y_3
1 0.1872073 1.3203964 -1.3392774 NA
2 0.5026465 -0.1830042 0.6394578 -0.07292972
3 0.7816282 NA -1.7383923 -0.62743554
4 -1.8949041 -0.1960061 0.2543012 0.18612357
5 0.8035139 1.7151699 NA -0.10530234
6 0.5682540 NA NA -0.94662401
I would not recommend choosing fixed values and
treating them as "unknown". I'm going to modify
the BUGS models that do that to more efficient
forms.
The x + x_mirror thing might work. It still
looks about as clunky as the alternatives.
mu <- dot_product(x, beta) + dot_product(x_mirror, beta);dot_product() is no worse than what we have to do anyway. That is still clunky, but it is the only thing that comes to mind for an arbitrary missingness pattern in the data.Instead, I suggested something like this in
the doc section I wrote yesterday on missing data:
data {
int N_obs;
int N_miss;
real y_obs[N_obs];
}
parameters {
real mu;
real(0,) sigma;
real y_miss[N_miss];
}
model {
for (n in 1:N_obs)
y_obs[n] ~ normal(mu,sigma);
for (n in 1:N_miss)
y_miss[n] ~ normal(mu,sigma);
}
Now if both y_obs and y_miss were sparse, they could
both have the same shape and have alternating missingness,
but there's not much point.
The less efficient alternative is to pack both into a
transformed parameter, as in this example, which computes
the same as the above,
data {
int K;
int N_obs;
int N_miss;
real y_obs[N_obs];
}
transformed data {
int N;
N <- N_obs + N_miss;
}
parameters {
real beta[K];
real(0,) sigma;
real y_miss[N_miss];
}
transformed parameters {
real y[N];
for (n in 1:N_obs)
y[n] <- y_obs[n];
for (n in 1:N_miss)
y[n + N_obs] <- y_miss[n];
}
model {
for (n in 1:N)
y[n] ~ normal(mu,sigma);
}
Sometimes you need to do that, for instance if y is
a multivariate parameter to another distribution and
you need it whole.
The second alternative is less efficient because
y[n] <- y_obs[n]
has to promote a double-based y_obs[n] to
a stan::agrad::var for y[n].
data {
int<lower=1> N;
int<lower=1> K;
matrix[N,K,has_missingness] X;
real y[N];
}
parameters {
matrix[N,K,!X] X_mirror;
real beta[K];
real<lower=0> sigma;
// other parameters
}
model {
for (i in 1:N) {
for (j in 1:K) {
if(X[i,j] == 0) {
// prior on X_mirror[i,j], which is often some model involving other parameters
}
}
}
// prior on beta and sigma and other parameters
y ~ normal(X * beta + X_mirror * beta, sigma);
}
Right. The main point of using a sparse vector x (with missing data in x "coded" as zero) and another sparse vector x_mirror of parameters (with zeros in the places that x is observed) is that the zeros aren't allocated / accessed / promoted.
The really big change for the sparsity solution with mirrors would
be handling NA somehow (that's a very fundamental change) and
a whole bunch of I/O and type inference if the mirror solution can't
be reduced to a regular matrix op, which it can't be efficiently.
col(X_mirror,j) ~ normal(mu, sigma);
Now that Eigen has essentially full support for sparse matrices, can we revisit this idea for dealing with an arbitrary missingness pattern in the data? By arbitrary missingness pattern, I mean a datamatrix that looks something like this:
x_1 y_1 y_2 y_3
1 0.1872073 1.3203964 -1.3392774 NA
2 0.5026465 -0.1830042 0.6394578 -0.07292972
3 0.7816282 NA -1.7383923 -0.62743554
4 -1.8949041 -0.1960061 0.2543012 0.18612357
5 0.8035139 1.7151699 NA -0.10530234
6 0.5682540 NA NA -0.94662401
Stan v1.0.2 can currently proceed iff the data can easily be sorted into blocks of fully observed and fully missing data so that the model{} section can separately loop over each block. The frustrating thing about missingness among several variables is that sometimes no such sorting is possible, and even in special cases where it is possible, it is not particularly easy for a user to do. For example, you could pull rows 2 and 4 into a separate matrix, but what do you do with rows 1, 3, 5, and 6, which have both observed and NA cells?