More experimentation

Signed-off-by: András Schmelczer <andras@schmelczer.dev>
This commit is contained in:
Andras Schmelczer 2022-04-03 21:46:35 +02:00
parent c01d55d291
commit c11880483d
43 changed files with 749 additions and 496 deletions

18
.vscode/settings.json vendored
View file

@ -1,3 +1,19 @@
{
"cSpell.words": ["pydantic", "pyplot", "sklearn", "Tfidf", "Vectorizer"]
"cSpell.words": [
"botocore",
"pydantic",
"pyplot",
"sklearn",
"Tfidf",
"tinydb",
"Vectorizer",
"xmargin"
],
"files.exclude": {
".env": true,
"**/.cache": true,
"**/.ipynb_checkpoints": true,
"**/.mypy_cache": true,
"**/.pytest_cache": true
}
}

1
example/.gitignore vendored
View file

@ -1,2 +1,3 @@
data
.ipynb_checkpoints
tracing_database.json

12
example/README.md Normal file
View file

@ -0,0 +1,12 @@
# Train Domain classifier from the [semantic scholar dataset](https://api.semanticscholar.org/corpus)
## Upload the dataset (or a part of it) to shared infrastructure
```sh
mkdir ss-data && cd ss-data
wget https://s3-us-west-2.amazonaws.com/ai2-s2-research-public/open-corpus/2022-02-01/manifest.txt
wget -B https://s3-us-west-2.amazonaws.com/ai2-s2-research-public/open-corpus/2022-02-01/ -i manifest.txt
cd -
python3 -m good_ai.open_s3 --secrets s3.ini --push ss-data
rm -rf ss-data
```

View file

@ -1,15 +1,21 @@
import json
from random import shuffle
from devtools import debug
from predict_domain import predict_domain
from good_ai import LargeFile, process_batch
from good_ai import process_batch
if __name__ == "__main__":
with open("data/s2-corpus-1583.json") as f:
with open(".cache/ss-data-0/s2-corpus-1583.json") as f:
raw = json.load(f)
LargeFile.configure_credentials_from_file("s3.ini")
shuffle(raw)
data = {f'{r["title"]} {r["abstract"]}': r["domain"] for r in raw[:5]}
print(process_batch(predict_domain, ["We have found a new type of chemical."]))
results = process_batch(predict_domain, data.keys())
for predicted, actual in zip(results, data.values()):
print(", ".join(actual))
debug(predicted)
print()

View file

@ -45,7 +45,7 @@ def predict_domain(
)
)
if sum(r.probability for r in results) >= cut_off_probability:
if sum(r.probability for r in results) >= cut_off_probability * 100:
break
return results

File diff suppressed because one or more lines are too long

0
good_ai/src/__init__.py Normal file
View file

View file

@ -1,2 +1,3 @@
from .deploy import process_batch
from .models import save_model, use_model
from .set_default_config import set_default_config, set_default_config_if_uninitialized

View file

@ -1,2 +0,0 @@
from .function_registry import function_registry
from .plugin import Plugin

View file

@ -1,23 +0,0 @@
from collections import defaultdict
from typing import Any, Callable, DefaultDict, List
from .plugin import Plugin
class FunctionRegistry:
def __init__(self) -> None:
self._registered_functions: DefaultDict[int, List[Plugin]] = defaultdict(
lambda: []
)
def add_plugin(self, function: Callable[..., Any], plugin: Plugin):
self._registered_functions[id(function)].append(plugin)
def get_plugins(self, function: Callable[..., Any]) -> List[Plugin]:
plugins = self._registered_functions[id(function)]
for p in plugins:
p.initialize()
return plugins
function_registry = FunctionRegistry()

View file

@ -1,19 +0,0 @@
from typing import Any, Callable
class Plugin:
def __init__(self, function: Callable[[Any], Any]):
self._function = function
self._initialized = False
def initialize(self):
if not self._initialized:
self.on_initialize()
self._initialized = True
def on_initialize(self):
pass
def __call__(self, *args: Any, **kwargs: Any) -> Any:
assert self._initialized
return self._function(*args, **kwargs)

