Plot 67
##############################################################
### Copyright (c) Richard Creamer 2019 - All Rights Reserved
### Inquiries: email 2to32minus1@gmail.com
##############################################################
### Globals
fontScale = 1.2 # Global font size variable
lf = 1.2 # Enlarge font of plot labels
verbose = FALSE # Verbose printing
lw = 2 # Line width
xMargin = 1 # X-axis margin for plots
yMargin = 1 # Y-axis margin for plots
de = 0.01 # Small constant for estimating derivatives numerically
smlPts = 1 # Small data point/circle
medPts = 1.5 # Medium data point/circle
lrgPts = 2.0 # Large data point/circle
### Run this function to generate this plot
runFunc = function() {
dw = 1000
dh = 750
path = "D:/Code/R/LeastSquaresPlay/PlotsAndAssocCode/Plot67/"
svSize = 1600
# Learn rate default (0.5) nIter default (100)
learnRate = 0.5
nIter = 100
savePngVerbose( paste(path, "Plot67.png", sep=""),
plotGaussianParamStepVectorsEx, w=svSize, h=svSize, un="px", gridSize=21, learnRate=learnRate, nIter=nIter )
}
### Plot Gaussian fit step vectors for multiple starting points
plotGaussianParamStepVectorsEx = function( nLevels=250, gridSize=3, learnRate=0.5, nIter=100 ) {
set.seed( 3 )
gd = makeGaussianTestData()
trueS = gd$trueSigma
trueA = gd$trueAmpl
maxA = trueA * 1.5
minA = -maxA
maxS = trueS * 3.5
minS = -maxS
ampls = seq( minA * 0.9, maxA * 0.9, length=gridSize )
sigmas = seq( minS * 0.9, maxS * 0.9, length=gridSize )
gridAmpls = seq( minA * 0.9, maxA * 0.9, length=400 )
gridSigmas = seq( minS * 0.9, maxS * 0.9, length=400 )
startAmpls = c()
startSigmas = c()
endAmpls = c()
endSigmas = c()
plotTitle = paste( "1D Gaussian Cost Function Contours + Gradient Descent Convergence Parameter Trails\n",
sprintf("Model: Amplitude = %.1f Sigma = %.1f gridSize = %d learnRate = %.2f nIter = %d",
trueA, trueS, gridSize, learnRate, nIter ), sep="" )
z = getGaussErrorGrid( meanGaussSsdErr, gridAmpls, gridSigmas, gd$x, gd$y )
contour( gridAmpls, gridSigmas, t(z), nlevels=nLevels, xlab="", ylab="", main=plotTitle )
addAxisLabels( xLabel="Amplitude", yLabel="Sigma", cexVal=1.7 )
for ( s in sigmas ) {
for ( a in ampls ) {
startAmpls[length(startAmpls)+1] = a
startSigmas[length(startSigmas)+1] = s
gd$guesses = list( a=a, s=s )
tuple = fitGauss1d( doPlot=FALSE, gd=gd, learnRate=learnRate, nIter=nIter )
aLegs = tuple$amplLegs
sLegs = tuple$sigmaLegs
nLegs = length( aLegs )
endAmpls[length(endAmpls)+1] = aLegs[nLegs]
endSigmas[length(endSigmas)+1] = sLegs[nLegs]
for ( j in 2:nLegs ) {
x1 = aLegs[j-1]
y1 = sLegs[j-1]
x2 = aLegs[j]
y2 = sLegs[j]
arrows( x1, y1, x2, y2, col="blue", angle=19, lwd=lw, length=0.125 )
}
}
}
# Draw beg/end circles last (so on top)
for ( i in 1:length( startAmpls ) ) {
points( startAmpls[i], startSigmas[i], col="chartreuse4", cex=1.5, lwd=lw+1 )
points( endAmpls[i], endSigmas[i], col="red2", cex=1.5, lwd=lw )
}
}
### Shared use case Gaussian data set generator used by several cooperating functions
makeGaussianTestData = function( trueAmpl=5, trueSigma=2, aGuess=sqrt(trueAmpl), sGuess=sqrt(trueSigma), nPts=40 ) {
set.seed( 3 )
x = seq( -5*trueSigma, 5*trueSigma, length=nPts )
y = gauss1dFunc( x, trueAmpl, trueSigma ) + rnorm( length(x), sd=0.2 )
guesses = list( a=aGuess, s=sGuess )
tuple = list( trueAmpl=trueAmpl, trueSigma=trueSigma, x=x, y=y, nPts=nPts, guesses=guesses )
return( tuple )
}
### 2D plot error contours for range of Amplitude and Sigma values
### Amplitude maps to x-axis, Sigma maps to y-axis
plotGaussErrorContours = function( nLevels=30, customTitle="", drawGradVecs=FALSE, normGradVecs=TRUE, gd=NULL ) {
set.seed( 3 )
if ( is.null( gd ) )
gd = makeGaussianTestData()
a = gd$trueAmpl
s = gd$trueSigma
x = gd$x
y = gd$y
minAmpl = a * 0.1; maxAmpl = a * 1.7
minSigma = s * 0.1; maxSigma = s * 1.7
# Adjust ranges so plot is square
deltaA = maxAmpl - minAmpl
deltaS = maxSigma - minSigma
maxDelta = if ( deltaA > deltaS ) deltaA else deltaS
if ( deltaA > deltaS ) {
maxSigma = minSigma + maxDelta
} else {
maxAmpl = minAmpl + maxDelta
}
nr = 200
nc = 200
ampls = seq( minAmpl, maxAmpl, length.out=nc )
sigmas = seq( minSigma, maxSigma, length.out=nr )
z = getGaussErrorGrid( meanGaussSsdErr, ampls, sigmas, x, y )
xLim1 = minAmpl
xLim2 = maxAmpl
yLim1 = minSigma
yLim2 = maxSigma
plotTitle = ""
if ( !drawGradVecs ) {
plotTitle = sprintf( "1D Gaussian Cost Function Contours\nTrue Model: Amplitude = %.1f Sigma = %.1f (noise added)", a, s )
} else if ( normGradVecs ) {
plotTitle = sprintf( "1D Gaussian Cost Function Contours w/Gradient (Unit) Vectors\nTrue Model: Amplitude = %.1f Sigma = %.1f (noise added)", a, s )
} else {
plotTitle = sprintf( "1D Gaussian Cost Function Contours w/Gradient Vectors\nTrue Model: Amplitude = %.1f Sigma = %.1f (noise added)", a, s )
}
if ( customTitle != "" )
plotTitle = customTitle
xform = contour( ampls, sigmas, t(z), axes=FALSE, cex.lab=lf, main=plotTitle, asp=1.0,
xlab="", ylab="", labcex=1.2, nlevels=nLevels, cex.main=1.3*fontScale, cex.axis=1.3 )
xAxisMin = ceiling( gpl()$xLim[1] )
xAxisMax = floor( gpl()$xLim[2] )
yAxisMin = ceiling( gpl()$yLim[1] )
yAxisMax = floor( gpl()$yLim[2] )
axis( side=1, at=xAxisMin:xAxisMax, cex.axis=1.3 )
axis( side=2, at=yAxisMin:yAxisMax, cex.axis=1.3 )
addAxisLabels( xLabel="Amplitude", yLabel="Sigma" )
box()
xLimits = axisLimits( x )
yLimits = axisLimits( y )
lines( c(-1,12), c( s, s ), col="blue", lwd=lw )
lines( c( a, a), c(-1,10), col="blue", lwd=lw )
if ( drawGradVecs ) {
gradPtsX1 = c()
gradPtsY1 = c()
gradPtsX2 = c()
gradPtsY2 = c()
gridSize=13
for ( sigma in seq( from=sigmas[2], to=sigmas[length(sigmas)-1], length=gridSize ) ) {
for ( ampl in seq( from=ampls[2], to=ampls[length(ampls)-1], length=gridSize ) ) {
gradPtsX1[length(gradPtsX1)+1] = ampl
gradPtsY1[length(gradPtsY1)+1] = sigma
da = gaussDeDa( x, y, ampl, sigma )
ds = gaussDeDs( x, y, ampl, sigma )
mag = sqrt( da*da + ds*ds )
if ( normGradVecs ) {
da = da/mag/4 # hard-coded case-specific scale factor, unit vecs a bit too long for plot
ds = ds/mag/4
} else {
da = da * 0.22 # hard-coded case-specific scale factor
ds = ds * 0.22
}
gradPtsX2[length(gradPtsX2)+1] = ampl - da
gradPtsY2[length(gradPtsY2)+1] = sigma - ds
}
}
points( gradPtsX1, gradPtsY1, col="blue", lwd=lw )
arrows( gradPtsX1, gradPtsY1, gradPtsX2, gradPtsY2, col="blue", lwd=lw, cex=medPts, angle=15, length=0.125 )
}
}
### Nonlinear least-squares gradient-descent curve fitting for Gaussian
fitGauss1d = function( learnRate = 0.5, nIter=100, nPts=40, drawPointLines=FALSE, doPlot=TRUE, numDerivs=FALSE, plotContours=FALSE, gd=NULL ) {
set.seed( 3 )
a = s = x = y = guesses = NULL
if ( is.null( gd ) )
gd = makeGaussianTestData()
a = gd$trueAmpl
s = gd$trueSigma
trueAmpl = gd$trueAmpl
trueSigma = gd$trueSigma
x = gd$x
y = gd$y
nPts=gd$nPts
guesses = gd$guesses
aEst = guesses$a
sEst = guesses$s
da = 0
ds = 0
amplLegs = c( aEst )
sigmaLegs = c( sEst )
if ( nIter > 0 ) {
# Basic implementation of Gradient Descent
for ( i in 1:nIter ) {
da = if ( numDerivs ) gaussDeDaNum( x, y, aEst, sEst ) else gaussDeDa( x, y, aEst, sEst )
ds = if ( numDerivs ) gaussDeDsNum( x, y, aEst, sEst ) else gaussDeDs( x, y, aEst, sEst )
aEst = aEst - learnRate * da
sEst = sEst - learnRate * ds
amplLegs[ length(amplLegs) + 1 ] = aEst
sigmaLegs[ length(sigmaLegs) + 1 ] = sEst
if ( verbose )
print( sprintf( "Mean ABS fit error: %f\n", meanGaussAbsErr( x, y, aEst, sEst ) ) )
}
}
finalMeanAbsFitError = meanGaussAbsErr( x, y, aEst, sEst )
if ( doPlot) {
plotTitle = "Gradient Descent Fit 1D Gaussian using Analytic Partial Derivatives"
if ( numDerivs )
plotTitle = "Gradient Descent Fit 1D Gaussian using Numerical Est. Partial Derivatives"
plot( x, y, cex.lab=lf, xlab="", ylab="", main=plotTitle, cex=medPts, lwd=lw+1, cex.main=1.3*fontScale, cex.axis=1.3 )
addAxisLabels()
predX = seq( x[1], x[length(x)], length.out=200 )
predY = gauss1dFunc( predX, aEst, sEst )
lines( predX, predY, col="blue", lwd=lw+1 )
if ( drawPointLines )
segments( x, y, x, gauss1dFunc( x, aEst, sEst ), col = "red" )
legText = c()
legText[length(legText)+1] = sprintf( "Num Iterations %d", nIter )
legText[length(legText)+1] = sprintf( "Learn Rate: %.3f", learnRate )
legText[length(legText)+1] = sprintf( "True Amplitude: %.2f", a )
legText[length(legText)+1] = sprintf( "True Sigma: %.2f", s )
legText[length(legText)+1] = sprintf( "Start Amplitude: %.2f", guesses$a )
legText[length(legText)+1] = sprintf( "Start Sigma: %.2f", guesses$s )
legText[length(legText)+1] = sprintf( "Final Amplitude: %.2f", aEst )
legText[length(legText)+1] = sprintf( "Final Sigma: %.2f", sEst )
legText[length(legText)+1] = sprintf( "Mean ABS Error: %.2f", meanGaussAbsErr( x, y, aEst, sEst ) )
legLtys = seq( from=0, to=0, length=length(legText) )
parSave = par( family="mono", font=2 )
legend( "topleft", inset=c(0,0), legend=legText, lty=legLtys, box.col="transparent", cex=1.3*fontScale, bg="transparent" )
par( parSave )
}
if ( plotContours ) {
plotGaussErrorContours( gd=gd )
lines( amplLegs, sigmaLegs, col="red", lwd=lw )
}
return( list( x=x, y=y, a=aEst, s=sEst, meanAbsFitError=finalMeanAbsFitError, amplLegs=amplLegs, sigmaLegs=sigmaLegs, lr=learnRate ) )
}
### Compute mean SSD Gaussian cost function over a grid of Amplitude and Sigma values
### Note: returned matrix may need to be transposed for some functions such as contour()
### TODO: Try to use outer()
getGaussErrorGrid = function( errorFunc, ampls, sigmas, x, y ) {
nr = length( sigmas )
nc = length( ampls )
z = matrix( nrow=nr, ncol=nc )
for ( row in 1:nr ) { # loop over y-coordinates (sigma)
sigma = sigmas[row]
for ( col in 1:nc ) { # loop over x-coordinates (amplitude)
ampl = ampls[col]
z[row, col] = errorFunc( x, y, a=ampl, sigma=sigma )
}
}
return( z )
}
### 1D Gaussian
gauss1dFunc = function( x, a, sigma ) {
return( a * exp( -0.5 * (x/sigma)^2 ) )
}
### Gaussian error/cost function - abs() of line lengths (not squared)
meanGaussAbsErr = function( x, y, a, sigma ) {
return( (1/length(x)) * sum( abs(gauss1dFunc( x, a, sigma ) - y) ) )
}
### Gaussian error/cost function - SSD
meanGaussSsdErr = function( x, y, a, sigma ) {
return( (1/length(x)) * sum( (gauss1dFunc( x, a, sigma ) - y)^2 ) )
}
### Analytical/exact partial derivative of error/cost function wrt Amplitude
gaussDeDa = function( x, y, a, sigma ) {
term1 = sum( gauss1dFunc(x, a, sigma) * exp( -0.5 * (x/sigma)^2) )
term2 = sum( y * exp( -0.5 * (x/sigma)^2) )
return( 2 * ( term1 - term2 )/length(x) )
}
### Analytical/exact partial derivative of error/cost function wrt Sigma
gaussDeDs = function( x, y, a, sigma ) {
v = 2*a/sigma^3
term1 = sum( gauss1dFunc(x, a, sigma) * x^2 * exp( -0.5 * (x/sigma)^2) )
term2 = sum( x^2 * y * exp( -0.5 * (x/sigma)^2) )
return( v * ( term1 - term2 )/length(x) )
}
### Numerical estimation of partial derivative of error/cost function wrt Amplitude
gaussDeDaNum = function( x, y, a, sigma ) {
e1 = meanGaussSsdErr( x, y, a, sigma )
e2 = meanGaussSsdErr( x, y, a + de, sigma )
return( (e2 - e1)/de )
}
### Numerical estimation of partial derivative of error/cost function wrt Sigma
gaussDeDsNum = function( x, y, a, sigma ) {
e1 = meanGaussSsdErr( x, y, a, sigma )
e2 = meanGaussSsdErr( x, y, a, sigma + de )
return( (e2 - e1)/de )
}
### Get Plot Limits : gpl()$xLim[1] --> left x-coord
gpl = function() {
u = par( "usr" )
return( list( xLim=u[1:2], yLim=u[3:4] ) )
}
### Compute plot axis limits to fit range of data
axisLimits = function( v, margin=0 ) {
lowerLimit = floor( min(v) ) - margin
upperLimit = ceiling( max(v) ) + margin
c( lowerLimit, upperLimit )
}
### Convience method to add x/y axis labels
addAxisLabels = function( xLabel="x", yLabel="y", cexVal=1.3 ) {
mtext( text=xLabel, side=1, line=2.5, cex=cexVal )
mtext( text=yLabel, side=2, line=2.5, cex=cexVal )
}
### Save to PNG file, specify width and height
savePngVerbose = function( path, plotFunc, w=512, h=512, un="px", doCopyright=TRUE, ... ) {
png( filename = path, type="cairo", units=un, width=w, height=h, pointsize=12, res=96 )
plotFunc( ... )
if ( doCopyright )
addCopyright()
dev.off()
}
### Add copyright notice to plot via text()
addCopyright = function() {
mtext( "Copyright \uA9 2019 Richard Creamer - All Rights Reserved", side=4, line=0, adj=0, cex=1.1 )
mtext( "Email: 2to32minus1@gmail.com", side=4, line=1, adj=0, cex=1.1 )
}
runFunc()