建築では多重下請けでやれてるのに業務システムでだめなのはなぜ?

建築では多重下請けでやれてるのに業務システムでだめなのはなぜ?という質問がブコメであって、似たような話もいくつか見かけたのですが、建築などの施工図面に相当するのはソースコードで、建築現場で多重下請けでやってる作業は、ソフトウェアだと(でも?)ビルドです。なのでソフトウェアでは自動化されています。

もしも業務システムの納品物が、バベッジの階差機関のような歯車を組み合わせた機械式の計算機で、ビル一棟分に歯車をつめこんで組み立てて納品するというようなことになれば、多重下請けで分業してビルドするのが最もよい方法ということになると思います。

追記
「継続的デリバリーのソフトウェア工学」では、「ソフトウェア開発を選んだ私たちがバカでない限り、私たちにとっての製造とは、ビルドボタンのクリックです」とあります。橋梁建設を例に、物理的な製造・生産との違いが説明されています。

多重下請けでは構造的にいいソフトウェアが作れない

多重下請けではエンジニアが育たないという話を前回のブログで引用していたのですが、そもそも多重下請けではまともなソフトウェアは開発できないんではないかという気持ちになりました。

多重下請けでは、上位受け会社の「SE」が「設計」を行い、下位受け会社の「PG」が実装を行うという役割分担があります。というか、今回の話はそういう役割分担がある多重下請けを前提とします。

そうすると、設計というのは会社間をまたがった契約文書であり、発注のための作業指示書であるということになります。ソフトウェア開発で本質的に必要な文書というよりは、ビジネス構造によって必要になったビジネス文書です。ちなみに派遣ではなく業務委託のはずなので詳細な作業指示になってはいけないのもポイントです。
※余談ですが「設計は必要である」という人の話をきいてみると、必要なのは実装のための設計ではなく保守のためのドキュメントということがほとんどでした。

しかしながら、ソフトウェアというのは実装してみないとわからないことが多く、実装前に完成の形を予測するのは難しいものです。そのため、適切にソフトウェアを構築したいのであれば、事前に作成された設計に完全に従うことは求めず、実装にともなって変更が加わっていく前提にする必要があります。もし設計にたいして忠実に実装できて価値があるソフトウェアになることを保証したいのであれば、実装相当の検証作業が必要になるはずです。

けれども、上で書いたように設計というのは契約の一部であり作業指示です。そうなると、そのとおり忠実に作ることが前提であり、修正は例外という扱いになり「もし変更が必要になれば差し戻す」という感じになります。変更するには上位存在の承認が必要ということもあったりします。

その結果、多少の不備であればいびつな実装になろうとも従ったほうがマシとなり、大きな変更の必要性に気づいたとしてもそれを差し戻していたら作業開始が遅れるのでそのままにするということも起きかねません。

不備であれば差し戻すとしても、よりよいアイデアが浮かんだからといって、わざわざ差し戻すインセンティブはありません。ソフトウェアのアイデアは実装時に浮かびがちにもかかわらず。

また設計側でも、実装しながらの試行錯誤をしないのであれば実装確度の高い前例踏襲で無難なものになりがちで、やることにあわせた特別な設計は行われにくくなると思います。

実装者に創造的能力を期待せず裁量をもたせず責任をもたせず設計通りにやることを求めるということであれば、「PG」という職種は取り換え可能なコーダーということになりますね。

その結果、価値のあるソフトウェアを開発するというモチベーションは失われ、よいソフトウェアを作ることではなく、契約どおり仕様書どおり設計どおりプロジェクトを完了することが目的化します。

それは、「「実装者が設計を勝手に変えてはいけない」というのはダメでは」のようなことを書いたときに「勝手に変えるのはダメ」という反応がいくつかあったものが、それではよいソフトウェアにならないというものではなく、トラブルなく契約を遂行してビジネスをまわすためにダメという指摘であったことからも伺えます。

その設計では使えないソフトウェアになることがわかったとしても、実装者には責任がなく、むしろそれを指摘して設計やりなおしになると実装開始が遅れるものの納期は変わらなかったりするので、価値をうまないソフトウェアをそのままつくるほうがビジネス的に正しいという、モラルハザードのようなものも発生しがちです。

このように、多重下請け構造ではプロセス自体がソフトウェアの性質を反映してよいものを作れるようになっていないために、よりよくビジネスをまわすことがよりよいソフトウェアにつながっておらず、むしろちゃんとまわそうとすると いいソフトウェアを求めてはいけないビジネス形態になっていて、よりよいものを作ることにインセンティブが働かず、構造的にいいソフトウェアが作れなくなってると思います。

※追記 ブコメで「建築や造船では同じような形でうまくやってるように見える」という疑問が書かれてますが、建築や造船でソフトウェアのソースコードに相当するのは施工図面で、下請けをやとって足場くんだり溶接したりしている作業はソフトウェアでいえばビルドです。ビルドはソフトウェアの場合完全に自動化されています。手作業でやるのであれば、多数の下請けを雇ってもうまくいくと思います。
※さらに追記 ブログにしました
建築では多重下請けでやれてるのに業務システムでだめなのはなぜ? - きしだのHatena
※もうひとつ追記 ↓の本にも「ソフトウェア開発を選んだ私たちがバカでない限り、私たちにとっての製造とは、ビルドボタンのクリックです」って書いてあります。

日本のSIerの技術力の低さの要因から考えるアメリカソフトウェアの強さ

この連休はなんだかSIerについて考えることが多かったのですが、そういうことを考えると、なぜアメリカのソフトウェアが強いのかがわかってきた気がします。

まず、もちろんSIerの技術力が低いといっても技術力が高いSIerもいるわけで、とくにこのブログを見てる人だと技術力の高い側にいる人が多いと思います。

けれども、DX白書2023によればSIerのIT人材というのは75万人いて、技術力の高い人はその一部で、多くは技術力の低い側にいるんじゃないでしょうか。

https://www.ipa.go.jp/publish/wp-dx/gmcbt8000000botk-att/000108046.pdf

2014年、ちょうど10年前に、プログラマSIerと自社サービスで2分化するんではないかというブログを書いていますが、そのまま現実になった形です。
プログラマ業界の二分化 - きしだのHatena

SIerの技術力が低い要因がその多重下請けという産業構造にあるのであれば、多くが技術力が低い側にいると考えられます。

多重下請け構造において、実際にコーディングを行うのは末端のエンジニアです。
三次請け、四次請けのエンジニアが二次請けの開発現場に送り込まれます。彼らが主に開発を担当するのは細分化されたモジュールや一機能の部品単位の案件が中心です。そのため、スキルアップがしづらく優秀なエンジニアが育ちにくいことが指摘されています。
また、多重下請け構造によって、末端のエンジニアが所属する会社への報酬も微々たる額になるため給与も上がりにくく、結果的に低待遇・低スキルの環境から抜け出せない点も課題です。
一方で一次請け、二次請けのエンジニアは管理業務が中心になるため、スキルのアップデートがしにくいことから、あらゆるレイヤーで業務を通じた技術力の向上が見込みにくい状況が起きやすくなります。
今こそ見直したい多重下請け構造—DXで内製化シフトは実現するか

そして、日本のIT人材の75%が従事するSI業界で技術力が低いことは、日本のソフトウェア産業の弱さにつながります。

ところで、2020年の経済財政報告に次のような図があります。

https://www5.cao.go.jp/j-j/wp/wp-je20/h07_hz040210.html

日本ではIT人材はIT産業に集中していて、IT産業以外にはあまりいない、つまり日本では海外に比べてITシステムの内製化がかなり遅れているという説明がされます。この図でみると、アメリカでは多くが内製ということになりますね。

けど、アメリカのような英語圏ではオフショア外注の障壁がゆるく、単に国外に出しているんではないかということも考えられます。

