nikkie-ftnextの日記

イベントレポートや読書メモを発信

イベントレポート | #azuremoku2 にてAzure ML Serviceのクイックスタート・チュートリアルを触りました

はじめに

いつも心は虹色に! nikkieです。
2/9開催の 第三十回 Azureもくもく会 のイベントレポートをお送りします。

勉強会の概要

第三十回 Azureもくもく会 @ 品川 - connpass

Azureに関する作業をもくもく行う会です。 XamarinでAzure連携を試してみる、WebAppsのGitを利用した自動デプロイをSlackに通知してみるのを試す等々、やりたかったこと研究したいことをもくもく作業できる場所です。 初心者の方から既にAzureを利用している玄人の方まで幅広くご参加いただける事を楽しみにしております。

2018年3月以来の約1年ぶりの参加でした。
先日以下のウェビナー1を聞き、Azure Machine Learning Service(以下Azure ML Serviceと略)で手を動かしてみたくなり、今回久しぶりに参加しました。

Connect(); 徹底解説シリーズ!~ AI 編 ~ 最新の環境が最短の開発だ! Azure Machine Learning update 2019年1月版 [ウェビナー]

取り組んだこと

以下3点で手を動かしました。2

達成したこと

手を動かしてわかったことを書いていきます。

前提

  • Workspaceの作成はAzure Portalを操作しています(azコマンドは今回使っていません)
  • Azure NotebookにてPythonコードを実行しています(Python3.6のノートブックを使用)3

※Azure ML ServiceはAzure Notebookからだけでなく、手元のPCのJupyter Notebookからでも扱えそうなので、今後試してみます

クイックスタート

Azure portal を使用したクイック スタート - Azure Machine Learning service | Microsoft Docs

  1. Azure PortalからWorkspaceを作成
  2. WorkspaceのリンクからAzure Notebookを開き、「Getting started」プロジェクトをクローン
  3. Azure NotebookからWorkspaceに接続し、Pythonコードを実行(後述のRun)

モンテカルロ法で円周率を推定しました。
2回実行しての結果がWorkspaceで確認できます。

f:id:nikkie-ftnext:20190209194010p:plain

現時点の理解を書き留めておきます:

  • Workspace:Azure ML Serviceの単位(コンテナレジストリやストレージがラップされている)
  • Experiment:Runを集めたもの。クイックスタートでは2つのRun(2回の推定結果)をもたせた
  • Run:Pythonのコードを実行、機械学習のタスクに対応。クイックスタートでは、1回の円周率の推定

チュートリアル1(MNIST)

イメージの分類チュートリアル:モデルをトレーニングする - Azure Machine Learning service | Microsoft Docs

ロジスティック回帰でMNIST(手書き数字の分類)に取り組みました。
円周率の推定の例との違いは、AzureにVM(CPUマシン)を立てていることです。

  • 円周率の推定:ローカル(Azure Notebook)で実行
  • MNIST:リモート(Azure Machine Learning コンピューティング=AzureのVM)で実行

リモートで実行するために、データや学習に使うPythonコードを送っています。
リモートでどのコードをどのデータに対して実行するかを指定(Runについて指定)したあとは、Azure Notebook上で状況が確認できました。
途中経過からリモートではDockerイメージのプルや途中経過の保存などが行われており、Workspaceがラップしているコンテナレジストリ、ストレージが関わってくるようです。

f:id:nikkie-ftnext:20190209194934p:plain

チュートリアル2(機械学習の自動化)

回帰モデルのチュートリアル: 自動化された ML - Azure Machine Learning service | Microsoft Docs

タクシーの料金を予測する回帰モデルを自動化して作成しました。
dataprep SDKでデータの前処理をしたあと、回帰モデルを自動で作成します。
Azure NotebookのFree CPUだと1イテレーションに1-2分かかったので、泣く泣く5回のうちから選抜しました。

f:id:nikkie-ftnext:20190209194958p:plain f:id:nikkie-ftnext:20190209195009p:plain f:id:nikkie-ftnext:20190209195020p:plain

今後調査したいこと

  • Azure NotebookのFree CPUよりスペックが高い環境で実行する(手元のPC上で実行、または、Azure NotebookからDSVMへ接続)
  • MNISTの例のようにリモート(AzureのVM)で自動化された機械学習を実行するにはどうすればいいのか?(データをまとめて送ればいい?)
  • dataprepを使った前処理が自分で書ける気がしないので、チュートリアルをやる https://docs.microsoft.com/ja-jp/python/api/overview/azure/dataprep/intro?view=azure-dataprep-py
  • または、pandasでタクシーデータを扱ってみる(dataprepがpandasライクに使えないことで、学習コストが上がってしまっている印象です😫)

他の方の取り組みから

感想

Azure ML Service、クイックスタートとチュートリアルでどんな感じのものかはつかめました。
個々のリソースの関係性を言語化するためにも、もう少し触りたいところです。
Kaggleのデータセットなどで使ってみようかな。(ログや性能の管理が楽になることを体感したい)

