はじめに
この記事は「プログラミング技術の変化で得られた知見・苦労話【PR】パソナテック Advent Calendar 2020」のために記載したものです。
なんかの拍子にQuitaのこの広告目に入って、そういや随分昔からプログラムやってるなぁ、と思い出し、
自分の知識整理かねて記事書いてみよう、と思い立った次第です。
ネタとしては表題の通り、20年Cをやっていたものの最近はほぼPythonしか書かなくなってしまった、という話ですが
なんでそうなってしまったかを自分の経験とか使い方とか踏まえて考察してみようと思います。
決して商品のM1Macにつられたわけではありません。いや興味はすっごくあるんですが。つか欲しいです...!
軽く自己紹介
まずお前誰やねん、というとこが分からないと苦労話もできませんので属性を書いておきます。
まじめにプログラムやったのは大学生以降ですね。
ちょくちょく他言語に触れながらも基本はC/C++で20年超です。
Pythonはここ3年ほど使ってますが最近はC/C++で書くことがほとんどなくなりPythonばっかりという状況です。
- 大学の講義でCに触れ、K&Rで学ぶ(~22歳)
- 大学院まで進んだが研究で使ってたのはVisualBasic(手っ取り早くGUI書きたかったので。~25歳)
- 社会人になり家電メーカーにて組み込みLinux家電とかを開発
- Linuxドライバ開発やカーネル弄りを担当してどっぷりC言語につかる(~30歳)
- その後itronとかipl※開発とかも手掛けるが相変わらずC言語(~3x歳)
- android開発を始めたのでちょっとJavaとかに手を出すも、android NDKやれと言われ再びC/C++に出戻り(~3x歳)
- 画像系処理の仕事に転向するも、主力はC/C++版openCV(~4x歳)
- 画像系は今後はAIやらんと駄目ねと思い立つ(3年ほど前)
- 現在4x歳のおっさん
Pythonなれそめ
画像系処理といっても家電メーカーなので、写真を綺麗にとれるようにちょっとした機能を入れましょう、程度の実装で済んでいましたが
ここ数年で「わが社もAIで新たな機能を開発すべし」みたいな声があちこちから降ってきました。
で、技術担当としてはAIやらねば、となったはいいものの、開発言語としてはほぼPythonしか選択肢がないわけです。
(一応C++とかもあったにはあったけど)
えー、言ってもインタプリタ言語でしょ?ちゃんと速度出るの?みたいな感じで仕方なくはじめてみた感じだったと思います。
画像系だととにかく速度が問題になることが多く(※)、より機械に近いC言語のようなプログラム言語を使って無駄をなくすというのがそれまでの自分内常識でした。
ただ、それまでの経験から「ユーザー数の少ないプラットフォームは情報の少なさで割を食う」ことは見えていたので、まずは一番メジャーなPython版で試さない事にはC++とかも難しかろう、と触ってみたのが最初だったはずです。
※例えば動画をカクつかせずに画像フィルタをかけろ、とか
Pythonおっそい
で、とりあえず画像処理とかやってみるわけです。
本当は業務に沿った処理内容になるのですが、ここでは適当なサンプルということで画素のRGBそれぞれの平均を求めてみる処理を書いてみます。
C++版のOpenCVはそれまでも使ってたので、同じように…
import cv2
import nump as np
def calc_img_mean(img):
bgr = np.zeros((3))
hwc = img.shape
for y in range(hwc[0]):
for x in range(hwc[1]):
bgr += img[y, x]
return bgr / (hwc[0] * hwc[1])
こっちはC++実装.
#include <opencv2/opencv.hpp>
cv::Vec3f calc_img_mean(const cv::Mat *img)
{
cv::Vec3f rgb_sum(0,0,0);
for(int y = 0;y < img->size().height;y++){
for(int x = 0;x < img->size().width;x++){
rgb_sum += (img->ptr<cv::Vec3b>(y))[x];
}
}
rgb_sum /= img->size().width * img->size().height;
return rgb_sum;
}
4000x3000くらいの画像に対して手元のPCで実行してみると、それぞれの実行時間は以下の通りになりました。
実装 | 実行時間(s) |
---|---|
C++ | 0.03 |
Python | 34.4 |
うぉぉぉい!遅すぎて変な声出ました。本当に1000倍遅い。なんぼ何でも遅すぎません…?
numpyが全てを解決する
で、世の中の人はどんな感じで書いてるんだ?と見たところ
img_mean = np.mean(img, axis=(0,1))
え、これでいいの?ってな感じでした。まずopencvで読み込んでるのにnumpyが出てきてるのに混乱。確かにcv::Matと概念似てるみたいですが..
で、実行時間は..346msec! 一気に100倍アップです。30秒だとどうにもなりませんが0.3秒なら工夫すりゃ何とかなりそうです。
いやいや、C++だってcv::mean()あるじゃん、と思われるでしょうが、C++ではここまで劇的には変わりません。というかこういうAPIがあることを後で知りました。
C/C++だと自分で書いてもそれなりに速度出るせいで, 機能探すより自分で書いちゃうんですよね...
実装 | 実行時間(s) |
---|---|
C++(自前ループ) | 0.03 |
C++(cv::mean()) | 0.015 |
Python(自前ループ) | 34.4 |
C++(np.mean()) | 0.35 |
それに本当にやりたいのは「画像の平均」みたいな定型処理じゃなく、用途ごとに色々ありますからね。
色々って何よ。うーん、じゃあBayer配列のRGB化とか...
def raw2rgb(raw):
h, w = raw.shape
rgb = np.empty((h//2,w//2,3), dtype=np.uint8)
rgb[...,0] = raw[1::2,1::2]
rgb[...,1] = (raw[1::2,::2] + raw[::2,1::2])//2
rgb[...,2] = raw[::2,::2]
return rgb
同じようにC++実装。
cv::Mat raw2rgb(const cv::Mat *raw)
{
cv::Mat rgb = cv::Mat_<cv::Vec3b>(
raw->size().height/2,
raw->size().width/2);
unsigned r, g, b;
for(int y=0;y < raw->size().height/2;y++){
for(int x=0;x < raw->size().width;x++){
r = (raw->ptr(y*2+1))[x*2+1];
b = (raw->ptr(y*2))[x*2];
g = ((raw->ptr(y*2+1))[x*2] + (raw->ptr(y*2))[x*2+1]) / 2;
cv::Vec3b rgb_value(r,g,b);
(rgb.ptr<cv::Vec3b>(y))[x] = rgb_value;
}
}
return rgb;
}
8000x6000サイズの配列を与えた時の処理時間は以下の通り。
実装(raw2rgb) | 実行時間(s) |
---|---|
C++(自前ループ) | 0.12 |
Python(numpy) | 0.13 |
なんと、C++自前ループとほぼ同じ速度が出てきてしまいました。そしてコードのシンプルさが凄い。
C++版のOpenCV使う時には型チェックのコンパイルエラーに悩まされてたりしてたのが、間違えようがない位にシンプルです。
Pythonいいじゃん
これで図にのり、当初は「C++実装のCaffeeとか使うか…?」とか考えてましたがPythonでAI進めることに。
で、これでいろいろ実装していくとC/C++と比べたPythonの利点がいろいろと。個人的にこれは便利と思った点を書いていきます。
numpyは神
最初のころは「numpyがPythonの本体なのでは...」とか失礼なことを考えるくらいでした。本当、画像処理にnumpyはこれ以上考えられない位の組み合わせです。
cv::Matと似ているとは言え, あっちはAPIによって型とかがいろいろ定義されててキャストだらけになりがち(※1)なのが, とにかくシンプルに書けます。
例えばOpenCVには画像を反転するflip()という関数があるのですが、AIフレームワークの中では対応する関数がないものもあります。
で、どうするんだと海外の掲示板とか見てみると
Q. 画像反転したいんだけどAPIないよ
A. img[::,::-1,...] ※2
みたいなことが書かれていて目から鱗でした。ちょっとした画像の変形とか解析ならnumpyだけでさくっとできてしまいます。
※1 筆者のopenCV知識不足によるものの可能性があります。とはいえnumpyだと知識いらずという点で優れているかと。
※2 numpy(および類似のAIフレームワークtensor)での「第2軸を逆順に並べる」という処理
辞書(dict)型がすごく便利
キーとオブジェクト参照を入れる辞書型が一つあるだけで, C/C++で構造体に求めていた役割はほぼすべて代替されているような印象です。
とりあえず辞書作って、ある関数の出力をキーAに、別の出力をキーBに..と突っ込んでいって、受ける側では自分の必要な情報だけ参照する、みたいなことができます。
これがC/C++だと構造体を定義しなおしたり関数引数型を変えたりと、処理途中に関数を追加しようと思うととにかく手続きが多い。その分チェックが厳密になるという点はありますが、そのためコードが増えたら意味ないと思うんですよね。コードが増えた分1行当たりにかけられる注意は減っていくわけで。
良くも悪くも実行時評価
関数を書いた時点では構文チェック程度しか行われておらず、引数が何者であるかなどは考慮されません。
なので次のような関数定義もできちゃいます。hello()メソッドがあるクラスなら何でも受け付けるわけですね。
で、その後で新しいクラスを作った時や人の作ったクラス流用する場合にもhello()メソッドさえ追加してしまえばfunc()に突っ込める訳です。めっちゃ便利。
これをC/C++でやろうとすると皆に共通の基底クラス作るなりして関数の引数型を宣言して..と段取りが大変です。
いやそういう大規模設計が必要なところもまだあるんでしょうが、今日日何か月もかけて大規模開発するってのは流行らない気がします。
def func(cls_inst):
cls_inst.hello()
class A(object):
def hello(self):
print("I am class A")
たまに困ることもあるけど
生データ操作には限界もある
ctypesとかstructとか使うことでバイナリデータの読み書きなんかもかなり行けるんですが、バイト境界を跨いだビットフィールド、みたいなデータの扱いは難しいですね。というかこんな部分はC/C++の出番でしょう。Pythonでやる需要もなさそうだし。
富豪プログラミングになりがち
Pythonで書いた関数をCに戻そうとすると, 膨大な自動変数を確保してるのに気づかされたりします。C++だと割と同じようなことできるけど、とにかくメモリを贅沢に使った富豪プログラミングになりがちです。
まぁAI使うようなところだと実行環境も富豪だったりするのでそれほど問題にならないんですが、組み込みCに画像処理を移植、なんてときは大変ですね。
名前の上書きに気づきにくい
変数宣言がなく、名前になんでも突っ込めるせいで気づかないうちに「モジュール名を別のオブジェクトで上書き→挙動がおかしくなる」みたいなことが起きます。
pycocotools.maskとかtorch.optimみたいな「自分でも変数として使いがちな名前」が危ない。
さすがに下みたいに直近に書くのは気づきますが、Jupyter noteとかで書いてると「1つ前のセルに戻ったら、さっき動いたコードが動かない」みたいなことをやってしまいます。
from pycocotools import mask
....
# アノテーション画像をCOCO辞書に変換
ann_img = np.array(Image.open(xxx))
mask = ann_img == clsid
mask.encode(mask) #エラー, maskは既にモジュールじゃなくてndarray
で、何故Pythonでしか書かなくなってしまったのか
私的には「プログラムに求められる事が変わった」ためだと思います。
昔書いていたころは
- とにかく速度が正義
- メモリも節約すべし
- 開発はじっくり時間かけてもいいよ
- 求められる機能はそんなに多くないよ
だったのですが、今は速度とメモリが最優先される環境はかなり減りました。
もちろんそうはいっても冒頭のループみたいな部分が問題になって、20年来C/C++から乗り換え進まなかったのですが、Pythonだとnumpyがほぼ解決してくれるのが大きいですね。
加えて、開発スタイルが大きく変わりました。
とにかく開発速度優先、できるかできないかをまず検討すべし、みたいな開発要求が増えています。
C/C++で書いてると開発速度がどうしても遅くなってしまうんですよね。コード量の問題もありますし、コンパイルとコーディングを行き来するのは思っている以上に開発速度を落としているんだな、というのもPythonを使って思いました。
できたコードを周囲に展開するのもC/C++だと開発環境だのライブラリだので一苦労ですしね。
以上、プログラミング技術の変化で得られた知見・苦労話でした。
記事を書いてみて
久々にC++書いたのでコード至らない点はご容赦ください(言い訳)。つか書いてないと忘れるものですね...