FC2ブログ

子供の落書き帳 Remix

15/4/13:ひと月に一度更新するブログになってしまっている

scikit-learnとTensorFlowによる実践機械学習 輪読会7章
2018/07/29(日) 21:45:58

2018/06/15(金)に、scikit-learnとTensorFlowによる実践機械学習輪読会#7で発表してきた。

scikit-learnとTensorFlowによる実践機械学習/原題:Hands-On Machine Learning with Scikit-Learn and TensorFlow」の7章の説明を担当した。資料はこちら!



前回の担当は4章の一部でした。こちらもあわせてどうぞ。
scikit-learnとTensorFlowによる実践機械学習 輪読会4章 子供の落書き帳 Remix

発表を担当した動機


以下2点の動機により、この章の発表をした。

・ランダムフォレストについてはあまり理解していなかったので、発表を通じて自分の理解を深めようと思ったため。
・kaggleなどで一般的に使われているxgboostやlightGBMが勾配ブースティングの手法なので、これらのライブラリが何をやっているのか押さえておいたほうが良いのかなと思った

前者に関してはバッチリ。発表するとなると単に参加するよりも何倍も本を読み込まなければならない。大変なのは確かだけど、その分知識が身についた。
後者に関しては、xgboostの方式をちゃんと理解するところまでは行っていない。

Extra-Treeの方式が分からない



スライドにも書いたが、Extra-Tree(Extremely Randomized Tree)の方式が分からなかった。
なぜなら、色々調べていると、参考サイトによって計算方法が微妙に異なっているからだ。

scikit-learn公式を見てみよう。
1.11. Ensemble methods - scikit-learn 0.19.2 documentation の文章を訳すと、以下のようになる。

Extremely Randomized Treeでは、乱数は分割が計算される方法に関してさらに一歩進んでいます。ランダムフォレストの場合と同様に、特徴量のランダムなサブセットが使用されますが、最も識別可能なしきい値を探す代わりに、特徴量ごとにランダムにしきい値が選び出され、これらのランダムに生成されたしきい値のうち最良のものが分割ルールとして選択されます。

(特徴量サブセットの中の)各特徴量について、ランダムにしきい値を決める。一度それらを全部試してみて、一番うまく分割できたものを選ぶ。「うまく分割できた」というのは具体的にいえば、候補の中でコスト関数が最小となるものである。

ところが、ネットを調べてみると他のサイトでは違ったことを書いている。

各splitで、対象変数X_iと閾値θ_jのいずれも完全にランダムに決める
(合成変量とアンサンブル:回帰森と加法モデルの要点 p.74)

(決定木の分岐をするときに)分岐関数候補をランダムに K 個選択
分岐関数の決定方法は、一般的には2つの方法が取られる

単純にランダム選択 ( K=1 ) Extremely Randomized Trees[ P. Geurts 06 ]
(Random Forestsとその応用(PDF) pp.15-16)

分岐関数の決定を完全にランダムに
ランダムフォレストの基礎と最新動向(PDF)

これを読む限り「それぞれの特徴量に対して試してみてから最良のものを選ぶ」とは書いていない。特徴量(X_i)をランダムに選択して、閾値(θ_j)をランダムに選択して、その条件で分岐させてしまう、というふうに読める。

一体どっちが正しいんだろうか。
色々探したけど、前者のscikit-learn公式と同様の説明を書いているところが見当たらない。
っていうことは後者だろうか。でも後者だと分岐のしかたが全くランダムなので、「良い分岐」になる確証が全く無い。そんな完全ランダムな分岐のしかたで、うまく分類できるのか? が疑問だ。


ここまで来たらscikit-learnのコードを追ってみるか、元々の論文に当たるかをしないと分からない気がする。


AdaBoost



ブースティングの中でも一般的なアルゴリズムらしいが、なかなか複雑だった。一通りサッと本を読んだだけだと、中身を把握しづらい。
理解できたら、なかなか興味深い手法だと思ったので、どこかで一回まとめてみたい。

ここでは手法の詳細は解説しないが、この動画の説明が、分かりやすくて良かったので大変オススメ。英語だけどスライドにだいたいの説明は書いてあり、説明の図も豊富なので、見る価値は十分ある。
(61) Ensembles (4): AdaBoost - YouTube

なお、Python 機械学習プログラミングのほうには、一回の反復処理を具体的に書いてあって、詳細な説明がされている。合わせて読むと良いだろう。


gradient boosting(勾配ブースティング)のlearning_rateは何を意味するか



scikit-learnとTensorFlowによる実践機械学習の本では、「個々の木の影響力を調整する」とだけ書いてあって、いまいち分かりにくい。(p.197)

scikit-learnの解説を見てみよう。
sklearn.ensemble.GradientBoostingRegressor関数の説明のほうを見ると、「それぞれの木の寄与率を、learning_rate倍に縮めます」としか書いていない。いまいちよく分からない。

ところが、同じscikit-learnの公式ドキュメントでもアンサンブル法の説明のところを見ると、数学的な定式化も含めて詳細に書いてある。こっちを読めば良かったんだ!

各ステップの関数を足す前に、learning_rateを掛け合わせている。これによって学習速度を下げて、過学習を抑えている。勾配ブースティングに対するこの操作は「Shrinkage(収縮)」と呼ばれているようだ。


以下、輪読会のときにいただいたコメント。

●learning_rateが大きいと、外れ値に対して値を合わせに行ってしまう。つまり外れ値を学習して過学習に陥る。learning_rateが小さければ『値を合わせに行く割合』が小さくなるので、過学習に陥りにくくなるのではないか。

●learning_rateが小さいと、学習するには多数の木を使う必要がある。そのため、1回の学習に時間がかかる。「パラメータを色々変えながらグリッドサーチで良いパラメータを見つけたい」というようなときは、learning_rateを大きくして、学習時間を短くしたほうが良い。
参考:Python 勾配ブースティングにおけるパラメータと調整方法について - CRUNKYおいしい

以上。それでは。
スポンサーサイト



  1. 2018/07/29(日) 21:45:58|
  2. プログラミング
  3. | トラックバック:0
  4. | コメント:0

Numpyのスライスによる要素取り出し、booleanによるインデックス指定について
2018/07/21(土) 17:46:28

きっかけ


scikit-learnとTensorFlowによる実践機械学習の輪読会に参加して読み進めている。

その中によく分からないコードがあったので、調査をしてまとめた。


動作が分からなかったコードがこれだ



handson-ml/06_decision_trees.ipynb at master | ageron/handson-ml | GitHubの[7]である。
(このソースコード自体は決定木について扱っている。しかし分からなかった部分は決定木に直接関係するものではなく、単なるNumpyのコードである)


from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data[:, 2:]
y = iris.target

X[(X[:, 1]==X[:, 1][y==1].max()) & (y==1)] # widest Iris-Versicolor flower

# => array([[4.8, 1.8]])


扱っているのは、機械学習でよく登場するiris(アヤメ)のデータセットだ。Xにはアヤメの「花弁の長さ」「花弁の幅」が2次元配列の形で入っている。yは各アヤメの品種を表す。
やっている処理は、本によれば「ある特定の品種(versicolor)のデータのうち、花弁の幅が最大のものを探し出すと、花弁の長さが4.8、花弁の幅が1.8である」らしい。しかし、どうしてこのコードでそれが出てくるのか分からなかった。

調べてみると、以下の3つの要素が組み合わさっている式だと分かった。順に説明する。

(A) X[:, 1] で特定行だけを抽出する



変数のあとに[]をつけて、その中で要素のインデックスを指定している。この方法の正式名称はスライス(スライシング)という。

2次元配列Xに対してX[:, 1]と書くと、行列の2列めだけを抽出する。

1つ目の次元(=行方向)に関しては何も条件を入れず、全ての要素を抽出している。「:」は全ての要素を表わす。
2つ目の次元(=列方向)については0始まりのインデックスで1の位置だけを抽出している。(つまり左から2番目の列である)

元の例はちょっとサイズが大きいので、小さい2次元配列の例でやってみよう。行列の2列めだけを抽出しているのが分かる。


import numpy as np
X1 = np.arange(12).reshape(3,4)

X1
# => array([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])

X1[:,1]
# => array([1, 5, 9])


