Полностью подключенная нейронная сеть
Давайте создадим библиотеку нейронной сети с нуля. Я имею в виду, почему бы и нет? Вы можете сказать: Пфф… Большое дело.. С Python и Numpy это всего лишь вопрос часов. Что, если я скажу вам, что буду использовать C++. Нет, я шучу. Я собираюсь использовать С.
Причина этого в том, что я хочу обучить свою сеть на GPU, а GPU не понимают ни Python, ни даже C++. Мой план состоит в том, чтобы использовать OpenCL вместе с C++ для создания полнофункциональной библиотеки для создания собственной нейронной сети и ее обучения. И чтобы немного оживить его, почему бы не реализовать свёрточную нейронную сеть вместо простой, скучной полносвязной НС. Но обо всем по порядку.
Давайте не будем сразу углубляться в код ядра графического процессора. Сначала мы должны построить скелет нашей библиотеки.
OpenCL::initialize_OpenCL();
std::vector<std::vector<float> > inputs, targets;
std::vector<std::vector<float> > testinputs;
std::vector<float> testtargets;
ConvNN m_nn;
std::vector<int> netVec;
netVec = { 1024,10 };
m_nn.createFullyConnectedNN(netVec, 1, 32);
m_nn.trainFCNN(inputs, targets, testinputs, testtargets, 50000);
m_nn.trainingAccuracy(testinputs, testtargets, 2000, 1);
Хорошо, это обычный процесс каждого конвейера машинного обучения с той разницей, что вместо функций Sklearn или Tensorflow здесь у нас есть C++. Вполне достижение! Верно?
Все идет нормально. У нас есть базовая версия нашего программного обеспечения. Теперь пришло время разработать фактическую структуру нейронной сети. Базовым объектом любой NN является узел, и множество узлов, сложенных вместе, образуют слой. Вот оно:
Узел и слой
typedef struct Node {
int numberOfWeights;
float weights(1200);
float output;
float delta;
}Node;
typedef struct Layer {
int numOfNodes;
Node nodes(1200);
}Layer;
Так как это простой C, мы не можем использовать std::vector и нам нужен простой C, потому что приведенный выше код будет скомпилирован и выполнен реальным GPU. Но мы приближаемся к этому. Обратите внимание, что лучше, чем массив с предопределенной длиной, каждый раз выделять необходимое пространство в памяти, но это в другой раз.
Мы создаем наши базовые структуры для узла и слоя, поэтому пришло время запрограммировать настоящую сеть, которая представляет собой просто набор слоев.
Нейронная сеть
h_netVec = newNetVec;
Layer *inputLayer = layer(h_netVec(0), 0);
h_layers.push_back(*inputLayer);
for (unsigned int i = 1; i <h_netVec.size(); i++)
{
Layer *hidlayer = layer(h_netVec(i), h_netVec(i - 1));
h_layers.push_back(*hidlayer);
}
Вот оно. Наша простая нейронная сеть написана на C++. По сути, это не более чем вектор слоев, где каждый слой является вектором узлов. Вы можете подумать, что наша работа здесь сделана. Ха-ха! Мы даже не близки. Мы должны обучить нашу сеть на реальных данных. Это время, когда OpenCL вступает в игру.
Графический процессор не может получить доступ к этим векторам, поэтому мы должны преобразовать их в другую структуру, называемую буфером, базовым элементом OpenCL. Но логика точно такая же, как и раньше.
Буферы OpenCL
d_InputBuffer = cl::Buffer(OpenCL::clcontext, CL_MEM_READ_WRITE, sizeof(float)*inpdim*inpdim);
tempbuf = cl::Buffer(OpenCL::clcontext, CL_MEM_READ_WRITE, sizeof(Node)*h_layers(0).numOfNodes);
(OpenCL::clqueue).enqueueWriteBuffer(tempbuf,CL_TRUE,0,sizeof(Node)*h_layers(0).numOfNodes,h_layers(0).nodes);
d_layersBuffers.push_back(tempbuf);
for (int i = 1; i<h_layers.size(); i++) {
tempbuf = cl::Buffer(OpenCL::clcontext, CL_MEM_READ_WRITE, sizeof(Node)*h_layers(i).numOfNodes);
(OpenCL::clqueue).enqueueWriteBuffer(tempbuf, CL_TRUE,0, sizeof(Node)*h_layers(i).numOfNodes, h_layers(i).nodes);
d_layersBuffers.push_back(tempbuf);
}
Не запутайтесь во всех этих “cl::”, “clqueue” и “context”. Это вещи OpenCL. Логика остается неосязаемой.
Прежде чем мы погрузимся в захватывающую часть, мы должны сделать еще одну вещь. Мы должны определить ядра OpenCL. Ядра — это фактический код, который выполняется графическим процессором. Всего нам нужно 3 ядра:
- Один для прямого распространения
- Один для обратного распространения в выходном слое
- Один для отсталых в скрытом слое
ядро
compoutKern = cl::Kernel(OpenCL::clprogram, "compout");
backpropoutKern = cl::Kernel(OpenCL::clprogram, "backpropout");
bakckprophidKern = cl::Kernel(OpenCL::clprogram, "backprophid");
Ты угадал. Настала очередь GPU. Я не буду вдаваться в подробности о том, как работает OpenCL и как GPU обрабатывает данные, но кое-что нужно помнить:
- Графические процессоры имеют много ядер, поэтому они подходят для распараллеливания.
- Мы считаем, что каждое ядро запускает код для одного узла слоя.
- Когда вычисления слоя завершены, мы переходим к следующему слою и так далее.
Обратное распространение
Имейте это в виду, теперь мы можем легко понять следующий фрагмент:
kernel void compout( global Node* nodes,global Node * prevnodes,int softflag)
{
const int n = get_global_size(0);
const int i = get_global_id(0);
float t = 0;
for ( int j = 0; j < nodes(i).numberOfWeights; j++)
t += nodes(i).weights(j) * prevnodes(j).output;
t+=0.1;
nodes(i).output =sigmoid(t);
}
А для обратного распространения имеем:
kernel void backprophid(global Node* nodes,global Node * prevnodes,global Node *nextnodes,int nextnumNodes,float a)
{
const int n = get_global_size(0);
const int i = get_global_id(0);
float delta = 0;
for (int j = 0; j !=nextnumNodes; j++)
delta += nextnodes(j).delta * nextnodes(j).weights(i);
delta *= devsigmoid(nodes(i).output);break;
nodes(i).delta = delta;
for (int j = 0; j != nodes(i).numberOfWeights; j++)
nodes(i).weights(j) -= a*delta*prevnodes(j).output;
}
kernel void backpropout(global Node* nodes,global Node * prevnodes,global float* targets,float a,int softflag )
{
const int n = get_global_size(0);
const int i = get_global_id(0);
float delta=0;
delta = (nodes(i).output-targets(i))*devsigmoid(nodes(i).output);
for (int j = 0; j !=nodes(i).numberOfWeights; j++)
nodes(i).weights(j) -= a*delta*prevnodes(j).output;
nodes(i).delta=delta;
}
Если вы чувствуете себя потерянным, позвольте мне напомнить вам уравнения для алгоритма обратного распространения:
Теперь все имеет смысл, верно?
Ну вот и все. Все, что нам нужно сделать, это загрузить наши данные и запустить ядра. Я не знаю, поняли ли вы это, но мы закончили. Мы просто создаем нашу сеть Neura полностью с нуля и обучаем ее работе с GPU.
Для получения полного кода посетите мой репозиторий github: Библиотека нейронных сетей
В следующей части мы расширим библиотеку, включив в нее сверточные нейронные сети. Следите за обновлениями…
* Раскрытие информации: Обратите внимание, что некоторые из приведенных выше ссылок могут быть партнерскими ссылками, и без дополнительной оплаты для вас мы будем получать комиссию, если вы решите совершить покупку после перехода по ссылке.