machine_classifier 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a1bab4de07763dcf153a9671580b246120cc590e
4
+ data.tar.gz: d15d5e884869e026103eb07f0b2ffa399bec16a9
5
+ SHA512:
6
+ metadata.gz: d4739013aa0015231592b7aec1d461c76f7cb1385612fad180fb7ed373c5c2a3f2d6b450db05c79c88e8a995ac74f1e63ace3149426e0f4097ebb612b262954d
7
+ data.tar.gz: 35bf27dc5a68fc51457493b14e76eb1da624a951e254cdcb84e4b9bb7ce19d66866b42f08e51a628d143779dd956eb9476e1909184bcbdb880300f77a1822b20
@@ -0,0 +1,5 @@
1
+ /bin/stubs
2
+ .bundle
3
+ *.gem
4
+ Gemfile.lock
5
+ pkg
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in machine_classifier.gemspec
4
+ gemspec
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2014 Riccardo Lucatuorto
2
+
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,55 @@
1
+ # MachineClassifier
2
+
3
+ MachineClassifier is a thin wrapper around the APIs used to
4
+ call machine learning classification systems.
5
+
6
+ Currently, only Google Prediction is supported.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'machine_classifier'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install machine_classifier
21
+
22
+ ## Usage
23
+
24
+ ### Google Prediction
25
+
26
+ ```ruby
27
+ require 'machine_classifier'
28
+
29
+ text = 'Some text that I want to classify'
30
+
31
+ configuration = MachineClassifier::Configuration.new do |conf|
32
+ conf.api_version = '1.6'
33
+ conf.developer_email = 'developer_email@example.com'
34
+ conf.private_key = 'Binary data'
35
+ conf.private_key_password = 'private_key_password'
36
+ conf.project = 'Google Prediction Project Name'
37
+ conf.model = 'Google Prediction Model'
38
+ conf.application_name = 'My Appliction'
39
+ conf.application_version = '1.0.0'
40
+ end
41
+
42
+ client = MachineClassifier::Client.new(configuration)
43
+ result = client.call(text)
44
+ raise 'Classification call failed' if not result.success?
45
+ puts "Text: #{text}"
46
+ puts "Label: #{result.winner}"
47
+ ```
48
+
49
+ ## Contributing
50
+
51
+ 1. Fork it ( https://github.com/[my-github-username]/machine_classifier/fork )
52
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
53
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
54
+ 4. Push to the branch (`git push origin my-new-feature`)
55
+ 5. Create a new Pull Request
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ task :default => :spec
5
+
6
+ RSpec::Core::RakeTask.new do |t|
7
+ t.pattern = 'spec/**/*_spec.rb'
8
+ end
@@ -0,0 +1,9 @@
1
+ require 'machine_classifier/version'
2
+ require 'machine_classifier/client'
3
+ require 'machine_classifier/authorizer'
4
+ require 'machine_classifier/result'
5
+ require 'machine_classifier/configuration'
6
+
7
+ module MachineClassifier
8
+ # Your code goes here...
9
+ end
@@ -0,0 +1,23 @@
1
+ module MachineClassifier
2
+ class Authorizer < Struct.new(:configuration)
3
+ SERVICE_URL = 'https://www.googleapis.com/auth/prediction'
4
+
5
+ def authorize
6
+ service_account.authorize
7
+ end
8
+
9
+ private
10
+
11
+ def service_account
12
+ @service_account ||= Google::APIClient::JWTAsserter.new(
13
+ configuration.developer_email,
14
+ SERVICE_URL,
15
+ key
16
+ )
17
+ end
18
+
19
+ def key
20
+ OpenSSL::PKCS12.new(configuration.private_key, configuration.private_key_password).key
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,78 @@
1
+ require 'google/api_client'
2
+
3
+ module MachineClassifier
4
+ class Client < Struct.new(:configuration)
5
+ HEADERS = {'Content-Type' => 'application/json'}
6
+
7
+ def call(text)
8
+ authorize!
9
+
10
+ predict = prediction_api.trainedmodels.predict
11
+ body = {input: {csvInstance: [text]}}.to_json
12
+
13
+ result = api_client.execute(
14
+ api_method: predict,
15
+ parameters: {'id' => model_id, 'project' => project_id},
16
+ body: body,
17
+ headers: HEADERS
18
+ )
19
+ data = JSON.parse(result.body)
20
+ Result.new(data)
21
+ end
22
+
23
+ def categories
24
+ output = metadata['dataDescription']['outputFeature']['text']
25
+ output.map { |category| category['value'] }
26
+ end
27
+
28
+ def confusion
29
+ metadata['modelDescription']['confusionMatrix']
30
+ end
31
+
32
+ def metadata
33
+ return @metadata if @metadata
34
+
35
+ authorize!
36
+
37
+ analyze = prediction_api.trainedmodels.analyze
38
+
39
+ result = api_client.execute(
40
+ api_method: analyze,
41
+ parameters: {'id' => model_id, 'project' => project_id},
42
+ headers: HEADERS
43
+ )
44
+ @metadata = JSON.parse(result.body)
45
+ end
46
+
47
+ private
48
+
49
+ def authorize!
50
+ return unless api_client.authorization.access_token.nil?
51
+ api_client.authorization = authorizer.authorize
52
+ end
53
+
54
+ def prediction_api
55
+ api_client.discovered_api('prediction', configuration.api_version)
56
+ end
57
+
58
+ def authorizer
59
+ @authorizer ||= MachineClassifier::Authorizer.new(configuration)
60
+ end
61
+
62
+ def api_client
63
+ @api_client ||= Google::APIClient.new(
64
+ application_name: configuration.application_name,
65
+ application_version: configuration.application_version,
66
+ )
67
+ end
68
+
69
+ def project_id
70
+ configuration.project
71
+ end
72
+
73
+ def model_id
74
+ configuration.model
75
+ end
76
+ end
77
+ end
78
+
@@ -0,0 +1,27 @@
1
+ module MachineClassifier
2
+ class Configuration
3
+ ATTRIBUTES = [
4
+ :api_version,
5
+ :application_name,
6
+ :application_version,
7
+ :developer_email,
8
+ :model,
9
+ :private_key,
10
+ :private_key_password,
11
+ :project
12
+ ]
13
+
14
+ ATTRIBUTES.each { |attribute| attr_accessor attribute }
15
+
16
+ def initialize
17
+ yield self if block_given?
18
+ end
19
+
20
+ def valid?
21
+ ATTRIBUTES.each do |attribute|
22
+ return false if self.send(attribute).nil? || self.send(attribute).empty?
23
+ end
24
+ true
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ module MachineClassifier
2
+ class Result < Struct.new(:data)
3
+ def winner
4
+ data['outputLabel']
5
+ end
6
+
7
+ def success?
8
+ data['kind'] == 'prediction#output'
9
+ end
10
+
11
+ def error?
12
+ data.include?('error')
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,3 @@
1
+ module MachineClassifier
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'machine_classifier/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'machine_classifier'
8
+ spec.version = MachineClassifier::VERSION
9
+ spec.authors = ['Joe Yates, Riccardo Lucatuorto']
10
+ spec.email = ['joe.g.yates@gmail.com, gnuduncan@gmail.com']
11
+ spec.summary = %q{A client for calling Machine learning classifiers.}
12
+ spec.description = %q{Call GooglePrediction's classification API}
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_runtime_dependency 'google-api-client'
22
+
23
+ spec.add_development_dependency 'bundler'
24
+ spec.add_development_dependency 'rake'
25
+ spec.add_development_dependency 'rspec'
26
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+ require 'google/api_client'
3
+
4
+ describe MachineClassifier::Authorizer do
5
+ let(:configuration) do
6
+ double(
7
+ 'MachineClassifier::Configuration',
8
+ private_key: private_key,
9
+ private_key_password: 'foo',
10
+ developer_email: 'dev@example.com'
11
+ )
12
+ end
13
+ let(:private_key) { 'BinaryPrivateData' }
14
+
15
+ subject do
16
+ MachineClassifier::Authorizer.new(configuration)
17
+ end
18
+
19
+ describe '#initialize' do
20
+ it 'expects a configuration' do
21
+ expect(subject.configuration).to eq(configuration)
22
+ end
23
+ end
24
+
25
+ describe '#authorize' do
26
+ let(:pkcs12_key) { 'pkcs12_key' }
27
+ let(:pkcs12) { double('PKCS12', key: pkcs12_key) }
28
+ let(:authorization) { double('Authorization') }
29
+ let(:service_account) { double('Google::APIClient::JWTAsserter', authorize: authorization) }
30
+ let(:prediction_url) { 'https://www.googleapis.com/auth/prediction' }
31
+
32
+ before do
33
+ allow(OpenSSL::PKCS12).to receive(:new).with(private_key, configuration.private_key_password).and_return(pkcs12)
34
+ allow(Google::APIClient::JWTAsserter).to receive(:new).with(configuration.developer_email, prediction_url, pkcs12_key).and_return(service_account)
35
+ end
36
+
37
+ it 'returns an authorization' do
38
+ expect(subject.authorize).to eq(authorization)
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,129 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+
4
+ describe MachineClassifier::Client do
5
+ let(:client_authorization) { double('ClientAuthorization') }
6
+ let(:api_client) { double('Google::APIClient', authorization: client_authorization) }
7
+ let(:authorizer) { double('MachineClassifier::Authorizer') }
8
+ let(:configuration) do
9
+ double(
10
+ 'MachineClassifier::Configuration',
11
+ api_version: 'v1.6',
12
+ application_name: 'MachineClassifier',
13
+ application_version: 'v0.01',
14
+ developer_email: 'foo@bar.it',
15
+ model: 'name.of.model',
16
+ private_key: '123AF',
17
+ private_key_password: '123456',
18
+ project: 'project.id'
19
+ )
20
+ end
21
+ let(:prediction_settings) {}
22
+
23
+ subject { MachineClassifier::Client.new(configuration) }
24
+
25
+ before do
26
+ allow(MachineClassifier::Authorizer).to receive(:new).and_return(authorizer)
27
+ allow(Google::APIClient).to receive(:new).and_return(api_client)
28
+ end
29
+
30
+ describe '#initialize' do
31
+ it 'expects a configuration parameter' do
32
+ expect(subject.configuration).to eq(configuration)
33
+ end
34
+ end
35
+
36
+ describe '#call' do
37
+ let(:authorization) { double('Authorization') }
38
+ let(:authorizer) { double('MachineClassifier::Authorizer', authorize: authorization) }
39
+ let(:predict) { double('Predict') }
40
+ let(:prediction_resource) { double('Prediction', trainedmodels: double('Hosted', predict: predict)) }
41
+ let(:model) { 'name.of.model' }
42
+ let(:project) { 'project.id' }
43
+ let(:prediction_settings) do
44
+ double(
45
+ 'google_prediction',
46
+ model: model,
47
+ project: project
48
+ )
49
+ end
50
+ let(:text) { 'This is great' }
51
+ let(:scores) do
52
+ {
53
+ 'positive' => 0.87,
54
+ 'neutral' => 0.21,
55
+ 'negative' => 0.02,
56
+ }
57
+ end
58
+
59
+ let(:output_multi) do
60
+ scores.map { |k, v| {'label' => k, 'score' => v} }
61
+ end
62
+
63
+ let(:prediction_url) do
64
+ "https://www.googleapis.com/prediction/v1.6/trainedmodels/#{model}/predict"
65
+ end
66
+ let(:prediction_result) do
67
+ {
68
+ 'kind' => 'prediction#output',
69
+ 'id' => 'sample.sentiment',
70
+ 'selfLink' => prediction_url,
71
+ 'outputLabel' => 'positive',
72
+ 'outputMulti' => output_multi
73
+ }
74
+ end
75
+ let(:body) { prediction_result.to_json }
76
+
77
+ let(:execute_result) { double('ApiCLient::Result', body: body) }
78
+ let(:result) { double('MachineClassifier::Result') }
79
+
80
+ before do
81
+ api_client.stub(:discovered_api).and_return(prediction_resource)
82
+ client_authorization.stub(:access_token).with().and_return(nil)
83
+ api_client.stub(:authorization=)
84
+ api_client.stub(:execute).and_return(execute_result)
85
+ allow(MachineClassifier::Result).to receive(:new).with(prediction_result).and_return(result)
86
+ end
87
+
88
+ before do
89
+ @result = subject.call(text)
90
+ end
91
+
92
+ it 'requests authorization' do
93
+ expect(authorizer).to have_received(:authorize).with()
94
+ end
95
+
96
+ it 'sets client authorization' do
97
+ expect(api_client).to have_received(:authorization=).with(authorization)
98
+ end
99
+
100
+ it 'loads the prediction API' do
101
+ expect(api_client).to have_received(:discovered_api).with('prediction', 'v1.6')
102
+ end
103
+
104
+ it 'loads the predict resource' do
105
+ expect(prediction_resource).to have_received(:trainedmodels).with()
106
+ expect(prediction_resource.trainedmodels).to have_received(:predict).with()
107
+ end
108
+
109
+ it 'calls the API' do
110
+ expected_body = {input: {csvInstance: [text]}}.to_json
111
+ expected_args = {
112
+ api_method: predict,
113
+ parameters: {'id' => model, 'project' => project},
114
+ body: expected_body,
115
+ headers: {'Content-Type' => 'application/json'},
116
+ }
117
+ expect(api_client).to have_received(:execute).with(expected_args)
118
+ end
119
+
120
+ it 'initializes a PredictionResult' do
121
+ expect(MachineClassifier::Result).to have_received(:new).with(prediction_result)
122
+ end
123
+
124
+ it 'returns the PredictionResult' do
125
+ expect(@result).to eq(result)
126
+ end
127
+ end
128
+ end
129
+
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe MachineClassifier::Configuration do
4
+ describe '#initialize' do
5
+ it 'calls a supplied block' do
6
+ calls = 0
7
+ described_class.new { |config| calls += 1 }
8
+ expect(calls).to eq(1)
9
+ end
10
+
11
+ it 'passes itself to a block' do
12
+ config = described_class.new { |config| config.application_name = 'AAAA' }
13
+ expect(config.application_name).to eq('AAAA')
14
+ end
15
+
16
+ specify 'the block is optional' do
17
+ expect { described_class.new }.to_not raise_error
18
+ end
19
+ end
20
+
21
+ describe '#valid?' do
22
+ subject do
23
+ described_class.new do |conf|
24
+ conf.api_version = 'api_version'
25
+ conf.application_name = 'application_name'
26
+ conf.application_version = 'application_version'
27
+ conf.developer_email = 'developer_email'
28
+ conf.model = 'model'
29
+ conf.private_key = 'private_key'
30
+ conf.private_key_password = 'private_key_password'
31
+ conf.project = 'project'
32
+ end
33
+ end
34
+
35
+ context 'with all values set' do
36
+ it 'is true' do
37
+ expect(subject.valid?).to be_true
38
+ end
39
+ end
40
+
41
+ %w(application_name application_version).each do |attribute|
42
+ context "with #{attribute} missing" do
43
+ it "is false" do
44
+ method = "#{attribute}=".intern
45
+ subject.send(method, nil)
46
+ expect(subject.valid?).to be_false
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe MachineClassifier::Result do
4
+ let(:data) do
5
+ {
6
+ 'outputLabel' => 'foo',
7
+ 'outputMulti' => [
8
+ {'label' => 'foo', 'score' => '0.4',},
9
+ {'label' => 'bar', 'score' => '0.3',},
10
+ {'label' => 'baz', 'score' => '0.3',},
11
+ ]
12
+ }
13
+ end
14
+
15
+ subject { MachineClassifier::Result.new(data) }
16
+
17
+ describe '#initialize' do
18
+ it 'expects a data parameter' do
19
+ expect(subject.data).to eq(data)
20
+ end
21
+ end
22
+
23
+ describe '#winner' do
24
+ it 'returns top category' do
25
+ expect(subject.winner).to eq('foo')
26
+ end
27
+ end
28
+
29
+ describe '#success?' do
30
+ it 'is false' do
31
+ expect(subject.success?).to be_false
32
+ end
33
+ end
34
+
35
+ describe '#error?' do
36
+ it 'is false' do
37
+ expect(subject.error?).to be_false
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,2 @@
1
+ require 'rspec'
2
+ require File.expand_path('../lib/machine_classifier', File.dirname(__FILE__))
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: machine_classifier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joe Yates, Riccardo Lucatuorto
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: google-api-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Call GooglePrediction's classification API
70
+ email:
71
+ - joe.g.yates@gmail.com, gnuduncan@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - Gemfile
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - lib/machine_classifier.rb
82
+ - lib/machine_classifier/authorizer.rb
83
+ - lib/machine_classifier/client.rb
84
+ - lib/machine_classifier/configuration.rb
85
+ - lib/machine_classifier/result.rb
86
+ - lib/machine_classifier/version.rb
87
+ - machine_classifier.gemspec
88
+ - spec/machine_classifier/authorizer_spec.rb
89
+ - spec/machine_classifier/client_spec.rb
90
+ - spec/machine_classifier/configuration_spec.rb
91
+ - spec/machine_classifier/result_spec.rb
92
+ - spec/spec_helper.rb
93
+ homepage: ''
94
+ licenses:
95
+ - MIT
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.0.3
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: A client for calling Machine learning classifiers.
117
+ test_files:
118
+ - spec/machine_classifier/authorizer_spec.rb
119
+ - spec/machine_classifier/client_spec.rb
120
+ - spec/machine_classifier/configuration_spec.rb
121
+ - spec/machine_classifier/result_spec.rb
122
+ - spec/spec_helper.rb