Краткое содержание

Данная статья описывает работу пакета nnhelper, предназначенного для создания и использования нейронных сетей в программах на языке Go.

Если Вы уже знакомы с машинным обучением и используете его в своей работе, то эта статья и описанные в ней примеры могут показаться вам слишком простыми. Если Вы в начале пути и хотите познакомиться с этой темой или вам хотелось бы научиться использовать нейроматрицу в ваших программах на языке Go, то вы попали точно по адресу.

Go пакет nnhelper разработан для быстрого создания нейронной сети и использовании ее в приложениях, написанных на языке Go. Для использования nnhelper не потребуется ничего дополнительного, кроме Go. Пакет nnhelper является надстройкой над пакетом gonn. И это единственная внешняя зависимость.

Нейронная сеть (нейроматрица)

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

Нейроматрица - это массивы значений с плавающей запятой, позволяющие дать нужные выходные значения в зависимости от входных значений. На вход нейроматрицы подаются известные нам значения, а на выходе выдаются ожидаемые нами результаты.

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

У нас два входных параметра и два выходных параметра. На вход подаем два числа со знаком плюс или минус. На выходе получаем два числа со значениями от 0 до 1. Если первое значение близко к 1, то результат Плюс, если второе значение близко к 1, то результат Минус. Т.е. на вход мы подаем реальные значения, а на выходе получаем массив результатов. Выбираем самое большое значение из массива и считаем это ответом.

Входные данные:			   		Результаты (1ый  – это Плюс, 2ой – это минус):
 1, 1  – плюс * плюс   		1, 0 – плюс
 1,-1  – плюс * минус  		0, 1 – минус
-1, 1  – минус* плюс   		0, 1 – минус
-1,-1  – минус* минус  		1, 0 – плюс

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

Перейдём к программированию

Хватит слов и теории. Посмотрим, как это выглядит на практике.

Создадим папку для проекта. В ней создадим три файла:

main.go 
sam03_inp.csv 
sam03_tar.csv

В файлах *.csv разместим наши входные и выходные данные:

sam03_inp.csv

1,1
1,-1
-1,1
-1,-1

sam03_tar.csv

1,0
0,1
0,1
1,0

В файле main.go создадим функцию main и напишем код:

// Константы с именами файлов наших данных и самой матрицы
const (
    SAM03_NN  = "sam03.nn"
    SAM03_INP = "sam03_inp.csv"
    SAM03_TAR = "sam03_tar.csv"
)

// Массив строк с понятными ответами для вывода на консоль
humanAnswers := []string{"Plus", "Minus"}

// Создаем матрицу с 2-я входами и 2-я выходами, из csv файлов, 
// если марица отсутствует на диске
if _, err := os.Stat(SAM03_NN); errors.Is(err, os.ErrNotExist) {
        log.Println("Create", SAM03_NN, "neural network")
        nnhelper.Create(2, 4, 2, false, SAM03_INP, SAM03_TAR, SAM03_NN, true)
}

// Загружаем матрицу из файла
nn := nnhelper.Load(SAM03_NN)

// Предыдущий оператор создал матрицу и теперь мы ее используем/тестируем: 
// создаем массив с данными для которых хотим получить результат и для этого,
// в цикле, выполняем функции nn.Answer и nn.AnswerToHuman
const (
        PLUS  = 1.0
        MINUS = -1.0
    )

    // Intput array for testing
    in := [][]float64{
        {PLUS, PLUS},   // Plus * Plus = Plus
        {PLUS, MINUS},  // Plus * Minus = Minus
        {MINUS, PLUS},  // Minus * Plus = Minus
        {MINUS, MINUS}, // Minus * Minus = Plus
        {3000, -0.001}, // Minus * Plus = Minus
    }
    for i := range in {
        out := nn.Answer(in[i]...)
        answer, _ := nn.AnswerToHuman(out, humanAnswers)
        fmt.Println(in[i], answer, out)
    }

Полный текст этого примера и файлы с данными находятся в папке examples/sam03

Запускаем пример на выполнение:

go run .

И получаем результаты:

[1 1] Plus [0.9944239772210877 0.005449692189449571]
[1 -1] Minus [0.006860785779850435 0.9935960167863507]
[-1 1] Minus [0.005651009980489101 0.994384581174021]
[-1 -1] Plus [0.9944591181959666 0.005221796400203198]
[3000 -0.001] Minus [0.005445102841471242 0.9960123783099599]

В результатах видим (см. первую строку):

  • наши исходные данные: [1,1]

  • результат переведенный в понятный вид:  Plus

  • результат полученный с выходов матрицы: [0.9944239772210877 0.005449692189449571]

Обратите внимание на последнюю строчку результатов. Наша матрица смогла дать правильный ответ на «неизвестные» входные данные. В этом и есть весь вкус нейронных сетей. Матрица дает ответы не только на входные данные, которым была обучена, но и на другие неизвестные ей входные параметры.

Что бы увидеть процесс обучения нейронной сети, нужно удалить файл sam03.nn и запустить пример.

