静かなる名辞

pythonとプログラミングのこと

TechAcademyがteratailの質問・回答を盗用?していた件

編集履歴

2019/03/22 朝

 投稿。その後数回微調整。

2019/03/22 夜

 タイトルと内容を全面的に改稿。

  • 現時点で確証がない部分について表現・内容を修正
  • 情報を追加

2019/03/23 朝

  • 追記

はじめに

 私はteratailというQAサイトで回答をしていて、pythonカテゴリ総合一位だったりします。あちこちのサイトを見ていたら、TechAcademyというプログラミングスクールのマガジンページがteratailの質問と回答を盗用しているかも? という話を見つけました。図々しいと思いながらも情報をまとめておきますので、読者の皆さんはなにかの参考にでも役立ててください。

先人たちのまとめ

 こちらのブログ記事などが概要を掴むのに良いと思います。

evalcat.hatenablog.com

 5ちゃんねるのteratailヲチスレでも取り上げられているので(現在進行形)、とりあえずリンクを貼っておきます。

medaka.5ch.net


 要約すると、

  • 「TechAcademyマガジン」というオウンドメディアで、主に「TechAcademyに実際に寄せられた質問に現役エンジニアのメンターが回答しました。」という体裁の記事にteratailからの転載と思われる投稿が多数(少なくとも2桁、下手したらそれ以上)ある。
  • 有志の作成した「転載記事リスト」には特定のメンターの名前(複数人。実名なのかどうか、そもそも実在する人物なのかは不明)が多く上がる。
  • TechAcademy側は「Twitterや匿名掲示板などで取り上げられた」記事を削除して対応中。

 という感じです。

一例

 こんな感じです。この問題が盛り上がり始めた、最初期のツイートで取り上げられたものです。なお、TechAcademyの元記事は消えていたのでWeb魚拓です。

【魚拓】curlコマンドをJavaScriptのAjaxで実行する方法とは【メンターが回答】 | TechAcademyマガジン
jQuery - curlコマンドをjqueryのajaxで実行する方法|teratail

 見ていただければわかる通り、出典として元の質問を示す訳でもなければ(まあ「TechAcademyに寄せられた」という体裁なので示せないのですが)、内容を書き換えて誤魔化すといった小手先の芸もありません。コードはもちろん、質問・回答の文言もほぼそのままコピペです。良識を疑うとかそれ以前に、「なんでこれで問題にならないと思ったの?」というのが率直な感想です。

 こういうのがン十件あり、深刻な事態を伺わせます。

法律上はどうなるのか

 ふと「teratailの規約が緩すぎて法律上何ら問題ない」ということになったら大変だと思い*1、teratailの規約を調べました。

第9条(権利帰属)

3.登録ユーザーは、投稿データについて、当社に対し、世界的、非独占的、無償、サブライセンス可能かつ譲渡可能な使用、複製、編集、改編、掲載、転載、公衆送信、上映、展示、提供、販売、譲渡、貸与、翻訳、翻案、配布などができる権利および二次的著作物に関する現著作権者の権利(著作権法21条ないし28条の権利をいい、商用利用を含む)に関するライセンスを付与します。
利用規約|teratail(テラテイル)

 基本的には健全な内容でした。teratailのユーザはteratailの運営会社に対して諸権利をライセンスしている、という立場です。

 よって、

  • teratail運営がTechAcademy運営に対して、こういう使い方が可能になるライセンスを付与したのであればセーフ
  • それ以外は法的にアウト

 という状況であると考えられます。ユーザがライセンスを付与しているのはteratailの運営会社に対してであって、それに関係ない事例に対しては通常の場合と同じ扱いになるからです。

 なお、こういう使い方をされるのを承知でteratail運営がデータの権利を渡した確率は蓋然的には限りなく0に近いと考えられます。あまりにも酷い使われ方であり、これを承諾したのであればteratail運営の信用問題にも発展しかねません。・・・が、その可能性も皆無ではないため、初稿の時点でこの記事に多くあった「断定的な表現」は後から修正しています(2019/03/22夜時点*2*3)。

 また、「TechAcademyに実際に寄せられた質問に現役エンジニアのメンターが回答しました。」という記載は著作権以外の問題も発生させます。teratailの質問・回答をコピペしてつくったQ&A記事の質問が実際にTechAcademyに寄せられている訳はないので、実態より多くの質問が寄せられている、と誤認させていることになります。これは一種の誇大広告「みたいなもの」でしょう。「こんなに賑わっているプログラミングスクールなら安心して勉強できる」と思って入会する人がいれば詐欺「のようなもの」です。ただ、私に法律関連の知識はないので、これがどの程度深刻なものかは正直なところわかりません。あくまでも参考として書いておきます。

そもそもTechAcademyとは何ぞや

 プログラミングスクールです。「最短○○でエンジニアに!」のような広告などをよく目にすると思います。

 スクールとしての評判は、ググって出てくるページなどを読んでください。実際に自分が体験した訳でもありませんし、これ以上述べることはしません。

 また、ありがちなことですが、この運営会社は営業活動の一貫としてオウンドメディアを運営し、Web記事も発信しています。「スーツを着たイケメンのお兄さんと女子大生風の女の子のアイコンがセリフでしゃべるページ」と言えば、検索で引っかかった記憶がある方も大勢いると思います。

 で、こういう盗用?なんて真似をしている会社はどんなところなのでしょうか? 去年に大炎上した侍エンジニア塾の場合、元中の人によれば「設立が新しく、資本規模も小さい会社」であり、いろいろな杜撰さも同情に値するかどうかは別として「わからんではない」という面がありました。

古巣の侍エンジニア塾(株式会社侍)の炎上について思うこと【2018/10/23追記】 - INODEVLOG

 ということで、運営会社のページです。重要そうなところだけ抜粋しています。

会社名 キラメックス株式会社
事業内容 プログラミング教育事業
設立 2009年2月2日
資本金 4,800万円(資本準備金を含む)
親会社 ユナイテッド株式会社(東京証券取引所マザーズ市場)

運営会社 | TechAcademy [テックアカデミー]

 歴史はそれなり(10年)にあるようです。

 ふーん、マザーズ上場企業が親会社ですか。立派な会社なのかな? と思ってしまいますが、どうやら完全に子会社化されたのは近年のようです。

ユナイテッド株式会社によるキラメックス株式会社の完全子会社化について(2016年2月3日) | キラメックス株式会社

 それ以前に資本関係があったのかどうかまでは調査不足で不明です。とはいえ、問題の記事は子会社化されてから出てきたものが多くを占める訳で、けっきょく「上場企業(の子会社)がこんな杜撰な真似をしている」という構図には変わりはありません。こういうのってコンプラ的に一発アウトだと思うんですが、どうなんでしょうね。

なぜ人は間違えるのか(なんでこんなことになってしまうのか)

 ここまでの説明でも、あまりにも酷い状況なのは皆さんにおわかりいただけたと思うのですが、「どうしてこんなことするの?」という疑問を持った方も大勢おられることでしょう。簡単にですが私が説明できる範囲で大雑把な背景を説明します(正確を心がけますが、憶測も含みます)。

 基本的に、まともな記事を供給する、というのはけっこうお金がかかります。「エンジニア」として食っていける実力のある人が一時間とか二時間かけて記事を書いたら、時給ン千円以上ですから相当なコストになります。ASCII.jpとかならそれでやっているかもしれませんが(まあ彼らも彼らで厳しいだろうけど)、ほとんどのところはコストを捻出できないでしょう。更に、オウンドメディアの場合は「安いコストで宣伝に寄与してくれればいい」程度に思われていて、それほど予算を割かれていないといった状況が容易に想像できます。

 なので、多くのIT系オウンドメディアは給料の安い自社社員(当然専門的な技術力はない)に書かせるか、クラウドソーシングなどで安く書いてくれる人を探して書かせるか、インターンにやらせるか、といった運営方法を取っていると思います。結果、qiitaの殴り書きよりひどいクオリティのオウンドメディア記事が量産されてしまう訳です。

 では、今回の件は、TechAcademyがクラウドソーシングとかで雇った人が暴走しちゃった?

 その可能性はほぼない、と個人的には考えます。普通、ライターにそこまでの裁量権はないからです。ああいう記事をライターの方が作ってしまうとしたら、「teratailの質問をTechAcademyに寄せられた質問として転載してほしい」という依頼を受けた、というケース以外まったく考えられません。

 頼まれもしないのにこっそりやってバレたら、次の仕事はなくなります。ヘタしたら「企業イメージを損なった」って訴えられかねない。

 だって、確実に真っ黒で法律違反な行為ですよ? ライターの裁量でできる訳がない。

 また、「インターネットで囁かれ始めてからの対応が『記事の削除』だった」という話もあり、限りなくTechAcademy本体*4が黒と言う他ありません。

 ではどの辺まで黒いか? 以下はすべて憶測になりますが、常識的な遵法意識とリスク感覚を持つ(であろう)上位の経営陣が最初からすべて把握していた、というのは考えづらい状況です。どちらかといえば、担当者数名くらいが暴走してこうなったような気がします、というかそれくらいが自然に思えます。

 いずれにせよまともな会社のやることとしては悪質すぎるし、企業イメージへの悪影響も大きいので、上の憶測が正しい場合、もしこれ以上オオゴトになれば担当者は誰かしら処分されるんじゃない? と見ています。あーあ、、、、

他にもがばいよ!

 さすがに盗用と比べると霞んで見えますが、クオリティは低いです。

ソースコード

n = 1
 n+=1
 print (n)

