svmkit 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8486463886064e5aa5169dbed20101a01d01101483226cf96e38b377144e153
4
- data.tar.gz: 79807cdbe9f10fba17cd91dbcd68c713180fd986e9b6fab575a849c762bb0d2e
3
+ metadata.gz: 1d52bf496a762b096a5f6dbeec278a1cae8079b53d6c91cc13c07dca7a799fde
4
+ data.tar.gz: e5ca2fed307b82e88dfe816691a4715d62a3187c1cad71421a48bea65037b19c
5
5
  SHA512:
6
- metadata.gz: 2ec6c45c99bda82813644b7b4a043a4dda27810e24e10b4588182cbc3dea106eb05ac013354177861ab4abf552d89670ea1f0d39d50976a232feedc0daee9030
7
- data.tar.gz: 373a566f294bab3d6fb232516f5720a877b6e20999c3253ae49044d759f83b06999fef4e259df91b950fe49c395042c1a74675fadb326460a7e65d54048ca325
6
+ metadata.gz: 620370c119300f3f419550609444eba4aa34561a954e8ec26cf6a0d3522cd32cabf1f6875092de5ab0dd202ebf7b772c1d6d6421cd05d90cfeeeeadea3cd0565
7
+ data.tar.gz: a0d8b5a7b91c4a8e2ffb4312a8082096a2e4fbd411e37bd36752ac19a66f6b9accf22be15894d6a88421b7296bde90ead33b55604085d139e5faec64b97f0f55
data/.rubocop.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  inherit_from: .rubocop_todo.yml
2
2
 
3
3
  AllCops:
4
- TargetRubyVersion: 2.1
4
+ TargetRubyVersion: 2.2
5
5
  DisplayCopNames: true
6
6
  DisplayStyleGuide: true
7
7
 
@@ -39,3 +39,6 @@ Naming/UncommunicativeMethodParamName:
39
39
 
40
40
  Style/FormatStringToken:
41
41
  Enabled: false
42
+
43
+ Style/NumericLiterals:
44
+ Enabled: false
data/HISTORY.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 0.6.1
2
+ - Add abstract class for linear estimator with stochastic gradient descent.
3
+ - Refactor linear estimators to use linear esitmator abstract class.
4
+ - Refactor decistion tree classes to avoid unneeded type conversion.
5
+
1
6
  # 0.6.0
2
7
  - Add class for Principal Component Analysis.
3
8
  - Add class for Non-negative Matrix Factorization.
@@ -121,11 +121,11 @@ module SVMKit
121
121
  # update
122
122
  if update_comps
123
123
  nume = coefficients.transpose.dot(x)
124
- deno = (coefficients.transpose.dot(coefficients)).dot(@components) + @params[:eps]
124
+ deno = coefficients.transpose.dot(coefficients).dot(@components) + @params[:eps]
125
125
  @components *= (nume / deno)
126
126
  end
127
127
  nume = x.dot(@components.transpose)
128
- deno = (coefficients.dot(@components)).dot(@components.transpose) + @params[:eps]
128
+ deno = coefficients.dot(@components).dot(@components.transpose) + @params[:eps]
129
129
  coefficients *= (nume / deno)
130
130
  # normalize
131
131
  norm = Numo::NMath.sqrt((@components**2).sum(1)) + @params[:eps]
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'svmkit/validation'
4
- require 'svmkit/base/base_estimator'
4
+ require 'svmkit/linear_model/sgd_linear_estimator'
5
5
  require 'svmkit/base/regressor'
6
- require 'svmkit/optimizer/nadam'
7
6
 
8
7
  module SVMKit
9
8
  module LinearModel
@@ -19,8 +18,7 @@ module SVMKit
19
18
  # *Reference*
20
19
  # - S. Shalev-Shwartz and Y. Singer, "Pegasos: Primal Estimated sub-GrAdient SOlver for SVM," Proc. ICML'07, pp. 807--814, 2007.
21
20
  # - L. Bottou, "Large-Scale Machine Learning with Stochastic Gradient Descent," Proc. COMPSTAT'10, pp. 177--186, 2010.
22
- class Lasso
23
- include Base::BaseEstimator
21
+ class Lasso < SGDLinearEstimator
24
22
  include Base::Regressor
25
23
  include Validation
26
24
 
@@ -40,29 +38,19 @@ module SVMKit
40
38
  #
41
39
  # @param reg_param [Float] The regularization parameter.
42
40
  # @param fit_bias [Boolean] The flag indicating whether to fit the bias term.
41
+ # @param bias_scale [Float] The scale of the bias term.
43
42
  # @param max_iter [Integer] The maximum number of iterations.
44
43
  # @param batch_size [Integer] The size of the mini batches.
45
44
  # @param optimizer [Optimizer] The optimizer to calculate adaptive learning rate.
46
45
  # If nil is given, Nadam is used.
47
46
  # @param random_seed [Integer] The seed value using to initialize the random generator.
48
- def initialize(reg_param: 1.0, fit_bias: false, max_iter: 1000, batch_size: 10, optimizer: nil, random_seed: nil)
49
- check_params_float(reg_param: reg_param)
47
+ def initialize(reg_param: 1.0, fit_bias: false, bias_scale: 1.0, max_iter: 1000, batch_size: 10, optimizer: nil, random_seed: nil)
48
+ check_params_float(reg_param: reg_param, bias_scale: bias_scale)
50
49
  check_params_integer(max_iter: max_iter, batch_size: batch_size)
51
50
  check_params_boolean(fit_bias: fit_bias)
52
51
  check_params_type_or_nil(Integer, random_seed: random_seed)
53
52
  check_params_positive(reg_param: reg_param, max_iter: max_iter, batch_size: batch_size)