実際、インドのバンガロールあたりは、90年代にアメリカのオフショア拠点として発展がはじまり、いまの「世界のIT拠点」という地位まで成長しています。そのとき、人件費の安いインドとの競争からアメリカ国内でのIT受託が立ち行かなくなったことも、Webサービス企業の成長につながったという話があります。

IT人材の数を見てみると、インドと中国がほぼ同数です。
https://prtimes.jp/main/html/rd/p/000001486.000005089.html
(日本のIT人材がこちらでは144万人になってますが、どうやら年15万人ずつくらいふえてるようです)

インドの中国は、どちらも人口が14億人で、それだけを考えれば同数のIT人材がいても不思議ではないのですが、AlibabaやTencentなどがありIT利用が進んでる中国と現状のインドを比べると、インド国内でのIT需要は供給を吸収できているとは思えません。

雑にインドのITエンジニアの150万人くらいはアメリカの仕事をしてるんじゃないかと思います。

先ほどの従事産業比率でいえば、アメリカは150万人がIT産業、300万人が非IT産業にいることになりますが、ここにインドの150万人をIT産業に組み入れれば、アメリカ国内のITのために作業するIT人材はIT/非ITで半々くらいになり、フランスやイギリスと同水準になります。

そうすると、アメリカのソフトウェアを開発している人は600万人ということになり、「なぜアメリカのソフトウェアは強いのか」の答えのひとつが「日本の4倍のIT人材がかかわってるから」であり、ではなぜアメリカ国内でもIT人材が増えたかというと、設計をそのままコードに置き換えればいいような付加価値の低い部分はアメリカの国外にだして、アメリカ国内では高付加価値のソフトウェアに集中した結果「稼げる産業」になったからというのがあるんではないかと思います。

※追記 SIerでの大半を占めていると思われる多重下請けの構造の話を書きました
多重下請けでは構造的にいいソフトウェアが作れない - きしだのHatena

Open SoraをGoogle Colabで動かして高速動画生成

さて、おうちで動画生成は難しいという結論になりました。
Open Soraを使っておうちのWindowsで動画生成する - きしだのHatena

ということで、GPUサーバー借りてやるのがいいのではと、Google Colabで試してみました。それなりに動画が生成できたので、末尾にいろいろ載せてます。

無償で使えるT4は16GBのGPUでOpen Soraは動かないので、課金が必要です。というか、24GBのL4でもout of memoryだったので、40GBのA100が必要です。

ガチャがんばろう。

最初にネタバレですが、セットアップだけで5コンピューティングユニットくらい消費します。まずはT4などを使って、起動してout of memoryが出るところまで手順を確認したあとで、A100を使うほうがいいです。

インストール

CUDAが12.2なので、12.1にダウングレードする必要があります。 NVIDIAのサイトのスクリプトだと「最新は12.5やで」みたいなメッセージを出して12.5をインストールしてしまうので、cudaのバージョンに12.1.0-1を指定するのと、途中でキーボードレイアウトを聞いてくるのでnoninteractiveをつけるのとが必要です。

!wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-ubuntu2204.pin
!sudo mv cuda-ubuntu2204.pin /etc/apt/preferences.d/cuda-repository-pin-600
!wget https://developer.download.nvidia.com/compute/cuda/12.1.0/local_installers/cuda-repo-ubuntu2204-12-1-local_12.1.0-530.30.02-1_amd64.deb
!sudo dpkg -i cuda-repo-ubuntu2204-12-1-local_12.1.0-530.30.02-1_amd64.deb
!sudo cp /var/cuda-repo-ubuntu2204-12-1-local/cuda-*-keyring.gpg /usr/share/keyrings/
!sudo apt-get update
!sudo DEBIAN_FRONTEND=noninteractive apt-get -y install cuda=12.1.0-1

リンクの貼り直し

!export PATH="/usr/local/cuda-12.1/bin:$PATH"
!export LD_LIBRARY_PATH="/usr/local/cuda-12.1/lib64:$LD_LIBRARY_PATH"

!unlink /usr/local/cuda
!ln -s /usr/local/cuda-12.1 /usr/local/cuda

