12,584 lectures
12,584 lectures

Embeddings 101 : Déverrouiller les relations sémantiques dans le texte

par Ritesh Modi14m2025/03/27
Read on Terminal Reader
Read this story w/o Javascript

Trop long; Pour lire

Les embeddings traitent des limites dans la façon dont les machines comprennent le langage. Ils ne sont pas seulement une nouvelle technique fantastique – ils traitent des limites fondamentales dans la compréhension du langage.
featured image - Embeddings 101 : Déverrouiller les relations sémantiques dans le texte
Ritesh Modi HackerNoon profile picture
0-item
1-item


Quand j'ai commencé à travailler avec des données texte il y a des années, le concept de l'intégration semblait inutilement complexe. J'étais à l'aise avec mes approches de sac de mots et des vecteurs TF-IDF simples.


J'ai travaillé avec des critiques de produits, et mes modèles traditionnels ont continué à classer les critiques avec du sarcasme ou un langage nuancé. Le problème est devenu clair: mes modèles n'ont pas compris que "ce produit est malade" pourrait en fait être positif ou que "le travail exactement comme prévu" pourrait être neutre ou négatif selon le contexte.


Ce n’est pas seulement une nouvelle technique fantastique – elle aborde les limites fondamentales de la façon dont les machines comprennent le langage.


The Old Days: Life Before Embeddings

Les vieux jours : la vie avant l'emballage

Comprenons l’histoire de l’utilisation de représentations textuelles avant les intégrations sans entrer dans les détails de ces méthodes et approches.

One-Hot Encoding

Il représentait chaque mot comme un vecteur rare avec tous les zéros sauf un seul « 1 » à la position correspondant à ce mot dans un vocabulaire. Il représentait les mots comme des vecteurs massifs, rares où chaque mot a sa propre dimension. Si votre vocabulaire avait 100 000 mots (ce qui est modeste), chaque vecteur de mot avait 99,999 zéros et un seul 1. Ces représentations ne nous disaient absolument rien de sens. Les mots « excellent » et « fantastique » étaient mathématiquement aussi différents que « excellent » et « terrible » – manquant complètement des relations sémantiques évidentes.


« chat » → [1, 0, 0, 0, ..., 0] (position 5432 du vocabulaire) « chien » → [0, 1, 0, 0, ..., 0] (position 8921 du vocabulaire)


limitations

    à
  • Explosion de dimensionnalité : les vecteurs avaient autant de dimensions que la taille du vocabulaire (souvent plus de 100 000)
  • à
  • Aucune relation sémantique : « chat » et « chaton » étaient aussi différents que « chat » et « avion » (tous équidistants)
  • à
  • Inefficacité computationnelle : multiplier ces matrices rares était extrêmement riche en ressources
  • à
  • Pas de généralisation: le système ne pouvait pas comprendre les mots en dehors de son vocabulaire d'origine
  • à

Bag-of-Words Approach

Il comptait les occurrences de mots dans les documents, parfois pesées par leur importance. Il traitait les mots dans les documents comme des collections de mots non classées, en jetant complètement l'ordre des mots. "Le chien mord l'homme" et "L'homme mord le chien" auraient des représentations identiques.


Document: "Le chat était assis sur le tapis" BoW: {"le": 2, "le chat": 1, "sat": 1, "on": 1, "mat": 1}

Les limitations :

    à
  • Perte de l'ordre des mots: "Le chien mord l'homme" et "L'homme mord le chien" avaient des représentations identiques
  • à
  • Spares vecteurs haute dimension : des vecteurs de taille de vocabulaire encore nécessaires
  • à
  • Aucune compréhension sémantique : les synonymes étaient représentés comme des caractéristiques complètement différentes
  • à
  • Aucune signification contextuelle : Chaque mot avait une représentation fixe indépendamment du contexte
  • à

N-grams

Pour capturer un certain ordre de mots, nous avons commencé à utiliser n-grammes (séquences de n mots consécutifs).


Avec les unigrams (paroles simples), vous pourriez avoir un vocabulaire de 100 000. Avec les bigrams (paires de mots), vous regardez soudainement des millions de caractéristiques potentielles. Avec les trigrams? Des milliards, en théorie. Même avec la découpe agressive, la dimensionnalité est devenue incontrôlable.