54
- @params = {}
55
- @params[:reg_param] = reg_param
56
- @params[:fit_bias] = fit_bias
57
- @params[:max_iter] = max_iter
58
- @params[:batch_size] = batch_size
59
- @params[:optimizer] = optimizer
60
- @params[:optimizer] ||= Optimizer::Nadam.new
61
- @params[:random_seed] = random_seed
62
- @params[:random_seed] ||= srand
63
- @weight_vec = nil
64
- @bias_term = nil
65
- @rng = Random.new(@params[:random_seed])
53
+ super
66
54
  end
67
55
 
68
56
  # Fit the model with given training data.
@@ -76,14 +64,14 @@ module SVMKit
76
64
  check_sample_tvalue_size(x, y)
77
65
 
78
66
  n_outputs = y.shape[1].nil? ? 1 : y.shape[1]
79
- _n_samples, n_features = x.shape
67
+ n_features = x.shape[1]
80
68
 
81
69
  if n_outputs > 1
82
70
  @weight_vec = Numo::DFloat.zeros(n_outputs, n_features)
83
71
  @bias_term = Numo::DFloat.zeros(n_outputs)
84
- n_outputs.times { |n| @weight_vec[n, true], @bias_term[n] = single_fit(x, y[true, n]) }
72
+ n_outputs.times { |n| @weight_vec[n, true], @bias_term[n] = partial_fit(x, y[true, n]) }
85
73
  else
86
- @weight_vec, @bias_term = single_fit(x, y)
74
+ @weight_vec, @bias_term = partial_fit(x, y)
87
75
  end
88
76
 
89
77
  self
@@ -119,60 +107,32 @@ module SVMKit
119
107
 
120
108
  private
121
109
 
122
- def single_fit(x, y)
123
- # Expand feature vectors for bias term.
124
- samples = @params[:fit_bias] ? expand_feature(x) : x
125
- # Initialize some variables.
126
- n_samples, n_features = samples.shape
127
- rand_ids = [*0...n_samples].shuffle(random: @rng)
128
- weight_vec = Numo::DFloat.zeros(n_features)
129
- left_weight_vec = Numo::DFloat.zeros(n_features)
130
- right_weight_vec = Numo::DFloat.zeros(n_features)
131
- left_optimizer = @params[:optimizer].dup
132
- right_optimizer = @params[:optimizer].dup
133
- # Start optimization.
134
- @params[:max_iter].times do |_t|
135
- # Random sampling.
136
- subset_ids = rand_ids.shift(@params[:batch_size])
137
- rand_ids.concat(subset_ids)
138
- data = samples[subset_ids, true]
139
- values = y[subset_ids]
140
- # Calculate gradients for loss function.
141
- loss_grad = loss_gradient(data, values, weight_vec)
142
- next if loss_grad.ne(0.0).count.zero?
143
- # Update weight.
144
- left_weight_vec = round_weight(left_optimizer.call(left_weight_vec, left_weight_gradient(loss_grad, data)))
145
- right_weight_vec = round_weight(right_optimizer.call(right_weight_vec, right_weight_gradient(loss_grad, data)))
146
- weight_vec = left_weight_vec - right_weight_vec
147
- end
148
- split_weight_vec_bias(weight_vec)
110
+ def partial_fit(x, y)
111
+ n_features = @params[:fit_bias] ? x.shape[1] + 1 : x.shape[1]
112
+ @left_weight = Numo::DFloat.zeros(n_features)
113
+ @right_weight = Numo::DFloat.zeros(n_features)
114
+ @left_optimizer = @params[:optimizer].dup
115
+ @right_optimizer = @params[:optimizer].dup
116
+ super
149
117
  end
150
118
 
151
- def loss_gradient(x, y, weight)
119
+ def calc_loss_gradient(x, y, weight)
152
120
  2.0 * (x.dot(weight) - y)
153
121
  end
154
122
 
155
- def left_weight_gradient(loss_grad, data)
156
- ((@params[:reg_param] + loss_grad).expand_dims(1) * data).mean(0)
123
+ def calc_new_weight(_optimizer, x, _weight, loss_gradient)
124
+ @left_weight = round_weight(@left_optimizer.call(@left_weight, calc_weight_gradient(loss_gradient, x)))
125
+ @right_weight = round_weight(@right_optimizer.call(@right_weight, calc_weight_gradient(-loss_gradient, x)))
126
+ @left_weight - @right_weight
157
127
  end
158
128
 
159
- def right_weight_gradient(loss_grad, data)
160
- ((@params[:reg_param] - loss_grad).expand_dims(1) * data).mean(0)
129
+ def calc_weight_gradient(loss_gradient, data)
130
+ ((@params[:reg_param] + loss_gradient).expand_dims(1) * data).mean(0)
161
131
  end
162
132
 
163
133
  def round_weight(weight)
164
134
  0.5 * (weight + weight.abs)
165
135
  end
166
-
167
- def expand_feature(x)
168
- Numo::NArray.hstack([x, Numo::DFloat.ones([x.shape[0], 1])])
169
- end
170
-
171
- def split_weight_vec_bias(weight_vec)
172
- weights = @params[:fit_bias] ? weight_vec[0...-1] : weight_vec
173
- bias = @params[:fit_bias] ? weight_vec[-1] : 0.0
174
- [weights, bias]
175
- end
176
136
  end
177
137
  end
178
138
  end
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'svmkit/validation'
4
- require 'svmkit/base/base_estimator'
4
+ require 'svmkit/linear_model/sgd_linear_estimator'
5
5
  require 'svmkit/base/regressor'
6
- require 'svmkit/optimizer/nadam'
7
6
 
8
7
  module SVMKit
9
8
  module LinearModel
@@ -16,8 +15,7 @@ module SVMKit
16
15
  # estimator.fit(training_samples, traininig_values)
17
16
  # results = estimator.predict(testing_samples)
18
17
  #
19
- class LinearRegression
20
- include Base::BaseEstimator
18
+ class LinearRegression < SGDLinearEstimator
21
19
  include Base::Regressor
22
20
  include Validation
23
21
 
@@ -36,27 +34,20 @@ module SVMKit
36
34
  # Create a new ordinary least square linear regressor.
37
35
  #
38
36
  # @param fit_bias [Boolean] The flag indicating whether to fit the bias term.
37
+ # @param bias_scale [Float] The scale of the bias term.
39
38
  # @param max_iter [Integer] The maximum number of iterations.
40
39
  # @param batch_size [Integer] The size of the mini batches.
41
40
  # @param optimizer [Optimizer] The optimizer to calculate adaptive learning rate.
42
41
  # If nil is given, Nadam is used.
43
42
  # @param random_seed [Integer] The seed value using to initialize the random generator.
44
- def initialize(fit_bias: false, max_iter: 1000, batch_size: 10, optimizer: nil, random_seed: nil)
43
+ def initialize(fit_bias: false, bias_scale: 1.0, max_iter: 1000, batch_size: 10, optimizer: nil, random_seed: nil)
44
+ check_params_float(bias_scale: bias_scale)
45
45
  check_params_integer(max_iter: max_iter, batch_size: batch_size)
46
46
  check_params_boolean(fit_bias: fit_bias)
47
47
  check_params_type_or_nil(Integer, random_seed: random_seed)
48
48
  check_params_positive(max_iter: max_iter, batch_size: batch_size)
49
- @params = {}
50
- @params[:fit_bias] = fit_bias
51
- @params[:max_iter] = max_iter
52
- @params[:batch_size] = batch_size
53
- @params[:optimizer] = optimizer
54
- @params[:optimizer] ||= Optimizer::Nadam.new
55
- @params[:random_seed] = random_seed
56
- @params[:random_seed] ||= srand
57
- @weight_vec = nil
58
- @bias_term = nil
59
- @rng = Random.new(@params[:random_seed])
49
+ super(reg_param: 0.0, fit_bias: fit_bias, bias_scale: bias_scale,
50
+ max_iter: max_iter, batch_size: batch_size, optimizer: optimizer, random_seed: random_seed)
60
51
  end
61
52
 
62
53
  # Fit the model with given training data.
@@ -75,9 +66,9 @@ module SVMKit
75
66
  if n_outputs > 1
76
67
  @weight_vec = Numo::DFloat.zeros(n_outputs, n_features)
77
68
  @bias_term = Numo::DFloat.zeros(n_outputs)
78
- n_outputs.times { |n| @weight_vec[n, true], @bias_term[n] = single_fit(x, y[true, n]) }
69
+ n_outputs.times { |n| @weight_vec[n, true], @bias_term[n] = partial_fit(x, y[true, n]) }
79
70
  else
80
- @weight_vec, @bias_term = single_fit(x, y)
71
+ @weight_vec, @bias_term = partial_fit(x, y)
81
72
  end
82
73
 
83
74
  self
@@ -113,47 +104,9 @@ module SVMKit
113
104
 
114
105
  private
115
106
 
116
- def single_fit(x, y)
117
- # Expand feature vectors for bias term.
118
- samples = @params[:fit_bias] ? expand_feature(x) : x
119
- # Initialize some variables.
120
- n_samples, n_features = samples.shape
121
- rand_ids = [*0...n_samples].shuffle(random: @rng)
122
- weight_vec = Numo::DFloat.zeros(n_features)
123
- optimizer = @params[:optimizer].dup
124
- # Start optimization.
125
- @params[:max_iter].times do |_t|
126
- # Random sampling.
127
- subset_ids = rand_ids.shift(@params[:batch_size])
128
- rand_ids.concat(subset_ids)
129
- data = samples[subset_ids, true]
130
- values = y[subset_ids]
131
- # Calculate gradients for loss function.
132
- loss_grad = loss_gradient(data, values, weight_vec)
133
- next if loss_grad.ne(0.0).count.zero?
134
- # Update weight.
135
- weight_vec = optimizer.call(weight_vec, weight_gradient(loss_grad, data, weight_vec))
136
- end
137
- split_weight_vec_bias(weight_vec)
138
- end
139
-
140
- def loss_gradient(x, y, weight)
107
+ def calc_loss_gradient(x, y, weight)
141
108
  2.0 * (x.dot(weight) - y)
142
109
  end
143
-
144
- def weight_gradient(loss_grad, data, _weight)
145
- (loss_grad.expand_dims(1) * data).mean(0)
146
- end
147
-
148
- def expand_feature(x)
149
- Numo::NArray.hstack([x, Numo::DFloat.ones([x.shape[0], 1])])
150
- end
151
-
152
- def split_weight_vec_bias(weight_vec)
153
- weights = @params[:fit_bias] ? weight_vec[0...-1] : weight_vec
154
- bias = @params[:fit_bias] ? weight_vec[-1] : 0.0
155
- [weights, bias]
156
- end
157
110
  end
158
111
  end
159
112
  end
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'svmkit/validation'
4
- require 'svmkit/base/base_estimator'
4
+ require 'svmkit/linear_model/sgd_linear_estimator'
5
5
  require 'svmkit/base/classifier'
6
- require 'svmkit/optimizer/nadam'
7
6
 
8
7
  module SVMKit
9
8
  module LinearModel
@@ -19,8 +18,7 @@ module SVMKit
19
18
  #
20
19
  # *Reference*
21
20
  # - S. Shalev-Shwartz, Y. Singer, N. Srebro, and A. Cotter, "Pegasos: Primal Estimated sub-GrAdient SOlver for SVM," Mathematical Programming, vol. 127 (1), pp. 3--30, 2011.
22
- class LogisticRegression
23
- include Base::BaseEstimator
21
+ class LogisticRegression < SGDLinearEstimator
24
22
  include Base::Classifier
25
23
  include Validation
26
24
 
@@ -58,20 +56,8 @@ module SVMKit
58
56
  check_params_boolean(fit_bias: fit_bias)
