/* inference_vb.cc - VB inference technique class declarations Adrian Groves and Michael Chappell, FMRIB Image Analysis Group Copyright (C) 2007 University of Oxford */ /* Part of FSL - FMRIB's Software Library http://www.fmrib.ox.ac.uk/fsl fsl@fmrib.ox.ac.uk Developed at FMRIB (Oxford Centre for Functional Magnetic Resonance Imaging of the Brain), Department of Clinical Neurology, Oxford University, Oxford, UK LICENCE FMRIB Software Library, Release 5.0 (c) 2012, The University of Oxford (the "Software") The Software remains the property of the University of Oxford ("the University"). The Software is distributed "AS IS" under this Licence solely for non-commercial use in the hope that it will be useful, but in order that the University as a charitable foundation protects its assets for the benefit of its educational and research purposes, the University makes clear that no condition is made or to be implied, nor is any warranty given or to be implied, as to the accuracy of the Software, or that it will be suitable for any particular purpose or for use under any specific conditions. Furthermore, the University disclaims all responsibility for the use which is made of the Software. It further disclaims any liability for the outcomes arising from using the Software. The Licensee agrees to indemnify the University and hold the University harmless from and against any and all claims, damages and liabilities asserted by third parties (including claims for negligence) which arise directly or indirectly from the use of the Software or the sale of any products based on the Software. No part of the Software may be reproduced, modified, transmitted or transferred in any form or by any means, electronic or mechanical, without the express permission of the University. The permission of the University is not required if the said reproduction, modification, transmission or transference is done without financial return, the conditions of this Licence are imposed upon the receiver of the product, and all original and amended source code is included in any transmitted product. You may be held legally responsible for any copyright infringement that is caused or encouraged by your failure to abide by these terms and conditions. You are not permitted under this Licence to use this Software commercially. Use for which any financial return is received shall be defined as commercial use, and includes (1) integration of all or part of the source code or the Software into a product for sale or license by or on behalf of Licensee to third parties or (2) use of the Software or any derivative of it for research with the final aim of developing software products for sale or license to a third party or (3) use of the Software or any derivative of it for research with the final aim of developing non-software products for sale or license to a third party, or (4) use of the Software to provide any service to an external organisation for which payment is received. If you are interested in using the Software commercially, please contact Isis Innovation Limited ("Isis"), the technology transfer company of the University, to negotiate a licence. Contact details are: innovation@isis.ox.ac.uk quoting reference DE/9564. */ #include "inference_vb.h" #include "convergence.h" #ifndef __FABBER_LIBRARYONLY using namespace NEWIMAGE; #endif void VariationalBayesInferenceTechnique::Setup(ArgsType& args) { Tracer_Plus tr("VariationalBayesInferenceTechnique::Setup"); // Call ancestor, which does most of the real work InferenceTechnique::Setup(args); // Load up initial prior and initial posterior MVNDist* loadPrior = new MVNDist( model->NumParams() ); MVNDist* loadPosterior = new MVNDist( model->NumParams() ); NoiseParams* loadNoisePrior = noise->NewParams(); NoiseParams* loadNoisePosterior = noise->NewParams(); string filePrior = args.ReadWithDefault("fwd-initial-prior", "modeldefault"); string filePosterior = args.ReadWithDefault("fwd-initial-posterior", "modeldefault"); if (filePrior == "modeldefault" || filePosterior == "modeldefault") model->HardcodedInitialDists(*loadPrior, *loadPosterior); if (filePrior != "modeldefault") loadPrior->Load(filePrior); if (filePosterior != "modeldefault") loadPosterior->Load(filePosterior); if ( (loadPosterior->GetSize() != model->NumParams()) || (loadPrior->GetSize() != model->NumParams()) ) throw Invalid_option("Size mismatch: model wants " + stringify(model->NumParams()) + ", initial prior (" + filePrior + ") is " + stringify(loadPrior->GetSize()) + ", initial posterior (" + filePosterior + ") is " + stringify(loadPosterior->GetSize()) + "\n"); filePrior = args.ReadWithDefault("noise-initial-prior", "modeldefault"); filePosterior = args.ReadWithDefault("noise-initial-posterior", "modeldefault"); if (filePrior == "modeldefault" || filePosterior == "modeldefault") noise->HardcodedInitialDists(*loadNoisePrior, *loadNoisePosterior); if (filePrior != "modeldefault") loadNoisePrior->InputFromMVN( MVNDist(filePrior) ); if (filePosterior != "modeldefault") loadNoisePosterior->InputFromMVN( MVNDist(filePosterior) ); // Make these distributions constant: assert(initialFwdPrior == NULL); assert(initialFwdPosterior == NULL); assert(initialNoisePrior == NULL); assert(initialNoisePosterior == NULL); initialFwdPrior = loadPrior; initialFwdPosterior = loadPosterior; initialNoisePrior = loadNoisePrior; initialNoisePosterior = loadNoisePosterior; loadPrior = loadPosterior = NULL; loadNoisePrior = loadNoisePosterior = NULL; // now, only accessible as consts. // Resume from a previous run? continueFromFile = args.ReadWithDefault("continue-from-mvn", ""); paramFilename = args.ReadWithDefault("continue-from-params",""); // optional list of parameters in MVN if (continueFromFile != "") { // Won't need these any more. They don't hurt, but why leave them around? // They can only cause trouble (if used by mistake). delete initialFwdPosterior; initialFwdPosterior = NULL; if ( !args.ReadBool("continue-fwd-only") ) { delete initialNoisePosterior; initialNoisePosterior = NULL; } } // Specify prior types and sources // Section in which spatial priors can be specified by parameter name on command line // param_spatial_priors_bymane (PSP_byname) // this overwrites any existing entries in the string vector modnames; //names of model parameters imagepriorstr.resize(model->NumParams()); model->NameParams(modnames); int npspnames=0; while (true) { npspnames++; string bytypeidx = args.ReadWithDefault("PSP_byname"+stringify(npspnames),"stop!"); if (bytypeidx == "stop!") break; //no more spriors have been specified // deal with the specification of this sprior (by name) //compare name to those in list of model names bool found=false; for (int p=0; pNumParams(); p++) { if (bytypeidx == modnames[p]) { found = true; char pspstype = convertTo(args.Read("PSP_byname"+stringify(npspnames)+"_type")); PriorsTypes[p]=pspstype; PSPidx.push_back(p); //record the index at which a PSP has been defined for use in spatialvb setup LOG_ERR("PSP_byname parameter " << bytypeidx << " at entry " << p << ", type: " << pspstype << endl); // now read in file name for an image prior (if appropriate) if (pspstype == 'I') { imagepriorstr[p] = args.Read("PSP_byname"+stringify(npspnames)+"_image"); } } } if (!found) { throw Invalid_option("ERROR: Prior specification by name, parameter " + bytypeidx + " does not exist in the model\n"); } } // Fix the linearization centres? lockedLinearFile = args.ReadWithDefault("locked-linear-from-mvn",""); // Maximum iterations allowed: string maxIterations = args.ReadWithDefault("max-iterations","10"); if (maxIterations.find_first_not_of("0123456789") != string::npos) throw Invalid_option("--convergence=its=?? parameter must be a positive number"); int its = atol(maxIterations.c_str()); if (its<=0) throw Invalid_option("--convergence=its=?? paramter must be positive"); // Figure out convergence-testing method: string convergence = args.ReadWithDefault("convergence", "maxits"); if (convergence == "maxits") conv = new CountingConvergenceDetector(its); else if (convergence == "pointzeroone") conv = new FchangeConvergenceDetector(its, 0.01); else if (convergence == "freduce") conv = new FreduceConvergenceDetector(its, 0.01); else if (convergence == "trialmode") { int maxtrials = convertTo(args.ReadWithDefault("max-trials","10")); conv = new TrialModeConvergenceDetector(its, maxtrials, 0.01); } else if (convergence == "lm") conv = new LMConvergenceDetector(its,0.01); else throw Invalid_option("Unrecognized convergence detector: '" + convergence + "'"); // Figure out if F needs to be calculated every iteration printF = args.ReadBool("print-free-energy"); needF = conv->UseF() || printF; haltOnBadVoxel = !args.ReadBool("allow-bad-voxels"); if (haltOnBadVoxel) LOG << "Note: numerical errors in voxels will cause the program to halt.\n" << "Use --allow-bad-voxels (with caution!) to keep on calculating.\n"; else LOG << "Using --allow-bad-voxels: numerical errors in a voxel will\n" << "simply stop the calculation of that voxel.\n" << "Check log for 'Going on to the next voxel' messages.\n" << "Note that you should get very few (if any) exceptions like this;" << "they are probably due to bugs or a numerically unstable model."; } void VariationalBayesInferenceTechnique::DoCalculations(const DataSet& allData) { Tracer_Plus tr("VariationalBayesInferenceTechnique::DoCalculations"); cout << "here" << endl; // extract data (and the coords) from allData for the (first) VB run const Matrix& origdata = allData.GetVoxelData(); //cerr << "Data MaxAbsValue = " << MaximumAbsoluteValue(data) << endl; const Matrix & coords = allData.GetVoxelCoords(); const Matrix & suppdata = allData.GetVoxelSuppData(); // Rows are volumes // Columns are (time) series // num Rows is size of (time) series // num Cols is size of volumes // pass in some (dummy) data/coords here just in case the model relies upon it // use the first voxel values as our dummies if (suppdata.Ncols() > 0) { model->pass_in_data( origdata.Column(1) , suppdata.Column(1) ); } else { model->pass_in_data( origdata.Column(1) ); } model->pass_in_coords(coords.Column(1)); int Nvoxels = origdata.Ncols(); if (origdata.Nrows() != model->NumOutputs()) throw Invalid_option("Data length (" + stringify(origdata.Nrows()) + ") does not match model's output length (" + stringify(model->NumOutputs()) + ")!"); #ifdef __FABBER_MOTION MCobj mcobj(allData); #endif //__FABBER_MOTION Matrix data(origdata.Nrows(),Nvoxels); data = origdata; Matrix modelpred(model->NumOutputs(),Nvoxels); //use this to store the model predictions in to pass to motion correction routine assert(resultMVNs.empty()); // Only call DoCalculations once resultMVNs.resize(Nvoxels, NULL); assert(resultFs.empty()); resultFs.resize(Nvoxels, 9999); // 9999 is a garbage default value // If we're continuing from previous saved results, load them here: bool continuingFromFile = (continueFromFile != ""); vector continueFromDists; if (continuingFromFile) { InitMVNFromFile(continueFromDists,continueFromFile, allData, paramFilename); //MVNDist::Save(continueFromDists,"temp",allData.GetMask()); // check that the MVN created is right //MVNDist::Load(continueFromDists, continueFromFile, allData.GetMask()); } if (lockedLinearFile != "") throw Invalid_option("The option --locked-linear-from-mvn doesn't work with --method=vb yet, but should be pretty easy to implement.\n"); const int nFwdParams = initialFwdPrior->GetSize(); const int nNoiseParams = initialNoisePrior->OutputAsMVN().GetSize(); // sort out loading for 'I' prior vector ImagePrior(nFwdParams); #ifndef __FABBER_LIBRARYONLY volume4D imagevol; #endif for (int k=1; k<=nFwdParams; k++) { if (PriorsTypes[k-1] == 'I') { LOG_ERR("Reading Image prior ("<0) cout << endl << "Motion correction step " << step << " of " << Nmcstep << endl; // loop over voxels doing VB calculations for (int voxel = 1; voxel <= Nvoxels; voxel++) { ColumnVector y = data.Column(voxel); ColumnVector vcoords = coords.Column(voxel); if (suppdata.Ncols() > 0) { ColumnVector suppy = suppdata.Column(voxel); model->pass_in_data( y , suppy ); } else { model->pass_in_data( y ); } model->pass_in_coords(vcoords); NoiseParams* noiseVox = NULL; if (continuefromprevious) { // noise params come from resultMVN noiseVox = noise->NewParams(); noiseVox->InputFromMVN( resultMVNs.at(voxel-1)->GetSubmatrix(nFwdParams+1, nFwdParams+nNoiseParams) ); } else if (initialNoisePosterior == NULL) // continuing noise params from file { assert(continuingFromFile); assert(continueFromDists.at(voxel-1)->GetSize() == nFwdParams+nNoiseParams); noiseVox = noise->NewParams(); noiseVox->InputFromMVN( continueFromDists.at(voxel-1) ->GetSubmatrix(nFwdParams+1, nFwdParams+nNoiseParams) ); } else { noiseVox = initialNoisePosterior->Clone(); /* if (continuingFromFile) assert(continueFromDists.at(voxel-1)->GetSize() == nFwdParams);*/ } const NoiseParams* noiseVoxPrior = initialNoisePrior; NoiseParams* const noiseVoxSave = noiseVox->Clone(); // give an indication of the progress through the voxels LOG << " Voxel " << voxel << " of " << Nvoxels << endl; if (fmod(voxel,floor(Nvoxels/10))==0) {cout << ". " << flush;} //LOG_ERR(" Voxel " << voxel << " of " << Nvoxels << endl); // << " sumsquares = " << (y.t() * y).AsScalar() << endl; double F = 1234.5678; MVNDist fwdPrior( *initialFwdPrior ); MVNDist fwdPosterior; if (continuefromprevious) { //use result from a previous run within fabber (presumably after motion correction) fwdPosterior = resultMVNs.at(voxel-1)->GetSubmatrix(1, nFwdParams); } if (continuingFromFile) { //use results from a previous run loaded from a file assert(initialFwdPosterior == NULL); fwdPosterior = continueFromDists.at(voxel-1)->GetSubmatrix(1, nFwdParams); } else { assert(initialFwdPosterior != NULL); fwdPosterior = *initialFwdPosterior; // any voxelwise initialisation model->Initialise(fwdPosterior); } MVNDist fwdPosteriorSave(fwdPosterior); MVNDist fwdPriorSave(fwdPrior); LinearizedFwdModel linear( model ); // Setup for ARD (fwdmodel will decide if there is anything to be done) double Fard = 0; model->SetupARD( fwdPosterior, fwdPrior, Fard ); // THIS USES ARD IN THE MODEL AND IS DEPRECEATED Fard = noise->SetupARD( model->ardindices, fwdPosterior, fwdPrior ); // Image priors for (int k=1; k<=nFwdParams; k++) { if (PriorsTypes[k-1] == 'I') { ColumnVector thisimageprior; thisimageprior = ImagePrior[k-1]; fwdPrior.means(k) = thisimageprior(voxel); } } try { linear.ReCentre( fwdPosterior.means ); noise->Precalculate( *noiseVox, *noiseVoxPrior, y ); conv->Reset(); // START the VB updates and run through the relevant iterations (according to the convergence testing) int iteration = 0; //count the iterations do { if ( conv-> NeedRevert() ) //revert to previous solution if the convergence detector calls for it { *noiseVox = *noiseVoxSave; // copy values, not pointers! fwdPosterior = fwdPosteriorSave; fwdPrior = fwdPriorSave; // need to revert prior too (in case ARD is in place) linear.ReCentre( fwdPosterior.means ); } if (needF) { F = noise->CalcFreeEnergy( *noiseVox, *noiseVoxPrior, fwdPosterior, fwdPrior, linear, y); F = F + Fard; } if (printF) LOG << " Fbefore == " << F << endl; // Save old values if called for if ( conv->NeedSave() ) { *noiseVoxSave = *noiseVox; // copy values, not pointers! fwdPosteriorSave = fwdPosterior; fwdPriorSave = fwdPrior; } // Do ARD updates (model will decide if there is anything to do here) if (iteration > 0) { model->UpdateARD( fwdPosterior, fwdPrior, Fard ); // THIS USES ARD IN THE MODEL AND IS DEPRECEATED Fard = noise->UpdateARD( model->ardindices, fwdPosterior, fwdPrior ); } // Theta update noise->UpdateTheta( *noiseVox, fwdPosterior, fwdPrior, linear, y, NULL, conv->LMalpha() ); if (needF) { F = noise->CalcFreeEnergy( *noiseVox, *noiseVoxPrior, fwdPosterior, fwdPrior, linear, y); F = F + Fard; } if (printF) LOG << " Ftheta == " << F << endl; // Alpha & Phi updates noise->UpdateNoise( *noiseVox, *noiseVoxPrior, fwdPosterior, linear, y ); if (needF) { F = noise->CalcFreeEnergy( *noiseVox, *noiseVoxPrior, fwdPosterior, fwdPrior, linear, y); F = F + Fard; } if (printF) LOG << " Fphi == " << F << endl; // Test of NoiseModel cloning: // NoiseModel* tmp = noise; noise = tmp->Clone(); delete tmp; // Linearization update // Update the linear model before doing Free eneergy calculation (and ready for next round of theta and phi updates) linear.ReCentre( fwdPosterior.means ); if (needF) { F = noise->CalcFreeEnergy( *noiseVox, *noiseVoxPrior, fwdPosterior, fwdPrior, linear, y); F = F + Fard; } if (printF) LOG << " Fnoise == " << F << endl; iteration++; } while ( !conv->Test( F ) ); // END of VB updates // Revert to old values at last stage if required if ( conv-> NeedRevert() ) { *noiseVox = *noiseVoxSave; // copy values, not pointers! fwdPosterior = fwdPosteriorSave; fwdPrior = fwdPriorSave; linear.ReCentre( fwdPosterior.means ); //just in case we go on to use this in motion correction } conv->DumpTo(LOG, " "); } catch (const overflow_error& e) { LOG_ERR(" Went infinite! Reason:" << endl << " " << e.what() << endl); //todo: write garbage or best guess to memory/file if (haltOnBadVoxel) throw; LOG_ERR(" Going on to the next voxel." << endl); } catch (Exception) { LOG_ERR(" NEWMAT Exception in this voxel:\n" << Exception::what() << endl); if (haltOnBadVoxel) throw; LOG_ERR(" Going on to the next voxel." << endl); } catch (...) { LOG_ERR(" Other exception caught in main calculation loop!!\n"); //<< " Use --halt-on-bad-voxel for more details." << endl; if (haltOnBadVoxel) throw; LOG_ERR(" Going on to the next voxel" << endl); } // now write the results to resultMVNs try { LOG << " Final parameter estimates (" << fwdPosterior.means.Nrows() << "x" << fwdPosterior.means.Ncols() << ") are: " << fwdPosterior.means.t() << endl; linear.DumpParameters(fwdPosterior.means, " "); //assert(resultMVNs.at(voxel-1) == NULL); // this is no longer a good check, since we might voerwrite previous results here resultMVNs.at(voxel-1) = new MVNDist( fwdPosterior, noiseVox->OutputAsMVN() ); if (needF) resultFs.at(voxel-1) = F; modelpred.Column(voxel) = linear.Offset(); // get the model prediction which is stored within the linearized forward model } catch (...) { // Even that can fail, due to results being singular LOG << " Can't give any sensible answer for this voxel; outputting zero +- identity\n"; MVNDist* tmp = new MVNDist(); tmp->SetSize(fwdPosterior.means.Nrows() + noiseVox->OutputAsMVN().means.Nrows()); tmp->SetCovariance(IdentityMatrix(tmp->means.Nrows())); resultMVNs.at(voxel-1) = tmp; if (needF) resultFs.at(voxel-1) = F; modelpred.Column(voxel) = linear.Offset(); // get the model prediction which is stored within the linearized forward model } delete noiseVox; noiseVox = NULL; delete noiseVoxSave; } //END of voxelwise updates //MOTION CORRECTION if (step0) { delete continueFromDists.back(); continueFromDists.pop_back(); } } VariationalBayesInferenceTechnique::~VariationalBayesInferenceTechnique() { delete conv; delete initialFwdPrior; delete initialFwdPosterior; }