Как использовать Redis в Ruby on Rails для эффективного получения скалярного произведения двух хэшей

У меня есть такая структура данных в базе данных в таблице функций с именем token_vector (хэш):

Feature.find(1).token_vector = { "a" => 0.1, "b" => 0.2, "c" => 0.3 }

Таких функций 25. Сначала я ввел данные в Redis с помощью этого в script/console:

REDIS.set(  "feature1",
            "#{ TokenVector.to_json Feature.find(1).token_vector }"
)
# ...
REDIS.set(  "feature25",
            "#{ TokenVector.to_json Feature.find(25).token_vector }"
)

TokenVector.to_json сначала преобразует хэш в формат JSON. 25 хэшей JSON, хранящихся в Redis, занимают около 8 МБ.

У меня есть метод под названием Analysis#locate. Этот метод берет скалярное произведение двух векторов token_vector. Скалярный продукт для хэшей работает следующим образом:

hash1 = { "a" => 1, "b" => 2, "c" => 3 }
hash2 = { "a" => 4, "b" => 5, "c" => 6, "d" => 7 }

Значения каждого перекрывающегося ключа в хеше (в данном случае a, b и c, а не d) попарно перемножаются, а затем суммируются.

Значение для a в hash1 равно 1, значение для a в hash2 равно 4. Умножьте их, чтобы получить 1*4 = 4.

Значение для b в hash1 равно 2, значение для b в hash2 равно 5. Умножьте их, чтобы получить 2*5 = 10.

Значение для c в hash1 равно 3, значение для c в hash2 равно 6. Умножьте их, чтобы получить 3*6 = 18.

Значение для d в hash1 не существует, значение для d в hash2 равно 7. В этом случае установите d = 0 для первого хэша. Умножьте их, чтобы получить 0*7 = 0.

Теперь сложите умноженные значения. 4 + 10 + 18 + 0 = 32. Это точечный продукт hash1 и hash2.

Analysis.locate( hash1, hash2 ) # => 32

У меня есть метод, который часто используется, Analysis#topicize. Этот метод принимает параметр token_vector, который представляет собой просто хэш, как и выше. Analysis#topicize берет скалярное произведение token_vector и каждой из 25 характеристик token_vectors и создает новый вектор этих 25 скалярных произведений, называемый feature_vector. feature_vector - это просто массив. Вот как выглядит код:

def self.topicize token_vector

  feature_vector = FeatureVector.new

  feature_vector.push(
    locate( token_vector, TokenVector.from_json( REDIS.get "feature1" ) )
  )
  # ...
  feature_vector.push(
    locate( token_vector, TokenVector.from_json( REDIS.get "feature25" ) )
  )

  feature_vector

end

Как видите, он берет скалярное произведение token_vector и token_vector каждой функции, которые я ввел в Redis выше, и помещает значение в массив.

Моя проблема в том, что это занимает около 18 секунд каждый раз, когда я вызываю метод. Я неправильно использую Redis? Я думаю, проблема может заключаться в том, что я не должен загружать данные Redis в Ruby. Должен ли я отправить Redis данные (token_vector) и написать функцию Redis, чтобы она выполняла функцию dot_product, а не писать ее с помощью кода Ruby?


person Eric    schedule 25.09.2011    source источник


Ответы (2)


Вам нужно будет профилировать его, чтобы быть уверенным, но я подозреваю, что вы теряете много времени при сериализации/десериализации объектов JSON. Вместо того, чтобы превращать token_vector в строку JSON, почему бы не поместить ее непосредственно в Redis, поскольку у Redis есть свой собственный хеш. тип?

REDIS.hmset "feature1",   *Feature.find(1).token_vector.flatten
# ...
REDIS.hmset "feature25",  *Feature.find(25).token_vector.flatten

Что делает Hash#flatten, так это превращает хэш типа { 'a' => 1, 'b' => 2 } в массив типа [ 'a', 1, 'b', 2 ], а затем мы используем splat (*) для отправки каждого элемента массива в качестве аргумента в Redis#hmset ("m" в "hmset" означает "несколько", как в «установить несколько хеш-значений одновременно»).

Затем, когда вы хотите вернуть его, используйте Redis#hgetall, который автоматически возвращает Ruby Hash:

def self.topicize token_vector
  feature_vector = FeatureVector.new

  feature_vector.push locate( token_vector, REDIS.hgetall "feature1" )
  # ...
  feature_vector.push locate( token_vector, REDIS.hgetall "feature25" )

  feature_vector
end

Однако! Поскольку вы заботитесь только о значениях, а не о ключах из хэша, вы можете немного упростить процесс, используя Redis#hvals, который просто возвращает массив значений вместо hgetall.

Во-вторых, вы можете потратить много циклов на locate, для которого вы не предоставили исходный код, но есть много способов написать метод скалярного произведения на Ruby, и некоторые из них более производительны, чем другие. В этой ветке ruby-talk освещаются некоторые ценные вопросы. Один из плакатов указывает на NArray, библиотеку, реализующую числовые массивы и векторы на C.