Open Soraをclone

!git clone https://github.com/hpcaitech/Open-Sora

カレントディレクトリを移動

cd Open-Sora

依存をインストールします。

!pip install -r requirements/requirements-cu121.txt
!pip install -v .

なんかこんなの出るけどキャンセルします。

apexをインストール。ここで17分くらいかかります。

!pip install -v --disable-pip-version-check --no-cache-dir --no-build-isolation --config-settings "--build-option=--cpp_ext" --config-settings "--build-option=--cuda_ext" git+https://github.com/NVIDIA/apex.git

flash_attnも必要

!pip install flash-attn --no-build-isolation

ngrokの準備

Gradioを使うので、別ブラウザからアクセスできるようにngrokでトンネルつくる必要があります。
アカウントを作りましょう。
https://ngrok.com/

authtokenを確認します。

abcdefのところを、そのauthtokenに置き換えて実行。

!wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz
!sudo tar -xvzf ngrok-v3-stable-linux-amd64.tgz -C /usr/local/bin
!ngrok config add-authtoken abcdef

起動

gradioと同時にngrokを起動します。

!python gradio/app.py & ngrok http http://localhost:7860

モデルをダウンロードしてるとディスクがギリギリになるので警告が出ますが無視しましょう。

起動すると、local URLが出ます。

もちろんそのURLではアクセスできないので、ngrok経由でアクセス。Endpointsを見に行くとアクセスのためのURLが載っています。

アクセスすると信用できるところだけにアクセスしようと警告が出ますが、自分で立てたサーバーなのでそのままVisit Site

でました。

消費リソース

動画を生成すると、25GB以上のGPUメモリを使います。

ステップあたりの時間と消費メモリはこんな感じ。480pなら16秒まで、720pの4秒までならなんとか大丈夫。生成時間は動画時間に比例していますね。

2s 4s 8s 16s
144p 0.13s / 22.7GB 0.26s / 22.7GB 0.53s / 22.7GB 1.15s / 23.3GB
240p 0.35s / 23.3GB 0.7s / 22.5GB 1.45s / 24.2GB 3.19s 26.1GB
360p 1.16s / 25.2GB 1.72s / 25.6GB 3.7s / 26.4GB 8.13s / 31.5GB
480p 1.53s / 23.4GB 3.15s / 27.7GB 6.95s / 30.1GB 15.5s / 38.7GB
720p 3.8s / 26.5GB 8.09s / 36.8GB out of memory out of memory

基本的には1-2分で生成が終わるので、いろいろ試しやすくなりました。
プロンプトを試すときには360pで10stepsくらいにして、ちゃんと生成するときに480pで50stepsにするという感じになると思います。ただ、解像度を変えると生成動画も変わるので、480pにしたときに10stepsで試してseedを探るというのも必要かもしれません。

結果

Garallyにあるプロンプトを480p 4秒で試すと、結構ちゃんと出ました。

a close-up shot of a woman standing in a dimly lit room. she is wearing a traditional chinese outfit, which includes a red and gold dress with intricate designs and a matching headpiece. the woman has her hair styled in an updo, adorned with a gold accessory. her makeup is done in a way that accentuates her features, with red lipstick and dark eyeshadow. she is looking directly at the camera with a neutral expression. the room has a rustic feel, with wooden beams and a stone wall visible in the background. the lighting in the room is soft and warm, creating a contrast with the woman's vibrant attire. there are no texts or other objects in the video. the style of the video is a portrait, focusing on the woman and her attire.

dancingのような動きを指定したプロンプトがダメっぽい。これは「japanese maiko is dancing」で奇跡的にうまくいったやつ。それでも、袖はぐちょぐちょしてますね。

「japanese maiko. she is wearing a pink kimono」にすると、安定してます。ちょっと動く静止画を生成するくらいがよさそう。

猫もおとなしく座ってれば大丈夫。というか猫はほぼ静止画ですね。

