Statistical Conformity: Feedback1

Introduction

The Feedback indexer applies an algorithm of my own devising called "statistical feedback"2 to ensure that each index value receives its fair share of usage over the extent of a sequence. The number of index values, along with the relative frequency with which each value should appear, is controlled by a set of weights {W0, W1, …, WN-1}, where Wj is the weight accorded to index value j and N is the supply size.

The Feedback indexer is the discrete counterpart to the Balance driver, to the extent that both rely upon statistical feedback. The difference is that Balance produces continuous uniform distributions which must be adapted to non-uniform distributions using a separate Transform unit. Feedback, by contrast, accepts a discrete distribution parametrically.

What "fair share" means depends upon the weights. When the weights are uniform (all equal), then "fair share" means equal share. The Feedback algorithm maintains a set of running statistics {S0, S1, …, SN-1}, where Sj reflects how often index value j has been selected up to the current sequence element. Favor is given to whichever index value has fallen farthest behind the others. When two or more index values have fallen equally behind, the choice between them is random.

When Wj = m×Wn, then index value j should occur m times as often in the sequence as index value n. This is accomplished by advancing the statistic for value j (when selected) as follows:

Sj  =  Sj  +  Δj

where

Δj  = 
A
Wj

Here the parameter A, which is independent of j, quantifies the accent or stress associated with the current sequence element.

Actual selection is based upon a set of decider values {θ0, θ1, …, θN-1}, where the calculation of θj begins with an estimation of what Sj will become, should index j be chosen, then leavens this estimation with randomness to cope with situations where two decider values would otherwise be equal or nearly equal:

θj  =  Sj  + 
A + H×(uj −0.5)
Wj

Here uj is calculated using Random.nextDouble(), while the parameter H (for heterogeneity) modulates the influence of (uj −0.5).

Table 1 emulates how the algorithm works with N=2, W0=1., W1=2., A=1., and H=0.1:

kAHW0Δ0S0Θ0W1Δ1S1Θ1Result
01.00.11.0001.0000.0001.0232.0000.5000.0000.4961
11.00.11.0001.0000.0000.9712.0000.5000.5000.9920
21.00.11.0001.0001.0002.0472.0000.5000.5000.9751
31.00.11.0001.0001.0002.0462.0000.5001.0001.5221
41.00.11.0001.0001.0002.0452.0000.5001.5002.0221
51.00.11.0001.0001.0001.9902.0000.5002.0002.4920
61.00.11.0001.0002.0002.9792.0000.5002.0002.5001
71.00.11.0001.0002.0002.9622.0000.5002.5003.0140
81.00.11.0001.0003.0004.0162.0000.5002.5002.9831
91.00.11.0001.0003.0003.9882.0000.5003.0003.4821
101.00.11.0001.0003.0004.0192.0000.5003.5004.0151
111.00.11.0001.0003.0003.9512.0000.5004.0004.5010
Table 1: Emulation of Statistical Feedback.

With H small relative to A, this mostly determinate algorithm maintains conformity over the very short term indeed. The first three rows (0-2) select indices 1, 0, 1; the next three rows (3-5) select indices 1, 1, 0; the three rows after that (6-8) select indices 1, 0, 1, and the final three rows (9-11) select indices 1, 1, 0. In each instance two sequence elements have index value 1 while the remaining element has value 0. This is exactly what the weights prescribed.

The Feedback algorithm takes an additional step not reflected in Table 1; that step is to offset the running statistics so that

ΣN-1 j=0 Sj =  0.

As explained in "Statistics and Compositional Balance"3, this step is needed to cope with situations where Wn falls to zero for some index value n. When that happens the increment Δn = 1/Wn becomes undefined. Since the decider value θn cannot be calculated, the value is simply skipped over. Sj keeps advancing for each j still active, but Sn stays fixed. If after some number of samples Wn becomes positive again, there's a whole lot of catching up to do. Keeping the running statistics centered around zero remedies this situation.

