洋食の日記

記事をです・ます調で書き始めれば良かったと後悔している人のブログです

弥生株式会社を退職しました

2024年9月30日をもって、弥生株式会社(株式会社Misoca)を退職しました。株式会社Misocaに入社したつもりが、途中、弥生株式会社に吸収合併されて、株式会社Misocaはなくなってしまいました。約7年間の勤務を振り返ってみると、1年以上も手や指の腱鞘炎に悩まされたり、腱鞘炎が落ち着いてきたかと思ったら帯状疱疹になったり、今年の健康診断では潰瘍性大腸炎であることが発覚したりと、身を粉にして働いたなぁ、といった感じです。もう若くないですね。そんなわけで、老害になってしまう前に退職しました。大変お世話になりました。ありがとうございました。

※弥生社のソーシャルメディアガイドラインに抵触しないよう、仕事内容には触れず、個人的な内容に留めました。あしからず。

2023年はいつのまにか健康になった年だった

ここ数年、腱鞘炎とか帯状疱疹とか腫瘍切ったりとか色々あったけど、2023年はそういう謎の体調不良に襲われることが無かった。

あるとしたら、吐き気がして起きてられないくらいの激しい頭痛に襲われたぐらい。それも、後日、脳神経内科でCTとか丁寧に検査して頂いた結果、特に異常はなくて「肩コリが原因ではないか?」ということだった。頓服薬的な鎮痛剤1シートと、筋肉の緊張を緩める薬を2週間分と、肩コリ軽減の体操を教わって終わった。体操してたら頭痛は起きなくなって、鎮痛剤はほとんど飲まなかった。CTを撮ったのは初めてだったが、頭の中にギッチギチに脳みそが詰まっているのを見て「これは、もっと脳を使わないとダメだ」と前向きな気持になった。

ここ3ヶ月くらいは、疲れが次の日に残ることがなく、さらにこの1ヶ月くらいは、仕事で夕方や夜は疲れてるんだけど、それはそれで別物というか、夜も本を読んだり活動できてる。以前は疲れて起きてられないことが多かった。

まだ始めて数週間だけど、食事も改善しようとしていて、スナック菓子とかコンビニ弁当とか冷凍チャーハンとか全然食べてない。カロリーとか炭水化物とかタンパク質とかを気にして食事をとっている。ちなみにCOMPの抹茶味をアーモンドミルク100mlと水150mlで溶かすのが最近のお気に入り。

特別な努力はしてなくて健康になったので、年齢とは合ってないが、2021年が前厄・2022年が本厄・2023年が後厄という感じで、厄年が前倒しでやってきたと思うことにした(雑)。健康不安がなくなり、気持ちが前向きになっているので、2024年はなにか大きな成果を出したい。

RumaleにHessian Eigenmapsによる次元削減を追加した

はじめに

最近、Rumaleのリリース記事を書くことをすっかり忘れていた。最近リリースしたv0.28.1では、多様体学習の非線形次元削減手法であるHessian Eigenmaps(a.k.a. Hessian Locally Linear Embedding, HLLE)を実装した。HLLEは、各点の十分に小さい近傍では、多様体上の近くの点までの測地線距離が、対応するパラメータ点間のユークリッド距離と同一となるという、Local isometryをもとにデータ分布の構造を捉える。

Donoho, D. L., and Grimes, C., "Hessian eigenmaps: Locally linear embedding techniques for high-dimensional data," In Proc. Natl. Acad. Sci. USA, vol. 100, no. 10, pp. 5591-5596, 2003.

インストール

HLLEは、Rumale::Manifoldに含まれる。HLLEだけが必要な場合は、以下でインストールできる。

gem install rumale-manifold

HLLEのアルゴリズム固有値分解を必要とするので、Numo::TinyLinalgもインストールする。

gem install numo-tiny_linalg

簡単な実験

多様体学習の例題としてよく用いられる、長方形がくるりと巻かれた三次元のスイスロールデータを、二次元空間に展開することを試す。データ点のプロットに、Numo::Gnuplotを使いたいので、これをインストールする。

brew install gnuplot
gem install numo-gnuplot

多様体学習の代表的な手法であるLocally Linear Embedding(LLE)とHLLEによりスイスロールデータを二次元空間に展開するスクリプトは以下のようになる。

# frozen_string_literal: true

require 'numo/gnuplot'
require 'numo/tiny_linalg'
Numo::Linalg = Numo::TinyLinalg unless defined?(Numo::Linalg)

require 'rumale/manifold/locally_linear_embedding'
require 'rumale/manifold/hessian_eigenmaps'
require 'rumale/utils'

def make_swiss_roll(n_samples: 1000, random_seed: nil)
  rng = Random.new(random_seed || srand)
  theta = 1.5 * Math::PI * (1 + 2 * Rumale::Utils.rand_uniform(n_samples, rng))
  y = 20 * Rumale::Utils.rand_uniform(n_samples, rng)
  x = theta * Numo::NMath.cos(theta)
  z = theta * Numo::NMath.sin(theta)
  data = Numo::NArray.vstack([x, y, z]).transpose.dup
  [data, theta]
end

x, y = make_swiss_roll(n_samples: 2000, random_seed: 20231224)
y = Numo::Int32.cast(y)

plots = y.to_a.uniq.sort.map { |l| [x[y.eq(l), 0], x[y.eq(l), 1], x[y.eq(l), 2], { t: l.to_s }] }

Numo.gnuplot do
  set(terminal: 'png')
  set(output: 'swissroll3d.png')
  set(ticslevel: 0)
  set(view: [84, 11, 1, 1])
  splot(*plots)
end

# trn = Rumale::Manifold::LocallyLinearEmbedding.new(n_components: 2, n_neighbors: 10)
trn = Rumale::Manifold::HessianEigenmaps.new(n_components: 2, n_neighbors: 10)
z = trn.fit_transform(x)

plots = y.to_a.uniq.sort.map { |l| [z[y.eq(l), 0], z[y.eq(l), 1], { t: l.to_s }] }

Numo.gnuplot do
  set(terminal: 'png')
  set(output: 'swissroll2d.png')
  set(ticslevel: 0)
  plot(*plots)
end

これを実行した結果が次のようになる。まずは元データ、

3次元のスイスロールデータ

次にLLEにより二次元空間に展開したデータ、

LLEにより2次元に埋め込まれたスイスロールデータ

そしてHLLEにより二次元空間に展開したデータ、

HLLEにより2次元に埋め込まれたスイスロールデータ

LLEでは、もとの長方形が歪んだ形で二次元空間に展開されているが、HLLEは歪むことなく展開されている。スイスロールの他のデータでも、HLLEはLLEよりも、非線形なデータ分布を捉えることができる。

おわりに

今回、多様体学習の手法の中でHessian Eigenmaps(HLLE)を選択したのは、以下のNeurIPSのproceedingsで使われていたためである。

Horan, D., Richardson, E., and Weiss, Y., "When Is Unsupervised Disentanglement Possible?," NeurIPS'21, Vol. 34, pp. 5150--5161, 2021.

タイトルの通りで、教師なしのdisentaglement(和訳がわからないが高次元空間で入り組んでるデータから本質的な要素を取り出そうという感じ)問題に挑戦したものである。HLLEで非線形次元削減して、FastICAで独立な成分を求めることで、平均相関係数を評価尺度に用いた実験では、Auto-endcoderなどよりも良い結果を得ている。今では古典的な手法となったHLLEとFastICAで、distangledな表現が得られるというのがおもしろい。

古典的機械学習アルゴリズムは、いわゆるプロ驚き屋が話題にしないので影が薄い感じだが、毎年なにかしら有名な国際会議で研究発表がされている。そんなわけで、2024年もRumaleの開発は続いていく。

github.com

Rumale::SVMにClustered SVMによる分類器を追加した

はじめに

Clustered Support Vector Machine(CSVM)は、データをクラスタリングし、各クラスターごとに線形SVMを学習することで、非線形な分類器を実現する手法である。また、Rumale::SVMは、Ruby機械学習ライブラリであるRumaleと同様のインターフェースで、様々なSVMアルゴリズムを利用できるGemである。このRumale::SVMに、CSVMを実装した。