Ну, пожалуй, на этом я закончу. Очень давно мечтал написать понятное и простое объяснение о том, как можно использовать нейронные сети в программировании. Надеюсь, что у меня это получилось.

В пакете есть ещё два примера:

  • sam02: На вход подаем время в 24-часовом формате и получаем ответ: Утро, Вечер, День или Ночь;

  • sam01: Пример матрицы для получения реакции игрового бота. На вход подается количество здоровья, наличие оружия, количество врагов, а на выходе получаем ответ на вопрос «что делать»: атаковать, красться, убегать или ничего не делать. 

Пакет размещен на Github:
https://github.com/kirill-scherba/nnhelper

С уважением,
Kirill Scherba

Комментарии (7)


  1. SemyonSinchenko
    08.05.2022 08:51
    +2

    А какой смысл в этой надстройке, если сам gonn заброшен и последний коммит там 9 лет назад?


    1. kirill-scherba Автор
      08.05.2022 11:56
      -3

      Я бы сказал не заброшен, а закончен. А Вас не смущает, что таблица умножения не обновлялась уже более 2-х тысяч лет и все равно ей продолжают пользоваться?


      1. SemyonSinchenko
        08.05.2022 12:08
        +2

        не заброшен, а закончен

        Закончен, но с TODO большими буквами на главной странице README?

        Ну и просто для справки, помимо описанных в gonn

        BackPropagation Network / RBF Network / Perceptron Network

        Есть еще много чего еще, например, Convolutional NN (1d, 2d, 3d, etc.), Recurrent NN (rnn, lstm, gru, etc.), Graph NN (gcnn, graphsage, etc.), Transformers и т.д. Конференции типа NIPS, где каждый год представляют что-то новое, являются одними из самых посещаемых... Так что сравнение про таблицу умножения звучит странно.


        1. kirill-scherba Автор
          08.05.2022 14:31
          -1

          Семен, я полностью с вами согласен, что в области машинного обучения очень много нового, и очень много людей этим интересуется. Я не хотел вас обидеть своим комментарием про "таблицу умножения" :-)

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

          Мне кажется, что если мы свалим на человека кучу непонятных терминов и названий, то он отложит эту тему ещё на год :-)

          Пакет gonn хорошо справляется с описанной в стате задачами, шустро работает, имеет небольшой размер и не тянет за собой зависимостей.

          Мне кажется, что читателю, с помощью nnhelper, будет легко и удобно начать использовать нейросети в своих проектах на Go. В случае необходимости всегда можно взять что-то более сложное.

          Мне удобно пользоваться nnhelper, я делюсь этим удобством с читателем.

          Спасибо!


  1. DmitriyTitov
    08.05.2022 12:13
    +1

    Тема интересная, но мне бы хотелось чуть больше вводной информации. Не подскажете какой-нибудь толковый источник по нейросетям для чайников?
    Посмотрел ваш пакет, и могу сказать, что даже если у вас всё ОК по части нейросетей, то сам пакет оставляет желать лучшего.

    Например,

    // ReadCsvFile read csv file and return float array
    func ReadCsv(filePath string) (out [][]float64, err error) {
    	csv, err := ReadCsvFile(filePath)
    	if err != nil {
    		return
    	}

    Если была ошибка при чтении файла, тут вместо неё вернётся nil.
    В вызывающем коде похожая ситуация:

    	input, _ := ReadCsv(inpupCsv)
    	if len(print) > 0 && print[0] {

    Ошибка вообще игнорируется.
    Надо работать над качеством!


    1. kirill-scherba Автор
      08.05.2022 14:02
      -1

      У меня нет под рукой ссылки на что-то совем вводное по нейросетям. Мне кажется эта информация доступна в сети. Если мне попадётся что-то простое, понятное и с вводной информацией, я обязательно Вам пришлю ссылку.

      Извените, Дмитрий, но я не вижу ошибки, о которой вы говорите, в функции "ReadCsvFile read csv file and return float array". Вот, добавил к проекту тест этой функции в файле csv_test.go:

      // Reading existing csv file
      func TestReadCsvFile(t *testing.T) {
      	_, err := ReadCsvFile("examples/sam01/sam01_inp.csv")
      	if err != nil {
      		t.Errorf("Can't read csv file, error = %s; want nil", err.Error())
      		return
      	}
      }
      
      // Reading non-existing csv file
      func TestReadCsvFile2(t *testing.T) {
      	_, err := ReadCsvFile("examples/sam01/sam01_inp_notexists.csv")
      	if err == nil {
      		t.Errorf("Does not return error when read not existing csv file, "+
      			"error = %v", err)
      		return
      	}
      }

      И оба теста проходят. Первый тест читает существующий файл, второй тест читает несуществующий файл.

      Спасибо.


      1. DmitriyTitov
        08.05.2022 21:45

        Да, код некрасивый, но конкретно в той части рабочий.

        В любом случае, пустой return который возвращает массив, определённый в заголовке, и затенённую ошибку - это классический пример плохого кода на Go.

        Хотя с учётом игнорирования ошибки в вызывающем коде, это, конечно, и не важно.