ぱたへね

ぱたへね

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

LeapMindの思い出

去年、LeapMindが解散するというニュースが飛び込んできた。

leapmind.io

twitterの関係者の様子を見ていると、不意打ちの解散では無くしっかり準備した解散だったのかなと思って見てた。

AIブームが来たとき、エッジAIの盛り上がりもすごくいくつもの会社がエッジAIに参入し、独自の環境を作り、ほとんどが消えていった。まあ、独自で作らなくてもラズパイレベルのハードがあれば普通に動くようになった。それじゃビジネスにならない。

当時色々出てきたエッジAI特化で残っているのってIdeinくらいじゃ無いかと思う。TAIの設立はもうちょっと後。

www.idein.jp

LeapMindを意識したのは都内の勉強会に呼ばれたとき、@yuki_mimu が司会やりながらビールの分析結果を楽しそうに話していたのが最初だ。FPGAで動くニューラルネットワークの話を聞いて、俺だったらもっと速く出来るんじゃ無いかなって思ってた。この辺、決め打ちの回路を高速化するのと、コンパイラを一個挟むのとの違いは難しい所だと思う。

同じ時期にIdeinの勉強会にも行って、コンボリューションの最適化の話を聞いたりもした。コンボリューションの最適化が全てだった時代があった。

AI周りの転職も活発だった時期で、LeapMindに落ちた人がうちに来たりして両方の話を聞けて面白かった。LeapMindでは採用が厳しくて来た人をすぐ判断しないと他社に流れちゃうって話を聞きながら、うち(パソナ)はAnaconda自力でインストールしたことあれば採用ですよって笑いながら話していた。

当時TFUG Kansaiをやっていて、何回か発表してもらったと思う。TAPLの翻訳者が来るぞと聞いて、サインをもらうつもりが本が見つからなくて挫折した。その方とは時々やりとりするくらいの仲だが、やっぱうちとはエンジニアの質が違うよなとずっと思ってた。とにかくLeapMindは格好良かった。

ちょっとエッジAIから離れることがあって、LeapMindの技術が宇宙に行ったとか、いやいや言ってないですよみたいな話を聞いていた。思い出した、探査機のはやぶさだ。それよりもはやぶさ2のFPGAがCyberWorkBench使っている方が驚いたな。

特にこの時期というのは無いけど、だんだんと知っている人がLeapMindを去って行って寂しくなっていたのは覚えてる。そういった人たちから、そろそろBlueoilの公開が終わるかも・・という話を聞いてローカルに全部保存しておいた。

ライセンスがApache 2.0なのでそのままUploadしてる。

github.com

ドキュメントもダウンロードしておくべきだったが今となっては遅い。

LeapMindは無くなったけど、LeapMindが作った物の一部はインターネットの片隅に残り、LLMを通して次の世代のコードの一部になる。

GenesisでURロボを動かしてみた

前回の続き

natsutan.hatenablog.com

姿勢を設定するset_dofs_position()を使って、URロボが動かせるようになった。

URDFからjointを数えると全部で7つのjointがある。 一つ目のjoint "shoulder_pan_joint"が、自由度を6持っていて、ロボの位置(x, y, z)と角度(rx, ry, rz)を指定している。残りの6つが実際に動かせる関節で、ここに角度を設定すればURロボットのARMが動かせる。これはこういう物なのか、どこかに記載されているのかは不明。

tkinterでUIを作り、動かして見た様子。

期待通りには動いているが、気になった点が三つ。

  • set_dofs_positionで姿勢を決めた後、物理エンジンに挙動を委ねると移動の影響でロボット自体が飛んで行く。本体は固定したい。
  • URDF内にjointの稼働範囲があるが、どう使うのかがよくわからない
  • 変な姿勢を取ると地面にめり込む。

最後の状況を避けるためには、姿勢を指定するのでは無く力で制御した方が良さそうだが僕の実力だと無理そう。

もう少し、チュートリアルを進めたい。