Gu, Q., and Han, J., "Clustered Support Vector Machines," In Proc. AISTATS'13, pp. 307--315, 2013.

インストールと使い方

Rumale::SVMのインストールは、gemコマンドで行う。

gem install rumale-svm

例として、LIBSVM Dataにあるletterデータセットの分類を行う。

wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/letter.scale.tr
wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/letter.scale.t

分類性能の評価のためrumale-evaluation_measureをインストールする。

gem install rumale-evaluation_measure

letterのデータを分類して、その分類の正確度を出力するスクリプトは以下の様になる。

require 'rumale/dataset'
require 'rumale/evaluation_measure'
require 'rumale/svm'

# letterデータを読み込む.
x_train, y_train = Rumale::Dataset.load_libsvm_file('letter.scale.tr')
x_test, y_test = Rumale::Dataset.load_libsvm_file('letter.scale.t')

# CSVMによる分類器を生成する.
# パラメータであるクラスタ数は128, 大域正則化パラメータは0.1, 正則化パラメータは10.0とした.
classifier = Rumale::SVM::ClusteredSVC.new(n_clusters: 128, reg_param_global: 0.1, reg_param: 10.0, random_seed: 42)

# 学習データで学習する.
classifier.fit(x_train, y_train)

# テストデータのラベルを推定する.
y_pred = classifier.predict(x_test)

# 推定結果の正確度を出力する.
acc = Rumale::EvaluationMeasure::Accuracy.new
puts format('Accuracy: %.1f%%', (100.0 * acc.score(y_test, y_pred)))

これを実行すると正確度は92.3%となった。

$ ruby example.rb
Accuracy: 92.3%

同様にして、線形SVMの正確度は69.7%、RBFカーネルカーネルSVMだと正確度は94.6%となった。CSVMが、線形SVMをベースとしているにも関わらず、非線形な分類器であるカーネルSVMに匹敵する結果を得られることがわかる。

アルゴリズムの説明

CSVMは、データをクラスタリングして、各クラスターごとに線形SVMを学習するというコンセプトだが、大域正則化というデータ全体での重みベクトルも学習することで、実際には一つに線形SVMの学習にたどり着く。

n個のラベルy_{i}が付与されたデータ\lbrace\mathbf{x}_{1}\ldots\mathbf{x}_{n}\rbraceが与えられ、クラスタリング手法によりk個のクラスタ\lbrace\mathbf{C}_{1}\ldots\mathbf{C}_{k}\rbraceに分けられたとする。また、l番目のクラスターに含まれるデータ数をn_{l}とする。このとき、CSVMの目的関数は次の様になる。

 \displaystyle
\text{argmin}_{\mathbf{w},\mathbf{w}_{l},\xi_{i}^{l}\leq0}\frac{\lambda}{2}||\mathbf{w}||^{2}+\frac{1}{2}\sum_{l=1}^{k}||\mathbf{w}_{l}-\mathbf{w}||^{2}+C\sum_{l=1}^{k}\sum_{i=1}^{n_{l}}\xi_{i}^{l}

 \displaystyle
\text{s.t.} \quad y_{i}^{l}\mathbf{w}_{l}^{\top}\mathbf{x}_{i}^{l}\leq 1-\xi_{i}^{l}, i=1,\ldots,n_{l},\forall l

ここで、\mathbf{w}_{l}l番目のクラスターでの線形分類するための重みであり、\mathbf{w}はデータ全体での線形分類するための重みである。\xi_{i}^{l}はスラック変数である。目的関数の第2項\frac{1}{2}\sum_{l=1}^{k}||\mathbf{w}_{l}-\mathbf{w}||^{2}は、大域正則化項で、各クラスターに過学習することを防ぐ。いま、\mathbf{v}_{l}=\mathbf{w}_{l}-\mathbf{w}とおくと、目的関数は次の様に書き直せる。

 \displaystyle
