diff --git a/docs/examples/hybrid-layer.ipynb b/docs/examples/hybrid-layer.ipynb index 589e13a1e4d3fc1d924c7bb851c19c56f9f30dd5..d3fb58c5978ea3da709d12eb850d16a846396e0b 100644 --- a/docs/examples/hybrid-layer.ipynb +++ b/docs/examples/hybrid-layer.ipynb @@ -151,39 +151,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "2067848296 1405\n", - "2212344012 2520\n", - "3313717465 206\n", - "3076736765 769\n", - "1778150425 4131\n", - "2067848296 1405\n", - "202708381 770\n", - "2212344012 2520\n", - "3374841595 2375\n", - "2067848296 1405\n", - "3508911095 2067\n", - "3454774732 not in encoder.idx_mapping\n", - "2379717389 3565\n", - "298452803 4356\n", - "1063320047 3369\n", - "4186256544 713\n", - "1846246980 858\n", - "3897916792 643\n", - "575623047 1476\n", - "3897916792 643\n" - ] - }, - { - "ename": "ValueError", - "evalue": "all the input arrays must have same number of dimensions, but the array at index 0 has 2 dimension(s) and the array at index 1 has 1 dimension(s)", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/Users/jamesbriggs/Documents/projects/aurelio-labs/semantic-router/docs/examples/hybrid-layer.ipynb Cell 14\u001b[0m line \u001b[0;36m3\n\u001b[1;32m <a href='vscode-notebook-cell:/Users/jamesbriggs/Documents/projects/aurelio-labs/semantic-router/docs/examples/hybrid-layer.ipynb#X16sZmlsZQ%3D%3D?line=0'>1</a>\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39msemantic_router\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mlayer\u001b[39;00m \u001b[39mimport\u001b[39;00m HybridDecisionLayer\n\u001b[0;32m----> <a href='vscode-notebook-cell:/Users/jamesbriggs/Documents/projects/aurelio-labs/semantic-router/docs/examples/hybrid-layer.ipynb#X16sZmlsZQ%3D%3D?line=2'>3</a>\u001b[0m dl \u001b[39m=\u001b[39m HybridDecisionLayer(encoder\u001b[39m=\u001b[39;49mencoder, decisions\u001b[39m=\u001b[39;49mdecisions)\n", - "File \u001b[0;32m~/Documents/projects/aurelio-labs/semantic-router/semantic_router/layer.py:137\u001b[0m, in \u001b[0;36mHybridDecisionLayer.__init__\u001b[0;34m(self, encoder, decisions, alpha)\u001b[0m\n\u001b[1;32m 134\u001b[0m \u001b[39mif\u001b[39;00m decisions:\n\u001b[1;32m 135\u001b[0m \u001b[39m# initialize index now\u001b[39;00m\n\u001b[1;32m 136\u001b[0m \u001b[39mfor\u001b[39;00m decision \u001b[39min\u001b[39;00m decisions:\n\u001b[0;32m--> 137\u001b[0m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_add_decision(decision\u001b[39m=\u001b[39;49mdecision)\n", - "File \u001b[0;32m~/Documents/projects/aurelio-labs/semantic-router/semantic_router/layer.py:156\u001b[0m, in \u001b[0;36mHybridDecisionLayer._add_decision\u001b[0;34m(self, decision)\u001b[0m\n\u001b[1;32m 154\u001b[0m sparse_embeds \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39msparse_encoder(decision\u001b[39m.\u001b[39mutterances)\n\u001b[1;32m 155\u001b[0m \u001b[39m# concatenate vectors to create hybrid vecs\u001b[39;00m\n\u001b[0;32m--> 156\u001b[0m embeds \u001b[39m=\u001b[39m np\u001b[39m.\u001b[39;49mconcatenate([\n\u001b[1;32m 157\u001b[0m dense_embeds, sparse_embeds\n\u001b[1;32m 158\u001b[0m ], axis\u001b[39m=\u001b[39;49m\u001b[39m1\u001b[39;49m)\n\u001b[1;32m 160\u001b[0m \u001b[39m# create decision array\u001b[39;00m\n\u001b[1;32m 161\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcategories \u001b[39mis\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n", - "\u001b[0;31mValueError\u001b[0m: all the input arrays must have same number of dimensions, but the array at index 0 has 2 dimension(s) and the array at index 1 has 1 dimension(s)" + "3454774732 not in encoder.idx_mapping\n" ] } ], @@ -195,9 +163,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "AxisError", + "evalue": "axis 1 is out of bounds for array of dimension 1", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAxisError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m/Users/jamesbriggs/Documents/projects/aurelio-labs/semantic-router/docs/examples/hybrid-layer.ipynb Cell 15\u001b[0m line \u001b[0;36m1\n\u001b[0;32m----> <a href='vscode-notebook-cell:/Users/jamesbriggs/Documents/projects/aurelio-labs/semantic-router/docs/examples/hybrid-layer.ipynb#X20sZmlsZQ%3D%3D?line=0'>1</a>\u001b[0m dl(\u001b[39m\"\u001b[39;49m\u001b[39mdon\u001b[39;49m\u001b[39m'\u001b[39;49m\u001b[39mt you love politics?\u001b[39;49m\u001b[39m\"\u001b[39;49m)\n", + "File \u001b[0;32m~/Documents/projects/aurelio-labs/semantic-router/semantic_router/layer.py:141\u001b[0m, in \u001b[0;36mHybridDecisionLayer.__call__\u001b[0;34m(self, text)\u001b[0m\n\u001b[1;32m 140\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39m__call__\u001b[39m(\u001b[39mself\u001b[39m, text: \u001b[39mstr\u001b[39m) \u001b[39m-\u001b[39m\u001b[39m>\u001b[39m \u001b[39mstr\u001b[39m \u001b[39m|\u001b[39m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m--> 141\u001b[0m results \u001b[39m=\u001b[39m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_query(text)\n\u001b[1;32m 142\u001b[0m top_class, top_class_scores \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_semantic_classify(results)\n\u001b[1;32m 143\u001b[0m passed \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_pass_threshold(top_class_scores, \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mscore_threshold)\n", + "File \u001b[0;32m~/Documents/projects/aurelio-labs/semantic-router/semantic_router/layer.py:204\u001b[0m, in \u001b[0;36mHybridDecisionLayer._query\u001b[0;34m(self, text, top_k)\u001b[0m\n\u001b[1;32m 202\u001b[0m sim_d \u001b[39m=\u001b[39m np\u001b[39m.\u001b[39mdot(\u001b[39mself\u001b[39m\u001b[39m.\u001b[39mindex, xq_d\u001b[39m.\u001b[39mT) \u001b[39m/\u001b[39m (index_norm \u001b[39m*\u001b[39m xq_d_norm)\n\u001b[1;32m 203\u001b[0m \u001b[39m# calculate sparse vec similarity\u001b[39;00m\n\u001b[0;32m--> 204\u001b[0m sparse_norm \u001b[39m=\u001b[39m norm(\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49msparse_index, axis\u001b[39m=\u001b[39;49m\u001b[39m1\u001b[39;49m)\n\u001b[1;32m 205\u001b[0m xq_s_norm \u001b[39m=\u001b[39m norm(xq_s\u001b[39m.\u001b[39mT)\n\u001b[1;32m 206\u001b[0m sim_s \u001b[39m=\u001b[39m np\u001b[39m.\u001b[39mdot(\u001b[39mself\u001b[39m\u001b[39m.\u001b[39msparse_index, xq_s\u001b[39m.\u001b[39mT) \u001b[39m/\u001b[39m (sparse_norm \u001b[39m*\u001b[39m xq_s_norm)\n", + "File \u001b[0;32m~/opt/anaconda3/envs/decision-layer/lib/python3.11/site-packages/numpy/linalg/linalg.py:2583\u001b[0m, in \u001b[0;36mnorm\u001b[0;34m(x, ord, axis, keepdims)\u001b[0m\n\u001b[1;32m 2580\u001b[0m \u001b[39melif\u001b[39;00m \u001b[39mord\u001b[39m \u001b[39mis\u001b[39;00m \u001b[39mNone\u001b[39;00m \u001b[39mor\u001b[39;00m \u001b[39mord\u001b[39m \u001b[39m==\u001b[39m \u001b[39m2\u001b[39m:\n\u001b[1;32m 2581\u001b[0m \u001b[39m# special case for speedup\u001b[39;00m\n\u001b[1;32m 2582\u001b[0m s \u001b[39m=\u001b[39m (x\u001b[39m.\u001b[39mconj() \u001b[39m*\u001b[39m x)\u001b[39m.\u001b[39mreal\n\u001b[0;32m-> 2583\u001b[0m \u001b[39mreturn\u001b[39;00m sqrt(add\u001b[39m.\u001b[39;49mreduce(s, axis\u001b[39m=\u001b[39;49maxis, keepdims\u001b[39m=\u001b[39;49mkeepdims))\n\u001b[1;32m 2584\u001b[0m \u001b[39m# None of the str-type keywords for ord ('fro', 'nuc')\u001b[39;00m\n\u001b[1;32m 2585\u001b[0m \u001b[39m# are valid for vectors\u001b[39;00m\n\u001b[1;32m 2586\u001b[0m \u001b[39melif\u001b[39;00m \u001b[39misinstance\u001b[39m(\u001b[39mord\u001b[39m, \u001b[39mstr\u001b[39m):\n", + "\u001b[0;31mAxisError\u001b[0m: axis 1 is out of bounds for array of dimension 1" + ] + } + ], "source": [ "dl(\"don't you love politics?\")" ] @@ -207,10 +190,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "if 3454774732 in encoder.idx_mapping:\n", - " print(\"yes\")" - ] + "source": [] }, { "cell_type": "code", @@ -220,11 +200,13 @@ "source": [] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "---\n", + "\n", + "#### Testing" + ] }, { "cell_type": "code", @@ -347,6 +329,15 @@ "sparse_vec.shape" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Finish Testing\n", + "\n", + "---" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/semantic_router/encoders/bm25.py b/semantic_router/encoders/bm25.py index 344f0820ae354645331dc6512456ffe65c60113b..c419ac8ff65c061c4e7f1829d2d23d5aee4b37ea 100644 --- a/semantic_router/encoders/bm25.py +++ b/semantic_router/encoders/bm25.py @@ -17,7 +17,7 @@ class BM25Encoder(BaseEncoder): def __call__(self, docs: list[str]) -> list[list[float]]: if len(docs) == 1: - sparse_dicts = self.model.encode_query(docs[0]) + sparse_dicts = self.model.encode_queries(docs) elif len(docs) > 1: sparse_dicts = self.model.encode_documents(docs) else: @@ -29,7 +29,6 @@ class BM25Encoder(BaseEncoder): values = output["values"] for idx, val in zip(indices, values): if idx in self.idx_mapping: - print(idx, self.idx_mapping[idx]) position = self.idx_mapping[idx] embeds[position] = val else: diff --git a/semantic_router/layer.py b/semantic_router/layer.py index e8b71576c4accd5936621f6da2889411caf2fe97..070c9c0bd37ab3d096e8d85753a93318eb68ce1d 100644 --- a/semantic_router/layer.py +++ b/semantic_router/layer.py @@ -111,6 +111,7 @@ class DecisionLayer: class HybridDecisionLayer: index = None + sparse_index = None categories = None score_threshold = 0.82 @@ -150,19 +151,19 @@ class HybridDecisionLayer: def _add_decision(self, decision: Decision): # create embeddings - dense_embeds = self.encoder(decision.utterances) * self.alpha - sparse_embeds = self.sparse_encoder(decision.utterances) * (1 - self.alpha) - # concatenate vectors to create hybrid vecs - embeds = np.concatenate([ - dense_embeds, sparse_embeds - ], axis=1) + dense_embeds = np.array( + self.encoder(decision.utterances) + ) # * self.alpha + sparse_embeds = np.array( + self.sparse_encoder(decision.utterances) + ) # * (1 - self.alpha) # create decision array if self.categories is None: - self.categories = np.array([decision.name] * len(embeds)) + self.categories = np.array([decision.name] * len(decision.utterances)) self.utterances = np.array(decision.utterances) else: - str_arr = np.array([decision.name] * len(embeds)) + str_arr = np.array([decision.name] * len(decision.utterances)) self.categories = np.concatenate([self.categories, str_arr]) self.utterances = np.concatenate([ self.utterances, @@ -170,17 +171,15 @@ class HybridDecisionLayer: ]) # create utterance array (the dense index) if self.index is None: - self.index = np.array(dense_embeds) + self.index = dense_embeds else: - embed_arr = np.array(dense_embeds) - self.index = np.concatenate([self.index, embed_arr]) + self.index = np.concatenate([self.index, dense_embeds]) # create sparse utterance array if self.sparse_index is None: - self.sparse_index = np.array(sparse_embeds) + self.sparse_index = sparse_embeds else: - sparse_embed_arr = np.array(sparse_embeds) self.sparse_index = np.concatenate([ - self.sparse_index, sparse_embed_arr + self.sparse_index, sparse_embeds ]) def _query(self, text: str, top_k: int = 5): @@ -195,17 +194,21 @@ class HybridDecisionLayer: xq_s = np.squeeze(xq_s) # convex scaling xq_d, xq_s = self._convex_scaling(xq_d, xq_s) - # concatenate to create single hybrid vec - xq = np.concatenate([xq_d, xq_s], axis=1) if self.index is not None: + # calculate dense vec similarity index_norm = norm(self.index, axis=1) - xq_norm = norm(xq.T) - sim = np.dot(self.index, xq.T) / (index_norm * xq_norm) + xq_d_norm = norm(xq_d.T) + sim_d = np.dot(self.index, xq_d.T) / (index_norm * xq_d_norm) + # calculate sparse vec similarity + sparse_norm = norm(self.sparse_index, axis=1) + xq_s_norm = norm(xq_s.T) + sim_s = np.dot(self.sparse_index, xq_s.T) / (sparse_norm * xq_s_norm) + total_sim = (sim_d + sim_s) # get indices of top_k records - top_k = min(top_k, sim.shape[0]) - idx = np.argpartition(sim, -top_k)[-top_k:] - scores = sim[idx] + top_k = min(top_k, total_sim.shape[0]) + idx = np.argpartition(total_sim, -top_k)[-top_k:] + scores = total_sim[idx] # get the utterance categories (decision names) decisions = self.categories[idx] if self.categories is not None else [] return [ @@ -216,8 +219,8 @@ class HybridDecisionLayer: def _convex_scaling(self, dense: list[float], sparse: list[float]): # scale sparse and dense vecs - dense = dense * self.alpha - sparse = sparse * (1 - self.alpha) + dense = np.array(dense) * self.alpha + sparse = np.array(sparse) * (1 - self.alpha) return dense, sparse def _semantic_classify(self, query_results: list[dict]) -> tuple[str, list[float]]: