こんにちは、お読みいただきありがとうございます。
少しだけ最初はフレームワークを作るまでの経緯を書いています。
「お前の話なんてどうでもいいからソースコードを見せろや」という人は「ついに完成」のパートまで飛ばして進んでくださいな。
ディープラーニングの勉強
エンジニアを始めて早3年、そろそろ自分の専門分野・得意分野を作らねば、と思い立ってDeep Learningを今年6月から勉強し始めました。
ただ、僕が勉強したかったのはDeep Learning(Neural Network)の仕組みであったのに対して、世の中にあるほとんどの学習マテリアルは既存のフレームワークを扱うためのものだったのです。
ネットで機械学習 スクラッチ
やNeural Network 1から作る
などと、小さな子供のような検索を毎日のようにしていました。
もちろん、このニーズを満たすページはあるにはありましたが、どれも前提知識が高く要求されていたりして当時の僕にとってはもはやちんぷんかんぷんで「何言ってんだコイツ」状態になっていたのでした。
良書との出会い
そんな中で僕にとっての良書、いや、神書とも言える斎藤康毅さんの「ゼロから作るDeep Learning」に出会いました。
この本の内容はまさに僕が欲していた内容そのものでしたし、何より僕のように前提の数学知識が乏しい人間にも理解できる粒度から話し始めてくれていたのです。
難しいなぁ、と思うところもまるで予知していたかのように解説が差し込まれており、好奇心のストライクゾーンのど真ん中にぶっ刺さったこのシリーズを学習することを決めました。
3ヶ月の楽しいチュートリアル
「ゼロから作るDeep Learning」は現在4冊存在します。
- その1:
FNN
とCNN
の仕組みについて - その2:
NLP(自然言語処理)
とRNN
の仕組みについて - その3:
ディープラーニングフレームワーク
の作り方 - その4:
強化深層学習
を扱っていますが、一般的なDeep Learning
という4冊なのですが、4は少々通常のDeep Learningから外れるので1~3を学習することに決めました。
それぞれが400ページ前後という大長編です。
僕は寝る間も惜しんで3ヶ月の大冒険をしました。
この旅が終わった時には「Deep Learning、完璧に理解した」と「俺、最強」というどこぞの目隠し白髪と同じムーブをかます最強感を感じていました。
ただ、二週間ほど経った頃に気づいたのです。
「あれ?俺、自分では何も残せていなくね?」
そうです。この3ヶ月の大冒険はもはや恩師とも言える著者様のガイドの下で行われたただのチュートリアルでしかなかったことに気づいたのです。
どこぞの鬼狩りの漫画で言えば、まだ選抜試験に通っていない、なんなら岩を切れてすらいない段階だったと。
何を作るか
そんなチュートリアルを終えたばかりのペーペーの僕はどんな旅を始めればいいのかをまず考えるに至りました。
その時、頭の中で響いたのは昔どこかで聞いた
「何かサービスを作るなら、自分が不便に思ったことを解決することを考えるんだ。」
ということです。
僕がこれまでの旅で一番不便だったのは世の中に初心者でも仕組みを理解できる文章が少なかったことです。
そこでこれまでの冒険譚を一つにまとめて次世代のDeep Learningを学ぼうとする同志にもっと機会を提供しようと思ったのでした。そう、ディープラーニングの仕組みを知れる文章を作ろうと思ったのです!
奇しくも、現在の世の中は大AI戦乱時代です。一定の需要はあると踏んでいました。
しかし、ただ文章を書くだけだとつまらない。
せっかくフレームワークの作り方まで教わったのだから、いっそその冒険譚を読む人が勉強用に使えるフレームワークも作ろうと考えました。
フレームワークの構想
いざ、フレームワークを作ると言ってもどんなフレームワークを作りたいのかをまずは考えました。
最終的には僕は以下の点を満たせば良いと考えるに至ります。
- ある程度、さまざまタスクをこなせること
- ソースコード自体でも勉強ができるほどシンプルな実装であること
- APIリファレンスが充実していること
上の二つはある程度、参考にするゼロつくの作者様が本のために作成されたDeZero
で満たされています。(もちろん、メモリ効率などの仕組みは別途実装が必要だとしても)
しかし、問題は三つ目です。
自分で作るだけ、自分用や作者様のように一緒に作っていこう、というものであればいざ知らず、他人に使わせるならばAPIリファレンスは必須です。
どうするべきか、考えた結果、一度これについては放り投げてとりあえず作り始めることにしました。
。。。これが地獄の入り口だったなんて、この時予想しろという方が無理な話です。
順調な開発
独自のフレームワークといっても、一番最初のベースはDeZero
であるため、最低限の機能は揃っています。
もちろん、僕自身も本を読みながら作成していたのでこれをベースに改良を加えていくことにしました。
そもそも、DeZero
も元を辿ればChainerから構想を得ています。
つまり改良を行う中でChainer
が大きな参考書になることは明白です。さらにこのChainer
の意思を受け継いでいるPyTorchのソースも確認していきます。
メモリ効率のためにまずはゼロつくでも紹介されていたAgressive Buffer Release
を実装します。
さらにいくつかの機能がDeZeroではシンプル化のため省かれているのでChainer
を参考に必要なものを揃えます。
そして初心者用として機械学習では欠かせないTitanic Datasetに対応させたかったため、データの前処理を自動で行う処理も入れていきます。
そうして独自フレームワークの原型が完成しました。
ここまで1ヶ月程度でした。
その後、DeZero
譲りの一枚ファイルの形式から一般的なディレクトリ構造にリファクタリングしたりはありましたが、ソース自体は2ヶ月経たずに完成したのでした。
順風満帆に見えた旅に翳りが見え始めたのはこの後からです。
ドキュメントの整備
APIリファレンスを作成する上でやはりドキュメントとしてまとめるのが良いと考えました。
APIリファレンスはSphinxというサービスで作られることが多いことを知って、まずはdocstringを書く必要があることを知りました。
地獄のdocstring
さあ、書くぞ、と始めたdocstringですが、
Leaky Rectified Linear Unit function.
This function is improved version of the ReLU(Rectified Linear Unit).
if x >= 0, :math:`f(x) = x` || if x < 0, :math:`f(x) = slope * x`
- The ``slope`` is a small constant value the default value is `0.2`.
Args:
x (:class:`marquetry.Container` or :class:`numpy.ndarray` or :class:`cupy.ndarray`):
Input container that is float array.
slope (float): small constant value. The default is 0.2.
Returns:
marquetry.Container: Output container. A float array.
Examples:
>>> x = np.array([[-1, 0], [2, -3], [-2, 1]], 'f')
>>> x
array([[-1., 0.],
[ 2., -3.],
[-2., 1.]], dtype=float32)
>>> leaky_relu(x, slope=0.2)
container([[-0.2 0. ]
[ 2. -0.6]
[-0.4 1. ]])
このように最初の一つを書いたときに気づきます。
「あれ?この文量をあと数百個のクラス・関数に向けて書くの?」
そう、後回しにした結果、もちろん全ての機能の実装を完璧に覚えているわけではないので、機能を再確認しながら詳細を記載する必要が出てしまったのです。
正直二度手間です。最初からdocstringを書いていれば。。。そんなことを考えたところで後の祭りでした。
この作業は実装と違って単に機能をソースコードから読み取る作業です。
コードを書いたり、ロジックを考える時に比べて楽しくもないですし、何より時間がかかります。
Chainer
からコピペすることも考えましたが、独自実装で変えている部分もあったのでそもそも書き直さなければならない部分があったのと、なんかプライドが許してくれませんでした。
一部、ChatGPTくんに手伝ってもらいながらこの地獄の時間は約1ヶ月かかりましたとさ。
みなさん、docstringは機能を作ったらそのまま一緒に書いちゃいましょうね。
学習マテリアルの作成
常に順風満帆な航海はないということを実感しながら、最初の目的に沿ってドキュメントに学習用のマテリアルを追加するためにSphinxのrstファイルを書き始めました。
初めて学ぶ・触るディープラーニングがこのフレームワークであってもディープラーニング、ひいては勾配降下法
を使用した機械学習の仕組みから理解できるようにディープラーニングの基礎を書き、さらにその中で読者も一緒に体験できるように実際のソースコードも載せていきます。
初めて触るときに引っかかりそうなところは解説を充実させていきます。もはや小さな本が作れそうな文量になりました。
docstringを英語で書いてしまったばかりにこのドキュメントを全て英語で書くはめになったので、正直心が7回くらい音を立てて折れていきました。
(そんな時は酒を飲んで忘れるのです。余談ですが、海外のエンジニアの方ってなんであんなにお酒が強いんですかね。怖いです。)
実はこのマテリアルの作成
- Get Started
- Example
という二章構成なのですが、docstringよりも時間がかかっています。
この航海の最大の大波である「大バグの乱」と「不便すぎる仕様の乱」が発生します。
ドキュメント作成の中でものすごい量のバグにヒットします。
僕自身、フレームワーク作成段階でもバグ取りはしていたのですが、開発者目線のデバッグは使用者目線に立つと全く足りていないのです。
一つコード例を記載しようとすると、一つのバグが見つかる、そんなことを繰り返していきます。
そして、Titanicデータセットやそのために実装した前処理の仕様が非常に不親切であることに気づきました。
開発者として便利のために作った機能の一部はユーザを混乱させるだけの機能だったのです。
この二つでソースコードを大改修することになります。
ドキュメントの作成はどんどん遅れていきます。そんなことをしている中で季節は春から夏を過ぎて秋に入っていました。
この航海の中での最大の波をなんとか乗り越えて全てのドキュメントが書き終わります。
ついに完成
そして、世の中が肌寒くなってきた時期にこのフレームワークをリリースすることになりました。
version 0.1.0として10/5 に公開済みです。
フレームワーク「Marquetry」
僕はこのフレームワークに「Marquetry」という名前をつけました。
名前をつけたのはまだ世の中が暑かった時期なので、順序は前後していますが、この名前はディープラーニングが小さな関数の組み合わせで作られて、その様相は組み合わせや順序によって変わるという点から日本の伝統工芸である「寄木細工」を連想して付けました。
また、学習者の人に向けられたこのフレームワークが使う人と組み合わさってユニークな未来を作って欲しいという願いも込めています。
Marquetryの詳細
せっかくここまでに話した通りの大波を超えて完成したドキュメントがあるので以下のリンクからご覧くださいな。
英語アレルギーの人はDeepLでの翻訳ではほぼほぼ意味がわかることを確認しているので、DeepLで訳して読んでくださると涙を流して喜ぶかもしれません。
Marquetryのソースコード
GitHubをご覧くださいな。
僕はスーパーエンジニアではないですし、どちらかといえば初心者なので一部のコードの汚さなどはご容赦くださいな。
MarquetryのTODO
とはいえ、いくつかまだできていないことがあるのでTODOを記載します。
For v0.2.0 (Released: 2023/10/22)
-
ドキュメントの日本語対応(Update: 日本語対応は優先的に行いましたとさ。) - 外部CSVデータをbuilt-inのDatasetと同様のインターフェースでの使用可能にする実装
For v0.3.0 (2023/12 予定)
- 動的グラフ読み込みの実装(パラメータではなく、モデルの出力と読み込み)
- フレームワークのマークを作成
- モデルの作成例を制作
For the future
- ONNXの対応
- データベースの直接利用対応
学習の成果物を作る利点
最後にこういった学習の結果として何かを作ることの利点を書いておこうと思います。
というのも、なんかここまで波乱のことしか書いてなくてなんかなぁ、と思ったので。
学習の結果として何か作って公開する利点なんて今更いう必要ないっていう人もいると思いますが、一応言っておくと何か成果物が残る形で作成するのはやっぱりオススメです。
もちろん、就職や転職で役立つ、というのはあるかもしれません。今時ポートフォリオは必須ですしね。
でも一番は、きっと楽しいですよ。
僕ももちろん大波小波書いていますが、全体を通して楽しかったです。
以下のコードは実際にドキュメント内で紹介しているものですが、これが実行できるまでに様々なエラーにぶつかりました。
それこそ、本や動画に従っていたらぶつかることがないようなものばかりでした。
でもその意味を理解して、修正したらコードが正常に動いてくれた時は泣きそうになる程嬉しかったのです。
import marquetry as mq
dataset = mq.datasets.Titanic(train_rate=0.8, label_columns=["pclass"], drop_columns=["name"])
test_dataset = dataset.test_data()
batch_size = 32
shuffle = True
dataloader = mq.dataloaders.DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)
test_dataloader = mq.dataloaders.DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
model = mq.models.Sequential(mq.layers.Linear(16), mq.layers.BatchNormalization(), mq.functions.relu, mq.layers.Linear(1))
optim = mq.optimizers.Adam()
optim.prepare(model)
max_epoch = 100
for epoch in range(max_epoch):
sum_acc, sum_loss = 0, 0
iterations = 0
for data, label in dataloader:
iterations += 1
y = model(data)
loss = mq.functions.classification_cross_entropy(y, label)
acc = mq.functions.evaluation.binary_accuracy(y, label)
model.clear_grads()
loss.backward()
optim.update()
sum_loss += float(loss.data)
sum_acc += float(acc.data)
print("{} / {} epoch | loss: {:.4f} | accuracy: {:.4f}"
.format(epoch + 1, max_epoch, sum_loss / iterations, sum_acc / iterations))
test_acc, test_loss = 0, 0
iterations = 0
with mq.test_mode():
for data, label in test_dataloader:
iterations += 1
y = model(data)
test_loss += float(mq.functions.classification_cross_entropy(y, label).data)
test_acc += float(mq.functions.evaluation.binary_accuracy(y, label).data)
print("Test data | loss: {:.4f} | accuracy: {:.4f}".format(test_loss / iterations, test_acc / iterations))
別にフレームワークとかにこだわる必要はありません。あなたが作ってみたいと思うものを作るので良いと思います。
大切なのは、楽しむことですから。
まとめ
勉強の一つの成果発表としてディープラーニングの学習者のためのフレームワークとドキュメント兼学習マテリアルを作ったらなんか色々前途多難でしたが、なんとか完成しました。
もし、これからディープラーニングを勉強したいなって人は一度覗いてくれると嬉しいです!
もし何か、お気づきの点などありましたら教えてくれると嬉しいです!
初めてQiitaに書き込みをしていて拙い点も多かったと思いますが、最後までお付き合いありがとうございました!
またお会いしましょうノシ