Alink's Talk (15): Итеративная оптимизация многослойных персептронов

машинное обучение

0x00 сводка

Alink — это платформа алгоритмов машинного обучения нового поколения, разработанная Alibaba на основе вычислительного движка реального времени Flink, и первая в отрасли платформа машинного обучения, которая поддерживает как пакетные, так и потоковые алгоритмы. Эта статья и предыдущая статья приведут вас к анализу реализации многослойного персептрона в Alink.

Поскольку общедоступная информация Alink слишком мала, нижеследующее - все предположения, и обязательно будут упущения и ошибки. Я надеюсь, что все укажут, и я обновлю ее в любое время в будущем.

0x01 предыдущий обзор

раньшеALink Talk (14): Общая архитектура многослойного персептронаМы поняли концепцию многоуровневого персептрона и общую архитектуру Alink Теперь мы начнем знакомить с тем, как ее оптимизировать.

1.1 Основные понятия

Давайте еще раз рассмотрим основные понятия:

  • Вход нейрона: аналогично линейной регрессии z = w1x1+ w2x2 +⋯ + wnxn = wT x (линейная пороговая единица (LTU)).

  • Выход нейрона: функция активации, аналогичная бинарной классификации, моделирует, что нейроны в биологии имеют только два состояния возбуждения и торможения. Увеличьте значение смещения, какой узел в выходном слое имеет наибольший вес и какой из них выводится.

  • Используя критерий Хебба, следующий метод корректировки веса относится к текущему весу и тренировочному эффекту.

  • Как научиться автоматически вычислять веса - обратное распространение, то есть сначала сделать прямой расчет, а затем сделать расчет ошибки в соответствии с текущим выходом, как направляющий сигнал, чтобы обратно настроить выходной вес предыдущего слоя, чтобы он попал в разумный диапазон и неоднократно настраиваться на первый слой, каждый раунд настройки имеет скорость обучения, после настройки сеть становится все более и более разумной.

1.2 Алгоритм обратного распространения ошибки

Процесс обучения нейронной сети с прямой связью на основе алгоритма обратного распространения ошибки (BP) можно разделить на следующие три этапа:

  1. Вычислить чистый вход z(l) и значение активации a(l) каждого слоя во время прямого распространения до последнего слоя;
  2. (Этап обратного распространения) Различает отклик возбуждения с целевым выходом, соответствующим обучающему входу, чтобы получить ошибку отклика скрытого слоя и выходного слоя. Вычислить член ошибки δ(l) для каждого слоя, используя обратное распространение ошибки;
  3. Веса в каждом синапсе обновляются следующим образом: умножьте входные ошибки возбуждения и отклика, чтобы получить градиент весов; умножьте этот градиент на шкалу, инвертируйте и добавьте к весам. То есть вычисляются частные производные параметров каждого слоя и обновляются параметры.

Это соотношение (в процентах) будет влиять на скорость и эффективность тренировочного процесса, отсюда и название «тренировочный фактор». Направление градиента указывает направление расширения ошибки, поэтому его необходимо изменить на противоположное при обновлении веса, тем самым уменьшив ошибку, вызванную весом.

1.3 Общая логика

Давайте рассмотрим общую логику.

在这里插入图片描述

0x02 Обучить нейронную сеть

Эта часть является основным событием, поэтому ее следует отделить от общей логики и сказать отдельно.

Функция FeedForwardTrainer.train завершит обучение нейросети, а именно:

DataSet<DenseVector> weights = trainer.train(trainData, getParams());

Логика примерно такая:

  • 1) Инициализировать модельinitCoef = initModel(data, this.topology);
  • 2) Сжать данные обучения в вектор,trainData = stack()。Это должно быть сделано для уменьшения объема передаваемых данных. Обратите внимание, что он преобразует входные данные из двух кортежей<label index, vector>Преобразовать в тройкуTuple3.of((double) count, 0., stacked);
  • 3) Создать целевую функцию оптимизацииnew AnnObjFunc(topology...)
  • 4) Создайте тренажерFeedForwardTrainer
  • 5) Тренер построит оптимизатор на основе целевой функцииOptimizer optimizer = new Lbfgs,то есть использоватьL-BFGSобучить нейронную сеть;

2.1 Инициализация модели

Код, связанный с моделью инициализации, выглядит следующим образом:

DataSet<DenseVector> initCoef = initModel(data, this.topology); // 随机初始化权重系数
optimizer.initCoefWith(initCoef);

public void initCoefWith(DataSet<DenseVector> initCoef) {
	this.coefVec = initCoef; // 就是赋值在内部变量上
}

Это нужно сравнить с линейной регрессией:

  • В линейной регрессии, если есть два признака, начальный коэффициент равенcoef = {DenseVector} "0.001 0.0 0.0 0.0", 4 значения конкретно"权重, b, w1, w2".

  • Многослойный персептрон Здесь коэффициент инициализации равенDenseVector.

initModel предназначен в основном для случайной инициализации весовых коэффициентов. Основная функция находится в разделе topology.getWeightSize().

private DataSet<DenseVector> initModel(DataSet<?> inputRel, final Topology topology) {
        if (initialWeights != null) {
          ......
        } else {
            return BatchOperator.getExecutionEnvironmentFromDataSets(inputRel).fromElements(0)
                .map(new RichMapFunction<Integer, DenseVector>() {
                    ......
                    @Override
                    public DenseVector map(Integer value) throws Exception {
                        // 这里是精华,获取了系数的size
                        DenseVector weights = DenseVector.zeros(topology.getWeightSize());                           
                        
                        for (int i = 0; i < weights.size(); i++) {
                            weights.set(i, random.nextGaussian() * initStdev);//随机初始化
                        }
                        return weights;
                    }
                })
                .name("init_weights");
        }
}

topology.getWeightSize() вызывает функцию FeedForwardTopology, которая должна проходить каждый слой и накапливать коэффициенты.