以下、全ソース

import numpy as np  
import tkinter as tk  
import genesis as gs  
import threading  
import queue  
  
sliders = []  
pose_queue = queue.Queue()  
  
def update_pose(value) -> None:  
    pose = np.zeros([12], dtype=np.float32)  
    for i, slider in enumerate(sliders):  
        pose[i] = slider.get()  
  
    # pose をqueueに入れる  
    pose_queue.put(pose)  
  
def start_tkinter():  
    root = tk.Tk()  
    root.title('UR5 Control')  
    # ウィンドウの横幅を指定  
    root.geometry('250x600')  
  
    # 最初の3つはx, y, z  
    label1 = tk.Label(root, text='x, y, z')  
    label1.pack()  
  
    for i in range(3):  
        slider = tk.Scale(root, from_=-1.0, to=1.0, resolution=0.01, orient=tk.HORIZONTAL, length=200)  
        slider.pack()  
        sliders.append(slider)  
  
    # 次の3つはrx, ry, rz  
    label2 = tk.Label(root, text='rx, ry, rz')  
    label2.pack()  
  
    for i in range(3):  
        slider = tk.Scale(root, from_=-3.14 / 2, to=3.14 / 2, resolution=0.01, orient=tk.HORIZONTAL, length=200)  
        slider.pack()  
        sliders.append(slider)  
  
    label3 = tk.Label(root, text='joints')  
    label3.pack()  
  
    for i in range(6):  
        slider = tk.Scale(root, from_=-3.14 , to=3.14, resolution=0.01, orient=tk.HORIZONTAL, length=200)  
        slider.pack()  
        sliders.append(slider)  
  
    # sliderの値が変わるとupdate_poseが呼ばれるようにする  
    for slider in sliders:  
        slider.config(command=update_pose)  
  
  
    root.mainloop()  
  
def main():  
    gs.init(backend=gs.gpu)  
  
    scene = gs.Scene(show_viewer=True)  
    plane = scene.add_entity(gs.morphs.Plane())  
    ur5 = scene.add_entity(  
        # ここにURDFファイルのパスを指定  
        gs.morphs.URDF(file='D:/home/myproj/genesis/pybullet_ur5_gripper/robots/urdf/ur5e.urdf'),  
    )  
  
    jnt_names = [  
        'shoulder_pan_joint',  
        'shoulder_lift_joint',  
        'elbow_joint',  
        'wrist_1_joint',  
        'wrist_2_joint',  
        'wrist_3_joint',  
        'ee_fixed_joint'  
        ]  
  
  
    dofs_idx = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]  
  
    scene.build()  
  
    # sliderの表示を別スレッドで実行する  
    t = threading.Thread(target=start_tkinter)  
    t.start()  
  
    pose = np.zeros([12], dtype=np.float32)  
  
    ur5.set_dofs_position(pose, dofs_idx)  
  
    for i in range(1000):  
        scene.step()  
  
        # queueからposeを取り出す  
        pose = pose_queue.get()  
        print('pose:', pose)  
        ur5.set_dofs_position(pose, dofs_idx)  
  
  
# main  
if __name__ == '__main__':  
    main()

GenesisでURロボットを表示する

Genesisのインストール

pytorchをインストールした後、ドキュメント通りにインストールするとtaichiでエラーがでました。 taichiだけ個別にインストールしても駄目。

pip install taichi-nightly
ERROR: Could not find a version that satisfies the requirement taichi-nightly (from versions: none)
ERROR: No matching distribution found for taichi-nightly

原因は使っていたPythonのバージョンでした。3.13はtaichiが対応していないので駄目、3.12に下げれば無事インストール出来ました。

URロボットのURDFをダウンロードする。

officialなURDFは見つからなかったので、ここからgit cloneしました。

github.com

Genesisで動かす。

Genesisが正しくインストールされていれば、サンプルの一行を変えるだけで表示されます。

import genesis as gs  
gs.init(backend=gs.gpu)  

scene = gs.Scene(show_viewer=True)  
plane = scene.add_entity(gs.morphs.Plane())  
franka = scene.add_entity(  
    # ここにURDFファイルのパスを指定
    gs.morphs.URDF(file='D:/home/myproj/genesis/pybullet_ur5_gripper/robots/urdf/ur5e.urdf'),  
)  
  
scene.build()  
  
for i in range(1000):  
    input()  
    scene.step()

最後にinput() を入れています。 すぐシミュレーションが終わってしまうので、キー入力待ちにして、動きを確認出来るようにしました。

実行すると無事URロボットが表示されました。

WSL2+MobaXtermでGENESISが動かなかった話

結論を先に書くとMobaXtermのX Serverが問題なので他のターミナルを使いましょう。

GENESIS自体のインストールはここの通りやればOK。

github.com

蛇のチュートリアルを実行すると、こんなエラーがでたり、バージョンによっては別のエラーが出ます。

[Genesis] [22:13:32] [INFO] Building scene <c9866d7>...
[Genesis] [22:13:35] [INFO] Compiling simulation kernels...
[Genesis] [22:13:40] [INFO] Building visualizer...
Exception in thread Thread-2 (_init_and_start_app):
Traceback (most recent call last):
  File "/home/natu/.pyenv/versions/3.12.8/lib/python3.12/threading.py", line 1075, in _bootstrap_inner
    self.run()
  File "/home/natu/.pyenv/versions/3.12.8/lib/python3.12/threading.py", line 1012, in run
    self._target(*self._args, **self._kwargs)
  File "/home/natu/myproj/genesis/Genesis/genesis/ext/pyrender/viewer.py", line 1138, in _init_and_start_app
    super(Viewer, self).__init__(
  File "/home/natu/myproj/genesis/.venv/lib/python3.12/site-packages/pyglet/window/xlib/__init__.py", line 167, in __init__
    super().__init__(*args, **kwargs)
  File "/home/natu/myproj/genesis/.venv/lib/python3.12/site-packages/pyglet/window/__init__.py", line 533, in __init__
    context = config.create_context(gl.current_context)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/natu/myproj/genesis/.venv/lib/python3.12/site-packages/pyglet/gl/xlib.py", line 117, in create_context
    return XlibContext(self, share)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/natu/myproj/genesis/.venv/lib/python3.12/site-packages/pyglet/gl/xlib.py", line 152, in __init__
    raise gl.ContextException(msg)

検索すればいろいろ対応策が出てきますが、どれもこれも解決せず。 ダメ元でPyCharmでWSLに入り、PyCharmのターミナルから実行すると、無事動作しました。

VS CodeでWSLに入りターミナルから実行しても同じように動きます。

Rust+Burnで線形回帰

データを作る所以外はほとんどCopilotまかせなので、いまいち実感がない。

use plotly::common::Mode;  
use burn::tensor::Tensor;  
use burn::backend::{Autodiff, Wgpu};  
use burn::backend::wgpu::WgpuDevice;  
  
type Backend = Wgpu;  
type AutoDIffBackend = Autodiff<Backend>;  
use plotly::Scatter;  
  
fn toy_data() -> (Vec<f32>, Vec<f32>) {  
    // 0.0~1.0の範囲でランダムなデータを100個生成してListを作成  
    //let device = Default::default();  
    let x = (0..100).map(|_| rand::random::<f32>()).collect::<Vec<f32>>();  
  
    // ノイズeを0.0から1.0の範囲で100個生成  
    let e = (0..100).map(|_| rand::random::<f32>()).collect::<Vec<f32>>();  
  
    // y = 2 * x + 5 + e  
    let y = x.iter().zip(e.iter()).map(|(x, e)| 2.0 * x + 5.0 + e).collect::<Vec<f32>>();  
  
    (x, y)  
}  
  
fn predict(x:&Vec<f32>,  w:&Tensor<AutoDIffBackend, 1>, b:&Tensor<AutoDIffBackend, 1>, device:&WgpuDevice ) -> Tensor<AutoDIffBackend, 1> {  
    let x = Tensor::<AutoDIffBackend, 1>::from_floats(x.as_slice(), &device);  
    let y = x * w.clone() + b.clone();  
    y  
}  
  
fn mean_squared_error(y: &Vec<f32>, y_hat: &Tensor<AutoDIffBackend, 1>) -> Tensor<AutoDIffBackend, 1> {  
    let y_hat = y_hat.clone();  
    let n = y.len() as f32;  
    let y = Tensor::<AutoDIffBackend, 1>::from_floats(y.as_slice(), &y_hat.device());  
    let loss = (y - y_hat).powf_scalar(2.0).sum() / n as f32;  
    loss  
}  
  
fn main() {  
    // 0.0~1.0の範囲でランダムなデータを100個生成してListを作成  
    let device = Default::default();  
  
    let (x, y) = toy_data();  
  
    let lr = 0.1;  
    let iter = 100;  
  
    let mut new_w = 0.0;  
    let mut new_b = 2.0;  
  
    for i in 0..iter {  
        let w = Tensor::<AutoDIffBackend, 1>::from_floats([new_w], &device).require_grad();  
        let b = Tensor::<AutoDIffBackend, 1>::from_floats([new_b], &device).require_grad();  
  
        let y_hat = predict(&x, &w, &b, &device);  
        let loss = mean_squared_error(&y, &y_hat);  
  
        let mut gradients = loss.backward();  
  
        let tensor_grad_w = w.grad_remove(&mut gradients);  
        let tensor_grad_b = b.grad_remove(&mut gradients);  
  
        new_w = w.into_scalar() - tensor_grad_w.unwrap().into_scalar() * lr;  
        new_b = b.into_scalar() - tensor_grad_b.unwrap().into_scalar() * lr;  
  
        if i % 10 == 0 {  
            println!("iter: {} loss = {}", i, loss.into_scalar());  
        }  
    }  
  
    println!("w = {}, b = {}", new_w, new_b);  
  
    let trace = Scatter::new(x.clone(), y.clone()).mode(Mode::Markers);  
    let mut plot = plotly::Plot::new();  
    plot.add_trace(trace);  
  
    let y_hat = x.iter().map(|x| new_w * x + new_b).collect::<Vec<f32>>();  
  
    let trace = Scatter::new(x.clone(), y_hat.clone()).mode(Mode::Lines);  
    plot.add_trace(trace);  
    plot.show();  
  
}

実行結果

Rust+Burnで勾配法

ローゼンブロック関数に対する勾配法のRust実装です。

元ネタはゼロから作るDeep Learning⑤から。

github.com

ソースはこれ。

use burn::tensor::Tensor;  
use burn::backend::{Autodiff, Wgpu};  
  
type Backend = Wgpu;  
type AutoDIffBackend = Autodiff<Backend>;  
  
  
fn rosenbrok(x0: &Tensor<AutoDIffBackend, 1>, x1: &Tensor<AutoDIffBackend, 1>) -> Tensor<AutoDIffBackend, 1> {  
    let x0 = x0.clone();  
    let x1 = x1.clone();  
  
    let y = (x1 - x0.clone().powf_scalar(2.0)).powf_scalar(2.0) * 100.0 + (x0  - 1.0).powf_scalar(2.0);  
    y  
}  
  
fn main() {  
    let device = Default::default();  
  
    let mut new_x0 = 0.0;  
    let mut new_x1 = 2.0;  
  
    let lr = 0.001;  
    let iter = 10000;  
    for i in 0..iter {  
        if i % 1000 == 0 {  
           println!("iter: {} x0 = {}, x1 = {}", i, new_x0, new_x1);  
        }  
        let x0 = Tensor::<AutoDIffBackend, 1>::from_floats([new_x0], &device).require_grad();  
        let x1 = Tensor::<AutoDIffBackend, 1>::from_floats([new_x1], &device).require_grad();  
  
        let y = rosenbrok(&x0, &x1);  
        let mut gradients = y.backward();  
  
        let tensor_grad0 = x0.grad_remove(&mut gradients);  
        let tensor_grad1 = x1.grad_remove(&mut gradients);  
  
        let grad0 = tensor_grad0.unwrap().into_scalar();  
        let grad1 = tensor_grad1.unwrap().into_scalar();  
  
        new_x0 = x0.into_scalar() - grad0 * lr;  
        new_x1 = x1.into_scalar() - grad1 * lr;  
  
    }  
    println!("x0 = {}, x1 = {}", new_x0, new_x1);  
}

最初上手く行かなかった所は、

let mut x0 = Tensor::<AutoDIffBackend, 1>::from_floats([new_x0], &device).require_grad();  

こうやってTensorを作った後、勾配を求めて更新するときに、こうすると駄目。

x0 = x0 - grad0 * lr;

この書き方だと計算グラフになってしまう。 一度スカラーにして再度Tensorを作り直しているが、これが想定されている使いかなのかがよくわからない。 計算結果はPythonバージョンとだいたい一致します。

Rust+Burnで勾配を取得する

BurnはRust製のDLフレームワークです。

burn.dev

実行前に少しだけ次元のチェックをしてくれます。

PyTorchでいうとdimのチェックをしてくれます。 [1, 3, 64, 64]と[3, 64, 64]は違うとエラーを出してくれますが、[1, 3, 64, 64]と[1, 3, 32, 32]は実行時のチェックになります。

Burnを使って単に自動微分の勾配を取得したかったのですが、それだけをやっているサンプルが見つかりませんでした。 なんとかここまで来れたのでメモ残しです。

ソースコード

rustソース

use burn::tensor::Tensor;  
use burn::backend::{Autodiff, Wgpu};  
  
type Backend = Wgpu;  
type AutoDIffBackend = Autodiff<Backend>;  
  
fn main() {  
    let device = Default::default();  
  
    let x = Tensor::<AutoDIffBackend, 1>::from_floats([5.0], &device).require_grad();  
    let y = x.clone().powf_scalar(2.0) * 3.0;  
  
    // 勾配を求めて表示する  
    let gradients = y.backward();  
    let tensor_grad = x.grad(&gradients).unwrap();  
  
    println!("y = {}", y);  
    println!("dy/dx = {:?}",  tensor_grad.into_scalar());  
  
}

carog.toml

[package]  
name = "ch5_nn"  
version = "0.1.0"  
edition = "2021"  
  
[dependencies]  
burn = { version = "~0.15", features = ["train", "wgpu", "vision", "candle"] }

説明

BackendにはWgpuを使い、さらにAutoDIffBackendを定義する。

type Backend = Wgpu;  
type AutoDIffBackend = Autodiff<Backend>;  

勾配が欲しいTensorにrequire_grad()をつける。 Tensorの所有権が移ってしまうと後で勾配が求められないのでclone()が必要。

    let x = Tensor::<AutoDIffBackend, 1>::from_floats([5.0], &device).require_grad();  
    let y = x.clone().powf_scalar(2.0) * 3.0;  

backward()した後勾配を求める。数値として取り出すには、into_scalar()が必要。

    let gradients = y.backward();  
    let tensor_grad = x.grad(&gradients).unwrap();  
    println!("dy/dx = {:?}",  tensor_grad.into_scalar());  

これで実行するとちゃんと勾配が求められています。

y = Tensor {
  data:
[74.99999],
  shape:  [1],
  device:  BestAvailable,
  backend:  "autodiff<fusion<jit<wgpu<wgsl>>>>",
  kind:  "Float",
  dtype:  "f32",
}
dy/dx = 30.0

ちなみにBurnはWSL2で動かすと遅いですが、Windowsで直接動かすと爆速です。 Windows環境の人はWindowsで動かす事をお勧めです。