59
57
  check_params_type_or_nil(Integer, random_seed: random_seed)
60
58
  check_params_positive(reg_param: reg_param, bias_scale: bias_scale, max_iter: max_iter, batch_size: batch_size)
61
- @params = {}
62
- @params[:reg_param] = reg_param
63
- @params[:fit_bias] = fit_bias
64
- @params[:bias_scale] = bias_scale
65
- @params[:max_iter] = max_iter
66
- @params[:batch_size] = batch_size
67
- @params[:optimizer] = optimizer
68
- @params[:optimizer] ||= Optimizer::Nadam.new
69
- @params[:random_seed] = random_seed
70
- @params[:random_seed] ||= srand
71
- @weight_vec = nil
72
- @bias_term = nil
59
+ super
73
60
  @classes = nil
74
- @rng = Random.new(@params[:random_seed])
75
61
  end
76
62
 
77
63
  # Fit the model with given training data.
@@ -86,21 +72,19 @@ module SVMKit
86
72
 
87
73
  @classes = Numo::Int32[*y.to_a.uniq.sort]
88
74
  n_classes = @classes.size
89
- _n_samples, n_features = x.shape
75
+ n_features = x.shape[1]
90
76
 
91
77
  if n_classes > 2
92
78
  @weight_vec = Numo::DFloat.zeros(n_classes, n_features)
93
79
  @bias_term = Numo::DFloat.zeros(n_classes)
94
80
  n_classes.times do |n|
95
81
  bin_y = Numo::Int32.cast(y.eq(@classes[n])) * 2 - 1
96
- weight, bias = binary_fit(x, bin_y)
97
- @weight_vec[n, true] = weight
98
- @bias_term[n] = bias
82
+ @weight_vec[n, true], @bias_term[n] = partial_fit(x, bin_y)
99
83
  end
100
84
  else
101
85
  negative_label = y.to_a.uniq.min
102
86
  bin_y = Numo::Int32.cast(y.ne(negative_label)) * 2 - 1
103
- @weight_vec, @bias_term = binary_fit(x, bin_y)
87
+ @weight_vec, @bias_term = partial_fit(x, bin_y)
104
88
  end
105
89
 
106
90
  self
@@ -169,47 +153,8 @@ module SVMKit
169
153
 
170
154
  private
171
155
 
172
- def binary_fit(x, y)
173
- # Expand feature vectors for bias term.
174
- samples = @params[:fit_bias] ? expand_feature(x) : x
175
- # Initialize some variables.
176
- n_samples, n_features = samples.shape
177
- rand_ids = [*0...n_samples].shuffle(random: @rng)
178
- weight_vec = Numo::DFloat.zeros(n_features)
179
- optimizer = @params[:optimizer].dup
180
- # Start optimization.
181
- @params[:max_iter].times do |_t|
182
- # random sampling
183
- subset_ids = rand_ids.shift(@params[:batch_size])
184
- rand_ids.concat(subset_ids)
185
- data = samples[subset_ids, true]
186
- labels = y[subset_ids]
187
- # calculate gradient for loss function.
188
- loss_grad = loss_gradient(data, labels, weight_vec)
189
- # update weight.
190
- weight_vec = optimizer.call(weight_vec, weight_gradient(loss_grad, data, weight_vec))
191
- end
192
- split_weight_vec_bias(weight_vec)
193
- end
194
-
195
- def loss_gradient(x, y, weight)
196
- z = x.dot(weight)
197
- grad = y / (Numo::NMath.exp(-y * z) + 1.0) - y
198
- grad
199
- end
200
-
201
- def weight_gradient(loss_grad, x, weight)
202
- x.transpose.dot(loss_grad) / @params[:batch_size] + @params[:reg_param] * weight
203
- end
204
-
205
- def expand_feature(x)
206
- Numo::NArray.hstack([x, Numo::DFloat.ones([x.shape[0], 1]) * @params[:bias_scale]])
207
- end
208
-
209
- def split_weight_vec_bias(weight_vec)
210
- weights = @params[:fit_bias] ? weight_vec[0...-1] : weight_vec
211
- bias = @params[:fit_bias] ? weight_vec[-1] : 0.0
212
- [weights, bias]
156
+ def calc_loss_gradient(x, y, weight)
157
+ y / (Numo::NMath.exp(-y * x.dot(weight)) + 1.0) - y
213
158
  end
214
159
  end
215
160
  end
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'svmkit/validation'
4
- require 'svmkit/base/base_estimator'
4
+ require 'svmkit/linear_model/sgd_linear_estimator'
5
5
  require 'svmkit/base/regressor'
6
- require 'svmkit/optimizer/nadam'
7
6
 
8
7
  module SVMKit
9
8
  module LinearModel
@@ -16,8 +15,7 @@ module SVMKit
16
15
  # estimator.fit(training_samples, traininig_values)
17
16
  # results = estimator.predict(testing_samples)
18
17
  #
19
- class Ridge
20
- include Base::BaseEstimator
18
+ class Ridge < SGDLinearEstimator
21
19
  include Base::Regressor
22
20
  include Validation
23
21
 
@@ -37,29 +35,19 @@ module SVMKit
37
35
  #
38
36
  # @param reg_param [Float] The regularization parameter.
39
37
  # @param fit_bias [Boolean] The flag indicating whether to fit the bias term.
38
+ # @param bias_scale [Float] The scale of the bias term.
40
39
  # @param max_iter [Integer] The maximum number of iterations.
41
40
  # @param batch_size [Integer] The size of the mini batches.
42
41
  # @param optimizer [Optimizer] The optimizer to calculate adaptive learning rate.
43
42
  # If nil is given, Nadam is used.
44
43
  # @param random_seed [Integer] The seed value using to initialize the random generator.
45
- def initialize(reg_param: 1.0, fit_bias: false, max_iter: 1000, batch_size: 10, optimizer: nil, random_seed: nil)
46
- check_params_float(reg_param: reg_param)
44
+ def initialize(reg_param: 1.0, fit_bias: false, bias_scale: 1.0, max_iter: 1000, batch_size: 10, optimizer: nil, random_seed: nil)
45
+ check_params_float(reg_param: reg_param, bias_scale: bias_scale)
47
46
  check_params_integer(max_iter: max_iter, batch_size: batch_size)
48
47
  check_params_boolean(fit_bias: fit_bias)
49
48
  check_params_type_or_nil(Integer, random_seed: random_seed)
50
49
  check_params_positive(reg_param: reg_param, max_iter: max_iter, batch_size: batch_size)
51
- @params = {}
52
- @params[:reg_param] = reg_param
53
- @params[:fit_bias] = fit_bias
54
- @params[:max_iter] = max_iter
55
- @params[:batch_size] = batch_size
56
- @params[:optimizer] = optimizer
57
- @params[:optimizer] ||= Optimizer::Nadam.new
58
- @params[:random_seed] = random_seed
59
- @params[:random_seed] ||= srand
60
- @weight_vec = nil
61
- @bias_term = nil
62
- @rng = Random.new(@params[:random_seed])
50
+ super
63
51
  end
64
52
 
65
53
  # Fit the model with given training data.
@@ -73,14 +61,14 @@ module SVMKit
73
61
  check_sample_tvalue_size(x, y)
74
62
 
75
63
  n_outputs = y.shape[1].nil? ? 1 : y.shape[1]
76
- _n_samples, n_features = x.shape
64
+ n_features = x.shape[1]
77
65
 
78
66
  if n_outputs > 1
79
67
  @weight_vec = Numo::DFloat.zeros(n_outputs, n_features)
80
68
  @bias_term = Numo::DFloat.zeros(n_outputs)
81
- n_outputs.times { |n| @weight_vec[n, true], @bias_term[n] = single_fit(x, y[true, n]) }
69
+ n_outputs.times { |n| @weight_vec[n, true], @bias_term[n] = partial_fit(x, y[true, n]) }
82
70
  else
83
- @weight_vec, @bias_term = single_fit(x, y)
71
+ @weight_vec, @bias_term = partial_fit(x, y)
84
72
  end
85
73
 
86
74
  self
@@ -116,47 +104,9 @@ module SVMKit
116
104
 
117
105
  private
118
106
 
119
- def single_fit(x, y)
120
- # Expand feature vectors for bias term.
121
- samples = @params[:fit_bias] ? expand_feature(x) : x
122
- # Initialize some variables.
123
- n_samples, n_features = samples.shape
124
- rand_ids = [*0...n_samples].shuffle(random: @rng)
125
- weight_vec = Numo::DFloat.zeros(n_features)
126
- optimizer = @params[:optimizer].dup
127
- # Start optimization.
128
- @params[:max_iter].times do |_t|
129
- # Random sampling.
130
- subset_ids = rand_ids.shift(@params[:batch_size])
131
- rand_ids.concat(subset_ids)
132
- data = samples[subset_ids, true]
133
- values = y[subset_ids]
134
- # Calculate gradients for loss function.
135
- loss_grad = loss_gradient(data, values, weight_vec)
136
- next if loss_grad.ne(0.0).count.zero?
137
- # Update weight.
138
- weight_vec = optimizer.call(weight_vec, weight_gradient(loss_grad, data, weight_vec))
139
- end
140
- split_weight_vec_bias(weight_vec)
141
- end
142
-
143
- def loss_gradient(x, y, weight)
107
+ def calc_loss_gradient(x, y, weight)
144
108
  2.0 * (x.dot(weight) - y)
145
109
  end
146
-
147
- def weight_gradient(loss_grad, data, weight)
148
- (loss_grad.expand_dims(1) * data).mean(0) + @params[:reg_param] * weight
149
- end
150
-
151
- def expand_feature(x)
152
- Numo::NArray.hstack([x, Numo::DFloat.ones([x.shape[0], 1])])
153
- end
154
-
155
- def split_weight_vec_bias(weight_vec)
156
- weights = @params[:fit_bias] ? weight_vec[0...-1] : weight_vec
157
- bias = @params[:fit_bias] ? weight_vec[-1] : 0.0
158
- [weights, bias]
159
- end
160
110
  end
161
111
  end
162
112
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'svmkit/base/base_estimator'
4
+ require 'svmkit/optimizer/nadam'
5
+
6
+ module SVMKit
7
+ module LinearModel
8
+ # SGDLinearEstimator is an abstract class for implementation of linear estimator
9
+ # with mini-batch stochastic gradient descent optimization.
10
+ # This class is used for internal process.
11
+ class SGDLinearEstimator
12
+ include Base::BaseEstimator
13
+
14
+ # Initialize a linear estimator.
15
+ #
16
+ # @param reg_param [Float] The regularization parameter.
17
+ # @param fit_bias [Boolean] The flag indicating whether to fit the bias term.
18
+ # @param bias_scale [Float] The scale of the bias term.
19
+ # @param max_iter [Integer] The maximum number of iterations.
20
+ # @param batch_size [Integer] The size of the mini batches.
21
+ # @param optimizer [Optimizer] The optimizer to calculate adaptive learning rate.
22
+ # If nil is given, Nadam is used.
23
+ # @param random_seed [Integer] The seed value using to initialize the random generator.
24
+ def initialize(reg_param: 1.0, fit_bias: false, bias_scale: 1.0,
25
+ max_iter: 1000, batch_size: 10, optimizer: nil, random_seed: nil)
26
+ @params = {}
27
+ @params[:reg_param] = reg_param
28
+ @params[:fit_bias] = fit_bias
29
+ @params[:bias_scale] = bias_scale
30
+ @params[:max_iter] = max_iter
31
+ @params[:batch_size] = batch_size
32
+ @params[:optimizer] = optimizer
33
+ @params[:optimizer] ||= Optimizer::Nadam.new
34
+ @params[:random_seed] = random_seed
35
+ @params[:random_seed] ||= srand
36
+ @weight_vec = nil
37
+ @bias_term = nil
38
+ @rng = Random.new(@params[:random_seed])
39
+ end
40
+
41
+ private
42
+
43
+ def partial_fit(x, y)
44
+ # Expand feature vectors for bias term.
45
+ samples = @params[:fit_bias] ? expand_feature(x) : x
46
+ # Initialize some variables.
47
+ n_samples, n_features = samples.shape
48
+ rand_ids = [*0...n_samples].shuffle(random: @rng)
49
+ weight = Numo::DFloat.zeros(n_features)
50
+ optimizer = @params[:optimizer].dup
51
+ # Optimization.
52
+ @params[:max_iter].times do |_t|
53
+ # Random sampling
54
+ subset_ids = rand_ids.shift(@params[:batch_size])
55
+ rand_ids.concat(subset_ids)
56
+ sub_samples = samples[subset_ids, true]
57
+ sub_targets = y[subset_ids]
58
+ # Update weight.
59
+ loss_gradient = calc_loss_gradient(sub_samples, sub_targets, weight)
60
+ next if loss_gradient.ne(0.0).count.zero?
61
+ weight = calc_new_weight(optimizer, sub_samples, weight, loss_gradient)
62
+ end
63
+ split_weight(weight)
64
+ end
65
+
66
+ def calc_loss_gradient(_x, _y, _weight)
67
+ raise NotImplementedError, "#{__method__} has to be implemented in #{self.class}."
68
+ end
69
+
70
+ def calc_new_weight(optimizer, x, weight, loss_gradient)
71
+ weight_gradient = x.transpose.dot(loss_gradient) / @params[:batch_size] + @params[:reg_param] * weight
72
+ optimizer.call(weight, weight_gradient)
73
+ end
74
+
75
+ def expand_feature(x)
76
+ n_samples = x.shape[0]
77
+ Numo::NArray.hstack([x, Numo::DFloat.ones([n_samples, 1]) * @params[:bias_scale]])
78
+ end
79
+
80
+ def split_weight(weight)
81
+ if @params[:fit_bias]
82
+ [weight[0...-1], weight[-1]]
83
+ else
84
+ [weight, 0.0]
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'svmkit/validation'
4
- require 'svmkit/base/base_estimator'
4
+ require 'svmkit/linear_model/sgd_linear_estimator'
5
5
  require 'svmkit/base/classifier'
6
- require 'svmkit/optimizer/nadam'
7
6
  require 'svmkit/probabilistic_output'
8
7
 
9
8
  module SVMKit
@@ -21,8 +20,7 @@ module SVMKit
21
20
  #
22
21
  # *Reference*
23
22
  # - S. Shalev-Shwartz and Y. Singer, "Pegasos: Primal Estimated sub-GrAdient SOlver for SVM," Proc. ICML'07, pp. 807--814, 2007.
24
- class SVC
25
- include Base::BaseEstimator
23
+ class SVC < SGDLinearEstimator
26
24
  include Base::Classifier
27
25
  include Validation
28
26
 
@@ -60,22 +58,11 @@ module SVMKit
60
58
  check_params_boolean(fit_bias: fit_bias, probability: probability)
61
59
  check_params_type_or_nil(Integer, random_seed: random_seed)
62
60
  check_params_positive(reg_param: reg_param, bias_scale: bias_scale, max_iter: max_iter, batch_size: batch_size)
63
- @params = {}
64
- @params[:reg_param] = reg_param
65
- @params[:fit_bias] = fit_bias
66
- @params[:bias_scale] = bias_scale
67
- @params[:max_iter] = max_iter
68
- @params[:batch_size] = batch_size
61
+ super(reg_param: reg_param, fit_bias: fit_bias, bias_scale: bias_scale,
62
+ max_iter: max_iter, batch_size: batch_size, optimizer: optimizer, random_seed: random_seed)
69
63
  @params[:probability] = probability
70
- @params[:optimizer] = optimizer
71
- @params[:optimizer] ||= Optimizer::Nadam.new
72
- @params[:random_seed] = random_seed
73
- @params[:random_seed] ||= srand
74
- @weight_vec = nil
75
- @bias_term = nil
76
64
  @prob_param = nil
77
65
  @classes = nil
78
- @rng = Random.new(@params[:random_seed])
79
66
  end
80
67
 
81
68
  # Fit the model with given training data.
@@ -90,7 +77,7 @@ module SVMKit
90
77
 
91
78
  @classes = Numo::Int32[*y.to_a.uniq.sort]
92
79
  n_classes = @classes.size
93
- _n_samples, n_features = x.shape
80
+ n_features = x.shape[1]
94
81
 
95
82
  if n_classes > 2
96
83
  @weight_vec = Numo::DFloat.zeros(n_classes, n_features)
@@ -98,11 +85,9 @@ module SVMKit
98
85
  @prob_param = Numo::DFloat.zeros(n_classes, 2)
99
86
  n_classes.times do |n|
100
87
  bin_y = Numo::Int32.cast(y.eq(@classes[n])) * 2 - 1
101
- weight, bias = binary_fit(x, bin_y)
102
- @weight_vec[n, true] = weight
103
- @bias_term[n] = bias
88
+ @weight_vec[n, true], @bias_term[n] = partial_fit(x, bin_y)
104
89
  @prob_param[n, true] = if @params[:probability]
105
- SVMKit::ProbabilisticOutput.fit_sigmoid(x.dot(weight.transpose) + bias, bin_y)
90
+ SVMKit::ProbabilisticOutput.fit_sigmoid(x.dot(@weight_vec[n, true].transpose) + @bias_term[n], bin_y)
106
91
  else
107
92
  Numo::DFloat[1, 0]
108
93
  end
@@ -110,7 +95,7 @@ module SVMKit
110
95
  else
