【Aidamy】 ディープラーニングで手書き文字を識別してみよう③

前回に引き続き、Aidamyの手書き文字認識コースをやっていく。
今回はコース4:データクレンジング。

データクレンジング

機械学習モデルにデータを読みこませるために、データの欠損値や画像の前処理などを行う手法のこと。

lambdaやmapなどの便利なPython記法

無名関数lambda

  • lambda '引数': '返り値'で関数を作成することができる。
  • 引数を取らず'Hello'と出力するだけの関数などは表現することができない。
# func1とfunc2は同じ
func1 = lambda x: x ** 2 + x + 3

def func2(x):
    return x ** 2 + x + 3

# func3とfunc4は同じ
# 引数は複数取れる
func3 = lambda x, y, z: x * y + z

def func4(x, y, z):
    return x * y + z

# func5とfunc6は同じ
# ifによる条件分岐を一行でかける
func5 = lambda x: x ** 2 + 3 if 0 <= x < 10 else x

def func6(x):
    if 0 <= x < 10:
        return x ** 2 + 3
    else:
        return x

# 以下のように関数名をつけなくても実行できる
(lambda x, y: x ** 2 + 3 * y)(3, 8)

listの分割

test_sentence = "this,is a.test sentence"

# 通常のsplit
test_sentence.split(' ')         #=> ["this,is" "a.test" "sentence"]

# 複数の記号で分割したい時
import re
re.split('[, .]', test_sentence) #=> ["this" "is" "a" "test" "sentence"]

高階関数map

  • 他の関数を引数と取る関数を高階関数と呼ぶ
  • map(適用したい関数, 配列)イテレータを返す
  • リストにしたいときはlist(map(関数, 配列))
  • forループより実行時間が短い
lst = [1, -2, -3, 4, -5]

list(map(abs, lst))
#=> [1 2 3 4 5]

filter関数

  • filter(条件となる関数, 配列)でlistの各要素から条件を満たす要素だけを取り出す
  • 条件となる関数は入力に対してTrue/Falseを返す関数
lst = [1, -2, -3, 4, -5]

list(map(lambda x: x > 0, lst))
#=> [1 4]

sorted関数

  • sort関数よりも自由度の高いソートができる
  • sorted(配列, key=キーとなる関数, reverse=False)
nest_list = [
    [0, 9],
    [1, 8],
    [2, 7],
    [3, 6],
    [4, 5]
]

# 第二要素をキーとしてソート
sorted(nest_list, key=lambda x: x[1])
#=> [[4, 5], [3, 6], [2, 7], [1, 8], [0, 9]]

内包表記

  • イテレータを作成するときはmapfilter、リストを作成するときは内包表記
[abs(x) for x in lst] # list(map(abs, lst))

[x for x in lst if x > 0] # list(filter(lambda x: x > 0, lst))
  • 多重ループも内包表記で表現することができる
xy_list = [[x, y] for x in x_list for y in y_list]

# 以下と同じ
xy_list = []
for x in x_list:
    for y in y_list:
        xy_list.append([x, y])

defaultdict

  • 通常のdict型と同じ様に使える
  • 辞書のデフォルト値を決めておくことができる
  • デフォルトをintに
from collections import defaultdict

lst = ['a', 'b', 'b', 'a', 'b', 'c', 'd']

d = defaultdict(int)
for key in lst:
    d[key] += 1

print(d)
#=> defaultdict(<class 'int'>, {'a': 2, 'b': 3, 'c': 1, 'd': 1})

# キーでソート
sorted(d.items())
#=> [('a', 2), ('b', 3), ('c', 1), ('d', 1)]

# 値で降順ソート
sorted(d.items(), key=lambda x:x[1], reverse=True)
#=> [('b', 3), ('a', 2), ('c', 1), ('d', 1)]
  • デフォルトをlistに
from collections import defaultdict
# まとめたいデータprice...(名前, 値段)
price = [
    ("apple", 50),
    ("banana", 120),
    ("grape", 500),
    ("apple", 70),
    ("lemon", 150),
    ("grape", 1000)
]

d = defaultdict(list)

for k, v in price:
    d[k].append(v)

print(d)
print([sum(v) / len(v) for v in d.values()])
# 出力
#=> defaultdict(<class 'list'>, {'apple': [50, 70], 'banana': [120], 'grape': [500, 1000], 'lemon': [150]})
#=> [60.0, 120.0, 750.0, 150.0]

Counter

  • 要素の数え上げに特化したクラス
from collections import Counter

lst = ['a', 'b', 'b', 'a', 'b', 'c', 'd']

d = Counter(lst)

print(d)
#=> Counter({'b': 3, 'a': 2, 'c': 1, 'd': 1})

# ソートして上位を出力
print(d.most_common(2)
#=> [('b', 3), ('a', 2)]

DataFrameを用いたデータクレンジング

Pandas

import pandas as pd

# アヤメデータを取得
iris_df = pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data', header=None)

# カラム名を指定
iris_df.columns = ["sepal length", "sepal width", "petal length", "petal width", "class"]

# 2つのデータを'ID'列でソートし、行番号を振りなおして連結
df1.append(df2).sort_values(by='ID').reset_index(drop=True)

# リストワイズ削除
# データ欠損のある行をまるごと消去
df.dropna()

# ペアワイズ削除
# データ欠損値の少ない列をだけを残す
df[[0, 2]].dropna()

# データの欠損値を0埋め
df.fillna(0)

# データの欠損値を前の値で埋める
df.fillna(method='ffill')

# データの欠損値をデータの平均値で埋める(平均値代入法)
df.fillna(df.mean())

# 重複のある行にTrueと表示
df.duplicated()

# 重複データ削除後のデータを表示
df.drop_duplicates()

# マッピング
# 共通のキーとなるデータに対してテーブルからそのキーに対応するデータを持ってくる
area_map ={"Tokyo":"Kanto"
          ,"Hokkaido":"Hokkaido"
          ,"Osaka":"Kansai"
          ,"Kyoto":"Kansai"}
city_df['region'] = city_df['city'].map(area_map)
#=> 新たに'region'列ができてそれぞれの地域が追加される

# bin分割
df_cut = pd.cut(df, bin_num)
# 各ビンの数を集計
pd.value_counts(df_cut)

CSV

import csv

# csvファイルの作成
with open('students.csv', 'w') as f:
    writer = csv.writer(f, lineterminator='\f')
    writer.writerow(['name', 'age', 'school'])
    writer.writerow(['Taro', 18, 'Handa'])
    writer.writerow(['Hanako', 16, 'Taketoyo'])
    writer.writerow(['Toranosuke', 28, 'Azabu'])
    writer.writerow(['Bakabon', 102, 'Kaisei'])
    writer.writerow(['Korosuke', 2, 'Asahigaoka'])

OpneCVの利用と画像データの前処理

RGBデータ

  • 画像はピクセルと呼ばれる小さな粒の集まりで表現される。
  • それぞれのピクセルの色を変えて画像を表現。
  • カラー画像はRed, Green, Blue(RGB)で表現。
  • 三色の明るさは多くの場合0~255(8bit)の数値
  • (255, 0, 0)は赤、(0, 0, 0)は黒、(255, 255, 255)は白
  • OpenCVでは一つのピクセルを表すための要素の数をチャンネル数と呼ぶ
    • RGB画像はチャンネル数3
    • モノクロ画像はチャンネル数1

画像データのフォーマット

OpenCV

  • 画像を扱うのに便利なライブラリ
  • インポート
import numpy as np
import cv2
  • 画像の作成と保存
# 画像の作成、保存
# 画像サイズ
img_size = (512, 512)

# 緑色の画像
my_img = np.array([[[0, 255, 0] for _ in range(img_size[1])] for _ in range(img_size[0])])

# 画像の保存
cv2.imwrite('my_green_img.jpg', my_img)

f:id:hanhan39:20180221141011j:plain
my_green_img.jpg

  • 画像の読み込みと表示
# 画像の読み込み
img = cv2.imread('./sample.jpg')

# 画像の表示
# sampleはウィンドウの名前
cv2.imshow('sample', img)

f:id:hanhan39:20180221141110j:plain
sample.jpg

  • トリミングとリサイズ
size = img.shape

# 行列の一部を取り出してくればトリミングできる
trimed_img = img[:size[0] // 2, :size[1] // 3]

# リサイズ
resized_img = cv2.resize(trimed_img, (trimed_img.shpe[1] * 2, trimed_img.shape[0] * 2))

f:id:hanhan39:20180221141125j:plain
trimed_img.jpg

f:id:hanhan39:20180221141130j:plain
resized_img.jpg

  • 回転・反転
img = cv2.imread("./sample.jpg")

# warpAffine()を用いるのに必要な行列を作成
# 第一引数:回転の中心(今回は画像の中心)
# 第二引数:回転角度(今回は180度)
# 第三引数:倍率(今回は2倍に拡大)
mat = cv2.getRotationMatrix2D(tuple(np.array(img.shape[:2]) / 2), 180, 2.0)

# アフィン変換
# 第一引数:変換したい画像
# 第二引数:上で生成した行列(mat)
# 第三引数:サイズ
affine_img = cv2.warpAffine(img, mat, img.shape[:2])

# 反転
# 第二引数:反転の軸(0: x軸 / 正: y軸 / 負: xy両方)
fliped_img = cv2.flip(img, 0)

f:id:hanhan39:20180221142625j:plain
affine_img.jpg

f:id:hanhan39:20180221142632j:plain
fliped_img.jpg

  • 色調変換・色反転
    • RGB -> Lab色空間
    • Lab色空間は人間の視覚に近似するよう設計されている
# Lab色空間に変換
lab_img = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)

# モノクロ画像に変換
gray_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)

