C# JSON文字列から不要な要素を削除する

JSONの扱いでちょっとした前処理が必要となりましたので、メモしておきます。

以下のようなフラットなJSON文字列を扱うケースがありました。

{
  "key1": "value1",
  "key2": "value2"
}

任意のキーと値(文字列型)が追加されるので、Dictionary型とした方が都合が良く、以下のようにJsonConvert.DeserializeObject<Dictionary<string, string>>(json)でデシリアライズさせていました。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace ConsoleApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var json = "{\"key1\":\"value1\",\"key2\":\"value2\"}";

            var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);

            foreach (var t in dictionary.Keys)
            {
                Console.WriteLine($"{t}: {dictionary[t]}");
            }
        }
    }
}

しかし業務要件の変更で、1つのキーのみ配列が渡されるようになりました。 (JSONでやり取りしている以上、当然そういうケースもあります。)

{
  "key1": "value1",
  "key2": "value2",
  "array1": [
    { "childkey1": "childvalue1" },
    { "childkey2": "childvalue2" }
  ]
}

フラットなkey-value構造が崩れるので、このままではパースできずにエラーとなってしまいます。

Unhandled Exception: Newtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing value: [. Path 'array1', line 1, position 43.

これを回避するためには、パースできるクラスを自製してそのオブジェクトにデシリアライズする方法もありますが、もしarray1が必要無いのであれば、JSON文字列からarray1のキーと値を削除する方が楽です。

こうしたケースに便利なのがJObject(Newtonsoft.Json.Linq名前空間)で、JSONに対してLINQで処理できるようになります。

くだんの問題は、一旦JObjectとしてJSON文字列を前処理させることで解決できます。

  1. JSON文字列をJObjectへデシリアライズさせて、Remove(LINQ)でarray1を削除する
  2. 再度文字列にシリアライズしてから、Dictionary<string, string>へデシリアライズする
using Newtonsoft.Json.Linq;

var json = "{\"key1\":\"value1\",\"key2\":\"value2\",\"array1\":[{\"childkey1\":\"childvalue1\"},{\"childkey2\":\"childvalue2\"}]}";

var jobj = JObject.Parse(json);
jobj.Remove("array1");

var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(jobj.ToString());

キー名がわからずとにかく第1階層にある文字列型だけを取り出したいという場合も、JObjectはICollection<KeyValuePair<string, JToken>>を実装しているので、以下のようにLINQで処理できます。

jobj = JObject.Parse(json);

var dictionary = new Dictionary<string, string>();

foreach (var j in jobj)
{
    if (j.Value.Type == JTokenType.String)
    {
        dictionary.Add(j.Key, j.Value.ToString());
    }
}

// ToDictionary()を使えば1行で書けます
dictionary = jobj.Properties()
                .Where(jp => jp.Value.Type == JTokenType.String)
                .ToDictionary(jp => jp.Name, jp => jp.Value.ToString());

JSONで何らか前処理が必要になったときのツールとして持っておくと便利ですね。

「Pythonによる機械学習入門」 第2部 基礎編のまとめ

「Pythonによる機械学習入門」を読みましたので、第2部で得たことをまとめます。

総評すると「scikit-learnを使えば機械学習で有名な各種手法がお手軽に試せるぜ」といった感じです。

あくまで入門なので「どう使えば良いのか?」が主たる関心事で、「その方法でなぜ答えに近づけるのか?答えから遠のいてしまうのか?」「AとBの使い分けをどうすれば良いのか?」といった手法に対する理論的な説明は、別で学ぶ必要があります。

とはいえ、scikit-learnに加えてpandasやpyplotなども紹介されており、第3部ではデータのクレンジングやグリッドサーチなどについても触れられていますので、今あるデータをこね回してみるのには十分かと思います。

全体は4部構成で、第1部が導入、第2部が分類・回帰・クラスタリングに関する基礎編、第3部が手の写真画像を使った分類とセンサデータの回帰を行う実践編、第4部が付録となっており、今回は第2部についてまとめます。

Pythonによる機械学習入門

Pythonによる機械学習入門

第3章 分類問題

scikit-learn付属のdigitsデータセット(8×8ピクセルの手書き数字画像)を使って、0〜9のラベルで分類します。

scikit-learnを使えば、classifier = tree.DecisionTreeClassifier()で呼び出すメソッドを変えることで簡単に分類器の種類へ変更できます。

from sklearn import tree
from sklearn import metrics

# 学習
classifier = tree.DecisionTreeClassifier()
classifier.fit(images[:train_size], labels[:train_size])

# 検証
predicted = classifier.predict(images[train_size:])

# 比較
print('Accuracy:', metrics.accuracy_score(expected, predicted))
  • 学習データと訓練データの分離方法
    • ホールドアウト検証では、対象データの一部をテストデータとして取り出してそれ以外全てを学習データとする
    • k-分割交差検証では、対象データをk個に分割して、内1個をテストデータ・それ以外を学習データとして、k回の学習・検証(推論)を繰り返し、平均値を認識率とする
      • 少数のデータの中で学習データとテストデータをやりくりする方法で、kが小さいほど学習データが増えるので性能は向上させやすい(学習・検証の回数が増えるので、時間はかかる)
  • 分類器の性能指標
    • 正答率(Accuracy):全検証データの内、正しく分類された検証データの割合
    • 適合率(Precision):あるラベルに分類されたデータの内、正しく分類された検証データの割合
    • 再現率(Recall):あるラベルに分類されるべき検証データの内、そのラベルに分類された割合
    • F値(F-measure):適合率と再現率の調和平均
    • 検証データに偏りがある場合、ラベルごとに算出される適合率や再現率の方が実体を正しく捉えられるケースも有る
  • アンサンブル学習では、性能の低い分類器(弱仮説器)を組み合わせて、それぞれの分類結果を集約する
    • Random Forestでは、学習データセットから重複・欠落を許したサブセットを作って、サブセット数だけ弱仮設器を学習させて、多数決で検証する(バギング)
    • AdaBoostでは、難度の高い学習データを分類できる弱仮設器を重視するように、重み付けする(ブースティング)
  • SVMでは、サンプルとの距離の二乗和が最大となるように分割する、2クラス分類器
    • 3クラス以上の場合は複数の分類器を組み合わせる

第4章 回帰問題

線形関数・非線形関数の値に乱数値を加えて擬似的に生成した波形のサンプルに対して、scikit-learnで線形回帰・非線形回帰を行います。

  • 線形回帰と非線形回帰
    • y=x2の場合でも、X=x2と変数に置き換えられるなら、それは線形回帰できる
  • model = linear_model.LinearRegression()を置き換えることで、使うモデルを変更できる
    • 最小二乗法、SVM、Random Forest、k-近傍法
from sklearn import linear_model

# 学習
model = linear_model.LinearRegression()
model.fit(x, y)

# 結果(傾きと切片)
print(model.coef_)
print(model.intercept_)
  • 重回帰(2変数以上)の場合は、入力を[[x1_1, x2_1], [x1_2, x2_2], ...]のような多次元配列にする
    • y = 3 * x**2 + 2 * x + 4の場合も、[[x1**2, x1], [x2**2, x2], ...]とすることで重回帰分析(線形)できる
x1_x2 = np.c_[x1, x2]
model = linear_model.LinearRegression()
model.fit(x1_x2, y)
  • 変数が多すぎる(例えば、4変数で表せる関数を9変数で回帰させる、など)と、表現力が高すぎて過学習の原因となる
    • モデルの複雑度を加味するRidge回帰やLasso回帰(罰則付き回帰)
    • 変数の数(ハイパーパラメータ)をどうやって推定するのか

第5章 クラスタリング

scikit-learn付属のirisデータセットを使って、花弁の長さと幅の値からあやめの品種(3種類)に分類します。

from sklearn import datasets

iris = datasets.load_iris()

iris['data'][i][j] # i番目のあやめのj番目のデータ
iris['target'][i] # i番目のあやめの品種
  • k-meansではクラスタ数を予め決めて、クラスタに属するデータから中心への距離が最小となるように、所属や中心を変更する
from sklearn import cluster

# k-means法で3クラスタに分類
model = cluster.KMeans(n_clusters=3)
# 学習
model.fit(data)
  • 階層的凝集型クラスタリングでは、1データ1クラスタからスタートして、近くのデータを凝集させながら、指定のクラスタ数まで絞る
    • 最短距離法、最長距離法、群平均法、ウォード法
  • 非階層的クラスタリング(k-meansもこの1つ)では、評価関数を定義して、その評価関数が最適になるように分割する方法
    • Affinity Propagation

Kinesis FirehoseでS3にアップロードしたファイルをAthenaで検索する

前回の投稿ではLambdaからエンキューされたメッセージをKinesis FirehoseでS3までアップロードしました。
今回はAthenaを使ってこのアップロードしたファイルをSQLで検索できるようにします。

Athena

S3バケットのファイルからSQLライクな構文で検索できるサーバレスなサービスで、スキャンされたデータ量に対してのみ課金されます。 現時点(2017/4/7)では米国リージョンのみの提供となっています。

Amazon Athena (サーバーレスのインタラクティブなクエリサービス) | AWS

今回は前回作成したバケットに対してAthenaで検索できるようにします。
また、あわせてパーティションも設定・作成します。 デフォルトだとバケット内の全ファイルをスキャンしますが、パーティションを使うことでスキャンするディレクトリを限定することができます。

DBとテーブルの作成

まずはAthenaでDBとテーブルを作成します。

Category Manager→Add tableでtestdbとtest_tableを作成します。

  • 前回作成したs3://delivery-s3/を選択します

f:id:ohke:20170406213621p:plain

データ形式JSONを指定します。

f:id:ohke:20170406213804p:plain

名前と型を指定して、3つのカラムを作成します。この時、JSONのキー名と同じカラム名にすることでマッピングします。

f:id:ohke:20170406213850p:plain

最後にパーティションの設定をして、完了です。
Kinesis Firehoseではバケット以下にyyyy/mm/dd/hhのフォルダを作成しますが、今回は日単位でパーティションを作成するため、year、month、dayをパーティションにします。

f:id:ohke:20170406214037p:plain

すると、SQLが実行され、テーブルが作成されます。

CREATE EXTERNAL TABLE IF NOT EXISTS testdb.test_table (
  `StringValue` string,
  `IntValue` int,
  `DoubleValue` double 
) PARTITIONED BY (
  year string,
  month string,
  day string 
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
WITH SERDEPROPERTIES (
  'serialization.format' = '1'
) LOCATION 's3://delivery-s3/'
TBLPROPERTIES ('has_encrypted_data'='false')

パーティションの作成

これでもうSQLを実行できるようになりますが、パーティションを作成しておきます。

2017/03/30でパーティションを作る場合は、以下のようなSQLになります。 同様に2017/04/06も作成しておきます。

ALTER TABLE testdb.test_table ADD PARTITION (year='2017',month='03',day='30') location 's3://delivery-s3/2017/03/30/'

作成したパーティションは、show partitionsで一覧を見れます。

show partitions testdb.test_table;
year=2017/month=03/day=30
year=2017/month=04/day=06

SQLの実行

試しに全件を取得してみます。

  • パーティションで設定したカラム(year、month、day)も表示されていることを確認してください
select * from testdb.test_table;

f:id:ohke:20170406220547p:plain

パーティションを指定して検索する場合、以下のようにwhereでyear、month、dayを指定します。

select * from testdb.test_table where year='2017' and month = '03' and day = '30' and stringvalue = 'text2';

スキャンされたデータ量を見ると、0.19KBから0.13KBへ減っています。 パーティションによってスキャンするファイルが2017/03/30以下のみになったためです。

f:id:ohke:20170406220440p:plain