вступление
Это часть серии сообщений в блогах, посвященных внедрению искусственного интеллекта. Если вас интересует предыстория этой истории или ее развитие:
На этой неделе мы покажем процесс тестирования и первые результаты модели. Мы будем использовать Google Organic Results Scraper API от SerpApi для сбора данных. Также вы можете проверить в игровой площадке более детальное представление данных, которые мы будем использовать.
Тренировочные данные
Вот структурная разбивка данных, которые мы храним для обучения в файле json:
[
{
"Key 1": Value_1,
"Key 2": Value_2,
"Key 3": Value_3,
"Key 4": [
"Value_1",
...
],
"Key 5": {
"Inner Key 1": Inner_Value_1,
...
},
...
]
Вот пример:
[
{
"position": 1,
"title": "Coffee - Wikipedia",
"link": "https://en.wikipedia.org/wiki/Coffee",
"displayed_link": "https://en.wikipedia.org › wiki › Coffee",
"snippet": "Coffee is a brewed drink prepared from roasted coffee beans, the seeds of berries from certain flowering plants in the Coffea genus. From the coffee fruit, ...",
"snippet_highlighted_words": [
"Coffee",
"coffee",
"coffee"
],
...
},
...
]
Ссылки, из которых мы собрали органическую выдачу Google:
Ссылка на чай (около 100 результатов)
Ссылка на кофе (около 100 результатов)
Структура тестирования
Мы уже подробно рассказывали, как мы обучали данные, в сообщениях блога за последние три недели. Сегодня мы проверим, насколько верна гипотеза, рассчитав точность обучения.
Мы можем повторно использовать классы Train
и Database
для создания примеров и создания примеров векторов со следующими строками:
example_vector = Database.word_to_tensor example
example_vector.map! {|el| el = el.nil? ? 0: el}
example_vector = Train.extend_vector example_vector
weighted_example = Train.product example_vector
example
здесь — строка, которую мы предоставляем. Любое значение для любого ключа в Google Organic Results, преобразованное в строку, будет допустимым примером.
Мы можем повторно использовать Database.word_to_tensor
, чтобы получить векторизованную версию нашей строки в соответствии с нашим словарем.
Если какое-либо значение равно nil
(null), которого нет в нашем словаре, оно будет заменено на 0
, которое является значением для нашего <unk>
(неизвестно).example_vector
, затем, следует расширить до максимального размера строки для целей расчета используя 1
s.weighted_example
будет произведением @@weights
, которое мы вычислили ранее в нашем векторизованном примере.
Ближайшие векторы этого значения в многомерном пространстве из предоставленных нами примеров должны иметь один и тот же ключ или их среднее значение должен привести нас к тому же ключу. Итак, в нашем случае, если приведенный нами пример не является snippet
, ближайшие векторы вокруг weighted_example
должны дать нам в среднем less than 0.5
(их тождества 0
и 1
). Вывод должен быть, что the example isn't a snippet
.
Мы измеряем расстояние нашего примера с каждым примером в наборе данных, используя формулу евклидова расстояния для многомерного пространства:
distances = []
vector_array.each_with_index do |comparison_vector, vector_index|
distances << Train.euclidean_distance(comparison_vector, weighted_example)
end
Возьмем индексы минимальных расстояний (k раз):
indexes = []
k.times do
index = distances.index(distances.min)
indexes << index
distances[index] = 1000000000
end
Затем мы берем реальные тождества каждого из этих векторов:
predictions = []
indexes.each do |index|
predictions << key_array[index].first.to_i
end
key_array
здесь массив, содержащий 0
или 1
в первом элементе каждой строки и строку во втором. Чтобы привести пример:
[
...
["0", "https://www.coffeebean.com"],
["1", "Born and brewed in Southern California since 1963, The Coffee Bean & Tea Leaf® is passionate about connecting loyal customers with carefully handcrafted ..."],
["0", "4"],
...
]
1
означает, что элемент является фрагментом, 0
означает, что это не так.
Вернем прогнозы:
prediction = (predictions.sum/predictions.size).to_f
if prediction < 0.5
puts "False - Item is not Snippet"
return 0
else
puts "True - Item is Snippet"
return 1
end
Вот полный метод для этого:
def test example, k, vector_array, key_array example_vector = Database.word_to_tensor example example_vector.map! {|el| el = el.nil? ? 0: el} example_vector = Train.extend_vector example_vector weighted_example = Train.product example_vector
distances = [] vector_array.each_with_index do |comparison_vector, vector_index| distances << Train.euclidean_distance(comparison_vector, weighted_example) end
indexes = [] k.times do index = distances.index(distances.min) indexes << index distances[index] = 1000000000 end
predictions = [] indexes.each do |index| predictions << key_array[index].first.to_i end
puts "Predictions: #{predictions}"
prediction = (predictions.sum/predictions.size).to_f if prediction < 0.5 puts "False - Item is not Snippet" return 0 else puts "True - Item is Snippet" return 1 end end
Тестирование с помощью органических результатов Google для фрагмента
Теперь, когда у нас есть функция для тестирования, давайте отделим сниппеты от не сниппетов в наших примерах:
true_examples = key_array.map {|el| el = el.first == "1" ? el.second : nil}.compact
false_examples = key_array.map {|el| el = el.first == "0" ? el.second : nil}.compact
Это облегчит нам расчет.
Давайте объявим пустой массив для сбора прогнозов и начнем с не сниппетов:
predictions = []
false_examples.each do |example| prediction = test example, 2, vector_array, key_array predictions << prediction end
predictions.map! {|el| el = el == 1 ? 0 : 1}
Поскольку мы знаем, что ни один из этих примеров не является snippet
, любое предсказание, дающее 1
, будет неверным. Итак, если мы проверим нашу модель на ложных примерах, а затем обратим 1
s на 0
s, а 0
s на 1
s, мы сможем объединить ее с нашими истинными примерами:
true_examples.each do |example|
prediction = test example, 2, vector_array, key_array
predictions << prediction
end
Теперь, когда мы заполнили желаемый массив:
prediction_train_accuracy = predictions.sum.to_f / predictions.size.to_f
puts "Prediction Accuracy for Training Set is: #{prediction_train_accuracy}"
Если мы разделим количество 1
на количество прогнозов, мы сможем рассчитать результаты точности.
Предварительные результаты
Мы проделали точно такой же процесс для данных, о которых упоминали ранее. Количество прогнозов для фрагмента было 1065
, значение k
было 2
, а значение n-gram
было 2
.
Модель правильно предсказала 872
раз. Это означает, что точность обучения была 0.8187793427230047
(%81.87
).
Это хорошее число для начала, и с дополнительными настройками и тестированием с большим набором данных можно было бы доказать, что первоначальная гипотеза верна.
Полный код
class Database def initialize json_data, vocab = { "<unk>" => 0, "<pad>" => 1 } super() @@pattern_data = [] @@vocab = vocab end ## Related to creating main database def self.add_new_data_to_database json_data, csv_path = nil json_data.each do |result| recursive_hash_pattern result, "" end @@pattern_data = @@pattern_data.reject { |pattern| pattern.include? nil }.uniq.compact path = "#{csv_path}master_database.csv" File.write(path, @@pattern_data.map(&:to_csv).join) end def self.element_pattern result, pattern @@pattern_data.append([result, pattern].flatten) end def self.element_array_pattern result, pattern result.each do |element| element_pattern element, pattern end end def self.assign hash, key, pattern if hash[key].is_a?(Hash) if pattern.present? pattern = "#{pattern}__#{key}" else pattern = "#{key}" end recursive_hash_pattern hash[key], pattern elsif hash[key].present? && hash[key].is_a?(Array) && hash[key].first.is_a?(Hash) if pattern.present? pattern = "#{pattern}__#{key}__n" else pattern = "#{key}" end hash[key].each do |hash_inside_array| recursive_hash_pattern hash_inside_array, pattern end elsif hash[key].present? && hash[key].is_a?(Array) if pattern.present? pattern = "#{pattern}__n" else pattern = "#{key}" end element_array_pattern hash[key], pattern else if pattern.present? pattern = "#{pattern}__#{key}" else pattern = "#{key}" end element_pattern hash[key], pattern end end def self.recursive_hash_pattern hash, pattern hash.keys.each do |key| assign hash, key, pattern end end ## Related to tokenizing def self.default_dictionary_hash { /\"/ => "", /\'/ => " \' ", /\./ => " . ", /,/ => ", ", /\!/ => " ! ", /\?/ => " ? ", /\;/ => " ", /\:/ => " ", /\(/ => " ( ", /\)/ => " ) ", /\// => " / ", /\s+/ => " ", /<br \/>/ => " , ", /http/ => "http", /https/ => " https ", } end def self.tokenizer word, dictionary_hash = default_dictionary_hash word = word.downcase dictionary_hash.keys.each do |key| word.sub!(key, dictionary_hash[key]) end word.split end def self.iterate_ngrams token_list, ngrams = 2 token_list.each do |token| 1.upto(ngrams) do |n| permutations = (token_list.size - n + 1).times.map { |i| token_list[i...(i + n)] } permutations.each do |perm| key = perm.join(" ") unless @@vocab.keys.include? key @@vocab[key] = @@vocab.size end end end end end def self.word_to_tensor word token_list = tokenizer word token_list.map {|token| @@vocab[token]} end ## Related to creating key-specific databases def self.create_key_specific_databases result_type = "organic_results", csv_path = nil, dictionary = nil, ngrams = nil, vocab_path = nil keys, examples = create_keys_and_examples keys.each do |key| specific_pattern_data = [] @@pattern_data.each_with_index do |pattern, index| word = pattern.first.to_s next if word.blank? if dictionary.present? token_list = tokenizer word, dictionary else token_list = tokenizer word end if ngrams.present? iterate_ngrams token_list, ngrams else iterate_ngrams token_list end if key == pattern.second specific_pattern_data << [ 1, word ] elsif (examples[key].to_s.to_i == examples[key]) && word.to_i == word next elsif (examples[key].to_s.to_i == examples[key]) && word.numeric? specific_pattern_data << [ 0, word ] elsif examples[key].numeric? && word.numeric? next elsif key.split("__").last == pattern.second.to_s.split("__").last specific_pattern_data << [ 1, word ] else specific_pattern_data << [ 0, word ] end end path = "#{csv_path}#{result_type}__#{key}.csv" File.write(path, specific_pattern_data.map(&:to_csv).join) end if vocab_path.present? save_vocab vocab_path else save_vocab end end def self.create_keys_and_examples keys = @@pattern_data.map { |pattern| pattern.second }.uniq examples = {} keys.each do |key| examples[key] = @@pattern_data.find { |pattern| pattern.first.to_s if pattern.second == key } end [keys, examples] end def self.numeric? return true if self =~ /\A\d+\Z/ true if Float(self) rescue false end def self.save_vocab vocab_path = "" path = "#{vocab_path}vocab.json" vocab = JSON.parse(@@vocab.to_json) File.write(path, JSON.pretty_generate(vocab)) end def self.read_vocab vocab_path vocab = File.read vocab_path @@vocab = JSON.parse(vocab) end def self.return_vocab @@vocab end end class Train def initialize csv_path @@csv_path = csv_path @@vector_arr = [] @@word_arr = [] @@maximum_word_size = 100 @@weights = Vector[] @@losses = [] end def self.read @@word_arr = CSV.read(@@csv_path) @@word_arr end def self.define_training_set vectors @@vector_arr = vectors end def self.auto_define_maximum_size @@maximum_word_size = @@vector_arr.map {|el| el.size}.max end def self.extend_vector vector vector_arr = vector.to_a (@@maximum_word_size - vector.size).times { vector_arr << 1 } Vector.[](*vector_arr) end def self.extend_vectors @@vector_arr.each_with_index do |vector, index| @@vector_arr[index] = extend_vector vector end end def self.initialize_weights weights = [] @@maximum_word_size.times { weights << 1.0 } @@weights = Vector.[](*weights) end def self.config k = 1, lr = 0.001 [k, lr] end def self.product vector @@weights.each_with_index do |weight, index| vector[index] = weight * vector[index] end vector end def self.euclidean_distance vector_1, vector_2 subtractions = (vector_1 - vector_2).to_a subtractions.map! {|sub| sub = sub*sub } Math.sqrt(subtractions.sum) end def self.k_neighbors distances, k indexes = [] (k).times do min = distances.index(distances.min) indexes << min distances[min] = distances.max + 1 end indexes end def self.make_prediction indexes predictions = [] indexes.each do |index| predictions << @@word_arr[index][0].to_i end predictions.sum/predictions.size end def self.update_weights result, indexes, vector, lr indexes.each do |index| subtractions = @@vector_arr[index] - vector subtractions.each_with_index do |sub, sub_index| if result == 0 && sub >= 0 @@weights[sub_index] = @@weights[sub_index] + lr elsif result == 0 && sub < 0 @@weights[sub_index] = @@weights[sub_index] - lr elsif result == 1 && sub >= 0 @@weights[sub_index] = @@weights[sub_index] - lr elsif result == 1 && sub < 0 @@weights[sub_index] = @@weights[sub_index] + lr end end end end def self.mean_absolute_error real, indexes errors = [] indexes.each do |index| errors << (@@word_arr[index][0].to_i - real).abs end (errors.sum/errors.size).to_f end def self.train vector, index k, lr = config vector = extend_vector vector vector = product vector distances = [] @@vector_arr.each_with_index do |comparison_vector, vector_index| if vector_index == index distances << 100000000 else distances << euclidean_distance(comparison_vector, vector) end end indexes = k_neighbors distances, k real = @@word_arr[index][0].to_i prob_prediction = make_prediction indexes prediction = prob_prediction > 0.5 ? 1 : 0 result = real == prediction ? 1 : 0 update_weights result, indexes, vector, lr loss = mean_absolute_error real, indexes @@losses << loss puts "Result : #{real}, Prediction: #{prediction}" puts "Loss: #{loss}" prediction end end json_path = "organic_results/example.json" json_data = File.read(json_path) json_data = JSON.parse(json_data) Database.new json_data ## For training from scratch Database.add_new_data_to_database json_data, csv_path = "organic_results/" Database.create_key_specific_databases result_type = "organic_results", csv_path = "organic_results/" ## Database.read_vocab "vocab.json" ## We will use an iteration of csvs within a specific path in the end csv_path = "organic_results/organic_results__snippet.csv" Train.new csv_path key_array = Train.read vector_array = key_array.map { |word| Database.word_to_tensor word[1] } Train.define_training_set vector_array Train.auto_define_maximum_size Train.extend_vectors Train.initialize_weights Train.config k = 2 vector_array.each_with_index do |vector, index| Train.train vector, index end def test example, k, vector_array, key_array example_vector = Database.word_to_tensor example example_vector.map! {|el| el = el.nil? ? 0: el} example_vector = Train.extend_vector example_vector weighted_example = Train.product example_vector distances = [] vector_array.each_with_index do |comparison_vector, vector_index| distances << Train.euclidean_distance(comparison_vector, weighted_example) end indexes = [] k.times do index = distances.index(distances.min) indexes << index distances[index] = 1000000000 end predictions = [] indexes.each do |index| predictions << key_array[index].first.to_i end puts "Predictions: #{predictions}" prediction = (predictions.sum/predictions.size).to_f if prediction < 0.5 puts "False - Item is not Snippet" return 0 else puts "True - Item is Snippet" return 1 end end true_examples = key_array.map {|el| el = el.first == "1" ? el.second : nil}.compact false_examples = key_array.map {|el| el = el.first == "0" ? el.second : nil}.compact predictions = [] false_examples.each do |example| prediction = test example, 2, vector_array, key_array predictions << prediction end predictions.map! {|el| el = el == 1 ? 0 : 1} true_examples.each do |example| prediction = test example, 2, vector_array, key_array predictions << prediction end prediction_train_accuracy = predictions.sum.to_f / predictions.size.to_f puts "Prediction Accuracy for Training Set is: #{prediction_train_accuracy}" end
Заключение
Я хотел бы извиниться перед читателем за то, что опоздал на один день с записью в блоге. Две недели спустя мы продемонстрируем, как сохранить их для реализации и дальнейших настроек для повышения точности.
Конечная цель этого проекта — создать open-source gem
для реализации всеми, кто использует структуру данных JSON в своем коде.
Я хотел бы поблагодарить читателя за внимание, и блестящих людей SerpApi, творящих чудеса даже в трудные времена, и за всю их поддержку.
Первоначально опубликовано на https://serpapi.com 21 апреля 2022 г.