1212from pyhealth .datasets import SampleDataset
1313from .base_model import BaseModel
1414
15- class N2V () :
16- """Node2Vec embeddings for OMOP concepts.
15+ class N2V :
16+ """Generate Node2Vec embeddings for OMOP concepts.
1717
18- This class builds a directed knowledge graph from OMOP concept and
19- concept relationship tables, then trains Node2Vec to generate
20- ontology-informed embeddings for medical concepts.
18+ Builds a directed knowledge graph from OMOP concept relationship tables
19+ and trains Node2Vec to create graph embeddings.
2120
2221 Attributes:
23- path: Path to the OMOP CSV files.
24- domain_type: List of OMOP domains used to filter concepts.
25- embedding_dim: Dimension of the learned embeddings.
22+ embedding_dim: Dimension of learned embeddings.
2623 walk_length: Length of each random walk.
27- num_walks: Number of walks generated per node.
28- graph: Directed graph constructed from OMOP concepts and relations.
24+ num_walks: Number of walks per node.
2925 """
3026 def __init__ (
31- self ,
32- embedding_dim :int = None ,
33- walk_length :int = None ,
34- num_walks :int = None
35- ):
27+ self ,
28+ embedding_dim : int | None = None ,
29+ walk_length : int | None = None ,
30+ num_walks : int | None = None ,
31+ ) -> None :
3632 self .embedding_dim = embedding_dim
3733 self .walk_length = walk_length
3834 self .num_walks = num_walks
3935
40- # Create graph from concept and their relationships data
41- def create_graph ( self , path , domain_type ) -> nx . DiGraph :
42- """
43- Create a directed graph from OMOP concept relationships.
44-
45- Loads concepts and their relationships from CSV files, filters by domain_type,
46- and builds a NetworkX DiGraph where nodes are concept IDs and edges are
47- concept relationships (maps_to) .
48-
36+ def create_graph (
37+ self , path : str , domain_type : list [ str ]
38+ ) -> nx . DiGraph :
39+ """ Create a Network directed graph from OMOP concept relationships.
40+
41+ Args:
42+ path: Path to OMOP concept CSV files.
43+ domain_type: List of domain IDs to include .
44+
4945 Returns:
50- nx.DiGraph: Directed graph with concept_id as nodes and relationships as edges.
51-
52- Raises:
53- FileNotFoundError: If CSV files are not found.
54- ValueError: If no concepts found for specified domains.
46+ Directed graph with concept IDs as nodes and relationships
47+ as edges.
5548 """
5649 # Load concept table
5750 concept_path = os .path .join (path , "2b_concept.csv" )
@@ -110,30 +103,36 @@ def create_graph(self, path, domain_type) -> nx.DiGraph:
110103
111104 return graph
112105
113- def _build_index_mapping (self , node_embeddings ):
114- """
115- Build a dictionary to map concept code to the index in node_embeddings.
116-
106+ def _build_index_mapping (
107+ self , node_embeddings : object
108+ ) -> dict [int , int ]:
109+ """Map concept codes to embedding indices.
110+
117111 Args:
118- node_embeddings: Gensim Word2Vec model word vectors
119-
112+ node_embeddings: Word2Vec model word vectors.
113+
120114 Returns:
121- dict: Mapping from concept_id (int) to index in embeddings
115+ Mapping from concept_id to index in embeddings.
122116 """
123117 return {int (key ): i for i , key in enumerate (node_embeddings .index_to_key )}
124118
125- def _get_vector_iso (self , code , node_embeddings , index_mapping , mean_vector ):
126- """
127- Return concept embedding for the given code or mean vector if not found.
128-
119+ def _get_vector_iso (
120+ self ,
121+ code : int ,
122+ node_embeddings : object ,
123+ index_mapping : dict ,
124+ mean_vector : np .ndarray ,
125+ ) -> np .ndarray :
126+ """Get embedding vector for code or mean vector if not found.
127+
129128 Args:
130- code: Concept ID
131- node_embeddings: Gensim Word2Vec model word vectors
132- index_mapping: Dictionary mapping concept_id to index
133- mean_vector: Mean vector to use as fallback
134-
129+ code: Concept ID.
130+ node_embeddings: Word2Vec model word vectors.
131+ index_mapping: Concept ID to embedding index mapping.
132+ mean_vector: Fallback vector to use if code not found.
133+
135134 Returns:
136- np.ndarray: Embedding vector for the concept
135+ Embedding vector for the concept.
137136 """
138137 index = index_mapping .get (int (code ))
139138 if index is not None :
@@ -142,16 +141,18 @@ def _get_vector_iso(self, code, node_embeddings, index_mapping, mean_vector):
142141 print (f"Code { code } not found, returning mean vector." )
143142 return mean_vector
144143
145- def generate_embeddings (self , graph ):
146- """
147- Generate node embeddings using Node2Vec algorithm.
148-
149- Creates a graph from OMOP concepts and applies Node2Vec to generate
150- embeddings for each concept based on its network structure.
151-
144+ def generate_embeddings (
145+ self , graph : nx .DiGraph
146+ ) -> tuple [np .ndarray , list ]:
147+ """Generate node embeddings using Node2Vec.
148+
149+ Args:
150+ graph: Directed graph of OMOP concepts and relationships.
151+
152152 Returns:
153- tuple: (embedding_matrix, node_ids) where embedding_matrix is the numpy array
154- of embeddings and node_ids is the list of graph node IDs in order.
153+ Tuple of (embedding_matrix, node_ids) where embedding_matrix
154+ is numpy array of embeddings and node_ids is the list of
155+ node IDs in order.
155156 """
156157 print (f"Graph created with { len (graph .nodes ())} nodes and { len (graph .edges ())} edges" )
157158
@@ -191,43 +192,65 @@ def generate_embeddings(self, graph):
191192 return embedding_matrix , keys
192193
193194class KeepEmbedding (BaseModel ):
194- """KEEP Embedding: Fine-tune Node2Vec embeddings using GloVe while penalizing
195- deviation from original embeddings.
196-
197- Balances:
198- - Co-occurrence structure (GloVe objective)
199- - Graph structure prior (Node2Vec via regularization)
200-
195+ """KEEP Embedding Framework
196+
197+
198+ Fine-tune Node2Vec embeddings using GloVe with graph regularization.
199+
200+ Balances co-occurrence structure (GloVe) with graph (Node2Vec) via regularization
201+ to generate medical concept embeddings.
202+
201203 Args:
202- dataset (SampleDataset): The dataset to train the model.
203- path (str): Path to OMOP data files for graph construction.
204- domain_type (list[str]): Domain types to include in graph.
205- embedding_dim (int): Dimension of embeddings.
206- walk_length (int): Length of random walks for Node2Vec.
207- num_walks (int): Number of random walks per node for Node2Vec.
208- lambda_reg (float): Regularization strength for Node2Vec prior. Default: 1.0.
209- reg_norm (str or float): Norm type for regularization ('cosine' or numeric p-norm).
210- Default: None (cosine similarity).
211- log_scale (bool): Whether to apply log scaling to regularization distance.
212- Default: False.
213- code_to_index (dict, optional): Mapping from concept codes to vocabulary indices.
214- If provided, embeddings are filtered to only include codes in this mapping.
215- device (str): Device to use ('cuda' or 'cpu'). Default: 'cpu'.
204+ dataset: Dataset to train the model.
205+ graph: Directed graph of concepts and relationships.
206+ embedding_dim: Dimension of embeddings.
207+ walk_length: Length of random walks for Node2Vec.
208+ num_walks: Number of random walks per node.
209+ num_words: Size of vocabulary.
210+ lambda_reg: Regularization strength (default: 1.0).
211+ reg_norm: Norm type ('cosine' or numeric p-norm, default: None).
212+ log_scale: Apply log scaling to regularization distance
213+ (default: False).
214+ code_to_index: Optional mapping from concept codes to indices.
215+ device: Device to use ('cuda' or 'cpu', default: 'cpu').
216+
217+ Examples:
218+ >>> from pyhealth.datasets import OMOPDataset
219+ >>> from pyhealth.models import KeepEmbedding
220+ >>> dataset = SampleDataset(num_patients=100, num_visits=10, num_codes=50)
221+ >>> graph = n2v.create_graph() # Build knowledge graph from concept and relationship tables
222+ >>> dataset = OMOPDataset(...)
223+ >>> # Build co-occurrence matrix from dataset
224+ >>> # Load co-occurrence matrix as GloveDatset Dataloader
225+ >>> model = KeepEmbedding(
226+ ... dataset=None,
227+ ... graph=graph,
228+ ... embedding_dim=128,
229+ ... walk_length=10,
230+ ... num_walks=5,
231+ ... num_words=50,
232+ ... lambda_reg=0.5,
233+ ... reg_norm='cosine',
234+ ... log_scale=True,
235+ ... device='cuda'
236+ ... )
237+ >>> # Use embeddings for with downstream PyHealth models
216238 """
217239
218- def __init__ (self ,
219- dataset : SampleDataset ,
220- graph : nx .Graph ,
221- embedding_dim :int ,
222- walk_length :int ,
223- num_walks :int ,
224- num_words : int ,
225- lambda_reg : float = 1.0 ,
226- reg_norm : str | float = None ,
227- log_scale : bool = False ,
228- code_to_index : dict = None ,
229- device : str = "cpu"
230- ):
240+ def __init__ (
241+ self ,
242+ dataset : SampleDataset ,
243+ graph : nx .Graph ,
244+ embedding_dim : int ,
245+ walk_length : int ,
246+ num_walks : int ,
247+ num_words : int ,
248+ lambda_reg : float = 1.0 ,
249+ reg_norm : str | float | None = None ,
250+ log_scale : bool = False ,
251+ code_to_index : dict | None = None ,
252+ device : str = "cpu" ,
253+ ) -> None :
231254 """Initialize KEEP Embedding model."""
232255 super ().__init__ (dataset = dataset )
233256
@@ -301,38 +324,26 @@ def __init__(self,
301324 print (f"Regularization norm: { reg_norm } " )
302325 print (f"Log scaling: { log_scale } " )
303326
304- def forward (self ,
305- i_indices : torch .Tensor = None ,
306- j_indices : torch .Tensor = None ,
307- counts : torch .Tensor = None ,
308- weights : torch .Tensor = None ,
309- ** kwargs ) -> dict [str , torch .Tensor ]:
310- """Forward pass for KEEP Embedding.
311-
312- Computes GloVe loss with optional Node2Vec regularization. For compatibility
313- with BaseModel.forward(), returns a dictionary with keys: loss, y_prob,
314- y_true, logit.
315-
316- For training GloVe objective, pass:
317- - i_indices: Token indices (batch_size,)
318- - j_indices: Context token indices (batch_size,)
319- - counts: Co-occurrence counts (batch_size,)
320- - weights: Weights for each co-occurrence pair (batch_size,)
321-
327+ def forward (
328+ self ,
329+ i_indices : torch .Tensor | None = None ,
330+ j_indices : torch .Tensor | None = None ,
331+ counts : torch .Tensor | None = None ,
332+ weights : torch .Tensor | None = None ,
333+ ** kwargs ,
334+ ) -> dict [str , torch .Tensor ]:
335+ """Compute GloVe loss with optional Node2Vec regularization.
336+
322337 Args:
323- i_indices (torch.Tensor, optional) : Token indices.
324- j_indices (torch.Tensor, optional) : Context token indices.
325- counts (torch.Tensor, optional) : Co-occurrence counts.
326- weights (torch.Tensor, optional) : Weights for loss terms.
338+ i_indices: Token indices (batch_size,) .
339+ j_indices: Context token indices (batch_size,) .
340+ counts: Co-occurrence counts (batch_size,) .
341+ weights: Weights for loss terms (batch_size,) .
327342 **kwargs: Additional arguments for compatibility.
328-
343+
329344 Returns:
330- dict: Dictionary with keys:
331- - loss: Total loss (GloVe + regularization if applicable)
332- - logit: Placeholder tensor (for BaseModel compatibility)
333- - y_prob: Placeholder tensor (for BaseModel compatibility)
334- - y_true: Placeholder tensor (for BaseModel compatibility)
335- - reg_loss: Regularization loss component (if applicable)
345+ Dictionary with keys: loss, logit, y_prob, y_true,
346+ reg_loss.
336347 """
337348
338349 # If no GloVe inputs provided, return dummy output
0 commit comments