@Override
public int getWeightSize() {
        int s = 0;
        for (Layer layer : layers) { //遍历各层
            s += layer.getWeightSize();
        }
        return s;
}

Размеры коэффициентов AffineLayer следующие:

@Override
public int getWeightSize() {
		return numIn * numOut + numOut;
}

FunctionalLayer не имеет коэффициентов

@Override
public int getWeightSize() {
	return 0;
}

SoftmaxLayerWithCrossEntropyLoss не имеет коэффициентов

@Override
public int getWeightSize() {
	return 0;
}

Просмотрите соответствующую топологию для этого примера:

this = {FeedForwardTopology@4951} 
 layers = {ArrayList@4944}  size = 4
      0 = {AffineLayer@4947} // 仿射层
       numIn = 4
       numOut = 5
      1 = {FuntionalLayer@4948} 
       activationFunction = {SigmoidFunction@4953}  // 激活函数
      2 = {AffineLayer@4949} // 仿射层
       numIn = 5
       numOut = 3
      3 = {SoftmaxLayerWithCrossEntropyLoss@4950}  // 激活函数

Таким образом, окончательный размер вектора DenseVector представляет собой сумму двух аффинных слоев.(4 + 5 * 5)+ (5 + 3 * 3) = 43.

2.2 Сжатые данные

Здесь два кортежа входных данных преобразуются и сжимаются в DenseVector. Целью сжатия здесь должно быть уменьшение объема передаваемых данных.

Если вход:

batchData = {ArrayList@11527}  size = 64
 0 = {Tuple2@11536} "(2.0,6.5 2.8 4.6 1.5)"
  f0 = {Double@11572} 2.0
  f1 = {DenseVector@11573} "6.5 2.8 4.6 1.5"
 1 = {Tuple2@11537} "(1.0,6.1 3.0 4.9 1.8)"
  f0 = {Double@11575} 1.0
  f1 = {DenseVector@11576} "6.1 3.0 4.9 1.8"
.....

Тогда окончательные выходные сжатые данные:

batch = {Tuple3@11532} 
 f0 = {Double@11578} 18.0
 f1 = {Double@11579} 0.0
 f2 = {DenseVector@11580} "6.5 2.8 4.6 1.5 6.1 3.0 4.9 1.8 7.3 2.9 6.3 1.8 5.7 2.8 4.5 1.3 6.4 2.8 5.6 2.1 6.7 2.5 5.8 1.8 6.3 3.3 4.7 1.6 7.2 3.6 6.1 2.5 7.2 3.0 5.8 1.6 4.9 2.4 3.3 1.0 7.4 2.8 6.1 1.9 6.5 3.2 5.1 2.0 6.6 2.9 4.6 1.3 7.9 3.8 6.4 2.0 5.2 2.7 3.9 1.4 6.4 2.7 5.3 1.9 6.8 3.0 5.5 2.1 5.7 2.5 5.0 2.0 2.0 1.0 1.0 2.0 1.0 1.0 2.0 1.0 1.0 2.0 1.0 1.0 2.0 1.0 2.0 1.0 1.0 1.0"

Конкретный код выглядит следующим образом:

static DataSet<Tuple3<Double, Double, Vector>>
    stack(DataSet<Tuple2<Double, DenseVector>> data ...) {
    
        return data
            .mapPartition(new MapPartitionFunction<Tuple2<Double, DenseVector>, Tuple3<Double, Double, Vector>>() {
                @Override
                public void mapPartition(Iterable<Tuple2<Double, DenseVector>> samples,
                                         Collector<Tuple3<Double, Double, Vector>> out) throws Exception {
                    List<Tuple2<Double, DenseVector>> batchData = new ArrayList<>(batchSize);
                    int cnt = 0;
                    Stacker stacker = new Stacker(inputSize, outputSize, onehot);
                    for (Tuple2<Double, DenseVector> sample : samples) {
                        batchData.set(cnt, sample);
                        cnt++;
                        if (cnt >= batchSize) { // 如果已经大于默认的数据块大小,就直接发送
                            // 把batchData的x-vec压缩到DenseVector中
                            Tuple3<Double, Double, Vector> batch = stacker.stack(batchData, cnt);
                            out.collect(batch);
                            cnt = 0;
                        }
                    }

                    // 如果压缩成功,则输出
                    if (cnt > 0) { // 没有大于默认数据块大小,也发送。cnt就是目前的数据块大小,针对本实例,是19,这也是后面能看到 matrix 维度 19 的来源。
                        Tuple3<Double, Double, Vector> batch = stacker.stack(batchData, cnt);
                        out.collect(batch); 
                    }
                }
            })
            .name("stack_data");
}

2.3 Создание целевой функции оптимизации

Напомним примечания о функции потерь и целевой функции:

  • Функция потерь: вычисляет ошибку выборки;
  • Функция стоимости: это среднее значение всех ошибок выборки на всем обучающем наборе, часто смешанное с функцией потерь;
  • Целевая функция: функция стоимости + член регуляризации;

Код целевой функции, формируемый в многослойном персептроне, выглядит следующим образом:

final AnnObjFunc annObjFunc = new AnnObjFunc(topology, inputSize, outputSize, onehotLabel, optimizationParams);

AnnObjFuncs — целевая функция оптимизации многоуровневого персептрона, которая определяется следующим образом:

  • топология — топология нейронной сети;
  • укладчик используется для сжатия и распаковки (последующий L-BFGS является векторной операцией, поэтому он также должен преобразовывать туда и обратно из матрицы в вектор);
  • topologyModel — расчетная модель;

Мы видим, что при вызове в API AnnObjFunc он увидитAnnObjFunc.topologyModelЕсть ли значение, если нет, сгенерируйте его.

public class AnnObjFunc extends OptimObjFunc {
    private Topology topology;
    private Stacker stacker;
    private transient TopologyModel topologyModel = null;
    
    @Override
    protected double calcLoss(Tuple3<Double, Double, Vector> labledVector, DenseVector coefVector) {
        // 看 AnnObjFunc.topologyModel 是否有值,如果没有就生成
        if (topologyModel == null) {
            topologyModel = topology.getModel(coefVector);
        } else {
            topologyModel.resetModel(coefVector);
        }
        Tuple2<DenseMatrix, DenseMatrix> unstacked = stacker.unstack(labledVector);
        return topologyModel.computeGradient(unstacked.f0, unstacked.f1, null);
    }

    @Override
    protected void updateGradient(Tuple3<Double, Double, Vector> labledVector, DenseVector coefVector, DenseVector updateGrad) {
        // 看 AnnObjFunc.topologyModel 是否有值,如果没有就生成
        if (topologyModel == null) {
            topologyModel = topology.getModel(coefVector);
        } else {
            topologyModel.resetModel(coefVector);
        }
        Tuple2<DenseMatrix, DenseMatrix> unstacked = stacker.unstack(labledVector);
        topologyModel.computeGradient(unstacked.f0, unstacked.f1, updateGrad);
    }
}

2.4 Генерация модели топологии в целевой функции

Как упоминалось выше, когда это конкретное поколение вызывается в API AnnObjFunc, оно увидит, имеет ли AnnObjFunc.topologyModel значение, и если нет, то оно будет сгенерировано. Здесь модель топологии будет создана на основе слоев FeedForwardTopology.

public TopologyModel getModel(DenseVector weights) {
	FeedForwardModel feedForwardModel = new FeedForwardModel(this.layers);
	feedForwardModel.resetModel(weights);
	return feedForwardModel;
}

Модель топологии определяется следующим образом, вы можете видеть, что в ней есть определенные слои и модели слоев.

public class FeedForwardModel extends TopologyModel {
    private List<Layer> layers; //具体层 
    private List<LayerModel> layerModels; //层模型
    /**
     * Buffers of outputs of each layers.
     */
    private transient List<DenseMatrix> outputs = null;
    /**
     * Buffers of deltas of each layers.
     */
    private transient List<DenseMatrix> deltas = null;
    
    public FeedForwardModel(List<Layer> layers) {
        this.layers = layers;
        this.layerModels = new ArrayList<>(layers.size());
        for (int i = 0; i < layers.size(); i++) {
            layerModels.add(layers.get(i).createModel());
        }
    }    
}

Функция оптимизации заключается в построении модели на основе каждого слоя, например

public LayerModel createModel() {
	return new AffineLayerModel(this);
}

Давайте посмотрим на состояние конкретных слоев модели.

2.4.1 AffineLayerModel

Определение выглядит следующим образом, и его конкретные функции, такие какeval,computePrevDelta,gradМы упомянем об этом позже.

public class AffineLayerModel extends LayerModel {
    private DenseMatrix w;
    private DenseVector b;

    // buffer for holding gradw and gradb
    private DenseMatrix gradw;
    private DenseVector gradb;

    private transient DenseVector ones = null;
}

2.4.2 FuntionalLayerModel

Определение выглядит следующим образом, и его конкретные функции, такие какeval,computePrevDelta,gradМы упомянем об этом позже.

public class FuntionalLayerModel extends LayerModel {
    private FuntionalLayer layer;
}

2.4.3 SoftmaxLayerModelWithCrossEntropyLoss

Это для конечного выходного слоя.

Слой SoftmaxWithLoss состоит из двух частей: softmax и cross entropy.

Для входного вектора z softmax k-е значение его выходного вектора равно:

ak=Softmax(z)k=eZkieZia_k = Softmax(z)_k = \frac{e^{Z_k}}{\sum_i e^{Z_i}}

И функция потери перекрестной энтропии:

Loss=iyi.ln(ai)Loss = - \sum_i y_i . ln(a_i)

Взяв производную функции потерь по а, получим:

δLi=LossZi=aiyiδ_L^i = \frac{∂Loss}{∂Z_i} = a_i - y_i

Конкретный код выглядит следующим образом, что в основном является реализацией математической формулы.

public class SoftmaxLayerModelWithCrossEntropyLoss extends LayerModel
    implements AnnLossFunction {		
    @Override
    public double loss(DenseMatrix output, DenseMatrix target, DenseMatrix delta) {
        int batchSize = output.numRows();
        MatVecOp.apply(output, target, delta, (o, t) -> t * Math.log(o));
        double loss = -(1.0 / batchSize) * delta.sum();
        MatVecOp.apply(output, target, delta, (o, t) -> o - t);
        return loss;
    }
}

2.4.3 Окончательная модель

Окончательная модель выглядит следующим образом:

this = {FeedForwardModel@10575} 

     layers = {ArrayList@10576}  size = 4
          0 = {AffineLayer@10601} 
          1 = {FuntionalLayer@10335} 
          2 = {AffineLayer@10602} 
          3 = {SoftmaxLayerWithCrossEntropyLoss@10603} 

     layerModels = {ArrayList@10579}  size = 4
          0 = {AffineLayerModel@10581} 
          1 = {FuntionalLayerModel@10433} 
          2 = {AffineLayerModel@10582} 
          3 = {SoftmaxLayerModelWithCrossEntropyLoss@10583} 

     outputs = null
     deltas = null

Краткое графическое представление выглядит следующим образом (слои в FeedForwardModel опущены):

在这里插入图片描述

2.5 Генерация оптимизаторов

Пересмотрите концепцию.

Наиболее распространенной оптимизацией целевой функции является исчерпывающий метод, который перебирает все возможные веса, чтобы найти группу, которая минимизирует потери, но этот метод попадет во вложенный цикл, который значительно снижает скорость выполнения, поэтому существует устройство оптимизации. . Задача оптимизатора — быстро найти оптимальное решение целевой функции.

Назначение функции потерь — найти наиболее подходящий набор весовых последовательностей в процессе обучения, то есть свести функцию потерь к минимуму. Оптимизатор или алгоритм оптимизации используется для минимизации функции потерь, обновляя веса и смещения после каждой эпохи обучения или эпохи, пока функция потерь не достигнет глобального оптимума.

Чтобы найти минимальное значение функции потерь, мы должны сначала иметь начальный вес, сопровождаемый вычисленными потерями. Затем нам нужно подумать о самой низкой точке функции потерь, и нам нужно рассмотреть две точки: в каком направлении идти и как далеко идти.

Поскольку мы хотим, чтобы значение потерь падало быстрее всего, мы должны найти текущее значение потерь в касательном направлении функции потерь, что является самым быстрым путем. Если это на трехмерной функции потерь, это более очевидно.Точка на трехмерной плоскости будет производить бесчисленные направления как касательные, а градиент - это направление, в котором функция изменяется быстрее всего среди бесчисленных меняющихся направлений определенной точки, то есть наклона в наибольшем направлении. Градиент является направленным, а направление отрицательного градиента является направлением самого быстрого спуска.

Итак, как же работает градиентный спуск?Только сейчас мы нашли обратное направление градиента как направление нашего прогресса, а затем нам нужно только решить проблему, как далеко идти. Итак, мы вводим скорость обучения, и расстояние, которое нам нужно пройти, — это размер градиента * скорость обучения. Поскольку более оптимизированный градиент будет меньше, пройденное расстояние будет все короче и короче. Скорость обучения не должна быть слишком большой, иначе она может пропустить самую низкую точку и привести к взрыву или исчезновению градиента; она не должна быть слишком маленькой, иначе градиентный спуск может быть очень медленным.

Существует два алгоритма оптимизации:

  • Алгоритм оптимизации первого порядка. Эти алгоритмы минимизируют или максимизируют функцию стоимости, используя значения градиента, связанные с параметрами. Первая производная говорит нам, является ли функция убывающей или возрастающей в определенной точке, короче говоря, она дает касательную к поверхности.

  • Алгоритм оптимизации второго порядка. Эти алгоритмы используют вторую производную для минимизации функции стоимости, также известной как гессиан. Вторые производные используются редко из-за их высокой вычислительной стоимости. Вторая производная говорит нам, является ли первая производная возрастающей или убывающей, что представляет кривизну функции. Вторая производная дает нам квадратичную поверхность, контактирующую с кривизной поверхности ошибки.

Здесь многослойный персептрон использует оптимизатор L-BFGS для обучения нейронной сети.

Optimizer optimizer = new Lbfgs(
            data.getExecutionEnvironment().fromElements(annObjFunc),
            trainData,
            BatchOperator
                .getExecutionEnvironmentFromDataSets(data)
                .fromElements(inputSize),
            optimizationParams
        );
optimizer.initCoefWith(initCoef);

0x03 Обучение L-BFGS

Эта часть сложнее и ее нужно выносить отдельно, то есть с помощью оптимизатора тренировать.

optimizer.optimize()
  .map(new MapFunction<Tuple2<DenseVector, double[]>, DenseVector>() {
    @Override
    public DenseVector map(Tuple2<DenseVector, double[]> value) throws Exception {
        return value.f0;
    }
});

Подробности о L-BFGS можно найти в предыдущей статье.Alink's Talk (11): L-BFGS Оптимизация линейной регрессии.

Вот обзор класса Lbfgs:

public class Lbfgs extends Optimizer {
    public DataSet <Tuple2 <DenseVector, double[]>> optimize() {
       DataSet <Row> model = new IterativeComQueue()
           ......
          .add(new CalcGradient())
           ......
          .add(new CalDirection(...))
          .add(new CalcLosses(...))
           ......
          .add(new UpdateModel(...))
           ......
          .exec();
    }
}

Можно увидеть несколько ключевых шагов:

  • CalcGradient()Рассчитать градиент
  • CalDirection(...)Рассчитать направление
  • CalcLosses(...)Рассчитать потери
  • UpdateModel(...)обновить модель

Структура алгоритма в основном не изменилась, разница заключается в конкретной целевой функции и функции потерь. Например, линейная регрессия использует UnaryLossObjFunc, а функция потерь — SquareLossFunc.

Мы заполняем ключевые шаги особенностями многослойного персептрона, чтобы получить отличие от линейной регрессии.

  • CalcGradient()Рассчитать градиент
    • 1) позвонитьAnnObjFunc.updateGradient;
      • 1.1) Вызов модели топологии в целевой функцииtopologyModel.computeGradientвычислять
        • 1.1.1) Рассчитать выход каждого слоя;forward(data, true)
        • 1.1.2) Рассчитать потери выходного слоя;labelWithError.loss
        • 1.1.3) Рассчитать дельту каждого слоя;layerModels.get(i).computePrevDelta
        • 1.1.4) Рассчитать градиент каждого слоя; `layerModels.get(i).grad
  • CalDirection(...)Рассчитать направление
    • Реализация здесь не использует топологическую модель целевой функции.
  • CalcLosses(...)Рассчитать потери
    • 1) позвонитьAnnObjFunc.updateGradient;
      • 1.1) Вызов модели топологии в целевой функцииtopologyModel.computeGradientвычислять
        • 1.1.1) Рассчитать выход каждого слоя;forward(data, true)
        • 1.1.2) Рассчитать потери выходного слоя;labelWithError.loss
  • UpdateModel(...)обновить модель
    • Реализация здесь не использует топологическую модель целевой функции.

3.1 CalcGradient вычисляет градиент

Функция CalcGradient.calc вызовет функцию вычисления градиента целевой функции.

// calculate local gradient
Double weightSum = objFunc.calcGradient(labledVectors, coef, grad.f0);

Функция objFunc.calcGradient является реализацией базового класса OptimObjFunc, и здесь будет вызываться конкретная реализация AnnObjFunc.updateGradient.

updateGradient(labelVector, coefVector, grad);

3.1.1 Целевая функция

Просмотрите определение целевой функции:

public class AnnObjFunc extends OptimObjFunc {
    
    protected void updateGradient(Tuple3<Double, Double, Vector> labledVector, DenseVector coefVector, DenseVector updateGrad) {
        if (topologyModel == null) {
            topologyModel = topology.getModel(coefVector);
        } else {
            topologyModel.resetModel(coefVector);
        }
        Tuple2<DenseMatrix, DenseMatrix> unstacked = stacker.unstack(labledVector);
        topologyModel.computeGradient(unstacked.f0, unstacked.f1, updateGrad);
    }    
}

  • Во-первых, это генерация топологической модели, этот шаг упоминался ранее.

  • Послеunstacked = stacker.unstack(labledVector);, распаковать данные, вернутьreturn Tuple2.of(features, labels);.

  • Последним шагом является вычисление градиента; этоFeedForwardModelкласс реализован.

3.1.2 Расчет градиента

Рассчитать градиент (эта функция также отвечает за расчет потерь) кода вFeedForwardModel.computeGradient, общая логика следующая:

CalcGradient.calcпозвонюobjFunc.calcGradient(Реализация OptimObjFunc)

  • 1) позвонитьAnnObjFunc.updateGradient;
    • 1.1) Вызов модели топологии в целевой функцииtopologyModel.computeGradientвычислять
      • 1.1.1) Рассчитать выход каждого слоя;forward(data, true)
      • 1.1.2) Рассчитать потери выходного слоя;labelWithError.loss
      • 1.1.3) Рассчитать дельту каждого слоя;layerModels.get(i).computePrevDelta
      • 1.1.4) Рассчитать градиент каждого слоя; `layerModels.get(i).grad

код показывает, как показано ниже:

public double computeGradient(DenseMatrix data, DenseMatrix target, DenseVector cumGrad) {
        // data 是 x,target是y
        // 计算各层的输出
        outputs = forward(data, true); 
    
        int currentBatchSize = data.numRows();
        if (deltas == null || deltas.get(0).numRows() != currentBatchSize) {
            deltas = new ArrayList<>(layers.size() - 1);
            int inputSize = data.numCols();
            for (int i = 0; i < layers.size() - 1; i++) {
                int outputSize = layers.get(i).getOutputSize(inputSize);
                deltas.add(new DenseMatrix(currentBatchSize, outputSize));
                inputSize = outputSize;
            }
        }
        int L = layerModels.size() - 1;
        AnnLossFunction labelWithError = (AnnLossFunction) this.layerModels.get(L);
        // 计算损失
        double loss = labelWithError.loss(outputs.get(L), target, deltas.get(L - 1));
        if (cumGrad == null) {
            return loss; // 如果只计算损失,则直接返回。
        }
        // 计算Delta;
        for (int i = L - 1; i >= 1; i--) {
            layerModels.get(i).computePrevDelta(deltas.get(i), outputs.get(i), deltas.get(i - 1));
        }
        int offset = 0;
        // 计算梯度;
        for (int i = 0; i < layerModels.size(); i++) {
            DenseMatrix input = i == 0 ? data : outputs.get(i - 1);
            if (i == layerModels.size() - 1) {
                layerModels.get(i).grad(null, input, cumGrad, offset);
            } else {
                layerModels.get(i).grad(deltas.get(i), input, cumGrad, offset);
            }
            offset += layers.get(i).getWeightSize();
        }
        return loss;
}

3.1.2.1 Расчет выходных данных каждого слоя

Вспомним из примера кода, что мы настроили нейронную сеть так:

.setLayers(new int[]{4, 5, 3})

В частности, он вызывает функцию eval каждой модели слоя.Первый слой не так просто записать в цикл, поэтому он пишется отдельно.

public class FeedForwardModel extends TopologyModel {
    public List<DenseMatrix> forward(DenseMatrix data, boolean includeLastLayer) {
        .....
        layerModels.get(0).eval(data, outputs.get(0));
        int end = includeLastLayer ? layers.size() : layers.size() - 1;
        for (int i = 1; i < end; i++) {
            layerModels.get(i).eval(outputs.get(i - 1), outputs.get(i));
        }
        return outputs;
	}
}

Конкретные слои называются следующим образом:

AffineLayerModel.eval

Это простое аффинное преобразование WX+b, которое выводится на выходе. в

@Override
public void eval(DenseMatrix data, DenseMatrix output) {
        int batchSize = data.numRows();
        for (int i = 0; i < batchSize; i++) {
            for (int j = 0; j < this.b.size(); j++) {
                output.set(i, j, this.b.get(j));
            }
        }
        BLAS.gemm(1., data, false, this.w, false, 1., output);
}

Где w, b заданы заранее.

this = {AffineLayerModel@10581} 
 w = {DenseMatrix@10592} "mat[4,5]:\n  0.07807905200944776,-0.03040913035034301,.....\n"
 b = {DenseVector@10593} "-0.058043439717701296 0.1415366160323592 0.017773419483873353 -0.06802435221045448 0.022751460286303204"
 gradw = {DenseMatrix@10594} "mat[4,5]:\n  0.0,0.0,0.0,0.0,0.0\n  0.0,0.0,0.0,0.0,0.0\n  0.0,0.0,0.0,0.0,0.0\n  0.0,0.0,0.0,0.0,0.0\n"
 gradb = {DenseVector@10595} "0.0 0.0 0.0 0.0 0.0"
 ones = null
FuntionalLayerModel.eval

Реализация выглядит следующим образом

public void eval(DenseMatrix data, DenseMatrix output) {
        for (int i = 0; i < data.numRows(); i++) {
            for (int j = 0; j < data.numCols(); j++) {
                output.set(i, j, this.layer.activationFunction.eval(data.get(i, j)));
            }
        }
}

Переменная класса

this = {FuntionalLayerModel@10433} 
 layer = {FuntionalLayer@10335} 
  activationFunction = {SigmoidFunction@10755} 

ввод

data = {DenseMatrix@10642} "mat[19,5]:\n  0.09069152145840428,-0.4117319046979133,-0.273491600786707,-0.3638766081567865,-0.17552469317567304\n"
 m = 19
 n = 5
 data = {double[95]@10668} 

Среди них активацияFunction вызывает SigmoidFunction.eval.

public class SigmoidFunction implements ActivationFunction {
    @Override
    public double eval(double x) {
        return 1.0 / (1 + Math.exp(-x));
    }
}
SoftmaxLayerModelWithCrossEntropyLoss.eval

Здесь рассчитывается конечный результат.

    public void eval(DenseMatrix data, DenseMatrix output) {
        int batchSize = data.numRows();
        for (int ibatch = 0; ibatch < batchSize; ibatch++) {
            double max = -Double.MAX_VALUE;
            for (int i = 0; i < data.numCols(); i++) {
                double v = data.get(ibatch, i);
                if (v > max) {
                    max = v;
                }
            }
            double sum = 0.;
            for (int i = 0; i < data.numCols(); i++) {
                double res = Math.exp(data.get(ibatch, i) - max);
                output.set(ibatch, i, res);
                sum += res;
            }
            for (int i = 0; i < data.numCols(); i++) {
                double v = output.get(ibatch, i);
                output.set(ibatch, i, v / sum);
            }
        }
    }
3.1.2.2 Расчет потерь

Код:

AnnLossFunction labelWithError = (AnnLossFunction) this.layerModels.get(L);
double loss = labelWithError.loss(outputs.get(L), target, deltas.get(L - 1));
if (cumGrad == null) {
	return loss; // 可以直接返回
}

Если вам не нужно вычислять градиент, вернитесь напрямую, в противном случае продолжите. Продолжаем здесь.

В частности, функция потерь при вызове SoftmaxLayerModelWithCrossEntropyLoss — это потеря выходного слоя.

output — это выходной слой, а target — метка y. Вычислительные потери почти такие же, как и обычные, за исключением того, что они делятся на batchSize.

public double loss(DenseMatrix output, DenseMatrix target, DenseMatrix delta) {
        int batchSize = output.numRows();
        MatVecOp.apply(output, target, delta, (o, t) -> t * Math.log(o));
        double loss = -(1.0 / batchSize) * delta.sum();
        MatVecOp.apply(output, target, delta, (o, t) -> o - t);
        return loss;
}
3.1.2.3 Расчет дельты

Метод градиентного спуска должен вычислять частные производные функции потерь для параметров.Если цепное правило используется для получения частных производных каждого параметра один за другим, он включает матричное дифференцирование, и эффективность относительно низка. Поэтому алгоритм обратного распространения часто используется в нейронных сетях для эффективного вычисления градиентов. В частности, после использования алгоритма обратного распространения ошибки для вычисления члена ошибки каждого слоя можно получить градиент параметров каждого слоя.

Мы определяем дельту как скрытый слойвзвешенный вводСтепень влияния на общую ошибку, т. е. delta_i, представляет собой член ошибки нейронов в слое l, который используется для представления влияния нейронов в слое l на окончательную потерю, а также отражает чувствительность окончательной гибели нейронов l-го слоя.

img

**Приведенная выше формула является формулой обратного распространения ошибки! **Поскольку погрешность слоя l может быть рассчитана по погрешности слоя l+1. Смысл алгоритма обратного распространения ошибки следующий: член ошибки нейрона l-го слоя равен градиенту функции активации нейрона, умноженному на ошибки всех нейронов l+1-го слоя, связанных с нейроном , вес и. Здесь W — множество всех весов и смещений).

Это может быть нелегко понять. Поиск трех объяснений может помочь вам лучше понять:

  • Поскольку градиентный спускДвигайтесь в направлении, противоположном градиенту (наклону), так что мы идем назад, мы должны умножить градиент, а затем подняться обратно.
  • Или это можно рассматривать как обратный проход ошибки (дельта). После того, как линия умножается на вес линии, а после точки умножается на частную производную узла (частная производная сигмоиды является краткой).
  • Или скажем так: ошибка выходного слоя передается скрытому слою с помощью транспонированной матрицы весов, так что мы можем использовать косвенную ошибку для обновления матрицы весов, связанной со скрытым слоем. Весовая матрица также играет роль транспортировки солдат в процессе обратного распространения, но на этот раз это выходная ошибка транспорта, а не входной сигнал.

Конкретный код выглядит следующим образом

for (int i = L - 1; i >= 1; i--) {
	layerModels.get(i).computePrevDelta(deltas.get(i), outputs.get(i), deltas.get(i - 1));
}

нужно знать, это:

  • Дельта последнего слоя была рассчитана по предыдущим потерям и сохраняется в deltas.get(L - 1) через параметр функции потерь;
  • Цикл рассчитывается со второго слоя вперед;

В этом примере на выходе четыре слоя (03), дельты трехслойные (02).

Используйте значение выходного слоя 4, чтобы вычислить значение слоя 3 дельт.

Используйте значение выходного слоя 3 и значение дельта-слоя 3, чтобы вычислить значение дельта-слоя 2. Конкретное уточнение для каждого слоя:

AffineLayerModel
public void computePrevDelta(DenseMatrix delta, DenseMatrix output, DenseMatrix prevDelta) {
	BLAS.gemm(1.0, delta, false, this.w, true, 0., prevDelta);
}

Роль функции gemm такова: C := alpha * A * B + beta * C. Это может быть связано с тем, что b мало, поэтому расчет опущен. (Сомнительно, если кто знает причину, дайте знать, спасибо)

public static void gemm(double alpha, DenseMatrix matA, boolean transA, DenseMatrix matB, boolean transB, double beta, DenseMatrix matC) 

Вот оно: 1.0 * delta * this.w + 0 * prevDelta.delta не транспонируется, this.w транспонируется

FuntionalLayerModel

Здесь производная * дельта, производная - скорость изменения.

public void computePrevDelta(DenseMatrix delta, DenseMatrix output, DenseMatrix prevDelta) {
        for (int i = 0; i < delta.numRows(); i++) {
            for (int j = 0; j < delta.numCols(); j++) {
                double y = output.get(i, j);
                prevDelta.set(i, j, this.layer.activationFunction.derivative(y) * delta.get(i, j));
            }
        }
}

Функция активации представляет собой сигмовидную функцию f(x) = 1/(1 + exp(-x)).

public class SigmoidFunction implements ActivationFunction {
    @Override
    public double eval(double x) {
        return 1.0 / (1 + Math.exp(-x));
    }

    @Override
    public double derivative(double z) {
        return (1 - z) * z; // 这里
    }
}
3.1.2.4 Вычисление градиента

Здесь он вычисляется спереди назад и, наконец, накапливается в cumGrad.

int offset = 0;
for (int i = 0; i < layerModels.size(); i++) {
    DenseMatrix input = i == 0 ? data : outputs.get(i - 1);
    if (i == layerModels.size() - 1) {
    	layerModels.get(i).grad(null, input, cumGrad, offset);
    } else {
    	layerModels.get(i).grad(deltas.get(i), input, cumGrad, offset);
    }
    offset += layers.get(i).getWeightSize();
}
AffineLayerModel

Поскольку производная состоит из двух частей: w, b, то здесь нужно вычислить две части. Распаковка предназначена для распаковки, а цель упаковки состоит в том, что окончательный L-BFGS должен быть рассчитан с помощью вектора.

    public void grad(DenseMatrix delta, DenseMatrix input, DenseVector cumGrad, int offset) {
        unpack(cumGrad, offset, this.gradw, this.gradb);
        int batchSize = input.numRows();
        // 计算w
        BLAS.gemm(1.0, input, true, delta, false, 1.0, this.gradw);
        if (ones == null || ones.size() != batchSize) {
            ones = DenseVector.ones(batchSize);
        }
        // 计算b
        BLAS.gemv(1.0, delta, true, this.ones, 1.0, this.gradb);
        pack(cumGrad, offset, this.gradw, this.gradb);
    }
FuntionalLayerModel

Здесь нет расчета градиента.

public void grad(DenseMatrix delta, DenseMatrix input, DenseVector cumGrad, int offset) {}

Последняя переменная класса выглядит следующим образом:

this = {FeedForwardModel@10394} 

    layers = {ArrayList@10405}  size = 4
     0 = {AffineLayer@10539} 
     1 = {FuntionalLayer@10377} 
     2 = {AffineLayer@10540} 
     3 = {SoftmaxLayerWithCrossEntropyLoss@10541} 

    layerModels = {ArrayList@10401}  size = 4
     0 = {AffineLayerModel@10543} 
     1 = {FuntionalLayerModel@10376} 
     2 = {AffineLayerModel@10544} 
     3 = {SoftmaxLayerModelWithCrossEntropyLoss@10398} 

     outputs = {ArrayList@10399}  size = 4
      0 = {DenseMatrix@10374} "mat[19,5]:\n  0.5258035858891295,0.40832346939250874,0.4339942803542127,0.4146645474481978,0.45503123177429533..."
      1 = {DenseMatrix@10374} "mat[19,5]:\n  0.5258035858891295,0.40832346939250874,0.4339942803542127,0.4146645474481978,0.45503123177429533..."
      2 = {DenseMatrix@10533} "mat[19,3]:\n  0.31968260294191225,0.3305393733681367,0.3497780236899511..."
      3 = {DenseMatrix@10533} "mat[19,3]:\n  0.31968260294191225,0.3305393733681367,0.3497780236899511\...."
         
     deltas = {ArrayList@10400}  size = 3
      0 = {DenseMatrix@10375} "mat[19,5]:\n  0.0052001689807435756,-0.002841992490130668,0.02414893572802383."
      1 = {DenseMatrix@10379} "mat[19,5]:\n  0.02085622230356763,-0.011763437253154471,0.09830897540282763,-0.005205953747031061."
      2 = {DenseMatrix@10528} "mat[19,3]:\n  -0.6803173970580878,0.3305393733681367,0.3497780236899511\."

3.2 Направление расчета CalDirection

Реализация здесь не использует топологическую модель целевой функции.

3.3 Расчет потерь

Он напрямую войдет в класс AnnObjFunc в objFunc.calcSearchValues.

Код для расчета убытка выглядит следующим образом:

for (Tuple3<Double, Double, Vector> labelVector : labelVectors) {
    for (int i = 0; i < numStep + 1; ++i) {
		losses[i] += calcLoss(labelVector, stepVec[i]) * labelVector.f0;
    }
}

Код calcLoss AnnObjFunc выглядит следующим образом, видно, что его модель топологии вызывается для завершения расчета.

protected double calcLoss(Tuple3<Double, Double, Vector> labledVector, DenseVector coefVector) {
        if (topologyModel == null) {
            topologyModel = topology.getModel(coefVector);
        } else {
            topologyModel.resetModel(coefVector);
        }
        Tuple2<DenseMatrix, DenseMatrix> unstacked = stacker.unstack(labledVector);
        return topologyModel.computeGradient(unstacked.f0, unstacked.f1, null);
}

Здесь вызывается calculateGradient для расчета потерь, которые вернутся раньше.

@Override
public double computeGradient(DenseMatrix data, DenseMatrix target, DenseVector cumGrad) {
        outputs = forward(data, true);
        ......
        AnnLossFunction labelWithError = (AnnLossFunction) this.layerModels.get(L);
        double loss = labelWithError.loss(outputs.get(L), target, deltas.get(L - 1));
        if (cumGrad == null) {
            return loss; // 这里计算返回
        }
        ...
}

3.4 Модель обновления UpdateModel

Здесь не используется топологическая модель целевой функции.

Модель вывода 0x04

Многослойный персептрон потребляет больше памяти, чем обычные алгоритмы, мне нужно увеличить параметры запуска ВМ в IDEA для успешной работы.

-Xms256m -Xmx640m -XX:PermSize=128m -XX:MaxPermSize=512m

Здесь я хочу немного пожаловаться на Alink: при локальной отладке нет возможности изменить параметры Env, например время сердцебиения. Это делает отладку неудобной.

Алгоритм выходной модели следующий:

        // output model
        DataSet<Row> modelRows = weights
            .flatMap(new RichFlatMapFunction<DenseVector, Row>() {
                @Override
                public void flatMap(DenseVector value, Collector<Row> out) throws Exception {
                    List<Tuple2<Long, Object>> bcLabels = getRuntimeContext().getBroadcastVariable("labels");
                    Object[] labels = new Object[bcLabels.size()];
                    bcLabels.forEach(t2 -> {
                        labels[t2.f0.intValue()] = t2.f1;
                    });

                    MlpcModelData model = new MlpcModelData(labelType);
                    model.labels = Arrays.asList(labels);
                    model.meta.set(ModelParamName.IS_VECTOR_INPUT, isVectorInput);
                    model.meta.set(MultilayerPerceptronTrainParams.LAYERS, layerSize);
                    model.meta.set(MultilayerPerceptronTrainParams.VECTOR_COL, vectorColName);
                    model.meta.set(MultilayerPerceptronTrainParams.FEATURE_COLS, featureColNames);
                    model.weights = value;
                    new MlpcModelDataConverter(labelType).save(model, out);
                }
            })
            .withBroadcastSet(labels, "labels");

// 当运行时候,参数如下:
value = {DenseVector@13212} 
     data = {double[43]@13252} 
      0 = -39.6567702949874
      1 = 16.74206842333768
      2 = 64.49084799006972
      3 = -1.6630682281137472
  ......

Класс данных модели определяется следующим образом

public class MlpcModelData {
    public Params meta = new Params();
    public DenseVector weights;
    public TypeInformation labelType;
    public List<Object> labels;
}

Окончательные данные модели примерно следующие:

model = {Tuple3@13307} "
     f0 = {Params@13308} "Params {vectorCol=null, isVectorInput=false, layers=[4,5,3], featureCols=["sepal_length","sepal_width","petal_length","petal_width"]}"
      params = {HashMap@13326}  size = 4
       "vectorCol" -> null
       "isVectorInput" -> "false"
       "layers" -> "[4,5,3]"
       "featureCols" -> "["sepal_length","sepal_width","petal_length","petal_width"]"
     f1 = {ArrayList@13309}  size = 1
      0 = "{"data":[-39.65676994108487,16.742068271166456,64.49084741971454,-1.6630682163468897,-66.71571933711216,-75.86297804171262,62.609759182998204,-101.47431688844591,31.546529394499977,17.597934397561986,85.36235323961661,-126.30772079054803,326.2329896163572,-29.720070636859894,-180.1693204840142,47.70255002863321,-63.44460914025362,136.6269589647343,-0.6446457887679123,-81.86976832863223,-16.333532816181705,15.4253068036318,-11.297177263474234,-1.1338164486683862,1.3011810728093451,-261.50388539155716,223.36901758842117,38.01966001651569,231.51463912615586,-152.59659885027318,-79.02863627644948,-48.28342595225583,-63.63975869014504,111.98667709535484,153.39174290331553,-121.04900950767653,-32.47876659498367,137.82909902591624,-43.99785013791728,-93.99354048054636,42.85135076273807,-24.8725999157641,-17.962438639217815]}"
       value = {char[829]@13325} 
       hash = 0
     f2 = {Arrays$ArrayList@13310}  size = 3
      0 = "Iris-setosa"
      1 = "Iris-virginica"
      2 = "Iris-versicolor"

0xEE Личная информация

★★★★★★Думая о жизни и технологиях★★★★★★

Публичный аккаунт WeChat: мысли Росси

Если вы хотите получать своевременные новости о статьях, написанных отдельными лицами, или хотите видеть технические материалы, рекомендованные отдельными лицами, обратите внимание.

ссылка 0xFF

Введение в сети глубокой прямой связи в глубоком обучении

Глубокое изучение китайского перевода

GitHub.com/Фэн Бинчу…

Введение в глубокое обучение — Аффинный слой (аффинный слой — матричный продукт)

Машинное обучение — соответствующие формулы многослойного персептрона MLP

Авария многослойного персептрона

Нейронная сеть (многослойный персептрон) Обнаружение мошенничества с кредитными картами (1)

Ручная ИНС — Слой потерь

【Машинное обучение】Искусственная нейронная сеть ИНС

Вывод формулы для искусственной нейронной сети (ИНС)

[Глубокое обучение] [Градиентный спуск] Понимание градиентного спуска и нейронных сетей (ANN) шаг за шагом с кодом)

Подробный анализ softmax и softmax loss

Функция потерь Softmax и расчет градиента

Softmax vs. Softmax-Loss: Numerical Stability

[Технический обзор] Потеря Softmax и ее варианты в одной статье

Введение в нейронные сети с прямой связью: почему это важно?

Базовое понимание глубокого обучения: нейронные сети с прямой связью в качестве примера

Модели контролируемого обучения и регрессии

Машинное обучение — нейронные сети с прямой связью

Продукт AI: нейронная сеть с прямой связью BP и проблема градиента

Нейронные сети с прямой связью для глубокого обучения (прямое распространение и обратное распространение ошибок)