(B) 上記のスライシングで、末尾の, :は省略してもよい




NumPyの公式ページにはこう書いてある。


If the number of objects in the selection tuple is less than N , then : is assumed for any subsequent dimensions.
拙訳:
選択に用いたタプルの中のオブジェクト数がNより小さい場合、後に続く全て次元に関して「:」があるものと見なされる。
Indexing -- NumPy v1.14 Manual



例えば、2次元配列Xに対してX[1:3]と指定すると、X[1:3, :]と指定したと見なされる、ということだ。


X1[1:3]
# => array([[ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])


実際には2次元の使用例が多いが、2次元配列に限らず、一般の多次元配列に対しても同様である。
例えば、Numpyの例では3次元配列に対して適用している。



# Numpy公式より引用:

x = np.array([[[1],[2],[3]], [[4],[5],[6]]])
x.shape
# => (2, 3, 1)
x[1:2]
# => array([[[4],
# [5],
# [6]]])


上の例で、x[1:2]はx[1:2, :, :]と書いたのと同じになる。

(C) X[条件式]と書くと、条件を満たすものだけを抽出できる



Numpy公式ページでは「Boolean array indexing」という名前で載っている。


X2 = np.arange(9)
np.random.shuffle(X2)

X2
# => array([5, 3, 0, 2, 1, 4, 8, 7, 6])

boolean_array = [True, False, True, False, True, False, True, False, True]
X2[boolean_array]
# => array([5, 0, 1, 8, 6])

X2[X2 < 5]
# => array([3, 0, 2, 1, 4])

X3 = X2.reshape(3,3)

X3
# => array([[5, 3, 0],
[2, 1, 4],
[8, 7, 6]])

X3[X3<3]
# => array([0, 2, 1])
# 注意:元の配列が2次元であっても、1次元配列に展開されることに注意!


参考文献



これらのページはとても参考になった。
(A),(B) NumPy配列ndarrayのスライスによる部分配列の選択と代入 | note.nkmk.me
(C) NumPy配列ndarrayから条件を満たす要素・行・列を抽出、削除 | note.nkmk.me



以上を踏まえて、もう一度読み解いてみると、こうなる



最後に、元のコードがどういう理屈で「ある特定の品種(versicolor)のデータのうち、花弁の幅が最大のもの」を抽出したのか、整理しておく。(元のコードに興味のない人は、これ以降は読まなくて良い。)

元々のコードを再度掲載しておく。

X[(X[:, 1]==X[:, 1][y==1].max()) & (y==1)]


少しずつ順番に見ていこう。
X[:, 1]は(A)で書いたとおり、「行列の2行目」だけを取り出す。
2次元配列Xの1行目には花弁の長さ、2行目には花弁の幅が入っているので、2行目の花弁の幅だけがが抽出される。

次にX[:, 1][y==1] だ。[y==1]はTrueとFalseの配列なので、それを指定すると(C)のBoolean array indexingによって条件を満たすものだけを取り出す。
日本語で書くと「y(品種)が1であるアヤメの花弁の幅」だけを抽出している。

X[:, 1][y==1].max()は上記のうちの最大値である。この値は1.8なので、分かりやすいように式を置き換えてしまおう。


X[(X[:, 1]==X[:, 1][y==1].max()) & (y==1)]

X[(X[:, 1]==1.8) & (y==1)]


ここまでくればあとはほぼ同じである。
X[:, 1]==1.8は「花弁の幅が」1.8かどうかのTrue/Falseの配列である。

(X[:, 1]==1.8) & (y==1) は
「y(品種)が1、かつXが1.8」のところだけがTrueであとがFalseの配列である。

最後に、これをXのインデックスに指定するとX[(X[:, 1]==1.8) & (y==1)] となり、これで
「ある特定の品種(1 = versicolor)のデータのうち、花弁の幅が最大のもの」が指定できた。

  1. 2018/07/21(土) 17:46:28|
  2. プログラミング
  3. | トラックバック:0
  4. | コメント:0

DLLAB DAY 2018 ハッカソン部門感想 #dllab
2018/07/17(火) 01:02:30

6月21日、DLLAB DAY 2018(ハッカソン) @ 丸の内KITTEに参加してきた。

