pyhaya’s diary

機械学習系の記事をメインで書きます

Google Cloud Platform (GCP)を気軽に勉強する

久しぶりのエントリーです。今日はGoogle Cloud Platform (GCP)について書いてみたいと思います。 GCPというのは、Googleが提供するクラウド上のインフラサービスで、簡単なウェブサービスから、ビッグデータ解析、機械学習など幅広い用途に使えることから今大きな注目を集めています。

提供しているサービス

f:id:pyhaya:20190226135158j:plain

GCPは実に様々なサービスを提供しています。下に例を挙げます。

  • Compute Engine
  • BigQuery : データウェアハウス
  • Cloud Datalab : Jupyter Notebookのような対話型環境
  • Cloud Dataflow : データを分散コンピューティングにより高速に処理
  • Cloud Strage : ストレージサービス

...

全部書いたらきりがないほどあります。ほかに何があるか見たい方は下のリンクをどうぞ
cloud.google.com

何が良いのか

このサービスを用いることで、例えば企業は自前でサーバーを構築・メンテナンスをする必要がなくなり、設計やコードを書くことに集中できるようになります。また、Googleの計算資源を用いることができるので、GPUやTPUといった、特に機械学習ではうれしいサービスを気軽に利用できるようになります。

料金の壁

しかし、このサービスはもちろん無料ではありません。使った時間と資源の量に比例して料金が発生します。GCPには料金の見積もりサービスもついているので、気になる方は計算してみるとよいと思います。

cloud.google.com


試しに、個人の趣味で利用することを想定して、Compute Engineを

  • 1 instance
  • CPU: 60個
  • RAM: 300GB
  • GPU: 8個
  • SSD: 6x375GB
  • 4 hours/month (週1で1時間使用を想定)

で見積もってみると料金は$17.53 / monthでした。これが高いと感じるか安いと感じるかは、人によると思いますが、私の場合は、GCP未経験なのでまずは無料で試してみたいと考えました。

GCPには無料トライアルがありますが、やはり最初は基本的なことを教わりながらやりたいと考えていたらCourseraでよいコースがありました。

CourseraのGCPコース

www.coursera.org

このコースは、Google Cloudが提供しているコースで、全5コースからなり、GCPの基本を学ぶことができます。また、Courseraが提供しているので7日間は無料で受講できます(修了証が欲しければ有料になってしまいますが...)。以下にこのコースの特徴を紹介します。

GCPの全体像を理解できる

全5コースで、GCPを使ったデータ処理の基本を学ぶことができます。計算環境の構築方法から、BigQueryを用いたデータ操作、そしてTensorflowを用いた学習まで一通り学べます。

GCPを実際に使える

これが私にとっては最もありがたかったことですが、コースの課題を解く際にGCPを実際に使うことができます。コースで一時利用用のアカウントを支給してくれるので、実際に講義で学んだことをGCP上で試すことができます。

一つ一つのコースはそんなに重くない

全部で5コースもあると、無料で受講したいと考えている人からすると多すぎるように感じます。しかし、実際には1つ1つのコースはそんなに重くなくて、私の場合には無料期間中に4コース目まで修了できました。

日本語の字幕付き

1コース目はなかった気がするのですが、2コース目からは講義動画で日本語字幕を表示できます。英語が苦手な人でもちゃんと理解ができます。




企業でも最近ではGCPを利用するところも増えていると思うので、勉強しておいて損はないと思います。(最後にとりあえず注意しておくと、Courseraは8日目に入る前に手動でCancel Subscriptionしないと勝手に料金が発生してしまうので、無料にこだわる人はそこだけは注意してください。)

Djangoで家計簿のWebアプリケーションを作る8 Chartjsを使ってグラフを描画する

久しぶりのエントリーです。今回は前々から言っていた、JavaScriptを用いたグラフの描画を行います。これまではPythonのMatplotlibを使ってSVG画像を作り、それをページに表示してきました。これをChartjsを使って書き直すことで、グラフ上にカーソルを移動させたときに値を表示するなどの、画像ではできなかったことを実現します。

前回の記事は下にあります。
pyhaya.hatenablog.com

Chartjsとは

ChartjsとはJavaScriptを使ってHTML上にきれいなグラフを表示させることができるものです。f:id:pyhaya:20190119190946p:plain
Chartjsを使ったグラフは公式サイトに様々載っていますが、どれもとてもきれいです。
www.chartjs.org

これはどうしても使ってみたいと思い、試行錯誤してみました。

どうやってDjangoのプロジェクトでChartjsを使うか

ChartjsはプロットするデータをJSON形式で入力します。「Chartjs 使い方」などと検索するとプロットデータをソースコードにべた書きする例がいくつもヒットします。しかし今回の例だとプロットデータはデータベースからとってきたいのでここが悩みの種です。私はこれまでJavaScriptを触ったことがなかったので、ずいぶん苦労しました。