資格試験が刷新されたなど、Azure周りの情報収集もでき、作業環境も快適(VIP会議室で特に椅子が快適)で大満足です。
運営者、参加者の皆さま、半日ありがとうございました & お疲れさまでした。


  1. https://github.com/aldente-dev/aldente-dev.github.io/blob/master/connect2018/webiner.md より。2/13にDevOps、2/20にk8sとウェビナーは続くようです

  2. ウェビナーで教えていただいた https://docs.microsoft.com/ja-jp/azure/machine-learning/service/ からクイックスタート→チュートリアルの順で取り組んでいます。

  3. クイックスタート用のプロジェクトをクローンすると、チュートリアルソースコード込みで自分のアカウントにコピーされました

イベントレポート | #WEBエンジニア勉強会11 にていろいろな話が聞けて世界が広がりました

はじめに

いつも心は虹色に! nikkieです。
2/1開催の #WEBエンジニア勉強会 のイベントレポートをお送りします。

勉強会の概要

WEBエンジニア勉強会 #11 (東京都, 渋谷) - connpass

第11回目になる、WEBエンジニア初心者でも参加できることを心がけ・気軽に発表できるWEBエンジニアのための勉強会コミュニティです。初心者の方でも、一人でも、SIerの方も、学生の方も、みなさんお気軽にご参加ください。
今回もビアバッシュ形式の勉強会です。 ピザ・飲み物・おつまみを用意する予定です。 飲み食いしながらワイワイとやりましょう!

昨年11月の10回目に続いて2回目の参加です。
初参加の10回目が楽しかったので、今回も楽しみにしていました。
アウトプットに結びつけるために今回はブログ枠での参加です。

10回目の私の様子はこちらのツイート一覧から観察できます。
また、いわしまんさんのブログに10回目の様子がまとまっています!
【イベント】WEBエンジニア勉強会#10にお邪魔してきた話 - Rのつく財団入り口

発表振り返り

冒頭の『「みてね」を推す理由』には残念ながら間に合わず(無念)、決済の仕組みの話から参加となりました。
Web開発に限らず、開発一般に共通する話が多かった印象です。
今回はどんな発表があったかを数行でまとめます(リンクからスライドをご覧いただけます)

11回目の私のツイート一覧はこちら
発表を聞いて考えたことや手を動かしたことを今後アウトプット予定です。

感想

10回目よりも多様なテーマの話が聞けた回でした(同じ卓で「色んな話が聞けていいですね」という話になりました)。
アウトプットのネタをたくさんいただいたので、少しずつになるかと思いますが、今後消化していきます。
発表者、運営者、参加者の皆さま、ありがとうございました。

イベントレポート | #rettypy にてタイタニックハンズオンの準備に取り組みました

はじめに

明けましておめでとうございます1、nikkeです。
2019年一発目のブログはもくもく会参加レポートです。

勉強会の概要

Pythonもくもく自習室 #17 @ Rettyオフィス 2019書き初め - connpass

週末の昼下がりに、Pythonに関するやりたいことを各自持ち寄り、ゆるゆると「自習」を進めていく会です。
Python使いおよびエンジニアライフのオアシス&知見を身につける「自習室」としてゆるくご活用いただければと思います。

「技術とイイゴハン(主にランチ)を楽しむ」rettypy。
2018年9月のPyConJP前の会以来の参加となりました。
前回:イベントレポート | Pythonもくもく自習室 #13 あやめの分類器をAPIに組み込み、Dockerイメージ作成 #rettypy - nikkie-ftnextの日記

取り組んだこと

1/30に開催するKaggleタイタニックの生存予測ハンズオンの準備を進めました。

先週のpyhack合宿にて壁打ちし、100件超えのコメントをいただきました。(レビューをいただき誠にありがとうございます!)

コメントに優先度をつけた上で、

の2点を進めました。

達成したこと

時間は限られていますが、少しずつアップデートしていきます。

知ったこと

PyDataのオーガナイザの方から、PyDataでのタイタニックハンズオンの経験(分析が大事)をご共有いただき、ありがたかったです。

他の方の取り組みから

Djangoの静的ファイル関連

Django Girls Tutorialで追加したCSSファイルが効かないという質問がありました。2
CSSでカワイくしよう · Django Girls Tutorial

shinyorkeさんの回答から私の静的ファイルの理解が浅いと気づき、ドキュメントを確認しました。
静的ファイル (画像、JavaScript、CSS など) を管理する | Django documentation | Django
PythonAnywhereでcollectstaticする意味がうっすらとわかった気がします。

  • debugがTrueなら自動で静的ファイル配信(runserver)
  • 本番サーバはcollectstaticで静的ファイル配信ディレクトリに収集

runserverと本番環境とで違うことでハマりやすくなっているように感じます。
Dockerで環境用意して本番環境の設定で開発してみようと思います。

なお本番サーバでの静的ファイル配信方法も数パターンあるようです。
静的ファイルのデプロイ | Django documentation | Django

  • アプリと同じサーバから配信するケース(Apacheの設定例あり)
  • 専用サーバから配信するケース
  • クラウドサービスやCDNから配信するケース(S3の例あり)

感想

「ハンズオンのTAやってもいいですよ」と立候補してくださった方がいらっしゃって、大変嬉しかったです。
Django関係でDjango Girls Tutorialを紹介いただき、参加した方のもくもくがはかどったのでしたら、翻訳に参加した身としてこんなに嬉しいことはありません。