111
96
  negative_label = y.to_a.uniq.min
112
97
  bin_y = Numo::Int32.cast(y.ne(negative_label)) * 2 - 1
113
- @weight_vec, @bias_term = binary_fit(x, bin_y)
98
+ @weight_vec, @bias_term = partial_fit(x, bin_y)
114
99
  @prob_param = if @params[:probability]
115
100
  SVMKit::ProbabilisticOutput.fit_sigmoid(x.dot(@weight_vec.transpose) + @bias_term, bin_y)
116
101
  else
@@ -188,50 +173,12 @@ module SVMKit
188
173
 
189
174
  private
190
175
 
191
- def binary_fit(x, y)
192
- # Expand feature vectors for bias term.
193
- samples = @params[:fit_bias] ? expand_feature(x) : x
194
- # Initialize some variables.
195
- n_samples, n_features = samples.shape
196
- rand_ids = [*0...n_samples].shuffle(random: @rng)
197
- weight_vec = Numo::DFloat.zeros(n_features)
198
- optimizer = @params[:optimizer].dup
199
- # Start optimization.
200
- @params[:max_iter].times do |_t|
201
- # random sampling.
202
- subset_ids = rand_ids.shift(@params[:batch_size])
203
- rand_ids.concat(subset_ids)
204
- data = samples[subset_ids, true]
205
- labels = y[subset_ids]
206
- # calculate gradient for loss function.
207
- loss_grad = loss_gradient(data, labels, weight_vec)
208
- next if loss_grad.ne(0.0).count.zero?
209
- # update weight.
210
- weight_vec = optimizer.call(weight_vec, weight_gradient(loss_grad, data, weight_vec))
211
- end
212
- split_weight_vec_bias(weight_vec)
213
- end
214
-
215
- def loss_gradient(x, y, weight)
176
+ def calc_loss_gradient(x, y, weight)
216
177
  target_ids = (x.dot(weight) * y).lt(1.0).where
217
178
  grad = Numo::DFloat.zeros(@params[:batch_size])
218
179
  grad[target_ids] = -y[target_ids]
219
180
  grad
220
181
  end
221
-
222
- def weight_gradient(loss_grad, x, weight)
223
- x.transpose.dot(loss_grad) / @params[:batch_size] + @params[:reg_param] * weight
224
- end
225
-
226
- def expand_feature(x)
227
- Numo::NArray.hstack([x, Numo::DFloat.ones([x.shape[0], 1]) * @params[:bias_scale]])
228
- end
229
-
230
- def split_weight_vec_bias(weight_vec)
231
- weights = @params[:fit_bias] ? weight_vec[0...-1] : weight_vec
232
- bias = @params[:fit_bias] ? weight_vec[-1] : 0.0
233
- [weights, bias]
234
- end
235
182
  end
236
183
  end
237
184
  end
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'svmkit/validation'
4
- require 'svmkit/base/base_estimator'
4
+ require 'svmkit/linear_model/sgd_linear_estimator'
5
5
  require 'svmkit/base/regressor'
6
- require 'svmkit/optimizer/nadam'
7
6
 
8
7
  module SVMKit
9
8
  module LinearModel
@@ -18,8 +17,7 @@ module SVMKit
18
17
  #
19
18
  # *Reference*
20
19
  # 1. S. Shalev-Shwartz and Y. Singer, "Pegasos: Primal Estimated sub-GrAdient SOlver for SVM," Proc. ICML'07, pp. 807--814, 2007.
21
- class SVR
22
- include Base::BaseEstimator
20
+ class SVR < SGDLinearEstimator
23
21
  include Base::Regressor
24
22
  include Validation
25
23
 
@@ -54,20 +52,9 @@ module SVMKit
54
52
  check_params_type_or_nil(Integer, random_seed: random_seed)
55
53
  check_params_positive(reg_param: reg_param, bias_scale: bias_scale, epsilon: epsilon,
56
54
  max_iter: max_iter, batch_size: batch_size)
57
- @params = {}
58
- @params[:reg_param] = reg_param
59
- @params[:fit_bias] = fit_bias
60
- @params[:bias_scale] = bias_scale
55
+ super(reg_param: reg_param, fit_bias: fit_bias, bias_scale: bias_scale,
56
+ max_iter: max_iter, batch_size: batch_size, optimizer: optimizer, random_seed: random_seed)
61
57
  @params[:epsilon] = epsilon
62
- @params[:max_iter] = max_iter
63
- @params[:batch_size] = batch_size
64
- @params[:optimizer] = optimizer
65
- @params[:optimizer] ||= Optimizer::Nadam.new
66
- @params[:random_seed] = random_seed
67
- @params[:random_seed] ||= srand
68
- @weight_vec = nil
69
- @bias_term = nil
70
- @rng = Random.new(@params[:random_seed])
71
58
  end
72
59
 
73
60
  # Fit the model with given training data.
@@ -81,14 +68,14 @@ module SVMKit
81
68
  check_sample_tvalue_size(x, y)
82
69
 
83
70
  n_outputs = y.shape[1].nil? ? 1 : y.shape[1]
84
- _n_samples, n_features = x.shape
71
+ n_features = x.shape[1]
85
72
 
86
73
  if n_outputs > 1
87
74
  @weight_vec = Numo::DFloat.zeros(n_outputs, n_features)
88
75
  @bias_term = Numo::DFloat.zeros(n_outputs)
89
- n_outputs.times { |n| @weight_vec[n, true], @bias_term[n] = single_fit(x, y[true, n]) }
76
+ n_outputs.times { |n| @weight_vec[n, true], @bias_term[n] = partial_fit(x, y[true, n]) }
90
77
  else
91
- @weight_vec, @bias_term = single_fit(x, y)
78
+ @weight_vec, @bias_term = partial_fit(x, y)
92
79
  end
93
80
 
94
81
  self
@@ -124,49 +111,13 @@ module SVMKit
124
111
 