いろいろ試行錯誤した結果、最終的には下のような方法で実現できました。

調べていると、JSONファイルを出力してそれをJQueryで読み込む例などが出てきたのですが、JS初心者には難しすぎたので(笑)今回は上の方法でやります。

コードを書く

HTMLを編集する

まずはHTMLから整えていきます。前回までのHTMLファイルはさすがにひどかったので今回はBootstrapを組み込んでスタイルを一新しました。

<!DOCTYPE html>
{% load static %}
<html lang='ja' dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>MoneyBook</title>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.1.4/Chart.min.js"></script>
    <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
    <link rel="stylesheet" type="text/css" href="{% static 'moneybook/main_style.css' %}">
    <script type="text/javascript" src="{% static 'moneybook/js/data.js' %}"></script>
    <script type="text/javascript" src="{% static 'moneybook/js/draw.js' %}"></script>
    <script>
      $(function () {
        let dateFormat = 'yy-mm-dd';
        $('#date_choice').datepicker({
        dateFormat: dateFormat
        });
      });
    </script>
  </head>
  <body>
    <div class="container">
      <div class="container-inner">
        <!-- ヘッダー -->
        <div class="row top-bg">
          <div class="col-md-8" >
            <h1>{{ year }}年 {{ month }}月</h1>
          </div>
          <div class="col-md-2 text-center mt-4">
            <a href="/moneybook/{{ prev_year }}/{{ prev_month }}">
              <h4> << {{ prev_month }}月</h4>
            </a>
          </div>
          <div class="col-md-2 text-center mt-4">
            <a href="/moneybook/{{ next_year }}/{{ next_month }}">
              <h4>{{ next_month }}月 >> </h4>
            </a>
          </div>
        </div>

        <!-- 支出の登録フォーム -->
        <div class="row">
          <div class="card">
            <div class="in-card">
              <h2>支出の登録</h2>
              <form action="" method="post" autocomplete="off" style="margin-top:3%;">
                {% csrf_token %}
                {% if form.errors %}
                  {% for errors in form.errors.values %}
                    {% for error in errors %}
                      <div class="alert alert-danger" role="alert">
                        {{ error }}
                      </div>
                    {% endfor %}
                  {% endfor %}
                {% endif %}
                <div class="form-group">
                  <div class="form-inline">
                    {{ form.used_date }}
                    {{ form.cost }}
                    {{ form.category }}
                  </div>
                  <hr>
                  <div class="form-inline">
                    {{ form.money_use }}
                    <input type="submit" name='add' class="btn btn-primary" value="登録">
                  </div>
                </div>
              </form>
            </div>
          </div>

          <!-- 総支出の表示 -->
          <div class="card">
            <div class="in-card">
              <h2>今月の総支出</h2>
              <div class="text-center total_cost">
                <h3 class="total">{{ total_cost }}円</h2>
              </div>
            </div>
          </div>
        </div>

        <div class="row">
          <!-- 支出履歴テーブル -->
          <div class="card">
            <div class="in-card">
              <h2>支出履歴</h2>
              <table style="table-layout:fixed;width:100%;margin-top:20px;">
                <thead class="pay_history">
                  <tr>
                      <th class="date">日付</th>
                      <th class="use">用途</th>
                      <th class="category">カテゴリー</th>
                      <th class="cost">金額</th>
                      <th class="delete">削除</th>
                  </tr>
                </thead>
                <tbody class="pay_history">
                  {% for m in money %}
                  <tr class="table_data">
                      <td class="date" id="table">{{ m.used_date.month }}/{{ m.used_date.day }}</td>
                      <td class="use" id="table">{{ m.money_use }}</td>
                      <td class="category" id="table">{{ m.get_category_display }}</td>
                      <td class="cost" id="table">{{ m.cost }}円</td>
                      <td class="delete" id="table">
                          <form action="" method="post">
                              {% csrf_token %}
                              <input type="hidden" name="used_date" value={{ m.used_date }}>
                              <input type="hidden" name="money_use" value={{ m.money_use }}>
                              <input type="hidden" name="cost" value={{ m.cost }}>
                              <button class="btn btn-outline-danger" type="submit" name="delete">
                                削除
                              </button>
                          </form>
                      </td>
                  </tr>
                  {% endfor %}
                </tbody>
              </table>
            </div>
          </div>

          <!-- 支出額の推移グラフ -->
          <div class="card">
            <div class="in-card">
              <h2>支出額の推移</h2>
              <canvas id="data"></canvas>
              <script type="text/javascript" src="{% static 'moneybook/js/draw.js' %}"></script>
            </div>
          </div>
        </div>
      </div>
    </div>
    <a href='/logout'>LOGOUT</a>
  </body>
</html>

描画に用いるのはdraw.jsという名前のJavaScriptファイルです。

JavaScriptコードを書く

PythonでJavaSciptコードを書くのですがさすがに全部書くのは嫌なので、2つに分けます。

  • data.js
  • draw.js

data.jsにプロットするデータが含まれるようにして、他の部分はdraw.jsに書きます。

draw.js

var ctx = document.getElementById("data").getContext("2d");
var myChart = new Chart(ctx, json);   #変数jsonはdata.jsに書く

PythonJavaScriptコードを生成する

ここが核になります。JavaScriptコードを生成するとかいうとなんか難しく聞こえますが open(file_name, 'w')を使って書き込むだけです。以下がソースコードです。

views.py

from django.shortcuts import render, redirect
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views import View
from django.utils import timezone
import calendar
import os


from .models import ExpenditureDetail
from .forms import ExpenditureForm

TODAY = str(timezone.now()).split("-")
# Create your views here.


class MainView(LoginRequiredMixin, View):
    login_url = "/login"
    redirect_field_name = ""

    def get(self, request, year=TODAY[0], month=TODAY[1]):
        money = ExpenditureDetail.objects.filter(
            used_date__year=year, used_date__month=month, user_id=request.user.id
        ).order_by("used_date")

        total = 0
        for m in money:
            total += m.cost

        next_year, next_month = get_next(year, month)
        prev_year, prev_month = get_prev(year, month)

        context = {
            "year": year,
            "month": month,
            "next_year": next_year,
            "next_month": next_month,
            "prev_year": prev_year,
            "prev_month": prev_month,
            "total_cost": total,
            "money": money,
            "form": ExpenditureForm(),
        }

        self.draw_graph(year, month, request.user.id)

        return render(request, "moneybook/mainview.html", context)

    def post(self, request, year=TODAY[0], month=TODAY[1]):
        data = request.POST
        form = ExpenditureForm(data)

        if "add" in data.keys():
            if form.is_valid():
                used_date = data["used_date"]
                cost = data["cost"]
                money_use = data["money_use"]
                category_choices = data["category"]

                used_date = timezone.datetime.strptime(used_date, "%Y-%m-%d")

                ExpenditureDetail.objects.create(
                    user_id=request.user.id,
                    used_date=used_date,
                    cost=cost,
                    money_use=money_use,
                    category=category_choices,
                )

            money = ExpenditureDetail.objects.filter(
                used_date__year=year, used_date__month=month, user_id=request.user.id
            ).order_by("used_date")

            total = 0
            for m in money:
                total += m.cost

            next_year, next_month = get_next(year, month)
            prev_year, prev_month = get_prev(year, month)

            context = {
                "year": year,
                "month": month,
                "next_year": next_year,
                "next_month": next_month,
                "prev_year": prev_year,
                "prev_month": prev_month,
                "total_cost": total,
                "money": money,
                "form": form,
            }

            self.draw_graph(year, month, request.user.id)

            return render(request, "moneybook/mainview.html", context)

        elif "delete" in data.keys():
            used_date = data["used_date"]
            cost = data["cost"]
            money_use = data["money_use"]

            used_date = used_date.replace("年", "-").replace("月", "-").replace("日", "")
            y, m, d = used_date.split("-")

            ExpenditureDetail.objects.filter(
                used_date__year=y,
                used_date__month=m,
                used_date__day=d,
                cost__iexact=cost,
                money_use__iexact=money_use,
            ).delete()

            return redirect(to="/moneybook/{}/{}".format(year, month))

    def draw_graph(self, year, month, user_id):
        money = ExpenditureDetail.objects.filter(
            used_date__year=year, used_date__month=month, user_id=user_id
        ).order_by("used_date")

        last_day = calendar.monthrange(int(year), int(month))[1] + 1
        day = [i for i in range(1, last_day)]
        cost = [0 for i in range(len(day))]
        for m in money:
            cost[int(str(m.used_date).split("-")[2]) - 1] += int(m.cost)

        text_day = ",".join(list(map(str, day)))
        text_cost = ",".join(list(map(str, cost)))

        json_template = (
            """var json = {
            type: 'bar',
            data: {
                labels: [
        """
            + str(text_day)
            + """
                ],
                datasets: [{
                    label: '支出',
                    data: [
        """
            + str(text_cost)
            + """
                    ],
                    borderWidth: 2,
                    strokeColor: 'rgba(0,0,255,1)',
                    backgroundColor: 'rgba(0,191,255,0.5)'
                }]
            },
            options: {
                scales: {
                    xAxes: [{
                        ticks: {
                            beginAtZero:true
                        },
                        scaleLabel: {
                            display: true,
                            labelString: '日付',
                            fontsize: 18
                        }
                    }],
                    yAxes: [{
                        ticks: {
                            beginAtZero:true
                        },
                        scaleLabel: {
                            display: true,
                            labelString: '支出額 (円)',
                            fontsize: 18
                        }
                    }]
                },
                responsive: true
            }
        }
        """
        )
        with open(
            os.path.dirname(os.path.abspath(__file__)) + "/static/moneybook/js/data.js",
            "w",
        ) as f:
            f.write(json_template)


