☰
两天学习冲刺总结:Python文件读取、神经网络与分治法的实战笔记
2026/11/3 23:40:40 网站建设 项目流程

这两天对我来说算是一个小型的知识冲刺。周六周日加起来大约14个小时,集中啃了四块东西:python文件读取、编程英语、机器学习里的神经网络、算法导论里的分治法。平时工作里也会用到文件读写和pandas,但对底层细节一直模模糊糊;机器学习只看过概念没动过手;算法停留在刷题层面,推导不深。这次干脆把每一个主题都按“概念-代码-踩坑-换角度再看”的方式过了一遍,感觉比单纯看视频效率高很多。这篇文章就当作一份可复用的学习总结,把我两天的笔记、示例代码和翻车记录都整理出来。如果你也是自学路线,这份内容应该能直接抄作业,不需要再自己到处凑资料。

1. Python文件读取:从内置open到pandas,这两天踩平的读写坑

文件读取是Python最基础也最容易出问题的环节。平时脚本里写open("xxx.txt")挺顺手,但一旦涉及路径、编码、大文件,各种坑就冒出来了。我这次系统地把内置文件操作和pandas的常见场景都过了一遍,整理成下面的笔记。

1.1 先搞清楚文件路径:相对路径和绝对路径的实战选择

大多数教程只告诉你open("data.txt"),但实际项目里这个写法很容易翻车。因为Python的工作目录不是你写代码的文件所在目录,而是你执行命令时所在的目录。比如你在/home/user/project下执行python /home/user/project/scripts/read.py,那open("data.txt")查找的是/home/user/project/data.txt,不是scripts/data.txt。

所以我现在写脚本,第一件事就是打印当前工作目录:

import os print(os.getcwd())

确认当前工作目录之后再决定用相对路径还是绝对路径。如果只是自己用,绝对路径最省心,但要考虑脚本在不同机器上运行时路径写死的问题。我自己的习惯是:脚本放在项目根目录下,数据文件放在data/子目录,然后用Path对象拼接路径:

from pathlib import Path base_dir = Path(__file__).resolve().parent data_file = base_dir / "data" / "example.txt"

Path对象比字符串os.path.join优雅很多,支持/运算符直接拼接,跨Windows、Linux、macOS都没有反斜杠转义的问题。Windows用户尤其容易踩反斜杠的坑,比如"C:\Users\name\data.txt"里的\n会被当成换行符,虽然在文件路径里多数情况不报错,但一旦目录名是\t、\n这种超恶心。用Path就完全避开这个问题。

1.2 open/with/编码:基础API不要只停留在写demo

open函数最需要注意的是模式参数。默认是"r"文本读模式,如果读二进制文件比如图片、音频,必须用"rb";写二进制用"wb"。还有个容易被忽略的encoding参数:open("file.txt", "r", encoding="utf-8")。不指定编码时,Windows下默认可能是gbk,Linux下是utf-8,同一份代码跨平台跑可能直接乱码。

我这次专门测试了read、readline、readlines三兄弟,它们的区别很简单:

  • read()一次性把整个文件内容读成字符串,适合小文件。
  • readline()每次读一行,适合逐步处理,比如日志文件。
  • readlines()返回每行组成的列表,注意它会保留换行符。

实际开发中最常用的是直接迭代文件对象,内存占用小:

with open("data.txt", "r", encoding="utf-8") as f: for line in f: print(repr(line))

打印用repr是因为每行结尾会带\n,直接print看不到换行的存在。如果需要去掉换行,用line.strip()或line.rstrip("\n")。这里有个小坑:strip()会把行首行尾的空格和\t也去掉,如果只在末尾去换行,最好用rstrip("\n")。

为什么一定要用with?因为文件打开后如果不关闭,进程会一直占着文件句柄。with是上下文管理器,代码块结束后自动调用close(),即使中间抛异常也会关闭。我自己早期写代码经常忘了close,后来形成肌肉记忆:只要用文件,一律with。

1.3 pandas读取Excel/CSV:真正的效率工具

如果是处理表格数据,直接读成DataFrame比手动逐行解析快一个数量级。这两天实测最常用的两个函数:

import pandas as pd df_csv = pd.read_csv("data.csv", encoding="utf-8") df_excel = pd.read_excel("data.xlsx", sheet_name="Sheet1", engine="openpyxl")

read_csv需要关注几个参数:sep指定分隔符,默认是逗号,但有些数据是制表符或分号;header指定表头在哪一行,header=None表示无表头;skiprows跳过开头几行。我这次处理一个从系统导出的csv,第一行是生成时间说明,第二行才是真正的列名,一开始直接读进去,列名全是乱的,后来加了skiprows=1才正常。

read_excel要特别注意engine参数。.xlsx文件需要openpyxl,.xls文件需要xlrd,没装对应库会直接报ImportError。另外sheet_name=None可以一次读取所有Sheet,返回一个字典,键是Sheet名,值是对应DataFrame。这个在处理多Sheet报表时非常实用。

读取之后的乱码问题大概率是编码不对。我建议拿到外部文件先做个小测试:

with open("unknown.csv", "rb") as f: print(f.read(50))

打印前50字节,根据内容判断是utf-8还是gbk。如果看到\xff\xfe开头,就是UTF-16,Python里要指定encoding="utf-16"。别问我为什么知道,问就是吃过亏。

1.4 异常处理和踩坑笔记

文件操作最常见的异常就三类:FileNotFoundError、PermissionError、UnicodeDecodeError。不处理异常时,程序直接崩溃,而且报错信息对新手不友好。我现在统一用try/except包住关键文件读取:

try: with open("data.txt", "r", encoding="utf-8") as f: content = f.read() except FileNotFoundError: print("文件不存在,先检查路径") except PermissionError: print("没有权限读取,检查文件权限") except UnicodeDecodeError: print("编码不对,尝试 gbk 或 utf-8")

这三天我还遇到一个奇怪问题:连续往同一个CSV文件追加写入,数据中间出现了空行。原因是用"w"模式写入时,如果newline不处理,Windows下写入的换行符会多一个\r。解决办法是打开文件时加newline="":

with open("output.csv", "w", newline="", encoding="utf-8") as f: f.write("a,b,c\n")

这个细节在文档里写得清楚,但很多人不会注意。总之,文件读取的边界情况比想象中的多,建议每天抽半小时专门处理一种异常,积累一段时间就很有手感。

2. 编程英语:不是背单词,是在报错信息里学编程

这个主题是我这两天加进去的计划。以前看英文文档总是硬着头皮,遇到报错就截图翻译,效率很低。后来发现英语和编程的联系比想象中紧密得多,很多概念单词本身就是操作或逻辑的描述,理解词根之后看代码、看文档都不需要“翻译”这个过程。

2.1 为什么自学编程要专门学编程英语

报错信息、官方文档、开源库的README、Stack Overflow上的回答,绝大部分都是英文。即使有中文教程,很多专业术语也保留了英文原词:比如epoch、batch、gradient,翻译成中文反而别扭。不懂这些词,连查资料都不知道搜什么关键词。

我这两天的经验是:不必专门背单词,但每遇到一个反复出现的词,就停下来查清楚它的动词、名词含义和语境。比如parse翻译是“解析”,但真正含义是把字符串拆成结构化数据,理解了这点,看到json.load()是“把JSON文本解析为Python对象”就一目了然。

2.2 高频词汇分类整理

我按日常开发的频率整理了几个类别,都是这两天真实用到的:

场景词汇含义
文件操作read, write, append, close读、写、追加、关闭
文件操作path, directory, file, extension路径、目录、文件、扩展名
数据结构list, tuple, dictionary, set列表、元组、字典、集合
数据结构push, pop, insert, delete, append压入、弹出、插入、删除、追加
流程控制iterate, loop, condition, branch遍历、循环、条件、分支
函数function, argument, parameter, return函数、实参、形参、返回
错误exception, traceback, syntax, runtime异常、回溯、语法、运行期
机器学习feature, label, sample, normalize特征、标签、样本、归一化
机器学习forward pass, backward pass, gradient前向传播、反向传播、梯度
算法divide, conquer, combine, recurrence分解、解决、合并、递归

这些词不需要刻意背,写代码时碰到一次记一次,比单词书有效得多。我是在笔记本上开了一页专记编程英语,只写单词+一个代码示例,不写中文翻译,因为代码里中文注释多了容易混淆思路。

2.3 在报错信息中养成阅读英文的习惯

报错信息是学习编程英语最好的语料。举一个实际的例子,这是我调试神经网络时最常见的报错:

Traceback (most recent call last): File "train.py", line 20, in <module> loss.backward() NameError: name 'loss' is not defined

第一行Traceback是“回溯”,指错误发生时的调用路径。然后每一行都说明文件名、行号、模块名,最后一行NameError:name 'loss' is not defined才是核心问题——loss这个名字没定义,也就是这个变量还没赋值或拼写错误。

很多新手看到Traceback就慌,其实只要读最后一行和往上数几行,定位很快。我现在遇到报错,第一反应不是翻译成中文,而是直接在脑子里过一遍:IndexError是索引越界,KeyError是字典/DataFrame里没有这个键,AttributeError是对象没有这个属性。用英文读秒懂,用中文反而要绕一圈。

2.4 通过英文术语反推概念本质

有些英文术语甚至能帮你理解概念本身。比如divide and conquer(分治法),conquer是“征服/解决”,在算法里是指把子问题解决掉,而不是“征服数据”,这个动词理解到位了,递归结构就自然了。backpropagation就是backward propagation的缩写,意思是误差从后往前传播,理解了“back”和“forward”的相对关系,神经网络的前向/反向过程就清晰了。recurrence和recur同根,表示“再次发生”,递归式就是一步一步往回套的递推关系。

所以编程英语不是独立于编程之外的技能,它嵌在每一行代码、每一个报错、每一份文档里。只要坚持“遇到一个词查清楚一个”,三个月之后读英文文档的流畅度会明显不一样。

3. 神经网络:从感知机到反向传播,把概念变成能跑的代码

机器学习涵盖太广,我这次特意聚焦神经网络,原因很简单:它既有直观的图形化理解(神经元、层、激活),又有严格的数学推导(链式法则、梯度),非常适合用来打通“理论”和“代码”之间的障碍。

3.1 神经网络的构成:从感知机到全连接层

先从最基础的结构说起。一个神经元接收多个输入,每个输入都有一个权重,神经元内部把输入加权求和,再加上一个偏置,最后通过激活函数输出结果。如果用数学表达,就是:

z = W * x + b a = activation(z)

其中W是权重向量,x是输入向量,b是偏置标量,activation是激活函数。多个神经元横向排布就是一层,层和层之间的输出-输入连接就是全连接层。一个最简单的两层神经网络就是:

  • 输入层:接收原始数据,比如一张28x28像素的图片,展平后是784维。
  • 隐藏层:若干神经元,对输入做非线性变换。
  • 输出层:给出最终的预测结果,比如手写数字分类的10个类别概率。

没有激活函数的神经网络无论堆多少层都只是线性变换,本质上是一个线性回归。激活函数的作用是引入非线性。我实践的三个主要激活函数:

函数公式特点
Sigmoid1/(1+e^(-x))输出在0到1之间,适合二分类输出;但两端梯度饱和
Tanh(e^x-e^(-x))/(e^x+e^(-x))输出在-1到1之间,均值更接近0
ReLUmax(0, x)计算简单,梯度不饱和,但负区间梯度为0

ReLU在隐藏层中基本是默认选择。一开始我纠结为什么负区间梯度为0了还能用,后来理解了反向传播是在损失函数可导的前提下做链式求导,负区间梯度为0只会让部分神经元“死亡”,但对整体来说稀疏性反而有助于泛化。

3.2 前向传播、损失、反向传播:一条链式法则串起来

前向传播很好理解:输入数据经过每一层计算,得到输出。然后计算损失函数,衡量预测值和真实值的差距。分类问题常用交叉熵损失,回归问题用均方误差。反向传播则是利用微积分里的链式法则,从输出层开始逐层计算每个参数对损失的梯度,然后用梯度下降更新参数。

我这次用手推了一个最简单两层网络的一轮反向传播,核心逻辑是这样的:

设输出层损失对输出层输出的梯度为delta_out,它等于损失函数对输出层激活值的偏导乘上激活函数的导数。隐藏层的误差delta_hidden等于delta_out乘上输出层权重,再乘上隐藏层激活函数的导数。最后每个参数的更新量就是该层的误差乘上上一层输出。

用numpy实现的核心部分:

import numpy as np def sigmoid(x): return 1 / (1 + np.exp(-x)) def sigmoid_deriv(a): return a * (1 - a) # 假设 W1, b1, W2, b2 已初始化,X 是输入,Y 是真实值 for epoch in range(200): # 前向传播 hidden = sigmoid(np.dot(X, W1) + b1) output = sigmoid(np.dot(hidden, W2) + b2) # 均方误差损失 loss = np.mean((Y - output) ** 2) # 反向传播 d_output = 2 * (output - Y) * sigmoid_deriv(output) d_hidden = np.dot(d_output, W2.T) * sigmoid_deriv(hidden) # 参数更新 W2 -= learning_rate * np.dot(hidden.T, d_output) b2 -= learning_rate * np.sum(d_output, axis=0) W1 -= learning_rate * np.dot(X.T, d_hidden) b1 -= learning_rate * np.sum(d_hidden, axis=0)

这段代码去掉了很多边角细节,比如初始化、归一化、轮次打印,但反向传播的核心计算就这几行。我发现只要自己推一遍链式法则,再看torch或TensorFlow里的loss.backward()就不会觉得是黑魔法。

3.3 学习率、batch、epoch:三个最容易被忽略的超参数

理论都通了,代码能跑起来,模型效果却可能很差。这两天我调参踩的坑都集中在三个超参数上:

  • learning_rate(学习率):每次参数更新的步长。初始设成0.1,损失曲线直接震荡,后来改成0.01才收敛。学习率过大会越过最优点,过小则收敛极慢。实践中常用0.01或0.001起步,配合学习率衰减。
  • batch(批次大小):每次更新参数用的样本数。最极端的是batch=1,即随机梯度下降,噪声很大;batch等于全量数据就是批量梯度下降,稳定但计算量大。常用32、64、128。batch越小,梯度噪声越大,反而有时能跳出局部最优。
  • epoch(轮次):完整遍历整个训练集的次数。不是越大越好,后期过拟合风险增加。我习惯在每个epoch结束时在验证集上算一下loss,一旦验证loss开始升高,就提前停止。

除此之外,输入数据的归一化非常重要。我一开始直接把0到255的像素值喂给神经网络,loss死活降不下去;归一化到0到1之后,一轮的效果就比之前十轮还好。原因是神经网络对输入尺度很敏感,大数值会让权重梯度范围变得很大,训练不稳定。

3.4 用numpy手搓一个两层网络,理解比调包更重要

这节想强调一点:如果你正在学神经网络,不要一上来就用PyTorch或TensorFlow,先用numpy手写一个最简版本。因为框架帮我们做了太多事:自动求导、反向传播、参数优化,全被封装成了黑盒。手写一遍之后,每个环节的输入输出维度、矩阵乘法形状、梯度计算公式,都会非常清晰。

比如最常见的报错ValueError: shapes (64,10) and (16,64) not aligned,看起来莫名其妙。但你自己写一遍就会发现,矩阵乘法要求前一个矩阵的列数等于后一个矩阵的行数,网络层与层之间的维度必须匹配。一旦理解了维度流动,这类报错一眼就看出是哪个W的形状写错了。

纸上推一遍加上手写一次,十个错误有八个都能自己定位。神经网络没那么神秘,本质上就是一堆矩阵乘法和微积分。

4. 分治法:归并排序、最大子数组与递归式推导

算法导论的分治法是我这两天的最后一盘菜。以前刷题时经常遇到merge_sort,但只知道是递归,没认真理解过“分治”这个思想。这次从头啃了一遍,很多东西都串起来了。

4.1 分治三步骤:分解、解决、合并

分治法的核心思想是三步:分解、解决、合并。

  • 分解(Divide):把原问题划分成若干个规模更小的子问题。
  • 解决(Conquer):递归地解决每个子问题。如果子问题小到可以直接解决,就不再递归。
  • 合并(Combine):把子问题的解合并成原问题的解。

最经典的例子就是归并排序:把数组从中间拆成两半,递归排序两半,然后再合并两个有序数组。这个思想之所以能成立,前提是子问题与原问题结构相同、规模更小,最终小到可以直接解决(比如数组长度为1时天然有序)。

理解这三点之后,看很多算法就顺了:快速排序是选一个基准值拆分,然后拼接;二分查找是每次砍掉一半,直接在子区间里继续;二叉树的遍历也是分治思想,先处理左子树,再处理右子树。我最大的体会是:分治不是一种具体算法,而是一种思考方式。

4.2 归并排序:学习分治的入门案例

归并排序的代码我在面试和日常工作里写过很多次,但这次才认真分析了它的过程。先看核心实现:

def merge_sort(nums): if len(nums) <= 1: return nums mid = len(nums) // 2 left = merge_sort(nums[:mid]) right = merge_sort(nums[mid:]) return merge(left, right) def merge(a, b): i = j = 0 result = [] while i < len(a) and j < len(b): if a[i] <= b[j]: result.append(a[i]) i += 1 else: result.append(b[j]) j += 1 result.extend(a[i:]) result.extend(b[j:]) return result

理解这个代码的关键在于递归的“信任”:不需要跟踪每一层递归到底怎么走,只需要相信merge_sort(left)能返回一个排好序的数组,merge_sort(right)也一样。然后是merge函数,它利用左右两个数组各自有序的特点,用双指针线性扫描合并,时间复杂度O(n)。

这里有个细节我之前一直忽略:nums[:mid]每次创建新列表,空间复杂度是O(n)。如果追求更优,可以用原地索引区间的方式,只传递数组和start、end下标,避免切片复制。实际工程中归并排序主要用于外部排序,比如内存放不下整个文件时,先分段排序再归并,完全不依赖随机访问。

4.3 最大子数组问题:跨越中点的思维转变

归并排序之后,我又选了最大子数组问题练手。问题描述是:给定一个整数数组,找出一个连续子数组,使得子数组的和最大。比如[-2, 1, -3, 4, -1, 2, 1, -5, 4]的最大子数组是[4, -1, 2, 1],和为6。

暴力解是枚举所有起点和终点,复杂度O(n²)。用分治法可以优化到O(n log n),而且思路很漂亮:把数组分成两半,最大子数组要么完全在左边,要么完全在右边,要么跨越中点。前两种情况递归解决即可,关键在第三种情况:从中间向左扫描找最大连续和,从中间向右扫描找最大连续和,两者之和就是跨越中点的最大和。

这个“跨越中点”的分析是重点:最大子数组不会说破就必须和哪个边界对齐,它可能横跨分界点。所以分治的合并步骤不能简单返回max(左,右),还必须考虑跨过中点的可能性。

我写这段代码时踩了一个逻辑坑:从中间向左扫描时,是在一个部分和的过程中找最大,而不是从端点直接开始累计。正确的做法是维护left_sum并不断更新最大值,扫描结束后得到左边包含中点元素的最大和。同理右边。最后跨中值就是left_max + right_max。

4.4 递归式和主定理:分析时间复杂度的工具

分治法的时间复杂度通常用递归式来表示。以归并排序为例,T(n)=2T(n/2)+O(n),意思是规模为n的问题被分解为2个规模为n/2的子问题,合并需要O(n)。解这个递归式的标准工具是主定理。

主定理针对形如T(n)=aT(n/b)+f(n)的递归式。比较n^(log_b a)和f(n)哪个增长更快:

  • 情况1:如果f(n)增长慢于n^(log_b a),则T(n)=Θ(n^(log_b a))。
  • 情况2:如果两者增长速度相当,则T(n)=Θ(n^(log_b a) log n)。
  • 情况3:如果f(n)增长快于n^(log_b a),且满足某种正则条件,则T(n)=Θ(f(n))。

以归并排序为例:a=2,b=2,f(n)=O(n),n^(log_b a)=n^1=n,f(n)和n^(log_b a)同阶,属于情况2,所以T(n)=Θ(n log n)。这几个公式看起来抽象,但套进去算两次就记住了。

我这次还花时间分析了二分查找的递归式:T(n)=T(n/2)+O(1),这里a=1,b=2,n^(log_2 1)=n^0=1,f(n)=O(1),两者同阶,情况2,结果是Θ(log n)。

4.5 分治法的边界条件:递归设计最容易翻车的地方

分治法看着简洁,实际写的第一个版本几乎都会在边界条件上翻车。最常见的问题:

  • 递归终止条件写错。比如归并排序里len(nums) <= 1,如果不加<=只写==,当传入空数组时就会无限递归。
  • 左右区间划分重叠或遗漏。比如mid = len(nums) // 2,左半部分是nums[:mid],右半部分是nums[mid:],两边不重叠也不遗漏。如果错写成nums[mid+1:],元素就会漏掉。
  • 跨越中点的范围问题。求最大子数组跨越中点时,左边扫描的起点是mid,右边扫描的终点是mid+1,两个子数组必须紧挨着,否则不能拼成一个连续数组。

这些边界坑没有捷径,只能靠多画几个例子和单步调试。我花了大约四十分钟,把数组长度从1到8的归并排序过程都手写了一遍,才彻底理解切片长度的计算逻辑。

另一个体会是:分析分治法的时间复杂度不能只看递归式,还要看合并步骤。有些问题分解很快,但合并时不小心写成了O(n²),整体复杂度就上去了。写完代码之后,自己算一遍merge里的循环次数和递归层数,能发现不少隐藏的性能问题。


两天时间不长,但把这四个主题串在一起之后,我突然觉得机器学习和算法并没有想象中那么“高大上”。神经网络的前向传播和反向传播就是链式法则和矩阵乘法,分治法就是分解、解决、合并,文件读取就是路径、编码和模式,编程英语就是多读报错、多查词根。都是很具体的事。最后分享一个我自己验证过的小习惯:学完一个主题的第二天早上,不翻笔记,直接重写一遍昨天的核心代码。写不出来就回去再看,写出来才是真的会了。这套流程帮我过滤掉很多“看过就会”的假象,也让我对接下来的深度学习案例有了更踏实的起点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询