inferx 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|