def get_next(year, month):
    year = int(year)
    month = int(month)

    if month == 12:
        return str(year + 1), "1"
    else:
        return str(year), str(month + 1)


def get_prev(year, month):
    year = int(year)
    month = int(month)
    if month == 1:
        return str(year - 1), "12"
    else:
        return str(year), str(month - 1)

とりあえずviews.pyの中身を全部載せていますが、描画に関係しているのは一番下のメソッドのみです。ここでやっていることは上のコメントを読めば大体わかると思いますが、JavaScriptコードをstringで用意しておいて、そこにデータを埋め込んでいく形になっています。ダブルクオーテーション3つでstringを作るとformat関数がうまく動いてくれないのでstringどうしを足しているため汚くなってしまっていますが、やっていることは単純です。

ちゃんと整ったコードは以下のサイトが非常に参考になると思います。
qiita.com

ここまでの結果

ここまでやってページを見てみると、次のようになります。

f:id:pyhaya:20200410132430p:plain
今話題のものを買い漁ってるふうにしてみました。ちゃんとできてますね。

まとめ

今回は、Chartjsを使ってグラフをより良いものにしました。その方法はJavaScriptコードをPythonから作るというものでした。これは私がJavaScriptわからな過ぎてとった苦肉の策なので、詳しい人から見たらベストプラクティスではないかもしれませんので注意してください(詳しい人いたら教えてください)。ぜんたいのコードは以下の GitHub リポジトリで公開しているのでここに乗せていない部分が気になる方はぜひこちらも見てください!

github.com

「How Google Works」を読んだ

Kindleで何か面白そうな本ないかなと探していたら、「How Google Works」という本を見つけました。Googleといえば知らない人がいないほどのIT分野の巨人ですよね。Googleでは社員はどのように働いているのか、そこに興味があって購入してみました。

誰が書いているのか

著者をみて見ると何人かの共著であることが見て取れます。どの人もググるとわかりますが、Googleの成長に大きな役割を果たした人ばかりです。
エリック・シュミット - Wikipedia
Jonathan Rosenberg (technologist) - Wikipedia
ラリー・ペイジ - Wikipedia

訳者は土方奈美という方です。訳者に関しては詳しくないのですが、翻訳本でよくある「訳がクソ」という感じは全くありませんでした。

どんな本だったか

私は、この本にエンジニア視点からの働き方というのを期待していました。しかし、実際に読んでみると、どちらかというと経営者の視点からの本でした(副題に「私たちの働き方とマネジメント」とあるので当たり前といえば当たり前なのですが...)。なので、最初はミスったかなと感じながら読んでいたのですが、読んでいるうちに考えは変わっていきました。本の中では繰り返しIT分野の成長がほかの分野と比べていかに急速かというのを述べていますが、このように状況が目まぐるしく変化するような状況では、エンジニアも経営者のような全体を俯瞰するような視点が必要なのだと感じます。

本書では、いくつかのテーマについて、Googleがどのような歴史をたどってきたかも見ながら解説しています。そのテーマとは

です。全体を読んでいて感じたのは、Googleがいかにエンジニアを大事にしているかということです。どうすれば優秀なエンジニアを雇うことができるかどのような環境を作ればエンジニアの能力を最大限発揮させることができるか、そんな考えが一貫して根底にあるような感じがしました。

そして、この本ではGoogleの手掛けてきたプロジェクトがどのようにして進んできたのか、おなじみのGmailGoogle earthなどの例を紹介しています。いくつもの事例を読みながら感じたのは、Google内部での上下関係の希薄さとスピードを重視する姿勢です。ITは目まぐるしいスピードで日々進歩しているのでこのような姿勢が、プラットフォーマーであり続けるために必要不可欠であるということを感じさせられます。

こんな感じのことばかり書いてあると、読んでいて「Google入りてぇ」ってなって勝手に勉強のモチベーションが上がりますwww。同時に日本企業もこんな感じにならないかなとも思うのですが、日本はアメリカとはバックグラウンドが異なるので猿真似してもうまくいかないでしょう。日本企業はこれからどうするべきなのか、そんなことにも考えが行く本でした。

AtomとVSCodeのJupyter Notebook環境を比較した

