module TensorStream # varoius utility functions for array processing module ArrayOpsHelper def split_tensor(input, begin_index, end_index, axis = 0) if axis.zero? input[begin_index...end_index] else input.collect do |item| split_tensor(item, begin_index, end_index, axis - 1) end end end def slice_tensor(input, start, size) return input if size.empty? start_index = start.shift current_size = size.shift dimen_size = if current_size == -1 input.size - 1 else start_index + current_size - 1 end input[start_index..dimen_size].collect do |item| if item.is_a?(Array) slice_tensor(item, start.dup, size.dup) else item end end end def array_set!(input, value) input.each_with_index do |element, index| if element.is_a?(Array) array_set(element, value) else input[index] = value[index] end end end def truncate(input, target_shape) rank = get_rank(input) return input if rank.zero? start = Array.new(rank) { 0 } slice_tensor(input, start, target_shape) end def _reduced_shape(input_shape, axes) return [] if axes.nil? # reduce to scalar axes = [axes] unless axes.is_a?(Array) return input_shape if axes.empty? axes.each do |dimen| input_shape[dimen] = 1 end input_shape end def broadcast(input_a, input_b) sa = shape_eval(input_a) sb = shape_eval(input_b) return [input_a, input_b] if sa == sb # descalar if sa.empty? input_a = [input_a] sa = [1] end if sb.empty? input_b = [input_b] sb = [1] end target_shape = shape_diff(sa, sb) if target_shape input_b = broadcast_dimensions(input_b, target_shape) else target_shape = shape_diff(sb, sa) raise "Incompatible shapes for op #{shape_eval(input_a)} vs #{shape_eval(input_b)}" if target_shape.nil? input_a = broadcast_dimensions(input_a, target_shape) end [input_a, input_b] end # explicit broadcasting helper def broadcast_dimensions(input, dims = []) return input if dims.empty? d = dims.shift if input.is_a?(Array) && (get_rank(input) - 1) == dims.size row_to_dup = input.collect { |item| broadcast_dimensions(item, dims.dup) } row_to_dup + Array.new(d) { row_to_dup }.flatten(1) elsif input.is_a?(Array) Array.new(d) { broadcast_dimensions(input, dims.dup) } else Array.new(d + 1) { input } end end # handle 2 tensor math operations def vector_op(vector, vector2, switch = false, safe = true, &block) if get_rank(vector) < get_rank(vector2) # upgrade rank of A duplicated = Array.new(vector2.size) { vector } return vector_op(duplicated, vector2, switch, &block) end return yield(vector, vector2) unless vector.is_a?(Array) vector.each_with_index.collect { |input, index| next vector_op(input, vector2, switch, &block) if input.is_a?(Array) && get_rank(vector) > get_rank(vector2) if safe && vector2.is_a?(Array) next nil if vector2.size != 1 && index >= vector2.size end z = if vector2.is_a?(Array) if index < vector2.size vector2[index] else raise "incompatible tensor shapes used during op" if vector2.size != 1 vector2[0] end else vector2 end if input.is_a?(Array) vector_op(input, z, switch, &block) else switch ? yield(z, input) : yield(input, z) end }.compact end def shape_diff(shape_a, shape_b) return nil if shape_b.size > shape_a.size reversed_a = shape_a.reverse reversed_b = shape_b.reverse reversed_a.each_with_index.collect { |s, index| next s if index >= reversed_b.size return nil if reversed_b[index] > s s - reversed_b[index] }.reverse end def tile_arr(input, dimen, multiples) t = multiples[dimen] if dimen == multiples.size - 1 return nil if t.zero? input * t # ruby array dup else new_arr = input.collect { |sub| tile_arr(sub, dimen + 1, multiples) }.compact return nil if new_arr.empty? new_arr * t end end def process_function_op(a, &block) # ruby scalar if (a.is_a?(Tensor) && a.shape.rank > 0) || a.is_a?(Array) vector_op(a, 0, &block) else yield a, 0 end end def get_rank(value, rank = 0) return rank unless value.is_a?(Array) return rank + 1 if value.empty? get_rank(value[0], rank + 1) end def last_axis(arr) return arr if get_rank(arr) <= 2 arr.inject([]).map do |sub, rows| rows + last_axis(sub) end end def softmax(arr) return arr if arr.empty? if !arr[0].is_a?(Array) c = arr.max arr = arr.map { |a| Math.exp(a - c) } sum = arr.reduce(:+) arr.collect do |input| input / sum end else arr.collect { |input| softmax(input) } end end def softmax_grad(arr) return arr if arr.empty? arr.each_with_index.collect do |input, index| if input.is_a?(Array) softmax_grad(input) else arr.each_with_index.collect do |input2, index2| if index != index2 -input * input2 else input * (1.0 - input) end end end end end def gather(params, indexes) indexes.collect do |index| if index.is_a?(Array) gather(params, index) else params[index] end end end # general case transposition with flat arrays def transpose_with_perm(arr, new_arr, shape, new_shape, perm) arr_size = shape.reduce(:*) divisors = shape.dup.drop(1).reverse.inject([1]) { |a, s| a << s * a.last }.reverse multipliers = new_shape.dup.drop(1).reverse.inject([1]) { |a, s| a << s * a.last }.reverse arr_size.times do |p| ptr = p index = [] divisors.each_with_object(index) do |div, a| a << (ptr / div.to_f).floor ptr = ptr % div end # remap based on perm remaped = perm.map { |x| index[x] } ptr2 = 0 multipliers.each_with_index do |m, idx| ptr2 += remaped[idx] * m end new_arr[ptr2] = arr[p] end [new_arr, new_shape] end def reduce_axis(current_axis, axis, val, keep_dims, &block) return val unless val.is_a?(Array) r = val.collect { |v| reduce_axis(current_axis + 1, axis, v, keep_dims, &block) } should_reduce_axis = axis.nil? || (axis.is_a?(Array) && axis.include?(current_axis)) || (current_axis == axis) if should_reduce_axis reduced_val = r[0] if r.size > 1 reduced_val = if block_given? yield(r[0..val.size]) else r[0..val.size].reduce(:+) end elsif r.empty? reduced_val = yield(nil) end keep_dims ? [reduced_val] : reduced_val else r end end def reduce(val, axis, keep_dims, &block) rank = get_rank(val) return val if axis&.is_a?(Array) && axis&.empty? axis = if axis.nil? nil elsif axis.is_a?(Array) return val if axis.empty? axis.map { |a| a < 0 ? rank - a.abs : a } else axis < 0 ? rank - axis.abs : axis end reduce_axis(0, axis, val, keep_dims, &block) end def arr_pad(arr, paddings, data_type = :float32, rank = 0) raise "padding #{paddings[rank]} needs to have to elements [before, after]" if paddings[rank].size != 2 before = paddings[rank][0] after = paddings[rank][1] pad_value = fp_type?(data_type) ? 0.0 : 0 if arr[0].is_a?(Array) next_dim_elem = arr.collect { |a| arr_pad(a, paddings, data_type, rank + 1) } padding = deep_dup_array(next_dim_elem[0], pad_value) Array.new(before) { padding } + next_dim_elem + Array.new(after) { padding } else Array.new(before) { pad_value } + arr + Array.new(after) { pad_value } end end def deep_dup_array(arr, value = nil) if arr.is_a?(Array) arr.dup.collect do |a| deep_dup_array(a, value) end else value.nil? ? arr : value end end def strided_slice(value, slices = []) current_slice = slices.dup selection = current_slice.shift return value if selection.nil? b, e, stride = selection b = value.size + b if b < 0 e = value.size + e + 1 if e < 0 indexes = if stride < 0 b.downto(e).select.with_index { |elem, index| (index % stride.abs) == 0 } else (b...e).step(stride) end indexes.map do |index| strided_slice(value[index], current_slice) end end def strided_slice_grad(value, grad, x, slices) current_slice = slices.dup selection = current_slice.shift current_shape = x.shift if selection.nil? array_set!(value, grad) end b, e, stride = selection b = value.size + b if b < 0 e = value.size + e + 1 if e < 0 indexes = if stride < 0 b.downto(e).select.with_index { |elem, index| (index % stride.abs) == 0 } else (b...e).step(stride) end indexes.each_with_index do |index, grad_index| if (value[index].is_a?(Array)) strided_slice_grad(value[index], grad[grad_index], x.dup, current_slice.dup) else value[index] = grad[grad_index] end end end end end