View file

@ -1,16 +1,23 @@
from functools import partial, reduce
from typing import Any, Callable, Iterable, Optional, Sequence
from good_ai.utilities.parallel_map import parallel_map
from ..core import function_registry
from ..set_default_config import set_default_config_if_uninitialized
from ..tracing import Trace, TracingContext
def process_batch(
function: Callable[..., Any],
batch: Iterable[Any],
concurrency: Optional[int] = None,
) -> Sequence[Any]:
plugins = function_registry.get_plugins(function)
composed = partial(reduce, lambda r, f: f(r), plugins)
return parallel_map(composed, batch, concurrency=concurrency)
) -> Sequence[Trace]:
set_default_config_if_uninitialized()
def inner(input: Any) -> Trace:
with TracingContext() as t:
t.log_input(input)
result = function(input)
output = t.log_output(result)
return output
return parallel_map(inner, batch, concurrency=concurrency)

View file

@ -1,20 +1,24 @@
import logging
from typing import Any, Optional
from typing import Any, Optional, Tuple
from joblib import load
from good_ai.open_s3 import LargeFile
from ..set_default_config import set_default_config_if_uninitialized
logger = logging.getLogger("models")
def load_model(
key: str, version: Optional[int] = None, return_path: bool = False
) -> Any:
) -> Tuple[Any, int]:
set_default_config_if_uninitialized()
file = LargeFile(name=key, mode="rb", version=version)
if return_path:
return file.get()
return file.get(), file.version
with file as f:
return load(f)
return load(f), file.version

View file

@ -6,12 +6,16 @@ from joblib import dump
from good_ai.open_s3 import LargeFile
from ..set_default_config import set_default_config_if_uninitialized
logger = logging.getLogger("models")
def save_model(
model: Union[Path, str, object], key: str, keep_last_n: Optional[int] = None
) -> int:
set_default_config_if_uninitialized()
file = LargeFile(name=key, mode="wb", keep_last_n=keep_last_n)
if isinstance(model, Path) or isinstance(model, str):

View file

@ -1,24 +1,33 @@
from typing import Literal, Union
from functools import wraps
from typing import Any, Callable, Literal, Union
from ..core import function_registry
from .use_model_plugin import UseModelPlugin
from ..tracing import Model, TracingContext
from .load_model import load_model
def use_model(
key: str, version: Union[int, Literal["latest"]] = None, return_path: bool = False
):
key: str,
*,
version: Union[int, Literal["latest"]],
return_path: bool = False,
model_kwarg_name: str = "model"
) -> Callable[..., Any]:
assert isinstance(version, int) or version == "latest"
def inner(f):
function_registry.add_plugin(
f,
UseModelPlugin(
f,
key=key,
version=version if isinstance(version, int) else None,
return_path=return_path,
),
)
return f
model, actual_version = load_model(
key=key,
version=None if version == "latest" else version,
return_path=return_path,
)
return inner
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
context = TracingContext.get_current_context()
if context:
context.log_model(Model(key=key, version=actual_version))
return func(*args, **kwargs, **{model_kwarg_name: model})
return wrapper
return decorator

View file

@ -1,28 +0,0 @@
from typing import Any, Callable, Optional
from ..core import Plugin
from .load_model import load_model
class UseModelPlugin(Plugin):
def __init__(
self,
function: Callable[[Any], Any],
key: str,
version: Optional[int],
return_path: bool,
):
def wrapper(*args, **kwargs):
return function(*args, **kwargs, model=self._model)
super().__init__(wrapper)
self._key = key
self._version = version
self._return_path = return_path
def on_initialize(self) -> None:
super().on_initialize()
self._model = load_model(
key=self._key, version=self._version, return_path=self._return_path
)

View file