仕事でよくJupyter Notebookを使ってのデータ解析を行うのですが、最近いちいちブラウザでNotebookを起動するのが面倒になってきました。そこでエディタを使ってJupyter Notebookを使おうと思って色々調べていたら、AtomVSCodeを使うのがどうやらよさそうだということがわかりました。

そこで、この記事では、両方の環境を使ってみてどちらがより使いやすいか比較してみたいと思います。

開発環境

環境構築

PythonとJupyter Notebookを使いたい場合には、Anacondaで入れてしまうのが一番簡単です(特にWindowsでは)。
weblabo.oscasierra.net

エディタをインストールする

AtomVSCodeのインストール方法は下のリンクからダウンロード、インストールするだけです。
atom.io
code.visualstudio.com

Python環境を整える

次に各エディタにPythonの環境を整えていきます。
Atomの場合:
https://hajipro.com/python/atom-python


VSCodeの場合:
docs.microsoft.com


手順はAtomのほうが圧倒的に楽です。

Jupyter Notebookを使う環境を整えていく

Atomの場合

AtomではFile -> Settingsを押すと、
f:id:pyhaya:20181202173420p:plain
という画面が出てきます。左側の列にCoreやらEditorやらいろいろ並んでいますが、一番下のInstallを選択し出てくる検索欄に「Hydrogen」と入れて検索します。後は出てきたHydrogenをインストールするだけです。

VSCodeの場合

こちらもAtomと似たような感じです。
f:id:pyhaya:20181202173854p:plain
VSCodeの場合には常に画面の一番左側にアイコンが5つ並んでいます。一番下の四角いやつがパッケージのインストールなどに使うものです。
このアイコンを押して、「Jupyter」と検索し、一番上に出てくるJupyterパッケージをインストールします。

使ってみる

いよいよ使ってみます。どちらのほうが使い勝手が良いでしょうか?

普通に使ってみる

まずは普通に、算術計算に使ってみます。次のコードを実行してみます。

x = 1
y = 2
x + y

両方ともコードブロックはブロックの先頭に#%%を入れることで表現できます。

Atom

f:id:pyhaya:20181202175025p:plain
Atomの場合はこんな感じ。ブロックの実行はAlt-Shift-Enterでできます。Shift-Enterだと一行ずつ実行になります。

VSCode

VSCodeの場合はこんな感じ。Shift-Enterで実行できます。Atomとは異なり画面分割されて出力されます。表示はこちらのほうがJupyter Notebookに近い印象です。
f:id:pyhaya:20181202175301p:plain

グラフを書く

下のコードを実行してみます。

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 10, 500)
y = np.sin(x)

plt.figure()
plt.plot(x, y)
plt.show()
Atom

f:id:pyhaya:20181202175919p:plain
きれいに出力されました。

VSCode

f:id:pyhaya:20181202180144p:plain

こちらもきれいに、、、とは言ってません。軸のラベルが背景の黒につぶされてしまっています。これをどうにかするには

plt.figure(facecolor='white')

と色を指定してやる必要があります。このめんどくささがあるので、描画に関してはAtomに軍配が上がりそうです。

CSVファイルを扱う

Atom

f:id:pyhaya:20181202180934p:plain
かなりシンプルな感じで出力されます。

VSCode

f:id:pyhaya:20181202181035p:plain
わかりにくいですが、行ごとに色が若干違っています。また、Atomとは異なり表にカーソルを持っていくと行がハイライトされます。この点はAtomより優れています。

まとめ

AtomVSCodeもよく使うようなJupyter Notebookの機能はちゃんと備わっているという感じです。ただ、細かいところで両者とも使いにくいところがありました。上には書きませんでしたが、VSCodeではos.chdir()をしてもセルが変わるとルートディレクトリに戻されるという謎の現象もありました。

どの機能を重要視するかによってどちらを選ぶかは変わりそうですが、私の場合は、グラフの色調整は結構気合い入れてやることが多いので、いちいち色指定しなくてよいAtomかなって感じです。

機械学習を原理から理解する 回帰

機械学習をただ使うだけでなく、どのような原理で動いているのか理解するために数学的な観点からちゃんとアルゴリズムを見てみます。

前回は線形回帰の記事を書きました。
pyhaya.hatenablog.com

今回は、前回の線形回帰のアルゴリズムを拡張して、高次の回帰をしていきます。

前回の復習(線形回帰)

前回はデータ点を直線で回帰するために、最小二乗法を使った方法について説明しました。ここでは回帰直線を


y  = \mathbf{w}\cdot\mathbf{x} \tag{1}

というように重みパラメータ\mathbf{w}とデータ\mathbf{x}内積で表現しました。そして、適切な重みを見つけるための指針として正解 tとの二乗誤差


\displaystyle \sum_{i=1}^n(y_i-t_i)^2 \tag{2}

を最小にしようということになり、行列計算によってこのような重みパラメータを得られることを見ました。

高次多項式での回帰

前回の記事だけでも、複数の特徴量の線形和の形での回帰は可能です。今回はさらにバリエーションを増やすために高次多項式での回帰を行います。つまり、回帰曲線は下のような式であらわされます。


y = w_1x + w_2x^2+w_3x^3+\cdots \tag{3}

見やすさのため変数は1つのものを書いています。また、いつも通りバイアスは重みパラメータ内部に入れています。

理論

実は、理論的にはこの回帰は線形回帰と同一の方法で実行することができます。というのも、(3)式はベクトルの形式で書き直すと


y = \mathbf{w}\cdot\mathbf{\phi}(x) \tag{4}

ここで\phi


\mathbf{\phi(x)} = (x, x^2, x^3,\cdots, 1)^T \tag{5}
です。最後の1はバイアスを重みパラメータの中に取り込んだために出てきた項です。この形は線形回帰でやったものと全く同じです。なので同じアルゴリズムで解くことができます。


A\mathbf{w}=b,\ \ \ \ \ \displaystyle (A =\sum_{i=1}^n \mathbf{\phi_i}\mathbf{phi_i}^T,\ \ \mathbf{b}=\sum_{i=1}^n y_i\mathbf{x_i}) \tag{6}

添え字iは訓練データの番号です。

実装(Python3)

上の理論を見ながらPythonで実装を行ってみます。

import matplotlib.pyplot as plt
import numpy as np

from sklearn.model_selection import train_test_split
from sklearn.datasets import make_regression

def Regression(X, y, dim):
    d = X.shape[1] + 1
    A = np.zeros((d+dim-1, d+dim-1))
    b = np.zeros((d+dim-1, 1))

    for i in range(len(X)):
        x = [] 
        for j in range(1, dim+1):
            x += list(X[i]**j)
        x.append(1)
        x = np.array(x).reshape((d+dim-1, 1))
        A += x.dot(x.T)
        b += y[i] * x

    return np.linalg.inv(A).dot(b)

if __name__ == "__main__":
    X, y = make_regression(
        n_samples=50,
        n_features=1,
        noise=4,
        random_state=42
    )

    dim = 2    #何次の多項式で回帰を行うか
    pred = Regression(X, y, dim)
    x = np.linspace(-2, 1.9, 50)
    y_pred = np.zeros_like(x)
    for i in range(0, dim):
        y_pred += pred[i][0] * x**(i+1)
    y_pred += pred[dim][0]
  
    plt.figure()
    plt.plot(X, y, 'o')
    plt.plot(x, y_pred)
    plt.show()

回帰結果を見てみる

回帰結果を次数を変えてみてみます。まずは二次の場合フィッティング結果は下のようになります。
f:id:pyhaya:20181207075342p:plain

回帰はうまくいっている印象です。次に次数を増やしてみます。十次のとき
f:id:pyhaya:20181207075443p:plain

明らかにノイズも拾って回帰してしまっています。これは訓練データを過度に学習してしまっている状況で、過学習状態となっています。

機械学習を原理から理解する 線形回帰

最近始めた機械学習の理論的な話の3つめです。相変わらず線形の話が続きます。過去記事を下に貼っておきます。

pyhaya.hatenablog.com
pyhaya.hatenablog.com

今回は、前回までの分類アルゴリズムとは異なり、回帰アルゴリズムについて書きます。

回帰とは

回帰というのは、データセットがあったときにパラメータと目的変数の間に成り立つ関係式を見つける操作です。実験とかで結果を理論曲線でフィッティングするイメージです。

...もう少し具体的な例を出すと、例えば、パラメータとして、過去1カ月分の降水量と気温、天候が与えられたときにある特定の日に収穫されたリンゴの糖度(目的変数)を予測するとかなると回帰を使います。降水量、気温、天候というデータを組み合わせることでリンゴの糖度を表現するわけです。

線形回帰

線形という表現からもわかるように、回帰の式に一次関数を使うのが線形回帰です。例えばパラメータが1つあって(xと書く)、そこからある変数の値(yと書く)を予測したいとします。いま、学習のためにいくつかデータが与えられていて、下のグラフのように分布しているとします。
f:id:pyhaya:20181129000333p:plain
これはおおむね線形に分布していることがわかります。これを一変数の一次関数で回帰したのがオレンジの直線になります。

回帰直線を求める

パラメータ

線形回帰をする際に求める必要があるのは次の2種類のパラメータです。一つは重みパラメータ \mathbf{w}そしてもう一つがバイアスbです。回帰に使うパラメータが1種類の時には重みパラメータは2次元平面上の直線の傾きを表します。


h(\mathbf{x}) = \mathbf{w}\cdot\mathbf{x}+b \tag{1}

ここで\mathbf{x}は与えられている学習データで、h(\mathbf{x})がそのデータから計算した目的変数の予測値です。ふつうは、バイアスも重みパラメータの中に取り込んだ下のような形を使います。


h(\mathbf{x}) = \mathbf{w}\cdot\mathbf{x} \tag{2}

ここで\mathbf{w}=(w_1, w_2,\cdots, b), \mathbf{x}=(x_1, x_2,\cdots, 1)

損失関数

回帰の際には、どれくらいうまく回帰ができているかを知るために損失関数を定義します。回帰によってこの損失関数を最小化することが目標になります。

線形回帰の際によく用いられる損失関数は二乗誤差です。二乗誤差は下のように定義されます。

\displaystyle
L_S(h)=\frac{1}{n}\sum_{i=1}^n(h(\mathbf{x_i})-y_i)^2 \tag{3}

nは学習データの数です。

損失関数を最小化する

損失関数をなるべく小さくするにはどのようにすればよいでしょうか?微分して0になる値を探します。

\displaystyle
\frac{d}{d\mathbf{x}}L_S(h)=\frac{2}{n}\sum_{i=1}^n(\mathbf{w}\cdot\mathbf{x_i}-y_i)\mathbf{x_i} =0 \tag{4}

ここで、2つ変数を導入します。

\displaystyle
A =\sum_{i=1}^n \mathbf{x_i}\mathbf{x_i}^T,\ \ \mathbf{b}=\sum_{i=1}^n y_i\mathbf{x_i} \tag{5}

Aは行列です。これを使うと、(4)式は下のように簡単に書くことができます。

 A\mathbf{w}=\mathbf{b} \tag{6}

つまり、パラメータはA逆行列が求まれば、


\mathbf{w}=A^{-1}\mathbf{b}\tag{7}

と求めることができます。

Pythonで書く

上の計算をPythonのコードに書き下すと下のようになります。

import matplotlib.pyplot as plt
import numpy as np

from sklearn.model_selection import train_test_split
from sklearn.datasets import make_regression


def LinearRegression(X, y):
    A = np.zeros((X.shape[1]+1, X.shape[1] + 1))
    b = np.zeros((X.shape[1]+1, 1))
    for i in range(len(X)):
        x = list(X[i])
        x.append(1)
        x = np.array(x).reshape((2, 1))
        A += x.dot(x.T)
        b += y[i] * x

    return np.linalg.inv(A).dot(b)


if __name__ == '__main__':
    X, y = make_regression(
        n_samples=50,
        n_features=1,
        noise=7,
    )

    w = LinearRegression(X, y)
    fit_x = np.linspace(-2, 2, 500)
    fit_y = fit_x * w[0, 0] + w[1, 0]
    plt.figure()
    plt.plot(X, y, 'o')
    plt.plot(fit_x, fit_y)
    plt.show()

9, 10行目でX.shape[1]+1のように+1しているのは、バイアスを重みパラメータの中に取り込むために、配列の要素数を1つ増やすためです。

このコードでは、簡単のために変数は1種類にしています。そのため計算結果は簡単に図示することができて、
f:id:pyhaya:20181129011247p:plain
うまくいっていることが見て取れます。

逆行列が存在しないとき

ここからは、少しだけ発展的な内容になります。

このアルゴリズムでは、計算に逆行列を使っています。しかし、逆行列は必ずしも存在するとは限りません。このようなときにはどのように計算すればよいか考えます。

逆行列が存在しないときというのは、線形代数の知識を使えば固有値に0を含むときであることがわかります。このようなときに行列Aを


A = VDV^\dagger \tag{8}

のように直交行列V固有値を対角成分にもつ対角行列Dを使って分解することができます。Dの対角成分に0が含まれなければ、Dのすべて対角成分をすべて逆数にした行列D^+がDの逆行列になり、実際、


VDV^\dagger VD^+V^\dagger=VDD^+V\dagger=VV^\dagger=1 \tag{9}

のように、真ん中から次々に単位行列に代わっていきます(直交行列Vは転置して複素共役をとったV^\daggerと掛けると単位行列になる性質を利用しています。 Wiki参照)。

対角成分に0が含まれていては、このような操作は行うことができません。この時にはD^+を次のように定義します。


D^+_{ii} = \begin{cases}1/D_{ii}\ \ \ \ (D_{ii}\neq 0)\\ 0 \ \ \ \ (\mathrm{else})\end{cases} \tag{10}

要するに、逆数とれるならとって、とれないなら0のままにしておくということです。このようなD^+によって計算されるA

A^+=VD^+V^\dagger \tag{11}

