Ahogrammer

Deep Dive Into NLP, ML and Cloud

Djangoにおけるアクセス制御の話

Djangoでアプリケーションを作っているとアクセス制御をしたくなることがあります。たとえば、会員サイトではプレミアムユーザと一般ユーザによってアクセスできる情報に差を付けたいことがあるでしょう。こういった機能は、少し規模の大きなサイトではよく見かけます。

Djangoでアクセス制御をする方法はいくつもあります。その中にはDjango組み込みの方法やアプリケーションとして提供されている方法もあります。本記事ではそれらを比較してどのように使えるのか、どういった場面に向いてそうなのかといったことを確認します。

本記事の構成は以下の通りです。

Django組み込みのパーミッション

Django組み込みのパーミッションは、ユーザかグループに適用できるモデルレベルのパーミッションです。INSTALLED_APPSdjango.contrib.authがある場合、Djangomigrateを実行したタイミングで、自動的にすべてのモデルに対して、閲覧(view)、追加(add)、変更(change)、削除(delete)の権限を作成します。この権限をユーザやグループに付与することで、実行できる操作を制限することができます。

組み込みのパーミッションについて例を見てみましょう。たとえば、appという名前のアプリケーションの中にPostという名前のモデルがあるとします。そうすると、migrateを実行した段階で以下の4つのパーミッションが作成されます。

  • app.add_post
  • app.change_post
  • app.delete_post
  • app.view_post

ユーザがこれらの権限を所持しているか確認するために、Userモデルで提供されているhas_permメソッドを使うことができます。

>>> user.has_perm('app.add_post')
True

権限の付与と削除は管理画面の「User permissions」から行うことができます。

f:id:Hironsan:20190419135830p:plain
Permissionの追加と削除

テンプレートから権限を参照したい場合、コンテキストに自動的に追加されるperms変数を使用することができます。

{% if perms.app.view_post %}
    This content will be shown users with view_post permission.
{% endif %}

ここまでは自動的に追加される権限について見てきましたが、自分でカスタムパーミッションを定義することもできます。カスタムパーミッションを定義することで、モデルに対する独自の権限を作成することができます。そのためには、モデルのpermissions属性を使います。

class Post(models.Model):                                                                                                                                
    content = models.TextField()                                                                                                                         
    author = models.ForeignKey(User, on_delete=models.CASCADE)                                                                                           
                                                                                                                                                         
    class Meta:                                                                                                                                          
        permissions = (                                                                                                                                  
            ('view_content', 'View content'),                                                                                                            
        )   

こうすることで、migrateを実行したときに自動的に権限が作成されます。

f:id:Hironsan:20190419140113p:plain
カスタムパーミッションの定義

より詳細な話はDjangoの公式ドキュメントを確認してください。

docs.djangoproject.com

django-guardian

django-guardianは、オブジェクトレベルのパーミッションを実装できるアプリです。Django組み込みのパーミッションがモデル内のすべてのオブジェクトへのアクセスを制御するのに対し、django-guardianはモデルの特定のインスタンスへのアクセスを制御できます。

わかりやすく説明するために具体例を考えましょう。たとえば、ユーザAがPost100を作成したとします。一方、ユーザBはPost200を作成したとします。こういった場合、django-guardianを使うことで、ユーザAがPost200にアクセスできないようにすることができます。

django-guardianは、Django組み込みのパーミッションと似たインターフェースで使うことができます。

>>> from django.contrib.auth.models import User
>>> from guardian.shortcuts import assign_perm
>>> from app.models import Post
>>> 
>>> john = User.objects.create(username='john')
>>> post = Post.objects.create(content='This is the content.', author=john)
>>> john.has_perm('view_content', post)
False
>>> assign_perm('view_content', john, post)
>>> john.has_perm('view_content', post)
True

パーミッションの設定はユーサだけでなくグループに対しても行うことができます。

