docs: private int gradientUpdate(List<TensorBuilder> factors,
        List<TensorBuilder> unaryFactors, double stepSize) {
      // The best assignment, as computed from only the unary factors.
      int[] variableNums = new int[unaryFactors.size()];
      int[] variableSizes = new int[unaryFactors.size()];
      int[] variableValues = new int[unaryFactors.size()];
      locallyDecodeFactors(unaryFactors, variableNums, variableSizes, variableValues);
  
      // Identify where unary factors disagree with larger factors to compute the
      // subgradient.
      List<Integer> variableDisagreementList = Lists.newArrayList();
      List<Integer> factorDisagreementList = Lists.newArrayList();
      List<Integer> factorValueList = Lists.newArrayList();
      for (int i = 0; i < factors.size(); i++) {
        TensorBuilder factor = factors.get(i);
        int[] bestFactorValues = factor.keyNumToDimKey(factor.getLargestValues(1)[0]);
        int[] factorVariableNums = factor.getDimensionNumbers();
  
        for (int j = 0; j < factorVariableNums.length; j++) {
          int index = Arrays.binarySearch(variableNums, factorVariableNums[j]);
          if (bestFactorValues[j] != variableValues[index]) {
            // Factor and unary assignments disagree on variableNums[index].
            // Store the disagreement, used later to compute the subgradient.
            variableDisagreementList.add(index);
            factorDisagreementList.add(i);
            factorValueList.add(bestFactorValues[j]);
  
            /*
            System.out.println("Disagreement " + Arrays.toString(factorVariableNums) + "=" + Arrays.toString(bestFactorValues) + ", "
                + variableNums[index] + "=" + variableValues[index]);
                */
          }
        }
      }
  
      // Update the factor lagrange multipliers based on the disagreements.
      // For each disagreement, the subgradient update decreases the weight of
      // each factor's maximum weight assignment, and increases the weight of
      // the disagreeing factor's maximum weight assignment.
      for (int i = 0; i < variableDisagreementList.size(); i++) {
        int variableIndex = variableDisagreementList.get(i);
        int factorNum = factorDisagreementList.get(i);
  
        TensorBuilder factor = factors.get(factorNum);
        TensorBuilder unaryFactor = unaryFactors.get(variableIndex);
  
        int variableNum = variableNums[variableIndex];
        int variableSize = variableSizes[variableIndex];
        int bestFactorValue = factorValueList.get(i);
        int bestUnaryValue = variableValues[variableIndex];
  
        SparseTensor unaryGradient = SparseTensor.singleElement(new int[] { variableNum },
            new int[] { variableSize }, new int[] { bestUnaryValue }, 1);
        SparseTensor factorGradient = SparseTensor.singleElement(new int[] { variableNum },
            new int[] { variableSize }, new int[] { bestFactorValue }, 1);
  
        unaryFactor.incrementWithMultiplier(unaryGradient, -1.0 * stepSize);
        unaryFactor.incrementWithMultiplier(factorGradient, stepSize);
        factor.incrementWithMultiplier(unaryGradient, stepSize);
        factor.incrementWithMultiplier(factorGradient, -1.0 * stepSize);
      }
  
      return variableDisagreementList.size();
    }