\text{argmin}_{\mathbf{w},\mathbf{v}_{l},\xi_{i}^{l}\leq0}\frac{\lambda}{2}||\mathbf{w}||^{2}+\frac{1}{2}\sum_{l=1}^{k}||\mathbf{v}_{l}-\mathbf{w}||^{2}+C\sum_{l=1}^{k}\sum_{i=1}^{n_{l}}\xi_{i}^{l}

 \displaystyle
\text{s.t.} \quad y_{i}^{l}(\mathbf{v}_{l}+\mathbf{w})^{\top}\mathbf{x}_{i}^{l}\leq 1-\xi_{i}^{l}, i=1,\ldots,n_{l},\forall l

さらに、問題を簡単にするため、データと重みベクトルをもとに、以下を定義する。

 \displaystyle
\tilde{\mathbf{w}}=\big\lbrack \sqrt{\lambda}\mathbf{w}^{\top},\mathbf{v}_{1}^{\top},\ldots,\mathbf{v}_{k}^{\top} \big\rbrack^{\top}

 \displaystyle
\tilde{\mathbf{x}}_{i}^{l}=\big\lbrack \frac{1}{\sqrt{\lambda}}\mathbf{x}_{i}^{l \top},\mathbf{0}^{\top}\ldots,\mathbf{x}_{i}^{l \top}\dots,\mathbf{0}^{\top}  \big\rbrack^{\top}

ここで、\tilde{\mathbf{x}}_{i}^{l}(l+1)番目の要素は\mathbf{x}_{i}^{l}となる。これにより、目的関数は一般的な線形SVMと同様のものに簡単化できる。

 \displaystyle
\text{argmin}_{\tilde{\mathbf{w}},\xi_{i}^{l}\leq0}\frac{\lambda}{2}||\tilde{\mathbf{w}}||^{2}+C\sum_{l=1}^{k}\sum_{i=1}^{n_{l}}\xi_{i}^{l}

 \displaystyle
\text{s.t.} \quad y_{i}^{l}\tilde{\mathbf{w}}^{\top}\tilde{\mathbf{x}}_{i}^{l}\leq 1-\xi_{i}^{l}, i=1,\ldots,n_{l},\forall l

このように、CSVMでは、各クラスタごとに線形SVMを学習するというところから、特徴変換により単一の線形SVMの問題にたどり着く。

よくある2-moonsデータで、CSVMの決定境界を示すと次のようになる。クラスタ数は8とした。

CSVMによる2-moonsデータの決定境界

非線形な決定境界が得られているのがわかる。クラスタ数を32と増やすとより滑らかな決定境界が得られる。

クラスタ数を32に増やしたCSVMによる2-moonsデータの決定境界

おわりに

Clustered Support Vector Machine(CSVM)は、各クラスタごとに線形SVMを学習することで、非線形な分類を実現しようという手法である。その最適化問題は、大域正則化を取り入れることで、特徴変換と単一の線形SVMの学習にいたるのがおもしろい。ただ、特徴変換後の特徴ベクトルの次元数は、クラスタ数に依存する。非線形でなめらかな決定境界を得ようとして、クラスタ数を大きくしたくなるが、そうすると、特徴変換後の特徴ベクトルの次元数が大きくなりすぎる。特に、元の特徴ベクトルの次元数が大きいと、安易にクラスタ数を増やすことはできない。

Rumale::SVMをライブラリとして充実させるために、2010年代前半頃の「カーネルSVMを大規模データがで学習するのが大変なので、なにかしら工夫して、線形SVM非線形分類器を実現しよう。」という手法を実装してきたが、そろそろこれぐらいで打ち止めにしようと思う。

github.com

Rumale::SVMにRandom Recursive SVMによる分類器を追加した

はじめに

Random Recursive Support Vector Machine(R2SVM)は、ランダム射影による特徴変換を繰り返すことで、線形SVM非線形な分類器を実現する手法である。また、Rumale::SVMは、Ruby機械学習ライブラリであるRumaleと同様のインターフェースで、様々なSVMアルゴリズムを利用できるGemである。このRumale::SVMに、R2SVMを実装した。