https://techacademy.jp/magazine/18190

 そのままコピペしました。きっと真似して書いた人は「IndentationError: unexpected indent」に悩まされるでしょう。

 推して知るべし・・・

今後の展開

  • とりあえずTechAcademyは事実関係を認めて謝罪した方が良いと思います。ライセンスの件についてはイマイチはっきりしませんが、アクセスしたユーザに嘘をついた(teratailのコピペをTechAcademyに寄せられた質問と回答として紹介した)ことは事実です。「なかったこと」にするには遅すぎます(最初から遅すぎるのだが)。逆に、本格的に炎上する前に傷を閉じられれば、相対的に浅く済む可能性もあります。
  • teratail運営はできれば早急にこの件についての立場を示すべき。「ああいう使い方をされるのを承知で売っていた疑惑」が少しでも残っている限り、ちょっと安心して使い続けることができません。

 以下の2つは、権利の不当な侵害があったとした場合ですが、

  • 権利侵害の不当利益返還請求とかは、個人で考える気には正直なれません。理論的にはできるだろうけど、手間に見合った額が取れるかと言うととても微妙な気しかしない。
  • teratailの運営は、不当に権利を侵害されたのであればなんか言ってみるのはありだと思います。ただ、teratailの運営会社のレバレジーズ(本業は人材アウトソーシングや転職支援などです)と、TechAcademy側にはそれなりの取引関係があることを考慮すると、最終的に「穏便に済ませる」といった判断に落ち着くのではないでしょうか。さすがに「今後も野放し」というのは、一teratailユーザとしては支持できないところなので、ほどほどの落とし所を探ってほしいのですが。

 おまけ。

  • この記事のアクセスが伸びる(伸びて)

今後の予防策

 プログラミングスクールや、オウンドメディア系のIT記事などが世間を騒がせた事例は去年幾つかありました。

qiita.com

 さすがにここまで続くと構造的な問題があるとしか言えません。予防策を書きます。

 とりあえず一般ユーザーに対しては、以下くらいしか言えることがありません。

  • オウンドメディア系記事は検索で出てきても信用しない。はっきり言ってクオリティの平均値は個人のブログ以下ですし、オウンドメディアによくある初心者向け的な内容であれば、書籍やチュートリアルを読んだ方がわかりやすくて正確な知識がつきます。
  • プログラミングスクールは・・・まあ、どうしても必要なら評判とか見て選んでください。オンライン学習なら書籍+progateとかやっすいサービスでも(優秀な人は)できると思うので、そういうのを検討するのもありです。また、オンラインスクール系よりは昔ながらの「専門学校」などの方が安定はしているでしょう。

 IT企業などの経営者の方が読んでいたらとしたら、私から伝えたいことは

  • 半端な気持ちで自社メディア、あるいは教育事業を運営できると思うな

 の一言ですね。まともにやるなら、相当のコストを覚悟してください。それで運営して黒字にならないなら、やるべきではないと思います。

 また、すでにそういう自社メディア、事業を抱えてしまっている企業の方がもしいたら、

  • いますぐ相当のコストを投入して、品質に問題がないかチェックし、問題があれば修正する

 のがおすすめです。潜在的な破壊力はすでに証明されていますから、もはや時限爆弾みたいなものです。炎上するまで放置するより、先になんとか処理した方が賢明だと思います。

まとめ

 とにかくひどい。そういう感想しかありません。なんかあれこれ論評するまでもない。

 とりあえず、関係者の皆さんには「まともに」対処してほしいところですね。

2019/03/23 追記

 現時点では、これに関係する記事がほとんど削除されているようです。このまま幕引きを図るつもりだと思われます。

 今後の動向をチェックし続け、何かあれば編集します。また、この件についてteratailの運営にも問い合わせているので、そちらの進展もいずれご報告します。

*1:私がteratailを使い続けるかどうかも考え直さないといけなくなります

*2:なお、それまでにこの記事が集めたPVは本ブログの一日の総PVの1割未満であり、集めたはてブやtwitterのシェアも片手で数えられる程度で、現時点で世間にさほど大きな影響を及ぼしてはいないことを付記しておきます

*3:また、この件については私からteratail運営に問い合わせを行っています。結果が返ってくれば追記しますが、あまり期待しないでください

*4:あるいはメディア運営をまるごと別会社に委託しているとか、そういう可能性もあるかもしれませんが、考慮しません

【python】複数のlist(など)を対象にmapを使う

 組み込みのmapは実は複数のiterableを引数に取れるように定義されています。

追加の iterable 引数が渡されたなら、 function はその数だけの引数を取らなければならず、全てのイテラブルから並行して取られた要素に適用されます。複数のイテラブルが与えられたら、このイテレータはその中の最短のイテラブルが尽きた時点で止まります。

2. 組み込み関数 — Python 3.6.8 ドキュメント

 簡単な例を見てみます。

>>> list(map(lambda x,y: x+y, [1,2,3], [4,5,6]))
[5, 7, 9]

 numpyが使えないor使うまでもないときに、ちょっとした配列操作をやるのに良いかもしれません。

 ドキュメントにもある通り、zipなどと同じく結果の長さは最短の引数に合わせられます。

>>> list(map(lambda x,y: x+y, [1,2,3], [4,5]))
[5, 7]

 また、itertools.startmapで同じことをやるとすると、こうなります。

>>> from itertools import starmap
>>> list(starmap(lambda x,y: x+y, zip([1,2,3], [4,5,6])))
[5, 7, 9]

www.haya-programming.com

 単に複数の引数を取る関数を使いたければ、mapの第三引数以降を使った方が簡単です。

【python】__slots__は速度的にどうなのか

概要

 __slots__を使うとメモリをケチれるという話はよく見かけますが、属性アクセスの速度については話を聞かないので調べてみました。

実験コード

import timeit

class A_slots:
    __slots__ = ["a"]
    def __init__(self):
        self.a = 42

class A_attr:
    def __init__(self):
        self.a = 42

a_s = A_slots()
a_a = A_attr()

for a in [a_s, a_a]:
    print("{:.4f}".format(timeit.timeit(lambda : a.a)))

 大したことはやっていません。

結果

0.1026
0.1114

 何回かやっても__slots__に定義した方が1割ほど速いという結果になりました。でも10^6回やって0.1秒なので、ほとんど問題にならないかも。

おまけ

>>> import timeit
>>> timeit.timeit(lambda :None)
0.08125272499659332

 timeitだけでこれくらいの時間がかかるので、実質2~3倍くらいは速いことになります。

【python】辞書で複数の値を一つのキーにする

概要

 複数の値を一つのキーにまとめて、結果と対応させたいというケースがあります。

>>> d = {1,2:"hoge", 3,4:"fuga"}  # こんな感じ?

 残念ながらこれはエラーになります。

  File "<stdin>", line 1
    d = {1,2:"hoge", 3,4:"fuga"}
            ^
SyntaxError: invalid syntax

どうすれば良いのか

 基本的にはtupleをキーにします。

>>> d = {(1,2):"hoge", (3,4):"fuga"}
>>> d[(1,2)]
'hoge'

 これで(1,2)というtupleと"hoge"をマッピングできました。

やりたいことはそうじゃなかった

 複数のキーのうち、どれでアクセスしても同じオブジェクトにマッピングされるものがほしい、というケースもあるでしょう。上のtupleを使ったコードはそれを満たしていません。