>>> from django.contrib.auth.models import Group
>>> group = Group.objects.create(name='editors')
>>> assign_perm('view_content', group, post)
>>> bob = User.objects.create(username='bob')
>>> bob.groups.add(group)
>>> bob.has_perm('view_content', post)
True

django-guardianにはこれ以外にもデコレータやテンプレートタグなどの便利な機能を提供しています。詳細は公式ドキュメントで確認してください。

django-guardian.readthedocs.io

django-role-permissions

django-role-permissionsは、ロールベースでパーミッションを管理するためのアプリケーションです。どのユーザがどのオブジェクトにアクセスできるのかを個別に設定するのではなく、ロールを定義してそれに基づいてパーミッションを管理します。

from rolepermissions.roles import AbstractUserRole

class Writer(AbstractUserRole):
    available_permissions = {
        'create_content': True,
        'view_content': True,
    }

class Reader(AbstractUserRole):
    available_permissions = {
        'create_content': False,
        'view_content': True,
    }

ユーザーにロールを割り当てたら、そのユーザーのロールとパーミッションについて確認することができます。

>>> from rolepermissions.checkers import has_permission, has_role
>>> from rolepermissions.roles import assign_role
>>> has_role(john, Writer)
False
>>> assign_role(john, Writer)
<class 'Writer'>
>>> has_role(john, Writer)
True
>>> has_permission(john, 'create_content')
True

オブジェクトレベルの権限のチェックは関数を定義して行うことができます。

from rolepermissions.permissions import register_object_checker

@register_object_checker()
def edit_content(role, user, obj):
    if role == Writer:
        return True
    if obj.author == user:
        return True
    return False

呼び出すときは以下のようにします。

>>> from rolepermissions.checkers import register_object_checker
>>> has_object_permission('edit_content', john, post)

django-role-permissionsは、紹介した以外にもミックスインやデコレータ、テンプレートタグなどの便利な機能を提供しています。詳細は公式ドキュメントで確認してください。

django-role-permissions.readthedocs.io

django-rules

django-rulesも、オブジェクトレベルのパーミッションを実装できるアプリです。特徴としては、パーミッションの管理にデータベースモデルを使用しない点を挙げられます。

まずは真理値を返す関数を定義します。この際、@rules.predicateデコレータを付ける必要があります。

@rules.predicate
def is_post_writter(user, obj):
    return obj.author == user

次に、定義した関数をパーミッションと結びつけます。

rules.add_rule('can_edit_post', is_post_writer)

そして最後に、オブジェクトを使ってパーミッションをテストします。

>>> rules.test_rule('can_edit_post', john, post)
True

おわりに

本記事で紹介したアプリケーション以外にもDjangoには多くのアクセス制御のためのサードパーティアプリケーションがあります。その比較に関しては以下のサイトを参照するのが良いと思います。

djangopackages.org

参考資料

今日からはじめるレコメンデーション -平均評価による推薦の問題点と対策-

前回の記事では人気度と新規性、またそれらをハイブリッドした手法による推薦の方法について紹介しました。そこでは、人気度と新規性に基づく手法の問題点とその解決方法について説明しました。

hironsan.hatenablog.com

今回は、平均評価に基づく推薦の問題点とその解決策について紹介します。今回紹介する問題点は以下の2つです。

  • 確信度が考慮されていない
  • 評価数が0の場合にスコアが不定になる

平均評価による推薦について考えるため、みなさんがECサイトを運営しているとしましょう。みなさんは運営しているECサイトで、平均評価によるレコメンデーションの実装を検討しています。平均評価による推薦では、商品の平均的な評価の大小に基づいて商品を推薦します。たとえば、Amazonでイヤホンを検索して評価順に並べると以下のような結果になります。

f:id:Hironsan:20190411082928p:plain
amazon.co.jpで「イヤホン」を検索した結果

平均評価による推薦は悪くなさそうですが、確信度(confidence)が考慮されていないという問題があります。たとえば、 一つの評価しかないけど星5の商品と、50000の評価があるけど星4.2の商品がある場合、平均評価による推薦では前者が推薦されます。これでは評価の高い新商品ばかり推薦されてしまうことでしょう。

f:id:Hironsan:20190411084520p:plain

信頼区間の導入

確信度を考慮する方法の一つとして信頼区間(Confidence Interval, CI)を使った方法があります。信頼区間統計学でお馴染みの概念ですが、確信度を考慮するために使うこともできます。情報推薦においては信頼区間の下限を使って候補のランク付けを行うことができます。以下に信頼区間の図を示しました。

f:id:Hironsan:20190418095152p:plain
信頼区間 from https://thepsychologist.bps.org.uk/volume-28/june-2015/methods-building-confidence-confidence-intervals

信頼区間を使った推薦について、もう少し具体的に説明します。

たとえば、2つの商品A, Bがあり、どちらも平均で星4の評価だったとします。ここで、商品Aの評価数は3、商品Bの評価数は100とします。このとき、2つの商品に対する信頼区間のグラフを描くと、商品Bのグラフが尖った形になり、下限の値も大きくなります。したがって、同じ平均評価でもその下限を使うことで、商品Bを推薦することができます。以下に示したように、イメージとしては、評価数が増えるほどグラフが尖った形になります。

f:id:Hironsan:20190418095600p:plain
グラフの差 from https://en.wikipedia.org/wiki/Margin_of_error

ここまでは概念的な説明をしてきましたが、評価数が多くなるとグラフの形が尖る話をもう少し数学的に説明します。

数学的な説明をするために、N個の評価Xが平均\mu、分散\sigma^{2}正規分布N(\mu, \sigma^{2})に従って独立に付けられると仮定します。そうすると、正規分布の再生性によりその和の分布Y正規分布N(N\mu, N\sigma^{2})に従います。和の分布から平均値の分布を求めるとその分散は \displaystyle{\frac{\sigma^{2}}{N}} となります。したがって、評価数が多くなるほど、分散が小さくなり、正規分布の形が細長くなるというわけです。この辺の話は、正規分布の再生性と分散の定数倍の公式を使えば証明できます。詳細についてはAppendixに示したので参照してください。

\displaystyle{
\overline{X} = \frac{1}{N} \sum_{i=1}^{N}X_{i}\\
X \sim N(\mu, \sigma^{2})\\
\overline{X} \sim  N(\mu, \frac{\sigma^{2}}{N})
}

このようにして、平均評価の分布が正規分布に従うことがわかれば、その信頼区間の下限を求めることは比較的容易にできます。たとえば、95%信頼区間を使うのだとすれば、その下限は「平均 - 1.96×標準偏差」で求めることができます。この1.96という値は正規分布表を参照することでわかります。

ここまでは各評価が正規分布に従っていることを仮定していましたが、正規分布に従っていない場合はどうなるのでしょうか?その場合も問題なく扱うことができます。なぜなら、評価がどんな分布に従っているのであれ、中心極限定理によりその平均の分布は正規分布に従うからです。式で示すと、以下のX_{i}がどんな分布に従っているのであれ、\overline{X}正規分布に従います。

\displaystyle{
\overline{X} = \frac{1}{N}(X_{1} + X_{2} + X_{3} + \cdots + X_{N})
}

正規分布以外の例として、コインの表裏やupvote/downvote、クリックされたか否かのような2種類の事象を表せるベルヌーイ分布について考えてみましょう。たとえば、\hat{p}=\frac{# upvotes}{N}をいいねが押された確率とします。試行回数をNとしたとき、ベルヌーイ分布の和は二項分布B(N, \hat{p})に従います。その期待値と分散はそれぞれN\hat{p}N\hat{p}(1-\hat{p})であることから、平均分布の期待値と分散はそれぞれ\hat{p}\frac{\hat{p}(1-\hat{p})}{N}となります。そうすると、信頼区間は以下のように計算できます。

f:id:Hironsan:20190417091437p:plain
正規近似した95%信頼区間

ベルヌーイ分布を正規近似することで、信頼区間の下限を求めることはできますが、ある状況下ではあまり良い近似をできません。そのような場合に使われるのが以下に示したWilson score intervalです。zは何%信頼区間を使うかによって変わる値で、95%信頼区間ならz=1.96です。正規近似に比べると複雑な形をしていますが、簡単に導くことができます。導出についてはAppendixを参照してください。

f:id:Hironsan:20190417094524p:plain
Wilson score intervalの式

ここまでは二種類の値のみを扱ってきましたが、これを拡張することができます。たとえば、5段階評価の場合を考えてみましょう。そのような場合、星1ならupに1、downに0、星3ならupに0.5、downに0.5、星4ならupに0.75、downに0.25というように値を割り当てます。そうすると、その平均と分散を求めることができるので、Wilson score intervalを使うことができます。

f:id:Hironsan:20190417100032p:plain
5段階評価のスコア割当方法

スムージングの導入

平均評価による手法の問題点として、評価数が0の場合にスコアが不定になるという問題があります。どういうことかというと、平均評価については以下の式で求めることができますが、評価数が0の場合はN=0となり、平均を計算することができないということです。

\displaystyle{
\overline{X} = \frac{1}{N} \sum_{i=1}^{N}X_{i}
}

この問題に対する一つの解としては、自然言語処理でよく使われているスムージング(smoothing)が挙げられます。スムージングとは何かと言うと、分母と分子に小さな数を割り当て、どんなものにも小さな確率を割り当てる処理のことです。たとえば、\mu_{0}をすべての評価の平均、\lambdaをスムージングのパラメータとすると以下のように定義することができます。

\displaystyle{
\overline{X} = \frac{\sum_{i=1}^{N}X_{i} + \lambda \mu_{0}}{N + \lambda} 
}

式だけ見ててもわからないので、値を当てはめてみましょう。今、全体の評価の平均を\mu_{0}=3\lambda=1とします。そうすると、評価数が0の場合にはスムージングした結果は3になります。つまり、評価が存在しない場合は全体の平均値を割り当てることができます。

また、この式では評価数が多いほどスムージング前の平均に近づくという性質があります。たとえば、評価4のデータが一つだけの場合は、スムージング結果は3.5になり、評価4のデータが10あれば結果は3.91、1000あれば3.99になります。

おわりに

今回は平均評価による推薦の問題点とその対策方法を紹介しました。平均評価による推薦に信頼区間の下限を使うことである種の確信度を考慮した推薦をできるようになるというメリットがあります。次回はベイズ的なアプローチについて紹介したいと思います。

Appendix

正規分布の再生性

正規分布の再生性というのは、確率変数X_1X_2が独立に正規分布N(\mu_1,{\sigma_1}^2), N(\mu_2,{\sigma_2}^2)にそれぞれ従うとき、X_1+X_2正規分布に従うというものです。また、その分布はN(\mu_1+\mu_2, {\sigma_1}^2+ {\sigma_2}^2)となります。

先の例の場合、N個の評価Xが平均\mu、分散\sigma^{2}正規分布N(\mu, \sigma^{2})に従って独立に付けられると仮定しました。そうすると、その和の分布は正規分布の再生性により、N(N\mu, N\sigma^{2})に従います。

ここから平均分布を求めるには、和の分布をNで割れば求めることができます。これには、期待値と分散に関する定数倍の公式を使うことができます。簡単なので以下にその証明を示しました。ここでの証明は離散分布に対する証明ですが、連続分布に対しても同様に証明することができます。

f:id:Hironsan:20190415091141p:plain
期待値の定数倍の公式

f:id:Hironsan:20190415091304p:plain
分散の定数倍の公式

これらの式を使うことにより、平均分布はN(\mu, \frac{\sigma^{2}}{N})に従うことがわかります。したがって、評価数が増えれば分散が小さくなり、結果としてグラフが尖った形になるということです。

Wilson score intervalの導出

f:id:Hironsan:20190417101215p:plain

参考資料

今日からはじめるレコメンデーション -Hacker Newsに学ぶスコア関数の設計-

レコメンデーションといえば、現在最も多く使われている技術の一つと言えるでしょう。その応用は数多く存在し、身近なところで言えば、Amazonによる関連商品の推薦やNetflixによる映画の推薦などに使われており、私たちの意思決定を支援しています。

一口にレコメンデーションといってもそのやり方は様々です。古くは行列分解を用いた手法から最近では機械学習、特にディープラーニングを用いた手法が多数提案されています。今回はその中でも手動で設計したスコア関数を使った手法についてHacker Newsを例に紹介します。大きくは以下の3つを紹介します。

  • 人気度に基づく手法
  • 新規性に基づく手法
  • ハイブリッドな手法

人気度に基づく手法

人気度(Popularity)に基づく手法というのは「他の人が好きなコンテンツなら、あなたも好きに違いない」という考えに基づいて推薦を行う手法です。たとえば、Netflixのようにユーザに映画を推薦する状況について考えてみましょう。世界的な映画データベースであるIMDbによると、2019年現在、最も評価が高い映画は「ショーシャンクの空に」、「ゴッドファーザー」「ゴッドファーザー2」の順番になっています。したがって、人気度に基づく手法ではこれらの映画をユーザに推薦します。

f:id:Hironsan:20190409083643p:plain
Movie ranking by IMDb

人気度に基づく手法を実装するのは難しくありません。もし、SQLを書いて実現するなら、以下のように書くことでユーザに推薦する候補を得られるでしょう。

SELECT movie_id 
FROM   movies 
ORDER  BY rating DESC;

人気度に基づく手法はシンプルですが問題点もあります。たとえば、私たちがイタリアに旅行した際にレストランを探しているとします。そのとき、人気度に基づく手法ではマクドナルドが推薦される可能性があります。私はマクドナルドのチーズバーガが好きなのですが、イタリアに来ているならピザやパスタを食べたいですし、そもそも誰もが知っているマクドナルドを推薦する必要があるのかは疑問です。

その他にも人気度に基づく手法では自分の好みに合わない候補が推薦されることがあります。たとえば、音楽を推薦する際にオリコンの週間ランキングから推薦した場合、少なくとも私の好みには全くあっていません。

f:id:Hironsan:20190409085208p:plain
オリコン週間シングルランキング 2019年04月08日付

また、Hacker Newsのようにニュースの推薦を行う場合にはコンテンツの新しさを考慮する必要があります。というのも、いくら人気の記事でもユーザは3年前の記事を読みたくはないだろうと考えられるからです。そういうわけで次に紹介するのが新規性に基づく手法です。

新規性に基づく手法

新規性(Recency)に基づく手法では、作成あるいは更新日時が新しいコンテンツをユーザに推薦します。たとえば、ニュースのように時事性の高いコンテンツを推薦したりする際には新規性を考慮します。技術者にとって身近なところでは、Hacker Newsのnewestは新規性に基づく推薦と言えるでしょう。

f:id:Hironsan:20190409090147p:plain
Newest news by Hacker News

新規性に基づく手法を実装するのは難しくありません。もし、SQLを書いて実現するなら、以下のように書くことでユーザに推薦する候補を得られるでしょう。

SELECT news_id 
FROM   news 
ORDER  BY created_at DESC;

新規性に基づく手法はシンプルですが問題点もあります。たとえば、有用でない記事が新しいというだけの理由で推薦されてしまいます。こうなると、スパムによってそのランキングは支配されてしまうでしょう。また、新規性に基づいて記事を推薦する場合、自分の全く興味のない分野の記事が推薦されることがあるでしょう。この問題に対して、プログラミングの知識を共有できるサイトQiitaではユーザが自分の興味のある分野のタグをフォローすることで、推薦される記事を絞り込んでいます。

f:id:Hironsan:20190409090733p:plain
Qiitaのタグフィード

このように、人気度と新規性を単体で使うといくつかの問題が出てきてしまいます。そういうわけで、実際にはこれらの手法をハイブリッドして使うことを考えます。なお、個人化の問題については扱うと長くなるので、次の機会にしておきます。

ハイブリッドな手法

ハイブリッドな手法では、Hacker Newsで使っているアルゴリズムを紹介しましょう。その前に人気度と新規性に基づく手法の問題を整理しておきます。人気度に基づく手法ではコンテンツの新しさは考慮されませんでした。そのため、ニュースの推薦で3年前の記事を紹介してしまう可能性があります。一方、新規性に基づく手法では有用でない記事が新しいというだけの理由で推薦される可能性があります。

ハイブリッドな手法の基本的な考え方としては人気度と新規性をバランスさせるようにスコア関数を設計します。概念的には、\displaystyle{\frac{f(popularity)}{g(recency)}}のような形でスコア関数を設計します。こうすることで、「いいね!」がたくさんついているような人気度が高い記事は分子の値を大きくして全体的なスコアを高く、一方で発信されてから時間が経った記事は分母の値を大きくして全体的なスコアを小さくすることができます。

Hacker Newsを例にスコア関数について見てみましょう。Hacker Newsのアルゴリズムを解説した記事によると、Hacker Newsでは以下の式を使って記事にスコア付を行っています。

\displaystyle{
score = \frac{(ups-downs-1)^{0.8}}{(age+2)^{1.8}} \times penalty
}

分子が人気度、分母が新規性を表しています。upsはupvoteの数、downsはdown voteの数、ageは投稿されてからの時間を表していると考えてください。

まず、注目すべきは分子の指数が分母より小さい点(0.8 < 1.8)です。これはつまり分母は分子より早く大きくなるということを示しています。こうすることで、どれだけupvoteが多くてもageは常にその成長を追い越すので、結果として新しい記事が上に来るような設計になっています。以下に示すように、スコア関数の形としては、最初に急激に上がって、ゆっくりと下がっていく形になっています。分母は最初の方はあまり効いていないのですが、時間が経つにつれ効いてくることを示しています。

f:id:Hironsan:20190410093412p:plain
HN article raw scores. http://www.righto.com/2013/11/how-hacker-news-ranking-really-works.htmlより引用

また、分子の指数が0.8である点にも工夫が見られます。指数を0.8とすることで分子の形は以下の図に示すようにsublinearの形になります。こうすることで、最初に獲得した100のupvoteは1000獲得した後の100upvoteより価値があるという考え方を入れられます。

f:id:Hironsan:20190410095037p:plain
Example of linear and sublinear. https://www.researchgate.net/figure/Examples-of-linear-and-sub-linear-functions-Hick-Hyman-law-follows-the-latter-pattern_fig2_270890224より引用

このように設計している理由としては、少数の記事がupvoteのほとんどを獲得し、大多数の記事はupvote数が少ないことに関係していると思われます。Linearに設計すると、1000vote得た記事に100voteの記事は勝負になりませんが、sublinearなら土俵に乗ることはできます。

最後に、penalty 項ではビジネスロジックを入れる事ができます。たとえば、画像だけの記事やリンク集のような記事はスコアが低くなるようにpenalty項を設計することができます。応用としては、たとえば自社でニュースのレコメンデーションシステムを作る際、penalty項をいじって自社に関係する記事に高いスコアを与えるといったことが考えられます。

おわりに

今回は手動で設計したスコア関数によるレコメンデーションの方法についてHacker Newsを例に紹介しました。これらの手法は学習が必要ないというメリットがありますが、その一方で推薦内容の個人化がされていないという欠点もあります。次回は行列分解による手法を例に個人化の方法について紹介したいと思います。

参考資料

www.righto.com