@ -0,0 +1,55 @@
import logging
import random
from pathlib import Path
from good_ai.good_ai.tracing.tracing_context import TracingContext
from good_ai.open_s3 import LargeFile
from .tracing import PersistenceDriver, TinyDbDriver
logger = logging.getLogger("good_ai")
_initialized = False
def set_default_config_if_uninitialized() -> None:
if not _initialized:
set_default_config()
def set_default_config(
log_level: int = logging.INFO,
s3_config: Path = Path("s3.ini"),
seed: int = 42,
tracing_db_driver: PersistenceDriver = TinyDbDriver(Path("tracing_database.json")),
) -> None:
global _initialized
logging.basicConfig(level=log_level)
_initialize_large_file(s3_config)
_set_seed(seed)
TracingContext.persistence_driver = tracing_db_driver
_initialized = True
logger.info(f"Defaults: configured ✅")
def _initialize_large_file(s3_config: Path) -> None:
if s3_config.exists():
LargeFile.configure_credentials_from_file(s3_config)
else:
logger.info(
f"Provided S3 config ({s3_config.resolve()}) not found, skipping LargeFile initialisation"
)
def _set_seed(seed: int) -> None:
random.seed(seed)
try:
import numpy
numpy.random.seed(seed + 1)
except ImportError:
pass

View file

@ -0,0 +1,4 @@
from .model import Model
from .persistence import MongoDbDriver, PersistenceDriver, TinyDbDriver
from .trace import Trace
from .tracing_context import TracingContext

View file

@ -0,0 +1,6 @@
from pydantic import BaseModel
class Model(BaseModel):
key: str
version: int

View file

@ -0,0 +1,3 @@
from .mongodb_driver import MongoDbDriver
from .persistence_driver import PersistenceDriver
from .tinydb_driver import TinyDbDriver

View file

@ -0,0 +1,5 @@
from .persistence_driver import PersistenceDriver
class MongoDbDriver(PersistenceDriver):
pass

View file

@ -0,0 +1,8 @@
from abc import ABC, abstractmethod
from typing import Any, Dict
class PersistenceDriver(ABC):
@abstractmethod
def save_document(self, document: Dict[str, Any]) -> str:
pass

View file

@ -0,0 +1,15 @@
from pathlib import Path
from typing import Any, Dict
from tinydb import TinyDB
from .persistence_driver import PersistenceDriver
class TinyDbDriver(PersistenceDriver):
def __init__(self, path_to_db: Path) -> None:
super().__init__()
self._db = TinyDB(path_to_db)
def save_document(self, document: Dict[str, Any]) -> str:
return self._db.insert(document)

View file

@ -0,0 +1,14 @@
from typing import Any, List
from pydantic import BaseModel
from .model import Model
class Trace(BaseModel):
created: str
execution_time_ms: float
input: Any
models: List[Model]
output: Any
evaluation: Any = None

View file

@ -0,0 +1,71 @@
import logging
import threading
from collections import defaultdict
from datetime import datetime
from types import TracebackType
from typing import Any, DefaultDict, List, Optional, Type
from .persistence import PersistenceDriver
logger = logging.getLogger("good_ai")
from .model import Model
from .trace import Trace
class TracingContext:
persistence_driver: PersistenceDriver
_contexts: DefaultDict[int, List["TracingContext"]] = defaultdict(lambda: [])
def __init__(self) -> None:
self._models: List[Model] = []
self._input: Any = None
self._output: Any = None
self._trace: Optional[Trace] = None
self._start_time = datetime.utcnow()
def log_input(self, input: Any) -> None:
self._input = input
def log_model(self, model: Model) -> None:
self._models.append(model)
def log_output(self, output: Any) -> Trace:
self._output = output
delta_time = (datetime.utcnow() - self._start_time).microseconds / 1000
self._trace = Trace(
created=self._start_time.isoformat(),
execution_time_ms=delta_time,
input=self._input,
models=self._models,
output=self._output,
)
return self._trace
@classmethod
def get_current_context(cls) -> Optional["TracingContext"]:
if cls._contexts[threading.get_ident()]:
return cls._contexts[threading.get_ident()][-1]
return None
def __enter__(self) -> "TracingContext":
self._contexts[threading.get_ident()].append(self)
return self
def __exit__(
self,
type: Optional[Type[BaseException]],
exception: Optional[BaseException],
traceback: Optional[TracebackType],
) -> bool:
assert self._contexts[threading.get_ident()][-1] == self
self._contexts[threading.get_ident()].remove(self)
if type is None:
assert self._trace is not None
self.persistence_driver.save_document(self._trace.dict())
else:
logger.exception(f"Could not finish operation: {exception}")
return True

