ハトネコエ Web がくしゅうちょう

プログラミングやサーバー・Web制作、チームマネジメントなど得た技術のまとめ

顔画像認識周りのメモ

手っ取り早いのは Haar-like

OpenCV に分類器が同梱されていてすぐに使えるので、試してみるのはとっても簡単です。
これらの記事が詳しいです。

上の記事ではそれぞれの分類器ファイルの説明が書いてあってとてもありがたいのですが、
『haarcascade_profileface.xml : 顔(証明写真)』のとこだけ、
Profile face は「横顔」の意味が正解ですので、そこだけ注意です。

実際に試してみるとこんな感じ。

f:id:nekonenene:20210219082512j:plainf:id:nekonenene:20210219082518j:plainf:id:nekonenene:20210219082522j:plain
Haar-like で試してみた顔認識結果

処理が1秒未満で終わるのは良いのですが、(2841 x 2200 px の画像で 0.74s 程度)
やはりこの手法は画像の白黒パターンの一致から判定しているだけなので、
顔が傾いているとダメだったり、必要以上の場所を認識してしまって、なかなか実用は難しそうです。

英語ですが、「テンプレートマッチング」と「畳み込みニューラルネットワーク」の違いの説明記事は
以下がいい感じでした。

What’s the Difference Between Haar-Feature Classifiers and Convolutional Neural Networks?

CNN(畳み込みニューラルネットワーク)か~

というわけで、実用可能な方向を考えると、
CNN(Convolutional Neural Networks=畳み込みニューラルネットワーク)か~。
なんですけど、ここはまだあれこれ試してないので、メモ程度にリンクをまとめときます。

使えそうなライブラリやアプリケーション

実装してみた記事に関しては、このあたりがわかりやすそうでした。

まだ試せていませんが、実装してみるときはここらへんの記事を読みながらやってみようと思います。

IBMの学習済みモデルを使ったAPIが簡単に作れる Model Asset Exchange なんかも便利だなーと思って読んでましたが、
IBMは2020/06に顔画像認識は肌の色の偏見に基づく可能性があるため撤退しているようで、
現在は Facial recognization は一覧にありませんでした。

【2019/4月更新】学習済みの様々なディープラーニング・モデルをメチャ簡単に利用できる! Model Asset Exchange(MAX)をご紹介します - Qiita

畳み込みニューラルネットワークがなんなのか、どうやって処理しているのかとか、
勉強しないといけないことが多いですが、時間あるときに調べていきたいと思います。

画像処理の基礎:フィルタリング,パターン認識から撮像過程モデルまで | Udemy

この Udemy の講座も、せっかく買ったのにまだほぼ見られてないので、
早く学びたいです。

prezto の rm や mv で確認プロンプトを出さないようにする

prezto を使っているんですが、
デフォルト設定だと rmmv はそれぞれ rm -imv -i になっていて、
安全なのですがやや面倒に感じることもあります。

この機能をオフにするには、

vi ~/.zpreztorc

G でファイル末尾に移動したのち、

zstyle ':prezto:module:utility' safe-ops 'no'

の1行を足してあげて、以下のようにします。

# Enabled safe options. This aliases cp, ln, mv and rm so that they prompt
# before deleting or overwriting files. Set to 'no' to disable this safer
# behavior.
# zstyle ':prezto:module:utility' safe-ops 'yes'
zstyle ':prezto:module:utility' safe-ops 'no'

これだけです。

何が起きているかと言うと、
~/.zprezto/modules/utility/init.zsh に以下のような記述があり、
safe-ops 'yes' (デフォルト)の場合には、
rmrm -iエイリアスとなるなどの設定がおこなわれています。

# Safe ops. Ask the user before doing anything destructive.
alias rmi="${aliases[rm]:-rm} -i"
alias mvi="${aliases[mv]:-mv} -i"
alias cpi="${aliases[cp]:-cp} -i"
alias lni="${aliases[ln]:-ln} -i"
if zstyle -T ':prezto:module:utility' safe-ops; then
  alias rm="${aliases[rm]:-rm} -i"
  alias mv="${aliases[mv]:-mv} -i"
  alias cp="${aliases[cp]:-cp} -i"
  alias ln="${aliases[ln]:-ln} -i"
fi

逆に no にすることで、ここのエイリアス設定をスキップすることが出来るわけですね。

Rails の scope をあまり使わない方がいい理由

1. Rails の scope とは

Rails には scope という便利機能があります。
https://railsguides.jp/active_record_querying.html#%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97

例えば

class Article < ApplicationRecord
  scope :draft, -> { where(published_at: nil) }
end

のように定義しておけば、下書きの一覧を表示するページで