# 画像の色を反転
inverted_img = cv2.bitwise_not(img)

f:id:hanhan39:20180221143505j:plain
lab_img.jpg

f:id:hanhan39:20180221143528j:plain
gray_img.jpg

f:id:hanhan39:20180221143532j:plain
inverted_img.jpg

OpenCVの利用

閾値処理(二値化)
  • 画像の容量を小さくするために、一定以上明るいもの、あるいは一定以上暗いものをすべて同じ値にする処理
  • cv2.threshold()で実現可能
  • 第一引数:処理する画像
  • 第二引数:しきい値
  • 第三引数:最大値
  • 第四引数:二値化の仕方
    • THRESH_BINARYしきい値よりも大きい値は最大値に、そうでないものは0
    • THRESH_BINARY_INVTHRESH_BINARYの逆
    • THRESH_TRUNCしきい値より大きい値しきい値に、そうでないものはそのまま
    • THRESH_TOZEROしきい値より大きい値はそのまま、小さい値は0
threshold, threshold_img = cv2.threshold(img, 75, 255, cv2.THRESH_BINARY)

f:id:hanhan39:20180221220953j:plain
threshold_img.jpg

マスキング
  • 画像の一部分のみをとりだす
  • 白黒でチャンネル数1のマスク用画像を容易
  • ある画像のマスク用画像の白い部分と同じ部分だけ抽出
mask = cv2.imread('mask.png', 0) # 第二引数を0にするとチャンネル数1に変換して読み込み

mask = cv2.resize(mask, (img.shape[1], img.shape[0]))

masked_img = cv2.bitwise_and(img, img, mask = mask)

f:id:hanhan39:20180221225352p:plain
mask.png

f:id:hanhan39:20180221225407j:plain
masked_img.jpg

ぼかし
  • あるピクセルの周りのn×n個のピクセルとの平均を取る
  • GaussianBlurを使用
  • 第一引数:元画像
  • 第二引数:n×nの値を指定(nは奇数)
  • 第三引数:x軸方向の偏差(通常は0)
 blur_img = cv2.GaussianBlur(img, (21, 21), 0)

f:id:hanhan39:20180221235027j:plain
blur_img.jpg

ノイズの除去
  • cv2.fastNlMeansDenoisingColord()を使う
膨張・収縮
  • 主に2値画像で行われる
  • あるピクセルを中心とし、フィルタ内の最大値をその中心の値にすることを膨張、最小値をその中心の値にすることを収縮という
  • フィルタは、中心のピクセルの上下4つを用いる方法と、自信を囲む8つを用いる方法の2通りが主。
  • uint8は8ビットで表された符号なしの整数
filt = np.array([[0, 1, 0],
                 [1, 0, 1],
                 [0, 1, 0]], np.uint8)

# 膨張
dilated_img = cv2.dilate(img, filt)
# 収縮
eroded_img = cv2.erode(img, filt)

f:id:hanhan39:20180221235258j:plain
dilated_img.jpg

f:id:hanhan39:20180221235357j:plain
eroded_img.jpg

長かった、、、。