ぱたへね

はてなダイアリーはrustの色分けができないのでこっちに来た

2024年読んで良かった本

パソナX-TECH Advent Calendar 2024 https://qiita.com/advent-calendar/2024/pasona_x_tech の8日目の記事です。

今年はロボットとDeep Learningがメインで勉強しつつ、派手な成果は出せなかったけど基礎力が上がった気がする一年でした。今年読んだ本の中で良かった本を紹介します。

Rustで作るプログラミング言語

gihyo.jp

Rust+処理系作成と、好きな人にはたまらない組み合わせです。感想はここ。

natsutan.hatenablog.com

筆者のZennも補講的な楽しさがあります。

zenn.dev

強化学習(第2版)

www.morikita.co.jp

実質Courseraのコースの紹介です。

natsutan.hatenablog.com

ロボットをやるからには強化学習は避けて通れないと思って勉強したが、勉強すればするほど難しいと言うよりは、こんなの使い物になるのかという疑問が出てくる。来年は実践でやってみたい。

ゼロから作るDeep Learning⑤

www.oreilly.co.jp

強化学習の分野でも拡散モデルは使われていて、勉強しておいて良かったなと後から感じた本です。一回では理解し切れてないので2週目勉強中。

natsutan.hatenablog.com

他にも・・

Human-in-the-Loop機械学習 仕事は今年一年ずっとこれだった気がする。

natsutan.hatenablog.com

統計学入門 東京大学出版会 統計の勉強に使った。有名な本らしく、練習問題の解説がWebで見れるのはうれしい。

diffusion policy用にzarrのデータを作る

時間がかかったけど、なんとかzarr化できたのでメモ。

ドキュメントはここ https://github.com/real-stanford/diffusion_policy?tab=readme-ov-file#replaybuffer

data/pusht_cchi_v7_replay.zarr
 ├── data
 │   ├── action (25650, 2) float32
 │   ├── img (25650, 96, 96, 3) float32
 │   ├── keypoint (25650, 9, 2) float32
 │   ├── n_contacts (25650, 1) float32
 │   └── state (25650, 5) float32
 └── meta
     └── episode_ends (206,) int64

この構成のzarrデータを作る必要がある。 dataの下に入力データを並べ、metaの下にepisode_endsの配列を用意する。 入力データは全部まとめて一連の配列にする。配列の区切り(エピソードの区切り)をepisode_endsに入れておく。

zarr.saveだけではこの構造で保存できず。

store = zarr.DirectoryStore(OUTPUT_FILE)  
root = zarr.group(store=store, overwrite=True)  

こうやってから、個別にroot.create_datasetで上手くデータ作れました。

import os  
import glob  
import zarr  
from PIL import Image  
import numpy as np  
  
ROOTDIR = 'dofbot_data/OK'  
OUTPUT_FILE = 'dofbot.zarr'  
  
def create_state(root_dir:str) -> zarr.core.Array:  
    # ディレクトリのリストを取得  
    pose_list = []  
  
    for d in sorted(os.listdir(root_dir)):  
        # pose.csvの読み込み  
        pose_csv = os.path.join(root_dir, d, 'pose.csv')  
        with open(pose_csv, 'r') as f:  
            # 1行ずつ読み込む  
            for line in f:  
                pose_words = line.split(',')  
                # float型のリストに変換  
                pose = [float(w) for w in pose_words]  
                pose_list.append(pose)  
  
    # zarr配列に変換  
    z = zarr.array(pose_list, chunks=(1, len(pose_list[0])), dtype='float32')  
    return z  
  
def create_images(root_dir:str) -> zarr.core.Array:  
  
    images = []  
    for d in sorted(os.listdir(root_dir)):  
        # ディレクトリの中のpngファイルのリストを取得  
        files = sorted(glob.glob(os.path.join(root_dir, d, '*.png')))  
        for f in files:  
            # 画像の読み込み  
            img = Image.open(f)  
            # 画像をリサイズ  
            img = img.resize((480, 480))  
            # 画像をuint8の配列に変換  
            img_arr = np.array(img).astype('uint8')  
            images.append(img_arr)  
  
  
    # zarr配列に変換  
    z = zarr.array(images, chunks=(1, 480, 480, 3), dtype='float32')  
    return z  
  
  
  
def calc_episode_ends(dir: str) -> list:  
    # ディレクトリのリストを取得してソート  
    dirs = sorted(os.listdir(dir))  
    episode_ends = []  
  
    ei = 0  
    for d in dirs:  
        # ディレクトリの中のpngファイルのリストを取得  
        files = glob.glob(os.path.join(dir, d, '*.png'))  
        ei += len(files)  
        episode_ends.append(ei)  
  
    return episode_ends  
  
  