Limitations:

Les limitations :
    à
  • Explosion combinatoire : le nombre de n-grammes possibles augmente de manière exponentielle
  • à
  • Sparsité des données : la plupart des n-grammes possibles n'apparaissent jamais dans les données de formation
  • à
  • Fenêtre de contexte limitée: Seules les relations capturées dans de petites fenêtres (typiquement 2-5 mots)
  • à

TF-IDF (Term Frequency-Inverse Document Frequency)

TF-IDF a amélioré les choses en pondérant les mots en fonction de leur importance pour un document spécifique relatif au corpus.

Limitations:

Les limitations :

Pas de signification sémantique : c’est le nombre et la fréquence des mots qui déterminent l’importance de leur utilisation.

The Embedding Revolution: What Changed?

La révolution : qu’est-ce qui a changé ?

La transition vers les embeddings n’était pas seulement une amélioration progressive ; c’était un changement de paradigme dans la façon dont nous représentons le langage.

Meaning Through Context

La compréhension fondamentale derrière les embeddings est trompeusement simple : les mots qui apparaissent dans des contextes similaires ont probablement des significations similaires.Si vous voyez « chien » et « chat » apparaître autour des mêmes types de mots (« animal de compagnie », « nourriture », « four »), ils sont probablement sémantiquement liés.


Des modèles d'emballage tels que Word2Vec ont capturé cela en formant les réseaux neuronaux pour prédire soit :


    à
  • Un mot basé sur son contexte environnant (Bag de mots continu)
  • à
  • Le contexte environnant basé sur un mot (Skip-gram)
  • à


Les poids de couche cachés de ces modèles sont devenus nos vecteurs de mots, codant les relations sémantiques dans les propriétés géométriques de l'espace vectoriel.


Quand j'ai d'abord conçu des vecteurs de mots et vu que "roi" - "homme" + "femme" ≈ "reine", je savais que nous étions sur quelque chose de révolutionnaire.


Les premiers modèles comme Word2Vec et GloVe ont donné à chaque mot un seul vecteur indépendamment du contexte.


"I need to bank the money" vs. "I'll meet you by the river bank"


Des modèles tels que BERT et GPT ont résolu cela en générant des embeddings différents pour le même mot en fonction de son contexte environnant.


Donc, d’abord, comprenons ce que sont les embeddings et comment ils ont transformé la PNL et abordé les limites des approches précédentes.

What Are Embeddings?

Quels sont les embeddings ?

Les embeddings sont des représentations numériques de données (texte, images, audio, etc.) dans un espace vectoriel continu.Pour le texte, les embeddings capturent les relations sémantiques entre les mots ou les documents, permettant aux machines de comprendre le sens d'une manière qui est mathématiquement traitable.

Key Concepts:

    à
  • Vecteurs : listes ordonnées de nombres représentant un point dans un espace multidimensionnel
  • à
  • Dimensions : le nombre de valeurs dans chaque vecteur (p. ex., 768-dim, 1024-dim)
  • à
  • Espace vectoriel : l'espace mathématique où existent les embeddings
  • à
  • Similarité sémantique : mesurée par la distance ou l'angle entre les vecteurs (plus proche = plus similaire)
  • à

What Do Dimensions Represent?

Chaque dimension dans un vecteur d'emballage représente une caractéristique ou un aspect appris des données. Contrairement à l'ingénierie des caractéristiques classique où les humains définissent ce que chaque dimension signifie, dans les modèles d'emballage modernes:


    à
  • Les dimensions émergent pendant la formation pour représenter des « concepts » abstraits
  • à
  • Les dimensions individuelles manquent souvent de sens spécifique interprétable par l'homme
  • à
  • Le vecteur complet, cependant, capture l'information sémantique de manière holistique
  • à
  • Certaines dimensions peuvent correspondre au sentiment, à la formalité, au sujet ou à la syntaxe, mais la plupart représentent des combinaisons complexes de caractéristiques.
  • à

Why We Need Embeddings

Les ordinateurs travaillent fondamentalement avec des nombres, pas des mots.Lors du traitement du langage, nous devons convertir le texte en représentations numériques qui:


    à
  1. Capture des relations sémantiques - des concepts similaires devraient avoir des représentations similaires
  2. à
  3. Conserver le sens contextuel – Le même mot peut signifier des choses différentes dans des contextes différents
  4. à
  5. Activer les opérations mathématiques – comme trouver des similitudes ou effectuer des analogies
  6. à
  7. Travailler efficacement à l’échelle – Traiter de grands volumes de texte sans explosion informatique
  8. à


Les embeddings résolvent ces problèmes en représentant des mots, des phrases ou des documents comme des vecteurs denses dans un espace continu où les relations sémantiques sont préservées comme des relations géométriques.

Les fondations des embeddings

Représentation du vecteur dense

Au lieu de vecteurs rares avec des milliers ou des millions de dimensions, les embeddings utilisent quelques centaines de dimensions denses où chaque dimension contribue à la signification.


"cat" → [0.2, -0.4, 0.1, -0.8, ..., 0.3] (300 dimensions)

"kitten" → [0.19, -0.38, 0.15, -0.75, ..., 0.29] (similar to "cat")


Cela rend les ordres de calcul de magnitude plus efficaces tout en permettant une représentation sémantique plus riche.

Sémantique de distribution

Les embeddings sont construits sur le principe que « vous connaîtrez un mot par l’entreprise qu’il garde » (J.R. Firth).En analysant quels mots apparaissent dans des contextes similaires, les embeddings capturent automatiquement des relations sémantiques.


Par exemple, « roi » et « reine » auront des contextes similaires, de sorte qu’ils auront des intégrations similaires, même s’ils apparaissent rarement dans la même position.

Propriétés mathématiques

Embedding spaces have remarkable mathematical properties:


vector("king") - vector("man") + vector("woman") ≈ vector("queen")


Cela permet le raisonnement analogique et les opérations sémantiques directement dans l'espace vectoriel.

Transfert d’apprentissage

Les embeddings pré-entraînés capturent des connaissances linguistiques générales qui peuvent être adaptées à des tâches spécifiques, ce qui réduit considérablement les données nécessaires pour de nouvelles applications.

Compréhension contextuelle

Les embeddings contextuels modernes (comme ceux de BERT, GPT, etc.) représentent le même mot différemment en fonction du contexte :


"I'll deposit money in the bank" → "bank" relates to finance

"I'll sit by the river bank" → "bank" relates to geography


Avec toutes les connaissances sur l'histoire et la compréhension des embeddings, il est temps de commencer à les utiliser.

Utiliser les modèles LLM/SLM pour générer des embeddings

Diverses équipes de recherche ont développé des modèles d'emballage formés sur divers ensembles de données couvrant plusieurs langues et domaines. Cette diversité entraîne des modèles dotés de vocabulaire très différent et de capacités de compréhension sémantique. Par exemple, des modèles formés principalement sur la littérature scientifique anglaise coderont des concepts techniques différemment de ceux formés sur le contenu des médias sociaux multilingue. Cette spécialisation permet aux praticiens de sélectionner des modèles d'emballage qui s'alignent le mieux sur leurs cas d'utilisation spécifiques.


La mise en œuvre pratique des embeddings a été beaucoup simplifiée par des bibliothèques telles que le package SentenceTransformer de Hugging Face, qui fournit un SDK complet pour travailler avec divers modèles d'embedding. De même, le SDK d'OpenAI offre un accès direct à leurs modèles d'embedding, qui ont montré des performances impressionnantes sur de nombreux critères. Ces outils, et bien d'autres, ont démocratisé l'accès aux technologies d'embedding de pointe, permettant aux développeurs d'intégrer la compréhension sémantique dans les applications sans avoir à former des modèles à partir de zéro.


Les modèles, pour le bien de cet article, devraient être traités comme une boîte noire qui prend les phrases comme une entrée et renvoie leur représentation vectorielle correspondante.

Utilisation de la bibliothèque de SentenceTransformers pour les embeddings

La façon la plus simple de générer des embeddings à l'aide de SentenceTransformer:

from sentence_transformers import SentenceTransformer

# Load a pre-trained model
model = SentenceTransformer('all-MiniLM-L6-v2')  # 384 dimensions

# Generate embeddings
texts = ["This is an example sentence", "Each sentence becomes a vector"]

embeddings = model.encode(texts)

print(f"Shape: {embeddings.shape}")  # (2, 384)

max_seq_length = model.tokenizer.model_max_length

print(max_seq_length) # 256


​​Le modèle "all-MiniLM-L6-v2" disponible chez HuggingFace a 384 dimensions. Cela signifie qu'il peut capturer 384 caractéristiques ou nuances pour un mot donné ou une phrase. La longueur de la séquence de ce modèle est de 256 jetons. Les phrases sont décomposées en mots et mots en jetons par le jetoniseur pendant le processus d'intégration. Le nombre de jetons générés pour une phrase est généralement de 25% à 40% plus que le nombre de mots dans la phrase.


La longueur de la séquence désigne le nombre de jetons qui peuvent être traités par le modèle en tant qu'entrée donnée. Tout ce qui est moins est pavé pour en faire 256 en longueur, et tout ce qui est plus est jeté.


La méthode d'encodage de la classe SentenceTransformer est un enveloppeur sur le mode d'inférence PyTorch pour utiliser le modèle.


from sentence_transformers import SentenceTransformer
import torch

# Load the model directly with SentenceTransformer
model = SentenceTransformer("sentence-transformers/msmarco-distilbert-base-tas-b")

# Input text
texts = ["This is an example sentence", "Each sentence becomes a vector"]

# Get embedding directly
with torch.no_grad():
    embedding = model.encode(texts, convert_to_tensor=True)
print(embedding)


Ici, la fonction torch.no_grad assure qu'aucun gradient n'est calculé pendant la propagation arrière.


Une autre façon plus générale de générer des embeddings en utilisant PyTorch :


# Load the model
model = AutoModel.from_pretrained("sentence-transformers/msmarco-distilbert-base-tas-b")

# Get the tokenizer
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/msmarco-distilbert-base-tas-b")

# Tokenize input
text = ["This is an example sentence"]
encoded_input = tokenizer(text, padding=True, truncation=True, return_tensors='pt')

# Get embedding of the [CLS] token
with torch.no_grad():
    outputs = model(**encoded_input, return_dict=True)
    cls_embedding = outputs.last_hidden_state[:, 0]
print(cls_embedding)


La différence entre ce et les tranches de code antérieures est que la fonction d'encodage a été remplacée par l'utilisation explicite du tokenizer et du modèle.


Une autre différence est que nous utilisons outputs.last_hidden_state[:, 0] pour récupérer le vecteur lié au jeton CLS. Ce jeton spécial CLS est ajouté à chaque phrase au début de chaque phrase, et il contient des informations accumulées sur la phrase entière.


It is to be noted that this approach of adding a CLS token is applicable to only certain transformer-based architectures, and this includes BERT and its variants and encoder-only-based transformers. Also, there are architectures like SBERT that provide embeddings for entire sentences without using this technique.


Best for:Tâches de classification et de prédiction au niveau de la séquence


Why they work:Le jeton [CLS] dans les modèles de style BERT est spécialement formé pour agréger des informations de l'ensemble de la séquence pendant la pré-entraînement.


When to choose:

    à
  • Lorsque vous utilisez des modèles BERT, RoBERTa ou similaires pour la classification
  • à
  • Lorsque vous avez besoin d'un seul vecteur représentant une séquence entière
  • à
  • Lorsque votre tâche en aval implique de prédire une propriété de tout le texte
  • à


La méthode CLS utilisée n'est qu'une des méthodes pour capturer les embeddings d'une phrase.

Mean Pooling

Prendre la moyenne de tous les embeddings de jetons est étonnamment efficace pour de nombreuses tâches.C'est ma méthode de go-to lorsque j'utilise les embeddings pour les tâches de ressemblance ou de récupération.


Best for:Similitudes sémantiques, récupération et représentations à but général.


Why it works:En moyenne sur toutes les représentations de jetons, le pooling moyen capture le contenu sémantique collectif tout en réduisant le bruit.


When to choose:

    à
  • Pour les applications de ressemblance de document ou de recherche sémantique
  • à
  • Lorsque vous avez besoin de représentations robustes qui ne sont pas dominées par un seul jeton
  • à
  • Lorsque les tests empiriques montrent qu'il dépasse les autres méthodes (il le fait souvent pour les tâches de similitude)
  • à


import torch
from transformers import AutoTokenizer, AutoModel

model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

# Tokenize input
texts = ["This is an example sentence", "Each sentence becomes a vector"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")

# Mean pooling
with torch.no_grad():
    outputs = model(**inputs)
    
    # Get attention mask to ignore padding tokens
    attention_mask = inputs['attention_mask']
    
    # Sum token embeddings and divide by the number of tokens
    sum_embeddings = torch.sum(outputs.last_hidden_state * attention_mask.unsqueeze(-1), dim=1)
    count_tokens = torch.sum(attention_mask, dim=1, keepdim=True)
    mean_embeddings = sum_embeddings / count_tokens

print(f"Shape: {mean_embeddings.shape}")  # (2, 768)


Max Pooling

Le regroupement maximum prend la valeur maximale pour chaque dimension sur tous les jetons.Il est étonnamment bon pour capturer des fonctionnalités importantes indépendamment de l'endroit où elles apparaissent dans le texte.


Best for:Tâches de détection et d'extraction d'informations


Why it works:Max pooling sélectionne l'activation la plus forte pour chaque dimension sur tous les jetons, capturant efficacement les caractéristiques les plus remarquables où qu'elles apparaissent dans le texte.


When to choose:

    à
  • Lorsque des caractéristiques spécifiques comptent plus que leur fréquence ou leur position
  • à
  • Lors de la recherche de la présence de concepts ou d'entités particuliers
  • à
  • Lorsque vous traitez avec de longs textes où des signaux importants peuvent être dilués en moyenne
  • à


import torch
from transformers import AutoTokenizer, AutoModel

model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

# Tokenize input
texts = ["This is an example sentence", "Each sentence becomes a vector"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")

# Max pooling
with torch.no_grad():
    outputs = model(**inputs)
    
    # Create a mask to ignore padding tokens for max pooling
    attention_mask = inputs['attention_mask'].unsqueeze(-1)
    
    # Replace padding token representations with -inf so they're never selected as max
    token_embeddings = outputs.last_hidden_state.masked_fill(attention_mask == 0, -1e9)
    
    # Take max over token dimension
    max_embeddings = torch.max(token_embeddings, dim=1)[0]

print(f"Shape: {max_embeddings.shape}")  # (2, 768)

Weighted Mean Pooling

La méthode de regroupement pondéré essaie de donner plus de poids aux jetons plus importants en fonction de la position (par exemple, donner plus de poids aux jetons ultérieurs).


Best for:Tâches où différentes parties de l'entrée ont une importance différente


Why it works:Tous les mots ne contribuent pas de la même manière au sens. Le regroupement pondéré vous permet de souligner certains jetons en fonction de leur position, de leurs scores d'attention ou d'autres mesures de pertinence.


When to choose:

    à
  • Lorsque l'ordre de la séquence est important (par exemple, donner plus de poids aux jetons ultérieurs)
  • à
  • Lorsque certains jetons sont intrinsèquement plus informatifs (par exemple, notions et verbes versus articles)
  • à
  • Lorsque vous avez une importance spécifique heuristique qui a du sens pour votre tâche
  • à


import torch
import torch.nn.functional as F
from transformers import AutoTokenizer, AutoModel

model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

# Tokenize input
texts = ["This is an example sentence", "Each sentence becomes a vector"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")

# Weighted mean pooling - more weight to later tokens
with torch.no_grad():
    outputs = model(**inputs)
    
    # Get token embeddings and attention mask
    token_embeddings = outputs.last_hidden_state
    attention_mask = inputs['attention_mask']
    
    # Create position-based weights (later positions get higher weights)
    input_lengths = torch.sum(attention_mask, dim=1).unsqueeze(-1)
    position_indices = torch.arange(token_embeddings.size(1)).unsqueeze(0).expand_as(attention_mask)
    position_weights = position_indices.float() / input_lengths.float()
    position_weights = position_weights * attention_mask
    
    # Normalize weights to sum to 1
    position_weights = position_weights / torch.sum(position_weights, dim=1, keepdim=True)
    
    # Apply weights and sum
    weighted_embeddings = torch.sum(token_embeddings * position_weights.unsqueeze(-1), dim=1)

print(f"Shape: {weighted_embeddings.shape}")  # (2, 768)


Dernière polarisation

Le dernier regroupement de jetons est une technique permettant de créer un seul vecteur d'incorporation à partir d'une séquence d'incorporations de jetons en sélectionnant uniquement la représentation du jeton final.


Best for:Modèles autorégressifs et traitement séquentiel


Why it works:Dans les modèles de gauche à droite tels que GPT, le jeton final contient le contexte accumulé de l'ensemble de la séquence, ce qui le rend riche en informations pour certaines tâches.


When to choose:

    à
  • Lorsque vous utilisez GPT ou d'autres modèles uniquement décodeurs
  • à
  • Lorsque vous travaillez avec des tâches qui dépendent fortement du contexte précédent
  • à
  • Pour la génération de texte ou les tâches d'achèvement
  • à


import torch
from transformers import AutoTokenizer, AutoModel

model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

# Tokenize input
texts = ["This is an example sentence", "Each sentence becomes a vector"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")

# Last token pooling
with torch.no_grad():
    outputs = model(**inputs)
    
    # Get the last non-padding token for each sequence
    attention_mask = inputs['attention_mask']
    last_token_indices = torch.sum(attention_mask, dim=1) - 1
    batch_indices = torch.arange(attention_mask.size(0))
    
    # Extract the last token embedding for each sequence
    last_token_embeddings = outputs.last_hidden_state[batch_indices, last_token_indices]

print(f"Shape: {last_token_embeddings.shape}")  # (2, 768)



Il y a beaucoup plus de façons, et ces méthodes peuvent être combinées ensemble ainsi que pour créer des méthodes personnalisées.Ce n'était que le début pour comprendre les embeddings comme un concept et une mise en œuvre de base pour obtenir des embeddings en utilisant différentes techniques.

Looking Forward: Where Embeddings Are Headed

Regardant vers l'avenir: Où les embeddings sont dirigés

L’espace d’emballage (pun intended) continue d’évoluer :


    à
  • Les embeddings multimodaux brisent les barrières entre le texte, les images, l'audio et la vidéo.Des modèles comme CLIP et DALL-E utilisent les embeddings pour créer un espace sémantique partagé entre différentes modalités.
  • à
  • Des architectures plus efficaces telles que MobileBERT et DistilBERT permettent d’utiliser des embeddings puissants sur des périphériques à bord avec des ressources limitées.
  • à
  • Les embeddings spécifiques aux domaines pré-entraînés sur des corporations spécialisées poussent l'état de l'art dans des domaines tels que la médecine, le droit et la finance.
  • à

Je suis particulièrement excité par les intégrations conscientes de la composition qui capturent mieux la façon dont le sens est construit à partir d'unités plus petites, ce qui pourrait enfin résoudre les défis de longue date avec la négation et les phrases de composition.

Final Thoughts

Pensées finales

Les embeddings ne sont pas seulement une autre technique de la PNL – ils sont un changement fondamental dans la façon dont les machines comprennent et traitent le langage. Ils nous ont déplacés de traiter le texte comme des symboles arbitraires à capturer le riche et complexe réseau de significations et de relations que les humains comprennent intuitivement.


Quelle que soit la tâche de la PNL sur laquelle vous travaillez, il est probable que des intégrations bien appliquées puissent le rendre meilleur.La clé est de comprendre non seulement comment les générer, mais quand et pourquoi utiliser différentes approches.


Et si vous utilisez toujours un sac de mots ou un codage unique pour l'analyse de texte...


Un monde de possibilités vous attend.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks