Antonio Filipovic
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    --- title: Amazon user-item recommender with TGN and Memgraph description: Over the course of the last few months, we at Memgraph have been working on something that we believe could help you guys with classical graph prediction tasks. With our latest newborn query module, you will have the possibility to do both label classification and link prediction. author: Antonio Filipovic parentCategory: Tutorial --- # PYTORCH x MEMGRAPH x GNN = :heart_decoration: Over the course of the last few months, we at **Memgraph** have been working on something that we believe could help you guys with classical graph prediction tasks. With our latest newborn query module, you will have the possibility to do both **label classification** and **link prediction**. But, how come a query module can do both label classification and link prediction? It's all thanks to **graph neural networks**, for short **GNNs**. :heart: # Graph neural networks Whether you are a software engineer or a deep learning enthusiast, there is a high chance you heard of **[graph neural networks](https://en.wikipedia.org/wiki/Graph_neural_network)** as a rising :star:. Maybe you even deep-doved into this topic and you are now ready for **[new MAGE spell](https://github.com/memgraph/mage)**. But even if you haven't, don't worry, I will try to give you a quick overview so you can catch up and follow along. Firstly as you already know graph consists of **nodes(vertices)** and **edges(relationships)**. ![](https://i.imgur.com/s3QOvNJ.png) Every node can have its **feature vector**, which essentially describes that node with a vector of numbers. We can look at this **feature vector** as the **representation vector** of each node, also called **embedding** of the node. To avoid getting lost in technical details, **graph neural networks** work as a **message passing<sup>[2]</sup>** system, where each node aggregates feature representations of its 1-hop neighbors. To be more precise, nodes don’t aggregate feature representations directly, but feature vectors obtained by dimensionality reduction using the *W* matrix (you can look at them as fully connected linear layers). This matrix projected feature vectors are called **messages** and they give expressive power to graph neural networks. ![](https://i.imgur.com/IeKYzNj.png) This idea originates from the field of **graph signal processing**. Now, we don't have time here to explain all about how we got from **signals** to **message passing**, but it's all math :heart:. Feel free to drop us a message on [Discord](https://discord.gg/memgraph) and we will make sure to create a blog post explaining the **graph neural network** introduction topic, not a simplified version, but one explaining all of it from the beginning, somewhere about [Tutorial on Spectral Clustering](https://arxiv.org/pdf/0711.0189.pdf) in 2007. If you would like to get a better understanding of the topic of the **graph neural networks** before continuing, I suggest you do the following: * read a following **[blog post](https://distill.pub/2021/gnn-intro/)** - it gives a gentle introduction to the topic * you can also check the **[the video explanation](https://www.youtube.com/watch?v=6g9vtxUmfwM&list=PLoROMvodv4rPLKxIpqhjhPgdQy7imNkDn&index=14&ab_channel=StanfordOnline)** by the Stanford professor Jure Leskovec about graph neural networks - I would honestly suggest to binge-watch the whole series, but if you don't have that much time, just watch the lectures called **[Message passing and Node Classification](https://www.youtube.com/watch?v=6g9vtxUmfwM&list=PLoROMvodv4rPLKxIpqhjhPgdQy7imNkDn&index=15&ab_channel=StanfordOnline)** and **[Introduction to Graph Neural Networks](https://www.youtube.com/watch?v=F3PgltDzllc&list=PLoROMvodv4rPLKxIpqhjhPgdQy7imNkDn&index=17&ab_channel=StanfordOnline)** * and if you want to deep-dive, which I suggest, I will leave [the following blog post](https://gordicaleksa.medium.com/how-to-get-started-with-graph-machine-learning-afa53f6f963a), it will be more than enough :mage: A reason why we wanted to give our **[MAGE](https://github.com/memgraph/mage)** a new spell in form of **GNN** is because **GNNs** are to **graphs** what **[CNNs](https://towardsdatascience.com/a-comprehensive-guide-to-convolutional-neural-networks-the-eli5-way-3bd2b1164a53)** are to **images**. **GNNs** can inductively learn about your dataset, which means that after training is complete you can apply their knowledge to a similar use-case, which is very cool since you don't need to retrain the whole algorithm. With other representation learning methods like **[DeepWalk](https://arxiv.org/abs/1403.6652)**, **[Node2Vec](https://arxiv.org/abs/1607.00653)**, **[Planetoid](https://arxiv.org/abs/1603.08861)**, we haven't been able to do that until, well, **GNNs**. **Now, why temporal graph neural networks?** Imagine you are in charge of a product where users interact with items every minute of every day, and they like some and hate the others. You would like to present them with more items they like, and not just that, you would love it if they bought those new items. This way you have a **stream** of data. Interactions appear across time, so you are dealing with a **temporal** dataset. The classical **GNNs** are not designed to work with streams, although they work very well on unseen data. But it is not all nails if you have a hammer - some methods work better on stream, others on static data. ## Temporal graph networks As you already know, we in [Memgraph](https://memgraph.com/) are all about streams. :dash: Thanks to the guys at [Twitter](https://twitter.com/twitterresearch), they developed a **GNN** that works on **temporal** graph networks. This way **GNNs** can deal with **continuous-time dynamic graphs**. In the image below you can see a schematic view of **temporal graph networks**. It is a lot to take, but the process once explained is not that complicated. <a href="https://i.imgur.com/Uy0UHBW.jpg"> <img border="10" alt="TGN" src="https://i.imgur.com/Uy0UHBW.jpg"> </a> Firstly, in **continuous-time dynamic graphs** you can model changes on graph which include **edge or node addition**, **edge or node feature transformation(update)**, **edge or node delition** as a time listed events. **[Temporal graph networks<sup>[1]</sup>](https://arxiv.org/pdf/2006.10637.pdf)** , shortend **TGNs**, work as following: * firstly, node embedding calculations work on the concept of message passing, which I hope you are familiar with until now :crossed_fingers: * TGNs use **events**, and whenever a new edge appears it represents **interaction event** between two nodes involved * from every **event** we create a **message** and use a **message aggregator** for all messages of the same node to get the **aggregated message** of every node * every node has it's own **memory** which represents **accumulated** state, updated with **aggregated message** by one of the **[LSTM or GRU](https://towardsdatascience.com/illustrated-guide-to-lstms-and-gru-s-a-step-by-step-explanation-44e9eb85bf21)** * Lastly, the embedding module is used to generate the temporal embedding There are two embedding module types we integrated into our **TGN** implementation: * **Graph attention layer**: it is a similar concept as in **[Graph attention networks](https://arxiv.org/abs/1710.10903)**, but here they use the original idea from Vaswani et al. **[Attention is all you need](https://arxiv.org/abs/1706.03762)** which includes queries, keys and values and everything else is the same. I suggest you look at the **TGN** paper to check the exact embedding calculation details. * **Graph sum layer**: this mechanism is completely similar to the **message passing** system There is a certain problem when dealing with **embedding updates**. We don't update embeddings for every node, only for ones that appear in a batch. Also, in order not to get so much into implementation details, we will try to abstractly explain the following problem. Nodes in the batch appear at different points in time. That's why we need to take into account when was their update so that we can only use neighbors which appeared in the graph before them. This is in case we update the whole representation of the graph with batch information, and then do the calculation, which is what we did. This leads to having a different computation graph for every node. You can see what it looks like in the image below: ![](https://i.imgur.com/hdCSd48.png) ## Amazon data example Now to try how this works, we have prepared a Jupyter Notebook on our **[GitHub repository](https://github.com/memgraph/jupyter-memgraph-tutorials)**. It is about [Amazon user-item reviews](http://snap.stanford.edu/data/amazon/productGraph/). In the following example, you will see how to do **link prediction** with **TGN**. # Exploring a Amazon data network in Memgraph Through this short tutorial, you will learn how to install Memgraph, connect to it from a Jupyter Notebook and perform data analysis on Amazon data set using **graph neural network** called **Temporal graph networks**. ## 1. Prerequisites For this tutorial, you will need to install: - [Jupyter](https://jupyter.org/install) - [Docker](https://docs.docker.com/get-docker/) - [GQLAlchemy](https://pypi.org/project/gqlalchemy/) Docker is used because Memgraph is a native Linux application and cannot be installed on Windows and macOS. ## 2. Installation using Docker After installing Docker, you can set up Memgraph by running: ``` docker run -it -p 7687:7687 -p 3000:3000 -p 7444:7444 memgraph/memgraph-platform ``` This command will start the download and after it finishes, run the Memgraph container. ## 3. Connecting to Memgraph with GQLAlchemy We will be using the **GQLAlchemy** object graph mapper (OGM) to connect to Memgraph and execute **Cypher** queries easily. GQLAlchemy also serves as a Python driver/client for Memgraph. You can install it using: ``` pip install gqlalchemy ``` > **Hint**: You may need to install [CMake](https://cmake.org/download/) before installing GQLAlchemy. Maybe you got confused when I mentioned Cypher. You can think of Cypher as SQL for graph databases. It contains many of the same language constructs like `CREATE`, `UPDATE`, `DELETE`... and it's used to query the database. ```python from gqlalchemy import Memgraph ``` ```python memgraph = Memgraph("127.0.0.1", 7687) ``` Let's make sure that Memgraph is empty before we start with anything else. ```python memgraph.drop_database() ``` Following command should output {number_of_nodes:0} ```python results = memgraph.execute_and_fetch( """ MATCH (n) RETURN count(n) AS number_of_nodes ; """ ) print(next(results)) ``` ## 4. Data analysis on Amazon product dataset You will load an **amazon product dataset** as list of cypher queries. Here is how it looks: <a href="https://i.imgur.com/yuL5SrP.png"> <img border="10" alt="dataset" src="https://i.imgur.com/yuL5SrP.png"> </a> One example of mentioned query is following one: ```cypher MERGE (a:User {id: 'A1BHUGKLYW6H7V', profile_name:'P. Lecuyer'}) MERGE (b:Item {id: 'B0007MCVQ2'}) MERGE (a)-[:REVIEWED {review_text:'Like all Clarks, these guys didnt disappoint. They fit great and look even better. For the price, I dont think a better deal exists out there for casual shoes.', feature: [161.0, 133.0, 0.782608695652174, 0.0, 0.031055900621118012, 0.17391304347826086, 0.043478260869565216, 36.0, 36.0, 1.0, 3.6944444444444446, 0.0, 0.0, 3.0, 1.0, 12.0, 0.055, 0.519, 0.427, 0.9238], review_time:1127088000, review_score:5.0}]->(b); ``` So as you can see, we have `User` and we have `Item` in our graph sheme. Every user has left a very **positive** review for **Item**. This wasn't a case for all reviews in original dataset, but we processed it and removed negative reviews (all reviews with `review_score` <= 3.0). Every `User` as an `id` and every `Item` user has reviewed has also an `id`. In this one query, we find the `User` and the `Item` with mentioned ids or we create one if such `User` or `Item` is missing in database. And we create an interaction event between them in term of an `edge` which has list of **20** edge features. This `edge_features` we created from review user has left to an item: ``` 1. Number of characters 2. Number of characters without counting white space 3. Fraction of alphabetical characters 4. Fraction of digits 5. Fraction of uppercase characters 6. Fraction of white spaces 7. Fraction of special characters, such as comma, exclamation mark, etc. 8. Number of words 9. Number of unique works 10. Number of long words (at least 6 characters) 11. Average word length 12. Number of unique stopwords 13. Fraction of stopwords 14. Number of sentences 15. Number of long sentences (at least 10 words) 16. Average number of words per sentence 17. Positive sentiment calculated by VADER # VADER - Valence Aware Dictionary and sEntiment Reasoner lexicon # and rule-based sentiment analysis tool 18. Negative sentiment calculated by VADER 19. Neutral sentiment calculated by VADER 20. Compound sentiment calculated by VADER ``` We should have also prepared features for a `User` and `Item`, but following features seemed enough for our **toy** example. One more **note**: In those dataset of queries we already prepared for you, there is one query that will change "working mode" of our **temporal graph networks** module to **evaluation(eval)** mode. When mode is changed for **tgn** it also stops doing **training** of the model, and starts doing **evaluation** of trained model. If you look inside of file you should find following query: ```cypher CALL tgn.set_mode("eval") YIELD *; ``` ### Trigger creation In order to process a dataset, we need to create trigger on **edge create** event if trigger with our name already doesn't exist. You will see, this check is a neat feature to have in your Jupyter notebook if you want just to rerun it without dumping **local Memgraph** instance if you are not working with **Docker**. ```python results = memgraph.execute_and_fetch("SHOW TRIGGERS;") trigger_exists = False for result in results: if result['trigger name'] == 'create_embeddings': print("Trigger already exists") trigger_exists = True break; if not trigger_exists: memgraph.execute( """ CREATE TRIGGER create_embeddings ON --> CREATE BEFORE COMMIT EXECUTE CALL tgn.update(createdEdges) RETURN 1; """ ) ``` ### Index creation for dataset **Memgraph** works best with **indexes** defined for nodes, in our case we will have **User** and **Item** types. ```python index_queries = ["CREATE INDEX ON :User(id);", "CREATE INDEX ON :Item(id);"] for query in index_queries: results = memgraph.execute_and_fetch(query) for result in results: continue ``` ### Training and evaluating Temporal Graph Networks In order to train **Temporal graph networks** on **Amazon dataset** we will split dataset into **train** and **eval** queries. Let's first load our **raw queries**. Each query creates an **edge** between **User** and **Item** thus representing a **positive** review of a certain **Item** by a **User**. ```python import os dir_path = os.getcwd() with open(f"{dir_path}/data/queries.cypherl", "r") as fh: raw_queries = fh.readlines() train_eval_split_ratio = 0.8 queries_index_split = int(len(raw_queries) * train_eval_split_ratio) train_queries = raw_queries[:queries_index_split] eval_queries = raw_queries[queries_index_split:] print(f"Num of train queries {len(train_queries)}") print(f"Num of eval queries {len(eval_queries)}") ``` Before we start importing **train** queries, first we need to set parameters for **temporal graph networks**. ```python # since we are doing link prediction, we use self_supervised mode learning_type = "self_supervised" batch_size = 200 #optimal size as defined in paper num_of_layers = 2 # GNNs don't need multiple layers, contrary to CNNs. layer_type = "graph_attn" # choose between graph_attn or graph_sum edge_message_function_type = "identity" # choose between identity or mlp message_aggregator_type = "last" # choose between last or mean memory_updater_type = "gru" # choose between gru or rnn attention_heads = 1 memory_dimension = 100 time_dimension = 100 num_edge_features = 20 num_node_features=100 # number of sampled neighbors num_neighbors = 15 # message dimension must be defined in the case we use MLP, # because then we define dimension of **projection** message_dimension = time_dimension + num_node_features + num_edge_features tgn_param_query = f"CALL tgn.set_params({{learning_type:'{learning_type}', batch_size: {batch_size}, num_of_layers:{num_of_layers}, layer_type:'{layer_type}', memory_dimension:{memory_dimension}, time_dimension:{time_dimension}, num_edge_features:{num_edge_features}, num_node_features:{num_node_features}, message_dimension:{message_dimension}, num_neighbors:{num_neighbors}, edge_message_function_type:'{edge_message_function_type}', message_aggregator_type:'{message_aggregator_type}', memory_updater_type:'{memory_updater_type}', attention_heads:{attention_heads}}}) YIELD *;" print(f"TGN param query: {tgn_param_query}") results = memgraph.execute_and_fetch(tgn_param_query) for result in results: print(result) ``` Now it is time to execute queries and do **first** epoch of **training**. ```python for query in train_queries: results = memgraph.execute_and_fetch(query.strip()) for result in results: continue ``` Now we need to change **TGN** mode to **eval** and start importing our **evaluation** queries. ```python results = memgraph.execute_and_fetch("CALL tgn.set_eval() YIELD *;") for result in results: print(result) for query in eval_queries: results = memgraph.execute_and_fetch(query.strip()) for result in results: continue ``` After our **stream** is done, we should probably do few more rounds of training and evaluation in order to have properly working model. We can do so with following query: ```python num_of_epochs = 5 results = memgraph.execute_and_fetch( f""" CALL tgn.train_and_eval({num_of_epochs}) YIELD * RETURN epoch_num, batch_num, precision, batch_process_time, batch_type ORDER BY epoch_num, batch_num; """ ) for result in results: continue ``` Now, let's get the results and then do some **plotting** to check whether **precision** increases between epochs. ```python from collections import defaultdict import matplotlib.pyplot as plt import numpy as np results_train_dict = defaultdict(list) results_eval_dict = defaultdict(list) results = memgraph.execute_and_fetch( """ CALL tgn.get_results() YIELD epoch_num, batch_num, precision, batch_process_time, batch_type RETURN epoch_num, batch_num, precision, batch_process_time, batch_type ORDER BY epoch_num, batch_num; """ ) for result in results: if result['batch_type'] == 'Train': results_train_dict[result['epoch_num']].append(result['precision']) else: results_eval_dict[result['epoch_num']].append(result['precision']) ``` Now when we have collected the results, let's first plot the average `accuracy` of `train` batches inside epoch, and the average `accuracy` of `eval` batches inside epoch. We can do that since every batch is the same size. (**NB**: `TGN` uses a predefined batch size.) ```python X_train = [] Y_train = [] for epoch, batches_precision in results_train_dict.items(): Y_train.append(np.mean(batches_precision)) X_train.append(epoch) X_eval = [] Y_eval = [] for epoch, batches_precision in results_eval_dict.items(): Y_eval.append(np.mean(batches_precision)) X_eval.append(epoch) #scatter plot plt.plot(X_train, Y_train, 'b', label="train") plt.plot(X_eval, Y_eval, 'r', label="eval") #add title plt.title('epoch - average batch precision') #add x and y labels plt.xlabel('epoch') plt.ylabel('precision') plt.legend(loc="upper left") #show plot plt.show() ``` ![](https://i.imgur.com/8TZYE7b.png) We can see that **average accuracy** increases, which is really good. Now we can start doing some recommending. Let's find `Users` who reviewed one `Item` positively and those who reviewed multiple `Items` positively. Our module will return what it believes should be a prediction score for yet unreviewed `Items`. ```python results = memgraph.execute_and_fetch( """ MATCH (n:User) WITH n LIMIT 15 MATCH (m:Item) OPTIONAL MATCH (n)-[r]->(m) WHERE r is null CALL tgn.predict_link_score(n,m) YIELD prediction WITH n,m, prediction ORDER BY prediction DESC LIMIT 10 MERGE (n)-[:PREDICTED_REVIEW {likelihood:prediction}]->(m); """ ) for result in results: print(result) ``` Now we can apply make following query in Memgraph Lab: ```cypher MATCH (u:User)-[pr:PREDICTED_REVIEW]->(i:Item), (u)-[r:REVIEWED]->(oi:Item) RETURN *; ``` And after [applying style](https://github.com/memgraph/jupyter-memgraph-tutorials/tree/main/pytorch_amazon_network_analysis), we get the following visualization. From image below we can see that most predictions are oriented towards one of the most popular items. <a href="https://i.imgur.com/eqQBcTm.jpg"> <img border="10" alt="dataset" src="https://i.imgur.com/eqQBcTm.jpg"> </a> ## Where to next? Well, I hope this was fun and that you have learned something. You can check out what we else we have prepared in **[MAGE 1.2 release](https://memgraph.com/blog/mage-1-2-release)**. If you loved our implementation, don't hesitate to give as a star on **[GitHub](https://github.com/memgraph/mage)** :star:. If you have any comment, you can contact us on **[Discord](https://discord.gg/memgraph)**. And lastly if you wish to continue on reading our blog posts, jump on **[our page](https://memgraph.com/blog)**. ## References [1] E. Rossi, B. Chamberlain, F. Frasca, D. Eynard, F. Monti, M. Bronstein (2020). Temporal Graph Networks for Deep Learning on Dynamic Graphs [2] W.L. Hamilton, R. Ying, and J. Leskovec. Inductive representation learning on large graphs. U Advances in Neural Information Processing Systems 30, 2017

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully