I figured out how to do it. The method I use is to do stochastic gradient descent on the loss given by the maximum dot product between all pairs of basis vectors, thus maximizing the angle between all of them.
Here's the code:
import numpy
import math
def generate_basis_set(rows, cols, thresh=-0.999, maxiters=0):
"""
Generate a random basis set matrix with size (rows,cols) where all rows are unit length.
The number of rows and columns can have any value. The row basis vectors are optimized so that
they are maximally spread out in the input space so as to minimize the maximum dot product
across all pairs of basis vectors. Note that this algorithm produces pairs of bases that are
the same except for a change of sign (180 degrees apart), which is typically what is desired
when initializing a RELU neural net layer since this gives positive and negative contrast units.
Parameter "thresh" is the cosine distance where cos(theta) = thresh and provides an early exit. So for
example to exit as soon as no vectors are closer than 60 degrees apart set thresh=cos(pi/3).
Parameter "maxiters" is the maximum number of allowed iterations, where maxiters=0 leads to automatic
behavior.
The return value is a tuple consisting of the weight matrix and the maximum value of the cosine
distance (range -1.0 to 1.0).
"""
print("Generating random matrix")
m = numpy.random.randn(rows,cols)
print("Normalizing")
rowmag = numpy.sqrt((m**2).sum(axis=1))
m /= rowmag.reshape(rows,1)
print("Forming %d x %d dot product matrix" % (rows, rows))
c = numpy.empty((rows,rows))
for i in range(rows):
for j in range(i,rows):
if i!=j:
c[i,j] = numpy.dot(m[i,:],m[j,:])
c[j,i] = -2.0
else:
c[i,j] = -2.0
alpha = 0.1
avemaxslow = 1.0
avemaxfast = 1.0
kslow = 0.01
kfast = 0.1
iters = 0
change_iters = 0
while alpha>0.001:
# get location of max value of cos distance
ind1, ind2 = numpy.unravel_index(c.argmax(), c.shape)
maxval = c[ind1,ind2]
if maxval<thresh:
break
print("%d: alpha=%f\tind1=%4d ind2=%4d\tcos_dist = %f" % (iters,alpha,ind1,ind2,maxval))
# update weights
w1 = m[ind1,:]
w2 = m[ind2,:]
m[ind1,:] -= alpha * w2
m[ind2,:] -= alpha * w1
# renormalize - divisor should never be zero if alpha is <<1
norm = math.sqrt((m[ind1,:]**2).sum())
m[ind1,:] /= norm
norm = math.sqrt((m[ind2,:]**2).sum())
m[ind2,:] /= norm
# update cos distances
for i in range(rows):
if i<ind1:
c[i,ind1] = numpy.dot(m[i,:],m[ind1,:])
elif i>ind1:
c[ind1,i] = numpy.dot(m[ind1,:],m[i,:])
if i<ind2:
c[i,ind2] = numpy.dot(m[i,:],m[ind2,:])
elif i>ind2:
c[ind2,i] = numpy.dot(m[ind2,:],m[i,:])
# update moving averages
if iters==0:
avemaxslow = maxval
avemaxfast = maxval
else:
avemaxslow += kslow * (maxval - avemaxslow)
avemaxfast += kfast * (maxval - avemaxfast)
deltamax = avemaxslow - avemaxfast
# update learning rate
if iters>change_iters+100 and deltamax < 0.0001:
alpha *= 0.9
change_iters = iters
iters += 1
if maxiters>0 and iters>maxiters:
break
return m, c.max()
rows = 100
cols = 16
weights, maxcosdist = generate_basis_set(rows,cols)
print(weights, maxcosdist)