1日ありがとうございました。


  1. 勉強会登壇という大きなアウトプットが控えており、その準備を優先していました。だいぶ間が空いてしまったと反省しており、アウトプットの敷居を下げて習慣を取り戻そうと思います。

  2. runserverで動かす際、テンプレートやCSSを追加したときにそれが見つからない場合があります。Ctrl+Cで一度止め、runserverを再度実行することで解決します。質問者の方は再実行で解決したそうです。

Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 3/3) コメント機能 <後編>

※1本の記事として書き上げましたが、記事が長くなりすぎたため、前編・後編に分けています。

ブログからコメントを作成できるようにする(承前)

コメント入力画面へ遷移するように設定(承前)

AttributeErrorへの対応として、blog/views.pyadd_comment_to_post関数を用意します。

from django.shortcuts import render, get_object_or_404
from django.utils import timezone
from .models import Post
# importの行は以下の1行のみ変更
from .forms import PostForm, CommentForm # CommentFormのimportを追加

# これまでに作成した関数は変更なし

# 以下の行を追加
def add_comment_to_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.post = post
            comment.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = CommentForm()
    return render(request, 'blog/add_comment_to_post.html', {'form': form})

Djangoフォーム · Django Girls Tutorial でやったように、comment = form.save(commit=False)で、コメント用フォームに結びつけられたデータからデータベースに保存するためのコメントのオブジェクトを生成しています。1
ForeignKeyのpostについて、comment.post = postとして、コメントが紐づく記事を設定しています。2
(記事のidを渡すのでなく、記事そのものを渡すと、Django側で処理してくれるようです。)

add_comment_to_post関数が追加されたので、記事詳細画面が表示されるようになります。(URL例:http://localhost:8000/post/1/
f:id:nikkie-ftnext:20181229182441p:plain

コメント入力フォーム追加

ところが、追加した「Add comment」ボタンをクリックすると、TemplateDoesNotExistというエラーが表示されます。
f:id:nikkie-ftnext:20181229182510p:plain

「Add comment」ボタンをクリックすると、先ほど追加したadd_comment_to_post関数が呼び出されます。
関数の中で、render(request, 'blog/add_comment_to_post.html', {'form': form})と、blog/add_comment_to_post.htmlテンプレートを表示するように設定しているのですが、そのテンプレートが見つからずエラーとなっています。
blog/add_comment_to_post.htmlテンプレートを作りましょう。

blog/templates/blog/add_comment_to_post.html

{% extends 'blog/base.html' %}

{% block content %}
  <h1>New comment</h1>
  <form method="POST" class="post-form">{% csrf_token %}
    {{ form.as_p }}
    <button type="submit" class="save btn btn-default">Send</button>
  </form>
{% endblock %}

add_comment_to_post.htmlpost_edit.htmlテンプレートと非常に似ています。
テンプレートが追加されたので、「Add comment」ボタンをクリックした後のエラーが消え、コメントが書けるようになりました。

▼ログインしていなくてもコメントが書けます f:id:nikkie-ftnext:20181229182522p:plain

▼追加されました f:id:nikkie-ftnext:20181229182533p:plain

コメントの承認機能と削除機能をつける

コメント承認機能・コメント削除機能追加

ここまででブログの記事に誰もがコメントを書け、そのコメントは誰でも見られるようになりました。
ここでは、ブログにログインしているユーザが承認したコメントに限って、誰でも見られるように修正します。

記事詳細画面の変更内容

  • ログインしているユーザは、すべてのコメントが見られる:(1)
  • ログインしているユーザは、コメントの承認または削除ができる3:(2)
  • ログインしていないユーザは、承認されたコメントのみを見られる:(3)

(1)〜(3)ができるように、記事詳細画面のテンプレートを編集します。

blog/templates/blog/post_detail.html

{% extends 'blog/base.html' %}

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <hr>
  <a class="btn btn-default" href="{% url 'add_comment_to_post' pk=post.pk %}">Add comment</a>
  {% for comment in post.comments.all %}
    {% if user.is_authenticated or comment.approved_comment %} <!-- 追加 (1)と(3)に対応 -->
    <div class="comment">
      <!-- div class="date" を変更。(2)に対応 -->
      <div class="date">
        {{ comment.created_date }}
        {% if not comment.approved_comment %}
          <a class="btn btn-default" href="{% url 'comment_remove' pk=comment.pk %}"><span class="glyphicon glyphicon-remove"></span></a>
          <a class="btn btn-default" href="{% url 'comment_approve' pk=comment.pk %}"><span class="glyphicon glyphicon-ok"></span></a>
        {% endif %}
      </div>
      <!-- div class="date" の変更 終わり -->
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
    {% endif %} <!-- 追加 -->
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
{% endblock %}

ログインしているユーザの場合、user.is_authenticatedTrueなので、すべてのコメントが表示されます。
そのうち、承認されていない各コメントには削除ボタンと承認ボタンが表示されます。
ログインしていないユーザの場合、comment.approved_commentがTrueになる(すなわち、承認済みの)コメントが表示されます。
承認済みなので、コメントに削除ボタンと承認ボタンは表示されません。4

記事詳細画面を表示すると、NoReverseMatchエラーです。 f:id:nikkie-ftnext:20181229182552p:plain
comment_remove(とcomment_approve)というURLが設定されていないために発生しているので、blog/urls.pyを編集します。

from django.urls import path
from . import views

urlpatterns = [
    # これまで作成したpath()関数に変更はなし
    # リストの末尾の要素として以下の2行を追加する
    path('comment/<int:pk>/approve/', views.comment_approve, name='comment_approve'),
    path('comment/<int:pk>/remove/', views.comment_remove, name='comment_remove'),
]

AttributeErrorにより、サーバが起動しなくなりました。
f:id:nikkie-ftnext:20181229182610p:plain

blog/views.pycomment_approve関数(とcomment_remove関数)が見つからないために発生しているので、これらの関数を追加します。

from django.shortcuts import render, get_object_or_404
from django.utils import timezone
# importの行は以下の1行のみ変更
from .models import Post, Comment # Commentのimportを追加
from .forms import PostForm, CommentForm

# これまでに作成した関数は変更なし

# 以下の行を追加
@login_required
def comment_approve(request, pk):
    comment = get_object_or_404(Comment, pk=pk)
    comment.approve()
    return redirect('post_detail', pk=comment.post.pk)

@login_required
def comment_remove(request, pk):
    comment = get_object_or_404(Comment, pk=pk)
    comment.delete()
    return redirect('post_detail', pk=comment.post.pk)

comment.post.pkでコメントが紐づく記事のpk(ID)を取得できます。
Djangoシェルでの確認

>>> from blog.models import Comment
>>> comment = Comment.objects.get(id=1)
>>> comment.post
<Post: 初投稿:dockerで環境構築>
>>> comment.post.pk
1

これでエラーは解決し、ログインするとコメントの承認/削除を行うことができます。
f:id:nikkie-ftnext:20181229182627p:plain

ログインしていないユーザには、承認したコメントだけが表示されます。
f:id:nikkie-ftnext:20181229182640p:plain

記事一覧に表示されるコメント数を承認されたコメント数に変更する

最後に、記事一覧のコメント数を承認されたコメントの数に変更します。
(ログインの有無にかかわらず、承認されたコメント数の表示となります)

変更するのはテンプレートとモデルの2箇所です。
まず、blog/templates/blog/post_list.htmlを変更します。

{% extends 'blog/base.html' %}

{% block content %}
  {% for post in posts %}
    <div class="post">
      <div class="date">
        <p>published: {{ post.published_date }}</p>
      </div>
      <h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
      <p>{{ post.text|linebreaksbr }}</p>
      <!-- 以下の1行だけを変更しています。変更前:<a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.comments.count }}</a> -->
      <a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.approved_comments.count }}</a>
    </div>
  {% endfor %}
{% endblock %}

