手書き認識エンジンを作成して App Store に出荷した方法
畳み込みニューラル ネットワークの構築から iOS への OCR の展開まで
プロジェクトの動機 ✍️ ??
数か月前に MNIST データセットの深層学習モデルを作成する方法を学んでいたときに、手書き文字を認識する iOS アプリを作成することになりました。
私の友人である百瀬嘉一さんは、日本語学習アプリ Nukon を開発していました。偶然にも、彼はそれに似た機能を持ちたいと思っていました。次に、数字認識エンジンよりも洗練されたもの、つまり日本語の文字 (ひらがなとカタカナ) 用の OCR (光学式文字認識/リーダー) を共同で構築しました。
Nukon の開発中、日本語の手書き認識に使用できる API はありませんでした。独自の OCR を構築するしかありませんでした。ゼロから構築することで得た最大のメリットは、オフラインで動作することでした。ユーザーはインターネットなしで山奥にいても、Nukon を開いて日本語学習の日常を維持できます。プロセス全体を通して多くのことを学びましたが、さらに重要なことは、ユーザーにより良い製品を出荷できることに興奮していたことです.
この記事では、iOS アプリ用の日本語 OCR を構築するプロセスを詳しく説明します。他の言語/記号用に作成したい場合は、データセットを変更して自由にカスタマイズしてください。
これ以上苦労することなく、カバーされる内容を見てみましょう:
パート 1️⃣:データセットを取得して画像を前処理する
パート 2️⃣:CNN (畳み込みニューラル ネットワーク) の構築とトレーニング
パート 3️⃣:トレーニング済みモデルを iOS に統合する
データセットを取得して画像を前処理しますか?
このデータセットは、手書きの文字と記号の 9 セットの画像を含む ETL Character Database から取得されます。ひらがなの OCR を作成するので、使用するデータセットは ETL8 です。
データベースから画像を取得するには、画像を読み取って .npz
に保存するヘルパー関数が必要です。
import struct
import numpy as np
from PIL import Image
sz_record = 8199
def read_record_ETL8G(f):
s = f.read(sz_record)
r = struct.unpack('>2H8sI4B4H2B30x8128s11x', s)
iF = Image.frombytes('F', (128, 127), r[14], 'bit', 4)
iL = iF.convert('L')
return r + (iL,)
def read_hiragana():
# Type of characters = 70, person = 160, y = 127, x = 128
ary = np.zeros([71, 160, 127, 128], dtype=np.uint8)
for j in range(1, 33):
filename = '../../ETL8G/ETL8G_{:02d}'.format(j)
with open(filename, 'rb') as f:
for id_dataset in range(5):
moji = 0
for i in range(956):
r = read_record_ETL8G(f)
if b'.HIRA' in r[2] or b'.WO.' in r[2]:
if not b'KAI' in r[2] and not b'HEI' in r[2]:
ary[moji, (j - 1) * 5 + id_dataset] = np.array(r[-1])
moji += 1
np.savez_compressed("hiragana.npz", ary)
hiragana.npz
になったら ファイルを読み込み、画像のサイズを 32x32 ピクセルに変更して、画像の処理を開始しましょう .また、データ拡張を追加して、回転およびズームされた追加の画像を生成します。私たちのモデルがさまざまな角度から文字の画像でトレーニングされると、モデルは人の手書きにうまく適応できるようになります。
import scipy.misc
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.models import Sequential
from keras.preprocessing.image import ImageDataGenerator
from keras.utils import np_utils
from sklearn.model_selection import train_test_split
# 71 characters
nb_classes = 71
# input image dimensions
img_rows, img_cols = 32, 32
ary = np.load("hiragana.npz")['arr_0'].reshape([-1, 127, 128]).astype(np.float32) / 15
X_train = np.zeros([nb_classes * 160, img_rows, img_cols], dtype=np.float32)
for i in range(nb_classes * 160):
X_train[i] = scipy.misc.imresize(ary[i], (img_rows, img_cols), mode='F')
y_train = np.repeat(np.arange(nb_classes), 160)
X_train, X_test, y_train, y_test = train_test_split(X_train, y_train, test_size=0.2)
# convert class vectors to categorical matrices
y_train = np_utils.to_categorical(y_train, nb_classes)
y_test = np_utils.to_categorical(y_test, nb_classes)
# data augmentation
datagen = ImageDataGenerator(rotation_range=15, zoom_range=0.20)
datagen.fit(X_train)
CNN の構築とトレーニング ?️
楽しい部分になりました! Keras を使用して、モデルの CNN (畳み込みニューラル ネットワーク) を構築します。モデルを最初に構築したとき、ハイパーパラメーターを試し、何度も調整しました。以下の組み合わせでは、98.77% という最高の精度が得られました。自分でさまざまなパラメーターを自由に試してみてください。
model = Sequential()
def model_6_layers():
model.add(Conv2D(32, 3, 3, input_shape=input_shape))
model.add(Activation('relu'))
model.add(Conv2D(32, 3, 3))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.5))
model.add(Conv2D(64, 3, 3))
model.add(Activation('relu'))
model.add(Conv2D(64, 3, 3))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.5))
model.add(Flatten())
model.add(Dense(256))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(nb_classes))
model.add(Activation('softmax'))
model_6_layers()
model.compile(loss='categorical_crossentropy',
optimizer='adam', metrics=['accuracy'])
model.fit_generator(datagen.flow(X_train, y_train, batch_size=16),
samples_per_epoch=X_train.shape[0],
nb_epoch=30, validation_data=(X_test, y_test))
モデルのパフォーマンスが不十分である場合のヒントを次に示します。 トレーニングステップで:
モデルは過適合です
これは、モデルが十分に一般化されていないことを意味します。直感的な説明については、この記事をご覧ください。
過剰適合を検出する方法 :acc
(精度) は上がり続けていますが、val_acc
(検証精度) は、トレーニング プロセスで逆のことを行います。
オーバーフィットの解決策 :正則化 (ドロップアウトなど)、データの増強、データセットの品質の向上
モデルが「学習」しているかどうかを知る方法
val_loss
の場合、モデルは学習していません (検証損失) は、トレーニングが進むにつれて増加するか減少しません。
TensorBoard を使用します — 時間の経過に伴うモデル パフォーマンスの視覚化を提供します。すべてのエポックを調べて値を常に比較するという面倒な作業から解放されます。
精度に満足しているので、重みとモデル構成をファイルとして保存する前に、ドロップアウト レイヤーを削除します。
for k in model.layers:
if type(k) is keras.layers.Dropout:
model.layers.remove(k)
model.save('hiraganaModel.h5')
iOS 部分に移る前に残っている唯一のタスクは、hiraganaModel.h5
の変換です。 CoreML モデルへ。
import coremltools
output_labels = [
'あ', 'い', 'う', 'え', 'お',
'か', 'く', 'こ', 'し', 'せ',
'た', 'つ', 'と', 'に', 'ね',
'は', 'ふ', 'ほ', 'み', 'め',
'や', 'ゆ', 'よ', 'ら', 'り',
'る', 'わ', 'が', 'げ', 'じ',
'ぞ', 'だ', 'ぢ', 'づ', 'で',
'ど', 'ば', 'び',
'ぶ', 'べ', 'ぼ', 'ぱ', 'ぴ',
'ぷ', 'ぺ', 'ぽ',
'き', 'け', 'さ', 'す', 'そ',
'ち', 'て', 'な', 'ぬ', 'の',
'ひ', 'へ', 'ま', 'む', 'も',
'れ', 'を', 'ぎ', 'ご', 'ず',
'ぜ', 'ん', 'ぐ', 'ざ', 'ろ']
scale = 1/255.
coreml_model = coremltools.converters.keras.convert('./hiraganaModel.h5',
input_names='image',
image_input_names='image',
output_names='output',
class_labels= output_labels,
image_scale=scale)
coreml_model.author = 'Your Name'
coreml_model.license = 'MIT'
coreml_model.short_description = 'Detect hiragana character from handwriting'
coreml_model.input_description['image'] = 'Grayscale image containing a handwritten character'
coreml_model.output_description['output'] = 'Output a character in hiragana'
coreml_model.save('hiraganaModel.mlmodel')
output_labels
後で iOS で表示される可能性のあるすべての出力です。
豆知識:日本語を理解している人なら、出力文字の順序がひらがなの「アルファベット順」と一致しないことを知っているかもしれません。 ETL8 の画像が「アルファベット順」ではないことに気付くのに時間がかかりました (これに気付いた Kaichi に感謝します)。データセットは日本の大学がまとめたものですが…?
トレーニング済みモデルを iOS に統合しますか?
いよいよすべてを組み立てます! hiraganaModel.mlmodel
をドラッグ アンド ドロップします。 Xcode プロジェクトに。次に、次のようなものが表示されます:
注意 :Xcode は、モデルのコピー時にワークスペースを作成します。コーディング環境をワークスペースに切り替える必要があります そうしないと、ML モデルは機能しません!
最終的な目標は、ひらがなモデルに画像を渡して文字を予測させることです。これを実現するために、ユーザーが書き込みできるようにシンプルな UI を作成し、ユーザーの書き込みを画像形式で保存します。最後に、画像のピクセル値を取得してモデルにフィードします。
順を追って説明しましょう:
<オール>UIView
に文字を「描く」 UIBezierPath
で import UIKit
class viewController: UIViewController {
@IBOutlet weak var canvas: UIView!
var path = UIBezierPath()
var startPoint = CGPoint()
var touchPoint = CGPoint()
override func viewDidLoad() {
super.viewDidLoad()
canvas.clipsToBounds = true
canvas.isMultipleTouchEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let point = touch?.location(in: canvas) {
startPoint = point
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let point = touch?.location(in: canvas) {
touchPoint = point
}
path.move(to: startPoint)
path.addLine(to: touchPoint)
startPoint = touchPoint
draw()
}
func draw() {
let strokeLayer = CAShapeLayer()
strokeLayer.fillColor = nil
strokeLayer.lineWidth = 8
strokeLayer.strokeColor = UIColor.orange.cgColor
strokeLayer.path = path.cgPath
canvas.layer.addSublayer(strokeLayer)
}
// clear the drawing in view
@IBAction func clearPressed(_ sender: UIButton) {
path.removeAllPoints()
canvas.layer.sublayers = nil
canvas.setNeedsDisplay()
}
}
strokeLayer.strokeColor
任意の色にすることができます。ただし、canvas
の背景色は 黒でなければなりません .トレーニング画像には白い背景と黒いストロークがありますが、ML モデルはこのスタイルの入力画像にうまく反応しません.
2. UIView
を回します UIImage
に CVPixelBuffer でピクセル値を取得します
拡張機能には、2 つのヘルパー関数があります。一緒に、これらは画像をピクセル値に相当するピクセル バッファーに変換します。入力 width
と height
両方とも 32 にする必要があります モデルの入力サイズは 32 x 32 ピクセルです。
pixelBuffer
を取得したらすぐに 、 model.prediction()
を呼び出すことができます pixelBuffer
を渡します .では、行きましょう! classLabel
の出力を得ることができます !
@IBAction func recognizePressed(_ sender: UIButton) {
// Turn view into an image
let resultImage = UIImage.init(view: canvas)
let pixelBuffer = resultImage.pixelBufferGray(width: 32, height: 32)
let model = hiraganaModel3()
// output a Hiragana character
let output = try? model.prediction(image: pixelBuffer!)
print(output?.classLabel)
}
extension UIImage {
// Resizes the image to width x height and converts it to a grayscale CVPixelBuffer
func pixelBufferGray(width: Int, height: Int) -> CVPixelBuffer? {
return _pixelBuffer(width: width, height: height,
pixelFormatType: kCVPixelFormatType_OneComponent8,
colorSpace: CGColorSpaceCreateDeviceGray(),
alphaInfo: .none)
}
func _pixelBuffer(width: Int, height: Int, pixelFormatType: OSType,
colorSpace: CGColorSpace, alphaInfo: CGImageAlphaInfo) -> CVPixelBuffer? {
var maybePixelBuffer: CVPixelBuffer?
let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue,
kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue]
let status = CVPixelBufferCreate(kCFAllocatorDefault,
width,
height,
pixelFormatType,
attrs as CFDictionary,
&maybePixelBuffer)
guard status == kCVReturnSuccess, let pixelBuffer = maybePixelBuffer else {
return nil
}
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer)
guard let context = CGContext(data: pixelData,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer),
space: colorSpace,
bitmapInfo: alphaInfo.rawValue)
else {
return nil
}
UIGraphicsPushContext(context)
context.translateBy(x: 0, y: CGFloat(height))
context.scaleBy(x: 1, y: -1)
self.draw(in: CGRect(x: 0, y: 0, width: width, height: height))
UIGraphicsPopContext()
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
return pixelBuffer
}
}
3. UIAlertController
で出力を表示します
この手順は完全にオプションです。冒頭の GIF に示されているように、結果を通知するアラート コントローラーを追加しました。
func informResultPopUp(message: String) {
let alertController = UIAlertController(title: message,
message: nil,
preferredStyle: .alert)
let ok = UIAlertAction(title: "Ok", style: .default, handler: { action in
self.dismiss(animated: true, completion: nil)
})
alertController.addAction(ok)
self.present(alertController, animated: true) { () in
}
}
出来上がり!デモ対応 (および App Store 対応) の OCR を作成しました。 ??
結論?
OCR の作成はそれほど難しくありません。ご覧のとおり、この記事は手順と問題で構成されており、このプロジェクトの構築中に遭遇しました。一連の Python コードを iOS に接続して実証可能にするプロセスを楽しんでおり、今後もそうするつもりです。
この記事が、OCR を作成したいと考えているが、どこから始めればよいか分からない人に役立つ情報を提供できることを願っています。
ソース コードを見つけることができます ここ.
ボーナス :浅いアルゴリズムの実験に興味がある場合は、読み続けてください!
[オプション] 浅いアルゴリズムでトレーニングしますか?
CNN を実装する前に、Kaichi と私は他の機械学習アルゴリズムをテストして、それらが仕事を完了できるかどうかを調べました (そして計算コストを節約できました!)。 KNN とランダム フォレストを選びました。
それらのパフォーマンスを評価するために、ベースライン精度を 1/71 =0.014 と定義しました。
日本語の知識がまったくない人が文字を正しく推測できる確率は 1.4% であると仮定しました。
したがって、その精度が 1.4% を超えることができれば、モデルはうまく機能します。そうだったか見てみましょう。 ?
KNN
最終的な精度は 54.84% でした。すでに 1.4% をはるかに上回っています!
ランダム フォレスト
精度は 79.23% で、ランダム フォレストは予想を上回りました。ハイパーパラメータを調整しながら、エスティメータの数とツリーの深さを増やすことで、より良い結果が得られました。森の中により多くの木 (推定量) があるということは、画像内のより多くの特徴が学習されることを意味すると考えました。また、ツリーが深ければ深いほど、特徴からより多くの詳細を学習します。
詳細を知りたい場合は、ランダム フォレストを使用した画像分類について説明しているこのペーパーを見つけました。
お読みいただきありがとうございます。ご意見やフィードバックは大歓迎です!
-
iOS11でAppStoreを使用する方法
App Storeは、iOS 11でこれまでで最大の再設計が行われ、元のコンテンツに重点が置かれ、新しいアプリやゲームの検索とダウンロードが簡単になりました。まったく新しいものです。AppStoreのロゴも一新されました。 これは歓迎すべき変更ですが(特にiOSゲーマーにとって)、iOS11でのAppStoreの動作にはいくつかの違いがあります。ここではiOS11でのAppStoreの新機能と使用方法について説明しますのでご安心ください。 。 新しいAppStoreを自分で見たいですか? iOS11ベータ版をインストールする方法は次のとおりです。 iOS 11AppStoreで新しいコ
-
iPhoneとiPadでAppStoreをUKに変更する方法
あなたは米国のAppStoreで立ち往生していますか、それともあなたのiOSデバイスはあなたが間違った国にいるのか間違った国から来ているのかを確信していますか?アプリと音楽の価格をドルまたはユーロで取得していますか、それとも地理的な制限により、英国で利用できるはずのコンテンツにアクセスできませんか? この簡単なチュートリアルでは、iPhoneまたはiPadのApp Storeを切り替えて、英国にいることを認識し、アプリ、音楽、テレビシリーズ、映画を英国の価格で購入できるようにする方法を示します。 地理的なトラブルシューティング これは、多くの Macworldによく知られている問題です。