最初に言ってしまうと、あんまり満足の行く内容ではなかったので、以下の文章はけっこう批判的である。
このイベントはカンファレンス部門とハッカソン部門に分かれていた。
カンファレンスに参加者として登録したのは254人、ハッカソン部門に登録したのは64人であり、ハッカソンの状況を知る人は少ない。
ハッカソンのほうに参加して感想を書いている人はそれほど多くないようだったので、書いておく価値もあるだろうと思う。

概要



まず、どんなイベントか概要を書こう。
Preferred NetworksとMicrosoftが中心となって作った、DLLABというコミュニティがある。
公式ページには「ディープラーニングに関連する、開発事例や最新技術動向を情報発信するコミュニティ」と書かれている。
そのDLLABの一周年を記念して、東京駅のすぐそばのKITTEで開催されたのが今回の「DLLAB DAY 2018」である。「深層学習を使いこなす日」と銘打っている。
イベントの公式ページ DEEP LEARNING LAB | DLLAB DAY 2018

前述の通り、参加者は「カンファレンス部門」と「ハッカソン部門」に分かれる。
午前中に基調講演があり、これは両部門の参加者とも聴講できる。
昼休みを挟んで13:00~17:40までが別々になる。
カンファレンス部門の参加者は講演を聴く。(同じ時間に3つの講演が同時進行する)
ハッカソン部門の人は別室に移動となり、そこでハッカソンを行う。
なお、ハッカソンは更に「CV(Computer Vision)」と「NLP(Natural Language Processing)」の2つに分かれている。俺はCVに参加した。
運営が事前に、4~5人の組で1つのチームになるようにメンバー編成していた。

17:40以降は懇親会であり、再びカンファレンスとハッカソンの参加者が一緒になる。食べ物・飲み物が提供された。

事前課題


自分にとって不利な情報を隠すのはフェアじゃないと思うのでちゃんと書いとこう。
事前課題が与えられたが、あまりちゃんとやらなかった。
事前課題の中では、MNISTやCIFAR10をchainerでCNNを使って解くやり方を説明している。
ザッと目を通したが、自分の環境上で動かしたのは一部だけにとどまった。
直前にやり始めて時間が足りなかったこと、「chainerのtutorialは以前やったから大体分かるだろう」と考えたのがその理由である。


期間中に起きたこと、行動



課題は当日発表された。githubにも上がっている。
GitHub - DeepLearningLab/PotatoChips-Classification
説明資料はこちら。Deep Learning Lab Day 2018 - Hackathon Presentation
ポテトチップスの分類である。パッケージの画像から、その種類を当てる分類タスクであるが、画像がそっくりの物があり、そんなに簡単ではなさそう。

最初は講師の指示のもと、Dockerを使って全チーム一斉に環境構築……のはずだが、ここで問題が発生した。
Dockerを展開するのに必要な領域が不足していて、先に何チームかが環境を構築した時点でそれ以外のチームが構築できなくなったのである。
幸い、自分のチームは少しだけ早くDockerコマンドを打っていたので、ここでつまづくことはなかった。
(その後、運営スタッフが設定を変更したらしく、最終的には全てのチームが環境構築できた。自分自身は被害がなかったので、この点については不満を感じていない。しかし、懇親会で話をしたら「ハッカソン終了の30分前まで環境が構築できなかった」チームもあるらしい。)

画像分類ということで、まずはVGG16の転移学習を作ってみるのが良いかなという相談をチーム内でした。
運営側はchainerを使うと良いよという話をしていた(注:Preferred NetworksとMicrosoftのコミュニティによるイベントである)。
しかし、5人チームのうち1人は、Keras+Tensorflowの方が慣れているというので、それでコードを書き始めた。
俺はCNNのコーディングは殆どやっていなかったので、運営におとなしく従ってchainerで取り掛かった。
もう1人も同様にchainerで書き始めた。
5人のうち残り2人は、最初に自己紹介した感じではあまり深層学習の経験がないようだった。
特にチーム内で教え合うようなこともなかったので、おそらく何をすればいいか分からない手持ち無沙汰な状態だったと思う。
(リーダーシップを取る人がいなかったので、役割分担が全く無く、個人がめいめいに作業する形になったことは反省している。)

