Недавно у меня возник резкий интерес к шахматным движкам, и желание разработать подобный. Писать конечно же нужно по последним веяниям моды с использованием оценки нейросети, но вот незадача, подобного опыта да и знаний у меня не имеется. Я собираюсь написать серию статей о моем тернистом пути изучения нейронных сетей. Постараюсь сразу расписывать все ответы на возникшие у меня вопросы. Основная цель - понять суть самым маленьким. Данная статья будет первой и надеюсь не последней.
Первым шагом нужно в принципе понять практическую часть работы нейронных сетей. Для достижения поставленной задачи я решил узнать основы, и написать нейронную сеть на столько простую, на сколько это возможно. Использовать я буду язык 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<float> celsiusData, std::vector<float> 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<float> celsiusData; std::vector<float> 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: "<<testCount<<"\n"<<"Degrees Fahrenheit: "<<mynn.valueFahrenheit(testCount); return 0; }
В результате получаем nan, ищем ошибку. Первое, что мне пришло в голову, это проверить значение веса и смещения при каждой итерации. Выясняется что наши вес и смещение улетают в бесконечность. После некоторых поисков я узнал, что данное явление называется взрывом градиента(Gradient Explosion) и чаще всего появляется при неправильном подборе начальных весов или скорости обучения. После добавления пары ноликов после точки в скорости обучений проблема решилась. Не буду утруждать себя слишком доскональным подбором скорости обучения и количества итераций обучения, оптимальные значения подобранные на скорую руку: learningRate = 0.00025, valueOfData = 100000. После обучения вес и смещение получили такие значения: Weight: 1.80001, Bias: 31.9994.
Попробуем повысить точность, заменив везде float на double. Это оказалось правильным решением, теперь при правильном количестве итераций вес всегда принимает значение 1.8 и смещение 32.
Весь код кому интересно:
Код
#include <iostream> #include <vector> #include <ctime> #include <cmath> 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: "<<weight<<"\n"<<"Bias: "<<bias<<"\n"; } void train(std::vector<double> celsiusData, std::vector<double> 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<double> celsiusData; std::vector<double> 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: "<<testCount<<"\n"<<"Degrees Fahrenheit: "<<mynn.valueFahrenheit(testCount)<<"\n"; mynn.printValue(); return 0; }
Теперь можно и попробовать сделать нахождение коэффициентов ��ункции . Переменную одного веса поменяем на вектор, и в тренировки добавим обновление каждого нейрона. Также теперь функция тренировки будет принимать вектор из векторов, так как у нас несколько коэффициентов. Упростим код для большей читабельности. В итоге наша функция примет такой вид:
void train(std::vector<std::vector<double>> inputValue, std::vector<double> 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 <iostream> #include <vector> #include <ctime> #include <cmath> class NeuralNetwork { private: std::vector<double> 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<std::vector<double>> inputValue, std::vector<double> 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<std::vector<double>> inputValue; std::vector<double> outputValue; double learningRate = 0.0002; int valueOfData = 70000; std::srand(std::time(nullptr)); for (int i = 0; i < valueOfData; i++) { std::vector<double> 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: "<<mynn.getWeight(0)<<"\n"<< "Weight 1: "<<mynn.getWeight(1)<<"\n"<< "Weight 2: "<<mynn.getWeight(2)<<"\n"<< "Bias: "<<mynn.getBias()<<"\n"; return 0; }
После проделанной работы у меня осталось смешанное впечатление. Это все кажется легким на первый взгляд, но это лишь вершина айсберга. Надеюсь в будущем я познаю все тонкости.