>>> d[1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 1

 しょうがないので、個別にキーに対応する値を登録してやることにします。

>>> s1 = "hoge"
>>> s2 = "fuga"
>>> d = {1:s1, 2:s1, 3:s2, 4:s2}
>>> d[1]
'hoge'
>>> d[2]
'hoge'

 うまく行っている気がする? でも、一つ変更しても他はつられて変わってくれたりはしませんね。

>>> d[1] = "piyo"
>>> d
{1: 'piyo', 2: 'hoge', 3: 'fuga', 4: 'fuga'}

 ポインタ的なものをもう一段追加して、連動して変更できるようにすることも一応可能です。listを使ってやってみます。

>>> l1 = ["hoge"]
>>> l2 = ["fuga"]
>>> d = {1:l1, 2:l1, 3:l2, 4:l2}
>>> d[1][0]
'hoge'
>>> d[1][0] = "piyo"
>>> d
{1: ['piyo'], 2: ['piyo'], 3: ['fuga'], 4: ['fuga']}

 組み込みでこういうことをやってくれるものは探した限りありませんでした。この方法だと面倒くさいというか使い方に注意が要るので、本格的に活用するつもりであれば自作クラスを作ると良いかもしれません。

【python】sklearn 0.20でclassification_reportの仕様が変わっていた

はじめに

 遅まきながら、sklearn 0.20でclassification_reportの仕様が変わったことに気づきました。

 基本的な使い方は変わりませんが、それなりに大きな変化になります。

変更点

 まず0.19の引数と出力のフォーマット。

sklearn.metrics.classification_report(y_true, y_pred,
    labels=None, target_names=None, 
    sample_weight=None, digits=2)
>>> print(classification_report(y_true, y_pred, target_names=target_names))
             precision    recall  f1-score   support

    class 0       0.50      1.00      0.67         1
    class 1       0.00      0.00      0.00         1
    class 2       1.00      0.67      0.80         3

avg / total       0.70      0.60      0.61         5

sklearn.metrics.classification_report — scikit-learn 0.19.2 documentation

 個人的に使い慣れていたのはこちらです。

 次に0.20の引数と出力のフォーマット。

sklearn.metrics.classification_report(y_true, y_pred, 
    labels=None, target_names=None, 
    sample_weight=None, digits=2, output_dict=False)
>>> print(classification_report(y_true, y_pred, target_names=target_names))
              precision    recall  f1-score   support

     class 0       0.50      1.00      0.67         1
     class 1       0.00      0.00      0.00         1
     class 2       1.00      0.67      0.80         3

   micro avg       0.60      0.60      0.60         5
   macro avg       0.50      0.56      0.49         5
weighted avg       0.70      0.60      0.61         5

sklearn.metrics.classification_report — scikit-learn 0.20.3 documentation

 変わっていますね。まず、output_dictという引数が追加されています。使い方は容易に想像がつき、ドキュメントにも説明がある通りですが辞書を返してくれるようになります。

 また、全体の結果のとりまとめのところでマイクロ平均、マクロ平均、重み付き平均を返してくれるようになりました。これらの意味については以前記事にしたので、そちらを見てください。

hayataka2049.hatenablog.jp

output_dictを試す

 大きな変更点はここなので、試してみましょう。ドキュメントと同様の例で打ち込んでいます。

>>> from sklearn.metrics import classification_report
>>> d = classification_report([0,1,2,2,2], [0,0,2,2,1],
...         target_names = ['class 0', 'class 1', 'class 2'],
...         output_dict=True)
>>> 
>>> from pprint import pprint
>>> pprint(d)
{'class 0': {'f1-score': 0.6666666666666666,
             'precision': 0.5,
             'recall': 1.0,
             'support': 1},
 'class 1': {'f1-score': 0.0, 'precision': 0.0, 'recall': 0.0, 'support': 1},
 'class 2': {'f1-score': 0.8,
             'precision': 1.0,
             'recall': 0.6666666666666666,
             'support': 3},
 'macro avg': {'f1-score': 0.48888888888888893,
               'precision': 0.5,
               'recall': 0.5555555555555555,
               'support': 5},
 'micro avg': {'f1-score': 0.6, 'precision': 0.6, 'recall': 0.6, 'support': 5},
 'weighted avg': {'f1-score': 0.6133333333333334,
                  'precision': 0.7,
                  'recall': 0.6,
                  'support': 5}}

 これはpandasデータフレームに変換できます。

>>> import pandas as pd
>>> df = pd.DataFrame(d)
>>> df
            class 0  class 1   class 2  macro avg  micro avg  weighted avg
f1-score   0.666667      0.0  0.800000   0.488889        0.6      0.613333
precision  0.500000      0.0  1.000000   0.500000        0.6      0.700000
recall     1.000000      0.0  0.666667   0.555556        0.6      0.600000
support    1.000000      1.0  3.000000   5.000000        5.0      5.000000

 なのでデータフレームを介して記録しておきパラメータチューニングに使うとか、TeXの表やビジュアル的なグラフなど任意のフォーマットに吐き出すといった処理が行いやすくなっています。

まとめ

 気づくのが遅れましたが、健全な方向に改良されたと思います。使いやすくなった反面、これで済むようになると他のsklearn.metricsの関数を叩かなくなるので、使い方を忘れるかも・・・という懸念があります(笑)。

【python】print関数を使いこなそう

ぼくたちは本当のprintを知らない

 pythonのprint関数については、たかがprintと思っている人も多いと思いますが、しかしオプションをすべて言える人はあまりいないと思います。把握しておくと出力の細かい制御をしたいとき役立ちます。

 そこで、printの使いこなしについて書きます。なお、念のために先に断っておくと、新し目のpython3のprint関数の話です。python2のprint文とはまったく異なります。

printのオプション

 ドキュメントによると以下の引数を取ります。

print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

組み込み関数 — Python 3.7.3rc1 ドキュメント

 *objectsは任意個のオブジェクトをprintできることを示します。sep=' 'は、オブジェクトの区切り文字が半角スペースになるということです。これは*objectsに渡したオブジェクトの数-1個表示されます。また、end='\n'は行末に何を置くかを指定しており、デフォルトは改行です。これは行末に1つだけ置かれます。

 特殊なのはfile引数とflush引数です。file引数は出力ストリームに対応し、デフォルトのsys.stdoutはいわゆる標準出力です。任意のファイルオブジェクトを与えられるので、ファイルに書き込むといった用途に使えます。flushはいわゆる出力バッファのフラッシュの挙動になります。何もしなければ標準出力は改行時にフラッシュされますが、これをTrueにするとprintを呼ぶたびにフラッシュされるようになります。

sepの使い方

 何も指定しないと半角スペースになるので、次のような見慣れた挙動になります。

>>> print(1, 2, 3)
1 2 3

 空の文字列にすることで間を詰めることが可能です。

>>> print(1, 2, 3, sep="")
123

 あるいは好きな文字列を渡すことも可能です。

>>> print(1, 2, 3, sep=",")
1,2,3
>>> print(1, 2, 3, sep="***")
1***2***3

 リストの要素を適当な区切り文字で区切って表示したいというような場合、str.joinで頑張るより、リストのアンパックとprintのsep引数を組み合わせて使った方がスマートだったりします。

>>> l = [1, 2, 3]
>>> print(",".join(map(str, l)))  # こちらの場合は型変換も必要
1,2,3
>>> print(*l, sep=",")  # これだけ
1,2,3

 裏を返せば表示形式はすべて__str__任せになるので、細かい出力フォーマットは指定できないということでもあるのですが、簡易的に使いたい場合はsepの方が便利です。

endの使い方

 改行させたくないときにendを空文字列にする、というのがよくあるユースケースです。

>>> for x in range(4):
...     print(x)
... 
0
1
2
3
>>> for x in range(4):
...     print(x, end="")
... 
0123

 最後に一回改行するために、もう一回printを呼ぶと良いと思います。

 プログレスバー的な進捗表示を簡単にやりたいとき重宝しますが、それっぽく見せるためには後述のflush引数も併用する必要があります。

fileの使い方

 fileはたとえばファイル出力に使えます。

>>> with open("hoge.txt", "w") as f:
...     for x in range(4):
...         print(x, file=f)
... 
>>> (ctrl-dでpythonを終了)
$ cat hoge.txt
0
1
2
3

 その気になればcsvファイルの文字列を手動で作って吐き出すといった用途に使えます。あるいはソケットを叩いたりするのにも使えると思います。ただ、濫用しすぎないことをおすすめします(通常は他に良い方法がある)。

flushの使い方

 flushは前述した通りendと組み合わせると便利です。

>>> import time
>>> for _ in range(10):
...     print("*", end="")
...     time.sleep(0.5)
... 
**********

 進捗表示をやりたくてこんなコードを書く人も多いと思いますが、これは期待通り動作しないと思います。確かに最終的な結果は同じでも、5秒ほどしてから出力が一気に出てくるという挙動になるはずです。これはsys.stdoutが改行されるまでフラッシュされないからです。

 flush=Trueで解決します。

>>> import time
>>> for _ in range(10):
...     print("*", end="", flush=True)
...     time.sleep(0.5)
... 
**********

 これでおよそ0.5秒おきに1つずつ表示されるはずです。

本当のprintを理解したあとの感想

 print一つとっても、意外といろいろな使い方があるということがわかるかと思います。出力の制御にけっこう使えます。

 こういう仕様はすべて公式ドキュメントに書いてあります。ドキュメントに普段から慣れ親しんでおくと、スムーズにコーディングできる、行き詰まったとき打開策が思い浮かぶ、ライバルと差が付けられる、といった利益があります。ドキュメントを読むのに慣れていない方は、printなど単純なものから読んでみると楽しいと思います。

【python】sklearnのRandomizedSearchCVを使ってみる

はじめに

 RandomizedSearchCVなるものがあるということを知ったので、使ってみます。うまく使うとグリッドサーチよりよい結果を生むかもしれないということです。

sklearn.model_selection.RandomizedSearchCV — scikit-learn 0.20.3 documentation

比較実験

 とりあえず、先に使い慣れたグリッドサーチでやってみます。digitsデータをSVMで分類するという、100回くらい見た気がするネタです。

コード

import time

import pandas as pd

from sklearn.svm import SVC
from sklearn.model_selection import GridSearchCV
from sklearn.datasets import load_digits

def main():
    digits = load_digits()
    svm = SVC()
    params = {"C":[0.1, 1, 10], "gamma":[0.001, 0.01, 0.1]}
    clf = GridSearchCV(svm, params, cv=5, iid=True,
                       return_train_score=False)
    t1 = time.time()
    clf.fit(digits.data, digits.target)
    t2 = time.time()
    print("{:.2f} 秒かかった".format(t2 - t1))
    result_df = pd.DataFrame(clf.cv_results_)
    result_df.sort_values(
        by="rank_test_score", inplace=True)
    print(result_df[["rank_test_score", 
                     "params", 
                     "mean_test_score"]])

if __name__ == "__main__":
    main()

結果

16.67 秒かかった
   rank_test_score                      params  mean_test_score
6                1   {'C': 10, 'gamma': 0.001}         0.972732
3                2    {'C': 1, 'gamma': 0.001}         0.971619
0                3  {'C': 0.1, 'gamma': 0.001}         0.943239
7                4    {'C': 10, 'gamma': 0.01}         0.705620
4                5     {'C': 1, 'gamma': 0.01}         0.695047
1                6   {'C': 0.1, 'gamma': 0.01}         0.111297
5                7      {'C': 1, 'gamma': 0.1}         0.104062
8                7     {'C': 10, 'gamma': 0.1}         0.104062
2                9    {'C': 0.1, 'gamma': 0.1}         0.102393

 まあ、これはこんなもんでしょう。しっかり最適パラメータを探せている気がします。3*3=9通りのグリッドサーチで、計算には16.67秒かかったとのことです。

RandomizedSearchCVにしてみる

 RandomizedSearchCVの特色は、scipyで作れる確率分布のオブジェクトを渡せることです。パラメータのリストを渡すことも可能ですが、それだと特色を活かした使い方にはなりません。

 scipyで確率分布のオブジェクトを作る方法については、以前の記事で説明したのでこちらを見てください。

www.haya-programming.com

 ここで期待されている「確率分布のオブジェクト」はrvs(分布に従うサンプルを得るメソッド)さえ使えれば何でも良いです。連続分布も離散分布も渡せます。なんなら自作でも行けると思います。

 といってみても、パラメータ設定を確率分布に落とし込むまでは少し頭の体操が要ると思います。

 次のように考えます。たとえば、SVMでパラメータチューニングするなら、とりあえずこんな感じでざっくり見る人が多いのではないかと思います。

[0.001, 0.01, 0.1, 1, 10, 100]

 ここで、0.001が出る頻度に対して0.002はそこそこ出てきてほしいけど、100の頻度に対して100.001は出てきてほしくなくて200とかになってほしい訳です。要するに、上の方になるほど出てくる確率が下がる分布がほしい訳で、こういうのは指数分布が向いている気がします。ということを踏まえて、scipy.stats.exponを使うことにします。

scipy.stats.expon — SciPy v1.2.1 Reference Guide

 scipyのこの辺のドキュメントの説明はお世辞にもわかりやすいとは言えないんですが、指数分布のパラメータは基本的に \lambda一つだけであり、ドキュメントの記述によると

A common parameterization for expon is in terms of the rate parameter lambda, such that pdf = lambda * exp(-lambda * x). This parameterization corresponds to using scale = 1 / lambda.

 ということらしいので、これに従って \lambdaを設定します。指数分布の期待値は \frac{1}{\lambda}なので、要するに平均にしたいあたりをscaleに指定すればいいことになります。

 ちょっと確認してみます。

>>> import matplotlib.pyplot as plt
>>> result = stats.expon.rvs(scale=0.1, size=1000)
>>> plt.figure()
<matplotlib.figure.Figure object at 0x7fe9e5e4b6a0>
>>> plt.hist(result)
(array([639., 222.,  76.,  44.,  11.,   3.,   3.,   1.,   0.,   1.]), array([7.33160559e-07, 9.56411007e-02, 1.91281468e-01, 2.86921836e-01,
       3.82562203e-01, 4.78202571e-01, 5.73842938e-01, 6.69483306e-01,
       7.65123673e-01, 8.60764041e-01, 9.56404408e-01]), <a list of 10 Patch objects>)
>>> plt.savefig("result1.png")
>>> result = stats.expon.rvs(scale=1, size=1000)
>>> plt.figure()
<matplotlib.figure.Figure object at 0x7fe9e5e1bba8>
>>> plt.hist(result)
(array([490., 246., 133.,  66.,  30.,  23.,   2.,   6.,   3.,   1.]), array([2.21631197e-04, 6.36283260e-01, 1.27234489e+00, 1.90840652e+00,
       2.54446814e+00, 3.18052977e+00, 3.81659140e+00, 4.45265303e+00,
       5.08871466e+00, 5.72477629e+00, 6.36083791e+00]), <a list of 10 Patch objects>)
>>> plt.savefig("result2.png")

result1.png
result1.png
result2.png
result2.png

 上手く行っているようですが、指数分布の場合は分散が \frac{1}{\lambda ^2}で要するに期待値の2乗なので、場合によってはいわゆる中央値より少し大きめにしたり、逆に小さめにしたいと思うこともあるかもしれません。

 というあたりを踏まえて、いよいよRandomizedSearchCVでやってみます。基本的な使い方はコードを見れば分かる通りで、paramsの値にscipyの確率分布を渡すこと、n_iterで試す回数を指定できること以外大きな違いはありません。n_iterですが、今回は30回やってみます。

コード

import time

import pandas as pd

from scipy import stats
from sklearn.svm import SVC
from sklearn.model_selection import RandomizedSearchCV
from sklearn.datasets import load_digits

def main():
    digits = load_digits()
    svm = SVC()
    params = {"C":stats.expon(scale=1), 
              "gamma":stats.expon(scale=0.01)}
    clf = RandomizedSearchCV(svm, params, cv=5, iid=True,
                             return_train_score=False, n_iter=30)
    t1 = time.time()
    clf.fit(digits.data, digits.target)
    t2 = time.time()
    print("{:.2f}秒かかった".format(t2 - t1))
    result_df = pd.DataFrame(clf.cv_results_)
    result_df.sort_values(
        by="rank_test_score", inplace=True)
    print(result_df[["rank_test_score", 
                     "param_C",
                     "param_gamma",
                     "mean_test_score"]])

if __name__ == "__main__":
    main()

結果

63.04秒かかった
    rank_test_score     param_C  param_gamma  mean_test_score
23                1     2.62231  0.000465122         0.972732
15                2     1.85208   0.00195101         0.967168
14                3    0.716893   0.00232262         0.963829
26                4    0.633723  0.000583885         0.960490
20                5    0.534343   0.00280065         0.952699
24                6     1.33912   0.00344179         0.949360
2                 7     4.06141   0.00355782         0.947691
21                8    0.460892   0.00307683         0.945465
19                9     1.78068   0.00481671         0.912632
10               10     3.26205   0.00679844         0.817474
29               11    0.288305   0.00525321         0.738453
1                12     1.01437   0.00874084         0.731219
3                13    0.385384   0.00590435         0.725097
5                14     0.61161   0.00702304         0.701169
12               15    0.317451   0.00597252         0.626600
6                16    0.122253   0.00444626         0.521425
4                17      1.1791    0.0187758         0.373400
28               18    0.960299    0.0174957         0.277685
7                19     1.91593    0.0219393         0.272120
18               20     1.17152    0.0264625         0.209794
13               21     1.48332    0.0297287         0.180301
22               22    0.496221    0.0110038         0.178631
11               23    0.747978    0.0178759         0.130217
17               24    0.549571    0.0147944         0.123539
0                25   0.0357021   0.00627669         0.116305
25               26   0.0551073   0.00739485         0.114079
27               27   0.0852409   0.00982068         0.111297
9                27    0.148459    0.0101576         0.111297
16               29    0.818395    0.0521121         0.102393
8                29  0.00447628    0.0442941         0.102393

 最良スコアそのものは実はGridSearchCVと(たまたま)同じですし、時間がケチれるということもないのですが、見ての通りある程度アタリのついている状態でうまい分布を指定してあげれば「最良パラメータの周囲を細かく探索する」といったカスタマイズが可能になります。

 とりあえずこれでどの辺りがいいのかは大体わかったので、今度はその辺りを平均にしてやってみます。2行、次のように書き換えただけです。

改変箇所

    params = {"C":stats.expon(scale=3), 
              "gamma":stats.expon(scale=0.001)}

結果

22.06秒かかった
    rank_test_score      param_C  param_gamma  mean_test_score
28                1      4.17858  0.000512401         0.974402
3                 2      3.64468  0.000429315         0.973845
14                3      6.71633  0.000842716         0.973289
22                3      2.69443   0.00116543         0.973289
6                 3      5.40707  0.000698026         0.973289
13                3      2.80969   0.00118559         0.973289
1                 7      3.00941   0.00135159         0.972732
11                7      2.10982  0.000993251         0.972732
12                7      3.73687   0.00112881         0.972732
2                10      1.18243   0.00144449         0.971063
24               11      1.86039   0.00167388         0.970506
16               11      1.25451   0.00150173         0.970506
10               13      2.45206  0.000338623         0.968837
0                14     0.827324   0.00171363         0.967724
25               15     0.651184   0.00175566         0.965498
29               16      1.24774  0.000424962         0.964385
20               17     0.625333  0.000705793         0.963829
15               18      7.14503  0.000158783         0.962716
27               19     0.461709   0.00106818         0.962159
5                20      2.82219   0.00280609         0.961046
4                21      1.47712  0.000229419         0.959377
7                22     0.536423    0.0005259         0.958264
8                23      2.39408  9.90463e-05         0.953812
18               24      3.53976  6.63105e-05         0.953255
21               24      3.46599  6.85291e-05         0.953255
23               26      3.43756  5.60631e-05         0.952142
26               27       12.188  2.79396e-05         0.951586
9                28     0.331784   0.00038362         0.951029
17               29      0.73821   4.9347e-05         0.930440
19               30  0.000110158  0.000966082         0.141347

 今度はGridSearchCVのときより良いスコアになるパラメータがいくつか出ました。まあ、digitsの大して多くもないデータ数で0.001の差を云々しても「誤差じゃね?」って感じですが・・・

 二段グリッドサーチでやっても同じことはできますが、グリッドを手打ちで入力したりといった手間がかかります。その点、RandomizedSearchCVは分布のパラメータを打ち込めば勝手にやってくれるので、大変助かるものがあります。

結論

 使えます。特に、グリッドサーチのグリッドを手打ちで入れるのが面倒くさいという人に向いています。ただし、どういう分布が良いのかは知識として持っていないといけません。まあ、わからなければ一様分布でアタリをつけて正規分布にして・・・とかでもなんとかなるでしょう。

 ただし、それなりに工夫する要素があるので、玄人向きです。kaggleで使われたりするのも納得がいきます。また、確率である以上ある程度の回数は回さないと安定しないので、そのへんにも注意した方が良いと思います(それでも数が増えてくるとグリッドサーチできめ細かくやるより全然マシですが・・・)。