Vinyals, O., Jia, Y., Deng, L., and Darrell, T., "Learning with Recursive Perceptual Representations," In Proc. NIPS’12, pp. 2825–2833, 2012.

インストールと使い方

Rumale::SVMのインストールは、gemコマンドで行う。

gem install rumale-svm

例として、LIBSVM Dataにあるletterデータセットの分類を行う。

wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/letter.scale.tr
wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/letter.scale.t

分類性能の評価のためrumale-evaluation_measureをインストールする。

gem install rumale-evaluation_measure

letterのデータを分類して、その分類の正確度を出力するスクリプトは以下の様になる。

require 'rumale/dataset'
require 'rumale/evaluation_measure'
require 'rumale/svm'

# letterデータを読み込む.
x_train, y_train = Rumale::Dataset.load_libsvm_file('letter.scale.tr')
x_test, y_test = Rumale::Dataset.load_libsvm_file('letter.scale.t')

# R2-SVMによる分類器を生成する.
# パラメータである隠れ層の数は2, 重みパラメータは0.9, 正則化パラメータは1.0とした.
classifier = Rumale::SVM::RandomRecursiveSVC.new(n_hidden_layers: 2, beta: 0.9, reg_param: 1.0, random_seed: 42)

# 学習データで学習する.
classifier.fit(x_train, y_train)

# テストデータのラベルを推定する.
y_pred = classifier.predict(x_test)

# 推定結果の正確度を出力する.
acc = Rumale::EvaluationMeasure::Accuracy.new
puts format('Accuracy: %.1f%%', (100.0 * acc.score(y_test, y_pred)))

これを実行すると正確度は72.6%となった。

$ ruby example.rb
Accuracy: 72.6%

比較のために線形SVMを試してみる。分類器の生成の箇所を以下で置き換えればよい。

classifier = Rumale::SVM::LinearSVC.new(reg_param: 1, random_seed: 42)

結果として、線形SVMの正確度は69.7%となった。R2SVMのほうが、良い分類性能を得ている。

$ ruby example.rb
Accuracy: 69.7%

ちなみに、RBFカーネルカーネルSVMだと正確度は94.6%と、R2SVMより良い数字が得られる...まあ合う合わないがあるようだ。

アルゴリズムの説明

R2SVMについて簡単に説明する。あるD次元の入力データを\mathbf{x}_{1}\in\mathbb{R}^{D}とする。データはC個のクラスに分けられている。また、この入力データに対する線形SVMの出力を\mathbf{o}_{1}\in\mathbb{R}^{C}とする。これらと、正規分布N(0,1)に従うランダム射影行列をW_{2,1}\in\mathbb{R}^{D\times C}を用いて、以下のように特徴変換を行う。

 \displaystyle
\mathbf{x}_{2}=\sigma(\mathbf{x}_{1}+\beta W_{2,1}\mathbf{o}_{1})

ここで、\sigma(\cdot)シグモイド関数であり、\betaは元のデータ\mathbf{x}_{1}をどれくらい移動するかを制御する重みパラメータとなる。 こうして得られた\mathbf{x}_{2}を入力として線形SVMを学習して出力\mathbf{o}_{2}を得て、ランダム射影行列で...と同様の変換をフィードフォワード的に繰り返す。あるl層の変換は次の様になる。

 \displaystyle
\mathbf{x}_{l+1}=\sigma(\mathbf{x}_{1}+\beta \sum_{i=1}^{l}W_{i+1,i}\mathbf{o}_{i})

出力を連結するのではなく、ランダム射影して足し合わせるのが面白い。

よくある2-moonsデータで、R2SVMの決定境界を示すと次のようになる。

半円の端が誤分類されているが、まあデータ分布に沿った非線形な決定境界となっている。

おわりに

Random Recursive SVM(R2SVM)は、線形SVMをdeepにする方法の一つである。隠れ層の変換は、線形SVMに限らず、各クラスに対する予測スコア的なものを出力できる学習アルゴリズムであれば適用できる。発想は面白いが、手元にあるデータでは、カーネルSVMを超えるようなことはなかったので、実用性はちょっとアレかもしれない。

github.com