125
112
  private
126
113
 
127
- def single_fit(x, y)
128
- # Expand feature vectors for bias term.
129
- samples = @params[:fit_bias] ? expand_feature(x) : x
130
- # Initialize some variables.
131
- n_samples, n_features = samples.shape
132
- rand_ids = [*0...n_samples].shuffle(random: @rng)
133
- weight_vec = Numo::DFloat.zeros(n_features)
134
- optimizer = @params[:optimizer].dup
135
- # Start optimization.
136
- @params[:max_iter].times do |_t|
137
- # random sampling
138
- subset_ids = rand_ids.shift(@params[:batch_size])
139
- rand_ids.concat(subset_ids)
140
- data = samples[subset_ids, true]
141
- values = y[subset_ids]
142
- # update the weight vector.
143
- loss_grad = loss_gradient(data, values, weight_vec)
144
- weight_vec = optimizer.call(weight_vec, weight_gradient(loss_grad, data, weight_vec))
145
- end
146
- split_weight_vec_bias(weight_vec)
147
- end
148
-
149
- def loss_gradient(x, y, weight)
114
+ def calc_loss_gradient(x, y, weight)
150
115
  z = x.dot(weight)
151
116
  grad = Numo::DFloat.zeros(@params[:batch_size])
152
117
  grad[(z - y).gt(@params[:epsilon]).where] = 1
153
118
  grad[(y - z).gt(@params[:epsilon]).where] = -1
154
119
  grad
155
120
  end
156
-
157
- def weight_gradient(loss_grad, x, weight)
158
- x.transpose.dot(loss_grad) / @params[:batch_size] + @params[:reg_param] * weight
159
- end
160
-
161
- def expand_feature(x)
162
- Numo::NArray.hstack([x, Numo::DFloat.ones([x.shape[0], 1]) * @params[:bias_scale]])
163
- end
164
-
165
- def split_weight_vec_bias(weight_vec)
166
- weights = @params[:fit_bias] ? weight_vec[0...-1] : weight_vec
167
- bias = @params[:fit_bias] ? weight_vec[-1] : 0.0
168
- [weights, bias]
169
- end
170
121
  end
171
122
  end
172
123
  end
@@ -213,7 +213,7 @@ module SVMKit
213
213
  end
214
214
 
215
215
  def put_leaf(node, y)
216
- node.probs = Numo::DFloat[*(@classes.to_a.map { |c| y.eq(c).count })] / node.n_samples
216
+ node.probs = Numo::DFloat.cast(@classes.map { |c| y.eq(c).count_true }) / node.n_samples
217
217
  node.leaf = true
218
218
  node.leaf_id = @n_leaves
219
219
  @n_leaves += 1
@@ -234,18 +234,18 @@ module SVMKit
234
234
  end
235
235
 
236
236
  def splited_ids(features, threshold)
237
- [features.le(threshold).where.to_a, features.gt(threshold).where.to_a]
237
+ [features.le(threshold).where, features.gt(threshold).where]
238
238
  end
239
239
 
240
240
  def gain(labels, labels_left, labels_right)
241
- prob_left = labels_left.size / labels.size.to_f
242
- prob_right = labels_right.size / labels.size.to_f
241
+ prob_left = labels_left.size.fdiv(labels.size)
242
+ prob_right = labels_right.size.fdiv(labels.size)
243
243
  impurity(labels) - prob_left * impurity(labels_left) - prob_right * impurity(labels_right)
244
244
  end
245
245
 
246
246
  def impurity(labels)
247
- posterior_probs = Numo::DFloat[*(labels.to_a.uniq.sort.map { |c| labels.eq(c).count })] / labels.size.to_f
248
- send(@criterion, posterior_probs)
247
+ cls = labels.to_a.uniq.sort
248
+ cls.size == 1 ? 0.0 : send(@criterion, Numo::DFloat[*(cls.map { |c| labels.eq(c).count_true.fdiv(labels.size) })])
249
249
  end
250
250
 
251
251
  def gini(posterior_probs)
@@ -208,7 +208,7 @@ module SVMKit
208
208
  end
209
209
 
210
210
  def splited_ids(features, threshold)
211
- [features.le(threshold).where.to_a, features.gt(threshold).where.to_a]
211
+ [features.le(threshold).where, features.gt(threshold).where]
212
212
  end
213
213
 
214
214
  def gain(values, values_left, values_right)
@@ -3,5 +3,5 @@
3
3
  # SVMKit is a machine learning library in Ruby.
4
4
  module SVMKit
5
5
  # @!visibility private
6
- VERSION = '0.6.0'.freeze
6
+ VERSION = '0.6.1'.freeze
7
7
  end
data/lib/svmkit.rb CHANGED
@@ -19,6 +19,7 @@ require 'svmkit/optimizer/rmsprop'
19
19
  require 'svmkit/optimizer/nadam'
20
20
  require 'svmkit/optimizer/yellow_fin'
21
21
  require 'svmkit/kernel_approximation/rbf'
22
+ require 'svmkit/linear_model/sgd_linear_estimator'
22
23
  require 'svmkit/linear_model/svc'
23
24
  require 'svmkit/linear_model/svr'
24
25
  require 'svmkit/linear_model/logistic_regression'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: svmkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - yoshoku
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-06-28 00:00:00.000000000 Z
11
+ date: 2018-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: numo-narray
@@ -140,6 +140,7 @@ files:
140
140
  - lib/svmkit/linear_model/linear_regression.rb
141
141
  - lib/svmkit/linear_model/logistic_regression.rb
142
142
  - lib/svmkit/linear_model/ridge.rb
143
+ - lib/svmkit/linear_model/sgd_linear_estimator.rb
143
144
  - lib/svmkit/linear_model/svc.rb
144
145
  - lib/svmkit/linear_model/svr.rb
145
146
  - lib/svmkit/model_selection/cross_validation.rb