こんにちは。昨日に引き続き @kichikuchi です。

最近はどこもかしこも機械学習の話題で持ちきりですね。

先日行われた AWS re:Invent では機械学習に関連するものとして AWS DeepLens, Amazon Comprehend, Amazon Rekognition Videoなどなど多数のサービスが発表されました。

一方でモバイルでもiOS11からCore MLという機械学習のためのフレームワークが新たに登場しました。

機械学習に関する便利なフレームワークやサービスが多数登場し、ますます熱気が高まっていく中で、いい加減キャッチアップしないわけにも行かないぞ!という気持ちになったので、先日のblack fridayキャンペーン中にUdemyの機械学習の入門講座を購入し受講してみました!

この講座では機械学習の初歩としてpythonを使い単回帰分析を実装するのですが、自分はiosのエンジニアとして日々swiftでお仕事をしているので、単回帰分析をswiftで実装し直してみました!

機械学習初歩

いきなり実装には入らずに、ざっくりとですが機械学習の概要について説明していきたいと思います。
まず初めに機械学習には3大トピックとして

  • 教師あり学習
  • 教師なし学習
  • 強化学習

というものがあります。

さらにこの教師あり学習は大きく

  • 回帰
  • 分類

に分けられます。

今回実装する単回帰分析は名前の通りこの分類の回帰に所属するものです。

単回帰分析とは、一つの説明変数から一つの目的変数を予測するもので、例えば 家賃(目的変数)を部屋の広さ(予想変数)で予測する 場合などに用います。
今回は上記の例をそのまま使い、部屋の広さから家賃を推測する、という問題設定の元以下進めていきたいと思います。

学習と推論

機械学習は、学習と推論という二つのフェーズに分けられます。
学習はサンプル(実際のデータ群)用いて、モデルを生成するフェーズです。
今回の問題では、サンプルは家賃と部屋の広さの組み合わせを指します。

続いて推論は、生成されたモデルを用いて入力xから出力yを推測するフェーズです。
今回のケースでは、部屋の広さ(入力x)をモデルに渡して家賃(出力y)を推測します。
ここで、推論を行う際に気をつけなければいけない概念として内挿と外挿というものがあります。

内挿と外挿

ざっくりというと、与えられたサンプルの範囲内の推論を行うことを内挿、範囲外の推論を行うことを外挿と呼びます。例えば今回の問題では、与えたサンプルの部屋の広さの範囲が25 - 55㎡の場合、37㎡の部屋の家賃を推論することを内挿、15㎡の部屋の家賃を推論することを外挿と呼び、外挿は正しく推論が行えません。

モデルの設定

学習フェーズへ進む前に、モデルを設定する必要があります。
モデルの設定はサンプルの傾向から人間が独自に行います。
今回の問題の場合、家賃は通常広さに正比例して25㎡の時に5万円、30㎡の時に6.5万円、40㎡の時に8.7万円、というように直線的に推移するので

 \hat{y} = ax + b
\\
\hat{y}: 予測値

で表現できそうだと仮定します。
x, yはサンプルとして与えるので、残った変数aとbを学習により決定する必要があります。
ちなみに y = ax + b は直線の方程式でaとbは傾きと切片ですね!

中心化(centering)

学習によりaとb両方を求めることももちろん可能なのですが、求めるパラメータが少ない方が計算が簡単になります。そこで、中心化をという処理を行います。中心化とは、モデルの直線がグラフの真ん中を通る(つまりb = 0)ようにサンプルを加工することです。
処理はとてもシンプルで、全てのサンプルのx, yそれぞれからxの平均、yの平均を引くだけです。

\left\{
\begin{array}{ll}
x_c = x - \bar{x}
\\
y_c = y - \bar{y}
\end{array}
\right.

\\
\bar{x}: xの平均
\\
\bar{y}: yの平均

評価関数

評価関数とは予測値がどれだけ実際の値に近いかを評価するための関数です。
今回は二乗誤差の総和を用います。

L = 
\sum_{n=1}^N
(y_n - \hat{y}_n)^2
\\
L: 評価関数
\\
y: 実際の値
\\
N:サンプル数

評価関数を最小化する

評価関数が小さいということは、予測値と実際の値が近いということになります。つまり、評価関数が最小となるパラメータを求めることにより精度の高いモデルが得られます!
ここで少しおさらいすると、予測値yを求める式は

 \hat{y} = ax + b

で、今回は中心化を行い b = 0 となるため

 \hat{y} = ax

となります。

これを評価関数に当てはめると

\sum_{n=1}^N
(y_n - ax_n)^2

となり、y, xは実際に与えられる値なので、評価関数を最小化する = 評価関数が最小となるaを求める ということになります。
具体的には、評価関数をaで微分した時に0になるようなaが今回求めたい値となります。

なぜ微分した時に0になるaが評価関数を最小とするaなのかという理由はここでは説明しませんが、そこまで難しいものではないので気になった方は調べてみてください!もしくは、僕がこの記事を書くきっかけになった講座では丁寧に説明されているので、気になった方は是非受講してみてください!

評価関数の微分を式で表すと

\frac{d}{da}L = 0

これを式変形すると

a = \frac{\sum_{n=1}^N
(x_n y_n)}{\sum_{n=1}^N
(x_n)^2}

とすることができます。これでaを求める準備ができたので、あとは計算すれば良いだけ!

実装

では、ここから実装に入ります。
上記の式を解くためには、行列演算を行えるようにならなければいけません。
そこで、行列を表すstructを作成し、行列の総和、平均、差、行列同士の積を求める関数実装します。
なお今回のケースでは1次元の行列演算ができれば式を解けるので、そのための最小実装のみをしています。

struct Matrix {

    let element: [Double]

    func sum() -> Double {
        return element.reduce(0.0) { $0 + $1 }
    }

    func mean() -> Double {
        return sum() / Double(element.count)
    }
}

func *(lhs: Matrix, rhs: Matrix) -> Matrix {
    if lhs.element.count != rhs.element.count {
        // 今回は要素数が違う場合を考慮しません
        assertionFailure()
    }

    var array: [Double] = []
    for i in 0..<lhs.element.count {
        let prod = lhs.element[i] * rhs.element[i]
        array.append(prod)
    }

    return Matrix(element: array)
}

func -(lhs: Matrix, rhs: Double) -> Matrix {
    let array = lhs.element.map { $0 - rhs }
    return Matrix(element: array)
}

これで必要な道具がそろったので、順に処理を行なっていきます。

// 学習に用いるサンプルデータ
// 部屋の広さ
let sample_x: [Double] = [40, 38, 36, 35, 32, 45, 55, 32, 28, 41, 44, 50, 43, 44, 31]
// 家賃
let sample_y: [Double] = [137000, 93000, 95000, 97000, 85000, 140000, 213000, 82000, 75000, 103000, 110000, 195000, 124000, 99000, 72000]

var x = Matrix(element: sample_x)
var y = Matrix(element: sample_y)

// 中心化
let bar_x = x.mean()
let c_x = x - bar_x

let bar_y = y.mean()
let c_y = y - bar_y

// パラメータを求める
let xx = c_x * c_x
let xy = c_x * c_y
let a = xy.sum() / xx.sum()

これでaが求まりました!

続いて、このaで本当に適切な家賃が求められるか試してみましょう。
aを求めたサンプルはセンタリング済みなのを考慮すると、予測値を求める式は以下の通りに変形できます。

y = a(x - \bar{x}) + \bar{y}

これを計算すると

let predict = a * (40 - bar_x) + bar_y
print(predict) // 116651.938378195

なんとなく良さそうな値が出ていますね!
ちなみにですが、x = 15を入れてみると

let predict = a * (15 - bar_x) + bar_y
print(predict) // -7427.54359234803

この部屋に住むと毎月お金がもらえてしまいますね...
なぜこのような結果になってしまうかというと、内挿と外挿の話が関わってきます!
今回のサンプルのxは最小値が28, 最大値が55でした。
なので15は外挿にあたり、正確に予測ができなかったというわけです。

最後に

今回の記事を書くにあたり参考にさせていただいた機械学習の入門講座では、全体を通してより詳細でわかりやすい説明がされており、特に評価関数を微分する箇所などは式変形の過程まで細かく解説されています。
非常にわかりやすいので、もし興味が湧いた方がいましたら、ぜひ受講してみてください!

今回のサンプルコード

明日の担当は @fortkle さんです!!

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.