Всем привет. Меня зовут Алмаз Хуснутдинов. Я делаю материалы, связанные с созданием ИИ. В этой статье я начну рассказывать про идею о некой системе команд, в основе которой лежит идея о детерминированности работы любой системы.
Содержание: Описание задачи из этой статьи, множественная логистическая регрессия, токенизация букв и слов, классификация слов, классификация команд, распознавание и выполнение команд.
Описание задачи, которую я рассматриваю в этой статье.
Нужно сделать программу, которая сможет выполнять какие-то действия на основе входных данных. В качестве входных данных будем рассматривать текст. В качестве действий будем рассматривать выполнение ветвлений (вызов функции), которые будут выполняться на основе прогноза модели, коротая обрабатывает входной текст.
Программа может получать входные данные через строку ввода. Затем она должна обработать текст и распознать команду, которую ввел пользователь. Данные могут быть как-то искажены, могут не представляться в том, виде, в котором система обучалась их получать.
Для того, чтобы правильно распознавать команды, необходимо как-то их аппроксимировать, кроме того, также необходимо аппроксимировать сами слова. Для этого будет использоваться множественная логистическая регрессия. Под аппроксимацией я понимаю именно то, что это означает, либо обобщение, как это обычно называют.
После того, как модель обработает текст она выводит список степени соответствия входных данных каждому возможному действию. Происходит выделение наибольшего из возможных вариантов ответа. Далее на основе этого варианта выполняется соответствующее ветвление и вызов соответствующей функции.
Я использую логистическую регрессию чисто интуитивно, этот выбор ничем не обоснован.
У меня есть туториал по логистической регрессии, здесь я расскажу только про то, как сделать множественную. Она представляет собой список из обычных моделей логистической регрессии, только каждая из них обучается классифицировать какой-то отдельный класс, а все остальные не классифицировать.
На вход этой модели подается список признаков, которые описывают объект. Каждый признак представляет собой число — количество данного признака в объекте, также признак может быть представлен в бинарном виде — присутствует или отсутствует, но это зависит от конкретной задачи.
На выходе модель должна выводить вероятность того, что данный на вход объект принадлежит к определенному классу. Каждая отдельная модель логистической регрессии делает прогноз того, что объект принадлежит к классу, который она обучена распознавать.
Код класса.
class MultyRegression:
def __init__(self, in_features, out_features):
self.logr_list = [LogisticRegression(in_features) for _ in range(out_features)]
def forward(self, input_features):
return [logr.forward(input_features) for logr in self.logr_list]
def fit(self, inputs, targets, epochs=100, lr=0.1):
for epoch in range(epochs):
for i in range(len(inputs)):
outputs = self.forward(inputs[i])
for j, logr in enumerate(self.logr_list):
logr.train(inputs[i], outputs[j], targets[j][i], len(inputs), lr)
В метод init нужно передать число входных и выходных признаков. Внутри создается список с моделями логистической регрессии. Каждая из них создается с одинаковым числом входных признаков. Число моделей равно числу выходных признаков (классов).
В методе forward создается список из ответов каждой модели. Каждой модели на вход передается один и тот же входной вектор.
В метод fit передается список входных векторов и целевых. В каждой итерации обучения нужно вычислить ответ — список ответов от всех моделей. После этого нужно вызвать у каждой модели метод train. В качестве целевого значения передается число, то есть единица или ноль, которое каждая модель должна выводить на данный inputs[i].
Далее пример использования.
inputs = [[0, 1], [0, 0], [0, 0], [0, 0], [1, 0], [1, 1]] # input data
targets = [[1, 0], [0, 1], [0, 1], [0, 1], [1, 0], [1, 0]] # target data
targets = transpose_matrix(targets)
model = MultyRegression(2, 2)
model.fit(inputs, targets, epochs=100, lr=0.8)
for i, inp in enumerate(inputs):
print(model.forward(inp), targets[0][i], targets[1][i])
Это набор данных для задачи «логическое или». На входе два числа, на выходе два класса (0 или 1). Матрицу целевых векторов нужно транспонировать, это из-за особенности реализации — в методе fit в том месте, где вызывается метод train для каждой модели.
Для того, чтобы работать с буквами и словами, нужно их представлять в виде векторов признаков. Самый базовый метод сделать это называется «one hot encoding». Вектор представляется в виде нулей и одной единицы в том элементе, который соответствует букве или слову. Так обычно делается для обработки последовательностей букв или слов.
В этом примере будем использовать способ под названием «мешок слов». Токены кодируются так же, только все векторы одного слова или предложения суммируются. То есть слово или предложение представляется в виде одного вектора из нулей, на местах определенных элементов будут числа, которые обозначают количество этой буквы или этого слова в последовательности. Например, последовательность «ппрривет», в ней 2 буквы (токена) «п», 1 буква (токен) «и» и так далее, остальные токены не встречаются в этой последовательности, поэтому на их местах стоят нули.
Слова представляются аналогично буквам, только их намного больше, поэтому их представляют в виде сжатых векторов признаков (embeddings), но в этом примере без ембедингов. Я не буду учитывать знаки препинания, если их учитывать, то возможно лучше будет рассматривать их как слова, а не как символы (буквы).
Для токенизации создадим отдельный класс, чтобы делегировать на него всю неудобную работу.
class Tokenizer:
def __init__(self, token_list):
self.token_to_idx = {token: i for i, token in enumerate(token_list)}
self.idx_to_token = {i: token for i, token in enumerate(token_list)}
self.tokens_num = len(token_list)
В метод init нужно передать список всех уникальных токенов, то есть букв или слов. В нем создаются 2 словаря, которые создают соответствие между индексом токена и его значением, чтобы можно было легко преобразовать токен в индекс и наоборот.
Метод token_to_ohe представляет токен в виде вектора из нулей и одной единицы на месте элемента, который соответствует индексу токена. Таким образом создаем целевой вектор для слова (или буквы), нужно просто указать 1 в соответствующем элементе выходного вектора в индексе данного слова (буквы). Метод vecto_to_token делает обратную операцию.
def token_to_ohe(self, token):
vector = [0 for _ in range(self.tokens_num)]
idx = self.token_to_idx[token]
vector[idx] = 1
return vector
def vector_to_token(self, vector):
idx = numpy.argmax(vector)
token = self.idx_to_token[idx]
return token
Метод sequence_to_vector представляет последовательность токенов в виде одного вектора «мешок слов». Таким образом можно создавать вектор представления слова. Создаем вектор из нулей и для каждой буквы (или слова) прибавляем 1 в соответствующем элементе вектора по индексу буквы.
def sequence_to_vector(self, sequence):
vector = [0 for _ in range(self.tokens_num)]
for item in sequence:
idx = self.token_to_idx[item]
vector[idx] += 1
return vector
Этот класс можно использовать для букв и для слов.
На вход могут подаваться искаженные данные, то есть слова с опечатками. На основе этой логистической регрессии можно распознавать слова, если они введены не правильно. Если какие-то слова классифицированы не правильно, то сама команда может быть распознана не правильно (это нужно учитывать, в этом примере я не сделал эту проверку).
Перед тем, как распознавать команду из слов, нужно распознать сами слова. Каждое слово состоит из нескольких букв, буквы рассматриваем как признаки объектов (слов). Всего будет 33 буквы, значит признаков тоже 33, дополнительно можно использовать другие символы, такие как знаки препинания.
На вход модели для распознавания слов будет подаваться вектор, который содержит информацию о введенном слове. Каждая буква имеет свой порядковый номер (индекс) в списке всех признаков. Нужно создать вектор из нулей с числом элементов 33. Для каждой буквы в слове нужно увеличить соответствующий элемент в этом векторе на 1. То есть в векторе указывается количество каждой буквы в слове.
Рассмотрим модель для распознавания слов. Ей нужно передать все буквы и уникальные слова в наборе данных. Для букв и слов создаем токенайзеры, они отвечают за представление букв и слов. Создаем модель множественной логистической регрессии. Обучаться она будет отдельно.
class WordModel:
def __init__(self, letters, unique_words):
self.letter_tokenizer = Tokenizer(letters)
self.words_tokenizer = Tokenizer(unique_words)
self.model = MultyRegression(len(letters), len(unique_words))
def fit(self, inputs, targets, epochs=100, lr=0.1):
self.model.fit(inputs, targets, epochs, lr)
def get_input(self, word):
return self.letter_tokenizer.sequence_to_vector(word)
def get_target(self, word):
return self.words_tokenizer.token_to_ohe(word)
def forward(self, inp):
return self.model.forward(inp)
Метод get_input преобразует строку в входной вектор признаков. Метод get_target преобразует слово в целевой вектор признаков. Эти методы обеспечивают небольшой функционал, с помощью которого можно подготавливать слова к тому, чтобы обработать их моделью.
Функция, чтобы преобразовывать обучающие данные. На вход передается список слов и список меток. Список целевых векторов нужно транспонировать, так как это особенность реализации множественной логистической регрессии.
def make_words_data(word_model, input_words, target_words):
inputs = [word_model.get_input(word) for word in input_words]
targets = [word_model.get_target(word) for word in target_words]
targets = transpose_matrix(targets)
return inputs, targets
Функция для проверки вывода модели после обучения. Выводится слово, которое подается на вход, выход модели и целевой вектор (целевые векторы нужно сначала транспонировать обратно, чтобы вывести их корректно).
def print_output(model, items_list, inputs, targets):
print("=========================")
targets = transpose_matrix(targets)
for i, inp in enumerate(inputs):
print(items_list[i], model.forward(inp), targets[i])
Создаем список всех символов. Потом нужно сделать список всех слов, например, загрузить их из файла со словами, для простоты я просто сделал инициализацию прямо в файле. Также необходимо указать метки для каждого слова. Потом создаем список уникальных слов и их число. Так же делаем тестовые данные.
letters = "абвгдеёжзийклмнопрстуфхцчщшъыьэюя"
words_list = ["привет", "пока", "покаа", "как", "каак", "дела", "сделай"]
target_words = ["привет", "пока", "пока", "как", "как", "дела", "сделай"]
test_words = ["првт", "пка", "поа"]
test_target_words = ["привет", "пока", "пока"]
unique_words = list(set(target_words))
Теперь можно создать обучающий и тестовый наборы данных, обучить модель и проверить ее.
inputs, targets = make_words_data(word_model, words_list, target_words)
test_inputs, test_targets = make_words_data(word_model, test_words, test_target_words)
word_model = WordModel(letters, unique_words)
word_model.fit(inputs, targets, epochs=100, lr=0.8)
print_output(word_model, words_list, inputs, targets)
print_output(word_model, test_words, test_inputs, test_targets)
output:
=========================
привет [0.005118926577732819, 0.9914886883246045, 0.0024402194981650084,
0.00696090047348459, 0.0012475341272907671] [0, 1, 0, 0, 0]
…
сделай [0.009077970566943741, 0.010123286193314486, 0.8890793203926629,
0.10124257506319338, 0.00636310358780177] [0, 0, 1, 0, 0]
=========================
првт [0.07809694837775111, 0.9703325366717745, 0.01052546174740245,
0.015548304709811485, 0.005337346636761862] [0, 1, 0, 0, 0]
…
поа [0.9782841249308825, 0.10765944142494452, 0.020116303139164023,
0.037643814697091084, 0.002395138327556411] [1, 0, 0, 0, 0]
Набор данных выглядит примерно так. Сначала выводится список целевых векторов, потом список входных векторов.
print(targets)
print(inputs)
output:
[[0, 0, 0, 1, 1, 0, 0, 0, 0],
…
[0, 0, 0, 0, 0, 0, 0, 0, 1]]
[[0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
…
[1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
В целевых векторах каждая единица обозначает нужно ли распознать данное слово или не нужно для каждой отдельной логистической регрессии. Матрица целевых векторов транспонирована, поэтому тут каждый вектор представлен для каждой отдельной модели, то есть первый вектор для первой модели, последний — для последней. Всего 9 примеров, для каждого из 9 примеров каждая модель должна распознать как 0 или как 1. Всего тут выведено 5 целевых векторов, так как всего 5 слов, и всего 5 моделей единичных регрессий.
Количество входных векторов тут выведено 9 (число элементов в каждом составляет 33). Во входных векторах представлено число каждой буквы в слове. Например, в первом векторе отображена единица в позиции буквы «в», потом «е» и так далее.
Теперь можно обработать вектор слова моделью распознавания слов и получить ответ модели. Метод predict получает на вход слово, обрабатывает его моделью и выводит распознанное слово. На выходе класс распознанного слова. Также можно посмотреть и вероятность, она в переменной out.
def predict(self, word):
inp = self.get_input(word)
out = self.forward(inp)
word = self.words_tokenizer.vector_to_token(out)
return word
print(word_model.predict("прввт"))
output:
привет
Можно оценивать влияние каждой буквы в слове на то, как будет распознано слово. Это можно узнать посмотрев на веса после обучения. Например, слова "пка" и "поа" распознаются по-разному, первое распознается на 0.46, а второе на 0.98 как слово «пока». Но это зависит от обучающих данных.
Далее делаем то же самое для распознавания последовательности слов. Теперь вместо букв будут слова. Соответственно, нужно сделать список уникальных слов, для того, чтобы представлять каждую команду в виде вектора. Каждое слово в команде рассматриваем как признак, также учитываем количество одинаковых слов, как и букв («мешок слов»).
Команды представлены в виде словаря, ключ словаря — команда, значение — метка команды. Также добавляем новые слова и указываем их метки.
commands = {
"привет": "привет",
"как дела": "как дела",
"пока": "отключение",
"отключить": "отключение",
"выйти": "отключение",
"изменить переменную": "изменить переменную"
}
words_list += ["отключить", "выйти", "изменить", "переменную"]
target_words += ["отключить", "выйти", "изменить", "переменная"]
Класс для распознавания команд сделан немного по-другому. В нем для токенизации слов используется модель для распознавания слов, они одновременной распознаются и токенизируются. В метод init передается список уникальных меток команд и модель для распознавания слов. Токенизатор слов берем из модели для распознавания слов. Для токенизации имен команд делаем отдельный токенизатор.
class CommandModel:
def __init__(self, unique_commands_names, words_recognizer):
self.words_recognizer = words_recognizer
self.words_tokenizer = words_recognizer.words_tokenizer
unique_words_num = words_recognizer.words_tokenizer.tokens_num
self.command_name_tokenizer = Tokenizer(unique_commands_names)
self.model = MultyRegression(unique_words_num, len(unique_commands_names))
В методе get_input происходит преобразование строки, в которой записана команда. Сначала команда разбивается на слова, потом каждое слово классифицируется моделью для распознавания слов. Потом токенизатор команд преобразует последовательность слов в вектор представления команды, который потом передается на вход модели для распознавания команд.
def get_input(self, input_command):
command_sequence = [self.words_recognizer.predict(word) for word in input_command.split()]
return self.words_tokenizer.sequence_to_vector(command_sequence)
Остальные методы работают аналогично как у класса WordModel.
Создание набора данных и обучение модели распознавания команд.
unique_words = list(set(target_words))
unique_commands_names = list(set(list(commands.values())))
input_commands = list(commands.keys())
target_commands = list(commands.values())
word_model = WordModel(letters, unique_words)
word_inputs, word_targets = make_words_data(word_model, words_list, target_words)
word_model.fit(word_inputs, word_targets)
command_model = CommandModel(unique_commands_names, word_model)
command_inputs, command_targets = make_commands_data(command_model, input_commands, target_commands)
command_model.fit(command_inputs, command_targets, epochs=100, lr=0.9)
Для работы command_model нужна модель word_model, поэтому сначала нужно создать и обучить ее.
Проверяем работу модели, выводим как модель распознает каждый пример из обучающих данных. Выводим ответ на искаженную команду «какк дделла», модель ее распознала правильно.
print_output(command_model, input_commands, command_inputs, command_targets)
input_command = "какк дделла"
print(command_model.predict(input_command))
output:
=========================
привет [0.07785547864835976, 0.014539640208980364, 0.8951833713017856, 0.028339080113225123] [0, 0, 1, 0]
...
изменить переменную [0.03903072069752127, 0.011182745156889445, 0.01253249822182718, 0.9440049246646178] [0, 0, 0, 1]
как дела
Теперь сделаем основную программу, в которой будет использоваться модель для распознавания команд. Я сделал отдельный метод, который заранее обучает модели и возвращает их, их можно легко получить в другом файле.
word_model, command_model = get_models()
Основной функционал программы выглядит так. Словарь, в котором хранятся некоторые переменные. Функции, которые сопоставляются с командами. И словарь с командами, каждое название команды сопоставляется с ссылкой на функцию.
variables = {"is_run": True}
def bye():
variables["is_run"] = False
actions = {"отключение": bye}
Основной цикл программы работает, пока не будет изменена переменная is_run. Сначала нужно ввести команду, потом передать введенную строку в модель для распознавания команд, она возвращает метку распознанной команды. Если такая метка есть в ключах словаря actions, то происходит вызов соответствующей функции по ссылке, которая хранится в значении ключа.
while variables["is_run"]:
input_command = input("enter a command: ")
command_name = command_model.predict(input_command)
print(command_name)
if command_name in actions:
actions[command_name]()
Вводим команду «ппокка», она распознается как команда «отключение». Далее происходит вызов функции bye, в ней изменяется переменная is_run и цикл while перестает работать.
output:
enter a command: ппокка
отключение
В качестве упражнения можно сделать команду по изменению любой переменной и по добавлению новой переменной.
В этом примере я встроил модель для распознавания слов в модель для распознавания команд. Лучше так не делать. Нужно вынести логику распознавания слов отдельно от логики распознавания команд. Но я так сделал для упрощения этого примера, если их отделять, то нужно будет написать больше кода для преобразования обучающих данных для модели команд.
В этой статье я лишь показал простой пример того, как можно использовать методы машинного обучения для создания простой программы. В следующих статьях на эту тему я расскажу про то, как можно создавать знания в системе команд динамически.
Как-то развивать эту идею с использованием моделей машинного обучения я не буду. Так как я исследую возможность создания динамической системы, а не статической, как это происходит традиционно. Логистическая регрессия является статической моделью, то есть ее невозможно дообучить на новых данных, если это сделать, то она перестанет нормально распознавать то, на чем она уже была обучена. Ее можно обучить заново, но я рассматриваю другую идею создания ИИ, которая связана с динамическим созданием знаний в модели, например, импульсные нейроны. Поэтому я буду развивать идею в сторону динамического создания знаний.
Почему я рассматриваю динамическое создание знаний. Я думаю, что это одно из важных свойств, которым должна обладать система, которая обладает интеллектом. С этим свойством знаком каждый, так как каждый человек приобретает знания динамически. Кроме того, сам процесс развития системы предполагает то, что система изменяется со временем.
Подписывайтесь на мой ТГК, чтобы не пропустить мои новые статьи по машинному обучению и идеи о создании цифрового интеллекта.
Папка с кодом из статьи на ГитХабе.