// Authors: Unknown. Please, if you are the author of this file, or if you // know who are the authors of this file, let us know, so we can give the // adequate credits and/or get the adequate authorizations. #include "numerics.h" #include #include #include #include namespace libNumerics { const flnum MinLM::DEFAULT_RELATIVE_TOL = 1E-3; const flnum MinLM::DEFAULT_LAMBDA_INIT = 1E-3; const flnum MinLM::DEFAULT_LAMBDA_FACT = 10.0; const flnum MinLM::EPSILON_KERNEL = 1E-9; inline flnum ABS(flnum x) { return (x >= 0)? x: -x; } /// Resolution by LU decomposition with pivot. bool solveLU(const matrix& A, const vector& B, vector& X) { X = B; return solveLU(A, X); } /// Replace X by A^{-1}X, by LU solver. bool solveLU(matrix A, vector& X) { assert(A.nrow() == A.ncol()); int n = A.nrow(); vector rowscale(n); // Implicit scaling of each row std::vector permut(n,0); // Permutation of rows // Get the implicit scaling information of each row for(int i=0; i< n; i++) { flnum max = 0.0; for(int j=0; j< n; j++) { flnum tmp = ABS(A(i,j)); if (tmp> max) max = tmp; } if(max == 0.0) return false; rowscale(i) = 1.0/max; } // Perform the decomposition for(int k=0; k < n; k++) { // Search for largest pivot element flnum max = rowscale(k)*ABS(A(k,k)); int imax = k; for(int i=k+1; i < n; i++) { flnum tmp = rowscale(i)*ABS(A(i,k)); if(tmp > max) { max = tmp; imax = i; } } if(max == 0.0) return false; // Interchange rows if needed if(k != imax) { A.swapRows(k, imax); rowscale(imax) = rowscale(k); // Scale of row k no longer needed } permut[k] = imax; // permut(k) was not initialized before flnum Akk = 1/A(k,k); for(int i=k+1; i < n; i++) { flnum tmp = A(i,k) *= Akk; // Divide by pivot for (int j=k+1;j < n; j++) // Reduce the row A(i,j) -= tmp*A(k,j); } } // Forward substitution for (int k=0; k < n; k++) { flnum sum = X(permut[k]); X(permut[k]) = X(k); for(int j = 0; j < k; j++) sum -= A(k,j)*X(j); X(k) = sum; } // Backward substitution for(int k=n-1; k >= 0; k--) { flnum sum = X(k); for(int j=k+1; j < n; j++) sum -= A(k,j)*X(j); X(k) = sum/A(k,k); } return true; } /// Decompose A into U diag(W) V^T with U(m,n) and V(n,n) having orthonormal /// vectors. SVD::SVD(const matrix& A) : m_U(A), m_V(A.ncol(),A.ncol()), m_W(A.ncol()) { compute(); sort(); } /// SVD computation. Initial matrix stored in m_U as input. void SVD::compute() { const flnum EPSILON = std::numeric_limits::epsilon(); const int SVD_MAX_ITS = 30; int rows = m_U.nrow(); int cols = m_U.ncol(); flnum g, scale, anorm; vector RV1(cols); // Householder reduction to bidiagonal form: anorm = g = scale = 0.0; for (int i=0; i< cols; i++) { int l = i + 1; RV1(i) = scale*g; g = scale = 0.0; if(i< rows) { for (int k=i; k< rows; k++) scale += ABS(m_U(k,i)); if (scale != 0.0) { flnum invScale=1.0/scale, s=0.0; for (int k=i; k< rows; k++) { m_U(k,i) *= invScale; s += m_U(k,i) * m_U(k,i); } flnum f = m_U(i,i); g = - withSignOf(std::sqrt(s),f); flnum h = 1.0 / (f*g - s); m_U(i,i) = f - g; for (int j=l; j< cols; j++) { s = 0.0; for (int k=i; k< rows; k++) s += m_U(k,i) * m_U(k,j); f = s * h; for (int k=i; k< rows; k++) m_U(k,j) += f * m_U(k,i); } for (int k=i; k< rows; k++) m_U(k,i) *= scale; } } m_W(i) = scale * g; g = scale = 0.0; if ( i< rows && i< cols-1 ) { for (int k=l; k< cols; k++) scale += ABS(m_U(i,k)); if (scale != 0.0) { flnum invScale=1.0/scale, s=0.0; for (int k=l; k< cols; k++) { m_U(i,k) *= invScale; s += m_U(i,k) * m_U(i,k); } flnum f = m_U(i,l); g = - withSignOf(std::sqrt(s),f); flnum h = 1.0 / (f*g - s); m_U(i,l) = f - g; for (int k=l; k< cols; k++) RV1(k) = m_U(i,k) * h; for (int j=l; j< rows; j++) { s = 0.0; for (int k=l; k< cols; k++) s += m_U(j,k) * m_U(i,k); for (int k=l; k< cols; k++) m_U(j,k) += s * RV1(k); } for (int k=l; k< cols; k++) m_U(i,k) *= scale; } } anorm = std::max(anorm, ABS(m_W(i)) + ABS(RV1(i)) ); } // Accumulation of right-hand transformations: m_V(cols-1,cols-1) = 1.0; for (int i= cols-2; i>=0; i--) { m_V(i,i) = 1.0; int l = i+1; g = RV1(l); if (g != 0.0) { flnum invgUil = 1.0 / (m_U(i,l)*g); for (int j=l; j< cols; j++) m_V(j,i) = m_U(i,j) * invgUil; for (int j=l; j< cols; j++){ flnum s = 0.0; for (int k=l; k< cols; k++) s += m_U(i,k) * m_V(k,j); for (int k=l; k< cols; k++) m_V(k,j) += s * m_V(k,i); } } for (int j=l; j< cols; j++) m_V(i,j) = m_V(j,i) = 0.0; } // Accumulation of left-hand transformations: for (int i=std::min(rows,cols)-1; i>=0; i--) { int l = i+1; g = m_W(i); for (int j=l; j< cols; j++) m_U(i,j) = 0.0; if (g != 0.0) { g = 1.0 / g; flnum invUii = 1.0 / m_U(i,i); for (int j=l; j< cols; j++) { flnum s = 0.0; for (int k=l; k< rows; k++) s += m_U(k,i) * m_U(k,j); flnum f = (s * invUii) * g; for (int k=i; k< rows; k++) m_U(k,j) += f * m_U(k,i); } for (int j=i; j< rows; j++) m_U(j,i) *= g; } else for (int j=i; j< rows; j++) m_U(j,i) = 0.0; m_U(i,i) = m_U(i,i) + 1.0; } // Diagonalization of the bidiagonal form: for (int k=cols-1; k>=0; k--) { // Loop over singular values for (int its=1; its<=SVD_MAX_ITS; its++) { bool flag = false; int l = k; int nm = k-1; while(l>0 && ABS(RV1(l)) > EPSILON*anorm) { // Test for splitting if(ABS(m_W(nm)) <= EPSILON*anorm) { flag = true; break; } l--; nm--; } if (flag) { // Cancellation of RV1(l), if l > 0 flnum c=0.0, s=1.0; for (int i=l; i< k+1; i++) { flnum f = s * RV1(i); RV1(i) = c * RV1(i); if (ABS(f)<=EPSILON*anorm) break; g = m_W(i); flnum h = SVD::hypot(f,g); m_W(i) = h; h = 1.0 / h; c = g * h; s = - f * h; for (int j=0; j< rows; j++) rotate(m_U(j,nm),m_U(j,i), c,s); } } flnum z = m_W(k); if (l==k) { // Convergence of the singular value if (z< 0.0) { // Singular value is made nonnegative m_W(k) = -z; for (int j=0; j< cols; j++) m_V(j,k) = - m_V(j,k); } break; } // Exception if convergence to the singular value not reached: if(its==SVD_MAX_ITS) throw SvdConvergenceError(); flnum x = m_W(l); // Get QR shift value from bottom 2x2 minor nm = k-1; flnum y = m_W(nm); g = RV1(nm); flnum h = RV1(k); flnum f = ( (y-z)*(y+z) + (g-h)*(g+h) ) / ( 2.0*h*y ); g = SVD::hypot(f,1.0); f = ( (x-z)*(x+z) + h*(y/(f+withSignOf(g,f)) - h) ) / x; // Next QR transformation (through Givens reflections) flnum c=1.0, s=1.0; for (int j=l; j<=nm; j++) { int i = j+1; g = RV1(i); y = m_W(i); h = s * g; g = c * g; z = SVD::hypot(f,h); RV1(j) = z; z = 1.0 / z; c = f * z; s = h * z; f = x*c + g*s; g = g*c - x*s; h = y * s; y *= c; for(int jj=0; jj < cols; jj++) rotate(m_V(jj,j),m_V(jj,i), c,s); z = SVD::hypot(f,h); m_W(j) = z; if (z!=0.0) { // Rotation can be arbitrary if z = 0.0 z = 1.0 / z; c = f * z; s = h * z; } f = c*g + s*y; x = c*y - s*g; for(int jj=0; jj < rows; jj++) rotate(m_U(jj,j),m_U(jj,i), c,s); } RV1(l) = 0.0; RV1(k) = f; m_W(k) = x; } } } /// Recompose from SVD. This should be the initial matrix. matrix SVD::compose() const { return m_U * m_W.diag() * m_V.t(); } flnum SVD::withSignOf(flnum a, flnum b) { return b >= 0 ? (a >= 0 ? a : -a) : (a >= 0 ? -a : a); } /// Replace hypot of math.h by robust numeric implementation. flnum SVD::hypot(flnum a, flnum b) { a = ABS(a); b = ABS(b); if(a > b) { b /= a; return a*std::sqrt(1.0 + b*b); } else if(b) { a /= b; return b*std::sqrt(1.0 + a*a); } return 0.0; } /// Utility function used while computing SVD. void SVD::rotate(flnum& a, flnum& b, flnum c, flnum s) { flnum d = a; a = +d*c +b*s; b = -d*s +b*c; } class SVDElement { public: SVDElement(const vector& W, int i) : m_val(W(i)), m_i(i) {} bool operator<(const SVDElement& e) const { return (m_val>e.m_val); } flnum m_val; int m_i; }; /// Sort SVD by decreasing order of singular value. void SVD::sort() { std::vector vec; for(int i=0; i < m_U.ncol(); i++) vec.push_back( SVDElement(m_W, i) ); std::sort(vec.begin(), vec.end()); // Apply permutation for(int i=m_U.ncol()-1; i >=0; i--) if(vec[i].m_i != i) { // Find cycle of i const vector colU = m_U.col(i); const vector colV = m_V.col(i); const flnum w = m_W(i); int j = i; while(vec[j].m_i != i) { m_U.paste(0,j, m_U.col(vec[j].m_i)); m_V.paste(0,j, m_V.col(vec[j].m_i)); m_W(j) = m_W(vec[j].m_i); std::swap(j,vec[j].m_i); } vec[j].m_i = j; m_U.paste(0,j, colU); m_V.paste(0,j, colV); m_W(j) = w; } } /// Constructor. MinLM::MinLM() : iterations(0), relativeTol(DEFAULT_RELATIVE_TOL), lambdaInit(DEFAULT_LAMBDA_INIT), lambdaFact(DEFAULT_LAMBDA_FACT) {} /// In equation JtJ X = B, remove columns of J close to 0, so that JtJ can be /// invertible void MinLM::compress(matrix& JtJ, vector& B) { flnum max=0; for(int i=0; i < JtJ.nrow(); i++) if(JtJ(i,i) > max) max = JtJ(i,i); max *= EPSILON_KERNEL; m_nullCols.clear(); for(int i=0; i < JtJ.nrow(); i++) if(JtJ(i,i) <= max) m_nullCols.push_back(i); if( m_nullCols.empty() ) return; int n=(int)m_nullCols.size(); matrix JtJ2(JtJ.nrow()-m_nullCols.size(), JtJ.ncol()-m_nullCols.size()); vector B2(B.nrow()-(int)m_nullCols.size()); for(int i=0,i2=0; i < JtJ.nrow(); i++) { if(i-i2 < n && m_nullCols[i-i2]==i) continue; for(int j=0,j2=0; j < JtJ.ncol(); j++) { if(j-j2 < n && m_nullCols[j-j2]==j) continue; JtJ2(i2,j2) = JtJ(i,j); j2++; } B2(i2) = B(i); i2++; } swap(JtJ,JtJ2); swap(B,B2); } /// Insert 0 in rows of B that were removed by \c compress() void MinLM::uncompress(vector& B) { if(m_nullCols.empty()) return; int n=(int)m_nullCols.size(); vector B2(B.nrow()+(int)m_nullCols.size()); for(int i=0,i2=0; i2 < B2.nrow(); i2++) if(i2-i < n && m_nullCols[i2-i]==i2) B2(i2)=0; else B2(i2) = B(i++); swap(B,B2); } /// Perform minimization. /// \a targetRMSE is the root mean square error aimed at. /// Return the reached RMSE. Since the class does not know the dimension, the /// real RMSE should be this value multiplied by sqrt(dim). For example, for 2-D /// points this would be sqrt(2) times the returned value. flnum MinLM::minimize(vector& P, const vector& yData, flnum targetRMSE, int maxIters) { flnum errorMax = targetRMSE*targetRMSE*yData.nrow(); vector yModel( yData.nrow() ); modelData(P, yModel); vector E( yData-yModel ); flnum error = E.qnorm(); matrix J( yData.nrow(), P.nrow() ); modelJacobian(P, J); matrix Jt = J.t(); matrix JtJ = Jt*J; vector B = Jt*E; compress(JtJ, B); flnum lambda = lambdaInit; for(iterations=0; iterations < maxIters && error > errorMax; iterations++) { matrix H(JtJ); for(int i = 0; i < H.nrow(); i++) H(i,i) *= 1+lambda; vector dP( P.nrow() ); solveLU(H, B, dP); uncompress(dP); vector tryP = P + dP; modelData(tryP, yModel); E = yData - yModel; flnum tryError = E.qnorm(); if(ABS(tryError-error) <= relativeTol*error) break; if(tryError > error) lambda *= lambdaFact; else { lambda /= lambdaFact; error = tryError; P = tryP; modelJacobian(P, J); Jt = J.t(); JtJ = Jt*J; B = Jt*E; compress(JtJ, B); } } return sqrt(error/yData.nrow()); } } // namespace libNumerics