Article.draft.order(updated_at: :desc)

などと呼び出せるようになります。

これはコードを短く書く上で有用な方法のひとつです。
何をしたいのかわかりやすいコードとなることでしょう……本来は!!

2. ダメンズメーカーの scope

しかし、実際のところそうはなりません。
scope の定義をまとめている箇所が、以下のようなダメなコードの養殖場となっているケースをよく見ます。

class User < ApplicationRecord
  has_many :articles
  has_and_belongs_to_many :roles, join_table: :users_roles

  scope :latest_users, -> { order(created_at: :desc).first(5) }
  scope :rank1_writer, -> { find_by(id: Article.pluck(:user_id).group_by(&:to_i).values.max_by(&:size).first) }
  scope :admin, -> { where(deleted_at: nil).select{ |user| user.roles.map(&:key).select{ |key| %w(super_admin admin).include?(key) } } }
  scope :active_writers, -> {
    latest_articles = Article.order(:updated_at).last(10) + Article.order(:created_at).last(10)
    where(id: latest_articles.pluck(:user_id))
  }
end

Rails のコードを複数社で見たことがある人は「ああ〜わかる〜」とうなずいてくれるかもしれません。

私がコードを書くときに意識していることとして、
「たとえ、既存コードのコピペでコードのほとんどを書くエンジニアが引き継ぐことになっても、ある程度回るようにしたい」
という思いがあるので、その観点でいくと、
「scope って使うべきではないのでは?」 と感じていますので、その理由をここではまとめます。

3. scope を使うべきでない理由

3-1. 1行で書こうとしてしまう

上の例では複数行で書いている例も混ぜましたが、基本、scope は1行で書くことが多いです。
それにならって他の人も1行で書こうとします。
そうして出来上がるのが以下のような、複雑な処理です。

scope :rank1_writer, -> { find_by(id: Article.pluck(:user_id).group_by(&:to_i).values.max_by(&:size).first) }
scope :admin, -> { where(deleted_at: nil).select{ |user| user.roles.map(&:key).select{ |key| %w(super_admin admin).include?(key) } } }

一見しただけでは何をしているのかわかりにくいです。
よく言われることですが、Ruby は短くコードが書けてしまうぶん、
可読性の低い1行の処理を書けてしまうので注意が必要です。

scope で書かれているメソッドは、何のクラスが返ってくるのか
一見してわからないことが大変よくあります。

3-2. コメントが書かれないことが多い

たとえ「メソッドの上部に yard に従った説明コメントを書くこと」という開発ルールがある会社でも、
scope にはコメントが書かれていないことがよくあります。
scope がメソッドであると意識されていないためです。

こうして、なんの処理をしているのか一見してもわからないメソッドが誕生してしまうのです。

scope でなくクラスメソッドを使うのであれば、3-1. や 3-2. の問題を回避できる可能性が高まります。

# もっとも記事を書いている User を返す。Article が1件もないときのみ nil を返す
# @return [User]
def self.rank1_writer
  writer_ids = Article.pluck(:user_id)
  most_wrote_writer_id = writer_ids.group_by(&:to_i).values.max_by(&:size)&.first
  User.find_by(id: most_wrote_writer_id)
end

# super_admin もしくは admin の権限を持つ User の一覧を返す
# @return [Array<User>]
def self.admin
  undeleted_users = User.where(deleted_at: nil)

  undeleted_users.select do |user|
    user_role_keys = user.roles.map(&:key)

    user_role_keys.select do |key|
      %w(super_admin admin).include?(key)
    end
  end
end

なんの処理をするメソッドか書いたおかげで、
メソッド名のひどさが際立ちましたが(笑)、
ともかく、変数名の自己ドキュメント化の要素も加わり、どういう処理でどんな値を返すのか、とてもわかりやすくなりました。

余談ですが、 self.admin メソッドは N+1 問題を発生させているヤバいコードです。
undeleted_users の数だけSQLクエリが発行されます。

3-3. テストコードが書かれないケースが多い

scope がメソッドであると意識されないゆえにテストコードが書かれないケースが多いです。

冒頭で紹介した

scope :draft, -> { where(published_at: nil) }

であれば、そのまますぎるのでテストコードを書く必要もないわけですが、

scope :rank1_writer, -> { find_by(id: Article.pluck(:user_id).group_by(&:to_i).values.max_by(&:size).first) }
scope :admin, -> { where(deleted_at: nil).select{ |user| user.roles.map(&:key).select{ |key| %w(super_admin admin).include?(key) } } }