自分自身は1から転移学習のコードを書き上げる能力は無いので、
「chainer VGG16 転移学習」などのキーワードでググって、出てきたページからコピーペーストをして、何とか無理やりコードを書いた。
しかし、切り貼りのしかたを間違えたらしく、よくわからないエラーに悩まされ続けて終わった。
Kerasで書いていたチームメイトもコードがどこか間違えていたらしく、動くことは動いたものの学習が全くできていなかった。
最終的にはKerasの結果を提出した。

我々のチームは実質的に成果なしだったが、
全体で見れば最終的に何らかの学習モデルを提出できたチームが大半であった。
VGGとかResnetなどの転移学習で書いたチームもあるし、全結合の3層MLPで提出したところもあったと思う。

途中から「ハッカソン、こんなに放置されるんか……」という境地に陥った。
「今回のイベントはハッカソンなんだから、教えてもらうもんじゃない、参加者が能動的に動くものなんだ。
手取り足取り教えてほしけりゃハンズオンのセミナーに行けよ」と言われてしまえば確かにその通りなのだが。
(注:俺はハッカソン形式のイベントに初めて参加した)

事前予告との相違点



Dockerの環境構築失敗は予想外のアクシデントだったので、致し方ない部分もあると思う。
しかしながら、事前予告と実際とで異なっている点が2点あった。

connpassのページでは

運営側から提供するもの
・課題、ベースとなるNNモデル

と書いてある。しかし実際には、ベースとなるNNモデルは提供されなかった。

そして、connpassを通じて送られてくるメールには

・また、本番の課題ですが、イベント数日前に公開予定です。

と書いてあった。しかし実際には、課題は本番開始時に公開された。


特に問題なのは1点目だと思っている。
なぜなら、「入力から出力まで正しく動くニューラルネットワークのソースコード」があれば、
あとは機械学習のアルゴリズム部分の改良に専念できたからである。
しかし実際には「0からコードを書いてね」だったので、
「入力データを読み込み、機械学習のアルゴリズムに入力できるように変換する」部分を間違って書いてはエラーになる、ということを自分は繰り返してしまった。

(実際には上記の部分も必要であり、重要である。それは間違いない。
しかし、例えばkaggleのコンペの期間は数ヶ月あるが、今回のハッカソンの期間は4時間程度である。
しかもその4時間の中に環境構築の時間も含まれている。
時間の短さを考えれば、この部分は正解を与えておき、調査や試行錯誤やミス修正に要する時間を省くべきではなかったか。)

疑問に思った点


そろそろ収拾がつかなくなってきたので走り書き。

connpass説明より

・17-18時は各チームの成果発表(2分ずつ)、その後、専門家からベストプラクティスを紹介いただきます。

→正解例のコードはgist上のを見せてもらったけど、
「中身の説明はしませんので、各自で読んでください」だったのでそれは「紹介」なのか?

もし運営側がこの記事を見ていたら、お願いがあります。
正解例のコード、確かgistのプライベートリポジトリだったと思うのですが、URLを資料の所に置いてもらえませんでしょうか。
URLをメモったのですが、間違ってPCの電源を落としてしまってメモを無くしました。



- Example から一歩進んで、実際に深層学習を使う方法を学ぶことができる

→今回は何を学んだというのだろう……

コードをコピペして繋ぎ合わせる力を学んだ、とかですかね……?
(繋ぎ合わせるの結局失敗したけど)

ハッカソンはハンズオンではないので、「教えてくれる人がいて、教材があって、そこから学ぶ」ではない。
「何も教えないので勝手にやって」で学ぶとしたら……他のチームメイトから学ぶ、ということだろうか?




事前課題から比べて、難易度が一気に高くなったな、と思った。
より具体的に言うと、機械学習アルゴリズムの手前の段階が必要だということである。

chainerだとget_mnist関数を使って一発でMNISTのデータをロードできる。
他の深層学習フレームワークもほぼ同様である(たぶん)。
また深層学習以外では、scikit-learnにはload_irisなどの関数があり、サンプルデータを関数1つでロードすることができる。
これらは大変便利ではあるのだが、慣れすぎてしまうと、いざ他のデータを入力すると苦労する羽目になる。
機械学習モデルに入力する前の段階も、ちゃんとできるようにならなきゃいかんな……と、今回のイベントで痛感した。



