CooCoo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/CooCoo.gemspec +47 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +88 -0
  6. data/README.md +123 -0
  7. data/Rakefile +81 -0
  8. data/bin/cuda-dev-info +25 -0
  9. data/bin/cuda-free +28 -0
  10. data/bin/cuda-free-trend +7 -0
  11. data/bin/ffi-gen +267 -0
  12. data/bin/spec_runner_html.sh +42 -0
  13. data/bin/trainer +198 -0
  14. data/bin/trend-cost +13 -0
  15. data/examples/char-rnn.rb +405 -0
  16. data/examples/cifar/cifar.rb +94 -0
  17. data/examples/img-similarity.rb +201 -0
  18. data/examples/math_ops.rb +57 -0
  19. data/examples/mnist.rb +365 -0
  20. data/examples/mnist_classifier.rb +293 -0
  21. data/examples/mnist_dream.rb +214 -0
  22. data/examples/seeds.rb +268 -0
  23. data/examples/seeds_dataset.txt +210 -0
  24. data/examples/t10k-images-idx3-ubyte +0 -0
  25. data/examples/t10k-labels-idx1-ubyte +0 -0
  26. data/examples/train-images-idx3-ubyte +0 -0
  27. data/examples/train-labels-idx1-ubyte +0 -0
  28. data/ext/buffer/Rakefile +50 -0
  29. data/ext/buffer/buffer.pre.cu +727 -0
  30. data/ext/buffer/matrix.pre.cu +49 -0
  31. data/lib/CooCoo.rb +1 -0
  32. data/lib/coo-coo.rb +18 -0
  33. data/lib/coo-coo/activation_functions.rb +344 -0
  34. data/lib/coo-coo/consts.rb +5 -0
  35. data/lib/coo-coo/convolution.rb +298 -0
  36. data/lib/coo-coo/core_ext.rb +75 -0
  37. data/lib/coo-coo/cost_functions.rb +91 -0
  38. data/lib/coo-coo/cuda.rb +116 -0
  39. data/lib/coo-coo/cuda/device_buffer.rb +240 -0
  40. data/lib/coo-coo/cuda/device_buffer/ffi.rb +109 -0
  41. data/lib/coo-coo/cuda/error.rb +51 -0
  42. data/lib/coo-coo/cuda/host_buffer.rb +117 -0
  43. data/lib/coo-coo/cuda/runtime.rb +157 -0
  44. data/lib/coo-coo/cuda/vector.rb +315 -0
  45. data/lib/coo-coo/data_sources.rb +2 -0
  46. data/lib/coo-coo/data_sources/xournal.rb +25 -0
  47. data/lib/coo-coo/data_sources/xournal/bitmap_stream.rb +197 -0
  48. data/lib/coo-coo/data_sources/xournal/document.rb +377 -0
  49. data/lib/coo-coo/data_sources/xournal/loader.rb +144 -0
  50. data/lib/coo-coo/data_sources/xournal/renderer.rb +101 -0
  51. data/lib/coo-coo/data_sources/xournal/saver.rb +99 -0
  52. data/lib/coo-coo/data_sources/xournal/training_document.rb +78 -0
  53. data/lib/coo-coo/data_sources/xournal/training_document/constants.rb +15 -0
  54. data/lib/coo-coo/data_sources/xournal/training_document/document_maker.rb +89 -0
  55. data/lib/coo-coo/data_sources/xournal/training_document/document_reader.rb +105 -0
  56. data/lib/coo-coo/data_sources/xournal/training_document/example.rb +37 -0
  57. data/lib/coo-coo/data_sources/xournal/training_document/sets.rb +76 -0
  58. data/lib/coo-coo/debug.rb +8 -0
  59. data/lib/coo-coo/dot.rb +129 -0
  60. data/lib/coo-coo/drawing.rb +4 -0
  61. data/lib/coo-coo/drawing/cairo_canvas.rb +100 -0
  62. data/lib/coo-coo/drawing/canvas.rb +68 -0
  63. data/lib/coo-coo/drawing/chunky_canvas.rb +101 -0
  64. data/lib/coo-coo/drawing/sixel.rb +214 -0
  65. data/lib/coo-coo/enum.rb +17 -0
  66. data/lib/coo-coo/from_name.rb +58 -0
  67. data/lib/coo-coo/fully_connected_layer.rb +205 -0
  68. data/lib/coo-coo/generation_script.rb +38 -0
  69. data/lib/coo-coo/grapher.rb +140 -0
  70. data/lib/coo-coo/image.rb +286 -0
  71. data/lib/coo-coo/layer.rb +67 -0
  72. data/lib/coo-coo/layer_factory.rb +26 -0
  73. data/lib/coo-coo/linear_layer.rb +59 -0
  74. data/lib/coo-coo/math.rb +607 -0
  75. data/lib/coo-coo/math/abstract_vector.rb +121 -0
  76. data/lib/coo-coo/math/functions.rb +39 -0
  77. data/lib/coo-coo/math/interpolation.rb +7 -0
  78. data/lib/coo-coo/network.rb +264 -0
  79. data/lib/coo-coo/neuron.rb +112 -0
  80. data/lib/coo-coo/neuron_layer.rb +168 -0
  81. data/lib/coo-coo/option_parser.rb +18 -0
  82. data/lib/coo-coo/platform.rb +17 -0
  83. data/lib/coo-coo/progress_bar.rb +11 -0
  84. data/lib/coo-coo/recurrence/backend.rb +99 -0
  85. data/lib/coo-coo/recurrence/frontend.rb +101 -0
  86. data/lib/coo-coo/sequence.rb +187 -0
  87. data/lib/coo-coo/shell.rb +2 -0
  88. data/lib/coo-coo/temporal_network.rb +291 -0
  89. data/lib/coo-coo/trainer.rb +21 -0
  90. data/lib/coo-coo/trainer/base.rb +67 -0
  91. data/lib/coo-coo/trainer/batch.rb +82 -0
  92. data/lib/coo-coo/trainer/batch_stats.rb +27 -0
  93. data/lib/coo-coo/trainer/momentum_stochastic.rb +59 -0
  94. data/lib/coo-coo/trainer/stochastic.rb +47 -0
  95. data/lib/coo-coo/transformer.rb +272 -0
  96. data/lib/coo-coo/vector_layer.rb +194 -0
  97. data/lib/coo-coo/version.rb +3 -0
  98. data/lib/coo-coo/weight_deltas.rb +23 -0
  99. data/prototypes/convolution.rb +116 -0
  100. data/prototypes/linear_drop.rb +51 -0
  101. data/prototypes/recurrent_layers.rb +79 -0
  102. data/www/images/screamer.png +0 -0
  103. data/www/images/screamer.xcf +0 -0
  104. data/www/index.html +82 -0
  105. metadata +373 -0
@@ -0,0 +1,2 @@
1
+ require 'coo-coo/data_sources/xournal.rb'
2
+ require 'coo-coo/data_sources/xournal/training_document.rb'
@@ -0,0 +1,25 @@
1
+ require 'coo-coo/data_sources/xournal/document'
2
+ require 'coo-coo/data_sources/xournal/loader'
3
+ require 'coo-coo/data_sources/xournal/saver'
4
+ require 'coo-coo/data_sources/xournal/renderer'
5
+ require 'coo-coo/data_sources/xournal/training_document'
6
+
7
+ module CooCoo
8
+ module DataSources
9
+ module Xournal
10
+ # Load a Xournal from a file.
11
+ # @param path [String] The file's path.
12
+ # @return [Document]
13
+ def self.from_file(path)
14
+ Loader.from_file(path)
15
+ end
16
+
17
+ # Load Xournal from an XML string.
18
+ # @param xml [String] Unprocessed XML
19
+ # @return [Document]
20
+ def self.from_xml(xml)
21
+ Loader.from_xml(xml)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,197 @@
1
+ require 'pathname'
2
+ require 'coo-coo/math'
3
+ require 'coo-coo/data_sources/xournal/training_document'
4
+ require 'coo-coo/data_sources/xournal/renderer'
5
+ require 'coo-coo/drawing/cairo_canvas'
6
+
7
+ module CooCoo
8
+ module DataSources
9
+ module Xournal
10
+ class BitmapStream
11
+ attr_reader :training_documents
12
+ attr_reader :labels
13
+ attr_reader :example_width, :example_height
14
+ attr_accessor :canvas_klass
15
+ attr_accessor :pen_scale
16
+ attr_reader :use_color
17
+ attr_accessor :shuffle
18
+
19
+ def initialize(options = Hash.new)
20
+ @training_documents = Array.new
21
+ @document_paths = Array.new
22
+ @pen_scale = options.fetch(:pen_scale, 1.0)
23
+ @example_width = options.fetch(:width, 28)
24
+ @example_height = options.fetch(:height, 28)
25
+ @num_labels = options[:num_labels]
26
+ if options[:labels]
27
+ @labels = File.read(options[:labels]).split("\n")
28
+ else
29
+ @labels = Array.new
30
+ end
31
+ @canvas_klass = options.fetch(:canvas, Drawing::CairoCanvas)
32
+ @use_color = options.fetch(:use_color, false)
33
+ @shuffle = options.fetch(:shuffle, 16)
34
+
35
+ options[:training_documents].each do |td|
36
+ add_training_document(td)
37
+ end
38
+ end
39
+
40
+ def add_training_document(path_or_td)
41
+ td = case path_or_td
42
+ when String then TrainingDocument.from_file(path_or_td)
43
+ when Pathname then TrainingDocument.from_file(path_or_td.to_s)
44
+ when TrainingDocument then path_or_td
45
+ else raise ArgumentError.new("#{path_or_td.inspect} is not a String, Pathname, or TrainingDocument")
46
+ end
47
+ process_training_document(td)
48
+ @document_paths << path_or_td unless td == path_or_td
49
+ @training_documents << td
50
+ self
51
+ end
52
+
53
+ def process_training_document(td)
54
+ td.labels.each do |l|
55
+ add_label(l)
56
+ end
57
+ end
58
+
59
+ def size
60
+ training_documents.reduce(0) do |total, td|
61
+ total + td.each_example.reduce(0) do |subtotal, ex|
62
+ subtotal + ex.size
63
+ end
64
+ end
65
+ end
66
+
67
+ def input_size
68
+ example_width * example_height * (@use_color ? 3 : 1)
69
+ end
70
+
71
+ def output_size
72
+ Math.max(@labels.size, @num_labels)
73
+ end
74
+
75
+ def add_label(label)
76
+ @labels << label unless @labels.find_index(label)
77
+ self
78
+ end
79
+
80
+ def encode_label(label)
81
+ i = @labels.find_index(label)
82
+ v = Vector.zeros(output_size)
83
+ v[i] = 1.0
84
+ v
85
+ end
86
+
87
+ def decode_output(output)
88
+ @labels[output.each.with_index.max[1]]
89
+ end
90
+
91
+ def encode_strokes_to_canvas(strokes, canvas)
92
+ canvas.fill_color = 'white'
93
+ canvas.stroke_color = 'white'
94
+ canvas.rect(0, 0, @example_width, @example_height)
95
+ ren = Renderer.new
96
+
97
+ strokes.each do |stroke|
98
+ ren.render_stroke(canvas, stroke, 0, 0, 1, 1, @example_width, @example_height)
99
+ end
100
+ end
101
+
102
+ def encode_strokes(strokes, return_canvas = false)
103
+ canvas = @canvas_klass.new(@example_width, @example_height)
104
+ if pen_scale != 1.0
105
+ strokes = strokes.collect { |s| s.scale(1.0, 1.0, pen_scale) }
106
+ end
107
+
108
+ encode_strokes_to_canvas(strokes, canvas)
109
+
110
+ if return_canvas
111
+ canvas.flush
112
+ else
113
+ canvas.to_vector(!@use_color) / 256.0
114
+ end
115
+ end
116
+
117
+ def each(yield_canvas = false, &block)
118
+ return to_enum(__method__, yield_canvas) unless block_given?
119
+
120
+ training_documents.each do |td|
121
+ stroke_set = 0
122
+
123
+ loop do
124
+ td.each_example.each_slice(shuffle) do |slice|
125
+ examples = slice.collect do |ex|
126
+ strokes = ex.stroke_sets[stroke_set]
127
+ [ ex.label, strokes ] unless strokes.nil? || strokes.empty?
128
+ end.reject(&:nil?)
129
+
130
+ raise StopIteration if examples.empty?
131
+
132
+ examples.shuffle.each do |(label, strokes)|
133
+ yield(encode_label(label), encode_strokes(strokes, yield_canvas))
134
+ end
135
+ end
136
+
137
+ stroke_set += 1
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ if $0 != __FILE__
147
+ require 'ostruct'
148
+
149
+ @options = OpenStruct.new
150
+ @options.training_documents = Array.new
151
+ @options.labels_path = nil
152
+ @options.width = 28
153
+ @options.height = 28
154
+ @options.shuffle = 128
155
+
156
+ require 'coo-coo/option_parser'
157
+
158
+ @opts = CooCoo::OptionParser.new do |o|
159
+ o.banner = "Xournal Training Document Bitmap Stream Generator"
160
+
161
+ o.on('--data-path PATH', String, 'Adds a Xournal training document to be loaded.') do |p|
162
+ @options.training_documents += Dir.glob(p).to_a
163
+ end
164
+
165
+ o.on('--data-labels PATH', String, 'Predefined list of labels to preset the one hot encoding.') do |p|
166
+ @options.labels = p
167
+ end
168
+
169
+ o.on('--data-num-labels NUMBER', Integer, 'Minimum number of labels in the model') do |n|
170
+ @options.num_labels = n.to_i
171
+ end
172
+
173
+ o.on('--data-width NUMBER', Integer, 'Width in pixels of the generated bitmaps.') do |n|
174
+ n = n.to_i
175
+ raise ArgumentError.new('data-width must be > 0') if n <= 0
176
+ @options.width = n
177
+ end
178
+
179
+ o.on('--data-height NUMBER', Integer, 'Height in pixels of the generated bitmaps.') do |n|
180
+ n = n.to_i
181
+ raise ArgumentError.new('data-height must be > 0') if n <= 0
182
+ @options.height = n
183
+ end
184
+
185
+ o.on('--data-shuffle NUMBER', Integer, 'Number of examples to shuffle before yielding.') do |n|
186
+ n = n.to_i
187
+ raise ArgumentError.new('data-shuffle must be > 0') if n <= 0
188
+ @options.shuffle = n
189
+ end
190
+ end
191
+
192
+ def training_set
193
+ CooCoo::DataSources::Xournal::BitmapStream.new(@options.to_h)
194
+ end
195
+
196
+ [ method(:training_set), @opts ]
197
+ end
@@ -0,0 +1,377 @@
1
+ module CooCoo
2
+ module DataSources
3
+ module Xournal
4
+ Colors = %w(black blue red green gray lightblue lightgreen magenta orange yellow white)
5
+
6
+ # The root of a Xournal document. Each document contains multiple
7
+ # {Page pages} which contain {Layer layers} with actual ink {Stroke strokes}, {Text text}, and {Image images}.
8
+ #
9
+ # More information on what is allowed can be found at:
10
+ # {http://xournal.sourceforge.net/manual.html#file-format}
11
+ class Document
12
+ VERSION = '0.4.8'
13
+
14
+ attr_accessor :title
15
+ attr_accessor :version
16
+ attr_reader :pages
17
+
18
+ def initialize(title = "Untitled Document", version = VERSION)
19
+ @title = title
20
+ @version = version
21
+ @pages = Array.new
22
+ yield(self) if block_given?
23
+ end
24
+
25
+ def add_page(page)
26
+ @pages << page
27
+ self
28
+ end
29
+
30
+ def delete_page_at(page_num)
31
+ @pages.delete_at(page_num)
32
+ self
33
+ end
34
+
35
+ def delete_page(page)
36
+ @pages.delete(page)
37
+ self
38
+ end
39
+
40
+ def each_page(&block)
41
+ @pages.each(&block)
42
+ end
43
+
44
+ def size
45
+ @pages.size
46
+ end
47
+
48
+ def save(*args)
49
+ Saver.save(self, *args)
50
+ end
51
+ end
52
+
53
+ class Page
54
+ attr_accessor :width, :height, :background
55
+ attr_reader :layers
56
+
57
+ def initialize(width, height, background = Background::Default)
58
+ @width = width
59
+ @height = height
60
+ @background = background
61
+ @layers = Array.new
62
+ yield(self) if block_given?
63
+ end
64
+
65
+ def add_layer(layer)
66
+ @layers << layer
67
+ self
68
+ end
69
+
70
+ def delete_layer_at(layer)
71
+ @layers.delete_at(layer)
72
+ end
73
+
74
+ def delete_layer(layer)
75
+ @layers.delete(layer)
76
+ end
77
+
78
+ def each_layer(&block)
79
+ @layers.each(&block)
80
+ end
81
+ end
82
+
83
+ class Background
84
+ attr_accessor :color
85
+ attr_accessor :style
86
+
87
+ Styles = [ 'plain', 'lined', 'ruled', 'graph' ]
88
+
89
+ def initialize(color = 'white', style = 'plain')
90
+ self.color = color
91
+ self.style = style
92
+ end
93
+
94
+ def style=(s)
95
+ raise ArgumentError.new("Invalid style #{s}") unless s == nil || Styles.include?(s)
96
+ @style = s
97
+ end
98
+
99
+ Default = self.new
100
+ end
101
+
102
+ class PixmapBackground
103
+ attr_accessor :domain
104
+ attr_accessor :filename
105
+
106
+ Domains = [ 'absolute', 'attach', 'clone' ]
107
+
108
+ def initialize(filename, domain = 'attach')
109
+ self.filename = filename
110
+ self.domain = domain
111
+ end
112
+
113
+ def domain=(d)
114
+ raise ArgumentError.new("Invalid domain #{d}") unless d == nil || Domains.include?(d)
115
+ @domain = d
116
+ end
117
+ end
118
+
119
+ class PDFBackground
120
+ attr_accessor :domain
121
+ attr_accessor :filename
122
+ attr_accessor :page_number
123
+
124
+ Domains = [ 'absolute', 'attach' ]
125
+
126
+ def initialize(filename, page_number = nil, domain = 'attach')
127
+ self.filename = filename
128
+ self.domain = domain
129
+ self.page_number = page_number
130
+ end
131
+
132
+ def domain=(d)
133
+ raise ArgumentError.new("Invalid domain #{d}") unless d == nil || Domains.include?(d)
134
+ @domain = d
135
+ end
136
+ end
137
+
138
+ class Layer
139
+ attr_reader :children
140
+
141
+ def initialize
142
+ @children = Array.new
143
+ end
144
+
145
+ def each(&block)
146
+ @children.each(&block)
147
+ end
148
+
149
+ def delete_child_at(n)
150
+ @children.delete_at(n)
151
+ self
152
+ end
153
+
154
+ def delete_child(child)
155
+ @children.delete(child)
156
+ self
157
+ end
158
+
159
+ def add_stroke(stroke)
160
+ @children << stroke
161
+ self
162
+ end
163
+
164
+ def strokes
165
+ @children.select { |c| c.kind_of?(Stroke) }
166
+ end
167
+
168
+ def each_stroke(&block)
169
+ strokes.each(&block)
170
+ end
171
+
172
+ def add_text(text)
173
+ @children << text
174
+ self
175
+ end
176
+
177
+ def text
178
+ @children.select { |c| c.kind_of?(Text) }
179
+ end
180
+
181
+ def each_text(&block)
182
+ text.each(&block)
183
+ end
184
+
185
+ def add_image(img)
186
+ @children << img
187
+ end
188
+
189
+ def images
190
+ @children.select { |c| c.kind_of?(Image) }
191
+ end
192
+
193
+ def each_image(&block)
194
+ images.each(&block)
195
+ end
196
+ end
197
+
198
+ class Stroke
199
+ attr_reader :samples
200
+ attr_accessor :tool
201
+ attr_accessor :color
202
+
203
+ Tools = [ 'pen', 'highlighter', 'eraser' ]
204
+ DefaultTool = Tools.first
205
+
206
+ def initialize(tool = 'pen', color = 'black', samples = nil)
207
+ self.tool = tool
208
+ @color = color
209
+ @samples = samples || Array.new
210
+ end
211
+
212
+ def tool=(t)
213
+ raise ArgumentError.new("Invalid tool: #{t.inspect}") unless t == nil || Tools.include?(t)
214
+ @tool = t
215
+ end
216
+
217
+ def add_sample(x, y, w = 1)
218
+ @samples << Sample.new(x, y, w)
219
+ self
220
+ end
221
+
222
+ def delete_sample_at(n)
223
+ @samples.delete_at(n)
224
+ self
225
+ end
226
+
227
+ def delete_sample(sample)
228
+ @samples.delete(sample)
229
+ self
230
+ end
231
+
232
+ def size
233
+ @samples.size
234
+ end
235
+
236
+ def each_sample(&block)
237
+ @samples.each(&block)
238
+ end
239
+
240
+ def translate(dx, dy)
241
+ self.class.new(tool, color, samples.collect { |s| s.translate(dx, dy) })
242
+ end
243
+
244
+ def scale(sx, sy, sw = 1.0)
245
+ self.class.new(tool, color, samples.collect { |s| s.scale(sx, sy, sw) })
246
+ end
247
+
248
+ def minmax
249
+ xmin = nil
250
+ xmax = nil
251
+ ymin = nil
252
+ ymax = nil
253
+
254
+ xmin, xmax = @samples.collect(&:x).minmax
255
+ ymin, ymax = @samples.collect(&:y).minmax
256
+
257
+ [ [ xmin, ymin ], [ xmax, ymax ] ]
258
+ end
259
+ end
260
+
261
+ class Sample
262
+ attr_accessor :width, :x, :y
263
+
264
+ def initialize(x, y, width = nil)
265
+ @x = x
266
+ @y = y
267
+ @width = width
268
+ end
269
+
270
+ def translate(dx, dy)
271
+ self.class.new(x + dx, y + dy, width)
272
+ end
273
+
274
+ def scale(sx, sy, sw)
275
+ self.class.new(x * sx, y * sy, width * sw)
276
+ end
277
+ end
278
+
279
+ class Text
280
+ attr_accessor :text, :size, :x, :y, :color, :font
281
+
282
+ def initialize(text, x, y, size = 12, color = 'black', font = 'Sans')
283
+ @text = text
284
+ @x = x
285
+ @y = y
286
+ @size = size
287
+ @color = color
288
+ @font = font
289
+ end
290
+
291
+ def left
292
+ x
293
+ end
294
+
295
+ def top
296
+ y
297
+ end
298
+
299
+ def right
300
+ x + width
301
+ end
302
+
303
+ def width
304
+ # TODO but how?
305
+ @text.length * @size
306
+ end
307
+
308
+ def bottom
309
+ y + height
310
+ end
311
+
312
+ def height
313
+ @size * @text.count("\n")
314
+ end
315
+ end
316
+
317
+ class Image
318
+ attr_accessor :left, :right, :top, :bottom
319
+ attr_accessor :data, :raw_data
320
+
321
+ def initialize(left, top, right, bottom, data = nil)
322
+ @left = left.to_f
323
+ @top = top.to_f
324
+ @right = right.to_f
325
+ @bottom = bottom.to_f
326
+ self.data = data
327
+ end
328
+
329
+ def data=(data)
330
+ case data
331
+ when String then
332
+ data = Base64.decode64(data)
333
+ @data = decode_image(data) rescue nil
334
+ @raw_data = data
335
+ when ChunkyPNG::Image then
336
+ @data = data
337
+ @raw_data = nil
338
+ when nil then @data = @raw_data = nil
339
+ else @raw_data = data
340
+ end
341
+ end
342
+
343
+ def raw_data
344
+ @raw_data || @data.to_blob
345
+ end
346
+
347
+ def sized_data(zx = 1.0, zy = 1.0)
348
+ if zx == 1.0 && zy == 1.0
349
+ @sized_data ||= @data.resample_bilinear(width, height)
350
+ else
351
+ @data.resample_bilinear((width * zx).to_i, (height * zy).to_i)
352
+ end
353
+ end
354
+
355
+ def width
356
+ (right - left).to_i
357
+ end
358
+
359
+ def height
360
+ (bottom - top).to_i
361
+ end
362
+
363
+ def decode_image(data)
364
+ ChunkyPNG::Image.from_string(data)
365
+ end
366
+
367
+ def data_string
368
+ Base64.encode64(raw_data)
369
+ end
370
+
371
+ def raw_data
372
+ @raw_data ||= @data.to_s
373
+ end
374
+ end
375
+ end
376
+ end
377
+ end