はじめまして
Letro事業部 KAIZENチームのteraです。
今年からアライドに入社して、8月まではSREチームで障害監視にかかわる開発をしていましたが、9月からはKAIZENチームで生産性向上のための開発や、テクニカルサポート対応などを行っています。
アライドに入社するまでのことをちょっと振り返ると、ほとんどがPHPでのバックエンド開発で、分業がはっきりしてない開発チームでは、フロントエンド兼ねる場合に、出来る範囲でJquery実装だったり、JSだったりを書いたりと、あまりモダンな開発には触れては来なかったんですね。それこそネイティブのスマホアプリなども経験しましたが、Objective-Cで止まってます。
Python
自分が実装しなかったものの、開発業務でPythonに触れる機会があり、簡単でもいいから少しはPythonを使ったなにかくらい動かしてみたい衝動が生まれてきたのでした。
初めてPythonに触れたのって確か2007年とかだったのですが、その頃はまだ今のように様々なライブラリ郡が整備されているわけでもなく、AIブームもなく、語弊はあるかもしれませんが、Webサービスを構成する言語の、少ない場合選択肢の一つという位置づけだったように思います。
それが今ではAIブームで、PythonはAIのための言語というイメージが強くなっているように思います。
画像検索
学生時代、卒業研究では画像検索の研究をして論文を書きました。画像処理のコアな部分は研究室の先生が用意していたライブラリがあったので、それをどう活用するか。各自内容を考えて、花の類似画像をどこまで分類できるかというテーマを立ち上げました。2002年、今から22年前の話です。ググるという言葉が生まれたばっかりな頃。そういう時代です。
TensorFlow
さて、ようやく本題になってきた感がありますが、何十年前も前から何千、何万人もの学者たちが積み重ねてきた叡智が今や、当時を遥かに上回る精度で、様々な分野で活用出来てしまうのですね。TensorFlowは、言わずとしれた2015年にGoogleが開発した機械学習のソフトウェアライブラリですね。それ以上のことは詳しくないので、ここでは深掘りしません。
ここで本題
本題と言いつつ、さて、私teraはですね、
東京は上板橋に本店のある、某辛いラーメンチェーン店が大好きなんです。渋谷と目黒には店舗があるので、アライドが本社を構える恵比寿にも店舗出店して欲しいです。
で、この辛いラーメンチェーン店は、辛さにランクを設けており、0〜10の11ランクを基本としています。(限定メニュー以外)
そこで、TensorFlowを使って、与えたラーメンの写真の辛さを当てられるか!ってことをやってみようと思いました!
ということで、今回の判定に利用したPython のコードです。
これを、今回のために実装しました、というと、とてもそれっぽくなるんですが、今回はほぼChatGPTが実装してくださいました。それはそれですごいことなのですが。一部手直ししています。
1. ライブラリのインポート
| 
					 1 2 3 4 5  | 
						import os import numpy as np from keras.models import Sequential, load_model from keras.layers import Conv2D, MaxPooling2D, Activation, Dropout, Flatten, Dense from keras.preprocessing.image import ImageDataGenerator, load_img, img_to_array  | 
					
2. 辛さ分類器クラスの定義
| 
					 1  | 
						class KarairamenSpicinessClassifier:  | 
					
2.1 イニシャライザ(コンストラクタ)
| 
					 1 2 3 4 5 6 7 8 9  | 
						    def __init__(self, input_shape=(150, 150, 3), model_path="karairamen_model.h5"):         self.input_shape = input_shape         self.model_path = model_path         if os.path.exists(self.model_path):             self.model = load_model(self.model_path)             print(f"Loaded model from {self.model_path}")         else:             self.model = self._build_model()  | 
					
最新のフォーマットはh5ではくkerasとのことなのですが、対応しきれませんでした。
2.2 モデルの構築
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19  | 
						    def _build_model(self):         # モデルの構築         model = Sequential()         model.add(Conv2D(32, (3, 3), activation='relu', input_shape=self.input_shape))         model.add(MaxPooling2D(pool_size=(2, 2)))         model.add(Conv2D(32, (3, 3), activation='relu'))         model.add(MaxPooling2D(pool_size=(2, 2)))         model.add(Conv2D(64, (3, 3), activation='relu'))         model.add(MaxPooling2D(pool_size=(2, 2)))         model.add(Flatten())         model.add(Dense(64, activation='relu'))         model.add(Dropout(0.5))         model.add(Dense(11, activation='softmax'))         model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])         return model  | 
					