コメントの数はPostモデルのapproved_commentsメソッドで取得した数としています。
しかし、approved_commentsはまだ実装されていないため、この時点ではコメント数には何も表示されません。
そこで、blog/models.pyを変更します。
Postモデルにapproved_commentsメソッドを追加します。

from django.db import models
from django.utils import timezone


class Post(models.Model):
    # プロパティには変更がないため省略

    def publish(self):
        self.published_date = timezone.now()
        self.save()

    # 以下のメソッドを追加するだけ
    def approved_comments(self):
        return self.comments.filter(approved_comment=True)
    
    def __str__(self):
        return self.title


class Comment(models.Model):
    # 変更がないため省略

approved_commentsメソッドでは、前編冒頭のCommentモデルのForeignKey設定で、post.commentsと書けるように設定したことを利用しています。
追加したメソッドをDjangoシェルで試してみます。

>>> from blog.models import Post
>>> post = Post.objects.get(id=1)
>>> post.approved_comments()
<QuerySet [<Comment: フォームからコメントを作りました!>, <Comment: 承認のテストです>]>
>>> post.approved_comments().count()
2

記事一覧画面で承認されたコメントの数が表示されるようになりました!
f:id:nikkie-ftnext:20181229182657p:plain

終わりに

コメント機能、手を動かす部分は長かったですが、やっていることはDjango Girls Tutorialでやってきた機能追加手順と変わらないと思います。
Djangoで機能追加するときの一連の手順の練習という印象です。

nikkie流Django機能追加手順5

  1. 新規ページへ遷移するリンクを追加
  2. 新規ページのURL設定
  3. 新規ページのビュー作成
  4. 新規ページのテンプレート作成

ExtensionsのDjango2系の動作確認はこれで完了です。
PyConJPのスプリントで協力いただいた翻訳があるので、そのレビューを進めていきます。
最後までお読みいただき、ありがとうございました。

脚注


  1. https://docs.djangoproject.com/ja/2.1/topics/forms/modelforms/#the-save-method

  2. ForeignKeyの同様の書き方として post.author = request.user がありました。ref: https://tutorial.djangogirls.org/ja/django_forms/

  3. ログインしているユーザであっても、一度承認されたコメントは削除できません。(ただし、Django Adminからは削除可能です)

  4. 一度承認されたコメントを削除できるようにする場合、この箇所を修正することになりそうです。(ログインしているユーザに限って、削除ボタンと承認ボタンを表示する)

  5. Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango - nikkie-ftnextの日記Djangoの機能追加手順」より

Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 3/3) コメント機能 <前編>

※1本の記事として書き上げましたが、記事が長くなりすぎたため、前編・後編に分けています。

はじめに

