Простейшая нейронная сеть, мой опыт и выводы
Недавно у меня возник резкий интерес к шахматным движкам, и желание разработать подобный. Писать конечно же нужно по последним веяниям моды с использованием оценки нейросети, но вот незадача, подобного опыта да и знаний у меня не имеется. Я собираюсь написать серию статей о моем тернистом пути изучения нейронных сетей. Постараюсь сразу расписывать все ответы на возникшие у меня вопросы. Основная цель — понять суть самым маленьким. Данная статья будет первой и надеюсь не последней.
Первым шагом нужно в принципе понять практическую часть работы нейронных сетей. Для достижения поставленной задачи я решил узнать основы, и написать нейронную сеть на столько простую, на сколько это возможно. Использовать я буду язык c++.
Максимально простой задачей будет написать нейронную сеть, которая конвертирует градусы цельсия в градусы фаренгейта. Подобная нейронная сеть будет иметь всего один вес и смещение если посмотреть на формулу . В идеале, после обучения наша нейронная сеть должна иметь вес 1.8 и смещение 32. Я буду использовать метод градиентного спуска.
Инициализируем класс хранящий нужные нам значения, в конструкторе изначальные значения поставим как душа пожелает.
class NeuralNetwork
{
private:
float weight;
float bias;
public:
NeuralNetwork(){
weight = 1.0;
bias = 1.0;
}
};
В класс добавим функцию, оценивающую возможное значение градусов фаренгейта при заданных градусах цельсия с помощью нашего веса и смещения.
float valueFahrenheit(float valueCelsius){
return valueCelsius*weight + bias;
}
Так же нам понадобится основная функция, в которой будет происходить вся магия (подбор). Она будет в себя принимать два вектора, хранящих значения градусов цельсия и соответствующие им градусы фаренгейта, а так же значение скорости обучения, которая будет определять, как быстро будут меняться вес и смещение с каждой итерацией.
В переменной result будем хранить результат работы нашей немного бессмысленной нейросети, для оценки требуемых изменений веса и смещения. В переменную error поместим разницу полученного и ожидаемого значения. Градиент веса учитывается в зависимости от величины отклонения (в нашем случае error) и входного значения celsiusData[i]. Градиент же смещения будет приравниваться только к величине ошибки. Это различие связано с тем, что вес определяет степень влияния каждого нейрона (не будем обращать внимание на то, что он у нас один), вес умножается на входное значение, и нам нужно корректировать веса, чтобы соответствовать данным обучения. С другой стороны, смещения является дополнительным параметром и не связано с входным значением. От веса и смещения отнимаем произведение нужных градиентов на скорость обучения. Отнимаем мы, а не прибавляем, так как градиент по сути показывает нам направление наискорейшего роста функции потерь, а мы стремимся как раз к обратному.
void train(std::vector celsiusData, std::vector fahrenheitData, float learningRate){
for (int i = 0; i < celsiusData.size(); i++)
{
float result = valueFahrenheit(celsiusData[i]);
float error = result - fahrenheitData[i];
float gradientWeight = error * celsiusData[i];
float gradientBias = error;
weight -= learningRate*gradientWeight;
bias -= learningRate*gradientBias;
}
}
Остается сгенерировать данные для примера.
std::srand(std::time(nullptr));
for (int i = 0; i < valueOfData; i++)
{
int value = std::rand()%200-100;
celsiusData.push_back(value);
fahrenheitData.push_back(value*1.8 + 32);
}
Несложными манипуляциями задаем нужные значения обучая нейронную сеть и проверяем ее работу.
int main(){
NeuralNetwork mynn;
std::vector celsiusData;
std::vector fahrenheitData;
float learningRate = 0.025;
int valueOfData = 10000;
std::srand(std::time(nullptr));
for (int i = 0; i < valueOfData; i++)
{
int value = std::rand()%200-100;
celsiusData.push_back(value);
fahrenheitData.push_back(value*1.8 + 32);
}
mynn.train(celsiusData,fahrenheitData,learningRate);
float testCount = 25.0;
std::cout<<"Degrees Celsius: "<
В результате получаем nan, ищем ошибку. Первое, что мне пришло в голову, это проверить значение веса и смещения при каждой итерации. Выясняется что наши вес и смещение улетают в бесконечность. После некоторых поисков я узнал, что данное явление называется взрывом градиента (Gradient Explosion) и чаще всего появляется при неправильном подборе начальных весов или скорости обучения. После добавления пары ноликов после точки в скорости обучений проблема решилась. Не буду утруждать себя слишком доскональным подбором скорости обучения и количества итераций обучения, оптимальные значения подобранные на скорую руку: learningRate = 0.00025, valueOfData = 100000. После обучения вес и смещение получили такие значения: Weight: 1.80001, Bias: 31.9994.
Попробуем повысить точность, заменив везде float на double. Это оказалось правильным решением, теперь при правильном количестве итераций вес всегда принимает значение 1.8 и смещение 32.
Весь код кому интересно:
Код
#include
#include
#include
#include
class NeuralNetwork
{
private:
double weight;
double bias;
public:
NeuralNetwork(){
weight = 1.0;
bias = 1.0;
}
double valueFahrenheit(double valueCelsius){
return valueCelsius*weight + bias;
}
void printValue(){
std::cout<<"Weight: "< celsiusData, std::vector fahrenheitData, double learningRate){
for (int i = 0; i < celsiusData.size(); i++)
{
double result = valueFahrenheit(celsiusData[i]);
double error = result - fahrenheitData[i];
double gradientWeight = error * celsiusData[i];
double gradientBias = error;
weight -= learningRate*gradientWeight;
bias -= learningRate*gradientBias;
//printValue();
}
}
};
int main(){
NeuralNetwork mynn;
std::vector celsiusData;
std::vector fahrenheitData;
double learningRate = 0.00025;
int valueOfData = 60000;
std::srand(std::time(nullptr));
for (int i = 0; i < valueOfData; i++)
{
int value = std::rand()%200-100;
celsiusData.push_back(value);
fahrenheitData.push_back(value*1.8 + 32);
}
mynn.train(celsiusData,fahrenheitData,learningRate);
double testCount = 1000.0;
std::cout<<"Degrees Celsius: "<
Теперь можно и попробовать сделать нахождение коэффициентов функции . Переменную одного веса поменяем на вектор, и в тренировки добавим обновление каждого нейрона. Также теперь функция тренировки будет принимать вектор из векторов, так как у нас несколько коэффициентов. Упростим код для большей читабельности. В итоге наша функция примет такой вид:
void train(std::vector> inputValue, std::vector outputValue, double learningRate){
for (int i = 0; i < outputValue.size(); i++)
{
double result = expectedValue(inputValue[i][0], inputValue[i][1], inputValue[i][2]);
double error = result - outputValue[i];
weight[0] -= learningRate * error * inputValue[i][0];
weight[1] -= learningRate * error * inputValue[i][1];
weight[2] -= learningRate * error * inputValue[i][2];
bias -= learningRate*error;
}
}
Обновляем генерацию данных для обучения, настраиваем скорость обучения и наслаждаемся.
Код
#include
#include
#include
#include
class NeuralNetwork
{
private:
std::vector weight;
double bias;
public:
NeuralNetwork(){
weight = {1.0,1.0,1.0};
bias = 1.0;
}
double getWeight(int value){
return weight[value];
}
double getBias(){
return bias;
}
double expectedValue(double a, double b, double c){
return a*weight[0] + b*weight[1] + c*weight[2] + bias;
}
void train(std::vector> inputValue, std::vector outputValue, double learningRate){
for (int i = 0; i < outputValue.size(); i++)
{
double result = expectedValue(inputValue[i][0], inputValue[i][1], inputValue[i][2]);
double error = result - outputValue[i];
weight[0] -= learningRate * error * inputValue[i][0];
weight[1] -= learningRate * error * inputValue[i][1];
weight[2] -= learningRate * error * inputValue[i][2];
bias -= learningRate*error;
}
}
};
double targetFunction(double a, double b, double c){
return a*7 + b*3 + c*5 + 32;
}
int main(){
NeuralNetwork mynn;
std::vector> inputValue;
std::vector outputValue;
double learningRate = 0.0002;
int valueOfData = 70000;
std::srand(std::time(nullptr));
for (int i = 0; i < valueOfData; i++)
{
std::vector input;
input.push_back((double)(std::rand()%200-100)/10);
input.push_back((double)(std::rand()%200-100)/10);
input.push_back((double)(std::rand()%200-100)/10);
inputValue.push_back(input);
outputValue.push_back(targetFunction(inputValue[i][0],
inputValue[i][1],
inputValue[i][2]));
}
mynn.train(inputValue, outputValue,learningRate);
std::cout<<"Weight 0: "<
После проделанной работы у меня осталось смешанное впечатление. Это все кажется легким на первый взгляд, но это лишь вершина айсберга. Надеюсь в будущем я познаю все тонкости.