正直この細かいパラメータを理解仕切ってないので、後日調べたいと思ってます。
2.3 モデルの訓練
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21  | 
						    def train(self, train_dir, epochs=50, batch_size=32):         # モデルの訓練         train_datagen = ImageDataGenerator(             rescale=1. / 255,             shear_range=0.2,             zoom_range=0.2,             horizontal_flip=True         )         train_generator = train_datagen.flow_from_directory(             train_dir,             target_size=self.input_shape[:2],             batch_size=batch_size,             class_mode='categorical'         )         self.model.fit(train_generator, steps_per_epoch=2000 // batch_size, epochs=epochs)         # Save the model         self.model.save(self.model_path)         print(f"Model saved to {self.model_path}")  | 
					
2.4 新しい画像の辛さを予測
| 
					 1 2 3 4 5 6 7 8  | 
						    def predict(self, hantei_image):         # 新しい画像の辛さを予測         img = load_img(hantei_image, target_size=self.input_shape[:2])         x = img_to_array(img)         x = np.expand_dims(x, axis=0)         preds = self.model.predict(x)         return np.argmax(preds, axis=1)[0]  | 
					
3. メインの実行部
| 
					 1 2 3 4 5 6 7 8 9 10 11 12  | 
						if __name__ == '__main__':     # メイン関数     classifier = KarairamenSpicinessClassifier()     # モデルの訓練     classifier.train('train_data/')     # 新しい画像の辛さを予測     hantei_dir = 'hantei_dir/'     hantei_image = hantei_dir + 'hantei_image.jpg'     predicted_level = classifier.predict(hantei_image)     print(f"予測された辛さのレベル: {predicted_level}")  | 
					
以上が使用したコードです。
モデルの準備
辛さランクの学習用モデル画像を用意しないとなりません。以下のようにディレクトリを作成して配置しておく必要があります。画像のファイル名は自由です。
画像は、なるべく各ランクで枚数に偏りが無い方が、結果に偏りが無いようです。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21  | 
						train_data/ │ ├── 0/ │   ├── image1.jpg │   ├── image2.jpg │   ├── ... │ ├── 1/ │   ├── image1.jpg │   ├── image2.jpg │   ├── ... │ ├── 2/ │   ├── ... │ ├── ... │ └── 10/     ├── image1.jpg     ├── image2.jpg     ├── ...  | 
					
結果は?
このラーメン店には大きくラーメンとつけ麺メニューがありますが、今回はラーメンメニューのみを対象としました。
準備した学習用モデルですが、公式サイトのメニュー画像からラーメンメニューだけでは7枚だけで、明らかに学習データとして心もとないので、有名ファンサイトから拝借しあわせて合計48枚を辛さランクの学習用モデルとして使用することとしました。
(実際問題、48枚でも明らかに学習用データとして少なすぎると思います。)
そして、私が実際に食べに行った時に撮影した写真を判定用画像として使用します。
では、実際に判定してみました。
1枚目。お店の看板メニューですね。野菜増ししてます。公式の辛さは5です。
辛さ2と判定されました。やはり精度はいまいちなのでしょうか。
続いて、熱狂的なファンのいる、地球の北の方の寒いそうなとこみたいな名前のメニューですね。公式の辛さは9です。
あれ。これも辛さ2と判定されてしまいました。もしやこのプログラム、辛さ2しか判定しないのでは、、、
もう一枚、別の画像で、同じメニューに挑戦します。
うぉぉぉおおおおおおおおおおおおお!ピタリ賞ワーイ!!!!!!
何が違うのでしょうね…
そして次、個人的に一番好きなメニューです。五目味噌タンメンと言います。とても普通の名前のメニューです。野菜大盛りがオススメです。公式の辛さは8です。
辛さ6ですか。ちょっと惜しい。ちょっと惜しい。
さて、少し番外編。
アライドのオフショア開発拠点、アライドテックベースのあるハノイに本店があるフォーティンの、初めての海外分店、フォーティントーキョーのフォー。辛さはいくつに判定されるのでしょうか。
???
見なかったことにします。
結論
生成AIにしてもそうですが、重要なのはやはり学習データモデルなのではないかということです。
どんなにロジックが優秀でも、元となるデータが不足していたり、そもそもの前提が間違っていたりすると誤った結論に導かれてしまうという事をあらためて体験したということになりました。
また、このプログラムは実行する度に実行結果が可変してしまうということもあり、そのあたりの理由なども追ってみて、精度を上げてみたいなとも思ってます。
それでは、またあえる日まで。
2023年1月入社。Letro事業部SREを経てKAIZENチームで生産性向上の開発、テクニカルサポートを担当してます。
                
            


        