View file

@ -3,8 +3,8 @@
import logging
from pathlib import Path
from large_file import LargeFile
from parse_arguments import parse_arguments
from .large_file import LargeFile
from .parse_arguments import parse_arguments
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)

View file

@ -0,0 +1,2 @@
def bytes_to_megabytes(bytes: int) -> str:
return f"{round(bytes / 1000 / 1000, 2):.2f}"

View file

@ -3,6 +3,8 @@ import threading
from logging import Logger
from pathlib import Path
from .bytes_to_megabytes import bytes_to_megabytes
class ProgressBar:
def __init__(self, file_size: int, logger: Logger, prefix: str):
@ -17,10 +19,12 @@ class ProgressBar:
with self._lock:
self._seen_so_far += bytes_amount
percentage = (self._seen_so_far / float(self._file_size)) * 100
size_length = len(str(self._file_size))
progress = str(self._seen_so_far).rjust(size_length)
file_size_mb = bytes_to_megabytes(self._file_size)
seen_so_far_mb = bytes_to_megabytes(self._seen_so_far)
progress = seen_so_far_mb.rjust(len(file_size_mb))
self._logger.info(
f"{self._prefix} {progress}/{self._file_size} bytes ({percentage:.1f}%)"
f"{self._prefix} {progress}/{file_size_mb} MB ({percentage:.1f}%)"
)

View file

@ -5,7 +5,7 @@ import shutil
import tempfile
from pathlib import Path
from types import TracebackType
from typing import IO, Any, Dict, List, Optional, Type, Union
from typing import IO, Any, List, Mapping, Optional, Type, Union, cast
import boto3
@ -65,7 +65,7 @@ class LargeFile:
offline_mode: bool = False,
):
self._name: str = name
self._version = version
self._version: int = cast(int, version)
self._mode: str = mode
self._keep_last_n = keep_last_n
self._offline_mode = offline_mode
@ -89,7 +89,7 @@ class LargeFile:
aws_secret_access_key: str,
large_files_bucket_name: str,
endpoint_url: Optional[str] = None,
**_: Dict[str, Any],
**_: Mapping[str, Any],
) -> None:
cls.region_name = aws_region_name
cls.access_key_id = aws_access_key_id
@ -188,7 +188,7 @@ class LargeFile:
Filename=str(tmp_file_archive),
Callback=None
if hide_progress
else DownloadProgressBar(size=size, name=key, logger=logger),
else DownloadProgressBar(name=str(key), size=size, logger=logger),
)
logger.info(f"Decompressing {self._local_name}")
shutil.unpack_archive(str(tmp_file_archive), tmp, "gztar")
@ -199,7 +199,7 @@ class LargeFile:
return destination
def push(self, path: Union[Path, str], hide_progress: bool = False) -> int:
def push(self, path: Union[Path, str], hide_progress: bool = False) -> None:
if isinstance(path, str):
path = Path(path)
@ -235,7 +235,7 @@ class LargeFile:
Key=self._s3_name,
Callback=None
if hide_progress
else UploadProgressBar(file_to_be_uploaded, logger=logger),
else UploadProgressBar(path=file_to_be_uploaded, logger=logger),
)
self.clean_up()

View file

@ -1,6 +1,6 @@
import unittest
from src.open_s3.helper import human_readable_to_byte
from src.good_ai.open_s3.helper import human_readable_to_byte
class TestHumanReadableToByte(unittest.TestCase):

