コード規約
コードを書くときに,今まで「動けばいい」と思っていた.個人の研究でコーディングするぶんには問題ない(そのコードの中身を理解できるなら)が,他の人と仕事をする上では,わかりやすい・メンテンナンスしやすいコードを書くことが重要になってくる.ということで,最も手近なRのコード規約を調べて勉強している.
一番わかりやすい.変数名とか関数名とかなるほどねと思う.
上とほぼ同じようなことが書いてある.
rstudio-pubs-static.s3.amazonaws.com
コーディングだけじゃなくて,ディレクトリ構造とかについても言及している.
「誰がそう決めたんだろう」と思うが,勝手気ままに自分のルールで書いていくよりはこういったものを参考にしたほうが良いかなと思った.
Boostnoteがいい
普段研究とか生活のメモとかは,全部Evernoteに記録していた.簡単に使えるし,見た目も良いし,Safariや他のアプリとの連携もよくできるので,スクラップブック的な用途でも重宝してきた.しかし,プログラムを書く頻度が増えてくると,いろいろと不満が出てきた.
- コードブロックが貧弱
- 3デバイス以上で使おうと思うとPremiumにしないといけない
- しかしPremiumにしたところで不要な機能がいっぱいある.プレゼンモードとか
- Markdownで書く&プレビュー -> 会社のconfluenceやブログにポストというワークフローが実現できない
とくに1つ目のコードブロックは結構困りものだった.
そこで見つけたのがBoostnote.割と前からあるが知らなかった.同僚とビデオ通話したときに,同僚が使っているのがちらっと見えて,そのきれいなコードブロックにピンときて使い始めた. 使ってみていいなと思った点を列挙すると
- Markdownでかける.リアルタイムにプレビューできる
- コードブロックがすごく良い
- Evernote的な検索がちゃんとできる.タグとかフォルダも作れるから整理もできる.
- 画像の挿入もドラッグ&ドロップで簡単
- ストレージをDropboxとかにおけば,デバイス間でノートをsyncできる (Boostnote内のデータを、クラウド上に同期する方法 - Boostnote).
- チェックボックスを作るとノート上部に進捗を%表示してくれる
要はプログラマに特化したEvernoteという感じ.一方イマイチなところもまだまだある
- プログラムを書かずに日記とかメモ的な運用をするならMarkdownは面倒(これは使い方の問題)
- 検索に日本語が入力できない.ペーストはできるが...
- 最新のリリース(GitHub - BoostIO/boost-releases: 🚀 Boostnote app releases & changelog)ではMarkdownエディタの横に警告マークが出てきて邪魔.
- Evernoteのインポートツールがあることはある(GitHub - BoostIO/ever2boost)が全く動かない.
仕事ではプログラムをよく書くので,完全にBoostnoteに移行した.一方家でやる研究やメモはあえてBoostnoteでかく必要はないから,Evernoteのほうがいいかなーと思っている. また,Evernoteのインポートツールは,いろいろ試行錯誤した結果
- Issueで議論されている通りのエラーが出るので,enexファイルをエディタか何かで編集し,不要な箇所を削除.
- sudo権限で
convert
を実行
すればできた.
Boostnoteがいい
普段研究とか生活のメモとかは,全部Evernoteに記録していた.簡単に使えるし,見た目も良いし,Safariや他のアプリとの連携もよくできるので,スクラップブック的な用途でも重宝してきた.しかし,プログラムを書く頻度が増えてくると,いろいろと不満が出てきた.
- コードブロックが貧弱
- 3デバイス以上で使おうと思うとPremiumにしないといけない
- しかしPremiumにしたところで不要な機能がいっぱいある.プレゼンモードとか
- Markdownで書く&プレビュー -> 会社のconfluenceやブログにポストというワークフローが実現できない
とくに1つ目のコードブロックは結構困りものだった.
そこで見つけたのがBoostnote.割と前からあるが知らなかった.同僚とビデオ通話したときに,同僚が使っているのがちらっと見えて,そのきれいなコードブロックにピンときて使い始めた. 使ってみていいなと思った点を列挙すると
- Markdownでかける.リアルタイムにプレビューできる
- コードブロックがすごく良い
- Evernote的な検索がちゃんとできる.タグとかフォルダも作れるから整理もできる.
- 画像の挿入もドラッグ&ドロップで簡単
- ストレージをDropboxとかにおけば,デバイス間でノートをsyncできる (Boostnote内のデータを、クラウド上に同期する方法 - Boostnote).
- チェックボックスを作るとノート上部に進捗を%表示してくれる
要はプログラマに特化したEvernoteという感じ.一方イマイチなところもまだまだある
- プログラムを書かずに日記とかメモ的な運用をするならMarkdownは面倒(これは使い方の問題)
- 検索に日本語が入力できない.ペーストはできるが...
- 最新のリリース(GitHub - BoostIO/boost-releases: 🚀 Boostnote app releases & changelog)ではMarkdownエディタの横に警告マークが出てきて邪魔.
- Evernoteのインポートツールがあることはある(GitHub - BoostIO/ever2boost)が全く動かない.
仕事ではプログラムをよく書くので,完全にBoostnoteに移行した.一方家でやる研究やメモはあえてBoostnoteでかく必要はないから,Evernoteのほうがいいかなーと思っている. また,Evernoteのインポートツールは,いろいろ試行錯誤した結果
- Issueで議論されている通りのエラーが出るので,enexファイルをエディタか何かで編集し,不要な箇所を削除.
- sudo権限で
convert
を実行
すればできた.
大阪に引っ越しました
来年から別の会社で働くために,大阪に引っ越してきた.有給消化で12月の第1週から,20足ぐらい早く正月休みに入ったが,そのほとんどが引っ越しとか引越し後の諸々の手続きとかで消滅して,現在に至る.特にトラブルもなく,体調を崩すこともなく,無事に引っ越しを終えることができてよかったと思う.家族もみんな元気です.
大阪は学生時代に住んでいたが,その時と比べて街のつくりとか建物や地理がだいぶ変わっている.写真は,昨日家族で行ったてんしば(天王寺の芝生公園)からの一枚.遠くに通天閣が見える.昔はここはホームレスがたくさんいて,カラオケ大会なんかをやっていたらしいけど,再開発でだいぶ様変わりした,と妻が話していた.
また,学生時代には気づかなかったが,子供に話しかける人がすごい多かったり,自転車乗りがすごい多かったり,東京とは人の性格もだいぶ違うなぁと思った.
冒頭に書いたが,退職するので,そのうち流行っている退職エントリーも書いてみようと思う.
Geron(2018) Scikit-learnとTensorFlowによる実践機械学習 第3章
標記の本を読んでいますが,身に付けるには読むだけでなくコードを書きながら読むことが一番ということで,演習問題を自分でコード書きながらやってみました.備忘録的にその結果を残します.
scikit-learnとTensorFlowによる実践機械学習
- 作者: Aurélien Géron,下田倫大,長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/04/26
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
なお,著者のGeronさんがgithubに文中のコードや演習問題のコードをアップしています.そこを参考にしました(というか,後半のスパム分類はほぼそこのコピペです).
計算は以前構築したGCP環境でjupyter notebookで実施してます.
Q1:MNISTにKNNを試そう
まず必要なモジュールをインポートします.
from sklearn.datasets import fetch_mldata import numpy as np from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import GridSearchCV, cross_val_score from sklearn.ensemble import RandomForestClassifier from sklearn.multiclass import OneVsOneClassifier from sklearn.linear_model import SGDClassifier from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.base import BaseEstimator, TransformerMixin from sklearn.decomposition import PCA from scipy.ndimage.interpolation import shift import matplotlib.pyplot as plt
不足してるものもあるかもしれないです.次にMNISTデータをフェッチして,訓練・検証データに分割します.フェッチは,上でロードしたfetch_mldataでいけます.今回は著者に習って,あえてtrain_test_splitを使わずやってみました.
#MNISTデータをフェッチする mnist = fetch_mldata("MNIST original") #訓練セットとテストセットに分ける X, y = mnist["data"],mnist["target"] X_train, X_test, Y_train, Y_test = X[:60000], X[60000:], y[:60000], y[60000:] shuffle_index = np.random.permutation(60000) X_train, Y_train = X_train[shuffle_index], Y_train[shuffle_index]
ここでKNNを試そうと思いましたが,なぜだかすごい時間がかかったので諦め,RandomForestを使いました.適当にチューニングしてます.
param_grid = { "max_depth":[5,10,100], "random_state":[9999] } rf_cv = GridSearchCV(RandomForestClassifier(),param_grid=param_grid,n_jobs=-1,verbose=2) rf_cv.fit(X_train,Y_train)
cross validationでaccuracyの平均とSDを求めます.
rf_cv_accuracy = cross_val_score(rf_cv.best_estimator_,X_train,Y_train,scoring="accuracy",cv=10) #かなりよい print("CV accuracy (mean): {}".format(np.mean(rf_cv_accuracy))) print("CV accuracy (sd): {}".format(np.sqrt(np.var(rf_cv_accuracy)))) >CV accuracy (mean): 0.9465169612074671 >CV accuracy (sd): 0.002701355399227736
本で言及されていたのですが,データをスケーリングするとうまく行きやすいらしいので,スケーリングを含むパイプラインを作ってみました.初めて作りましたが,簡単です.
std_rf_pipeline = Pipeline([ ("scaler", StandardScaler()), ("clf", GridSearchCV(RandomForestClassifier(),param_grid=param_grid,n_jobs=-1,verbose=2)) ]) std_rf_pipeline.fit(X_train,Y_train)
先ほどと同様にCVでaccuracyの平均とSDを求めます.
rf_cv_accuracy = cross_val_score(std_rf_pipeline,X_train,Y_train,scoring="accuracy",cv=10) #少し向上した print("CV accuracy (mean): {}".format(np.mean(rf_cv_accuracy))) print("CV accuracy (sd): {}".format(np.sqrt(np.var(rf_cv_accuracy)))) >CV accuracy (mean): 0.9464666780536921 >CV accuracy (sd): 0.00265692646724053
本当にちょびっとだけ向上しました.
Q2: データの水増し(data augmentation)
訓練データを上下左右4方向に1ピクセルずつずらし,データを水増しする関数を作り,水増しする前の結果と比べてみます.
def shift_image(image, dx, dy): image = image.reshape((28, 28))#MNISTの配列を28*28の行列の形に戻す shifted_image = shift(image, [dy, dx], cval=0, mode="constant") #ずらす値dx, dyを与えてshift return shifted_image.reshape([-1]) #入力とおなじ形に直して返す
こんな感じでシフトできました.
次にこの関数を使って訓練データを水増しします.
X_train_aug_left = [shift_image(image,-1,0) for image in X_train] X_train_aug_right = [shift_image(image,1,0) for image in X_train] X_train_aug_up = [shift_image(image,0,1) for image in X_train] X_train_aug_down = [shift_image(image,0,-1) for image in X_train] X_train_aug = np.r_[X_train,X_train_aug_left,X_train_aug_right,X_train_aug_down,X_train_aug_up] Y_train_aug = np.r_[Y_train,Y_train,Y_train,Y_train,Y_train]
先ほど作ったパイプラインを水増しした訓練データで訓練し,結果を比較してみます.
rf_clf_aug = std_rf_pipeline.fit(X_train_aug,Y_train_aug) aug_rf_cv_accuracy = cross_val_score(rf_clf_aug,X_train_aug,Y_train_aug,scoring="accuracy",cv=10) #結構向上した print("CV accuracy (mean): {}".format(np.mean(aug_rf_cv_accuracy))) print("CV accuracy (sd): {}".format(np.sqrt(np.var(aug_rf_cv_accuracy)))) >CV accuracy (mean): 0.9560493405604934 >CV accuracy (sd): 0.00910255937880674
少しだけ向上しました.モデルを色々いじったりするよりは,データを増やしたり,根本的な部分を調整するほうが結果の向上幅は大きいようです.
ついでに,パイプラインにPCAも加えてみます.成分数は適当に100としました.
pca_aug_std_rf_pipeline = Pipeline([ ("scaler", StandardScaler()), ("pca",PCA(n_components=100)), ("clf", GridSearchCV(RandomForestClassifier(),param_grid=param_grid,n_jobs=-1,verbose=2)) ]) rf_clf_aug_pca = pca_aug_std_rf_pipeline.fit(X_train_aug,Y_train_aug) aug_pca_rf_cv_accuracy = cross_val_score(rf_clf_aug_pca,X_train_aug,Y_train_aug,scoring="accuracy",cv=10) #結構向上した print("CV accuracy (mean): {}".format(np.mean(aug_pca_rf_cv_accuracy))) print("CV accuracy (sd): {}".format(np.sqrt(np.var(aug_pca_rf_cv_accuracy)))) >CV accuracy (mean): 0.9101188311011883 >CV accuracy (sd): 0.01848845832144993
かえって大幅に低下しましたね・・・適当に主成分を選んだのがまずかったと思います.
Q3: Titanicデータセットに挑戦
これはすでにやったことがあるので飛ばしました.
Q4: スパム分類器を作りなさい(難易度高)
今まで扱ったことのないデータなので,Githubのコードを解読しながら進めました.
まずデータを準備します.urllibで取得し,SPAMとHAM(迷惑メールでない)に分けてディレクトリに保存します.
import os import tarfile from six.moves import urllib DOWNLOAD_ROOT = "http://spamassassin.apache.org/old/publiccorpus/" #Apache SpamAssassinのURL HAM_URL = DOWNLOAD_ROOT + "20030228_easy_ham.tar.bz2"#HAMのファイルの場所 SPAM_URL = DOWNLOAD_ROOT + "20030228_spam.tar.bz2" #SPAMのファイルの場所 SPAM_PATH = os.path.join("datasets", "spam") #データを保存するディレクトリ def fetch_spam_data(spam_url=SPAM_URL, spam_path=SPAM_PATH): #フェッチする関数 このあたりは決まり手なんだろうな パターン if not os.path.isdir(spam_path): #ディレクトリがなければ作る os.makedirs(spam_path) for filename, url in (("ham.tar.bz2", HAM_URL), ("spam.tar.bz2", SPAM_URL)): #HAMとSPAMのメールについて繰り返し path = os.path.join(spam_path, filename) if not os.path.isfile(path): urllib.request.urlretrieve(url, path) #データを取得 tar_bz2_file = tarfile.open(path) #書き込み tar_bz2_file.extractall(path=SPAM_PATH) tar_bz2_file.close() fetch_spam_data()
次に,具体的なファイル名を取得していきます.なぜか20文字以上のファイル名を抽出していますが,なぜかはわかりませんでした.
HAM_DIR = os.path.join(SPAM_PATH, "easy_ham")#このeasy_hamというディレクトリは展開すると勝手にあるものみたい SPAM_DIR = os.path.join(SPAM_PATH, "spam") ham_filenames = [name for name in sorted(os.listdir(HAM_DIR)) if len(name) > 20] #なぜ20以上にするのか? spam_filenames = [name for name in sorted(os.listdir(SPAM_DIR)) if len(name) > 20] print("number of hum mails: {}".format(len(ham_filenames))) print("number of spam mails: {}".format(len(spam_filenames))) >number of hum mails: 2500 >number of spam mails: 500
ファイル名はこんなかんじです.
ham_filenames[1:10]#こんな感じのファイル名 >['00002.9c4069e25e1ef370c078db7ee85ff9ac', > '00003.860e3c3cee1b42ead714c5c874fe25f7', > '00004.864220c5b6930b209cc287c361c99af1', > '00005.bf27cdeaf0b8c4647ecd61b1d09da613', > '00006.253ea2f9a9cc36fa0b1129b04b806608', > '00007.37a8af848caae585af4fe35779656d55', > '00008.5891548d921601906337dcf1ed8543cb', > '00009.371eca25b0169ce5cb4f71d3e07b9e2d', > '00010.145d22c053c1a0c410242e46c01635b3']
次に,保存したemailを読み込んでいきます.
import email #emailを送ったりできるパッケージだが,ここではemailをパースするために使う import email.policy def load_email(is_spam, filename, spam_path=SPAM_PATH): directory = "spam" if is_spam else "easy_ham" #SPAMかどうかでみるディレクトリを変える with open(os.path.join(spam_path, directory, filename), "rb") as f: #ファイルを開いて return email.parser.BytesParser(policy=email.policy.default).parse(f) #パースして返す ham_emails = [load_email(is_spam=False, filename=name) for name in ham_filenames] spam_emails = [load_email(is_spam=True, filename=name) for name in spam_filenames] print(ham_emails[200]) >Return-Path: <ilug-admin@linux.ie> >Delivered-To: zzzz@localhost.netnoteinc.com >Received: from localhost (localhost [127.0.0.1]) > by phobos.labs.netnoteinc.com (Postfix) with ESMTP id BD9B044156 > for <zzzz@localhost>; Wed, 28 Aug 2002 05:47:26 -0400 (EDT) >Received: from phobos [127.0.0.1] > by localhost with IMAP (fetchmail-5.9.0) > for zzzz@localhost (single-drop); Wed, 28 Aug 2002 10:47:26 +0100 (IST) >Received: from lugh.tuatha.org (root@lugh.tuatha.org [194.125.145.45]) by ...
メール本文だけではなく,メールアドレス受信時刻なんかのデータも入っています.
メール本文には,plain textもあれば,htmlで書かれたメールもあります.これをコンテンツタイプと呼びましょう.また,メールの中にメールが入れ子になっていて(例えば返信した時とか)一つのメールが複数のコンテンツタイプを有している場合もあります.それを解析する関数を定義します.
#メールの構造を解析する関数 関数の中でこの関数を再帰的に呼び出す def get_email_structure(email): if isinstance(email, str):#型をチェックする return email payload = email.get_payload()#付加的情報を除いたもの if isinstance(payload, list):#リストだったら(つまりmultipartからなるメール),その要素一つ一つにこの関数をapply. 最終的に文字列になるまでやる return "multipart({})".format(", ".join([ get_email_structure(sub_email) for sub_email in payload ])) else: return email.get_content_type() get_email_structure(ham_emails[1]) #singlepartのメール > 'text/plain' get_email_structure(spam_emails[23])#これはmultipart >'multipart(text/plain, text/html)'
この関数を使ってコンテンツタイプを数え上げ,特徴量とすることを考えます.
from collections import Counter #コンテントタイプを集計する関数 def structures_counter(emails): structures = Counter() for email in emails: structure = get_email_structure(email) structures[structure] += 1 return structures ham_emails_types = structures_counter(ham_emails).most_common() ham_types_val = [obj[1] for obj in ham_emails_types] ham_types_type = [obj[0] for obj in ham_emails_types] spam_emails_types = structures_counter(spam_emails).most_common() spam_types_val = [obj[1] for obj in spam_emails_types] spam_types_type = [obj[0] for obj in spam_emails_types]
集計結果を描画してみましょう.スパムはマルチコンテンツで,かつHTMLで書かれたメールが多いようです.
#HAM: ほとんどテキスト,署名がある #SPAM: HTMLがおおい書名があるものはない 画像とかを含んでる fig, (ax1, ax2) = plt.subplots(ncols=2,figsize=(15,8)) ax1.bar(ham_types_type,height=ham_types_val) ax1.tick_params(rotation=90) ax1.set_title("HAM") ax2.bar(spam_types_type,height=spam_types_val) ax2.tick_params(rotation=90) ax2.set_title("SPAM")
これらの準備を済ませた上で,学習の準備を進めていきます.まずtrain_test_splitです.
import numpy as np from sklearn.model_selection import train_test_split X = np.array(ham_emails + spam_emails) y = np.array([0] * len(ham_emails) + [1] * len(spam_emails)) #+でつなげる X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
次にhtmlをプレーンテキストに直します.これは,後に文章のステミングをするときに必要になります.
import re from html import unescape def html_to_plain_text(html): #HTMLをテキストに戻す text = re.sub('<head.*?>.*?</head>', '', html, flags=re.M | re.S | re.I) #ヘッダを落とす text = re.sub('<a\s.*?>', ' HYPERLINK ', text, flags=re.M | re.S | re.I) #ハイパーリンクを文字列に text = re.sub('<.*?>', '', text, flags=re.M | re.S) #これはなにをしているのか? text = re.sub(r'(\s*\n)+', '\n', text, flags=re.M | re.S) #改行をとる return unescape(text) html_spam_emails = [email for email in X_train[y_train==1] if get_email_structure(email) == "text/html"] sample_html_spam = html_spam_emails[7] print(sample_html_spam.get_content().strip()[:1000])#変換前 ><HTML><HEAD><TITLE></TITLE><META http-equiv="Content-Type" content="text/html; charset=windows-1252">><STYLE>A:link {TEX-DECORATION: none}A:active {TEXT-DECORATION: none}A:visited {TEXT-DECORATION: >none}A:hover {COLOR: #0033ff; TEXT-DECORATION: underline}</STYLE><META content="MSHTML 6.00.2713.1100" >name="GENERATOR"></HEAD> ><BODY text="#000000" vLink="#0033ff" link="#0033ff" bgColor="#CCCC99"><TABLE borderColor="#660000" cellSpacing="0" >cellPadding="0" border="0" width="100%"><TR><TD bgColor="#CCCC99" valign="top" colspan="2" height="27"> ... print(html_to_plain_text(sample_html_spam.get_content())[:1000])#変換後 >OTC > Newsletter >Discover Tomorrow's Winners >For Immediate Release >Cal-Bay (Stock Symbol: CBYI) >Watch for analyst "Strong Buy Recommendations" and several advisory newsletters picking CBYI. CBYI has filed to be traded >on the OTCBB, share prices historically INCREASE when companies get listed on this larger trading exchange. CBYI is >trading around 25 cents and should skyrocket to $2.66 - $3.25 a share in the near future. ...
さらに,上の関数の出力をテキストに直す関数を定義します.
#さらに上の出力をテキストに直す def email_to_text(email): html = None for part in email.walk(): ctype = part.get_content_type() if not ctype in ("text/plain", "text/html"): continue try: content = part.get_content() except: # in case of encoding issues content = str(part.get_payload()) if ctype == "text/plain": return content else: html = content if html: return html_to_plain_text(html)
次に,ntlkを使ってステミングを実行していきます.これはステミングの例です.
import nltk stemmer = nltk.PorterStemmer() for word in ("Computations", "Computation", "Computing", "Computed", "Compute", "Compulsive"): print(word, "=>", stemmer.stem(word)) >Computations => comput >Computation => comput >Computing => comput >Computed => comput >Compute => comput >Compulsive => compuls
ステミングにより,活用形や複数形から,単語の根幹(ステム)を取り出せます.
上の作業をクラスにまとめていきます.パイプラインを上で組んだ時と同じように,ダックタイピングの特性を活かして,fitとtransformのメソッドを作りますが,前処理ではtransformしか必要ないので,fitメソッドはなにもしないように定義してやります.
from sklearn.base import BaseEstimator, TransformerMixin class EmailToWordCounterTransformer(BaseEstimator, TransformerMixin): def __init__(self, strip_headers=True, lower_case=True, remove_punctuation=True, replace_urls=True, replace_numbers=True, stemming=True): self.strip_headers = strip_headers self.lower_case = lower_case self.remove_punctuation = remove_punctuation self.replace_urls = replace_urls self.replace_numbers = replace_numbers self.stemming = stemming def fit(self, X, y=None): #何もしない return self def transform(self, X, y=None): X_transformed = [] for email in X: #emailに関して繰り返し text = email_to_text(email) or "" #メールをテキストにする if self.lower_case: text = text.lower() #すべて小文字にする if self.replace_urls and url_extractor is not None: urls = list(set(url_extractor.find_urls(text))) #メール中のurlを抽出する urls.sort(key=lambda url: len(url), reverse=True) for url in urls: text = text.replace(url, " URL ") if self.replace_numbers: #数値を置き換えるか text = re.sub(r'\d+(?:\.\d*(?:[eE]\d+))?', 'NUMBER', text) if self.remove_punctuation: #句読点を取るかどうか text = re.sub(r'\W+', ' ', text, flags=re.M) word_counts = Counter(text.split()) if self.stemming and stemmer is not None:#単語の数え上げ+ステミング stemmed_word_counts = Counter() for word, count in word_counts.items(): stemmed_word = stemmer.stem(word) stemmed_word_counts[stemmed_word] += count word_counts = stemmed_word_counts X_transformed.append(word_counts) return np.array(X_transformed)
これのtransformメソッドで,次のような単語の数え上げが返ってきます.このメールは3つのメールを内包したマルチコンテントなので,3つの結果が返ってきます.
X_few = X_train[:3] X_few_wordcounts = EmailToWordCounterTransformer().fit_transform(X_few) X_few_wordcounts #3つのmultimailを内包しているので,3つのCounterがかえってくる >array([Counter({'chuck': 1, 'murcko': 1, 'wrote': 1, 'stuff': 1, 'yawn': 1, 'r': 1}), > Counter({'the': 11, 'of': 9, 'and': 8, 'all': 3, 'christian': 3, 'to': 3, 'by': 3, 'jefferson': 2, 'i': 2, 'have': 2, 'superstit': 2, 'one': 2, 'on': 2, 'been': 2, 'ha': 2, 'half': 2, 'rogueri': 2, 'teach': 2, 'jesu': 2, 'some': 1, 'interest': 1, 'quot': 1, 'url': 1, 'thoma': 1, 'examin': 1, 'known': 1, 'word': 1, 'do': 1, 'not': 1, 'find': 1, 'in': 1, 'our': 1, 'particular': 1, 'redeem': 1, 'featur': 1, 'they': 1, 'are': 1, 'alik': 1, 'found': 1, 'fabl': 1, 'mytholog': 1, 'million': 1, 'innoc': 1, 'men': 1, 'women': 1, 'children': 1, 'sinc': 1, 'introduct': 1, 'burnt': 1, 'tortur': 1, 'fine': 1, 'imprison': 1, 'what': 1, 'effect': 1, 'thi': 1, 'coercion': 1, 'make': 1, 'world': 1, 'fool': 1, 'other': 1, 'hypocrit': 1, 'support': 1, 'error': 1, 'over': 1, 'earth': 1, 'six': 1, 'histor': 1, 'american': 1, 'john': 1, 'e': 1, 'remsburg': 1, 'letter': 1, 'william': 1, 'short': 1, 'again': 1, 'becom': 1, 'most': 1, 'pervert': 1, 'system': 1, 'that': 1, 'ever': 1, 'shone': 1, 'man': 1, 'absurd': 1, 'untruth': 1, 'were': 1, 'perpetr': 1, 'upon': 1, 'a': 1, 'larg': 1, 'band': 1, 'dupe': 1, 'import': 1, 'led': 1, 'paul': 1, 'first': 1, 'great': 1, 'corrupt': 1}), > Counter({'url': 5, 's': 3, 'group': 3, 'to': 3, 'in': 2, 'forteana': 2, 'martin': 2, 'an': 2, 'and': 2, 'we': 2, 'is': 2, 'yahoo': 2, 'unsubscrib': 2, 'y': 1, 'adamson': 1, 'wrote': 1, 'for': 1, 'altern': 1, 'rather': 1, 'more': 1, 'factual': 1, 'base': 1, 'rundown': 1, 'on': 1, 'hamza': 1, 'career': 1, 'includ': 1, 'hi': 1, 'belief': 1, 'that': 1, 'all': 1, 'non': 1, 'muslim': 1, 'yemen': 1, 'should': 1, 'be': 1, 'murder': 1, 'outright': 1, 'know': 1, 'how': 1, 'unbias': 1, 'memri': 1, 'don': 1, 't': 1, 'html': 1, 'rob': 1, 'sponsor': 1, 'number': 1, 'dvd': 1, 'free': 1, 'p': 1, 'join': 1, 'now': 1, 'from': 1, 'thi': 1, 'send': 1, 'email': 1, 'your': 1, 'use': 1, 'of': 1, 'subject': 1})], > dtype=object)
最後に上の結果を特徴量ベクトルとして格納するクラスを作ります.
from scipy.sparse import csr_matrix #上の結果をベクトルに変換するクラス class WordCounterToVectorTransformer(BaseEstimator, TransformerMixin): def __init__(self, vocabulary_size=1000): #引数のvocabulary_sizeを増やせばさらに多くの単語を扱える self.vocabulary_size = vocabulary_size def fit(self, X, y=None): total_count = Counter() for word_count in X: for word, count in word_count.items(): total_count[word] += min(count, 10) most_common = total_count.most_common()[:self.vocabulary_size] self.most_common_ = most_common self.vocabulary_ = {word: index + 1 for index, (word, count) in enumerate(most_common)} return self def transform(self, X, y=None): rows = [] cols = [] data = [] for row, word_count in enumerate(X): for word, count in word_count.items(): rows.append(row) cols.append(self.vocabulary_.get(word, 0)) data.append(count) return csr_matrix((data, (rows, cols)), shape=(len(X), self.vocabulary_size + 1))#sparse matrixにする ほぼ0なので
こんな感じで動きます.
vocab_transformer = WordCounterToVectorTransformer(vocabulary_size=10)#例のために単語数を少なくする X_few_vectors = vocab_transformer.fit_transform(X_few_wordcounts) X_few_vectors.toarray() #10単語が3メール分ある >array([[ 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], > [99, 11, 9, 8, 1, 3, 3, 1, 3, 2, 3], > [65, 0, 1, 2, 5, 3, 1, 2, 0, 1, 0]], dtype=int64) vocab_transformer.vocabulary_ #数え上げた単語の結果 >{'the': 1, > 'of': 2, > 'and': 3, > 'url': 4, > 'to': 5, > 'all': 6, > 'in': 7, > 'christian': 8, > 'on': 9, > 'by': 10}
それでは待ちに待った学習を開始します.まずパイプラインを作りましょう.パイプラインでは
- EmalToWordCounterTransformerでメールのパース,ステミング,単語の数え上げを行う
- WordCounterToVectorTransformerで,上の結果(dict形式)をベクトルになおす
を実施します.この前処理パイプラインは,訓練データにのみfitさせることに注意します.
from sklearn.pipeline import Pipeline preprocess_pipeline = Pipeline([ ("email_to_wordcount", EmailToWordCounterTransformer()), ("wordcount_to_vector", WordCounterToVectorTransformer()), ]) X_train_transformed = preprocess_pipeline.fit_transform(X_train) X_test_transformed = preprocess_pipeline.transform(X_test) #fit_transformではないことに注意 はまった
ロジスティック回帰を適用し,各種指標を計算します.
from sklearn.linear_model import LogisticRegression, LogisticRegressionCV from sklearn.model_selection import cross_val_score from sklearn.metrics import precision_score, recall_score from sklearn.metrics import roc_curve, auc cv_log = LogisticRegressionCV() cv_log.fit(X_train_transformed,y_train) ml_log = LogisticRegression(C=cv_log.C_[0])#best parameter ml_log.fit(X_train_transformed,y_train) score_log = cross_val_score(ml_log, X_train_transformed, y_train, cv=3, verbose=3) y_pred_log = ml_log.predict(X_test_transformed) fpr_log, tpr_log, _ = roc_curve(y_test,y_pred_log) print("CV mean: {}".format(score_log.mean())) print("CV sd: {}".format(np.sqrt(np.var(score_log)))) print("Test accuracy: {}".format(ml_log.score(X_test_transformed,y_test))) print("Precision: {}".format(precision_score(y_test,y_pred_log))) print("Recall: {}".format(recall_score(y_test,y_pred_log))) print("AUC: {}".format(auc(fpr_log,tpr_log))) >CV mean: 0.9858333333333333 >CV sd: 0.004823265376162628 >Test accuracy: 0.9916666666666667 >Precision: 0.96875 >Recall: 0.9789473684210527 >AUC: 0.9865033871808233
結構いい感じです.XGBoostも試します.
from xgboost import XGBClassifier ml_xgb = XGBClassifier() ml_xgb.fit(X_train_transformed,y_train) score_xgb = cross_val_score(ml_xgb, X_train_transformed, y_train, cv=3, verbose=3) y_pred_xgb = ml_xgb.predict(X_test_transformed) fpr_xgb, tpr_xgb, _ = roc_curve(y_test,y_pred_xgb) print("CV mean: {}".format(score_xgb.mean())) print("CV sd: {}".format(np.sqrt(np.var(score_xgb)))) print("Test accuracy: {}".format(ml_xgb.score(X_test_transformed,y_test))) print("Precision: {}".format(precision_score(y_test,y_pred_xgb))) print("Recall: {}".format(recall_score(y_test,y_pred_xgb))) print("AUC: {}".format(auc(fpr_xgb,tpr_xgb))) >CV mean: 0.9816666666666666 >CV sd: 0.00523741878749025 >Test accuracy: 0.9916666666666667 >Precision: 1.0 >Recall: 0.9473684210526315 >AUC: 0.9736842105263157
全然チューニングしていませんが,素晴らしい結果です.
かなり長くなりましたが,MNISTデータの水増し,標準化,PCAをパイプラインで構築,加えてスパム分類器の構築を通して自然言語の前処理から機械学習モデルの適用までを学びました.特に後者の自然言語処理は,細かいことはわかってませんが,大まかな作業の流れがつかめたような気がします.英語だと単語の分割が非常に簡単ですが,日本語だと難しそうですね.
Goodfellow et al (2016)のDeeplearningが良かった
少し前から,Goodfellow, Bengio & Courville (2016)のDeeplearningを読んでいます.ほとんどの方がよく知っている本かもしれません.表紙が悪夢のようなイラストの本です.
本のサイトはこちらhttps://www.deeplearningbook.org
GithubでPDF版も配布されています(僕はこちらを読んでいます). github.com
こういった技術書は紙のほうが読みやすいという人は,印刷版を買うのも良いと思います.
Deep Learning (Adaptive Computation and Machine Learning)
- 作者: Ian Goodfellow,Yoshua Bengio,Aaron Courville,Francis Bach
- 出版社/メーカー: The MIT Press
- 発売日: 2016/11/18
- メディア: ハードカバー
- この商品を含むブログ (1件) を見る
もともと,この本はよく通勤中やランニング中に聞いているポッドキャストのひとつ,misreading chatで紹介されていたもので,興味を持っていました.ひょんなことから会社で印刷版を買うことになり,自宅に持ち帰ったりするのが面倒だったので,結局PDF版を読んでいます.
先日,第1部のApplied math and Machine Learning Basicsを読み終わりまして,結構感動したことが多かったので,その感想を雑にまとめておきます.
数学的道具の説明が必要かつ十分
本書では,機械学習全般,特にDeeplearningの歴史を振り返りつつ,近年の動向を概観した後に,線形代数の基礎的な話が始まります.大学院の研究で普段線形代数をめちゃくちゃ良く使うので,こういう線形代数の話は耳にタコができるほど聞いてきたのですが,シンプルかつ明快な線形代数のイントロがとても心地よかったです.特に,固有値分解の一般化としての特異値分解,ムーアペンローズ一般逆行列などの説明が好みでした.最後に,学んだことを使って主成分分析を導出するエンディングも,素敵です.
その後,線形代数と同じく重要な数学的道具として,確率論や情報理論,最適化の話が続きます.だいたいこういった数学的道具の説明は「小難しくて理解できない割に,本論ではあまり使わない」ものが多いように感じていましたが,説明も簡潔かつ直感的に理解しやすい,必要十分な説明に留められていると感じました.
新しく学んだこと1: CapacityとVC次元
5.2では,機械学習モデルのUnder/Overfittingの考え方を紹介する際に,モデルの複雑性=capacityという概念を導入して説明しています.良いモデルとは,データに対して十分なcapacityを持つものだと考えられますが,そのcapacityを評価するための重要な概念として,VC次元(Vapnic-Chervonenkis dimension)を紹介しています.これは「任意のラベル付けで完全に分離できるデータポイントの数」であり,たとえば2次元空間上のデータポイントなら,3点までならどんなラベル付けをしたとしても完全に分離できるデータポイントは3です.よってこのときのVC次元は3になります.このVC次元は,無限のサイズの仮説集合に対して,まぁまぁいい学習を行うために必要な訓練データの数を導く上で必要になるらしいです(参考:計算論的学習理論入門 -PAC学習とかVC次元とか-).
新しく学んだこと2: ベイズ推定と正則化
線形回帰モデルのベイズ推定を解説している箇所があります.ここではいろいろと式を展開してパラメータの事後分布を求めていくと,Ridge回帰のclosed formの解に事後平均が一致することが示されています.つまり,ベイズ推定とは本質的には正則化である,という結論らしく,この点は今まで知らなかったポイントでした.
機械学習アルゴリズムの構築
データに対してどのような機械学習アルゴリズムを適用するかを表現するためには,次の4つのbuilding blockを明らかにせよ,という主張です. - どのデータを使うか? - 適用しようとしているモデルは何か? - 損失関数は何か? - 最適化アルゴリズムは何か? たとえば,同じデータであっても,モデルを変えればまったく違ったアルゴリズムになりますし,最適化アルゴリズムを変えれば違った解が得られるかもしれません.逆に言えば,4つのうちどれかがはっきりと決まっていないと,ほかの人にアルゴリズムを伝えるときに,正しく伝えられない=再現性が取れない可能性があります.この辺は,実務家として理解しておくべきことだと思いました.
書きながら「理解が甘いなぁ」というところも再発見しましたが,何度も書いているように,必要十分な知識を簡潔に得ることのできる本(というかまだイントロですが)だと感じました.これから本題のdeeplearningに入っていきますが,楽しみです.
また感想がまとまったら書きます.
参考文献
Goodfellow, I., Bengio, Y., Courville, A., & Bengio, Y. (2016). Deep learning (Vol. 1). Cambridge: MIT press.
普段の時間の使い方と研究の記録
僕は普段会社員として働くかたわら,2年前からとある大学の博士課程に入学して,博士学生として研究もやっています.そのような二足のわらじを履いてはや二年,いろいろ試行錯誤しつつたどり着いた時間の使い方などについて書きたいと思います.
一日の流れ
だいたいこんな感じで仕事と研究を両立させています.
- 7:00 起床(たまに早く起きてジョギングなど運動)
- 8:00 出社
- 18:00 帰宅
- 21:00 食事や入浴を済ませ研究開始
- 23:30 研究終了
- 24:00 一日のまとめなどして就寝
割とホワイトな会社かつ「遅くまで会社にいないやつ」と周囲に印象づけることに気をつけて会社員生活を歩んできたので,結構早く帰宅しています.また,専攻は多変量解析法の理論的研究(主成分分析や因子分析などの多変量解析法をいろいろ魔改造する研究)なので,基本的に自宅で研究できます.先生とはたまに連絡を取りつつ,主に論文を書いたりしています.
今はこんな感じの時間の使い方で落ち着いているのですが,こういうパターンを発見するまでにいろいろと紆余曲折がありました.その中で学んだことを以下にまとめます.
遅くまで研究しすぎない
院生になりたての頃は「研究頑張るぞ」と息巻いて,深夜2時や3時まで研究をがんばる夜も多くありました.しかし今年で30代に入った事もあってか,遅くまで研究すると次の日の仕事にかなり響きます.何日か繰り返すと慣れるかなと思ったのですが,全く慣れず,それどころか次の日の夜は早く寝たい気持ちになってしまうので,結局研究に避ける時間が少なくなる事に気づきました.そんなこともあって,最近は(よほど何かの締切に追われていない限りは)24時までには研究を終えて床につくようにしています.「細く長く」続けていくスタイルが僕にはあっているようです.
子供が生まれると研究に割ける時間が増える
子供が生まれる前は,夕食後妻とダラダラしていたので,結局研究を開始できるのは妻が眠って以降の22時頃からでした.しかし子供が生まれて以降,妻と子供が遅くとも21時頃には,規則正しく就寝するようになったので,その分長く研究時間が取れています.周囲には「子供が生まれると自分の時間がなくなるよね」と言われていましたが,実際生まれるとそんなことはないなぁと思いました.もちろん,夜泣きがひどいときにはあやすのを手伝ったりもします.
ちゃんと研究してると仕事にもいい影響が出る
夜やっている研究と昼の仕事とが比較的近いせいもあり,夜しっかり研究していると,その時の気づきを仕事に応用したり,反対に仕事の気づきを研究に応用したりと,二足のわらじならではのメリットも大きいなと思います.頭も切り替わっていい感じです.
記録を取ることは大事
研究生活を始めた当初から,画像みたいな感じで研究ノートをEvernoteにつけています.ほぼ日記みたいな感じです. 毎日やったことや,やり残したこと,感想などを書き付けておくと,達成感もあるし,タスク管理も簡単にできるので,重宝しています.1年前の研究ノートとか見ると,結構懐かしい気持ちになります.WunderlistとかAsanaとか,タスク管理やプロジェクト管理のアプリを使ってみたこともあったけど,結局めんどくさくてあんまり使わず,Evernoteに落ち着いています.
また思いついたら書きます(ブルーライトカットメガネを会社に忘れてきて,目がしょぼしょぼして疲れる...).