inferx 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +50 -0
- data/Rakefile +2 -0
- data/inferx.gemspec +21 -0
- data/lib/inferx/adapter.rb +52 -0
- data/lib/inferx/categories.rb +53 -0
- data/lib/inferx/category.rb +139 -0
- data/lib/inferx/version.rb +3 -0
- data/lib/inferx.rb +56 -0
- data/spec/inferx/adapter_spec.rb +75 -0
- data/spec/inferx/categories_spec.rb +151 -0
- data/spec/inferx/category_spec.rb +206 -0
- data/spec/inferx_spec.rb +137 -0
- data/spec/spec_helper.rb +7 -0
- metadata +99 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Takahiro Kondo
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
What is inferx
|
2
|
+
==============
|
3
|
+
|
4
|
+
It is Naive Bayes classifier, and the training data is kept always by Redis.
|
5
|
+
|
6
|
+
Installation
|
7
|
+
------------
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'inferx'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install inferx
|
20
|
+
|
21
|
+
Usage
|
22
|
+
-----
|
23
|
+
|
24
|
+
require 'inferx'
|
25
|
+
|
26
|
+
inferx = Inferx.new
|
27
|
+
inferx.categories.add(:red, :green, :blue)
|
28
|
+
|
29
|
+
inferx.categories[:red].train(%w(
|
30
|
+
he
|
31
|
+
buy
|
32
|
+
apple
|
33
|
+
its
|
34
|
+
apple
|
35
|
+
fresh
|
36
|
+
))
|
37
|
+
|
38
|
+
inferx.categories[:green].train(%w(grasses ...))
|
39
|
+
inferx.categories[:blue].train(%w(sea ...))
|
40
|
+
|
41
|
+
puts inferx.classify(%w(apple ...)) # => red
|
42
|
+
|
43
|
+
Contributing
|
44
|
+
------------
|
45
|
+
|
46
|
+
1. Fork it
|
47
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
48
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
49
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
50
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/inferx.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/inferx/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Takahiro Kondo"]
|
6
|
+
gem.email = ["kondo@atedesign.net"]
|
7
|
+
gem.description = %q{It is Naive Bayes classifier, and the training data is kept always by Redis.}
|
8
|
+
gem.summary = %q{Naive Bayes classifier, the training data on Redis}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "inferx"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Inferx::VERSION
|
17
|
+
|
18
|
+
gem.add_runtime_dependency 'redis'
|
19
|
+
|
20
|
+
gem.add_development_dependency 'rspec'
|
21
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Inferx
|
2
|
+
class Adapter
|
3
|
+
|
4
|
+
# @param [Redis] an instance of Redis
|
5
|
+
# @param [String] namespace of keys to be used to Redis
|
6
|
+
def initialize(redis, namespace = nil)
|
7
|
+
@redis = redis
|
8
|
+
@namespace = namespace
|
9
|
+
end
|
10
|
+
|
11
|
+
# Get the key for access to categories.
|
12
|
+
#
|
13
|
+
# @return [String] the key
|
14
|
+
def categories_key
|
15
|
+
@categories_key ||= make_categories_key
|
16
|
+
end
|
17
|
+
|
18
|
+
# Make the key for access to categories.
|
19
|
+
#
|
20
|
+
# @return [String] the key
|
21
|
+
def make_categories_key
|
22
|
+
parts = %w(inferx categories)
|
23
|
+
parts.insert(1, @namespace) if @namespace
|
24
|
+
parts.join(':')
|
25
|
+
end
|
26
|
+
|
27
|
+
# Make the key for access to scores stored each by word.
|
28
|
+
#
|
29
|
+
# @param [Symbol] a category name
|
30
|
+
# @return [String] the key
|
31
|
+
def make_category_key(category_name)
|
32
|
+
"#{categories_key}:#{category_name}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Spawn an instance of any class.
|
36
|
+
#
|
37
|
+
# @param [Class] any class, constructor takes the instance of Redis to
|
38
|
+
# first argument, and takes the namespace to last argument
|
39
|
+
# @return [Object] a instance of the class
|
40
|
+
def spawn(klass, *args)
|
41
|
+
klass.new(@redis, *args, @namespace)
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
%w(hdel hexists hget hincrby hkeys hsetnx).each do |command|
|
47
|
+
define_method(command) do |*args|
|
48
|
+
@redis.__send__(command, categories_key, *args)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'inferx/adapter'
|
2
|
+
require 'inferx/category'
|
3
|
+
|
4
|
+
class Inferx
|
5
|
+
class Categories < Adapter
|
6
|
+
|
7
|
+
# Get all category names.
|
8
|
+
#
|
9
|
+
# @return [Array<Symbol>] category names
|
10
|
+
def all
|
11
|
+
(hkeys || []).map(&:to_sym)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get a category according name.
|
15
|
+
#
|
16
|
+
# @param [Symbol] category name
|
17
|
+
# @return [Inferx::Category] category
|
18
|
+
def get(category_name)
|
19
|
+
raise ArgumentError, "'#{category_name}' is missing" unless hexists(category_name)
|
20
|
+
spawn(Category, category_name)
|
21
|
+
end
|
22
|
+
alias [] get
|
23
|
+
|
24
|
+
# Add categories.
|
25
|
+
#
|
26
|
+
# @param [Array<Symbol>] category names
|
27
|
+
def add(*category_names)
|
28
|
+
@redis.pipelined do
|
29
|
+
category_names.each { |category_name| hsetnx(category_name, 0) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Remove categories.
|
34
|
+
#
|
35
|
+
# @param [Array<Symbol>] category names
|
36
|
+
def remove(*category_names)
|
37
|
+
@redis.pipelined do
|
38
|
+
category_names.each { |category_name| hdel(category_name) }
|
39
|
+
@redis.del(*category_names.map(&method(:make_category_key)))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
include Enumerable
|
44
|
+
|
45
|
+
# Apply process for each category.
|
46
|
+
#
|
47
|
+
# @yield a block to be called for every category
|
48
|
+
# @yieldparam [Inferx::Category] category
|
49
|
+
def each
|
50
|
+
all.each { |category_name| yield spawn(Category, category_name) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'inferx/adapter'
|
2
|
+
|
3
|
+
class Inferx
|
4
|
+
class Category < Adapter
|
5
|
+
|
6
|
+
# @param [Redis] an instance of Redis
|
7
|
+
# @param [Symbol] a category name
|
8
|
+
# @param [String] namespace of keys to be used to Redis
|
9
|
+
def initialize(redis, name, namespace = nil)
|
10
|
+
super(redis, namespace)
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :name
|
15
|
+
|
16
|
+
# Get words with scores in the category.
|
17
|
+
#
|
18
|
+
# @param [Hash] options
|
19
|
+
# - `:score => Integer`: lower limit for getting by score
|
20
|
+
# - `:rank => Integer`: upper limit for getting by rank
|
21
|
+
#
|
22
|
+
# @return [Hash<String, Integer>] words with scores
|
23
|
+
def all(options = {})
|
24
|
+
words_with_scores = if score = options[:score]
|
25
|
+
zrevrangebyscore('+inf', score, :withscores => true)
|
26
|
+
else
|
27
|
+
rank = options[:rank] || -1
|
28
|
+
zrevrange(0, rank, :withscores => true)
|
29
|
+
end
|
30
|
+
|
31
|
+
size = words_with_scores.size
|
32
|
+
index = 1
|
33
|
+
|
34
|
+
while index < size
|
35
|
+
words_with_scores[index] = words_with_scores[index].to_i
|
36
|
+
index += 2
|
37
|
+
end
|
38
|
+
|
39
|
+
Hash[*words_with_scores]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Get score of a word.
|
43
|
+
#
|
44
|
+
# @param [String] a word
|
45
|
+
# @return [Integer, nil]
|
46
|
+
# - when the word is member, score of the word
|
47
|
+
# - when the word is not member, nil
|
48
|
+
def get(word)
|
49
|
+
score = zscore(word)
|
50
|
+
score ? score.to_i : nil
|
51
|
+
end
|
52
|
+
alias [] get
|
53
|
+
|
54
|
+
# Enhance the training data giving words.
|
55
|
+
#
|
56
|
+
# @param [Array<String>] words
|
57
|
+
def train(words)
|
58
|
+
@redis.pipelined do
|
59
|
+
increase = collect(words).inject(0) do |count, pair|
|
60
|
+
zincrby(pair[1], pair[0])
|
61
|
+
count + pair[1]
|
62
|
+
end
|
63
|
+
|
64
|
+
hincrby(name, increase) if increase > 0
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Attenuate the training data giving words.
|
69
|
+
#
|
70
|
+
# @param [Array<String>] words
|
71
|
+
def untrain(words)
|
72
|
+
decrease = 0
|
73
|
+
|
74
|
+
values = @redis.pipelined do
|
75
|
+
decrease = collect(words).inject(0) do |count, pair|
|
76
|
+
zincrby(-pair[1], pair[0])
|
77
|
+
count + pair[1]
|
78
|
+
end
|
79
|
+
|
80
|
+
zremrangebyscore('-inf', 0)
|
81
|
+
end
|
82
|
+
|
83
|
+
values[0..-2].each do |score|
|
84
|
+
score = score.to_i
|
85
|
+
decrease += score if score < 0
|
86
|
+
end
|
87
|
+
|
88
|
+
hincrby(name, -decrease) if decrease > 0
|
89
|
+
end
|
90
|
+
|
91
|
+
# Get total of scores.
|
92
|
+
#
|
93
|
+
# @return [Integer] total of scores
|
94
|
+
def size
|
95
|
+
(hget(name) || 0).to_i
|
96
|
+
end
|
97
|
+
|
98
|
+
# Get effectively scores for each word.
|
99
|
+
#
|
100
|
+
# @param [Array<String>] words
|
101
|
+
# @param [Hash<String, Integer>] words with scores prepared in advance for
|
102
|
+
# reduce access to Redis
|
103
|
+
# @return [Array<Integer>] scores for each word
|
104
|
+
def scores(words, words_with_scores = {})
|
105
|
+
scores = @redis.pipelined do
|
106
|
+
words.each do |word|
|
107
|
+
zscore(word) unless words_with_scores[word]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
index = 0
|
112
|
+
|
113
|
+
next_score = lambda do
|
114
|
+
score = scores[index]
|
115
|
+
index += 1
|
116
|
+
score ? score.to_i : nil
|
117
|
+
end
|
118
|
+
|
119
|
+
words.map { |word| words_with_scores[word] || next_score[] }
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
%w(zrevrange zrevrangebyscore zscore zincrby zremrangebyscore).each do |command|
|
125
|
+
define_method(command) do |*args|
|
126
|
+
@category_key ||= make_category_key(@name)
|
127
|
+
@redis.__send__(command, @category_key, *args)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def collect(words)
|
132
|
+
words.inject({}) do |hash, word|
|
133
|
+
hash[word] ||= 0
|
134
|
+
hash[word] += 1
|
135
|
+
hash
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/inferx.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
require 'inferx/version'
|
4
|
+
require 'inferx/categories'
|
5
|
+
|
6
|
+
class Inferx
|
7
|
+
|
8
|
+
# @param [Hash] options
|
9
|
+
# - `:namespace => String`: namespace of keys to be used to Redis
|
10
|
+
#
|
11
|
+
# Other options are passed to Redis#initialize in:
|
12
|
+
# https://github.com/redis/redis-rb
|
13
|
+
def initialize(options = {})
|
14
|
+
namespace = options.delete(:namespace)
|
15
|
+
redis = Redis.new(options)
|
16
|
+
@categories = Categories.new(redis, namespace)
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :categories
|
20
|
+
|
21
|
+
# Get a score of a category according to a set of words.
|
22
|
+
#
|
23
|
+
# @param [Inferx::Category] a category for scoring
|
24
|
+
# @param [Array<String>] a set of words
|
25
|
+
# @return [Float] a score of the category
|
26
|
+
def score(category, words)
|
27
|
+
size = category.size.to_f
|
28
|
+
return -Float::INFINITY unless size > 0
|
29
|
+
words_with_scores = category.all(:rank => 500)
|
30
|
+
scores = category.scores(words, words_with_scores)
|
31
|
+
scores.inject(0) { |s, score| s + Math.log((score || 0.1) / size) }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get a score for each category according to a set of words.
|
35
|
+
#
|
36
|
+
# @param [Array<String>] a set of words
|
37
|
+
# @return [Hash<Symbol, Float>] scores to key a category
|
38
|
+
#
|
39
|
+
# @see #score
|
40
|
+
def classifications(words)
|
41
|
+
words = words.uniq
|
42
|
+
Hash[@categories.map { |category| [category.name, score(category, words)] }]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Classify words to any one category
|
46
|
+
#
|
47
|
+
# @param [Array<String>] a set of words
|
48
|
+
# @return [Symbol] most high-scoring category name
|
49
|
+
#
|
50
|
+
# @see #score
|
51
|
+
# @see #classifications
|
52
|
+
def classify(words)
|
53
|
+
category = classifications(words).max_by { |score| score[1] }
|
54
|
+
category ? category[0] : nil
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'inferx/adapter'
|
3
|
+
|
4
|
+
describe Inferx::Adapter, '#initialize' do
|
5
|
+
it 'sets the instance of Redis to @redis' do
|
6
|
+
redis = redis_stub
|
7
|
+
adapter = described_class.new(redis)
|
8
|
+
adapter.instance_eval { @redis }.should == redis
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'with a namespace' do
|
12
|
+
it 'sets the namespace to @namespace' do
|
13
|
+
adapter = described_class.new(redis_stub, 'example')
|
14
|
+
adapter.instance_eval { @namespace }.should == 'example'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe Inferx::Adapter, '#categories_key' do
|
20
|
+
it "calls #{described_class}#make_category_key" do
|
21
|
+
adapter = described_class.new(redis_stub)
|
22
|
+
adapter.should_receive(:make_categories_key)
|
23
|
+
adapter.categories_key
|
24
|
+
end
|
25
|
+
|
26
|
+
it "calls #{described_class}#make_category_key once in same instance" do
|
27
|
+
adapter = described_class.new(redis_stub)
|
28
|
+
adapter.should_receive(:make_categories_key).and_return('inferx:categories')
|
29
|
+
adapter.categories_key
|
30
|
+
adapter.categories_key
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe Inferx::Adapter, '#make_categories_key' do
|
35
|
+
it 'returns the key for access to categories' do
|
36
|
+
adapter = described_class.new(redis_stub)
|
37
|
+
adapter.categories_key.should == 'inferx:categories'
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'with a namespace' do
|
41
|
+
it 'returns the key included the namespace' do
|
42
|
+
adapter = described_class.new(redis_stub, 'example')
|
43
|
+
adapter.categories_key.should == 'inferx:example:categories'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe Inferx::Adapter, '#make_category_key' do
|
49
|
+
it 'returns the key for access to to scores stored each by word' do
|
50
|
+
adapter = described_class.new(redis_stub)
|
51
|
+
adapter.make_category_key(:red).should == 'inferx:categories:red'
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'with a namespace' do
|
55
|
+
it 'returns the key included the namespace' do
|
56
|
+
adapter = described_class.new(redis_stub, 'example')
|
57
|
+
adapter.make_category_key(:red).should == 'inferx:example:categories:red'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe Inferx::Adapter, '#spawn' do
|
63
|
+
it 'calls constructor of the class with the instance variables and the arguments' do
|
64
|
+
redis = redis_stub
|
65
|
+
adapter = described_class.new(redis, 'example')
|
66
|
+
klass = mock.tap { |m| m.should_receive(:new).with(redis, 'arg1', 'arg2', 'example') }
|
67
|
+
adapter.spawn(klass, 'arg1', 'arg2')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'returns an instance of the class' do
|
71
|
+
adapter = described_class.new(redis_stub)
|
72
|
+
klass = stub.tap { |s| s.stub!(:new).and_return('klass') }
|
73
|
+
adapter.spawn(klass).should == 'klass'
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'inferx/categories'
|
3
|
+
|
4
|
+
describe Inferx::Categories do
|
5
|
+
it 'includes Enumerable' do
|
6
|
+
described_class.should be_include(Enumerable)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe Inferx::Categories, '#initialize' do
|
11
|
+
it 'calls Inferx::Adapter#initialize' do
|
12
|
+
redis = redis_stub
|
13
|
+
categories = described_class.new(redis, 'example')
|
14
|
+
categories.instance_eval { @redis }.should == redis
|
15
|
+
categories.instance_eval { @namespace }.should == 'example'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe Inferx::Categories, '#all' do
|
20
|
+
it 'calls Redis#hkeys' do
|
21
|
+
redis = redis_stub do |s|
|
22
|
+
s.should_receive(:hkeys).with('inferx:categories')
|
23
|
+
end
|
24
|
+
|
25
|
+
categories = described_class.new(redis)
|
26
|
+
categories.all
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns the all categories as Symbol' do
|
30
|
+
redis = redis_stub do |s|
|
31
|
+
s.stub!(:hkeys).and_return(%w(red green blue))
|
32
|
+
end
|
33
|
+
|
34
|
+
categories = described_class.new(redis)
|
35
|
+
categories.all.should == [:red, :green, :blue]
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'returns an empty array if the key is missing' do
|
39
|
+
redis = redis_stub do |s|
|
40
|
+
s.stub!(:hkeys).and_return([])
|
41
|
+
end
|
42
|
+
|
43
|
+
categories = described_class.new(redis)
|
44
|
+
categories.all.should be_empty
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe Inferx::Categories, '#get' do
|
49
|
+
it 'calls Redis#hexists' do
|
50
|
+
redis = redis_stub do |s|
|
51
|
+
s.should_receive(:hexists).with('inferx:categories', :red).and_return(true)
|
52
|
+
end
|
53
|
+
|
54
|
+
categories = described_class.new(redis)
|
55
|
+
categories.get(:red)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'calles Inferx::Category.new with the instance of Redis, the category name and the namepsace' do
|
59
|
+
redis = redis_stub do |s|
|
60
|
+
s.stub!(:hexists).and_return(true)
|
61
|
+
end
|
62
|
+
|
63
|
+
Inferx::Category.should_receive(:new).with(redis, :red, 'example')
|
64
|
+
categories = described_class.new(redis, 'example')
|
65
|
+
categories.get(:red)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'returns an instance of Inferx::Category' do
|
69
|
+
redis = redis_stub do |s|
|
70
|
+
s.stub!(:hexists).and_return(true)
|
71
|
+
end
|
72
|
+
|
73
|
+
categories = described_class.new(redis)
|
74
|
+
categories.get(:red).should be_an(Inferx::Category)
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'with a missing category' do
|
78
|
+
it 'raises ArgumentError' do
|
79
|
+
redis = redis_stub do |s|
|
80
|
+
s.stub!(:hexists).and_return(false)
|
81
|
+
end
|
82
|
+
|
83
|
+
categories = described_class.new(redis)
|
84
|
+
lambda { categories.get(:red) }.should raise_error(ArgumentError, /'red' is missing/)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe Inferx::Categories, '#add' do
|
90
|
+
it 'calls Redis#hsetnx' do
|
91
|
+
redis = redis_stub do |s|
|
92
|
+
s.should_receive(:hsetnx).with('inferx:categories', :red, 0)
|
93
|
+
end
|
94
|
+
|
95
|
+
categories = described_class.new(redis)
|
96
|
+
categories.add(:red)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'calls Redis#hsetnx according to the number of the categories' do
|
100
|
+
redis = redis_stub do |s|
|
101
|
+
s.should_receive(:hsetnx).with('inferx:categories', :red, 0)
|
102
|
+
s.should_receive(:hsetnx).with('inferx:categories', :green, 0)
|
103
|
+
end
|
104
|
+
|
105
|
+
categories = described_class.new(redis)
|
106
|
+
categories.add(:red, :green)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe Inferx::Categories, '#remove' do
|
111
|
+
it 'calls Redis#hdel and Redis#del' do
|
112
|
+
redis = redis_stub do |s|
|
113
|
+
s.should_receive(:hdel).with('inferx:categories', :red)
|
114
|
+
s.should_receive(:del).with('inferx:categories:red')
|
115
|
+
end
|
116
|
+
|
117
|
+
categories = described_class.new(redis)
|
118
|
+
categories.remove(:red)
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'calls Redis#hdel according to the number of the categories' do
|
122
|
+
redis = redis_stub do |s|
|
123
|
+
s.should_receive(:hdel).with('inferx:categories', :red)
|
124
|
+
s.should_receive(:hdel).with('inferx:categories', :green)
|
125
|
+
s.should_receive(:del).with('inferx:categories:red', 'inferx:categories:green')
|
126
|
+
end
|
127
|
+
|
128
|
+
categories = described_class.new(redis)
|
129
|
+
categories.remove(:red, :green)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe Inferx::Categories, '#each' do
|
134
|
+
before do
|
135
|
+
@redis = redis_stub do |s|
|
136
|
+
s.stub!(:hkeys).and_return(%w(red green blue))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'passes an instance of Inferx::Category to the block' do
|
141
|
+
categories = described_class.new(@redis)
|
142
|
+
categories.each { |category| category.should be_an(Inferx::Category) }
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'calls the block according to the number of the categories' do
|
146
|
+
n = 0
|
147
|
+
categories = described_class.new(@redis)
|
148
|
+
categories.each { |category| n += 1 }
|
149
|
+
n.should == 3
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'inferx/category'
|
3
|
+
|
4
|
+
describe Inferx::Category, '#initialize' do
|
5
|
+
it 'calls Inferx::Adapter#initialize' do
|
6
|
+
redis = redis_stub
|
7
|
+
category = described_class.new(redis, :red, 'example')
|
8
|
+
category.instance_eval { @redis }.should == redis
|
9
|
+
category.instance_eval { @namespace }.should == 'example'
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'sets the category name to the name attribute' do
|
13
|
+
category = described_class.new(redis_stub, :red)
|
14
|
+
category.name.should == :red
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe Inferx::Category, '#all' do
|
19
|
+
it 'calls Redis#revrange' do
|
20
|
+
redis = redis_stub do |s|
|
21
|
+
s.should_receive(:zrevrange).with('inferx:categories:red', 0, -1, :withscores => true).and_return([])
|
22
|
+
end
|
23
|
+
|
24
|
+
category = described_class.new(redis, :red)
|
25
|
+
category.all
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'returns the words with the score' do
|
29
|
+
redis = redis_stub do |s|
|
30
|
+
s.stub!(:zrevrange).with('inferx:categories:red', 0, -1, :withscores => true).and_return(%w(apple 2 strawberry 3))
|
31
|
+
end
|
32
|
+
|
33
|
+
category = described_class.new(redis, :red)
|
34
|
+
category.all.should == {'apple' => 2, 'strawberry' => 3}
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'with the rank option' do
|
38
|
+
it 'calls Redis#zrevrange' do
|
39
|
+
redis = redis_stub do |s|
|
40
|
+
s.should_receive(:zrevrange).with('inferx:categories:red', 0, 1000, :withscores => true).and_return([])
|
41
|
+
end
|
42
|
+
|
43
|
+
category = described_class.new(redis, :red)
|
44
|
+
category.all(:rank => 1000)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'with the score option' do
|
49
|
+
it 'calls Redis#zrevrangebyscore' do
|
50
|
+
redis = redis_stub do |s|
|
51
|
+
s.should_receive(:zrevrangebyscore).with('inferx:categories:red', '+inf', 2, :withscores => true).and_return([])
|
52
|
+
end
|
53
|
+
|
54
|
+
category = described_class.new(redis, :red)
|
55
|
+
category.all(:score => 2)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe Inferx::Category, '#get' do
|
61
|
+
it 'calls Redis#zscore' do
|
62
|
+
redis = redis_stub do |s|
|
63
|
+
s.should_receive(:zscore).with('inferx:categories:red', 'apple')
|
64
|
+
end
|
65
|
+
|
66
|
+
category = described_class.new(redis, :red)
|
67
|
+
category.get('apple')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'returns the score as Integer' do
|
71
|
+
redis = redis_stub do |s|
|
72
|
+
s.stub!(:zscore).with('inferx:categories:red', 'apple').and_return('1')
|
73
|
+
end
|
74
|
+
|
75
|
+
category = described_class.new(redis, :red)
|
76
|
+
category.get('apple').should == 1
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'with a missing word' do
|
80
|
+
it 'returns nil' do
|
81
|
+
redis = redis_stub do |s|
|
82
|
+
s.stub!(:zscore).with('inferx:categories:red', 'strawberry').and_return(nil)
|
83
|
+
end
|
84
|
+
|
85
|
+
category = described_class.new(redis, :red)
|
86
|
+
category.get('strawberry').should be_nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe Inferx::Category, '#train' do
|
92
|
+
it 'calls Redis#zincrby and Redis#hincrby' do
|
93
|
+
redis = redis_stub do |s|
|
94
|
+
s.should_receive(:zincrby).with('inferx:categories:red', 2, 'apple')
|
95
|
+
s.should_receive(:zincrby).with('inferx:categories:red', 3, 'strawberry')
|
96
|
+
s.should_receive(:hincrby).with('inferx:categories', :red, 5)
|
97
|
+
end
|
98
|
+
|
99
|
+
category = described_class.new(redis, :red)
|
100
|
+
category.train(%w(apple strawberry apple strawberry strawberry))
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'with no update' do
|
104
|
+
it 'does not call Redis#hincrby' do
|
105
|
+
redis = redis_stub do |s|
|
106
|
+
s.should_not_receive(:hincrby)
|
107
|
+
end
|
108
|
+
|
109
|
+
category = described_class.new(redis, :red)
|
110
|
+
category.train(%w())
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe Inferx::Category, '#untrain' do
|
116
|
+
it 'calls Redis#zincrby, Redis#zremrangebyscore and Redis#hincrby' do
|
117
|
+
redis = redis_stub do |s|
|
118
|
+
s.should_receive(:zincrby).with('inferx:categories:red', -2, 'apple')
|
119
|
+
s.should_receive(:zincrby).with('inferx:categories:red', -3, 'strawberry')
|
120
|
+
s.should_receive(:zremrangebyscore).with('inferx:categories:red', '-inf', 0).and_return(%w(3 -2 1))
|
121
|
+
s.should_receive(:hincrby).with('inferx:categories', :red, -3)
|
122
|
+
end
|
123
|
+
|
124
|
+
category = described_class.new(redis, :red)
|
125
|
+
category.untrain(%w(apple strawberry apple strawberry strawberry))
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'with no update' do
|
129
|
+
it 'does not call Redis#hincrby' do
|
130
|
+
redis = redis_stub do |s|
|
131
|
+
s.stub!(:zincrby)
|
132
|
+
s.stub!(:zremrangebyscore).and_return(%w(-2 -3 2))
|
133
|
+
s.should_not_receive(:hincrby)
|
134
|
+
end
|
135
|
+
|
136
|
+
category = described_class.new(redis, :red)
|
137
|
+
category.untrain(%w(apple strawberry apple strawberry strawberry))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe Inferx::Category, '#size' do
|
143
|
+
it 'calls Redis#hget' do
|
144
|
+
redis = redis_stub do |s|
|
145
|
+
s.should_receive(:hget).with('inferx:categories', :red)
|
146
|
+
end
|
147
|
+
|
148
|
+
category = described_class.new(redis, :red)
|
149
|
+
category.size
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'returns total of the score of the words as Integer' do
|
153
|
+
redis = redis_stub do |s|
|
154
|
+
s.stub!(:hget).and_return('1')
|
155
|
+
end
|
156
|
+
|
157
|
+
category = described_class.new(redis, :red)
|
158
|
+
category.size.should == 1
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'with the missing key' do
|
162
|
+
it 'returns 0' do
|
163
|
+
redis = redis_stub do |s|
|
164
|
+
s.stub!(:hget).and_return(nil)
|
165
|
+
end
|
166
|
+
|
167
|
+
category = described_class.new(redis, :red)
|
168
|
+
category.size.should == 0
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe Inferx::Category, '#scores' do
|
174
|
+
it 'calls Redis#zscore' do
|
175
|
+
redis = redis_stub do |s|
|
176
|
+
s.should_receive(:zscore).with('inferx:categories:red', 'apple')
|
177
|
+
s.should_receive(:zscore).with('inferx:categories:red', 'strawberry')
|
178
|
+
end
|
179
|
+
|
180
|
+
category = described_class.new(redis, :red)
|
181
|
+
category.scores(%w(apple strawberry))
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'returns the scores' do
|
185
|
+
redis = redis_stub do |s|
|
186
|
+
s.stub!(:pipelined).and_return(%w(2 3))
|
187
|
+
end
|
188
|
+
|
189
|
+
category = described_class.new(redis, :red)
|
190
|
+
scores = category.scores(%w(apple strawberry))
|
191
|
+
scores.should == [2, 3]
|
192
|
+
end
|
193
|
+
|
194
|
+
context 'with words with scores' do
|
195
|
+
it 'returns the scores to use the cache' do
|
196
|
+
redis = redis_stub do |s|
|
197
|
+
s.should_not_receive(:zscore).with('inferx:categories:red', 'strawberry')
|
198
|
+
s.stub!(:pipelined).and_return { |&block| block.call; [2] }
|
199
|
+
end
|
200
|
+
|
201
|
+
category = described_class.new(redis, :red)
|
202
|
+
scores = category.scores(%w(apple strawberry), 'strawberry' => 3, 'hoge' => 1)
|
203
|
+
scores.should == [2, 3]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
data/spec/inferx_spec.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'inferx'
|
3
|
+
|
4
|
+
describe Inferx do
|
5
|
+
it 'is an instance of Class' do
|
6
|
+
described_class.should be_an_instance_of(Class)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'has VERSION constant' do
|
10
|
+
described_class.should be_const_defined(:VERSION)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe Inferx, '#initialize' do
|
15
|
+
it "calls #{described_class}::Categories.new with a connection of Redis and the namespace option" do
|
16
|
+
redis = redis_stub
|
17
|
+
Inferx::Categories.should_receive(:new).with(redis, 'example')
|
18
|
+
described_class.new(:namespace => 'example')
|
19
|
+
end
|
20
|
+
|
21
|
+
it "sets an instance of #{described_class}::Categories to the categories attribute" do
|
22
|
+
redis_stub
|
23
|
+
inferx = described_class.new
|
24
|
+
inferx.categories.should be_an(Inferx::Categories)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe Inferx, '#score' do
|
29
|
+
before do
|
30
|
+
@inferx = described_class.new
|
31
|
+
end
|
32
|
+
|
33
|
+
it "calls #{described_class}::Categories#size, #{described_class}::Category#all and #{described_class}::Category#scores" do
|
34
|
+
category = mock.tap do |m|
|
35
|
+
m.should_receive(:size).and_return(5)
|
36
|
+
m.should_receive(:all).with(:rank => 500).and_return('apple' => 2)
|
37
|
+
m.should_receive(:scores).with(%w(apple), 'apple' => 2).and_return([2])
|
38
|
+
end
|
39
|
+
|
40
|
+
@inferx.score(category, %w(apple))
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'returns an expected score' do
|
44
|
+
{
|
45
|
+
[%w(apple), 2, [2]] => 0.0,
|
46
|
+
[%w(apple), 2, [nil]] => -2.995732273553991,
|
47
|
+
[%w(apple), 3, [nil]] => -3.4011973816621555
|
48
|
+
}.each do |args, expected|
|
49
|
+
words, size, scores = args
|
50
|
+
|
51
|
+
category = stub.tap do |s|
|
52
|
+
s.stub!(:size).and_return(size)
|
53
|
+
s.stub!(:scores).and_return(scores)
|
54
|
+
s.stub!(:all)
|
55
|
+
end
|
56
|
+
|
57
|
+
@inferx.score(category, words).should == expected
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'returns a negative infinity number if the category does not have words' do
|
62
|
+
category = stub.tap do |s|
|
63
|
+
s.stub!(:size).and_return(0)
|
64
|
+
end
|
65
|
+
|
66
|
+
score = @inferx.score(category, %w(apple))
|
67
|
+
score.should be_infinite
|
68
|
+
score.should < 0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe Inferx, '#classifications' do
|
73
|
+
before do
|
74
|
+
categories = [:red, :green, :blue].map do |category_name|
|
75
|
+
stub.tap { |s| s.stub!(:name).and_return(category_name) }
|
76
|
+
end
|
77
|
+
|
78
|
+
@inferx = described_class.new.tap do |s|
|
79
|
+
s.instance_eval { @categories = categories }
|
80
|
+
s.stub!(:score).and_return { |category, words| "score of #{category.name}" }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it "calls #{described_class}#score" do
|
85
|
+
@inferx.tap do |m|
|
86
|
+
m.should_receive(:score).with(@inferx.categories[0], %w(apple))
|
87
|
+
m.should_receive(:score).with(@inferx.categories[1], %w(apple))
|
88
|
+
m.should_receive(:score).with(@inferx.categories[2], %w(apple))
|
89
|
+
end
|
90
|
+
|
91
|
+
@inferx.classifications(%w(apple))
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'calls uniq method of the words' do
|
95
|
+
words = %w(apple).tap do |m|
|
96
|
+
m.should_receive(:uniq).and_return(m)
|
97
|
+
end
|
98
|
+
|
99
|
+
@inferx.classifications(words)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'returns the scores to key the category name' do
|
103
|
+
@inferx.classifications(%w(apple)).should == {
|
104
|
+
:red => 'score of red',
|
105
|
+
:green => 'score of green',
|
106
|
+
:blue => 'score of blue'
|
107
|
+
}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe Inferx, '#classify' do
|
112
|
+
before do
|
113
|
+
@inferx = described_class.new.tap do |s|
|
114
|
+
s.stub!(:classifications).and_return(:red => -2, :green => -1, :blue => -3)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it "calls #{described_class}#classifications" do
|
119
|
+
@inferx.tap do |m|
|
120
|
+
m.should_receive(:classifications).with(%w(apple)).and_return(:red => -2)
|
121
|
+
end
|
122
|
+
|
123
|
+
@inferx.classify(%w(apple))
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'returns the most high-scoring category' do
|
127
|
+
@inferx.classify(%w(apple)).should == :green
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'returns nil if the categories is nothing' do
|
131
|
+
@inferx.tap do |s|
|
132
|
+
s.stub!(:classifications).and_return({})
|
133
|
+
end
|
134
|
+
|
135
|
+
@inferx.classify(%w(apple)).should be_nil
|
136
|
+
end
|
137
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: inferx
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.5
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Takahiro Kondo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: It is Naive Bayes classifier, and the training data is kept always by
|
47
|
+
Redis.
|
48
|
+
email:
|
49
|
+
- kondo@atedesign.net
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- LICENSE
|
57
|
+
- README.md
|
58
|
+
- Rakefile
|
59
|
+
- inferx.gemspec
|
60
|
+
- lib/inferx.rb
|
61
|
+
- lib/inferx/adapter.rb
|
62
|
+
- lib/inferx/categories.rb
|
63
|
+
- lib/inferx/category.rb
|
64
|
+
- lib/inferx/version.rb
|
65
|
+
- spec/inferx/adapter_spec.rb
|
66
|
+
- spec/inferx/categories_spec.rb
|
67
|
+
- spec/inferx/category_spec.rb
|
68
|
+
- spec/inferx_spec.rb
|
69
|
+
- spec/spec_helper.rb
|
70
|
+
homepage: ''
|
71
|
+
licenses: []
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.8.21
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Naive Bayes classifier, the training data on Redis
|
94
|
+
test_files:
|
95
|
+
- spec/inferx/adapter_spec.rb
|
96
|
+
- spec/inferx/categories_spec.rb
|
97
|
+
- spec/inferx/category_spec.rb
|
98
|
+
- spec/inferx_spec.rb
|
99
|
+
- spec/spec_helper.rb
|