This is a summary of the presentation I did today about for the Crystal Language 1.0 Conference.

Machine Learning with is bindings to the MXNet machine learning/deep learning library and an implementation of the MXNet Gluon deep learning library. is also kind of a port of a port.

A few years ago I decided I wanted to tune up my machine learning skills, but I didn't want to use Python. At about the same time, I discovered both MXNet, with support for multiple programming languages, and mxnet.rb, a set of Ruby bindings to MXNet by Kenta Murata. For a while I was happy. But Kenta's bindings didn't include an implementation of Gluon.

Gluon is a high-level framework for building deep neural networks. I was running through the lessons in Deep Learning - The Straight Dope and bumped into lessons that required Gluon. The obvious solution was to implement Gluon. Eventually parts of my implementation made their way into mxnet.rb.

Around the time my Ruby code was being merged, I realized I'd found another language that I was really jazzed about—Crystal. You all know about Crystal—it's got Ruby-like syntax and Ruby-like ergonomics, but it's blazingly fast. So again I did the obvious thing and ported my code to Crystal.

Lessons Learned

You can almost mechanically convert Ruby code into Crystal code. Consider the following two blocks of code. Which is Ruby and which is Crystal?

def self.import(filename, inputs,
                epoch: 0, ctx: MXNet.cpu,
                allow_missing: false,
                ignore_extra: false)
  output = MXNet::Symbol.load('%s-symbol.json' % filename)
  inputs = [inputs] unless inputs.is_a?(Array)
  inputs = { |i| MXNet::Symbol.var(i) }, inputs).tap do |block|
    if epoch
      filename = '%s-%04d.params' % [filename, epoch]
      arg_dict = MXNet::NDArray.load(filename)
      arg_dict = arg_dict.transform_keys do |k|
        k.gsub(/^(arg:|aux:)/, '')
def self.import(filename, inputs,
                epoch = 0, ctx = MXNet.cpu,
                allow_missing = false,
                ignore_extra = false)
  outputs = [MXNet::Symbol.load("%s-symbol.json" % filename)]
  inputs = [inputs] unless inputs.is_a?(Array)
  inputs = { |i| MXNet::Symbol.var(i) }, inputs).tap do |block|
    if epoch
      filename = "%s-%04d.params" % [filename, epoch]
      arg_dict = MXNet::NDArray.load(filename)
      arg_dict = arg_dict.transform_keys do |k|
        k.gsub(/^(arg:|aux:)/, "")

Adding the missing but required type annotations and fixing the parts of the Ruby code that play fast and loose took the most work—for example, the Ruby (and Python, in its own way) code makes use of method_missing which is difficult to replicate in Crystal.

Crystal Language syntax for defining bindings to native libraries is first class. Seriously, I don't know who made the call but they sure got it right. In contrast, the Ruby bindings were difficult to write, although I did learn a lot about writing Ruby extensions in the process. (Ruby extensions have long been in the same boat as Elisp programming—something I am interested in but can't work up the enthusiasm to actually do.)

Crystal macros are powerful tools for removing boilerplate. Almost every operation you need in MXNet is discoverable and dynamically callable at runtime. The Ruby (and Python) way is to introspect all of the operation metadata (names, parameters, their documentation strings, etc.) at runtime and to dynamically create methods for use by application code. This is pretty slick and arguably what you want in Crystal as well, albeit at compile time.  For performance reasons I wrote a tool that dumped the metadata into a Crystal language data structure ahead of time, and then used macros at compile time to create methods.

Work on has temporarily taken a back seat to work on ktistec, but as ktistec matures I look forward to spending more time on

#crystalconference2021 #machinelearning #mxnet #crystal

en route to 3rd place

we’ve played trickerion a few times now, the last time with the dark alley rules. it’s a good game, balancing strategic planning with enough direct player conflict to be interesting. most information is public knowledge, and, aside from a few dice rolls, there isn’t much left purely to chance.

to win you need to carefully optimize your possible plays every round, and if there’s a weakness, it’s the number of possibilities and the time it takes to carefully weigh them, especially when playing with the dark alley. consider playing with a turn timer.

#trickerion #boardgame

napa cabbage

this is my first attempt at making kimchi.

as a kid, i lived in a very small town in montana, but next door to a korean family. i remember two foods: seaweed and kimchi. i can't remember if i liked them then (probably not) but i think they're amazing now.

i’m patiently working my way toward a 1.0 release of the ktistec server. the 1.0 release must have a stable database schema—that’s the one hard requirement. in terms of features, it’s close now. i’m adding internal metrics for the power user and block lists. then it ships!