brown striped cat is sitting in shibuya street. the city is illuminated by neon light. few people is walking in the background.the camera pans slowly through the scene

海の画像をちょっと波立たせるとかはかなりいいです。プロンプトは「sea is waving. cloud is flowing in the sky」

元画像はこれです。

ということで、特性を把握して、ちょっと背景を動かすという感じであれば、結構使えそうですね。

Open Soraを使っておうちのWindowsで動画生成する

Open-Soraという、オープンな動画生成モデルのバージョン1.2が6/17に出ていたのでWindowsで試してみました。
GPUメモリほしい。
GitHub - hpcaitech/Open-Sora: Open-Sora: Democratizing Efficient Video Production for All

Open-Soraのアーキテクチャとかはこちらが詳しい
【次世代動画生成】Open-Sora徹底解説【OSS版Sora?】

Windowsの場合、SpacesというモジュールがForkProcessesというモジュールを使っていて、これがUNIX系OSじゃないと動かないのでWSLを使う必要があります。

ということでWSLへのCUDAのインストール。11.8を使うことにします。ここの手順どおり。
https://developer.nvidia.com/cuda-11-8-0-download-archive?target_os=Linux&target_arch=x86_64&Distribution=WSL-Ubuntu&target_version=2.0&target_type=deb_local

wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-wsl-ubuntu.pin
sudo mv cuda-wsl-ubuntu.pin /etc/apt/preferences.d/cuda-repository-pin-600
wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda-repo-wsl-ubuntu-11-8-local_11.8.0-1_amd64.deb
sudo dpkg -i cuda-repo-wsl-ubuntu-11-8-local_11.8.0-1_amd64.deb
sudo cp /var/cuda-repo-wsl-ubuntu-11-8-local/cuda-*-keyring.gpg /usr/share/keyrings/
sudo apt-get update
sudo apt-get -y install cuda

リポジトリのclone

git clone https://github.com/hpcaitech/Open-Sora.git
cd Open-Sora

venvとか作っておくほうがいいですね。

python -m venv opensora
source opensora/bin/activate

依存のインストール

pip install -v .

CUDA11.8を使う場合は、xformersは自分でインストールします。

pip install -U xformers --index-url https://download.pytorch.org/whl/cu118

PyTorch 2.3.0を入れてくるので、改めて2.3.1を入れなおし。 https://pytorch.org/get-started/locally/

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

flash_attnも必要

pip install flash_attn --no-build-isolation

あと、ffmpegが必要です。なくてもいいけど、SORAのロゴを入れるのに使います。

sudo apt-get install ffmpeg

入れなくても動くけど、もしapexを入れるならpython.hがないというエラーを回避するためにpython-devが必要です。バージョンはインストールされているPythonにあわせてください。

sudo apt install python3.11-dev

apexを入れる場合のインストールはこう。

pip install -v --disable-pip-version-check --no-cache-dir --no-build-isolation --config-settings "--build-option=--cpp_ext" --config-settings "--build-option=--cuda_ext" git+https://github.com/NVIDIA/apex.git

そしたら起動

python gradio/app.py

360p 2秒 30ステップでRTX 4060 Ti 16GBだと2時間半かかりました。プロンプトは「2 cats are fignting」

画像を指定してやってみます。
「the man drinking beer」だけど、そうくるかーという感じw

数時間かかるのは、モデルがGPUに乗り切れてなくてCPUにオフロードしてるからです。
動画が安定しないのは、どうやら360p以下ではまともな絵が生成できないっぽい。
144pで「cat is dancing on the shibuya street」ではこんな感じになりました。

それではと、480pの動画を作ろうと思ったら、2時間たっても1stepも進まないので絶望。

GPUメモリは24GBでも足りないみたいなので、家庭用GPUでは無理そうですね。4060 Ti 16GBを2枚挿すといけるのかも?

追記: Google Colabに課金して動かす手順をまとめました。あとdancingとか動きを指定するとダメぽく、sittingとするなど静止画の背景を動かすくらいでやるとよさそうです。
Open SoraをGoogle Colabで動かして高速動画生成 - きしだのHatena

