普通视图

发现新文章,点击刷新页面。
昨天以前Ethan's Blog

RAG系列-语义分块RAG(Semantic Chunking RAG)

作者 作者
2025年6月18日 00:00

02. 语义分块RAG(Semantic Chunking RAG)

方法简介

语义分块RAG通过计算句子间的语义相似度来智能分块,而不是简单的固定长度分块。它使用百分位数、标准差或四分位距等方法找到语义断点,将文本分割成语义连贯的块,提升检索精度。

核心代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import fitz
import os
import numpy as np
import json
from openai import OpenAI

def extract_text_from_pdf(pdf_path):
    """
    Extracts text from a PDF file.

    Args:
    pdf_path (str): Path to the PDF file.

    Returns:
    str: Extracted text from the PDF.
    """
    # Open the PDF file
    mypdf = fitz.open(pdf_path)
    all_text = ""  # Initialize an empty string to store the extracted text

    # Iterate through each page in the PDF
    for page in mypdf:
        # Extract text from the current page and add spacing
        all_text += page.get_text("text") + " "

    # Return the extracted text, stripped of leading/trailing whitespace
    return all_text.strip()

# Initialize the OpenAI client with the base URL and API key
client = OpenAI(
    base_url="https://api.studio.nebius.com/v1/",
    api_key=os.getenv("OPENAI_API_KEY")  # Retrieve the API key from environment variables
)

def get_embedding(text, model="BAAI/bge-en-icl"):
    """
    Creates an embedding for the given text using OpenAI.

    Args:
    text (str): Input text.
    model (str): Embedding model name.

    Returns:
    np.ndarray: The embedding vector.
    """
    response = client.embeddings.create(model=model, input=text)
    return np.array(response.data[0].embedding)

def cosine_similarity(vec1, vec2):
    """
    Computes cosine similarity between two vectors.

    Args:
    vec1 (np.ndarray): First vector.
    vec2 (np.ndarray): Second vector.

    Returns:
    float: Cosine similarity.
    """
    return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

def compute_breakpoints(similarities, method="percentile", threshold=90):
    """
    Computes chunking breakpoints based on similarity drops.

    Args:
    similarities (List[float]): List of similarity scores between sentences.
    method (str): 'percentile', 'standard_deviation', or 'interquartile'.
    threshold (float): Threshold value (percentile for 'percentile', std devs for 'standard_deviation').

    Returns:
    List[int]: Indices where chunk splits should occur.
    """
    # Determine the threshold value based on the selected method
    if method == "percentile":
        # Calculate the Xth percentile of the similarity scores
        threshold_value = np.percentile(similarities, threshold)
    elif method == "standard_deviation":
        # Calculate the mean and standard deviation of the similarity scores
        mean = np.mean(similarities)
        std_dev = np.std(similarities)
        # Set the threshold value to mean minus X standard deviations
        threshold_value = mean - (threshold * std_dev)
    elif method == "interquartile":
        # Calculate the first and third quartiles (Q1 and Q3)
        q1, q3 = np.percentile(similarities, [25, 75])
        # Set the threshold value using the IQR rule for outliers
        threshold_value = q1 - 1.5 * (q3 - q1)
    else:
        # Raise an error if an invalid method is provided
        raise ValueError("Invalid method. Choose 'percentile', 'standard_deviation', or 'interquartile'.")

    # Identify indices where similarity drops below the threshold value
    return [i for i, sim in enumerate(similarities) if sim < threshold_value]

def split_into_chunks(sentences, breakpoints):
    """
    Splits sentences into semantic chunks.

    Args:
    sentences (List[str]): List of sentences.
    breakpoints (List[int]): Indices where chunking should occur.

    Returns:
    List[str]: List of text chunks.
    """
    chunks = []  # Initialize an empty list to store the chunks
    start = 0  # Initialize the start index

    # Iterate through each breakpoint to create chunks
    for bp in breakpoints:
        # Append the chunk of sentences from start to the current breakpoint
        chunks.append(". ".join(sentences[start:bp + 1]) + ".")
        start = bp + 1  # Update the start index to the next sentence after the breakpoint

    # Append the remaining sentences as the last chunk
    chunks.append(". ".join(sentences[start:]))
    return chunks  # Return the list of chunks

def create_embeddings(text_chunks):
    """
    Creates embeddings for each text chunk.

    Args:
    text_chunks (List[str]): List of text chunks.

    Returns:
    List[np.ndarray]: List of embedding vectors.
    """
    # Generate embeddings for each text chunk using the get_embedding function
    return [get_embedding(chunk) for chunk in text_chunks]

def semantic_search(query, text_chunks, chunk_embeddings, k=5):
    """
    Finds the most relevant text chunks for a query.

    Args:
    query (str): Search query.
    text_chunks (List[str]): List of text chunks.
    chunk_embeddings (List[np.ndarray]): List of chunk embeddings.
    k (int): Number of top results to return.

    Returns:
    List[str]: Top-k relevant chunks.
    """
    # Generate an embedding for the query
    query_embedding = get_embedding(query)

    # Calculate cosine similarity between the query embedding and each chunk embedding
    similarities = [cosine_similarity(query_embedding, emb) for emb in chunk_embeddings]

    # Get the indices of the top-k most similar chunks
    top_indices = np.argsort(similarities)[-k:][::-1]

    # Return the top-k most relevant text chunks
    return [text_chunks[i] for i in top_indices]

def generate_response(system_prompt, user_message, model="meta-llama/Llama-3.2-3B-Instruct"):
    """
    Generates a response from the AI model based on the system prompt and user message.

    Args:
    system_prompt (str): The system prompt to guide the AI's behavior.
    user_message (str): The user's message or query.
    model (str): The model to be used for generating the response. Default is "meta-llama/Llama-2-7B-chat-hf".

    Returns:
    dict: The response from the AI model.
    """
    response = client.chat.completions.create(
        model=model,
        temperature=0,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_message}
        ]
    )
    return response

# 完整调用流程
def semantic_chunking_rag_pipeline(pdf_path, query):
    # 1. 提取PDF文本
    extracted_text = extract_text_from_pdf(pdf_path)

    # 2. 按句子分割
    sentences = extracted_text.split(". ")

    # 3. 生成句子嵌入
    embeddings = [get_embedding(sentence) for sentence in sentences]

    # 4. 计算句子间相似度
    similarities = [cosine_similarity(embeddings[i], embeddings[i + 1]) for i in range(len(embeddings) - 1)]

    # 5. 计算断点(使用百分位数方法)
    breakpoints = compute_breakpoints(similarities, method="percentile", threshold=90)

    # 6. 分割成语义块
    text_chunks = split_into_chunks(sentences, breakpoints)

    # 7. 创建块嵌入
    chunk_embeddings = create_embeddings(text_chunks)

    # 8. 语义搜索
    top_chunks = semantic_search(query, text_chunks, chunk_embeddings, k=2)

    # 9. 生成回答
    system_prompt = "You are an AI assistant that strictly answers based on the given context. If the answer cannot be derived directly from the provided context, respond with: 'I do not have enough information to answer that.'"
    user_prompt = "\n".join([f"Context {i + 1}:\n{chunk}\n=====================================\n" for i, chunk in enumerate(top_chunks)])
    user_prompt = f"{user_prompt}\nQuestion: {query}"

    ai_response = generate_response(system_prompt, user_prompt)
    return ai_response.choices[0].message.content

代码讲解

  • 句子分割:按句号分割文本成句子
  • 嵌入生成:为每个句子生成向量表示
  • 相似度计算:计算相邻句子的余弦相似度
  • 断点检测:使用百分位数方法找到语义断点
  • 语义分块:根据断点将句子组合成语义块
  • 检索生成:基于语义块进行检索和答案生成

主要特点

  • 基于语义相似度的智能分块
  • 支持多种断点检测方法(百分位数、标准差、四分位距)
  • 保持语义连贯性
  • 比固定长度分块更精准

使用场景

  • 长文档处理
  • 需要保持语义完整性的场景
  • 复杂问答系统
  • 学术论文、技术文档等结构化文本

RAG系列-基础RAG(Simple RAG)

作者 作者
2025年6月18日 00:00

01. 基础RAG(Simple RAG)

方法简介

基础RAG(Retrieval-Augmented Generation)是最简单的检索增强生成方法。它通过向量化检索获取与用户查询最相关的文档片段,并将这些片段作为上下文输入给大语言模型进行答案生成。

核心代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import fitz
import os
import numpy as np
import json
from openai import OpenAI

def extract_text_from_pdf(pdf_path):
    """
    Extracts text from a PDF file and prints the first `num_chars` characters.

    Args:
    pdf_path (str): Path to the PDF file.

    Returns:
    str: Extracted text from the PDF.
    """
    # Open the PDF file
    mypdf = fitz.open(pdf_path)
    all_text = ""  # Initialize an empty string to store the extracted text

    # Iterate through each page in the PDF
    for page_num in range(mypdf.page_count):
        page = mypdf[page_num]  # Get the page
        text = page.get_text("text")  # Extract text from the page
        all_text += text  # Append the extracted text to the all_text string

    return all_text  # Return the extracted text

def chunk_text(text, n, overlap):
    """
    Chunks the given text into segments of n characters with overlap.

    Args:
    text (str): The text to be chunked.
    n (int): The number of characters in each chunk.
    overlap (int): The number of overlapping characters between chunks.

    Returns:
    List[str]: A list of text chunks.
    """
    chunks = []  # Initialize an empty list to store the chunks

    # Loop through the text with a step size of (n - overlap)
    for i in range(0, len(text), n - overlap):
        # Append a chunk of text from index i to i + n to the chunks list
        chunks.append(text[i:i + n])

    return chunks  # Return the list of text chunks

# Initialize the OpenAI client with the base URL and API key
client = OpenAI(
    base_url="https://api.studio.nebius.com/v1/",
    api_key=os.getenv("OPENAI_API_KEY")  # Retrieve the API key from environment variables
)

def create_embeddings(text, model="BAAI/bge-en-icl"):
    """
    Creates embeddings for the given text using the specified OpenAI model.

    Args:
    text (str): The input text for which embeddings are to be created.
    model (str): The model to be used for creating embeddings. Default is "BAAI/bge-en-icl".

    Returns:
    dict: The response from the OpenAI API containing the embeddings.
    """
    # Create embeddings for the input text using the specified model
    response = client.embeddings.create(
        model=model,
        input=text
    )

    return response  # Return the response containing the embeddings

def cosine_similarity(vec1, vec2):
    """
    Calculates the cosine similarity between two vectors.

    Args:
    vec1 (np.ndarray): The first vector.
    vec2 (np.ndarray): The second vector.

    Returns:
    float: The cosine similarity between the two vectors.
    """
    # Compute the dot product of the two vectors and divide by the product of their norms
    return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

def semantic_search(query, text_chunks, embeddings, k=5):
    """
    Performs semantic search on the text chunks using the given query and embeddings.

    Args:
    query (str): The query for the semantic search.
    text_chunks (List[str]): A list of text chunks to search through.
    embeddings (List[dict]): A list of embeddings for the text chunks.
    k (int): The number of top relevant text chunks to return. Default is 5.

    Returns:
    List[str]: A list of the top k most relevant text chunks based on the query.
    """
    # Create an embedding for the query
    query_embedding = create_embeddings(query).data[0].embedding
    similarity_scores = []  # Initialize a list to store similarity scores

    # Calculate similarity scores between the query embedding and each text chunk embedding
    for i, chunk_embedding in enumerate(embeddings):
        similarity_score = cosine_similarity(np.array(query_embedding), np.array(chunk_embedding.embedding))
        similarity_scores.append((i, similarity_score))  # Append the index and similarity score

    # Sort the similarity scores in descending order
    similarity_scores.sort(key=lambda x: x[1], reverse=True)
    # Get the indices of the top k most similar text chunks
    top_indices = [index for index, _ in similarity_scores[:k]]
    # Return the top k most relevant text chunks
    return [text_chunks[index] for index in top_indices]

