├── .gitignore ├── LICENSE ├── Readme.md ├── matplotlibrc ├── package.json ├── requirements.txt └── src ├── create_train_data.py ├── img.py ├── model.py ├── predict.js ├── predict.py └── train.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # train data 107 | data/captcha 108 | data/train 109 | 110 | # model log 111 | log 112 | .DS_Store 113 | 114 | # node.js 115 | node_modules/ 116 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Do Minh Hai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Captcha Breaker 2 | Build with Tensorflow (ConvNets) and Node.js :muscle::muscle::muscle: 3 | 4 | E.x: *Amazon Captcha* (click image below to watch demo video) 5 | 6 | [![Amazon Captcha](https://i.ytimg.com/vi/pruaoG-MSo4/hqdefault.jpg)](https://youtu.be/pruaoG-MSo4) 7 | 8 | # Installation 9 | #### Python packages 10 | ``` 11 | $ pip install -r requirements.txt 12 | ``` 13 | 14 | #### Node.js packages (Node.js user only) 15 | ``` 16 | $ npm i 17 | ``` 18 | 19 | # Usage 20 | ## 1. Create train data 21 | #### Prepare your training dataset 22 | * Copy captcha images to `data/captcha` folder 23 | ``` 24 | |_data 25 | |_captcha 26 | |_ xss7.jpg 27 | |_ tvu4.jpg 28 | ``` 29 | **IMPORTANT:** Note each image file is named with it's own solution. 30 | 31 | That means that if an image is named `A1bD3.jpg`, it corresponds to a captcha's whose solution is `A1bD3` 32 | 33 | #### Build train data for model 34 | Run `src/create_train_data.py` will save your train data as `data/captcha.npz` compressed file. 35 | ``` 36 | $ python src/create_train_data.py 37 | ``` 38 | 39 | The compressed `data/captcha.npz` includes: 40 | * Train Data ( `x_train`, `y_train` ): `80%` 41 | * Test Data ( `x_test`, `y_test` ): `20%` 42 | 43 | ## 2. Train 44 | Run `src/train.py` to train the model with your own dataset. 45 | ``` 46 | $ python src/train.py 47 | ``` 48 | 49 | Take :coffee: or :tea: while waiting! 50 | 51 | ## 3. Attack 52 | Now, enjoy your war :fire::fire::fire: :stuck_out_tongue_winking_eye::stuck_out_tongue_winking_eye::stuck_out_tongue_winking_eye: 53 | 54 | #### Python 55 | ``` 56 | $ python src/predict --fname YOUR_IMAGE_PATH_or_URL 57 | ``` 58 | 59 | Sample output: 60 | ``` 61 | loading image: data/captcha/captcha_2.jpg 62 | load captcha classifier 63 | predict for 1 char: `X` with probability: 99.956% 64 | predict for 2 char: `I` with probability: 99.909% 65 | predict for 3 char: `N` with probability: 99.556% 66 | predict for 4 char: `C` with probability: 99.853% 67 | predict for 5 char: `H` with probability: 99.949% 68 | predict for 6 char: `A` with probability: 98.889% 69 | Captcha: `XINCHA` with confident: `99.686%` 70 | XINCHA 71 | ``` 72 | 73 | #### Node.js 74 | ```js 75 | const captchaPredict = require('src/predict') 76 | 77 | captchaPredict(YOUR_IMAGE_PATH_or_URL) 78 | .then(console.log) 79 | .catch(console.error) 80 | ``` 81 | Sample output: 82 | ``` 83 | [ 84 | "loading image: data/captcha/captcha_2.jpg", 85 | "load captcha classifier", 86 | "predict for 1 char: `X` with probability: 99.956%", 87 | "predict for 2 char: `I` with probability: 99.909%", 88 | "predict for 3 char: `N` with probability: 99.556%", 89 | "predict for 4 char: `C` with probability: 99.853%", 90 | "predict for 5 char: `H` with probability: 99.949%", 91 | "predict for 6 char: `A` with probability: 98.889%", 92 | "Captcha: `XINCHA` with confident: `99.686%`", 93 | "XINCHA" 94 | ] 95 | ``` 96 | -------------------------------------------------------------------------------- /matplotlibrc: -------------------------------------------------------------------------------- 1 | backend: TkAgg 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "captcha-breaker", 3 | "version": "1.0.0", 4 | "description": "Breaking Captcha with Tensorflow", 5 | "main": "src/predict.js", 6 | "dependencies": { 7 | "python-shell": "^0.5.0" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/dominhhai/captcha-breaker.git" 16 | }, 17 | "author": "Do Minh Hai", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/dominhhai/captcha-breaker/issues" 21 | }, 22 | "homepage": "https://github.com/dominhhai/captcha-breaker#README" 23 | } 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | absl-py==0.2.2 2 | astor==0.6.2 3 | backports.functools-lru-cache==1.5 4 | backports.weakref==1.0.post1 5 | bleach==1.5.0 6 | cloudpickle==0.5.3 7 | cycler==0.10.0 8 | dask==0.18.0 9 | decorator==4.3.0 10 | enum34==1.1.6 11 | funcsigs==1.0.2 12 | futures==3.2.0 13 | gast==0.2.0 14 | grpcio==1.12.1 15 | html5lib==0.9999999 16 | kiwisolver==1.0.1 17 | Markdown==2.6.11 18 | matplotlib==2.2.2 19 | mock==2.0.0 20 | networkx==2.1 21 | numpy==1.14.5 22 | pbr==4.0.4 23 | Pillow==6.2.0 24 | protobuf==3.6.0 25 | pyparsing==2.2.0 26 | python-dateutil==2.7.3 27 | pytz==2018.4 28 | PyWavelets==0.5.2 29 | scikit-image==0.14.0 30 | scipy==1.1.0 31 | six==1.11.0 32 | subprocess32==3.5.2 33 | tensorboard==1.8.0 34 | tensorflow==1.12.2 35 | termcolor==1.1.0 36 | toolz==0.9.0 37 | Werkzeug==0.15.3 38 | -------------------------------------------------------------------------------- /src/create_train_data.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, division 2 | import os 3 | import json 4 | from skimage import io 5 | from skimage.color import rgb2gray 6 | from img import split_letters 7 | import numpy as np 8 | 9 | 10 | # Helper methods 11 | def strip_extension(filename): 12 | return filename[:filename.rindex('.')] 13 | 14 | 15 | def build_data_map(data_path): 16 | files = os.listdir(data_path) 17 | return {x: strip_extension(x) for x in files} 18 | 19 | 20 | DATA_DIR = 'data' 21 | DATA_FULL_DIR = os.path.join(DATA_DIR, 'captcha') 22 | DATA_TRAIN_DIR = os.path.join(DATA_DIR, 'train') 23 | DATA_TRAIN_FILE = os.path.join(DATA_DIR, 'captcha') 24 | 25 | # array of tuple of binary image and label 26 | data_x = [] 27 | data_y = [] 28 | 29 | # build image contents map 30 | image_contents = build_data_map(DATA_FULL_DIR) 31 | 32 | # load image and save letters 33 | counter = 0 34 | 35 | for fname, contents in image_contents.iteritems(): 36 | counter += 1 37 | print(counter, fname, contents) 38 | original_image = io.imread(os.path.join(DATA_FULL_DIR, fname)) 39 | grayscale_image = rgb2gray(original_image) 40 | 41 | # split image 42 | letters = split_letters(grayscale_image, num_letters=len(contents), debug=True) 43 | if letters != None: 44 | for i, letter in enumerate(letters): 45 | content = contents[i] 46 | # add to dataset 47 | data_x.append(letter) 48 | data_y.append(np.uint8(ord(content) - 65)) # 65: 'A' 49 | 50 | # save letter into train folder 51 | fpath = os.path.join(DATA_TRAIN_DIR, content) 52 | if not os.path.exists(fpath): 53 | os.makedirs(fpath) 54 | letter_fname = os.path.join(fpath, str(i+1) + '-' + strip_extension(fname) + '.png') 55 | io.imsave(letter_fname, 255 - letter) # invert black <> white color 56 | else: 57 | print('Letters is not valid') 58 | break 59 | 60 | # split into train and test data set 61 | train_num = int(len(data_y) * 0.8) # 80% 62 | 63 | # save train data 64 | print('saving dataset') 65 | np.savez_compressed(DATA_TRAIN_FILE, 66 | x_train=data_x[:train_num], y_train=data_y[:train_num], 67 | x_test=data_x[train_num:], y_test=data_y[train_num:]) 68 | -------------------------------------------------------------------------------- /src/img.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, division 2 | from math import floor, ceil 3 | from skimage import img_as_ubyte 4 | from skimage.measure import find_contours 5 | from skimage.util import crop 6 | from skimage.transform import resize 7 | 8 | import matplotlib.pyplot as plt 9 | 10 | SHIFT_PIXEL = 10 # shift image from right to left 11 | BINARY_THRESH = 30 # image binary thresh 12 | LETTER_SIZE = (36, 36) # letter width, heigth 13 | 14 | def split_letters(image, num_letters=6, debug=False): 15 | ''' 16 | split full captcha image into `num_letters` lettersself. 17 | return list of letters binary image (0: white, 255: black) 18 | ''' 19 | # move left 20 | left = crop(image, ((0, 0), (0, image.shape[1]-SHIFT_PIXEL)), copy=True) 21 | image[:,:-SHIFT_PIXEL] = image[:,SHIFT_PIXEL:] 22 | image[:,-SHIFT_PIXEL:] = left 23 | # binarization 24 | binary = image > BINARY_THRESH 25 | # find contours 26 | contours = find_contours(binary, 0.5) 27 | contours = [[ 28 | [int(floor(min(contour[:, 1]))), int(floor(min(contour[:, 0])))], # top-left point 29 | [int(ceil(max(contour[:, 1]))), int(ceil(max(contour[:, 0])))] # down-right point 30 | ] for contour in contours] 31 | # keep letters order 32 | contours = sorted(contours, key=lambda contour: contour[0][0]) 33 | # find letters box 34 | letter_boxs = [] 35 | for contour in contours: 36 | if len(letter_boxs) > 0 and contour[0][0] < letter_boxs[-1][1][0] - 5: 37 | # skip inner contour 38 | continue 39 | # extract letter boxs by contour 40 | boxs = get_letter_boxs(binary, contour) 41 | for box in boxs: 42 | letter_boxs.append(box) 43 | # check letter outer boxs number 44 | if len(letter_boxs) != num_letters: 45 | print('ERROR: number of letters is NOT valid', len(letter_boxs)) 46 | # debug 47 | if debug: 48 | print(letter_boxs) 49 | plt.imshow(binary, interpolation='nearest', cmap=plt.cm.gray) 50 | for [x_min, y_min], [x_max, y_max] in letter_boxs: 51 | plt.plot( 52 | [x_min, x_max, x_max, x_min, x_min], 53 | [y_min, y_min, y_max, y_max, y_min], 54 | linewidth=2) 55 | plt.xticks([]) 56 | plt.yticks([]) 57 | plt.show() 58 | return None 59 | 60 | # normalize size (40x40) 61 | letters = [] 62 | for [x_min, y_min], [x_max, y_max] in letter_boxs: 63 | letter = resize(image[y_min:y_max, x_min:x_max], LETTER_SIZE) 64 | letter = img_as_ubyte(letter < 0.6) 65 | letters.append(letter) 66 | 67 | return letters 68 | 69 | def get_letter_boxs(binary, contour): 70 | boxs = [] 71 | w = contour[1][0] - contour[0][0] # width 72 | h = contour[1][1] - contour[0][1] # height 73 | if w < 10: 74 | # skip too small contour (noise) 75 | return boxs 76 | 77 | if w < 37 and w / h < 1.1: 78 | boxs.append(contour) 79 | else: 80 | # split 2 letters if w is large 81 | x_mean = contour[0][0] + int(round(w / 2)) 82 | sub_contours = [ 83 | [contour[0], [x_mean, contour[1][1]]], 84 | [[x_mean, contour[0][1]], contour[1]] 85 | ] 86 | for [x_min, y_min], [x_max, y_max] in sub_contours: 87 | # fit y_min, y_max 88 | y_min_val = min(binary[y_min + 1, x_min:x_max]) 89 | y_max_val = min(binary[y_max - 1, x_min:x_max]) 90 | while y_min_val or y_max_val: 91 | if y_min_val: 92 | y_min += 1 93 | y_min_val = min(binary[y_min + 1, x_min:x_max]) 94 | if y_max_val: 95 | y_max -= 1 96 | y_max_val = min(binary[y_max - 1, x_min:x_max]) 97 | 98 | boxs.append([[x_min, y_min], [x_max, y_max]]) 99 | 100 | return boxs 101 | -------------------------------------------------------------------------------- /src/model.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, division 2 | import tensorflow as tf 3 | 4 | MODEL_LOG_DIR = 'checkpoint' 5 | 6 | def cnn_model_fn(features, labels, mode): 7 | """Model function for CNN.""" 8 | # Input Layer 9 | input_layer = tf.reshape(features['x'], [-1, 36, 36, 1]) 10 | 11 | # Convolutional Layer #1 12 | conv1 = tf.layers.conv2d( 13 | inputs=input_layer, 14 | filters=32, 15 | kernel_size=[5, 5], 16 | padding='same', 17 | activation=tf.nn.relu) 18 | 19 | # Pooling Layer #1 20 | pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2) 21 | 22 | # Convolutional Layer #2 and Pooling Layer #2 23 | conv2 = tf.layers.conv2d( 24 | inputs=pool1, 25 | filters=64, 26 | kernel_size=[5, 5], 27 | padding='same', 28 | activation=tf.nn.relu) 29 | pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2) 30 | 31 | # Dense Layer 32 | pool2_flat = tf.layers.flatten(inputs=pool2) 33 | dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu) 34 | dropout = tf.layers.dropout( 35 | inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN) 36 | 37 | # Logits Layer 38 | logits = tf.layers.dense(inputs=dropout, units=26) 39 | 40 | predictions = { 41 | # Generate predictions (for PREDICT and EVAL mode) 42 | 'classes': tf.argmax(input=logits, axis=1), 43 | # Add `softmax_tensor` to the graph. It is used for PREDICT and by the 44 | # `logging_hook`. 45 | 'probabilities': tf.nn.softmax(logits, name='softmax_tensor') 46 | } 47 | 48 | if mode == tf.estimator.ModeKeys.PREDICT: 49 | return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions) 50 | 51 | # Calculate Loss (for both TRAIN and EVAL modes) 52 | loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits) 53 | 54 | # Configure the Training Op (for TRAIN mode) 55 | if mode == tf.estimator.ModeKeys.TRAIN: 56 | optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001) 57 | train_op = optimizer.minimize( 58 | loss=loss, 59 | global_step=tf.train.get_global_step()) 60 | return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op) 61 | 62 | # Add evaluation metrics (for EVAL mode) 63 | eval_metric_ops = { 64 | 'accuracy': tf.metrics.accuracy( 65 | labels=labels, predictions=predictions['classes'])} 66 | return tf.estimator.EstimatorSpec( 67 | mode=mode, loss=loss, eval_metric_ops=eval_metric_ops) 68 | 69 | captcha_classifier = tf.estimator.Estimator( 70 | model_fn=cnn_model_fn, 71 | model_dir=MODEL_LOG_DIR 72 | ) 73 | -------------------------------------------------------------------------------- /src/predict.js: -------------------------------------------------------------------------------- 1 | const PythonShell = require('python-shell') 2 | 3 | const PY_SCRIPT = 'src/predict.py' 4 | 5 | module.exports = image_path => (new Promise((resolve, reject) => { 6 | let opts = { } 7 | if (image_path) { 8 | opts.args = ['--fname', image_path] 9 | } 10 | PythonShell.run(PY_SCRIPT, opts, (err, results) => { 11 | if (err) return reject(err) 12 | resolve(results) 13 | }) 14 | })) 15 | -------------------------------------------------------------------------------- /src/predict.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | import argparse 4 | from skimage import io 5 | import tensorflow as tf 6 | from img import split_letters 7 | from model import captcha_classifier 8 | 9 | # disable all warnings 10 | import warnings 11 | warnings.filterwarnings('ignore') 12 | # disable tf runtime message 13 | import os 14 | os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' 15 | 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument('--fname', default='data/test.jpg', help='captcha image path') 18 | 19 | def main(argv): 20 | args = parser.parse_args(argv[1:]) 21 | if args.fname == None: 22 | print('Capture Image is missing!') 23 | return None 24 | 25 | # load image 26 | print('loading image:', args.fname) 27 | image = io.imread(args.fname) 28 | 29 | # split into letters 30 | letters = split_letters(image) 31 | if letters == None: 32 | print('Letters is not valid') 33 | return None 34 | 35 | # predict 36 | content = '' 37 | probs = [] 38 | for i, letter in enumerate(letters): 39 | letter = letter.astype('float32') / 255 40 | predict_input_fn = tf.estimator.inputs.numpy_input_fn( 41 | x={'x': letter}, 42 | y=None, 43 | num_epochs=1, 44 | shuffle=False) 45 | predictions = captcha_classifier.predict(input_fn=predict_input_fn) 46 | for pred_dict in predictions: 47 | class_id = pred_dict['classes'] 48 | prob = pred_dict['probabilities'][class_id] 49 | class_name = chr(65 + class_id) # from 65 : 'A' 50 | print('predict for {} char: `{}` with probability: {:.3f}%'.format( 51 | i+1, class_name, prob * 100)) 52 | content += class_name 53 | probs.append(prob) 54 | print('Captcha: `{}` with confident: `{:.3f}%`'.format( 55 | content, 100*sum(probs)/len(probs))) 56 | print(content) 57 | 58 | if __name__ == '__main__': 59 | tf.app.run(main) 60 | -------------------------------------------------------------------------------- /src/train.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, absolute_import, division 2 | import argparse 3 | import numpy as np 4 | import tensorflow as tf 5 | from model import captcha_classifier 6 | 7 | MODEL_LOG_DIR = 'checkpoint' 8 | 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('--batch_size', default=100, type=int, help='batch size') 11 | parser.add_argument('--train_steps', default=3000, type=int, 12 | help='number of training steps') 13 | 14 | def main(argv): 15 | # run argv 16 | args = parser.parse_args(argv[1:]) 17 | print(args. batch_size, args.train_steps) 18 | # Load training and eval data 19 | with np.load('data/captcha.npz') as f: 20 | x_train, y_train = f['x_train'], f['y_train'] 21 | x_test, y_test = f['x_test'], f['y_test'] 22 | print(x_train.shape, y_train.shape, x_test.shape, y_test.shape) 23 | x_train = x_train.astype('float32') / 255 24 | x_test = x_test.astype('float32') / 255 25 | y_train = np.asarray(y_train, dtype=np.int32) 26 | y_test = np.asarray(y_test, dtype=np.int32) 27 | 28 | # Set up logging for predictions 29 | tensors_to_log = {'probabilities': 'softmax_tensor'} 30 | logging_hook = tf.train.LoggingTensorHook( 31 | tensors=tensors_to_log, 32 | every_n_iter=50 33 | ) 34 | 35 | # Train the model 36 | train_input_fn = tf.estimator.inputs.numpy_input_fn( 37 | x={'x': x_train}, 38 | y=y_train, 39 | batch_size=args.batch_size, 40 | num_epochs=None, 41 | shuffle=True 42 | ) 43 | captcha_classifier.train( 44 | input_fn=train_input_fn, 45 | steps=args.train_steps, 46 | hooks=[logging_hook] 47 | ) 48 | 49 | # Evaluate the model and print results 50 | eval_input_fn = tf.estimator.inputs.numpy_input_fn( 51 | x={'x': x_test}, 52 | y=y_test, 53 | num_epochs=1, 54 | shuffle=False) 55 | eval_results = captcha_classifier.evaluate(input_fn=eval_input_fn) 56 | print(eval_results) 57 | 58 | if __name__ == '__main__': 59 | tf.logging.set_verbosity(tf.logging.INFO) 60 | tf.app.run(main) 61 | --------------------------------------------------------------------------------