View file

@ -8,7 +8,7 @@ import botocore.session
PATH = Path(__file__).parent.resolve()
from src.open_s3 import LargeFile
from src.good_ai import LargeFile
credentials = {
"aws_region_name": "your_region_like_eu-west-2",
@ -30,7 +30,7 @@ class TestLargeFile(unittest.TestCase):
self.assertRaises(ValueError, LargeFile, "test-file", "test")
@patch("botocore.session")
def test_initialized_with_dict(self, session) -> None:
def test_initialized_with_dict(self, session: Any) -> None:
session_mock = Mock()
session.get_session = create_autospec(
botocore.session.get_session, return_value=session_mock
@ -58,7 +58,13 @@ class TestLargeFile(unittest.TestCase):
session_mock.create_client = Mock(return_value=s3)
LargeFile.configure_credentials(**credentials)
LargeFile.configure_credentials(
aws_region_name=credentials["aws_region_name"],
aws_access_key_id=credentials["aws_access_key_id"],
aws_secret_access_key=credentials["aws_secret_access_key"],
large_files_bucket_name=credentials["large_files_bucket_name"],
endpoint_url=credentials["endpoint_url"],
)
lf = LargeFile("test-file")
session_mock.set_credentials.assert_called_once_with(
access_key=credentials["aws_access_key_id"],

View file

@ -1,4 +1,4 @@
from sus.publication_tei.models import (
from good_ai.utilities.publication_tei.models import (
Affiliation,
Author,
Meta,

View file

@ -1,6 +1,6 @@
import unittest
from src.sus.clean import clean
from src.good_ai.utilities.clean import clean
class TestClean(unittest.TestCase):

View file

@ -1,7 +1,7 @@
import unittest
from pathlib import Path
from src.sus.evaluate_ranking import evaluate_ranking
from src.good_ai.utilities.evaluate_ranking import evaluate_ranking
class TestEvaluateRanking(unittest.TestCase):

View file

@ -1,6 +1,6 @@
import unittest
from src.sus.get_sentences import get_sentences
from src.good_ai.utilities.get_sentences import get_sentences
class TestGetSentences(unittest.TestCase):

View file

@ -1,6 +1,10 @@
import unittest
from src.sus.language import english_name_of_language, is_english, predict_language
from src.good_ai.utilities.language import (
english_name_of_language,
is_english,
predict_language,
)
class TestLanguage(unittest.TestCase):

View file

@ -1,6 +1,6 @@
import unittest
from src.sus.lemmatize_text import lemmatize_text
from src.good_ai.utilities.lemmatize_text import lemmatize_text
class TestLemmatizeText(unittest.TestCase):

View file

@ -1,7 +1,7 @@
import unittest
from src.sus.lemmatize_text import lemmatize_token
from src.sus.nlp import nlp
from src.good_ai.utilities.lemmatize_text import lemmatize_token
from src.good_ai.utilities.nlp import nlp
class TestLemmatizeToken(unittest.TestCase):

View file

@ -1,6 +1,6 @@
import unittest
from src.sus.match_names.match_names import match_names
from src.good_ai.utilities.match_names.match_names import match_names
class TestMatchNames(unittest.TestCase):

View file

@ -1,6 +1,6 @@
import unittest
from src.sus.parallel_map import parallel_map
from src.good_ai.utilities.parallel_map import parallel_map
COUNT = int(1e5) + 3

View file

@ -1,7 +1,7 @@
import unittest
from pathlib import Path
from src.sus.publication_tei import PublicationTEI
from src.good_ai.utilities.publication_tei import PublicationTEI
from .data.parsed import authors, content, metadata, sentences

View file

@ -1,6 +1,6 @@
import unittest
from src.sus.unique import unique
from src.good_ai.utilities.unique import unique
original = [
("a", 1),

230
ideas.drawio Normal file

File diff suppressed because one or more lines are too long