SLIMEからOpenGLを触れるようにする

Lispの開発においてSLIMEは必須のものであり生命線。

これがあるとないとでは開発効率は10倍以上は変わるだろうし、LispLispたらしめている要因の一つであるのがSLIMEなんだと思う。

そんなSLIMEを使って現在は開発を進めているのだけれど、実は一つ問題が発生していた。

現在趣味でCommon Lispでグラフィック系のものを作っているのだけれど、SLIMEからだとウィンドウが立ち上がらないのである。

これがSLIMEで長いこと目の上のタンコブであったわけだけど、今回なんとか解決したのでメモ書き。

概要

環境はmacOS CatalinaでSBCLのバージョンは1.5.7.55、Emacsは26.3。

そもそもSLIMEとはEmacs起動中に裏側でLispのREPLを起動したサーバーを立てておいて、それをエディタからそのREPLに通信するという方式をとっている。

今回問題が起きていたのは、SLIMEのREPLサーバーに対しての通信方式で、デフォルトだと通信に個別のスレッドを使用する方法を採用している。

これだとREPL上の白黒のデータを動かすようなスレッドはちゃんと動いてもグラフィック系のような別ウィンドウを立ち上げるものはうまくいかない(個別のスレッドなのでデータのやり取りが途切れてしまう)。

common-lisp.net

なので、SLIMEの通信方式をデフォルトの個別のスレッドを使用する方式からループアプローチに変更する必要がある。

コード

自分の環境では、Emacsの設定ファイルはホームディレクトリに設定ファイルを格納したファイルを入れており、SLIMEはそのディレクトリ内に格納している。

実際、

~/.emacs.d
├── init.el
├── slime/
│   ├── swank.lisp
│   ├── README.md
...

というような構造をとっている。

SLIMEの利用に関してはinit.elの中にて(add-to-list 'load-path (expand-file-name "~/.emacs.d/slime"))としてSLIMEのファイルをロードしている。

さて、今回変更を書き加えるのはこの~/.emacs.d/slime/内のswank.lisp

まず、先ほど述べた通り、SLIMEの通信方式を変える。

このswank.lispの130行目あたりから始まるConnectionのところにて、*communication-style*nilから:fd-handlerにする。

(defstruct (connection
             (:constructor %make-connection)
             (:conc-name connection.)
             (:print-function print-connection))
  ;; The listening socket. (usually closed)
  (socket           (missing-arg) :type t :read-only t)
  ;; Character I/O stream of socket connection.  Read-only to avoid
  ;; race conditions during initialization.
  (socket-io        (missing-arg) :type stream :read-only t)
  ;; Optional dedicated output socket (backending `user-output' slot).
  ;; Has a slot so that it can be closed with the connection.
  (dedicated-output nil :type (or stream null))
  ;; Streams that can be used for user interaction, with requests
  ;; redirected to Emacs.
  (user-input       nil :type (or stream null))
  (user-output      nil :type (or stream null))
  (user-io          nil :type (or stream null))
  ;; Bindings used for this connection (usually streams)
  (env '() :type list)
  ;; A stream that we use for *trace-output*; if nil, we user user-output.
  (trace-output     nil :type (or stream null))
  ;; A stream where we send REPL results.
  (repl-results     nil :type (or stream null))
  ;; Cache of macro-indentation information that has been sent to Emacs.
  ;; This is used for preparing deltas to update Emacs's knowledge.
  ;; Maps: symbol -> indentation-specification
  (indentation-cache (make-hash-table :test 'eq) :type hash-table)
  ;; The list of packages represented in the cache:
  (indentation-cache-packages '())
  ;; The communication style used.
  (communication-style :fd-handler :type (member nil :spawn :sigio :fd-handler))  ;; この部分!
  )

次に、swank.lispの最後の方にて、(defun before-init ...(setq *communication-style* nil)と書き加える。

(defun before-init (version load-path)
  (pushnew :swank *features*)
  (setq *swank-wire-protocol-version* version)
  (setq *load-path* load-path)
  (setq *communication-style* nil))  ;; この部分!

この2つをやれば動くようになった。

f:id:komi1230:20191201184202p:plain

果たしてこれが正しいのかはわからないけど、とにかく動くようになって作業が捗るようになったので良いと思う。

まとめ

今回はSLIMEの設定ファイルを直接いじることによってSLIMEからCL-OpenGLを叩けるようにした。

なんとか解決したので、ようやく開発にスピード感が出てくると思われる。

がんばっていきたい。

自分がAI系のことを勉強し始めてから今まで

この記事は Softbank AI部 Advent Calendar 2019 その2 の1日目の記事。

とうとう来てしまった12月。

もう2019年も終わりに近づいており、今年もこうしてアドベントカレンダーの時期が来たわけである。

Softbank AI部のアドベントカレンダーは2つあり、一つ目が

adventar.org

で、その2が

adventar.org

という具合で、この記事はアドベントカレンダーその2の第一日目となる。

実は、このソフトバンクAI部のアドベントカレンダーの企画を提案したのは自分だったりする。

ツイッターを見てると色々な企業がアドベントカレンダーをやっていて、そういうのを見ながら楽しそうだなぁ....と漠然と思っていたのだけれど、縁あって(まだ内定者だけど)Softbank AI部のSlackに入れもらい、そこでちょろっと話を振ってみたら割とみんなやってくれそうな感じがあり、それで話が進んだというのがきっかけ。

なんやかんやで25日分全部埋まっており、本当に感動しております。

本題へ

とりあえず前置きは置いといて本題。

実はこの記事を書き始めるまでどんなことを書こうかずっと悩んでいて、何か開発したものの公開にしたかったのだけど、ここ最近が忙しすぎるというのと使ってるOSSが謎のバグで色々試行錯誤していたら開発が全く捗らず...

ということで開発したものの公開はまだ別の機会に回すとして、このエントリーでは自分がAI系のことを勉強し始めていたら今までの振り返りをしようかなと思う。

AI系の勉強の一番最初

自分がAI系の勉強、特に機械学習を始めたのは学部1回生の春休みとかだったと思う。

大学に入ってからはずっとプログラミングをやっていて、ずっとC言語CUIのゲームを作ったり色々していた。

そんな中、ちょうど2017年の頭くらいにゼロから作るディープラーニングが発売され、当時もメディアがAIガーと騒いでた時期だったので自分もなんとなく勉強しようと思い、その本を買ってAI系の勉強をスタートした感じである。

ゼロから(ryを始めるに際して初めてPythonを使い始めたわけだけれど、ぞれまでずっとC言語を使っていたので、なんと素晴らしい言語なんだろう!!!!と感動した記憶がある。

ゼロ(ryを終えたあとはもっと色々勉強しようと思い、そこでPRMLを読んだり当時松尾研が翻訳していたIan GoodfellowのDeep Learningを読んだりして勉強を進めていた。

これがだいたい学部1回生の春休みから2回生の6月くらいまでの話。

日立での研究

この頃、実家からの仕送りが減り始めたのでバイトをする必要が出てきて、そこでバイト先を探していた。

京大には生協が斡旋するバイト紹介のサイトがあるのだけれど、そこを見てみたら日立製作所 未来課題共同探索部門というのが引っかかっているのを発見。

内容は良くわからなかったけどとりあえず面白そうだったのでなんとなく申し込んでみた。

ものすごく簡単なノリだったが、実はこれが自分の人生を大きく変えた出会いだった。

早速面接をしようということになり、日時を決めて指定されたオフィスに向かう。

そこで面接で出てきたのはスキンヘッドの強面なお兄さんで、自分は正直かなりビビりながらの話していた。

ちなみに余談だが、このスキンヘッドの方はのちの上司となる方で、同時に自分が心の底から尊敬しており、今自分がEmacsを使ったりしているのはその人からの影響がものすごく大きかったりする。

さて、面接の内容としては多岐にわたって、

  • CS : 配列とポインタはメモリでどのような挙動の違いがあるか
  • 機械学習のこと : CNNにおけるパディングはデータ構造に対してどのような効果を与えるか
  • 今までのプログラミング経験 : 言語やGitHubの使用経験など

あたりを聞かれた。

数日後、採用の連絡をもらい、晴れて日立でアルバイトすることになった。

自分はそれまでそのバイトはちょっとした開発をするものだと思っていたのだけれど、実は普通の研究員で、ガッツリ機械学習の研究をするというのが業務内容だった。

で、周りの学生研究員はというと、だいたい修士か博士の人で、なぜかそこから飛んで学部2年の自分がいるという状況で、周りのレベルの高さに震えながらのアルバイト環境だった。

日立でのテーマはいわゆるAIを実現するというやつで、そこから付随するテーマ(転移学習やNLP、計算軽量化とか)を各学生が論文サーベイと実装をして社員向けに発表をするというのが主な業務の進行フローという感じ。

自分に割り当てられたのは強化学習だった。

そこから約一年間、強化学習関係の論文を無限にサーベイしては面白そうなものを追試実装、数値比較をしてネットワークの工夫やデータの入れ方を工夫するなど、本当にちゃんと研究していた。

研究環境は非常に良く、当時は高価だったGeForce 1080Tiを3つ並列で繋げたり、ラズパイ100個並列コンピューティングもできたし、ソファと冷蔵庫は完備、コーヒー飲める、めちゃくちゃオフィスきれい、という感じで最高だった。

そこでぼーっと論文を読んで好きなものを実装しているだけでお金がもらえるというので、ホントに素晴らしい環境だったと思う。

留学

せっかくの大学生活ということで、留学したい気持ちが出てきたので、留学することに。

その際、留学資金の確保のために強化学習の本を書いてクラウドファンディングで売ったりした。

1週間くらいで30万くらい集まり、なんとか留学(スイス)に行けるだけの資金が用意できた。

ちなみに当時の本がこちらなので、良ければご参照くださいませ。

github.com

留学先では主に機械学習系の授業やCSの授業をとったりした。

現地ではハッカソンに出たりして、色々楽しく勉強していたと思う。

帰国、就活

その後、帰国してから今までの云々については就活エントリーとかを書いたのでそちらを参照。

komi.hatenadiary.com

自分がソフトバンクを選んだ理由として、事業的にめちゃくちゃ面白そうだったので、そこにグッときたというのが率直なところ。

エンジニアリングの方ではエンジニア一般のイメージとしてはパッとしないかもだけど、そこらへんに関してはこれから自分が盛り上げていくつもりなので、がんばっていこうと思う。

ちなみに現在の取り組みとして、あまりAI系のことはしっかりやっておらず、どちらかというと開発方面のことばかりやっている。

今はアイフルでバイトをしており、画像認識のシステムの開発などもしたりするが、どちらかというとそういう細かい部分のチューニングをしているよりプロダクションのコードを書くことの方が多く、そういう意味でフロントからサーバー、インフラ、機械学習系、PMをこなすフルスタックエンジニアとして奔走しているのが最近の取り組み。

あと、今は卒業研究で画像の超解像化をやっており、GANを使って云々するというようなモデルを実装したりしている。

f:id:komi1230:20191201130042p:plain

f:id:komi1230:20191201130106p:plain

画質の粗い画像を高画質化するというのが今AI系としてやっていること。

基本的にはGoogle Colab上でコードを書いているのだけれど、そのうちGitHub等で公開する予定。

雑感

自分はAI系の人なのかというと若干謎なのだけれど、機械学習アルゴリズムの実装もカバーしているエンジニアという雑な括りだとちょうど自分が当てはまるのではないかなぁと思う。

自分でも色々作っていて、例えば前に作ったGunosyの記事をスクレイピングしてきてそのカテゴリーを判別するモデルを前に作ったり。

github.com

内部ではガウシアンナイーブベイズを採用していて、これについては解説記事も前に書いた。

qiita.com

他にもカルマンフィルターの記事とかも。

qiita.com

正直、あまり自分は何かの分野に固執しているわけではなくて、単純に自分が面白いと思ったことをやり続けているだけで、もしかしたら数年後は全然違うことをやっているかもしれない。

ただ、今は機械学習のシステムをプロダクトとして仕上げるのが面白いので、まだまだAI屋さんとしてがんばっていこうと思う。

来年からはソフトバンクで面白い仕事をどんどんやっていきたい。

Common Lispの外部ライブラリと名前空間

今回はCommon Lispのプロジェクトでの外部ライブラリの利用について、ちょっとしたメモ書き。

実はちょっと前にCommon Lispでの依存関係を処理してくれるASDFについて記事を書いたのだけれど...

komi.hatenadiary.com

この記事ではローカルでライブラリを作成した場合の依存関係を書いただけだったが、外部ライブラリの利用やdefpackageによる名前空間の衝突についてはカバーしていなかったので、今回をそこらへんを。

外部ライブラリ

例えばAlexandriaなどのものは.asdファイルのdepends-onに書けばよい。

(defsystem "hoge-foo"
  :version "0.1.0"
  :author ""
  :license ""
  :serial t
  :depends-on ("alexandria"
               "woo")
  :components ((:module "src"
                :components
                ((:file "main"))))
  :description ""
  :in-order-to ((test-op (test-op "hoge-foo/tests"))))

ここに書いておいて、REPLで

(asdf:load-system :hoge-foo

とやればロードができる。

パッケージ名の工夫

GitHubとかをのぞいているとdefpackageにて

(in-package :cl-user)
(defpackage #:hoge
  (:use :cl))
(in-package #:hoge)

(defun ....

というようにdefpackageのあとにシャープ#をつけているものがある。

これは、Common Lispではパッケージ名を他のシンボルと同様に見てしまうことがあるらしく、それを避けるためにシャープ#をつけることによって他のシンボルとは別物として扱ってもらえるようにするらしい。

プログラム中のライブラリの使用

外部ライブラリの利用について、ASDFの中で依存ライブラリの定義をしてあれば名前空間つきの関数の利用は可能らしい。

例えば.asdの中のdepend-onにalexandriaを書いてあれば

(in-package :cl-user)
(defpackage #:hoge
  (:use :cl))
(in-package #:hoge)


(defun hoge ()
  (alexandria:map-iota #'print 10))

というようにdefpackageで特にimportの指定をせずとも関数を使うことが可能である。

ただ、名前空間が衝突してしまうことを避けるためだったり依存ライブラリの利用している関数を明示的に示すため、defpackage内に:import-fromで使うものだけimportするのが一般常識というやつらしい。

上記の例だと

(in-package :cl-user)
(defpackage #:hoge
  (:use :cl)
  (:import-from :alexandria
                :map-iota)
(in-package #:hoge)


(defun hoge ()
  (map-iota #'print 10))

とする感じ。

このようにdefpackage内で:import-fromで依存パッケージを書いておけば[名前空間]:[関数名]のように名前空間を書かずに良くなってコードも微妙にスッキリする。

ローカル同士の依存関係

外部ライブラリの使い方は分かったし名前空間の処理を上手いことやる方法は分かったので、ではローカル同士の依存関係はASDFでどのように扱うのだろう。

例えばhoge.lispfoo.lispを利用している場合、

hoge.lisp

(in-package :cl-user)
(defpackage #:hoge-package
  (:use :cl)
  (:import-from :foo-package
                :hoge)
(in-package #:hoge-package)


(defun my-hoge ()
  (format t "Hello !")
  (hoge))

となっていて、foo.lisp

(in-package :cl-user)
(defpackage #:foo-package
  (:use :cl)
  (:import-from :alexandria
                :map-iota)
(in-package #:foo-package)


(defun my-foo ()
  (map-iota #'print 10))

となっているような状況を考える。

この場合、ASDFには

(defsystem "hoge-foo"
  :version "0.1.0"
  :author ""
  :license ""
  :serial t
  :depends-on ("alexandria"
               "woo")
  :components ((:module "src"
                :components
                ((:file "hoge" :depends-on ("foo")))
                ((:file "foo"))
  :description ""
  :in-order-to ((test-op (test-op "hoge-foo/tests"))))

というように:componentsのところの内部の各ファイルについて依存関係を書いてやればオッケー。

まとめ

今回はライブラリの利用やローカルファイル同士の依存関係について簡単にまとめた。

多分恐らくこれでASDF経由での依存関係のまとめは網羅したと思われる(多分)

割とASDFの設定は慣れれば意外と簡単かもと感じてしまうけど、昔は色々良く分かってなくて困ってたので、実は学習コスト高いのでは?という顔をしてる最近。

ちなみにCommon LispにはQuicklispなるものが存在するが、これについてはPythonでいうpip的な感じだと思う。

ASDFはどちらかといえばpipenvくらいの感じ。

Quicklispは手元で簡単に遊ぶための手軽なライブラリマネージャ的なやつで、ASDFがプロジェクトのまとめをやってくれるようなイメージ。

まあなんであれ、とりあえず今回はASDFをある程度使いこなせるようになったので、これで晴れてCommon Lispのプロジェクトをサクサク作ることができるようになったと思う。

ちなみに自分はまだRoswellについてはしっかり触ったことがないので、Roswellについても使えるようになりたいなぁなんて思ったり。

とりあえず今日はここまで。

CLOS再訪

全人類wanimaを聴くべき。

wanima聴いてるとよくわからんけどめちゃくちゃ元気が出てくる。

自分は作業するときいつも音楽を聴いているのだけれど、あまり甘ったるいJ-POPなどは聴かずJazzやクラシック、インストあたりを聴いてる。

ただたまにロックなども聴きたくなるわけで、それで先ほど色々Apple Musicを漁ってたらwanimaがグッときた次第なのである。

ということで全人類はwanimaを聴くべき。毎日元気になれる。

本題へ

さて、今日はCLOSについて。

数値計算するとき計算結果のためのplotterが欲しくなるわけだけど、Common Lispではあまりそういうのがないらしい。

一応Gnuplotのラッパーはあるが、PythonでいうMatplotlibのように完全にまとまったものはどうやら存在しないらしい(もしかしたら自分のリサーチ不足かもしれないが)

そんなわけで自分で何か作れるかなと思ってちょっとQiitaの記事とかを色々漁ってたら、どうやらCL-OpenGLとかいうOpenGLのラッパーが存在するので、それを使えばいけそう。

実はCommon Lisp用のPlotterを作る計画は割と結構前からあったのだけれど、なんやかんやで忙しかったりまとまった時間を取れなくて開発が後回しになっていた。

で、最近ちょっと取り組み始めた次第。

基本的にこのページを参考にしている。

qiita.com

この手のグラフィック系のものは基本的にオブジェクト指向型で書いていくのがデフォであるわけだけど、Common LispにはCLOSというCommon Lisp Object Systemというのがあり、今回はそれで書いていく。

そんなわけでCLOSを使うわけだけど、つまづいたポイントをざっとメモしていく。

Common Lispのクラスシステム

SBCLの実装を眺めているとCommon Lispにおけるデータ構造はビルトインクラスとして色々用意されていることがわかる。

github.com

さて、REPLで動作クラスが何かを調べる際は

(class-of 10)
;; => #<BUILT-IN-CLASS SB-KERNEL:FIXNUM>

(class-of "hoge")
;; => #<BUILT-IN-CLASS SB-KERNEL:SIMPLE-CHARACTER-STRING>

(class-of #(1 2 3))
;; => #<BUILT-IN-CLASS COMMON-LISP:SIMPLE-VECTOR>

などすればわかる。

Common Lispでクラスを定義する際は

(defclass クラス名 (スーパークラス)
  (スロット[クラスに所属するもの])
  (オプション))

というような形式となっており、具体的に

(defclass a-class ()
  ((a-slot :initarg :a)))

といった具合。

このクラスのインスタンスを生成して動作クラスを確認すると

(class-of (make-instance 'a-class :a 10))
;; => #<STANDARD-CLASS COMMON-LISP-USER::A-CLASS>

とわかる。

また、クラス内のスロットの取り出しは

(slot-value (make-instance 'a-class :a 10) 'a-slot)
;; => 10

で可能。

メソッド

PythonでクラスというとAttributeとメソッドが一緒にいるイメージで、例えば

class hoge:
    def __init__(self, a):
        self.a = a

    def foo(self):
        return a

というような具合。

一方でCLOSではクラスはあくまでクラスはスロットの塊を定義するだけで、総称関数をまた別で用意する。

(defclass foo () ())                   
(defstruct foo-struct)                  

(defgeneric foo (obj))          

(defmethod foo ((x foo))
  "foo class!")

(defmethod foo ((x foo-struct))
  "foo struct!")

;; built-in-class float
(defmethod foo ((x float))
  "float!")

;; built-in-class function
(defmethod foo ((x function))
  "function!")

;; foo specifier
(defmethod foo ((x (eql 1)))
  "one!")


(let ((a (make-instance 'foo))
      (b (make-foo-struct))
      (c pi)
      (d #'car)
      (e 1))
  (mapcar #'foo (list a b c d e)))
;=> ("foo class!" "foo struct!" "float!" "function!" "one!")

クラスの使いどころ

そもそも関数型プログラミングのお作法が基本となるLispでクラスを使うのはどんなタイミングか。

これはプログラミング一般の話で、プログラム内に現れるデータを一括で管理したいときに有用なものとなる。

で、Common Lispではクラスを作る方法としては主にdefclassとdefstructureの2つの方法がある。

これらの違いとしてはほぼ同じだが、特徴として

  • defclassは多重継承をするとき
  • defstructureは速い(単一継承はできる)

というようなノリで、機能的には多重継承を使うかどうかくらいが違いとなる。

なのでCommon Lispではたくさんオブジェクトを生成しては捨てるというようなプログラムを書く際は構造体を使うのがベストプラクティスとなる。

では、クラスで何ができるだろう。

これは色々できるが、一つにオリジナルの型を定義できることがある。

Common Lispでの型とは動作クラスのことで、実際に総称関数を定義する際に動作クラスに基づいて挙動を変えることができた。

ちなみに型の定義にはdeftypeを使うこともできる。

(deftype uint () '(integer 0))

(typep 23 'uint) ;; => T
(typep -23 'uint) ;; => NIL

また型ごとに挙動を変えるtypecaseも存在する。

このdeftypetypepで扱う型はもともとCommon Lispのビルトインクラスのものでしかないので、自前で用意したクラスのものを扱うにはインスタンスを作成する必要がある。

ぱっと見だとdeftypeでできることはdefmethodなどでもできそうな感じだが、ここらへんはCommon Lispあるあるで、様々なマクロが用意されていて一つのことを複数のやり方で実現できる。

ここらへんは機能的には細かい違いがあったり速度の違いがあったりするのだろうが、若干よくわからないことがあっても割となんとかなったりする。

とにかくCommon Lispではクラスはこのような扱いとなる。

まとめ

Common Lispは割とクラス周りのマクロが充実しているが、その代わりにそれぞれの違いを把握するのが若干難しく、完全理解にはそれなりの時間を要しそうである。

CLOSのシステムは色々だが、マスターすれば確実にプログラムの表現力がだいぶ向上するはずなので友好活用していきたい。

個人的に、CLOSはJavaのようなガチガチさがなく割とゆるっとしている印象で、割と好印象だったりする(そもそもで自分はあまりJavaが好きではないのだけれど)

とりあえずフワッとCLOSを理解したので開発をがんばっていきたい。

Common Lispの処理系SBCLにおける型と最適化

ここ数日はしんどいスケジュールだった。

11/19(火)に8時間ほど働いた後に夜行バスで東京に移動、11/20, 21でLINE DEVELOPER DAYに2日間参加して夜行バスで京都に戻り、その後8時間労働。

少々この1週間はおおよそ人権がないのではという感じだったが、なんとか乗り切ってゆっくりとした休日を迎えている。

そんな休日ということで今日はのんびり散歩したりしていたのだが、ふとLispの型と最適化方針について気になったので今日は少しそこらへんについて調べていた。

今回のエントリーはそこらへんを簡単にまとめたもの。

結論から

ちなみに結論から言うと、Lispはめちゃくちゃ速い言語である。

ちゃんと型を指定した上で最適化してコンパイルすればC言語と遜色ないレベルの速度が出る。

Lispインタプリタコンパイラの両方で実行ができる言語で、インタプリタで実行した際あまり速度は期待できないのは事実ではあるが。

色々な記事でLispはめちゃくちゃ遅いというように断定するようなものがあったりするが、これはLispについて何も知らないド素人が自分はプログラミング言語について詳しいですよという顔をするために書いたクソ記事である可能性が高い。

実際にこんな名言もある。

Cで書くコードの方がCommon Lispで書くより速いって人がいたら、それは彼のCの技量が高すぎるってことだね。

“If you can't outperform C in CL, you're too good at C.” — Eric Naggum

また、このSchemeのサイトにて川合史朗さんも

普通に(教科書的に)書かれたLispコードは、コンパイルしても確かにCより 数倍遅いでしょう (ソースコード量は数分の一でしょうが)。

C並に速いLispコードを書くためには、いくつも気を付けなければならないことがあります。

  • inner loopでメモリをアロケートしない: Cと違ってLispの場合は暗黙のうちに メモリアロケーションが入ることが多いので、どういう操作だとアロケーションが 入らないかを熟知している必要があります。
  • 型宣言をつけまくる: あいにく、商用のLispコンパイラでもあまり賢い 型推論は行ってくれないみたいで、とにかく鬼のように型宣言をつけまくる 必要があります。
  • safetyを0にすると処理系は救ってくれない:宣言された型とは違う型の オブジェクトを渡すとSEGVしてくれます ;-(

なので、C並に速いLispコードは見た目も安全性もC並になる、というのが 私 (Shiro) の経験です。

このように高速化に際して危険性を孕みつつもC並みに高速化することへの可能性について語っている。

本エントリーではどのように型を指定してプログラムを高速化するかということを簡単にまとめる。

Lispというプログラミング言語

プログラミング言語は型についてシステムとして(静的|動的)な(強い|弱い)型付けという説明がなされたりする。

静的型付けは、簡単に説明すると変数や関数に型を予め定義しておき、その型以外のデータを変数では使えないような型システムのこと。

一方で動的型付けとは、動的に型を付けるという読んでそのままの通り、プログラムを書くときに変数や関数に何が入ってくるかというのが特に決まっていない形を指す。

主に静的型付けとして代表されるのがC言語とかで、動的型付けとして代表的な言語がPythonとか。

また、強い型付けと弱い型付けについて、プログラムでは基本的に演算は型の指定をしてくるが、もし指定ではない型を用いてしまった場合にどのような挙動を示すかについての分類を強い型付けとか弱い型付けとか言ったりする。

具体的に、C言語

int a = 10;
char b = "20";

のようにしたときa + bは整数と文字列の和をとろうとしてエラーを吐く。

しかしJavaScriptだと10 + "20"は結果として"1020"という文字列を返してくる。

型の強い弱いに関して、整数型を浮動小数点型に自動的にキャストするような言語は弱い型付けと言っていいと思われる。

個人的にはここらへんの型システムとプログラミングスタイル(オブジェクト指向型と関数型)については色々言いたいことがあるのだけれど、それについては来月のアドベントカレンダーまで温めておこうと思う。

さて、このような型システムにおいて、Lispは動的な強い型付けの言語であると言うことができる。

動的な型と速度の問題

動的な型を用いた言語は、プログラムの実行に際してその都度に変数や関数の型をチェックしたり推論したりするので、静的な言語に比べて一般的に実行速度で劣る。

動的な型システムを持つ言語ではプログラムの実行に際してユーザーが厳密に指定する必要はないので高速な開発に向いていると言えるが、その分プログラムの実行速度は遅くなるのでこれらの型システムとユーザーの自由度(開発スピード)はトレードオフの関係にあると言える。

余談だが、自分が一番最初に触ったプログラミング言語C言語で、最初はプログラミングとは変数を型とともに宣言してそれをコンピュータに読ませる必要があるものだと認識していたのだけど、そのあとにPythonを触ってこんなにも使いやすい言語が存在するのかと非常に驚いた記憶がある。

Pythonを触り始めて以降、自分はアルゴリズムの実装において基本的に静的な型システムを持つ言語は使っておらず、だいたいLispPythonばかり使っている。

動的な型システムの言語で型を指定して速くする

さて、先述の通り、動的な型システムを持つ言語でプログラムを普通に記述するとC言語のような速度は出ない。

しかし、型の指定を行えばかなりの高速化が狙える。

例えば具体的に以下の階乗を計算するコードを見てみよう。

(defun factorial (n)
  (if (= n 1)
      1
      (* n
         (factorial (1- n)))))

これで10000の階乗を計算すると、実行時間は以下の通り。

CL-USER> (time (factorial 10000))

Evaluation took:
  0.050 seconds of real time
  0.050077 seconds of total run time (0.044013 user, 0.006064 system)
  [ Run times consist of 0.026 seconds GC time, and 0.025 seconds non-GC time. ]
  100.00% CPU
  110,624,296 processor cycles
  4 page faults
  69,740,608 bytes consed

ちなみにコンパイルすると実行速度は以下の通り。

CL-USER> (time (factorial 10000))
Evaluation took:
  0.038 seconds of real time
  0.037463 seconds of total run time (0.024041 user, 0.013422 system)
  [ Run times consist of 0.005 seconds GC time, and 0.033 seconds non-GC time. ]
  97.37% CPU
  82,108,806 processor cycles
  69,739,648 bytes consed

さて、こいつに型宣言をつけてやる。

まず関数の型について、

(declaim (ftype [typespec] [関数名]))
(defun 関数名 (引数)
  (...))

というような感じで引数と返り値の型を定義する。

今回のケースにおいて10000の階乗を計算するが、今回のSBCLにおいてmost-positive-fixnum4611686018427387903であってそれを超えるとintegerになるので、引数の型をfixnumとして返り値をinteger`とする。

(declaim (ftype (function (fixnum) (values integer &optional)) factorial))
(defun factorial (n)
  (if (= n 1)
      1
      (* n
         (factorial (1- n)))))

これをコンパイルして実行した結果として

CL-USER> (time (factorial 10000))
Evaluation took:
  0.021 seconds of real time
  0.021132 seconds of total run time (0.020203 user, 0.000929 system)
  [ Run times consist of 0.005 seconds GC time, and 0.017 seconds non-GC time. ]
  100.00% CPU
  46,617,370 processor cycles
  69,735,360 bytes consed

うん、スクリプトとして実行した際の実行時間は0.050077コンパイル時の実行時間が0.37463だったので、これらと比較したらだいぶ速くなった。

次に変数の型宣言をしてやる。

これは

(declare (type fixnum 変数名))

としてやれば良い。

また、同時に各出力フォームについても型を宣言してやる。

(declaim (ftype (function (fixnum) (values integer &optional)) factorial))
(defun factorial (n)
  (declare (type fixnum n))
  (the integer
       (if (= n 1)
           1
           (* n
              (factorial (1- n))))))

速度は以下の通り。

CL-USER> (time (factorial 10000))
Evaluation took:
  0.022 seconds of real time
  0.022008 seconds of total run time (0.021223 user, 0.000785 system)
  [ Run times consist of 0.005 seconds GC time, and 0.018 seconds non-GC time. ]
  100.00% CPU
  47,991,386 processor cycles
  69,754,544 bytes consed

パッと見だと前より遅くなっているが、システム側の実行時間はさらに短縮されていることがわかる。

さて、ダメ押しで関数のインライン化をして関数の呼び出しを節約し、呼び出しのオーバヘッドを殺そうと思う。

(declaim (ftype (function (fixnum) (values integer &optional)) factorial)
         (inline factorial))
(defun factorial (n)
  (declare (type fixnum n))
  (the integer
       (if (= n 1)
           1
           (* n
              (factorial (1- n))))))

これはコンパイラ側に関数を埋め込むことになり、インライン展開されたコードが実行されればされるほど効果的となる。

Evaluation took:
  0.026 seconds of real time
  0.026104 seconds of total run time (0.025496 user, 0.000608 system)
  [ Run times consist of 0.009 seconds GC time, and 0.018 seconds non-GC time. ]
  100.00% CPU
  57,044,664 processor cycles
  66,851,024 bytes consed

今回は単純な再帰アルゴリズムなのでインライン化の恩恵を受けてないが(むしろ遅くなってる)、これは大きなプロジェクトなどでじわじわと効いてくる最適化なので重宝したい。

まとめ

今回はSBCLでコードを高速化する手法についてざっとまとめた。

最終的に最初のインタプリタとして実行した際の倍以上の速度を獲得することができた。

Lispはかなり速い部類の言語だと思うし、そもそも言語としてアルゴリズムの表現性が高いのでコードデザインの部分として最適化の余地はいくらでもあるように思える。

自分もまだまだCommon LIspについては未熟であるが、もっと極めていきたい。

ClojureのソースコードをClojarsにpushする

今日もClojureの進捗生産。

普段から

  • コードを書く
  • わからんことが発生する
  • 色々ググって調べる
  • 問題解決
  • 問題の発生から解決に至るまでのプロセスをブログで記録に残す

というサイクルを徹底していて、なのでブログの更新は自分が何かコードを書いていてつまづいた形跡となっている。

で、このサイクルを実行するには多少まとまった時間が必要なわけなのだけれど、なぜか自分でもよくわからないのだが色々書類作業が発生していて、あまりここ最近はコードを書くことができていない。

ここでいう書類作業とは、内定先の入社までにやるべきhogehogeだったり各所へのメール出しだったり色々。

ホントにこの手の書類作業はツラいものなのでできるだけのんびり一人でコードを書いている時間の方がハッピーなのだけれど、まあ世の中そう簡単にはうまくいかないわけで。

...とまああまり無駄な前置きをせずにさっさと今日やったことをまとめておこうと思う。

Clojarsへのpush

前回のエントリーでLeiningenの:dependenciesに記述されるものはローカルのディレクトリではなくClojarsであることを述べた。

komi.hatenadiary.com

今回はその続きとして、作ったリポジトリをClojarsへpushする。

前準備

まず前提として、今回の実行環境はmacOS Catalinaの最新バージョンである。

なぜここにきていきなり環境の話をするかというと、今回のClojarsへのpushに際してgnupgというものを用いて暗号化する必要があり、これが環境ごとによって微妙に挙動が異なるため。

さて、まず最初に行うこととして、Clojarsのアカウントを作成する。

clojars.org

これを行ったあと、~/.lein/credentials.cljに以下を記述する。

{#"https://clojars.org/repo"
  {:username  "ユーザー名" :password  "パスワード"}}

これを書いて保存すればオッケー。

GPGを準備

次に、先述のgnupgをインストールする。

今回はmacOSなのでHomebrewで行う。

$ brew install gnupg

インストールできたら、

$ gpg --version

などで確認。

このgnupgはいわゆる公開鍵と秘密鍵でhogehogeするものなので、まず鍵を生成しなければいけない。

なので以下のコマンドで鍵を生成。

$ gpg --gen-key

名前とメールアドレスを要求されるのでテキトーに入力しておく。

ここでメールアドレスは別に入れなくてもなんとかなるらしい(多分)(自分は問題なかった)(ホントにこれでいいかはわからない)

それらの入力が終わったらoの大文字のOで終了。

さて、そうしたらターミナルの画面がパッと切り替わって、なんだか文字化けしながら何か選択する感じの画面が出てくる。

f:id:komi1230:20191117003722p:plain
こちらのQiitaより拝借。GPGの画面。

文字化けしていてパッと見だと何を言っているのか理解不能だが、実はここはパスワードを入力させて最後にOKかキャンセルするかを選択する画面らしい。

GUIならまだしもCUIで無理をした結果、環境の違いでこうなってしまったらしい。悲しい。

一応なんかしらの言語設定を施すことでこの文字化けを回避することができるらしいが、わざわざそれをやるのはめんどくさいのでこのまま入力する。

この画面のままパスワードを入力していくと、だんだん * * * *と溜まっていくのがわかると思う。

入力が終わったら矢印キーで下のOKを選択し、Enterで抜ける。

そうすると鍵の生成が終わり。

Clojarsにpushするぞ

鍵の準備が終わったので、早速ClojarsへのCredentialsを暗号化する。

$ gpg --default-recipient-self -e ~/.lein/credentials.clj > ~/.lein/credentials.clj.gpg

これで暗号化できるはず。

さて、ようやく待ちに待ったClojarsへのpushを行う。

自分のClojureのプロジェクトへのリポジトリに移動し、以下のコマンド

$ lein deploy clojars

これでできる。

バージョン情報やプロジェクト名などはproject.cljから勝手に読み取ってくれる。便利。

pushする際に名前とパスワードが求められるので、入力。

これにて終了。

雑記

このデプロイコマンドを打つと勝手にtarget/が生成されるが、これが自分のGitHubリポジトリへのpushに邪魔だという人は.gitignoreにて

**/target/

と書いておけば全階層のtarget/ディレクトリが無視される。

まとめ

今回はgpgを用意してClojarsへのpushまでを行った。

これにて自分のプロジェクトがClojarsに乗っかった状況になるので、Leiningenで:dependenciesに書けば読んでくれるということになる。

実は今回のエントリーのモチベは、前のエントリーにて発表したClojure機械学習ライブラリのMoeにある。

Moeの進捗について、まだプロジェクト開始から数日しか経っておらず、これに加えてなかなか開発する時間が取れていないのでまだ全然なのだが、今日先ほど入力データのバリデーションを行ってくれるような簡単なユーティリティ関数を用意したので、それをまずClojarsにあげてみて確認したというのが今回の背景にある。

進捗はダメダメだが、その過程でClojureのお作法について色々学んでいっているので数ヶ月後にはかなり成長しているだろうことを期待。

ではでは、本日もお疲れ様でした。

Leiningenでプロジェクトの配下にサブプロジェクトがあるような構成を作る

実は今日でClojureに入門してそろそろ1ヶ月が経とうとしている。

Clojureを始めるきっかけとなったのは『7つの言語 7つの世界』だった。

ちょうど1ヶ月前、フォロワーの方と大阪でお茶することになったのだが、少し早めに着いてしまったので書店で本を物色して時間を潰していた。

その際この本を見かけ、なんとなくClojureの章の数ページだけ見てみるとClojureはおもしろいぞ〜的な感じの雰囲気で、それでちょっと興味を持ったのがきっかけ。

もともと趣味Lisperだった(Common Lisp書いてた)のだが、Clojureについては存在を認知していたもののなぜか手を出してこなかった(恐らくCommon Lispでそれなりに楽しかったから?)

まあそんなこんなでClojureを始めたというわけである。

で、今のところの感想として、Common LispよりClojureの方が馴染むかなぁといったところ。

ただ、Common Lispとの違いに戸惑って変なところでつまづいたりしてるので大変といえば大変。

ということでちまちまブログを更新してはClojureの勉強メモをつけているのがここ最近の話である。

とまあ回顧録的な話はこの辺にしておいて、そろそろ本題へ入ろうと思う。

自作ライブラリ書いてみたい

自分はもともとLispの人というより機械学習の人というような感じでツイッターでは振る舞っていたのだが、最近はLisp好きの人というような感じになってる。

ただ、一応話題のAI人材ということで、卒業研究も画像処理関係のタスクなので機械学習をやらなきゃいけないわけで。

そんなこんなで最近はLisp機械学習をやろうとウダウダしているわけである。

で、先日Clojure機械学習関係のリポジトリあるかなと探してみたところ、どれも最後のコミットが数年前という感じだった。

これはClojure機械学習系のライブラリ書いてみるのにいい機会かなと思い、Clojure機械学習ライブラリを書こうという思考に至るわけである。

まあ実際のところJavaでは機械学習ライブラリは存在していて、ClojureJavaの資産を利用できるので今からClojureで書くのは無駄なのだけれども...(おまけにJavaの方が速い)

とにかく、完全に自己満なのだがClojure機械学習自作ライブラリを作りたいというわけである。

Clojureディレクトリ構成がわからない

で、リポジトリを作ってプロジェクトを立ち上げようと思ったわけだけれど、今後の拡張可能性を考えてscikit-learnのようなプロジェクト構成にしようと考えた。

具体的に、

project_root
├── project.clj
├── README.md
├── LICENSE
├── src
    ├── linear_model
    │   ├── hoge.clj
    │   ├── foo.clj
    └── svm
        ├── bar.clj
        ├── piyo.clj

というような感じを想定していた。

なぜこうしたかというと、機械学習アルゴリズムは膨大で、線形回帰をはじめとしてニューラルネットや決定木があり、それらをいちいちリポジトリで切っていくより1つのリポジトリでまとまっていた方が扱いやすいと考えたため。

要するに、自分が所望のものはプロジェクトの配下にサブプロジェクトを配置するような感じにしたかったのである。

さて、そんなことを考えていた一方でLeiningenでlein new default hogeで作れるプロジェクトの雛形は

hoge
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│   └── intro.md
├── project.clj
├── resources
├── src
│   └── hoge
│       └── core.clj
└── test
    └── hoge
        └── core_test.clj

というような具合で、project.cljの直下にsrc/というディレクトリがあって、そこからソースコードが存在する。

そうしてプロジェクトのディレクトリ構成とLeiningenの依存関係について少し悩み始める。

Leiningenの:dependenciesが見ているもの

結論から言ってしまうとLeiningenでの:dependenciesはClojarsを参照しているのでローカルのディレクトリ構成は一切無関係で、プロジェクト配下のサブプロジェクトそれぞれをClojarsに登録さえすればプロジェクトの統一的なパッケージ化が実現できるらしい。

これは手元でlein new default hogeとして、本来やりたい構成を再現しながらlein runをしてみたところエラーを観測したのでこのような結論を出した次第である(恐らくどこかのドキュメントを参照すれば書いてありそうだが)

実際、例えばClojureのWebアプリフレームワークRingはまさにこの形をしていて、ring-corering-jetty-adapterなどのそれぞれをClojarsに登録してあり、本体のringproject.clj:dependenciesでそれらを包括するだけの形となっている。

github.com

ということで複数のサブプロジェクトから構成されるプロジェクトについては自由な配置にして良いので、大まかなディレクトリ構成については決まったわけである。

namespaceとディレクトリ名

先日ツイッターdefproject名前空間の違いがわからんという旨のツイートをしたところ、以下のようなことを教えてもらった。

なるほど、名前空間の定義もある程度の規則があるらしい。

そこでもう一度色々なClojureライブラリのディレクトリ構成を見てみると、名前空間hoge.foo.barというような場合は

src
├── hoge
    ├── foo
        ├── bar
            ├── main.clj
            ├── sub.clj

のようにディレクトリ名の重なり方と名前空間の名称はリンクしているのが良いらしい。

最終的に

以上の色々な試行錯誤を経て、最終的に今回自分が作りたいプロジェクトの構成は以下のようになった。

moe
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│   └── intro.md
├── project.clj
└── src
    ├── linear-model
    │   ├── CHANGELOG.md
    │   ├── README.md
    │   ├── doc
    │   │   └── intro.md
    │   ├── project.clj
    │   ├── src
    │   │   └── moe
    │   │       └── linear-model
    │   │           ├── base.clj
    │   │           └── core.clj
    │   └── test
    │       └── moe
    │           └── linear-model
    │               └── core_test.clj
    └── svm
        ├── CHANGELOG.md
        ├── README.md
        ├── doc
        │   └── intro.md
        ├── project.clj
        ├── src
        │   └── moe
        │       └── svm
        │           └── core.clj
        └── test
            └── moe
                └── svm
                    └── core_test.clj

今回のプロジェクトの名前はMoeで、ハワイ語で夢という意味があり、それにちなんで命名した。

github.com

ちなみにハワイ語命名に良く、

  • Lea : 幸福
  • Mahina : 月
  • Maoli : 本物の
  • Leo : 声
  • Koa : 勇敢な

など良さげな単語がたくさんあるので、ぜひ命名に困ったらハワイ語を探してみるといいと思う。

latte.la

まとめ

今回はLeiningenの:dependenciesが参照しているものについて簡単に調べ、その上で今回やりたいことを実現するためのディレクトリ構成を作った。

とりあえず当初の目的だった機械学習ライブラリの自作の準備ができたので、これからようやくアルゴリズムの実装に落ち着いて取り組むことができる。

別に今回のプロジェクトについては納期も無ければ要件をガチャガチャ変更する上司もいないし社内政治でプロジェクトがポシャる可能性も存在しないので、のんびりやっていこうと思う。

そういえば来月のアドベントカレンダーは2つ登録していて両方ともLisp系で書く予定なので、ある程度このプロジェクトが形になればEat your own dog foodを実践して何か記事にできるかなとか考えている。

とにかくこれからがんばっていくぞい〜