Spaces:
Runtime error
Runtime error
Upload 59 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- Dockerfile +18 -0
- GMatch4py/mcp_output/README_MCP.md +48 -0
- GMatch4py/mcp_output/analysis.json +119 -0
- GMatch4py/mcp_output/diff_report.md +66 -0
- GMatch4py/mcp_output/mcp_plugin/__init__.py +0 -0
- GMatch4py/mcp_output/mcp_plugin/adapter.py +87 -0
- GMatch4py/mcp_output/mcp_plugin/main.py +13 -0
- GMatch4py/mcp_output/mcp_plugin/mcp_service.py +42 -0
- GMatch4py/mcp_output/requirements.txt +9 -0
- GMatch4py/mcp_output/start_mcp.py +30 -0
- GMatch4py/mcp_output/workflow_summary.json +198 -0
- GMatch4py/source/.travis.yml +14 -0
- GMatch4py/source/LICENSE +21 -0
- GMatch4py/source/README.md +160 -0
- GMatch4py/source/__init__.py +4 -0
- GMatch4py/source/gmatch4py/__init__.py +27 -0
- GMatch4py/source/gmatch4py/bag_of_cliques.pyx +167 -0
- GMatch4py/source/gmatch4py/base.pxd +19 -0
- GMatch4py/source/gmatch4py/base.pyx +218 -0
- GMatch4py/source/gmatch4py/bon.pyx +40 -0
- GMatch4py/source/gmatch4py/embedding/__init__.py +0 -0
- GMatch4py/source/gmatch4py/embedding/deepwalk.pyx +173 -0
- GMatch4py/source/gmatch4py/embedding/graph.pyx +319 -0
- GMatch4py/source/gmatch4py/embedding/graph2vec.pyx +184 -0
- GMatch4py/source/gmatch4py/embedding/node2vec.pyx +255 -0
- GMatch4py/source/gmatch4py/embedding/skipgram.pyx +26 -0
- GMatch4py/source/gmatch4py/embedding/walks.pyx +103 -0
- GMatch4py/source/gmatch4py/ged/__init__.py +2 -0
- GMatch4py/source/gmatch4py/ged/abstract_graph_edit_dist.pxd +19 -0
- GMatch4py/source/gmatch4py/ged/abstract_graph_edit_dist.pyx +197 -0
- GMatch4py/source/gmatch4py/ged/bipartite_graph_matching_2.pyx +238 -0
- GMatch4py/source/gmatch4py/ged/graph_edit_dist.pxd +10 -0
- GMatch4py/source/gmatch4py/ged/graph_edit_dist.pyx +57 -0
- GMatch4py/source/gmatch4py/ged/greedy_edit_distance.pyx +30 -0
- GMatch4py/source/gmatch4py/ged/hausdorff_edit_distance.pyx +220 -0
- GMatch4py/source/gmatch4py/graph.pxd +122 -0
- GMatch4py/source/gmatch4py/graph.pyx +389 -0
- GMatch4py/source/gmatch4py/helpers/__init__.py +1 -0
- GMatch4py/source/gmatch4py/helpers/general.pyx +23 -0
- GMatch4py/source/gmatch4py/helpers/reader.pyx +79 -0
- GMatch4py/source/gmatch4py/jaccard.pyx +52 -0
- GMatch4py/source/gmatch4py/kernels/__init__.py +1 -0
- GMatch4py/source/gmatch4py/kernels/adjacency.pyx +52 -0
- GMatch4py/source/gmatch4py/kernels/random_walk_kernel.pyx +93 -0
- GMatch4py/source/gmatch4py/kernels/shortest_path_kernel.pyx +149 -0
- GMatch4py/source/gmatch4py/kernels/weisfeiler_lehman.pyx +144 -0
- GMatch4py/source/gmatch4py/mcs.pyx +45 -0
- GMatch4py/source/gmatch4py/vertex_edge_overlap.pyx +57 -0
- GMatch4py/source/gmatch4py/vertex_ranking.pyx +42 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
GMatch4py/source/logo2.png filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10
|
| 2 |
+
|
| 3 |
+
RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
|
| 4 |
+
USER user
|
| 5 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 10 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY --chown=user . /app
|
| 13 |
+
ENV MCP_TRANSPORT=http
|
| 14 |
+
ENV MCP_PORT=7860
|
| 15 |
+
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
|
| 18 |
+
CMD ["python", "GMatch4py/mcp_output/start_mcp.py"]
|
GMatch4py/mcp_output/README_MCP.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GMatch4py MCP (Model Context Protocol) Service
|
| 2 |
+
|
| 3 |
+
## Project Introduction
|
| 4 |
+
GMatch4py is a Python library designed for graph matching tasks. It provides a suite of tools and services to facilitate the comparison and analysis of graph structures. The library is structured to support various graph matching algorithms and is ideal for developers looking to integrate graph-based solutions into their applications.
|
| 5 |
+
|
| 6 |
+
## Installation Method
|
| 7 |
+
To install GMatch4py, ensure you have Python installed on your system. The library's dependencies are listed in the `requirements.txt` file. You can install the necessary packages using pip:
|
| 8 |
+
|
| 9 |
+
pip install -r requirements.txt
|
| 10 |
+
|
| 11 |
+
Alternatively, you can install the package directly from the source:
|
| 12 |
+
|
| 13 |
+
git clone https://github.com/jacquesfize/GMatch4py.git
|
| 14 |
+
cd GMatch4py
|
| 15 |
+
python setup.py install
|
| 16 |
+
|
| 17 |
+
## Quick Start
|
| 18 |
+
To quickly get started with GMatch4py, you can use the following example to call the main functions:
|
| 19 |
+
|
| 20 |
+
1. Import the library:
|
| 21 |
+
|
| 22 |
+
import gmatch4py
|
| 23 |
+
|
| 24 |
+
2. Use the graph matching services:
|
| 25 |
+
|
| 26 |
+
# Example of using a graph matching service
|
| 27 |
+
matcher = gmatch4py.SomeMatcher()
|
| 28 |
+
result = matcher.compare(graph1, graph2)
|
| 29 |
+
|
| 30 |
+
Replace `SomeMatcher` with the specific matcher you wish to use.
|
| 31 |
+
|
| 32 |
+
## Available Tools and Endpoints List
|
| 33 |
+
GMatch4py offers several services categorized under different modules:
|
| 34 |
+
|
| 35 |
+
- `gmatch4py.embedding`: Services for graph embedding.
|
| 36 |
+
- `gmatch4py.ged`: Services for graph edit distance calculations.
|
| 37 |
+
- `gmatch4py.helpers`: Utility services to assist with graph operations.
|
| 38 |
+
- `gmatch4py.kernels`: Services for graph kernel computations.
|
| 39 |
+
|
| 40 |
+
Each module provides specialized functions to handle various aspects of graph matching and analysis.
|
| 41 |
+
|
| 42 |
+
## Common Issues and Notes
|
| 43 |
+
- Ensure all dependencies are installed as specified in `requirements.txt`.
|
| 44 |
+
- The library is designed to be lightweight with a low intrusiveness risk and simple complexity.
|
| 45 |
+
- Performance may vary based on the size and complexity of the graphs being processed.
|
| 46 |
+
|
| 47 |
+
## Reference Links or Documentation
|
| 48 |
+
For more information, visit the [GMatch4py GitHub repository](https://github.com/jacquesfize/GMatch4py). The repository contains additional documentation and resources to help you make the most of the library.
|
GMatch4py/mcp_output/analysis.json
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"summary": {
|
| 3 |
+
"repository_url": "https://github.com/jacquesfize/GMatch4py",
|
| 4 |
+
"summary": "Imported via zip fallback, file count: 11",
|
| 5 |
+
"file_tree": {
|
| 6 |
+
".travis.yml": {
|
| 7 |
+
"size": 223
|
| 8 |
+
},
|
| 9 |
+
"README.md": {
|
| 10 |
+
"size": 6332
|
| 11 |
+
},
|
| 12 |
+
"gmatch4py/__init__.py": {
|
| 13 |
+
"size": 726
|
| 14 |
+
},
|
| 15 |
+
"gmatch4py/embedding/__init__.py": {
|
| 16 |
+
"size": 0
|
| 17 |
+
},
|
| 18 |
+
"gmatch4py/ged/__init__.py": {
|
| 19 |
+
"size": 18
|
| 20 |
+
},
|
| 21 |
+
"gmatch4py/helpers/__init__.py": {
|
| 22 |
+
"size": 16
|
| 23 |
+
},
|
| 24 |
+
"gmatch4py/kernels/__init__.py": {
|
| 25 |
+
"size": 16
|
| 26 |
+
},
|
| 27 |
+
"requirements.txt": {
|
| 28 |
+
"size": 44
|
| 29 |
+
},
|
| 30 |
+
"setup.py": {
|
| 31 |
+
"size": 2812
|
| 32 |
+
},
|
| 33 |
+
"test/gmatch4py_performance_test.py": {
|
| 34 |
+
"size": 1230
|
| 35 |
+
},
|
| 36 |
+
"test/test.py": {
|
| 37 |
+
"size": 6131
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"processed_by": "zip_fallback",
|
| 41 |
+
"success": true
|
| 42 |
+
},
|
| 43 |
+
"structure": {
|
| 44 |
+
"packages": [
|
| 45 |
+
"source.gmatch4py",
|
| 46 |
+
"source.gmatch4py.embedding",
|
| 47 |
+
"source.gmatch4py.ged",
|
| 48 |
+
"source.gmatch4py.helpers",
|
| 49 |
+
"source.gmatch4py.kernels"
|
| 50 |
+
]
|
| 51 |
+
},
|
| 52 |
+
"dependencies": {
|
| 53 |
+
"has_environment_yml": false,
|
| 54 |
+
"has_requirements_txt": true,
|
| 55 |
+
"pyproject": false,
|
| 56 |
+
"setup_cfg": false,
|
| 57 |
+
"setup_py": true
|
| 58 |
+
},
|
| 59 |
+
"entry_points": {
|
| 60 |
+
"imports": [],
|
| 61 |
+
"cli": [],
|
| 62 |
+
"modules": []
|
| 63 |
+
},
|
| 64 |
+
"llm_analysis": {
|
| 65 |
+
"core_modules": [
|
| 66 |
+
{
|
| 67 |
+
"package": "setup",
|
| 68 |
+
"module": "setup",
|
| 69 |
+
"functions": [
|
| 70 |
+
"makeExtension",
|
| 71 |
+
"scandir"
|
| 72 |
+
],
|
| 73 |
+
"classes": [],
|
| 74 |
+
"function_signatures": {
|
| 75 |
+
"scandir": [
|
| 76 |
+
"dir",
|
| 77 |
+
"files"
|
| 78 |
+
],
|
| 79 |
+
"makeExtension": [
|
| 80 |
+
"extName"
|
| 81 |
+
]
|
| 82 |
+
},
|
| 83 |
+
"description": "Discovered via AST scan"
|
| 84 |
+
}
|
| 85 |
+
],
|
| 86 |
+
"cli_commands": [],
|
| 87 |
+
"import_strategy": {
|
| 88 |
+
"primary": "import",
|
| 89 |
+
"fallback": "blackbox",
|
| 90 |
+
"confidence": 0.9
|
| 91 |
+
},
|
| 92 |
+
"dependencies": {
|
| 93 |
+
"required": [],
|
| 94 |
+
"optional": []
|
| 95 |
+
},
|
| 96 |
+
"risk_assessment": {
|
| 97 |
+
"import_feasibility": 0.8,
|
| 98 |
+
"intrusiveness_risk": "low",
|
| 99 |
+
"complexity": "simple"
|
| 100 |
+
}
|
| 101 |
+
},
|
| 102 |
+
"deepwiki_analysis": {
|
| 103 |
+
"repo_url": "https://github.com/jacquesfize/GMatch4py",
|
| 104 |
+
"repo_name": "GMatch4py",
|
| 105 |
+
"content": "jacquesfize/GMatch4py\nA graph matching library for Python\nRepository Not Indexed\nThis repository hasn't been indexed yet. Indexing allows you to explore code structure, find documentation, and understand dependencies.\nIndexing typically takes 2-10 minutes to complete after it starts indexing\nOnce indexed, you'll have full access to code exploration and search functionality",
|
| 106 |
+
"model": "gpt-4o-2024-08-06",
|
| 107 |
+
"source": "selenium",
|
| 108 |
+
"success": true
|
| 109 |
+
},
|
| 110 |
+
"deepwiki_options": {
|
| 111 |
+
"enabled": true,
|
| 112 |
+
"model": "gpt-4o-2024-08-06"
|
| 113 |
+
},
|
| 114 |
+
"risk": {
|
| 115 |
+
"import_feasibility": 0.8,
|
| 116 |
+
"intrusiveness_risk": "low",
|
| 117 |
+
"complexity": "simple"
|
| 118 |
+
}
|
| 119 |
+
}
|
GMatch4py/mcp_output/diff_report.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Difference Report for GMatch4py Project
|
| 2 |
+
|
| 3 |
+
## Project Overview
|
| 4 |
+
|
| 5 |
+
**Repository:** GMatch4py
|
| 6 |
+
**Project Type:** Python Library
|
| 7 |
+
**Main Features:** Basic Functionality
|
| 8 |
+
**Report Generated On:** 2026-02-07 13:40:57
|
| 9 |
+
|
| 10 |
+
GMatch4py is a Python library designed to provide graph matching functionalities. It is widely used in applications requiring graph comparison and analysis. The project aims to offer efficient and easy-to-use tools for researchers and developers working with graph data structures.
|
| 11 |
+
|
| 12 |
+
## Difference Analysis
|
| 13 |
+
|
| 14 |
+
### Summary of Changes
|
| 15 |
+
|
| 16 |
+
- **New Files Added:** 8
|
| 17 |
+
- **Modified Files:** 0
|
| 18 |
+
- **Intrusiveness:** None
|
| 19 |
+
- **Workflow Status:** Success
|
| 20 |
+
- **Test Status:** Failed
|
| 21 |
+
|
| 22 |
+
### New Files
|
| 23 |
+
|
| 24 |
+
The addition of 8 new files suggests an expansion in the library's capabilities or the introduction of new features. However, without modifications to existing files, it appears that these changes are isolated and do not alter the core functionality of the library.
|
| 25 |
+
|
| 26 |
+
### Workflow and Test Status
|
| 27 |
+
|
| 28 |
+
- **Workflow Status:** The workflow has been executed successfully, indicating that the integration and deployment processes are functioning as expected.
|
| 29 |
+
- **Test Status:** The test status is marked as failed, which is a critical issue that needs immediate attention. This failure suggests that the new additions may have introduced bugs or that the existing test cases are not comprehensive enough to cover the new functionalities.
|
| 30 |
+
|
| 31 |
+
## Technical Analysis
|
| 32 |
+
|
| 33 |
+
### New Files
|
| 34 |
+
|
| 35 |
+
The introduction of new files could include additional modules, scripts, or resources that enhance the library's capabilities. A detailed review of these files is necessary to understand their purpose and integration within the existing framework.
|
| 36 |
+
|
| 37 |
+
### Test Failures
|
| 38 |
+
|
| 39 |
+
The failure in testing indicates potential issues in the new code. It is crucial to identify whether the failures are due to:
|
| 40 |
+
- Incompatibility with existing code.
|
| 41 |
+
- Insufficient or outdated test cases.
|
| 42 |
+
- Bugs introduced in the new files.
|
| 43 |
+
|
| 44 |
+
## Recommendations and Improvements
|
| 45 |
+
|
| 46 |
+
1. **Conduct a Detailed Code Review:** Examine the new files to ensure they adhere to the project's coding standards and integrate seamlessly with existing components.
|
| 47 |
+
|
| 48 |
+
2. **Enhance Test Coverage:** Update and expand the test suite to include cases for the new functionalities. Ensure that all edge cases are considered to prevent future failures.
|
| 49 |
+
|
| 50 |
+
3. **Debug and Resolve Test Failures:** Identify the root cause of the test failures and implement fixes. This may involve debugging the new code or refining the test cases.
|
| 51 |
+
|
| 52 |
+
4. **Documentation Update:** Ensure that the documentation reflects the new changes, providing clear guidance on how to utilize the new features.
|
| 53 |
+
|
| 54 |
+
## Deployment Information
|
| 55 |
+
|
| 56 |
+
The successful workflow status indicates that the deployment pipeline is operational. However, given the test failures, it is advisable to hold off on deploying the new version until all issues are resolved and the test status is marked as successful.
|
| 57 |
+
|
| 58 |
+
## Future Planning
|
| 59 |
+
|
| 60 |
+
- **Regular Updates:** Schedule regular updates and reviews to ensure the library remains robust and up-to-date with the latest developments in graph matching algorithms.
|
| 61 |
+
- **Community Engagement:** Encourage community feedback and contributions to identify potential areas for improvement and new feature requests.
|
| 62 |
+
- **Performance Optimization:** Continuously monitor and optimize the library's performance to handle larger datasets efficiently.
|
| 63 |
+
|
| 64 |
+
## Conclusion
|
| 65 |
+
|
| 66 |
+
The GMatch4py project has seen significant additions with the introduction of new files. However, the test failures highlight the need for immediate attention to ensure the library's reliability and functionality. By addressing the recommendations outlined in this report, the project can continue to provide valuable tools for graph analysis and maintain its reputation as a reliable Python library.
|
GMatch4py/mcp_output/mcp_plugin/__init__.py
ADDED
|
File without changes
|
GMatch4py/mcp_output/mcp_plugin/adapter.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Path settings
|
| 5 |
+
source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
|
| 6 |
+
sys.path.insert(0, source_path)
|
| 7 |
+
|
| 8 |
+
# Import statements
|
| 9 |
+
try:
|
| 10 |
+
from gmatch4py.setup import makeExtension, scandir
|
| 11 |
+
except ImportError as e:
|
| 12 |
+
print("Failed to import modules from gmatch4py. Ensure the source directory is correctly set.")
|
| 13 |
+
raise e
|
| 14 |
+
|
| 15 |
+
class Adapter:
|
| 16 |
+
"""
|
| 17 |
+
Adapter class for the MCP plugin, utilizing the GMatch4py library.
|
| 18 |
+
This class provides methods to interact with the core functionalities of the library.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self):
|
| 22 |
+
"""
|
| 23 |
+
Initialize the Adapter class with default settings.
|
| 24 |
+
"""
|
| 25 |
+
self.mode = "import"
|
| 26 |
+
|
| 27 |
+
# -------------------------------------------------------------------------
|
| 28 |
+
# Function Call Methods
|
| 29 |
+
# -------------------------------------------------------------------------
|
| 30 |
+
|
| 31 |
+
def call_make_extension(self, *args, **kwargs):
|
| 32 |
+
"""
|
| 33 |
+
Call the makeExtension function from gmatch4py.setup.
|
| 34 |
+
|
| 35 |
+
Parameters:
|
| 36 |
+
- args: Positional arguments for makeExtension.
|
| 37 |
+
- kwargs: Keyword arguments for makeExtension.
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
- dict: A dictionary containing the status and result or error message.
|
| 41 |
+
"""
|
| 42 |
+
try:
|
| 43 |
+
result = makeExtension(*args, **kwargs)
|
| 44 |
+
return {"status": "success", "result": result}
|
| 45 |
+
except Exception as e:
|
| 46 |
+
return {"status": "error", "message": f"Failed to call makeExtension: {str(e)}"}
|
| 47 |
+
|
| 48 |
+
def call_scandir(self, *args, **kwargs):
|
| 49 |
+
"""
|
| 50 |
+
Call the scandir function from gmatch4py.setup.
|
| 51 |
+
|
| 52 |
+
Parameters:
|
| 53 |
+
- args: Positional arguments for scandir.
|
| 54 |
+
- kwargs: Keyword arguments for scandir.
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
- dict: A dictionary containing the status and result or error message.
|
| 58 |
+
"""
|
| 59 |
+
try:
|
| 60 |
+
result = scandir(*args, **kwargs)
|
| 61 |
+
return {"status": "success", "result": result}
|
| 62 |
+
except Exception as e:
|
| 63 |
+
return {"status": "error", "message": f"Failed to call scandir: {str(e)}"}
|
| 64 |
+
|
| 65 |
+
# -------------------------------------------------------------------------
|
| 66 |
+
# Error Handling and Fallback
|
| 67 |
+
# -------------------------------------------------------------------------
|
| 68 |
+
|
| 69 |
+
def handle_import_failure(self):
|
| 70 |
+
"""
|
| 71 |
+
Handle import failures gracefully and provide guidance.
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
- dict: A dictionary containing the status and guidance message.
|
| 75 |
+
"""
|
| 76 |
+
return {
|
| 77 |
+
"status": "error",
|
| 78 |
+
"message": "Import failed. Please ensure the source directory is correctly set and all dependencies are installed."
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
# Example usage
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
adapter = Adapter()
|
| 84 |
+
result = adapter.call_make_extension()
|
| 85 |
+
print(result)
|
| 86 |
+
result = adapter.call_scandir()
|
| 87 |
+
print(result)
|
GMatch4py/mcp_output/mcp_plugin/main.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Service Auto-Wrapper - Auto-generated
|
| 3 |
+
"""
|
| 4 |
+
from mcp_service import create_app
|
| 5 |
+
|
| 6 |
+
def main():
|
| 7 |
+
"""Main entry point"""
|
| 8 |
+
app = create_app()
|
| 9 |
+
return app
|
| 10 |
+
|
| 11 |
+
if __name__ == "__main__":
|
| 12 |
+
app = main()
|
| 13 |
+
app.run()
|
GMatch4py/mcp_output/mcp_plugin/mcp_service.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
|
| 5 |
+
if source_path not in sys.path:
|
| 6 |
+
sys.path.insert(0, source_path)
|
| 7 |
+
|
| 8 |
+
from fastmcp import FastMCP
|
| 9 |
+
|
| 10 |
+
from setup import scandir, makeExtension
|
| 11 |
+
|
| 12 |
+
mcp = FastMCP("unknown_service")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@mcp.tool(name="makeExtension", description="Auto-wrapped function makeExtension")
|
| 16 |
+
def makeExtension(payload: dict):
|
| 17 |
+
try:
|
| 18 |
+
if makeExtension is None:
|
| 19 |
+
return {"success": False, "result": None, "error": "Function makeExtension is not available"}
|
| 20 |
+
result = makeExtension(**payload)
|
| 21 |
+
return {"success": True, "result": result, "error": None}
|
| 22 |
+
except Exception as e:
|
| 23 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 24 |
+
|
| 25 |
+
@mcp.tool(name="scandir", description="Auto-wrapped function scandir")
|
| 26 |
+
def scandir(payload: dict):
|
| 27 |
+
try:
|
| 28 |
+
if scandir is None:
|
| 29 |
+
return {"success": False, "result": None, "error": "Function scandir is not available"}
|
| 30 |
+
result = scandir(**payload)
|
| 31 |
+
return {"success": True, "result": result, "error": None}
|
| 32 |
+
except Exception as e:
|
| 33 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def create_app():
|
| 38 |
+
"""Create and return FastMCP application instance"""
|
| 39 |
+
return mcp
|
| 40 |
+
|
| 41 |
+
if __name__ == "__main__":
|
| 42 |
+
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
GMatch4py/mcp_output/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastmcp
|
| 2 |
+
fastapi
|
| 3 |
+
uvicorn[standard]
|
| 4 |
+
pydantic>=2.0.0
|
| 5 |
+
decorator
|
| 6 |
+
scipy
|
| 7 |
+
networkx==2.1
|
| 8 |
+
numpy
|
| 9 |
+
cython
|
GMatch4py/mcp_output/start_mcp.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
MCP Service Startup Entry
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
project_root = os.path.dirname(os.path.abspath(__file__))
|
| 9 |
+
mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
|
| 10 |
+
if mcp_plugin_dir not in sys.path:
|
| 11 |
+
sys.path.insert(0, mcp_plugin_dir)
|
| 12 |
+
|
| 13 |
+
from mcp_service import create_app
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
"""Start FastMCP service"""
|
| 17 |
+
app = create_app()
|
| 18 |
+
# Use environment variable to configure port, default 8000
|
| 19 |
+
port = int(os.environ.get("MCP_PORT", "8000"))
|
| 20 |
+
|
| 21 |
+
# Choose transport mode based on environment variable
|
| 22 |
+
transport = os.environ.get("MCP_TRANSPORT", "stdio")
|
| 23 |
+
if transport == "http":
|
| 24 |
+
app.run(transport="http", host="0.0.0.0", port=port)
|
| 25 |
+
else:
|
| 26 |
+
# Default to STDIO mode
|
| 27 |
+
app.run()
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
main()
|
GMatch4py/mcp_output/workflow_summary.json
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"repository": {
|
| 3 |
+
"name": "GMatch4py",
|
| 4 |
+
"url": "https://github.com/jacquesfize/GMatch4py",
|
| 5 |
+
"local_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/GMatch4py",
|
| 6 |
+
"description": "Python library",
|
| 7 |
+
"features": "Basic functionality",
|
| 8 |
+
"tech_stack": "Python",
|
| 9 |
+
"stars": 0,
|
| 10 |
+
"forks": 0,
|
| 11 |
+
"language": "Python",
|
| 12 |
+
"last_updated": "",
|
| 13 |
+
"complexity": "simple",
|
| 14 |
+
"intrusiveness_risk": "low"
|
| 15 |
+
},
|
| 16 |
+
"execution": {
|
| 17 |
+
"start_time": 1770442744.920794,
|
| 18 |
+
"end_time": 1770442802.9103656,
|
| 19 |
+
"duration": 57.989572286605835,
|
| 20 |
+
"status": "success",
|
| 21 |
+
"workflow_status": "success",
|
| 22 |
+
"nodes_executed": [
|
| 23 |
+
"download",
|
| 24 |
+
"analysis",
|
| 25 |
+
"env",
|
| 26 |
+
"generate",
|
| 27 |
+
"run",
|
| 28 |
+
"review",
|
| 29 |
+
"finalize"
|
| 30 |
+
],
|
| 31 |
+
"total_files_processed": 5,
|
| 32 |
+
"environment_type": "unknown",
|
| 33 |
+
"llm_calls": 0,
|
| 34 |
+
"deepwiki_calls": 0
|
| 35 |
+
},
|
| 36 |
+
"tests": {
|
| 37 |
+
"original_project": {
|
| 38 |
+
"passed": false,
|
| 39 |
+
"details": {},
|
| 40 |
+
"test_coverage": "100%",
|
| 41 |
+
"execution_time": 0,
|
| 42 |
+
"test_files": []
|
| 43 |
+
},
|
| 44 |
+
"mcp_plugin": {
|
| 45 |
+
"passed": true,
|
| 46 |
+
"details": {},
|
| 47 |
+
"service_health": "healthy",
|
| 48 |
+
"startup_time": 0,
|
| 49 |
+
"transport_mode": "stdio",
|
| 50 |
+
"fastmcp_version": "unknown",
|
| 51 |
+
"mcp_version": "unknown"
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"analysis": {
|
| 55 |
+
"structure": {
|
| 56 |
+
"packages": [
|
| 57 |
+
"source.gmatch4py",
|
| 58 |
+
"source.gmatch4py.embedding",
|
| 59 |
+
"source.gmatch4py.ged",
|
| 60 |
+
"source.gmatch4py.helpers",
|
| 61 |
+
"source.gmatch4py.kernels"
|
| 62 |
+
]
|
| 63 |
+
},
|
| 64 |
+
"dependencies": {
|
| 65 |
+
"has_environment_yml": false,
|
| 66 |
+
"has_requirements_txt": true,
|
| 67 |
+
"pyproject": false,
|
| 68 |
+
"setup_cfg": false,
|
| 69 |
+
"setup_py": true
|
| 70 |
+
},
|
| 71 |
+
"entry_points": {
|
| 72 |
+
"imports": [],
|
| 73 |
+
"cli": [],
|
| 74 |
+
"modules": []
|
| 75 |
+
},
|
| 76 |
+
"risk_assessment": {
|
| 77 |
+
"import_feasibility": 0.8,
|
| 78 |
+
"intrusiveness_risk": "low",
|
| 79 |
+
"complexity": "simple"
|
| 80 |
+
},
|
| 81 |
+
"deepwiki_analysis": {
|
| 82 |
+
"repo_url": "https://github.com/jacquesfize/GMatch4py",
|
| 83 |
+
"repo_name": "GMatch4py",
|
| 84 |
+
"content": "jacquesfize/GMatch4py\nA graph matching library for Python\nRepository Not Indexed\nThis repository hasn't been indexed yet. Indexing allows you to explore code structure, find documentation, and understand dependencies.\nIndexing typically takes 2-10 minutes to complete after it starts indexing\nOnce indexed, you'll have full access to code exploration and search functionality",
|
| 85 |
+
"model": "gpt-4o-2024-08-06",
|
| 86 |
+
"source": "selenium",
|
| 87 |
+
"success": true
|
| 88 |
+
},
|
| 89 |
+
"code_complexity": {
|
| 90 |
+
"cyclomatic_complexity": "medium",
|
| 91 |
+
"cognitive_complexity": "medium",
|
| 92 |
+
"maintainability_index": 75
|
| 93 |
+
},
|
| 94 |
+
"security_analysis": {
|
| 95 |
+
"vulnerabilities_found": 0,
|
| 96 |
+
"security_score": 85,
|
| 97 |
+
"recommendations": []
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
"plugin_generation": {
|
| 101 |
+
"files_created": [
|
| 102 |
+
"mcp_output/start_mcp.py",
|
| 103 |
+
"mcp_output/mcp_plugin/__init__.py",
|
| 104 |
+
"mcp_output/mcp_plugin/mcp_service.py",
|
| 105 |
+
"mcp_output/mcp_plugin/adapter.py",
|
| 106 |
+
"mcp_output/mcp_plugin/main.py",
|
| 107 |
+
"mcp_output/requirements.txt",
|
| 108 |
+
"mcp_output/README_MCP.md"
|
| 109 |
+
],
|
| 110 |
+
"main_entry": "start_mcp.py",
|
| 111 |
+
"requirements": [
|
| 112 |
+
"fastmcp>=0.1.0",
|
| 113 |
+
"pydantic>=2.0.0"
|
| 114 |
+
],
|
| 115 |
+
"readme_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/GMatch4py/mcp_output/README_MCP.md",
|
| 116 |
+
"adapter_mode": "import",
|
| 117 |
+
"total_lines_of_code": 0,
|
| 118 |
+
"generated_files_size": 0,
|
| 119 |
+
"tool_endpoints": 0,
|
| 120 |
+
"supported_features": [
|
| 121 |
+
"Basic functionality"
|
| 122 |
+
],
|
| 123 |
+
"generated_tools": [
|
| 124 |
+
"Basic tools",
|
| 125 |
+
"Health check tools",
|
| 126 |
+
"Version info tools"
|
| 127 |
+
]
|
| 128 |
+
},
|
| 129 |
+
"code_review": {},
|
| 130 |
+
"errors": [],
|
| 131 |
+
"warnings": [],
|
| 132 |
+
"recommendations": [
|
| 133 |
+
"Improve test coverage by adding more unit tests",
|
| 134 |
+
"Ensure all existing tests are passing and fix any failing tests",
|
| 135 |
+
"Consider indexing the repository to enhance code exploration and search functionality",
|
| 136 |
+
"Update the README file to include detailed setup and usage instructions",
|
| 137 |
+
"Verify and update dependencies in requirements.txt to ensure compatibility",
|
| 138 |
+
"Consider adding a pyproject.toml or setup.cfg for better dependency management",
|
| 139 |
+
"Review and optimize the plugin code for performance improvements",
|
| 140 |
+
"Conduct a code review to identify potential code quality issues",
|
| 141 |
+
"Implement continuous integration to automate testing and deployment",
|
| 142 |
+
"Document the core modules and functions for better maintainability."
|
| 143 |
+
],
|
| 144 |
+
"performance_metrics": {
|
| 145 |
+
"memory_usage_mb": 0,
|
| 146 |
+
"cpu_usage_percent": 0,
|
| 147 |
+
"response_time_ms": 0,
|
| 148 |
+
"throughput_requests_per_second": 0
|
| 149 |
+
},
|
| 150 |
+
"deployment_info": {
|
| 151 |
+
"supported_platforms": [
|
| 152 |
+
"Linux",
|
| 153 |
+
"Windows",
|
| 154 |
+
"macOS"
|
| 155 |
+
],
|
| 156 |
+
"python_versions": [
|
| 157 |
+
"3.8",
|
| 158 |
+
"3.9",
|
| 159 |
+
"3.10",
|
| 160 |
+
"3.11",
|
| 161 |
+
"3.12"
|
| 162 |
+
],
|
| 163 |
+
"deployment_methods": [
|
| 164 |
+
"Docker",
|
| 165 |
+
"pip",
|
| 166 |
+
"conda"
|
| 167 |
+
],
|
| 168 |
+
"monitoring_support": true,
|
| 169 |
+
"logging_configuration": "structured"
|
| 170 |
+
},
|
| 171 |
+
"execution_analysis": {
|
| 172 |
+
"success_factors": [
|
| 173 |
+
"Successful execution of all workflow nodes",
|
| 174 |
+
"Healthy service status of the MCP plugin"
|
| 175 |
+
],
|
| 176 |
+
"failure_reasons": [],
|
| 177 |
+
"overall_assessment": "good",
|
| 178 |
+
"node_performance": {
|
| 179 |
+
"download_time": "Efficient download with no delays",
|
| 180 |
+
"analysis_time": "Completed within expected time frame",
|
| 181 |
+
"generation_time": "Code generation was swift and successful",
|
| 182 |
+
"test_time": "Testing was limited due to lack of original project test success"
|
| 183 |
+
},
|
| 184 |
+
"resource_usage": {
|
| 185 |
+
"memory_efficiency": "Memory usage data not available",
|
| 186 |
+
"cpu_efficiency": "CPU usage data not available",
|
| 187 |
+
"disk_usage": "Disk usage was minimal due to small project size"
|
| 188 |
+
}
|
| 189 |
+
},
|
| 190 |
+
"technical_quality": {
|
| 191 |
+
"code_quality_score": 75,
|
| 192 |
+
"architecture_score": 70,
|
| 193 |
+
"performance_score": 65,
|
| 194 |
+
"maintainability_score": 75,
|
| 195 |
+
"security_score": 85,
|
| 196 |
+
"scalability_score": 70
|
| 197 |
+
}
|
| 198 |
+
}
|
GMatch4py/source/.travis.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
language: python
|
| 2 |
+
python:
|
| 3 |
+
- "3.6"
|
| 4 |
+
|
| 5 |
+
notifications:
|
| 6 |
+
email: false
|
| 7 |
+
|
| 8 |
+
install:
|
| 9 |
+
- pip install cython numpy networkx scipy scikit-learn pandas gensim joblib gensim psutil --upgrade
|
| 10 |
+
- pip install .
|
| 11 |
+
|
| 12 |
+
script:
|
| 13 |
+
- echo "1"
|
| 14 |
+
|
GMatch4py/source/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2018 Jacques Fize
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
GMatch4py/source/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+

|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
[](https://travis-ci.com/Jacobe2169/GMatch4py)
|
| 5 |
+
# GMatch4py a graph matching library for Python
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
GMatch4py is a library dedicated to graph matching. Graph structure are stored in NetworkX graph objects.
|
| 9 |
+
GMatch4py algorithms were implemented with Cython to enhance performance.
|
| 10 |
+
|
| 11 |
+
## Requirements
|
| 12 |
+
|
| 13 |
+
* Python 3
|
| 14 |
+
* Numpy and Cython installed (if not : `(sudo) pip(3) install numpy cython`)
|
| 15 |
+
|
| 16 |
+
## Installation
|
| 17 |
+
|
| 18 |
+
To install `GMatch4py`, run the following commands:
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
git clone https://github.com/Jacobe2169/GMatch4py.git
|
| 22 |
+
cd GMatch4py
|
| 23 |
+
(sudo) pip(3) install .
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
## Get Started
|
| 27 |
+
### Graph input format
|
| 28 |
+
|
| 29 |
+
In `GMatch4py`, algorithms manipulate `networkx.Graph`, a complete graph model that
|
| 30 |
+
comes with a large spectrum of parser to load your graph from various inputs : `*.graphml,*.gexf,..` (check [here](https://networkx.github.io/documentation/stable/reference/readwrite/index.html) to see all the format accepted)
|
| 31 |
+
|
| 32 |
+
### Use GMatch4py
|
| 33 |
+
If you want to use algorithms like *graph edit distances*, here is an example:
|
| 34 |
+
|
| 35 |
+
```python
|
| 36 |
+
# Gmatch4py use networkx graph
|
| 37 |
+
import networkx as nx
|
| 38 |
+
# import the GED using the munkres algorithm
|
| 39 |
+
import gmatch4py as gm
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
In this example, we use generated graphs using `networkx` helpers:
|
| 43 |
+
```python
|
| 44 |
+
g1=nx.complete_bipartite_graph(5,4)
|
| 45 |
+
g2=nx.complete_bipartite_graph(6,4)
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
All graph matching algorithms in `Gmatch4py` work this way:
|
| 49 |
+
* Each algorithm is associated with an object, each object having its specific parameters. In this case, the parameters are the edit costs (delete a vertex, add a vertex, ...)
|
| 50 |
+
* Each object is associated with a `compare()` function with two parameters. First parameter is **a list of the graphs** you want to **compare**, i.e. measure the distance/similarity (depends on the algorithm). Then, you can specify a sample of graphs to be compared to all the other graphs. To this end, the second parameter should be **a list containing the indices** of these graphs (based on the first parameter list). If you rather compute the distance/similarity **between all graphs**, just use the `None` value.
|
| 51 |
+
|
| 52 |
+
```python
|
| 53 |
+
ged=gm.GraphEditDistance(1,1,1,1) # all edit costs are equal to 1
|
| 54 |
+
result=ged.compare([g1,g2],None)
|
| 55 |
+
print(result)
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
The output is a similarity/distance matrix :
|
| 59 |
+
```python
|
| 60 |
+
array([[0., 14.],
|
| 61 |
+
[10., 0.]])
|
| 62 |
+
```
|
| 63 |
+
This output result is "raw", if you wish to have normalized results in terms of distance (or similarity) you can use :
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
ged.similarity(result)
|
| 67 |
+
# or
|
| 68 |
+
ged.distance(result)
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## Exploit nodes and edges attributes
|
| 72 |
+
|
| 73 |
+
In this latest version, we add the possibility to exploit graph attributes ! To do so, the `base.Base` is extended with the `set_attr_graph_used(node_attr,edge_attr)` method.
|
| 74 |
+
|
| 75 |
+
```python
|
| 76 |
+
import networkx as nx
|
| 77 |
+
import gmatch4py as gm
|
| 78 |
+
ged = gm.GraphEditDistance(1,1,1,1)
|
| 79 |
+
ged.set_attr_graph_used("theme","color") # Edge colors and node themes attributes will be used.
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
## List of algorithms
|
| 83 |
+
|
| 84 |
+
* Graph Embedding
|
| 85 |
+
* Graph2Vec [1]
|
| 86 |
+
* Node Embedding
|
| 87 |
+
* DeepWalk [7]
|
| 88 |
+
* Node2vec [8]
|
| 89 |
+
* Graph kernels
|
| 90 |
+
* Random Walk Kernel (*debug needed*) [3]
|
| 91 |
+
* Geometrical
|
| 92 |
+
* K-Step
|
| 93 |
+
* Shortest Path Kernel [3]
|
| 94 |
+
* Weisfeiler-Lehman Kernel [4]
|
| 95 |
+
* Subtree Kernel
|
| 96 |
+
* Graph Edit Distance [5]
|
| 97 |
+
* Approximated Graph Edit Distance
|
| 98 |
+
* Hausdorff Graph Edit Distance
|
| 99 |
+
* Bipartite Graph Edit Distance
|
| 100 |
+
* Greedy Edit Distance
|
| 101 |
+
* Vertex Ranking [2]
|
| 102 |
+
* Vertex Edge Overlap [2]
|
| 103 |
+
* Bag of Nodes (a bag of words model using nodes as vocabulary)
|
| 104 |
+
* Bag of Cliques (a bag of words model using cliques as vocabulary)
|
| 105 |
+
* MCS [6]
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
## Publications associated
|
| 109 |
+
|
| 110 |
+
* [1] Narayanan, Annamalai and Chandramohan, Mahinthan and Venkatesan, Rajasekar and Chen, Lihui and Liu, Yang. Graph2vec: Learning distributed representations of graphs. MLG 2017, 13th International Workshop on Mining and Learning with Graphs (MLGWorkshop 2017).
|
| 111 |
+
* [2] Papadimitriou, P., Dasdan, A., & Garcia-Molina, H. (2010). Web graph similarity for anomaly detection. Journal of Internet Services and Applications, 1(1), 19-30.
|
| 112 |
+
* [3] Vishwanathan, S. V. N., Schraudolph, N. N., Kondor, R., & Borgwardt, K. M. (2010). Graph kernels. Journal of Machine Learning Research, 11(Apr), 1201-1242.
|
| 113 |
+
* [4] Shervashidze, N., Schweitzer, P., Leeuwen, E. J. V., Mehlhorn, K., & Borgwardt, K. M. (2011). Weisfeiler-lehman graph kernels. Journal of Machine Learning Research, 12(Sep), 2539-2561.
|
| 114 |
+
* [5] Fischer, A., Riesen, K., & Bunke, H. (2017). Improved quadratic time approximation of graph edit distance by combining Hausdorff matching and greedy assignment. Pattern Recognition Letters, 87, 55-62.
|
| 115 |
+
* [6] A graph distance metric based on the maximal common subgraph, H. Bunke and K. Shearer, Pattern Recognition Letters, 1998
|
| 116 |
+
* [7] Perozzi, B., Al-Rfou, R., & Skiena, S. (2014, August). Deepwalk: Online learning of social representations. In Proceedings of the 20th ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 701-710). ACM.
|
| 117 |
+
* [8] node2vec: Scalable Feature Learning for Networks. Aditya Grover and Jure Leskovec. Knowledge Discovery and Data Mining, 2016.
|
| 118 |
+
|
| 119 |
+
## Author(s)
|
| 120 |
+
|
| 121 |
+
Jacques Fize, *jacques[dot]fize[at]cirad[dot]fr*
|
| 122 |
+
|
| 123 |
+
Some algorithms from other projects were integrated to Gmatch4py. **Be assured that
|
| 124 |
+
each code is associated with a reference to the original.**
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
## CHANGELOG
|
| 128 |
+
|
| 129 |
+
### 18.06.2022
|
| 130 |
+
* Debug the `skipgram` import
|
| 131 |
+
* Gmatch4py should work with new gensim version
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
### 7.05.2019
|
| 135 |
+
|
| 136 |
+
* Debug (problems with float edge weight)
|
| 137 |
+
* Add the `AbstractEditDistance.edit_path(G,H)` method that return the edit path, the cost matrix and the selected cost index in the cost matrix
|
| 138 |
+
* Add a tqdm progress bar for the `gmatch4py.helpers.reader.import_dir()` function
|
| 139 |
+
|
| 140 |
+
### 12.03.2019
|
| 141 |
+
|
| 142 |
+
* Add Node2vec
|
| 143 |
+
|
| 144 |
+
### 05.03.2019
|
| 145 |
+
|
| 146 |
+
* Add Graph Embedding algorithms
|
| 147 |
+
* Remove depreciated methods and classes
|
| 148 |
+
* Add logo
|
| 149 |
+
* Update documentation
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
### 25.02.2019
|
| 153 |
+
* Add New Graph Class. Features : Cython Extensions, precomputed values (degrees, neighbor info), hash representation of edges and nodes for a faster comparison
|
| 154 |
+
* Some algorithms are parallelized such as graph edit distances or Jaccard
|
| 155 |
+
|
| 156 |
+
## TODO List
|
| 157 |
+
|
| 158 |
+
* Debug algorithms --> Random Walk Kernel, Deltacon
|
| 159 |
+
* Optimize algorithms --> Vertex Ranking
|
| 160 |
+
|
GMatch4py/source/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
GMatch4py Project Package Initialization File
|
| 4 |
+
"""
|
GMatch4py/source/gmatch4py/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
# Graph Edit Distance algorithms import
|
| 4 |
+
from .ged.graph_edit_dist import *
|
| 5 |
+
from .ged.greedy_edit_distance import *
|
| 6 |
+
from .ged.bipartite_graph_matching_2 import *
|
| 7 |
+
from .ged.hausdorff_edit_distance import *
|
| 8 |
+
|
| 9 |
+
# Kernels algorithms import
|
| 10 |
+
from .kernels.weisfeiler_lehman import *
|
| 11 |
+
from .kernels.shortest_path_kernel import *
|
| 12 |
+
|
| 13 |
+
# Graph Embedding import
|
| 14 |
+
from .embedding.graph2vec import *
|
| 15 |
+
from .embedding.deepwalk import *
|
| 16 |
+
from .embedding.node2vec import *
|
| 17 |
+
# Helpers import
|
| 18 |
+
from .helpers.reader import *
|
| 19 |
+
from .helpers.general import *
|
| 20 |
+
|
| 21 |
+
# Basic algorithms import
|
| 22 |
+
from .bag_of_cliques import *
|
| 23 |
+
from .mcs import *
|
| 24 |
+
from .vertex_edge_overlap import *
|
| 25 |
+
from .vertex_ranking import *
|
| 26 |
+
from .jaccard import *
|
| 27 |
+
from .bon import *
|
GMatch4py/source/gmatch4py/bag_of_cliques.pyx
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
import copy
|
| 4 |
+
from typing import Sequence
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
import numpy as np
|
| 8 |
+
cimport numpy as np
|
| 9 |
+
from scipy.sparse import csr_matrix,lil_matrix
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
from .base cimport Base
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
cdef class BagOfCliques(Base):
|
| 16 |
+
"""
|
| 17 |
+
The Bag of Cliques is representation of a graph corpus using the well-known *bag of words* model. Here, instead of
|
| 18 |
+
word, we use unique cliques found in the graphs as a vocabulary. A clique is a highly connected graph where all the vertices are connected by an edge.
|
| 19 |
+
|
| 20 |
+
The resulting representation is then use to compute similarity value between graphs. For this purpose, we use the cosine
|
| 21 |
+
similarity.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self):
|
| 25 |
+
"""
|
| 26 |
+
Constructor of Bag Of Cliques.
|
| 27 |
+
"""
|
| 28 |
+
Base.__init__(self,0,True)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 32 |
+
b=BagOfCliques()
|
| 33 |
+
bog=b.get_bag_of_cliques(listgs).astype(np.float32)
|
| 34 |
+
cdef int n=bog.shape[0]
|
| 35 |
+
cdef np.ndarray scores = np.zeros((n,n))
|
| 36 |
+
cdef int i
|
| 37 |
+
for i in range(len(scores)):
|
| 38 |
+
if selected:
|
| 39 |
+
if not i in selected:
|
| 40 |
+
continue
|
| 41 |
+
bog_i=bog[i]
|
| 42 |
+
for j in range(i,len(scores)):
|
| 43 |
+
bog_j=bog[j]
|
| 44 |
+
scores[i,j]=(np.dot(bog_i,bog_j.T))/(np.sqrt(np.sum(bog_i**2))*np.sqrt(np.sum(bog_j**2))) # Can be computed in one line
|
| 45 |
+
scores[j,i]=scores[i,j]
|
| 46 |
+
return scores
|
| 47 |
+
|
| 48 |
+
def get_unique_cliques(self, graphs):
|
| 49 |
+
"""
|
| 50 |
+
Return a cliques found in a set of graphs
|
| 51 |
+
Parameters
|
| 52 |
+
----------
|
| 53 |
+
graphs: networkx.Graph array
|
| 54 |
+
list of graphs
|
| 55 |
+
|
| 56 |
+
Returns
|
| 57 |
+
-------
|
| 58 |
+
list
|
| 59 |
+
Cliques set
|
| 60 |
+
"""
|
| 61 |
+
t = {}
|
| 62 |
+
c_ = 0
|
| 63 |
+
cdef list clique_vocab = []
|
| 64 |
+
cdef list cli_temp
|
| 65 |
+
cdef list cliques
|
| 66 |
+
cdef int len_graphs=len(graphs)
|
| 67 |
+
cdef int km= -1
|
| 68 |
+
for g in graphs:
|
| 69 |
+
km+=1
|
| 70 |
+
if not g:
|
| 71 |
+
continue
|
| 72 |
+
cliques = list(nx.find_cliques(nx.Graph(g)))
|
| 73 |
+
for clique in cliques:
|
| 74 |
+
cli_temp = copy.deepcopy(clique)
|
| 75 |
+
new_clique = False
|
| 76 |
+
for i in range(len(clique)):
|
| 77 |
+
flag = False
|
| 78 |
+
v = None # vertex deleted
|
| 79 |
+
for vertex in cli_temp:
|
| 80 |
+
if vertex in t:
|
| 81 |
+
v = vertex
|
| 82 |
+
flag = True
|
| 83 |
+
|
| 84 |
+
if not flag in t:
|
| 85 |
+
v = cli_temp[0]
|
| 86 |
+
t[v] = {}
|
| 87 |
+
new_clique = True
|
| 88 |
+
t = t[v]
|
| 89 |
+
cli_temp.remove(v)
|
| 90 |
+
|
| 91 |
+
if new_clique:
|
| 92 |
+
c_ += 1
|
| 93 |
+
clique_vocab.append(clique)
|
| 94 |
+
return clique_vocab
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def clique2str(self,cliques):
|
| 98 |
+
"""
|
| 99 |
+
Return a "hash" string of a clique
|
| 100 |
+
|
| 101 |
+
Parameters
|
| 102 |
+
----------
|
| 103 |
+
cliques: array
|
| 104 |
+
|
| 105 |
+
Returns
|
| 106 |
+
-------
|
| 107 |
+
str
|
| 108 |
+
hash of a clique
|
| 109 |
+
"""
|
| 110 |
+
try:
|
| 111 |
+
return "".join(sorted(cliques))
|
| 112 |
+
except:
|
| 113 |
+
return "".join(sorted(list(map(str,cliques))))
|
| 114 |
+
|
| 115 |
+
def transform_clique_vocab(self,clique_vocab):
|
| 116 |
+
"""
|
| 117 |
+
Transform cliques found in `get_unique_cliques()` in a proper format to build the "Bag of Cliques"
|
| 118 |
+
|
| 119 |
+
Parameters
|
| 120 |
+
----------
|
| 121 |
+
clique_vocab : array
|
| 122 |
+
contains cliques
|
| 123 |
+
Returns
|
| 124 |
+
-------
|
| 125 |
+
dict
|
| 126 |
+
new clique vocab format
|
| 127 |
+
"""
|
| 128 |
+
cdef dict new_vocab={}
|
| 129 |
+
cdef int len_voc=len(clique_vocab)
|
| 130 |
+
for c in range(len_voc):
|
| 131 |
+
#print(c)
|
| 132 |
+
new_vocab[self.clique2str(clique_vocab[c])]=c
|
| 133 |
+
return new_vocab
|
| 134 |
+
|
| 135 |
+
def get_bag_of_cliques(self, graphs):
|
| 136 |
+
"""
|
| 137 |
+
Return a the Bag of Cliques representation from a graph set.
|
| 138 |
+
|
| 139 |
+
Parameters
|
| 140 |
+
----------
|
| 141 |
+
graphs : networkx.Graph array
|
| 142 |
+
list of graphs
|
| 143 |
+
|
| 144 |
+
Returns
|
| 145 |
+
-------
|
| 146 |
+
np.ndarray
|
| 147 |
+
bag of cliques
|
| 148 |
+
"""
|
| 149 |
+
cdef list clique_vocab=self.get_unique_cliques(graphs)
|
| 150 |
+
cdef dict map_str_cliques=self.transform_clique_vocab(clique_vocab)
|
| 151 |
+
cdef int l_v=len(clique_vocab)
|
| 152 |
+
boc = np.zeros((len(graphs), l_v))
|
| 153 |
+
cdef np.ndarray vector
|
| 154 |
+
cdef list cliques
|
| 155 |
+
cdef str hash
|
| 156 |
+
|
| 157 |
+
for g in range(len(graphs)):
|
| 158 |
+
#sys.stdout.write("\r{0}/{1}".format(g,len(graphs)))
|
| 159 |
+
gr = graphs[g]
|
| 160 |
+
vector = np.zeros(l_v)
|
| 161 |
+
cliques = list(nx.find_cliques(nx.Graph(gr)))
|
| 162 |
+
for clique in cliques:
|
| 163 |
+
hash=self.clique2str(clique)
|
| 164 |
+
if hash in map_str_cliques:
|
| 165 |
+
vector[map_str_cliques[hash]] = 1
|
| 166 |
+
boc[g] = vector
|
| 167 |
+
return boc
|
GMatch4py/source/gmatch4py/base.pxd
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cimport numpy as np
|
| 2 |
+
|
| 3 |
+
cdef class Base:
|
| 4 |
+
## Attribute(s)
|
| 5 |
+
cdef int type_alg
|
| 6 |
+
cdef bint normalized
|
| 7 |
+
cdef int cpu_count
|
| 8 |
+
cdef str node_attr_key
|
| 9 |
+
cdef str edge_attr_key
|
| 10 |
+
## Methods
|
| 11 |
+
cpdef np.ndarray compare(self,list graph_list, list selected)
|
| 12 |
+
cpdef np.ndarray compare_old(self,list listgs, list selected)
|
| 13 |
+
cpdef np.ndarray distance(self, np.ndarray matrix)
|
| 14 |
+
cpdef np.ndarray similarity(self, np.ndarray matrix)
|
| 15 |
+
cpdef bint isAccepted(self,G,index,selected)
|
| 16 |
+
cpdef np.ndarray get_selected_array(self,selected,size_corpus)
|
| 17 |
+
|
| 18 |
+
cpdef set_attr_graph_used(self, str node_attr_key, str edge_attr_key)
|
| 19 |
+
|
GMatch4py/source/gmatch4py/base.pyx
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
cimport numpy as np
|
| 5 |
+
import networkx as nx
|
| 6 |
+
cimport cython
|
| 7 |
+
import multiprocessing
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
cpdef np.ndarray minmax_scale(np.ndarray matrix):
|
| 12 |
+
"""
|
| 13 |
+
Optimize so it can works with Cython
|
| 14 |
+
:param matrix:
|
| 15 |
+
:return:
|
| 16 |
+
"""
|
| 17 |
+
cdef double min_,max_
|
| 18 |
+
cdef np.ndarray x
|
| 19 |
+
x=np.ma.masked_invalid(matrix)
|
| 20 |
+
max_=np.max(x)
|
| 21 |
+
return x/(max_)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
cdef class Base:
|
| 25 |
+
"""
|
| 26 |
+
This class define the common methods to all Graph Matching algorithm.
|
| 27 |
+
|
| 28 |
+
Attributes
|
| 29 |
+
----------
|
| 30 |
+
type_alg : int
|
| 31 |
+
Indicate the type of measure returned by the algorithm :
|
| 32 |
+
|
| 33 |
+
* 0 : similarity
|
| 34 |
+
* 1 : distance
|
| 35 |
+
normalized : bool
|
| 36 |
+
Indicate if the algorithm return normalized results (between 0 and 1)
|
| 37 |
+
|
| 38 |
+
"""
|
| 39 |
+
def __cinit__(self):
|
| 40 |
+
self.type_alg=0
|
| 41 |
+
self.normalized=False
|
| 42 |
+
|
| 43 |
+
def __init__(self,type_alg,normalized,node_attr_key="",edge_attr_key=""):
|
| 44 |
+
"""
|
| 45 |
+
Constructor of Base
|
| 46 |
+
|
| 47 |
+
Parameters
|
| 48 |
+
----------
|
| 49 |
+
type_alg : int
|
| 50 |
+
Indicate the type of measure returned by the algorithm :
|
| 51 |
+
|
| 52 |
+
* **0** : similarity
|
| 53 |
+
* **1** : distance
|
| 54 |
+
normalized : bool
|
| 55 |
+
Indicate if the algorithm return normalized results (between 0 and 1)
|
| 56 |
+
"""
|
| 57 |
+
if type_alg <0:
|
| 58 |
+
self.type_alg=0
|
| 59 |
+
elif type_alg >1 :
|
| 60 |
+
self.type_alg=1
|
| 61 |
+
else:
|
| 62 |
+
self.type_alg=type_alg
|
| 63 |
+
self.normalized=normalized
|
| 64 |
+
self.cpu_count=multiprocessing.cpu_count()
|
| 65 |
+
self.node_attr_key=node_attr_key
|
| 66 |
+
self.edge_attr_key=edge_attr_key
|
| 67 |
+
|
| 68 |
+
cpdef set_attr_graph_used(self, str node_attr_key, str edge_attr_key):
|
| 69 |
+
"""
|
| 70 |
+
Set graph attribute used by the algorithm to compare graphs.
|
| 71 |
+
Parameters
|
| 72 |
+
----------
|
| 73 |
+
node_attr_key : str
|
| 74 |
+
key of the node attribute
|
| 75 |
+
edge_attr_key: str
|
| 76 |
+
key of the edge attribute
|
| 77 |
+
|
| 78 |
+
"""
|
| 79 |
+
self.node_attr_key=node_attr_key
|
| 80 |
+
self.edge_attr_key=edge_attr_key
|
| 81 |
+
|
| 82 |
+
cpdef np.ndarray get_selected_array(self,selected,size_corpus):
|
| 83 |
+
"""
|
| 84 |
+
Return an array which define which graph will be compared in the algorithms.
|
| 85 |
+
Parameters
|
| 86 |
+
----------
|
| 87 |
+
selected : list
|
| 88 |
+
indices of graphs you wish to compare
|
| 89 |
+
size_corpus :
|
| 90 |
+
size of your dataset
|
| 91 |
+
|
| 92 |
+
Returns
|
| 93 |
+
-------
|
| 94 |
+
np.ndarray
|
| 95 |
+
selected vector (1 -> selected, 0 -> not selected)
|
| 96 |
+
"""
|
| 97 |
+
cdef double[:] selected_test = np.zeros(size_corpus)
|
| 98 |
+
if not selected == None:
|
| 99 |
+
for ix in range(len(selected)):
|
| 100 |
+
selected_test[selected[ix]]=1
|
| 101 |
+
return np.array(selected_test)
|
| 102 |
+
else:
|
| 103 |
+
return np.array(selected_test)+1
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
cpdef np.ndarray compare_old(self,list listgs, list selected):
|
| 107 |
+
"""
|
| 108 |
+
Soon will be depreciated ! To store the old version of an algorithm.
|
| 109 |
+
Parameters
|
| 110 |
+
----------
|
| 111 |
+
listgs : list
|
| 112 |
+
list of graphs
|
| 113 |
+
selected
|
| 114 |
+
selected graphs
|
| 115 |
+
|
| 116 |
+
Returns
|
| 117 |
+
-------
|
| 118 |
+
np.ndarray
|
| 119 |
+
distance/similarity matrix
|
| 120 |
+
"""
|
| 121 |
+
pass
|
| 122 |
+
|
| 123 |
+
@cython.boundscheck(False)
|
| 124 |
+
cpdef np.ndarray compare(self,list graph_list, list selected):
|
| 125 |
+
"""
|
| 126 |
+
Return the similarity/distance matrix using the current algorithm.
|
| 127 |
+
|
| 128 |
+
>>>Base.compare([nx.Graph(),nx.Graph()],None)
|
| 129 |
+
>>>Base.compare([nx.Graph(),nx.Graph()],[0,1])
|
| 130 |
+
|
| 131 |
+
Parameters
|
| 132 |
+
----------
|
| 133 |
+
graph_list : networkx.Graph array
|
| 134 |
+
Contains the graphs to compare
|
| 135 |
+
selected : int array
|
| 136 |
+
Sometimes, you only wants to compute similarity of some graphs to every graphs. If so, indicate their indices in
|
| 137 |
+
`graph_list`, else, put the None value.
|
| 138 |
+
the None value
|
| 139 |
+
Returns
|
| 140 |
+
-------
|
| 141 |
+
np.ndarray
|
| 142 |
+
distance/similarity matrix
|
| 143 |
+
|
| 144 |
+
"""
|
| 145 |
+
pass
|
| 146 |
+
|
| 147 |
+
cpdef np.ndarray distance(self, np.ndarray matrix):
|
| 148 |
+
"""
|
| 149 |
+
Return a normalized distance matrix
|
| 150 |
+
Parameters
|
| 151 |
+
----------
|
| 152 |
+
matrix : np.ndarray
|
| 153 |
+
Similarity/distance matrix you wish to transform
|
| 154 |
+
|
| 155 |
+
Returns
|
| 156 |
+
-------
|
| 157 |
+
np.ndarray
|
| 158 |
+
distance matrix
|
| 159 |
+
"""
|
| 160 |
+
if self.type_alg == 1:
|
| 161 |
+
if not self.normalized:
|
| 162 |
+
matrix=np.ma.getdata(minmax_scale(matrix))
|
| 163 |
+
return matrix
|
| 164 |
+
else:
|
| 165 |
+
if not self.normalized:
|
| 166 |
+
matrix=np.ma.getdata(minmax_scale(matrix))
|
| 167 |
+
return 1-matrix
|
| 168 |
+
|
| 169 |
+
cpdef np.ndarray similarity(self, np.ndarray matrix):
|
| 170 |
+
"""
|
| 171 |
+
Return a normalized similarity matrix
|
| 172 |
+
Parameters
|
| 173 |
+
----------
|
| 174 |
+
matrix : np.ndarray
|
| 175 |
+
Similarity/distance matrix you wish to transform
|
| 176 |
+
|
| 177 |
+
Returns
|
| 178 |
+
-------
|
| 179 |
+
np.array
|
| 180 |
+
similarity matrix
|
| 181 |
+
"""
|
| 182 |
+
if self.type_alg == 0:
|
| 183 |
+
return matrix
|
| 184 |
+
else:
|
| 185 |
+
if not self.normalized:
|
| 186 |
+
matrix=np.ma.getdata(minmax_scale(matrix))
|
| 187 |
+
return 1-matrix
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
cpdef bint isAccepted(self,G,index,selected):
|
| 191 |
+
"""
|
| 192 |
+
Indicate if the graph will be compared to the other. A graph is "accepted" if :
|
| 193 |
+
* G exists(!= None) and not empty (|vertices(G)| >0)
|
| 194 |
+
* If selected graph to compare were indicated, check if G exists in selected
|
| 195 |
+
|
| 196 |
+
Parameters
|
| 197 |
+
----------
|
| 198 |
+
G : networkx.Graph
|
| 199 |
+
Graph
|
| 200 |
+
index : int
|
| 201 |
+
index in the graph list parameter in `Base.compare()`
|
| 202 |
+
selected : int array
|
| 203 |
+
`selected` parameter value in `Base.compare()`
|
| 204 |
+
|
| 205 |
+
Returns
|
| 206 |
+
-------
|
| 207 |
+
bool :
|
| 208 |
+
if is accepted
|
| 209 |
+
"""
|
| 210 |
+
f=True
|
| 211 |
+
if not G:
|
| 212 |
+
f=False
|
| 213 |
+
elif len(G)== 0:
|
| 214 |
+
f=False
|
| 215 |
+
if selected:
|
| 216 |
+
if not index in selected:
|
| 217 |
+
f=False
|
| 218 |
+
return f
|
GMatch4py/source/gmatch4py/bon.pyx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
import numpy as np
|
| 5 |
+
cimport numpy as np
|
| 6 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 7 |
+
from .base cimport Base
|
| 8 |
+
|
| 9 |
+
cdef class BagOfNodes(Base):
|
| 10 |
+
"""
|
| 11 |
+
We could call this algorithm Bag of nodes
|
| 12 |
+
"""
|
| 13 |
+
def __init__(self):
|
| 14 |
+
Base.__init__(self,0,True)
|
| 15 |
+
|
| 16 |
+
cpdef np.ndarray compare(self,list graph_list, list selected):
|
| 17 |
+
nodes = list()
|
| 18 |
+
for g in graph_list:
|
| 19 |
+
nodes.extend(list(g.nodes()))
|
| 20 |
+
|
| 21 |
+
vocabulary = list(set(nodes))
|
| 22 |
+
hash_voc = {}
|
| 23 |
+
i = 0
|
| 24 |
+
for se in vocabulary:
|
| 25 |
+
hash_voc[se] = i
|
| 26 |
+
i += 1
|
| 27 |
+
n, m = len(graph_list), len(hash_voc)
|
| 28 |
+
bow_matrix = np.zeros((n, m))
|
| 29 |
+
i = 0
|
| 30 |
+
for g in range(len(graph_list)):
|
| 31 |
+
graph = graph_list[g]
|
| 32 |
+
nodes = list(graph.nodes())
|
| 33 |
+
for nod in nodes:
|
| 34 |
+
j = hash_voc[nod]
|
| 35 |
+
bow_matrix[i, j] = 1
|
| 36 |
+
i += 1
|
| 37 |
+
|
| 38 |
+
sim_matrix = cosine_similarity(bow_matrix)
|
| 39 |
+
np.fill_diagonal(sim_matrix, 1)
|
| 40 |
+
return sim_matrix
|
GMatch4py/source/gmatch4py/embedding/__init__.py
ADDED
|
File without changes
|
GMatch4py/source/gmatch4py/embedding/deepwalk.pyx
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#! /usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
import random
|
| 7 |
+
|
| 8 |
+
from io import open
|
| 9 |
+
from argparse import ArgumentParser, FileType, ArgumentDefaultsHelpFormatter
|
| 10 |
+
from collections import Counter
|
| 11 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 12 |
+
import logging
|
| 13 |
+
from multiprocessing import cpu_count
|
| 14 |
+
|
| 15 |
+
import networkx as nx
|
| 16 |
+
import numpy as np
|
| 17 |
+
cimport numpy as np
|
| 18 |
+
from six import text_type as unicode
|
| 19 |
+
from six import iteritems
|
| 20 |
+
from six.moves import range
|
| 21 |
+
|
| 22 |
+
from gensim.models import Word2Vec
|
| 23 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 24 |
+
from joblib import Parallel, delayed
|
| 25 |
+
import psutil
|
| 26 |
+
|
| 27 |
+
cimport cython
|
| 28 |
+
from ..base cimport Base
|
| 29 |
+
import gmatch4py.embedding.graph as graph2
|
| 30 |
+
import gmatch4py.embedding.walks as serialized_walks
|
| 31 |
+
from .skipgram import Skipgram
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
p = psutil.Process(os.getpid())
|
| 35 |
+
try:
|
| 36 |
+
p.set_cpu_affinity(list(range(cpu_count())))
|
| 37 |
+
except AttributeError:
|
| 38 |
+
try:
|
| 39 |
+
p.cpu_affinity(list(range(cpu_count())))
|
| 40 |
+
except AttributeError:
|
| 41 |
+
pass
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def process(gr, number_walks = 10, walk_length = 40, window_size = 5, vertex_freq_degree = False, workers = 1, representation_size = 64, max_memory_data_size = 1000000000, seed = 0):
|
| 45 |
+
"""
|
| 46 |
+
Return a DeepWalk embedding for a graph
|
| 47 |
+
|
| 48 |
+
Parameters
|
| 49 |
+
----------
|
| 50 |
+
gr : nx.Graph
|
| 51 |
+
graph
|
| 52 |
+
number_walks : int, optional
|
| 53 |
+
Number of walk (the default is 10)
|
| 54 |
+
walk_length : int, optional
|
| 55 |
+
Length of the random walk started at each node (the default is 40)
|
| 56 |
+
window_size : int, optional
|
| 57 |
+
Window size of skipgram model. (the default is 5)
|
| 58 |
+
vertex_freq_degree : bool, optional
|
| 59 |
+
Use vertex degree to estimate the frequency of nodes (the default is False)
|
| 60 |
+
workers : int, optional
|
| 61 |
+
Number of parallel processes (the default is 1)
|
| 62 |
+
representation_size : int, optional
|
| 63 |
+
Number of latent dimensions to learn for each node (the default is 64)
|
| 64 |
+
max_memory_data_size : int, optional
|
| 65 |
+
'Size to start dumping walks to disk, instead of keeping them in memory. (the default is 1000000000)
|
| 66 |
+
seed : int, optional
|
| 67 |
+
Seed for random walk generator (the default is 0)
|
| 68 |
+
|
| 69 |
+
Returns
|
| 70 |
+
-------
|
| 71 |
+
np.array
|
| 72 |
+
DeepWalk embedding
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
if len(gr.edges())<1:
|
| 76 |
+
return np.zeros((1,representation_size))
|
| 77 |
+
G = graph2.from_networkx(gr.copy(), undirected=gr.is_directed())
|
| 78 |
+
num_walks = len(G.nodes()) * number_walks
|
| 79 |
+
|
| 80 |
+
data_size = num_walks * walk_length
|
| 81 |
+
|
| 82 |
+
#print("Data size (walks*length): {}".format(data_size))
|
| 83 |
+
|
| 84 |
+
if data_size < max_memory_data_size:
|
| 85 |
+
#print("Walking...")
|
| 86 |
+
walks = graph2.build_deepwalk_corpus(G, num_paths=number_walks,
|
| 87 |
+
path_length=walk_length, alpha=0, rand=random.Random(seed))
|
| 88 |
+
#print("Training...")
|
| 89 |
+
model = Word2Vec(walks, vector_size=representation_size,
|
| 90 |
+
window=window_size, min_count=0, sg=1, hs=1, workers=workers)
|
| 91 |
+
else:
|
| 92 |
+
#print("Data size {} is larger than limit (max-memory-data-size: {}). Dumping walks to disk.".format(
|
| 93 |
+
# data_size, max_memory_data_size))
|
| 94 |
+
#print("Walking...")
|
| 95 |
+
|
| 96 |
+
walks_filebase = "temp.walks"
|
| 97 |
+
walk_files = serialized_walks.write_walks_to_disk(G, walks_filebase, num_paths=number_walks,
|
| 98 |
+
path_length=walk_length, alpha=0, rand=random.Random(seed),
|
| 99 |
+
num_workers=workers)
|
| 100 |
+
|
| 101 |
+
#print("Counting vertex frequency...")
|
| 102 |
+
if not vertex_freq_degree:
|
| 103 |
+
vertex_counts = serialized_walks.count_textfiles(
|
| 104 |
+
walk_files, workers)
|
| 105 |
+
else:
|
| 106 |
+
# use degree distribution for frequency in tree
|
| 107 |
+
vertex_counts = G.degree(nodes=G.iterkeys())
|
| 108 |
+
|
| 109 |
+
#print("Training...")
|
| 110 |
+
walks_corpus = serialized_walks.WalksCorpus(walk_files)
|
| 111 |
+
model = Skipgram(sentences=walks_corpus, vocabulary_counts=vertex_counts,
|
| 112 |
+
size=representation_size,
|
| 113 |
+
window=window_size, min_count=0, trim_rule=None, workers=workers)
|
| 114 |
+
|
| 115 |
+
return model.wv.vectors
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
cdef class DeepWalk(Base):
|
| 119 |
+
"""
|
| 120 |
+
Based on :
|
| 121 |
+
@inproceedings{Perozzi:2014:DOL:2623330.2623732,
|
| 122 |
+
author = {Perozzi, Bryan and Al-Rfou, Rami and Skiena, Steven},
|
| 123 |
+
title = {DeepWalk: Online Learning of Social Representations},
|
| 124 |
+
booktitle = {Proceedings of the 20th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining},
|
| 125 |
+
series = {KDD '14},
|
| 126 |
+
year = {2014},
|
| 127 |
+
isbn = {978-1-4503-2956-9},
|
| 128 |
+
location = {New York, New York, USA},
|
| 129 |
+
pages = {701--710},
|
| 130 |
+
numpages = {10},
|
| 131 |
+
url = {http://doi.acm.org/10.1145/2623330.2623732},
|
| 132 |
+
doi = {10.1145/2623330.2623732},
|
| 133 |
+
acmid = {2623732},
|
| 134 |
+
publisher = {ACM},
|
| 135 |
+
address = {New York, NY, USA},
|
| 136 |
+
keywords = {deep learning, latent representations, learning with partial labels, network classification, online learning, social networks},
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
Original Code : https://github.com/phanein/deepwalk
|
| 140 |
+
|
| 141 |
+
Modified by : Jacques Fize
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
def __init__(self):
|
| 145 |
+
Base.__init__(self,0,False)
|
| 146 |
+
|
| 147 |
+
def extract_embedding(self, listgs):
|
| 148 |
+
"""
|
| 149 |
+
Extract DeepWalk embedding of each graph in `listgs`
|
| 150 |
+
|
| 151 |
+
Parameters
|
| 152 |
+
----------
|
| 153 |
+
listgs : list
|
| 154 |
+
list of graphs
|
| 155 |
+
|
| 156 |
+
Returns
|
| 157 |
+
-------
|
| 158 |
+
list
|
| 159 |
+
list of embeddings
|
| 160 |
+
"""
|
| 161 |
+
|
| 162 |
+
from tqdm import tqdm
|
| 163 |
+
models = Parallel(n_jobs = cpu_count())(delayed(process)(nx.Graph(g)) for g in tqdm(listgs,desc="Extracting Embeddings..."))
|
| 164 |
+
return models
|
| 165 |
+
|
| 166 |
+
@cython.boundscheck(False)
|
| 167 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 168 |
+
# Selected is ignored
|
| 169 |
+
models = self.extract_embedding(listgs)
|
| 170 |
+
vector_matrix = np.array([mod.mean(axis=0) for mod in models]) # Average nodes representations
|
| 171 |
+
cs = cosine_similarity(vector_matrix)
|
| 172 |
+
return cs
|
| 173 |
+
|
GMatch4py/source/gmatch4py/embedding/graph.pyx
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
"""Graph utilities."""
|
| 5 |
+
|
| 6 |
+
import logging
|
| 7 |
+
import sys
|
| 8 |
+
from io import open
|
| 9 |
+
from os import path
|
| 10 |
+
from time import time
|
| 11 |
+
from glob import glob
|
| 12 |
+
from six.moves import range, zip, zip_longest
|
| 13 |
+
from six import iterkeys
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
from collections.abc import Iterable
|
| 17 |
+
except ImportError:
|
| 18 |
+
from collections import Iterable
|
| 19 |
+
from collections import defaultdict
|
| 20 |
+
|
| 21 |
+
import random
|
| 22 |
+
from random import shuffle
|
| 23 |
+
from itertools import product,permutations
|
| 24 |
+
from scipy.io import loadmat
|
| 25 |
+
from scipy.sparse import issparse
|
| 26 |
+
|
| 27 |
+
logger = logging.getLogger("deepwalk")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
__author__ = "Bryan Perozzi"
|
| 31 |
+
__email__ = "bperozzi@cs.stonybrook.edu"
|
| 32 |
+
|
| 33 |
+
LOGFORMAT = "%(asctime).19s %(levelname)s %(filename)s: %(lineno)s %(message)s"
|
| 34 |
+
|
| 35 |
+
class Graph(defaultdict):
|
| 36 |
+
"""Efficient basic implementation of nx `Graph' – Undirected graphs with self loops"""
|
| 37 |
+
def __init__(self):
|
| 38 |
+
super(Graph, self).__init__(list)
|
| 39 |
+
|
| 40 |
+
def nodes(self):
|
| 41 |
+
return self.keys()
|
| 42 |
+
|
| 43 |
+
def adjacency_iter(self):
|
| 44 |
+
return self.iteritems()
|
| 45 |
+
|
| 46 |
+
def subgraph(self, nodes={}):
|
| 47 |
+
subgraph = Graph()
|
| 48 |
+
|
| 49 |
+
for n in nodes:
|
| 50 |
+
if n in self:
|
| 51 |
+
subgraph[n] = [x for x in self[n] if x in nodes]
|
| 52 |
+
|
| 53 |
+
return subgraph
|
| 54 |
+
|
| 55 |
+
def make_undirected(self):
|
| 56 |
+
|
| 57 |
+
t0 = time()
|
| 58 |
+
|
| 59 |
+
for v in self.keys():
|
| 60 |
+
for other in self[v]:
|
| 61 |
+
if v != other:
|
| 62 |
+
self[other].append(v)
|
| 63 |
+
|
| 64 |
+
t1 = time()
|
| 65 |
+
logger.info('make_directed: added missing edges {}s'.format(t1-t0))
|
| 66 |
+
|
| 67 |
+
self.make_consistent()
|
| 68 |
+
return self
|
| 69 |
+
|
| 70 |
+
def make_consistent(self):
|
| 71 |
+
t0 = time()
|
| 72 |
+
for k in iterkeys(self):
|
| 73 |
+
self[k] = list(sorted(set(self[k])))
|
| 74 |
+
|
| 75 |
+
t1 = time()
|
| 76 |
+
logger.info('make_consistent: made consistent in {}s'.format(t1-t0))
|
| 77 |
+
|
| 78 |
+
self.remove_self_loops()
|
| 79 |
+
|
| 80 |
+
return self
|
| 81 |
+
|
| 82 |
+
def remove_self_loops(self):
|
| 83 |
+
|
| 84 |
+
removed = 0
|
| 85 |
+
t0 = time()
|
| 86 |
+
|
| 87 |
+
for x in self:
|
| 88 |
+
if x in self[x]:
|
| 89 |
+
self[x].remove(x)
|
| 90 |
+
removed += 1
|
| 91 |
+
|
| 92 |
+
t1 = time()
|
| 93 |
+
|
| 94 |
+
logger.info('remove_self_loops: removed {} loops in {}s'.format(removed, (t1-t0)))
|
| 95 |
+
return self
|
| 96 |
+
|
| 97 |
+
def check_self_loops(self):
|
| 98 |
+
for x in self:
|
| 99 |
+
for y in self[x]:
|
| 100 |
+
if x == y:
|
| 101 |
+
return True
|
| 102 |
+
|
| 103 |
+
return False
|
| 104 |
+
|
| 105 |
+
def has_edge(self, v1, v2):
|
| 106 |
+
if v2 in self[v1] or v1 in self[v2]:
|
| 107 |
+
return True
|
| 108 |
+
return False
|
| 109 |
+
|
| 110 |
+
def degree(self, nodes=None):
|
| 111 |
+
if isinstance(nodes, Iterable):
|
| 112 |
+
return {v:len(self[v]) for v in nodes}
|
| 113 |
+
else:
|
| 114 |
+
return len(self[nodes])
|
| 115 |
+
|
| 116 |
+
def order(self):
|
| 117 |
+
"Returns the number of nodes in the graph"
|
| 118 |
+
return len(self)
|
| 119 |
+
|
| 120 |
+
def number_of_edges(self):
|
| 121 |
+
"Returns the number of nodes in the graph"
|
| 122 |
+
return sum([self.degree(x) for x in self.keys()])/2
|
| 123 |
+
|
| 124 |
+
def number_of_nodes(self):
|
| 125 |
+
"Returns the number of nodes in the graph"
|
| 126 |
+
return self.order()
|
| 127 |
+
|
| 128 |
+
def random_walk(self, path_length, alpha=0, rand=random.Random(), start=None):
|
| 129 |
+
""" Returns a truncated random walk.
|
| 130 |
+
|
| 131 |
+
path_length: Length of the random walk.
|
| 132 |
+
alpha: probability of restarts.
|
| 133 |
+
start: the start node of the random walk.
|
| 134 |
+
"""
|
| 135 |
+
G = self
|
| 136 |
+
if start:
|
| 137 |
+
path = [start]
|
| 138 |
+
else:
|
| 139 |
+
# Sampling is uniform w.r.t V, and not w.r.t E
|
| 140 |
+
path = [rand.choice(list(G.keys()))]
|
| 141 |
+
|
| 142 |
+
while len(path) < path_length:
|
| 143 |
+
cur = path[-1]
|
| 144 |
+
if len(G[cur]) > 0:
|
| 145 |
+
if rand.random() >= alpha:
|
| 146 |
+
path.append(rand.choice(G[cur]))
|
| 147 |
+
else:
|
| 148 |
+
path.append(path[0])
|
| 149 |
+
else:
|
| 150 |
+
break
|
| 151 |
+
return [str(node) for node in path]
|
| 152 |
+
|
| 153 |
+
# TODO add build_walks in here
|
| 154 |
+
|
| 155 |
+
def build_deepwalk_corpus(G, num_paths, path_length, alpha=0,
|
| 156 |
+
rand=random.Random(0)):
|
| 157 |
+
walks = []
|
| 158 |
+
|
| 159 |
+
nodes = list(G.nodes())
|
| 160 |
+
|
| 161 |
+
for cnt in range(num_paths):
|
| 162 |
+
rand.shuffle(nodes)
|
| 163 |
+
for node in nodes:
|
| 164 |
+
walks.append(G.random_walk(path_length, rand=rand, alpha=alpha, start=node))
|
| 165 |
+
|
| 166 |
+
return walks
|
| 167 |
+
|
| 168 |
+
def build_deepwalk_corpus_iter(G, num_paths, path_length, alpha=0,
|
| 169 |
+
rand=random.Random(0)):
|
| 170 |
+
walks = []
|
| 171 |
+
|
| 172 |
+
nodes = list(G.nodes())
|
| 173 |
+
|
| 174 |
+
for cnt in range(num_paths):
|
| 175 |
+
rand.shuffle(nodes)
|
| 176 |
+
for node in nodes:
|
| 177 |
+
yield G.random_walk(path_length, rand=rand, alpha=alpha, start=node)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def clique(size):
|
| 181 |
+
return from_adjlist(permutations(range(1,size+1)))
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
# http://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks-in-python
|
| 185 |
+
def grouper(n, iterable, padvalue=None):
|
| 186 |
+
"grouper(3, 'abcdefg', 'x') --> ('a','b','c'), ('d','e','f'), ('g','x','x')"
|
| 187 |
+
return zip_longest(*[iter(iterable)]*n, fillvalue=padvalue)
|
| 188 |
+
|
| 189 |
+
def parse_adjacencylist(f):
|
| 190 |
+
adjlist = []
|
| 191 |
+
for l in f:
|
| 192 |
+
if l and l[0] != "#":
|
| 193 |
+
introw = [int(x) for x in l.strip().split()]
|
| 194 |
+
row = [introw[0]]
|
| 195 |
+
row.extend(set(sorted(introw[1:])))
|
| 196 |
+
adjlist.extend([row])
|
| 197 |
+
|
| 198 |
+
return adjlist
|
| 199 |
+
|
| 200 |
+
def parse_adjacencylist_unchecked(f):
|
| 201 |
+
adjlist = []
|
| 202 |
+
for l in f:
|
| 203 |
+
if l and l[0] != "#":
|
| 204 |
+
adjlist.extend([[int(x) for x in l.strip().split()]])
|
| 205 |
+
|
| 206 |
+
return adjlist
|
| 207 |
+
|
| 208 |
+
def load_adjacencylist(file_, undirected=False, chunksize=10000, unchecked=True):
|
| 209 |
+
|
| 210 |
+
if unchecked:
|
| 211 |
+
parse_func = parse_adjacencylist_unchecked
|
| 212 |
+
convert_func = from_adjlist_unchecked
|
| 213 |
+
else:
|
| 214 |
+
parse_func = parse_adjacencylist
|
| 215 |
+
convert_func = from_adjlist
|
| 216 |
+
|
| 217 |
+
adjlist = []
|
| 218 |
+
|
| 219 |
+
t0 = time()
|
| 220 |
+
|
| 221 |
+
total = 0
|
| 222 |
+
with open(file_) as f:
|
| 223 |
+
for idx, adj_chunk in enumerate(map(parse_func, grouper(int(chunksize), f))):
|
| 224 |
+
adjlist.extend(adj_chunk)
|
| 225 |
+
total += len(adj_chunk)
|
| 226 |
+
|
| 227 |
+
t1 = time()
|
| 228 |
+
|
| 229 |
+
logger.info('Parsed {} edges with {} chunks in {}s'.format(total, idx, t1-t0))
|
| 230 |
+
|
| 231 |
+
t0 = time()
|
| 232 |
+
G = convert_func(adjlist)
|
| 233 |
+
t1 = time()
|
| 234 |
+
|
| 235 |
+
logger.info('Converted edges to graph in {}s'.format(t1-t0))
|
| 236 |
+
|
| 237 |
+
if undirected:
|
| 238 |
+
t0 = time()
|
| 239 |
+
G = G.make_undirected()
|
| 240 |
+
t1 = time()
|
| 241 |
+
logger.info('Made graph undirected in {}s'.format(t1-t0))
|
| 242 |
+
|
| 243 |
+
return G
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def load_edgelist(file_, undirected=True):
|
| 247 |
+
G = Graph()
|
| 248 |
+
with open(file_) as f:
|
| 249 |
+
for l in f:
|
| 250 |
+
x, y = l.strip().split()[:2]
|
| 251 |
+
x = int(x)
|
| 252 |
+
y = int(y)
|
| 253 |
+
G[x].append(y)
|
| 254 |
+
if undirected:
|
| 255 |
+
G[y].append(x)
|
| 256 |
+
|
| 257 |
+
G.make_consistent()
|
| 258 |
+
return G
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def load_matfile(file_, variable_name="network", undirected=True):
|
| 262 |
+
mat_varables = loadmat(file_)
|
| 263 |
+
mat_matrix = mat_varables[variable_name]
|
| 264 |
+
|
| 265 |
+
return from_numpy(mat_matrix, undirected)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def from_networkx(G_input, undirected=True):
|
| 269 |
+
G = Graph()
|
| 270 |
+
|
| 271 |
+
for _, x in enumerate(G_input):
|
| 272 |
+
for y in iterkeys(G_input[x]):
|
| 273 |
+
G[x].append(y)
|
| 274 |
+
|
| 275 |
+
if undirected:
|
| 276 |
+
G.make_undirected()
|
| 277 |
+
|
| 278 |
+
return G
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def from_numpy(x, undirected=True):
|
| 282 |
+
G = Graph()
|
| 283 |
+
|
| 284 |
+
if issparse(x):
|
| 285 |
+
cx = x.tocoo()
|
| 286 |
+
for i,j,v in zip(cx.row, cx.col, cx.data):
|
| 287 |
+
G[i].append(j)
|
| 288 |
+
else:
|
| 289 |
+
raise Exception("Dense matrices not yet supported.")
|
| 290 |
+
|
| 291 |
+
if undirected:
|
| 292 |
+
G.make_undirected()
|
| 293 |
+
|
| 294 |
+
G.make_consistent()
|
| 295 |
+
return G
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def from_adjlist(adjlist):
|
| 299 |
+
G = Graph()
|
| 300 |
+
|
| 301 |
+
for row in adjlist:
|
| 302 |
+
node = row[0]
|
| 303 |
+
neighbors = row[1:]
|
| 304 |
+
G[node] = list(sorted(set(neighbors)))
|
| 305 |
+
|
| 306 |
+
return G
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def from_adjlist_unchecked(adjlist):
|
| 310 |
+
G = Graph()
|
| 311 |
+
|
| 312 |
+
for row in adjlist:
|
| 313 |
+
node = row[0]
|
| 314 |
+
neighbors = row[1:]
|
| 315 |
+
G[node] = neighbors
|
| 316 |
+
|
| 317 |
+
return G
|
| 318 |
+
|
| 319 |
+
|
GMatch4py/source/gmatch4py/embedding/graph2vec.pyx
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import json
|
| 3 |
+
import glob
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
cimport numpy as np
|
| 9 |
+
import numpy.distutils.system_info as sysinfo
|
| 10 |
+
|
| 11 |
+
from joblib import Parallel, delayed
|
| 12 |
+
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
|
| 13 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 14 |
+
|
| 15 |
+
from ..base cimport Base
|
| 16 |
+
cimport cython
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class WeisfeilerLehmanMachine:
|
| 20 |
+
"""
|
| 21 |
+
Weisfeiler Lehman feature extractor class.
|
| 22 |
+
"""
|
| 23 |
+
def __init__(self, graph, features, iterations):
|
| 24 |
+
"""
|
| 25 |
+
Initialization method which executes feature extraction.
|
| 26 |
+
|
| 27 |
+
Parameters
|
| 28 |
+
----------
|
| 29 |
+
graph : nx.Graph
|
| 30 |
+
graph
|
| 31 |
+
features : dict
|
| 32 |
+
Feature hash table.
|
| 33 |
+
iterations : int
|
| 34 |
+
number of WL iteration
|
| 35 |
+
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
self.iterations = iterations
|
| 39 |
+
self.graph = graph
|
| 40 |
+
self.features = features
|
| 41 |
+
self.nodes = self.graph.nodes()
|
| 42 |
+
self.extracted_features = [str(v) for k,v in features.items()]
|
| 43 |
+
self.do_recursions()
|
| 44 |
+
|
| 45 |
+
def do_a_recursion(self):
|
| 46 |
+
"""
|
| 47 |
+
The method does a single WL recursion.
|
| 48 |
+
|
| 49 |
+
Returns
|
| 50 |
+
-------
|
| 51 |
+
dict
|
| 52 |
+
The hash table with extracted WL features.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
new_features = {}
|
| 56 |
+
for node in self.nodes:
|
| 57 |
+
nebs = self.graph.neighbors(node)
|
| 58 |
+
degs = [self.features[neb] for neb in nebs]
|
| 59 |
+
features = "_".join([str(self.features[node])]+list(set(sorted([str(deg) for deg in degs]))))
|
| 60 |
+
hash_object = hashlib.md5(features.encode())
|
| 61 |
+
hashing = hash_object.hexdigest()
|
| 62 |
+
new_features[node] = hashing
|
| 63 |
+
self.extracted_features = self.extracted_features + list(new_features.values())
|
| 64 |
+
return new_features
|
| 65 |
+
|
| 66 |
+
def do_recursions(self):
|
| 67 |
+
"""
|
| 68 |
+
The method does a series of WL recursions.
|
| 69 |
+
"""
|
| 70 |
+
for iteration in range(self.iterations):
|
| 71 |
+
self.features = self.do_a_recursion()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def dataset_reader(graph):
|
| 75 |
+
"""
|
| 76 |
+
Function to extract features from a networkx graph
|
| 77 |
+
|
| 78 |
+
Parameters
|
| 79 |
+
----------
|
| 80 |
+
graph : nx.Graph
|
| 81 |
+
graph
|
| 82 |
+
|
| 83 |
+
Returns
|
| 84 |
+
-------
|
| 85 |
+
dict
|
| 86 |
+
Features hash table.
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
features = dict(nx.degree(graph))
|
| 90 |
+
|
| 91 |
+
features = {k:v for k,v, in features.items()}
|
| 92 |
+
return graph, features
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def feature_extractor(graph, ix, rounds):
|
| 96 |
+
"""
|
| 97 |
+
Function to extract WL features from a graph
|
| 98 |
+
|
| 99 |
+
Parameters
|
| 100 |
+
----------
|
| 101 |
+
graph : nx.Graph
|
| 102 |
+
graph
|
| 103 |
+
ix : int
|
| 104 |
+
index of the graph in the dataset
|
| 105 |
+
rounds : int
|
| 106 |
+
number of WL iterations
|
| 107 |
+
|
| 108 |
+
Returns
|
| 109 |
+
-------
|
| 110 |
+
TaggedDocument
|
| 111 |
+
random walks
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
graph, features = dataset_reader(graph)
|
| 115 |
+
machine = WeisfeilerLehmanMachine(graph,features,rounds)
|
| 116 |
+
doc = TaggedDocument(words = machine.extracted_features , tags = ["g_{0}".format(ix)])
|
| 117 |
+
return doc
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def generate_model(graphs, iteration = 2, dimensions = 64, min_count = 5, down_sampling = 0.0001, learning_rate = 0.0001, epochs = 10, workers = 4 ):
|
| 122 |
+
"""
|
| 123 |
+
Main function to read the graph list, extract features, learn the embedding and save it.
|
| 124 |
+
|
| 125 |
+
Parameters
|
| 126 |
+
----------
|
| 127 |
+
graphs : nx.Graph
|
| 128 |
+
Input graph
|
| 129 |
+
iteration : int, optional
|
| 130 |
+
number of iteration (the default is 2)
|
| 131 |
+
dimensions : int, optional
|
| 132 |
+
output vector dimension (the default is 64)
|
| 133 |
+
min_count : int, optional
|
| 134 |
+
min count parameter of Doc2vec model (the default is 5)
|
| 135 |
+
down_sampling : float, optional
|
| 136 |
+
Down sampling rate for frequent features. (the default is 0.0001)
|
| 137 |
+
learning_rate : float, optional
|
| 138 |
+
Initial learning rate (the default is 0.0001, which [default_description])
|
| 139 |
+
epochs : int, optional
|
| 140 |
+
Number of epochs (the default is 10)
|
| 141 |
+
workers : int, optional
|
| 142 |
+
Number of workers (the default is 4)
|
| 143 |
+
|
| 144 |
+
Returns
|
| 145 |
+
-------
|
| 146 |
+
[type]
|
| 147 |
+
[description]
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
document_collections = Parallel(n_jobs = workers)(delayed(feature_extractor)(g, ix,iteration) for ix,g in tqdm(enumerate(graphs),desc="Extracting Features..."))
|
| 151 |
+
graphs=[nx.relabel_nodes(g,{node:str(node) for node in list(g.nodes)},copy=True) for g in graphs]
|
| 152 |
+
model = Doc2Vec(document_collections,
|
| 153 |
+
vector_size = dimensions,
|
| 154 |
+
window = 0,
|
| 155 |
+
min_count = min_count,
|
| 156 |
+
dm = 0,
|
| 157 |
+
sample = down_sampling,
|
| 158 |
+
workers = workers,
|
| 159 |
+
epochs = epochs,
|
| 160 |
+
alpha = learning_rate)
|
| 161 |
+
return model
|
| 162 |
+
|
| 163 |
+
cdef class Graph2Vec(Base):
|
| 164 |
+
"""
|
| 165 |
+
Based on :
|
| 166 |
+
graph2vec: Learning distributed representations of graphs.
|
| 167 |
+
Narayanan, Annamalai and Chandramohan, Mahinthan and Venkatesan, Rajasekar and Chen, Lihui and Liu, Yang
|
| 168 |
+
MLG 2017, 13th International Workshop on Mining and Learning with Graphs (MLGWorkshop 2017)
|
| 169 |
+
|
| 170 |
+
Original Code : https://github.com/benedekrozemberczki/graph2vec
|
| 171 |
+
|
| 172 |
+
Modified by : Jacques Fize
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
def __init__(self):
|
| 176 |
+
Base.__init__(self,0,False)
|
| 177 |
+
|
| 178 |
+
@cython.boundscheck(False)
|
| 179 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 180 |
+
# Selected is ignored
|
| 181 |
+
model = generate_model(listgs)
|
| 182 |
+
vector_matrix = model.docvecs.vectors_docs
|
| 183 |
+
cs = cosine_similarity(vector_matrix)
|
| 184 |
+
return cs
|
GMatch4py/source/gmatch4py/embedding/node2vec.pyx
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
cimport numpy as np
|
| 5 |
+
from gensim.models import Word2Vec
|
| 6 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 7 |
+
|
| 8 |
+
from ..base cimport Base
|
| 9 |
+
cimport cython
|
| 10 |
+
from joblib import Parallel, delayed
|
| 11 |
+
import networkx as nx
|
| 12 |
+
|
| 13 |
+
class Graph():
|
| 14 |
+
def __init__(self, nx_G, is_directed, p, q):
|
| 15 |
+
self.G = nx_G
|
| 16 |
+
self.is_directed = is_directed
|
| 17 |
+
self.p = p
|
| 18 |
+
self.q = q
|
| 19 |
+
|
| 20 |
+
def node2vec_walk(self, walk_length, start_node):
|
| 21 |
+
'''
|
| 22 |
+
Simulate a random walk starting from start node.
|
| 23 |
+
'''
|
| 24 |
+
G = self.G
|
| 25 |
+
alias_nodes = self.alias_nodes
|
| 26 |
+
alias_edges = self.alias_edges
|
| 27 |
+
|
| 28 |
+
walk = [start_node]
|
| 29 |
+
|
| 30 |
+
while len(walk) < walk_length:
|
| 31 |
+
cur = walk[-1]
|
| 32 |
+
cur_nbrs = sorted(G.neighbors(cur))
|
| 33 |
+
if len(cur_nbrs) > 0:
|
| 34 |
+
if len(walk) == 1:
|
| 35 |
+
walk.append(
|
| 36 |
+
cur_nbrs[alias_draw(alias_nodes[cur][0], alias_nodes[cur][1])])
|
| 37 |
+
else:
|
| 38 |
+
prev = walk[-2]
|
| 39 |
+
next = cur_nbrs[alias_draw(alias_edges[(prev, cur)][0],
|
| 40 |
+
alias_edges[(prev, cur)][1])]
|
| 41 |
+
walk.append(next)
|
| 42 |
+
else:
|
| 43 |
+
break
|
| 44 |
+
|
| 45 |
+
return walk
|
| 46 |
+
|
| 47 |
+
def simulate_walks(self, num_walks, walk_length):
|
| 48 |
+
'''
|
| 49 |
+
Repeatedly simulate random walks from each node.
|
| 50 |
+
'''
|
| 51 |
+
# sys.stdout.write("\r")
|
| 52 |
+
G = self.G
|
| 53 |
+
walks = []
|
| 54 |
+
nodes = list(G.nodes)
|
| 55 |
+
for walk_iter in range(num_walks):
|
| 56 |
+
# sys.stdout.write(
|
| 57 |
+
# '\rWalk iteration: {0}/{1}'.format(walk_iter + 1, num_walks))
|
| 58 |
+
random.shuffle(nodes)
|
| 59 |
+
for node in nodes:
|
| 60 |
+
walks.append(self.node2vec_walk(
|
| 61 |
+
walk_length=walk_length, start_node=node))
|
| 62 |
+
|
| 63 |
+
return walks
|
| 64 |
+
|
| 65 |
+
def get_alias_edge(self, src, dst):
|
| 66 |
+
'''
|
| 67 |
+
Get the alias edge setup lists for a given edge.
|
| 68 |
+
'''
|
| 69 |
+
G = self.G
|
| 70 |
+
p = self.p
|
| 71 |
+
q = self.q
|
| 72 |
+
|
| 73 |
+
unnormalized_probs = []
|
| 74 |
+
for dst_nbr in sorted(G.neighbors(dst)):
|
| 75 |
+
if dst_nbr == src:
|
| 76 |
+
unnormalized_probs.append(G[dst][dst_nbr]['weight'] / p)
|
| 77 |
+
elif G.has_edge(dst_nbr, src):
|
| 78 |
+
unnormalized_probs.append(G[dst][dst_nbr]['weight'])
|
| 79 |
+
else:
|
| 80 |
+
unnormalized_probs.append(G[dst][dst_nbr]['weight'] / q)
|
| 81 |
+
norm_const = sum(unnormalized_probs)
|
| 82 |
+
normalized_probs = [
|
| 83 |
+
float(u_prob) / norm_const for u_prob in unnormalized_probs]
|
| 84 |
+
|
| 85 |
+
return alias_setup(normalized_probs)
|
| 86 |
+
|
| 87 |
+
def preprocess_transition_probs(self):
|
| 88 |
+
'''
|
| 89 |
+
Preprocessing of transition probabilities for guiding the random walks.
|
| 90 |
+
'''
|
| 91 |
+
G = self.G
|
| 92 |
+
is_directed = self.is_directed
|
| 93 |
+
|
| 94 |
+
alias_nodes = {}
|
| 95 |
+
for node in list(G.nodes):
|
| 96 |
+
unnormalized_probs = [G[node][nbr]['weight']
|
| 97 |
+
for nbr in sorted(G.neighbors(node))]
|
| 98 |
+
norm_const = sum(unnormalized_probs)
|
| 99 |
+
normalized_probs = [
|
| 100 |
+
float(u_prob) / norm_const for u_prob in unnormalized_probs]
|
| 101 |
+
alias_nodes[node] = alias_setup(normalized_probs)
|
| 102 |
+
|
| 103 |
+
alias_edges = {}
|
| 104 |
+
triads = {}
|
| 105 |
+
|
| 106 |
+
if is_directed:
|
| 107 |
+
for edge in list(G.edges()):
|
| 108 |
+
alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])
|
| 109 |
+
else:
|
| 110 |
+
for edge in list(G.edges()):
|
| 111 |
+
alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])
|
| 112 |
+
alias_edges[(edge[1], edge[0])] = self.get_alias_edge(
|
| 113 |
+
edge[1], edge[0])
|
| 114 |
+
|
| 115 |
+
self.alias_nodes = alias_nodes
|
| 116 |
+
self.alias_edges = alias_edges
|
| 117 |
+
|
| 118 |
+
return
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def alias_setup(probs):
|
| 122 |
+
'''
|
| 123 |
+
Compute utility lists for non-uniform sampling from discrete distributions.
|
| 124 |
+
Refer to https://hips.seas.harvard.edu/blog/2013/03/03/the-alias-method-efficient-sampling-with-many-discrete-outcomes/
|
| 125 |
+
for details
|
| 126 |
+
'''
|
| 127 |
+
K = len(probs)
|
| 128 |
+
q = np.zeros(K)
|
| 129 |
+
J = np.zeros(K, dtype=np.int)
|
| 130 |
+
|
| 131 |
+
smaller = []
|
| 132 |
+
larger = []
|
| 133 |
+
for kk, prob in enumerate(probs):
|
| 134 |
+
q[kk] = K * prob
|
| 135 |
+
if q[kk] < 1.0:
|
| 136 |
+
smaller.append(kk)
|
| 137 |
+
else:
|
| 138 |
+
larger.append(kk)
|
| 139 |
+
|
| 140 |
+
while len(smaller) > 0 and len(larger) > 0:
|
| 141 |
+
small = smaller.pop()
|
| 142 |
+
large = larger.pop()
|
| 143 |
+
|
| 144 |
+
J[small] = large
|
| 145 |
+
q[large] = q[large] + q[small] - 1.0
|
| 146 |
+
if q[large] < 1.0:
|
| 147 |
+
smaller.append(large)
|
| 148 |
+
else:
|
| 149 |
+
larger.append(large)
|
| 150 |
+
|
| 151 |
+
return J, q
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def alias_draw(J, q):
|
| 155 |
+
'''
|
| 156 |
+
Draw sample from a non-uniform discrete distribution using alias sampling.
|
| 157 |
+
'''
|
| 158 |
+
K = len(J)
|
| 159 |
+
|
| 160 |
+
kk = int(np.floor(np.random.rand() * K))
|
| 161 |
+
if np.random.rand() < q[kk]:
|
| 162 |
+
return kk
|
| 163 |
+
else:
|
| 164 |
+
return J[kk]
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def learn_embeddings(walks, dimensions, window_size, nb_workers, nb_iter):
|
| 168 |
+
'''
|
| 169 |
+
Learn embeddings by optimizing the Skipgram objective using SGD.
|
| 170 |
+
'''
|
| 171 |
+
walks_ = [list(map(str, walk)) for walk in walks]
|
| 172 |
+
model = Word2Vec(walks_, size=dimensions, window=window_size,
|
| 173 |
+
min_count=0, sg=1, workers=nb_workers, iter=nb_iter)
|
| 174 |
+
return model
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def compute_graph_model(nx_graph, **kwargs):
|
| 178 |
+
'''
|
| 179 |
+
Pipeline for representational learning for all nodes in a graph.
|
| 180 |
+
@param nx_graph
|
| 181 |
+
@kwarg p: int
|
| 182 |
+
@kwarg q: int
|
| 183 |
+
'''
|
| 184 |
+
p = kwargs.get("p", 1)
|
| 185 |
+
q = kwargs.get("q", 1)
|
| 186 |
+
dimensions = kwargs.get("dimensions", 128)
|
| 187 |
+
window_size = kwargs.get("window_size", 10)
|
| 188 |
+
nb_workers = kwargs.get("nb_workers", 8)
|
| 189 |
+
nb_iter = kwargs.get("nb_iter", 1)
|
| 190 |
+
num_walks = kwargs.get("num_walks", 10)
|
| 191 |
+
walk_length = kwargs.get("walk_length", 80)
|
| 192 |
+
directed = kwargs.get("directed", False)
|
| 193 |
+
|
| 194 |
+
G = Graph(nx_graph, directed, p, q)
|
| 195 |
+
G.preprocess_transition_probs()
|
| 196 |
+
walks = G.simulate_walks(num_walks, walk_length)
|
| 197 |
+
return learn_embeddings(walks, dimensions, window_size, nb_workers, nb_iter).wv.vectors
|
| 198 |
+
|
| 199 |
+
cdef class Node2Vec(Base):
|
| 200 |
+
"""
|
| 201 |
+
Based on :
|
| 202 |
+
Extract Node2vec embedding of each graph in `listgs`
|
| 203 |
+
@inproceedings{Grover:2016:NSF:2939672.2939754,
|
| 204 |
+
author = {Grover, Aditya and Leskovec, Jure},
|
| 205 |
+
title = {Node2Vec: Scalable Feature Learning for Networks},
|
| 206 |
+
booktitle = {Proceedings of the 22Nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining},
|
| 207 |
+
series = {KDD '16},
|
| 208 |
+
year = {2016},
|
| 209 |
+
isbn = {978-1-4503-4232-2},
|
| 210 |
+
location = {San Francisco, California, USA},
|
| 211 |
+
pages = {855--864},
|
| 212 |
+
numpages = {10},
|
| 213 |
+
url = {http://doi.acm.org/10.1145/2939672.2939754},
|
| 214 |
+
doi = {10.1145/2939672.2939754},
|
| 215 |
+
acmid = {2939754},
|
| 216 |
+
publisher = {ACM},
|
| 217 |
+
address = {New York, NY, USA},
|
| 218 |
+
keywords = {feature learning, graph representations, information networks, node embeddings},
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
Original code : https://github.com/aditya-grover/node2vec
|
| 222 |
+
|
| 223 |
+
Modified by : Jacques Fize
|
| 224 |
+
"""
|
| 225 |
+
|
| 226 |
+
def __init__(self):
|
| 227 |
+
Base.__init__(self,0,False)
|
| 228 |
+
|
| 229 |
+
def extract_embedding(self, listgs):
|
| 230 |
+
"""
|
| 231 |
+
Extract Node2vec embedding of each graph in `listgs`
|
| 232 |
+
|
| 233 |
+
Parameters
|
| 234 |
+
----------
|
| 235 |
+
listgs : list
|
| 236 |
+
list of graphs
|
| 237 |
+
|
| 238 |
+
Returns
|
| 239 |
+
-------
|
| 240 |
+
list
|
| 241 |
+
list of embeddings
|
| 242 |
+
"""
|
| 243 |
+
|
| 244 |
+
from tqdm import tqdm
|
| 245 |
+
models = Parallel(n_jobs = self.cpu_count)(delayed(compute_graph_model)(g,directed=g.is_directed()) for g in tqdm(listgs,desc="Extracting Embeddings..."))
|
| 246 |
+
return models
|
| 247 |
+
|
| 248 |
+
@cython.boundscheck(False)
|
| 249 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 250 |
+
# Selected is ignored
|
| 251 |
+
[nx.set_edge_attributes(g,1,'weight') for g in listgs]
|
| 252 |
+
models = self.extract_embedding(listgs)
|
| 253 |
+
vector_matrix = np.array([mod.mean(axis=0) for mod in models]) # Average nodes representations
|
| 254 |
+
cs = cosine_similarity(vector_matrix)
|
| 255 |
+
return cs
|
GMatch4py/source/gmatch4py/embedding/skipgram.pyx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from multiprocessing import cpu_count
|
| 3 |
+
|
| 4 |
+
from gensim.models import Word2Vec
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger("deepwalk")
|
| 7 |
+
|
| 8 |
+
class Skipgram(Word2Vec):
|
| 9 |
+
"""A subclass to allow more customization of the Word2Vec internals."""
|
| 10 |
+
|
| 11 |
+
def __init__(self, vocabulary_counts=None, **kwargs):
|
| 12 |
+
|
| 13 |
+
self.vocabulary_counts = None
|
| 14 |
+
|
| 15 |
+
kwargs["min_count"] = kwargs.get("min_count", 0)
|
| 16 |
+
kwargs["workers"] = kwargs.get("workers", cpu_count())
|
| 17 |
+
kwargs["size"] = kwargs.get("size", 128)
|
| 18 |
+
kwargs["sentences"] = kwargs.get("sentences", None)
|
| 19 |
+
kwargs["window"] = kwargs.get("window", 10)
|
| 20 |
+
kwargs["sg"] = 1
|
| 21 |
+
kwargs["hs"] = 1
|
| 22 |
+
|
| 23 |
+
if vocabulary_counts != None:
|
| 24 |
+
self.vocabulary_counts = vocabulary_counts
|
| 25 |
+
|
| 26 |
+
super(Skipgram, self).__init__(**kwargs)
|
GMatch4py/source/gmatch4py/embedding/walks.pyx
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from io import open
|
| 3 |
+
from os import path
|
| 4 |
+
from time import time
|
| 5 |
+
from multiprocessing import cpu_count
|
| 6 |
+
import random
|
| 7 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 8 |
+
from collections import Counter
|
| 9 |
+
|
| 10 |
+
from six.moves import zip
|
| 11 |
+
|
| 12 |
+
from . import graph
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger("deepwalk")
|
| 15 |
+
|
| 16 |
+
__current_graph = None
|
| 17 |
+
|
| 18 |
+
# speed up the string encoding
|
| 19 |
+
__vertex2str = None
|
| 20 |
+
|
| 21 |
+
def count_words(file):
|
| 22 |
+
""" Counts the word frequences in a list of sentences.
|
| 23 |
+
|
| 24 |
+
Note:
|
| 25 |
+
This is a helper function for parallel execution of `Vocabulary.from_text`
|
| 26 |
+
method.
|
| 27 |
+
"""
|
| 28 |
+
c = Counter()
|
| 29 |
+
with open(file, 'r') as f:
|
| 30 |
+
for l in f:
|
| 31 |
+
words = l.strip().split()
|
| 32 |
+
c.update(words)
|
| 33 |
+
return c
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def count_textfiles(files, workers=1):
|
| 37 |
+
c = Counter()
|
| 38 |
+
with ProcessPoolExecutor(max_workers=workers) as executor:
|
| 39 |
+
for c_ in executor.map(count_words, files):
|
| 40 |
+
c.update(c_)
|
| 41 |
+
return c
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def count_lines(f):
|
| 45 |
+
if path.isfile(f):
|
| 46 |
+
num_lines = sum(1 for line in open(f))
|
| 47 |
+
return num_lines
|
| 48 |
+
else:
|
| 49 |
+
return 0
|
| 50 |
+
|
| 51 |
+
def _write_walks_to_disk(args):
|
| 52 |
+
num_paths, path_length, alpha, rand, f = args
|
| 53 |
+
G = __current_graph
|
| 54 |
+
t_0 = time()
|
| 55 |
+
with open(f, 'w') as fout:
|
| 56 |
+
for walk in graph.build_deepwalk_corpus_iter(G=G, num_paths=num_paths, path_length=path_length,
|
| 57 |
+
alpha=alpha, rand=rand):
|
| 58 |
+
fout.write(u"{}\n".format(u" ".join(v for v in walk)))
|
| 59 |
+
logger.debug("Generated new file {}, it took {} seconds".format(f, time() - t_0))
|
| 60 |
+
return f
|
| 61 |
+
|
| 62 |
+
def write_walks_to_disk(G, filebase, num_paths, path_length, alpha=0, rand=random.Random(0), num_workers=cpu_count(),
|
| 63 |
+
always_rebuild=True):
|
| 64 |
+
global __current_graph
|
| 65 |
+
__current_graph = G
|
| 66 |
+
files_list = ["{}.{}".format(filebase, str(x)) for x in list(range(num_paths))]
|
| 67 |
+
expected_size = len(G)
|
| 68 |
+
args_list = []
|
| 69 |
+
files = []
|
| 70 |
+
|
| 71 |
+
if num_paths <= num_workers:
|
| 72 |
+
paths_per_worker = [1 for x in range(num_paths)]
|
| 73 |
+
else:
|
| 74 |
+
paths_per_worker = [len(list(filter(lambda z: z!= None, [y for y in x])))
|
| 75 |
+
for x in graph.grouper(int(num_paths / num_workers)+1, range(1, num_paths+1))]
|
| 76 |
+
|
| 77 |
+
with ProcessPoolExecutor(max_workers=num_workers) as executor:
|
| 78 |
+
for size, file_, ppw in zip(executor.map(count_lines, files_list), files_list, paths_per_worker):
|
| 79 |
+
if always_rebuild or size != (ppw*expected_size):
|
| 80 |
+
args_list.append((ppw, path_length, alpha, random.Random(rand.randint(0, 2**31)), file_))
|
| 81 |
+
else:
|
| 82 |
+
files.append(file_)
|
| 83 |
+
|
| 84 |
+
with ProcessPoolExecutor(max_workers=num_workers) as executor:
|
| 85 |
+
for file_ in executor.map(_write_walks_to_disk, args_list):
|
| 86 |
+
files.append(file_)
|
| 87 |
+
|
| 88 |
+
return files
|
| 89 |
+
|
| 90 |
+
class WalksCorpus(object):
|
| 91 |
+
def __init__(self, file_list):
|
| 92 |
+
self.file_list = file_list
|
| 93 |
+
def __iter__(self):
|
| 94 |
+
for file in self.file_list:
|
| 95 |
+
with open(file, 'r') as f:
|
| 96 |
+
for line in f:
|
| 97 |
+
yield line.split()
|
| 98 |
+
|
| 99 |
+
def combine_files_iter(file_list):
|
| 100 |
+
for file in file_list:
|
| 101 |
+
with open(file, 'r') as f:
|
| 102 |
+
for line in f:
|
| 103 |
+
yield line.split()
|
GMatch4py/source/gmatch4py/ged/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
GMatch4py/source/gmatch4py/ged/abstract_graph_edit_dist.pxd
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
cimport numpy as np
|
| 3 |
+
from ..base cimport Base
|
| 4 |
+
|
| 5 |
+
cdef class AbstractGraphEditDistance(Base):
|
| 6 |
+
cdef double node_del
|
| 7 |
+
cdef double node_ins
|
| 8 |
+
cdef double edge_del
|
| 9 |
+
cdef double edge_ins
|
| 10 |
+
cdef np.ndarray cost_matrix
|
| 11 |
+
cdef bint weighted
|
| 12 |
+
|
| 13 |
+
cpdef double distance_ged(self,G,H)
|
| 14 |
+
cdef list edit_costs(self,G,H)
|
| 15 |
+
cpdef np.ndarray create_cost_matrix(self,G,H)
|
| 16 |
+
cdef double insert_cost(self, int i, int j, nodesH, H)
|
| 17 |
+
cdef double delete_cost(self, int i, int j, nodesG, G)
|
| 18 |
+
cpdef double substitute_cost(self, node1, node2, G, H)
|
| 19 |
+
|
GMatch4py/source/gmatch4py/ged/abstract_graph_edit_dist.pyx
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: UTF-8 -*-
|
| 2 |
+
from __future__ import print_function
|
| 3 |
+
|
| 4 |
+
import sys
|
| 5 |
+
import warnings
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
cimport numpy as np
|
| 9 |
+
import networkx as nx
|
| 10 |
+
from cython.parallel cimport prange,parallel
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
from munkres import munkres
|
| 14 |
+
except ImportError:
|
| 15 |
+
warnings.warn("To obtain optimal results install the Cython 'munkres' module at https://github.com/jfrelinger/cython-munkres-wrapper")
|
| 16 |
+
from scipy.optimize import linear_sum_assignment as munkres
|
| 17 |
+
|
| 18 |
+
from ..base cimport Base
|
| 19 |
+
from ..helpers.general import parsenx2graph
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
cdef class AbstractGraphEditDistance(Base):
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def __init__(self, node_del,node_ins,edge_del,edge_ins):
|
| 27 |
+
Base.__init__(self,1,False)
|
| 28 |
+
|
| 29 |
+
self.node_del = node_del
|
| 30 |
+
self.node_ins = node_ins
|
| 31 |
+
self.edge_del = edge_del
|
| 32 |
+
self.edge_ins = edge_ins
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
cpdef double distance_ged(self,G,H):
|
| 36 |
+
"""
|
| 37 |
+
Return the distance value between G and H
|
| 38 |
+
|
| 39 |
+
Parameters
|
| 40 |
+
----------
|
| 41 |
+
G : gmatch4py.Graph
|
| 42 |
+
graph
|
| 43 |
+
H : gmatch4py.Graph
|
| 44 |
+
graph
|
| 45 |
+
|
| 46 |
+
Returns
|
| 47 |
+
-------
|
| 48 |
+
int
|
| 49 |
+
distance
|
| 50 |
+
"""
|
| 51 |
+
cdef list opt_path = self.edit_costs(G,H)
|
| 52 |
+
return np.sum(opt_path)
|
| 53 |
+
|
| 54 |
+
def edit_path(self,G,H):
|
| 55 |
+
"""
|
| 56 |
+
Return the edit path along with the cost matrix and the selected indices from the Munkres Algorithm
|
| 57 |
+
|
| 58 |
+
Parameters
|
| 59 |
+
----------
|
| 60 |
+
G : nx.Graph
|
| 61 |
+
first graph
|
| 62 |
+
H : nx.Graph
|
| 63 |
+
second graph
|
| 64 |
+
|
| 65 |
+
Returns
|
| 66 |
+
-------
|
| 67 |
+
np.array(1D), np.array(2D), (np.array(2D) if munkres) or (np.array(1,2) if scipy)
|
| 68 |
+
edit_path, cost_matrix, munkres results
|
| 69 |
+
"""
|
| 70 |
+
cost_matrix = self.create_cost_matrix(G,H).astype(float)
|
| 71 |
+
index_path= munkres(cost_matrix)
|
| 72 |
+
return cost_matrix[index_path], cost_matrix, index_path
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
cdef list edit_costs(self, G, H):
|
| 76 |
+
"""
|
| 77 |
+
Return the optimal path edit cost list, to transform G into H
|
| 78 |
+
|
| 79 |
+
Parameters
|
| 80 |
+
----------
|
| 81 |
+
G : gmatch4py.Graph
|
| 82 |
+
graph
|
| 83 |
+
H : gmatch4py.Graph
|
| 84 |
+
graph
|
| 85 |
+
|
| 86 |
+
Returns
|
| 87 |
+
-------
|
| 88 |
+
np.array
|
| 89 |
+
edit path
|
| 90 |
+
"""
|
| 91 |
+
cdef np.ndarray cost_matrix = self.create_cost_matrix(G,H).astype(float)
|
| 92 |
+
return cost_matrix[munkres(cost_matrix)].tolist()
|
| 93 |
+
|
| 94 |
+
cpdef np.ndarray create_cost_matrix(self, G, H):
|
| 95 |
+
"""
|
| 96 |
+
Creates a |N+M| X |N+M| cost matrix between all nodes in
|
| 97 |
+
graphs G and H
|
| 98 |
+
Each cost represents the cost of substituting,
|
| 99 |
+
deleting or inserting a node
|
| 100 |
+
The cost matrix consists of four regions:
|
| 101 |
+
|
| 102 |
+
substitute | insert costs
|
| 103 |
+
-------------------------------
|
| 104 |
+
delete | delete -> delete
|
| 105 |
+
|
| 106 |
+
The delete -> delete region is filled with zeros
|
| 107 |
+
|
| 108 |
+
Parameters
|
| 109 |
+
----------
|
| 110 |
+
G : gmatch4py.Graph
|
| 111 |
+
graph
|
| 112 |
+
H : gmatch4py.Graph
|
| 113 |
+
graph
|
| 114 |
+
|
| 115 |
+
Returns
|
| 116 |
+
-------
|
| 117 |
+
np.array
|
| 118 |
+
cost matrix
|
| 119 |
+
"""
|
| 120 |
+
cdef int n,m
|
| 121 |
+
try:
|
| 122 |
+
n = G.number_of_nodes()
|
| 123 |
+
m = H.number_of_nodes()
|
| 124 |
+
except:
|
| 125 |
+
n = G.size()
|
| 126 |
+
m = H.size()
|
| 127 |
+
cdef np.ndarray cost_matrix = np.zeros((n+m,n+m))
|
| 128 |
+
cdef list nodes1 = list(G.nodes())
|
| 129 |
+
cdef list nodes2 = list(H.nodes())
|
| 130 |
+
cdef int i,j
|
| 131 |
+
for i in range(n):
|
| 132 |
+
for j in range(m):
|
| 133 |
+
cost_matrix[i,j] = self.substitute_cost(nodes1[i], nodes2[j], G, H)
|
| 134 |
+
|
| 135 |
+
for i in range(m):
|
| 136 |
+
for j in range(m):
|
| 137 |
+
cost_matrix[i+n,j] = self.insert_cost(i, j, nodes2, H)
|
| 138 |
+
|
| 139 |
+
for i in range(n):
|
| 140 |
+
for j in range(n):
|
| 141 |
+
cost_matrix[j,i+m] = self.delete_cost(i, j, nodes1, G)
|
| 142 |
+
|
| 143 |
+
return cost_matrix
|
| 144 |
+
|
| 145 |
+
cdef double insert_cost(self, int i, int j, nodesH, H):
|
| 146 |
+
"""
|
| 147 |
+
Return the insert cost of the ith nodes in H
|
| 148 |
+
|
| 149 |
+
Returns
|
| 150 |
+
-------
|
| 151 |
+
int
|
| 152 |
+
insert cost
|
| 153 |
+
"""
|
| 154 |
+
raise NotImplementedError
|
| 155 |
+
|
| 156 |
+
cdef double delete_cost(self, int i, int j, nodesG, G):
|
| 157 |
+
"""
|
| 158 |
+
Return the delete cost of the ith nodes in H
|
| 159 |
+
|
| 160 |
+
Returns
|
| 161 |
+
-------
|
| 162 |
+
int
|
| 163 |
+
delete cost
|
| 164 |
+
"""
|
| 165 |
+
raise NotImplementedError
|
| 166 |
+
|
| 167 |
+
cpdef double substitute_cost(self, node1, node2, G, H):
|
| 168 |
+
"""
|
| 169 |
+
Return the substitute cost of between the node1 in G and the node2 in H
|
| 170 |
+
|
| 171 |
+
Returns
|
| 172 |
+
-------
|
| 173 |
+
int
|
| 174 |
+
substitution cost
|
| 175 |
+
"""
|
| 176 |
+
raise NotImplementedError
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 180 |
+
cdef int n = len(listgs)
|
| 181 |
+
cdef double[:,:] comparison_matrix = np.zeros((n, n))
|
| 182 |
+
listgs=parsenx2graph(listgs,self.node_attr_key,self.edge_attr_key)
|
| 183 |
+
cdef long[:] n_nodes = np.array([g.size() for g in listgs])
|
| 184 |
+
cdef double[:] selected_test = np.array(self.get_selected_array(selected,n))
|
| 185 |
+
cdef int i,j
|
| 186 |
+
cdef float inf=np.inf
|
| 187 |
+
|
| 188 |
+
with nogil, parallel(num_threads=self.cpu_count):
|
| 189 |
+
for i in prange(n,schedule='static'):
|
| 190 |
+
for j in range(n):
|
| 191 |
+
if n_nodes[i]>0 and n_nodes[j]>0 and selected_test[i] == 1 :
|
| 192 |
+
with gil:
|
| 193 |
+
comparison_matrix[i][j] = self.distance_ged(listgs[i],listgs[j])
|
| 194 |
+
else:
|
| 195 |
+
comparison_matrix[i][j] = inf
|
| 196 |
+
#comparison_matrix[j, i] = comparison_matrix[i, j]
|
| 197 |
+
return np.array(comparison_matrix)
|
GMatch4py/source/gmatch4py/ged/bipartite_graph_matching_2.pyx
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
import numpy as np
|
| 3 |
+
cimport numpy as np
|
| 4 |
+
from ..base cimport Base
|
| 5 |
+
from cython.parallel cimport prange,parallel
|
| 6 |
+
from ..helpers.general import parsenx2graph
|
| 7 |
+
cimport cython
|
| 8 |
+
|
| 9 |
+
cdef class BP_2(Base):
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
cdef int node_del
|
| 13 |
+
cdef int node_ins
|
| 14 |
+
cdef int edge_del
|
| 15 |
+
cdef int edge_ins
|
| 16 |
+
|
| 17 |
+
def __init__(self, int node_del=1, int node_ins=1, int edge_del=1, int edge_ins=1):
|
| 18 |
+
"""
|
| 19 |
+
BP_2 Constructor
|
| 20 |
+
|
| 21 |
+
Parameters
|
| 22 |
+
----------
|
| 23 |
+
node_del :int
|
| 24 |
+
Node deletion cost
|
| 25 |
+
node_ins : int
|
| 26 |
+
Node insertion cost
|
| 27 |
+
edge_del : int
|
| 28 |
+
Edge Deletion cost
|
| 29 |
+
edge_ins : int
|
| 30 |
+
Edge Insertion cost
|
| 31 |
+
"""
|
| 32 |
+
Base.__init__(self,1,False)
|
| 33 |
+
self.node_del = node_del
|
| 34 |
+
self.node_ins = node_ins
|
| 35 |
+
self.edge_del = edge_del
|
| 36 |
+
self.edge_ins = edge_ins
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@cython.boundscheck(False)
|
| 40 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 41 |
+
cdef int n = len(listgs)
|
| 42 |
+
cdef list new_gs=parsenx2graph(listgs)
|
| 43 |
+
cdef double[:,:] comparison_matrix = np.zeros((n, n))
|
| 44 |
+
cdef double[:] selected_test = self.get_selected_array(selected,n)
|
| 45 |
+
cdef int i,j
|
| 46 |
+
cdef long[:] n_nodes = np.array([g.size() for g in new_gs])
|
| 47 |
+
cdef long[:] n_edges = np.array([g.density() for g in new_gs])
|
| 48 |
+
|
| 49 |
+
with nogil, parallel(num_threads=self.cpu_count):
|
| 50 |
+
for i in prange(n,schedule='static'):
|
| 51 |
+
for j in range(i,n):
|
| 52 |
+
if n_nodes[i] > 0 and n_nodes[j] > 0 and selected_test[i] == 1:
|
| 53 |
+
with gil:
|
| 54 |
+
comparison_matrix[i, j] = self.bp2(new_gs[i], new_gs[j])
|
| 55 |
+
else:
|
| 56 |
+
comparison_matrix[i, j] = 0
|
| 57 |
+
comparison_matrix[j, i] = comparison_matrix[i, j]
|
| 58 |
+
|
| 59 |
+
return np.array(comparison_matrix)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
cdef double bp2(self, g1, g2):
|
| 63 |
+
"""
|
| 64 |
+
Compute the BP2 similarity value between two `networkx.Graph`
|
| 65 |
+
|
| 66 |
+
Parameters
|
| 67 |
+
----------
|
| 68 |
+
g1 : gmatch4py.Graph
|
| 69 |
+
First Graph
|
| 70 |
+
g2 : gmatch4py.Graph
|
| 71 |
+
Second Graph
|
| 72 |
+
|
| 73 |
+
Returns
|
| 74 |
+
-------
|
| 75 |
+
float
|
| 76 |
+
similarity value
|
| 77 |
+
"""
|
| 78 |
+
return np.min([self.distance_bp2(self.psi(g1,g2)),self.distance_bp2(self.psi(g2,g1))])
|
| 79 |
+
|
| 80 |
+
cdef double distance_bp2(self,e):
|
| 81 |
+
"""
|
| 82 |
+
Return the distance based on the edit path found.
|
| 83 |
+
Parameters
|
| 84 |
+
----------
|
| 85 |
+
e : list
|
| 86 |
+
Contains the edit path costs
|
| 87 |
+
|
| 88 |
+
Returns
|
| 89 |
+
-------
|
| 90 |
+
double
|
| 91 |
+
Return sum of the costs from the edit path
|
| 92 |
+
"""
|
| 93 |
+
return np.sum(e)
|
| 94 |
+
|
| 95 |
+
cdef list psi(self,g1,g2):
|
| 96 |
+
"""
|
| 97 |
+
Return the optimal edit path :math:`\psi` based on BP2 algorithm.
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
Parameters
|
| 101 |
+
----------
|
| 102 |
+
g1 : networkx.Graph
|
| 103 |
+
First Graph
|
| 104 |
+
g2 : networkx.Graph
|
| 105 |
+
Second Graph
|
| 106 |
+
|
| 107 |
+
Returns
|
| 108 |
+
-------
|
| 109 |
+
list
|
| 110 |
+
list containing costs from the optimal edit path
|
| 111 |
+
"""
|
| 112 |
+
cdef list psi_=[]
|
| 113 |
+
cdef list nodes1 = list(g1.nodes())
|
| 114 |
+
cdef list nodes2 = list(g2.nodes())
|
| 115 |
+
for u in nodes1:
|
| 116 |
+
v=None
|
| 117 |
+
for w in nodes2:
|
| 118 |
+
if 2*self.fuv(g1,g2,u,w) < self.fuv(g1,g2,u,None) + self.fuv(g1,g2,None,w)\
|
| 119 |
+
and self.fuv(g1,g2,u,w) < self.fuv(g1,g2,u,v):
|
| 120 |
+
v=w
|
| 121 |
+
psi_.append(self.fuv(g1,g2,u,v))
|
| 122 |
+
if u:
|
| 123 |
+
nodes1= list(set(nodes1).difference(set([u])))
|
| 124 |
+
if v:
|
| 125 |
+
nodes2= list(set(nodes2).difference(set([v])))
|
| 126 |
+
for v in nodes2:
|
| 127 |
+
psi_.append(self.fuv(g1,g2,None,v))
|
| 128 |
+
return psi_
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
cdef float fuv(self, g1, g2, str n1, str n2):
|
| 133 |
+
"""
|
| 134 |
+
Compute the Node Distance function
|
| 135 |
+
Parameters
|
| 136 |
+
----------
|
| 137 |
+
g1 : gmatch4py.Graph
|
| 138 |
+
First graph
|
| 139 |
+
g2 : gmatch4py.Graph
|
| 140 |
+
Second graph
|
| 141 |
+
n1 : int or str
|
| 142 |
+
identifier of the first node
|
| 143 |
+
n2 : int or str
|
| 144 |
+
identifier of the second node
|
| 145 |
+
|
| 146 |
+
Returns
|
| 147 |
+
-------
|
| 148 |
+
float
|
| 149 |
+
node distance
|
| 150 |
+
"""
|
| 151 |
+
if n2 == None: # Del
|
| 152 |
+
return self.node_del + ((self.edge_del / 2.) * g1.degree(n1))
|
| 153 |
+
if n1 == None: # Insert
|
| 154 |
+
return self.node_ins + ((self.edge_ins / 2.) * g2.degree(n2))
|
| 155 |
+
else:
|
| 156 |
+
if n1 == n2:
|
| 157 |
+
return 0
|
| 158 |
+
return (self.node_del + self.node_ins + self.hed_edge(g1, g2, n1, n2)) / 2
|
| 159 |
+
|
| 160 |
+
cdef float hed_edge(self, g1, g2, str n1, str n2):
|
| 161 |
+
"""
|
| 162 |
+
Compute HEDistance between edges of n1 and n2, respectively in g1 and g2
|
| 163 |
+
Parameters
|
| 164 |
+
----------
|
| 165 |
+
g1 : gmatch4py.Graph
|
| 166 |
+
First graph
|
| 167 |
+
g2 : gmatch4py.Graph
|
| 168 |
+
Second graph
|
| 169 |
+
n1 : int or str
|
| 170 |
+
identifier of the first node
|
| 171 |
+
n2 : int or str
|
| 172 |
+
identifier of the second node
|
| 173 |
+
|
| 174 |
+
Returns
|
| 175 |
+
-------
|
| 176 |
+
float
|
| 177 |
+
HEDistance between g1 and g2
|
| 178 |
+
"""
|
| 179 |
+
return self.sum_gpq(g1, n1, g2, n2) + self.sum_gpq(g1, n1, g2, n2)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
cdef float sum_gpq(self, g1, str n1, g2, str n2):
|
| 183 |
+
"""
|
| 184 |
+
Compute Nearest Neighbour Distance between edges around n1 in G1 and edges around n2 in G2
|
| 185 |
+
Parameters
|
| 186 |
+
----------
|
| 187 |
+
g1 : gmatch4py.Graph
|
| 188 |
+
First graph
|
| 189 |
+
g2 : gmatch4py.Graph
|
| 190 |
+
Second graph
|
| 191 |
+
n1 : int or str
|
| 192 |
+
identifier of the first node
|
| 193 |
+
n2 : int or str
|
| 194 |
+
identifier of the second node
|
| 195 |
+
|
| 196 |
+
Returns
|
| 197 |
+
-------
|
| 198 |
+
float
|
| 199 |
+
Nearest Neighbour Distance
|
| 200 |
+
"""
|
| 201 |
+
|
| 202 |
+
#if isinstance(g1, nx.MultiDiGraph):
|
| 203 |
+
cdef list edges1 = g1.get_edges_no(n1) if n1 else []
|
| 204 |
+
cdef list edges2 = g2.get_edges_no(n2) if n2 else []
|
| 205 |
+
|
| 206 |
+
cdef np.ndarray min_sum = np.zeros(len(edges1))
|
| 207 |
+
edges2.extend([None])
|
| 208 |
+
cdef np.ndarray min_i
|
| 209 |
+
for i in range(len(edges1)):
|
| 210 |
+
min_i = np.zeros(len(edges2))
|
| 211 |
+
for j in range(len(edges2)):
|
| 212 |
+
min_i[j] = self.gpq(edges1[i], edges2[j])
|
| 213 |
+
min_sum[i] = np.min(min_i)
|
| 214 |
+
return np.sum(min_sum)
|
| 215 |
+
|
| 216 |
+
cdef float gpq(self, str e1, str e2):
|
| 217 |
+
"""
|
| 218 |
+
Compute the edge distance function
|
| 219 |
+
Parameters
|
| 220 |
+
----------
|
| 221 |
+
e1 : str
|
| 222 |
+
first edge identifier
|
| 223 |
+
e2
|
| 224 |
+
second edge indentifier
|
| 225 |
+
Returns
|
| 226 |
+
-------
|
| 227 |
+
float
|
| 228 |
+
edge distance
|
| 229 |
+
"""
|
| 230 |
+
|
| 231 |
+
if e2 == None: # Del
|
| 232 |
+
return self.edge_del
|
| 233 |
+
if e1 == None: # Insert
|
| 234 |
+
return self.edge_ins
|
| 235 |
+
else:
|
| 236 |
+
if e1 == e2:
|
| 237 |
+
return 0
|
| 238 |
+
return (self.edge_del + self.edge_ins) / 2.
|
GMatch4py/source/gmatch4py/ged/graph_edit_dist.pxd
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
cimport numpy as np
|
| 3 |
+
from .abstract_graph_edit_dist cimport AbstractGraphEditDistance
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
cdef class GraphEditDistance(AbstractGraphEditDistance):
|
| 7 |
+
cpdef object relabel_cost(self, node1, node2, G, H)
|
| 8 |
+
cpdef double substitute_cost(self, node1, node2, G, H)
|
| 9 |
+
cdef double delete_cost(self, int i, int j, nodesG, G)
|
| 10 |
+
cdef double insert_cost(self, int i, int j, nodesH, H)
|
GMatch4py/source/gmatch4py/ged/graph_edit_dist.pyx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: UTF-8 -*-
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
import networkx as nx
|
| 6 |
+
import numpy as np
|
| 7 |
+
cimport numpy as np
|
| 8 |
+
from .abstract_graph_edit_dist cimport AbstractGraphEditDistance
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
cdef class GraphEditDistance(AbstractGraphEditDistance):
|
| 13 |
+
|
| 14 |
+
def __init__(self,node_del,node_ins,edge_del,edge_ins,weighted=False):
|
| 15 |
+
AbstractGraphEditDistance.__init__(self,node_del,node_ins,edge_del,edge_ins)
|
| 16 |
+
self.weighted=weighted
|
| 17 |
+
|
| 18 |
+
cpdef double substitute_cost(self, node1, node2, G, H):
|
| 19 |
+
return self.relabel_cost(node1, node2, G, H)
|
| 20 |
+
|
| 21 |
+
cpdef object relabel_cost(self, node1, node2, G, H):
|
| 22 |
+
## Si deux noeuds égaux
|
| 23 |
+
if node1 == node2 and G.degree(node1) == H.degree(node2):
|
| 24 |
+
return 0.0
|
| 25 |
+
elif node1 == node2 and G.degree(node1) != H.degree(node2):
|
| 26 |
+
#R = Graph(self.add_edges(node1,node2,G),G.get_node_key(),G.get_egde_key())
|
| 27 |
+
#R2 = Graph(self.add_edges(node1,node2,H),H.get_node_key(),H.get_egde_key())
|
| 28 |
+
#inter_= R.size_edge_intersect(R2)
|
| 29 |
+
R=set(G.get_edges_no(node1))
|
| 30 |
+
R2=set(H.get_edges_no(node2))
|
| 31 |
+
inter_=R.intersection(R2)
|
| 32 |
+
add_diff=abs(len(R2)-len(inter_))#abs(R2.density()-inter_)
|
| 33 |
+
del_diff=abs(len(R)-len(inter_))#abs(R.density()-inter_)
|
| 34 |
+
return (add_diff*self.edge_ins)+(del_diff*self.edge_del)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
#si deux noeuds connectés
|
| 38 |
+
if G.has_edge(node1,node2) or G.has_edge(node2,node1):
|
| 39 |
+
return self.node_ins+self.node_del
|
| 40 |
+
if not node2 in G.nodes():
|
| 41 |
+
nodesH=H.nodes()
|
| 42 |
+
index=list(nodesH).index(node2)
|
| 43 |
+
return self.node_del+self.node_ins+self.insert_cost(index,index,nodesH,H)
|
| 44 |
+
return sys.maxsize
|
| 45 |
+
|
| 46 |
+
cdef double delete_cost(self, int i, int j, nodesG, G):
|
| 47 |
+
if i == j:
|
| 48 |
+
return self.node_del+(G.degree(nodesG[i],weight=True)*self.edge_del) # Deleting a node implicate to delete in and out edges
|
| 49 |
+
return sys.maxsize
|
| 50 |
+
|
| 51 |
+
cdef double insert_cost(self, int i, int j, nodesH, H):
|
| 52 |
+
if i == j:
|
| 53 |
+
deg=H.degree(nodesH[j],weight=True)
|
| 54 |
+
if isinstance(deg,dict):deg=0
|
| 55 |
+
return self.node_ins+(deg*self.edge_ins)
|
| 56 |
+
else:
|
| 57 |
+
return sys.maxsize
|
GMatch4py/source/gmatch4py/ged/greedy_edit_distance.pyx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
from .graph_edit_dist cimport GraphEditDistance
|
| 5 |
+
import numpy as np
|
| 6 |
+
cimport numpy as np
|
| 7 |
+
from cython.parallel cimport prange,parallel
|
| 8 |
+
|
| 9 |
+
cdef class GreedyEditDistance(GraphEditDistance):
|
| 10 |
+
"""
|
| 11 |
+
Implementation of the Greedy Edit Distance presented in :
|
| 12 |
+
|
| 13 |
+
Improved quadratic time approximation of graph edit distance by Hausdorff matching and greedy assignement
|
| 14 |
+
Andreas Fischer, Kaspar Riesen, Horst Bunke
|
| 15 |
+
2016
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
def __init__(self,node_del,node_ins,edge_del,edge_ins):
|
| 19 |
+
GraphEditDistance.__init__(self,node_del,node_ins,edge_del,edge_ins)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
cdef list edit_costs(self, G, H):
|
| 23 |
+
cdef np.ndarray cost_matrix=self.create_cost_matrix(G,H)
|
| 24 |
+
cdef np.ndarray cost_matrix_2=cost_matrix.copy().astype(np.double)
|
| 25 |
+
cdef list psi=[]
|
| 26 |
+
for i in range(len(cost_matrix)):
|
| 27 |
+
phi_i=np.argmin(cost_matrix_2[i])
|
| 28 |
+
cost_matrix_2[:,phi_i]=sys.maxsize
|
| 29 |
+
psi.append([i,phi_i]) #+i to compensate the previous column deletion
|
| 30 |
+
return [cost_matrix[psi[i][0]][psi[i][1]] for i in range(len(psi))]
|
GMatch4py/source/gmatch4py/ged/hausdorff_edit_distance.pyx
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
cimport numpy as np
|
| 5 |
+
from ..base cimport Base
|
| 6 |
+
from cython.parallel cimport prange,parallel
|
| 7 |
+
from ..helpers.general import parsenx2graph
|
| 8 |
+
cimport cython
|
| 9 |
+
|
| 10 |
+
cdef class HED(Base):
|
| 11 |
+
"""
|
| 12 |
+
Implementation of Hausdorff Edit Distance described in
|
| 13 |
+
|
| 14 |
+
Improved quadratic time approximation of graph edit distance by Hausdorff matching and greedy assignement
|
| 15 |
+
Andreas Fischer, Kaspar Riesen, Horst Bunke
|
| 16 |
+
2016
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
cdef int node_del
|
| 20 |
+
cdef int node_ins
|
| 21 |
+
cdef int edge_del
|
| 22 |
+
cdef int edge_ins
|
| 23 |
+
|
| 24 |
+
def __init__(self, int node_del=1, int node_ins=1, int edge_del=1, int edge_ins=1):
|
| 25 |
+
"""
|
| 26 |
+
HED Constructor
|
| 27 |
+
|
| 28 |
+
Parameters
|
| 29 |
+
----------
|
| 30 |
+
node_del :int
|
| 31 |
+
Node deletion cost
|
| 32 |
+
node_ins : int
|
| 33 |
+
Node insertion cost
|
| 34 |
+
edge_del : int
|
| 35 |
+
Edge Deletion cost
|
| 36 |
+
edge_ins : int
|
| 37 |
+
Edge Insertion cost
|
| 38 |
+
"""
|
| 39 |
+
Base.__init__(self,1,False)
|
| 40 |
+
self.node_del = node_del
|
| 41 |
+
self.node_ins = node_ins
|
| 42 |
+
self.edge_del = edge_del
|
| 43 |
+
self.edge_ins = edge_ins
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@cython.boundscheck(False)
|
| 47 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 48 |
+
cdef int n = len(listgs)
|
| 49 |
+
cdef list new_gs=parsenx2graph(listgs,self.node_attr_key,self.edge_attr_key)
|
| 50 |
+
cdef double[:,:] comparison_matrix = np.zeros((n, n))
|
| 51 |
+
cdef double[:] selected_test = np.array(self.get_selected_array(selected,n))
|
| 52 |
+
cdef int i,j
|
| 53 |
+
cdef long[:] n_nodes = np.array([g.size() for g in new_gs])
|
| 54 |
+
cdef long[:] n_edges = np.array([g.density() for g in new_gs])
|
| 55 |
+
|
| 56 |
+
with nogil, parallel(num_threads=self.cpu_count):
|
| 57 |
+
for i in prange(n,schedule='static'):
|
| 58 |
+
for j in range(i,n):
|
| 59 |
+
if n_nodes[i] > 0 and n_nodes[j] > 0 and selected_test[i] == True:
|
| 60 |
+
with gil:
|
| 61 |
+
comparison_matrix[i, j] = self.hed(new_gs[i], new_gs[j])
|
| 62 |
+
else:
|
| 63 |
+
comparison_matrix[i, j] = 0
|
| 64 |
+
comparison_matrix[j, i] = comparison_matrix[i, j]
|
| 65 |
+
|
| 66 |
+
return np.array(comparison_matrix)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
cdef float hed(self, g1, g2):
|
| 70 |
+
"""
|
| 71 |
+
Compute the HED similarity value between two `gmatch4py.Graph`
|
| 72 |
+
|
| 73 |
+
Parameters
|
| 74 |
+
----------
|
| 75 |
+
g1 : gmatch4py.Graph
|
| 76 |
+
First Graph
|
| 77 |
+
g2 : gmatch4py.Graph
|
| 78 |
+
Second Graph
|
| 79 |
+
|
| 80 |
+
Returns
|
| 81 |
+
-------
|
| 82 |
+
float
|
| 83 |
+
similarity value
|
| 84 |
+
"""
|
| 85 |
+
return self.sum_fuv(g1, g2) + self.sum_fuv(g2, g1)
|
| 86 |
+
|
| 87 |
+
cdef float sum_fuv(self, g1, g2):
|
| 88 |
+
"""
|
| 89 |
+
Compute Nearest Neighbour Distance between G1 and G2
|
| 90 |
+
Parameters
|
| 91 |
+
----------
|
| 92 |
+
g1 : gmatch4py.Graph
|
| 93 |
+
First graph
|
| 94 |
+
g2 : gmatch4py.Graph
|
| 95 |
+
Second graph
|
| 96 |
+
|
| 97 |
+
Returns
|
| 98 |
+
-------
|
| 99 |
+
float
|
| 100 |
+
Nearest Neighbour Distance
|
| 101 |
+
"""
|
| 102 |
+
|
| 103 |
+
cdef np.ndarray min_sum = np.zeros(g1.size())
|
| 104 |
+
cdef list nodes1 = list(g1.nodes())
|
| 105 |
+
cdef list nodes2 = list(g2.nodes())
|
| 106 |
+
nodes2.extend([None])
|
| 107 |
+
cdef np.ndarray min_i
|
| 108 |
+
for i in range(g1.size()):
|
| 109 |
+
min_i = np.zeros(g2.size())
|
| 110 |
+
for j in range(g2.size()):
|
| 111 |
+
min_i[j] = self.fuv(g1, g2, nodes1[i], nodes2[j])
|
| 112 |
+
min_sum[i] = np.min(min_i)
|
| 113 |
+
return np.sum(min_sum)
|
| 114 |
+
|
| 115 |
+
cdef float fuv(self, g1, g2, str n1, str n2):
|
| 116 |
+
"""
|
| 117 |
+
Compute the Node Distance function
|
| 118 |
+
Parameters
|
| 119 |
+
----------
|
| 120 |
+
g1 : gmatch4py.Graph
|
| 121 |
+
First graph
|
| 122 |
+
g2 : gmatch4py.Graph
|
| 123 |
+
Second graph
|
| 124 |
+
n1 : int or str
|
| 125 |
+
identifier of the first node
|
| 126 |
+
n2 : int or str
|
| 127 |
+
identifier of the second node
|
| 128 |
+
|
| 129 |
+
Returns
|
| 130 |
+
-------
|
| 131 |
+
float
|
| 132 |
+
node distance
|
| 133 |
+
"""
|
| 134 |
+
if n2 == None: # Del
|
| 135 |
+
return self.node_del + ((self.edge_del / 2.) * g1.degree(n1))
|
| 136 |
+
if n1 == None: # Insert
|
| 137 |
+
return self.node_ins + ((self.edge_ins / 2.) * g2.degree(n2))
|
| 138 |
+
else:
|
| 139 |
+
if n1 == n2:
|
| 140 |
+
return 0
|
| 141 |
+
return (self.node_del + self.node_ins + self.hed_edge(g1, g2, n1, n2)) / 2
|
| 142 |
+
|
| 143 |
+
cdef float hed_edge(self, g1, g2, str n1, str n2):
|
| 144 |
+
"""
|
| 145 |
+
Compute HEDistance between edges of n1 and n2, respectively in g1 and g2
|
| 146 |
+
Parameters
|
| 147 |
+
----------
|
| 148 |
+
g1 : gmatch4py.Graph
|
| 149 |
+
First graph
|
| 150 |
+
g2 : gmatch4py.Graph
|
| 151 |
+
Second graph
|
| 152 |
+
n1 : int or str
|
| 153 |
+
identifier of the first node
|
| 154 |
+
n2 : int or str
|
| 155 |
+
identifier of the second node
|
| 156 |
+
|
| 157 |
+
Returns
|
| 158 |
+
-------
|
| 159 |
+
float
|
| 160 |
+
HEDistance between g1 and g2
|
| 161 |
+
"""
|
| 162 |
+
return self.sum_gpq(g1, n1, g2, n2) + self.sum_gpq(g1, n1, g2, n2)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
cdef float sum_gpq(self, g1, str n1, g2, str n2):
|
| 166 |
+
"""
|
| 167 |
+
Compute Nearest Neighbour Distance between edges around n1 in G1 and edges around n2 in G2
|
| 168 |
+
Parameters
|
| 169 |
+
----------
|
| 170 |
+
g1 : gmatch4py.Graph
|
| 171 |
+
First graph
|
| 172 |
+
g2 : gmatch4py.Graph
|
| 173 |
+
Second graph
|
| 174 |
+
n1 : int or str
|
| 175 |
+
identifier of the first node
|
| 176 |
+
n2 : int or str
|
| 177 |
+
identifier of the second node
|
| 178 |
+
|
| 179 |
+
Returns
|
| 180 |
+
-------
|
| 181 |
+
float
|
| 182 |
+
Nearest Neighbour Distance
|
| 183 |
+
"""
|
| 184 |
+
|
| 185 |
+
#if isinstance(g1, nx.MultiDiGraph):
|
| 186 |
+
cdef list edges1 = g1.get_edges_no(n1) if n1 else [] # rename method ...
|
| 187 |
+
cdef list edges2 = g2.get_edges_no(n2) if n2 else []
|
| 188 |
+
|
| 189 |
+
cdef np.ndarray min_sum = np.zeros(len(edges1))
|
| 190 |
+
edges2.extend([None])
|
| 191 |
+
cdef np.ndarray min_i
|
| 192 |
+
for i in range(len(edges1)):
|
| 193 |
+
min_i = np.zeros(len(edges2))
|
| 194 |
+
for j in range(len(edges2)):
|
| 195 |
+
min_i[j] = self.gpq(edges1[i], edges2[j])
|
| 196 |
+
min_sum[i] = np.min(min_i)
|
| 197 |
+
return np.sum(min_sum)
|
| 198 |
+
|
| 199 |
+
cdef float gpq(self, str e1, str e2):
|
| 200 |
+
"""
|
| 201 |
+
Compute the edge distance function
|
| 202 |
+
Parameters
|
| 203 |
+
----------
|
| 204 |
+
e1 : str
|
| 205 |
+
first edge identifier
|
| 206 |
+
e2
|
| 207 |
+
second edge indentifier
|
| 208 |
+
Returns
|
| 209 |
+
-------
|
| 210 |
+
float
|
| 211 |
+
edge distance
|
| 212 |
+
"""
|
| 213 |
+
if e2 == None: # Del
|
| 214 |
+
return self.edge_del
|
| 215 |
+
if e1 == None: # Insert
|
| 216 |
+
return self.edge_ins
|
| 217 |
+
else:
|
| 218 |
+
if e1 == e2:
|
| 219 |
+
return 0
|
| 220 |
+
return (self.edge_del + self.edge_ins) / 2.
|
GMatch4py/source/gmatch4py/graph.pxd
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cimport numpy as np
|
| 2 |
+
|
| 3 |
+
cdef class Graph:
|
| 4 |
+
##################################
|
| 5 |
+
# ATTRIBUTES
|
| 6 |
+
##################################
|
| 7 |
+
|
| 8 |
+
# GRAPH PROPERTY ATTRIBUTES
|
| 9 |
+
###########################
|
| 10 |
+
cdef bint is_directed # If the graph is directed
|
| 11 |
+
cdef bint is_multi # If the graph is a Multi-Graph
|
| 12 |
+
cdef bint is_node_attr
|
| 13 |
+
cdef bint is_edge_attr
|
| 14 |
+
|
| 15 |
+
# ATTR VAL ATTRIBUTES
|
| 16 |
+
#####################
|
| 17 |
+
cdef str node_attr_key # Key that contains the main attr value for a node
|
| 18 |
+
cdef str edge_attr_key # Key that contains the main attr value for an edge
|
| 19 |
+
cdef set unique_node_attr_vals # list
|
| 20 |
+
cdef set unique_edge_attr_vals # list
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
## NODE ATTRIBUTES
|
| 24 |
+
#################
|
| 25 |
+
|
| 26 |
+
cdef list nodes_list # list of nodes ids
|
| 27 |
+
cdef list nodes_attr_list # list of attr value for each node (following nodes list order)
|
| 28 |
+
cdef list nodes_hash # hash representation of every node
|
| 29 |
+
cdef set nodes_hash_set # hash representation of every node (set version for intersection and union operation)
|
| 30 |
+
cdef dict nodes_idx # index of each node in `nodes_list`
|
| 31 |
+
cdef list nodes_weight # list that contains each node's weight (following nodes_list order)
|
| 32 |
+
cdef long[:] nodes_degree # degree list
|
| 33 |
+
cdef long[:] nodes_degree_in # in degree list
|
| 34 |
+
cdef long[:] nodes_degree_out # out degree list
|
| 35 |
+
cdef double[:] nodes_degree_weighted #weighted vers. of nodes_degree
|
| 36 |
+
cdef double[:] nodes_degree_in_weighted #weighted vers. of nodes_degree_in
|
| 37 |
+
cdef double[:] nodes_degree_out_weighted #weighted vers. of nodes_degree_out
|
| 38 |
+
cdef dict degree_per_attr # degree information per attr val
|
| 39 |
+
cdef dict degree_per_attr_weighted # degree information per attr val
|
| 40 |
+
cdef list attr_nodes # list of attr(dict) values for each node
|
| 41 |
+
cdef dict edges_of_nodes # list of egdes connected to each node
|
| 42 |
+
|
| 43 |
+
# EDGES ATTRIBUTES
|
| 44 |
+
##################
|
| 45 |
+
|
| 46 |
+
cdef list edges_list # edge list
|
| 47 |
+
cdef list edges_attr_list # list of attr value for each edge (following nodes list order)
|
| 48 |
+
cdef dict edges_hash_idx # index of hash in edges_list and edges_attr_list
|
| 49 |
+
cdef list edges_hash # hash representation of every edges ## A VOIR !
|
| 50 |
+
cdef set edges_hash_set # set of hash representation of every edges (set version for intersection and union operation)
|
| 51 |
+
cdef dict edges_weight # list that contains each node's weight (following nodes_list order)
|
| 52 |
+
cdef dict edges_hash_map #[id1,[id2,hash]]
|
| 53 |
+
cdef list attr_edges # list of attr(dict) values for each edge
|
| 54 |
+
|
| 55 |
+
# SIZE ATTTRIBUTE
|
| 56 |
+
###############
|
| 57 |
+
|
| 58 |
+
cdef long number_of_nodes # number of nodes
|
| 59 |
+
cdef long number_of_edges # number of edges
|
| 60 |
+
|
| 61 |
+
cdef dict number_of_edges_per_attr # number of nodes per attr value
|
| 62 |
+
cdef dict number_of_nodes_per_attr # number of edges per attr value
|
| 63 |
+
|
| 64 |
+
cdef object nx_g
|
| 65 |
+
|
| 66 |
+
##################################
|
| 67 |
+
# METHODS
|
| 68 |
+
##################################
|
| 69 |
+
|
| 70 |
+
# DIMENSION GETTER
|
| 71 |
+
##################
|
| 72 |
+
cpdef long size(self)
|
| 73 |
+
cpdef int size_attr(self, attr_val)
|
| 74 |
+
|
| 75 |
+
cpdef long density(self)
|
| 76 |
+
cpdef int density_attr(self, str attr_val)
|
| 77 |
+
|
| 78 |
+
# HASH FUNCTION
|
| 79 |
+
###############
|
| 80 |
+
cpdef str hash_node(self,str n1)
|
| 81 |
+
cpdef str hash_edge(self,str n1,str n2)
|
| 82 |
+
cpdef str hash_node_attr(self,str n1, str attr_value)
|
| 83 |
+
cpdef str hash_edge_attr(self,str n1,str n2, str attr_value)
|
| 84 |
+
|
| 85 |
+
## EXIST FUNCTION
|
| 86 |
+
###############
|
| 87 |
+
cpdef bint has_node(self,str n_id)
|
| 88 |
+
cpdef bint has_edge(self,str n_id1,str n_id2)
|
| 89 |
+
|
| 90 |
+
## LEN FUNCTION
|
| 91 |
+
###############
|
| 92 |
+
cpdef int size_node_intersect(self,Graph G)
|
| 93 |
+
cpdef int size_node_union(self,Graph G)
|
| 94 |
+
|
| 95 |
+
cpdef int size_edge_intersect(self,Graph G)
|
| 96 |
+
cpdef int size_edge_union(self,Graph G)
|
| 97 |
+
|
| 98 |
+
# DEGREE FUNCTION
|
| 99 |
+
#################
|
| 100 |
+
cpdef double degree(self,str n_id, bint weight=*)
|
| 101 |
+
cpdef double in_degree(self,str n_id, bint weight=*)
|
| 102 |
+
cpdef double out_degree(self,str n_id, bint weight=*)
|
| 103 |
+
|
| 104 |
+
cpdef double in_degree_attr(self,str n_id,str attr_val, bint weight=*)
|
| 105 |
+
cpdef double out_degree_attr(self,str n_id,str attr_val, bint weight=*)
|
| 106 |
+
cpdef double degree_attr(self,str n_id,str attr_val, bint weight=*)
|
| 107 |
+
|
| 108 |
+
## GETTER
|
| 109 |
+
#########
|
| 110 |
+
|
| 111 |
+
cpdef list get_edges_ed(self,str e1, str e2)
|
| 112 |
+
cpdef list get_edges_no(self,str n)
|
| 113 |
+
cpdef set get_edges_hash(self)
|
| 114 |
+
cpdef set get_nodes_hash(self)
|
| 115 |
+
|
| 116 |
+
cpdef str get_node_key(self)
|
| 117 |
+
cpdef str get_egde_key(self)
|
| 118 |
+
|
| 119 |
+
cpdef dict get_edge_attrs(self,edge_hash)
|
| 120 |
+
cpdef dict get_node_attrs(self, node_hash)
|
| 121 |
+
cpdef dict get_node_attr(self, node_hash)
|
| 122 |
+
cpdef dict get_edge_attr(self,edge_hash)
|
GMatch4py/source/gmatch4py/graph.pyx
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from libcpp.map cimport map
|
| 2 |
+
from libcpp.utility cimport pair
|
| 3 |
+
from libcpp.string cimport string
|
| 4 |
+
from libcpp.vector cimport vector
|
| 5 |
+
import numpy as np
|
| 6 |
+
cimport numpy as np
|
| 7 |
+
import networkx as nx
|
| 8 |
+
|
| 9 |
+
cdef class Graph:
|
| 10 |
+
|
| 11 |
+
def __init__(self,G, node_attr_key="",edge_attr_key=""):
|
| 12 |
+
self.nx_g=G
|
| 13 |
+
|
| 14 |
+
#GRAPH PROPERTY INIT
|
| 15 |
+
self.is_directed = G.is_directed()
|
| 16 |
+
self.is_multi = G.is_multigraph()
|
| 17 |
+
self.is_node_attr=(True if node_attr_key else False)
|
| 18 |
+
self.is_edge_attr=(True if edge_attr_key else False)
|
| 19 |
+
if self.is_multi and not self.is_edge_attr:
|
| 20 |
+
if not len(nx.get_edge_attributes(G,"id")) == len(G.edges(data=True)):
|
| 21 |
+
i=0
|
| 22 |
+
for id1 in G.adj:
|
| 23 |
+
for id2 in G.adj[id1]:
|
| 24 |
+
for id3 in G.adj[id1][id2]:
|
| 25 |
+
G._adj[id1][id2][id3]["id"]=str(i)
|
| 26 |
+
i+=1
|
| 27 |
+
self.is_edge_attr = True
|
| 28 |
+
edge_attr_key = "id"
|
| 29 |
+
|
| 30 |
+
# for ed in
|
| 31 |
+
|
| 32 |
+
#len(nx.get_edge_attributes(G1,"id")) == len(G1.edges(data=True))
|
| 33 |
+
|
| 34 |
+
if len(G) ==0:
|
| 35 |
+
self.__init_empty__()
|
| 36 |
+
|
| 37 |
+
else:
|
| 38 |
+
a,b=list(zip(*list(G.nodes(data=True))))
|
| 39 |
+
self.nodes_list,self.attr_nodes=list(a),list(b)
|
| 40 |
+
if G.number_of_edges()>0:
|
| 41 |
+
e1,e2,d=zip(*list(G.edges(data=True)))
|
| 42 |
+
self.attr_edges=list(d)
|
| 43 |
+
self.edges_list=list(zip(e1,e2))
|
| 44 |
+
else:
|
| 45 |
+
self.edges_list=[]
|
| 46 |
+
self.attr_edges=[]
|
| 47 |
+
|
| 48 |
+
if self.is_node_attr:
|
| 49 |
+
self.node_attr_key = node_attr_key
|
| 50 |
+
self.nodes_attr_list = [attr_dict[node_attr_key] for attr_dict in self.attr_nodes]
|
| 51 |
+
self.unique_node_attr_vals=set(self.nodes_attr_list)
|
| 52 |
+
|
| 53 |
+
if self.is_edge_attr:
|
| 54 |
+
self.edge_attr_key = edge_attr_key
|
| 55 |
+
self.edges_attr_list = [attr_dict[edge_attr_key] for attr_dict in self.attr_edges]
|
| 56 |
+
self.unique_edge_attr_vals=set(self.edges_attr_list)
|
| 57 |
+
|
| 58 |
+
# NODE Information init
|
| 59 |
+
#######################
|
| 60 |
+
|
| 61 |
+
self.nodes_hash=[self.hash_node_attr(node,self.nodes_attr_list[ix]) if self.is_node_attr else self.hash_node(node) for ix, node in enumerate(self.nodes_list) ]
|
| 62 |
+
self.nodes_hash_set=set(self.nodes_hash)
|
| 63 |
+
self.nodes_idx={node:ix for ix, node in enumerate(self.nodes_list)}
|
| 64 |
+
self.nodes_weight=[attr_dict["weight"] if "weight" in attr_dict else 1 for attr_dict in self.attr_nodes]
|
| 65 |
+
degree_all=[]
|
| 66 |
+
degree_in=[]
|
| 67 |
+
degree_out=[]
|
| 68 |
+
|
| 69 |
+
degree_all_weighted=[]
|
| 70 |
+
degree_in_weighted=[]
|
| 71 |
+
degree_out_weighted=[]
|
| 72 |
+
if self.is_edge_attr:
|
| 73 |
+
self.degree_per_attr={attr_v:{n:{"in":0,"out":0} for n in self.nodes_list} for attr_v in self.unique_edge_attr_vals}
|
| 74 |
+
self.degree_per_attr_weighted={attr_v:{n:{"in":0,"out":0} for n in self.nodes_list} for attr_v in self.unique_edge_attr_vals}
|
| 75 |
+
# Retrieving Degree Information
|
| 76 |
+
self.edges_of_nodes={}
|
| 77 |
+
for n in self.nodes_list:
|
| 78 |
+
self.edges_of_nodes[n]=[self.hash_edge_attr(e1,e2,attr_dict[self.edge_attr_key]) if self.is_edge_attr else self.hash_edge(e1,e2) for e1,e2,attr_dict in G.edges(n,data=True)]
|
| 79 |
+
degree_all.append(G.degree(n))
|
| 80 |
+
degree_all_weighted.append(G.degree(n,weight="weight"))
|
| 81 |
+
if self.is_directed:
|
| 82 |
+
degree_in.append(G.in_degree(n))
|
| 83 |
+
degree_in_weighted.append(G.in_degree(n,weight="weight"))
|
| 84 |
+
degree_out.append(G.out_degree(n))
|
| 85 |
+
degree_out_weighted.append(G.out_degree(n))
|
| 86 |
+
else:
|
| 87 |
+
degree_in.append(degree_all[-1])
|
| 88 |
+
degree_in_weighted.append(degree_all_weighted[-1])
|
| 89 |
+
degree_out.append(degree_all[-1])
|
| 90 |
+
degree_out_weighted.append(degree_all_weighted[-1])
|
| 91 |
+
if self.is_edge_attr:
|
| 92 |
+
if self.is_directed:
|
| 93 |
+
in_edge=list(G.in_edges(n,data=True))
|
| 94 |
+
out_edge=list(G.out_edges(n,data=True))
|
| 95 |
+
for n1,n2,attr_dict in in_edge:
|
| 96 |
+
self.degree_per_attr[attr_dict[self.edge_attr_key]][n]["in"]+=1
|
| 97 |
+
self.degree_per_attr_weighted[attr_dict[self.edge_attr_key]][n]["in"]+=1*(attr_dict["weight"] if "weight" in attr_dict else 1 )
|
| 98 |
+
|
| 99 |
+
for n1,n2,attr_dict in out_edge:
|
| 100 |
+
self.degree_per_attr[attr_dict[self.edge_attr_key]][n]["out"]+=1
|
| 101 |
+
self.degree_per_attr_weighted[attr_dict[self.edge_attr_key]][n]["out"]+=1*(attr_dict["weight"] if "weight" in attr_dict else 1 )
|
| 102 |
+
|
| 103 |
+
else:
|
| 104 |
+
edges=G.edges(n,data=True)
|
| 105 |
+
for n1,n2,attr_dict in edges:
|
| 106 |
+
self.degree_per_attr[attr_dict[self.edge_attr_key]][n]["in"]+=1
|
| 107 |
+
self.degree_per_attr[attr_dict[self.edge_attr_key]][n]["out"]+=1
|
| 108 |
+
self.degree_per_attr_weighted[attr_dict[self.edge_attr_key]][n]["in"]+=1*(attr_dict["weight"] if "weight" in attr_dict else 1 )
|
| 109 |
+
self.degree_per_attr_weighted[attr_dict[self.edge_attr_key]][n]["out"]+=1*(attr_dict["weight"] if "weight" in attr_dict else 1 )
|
| 110 |
+
|
| 111 |
+
self.nodes_degree=np.array(degree_all)
|
| 112 |
+
self.nodes_degree_in=np.array(degree_in)
|
| 113 |
+
self.nodes_degree_out=np.array(degree_out)
|
| 114 |
+
|
| 115 |
+
self.nodes_degree_weighted=np.array(degree_all_weighted).astype(np.double)
|
| 116 |
+
self.nodes_degree_in_weighted=np.array(degree_in_weighted).astype(np.double)
|
| 117 |
+
self.nodes_degree_out_weighted=np.array(degree_out_weighted).astype(np.double)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# EDGE INFO INIT
|
| 121 |
+
#################
|
| 122 |
+
|
| 123 |
+
self.edges_hash=[]
|
| 124 |
+
self.edges_hash_map = {}
|
| 125 |
+
self.edges_hash_idx = {}
|
| 126 |
+
for ix, ed in enumerate(self.edges_list):
|
| 127 |
+
e1,e2=ed
|
| 128 |
+
if not e1 in self.edges_hash_map:self.edges_hash_map[e1]={}
|
| 129 |
+
|
| 130 |
+
hash_=self.hash_edge_attr(e1,e2,self.edges_attr_list[ix]) if self.is_edge_attr else self.hash_edge(e1,e2)
|
| 131 |
+
if self.is_multi and self.is_edge_attr:
|
| 132 |
+
if not e2 in self.edges_hash_map[e1]:self.edges_hash_map[e1][e2]={}
|
| 133 |
+
self.edges_hash_map[e1][e2][self.edges_attr_list[ix]]=hash_
|
| 134 |
+
else:
|
| 135 |
+
self.edges_hash_map[e1][e2]=hash_
|
| 136 |
+
self.edges_hash_idx[hash_]=ix
|
| 137 |
+
self.edges_hash.append(hash_)
|
| 138 |
+
self.edges_hash_set=set(self.edges_hash)
|
| 139 |
+
|
| 140 |
+
self.edges_weight={}
|
| 141 |
+
for e1,e2,attr_dict in list(G.edges(data=True)):
|
| 142 |
+
hash_=self.hash_edge_attr(e1,e2,attr_dict[self.edge_attr_key]) if self.is_edge_attr else self.hash_edge(e1,e2)
|
| 143 |
+
self.edges_weight[hash_]=attr_dict["weight"] if "weight" in attr_dict else 1
|
| 144 |
+
|
| 145 |
+
self.number_of_edges = len(self.edges_list)
|
| 146 |
+
self.number_of_nodes = len(self.nodes_list)
|
| 147 |
+
|
| 148 |
+
if self.is_edge_attr and self.number_of_edges >0:
|
| 149 |
+
self.number_of_edges_per_attr={attr:0 for attr in self.unique_edge_attr_vals}
|
| 150 |
+
for _,_,attr_dict in list(G.edges(data=True)):
|
| 151 |
+
self.number_of_edges_per_attr[attr_dict[self.edge_attr_key]]+=1
|
| 152 |
+
|
| 153 |
+
if self.is_node_attr and self.number_of_nodes >0:
|
| 154 |
+
self.number_of_nodes_per_attr={attr:0 for attr in self.unique_node_attr_vals}
|
| 155 |
+
for _,attr_dict in list(G.nodes(data=True)):
|
| 156 |
+
self.number_of_nodes_per_attr[attr_dict[self.node_attr_key]]+=1
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# HASH FUNCTION
|
| 160 |
+
cpdef str hash_node(self,str n1):
|
| 161 |
+
return "{0}".format(n1)
|
| 162 |
+
|
| 163 |
+
cpdef str hash_edge(self,str n1,str n2):
|
| 164 |
+
if not self.is_directed:
|
| 165 |
+
return "_".join(sorted([n1,n2]))
|
| 166 |
+
return "_".join([n1,n2])
|
| 167 |
+
|
| 168 |
+
cpdef str hash_node_attr(self,str n1, str attr_value):
|
| 169 |
+
return "_".join([n1,attr_value])
|
| 170 |
+
|
| 171 |
+
cpdef str hash_edge_attr(self,str n1,str n2, str attr_value):
|
| 172 |
+
if self.is_directed:
|
| 173 |
+
return "_".join([n1,n2,attr_value])
|
| 174 |
+
ed=sorted([n1,n2])
|
| 175 |
+
ed.extend([attr_value])
|
| 176 |
+
return "_".join(ed)
|
| 177 |
+
|
| 178 |
+
## EXIST FUNCTION
|
| 179 |
+
cpdef bint has_node(self,str n_id):
|
| 180 |
+
if n_id in self.nodes_list:
|
| 181 |
+
return True
|
| 182 |
+
return False
|
| 183 |
+
|
| 184 |
+
cpdef bint has_edge(self,str n_id1,str n_id2):
|
| 185 |
+
if self.number_of_edges == 0:
|
| 186 |
+
return False
|
| 187 |
+
if self.is_directed:
|
| 188 |
+
if n_id1 in self.edges_hash_map and n_id2 in self.edges_hash_map[n_id1]:
|
| 189 |
+
return True
|
| 190 |
+
else:
|
| 191 |
+
if n_id1 in self.edges_hash_map and n_id2 in self.edges_hash_map[n_id1]:
|
| 192 |
+
return True
|
| 193 |
+
if n_id2 in self.edges_hash_map and n_id1 in self.edges_hash_map[n_id2]:
|
| 194 |
+
return True
|
| 195 |
+
return False
|
| 196 |
+
|
| 197 |
+
## LEN FUNCTION
|
| 198 |
+
cpdef int size_node_intersect(self,Graph G):
|
| 199 |
+
if self.number_of_nodes == 0:
|
| 200 |
+
return 0
|
| 201 |
+
return len(self.nodes_hash_set.intersection(G.nodes_hash_set))
|
| 202 |
+
cpdef int size_node_union(self,Graph G):
|
| 203 |
+
return len(self.nodes_hash_set.union(G.nodes_hash_set))
|
| 204 |
+
|
| 205 |
+
cpdef int size_edge_intersect(self,Graph G):
|
| 206 |
+
if self.number_of_edges == 0:
|
| 207 |
+
return 0
|
| 208 |
+
return len(self.edges_hash_set.intersection(G.edges_hash_set))
|
| 209 |
+
cpdef int size_edge_union(self,Graph G):
|
| 210 |
+
return len(self.edges_hash_set.union(G.edges_hash_set))
|
| 211 |
+
|
| 212 |
+
## GETTER
|
| 213 |
+
|
| 214 |
+
def get_nx(self):
|
| 215 |
+
return self.nx_g
|
| 216 |
+
|
| 217 |
+
def nodes(self,data=False):
|
| 218 |
+
if data:
|
| 219 |
+
if self.number_of_nodes == 0:
|
| 220 |
+
return [],[]
|
| 221 |
+
return self.nodes_list,self.attr_nodes
|
| 222 |
+
|
| 223 |
+
if self.number_of_nodes == 0:
|
| 224 |
+
return []
|
| 225 |
+
return self.nodes_list
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def edges(self,data=False):
|
| 229 |
+
if data:
|
| 230 |
+
if self.number_of_edges == 0:
|
| 231 |
+
return [],[]
|
| 232 |
+
return self.edges_list,self.attr_edges
|
| 233 |
+
|
| 234 |
+
if self.number_of_edges == 0:
|
| 235 |
+
return []
|
| 236 |
+
return self.edges_list
|
| 237 |
+
|
| 238 |
+
cpdef list get_edges_ed(self,str e1,str e2):
|
| 239 |
+
if self.is_edge_attr:
|
| 240 |
+
hashes=self.edges_hash_map[e1][e2]
|
| 241 |
+
return [(e1,e2,self.edges_attr_list[self.edges_hash_idx[hash_]])for hash_ in hashes]
|
| 242 |
+
|
| 243 |
+
return [(e1,e2,None)]
|
| 244 |
+
|
| 245 |
+
cpdef list get_edges_no(self,str n):
|
| 246 |
+
return self.edges_of_nodes[n]
|
| 247 |
+
|
| 248 |
+
cpdef dict get_edge_attr(self,edge_hash):
|
| 249 |
+
return self.edges_attr_list[self.edges_hash_idx[edge_hash]]
|
| 250 |
+
|
| 251 |
+
cpdef dict get_node_attr(self, node_hash):
|
| 252 |
+
return self.edges_attr_list[self.edges_hash_idx[node_hash]]
|
| 253 |
+
|
| 254 |
+
cpdef dict get_edge_attrs(self,edge_hash):
|
| 255 |
+
return self.attr_edges[self.edges_hash_idx[edge_hash]]
|
| 256 |
+
|
| 257 |
+
cpdef dict get_node_attrs(self, node_hash):
|
| 258 |
+
return self.attr_nodes[self.edges_hash_idx[node_hash]]
|
| 259 |
+
|
| 260 |
+
cpdef set get_edges_hash(self):
|
| 261 |
+
return self.edges_hash_set
|
| 262 |
+
|
| 263 |
+
cpdef set get_nodes_hash(self):
|
| 264 |
+
return self.nodes_hash_set
|
| 265 |
+
|
| 266 |
+
cpdef str get_node_key(self):
|
| 267 |
+
return self.node_attr_key
|
| 268 |
+
|
| 269 |
+
cpdef str get_egde_key(self):
|
| 270 |
+
return self.edge_attr_key
|
| 271 |
+
#####
|
| 272 |
+
|
| 273 |
+
cpdef long size(self):
|
| 274 |
+
return self.number_of_nodes
|
| 275 |
+
|
| 276 |
+
cpdef int size_attr(self, attr_val):
|
| 277 |
+
return self.number_of_nodes_per_attr[attr_val]
|
| 278 |
+
|
| 279 |
+
cpdef long density(self):
|
| 280 |
+
return self.number_of_edges
|
| 281 |
+
|
| 282 |
+
cpdef int density_attr(self, str attr_val):
|
| 283 |
+
return self.number_of_edges_per_attr[attr_val]
|
| 284 |
+
|
| 285 |
+
cpdef double degree(self,str n_id, bint weight=False):
|
| 286 |
+
if weight:
|
| 287 |
+
return self.nodes_degree_weighted[self.nodes_idx[n_id]]
|
| 288 |
+
return self.nodes_degree[self.nodes_idx[n_id]]
|
| 289 |
+
|
| 290 |
+
cpdef double in_degree(self,str n_id, bint weight=False):
|
| 291 |
+
if weight:
|
| 292 |
+
return self.nodes_degree_in_weighted[self.nodes_idx[n_id]]
|
| 293 |
+
return self.nodes_degree_in[self.nodes_idx[n_id]]
|
| 294 |
+
|
| 295 |
+
cpdef double out_degree(self,str n_id, bint weight=False):
|
| 296 |
+
if weight:
|
| 297 |
+
return self.nodes_degree_out_weighted[self.nodes_idx[n_id]]
|
| 298 |
+
return self.nodes_degree_out[self.nodes_idx[n_id]]
|
| 299 |
+
|
| 300 |
+
cpdef double in_degree_attr(self,str n_id,str attr_val, bint weight=False):
|
| 301 |
+
if not self.is_edge_attr and not self.is_directed:
|
| 302 |
+
raise AttributeError("No edge attribute have been defined")
|
| 303 |
+
if weight:
|
| 304 |
+
return self.degree_per_attr_weighted[attr_val][n_id]["in"]
|
| 305 |
+
return self.degree_per_attr[attr_val][n_id]["in"]
|
| 306 |
+
|
| 307 |
+
cpdef double out_degree_attr(self,str n_id,str attr_val, bint weight=False):
|
| 308 |
+
if not self.is_edge_attr and not self.is_directed:
|
| 309 |
+
raise AttributeError("No edge attribute have been defined")
|
| 310 |
+
if weight:
|
| 311 |
+
return self.degree_per_attr_weighted[attr_val][n_id]["out"]
|
| 312 |
+
return self.degree_per_attr[attr_val][n_id]["out"]
|
| 313 |
+
|
| 314 |
+
cpdef double degree_attr(self,str n_id,str attr_val, bint weight=False):
|
| 315 |
+
if not self.is_edge_attr:
|
| 316 |
+
raise AttributeError("No edge attribute have been defined")
|
| 317 |
+
if not self.is_directed:
|
| 318 |
+
if weight:
|
| 319 |
+
return self.degree_per_attr_weighted[attr_val][n_id]["out"]
|
| 320 |
+
return self.degree_per_attr[attr_val][n_id]["out"]
|
| 321 |
+
if weight:
|
| 322 |
+
return self.degree_per_attr_weighted[attr_val][n_id]["in"] + self.degree_per_attr_weighted[attr_val][n_id]["out"]
|
| 323 |
+
return self.degree_per_attr[attr_val][n_id]["out"] + self.degree_per_attr[attr_val][n_id]["in"]
|
| 324 |
+
|
| 325 |
+
#GRAPH SETTER
|
| 326 |
+
def add_node(self,str id_,**kwargs):
|
| 327 |
+
if not self.node_attr_key in kwargs:
|
| 328 |
+
print("Node not added because information lacks")
|
| 329 |
+
return self
|
| 330 |
+
if id_ in self.nodes_idx:
|
| 331 |
+
print("Already in G")
|
| 332 |
+
return self
|
| 333 |
+
G=self.nx_g.copy()
|
| 334 |
+
G.add_node(id_,**kwargs)
|
| 335 |
+
return Graph(G,self.node_attr_key,self.edge_attr_key)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def add_edge(self,str n1,str n2,**kwargs):
|
| 339 |
+
G=self.nx_g.copy()
|
| 340 |
+
G.add_edge(n1,n2,**kwargs)
|
| 341 |
+
return Graph(G,self.node_attr_key,self.edge_attr_key)
|
| 342 |
+
|
| 343 |
+
def remove_node(self,str id_):
|
| 344 |
+
if not id_ in self.nodes_idx:
|
| 345 |
+
print("Already removed in G")
|
| 346 |
+
return self
|
| 347 |
+
G=self.nx_g.copy()
|
| 348 |
+
G.remove_node(id_)
|
| 349 |
+
return Graph(G,self.node_attr_key,self.edge_attr_key)
|
| 350 |
+
|
| 351 |
+
def remove_edge(self,str n1,str n2,**kwargs):
|
| 352 |
+
G=self.nx_g.copy()
|
| 353 |
+
edges=G.edges([n1,n2],data=True)
|
| 354 |
+
if len(edges) == 0:
|
| 355 |
+
return self
|
| 356 |
+
elif len(edges)<2:
|
| 357 |
+
G.remove_edge(n1,n2)
|
| 358 |
+
else:
|
| 359 |
+
if not self.edge_attr_key in kwargs:
|
| 360 |
+
for i in range(len(edges)):
|
| 361 |
+
G.remove_edge(n1,n2,i)
|
| 362 |
+
else:
|
| 363 |
+
key,val,i=self.edge_attr_key, kwargs[self.edge_attr_key],0
|
| 364 |
+
for e1,ed2,attr_dict in edges:
|
| 365 |
+
if attr_dict[key] == val:
|
| 366 |
+
G.remove_edge(n1,n2,i)
|
| 367 |
+
break
|
| 368 |
+
i+=1
|
| 369 |
+
|
| 370 |
+
return Graph(G,self.node_attr_key,self.edge_attr_key)
|
| 371 |
+
|
| 372 |
+
def __init_empty__(self):
|
| 373 |
+
self.nodes_list,self.nodes_attr_list,self.nodes_hash,self.nodes_weight,self.attr_nodes=[],[],[],[],[]
|
| 374 |
+
self.nodes_degree,self.nodes_degree_in,self.nodes_degree_out,self.nodes_degree_weighted,self.nodes_degree_in_weighted,self.nodes_degree_out_weighted=np.array([],dtype=np.long),np.array([],dtype=np.long),np.array([],dtype=np.long),np.array([],dtype=np.double),np.array([],dtype=np.double),np.array([],dtype=np.double)
|
| 375 |
+
self.nodes_idx,self.degree_per_attr,self.degree_per_attr_weighted={},{},{}
|
| 376 |
+
self.nodes_hash_set=set([])
|
| 377 |
+
self.number_of_nodes = 0
|
| 378 |
+
|
| 379 |
+
self.number_of_edges = 0
|
| 380 |
+
self.edges_list=[]
|
| 381 |
+
self.edges_attr_list =[]
|
| 382 |
+
self.edges_hash_idx = {}
|
| 383 |
+
self.edges_hash = []
|
| 384 |
+
self.edges_hash_set= set([])
|
| 385 |
+
self.edges_weight={}
|
| 386 |
+
self.edges_hash_map={}
|
| 387 |
+
self.attr_edges=[]
|
| 388 |
+
|
| 389 |
+
|
GMatch4py/source/gmatch4py/helpers/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
GMatch4py/source/gmatch4py/helpers/general.pyx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ..graph cimport Graph
|
| 2 |
+
import networkx as nx
|
| 3 |
+
|
| 4 |
+
def parsenx2graph(list_gs,node_attr_key="",edge_attr_key=""):
|
| 5 |
+
"""
|
| 6 |
+
Parse list of Networkx graphs into Gmatch4py graph format
|
| 7 |
+
Parameters
|
| 8 |
+
----------
|
| 9 |
+
list_gs : list
|
| 10 |
+
list of graph
|
| 11 |
+
node_attr_key : str
|
| 12 |
+
node attribute used for the hash
|
| 13 |
+
edge_attr_key: str
|
| 14 |
+
edge attribute used for the hash
|
| 15 |
+
|
| 16 |
+
Returns
|
| 17 |
+
-------
|
| 18 |
+
list
|
| 19 |
+
list of gmatch4py.Graph
|
| 20 |
+
"""
|
| 21 |
+
new_gs=[nx.relabel_nodes(g,{node:str(node) for node in list(g.nodes)},copy=True) for g in list_gs]
|
| 22 |
+
new_gs=[Graph(g,node_attr_key,edge_attr_key) for g in new_gs]
|
| 23 |
+
return new_gs
|
GMatch4py/source/gmatch4py/helpers/reader.pyx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
import sys, os, glob, json, re
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from tqdm import tqdm
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
The reader submodule contains high-level function to read and store graphs from various files.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
methods_read_graph={
|
| 14 |
+
"gexf":nx.read_gexf,
|
| 15 |
+
"gml":nx.read_gml,
|
| 16 |
+
"graphml":nx.read_graphml
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
def extract_index(fn):
|
| 20 |
+
"""
|
| 21 |
+
Extract index from filename
|
| 22 |
+
Parameters
|
| 23 |
+
----------
|
| 24 |
+
fn : str
|
| 25 |
+
filename
|
| 26 |
+
|
| 27 |
+
Returns
|
| 28 |
+
-------
|
| 29 |
+
int
|
| 30 |
+
index
|
| 31 |
+
"""
|
| 32 |
+
try:
|
| 33 |
+
return int(re.findall("\d+",fn)[-1])
|
| 34 |
+
except:
|
| 35 |
+
print("No number found !")
|
| 36 |
+
return 0
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def import_dir(directory,format="gexf",numbered=True):
|
| 40 |
+
"""
|
| 41 |
+
Based on a given directory, import all graphs and store them in a list/array
|
| 42 |
+
|
| 43 |
+
Parameters
|
| 44 |
+
----------
|
| 45 |
+
directory : str
|
| 46 |
+
directory path where graphs are stored
|
| 47 |
+
format : str
|
| 48 |
+
graph file format
|
| 49 |
+
numbered
|
| 50 |
+
if graph filename are numbered
|
| 51 |
+
Returns
|
| 52 |
+
-------
|
| 53 |
+
array
|
| 54 |
+
graphs
|
| 55 |
+
"""
|
| 56 |
+
if not os.path.exists(directory):
|
| 57 |
+
raise FileNotFoundError("{0} does not exists".format(directory))
|
| 58 |
+
if not format in methods_read_graph:
|
| 59 |
+
raise NotImplementedError("{0} is not implemented !".format(format))
|
| 60 |
+
|
| 61 |
+
# Retrieve filename
|
| 62 |
+
fns = glob.glob(os.path.join(directory, "*.{0}".format(format)))
|
| 63 |
+
|
| 64 |
+
graphs=[]
|
| 65 |
+
if numbered:
|
| 66 |
+
n=max([extract_index(fn) for fn in fns])
|
| 67 |
+
graphs= [nx.Graph()]*(n+1)
|
| 68 |
+
|
| 69 |
+
association_map, i = {}, 0
|
| 70 |
+
for fn in tqdm(fns,desc="Loading Graphs from {0}".format(directory)):
|
| 71 |
+
if not numbered:
|
| 72 |
+
graphs.append(methods_read_graph[format](fn))
|
| 73 |
+
association_map[fn]=i
|
| 74 |
+
i+=1
|
| 75 |
+
else:
|
| 76 |
+
graphs[extract_index(fn)]=methods_read_graph[format](fn)
|
| 77 |
+
if not numbered:
|
| 78 |
+
return association_map,graphs
|
| 79 |
+
return graphs
|
GMatch4py/source/gmatch4py/jaccard.pyx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
cimport numpy as np
|
| 5 |
+
|
| 6 |
+
from .base cimport Base
|
| 7 |
+
from .helpers.general import parsenx2graph
|
| 8 |
+
from cython.parallel cimport prange,parallel
|
| 9 |
+
cimport cython
|
| 10 |
+
|
| 11 |
+
cdef class Jaccard(Base):
|
| 12 |
+
|
| 13 |
+
def __init__(self):
|
| 14 |
+
Base.__init__(self,0,True)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@cython.boundscheck(False)
|
| 18 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 19 |
+
cdef int n = len(listgs)
|
| 20 |
+
cdef list new_gs=parsenx2graph(listgs,self.node_attr_key,self.edge_attr_key)
|
| 21 |
+
cdef double[:,:] comparison_matrix = np.zeros((n, n))
|
| 22 |
+
cdef long[:] n_nodes = np.array([g.size() for g in new_gs])
|
| 23 |
+
cdef long[:] n_edges = np.array([g.density() for g in new_gs])
|
| 24 |
+
cdef int i,j
|
| 25 |
+
|
| 26 |
+
cdef double[:] selected_test = np.array(self.get_selected_array(selected,n))
|
| 27 |
+
|
| 28 |
+
cdef double[:,:] intersect_len_nodes = np.zeros((n, n))
|
| 29 |
+
cdef double[:,:] intersect_len_edges = np.zeros((n, n))
|
| 30 |
+
cdef double[:,:] union_len_nodes = np.zeros((n, n))
|
| 31 |
+
cdef double[:,:] union_len_edges = np.zeros((n, n))
|
| 32 |
+
for i in range(n):
|
| 33 |
+
for j in range(i,n):
|
| 34 |
+
intersect_len_nodes[i][j]=new_gs[i].size_node_intersect(new_gs[j])
|
| 35 |
+
intersect_len_edges[i][j]=new_gs[i].size_edge_intersect(new_gs[j])#len(set(hash_edges[i]).intersection(hash_edges[j]))
|
| 36 |
+
union_len_nodes[i][j]=new_gs[i].size_node_union(new_gs[j])
|
| 37 |
+
union_len_edges[i][j]=new_gs[i].size_edge_union(new_gs[j])
|
| 38 |
+
with nogil, parallel(num_threads=self.cpu_count):
|
| 39 |
+
for i in prange(n,schedule='static'):
|
| 40 |
+
for j in range(i,n):
|
| 41 |
+
if n_nodes[i] > 0 and n_nodes[j] > 0 and selected_test[i] == 1:
|
| 42 |
+
if union_len_edges[i][j] >0 and union_len_nodes[i][j] >0:
|
| 43 |
+
comparison_matrix[i][j]= \
|
| 44 |
+
(intersect_len_edges[i][j]/union_len_edges[i][j])*\
|
| 45 |
+
(intersect_len_nodes[i][j]/union_len_nodes[i][j])
|
| 46 |
+
|
| 47 |
+
else:
|
| 48 |
+
comparison_matrix[i][j] = 0.
|
| 49 |
+
|
| 50 |
+
comparison_matrix[j][i] = comparison_matrix[i][j]
|
| 51 |
+
|
| 52 |
+
return np.array(comparison_matrix)
|
GMatch4py/source/gmatch4py/kernels/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
GMatch4py/source/gmatch4py/kernels/adjacency.pyx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import networkx as nx
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
def get_adjacency(G1,G2):
|
| 5 |
+
"""
|
| 6 |
+
Return adjacency matrices of two graph based on nodes present in both of them.
|
| 7 |
+
|
| 8 |
+
Parameters
|
| 9 |
+
----------
|
| 10 |
+
G1 : nx.Graph
|
| 11 |
+
first graph
|
| 12 |
+
G2 : nx.Graph
|
| 13 |
+
second graph
|
| 14 |
+
|
| 15 |
+
Returns
|
| 16 |
+
-------
|
| 17 |
+
tuple of np.array
|
| 18 |
+
adjacency matrices of G1 and G2
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
# Extract nodes
|
| 22 |
+
nodes_G1=list(G1.nodes())
|
| 23 |
+
nodes_G2=list(G2.nodes())
|
| 24 |
+
|
| 25 |
+
# Get Adjacency Matrix for each graph
|
| 26 |
+
adj_original_G1 = nx.convert_matrix.to_numpy_matrix(G1,nodes_G1)
|
| 27 |
+
adj_original_G2 = nx.convert_matrix.to_numpy_matrix(G2,nodes_G2)
|
| 28 |
+
|
| 29 |
+
# Get old index
|
| 30 |
+
index_node_G1={node: ix for ix,node in enumerate(nodes_G1)}
|
| 31 |
+
index_node_G2={node: ix for ix,node in enumerate(nodes_G2)}
|
| 32 |
+
|
| 33 |
+
# Building new indices
|
| 34 |
+
nodes_unique = list(set(nodes_G1).union(nodes_G2))
|
| 35 |
+
new_node_index = {node:i for i,node in enumerate(nodes_unique)}
|
| 36 |
+
|
| 37 |
+
n=len(nodes_unique)
|
| 38 |
+
|
| 39 |
+
#Generate new adjacent matrices
|
| 40 |
+
new_adj_G1= np.zeros((n,n))
|
| 41 |
+
new_adj_G2= np.zeros((n,n))
|
| 42 |
+
|
| 43 |
+
# Filling old values
|
| 44 |
+
for n1 in nodes_unique:
|
| 45 |
+
for n2 in nodes_unique:
|
| 46 |
+
if n1 in G1.nodes() and n2 in G1.nodes():
|
| 47 |
+
new_adj_G1[new_node_index[n1],new_node_index[n2]]=adj_original_G1[index_node_G1[n1],index_node_G1[n2]]
|
| 48 |
+
if n1 in G2.nodes() and n2 in G2.nodes():
|
| 49 |
+
new_adj_G2[new_node_index[n1],new_node_index[n2]]=adj_original_G2[index_node_G2[n1],index_node_G2[n2]]
|
| 50 |
+
|
| 51 |
+
return new_adj_G1,new_adj_G2
|
| 52 |
+
|
GMatch4py/source/gmatch4py/kernels/random_walk_kernel.pyx
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
class GeometricRandomWalkKernel():
|
| 7 |
+
__type__ = "sim"
|
| 8 |
+
@staticmethod
|
| 9 |
+
def maxDegree(G):
|
| 10 |
+
degree_sequence = sorted(nx.degree(G).values(), reverse=True) # degree sequence
|
| 11 |
+
|
| 12 |
+
# print "Degree sequence", degree_sequence
|
| 13 |
+
dmax = max(degree_sequence)
|
| 14 |
+
return dmax
|
| 15 |
+
@staticmethod
|
| 16 |
+
def compare(listgs):
|
| 17 |
+
|
| 18 |
+
n = len(listgs)
|
| 19 |
+
comparison_matrix=np.zeros((n,n))
|
| 20 |
+
for i in range(n):
|
| 21 |
+
for j in range(i,n):
|
| 22 |
+
if len(listgs[i]) <1 or len(listgs[j]) <1:
|
| 23 |
+
comparison_matrix[i, j] = 0
|
| 24 |
+
comparison_matrix[j, i] = 0
|
| 25 |
+
continue
|
| 26 |
+
direct_product_graph=nx.tensor_product(listgs[i],listgs[j])
|
| 27 |
+
Ax = nx.adjacency_matrix(direct_product_graph).todense()
|
| 28 |
+
try:
|
| 29 |
+
la = 1/ ((GeometricRandomWalkKernel.maxDegree(direct_product_graph)**2)+1) # lambda value
|
| 30 |
+
except:
|
| 31 |
+
la= pow(1,-6)
|
| 32 |
+
eps = pow(10,-10)
|
| 33 |
+
I=np.identity(Ax.shape[0])
|
| 34 |
+
I_vec=np.ones(Ax.shape[0])
|
| 35 |
+
x=I_vec.copy()
|
| 36 |
+
x_pre=np.zeros(Ax.shape[0])
|
| 37 |
+
c=0
|
| 38 |
+
|
| 39 |
+
while (np.linalg.norm(x-x_pre)) > eps:
|
| 40 |
+
if c > 100:
|
| 41 |
+
break
|
| 42 |
+
x_pre=x
|
| 43 |
+
|
| 44 |
+
x= I_vec + la*np.dot(Ax,x_pre.T)
|
| 45 |
+
c+=1
|
| 46 |
+
comparison_matrix[i,j]=np.sum(x)
|
| 47 |
+
comparison_matrix[j,i]=comparison_matrix[i,j]
|
| 48 |
+
print(comparison_matrix)
|
| 49 |
+
for i in range(n):
|
| 50 |
+
for j in range(i,n):
|
| 51 |
+
comparison_matrix[i,j] = (comparison_matrix[i,j]/np.sqrt(comparison_matrix[i,i]*comparison_matrix[j,j]))
|
| 52 |
+
comparison_matrix[j,i]=comparison_matrix[i,j]
|
| 53 |
+
return comparison_matrix
|
| 54 |
+
|
| 55 |
+
class KStepRandomWalkKernel():
|
| 56 |
+
__type__ = "sim"
|
| 57 |
+
@staticmethod
|
| 58 |
+
def maxDegree(G):
|
| 59 |
+
degree_sequence = sorted(nx.degree(G).values(), reverse=True) # degree sequence
|
| 60 |
+
# print "Degree sequence", degree_sequence
|
| 61 |
+
dmax = max(degree_sequence)
|
| 62 |
+
return dmax
|
| 63 |
+
@staticmethod
|
| 64 |
+
def compare(listgs,lambda_list=[1,1,1]):
|
| 65 |
+
k=len(lambda_list)
|
| 66 |
+
if not len(lambda_list) == k:
|
| 67 |
+
raise AttributeError
|
| 68 |
+
n = len(listgs)
|
| 69 |
+
comparison_matrix=np.zeros((n,n))
|
| 70 |
+
for i in range(n):
|
| 71 |
+
for j in range(i,n):
|
| 72 |
+
if len(listgs[i]) <1 or len(listgs[j]) <1:
|
| 73 |
+
comparison_matrix[i, j] = 0
|
| 74 |
+
comparison_matrix[j, i] = 0
|
| 75 |
+
continue
|
| 76 |
+
direct_product_graph=nx.tensor_product(listgs[i],listgs[j])
|
| 77 |
+
Ax = nx.adjacency_matrix(direct_product_graph).todense()
|
| 78 |
+
eps = pow(10,-10)
|
| 79 |
+
I=np.identity(Ax.shape[0])
|
| 80 |
+
ax_pow = I.copy()
|
| 81 |
+
sum_ = lambda_list[0] * I
|
| 82 |
+
for kk in range(1, k):
|
| 83 |
+
ax_pow *= Ax
|
| 84 |
+
sum_ += lambda_list[kk] * ax_pow
|
| 85 |
+
|
| 86 |
+
comparison_matrix[i, j] = np.sum(sum_)/(len(listgs[i])**2 * len(listgs[j])**2)
|
| 87 |
+
comparison_matrix[j,i] = comparison_matrix[i,j]
|
| 88 |
+
|
| 89 |
+
for i in range(n):
|
| 90 |
+
for j in range(i,n):
|
| 91 |
+
comparison_matrix[i,j] = comparison_matrix[i,j]/np.sqrt(comparison_matrix[i,i]*comparison_matrix[j,j])
|
| 92 |
+
comparison_matrix[j,i]=comparison_matrix[i,j]
|
| 93 |
+
return comparison_matrix
|
GMatch4py/source/gmatch4py/kernels/shortest_path_kernel.pyx
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
"""
|
| 4 |
+
Shortest-Path graph kernel.
|
| 5 |
+
Python implementation based on: "Shortest-path kernels on graphs", by
|
| 6 |
+
Borgwardt, K.M.; Kriegel, H.-P., in Data Mining, Fifth IEEE
|
| 7 |
+
International Conference on , vol., no., pp.8 pp.-, 27-30 Nov. 2005
|
| 8 |
+
doi: 10.1109/ICDM.2005.132
|
| 9 |
+
Author : Sandro Vega-Pons, Emanuele Olivetti
|
| 10 |
+
Modified by : Jacques Fize
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import networkx as nx
|
| 14 |
+
import numpy as np
|
| 15 |
+
cimport numpy as np
|
| 16 |
+
from scipy.sparse.csgraph import floyd_warshall
|
| 17 |
+
from .adjacency import get_adjacency
|
| 18 |
+
from cython.parallel cimport prange,parallel
|
| 19 |
+
from ..helpers.general import parsenx2graph
|
| 20 |
+
from ..base cimport Base
|
| 21 |
+
cimport cython
|
| 22 |
+
|
| 23 |
+
cdef class ShortestPathGraphKernel(Base):
|
| 24 |
+
"""
|
| 25 |
+
Shorthest path graph kernel.
|
| 26 |
+
"""
|
| 27 |
+
def __init__(self):
|
| 28 |
+
Base.__init__(self,0,False)
|
| 29 |
+
|
| 30 |
+
def compare_two(self,g_1, g_2):
|
| 31 |
+
"""Compute the kernel value (similarity) between two graphs.
|
| 32 |
+
Parameters
|
| 33 |
+
----------
|
| 34 |
+
g1 : networkx.Graph
|
| 35 |
+
First graph.
|
| 36 |
+
g2 : networkx.Graph
|
| 37 |
+
Second graph.
|
| 38 |
+
Returns
|
| 39 |
+
-------
|
| 40 |
+
k : The similarity value between g1 and g2.
|
| 41 |
+
"""
|
| 42 |
+
# Diagonal superior matrix of the floyd warshall shortest
|
| 43 |
+
# paths:
|
| 44 |
+
if isinstance(g_1,nx.Graph) and isinstance(g_2,nx.Graph):
|
| 45 |
+
g_1,g_2= get_adjacency(g_1,g_2)
|
| 46 |
+
|
| 47 |
+
fwm1 = np.array(floyd_warshall(g_1))
|
| 48 |
+
fwm1[np.isinf(fwm1)] = 0
|
| 49 |
+
fwm1[np.isnan(fwm1)] = 0
|
| 50 |
+
fwm1 = np.triu(fwm1, k=1)
|
| 51 |
+
bc1 = np.bincount(fwm1.reshape(-1).astype(int))
|
| 52 |
+
|
| 53 |
+
fwm2 = np.array(floyd_warshall(g_2))
|
| 54 |
+
fwm2[np.isinf(fwm2)] = 0
|
| 55 |
+
fwm2[np.isnan(fwm2)] = 0
|
| 56 |
+
fwm2 = np.triu(fwm2, k=1)
|
| 57 |
+
bc2 = np.bincount(fwm2.reshape(-1).astype(int))
|
| 58 |
+
|
| 59 |
+
# Copy into arrays with the same length the non-zero shortests
|
| 60 |
+
# paths:
|
| 61 |
+
v1 = np.zeros(max(len(bc1), len(bc2)) - 1)
|
| 62 |
+
v1[range(0, len(bc1)-1)] = bc1[1:]
|
| 63 |
+
|
| 64 |
+
v2 = np.zeros(max(len(bc1), len(bc2)) - 1)
|
| 65 |
+
v2[range(0, len(bc2)-1)] = bc2[1:]
|
| 66 |
+
|
| 67 |
+
return np.sum(v1 * v2)
|
| 68 |
+
|
| 69 |
+
@cython.boundscheck(False)
|
| 70 |
+
cpdef np.ndarray compare(self,list graph_list, list selected):
|
| 71 |
+
"""Compute the all-pairs kernel values for a list of graphs.
|
| 72 |
+
This function can be used to directly compute the kernel
|
| 73 |
+
matrix for a list of graphs. The direct computation of the
|
| 74 |
+
kernel matrix is faster than the computation of all individual
|
| 75 |
+
pairwise kernel values.
|
| 76 |
+
Parameters
|
| 77 |
+
----------
|
| 78 |
+
graph_list: list
|
| 79 |
+
A list of graphs (list of networkx graphs)
|
| 80 |
+
Return
|
| 81 |
+
------
|
| 82 |
+
K: numpy.array, shape = (len(graph_list), len(graph_list))
|
| 83 |
+
The similarity matrix of all graphs in graph_list.
|
| 84 |
+
"""
|
| 85 |
+
cdef int n = len(graph_list)
|
| 86 |
+
cdef double[:,:] k = np.zeros((n, n))
|
| 87 |
+
cdef int cpu_count = self.cpu_count
|
| 88 |
+
cdef int i,j
|
| 89 |
+
cdef list adjacency_matrices = [[None for i in range(n)]for j in range(n)]
|
| 90 |
+
|
| 91 |
+
for i in range(n):
|
| 92 |
+
for j in range(i, n):
|
| 93 |
+
adjacency_matrices[i][j] = get_adjacency(graph_list[i],graph_list[j])
|
| 94 |
+
adjacency_matrices[j][i] = adjacency_matrices[i][j]
|
| 95 |
+
|
| 96 |
+
with nogil, parallel(num_threads=cpu_count):
|
| 97 |
+
for i in prange(n,schedule='static'):
|
| 98 |
+
for j in range(i, n):
|
| 99 |
+
with gil:
|
| 100 |
+
if len(graph_list[i]) > 0 and len(graph_list[j]) >0:
|
| 101 |
+
a,b=adjacency_matrices[i][j]
|
| 102 |
+
k[i][j] = self.compare_two(a,b)
|
| 103 |
+
k[j][i] = k[i][j]
|
| 104 |
+
|
| 105 |
+
k_norm = np.zeros((n,n))
|
| 106 |
+
for i in range(n):
|
| 107 |
+
for j in range(i,n):
|
| 108 |
+
k_norm[i, j] = k[i][j] / np.sqrt(k[i][i] * k[j][j])
|
| 109 |
+
k_norm[j, i] = k_norm[i, j]
|
| 110 |
+
|
| 111 |
+
return np.nan_to_num(k_norm)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
cdef class ShortestPathGraphKernelDotCostMatrix(ShortestPathGraphKernel):
|
| 116 |
+
"""
|
| 117 |
+
Instead of just multiply the count of distance values fou,d between nodes of each graph, this version propose to multiply the node distance matrix generated from each graph.
|
| 118 |
+
"""
|
| 119 |
+
def __init__(self):
|
| 120 |
+
ShortestPathGraphKernel.__init__(self)
|
| 121 |
+
|
| 122 |
+
def compare_two(self,g_1, g_2):
|
| 123 |
+
"""Compute the kernel value (similarity) between two graphs.
|
| 124 |
+
Parameters
|
| 125 |
+
----------
|
| 126 |
+
g1 : networkx.Graph
|
| 127 |
+
First graph.
|
| 128 |
+
g2 : networkx.Graph
|
| 129 |
+
Second graph.
|
| 130 |
+
Returns
|
| 131 |
+
-------
|
| 132 |
+
k : The similarity value between g1 and g2.
|
| 133 |
+
"""
|
| 134 |
+
# Diagonal superior matrix of the floyd warshall shortest
|
| 135 |
+
# paths:
|
| 136 |
+
if isinstance(g_1,nx.Graph) and isinstance(g_2,nx.Graph):
|
| 137 |
+
g_1,g_2= get_adjacency(g_1,g_2)
|
| 138 |
+
|
| 139 |
+
fwm1 = np.array(floyd_warshall(g_1))
|
| 140 |
+
fwm1[np.isinf(fwm1)] = 0
|
| 141 |
+
fwm1[np.isnan(fwm1)] = 0
|
| 142 |
+
fwm1 = np.triu(fwm1, k=1)
|
| 143 |
+
|
| 144 |
+
fwm2 = np.array(floyd_warshall(g_2))
|
| 145 |
+
fwm2[np.isinf(fwm2)] = 0
|
| 146 |
+
fwm2[np.isnan(fwm2)] = 0
|
| 147 |
+
fwm2 = np.triu(fwm2, k=1)
|
| 148 |
+
|
| 149 |
+
return np.sum(fwm1 * fwm2)
|
GMatch4py/source/gmatch4py/kernels/weisfeiler_lehman.pyx
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
"""Weisfeiler_Lehman graph kernel.
|
| 4 |
+
|
| 5 |
+
Python implementation based on: "Weisfeiler-Lehman Graph Kernels", by:
|
| 6 |
+
Nino Shervashidze, Pascal Schweitzer, Erik J. van Leeuwen, Kurt
|
| 7 |
+
Mehlhorn, Karsten M. Borgwardt, JMLR, 2012.
|
| 8 |
+
http://jmlr.csail.mit.edu/papers/v12/shervashidze11a.html
|
| 9 |
+
|
| 10 |
+
Author : Sandro Vega-Pons, Emanuele Olivetti
|
| 11 |
+
Source : https://github.com/emanuele/jstsp2015/blob/master/gk_weisfeiler_lehman.py
|
| 12 |
+
Modified by : Jacques Fize
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import copy
|
| 16 |
+
|
| 17 |
+
import networkx as nx
|
| 18 |
+
import numpy as np
|
| 19 |
+
cimport numpy as np
|
| 20 |
+
from ..base cimport Base
|
| 21 |
+
from ..base import minmax_scale
|
| 22 |
+
from scipy.sparse import csc_matrix,lil_matrix
|
| 23 |
+
|
| 24 |
+
cdef class WeisfeleirLehmanKernel(Base):
|
| 25 |
+
|
| 26 |
+
cdef int h
|
| 27 |
+
|
| 28 |
+
def __init__(self,h=2):
|
| 29 |
+
Base.__init__(self,0,True)
|
| 30 |
+
self.h=h
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
cpdef np.ndarray compare(self,list graph_list, list selected):
|
| 34 |
+
"""Compute the all-pairs kernel values for a list of graphs.
|
| 35 |
+
This function can be used to directly compute the kernel
|
| 36 |
+
matrix for a list of graphs. The direct computation of the
|
| 37 |
+
kernel matrix is faster than the computation of all individual
|
| 38 |
+
pairwise kernel values.
|
| 39 |
+
Parameters
|
| 40 |
+
----------
|
| 41 |
+
graph_list: list
|
| 42 |
+
A list of graphs (list of networkx graphs)
|
| 43 |
+
h : interger
|
| 44 |
+
Number of iterations.
|
| 45 |
+
node_label : boolean
|
| 46 |
+
Whether to use original node labels. True for using node labels
|
| 47 |
+
saved in the attribute 'node_label'. False for using the node
|
| 48 |
+
degree of each node as node attribute.
|
| 49 |
+
Return
|
| 50 |
+
------
|
| 51 |
+
K: numpy.array, shape = (len(graph_list), len(graph_list))
|
| 52 |
+
The similarity matrix of all graphs in graph_list.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
cdef int n = len(graph_list)
|
| 56 |
+
cdef int n_nodes = 0
|
| 57 |
+
cdef int n_max = 0
|
| 58 |
+
cdef int i,j
|
| 59 |
+
# Compute adjacency lists and n_nodes, the total number of
|
| 60 |
+
# nodes in the dataset.
|
| 61 |
+
for i in range(n):
|
| 62 |
+
n_nodes += graph_list[i].number_of_nodes()
|
| 63 |
+
|
| 64 |
+
# Computing the maximum number of nodes in the graphs. It
|
| 65 |
+
# will be used in the computation of vectorial
|
| 66 |
+
# representation.
|
| 67 |
+
if n_max < graph_list[i].number_of_nodes():
|
| 68 |
+
n_max = graph_list[i].number_of_nodes()
|
| 69 |
+
|
| 70 |
+
phi = np.zeros((n_nodes, n), dtype=np.uint64)
|
| 71 |
+
phi=lil_matrix(phi)
|
| 72 |
+
|
| 73 |
+
# INITIALIZATION: initialize the nodes labels for each graph
|
| 74 |
+
# with their labels or with degrees (for unlabeled graphs)
|
| 75 |
+
|
| 76 |
+
cdef list labels = [0] * n
|
| 77 |
+
cdef dict label_lookup = {}
|
| 78 |
+
cdef int label_counter = 0
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# label_lookup is an associative array, which will contain the
|
| 82 |
+
# mapping from multiset labels (strings) to short labels
|
| 83 |
+
# (integers)
|
| 84 |
+
|
| 85 |
+
cdef list nodes
|
| 86 |
+
for i in range(n):
|
| 87 |
+
nodes = list(graph_list[i].nodes)
|
| 88 |
+
# It is assumed that the graph has an attribute
|
| 89 |
+
# 'node_label'
|
| 90 |
+
labels[i] = np.zeros(len(nodes), dtype=np.int32)
|
| 91 |
+
|
| 92 |
+
for j in range(len(nodes)):
|
| 93 |
+
if not (nodes[j] in label_lookup):
|
| 94 |
+
label_lookup[nodes[j]] = str(label_counter)
|
| 95 |
+
labels[i][j] = label_counter
|
| 96 |
+
label_counter += 1
|
| 97 |
+
else:
|
| 98 |
+
labels[i][j] = label_lookup[nodes[j]]
|
| 99 |
+
# labels are associated to a natural number
|
| 100 |
+
# starting with 0.
|
| 101 |
+
|
| 102 |
+
phi[labels[i][j], i] += 1
|
| 103 |
+
|
| 104 |
+
graph_list[i]=nx.relabel_nodes(graph_list[i],label_lookup)
|
| 105 |
+
|
| 106 |
+
# cdef np.ndarray[np.float64_t] k
|
| 107 |
+
k = np.dot(phi.transpose(), phi)
|
| 108 |
+
# MAIN LOOP
|
| 109 |
+
cdef int it = 0
|
| 110 |
+
|
| 111 |
+
new_labels = copy.deepcopy(labels) # Can't work without it !!!
|
| 112 |
+
|
| 113 |
+
while it < self.h:
|
| 114 |
+
# create an empty lookup table
|
| 115 |
+
label_lookup = {}
|
| 116 |
+
label_counter = 0
|
| 117 |
+
|
| 118 |
+
phi = np.zeros((n_nodes, n))
|
| 119 |
+
for i in range(n):
|
| 120 |
+
nodes = list(graph_list[i].nodes)
|
| 121 |
+
for v in range(len(nodes)):
|
| 122 |
+
# form a multiset label of the node v of the i'th graph
|
| 123 |
+
# and convert it to a string
|
| 124 |
+
|
| 125 |
+
long_label = []
|
| 126 |
+
long_label.extend(nx.neighbors(graph_list[i],nodes[v]))
|
| 127 |
+
|
| 128 |
+
long_label_string = "".join(long_label)
|
| 129 |
+
# if the multiset label has not yet occurred, add it to the
|
| 130 |
+
# lookup table and assign a number to it
|
| 131 |
+
if not (long_label_string in label_lookup):
|
| 132 |
+
label_lookup[long_label_string] = str(label_counter)
|
| 133 |
+
new_labels[i][v] = label_counter
|
| 134 |
+
label_counter += 1
|
| 135 |
+
else:
|
| 136 |
+
new_labels[i][v] = label_lookup[long_label_string]
|
| 137 |
+
# fill the column for i'th graph in phi
|
| 138 |
+
aux = np.bincount(new_labels[i])
|
| 139 |
+
phi[new_labels[i], i] += aux[new_labels[i]]
|
| 140 |
+
|
| 141 |
+
k += np.dot(phi.transpose(), phi)
|
| 142 |
+
it = it + 1
|
| 143 |
+
|
| 144 |
+
return np.ma.getdata(minmax_scale(k))
|
GMatch4py/source/gmatch4py/mcs.pyx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
import numpy as np
|
| 3 |
+
cimport numpy as np
|
| 4 |
+
from .graph cimport Graph
|
| 5 |
+
from .base cimport Base
|
| 6 |
+
from cython.parallel cimport prange,parallel
|
| 7 |
+
from .helpers.general import parsenx2graph
|
| 8 |
+
cimport cython
|
| 9 |
+
|
| 10 |
+
cdef class MCS(Base):
|
| 11 |
+
"""
|
| 12 |
+
*A graph distance metric based on the maximal common subgraph, H. Bunke and K. Shearer,
|
| 13 |
+
Pattern Recognition Letters, 1998*
|
| 14 |
+
"""
|
| 15 |
+
def __init__(self):
|
| 16 |
+
Base.__init__(self,0,True)
|
| 17 |
+
|
| 18 |
+
@cython.boundscheck(False)
|
| 19 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 20 |
+
cdef int n = len(listgs)
|
| 21 |
+
cdef double [:,:] comparison_matrix = np.zeros((n, n))
|
| 22 |
+
cdef double[:] selected_test = np.array(self.get_selected_array(selected,n))
|
| 23 |
+
cdef list new_gs=parsenx2graph(listgs,self.node_attr_key,self.edge_attr_key)
|
| 24 |
+
cdef long[:] n_nodes = np.array([g.size() for g in new_gs])
|
| 25 |
+
cdef double [:,:] intersect_len_nodes = np.zeros((n, n))
|
| 26 |
+
cdef int i,j
|
| 27 |
+
for i in range(n):
|
| 28 |
+
for j in range(i,n):
|
| 29 |
+
intersect_len_nodes[i][j]=new_gs[i].size_node_intersect(new_gs[j])
|
| 30 |
+
|
| 31 |
+
with nogil, parallel(num_threads=self.cpu_count):
|
| 32 |
+
for i in prange(n,schedule='static'):
|
| 33 |
+
for j in range(i, n):
|
| 34 |
+
if n_nodes[i] > 0 and n_nodes[j] > 0 and selected_test[i] == 1:
|
| 35 |
+
comparison_matrix[i][j] = intersect_len_nodes[i][j]/max(n_nodes[i],n_nodes[j])
|
| 36 |
+
else:
|
| 37 |
+
comparison_matrix[i][j] = 0.
|
| 38 |
+
if i==j:
|
| 39 |
+
comparison_matrix[i][j]=1
|
| 40 |
+
comparison_matrix[j][i] = comparison_matrix[i][j]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
return np.array(comparison_matrix)
|
| 44 |
+
|
| 45 |
+
|
GMatch4py/source/gmatch4py/vertex_edge_overlap.pyx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
cimport numpy as np
|
| 5 |
+
|
| 6 |
+
from .graph cimport Graph
|
| 7 |
+
from cython.parallel cimport prange,parallel
|
| 8 |
+
from .helpers.general import parsenx2graph
|
| 9 |
+
cimport cython
|
| 10 |
+
from .base cimport Base
|
| 11 |
+
|
| 12 |
+
cdef class VertexEdgeOverlap(Base):
|
| 13 |
+
|
| 14 |
+
"""
|
| 15 |
+
Vertex/Edge Overlap Algorithm
|
| 16 |
+
presented in Web graph similarity for anomaly detection, Journal of Internet Services and Applications, 2008
|
| 17 |
+
by P. Papadimitriou, A. Dasdan and H.Gracia-Molina
|
| 18 |
+
|
| 19 |
+
Code Author : Jacques Fize
|
| 20 |
+
"""
|
| 21 |
+
def __init__(self):
|
| 22 |
+
Base.__init__(self,0,True)
|
| 23 |
+
|
| 24 |
+
@cython.boundscheck(False)
|
| 25 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 26 |
+
cdef int n = len(listgs)
|
| 27 |
+
cdef list new_gs=parsenx2graph(listgs,self.node_attr_key,self.edge_attr_key)
|
| 28 |
+
cdef double[:,:] comparison_matrix = np.zeros((n, n))
|
| 29 |
+
cdef int denom,i,j
|
| 30 |
+
cdef long[:] n_nodes = np.array([g.size() for g in new_gs])
|
| 31 |
+
cdef long[:] n_edges = np.array([g.density() for g in new_gs])
|
| 32 |
+
|
| 33 |
+
cdef double[:] selected_test = np.array(self.get_selected_array(selected,n))
|
| 34 |
+
|
| 35 |
+
cdef double[:,:] intersect_len_nodes = np.zeros((n, n))
|
| 36 |
+
cdef double[:,:] intersect_len_edges = np.zeros((n, n))
|
| 37 |
+
for i in range(n):
|
| 38 |
+
for j in range(i,n):
|
| 39 |
+
intersect_len_nodes[i][j]=new_gs[i].size_node_intersect(new_gs[j])
|
| 40 |
+
intersect_len_edges[i][j]=new_gs[i].size_edge_intersect(new_gs[j])#len(set(hash_edges[i]).intersection(hash_edges[j]))
|
| 41 |
+
|
| 42 |
+
with nogil, parallel(num_threads=self.cpu_count):
|
| 43 |
+
for i in prange(n,schedule='static'):
|
| 44 |
+
for j in range(i,n):
|
| 45 |
+
if n_nodes[i] > 0 and n_nodes[j] > 0 and selected_test[i] == 1:
|
| 46 |
+
denom=n_nodes[i]+n_nodes[j]+\
|
| 47 |
+
n_edges[i]+n_edges[j]
|
| 48 |
+
if denom > 0:
|
| 49 |
+
comparison_matrix[i][j]=(2*(intersect_len_nodes[i][j]
|
| 50 |
+
+intersect_len_edges[i][j]))/denom # Data = True --> For nx.MultiDiGraph
|
| 51 |
+
if i==j:
|
| 52 |
+
comparison_matrix[i][j]=1
|
| 53 |
+
comparison_matrix[j][i] = comparison_matrix[i][j]
|
| 54 |
+
return np.array(comparison_matrix)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
|
GMatch4py/source/gmatch4py/vertex_ranking.pyx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding = utf-8
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
import numpy as np
|
| 5 |
+
cimport numpy as np
|
| 6 |
+
from scipy.stats import spearmanr
|
| 7 |
+
|
| 8 |
+
from .base cimport Base
|
| 9 |
+
|
| 10 |
+
cdef class VertexRanking(Base):
|
| 11 |
+
"""
|
| 12 |
+
Vertex Ranking
|
| 13 |
+
presented in Web graph similarity for anomaly detection, Journal of Internet Services and Applications, 2008 # Maybe not ??
|
| 14 |
+
by P. Papadimitriou, A. Dasdan and H.Gracia-Molina
|
| 15 |
+
|
| 16 |
+
Code Author : Jacques Fize
|
| 17 |
+
|
| 18 |
+
"""
|
| 19 |
+
def __init__(self):
|
| 20 |
+
Base.__init__(self,0,True)
|
| 21 |
+
|
| 22 |
+
cpdef np.ndarray compare(self,list listgs, list selected):
|
| 23 |
+
cdef int n,i,j # number of graphs
|
| 24 |
+
n = len(listgs)
|
| 25 |
+
|
| 26 |
+
cdef np.ndarray comparison_matrix = np.zeros((n,n)) #similarity matrix
|
| 27 |
+
cdef list X,Y,pager_i,pager_j,page_r,node_intersection #temp data (page rank data for the most part)
|
| 28 |
+
page_r=[nx.pagerank(nx.DiGraph(g)) for g in listgs]
|
| 29 |
+
for i in range(n):
|
| 30 |
+
pager_i=list(page_r[i])
|
| 31 |
+
for j in range(i,n):
|
| 32 |
+
g1,g2=listgs[i],listgs[j]
|
| 33 |
+
f=self.isAccepted(g1,i,selected)
|
| 34 |
+
pager_j=list(page_r[j])
|
| 35 |
+
node_intersection=list(set(pager_i) & set(pager_j))
|
| 36 |
+
X,Y=[],[]
|
| 37 |
+
for node in node_intersection:
|
| 38 |
+
X.append(page_r[i][node])
|
| 39 |
+
Y.append(page_r[j][node])
|
| 40 |
+
comparison_matrix[i,j] = spearmanr(X,Y)[0]
|
| 41 |
+
comparison_matrix[j,i] = comparison_matrix[i,j]
|
| 42 |
+
return np.nan_to_num(comparison_matrix)
|