Если я правильно понимаю ваш код, его можно было бы переопределить примерно так (prereq: gem install narray):

require 'narray'

def self.topicize token_vector
  # Make sure token_vector is an NVector
  token_vector  = NVector.to_na token_vector unless token_vector.is_a? NVector
  num_feats     = 25

  # Use Redis#multi to bundle every operation into one call.
  # It will return an array of all 25 features' token_vectors.
  feat_token_vecs = REDIS.multi do
    num_feats.times do |feat_idx|
      REDIS.hvals "feature#{feat_idx + 1}"
    end
  end 

  pad_to_len = token_vector.length

  # Get the dot product of each of those arrays with token_vector
  feat_token_vecs.map do |feat_vec|
    # Make sure the array is long enough by padding it out with zeroes (using
    # pad_arr, defined below). (Since Redis only returns strings we have to
    # convert each value with String#to_f first.)
    feat_vec = pad_arr feat_vec.map(&:to_f), pad_to_len

    # Then convert it to an NVector and do the dot product
    token_vector * NVector.to_na(feat_vec)

    # If we need to get a Ruby Array out instead of an NVector use #to_a, e.g.:
    # ( token_vector * NVector.to_na(feat_vec) ).to_a
  end
end

# Utility to pad out array with zeroes to desired size
def pad_arr arr, size
  arr.length < size ?
    arr + Array.new(size - arr.length, 0) : arr
end

Надеюсь, это полезно!

person Jordan Running    schedule 25.09.2011
comment
Похоже, я столкнулся с проблемой в feature_vector.push( locate( token_vector, REDIS.hgetall( "feature1" ) ) ). token_vector относится к классу TokenVector, а REDIS.hgetall( "feature1" ) относится к классу Hash. Мне нужно, чтобы REDIS.hgetall( "feature1" ) относился к классу TokenVector. TokenVector на самом деле является подклассом Hash, только с некоторыми дополнительными методами. Как изменить класс REDIS.hgetall( "feature1" ) с Hash на TokenVector? - person Eric; 26.09.2011
comment
Короче: locate( token_vector, TokenVector[ REDIS.hgetall "feature1" ] ). Поскольку TokenVector является подклассом Hash, он наследует метод класса Hash [], который может принимать в качестве аргумента Hash. ruby-doc.org/core/classes/Hash.src/ M000716.html - person Jordan Running; 26.09.2011
comment
На самом деле я опубликовал ответ, но на самом деле это продолжение моего предыдущего комментария. Можешь взглянуть? Спасибо. - person Eric; 26.09.2011

На самом деле это не ответ, а просто продолжение моего предыдущего комментария, поскольку это, вероятно, не вписывается в комментарий. Похоже, проблема Hash/TokenVector могла быть не единственной проблемой. Я делаю:

token_vector = Feature.find(1).token_vector
Analysis.locate( token_vector, TokenVector[ REDIS.hgetall( "feature1" ) ] )

и получить эту ошибку:

TypeError: String can't be coerced into Float
from /Users/RedApple/S/lib/analysis/vectors.rb:26:in `*'
from /Users/RedApple/S/lib/analysis/vectors.rb:26:in `block in dot'
from /Users/RedApple/S/lib/analysis/vectors.rb:24:in `each'
from /Users/RedApple/S/lib/analysis/vectors.rb:24:in `inject'
from /Users/RedApple/S/lib/analysis/vectors.rb:24:in `dot'
from /Users/RedApple/S/lib/analysis/analysis.rb:223:in `locate'
from (irb):6
from /Users/RedApple/.rvm/rubies/ruby-1.9.2-p290/bin/irb:16:in `<main>'

Analysis#locate выглядит так:

def self.locate vector1, vector2
  vector1.dot vector2
end

Вот соответствующая часть analysis/vectors.rb, строки 23-28, метод TokenVector#dot:

def dot vector
  inject 0 do |product,item|
    axis, value = item
    product + value * ( vector[axis] || 0 )
  end
end

Я не уверен, в чем проблема.

person Eric    schedule 26.09.2011
comment
О, мой плохой. Драгоценный камень Redis возвращает только строки, поэтому сначала вам придется преобразовать каждое значение в число. С хэшем вы не можете просто сделать map &:to_f, увы, но, например. (hsh = REDIS.hgetall(...)).merge(hsh) {|key,val| val.to_f } должно помочь. Если вы хотите, вы можете переопределить TokenVector.[], чтобы сделать это на месте. - person Jordan Running; 26.09.2011
comment
Большое спасибо, это сработало. Я хотел бы дать вам 5 баллов за ваш ответ. - person Eric; 26.09.2011
comment
Большой! Получилось ли у вас прибавка скорости? - person Jordan Running; 26.09.2011
comment
Время запуска Analysis.topicize( token_vector ) увеличилось с 18 до менее 1 секунды. - person Eric; 26.09.2011