From 56eb1dd60ecee81e905c7d653c8e75cfbaa3f952 Mon Sep 17 00:00:00 2001 From: Stefan Krawczyk Date: Wed, 10 Apr 2024 22:05:45 -0700 Subject: [PATCH 1/5] Adds NER example using HF datasets & lancedb This shows how one might build a pipeline and utilize models to extract entities and embeddings. Then save them to lancedb, and then use both to query over them. WIP (+4 squashed commits) Squashed commits: [c91afb69] wip [17cd2976] Gets example to run on HF datasets properly TODOs: - tidy up - README - remove parallel in favor of discussion [f8409340] TODOs: 1. remove parallel - doesn't make sense for GPU case as you can't parallelize that, and you want to use datasets.map() for batching. 2. make it run on datasets [b76d0110] WIP create lanceDB NER example --- examples/LLM_Workflows/NER_Example/README.md | 17 + .../NER_Example/lancedb_module.py | 104 ++++++ .../NER_Example/ner_extraction.py | 176 ++++++++++ .../NER_Example/ner_extraction_pipeline.png | Bin 0 -> 197354 bytes .../LLM_Workflows/NER_Example/notebook.ipynb | 331 ++++++++++++++++++ .../NER_Example/requirements.txt | 7 + examples/LLM_Workflows/NER_Example/run.py | 89 +++++ 7 files changed, 724 insertions(+) create mode 100644 examples/LLM_Workflows/NER_Example/README.md create mode 100644 examples/LLM_Workflows/NER_Example/lancedb_module.py create mode 100644 examples/LLM_Workflows/NER_Example/ner_extraction.py create mode 100644 examples/LLM_Workflows/NER_Example/ner_extraction_pipeline.png create mode 100644 examples/LLM_Workflows/NER_Example/notebook.ipynb create mode 100644 examples/LLM_Workflows/NER_Example/requirements.txt create mode 100644 examples/LLM_Workflows/NER_Example/run.py diff --git a/examples/LLM_Workflows/NER_Example/README.md b/examples/LLM_Workflows/NER_Example/README.md new file mode 100644 index 000000000..090282ab7 --- /dev/null +++ b/examples/LLM_Workflows/NER_Example/README.md @@ -0,0 +1,17 @@ +# Document processing with Named Entity Recognition (NER) for RAG +This example demonstrates how to use the Named Entity Recognition (NER) model to extract entities from a document. +This extra metadata can be used when querying over the documents in the RAG model to filter to the documents +that contain the entities of interest. + +The pipeline we create can be seen in the image below. +![pipeine](ner_extraction_pipeline.png) + +To run this example: +1. Install the requirements by running `pip install -r requirements.txt` +2. Run the script `python run.py`. Some example commands: + + - python run.py medium_docs load + - python run.py medium_docs query --query "Why does SpaceX want to build a city on Mars?" + - python run.py medium_docs query --query "How are autonomous vehicles changing the world?" + +3. To see the full list of commands run `python run.py --help`. diff --git a/examples/LLM_Workflows/NER_Example/lancedb_module.py b/examples/LLM_Workflows/NER_Example/lancedb_module.py new file mode 100644 index 000000000..8de3dec56 --- /dev/null +++ b/examples/LLM_Workflows/NER_Example/lancedb_module.py @@ -0,0 +1,104 @@ +from typing import Union + +import lancedb +import numpy as np +import pyarrow as pa +from datasets import Dataset +from datasets.formatting.formatting import LazyBatch +from sentence_transformers import SentenceTransformer + + +def db_client() -> lancedb.DBConnection: + """the lancedb client""" + return lancedb.connect("./.lancedb") + + +def _write_to_lancedb( + data: Union[list[dict], pa.Table], db: lancedb.DBConnection, table_name: str +) -> int: + """Helper function to write to lancedb. + + This can handle the case the table exists or it doesn't. + """ + try: + db.create_table(table_name, data) + except (OSError, ValueError): + tbl = db.open_table(table_name) + tbl.add(data) + return len(data) + + +def _batch_write(dataset_batch: LazyBatch, db, table_name, other_columns) -> None: + """Helper function to batch write to lancedb.""" + # we pull out the pyarrow table and select what we want from it + _write_to_lancedb( + dataset_batch.pa_table.select(["vector", "named_entities"] + other_columns), db, table_name + ) + return None + + +def loaded_lancedb_table( + final_dataset: Dataset, + db_client: lancedb.DBConnection, + table_name: str, + metadata_of_interest: list[str], + write_batch_size: int = 100, +) -> lancedb.table.Table: + """Loads the data into lancedb explicitly -- but we lose some visibility this way. + + This function uses batching to write to lancedb. + """ + final_dataset.map( + _batch_write, + batched=True, + batch_size=write_batch_size, + fn_kwargs={ + "db": db_client, + "table_name": table_name, + "other_columns": metadata_of_interest, + }, + desc="writing to lancedb", + ) + return db_client.open_table(table_name) + + +def lancedb_table(db_client: lancedb.DBConnection, table_name: str = "tw") -> lancedb.table.Table: + """Table to query against""" + tbl = db_client.open_table(table_name) + return tbl + + +def lancedb_result( + query: str, + named_entities: list[str], + retriever: SentenceTransformer, + lancedb_table: lancedb.table.Table, + top_k: int = 10, + prefilter: bool = True, +) -> dict: + """Result of querying lancedb. + + :param query: the query + :param named_entities: the named entities found in the query + :param retriever: the model to create the embedding from the query + :param lancedb_table: the lancedb table to query against + :param top_k: number of top results + :param prefilter: whether to prefilter results before cosine distance + :return: dictionary result + """ + # create embeddings for the query + query_vector = np.array(retriever.encode(query).tolist()) + + # query the lancedb table + query_builder = lancedb_table.search(query_vector, vector_column_name="vector") + if named_entities: + # applying named entity filter if something was returned + where_clause = f"array_length(array_intersect({named_entities}, named_entities)) > 0" + query_builder = query_builder.where(where_clause, prefilter=prefilter) + result = ( + query_builder.select(["title", "url", "named_entities"]) # what to return + .limit(top_k) + .to_list() + ) + # could rerank results here + return {"Query": query, "Query Entities": named_entities, "Result": result} diff --git a/examples/LLM_Workflows/NER_Example/ner_extraction.py b/examples/LLM_Workflows/NER_Example/ner_extraction.py new file mode 100644 index 000000000..898d9ee55 --- /dev/null +++ b/examples/LLM_Workflows/NER_Example/ner_extraction.py @@ -0,0 +1,176 @@ +from typing import Union + +import torch +from datasets import Dataset, load_dataset # noqa: F401 +from datasets.formatting.formatting import LazyBatch +from sentence_transformers import SentenceTransformer +from transformers import ( + AutoModelForTokenClassification, + AutoTokenizer, + PreTrainedModel, + PreTrainedTokenizer, + pipeline, +) +from transformers.pipelines import base + +from hamilton.function_modifiers import load_from, save_to, source, value + +# Could explicitly load the dataset this way +# def medium_articles() -> Dataset: +# """Loads medium dataset into a hugging face dataset""" +# ds = load_dataset( +# "fabiochiu/medium-articles", +# data_files="medium_articles.csv", +# split="train" +# ) +# return ds + + +@load_from.hf_dataset( + path=value("fabiochiu/medium-articles"), + data_files=value("medium_articles.csv"), + split=value("train"), +) +def medium_articles(dataset: Dataset) -> Dataset: + """Loads medium dataset into a hugging face dataset""" + return dataset + + +def sampled_articles( + medium_articles: Dataset, + sample_size: int = 104, + random_state: int = 32, + max_text_length: int = 1000, +) -> Dataset: + """Samples the articles and does some light transformations. + Transformations: + - selects the first 1000 characters of text. This is for performance here. But in real life you'd \ + do something for your use case. + - Joins article title and the text to create one text string. + """ + # Filter out entries with NaN values in 'text' or 'title' fields + dataset = medium_articles.filter( + lambda example: example["text"] is not None and example["title"] is not None + ) + + # Shuffle and take the first 10000 samples + dataset = dataset.shuffle(seed=random_state).select(range(sample_size)) + + # Truncate the 'text' to the first 1000 characters + dataset = dataset.map(lambda example: {"text": example["text"][:max_text_length]}) + + # Concatenate the 'title' and truncated 'text' + dataset = dataset.map(lambda example: {"title_text": example["title"] + ". " + example["text"]}) + return dataset + + +def device() -> str: + """Whether this is a CUDA or CPU enabled device.""" + return "cuda" if torch.cuda.is_available() else "cpu" + + +def NER_model_id() -> str: + """Model ID to use + To extract named entities, we will use a NER model finetuned on a BERT-base model. + The model can be loaded from the HuggingFace model hub. + Use `overrides={"NER_model_id": VALUE}` to switch this without changing code. + """ + return "dslim/bert-base-NER" + + +def tokenizer(NER_model_id: str) -> PreTrainedTokenizer: + """Loads the tokenizer for the NER model ID from huggingface""" + return AutoTokenizer.from_pretrained(NER_model_id) + + +def model(NER_model_id: str) -> PreTrainedModel: + """Loads the NER model from huggingface""" + return AutoModelForTokenClassification.from_pretrained(NER_model_id) + + +def ner_pipeline( + model: PreTrainedModel, tokenizer: PreTrainedTokenizer, device: str +) -> base.Pipeline: + """Loads the tokenizer and model into a NER pipeline. That is it combines them.""" + device_no = torch.cuda.current_device() if device == "cuda" else None + return pipeline( + "ner", model=model, tokenizer=tokenizer, aggregation_strategy="max", device=device_no + ) + + +def retriever( + device: str, retriever_model_id: str = "flax-sentence-embeddings/all_datasets_v3_mpnet-base" +) -> SentenceTransformer: + """Our retriever model to create embeddings. + + A retriever model is used to embed passages (article title + first 1000 characters) + and queries. It creates embeddings such that queries and passages with similar + meanings are close in the vector space. We will use a sentence-transformer model + as our retriever. The model can be loaded as follows: + """ + return SentenceTransformer(retriever_model_id, device=device) + + +def _extract_named_entities_text( + title_text_batch: Union[LazyBatch, list[str]], _ner_pipeline +) -> list[list[str]]: + """Helper function to extract named entities given a batch of text.""" + # extract named entities using the NER pipeline + extracted_batch = _ner_pipeline(title_text_batch) + # this should be extracted_batch = dataset.map(ner_pipeline) + entities = [] + # loop through the results and only select the entity names + for text in extracted_batch: + ne = [entity["word"] for entity in text] + entities.append(ne) + _named_entities = [list(set(entity)) for entity in entities] + return _named_entities + + +def _batch_map(dataset: LazyBatch, _retriever, _ner_pipeline) -> dict: + """Helper function to created the embedding vectors and extract named entities""" + title_text_list = dataset["title_text"] + emb = _retriever.encode(title_text_list) + _named_entities = _extract_named_entities_text(title_text_list, _ner_pipeline) + return { + "vector": emb, + "named_entities": _named_entities, + } + + +def columns_of_interest() -> list[str]: + """The columns we expect to pull from the dataset to be saved to lancedb""" + return ["vector", "named_entities", "title", "url", "authors", "timestamp", "tags"] + + +@save_to.lancedb( + db_client=source("db_client"), + table_name=source("table_name"), + columns_to_write=source("columns_of_interest"), + output_name_="load_into_lancedb", +) +def final_dataset( + sampled_articles: Dataset, + retriever: SentenceTransformer, + ner_pipeline: base.Pipeline, +) -> Dataset: + """The final dataset to be pushed to lancedb. + + This adds two columns: + + - vector -- the vector embedding + - named_entities -- the names of entities extracted from the text + """ + # goes over the data in batches so that the GPU can be properly utilized. + final_ds = sampled_articles.map( + _batch_map, + batched=True, + fn_kwargs={"_retriever": retriever, "_ner_pipeline": ner_pipeline}, + desc="extracting entities", + ) + return final_ds + + +def named_entities(query: str, ner_pipeline: base.Pipeline) -> list[str]: + """The entities to extract from the query via the pipeline.""" + return _extract_named_entities_text([query], ner_pipeline)[0] diff --git a/examples/LLM_Workflows/NER_Example/ner_extraction_pipeline.png b/examples/LLM_Workflows/NER_Example/ner_extraction_pipeline.png new file mode 100644 index 0000000000000000000000000000000000000000..f838ac01c827d2f19a3101ef4414ec6a21085cfa GIT binary patch literal 197354 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Ödky@#b2rw^eDH5#epd$>TaaDYQ{Jcnuk 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

yrZFc7j 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

bNHx_J7seEFko?Uux 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 literal 0 HcmV?d00001 diff --git a/examples/LLM_Workflows/NER_Example/notebook.ipynb b/examples/LLM_Workflows/NER_Example/notebook.ipynb new file mode 100644 index 000000000..f1c31eb91 --- /dev/null +++ b/examples/LLM_Workflows/NER_Example/notebook.ipynb @@ -0,0 +1,331 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": { + "collapsed": true + }, + "source": [ + "!pip install sentence_transformers datasets lancedb sf-hamilton -qU" + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# NER Powered Semantic Search\n", + "\n", + "This notebook shows how to use Named Entity Recognition (NER) for vector search with LanceDB & Hamilton. We will:\n", + "\n", + "1. Extract named entities from text.\n", + "2. Store them in a LanceDB as metadata (alongside respective text & embedding vectors).\n", + "3. We extract named entities from incoming queries and use them to filter and search only through records containing these named entities.\n", + "\n", + "This is particularly helpful if you want to restrict the search to records that contain information about the named entities that are also found within the query.\n", + "\n", + "Let's get started." + ], + "metadata": { + "collapsed": false + }, + "id": "f16043d07abadc81" + }, + { + "cell_type": "code", + "source": [ + "%load_ext hamilton.plugins.jupyter_magic" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-10T22:52:53.305913Z", + "start_time": "2024-04-10T22:52:50.668455Z" + } + }, + "id": "c3779bcd738db9e0", + "execution_count": 4, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "%%cell_to_module -m ner_search --display --config '{\"mode\":\"ingestion\"}'\n", + "import lancedb\n", + "from datasets import load_dataset\n", + "import pandas as pd\n", + "import torch\n", + "from sentence_transformers import SentenceTransformer\n", + "from transformers import AutoTokenizer, AutoModelForTokenClassification\n", + "from transformers import pipeline\n", + "from transformers.pipelines import base\n", + "from hamilton.htypes import Parallelizable, Collect\n", + "from hamilton.function_modifiers import config\n", + "import numpy as np\n", + "\n", + "def medium_articles() -> pd.DataFrame:\n", + " # load the dataset and convert to pandas dataframe\n", + " df = load_dataset(\n", + " \"fabiochiu/medium-articles\", data_files=\"medium_articles.csv\", split=\"train\"\n", + " ).to_pandas()\n", + " return df\n", + "\n", + "def sampled_articles(medium_articles: pd.DataFrame) -> pd.DataFrame:\n", + " df = medium_articles.dropna().sample(20000, random_state=32)\n", + " # select first 1000 characters\n", + " df[\"text\"] = df[\"text\"].str[:1000]\n", + " # join article title and the text\n", + " df[\"title_text\"] = df[\"title\"] + \". \" + df[\"text\"]\n", + " return df\n", + "\n", + "def device() -> int:\n", + " return torch.cuda.current_device() if torch.cuda.is_available() else None\n", + "\n", + "\n", + "def model_id() -> str:\n", + " # To extract named entities, we will use a NER model finetuned on a BERT-base model.\n", + " # The model can be loaded from the HuggingFace model hub\n", + " return \"dslim/bert-base-NER\"\n", + "\n", + "def tokenizer(model_id: str) -> AutoTokenizer:\n", + " \"\"\"load the tokenizer from huggingface\"\"\"\n", + " print(\"Loading the tokenizer\")\n", + " return AutoTokenizer.from_pretrained(model_id)\n", + "\n", + "def model(model_id: str) -> object:\n", + " \"\"\"load the NER model from huggingface\"\"\"\n", + " print(\"Loading the model\")\n", + " return AutoModelForTokenClassification.from_pretrained(model_id)\n", + "\n", + "# load the tokenizer and model into a NER pipeline\n", + "def ner_pipeline(model: object, tokenizer: AutoTokenizer, device: int) -> base.Pipeline:\n", + " print(\"Loading the ner_pipeline\")\n", + " return pipeline(\n", + " \"ner\", model=model, tokenizer=tokenizer, aggregation_strategy=\"max\", device=device\n", + " )\n", + "\n", + "def retriever(device: int) -> SentenceTransformer:\n", + " \"\"\"A retriever model is used to embed passages (article title + first 1000 characters) and queries. It creates embeddings such that queries and passages with similar meanings are close in the vector space. We will use a sentence-transformer model as our retriever. The model can be loaded as follows:\n", + " \"\"\"\n", + " print(\"Loading the retriever model\")\n", + " return SentenceTransformer(\n", + " \"flax-sentence-embeddings/all_datasets_v3_mpnet-base\", device=device\n", + " )\n", + "\n", + "\n", + "def db() -> lancedb.DBConnection:\n", + " return lancedb.connect(\"./.lancedb\")\n", + "\n", + "def batch_size() -> int:\n", + " # we will use batches of 64\n", + " return 64\n", + "\n", + "def batch(sampled_articles: pd.DataFrame, batch_size: int) -> Parallelizable[pd.DataFrame]:\n", + " # split the articles into batches\n", + " for i in range(0, len(sampled_articles), batch_size):\n", + " # find end of batch\n", + " i_end = min(i + batch_size, len(sampled_articles))\n", + " # extract batch\n", + " batch = sampled_articles.iloc[i:i_end].copy()\n", + " yield batch\n", + "\n", + "def title_text(batch: pd.DataFrame) -> list[str]:\n", + " return batch[\"title_text\"].tolist()\n", + "\n", + "def embeddings(title_text: list[str], retriever: SentenceTransformer) -> list[list[float]]:\n", + " # generate embeddings for batch\n", + " return retriever.encode(title_text).tolist()\n", + "\n", + "def entities(title_text: list[str], ner_pipeline: base.Pipeline) -> list[list[str]]:\n", + " # extract named entities using the NER pipeline\n", + " extracted_batch = ner_pipeline(title_text)\n", + " entities = []\n", + " # loop through the results and only select the entity names\n", + " for text in extracted_batch:\n", + " ne = [entity[\"word\"] for entity in text]\n", + " entities.append(ne)\n", + " return entities\n", + "\n", + "def named_entities(entities: list[list[str]]) -> list[list[str]]:\n", + " return [list(set(entity)) for entity in entities]\n", + "\n", + "def meta(batch: pd.DataFrame, named_entities: list[list[str]]) -> list[dict]:\n", + " # create a dataframe we want for metadata\n", + " df = batch.drop(\"title_text\", axis=1)\n", + " df[\"named_entities\"] = named_entities\n", + " return df.to_dict(orient=\"records\")\n", + "\n", + "def to_upsert(embeddings: list[list[float]],\n", + " meta: list[dict],\n", + " named_entities: list[list[str]]) -> list[tuple[list[float], dict, list[str]]]:\n", + " return list(zip(embeddings, meta, named_entities))\n", + "\n", + "def data(to_upsert: Collect[list[tuple[list[float], dict, list[str]]]]) -> list[dict]:\n", + " data = []\n", + " for result in to_upsert:\n", + " for emb, meta, entity in result:\n", + " temp = dict()\n", + " temp[\"vector\"] = np.array(emb)\n", + " temp[\"metadata\"] = meta\n", + " temp[\"named_entities\"] = entity\n", + " data.append(temp)\n", + " return data\n", + "\n", + "@config.when(mode=\"ingestion\")\n", + "def lancedb_table__ingestion(db: lancedb.DBConnection, data: list[dict], table_name: str = \"tw\") -> lancedb.table.Table:\n", + " tbl = db.create_table(table_name, data)\n", + " return tbl\n", + "\n", + "@config.when(mode=\"query\")\n", + "def lancedb_table__query(db: lancedb.DBConnection, table_name: str = \"tw\") -> lancedb.table.Table:\n", + " tbl = db.open_table(table_name)\n", + " return tbl\n", + "\n", + "def search_lancedb(query: str,\n", + " ner_pipeline: base.Pipeline,\n", + " retriever: SentenceTransformer,\n", + " lancedb_table: lancedb.table.Table) -> dict:\n", + " # extract named entities from the query\n", + " ne = entities([query], ner_pipeline)[0] # Note: we're directly calling the function here.\n", + " # create embeddings for the query\n", + " xq = retriever.encode(query).tolist()\n", + " # query the lancedb table while applying named entity filter\n", + " xc = lancedb_table.search(xq).to_list()\n", + " # extract article titles from the search result\n", + " r = [\n", + " x[\"metadata\"][\"title\"]\n", + " for x in xc\n", + " for i in x[\"metadata\"][\"named_entities\"]\n", + " if i in ne\n", + " ]\n", + " return {\"Extracted Named Entities\": ne, \"Result\": r}" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-11T00:37:00.900505Z", + "start_time": "2024-04-11T00:37:00.359482Z" + } + }, + "id": "68bbb0d987a85d06", + "execution_count": 37, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "from hamilton import driver\n", + "dr = (\n", + " driver.Builder()\n", + " .with_config({\"mode\": \"ingestion\"})\n", + " .with_modules(ner_search)\n", + " .enable_dynamic_execution(allow_experimental_mode=True)\n", + " .build()\n", + ")\n", + "results = dr.execute([\"retriever\", \"ner_pipeline\", \"data\", \"lancedb_table\"])" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-11T00:11:41.091151Z", + "start_time": "2024-04-10T23:47:47.710760Z" + } + }, + "id": "9dd70c2ceecbbb71", + "execution_count": 35, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "dr.execute([\"search_lancedb\"], \n", + " inputs={\"query\": \"How Data is changing the world?\"},\n", + " overrides=results)\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-11T00:11:41.103254Z", + "start_time": "2024-04-11T00:11:41.099972Z" + } + }, + "id": "e5686dd7ee49b2e4", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "dr_inference_only = (\n", + " driver.Builder()\n", + " .with_config({\"mode\": \"query\"})\n", + " .with_modules(ner_search)\n", + " .build()\n", + ")\n", + "dr_inference_only.execute([\"search_lancedb\"], \n", + " inputs={\"query\": \"How Data is changing the world?\", \"table_name\": \"temp1\"},\n", + " )" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-11T01:26:54.514537Z", + "start_time": "2024-04-11T01:26:50.632905Z" + } + }, + "id": "2864676c0f73d8f2", + "execution_count": 40, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "dr_inference_only.execute([\"search_lancedb\"], \n", + " inputs={\"query\": \"How Data is changing the world?\", \"table_name\": \"temp1\"},\n", + " )" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-11T04:18:47.049954Z", + "start_time": "2024-04-11T04:18:43.767788Z" + } + }, + "id": "cb661621910276ca", + "execution_count": 42, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "collapsed": false + }, + "id": "9dca46791620e5a6", + "execution_count": null, + "outputs": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/LLM_Workflows/NER_Example/requirements.txt b/examples/LLM_Workflows/NER_Example/requirements.txt new file mode 100644 index 000000000..7b6a55b65 --- /dev/null +++ b/examples/LLM_Workflows/NER_Example/requirements.txt @@ -0,0 +1,7 @@ +datasets +lancedb +numpy +sentence_transformers +sf-hamilton[visualization, sdk] +torch +tqdm diff --git a/examples/LLM_Workflows/NER_Example/run.py b/examples/LLM_Workflows/NER_Example/run.py new file mode 100644 index 000000000..3dae7c469 --- /dev/null +++ b/examples/LLM_Workflows/NER_Example/run.py @@ -0,0 +1,89 @@ +import argparse + +import lancedb_module +import ner_extraction + +from hamilton import driver, lifecycle + + +def build_driver(adapter_list): + """Builds the driver with the necessary modules and adapters.""" + dr = ( + driver.Builder() + .with_config({}) + .with_modules(ner_extraction, lancedb_module) + .with_adapters(*adapter_list) + .build() + ) + return dr + + +def load_data(table_name: str, use_tracker: bool = False): + adapter_list = [lifecycle.PrintLn()] + if use_tracker: + from hamilton_sdk import adapters + + tracker = adapters.HamiltonTracker( + project_id=41, # modify this as needed + username="elijah@dagworks.io", + dag_name="ner-lancedb-pipeline", + tags={"context": "extraction", "team": "MY_TEAM", "version": "1"}, + ) + adapter_list.append(tracker) + + dr = build_driver(adapter_list) + # display the graph + dr.display_all_functions("ner_extraction_pipeline.png") + + results = dr.execute( + ["load_into_lancedb"], + inputs={"table_name": table_name}, + ) + print(results) + + +def query_data(query: str, table_name: str, use_tracker: bool = False): + adapter_list = [lifecycle.PrintLn()] + if use_tracker: + from hamilton_sdk import adapters + + tracker = adapters.HamiltonTracker( + project_id=41, # modify this as needed + username="elijah@dagworks.io", + dag_name="ner-lancedb-pipeline", + tags={"context": "inference", "team": "MY_TEAM", "version": "1"}, + ) + adapter_list.append(tracker) + + dr = build_driver(adapter_list) + + r = dr.execute(["lancedb_result"], inputs={"query": query, "table_name": table_name}) + print(r) + + +def main(): + parser = argparse.ArgumentParser(description="Process command-line arguments.") + parser.add_argument("table_name", help="The name of the table.") + parser.add_argument("operation", choices=["load", "query"], help="The operation to perform.") + parser.add_argument("--query", help="The query to run. Required if operation is 'query'.") + parser.add_argument("--use-tracker", action="store_true", help="Whether to use the tracker.") + + args = parser.parse_args() + + if args.operation == "query" and args.query is None: + parser.error("The --query argument is required when operation is 'query'.") + + if args.operation == "load": + load_data(args.table_name, args.use_tracker) + else: + query_data(args.query, args.table_name, args.use_tracker) + + +if __name__ == "__main__": + """ + Some example commands: + > python run.py medium_docs load + > python run.py medium_docs query --query "Why does SpaceX want to build a city on Mars?" + > python run.py medium_docs query --query "How are autonomous vehicles changing the world?" + """ + main() From 95e74809efc1ddbdaac90c9a0e1f3410ccd2c2a8 Mon Sep 17 00:00:00 2001 From: Stefan Krawczyk Date: Mon, 20 May 2024 20:19:58 -0400 Subject: [PATCH 2/5] Adds hugging face loader and two savers This adds support for loading hugging face datasets. It then also supports saving it to parquet and to lancedb. Adds tests. Putting lancedb saver here is arbitrary, but because we would need to check installed dependencies either way, I felt it would be simpler to put here for now. Ideally we could convert between common formats to help here. E.g. pyarrow tables could be something to simplify things. --- hamilton/function_modifiers/base.py | 1 + hamilton/io/utils.py | 3 +- hamilton/plugins/huggingface_extensions.py | 206 +++++++++++++++++++ requirements-test.txt | 2 + tests/plugins/test_huggingface_extensions.py | 62 ++++++ tests/resources/hf_datasets/foo.csv | 2 + 6 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 hamilton/plugins/huggingface_extensions.py create mode 100644 tests/plugins/test_huggingface_extensions.py create mode 100644 tests/resources/hf_datasets/foo.csv diff --git a/hamilton/function_modifiers/base.py b/hamilton/function_modifiers/base.py index d3bd1ca82..bb7a0b948 100644 --- a/hamilton/function_modifiers/base.py +++ b/hamilton/function_modifiers/base.py @@ -38,6 +38,7 @@ "vaex", "ibis", "dlt", + "huggingface", ] for plugin_module in plugins_modules: try: diff --git a/hamilton/io/utils.py b/hamilton/io/utils.py index 95dd9588d..64be1296e 100644 --- a/hamilton/io/utils.py +++ b/hamilton/io/utils.py @@ -1,6 +1,7 @@ import os import time from datetime import datetime +from os import PathLike from pathlib import Path from typing import Any, Dict, Union from urllib import parse @@ -12,7 +13,7 @@ FILE_METADATA = "file_metadata" -def get_file_metadata(path: Union[str, Path]) -> Dict[str, Any]: +def get_file_metadata(path: Union[str, Path, PathLike]) -> Dict[str, Any]: """Gives metadata from loading a file. Note: we reserve the right to change this schema. So if you're using this come diff --git a/hamilton/plugins/huggingface_extensions.py b/hamilton/plugins/huggingface_extensions.py new file mode 100644 index 000000000..3f6ec8d74 --- /dev/null +++ b/hamilton/plugins/huggingface_extensions.py @@ -0,0 +1,206 @@ +import dataclasses +from os import PathLike +from typing import Any, BinaryIO, Collection, Dict, Mapping, Optional, Sequence, Tuple, Type, Union + +try: + from datasets import ( + Dataset, + DatasetDict, + DownloadConfig, + DownloadMode, + Features, + IterableDataset, + IterableDatasetDict, + VerificationMode, + Version, + load_dataset, + ) + from datasets.formatting.formatting import LazyBatch +except ImportError: + raise NotImplementedError("huggingface datasets library is not installed.") + +try: + import lancedb + from lancedb import table # noqa: F401 +except ImportError: + lancedb = None + +from hamilton import registry +from hamilton.io import utils +from hamilton.io.data_adapters import DataLoader, DataSaver + +COLUMN_FRIENDLY_DF_TYPE = False + +HF_types = (DatasetDict, Dataset, IterableDatasetDict, IterableDataset) + + +@dataclasses.dataclass +class HuggingFaceDSLoader(DataLoader): + path: str + dataset_name: Optional[str] = None # this can't be `name` because it clashes with `.name()` + data_dir: Optional[str] = None + data_files: Optional[Union[str, Sequence[str], Mapping[str, Union[str, Sequence[str]]]]] = None + split: Optional[str] = None + cache_dir: Optional[str] = None + features: Optional[Features] = None + download_config: Optional[DownloadConfig] = None + download_mode: Optional[Union[DownloadMode, str]] = None + verification_mode: Optional[Union[VerificationMode, str]] = None + ignore_verifications = "deprecated" + keep_in_memory: Optional[bool] = None + save_infos: bool = False + revision: Optional[Union[str, Version]] = None + token: Optional[Union[bool, str]] = None + use_auth_token = "deprecated" + task = "deprecated" + streaming: bool = False + num_proc: Optional[int] = None + storage_options: Optional[Dict] = None + config_kwargs: Optional[Dict] = None + + @classmethod + def applicable_types(cls) -> Collection[Type]: + return list(HF_types) + + def _get_loading_kwargs(self) -> dict: + # Puts kwargs in a dict + kwargs = dataclasses.asdict(self) + # we send path separately + del kwargs["path"] + config_kwargs: Optional[dict] = kwargs.pop("config_kwargs", None) + if config_kwargs: + # add config kwargs as needed. + kwargs.update(config_kwargs) + + # need to pass in name + kwargs["name"] = kwargs.pop("dataset_name", None) + + return kwargs + + def load_data(self, type_: Type) -> Tuple[Union[HF_types], dict[str, Any]]: + """""" + ds = load_dataset(self.path, **self._get_loading_kwargs()) + is_dataset = isinstance(ds, Dataset) + f_meta = {"path": self.path} + ds_meta = {"rows": ds.num_rows, "columns": ds.column_names} + if is_dataset: + ds_meta["size_in_bytes"] = ds.size_in_bytes + ds_meta["features"] = ds.features.to_dict() + return ds, {"file_metadata": f_meta, "dataset_metadata": ds_meta} + + @classmethod + def name(cls) -> str: + return "hf_dataset" + + +@dataclasses.dataclass +class HuggingFaceDSParquetSaver(DataSaver): + path_or_buf: Union[PathLike, BinaryIO] + batch_size: Optional[int] = None + parquet_writer_kwargs: Optional[dict] = None + + @classmethod + def applicable_types(cls) -> Collection[Type]: + return list(HF_types) + + @classmethod + def applies_to(cls, type_: Type[Type]) -> bool: + return type_ in HF_types + + def _get_saving_kwargs(self) -> dict: + # Puts kwargs in a dict + kwargs = dataclasses.asdict(self) + # but we send it separately + del kwargs["path_or_buf"] + parquet_writer_kwargs: Optional[dict] = kwargs.pop("parquet_writer_kwargs", None) + if parquet_writer_kwargs: + # add config kwargs as needed. + kwargs.update(parquet_writer_kwargs) + + return kwargs + + def save_data(self, ds: Union[HF_types]) -> Dict[str, Any]: + is_dataset = isinstance(ds, Dataset) + ds.to_parquet(self.path_or_buf, **self._get_saving_kwargs()) + ds_meta = { + "rows": ds.num_rows, + "columns": ds.column_names, + } + if is_dataset: + ds_meta.update({"size_in_bytes": ds.size_in_bytes, "features": ds.features.to_dict()}) + if isinstance(self.path_or_buf, BinaryIO): + f_meta = {} + else: + f_meta = (utils.get_file_metadata(self.path_or_buf),) + return {"file_metadata": f_meta, "dataset_metadata": ds_meta} + + @classmethod + def name(cls) -> str: + return "parquet" + + +if lancedb is not None: + + def _batch_write( + dataset_batch: LazyBatch, db: lancedb.DBConnection, table_name: str, columns: str + ) -> None: + """Helper function to batch write to lancedb.""" + if columns is None: + data = dataset_batch.pa_table + else: + data = dataset_batch.pa_table.select(columns) + try: + db.create_table(table_name, data) + except (OSError, ValueError): + tbl = db.open_table(table_name) + tbl.add(data) + return None + + @dataclasses.dataclass + class HuggingFaceDSLanceDBSaver(DataSaver): + db_client: lancedb.DBConnection + table_name: str + columns_to_write: list[str] = None # None means all. + write_batch_size: int = 100 + + @classmethod + def applicable_types(cls) -> Collection[Type]: + return list(HF_types) + + def save_data(self, ds: Union[HF_types]) -> Dict[str, Any]: + ds.map( + _batch_write, + batched=True, + batch_size=self.write_batch_size, + fn_kwargs={ + "db": self.db_client, + "table_name": self.table_name, + "columns": self.columns_to_write, + }, + desc=f"writing to lancedb table {self.table_name}", + ) + is_dataset = isinstance(ds, Dataset) + ds_meta = { + "rows": ds.num_rows, + "columns": ds.column_names, + } + if is_dataset: + ds_meta.update( + {"size_in_bytes": ds.size_in_bytes, "features": ds.features.to_dict()} + ) + return {"db_meta": {"table_name": self.table_name}, "dataset_metadata": ds_meta} + + @classmethod + def name(cls) -> str: + return "lancedb" + + +def register_data_loaders_savers(): + loaders = [HuggingFaceDSLoader, HuggingFaceDSParquetSaver] + if lancedb: + loaders.append(HuggingFaceDSLanceDBSaver) + for loader in loaders: + registry.register_adapter(loader) + + +register_data_loaders_savers() diff --git a/requirements-test.txt b/requirements-test.txt index 9cf9227fe..d58a4192d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,11 +1,13 @@ connectorx dask dask-expr; python_version >= '3.9' +datasets # huggingface datasets diskcache dlt fsspec graphviz kaleido +lancedb lightgbm lxml lz4 diff --git a/tests/plugins/test_huggingface_extensions.py b/tests/plugins/test_huggingface_extensions.py new file mode 100644 index 000000000..6289de8d6 --- /dev/null +++ b/tests/plugins/test_huggingface_extensions.py @@ -0,0 +1,62 @@ +import pathlib + +import lancedb +import numpy as np +from datasets import Dataset, DatasetDict + +from hamilton.plugins import huggingface_extensions + + +def test_hfds_loader(): + path_to_test = "tests/resources/hf_datasets" + reader = huggingface_extensions.HuggingFaceDSLoader(path_to_test) + ds, metadata = reader.load_data(DatasetDict) + + assert huggingface_extensions.HuggingFaceDSLoader.applicable_types() == list( + huggingface_extensions.HF_types + ) + assert reader.applies_to(DatasetDict) + assert reader.applies_to(Dataset) + assert ds.shape == {"train": (1, 3)} + + +def test_hfds_parquet_saver(tmp_path: pathlib.Path): + file_path = tmp_path / "testhf.parquet" + saver = huggingface_extensions.HuggingFaceDSParquetSaver(file_path) + ds = Dataset.from_dict({"a": [1, 2, 3]}) + metadata = saver.save_data(ds) + assert file_path.exists() + assert metadata["dataset_metadata"] == { + "columns": ["a"], + "features": {"a": {"_type": "Value", "dtype": "int64"}}, + "rows": 3, + "size_in_bytes": None, + } + assert "file_metadata" in metadata + assert huggingface_extensions.HuggingFaceDSParquetSaver.applicable_types() == list( + huggingface_extensions.HF_types + ) + assert saver.applies_to(DatasetDict) + assert saver.applies_to(Dataset) + + +def test_hfds_lancedb_saver(tmp_path: pathlib.Path): + db_client = lancedb.connect(tmp_path / "lancedb") + saver = huggingface_extensions.HuggingFaceDSLanceDBSaver(db_client, "test_table") + ds = Dataset.from_dict({"vector": [np.array([1.0, 2.0, 3.0])], "named_entities": ["a"]}) + metadata = saver.save_data(ds) + assert metadata == { + "dataset_metadata": { + "columns": ["vector", "named_entities"], + "features": { + "named_entities": {"_type": "Value", "dtype": "string"}, + "vector": {"_type": "Sequence", "feature": {"_type": "Value", "dtype": "float64"}}, + }, + "rows": 1, + "size_in_bytes": None, + }, + "db_meta": {"table_name": "test_table"}, + } + assert db_client.open_table("test_table").search().to_list() == [ + {"named_entities": "a", "vector": [1.0, 2.0, 3.0]} + ] diff --git a/tests/resources/hf_datasets/foo.csv b/tests/resources/hf_datasets/foo.csv new file mode 100644 index 000000000..dc800ea04 --- /dev/null +++ b/tests/resources/hf_datasets/foo.csv @@ -0,0 +1,2 @@ +col1,col2,col3 +"a",1,"n" From 91f53fd4bfa7dc450c1f29bc4ac46ed726b20a34 Mon Sep 17 00:00:00 2001 From: Stefan Krawczyk Date: Mon, 20 May 2024 20:29:50 -0400 Subject: [PATCH 3/5] Adds Union type check for saver if it supports multiple types Say we have this - and want to save it with a saver: ```python def foo() -> Union[int, float]: return ... ``` If the saver's applicable_types is [int, float], this would previously fail, now it does not. Added test for this. If the saver's applicable_type was just `float` or `int`, then rightly this fails -- added test for that explicitly. --- hamilton/io/data_adapters.py | 6 +++++- tests/io/test_data_adapters.py | 36 +++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/hamilton/io/data_adapters.py b/hamilton/io/data_adapters.py index eb652ebb3..4f1f0a30b 100644 --- a/hamilton/io/data_adapters.py +++ b/hamilton/io/data_adapters.py @@ -186,7 +186,11 @@ def applies_to(cls, type_: Type[Type]) -> bool: :param type_: Candidate type :return: True if this data saver can handle to the type, False otherwise. """ - for save_to in cls.applicable_types(): + applicable_types = cls.applicable_types() + if len(applicable_types) > 1 and typing.Union[tuple(applicable_types)] == type_: + # if someone outputs the union of what we support we should match it. + return True + for save_to in applicable_types: # is the adapter type `save_to` a superclass of `type_` ? # i.e. is `type_` a subclass of `save_to` ? if custom_subclass_check(type_, save_to): diff --git a/tests/io/test_data_adapters.py b/tests/io/test_data_adapters.py index ea49b88a7..175a605e0 100644 --- a/tests/io/test_data_adapters.py +++ b/tests/io/test_data_adapters.py @@ -1,5 +1,5 @@ import dataclasses -from typing import Any, Collection, Dict, Tuple, Type +from typing import Any, Collection, Dict, Tuple, Type, Union from hamilton.io.data_adapters import DataLoader, DataSaver @@ -72,6 +72,23 @@ def name(cls) -> str: pass +@dataclasses.dataclass +class MockDataSaver3(DataSaver): + required_param: int + default_param: int = 1 + + @classmethod + def applicable_types(cls) -> Collection[Type]: + return [int, float] + + def save_data(self, type_: Type) -> Tuple[int, Dict[str, Any]]: + pass + + @classmethod + def name(cls) -> str: + pass + + def test_data_loader_get_required_params(): assert MockDataLoader.get_required_arguments() == {"required_param": int} assert MockDataLoader.get_optional_arguments() == {"default_param": int} @@ -105,3 +122,20 @@ def test_saver_applies_to(): assert saver2.applies_to(int) is True # bool -> int -- bool is a subclass of int assert saver2.applies_to(bool) is True + # [int, float] -> int -- can't handle union type here correctly + assert saver2.applies_to(Union[int, float]) is False + + +def test_saver_applies_to_union_of_all_types(): + """If a saver supports saving multiple things, then we need to take the union of that type. + + Why? well if there's a type that outputs a union, then we will not match it when + we should. So the only way to do that is to take the union of all types if there's + more than 1 and simply match it. + """ + saver = MockDataSaver3(1, 3) + assert saver.applies_to(Union[float, int]) is True + # order shouldn't matter + assert saver.applies_to(Union[int, float]) is True + assert saver.applies_to(int) is True + assert saver.applies_to(float) is True From ea5279368801653da9e5d36fed063a273859b4ce Mon Sep 17 00:00:00 2001 From: Stefan Krawczyk Date: Tue, 21 May 2024 02:16:43 -0400 Subject: [PATCH 4/5] Fixes type issue with 3.8 for HF DS loader Also cleans up notebook and adds comments to code --- .../LLM_Workflows/NER_Example/notebook.ipynb | 2855 +++++++++++++++-- hamilton/plugins/huggingface_extensions.py | 15 +- 2 files changed, 2648 insertions(+), 222 deletions(-) diff --git a/examples/LLM_Workflows/NER_Example/notebook.ipynb b/examples/LLM_Workflows/NER_Example/notebook.ipynb index f1c31eb91..68739729c 100644 --- a/examples/LLM_Workflows/NER_Example/notebook.ipynb +++ b/examples/LLM_Workflows/NER_Example/notebook.ipynb @@ -5,325 +5,2742 @@ "execution_count": null, "id": "initial_id", "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, + "outputs": [], "source": [ "!pip install sentence_transformers datasets lancedb sf-hamilton -qU" - ], - "outputs": [] + ] }, { "cell_type": "markdown", + "id": "f16043d07abadc81", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ - "# NER Powered Semantic Search\n", + "# How to use Lancedb with NER semantic search for RAG \n", + "In this post we’ll walk through an example pipeline written in Hamilton to embed some text, and also capture extra metadata about the text that will be used when looking up data for RAG via semantic search with LanceDB.\n", + "\n", + "Why capture, or rather extract (as you’ll see), extra metadata? Because you can use it to filter results to improve accuracy. You’ll need more than just cosine similarity to achieve a quality system [\\[1\\]](https://jxnl.co/writing/2024/05/11/low-hanging-fruit-for-rag-search/). [Named Entity Recognition (NER)](https://en.wikipedia.org/wiki/Named-entity_recognition) is just one approach to gather extra metadata from text that can be used for this purpose.\n", "\n", - "This notebook shows how to use Named Entity Recognition (NER) for vector search with LanceDB & Hamilton. We will:\n", + "> In short, we use the NER model to further filter the semantic search results. The predicted named entities are used as “filters” (pre or post) to filter the vector search results. This is particularly helpful if you want to restrict the search to records that contain information about the named entities that are also found within the query.\n", + "\n", + "In this notebook we'll build out a processing pipeline and walkthrough the code to:\n", "\n", "1. Extract named entities from text.\n", - "2. Store them in a LanceDB as metadata (alongside respective text & embedding vectors).\n", + "2. Store them in a LanceDB as metadata (alongside embedding vectors).\n", "3. We extract named entities from incoming queries and use them to filter and search only through records containing these named entities.\n", "\n", - "This is particularly helpful if you want to restrict the search to records that contain information about the named entities that are also found within the query.\n", "\n", "Let's get started." - ], - "metadata": { - "collapsed": false - }, - "id": "f16043d07abadc81" + ] }, { "cell_type": "code", - "source": [ - "%load_ext hamilton.plugins.jupyter_magic" - ], + "execution_count": 1, + "id": "c3779bcd738db9e0", "metadata": { - "collapsed": false, "ExecuteTime": { "end_time": "2024-04-10T22:52:53.305913Z", "start_time": "2024-04-10T22:52:50.668455Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false } }, - "id": "c3779bcd738db9e0", - "execution_count": 4, - "outputs": [] + "outputs": [], + "source": [ + "# load the jupyter magic\n", + "%load_ext hamilton.plugins.jupyter_magic" + ] + }, + { + "cell_type": "markdown", + "id": "bc423c9e-bbbe-4092-a72b-88794a2a9d6a", + "metadata": {}, + "source": [ + "# Load the data\n", + "Here we first start by loading the dataset from huggingface.\n", + "\n", + "Here we use a DataLoader that Hamilton comes with to load the dataset for us. We could do some filtering within the loading function, but instead choose to break it out into another function to sample and augment the loaded dataset.\n", + "\n", + "Note about the sampling below, in real life we’d use the full data set. We sample here to make this example tractable to run. Otherwise we modify the data set in the following way:\n", + "1. We remove documents with empty titles and text.\n", + "2. We truncate text to only be the first 1000 characters. This is to limit the dataset size, but to also make it fit into our the context window that creates our embeddings. In real life you’d probably want to process the entire text somehow, or create separate embeddings for different text chunks, etc.\n", + "3. Further to simplify things, we combine the title & text into a single field for NER & embedding purposes. We assume the title and the first 1000 characters of text contain enough information to get a general gist of the document to create an embedding and get relevant entities out. \n" + ] }, { "cell_type": "code", + "execution_count": 10, + "id": "48fb3804-c302-4275-8da3-6563a62c2e2a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "medium_articles\n", + "\n", + "medium_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "sampled_articles\n", + "\n", + "sampled_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset->medium_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs\n", + "\n", + "max_text_length\n", + "int\n", + "sample_size\n", + "int\n", + "random_state\n", + "int\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "input\n", + "\n", + "input\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "%%cell_to_module -m ner_search --display --config '{\"mode\":\"ingestion\"}'\n", - "import lancedb\n", - "from datasets import load_dataset\n", - "import pandas as pd\n", - "import torch\n", - "from sentence_transformers import SentenceTransformer\n", - "from transformers import AutoTokenizer, AutoModelForTokenClassification\n", - "from transformers import pipeline\n", - "from transformers.pipelines import base\n", - "from hamilton.htypes import Parallelizable, Collect\n", - "from hamilton.function_modifiers import config\n", - "import numpy as np\n", + "%%incr_cell_to_module ner_module -i 1 --display\n", "\n", - "def medium_articles() -> pd.DataFrame:\n", - " # load the dataset and convert to pandas dataframe\n", - " df = load_dataset(\n", - " \"fabiochiu/medium-articles\", data_files=\"medium_articles.csv\", split=\"train\"\n", - " ).to_pandas()\n", - " return df\n", + "from datasets import Dataset\n", + "from hamilton.function_modifiers import load_from, save_to, source, value\n", "\n", - "def sampled_articles(medium_articles: pd.DataFrame) -> pd.DataFrame:\n", - " df = medium_articles.dropna().sample(20000, random_state=32)\n", - " # select first 1000 characters\n", - " df[\"text\"] = df[\"text\"].str[:1000]\n", - " # join article title and the text\n", - " df[\"title_text\"] = df[\"title\"] + \". \" + df[\"text\"]\n", - " return df\n", + "@load_from.hf_dataset(\n", + " path=value(\"fabiochiu/medium-articles\"),\n", + " data_files=value(\"medium_articles.csv\"),\n", + " split=value(\"train\"),\n", + ")\n", + "def medium_articles(dataset: Dataset) -> Dataset:\n", + " \"\"\"Loads medium dataset into a hugging face dataset\"\"\"\n", + " return dataset\n", "\n", - "def device() -> int:\n", - " return torch.cuda.current_device() if torch.cuda.is_available() else None\n", "\n", + "def sampled_articles(\n", + " medium_articles: Dataset,\n", + " sample_size: int = 104,\n", + " random_state: int = 32,\n", + " max_text_length: int = 1000,\n", + ") -> Dataset:\n", + " \"\"\"Samples the articles and does some light transformations.\n", + " Transformations:\n", + " - selects the first 1000 characters of text. This is for performance here. But in real life you'd \\\n", + " do something for your use case.\n", + " - Joins article title and the text to create one text string.\n", + " \"\"\"\n", + " # Filter out entries with NaN values in 'text' or 'title' fields\n", + " dataset = medium_articles.filter(\n", + " lambda example: example[\"text\"] is not None and example[\"title\"] is not None\n", + " )\n", "\n", - "def model_id() -> str:\n", - " # To extract named entities, we will use a NER model finetuned on a BERT-base model.\n", - " # The model can be loaded from the HuggingFace model hub\n", - " return \"dslim/bert-base-NER\"\n", + " # Shuffle and take the first 10000 samples\n", + " dataset = dataset.shuffle(seed=random_state).select(range(sample_size))\n", "\n", - "def tokenizer(model_id: str) -> AutoTokenizer:\n", - " \"\"\"load the tokenizer from huggingface\"\"\"\n", - " print(\"Loading the tokenizer\")\n", - " return AutoTokenizer.from_pretrained(model_id)\n", + " # Truncate the 'text' to the first 1000 characters\n", + " dataset = dataset.map(lambda example: {\"text\": example[\"text\"][:max_text_length]})\n", "\n", - "def model(model_id: str) -> object:\n", - " \"\"\"load the NER model from huggingface\"\"\"\n", - " print(\"Loading the model\")\n", - " return AutoModelForTokenClassification.from_pretrained(model_id)\n", + " # Concatenate the 'title' and truncated 'text'\n", + " dataset = dataset.map(lambda example: {\"title_text\": example[\"title\"] + \". \" + example[\"text\"]})\n", + " return dataset" + ] + }, + { + "cell_type": "markdown", + "id": "968e7b65-3f37-409f-b9cb-cf2a2ef6d462", + "metadata": {}, + "source": [ + "# Create the NER tokenizer and model\n", + "We now can add to our pipeline loading the tokenizer and model that will extract entities for us from text. The NER model here is finetuned on a BERT-base model. All the models are loaded from huggingface.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "9361f411-cb85-47ff-be7d-fc35c4019685", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "medium_articles\n", + "\n", + "medium_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "sampled_articles\n", + "\n", + "sampled_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "retriever\n", + "\n", + "retriever\n", + "SentenceTransformer\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset->medium_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "NER_model_id\n", + "\n", + "NER_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "model\n", + "\n", + "model\n", + "PreTrainedModel\n", + "\n", + "\n", + "\n", + "NER_model_id->model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "tokenizer\n", + "\n", + "tokenizer\n", + "PreTrainedTokenizer\n", + "\n", + "\n", + "\n", + "NER_model_id->tokenizer\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "device\n", + "\n", + "device\n", + "str\n", + "\n", + "\n", + "\n", + "device->retriever\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "ner_pipeline\n", + "\n", + "ner_pipeline\n", + "Pipeline\n", + "\n", + "\n", + "\n", + "device->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "model->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "tokenizer->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_retriever_inputs\n", + "\n", + "retriever_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "_retriever_inputs->retriever\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs\n", + "\n", + "max_text_length\n", + "int\n", + "sample_size\n", + "int\n", + "random_state\n", + "int\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "input\n", + "\n", + "input\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%incr_cell_to_module ner_module -i 2 --display\n", + "\n", + "import torch\n", + "from transformers import (\n", + " AutoModelForTokenClassification,\n", + " AutoTokenizer,\n", + " PreTrainedModel,\n", + " PreTrainedTokenizer,\n", + " pipeline,\n", + ")\n", + "from transformers.pipelines import base\n", + "\n", + "def device() -> str:\n", + " \"\"\"Whether this is a CUDA or CPU enabled device.\"\"\"\n", + " return \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", "\n", - "# load the tokenizer and model into a NER pipeline\n", - "def ner_pipeline(model: object, tokenizer: AutoTokenizer, device: int) -> base.Pipeline:\n", - " print(\"Loading the ner_pipeline\")\n", - " return pipeline(\n", - " \"ner\", model=model, tokenizer=tokenizer, aggregation_strategy=\"max\", device=device\n", - " )\n", "\n", - "def retriever(device: int) -> SentenceTransformer:\n", - " \"\"\"A retriever model is used to embed passages (article title + first 1000 characters) and queries. It creates embeddings such that queries and passages with similar meanings are close in the vector space. We will use a sentence-transformer model as our retriever. The model can be loaded as follows:\n", + "def NER_model_id() -> str:\n", + " \"\"\"Model ID to use\n", + " To extract named entities, we will use a NER model finetuned on a BERT-base model.\n", + " The model can be loaded from the HuggingFace model hub.\n", + " Use `overrides={\"NER_model_id\": VALUE}` to switch this without changing code.\n", " \"\"\"\n", - " print(\"Loading the retriever model\")\n", - " return SentenceTransformer(\n", - " \"flax-sentence-embeddings/all_datasets_v3_mpnet-base\", device=device\n", - " )\n", + " return \"dslim/bert-base-NER\"\n", + "\n", "\n", + "def tokenizer(NER_model_id: str) -> PreTrainedTokenizer:\n", + " \"\"\"Loads the tokenizer for the NER model ID from huggingface\"\"\"\n", + " return AutoTokenizer.from_pretrained(NER_model_id)\n", "\n", - "def db() -> lancedb.DBConnection:\n", - " return lancedb.connect(\"./.lancedb\")\n", "\n", - "def batch_size() -> int:\n", - " # we will use batches of 64\n", - " return 64\n", + "def model(NER_model_id: str) -> PreTrainedModel:\n", + " \"\"\"Loads the NER model from huggingface\"\"\"\n", + " return AutoModelForTokenClassification.from_pretrained(NER_model_id)\n", + "\n", + "\n", + "def ner_pipeline(\n", + " model: PreTrainedModel, tokenizer: PreTrainedTokenizer, device: str\n", + ") -> base.Pipeline:\n", + " \"\"\"Loads the tokenizer and model into a NER pipeline. That is it combines them.\"\"\"\n", + " device_no = torch.cuda.current_device() if device == \"cuda\" else None\n", + " return pipeline(\n", + " \"ner\", model=model, tokenizer=tokenizer, aggregation_strategy=\"max\", device=device_no\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "616b28da-2fb9-435d-bb58-98af161f8d89", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/stefankrawczyk/.pyenv/versions/3.10.4/envs/ner-example-py310/lib/python3.10/site-packages/huggingface_hub/file_download.py:1132: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n", + " warnings.warn(\n", + "Some weights of the model checkpoint at dslim/bert-base-NER were not used when initializing BertForTokenClassification: ['bert.pooler.dense.bias', 'bert.pooler.dense.weight']\n", + "- This IS expected if you are initializing BertForTokenClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n", + "- This IS NOT expected if you are initializing BertForTokenClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n" + ] + }, + { + "data": { + "text/plain": [ + "[[{'entity_group': 'ORG',\n", + " 'score': 0.9978863,\n", + " 'word': 'Mars Rover',\n", + " 'start': 4,\n", + " 'end': 14},\n", + " {'entity_group': 'ORG',\n", + " 'score': 0.99731904,\n", + " 'word': 'NASA',\n", + " 'start': 20,\n", + " 'end': 24}]]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# this is what the NER pipeline produces\n", + "text = \"The Mars Rover from NASA reached the red planet yesterday.\"\n", + "ner_pipeline(model(NER_model_id()), tokenizer(NER_model_id()), \"cpu\")([text])" + ] + }, + { + "cell_type": "markdown", + "id": "2ac38785-e69c-417e-b19a-6aa8c46d5550", + "metadata": {}, + "source": [ + "# Create the embedding model\n", + "Next we load the retriever model that will create embeddings, i.e. a vector/list of floats, that encode our text. Specifically it will embed passages (article title + first 1000 characters) and also be used to create an embedding from the search query that will be provided at inference time. It creates embeddings such that queries and passages with similar meanings are close in the vector space. We will use a sentence-transformer model as our retriever. The model can be loaded using the following code." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "934101e9-59b9-4002-8ab0-5aa289ca9f0a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "medium_articles\n", + "\n", + "medium_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "sampled_articles\n", + "\n", + "sampled_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "retriever\n", + "\n", + "retriever\n", + "SentenceTransformer\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset->medium_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "NER_model_id\n", + "\n", + "NER_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "model\n", + "\n", + "model\n", + "PreTrainedModel\n", + "\n", + "\n", + "\n", + "NER_model_id->model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "tokenizer\n", + "\n", + "tokenizer\n", + "PreTrainedTokenizer\n", + "\n", + "\n", + "\n", + "NER_model_id->tokenizer\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "device\n", + "\n", + "device\n", + "str\n", + "\n", + "\n", + "\n", + "device->retriever\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "ner_pipeline\n", + "\n", + "ner_pipeline\n", + "Pipeline\n", + "\n", + "\n", + "\n", + "device->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "model->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "tokenizer->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_retriever_inputs\n", + "\n", + "retriever_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "_retriever_inputs->retriever\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs\n", + "\n", + "max_text_length\n", + "int\n", + "sample_size\n", + "int\n", + "random_state\n", + "int\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "input\n", + "\n", + "input\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%incr_cell_to_module ner_module -i 3 --display\n", + "from sentence_transformers import SentenceTransformer\n", + "\n", + "def retriever(\n", + " device: str, retriever_model_id: str = \"flax-sentence-embeddings/all_datasets_v3_mpnet-base\"\n", + ") -> SentenceTransformer:\n", + " \"\"\"Our retriever model to create embeddings.\n", "\n", - "def batch(sampled_articles: pd.DataFrame, batch_size: int) -> Parallelizable[pd.DataFrame]:\n", - " # split the articles into batches\n", - " for i in range(0, len(sampled_articles), batch_size):\n", - " # find end of batch\n", - " i_end = min(i + batch_size, len(sampled_articles))\n", - " # extract batch\n", - " batch = sampled_articles.iloc[i:i_end].copy()\n", - " yield batch\n", + " A retriever model is used to embed passages (article title + first 1000 characters)\n", + " and queries. It creates embeddings such that queries and passages with similar\n", + " meanings are close in the vector space. We will use a sentence-transformer model\n", + " as our retriever. The model can be loaded as follows:\n", + " \"\"\"\n", + " return SentenceTransformer(retriever_model_id, device=device)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "14f7a7ef-4009-4458-9075-a91bcdcce73c", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[ 3.60962786e-02, -3.31540257e-02, 8.81905016e-03,\n", + " 4.30133902e-02, 2.57134414e-03, -9.96292103e-03,\n", + " 2.37981323e-02, 3.95706780e-02, -3.06305103e-02,\n", + " -7.25629227e-03, 2.78342664e-02, -6.24335743e-03,\n", + " 1.61879994e-02, 5.16220666e-02, -2.63889283e-02,\n", + " -4.60373871e-02, -1.31765818e-02, 1.28921298e-02,\n", + " 3.41649004e-03, 5.24031892e-02, -1.23013509e-02,\n", + " 5.96170649e-02, -2.11504893e-03, -2.70127449e-02,\n", + " -2.94139981e-03, -9.35570523e-03, -2.01370083e-02,\n", + " 1.44259995e-02, -1.74326338e-02, -4.83241268e-02,\n", + " -2.26764623e-02, 2.10481621e-02, -7.59536773e-03,\n", + " -4.23350669e-02, 8.56910187e-09, 4.39378107e-03,\n", + " 1.55015690e-02, -9.36352648e-03, -2.30528452e-02,\n", + " 4.53829952e-02, 2.28481553e-02, 7.45767634e-03,\n", + " -4.02662642e-02, 8.16163141e-03, 7.33977463e-03,\n", + " 6.47669807e-02, 3.71067561e-02, 5.97369075e-02,\n", + " -2.98210960e-02, 6.30364046e-02, 5.54212602e-03,\n", + " -8.19289126e-03, -3.08336155e-03, -1.38418591e-02,\n", + " 1.55548424e-01, 1.60365384e-02, -8.71572923e-03,\n", + " -3.56814340e-02, 2.89250687e-02, 8.45119804e-02,\n", + " 2.88498159e-02, 3.69493775e-02, 1.47784995e-02,\n", + " -3.88618093e-03, -9.33126081e-03, 4.83849421e-02,\n", + " 1.69343092e-02, -2.30700262e-02, 2.27935631e-02,\n", + " 2.94216126e-02, 7.52457082e-02, -1.10501163e-02,\n", + " 6.53248979e-03, 5.70445955e-02, -3.45314890e-02,\n", + " -5.34237772e-02, -7.21587427e-03, 8.86644349e-02,\n", + " -6.72957674e-02, -4.45436500e-03, -4.05082814e-02,\n", + " 1.90933086e-02, 1.49944937e-02, 2.62058433e-03,\n", + " -6.92337230e-02, 2.91210935e-02, -3.49241719e-02,\n", + " 7.13418995e-05, 3.01802810e-03, 2.35063839e-03,\n", + " -5.61731309e-02, -9.11250710e-03, 1.91500615e-02,\n", + " -1.59010589e-02, 5.99949211e-02, -3.04716192e-02,\n", + " -5.89178279e-02, -7.91868716e-02, 4.32029627e-02,\n", + " -2.97455639e-02, -3.12141869e-02, -2.72418000e-02,\n", + " -2.25155856e-02, 3.63038741e-02, -2.89564170e-02,\n", + " -1.44697009e-02, 2.85882056e-02, 3.80617008e-02,\n", + " -4.70239818e-02, 4.80013639e-02, 1.14646424e-02,\n", + " -2.21854951e-02, -6.22103997e-02, -1.82279553e-02,\n", + " -2.92093325e-02, 3.50595266e-02, 3.67511436e-02,\n", + " -5.65246725e-03, 4.00411002e-02, 4.38005757e-03,\n", + " -3.30929421e-02, 4.05450398e-03, 2.58010644e-02,\n", + " 2.59981975e-02, -5.12769772e-03, -1.87090952e-02,\n", + " 1.96436774e-02, -2.08113249e-03, -2.07201689e-02,\n", + " 3.95227000e-02, 2.37268265e-02, 6.40152171e-02,\n", + " 8.18362907e-02, 8.38510040e-03, -1.20331924e-02,\n", + " 5.18913865e-02, -3.95324975e-02, -1.27038434e-02,\n", + " 1.71059091e-02, 2.46613976e-02, -3.63002680e-02,\n", + " -2.93706506e-02, 1.01808575e-03, -4.56925109e-03,\n", + " -7.12503865e-03, 2.16140412e-02, 5.93632041e-03,\n", + " -2.52346676e-02, 2.48365477e-02, 3.33161354e-02,\n", + " -5.50414585e-02, 2.04023216e-02, -3.89450192e-02,\n", + " -5.56852855e-02, 1.64508075e-02, -1.77108273e-02,\n", + " 7.37611130e-02, -5.47070689e-02, 3.79128493e-02,\n", + " -4.76424489e-03, 1.55324098e-02, 3.15646902e-02,\n", + " -5.03879003e-02, 4.62604966e-03, 3.08783911e-02,\n", + " 5.00634573e-02, -6.51622564e-02, 3.44836083e-03,\n", + " -3.80689185e-03, -2.13992577e-02, 5.93210198e-03,\n", + " 9.66137741e-03, 2.43846960e-02, 7.57200345e-02,\n", + " -1.29039232e-02, 8.08231980e-02, -2.19743047e-03,\n", + " 1.85493231e-02, -7.74908438e-02, 5.33490926e-02,\n", + " 9.79409739e-03, -5.43809775e-03, -2.69306395e-02,\n", + " 1.41466148e-02, -3.78070064e-02, 4.91003208e-02,\n", + " 5.70112690e-02, 2.79056765e-02, 2.61175148e-02,\n", + " -2.50035003e-02, -5.67776710e-03, -2.96256109e-03,\n", + " 3.88234388e-04, -3.26094292e-02, 1.84452403e-02,\n", + " 1.64732281e-02, -5.52664399e-02, -2.42808014e-02,\n", + " 3.05803847e-02, 4.70533036e-03, 4.27689292e-02,\n", + " 2.96906605e-02, 4.54030745e-02, 4.79164794e-02,\n", + " 2.46673524e-02, 1.51533289e-02, 7.23499209e-02,\n", + " -4.98598032e-02, 5.23909926e-02, 3.02121956e-02,\n", + " 8.87216593e-04, 4.75466773e-02, -1.72570907e-02,\n", + " 1.12080984e-02, -1.26648881e-02, -1.69910006e-02,\n", + " 4.16054716e-03, -1.89250205e-02, -5.16009890e-02,\n", + " 3.31343338e-02, 2.45700008e-03, 4.01594676e-02,\n", + " -5.69379590e-02, 5.54539561e-02, 4.44991030e-02,\n", + " -2.80060563e-02, -1.36538371e-02, -1.00473007e-02,\n", + " -2.33908128e-02, 4.15213034e-03, -4.19142731e-02,\n", + " 5.46232471e-03, 3.80426273e-02, 1.97659284e-02,\n", + " -2.66039837e-02, 3.71009368e-03, -2.21916772e-02,\n", + " -8.75609890e-02, -2.90109701e-02, -6.08509555e-02,\n", + " 3.57717238e-02, 5.24172047e-03, -1.02223014e-03,\n", + " 2.02417001e-02, -8.86212941e-03, -4.80997078e-02,\n", + " -1.33355176e-02, 5.32028452e-03, 6.88390457e-04,\n", + " 9.89494286e-03, -3.98303708e-03, -2.71222810e-03,\n", + " 2.49915905e-02, 1.68020558e-02, 2.85249539e-02,\n", + " -1.22867841e-02, -1.26059232e-02, -1.02110012e-02,\n", + " 6.40016049e-02, 8.37008469e-03, 4.59938608e-02,\n", + " -3.00367512e-02, -2.70720273e-02, 5.37466840e-04,\n", + " 4.57689986e-02, 1.11166155e-02, -4.76786457e-02,\n", + " -4.55223732e-02, -4.03027646e-02, -1.53549947e-02,\n", + " -2.45466754e-02, 2.08480880e-02, 7.20439330e-02,\n", + " 4.96347398e-02, 2.49973685e-02, -3.96961672e-03,\n", + " -5.61507680e-02, 1.07510388e-03, -3.20086367e-02,\n", + " 1.80948917e-02, 1.27756372e-02, -1.42341442e-02,\n", + " -6.12534489e-03, -9.90991294e-03, -9.46271978e-03,\n", + " 3.77464481e-02, 1.68987773e-02, -7.38439709e-02,\n", + " -2.62885708e-02, 5.70751764e-02, 8.65498371e-03,\n", + " -2.94710528e-02, -7.22954609e-03, -2.57841013e-02,\n", + " 2.56506708e-02, -3.63825373e-02, 5.47279343e-02,\n", + " 3.73191424e-02, 7.21509680e-02, 7.65907988e-02,\n", + " 2.12801695e-02, -1.47611247e-02, -7.92645104e-03,\n", + " 5.18372795e-03, 4.15485464e-02, 1.87958721e-02,\n", + " -8.61883257e-03, -8.73448476e-02, 3.89653482e-02,\n", + " 1.36772767e-02, 8.71754251e-03, -3.39957047e-03,\n", + " -3.89559716e-02, -2.59164963e-02, -2.75201444e-02,\n", + " 9.06339940e-03, -2.74889991e-02, -1.20448750e-02,\n", + " -4.28144746e-02, -6.95132092e-02, -3.27884685e-03,\n", + " -1.39576485e-02, 1.10945562e-02, 1.90854818e-02,\n", + " -1.12206517e-02, -4.97540049e-02, -6.25470951e-02,\n", + " 9.18536633e-03, 5.13912737e-02, -3.07003036e-02,\n", + " 3.13394368e-02, 2.89901160e-02, -6.14287071e-02,\n", + " 4.82854582e-02, 3.91877145e-02, 5.23225404e-03,\n", + " -2.32016873e-02, 2.23461054e-02, 4.73749638e-02,\n", + " 2.00397186e-02, -4.97329831e-02, -3.63437682e-02,\n", + " 4.96318787e-02, -8.55642706e-02, -5.36590740e-02,\n", + " -6.84164762e-02, 2.26597078e-02, 1.21897319e-02,\n", + " -5.97152300e-02, -6.86289445e-02, 2.02023555e-02,\n", + " 3.99615839e-02, -3.89971510e-02, -2.59903707e-02,\n", + " -4.20581661e-02, -1.67649258e-02, 3.28528881e-03,\n", + " -1.54073536e-02, -1.28716780e-02, -3.22402827e-02,\n", + " 3.11157983e-02, 1.36079248e-02, -5.29781915e-02,\n", + " -2.45646909e-02, 2.02353336e-02, -1.05925892e-02,\n", + " -5.55798877e-03, 9.90392826e-03, 3.43095232e-03,\n", + " 1.02578243e-02, -4.14645672e-02, -7.71018397e-03,\n", + " -4.29285690e-03, 3.13787013e-02, 8.97466019e-03,\n", + " 3.63822468e-02, 6.27918029e-03, -8.00463557e-03,\n", + " 6.55080192e-03, -7.97851104e-03, 3.00842541e-04,\n", + " -4.07078750e-02, -8.72434396e-03, 1.35801286e-02,\n", + " -1.34013873e-02, 8.53130035e-03, -3.59485000e-02,\n", + " -3.83378863e-02, 7.85570443e-02, -7.81345740e-03,\n", + " -2.92420667e-03, -4.23185863e-02, -1.91911459e-02,\n", + " -4.74103875e-02, -9.45227873e-03, 4.83201398e-03,\n", + " -3.51743437e-02, 2.67341584e-02, -1.51325874e-02,\n", + " 2.68554810e-04, 3.15276929e-03, 5.03170155e-02,\n", + " 1.92302205e-02, 3.92147750e-02, 1.45246563e-02,\n", + " 5.26193483e-03, -3.38331163e-02, 6.62993581e-04,\n", + " -4.11011912e-02, 3.33439768e-03, 4.10068408e-02,\n", + " -2.29125712e-02, 9.55220088e-02, 3.63799930e-02,\n", + " 4.99948226e-02, 1.06081786e-03, 2.43593454e-02,\n", + " -3.74247809e-03, 2.16635019e-02, -4.70961304e-03,\n", + " -2.22381279e-02, 1.30076380e-02, 3.92022841e-02,\n", + " -4.55784472e-03, 1.15262605e-02, 8.73760693e-03,\n", + " 2.28593033e-03, -6.67265104e-03, 8.77784006e-03,\n", + " -2.90497020e-02, -3.49355116e-02, 2.00866582e-03,\n", + " -8.68706331e-02, 3.63544896e-02, -3.93404067e-02,\n", + " -1.09610453e-01, 1.73198956e-03, -6.42923173e-03,\n", + " -1.36668487e-02, -5.94285950e-02, 3.10135763e-02,\n", + " -2.96281464e-03, 7.39220381e-02, -5.66292228e-03,\n", + " -1.40256761e-02, 7.05927163e-02, -3.21498588e-02,\n", + " 1.28106093e-02, -4.65438589e-02, -3.63234505e-02,\n", + " -6.28645672e-03, -8.84195603e-03, -4.16300111e-02,\n", + " -2.92248707e-02, 2.20061969e-02, 1.07009709e-02,\n", + " -8.38732161e-03, -4.97266352e-02, -6.19839504e-02,\n", + " -5.66809392e-03, -1.37819005e-02, 1.10032108e-08,\n", + " -5.50880842e-02, 3.94730568e-02, -1.62433982e-02,\n", + " 4.45250012e-02, 4.34459820e-02, -3.24180685e-02,\n", + " 1.86627898e-02, 4.14110487e-04, -8.44709575e-03,\n", + " 7.95873441e-03, -2.65822858e-02, 1.14863897e-02,\n", + " -2.68137287e-02, -2.03993917e-02, -4.44443598e-02,\n", + " -7.90846273e-02, 3.23186889e-02, 1.95921995e-02,\n", + " -1.74563341e-02, 3.09612439e-03, -2.13709958e-02,\n", + " -5.17976247e-02, 1.02625117e-02, -1.46095185e-02,\n", + " -7.85705866e-04, -2.90651433e-02, -2.32787617e-02,\n", + " 4.79632542e-02, -2.69608293e-02, -4.38591242e-02,\n", + " -3.48058455e-02, 1.27649689e-02, 1.65850390e-02,\n", + " 4.80354093e-02, 2.48051025e-02, -3.45652290e-02,\n", + " 2.07881834e-02, 2.76347026e-02, 3.49310189e-02,\n", + " -1.36970505e-02, -1.46573661e-02, -2.92295665e-02,\n", + " -9.11438763e-02, 1.83510091e-02, 1.67318657e-02,\n", + " 4.89241304e-03, 6.91548362e-03, -1.55162951e-02,\n", + " 1.92494318e-02, 4.27412428e-03, -6.45834655e-02,\n", + " -5.60014099e-02, -8.51061475e-03, -6.69982433e-02,\n", + " 2.29862519e-02, -2.99270693e-02, 6.02340437e-02,\n", + " -1.12921549e-02, 2.84359679e-02, -4.08170968e-02,\n", + " 5.21569559e-03, -1.86661053e-02, -4.33015861e-02,\n", + " -1.27073908e-02, 8.43595248e-03, 2.64639854e-02,\n", + " -5.36046131e-03, 2.97620259e-02, -4.93885076e-04,\n", + " 1.29855145e-02, 2.29265802e-02, -1.37690296e-02,\n", + " -5.01243770e-02, 1.75731406e-02, 2.49196496e-02,\n", + " 5.08863367e-02, -2.86997180e-03, -4.78268117e-02,\n", + " 1.01278253e-01, 3.66900414e-02, 6.19485863e-02,\n", + " 3.21024540e-03, 7.56731778e-02, -2.11994573e-02,\n", + " -4.42590937e-02, -2.64976192e-02, -2.66530327e-02,\n", + " -2.09754724e-02, -6.58231229e-03, 3.46418619e-02,\n", + " -4.56248075e-02, 9.63507593e-03, -3.32122408e-02,\n", + " -2.24238628e-10, -5.45747019e-03, -6.03148006e-02,\n", + " -1.29531585e-02, 4.66388501e-02, -1.83035545e-02,\n", + " 8.13329406e-03, -2.12679580e-02, 3.94083606e-03,\n", + " 3.26677673e-02, 1.50923189e-02, -1.45858675e-02,\n", + " 2.18188744e-02, 1.21959923e-02, 1.52391512e-02,\n", + " -1.38983773e-02, -3.14303413e-02, 2.71212198e-02,\n", + " -1.26387030e-02, 4.07406501e-03, -2.38309093e-02,\n", + " 5.65349907e-02, 1.62084051e-03, 4.00588177e-02,\n", + " -6.29829988e-02, 1.13373147e-02, 2.39813961e-02,\n", + " -2.09959485e-02, -8.17801654e-02, 2.19626781e-02,\n", + " -1.11062312e-02, -2.92382967e-02, -6.56387489e-03,\n", + " -8.35734326e-03, 1.15756495e-02, -1.18839787e-02,\n", + " 9.94963497e-02, -7.10078515e-03, -3.85195948e-02,\n", + " -3.75379287e-02, 3.29218954e-02, -6.22654036e-02,\n", + " 8.00584239e-05, 3.03280260e-02, -3.60623526e-04,\n", + " 5.26082106e-02, -4.04131003e-02, 2.93430295e-02,\n", + " -1.63055733e-02, -1.33592132e-02, -2.18235105e-02,\n", + " 1.05328085e-02, 2.61274725e-02, -1.83648933e-02,\n", + " 1.02028791e-02, -3.07935383e-02, 1.24935098e-02,\n", + " 3.44416276e-02, 1.67941470e-02, -6.70520738e-02,\n", + " -1.46601954e-02, -3.90077084e-02, 3.68831418e-02,\n", + " -3.51772308e-02, -2.43946034e-02, 1.51652172e-02,\n", + " 5.97533248e-02, -4.58799116e-02, 4.62583499e-03,\n", + " -4.59687486e-02, -1.49805769e-02, 3.87360342e-02,\n", + " 3.69152017e-02, -9.81731620e-03, 6.09802119e-02,\n", + " -1.89866126e-02, -4.04221751e-02, -1.44595830e-02,\n", + " 1.90329887e-02, -7.29578510e-02, -1.61912106e-02,\n", + " 4.42078449e-02, 3.00968252e-02, -5.99590354e-02,\n", + " -1.73724014e-02, -4.17371513e-04, -5.59736714e-02,\n", + " -2.08146255e-02, 1.44553808e-02, -6.88614976e-03,\n", + " 3.04765478e-02, -2.06166115e-02, 1.63799357e-02,\n", + " 2.36149486e-02, 7.86401331e-03, -6.60859197e-02,\n", + " 5.93789592e-02, -2.23904233e-02, 1.63225979e-02,\n", + " -3.92370820e-02, -5.28906733e-02, -7.95797929e-02,\n", + " 5.23762591e-03, 1.59868654e-02, 5.77198192e-02,\n", + " 6.40571713e-02, 1.73853505e-02, 5.13109937e-02,\n", + " -1.59433540e-02, -8.83233324e-02, -1.93907954e-02,\n", + " -1.34620685e-02, -4.68650274e-03, 5.16196974e-02,\n", + " 2.87868734e-02, -1.12800738e-02, -2.01114248e-02,\n", + " 3.91260386e-02, 5.36062941e-02, -2.65886653e-02,\n", + " -2.63306983e-02, 6.47996319e-03, -2.41869520e-02,\n", + " -1.47397565e-02, 6.82561146e-03, 2.61856569e-03,\n", + " 3.40498760e-02, 3.45875360e-02, 2.95602926e-03,\n", + " 7.91043043e-02, -7.97181204e-03, 5.71655459e-04,\n", + " -9.14053235e-05, 4.37791101e-08, 2.43839417e-02,\n", + " 2.40845401e-02, 7.36697763e-02, 2.03841217e-02,\n", + " -4.34769057e-02, 1.09440416e-01, 3.75518426e-02,\n", + " 3.39452252e-02, -5.80685697e-02, 4.10885131e-03,\n", + " 5.68004027e-02, -2.88534556e-02, 1.13896681e-02,\n", + " 4.62091295e-03, -6.56217337e-02, -7.07318075e-04,\n", + " -3.28697041e-02, -6.30226508e-02, -4.00098562e-02,\n", + " -5.63813262e-02, 8.35973397e-02, -2.56133545e-03,\n", + " 1.48774534e-02, -1.49532510e-02, 1.81542169e-02,\n", + " -2.15953421e-02, -2.42270343e-02, -5.58906458e-02,\n", + " 7.23159835e-02, 9.63630155e-03, -9.02656745e-03,\n", + " -7.60971010e-02, -4.79021706e-02, 6.48496114e-03,\n", + " 6.59549655e-03, -6.96884394e-02, 2.36046128e-02,\n", + " 4.66829985e-02, 1.34313516e-02, 1.05467699e-01,\n", + " -2.92742476e-02, -3.37985717e-02, -3.49831954e-02,\n", + " -8.66085198e-03, 5.21409437e-02, 5.00857122e-02,\n", + " -1.01787001e-02, 2.89740656e-02, -6.33968040e-02,\n", + " 9.56533942e-03, 4.16928949e-03, 6.22191243e-02,\n", + " 4.69440082e-03, -2.36494374e-02, -4.71315579e-03,\n", + " -3.99447493e-02, 1.59526989e-02, -2.06302442e-02,\n", + " 3.75677249e-03, 1.65763125e-02, -5.27706966e-02,\n", + " 4.87393290e-02, -5.44341803e-02, 8.20284113e-02,\n", + " 3.47531103e-02, -4.45972905e-02, 2.54839137e-02,\n", + " -3.99673993e-33, 1.05813500e-02, -7.13590011e-02,\n", + " -2.64143776e-02, 2.19693445e-02, 4.34513204e-03,\n", + " -1.09376190e-02, 8.32656324e-02, -1.43483225e-02,\n", + " 3.95661704e-02, -4.32383418e-02, -3.50103481e-03]], dtype=float32)" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# what the embedding model produces\n", + "retriever(\"cpu\").encode([\"this is some text\"])" + ] + }, + { + "cell_type": "markdown", + "id": "1ad0e72d-af6f-4ecb-a283-283da149c305", + "metadata": {}, + "source": [ + "# Extracting entities & creating embeddings\n", + "Next let’s put this all together to extract entities & embed the documents.\n", "\n", - "def title_text(batch: pd.DataFrame) -> list[str]:\n", - " return batch[\"title_text\"].tolist()\n", + "We do this by using Huggingface dataset’s map functionality. Using this ensures that data can be loaded into batches to ensure that data hungry GPUs are appropriately fed with data. What you need to provide to this function is a function that contains the logic you want to apply to it. So below we create some helper functions for that purpose. This also helps ensure unit testability, while also keeping the code clean. We then wire these helper functions up to the map functions to create the vector embedding and named_entities columns on the dataset. \n", "\n", - "def embeddings(title_text: list[str], retriever: SentenceTransformer) -> list[list[float]]:\n", - " # generate embeddings for batch\n", - " return retriever.encode(title_text).tolist()\n", + "We then prepare this for loading into lancedb by using the `@save_to` data saver. This uses batching to write chunks of the dataset to lancedb." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "e3a57de3-3fc4-4693-8d73-cedbb3ba8060", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "final_dataset\n", + "\n", + "final_dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "load_into_lancedb\n", + "\n", + "\n", + "load_into_lancedb\n", + "HuggingFaceDSLanceDBSaver\n", + "\n", + "\n", + "\n", + "final_dataset->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "device\n", + "\n", + "device\n", + "str\n", + "\n", + "\n", + "\n", + "retriever\n", + "\n", + "retriever\n", + "SentenceTransformer\n", + "\n", + "\n", + "\n", + "device->retriever\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "ner_pipeline\n", + "\n", + "ner_pipeline\n", + "Pipeline\n", + "\n", + "\n", + "\n", + "device->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "model\n", + "\n", + "model\n", + "PreTrainedModel\n", + "\n", + "\n", + "\n", + "model->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "tokenizer\n", + "\n", + "tokenizer\n", + "PreTrainedTokenizer\n", + "\n", + "\n", + "\n", + "tokenizer->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles\n", + "\n", + "medium_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "sampled_articles\n", + "\n", + "sampled_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "retriever->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "columns_of_interest\n", + "\n", + "columns_of_interest\n", + "list\n", + "\n", + "\n", + "\n", + "columns_of_interest->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset->medium_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "sampled_articles->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "NER_model_id\n", + "\n", + "NER_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "NER_model_id->model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "NER_model_id->tokenizer\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "ner_pipeline->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_load_into_lancedb_inputs\n", + "\n", + "table_name\n", + "str\n", + "db_client\n", + "DBConnection\n", + "\n", + "\n", + "\n", + "_load_into_lancedb_inputs->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_retriever_inputs\n", + "\n", + "retriever_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "_retriever_inputs->retriever\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs\n", + "\n", + "max_text_length\n", + "int\n", + "sample_size\n", + "int\n", + "random_state\n", + "int\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "input\n", + "\n", + "input\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n", + "materializer\n", + "\n", + "\n", + "materializer\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%incr_cell_to_module ner_module -i 4 --display\n", + "from datasets.formatting.formatting import LazyBatch\n", + "from typing import Union\n", "\n", - "def entities(title_text: list[str], ner_pipeline: base.Pipeline) -> list[list[str]]:\n", + "def _extract_named_entities_text(\n", + " title_text_batch: Union[LazyBatch, list[str]], _ner_pipeline\n", + ") -> list[list[str]]:\n", + " \"\"\"Helper function to extract named entities given a batch of text.\"\"\"\n", " # extract named entities using the NER pipeline\n", - " extracted_batch = ner_pipeline(title_text)\n", + " extracted_batch = _ner_pipeline(title_text_batch)\n", + " # this should be extracted_batch = dataset.map(ner_pipeline)\n", " entities = []\n", " # loop through the results and only select the entity names\n", " for text in extracted_batch:\n", " ne = [entity[\"word\"] for entity in text]\n", " entities.append(ne)\n", - " return entities\n", - "\n", - "def named_entities(entities: list[list[str]]) -> list[list[str]]:\n", - " return [list(set(entity)) for entity in entities]\n", - "\n", - "def meta(batch: pd.DataFrame, named_entities: list[list[str]]) -> list[dict]:\n", - " # create a dataframe we want for metadata\n", - " df = batch.drop(\"title_text\", axis=1)\n", - " df[\"named_entities\"] = named_entities\n", - " return df.to_dict(orient=\"records\")\n", - "\n", - "def to_upsert(embeddings: list[list[float]],\n", - " meta: list[dict],\n", - " named_entities: list[list[str]]) -> list[tuple[list[float], dict, list[str]]]:\n", - " return list(zip(embeddings, meta, named_entities))\n", - "\n", - "def data(to_upsert: Collect[list[tuple[list[float], dict, list[str]]]]) -> list[dict]:\n", - " data = []\n", - " for result in to_upsert:\n", - " for emb, meta, entity in result:\n", - " temp = dict()\n", - " temp[\"vector\"] = np.array(emb)\n", - " temp[\"metadata\"] = meta\n", - " temp[\"named_entities\"] = entity\n", - " data.append(temp)\n", - " return data\n", - "\n", - "@config.when(mode=\"ingestion\")\n", - "def lancedb_table__ingestion(db: lancedb.DBConnection, data: list[dict], table_name: str = \"tw\") -> lancedb.table.Table:\n", - " tbl = db.create_table(table_name, data)\n", - " return tbl\n", + " _named_entities = [list(set(entity)) for entity in entities]\n", + " return _named_entities\n", "\n", - "@config.when(mode=\"query\")\n", - "def lancedb_table__query(db: lancedb.DBConnection, table_name: str = \"tw\") -> lancedb.table.Table:\n", - " tbl = db.open_table(table_name)\n", - " return tbl\n", "\n", - "def search_lancedb(query: str,\n", - " ner_pipeline: base.Pipeline,\n", - " retriever: SentenceTransformer,\n", - " lancedb_table: lancedb.table.Table) -> dict:\n", - " # extract named entities from the query\n", - " ne = entities([query], ner_pipeline)[0] # Note: we're directly calling the function here.\n", - " # create embeddings for the query\n", - " xq = retriever.encode(query).tolist()\n", - " # query the lancedb table while applying named entity filter\n", - " xc = lancedb_table.search(xq).to_list()\n", - " # extract article titles from the search result\n", - " r = [\n", - " x[\"metadata\"][\"title\"]\n", - " for x in xc\n", - " for i in x[\"metadata\"][\"named_entities\"]\n", - " if i in ne\n", - " ]\n", - " return {\"Extracted Named Entities\": ne, \"Result\": r}" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-11T00:37:00.900505Z", - "start_time": "2024-04-11T00:37:00.359482Z" - } - }, - "id": "68bbb0d987a85d06", - "execution_count": 37, - "outputs": [] + "def _batch_map(dataset: LazyBatch, _retriever, _ner_pipeline) -> dict:\n", + " \"\"\"Helper function to created the embedding vectors and extract named entities\"\"\"\n", + " title_text_list = dataset[\"title_text\"]\n", + " emb = _retriever.encode(title_text_list)\n", + " _named_entities = _extract_named_entities_text(title_text_list, _ner_pipeline)\n", + " return {\n", + " \"vector\": emb,\n", + " \"named_entities\": _named_entities,\n", + " }\n", + "\n", + "\n", + "def columns_of_interest() -> list[str]:\n", + " \"\"\"The columns we expect to pull from the dataset to be saved to lancedb\"\"\"\n", + " return [\"vector\", \"named_entities\", \"title\", \"url\", \"authors\", \"timestamp\", \"tags\"]\n", + "\n", + "\n", + "@save_to.lancedb(\n", + " db_client=source(\"db_client\"),\n", + " table_name=source(\"table_name\"),\n", + " columns_to_write=source(\"columns_of_interest\"),\n", + " output_name_=\"load_into_lancedb\",\n", + ")\n", + "def final_dataset(\n", + " sampled_articles: Dataset,\n", + " retriever: SentenceTransformer,\n", + " ner_pipeline: base.Pipeline,\n", + ") -> Dataset:\n", + " \"\"\"The final dataset to be pushed to lancedb.\n", + "\n", + " This adds two columns:\n", + "\n", + " - vector -- the vector embedding\n", + " - named_entities -- the names of entities extracted from the text\n", + " \"\"\"\n", + " # goes over the data in batches so that the GPU can be properly utilized.\n", + " final_ds = sampled_articles.map(\n", + " _batch_map,\n", + " batched=True,\n", + " fn_kwargs={\"_retriever\": retriever, \"_ner_pipeline\": ner_pipeline},\n", + " desc=\"extracting entities\",\n", + " )\n", + " return final_ds" + ] + }, + { + "cell_type": "markdown", + "id": "3b49f593-5820-4fc5-b1a8-f4a8433779b1", + "metadata": {}, + "source": [ + "# Load data into lancedb\n", + "\n", + "With our processing pipeline now ready, let's load some data into lancedb.\n", + "\n", + "We'll do this by instantiating a driver to execute our pipeline." + ] }, { "cell_type": "code", + "execution_count": 24, + "id": "ea373589-dfb2-408a-91f2-97e0f0aa029d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "final_dataset\n", + "\n", + "final_dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "load_into_lancedb\n", + "\n", + "\n", + "load_into_lancedb\n", + "HuggingFaceDSLanceDBSaver\n", + "\n", + "\n", + "\n", + "final_dataset->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "device\n", + "\n", + "device\n", + "str\n", + "\n", + "\n", + "\n", + "retriever\n", + "\n", + "retriever\n", + "SentenceTransformer\n", + "\n", + "\n", + "\n", + "device->retriever\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "ner_pipeline\n", + "\n", + "ner_pipeline\n", + "Pipeline\n", + "\n", + "\n", + "\n", + "device->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "model\n", + "\n", + "model\n", + "PreTrainedModel\n", + "\n", + "\n", + "\n", + "model->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "tokenizer\n", + "\n", + "tokenizer\n", + "PreTrainedTokenizer\n", + "\n", + "\n", + "\n", + "tokenizer->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles\n", + "\n", + "medium_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "sampled_articles\n", + "\n", + "sampled_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "retriever->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "columns_of_interest\n", + "\n", + "columns_of_interest\n", + "list\n", + "\n", + "\n", + "\n", + "columns_of_interest->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset->medium_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "sampled_articles->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "NER_model_id\n", + "\n", + "NER_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "NER_model_id->model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "NER_model_id->tokenizer\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "ner_pipeline->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_load_into_lancedb_inputs\n", + "\n", + "table_name\n", + "str\n", + "db_client\n", + "DBConnection\n", + "\n", + "\n", + "\n", + "_load_into_lancedb_inputs->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_retriever_inputs\n", + "\n", + "retriever_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "_retriever_inputs->retriever\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs\n", + "\n", + "max_text_length\n", + "int\n", + "sample_size\n", + "int\n", + "random_state\n", + "int\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "input\n", + "\n", + "input\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n", + "materializer\n", + "\n", + "\n", + "materializer\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "from hamilton import driver\n", + "from hamilton import driver, lifecycle\n", "dr = (\n", " driver.Builder()\n", - " .with_config({\"mode\": \"ingestion\"})\n", - " .with_modules(ner_search)\n", - " .enable_dynamic_execution(allow_experimental_mode=True)\n", + " .with_config({})\n", + " .with_modules(ner_module)\n", + " .with_adapters(lifecycle.PrintLn())\n", " .build()\n", ")\n", - "results = dr.execute([\"retriever\", \"ner_pipeline\", \"data\", \"lancedb_table\"])" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-11T00:11:41.091151Z", - "start_time": "2024-04-10T23:47:47.710760Z" - } - }, - "id": "9dd70c2ceecbbb71", - "execution_count": 35, - "outputs": [] + "dr" + ] }, { "cell_type": "code", - "source": [ - "dr.execute([\"search_lancedb\"], \n", - " inputs={\"query\": \"How Data is changing the world?\"},\n", - " overrides=results)\n" + "execution_count": 27, + "id": "a3dc66e6-c728-4d57-a08d-80b2aee72f40", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Executing node: columns_of_interest.\n", + "Finished debugging node: columns_of_interest in 258μs. Status: Success.\n", + "Executing node: medium_articles.load_data.dataset.\n", + "Finished debugging node: medium_articles.load_data.dataset in 1.85s. Status: Success.\n", + "Executing node: medium_articles.select_data.dataset.\n", + "Finished debugging node: medium_articles.select_data.dataset in 19.1μs. Status: Success.\n", + "Executing node: medium_articles.\n", + "Finished debugging node: medium_articles in 25μs. Status: Success.\n", + "Executing node: sampled_articles.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c465a0f0f02b43059944489be1ee336c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Filter: 0%| | 0/192368 [00:00\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "lancedb_result\n", + "\n", + "lancedb_result\n", + "dict\n", + "\n", + "\n", + "\n", + "final_dataset\n", + "\n", + "final_dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "load_into_lancedb\n", + "\n", + "\n", + "load_into_lancedb\n", + "HuggingFaceDSLanceDBSaver\n", + "\n", + "\n", + "\n", + "final_dataset->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "device\n", + "\n", + "device\n", + "str\n", + "\n", + "\n", + "\n", + "retriever\n", + "\n", + "retriever\n", + "SentenceTransformer\n", + "\n", + "\n", + "\n", + "device->retriever\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "ner_pipeline\n", + "\n", + "ner_pipeline\n", + "Pipeline\n", + "\n", + "\n", + "\n", + "device->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "model\n", + "\n", + "model\n", + "PreTrainedModel\n", + "\n", + "\n", + "\n", + "model->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "tokenizer\n", + "\n", + "tokenizer\n", + "PreTrainedTokenizer\n", + "\n", + "\n", + "\n", + "tokenizer->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles\n", + "\n", + "medium_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "sampled_articles\n", + "\n", + "sampled_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "lancedb_table\n", + "\n", + "lancedb_table\n", + "Table\n", + "\n", + "\n", + "\n", + "lancedb_table->lancedb_result\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "columns_of_interest\n", + "\n", + "columns_of_interest\n", + "list\n", + "\n", + "\n", + "\n", + "columns_of_interest->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "retriever->lancedb_result\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "retriever->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset->medium_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "sampled_articles->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "NER_model_id\n", + "\n", + "NER_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "NER_model_id->model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "NER_model_id->tokenizer\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "ner_pipeline->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "named_entities\n", + "\n", + "named_entities\n", + "list\n", + "\n", + "\n", + "\n", + "ner_pipeline->named_entities\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "named_entities->lancedb_result\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_lancedb_result_inputs\n", + "\n", + "prefilter\n", + "bool\n", + "top_k\n", + "int\n", + "query\n", + "str\n", + "\n", + "\n", + "\n", + "_lancedb_result_inputs->lancedb_result\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_load_into_lancedb_inputs\n", + "\n", + "table_name\n", + "str\n", + "db_client\n", + "DBConnection\n", + "\n", + "\n", + "\n", + "_load_into_lancedb_inputs->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_lancedb_table_inputs\n", + "\n", + "table_name\n", + "str\n", + "db_client\n", + "DBConnection\n", + "\n", + "\n", + "\n", + "_lancedb_table_inputs->lancedb_table\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_retriever_inputs\n", + "\n", + "retriever_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "_retriever_inputs->retriever\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs\n", + "\n", + "max_text_length\n", + "int\n", + "sample_size\n", + "int\n", + "random_state\n", + "int\n", + "\n", + "\n", + "\n", + "_sampled_articles_inputs->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_named_entities_inputs\n", + "\n", + "query\n", + "str\n", + "\n", + "\n", + "\n", + "_named_entities_inputs->named_entities\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "input\n", + "\n", + "input\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n", + "materializer\n", + "\n", + "\n", + "materializer\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" } - }, - "id": "e5686dd7ee49b2e4", - "execution_count": null, - "outputs": [] + ], + "source": [ + "%%incr_cell_to_module ner_module -i 5 --display \n", + "\n", + "import lancedb\n", + "import numpy as np\n", + "\n", + "def named_entities(query: str, ner_pipeline: base.Pipeline) -> list[str]:\n", + " \"\"\"The entities to extract from the query via the pipeline.\"\"\"\n", + " return _extract_named_entities_text([query], ner_pipeline)[0]\n", + "\n", + "def lancedb_table(db_client: lancedb.DBConnection, table_name: str = \"tw\") -> lancedb.table.Table:\n", + " \"\"\"Table to query against\"\"\"\n", + " tbl = db_client.open_table(table_name)\n", + " return tbl\n", + "\n", + "\n", + "def lancedb_result(\n", + " query: str,\n", + " named_entities: list[str],\n", + " retriever: SentenceTransformer,\n", + " lancedb_table: lancedb.table.Table,\n", + " top_k: int = 10,\n", + " prefilter: bool = True,\n", + ") -> dict:\n", + " \"\"\"Result of querying lancedb.\n", + "\n", + " :param query: the query\n", + " :param named_entities: the named entities found in the query\n", + " :param retriever: the model to create the embedding from the query\n", + " :param lancedb_table: the lancedb table to query against\n", + " :param top_k: number of top results\n", + " :param prefilter: whether to prefilter results before cosine distance\n", + " :return: dictionary result\n", + " \"\"\"\n", + " # create embeddings for the query\n", + " query_vector = np.array(retriever.encode(query).tolist())\n", + "\n", + " # query the lancedb table\n", + " query_builder = lancedb_table.search(query_vector, vector_column_name=\"vector\")\n", + " if named_entities:\n", + " # applying named entity filter if something was returned\n", + " where_clause = f\"array_length(array_intersect({named_entities}, named_entities)) > 0\"\n", + " query_builder = query_builder.where(where_clause, prefilter=prefilter)\n", + " result = (\n", + " query_builder.select([\"title\", \"url\", \"named_entities\"]) # what to return\n", + " .limit(top_k)\n", + " .to_list()\n", + " )\n", + " # could rerank results here\n", + " return {\"Query\": query, \"Query Entities\": named_entities, \"Result\": result}\n" + ] + }, + { + "cell_type": "markdown", + "id": "ec0d830d-83c4-4ad7-b64c-003922393852", + "metadata": {}, + "source": [ + "# Execute some queries\n", + "\n", + "We can now run a few queries against what's in lancedb." + ] }, { "cell_type": "code", + "execution_count": 32, + "id": "dcfd624c-b0d5-474c-b7d3-3dc01a24b8fe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Executing node: NER_model_id.\n", + "Finished debugging node: NER_model_id in 298μs. Status: Success.\n", + "Executing node: model.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/stefankrawczyk/.pyenv/versions/3.10.4/envs/ner-example-py310/lib/python3.10/site-packages/huggingface_hub/file_download.py:1132: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n", + " warnings.warn(\n", + "Some weights of the model checkpoint at dslim/bert-base-NER were not used when initializing BertForTokenClassification: ['bert.pooler.dense.bias', 'bert.pooler.dense.weight']\n", + "- This IS expected if you are initializing BertForTokenClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n", + "- This IS NOT expected if you are initializing BertForTokenClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished debugging node: model in 394ms. Status: Success.\n", + "Executing node: tokenizer.\n", + "Finished debugging node: tokenizer in 131ms. Status: Success.\n", + "Executing node: device.\n", + "Finished debugging node: device in 27.9μs. Status: Success.\n", + "Executing node: ner_pipeline.\n", + "Finished debugging node: ner_pipeline in 2.23ms. Status: Success.\n", + "Executing node: named_entities.\n", + "Finished debugging node: named_entities in 27.5ms. Status: Success.\n", + "Executing node: retriever.\n", + "Finished debugging node: retriever in 1.11s. Status: Success.\n", + "Executing node: lancedb_table.\n", + "Finished debugging node: lancedb_table in 240μs. Status: Success.\n", + "Executing node: lancedb_result.\n", + "Finished debugging node: lancedb_result in 110ms. Status: Success.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'lancedb_result': {'Query': 'What is the future of autonomous vehicles?',\n", + " 'Query Entities': [],\n", + " 'Result': [{'title': 'Automated large scale data generation for autonomous vehicle',\n", + " 'url': 'https://medium.com/mars-auto/automated-large-scale-data-generation-for-autonomous-vehicle-59de8b26357e',\n", + " 'named_entities': ['Busan', 'Seoul'],\n", + " '_distance': 1.1834863424301147},\n", + " {'title': 'How to Scale Up Tech Solutions and Amplify Their Sustainability Impacts',\n", + " 'url': 'https://medium.com/ksapa/how-to-scale-up-tech-solutions-and-amplify-their-sustainability-impacts-5124b192294d',\n", + " 'named_entities': ['Augmented Reality',\n", + " 'AI',\n", + " 'Solutions',\n", + " 'IoT',\n", + " 'Machine Learning',\n", + " 'Internet of Things',\n", + " 'Virtual Reality',\n", + " 'Global Goals',\n", + " 'AR'],\n", + " '_distance': 1.3800410032272339},\n", + " {'title': 'Tech vs. Regulators: A Case In Point',\n", + " 'url': 'https://medium.com/@nimishaagr/tech-vs-regulators-a-case-in-point-3959b8c81d27',\n", + " 'named_entities': ['UK',\n", + " 'New Delhi',\n", + " 'Lianhao Qu',\n", + " 'Chinese',\n", + " 'Hong Kong',\n", + " 'Unsplash',\n", + " 'Comparitech',\n", + " 'Christian Lange',\n", + " 'In Point'],\n", + " '_distance': 1.5780136585235596},\n", + " {'title': 'Not the neighborhood he left: Biden’s international challenge',\n", + " 'url': 'https://medium.com/@info-63603/not-the-neighborhood-he-left-bidens-international-challenge-d023f7ed26d0',\n", + " 'named_entities': ['United States',\n", + " 'Joe Biden',\n", + " 'Russia',\n", + " 'American',\n", + " 'Trump',\n", + " 'Biden',\n", + " 'China'],\n", + " '_distance': 1.5939494371414185},\n", + " {'title': '⚡ Mega US Solar project announced, UK readies for 12GW Renewables Auction, Carbon Pricing as a Motivator, and Blockchain — Climate friend or foe?',\n", + " 'url': 'https://medium.com/the-carbon-cut/mega-us-solar-project-announced-uk-readies-for-12gw-renewables-auction-carbon-pricing-as-a-24f026c13a1e',\n", + " 'named_entities': ['UK',\n", + " 'Mega US Solar',\n", + " 'America',\n", + " 'Carbon Pricing',\n", + " 'Carbon',\n", + " 'Blockchain',\n", + " 'U. S',\n", + " 'Jakub Rzeplinski',\n", + " 'Goldman Sachs',\n", + " 'Cut',\n", + " 'Energy Ministry',\n", + " 'Power',\n", + " 'Energy Industry',\n", + " 'European Environment Agency',\n", + " 'American Wind Energy Association',\n", + " 'Contracts for Difference Round Four',\n", + " 'Auction',\n", + " 'Solar Project'],\n", + " '_distance': 1.6431432962417603},\n", + " {'title': 'Article Comment: We fight networks by realizing we are networks.',\n", + " 'url': 'https://medium.com/greyswandigital/michael-in-times-of-crisis-when-verification-of-information-sources-may-be-incomplete-we-also-f8788439e9c4',\n", + " 'named_entities': ['Michael'],\n", + " '_distance': 1.6467256546020508},\n", + " {'title': 'The EU-Asia Connectivity Strategy',\n", + " 'url': 'https://medium.com/freeman-spogli-institute-for-international-studies/the-eu-asia-connectivity-strategy-8ce605a5d8a4',\n", + " 'named_entities': ['East Asia',\n", + " 'EU',\n", + " 'Stanford University',\n", + " 'Europe',\n", + " 'Central',\n", + " 'BRI',\n", + " 'Justin Tomczyk',\n", + " 'Economic Policy Research Center',\n", + " 'East European',\n", + " 'Belt',\n", + " 'and Road',\n", + " 'Southeast',\n", + " 'Eurasian Studies',\n", + " 'European Commission',\n", + " 'Initiative',\n", + " 'FSI Global',\n", + " 'Asia Connectivity Strategy',\n", + " 'Russian'],\n", + " '_distance': 1.6500868797302246},\n", + " {'title': 'Police Brutality During COVID19 Continues Unabated',\n", + " 'url': 'https://extremearturo.medium.com/police-brutality-during-covid19-continues-unabated-10d427c4225c',\n", + " 'named_entities': ['America',\n", + " 'United Nations',\n", + " 'Asia',\n", + " 'United States',\n", + " 'Americans',\n", + " 'Latin America',\n", + " 'COVID19',\n", + " 'UN',\n", + " 'Fibonacci Blue',\n", + " 'Africa',\n", + " 'Creative Commons'],\n", + " '_distance': 1.6865490674972534},\n", + " {'title': 'Poeple’s of Israel are protesting against Netanyahu',\n", + " 'url': 'https://medium.com/@bazranorotlo/poeples-of-israel-are-protesting-against-netanyahu-cc31b4a04a8f',\n", + " 'named_entities': ['Benny Gantz',\n", + " 'Blue and White',\n", + " 'Poeple ’ s',\n", + " 'Knesset',\n", + " 'COVID - 19',\n", + " 'Saudi Arabia',\n", + " 'United Arab Emirates',\n", + " 'Gantz',\n", + " 'Bahrain',\n", + " 'Likud',\n", + " 'Israel',\n", + " 'Netanyahu'],\n", + " '_distance': 1.6886379718780518},\n", + " {'title': 'Weekly Digest: New Livestream, #10YearsChallenge and an Ultimate Guide to our Customer Support',\n", + " 'url': 'https://medium.com/crypterium/weeklydigest-livestream-10yearchallenge-support-2733b5f9f944',\n", + " 'named_entities': ['Lisbon',\n", + " 'Rafael Carrascosa',\n", + " 'Facebook',\n", + " 'Moscow',\n", + " 'Rafael',\n", + " 'Crypterium',\n", + " 'Hong Kong',\n", + " 'Youtube',\n", + " 'Global Partnerships',\n", + " 'London',\n", + " 'GMT'],\n", + " '_distance': 1.6984761953353882}]}}" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "dr_inference_only = (\n", + "dr_query = (\n", " driver.Builder()\n", - " .with_config({\"mode\": \"query\"})\n", - " .with_modules(ner_search)\n", + " .with_config({})\n", + " .with_modules(ner_module)\n", + " .with_adapters(lifecycle.PrintLn())\n", " .build()\n", ")\n", - "dr_inference_only.execute([\"search_lancedb\"], \n", - " inputs={\"query\": \"How Data is changing the world?\", \"table_name\": \"temp1\"},\n", - " )" + "dr_query.execute([\"lancedb_result\"], \n", + " inputs={\"table_name\": table_name, \n", + " \"query\": \"What is the future of autonomous vehicles?\",\n", + " \"db_client\": db_client\n", + " })" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "f6d03340-1e21-4dfc-bfbf-6f06ee44c2a2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Executing node: NER_model_id.\n", + "Finished debugging node: NER_model_id in 184μs. Status: Success.\n", + "Executing node: model.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of the model checkpoint at dslim/bert-base-NER were not used when initializing BertForTokenClassification: ['bert.pooler.dense.bias', 'bert.pooler.dense.weight']\n", + "- This IS expected if you are initializing BertForTokenClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n", + "- This IS NOT expected if you are initializing BertForTokenClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished debugging node: model in 988ms. Status: Success.\n", + "Executing node: tokenizer.\n", + "Finished debugging node: tokenizer in 137ms. Status: Success.\n", + "Executing node: device.\n", + "Finished debugging node: device in 24.1μs. Status: Success.\n", + "Executing node: ner_pipeline.\n", + "Finished debugging node: ner_pipeline in 1.93ms. Status: Success.\n", + "Executing node: named_entities.\n", + "Finished debugging node: named_entities in 27.9ms. Status: Success.\n", + "Executing node: retriever.\n", + "Finished debugging node: retriever in 9.37s. Status: Success.\n", + "Executing node: lancedb_table.\n", + "Finished debugging node: lancedb_table in 510μs. Status: Success.\n", + "Executing node: lancedb_result.\n", + "Finished debugging node: lancedb_result in 38.3ms. Status: Success.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'lancedb_result': {'Query': 'Who is Joe Biden?',\n", + " 'Query Entities': ['Joe Biden'],\n", + " 'Result': [{'title': 'Not the neighborhood he left: Biden’s international challenge',\n", + " 'url': 'https://medium.com/@info-63603/not-the-neighborhood-he-left-bidens-international-challenge-d023f7ed26d0',\n", + " 'named_entities': ['United States',\n", + " 'Joe Biden',\n", + " 'Russia',\n", + " 'American',\n", + " 'Trump',\n", + " 'Biden',\n", + " 'China'],\n", + " '_distance': 0.9555794596672058}]}}" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } ], + "source": [ + "dr_query.execute(\n", + " [\"lancedb_result\"], \n", + " inputs={\n", + " \"table_name\": table_name, \n", + " \"query\": \"Who is Joe Biden?\",\n", + " \"db_client\": db_client\n", + " }\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "9dca46791620e5a6", "metadata": { "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-11T01:26:54.514537Z", - "start_time": "2024-04-11T01:26:50.632905Z" + "jupyter": { + "outputs_hidden": false } }, - "id": "2864676c0f73d8f2", - "execution_count": 40, - "outputs": [] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Executing node: NER_model_id.\n", + "Finished debugging node: NER_model_id in 99.9μs. Status: Success.\n", + "Executing node: model.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Some weights of the model checkpoint at dslim/bert-base-NER were not used when initializing BertForTokenClassification: ['bert.pooler.dense.bias', 'bert.pooler.dense.weight']\n", + "- This IS expected if you are initializing BertForTokenClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n", + "- This IS NOT expected if you are initializing BertForTokenClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished debugging node: model in 273ms. Status: Success.\n", + "Executing node: tokenizer.\n", + "Finished debugging node: tokenizer in 130ms. Status: Success.\n", + "Executing node: device.\n", + "Finished debugging node: device in 25.3μs. Status: Success.\n", + "Executing node: ner_pipeline.\n", + "Finished debugging node: ner_pipeline in 1.78ms. Status: Success.\n", + "Executing node: named_entities.\n", + "Finished debugging node: named_entities in 27.5ms. Status: Success.\n", + "Executing node: retriever.\n", + "Finished debugging node: retriever in 1.15s. Status: Success.\n", + "Executing node: lancedb_table.\n", + "Finished debugging node: lancedb_table in 149μs. Status: Success.\n", + "Executing node: lancedb_result.\n", + "Finished debugging node: lancedb_result in 27.1ms. Status: Success.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'lancedb_result': {'Query': 'How Data is changing the world?',\n", + " 'Query Entities': ['Data'],\n", + " 'Result': []}}" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dr_query.execute(\n", + " [\"lancedb_result\"], \n", + " inputs={\n", + " \"table_name\": table_name, \n", + " \"query\": \"How Data is changing the world?\",\n", + " \"db_client\": db_client\n", + " }\n", + ")" + ] }, { - "cell_type": "code", + "metadata": {}, + "cell_type": "markdown", "source": [ - "dr_inference_only.execute([\"search_lancedb\"], \n", - " inputs={\"query\": \"How Data is changing the world?\", \"table_name\": \"temp1\"},\n", - " )" + "# Summary\n", + "In this notebook we:\n", + "\n", + "1. incrementally created a pipeline to process documents\n", + "2. the pipeline extracted named entities\n", + "3. the pipeline created vectors embeddings from text\n", + "4. we pushed all the data into lanceDB to then query against\n", + "\n", + "# Extensions\n", + "There's many ways to extend this pipeline. Here are a few ideas:\n", + "\n", + "1. Use a different NER model.\n", + "2. Use a different embedding model.\n", + "3. Use a different database.\n", + "4. Use more data to filter the results by, e.g. ACLs if applicable.\n", + "5. Use query expansion to improve the results by expanding the extracted entities from the query.\n", + "6. Use a re-ranking algorithm to rank the results.\n", + "7. Work on document chunking to optimize for your particular RAG use case." ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-04-11T04:18:47.049954Z", - "start_time": "2024-04-11T04:18:43.767788Z" - } - }, - "id": "cb661621910276ca", - "execution_count": 42, - "outputs": [] + "id": "92cc0de121f3a3f5" }, { + "metadata": {}, "cell_type": "code", - "source": [], - "metadata": { - "collapsed": false - }, - "id": "9dca46791620e5a6", + "outputs": [], "execution_count": null, - "outputs": [] + "source": "", + "id": "54c5cf4aea8e2665" } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.10.4" } }, "nbformat": 4, diff --git a/hamilton/plugins/huggingface_extensions.py b/hamilton/plugins/huggingface_extensions.py index 3f6ec8d74..b20c08057 100644 --- a/hamilton/plugins/huggingface_extensions.py +++ b/hamilton/plugins/huggingface_extensions.py @@ -36,6 +36,8 @@ @dataclasses.dataclass class HuggingFaceDSLoader(DataLoader): + """Data loader for hugging face datasets. Uses load_data method.""" + path: str dataset_name: Optional[str] = None # this can't be `name` because it clashes with `.name()` data_dir: Optional[str] = None @@ -77,8 +79,8 @@ def _get_loading_kwargs(self) -> dict: return kwargs - def load_data(self, type_: Type) -> Tuple[Union[HF_types], dict[str, Any]]: - """""" + def load_data(self, type_: Type) -> Tuple[Union[HF_types], Dict[str, Any]]: + """Loads the data set given the path and class values.""" ds = load_dataset(self.path, **self._get_loading_kwargs()) is_dataset = isinstance(ds, Dataset) f_meta = {"path": self.path} @@ -95,6 +97,8 @@ def name(cls) -> str: @dataclasses.dataclass class HuggingFaceDSParquetSaver(DataSaver): + """Saves a Huggingface dataset to parquet.""" + path_or_buf: Union[PathLike, BinaryIO] batch_size: Optional[int] = None parquet_writer_kwargs: Optional[dict] = None @@ -110,7 +114,7 @@ def applies_to(cls, type_: Type[Type]) -> bool: def _get_saving_kwargs(self) -> dict: # Puts kwargs in a dict kwargs = dataclasses.asdict(self) - # but we send it separately + # we put path_or_buff as a positional argument del kwargs["path_or_buf"] parquet_writer_kwargs: Optional[dict] = kwargs.pop("parquet_writer_kwargs", None) if parquet_writer_kwargs: @@ -120,6 +124,7 @@ def _get_saving_kwargs(self) -> dict: return kwargs def save_data(self, ds: Union[HF_types]) -> Dict[str, Any]: + """Saves the data to parquet.""" is_dataset = isinstance(ds, Dataset) ds.to_parquet(self.path_or_buf, **self._get_saving_kwargs()) ds_meta = { @@ -139,6 +144,7 @@ def name(cls) -> str: return "parquet" +# we do this here just in case lancedb is not installed. if lancedb is not None: def _batch_write( @@ -158,6 +164,8 @@ def _batch_write( @dataclasses.dataclass class HuggingFaceDSLanceDBSaver(DataSaver): + """Data saver that saves Huggingface datasets to lancedb.""" + db_client: lancedb.DBConnection table_name: str columns_to_write: list[str] = None # None means all. @@ -168,6 +176,7 @@ def applicable_types(cls) -> Collection[Type]: return list(HF_types) def save_data(self, ds: Union[HF_types]) -> Dict[str, Any]: + """This batches writes to lancedb.""" ds.map( _batch_write, batched=True, From 328c2271a14c4b7baa611b307e6b0b279a768b65 Mon Sep 17 00:00:00 2001 From: Stefan Krawczyk Date: Tue, 21 May 2024 14:09:36 -0700 Subject: [PATCH 5/5] Finishes NER example Makes some changes to make sure things run on google collab. Plus some minor documentation / wording updates. --- examples/LLM_Workflows/NER_Example/README.md | 22 +- .../NER_Example/lancedb_module.py | 13 +- .../LLM_Workflows/NER_Example/notebook.ipynb | 2403 ++++++++--------- examples/LLM_Workflows/NER_Example/run.py | 24 +- 4 files changed, 1120 insertions(+), 1342 deletions(-) diff --git a/examples/LLM_Workflows/NER_Example/README.md b/examples/LLM_Workflows/NER_Example/README.md index 090282ab7..00bc5851a 100644 --- a/examples/LLM_Workflows/NER_Example/README.md +++ b/examples/LLM_Workflows/NER_Example/README.md @@ -1,12 +1,26 @@ # Document processing with Named Entity Recognition (NER) for RAG -This example demonstrates how to use the Named Entity Recognition (NER) model to extract entities from a document. -This extra metadata can be used when querying over the documents in the RAG model to filter to the documents -that contain the entities of interest. +This example demonstrates how to use a Named Entity Recognition (NER) model to extract entities from text along +with embeddings to facilitate querying with more precision. Specifically we'll use the entities here to filter to +the documents that contain the entities of interest. + +In general the concept we're showing here, is that if you extract extra metadata, like the entities text mentions, +this can be used when trying to find the most relevant text to pass to an LLM in a retrieval augmented generation (RAG) +context. The pipeline we create can be seen in the image below. ![pipeine](ner_extraction_pipeline.png) -To run this example: +To run this in a notebook: + +1. Install the requirements by running `pip install -r requirements.txt`. +2. Install `jupyter` by running `pip install jupyter`. +3. Run `jupyter notebook` in the current directory and open `notebook.ipynb`. + +Alternatively open this notebook in Google Colab by clicking the button below: + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/dagworks-inc/hamilton/blob/main/examples/LLM_Workflows/NER_Example/notebook.ipynb) + +To run this example via the commandline : 1. Install the requirements by running `pip install -r requirements.txt` 2. Run the script `python run.py`. Some example commands: diff --git a/examples/LLM_Workflows/NER_Example/lancedb_module.py b/examples/LLM_Workflows/NER_Example/lancedb_module.py index 8de3dec56..790898419 100644 --- a/examples/LLM_Workflows/NER_Example/lancedb_module.py +++ b/examples/LLM_Workflows/NER_Example/lancedb_module.py @@ -28,12 +28,13 @@ def _write_to_lancedb( return len(data) -def _batch_write(dataset_batch: LazyBatch, db, table_name, other_columns) -> None: +def _batch_write(dataset_batch: LazyBatch, db, table_name, columns_of_interest) -> None: """Helper function to batch write to lancedb.""" # we pull out the pyarrow table and select what we want from it - _write_to_lancedb( - dataset_batch.pa_table.select(["vector", "named_entities"] + other_columns), db, table_name - ) + if columns_of_interest is not None: + _write_to_lancedb(dataset_batch.pa_table.select(columns_of_interest), db, table_name) + else: + _write_to_lancedb(dataset_batch.pa_table, db, table_name) return None @@ -41,7 +42,7 @@ def loaded_lancedb_table( final_dataset: Dataset, db_client: lancedb.DBConnection, table_name: str, - metadata_of_interest: list[str], + columns_of_interest: list[str], write_batch_size: int = 100, ) -> lancedb.table.Table: """Loads the data into lancedb explicitly -- but we lose some visibility this way. @@ -55,7 +56,7 @@ def loaded_lancedb_table( fn_kwargs={ "db": db_client, "table_name": table_name, - "other_columns": metadata_of_interest, + "columns_of_interest": columns_of_interest, }, desc="writing to lancedb", ) diff --git a/examples/LLM_Workflows/NER_Example/notebook.ipynb b/examples/LLM_Workflows/NER_Example/notebook.ipynb index 68739729c..79d202089 100644 --- a/examples/LLM_Workflows/NER_Example/notebook.ipynb +++ b/examples/LLM_Workflows/NER_Example/notebook.ipynb @@ -12,7 +12,7 @@ }, "outputs": [], "source": [ - "!pip install sentence_transformers datasets lancedb sf-hamilton -qU" + "!pip install sentence_transformers datasets lancedb sf-hamilton[visualization] -qU" ] }, { @@ -25,8 +25,8 @@ } }, "source": [ - "# How to use Lancedb with NER semantic search for RAG \n", - "In this post we’ll walk through an example pipeline written in Hamilton to embed some text, and also capture extra metadata about the text that will be used when looking up data for RAG via semantic search with LanceDB.\n", + "# How to use Lancedb with NER semantic search \\[for RAG\\]\n", + "In this post we’ll walk through an example pipeline written in Hamilton to embed some text, and also capture extra metadata about the text that can be used when deciding what data to pull for RAG. This is a form of \"semantic search\" and we use LanceDB to store our data and query over it.\n", "\n", "Why capture, or rather extract (as you’ll see), extra metadata? Because you can use it to filter results to improve accuracy. You’ll need more than just cosine similarity to achieve a quality system [\\[1\\]](https://jxnl.co/writing/2024/05/11/low-hanging-fruit-for-rag-search/). [Named Entity Recognition (NER)](https://en.wikipedia.org/wiki/Named-entity_recognition) is just one approach to gather extra metadata from text that can be used for this purpose.\n", "\n", @@ -80,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 2, "id": "48fb3804-c302-4275-8da3-6563a62c2e2a", "metadata": {}, "outputs": [ @@ -93,98 +93,98 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", - "\n", - "\n", - "\n", - "medium_articles\n", - "\n", - "medium_articles\n", - "Dataset\n", + "\n", + "Legend\n", "\n", "\n", - "\n", + "\n", "sampled_articles\n", - "\n", - "sampled_articles\n", - "Dataset\n", + "\n", + "sampled_articles\n", + "Dataset\n", "\n", - "\n", - "\n", - "medium_articles->sampled_articles\n", - "\n", - "\n", + "\n", + "\n", + "medium_articles.load_data.dataset\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", "\n", "\n", - "\n", - "medium_articles.select_data.dataset\n", - "\n", - "medium_articles.select_data.dataset\n", - "Dataset\n", - "\n", - "\n", - "\n", - "medium_articles.select_data.dataset->medium_articles\n", - "\n", - "\n", - "\n", - "\n", "\n", - "medium_articles.load_data.dataset\n", - "\n", - "medium_articles.load_data.dataset\n", - "Tuple\n", + "medium_articles.select_data.dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", "\n", "\n", "\n", "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles\n", + "\n", + "medium_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset->medium_articles\n", + "\n", + "\n", "\n", "\n", "\n", "_sampled_articles_inputs\n", - "\n", - "max_text_length\n", - "int\n", - "sample_size\n", - "int\n", - "random_state\n", - "int\n", + "\n", + "sample_size\n", + "int\n", + "max_text_length\n", + "int\n", + "random_state\n", + "int\n", "\n", "\n", - "\n", + "\n", "_sampled_articles_inputs->sampled_articles\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", "input\n", - "\n", - "input\n", + "\n", + "input\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 10, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -244,7 +244,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 3, "id": "9361f411-cb85-47ff-be7d-fc35c4019685", "metadata": {}, "outputs": [ @@ -257,189 +257,163 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", - "\n", - "\n", - "\n", - "medium_articles\n", - "\n", - "medium_articles\n", - "Dataset\n", + "\n", + "Legend\n", "\n", "\n", - "\n", + "\n", "sampled_articles\n", - "\n", - "sampled_articles\n", - "Dataset\n", - "\n", - "\n", - "\n", - "medium_articles->sampled_articles\n", - "\n", - "\n", + "\n", + "sampled_articles\n", + "Dataset\n", "\n", - "\n", + "\n", "\n", - "retriever\n", - "\n", - "retriever\n", - "SentenceTransformer\n", + "tokenizer\n", + "\n", + "tokenizer\n", + "PreTrainedTokenizer\n", "\n", - "\n", + "\n", "\n", + "ner_pipeline\n", + "\n", + "ner_pipeline\n", + "Pipeline\n", + "\n", + "\n", + "\n", + "tokenizer->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", "medium_articles.select_data.dataset\n", - "\n", - "medium_articles.select_data.dataset\n", - "Dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles\n", + "\n", + "medium_articles\n", + "Dataset\n", "\n", "\n", - "\n", + "\n", "medium_articles.select_data.dataset->medium_articles\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "NER_model_id\n", - "\n", - "NER_model_id\n", - "str\n", - "\n", - "\n", - "\n", - "model\n", - "\n", - "model\n", - "PreTrainedModel\n", + "medium_articles.load_data.dataset\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", "\n", - "\n", + "\n", "\n", - "NER_model_id->model\n", - "\n", - "\n", + "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", + "\n", + "\n", "\n", - "\n", - "\n", - "tokenizer\n", - "\n", - "tokenizer\n", - "PreTrainedTokenizer\n", + "\n", + "\n", + "NER_model_id\n", + "\n", + "NER_model_id\n", + "str\n", "\n", "\n", - "\n", + "\n", "NER_model_id->tokenizer\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "medium_articles.load_data.dataset\n", - "\n", - "medium_articles.load_data.dataset\n", - "Tuple\n", + "\n", + "\n", + "model\n", + "\n", + "model\n", + "PreTrainedModel\n", "\n", - "\n", + "\n", + "\n", + "NER_model_id->model\n", + "\n", + "\n", + "\n", + "\n", "\n", - "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", - "\n", - "\n", + "model->ner_pipeline\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "device\n", - "\n", - "device\n", - "str\n", - "\n", - "\n", - "\n", - "device->retriever\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "ner_pipeline\n", - "\n", - "ner_pipeline\n", - "Pipeline\n", + "\n", + "device\n", + "str\n", "\n", "\n", - "\n", + "\n", "device->ner_pipeline\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "model->ner_pipeline\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "tokenizer->ner_pipeline\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "_retriever_inputs\n", - "\n", - "retriever_model_id\n", - "str\n", + "\n", + "\n", "\n", - "\n", - "\n", - "_retriever_inputs->retriever\n", - "\n", - "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "_sampled_articles_inputs\n", - "\n", - "max_text_length\n", - "int\n", - "sample_size\n", - "int\n", - "random_state\n", - "int\n", + "\n", + "sample_size\n", + "int\n", + "max_text_length\n", + "int\n", + "random_state\n", + "int\n", "\n", "\n", - "\n", + "\n", "_sampled_articles_inputs->sampled_articles\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "input\n", - "\n", - "input\n", + "\n", + "input\n", "\n", "\n", - "\n", + "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 20, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -493,7 +467,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 4, "id": "616b28da-2fb9-435d-bb58-98af161f8d89", "metadata": {}, "outputs": [ @@ -523,7 +497,7 @@ " 'end': 24}]]" ] }, - "execution_count": 14, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -545,7 +519,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 4, "id": "934101e9-59b9-4002-8ab0-5aa289ca9f0a", "metadata": {}, "outputs": [ @@ -558,189 +532,189 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", - "\n", - "\n", - "\n", - "medium_articles\n", - "\n", - "medium_articles\n", - "Dataset\n", + "\n", + "Legend\n", "\n", "\n", - "\n", + "\n", "sampled_articles\n", - "\n", - "sampled_articles\n", - "Dataset\n", + "\n", + "sampled_articles\n", + "Dataset\n", "\n", - "\n", + "\n", + "\n", + "tokenizer\n", + "\n", + "tokenizer\n", + "PreTrainedTokenizer\n", + "\n", + "\n", + "\n", + "ner_pipeline\n", + "\n", + "ner_pipeline\n", + "Pipeline\n", + "\n", + "\n", "\n", - "medium_articles->sampled_articles\n", - "\n", - "\n", + "tokenizer->ner_pipeline\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "retriever\n", - "\n", - "retriever\n", - "SentenceTransformer\n", + "\n", + "retriever\n", + "SentenceTransformer\n", "\n", "\n", - "\n", + "\n", "medium_articles.select_data.dataset\n", - "\n", - "medium_articles.select_data.dataset\n", - "Dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles\n", + "\n", + "medium_articles\n", + "Dataset\n", "\n", "\n", - "\n", + "\n", "medium_articles.select_data.dataset->medium_articles\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", + "\n", + "\n", + "\n", + "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "NER_model_id\n", - "\n", - "NER_model_id\n", - "str\n", + "\n", + "NER_model_id\n", + "str\n", + "\n", + "\n", + "\n", + "NER_model_id->tokenizer\n", + "\n", + "\n", "\n", "\n", "\n", "model\n", - "\n", - "model\n", - "PreTrainedModel\n", + "\n", + "model\n", + "PreTrainedModel\n", "\n", "\n", - "\n", + "\n", "NER_model_id->model\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "tokenizer\n", - "\n", - "tokenizer\n", - "PreTrainedTokenizer\n", - "\n", - "\n", - "\n", - "NER_model_id->tokenizer\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "medium_articles.load_data.dataset\n", - "\n", - "medium_articles.load_data.dataset\n", - "Tuple\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", - "\n", - "\n", + "model->ner_pipeline\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "device\n", - "\n", - "device\n", - "str\n", + "\n", + "device\n", + "str\n", + "\n", + "\n", + "\n", + "device->ner_pipeline\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "device->retriever\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "ner_pipeline\n", - "\n", - "ner_pipeline\n", - "Pipeline\n", + "\n", + "\n", "\n", - "\n", - "\n", - "device->ner_pipeline\n", - "\n", - "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", "\n", - "\n", - "\n", - "model->ner_pipeline\n", - "\n", - "\n", + "\n", + "\n", + "_sampled_articles_inputs\n", + "\n", + "sample_size\n", + "int\n", + "max_text_length\n", + "int\n", + "random_state\n", + "int\n", "\n", - "\n", - "\n", - "tokenizer->ner_pipeline\n", - "\n", - "\n", + "\n", + "\n", + "_sampled_articles_inputs->sampled_articles\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "_retriever_inputs\n", - "\n", - "retriever_model_id\n", - "str\n", + "\n", + "retriever_model_id\n", + "str\n", "\n", "\n", - "\n", + "\n", "_retriever_inputs->retriever\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "_sampled_articles_inputs\n", - "\n", - "max_text_length\n", - "int\n", - "sample_size\n", - "int\n", - "random_state\n", - "int\n", - "\n", - "\n", - "\n", - "_sampled_articles_inputs->sampled_articles\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", "input\n", - "\n", - "input\n", + "\n", + "input\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 15, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -764,281 +738,36 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 6, "id": "14f7a7ef-4009-4458-9075-a91bcdcce73c", "metadata": { "scrolled": true }, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/stefankrawczyk/.pyenv/versions/3.10.4/envs/ner-example-py310/lib/python3.10/site-packages/huggingface_hub/file_download.py:1132: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n", + " warnings.warn(\n" + ] + }, { "data": { "text/plain": [ - "array([[ 3.60962786e-02, -3.31540257e-02, 8.81905016e-03,\n", - " 4.30133902e-02, 2.57134414e-03, -9.96292103e-03,\n", - " 2.37981323e-02, 3.95706780e-02, -3.06305103e-02,\n", - " -7.25629227e-03, 2.78342664e-02, -6.24335743e-03,\n", - " 1.61879994e-02, 5.16220666e-02, -2.63889283e-02,\n", - " -4.60373871e-02, -1.31765818e-02, 1.28921298e-02,\n", - " 3.41649004e-03, 5.24031892e-02, -1.23013509e-02,\n", - " 5.96170649e-02, -2.11504893e-03, -2.70127449e-02,\n", - " -2.94139981e-03, -9.35570523e-03, -2.01370083e-02,\n", - " 1.44259995e-02, -1.74326338e-02, -4.83241268e-02,\n", - " -2.26764623e-02, 2.10481621e-02, -7.59536773e-03,\n", - " -4.23350669e-02, 8.56910187e-09, 4.39378107e-03,\n", - " 1.55015690e-02, -9.36352648e-03, -2.30528452e-02,\n", - " 4.53829952e-02, 2.28481553e-02, 7.45767634e-03,\n", - " -4.02662642e-02, 8.16163141e-03, 7.33977463e-03,\n", - " 6.47669807e-02, 3.71067561e-02, 5.97369075e-02,\n", - " -2.98210960e-02, 6.30364046e-02, 5.54212602e-03,\n", - " -8.19289126e-03, -3.08336155e-03, -1.38418591e-02,\n", - " 1.55548424e-01, 1.60365384e-02, -8.71572923e-03,\n", - " -3.56814340e-02, 2.89250687e-02, 8.45119804e-02,\n", - " 2.88498159e-02, 3.69493775e-02, 1.47784995e-02,\n", - " -3.88618093e-03, -9.33126081e-03, 4.83849421e-02,\n", - " 1.69343092e-02, -2.30700262e-02, 2.27935631e-02,\n", - " 2.94216126e-02, 7.52457082e-02, -1.10501163e-02,\n", - " 6.53248979e-03, 5.70445955e-02, -3.45314890e-02,\n", - " -5.34237772e-02, -7.21587427e-03, 8.86644349e-02,\n", - " -6.72957674e-02, -4.45436500e-03, -4.05082814e-02,\n", - " 1.90933086e-02, 1.49944937e-02, 2.62058433e-03,\n", - " -6.92337230e-02, 2.91210935e-02, -3.49241719e-02,\n", - " 7.13418995e-05, 3.01802810e-03, 2.35063839e-03,\n", - " -5.61731309e-02, -9.11250710e-03, 1.91500615e-02,\n", - " -1.59010589e-02, 5.99949211e-02, -3.04716192e-02,\n", - " -5.89178279e-02, -7.91868716e-02, 4.32029627e-02,\n", - " -2.97455639e-02, -3.12141869e-02, -2.72418000e-02,\n", - " -2.25155856e-02, 3.63038741e-02, -2.89564170e-02,\n", - " -1.44697009e-02, 2.85882056e-02, 3.80617008e-02,\n", - " -4.70239818e-02, 4.80013639e-02, 1.14646424e-02,\n", - " -2.21854951e-02, -6.22103997e-02, -1.82279553e-02,\n", - " -2.92093325e-02, 3.50595266e-02, 3.67511436e-02,\n", - " -5.65246725e-03, 4.00411002e-02, 4.38005757e-03,\n", - " -3.30929421e-02, 4.05450398e-03, 2.58010644e-02,\n", - " 2.59981975e-02, -5.12769772e-03, -1.87090952e-02,\n", - " 1.96436774e-02, -2.08113249e-03, -2.07201689e-02,\n", - " 3.95227000e-02, 2.37268265e-02, 6.40152171e-02,\n", - " 8.18362907e-02, 8.38510040e-03, -1.20331924e-02,\n", - " 5.18913865e-02, -3.95324975e-02, -1.27038434e-02,\n", - " 1.71059091e-02, 2.46613976e-02, -3.63002680e-02,\n", - " -2.93706506e-02, 1.01808575e-03, -4.56925109e-03,\n", - " -7.12503865e-03, 2.16140412e-02, 5.93632041e-03,\n", - " -2.52346676e-02, 2.48365477e-02, 3.33161354e-02,\n", - " -5.50414585e-02, 2.04023216e-02, -3.89450192e-02,\n", - " -5.56852855e-02, 1.64508075e-02, -1.77108273e-02,\n", - " 7.37611130e-02, -5.47070689e-02, 3.79128493e-02,\n", - " -4.76424489e-03, 1.55324098e-02, 3.15646902e-02,\n", - " -5.03879003e-02, 4.62604966e-03, 3.08783911e-02,\n", - " 5.00634573e-02, -6.51622564e-02, 3.44836083e-03,\n", - " -3.80689185e-03, -2.13992577e-02, 5.93210198e-03,\n", - " 9.66137741e-03, 2.43846960e-02, 7.57200345e-02,\n", - " -1.29039232e-02, 8.08231980e-02, -2.19743047e-03,\n", - " 1.85493231e-02, -7.74908438e-02, 5.33490926e-02,\n", - " 9.79409739e-03, -5.43809775e-03, -2.69306395e-02,\n", - " 1.41466148e-02, -3.78070064e-02, 4.91003208e-02,\n", - " 5.70112690e-02, 2.79056765e-02, 2.61175148e-02,\n", - " -2.50035003e-02, -5.67776710e-03, -2.96256109e-03,\n", - " 3.88234388e-04, -3.26094292e-02, 1.84452403e-02,\n", - " 1.64732281e-02, -5.52664399e-02, -2.42808014e-02,\n", - " 3.05803847e-02, 4.70533036e-03, 4.27689292e-02,\n", - " 2.96906605e-02, 4.54030745e-02, 4.79164794e-02,\n", - " 2.46673524e-02, 1.51533289e-02, 7.23499209e-02,\n", - " -4.98598032e-02, 5.23909926e-02, 3.02121956e-02,\n", - " 8.87216593e-04, 4.75466773e-02, -1.72570907e-02,\n", - " 1.12080984e-02, -1.26648881e-02, -1.69910006e-02,\n", - " 4.16054716e-03, -1.89250205e-02, -5.16009890e-02,\n", - " 3.31343338e-02, 2.45700008e-03, 4.01594676e-02,\n", - " -5.69379590e-02, 5.54539561e-02, 4.44991030e-02,\n", - " -2.80060563e-02, -1.36538371e-02, -1.00473007e-02,\n", - " -2.33908128e-02, 4.15213034e-03, -4.19142731e-02,\n", - " 5.46232471e-03, 3.80426273e-02, 1.97659284e-02,\n", - " -2.66039837e-02, 3.71009368e-03, -2.21916772e-02,\n", - " -8.75609890e-02, -2.90109701e-02, -6.08509555e-02,\n", - " 3.57717238e-02, 5.24172047e-03, -1.02223014e-03,\n", - " 2.02417001e-02, -8.86212941e-03, -4.80997078e-02,\n", - " -1.33355176e-02, 5.32028452e-03, 6.88390457e-04,\n", - " 9.89494286e-03, -3.98303708e-03, -2.71222810e-03,\n", - " 2.49915905e-02, 1.68020558e-02, 2.85249539e-02,\n", - " -1.22867841e-02, -1.26059232e-02, -1.02110012e-02,\n", - " 6.40016049e-02, 8.37008469e-03, 4.59938608e-02,\n", - " -3.00367512e-02, -2.70720273e-02, 5.37466840e-04,\n", - " 4.57689986e-02, 1.11166155e-02, -4.76786457e-02,\n", - " -4.55223732e-02, -4.03027646e-02, -1.53549947e-02,\n", - " -2.45466754e-02, 2.08480880e-02, 7.20439330e-02,\n", - " 4.96347398e-02, 2.49973685e-02, -3.96961672e-03,\n", - " -5.61507680e-02, 1.07510388e-03, -3.20086367e-02,\n", - " 1.80948917e-02, 1.27756372e-02, -1.42341442e-02,\n", - " -6.12534489e-03, -9.90991294e-03, -9.46271978e-03,\n", - " 3.77464481e-02, 1.68987773e-02, -7.38439709e-02,\n", - " -2.62885708e-02, 5.70751764e-02, 8.65498371e-03,\n", - " -2.94710528e-02, -7.22954609e-03, -2.57841013e-02,\n", - " 2.56506708e-02, -3.63825373e-02, 5.47279343e-02,\n", - " 3.73191424e-02, 7.21509680e-02, 7.65907988e-02,\n", - " 2.12801695e-02, -1.47611247e-02, -7.92645104e-03,\n", - " 5.18372795e-03, 4.15485464e-02, 1.87958721e-02,\n", - " -8.61883257e-03, -8.73448476e-02, 3.89653482e-02,\n", - " 1.36772767e-02, 8.71754251e-03, -3.39957047e-03,\n", - " -3.89559716e-02, -2.59164963e-02, -2.75201444e-02,\n", - " 9.06339940e-03, -2.74889991e-02, -1.20448750e-02,\n", - " -4.28144746e-02, -6.95132092e-02, -3.27884685e-03,\n", - " -1.39576485e-02, 1.10945562e-02, 1.90854818e-02,\n", - " -1.12206517e-02, -4.97540049e-02, -6.25470951e-02,\n", - " 9.18536633e-03, 5.13912737e-02, -3.07003036e-02,\n", - " 3.13394368e-02, 2.89901160e-02, -6.14287071e-02,\n", - " 4.82854582e-02, 3.91877145e-02, 5.23225404e-03,\n", - " -2.32016873e-02, 2.23461054e-02, 4.73749638e-02,\n", - " 2.00397186e-02, -4.97329831e-02, -3.63437682e-02,\n", - " 4.96318787e-02, -8.55642706e-02, -5.36590740e-02,\n", - " -6.84164762e-02, 2.26597078e-02, 1.21897319e-02,\n", - " -5.97152300e-02, -6.86289445e-02, 2.02023555e-02,\n", - " 3.99615839e-02, -3.89971510e-02, -2.59903707e-02,\n", - " -4.20581661e-02, -1.67649258e-02, 3.28528881e-03,\n", - " -1.54073536e-02, -1.28716780e-02, -3.22402827e-02,\n", - " 3.11157983e-02, 1.36079248e-02, -5.29781915e-02,\n", - " -2.45646909e-02, 2.02353336e-02, -1.05925892e-02,\n", - " -5.55798877e-03, 9.90392826e-03, 3.43095232e-03,\n", - " 1.02578243e-02, -4.14645672e-02, -7.71018397e-03,\n", - " -4.29285690e-03, 3.13787013e-02, 8.97466019e-03,\n", - " 3.63822468e-02, 6.27918029e-03, -8.00463557e-03,\n", - " 6.55080192e-03, -7.97851104e-03, 3.00842541e-04,\n", - " -4.07078750e-02, -8.72434396e-03, 1.35801286e-02,\n", - " -1.34013873e-02, 8.53130035e-03, -3.59485000e-02,\n", - " -3.83378863e-02, 7.85570443e-02, -7.81345740e-03,\n", - " -2.92420667e-03, -4.23185863e-02, -1.91911459e-02,\n", - " -4.74103875e-02, -9.45227873e-03, 4.83201398e-03,\n", - " -3.51743437e-02, 2.67341584e-02, -1.51325874e-02,\n", - " 2.68554810e-04, 3.15276929e-03, 5.03170155e-02,\n", - " 1.92302205e-02, 3.92147750e-02, 1.45246563e-02,\n", - " 5.26193483e-03, -3.38331163e-02, 6.62993581e-04,\n", - " -4.11011912e-02, 3.33439768e-03, 4.10068408e-02,\n", - " -2.29125712e-02, 9.55220088e-02, 3.63799930e-02,\n", - " 4.99948226e-02, 1.06081786e-03, 2.43593454e-02,\n", - " -3.74247809e-03, 2.16635019e-02, -4.70961304e-03,\n", - " -2.22381279e-02, 1.30076380e-02, 3.92022841e-02,\n", - " -4.55784472e-03, 1.15262605e-02, 8.73760693e-03,\n", - " 2.28593033e-03, -6.67265104e-03, 8.77784006e-03,\n", - " -2.90497020e-02, -3.49355116e-02, 2.00866582e-03,\n", - " -8.68706331e-02, 3.63544896e-02, -3.93404067e-02,\n", - " -1.09610453e-01, 1.73198956e-03, -6.42923173e-03,\n", - " -1.36668487e-02, -5.94285950e-02, 3.10135763e-02,\n", - " -2.96281464e-03, 7.39220381e-02, -5.66292228e-03,\n", - " -1.40256761e-02, 7.05927163e-02, -3.21498588e-02,\n", - " 1.28106093e-02, -4.65438589e-02, -3.63234505e-02,\n", - " -6.28645672e-03, -8.84195603e-03, -4.16300111e-02,\n", - " -2.92248707e-02, 2.20061969e-02, 1.07009709e-02,\n", - " -8.38732161e-03, -4.97266352e-02, -6.19839504e-02,\n", - " -5.66809392e-03, -1.37819005e-02, 1.10032108e-08,\n", - " -5.50880842e-02, 3.94730568e-02, -1.62433982e-02,\n", - " 4.45250012e-02, 4.34459820e-02, -3.24180685e-02,\n", - " 1.86627898e-02, 4.14110487e-04, -8.44709575e-03,\n", - " 7.95873441e-03, -2.65822858e-02, 1.14863897e-02,\n", - " -2.68137287e-02, -2.03993917e-02, -4.44443598e-02,\n", - " -7.90846273e-02, 3.23186889e-02, 1.95921995e-02,\n", - " -1.74563341e-02, 3.09612439e-03, -2.13709958e-02,\n", - " -5.17976247e-02, 1.02625117e-02, -1.46095185e-02,\n", - " -7.85705866e-04, -2.90651433e-02, -2.32787617e-02,\n", - " 4.79632542e-02, -2.69608293e-02, -4.38591242e-02,\n", - " -3.48058455e-02, 1.27649689e-02, 1.65850390e-02,\n", - " 4.80354093e-02, 2.48051025e-02, -3.45652290e-02,\n", - " 2.07881834e-02, 2.76347026e-02, 3.49310189e-02,\n", - " -1.36970505e-02, -1.46573661e-02, -2.92295665e-02,\n", - " -9.11438763e-02, 1.83510091e-02, 1.67318657e-02,\n", - " 4.89241304e-03, 6.91548362e-03, -1.55162951e-02,\n", - " 1.92494318e-02, 4.27412428e-03, -6.45834655e-02,\n", - " -5.60014099e-02, -8.51061475e-03, -6.69982433e-02,\n", - " 2.29862519e-02, -2.99270693e-02, 6.02340437e-02,\n", - " -1.12921549e-02, 2.84359679e-02, -4.08170968e-02,\n", - " 5.21569559e-03, -1.86661053e-02, -4.33015861e-02,\n", - " -1.27073908e-02, 8.43595248e-03, 2.64639854e-02,\n", - " -5.36046131e-03, 2.97620259e-02, -4.93885076e-04,\n", - " 1.29855145e-02, 2.29265802e-02, -1.37690296e-02,\n", - " -5.01243770e-02, 1.75731406e-02, 2.49196496e-02,\n", - " 5.08863367e-02, -2.86997180e-03, -4.78268117e-02,\n", - " 1.01278253e-01, 3.66900414e-02, 6.19485863e-02,\n", - " 3.21024540e-03, 7.56731778e-02, -2.11994573e-02,\n", - " -4.42590937e-02, -2.64976192e-02, -2.66530327e-02,\n", - " -2.09754724e-02, -6.58231229e-03, 3.46418619e-02,\n", - " -4.56248075e-02, 9.63507593e-03, -3.32122408e-02,\n", - " -2.24238628e-10, -5.45747019e-03, -6.03148006e-02,\n", - " -1.29531585e-02, 4.66388501e-02, -1.83035545e-02,\n", - " 8.13329406e-03, -2.12679580e-02, 3.94083606e-03,\n", - " 3.26677673e-02, 1.50923189e-02, -1.45858675e-02,\n", - " 2.18188744e-02, 1.21959923e-02, 1.52391512e-02,\n", - " -1.38983773e-02, -3.14303413e-02, 2.71212198e-02,\n", - " -1.26387030e-02, 4.07406501e-03, -2.38309093e-02,\n", - " 5.65349907e-02, 1.62084051e-03, 4.00588177e-02,\n", - " -6.29829988e-02, 1.13373147e-02, 2.39813961e-02,\n", - " -2.09959485e-02, -8.17801654e-02, 2.19626781e-02,\n", - " -1.11062312e-02, -2.92382967e-02, -6.56387489e-03,\n", - " -8.35734326e-03, 1.15756495e-02, -1.18839787e-02,\n", - " 9.94963497e-02, -7.10078515e-03, -3.85195948e-02,\n", - " -3.75379287e-02, 3.29218954e-02, -6.22654036e-02,\n", - " 8.00584239e-05, 3.03280260e-02, -3.60623526e-04,\n", - " 5.26082106e-02, -4.04131003e-02, 2.93430295e-02,\n", - " -1.63055733e-02, -1.33592132e-02, -2.18235105e-02,\n", - " 1.05328085e-02, 2.61274725e-02, -1.83648933e-02,\n", - " 1.02028791e-02, -3.07935383e-02, 1.24935098e-02,\n", - " 3.44416276e-02, 1.67941470e-02, -6.70520738e-02,\n", - " -1.46601954e-02, -3.90077084e-02, 3.68831418e-02,\n", - " -3.51772308e-02, -2.43946034e-02, 1.51652172e-02,\n", - " 5.97533248e-02, -4.58799116e-02, 4.62583499e-03,\n", - " -4.59687486e-02, -1.49805769e-02, 3.87360342e-02,\n", - " 3.69152017e-02, -9.81731620e-03, 6.09802119e-02,\n", - " -1.89866126e-02, -4.04221751e-02, -1.44595830e-02,\n", - " 1.90329887e-02, -7.29578510e-02, -1.61912106e-02,\n", - " 4.42078449e-02, 3.00968252e-02, -5.99590354e-02,\n", - " -1.73724014e-02, -4.17371513e-04, -5.59736714e-02,\n", - " -2.08146255e-02, 1.44553808e-02, -6.88614976e-03,\n", - " 3.04765478e-02, -2.06166115e-02, 1.63799357e-02,\n", - " 2.36149486e-02, 7.86401331e-03, -6.60859197e-02,\n", - " 5.93789592e-02, -2.23904233e-02, 1.63225979e-02,\n", - " -3.92370820e-02, -5.28906733e-02, -7.95797929e-02,\n", - " 5.23762591e-03, 1.59868654e-02, 5.77198192e-02,\n", - " 6.40571713e-02, 1.73853505e-02, 5.13109937e-02,\n", - " -1.59433540e-02, -8.83233324e-02, -1.93907954e-02,\n", - " -1.34620685e-02, -4.68650274e-03, 5.16196974e-02,\n", - " 2.87868734e-02, -1.12800738e-02, -2.01114248e-02,\n", - " 3.91260386e-02, 5.36062941e-02, -2.65886653e-02,\n", - " -2.63306983e-02, 6.47996319e-03, -2.41869520e-02,\n", - " -1.47397565e-02, 6.82561146e-03, 2.61856569e-03,\n", - " 3.40498760e-02, 3.45875360e-02, 2.95602926e-03,\n", - " 7.91043043e-02, -7.97181204e-03, 5.71655459e-04,\n", - " -9.14053235e-05, 4.37791101e-08, 2.43839417e-02,\n", - " 2.40845401e-02, 7.36697763e-02, 2.03841217e-02,\n", - " -4.34769057e-02, 1.09440416e-01, 3.75518426e-02,\n", - " 3.39452252e-02, -5.80685697e-02, 4.10885131e-03,\n", - " 5.68004027e-02, -2.88534556e-02, 1.13896681e-02,\n", - " 4.62091295e-03, -6.56217337e-02, -7.07318075e-04,\n", - " -3.28697041e-02, -6.30226508e-02, -4.00098562e-02,\n", - " -5.63813262e-02, 8.35973397e-02, -2.56133545e-03,\n", - " 1.48774534e-02, -1.49532510e-02, 1.81542169e-02,\n", - " -2.15953421e-02, -2.42270343e-02, -5.58906458e-02,\n", - " 7.23159835e-02, 9.63630155e-03, -9.02656745e-03,\n", - " -7.60971010e-02, -4.79021706e-02, 6.48496114e-03,\n", - " 6.59549655e-03, -6.96884394e-02, 2.36046128e-02,\n", - " 4.66829985e-02, 1.34313516e-02, 1.05467699e-01,\n", - " -2.92742476e-02, -3.37985717e-02, -3.49831954e-02,\n", - " -8.66085198e-03, 5.21409437e-02, 5.00857122e-02,\n", - " -1.01787001e-02, 2.89740656e-02, -6.33968040e-02,\n", - " 9.56533942e-03, 4.16928949e-03, 6.22191243e-02,\n", - " 4.69440082e-03, -2.36494374e-02, -4.71315579e-03,\n", - " -3.99447493e-02, 1.59526989e-02, -2.06302442e-02,\n", - " 3.75677249e-03, 1.65763125e-02, -5.27706966e-02,\n", - " 4.87393290e-02, -5.44341803e-02, 8.20284113e-02,\n", - " 3.47531103e-02, -4.45972905e-02, 2.54839137e-02,\n", - " -3.99673993e-33, 1.05813500e-02, -7.13590011e-02,\n", - " -2.64143776e-02, 2.19693445e-02, 4.34513204e-03,\n", - " -1.09376190e-02, 8.32656324e-02, -1.43483225e-02,\n", - " 3.95661704e-02, -4.32383418e-02, -3.50103481e-03]], dtype=float32)" + "array([ 0.03609628, -0.03315403, 0.00881905, 0.04301339, 0.00257134,\n", + " -0.00996292, 0.02379813, 0.03957068, -0.03063051, -0.00725629],\n", + " dtype=float32)" ] }, - "execution_count": 18, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# what the embedding model produces\n", - "retriever(\"cpu\").encode([\"this is some text\"])" + "# what the embedding model produces -- just show first 10 numbers\n", + "retriever(\"cpu\").encode([\"this is some text\"])[0][0:10]" ] }, { @@ -1056,7 +785,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 5, "id": "e3a57de3-3fc4-4693-8d73-cedbb3ba8060", "metadata": {}, "outputs": [ @@ -1069,263 +798,263 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", - "\n", + "\n", "\n", - "final_dataset\n", - "\n", - "final_dataset\n", - "Dataset\n", + "columns_of_interest\n", + "\n", + "columns_of_interest\n", + "list\n", "\n", "\n", - "\n", + "\n", "load_into_lancedb\n", - "\n", - "\n", - "load_into_lancedb\n", - "HuggingFaceDSLanceDBSaver\n", + "\n", + "\n", + "load_into_lancedb\n", + "HuggingFaceDSLanceDBSaver\n", "\n", - "\n", - "\n", - "final_dataset->load_into_lancedb\n", - "\n", - "\n", + "\n", + "\n", + "columns_of_interest->load_into_lancedb\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "device\n", - "\n", - "device\n", - "str\n", - "\n", - "\n", - "\n", - "retriever\n", - "\n", - "retriever\n", - "SentenceTransformer\n", + "final_dataset\n", + "\n", + "final_dataset\n", + "Dataset\n", "\n", - "\n", + "\n", "\n", - "device->retriever\n", - "\n", - "\n", + "final_dataset->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "tokenizer\n", + "\n", + "tokenizer\n", + "PreTrainedTokenizer\n", "\n", "\n", - "\n", + "\n", "ner_pipeline\n", - "\n", - "ner_pipeline\n", - "Pipeline\n", + "\n", + "ner_pipeline\n", + "Pipeline\n", "\n", - "\n", - "\n", - "device->ner_pipeline\n", - "\n", - "\n", + "\n", + "\n", + "tokenizer->ner_pipeline\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "model\n", - "\n", - "model\n", - "PreTrainedModel\n", + "\n", + "model\n", + "PreTrainedModel\n", "\n", "\n", - "\n", + "\n", "model->ner_pipeline\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", + "sampled_articles\n", + "\n", + "sampled_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "sampled_articles->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", "medium_articles.load_data.dataset\n", - "\n", - "medium_articles.load_data.dataset\n", - "Tuple\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", "\n", "\n", - "\n", + "\n", "medium_articles.select_data.dataset\n", - "\n", - "medium_articles.select_data.dataset\n", - "Dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", "\n", "\n", - "\n", + "\n", "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "tokenizer\n", - "\n", - "tokenizer\n", - "PreTrainedTokenizer\n", - "\n", - "\n", - "\n", - "tokenizer->ner_pipeline\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "medium_articles\n", - "\n", - "medium_articles\n", - "Dataset\n", + "\n", + "medium_articles\n", + "Dataset\n", "\n", - "\n", - "\n", - "sampled_articles\n", - "\n", - "sampled_articles\n", - "Dataset\n", + "\n", + "\n", + "medium_articles.select_data.dataset->medium_articles\n", + "\n", + "\n", "\n", - "\n", - "\n", - "medium_articles->sampled_articles\n", - "\n", - "\n", + "\n", + "\n", + "ner_pipeline->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "retriever\n", + "\n", + "retriever\n", + "SentenceTransformer\n", "\n", "\n", "\n", "retriever->final_dataset\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "columns_of_interest\n", - "\n", - "columns_of_interest\n", - "list\n", + "\n", + "\n", + "NER_model_id\n", + "\n", + "NER_model_id\n", + "str\n", "\n", - "\n", + "\n", + "\n", + "NER_model_id->tokenizer\n", + "\n", + "\n", + "\n", + "\n", "\n", - "columns_of_interest->load_into_lancedb\n", - "\n", - "\n", + "NER_model_id->model\n", + "\n", + "\n", "\n", - "\n", - "\n", - "medium_articles.select_data.dataset->medium_articles\n", - "\n", - "\n", + "\n", + "\n", + "device\n", + "\n", + "device\n", + "str\n", "\n", - "\n", - "\n", - "sampled_articles->final_dataset\n", - "\n", - "\n", + "\n", + "\n", + "device->ner_pipeline\n", + "\n", + "\n", "\n", - "\n", - "\n", - "NER_model_id\n", - "\n", - "NER_model_id\n", - "str\n", + "\n", + "\n", + "device->retriever\n", + "\n", + "\n", "\n", - "\n", - "\n", - "NER_model_id->model\n", - "\n", - "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", "\n", - "\n", - "\n", - "NER_model_id->tokenizer\n", - "\n", - "\n", + "\n", + "\n", + "_sampled_articles_inputs\n", + "\n", + "sample_size\n", + "int\n", + "max_text_length\n", + "int\n", + "random_state\n", + "int\n", "\n", - "\n", - "\n", - "ner_pipeline->final_dataset\n", - "\n", - "\n", + "\n", + "\n", + "_sampled_articles_inputs->sampled_articles\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "_load_into_lancedb_inputs\n", - "\n", - "table_name\n", - "str\n", - "db_client\n", - "DBConnection\n", + "\n", + "table_name\n", + "str\n", + "db_client\n", + "DBConnection\n", "\n", "\n", - "\n", + "\n", "_load_into_lancedb_inputs->load_into_lancedb\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "_retriever_inputs\n", - "\n", - "retriever_model_id\n", - "str\n", + "\n", + "retriever_model_id\n", + "str\n", "\n", "\n", - "\n", + "\n", "_retriever_inputs->retriever\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "_sampled_articles_inputs\n", - "\n", - "max_text_length\n", - "int\n", - "sample_size\n", - "int\n", - "random_state\n", - "int\n", - "\n", - "\n", - "\n", - "_sampled_articles_inputs->sampled_articles\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", "input\n", - "\n", - "input\n", + "\n", + "input\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n", "materializer\n", - "\n", - "\n", - "materializer\n", + "\n", + "\n", + "materializer\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 22, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -1409,7 +1138,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 8, "id": "ea373589-dfb2-408a-91f2-97e0f0aa029d", "metadata": {}, "outputs": [ @@ -1422,263 +1151,263 @@ "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", - "\n", + "\n", "\n", - "final_dataset\n", - "\n", - "final_dataset\n", - "Dataset\n", + "medium_articles.load_data.dataset\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", "\n", - "\n", - "\n", - "load_into_lancedb\n", - "\n", - "\n", - "load_into_lancedb\n", - "HuggingFaceDSLanceDBSaver\n", + "\n", + "\n", + "medium_articles.select_data.dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", "\n", - "\n", - "\n", - "final_dataset->load_into_lancedb\n", - "\n", - "\n", + "\n", + "\n", + "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", + "\n", + "\n", "\n", - "\n", - "\n", - "device\n", - "\n", - "device\n", - "str\n", + "\n", + "\n", + "medium_articles\n", + "\n", + "medium_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "medium_articles.select_data.dataset->medium_articles\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "retriever\n", - "\n", - "retriever\n", - "SentenceTransformer\n", + "\n", + "retriever\n", + "SentenceTransformer\n", "\n", - "\n", - "\n", - "device->retriever\n", - "\n", - "\n", + "\n", + "\n", + "final_dataset\n", + "\n", + "final_dataset\n", + "Dataset\n", "\n", - "\n", - "\n", - "ner_pipeline\n", - "\n", - "ner_pipeline\n", - "Pipeline\n", + "\n", + "\n", + "retriever->final_dataset\n", + "\n", + "\n", "\n", - "\n", - "\n", - "device->ner_pipeline\n", - "\n", - "\n", + "\n", + "\n", + "sampled_articles\n", + "\n", + "sampled_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "sampled_articles->final_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "columns_of_interest\n", + "\n", + "columns_of_interest\n", + "list\n", + "\n", + "\n", + "\n", + "load_into_lancedb\n", + "\n", + "\n", + "load_into_lancedb\n", + "HuggingFaceDSLanceDBSaver\n", + "\n", + "\n", + "\n", + "columns_of_interest->load_into_lancedb\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "model\n", - "\n", - "model\n", - "PreTrainedModel\n", + "\n", + "model\n", + "PreTrainedModel\n", + "\n", + "\n", + "\n", + "ner_pipeline\n", + "\n", + "ner_pipeline\n", + "Pipeline\n", "\n", "\n", "\n", "model->ner_pipeline\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "medium_articles.load_data.dataset\n", - "\n", - "medium_articles.load_data.dataset\n", - "Tuple\n", + "\n", + "\n", "\n", - "\n", - "\n", - "medium_articles.select_data.dataset\n", - "\n", - "medium_articles.select_data.dataset\n", - "Dataset\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", "\n", - "\n", - "\n", - "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", - "\n", - "\n", + "\n", + "\n", + "final_dataset->load_into_lancedb\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "tokenizer\n", - "\n", - "tokenizer\n", - "PreTrainedTokenizer\n", + "\n", + "tokenizer\n", + "PreTrainedTokenizer\n", "\n", "\n", "\n", "tokenizer->ner_pipeline\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "medium_articles\n", - "\n", - "medium_articles\n", - "Dataset\n", - "\n", - "\n", - "\n", - "sampled_articles\n", - "\n", - "sampled_articles\n", - "Dataset\n", - "\n", - "\n", - "\n", - "medium_articles->sampled_articles\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "retriever->final_dataset\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "columns_of_interest\n", - "\n", - "columns_of_interest\n", - "list\n", - "\n", - "\n", - "\n", - "columns_of_interest->load_into_lancedb\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "medium_articles.select_data.dataset->medium_articles\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "sampled_articles->final_dataset\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "NER_model_id\n", - "\n", - "NER_model_id\n", - "str\n", + "\n", + "NER_model_id\n", + "str\n", "\n", "\n", - "\n", + "\n", "NER_model_id->model\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "NER_model_id->tokenizer\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "ner_pipeline->final_dataset\n", - "\n", - "\n", + "\n", + "\n", + "device\n", + "\n", + "device\n", + "str\n", "\n", - "\n", - "\n", - "_load_into_lancedb_inputs\n", - "\n", - "table_name\n", - "str\n", - "db_client\n", - "DBConnection\n", + "\n", + "\n", + "device->retriever\n", + "\n", + "\n", "\n", - "\n", - "\n", - "_load_into_lancedb_inputs->load_into_lancedb\n", - "\n", - "\n", + "\n", + "\n", + "device->ner_pipeline\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "ner_pipeline->final_dataset\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "_retriever_inputs\n", - "\n", - "retriever_model_id\n", - "str\n", + "\n", + "retriever_model_id\n", + "str\n", "\n", "\n", - "\n", + "\n", "_retriever_inputs->retriever\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "_sampled_articles_inputs\n", - "\n", - "max_text_length\n", - "int\n", - "sample_size\n", - "int\n", - "random_state\n", - "int\n", + "\n", + "sample_size\n", + "int\n", + "random_state\n", + "int\n", + "max_text_length\n", + "int\n", "\n", "\n", - "\n", + "\n", "_sampled_articles_inputs->sampled_articles\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_load_into_lancedb_inputs\n", + "\n", + "table_name\n", + "str\n", + "db_client\n", + "DBConnection\n", + "\n", + "\n", + "\n", + "_load_into_lancedb_inputs->load_into_lancedb\n", + "\n", + "\n", "\n", "\n", "\n", "input\n", - "\n", - "input\n", + "\n", + "input\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n", "materializer\n", - "\n", - "\n", - "materializer\n", + "\n", + "\n", + "materializer\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 24, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -1697,7 +1426,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 9, "id": "a3dc66e6-c728-4d57-a08d-80b2aee72f40", "metadata": {}, "outputs": [ @@ -1706,65 +1435,17 @@ "output_type": "stream", "text": [ "Executing node: columns_of_interest.\n", - "Finished debugging node: columns_of_interest in 258μs. Status: Success.\n", + "Finished debugging node: columns_of_interest in 609μs. Status: Success.\n", "Executing node: medium_articles.load_data.dataset.\n", - "Finished debugging node: medium_articles.load_data.dataset in 1.85s. Status: Success.\n", + "Finished debugging node: medium_articles.load_data.dataset in 1.87s. Status: Success.\n", "Executing node: medium_articles.select_data.dataset.\n", - "Finished debugging node: medium_articles.select_data.dataset in 19.1μs. Status: Success.\n", + "Finished debugging node: medium_articles.select_data.dataset in 17.9μs. Status: Success.\n", "Executing node: medium_articles.\n", - "Finished debugging node: medium_articles in 25μs. Status: Success.\n", - "Executing node: sampled_articles.\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c465a0f0f02b43059944489be1ee336c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Filter: 0%| | 0/192368 [00:00\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", - "\n", + "\n", "\n", - "lancedb_result\n", - "\n", - "lancedb_result\n", - "dict\n", + "columns_of_interest\n", + "\n", + "columns_of_interest\n", + "list\n", + "\n", + "\n", + "\n", + "load_into_lancedb\n", + "\n", + "\n", + "load_into_lancedb\n", + "HuggingFaceDSLanceDBSaver\n", + "\n", + "\n", + "\n", + "columns_of_interest->load_into_lancedb\n", + "\n", + "\n", "\n", "\n", "\n", "final_dataset\n", - "\n", - "final_dataset\n", - "Dataset\n", - "\n", - "\n", - "\n", - "load_into_lancedb\n", - "\n", - "\n", - "load_into_lancedb\n", - "HuggingFaceDSLanceDBSaver\n", + "\n", + "final_dataset\n", + "Dataset\n", "\n", "\n", - "\n", + "\n", "final_dataset->load_into_lancedb\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "device\n", - "\n", - "device\n", - "str\n", + "lancedb_table\n", + "\n", + "lancedb_table\n", + "Table\n", "\n", - "\n", - "\n", - "retriever\n", - "\n", - "retriever\n", - "SentenceTransformer\n", + "\n", + "\n", + "lancedb_result\n", + "\n", + "lancedb_result\n", + "dict\n", + "\n", + "\n", + "\n", + "lancedb_table->lancedb_result\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "named_entities\n", + "\n", + "named_entities\n", + "list\n", + "\n", + "\n", + "\n", + "named_entities->lancedb_result\n", + "\n", + "\n", "\n", - "\n", - "\n", - "device->retriever\n", - "\n", - "\n", + "\n", + "\n", + "tokenizer\n", + "\n", + "tokenizer\n", + "PreTrainedTokenizer\n", "\n", "\n", - "\n", + "\n", "ner_pipeline\n", - "\n", - "ner_pipeline\n", - "Pipeline\n", + "\n", + "ner_pipeline\n", + "Pipeline\n", "\n", - "\n", - "\n", - "device->ner_pipeline\n", - "\n", - "\n", + "\n", + "\n", + "tokenizer->ner_pipeline\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "model\n", - "\n", - "model\n", - "PreTrainedModel\n", + "\n", + "model\n", + "PreTrainedModel\n", "\n", "\n", - "\n", + "\n", "model->ner_pipeline\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "sampled_articles\n", + "\n", + "sampled_articles\n", + "Dataset\n", + "\n", + "\n", + "\n", + "sampled_articles->final_dataset\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "medium_articles.load_data.dataset\n", - "\n", - "medium_articles.load_data.dataset\n", - "Tuple\n", + "\n", + "medium_articles.load_data.dataset\n", + "Tuple\n", "\n", "\n", - "\n", + "\n", "medium_articles.select_data.dataset\n", - "\n", - "medium_articles.select_data.dataset\n", - "Dataset\n", + "\n", + "medium_articles.select_data.dataset\n", + "Dataset\n", "\n", "\n", - "\n", + "\n", "medium_articles.load_data.dataset->medium_articles.select_data.dataset\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "tokenizer\n", - "\n", - "tokenizer\n", - "PreTrainedTokenizer\n", - "\n", - "\n", - "\n", - "tokenizer->ner_pipeline\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "medium_articles\n", - "\n", - "medium_articles\n", - "Dataset\n", - "\n", - "\n", - "\n", - "sampled_articles\n", - "\n", - "sampled_articles\n", - "Dataset\n", - "\n", - "\n", - "\n", - "medium_articles->sampled_articles\n", - "\n", - "\n", + "\n", + "medium_articles\n", + "Dataset\n", "\n", - "\n", - "\n", - "lancedb_table\n", - "\n", - "lancedb_table\n", - "Table\n", + "\n", + "\n", + "medium_articles.select_data.dataset->medium_articles\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "lancedb_table->lancedb_result\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "columns_of_interest\n", - "\n", - "columns_of_interest\n", - "list\n", + "ner_pipeline->final_dataset\n", + "\n", + "\n", "\n", - "\n", - "\n", - "columns_of_interest->load_into_lancedb\n", - "\n", - "\n", + "\n", + "\n", + "ner_pipeline->named_entities\n", + "\n", + "\n", "\n", - "\n", - "\n", - "retriever->lancedb_result\n", - "\n", - "\n", + "\n", + "\n", + "retriever\n", + "\n", + "retriever\n", + "SentenceTransformer\n", "\n", "\n", - "\n", + "\n", "retriever->final_dataset\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "medium_articles.select_data.dataset->medium_articles\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "sampled_articles->final_dataset\n", - "\n", - "\n", + "\n", + "\n", + "retriever->lancedb_result\n", + "\n", + "\n", "\n", "\n", "\n", "NER_model_id\n", - "\n", - "NER_model_id\n", - "str\n", - "\n", - "\n", - "\n", - "NER_model_id->model\n", - "\n", - "\n", + "\n", + "NER_model_id\n", + "str\n", "\n", "\n", - "\n", + "\n", "NER_model_id->tokenizer\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "ner_pipeline->final_dataset\n", - "\n", - "\n", + "\n", + "\n", + "NER_model_id->model\n", + "\n", + "\n", "\n", - "\n", - "\n", - "named_entities\n", - "\n", - "named_entities\n", - "list\n", + "\n", + "\n", + "device\n", + "\n", + "device\n", + "str\n", "\n", - "\n", - "\n", - "ner_pipeline->named_entities\n", - "\n", - "\n", + "\n", + "\n", + "device->ner_pipeline\n", + "\n", + "\n", "\n", - "\n", - "\n", - "named_entities->lancedb_result\n", - "\n", - "\n", + "\n", + "\n", + "device->retriever\n", + "\n", + "\n", "\n", - "\n", + "\n", + "\n", + "medium_articles->sampled_articles\n", + "\n", + "\n", + "\n", + "\n", "\n", - "_lancedb_result_inputs\n", - "\n", - "prefilter\n", - "bool\n", - "top_k\n", - "int\n", - "query\n", - "str\n", + "_lancedb_table_inputs\n", + "\n", + "table_name\n", + "str\n", + "db_client\n", + "DBConnection\n", "\n", - "\n", + "\n", "\n", - "_lancedb_result_inputs->lancedb_result\n", - "\n", - "\n", + "_lancedb_table_inputs->lancedb_table\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "_load_into_lancedb_inputs\n", - "\n", - "table_name\n", - "str\n", - "db_client\n", - "DBConnection\n", + "_named_entities_inputs\n", + "\n", + "query\n", + "str\n", "\n", - "\n", - "\n", - "_load_into_lancedb_inputs->load_into_lancedb\n", - "\n", - "\n", + "\n", + "\n", + "_named_entities_inputs->named_entities\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "_lancedb_table_inputs\n", - "\n", - "table_name\n", - "str\n", - "db_client\n", - "DBConnection\n", - "\n", - "\n", - "\n", - "_lancedb_table_inputs->lancedb_table\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "_retriever_inputs\n", - "\n", - "retriever_model_id\n", - "str\n", + "_lancedb_result_inputs\n", + "\n", + "prefilter\n", + "bool\n", + "top_k\n", + "int\n", + "query\n", + "str\n", "\n", - "\n", - "\n", - "_retriever_inputs->retriever\n", - "\n", - "\n", + "\n", + "\n", + "_lancedb_result_inputs->lancedb_result\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "_sampled_articles_inputs\n", - "\n", - "max_text_length\n", - "int\n", - "sample_size\n", - "int\n", - "random_state\n", - "int\n", + "\n", + "sample_size\n", + "int\n", + "max_text_length\n", + "int\n", + "random_state\n", + "int\n", "\n", "\n", - "\n", + "\n", "_sampled_articles_inputs->sampled_articles\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", + "\n", + "_load_into_lancedb_inputs\n", + "\n", + "table_name\n", + "str\n", + "db_client\n", + "DBConnection\n", + "\n", + "\n", + "\n", + "_load_into_lancedb_inputs->load_into_lancedb\n", + "\n", + "\n", + "\n", + "\n", "\n", - "_named_entities_inputs\n", - "\n", - "query\n", - "str\n", + "_retriever_inputs\n", + "\n", + "retriever_model_id\n", + "str\n", "\n", - "\n", - "\n", - "_named_entities_inputs->named_entities\n", - "\n", - "\n", + "\n", + "\n", + "_retriever_inputs->retriever\n", + "\n", + "\n", "\n", "\n", "\n", "input\n", - "\n", - "input\n", + "\n", + "input\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n", "materializer\n", - "\n", - "\n", - "materializer\n", + "\n", + "\n", + "materializer\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 29, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -2339,7 +2000,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 11, "id": "dcfd624c-b0d5-474c-b7d3-3dc01a24b8fe", "metadata": {}, "outputs": [ @@ -2348,7 +2009,7 @@ "output_type": "stream", "text": [ "Executing node: NER_model_id.\n", - "Finished debugging node: NER_model_id in 298μs. Status: Success.\n", + "Finished debugging node: NER_model_id in 179μs. Status: Success.\n", "Executing node: model.\n" ] }, @@ -2367,21 +2028,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "Finished debugging node: model in 394ms. Status: Success.\n", + "Finished debugging node: model in 584ms. Status: Success.\n", "Executing node: tokenizer.\n", - "Finished debugging node: tokenizer in 131ms. Status: Success.\n", + "Finished debugging node: tokenizer in 152ms. Status: Success.\n", "Executing node: device.\n", - "Finished debugging node: device in 27.9μs. Status: Success.\n", + "Finished debugging node: device in 42μs. Status: Success.\n", "Executing node: ner_pipeline.\n", - "Finished debugging node: ner_pipeline in 2.23ms. Status: Success.\n", + "Finished debugging node: ner_pipeline in 2.02ms. Status: Success.\n", "Executing node: named_entities.\n", - "Finished debugging node: named_entities in 27.5ms. Status: Success.\n", + "Finished debugging node: named_entities in 52.3ms. Status: Success.\n", "Executing node: retriever.\n", - "Finished debugging node: retriever in 1.11s. Status: Success.\n", + "Finished debugging node: retriever in 1.75s. Status: Success.\n", "Executing node: lancedb_table.\n", - "Finished debugging node: lancedb_table in 240μs. Status: Success.\n", + "Finished debugging node: lancedb_table in 429μs. Status: Success.\n", "Executing node: lancedb_result.\n", - "Finished debugging node: lancedb_result in 110ms. Status: Success.\n" + "Finished debugging node: lancedb_result in 87.1ms. Status: Success.\n" ] }, { @@ -2518,7 +2179,7 @@ " '_distance': 1.6984761953353882}]}}" ] }, - "execution_count": 32, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -2540,7 +2201,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 12, "id": "f6d03340-1e21-4dfc-bfbf-6f06ee44c2a2", "metadata": {}, "outputs": [ @@ -2549,7 +2210,7 @@ "output_type": "stream", "text": [ "Executing node: NER_model_id.\n", - "Finished debugging node: NER_model_id in 184μs. Status: Success.\n", + "Finished debugging node: NER_model_id in 309μs. Status: Success.\n", "Executing node: model.\n" ] }, @@ -2566,21 +2227,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "Finished debugging node: model in 988ms. Status: Success.\n", + "Finished debugging node: model in 405ms. Status: Success.\n", "Executing node: tokenizer.\n", - "Finished debugging node: tokenizer in 137ms. Status: Success.\n", + "Finished debugging node: tokenizer in 133ms. Status: Success.\n", "Executing node: device.\n", - "Finished debugging node: device in 24.1μs. Status: Success.\n", + "Finished debugging node: device in 39.3μs. Status: Success.\n", "Executing node: ner_pipeline.\n", - "Finished debugging node: ner_pipeline in 1.93ms. Status: Success.\n", + "Finished debugging node: ner_pipeline in 1.9ms. Status: Success.\n", "Executing node: named_entities.\n", - "Finished debugging node: named_entities in 27.9ms. Status: Success.\n", + "Finished debugging node: named_entities in 26.4ms. Status: Success.\n", "Executing node: retriever.\n", - "Finished debugging node: retriever in 9.37s. Status: Success.\n", + "Finished debugging node: retriever in 1.28s. Status: Success.\n", "Executing node: lancedb_table.\n", - "Finished debugging node: lancedb_table in 510μs. Status: Success.\n", + "Finished debugging node: lancedb_table in 371μs. Status: Success.\n", "Executing node: lancedb_result.\n", - "Finished debugging node: lancedb_result in 38.3ms. Status: Success.\n" + "Finished debugging node: lancedb_result in 64.9ms. Status: Success.\n" ] }, { @@ -2600,7 +2261,7 @@ " '_distance': 0.9555794596672058}]}}" ] }, - "execution_count": 34, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -2691,17 +2352,122 @@ ] }, { + "cell_type": "markdown", + "id": "3fc9d8d9-ed58-4d83-a833-093887108e4f", + "metadata": {}, + "source": [ + "# Connect with tracking & telemetry\n", + "To gain more visibility into execution we can connect with the [Hamilton UI](https://blog.dagworks.io/p/hamilton-ui-streamlining-metadata?r=2cg5z1&utm_campaign=post&utm_medium=web). The following code assumes you have things running locally already. If you're looking for the SaaS version, signup for the free tier at [DAGWorks Inc](https://www.dagworks.io/hamliton)." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e9d1d27e-9912-4300-a005-2290bb49034e", "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "Capturing execution run. Results can be found at http://localhost:8242/dashboard/project/41/runs/51\n", + "\n", + "/Users/stefankrawczyk/.pyenv/versions/3.10.4/envs/ner-example-py310/lib/python3.10/site-packages/huggingface_hub/file_download.py:1132: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.\n", + " warnings.warn(\n", + "Some weights of the model checkpoint at dslim/bert-base-NER were not used when initializing BertForTokenClassification: ['bert.pooler.dense.bias', 'bert.pooler.dense.weight']\n", + "- This IS expected if you are initializing BertForTokenClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n", + "- This IS NOT expected if you are initializing BertForTokenClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n", + "\n", + "Captured execution run. Results can be found at http://localhost:8242/dashboard/project/41/runs/51\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "{'lancedb_result': {'Query': 'Who is Joe Biden?',\n", + " 'Query Entities': ['Joe Biden'],\n", + " 'Result': [{'title': 'Not the neighborhood he left: Biden’s international challenge',\n", + " 'url': 'https://medium.com/@info-63603/not-the-neighborhood-he-left-bidens-international-challenge-d023f7ed26d0',\n", + " 'named_entities': ['United States',\n", + " 'Joe Biden',\n", + " 'Russia',\n", + " 'American',\n", + " 'Trump',\n", + " 'Biden',\n", + " 'China'],\n", + " '_distance': 0.9555794596672058}]}}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from hamilton_sdk import adapters\n", + "from hamilton import driver\n", + "import uuid\n", + "\n", + "import lancedb\n", + "table_name = \"medium_docs\"\n", + "db_client = lancedb.connect(\"./.lancedb\")\n", + "RUN_ID = str(uuid.uuid4())\n", + "\n", + "tracker = adapters.HamiltonTracker(\n", + " project_id=41, # <--- modify this \n", + " username=\"elijah@dagworks.io\", # <--- modify this \n", + " dag_name=\"ner-lancedb-pipeline\",\n", + " tags={\"context\": \"querying\",\n", + " \"team\": \"MY_TEAM\",\n", + " \"run_id\": RUN_ID,\n", + " \"version\": \"1\"},\n", + ")\n", + "dr_query = (\n", + " driver.Builder()\n", + " .with_config({})\n", + " .with_modules(ner_module)\n", + " .with_adapters(tracker)\n", + " .build()\n", + ")\n", + "dr_query.execute(\n", + " [\"lancedb_result\"], \n", + " inputs={\n", + " \"table_name\": table_name, \n", + " \"query\": \"Who is Joe Biden?\",\n", + " \"db_client\": db_client\n", + " }\n", + ")" + ] + }, + { "cell_type": "markdown", + "id": "92cc0de121f3a3f5", + "metadata": {}, "source": [ "# Summary\n", "In this notebook we:\n", "\n", - "1. incrementally created a pipeline to process documents\n", - "2. the pipeline extracted named entities\n", + "1. incrementally created a pipeline to process medium articles\n", + "2. the pipeline extracted named entities from the articles\n", "3. the pipeline created vectors embeddings from text\n", "4. we pushed all the data into lanceDB to then query against\n", "\n", + "# Next steps to combine with RAG\n", + "We now have a database that can query over medium articles via cosine similarity, as well as\n", + "using extra metadata, in this case named entities referenced in the text, extracted to help us filter results.\n", + "\n", + "With this general blueprint, you can then play around with and modify what context you would\n", + "retrieve given a user query to then populate a prompt with to send to an LLM.\n", + "\n", + "For example, we could take the URLs returned and load the document that way, or \n", + "adjust what is stored in lancedb and return text stored there, etc. If you'd \n", + "like to build a conversational agent, we refer you to Hamilton's sister framework\n", + "[Burr](https://github.com/dagworks-inc/burr) that can help you build, curate,\n", + "and debug your application.\n", + "\n", + "\n", "# Extensions\n", "There's many ways to extend this pipeline. Here are a few ideas:\n", "\n", @@ -2712,16 +2478,15 @@ "5. Use query expansion to improve the results by expanding the extracted entities from the query.\n", "6. Use a re-ranking algorithm to rank the results.\n", "7. Work on document chunking to optimize for your particular RAG use case." - ], - "id": "92cc0de121f3a3f5" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, - "source": "", - "id": "54c5cf4aea8e2665" + "id": "54c5cf4aea8e2665", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/examples/LLM_Workflows/NER_Example/run.py b/examples/LLM_Workflows/NER_Example/run.py index 3dae7c469..56b57c94d 100644 --- a/examples/LLM_Workflows/NER_Example/run.py +++ b/examples/LLM_Workflows/NER_Example/run.py @@ -19,13 +19,14 @@ def build_driver(adapter_list): def load_data(table_name: str, use_tracker: bool = False): + """Runs the DAG to load data into LanceDB.""" adapter_list = [lifecycle.PrintLn()] if use_tracker: from hamilton_sdk import adapters tracker = adapters.HamiltonTracker( project_id=41, # modify this as needed - username="elijah@dagworks.io", + username="elijah@dagworks.io", # modify this as needed dag_name="ner-lancedb-pipeline", tags={"context": "extraction", "team": "MY_TEAM", "version": "1"}, ) @@ -43,13 +44,14 @@ def load_data(table_name: str, use_tracker: bool = False): def query_data(query: str, table_name: str, use_tracker: bool = False): + """Runs the DAG to query LanceDB.""" adapter_list = [lifecycle.PrintLn()] if use_tracker: from hamilton_sdk import adapters tracker = adapters.HamiltonTracker( project_id=41, # modify this as needed - username="elijah@dagworks.io", + username="elijah@dagworks.io", # modify this as needed dag_name="ner-lancedb-pipeline", tags={"context": "inference", "team": "MY_TEAM", "version": "1"}, ) @@ -61,7 +63,13 @@ def query_data(query: str, table_name: str, use_tracker: bool = False): print(r) -def main(): +if __name__ == "__main__": + """ + Some example commands: + > python run.py medium_docs load + > python run.py medium_docs query --query "Why does SpaceX want to build a city on Mars?" + > python run.py medium_docs query --query "How are autonomous vehicles changing the world?" + """ parser = argparse.ArgumentParser(description="Process command-line arguments.") parser.add_argument("table_name", help="The name of the table.") parser.add_argument("operation", choices=["load", "query"], help="The operation to perform.") @@ -77,13 +85,3 @@ def main(): load_data(args.table_name, args.use_tracker) else: query_data(args.query, args.table_name, args.use_tracker) - - -if __name__ == "__main__": - """ - Some example commands: - > python run.py medium_docs load - > python run.py medium_docs query --query "Why does SpaceX want to build a city on Mars?" - > python run.py medium_docs query --query "How are autonomous vehicles changing the world?" - """ - main()