Многослойный перцептрон

Многослойный перцептрон — разновидность нейронной сети с прямым распространением сигнала и алгоритмом обратного распространения ошибок (backpropagation) в качестве метода обучения.

О том как реализовать простейший перцептрон и пойдет речь в этой статье. Цель — реализовать данную структуру без использования специальных библиотек.

Базовая структура

Весь код представлен на языке C++ с использованием компилятора g++ версии 6.2.0.

Создадим файл с именем «MLP.h»

Так как перцептрон состоит из нескольких слоев, логично описать структуру отдельного слоя. Запишем в файле «MLP.h» следующее:

#ifndef MLP_H
#define MLP_H

#include <vector>
#include <math.h> // функция exp
#include <ctime> // для конструкции srand(time(NULL));

using namespace std;
struct Layer {
vector<double> n; // набор узлов
vector<double> w; // матрица весов связей со следующим слоем
vector<double> dw; // матрица прирощений весов
vector<double> b; // вектор смещений (bias)
vector<double> db;// вектор прирощений смещений
vector<double> d; // вектор ошибок, соответстующих узлам слоя
};

#endif

Элементами структуры Layer являются динамические массивы, размер которых будет известен после указания количества слоев и узлов в слое.

Следующая структура — сам перцептрон. Запишем перед строкой «#endif» следующий код:

struct MLP {
std::vector<Layer> layers; // слои перцептрона
std::vector<double> ts; // вектор требуемых значений выходного слоя
double ALPHA = 0.7; // момент и норма обучения
double BETHA = 0.8;
MLP(int lCount) {
layers.resize(lCount);
};

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

Загрузка и сохранение

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

void load_file(const char* fname) {
ifstream is(fname, std::ios_base::binary);
if(is) {
for(int i=0; i<layers.size(); i++) {
is.read((char*)layers[i].w.data(),
layers[i].w.size()*sizeof(double));
is.read((char*)layers[i].b.data(),
layers[i].b.size()*sizeof(double));
};
};
is.close();
};
void save_file(const char* fname) {
ofstream os(fname, std::ios_base::binary);
for(int i=0; i<layers.size(); i++) {
os.write((char*)layers[i].w.data(),
layers[i].w.size()*sizeof(double));
os.write((char*)layers[i].b.data(),
layers[i].b.size()*sizeof(double));
};
os.close();
};

Инициализация

Создадим функцию установки количества узлов в слое:

void set_layer_size(int l, int count){
layers[l].n.resize(count);
layers[l].d.resize(count);
if(l == layers.size()-1)
ts.resize(count);
};

Количество узлов выходного слоя соответствует размерности вектора требуемых значений. «l» — номер слоя (от 0 до n-1, где n — количество слоев), «count» — количество узлов в слое.

Следующая функция определяет размерности матриц весов и векторов смещений исходя из количеств узлов в слоях.

void init() {
srand(time(NULL));
Layer *tL0, *tL1;
for(int i=0; i<layers.size()-1; i++) {
tL0 = &layers[i];
tL1 = &layers[i+1];
tL0->b.resize(tL1->n.size(), 0);
tL0->db.resize(tL1->n.size(), 0);
tL0->w.resize(tL0->n.size()*tL1->n.size());
for(int j=0; j<tL0->w.size(); j++) tL0->w[j] = ((double)(rand() % tL0->n.size()) + 1) / (10*tL0->n.size());
tL0->dw.resize(tL0->n.size()*tL1->n.size(), 0.0);
};
};

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

Расчет ошибки

Построим функцию расчета ошибки:

double calcEps() {
double sum = 0;
Layer& lL = layers[layers.size()-1];
for(int i=0; i<lL.n.size(); i++)
sum += (lL.n[i] - ts[i])*(lL.n[i] - ts[i]);
return sqrt(sum)/sqrt(lL.n.size());
};

Здесь ошибка — это нормированное значение корня из суммы квадратов разностей полученных и требуемых значений. Область значений функции — [0, 1].

Ввод и вывод

Следующие функции предназначены для ввода и вывода данных соответственно.

void set_input(double* src) {
for(int i=0; i<layers[0].n.size(); i++)
layers[0].n[i] = src[i];
};

void get_output(double* dest) {
Layer& tL = layers[layers.size()-1];
for(int i=0; i<tL.n.size(); i++)
dest[i] = tL.n[i];
};

В качестве аргументов выступают указатели на область памяти, соответствующую массиву входных (выходных) значений.

Функция активности

Функцией активности нейрона будем считать сигмоид.

double sigm(double x) {
return 1/(1+exp(-x));
};

Прямое распространение сигнала

На данном этапе мы определили все необходимые функции для реализации алгоритма прямого распространения сигнала:

void forwrd() {
Layer *tL0, *tL1;
for(int i=0; i<layers.size()-1; i++) {
tL0 = &layers[i];
tL1 = &layers[i+1];
for(int j=0; j<tL1->n.size(); j++) {
double sum;
sum = 0;
for(int k=0; k<tL0->n.size(); k++)
sum += tL0->n[k] * tL0->w[j*tL0->n.size()+k];
sum += tL0->b[j];
tL1->n[j] = sigm(sum);
};
};
};

Функция установки требуемых значений:

void set_ts(double* src) {
for(int i=0; i<ts.size(); i++)
ts[i] = src[i];
};

Расчет дельт

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

void calcDelta() {
Layer* lastLayer = &layers[layers.size()-1];
for(int i=0; i<lastLayer->d.size(); i++)
lastLayer->d[i] = (lastLayer->n[i] - ts[i]) * lastLayer->n[i] * (1 - lastLayer->n[i]);
for(int i=layers.size()-2; i>=0; i--) {
Layer *tL0, *tL1;
double sum;
tL0 = &layers[i];
tL1 = &layers[i+1];
for(int k=0; k<tL0->d.size(); k++) {
sum = 0;
for(int j=0; j<tL1->d.size(); j++)
sum += tL0->w[j*tL0->d.size()+k]*tL1->d[j];
tL0->d[k] = sum * tL0->n[k] * (1 - tL0->n[k]);
};
};
};

Алгоритм обратного распространения ошибки описан во многих источниках, поэтому останавливаться на нем нет смысла. Следует только отметить, что необходимые производные рассчитываются для выходного слоя и, затем, для всех предыдущих в обратном порядке.

Корректирвка весов

Заключительная часть — это функция корректировки весов сети.

void backwrd() {
calcDelta();
for(int i=layers.size()-2; i>=0; i--) {
Layer *tL0, *tL1;
tL0 = &layers[i];
tL1 = &layers[i+1];
for(int k=0; k<tL0->n.size(); k++)
for(int j=0; j<tL1->n.size(); j++) {
tL0->dw[j*tL0->n.size()+k] = BETHA*tL0->dw[j*tL0->n.size()+k] - ALPHA*tL1->d[j]*tL0->n[k];
tL0->w[j*tL0->n.size()+k] += tL0->dw[j*tL0->n.size()+k];
};
for(int j=0; j<tL1->n.size(); j++) {
tL0->db[j] = BETHA*tL0->db[j] - ALPHA*tL1->d[j];
tL0->b[j] += tL0->db[j];
};
};
};

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

Добавить комментарий

Ваш e-mail не будет опубликован.