2020-08-14 14:49:26 +00:00
from typing import Optional , List , Tuple
2020-08-13 15:38:30 +00:00
from enum import Enum
2020-08-02 13:18:30 +00:00
from pathlib import Path
2020-08-14 14:49:26 +00:00
from wasabi import Printer , diff_strings
from thinc . api import Config
2020-08-13 15:38:30 +00:00
import srsly
import re
2020-11-27 07:17:14 +00:00
from jinja2 import Template
2020-08-02 13:18:30 +00:00
2020-08-14 12:06:22 +00:00
from . . import util
2020-08-24 13:56:03 +00:00
from . . language import DEFAULT_CONFIG_PRETRAIN_PATH
2020-08-19 11:33:15 +00:00
from . . schemas import RecommendationSchema
2021-03-12 09:00:31 +00:00
from . _util import init_cli , Arg , Opt , show_validation_error , COMMAND
from . _util import string_to_list , import_code
2020-08-13 15:38:30 +00:00
2020-08-19 11:33:15 +00:00
ROOT = Path ( __file__ ) . parent / " templates "
TEMPLATE_PATH = ROOT / " quickstart_training.jinja "
RECOMMENDATIONS = srsly . read_yaml ( ROOT / " quickstart_training_recommendations.yml " )
2020-08-13 15:38:30 +00:00
class Optimizations ( str , Enum ) :
efficiency = " efficiency "
accuracy = " accuracy "
2020-08-02 13:18:30 +00:00
@init_cli.command ( " config " )
def init_config_cli (
# fmt: off
2020-08-21 10:06:19 +00:00
output_file : Path = Arg ( . . . , help = " File to save config.cfg to or - for stdout (will only output config and no additional logging info) " , allow_dash = True ) ,
2020-08-13 15:38:30 +00:00
lang : Optional [ str ] = Opt ( " en " , " --lang " , " -l " , help = " Two-letter code of the language to use " ) ,
2020-09-03 11:13:03 +00:00
pipeline : Optional [ str ] = Opt ( " tagger,parser,ner " , " --pipeline " , " -p " , help = " Comma-separated names of trainable pipeline components to include (without ' tok2vec ' or ' transformer ' ) " ) ,
2020-08-13 15:38:30 +00:00
optimize : Optimizations = Opt ( Optimizations . efficiency . value , " --optimize " , " -o " , help = " Whether to optimize for efficiency (faster inference, smaller model, lower memory consumption) or higher accuracy (potentially larger and slower model). This will impact the choice of architecture, pretrained weights and related hyperparameters. " ) ,
2020-12-09 00:00:51 +00:00
gpu : bool = Opt ( False , " --gpu " , " -G " , help = " Whether the model can run on GPU. This will impact the choice of architecture, pretrained weights and related hyperparameters. " ) ,
2020-09-17 14:05:40 +00:00
pretraining : bool = Opt ( False , " --pretraining " , " -pt " , help = " Include config for pretraining (with ' spacy pretrain ' ) " ) ,
2021-01-05 02:49:59 +00:00
force_overwrite : bool = Opt ( False , " --force " , " -F " , help = " Force overwriting the output file " ) ,
2020-08-02 13:18:30 +00:00
# fmt: on
) :
2020-08-13 15:38:30 +00:00
"""
Generate a starter config . cfg for training . Based on your requirements
specified via the CLI arguments , this command generates a config with the
2020-09-22 08:40:05 +00:00
optimal settings for your use case . This includes the choice of architecture ,
2020-08-13 15:38:30 +00:00
pretrained weights and related hyperparameters .
2020-09-04 10:58:50 +00:00
2021-01-30 09:09:38 +00:00
DOCS : https : / / spacy . io / api / cli #init-config
2020-08-13 15:38:30 +00:00
"""
if isinstance ( optimize , Optimizations ) : # instance of enum from the CLI
optimize = optimize . value
2020-09-12 12:43:22 +00:00
pipeline = string_to_list ( pipeline )
2020-12-08 23:57:55 +00:00
is_stdout = str ( output_file ) == " - "
2021-01-05 02:49:59 +00:00
if not is_stdout and output_file . exists ( ) and not force_overwrite :
msg = Printer ( )
msg . fail (
" The provided output file already exists. To force overwriting the config file, set the --force or -F flag. " ,
exits = 1 ,
)
2020-12-08 16:37:20 +00:00
config = init_config (
2020-09-17 14:57:02 +00:00
lang = lang ,
pipeline = pipeline ,
optimize = optimize ,
2020-12-09 00:00:51 +00:00
gpu = gpu ,
2020-09-17 14:57:02 +00:00
pretraining = pretraining ,
2020-12-08 23:57:55 +00:00
silent = is_stdout ,
2020-09-17 14:57:02 +00:00
)
2020-12-08 16:37:20 +00:00
save_config ( config , output_file , is_stdout = is_stdout )
2020-08-02 13:18:30 +00:00
2020-08-14 14:49:26 +00:00
@init_cli.command ( " fill-config " )
def init_fill_config_cli (
# fmt: off
base_path : Path = Arg ( . . . , help = " Base config to fill " , exists = True , dir_okay = False ) ,
output_file : Path = Arg ( " - " , help = " File to save config.cfg to (or - for stdout) " , allow_dash = True ) ,
2020-09-17 14:57:02 +00:00
pretraining : bool = Opt ( False , " --pretraining " , " -pt " , help = " Include config for pretraining (with ' spacy pretrain ' ) " ) ,
2021-03-12 09:00:31 +00:00
diff : bool = Opt ( False , " --diff " , " -D " , help = " Print a visual diff highlighting the changes " ) ,
code_path : Optional [ Path ] = Opt ( None , " --code-path " , " --code " , " -c " , help = " Path to Python file with additional code (registered functions) to be imported " ) ,
2020-08-14 14:49:26 +00:00
# fmt: on
) :
"""
Fill partial config . cfg with default values . Will add all missing settings
from the default config and will create all objects , check the registered
functions for their default values and update the base config . This command
can be used with a config generated via the training quickstart widget :
2021-01-30 09:09:38 +00:00
https : / / spacy . io / usage / training #quickstart
2020-09-04 10:58:50 +00:00
2021-01-30 09:09:38 +00:00
DOCS : https : / / spacy . io / api / cli #init-fill-config
2020-08-14 14:49:26 +00:00
"""
2021-03-12 09:00:31 +00:00
import_code ( code_path )
2020-08-24 13:56:03 +00:00
fill_config ( output_file , base_path , pretraining = pretraining , diff = diff )
2020-08-14 14:49:26 +00:00
def fill_config (
2020-09-01 10:07:04 +00:00
output_file : Path ,
base_path : Path ,
* ,
pretraining : bool = False ,
diff : bool = False ,
silent : bool = False ,
2020-08-14 14:49:26 +00:00
) - > Tuple [ Config , Config ] :
is_stdout = str ( output_file ) == " - "
2020-09-01 10:07:04 +00:00
no_print = is_stdout or silent
msg = Printer ( no_print = no_print )
2020-08-14 14:49:26 +00:00
with show_validation_error ( hint_fill = False ) :
2020-08-16 13:46:29 +00:00
config = util . load_config ( base_path )
2020-09-27 20:21:31 +00:00
nlp = util . load_model_from_config ( config , auto_fill = True , validate = False )
2020-08-24 20:53:47 +00:00
# Load a second time with validation to be extra sure that the produced
# config result is a valid config
2020-09-27 20:21:31 +00:00
nlp = util . load_model_from_config ( nlp . config )
2020-08-24 13:56:03 +00:00
filled = nlp . config
2021-02-14 03:02:14 +00:00
# If we have sourced components in the base config, those will have been
# replaced with their actual config after loading, so we have to re-add them
sourced = util . get_sourced_components ( config )
filled [ " components " ] . update ( sourced )
2020-08-24 13:56:03 +00:00
if pretraining :
validate_config_for_pretrain ( filled , msg )
pretrain_config = util . load_config ( DEFAULT_CONFIG_PRETRAIN_PATH )
filled = pretrain_config . merge ( filled )
2020-08-16 12:19:43 +00:00
before = config . to_str ( )
2020-08-24 13:56:03 +00:00
after = filled . to_str ( )
2020-08-16 12:19:43 +00:00
if before == after :
msg . warn ( " Nothing to auto-fill: base config is already complete " )
else :
msg . good ( " Auto-filled config with all values " )
2020-09-01 10:07:04 +00:00
if diff and not no_print :
2020-08-16 12:19:43 +00:00
if before == after :
msg . warn ( " No diff to show: nothing was auto-filled " )
else :
msg . divider ( " START CONFIG DIFF " )
print ( " " )
print ( diff_strings ( before , after ) )
msg . divider ( " END CONFIG DIFF " )
print ( " " )
2020-09-01 10:07:04 +00:00
save_config ( filled , output_file , is_stdout = is_stdout , silent = silent )
return config , filled
2020-08-14 14:49:26 +00:00
2020-08-02 13:18:30 +00:00
def init_config (
2020-09-17 14:57:02 +00:00
* ,
lang : str ,
pipeline : List [ str ] ,
optimize : str ,
2020-12-09 00:00:51 +00:00
gpu : bool ,
2020-09-17 14:57:02 +00:00
pretraining : bool = False ,
2020-12-08 23:57:55 +00:00
silent : bool = True ,
2020-12-08 16:37:20 +00:00
) - > Config :
2020-12-08 23:57:55 +00:00
msg = Printer ( no_print = silent )
2020-08-13 15:38:30 +00:00
with TEMPLATE_PATH . open ( " r " ) as f :
template = Template ( f . read ( ) )
2020-08-19 11:33:15 +00:00
# Filter out duplicates since tok2vec and transformer are added by template
pipeline = [ pipe for pipe in pipeline if pipe not in ( " tok2vec " , " transformer " ) ]
2021-01-27 12:56:33 +00:00
defaults = RECOMMENDATIONS [ " __default__ " ]
reco = RecommendationSchema ( * * RECOMMENDATIONS . get ( lang , defaults ) ) . dict ( )
2020-08-13 15:38:30 +00:00
variables = {
" lang " : lang ,
" components " : pipeline ,
" optimize " : optimize ,
2020-12-09 00:00:51 +00:00
" hardware " : " gpu " if gpu else " cpu " ,
2020-08-15 12:50:29 +00:00
" transformer_data " : reco [ " transformer " ] ,
" word_vectors " : reco [ " word_vectors " ] ,
2020-08-19 11:33:15 +00:00
" has_letters " : reco [ " has_letters " ] ,
2020-08-13 15:38:30 +00:00
}
2020-08-21 10:06:19 +00:00
if variables [ " transformer_data " ] and not has_spacy_transformers ( ) :
msg . warn (
" To generate a more effective transformer-based config (GPU-only), "
" install the spacy-transformers package and re-run this command. "
" The config generated now does not use transformers. "
)
variables [ " transformer_data " ] = None
2020-08-15 12:50:29 +00:00
base_template = template . render ( variables ) . strip ( )
2020-08-13 15:38:30 +00:00
# Giving up on getting the newlines right in jinja for now
base_template = re . sub ( r " \ n \ n \ n+ " , " \n \n " , base_template )
2020-08-15 12:50:29 +00:00
# Access variables declared in templates
template_vars = template . make_module ( variables )
2020-08-13 15:38:30 +00:00
use_case = {
" Language " : lang ,
" Pipeline " : " , " . join ( pipeline ) ,
" Optimize for " : optimize ,
" Hardware " : variables [ " hardware " ] . upper ( ) ,
2021-01-27 12:56:33 +00:00
" Transformer " : template_vars . transformer . get ( " name " )
if template_vars . use_transformer
else None ,
2020-08-13 15:38:30 +00:00
}
2021-01-06 02:07:14 +00:00
msg . info ( " Generated config template specific for your use case " )
2020-08-13 15:38:30 +00:00
for label , value in use_case . items ( ) :
msg . text ( f " - { label } : { value } " )
2020-08-14 14:49:26 +00:00
with show_validation_error ( hint_fill = False ) :
2020-08-15 12:50:29 +00:00
config = util . load_config_from_str ( base_template )
2020-09-27 20:21:31 +00:00
nlp = util . load_model_from_config ( config , auto_fill = True )
2020-09-17 14:05:40 +00:00
config = nlp . config
if pretraining :
validate_config_for_pretrain ( config , msg )
pretrain_config = util . load_config ( DEFAULT_CONFIG_PRETRAIN_PATH )
config = pretrain_config . merge ( config )
2020-08-13 15:38:30 +00:00
msg . good ( " Auto-filled config with all values " )
2020-12-08 16:37:20 +00:00
return config
2020-08-14 14:49:26 +00:00
2020-09-01 10:07:04 +00:00
def save_config (
config : Config , output_file : Path , is_stdout : bool = False , silent : bool = False
) - > None :
no_print = is_stdout or silent
msg = Printer ( no_print = no_print )
2020-08-13 15:38:30 +00:00
if is_stdout :
2020-08-14 14:49:26 +00:00
print ( config . to_str ( ) )
2020-08-13 15:38:30 +00:00
else :
2020-08-26 08:26:57 +00:00
if not output_file . parent . exists ( ) :
output_file . parent . mkdir ( parents = True )
2020-08-14 14:49:26 +00:00
config . to_disk ( output_file , interpolate = False )
2020-08-13 15:38:30 +00:00
msg . good ( " Saved config " , output_file )
2020-09-03 11:13:03 +00:00
msg . text ( " You can now add your data and train your pipeline: " )
2020-08-13 15:38:30 +00:00
variables = [ " --paths.train ./train.spacy " , " --paths.dev ./dev.spacy " ]
2020-09-01 10:07:04 +00:00
if not no_print :
print ( f " { COMMAND } train { output_file . parts [ - 1 ] } { ' ' . join ( variables ) } " )
2020-08-02 13:18:30 +00:00
2020-08-21 10:06:19 +00:00
def has_spacy_transformers ( ) - > bool :
2020-08-13 15:38:30 +00:00
try :
import spacy_transformers # noqa: F401
2020-08-21 10:06:19 +00:00
return True
2020-08-13 15:38:30 +00:00
except ImportError :
2020-08-18 11:38:43 +00:00
return False
2020-08-24 13:56:03 +00:00
def validate_config_for_pretrain ( config : Config , msg : Printer ) - > None :
if " tok2vec " not in config [ " nlp " ] [ " pipeline " ] :
msg . warn (
" No tok2vec component found in the pipeline. If your tok2vec "
" component has a different name, you may need to adjust the "
" tok2vec_model reference in the [pretraining] block. If you don ' t "
" have a tok2vec component, make sure to add it to your [components] "
" and the pipeline specified in the [nlp] block, so you can pretrain "
" weights for it. "
)