def generate_response(system_prompt, user_message, model="meta-llama/Llama-3.2-3B-Instruct"):
    """
    Generates a response from the AI model based on the system prompt and user message.

    Args:
    system_prompt (str): The system prompt to guide the AI's behavior.
    user_message (str): The user's message or query.
    model (str): The model to be used for generating the response. Default is "meta-llama/Llama-2-7B-chat-hf".

    Returns:
    dict: The response from the AI model.
    """
    response = client.chat.completions.create(
        model=model,
        temperature=0,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_message}
        ]
    )
    return response

# 完整调用流程
def simple_rag_pipeline(pdf_path, query):
    # 1. 提取PDF文本
    extracted_text = extract_text_from_pdf(pdf_path)

    # 2. 分块处理
    text_chunks = chunk_text(extracted_text, 1000, 200)

    # 3. 创建嵌入
    response = create_embeddings(text_chunks)

    # 4. 语义搜索
    top_chunks = semantic_search(query, text_chunks, response.data, k=2)

    # 5. 生成回答
    system_prompt = "You are an AI assistant that strictly answers based on the given context. If the answer cannot be derived directly from the provided context, respond with: 'I do not have enough information to answer that.'"
    user_prompt = "\n".join([f"Context {i + 1}:\n{chunk}\n=====================================\n" for i, chunk in enumerate(top_chunks)])
    user_prompt = f"{user_prompt}\nQuestion: {query}"

    ai_response = generate_response(system_prompt, user_prompt)
    return ai_response.choices[0].message.content

代码讲解

  • 文档处理:使用PyMuPDF提取PDF文本,按字符数分块
  • 嵌入生成:使用BAAI/bge-en-icl模型生成文本嵌入
  • 语义搜索:计算查询与文档块的余弦相似度,返回最相关的k个片段
  • 答案生成:将检索到的上下文与用户问题输入LLM生成答案

主要特点

  • 实现简单,易于理解和扩展
  • 使用余弦相似度进行语义检索
  • 支持PDF文档处理
  • 可配置的检索数量k

使用场景

  • FAQ自动问答
  • 小型企业知识库
  • 结构化文档检索增强
  • 基础文档问答系统

2025年展望

作者 作者
2025年1月1日 00:00

2024年回顾

1月-2月:安家落户

终于完成了人生中的一件大事 - 买房。拿到房产证的那一刻,我和妻子都感到无比欣喜。这个新家不仅是一个住所,更是我们对未来生活的美好期待。

3月-4月:平稳前行

这段时间主要是日常工作和还贷。虽然每个月能存下的钱不多,但生活依然充满欢乐。我们学会了在有限的预算中寻找生活的乐趣。

5月-6月:职场动荡

公司经历了裁员风波,虽然我幸免于难,但这次事件让我对公司的未来产生了疑虑。这段时间充满了迷茫和不确定性,尝试了很多事情但进展不大。

7月-10月:装修新家

新房进入装修阶段,虽然经济压力较大,但每周回长沙监工的过程充满了期待和喜悦。看着新家一点点成型,所有的辛苦都值得。

11月-12月:健康与AI探索

体检发现患有桥本甲状腺炎,这让我开始更加关注身体健康。同时,AI技术的快速发展让我产生了强烈的危机感。经过深入思考和实践,我决定拥抱AI而不是恐惧它。

这段时间,我深入探索了多种AI工具:

  • Cursor
  • Windsurf(开通了Pro版)
  • Cline
  • Aider
  • Zed AI

通过实践,我发现每种工具都有其独特优势,于是开始尝试多种工具结合使用。12月中旬,我开始利用AI接一些外包项目,主要目的有两个:

  1. 缓解经济压力
  2. 深入探索AI能力,提升工作效率

2025年展望

在新的一年里,我为自己设定了以下目标:

  1. AI SOP优化:总结出一套适合自己的AI使用流程和标准操作程序
  2. 产品开发:借助AI工具,完成第一个产品的MVP(最小可行产品)
  3. 全栈开发:开始探索全栈式开发,提升技术广度
  4. AI辅助学习:建立高效的AI辅助学习体系,加速知识获取
  5. 事业基础:为未来的事业发展打下坚实基础

2025年将是充满挑战和机遇的一年。我相信,通过合理利用AI工具,持续学习和自我提升,我能够实现这些目标,为未来创造更多可能性。

“未来属于那些相信梦想之美的人。” - 埃莉诺·罗斯福

当下的事情

作者 作者
2024年11月14日 08:10

当下

本页记录当下我需要专注的事情。更新于2024/12/02 于中国武汉

生活

  • 日常工作:练习专注,寻找目标感
    • 项目稳步推进
    • 测试同学的挑衅淡定对待,工作而已
  • 业余生活:稳定作息,健康生活
    • 坚持做饭
    • 有节奏的作息,拒绝熬夜
  • 运动健身:提高基础代谢
    • 开始跑步,每周至少两次
    • 继续羽毛球运动

学习

  • 读书:
    • 阅读《build a large language model from scratch》 60%
    • 阅读《真需求》梁宁
    • 阅读《亲密关系》罗兰.米勒 20%
    • 阅读《桥本甲状腺炎90天治疗方案》20%
    • 阅读《learning-ebpf》5%
  • 技术:
    • 学习深度包解析技术
    • 学习TCP协议相关知识
    • 学习rust相关知识
  • 写作:
    • 提升写作方面的能力

项目

  • 流量采集器

读《程序员修炼之道》

作者 作者
2023年1月2日 00:00

务实的哲学

  • 团队信任对于创造力和协作至关重要,关键时刻信任的破坏几乎无法修复

  • 提供选择,别找借口– 小黄鸭编程

  • 破窗理论– 不要因为一些危急的事情,造成附加伤害,尽可能控制软件的熵

  • 人们都觉得,加入一个推进中的成功项目更容易一些(煮石头汤的故事)

  • 永远审视项目,不要做温水青蛙,先养成仔细观察周围环境的习惯,然后再项目中这样做

  • 知识和经验是你最重要的资产,但是它们是时效资产,学习新事物的能力是你最重要的战略资产。 知识组合:

    1. 定期投资–安排一个固定的时间和地点学习

      • 每年学习一门新语言
      • 每月读一本技术书
      • 读非技术书
      • 上课– 了解公司之外的人都在做什么
      • 尝试不同的环境
      • 与时俱进–关心最新技术的进展

      想法的交叉是很重要的 批判性思维–批判性思考独到的和听到的东西

    2. 多样化– 熟悉的技能越多越好

    3. 风险管理–不同技术在高风险高回报到低风险低回报区间均匀分布,不要把技术鸡蛋放在一个篮子里

    4. 低买高卖–在一项新兴技术流行之前就开始学习,不过这是押宝

    5. 重新评估调整–不断刷新自己的知识库

  • 批判性思维

    1. 五次为什么
    2. 谁从中收益
    3. 有什么背景
    4. 什么时候在哪里工作可以工作起来
    5. 为什么是这个问题
  • 写一个大纲, 问自己:这是否用正确的方式表达了我想表达的东西,以及现在是表达这个东西的好时机吗?

务实的方法

ETC(easy to change)

核心知道思想

  • DRY(Don’t repeat yourself)
  • 正交性 良好设计中,数据库相关代码应该和用户界面保持正交, 当系统的组件相互之间高度依赖时,就没有局部修理这回事。

Django源码系列:文件变化后server自动重启机制

作者 作者
2022年6月28日 00:00

初试 - 文件变化后 server 自动重启

本源码系列是基于 Django4.0 的源码,可以自行到django 官方下载。

在此之前,不妨先了解下 django 是如何做到自动重启的

开始

django 使用 runserver 命令的时候,会启动俩个进程。

runserver 主要调用了 django/utils/autoreload.pymain 方法。
至于为何到这里的,我们这里不作详细的赘述,后面篇章会进行说明。

主线程通过 os.stat 方法获取文件最后的修改时间进行比较,继而重新启动 django 服务(也就是子进程)。

大概每秒监控一次。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# django/utils/autoreload.py 的 reloader_thread 方法

def reloader_thread():
    ...
    # 监听文件变化
    # -- Start
    # 这里主要使用了 `pyinotify` 模块,因为目前可能暂时导入不成功,使用 else 块代码
    # USE_INOTIFY 该值为 False
    if USE_INOTIFY:
        fn = inotify_code_changed
    else:
        fn = code_changed
    # -- End
    while RUN_RELOADER:
        change = fn()
        if change == FILE_MODIFIED:
            sys.exit(3)  # force reload
        elif change == I18N_MODIFIED:
            reset_translations()
        time.sleep(1)

code_changed 根据每个文件的最后修改时间是否发生变更,则返回 True 达到重启的目的。

父子进程&多线程

关于重启的代码在 python_reloader 函数内

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# django/utils/autoreload.py

def restart_with_reloader():
    # 在这里开始设置环境变量为true
    new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"}
    args = get_child_arguments() #获取执行的命令参数
    # 重启命令在这里开始生效
    while True:
        p = subprocess.run(args, env=new_environ, close_fds=False)
        if p.returncode != 3:
            return p.returncode


def run_with_reloader(main_func, *args, **kwargs):
    signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
    # 刚开始 DJANGO_AUTORELOAD_ENV是没有被设置为true的所以这里会进入到else里。
    try:
        if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true":
            reloader = get_reloader()
            logger.info(
                "Watching for file changes with %s", reloader.__class__.__name__
            )
            start_django(reloader, main_func, *args, **kwargs) # 开启django服务线程
        else:
            exit_code = restart_with_reloader()
            sys.exit(exit_code) # 0为正常退出,其他的会抛出相关的错误
    except KeyboardInterrupt:
        pass

程序启动,因为没有 RUN_MAIN 变量,所以走的 else 语句块。

颇为有趣的是,restart_with_reloader 函数中使用 subprocess.run 方法执行了启动程序的命令( e.g:python3 manage.py runserver ),此刻 RUN_MAIN 的值为 True ,接着执行 _thread.start_new_thread(main_func, args, kwargs) 开启新线程,意味着启动了 django 服务。

如果子进程不退出,则停留在 run 方法这里(进行请求处理),如果子进程退出,退出码不是 3,while 则被终结。反之就继续循环,重新创建子进程。

总结

以上就是 django 检测文件修改而达到重启服务的实现流程。

结合 subprocess.run 和 环境变量 创建俩个进程。主进程负责监控子进程和重启子进程。 子进程下通过开启一个新线程(也就是 django 服务)。主线程监控文件变化,如果变化则通过 sys.exit(3) 来退出子进程,父进程获取到退出码不是 3 则继续循环创建子进程,反之则退出整个程序。

好,到这里。我们勇敢的迈出了第一步,我们继续下一个环节!!! ヾ(◍°∇°◍)ノ゙

关于周报这种小事

作者 作者
2022年6月16日 00:00

从认识到双向链接开始我先使用了 obsidian,然而对于我这种懒癌晚期的人来说,需要结构化的记录,真的不太适合我「取一个好听的名字真的太难了」。当然有一说一,双链这个观点真的是太妙了。

现在的我已经全面转向了 logseq 来进行笔记记录,不得不说这种支持自定义代码的笔记真的不错「虽然我到目前为止使用最多的是高级查询TODO这两个功能,记录笔记的话只是零星记录了几句话,并没有详细的记录或者输出一些东西。」。在 logseq 中是以日期为主线的,这免去了我对要写的内容的抽象主题的负担。

工作日报周报

首先对任务进行分类,在学习了 Logseq 的高级查询语法并了解其能力之后,我决定使用#tag这种形式来组织个人任务和工作任务,属于工作任务的会标记上#work标签,个人任务就不进行标签。

首先就是工作,这个是大块内容,我目前使用 logseq 生成日报&周报,用于之后提交到绩效评估系统中。

日报/周报代码

日报和周报代码上只有 inputs[:yesterday]更改成 inputs[:7d]即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#+BEGIN_QUERY
{
    :title "查询昨天完成的任务"
    :query [:find (pull ?b [*])
       :in $ ?start ?end
       :where
       [?b :block/marker ?m]
       [?b :block/page ?p]
       [?p :page/journal? true]
       [?p :page/journal-day ?d]
       [(>= ?d ?start)]
       [(<= ?d ?end)]
       [?b :block/path-refs [:block/name "work"]]
       [(= "DONE" ?m)]
   ]
   :inputs [:yesterday :today]
}
#+END_QUERY

效果图如下:

Randy Pausch 教授的最后一课

作者 作者
2022年5月29日 00:00

在重听李沐老师的《Resnet 论文精读》这一课的时候,ps:「之前没有好好读,:>)羞耻」。在讲到双栏论文中第一页的第二栏最上面,这个位置在学术界是非常重要的。提到了 Randy Pausch 教授在图形学开创了这一风格的写法,然后提到了他的最后一课「深刻印象」。

于是我从 cmu 网站中找到了当年演讲的材料,并完整的看了视频,这份笔记是 Randy 的人生经验和建议的抄录。

人生经验

  1. Have something to bring to the table, because that will make you more welcom. 你必须要有一些真本领,这样可以让你更受欢迎。
  1. You’ve got to get the fundamentals down because otherwise the fancy stuff isn’t going to work. 你必须练好基本功,否则后面的事情都不会发生。
  1. That was a bit of a setback. But remember, the brick walls are there for a reason. The brick walls are not there to keep us out. The brick walls are there to give us a chance to show how badly we want something. Becuase the brick walls are there to stop the people who don’t want it badly enough. They’re there to stop the other people. Remember brick walls let us show our dedication. They are there to separate us from the people who don’t really want to achieve their childhood dreams. 你总会遇到挫折。但是记住,它们的出现不是没有原因的。砖墙并不是为了挡住我们。它在那里,只是为了测试,我们的决心到底有多迫切。它在那里挡住那些没有强烈决心的人。它不让那些人通过。记住,砖墙的存在是为了显示我们自己付出的决心。它使得我们,同那些并不真的想实现梦想的人得以区分。

为人处世的建议

  1. helping others. 帮助他人。
  1. never lose the childlike wonder. It’s what drives us. 永远不要失去好奇心,它是人类前进的动力。
  1. Loyalty is a two way street. 诚以待人,这样别人也会忠实的对待你。
  1. Never give up. 永远不要放弃
  1. You can’t get there alone. People have to help you. You get people to help you by telling the truth. 你不能单打独斗,必须有人来帮你。只要你讲真话,就会有人来帮你。
  1. Apologize when you screw up and focus on other people, not on yourself. 当你把事情搞砸,首先要向别人道歉,首先关心他们的损失,而不是你自己的损失。
  1. When you do the right thing, good stuff has a way of happening. 如果你做了正确的事,好的结果自然会发生。
  1. Get a feedback loop and listen to it. 注意倾听反馈。
  1. Show gratitude. 感恩
  1. Don’t complain. Just work harder. 不要抱怨,而要加倍努力。
  1. Be good at something, it makes you valuable. 要有一技之长,它使你有价值。
  1. Work hard. 努力再努力。
  1. Find the best in everybody. 注意发现他人的优点。
  1. Be prepared. Luck is truly where preparation meets opportunity. 做好准备。所谓幸运,真的是机会和准备的结合。

transformer源码阅读

作者 作者
2022年5月28日 00:00

本文介绍 Tranformer 的代码。

模型结构

Encoder 将输入序列$(x_{1},\cdots,x_{n})$ 映射成一个连续的序列$z = (z_{1},\cdots,z_{n})$。而 Decoder 根据$z$来解码得到输出序列$(y_{1},\cdots,y_{m})$。Decoder 是自回归的(auto-regressive)–它会把前一个时刻的输出作为当前时刻的输入。Encoder-Decoder 结构模型的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class EncoderDecoder(nn.Module):
	"""
	标准的Encoder-Decoder架构。
	"""
	def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
		super(EncoderDecoder,self).__init__()
		self.encoder = encoder
		self.decoder = decoder
		# 源语言和目标语言的embedding
		self.src_embed = src_embed
		self.tgt_embed = tgt_embed
		# generator主要是根据Decoder的隐状态输出当前时刻的词(单个词)
		# 基本的实现就是隐状态输入一个全连接层,然后接一个softmax变成概率
		self.generator = generator

	def forward(self, src, tgt, src_mask, tgt_mask):
		# 首先调用encode方法对输入进行编码,然后调用decode方法进行解码
		return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)

	def encode(self, src, src_mask):
		# 调用self.encoder函数
		return self.encoder(self.src_embed(src), src_mask)

	def decode(self, memory, src_mask, tgt, tgt_mask):
		# 调用self.decoder函数 注意⚠️:这里定义的memery是encoder的输出结果。
		return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

EncoderDecoder 定义了一种通用的 Encoder-Decoder 架构,具体的 Encoder、Decoder、src_embed、target_embed 和 generator 都是构造函数传入的参数。这样我们做实验更换不同的组件就会更加方便。

1
2
3
4
5
6
7
8
class Generator(nn.Module):
	def __init__(self, d_model, vocab):
		super(Generator, self).__init__()
		# d_model是Decoder输出的大小,vocab是词典的大小
		self.proj = nn.Linear(d_model, vocab)

	def forward(self, x):
		return F.log_softmax(self.proj(x), dim=-1)

注意 ⚠️:Generator 返回的是 softmax 的 log 值。在 pytorch 中为了计算交叉熵损失,有两种方法。第一种方法就是 nn.CrossEntropyLoss(),一种是使用 NLLLoss()。第一种方法更加容易懂,但是在很多开源代码里第二种更常见。

CrossEntropyLoss:

1
2
3
4
5
6
criterion = nn.CrossEntropyLoss()

x = torch.randn(1,5) # 服从0-1的正太分布。
y = torch.empty(1, dtype = torch.long).random_(5)

loss = criterion(x,y)

比如上面的代码,假设是 5 分类问题,x表示模型的输出 logits(batch=1),而 y 是真实分类的下标(0-4)。实际的计算过程为:$loss = -\sum^{5}{i=1}y{i}log(softmax(x_{i}))$。

NLLLoss(Negative Log Likelihood Loss)是计算负 log 似然损失。它输入的 x 是 log_softmax 之后的结果(长度为 5 的数组),y 是真实分类(0-4),输出就是 x[y]。因此代码为:

1
2
3
4
5
m = F.log_softmax(x, dim=1)
criterion = nn.NLLLoss()
x = torch.randn(1, 5)
y = torch.empty(1, dtype = torch.long).random_(5)
loss = criterion(m(x), y)

Transformer 模型也是遵循上面的架构,只不过它的 Encoder 是 N(6)个 EncoderLayer 组成,每个 EncoderLayer 包含一个 Self-Attention SubLayer 层和一个全连接 SubLayer 层。而它的 Decoder 也是 N(6)个 DecoderLayer 组成,每个 DecoderLayer 包含一个 Self-Attention SubLayer 层、Attention SubLayer 层和全连接 SubLayer 层。如下图所示。

Encoder 和 Decoder Stack

前面说了 Encoder 和 Decoder 都是由 N 个相同结构的 Layer 堆积(stack)而成。因此我们首先定义 clones 函数,用于克隆相同的 SubLayer。

1
2
3
def clones(module, N):
	# 克隆N个完全相同的SubLayer,使用了copy.deepcopy
	return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

这里使用了 nn.ModuleList, ModuleList 就像一个普通的 Python 的 List,我们可以使用下标来访问它,它的好处是传入的 ModuleList 的所有 Module 都会注册的 PyTorch 里,这样 Optimizer 就能找到这里面的参数,从而能够用梯度下降更新这些参数。但是 nn.ModuleList 并不是 Module(的子类),因此它没有 forward 等方法,我们通常把它放到某个 Module 里。接下来定义 Encoder:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Encoder(nn.Module):
	# Encoder是N个EncoderLayer的stack
	def __init__(self, layer, N):
		super(Encoder, self).__init__()
		# layer是一个SubLayer,我们clone N个
		self.layers = clones(layer, N)
		# 再加一个LayerNorm层
		self.norm = LayerNorm(layer.size)

	def forward(self, x, mask):
		# 逐层进行处理
		for layer in self.layers:
			x = layer(x, mask)
		# 最后进行LayerNorm
		return self.norm(x)

Encoder 就是 N 个 SubLayer 的 stack,最后加上一个 LayerNorm。我们来看 LayerNorm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class LayerNorm(nn.Module):
	def __init__(self, features, eps=1e-6):
		super(LayerNorm, self).__init__()
		self.a_2 = nn.Parameter(torch.ones(features))
		self.b_2. = nn.Parameter(torch.zeros(feagures))
		self.eps = eps
	def forward(self, x):
		mean = x.mean(-1, keepdim = True)
		std = x.std(-1, keepdim = True)
		return self.a_2 * (x - mean)/(std+self.eps) + self.b_2

LayerNorm:假设数据为[batch_size, unit, 1, features],这里是对整个样本进行 normalization。这里的 Layer Normalization 不是 Batch Normalization。

1
2
3
x -> attention(x) -> x+self-attention(x)[残差] -> layernorm(x+self-attention(x)) => y

y -> dense(y) -> y+dense(y) -> layernorm(y+dense(y)) => z(输入下一层)

这里稍微做了一点修改, 在 self-attention 和 dense 之后加了一个 dropout 层。另一个不同支持就是把 layernorm 层放到前面了。这里的模型为:

1
2
x -> layernorm(x) -> attention(layernorm(x)) -> a + attention(layernorm(x)) => y
y -> layernorm(y) -> dense(layernorm(y)) -> y+dense(layernorm(y))

原始论文的 layernorm 放在最后;而这里把它放在最前面并且在 Encoder 的最后在加了一个 layernorm。这里的实现和论文的实现基本是一致的,只是给最底层的输入 x 多做了一个 LayerNorm,而原始论文是没有的。下面是 Encoder 中的 forward 方法,这样比读者可能会比较清楚为什么 N 个 EncoderLayer 处理完成后还需要一个 LayerNorm。

1
2
3
4
def forward(self, x, mask):
	for layer in self.layers:
		x = layer(x, mask)
	return self.norm(x)

不管是 Self-Attention 还是全连接层,都首先是 LayerNorm,然后是 Self-Attention/Dense,然后是 Dropout,最好是残差连接。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

class SublayerConnection(nn.Module):
	"""
	LayerNorm+sublayer(Self-Attention/Dense) + dropout + 残差连接
	为了简单,把LayerNorm放到了前面,这和原始论文稍有不同,原始论文LayerNorm在最后。
	"""
	def __init__(self, size, dropout):
		supper(SublayerConnection, self).__init__()
		self.norm = LayerNorm(size)
		self.dropout = nn.Droupout(dropout)
	def forward(self, x, sublayer):
		# sublayer是传入的参数,之后进行残差连接
		return x+self.dropout(sublayer(self.norm(x)))

Self-Attention 或者 Dense 并不在这里进行构造,而是放在了 EncoderLayer 里,在 forward 的时候由 EncoderLayer 传入。这样的好处是更加通用,比如 Decoder 也是类似的需要在 Self-Attention、Attention 或者 Dense 前面加上 LayerNorm 和 Dropout 以及残差连接,我们就可以复用代码。但是这里要求传入的 sublayer 可以使用一个参数来调用的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class EncoderLayer(nn.Module):
	# EncoderLayer由self-attn和feed_forward组成
	def __init__(self, size, self_attn, feed_forward, dropout):
		super(EncoderLayer, self).__init__()
		self.size = size
		self.self_attn = self_attn
		self.feed_forward = feed_forward
		self.sublayer = clones(SublayerConnection(size, dropout), 2)
	def forward(self, x, mask):
		x = self.sublayer[0](x, lambda x:self.self_attn(x,x,x,mask))
		return self.sublayer[1](x, self.feed_forward)

为了复用,这里的 self_attn 层和 feed_forward 层也是传入的参数,这里只构造两个 SublayerConnection。forward 调用 sublayer0call方法,最终会调到它的 forward 方法,而这个方法需要两个参数,一个是输入 Tensor, 一个是一个 callable, 并且这个 callable 可以用一个参数来调用。而 self_attn 函数需要 4 个参数(Query 的输入,key 的输入,Value 的输入和 Mask),因此这里我们使用 lambda 的技巧把它变成一个参数 x 的函数(mask 可以堪称已知的数)。因为 lambda 的形参也叫 x.

Decoder

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Decoder(nn.Module):
	def __init__(self, layer, N):
		super(Decoder, self).__init__()
		self.layers = clones(layer, N)
		self.norm = LayerNorm(layer.size)

	def forward(self, x, memory, src_mask, tgt_mask):
		for layer in self.layers:
			x = layer(x, memory, src_mask, tgt_mask)
		return self.norm(x)

Decoder 也是 N 个 DecoderLayer 的 stack,参数 layer 是 DecoderLayer,它也是一个 callable,最终call会调用 DecoderLayer.forward 方法,这个方法需要 4 个参数,输入 x, Encoder 层的输出 memory, 输入 Encoder 的 Mask(src_mask)和输入 Decoder 的 Mask(tgt_mask)。所有这里的 Decoder 的 forward 也需要 4 个参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class DecoderLayer(nn.Module):
	# Decoder包括self-attn, src-attn, feed_forward
	def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
		super(DecoderLayer, self).__init__()
		self.size = size
		self.self_attn = self_attn
		self.src_attn = src_attn
		self.feed_forward = feed_forward
		self.sublayer = clones(SublayerConnection(size, dropout), 3)
	def forward(self, x, memory, src_mask, tgt_mask):
		m = memory
		x = self.sublayer[0](x, lambda x: self.self_attn(x,x,x,tgt_mask))
		x = self.sublayer[1](x, lambda x: self.src_attn(x,m,m,src_mask))
		return self.sublayer[2](x, self.feed_forward)

DecoderLayer 比 EncoderLayer 多了一个 src-attn 层,这是 Decoder 时 attend to Encoder 的输出(memory)。src_attn 和 self_attn 的实现是一样的,只不过使用的 Query, Key 和 Value 的输入不同。普通的 Attention(src_attn)的 Query 是从下层输入进行来的。 Key 和 Value 是 Encoder 最后一层的输出 memory;而 Self-Attention 的 Query, Key 和 Value 都是来自下层输入进来的。

Decoder 和 Encoder 有一个关键的不同:Decoder 在解码第 t 个时刻的时候只能用$1 \cdots t$时刻的输入,而不能使用$t+1$时刻及其之后的输入。因此我们需要一个函数来产生一个 Mask 矩阵,代码如下:

1
2
3
4
5
def subsequent_mask(size):
	# mask out subsequent positoins
	attn_shape = (1, size, size)
	subsequent_mask = np.triu(np.ones(attn_shape),k=1).astype('uint8')
	return torch.from_numpy(subsequent_mask) == 0

我们阅读代码之前先看它的输出:

1
2
3
4
5
6
7
print(subsequent_mask(5))
# 输出
1 0 0 0 0
1 1 0 0 0
1 1 1 0 0
1 1 1 1 0
1 1 1 1 1

我们发现它输出的是一个方阵,对角线和下面都是 1。第一行只有第一列是 1,它的意思是时刻 1 只能 attend to 输入 1, 第三行说明时刻 3 可以 attend to ${1, 2, 3 }$而不能 attend to ${4,5}$的输入,因为在真正 Decoder 的时候这是属于 Future 的信息。

MultiHeadedAttention 多头注意力机制

Attention(包括 Self-Attention 和普通的 Attention)可以堪称一个函数,它的输入是 Query,Key,Value 和 Mask,输出是一个 Tensor。其中输出是 Value 的加权平均,而权重来自 Query 和 Key 的计算。具体的计算如下图所示,计算公式为:$$Attention(Q,K,V) = softmax(\frac{QK^{T}}{\sqrt{d_{k}}})V$$

代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def attention(query, key, value, mask=None,dropout=None):
	d_k = query.size(-1)
	scores = torch.matmul(query,key.transpose(-2,-1))/math.sqrt(d_k)
	if mask is not None:
		scores = scores.mask_fill(mask==0,-1e9)
	p_attn = F.softmax(scores, dim=-1)

	if dropout is not None:
		p_attn = dropout(p_attn)
	return torch.matmul(p_attn,value),p_attn

这里主要的疑问是在score.mask_fill,主要用于把 mask 是 0 的变成一个很小的数,这样后面经过 softmax 之后的概率就很接近零(但是理论上还是用了很少一点点未来的信息)。

之前介绍过,对于每一个 Head,都是用三个矩阵$W^{Q}$,$W^{K}$,$W^{V}$把输入转换成 Q,K 和 V。然后分别用每一个 Head 进行 Self- Attention 的计算,最后把 N 个 Head 的输出拼接起来,最后用一个矩阵$W^{O}$把输出压缩一下。具体计算框架为:

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MultiHeadAttention(nn.Module):
	def __init__(self, h, d_model, dropout=0.1):
		super(MultiHeadAttention, self).__init__()
		assert d_model % h == 0
		self.d_k = d_model // h # 这里是整除
		self.h = h
		self.linears = clones(nn.Linear(d_model,d_k),4)
		self.attn = None
		self.dropout = nn.Dropout(p=dropout)

	def foward(self, query, key, value, mask=None):
		if mask is not None:
			# 所有h个head的mask都是相同的
			mask = mask.unsqueeze(1)
		nbatches = query.size(0)
		# 1) 首先使用线性变换,然后把d_model分配给h个Head,每个head为d_k=d_model/h
		query, key, value = [l(x).view(nbatches,-1,self.h,self.d_k).transpose(1,2) for l,x in zip(self.linears,(query, key, value))]
		# 2) 使用attention函数计算
		x, self.attn = attention(query, key, value, mask=mask,dropout = self.dropout)
		# 3)
		x = x.transpose(1,2).contiguous().view(nbatches, -1,self.h*self.d_k)
		return self.linears[-1](x)

我们首先来看构造函数, 这里 d_model(512)是 Multi-Head 的输出大小,因为有 h(8)个 head, 因此每个 head 的 d_k=512/8=64。接着我们构造 4 个(d_model $*$ d_model)的矩阵,后面我们会看到它的用处。最后是构造一个 Dropout 层。

然后我们来看 forward 方法。输入的 mask 是(batch,1,time)的,因为每个 head 的 mask 都是一样的,所以先用 unsqueeze(1)变成(batch,1,1,time),mask 我们前面已经分析过了。

接下来就是根据输入的 query, key, value 计算变换后的 Multi-Head 的 query, key 和 value。zip(self.linears, (query,key,value))是把(self.linear[0], self.linear[1], self.linear[2])(query, key, value)放在一起遍历。我们可以只看一个self.linear[0] (query)。根据构造函数的定义,self.linear[0]是一个[512,512]的矩阵,而query(batch, time, 512),相乘之后得到的新 query 还是 512 维的向量,然后用 view 把它变成(batch, time, 8, 64)。然后 transpose 成(batch, 8, time, 64),这是 attention 函数要求的 shape。分别对 8 个 Head,每个 Head 的 Query 都是 64 维。最后使用self.linear[-1]x进行线性变换,self.linear[-1][512, 512]的,因此最终的输出还是(batch, time, 512)

对个人发展的思考

作者 作者
2022年5月15日 00:00

这一段时间我也一直在思考,毕业之后的发展。读了许多大佬写的博客,想着能够从中汲取一些经验和启发。发现大家都有一个共性就是善于总结和对自己的职业生涯进行了一定的规划,他们清楚的了解自己的定位和未来想要得到什么。我一直坚信花时间来整理状态,是一件很值得投资的事情。这篇文章也相当于研究生生涯总结和职业生涯的开篇吧!

毕业前的时光算得上是踏上职场前最后的悠闲。尽管对未来充满了希望,与此同时,我的内心也充斥着彷徨。害怕在未来几年一无所成,害怕自己达不到自己所期待的那种境界。每年设定目标,然而能够完成的却寥寥无几。才有了现在对未来的迷茫,心中一腔热血却像苍蝇一样,漫无目的。这可能也是普通人和大佬之间的区别吧!阅读过大佬的文章后,有下面的启示。

别多想,多实践

  1. 你思考的 90%的问题,至少 1000 年前,先哲们都给了答案。
  2. 剩下的 10%,300 年前的哲学家,思想家一定给出了答案。
  3. 绝对不存在你思考的问题,历史上的思想家没有给出答案。所以大多数的时候,没看书之前,你的思考,是徒劳的。

大多数时候,我们所思考的问题,前人都已经思考过了。我们需要做的是,踏踏实实的沉下心来,进行学习即可。我们总在思考未来应该如何做才能快速的成长,追求完成一件事情的形式。殊不知未来正是由无数个当下的事情组成的。与其担心未来,还不如沉下心来把当下的事情做到极致,未来一定会出现在你的眼前。

尽早规划(长线思维)

  1. 怎么过一天,就怎么过一生,如果认为明天或一年之后会有所改变,那么今天的自己是一年前希望看到的自己么。
  2. 随着时间的推移,资产(你认为有价值的一切)变得更有价值还是廉价
  3. 把每一个场景都看成投资场景,每一个行为当作投资行为,重视它对现在及将来的影响

咱们中国有一个不变的传统就是“五年规划”,国家尚且如此,何况人?所以我们也要及早的对自己进行一个五年规划,这个规划应该是方向性的,不是细节的。细节应当是每年每季度每月具体去完成以达到五年规划中的目标。

注重输出

  1. 只是看书或者视频,容易造成已经理解了某个知识点的错觉,短期记忆未经加固,很快就会「挥发」
  2. 无输出不输入,输出的方式可以是文章或者视频或者闲聊,经过强化后的内容更容易进入长期记忆
  3. 输出的过程会联结之前的积累,让知识更扎实,输出过程也会更流畅,输入和输出的比例可以控制在 3:7

在知识库系统的过程中,我们往往过于完美主义,想把知识系统工具一次性构建完整,然后把大量的时间都放在了工具的搭建中,折腾了很多中工具:notion, obsidian, logseq 等。我个人认为,工具是在自己不断的使用的过程中完善的,知识库也是一样,首先我们要有输出,之后在慢慢的进行调整,最后会实现一个理想中适合自己的知识系统的。我在这里建议放弃完美主义,在实践中不断的调整自己调整工具。还有一个就是逃离算法推荐吧,拥抱内容的多元化。

不要只停留在理论上,去实践,会发现更多的问题和挑战,也更有趣味,“what i cannot create, i do not understand”。

参考资料

  1. 工程师的成长
  2. 写在 28 岁边上
  3. 技术同学在业务中的成长
  4. 用未来的视角来看今天
❌
❌