気ままに実装する機械学習

機械学習に興味のある大学院生によるブログです.

vimでMarkdown記法をローカルでプレビューしたい

MarkdowngithubREADMEはてなブログの記事をMarkdownで書いているとプレビューしたくなります。

今まではgithubであれば、addしてcommitしてpushしてブラウザで更新して間違っている箇所を修正という非常にめんどくさい作業を繰り返してました。。。

そこで、vimで編集してローカルでプレビューしたくなったので色々調べたら良いプラグインを見つけたので紹介したいと思います。

vimプラグインを導入したいのですが、プラグインを管理するためにNeoBundleを使った方法を紹介します。 もしNeoBundleがインストール済みであれば2節から読んでください。

 

 

1. NeoBundleのインストール方法

NeoBundleプラグインの導入やアップデートが簡単になります。

$ mkdir ~/.vim/bundle
$ git clone https://github.com/Shougo/neobundle.vim ~/.vim/bundle/neobundle.vim

また、vimrcに以下の記述を追加します。

" Note: Skip initialization for vim-tiny or vim-small.
if 0 | endif

if &compatible
set nocompatible " Be iMproved
endif

" Required:
set runtimepath+=~/.vim/bundle/neobundle.vim/

" Required:
call neobundle#begin(expand('~/.vim/bundle/'))

" Let NeoBundle manage NeoBundle
" Required:
NeoBundleFetch 'Shougo/neobundle.vim'

" My Bundles here:
" Refer to |:NeoBundle-examples|.
" Note: You don't set neobundle setting in .gvimrc!

call neobundle#end()

" Required:
filetype plugin indent on

" If there are uninstalled bundles found on startup,
" this will conveniently prompt you to install them.
NeoBundleCheck

2. vimプラグインを導入

必要なプラグインをインストールします。 NeoBundleをインストールしていれば、

NeoBundle 'plasticboy/vim-markdown'
NeoBundle 'kannokanno/previm'
NeoBundle 'tyru/open-browser.vim'

と記述して保存するだけで大丈夫です。 その後

:NeoBundleInstall

プラグインを導入するか、次にvimを開いたときにインストールするか聞かれるのでyesと答えればインストールが始まります。

最後に.md拡張子ファイルもmarkdownとなるように

au BufRead,BufNewFile *.md set filetype=markdown

と記述して終わりです。

3. 実行方法

vimMarkdown記法で編集します。 その後

:PrevimOpen

と打つとローカルで表示されます。

4. 結果

f:id:linearml:20180529160212p:plain
結果

 

matplotlibのインストールにつまずいたお話

新しいmacbookmatplotlibをインストールしようとした時に少しつまずいたので備忘録として記事にします。

1. 吐き出したエラー達


まず普通に

pip install matplotlib

でインストールしようとすると

Command "python setup.py egg_info" failed with error code 1 in /private/tmp/pip-build-oeUHU7/matplotlib

と出ました。

もう少し上の文を見ると