Profile

Figure 1 illustrates four examples of Feedback output with sequences of 200 samples.


Figure 1: Sample output from Feedback.next() with different weightings and heterogeneity settings. The left graph in each row displays samples in time-series while the right graph in the same row presents a histogram analyzed from the same samples.

The vertical x axes for the two graphs in each row represent the index domain from zero to N; the horizontal j axis of the time-series graph (left) plots ordinal sequence numbers; the horizontal f(x) axis of the histogram (right) plots the relative concentration of samples at each point in the driver domain.

The top two sequences illustrate scenarios with uniform weights. More specifically, the array of weights was {1., 1., 1., 1., 1.}.

The heterogeneity setting for the topmost sequence was 0.1. Upon close examination you can verify that the first five samples are {2, 0, 4, 3, 1}, the next five samples are {0, 2, 4, 3, 1}, the next five samples are {2, 4, 1, 0, 3}, and so fourth: each successive frame of five samples presents all five indices. The histogram on the right is absolutely flat, reflecting the fact that each index value appears exactly 40 times over the length of the sequence.

The heterogeneity setting for the second-from-top sequence was 4.0. It is no longer true that successive frames of five samples present all five indices. However, any 10 consecutive samples will contain approximately 2 instances of each index value. The histogram for this second sequence, taken as a whole, only shows the very slightest aberration from perfect uniformity.

The bottom two sequences illustrate scenarios with non-uniform weights. In these examples the array of weights was {1., 1./2., 1./3., 1./4., 1./5.}.

The heterogeneity setting for the second-from-bottom sequence was 0.1. This means that any 27 consecutive samples should contain 12 instances of index 0, 6 instances of index 1, 4 instances of index 2, 3 instances of index 3, and 2 instances of index 4. The histogram on the right confirms that the actual counts of index values conforms to the prescribed weights.

The heterogeneity setting for the bottommost sequence was 4.0. Short-term conformity is less evident, but here any 54 consecutive samples should contain nearly 24 instances of index 0, 12 instances of index 1, 8 instances of index 2, 6 instances of index 3, and 4 instances of index 4. The histogram for this bottommost sequence is not readily distinguishable from the histogram for the non-uniform sequence with heterogeneity 0.1.

Stress

Consider this abstract scenario: there is a collection of entities, the objective is to select an attribute for each entity from a supply in such a matter that each supply element is emphasized equally. A musical version of this scenario has notes as entities and scale degrees as attributes. A graphic version has shapes as entities and colors as attributes. The Feedback indexer offers one way of addressing these analogous scenarios. Equal emphasis will result if the weights associated with supply elements are all the same.

Except, what happens when the note durations are unequal, or when the shapes have different areas? Does a whole-note E balance with a sixteenth-note F? Does a large pink shape balance with a small red shape?

Othodox serialism maintains that a whole-note E does indeed balance with a sixteenth-note F. Yet serialism provides a carve out: row elements can be repeated if done so in a decorative way; for example, as a tremolo.

Suppose you believe in equal balance, but unlike an orthodox serialist you believe that 16 sixteenth notes are required to balance one whole note. If so, the Feedback indexer can accomodate you. If you're about to select a degree for a sixteenth note, set A = 1. If you're about to select a degree for a whole note, set A = 16.

Suppose you believe that duration counts for something but that just showing up also counts for something, and familiarity breeds contempt. A comprimise might be to set A to the square root of the duration. If you're about to select a degree for a sixteenth note, set A = √1 = 1. If you're about to select a degree for a whole note, set A = √16 = 4.

Considerations become even more murky when some weights are much smaller than others. The mere fact that an attribute is rare means that when that attribute is selected for an entity, the attribute itself imparts stress to the entity. Thus a yellow life vest can be easily discernable amongst a sea of blue.

/**
 * The {@link Feedback} class implements selection using statistical feedback.
 * @author Charles Ames
 */