「いいね」による名誉毀損「いいね罪」はなりたたなくなった?

X(Twitter)で他人の「いいね」が見えなくなりましたね。
ということは、以前話題になった「いいね罪」は成り立たなくなったってことですね。

「いいね罪」というのは、誹謗中傷している第三者ツイートに多数「いいね」したことが名誉毀損不法行為とみなされたときに話題になりました。
杉田水脈議員が敗訴 中傷投稿「いいね」で賠償、初の確定 - 日本経済新聞

この「いいね」がされた時期は2018年5月くらいということで、2017年3月から「いいね」がタイムラインに表示されるようになっていたことから、RTに近かったのですよね。
そのことも併せて、「いいね」による拡散効果が認められたことからの不法認定だったと思います。
Twitterはなぜ他人の「いいね」をタイムラインに流す仕様を続けるのか 運営に直接聞いてみた - ねとらぼ

これはユーザーから不評だったのだけど、「実際の数字を見ると、日本も含め、「いいね」表示をされたツイートへのリツイートや返信などといった反応がとても高い」ということで続いていました。
“いいね”をフォロワーにさらさないで! Twitterの機能「〇〇さんがいいねしました」を巡る漫画に共感集まる - ねとらぼ

ただ、メモ的に「いいね」を使っている人のためにブックマーク機能が2018年2月に導入されています。
ツイートを簡単に保存、共有できるようにしました

その後、2021年くらいからトップツイートとタイムラインをわける試みが始まりいろいろと仕様がかわり、いまでは「いいね」は「おすすめ」にも表示されなくなっています。(ただし、さんざん「表示を減らす」した成果の可能性もあり他の人には出ていたかもしれない)

そして、「いいね」非公開化。
Xが「いいね!」非公開化 日本ユーザーは大混乱 イーロン・マスク氏「非公開にしてから『いいね!』激増!」 - ITmedia NEWS

プロフィールを見にいくと「いいね」欄が消えています。

他の人についた「いいね」も、数だけ見えて誰がいいねしたかは見れなくなっていますね。

ということで、現在はツイート主からだけ誰からの「いいね」か確認できるようになっていて、ブックマークに近くなってます。
「おすすめ」に個人の嗜好をより反映させる最適化のために、人目はばからず「いいね」できるようにした説があるけど、それはつまり嗜好を見れるほどにはブックマークが利用されてないってことなんだろうな。

まあ、ともかく、これで誰がどのツイートに「いいね」したことは見れなくなったので、「いいね」しても名誉毀損で裁判されることはなくなりそうで、「いいね罪」は成立しなくなりましたね。

しかし、こういった変更がたとえば裁判中に行われると、「いいね」にどんな拡散力があるかとかの証拠集めが大変そう。

そういえば「ふぁぼ」が「いいね」になったのいつだっけ?と思ったら2015年か。
Twitterのお気に入り(ふぁぼ)が「ハート」に 「★」から正式に変更 呼び方は「いいね」に - ITmedia NEWS

2013年には一瞬「足あと」になってたらしい。これは記憶にない。
Twitterの「お気に入り」が「足あと」に ふぁぼられ数の表示欄に変化 - ねとらぼ

初期には通知も来ず、各人のふぁぼ一覧をクロールして集計した「ふぁぼったー」を使って「ふぁぼられ」を確認していたけど、いつからふぁぼ数表示されてるかはわからなかった。2010年にはまだっぽい。

ところでこれは、ネットの誹謗中傷裁判が、弁護士のところにいって情報開示請求されてといったところから、原告被告がどんな感じになるか描いてるマンガ。

日本語CLIPを使って画像検索を作ったら素晴らしすぎた

LINEヤフーから日本語CLIPが出ていたので、どうやって使うんだろうと試してたら、なんかめちゃくちゃ便利な画像検索ができてしまいました。

clip-japanese-basic

LINEヤフーの日本語CLIP、clip-japanese-baseはこちらで紹介されています。
高性能な日本語マルチモーダル基盤モデル「clip-japanese-base」を公開しました

HuggingFaceのモデルはこちら。
https://huggingface.co/line-corporation/clip-japanese-base

CLIPとは?

ところでCLIPとは、となりますけど、OpenAIが公開してる、言語と画像を扱える機械学習モデルです。Contrastive Language-Image Pre-Trainingの略らしい。
https://openai.com/index/clip/

言葉と画像に対してそれぞれベクトルを返してくれて、内容が近ければ同じような向きになっているという仕組みです。

なので、一つの画像のベクトルをとってきて、複数の言葉のベクトルと比べてみれば、どの言葉が一番近いかというのがわかります。

HuggingFaceのサンプルでは「犬」「猫」「象」と比べてどれに近いかという分類をしてますね。

text = tokenizer(["犬", "猫", "象"])

そして犬の画像を与えてるので、最初の値が1になってる。

[[1., 0., 0.]]

逆に、一つの言葉のベクトルをとってきて、複数の画像のうち近い向きのものを探してくれば画像の検索になるというわけです。

日本語CLIPは、rinna、Stable AI、Recruitからも出ています。
rinna社、日本語に特化した言語画像モデルCLIPを公開|rinna株式会社
最高性能の、日本語画像言語特徴抽出モデル「Japanese Stable CLIP」をリリースしました — Stability AI Japan
Recruit Data Blog | 日本語CLIP 学習済みモデルと評価用データセットの公開

CLIPサーバーをつくる

ということでCLIPを使ってなにかを作りたいのですけど、HuggingFaceのモデルなので基本的にはPythonで動かします。
けれど処理はJavaで書きたいので、Web APIをつくります。
FastAPIというのを使ってWeb APIをつくりました。

画像からベクトルを得るimage_embedというエンドポイントを用意。あとでこれを使って、画像のインデックスを作ります。

@app.post("/image_embed")
def embed_text(request: TextRequest):
  image = Image.open(request.text)
  image_t = processor(image, return_tensors="pt").to(device)
  with torch.no_grad():
    image_features = model.get_image_features(**image_t)
  embedding = image_features.cpu().numpy().tolist()
  return {"embedding": embedding[0]}

それと、言語からベクトルを得るtext_embedというエンドポイントを用意。検索時にこれで検索語句からベクトルをとってきて、画像インデックスの中から近いベクトルをもってるものを探していきます。

@app.post("/text_embed")
def embed_text(request: TextRequest):
  text_t = tokenizer(request.text).to(device)
  with torch.no_grad():
    text_features = model.get_text_features(**text_t)
  embedding = text_features.cpu().numpy().tolist()
  return {"embedding": embedding[0]}

ソースはこれ。
https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c#file-clip_server-py

Javaからの呼び出し

PythonのWeb APIJavaから呼び出すコードを書きます。HttpClientを使うのだけど、FastAPIがHTTP2 Upgradeに対応してないようで、HttpClientがUpgrade: h2cというヘッダーをつけないように、HTTP/1.1を指定しておきます。

private static final HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_1_1) 
        .build();

というのを前回のブログにまとめてます。
PythonのFastAPIにJavaのHttpClientから接続しようとするとupgradeできないというエラーになるのでHTTP 1.1を指定する - きしだのHatena

あとは、呼び出すのみ。

String json = mapper.writeValueAsString(new TextRequest(text));
var req = HttpRequest.newBuilder()
        .uri(URI.create(BASE_URL + endPoint))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
var body = mapper.readValue(res.body(), EmbedResponse.class);
return body.embedding();

コードはこれ。
https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c#file-clipclient-java

画像からインデックスを作る

さて、JavaからWeb APIを経由してCLIPを呼び出せるようになったので、まずは画像からインデックスを作ります。
MongoDBとかに保存しようかと思ったけど、作ったインデックスを変更することはないので、ざっくりJSONで保存してます。