などの明らかに複雑な処理であればテストコードは書かれるべきです。
例えば rank1_writer メソッドは Article が1件もない場合に undefined method 'first' for nil:NilClass でエラーを吐くバグがありますしね。

4. scope を使うのであれば

チーム開発において scope を使うのであれば、
以下のルールを徹底すると多少マシになるかと思います。

4-1. ActiveRecord::Relation を返すように義務付ける

個人的には scope って ActiveRecord::Relation を返す目的で使うものだと思っていたのですが、
以下のように Array など別のクラスを返す scope がよくあります。

scope :latest_users, -> { order(created_at: :desc).first(5) }

これだと scope メソッド同士での連結ができませんし、
一度 Array になってしまうと使い道にも制限が出てしまうので、あまり良くないと思います。

4-2. コメントを書く・テストを書く

上の 3-2. および 3-3. で指摘したことの逆ですが、
scope であってもコメントを書くこと、テストを書くことをルール化しておけば、
コードの可用性は多少マシになると思います。

4-3. メソッドチェーンが3つ以上になるならクラスメソッドにする

Webアプリケーションの運用を続けていると、
既存メソッドの条件が変わっていくことは往々にしてあります。

例えば、活動しているライター一覧を表示するページに使う scope が以下のようであったとして、

scope :active_writers, -> { where(deleted_at: nil, type: User.types[:writer]).order(updated_at: :desc) }

「最近記事を書いているライターに限定してほしい」という声があったときに以下のように変更するとしましょう。

scope :active_writers, -> { left_joins(:articles).where(deleted_at: nil, type: User.types[:writer]).where("articles.created_at > ?", 1.month.ago).order(updated_at: :desc) }

このときに、「これが1行で書かれているのは処理の内容としても、見やすさとしても、わかりにくいのでは?」
と考えて、scope でなくクラスメソッドに変更するのは大いにアリだと思います。

5. でも scope を使う必要ってある?

5-1. ActiveRecord::Relation と Array の違いがわからない問題

4. のように対応案を書きましたが、実は 4-1. の時点で難しいです。
ちゃんと Rails がわかる人がレビュアーにいるうちはいいのですが、

Article.order(:created_at).last(3)

Article.order(created_at: :desc).limit(3)

の返すクラスが違うと知らない人だけになってくると、4-1. のルールすら守られなくなってきます。

「いやいや、 .class.name でつなげば何のクラスが返ってくるかわかるでしょ?」と思うかもしれませんが、
それはプログラミングに慣れている人の考えで、始めたての人はメソッドの返り値のクラスがなにかを意識していません。

5-2. DRY にしないほうが可読性いい説

scope は短いメソッドチェーンのものに限って使うのなら良いかもしれません。しかし、

scope :draft, -> { where(published_at: nil) }
scope :active_writers, -> { where(deleted_at: nil, type: User.types[:writer]).order(updated_at: :desc) }

のような単純なものにしか使わないのであれば、そもそも scope を使わなくていいかもしれません。

controller で使われているときに「このメソッドは実際どんな定義なんだろう?」と model 側を見に行く必要が発生しませんし、
どんなSQLクエリが発行されるのか、メソッドチェーンを見るだけで想像しやすい利点があります。

処理の同じところがあればまとめたくなるのは、プログラマーとして健全な考え方ですが、
Query Builder に関しては、ベタ書きする方が
結果的に可読性が高くバグを起こしにくくなることも多いのではと考えています。
(これは賛否両論あるでしょう)

# scope 使用
@active_writers = User.active_writers.page(params[:page]).per(20)

# scope を使わない場合
@active_writers =
  User
    .where(deleted_at: nil, type: User.types[:writer])
    .order(updated_at: :desc)
    .page(params[:page]).per(20)

6. 結論

というわけで個人的には、
「たとえ、既存コードのコピペでコードのほとんどを書くエンジニアが引き継ぐことになっても、ある程度回るようにしたい」
という前提に立つのであれば、scope に関しては

  • scope を使わずクラスメソッドを使うこと
  • メソッドには必ず yard に従ったコメントを付けること
  • 各メソッドへのテストを書くこと
  • default_scope は絶対に使わない(この記事で触れませんでしたが、バグの温床になりやすい)

あたりをルールとしてコードを書くかなあ、と思います。

もちろん、コードレビューの環境がしっかり整っているならこういったルールがなくとも品質が担保されると思うのですが、
プロダクトが長く続くものという前提に立ったとき
「たとえ会社のエンジニア採用能力が落ちて、今いる人が全員いなくなった後にコピペエンジニアしか残らなかった」という場合を考慮すると、
コードが無法地帯にならないためにはこういったルールを先んじて制定しておくと安心度が高まるかなと考えています。