
【機械学習】ブログのサムネ画像をクラスタリングしてみる!
2021.12.20
サムネイル画像をクラスタリングするお

(株)ライトコードの競馬サイエンティストこと、新田(にった)です!
何かをクラスタリングしたいと、ある日、ふと思いました。
既に100記事を超えた弊社のブログ。
じゃあ、記事のサムネ画像を自動で分類させてみるか…(暇つぶしに)
ということで、今回は、弊社ブログのサムネイル画像をクラスタリングするお。
クラスタリングとは
クラスタリングは、教師なし学習の一種です。
データを特徴によって、いくつかの塊に分けることができます。
例えば、以下のグラフを見て下さい
固まったデータに対して事前に何も正解ラベルを与えていなくても右のように分類して(クラスタに分けて)くれるのです。
【散布図1】
【散布図2】
今回は、弊社ブログのサムネイル画像に対して実行してみようと思います。
K-Means法
K-Means法は、シンプルなアルゴリズムです。
1. ランダムにクラスタを割り当てる
2. 以下を繰り返す
■A. 各クラスタの中心(=平均=means)を計算
■B. 各データと中心の距離を求めて最も近い中心のクラスタに再度割り当てる
クラスタの割り当てが変わらなかったり重心が変化しなかったりしたら終了。
データの距離を元にクラスタに分割していくことになります。
画像のnumpy配列をflatにしてそのまま特徴量とする
画像もRGBの数値なので、数値の配列を変形して学習してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | from glob import glob import shutil import cv2 import os from sklearn.cluster import KMeans import numpy as np # 画像をnumpy配列で読み込み、変形 impathlist = glob(IMAGE_DIR) features = np.array([cv2.resize(cv2.imread(p), (64, 64), cv2.INTER_CUBIC) for p in impathlist]) features = features.reshape(features.shape[0], -1) # モデルの作成 model = KMeans(n_clusters=10).fit(features) # クラスタ数を変更して試したいので古い出力結果は消す for i in range(model.n_clusters): cluster_dir = OUTPUT_DIR + "/cluster{}".format(i) if os.path.exists(cluster_dir): shutil.rmtree(cluster_dir) os.makedirs(cluster_dir) # 結果をクラスタごとにディレクトリに保存 for label, p in zip(model.labels_, impathlist): shutil.copyfile(p, OUTPUT_DIR + '/cluster{}/{}'.format(label, p.split('/')[-1])) |
最初に読み込んだ時点では、画像サイズやタテヨコ比はマチマチです。
そのため、一律で「64×64」のサイズに変換しています。
また、その時点での features.shape は、例えば (100, 64, 64, 3) だったとしたら、 (100, 12288) のように平坦な形へ変形しています。
そして、 n_clusters では、分けるクラスタの数を指定しています。
一応、「エルボー法」「シルエット分析」など最適なクラスタ数を決める手法はあるのですが、今回は n_clusters をいろいろ変えてみて試してみることにします。
クラスタ数を10にしてみた結果
上の画像は、縦にクラスタが並んでいる感じになります。
一番右端のクラスタは、キレイに簡易手書き数字認識アプリの記事の「前編」「後編」だけのクラスタとなっています。
また「第1回」「第2回」などのシリーズ物は全て同じクラスタ内には入っていますね。
少し面白いのは、左から5番目のクラスタです。
なんとなーく「左が暗い」「右が明るい」となっており、似ているといえば似ているような(?)気もします。
ボカしてみると...
たしかに。
実際、どのようなことになっているか?
先ほどのK-Means法の仕組みを思い出してみましょう。
K-Means法では、距離を元にクラスタに分割しています。
また、sklearnのKMeansは、距離の計算にユークリッド距離をつかっています。
画像の距離は、各ピクセルごとの二乗和の平方根で表されます。
簡単にするため白黒画像で説明すると、
画像Aと画像Bの距離は \(\sqrt{10}\) 、AとCの距離は \(\sqrt{5}\) となります。
一方で画像的な見た目は、どちらも縦線なのでAとBの方が近いように感じます。
とてもシンプルな画像なら良いのですが、それなりのカラー画像や写真に対しては、そのままK-Means法を行うのは少し無理があるように思います。
PCA
PCA(Principal Components Analysis: 主成分分析)は、多変数を少ない変数で表現し直す方法です。
今回の記事で、詳細は避けますが、軸を新たにとり直すため、先ほどの方法では全く同じ場所のピクセル同士を参照していたという点が緩和されます。
1 2 3 4 | from sklearn.decomposition.pca import PCA pca = PCA(n_components=22) components = pca.fit_transform(features) |
n_components は、実行後の次元数を指定します。
何次元に削減するかは、こちらで決める値となります。
1 | print("PCA累積寄与率: {}".format(sum(pca.explained_variance_ratio_))) |
累積寄与率は、各寄与率を足し合わせたもので、元のデータの何%を説明できているかを表します。
ここでは、累積寄与率70%となったので、 n_components=22 としましたが、もう少し落としてもいいかもしれません。
圧縮したcomponentsをK-Meansのinputとした結果
先ほどの結果と似てはいますが、シリーズ物がそれのみのクラスタとして分割されやすくなりました。
教師あり学習のデータチェックなどで(ほとんど同じような画像が含まれているケース)使えそうですね。
ただ、やりたいことは、ほとんど同じ画像をまとめたり検出したりすることではなく、クラスタリングなのでもう一歩進んだ結果が欲しいところです。
学習済みネットワークを特徴抽出器として使う
imagenetで学習済みネットワークは、100万枚を超える画像で1000種類のラベルに分類するよう学習しています。
今回は、比較的軽量な MobileNetV2 を使ってみます。
まずは、そのまま普通に予測してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | imlist = [] for p in impathlist: img = image.load_img(p, target_size=(128, 128), grayscale=is_gray) x = image.img_to_array(img) x = preprocess_input(x) imlist.append(x) imlist = np.array(imlist) # 学習済みモデルのロード・予測 mobilenet = applications.MobileNetV2(include_top=True, input_shape=(128, 128, 3), weights='imagenet') pred = mobilenet.predict(imlist) predict_result = decode_predictions(pred) |
predict_result には、予測結果の上位5件が入っています。
ここで、一部結果を見てみましょう。
この画像は?
1 2 3 4 5 | [('n06596364', 'comic_book', 0.8577052), ('n03000134', 'chainlink_fence', 0.010079569), ('n07248320', 'book_jacket', 0.00811937), ('n03598930', 'jigsaw_puzzle', 0.007505652), ('n03642806', 'laptop', 0.004030874)] |
「comicbook」と出ました。
確かにアメコミ調ですね。
この画像は?
1 2 3 4 5 | [('n09229709', 'bubble', 0.063828304), ('n01910747', 'jellyfish', 0.05993653), ('n03916031', 'perfume', 0.04591314), ('n03476991', 'hair_spray', 0.045898672), ('n04584207', 'wig', 0.038544197)] |
「泡」「クラゲ」「香水」「ヘアスプレー」「カツラ」どれも違うのですが、なんとなくわかりますね。
この画像は?
1 2 3 4 5 | [[('n03630383', 'lab_coat', 0.09676023), ('n03594734', 'jean', 0.09079775), ('n04350905', 'suit', 0.0736219), ('n03832673', 'notebook', 0.053408336), ('n03617480', 'kimono', 0.053135008)]] |
・・・「lab_coat(白衣)」。
うーん、微妙に違いますね。
でも、「jean」「 suit」は正解と言えます。
この画像は?
1 2 3 4 5 | [('n02776631', 'bakery', 0.45998523), ('n04443257', 'tobacco_shop', 0.03320225), ('n06596364', 'comic_book', 0.026826018), ('n04325704', 'stole', 0.026023453), ('n02667093', 'abaya', 0.023440091)] |
「パン屋」「タバコ屋」...。
分かるような分からないような...(笑)
学習済みネットワークから特徴を抽出
では、学習済みネットワークから特徴を抽出しましょう。
全結合層は、事前にラベルづけされた1000種類のラベルに分類を行なっているのでそこは必要ありません.
また、畳み込み最終層の出力は「ボトルネック特徴量」と呼ばれます.
1 2 3 | # 全結合層はいらないのでinclude_top=False model = applications.MobileNetV2(include_top=False, input_shape=[128, 128, 3], weights='imagenet') bottleneck_features = model.predict(imlist) |
いろいろパラメータなど変更の余地はあるのですが、試したことは、
- pcaの n_components
- pcaの代わりにt-SNEなどを使う
- KMeansの n_clusters
- 多層CNNは深い層ほど複雑な特徴を学習するため、抽象的な特徴に注目したい場合は終盤の畳み込み層も使わず、少し前の出力を使う。
- 自然の画像ではなくイラストやパターン、またはその組み合わせでは特に彩度が高いため、grayscaleに変換してみる.。ただし学習済みモデルはRGBで学習しているため、データの形はRGBの形式にする
- 学習済みネットワークを別のネットワークに切り替えてみる
などです。
結果
注目すべきは、左から4番目のクラスタです。
人の画像ばかり入っています!(一枚微妙に違うものが入っていますが)
学習済みモデルは、「人物」といったラベルでは学習していませんでしたが、人物に共通する特徴量から同じクラスタに分割することができたようです。
他にも
- 一番左のクラスタは矩形の多いもの
- 左から二番目のクラスタはコミック調
- 右から6番目のクラスタは画面
など、関連性のある組み合わせが出てきました。
おまけ
ブログサムネイルには、「データ自体にまとまりがあまりないこと」と、「イラストやコミックに対して学習済みネットワークはパフォーマンスを落としてしまう」ということもあります。
そのため、ブログサムネイルではなく普通の写真でも試してみることにしました。
普通の写真でも試してみる
「男性」「女性」「犬」「猫」「鳥」「魚」「車」飛行機」「船」
の10種類を、5枚ずつの写真を集めて実行したところ...
「女性」「船」「飛行機」「車」 は、狙い通りのクラスタになりました。
「犬」「猫」「花」「鳥」「魚」は、バリエーションも多く、こちらが意図した通りの(まるで教師あり学習のような)分類をクラスタリングで実現するには難しいですが、ある程度、似たような画像をまとめることはできました。
まとめ
- シンプルな画像に対するクラスタリングや、色味などに基づいてクラスタリングするにはそのままK-Means法でもよい。
- 写真のような画像には学習済みネットワークを使うと何が写っているかや質感にも焦点を当てたクラスタリングが可能。
- 決めるパラメータは多く、実行してみつつ調整してみる、を地道にやる必要がありそう。
こちらの記事もオススメ!
書いた人はこんな人

IT技術11月 20, 2023Airflowでよく使う便利なOperator一覧
IT技術11月 16, 2023AirflowでJinjaテンプレートを使ってSQLを実行する
IT技術5月 22, 2023【ISUCON部】ChatGPTとWebプログラミング
IT技術8月 20, 2021統計検定準1級(CBT)に合格した話