var images = Files.list(Path.of(PATH))
        .filter(p -> !Files.isDirectory(p))
        .map(p -> new ImageData(p, ClipClient.imageEmbedding(p.toString())))
        .toArray(ImageData[]::new);
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(Files.newOutputStream(Path.of("index.json")), images);

ちゃんとやるときはFiles.walkなどを使いましょう。
あと、CLIPの返すベクトルが単位ベクトルではないようなので、ちゃんと長さ1になるよう正規化したほうがいいです。

ソースこれ。
https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c#file-createindex-java

検索ワードと比較する

インデックスができたら、検索です。
検索語句のベクトルと角度が近いベクトルの画像を探すことになります。

大量の画像に対してまじめにやるなら近似最近傍探索(ANN)を使うほうがいいのだけど、個人の画像で数万件程度だと素朴な処理で十分です。

まずは角度をとるので内積。これVector APISIMD化すると速そうだけど、要素数が少ないので、あまり効きませんでした。

    static float prod(float[] a, float[] b) {
        float score = 0;
        for(int i = 0; i < a.length; ++i) {
            score += a[i] * b[i];
        }
    }

内積ではコサインがとれるので、大きい方が角度が浅く2つのベクトルが近いということになります。あとは大きいものを5つくらいとってくるようにすればいいだけ。

    Result[] search(String text) {
        var emb = ClipClient.textEmbedding(text);

        var top5 = new Result[topCount + 1];
        for (var img : images) {
            var score = prod(emb, img.embedding());
            for (int i = top5.length - 2; i >= 0; --i) {
                if (top5[i] == null) {
                    if (i == 0 || top5[i - 1] != null) {
                        top5[i] = new Result(img, score);
                    }
                    continue;
                }
                if (top5[i].score() < score) {
                    top5[i + 1] = top5[i];
                    top5[i] = new Result(img, score);
                }
            }
        }
        return Arrays.stream(top5).limit(top5.length - 1).toArray(Result[]::new);
    }

件数が少ないので、マルチスレッドで並列化しても効果なかった。

ソースこれ。
https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c#file-sercher-java

UIをつくる

じゃあ処理が全部できんたのでUIつくりましょう。ふつうのSwingのアプリです。 結果の表示をJTextPaneにHTMLで表示とか試してたのだけど、結局自分で描画しました。
ただ、そのときImageIO.readJPEGのOrientationを見てくれないので、metadata-extractorでOrientaionをみて回転するコードが必要です。

        int ori = 1;
        try {
            var metadata = ImageMetadataReader.readMetadata(Files.newInputStream(path));
            var dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
            ori = dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
        } catch(Exception ex) {}
        var trans = new AffineTransform();
        switch (ori) {
            case 6 -> {//右
                trans.translate(scaledH, 0);
                trans.rotate(Math.toRadians(90));
            }
            case 3 -> {//逆
                trans.translate(scaledW, scaledH);
                trans.rotate(Math.toRadians(180));
            }
            case 8 -> {//左
                trans.translate(0, scaledW);
                trans.rotate(Math.toRadians(270));
            }
        }
        var rotated = new BufferedImage(scaledW, scaledH, img.getType());
        var op = new AffineTransformOp(trans, AffineTransformOp.TYPE_BICUBIC);
        op.filter(img, rotated);

https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c#file-drawui-java

結果

結構いい感じです。
「戦う猫」でケンカ中の猫の写真がでてることから、ちゃんとシチュエーションの認識もできていますね。
また、「オブジェクト指向の本」でオブジェクト指向の本や「Object Oriented」と書いてあるプレゼンテーションが出ていることから、文字が読めていることもわかります。

あと、「寝てる猫」で出てる写真は結構気に入ってたけど探そうと思ってもなかなかみつからないやつで、こういったなつかしい写真がいろいろ掘り起こされるのが面白いです。

ということで、割と簡単なプログラムでめちゃくちゃ便利な検索ができるので、みんな作りましょう。
ソース、すでに出してますけど、あらためて。
https://gist.github.com/kishida/6c66b3c212f432a19aa176859163e93c