セーフィー株式会社のブログ - TECH PLAY

TECH PLAY

セーフィー株式会社

セーフィー株式会社 の技術ブログ

269

こんにちは、セーフィーでサーバーサイドの開発をしてる金成です。 今回は、ArchlinuxのインストールRTAを行ってみたいと思います。 Archlinuxと会社の業務とはほぼ関係ありませんが、やってみて色々勉強になったので共有させてください。 Archlinuxとは何か Archlinux install RTA レギュレーションについて Archlinuxのインストール手順について 1. 起動モードの確認 2. インターネットへの接続 3. システムクロックの更新 4. パーティションを作成 5. パーティションをフォーマットする 6. ファイルシステムのマウント 7. 必須パッケージのインストール 8. fstabの生成 9. chrootする 10. タイムゾーンの設定 11. ローカリゼーション 12. ホスト名の設定 13. ルートパスワードの設定 14. ブートローダーのインストール、設定 15. 再起動 今回実行するコマンドのまとめ 記録 完走した感想 参考 Archlinuxとは何か Archlinuxとは、Linuxディストリビューションのうちの一つです。 そもそもLinuxディストリビューションとは、OSの中核となるカーネルとその他のソフトウェア群を一つにまとめ、インストール・利用しやすくしたものです。 概ねLinuxディストリビューションにはパッケージマネージャー(apt, yum や dnfなど)も同梱され、ソフトウェアの維持管理がしやすいようになっています。 有名なディストリビューションは、ubuntuなどDebianを祖とするものやFedoraやcentOSなど Red Hat Linuxの系譜をひくものがあります。それ以外だと、OpenSUSEなどのslakwareの系譜のディストリビューションやGentoo Linuxなど変わったものも存在します。普段の開発では、ubuntuやcentOSなどにお世話になることが多いかもしれません。 今回利用するArchlinuxは、Debian系やRedHat系のどちらにも属さず、商用に利用されることはほとんどないOSです。 個人のデスクトップPC用に使う、自宅サーバーに使うなどが主な用途です。 自分で調べて解決できるユーザーをターゲットとしたOSのため、ユーザーフレンドリーでなく(GUIもデフォルトだとない)決して扱いやすいものではありませんが、 軽量で不必要なものの入っていないためカスタマイズ性が高く、好きなだけイジることができるため、自分好みのPCを組むことができるOSとなってます。 *1 Archlinux install RTA Archlinux Install RTAは、Archlinuxをライブ起動してからインストールし、各種設定を完了するまでの時間を競うバトルです。 先程書いた通り、Archlinuxは軽量なOSのため、インストールした時点ではほぼほぼ何も設定されいません。ディスクのパーティショニング、時刻の設定、ルートユーザーの作成 など一通りの初期設定をインストール時に行う必要があります。USBに焼いたisoファイルで起動して、各初期設定を終えてリブートするまでの時間を競うのが今回やることです。 今までネット上でゲリラ的に開催されていますが、決まったルールはないので、今回はこちらでルールを決めて行いたいと思います。 *2 レギュレーションについて 実機へのインストールだと、マシンごとの差が大きいため、仮想環境を使います。 Virtual Boxは筆者の使用してるPCだと起動できないので、今回はUTMを利用します。 レギュレーションは以下の通りです。 起動モードはUEFIモード インターネットへは接続できる状態にある partionは、ESPとルートの二つを作成する ローカルタイムの設定をする 言語設定をする /etc/hostnameはセットする bootloaderは何を使っても良い(今回は、systemd-bootを使う) live起動で、ターミナルが入力できるようになったらタイマースタート 再起動し、uname -r の表示がされたらタイマーストップ 仮想環境のUTMを使う utmの設定は、x86_64 アーキテクチャ、メモリ4Mib, ストレージ64GB, コア数2, システムStandard PC (Q35 + ICH9 2009) とする Archlinuxのインストール手順について Archlinuxのインストール手順については公式サイトのものを利用します。 始める前に、今回のインストール手順について解説していきます。なお、Archlinuxのインストールメディアはすでに準備してあるものとします。 以下コマンドをうち、実行完了するまでを競います 1. 起動モードの確認 UEFI変数(OSとファームウェアが情報を交換するために定義された変数)を、efivarsディレクトリを表示することで確認します。 ls /sys/firmware/efi/efivars # 起動モードの確認。正しく表示されればUEFIモード 2. インターネットへの接続 ネットワークインターフェースの有効化の確認と、ping での接続確認を行います ip link ping -c 3 archlinux.jp 3. システムクロックの更新 ライブ環境では、インターネットの接続が確立されると時刻が同期されます。 下記のコマンドで現在の設定を確認します timedatectl status 4. パーティションを作成 パーティショニングとは、ストレージデバイス(HDDやSSDなどの記録装置)を分割することを指します。論理的に分割された領域をパーティションといいいます。 これにより、1のハードディスクに複数のファイルシステムをもつことや用途別にパーティション作成することが可能になります。 パーティションの情報を格納するパーティションテーブルには、GPTとMBRの二つがありますが、今回はGPTを使用します Archlinuxでは、インストールに以下のパーティションが必要になります ルートディレクトリ / のパーティション EFIシステムパーティション (OSの起動やブートローダーの管理に利用) 今回はpartedコマンドで、0%-551MibをEFIシステムパーティション用に、残りをルートディレクリのパーティション用に分割します。 parted /dev/sda # インタラクティブモードでpartedを実行 (parted) mklabel gpt # PTにGPTを指定 (parted) mkpart ESP fat32 0 % 551Mib # EFIシステムパーティションを作成 (parted) set 1 esp on # パーティションを起動可能に (parted) mkpart primary ext4 551Mib 100 % # ルートディレクトリのパーティションを作成 (parted) q 5. パーティションをフォーマットする 作成したパーティションは、適切なファイルシステムでフォーマットされる必要があります。 下記のコマンドで作成したファイルシステムをフォーマットします。 mkfs.fat -F 32 /dev/sda1 # EFIシステムパーティションのフォーマット mkfs.ext4 /dev/sda2 # ルートディレクトリのパーティションのフォーマット 6. ファイルシステムのマウント マウントとは、OSにストレージやパーティションにあるファイルシステムを認識させ、アクセス可能にすることを指します。 下記のコマンドで、EFIシステムパーティション・ルートディレクトリのパーティションのファイルシステムを認識させ、アクセス可能にします mount /dev/sda2 /mnt # ルートディレクトリ mount --mkdir /dev/sda1 /mnt/boot # EFIシステムパーティション 7. 必須パッケージのインストール baseパッケージ、カーネル、ファームウェアのインストールをします. このコマンドでは、テキストエディタやネットワークを設定するソフトウェア(ネットワークマネージャー etc) のインストールは含まれていません。 場合によっては、インストール後にインターネットに接続できない状態になる可能性があります。通常であれば、エディタやネットワークマネージャーもインストールすることをお勧めします。 pacstrap -K /mnt base linux linux-firmware 8. fstabの生成 fstabファイルとは、パーティションやブロックデバイスをどうやってファイルシステムにマウントするかを記述したファイルです。 下記のコマンドでfstabを生成します。 genfstab -U /mnt >> /mnt/etc/fstab 9. chrootする 新しくインストールしたシステムにchrootします arch-chroot /mnt 10. タイムゾーンの設定 下記のコマンドでタイムゾーンを設定します。今回は Asia/Tokyo にします また、ハードウェアクロックをシステムの現在時刻に設定します ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime hwclock --systohc 11. ローカリゼーション en_US.UTF-8 のロケールの生成、環境変数の設定をします sed -i -e "171 s/^#//" /etc/locale.gen # 171行目に#en_US.UTF-8があるので、先頭の#を削除 locale-gen echo LANG=en_US.UTF-8 > /etc/locale.conf 12. ホスト名の設定 ホスト名とは、ネットワーク上でマシンを識別するために作られる名前です 基本的に好きな名前を使って大丈夫です 今回は入力速度を考慮して、safieとします echo safie > /etc/hostname 13. ルートパスワードの設定 パスワードを設定します passwd 14. ブートローダーのインストール、設定 ブートローダーとはパソコンの起動時に呼び出されるプログラムで、ストレージからOSを呼び出して起動するものです。 今回は、systemd-bootがbaseパッケージとともにインストールされるため、こちらを利用します。 下記のコマンドで、EFIブートマネージャーのインストール、設定を行います。 bootctl install # bootloaderのインストール cat << EOF > /boot/loader/loader.conf # 設定ファイルの作成. default arch timeout 0 editor no EOF cat << EOF > /boot/loader/entries/arch.conf # ローダーの追加 title archlinux linux /vmlinuz-linux initrd /initramfs-linux.img options root=UUID= $( blkid -s UUID -o value /dev/sda2 ) rw EOF 15. 再起動 exitで、chroot環境から抜けて、再起動します。任意ですが、アンマウントも実行します exit # live環境に戻る umount -R /mnt # アンマウント poweroff # シャットダウン. 停止後、インストールメディアを取り除きUTMで再起動。 rootユーザーでログインし、uname でカーネルのバージョンを表示したところでタイマーストップで す。 uname -r 今回実行するコマンドのまとめ ls /sys/firmware/efi/efivars # 起動モードの確認。正しく表示されればUEFIモード ip link # ネットワークインターフェースの状態を確認 ping -c 3 archlinux.jp # archlinux.jpとの疎通確認 timedatectl status # 時刻確認 parted /dev/sda # パーティション作成 (parted) mklabel gpt (parted) mkpart ESP fat32 0 % 551Mib (parted) set 1 esp on (parted) mkpart primary ext4 551Mib 100 % (parted) q mkfs.fat -F 32 /dev/sda1 # フォーマット mkfs.ext4 /dev/sda2 mount /dev/sda2 /mnt # マウント mount --mkdir /dev/sda1 /mnt/boot pacstrap -K /mnt base linux linux-firmware genfstab -U /mnt >> /mnt/etc/fstab arch-chroot /mnt ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime hwclock --systohc sed -i -e "171 s/^#//" /etc/locale.gen locale-gen echo LANG=en_US.UTF-8 > /etc/locale.conf echo safie > /etc/hostname # 入力速度を考慮してsafieに passwd bootctl install # bootloaderのインストール cat << EOF > /boot/loader/loader.conf # 設定ファイルの作成。起動時にarch.confを見るように default arch timeout 0 editor no EOF cat << EOF > /boot/loader/entries/arch.conf # ローダーエントリの追加 title archlinux linux /vmlinuz-linux initrd /initramfs-linux.img options root=UUID= $( blkid -s UUID -o value /dev/sda2 ) rw EOF exit umount -R /mnt # マウントを解除 poweroff # 電源を切る # utmでCD/DVDを消去し起動 # rootでログイン uname -r # 表示されたらタイマーストップ 記録 記録は、11:07.19 でした 完走した感想 完走した感想ですが、キーボードの入力速度とコマンドをどれだけ記憶してるかが勝負を分つと思います。 OSのインストールに関わるコマンドやツールを色々と知ることができたので楽しかったです。 ちなみに、このレギュレーションでやってる人はいなかったので私が世界一です(N=1)。皆さんの挑戦をお待ちしております。 最後になりますが、セーフィーではエンジニアを積極的に募集しています。気になる方はこちらをご覧ください! https://safie.co.jp/teams/engineering/ カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 最後までお読みいただき、ありがとうございました。 参考 Arch Linux - ArchWiki インストールガイド - ArchWiki Parted User’s Manual *1 : ただ、archlinuxのwikiは説明が充実しており、わかりやすいです。 *2 : インストールのスピードを競う必要性は全くありません。
はじめに こんにちは、セーフィー株式会社で iOS エンジニアをしている篁です。 普段は Safie Viewer の iOS アプリを開発しています。 およそ1年半前に Registered Scrum Master 研修を受ける機会に恵まれ、それ以来 iOS チームのスクラムマスターをしています。 私のチームでは、1年前からスプリントの振り返りに Sailboat Retrospective というフレームワークを利用し始めました。利用してみていくつかのメリットを感じられたため、この記事で紹介させていただきます。 はじめに Sailboat Retrospective とは 導入の経緯 振り返りのアジェンダ アイスブレイク (5分) 前回のスプリントで設定したアクションの振り返り (1分) 今スプリントで完了できなかったタスクの確認 (5分) 今スプリントの振り返り (5〜10分) 付箋の内容について各メンバーから共有してもらう (10分) 次回のスプリントで具体的にアクションすべき付箋を決める (1分) 具体的なネクストアクションを決める (2〜4分) まとめ 参考 Sailboat Retrospective とは Sailboat Retrospective は、スクラムチームがプロジェクトの振り返りを行うためのフレームワークの一つです。 プロジェクトをヨットでの航海に見立て、チーム (ヨットの乗組員) が目標に向かって進むための推進力 (追い風) や障害 (錨) などを視覚的に表現します。 私のチームで用いている具体的な要素とそのメタファーは以下の通りです。 要素 メタファー 内容 Goal ヨットが向かう島 スプリントゴール Wind ヨットを進める追い風 プロジェクトを前進させるポジティブな要素や成功要因。例えば、良いコミュニケーション、良いリサーチ、なんでもOK Sun 行き先を照らす太陽 嬉しかったこと、良かったこと。goal と関係なくてもOK Cloud 影を落とす雲 モヤモヤ気になることなんでも Anchor ヨットの動きを妨げる錨 チームの進行を妨げる課題や問題点。例えば、コミュニケーション不足、タスクに集中する時間が少ない、その他なんでもOK Reef (小) 海中の小さな岩礁、目には見えないが進行を妨げる可能性がある 近い将来、チームに悪影響を及ぼす可能性のあるリスクや障害 Reef (大) 海中の大きな岩礁、目には見えないが進行を妨げる可能性がある 将来、チームに悪影響を及ぼす可能性のあるリスクや障害 表にある要素以外にも、チームでオリジナルの要素を追加したり、不要な要素を除いたりして、チームに最適化していくのが良いと思います。 私のチームでは、普段から Figma の利用に慣れていたこともあり、FigJam の Sailboat Retro テンプレート を利用しています。他にも楽しいテンプレートが色々とあるようです。 各要素の振り返りを視覚的に見やすく整理することで、改善点の明確化と次のアクションプランの策定がスムーズに進みます。 最終的には、付箋やリアクションが貼られた以下のような状態になります。詳細内容については、後述の振り返りのアジェンダで説明します。 導入の経緯 私のチームでは、以前は KPT フレームワークを利用して振り返りを行っていました。 KPT は Keep、Problem、Try の3つの要素に対して振り返りを行います。 要素 内容 Keep 上手くいっていることや引き続き実施したいこと Problem 課題や問題点、改善が必要なこと Try 試してみたい新しいアイディアや改善策 KPT は、短時間で振り返りを行うのに適しており、チームがすぐに具体的なアクションプランを立てることができます。 しかし、手軽に振り返りができる一方で、以下の課題を感じていました。 反省会の意味合いが強くなり、固い雰囲気になる 要素が大きく分類されているため、少し抽象的な意見が出やすい 毎回似たような振り返りになりがち 課題を共有したものの解決されずに長く残り続ける そんなときに Sailboat Retrospective フレームワークを知り、上記課題を解決することを期待して導入に至りました。 導入時には、KPT を利用していたチーム状況が似ていることもあり、 こちらの記事 を参考にさせていただき、振り返り要素などを参考にしました。 振り返りのアジェンダ 私のチームでの振り返りのアジェンダを紹介します。 ファシリテータはスクラムマスターが務めますが、慣れてきたらメンバーで当番制にしても良いかもしれません。 アイスブレイク (5分) チームメンバーの調子と1つの話題について付箋で共有してもらい、ワイワイ話します。 前回のスプリントで設定したアクションの振り返り (1分) 前回のスプリントで設定した、今スプリントで実施すべきアクションを達成できたかどうかを確認します。 達成できていたら付箋を剥がし、達成できなかった場合は原因を議論して、継続アクションとするか別のアクションにするかを決定します。 今スプリントで完了できなかったタスクの確認 (5分) スプリントで完了できなかったタスクがあれば、担当者に進捗と完了できなかった原因などを共有してもらいます。 セーフィーでは Backlog でタスクを管理しているため、実際にチケットを眺めながら確認しています。このときにベロシティなどについてもチームで共有します。 今スプリントの振り返り (5〜10分) まずは5分間を計測し、各メンバーが今スプリントでの出来事や感じたことを自由に付箋に書いて各要素に貼っていきます。時間が足りないときは適宜延長します。 FigJam ではタイマーと連動した BGM を流すことができ、作業中も静まり返らないようにするため、個人的にお気に入りの機能です。 付箋の内容について各メンバーから共有してもらう (10分) 付箋を順番に見ていき、付箋を貼ったメンバーから内容を共有してもらいます。 私のチームではポジティブな要素から順番に共有しています。他のメンバーは良いところを褒めたり、リアクションスタンプを付箋に追加して、メンバー全員が議論に参加するという雰囲気作りを意識します。 次回のスプリントで具体的にアクションすべき付箋を決める (1分) 1分間を計測し、各自が次回のスプリントで具体的なアクションとして取り組むべきと考える付箋に投票します。 投票数の制限はなく、一人あたり何票投票してもOKです。このとき、投票者が分かるようにしています。FigJam では自分のアイコンのスタンプを利用できます。 具体的なネクストアクションを決める (2〜4分) 1票以上投票されている付箋の内容について議論し、具体的なネクストアクションを決めます。 議論の内容や決定したアクションは、別枠を設けて新しい付箋に書いて貼ります。実行しづらい抽象的なアクションを避けて、可能な限り具体的に実行しやすいアクションにします。たとえば、チームのルールとしてドキュメント化、タスクとしてチケットの作成、チームで相談したいのでカレンダーに追加、などです。 まとめ これまで Sailboat Retrospective での振り返りを行ってきましたが、先述した課題は解決され、以前よりチームの一体感が醸成されたと感じています。 Sailboat Retrospective には、改めて以下のメリットがあると感じています。 視覚的な要素を含むことにより、チームメンバーが直感的に理解しやすく、リアクティブに楽しく振り返りできる 問題点 (Anchor) や推進力 (Wind) など、プラス面とマイナス面をバランスよく議論することにより、チームがどの方向に進むのか、何が進捗を妨げているのかを明確に把握できる 具体的なネクストアクションを設定することで、課題が長い間放置されず解決に進む ルールも簡単なので、振り返りを少し変えてみたいというチームにオススメしたいです。今後もより良い振り返りに改善していきます。 参考 Sailboat Retro FigJam board 脱 KPT 法で楽しく有意義に振り返り! Sailboat Retrospective Backlog
はじめに はじめまして!事業戦略部の松本です。ビジネスサイドでSafie API関連のサポートを担当しています。 本記事ではSafie APIとDeep Learningを利用して、飼い猫達を画像分類し給仕DXをしたのでお伝えしていきたいと思います。 はじめに やりたいこと Safie APIとは どうするか 画像取得APIを実行してみる 猫を学習 モデルを学習する関数 実行するタイミングを決める いつでもどこでも見られるようにする やりたいこと 我が家には飼い猫のチャチャ(メス: 2歳7か月)とクロ(オス: 1歳11か月)が暮らしています。 サビ猫のチャチャ 黒猫のクロ 当初はチャチャを1匹でお迎えしていましたが、YouTubeなどで2匹の飼い猫が仲良く暮らしているのを見て憧れ、1年ほど後にクロもお迎えすることにしました。 初めての多頭飼い(複数のペットを飼うこと)だったので、事前に色々予習をしました。生活回りのものは分けた方がいいということだったので、餌やり器や水やり器、トイレなども別々で用意していたのですが、幸か不幸か2匹は同じものを利用しているようでした。 そこで出てきたのが、チャチャとクロが「 それぞれどのくらいの餌を食べているのか分からない問題 」(以後、猫餌問題)でした。 今までチャチャ1匹の時は、食べている餌の量でその日の体調の良し悪しが分かっていたのですが、2匹が同じ餌やり器を共有することでそれが分からなくなってしまったのです…...。 そこで、Safie APIとディープラーニングを組み合わせて、チャチャとクロのそれぞれがどのくらい餌を食べているのか判別するようにしました! Safie APIとは Safieでは、ユーザ様がSafieカメラで撮影した映像や静止画を自社のツールに組み込んだり、AIerなどが提供する映像解析と連携させるために、 オープンAPI であるSafie APIを提供しています。 Safie APIでは映像や静止画などのバイナリデータの他、Safie OneなどのエッジAIを搭載したカメラで利用できるAI-App人数カウントのデータも取得できます。その他にも取得できるデータは様々あり、取得方法も併せて 開発者向けサイトSafie Developers に纏まっています。 もちろんデータの利用には映像データ所有者のユーザ様の事前許諾が必要で、認可/ 認証にはOAuth2.0/ APIキーを採用しています。 どうするか 今回は猫餌問題解決のため、「画像取得API」を利用することにしました。このAPIでは実行したタイミングの最新の静止画が取得出来る他、指定した過去の任意の時点の静止画を取得することもできます(録画プランの範囲内に限ります)。 我が家では電動で猫の餌が出る餌やり器を利用しています。この餌やり器をSafieカメラで撮影し、そこに映った猫がチャチャなのかクロなのかを TensorFlow を利用し画像分類して判別できるようにしたいと思います。 画像取得APIを実行してみる まず画像取得APIを利用して、カメラの静止画を取得してみます。Safie APIを利用するにはまず Safie Developers でアプリケーションを作成します。 Safie APIでは認証方式をOAuth2.0とAPIキーの2種類から選択できます。APIキー方式ではトライアル利用もできるため、今回はAPIキー方式を採用します。 APIキーの発行もSafie Develoeprsから実施します。 実際の画像取得APIの実行方法については、 APIリファレンス で仕様を確認できます。 リファレンスではAPIリクエスト時のエンドポイントやパラメータ、レスポンスの内容が確認できます。 画像取得API で必要なパラメータはdevice_idとtimestampで、timestampを省略すると直近の静止画がレスポンスで返ります。device_idはSafieカメラごとに固有のIDでシリアルナンバーとは異なり、 デバイス一覧取得API で確認できます。 import requests api_key = "YOUR_API_KEY_HERE" device_id = "YOUR_DEVICE_ID_HERE" url = f "https://openapi.safie.link/v2/devices/{device_id}/image" headers = { "Safie-Api-Key" : api_key} try : res = requests.get(url, headers=headers) res.raise_for_status() except : print (f "Error! Status code: {res.status_code}" ) else : with open ( "./safie_image.jpg" , "wb" ) as f: f.write(res.content) APIを実行する際はリクエストヘッダにAPIキーを付与します。実行したところ静止画が保存されていることが確認できました。 保存される静止画のイメージ。チャチャが餌を食べているところ。 猫を学習 次に保存した写真を分類するための前準備として「チャチャ」と「クロ」と「猫が映っていない」の3パターンをTensorFlowを使って機械学習させていきます。 具体的には、それぞれの食事中の写真を数百枚ずつほど用意しました。 ここは手動となるので地道にSafie Viewerとにらめっこしながら作業しますが、Safieの多くのカメラには動きのあった時間を検知する「モーション検知機能」が標準で用意されているのでそこまで大変な作業ではありませんでした。 猫は部屋が暗い夜間でも餌を食べます。Safieの多くのカメラには真っ暗な環境でも白黒の映像を撮影できる「IR機能」が備わっているため、今回はIR機能を常時ONにし、学習させる画像データもIRがONの白黒のものにしました。 これらの写真を使ってデータセットの作成・モデルの構築をしていきます。 こちらのサイト を参考にしました。 1.データセットの作成。データ拡張のため画像の回転・反転を行っています。 from PIL import Image import glob import numpy as np from PIL import ImageFile ImageFile.LOAD_TRUNCATED_IMAGES = True classes = [ "chacha" , "kuro" , "na" ] num_classes = len (classes) image_size = 64 num_testdata = 25 X_train = [] X_test = [] y_train = [] y_test = [] for index, classlabel in enumerate (classes): photos_dir = "./" + classlabel files = glob.glob(photos_dir + "/*.jpg" ) for i, file in enumerate (files): image = Image.open( file ) image = image.convert( "RGB" ) image = image.resize((image_size, image_size)) data = np.asarray(image) if i < num_testdata: X_test.append(data) y_test.append(index) else : for angle in range (- 20 , 20 , 5 ): img_r = image.rotate(angle) data = np.asarray(img_r) X_train.append(data) y_train.append(index) img_trains = img_r.transpose(Image.FLIP_LEFT_RIGHT) data = np.asarray(img_trains) X_train.append(data) y_train.append(index) X_train = np.array(X_train) X_test = np.array(X_test) y_train = np.array(y_train) y_test = np.array(y_test) xy = (X_train, X_test, y_train, y_test) np.save( "./chacha_kuro_datasets.npy" , xy) 2.モデルの作成(一部)。作成したデータセットから猫の判別に利用するKerasモデルを作成しています。 # データを読み込む関数 def load_data (): X_train, X_test, y_train, y_test = np.load( "./chacha_kuro_datasets.npy" , allow_pickle= True ) X_train = X_train.astype( "float" ) / 255 X_test = X_test.astype( "float" ) / 255 y_train = np_utils.to_categorical(y_train, num_classes) y_test = np_utils.to_categorical(y_test, num_classes) return X_train, y_train, X_test, y_test モデルを学習する関数 def train (X, y, X_test, y_test): model = Sequential() model.add(Conv2D( 32 ,( 3 , 3 ), padding= 'same' ,input_shape=X.shape[ 1 :])) model.add(Activation( 'relu' )) model.add(Conv2D( 32 ,( 3 , 3 ))) model.add(Activation( 'relu' )) model.add(MaxPooling2D(pool_size=( 2 , 2 ))) model.add(Dropout( 0.1 )) model.add(Conv2D( 64 ,( 3 , 3 ), padding= 'same' )) model.add(Activation( 'relu' )) model.add(Conv2D( 64 ,( 3 , 3 ))) model.add(Activation( 'relu' )) model.add(MaxPooling2D(pool_size=( 2 , 2 ))) model.add(Dropout( 0.25 )) model.add(Flatten()) model.add(Dense( 512 )) model.add(Activation( 'relu' )) model.add(Dropout( 0.45 )) model.add(Dense(num_classes)) model.add(Activation( 'softmax' )) opt = RMSprop(lr= 0.00005 , decay= 1e-6 ) model.compile(loss= 'categorical_crossentropy' ,optimizer=opt,metrics=[ 'accuracy' ]) model.fit(X, y, batch_size= 28 , epochs= 40 ) model.save( './cnn.h5' ) 動作の確認。作成したモデルをロードしテスト用の写真を判別できるかやってみます! import keras import numpy as np from PIL import Image from keras.models import load_model model = load_model( "./cnn.h5" ) img = Image.open( "./chacha_pic_for_test.jpg" ) img = img.convert( 'RGB' ) img = img.resize(( 64 , 64 )) img = np.asarray(img) img = img / 255.0 prd = model.predict(np.array([img])) prelabel = np.argmax(prd, axis= 1 ) if prelabel == 0 : print ( "チャチャの写真です" ) elif prelabel == 1 : print ( "クロの写真です" ) elif prelabel == 2 : print ( "どちらでもないです" ) 実行するタイミングを決める 餌やり器から餌がなくなった状態を検出し、そのタイミングから60秒さかのぼって上記のコードを実行するようにします。 def is_esa_empty (image_from_safie: bytes ) -> bool : img_binarystream = io.BytesIO(image_from_safie) img_pil = Image.open(img_binarystream) img_numpy = np.asarray(img_pil) img = cv2.cvtColor(img_numpy, cv2.IMREAD_COLOR) # 餌の皿部分以外をマスクする h, w = img.shape[: 2 ] mask = np.zeros((h, w), dtype=np.uint8) cv2.circle(mask, center=( 645 , 340 ), radius= 170 , color= 255 , thickness=- 1 ) img[mask== 0 ] = [ 255 , 255 , 255 ] # mask の値が 0 の画素は黒で塗りつぶす。 # 白黒に変更 img_bw = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二値化 _, thresh = cv2.threshold(img_bw, 106 , 255 , cv2.THRESH_BINARY_INV) # 輪郭検出 contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 輪郭描画 cv2.drawContours(img, contours, - 1 , ( 0 , 255 , 0 ), 2 ) # 餌ひとかけらの面積を400として餌の数を算出 esa_count = int ( sum ([cv2.contourArea(contour, False ) for contour in contours]) / 400 ) # 餌の数が10を切ったらemptyとする return True if esa_count < 10 else False いつでもどこでも見られるようにする 最後に、Safieのカメラと同じようにいつでもどこでも猫の餌の状況を確認できるように、上記のコードで取得できたデータをデータベースに保存し、Webサーバを立ててアクセスできるようにしました。 どちらの猫がいつどのくらい食べているのか簡単に分かり健康管理に役立てています。 また、在宅していないタイミングでも元気に餌を食べてるか確認できるのでとても便利です。 以上、SafieカメラとAPIを利用して、飼い猫の給仕DXをした話でした。皆さんも是非Safie APIを使って日々の生活や業務の効率化に役立ててみてください! 参考文献: AI Academy - Deep Learningで犬・猫を分類してみよう
はじめに CVPR2024は、コンピュータビジョンとパターン認識の分野における最前線の研究成果を集める国際会議です。今年の論文提出数は11532件で、昨年のCVPR2023から26%の増加を記録しました。その中で採択されたのは2719件、採択率は23.6%です。この中から特に優れた24件の論文が Best Paper Award 候補として選出されました。 本記事では、これらのアワード候補となった論文の概要と、その技術的な特徴を紹介します。最先端の技術動向の理解や、今後の研究開発に役立てていただければ幸いです。 はじめに Best Paper Award 候補 Objects as volumes: A stochastic geometry view of opaque solids Repurposing Diffusion-Based Image Generators for Monocular Depth Estimation Comparing the Decision-Making Mechanisms by Transformers and CNNs via Explanation Methods MMMU: A Massive Multi-discipline Multimodal Understanding and Reasoning Benchmark for Expert AGI EventPS: Real-Time Photometric Stereo Using an Event Camera MemSAM: Taming Segment Anything Model for Echocardiography Video Segmentation Correlation-aware Coarse-to-fine MLPs for Deformable Medical Image Registration Producing and Leveraging Online Map Uncertainty in Trajectory Prediction SpiderMatch: 3D Shape Matching with Global Optimality and Geometric Consistency PaSCo: Urban 3D Panoptic Scene Completion with Uncertainty Awareness PlatoNeRF: 3D Reconstruction in Plato’s Cave via Single-View Two-Bounce Lidar Temporally Consistent Unbalanced Optimal Transport for Unsupervised Action Segmentation Rich Human Feedback for Text-to-Image Generation BIOCLIP: A Vision Foundation Model for the Tree of Life Grounding and Enhancing Grid-based Models for Neural Fields NeRF-HuGS: Improved Neural Radiance Fields in Non-static Scenes Using Heuristics-Guided Segmentation Mip-Splatting: Alias-free 3D Gaussian Splatting pixelSplat: 3D Gaussian Splats from Image Pairs for Scalable Generalizable 3D Reconstruction MLP Can Be A Good Transformer Learner Task-Driven Wavelets using Constrained Empirical Risk Minimization Image Processing GNN: Breaking Rigidity in Super-Resolution Generative Image Dynamics Analyzing and Improving the Training Dynamics of Diffusion Models EGTR: Extracting Graph from Transformer for Scene Graph Generation おわりに Best Paper Award 候補 Award Candidate 論文の概要をセッション順に記載します。 Objects as volumes: A stochastic geometry view of opaque solids 著者:Bailey Miller · Hanyu Chen · Alice Lai · Ioannis Gkioulekas セッション:Orals 1B: Vision and Graphics 著者らは、物理法則に基づいて不透明物体をボリュームモデルで表現する理論を提案しています。まず、ランダムな指示関数を用いて不透明物体を確率的に表現し、光が物体を通過する際に減衰が指数関数的に変化する条件から、光の減衰係数を定式化します。この理論の一般化することで、等方性および異方性の散乱や、不透明物体の陰関数表現に対応できることを示しています。この手法によって、優れた3次元再構成を行うことができます。 本論文で提案している理論の概要を示しています。減衰係数 を密度(density)と投影面積(projected area)の積として表します。密度は、平均された陰関数(mean implicit)から空隙率(vacancy)を通して計算することができます。また、投影面積は、異方性パラメータを用いて計算します。 Repurposing Diffusion-Based Image Generators for Monocular Depth Estimation 著者:Bingxin Ke · Anton Obukhov · Shengyu Huang · Nando Metzger · Rodrigo Caye Daudt · Konrad Schindler セッション:Orals 3A: 3D from Single View 単眼深度推定は一般に、学習データに未知のデータに対して難易度が高い傾向にあります。そこで著者らは、豊富な事前知識をもつ既存の生成拡散モデルを利用した単眼深度推定手法を提案しています。提案法は Stable Diffusion から派生した手法で、合成データ(synthetic data)でファインチューニングされています。またゼロショット転移を用いることで未知のデータにも強く、単眼深度推定タスクでSOTAな結果を出しています。 ファインチューニングの過程は以下のステップで行われます。事前訓練されたStable Diffusionを利用し、画像 と深度 を潜在空間 と に変換します。 にノイズを付加し後、 と連結し、これらを入力に元の深度の潜在空間を復元するようにU-Netをファインチューニングします。損失関数は、元のノイズ とモデルが予測したノイズ の間の二乗誤差となっています。 Comparing the Decision-Making Mechanisms by Transformers and CNNs via Explanation Methods 著者:Mingqi Jiang · Saeed Khorram · Li Fuxin セッション: Orals 3B: Vision, Language, and Reasoning 著者らは、視覚認識バックボーンモデルの意思決定を分析するために、サブ説明カウント法(sub-explanation counting) と クロステスティング(cross-testing)の2つの方法を提案しています。これにより、ネットワークが持つ合成性(compositionality)と分離性(disjunctivism)という2つの特性の違いが明らかになります。 収穫機の画像をサブ説明カウント法で分析しています。合成的なモデルであるConvNeXtやトランスフォーマーは、画像の複数の部分を一緒に見て意思決定を行います。したがって、右上のツリーのように入力画像の一部がマスクされた場合、推論されるクラスラベルの信頼度はわずかに減少します。一方で、分離的なモデルであるCNNや蒸留トランスフォーマーは、少ない部分から判断するため、右下の画像のように大きな信頼度を出力します。この分析は、各モデルがどのように画像の部分を利用しているかを明らかにします。 MMMU: A Massive Multi-discipline Multimodal Understanding and Reasoning Benchmark for Expert AGI 著者:Xiang Yue · Yuansheng Ni · Kai Zhang · Tianyu Zheng · Ruoqi Liu · Ge Zhang · Samuel Stevens · Dongfu Jiang · Weiming Ren · Yuxuan Sun · Cong Wei · Botao Yu · Ruibin Yuan · Renliang Sun · Ming Yin · Boyuan Zheng · Zhenzhu Yang · Yibo Liu · Wenhao Huang · Huan Sun · Yu Su · Wenhu Chen セッション:Orals 3B: Vision, Language, and Reasoning 著者らは、マルチモーダルなモデルを評価するための、大学レベルの知識と熟考を要する大規模な複数分野のタスクからなる新しいベンチマークを提案しています。MMMUは、既存のベンチマークとは異なり特定分野の知識を必要とする高度な認識と推論に焦点を当てており、14のオープンソースLLMや、GPT-4V (ision)、Geminiの評価結果からも、その難しさが示されています。MMMUは、次世代の専門的な人工知能モデルの構築を促進することを期待しています。 MMMUには大学の試験、クイズ、教科書から厳選された11,500の多様な質問が含まれており、アート&デザイン、ビジネス、科学、健康&医療、人文社会科学、技術&工学の6つの主要分野をカバーしています。チャート、図、地図、表、楽譜、化学構造など、異種性の高い30種類の画像も含まれています。MMMUのテストでは、GPT-4VとGemini Ultraがそれぞれ56%と59%の精度しか達成しておらず、これらのモデルにはまだ改良の余地が大きいことを示しています。 EventPS: Real-Time Photometric Stereo Using an Event Camera 著者:Bohan Yu · Jieji Ren · Jin Han · Feishi Wang · Jinxiu Liang · Boxin Shi セッション:Orals 3C: Medical and Physics-Based Vision 本記事を執筆している 2024/6/4 時点で論文が未公開のため、関連情報を記載します。 タイトルから、本提案はイベントカメラを用いてリアルタイムにPhotometric Stereo(照度差ステレオ)を行う手法と考えられます。イベントカメラとはマイクロ秒単位の時間分解能を持つカメラデバイスです。Photometric Stereo は照明方向を変えながら撮影を行い、画素ごとの明るさの変化から表面の法線方向を推定する手法です。 著者らが公開しているデモ動画では、イベントカメラの撮影結果、法線の推定結果、測定するオブジェクトなどが示されています。 MemSAM: Taming Segment Anything Model for Echocardiography Video Segmentation 著者:Xiaolong Deng · Huisi Wu · Runhao Zeng · Jing Qin セッション:Orals 3C: Medical and Physics-Based Vision 著者らは、SAM(Segment Anything Model)を医療ビデオに適用することで、超音波ビデオセグメンテーションの課題に取り組んでいます。提案モデルは、空間と時間情報を組み合わせた時空メモリの使用と、予測されたマスクを活用してメモリの品質を向上させるメモリ強化メカニズムを提案しています。これにより、セグメンテーションの精度と一貫性が向上しています。 提案法は主にSAMとメモリの2つのコンポーネントで構成されています。画像エンコーダは入力画像を、プロンプトエンコーダは外部プロンプトをそれぞれ埋め込みにエンコードし、マスクデコーダが統合してセグメンテーションマスクを予測します。画像埋め込みはメモリ特徴空間に保存され、デコード時にメモリからメモリプロンプトが読み出され利用されます。メモリは強化とエンコーダを通じて更新されます。 Correlation-aware Coarse-to-fine MLPs for Deformable Medical Image Registration 著者:Mingyuan Meng · Dagan Feng · Lei Bi · Jinman Kim セッション:Orals 3C: Medical and Physics-Based Vision 近年の医用画像位置合わせでは、変形の大きい脳や心臓のMRI画像に対応するため、Transformer ベースの手法が注目されていますが、計算コストが高く高解像度の特徴を扱えない課題があります。著者らは、MLP(多層パーセプトロン)ベースのネットワークを提案しています。提案法は、複数の解像度の特徴マップを利用して、粗から密(coarse-to-file)へ段階的に位置合わせ(registration)を行います。この手法は、高い計算効率を保ちながら、変形の大きい医用画像に対してもリアルタイムに優れた性能を発揮します。 提案法は、CNNベースの階層的な特徴抽出エンコーダと、CMW-MLPブロックを用いたデコーダから構成されます。初期ステップでは、エンコーダから得られた最も粗い解像度の特徴マップ( と )を使用し、初期の変形フィールド を生成します。以降のステップでは、前のステップで得られた変形フィールドを使用して、次の解像度の特徴マップを変形し、再度CMW-MLPブロックに入力します。このプロセスを繰り返すことで、段階的に詳細な位置合わせを実現します。 Producing and Leveraging Online Map Uncertainty in Trajectory Prediction 著者:Xunjiang Gu · Guanyu Song · Igor Gilitschenski · Marco Pavone · Boris Ivanovic セッション:Orals 4A: Autonomous Navigation and Egocentric Vision 著者らは、Autonomous Vehicle(AV)の軌道予測において、オンラインマップの不確実性を推定する手法を提案しています。提案法は、不確実性を考慮することで軌道予測の学習の収束が最大で50%速くなり、軌道予測性能が最大で15%向上します。 AVで取得したカメラやLiDARなどのセンサーデータは、BEV(Bird’s Eye View)特徴量にエンコードされます。特徴量を用いて、分類モデルはマップ要素(車線、道路境界など)を予想します。回帰モデルは、ポリラインやポリゴンなどのマップ要素の頂点と、ラプラス分布で表される不確実性を予測します。生成された確率的なオンラインマップは、GNN(Graph Neural Network)やTransformer などの確率的エンコーダに入力され、軌道予測が行われます。 SpiderMatch: 3D Shape Matching with Global Optimality and Geometric Consistency 著者:Paul Roetzer · Florian Bernard セッション:Orals 4B: 3D Vision 著者らは3次元形状マッチングのための新しい経路ベースの手法を提案しています。様々な種類のマッチング問題を解くための一般的なアプローチとして、直積空間上での最短経路を求める方法があります。これは多項式時間で大域的に最適な解を計算することができますが、3次元形状マッチングへの自然な一般化は難しいことが広く知られています。著者らはこれらの課題を解決し、効率的に大域最適な解を得る手法を提案しています。 提案法は、3次元形状を3次元形状表面をなぞる長い自己交差曲線(SpiderCurve)として表現します。その結果、密なジオメトリに対しても、幾何的に矛盾のない形状マッチングができることを示しています。実行時間の検証では、メッシュ数が増えても現実的な時間でスケールしています。 PaSCo: Urban 3D Panoptic Scene Completion with Uncertainty Awareness 著者:Anh-Quan Cao · Angela Dai · Raoul de Charette セッション:Orals 4B: 3D Vision 著者らは、不完全な3次元点群からジオメトリ、セマンティック、インスタンスを推定するPanoptic Scene Completion (PSC)という新しいタスクを提案しています。提案手法はマルチスケールスパース生成デコーダーを利用し、さらにMulti-Input Multi-Outputを導入することで、不確実度を推定できるようにしています。 提案するネットワークは、同一のアーキテクチャを持つ複数のサブネットが含まれます。各サブネットは一部のパラメータを除いて共通のパラメータを使用します。1つの点群データからデータ拡張を行い、複数の入力データを生成し、それぞれの入力データをサブネットで演算することで、入力データと同数のPSCを出力します。複数の予測をアンサンブルすることで、不確実性の推定と予測精度の向上を行います。 PlatoNeRF: 3D Reconstruction in Plato’s Cave via Single-View Two-Bounce Lidar 著者:Tzofi Klinghoffer · Xiaoyu Xiang · Siddharth Somasundaram · Yuchen Fan · Christian Richardt · Ramesh Raskar · Rakesh Ranjan セッション:Orals 4B: 3D Vision 著者らは、単一視点で3D再構築を行う手法を提案しています。従来の単一視点の3D再構築に使われていたNeRF(Neural Radiance Fields)は、物理的に正確ではない事前情報に依存しています。本手法は、LiDAR(Light Detection and Ranging)データとNeRFを用いて、複雑なシーンの3Dモデルを生成します。本手法は、2回までの反射光を測定する技術を活用し、従来のNeRF手法と比較して、環境光や反射光の事前データに依存せずに再構築精度を向上させています。 シーンの各点をパルスレーザーで照射し、反射した光の飛行時間を測定します。このデータを用いてNeRFを訓練し、一次反射および二次反射光の経路をモデリングします。生成される3Dジオメトリは、密度推定に基づいて可視部分と隠された部分の両方を高精度に再構築します。 Temporally Consistent Unbalanced Optimal Transport for Unsupervised Action Segmentation 著者:Ming Xu · Stephen Gould セッション:Orals 4C: Action and Motion 著者らは、長時間動画のアクションセグメンテーションタスクに対して、ASOT(Action Segmentation Optimal Transport)という手法を提案しています。この手法は、従来の方法が抱えるアクション順序の仮定が必要であることや、アクションの時間的一貫性の欠如といった課題を解決しています。また、GPUを用いた数値解法アルゴリズムも提案されています。 ビデオフレームとアクションの間のコスト行列 には大きなノイズが含まれています。提案法(ASOT)は、アクションカテゴリが変更されるコスト と、隣接するビデオフレームでアクションが変更されるコスト を加味することで、最適な割り当て を予測します。提案法は最適輸送問題の一種である、GW(Gromov-Wasserstein)問題に基づいて構築されています。 Rich Human Feedback for Text-to-Image Generation 著者:Youwei Liang · Junfeng He · Gang Li · Peizhao Li · Arseniy Klimovskiy · Nicholas Carolan · Jiao Sun · Jordi Pont-Tuset · Sarah Young · Feng Yang · Junjie Ke · Krishnamurthy Dvijotham · Katherine Collins · Yiwen Luo · Yang Li · Kai Kohlhoff · Deepak Ramachandran · Vidhya Navalpakkam セッション:Orals 5A: Datasets and Evaluation 最近のText-to-Imageモデルは進歩していますが、生成画像には不自然さ、テキストとの不一致などの問題があります。これに対し、本研究では、生成画像の問題のある領域を詳細にアノテーションし、18Kの人間のフィードバックデータ(RichHF-18K)を収集しました。このデータセットから、フィードバックを予測するマルチモーダル変換器を訓練することで、ファインチューニングに使用するための高品質な画像を選別したり、生成画像中の問題のある領域のマスクを作成することに活用できます。 フィードバックモデルのアーキテクチャは、Text-to-Imageモデルからの生成画像とそのテキストプロンプトを入力とします。ViT(ビジョントランスフォーマー)から出力される画像トークンと、Text-embedモジュールから出力されるテキストトークンに自己注意機構を適用し、画像とテキスト情報を融合します。画像トークンは特徴マップに再構成され、不自然な箇所のヒートマップと品質スコアを出力します。テキストトークンは、画像トークンと共にトランスフォーマーデコーダに送られ、入力されたプロンプトと生成結果のどこがずれていたかを説明する文章を出力します。 BIOCLIP: A Vision Foundation Model for the Tree of Life 著者:Samuel Stevens · Jiaman Wu · Matthew Thompson · Elizabeth Campolongo · Chan Hee Song · David Carlyn · Li Dong · Wasila Dahdul · Charles Stewart · Tanya Berger-Wolf · Wei-Lun Chao · Yu Su セッション:Orals 5A: Datasets and Evaluation 著者らは、生物の画像から特徴量を抽出するための大規模なマルチモーダルモデルBioCLIPを提案しています。「TreeOfLife-10M」と呼ばれる45万種以上の生物を含む1000万枚以上の画像データセットを構築し、そのデータを用いてCLIPスタイルのモデルを学習しています。提案法により、10種類のデータセットに対する5ショット分類の平均精度が、51.5%(従来法:CLIP)から 68.8%に向上しました。 提案法は、テキストと画像の対照学習を行います。テキストをエンコードする際に、着目する階層よりも上位の分類名を含めることで、生物分類の階層の特徴と、画像の特徴の関連を学習します。対照学習では、同種のテキストの埋め込みと画像の埋め込みは近くなるように、異種のペアは遠くなるような目的関数を用います。これにより、提案モデルに系統樹の階層構造が反映されます。 Grounding and Enhancing Grid-based Models for Neural Fields 著者:Zelin Zhao · FENGLEI FAN · Wenlong Liao · Junchi Yan セッション:Orals 5B: 3D from Multiview and Sensors 著者らは、ニューラルフィールドの表現に一般的に利用されるグリッドベースのモデルを分析するための理論的枠組みを提案します。これらのモデルの近似および汎化挙動は、Grid Tangent Kernelsと呼ばれるグリッドモデル固有の量よって決定されることを示します。この枠組みを用いて、MulFAGridという新しいモデルを提案し、2D画像フィッティング、3D符号付き距離場(SDF)の再構築などのタスクで優れた性能が得られることを示しました。 上側の処理経路は、座標 をインデックス関数に入力し、位置に対してノードの集合を返し、ノード に関連付けられた特徴(重み)ベクトルを計算します。下側の経路は、多重化フィルターを用いてフーリエ特徴量を処理した後、正規化レイヤーを用いてカーネル関数を計算します。最後に、特徴ベクトルとカーネル関数を要素ごとに乗算して求める関数の近似値を出力します。 NeRF-HuGS: Improved Neural Radiance Fields in Non-static Scenes Using Heuristics-Guided Segmentation 著者:Jiahao Chen · Yipeng Qin · Lingjie Liu · Jiangbo Lu · Guanbin Li セッション:Orals 5B: 3D from Multiview and Sensors NeRFを用いて3Dモデルを構築する際、動的なシーンでは物体の移動や影の変化により画像間の一貫性が失われ、適用が難しいという問題がありました。この問題を解決するために、ヒューリスティックを用いて動的シーンから静的な3Dモデルを構築する手法を提案しています。合成データと実データを用いた実験により、提案手法は従来手法を上回る性能を示しています。 最下部に示されている提案法は2種類のヒューリスティックを用います。SfM(Structure-from-Motion)ヒューリスティックは、静的な物体において特徴点のマッチング頻度が高いことを利用します。カラー残差ヒューリスティックは、NeRFモデルのカラー残差が静的な物体で低いことを利用します。これらの情報をプロンプトとしてSAM(Segment Anything Model)に入力することで正確な静的マップが得られます。 Mip-Splatting: Alias-free 3D Gaussian Splatting 著者:Zehao Yu, Anpei Chen, Binbin Huang, Torsten Sattler, Andreas Geiger セッション:Orals 5B: 3D from Multiview and Sensors 著者らは、3D Gaussian Splatting(3DGS)技術において、様々なスケールで高品質に物体の3D画像をレンダリングするMip-Splattingという手法を提案しています。従来法は、ズームインズームアウトを行う際にノイズや歪みなどのエイリアスや、不自然なエッジのようなアーティファクトが生じる問題がありました。提案法では、高周波成分の処理やレンダリング時のフィルタの変更によってこれらの問題を解決しています。 3DGSは3Dシーンを3D ガウシアンで表現し、フィルタ処理を行い画像をレンダリングします。標本化定理に基づく適切なレンダリングのサンプリングレートを選択しない場合、高周波成分が正しく処理されず、エイリアスが発生します。また、焦点距離やカメラ距離の変化によって3D ガウシアンのサイズが過度に大きくなる時には、ぼやけや歪みが発生します。提案法では、3D smoothing filterを用いて3Dガウシアンのサイズを制約することで、高周波成分に含まれるアーティファクトを抑制します。また、レンダリング時に適切なフィルタサイズを選択することでエイリアスを抑制します。 pixelSplat: 3D Gaussian Splats from Image Pairs for Scalable Generalizable 3D Reconstruction 著者:David Charatan · Sizhe Lester Li · Andrea Tagliasacchi · Vincent Sitzmann セッション:Orals 5B: 3D from Multiview and Sensors 著者らは、画像のペア群から3次元輝度場を再構成するフィードフォワードモデルを提案しています。提案法は、3次元輝度場の高速な推論と、リアルタイムでメモリ効率の良いレンダリングを特徴としています。実世界のRealEstate10kデータセットとACIDデータセットにおいて、解釈可能で編集可能な3次元輝度場を再構成しながら、最先端のライトフィールド変換器を凌駕し、レンダリングを2.5倍高速化します。 提案モデルは3次元輝度場を3次元ガウスプリミティブによってパラメータ化します。入力画像の画素の特徴量 ( はピクセル座標) から、ガウスプリミティブのパラメータ を生成し、ガウススプラッティングによりレンダリングすることで2次元画像を求めます。 は入力画像をResNet-50やDINO ViT-B/8でエンコードしたものです。ガウスプリミティブのパラメータは が平均、 が共分散、 が透明度、 が球面調和関数の係数です。 と はNNモデル により予測され、 と は が予測したピクセルごとの深さの離散確率分布から間接的に予測されます。 MLP Can Be A Good Transformer Learner 著者:Sihao Lin · Pumeng Lyu · Dongrui Liu · Tao Tang · Xiaodan Liang · Andy Song · Xiaojun Chang セッション:Orals 5C: Low-Shot, Self-Supervised, SemiSupervised Learning セルフアテンションはトランスフォーマーの重要な要素ですが、計算リソースを多く必要とすることが課題です。著者らは、この問題を改善する手法を提案しています。具体的には、エントロピーを利用して重要性の低いアテンション層を特定し、レイヤーを軽量化することでメモリーの負荷を削減します。また、重要性の低いアテンション層の知識をその後段のMLP(Multi-Layer Perceptron)層に移すための学習手法も提案しています。 上の図は、DeiT-Bの各ブロックのエントロピーを測定し、入力に近い層ではアテンション層とそれに続くMLPレイヤでは同程度にエントロピーが低いことを示しています。次に、それを恒等写像(Identical Mapping)に置き換えることでレイヤーの修正を行います。結果としてDeiT-Bのパラメタを13.7%削減し、同一メモリ量で処理できる画像の量が20.5%増加しています。 Task-Driven Wavelets using Constrained Empirical Risk Minimization 著者:Eric Marcus · Ray Sheombarsing · Jan-Jakob Sonke · Jonas Teuwen セッション:Orals 6A: Low-level Vision and Remote Sensing 従来のディープニューラルネットワークでは、損失関数を追加することでソフト制約を課すことが一般的です。著者らは一部のパラメータに厳密な制約を設ける新しいフレームワークCERMを提案しています。具体的には、ニューラルネットワークの重みに特定の制約を課すことで、モデルの特性を制御します。これにより、異なるスケールの情報やノイズの影響を効率的に低減し、医療画像のタスクで優れた性能を示しています。 提案法は、「制約付き経験リスク最小化」(CERM)を用いて、畳み込みフィルターをウェーブレットに制約します。まず、ウェーブレットを特徴付ける方程式を導出します。導出された方程式から、リファインメントマスクと呼ばれるウェーブレットを一意に定める多項式を得ます。畳み込みフィルターに対して、リファインメントマスクの係数を制約として課します。これらの制約の下で、ネットワークを学習します。提案法を用いることで、特定のパターンを効果的に捉えることができます。 Image Processing GNN: Breaking Rigidity in Super-Resolution 著者:Yuchuan Tian · Hanting Chen · Chao Xu · Yunhe Wang セッション:Orals 6A: Low-level Vision and Remote Sensing 著者らは、超解像(Super-Resolution: SR)タスクにおいて、Graph Neural Network(GNN)を利用した手法(Image Processing GNN: IPG)を提案しています。提案法では、画素をノードとする2種類のグラフを構築します。ローカルグラフは、近隣の画素でノードを接続することで細部のディテールやテクスチャの復元を行います。一方、グローバルグラフは、画像全体で画素を接続することで、大局的なパターンを捉えます。また、提案法は、画像の単純で均一な部分と、細かいディテールが含まれている部分で、ノード間の接続数を調整し、効率的な情報集約を行います。 Urban100データセットを用いた検証結果では、提案手法(IPG)は、Bicubic補間や他の超解像手法よりも、元の高解像度画像(HQ)をより正確に復元しています。 Generative Image Dynamics 著者:Zhengqi Li · Richard Tucker · Noah Snavely · Aleksander Holynski セッション:Orals 6B: Image & Video Synthesis 著者らは、静止画像から自然でリアルな動きを持つ映像を生成する Generative Image Dynamics という手法を提案しています。提案法は、木々や花などの自然な動きを含む動画を教師データとして拡散モデルを学習します。この手法により、静止画像がインタラクティブで魅力的なコンテンツに変換されることが期待されます。 静止画像とノイズのある潜在変数をデノイズネットワーク に入力すると、各画素の動きを周波数領域で表現したスペクトルボリュームを予測します。 は2D 空間層とアテンション層が交互に配置されており、これにより特徴量マップ内のノイズを減少させながら、空間的な関係と周波数間の関係を統合します。スペクトルボリュームを逆フーリエ変換することで、各画素の変位を求めることができます。 Analyzing and Improving the Training Dynamics of Diffusion Models 著者:Tero Karras · Miika Aittala · Jaakko Lehtinen · Janne Hellsten · Timo Aila · Samuli Laine セッション:Orals 6B: Image & Video Synthesis 著者らは、一般的なADM (Ablated Diffusion Model) アーキテクチャにおいて、高レベルの構造を変更せずに拡散モデルの学習時に問題となる確率的な挙動をする損失関数等に対処するため、ネットワーク層を再設計しています。具体的には、学習過程でアクティベーション出力および重みの大きさを一定の範囲内に維持するようにしています。これにより、ImageNet-512の合成において、生成画像と実画像の統計的類似度を評価する指標であるFIDが2.41から1.81に改善されています。 U-NetベースのADMでは、エンコーダはスキップ接続を用いてデコーダに接続され、埋め込みがノイズレベルとクラスラベルを条件付けます。ADMはResNetをベースとしていますが、メインパスでは正規化が無いため、アクティベーション出力の増加を抑えられません。提案手法では、メインパスでのアクティベーション出力を一定の範囲に維持するために、MPブロック等を導入します。 EGTR: Extracting Graph from Transformer for Scene Graph Generation 著者:Jinbae Im · JeongYeon Nam · Nokyung Park · Hyungmin Lee · Seunghyun Park セッション:Orals 6C: Multi-Modal Learning シーングラフ生成(Scene Graph Generation: SGG)は、画像内のオブジェクトとそれらの間の関係を構造化するタスクです。SGGの従来法の多くはオブジェクト検出と関係予測を別々のステージで行うためモデル構造が複雑になり、計算コストが高くなることや、一貫性のある学習が難しいといった課題があります。提案法ではセルフアテンションを用いた軽量なワンステージモデルでこれらの問題を克服しています。 提案法は、入力画像をDETRエンコーダでエンコードし、続いてDETRデコーダーでオブジェクトを検出します。同時に、DETRデコーダーのアテンション層ごとの特徴量を用いることで、オブジェクト同士の関係性表現(Relation Representation) を計算します。関係性表現は、アテンション層ごとの重みを用いて、加重和がなされます。Relation Headは、関係性のラベルやその強度を推論し関係性グラフ(Relation Graph)を計算します。最後に検出したオブジェクトと関係性グラフを組み合わせてシーングラフを作成します。 おわりに 本記事では、CVPR2024のBest Paper Award候補となった論文を紹介し、コンピュータビジョンとパターン認識の分野における重要な進展を示しました。いずれの論文も興味深く、面白い内容でした。 現在、私の所属するAI Visionグループは9人で構成されています。今回の論文紹介は、各メンバーが分担して取り組みました。私たちのグループでは、最先端の研究を取り入れたAIの開発を行っています。興味のある方は募集要項をご覧ください。
はじめに 第 2 開発部モバイルグループで iOS テックリードをしている鞆です。 2024 年 5 月 23 日に開催された Findy さん主催のイベント TechBrew in 東京 ~モバイルアプリの技術的負債に向き合う~ にて、発表させていただきました。 findy.connpass.com 当日の様子を含めてイベントレポート的な形でご紹介できればと思います! はじめに 会場 オープニング 発表内容 Bitkeyのモバイルアプリを進化させるための歩き方 @arasan01_me モバイルアプリの技術的負債に全社を挙げて取り組む考え方 @mikity01985 パッケージ管理でモバイル開発を安全に進める @entaku_0818 GitHub Copilotで技術的負債に挑んでみる 懇親会 会場 Findy さんが 2024 年 4 月に大崎駅近くにオフィス移転されたとのことで、この新オフィスに併設されたイベントスペースが会場でした。 findy.co.jp 大崎駅からのアクセス についても画像付きで紹介されており、イベントスペース自体もオフィス受付のすぐ横にあるため、迷うことはなさそうです。 イベントスペース自体も 100 名収容可能ということで余裕があり、クロークやコントロールルームまである非常に本格的な作りでした。 開催時間が遅めということもあってか照明についても暗めとなっており、非常に雰囲気のある状態に仕上がっていました。 受付向かって右側すぐがイベントスペース入り口 スクリーン横には音響管理用のコントロールルーム 今回のイベントでは利用しませんでしたがカウンタースペースもありました オープニング Findy さんのイベントではオープニングで必ず乾杯!から始まるとのことなので、まずは全員で乾杯してイベントの開始となりました。 またここで、 Findy さんが作ったという IPA (クラフトビールのほうです) の紹介もありました。 挑戦 IPA 開発生産性 IPA Findy さん主催のイベントで付いてくる「TechBrew」という名称が正直なところ謎だったのですが、文字通り本当に醸していました。 挑戦 IPA と開発生産性 IPA の 2 種類があり、オフラインイベントで振舞われているとのことでしたが、限定本数となるため気になる方はお早めにとのことでした! 発表内容 私のほうからは、セーフィーで開発している Safie Viewer の iOS 版アプリ に関する技術的負債への取り組みに関して、ここ 1 年ほどで実施、検討している内容について事例紹介といった形で発表させていただきました。 speakerdeck.com 10 分間の発表に詰め込みすぎてしまった感があるのが反省です。 また、今回のテーマである「モバイルアプリの技術的負債に向き合う」についても、セーフィーのモバイルグループが置かれている現状にまさにマッチしたものとなっており、発表された内容それぞれが非常にためになりました。 Bitkeyのモバイルアプリを進化させるための歩き方 @arasan01_me speakerdeck.com Bitkey さんの workhub アプリに関して、専業エンジニアがいなかった React Native 時代から現在までの歴史に関して、技術選定の方針やネイティブ化への移行対応等の具体例を交えた発表でした。 TCA の採用についても一度見送ってから再度検討して採用する等、外部環境の変化に対して素早く対応していけるというところに開発チームの強さを感じました。 「技術選定の正解は?」という問に対して、「自分たち (開発チーム) がいいと思う方法全てが正解」という部分が非常に印象的で、セーフィーのモバイルグループでも技術選定で迷うことが非常に多いのですが、これに対して勇気づけられるような発表でした。 モバイルアプリの技術的負債に全社を挙げて取り組む考え方 @mikity01985 speakerdeck.com モバイルアプリの技術的負債を組織内でどう扱っていくかという点に関して、 EM ならではの高い視座で俯瞰的に見ていくような発表で、セーフィーの現状にも当てはまる部分が多かったです。 特に「技術的負債の中でもチームに閉じてしまうものは外部への説明に困る」といった指摘は過去を振り返ってみてもまさにその通りで、外部とのコミュニケーションに関する方法論等は非常に参考になりました。 モバイルアプリ開発における Four Key Metrics の難しさ等についても腹落ちする部分が多く、個人的には共感できる部分が多い発表でした。 パッケージ管理でモバイル開発を安全に進める @entaku_0818 speakerdeck.com 比較的歴史のある Voicy さんのアプリにおける過度な共通化という問題とそれに対する回答としての責務分割のお話しでした。 Swift Package Manager を利用してモジュール / パッケージとしてカプセル化することによって、責務としてのインターフェイスを明示化、また依存性を制限することでシステムとして安全な状態を保っているとのことで、昨今流行りの Swift Package Manager ベースのマルチモジュール化の利点として説得力のある内容だったかと思います。。 Safie Viewer の iOS 版でもこの辺りはやろうと思っていて進んでいない部分だったので、先行事例として非常に参考になる発表でした。 GitHub Copilotで技術的負債に挑んでみる www.docswell.com 意外と貴重なモバイル開発領域における GitHub Copilot 利用に関する発表でした。 特に iOS 開発ですと、 Xcode が公式に対応しておらず 3rd party ツールに依存してしまうということもあり、セーフィー社内や個人的にも GitHub Copilot を有効活用しきれていないという感触はあったのですが、やはり同様の辛みがあるのだなという納得感がありました。 また実際にデモで見せていただいた GitHub Copilot Chat との連携についてはほぼ利用していなかったため、 code suggestion 以外の使い方として一つ知見が増えました。 懇親会 一通り発表が終了した後は立食形式での懇親会となりました。 IPA も十分に用意いただいてました! 完全オフラインイベントということもあり、比較的小規模な集まりとはなったのですが、逆に参加者間でコミュニケーションが取りやすいといった面がありました。 モバイルアプリに関するイベントだったのですが、他職種のエンジニアの方も参加されており、意外なところでネットワークの広がりもできました! まとめ 今回、 Findy さん側からお話しをいただいての発表、イベント参加だったのですがテーマ的にも得ることが多いイベントでした。 完全オフラインイベントへの参加というのもコロナ禍以降で久しぶりだったのですが、やはりオフラインならではの良さ、密度の高さというのを感じました。 最後になりますがイベント企画、ならびに会場を提供いただいた Findy さん、誠にありがとうございました!
はじめに Data-centric AI Cleanlab 物体検出データセットのクレンジング データセット作成 物体検出モデルの学習 予測と正解データの生成 Cleanlabによる品質スコア算出 アノテーション修正 Cleanlabをフル活用するためのTips 同じ正解データや予測を複数回マッチングさせないようにする 閾値をモデルやデータセットに応じて適切に設定する スコアが1.0になっている画像でも一通り確認する まとめ はじめに セーフィー株式会社  で画像認識AIの開発エンジニアをしている水野です。 現在、 AI-App 人数カウント で利用される物体検出モデルの精度改善に取り組んでいます。物体検出モデルの精度改善方法としては様々な手法が考えられますが、近年はData-centric AIというアプローチが注目されています。そこで本稿では、Data-centric AIで物体検出モデルの精度を改善する一手法として Cleanlab を用いたデータセットのクレンジング方法について紹介します。 Data-centric AI Data-centric AIとは、2021年3月に Andrew Ng氏の講演「From Model-centric to Data-centric AI」 で提唱された概念で、AIモデルを固定してデータセットを改善することでモデルの精度改善を実現するアプローチのことです。これは従来主流であったデータセットを固定してAIモデルを改善するModel-centric AIとは対極にある考え方と言えそうです。ただしこれらはどちらのアプローチが優れているという話ではなく、実際の開発では両方の観点で改善していく必要があります。 データセットを改善したい場合、どのようなアプローチが考えられるでしょうか。単純に考えると画像およびアノテーションを人の目で確認し、問題のあるデータを抽出して修正するという方法が挙げられそうですが、この方法にはいくつかの課題があります。 現実のデータセットでは画像枚数が数千枚~数万枚という規模であり、人の目で一通り確認しミス無く修正しきるのはかなり時間が掛かる データセットがクリーンであることを客観的に示すことが難しい このようなデータセット改善の作業で発生する課題を解決するために Cleanlab のようなData-centric AI向けのライブラリが活用できます。 Cleanlab Cleanlab は confident learning の考え方をベースとしてデータセットのノイズを抽出するためのライブラリです。Cleanlabを使用することでデータセット内に存在するアノテーションの問題を自動的に抽出することができます。また画像ごとの品質スコアが出力されるので、アノテーションの品質がどの程度なのか定量的に示すことが可能です。従来は画像識別タスクのみサポートされていたのですが、 2023年9月のリリース から物体検出タスクに対応しています。 Cleanlabを用いた基本的なデータクレンジングの流れは下記の通りです。 データセット作成: データセットをtrainとvalidationに分けます AIモデルの学習: trainデータでモデルを作成します 予測生成: 学習したモデルを用いてvalidationデータに対する予測を生成します Cleanlabによる品質スコア算出: validationデータに対する予測と正解データを比較して画像ごとの品質スコアを算出します アノテーション修正: 品質スコアの低い画像のアノテーションを修正します お気づきの方もいるかもしれませんが、このままではvalidationデータに含まれる画像の品質しか判定できません。そこで実際にはK-fold分割を用いたOut-of-fold予測を生成し、全データに対する品質スコアを算出します。5-fold分割時の処理を図で書くと以下のようになります。5つに分割したデータで生成した各モデルで予測を生成し、結合することで実質的に全データに対する予測を生成することが出来ます。本稿では簡単化のために技術的な背景や詳細の説明は省略しますが、ご興味のある方は 公式の解説ページ をご参照ください。 5-fold分割によるOut-of-fold予測生成の流れ ( 公式の解説ページ の図をベースに作成) 物体検出データセットのクレンジング ここからは物体検出データセットに対する実際のクレンジングの方法を説明します。 物体検出モデルとして YOLOv8 、データセットとして COCO を使用します。 データセット作成 クレンジングしたいデータセットをtrainとvalidationに分割します。既に説明した通り、データセット全体をクレンジングする場合はOut-of-fold予測を生成する必要がありますので、COCOデータセット全体をクレンジングしたい場合はCOCOから提供されているtrainデータとvalidationデータを混ぜた上でK-fold分割を実施します。 K-fold分割では scikit-learn がよく利用されます。データセットの特性に応じて適切な分割方法を選択する必要がありますので、 公式ページ を参照し各自の状況に合わせた手法を選択してください。また分割数をいくつにすべきかという問題ですが、理想的には多い方が良いのですが分割数が増えると学習に必要な時間が膨大になってしまうため、実用上は5-fold分割がお勧めです。 Kaggle のコンペ等でも5-fold分割はよく利用されますし、trainとvalidationの割合としても4:1というのはバランスが良いと思います。 5-fold分割したデータセットの構成例を以下に示します。YOLO形式のラベルフォーマット等は 公式の解説ページ をご参照ください。またCOCOのフルデータセットの場合、5-fold合計で100GB(1foldあたり20GB)のディスク容量を必要としますのでディスクの空き容量にご注意ください。 $ tree datasets/ datasets/ ├── coco_fold1 │   ├── images │   │   ├── train │   │   │   └── xxx.jpg │   │   └── val │   │   └── yyy.jpg │   └── labels │   ├── train │   │   └── xxx.txt │   └── val │   └── yyy.txt ├── coco_fold2 ├── coco_fold3 ├── coco_fold4 └── coco_fold5 各foldに対応するyamlファイルの作成も必要です。fold1の例を以下に示します。 # Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..] path : ../datasets/coco_fold1 # dataset root dir train : images/train # train images (relative to 'path') val : images/val # val images (relative to 'path') test : # test images (optional) # Classes (80 COCO classes) names : 0 : person 1 : bicycle 2 : car # ... 77 : teddy bear 78 : hair drier 79 : toothbrush 物体検出モデルの学習 K-fold分割で生成した各foldで学習を実行しfold数分のモデルを生成します。まずは学習を実行するために公式のDockerコンテナを立ち上げます。 $ docker run --ipc=host -it --gpus all -v <DATASET_ROOT>:/usr/src/datasets ultralytics/ultralytics:latest 次にさきほど作成した各foldのデータセットごとに学習コマンドを実行します。yoloコマンドで学習を実行する場合のBashでのスクリプト例を示します。YOLOv8ではモデルのバリエーションとしてYOLOv8nからYOLOv8xまで5種類ラインナップがあり、ここではYOLOv8mを使用していますが、Cleanlabはモデルに依存しない手法なので基本的にどのモデルを選択しても問題ありません。実行する計算機環境やどれぐらいの時間でスコアを算出したいかに応じて選択してください。 for i in {1..5}; do yolo train data=/usr/src/datasets/coco_fold${i}.yaml model=yolov8m.pt done 予測と正解データの生成 各foldで学習したモデルで各foldのvalidationデータに対する推論を実行し、各推論結果を結合することでデータセット全体に対する予測を出力します。生成した予測はCleanlabが入力として期待するフォーマットに変換する必要があります。またvalidationデータに対する正解データの生成も必要です。 YOLOv8を用いて予測と正解データの生成を実行するサンプルスクリプトは以下の通りです。出力される predictions.pkl が予測、 labels.pkl が正解データに対応しています。 import os import pickle import numpy as np from tqdm import tqdm from ultralytics import YOLO from ultralytics.data.dataset import load_dataset_cache_file from ultralytics.engine.results import Results def make_prediction (results: Results, num_classes: int = 80 ) -> np.ndarray: """YOLOv8の推論結果をCleanlabの入力形式に変換""" pred_cls = results.boxes.cls.cpu().numpy() pred_conf = results.boxes.conf.cpu().numpy() pred_boxes = results.boxes.xyxy.cpu().numpy() prediction = [] for target_cls in range (num_classes): target_cls_mask = pred_cls == target_cls if sum (target_cls_mask) == 0 : # 該当クラスの検知結果が無い場合の処理 prediction.append(np.empty(( 0 , 5 ), dtype=np.float32)) else : target_cls_boxes = pred_boxes[target_cls_mask] target_cls_conf = pred_conf[target_cls_mask] target_cls_pred = np.concatenate([target_cls_boxes, target_cls_conf[:, None ]], axis= 1 ) prediction.append(target_cls_pred) return np.array(prediction) def make_label (data: dict ) -> dict : """YOLOv8の正解データをCleanlabの入力形式に変換""" label = { "labels" : data[ "cls" ].flatten().astype( int ), "seg_map" : os.path.basename(data[ "im_file" ])} # 正規化されたcx, cy, w, hの2次元アレイをshapeの値で元の座標に戻して、xyxy座標に変換 bboxes = data[ "bboxes" ] * np.array([data[ "shape" ][ 1 ], data[ "shape" ][ 0 ], data[ "shape" ][ 1 ], data[ "shape" ][ 0 ]]) bboxes[:, 0 ] = bboxes[:, 0 ] - bboxes[:, 2 ] / 2 bboxes[:, 1 ] = bboxes[:, 1 ] - bboxes[:, 3 ] / 2 bboxes[:, 2 ] = bboxes[:, 0 ] + bboxes[:, 2 ] bboxes[:, 3 ] = bboxes[:, 1 ] + bboxes[:, 3 ] label[ "bboxes" ] = bboxes return label def main () -> None : """5-foldのOut-of-fold予測および正解データを生成""" model_paths = [ "/usr/src/ultralytics/runs/detect/train/weights/best.pt" , "/usr/src/ultralytics/runs/detect/train2/weights/best.pt" , "/usr/src/ultralytics/runs/detect/train3/weights/best.pt" , "/usr/src/ultralytics/runs/detect/train4/weights/best.pt" , "/usr/src/ultralytics/runs/detect/train5/weights/best.pt" , ] dataset_paths = [ "/usr/src/datasets/coco_fold1/labels/val.cache" , "/usr/src/datasets/coco_fold2/labels/val.cache" , "/usr/src/datasets/coco_fold3/labels/val.cache" , "/usr/src/datasets/coco_fold4/labels/val.cache" , "/usr/src/datasets/coco_fold5/labels/val.cache" , ] predictions: list [np.ndarray] = [] labels: list [np.ndarray] = [] for model_path, dataset_path in zip (model_paths, dataset_paths): model = YOLO(model_path) cache = load_dataset_cache_file(dataset_path) for data in tqdm(cache[ "labels" ]): results = model(data[ "im_file" ]) predictions.append(make_prediction(results[ 0 ])) labels.append(make_label(data)) # pickleファイルとしてpredictionsとlabelsを保存 with open ( "predictions.pkl" , "wb" ) as f: pickle.dump(predictions, f) with open ( "labels.pkl" , "wb" ) as f: pickle.dump(labels, f) if __name__ == "__main__" : main() Cleanlabによる品質スコア算出 validationデータに対する予測と正解データを比較して画像ごとの品質スコアを算出します。品質スコアは0.0から1.0の範囲の数値で出力され、数値が高いほどアノテーション品質が高いことを表します。先ほどのスクリプトで predictions.pkl と labels.pkl が生成されているはずなので、これをCleanlabの 物体検出向けのAPI に入力します。Cleanlabは pip install Cleanlab 等でインストールできます。 スコアを算出し、スコアの低い画像を確認するサンプルコードは以下の通りです。COCOは80クラスのデータセットですが、説明のしやすさのためpersonクラスのみに絞った場合の例になっています。このコードを Jupyter Notebook 等で実行することで品質スコアの低い画像およびアノテーションを確認することが出来ます。 import pickle from Cleanlab.object_detection.rank import ( get_label_quality_scores, issues_from_scores, ) from Cleanlab.object_detection.summary import visualize IMAGE_PATH = 'all_images' predictions = pickle.load( open ( "predictions.pkl" , "rb" )) labels = pickle.load( open ( "labels.pkl" , "rb" )) # 各画像の品質スコアを計算 scores = get_label_quality_scores(labels, predictions) # 品質スコアが0.5を下回る画像インデックスをスコアの低い順にして取得 issue_idx = issues_from_scores(scores, threshold= 0.5 ) # 一番スコアの低い画像を表示 class_names = { "0" : "person" } issue_to_visualize = issue_idx[ 0 ] # ここの数値を変更することで別の画像を表示可能 label = labels[issue_to_visualize] prediction = predictions[issue_to_visualize] image_path = IMAGE_PATH + label[ 'seg_map' ] visualize(image_path, label=label, prediction=prediction, class_names=class_names, overlay= False ) 私の手元で試したCOCOデータセットにおけるいくつかスコアの低い画像および高い画像の例を載せておきます。図中の左画像の赤枠が正解データ、右画像の青枠がモデルによる予測です。 スコアの低い画像の例 右画像のモデルは左奥にいる人を予測できているにも関わらず、左画像ではアノテーションがされていないことでスコアが低くなっています (スコア: 2.092e-34) この画像は Timothy Krause氏によるMan with umbrella をCleanlabにより可視化したものです。 (C) 2012 Timothy Krause, Man with umbrella, License: http://creativecommons.org/licenses/by/2.0/ 右画像のモデルは4人の人物を正確に予測していますが、左画像では4人を囲うようにアノテーションされているためスコアが低くなっています (スコア: 4.821e-07) ちなみにCOCOでは複数人をまとめてアノテーションする場合にiscrowdという属性をTrueにするというルールがありますが、この画像はiscrowdがFalseになっていたので、そういった観点でもアノテーションに不備があると言えます この画像は Luca Vanzella氏によるDSCN0073b をCleanlabにより可視化したものです。 (C) 2005 Luca Vanzella, DSCN0073b, License: https://creativecommons.org/licenses/by-sa/2.0/ スコアの高い画像の例 正解データと予測がほぼほぼ一致しているのでスコアが高くなっています (スコア: 0.987) この画像は Michael Elleray氏によるSurfin' をCleanlabにより可視化したものです。 (C) 2011 Michael Elleray, Surfin', License: http://creativecommons.org/licenses/by/2.0/ この画像は Wonderlane氏によるMan making a sandwich, Vietnamese / Chinese, International District, Seattle, Washington, USA をCleanlabにより可視化したものです。 (C) 2010 Wonderlane, Man making a sandwich, Vietnamese / Chinese, International District, Seattle, Washington, USA, License: http://creativecommons.org/licenses/by/2.0/ アノテーション修正 スコアの計算が出来たらスコアの低い画像群の確認と修正を実施します。アノテーションツールとしてセーフィーでは FastLabel社 のツールをよく利用しています。その他 CVAT やYOLOv8公式でサポートされている Roboflow 等、様々なツールがありますので利用しやすいものを使用してください。 Cleanlabをフル活用するためのTips 今回Cleanlabを利用する中で気づいた課題やそれに対する対策についていくつか説明します。 同じ正解データや予測を複数回マッチングさせないようにする Cleanlabの現状の実装(2024/05/17時点)ではスコア計算時に同じ正解データや予測が複数回マッチングされる場合があり、スコアが正しく計算できない場合があります。スコアを出力してみて複数回マッチングの影響で意図しないスコアが出ているようであれば、スコア計算処理のマッチング部分を修正する必要があります。 閾値をモデルやデータセットに応じて適切に設定する Cleanlabでは信頼度の低い予測をスコア計算に使用しないように2つの閾値を設けています。この閾値の設定によって出力されるスコアの傾向が大きく変わるので、自分が学習したモデルやデータセットの特性に応じて調整することで精度の高いスコア計算が可能になります。 スコアが1.0になっている画像でも一通り確認する Cleanlabのスコアは0.0から1.0までの数値で表されるため、定義としては1.0は最もスコアが高い(アノテーション品質が高い)ことになります。しかし正解データと予測でマッチングするのものが無かった場合等にCleanlabはスコアのデフォルト値である1.0を出力するため、1.0というスコアは必ずしもアノテーションに誤りが無いことを表してはいません。実際上正解データと予測がぴったり合う(スコアが1.0になる)ことはほとんどあり得ないので、1.0のスコアが出力されたということは何らかの問題がある可能性があります。 以下の画像は一例ですが、スコアが1.0にも関わらず正解データではアノテーションが1つ漏れていることが分かります。これは予測のbboxの信頼度が低く、正解データとのマッチングプロセスがスキップされるために発生していると考えられます。 この画像は Brad Greenlee氏によるDonut Tower II をCleanlabにより可視化したものです。 (C) 2007 Brad Greenlee, Donut Tower II, License: http://creativecommons.org/licenses/by/2.0/ まとめ 今回はCleanlabを用いたData-centric AIによる物体検出データセットのクレンジング方法について紹介しました。Cleanlabを利用することで効率的にデータセットを改善することが出来、モデルを変えなくてもモデルの精度改善が可能になります。実際の改善では今回紹介したData-centricおよびModel-centricの両方の観点で取り組むことが重要ではありますが、Model-centricアプローチによる精度改善に手詰まりを感じられている方がいれば試してみるのも良いと思います。 最後になりますが、セーフィーではエンジニアを積極的に募集しています。気になる方はこちらをご覧ください! https://safie.co.jp/teams/engineering/ カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 最後までお読みいただき、ありがとうございました。
こんにちは!23新卒のフロントエンドエンジニアの一氏です。 新卒エンジニアの研修の一環として、社内課題の解決をテーマに企画から実装まで行う自由度の高いチーム開発研修を実施しました。 チーム開発研修を行う過程で私たち5人の新卒エンジニアはどんなことを考えていたか、何を学んだかをまとめました。 研修概要 目的 課題 チーム開発研修 研修の流れ アイディア出し 開発初期にしたこと 開発 成果物詳細 こだわりポイント 学びが深かったこと どの機能をどこまで作り込むか フロントエンド開発 アジャイル開発 もっとこうすればよかったこと 報連相の重要性 APIの仕様のすり合わせ 最後に 研修概要 目的 今回のチーム開発研修を行うにあたって、初めに以下の3つの目的が共有されました。 セーフィーで取り扱っているカメラの内部構造についてエンジニアとして理解しておく システム開発で補助をもらいながら自走可能な状態になる 業務(チーム開発)で必要なスキルを磨く(工数・チケット管理等) 特にカメラの内部構造の理解はハードウェアを扱っている弊社ならではの目的であり、面白いポイントだと思います。 課題 提示された課題は「社内課題の解決 *1 」でした。 具体的な社内課題のテーマや開発手法、開発環境などは新卒が自由に決めることができました。 制約として、セキュリティ担保のためのインフラチームによる設計レビューやRaspberry Pi(カメラ)の利用などは課せられましたが、それ以外は自由です。 目的・課題が与えられた当初はこんなに自由度高く企画から開発まで研修で行うのかという驚きが半分、新卒が企画から開発まで全てを通して実施することができるのかという不安が半分でした。 また研修で企画から開発まで一通りの流れを経験することは、配属後の業務に役に立つだろうという期待もありました。 チーム開発研修 研修の流れ 期間は2023年6月12日〜7月31日までの1ヶ月半ほどで、以下の流れで行いました。 アイディア出し 初めに何の社内課題をどのように解決するかアイディア出しを行いました。 どんな課題があるのか調査し、マインドマップを作成し、アイディアを煮詰めていきました。 アイディア出しで難しかった点は3点あります。 1点目は、フロント・サーバー・デバイスの3つの領域をカバーすることで、新卒エンジニアメンバーの配属先と同じ領域をカバーしたいと考えたためです。 2点目は、目的の1つであるカメラの内部構造の理解のためにデバイス(Raspberry Pi)の利用が必要だったことです。 3点目は、実際に私たち自身が使いたくなるようなアイディアであることで、社内課題の解決のために開発するならこれから先本当に社内で使ってくれるものを開発した方がやる気が出ると考えたためです。 上記の難しかった点と社内課題の解決というある意味選択肢が無限にある課題を満たすため様々なアイディアが出てきました。 最終的にチーム開発のテーマは弊社の福利厚生としてある社内図書を、映像から顔認証と書籍のバーコード読み取りを通して貸し借りを行う、書籍管理システムの作成になりました。 開発初期にしたこと アイディアが決定した後はプロジェクトとして進めていくのに必要なことを実施・決定していきました。具体的には以下の内容です。 プロジェクト計画書作成 要件定義書など各種仕様書作成 顔認証などの技術検証 環境構築 開発手法決定 各種仕様書の作成は新卒エンジニア全員がほぼ経験なし or 未経験で、どのように作成すれば良いのか手探りで進めていきました。 チーム開発研修の前にUdemyでプロジェクト管理の講座でどのような仕様書が必要か学んでいましたが、講座を見るのと実際に作成するのとでは勝手が大きく違いました。 特に1ヶ月半という短い期間でどの仕様書をどこまで作り込むべきか悩みながら作成しました。 仕様書を作成した後で、この部分は今回の研修では考えなくてよかったということや、初めから完璧な仕様書を作ろうとして想定以上に時間がかかってしまったなどの反省点がありました。 途中からは不完全でもある程度仕様書を作成した時点で上司にレビューしていただき、方向性が間違っていないか確認するようにしました。 早めにレビューしていただくことで、余計な手戻りを少なくしつつ仕様書作成が進められることを学びました。 今回作成するプロダクトは研修の制約としてRaspberry Piの使用が必須であるため、フロント・サーバー・デバイスの3つの環境構築が必要でした。 フロントは今回はフレームワークに頼らずできるだけ地力をつけたいという思いからフレームワークなしのTypeScriptとWeb Componentsで実装しました。(ご想像の通り茨の道でした。) サーバーは弊社で採用されることが多いFastAPIとしました。また、書籍の貸出履歴の閲覧機能などの開発工数を抑えられるようにNotion APIを利用しました。 Notion APIを採用することでNotionのデータベースをAPI経由で操作できるようになります。 デバイスは初めC++で環境構築していたのですが、購入したRaspberry Pi カメラモジュール V3がC++で動かず… Pythonでは動作したため、デバイスで使用する言語をPythonに変更しました。 Pythonに変更する事に合わせて、サーバー自体もFastAPIを使用したローカルAPIサーバーに変更することにしました。 開発 開発はアジャイル開発で進めました。今回は1スプリントを2週間として、2スプリント回すことになりました。 具体的に学んだことは後述しますが、困ったことを気軽に相談できる環境や1スプリントで何をするのかを明確にすることなどを学べたことは、今後様々な開発を行う中で役に立つと思います。 開発中に難しい箇所があった際は、フロント・サーバー・デバイスの各領域のエンジニアに相談しながら進めることができました。 実装で困ったことがあった際は基本的にSlackで随時相談しつつ、時には実装のレビューをしていただきながら進めました。各領域のエンジニアに相談できる環境は非常に心強かったです。 最後の1週間はリリースの準備として、デプロイ環境の整備や利用者向けのドキュメント作成、発表資料作成、バグ修正を行いました。リリースに向けて行うことも多く、ギリギリまでドタバタしながら作業していました。もちろん実際のサービスのリリース作業と比較すると簡単なリリース作業とは思いますが、リリースのためにする作業を一通り経験できたのはよかったです。 成果物詳細 最終的な成果物である書籍管理システム librarian は主に以下の機能を提供するサービスです。 顔認証および書籍のバーコードスキャンを用いた書籍の貸出・返却 書籍一覧の閲覧はNotionで可能 初めにユーザーは貸出か返却かを選択します。 開始画面 顔認証は事前のユーザー登録時に顔画像を登録し、登録した顔画像とスキャンした顔画像の特徴が同じか FaceNet で識別しています。 顔画像はAWSのS3上に特徴量として保存しています。 当初はRaspberry Pi上で顔認証処理を行っていましたが、処理に5秒ほどかかっていました。 サーバー上で顔認証処理を行うように変更した結果、顔認証の処理時間は1.5秒ほどになりました。(今回は社内でのみ使うシステムだったためサーバー上で顔認証処理を行うという力技で解決しました。) 顔認証成功画面 書籍のバーコードスキャンは pyzbar を用いて映像からバーコードのISBN番号を取得しています。取得したISBN番号からNotionの書籍一覧データベースの書籍情報を取得して表示します。 バーコードスキャン画面 顔認証と書籍のバーコードスキャンをした後は貸出・返却ボタンをクリックして貸出・返却処理を行います。貸出・返却処理が完了するとNotionのデータベースが更新されます。 確認画面 貸出・返却処理完了画面 なお今回は時間の問題からユーザー登録・削除はAPIのみ作成しSwagger UIから直接APIを叩き、書籍登録・削除はNotionデータベースに直接入力としました。 こだわりポイント 作成するにあたって特にこだわって作成した点は以下の2点です。 顔認証・バーコードスキャン機能による直感的なUI/UXの提供 Notionデータベースによる書籍一覧と貸出履歴の閲覧 カメラで撮影した映像から顔認証とバーコードスキャンを行うことで、貸出・返却の際に利用者カードのような持ち物は必要なく、ログインや書籍の検索などの面倒な操作なしで利用できます。 開発する際は実装前に顔認証やバーコードスキャンをした時の動作イメージを考えるため、事前にFigmaでプロトタイプを作成し、UI/UXはどうかトレーナーにも確認していただいてから実装に入りました。結果としてほぼイメージ通りの動作を実現することができました。 また、Notionデータベースを利用したことは開発工数の削減に大きく貢献しました。 Notionのデータベース機能を利用することで、データベースに関する開発と書籍一覧等の表示に関する開発の工数を削減することができました。Notionのデータベースはフィルターや並べ替え、検索機能もついており、これらの機能も含めて実装する場合は研修期間内に開発を完了させることはできなかったと思います。 作成した書籍管理システム librarian は今後ユーザー登録や書籍登録、社内への周知などを行ない、現在は実際に社内で運用されております。 学びが深かったこと どの機能をどこまで作り込むか どの機能をどこまで作り込むかに関しては1ヶ月半という開発期間ではごく一部の機能しか作り込むことはできませんでした。 開発当初は追加機能についても多くのアイディアが出ていたのですが、実際にはほぼ必須の機能しか実装することはできませんでした。 一般的には開発の難易度や開発期間、メンバーの習熟度などをふまえて、どのくらいの機能を作ることができるかを決められると思います。 今回の開発ではこれらをふまえてどの機能まで作るか目安を考える力が不足しており、1ヶ月半の期間で開発できる機能を超えて開発計画を立ててしまいました。 開発計画が予定通り進行していないことは1スプリントでどのような成果物ができたかを確認するスプリントレビューで気づきました。今回は研修期間を伸ばすことはできなかったためMVPを作成することを最優先として、実装しなくても良い機能はどれか改めて議論することになりました。 最終的にユーザー登録・削除機能はAPIのみ開発してフロントエンドの開発をしないことにして、なんとかMVPを作成することができました。 実際の開発ではQCDの観点もふまえて開発する機能を決定していくと思います。今回の研修で改めてプロジェクト計画を予定通りに進行させる難しさを学びました。 フロントエンド開発 今回フロントエンドはフレームワークなしのTypeScriptとWeb Componentsで実装することにしました。 弊社ではサービスによってAngular, Vue, Reactのフレームワークを使い分けており、フロントエンド開発の地力の部分を学びたいという思いから、フレームワークなしという選択をしました。 しかしフレームワークなしでSPAを実装したことがあるメンバーはおらず、どのように実装すれば良いのか初めは検討もつけることができませんでした。 そんな中メンバーの一人がフレームワークなしで開発する方法を調べてベースとなる開発環境を構築してくれたのですが、この開発環境のおかげでスムーズに開発に入ることができました。 作成した開発環境には事前にTSとテンプレートの組み合わせ方、ライフサイクルメソッドの使い方、コンポーネント化の方法の例が書かれていました。この例やその他のWeb Componentsでの実装方法を参考にしつつ実装を進めたことで、フロントエンドの開発のよりコアな部分の知識を身につけることができました。 アジャイル開発 今回メンバー全員が初めてのアジャイル開発に取り組みました。 弊社ではアジャイル開発を採用しているチームが多いことや内定者インターンの際はウォーターフォール開発を採用したことからアジャイル開発を採用しました。 1スプリントを2週間として、デイリースタンドアップ、スプリントプランニング、スプリントレビュー、レトロスペクティブといったスクラムイベントを実施しました。 アジャイル開発は初めてということもあり、1スプリントでどのくらいのポイントを消化できるのか、開発の方法をどのように改善していくのか手探りで進めていきました。 みんながアジャイル初心者だったことでお互いに進め方でわからない部分を気軽に相談することもできました。 今回はスプリントを2周しか回すことができなかったのですが、1周目のスプリントに比べて2周目のスプリントはベロシティを1.2倍に増やすことができました。 ベロシティが増えた要因として、だんだんと開発に慣れてタスクの抜け漏れが減ったこと、わからない部分はお互いにすぐに相談することで素早く問題解決できるようになったことが挙げられると思います。 もっとこうすればよかったこと 報連相の重要性 開発を進める中で報連相の重要性を実感しました。 例えばプロジェクト計画書の作成で上司にレビューをしてもらう必要があったのですが、初めからある程度完成したものを提出しようとしていました。 しかし、レビューをしていただくと今回のチーム開発では必要な項目が足りなかったり不必要な項目があったりしました。 どの項目が必要かを事前に相談してから作成すれば大きな修正は少なくなったと思います。 また、開発中に実装方法で悩んだ際に一人で悩み続けてしまうことがありましたが、結局他のメンバーやトレーナーに相談するとすぐに解決するということが何回かありました。 これらの経験から自分だけで悩み続けるのではなく、早めに他のメンバーやトレーナー、上司と方向性の確認や相談をすることが大切なことを再認識しました。 今回のチーム開発研修では特に研修の初期はなかなか相談することができませんでしたが、開発が進むにつれてなんでも相談できるようになってきました。 しかし、どのくらい悩んだら相談するかのラインを引くのは個人的に苦手意識があります。 なんでも相談しすぎると周りの時間を奪うことになり、相談しなさすぎるといつまでも同じ箇所で悩んで自分の時間を浪費することになります。 15分悩んだら相談するというようなルールをチームで決めることが開発を行う中で重要と感じました。 APIの仕様のすり合わせ 今回の開発の1番の反省点としてAPIの仕様のすり合わせに時間がかかったことが挙げられます。 APIの仕様でメンバー間で齟齬があったことや考慮漏れがあったことで後でAPI仕様書を修正することがありました。原因として以下の点が考えられます。 API仕様書の作成とAPIの実装を同時に行った API仕様書のレビュー方法が曖昧だった 今回APIはFastAPIで作成しましたが、FastAPIはSwagger UIを用いたAPIドキュメントを作成することができます。 今回はSwagger UIをそのままAPI仕様書とすることにしたのですが、どこまで実装してからAPI仕様書としてレビューするのかを明確にしないまま開発を進めてしまいました。 その結果、API仕様書のレビューをせずにいきなり実装まで行い、後からAPIの修正が必要な場面が何度かありました。API仕様書としてレビューできる最低限の部分だけ実装してAPI仕様書のレビューを行うというルールを作ることで防げたと思います。 また、API仕様書はトレーナーからのレビューが必須だったのですが、メンバー間でのレビューを必須としていませんでした。 トレーナーからのレビューは主にAPI仕様書の形式が問題ないかをレビューしており、APIの細かいパラメータやURIが実装をする上で問題ないかのレビューを行なっていなかったため、後からパラメータの不備などに気づくことがありました。 パラメータやURIについてはトレーナーなだけでなく、実装を行うメンバー間でレビューを行うなど、誰がどのような観点でAPI仕様書のレビューを行うのかを明確にすることが必要だったと感じました。 最後に 今回のチーム開発研修は1ヶ月半という短い期間で企画からリリースまで開発の流れを一通り行うことができる貴重な経験となりました。今後は研修で学んだことを活かして、映像から未来をつくるシステム開発に取り組んでいきたいと思います。 *1 : 本研修における社内課題の解決とは業務を支援するツールの作成ではなく、社内の環境を快適にするための何かを作成することです。
はじめまして! エンジニアリングオフィスの井上です。 本記事では、開発本部(エンジニア向け)の社内アイデアソンの企画運営に携わりましたので取り組みの様子や内容についてお伝えさせていただこうと思います! 突然ですが「どこでもドア」って売れると思いますか? そもそも「アイデアソン」ってなに 開発本部でアイデアソンをする目的 アイデアソンのテーマ アイデアソンの日程 Day1で何をしたか 使用したビジネスフレームワーク Day1終了後からDay2までの一週間 Day2で何をしたか Day2結果発表 アイデアソンを実施した感想 参加者からの感想 社内アイデアソンに参加した感想を教えてください 最後に 突然ですが「どこでもドア」って売れると思いますか? いきなりですが某猫型ロボットでおなじみの「どこでもドア」、もし製品化したら売れると思いますか?みんな欲しいから売れるに決まってるよね、と自分は考えていました。 では、「どこでもドア」の価値はどのようなものでしょうか? 下準備無しで任意の場所へ瞬時に往来できる、というのが「どこでもドア」の価値ですよね。この価値に釣り合う価格は、一体どうやって決めるのでしょうか?難しいですよね。 価格設定やルールを作るためには、主要なターゲットを明確に定義する必要があります。 仮に「どこでもドアが」販売されたとしたとしたら国際問題へと発展する可能性があります。価値の項でも記載しましたが、任意の場所へ瞬時に往来できる道具があったら使い方次第で国を跨ぐ大犯罪をリスク無しで実行できてしまいます。 上記を踏まえて考えてみると、使用に際して法整備も必要になるので顧客は一般の方や法人かもしれませんが、ステークホルダーとして政府や各国との調整が必要です。 また、利用規約に違反した人を取り締まる仕組みや機関も必要でしょう。 全世界で利用された場合のメンテナンスやケアはどうなるでしょうか。 etcetc….. 軽く考えただけでも課題が山積みです。 どこでもドアのように、凄まじく便利な物があったとしても実際に「売る」となると簡単ではない、売るのは難しいな・・・と自分は思いました。 日頃のエンジニア業務だとアイデアを出した後に深堀し続けて、どう売るか?顧客は?販売チャネルは?という事を考える機会はあまりありません。 そのための機会として、開発本部のエンジニアを対象にアイデアソンを開催しました。 そもそも「アイデアソン」ってなに アイデアソンは「アイデア(idea)」と「マラソン(marathon)」をかけ合わせた造語で、みんなで集まり議論を行いながら様々なビジネスアイデアを出し合う、という取り組みです。 似たような言葉に「ハッカソン(hack + marathon)」があります。 ハッカソンは「開発」に重きを置くのに対して、アイデアソンは「アイデア」に重きを置いています。 そのため、ハッカソンに比べて専門知識が必要なく、誰でも参加しやすいというのがアイデアソンの大きな特徴になります。 開発本部でアイデアソンをする目的 セーフィーの開発本部は2024年4月現在、以下のように職能にて組織が分かれています。 ※セーフィーの組織についてより詳しく知りたい方は、是非引用元のVPoEの仕事について - Safie Engineers' Blog! をご参照ください! 一般的には、組織というのは職能別に分かれているため、職能外の技能・知見は吸収し難いという課題があります。その課題を解決するためにも、一緒にプロダクトの将来を考えるアイデアソンをチーム横断で実施することにより、より人となりを知ったり、社内ネットワーキングの向上を一つの目的としました。 なので、参加者は応募形式とし、できるだけ同じ部署にならないようばらけたチーム構成にしています。 また、アイデアがあってもそれをサービス化するのは難しいため、アイデアをサービス化するためにファクトとして何が必要か、という事を体系立てて学ぶ事ができる機会を提供することを目指しました。 アイデアソンのテーマ Safieの映像プラットフォームを活かし、新しいビジネスアイディアを創出しよう! というものです。 参加者の関心がある課題の解決のために、セーフィー株式会社がどのようにアプローチできるか、という事を主軸に置いて、今回のテーマを設定しました。 また、セーフィーが展開している「映像プラットフォーム」を利用することで、何が出来るのか・実現できるのかということをアイデアソンを通して再発見してもらう事も本テーマを設定した理由の1つです。 アイデアソンの日程 今回の社内アイデアソンは一週間の期間で開催しました。 初日(Day1)はキックオフ、最終日(Day2)に発表日を設定し、それ以外の期間中はチームごとに集まって相談・ブレストして資料作成を進める方針にしました。 Day1で何をしたか まず、Day1にて各チームでアイデア出しをしてもらい、これぞ!というアイデアをチームの発表内容に選定してもらいます。 じゃあ各々始めてください!といって放り出す事はせず、アイデアを出す際の考え方や、アイデアをブラッシュアップするための手法・ツールについての座学を準備しました。 本アイデアソンでは、LeanCanvasというビジネスフレームワークも用いてアイデアの深堀や構造化を行いました。 各チームでアイデアの相談をしている風景 使用したビジネスフレームワーク Lean Canvasとは、新しいビジネスモデルの開発を生産効率性の向上と問題の顕在化によるムダの徹底的排除というアプローチで目指すマネジメント論を指します。 LeanCanvasツール このツール上の各項目を埋めていく事でアイデアを短時間でシンプルに可視化・検証できるため、効率的なブラッシュアップが可能で認識共有が容易といった利点があります。 上記のフレームワークを利用しつつ、実際にチーム内で現実問題として困っていること、改善されたら便利になることを挙げてもらい、セーフィー株式会社の得意分野である「映像」を用いて解決に持っていくことを模索しました。 実際にアイデアを出し合い、各チームで相談している際の風景 各チームメンバーから秀逸なアイデアが生まれ、選定に苦労していました Day1終了後からDay2までの一週間 Day1で各チームのアイデアが確定したので、Day2までの期間でプレゼン資料の作成に取り組みました。 この期間中Day1で実施した講義内容も踏まえ、各チームメンバーが役割分担を行い、競合調査・市場調査・実現性を高めるための課題の洗い出し等を実施しました。 Day2で何をしたか Day2では、各チームで作り上げたアイデアの発表を行いました。 発表の場では、実際にセーフィーの役員・部長陣がビジネスからの観点やプロダクトとしての実現性等を加味して審査頂きました。 部室長の前で練りに練ったアイデアを発表している様子です Day2結果発表 栄えある第一回社内アイデアソン優勝チームは、チームAでした! アイデアの内容については細かな内容を公表することはできませんが、優勝の決め手として、「未来の意思決定に大きく作用することができる」という所に審査員が未来と発展性を感じたからでした。 アイデアソン終了後の集合写真。無事成功して安堵しました アイデアソンを実施した感想 今回の社内アイデアソンを振り返ってみると、開発本部でアイデアソンを開催する目的である アイデアをサービス化するために体系立てた学びの機会を提供する チーム横断的な社内ネットワーキングの向上 この2つについて、間違いなく達成できたと感じています。 ・アイデアをサービス化するために体系立てた学びの機会を提供する アイデアソンを行うのは初めてだったので、業務時間内でアイデアやアウトプットができるか、といった不安がありました。 実施した結果、参加者全員が楽しみながらアイデア出しや資料作成をされており、全体を通して良いアイデアソンになったと思います。 チームで選定したアイデアは勿論、個々人のアイデアについても、これがあったら世の中もっと便利に・安全になる!実現したい!という熱量が感じられる物でした。 個々人の中で貯めこんでいたアイデア・思い・仕組みを具現化できるのはアイデアソンならではだと思います。 運営側ではありますが、自分も「こういう考え方があるのか」「人の目を釘付けにするプレゼン資料はこう作るのか」といった学びを得る事ができました。 ・ チーム横断的な社内ネットワーキングの向上 他部署の方々と気軽に相談・会話ができる方が増え、社内交流に寄与できたと実感できました。 既に関係値があるメンバーの新しい一面を見ることができるので、社内イベントとして実施するのはかなり有効だと感じました。 懇親会では「あのアイデア良かったね!」という言葉もあれば、「あのアイデアに既存のプロダクトと合体させたらもっと魅力的になるんじゃないか」といったフィードバックもあり、盛り上がりました。 エンジニア同士が交流を深め切磋琢磨できる環境を提供でき、実りのある経験を得られたと執筆しながら感じています。 参加者からの感想 参加者の皆さんに記入いただいたアンケートの感想を公開します。 社内アイデアソンに参加した感想を教えてください アイデア出しから、ターゲット選定、実現性判断、プレゼン資料作成まで一連の流れを経験できて学びになった。 他のグループの発表を聞いて、この部分もう少し深堀できたななど、振り返れるところが大きかった。 普段とは違った頭の使い方ができて楽しかったです。マネーの虎みたいでした。 自分がこういう製品が欲しいなと思ったものを他の人の意見をもらいながら、あーでもないこーでもないとブラッシュアップして、最終的にプレゼン出来るところまで持っていけたのがとても良かった。 普段使わない頭の使い方をすることができた。リーンキャンパスなどのフレームワークは実際に使ってみないと理解できないので良いきっかけだったと思う。 全体的に面白かったです。参加した人は、「こういうサービス作りたい、必要だ!」と本気で思って参加されているように思ったので、出た案が企画に回されて、具体的なサービスとして実現される可能性がもっと感じられれば良かったと思います。 アイデアソンを通して、他の人のアイデア・資料の作り方の考え方を実際に見て体感できたのはプラスだった。 普段関係値が無い人と一緒に作業を行う事で、コミュニケーションを取れる機会になったのがよかった。 最後に 本記事では、社内アイデアソンについてご紹介させていただきました。 セーフィーではエンジニアを積極的に募集しています。どのような職種があるのか気になる方はこちらをご覧ください! https://safie.co.jp/teams/engineering/ カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 皆様のご応募、心よりお待ちしております! 最後までお読みいただき、ありがとうございました。
はじめに セーフィー株式会社の第3開発部 AIVisionグループで画像認識AIの開発エンジニアをしている土井 慎也です。 今回は、Segment Anything Model(SAM)というセグメンテーションモデルで、テキストプロンプトを使用してみたいと思います。 はじめに Segment Anything Modelとは Grounded-SAMとは 実践 環境構築 モデルダウンロード 画像とテキストプロンプト、閾値を指定 実行 出力結果 まとめ 最後に Segment Anything Modelとは Meta社(旧Facebook)が2023年4月に発表 公式ページ: https://segment-anything.com/ 公式コード: https://github.com/facebookresearch/segment-anything ゼロショットセグメンテーションモデル 未知の物体に対して、追加学習を必要としない 1100万枚の画像と10億以上のマスクを使用して学習 プロンプトにセグメンテーションを行いたい物体への点や矩形を指定 論文ではテキストをプロンプトとして使用している例があるが、コードは公開されていない 論文ではテキストをプロンプトとして使用している例があるが、コードは公開されていない! 今回、テキストプロンプトを可能にするために、 Grounded-Segment-Anything(Grounded-SAM) を代わりに使用する Grounded-SAMとは Grounded-DINOというテキストプロンプトが利用可能なゼロショット物体検出モデルとSAMを組み合わせたもの Grounded-DINOとSAMの二つのモデルを使うため大きく二つの処理に分かれます。 Grounded-DINO 画像と検出したい物体のテキストプロンプトを入力 検出結果として矩形を出力 SAM 画像と検出結果の矩形をプロンプトとして入力 セグメンテーション結果を出力 実践 環境構築 リポジトリをクローンする git clone git@github.com:IDEA-Research/Grounded-Segment-Anything.git リポジトリをクローン後、 Grounded-SAM: Installation を参考に環境にあったインストールを実行 モデルダウンロード cd Grounded-Segment-Anything wget https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth wget https://github.com/IDEA-Research/GroundingDINO/releases/download/v0.1.0-alpha/groundingdino_swint_ogc.pth 画像とテキストプロンプト、閾値を指定 grounded_sam_simple_demo.py#L31-L35 ... SOURCE_IMAGE_PATH = "./assets/demo2.jpg" CLASSES = [ "The running dog" ] BOX_THRESHOLD = 0.25 TEXT_THRESHOLD = 0.25 NMS_THRESHOLD = 0.8 ... テキストプロンプトはドット(.)区切りで入力することで複数種類の検出が可能 実行 cd Grounded-Segment-Anything python grounded_sam_simple_demo.py 出力結果 The running dog Grounded-DINOの出力 © 2020 IDEA, Inc .  Apache-2.0 license .  Grounded-DINO+SAMの出力 © 2020 IDEA, Inc .  Apache-2.0 license .  Horse. Clouds. Grasses. Sky. Hill Grounded-DINOの出力 © 2020 IDEA, Inc .  Apache-2.0 license .  Grounded-DINO+SAMの出力 © 2020 IDEA, Inc .  Apache-2.0 license .  まとめ 今回、Grounded-SAMを利用することで自由なテキストプロンプトでSAMを利用できることを紹介しました。 この技術を使用することで、新たなデータセットを作成する際には、ある程度アノテーションを自動的に作成できるようになり、モデル改善などのスピードを上げることができると思います。実際に、アノテーションツールなどに組み込まれていたりします。 また、SAMの精度も十分な精度が出ているように思えますが、下記のようにさらに高精度なセグメンテーションができる SAM-HQ という物も発表されています。これもGrounded-SAMで使用することができるので、気になる方はぜひ使ってみてください。 SAMとSAM-HQの比較 © 2020 IDEA, Inc .  Apache-2.0 license .  © 2020 IDEA, Inc .  Apache-2.0 license .  最後に セーフィーではエンジニアを積極的に募集しています。気になる方はこちらをご覧ください! https://safie.co.jp/teams/engineering/ カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 最後までお読みいただき、ありがとうございました。
こんにちは。サーバサイドエンジニアの村田 ( @naofumimurata ) です。 本記事では、セーフィーのシステムでカメラ映像の録画機能を担うアプリケーションのデプロイを改善した話を共有したいと思います。 セーフィーの録画・配信システム カメラサーバのデプロイの課題 デプロイの流れ 実行環境 問題 時間がかかる 作業負荷が高い メンテナンス性が悪い 結果どういう状態になったか 改善に向けた取り組み GitHub Actions + AWS CodeDeployの構成に 監視の強化 成果 デプロイ時間の短縮 作業負荷の軽減 デプロイ頻度の向上 まとめ セーフィーの録画・配信システム セーフィーはクラウド防犯カメラ・録画サービスを提供しています。 バックエンドのシステムとしては、まずカメラから映像を常時受け取りストレージに保存するアプリケーション(本記事では以降「カメラサーバ」と呼びます)が稼働しています。カメラサーバでは映像の保存の他にもカメラの情報管理や制御などを行なっています。ユーザはSafie Viewerというフロントアプリケーション(Web/モバイル)を利用し、カメラサーバに入ってきた映像を別の動画配信アプリケーションを介して閲覧することができます。 カメラサーバはJava、動画配信アプリケーションはGolangで実装されています。 インフラ環境は全てAWSを利用しており、カメラサーバはEC2インスタンス上で稼働しています。現在は 約20万台以上 のカメラが接続しており、カメラサーバだけで 約2,000台以上 のEC2インスタンスが稼働しています。 インフラ構成の特徴的な点として、負荷分散のために水平分割の構成を取っています。一定カメラ台数(約13000台程度)ごとにそれを処理するためのインフラ構成(ネットワーク、DB、redis、サーバ類)一式を構築しています。社内ではこの水平分割の単位を「ゾーン」と呼んでおり、本記事でもこの用語を使います。 カメラサーバのデプロイの課題 セーフィーのシステムにおいてカメラサーバは特に重要な役割を担っていますが、そのデプロイには大きな課題がありました。ここでは具体的なデプロイの流れとどういった課題があったかについて紹介します。 デプロイの流れ カメラサーバのデプロイではざっくり以下のことを行う必要があります。 アプリケーションのビルド AMIのビルド ゴールデンイメージを用意しスケーリングで利用 jarファイルの配布 redis, DB上の接続、管理情報を更新 カメラサーバに対して停止リクエストを送る カメラサーバが担当しているデバイスを他のカメラサーバに移動させる プロセス再起動 改善前の構成 単純にビルド、成果物の配布、プロセス再起動だけではないステップ(2、3、5番)があるのが特徴的かと思います。 まず、アプリケーションのビルドを行います。カメラサーバはJavaで実装されているのでjarファイルを生成します。次にAMIのビルドを行います。カメラサーバはゴールデンイメージ運用を行なっており、jarファイルを含むAMIを用意しスケーリングで利用しています。スケーリングはEC2 Auto Scalingではなく自前でスケーリングを行うアプリケーションが別に存在するという特殊な構成になっているのですが、ここでは詳しい紹介は割愛します。生成したjarファイルをインスタンスに配布した後、プロセス再起動の前に幾つかやることがあります。 まず、redisとDBのデータ更新を行います。カメラサーバへのカメラ割り当て状況や、カメラの接続状態・割り当て履歴といった情報をredis、DBで管理しており他のバックエンドアプリケーションからも利用されています。デプロイ時にはそのカメラサーバをサービス全体から外す必要があるため、正しい情報に更新を行います。 次にカメラサーバに対してRPCでシャットダウンの要求を送ります。プロセス再起動前にこれを行う目的はカメラの接続品質向上のためです。カメラサーバはシャットダウンの要求を受け取ると担当しているカメラに対して他のカメラサーバへの再接続を指示します。全てのカメラが別のカメラサーバに移動するのを待った後にプロセスを終了させています。 無事にシャットダウン要求が処理された後にプロセスを再起動し新しいバージョンのアプリケーションが起動します。 実行環境 上記の作業は全てスクリプト化されており、シェルスクリプトとAnsibleとPythonスクリプトを組み合わせて実現されていました。スクリプトの実行はデプロイ作業用のEC2インスタンス上で手動で実行する必要がありました。 問題 さて、ここまで紹介してきたカメラサーバのデプロイ構成ですが、サービスの成長に伴い様々な問題が現れてきました。 時間がかかる まず一番大きな問題は、デプロイにもの凄く時間がかかるということでした。実際にどのくらい時間がかかっていたかというと、全てのカメラサーバのデプロイを完了するのに最短で 30時間強 かかっていました。 これには幾つかの要因があるのですが、まず根本的なところとして2000台を超えるインスタンスがあり対象が多いということがありますが、それに加えてスクリプトの作りの問題で並列度が著しく低いということがありました。 前述の通り、セーフィーの録画配信周りのシステムは「ゾーン」と呼ばれる単位で構築されており、デプロイもゾーン単位で行う必要があるのですが、1ゾーンずつ逐次でしか実行できないようになっていました。1ゾーン内では複数インスタンス並列で実行されるのですが、サービスの成長と共にゾーンの数が多くなったため全体として見るとかなり並列度が低いという状態となっていました。 1ゾーンで約2、3時間程度かかっており、ゾーン数が15を超えたあたりからトータルで30時間を超えるようになっていました。 作業負荷が高い 次にデプロイ作業を行うエンジニアの負荷がとても高いという問題がありました。まず、デプロイが自動化されておらず手動でデプロイスクリプトを実行する必要がありました。くわえて、スクリプトも実行したら放置で済むというわけではなく頻繁に途中で失敗するようになっており、それへの対応のため作業者がつきっきりで画面を見ている必要がありました。 デプロイスクリプトが途中で失敗する原因としては、前述の通りデプロイ時に接続カメラを別のカメラサーバに移動させる処理があり、これが結構時間がかかる(なかなか移動してくれないカメラがいたりする)ためでした。実際のところここの待ち時間はカメラ依存であるため仕方がないのですが、デプロイスクリプトの待ち時間の判定がシビアで頻繁にタイムアウトし失敗判定になるという状態になっていました。 途中で失敗してしまった場合は、エンジニアが実際にカメラサーバのインスタンスに入ってログを確認し問題ないかをチェックし、問題なければ再開するということを行う必要があり、頻繁に失敗することが作業負荷を著しくあげていました。 メンテナンス性が悪い 最後はデプロイの実行環境およびツールに関することで、メンテナンス性が悪いという問題がありました。 前述の通り、デプロイスクリプトはシェルスクリプトとAnsibleとPythonスクリプトが組み合わせてできているのですが、かなり複雑怪奇になっており何が実行されているのかを把握するのが難しい状態になっていました。特にPythonスクリプトの部分に関しては別のアプリケーション実装の機能を部分的に呼び出すような特殊な構成になっており、挙動を変えるには別のアプリケーションの修正を行う必要があるという状態になっていました。 結果どういう状態になったか このような問題によって、以下のような現象が発生するようになりました リリーススケジュールが後ろ倒しになる カメラサーバの変更を回避するようになる まずデプロイにもの凄く時間がかかるので、カメラサーバに変更を加えた場合それによってプロダクトのリリーススケジュールが大きく後ろ倒しになることがありました。最短で約30時間強かかると書きましたが、これは休まず寝ずに作業した場合の話で、実際にはエンジニアが日中にスケジュールが空いている時間で作業することになるので、デプロイしたいと思ってから実際に完了するまでもっと時間がかかります。金曜リリースを避けつつ、セーフィーでは本番環境へのデプロイは複数人体制で行うようにしているため、スケジュールを調整したりすると実際には2、3週間から長い場合1ヶ月程度かかっていました。 そしてもの凄く作業負荷が高いので、みんなカメラサーバのデプロイをやりたくなくなりました。その結果カメラサーバへの変更を避けるようになり、本来ならカメラサーバにあるべき機能などを別のところで実装しだし、カメラサーバへの変更を回避するようになりました。 改善に向けた取り組み こういった問題を解決するために、カメラサーバのデプロイ改善に取り組むことにしました。有志のメンバーでワーキンググループを組成し現状の調査から解決まで取り組みました。 GitHub Actions + AWS CodeDeployの構成に デプロイの構成を見直し、GitHub ActionsとAWS CodeDeployを利用した構成へとまるっと作り替えを行いました。ツールの選定理由としては、GitHub ActionsとAWS CodeDeployどちらも既に利用実績があったことが大きなポイントでした。 これまで作業用インスタンスで行っていたアプリケーションのビルドをGitHub Actions上で自動で行い、成果物の配布とアプリの再起動処理はCodeDeployのスクリプトとしてカメラサーバインスタンス上で実行するような構成にしました。 また、併せてカメラサーバのEC2インスタンスをEC2 Auto Scaling Group (ASG)の管理下に入れるように変更しました。これによりASGとCode Deployの連携機能でASGで起動したインスタンスにはCodeDeployの最新のリビジョンが自動デプロイされるようになったため、ゴールデンイメージが不要となりAMIをビルドするステップを消すことができました。 アプリケーションの終了・再起動に関連する処理はこれまでPythonスクリプトで行っていましたが、別アプリケーションの機能に密結合な実装となっており、これをカメラサーバインスタンス上に配置して実行できるようにするのは後々のメンテナンス性を考えて避けたかったため、機能を切り出したものをGolangで新たに実装しました。Golangの場合、バイナリを配置するだけで動かせるので、アプリケーションの成果物と一緒に配布し実行させる構成にしました。 また、問題だったデプロイスクリプトの失敗判定などもGolang実装に移行する段階で修正し、無駄に失敗判定にならないようにしました。 改善後の構成 監視の強化 これまではデプロイ時の正常性の確認は、作業者がEC2インスタンスに入ってログなどを目視で確認するというかなり温かみのあるやり方で行っていたため、作業負荷が非常に高いという問題がありました。 デプロイ構成を一新するにあたりここについても改善するべく、CodeDeployのデプロイスクリプトでの正常性確認ステップに加えて、メトリクス監視の強化を行いました。 セーフィーではPrometheusによるメトリクス監視の仕組みが既にありインスタンスレベルでの監視は行われていましたが、今回はアプリケーションにPrometheus Exporterを新たに実装、正常性を確認するためのメトリクスを追加し、問題があればアラートが発報し作業者が都度ログを確認する作業などが不要となるようにしました。 成果 試行錯誤の末、約1年ほど前に新たなデプロイ構成に移行が完了しました。 その結果、以下のような成果が得られました。 デプロイ時間の短縮 一番の大きな問題だったデプロイにかかる時間ですが、これは大幅に短縮できました。 これまでは最短約30時間強かかっていましたが、改善後は最短で 3時間強 程度でできるようになりました。新しい構成ではゾーン毎に並列で実行できるようになったため、ゾーン数によらず同時に全ゾーンに対してデプロイを行えるようになったところが大きなポイントでした。 作業負荷の軽減 作業負荷の問題についても大きく改善できました。これまでは、作業者が手動でデプロイを行い画面につきっきりで作業を行う必要がありましたが、新しい構成ではGitHubのイベントをトリガーに自動でデプロイが実行されるようになり、デプロイ中も基本的に放置しておけば良くなりました。 デプロイ頻度の向上 デプロイの頻度も大きく改善しました。セーフィーでは検証環境と本番環境の2つの環境があるのですが、検証環境では 9倍 程度、本番環境では 3倍 程度向上しました。 まとめ 本記事では、セーフィーのシステムで特に重要な役割を担うカメラサーバのデプロイの問題を改善した事例を共有させていただきました。大きな問題で改善に向けて様々な苦労がありましたが、無事にやり切ることができ、実際に改善効果も得られてよかったかなと思います。デプロイ構成の移行にあたっては互換性の維持を重視し、なるべく実行する処理は変えずに改善することを意識していたため、そもそもの処理の内容自体にまだまだ改善の余地が残っていると思っており、その辺りの根本的な改善も継続してやっていきたいと考えています。 さいごに、セーフィーではエンジニアを募集しております。ご興味が出た際はぜひご応募いただけたらと思います! https://safie.co.jp/teams/engineering/ 最後までお読みいただき、ありがとうございました。
はじめに セーフィー株式会社のVPoEを務めている谷口元信です。 2022年4月からVPoEとしてエンジニアの組織形成と運営に取り組んでいます。 今回はVPoEに何が求められるかについて私なりの解釈を伝えた上で、取り組んだ事と今後についてをお話ししたいと思います。 はじめに なぜVPoEが必要なのか? なぜVPoEになったのか? 取り組んだ事 取り組めた事 2022年度 育成 オンボーディングの体制強化 組織 プロダクト単位での開発体制の改善 採用 採用のプロセス改善 2023年度 育成 オンボーディングの体制強化 評価 スキルマップの可視化 組織 プロダクト単位での開発体制の改善 採用 採用のプロセス改善 エンジニア組織の認知度向上 今後について まとめ なぜVPoEが必要なのか? VPoEとはVice President of Engineeringの略ですが、直訳すると技術の副責任者となります。 CTOが最高技術責任者なので、そのサポートをする立ち位置としてVPoEが存在すると私は考えています。 海外のIT企業では2000年代初頭からVPoE職が生まれたと聞いています。 インターネットの普及が1990年代から広がっていくに伴って、インターネットを通じたサービスを提供する企業も増えていきました。 そういった企業がビジネスを拡大していくにあたって、企業内のエンジニア社員数も増えていき組織の管理が既存のやり方だけでは難しくなっていきました。 そのような状況の中でエンジニア組織に直接責任を持つ立場としてCTOが全ての責任を背負っていましたが、CTOに求められる責務が大きくなってきたので責任範囲を分けていく事が企業で求められていった結果、VPoE職が必要となった訳です。 CTO :技術を使った意思決定においての経営責任 VPoE :技術を使った意思決定を遂行する組織への責任 と分ける事で、CTOは技術に対する経営責任に集中して業務を進める事が出来るようになり、技術選定や選定した技術を使ったプロダクトのビジネス展開における企業としての意思決定速度を上げる所に貢献しやすくなります。 VPoEはエンジニア組織への責任を担う事で組織内のパフォーマンス向上や組織間の連携向上に集中する事が出来、経営で意思決定された方針を元に組織の機動力向上に貢献しやすくなります。 私はどちらも大事な役割だと思いますし、CTOとVPoEが相互に理解しながら仕事を進めていく事が組織を運営する中で意識しなければならないポイントだと思っています。 なぜVPoEになったのか? 広義でのVPoE職の役割については書かせてもらいましたが、続いてセーフィーで私がVPoE職についた経緯をお話しします。 私が入社したのは2019年4月ですが、2021年11月までは概ね下記のような職能別組織となっていました。 エンジニア数は50名程で、各技術領域をグループでまとめながら開発をしていく体制となっていました。 そして、2021年12月から事業部制組織に移行しました。 ここで大きな課題が生まれました。 3つの事業部とカスタマーサービス本部に職能単位のエンジニアを配置しましたが、各事業部でビジネス展開したいプロダクトがある中で各事業に絞り込んで開発を進めるだけのマンパワーが足りなかったのです。 一般的なSaaS企業だとフロントエンド、バックエンドのフルスタックエンジニアが入れば事業部で求められるプロダクトに対して一気通貫でコミットする事が可能です。 ただ、セーフィーの場合はフロントエンド、バックエンドに加えてネットワークカメラを主としたデバイス領域も一気通貫で見れるフルスタックエンジニアが圧倒的に不足していました。 その結果、毎月のように事業部間でエンジニアのアサイン調整をして開発を進める事となったわけです。 本来であれば事業部制に移行する事でそれぞれの事業領域を深堀しながらニーズに合ったプロダクトを提供してビジネス拡大のスピードを上げられる想定でしたが、逆にスピードが下がる結果となりました。 組織拡大をしていく中での痛みだと捉えていますが、その痛みを解決するために自ら手上げをして2022年4月からエンジニア組織の改善に対して取り組みを始めました。 改善するプロセスとしては、今までの組織で課題になっていた事を現状整理しつつステップに分けて組織変更をする案を作って実施しました。 組織変更した結果のあるべき姿として2022年4月に考えた組織は以下となります。 プロダクトに対してはバーチャルな組織で横断して向き合いながら、各技術領域における生産性を向上するための組織として考えました。 これは短期的な計画で1年以内にはこの形にするために2022年内に3回の組織変更をしてハレーションが起きないように進めました。 合わせて事業部と連携して開発を進めるにあたっての会議体を再設計し、情報共有が滞る事なく進められるようにもしました。 関連する部署とも合意を取りながら経営陣に組織変更に対して納得してもらって進める事は結構な労力と時間が必要でしたが、無事に進める事が出来ました。 2022年11月には以下の組織に変更しました。 領域が近い技術領域を部としてまとめながら、グループで開発のアサインを調整しやすい形に変えました。 過去にやりやすかった組織でのやり方に近い形に一先ずは変えつつ、横串で組織改善をするためにエンジニアリングオフィスも新設しました。 なので、2022年11月がエンジニア組織の再出発と私の中では定義しています。 取り組んだ事 再出発した中でVPoEとして取り組むべき事を育成、評価、組織、採用のカテゴリに分けて言語化しました。 やるべき事はたくさんありますが、エンジニアリングオフィスのメンバーを少しずつ増やしながら優先順位をつけて取り組む事としました。 取り組めた事 年度ごとに取り組めた事を洗い出しながら書いていきます。 2022年度 主には組織変更に伴う調整に多くの時間を割きました。また、採用プロセスの改善で応募から最終面接に至るまでのプロセス全体を短くするためにエンジニアの最終面接を私に委譲してもらいつつ採用グループとの連携に力を入れました。 育成 オンボーディングの体制強化 各グループで代表者を選出してもらい、それぞれのグループで現在使っているオンボーディング資料を洗い出しながら、必要に応じて足りてない要素を強化するための資料を追加していけるようにしました。 2023年4月から新卒1期生を受け入れるにあたって、内定承諾者のインターン受け入れを行ってエンジニアとしての開発体験をしてもらう設計・運用をしました。 組織 プロダクト単位での開発体制の改善 事業部制から開発本部にエンジニア組織を移行するために関係部署、経営陣と合意を取りながら進めて組織変更を実施しました。移行するにあたって生産性を向上していくための会議体設計をしながら業務への影響を減らしながら進めました。 採用 採用のプロセス改善 最終面接をCEOから委譲してもらい、選考におけるプロセスを効率化しました。 採用担当とデイリーミーティングを設けながら、候補者毎でのスケジュール調整を最適化するようにしました。 2023年度 育成 オンボーディングの体制強化 2022年度に引き続き、内定承諾者のインターン受け入れを行いました。2023年度はエンジニアリングオフィスのメンバーに教育を委譲し、再現性のある体制作りに努めました。 udemyを活用して技術力向上に励む事が出来る仕組みを作り始め、希望者が自学出来るようにしました。 評価 スキルマップの可視化 テックリードやエンジニアリングマネージャの職位を定義しながら、CTOや部長と共に職位に対するランクの定義を作り開発本部内で誰でも確認出来るようにしました。 組織 プロダクト単位での開発体制の改善 エンジニアリングオフィスのメンバーの採用を行い、エンジニアリングオフィスで取り組める事を増やせるようにしました。 現在の開発体制の現状整理と改善に向けての中長期の開発本部の体制を再設計しました。 採用 採用のプロセス改善 継続して採用担当とデイリーミーティングを設けながら、エンジニアリングオフィスのグループリーダーにも入ってもらい更なるプロセスの効率化を行いました。 エンジニア組織の認知度向上 セーフィーで初となるアイディアソンの企画・運営を行い、学生の方々にセーフィーを知ってもらいつつアイディアを形にするプロセスを学ぶきっかけ作りを提供しました。 技育祭に登壇し、ネットワークカメラの将来について技術周りも含めてプレゼンテーションを行いました。結果として初めて知った企業として登壇企業の中で1位となりました。 振り返ってみると、VPoEになってから総じて採用に時間をかけている事を実感しています。 本当であればもっと育成にも時間を使いたいところではありますが、開発業務における課題にも一定の時間を使っているので年単位で取り組む業務にフォーカスして取り組んでいかなければならないという自省もあります。 今後について 2023年度までに採用に多くの時間を割いてきましたが、2024年度は特に育成にフォーカスした改善を行っていきたいと思っています。 今いるエンジニアの成長が積み重ねる事で組織の成長にもつながると私は思っています。 私自身は20数年ほど開発現場で色々な開発をしてきましたが、私が成長していくにあたって必要だった事は出来る限り言語化しながら成長に対して必要な事を各自が考えて取捨選択して成長していける組織になる事を望んでいます。 まとめ 各企業には事情の異なる課題があるのでVPoEには臨機応変な課題解決能力が必要だと思っています。 エンジニアとしてプログラムに向き合って課題解決していく経験をされていた方は組織に向き合って課題解決していく所に置き換えて考えるとVPoEという職種が理解しやすいかもしれません。 プログラム以上に不確定要素は多くありますが、VPoEという職種は今後ますます必要となりますので、興味がある方がいましたら私の体験も踏まえてお話をしますので、ぜひご連絡下さい。 最後まで読んでいただき、ありがとうございました。
こんにちは。Safieの画像認識チームでサブグループリーダーをしている柏木です。 今回は私が主担当としてAIアプリ開発に携わった、 エッジAI搭載屋外向けクラウド録画カメラ について紹介します。 プレスリリースは こちら にあります。興味のある方はご覧ください。 エッジAI搭載屋外向けクラウドカメラについて 本製品は人物検出を行うAIアプリが搭載されたAIネットワークカメラで、i-PRO(アイプロ)株式会社(以下i-PRO)と共同開発したものです。i-PRO株式会社はパナソニックの技術を引き継いで2019年に設立した会社で、ネットワークカメラやセキュリティシステムの開発と販売を行っています。 今回i-PROのカメラにセーフィー独自のファームウェアおよびアプリを搭載することで、特別仕様のカメラの開発を行いました。 エッジAI搭載屋外向けクラウドカメラについて アプリ開発上の違い 実装上の工夫 独自データセットによる学習 終わりに 開発中のカメラ こちらのカメラにはAI-App 人数カウント(旧名: Store People Detection Pack)が搭載されています。AI-App 人数カウントはSafie Oneで搭載されていたアプリであり、カメラに映る人物の検知や、UI上で設定したラインを通過する人数のカウントを行うことができます。技術的な詳細については 過去のブログ にまとまっています。本アプリに興味がある方はご覧ください。 アプリ開発上の違い i-PROのカメラにおけるAI-App 人数カウントの開発は、Safie Oneで元々搭載していたアプリを移植した形になります。カメラが変わったことによるアプリの一番大きな違いは、Object Detection(物体検出)です。以下がアプリのシステム全体の構成です。 SafieOneとi-PRO、双方ともコードの大部分はC++で書かれており、これらに違いはあまりありません。特にTrackingとEvent Publishmentはほとんどそのままのコードで動作しました。 しかし、2つの機種ではカメラに搭載しているAIチップが異なります。SafieOneではQualcomm製のチップを搭載しており、 SNPE (Snapdragon Neural Processing Engine) SDK というSDKを利用することでチップでの推論が可能となっていました。一方、i-PROではambarella社のチップを搭載しており、i-PROの提供している独自SDKを利用して開発をする必要がありました。 SNPEとi-PROのSDKで、機能面での違いはそこまで大きくないです。しかし細かい仕様の違いは結構ありました。具体的は以下の面で違いがありました。 モデル変換ツール SNPEは独自形式への変換と量子化をステップごとに実行可能 i-PROは独自形式への変換と量子化がセット モデル読み込み SNPEはモデルをポインタで読み込む i-PROはパスを指定し、ファイルを読み込む 前処理の組み込み SNPEは前処理があまり用意されていない i-PROは一部前処理を組み込み可能 実装上の工夫 今回のアプリ開発はAI-App 人数カウントの移植だったため、実装上は大部分がSafie Oneのアプリと同じものでした。そこで、アプリのコードはi-PRO用に新規作成することはせず、SafieOneと同じコードで必要な箇所だけ分岐することにしました。 こうすることで保守コストを抑えることができます。例えばTrackingアルゴリズムの改修をする際はSafie Oneとi-PROの双方で適用することができます。 AI-App 人数カウントは、Make時にカメラ機種に応じてビルドタイプを変更してビルドしております。そこで機種で共通のIDetectorという検出用のInterfaceを作成し、機種専用のYoloV5がそれを継承する実装としました(今回の検出器ではYoloV5を用いています)。機種ごとに読み込むYoloV5のコードが格納されているディレクトリを分けることで実現していまます。 こうすることで、機種ごとの違いを意識せずにDetectorを扱うことができます。 classDiagram IDetector <|.. YoloV5_SafieOne IDetector <|.. YoloV5_iPro class IDetector{ + void Detect() } class YoloV5_SafieOne{ - void preprocess + void Detect() } class YoloV5_iPro{ - void preprocess + void Detect() } 独自データセットによる学習 学習モデルは屋外向けに再学習しました。屋内をターゲットとしているSafieOneと、屋外を対象としているi-PROでは、同じアプリでも検出するシーンが異なります。SafieOneでは屋内に特化したオリジナルデータセットを作成して学習していました。以下の画像のようなものをイメージしていただければと思います。 屋内データセットのイメージ 一方で屋外では以下の画像のようなイメージです。そこで、屋外においても独自に新しく画像を収集し、データセットを作成しました。屋外では背景が屋内と比較して乱雑でシーンが多様であるため、検出がやや難しくなります。今回は、本カメラが設置されるであろうシーンをいくつか選定し、それらのシーンに注力したデータセットを作成・学習しました。 屋外データセットのイメージ( APhoto より) 屋内データセット、屋外データセットそれぞれに対して、屋内用モデル・屋外用モデルで評価を行いました。PR曲線などの評価指標では大きな改善となりました。以下に結果を載せておきます。 屋内データセットに対するPR曲線 屋外データセットに対するPR曲線 屋内データセットでは大きな劣化はなく、屋外データセットにおける精度向上が見込めました。 もちろん評価ではPR曲線のような物体検出の評価だけではなく、設定したラインやエリアを通過する人数のカウント精度や人検知精度など、アプリのEnd-to-Endな評価も行っております。 今後は実際に製品を使っていただいたお客様の声や、データをもとにさらなる精度改善を行う予定です。 終わりに 屋外向けAIカメラの開発について紹介しました。複数のカメラを扱っている、セーフィーならではのEdgeAIの開発の話をしたつもりです。これをきっかけに興味を持っていただければ嬉しいです。 セーフィーではエンジニアを積極的に募集しています。どのような職種があるのか気になる方は こちら をご覧ください。カジュアル面談から受け付けておりますので、気軽に応募いただければと思います。
こんにちは。セーフィーでセキュリティマネジメント業務をしている川部です。 今回はサービス障害ハンドリングについて記載したいと思います。 サービス障害とセキュリティ?と疑問に思った方もいるかもしれません。 セキュリティの基本的な考えにあるCIAの可用性の観点から、セキュリティマネジメント業務の一環としてサービス障害発生時の取りまとめを担当しています。 主な業務はエンジニアと協力し、事象の詳細確認や影響するお客様の洗い出し、対外的な対応など多義にわたります。 今回はサービス障害ハンドリングにおける工夫という観点でお話したいと思いますのでSaaS企業で障害対応される皆さんや障害対応を任された皆さんの一助になれば幸いです。 セーフィーを取り巻く環境 スピード感をもって対応するための施策 サービス品質の維持管理するための施策 セーフィーを取り巻く環境 セーフィーでは現時点(2023年6月)で20.7万台のクラウドカメラが日々お客様の元で稼働している状況です。業界のシェア約56%であるため、広範囲な障害が発生した際は非常に多くのお客様に影響があるサービスです。 また当社の製品は様々な業界でご利用いただいており、防犯用途等で使用いただいてるお客様も少なくありません。 そのため、障害によってはお客様の目的が達成できないばかりか、お客様のビジネスにクリティカルな影響を与えてしまう可能性もあり、障害発生時の現場の緊張感は非常に高くなります。 一方で同じ商品でもBtoB向け、BtoC向けの商流があり、それぞれ障害対応時の観点も異なります。 BtoC向けであれば弊社のサポートを通して、お客様をサポートできるように社内への展開する情報を平準化しつつ、スムーズに情報展開する必要があります。 BtoB向けであれば、弊社がOEMとなり、パートナー企業様を通してサービスを提供しているため、お客様への情報展開が直販に比べ遅くなります。 そのため情報伝達のスピードが非常に重要になりますし、お客様の混乱を回避するため、パートナー企業様と発信内容を統一する必要があったりと気にすべきことはたくさんあります。 そのような背景の中、障害をハンドリングするわけですが、今回はハンドリングを行う上での大きな工夫2点をお伝えします。 スピード感をもって対応するための施策 一つ目は「スピード感をもって対応するための施策」です。 セーフィーでは障害報告が社内で上がった後、その障害の重要度を判断し、対応のスピードや対外報告の範囲を決定します。 内容によってはホームページや自社アプリ上で情報を展開する必要がありますし、影響範囲が小さければ個別に対応するなど重要度に応じてその後の工程を変えています。 この重要度判断に基づき、各部門が対応に向けて動き出します。なのでいち早く重要度を決定することがその後工程をスムーズにつなげるコツになります。 ただし、重要度を判断するには発生した障害事象を理解しながら影響範囲等を見定める必要があります。 一般的に判断と聞くと定量的な指標に基づき、判断するのだろうと思われるかと思います。もちろんそれも間違ってないです。 ただし障害はもともと想定していないもの(想定できるものは上流工程で潰されている前提ですが・・・)なので、発生事象によっては定量的な数字がスピーディーに取得できないものや定量的な指標で影響を判断できない事象等も考えられます。 そのため弊社では重要度判断基準として定量的、定性的な指標それぞれを設けて運用することでスムーズに判断できるようにしています。 サービス品質の維持管理するための施策 2つ目の工夫は障害対応が終わった後の話です。 障害発生後、暫定的に障害の封じ込めを行い、恒久対策を施し、より高い品質維持できるように工夫しています。 そのため非常にベタではありますが、障害を履歴として管理しており、遡って確認することはもちろん、発生した障害箇所により恒久的な対策やその日程感などを管理しています。 また障害の重要度に応じて対策に対しても各部門やCTOや経営層の確認を必須とする仕組みを整え、運用することで効率的かつサービス品質向上にもつながるように管理しています。 サービス障害はネガティブな事象と捉えることもできますが、サービスをより良いものにできるきっかけでもあるので、そういった意識をもってハンドリングにあたっています。 以上セーフィーにおけるサービス障害ハンドリングの工夫でした。 同じように障害対応で旗振りをしている方や障害対応について気になっていた方など何かの参考になればと思います。
​データ分析基盤グループでデータエンジニアをしている平川です。 DataVaultに関する記事の第3回目となります。 第3回の記事は、DataVaultモデリングをしている際に困った状態の対処方法についてまとめていきます 第1回: DataVaultってなに?どんな特徴があるの? 第2回: automate_dvを使ってDataVaultモデリングの中心となるテーブルを作ってみてわかったこと  第3回: BusinessVault、発展的なSatelliteテーブルやキーがNullだった場合の対処方法についてなど ← 今回はここ   前回のおさらい はじめに BusinessVaultとは StatusTrackingSatellite, EffectivitySatelliteの紹介 Status Tracking Satelliteとは Status Tracking Satelliteのテーブル構造 Effectivity Satelliteとは Effectivity Satelliteの構造 StatusTrackingSatelliteとEffectivity Satelliteの使い分け ゴーストレコード、ヌルキーとゼロキー ゴーストレコードとは ゼロキー(≒ ヌルキー)とは これらの概念を取り入れるメリット まとめ 参考資料 前回のおさらい 前回は、DataVaultモデリングの実装を簡易化するパッケージであるautomate_dvの紹介をさせていただきました。 特にDataVaultモデリングの中心となるHub/Link/Satelliteの生成について紹介させていただきました。 はじめに 今回の記事では、以下3つのトピックについて紹介させていただきます。 Hub/Link/Satelliteを利用する際に出くわす課題への対処法の1つとしてBusinessVaultについて 通常のSatelliteに追加機能を持たせたStatusTrackingSatelliteとEffectivitySatelliteについて ソーステーブルのキーの欠損などに対応するゼロキーやゴーストレコードについて BusinessVaultとは BusinessVaultとはRawVaultのデータを使いやすく整理し、特定のビジネスルールや変換処理を適用したモデルです。 この層は、RawVault層とユーザーが直接アクセスする層の間に位置しています。 このモデルを作るメリット・デメリットはいくつかあります。 メリット ビジネスロジックやデータ変換ルールを集約することで、Rawデータをルールに従った形で整理できます。これにより、分析が容易になることが考えられます。 多数のJOINや複雑な条件を含むクエリを事前に処理することで、パフォーマンス面での向上が見込めます デメリット BusinessVaultを生成する際にしばしば非正規化をするのでデータが冗長になります 適用するビジネスルールが頻繁に変わる場合、管理が困難になる可能性があります StatusTrackingSatellite, EffectivitySatelliteの紹介 Status Tracking Satelliteとは StatusTrackingSatelliteは、CDCが作用しない場合のデータの状態を追跡・管理する際にとても役に立ちます。 データソースでCDCが作用しない場合、ステータスの追跡が重要となり、ステータスの追跡をしない場合、いつレコードが消えたのかをSatelliteで把握するのは非常に困難です。 CDCが作用しない場合にはこのテーブルは非常に有用ですが、StatusTrackingSatelliteから必要なデータを取得するのは通常のSatelliteと比べるとSQLが複雑になります。 Status Tracking Satelliteのテーブル構造 Satelliteのテーブル構造とほぼ一緒ですが、record_statusというカラムを追加します。このカラムは名前の通りですが、レコードの状態を表し以下の3つのステータスを持ちます。 挿入("I"): 新しく追加されるレコードはrecord_status = 'I'としてレコードが追加されます。 更新("U"): 既存のレコードの属性情報が変わった際は、record_status = 'U'のレコードが追加されます 削除("D"): Satelliteテーブルには存在して、データソースに存在しないレコードには、record_status = 'D'としてレコードが追加されます Effectivity Satelliteとは Effectivity Satelliteはビジネスキー間の開始日と終了日を保持することで、有効期間を追跡することが可能になります。 Effectivity Satelliteの構造 StatusTrackingSatellite同様に、Satelliteテーブルが持つ基本的なカラムを持ちます。 Effectivity Satelliteには、開始日と終了日が追加されます。(automate-dvのeff_satではEFFECTIVE_FROMも追加されるようです) それぞれのカラムには以下のような値を格納します。 - 開始日: ビジネスキー間の関係性が開始した日付。 - 終了日: ビジネスキー間の関係性が終了した日付。関係性が続いている場合は、9999-12-31のように未来日が格納される。 StatusTrackingSatelliteとEffectivity Satelliteの使い分け データソースからレコードが削除された場合も、終了日を取っていれば対応できますが、StatusTrackingSatelliteとEffectivitySatelliteの違いは、追跡する対象が異なる点にあります。 前者はデータ自体の時間変化を追跡しますが、後者はビジネスキー同士の関係の有効期間を追跡します。 期間に関するデータを保持したい際には、EffectivitySatelliteが有用であると言えます。 例えば、EffectivitySatelliteを活用してキャンペーン情報と製品情報の関係性を保持することにより、売上情報との組み合わせで特定のキャンペーンがどのような影響を与えていたかを期間を絞って分析することが可能になります。 Effectivity Satelliteは終了日を更新する必要があるため、頻繁に終了日に値が入る場合はその都度updateされることになります。 両者で共通しているのは、データを抽出する際のクエリが複雑になる点です。(期間の指定やrecord_statusの絞り込みが必要になります) ゴーストレコード、ヌルキーとゼロキー ゴーストレコードとは データ(レコード)が存在しない場合に利用される仮のレコードです。このレコードを追加することで、各種モデルを結合するときのクエリを単純化することが可能になります。 ゼロキー(≒ ヌルキー)とは ビジネスキーが不明瞭であったり、欠落している場合に用いられる概念です。このキーを導入することで、データの品質を理解する手助けをし、品質改善の対策をする際に用いることが可能になります。 また、ゼロキーの概念を取り入れていない場合もしくはautomate-dvを使用してハッシュ化している場合、ハッシュ化したキーの値がnullになります。 これらの概念を取り入れるメリット データボルトモデリングをしているデータレイヤーの整合性や一貫性を保つことができます。 特にキーに欠落が発生する場合はゼロキーの概念を取り入れるべきだと思います。 ゴーストレコードについては、一時的にレコードが生成されていないという状況(例えば不具合等でデータが遅れて連携されるなど)であれば導入しなくても良いかなと思います。 まとめ 今回の記事では、DataVaultモデリングをする際に特定の場面で有効である概念の紹介をさせていただきました。 今回紹介したSatelliteを派生させたものや、キーなどがゴーストレコード、ゼロキー等である場合の対処方法は登場頻度は少ないかもしれないですが、データソースの状態や構造を直接的に変更することは難しいので、そのような時にこれらの概念を適用することでDataVaultモデリングの整合性を保ったままモデリングをすることが可能になります。 参考資料 The Data Vault Guru: a pragmatic guide on building a data vault automate_dvドキュメントページ
サーバーサイドエンジニアの三村です。 弊社では2024年の初めから国外へサービス展開をする準備として、一年ほど前からシステムの国際化対応を行ってきました。 この準備には、サービスの多言語対応や日本標準時以外のタイムゾーンでサービスが利用できるようにする改修などが含まれますが、サーバーチームでは特に後者に苦労しました。 そこでこの改修で得たPythonでタイムゾーンを扱う際の知見の一部を、このブログ記事にまとめます。 タイムゾーンについて タイムゾーンの基準 UTCオフセットだけでタイムゾーンを扱う問題点 サマータイム 特定タイムゾーンでのUTCオフセット変更 IANAのTime Zone Database (余談) Time Zone Databaseから読み取る世界の歴史 Pythonでの実装の注意点 pytzをdatetimeオブジェクトのコンストラクタに渡す pytzオブジェクトをdatetimeのコンストラクタに渡してもうまく動かない理由 zoneinfoファイルの「1901年問題」 strftime(%s)でのエポック秒変換 まとめ タイムゾーンについて Pythonでの実装の話に入る前に、まずはタイムゾーンの概念自体について説明します。 タイムゾーンの基準 世界中の各地域は異なる標準時間を用いていて、地域間に時差があることは言うまでもないと思います。 そしてこれらの異なる時間帯の基準となる時刻が存在することも有名かと思います。 こちらの基準時刻は、イギリスのグリニッジ天文台における時間を基に定められていると、昔学校で習った方も多いと思います。 経度0度の地点にある同天文台を基準時間とする考えは、GMT (Greenwich Mean Time)と呼ばれるものであり、現在こちらはUTC (Coordinated Universal Time)という概念に取って代わられています。 慣習的に同一のものと扱われることもある両者ですが、厳密には概念的、算出方法的な違いがあります。 GMTはグリニッジ天文台での地方平均時を表します。つまりGMTは特定地域での天体観測を基に算出される、ロンドンあたりを指す一つのタイムゾーンです。 比べてUTCは、天体観測だけではなく原子時計に基づく計算などから算出される基準時で、これ自体はどこかの地域のタイムゾーンではないです。地球上の地域の標準時を、この基準時との差分で表現するために存在しています。 UTCオフセットだけでタイムゾーンを扱う問題点 UTCのような基準となる時間の概念が存在すれば、それとのオフセット(基準との差異)のみを用いてシステムで世界中の時間を取り扱えそうだと一見思えます。しかし、実際はそんな甘くはないです。以下に、特定地域の人間の用いる時間の概念が単一のUTCオフセットでは表せないことの例を挙げます。 サマータイム 一部のタイムゾーンでは、一年を通して同じUTCオフセットの時間を用いているわけではなく、日照時間が長い時期にはサマータイムとして時間を早めることが行われています。 加えて、サマータイムの開始・終了時期も毎年一定とは限らず、頻繁に変更される地域もあります。 (例:1998年のブラジルでは、 サマータイムの開始日をローマ教皇の来訪時期とずらすため一日遅らせました。 ) 特定タイムゾーンでのUTCオフセット変更 歴史を遡れば、地球上の多くの地点で採用している標準時間の変更が行われてきました。 (例:サモアでは、 2011年にタイムゾーンをUTC-11からUTC+13に変更しました。 ) 後述する通り、このような標準時間の変更を経験した地域には、日本も含まれます。 IANAのTime Zone Database 上述の通り、人々が生活で用いている時間の概念は、季節的や歴史的な理由から単純なUTCオフセット一つでは表現しきれない場合があります。 そこで、このような一般的に使われるタイムゾーンを、季節的な変化や歴史的な経緯も包括してまとめているのが、Time Zone Databaseです。 www.iana.org Asia/Tokyo などのよく見かけるタイムゾーンの表記は、IANAによって編纂されているこちらのデータベースによって規定されています。 こちらのデータベースの情報は、プログラムからはいろいろな環境で利用できます。Unix likeなシステムであれば /usr/share/zoneinfo/ にバイナリ形式で入っているほか、Pythonの実行環境から参照したい場合は tzdataというモジュール を用いることでOSによらず利用できます。 こちらの中身を確認する一番簡単な方法は、上記のIANAのリンクを開き、データベースのファイルをダウンロードし解凍し開くことです。試しにニューヨークのタイムゾーン情報を開いてみると、以下のようになっています。 # Monthly Notices of the Royal Astronomical Society 44, 4 (1884-02-08), 208 # says that New York City Hall time was 3 minutes 58.4 seconds fast of # Eastern time (i.e., -4:56:01.6) just before the 1883 switch. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule NYC 1920 only - Mar lastSun 2 : 00 1 : 00 D Rule NYC 1920 only - Oct lastSun 2 : 00 0 S Rule NYC 1921 1966 - Apr lastSun 2 : 00 1 : 00 D Rule NYC 1921 1954 - Sep lastSun 2 : 00 0 S Rule NYC 1955 1966 - Oct lastSun 2 : 00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF -4:56:01.6 Zone America/New_York - 4 : 56 : 02 - LMT 1883 Nov 18 17 :00u - 5 : 00 US E%sT 1920 - 5 : 00 NYC E%sT 1942 - 5 : 00 US E%sT 1946 - 5 : 00 NYC E%sT 1967 - 5 : 00 US E%sT 最初にコメントで、1883年の標準時間の変更までは東海岸時間より3分58.4秒早かったとする資料の紹介がされています。 その後過去のサマータイムの実施の歴史や、コメントで書かれていた1883年以前の時間の現在との時差についてが記述されています。 (余談) Time Zone Databaseから読み取る世界の歴史 上記のニューヨークの例の通り、Time Zone Databaseは単なる標準時の変化等を考慮した時刻変換のルールが記載されているのみでなく、豊富なコメントもあり読み物としても面白いです。日本の例を見てみます。 # From Paul Eggert (1995-03-06): # Today's _Asahi Evening News_ (page 4) reports that Japan had # daylight saving between 1948 and 1951, but "the system was discontinued # because the public believed it would lead to longer working hours." # From Mayumi Negishi in the 2005-08-10 Japan Times: # http://www.japantimes.co.jp/cgi-bin/getarticle.pl5?nn20050810f2.htm # Occupation authorities imposed daylight-saving time on Japan on # [1948-05-01].... But lack of prior debate and the execution of # daylight-saving time just three days after the bill was passed generated # deep hatred of the concept.... The Diet unceremoniously passed a bill to # dump the unpopular system in October 1951, less than a month after the San # Francisco Peace Treaty was signed. (A government poll in 1951 showed 53% # of the Japanese wanted to scrap daylight-saving time, as opposed to 30% who # wanted to keep it.) # -- 中略 -- # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Japan 1948 only - May Sat>= 1 24 : 00 1 : 00 D Rule Japan 1948 1951 - Sep Sat>= 8 25 : 00 0 S Rule Japan 1949 only - Apr Sat>= 1 24 : 00 1 : 00 D Rule Japan 1950 1951 - May Sat>= 1 24 : 00 1 : 00 D # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Tokyo 9 : 18 : 59 - LMT 1887 Dec 31 15 :00u 9 : 00 Japan J%sT コメント及び Rule の項には、日本が過去に実施していたサマータイムの情報が載っています。終戦直後日本ではGHQの指導によりタイムゾーンを導入していたようですが、人々はサマータイムによって労働時間を伸ばされるのではないかと不安になり大変不人気であったため、サンフランシスコ平和条約締結後日本が主権を回復すると、すぐにサマータイムは廃止されたそうです。 ちなみに Zone の項目にある通り、日本は1888年の正月より前には今より 18分59秒 早い標準時間を用いていました(東京の地方平均時)。 今までプログラムで日本時間を扱うコードを書いた際に、タイムゾーン取り扱いの不備で意図した時刻よりも 18分59秒 ずれた時刻となるバグを起こすなどしてこの時間を見たこともあるかと思いますが、この時間はここからきています(詳しい例は後述します)。 Pythonでの実装の注意点 やっとPythonの話に入ります。 Pythonでtimezone awareな形でdatetimeオブジェクトを取り扱う際に陥りそうな誤りを、いくつか挙げてみます。 pytzをdatetimeオブジェクトのコンストラクタに渡す Pythonにおけるタイムゾーン関連のよくある間違いでは一番有名な話かもしれませんが、 pytz 型のタイムゾーンをdatetimeオブジェクトのコンストラクタに渡すと、大抵の場合意図しないdatetimeオブジェクトが作成されます。 >>> import datetime >>> import pytz >>> datetime.datetime( 2023 , 8 , 25 , 12 , 0 , 0 , tzinfo=pytz.timezone( 'Asia/Tokyo' )).utcoffset() datetime.timedelta(seconds= 33540 ) # ↑9時間19分 上記のようなコードを書いた場合、実装者は大抵の場合UTCオフセットが9時間ちょうどのタイムゾーンのオブジェクトの生成を期待していると思いますが、実際には9時間19分となっています。 pytzはdatetimeオブジェクトのコンストラクタに渡されるような利用方法は想定されておらず(詳しくは後述)、このような利用方法ができないことが ドキュメントに記載されています。 This library differs from the documented Python API for tzinfo implementations; if you want to create local wallclock times you need to use the localize() method documented in this document. 上記引用にある通り、pytzでdatetimeオブジェクトにタイムゾーン情報を付与したい場合は、 localize() を用いるのが正解です。 >>> pytz.timezone( 'Asia/Tokyo' ).localize(datetime.datetime( 2023 , 8 , 25 , 12 , 0 , 0 )).utcoffset() datetime.timedelta(seconds= 32400 ) # ↑9時間ちょうど ちなみに、pytzをdatetimeオブジェクトのコンストラクタに渡す際のバグは上に例示しましたが、これはコンストラクタだけでなく replace() の引数として渡しても同様の事象は起こります。 >>> import datetime >>> import pytz # まずはtimezone unawareなdatetimeオブジェクトを作成 >>> d = datetime.datetime( 2023 , 8 , 25 , 12 , 0 , 0 ) >>> d datetime.datetime( 2023 , 8 , 25 , 12 , 0 ) # これにreplace()でtimezone情報をつけると、やはり19分ずれる >>> d.replace(tzinfo=pytz.timezone( 'Asia/Tokyo' )).utcoffset() datetime.timedelta(seconds= 33540 ) # ↑9時間19分 こちらの理由は単純で、datetimeクラスの replace() は内部で新規にオブジェクトの生成を行っていて、その際にコンストラクタの引数に replace() の引数をそのまま渡しているためです。 github.com def replace (self, year= None , month= None , day= None , hour= None , minute= None , second= None , microsecond= None , tzinfo= True , *, fold= None ): """Return a new datetime with new values for the specified fields.""" if year is None : year = self.year if month is None : month = self.month if day is None : day = self.day if hour is None : hour = self.hour if minute is None : minute = self.minute if second is None : second = self.second if microsecond is None : microsecond = self.microsecond if tzinfo is True : tzinfo = self.tzinfo if fold is None : fold = self.fold return type (self)(year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold) 最終行で、新たにオブジェクトを作成しそのコンストラクタの中でtzinfoも渡しています。 pytzオブジェクトをdatetimeのコンストラクタに渡してもうまく動かない理由 pytzのこの挙動はPython界隈では結構有名ですが、なぜこのような挙動になっているのか説明しているブログ記事等は、(自分のなけなしのリサーチ能力だと)見当たりませんでした。そこで、少し自分で実装を調べてみました。 まず、Pythonのdatetimeのドキュメントには以下のような記述がありました。 docs.python.org For applications requiring aware objects, datetime and time objects have an optional time zone information attribute, tzinfo, that can be set to an instance of a subclass of the abstract tzinfo class. These tzinfo objects capture information about the offset from UTC time, the time zone name, and whether daylight saving time is in effect. Only one concrete tzinfo class, the timezone class, is supplied by the datetime module. The timezone class can represent simple timezones with fixed offsets from UTC, such as UTC itself or North American EST and EDT timezones. Supporting timezones at deeper levels of detail is up to the application. The rules for time adjustment across the world are more political than rational, change frequently, and there is no standard suitable for every application aside from UTC. Pythonの標準ライブラリであるdatetimeでは、タイムゾーン情報としては tzinfo という抽象クラス *1 と、その実装として timezone というクラスを用意していますが、こちらの実装はUTCオフセットが固定である前提となっています。UTCオフセットが歴史的経緯などで変わってくるような、実際の時間の概念に近いものが使いたければ、同ライブラリを利用するアプリケーション側で tzinfo をよしなに実装してどうにかしてね、という意図のようです。 こちらのtzinfoのクラスですが、実装の際にUTCオフセットをいい感じに対応するには utcoffset() というメソッドを上書きしてほしいそうです。 class tzinfo : """Abstract base class for time zone info classes. Subclasses must override the tzname(), utcoffset() and dst() methods. """ __slots__ = () def tzname (self, dt): "datetime -> string name of time zone." raise NotImplementedError ( "tzinfo subclass must override tzname()" ) def utcoffset (self, dt): "datetime -> timedelta, positive for east of UTC, negative for west of UTC" raise NotImplementedError ( "tzinfo subclass must override utcoffset()" ) def dst (self, dt): """datetime -> DST offset as timedelta, positive for east of UTC. Return 0 if DST not in effect. utcoffset() must include the DST offset. """ raise NotImplementedError ( "tzinfo subclass must override dst()" ) # 以下略 次に、datetimeオブジェクトのコンストラクタに渡しても正しく動く、 dateutil.tz の実装ではこの utcoffset() をどう実装しているのかみてみます。 github.com def utcoffset (self, dt): if dt is None : return None if not self._ttinfo_std: return ZERO return self._find_ttinfo(dt).delta # 中略 ↓こちらは上の関数から呼ばれている def _find_ttinfo (self, dt): idx = self._resolve_ambiguous_time(dt) return self._get_ttinfo(idx) # 中略 ↓こちらは上の関数から呼ばれている def _resolve_ambiguous_time (self, dt): idx = self._find_last_transition(dt) # If we have no transitions, return the index _fold = self._fold(dt) if idx is None or idx == 0 : return idx # If it's ambiguous and we're in a fold, shift to a different index. idx_offset = int ( not _fold and self.is_ambiguous(dt, idx)) return idx - idx_offset 上のコードでは、 utcoffset() が呼ばれた際には毎回、内部で標準時間の変遷の歴史を参照する関数を呼び出し、その結果を参考に採用すべきUTCとのオフセットを導き出しています。このことから、dateutil.tzでは utcoffset() が呼び出されると、その都度標準時間の遍歴を考慮してUTCオフセットの判定をしていることがわかります。 比べて、コンストラクタに渡すとうまく動かないpytzの方の実装を見てみます。 github.com def utcoffset (self, dt, is_dst= None ): # 関数のコメントは長いので略 if dt is None : return None elif dt.tzinfo is not self: dt = self.localize(dt, is_dst) return dt.tzinfo._utcoffset else : return self._utcoffset こちらは条件分岐によっては、dateutil.tzでの場合のようなUTCオフセットの決定のための複雑な計算はせずに、事前にクラスにセットされた _utcoffset の値を単純に返すのみになります。基本的にdatetimeオブジェクトが生成されてからは utcoffset() が呼ばれる際には、上記コードの条件分岐一番最後の else 節に入るようであったので、pytzはインスタンス作成時に設定されたUTCオフセットを返し続ける挙動になっていそうです。 上記の実装2例からは、dateutil.tzの方はUTCオフセットを参照された場合は毎回引数のdatetimeオブジェクトの日付を確認してUTCオフセットを決定していて、pytzの方は一度UTCオフセットの値が設定されたらそれを返し続ける、という違いが見て取れます。 datetimeオブジェクトのコンストラクタにタイムゾーンを渡すユースケースの場合、先に引数として渡すために作成されるタイムゾーンオブジェクトは作成予定のdatetimeオブジェクトを読み取れないので、どのような tzinfo の実装であれ一旦は正確にはUTCオフセットを決められない状態になります。dateutil.tzの方はその後改めてdatetimeオブジェクトの日付を確認してUTCオフセットを再計算するタイミングがあるのに対し、pytzの場合はそれがなさそうです。 このことから、pytzオブジェクトをdatetimeオブジェクトのコンストラクタに渡すと、UTCオフセットが不明な状態で記録された該当タイムゾーンでの最も古いUTCオフセットの情報を保持し続け *2 、上述のような挙動が起こると思われます。 zoneinfoファイルの「1901年問題」 上で、Time Zone Databseの情報はUnix likeな環境では /usr/share/zoneinfo 以下に同情報が入っていると書きました。 このファイルを用いてタイムゾーンを扱う際の意図しない挙動を紹介します。 一部の環境 *3 で以下のようなコードを実行すると、意図した時間よりも18分59秒ずれることになります。 >>> import datetime >>> import dateutil.tz # 年月日はなしに時間のみを指定してdatetimeオブジェクトを作成し、それにdateutil.tzでAsia/Tokyoのタイムゾーン情報を付与し、UTCオフセットを取得 >> datetime.datetime.strptime( '12:00' , '%H:%M' ).replace(tzinfo=dateutil.tz.gettz( 'Asia/Tokyo' )).utcoffset() datetime.timedelta(seconds= 33539 ) # UTCオフセットは9時間(32400秒)を期待しているのに、9時間18分59秒(33539秒)となった (上のコードはそもそも、日付のない時間のデータを扱うのにdatetime.timeではなく余計に情報量の多いdatetime.datetimeを用いているところが本質的な間違いですが、そこには目を瞑ってください。) こちらは、datetimeオブジェクトで時間のみを指定すると年月日は1900年1月1日となること、一部の環境でTime Zone Databaseの情報が載っているバイナリのファイルで日付データを32ビットまでしか扱えていないこと、の二つの要因が重なってこのような挙動になっています。 前者については、1900年1月1日時点では既に日本では今と同じ標準時間が採用されている状態であったため、こちら単体ではUTCオフセットは9時間18分59秒とはならないはずです。 後者については、32ビットで扱える時間の範囲を超えるとコンピュータが誤動作すると言われる「2038年問題」と同源です。32bitの秒数でエポック時間から負の方向に向かって表現できる限界は1901年の12月13日であり、datetimeオブジェクトに自動でつけられた1900年1月1日はこれよりも古いため、一部環境でUTCオフセットの計算がうまくいかなっているようです。 上記の事象が再現する環境で参照されているタイムゾーンの情報のファイルを、 RFC8536での仕様 をもとにバイナリを読んでみます。 $ hexdump -C /usr/share/zoneinfo/Asia/Tokyo 00000000 54 5a 69 66 32 00 00 00 00 00 00 00 00 00 00 00 |TZif2...........| 00000010 00 00 00 00 00 00 00 04 00 00 00 04 00 00 00 00 |................| 00000020 00 00 00 09 00 00 00 04 00 00 00 0c 80 00 00 00 |................| 00000030 d7 3e 02 70 d7 ed 59 f0 d8 f8 fa 70 d9 cd 3b f0 |.>.p..Y....p..;.| 00000040 db 07 00 f0 db ad 1d f0 dc e6 e2 f0 dd 8c ff f0 |................| 00000050 03 01 02 01 02 01 02 01 02 00 00 83 03 00 00 00 |................| 00000060 00 8c a0 01 04 00 00 7e 90 00 08 00 00 7e 90 00 |.......~.....~..| 00000070 08 4c 4d 54 00 4a 44 54 00 4a 53 54 00 00 00 00 |.LMT.JDT.JST....| 00000080 01 00 00 00 01 54 5a 69 66 32 00 00 00 00 00 00 |.....TZif2......| 00000090 00 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 |................| 000000a0 04 00 00 00 00 00 00 00 0a 00 00 00 04 00 00 00 |................| 000000b0 0c f8 00 00 00 00 00 00 00 ff ff ff ff 65 c2 a4 |.............e..| 000000c0 70 ff ff ff ff d7 3e 02 70 ff ff ff ff d7 ed 59 |p.....>.p......Y| 000000d0 f0 ff ff ff ff d8 f8 fa 70 ff ff ff ff d9 cd 3b |........p......;| 000000e0 f0 ff ff ff ff db 07 00 f0 ff ff ff ff db ad 1d |................| 000000f0 f0 ff ff ff ff dc e6 e2 f0 ff ff ff ff dd 8c ff |................| 00000100 f0 00 03 01 02 01 02 01 02 01 02 00 00 83 03 00 |................| 00000110 00 00 00 8c a0 01 04 00 00 7e 90 00 08 00 00 7e |.........~.....~| 00000120 90 00 08 4c 4d 54 00 4a 44 54 00 4a 53 54 00 00 |...LMT.JDT.JST..| 00000130 00 00 01 00 00 00 01 0a 4a 53 54 2d 39 0a |........JST-9.| 0000013e 下記画像に説明を書いた通り、本ファイルにはUTCオフセットが今よりも18分59秒ずれていた時期のルールが記載されていますが、そちらが採用されていた最後の時間にあたる部分に、符号付き32bitのintergerの最小値である 80 00 00 00 (-2147483648)が入っています。こちらをUNIX timeとして読み取ると1901年12月13日中の時間になるため、それ以前の時間はこのファイルを参照する限りUTCオフセットが9時間18分59秒と扱われてしまいます。(実際にこのUTCオフセットの標準時間が利用されていたのは1887年末までなので、およそ14年分の日本時間は正しく変換できないことになります。) 逆に、この現象が起きない筆者のMac上の同じファイルを見てみると、この符号付き32bit整数ではUNIX timeとして扱えない時期のデータは、省略されていることがわかりました。 % hexdump -C /usr/share/zoneinfo/Asia/Tokyo 00000000 54 5a 69 66 32 00 00 00 00 00 00 00 00 00 00 00 |TZif2...........| 00000010 00 00 00 00 00 00 00 03 00 00 00 03 00 00 00 00 |................| 00000020 00 00 00 08 00 00 00 03 00 00 00 08 d7 3e 02 70 |.............>.p| 00000030 d7 ed 59 f0 d8 f8 fa 70 d9 cd 3b f0 db 07 00 f0 |..Y....p..;.....| 00000040 db ad 1d f0 dc e6 e2 f0 dd 8c ff f0 00 01 00 01 |................| 00000050 00 01 00 01 00 00 8c a0 01 00 00 00 7e 90 00 04 |............~...| 00000060 00 00 7e 90 00 04 4a 44 54 00 4a 53 54 00 00 00 |..~...JDT.JST...| 00000070 01 00 00 01 54 5a 69 66 32 00 00 00 00 00 00 00 |....TZif2.......| 00000080 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 04 |................| 00000090 00 00 00 00 00 00 00 09 00 00 00 04 00 00 00 0c |................| 000000a0 ff ff ff ff 65 c2 a4 70 ff ff ff ff d7 3e 02 70 |....e..p.....>.p| 000000b0 ff ff ff ff d7 ed 59 f0 ff ff ff ff d8 f8 fa 70 |......Y........p| 000000c0 ff ff ff ff d9 cd 3b f0 ff ff ff ff db 07 00 f0 |......;.........| 000000d0 ff ff ff ff db ad 1d f0 ff ff ff ff dc e6 e2 f0 |................| 000000e0 ff ff ff ff dd 8c ff f0 03 01 02 01 02 01 02 01 |................| 000000f0 02 00 00 83 03 00 00 00 00 8c a0 01 04 00 00 7e |...............~| 00000100 90 00 08 00 00 7e 90 00 08 4c 4d 54 00 4a 44 54 |.....~...LMT.JDT| 00000110 00 4a 53 54 00 00 00 00 01 00 00 00 01 0a 4a 53 |.JST..........JS| 00000120 54 2d 39 0a |T-9.| 00000124 データの個数が問題の起こる環境と比べ一つ減っていることと、9時間18分59秒のUTCオフセットを表す 83 03 (09:18:59を秒数にした33540の16進数表記)という値が(時間の変換ルールの項目からは)見当たらないことがわかります。 これによって1888年1月1日から1901年12月13日までの日本の日付を正しく扱えるようになりますが、逆に1887年末以前のデータは実際の当時の標準時とはずれた時間で取り扱われます。 # 1887年末以前の標準時がzoneinfoファイルに記録されていない、筆者のMacで実行 >>> datetime.datetime( 1887 , 1 , 1 , 0 , 0 , 0 , tzinfo=dateutil.tz.gettz( "Asia/Tokyo" )).utcoffset() datetime.timedelta(seconds= 32400 ) # ↑実際は33540(9時間18分59秒)であるべきが、32400(9時間)となってしまっている 何らかの理由で1887年以前の日本の日付データを扱いたい場合は、こちらの挙動に気をつける必要がありそうです。 strftime(%s)でのエポック秒変換 datetimeオブジェクトをエポック秒に変換したい場合に、 strftime(%s) を用いることでこれが実現できると書いてある記事がちらほら存在します。 しかしこちらは Pythonのdatetimeのドキュメント ではサポートされているとは一切書いておらず、非推奨です。 # 日本時間で動く環境です >>> import time >>> time.tzname ( 'JST' , 'JST' ) >>> import datetime >>> import dateutil.tz # 現在時刻を取得した後、それにニューヨークのタイムゾーンを付与します >>> d = datetime.datetime.now().astimezone(dateutil.tz.gettz( 'America/New_York' )) >>> d datetime.datetime( 2024 , 2 , 8 , 1 , 41 , 57 , 844204 , tzinfo=tzfile( '/usr/share/zoneinfo/America/New_York' )) # strftime(%s)を用いてエポック秒に変換 >>> epoch_s = d.strftime( "%s" ) >>> epoch_s '1707324117' # これをdatetimeに変換し再びニューヨークのタイムゾーンをつけると、もとより14時間ずれていることがわかります >>> datetime.datetime.fromtimestamp( int (epoch_s)).astimezone(dateutil.tz.gettz( 'America/New_York' )) datetime.datetime( 2024 , 2 , 7 , 11 , 41 , 57 , tzinfo=tzfile( '/usr/share/zoneinfo/America/New_York' )) # こちらはエポック秒変換の際に、ニューヨークでの時間としてではなく日本時間として変換されたためです 上記のコード例の通り、 strftime(%s) だと変換元のdatetimeオブジェクトがtimezone awareであったとしても、変換時にはそのタイムゾーン情報ではなく、実行環境のタイムゾーン情報を参照してしまいます。 (日本時間で動く環境で、日本時間の時刻データのみを扱っていると気づかずにこのような実装が紛れてしまうかもしれませんが、どちらか一方が日本時間以外となるとこちらは不具合を起こします。) こちらdatetimeオブジェクトをエポック秒に変換したい場合は、 timestamp() を使うのが正解です。 まとめ コンピュータで扱う場合のタイムゾーンの概念は、単なる地域間の時差の寄せ集めではなく、歴史的変遷など時間軸の情報も含んだ複雑なものです (Pythonやタイムゾーンとかに限った話ではないですが)ちゃんとドキュメント読みましょう *1 : 厳密にいうとこれは抽象クラスとは呼ばないかもしれないですが、実際の実装はなく利用側での関数のoverrideを期待しているクラスという意味で、雑にこの語を使ってます *2 : pytzのオブジェクトがコンストラクタ内でとりあえず最も古い時期のUTCオフセットを参照する部分の実装はこちら: https://github.com/stub42/pytz/blob/fb43f957c5149e750c3be3cfc72b22ad94db4886/src/pytz/tzinfo.py#L189 *3 : ubuntu:focal-20230412のDockerイメージをもとにしたコンテナでapt-get install tzdataをした際に発生するのを確認しました
はじめに セーフィー株式会社 の AI Vision グループでテックリードを務めます橋本貴博です。 セーフィーではネットワークカメラ上で動作するエッジアプリケーションの開発にC++を利用しています。公開されている推論モデルはPythonで実装されていることが多いため、C++への移植が必要です。 この記事では、GitHub Copilot を使って 推論モデルの前後処理をPythonからC++に変換した結果を紹介したいと思います。 はじめに アルゴリズム 実験条件 前処理 後処理 むすび アルゴリズム mmsegmentation リポジトリで公開されている PP-MobileSeg のPython実装された推論コードを利用します。PP-MobileSeg は、2023年4月に発表された、モバイル向けセマンティックセグメンテーションのSOTAモデルです。論文は こちら から読めます。 引用元: PaddlePaddle/PaddleSeg (GitHub) / Apache License 2.0 実験条件 mmsegmentation をcloneし、リポジトリのルートを VSCode の Open Folder から開きます git clone git@github.com:open-mmlab/mmsegmentation.git 対象とするファイル(projects/pp_mobileseg/inference_onnx.py)のみをエディタで開きます GitHub Copilot Chat の New Session を作成して、過去のチャット履歴がコンテキストに入らないようにしています(実際に利用する際は何往復か対話をしたほうが良い結果が得られると思います) 前処理 推論コード inference_onnx.py の L39 - 45 をC++に変換してみます。入力された画像にリサイズと正規化を適用する一般的な前処理です。 # Resize resized_img = cv2.resize(img, input_size) # Normalize mean = np.array([ 123.575 , 116.28 , 103.53 ], dtype=np.float32) std = np.array([ 58.395 , 57.12 , 57.375 ], dtype=np.float32) resized_img = (resized_img - mean) / std コードを選択しCopilotのコンテキストに含めた状態でChatを使用します。「C++に書き直して」だけだとEigenライブラリも使ってしまったので、OpenCVライブラリだけを使うように指示します。プロンプトと回答は次の通りです。 t-hashimoto-safie: OpenCVだけを使って、C++に書き直して GitHub Copilot: 以下に、選択されたPythonコードをC++で書き直したものを示します。このコードはOpenCVライブラリを使用しています。 // Include necessary libraries #include <opencv2/opencv.hpp> #include <opencv2/core/core.hpp> // ... // Assuming 'img' is your input cv::Mat image cv::Mat resized_img; cv::Size input_size(your_width, your_height); // replace with your dimensions // Resize cv::resize(img, resized_img, input_size); // Normalize cv::Scalar mean(123.675, 116.28, 103.53); cv::Scalar std(58.395, 57.12, 57.375); resized_img = (resized_img - mean) / std; このコードは、Pythonコードと同じように画像をリサイズし、正規化します。ただし、C++のOpenCVでは、 cv::Scalar を使用して平均と標準偏差を定義します。また、リサイズのための新しいサイズは cv::Size オブジェクトとして定義されます。 正解です!以下のポイントが押さえられています。 OpenCV API は Python と C++ で微妙にインターフェースが異なることを理解している C++ の cv::resize メソッドはリサイズ後の画像の参照渡しする C++で変数の定義に必要な型を推測できている リサイズ後の画像を cv::Mat 型で事前に定義している np.array の代わりに cv::Scalar を使えばよい 後処理 次に、 inference_onnx.py の L108-L122 を書き直してもらいます。以下のコードは、ピクセルごとのラベル(int型)を示す seg_map を、ラベルに対応する色で入力画像 img に重畳し、ファイルに保存する関数です。セマンティックセグメンテーションで一般的に行われる可視化処理です。 def visualize (img: np.ndarray, seg_map: np.ndarray, filename: str = 'output.jpg' , opacity: float = 0.8 ) -> np.ndarray: assert 0.0 <= opacity <= 1.0 , 'opacity should be in range [0, 1]' palette = np.array(PALETTE) color_seg = np.zeros((seg_map.shape[ 0 ], seg_map.shape[ 1 ], 3 ), dtype=np.uint8) for label, color in enumerate (palette): color_seg[seg_map == label, :] = color # convert to BGR color_seg = color_seg[..., ::- 1 ] img = img * ( 1 - opacity) + color_seg * opacity cv2.imwrite(filename, img) GitHub Copilot へのプロンプトと回答は以下の通りです。 t-hashimoto-safie: OpenCVだけを使って、C++に書き直して GitHub Copilot: 以下に、PythonからC++への変換を行ったコードを示します。このコードは、OpenCVライブラリを使用しています。 #include <opencv2/opencv.hpp> #include <vector> #include <cassert> // Assuming PALETTE is a vector of cv::Vec3b (BGR colors) extern std::vector<cv::Vec3b> PALETTE; void visualize(cv::Mat& img, cv::Mat& seg_map, std::string filename = "output.jpg", float opacity = 0.8) { assert(0.0 <= opacity && opacity <= 1.0); // opacity should be in range [0, 1] cv::Mat color_seg = cv::Mat::zeros(seg_map.size(), CV_8UC3); for (int label = 0; label < PALETTE.size(); ++label) { color_seg.setTo(PALETTE[label], seg_map == label); } img = img * (1 - opacity) + color_seg * opacity; cv::imwrite(filename, img); } このC++コードは、Pythonコードと同じように動作します。各ラベルに対応する色をセグメンテーションマップに適用し、その結果を元の画像に重ねて表示します。その後、結果を指定されたファイル名で保存します。 こちらも正解です!気づいた点としては以下の通りです。 利用する C++ API の理解 ラベルと一致するピクセルに色を付ける処理には [Mat::setTo](https://docs.opencv.org/4.x/d3/d63/classcv_1_1Mat.html#a0440e2a164c0b0d8462fb1e487be9876) メソッドを使えばよい。 効率化のための提案 もともとの PythonコードRGB画像からBGRに変換をする部分が非効率だった。はじめからBGRのパレット std::vector<cv::Vec3b> PALETTE を用意したほうが良いと提案している。 むすび GItHub Copilot に「プログラミング言語」から「他のプログラミング言語」に変換するタスクを解かせてみました。APIリファレンスを調べながらコーディングしなくて済むので、作業時間が短縮できたと感じました。なんとなくは覚えているけど、正確な書き方を忘れたといった場合に効果があるのではないでしょうか。 セーフィーの開発部門では GitHub Copilot を導入しています。
はじめに リリースから3年以上経過しようやくモダンな開発環境に近づけていく活動ができるようになるくらい体制が整って来ました。 今回はAndroid版Safie Viewer for Mobileが2023年に行った改善活動の振り返りの話をしたいと思います。 はじめに ユニットテストの導入 ビジュアルリグレッションテストの導入 Jetpack Composeの導入 Detektの導入 Dependabotの導入 ビルドスクリプト周りの見直し 便利だったGithubActionsのAction 最後に ユニットテストの導入 改善活動の一歩としてユニットテストを導入しました。 ここまでの間テストがないという状況が続いており機能追加やライブラリのアップデートをした時手動でのテストが欠かせない状態でした。 今年からユニットテストを導入したので手動での検証作業が少し緩和されたという状態になりました。 カバレッジ率はまだ約12%というのが現実ですが、来年以降もカバレッジ率を高めていくような活動を続けていきます。 ビジュアルリグレッションテストの導入 ユニットテストに加え Roborazzi を使用したビジュアルリグレッションテスト(以下VRT)も合わせて導入しました。 Roborazziは nowinandroid や DoroidKaigi2023 でも採用されているライブラリで、JVM上でスクリーンショットを撮影し差分比較することのできるライブラリです。 Safie Viewer for Mobileでは サンプル を参考にGithubActionsのワークフローを作成し、プルリクエスト上でVRTを実行し差分比較を行えるような環境を用意しました。 プルリクエスト上で差分の確認ができる Safie Viewer for Mobileは多言語化対応をしており動作確認のため端末の言語設定を切り替えてから対象の画面を表示しレイアウトに問題がないかを確認しておりましたがRoborazzi導入後は自動で言語毎のスクリーンショットを撮って確認する事ができるので、かなりの作業時間の削減に繋がりました。 多言語化対応しているようなアプリであれば非常に強力なライブラリになるかと思います。 github.com Jetpack Composeの導入 今年からXMLからJetpack Composeにレイアウトの作りを置き換えるという事を始めました。 移行率は10%程度とまだ殆どがXMLでの構成となっておりますが、小さい画面から順にJetpack Composeに置き換えるといった活動をしています。 Jetpack Composeに置き換えて一番大きく感じた利点はテストの書きやすさにあると感じました。 これまで、Activity/FragmentのUI周りのテストを書くのは敷居が高いと感じ実機で手動確認で済ませる事が多かったのですがJetpack Composeであれば 表示のテスト 状態変化のテスト がかなり簡単に書けるようになったと思います。 developer.android.com Detektの導入 静的コード解析として、Detektを導入しました。 GitHubActionsで実行したDetektの結果を alaegin/Detekt-Action を使用して通知する仕組みで運用しています。 目的としては、これまで目視で確認していた「コーディングルールに則って書かれているかどうか」を自動で確認するためです。 その他にDetektの便利なところは、メソッド毎に複雑度の数値「CyclomaticComplexMethod」と「CognitiveComplexMethod」が測れることにあると思います。 今まで感覚的に「このメソッドなんか複雑に感じる」というのを数値で見る事ができるのでレビューの際に指摘しやすくなるといった効果があります。 detekt.dev Dependabotの導入 今まで手作業で確認していたライブラリのアップデートをDependabotを導入して自動化しました。 手作業で確認していた分の時間がかなり削減されたので開発者体験の向上に繋がりました。 docs.github.com ビルドスクリプト周りの見直し ビルドスクリプト周りの改善として以下の事を行いました Groovy から KTSに移行 Android Gradle Pluginを7系から8系にアップデート Version Catalogの導入 Androidは今後KTSでビルドスクリプトを書くのが標準なので、それに合わせてSafie Viewer for Mobileも全てKTSに置き換えました。 ついでにライブラリのバージョンもVersion Catalogで管理するようにしましたが今のところ大きな恩恵は受けておらず、これは将来的にマルチモジュール対応などを行った時に効果を発揮することになると思います。 developer.android.com developer.android.com 便利だったGithubActionsのAction r0adkll/upload-google-play GithubActonsからアプリのリリースファイルを作成しGooglePlayConsoleへ自動アップロードする為のActionです。 このActionを導入してアプリのリリース作業を半自動化する事ができました。 AlexSim93/pull-request-analytics-action PullRequestを解析し オープンしたプルリクエストの数 マージしたプルリクエストの数 プルリクエストを承認した回数 マージまでに掛かった時間 などのレポートを出力できるActionで、取り急ぎこれまでのプルリクエストの状況を確認したかった事があったので便利でした。 執筆時のバージョンは1.8.4ですが、頻繁に更新されているライブラリで日に日に新機能が追加されて行っているのが特徴的です。 github.com github.com 最後に 2023年は改善活動に力を入れた1年でした。 1年かけて様々なものを導入して来ましたが、まだまだ改善していきたい箇所が多く継続的に改善活動を続けていく必要があります。 日々の改善活動を通して、チームとして力を伸ばしていきたいと思います。 また、セーフィーではエンジニアの採用を積極的に行っております。もし興味が出てきた際はぜひご応募いただけたらと思います。 safie.co.jp
はじめに セーフィー株式会社 の AI Vision グループでテックリードを務めます橋本貴博です。セーフィーの一部のAIネットワークカメラは、Snapdragon Neural Processing Engine(SNPE)をランタイムに使ってエッジ推論を行っています。この記事では、SOTA セグメンテーションモデル PP-MobileSeg を SNPEで動かす方法を解説したいと思います。 はじめに PP-MobileSegとは? モデル変換 大まかな流れ PyTorch から ONNX への変換 ONNX モデルの修正 ONNX から DLC への変換 推論結果 むすび PP-MobileSegとは? PP-MobileSeg は、2023年4月に発表された、モバイル向けセマンティックセグメンテーションのSOTAモデルです。 ADE20Kデータセット で学習がなされており、既存モデルと比較して低レイテンシ、かつ、認識性能(mIoU)が高いことが分かります。論文は こちら (arXiv) から読めます。 引用元: PaddlePaddle/PaddleSeg (GitHub) / Apache License 2.0 モデル変換 大まかな流れ PP-MobileSegのPyTorchモデルをONNXモデルに変換します。ONNXモデルで移植性向上のためのノード編集を行なった後、ONNXモデルからSNPEのネイティブフォーマット(DLC形式)に変換します。 PyTorch から ONNX への変換 mmsegmentation リポジトリ v1.1.2 で 公開(2023年9月)されている PP-MobileSeg-Tiny の PyTorchモデルを利用します。はじめに、 Get started: Install and Run MMSeg を参考に環境構築を済ませておきます。 公式マニュアル に記載の手順にしたがって作業すれば、PyTorchモデルからONNXモデルに変換できます。 ONNX モデルの修正 ONNXモデルで使われている HardSigmoid オペレータ は、SNPEでサポートされないため、単純なオペレータの組み合わせに変換します。SNPEでサポートされるオペレータは 公式リファレンス から確認できます。 活性化関数に HardSigmoid が使われている。 https://netron.app/ で可視化。 まず、今回のノード編集で利用する ONNX の opset バージョンを確認しておきます。opset versionによってオペレータの定義が異なるため、入力モデルの opset バージョンに揃えることにします。以下のスクリプトはモデルを読み込み opset バージョンを表示します。 import onnx model = onnx.load(input_path) for opset in model.opset_import: print(f"Model opset version: {opset.version}") 今回使用するモデルの opset バージョンは 11 ということが分かります。 model opset version: 11 次に、HardSigmoid オペレータを変換します。HardSigmoid の定義は、入力を x 、出力を y として、 y = max ( 0 , min ( 1 , alpha * x + beta)) # alpha, beta はパラメータ なので、ONNXの Mul、Add、Clip オペレータ を用いて分解できます。 x1 = Mul(alpha, x) x2 = Add(x1, beta) y = Clip(x2, 0, 1) 以下のスクリプトは、Mul、Add、Clip を用いて、ONNXモデルのすべての HardSigmoid を消去します。13か所で HardSigmoid が使われていることが分かります。 from onnx import TensorProto, defs, helper count = 0 for pos, node in enumerate (model.graph.node): if node.op_type == "HardSigmoid" : count += 1 # Count HardSigmoid operator # Create Mul node and insert n0_name = node.name x0_name = node.input[ 0 ] alpha = node.attribute[ 0 ].f alpha_name = n0_name + "_alpha" alpha_tensor = helper.make_tensor(alpha_name, TensorProto.FLOAT, [ 1 ], [alpha]) y0_name = n0_name + "_mul" multiply_node = helper.make_node( "Mul" , [x0_name, alpha_name], [y0_name]) model.graph.node.insert(pos, multiply_node) # Create Add node and insert y1_name = n0_name + "_add" beta_name = n0_name + "_beta" beta_tensor = helper.make_tensor(beta_name, TensorProto.FLOAT, [ 1 ], [ 0.5 ]) add_node = helper.make_node( "Add" , [y0_name, beta_name], [y1_name]) model.graph.node.insert(pos + 1 , add_node) # Create Clip node and insert y2_name = node.output[ 0 ] min_name = n0_name + "_min" max_name = n0_name + "_max" min_tensor = helper.make_tensor(min_name, TensorProto.FLOAT, [ 1 ], [ 0.0 ]) max_tensor = helper.make_tensor(max_name, TensorProto.FLOAT, [ 1 ], [ 1.0 ]) clip_node = helper.make_node( "Clip" , [y1_name, min_name, max_name], [y2_name]) model.graph.node.insert(pos + 2 , clip_node) model.graph.node.remove(node) # Remove HardSigmoid node model.graph.initializer.extend([alpha_tensor, beta_tensor, min_tensor, max_tensor]) print (f "# hardsigmoid nodes: {count}" ) # Outputs 13 作成されたモデルを検証して保存します。 onnx.checker.check_model(model) # Validation onnx.save(model, output_path) # Save 変換後のONNXモデルを見ると、HardSigmoid オペレータが、Mul、Add、Clip オペレータに置き換わっていることが確認できます。 HardSigmoid が Mul/Add/Clip に変換されている。 https://netron.app/ で可視化。 ONNX から DLC への変換 SNPE SDKに含まれるツール を使って、ONNXモデルからDLCモデルに変換が可能です。モデル実行時の入力テンソルのサイズを固定します。 snpe-onnx-to-dlc --input_network model.onnx --input_dim "input" 1,3,512,512 推論結果 当社のAIネットワークカメラ(Safie One)にモデルをインストールし、推論を行います。クラウド経由でモデルをアップロードできます。 SafieOne 推論が動作する様子をWebアプリ(Safie Viewer)から確認します。ここでは人物クラスを水色で表示しています。人物の輪郭が抽出されていることが分かります。 人物が水色でセグメンテーションされている むすび セマンティックセグメンテーションのSOTAモデル PP-MobileSeg をAI ネットワークカメラで動かしました。ONNXモデルからHardSigmoid オペレータを除去し、DLCモデルに変換する方法を詳しく解説しました。最後に、AIネットワークカメラ上の推論結果をWebアプリから確認しました。 セーフィーではエッジAIを用いたプロダクトの開発を行っています!
Androidチームの品質改善の取り組みとして、今回はアプリサイズを計測した話をしたいと思います。 はじめに 動作環境 rulerとは rulerの導入方法 settings.gradleの編集 app/build.gradleの編集 analyzeタスクの実行 継続的に計測する ワークフロー 最後に はじめに 頻繁にアプリを更新しているとある日突然「アプリのサイズが肥大化している」と気が付く時があります。 新しいリソースファイルを追加した 新しいライブラリを導入した など原因は様々考えられますがこれがいつの時期から発生したのかを調べるのは一苦労です。 そうならない為にも今回は「ruler」を使用し継続的にアプリのサイズを監視できる仕組みを導入しました。 動作環境 Android Studio:Flamingo 2022.2.1 Patch 2 AGP:8.0.2 Gradle Version:8.0 ruler:1.4.0 rulerとは ruler とは、Spotifyが公開しているGradle pluginです。 アプリのサイズはもちろんのこと、各モジュールと依存関係がどれくらいアプリのサイズに影響を与えているかを レポートで出力することができるプラグインです。 rulerの導入方法 settings.gradleの編集 プロジェクト直下の「settings.gradle」を編集しrulerのプラグインを追加します。 pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } //追加 resolutionStrategy { eachPlugin { if (requested.id.id == "com.spotify.ruler") { useModule("com.spotify.ruler:ruler-gradle-plugin:1.4.0") } } } } dependencyResolutionManagement { //省略 } 参考: https://github.com/spotify/ruler/issues/85 app/build.gradleの編集 app/build.gradleにrulerの設定を追記します plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'com.spotify.ruler' //追加 } android { //省略 } dependencies { //省略 } //追加 環境に応じて設定値は変更してください。 ruler { abi.set("arm64-v8a") locale.set("ja_JP") screenDensity.set(480) sdkVersion.set(33) } analyzeタスクの実行 ここまで上手くいくとGradleタスクに「analyze Bundle」のタスクが追加されています。 このタスクを実行するとアプリサイズを計測したHTMLとJSONのレポートが出力されます。 継続的に計測する いつアプリのサイズが肥大化したのか継続的に監視する為にrulerのタスクをGithub Actionsのワークフローに取り入れ 出力したJSONファイルを解析し内製のダッシュボードに記録ことで監視する仕組みを導入しました。 ワークフロー 以下実際に作成したものから抜粋したワークフローです。 Kotlin Script「sendAppSizeToDashboard.main.kts」でJSONを解析し結果を内製のダッシュボードに送信するという仕組みを作りました。 name: Output App Size Task on: push: paths: - 'app/**' branches: - main jobs: rulerJob: name: Exec ruler runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' cache: gradle - name: Make gradlew executable run: chmod +x ./gradlew - name: Run ruler run: ./gradlew analyzeStagingDebugBundle - name: Make scripts executable run: chmod -R +x ./.github/workflows/scripts #rulerの結果を内製ダッシュボードに送信するスクリプトを実行 - name: Send dashboard env: TZ: 'Asia/Tokyo' run: ./.github/workflows/scripts/sendAppSizeToDashboard.main.kts 計測したデータ 計測データを元に数値をグラフで可視化 最後に 今回の取り組みでは rulerを使用した継続的なアプリサイズの計測 という環境を作りました。 以下の記事によると「アプリのサイズはインストール率に影響している」との事なので、もし「アプリのサイズを計測したい」と思った際に本記事が少しでも参考になれば幸いです。 developer.android.com また、セーフィーではエンジニアの採用を積極的に行っております。もし興味が出てきた際はぜひご応募いただけたらと思います。 safie.co.jp
今回は直近のAndroidチームの品質改善の取り組みとして、ユニットテストを導入した件についてお話したいと思います。 はじめに 実際にやった事 ユニットテストを書く どこからユニットテストを書くか 技術スタック JUnit4 + Mockito-Kotlin + Truthを使用したテストコード カバレッジ率の可視化 Jacocoの設定 Jacocoの出力 CIで自動でテストが実行される環境 導入したアクション ワークフロー 最後に はじめに Android版Safie Viewer for Mobileは開発が始まって3年が経過しようとしています。 3年間、継ぎ足しを重ねて作成してきたコードはもうじき5万行を超える大きさになりました。 Androidチームは直近までプロジェクトのメンバーが少なく、機能を実装してリリースする事で精一杯になっていました。 なんとかリリースまで辿り着けているものの、目の前には日に日に複雑になっていくコードがあるというのが現実でコードのリファクタリングまで手が回らずメンテナンスする事ができない状況に陥っていました。 この状況はAndroidチームとしても今後の課題となって行く事が目に見えていたため リファクタリングの前段階として、既存コードが設計通りに動くかの保証を目的としたユニットテストを導入する事にしました。 実際にやった事 主な取り組みとして以下の3点を行いました ユニットテストを書く カバレッジ率の可視化 CIで自動でテストが実行される環境構築 ユニットテストを書く どこからユニットテストを書くか Android版Safie Viewer for Mobileのアーキテクチャは 公式 が推奨しているような UI Layer Domain Layer Data Layer の3層に分け、責務を分離する事を心がけ作られています。 幸いにもこのルールが守られている事もあってか、レイヤ間が疎結合にはなっておりユニットテストが書ける土壌にはありました。 アーキテクチャの図(公式より引用) しかしながら、土壌はあったもののAndroidチームとしてこれまでユニットテストを書くという文化が無かったので いきなりActivity/FragmentやViewModelなどの画面に近い箇所のテストコードを書くのはハードルが高いという事もあり 画面から遠く機能としても孤立しているData Layerからテストを書き始め、徐々に上のレイヤーに向かっていくという方針にしました。 技術スタック 今回、ユニットテストを書くために導入した技術スタックは主に以下になります。 JUnit4 Androidのプロジェクト作成時に既に導入されているテストフレームワーク Robolectric Android依存部分のテストを行うためのライブラリ Mockito-Kotlin モックライブラリ Mockito をKotlinでも使いやすくしたライブラリ Truth アサーションライブラリ Jacoco テストカバー率の測定 JUnit4 + Mockito-Kotlin + Truthを使用したテストコード 記述例として、下記のようなAndroidに依存しないData Layerのクラスを用意してテストコードを書きます。 package com.safie.test.data import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext interface EmployeeService { /**従業員IDの最大値を取得*/ fun getMaxId(): Int } class EmployeeRepository( private val service: EmployeeService, private val dispatcher: CoroutineDispatcher ) { /** * IDの最大値にプラス1した、新しい従業員IDを取得 * @return 従業員ID */ suspend fun getNewId() = withContext(dispatcher) { val maxId = service.getMaxId() return@withContext maxId + 1 } } 上記のようなクラスに対しては次のようなテストコードを書くことができます。 import com.google.common.truth.Truth import com.safie.test.data.EmployeeRepository import com.safie.test.data.EmployeeService import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.whenever class EmployeeRepositoryTest { @Test fun testGetNewId() = runTest { val mockEmployeeService: EmployeeService = mock() val dispatcher = StandardTestDispatcher(testScheduler) val employeeRepository = EmployeeRepository(mockEmployeeService, dispatcher) //getMaxId()を実行した時、10を返すように設定 val mockMaxId = 10 whenever(mockEmployeeService.getMaxId()).thenReturn(mockMaxId) //maxIdにプラス1した値が返却される事を検証 val id = employeeRepository.getNewId() Truth.assertThat(id).isEqualTo(mockMaxId + 1) } } カバレッジ率の可視化 ユニットテストを書き続けるというモチベーションを維持するのはとても大変です。 そのモチベーションを維持する取り組みとして テストコードによってどの行が実行されたか 各クラスのカバレッジ率は何%か の計測結果をレポートとして出力するためにJacocoの導入をしました。 Jacocoの設定 以下のQiitaの記事を参考にJacocoの設定を行いました。 Android開発のテストカバー率取得にはこのツールを使い分けると良いという話 #Android - Qiita apply plugin: 'jacoco' jacoco { toolVersion = "0.8.9" } android.applicationVariants.all { variant -> def variantName = variant.name.capitalize() //ex. ProdDebug def realVariantName = variant.name //ex. prodDebug if (variant.buildType.name != "debug") { return } task("jacoco${variantName}TestReport", type: JacocoReport) { dependsOn "test${variantName}UnitTest" group = "testing" description = "Generate Jacoco coverage reports for ${realVariantName}" reports { xml.required = false html.required = true } //無視するファイル(excludes)の設定を行います def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', 'android/**/*.*', 'androidx/**/*.*', '**/Lambda$*.class', '**/Lambda.class', '**/*Lambda.class', '**/*Lambda*.class', '**/*Lambda*.*', '**/*Builder.*', '**/*Activity*.class', '**/*Fragment*.class' ] def javaDebugTree = fileTree(dir: "${buildDir}/intermediates/javac/${realVariantName}/compile${variantName}JavaWithJavac/classes", excludes: fileFilter) def kotlinDebugTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${realVariantName}", excludes: fileFilter) def mainSrc = "${project.projectDir}/src/main/java" getSourceDirectories().setFrom(files([mainSrc])) //Java, Kotlin混在ファイル対応 getClassDirectories().setFrom(files([javaDebugTree, kotlinDebugTree])) getExecutionData().setFrom(fileTree(dir: project.projectDir, includes: [ '**/*.exec', //JUnit Test Result '**/*.ec']) //Espresso Test Result ) } } Jacocoの出力 設定したJacocoのタスクを実行することで以下のようなHTMLのレポートが出力され テストコードによってどの行が実行されたか 各クラスのカバレッジ率は何%か を確認をすることができます。 CIで自動でテストが実行される環境 Android版Safie Viewer for MobileではCIにGithub Actionsを採用しております。 PullRequestのイベントをトリガーにテストを自動で実行し、テストの実行結果と現在のカバレッジ率を表示するGithub Actionsのワークフローを用意しました。 導入したアクション ユニットテストの結果とカバレッジ率を表示する為に導入したアクションは以下になります。 action-junit-report Junitで出力したテスト結果のレポートを表示するワークフロー jacoco-reporter Jacocoで出力されたXMLをインプットに結果を表示するワークフロー ワークフロー 今回の取り組みで実際に用意したワークフローは以下の通りです。 name: PullRequest Tasks on: pull_request: paths: - 'app/**' branches: - main jobs: UnitTestJob: name: Android Staging Unit Test runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v3 - name: set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' cache: gradle - name: Make gradlew executable run: chmod +x ./gradlew - name: run UnitTest StagingDebug run: ./gradlew jacocoStagingDebugTestReport - name: JaCoCo Code Coverage Report id: jacoco_reporter uses: PavanMudigonda/jacoco-reporter@v4.8 with: coverage_results_path: ${{ github.workspace }}/app/build/reports/jacoco/jacocoStagingDebugTestReport/jacocoStagingDebugTestReport.xml coverage_report_name: Coverage coverage_report_title: JaCoCo github_token: ${{ secrets.GITHUB_TOKEN }} skip_check_run: false minimum_coverage: 40 fail_below_threshold: false publish_only_summary: false - name: Add Coverage Job Summary run: echo "${{ steps.jacoco_reporter.outputs.coverageSummary }}" >> $GITHUB_STEP_SUMMARY - name: Publish Test Report uses: mikepenz/action-junit-report@v3 if: cancelled() != true with: report_paths: '**/build/test-results/*/TEST-*.xml' このワークフローが実行されるとPullRequest上でテストの結果や現在のカバレッジ率を確認することができるようになります。 ワークフローの実行結果 jacoco-reporterの結果 最後に 今回行った取り組みは ユニットテストを書く カバレッジ率の可視化 CIで自動でテストが実行される環境構築 だけの最低限の環境を作っただけにすぎず、これだけではチームにユニットテストを書いていく文化が広がっていきません。 しかしながら、ユニットテストを作る環境を導入した事は開発チームにとって重要な足がかりとなります。 今後は、チーム全体でテストの重要性を認識しつつユニットテストを書く事が当たり前の文化を作っていく取り組みをしていきます。 また、セーフィーではエンジニアの採用を積極的に行っております。もし興味が出てきた際はぜひご応募いただけたらと思います。 safie.co.jp