public class Feedback
extends WriteableEntity implements Indexer {
   private DiscreteDistribution distribution;
   private double stress;
   private double heterogeneity;
   private double balance[];
   private int position;
   private Random random;
   /**
    * Constructor for {@link Feedback} instances.
    * @param container A {@link WriteableEntity} which contains this instance.
    */
   public Feedback(WriteableEntity container) {
      super(container);
      setIDQuality(AttributeQuality.MODIFIABLE);
      setNameQuality(AttributeQuality.MODIFIABLE);
      this.balance = null;
      this.random = SharedRandomGenerator.getSingleton().getRandom();
      this.distribution = new DiscreteDistribution(this);
      this.distribution.setIDQuality(AttributeQuality.UNUSED);
      this.distribution.setNameQuality(AttributeQuality.UNUSED);
      this.stress = Double.NaN;
      this.heterogeneity = Double.NaN;
      this.position = Integer.MIN_VALUE;
   }
   /**
    * Set the count of index choices.
    * @param limit The intended length of the {@link balance} array.
    * @return True if the length has changed; false otherwise.
    */
   private boolean allocateBalances(int limit) {
      if (0 >= limit) throw new IllegalArgumentException("Argument not positive");
      if (null != balance) {
         if (balance.length == limit) return false;
      }
      balance = new double[limit];
      resetBalances();
      makeDirty();
      return true;
   }
   private void resetBalances() {
      int limit = getLimit();
      for (position = 0; position < limit; position++) {
         balance[position] = 0.;
      }
   }
   /**
    * Get the count of index choices.
    * @return The allocated length of the {@link #balance} array.
    */
   public int getLimit() {
      if (null == balance) throw new UninitializedException("Balance array not dimensioned");
      return balance.length;
   }
   /**
    * Set the weights data.
    * @param weights An array of non-negative double-precision numbers.
    * @throws IllegalArgumentException when the argument is null.
    * @throws IllegalArgumentException when any weight is negative.
    */
   public void setWeights(double[] weights) {
      if (null == weights)
         throw new IllegalArgumentException("Null argument");
      allocateBalances(weights.length);
      distribution.initialize();
      int items = weights.length;
      for (int index = 0; index < items; index++) {
         distribution.addItem(index, weights[index]);
      }
      distribution.normalize();
      makeDirty();
   }
   /**
    * Getter for {@link #stress}.
    * @return The assigned {@link #stress} value.
    * @throws UninitializedException when the {@link #stress} value is uninitialized.
    */
   public double getStress() {
      if (Double.isNaN(stress)) throw new UninitializedException("Stress not initialized");
      return stress;
   }
   /**
    * Setter for {@link #stress}.
    * @param stress The intended {@link #stress} value.
    * @return True if {@link #stress} has changed; false otherwise.
    * @throws IllegalArgumentException when the argument is not positive.
    */
   public boolean setStress(double stress) {
      if (stress < MathMethods.TINY)
         throw new IllegalArgumentException("Stress must be positive");
      if (this.stress != stress) {
         this.stress = stress;
         makeDirty();
         return true;
      }
      return false;
   }
   /**
    * Getter for {@link #heterogeneity}.
    * @return The assigned {@link #heterogeneity} value.
    * @throws UninitializedException when the {@link #heterogeneity} value is uninitialized.
    */
   public double getHeterogeneity() {
      if (Double.isNaN(heterogeneity)) throw new UninitializedException("Heterogeneity not initialized");
      return heterogeneity;
   }
   /**
    * Setter for {@link #heterogeneity}.
    * @param heterogeneity The intended {@link #heterogeneity} value.
    * @return True if {@link #heterogeneity} has changed; false otherwise.
    * @throws IllegalArgumentException when the argument is not positive.
    */
   public boolean setHeterogeneity(double heterogeneity) {
      if (heterogeneity < MathMethods.TINY)
         throw new IllegalArgumentException("Heterogeneity must be positive");
      if (this.heterogeneity != heterogeneity) {
         this.heterogeneity = heterogeneity;
         makeDirty();
         return true;
      }
      return false;
   }
   /**
    * Update usage statistics in the event that the indicated index has been selected.
    * @param usedIndex The indicated index.
    */
   public void useIndex(int usedIndex) {
      double usedWeight = Double.NaN;
      int index = 0;
      int count = 0;
      double stress = getStress();
      Iterator<DiscreteDistributionItem> iterator = distribution.iterateItems();
      while (iterator.hasNext()) {
         DiscreteDistributionItem item = iterator.next();
         double weight = item.getWeight();
         if (weight > MathMethods.TINY) {
            count++;
            if (usedIndex == index) usedWeight = weight;
         }
         index++;
      }
      double increment = stress / usedWeight;
      balance[usedIndex] += increment;
      double decrement = increment / count;
      index = 0;
      iterator = distribution.iterateItems();
      while (iterator.hasNext()) {
         DiscreteDistributionItem item = iterator.next();
         double weight = item.getWeight();
         if (weight > MathMethods.TINY) {
            balance[index] -= decrement;
         }
         index++;
      }
   }
   public Integer next() {
      double bestScore = Double.MAX_VALUE;
      int bestIndex = Integer.MAX_VALUE;
      int index = 0;
      double heterogeneity = getHeterogeneity();
      double stress = getStress();
      Iterator<DiscreteDistributionItem> iterator = distribution.iterateItems();
      while (iterator.hasNext()) {
         DiscreteDistributionItem item = iterator.next();
         double weight = item.getWeight();
         if (weight > MathMethods.TINY) {
            double score = balance[index] + ((stress + (heterogeneity * (random.nextDouble()-0.5))) / weight);
            if (score < bestScore) {
               bestScore = score;
               bestIndex = index;
            }
         }
         index++;
      }
      if (Integer.MAX_VALUE == bestIndex)
         throw new RuntimeException("No nonzero weights");
      useIndex(bestIndex);
      return bestIndex;
   }
   @Override
   public boolean hasNext() {
      return true;
   }
   @Override
   public boolean hasReset() {
      return true;
   }
   @Override
   public void reset() {
      resetBalances();
   }
   @Override
   public double minGraphValue() {
      if (0 == distribution.itemCount()) throw new UnsupportedOperationException("Distribution has no values");
      return distribution.getMaxValue();
   }
   @Override
   public double maxGraphValue() {
      if (0 == distribution.itemCount()) throw new UnsupportedOperationException("Distribution has no values");
      return distribution.getMinValue();
   }
}
Listing 1: The Feedback implementation class.

Coding

The type hierarchy for Feedback is:

Listing 1 provides the source code for the Feedback class. The embedded DiscreteDistribution instance, distribution, holds the set of weights symbolized above as {W0, W1, …, WN-1}. The double array balance holds the running statistics symbolized above as {S0, S1, …, SN-1}. The stress field holds the accent/stress value symbolized above as A. The heterogeneity field holds the randomness-modulating factor symbolized above as H.

The Feedback indexer being frame-free, method Feedback.hasNext() is hard coded to always return true. The public-facing method Feedback.next() choses which index to present based upon decider values calculated directly in the method. Once an index has been selected, next() delegates to useIndex() the job of incrementing the pertinant statistic; this private helper method also takes care of recentering all the statistics around zero.

Comments

  1. The present text is adapted from my Leonardo Music Journal article from 1992, "A Catalog of Sequence Generators". The heading is "Statistical Feedback", p. 69.
  2. Statistical feedback is explained in my Perspectives of New Music article from 1990, "Statistics and Compositional Balance".
  3. The topic heading is "Statistical Exclusion" on page 100.

© Charles Ames Page created: 2022-08-29 Last updated: 2022-08-29