Kaggle Wheatコンペからの学び 〜 物体検出コンペで当たり前に行われている(っぽい)こと
先頃、KaggleのGlobal Wheat Detectionコンペに参加しました。
大学の研究・仕事を通じてこれまで画像データに触れることがあまりなかったのと、エールビールが大好きな自分としては小麦を眺めているだけで幸せな気分になれたことから、参加することにしました。
結果は、コンペ終了直後時点で、2270の参加チーム中、138位でした。
(注:本コンペは、ライセンス問題に伴いコンペ終了直後の順位がどの程度確定的なものか不明瞭なので、「コンペ終了直後時点で」と書きます。別記事でライセンス問題については触れるかもしれません。)
ただその実態は、人様のNotebookをコピー&ペースト&継ぎ接ぎ、Discussionのアイデアを目を皿のようにして読み込んでパクる、の連発だったので、良い意味で自分の実力と言えるかは甚だ疑問です。
しかし、初めて物体検出コンペに参加し、私見ですが何が当たり前に行われているのかまでは把握できた気がするので、以下メモします。
問題設定・データ概要
簡単に問題設定に触れておきます。
世界各地で撮影された小麦たちの画像から、小麦の頭部を検出するタスクです。クラスが1つの物体検出タスクとなります。

小麦の頭部を高精度に検出できれば、収量の予測であったり、天候不順などによる量やサイズの異常検知に役立つと考えられます。
小麦の数が多く容易に重なってしまったり、風の影響でぼやけてしまうといった画像そのものの検出の難しさに加え、小麦は世界各地で栽培されており栽培されている品種・環境などによる形状の違いもあるため、異なるロケーションでも汎用的に利用可能な検出モデルを構築することが求められています。
撮影されたロケーションは、訓練データ・テストデータ併せて11ある模様です。
訓練データは3422枚、公開されているテストデータは10枚です。
もちろん公開されている10枚のテストデータでスコアが計算され順位が決まるのではなく、公開されていないテストデータが1200枚程度あり、こちらでスコアが計算されます。
どのように公開されていないテストデータで評価が行われるのかというと、本コンペはコードコンペティションと呼ばれる形式で、少なくとも推論コードはKaggleのNotebookで作成する必要があります。
推論コードは必ず所定の形式のsubmisson.csvを出力するよう作成する必要があり、公開されているテストデータに対する推論を行ったsubmission.csvを提出すると、テストデータが公開されていないものに置き換えられてコードが再実行され、スコアが算出されます。
訓練コードは外部環境で作ってOKで、その場合学習で得たweightsをKaggleのDatasetにuploadし、推論コードで利用します。
訓練データの数が3000枚強と比較的少ないため、Data Augmentationが重要になります。
また、bboxが1つもない画像が50枚程度あったり、bboxが異常に大きい(面積の最大は529788!)・小さいものもあり、それらの前処理も必要となります。
さらに、テストデータのロケーション別の構成割合は、こちらで分析されており、原著論文のデータセットの情報から訓練データの構成を差し引くことで推測できます。

この分析から2点の事実が分かります。
- 訓練データとテストデータで、撮影されているロケーションに重複はない
- テストデータの中で、UTokyo_1で撮影された画像が8割近くを占める
1点目の事実から、本コンペではCVの結果よりも、LBの結果を信じた方が良いかもしれないと考えました。実際、Local CVよりもPublic LBの方がPrivate LBとの相関がありました。
もう1つ重要な点として、原著論文にあるように、コンペで与えられている1024 x 1024の画像は、より大きな画像からcropされたものになっています。

このことから何が言えるかというと、ジグソーパズルのように画像をつなぎ合わせて元の画像を再生すれば、そこから新たな訓練データを生成できることになります。(実装)
これは自分の場合もスコアアップに寄与したので、重要な発見だったと思います。
詳細なEDAについてはこちらが素晴らしいので、参考にしてください。
当たり前に行われていること
それでは以下、多くのソリューションで取り入れられている(と私には見えた)手法で、私も試行してみたものを列挙します。
- Dataの修正・追加
- Data Augmentation
- アーキテクチャの選択
- YOLO(コンペ参加直後に触っていた。v5はライセンスの問題で使用禁止に。単独ではおそらく最高精度が出せるモデルだった)
- EfficientDet(YOLOv5が禁止になってからはひたすらD5を中心にEfficientDetで実験していた。EfficientNetの考え方を取り入れた物体検出モデル。実装)
- 他は試してないが、DetectorRSやUniverseNetが良いなどの報告あり
- 高解像度で学習
- リサイズを行わず1024 x 1024の画像で学習(Colab Proではbatch size 1でギリギリCUDA out of memoryを回避できる)
- TTA(テストデータもaugmentation。実装)
- Pseudo Labeling (テーブルデータでもお馴染み、テストデータを予測し確信度の高いラベルのみ訓練データに取り入れて再予測。実装)
- Ensemble (精度を求めるKaggleではWBFが強い場合が多そう。実装, 解説)
上位ソリューション
列挙した手法を忠実に試すだけでも、時間さえとれれば銅メダル圏内くらいまでは行けると思いますが、銀・金圏を狙うならこれでは足りないと思います。
そこで弱々勢がやるべきことは、とにかく上位ソリューションから学ぶことだと思います。有難いことに素晴らしい解法をシェアしてくださっている方がいらっしゃいます。簡単に邦訳しておきます。
1st place solution
Summary
- MMDetectionフレームワーク上で様々なモデルを試した
- 試したアーキテクチャ:GFocal, ATSS, UniverseNet, DetectorRS, SOLO-v2
- 最終的にアンサンブルの対象とした最高精度のモデル
- DetectoRS with the ResNet50 backbone (https://github.com/joe-siyuan-qiao/DetectoRS)
- UniverseNet+GFL with the Res2Net101 backbone (https://github.com/shinya7y/UniverseNet)
- 各々でPseudo Labelingを1ラウンド行いスコアを上げた。また汎化性能を上げるため十分なaugmentationを行った
"ジグソーパズル"
- "ジグソーパズル"によるdata augmentationを行い1330枚の画像を生成。但し、bboxは画像の境界線上にあるなどの理由で自動生成できなかったため、オフラインでbboxを生成した上でpseudo labelingでbboxを生成した
Validation
- MultilabelStratifiedKFold with 5 folds (https://github.com/trent-b/iterative-stratification)
- bboxの数、bboxの面積の中央値、画像のsourceに基づいてstratify
- パズルにより生成した画像が他のfoldに入ってLeakしないようにした
- 原著論文によると、訓練データのusask1, ethz1はテストデータのUTokyo1, UTokyo2, UQ1, NAU1とかなり異なることから、usask1, ethz1はvalidationには使わないようにした
- ただ、Local CVとLBは相関しなかったので、最終的にはLBを主に注視してパラメータのチューニングを行った
- 学習は最初の1 foldのみを使って行った
Augmentation
- Albumentationsによるaugmentation
- HorizontalFlip, ShiftScaleRotate, RandomRotate90
- RandomBrightnessContrast, HueSaturationValue, RGBShift
- RandomGamma
- CLAHE
- Blur, MotionBlur
- GaussNoise
- ImageCompression
- CoarseDropout
- RandomBBoxesSafeCrop(ランダムにN個のbboxを選び、それらが欠けないようにcrop)
- Image colorization (https://www.kaggle.com/orkatz2/pytorch-pix-2-pix-for-image-colorization)
- Style transfer (https://github.com/bethgelab/stylize-datasets)(スタイルには公開されている10枚のテストデータを用いた)
- Mosaic
- Mixup
- Multi-scale Training
外部データ
モデル
- DetectoRS with ResNet50とUniverseNet+GFL with Res2Net101をメインのモデルとした。DetectoRSはUniverseNetより少し精度が良いが非常に遅かった
- Single DetectoRS Public LB score without pseudo labeling: 0.7592
- Single UniverseNet Public LB score without pseudo labeling: 0.7567
DetectoRSでは以下を利用した:
- LabelSmoothCrossEntropyLoss with parameter 0.1
- Empirical Attention (https://github.com/open-mmlab/mmdetection/tree/master/configs/empirical_attention)
学習パイプライン
推論
全てのモデルでTTA x 6を行った:
- Multi-scale Testing with scales [(1408, 1408), (1536, 1536)]
- Flips: [original, horizontal, vertical]
- 後処理はNMS
Pseudo Labeling
- テストデータに対して推論を行い、確信度を
confidence = np.mean(scores > 0.75)により算出。confidenceが0.6を超えるもののみラベルした - source: usask1, ethz1と、mosaic, mixup, colorization, style transferなどでaugmentされたデータはpseudo labelingを行わなかった
- 1 epoch, 1 round, 1 stage
- Data: original data + 3 x pseudo test data
アンサンブル
- WBFによるアンサンブル
- DetectoRSとUniverseNetのスコアの分布が異なったので、scaling using rankdata (https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.rankdata.html) を行った
scaled_scores = 0.5 * (rankdata(scores) / len(scores)) + 0.5
- WBF parameters:
- weights=[0.65, 0.35] respectively for models [DetectoRS, UniverseNet]
- iou_thr=0.55
- score_thr=0.45
最終サブミットのパイプライン(0.7262 on Private LB and 0.7734 on Public LB)
行わなかったこと
- MMDetectionとYOLOV5のアンサンブル(YOLOv5が禁止されたため断念)
- MMDetectionとEfficientDetのアンサンブル(試す時間が足りず。時間があればWBFのパラメータをチューニングしスコアを改善できたと考えている)
行ったが効果がなかったこと
- Wheat Ears Detection Dataset: CVは改善したが、LBは改善せず
- 同一モデルに対する2ラウンド以上のPseudo Labeling
- Scale-aware testing(Waymo Open Dataset Challenge 2020の1st solutionで採用)
- DetectoRSとUniverseNetに対するRotate90 TTA
4th place solution
Summary
- 十分なaugmentation (Custom mosaic, Mixup)
- EfficientDet
- Faster RCNN FPN
- マルチスケールのモデルをWBFによりアンサンブル
- TTA x 8 (hflip, vflip, rotate90)
- Pseudo Labeling
Augmentation
- Custom mosaic
- 通常のcutmixでは2つの画像を合成するところを、4つの画像を合成した
- bboxのboarderの情報が保存されるようにcustomize
- Mixup
- RandomCrop, HorizontalFlip, VerticalFlip, ToGray, IAAAdditiveGaussianNoise, GaussNoise, MotionBlur, MedianBlur, Blur, CLAHE, Sharpen, Emboss, RandomBrightnessContrast, HueSaturationValue
外部データ
- wheat spikes (https://www.kaggle.com/c/global-wheat-detection/discussion/164346), for license please refer (https://www.kaggle.com/c/global-wheat-detection/discussion/164346#928613)
- wheat 2017 (https://plantimages.nottingham.ac.uk/) at post (https://www.kaggle.com/c/global-wheat-detection/discussion/148561#863159).
モデル
- 5 folds, stratified-kfold, splitted by source(usask1, arvalis1, arvalis_2…)
- Optimizer: Adam with initial LR 5e-4 for EfficientDet and SGD with initial LR 5e-3 for Faster RCNN FPN
- LR Scheduler: cosine-annealing
- Mixed precision training with nvidia-apex
スコア
Valid AP/Public LB AP
- EfficientDet-d7 image-size 768: Fold0 0.709/0.746, Fold1 0.716/0.750, Fold 2 0.707/0.749, Fold3 0.716/0.748, Fold4 0.713/0.740
- EfficientDet-d7 image-size 1024: Fold1,3
- EfficientDet-d5 image-size 512: Fold4
- Faster RCNN FPN-resnet152 image-size 1024: Fold1
- Ensemble 9 models above using wbf can achieve 0.7629 Public LB/0.7096 Private LB
Pseudo Labeling
- Base: EfficientDet-d6 image-size 640 Fold1 0.716 Val AP/0.7483 Public LB/0.6822 Private LB
- Pseudo labeling step1: Train EfficientDet-d6 10 epochs on trainset + hidden testset (output of ensembling) with mixup, load checkpoint from base: 0.7719 Public LB/0.7175 Private LB
- Continue train EfficientDet-d6 6 epochs on trainset + hidden testset (output of pseudo labeling step1) with mixup, load checkpoint from pseudo labeling step1 : 0.7754 Public LB/0.7205 Private LB
20th place solution
パイプライン
Validation
usask_1の全データと他のsourceの一部データの663枚をvalidationデータに固定
Augmentation
Custom augmentation
- crop and pad: borderのbboxをcropするのは良くないと考え、borderのbboxとbbox内部の画像は除去した
- crop and resize
- resize and pad
- Color Transfer between Images
- mixup: 訓練データ中の2画像をmixupする代わりに、bboxのある画像とない画像をmixup
- Rotation
Albumentations
- A.HorizontalFlip(p=0.5),
- A.VerticalFlip(p=0.5),
- A.RandomRotate90(p=0.5),
custom augmentationと競合するのでmosaicは行わず
外部データ
- SPIKE dataset
結果
上位ソリューションからの学び
学びしかないわけですが、その中で共通点を探してみると、訓練データの少ないコンペだったことから、Data Augmentationを十分に行ったソリューションが上位に来ている印象です。
augmentationの手法は様々ですが、albumentationsによるaugmentは行いつつ、加えて"ジグソーパズル"などを活用したcustomの手法でaugmentするのが重要だったようです。
customの手法以外では、mosaicは覚えておくべき手法だと思いました。(実装、解説)
おわりに
テーブルコンペ以上に、画像コンペは上位陣が神々のお戯れみたいになってる印象ですが、とは言え一般ピーポーでも基本に忠実にやっていけばメダル圏内までは行けるのかな、とも思いました。
本記事が、画像コンペやってみたいけどなんか怖い、という人の助けに少しでもなれば幸いです。
AtCoder Beginner Contest 171 E - Red Scarf (XORを使う問題)
珍しくE問題が解けたので簡単に解説を入れます。
https://atcoder.jp/contests/abc171/tasks/abc171_e
端的に言えば、自分以外の整数の xor が分かっている状態で、自分の数を求めろという問題です。
ポイントは、Nが偶数であるということです。XORの性質を知っていれば、制約でNが偶数であることに言及されている時点でピンとくるかもしれません。
XORの性質として自分自身とのXORは0になるというものがあります。つまり、偶数回登場する値のXORは常に0になります。
この性質を使うと、以下のように各整数を求められます。整数をniとします。

aiを求めるには、i!=jのすべてのnjのXORをとれば良いです。
なぜなら、i!=jのすべてのajのXORをとると、自分以外のnjは全て偶数回XORがとられて0に消し込まれる一方、niだけは奇数回XORがとられるので残るからです。
つまり、i=1の整数n1は、
n1 = a2 ^ a3 ^ ... ^ an
と求められるわけですが、残りのniを求めるにあたっては、i=2を例にとれば、
n2 = a1 ^ a3 ^ ... ^ an
となることから、
n2 = n1 ^ a1 ^ a2
とすれば求まることに注意します。
実装は以下の通りです。
import sys sys.setrecursionlimit(10**6) n = int(input()) a = list(map(int, input().split())) su = 0 for i in range(1, n): su ^= a[i] ans = [su] for i in range(n-1): su ^= a[i] ^ a[i+1] ans.append(su) print(*ans)
以下、D問題までのPython ACコードです。
https://atcoder.jp/contests/abc171/tasks/abc171_a
import sys sys.setrecursionlimit(10**6) s = input() s_low = s.lower() if s == s_low: print('A') else: print('a')
https://atcoder.jp/contests/abc171/tasks/abc171_b
import sys sys.setrecursionlimit(10**6) n, k = map(int, input().split()) p = list(map(int, input().split())) p.sort() print(sum(p[:k]))
https://atcoder.jp/contests/abc171/tasks/abc171_c
import sys sys.setrecursionlimit(10**6) n = int(input()) d = {(i-97+1)%26:chr(i) for i in range(97,97+26)} l = [] while n > 0: tmp = n%26 l.append(tmp) if tmp == 0: n = n//26-1 else: n //= 26 ans = '' for i in l[::-1]: ans += d[i] print(ans)
https://atcoder.jp/contests/abc171/tasks/abc171_d
import sys sys.setrecursionlimit(10**6) n = int(input()) A = list(map(int, input().split())) q = int(input()) readline = sys.stdin.readline BC = [[int(i) for i in readline().split()] for _ in range(q)] d = {} for a in A: d[a] = d.get(a, 0) + 1 ans = [] su = sum(A) for b,c in BC: if b in d: su = su - b*d[b] + c*d[b] d[c] = d.get(c, 0) + d[b] d.pop(b) ans.append(su) else: ans.append(su) print(*ans)
これまでのA-D問題を中心としたPythonコードは以下に入れています。
AtCoder Beginner Contest 170 (A~D)
先日「D問題はさくっとACできるようになってきた」などとほざきましたが、やってしまいました。
どうも、 予め要素ごとのboolなどの情報(今回のD問題で言えば割り切れるかどうか)を配列で持っておく 発想が身についていない模様。激しく反省せねば。。。
D問題までのPython ACコードです。
https://atcoder.jp/contests/abc170/tasks/abc170_a
import sys sys.setrecursionlimit(10**6) x = list(map(int, input().split())) print(x.index(0)+1)
https://atcoder.jp/contests/abc170/tasks/abc170_b
""" a+b = x 2*a+4*b = y 2*a+2*b = 2*x 2*a+4*b = y b = y/2-x a = 2*x-y/2 """ import sys sys.setrecursionlimit(10**6) x, y = map(int, input().split()) b = y/2-x a = 2*x-y/2 if b >= 0 and b == int(b) and a >= 0 and a == int(a): print('Yes') else: print('No')
https://atcoder.jp/contests/abc170/tasks/abc170_c
import sys sys.setrecursionlimit(10**6) x, n = map(int, input().split()) if n != 0: p = list(map(int, input().split())) ll = [x] lu = [x] i = 1 for _ in range(2*n): if ll[-1] not in p: print(ll[-1]) break elif lu[-1] not in p: print(lu[-1]) break ll.append(ll[-1]-i) lu.append(lu[-1]+i) else: print(x)
https://atcoder.jp/contests/abc170/tasks/abc170_d
""" Point: 割り切れるかどうかの情報を配列で持っておく """ N = int(input()) A = [int(i) for i in input().split()] D = set() S = set() for i in range(N): if A[i] in S: D.add(A[i]) else: S.add(A[i]) M = max(S) X = [1 for i in range(M+1)] # 割り切れない場合1 for a in S: if a in D: X[a] = 0 for i in range(2*a, M+1, a): X[i] = 0 ans = 0 for i in range(N): if X[A[i]] == 1: ans += 1 print(ans)
これまでのA-D問題を中心としたPythonコードは以下に入れています。
AtCoder Beginner Contest 169 (A~D)
D問題はさくっとACできるようになってきましたが、B問題でWA吐いて萎えています。。。
D問題までのPython ACコードです。
https://atcoder.jp/contests/abc169/tasks/abc169_a
import sys sys.setrecursionlimit(10**6) a, b = map(int, input().split()) print(a*b)
https://atcoder.jp/contests/abc169/tasks/abc169_b
import sys sys.setrecursionlimit(10**6) n = int(input()) A = list(map(int, input().split())) ans = 1 m = 1e18 if 0 in A: print(0) else: for a in A: ans *= a if ans > m: print(-1) break else: print(ans)
https://atcoder.jp/contests/abc169/tasks/abc169_c
""" keyword: decimal """ import sys sys.setrecursionlimit(10**6) a, b = map(str, input().split()) from decimal import * ans = Decimal(a)*Decimal(b) ans = int(ans) print(ans)
https://atcoder.jp/contests/abc169/tasks/abc169_d
""" keyword: 素因数分解、二分探索 まずは素因数分解 素因数分解の結果、例えば素数2が5個現れる場合、異なるzで割る際は、2, 2*2, 2*2*2の3つのzで割るのが、最大で行える操作の回数 つまり、各素数について、最大で行える操作の回数mは、1からmまでの公差1の等差数列の和 m*(m+1)/2 が素数の数以下であるような最大の値 このようなmの値は、二分探索で求まる """ import sys sys.setrecursionlimit(10**6) n = int(input()) pf = {} for i in range(2,int(n**0.5)+1): while n % i == 0: pf[i] = pf.get(i,0) + 1 n //= i if n > 1: pf[n] = 1 ans = 0 for v in pf.values(): l = 1; r = v while l+1 < r: c = (l+r)//2 if c*(c+1)/2 <= v: l = c else: r = c ans += l print(ans)
これまでのA-D問題を中心としたPythonコードは以下に入れています。
テレワーク時に自宅ですぐできるWiFi環境改善
直前まで昨日のAtCoder ABCの記事を書こうと思っていましたが、たまにはコードやデータが出てくる記事は一休みし、ライフハック的記事を投下してみます。
ZoomやGoogle MeetなどWeb会議が日常になる中で、ネットゲーマー以外はさほど気にしたことのなかった自宅のWiFi環境を改善する必要に迫られている人、多数いるのではないかと思います。
私は正直、自宅のWiFi環境を改善しなくとも不具合を感じるシーンはあまりなかったのですが、現状の自宅の通信環境が世の中的に見て良いのか悪いのか、いずれだったとしても手軽に改善できる手段はあるのか、調べた方が良いと思い、軽く動いてみました。
信号強度の話
私は光回線のワイヤレスでネットワークに接続していますが、ワイヤレスの信号強度を確認する手段が以下にまとめられています。
私はMacBook Pro (OS: Catalina v10.15.4)を利用しているので、以下手順で信号強度を調べることができます。
1. Spotlight(右上の虫眼鏡ボタン)をクリックして、「ワイヤレス診断」と入力
2. 起動したら[Command]+[Option]+[4]キーを押す
すると、SSIDごとに受信信号強度が表示されます。以下のような感じです。

テーブル中の「RSSI」が受信信号強度にあたります。
ちなみにSSIDですが、私の場合「pr500m-xxx-1」「pr500m-xxx-2」「pr500m-xxx-3」と連番が振られた3つのIDが与えられています。
本来SSIDはセキュリティレベルを変えて通信環境を作る場合に使われる(セキュリティレベルを高くすべき仕事用PCにはそのように設定したxxx-1を使い、セキュリティレベルが高い必要がないゲーム用にはxxx-3を使う、など)もののようですが、多くの家庭ではそのように使い分けていることはなく、なんとなく「xxx-1」を使っています、というのがほとんどではないかと思います。私もそうでした。
本記事の主旨的にはSSID自体はそこまで重要ではないのですが、後でもう一度出てきます。
ひとまず、信号強度の話に戻ります。
強度の見方ですが、以下にまとめられています。
引用すると、
-30 〜 -61dBm 非常に良い
-63 〜 -73dBm 良い
-75 〜 -85dBm 普通
-87 〜 -97dBm 悪い
となります。
私の場合、作業部屋では大体-50dBmでした。これがWiFiルーターに近い場所に移動すると-30dBmになりました。
WiFiルーターと通信しようとしているデバイスの位置関係が、かなり大きく通信環境に影響を与えることがわかります。
私の場合、作業部屋でも「非常に良い」にカテゴライズされる程度の環境の悪化で済んでいたので問題ありませんでしたが、診断の結果数値が悪かった場合は、先ほどの記事の引用ですが以下手段を試すことが推奨されています。
繰り返しになりますが、WiFiルーターの場所はかなり大きく影響することが我が家でも実証されました。
実際の通信速度の話
信号強度が改善できたのでOK!と言いたいところですが、もう1点手軽に診断・改善できるポイントがあります。
それは周波数帯の変更です。
比較的新世代のルーターを使っている方は、2.4GHzと5GHzの2つの周波数帯を使えるようになっているのではないかと思います。
私の場合も、以下の赤枠のように、3つのSSIDのうち1つは5GHzの周波数帯のものでした。

この周波数帯、簡単に言えば道路のようなもので、いくら道が太くても(=信号強度が強くても)みんなが使っていればなかなか進まない(=通信が体感で遅い)し、みんなが使っていなければ信号強度以上に早く感じる、というものになります。
細かく言えば、以下記事にあるようなメリデメがあります。
そして、おそらく多くの家庭では(私の家も含めて)特に気にせず2.4GHzを使っていることが多いのではないかと思いますが、この周波数帯はいろいろなデバイスで使われることが多いため、「混雑」していて思ったほど通信が早くないことがあります。
そこで、5GHz帯のSSIDに変更することをオススメします。
実際、ブラウザの通信速度を計測する以下のサイトを利用して測ったところ、
2.4GHz : 30~50Mbps
5GHz: 150~200Mbps
と、大きな改善が見られました!
効果を実感するのはこれからですが、数値上は効果は明らか、と思います。
おわりに
以上のプロセス、私のMacBook環境ではものの5分程度で診断・改善が完了します。
私含む面倒くさがりなあなたも、少し重い腰を上げるだけで素敵なWeb会議ライフ?が送れるかもしれませんので、お試しあれ。
ウィズコロナ期のデータサイエンス周りのビジネス環境を推測してみる
GWが始まりましたね。ぼっち・インドアな私でもさすがに外に繰り出すか〜と思う期間ですが、まさかこんな形で始まるとは思いませんでした。
多くの皆さんは積読の解消や、Courseraなどオンラインの学習、はたまたデータサイエンスコンペに参加して腕を磨くなど、とても有為な過ごし方をされていると思いますが、
私は無為にポエムでも書いてみることにしました。
ポエムのテーマですが、昨今のパンデミックに伴い有効求人倍率は駄々下がり、特に飲食業界や旅行業界は連日その苦境が報じられていますが、翻って我々の属するデータサイエンス業界(そんな業界はないが。。。)はどうなのか、受注環境やビジネスオポチュニティーはどうなってしまうのか、現時点で私から見えている景色をもとに推測してみました。
注:後から読み返すと、端々で受託開発目線でビジネス環境を捉えていました。受託開発以外の仕事ももちろん多くあるので本意ではないのですが、おそらく受託開発目線が最も分かりやすく環境の良し悪しを捉えられるためかと思います。ご容赦ください。
結論
早速結論をまとめてしまうと、
短期的には仕事の量は減るが、長期的には増える。但しその仕事の担い手は今までとはかなり変わる
と推測しています。
データサイエンス界隈の仕事環境については、冒頭例に挙げた業界に比べれば、ポジティブな面もあると考えています。
ただそれは、経済取引の量の観点ではポジティブという意味で、仕事の担い手は結構変わり、新たなプレイヤーも出てくれば既存のプレイヤーが淘汰される、ある意味厳しい局面が訪れる気がしています。
仕事の絶対量と内訳という非常にざっくりした二軸で、もう少し語ってみます。
絶対量の話
まず仕事の絶対量ですが、テキトーなグラフで可視化すると、以下のように推移すると考えています。

短期的には落ち込むが、長期的には伸びるという見立てです。
短期的にネガティブな面
短期的にネガティブに見る事由ですが、データサイエンス界隈の仕事が
不要不急とみなされて、コストカットされる
と考えるためです。
データサイエンスは「それがなければビジネスが立ち行かない」といった類のものではないことも多く、企業からすればコストカットの対象になりやすいです。
もう少し解像度をあげると、ビジネスが好況であったり将来を見据える余裕があるような優良企業だけがデータサイエンスに関する取り組みを継続し、そうでない企業は一斉にひくことが予想されます。
またテーマの面でも、ROIがしっかり出ると見込まれる案件は継続されるが、そうではない取り組みは待ったがかかる可能性が高いと考えています。
ROIを出すなんて当然、むしろ淘汰が進んで良いじゃないかという声もあるかと思いますが、やる前からROIが出ると分かってる案件ばかりではないので(むしろそうではないものに価値があるケースも多々)、R&Dの色合いが濃く不確実性の高い案件は、結構な割合で止まってしまうのではないかと思います。
長期的にポジティブな面
一方で長期目線では、確実に仕事は増えるだろうと考えています。それは
コミュニケーションのオンライン化に伴い今まで止まっていた・検討もされていなかったデータ活用案件の動き出し
があると考えるためです。
多くのコミュニケーションがオンライン化されることで、今まで「その業務、データがあれば機械学習入れて効率化できるかもしれないんですけどね〜」などと言って話が終わったり、果てない人工データ作成の旅に出てそのまま帰ってこなかったりしたものが、In-Outのデータが蓄積されることで機械学習や、データドリブンの意思決定を導入できるチャンスが出てくると思っています。
ただまずはコミュニケーションや業務プロセスをデータ化するところから始まるので、実際に蓄積したデータ活用の取り組みが経済取引に転化するには少しラグがあると考えています。
そういう意味で長期目線にはなりますが、しかしデータサイエンティストの腕の見せ所は増えるはずです。
内訳の話
ただし、長期目線で増えるビジネス機会を捉えるプレイヤーは、これまでのプレイヤーとは変わってくるだろうとも見ています。
新たなプレイヤーが来るよという話と、既存のプレイヤーの勢力図も変わるよという話の2つがあります。
テキトーなグラフ第2段を示します。

新たに蓄積されたデータを料理できるプレイヤーが勢力を増す
分かりやすく具体例を示せば、今Zoom上にどんどんデータが溜まっています。ビジネスのコミュニケーションのみならずプライベートのコミュニケーションまで、彼らはデータ化できる環境にあります。
今は音声データ、チャットのテキストデータ、それらに紐づくメタデータの状態ですが、例えば営業がクライアントに対してどういう営業をしているか、またそれをどう上司に報告しているかといったデータが蓄積され、今までSalesforce上に乗っていた以上にリッチなデータが溜まっています。
そういった新しいデータを活用したサービスを提供できるプレイヤーの一番手は誰かと言えば、やはりデータホルダー(この場合Zoom)です。
オンラインのコミュニケーションをデータレイヤーで抑えたプレイヤーが、データサイエンス界隈でも新たなプレイヤーとして伸びてくると考えています。
オンラインでも認識できる無形の信用を構築したプレイヤーが生き残り、そうでないプレイヤーは淘汰される
データサイエンス界隈の仕事のように、価格競争に陥りづらい高付加価値サービスの発注意思決定のロジックは、「サービスの品質が良い」か「信用が置ける」ところに発注する、のいずれかがメインと思います。
オフラインのコミュニケーションが容易でない中では、特に後者の信用を構築する方法が限定されたことが大きな変化です。
信用を構築するには、「客観的・定量的な指標(技術力の高さを示す論文数、コンペなどオープン競争での実績、案件実績、企業としての歴史など)で自分たちが信用できることを示す」か「人間関係を作る中で自分たちが信用できることを示す」の二択だと私は考えていますが、特に今後は後者の活動が難しくなるのではないでしょうか。
そのような中で割りを食うのは、よく言われていますが「他を圧倒するような技術力があるわけでもなく、長年の実績があるわけでもない中小企業・フリーランス」だと思っています。
今後は、他との違いを示せない中小企業やフリーランスは仕事の機会が減り、その分の仕事の機会が増え人材不足に悩むことになる大手企業に、人材が流出していくのではないか、と考えています。
おわりに
パンデミックがここまで大事になるとは誰も予測できていなかった(少なくとも事前にアクションは取れていなかった)ように、上記推測も後から振り返れば愚文と成り果てるのかもしれません。
しかし、先を見通すトライを重ねることには価値があると思い、大事な?休日をポエムの時間に割いてみました。
ここまでお読みいただけた方々、是非皆様の見解をお聞かせいただけると嬉しいです。
私もこの辺りの見立ては日々アップデートして、また考えがまとまったときに投下したいと思います。
では皆様、良き休日を。
AtCoder Beginner Contest 164 D 数学よく分からないマンが解説を加えてみる
もはやAtCoder Mathematical Contestだという声の上がっている表題の問題について、
以下かつっぱさんの解説が華麗すぎてすぐには分からなかったので、数学がよく分からない人でもなんとなくわかった気になれるよう、解説を加えてみます。
考え方
文字列Sが"60574038"だったとします。
このとき、1文字目から4文字目の"6057"が割り切れるかを考えてみます。
Aを右端から"6057"の左端までの値、Bを右端から"6057"の右端手前までの値と考えます。つまり
A = 60574038
B = 4038
です。
このとき、AからBを取り除いたA-B(6057)が2019で割り切れるかどうかは
(A-B) / (10**4) % 2019 = 0
と表現できますが、ここで10**nと2019は互いに素なので、上の式は
(A-B) % 2019 = 0
と等価です。これは
A % 2019 = B % 2019
と等価です。つまり、2019で割った時のAとBの余りが同じなら、A-Bは2019で割り切れます。
このような(A,B)の組み合わせの数を計算すれば良いので、
余りが同じAやBがn個あったとき、その組み合わせのnC2、つまりn*(n-1)/2を計算すれば良いことになります。
従って本問題は、
まず余りの値ごとに、Sの中で余りが同じになる値の数を数え(余りが0の値が何個、余りが1の値が何個、、、)、
次に余りの値ごとに、組み合わせの数を計算し、合計した値が答え、
となります。
コード
コードは以下の通り。かつっぱさんのコード丸パクリですが、コメントで説明を加えます。
s = input()[::-1] # 右からn桁の数についてループを回していきたいので、文字列を反転させる sum_of_digits = 0 # 余りの値 cnts = [0] * 2019 # 余りの値ごとに余りが同じ値の数を格納する変数。2019で割る場合、余りは0~2018までの2019通りあるので、リストの長さは2019 cnts[0] = 1 # ※1 d = 1 # sum_of_digitsの計算に使う10の累乗値 # まず余りの値ごとに余りが同じ値の数を数える for c in s: sum_of_digits += int(c) * d sum_of_digits %= 2019 d *= 10 d %= 2019 # ※2 cnts[sum_of_digits] += 1 # 次に余りの値ごとにその組み合わせを計算し、足し合わせる ans = 0 for cnt in cnts: ans += cnt * (cnt - 1) // 2 print(ans)
※1
Bとしては0桁の値をとれることに注意します。例えば
A = 4038
B =
A-B = 4038
というケースです。
for c in sとやるとcは1桁の値から始まります。
そこで0桁の値の余りはfor文の前に計算しておきます。0桁の値を2019で割った余りは0なので、cnts[0]に1を代入します。
※2
d %= 2019で計算時間を抑えます。
この処理をしても結果に影響がないのは、直感的には以下で理解できます。
2020 * 10000 % 2019 = 2
2020 * 1924 % 2019 = 2 (10000 % 2019 = 1924)


.png?generation=1596588070622865&alt=media)
