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