reslove
Build-Deploy-Actions
Details
Build-Deploy-Actions
Details
This commit is contained in:
parent
3eddca3e36
commit
3888929644
|
@ -0,0 +1,15 @@
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
|
__pycache__/
|
||||||
|
/test*
|
||||||
|
test.py
|
||||||
|
/logs
|
||||||
|
/uploads
|
||||||
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
|
*.png
|
||||||
|
*.bmp
|
||||||
|
*.gif
|
||||||
|
*.h5
|
||||||
|
*.txt
|
||||||
|
*.json
|
|
@ -0,0 +1,12 @@
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.6"
|
||||||
|
# command to install dependencies
|
||||||
|
install:
|
||||||
|
- pip install -e .[test]
|
||||||
|
- pip install tensorflow
|
||||||
|
# command to run tests
|
||||||
|
script:
|
||||||
|
- pytest
|
||||||
|
- flake8
|
||||||
|
- mypy .
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Kichang Kim
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,99 @@
|
||||||
|
# DeepDanbooru
|
||||||
|
[](https://www.python.org/doc/versions/)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](http://kanotype.iptime.org:8003/deepdanbooru/)
|
||||||
|
|
||||||
|
**DeepDanbooru** is anime-style girl image tag estimation system. You can estimate your images on my live demo site, [DeepDanbooru Web](http://kanotype.iptime.org:8003/deepdanbooru/).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
DeepDanbooru is written by Python 3.6. Following packages are need to be installed.
|
||||||
|
- tensorflow>=2.1.0
|
||||||
|
- Click>=7.0
|
||||||
|
- numpy>=1.16.2
|
||||||
|
- requests>=2.22.0
|
||||||
|
- scikit-image>=0.15.0
|
||||||
|
- six>=1.13.0
|
||||||
|
|
||||||
|
Or just use `requirements.txt`.
|
||||||
|
```
|
||||||
|
> pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
alternatively you can install it with pip. Note that by default, tensorflow is not included.
|
||||||
|
|
||||||
|
To install it with tensorflow, add `tensorflow` extra package.
|
||||||
|
|
||||||
|
```
|
||||||
|
> # default installation
|
||||||
|
> pip install .
|
||||||
|
> # with tensorflow package
|
||||||
|
> pip install .[tensorflow]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
1. Prepare dataset. If you don't have, you can use [DanbooruDownloader](https://github.com/KichangKim/DanbooruDownloader) for download the dataset of [Danbooru](https://danbooru.donmai.us/). If you want to make your own dataset, see [Dataset Structure](#dataset-structure) section.
|
||||||
|
2. Create training project folder.
|
||||||
|
```
|
||||||
|
> deepdanbooru create-project [your_project_folder]
|
||||||
|
```
|
||||||
|
3. Prepare tag list. If you want to use latest tags, use following command. It downloads tag from Danbooru server.
|
||||||
|
```
|
||||||
|
> deepdanbooru download-tags [your_project_folder]
|
||||||
|
```
|
||||||
|
4. (Option) Filtering dataset. If you want to train with optional tags (rating and score), you should convert it as system tags.
|
||||||
|
```
|
||||||
|
> deepdanbooru make-training-database [your_dataset_sqlite_path] [your_filtered_sqlite_path]
|
||||||
|
```
|
||||||
|
5. Modify `project.json` in the project folder. You should change `database_path` setting to your actual sqlite file path.
|
||||||
|
6. Start training.
|
||||||
|
```
|
||||||
|
> deepdanbooru train-project [your_project_folder]
|
||||||
|
```
|
||||||
|
7. Enjoy it.
|
||||||
|
```
|
||||||
|
> deepdanbooru evaluate [image_file_path or folder]... --project-path [your_project_folder] --allow-folder
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dataset Structure
|
||||||
|
DeepDanbooru uses following folder structure for input dataset. SQLite file can be any name, but must be located in same folder to `images` folder.
|
||||||
|
```
|
||||||
|
MyDataset/
|
||||||
|
├── images/
|
||||||
|
│ ├── 00/
|
||||||
|
│ │ ├── 00000000000000000000000000000000.jpg
|
||||||
|
│ │ ├── ...
|
||||||
|
│ ├── 01/
|
||||||
|
│ │ ├── ...
|
||||||
|
│ └── ff/
|
||||||
|
│ ├── ...
|
||||||
|
└── my-dataset.sqlite
|
||||||
|
```
|
||||||
|
The core is SQLite database file. That file must be contains following table structure.
|
||||||
|
```
|
||||||
|
posts
|
||||||
|
├── id (INTEGER)
|
||||||
|
├── md5 (TEXT)
|
||||||
|
├── file_ext (TEXT)
|
||||||
|
├── tag_string (TEXT)
|
||||||
|
└── tag_count_general (INTEGER)
|
||||||
|
```
|
||||||
|
The filename of image must be `[md5].[file_ext]`. If you use your own images, `md5` don't have to be actual MD5 hash value.
|
||||||
|
|
||||||
|
`tag_string` is space splitted tag list, like `1girl ahoge long_hair`.
|
||||||
|
|
||||||
|
`tag_count_general` is used for the project setting, `minimum_tag_count`. Images which has equal or larger value of `tag_count_general` are used for training.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
**Project** is minimal unit for training on DeepDanbooru. You can modify various parameters for training.
|
||||||
|
```
|
||||||
|
MyProject/
|
||||||
|
├── project.json
|
||||||
|
└── tags.txt
|
||||||
|
```
|
||||||
|
`tags.txt` contains all tags for estimating. You can make your own list or download latest tags from Danbooru server. It is simple newline-separated file like this:
|
||||||
|
```
|
||||||
|
1girl
|
||||||
|
ahoge
|
||||||
|
...
|
||||||
|
```
|
|
@ -0,0 +1,8 @@
|
||||||
|
import deepdanbooru.commands
|
||||||
|
import deepdanbooru.data
|
||||||
|
import deepdanbooru.extra
|
||||||
|
import deepdanbooru.image
|
||||||
|
import deepdanbooru.io
|
||||||
|
import deepdanbooru.model
|
||||||
|
import deepdanbooru.project
|
||||||
|
import deepdanbooru.train
|
|
@ -0,0 +1,89 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
import deepdanbooru as dd
|
||||||
|
|
||||||
|
__version__ = '1.0.0'
|
||||||
|
|
||||||
|
|
||||||
|
@click.version_option(prog_name='DeepDanbooru', version=__version__)
|
||||||
|
@click.group()
|
||||||
|
def main():
|
||||||
|
'''
|
||||||
|
AI based multi-label girl image classification system, implemented by using TensorFlow.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@main.command('create-project')
|
||||||
|
@click.argument('project_path', type=click.Path(exists=False, resolve_path=True, file_okay=False, dir_okay=True))
|
||||||
|
def create_project(project_path):
|
||||||
|
dd.commands.create_project(project_path)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command('download-tags')
|
||||||
|
@click.option('--limit', default=10000, help='Limit for each category tag count.')
|
||||||
|
@click.option('--minimum-post-count', default=500, help='Minimum post count for tag.')
|
||||||
|
@click.option('--overwrite', help='Overwrite tags if exists.', is_flag=True)
|
||||||
|
@click.argument('path', type=click.Path(exists=False, resolve_path=True, file_okay=False, dir_okay=True))
|
||||||
|
def download_tags(path, limit, minimum_post_count, overwrite):
|
||||||
|
dd.commands.download_tags(path, limit, minimum_post_count, overwrite)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command('make-training-database')
|
||||||
|
@click.argument('source_path', type=click.Path(exists=True, resolve_path=True, file_okay=True, dir_okay=False), nargs=1, required=True)
|
||||||
|
@click.argument('output_path', type=click.Path(exists=False, resolve_path=True, file_okay=True, dir_okay=False), nargs=1, required=True)
|
||||||
|
@click.option('--start-id', default=1, help='Start id.', )
|
||||||
|
@click.option('--end-id', default=sys.maxsize, help='End id.')
|
||||||
|
@click.option('--use-deleted', help='Use deleted posts.', is_flag=True)
|
||||||
|
@click.option('--chunk-size', default=5000000, help='Chunk size for internal processing.')
|
||||||
|
@click.option('--overwrite', help='Overwrite tags if exists.', is_flag=True)
|
||||||
|
@click.option('--vacuum', help='Execute VACUUM command after making database.', is_flag=True)
|
||||||
|
def make_training_database(source_path, output_path, start_id, end_id, use_deleted, chunk_size, overwrite, vacuum):
|
||||||
|
dd.commands.make_training_database(source_path, output_path, start_id, end_id,
|
||||||
|
use_deleted, chunk_size, overwrite, vacuum)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command('train-project')
|
||||||
|
@click.argument('project_path', type=click.Path(exists=True, resolve_path=True, file_okay=False, dir_okay=True))
|
||||||
|
@click.option('--source-model', type=click.Path(exists=True, resolve_path=True, file_okay=True, dir_okay=False))
|
||||||
|
def train_project(project_path, source_model):
|
||||||
|
dd.commands.train_project(project_path, source_model)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command('evaluate-project', help='Evaluate the project. If the target path is folder, it evaulates all images recursively.')
|
||||||
|
@click.argument('project_path', type=click.Path(exists=True, resolve_path=True, file_okay=False, dir_okay=True))
|
||||||
|
@click.argument('target_path', type=click.Path(exists=True, resolve_path=True, file_okay=True, dir_okay=True))
|
||||||
|
@click.option('--threshold', help='Threshold for tag estimation.', default=0.5)
|
||||||
|
def evaluate_project(project_path, target_path, threshold):
|
||||||
|
dd.commands.evaluate_project(project_path, target_path, threshold)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command('grad-cam', help='Experimental feature. Calculate activation map using Grad-CAM.')
|
||||||
|
@click.argument('project_path', type=click.Path(exists=True, resolve_path=True, file_okay=False, dir_okay=True))
|
||||||
|
@click.argument('target_path', type=click.Path(exists=True, resolve_path=True, file_okay=True, dir_okay=True))
|
||||||
|
@click.argument('output_path', type=click.Path(resolve_path=True, file_okay=False, dir_okay=True), default='.')
|
||||||
|
@click.option('--threshold', help='Threshold for tag estimation.', default=0.5)
|
||||||
|
def grad_cam(project_path, target_path, output_path, threshold):
|
||||||
|
dd.commands.grad_cam(project_path, target_path, output_path, threshold)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command('evaluate', help='Evaluate model by estimating image tag.')
|
||||||
|
@click.argument('target_paths', nargs=-1, type=click.Path(exists=True, resolve_path=True, file_okay=True, dir_okay=True))
|
||||||
|
@click.option('--project-path', type=click.Path(exists=True, resolve_path=True, file_okay=False, dir_okay=True),
|
||||||
|
help='Project path. If you want to use specific model and tags, use --model-path and --tags-path options.')
|
||||||
|
@click.option('--model-path', type=click.Path(exists=True, resolve_path=True, file_okay=True, dir_okay=False))
|
||||||
|
@click.option('--tags-path', type=click.Path(exists=True, resolve_path=True, file_okay=True, dir_okay=False))
|
||||||
|
@click.option('--threshold', default=0.5)
|
||||||
|
@click.option('--allow-gpu', default=False, is_flag=True)
|
||||||
|
@click.option('--compile/--no-compile', 'compile_model', default=False)
|
||||||
|
@click.option('--allow-folder', default=False, is_flag=True, help='If this option is enabled, TARGET_PATHS can be folder path and all images (using --folder-filters) in that folder is estimated recursively. If there are file and folder which has same name, the file is skipped and only folder is used.')
|
||||||
|
@click.option('--folder-filters', default='*.[Pp][Nn][Gg],*.[Jj][Pp][Gg],*.[Jj][Pp][Ee][Gg],*.[Gg][Ii][Ff]', help='Glob pattern for searching image files in folder. You can specify multiple patterns by separating comma. This is used when --allow-folder is enabled. Default:*.[Pp][Nn][Gg],*.[Jj][Pp][Gg],*.[Jj][Pp][Ee][Gg],*.[Gg][Ii][Ff]')
|
||||||
|
@click.option('--verbose', default=False, is_flag=True)
|
||||||
|
def evaluate(target_paths, project_path, model_path, tags_path, threshold, allow_gpu, compile_model, allow_folder, folder_filters, verbose):
|
||||||
|
dd.commands.evaluate(target_paths, project_path, model_path, tags_path, threshold, allow_gpu, compile_model, allow_folder, folder_filters, verbose)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,7 @@
|
||||||
|
from .create_project import create_project
|
||||||
|
from .download_tags import download_tags
|
||||||
|
from .make_training_database import make_training_database
|
||||||
|
from .train_project import train_project
|
||||||
|
from .evaluate_project import evaluate_project
|
||||||
|
from .grad_cam import grad_cam
|
||||||
|
from .evaluate import evaluate, evaluate_image
|
|
@ -0,0 +1,15 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
import deepdanbooru as dd
|
||||||
|
|
||||||
|
|
||||||
|
def create_project(project_path):
|
||||||
|
"""
|
||||||
|
Create new project with default parameters.
|
||||||
|
"""
|
||||||
|
dd.io.try_create_directory(project_path)
|
||||||
|
project_context_path = os.path.join(project_path, 'project.json')
|
||||||
|
dd.io.serialize_as_json(
|
||||||
|
dd.project.DEFAULT_PROJECT_CONTEXT, project_context_path)
|
||||||
|
|
||||||
|
print(f'New project was successfully created. ({project_path})')
|
|
@ -0,0 +1,162 @@
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import deepdanbooru as dd
|
||||||
|
|
||||||
|
|
||||||
|
def download_category_tags(category, minimum_post_count, limit, page_size=1000, order='count'):
|
||||||
|
category_to_index = {
|
||||||
|
'general': 0,
|
||||||
|
'artist': 1,
|
||||||
|
'copyright': 3,
|
||||||
|
'character': 4
|
||||||
|
}
|
||||||
|
|
||||||
|
gold_only_tags = ['loli', 'shota', 'toddlercon']
|
||||||
|
|
||||||
|
if category not in category_to_index:
|
||||||
|
raise Exception(f'Not supported category : {category}')
|
||||||
|
|
||||||
|
category_index = category_to_index[category]
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
'limit': page_size,
|
||||||
|
'page': 1,
|
||||||
|
'search[order]': order,
|
||||||
|
'search[category]': category_index
|
||||||
|
}
|
||||||
|
|
||||||
|
request_url = 'https://danbooru.donmai.us/tags.json'
|
||||||
|
|
||||||
|
tags = set()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
response = requests.get(request_url, params=parameters)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
response_tags = [tag_json['name']
|
||||||
|
for tag_json in response_json if tag_json['post_count'] >= minimum_post_count]
|
||||||
|
|
||||||
|
if not response_tags:
|
||||||
|
break
|
||||||
|
|
||||||
|
is_full = False
|
||||||
|
|
||||||
|
for tag in response_tags:
|
||||||
|
if tag in gold_only_tags:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tags.add(tag)
|
||||||
|
|
||||||
|
if len(tags) >= limit:
|
||||||
|
is_full = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_full:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
parameters['page'] += 1
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
def download_tags(project_path, limit, minimum_post_count, is_overwrite):
|
||||||
|
print(
|
||||||
|
f'Start downloading tags ... (limit:{limit}, minimum_post_count:{minimum_post_count})')
|
||||||
|
|
||||||
|
log = {
|
||||||
|
'date': time.strftime("%Y/%m/%d %H:%M:%S"),
|
||||||
|
'limit': limit,
|
||||||
|
'minimum_post_count': minimum_post_count
|
||||||
|
}
|
||||||
|
|
||||||
|
system_tags = [
|
||||||
|
'rating:safe',
|
||||||
|
'rating:questionable',
|
||||||
|
'rating:explicit',
|
||||||
|
# 'score:very_bad',
|
||||||
|
# 'score:bad',
|
||||||
|
# 'score:average',
|
||||||
|
# 'score:good',
|
||||||
|
# 'score:very_good',
|
||||||
|
]
|
||||||
|
|
||||||
|
category_definitions = [
|
||||||
|
{
|
||||||
|
'category_name': 'General',
|
||||||
|
'category': 'general',
|
||||||
|
'path': os.path.join(project_path, 'tags-general.txt'),
|
||||||
|
},
|
||||||
|
# {
|
||||||
|
# 'category_name': 'Artist',
|
||||||
|
# 'category': 'artist',
|
||||||
|
# 'path': os.path.join(path, 'tags-artist.txt'),
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# 'category_name': 'Copyright',
|
||||||
|
# 'category': 'copyright',
|
||||||
|
# 'path': os.path.join(path, 'tags-copyright.txt'),
|
||||||
|
# },
|
||||||
|
{
|
||||||
|
'category_name': 'Character',
|
||||||
|
'category': 'character',
|
||||||
|
'path': os.path.join(project_path, 'tags-character.txt'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
all_tags_path = os.path.join(project_path, 'tags.txt')
|
||||||
|
|
||||||
|
if not is_overwrite and os.path.exists(all_tags_path):
|
||||||
|
raise Exception(f'Tags file is already exists : {all_tags_path}')
|
||||||
|
|
||||||
|
dd.io.try_create_directory(os.path.dirname(all_tags_path))
|
||||||
|
dd.io.serialize_as_json(
|
||||||
|
log, os.path.join(project_path, 'tags_log.json'))
|
||||||
|
|
||||||
|
categories_for_web = []
|
||||||
|
categories_for_web_path = os.path.join(project_path, 'categories.json')
|
||||||
|
tag_start_index = 0
|
||||||
|
|
||||||
|
total_tags_count = 0
|
||||||
|
|
||||||
|
with open(all_tags_path, 'w') as all_tags_stream:
|
||||||
|
for category_definition in category_definitions:
|
||||||
|
category = category_definition['category']
|
||||||
|
category_tags_path = category_definition['path']
|
||||||
|
|
||||||
|
print(f'{category} tags are downloading ...')
|
||||||
|
tags = download_category_tags(category, minimum_post_count, limit)
|
||||||
|
|
||||||
|
tags = dd.extra.natural_sorted(tags)
|
||||||
|
tag_count = len(tags)
|
||||||
|
if tag_count == 0:
|
||||||
|
print(f'{category} tags are not exists.')
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print(f'{tag_count} tags are downloaded.')
|
||||||
|
|
||||||
|
with open(category_tags_path, 'w') as category_tags_stream:
|
||||||
|
for tag in tags:
|
||||||
|
category_tags_stream.write(f'{tag}\n')
|
||||||
|
all_tags_stream.write(f'{tag}\n')
|
||||||
|
|
||||||
|
categories_for_web.append(
|
||||||
|
{'name': category_definition['category_name'], 'start_index': tag_start_index})
|
||||||
|
|
||||||
|
tag_start_index += len(tags)
|
||||||
|
total_tags_count += tag_count
|
||||||
|
|
||||||
|
for tag in system_tags:
|
||||||
|
all_tags_stream.write(f'{tag}\n')
|
||||||
|
|
||||||
|
categories_for_web.append(
|
||||||
|
{'name': 'System', 'start_index': total_tags_count}
|
||||||
|
)
|
||||||
|
|
||||||
|
dd.io.serialize_as_json(categories_for_web, categories_for_web_path)
|
||||||
|
|
||||||
|
print(f'Total {total_tags_count} tags are downloaded.')
|
||||||
|
|
||||||
|
print('All processes are complete.')
|
|
@ -0,0 +1,77 @@
|
||||||
|
import os
|
||||||
|
from typing import Any, Iterable, List, Tuple, Union
|
||||||
|
|
||||||
|
import six
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
import deepdanbooru as dd
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_image(
|
||||||
|
image_input: Union[str, six.BytesIO], model: Any, tags: List[str], threshold: float
|
||||||
|
) -> Iterable[Tuple[str, float]]:
|
||||||
|
width = model.input_shape[2]
|
||||||
|
height = model.input_shape[1]
|
||||||
|
|
||||||
|
image = dd.data.load_image_for_evaluate(
|
||||||
|
image_input, width=width, height=height)
|
||||||
|
|
||||||
|
image_shape = image.shape
|
||||||
|
image = image.reshape(
|
||||||
|
(1, image_shape[0], image_shape[1], image_shape[2]))
|
||||||
|
y = model.predict(image)[0]
|
||||||
|
|
||||||
|
result_dict = {}
|
||||||
|
|
||||||
|
for i, tag in enumerate(tags):
|
||||||
|
result_dict[tag] = y[i]
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
if result_dict[tag] >= threshold:
|
||||||
|
yield tag, result_dict[tag]
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate(target_paths, project_path, model_path, tags_path, threshold, allow_gpu, compile_model, allow_folder, folder_filters, verbose):
|
||||||
|
if not allow_gpu:
|
||||||
|
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
|
||||||
|
|
||||||
|
if not model_path and not project_path:
|
||||||
|
raise Exception('You must provide project path or model path.')
|
||||||
|
|
||||||
|
if not tags_path and not project_path:
|
||||||
|
raise Exception('You must provide project path or tags path.')
|
||||||
|
|
||||||
|
target_image_paths = []
|
||||||
|
|
||||||
|
for target_path in target_paths:
|
||||||
|
if allow_folder and not os.path.isfile(target_path):
|
||||||
|
target_image_paths.extend(dd.io.get_image_file_paths_recursive(target_path, folder_filters))
|
||||||
|
else:
|
||||||
|
target_image_paths.append(target_path)
|
||||||
|
|
||||||
|
target_image_paths = dd.extra.natural_sorted(target_image_paths)
|
||||||
|
|
||||||
|
if model_path:
|
||||||
|
if verbose:
|
||||||
|
print(f'Loading model from {model_path} ...')
|
||||||
|
model = tf.keras.models.load_model(model_path, compile=compile_model)
|
||||||
|
else:
|
||||||
|
if verbose:
|
||||||
|
print(f'Loading model from project {project_path} ...')
|
||||||
|
model = dd.project.load_model_from_project(project_path, compile_model=compile_model)
|
||||||
|
|
||||||
|
if tags_path:
|
||||||
|
if verbose:
|
||||||
|
print(f'Loading tags from {tags_path} ...')
|
||||||
|
tags = dd.data.load_tags(tags_path)
|
||||||
|
else:
|
||||||
|
if verbose:
|
||||||
|
print(f'Loading tags from project {project_path} ...')
|
||||||
|
tags = dd.project.load_tags_from_project(project_path)
|
||||||
|
|
||||||
|
for image_path in target_image_paths:
|
||||||
|
print(f'Tags of {image_path}:')
|
||||||
|
for tag, score in evaluate_image(image_path, model, tags, threshold):
|
||||||
|
print(f'({score:05.3f}) {tag}')
|
||||||
|
|
||||||
|
print()
|
|
@ -0,0 +1,51 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
import deepdanbooru as dd
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_project(project_path, target_path, threshold):
|
||||||
|
if not os.path.exists(target_path):
|
||||||
|
raise Exception(f'Target path {target_path} is not exists.')
|
||||||
|
|
||||||
|
if os.path.isfile(target_path):
|
||||||
|
taget_image_paths = [target_path]
|
||||||
|
|
||||||
|
else:
|
||||||
|
patterns = [
|
||||||
|
'*.[Pp][Nn][Gg]',
|
||||||
|
'*.[Jj][Pp][Gg]',
|
||||||
|
'*.[Jj][Pp][Ee][Gg]',
|
||||||
|
'*.[Gg][Ii][Ff]'
|
||||||
|
]
|
||||||
|
|
||||||
|
taget_image_paths = dd.io.get_file_paths_in_directory(
|
||||||
|
target_path, patterns)
|
||||||
|
|
||||||
|
taget_image_paths = dd.extra.natural_sorted(taget_image_paths)
|
||||||
|
|
||||||
|
project_context, model, tags = dd.project.load_project(project_path)
|
||||||
|
|
||||||
|
width = project_context['image_width']
|
||||||
|
height = project_context['image_height']
|
||||||
|
|
||||||
|
for image_path in taget_image_paths:
|
||||||
|
image = dd.data.load_image_for_evaluate(
|
||||||
|
image_path, width=width, height=height)
|
||||||
|
|
||||||
|
image_shape = image.shape
|
||||||
|
# image = image.astype(np.float16)
|
||||||
|
image = image.reshape(
|
||||||
|
(1, image_shape[0], image_shape[1], image_shape[2]))
|
||||||
|
y = model.predict(image)[0]
|
||||||
|
|
||||||
|
result_dict = {}
|
||||||
|
|
||||||
|
for i, tag in enumerate(tags):
|
||||||
|
result_dict[tag] = y[i]
|
||||||
|
|
||||||
|
print(f'Tags of {image_path}:')
|
||||||
|
for tag in tags:
|
||||||
|
if result_dict[tag] >= threshold:
|
||||||
|
print(f'({result_dict[tag]:05.3f}) {tag}')
|
||||||
|
|
||||||
|
print()
|
|
@ -0,0 +1,111 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
import deepdanbooru as dd
|
||||||
|
from scipy import ndimage
|
||||||
|
|
||||||
|
|
||||||
|
@tf.function
|
||||||
|
def get_gradient(model, x, output_mask):
|
||||||
|
with tf.GradientTape() as tape:
|
||||||
|
output = model(x)
|
||||||
|
gradcam_loss = tf.reduce_sum(tf.multiply(output_mask, output))
|
||||||
|
|
||||||
|
return tape.gradient(gradcam_loss, x)
|
||||||
|
|
||||||
|
|
||||||
|
def norm_clip_grads(grads):
|
||||||
|
upper_quantile = np.quantile(grads, 0.99)
|
||||||
|
lower_quantile = np.quantile(grads, 0.01)
|
||||||
|
clipped_grads = np.abs(np.clip(grads, lower_quantile, upper_quantile))
|
||||||
|
|
||||||
|
return clipped_grads / np.max(clipped_grads)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_grads(grads):
|
||||||
|
return ndimage.median_filter(grads, 10)
|
||||||
|
|
||||||
|
|
||||||
|
def to_onehot(length, index):
|
||||||
|
value = np.zeros(shape=(1, length), dtype=np.float32)
|
||||||
|
value[0, index] = 1.0
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def grad_cam(project_path, target_path, output_path, threshold):
|
||||||
|
# os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
|
||||||
|
|
||||||
|
if not os.path.exists(target_path):
|
||||||
|
raise Exception(f'Target path {target_path} is not exists.')
|
||||||
|
|
||||||
|
if os.path.isfile(target_path):
|
||||||
|
taget_image_paths = [target_path]
|
||||||
|
else:
|
||||||
|
patterns = [
|
||||||
|
'*.[Pp][Nn][Gg]',
|
||||||
|
'*.[Jj][Pp][Gg]',
|
||||||
|
'*.[Jj][Pp][Ee][Gg]',
|
||||||
|
'*.[Gg][Ii][Ff]'
|
||||||
|
]
|
||||||
|
|
||||||
|
taget_image_paths = dd.io.get_file_paths_in_directory(
|
||||||
|
target_path, patterns)
|
||||||
|
|
||||||
|
taget_image_paths = dd.extra.natural_sorted(taget_image_paths)
|
||||||
|
|
||||||
|
model = dd.project.load_model_from_project(project_path)
|
||||||
|
tags = dd.project.load_tags_from_project(project_path)
|
||||||
|
width = model.input_shape[2]
|
||||||
|
height = model.input_shape[1]
|
||||||
|
|
||||||
|
dd.io.try_create_directory(output_path)
|
||||||
|
|
||||||
|
for image_path in taget_image_paths:
|
||||||
|
image = dd.data.load_image_for_evaluate(
|
||||||
|
image_path, width=width, height=height)
|
||||||
|
image_name = os.path.splitext(os.path.basename(image_path))[0]
|
||||||
|
|
||||||
|
image_folder = os.path.join(output_path, image_name)
|
||||||
|
dd.io.try_create_directory(image_folder)
|
||||||
|
|
||||||
|
Image.fromarray(np.uint8(
|
||||||
|
image * 255.0)).save(os.path.join(image_folder, f'input.png'))
|
||||||
|
image_for_result = image
|
||||||
|
image_shape = image.shape
|
||||||
|
y = model.predict(image.reshape(
|
||||||
|
(1, image_shape[0], image_shape[1], image_shape[2])))[0]
|
||||||
|
|
||||||
|
result_dict = {}
|
||||||
|
|
||||||
|
estimated_tags = []
|
||||||
|
|
||||||
|
for i, tag in enumerate(tags):
|
||||||
|
result_dict[tag] = y[i]
|
||||||
|
|
||||||
|
if y[i] >= threshold:
|
||||||
|
estimated_tags.append((i, tag))
|
||||||
|
|
||||||
|
print(f'Tags of {image_path}:')
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
if result_dict[tag] >= threshold:
|
||||||
|
print(f'({result_dict[tag]:05.3f}) {tag}')
|
||||||
|
|
||||||
|
image = image.astype(np.float32)
|
||||||
|
|
||||||
|
for estimated_tag in estimated_tags:
|
||||||
|
print(f'Calculating grad-cam ... ({estimated_tag[1]})')
|
||||||
|
grads = get_gradient(model, tf.Variable(
|
||||||
|
[image]), to_onehot(len(tags), estimated_tag[0]))[0]
|
||||||
|
print('Normalizing gradients ...')
|
||||||
|
grads = norm_clip_grads(grads)
|
||||||
|
print('Filtering gradients ...')
|
||||||
|
grads = filter_grads(grads)
|
||||||
|
Image.fromarray(np.uint8(grads * 255.0)
|
||||||
|
).save(os.path.join(image_folder, f'result-{estimated_tag[1]}.png'.replace(':', '_').replace('/', '_')))
|
||||||
|
mask_array = np.stack([np.max(
|
||||||
|
grads, axis=-1)]*3, axis=2)
|
||||||
|
Image.fromarray(np.uint8(np.multiply(image_for_result, mask_array)
|
||||||
|
* 255.0)).save(os.path.join(image_folder, f'result-{estimated_tag[1]}-masked.png'.replace(':', '_').replace('/', '_')))
|
|
@ -0,0 +1,122 @@
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
def make_training_database(source_path, output_path, start_id, end_id,
|
||||||
|
use_deleted, chunk_size, overwrite, vacuum):
|
||||||
|
'''
|
||||||
|
Make sqlite database for training. Also add system tags.
|
||||||
|
'''
|
||||||
|
if source_path == output_path:
|
||||||
|
raise Exception('Source path and output path is equal.')
|
||||||
|
|
||||||
|
if os.path.exists(output_path):
|
||||||
|
if overwrite:
|
||||||
|
os.remove(output_path)
|
||||||
|
else:
|
||||||
|
raise Exception(f'{output_path} is already exists.')
|
||||||
|
|
||||||
|
source_connection = sqlite3.connect(source_path)
|
||||||
|
source_connection.row_factory = sqlite3.Row
|
||||||
|
source_cursor = source_connection.cursor()
|
||||||
|
|
||||||
|
output_connection = sqlite3.connect(output_path)
|
||||||
|
output_connection.row_factory = sqlite3.Row
|
||||||
|
output_cursor = output_connection.cursor()
|
||||||
|
|
||||||
|
table_name = 'posts'
|
||||||
|
id_column_name = 'id'
|
||||||
|
md5_column_name = 'md5'
|
||||||
|
extension_column_name = 'file_ext'
|
||||||
|
tags_column_name = 'tag_string'
|
||||||
|
tag_count_general_column_name = 'tag_count_general'
|
||||||
|
rating_column_name = 'rating'
|
||||||
|
score_column_name = 'score'
|
||||||
|
deleted_column_name = 'is_deleted'
|
||||||
|
|
||||||
|
# Create output table
|
||||||
|
print('Creating table ...')
|
||||||
|
output_cursor.execute(f"""CREATE TABLE {table_name} (
|
||||||
|
{id_column_name} INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
{md5_column_name} TEXT,
|
||||||
|
{extension_column_name} TEXT,
|
||||||
|
{tags_column_name} TEXT,
|
||||||
|
{tag_count_general_column_name} INTEGER )""")
|
||||||
|
output_connection.commit()
|
||||||
|
print('Creating table is complete.')
|
||||||
|
|
||||||
|
current_start_id = start_id
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print(
|
||||||
|
f'Fetching source rows ... ({current_start_id}~)')
|
||||||
|
source_cursor.execute(
|
||||||
|
f"""SELECT
|
||||||
|
{id_column_name},{md5_column_name},{extension_column_name},{tags_column_name},{tag_count_general_column_name},{rating_column_name},{score_column_name},{deleted_column_name}
|
||||||
|
FROM {table_name} WHERE ({id_column_name} >= ?) ORDER BY {id_column_name} ASC LIMIT ?""",
|
||||||
|
(current_start_id, chunk_size))
|
||||||
|
|
||||||
|
rows = source_cursor.fetchall()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
|
||||||
|
insert_params = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
post_id = row[id_column_name]
|
||||||
|
md5 = row[md5_column_name]
|
||||||
|
extension = row[extension_column_name]
|
||||||
|
tags = row[tags_column_name]
|
||||||
|
general_tag_count = row[tag_count_general_column_name]
|
||||||
|
rating = row[rating_column_name]
|
||||||
|
# score = row[score_column_name]
|
||||||
|
is_deleted = row[deleted_column_name]
|
||||||
|
|
||||||
|
if post_id > end_id:
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_deleted and not use_deleted:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rating == 's':
|
||||||
|
tags += f' rating:safe'
|
||||||
|
elif rating == 'q':
|
||||||
|
tags += f' rating:questionable'
|
||||||
|
elif rating == 'e':
|
||||||
|
tags += f' rating:explicit'
|
||||||
|
|
||||||
|
# if score < -6:
|
||||||
|
# tags += f' score:very_bad'
|
||||||
|
# elif score >= -6 and score < 0:
|
||||||
|
# tags += f' score:bad'
|
||||||
|
# elif score >= 0 and score < 7:
|
||||||
|
# tags += f' score:average'
|
||||||
|
# elif score >= 7 and score < 13:
|
||||||
|
# tags += f' score:good'
|
||||||
|
# elif score >= 13:
|
||||||
|
# tags += f' score:very_good'
|
||||||
|
|
||||||
|
insert_params.append(
|
||||||
|
(post_id, md5, extension, tags, general_tag_count))
|
||||||
|
|
||||||
|
if insert_params:
|
||||||
|
print('Inserting ...')
|
||||||
|
output_cursor.executemany(
|
||||||
|
f"""INSERT INTO {table_name} (
|
||||||
|
{id_column_name},{md5_column_name},{extension_column_name},{tags_column_name},{tag_count_general_column_name})
|
||||||
|
values (?, ?, ?, ?, ?)""", insert_params)
|
||||||
|
output_connection.commit()
|
||||||
|
|
||||||
|
current_start_id = rows[-1][id_column_name] + 1
|
||||||
|
|
||||||
|
if current_start_id > end_id or len(rows) < chunk_size:
|
||||||
|
break
|
||||||
|
|
||||||
|
if vacuum:
|
||||||
|
print('Vacuum ...')
|
||||||
|
output_cursor.execute('vacuum')
|
||||||
|
output_connection.commit()
|
||||||
|
|
||||||
|
source_connection.close()
|
||||||
|
output_connection.close()
|
|
@ -0,0 +1,222 @@
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
import deepdanbooru as dd
|
||||||
|
|
||||||
|
|
||||||
|
def train_project(project_path, source_model):
|
||||||
|
project_context_path = os.path.join(project_path, 'project.json')
|
||||||
|
project_context = dd.io.deserialize_from_json(project_context_path)
|
||||||
|
|
||||||
|
width = project_context['image_width']
|
||||||
|
height = project_context['image_height']
|
||||||
|
database_path = project_context['database_path']
|
||||||
|
minimum_tag_count = project_context['minimum_tag_count']
|
||||||
|
model_type = project_context['model']
|
||||||
|
optimizer_type = project_context['optimizer']
|
||||||
|
learning_rate = project_context['learning_rate'] if 'learning_rate' in project_context else 0.001
|
||||||
|
learning_rates = project_context['learning_rates'] if 'learning_rates' in project_context else None
|
||||||
|
minibatch_size = project_context['minibatch_size']
|
||||||
|
epoch_count = project_context['epoch_count']
|
||||||
|
export_model_per_epoch = project_context[
|
||||||
|
'export_model_per_epoch'] if 'export_model_per_epoch' in project_context else 10
|
||||||
|
checkpoint_frequency_mb = project_context['checkpoint_frequency_mb']
|
||||||
|
console_logging_frequency_mb = project_context['console_logging_frequency_mb']
|
||||||
|
rotation_range = project_context['rotation_range']
|
||||||
|
scale_range = project_context['scale_range']
|
||||||
|
shift_range = project_context['shift_range']
|
||||||
|
|
||||||
|
# disable PNG warning
|
||||||
|
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
|
||||||
|
# tf.logging.set_verbosity(tf.logging.ERROR)
|
||||||
|
|
||||||
|
# tf.keras.backend.set_epsilon(1e-6)
|
||||||
|
# tf.keras.mixed_precision.experimental.set_policy('infer_float32_vars')
|
||||||
|
# tf.config.gpu.set_per_process_memory_growth(True)
|
||||||
|
|
||||||
|
if optimizer_type == 'adam':
|
||||||
|
optimizer = tf.optimizers.Adam(learning_rate)
|
||||||
|
print('Using Adam optimizer ... ')
|
||||||
|
elif optimizer_type == 'sgd':
|
||||||
|
optimizer = tf.optimizers.SGD(
|
||||||
|
learning_rate, momentum=0.9, nesterov=True)
|
||||||
|
print('Using SGD optimizer ... ')
|
||||||
|
elif optimizer_type == 'rmsprop':
|
||||||
|
optimizer = tf.optimizers.RMSprop(learning_rate)
|
||||||
|
print('Using RMSprop optimizer ... ')
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"Not supported optimizer : {optimizer_type}")
|
||||||
|
|
||||||
|
if model_type == 'resnet_152':
|
||||||
|
model_delegate = dd.model.resnet.create_resnet_152
|
||||||
|
elif model_type == 'resnet_custom_v1':
|
||||||
|
model_delegate = dd.model.resnet.create_resnet_custom_v1
|
||||||
|
elif model_type == 'resnet_custom_v2':
|
||||||
|
model_delegate = dd.model.resnet.create_resnet_custom_v2
|
||||||
|
elif model_type == 'resnet_custom_v3':
|
||||||
|
model_delegate = dd.model.resnet.create_resnet_custom_v3
|
||||||
|
elif model_type == 'resnet_custom_v4':
|
||||||
|
model_delegate = dd.model.resnet.create_resnet_custom_v4
|
||||||
|
else:
|
||||||
|
raise Exception(f'Not supported model : {model_type}')
|
||||||
|
|
||||||
|
print('Loading tags ... ')
|
||||||
|
tags = dd.project.load_tags_from_project(project_path)
|
||||||
|
output_dim = len(tags)
|
||||||
|
|
||||||
|
print(f'Creating model ({model_type}) ... ')
|
||||||
|
# tf.keras.backend.set_learning_phase(1)
|
||||||
|
|
||||||
|
if source_model:
|
||||||
|
model = tf.keras.models.load_model(source_model)
|
||||||
|
print(f'Model : {model.input_shape} -> {model.output_shape} (loaded from {source_model})')
|
||||||
|
else:
|
||||||
|
inputs = tf.keras.Input(shape=(height, width, 3),
|
||||||
|
dtype=tf.float32) # HWC
|
||||||
|
ouputs = model_delegate(inputs, output_dim)
|
||||||
|
model = tf.keras.Model(inputs=inputs, outputs=ouputs, name=model_type)
|
||||||
|
print(f'Model : {model.input_shape} -> {model.output_shape}')
|
||||||
|
|
||||||
|
model.compile(optimizer=optimizer, loss=tf.keras.losses.BinaryCrossentropy(),
|
||||||
|
metrics=[tf.keras.metrics.Precision(), tf.keras.metrics.Recall()])
|
||||||
|
|
||||||
|
print(f'Loading database ... ')
|
||||||
|
image_records = dd.data.load_image_records(
|
||||||
|
database_path, minimum_tag_count)
|
||||||
|
|
||||||
|
# Checkpoint variables
|
||||||
|
used_epoch = tf.Variable(0, dtype=tf.int64)
|
||||||
|
used_minibatch = tf.Variable(0, dtype=tf.int64)
|
||||||
|
used_sample = tf.Variable(0, dtype=tf.int64)
|
||||||
|
offset = tf.Variable(0, dtype=tf.int64)
|
||||||
|
random_seed = tf.Variable(0, dtype=tf.int64)
|
||||||
|
|
||||||
|
checkpoint = tf.train.Checkpoint(
|
||||||
|
optimizer=optimizer,
|
||||||
|
model=model,
|
||||||
|
used_epoch=used_epoch,
|
||||||
|
used_minibatch=used_minibatch,
|
||||||
|
used_sample=used_sample,
|
||||||
|
offset=offset,
|
||||||
|
random_seed=random_seed)
|
||||||
|
|
||||||
|
manager = tf.train.CheckpointManager(
|
||||||
|
checkpoint=checkpoint,
|
||||||
|
directory=os.path.join(project_path, 'checkpoints'),
|
||||||
|
max_to_keep=3)
|
||||||
|
|
||||||
|
if manager.latest_checkpoint:
|
||||||
|
print(f"Checkpoint exists. Continuing training ... ({datetime.datetime.now()})")
|
||||||
|
checkpoint.restore(manager.latest_checkpoint)
|
||||||
|
print(f'used_epoch={int(used_epoch)}, used_minibatch={int(used_minibatch)}, used_sample={int(used_sample)}, offset={int(offset)}, random_seed={int(random_seed)}')
|
||||||
|
else:
|
||||||
|
print(f'No checkpoint. Starting new training ... ({datetime.datetime.now()})')
|
||||||
|
|
||||||
|
epoch_size = len(image_records)
|
||||||
|
slice_size = minibatch_size * checkpoint_frequency_mb
|
||||||
|
loss_sum = 0.0
|
||||||
|
loss_count = 0
|
||||||
|
used_sample_sum = 0
|
||||||
|
last_time = time.time()
|
||||||
|
|
||||||
|
while int(used_epoch) < epoch_count:
|
||||||
|
print(f'Shuffling samples (epoch {int(used_epoch)}) ... ')
|
||||||
|
epoch_random = random.Random(int(random_seed))
|
||||||
|
epoch_random.shuffle(image_records)
|
||||||
|
|
||||||
|
# Udpate learning rate
|
||||||
|
if learning_rates:
|
||||||
|
for learning_rate_per_epoch in learning_rates:
|
||||||
|
if learning_rate_per_epoch['used_epoch'] <= int(used_epoch):
|
||||||
|
learning_rate = learning_rate_per_epoch['learning_rate']
|
||||||
|
print(f'Trying to change learning rate to {learning_rate} ...')
|
||||||
|
optimizer.learning_rate.assign(learning_rate)
|
||||||
|
print(f'Learning rate is changed to {optimizer.learning_rate} ...')
|
||||||
|
|
||||||
|
while int(offset) < epoch_size:
|
||||||
|
image_records_slice = image_records[int(offset):min(
|
||||||
|
int(offset) + slice_size, epoch_size)]
|
||||||
|
|
||||||
|
image_paths = [image_record[0]
|
||||||
|
for image_record in image_records_slice]
|
||||||
|
tag_strings = [image_record[1]
|
||||||
|
for image_record in image_records_slice]
|
||||||
|
|
||||||
|
dataset_wrapper = dd.data.DatasetWrapper(
|
||||||
|
(image_paths, tag_strings), tags, width, height, scale_range=scale_range, rotation_range=rotation_range, shift_range=shift_range)
|
||||||
|
dataset = dataset_wrapper.get_dataset(minibatch_size)
|
||||||
|
|
||||||
|
for (x_train, y_train) in dataset:
|
||||||
|
sample_count = x_train.shape[0]
|
||||||
|
|
||||||
|
step_result = model.train_on_batch(
|
||||||
|
x_train, y_train, reset_metrics=False)
|
||||||
|
|
||||||
|
used_minibatch.assign_add(1)
|
||||||
|
used_sample.assign_add(sample_count)
|
||||||
|
used_sample_sum += sample_count
|
||||||
|
loss_sum += step_result[0]
|
||||||
|
loss_count += 1
|
||||||
|
|
||||||
|
if int(used_minibatch) % console_logging_frequency_mb == 0:
|
||||||
|
# calculate logging informations
|
||||||
|
current_time = time.time()
|
||||||
|
delta_time = current_time - last_time
|
||||||
|
step_metric_precision = step_result[1]
|
||||||
|
step_metric_recall = step_result[2]
|
||||||
|
if step_metric_precision + step_metric_recall > 0.0:
|
||||||
|
step_metric_f1_score = 2.0 * \
|
||||||
|
(step_metric_precision * step_metric_recall) / \
|
||||||
|
(step_metric_precision + step_metric_recall)
|
||||||
|
else:
|
||||||
|
step_metric_f1_score = 0.0
|
||||||
|
average_loss = loss_sum / float(loss_count)
|
||||||
|
samples_per_seconds = float(
|
||||||
|
used_sample_sum) / max(delta_time, 0.001)
|
||||||
|
progress = float(int(used_sample)) / \
|
||||||
|
float(epoch_size * epoch_count) * 100.0
|
||||||
|
remain_seconds = float(
|
||||||
|
epoch_size * epoch_count - int(used_sample)) / max(samples_per_seconds, 0.001)
|
||||||
|
eta_datetime = datetime.datetime.now() + datetime.timedelta(seconds=remain_seconds)
|
||||||
|
eta_datetime_string = eta_datetime.strftime(
|
||||||
|
'%Y-%m-%d %H:%M:%S')
|
||||||
|
print(
|
||||||
|
f'Epoch[{int(used_epoch)}] Loss={average_loss:.6f}, P={step_metric_precision:.6f}, R={step_metric_recall:.6f}, F1={step_metric_f1_score:.6f}, Speed = {samples_per_seconds:.1f} samples/s, {progress:.2f} %, ETA = {eta_datetime_string}')
|
||||||
|
|
||||||
|
# reset for next logging
|
||||||
|
model.reset_metrics()
|
||||||
|
loss_sum = 0.0
|
||||||
|
loss_count = 0
|
||||||
|
used_sample_sum = 0
|
||||||
|
last_time = current_time
|
||||||
|
|
||||||
|
offset.assign_add(slice_size)
|
||||||
|
print(f'Saving checkpoint ... ({datetime.datetime.now()})')
|
||||||
|
manager.save()
|
||||||
|
|
||||||
|
used_epoch.assign_add(1)
|
||||||
|
random_seed.assign_add(1)
|
||||||
|
offset.assign(0)
|
||||||
|
|
||||||
|
if int(used_epoch) % export_model_per_epoch == 0:
|
||||||
|
print('Saving model ... (per epoch {export_model_per_epoch})')
|
||||||
|
export_path = os.path.join(
|
||||||
|
project_path, f'model-{model_type}.h5.e{int(used_epoch)}')
|
||||||
|
model.save(export_path, include_optimizer=False, save_format='h5')
|
||||||
|
|
||||||
|
print('Saving model ...')
|
||||||
|
model_path = os.path.join(
|
||||||
|
project_path, f'model-{model_type}.h5')
|
||||||
|
|
||||||
|
# tf.keras.experimental.export_saved_model throw exception now
|
||||||
|
# see https://github.com/tensorflow/tensorflow/issues/27112
|
||||||
|
model.save(model_path, include_optimizer=False)
|
||||||
|
|
||||||
|
print('Training is complete.')
|
||||||
|
print(
|
||||||
|
f'used_epoch={int(used_epoch)}, used_minibatch={int(used_minibatch)}, used_sample={int(used_sample)}')
|
|
@ -0,0 +1,29 @@
|
||||||
|
from typing import Any, Union
|
||||||
|
|
||||||
|
import six
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
import deepdanbooru as dd
|
||||||
|
|
||||||
|
from .dataset import load_image_records, load_tags
|
||||||
|
from .dataset_wrapper import DatasetWrapper
|
||||||
|
|
||||||
|
|
||||||
|
def load_image_for_evaluate(
|
||||||
|
input_: Union[str, six.BytesIO], width: int, height: int, normalize: bool = True
|
||||||
|
) -> Any:
|
||||||
|
if isinstance(input_, six.BytesIO):
|
||||||
|
image_raw = input_.getvalue()
|
||||||
|
else:
|
||||||
|
image_raw = tf.io.read_file(input_)
|
||||||
|
image = tf.io.decode_png(image_raw, channels=3)
|
||||||
|
|
||||||
|
image = tf.image.resize(
|
||||||
|
image, size=(height, width), method=tf.image.ResizeMethod.AREA, preserve_aspect_ratio=True)
|
||||||
|
image = image.numpy() # EagerTensor to np.array
|
||||||
|
image = dd.image.transform_and_pad_image(image, width, height)
|
||||||
|
|
||||||
|
if normalize:
|
||||||
|
image = image / 255.0
|
||||||
|
|
||||||
|
return image
|
|
@ -0,0 +1,40 @@
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
def load_tags(tags_path):
|
||||||
|
with open(tags_path, 'r') as tags_stream:
|
||||||
|
tags = [tag for tag in (tag.strip() for tag in tags_stream) if tag]
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
def load_image_records(sqlite_path, minimum_tag_count):
|
||||||
|
if not os.path.exists(sqlite_path):
|
||||||
|
raise Exception(f'SQLite database is not exists : {sqlite_path}')
|
||||||
|
|
||||||
|
connection = sqlite3.connect(sqlite_path)
|
||||||
|
connection.row_factory = sqlite3.Row
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
image_folder_path = os.path.join(os.path.dirname(sqlite_path), 'images')
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT md5, file_ext, tag_string FROM posts WHERE (file_ext = 'png' OR file_ext = 'jpg' OR file_ext = 'jpeg') AND (tag_count_general >= ?) ORDER BY id",
|
||||||
|
(minimum_tag_count,))
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
image_records = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
md5 = row['md5']
|
||||||
|
extension = row['file_ext']
|
||||||
|
image_path = os.path.join(
|
||||||
|
image_folder_path, md5[0:2], f'{md5}.{extension}')
|
||||||
|
tag_string = row['tag_string']
|
||||||
|
|
||||||
|
image_records.append((image_path, tag_string))
|
||||||
|
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
return image_records
|
|
@ -0,0 +1,98 @@
|
||||||
|
import random
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
import deepdanbooru as dd
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetWrapper:
|
||||||
|
"""
|
||||||
|
Wrapper class for data pipelining/augmentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, inputs, tags, width, height, scale_range, rotation_range, shift_range):
|
||||||
|
self.inputs = inputs
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.scale_range = scale_range
|
||||||
|
self.rotation_range = rotation_range
|
||||||
|
self.shift_range = shift_range
|
||||||
|
self.tag_all_array = np.array(tags)
|
||||||
|
|
||||||
|
def get_dataset(self, minibatch_size):
|
||||||
|
dataset = tf.data.Dataset.from_tensor_slices(self.inputs)
|
||||||
|
dataset = dataset.map(
|
||||||
|
self.map_load_image, num_parallel_calls=tf.data.experimental.AUTOTUNE)
|
||||||
|
dataset = dataset.apply(tf.data.experimental.ignore_errors())
|
||||||
|
dataset = dataset.map(
|
||||||
|
self.map_transform_image_and_label, num_parallel_calls=tf.data.experimental.AUTOTUNE)
|
||||||
|
dataset = dataset.batch(minibatch_size)
|
||||||
|
dataset = dataset.prefetch(
|
||||||
|
buffer_size=tf.data.experimental.AUTOTUNE)
|
||||||
|
# dataset = dataset.apply(
|
||||||
|
# tf.data.experimental.prefetch_to_device('/device:GPU:0'))
|
||||||
|
|
||||||
|
return dataset
|
||||||
|
|
||||||
|
def map_load_image(self, image_path, tag_string):
|
||||||
|
image_raw = tf.io.read_file(image_path)
|
||||||
|
image = tf.io.decode_png(image_raw, channels=3)
|
||||||
|
|
||||||
|
if self.scale_range:
|
||||||
|
pre_scale = self.scale_range[1]
|
||||||
|
else:
|
||||||
|
pre_scale = 1.0
|
||||||
|
|
||||||
|
size = (int(self.height * pre_scale), int(self.width * pre_scale))
|
||||||
|
|
||||||
|
image = tf.image.resize(
|
||||||
|
image, size=size, method=tf.image.ResizeMethod.AREA, preserve_aspect_ratio=True)
|
||||||
|
|
||||||
|
return (image, tag_string)
|
||||||
|
|
||||||
|
def map_transform_image_and_label(self, image, tag_string):
|
||||||
|
return tf.py_function(self.map_transform_image_and_label_py, (image, tag_string), (tf.float32, tf.float32))
|
||||||
|
|
||||||
|
def map_transform_image_and_label_py(self, image, tag_string):
|
||||||
|
# transform image
|
||||||
|
image = image.numpy()
|
||||||
|
|
||||||
|
if self.scale_range:
|
||||||
|
scale = random.uniform(
|
||||||
|
self.scale_range[0], self.scale_range[1]) * (1.0 / self.scale_range[1])
|
||||||
|
else:
|
||||||
|
scale = None
|
||||||
|
|
||||||
|
if self.rotation_range:
|
||||||
|
rotation = random.uniform(
|
||||||
|
self.rotation_range[0], self.rotation_range[1])
|
||||||
|
else:
|
||||||
|
rotation = None
|
||||||
|
|
||||||
|
if self.shift_range:
|
||||||
|
shift_x = random.uniform(self.shift_range[0], self.shift_range[1])
|
||||||
|
shift_y = random.uniform(self.shift_range[0], self.shift_range[1])
|
||||||
|
shift = (shift_x, shift_y)
|
||||||
|
else:
|
||||||
|
shift = None
|
||||||
|
|
||||||
|
image = dd.image.transform_and_pad_image(
|
||||||
|
image=image,
|
||||||
|
target_width=self.width,
|
||||||
|
target_height=self.height,
|
||||||
|
rotation=rotation,
|
||||||
|
scale=scale,
|
||||||
|
shift=shift)
|
||||||
|
|
||||||
|
image = image / 255.0 # normalize to 0~1
|
||||||
|
# image = image.astype(np.float32)
|
||||||
|
|
||||||
|
# transform tag
|
||||||
|
tag_string = tag_string.numpy().decode()
|
||||||
|
tag_array = np.array(tag_string.split(' '))
|
||||||
|
|
||||||
|
labels = np.where(np.isin(self.tag_all_array,
|
||||||
|
tag_array), 1, 0).astype(np.float32)
|
||||||
|
|
||||||
|
return (image, labels)
|
|
@ -0,0 +1,18 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def atoi(text):
|
||||||
|
return int(text) if text.isdigit() else text
|
||||||
|
|
||||||
|
|
||||||
|
def natural_keys(text):
|
||||||
|
"""
|
||||||
|
alist.sort(key=natural_keys) sorts in human order
|
||||||
|
http://nedbatchelder.com/blog/200712/human_sorting.html
|
||||||
|
(See Toothy's implementation in the comments)
|
||||||
|
"""
|
||||||
|
return [atoi(c) for c in re.split(r'(\d+)', text)]
|
||||||
|
|
||||||
|
|
||||||
|
def natural_sorted(iterable):
|
||||||
|
return sorted(iterable, key=natural_keys)
|
|
@ -0,0 +1,48 @@
|
||||||
|
import tensorflow as tf
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# gpus = tf.config.experimental.list_physical_devices('GPU')
|
||||||
|
|
||||||
|
# if gpus:
|
||||||
|
# try:
|
||||||
|
# for gpu in gpus:
|
||||||
|
# tf.config.experimental.set_memory_growth(gpu, True)
|
||||||
|
# except RuntimeError as e:
|
||||||
|
# print(e)
|
||||||
|
|
||||||
|
|
||||||
|
def grad(y, x):
|
||||||
|
V = tf.keras.layers.Lambda(lambda z: tf.gradients(
|
||||||
|
z[0], z[1]), output_shape=[1])([y, x])
|
||||||
|
return V
|
||||||
|
|
||||||
|
|
||||||
|
def grad_cam_test(model, x, some_variable):
|
||||||
|
fixed_input = model.inputs
|
||||||
|
fixed_output = tf.keras.layers.Lambda(lambda z: tf.keras.backend.gradients(
|
||||||
|
z[0], z[1]), output_shape=[2])([model.inputs[0], model.outputs[0]])
|
||||||
|
|
||||||
|
grad_model = tf.keras.Model(inputs=fixed_input, outputs=fixed_output)
|
||||||
|
|
||||||
|
return grad_model.predict(x)
|
||||||
|
|
||||||
|
|
||||||
|
def run_test():
|
||||||
|
# Generate sample model
|
||||||
|
x = tf.keras.Input(shape=(2))
|
||||||
|
y = tf.keras.layers.Dense(2)(x)
|
||||||
|
model = tf.keras.Model(inputs=x, outputs=y)
|
||||||
|
target = np.array([[1.0, 2.0]], dtype=np.float32)
|
||||||
|
|
||||||
|
# Calculate gradient using numpy array
|
||||||
|
input_numpy = np.array([[0.0, 0.0]])
|
||||||
|
grad_output_numpy = grad_cam_test(model, input_numpy, target)
|
||||||
|
print(f'numpy: {grad_output_numpy}')
|
||||||
|
|
||||||
|
# Calculate gradient using tf.Variable
|
||||||
|
input_variable = tf.constant([[0.0, 0.0]])
|
||||||
|
grad_output_variable = grad_cam_test(model, input_variable, target)
|
||||||
|
print(f'variable: {grad_output_variable}')
|
||||||
|
|
||||||
|
|
||||||
|
run_test()
|
|
@ -0,0 +1,56 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import skimage.transform
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_image_scale(source_width, source_height, target_width, target_height):
|
||||||
|
"""
|
||||||
|
Calculate scale for image resizing while preserving aspect ratio.
|
||||||
|
"""
|
||||||
|
if source_width == target_width and source_height == target_height:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
source_ratio = source_width / source_height
|
||||||
|
target_ratio = target_width / target_height
|
||||||
|
|
||||||
|
if target_ratio < source_ratio:
|
||||||
|
scale = target_width / source_width
|
||||||
|
else:
|
||||||
|
scale = target_height / source_height
|
||||||
|
|
||||||
|
return scale
|
||||||
|
|
||||||
|
|
||||||
|
def transform_and_pad_image(image, target_width, target_height, scale=None, rotation=None, shift=None, order=1, mode='edge'):
|
||||||
|
"""
|
||||||
|
Transform image and pad by edge pixles.
|
||||||
|
"""
|
||||||
|
image_width = image.shape[1]
|
||||||
|
image_height = image.shape[0]
|
||||||
|
image_array = image
|
||||||
|
|
||||||
|
# centerize
|
||||||
|
t = skimage.transform.AffineTransform(
|
||||||
|
translation=(-image_width * 0.5, -image_height * 0.5))
|
||||||
|
|
||||||
|
if scale:
|
||||||
|
t += skimage.transform.AffineTransform(scale=(scale, scale))
|
||||||
|
|
||||||
|
if rotation:
|
||||||
|
radian = (rotation / 180.0) * math.pi
|
||||||
|
t += skimage.transform.AffineTransform(rotation=radian)
|
||||||
|
|
||||||
|
t += skimage.transform.AffineTransform(
|
||||||
|
translation=(target_width * 0.5, target_height * 0.5))
|
||||||
|
|
||||||
|
if shift:
|
||||||
|
t += skimage.transform.AffineTransform(
|
||||||
|
translation=(target_width * shift[0], target_height * shift[1]))
|
||||||
|
|
||||||
|
warp_shape = (target_height, target_width)
|
||||||
|
|
||||||
|
image_array = skimage.transform.warp(
|
||||||
|
image_array, (t).inverse, output_shape=warp_shape, order=order, mode=mode)
|
||||||
|
|
||||||
|
return image_array
|
|
@ -0,0 +1,28 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_as_json(target_object, path, encoding='utf-8'):
|
||||||
|
with open(path, 'w', encoding=encoding) as stream:
|
||||||
|
stream.write(json.dumps(target_object, indent=4, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
def deserialize_from_json(path, encoding='utf-8'):
|
||||||
|
with open(path, 'r', encoding=encoding) as stream:
|
||||||
|
return json.loads(stream.read())
|
||||||
|
|
||||||
|
|
||||||
|
def try_create_directory(path):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
os.makedirs(path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_paths_in_directory(path, patterns):
|
||||||
|
return [str(file_path) for pattern in patterns for file_path in Path(path).rglob(pattern)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_file_paths_recursive(folder_path, patterns_string):
|
||||||
|
patterns = patterns_string.split(',')
|
||||||
|
|
||||||
|
return get_file_paths_in_directory(folder_path, patterns)
|
|
@ -0,0 +1,8 @@
|
||||||
|
import deepdanbooru.model.layers
|
||||||
|
import deepdanbooru.model.losses
|
||||||
|
|
||||||
|
from .resnet import create_resnet_152
|
||||||
|
from .resnet import create_resnet_custom_v1
|
||||||
|
from .resnet import create_resnet_custom_v2
|
||||||
|
from .resnet import create_resnet_custom_v3
|
||||||
|
from .resnet import create_resnet_custom_v4
|
|
@ -0,0 +1,60 @@
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
|
||||||
|
def conv(x, filters, kernel_size, strides=(1, 1), padding='same', initializer='he_normal'):
|
||||||
|
c = tf.keras.layers.Conv2D(
|
||||||
|
filters=filters, kernel_size=kernel_size, strides=strides, padding=padding, kernel_initializer=initializer, use_bias=False)(x)
|
||||||
|
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def conv_bn(x, filters, kernel_size, strides=(1, 1), padding='same', initializer='he_normal', bn_gamma_initializer='ones'):
|
||||||
|
c = conv(x, filters=filters, kernel_size=kernel_size,
|
||||||
|
strides=strides, padding=padding, initializer=initializer)
|
||||||
|
|
||||||
|
c_bn = tf.keras.layers.BatchNormalization(
|
||||||
|
gamma_initializer=bn_gamma_initializer)(c)
|
||||||
|
|
||||||
|
return c_bn
|
||||||
|
|
||||||
|
|
||||||
|
def conv_bn_relu(x, filters, kernel_size, strides=(1, 1), padding='same', initializer='he_normal', bn_gamma_initializer='ones'):
|
||||||
|
c_bn = conv_bn(x, filters=filters, kernel_size=kernel_size, strides=strides, padding=padding,
|
||||||
|
initializer=initializer, bn_gamma_initializer=bn_gamma_initializer)
|
||||||
|
|
||||||
|
return tf.keras.layers.Activation('relu')(c_bn)
|
||||||
|
|
||||||
|
|
||||||
|
def conv_gap(x, output_filters, kernel_size=(1, 1)):
|
||||||
|
x = conv(x, filters=output_filters, kernel_size=kernel_size)
|
||||||
|
x = tf.keras.layers.GlobalAveragePooling2D()(x)
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def repeat_blocks(x, block_delegate, count, **kwargs):
|
||||||
|
assert count >= 0
|
||||||
|
|
||||||
|
for _ in range(count):
|
||||||
|
x = block_delegate(x, **kwargs)
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def squeeze_excitation(x, reduction=16):
|
||||||
|
"""
|
||||||
|
Squeeze-Excitation layer from https://arxiv.org/abs/1709.01507
|
||||||
|
"""
|
||||||
|
output_filters = x.shape[-1]
|
||||||
|
|
||||||
|
assert output_filters // reduction > 0
|
||||||
|
|
||||||
|
s = x
|
||||||
|
|
||||||
|
s = tf.keras.layers.GlobalAveragePooling2D()(s)
|
||||||
|
s = tf.keras.layers.Dense(
|
||||||
|
output_filters//reduction, activation='relu')(x)
|
||||||
|
s = tf.keras.layers.Dense(
|
||||||
|
output_filters, activation='sigmoid')(x)
|
||||||
|
x = tf.keras.layers.Multiply()([x, s])
|
||||||
|
|
||||||
|
return x
|
|
@ -0,0 +1,24 @@
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
|
||||||
|
def focal_loss(alpha=0.25, gamma=2.0, epsilon=1e-6):
|
||||||
|
def loss(y_true, y_pred):
|
||||||
|
value = -alpha * y_true * tf.math.pow(1.0 - y_pred, gamma) * tf.math.log(y_pred + epsilon) - (
|
||||||
|
1.0 - alpha) * (1.0 - y_true) * tf.math.pow(y_pred, gamma) * tf.math.log(1.0 - y_pred + epsilon)
|
||||||
|
|
||||||
|
return tf.math.reduce_sum(value)
|
||||||
|
|
||||||
|
return loss
|
||||||
|
|
||||||
|
|
||||||
|
def binary_crossentropy(epsilon=1e-6):
|
||||||
|
def loss(y_true, y_pred):
|
||||||
|
clipped_y_pred = tf.clip_by_value(y_pred, epsilon, tf.float32.max)
|
||||||
|
clipped_y_pred_nega = tf.clip_by_value(
|
||||||
|
1.0 - y_pred, epsilon, tf.float32.max)
|
||||||
|
value = (-y_true) * tf.math.log(clipped_y_pred) - \
|
||||||
|
(1.0 - y_true) * tf.math.log(clipped_y_pred_nega)
|
||||||
|
|
||||||
|
return tf.math.reduce_sum(value)
|
||||||
|
|
||||||
|
return loss
|
|
@ -0,0 +1,190 @@
|
||||||
|
import numpy as np
|
||||||
|
import tensorflow as tf
|
||||||
|
import deepdanbooru as dd
|
||||||
|
|
||||||
|
|
||||||
|
def resnet_bottleneck_block(x, output_filters, inter_filters, activation=True, se=False):
|
||||||
|
c1 = dd.model.layers.conv_bn_relu(x, inter_filters, (1, 1))
|
||||||
|
c2 = dd.model.layers.conv_bn_relu(c1, inter_filters, (3, 3))
|
||||||
|
c3 = dd.model.layers.conv_bn(
|
||||||
|
c2, output_filters, (1, 1), bn_gamma_initializer='zeros')
|
||||||
|
|
||||||
|
if se:
|
||||||
|
c3 = dd.model.layers.squeeze_excitation(c3)
|
||||||
|
|
||||||
|
p = tf.keras.layers.Add()([c3, x])
|
||||||
|
|
||||||
|
if activation:
|
||||||
|
return tf.keras.layers.Activation('relu')(p)
|
||||||
|
else:
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def resnet_bottleneck_inc_block(x, output_filters, inter_filters, strides1x1=(1, 1), strides2x2=(2, 2), se=False):
|
||||||
|
c1 = dd.model.layers.conv_bn_relu(
|
||||||
|
x, inter_filters, (1, 1), strides=strides1x1)
|
||||||
|
c2 = dd.model.layers.conv_bn_relu(
|
||||||
|
c1, inter_filters, (3, 3), strides=strides2x2)
|
||||||
|
c3 = dd.model.layers.conv_bn(
|
||||||
|
c2, output_filters, (1, 1), bn_gamma_initializer='zeros')
|
||||||
|
|
||||||
|
if se:
|
||||||
|
c3 = dd.model.layers.squeeze_excitation(c3)
|
||||||
|
|
||||||
|
strides = np.multiply(strides1x1, strides2x2)
|
||||||
|
s = dd.model.layers.conv_bn(
|
||||||
|
x, output_filters, (1, 1), strides=strides) # shortcut
|
||||||
|
|
||||||
|
p = tf.keras.layers.Add()([c3, s])
|
||||||
|
|
||||||
|
return tf.keras.layers.Activation('relu')(p)
|
||||||
|
|
||||||
|
|
||||||
|
def resnet_original_bottleneck_model(x, filter_sizes, repeat_sizes, final_pool=True, se=False):
|
||||||
|
"""
|
||||||
|
https://github.com/Microsoft/CNTK/blob/master/Examples/Image/Classification/ResNet/Python/resnet_models.py
|
||||||
|
"""
|
||||||
|
assert len(filter_sizes) == len(repeat_sizes)
|
||||||
|
|
||||||
|
x = dd.model.layers.conv_bn_relu(
|
||||||
|
x, filter_sizes[0] // 4, (7, 7), strides=(2, 2))
|
||||||
|
x = tf.keras.layers.MaxPool2D((3, 3), strides=(
|
||||||
|
2, 2), padding='same')(x)
|
||||||
|
|
||||||
|
for i in range(len(repeat_sizes)):
|
||||||
|
x = resnet_bottleneck_inc_block(
|
||||||
|
x=x,
|
||||||
|
output_filters=filter_sizes[i],
|
||||||
|
inter_filters=filter_sizes[i] // 4,
|
||||||
|
strides2x2=(2, 2) if i > 0 else (1, 1),
|
||||||
|
se=se)
|
||||||
|
x = dd.model.layers.repeat_blocks(
|
||||||
|
x=x,
|
||||||
|
block_delegate=resnet_bottleneck_block,
|
||||||
|
count=repeat_sizes[i],
|
||||||
|
output_filters=filter_sizes[i],
|
||||||
|
inter_filters=filter_sizes[i] // 4,
|
||||||
|
se=se)
|
||||||
|
|
||||||
|
if final_pool:
|
||||||
|
x = tf.keras.layers.AveragePooling2D((7, 7), name='ap_final')(x)
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def resnet_longterm_bottleneck_model(x, filter_sizes, repeat_sizes, final_pool=True, se=False):
|
||||||
|
"""
|
||||||
|
Add long-term shortcut.
|
||||||
|
"""
|
||||||
|
assert len(filter_sizes) == len(repeat_sizes)
|
||||||
|
|
||||||
|
x = dd.model.layers.conv_bn_relu(
|
||||||
|
x, filter_sizes[0] // 4, (7, 7), strides=(2, 2))
|
||||||
|
x = tf.keras.layers.MaxPool2D((3, 3), strides=(
|
||||||
|
2, 2), padding='same')(x)
|
||||||
|
|
||||||
|
for i in range(len(repeat_sizes)):
|
||||||
|
x = resnet_bottleneck_inc_block(
|
||||||
|
x=x,
|
||||||
|
output_filters=filter_sizes[i],
|
||||||
|
inter_filters=filter_sizes[i] // 4,
|
||||||
|
strides2x2=(2, 2) if i > 0 else (1, 1),
|
||||||
|
se=se)
|
||||||
|
x_1 = dd.model.layers.repeat_blocks(
|
||||||
|
x=x,
|
||||||
|
block_delegate=resnet_bottleneck_block,
|
||||||
|
count=repeat_sizes[i] - 1,
|
||||||
|
output_filters=filter_sizes[i],
|
||||||
|
inter_filters=filter_sizes[i] // 4,
|
||||||
|
se=se)
|
||||||
|
x_1 = resnet_bottleneck_block(
|
||||||
|
x_1,
|
||||||
|
output_filters=filter_sizes[i],
|
||||||
|
inter_filters=filter_sizes[i] // 4,
|
||||||
|
activation=False,
|
||||||
|
se=se)
|
||||||
|
|
||||||
|
x = tf.keras.layers.Add()([x_1, x]) # long-term shortcut
|
||||||
|
x = tf.keras.layers.Activation('relu')(x)
|
||||||
|
|
||||||
|
if final_pool:
|
||||||
|
x = tf.keras.layers.AveragePooling2D((7, 7), name='ap_final')(x)
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def create_resnet_152(x, output_dim):
|
||||||
|
"""
|
||||||
|
Original ResNet-152 Model.
|
||||||
|
"""
|
||||||
|
filter_sizes = [256, 512, 1024, 2048]
|
||||||
|
repeat_sizes = [2, 7, 35, 2]
|
||||||
|
|
||||||
|
x = resnet_original_bottleneck_model(
|
||||||
|
x, filter_sizes=filter_sizes, repeat_sizes=repeat_sizes)
|
||||||
|
|
||||||
|
x = tf.keras.layers.Flatten()(x)
|
||||||
|
x = tf.keras.layers.Dense(output_dim)(x)
|
||||||
|
x = tf.keras.layers.Activation('sigmoid')(x)
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def create_resnet_custom_v1(x, output_dim):
|
||||||
|
"""
|
||||||
|
DeepDanbooru web (until 2019/04/20)
|
||||||
|
Short, wide
|
||||||
|
"""
|
||||||
|
filter_sizes = [256, 512, 1024, 2048, 4096]
|
||||||
|
repeat_sizes = [2, 7, 35, 2, 2]
|
||||||
|
|
||||||
|
x = resnet_original_bottleneck_model(
|
||||||
|
x, filter_sizes=filter_sizes, repeat_sizes=repeat_sizes, final_pool=False)
|
||||||
|
|
||||||
|
x = dd.model.layers.conv_gap(x, output_dim)
|
||||||
|
x = tf.keras.layers.Activation('sigmoid')(x)
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def create_resnet_custom_v2(x, output_dim):
|
||||||
|
"""
|
||||||
|
Experimental (blazing-deep network)
|
||||||
|
Deep, narrow
|
||||||
|
"""
|
||||||
|
filter_sizes = [256, 512, 1024, 1024, 1024, 2048]
|
||||||
|
repeat_sizes = [2, 7, 40, 16, 16, 6]
|
||||||
|
|
||||||
|
x = resnet_original_bottleneck_model(
|
||||||
|
x, filter_sizes=filter_sizes, repeat_sizes=repeat_sizes, final_pool=False)
|
||||||
|
|
||||||
|
x = dd.model.layers.conv_gap(x, output_dim)
|
||||||
|
x = tf.keras.layers.Activation('sigmoid')(x)
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def create_resnet_custom_v3(x, output_dim):
|
||||||
|
filter_sizes = [256, 512, 1024, 1024, 2048, 4096]
|
||||||
|
repeat_sizes = [2, 7, 19, 19, 2, 2]
|
||||||
|
|
||||||
|
x = resnet_original_bottleneck_model(
|
||||||
|
x, filter_sizes=filter_sizes, repeat_sizes=repeat_sizes, final_pool=False)
|
||||||
|
|
||||||
|
x = dd.model.layers.conv_gap(x, output_dim)
|
||||||
|
x = tf.keras.layers.Activation('sigmoid')(x)
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def create_resnet_custom_v4(x, output_dim):
|
||||||
|
filter_sizes = [256, 512, 1024, 1024, 1024, 2048]
|
||||||
|
repeat_sizes = [2, 7, 10, 10, 10, 2]
|
||||||
|
|
||||||
|
x = resnet_original_bottleneck_model(
|
||||||
|
x, filter_sizes=filter_sizes, repeat_sizes=repeat_sizes, final_pool=False)
|
||||||
|
|
||||||
|
x = dd.model.layers.conv_gap(x, output_dim)
|
||||||
|
x = tf.keras.layers.Activation('sigmoid')(x)
|
||||||
|
|
||||||
|
return x
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .project import DEFAULT_PROJECT_CONTEXT
|
||||||
|
from .project import load_project
|
||||||
|
from .project import load_model_from_project
|
||||||
|
from .project import load_tags_from_project
|
|
@ -0,0 +1,50 @@
|
||||||
|
import os
|
||||||
|
import deepdanbooru as dd
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
DEFAULT_PROJECT_CONTEXT = {
|
||||||
|
'image_width': 299,
|
||||||
|
'image_height': 299,
|
||||||
|
'database_path': None,
|
||||||
|
'minimum_tag_count': 20,
|
||||||
|
'model': 'resnet_custom_v2',
|
||||||
|
'minibatch_size': 32,
|
||||||
|
'epoch_count': 10,
|
||||||
|
'export_model_per_epoch': 10,
|
||||||
|
'checkpoint_frequency_mb': 200,
|
||||||
|
'console_logging_frequency_mb': 10,
|
||||||
|
'optimizer': 'adam',
|
||||||
|
'learning_rate': 0.001,
|
||||||
|
'rotation_range': [0.0, 360.0],
|
||||||
|
'scale_range': [0.9, 1.1],
|
||||||
|
'shift_range': [-0.1, 0.1]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_project(project_path):
|
||||||
|
project_context_path = os.path.join(project_path, 'project.json')
|
||||||
|
project_context = dd.io.deserialize_from_json(project_context_path)
|
||||||
|
tags = dd.data.load_tags_from_project(project_path)
|
||||||
|
|
||||||
|
model_type = project_context['model']
|
||||||
|
model_path = os.path.join(project_path, f'model-{model_type}.h5')
|
||||||
|
model = tf.keras.models.load_model(model_path)
|
||||||
|
|
||||||
|
return project_context, model, tags
|
||||||
|
|
||||||
|
|
||||||
|
def load_model_from_project(project_path, compile_model=True):
|
||||||
|
project_context_path = os.path.join(project_path, 'project.json')
|
||||||
|
project_context = dd.io.deserialize_from_json(project_context_path)
|
||||||
|
|
||||||
|
model_type = project_context['model']
|
||||||
|
model_path = os.path.join(project_path, f'model-{model_type}.h5')
|
||||||
|
model = tf.keras.models.load_model(model_path, compile=compile_model)
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def load_tags_from_project(project_path):
|
||||||
|
tags_path = os.path.join(project_path, 'tags.txt')
|
||||||
|
|
||||||
|
return dd.data.load_tags(tags_path)
|
|
@ -0,0 +1,15 @@
|
||||||
|
@echo off
|
||||||
|
:start
|
||||||
|
set RETRY_COUNT=0
|
||||||
|
set TF_CPP_MIN_LOG_LEVEL=2
|
||||||
|
|
||||||
|
:run
|
||||||
|
call %* || goto :wait
|
||||||
|
pause
|
||||||
|
exit /B 0
|
||||||
|
|
||||||
|
:wait
|
||||||
|
echo %date% %time% Retry %RETRY_COUNT%
|
||||||
|
set /a RETRY_COUNT=%RETRY_COUNT%+1
|
||||||
|
timeout /t 3 /nobreak
|
||||||
|
goto :run
|
|
@ -0,0 +1,14 @@
|
||||||
|
[mypy]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 260
|
||||||
|
exclude =
|
||||||
|
.tox,
|
||||||
|
venv
|
||||||
|
ignore =
|
||||||
|
# E226 missing whitespace around arithmetic operator
|
||||||
|
# F401 'X' imported but unused
|
||||||
|
# W503 line break before binary operator
|
||||||
|
E226,
|
||||||
|
F401,
|
||||||
|
W503,
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
with open("README.md", "r", encoding='utf-8') as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
|
||||||
|
with open('deepdanbooru/__main__.py', encoding='utf-8') as f:
|
||||||
|
version = re.search('__version__ = \'([^\']+)\'', f.read()).group(1) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
install_requires = [
|
||||||
|
]
|
||||||
|
tensorflow_pkg = 'tensorflow>=2.1.0'
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name="deepdanbooru",
|
||||||
|
version=version,
|
||||||
|
author="Kichang Kim",
|
||||||
|
author_email="admin@kanotype.net",
|
||||||
|
description="DeepDanbooru is AI based multi-label girl image classification system, "
|
||||||
|
"implemented by using TensorFlow.",
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="https://github.com/KichangKim/DeepDanbooru",
|
||||||
|
packages=setuptools.find_packages(),
|
||||||
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
],
|
||||||
|
python_requires='>=3.6',
|
||||||
|
install_requires=install_requires,
|
||||||
|
extras_require={
|
||||||
|
'tensorflow': [tensorflow_pkg],
|
||||||
|
'test': ['pytest', 'flake8', 'mypy']
|
||||||
|
},
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"deepdanbooru=deepdanbooru.__main__:main",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
|
@ -9,6 +9,8 @@ RUN pip config set global.index-url https://pypi.mirrors.ustc.edu.cn/simple
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
RUN cd DeepDanbooru && python setup.py install
|
||||||
|
|
||||||
ENV HF_TOKEN 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
|
ENV HF_TOKEN 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
|
||||||
|
|
||||||
CMD ["python", "app.py"]
|
CMD ["python", "app.py"]
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
pillow>=9.0.0
|
pillow>=9.0.0
|
||||||
tensorflow>=2.7.0
|
tensorflow>=2.7.0
|
||||||
git+https://github.com/KichangKim/DeepDanbooru@v3-20200915-sgd-e30#egg=deepdanbooru
|
Click>=7.0
|
||||||
|
numpy>=1.16.2
|
||||||
|
scikit-image>=0.15.0
|
||||||
|
tensorflow>=2.1.0
|
||||||
|
requests>=2.22.0
|
||||||
|
six>=1.13.0
|
||||||
|
#git+https://github.com/KichangKim/DeepDanbooru@v3-20200915-sgd-e30#egg=deepdanbooru
|
||||||
|
|
Loading…
Reference in New Issue