Одно из основных достоинств онлайновых приложений состоит в том, что они все время получают обратную связь в виде поведения пользователей. В случае поисковой машины каждый пользователь тут же сообщает о том, насколько ему понравились результаты поиска, щелкая по одному результату и игнорируя остальные. Мы рассмотрим способ регистрации действий пользователя после получения результатов и то, как собранную таком образом информацию можно применить для более качественного ранжирования результатов.
Итак, для тех, кто справился с предыдущей статьей, создал и запустил своего поискового робота, сегодня мы построим и обучим искусственную нейронную сеть. Предлагая ей поисковой запрос, результаты обработки запроса и ту ссылку, по которой пользователь пе- решел, мы научим нейронную сеть, как использовать для упорядочения результатов поиска, реальный выбор пользователя.
Итак, для тех, кто справился с предыдущей статьей, создал и запустил своего поискового робота, сегодня мы построим и обучим искусственную нейронную сеть. Предлагая ей поисковой запрос, результаты обработки запроса и ту ссылку, по которой пользователь пе- решел, мы научим нейронную сеть, как использовать для упорядочения результатов поиска, реальный выбор пользователя.
Проектирование сети отслеживания переходов
Есть много разновидностей нейронных сетей, но все они состоят из множества узлов (нейронов) и связей между ними (синапсов). Мы собираемся построить MLP сеть (multilayer perceptron). Такая сеть состоит из нескольких уровней нейронов, первый из которых принимает входные данные – в данном случае поисковой запрос. Последний уровень возвращает результаты – список весов найденных URL.![]() |
| Уровни в БД: word_list word_hidden hidden_node hidden_urls url_list . |
Как пример,
в сети, изображенной на рисунке выше, сколько-то человек переходили на сайт Всемирного банка (World Bank) после запроса world bank, и это усилило ассоциацию между данными словами и URL. Сплошными линиями показаны сильные связи, а полужирный текст говорит о том, что узел стал очень активным.
Мы будем обучать сеть с помощью алгоритма обратного распространения backpropagation.
Прошу обратить внимание на подпись под картинкой. Каждый уровень (прямоугольничек или стрелочка) имеет свое отображение в БД. Я подписал названия таблиц им соответствующих, для большего понимания, что мы будем делать, но если сейчас Вам что-то не очень понятно, вернитесь к прочтению данного принципа, после описания механизма работы и все встанет на свои места!)
Подготовка базы данных
У тех, кто осилил прошлую статью, уже есть 2 таблицы - это word_list и url_list, где хранятся списки слов и урлов, соответственно. Теперь нужно еще 3: 1 - hidden_node (скрытый слой) и 2 таблицы связи с ним (одна hidden_word – для связей между слоем слов и скрытым слоем, другая hidden_url – для связей между скрытым и выходным слоем).
def get_strength(self, from_id, to_id, layer):
if layer == 0:
table = 'word_hidden'
else:
table = 'hidden_url'
self.cur.execute('select strength from %s where from_id = %d and to_id = %d' % (table, from_id, to_id))
res = self.cur.fetchone()
if res == None:
if layer == 0:
return -0.2
if layer == 1:
return 0
return res[0]
Еще необходим метод set_strength, который выясняет, существует ли уже связь, и создает либо обновляет связь, приписывая ей заданную силу. Он будет использоваться в коде для обучения сети:
def set_strength(self, from_id, to_id, table, strength):
self.cur.execute('select id from %s where from_id = %d and to_id = %d' % (table, from_id, to_id))
res = self.cur.fetchone()
if res == None:
self.cur.execute('insert into %s (from_id, to_id, strength) values (%d, %d, %f)' % (table, from_id, to_id, strength))
else:
row_id = res[0]
self.cur.execute('update %s set strength = %f where id = %d' % (table, strength, row_id))
self.db_commit()
Можно было бы предварительно создать гигантскую сеть с тысячами узлов в скрытом слое и уже готовыми связями, но мы с Вами будем создавать скрытые узлы, когда в них возникает надобность. def generate_hidden_node(self, word_ids, urls):
# Для простоты ограничимся 2-х словными фразами
if len(word_ids) > 3:
return None
# Проверить, создавали ли мы уже узел для данного набора слов
sorted_words = map(str, word_ids)
sorted_words.sort()
create_key = '_'.join(sorted_words)
self.cur.execute("select count(id) from hidden_node where create_key = '%s'" % create_key)
count_hidden_id = self.cur.fetchone()
# Если нет, создадим сейчас
if not count_hidden_id:
self.cur.execute("insert into hidden_node (create_key) values ('%s') returning hidden_node.id" % create_key)
hidden_id = self.cur.fetchone()[0]
# Задать веса по умолчанию
for word_id in word_ids:
self.set_strength(word_id, hidden_id, 'word_hidden', 1.0/len(word_ids))
for url_id in urls:
self.set_strength(hidden_id, url_id, 'hidden_url', 0.1)
self.db_commit()
Прямой проход
Теперь все готово для написания функций, которые принимают на входе слова, активируют связи в сети и формируют выходные сигналы для URL.![]() |
| Функция tanh |
def get_all_hidden_ids(self, word_ids, url_ids):
l1 = {}
for word_id in word_ids:
self.cur.execute('select to_id from word_hidden where from_id = %d' % word_id)
cur = self.cur.fetchall()
for row in cur:
l1[row[0]] = 1
for url_id in url_ids:
cur = self.cur.execute('select from_id from hidden_url where to_id = %d' % url_id)
cur = self.cur.fetchall()
for row in cur:
l1[row[0]] = 1
return l1.keys()
Нам также понадобится метод для конструирования релевантной сети с текущими весами из базы данных. Эта функция инициализирует различные переменные экземпляра класса: список слов, относящиеся к запросу узлы и URL, уровень выходного сигнала для каждого узла и веса всех связей между узлами. Веса считываются из базы данных с помощью ранее разработанных функций. def setup_network(self, word_ids, url_ids):
# списки значений
self.word_ids = word_ids
self.hidden_ids = self.get_all_hidden_ids(word_ids, url_ids)
self.url_ids = url_ids
# выходные сигналы узлов
self.hidden_word_output = [1.0]*len(self.word_ids)
self.hidden_layer_output = [1.0]*len(self.hidden_ids)
self.hidden_url_output = [1.0]*len(self.url_ids)
# создаем матрицу весов
self.word_layer_weights = [
[self.get_strength(word_id, hidden_id, 0) for hidden_id in self.hidden_ids] for word_id in self.word_ids
]
self.layer_url_weights = [
[self.get_strength(hidden_id, url_id, 1) for url_id in self.url_ids] for hidden_id in self.hidden_ids
]
Вот теперь все готово для реализации алгоритма feedforward. Он принимает список входных сигналов, пропускает их через сеть и возвращает выходные сигналы от всех узлов на выходном уровне. Поскольку в данном случае мы сконструировали сеть только для слов, входящих в запрос, то выходной сигнал от всех входных узлов равен 1: def feed_forward(self):
# единственные входные сигналы – слова из запроса
for i in xrange(len(self.word_ids)):
self.hidden_word_output[i] = 1.0
# возбуждение скрытых узлов
for j in xrange(len(self.hidden_ids)):
sum = 0.0
for i in xrange(len(self.word_ids)):
sum = sum + self.hidden_word_output[i] * self.word_layer_weights[i][j]
self.hidden_layer_output[j] = tanh(sum)
# возбуждение выходных узлов
for k in xrange(len(self.url_ids)):
sum = 0.0
for j in range(len(self.hidden_ids)):
sum = sum + self.hidden_layer_output[j] * self.layer_url_weights[j][k]
self.hidden_url_output[k] = tanh(sum)
return self.hidden_url_output[:]
Алгоритм feedforward в цикле обходит все узлы скрытого слоя и для каждого из них вычисляет сумму величин выходных сигналов от узлов входного слоя, помноженных на вес соответствующей связи. Выходной сигнал каждого скрытого узла – это результат применения функции tanh к взвешенной сумме входных сигналов. Этот сигнал передается на выходной уровень. Выходной уровень делает то же самое – умножает полученные от предыдущего уровня сигналы на веса связей и применяет функцию tanh для получения окончательного результата. Сеть можно легко обобщить, введя дополнительные уровни, которые будут преобразовывать выходные сигналы от предыдущего уровня во входные сигналы следующему. def get_result(self, word_ids, url_ids): self.setup_network(word_ids, url_ids) return self.feed_forward()
Числа в полученном списке обозначают релевантность поданных на вход URL. Не удивляйтесь, что для всех URL нейронная сеть выдает один и тот же ответ, ведь она еще не прошла обучения!)
Обучение с обратным распространением
А вот теперь начинаются самое интересное. Сеть принимает вход- ные сигналы и генерирует выходные, но, поскольку ее не научили, какой результат считать хорошим, выдаваемые ею ответы практически бесполезны. Сейчас мы это исправим!)def dtanh(y): return 1.0 - y*y
- Вычислить разность между текущим и желательным уровнем выходного сигнала.
- С помощью функции dtanh определить, насколько должен измениться суммарный входной сигнал для этого узла.
- Изменить вес каждой входящей связи пропорционально ее текущему весу и скорости обучения.
- Изменить выходной сигнал узла на сумму весов каждой выходной связи, умноженных на величину требуемого изменения выходного сигнала конечного узла этой связи.
- С помощью функции dtanh вычислить, насколько должен измениться суммарный входной сигнал для этого узла.
- Изменить вес каждой входящей связи пропорционально ее текущему весу и скорости обучения.
def back_propagate(self, targets, N=0.5):
# вычислить поправки для выходного слоя
output_deltas = [0.0] * len(self.urlids)
for k in xrange(len(self.urlids)):
error = targets[k] - self.hidden_url_output[k]
output_deltas[k] = dtanh(self.hidden_url_output[k]) * error
# вычислить поправки для скрытого слоя
hidden_deltas = [0.0] * len(self.hiddenids)
for j in xrange(len(self.hiddenids)):
error = 0.0
for k in xrange(len(self.urlids)):
error = error + output_deltas[k]*self.layer_url_weights[j][k]
hidden_deltas[j] = dtanh(self.hidden_layer_output[j]) * error
# обновить веса связеи между узлами скрытого и выходного слоя
for j in xrange(len(self.hiddenids)):
for k in xrange(len(self.urlids)):
change = output_deltas[k]*self.hidden_layer_output[j]
self.layer_url_weights[j][k] = self.layer_url_weights[j][k] + N*change
# обновить веса связей между узлами входного и скрытого слоя
for i in xrange(len(self.wordids)):
for j in xrange(len(self.hiddenids)):
change = hidden_deltas[j]*self.hidden_word_output[i]
self.word_layer_weights[i][j] = self.word_layer_weights[i][j] + N*change
Осталось только написать простой метод, который подготовит сеть, вызовет алгоритм feed_forward и запустит обратное распространение. Этот метод принимает в качестве параметров списки word_ids, url_ids и выбранный URL:
def train_query(self, word_ids, url_ids, selected_url_id): # сгенерировать скрытыи узел, если необходимо self.generate_hidden_node(word_ids, url_ids) self.setup_network(word_ids, url_ids) self.feed_forward() targets = [0.0]*len(urlids) targets[urlids.index(selected_url_id)] = 1.0 error = self.back_propagate(targets) self.update_database()
def update_database(self):
for i in xrange(len(self.word_ids)):
for j in xrange(len(self.hidden_ids)):
self.set_strength(self.word_ids[i], self. hidden_ids[j], 0, self.word_layer_weights[i][j])
for j in xrange(len(self.hidden_ids)):
for k in xrange(len(self.url_ids)):
self.set_strength(self.hidden_ids[j], self.url_ids[k], 1, self.layer_url_weights[j][k])
Подключение к поисковой машине
Ну вот и все!) Нам осталось только подключить результаты работы сети к поисковой машине. Это для тех, кто читал прошлую статью). Соответствующая функция похожа на все остальные весовые функции.
def neiron_net_score(self, rows, word_ids):
# Get unique URL IDs as an ordered list
url_ids = [row[0] for row in rows]
nn_res = self.neiron_net.get_result(word_ids, url_ids)
scores = {url_ids[i]: nn_res[i] for i in xrange(len(url_ids))}
return self.normalize_scores(scores)
Включите его в список weights и поэкспериментируйте с заданием разных весов. На практике следует повременить с привлечением этого метода к реальному ранжированию до тех пор, пока сеть не обучится на большом числе примеров.



Комментариев нет:
Отправить комментарий