class Libmf::Model

Public Class Methods

new(**options) click to toggle source
# File lib/libmf/model.rb, line 3
def initialize(**options)
  @options = options
end

Public Instance Methods

bias() click to toggle source
# File lib/libmf/model.rb, line 50
def bias
  model[:b]
end
columns() click to toggle source
# File lib/libmf/model.rb, line 42
def columns
  model[:n]
end
cv(data, folds: 5) click to toggle source
# File lib/libmf/model.rb, line 25
def cv(data, folds: 5)
  problem = create_problem(data)
  FFI.mf_cross_validation(problem, folds, param)
end
factors() click to toggle source
# File lib/libmf/model.rb, line 46
def factors
  model[:k]
end
fit(data, eval_set: nil) click to toggle source
# File lib/libmf/model.rb, line 7
def fit(data, eval_set: nil)
  train_set = create_problem(data)

  @model =
    if eval_set
      eval_set = create_problem(eval_set)
      FFI.mf_train_with_validation(train_set, eval_set, param)
    else
      FFI.mf_train(train_set, param)
    end

  nil
end
load_model(path) click to toggle source
# File lib/libmf/model.rb, line 34
def load_model(path)
  @model = FFI.mf_load_model(path)
end
p_factors(format: nil) click to toggle source
# File lib/libmf/model.rb, line 54
def p_factors(format: nil)
  _factors(model[:p], rows, format)
end
predict(row, column) click to toggle source
# File lib/libmf/model.rb, line 21
def predict(row, column)
  FFI.mf_predict(model, row, column)
end
q_factors(format: nil) click to toggle source
# File lib/libmf/model.rb, line 58
def q_factors(format: nil)
  _factors(model[:q], columns, format)
end
rows() click to toggle source
# File lib/libmf/model.rb, line 38
def rows
  model[:m]
end
save_model(path) click to toggle source
# File lib/libmf/model.rb, line 30
def save_model(path)
  FFI.mf_save_model(model, path)
end

Private Instance Methods

_factors(ptr, n, format) click to toggle source
# File lib/libmf/model.rb, line 64
def _factors(ptr, n, format)
  case format
  when :numo
    Numo::SFloat.from_string(ptr.read_bytes(n * factors * 4)).reshape(n, factors)
  when nil
    ptr.read_array_of_float(n * factors).each_slice(factors).to_a
  else
    raise ArgumentError, "Invalid format"
  end
end
create_problem(data) click to toggle source
# File lib/libmf/model.rb, line 104
def create_problem(data)
  if data.is_a?(String)
    # need to expand path so it's absolute
    return FFI.mf_read_problem(File.expand_path(data))
  end

  raise Error, "No data" if data.empty?

  # TODO do in C for better performance
  # can use FIX2INT() and RFLOAT_VALUE() instead of pack
  # and write directly to C string
  buffer = String.new
  pack_format = "iif"
  data.each do |row|
    row.pack(pack_format, buffer: buffer)
  end

  r = ::FFI::MemoryPointer.new(FFI::Node, data.size)
  r.write_bytes(buffer)

  # double check size is what we expect
  # FFI will throw an error above if too long
  raise Error, "Bad buffer size" if r.size != buffer.bytesize

  m = data.max_by { |r| r[0] }[0] + 1
  n = data.max_by { |r| r[1] }[1] + 1

  prob = FFI::Problem.new
  prob[:m] = m
  prob[:n] = n
  prob[:nnz] = data.size
  prob[:r] = r
  prob
end
model() click to toggle source
# File lib/libmf/model.rb, line 75
def model
  raise Error, "Not fit" unless @model
  @model
end
param() click to toggle source
# File lib/libmf/model.rb, line 80
def param
  param = FFI.mf_get_default_param
  options = @options.dup
  # silence insufficient blocks warning with default params
  options[:bins] ||= 25 unless options[:nr_bins]
  options[:copy_data] = false unless options.key?(:copy_data)
  options_map = {
    :loss => :fun,
    :factors => :k,
    :threads => :nr_threads,
    :bins => :nr_bins,
    :iterations => :nr_iters,
    :learning_rate => :eta,
    :nmf => :do_nmf
  }
  options.each do |k, v|
    k = options_map[k] if options_map[k]
    param[k] = v
  end
  # do_nmf must be true for generalized KL-divergence
  param[:do_nmf] = true if param[:fun] == 2
  param
end