最後に一つ。
Dockerによる環境構築失敗に対して
「色々と(トラブルが)ありましたが、進めますね」という言い方が何度かあったことである。
金を返せなどと要求するつもりは別に無い。
せめて「ご迷惑をおかけしました、すみません」という一言が欲しかったと思うのである。、
そういう台詞はついぞ聞くことが無かったし、無かったせいで心象がより一層悪くなってしまったので、今こうして書いている。

参加費との釣り合い



無料~低価格のイベントならば、「ま、しょうがないか」と納得することもできた。
どうにも釈然としないのは、このイベントの参加費が10000円だったからであろう。

PyConとかRubyKaigiとかに出たことなかった俺には
イベント一発で1万円は余裕の過去最高金額であった。
scikit-learnとTensolFlowの本を買うときもいくぶん躊躇してたけど、その2倍以上する。
俺は個人的にイベントに申し込みしていて、会社から費用を出してもらっていないので、つまり自腹である。
自腹を切って行った結果がこれか……!!
(余談であるが、「会社から費用を出してもらった」「上司に『勉強しに行ってこい』と言われて申し込んだ」という人も多く、普段俺が行くような勉強会とは客層がやや違っていたように思う。)


ブレインパッドとかキカガクとかのハンズオンセミナーで手取り足取り教えてもらおうとすれば、
1万円なんてものでは済まない。
(機械学習による問題解決実践 (データサイエンティスト入門研修) | データ活用人材育成サービス by BrainPad:2日216000円)
(ディープラーニング ハンズオンセミナー | 株式会社キカガク:3日216000円)
それに比べりゃ遥かに安いと思うべきなのか。

ちなみに、dllabのイベントがいつも高額というわけではない。このイベントの次の週にもイベントがあった。
Microsoft Machine Learning Meetup - connpass
この参加費は500円である。……ちきしょー。
ええ、分かってます。
普段は品川のマイクロソフト本社で開催するから会場費がかからないんですよね。
それと比べて今回のDLLAB DAY 2018は東京駅近くの会場を借りたから会場費がかかっています。そのためには参加費を上げる必要があったんですよね。

ハッカソン参加者の感想レポート


他のハッカソン参加者の感想レポートを見つけたので、最後に貼っておきます。

DLLAB DAY 2018 ハッカソンに参加してきた - OKKAH NET
DLLAB DAY 2018行ってきました | 彌冨 仁研究室
DLLAB Dayのハッカソンに参加してきたよ(実装編) - テキトー
  1. 2018/07/17(火) 01:02:30|
  2. プログラミング
  3. | トラックバック:0
  4. | コメント:0

sedでスペースをタブに置換 (連続スペースは1文字とみなす)
2018/07/08(日) 12:57:29

きっかけ


Linuxのmpstatで取ったパフォーマンスの記録をExcelに貼り付ける必要があった。
Excelでは空白区切りを認識しないので、空白(半角スペース)をタブに変換したい。
このとき、連続する空白は1つのタブ記号に置換したい。
(連続しない1つだけ単独の空白も1つのタブ記号に置換したい。)

Linuxコマンドのsedを使ってやってみた。
(Excelの「テキスト ファイル ウィザード」を使えば可能ではあるが、GUIで大変なので)

環境 (sedのバージョン)


GNU sed、BSD sed、POSIXとかで微妙に挙動が違うらしい……

BSD sed などに代表される POSIX に準拠した sed は OS X などで標準で使用されているが、一般的な Linux で用いられる sed は後述する GNU sed であることが多い。ゆえに、Linux ユーザが OS X の sed を使用するとオプションなどのいわゆる「方言」的な違いに混乱することでしょう。
sed コマンドの用法 - Qiita


この記事では、以下の環境で確認した。

cygwin 64bit版

$ sed --version
sed (GNU sed) 4.2.2
Copyright (C) 2012 Free Software Foundation, Inc.
(後略)



「正規表現を有効にする」には?


正確には「拡張正規表現(extended regular expressions)を有効にする」らしい。
拡張正規表現なしでsedを使っても一部の正規表現は使えるが、普段の正規表現のやり方と微妙に違ったり、使えない演算子があったりして、かえってややこしい。
拡張正規表現を有効にしておいたほうが分かりやすい。

「-r -eを使おう」と書いてあるサイトと、
「-Eを使おう」と書いてあるサイトがある。

-eは「コマンドの指定をする」オプションであるため、
「-r -e コマンド」だと正しく動くが、「-e -r コマンド」だと-eとコマンドが離れているためエラーになる。
しか実のところ、-eは省略しても良いらしい。謎だ……。

置換の指定コマンド



以下のどれか1つ。
's/ +/\t/g'
's/\s+/\t/g'
's/ {1,}/\t/g'
's/\s{1,}/\t/g'


s : 置換
g : 行の全体で置換を実行 (つけないと行内で最初に出てきたスペースだけ置換される)

\s 空白とタブ両方に対応する
+ 1回以上の連続
{n,} で「n回以上の連続」を指定できる。

\sを使った方法は、入力にタブが無ければ期待通りに動く。

まとめ



最終的に、
「入力ファイルのスペースをタブに変換して出力する、ただし連続したスペースは1つのタブに変換する」
コマンドは、以下の通りである。

以下のどれか1つ。(ファイル名をinput.txt, output.txtとする)
sed -E 's/ +/\t/g' input.txt > output.txt
sed -E 's/\s+/\t/g' input.txt > output.txt
sed -E 's/ {1,}/\t/g' input.txt > output.txt
sed -E 's/\s{1,}/\t/g' input.txt > output.txt

「-E」の代わりに「-r -e」でもよい。「-r」でもよい。
  1. 2018/07/08(日) 12:57:29|
  2. プログラミング
  3. | トラックバック:0
  4. | コメント:0

pythonの正規表現のmatch, search, findall関数の違いを調べて整理した
2018/07/01(日) 22:18:41

以下のサイトが役に立った。自分で調査した結果も併せて整理しておく。
Pythonでの正規表現の使い方 - Qiita
正規表現 HOWTO ? Python 3.6.5 ドキュメント

共通


pythonのバージョンは3.6.0。

正規表現でマッチするものが見つかったときには、matchオブジェクトというオブジェクトが使われる。
matchオブジェクトの中に、マッチした箇所の情報が格納される。
(今回はmatchオブジェクトの中身の構造については触れない)

match関数 文字列の先頭だけを探す


これは検索対象の先頭からだけ検索して、マッチするものがあるか判定する。
探しているパターンが先頭にあればマッチするが、途中に含まれていてもマッチしない。

・マッチしなかったらNoneが返る。
・マッチした場合はmatchオブジェクトが返る。
・matchオブジェクトの中にはマッチした箇所の情報が入っている。

以下は特殊なシンタックスを使わない、ごく単純な例だ。
下の例の2つ目では、では"regular expression"の中に"lar"は含まれているが、最初ではないのでマッチせず、Noneが返っている。


import re

result = re.match("reg", "regular expression")
print(result)
print(type(result))
# 出力:
# <_sre.SRE_Match object; span=(0, 3), match='reg'>
#

result = re.match("lar", "regular expression")
print(result)
print(type(result))
# 出力:
# None
#



search関数 文字列の全ての位置を探す。


これは検索対象の位置にかかわらず検索する。


result = re.search("reg", "regular expression")
print(result)
print(type(result))
# 出力:
# <_sre.SRE_Match object; span=(0, 3), match='reg'>
#

result = re.search("lar", "regular expression")
print(result)
print(type(result))
# 出力:
# <_sre.SRE_Match object; span=(4, 7), match='lar'>
#


文字列の途中でもマッチしているのが、match関数と違う点である。
また、マッチしない場合は、match関数と同様にNoneが返る。

search関数の注意:パターンにマッチするのが2つ以上ある場合は最初の1つだけが返る。

"regular expression"の中で、「アルファベットが1文字以上連続する箇所」を探してみよう。
これは「\w+」と書き表せる。
(厳密には、\wはアルファベット、数字、およびアンダースコアの集合である)
このパターンに該当するのは「regular」と「expression」の2つだが、返り値を見ると最初の「regular」の部分だけが入っている。


