Ahogrammer

Deep Dive Into NLP, ML and Cloud

Django Rest Frameworkでソーシャル認証

Djangoには、組み込みのユーザ認証の仕組みがありますが、GitHubTwitterFacebookなどのサービスを介したソーシャル認証はサポートされていません。しかし、幸いなことに、サードパーティー製のパッケージを使って簡単に実装することができます。

本記事では、django-allauthを使ってTwitterアカウントによるログインを行う方法について説明します。Twitterでのログイン方法について説明しますが、FacebookGoogleGitHubアカウントに対してもほぼ同様の実装を使ってログインすることができます。

準備

まずは環境構築を行いましょう。ここでは、pipenvを使って必要なパッケージをインストールし、Djangoのプロジェクトとアプリケーションを作成します。

$ mkdir django-social-auth && cd django-social-auth
$ pipenv install django djangorestframework django-allauth django-rest-auth
$ pipenv shell
(django-social-auth) $ django-admin startproject auth .
(django-social-auth) $ python manage.py startapp src

パッケージとしては、以下の4つをインストールしました。

インストールできたら、開発サーバを起動してhttp://127.0.0.1:8000/を開きます。

(django-social-auth) $ python manage.py migrate
(django-social-auth) $ python manage.py runserver

以下の画面が確認できたらOKです。

f:id:Hironsan:20190423080236p:plain
DjangoのWelcomeページ

プロジェクトの設定

必要なパッケージをインストールできたので、プロジェクトの設定を行います。まずは、settings.pyINSTALLED_APPSにアプリケーションを追加します。以下のように設定しています。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',  # new
    'rest_framework.authtoken',  # new
    'rest_auth',  # new
    'django.contrib.sites',  # new
    'allauth',  # new
    'allauth.account',  # new
    'allauth.socialaccount',  # new
    'allauth.socialaccount.providers.twitter',  # new
]

SITE_ID = 1

アプリケーションを追加したらマイグレーションをしておきます。

$ python manage.py migrate

ビューの作成

アプリケーションを作成したら、ビューを作成します。ソーシャル認証用のビューやTwitter用のSerializerはすでにallauthrest_authに用意されているのでそれらを使って以下のように書きます。

from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter
from rest_auth.registration.views import SocialLoginView
from rest_auth.social_serializers import TwitterLoginSerializer

class TwitterLogin(SocialLoginView):
    serializer_class = TwitterLoginSerializer
    adapter_class = TwitterOAuthAdapter

ビューを書いたらURLに紐づけて登録しておきましょう。settings.pyに以下のように書いておきます。

from django.contrib import admin
from django.urls import path
from src.views import TwitterLogin

urlpatterns = [
    path('admin/', admin.site.urls),
    path('rest-auth/twitter/', TwitterLogin.as_view(), name='twitter_login')
]

この時点で、http://127.0.0.1:8000/rest-auth/twitter/にアクセスすると以下の画面が表示されます。

f:id:Hironsan:20190423083113p:plain
http://127.0.0.1:8000/rest-auth/twitter/

Twitter OAuthキーの取得

Twitter OAuth用のキーを取得するために以下のページへ移動します。

f:id:Hironsan:20190423083558p:plain
Developer Apps

移動したら、アプリケーションを作成します。そうすると、Consumer KeyとAccess Tokenが得られます。これらのトークンは外に漏れると悪用される可能性があるので、その扱いには十分に気をつけてください。

f:id:Hironsan:20190423093744p:plain

トークンの設定

取得したトークンはDjangoのadminページから設定する必要があります。そのために、以下のコマンドを使ってDjangoのsuperuserを作成します。

$ python manage.py createsuperuser

作成したら、作成したアカウントを使ってhttp://127.0.0.1:8000/adminにログインします。そうすると、以下の画面に遷移します。

f:id:Hironsan:20190423095820p:plain
Adminページ

Adminページにログインしたら、「Site」を選択してドメイン名を変更します。今回はテストなので127.0.0.1にしておきます。

f:id:Hironsan:20190423100022p:plain

次に、取得したトークンを設定するため、adminページから「Social Applications」を選択します。ここでOAuthの設定を行うことができます。今回はProviderとしてTwitterを選択し、Client IDとSecret keyに取得したConsumer keyとConsumer Secretを入力します。最後に、サイトを追加して保存します。

f:id:Hironsan:20190423100325p:plain
Social Applications

以上でadminページでの設定は完了です。

ためしにログインしてみましょう。http://127.0.0.1:8001/rest-auth/twitter/へ移動すると以下の画面が表示されます。ここに取得したTwitterAccess TokenとAccess Token Secretを入力してPOSTします。

f:id:Hironsan:20190423100702p:plain

以下のようにkeyが含まれたJSONを取得できれば認証に成功しています。

HTTP 200 OK
Allow: POST, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "key": "335a0kmcrypv56h3xgf7jabit8l2oewnqz4du91s"
}

参考資料

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

参考資料