いつも心は虹色に! nikkieです。
Django Girls Tutorial ExtensionsのDjango2系動作確認の第3弾(最終回)です。
第1弾はこちら:Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango - nikkie-ftnextの日記
第2弾はこちら:Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 2/3) セキュリティ編 - nikkie-ftnextの日記
(#pyhack でもくもくして、このブログを作成しました)

コメント機能を実装したブログはこのような感じです。
f:id:nikkie-ftnext:20181229182225p:plain

背景

Django Girls Tutorial Extensionsでは、Django Girls Tutorialで作ったブログアプリに機能追加していきます。
ExtensionsのコードはDjango 1系で書かれており、Django Girls Tutorialの翻訳メンバーの間では「Django 2系に書き換えたいね」という話が出ていました。

Django 2系に書き換える対象は、3ページあります。

今回はコメント機能のチュートリアルを進める中で気づいたことを書きます。
Extensionsに取り組まれる方の参考になれば幸いです。

前提

  • macOS 10.13.6
  • Python 3.6.6
  • Django 2.0.9
  • venvで仮想環境を作って動かしています(スクショ中に「Dockerで動かしています」とありますが、今回は当てはまりません)

コメント機能追加の流れ

大きく分けると3ステップです。

  1. コメントを管理画面から入力できるようにし、画面に表示する
    • コメントのモデルを追加
    • コメントのモデル用のマイグレーション実施
    • 管理画面から操作できるように設定
    • ブログアプリにコメント表示(記事詳細画面、記事一覧画面)
  2. ブログからコメントを作成できるようにする
    • コメント用のフォーム追加
    • 記事詳細画面からコメント入力画面へ遷移するように設定
      • 記事詳細画面のテンプレートにボタンを追加
      • コメント入力画面のURL設定
      • コメント入力画面のビュー関数作成
    • コメント入力フォーム追加
  3. コメントの承認機能と削除機能をつける
    • 記事詳細画面のテンプレートにコメント承認ボタン・削除ボタンを追加
    • コメント承認機能、コメント削除機能用のURL設定
    • コメント承認用ビュー関数、コメント削除用ビュー関数作成
    • 記事一覧に表示されるコメント数を承認されたコメント数に変更する

それでは順に見ていきましょう。

コメントを管理画面から入力できるようにし、画面に表示する

コメント用のクラス追加

blog/models.pyCommentクラスを追加します。

# importの行とPostモデルの定義は省略

class Comment(models.Model):
    post = models.ForeignKey('blog.Post', on_delete=models.CASCADE, related_name='comments')
    author = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    approved_comment = models.BooleanField(default=False)

    def approve(self):
        self.approved_comment = True
        self.save()

    def __str__(self):
        return self.text

Post(記事)とComment(コメント)の関係は

  • 1つのコメントは1つの記事につく (A)
  • 1つの記事は複数のコメントを持つ (B)

です。
commentをCommentモデルのインスタンスとすると、comment.postとしてコメントの紐づく記事にアクセスできます。(A)
postをPostモデルのインスタンスとすると、post.comment_set1として記事に紐づくコメント全体にアクセスできます。2 (B)
(B)のケースで、ForeignKeyrelated_name引数を設定することで、post.<related_name引数の値>というアクセスが可能になります。
(今回のコードではpost.commentsと書けるということです)

管理画面からコメントを作成できるようにする

新しくコメントモデルを追加した後は、マイグレーションを実施します。

$ python manage.py makemigrations blog
$ python manage.py migrate blog

管理画面からコメントが作成できるように、blog/admin.pyを編集します。

from django.contrib import admin
from .models import Post, Comment # Commentを追加

admin.site.register(Post)
admin.site.register(Comment) # 追加

f:id:nikkie-ftnext:20181229182250p:plain

コメントを表示する

管理画面から作成したコメントを表示します。
記事詳細画面、記事一覧画面の順で表示するコードを追加していきます。

blog/templates/blog/post_detail.html
(ブログを書いている途中、詳細画面に<hr>タグ(記事本文とコメントを分けるための水平線を表示)を書き忘れていることに気づきました。
以下のスクショの詳細画面には水平線が表示されていませんが、本来は表示されるはずです。)

{% extends 'blog/base.html' %}

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <!-- 追加 -->
  <hr>
  {% for comment in post.comments.all %}
    <div class="comment">
      <div class="date">{{ comment.created_date }}</div>
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
  <!-- 追加 終わり -->
{% endblock %}

{% empty %}というタグが使われていますが、これは{% for %}タグで「ループさせようとした配列が空、または存在しなかった場合に表示する文字列を指定」するものだそうです。3

コメントの本文の表示にlinebreaksというフィルタが使われています。
記事の本文の表示ではlinebreaksbrでした。<p>{{ post.text|linebreaksbr }}</p> 4
2つの挙動の違いは以下の通りです。

  • linebreaksbrは、改行を<br>タグに置き換えます5
    • Joel\nis a\n\nslugを渡すと、Joel<br>is a<br><br>slugとなりました
  • linebreaksタグは空行が続く改行を<p>タグに置き換え、改行を<br>タグに置き換えます6
    • Joel\nis a\n\nslugを渡すと、<p>Joel<br>is a</p><p>slug</p>となりました

記事詳細画面にコメントが表示されるようになりました!
f:id:nikkie-ftnext:20181229182308p:plain

コメントにスタイルを設定します。

blog/static/css/blog.css

/* これまでのスタイル設定の下に追加 */
.comment {
  margin: 20px 0px 20px 20px;
}

スタイルを設定するとコメントが右に寄ります。
f:id:nikkie-ftnext:20181229182337p:plain

続いて、記事一覧画面に、各記事に紐づくコメントの数を表示します。

blog/templates/blog/post_list.html

{% extends 'blog/base.html' %}

{% block content %}
  {% for post in posts %}
    <div class="post">
      <div class="date">
        <p>published: {{ post.published_date }}</p>
      </div>
      <h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
      <p>{{ post.text|linebreaksbr }}</p>
      <!-- 以下の1行を追加するだけです -->
      <a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.comments.count }}</a>
    </div>
  {% endfor %}
{% endblock %}

post.comments.countで各記事のコメント件数が取得されます。
Djangoシェルで試してみました。

>>> from blog.models import Post
>>> post = Post.objects.get(pk=1)
>>> post.comments.count()
2

f:id:nikkie-ftnext:20181229182405p:plain

ブログからコメントを作成できるようにする

コメント用のフォームを追加

blog/forms.pyにコメント用のフォーム(CommentForm)を追加します。

from django import forms

from .models import Post, Comment # Commentのimportを追加

class PostForm(forms.ModelForm):
    # 変更はないため省略

# 以下を追加する
class CommentForm(forms.ModelForm):

    class Meta:
        model = Comment
        fields = ('author', 'text',)

コメント入力画面へ遷移するように設定

記事詳細画面からコメント入力画面へ遷移するように設定していきます。
Django URL · Django Girls Tutorial 以降で経験したように、エラーを出しながら1つ1つ解決していきます。

記事詳細画面のテンプレートに、コメント入力画面へ遷移するボタン(aタグ)を追加します。
blog/templates/blog/post_detail.html

{% extends 'blog/base.html' %}

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <hr>
  <!-- 追加 -->
  <a class="btn btn-default" href="{% url 'add_comment_to_post' pk=post.pk %}">Add comment</a>
  <!-- 追加 終わり -->
  {% for comment in post.comments.all %}
    <div class="comment">
      <div class="date">{{ comment.created_date }}</div>
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
{% endblock %}

記事詳細画面にアクセスすると(URL例:http://localhost:8000/post/1/)、NoReverseMatchというエラーが出ます。(図はExtensions参照)
これは、追加した箇所のhref="{% url 'add_comment_to_post' pk=post.pk %}が原因です。
add_comment_to_postというURLが未設定のために発生しています。
では、blog/urls.pyで設定しましょう。

from django.urls import path
from . import views

urlpatterns = [
    # これまで作成したpath()関数に変更はなし
    # リストの末尾の要素として以下の1行を追加する
    path('post/<int:pk>/comment/', views.add_comment_to_post, name='add_comment_to_post'),
]

すると、別のエラーAttributeErrorが現れ、runserverに失敗します。(NoReverseMatchというエラーを解決し、先に進んだと考えましょう)

f:id:nikkie-ftnext:20181229182426p:plain

blog/urls.pyに追加した行で、http://localhost:8000/post/1/comment/のようなURLに対して、blog/views.pyadd_comment_to_post関数を実行するように設定しています。
ところが、現時点ではblog/views.pyadd_comment_to_post関数が見つからないため、エラーとなっています。
では、blog/views.pyadd_comment_to_post関数を用意しましょう。

この解決は後編の記事に続きます。

脚注


  1. 「ForeignKey 定義内の related_name パラメータをセットして FOO_set の名前をオーバーライドできます」ref: https://docs.djangoproject.com/ja/2.1/topics/db/queries/#backwards-related-objects

  2. Djangoモデル の章でPostモデルにauthor = models.ForeignKey('auth.User', on_delete=models.CASCADE)と定義しています。これにより、me = User.objects.get(username='ftnext')がauthorとなる記事はme.post_set.all()で取得できます。(Djangoシェルで確認する場合は、まずfrom django.contrib.auth.models import Userを実行してください)

  3. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#for-empty

  4. https://tutorial.djangogirls.org/ja/template_extending/

  5. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaksbr

  6. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaks

イベントレポート | #imas_hack にてPythonでアイドルのシルエットをアスキーアートっぽくしました

はじめに

いつも心は虹色に! nikkieです。
忙しい年の瀬ではありますが、アイマスハッカソンに参加してきました。
作りたいプロダクトのアイデアを一通り試すことができ、他の方の取り組みからエンジニアという副業をアイマスでハックするという視点に気づくことができました。

イベントの概要

【増枠】アイマスハッカソン2018 - connpass

プロデューサーの皆様のあふれんばかり愛を表現する場として今回のハッカソンを企画しました。
皆様のアイドルへの愛を表現する場として、 また同僚のアイドルへの愛を感じる場として、是非お楽しみください!

2016年の初回から存在は知っていたアイマスハッカソン
バリバリ活動されている方の中に入ることに参加前は不安も感じていました。
プロデューサーとして愛はあれども副業メインすぎるためです。
参加してみたところ、不安は杞憂で、1日楽しめました。

プロデューサーとしてのnikkie

取り組んだこと

ニコニコ動画で見た、アイドルのシルエットのアスキーアートをプログラムから自動生成することに取り組みました。

▼こんなアスキーアートを作りたい f:id:nikkie-ftnext:20181223003240j:plain

シルエットの切り出しについては、前日に触っています:イベントレポート | 第94回 #pyhack にてDjango Girls Tutorial Extensionで手を動かし、Background Removal APIを触りました - nikkie-ftnextの日記
シルエットが切り出された前提で、アスキーアートのように画像を加工することに取り組みました。

  • 色付きの四角と白い四角を並べて表現することにする(まずMVPを作る)
  • 色付きの四角はアイドルのイメージカラーとする2
  • 四角に置き換える際は、以前取り組んだモザイクアートのプログラムが流用できそう
  • まずシルエットを2色に減色する。四角で置き換えるため格子状に区切り、どちらの色が多いかで、色付きの四角で置き換えるか、白い四角で置き換えるか判断する

成果物

f:id:nikkie-ftnext:20181224232020p:plain

ソースコードはこちら:
20181223 #imas_hack · GitHub

開発環境

  • Python 3.6.6
  • jupyter==1.0.0
  • matplotlib==3.0.2
  • numpy==1.15.4
  • Pillow==5.3.0
  • scikit-learn==0.20.2

notebookのポイント

画像の原色処理には以下を使います。

Pillowを使う理由は、キャラクターを抜き出した画像の仕様によります。
remove.bg で抜き出しているのですが、背景の部分の画素はA(alpha 透過度)が0に設定されているだけで、RGBの色情報を持ちます。
次のQiita記事を試したところ、背景込みで2色に原色した画像ができました:【機械学習】scikit learnでの画像減色プログラム - Qiita
(なお、KMeansでのクラスタリングでRGBの値のみを使うのは妥当と考えています)
元の画像のAの情報を保持する必要があるようなので、opencvから使い慣れたPillowにスイッチしました。
減色後に元の画像と同様のAの値を設定することで背景を消すことができました。

減色処理に使う画素の範囲についても2通り考えられます。
一つは背景込みで全部の画素を使う方法、もう一つはA=0となっている背景の画素を除く方法です。
現時点ではどちらを使うかは画像によるという結論です。
ソースコード箱崎星梨花のケースでは全部の画素を使ったところイマイチだったので、背景の画素を除きました)

numpyでの画像の扱いについて

  1. Pillowで画像を読み込み、RGBAのタプルからなるリストを作る(画像の縦と横の情報を消し、一列に並べた状態):getdata
  2. 1のリストの各要素からRGBの値のみ取得して新たなリストを作る(このRGBのデータをクラスタリングに使う。ここで背景の画素を除く場合もある)
  3. 2のRGBのリストを行列に変換(2で画素を除いていない場合、サイズ:(画像の縦 × 横, 3チャネルRGB))
  4. 3の行列をKMeansに渡してクラスタリング。2色を見つける
  5. 3の行列について各画素がどちらの色になるか判定する。このとき1で読み込んだ画素のAの情報を追加する(2で画素を除いていない場合、サイズ:(画像の縦 × 横, 4チャネルRGBA)という行列ができる)
  6. 画像を表す状態に行列のサイズを変換する:reshape
  7. 行列からRGBAの画像に変換する:fromarray

5ではnp.uint8という型を指定する必要がありました。(これを指定しないと画像が表示されません)
また5でnp.append()を使って一次元配列の末尾に要素を追加するのにハマりました。
参考:配列末尾へ要素を追加するNumPyのappendの使い方 - DeepAge

第三引数を指定しないと第一、第二引数で指定された配列のshapeとは関係なく一次元配列が生成されます。

2色に減色した画像を色付きの四角と白い四角で作成する処理は、過去に作ったモザイクアートのプログラムをベースにしています。

  • 画像を5×5の格子に分割(余りが出ないようにするためにサイズが5の倍数になるようにcropしておく)
  • 減色処理のうちどちらの色が多いか数える。ここで多い方の色で塗ると減色した2色からなるモザイク調になる
  • 塗り方の指定をアイドルのイメージカラーと白色に変更(ここは人の目で見て対応させています)

改良点はいくつかありますが、アイデアが実現できたのでハッカソンでの進捗には満足しています。

  • ノートブックを複製しているので、パッケージ化する
  • 減色する色を3−4色に増やし、この色はイメージカラーの四角、この色は白い四角というように人の目で判断する
  • 四角以外の図形を導入してみる

あにべんLT再演しました

副業メインでも愛を叫びたかったので、5分LTしました。
「もう時間がないんです」を差し込んでもらえたり、春香カラーのサイリウム降っていただけたり、発表してよかったです。

他の方の取り組みから

今回参加して気づいたのはエンジニアという副業をアイマスでハックする視点です。
具体的には、

  • 新しい技術を触るときにアイマスへのコントリビュートを兼ねた開発をする(例:Vue.jsを触ってアイマスの〇〇を作る)
  • 開発体験をアイマスでハックする(テストデータにアイドルの名前を使うことでノッて開発できる)

の2つで、「個人開発の中に取り入れたいな」と思っています。

皆さんの取り組みは非常に興味深くて、

  • 曲同士の似た部分を判定
  • Dockerコンテナの名前をアイマスっぽくする

などなど、成果物が気になる取り組みがたくさんありました。
また、コードが書けることはコントリビュートに必須ではないことも分かりました。
Microsoft Flow」などノンコーディングなツールでカバーできるため、大事なのはコントリビュートしたい強い気持ちと言えそうです。

知った中で気になったものを挙げておきます。

感想

技術で真剣に遊ぶのはやっぱりいいですね。
もくもく会で少しずつでも積み上げてきたから、今回やりたいことが一通り実現できたのだと考えています。
全力で遊んで少しはできるようになった減色処理やnumpyでの画像の扱いが、エンジニアとしての幅を広げることにもなると考えています。

参加者は文脈を共有しているので、LTはめちゃくちゃ盛り上がりました。
LT中にアップルパイつまみ食いしちゃったり4、マイニング始めたりと、字面だけ見たらカオスすぎですが、楽しませていただきました。
LTを盛り上げられるのも、歴戦のプロデューサーだからこそだと思います。

会場スポンサーのSpeeeさん、オフィスの蔵書量がすごかったです。
図書館になっていて、同じ背表紙の色のオライリー本で各段が埋まっていて圧巻でした。
同じスペースでOSS Daysというイベントを定期的に開催されているそうで、またお邪魔してみたいです。

参加者、運営者、スポンサーの皆さま、1日どうもありがとうございました。
本当に楽しかったので、今後も参加していきたいです。


  1. いわしまんさんのブログ記事 【イベント】WEBエンジニア勉強会#10にお邪魔してきた話 - Rのつく財団入り口 より。ハッカソンに飛び込むのにこの言葉に背中を押していただきました。ありがとうございます。(次回お会いしたら、直接お礼をお伝えしよう)

  2. アイドルのイメージカラー - アイマスDB(情報まとめ)

  3. https://kotobank.jp/word/RDF-49

  4. 「アップルパイ・プリンセス」でアップルパイを作る - #つくりおき

イベントレポート | 第94回 #pyhack にてDjango Girls Tutorial Extensionで手を動かし、Background Removal APIを触りました

はじめに

いつも心は虹色に! nikkieです。
2018年のもくもく会納めに #pyhack に参加してきました。

勉強会の概要

(第94回)Python mini Hack-a-thon - connpass

スプリントのゆるい版みたいな感じで各自自分でやりたいことを持ってきて、勝手に開発を進めています。参加費は無料です。 初めての方も常連さんもぜひご参加ください。

今回は、常連さんから初参加の方まで、満員御礼といった感じでした。

取り組んだこと

  • Django Girls Tutorial ExtensionのDjango2系での動作確認
  • 画像の背景を除いてくれるAPIを試す(Background Removal API

ExtensionのDjango2系での動作確認

Django Girls Tutorialの拡張版(翻訳進行中)のコメントの章Django 2.x系に書き換えたらどうなるかを検証しました。

この章に取り組むと、コメントの表示機能や入力機能、管理者が承認/削除する機能が追加されます。
Django 2.x系向けの変更点は、urls.pyをpath関数で書き換えるくらいでした。(url関数でも動くので必須ではありません)

■記事一覧画面(承認されたコメントの数が表示される)
f:id:nikkie-ftnext:20181223003117p:plain

■記事詳細画面(ログインユーザはコメントを管理できる。ログインしていないユーザもコメントが可能)
f:id:nikkie-ftnext:20181223003200p:plain

検証が終わったので、原文のコードについてPRを出しました。
今後は日本語翻訳のレビューを進めていきます。
検証時に気づいたことをブログにまとめているところです。(長くかかりそうなので次の作業へスイッチしました)

画像の背景を除いてくれるAPI

人物が写った画像(写真)の背景を消し、人物だけを取り出すAPIを最近知りました。

知ったときに思ったのは、「アニメ画像からアニメキャラクターを取り出せるのでは?」でした。

切り出したキャラクターを加工して、以下のアスキーアートをプログラムから作ろうとしています。
色付きの四角と枠線だけの四角から構成されるシルエット画像です。
(2018年のニコニコ動画でのアイドルマスターシリーズ一挙放送で見かけたアスキーアートです)
f:id:nikkie-ftnext:20181223003240j:plain

手元にモザイクアートのプログラム(Python製)があるので、これをベースに実現できると考えています。
背景を除くAPIで切り出したキャラクターだけの画像について、格子状に分割し、各領域を色で塗りつぶすのか否かを判定するというロジックで実装予定です。
(23日の某ハッカソンで試します)

このAPI(Webアプリ)で 遊んで どんな画像なら切り出せるかを検証していました。

  • キービジュアルのような大きめのアニメイラストは切り出せる(響け!ユーフォニアム、君の名は)
  • ARキャラクターは切り出せる(デレステや舞台めぐりのAR写真)
  • 私のツイッターアイコンのような顔のアップの画像からは切り出せない
    • 画像の領域を広げてみたが、今のところ成功していない

API Beta版のテストユーザを募集していたので、申込んでみました。(想定していない使い方だと思いますが、通ることを祈ってます)

他の方の取り組みから

自分のメモとしてツイートを一元化。

感想

本編から懇親会と非常に楽しい時間でした。
#pyhack を機に、ほぼ毎週のようにPythonもくもく会に参加し始めて1年が経ち、振り返ると感慨深いものがありました。
次回は冬山合宿、せっかくのまとまった開発期間なので、取り組むことを決めて参加しようと思います。
皆さま、1日ありがとうございました!