として逆行列の代わりに使えば、

\displaystyle
A\mathbf{w}=A(A^+\mathbf{b})=VDV^\dagger(VD^+V^\dagger)\mathbf{b} = \sum_{i:D_{ii}\neq0} \mathbf{v}_i\mathbf{v}_i^T\mathbf{b} \tag{12}

となります。A\mathbf{v}V中の列ベクトルです。最右辺を見てみると、A\mathbf{w}D_{ii}\neq 0であるようなiについての\mathbf{v}_iによって張られる空間への射影になっていることがわかります。このように空間を制限して考え直すことで、逆行列が存在しない場合においても回帰計算をすることが可能になります。

機械学習を原理から理解する パーセプトロン

内容的には、前回の記事の続き的な感じになります。
pyhaya.hatenablog.com

今回は、二値分類をパーセプトロンを使って実装します。パーセプトロンといえば、ニューラルネットワークディープラーニングの基本という感じの学習器です。前回に引き続き、理論を説明した後にPythonでの実装を示します。

対象とする問題

いくつかの特徴量が与えられていて、それに対して正解ラベルが+1, -1で与えられているような状況を考えます。考える仮説空間はhalf-spacesクラス(日本語訳がわからない)です。

仮説空間という言葉が出てくると難しく感じますが、予測に使うのは下のような関数です。


y = sign(\mathbf{w}\cdot\mathbf{x} + b)\tag{1}

 \mathbf{w}, bは重みとバイアスです。基本的には線形関数で、最終的にsignで符号だけ取り出すことで予測値 yを算出しています。

学習アルゴリズム

これは前回の記事と同じなので結果のみ示します。最終的には、

 t_i(\mathbf{w}\cdot\mathbf{x}_i) > 0 \tag{2}

を満たす重みパラメータ\mathbf{w}を求めることが目標になります。

どのように解くか

前回の記事では、これを線形計画問題だと考えて解きました。今回は、線形計画に比べると直感的な方法でパラメータを求めていきます。

まず、パラメータ\mathbf{w} t_1\mathbf{x}_1で初期化します。なぜこのような値で初期化するかというと、こうすることで

t_1(\mathbf{w}\cdot\mathbf{x}_1)=t_1^2x_1^2 > 0 \tag{3}

となって、ひとまず最初のデータに関して学習させることができるためです。

次にやることは \mathbf{w}はこれで最適か調べることです。学習データを(2)式に代入していって、不等式が成り立っているか確かめます。すべての学習データで不等式が成立していればこのパラメータが最適値とすることができます。しかしほとんどの場合はこううまくはいきません。 i番目のデータで

t_i(\mathbf{w}\cdot\mathbf{x}_i) \le 0 \tag{4}

となってしまった場合を考えましょう。どうすればよいでしょう。要するに、左辺を増やす方向に\mathbf{w}を更新すればよいです。どうやるかというと、

 \mathbf{w}\rightarrow \mathbf{w}+y_i\mathbf{x}_i\tag{5}

このとき、(4)式の左辺は

t_i(\mathbf{w}\cdot\mathbf{x}_i) \rightarrow t_i(\mathbf{w}\cdot\mathbf{x}_i)+\| \mathbf{x}_i\|^2\tag{6}

となります。ここで、正解ラベルt_iは±1しかとらないことを使いました。無事左辺を増やせました。このようにして、すべての学習データについて(2)式が成り立つまで値の更新を続けます。

Pythonでの実装

値の更新は、現実問題としてすべての学習データを間違えることなく予測することは不可能なことが多いので、適当なところで打ち切ります。100回の値更新で打ち切るようにしたのが下のプログラムです。

import numpy as np

from sklearn.model_selection import train_test_split
from sklearn.datasets import make_classification

def Perceptron(X, y):
	X = np.array(X)
	w = np.zeros(len(X[0]))
	w = y[0] * X[0]

	for count in range(100):
		success = True
		for i in range(len(X)):
			if np.sign(w.dot(X[i])) == np.sign(y[i]):
				continue
			else:
				w += y[i]*X[i]
				success = False
				break
		if success:
			break
	
	return w

if __name__ == '__main__':
	#サンプル生成
	X, y = make_classification(
			n_samples=500,
			n_classes=2,
			n_features=2,
			n_redundant=0,
			class_sep=1.5,
			)

	y = list(map(lambda x: -1 if x == 0 else 1, y))
	X = np.array(X)
	y = np.array(y)
	X_train, X_test, y_train, y_test = train_test_split(X, y)

	w = Perceptron(X_train, y_train)
	accuracy = np.sum(y_test*X_test.dot(w) > 0)
	print(accuracy / len(X_test))

これを実行すると、だいたい95%程度の精度が出ていることが確認できます。