* The following required packages can not be built :
* freetype
* Try installing free type with `brew install
* freetype` and pkg-config with `brew install pkg-
*config

と書いてあったので

brew install freetype
brew install pkg-config

でそれぞれインストールしました。

その後、(念のためpipをupgradeする)

pip install --upgrade pip
pip install marplotlib

とすると、先ほどとは違う感じになってうまく行きそうになるがやっぱり失敗します。

原因を探ると、

Uninstalling six-1.4.1:
()
OSErro: [Errno 1] Operation not permitted (hoge hoge)

つまり、すでにインストールされていたsixをアンインストールしようとしてるけど権限がなくてエラーが起きているそうです。

2. 解決策


なので、sixを無視するために、

pip install matplotlib --ignore-installed six

というオプションをつけました。

その結果、無事インストールに成功しました。

天気を知らせてくれるtwitter botを作ってみた

前回はgoogle calendarに予定を追加したり確認したりできるbotを紹介しました。
linearml.hatenablog.com


今回は天気を知らせてくれるtwitter botの紹介です。

テレビで天気を見てから学校行くなんてことはないですし、(正直)天気アプリを起動してまで確認することではないと思っています笑

しかしtwitterは毎朝開きます。なので、twitter botが教えてくれれば悩み解決です。

1. 天気APIの取得

今回はlivedoor天気情報が提供しているAPIを用いています。
APIから天気の情報を取得するためには地域コードが必要になってきます。
そこで最初に、地域を入力とし、地域コードを返すプログラムを作成しました。
各地域コードはこちらxml形式で提供されていたので、Beautiful Soupを用いて作成します。
Beautiful Soupについての詳細は今回は割愛させていただきます(Qiitaに詳しい記事がたくさんあります)。
今回は都道府県名を入力クエリとして想定しています。
(下記プログラムを動かすにはxmlファイルを"region_id.xml"という名前で保存する必要があります。)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from bs4 import BeautifulSoup
import requests

def check_id(setCity):
	soup = BeautifulSoup(open('region_id.xml'),'html.parser')
	pref = soup.find('pref',title = setCity)
	if pref is not None:
		return True
	return False

def get_id(setPref):
	tmp = setPref[len(setPref) - 1]
	hokkaidou = ['道北','道南','道央']
	if tmp !=  '都' and setPref == '東京':
		setPref += '都'
	elif tmp != '府' and (setPref == '大阪' or setPref == '京都'):
		setPref += '府'
	elif tmp != '県' and setPref not in hokkaidou:
		setPref += '県'
	elif setPref == '北海道':
		setPref == '道央'

	soup = BeautifulSoup(open('region_id.xml'),'html.parser')
	pref = soup.find('pref',title = setPref)
	city = pref.find('city')
	txt = get_weather(city.get('id'))
	return txt


def get_weather(cityId):
	url = 'http://weather.livedoor.com/forecast/webservice/json/v1'
	payload = { 'city' : cityId }
	data = requests.get(url,params = payload).json()
	txt = ''
	txt += data['title'] + '\n'
	
	for weather in data['forecasts']:
		txt += weather['dateLabel'] + ':' + weather['telop'] + '\n'
		if  weather['temperature']['max'] is not None:
			txt += ' 最高気温は' + weather['temperature']['max']['celsius'] + '\n'
		else:
			txt += ' 最高気温情報はありません。\n'
		if weather['temperature']['min'] is not None:
			txt += ' 最低気温は' + weather['temperature']['min']['celsius']  + '\n'
		else :
			txt += ' 最低気温情報はありません。\n'
	return txt

if __name__ == '__main__':
	pref = '東京'.encode('utf-8')
	print get_id(pref)

xmlでは北海道は、「道央、道北、道南」に分かれていたので、北海道と入力した場合は札幌を出力するために「道央」に変換するようにしています。
また、"東京都"と"東京"や"大阪"と"大阪府"のように"都道府県"がついている場合とついていない場合のどちらにも対応できるようにしています。
このプログラムを動かすと以下のような出力結果が得られます。

東京都 東京 の天気
今日:晴れ
 最高気温は14
 最低気温情報はありません。
明日:晴れ
 最高気温は16
 最低気温は3

ツイート部分は前回の記事
linearml.hatenablog.com
で紹介した通りなので、そのツイート部分にこのテキスト情報を渡せば終わりです。

2.ツイート制御部分

今回は、3つの機能を新たに加えました。

  1. botに"w 都道府県名"とリプを送るとその地域の天気情報を返す機能
  2. botに"w s 都道府県名"とリプを送るとその地域をデフォルト地域として設定してくれる機能(存在しない都道府県を指定された場合その旨をリプで返す)
  3. botに"w"とリプを送るとデフォルト地域の天気情報を返す機能

前回の記事ではカレンダーに予定を登録する時のオプションを"r"、確認する時のオプションを"c"としていましたが今回はweatherの"w"をユーザに指定させます。
変更部分は以下の通りです。

~略~
elif reply[1] == 'w':
    if len(reply) == 3:
        txt = str(gr.get_id(reply[2]))
        tweet = '@' + str(status.user.screen_name) + ' ' + txt  + '\n' + str(datetime.datetime.today())
        api.update_status(status=tweet)
    elif len(reply) == 4:
        if reply[3] == '北海道':
                reply[3] = '道央'
        flag = gr.check_id(reply[3])
        if flag:
            file = open('defaultCity.txt','w')
            file.write(reply[3])
            file.close()
            tweet = '@' + str(status.user.screen_name) + ' デフォルト地域を' + reply[3] + 'に設定しました。' 
        else:
            tweet = '@' + str(status.user.screen_name) + ' ' + reply[3] + 'は存在しません。'

        api.update_status(status=tweet)

    elif len(reply) == 2:
        file = open('defaultCity.txt','r')
        defaultCity = file.readline()
        txt = str(gr.get_id(defaultCity))
        file.close()
        tweet = '@' + str(status.user.screen_name) + ' ' + txt  + '\n' + str(datetime.datetime.today())
        api.update_status(status=tweet)

    else:
        tweet = '@' + userName + ' You should reply : w city.'
        api.update_status(status=tweet)
~略~

この部分を新たに追加すれば動きます。
デフォルト地域はデータベースを使ってもいいのですが少し大げさな気もするので今回はテキストファイルで保存しています。
また、herokuやらなんやらでスケジューリングすれば毎朝自動で天気情報を送ってくれたりもします。
githubに全体のソースコードを載せておきます。

3. 動作確認

f:id:linearml:20180302082854p:plain
f:id:linearml:20180302082857p:plain
f:id:linearml:20180302082900p:plain

次はどんな機能を追加しましょうか。
今の所ありきたりのことしかできないので、もっと面白いことをやりたいですね。
とりあえず日々のことをtwitterで済ませてしまおう計画は続けつつ、面白いアイデアが思いつければいいなと思っています。

google calendarに予定を追加するTwitter Botを作ってみた

 

今回は、Twitter上でgoogle calendarに予定を追加し、確認できるbotを作成してみたので記事にしてみました。
(ソースは一応githubにも載せておきます。)

使用言語はpythonです。
予定を追加するときはbot
"r year/month/day/hour of start/minutes of start/hour of end/minutes of end"
とリプライすることで登録できるようになっています。
開始時刻終了時刻は共に省略可能です。
開始時刻が省略されている場合は終日の予定、終了時刻だけが省略されている場合は開始時刻から1時間の予定を追加するようにしています。(オプションのrはregister)

予定を確認するときは
"c"
と送るだけで直近の予定を返してくれます。(オプションのcはcheck)

1. bot用のアカウントをTwitter上で登録

2. Twitter APIで各種keyを取得

Twitter App で新規appを作成 (作成したアカウントでログイン)
以下のページに遷移するので適当に入力 (参考までにそれぞれの項目が何を意味するのか列挙しておきます)
・ Name : アプリ名
・ Website : 自分が持つwebサイトなどのURL
・ Callback URL : OAuthによる認証成功時にリダイレクトされるURL
       (適当なもので良いらしい)
以下のkeyを取得
・ Consumer Key
・ Consumer Secret
Access Token
Access Token Secret

3. Google Calendar APIの登録

Google APIsから登録をします。
(画面通り進むとプロジェクトへの認証情報への追加という画面に遷移するので、一度キャンセルを押します。
認証情報OAuth同意画面タブでユーザに表示するサービス名を適当に設定します。
認証情報 -> 認証情報を作成 -> OAuthクライアントIDを選択します。
クライアントIDの作成その他を選択して名前を適当に入力するとクライアントIDが発行されます。作成されたクライアントIDをクリックするとJSONをダウンロードと出るのでダウンロードします。ダウンロードされたファイルをclient_secret.jsonという名前で保存しておきます。

あとはGoogle APイクライアントをインストールすれば終わりです。

pip install google-api-python-client

4. Pythonソースコードを書く

その前に必要なライブラリをインストール

pip install twitter
  1. 先ほど取得したkeyを入力 (ファイル名 : settings.py)
CONSUMER_KEY = "*********"
CONSUMER_SECRET = "*********"
ACCESS_TOKEN ="*********"
ACCESS_TOKEN_SECRET = "*********"
  1. botが直近の予定をツイートしている部分は以下の通りです。(ファイル名 : tweet.py)
#coding: utf-8

from requests_oauthlib import OAuth1Session
import json
import settings
import random
import datetime
import quickstart
import tweepy


CK = settings.CONSUMER_KEY
CS = settings.CONSUMER_SECRET
AT = settings.ACCESS_TOKEN
AS = settings.ACCESS_TOKEN_SECRET

# create a twitter object
auth = tweepy.OAuthHandler(CK, CS)
auth.set_access_token(AT, AS)
 
api = tweepy.API(auth)

def tweet(userName):
	twitter = OAuth1Session(settings.CONSUMER_KEY,
							settings.CONSUMER_SECRET,
							settings.ACCESS_TOKEN,
							settings.ACCESS_TOKEN_SECRET)

	schedule = quickstart.get_schedule()
	if schedule is not None:
		tweets = [schedule + '!!']
	else:
		tweets = ['No events today!!!']
	randomtweet = tweets[random.randrange(len(tweets))]

	params = {"status": '@' + userName + ' ' + randomtweet + ' ' + str(datetime.datetime.today())} 
	req = twitter.post("https://api.twitter.com/1.1/statuses/update.json", params = params)

リプライを受け取って処理を行っている部分が以下の部分です。 (ファイル名 : auto_reply.py )

#coding:utf-8
import tweepy
import datetime
import settings

import write_schedule as ws
import tweet as tw

CK = settings.CONSUMER_KEY
CS = settings.CONSUMER_SECRET
AT = settings.ACCESS_TOKEN
AS = settings.ACCESS_TOKEN_SECRET

# create a twitter objext
auth = tweepy.OAuthHandler(CK, CS)
auth.set_access_token(AT, AS)
 
api = tweepy.API(auth)
 
class Listener(tweepy.StreamListener):
    def on_status(self, status):
        status.created_at += datetime.timedelta(hours=9)

        userName = 'your user Name'
        botName = 'bot Name'
        gmail = 'gmail address'
        try:
            # if bot recieve reply, it reply or add schedule
            if str(status.in_reply_to_screen_name)== botName and str(status.user.screen_name) == userName:
                reply = str(status.text).split(' ')
                if reply[1] == 'c':
                    tw.tweet(useName)
                elif reply[1] == 'r':
                    day = reply[2]
                    event = reply[3]
                    tweet = '@' + str(status.user.screen_name) + ' I add schedule about ' + event + ' at ' + day + '!\n' + str(datetime.datetime.today())
                    print (tweet)
                    api.update_status(status=tweet)
                    ws.main(day,event,userName,gmail)
                
        except:
            tweet = '@' + userName + ' You should reply : y/m/d/(start h/start m/end h/end m) event'
            api.update_status(status=tweet)

        return True

    def on_error(self, status_code):
        print('Got an error with status code: ' + str(status_code))
        return True
     
    def on_timeout(self):
        print('Timeout...')
        return True
 
listener = Listener()
stream = tweepy.Stream(auth, listener)
stream.userstream()

最後にカレンダーに予定を追加する部分です。(ファイル名 : write_schedule.py)

#coding:utf-8
from __future__ import print_function
import httplib2
import os

from apiclient import discovery
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage

import datetime
import tweepy
import settings

CK = settings.CONSUMER_KEY
CS = settings.CONSUMER_SECRET
AT = settings.ACCESS_TOKEN
AS = settings.ACCESS_TOKEN_SECRET

# create a twitter object
auth = tweepy.OAuthHandler(CK, CS)
auth.set_access_token(AT, AS)
 
api = tweepy.API(auth)

try:
    import argparse
    flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()
except ImportError:
    flags = None

# If modifying these scopes, delete your previously saved credentials
# at ~/.credentials/calendar-python-quickstart.json
SCOPES = 'https://www.googleapis.com/auth/calendar'
CLIENT_SECRET_FILE = 'client_secret.json'
APPLICATION_NAME = 'Google Calendar API Python Quickstart'


def get_credentials():
    '''Gets valid user credentials from storage.

    If nothing has been stored, or if the stored credentials are invalid,
    the OAuth2 flow is completed to obtain the new credentials.

    Returns:
        Credentials, the obtained credential.
    '''
    home_dir = os.path.expanduser('~')
    credential_dir = os.path.join(home_dir, '.credentials')
    if not os.path.exists(credential_dir):
        os.makedirs(credential_dir)
    credential_path = os.path.join(credential_dir,
                                   'calendar-python-quickstart.json')

    store = Storage(credential_path)
    credentials = store.get()
    if not credentials or credentials.invalid:
        flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
        flow.user_agent = APPLICATION_NAME
        if flags:
            credentials = tools.run_flow(flow, store, flags)
        else: # Needed only for compatibility with Python 2.6
            credentials = tools.run(flow, store)
        print('Storing credentials to ' + credential_path)
    return credentials

def main(day,event,userName,gmail):
    '''Shows basic usage of the Google Calendar API.

    Creates a Google Calendar API service object and outputs a list of the next
    10 events on the user's calendar.
    '''

    credentials = get_credentials()
    http = credentials.authorize(httplib2.Http())
    service = discovery.build('calendar', 'v3', http=http)

    #now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
    
    try:
        tmp = day.split('/')
        day = datetime.date(int(tmp[0]),int(tmp[1]),int(tmp[2]))
        length = len(tmp)
        if length <= 3:
            allday = True
        else:
            day2 = day
            start = datetime.time(int(tmp[3]),int(tmp[4]),00)
            allday = False
            if length == 7:
               end = datetime.time(int(tmp[5]),int(tmp[6]),00)
            else:
                end = datetime.time(int(tmp[3])+1,int(tmp[4]),00)
        
        if not allday:
            event = {
                'summary': event,
                'start': {
                'dateTime': str(day) + 'T' + str(start),
                'timeZone': 'Asia/Tokyo',
            },
            'end': {
                'dateTime': str(day2) + 'T' + str(end),
                'timeZone': 'Asia/Tokyo',
            },
            'recurrence': [
                'RRULE:FREQ=DAILY;COUNT=1'
            ],
            }
        else:
            event = {
                'summary': event,
                'start': {
                'date' : str(day),
                'timeZone': 'Asia/Tokyo',
            },
            'end': {
                'date' : str(day),
                'timeZone': 'Asia/Tokyo',
            },
            'recurrence': [
                'RRULE:FREQ=DAILY;COUNT=1'
            ],
            }
        
        print(event)
        calendarId = gmail
        event = service.events().insert(calendarId=calendarId, body=event).execute()
    except:
        import traceback
        traceback.print_exc()
        tweet = '@' + userName + ' You should reply : r y/m/d/(start h/start m/end h/end m) event'
        api.update_status(status=tweet)

if __name__ == '__main__':
    main('2018/02/12','sample')

最後に

python auto_reply.py

botが稼働します。
色んなかたが作成したソースコードを寄せ集めて少し修正した程度ですがこれで一応動きます。

5. 動作確認

f:id:linearml:20180224014425p:plain
f:id:linearml:20180224014016p:plain
f:id:linearml:20180224014226p:plain

今後も何か機能増やしたりして、Twitterで全てのことができるようにしたいなあと思ってたりします笑

線形回帰 線形基底関数


以前の更新からかなりの期間が空いてしまいました。。。 セスペの勉強やらゼミが忙しかったりとしたのでという言い訳をさせていただきます。。。 セスペが受かってたら勉強方法などの記事を書こうと思っていますが午後の手応えが全くないのでまず受かってないと思います笑

まあ余談はそのぐらいにしといて今回はビショップ本の第3章の線形回帰モデルです。その中でも基底関数のお話。 2章までで密度推定やクラスタリングなど教師なし学習について書いてありました。第3章からは教師あり学習の話になります。その中でも回帰の問題。 回帰問題とは、入力をD次元ベクトルxとし、そのベクトルから1つあるいは複数の目標変数tの値を予測することです。 いくつか前に紹介した多項式曲線フィッティングは回帰の一例にあたります。 最も単純な線形モデルは入力変数に関しても線形な関数です。 一般的には、入力変数を基底関数と呼ばれる非線形な関数に与え、入力変数に関しては非線形な形にした方が有用である。 入力ベクトルに関しては非線形でありかつ、パラメータに関しては線形のため、今まで通りの議論が成り立ち解析が容易であるという利点がある。 回帰の最も単純なアプローチは、x \rightarrow tとなる適当な関数y(x)を構成することです。 より一般的な確率論的な見方では条件付き確率を用いてp(t|\boldsymbol{x})をモデル化します。これは予測分布と呼ばれます。 この条件付き分布を使えば、適切に選んだ損失関数の期待値を最小にするように任意のxの新たな値に対するtを予測することができます。

まず、最も単純な線形回帰モデルを紹介します。それは、入力変数の線形結合

\displaystyle y(\boldsymbol{x},\boldsymbol{w}) = w_0 + w_1 x_1 + \cdots + w_D x_D

で表されます。(入力変数に対しても線形であるが)パラメータに関して線形であるという重要な性質があります。入力変数に関して線形であると表現能力に乏しいため、入力変数に関して非線形な関数の線形結合を考えます。それは基底関数\phi_j を用いて

\displaystyle y(\boldsymbol{x},\boldsymbol{w}) = w_0 + w_1 \phi_1 (\boldsymbol{x}) + \cdots + w_D \phi_D (\boldsymbol{x})
\displaystyle y(\boldsymbol{x},\boldsymbol{w}) = \boldsymbol{w}^{T}\boldsymbol{\phi(x)} 

と表すことができます。こうすることで、入力変数に関しては非線形だが、パラメータに関しては線形である形が表現できます。 多項式フィッティングの例では、\phi_j (x) = x^{j}という基底関数を用いていました。 他にも基底関数はたくさんあり、ビショップ本では以下の2つが紹介されています。 まずは、ガウス基底関数です。

\displaystyle \phi_j (x) = exp\{- \frac{(x-\mu_{j})^{2}}{2s^{2}}\} 

\mu_jは入力空間における基底関数の位置を表し、sは空間的な尺度を表します。 もう一つにシグモイド基底関数があります。

\displaystyle \phi_j (x) = \sigma(\frac{x - \mu_j}{s}) 

ただし\sigma(a)はロジスティックシグモイド関数と呼ばれ、

\displaystyle \sigma(a) = \frac{1}{1+exp(-a)} 

で定義されます。 ガウス基底関数とシグモイド関数の図とそのプログラムを以下に載せておきます。

ガウス基底関数

f:id:linearml:20171101041535p:plain

シグモイド基底関数 f:id:linearml:20171101041544p:plain

後は、\displaystyle y(\boldsymbol{x},\boldsymbol{w}) = \boldsymbol{w}^{T}\boldsymbol{\phi(x)} の式に対して最尤推定でパラメータ\boldsymbol{w}を推定すれば良いことになります。 最尤推定についてはいつも通りパラメータで偏微分してイコール0と置き、\boldsymbol{w}について解くだけです。今回も閉じた形で求まりますが結果は後日追加します(多分、、、) 最尤推定による回帰はこのように基底関数をかます事で表現力を高めることができます。またパラメータに関しては線形なので閉じた形で推定値が求まるという利点もあります。

交差検定

今回はモデル選択の時に使われる手法の、交差検定について軽くまとめて実装して見たいと思う.

交差検定

訓練とテストに使えるデータには限りがあり、良いモデルを選択するために得られたデータをできるだけたくさん訓練に使いたい. しかし、確認用に使うテストデータが小さいとたまたま良い精度が出てしまったのかもしれないなど、うまく評価ができない. そこでよく用いられる手法に交差検定 (cross-validation) がある. 得られたデータのうち\frac{S-1}{S}の割合のデータを訓練に使い、残りをテストデータに使う. 例えば全データ数が[N = 100]とし、S = 4とする. この時訓練に使うデータは全体の\frac{4-1}{4}、つまり7.5割(75個)を使い残りの2.5割(25個)をテストデータに使う. この場合全データをS分割したことになる. したがって、テストに使える訓練データはS個あるので、学習と評価をS回繰り返す. S回分の精度の平均を学習器の精度として採用することで、比較を行うことができる. 1回目の学習では最初の25個をテストデータ、残りを訓練データに、2回目は次のブロックのテストデータを使う. これを繰り返す.
f:id:linearml:20170923033620p:plain 欠点としては、分割数と訓練数が比例することである. 一回の訓練に時間のかかる学習器に対して、交差検定をしようとすると、S回訓練を行うことになるので、膨大な時間がかかることは想像つく.
以下に私が書いたソースコード載せる. 分類器はサポートベクタマシン、データセットはirisを使う.

交差検定

16,17,25行目で与えられたデータをシャッフルするためのマスクを定義している. 28,29行目でs回目のテストデータ、30,31行目では学習に使う訓練データを作成している. setdiff1d(a,b)はaとbの差集合を求めることができるので、それを利用して訓練データとテストデータを分割することにした. 36,37,38行目で精度(この指標についてはまた今度)を計算して保存しておく. 最後にその平均を出力することにしている

結果は以下のように得られた.
micro precision : 0.96
micro recall : 0.96
micro F1 : 0.96

多項式曲線フィッティング

ビショップ本では多項式曲線フィッティングから始まっている. この単純な回帰から多くの重要な概念を説明したいらしい. まず、実数値の入力変数xから、実数値の目的変数tを予測することを考える. 今回は人工的に生成したデータを使って例で考え、後で実装を試みる. ここで訓練データとしてN個のxを並べた、\boldsymbol{x} = (x_1,\cdots,x_N)^{T}とそれぞれに対する観測値である\boldsymbol{t} = (t_1,\cdots,t_N)^{T}が与えられたとする. 例えば、N=10個のデータからなる訓練集合をプロットしたものを以下に載せる. f:id:linearml:20170919065043p:plain ただし、ここでは関数sin(2\pi x)にランダムなノイズを加えて生成したものをプロットしている. この訓練データを用いて、新たな入力変数\hat{x}に対する観測値\hat{t}を予測することが目標である. そのためのアプローチの一つに曲線フィッティングがある. ビショップ本ではあまり形式ばらない形で話を進めている. ここでは以下のような多項式を使ってフィッティングすることを考える.

\displaystyle y(x,\boldsymbol{w}) = \sum_{i=0}^{M}w_{i}x^{i} 

ただし、M多項式の次数である. \boldsymbol{w}に関して線形であることから、線形モデルと呼ばれている. (3章、4章で詳しく議論しているのでそこらへんはまたの機会に) フィッティングでは訓練データを多項式に当てはめて係数の値、つまり\boldsymbol{w}を求める. これは、\boldsymbol{w}をランダムに固定したときの多項式の値と、訓練データの目的変数の値との差を最小化することで達成することができる. 全ての訓練データの入力変数と目的変数との差が小さくなるように作られた曲線が求めるべき曲線ということになるので直感的にも理解しやすい. 誤差関数にも色々あるが、今回は単純で広く用いられている、二乗和誤差で考える.

\displaystyle E(\boldsymbol{w}) = \frac{1}{2}\sum_{n=1}^{N}\{y(x_{n},\boldsymbol{w}) - t_{n}\}^{2}

これを最小にする\boldsymbol{w}を求めるのが目標なので、\boldsymbol{w}偏微分して0と置き、方程式を解くことになる. その微分動作の時に降りてくる指数部の2と相殺させるために係数を1/2にしている. 誤差関数を\boldsymbol{w}偏微分すると次のようになる(演習問題となっている).

\displaystyle \sum_{j=0}^{M}A_{ij}w_{j} = T_{i}
\displaystyle A_{ij} = \sum_{n=1}^{N}(x_{n})^{i+j}
\displaystyle T_{i} = \sum_{n=1}^{N}(x_{n})^{i}t_{n}

この式を\boldsymbol{w}に関して解けば良い. 求まった係数を\boldsymbol{w}^{*}とすると、結果としてy(x,\boldsymbol{w}^{*})が求まった多項式として表される. あとは、次数Mを幾つに選択すれば良いかという問題が残っているが、この問題をモデル選択と呼ぶ. 例として、M = 0,1,3,9に対してフィッテイングした結果と実装したソースを以下に示す.

f:id:linearml:20170919063200p:plainf:id:linearml:20170919063204p:plainf:id:linearml:20170919063208p:plainf:id:linearml:20170919063218p:plain
M = 0,1,3,9に対してのフィッテイング結果

多項式曲線フィッティング

M=3のときが最もよく再現できているように見える. 次数をM=9にした場合、全ての点を通っているのでE(\boldsymbol{w}^{*})=0になっているが曲線は元の形からは程遠い. このような振る舞いを過学習と呼ばれている. 訓練データに対してはうまく表現できていても、新しいデータに対しては全く使い物にならない、汎用性がない. さて、この汎化性能とMの関係を定量的に評価したいときの指標として平均二乗平方根誤差(Root Mean Square error; RMS)というものがある.

\displaystyle E_{RMS} = \sqrt{2E(\boldsymbol{w}^{*})/N}

平均をとることで、サイズが異なるデータに対して比較ができる. 訓練データとテストデータのRMSをグラフ化したものを以下に載せる. f:id:linearml:20170919063356p:plain Mが小さい時は、誤差がかなり大きく、対応する多項式は柔軟性に欠け、元の関数をうまく表現できていない. Mが3から7の間では誤差が小さくうまく表現できているように見える. Mが8を超えると訓練データとの誤差は0に限りなく近くなるがテストデータとの誤差は極端に増える. よってMが大きければ良いわけではない. 今回の場合はM=3で十分であると言える. テイラー展開などの級数展開を思い浮かべると次数を増やすほど良い結果が得られると期待してしまうから困ってしまう. また、訓練データ数を増やしたときの結果を以下に示す.

f:id:linearml:20170919063419p:plainf:id:linearml:20170919063426p:plain

訓練データ数を増やしたとき
図の通り、訓練データ数が多いほど過学習が起きにくい. また、過学習を避けるためにベイズ的アプローチを採用すれば良いがこの話は3章の内容なのでまた今度. この章では、過学習を抑制するためのテクニックとして正則化を紹介している. 誤差関数に対し罰金項を付加することで、係数が大きな値になることを防ごうとしている. 罰金項のうち最も単純なものは係数の2乗和である.

\displaystyle \hat{E}(\boldsymbol{w}) = \frac{1}{2} \sum_{n=1}^{N}\{y(x_{n},\boldsymbol{w})-t_{n}\}^{2} + \frac{\lambda}{2} \boldsymbol{w}^{T}\boldsymbol{w}

で表される. 係数\lambdaは罰金項と二乗誤差の和の項との相対的な重要度を調節している. この場合も先ほどと同様に\boldsymbol{w}偏微分して0と置いて方程式を解けば\boldsymbol{w}^{*}が閉じた形で求まる. 統計学の分野では縮小推定、ニューラルネットワークでは荷重減衰と呼ばれている. \lambdaは交差検定などを行なって決定すると良い.