result = re.search("\w+", "regular expression")
print(result)
print(type(result))
# 出力:
# <_sre.SRE_Match object; span=(0, 7), match='regular'>
#



findall関数 パターンに当てはまるもの全てをリストにして返す



マッチする箇所が複数あったら、1つ目だけではなく全て返してほしい、という場合もあるだろう。
このような場合ではfindall関数を使う。

返り値の決まり方は結構ややこしい。
Python 3.6.5 ドキュメントのre.findallの箇所にはこう書かれている。
「Return all non-overlapping matches of pattern in string, as a list of strings. (中略) If one or more groups are present in the pattern, return a list of groups; this will be a list of tuples if the pattern has more than one group. 」
3つの場合分けがある。順に見ていこう。

まず、1つ目の場合。パターンの指定時にグルーピングの( )を使わない場合、返り値は文字列のリストである。

result = re.findall("\w+", "regular expression")
print(result)
print(type(result))
print(type(result[0]))
# 出力:
# ['regular', 'expression']
#
#



グルーピングの( )を使う場合、まずパターン内に( )が1つだけであれば、グループ(内にある文字列)のリストになる。
下の例の2つ目では、マッチングは1つ目の例と同様になるが、( )の中にeが入っていないので返り値にeが含まれていない。

result = re.findall("(e.)", "regular expression")
print(result)
print(type(result))
print(type(result[0]))
# 出力:
# ['eg', 'ex', 'es']
#
#

result = re.findall("e(.)", "regular expression")
print(result)
print(type(result))
print(type(result[0]))
# 出力:
# ['g', 'x', 's']
#
#


そしてパターン内に( )が2つ以上ある場合、グループ(内にある文字列)をタプルにしたもののリストになる。
下の例の2つ目では( )がネスト(入れ子)になっているが、タプル内の順序は左括弧の登場順である。

result = re.findall("(e)(.)", "regular expression")
print(result)
print(type(result))
print(type(result[0]))
# [('e', 'g'), ('e', 'x'), ('e', 's')]
#
#

result = re.findall("(e)((.)..)", "regular expression")
print(result)
print(type(result))
print(type(result[0]))
# 出力:
# [('e', 'gul', 'g'), ('e', 'xpr', 'x'), ('e', 'ssi', 's')]
#
#



最後に、返り値の方式にかかわらず、マッチしない場合は空のリストが返る。

result = re.findall("AAAAA", "regular expression")
print(result)
print(type(result))
# 出力:
# []
#



まとめ


3つの関数の仕様を整理しておこう。
■match関数は文字列の先頭だけを探す。
■search関数は文字列の全ての位置を探す。パターンに当てはまるものが複数ある場合は最初のものを返す。
■findall関数は文字列の全ての位置を探す。パターンに当てはまるもの全てをリストにして返す。返り値は以下の3通りある:
  -パターンの指定にグルーピングの( )を使わない場合、返り値は文字列のリストである。
  -パターンの指定にグルーピングの( )を使う場合、パターン内に( )が1つだけであれば、グループ(内にある文字列)のリストになる。
  -パターンの指定にグルーピングの( )を使う場合、パターン内に( )が2つ以上であれば、グループ(内にある文字列)をタプルにしたもののリストになる。


書いたきっかけとおまけ



正規表現でsearchを使うべきところでmatchを使っていて、ずっとNoneが返ってきてしまい、だいぶ時間を費やした。

何も考えずに「python 正規表現」でググったとき、python公式ドキュメントで最初に見つかるのはこれだ。
6.2. re ? 正規表現操作 ? Python 3.6.5 ドキュメント
これはリファレンスであり、初めて使う人が参考にするには厳しい。
終わり近くの6.2.5.3. search() vs. match()という箇所で一応言及はしているのだが、最初に見た人がここにたどり着くのは難しい。
同じ公式ドキュメントなら、以下のほうがオススメである。
正規表現 HOWTO ? Python 3.6.5 ドキュメント
  1. 2018/07/01(日) 22:18:41|
  2. プログラミング
  3. | トラックバック:0
  4. | コメント:0