def create_action(states: zarr.core.Array, episode_ends:list) -> zarr.core.Array:  
  
    actions = []  
    for idx in range(len(states)):  
        if idx + 1 in episode_ends:  
            action = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]  
        else:  
            action_now = states[idx]  
            action_next = states[idx+1]  
            action = [action_next[0] - action_now[0],  
                      action_next[1] - action_now[1],  
                      action_next[2] - action_now[2],  
                      action_next[3] - action_now[3],  
                      action_next[4] - action_now[4],  
                      action_next[5] - action_now[5]]  
  
        actions.append(action)  
  
  
    z = zarr.array(actions, chunks=(1,), dtype='float32')  
    return z  
  
def main():  
    episode_ends = calc_episode_ends(ROOTDIR)  
    states = create_state(ROOTDIR)  
    actions = create_action(states, episode_ends)  
    images = create_images(ROOTDIR)  
  
    store = zarr.DirectoryStore(OUTPUT_FILE)  
    root = zarr.group(store=store, overwrite=True)  
  
    root.create_dataset('meta/episode_ends', data=episode_ends)  
    root.create_dataset('data/state', data=states)  
    root.create_dataset('data/action', data=actions)  
    root.create_dataset('data/img', data=images)  
  
    print(root.info)  
    print('saved', OUTPUT_FILE)  
  
  
if __name__ == '__main__':  
    main()

プログラミング写経のすすめ

プログラミング写経のすすめというタイトルで、社内勉強会しました。

  • 新しくプログラミング言語を学びたいけどどこから始めて良いのかわからない。
  • 漠然とスキルアップしたい気持ちがあるけど、何をして良いのかわからない
  • 文法はわかるけど、まとまったプログラムが一から書けない
  • 写経って初心者向けじゃ無いの?やる必要あるの?

こういう人向けです。 勉強会の時には、プログラミングに限らず自分でやりたことがある人、継続できる人は写経しないでそっちやってくださいという前振りから入ってます。万人にこれを勧めているわけでは無いです。

speakerdeck.com

なんかスキルアップで悩む人の参考になればと思ってまとめました。

Rustで作るプログラミング言語

だいたいやりたいところまでやったので感想を。 めっちゃ良い本なので、自作プログラミング言語興味ある人にお勧めです。

gihyo.jp

どんな本

Rustを使ってスタックマシーン、パーサー、ASTインタープリター、バイトコードコンパイラ、実行環境を作っていきます。型チェックのシステムやコルーチン等のトピックもあり、実際に動く所までやります。

Rustの説明は少なめで結構いろんなテクは使われているので、別途Rustの本は必要です。途中からはパーサー(+トーカナイザー)として、ライブラリのnomを使います。yaccのように別途設定ファイルがなく、ソースコードにrustの文法で直接ルールを書いていくスタイルです。

ソースコードは段階毎にgithubに公開されていて、写経するならそこを見ながらやれば大丈夫です。

github.com

対象読者

本の対象読者はこう書かれています。

  • いずれかの言語で中級以上のプログラミング経験をもつこと
  • コマンドプロンプトやシェルを使って最低限の操作ができる人
  • スタック、ヒープ、ツリー等の基本的なデータ構造を理解している人
  • Rustに興味があり、なにか作ってみる題材がないか探している人
  • 理論よりも実践を通してプログラミング言語を学ぶタイプの人

実際に内容をみても、結構中級者向けでした。

加えてプログラミング言語を作ってみようって人の一冊目にはお勧めしません。別の本で軽く勉強するか、できたら実装してからこの本に挑戦した方が良いです。パーサーの細かい動きの説明は足りない気がします。

特に拘りがが無ければ、低レイヤを知りたい人のためのCコンパイラ作成入門がお勧めです。

www.sigbus.info

Rustの観点から見ると本の説明とgithubのソースコードとの同期が時々取れてなく、自力で突破する必要があります。図は少なめ。前半にAST、後半にスタックの図が少しあるくらいで、その辺の基本的なデータ構造はわかっている前提です。とはいえ、そんなトリッキーな実装は無くどちらかというとわかりやすさを優先した素直な実装に感じました。

スタックベースの仮想マシーン

最初にスタックベースの仮想マシーンを作ります。ここから難易度が高かったです。 この章はわからなかったら飛ばしても良いと感じました。

/x 10 def /y 20 def { x y < } { x } { y } if

このコードは変数の宣言とifによる処理の分岐をしています。知っていれば典型的なスタックベースのソースコードなのですが、初めてだと何をやっているのかわからないと思います。

また不要な空白をスキップする機能が入ってないので、空白がちょうど1個出ないと駄目。2個続けるとそこが空文字列になって処理が終わってしまい、これに気がつくまで結構時間かかりました。

繰り返しですが、この章は上手く動かなかったら飛ばして次のパーサーに行っても良いです。次の章からが本番です。

パーサー

三章から本番で、独自言語のパーサーとインタープリターを作っていきます。 ここでnomというパーサコンビネータのライブラリを導入します。

github.com

パーサーの実装部分のソースコードです。細かい説明はないので、これみてあーこういう事してるんだろうなって想像する力は必要です。なので一回別の本で何かパーサーを実装してからの方が理解は進むと思います。

fn parens(i: Span) -> IResult<Span, Expression> {  
    space_delimited(delimited(tag("("), expr, tag(")")))(i)  
  
}

fn expr(i: Span) -> IResult<Span, Expression> {  
    alt((if_expr, cond_expr, num_expr))(i)  
}

fn identifier (input: Span ) -> IResult<Span, Span> {  
    recognize(pair(  
        alt((alpha1, tag("_"))),  
        many0(alt((alphanumeric1, tag("_")))),  
    ))(input)  
}

このライブラリが面白いのは、トーカナイザーとパーサーが別れてないんですね。実際パーサー書いてみるとトーカナイズと分ける必要ないんじゃ・・って思いますが、実際に別れていない形の実装は初めて見ました。普通の教科書だとトーカナイザーを教える為に、1章取って正規表現の説明があったりするのでとても新鮮に感じました。

スクリプト言語ランタイム

ここではインタープリターに機能を追加していきます。設計上のトレードオフについても言及があり面白いです。変数に宣言が必要なのか不要なのか、変数への代入は文なのか式なのか。あまり小難しい話に踏み込まず、実際のプログラミング言語がどうなっているかを紹介し、自作言語の仕様を決めていきます。

その中でもifを式にするか文にするかの部分が面白い。僕は無意識にif文って読んでしまうくらいifは文派です。C言語がそうなのでその影響を受けていると思います。この本では、ifは式として実装します。そうなると値を返す必要があり、if式にelseが無い場合の処理で、何を返すのか?という課題が出てきます。

実装している部分です。

If(cond, t_case, f_case) => {  
    if coerce_i64(&eval(cond, frame)?) != 0 {  
        eval_stmts(t_case, frame)?  
    } else if let Some(f_case) = f_case {  
        eval_stmts(f_case, frame)?  
    } else {  
        Value::I64(0)
    }

elseに相当する処理があるかないかわからないのでOption型とし、中身があればその処理結果を、中身が無ければI64(0)を返します。今までの言語処理系を作ろう系の本で、この処理を書いたことが無いです。面白いですね。

最後、暗黙の型変換をRustらしく書いておしまいです。

静的型付けと型チェック

ここは実行前に演算の型同士をチェックする仕組みを入れます。そんなに難しくないです。

ここでもifは式なので、elseが無く条件が成立しなかったときは、条件が成立したときの型と同じ型の何かを返す処理が入ってます。面白い。

ここでパース時にエラーがあったときにパース対象のエラーの場所を出す仕組みを入れます。この修正が結構大変で、今までもインタープリターに読ませるソースが間違っていて動かない事がしょっちゅうあった事もあり、もっと早く導入してほしかったと思いました。

バイトコードへのコンパイル

ここまではソースを読んでそのASTを直接実行していましたが、ここから一度バイトコードへ変換しファイルに保存した後、そのファイルを読み込んでスタックマシーンで動作する仕組みを作っていきます。

今まで通り最小限のコードから徐々に機能を足していきますが、追加する機能は説明済みですよねで飛ばされるので、githubのコードを眺めながら実装していきます。写経はかなり辛い部類に入ります。

レジスタの無いスタックマシーンのループはbreakやconituneまでいれると難易度が高かったです。低レイヤを知りたい人のためのCコンパイラ作成入門(https://www.sigbus.info/compilerbook)の実装と比べてみるのも面白いです。両方ともコンパイラの実装が進んで行くと、最終的なデータ構造が関数単位になるのは、おおって思いました。

ここが出来たところで、マンデルブロー動いて満足。

この後の型チェックは一度実装しているし、コルーチンからデバッガの流れも自力実装出来そうだったのでここまでにしました。面白かったです。

最後に

コードが素晴らしいだけで無く、本文で説明されているプログラミング言語に対する洞察がとても面白かったです。

一点お願いがあるとすれば、簡単で良いので最適化の情報が欲しかったです。ASTを作ってから、定数をたたみ込んだりループを展開したりしながらASTを変更するのをRustでどう書けば良いのかが難しく、一個でも二個でも例があればうれしかったです。

Rustのソースは素晴らしく、本の内容も面白く、プログラミング言語を作りたい人の二冊目としてお勧めします。

Diffusion Policyの学習にはシミュレーターが要らないという話

Diffusion Policyの話。

github.com

ここにあるコードを動かそうとすると、シミュレーターとしてGym環境が必要。論文を読んでいても、拡散モデルで軌道生成まではわかったが、それと強化学習が結びつかない。NNは次のActionを出力するから実質Policyなんだけど、どこでPolicyを学習しているのか、その学習方法は何なのかがわからず困っていた。

結果を言うと、Diffusion Policyではシミュレーターを使った強化学習は行ってない。拡散モデルを使って軌道を生成ししているだけ。シミュレーターは学習後の評価にのみつかっている。

確認するためにシミュレーターをいじってみる。

一番簡単なPush-Tであれば、pusht_env.pyがシミュレーター部分の実装になる。 ここのstep関数を置き換える。

   def step(self, action):
        #natu
        obs = np.array([220.35896549,376.36618479,186.64523911,486.46340961,115.90777847, 387.19133526, 173.2922699,
                         421.48437703, 260.59202287, 376.4185838, 156.51945005, 385.22763402, 202.13263356, 449.69142947,
                         213.30533348, 413.37675723, 137.2927573,  417.84081363, 239.71375082, 224.04484566,   1.,
                         1.,           1.,           1.,           1.,   1.,           1.,           1.,           1.,
                         1.,   1.,           1. ,          1.,           1.,           1.,   1.,           1.,           1.,
                         1.,           1.,        ])
        info = {'block_pose': [305.66986769, 150.93339659,  -0.92194449], 'goal_pose': [256.,         256.,           0.78539816], 'n_contacts': 1, 'pos_agent': [296.81839434, 147.6821614 ], 'vel_agent': [782.25953338, 299.67773711]}
        return obs, 0.0, True, info

一回動かして、その時の値を常に返すようにして、一回目でdoneをTrueにして返すように修正する。

これで一回でもstepが呼ばれるとエピソードがいきなり終了して、もし学習に使っていれば上手く学習できなくなる。

この状態で学習をしても、ちゃんと学習は進む。step関数を元に戻してevalしてもちゃんと動くpush-tが見れる。

つまり、Diffusion Policyは強化学習をしていない。

Diffusion Policyの学習データを覗いてみる

Diffusion Policyを動かそうとしています。

github.com

学習データがhttps://diffusion-policy.cs.columbia.edu/data/training/からダウンロードできます。 zarrというフォーマットから画像を取り出して確認してみました。

ソース

import zarr  
from PIL import Image  
  
#https://diffusion-policy.cs.columbia.edu/data/training/  
z = zarr.open('data/pusht_cchi_v7_replay.zarr')  
  
zimg = z['data/img']  
  
first_img = zimg[0].astype('uint8')  
pil_img = Image.fromarray(first_img)  
pil_img.save('first.png')  
  
print(zimg.info)

ダウンロードしたデータの位置を合わせて実行するとこう表示されます。

Name               : /data/img
Type               : zarr.core.Array
Data type          : float32
Shape              : (25650, 96, 96, 3)
Chunk shape        : (161, 96, 96, 3)
Order              : C
Read-only          : False
Compressor         : Blosc(cname='zstd', clevel=5, shuffle=BITSHUFFLE,
                   : blocksize=0)
Store type         : zarr.storage.DirectoryStore
No. bytes          : 2836684800 (2.6G)
No. bytes stored   : 29791685 (28.4M)
Storage ratio      : 95.2
Chunks initialized : 160/160

ポイントとしては、zimg = z['data/img'] とディレクトリのようにして画像ファイルの塊へアクセスすることと、uint8に変換することくらいです。

最初の画像はこうなっていて、puth Tのデータになってます。

Rustで二項係数を求める

Qiitaで見つけて面白かった記事の紹介。 二項係数を定義通りにやろうとすると結構大変らしい。 32bitだとあっさりオーバーフローするのは想像できてなかった。

qiita.com

ライブラリ使うならstatrsのbiominalが使える。

use statrs::function::factorial::binomial;  
  
fn main() {  
    let result = binomial(5, 2);  
    println!("{}", result);  
}