0x00 сводка
Alink — это платформа алгоритмов машинного обучения нового поколения, разработанная Alibaba на основе вычислительного движка реального времени Flink, и первая в отрасли платформа машинного обучения, которая поддерживает как пакетные, так и потоковые алгоритмы. Эта статья и предыдущая статья приведут вас к анализу реализации многослойного персептрона в Alink.
Поскольку общедоступная информация Alink слишком мала, нижеследующее - все предположения, и обязательно будут упущения и ошибки. Я надеюсь, что все укажут, и я обновлю ее в любое время в будущем.
0x01 предыдущий обзор
раньшеALink Talk (14): Общая архитектура многослойного персептронаМы поняли концепцию многоуровневого персептрона и общую архитектуру Alink Теперь мы начнем знакомить с тем, как ее оптимизировать.
1.1 Основные понятия
Давайте еще раз рассмотрим основные понятия:
-
Вход нейрона: аналогично линейной регрессии z = w1x1+ w2x2 +⋯ + wnxn = wT x (линейная пороговая единица (LTU)).
-
Выход нейрона: функция активации, аналогичная бинарной классификации, моделирует, что нейроны в биологии имеют только два состояния возбуждения и торможения. Увеличьте значение смещения, какой узел в выходном слое имеет наибольший вес и какой из них выводится.
-
Используя критерий Хебба, следующий метод корректировки веса относится к текущему весу и тренировочному эффекту.
-
Как научиться автоматически вычислять веса - обратное распространение, то есть сначала сделать прямой расчет, а затем сделать расчет ошибки в соответствии с текущим выходом, как направляющий сигнал, чтобы обратно настроить выходной вес предыдущего слоя, чтобы он попал в разумный диапазон и неоднократно настраиваться на первый слой, каждый раунд настройки имеет скорость обучения, после настройки сеть становится все более и более разумной.
1.2 Алгоритм обратного распространения ошибки
Процесс обучения нейронной сети с прямой связью на основе алгоритма обратного распространения ошибки (BP) можно разделить на следующие три этапа:
- Вычислить чистый вход z(l) и значение активации a(l) каждого слоя во время прямого распространения до последнего слоя;
- (Этап обратного распространения) Различает отклик возбуждения с целевым выходом, соответствующим обучающему входу, чтобы получить ошибку отклика скрытого слоя и выходного слоя. Вычислить член ошибки δ(l) для каждого слоя, используя обратное распространение ошибки;
- Веса в каждом синапсе обновляются следующим образом: умножьте входные ошибки возбуждения и отклика, чтобы получить градиент весов; умножьте этот градиент на шкалу, инвертируйте и добавьте к весам. То есть вычисляются частные производные параметров каждого слоя и обновляются параметры.
Это соотношение (в процентах) будет влиять на скорость и эффективность тренировочного процесса, отсюда и название «тренировочный фактор». Направление градиента указывает направление расширения ошибки, поэтому его необходимо изменить на противоположное при обновлении веса, тем самым уменьшив ошибку, вызванную весом.
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-е значение его выходного вектора равно:
И функция потери перекрестной энтропии:
Взяв производную функции потерь по а, получим:
Конкретный код выглядит следующим образом, что в основном является реализацией математической формулы.
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
- 1.1.1) Рассчитать выход каждого слоя;
- 1.1) Вызов модели топологии в целевой функции
- 1) позвонить
-
CalDirection(...)
Рассчитать направление- Реализация здесь не использует топологическую модель целевой функции.
-
CalcLosses(...)
Рассчитать потери- 1) позвонить
AnnObjFunc.updateGradient
;- 1.1) Вызов модели топологии в целевой функции
topologyModel.computeGradient
вычислять- 1.1.1) Рассчитать выход каждого слоя;
forward(data, true)
- 1.1.2) Рассчитать потери выходного слоя;
labelWithError.loss
- 1.1.1) Рассчитать выход каждого слоя;
- 1.1) Вызов модели топологии в целевой функции
- 1) позвонить
-
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
- 1.1.1) Рассчитать выход каждого слоя;
- 1.1) Вызов модели топологии в целевой функции
код показывает, как показано ниже:
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-го слоя.
**Приведенная выше формула является формулой обратного распространения ошибки! **Поскольку погрешность слоя 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
Введение в сети глубокой прямой связи в глубоком обучении
Глубокое изучение китайского перевода
Введение в глубокое обучение — Аффинный слой (аффинный слой — матричный продукт)
Машинное обучение — соответствующие формулы многослойного персептрона MLP
Авария многослойного персептрона
Нейронная сеть (многослойный персептрон) Обнаружение мошенничества с кредитными картами (1)
【Машинное обучение】Искусственная нейронная сеть ИНС
Вывод формулы для искусственной нейронной сети (ИНС)
Подробный анализ softmax и softmax loss
Функция потерь Softmax и расчет градиента
Softmax vs. Softmax-Loss: Numerical Stability
[Технический обзор] Потеря Softmax и ее варианты в одной статье
Введение в нейронные сети с прямой связью: почему это важно?
Базовое понимание глубокого обучения: нейронные сети с прямой связью в качестве примера
Модели контролируемого обучения и регрессии
Машинное обучение — нейронные сети с прямой связью
Продукт AI: нейронная сеть с прямой связью BP и проблема градиента