干了这么多年算法岗,被人问得最多的问题不是"模型怎么调",而是"你们用的Python到底跟普通Python有啥区别"。每次我都得解释一遍:不是Python有区别,而是算法工程师日常打交道的是一堆高级库——NumPy、Pandas、Scikit-learn、PyTorch这些。它们才是真正把Python从"胶水语言"抬进AI领域的功臣。
这篇文章不打算做那种面面俱到的API手册,我想从一个算法工程师的真实工作流出发,聊聊我们是怎么选库、用库、避坑的。无论你是准备入行算法岗,还是已经在做数据分析想往机器学习方向靠,或者纯粹好奇"高级库"这三个字到底指什么,这篇内容应该都能给你一些参考。我会把环境搭建、核心库的底层逻辑、建模训练、性能优化到完整案例全部串一遍,顺带把那些文档里不会写、但实际踩过才知道的坑也交代清楚。
1. 先搞清楚一件事:算法工程师的"高级库"到底指什么
很多刚接触Python的人会把"高级"理解成"功能多""代码难",这是误区。算法工程师嘴里说的高级库,指的是那些把复杂底层逻辑封装好、提供高层抽象接口、让你能用几行代码完成几十行甚至几百行底层操作的第三方库。它们解决的不是"写不出功能"的问题,而是"如何在合理时间内写出高性能、可维护的算法代码"的问题。
1.1 算法工程师眼中的库分类
我习惯把日常使用的库分成四类,这个分类方式跟官方文档不一样,但更贴近实际工作:
- 数值计算底座:NumPy、SciPy。这两兄弟负责所有矩阵运算、线性代数、数值积分等底子活。NumPy的
ndarray是整个Python科学计算生态的地基,Pandas、Scikit-learn、PyTorch底层都离不开它。 - 数据处理与特征工程:Pandas、Polars。算法项目里70%的时间花在数据清洗和特征构造上,Pandas的
DataFrame模型让表格数据处理变得极其顺手。Polars是后起之秀,处理超大DataFrame时性能优势明显。 - 建模与训练:Scikit-learn、PyTorch、TensorFlow、XGBoost、LightGBM。传统机器学习用Scikit-learn全家桶,深度学习上PyTorch做研究和落地都很顺手,LightGBM和XGBoost在表格数据上依然是杀手锏。
- 工程化与性能工具:joblib、Numba、multiprocessing、asyncio、Dask。这些库负责把算法代码从"能跑"变成"跑得快",也负责模型保存、并发调度、分布式计算。
1.2 为什么Python本身不够用
纯Python解释器的执行效率在数值计算面前是完全不够看的,原因在于Python是动态类型语言,每次变量操作都要经过类型检查和装箱拆箱。你用纯Python写一个双层for循环做矩阵乘法,数据量稍微上去一点就得等到怀疑人生。
高级库的解决思路很直接:把计算密集的部分用C、C++或者CUDA实现,只把接口暴露给Python。NumPy的核心就是C语言实现的,PyTorch的张量操作底层是C++和CUDA,Python在这一层只是"调度员"。你调用np.dot(A, B),真正做矩阵乘法的是编译好的机器码,Python只是把两个数组的指针传进去,再把结果包装成对象返回给你。理解了这个,你就知道为什么"尽量用库函数代替手写循环"是算法工程师的第一条铁律。
2. 环境搭建是最容易翻车的一环:版本、虚拟环境与CUDA
先说一个我见过无数次的场景:新人装了Python,直接pip install numpy,然后又装了TensorFlow,结果导入时报错,提示numpy版本不兼容。紧接着去升级numpy,升级完发现Pandas又不兼容了。这是经典的"依赖地狱"。
2.1 Python解释器版本怎么选
现在(2025年前后)我推荐新项目直接用Python 3.10或3.11。3.12和3.13虽然发布了一段时间,但部分第三方库的预编译wheel可能还没跟上,编译源码安装不仅慢,还容易在Windows上因为缺编译器而失败。3.8和3.9太老,PyTorch、Polars这些库的新版本已经开始放弃对它们的支持。
选版本有个很简单的方法:去你想用的核心库(比如PyTorch)官网看它最新的稳定版要求什么Python范围,以那个为准。因为Python解释器本身不是你的瓶颈,第三方库的兼容性才是。
2.2 conda与pip的定位差异
我自己的习惯是:用conda管理Python环境,用pip安装环境内的Python包。conda的强项是环境隔离和二进制依赖管理——它连CUDA toolkit、MKL这些非Python的原生库都能一起管理,这是pip做不到的。pip的强项是Python包生态最全,很多冷门库只发到了PyPI,conda上找不到。
具体操作层面,新建一个算法项目环境:
conda create -n ml_project python=3.11 conda activate ml_project pip install numpy pandas scikit-learn torch torchvision这里有一个小经验:能先用conda装的库就优先用conda。因为conda在解析依赖时更严格,会把潜在的版本冲突尽早暴露出来。而pip的依赖解析相对宽松,有时候装完了跑起来才发现某个版本不兼容。
2.3 深度学习框架的CUDA版本匹配
如果你要跑PyTorch,安装之前先搞清楚机器上的CUDA驱动和PyTorch要求的关系。有个概念必须区分清楚:系统CUDA驱动版本和CUDA toolkit运行时版本不是一回事。驱动版本向下兼容,比如你的驱动支持CUDA 12.4,那CUDA 12.1的PyTorch也能跑。
安装PyTorch最稳的方式是去PyTorch官网的install页面,选择你的操作系统和CUDA版本,它会给出对应的pip命令。千万别自己瞎猜版本号。手动指定一个CUDA版本装PyTorch的命令大致是:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121装完验证一下:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"如果torch.cuda.is_available()返回False,大概率是CUDA驱动版本太老或者你的机器根本没NVIDIA GPU。先别急着重装,用nvidia-smi看一下驱动支持的CUDA版本,再决定换哪个PyTorch版本。
2.4 vscode配置Python环境的细节
VSCode现在是算法工程师的主流编辑器,但很多人配置完还是遇到解释器选错的问题。核心操作是:Ctrl+Shift+P打开命令面板,输入Python: Select Interpreter,选择你conda环境对应的解释器路径。如果你开了终端,也可以用conda activate先激活环境,再在VSCode终端里运行code .打开项目,这样VSCode会自动识别当前激活的环境。
还有个很容易被忽略的配置:.vscode/settings.json里可以指定默认解释器路径,这样即使你开了多个终端也不会选错:
{ "python.defaultInterpreterPath": "C:/Users/你的用户名/anaconda3/envs/ml_project/python.exe" }Windows下路径一定要写对,macOS/Linux则是/path/to/conda/envs/ml_project/bin/python。
3. NumPy与Pandas的底层逻辑:用向量化思维替代循环思维
3.1 NumPy的广播机制是性能的第一桶金
接触过NumPy的人都知道ndarray,但真正理解广播机制的人并不多。所谓广播,是指两个形状不同的数组做运算时,NumPy能自动把形状较小的数组"扩展"成匹配的形状,而不是复制数据。这个机制用好,很多原本要写for循环的代码可以直接用一行向量化操作完成。
举个特征工程里的例子:你有一批样本特征矩阵X,形状是(10000, 50),要做一个标准化操作,即每个维度减去均值再除以标准差:
import numpy as np X = np.random.randn(10000, 50) mean = X.mean(axis=0) # shape (50,) std = X.std(axis=0) # shape (50,) # 广播机制:X (10000,50) 与 mean (50,) 相减,mean被自动扩展为 (1,50) 再广播到每一行 X_normalized = (X - mean) / std这段代码里没有一行循环。(10000, 50)和(50,)之间的相减在NumPy内部是C语言级别完成的,速度比用Python循环快两个数量级以上。数据量越大,差距越明显。
3.2 别用apply胆战心惊,但也别滥用它
Pandas的apply函数在算法工程师群体里口碑两极分化。新手喜欢它,因为可以写自定义函数逐行处理;老手警惕它,因为apply本质上是Python级别的循环,性能远不如向量化操作。
一个常见的陷阱:对DataFrame按行做复杂逻辑处理时习惯性写apply,结果几百行代码在几百万行的数据上跑了十几分钟。正确的思路是尽可能用内置的向量化方法——df['col1'] + df['col2']这类操作就是Pandas里最快的;其次是用groupby().transform()做分组统计;最后才考虑apply。
给一个对比案例:对一列数据做分桶,把数值映射为类别。
import pandas as pd import numpy as np df = pd.DataFrame({'value': np.random.randn(100000)}) # 方式一:apply逐行处理 df['bucket_apply'] = df['value'].apply( lambda x: 'high' if x > 1 else ('mid' if x > -1 else 'low') ) # 方式二:np.select向量化处理 conditions = [ df['value'] > 1, df['value'] > -1 ] choices = ['high', 'mid'] df['bucket_vector'] = np.select(conditions, choices, default='low')实际跑下来,向量化版比apply快20倍以上。而且在算法项目里,数据量动辄百万行起步,这种差距会直接决定你能否在下班前跑完特征工程。
3.3 merge与groupby:数据科学的左膀右臂
做特征工程免不了把多个表拼在一起。Pandas的merge函数对应的是SQL里的各种join。有个细节我经常提醒新人:尽量确保合并键是索引或者排序好的,合并前先sort_values,能显著提高merge速度。原因在于Pandas的merge需要先对连接键排序,如果已经是排好序的,可以走快速路径。
groupby则是"分组-聚合-扩展"三步曲。算法里常见操作是:按用户分组算统计特征,再把结果拼回原表。这个操作的写法有讲究:
# 计算每个用户的消费均值,并映射回原表的每一行 user_mean = df.groupby('user_id')['amount'].transform('mean') df['user_mean_amount'] = user_meantransform和agg的区别在于:agg返回的是每个组一个值的汇总表,transform返回的是与原表等长的序列,可以直接赋值给新列。这个特性在做特征扩展时非常好用,不用先merge再对齐索引。
4. 建模与训练:从Scikit-learn到PyTorch的选型与工程化
4.1 Scikit-learn的Pipeline机制
Scikit-learn是传统机器学习最成熟的库,接口统一、文档清晰,但很多人用的时候只调单个模型,忽略了它最精华的部分——Pipeline。
Pipeline把数据预处理、特征变换和模型训练打包成一个整体,这样你在交叉验证时不会因为数据处理步骤里包含了目标值信息而出现数据泄露。更直观的价值是:网格搜索时不需要同时调一堆步骤的参数名。
一个标准流程的示例:
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV pipe = Pipeline([ ('scaler', StandardScaler()), ('pca', PCA(n_components=0.95)), ('clf', RandomForestClassifier(n_estimators=200, random_state=42)) ]) param_grid = { 'pca__n_components': [10, 20, 0.95], # PCA的n_components参数 'clf__max_depth': [5, 10, None] } grid = GridSearchCV(pipe, param_grid, cv=5, scoring='accuracy', n_jobs=-1) grid.fit(X_train, y_train)注意参数名里的pca__和clf__前缀,这是Pipeline步骤名和参数名的拼接规则。用这种方式,整个建模流程可复现、可追溯,也方便后续切换不同的预处理策略。这是工程化落地的重要一环。
4.2 什么时候该放弃Scikit-learn上PyTorch
Scikit-learn什么都好,但天生不擅长两件事:一是超大规模数据(比如上千万样本的神经网络训练),二是非结构化数据(图像、文本、序列)。
PyTorch的优势在于动态计算图和灵活的张量操作。它让你可以随时打印中间结果、断点调试,对研究型工作极其友好。写PyTorch代码时,核心是构建Dataset和DataLoader,它们负责数据的加载和批量喂给模型。
一个常见的错误是:为了省事把所有训练数据一次性to(device)搬到GPU显存,结果OOM。正确做法是让DataLoader按批次加载。下面这个写法是标准的:
from torch.utils.data import Dataset, DataLoader class MyDataset(Dataset): def __init__(self, X, y): self.X = torch.tensor(X, dtype=torch.float32) self.y = torch.tensor(y, dtype=torch.long) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] dataset = MyDataset(X_train, y_train) loader = DataLoader(dataset, batch_size=256, shuffle=True, num_workers=4)num_workers=4表示用4个子进程并行加载数据,这是避免GPU空转等待数据的关键配置。训练时模型训练和数据处理可以重叠进行,GPU利用率能明显提升。不过num_workers不是越大越好,Windows下子进程数量过多可能导致内存暴涨甚至报错,4到8是比较稳妥的范围。
4.3 模型持久化的正确姿势
训练完模型要保存,Scikit-learn官方推荐用joblib.dump而不是pickle.dump,因为joblib对大数组的序列化效率更高。但对PyTorch模型,只保存状态字典就行,不用整个模型对象序列化:
import torch # 保存 torch.save(model.state_dict(), 'model_weights.pth') # 加载 model = MyModel() model.load_state_dict(torch.load('model_weights.pth')) model.eval()这里面有个细节:加载权重前必须先实例化模型类,让模型结构和权重匹配。另外,eval()模式一定要调用,否则模型里的Dropout和BatchNorm在推理时会用训练模式的行为,结果就会出现"训练挺好,上线就拉胯"的诡异问题。
5. 性能突围:多进程、协程与Numba加速
算法工程师写代码跟后端工程师有个很大的思维差异:后端关心高并发下的响应能力,算法工程师关心的是单机计算资源的极致利用。前者用协程、消息队列,后者则更依赖多进程和向量化。
5.1 GIL锁决定了Python多线程的局限性
Python解释器有一个全局解释器锁(GIL),它保证同一时刻只有一个线程执行Python字节码。也就是说,CPU密集型任务用多线程不仅不会加速,反而会因为频繁切换上下文而变慢。这是很多人刚接触threading模块后百思不得其解的坑。
正确的处理方式分两种:
- CPU密集型(矩阵运算、特征计算):用
multiprocessing多进程,让每个进程拥有独立的解释器和GIL,从而利用多核CPU。 - IO密集型(爬数据、读文件、调API):用
asyncio协程,单线程内通过事件循环处理并发IO,本质上不费额外资源。
5.2 multiprocessing的实用姿势
用multiprocessing.Pool做并行最常用,它能自动把任务分发给多个进程,收集结果并保持顺序。一个典型场景是:对大量参数组合做独立的交叉验证评估。
from multiprocessing import Pool import numpy as np def evaluate(params): lr, reg = params # 这里做训练和评估,返回分数 score = np.random.rand() # 举例 return (lr, reg, score) param_combos = [(0.01, 0.1), (0.01, 1.0), (0.1, 0.1), (0.1, 1.0)] with Pool(processes=4) as pool: results = pool.map(evaluate, param_combos) for lr, reg, score in results: print(f"lr={lr}, reg={reg}, score={score:.4f}")注意一点:被Pool.map调用的函数必须能被pickle序列化。如果是写在if __name__ == '__main__':内部的局部函数,Windows下会报PicklingError。所以要么把函数定义在模块顶层,要么用functools.partial传参数,总之不能是闭包函数。
5.3 asyncio的正确打开方式
协程解决的是高并发IO场景的等待问题。在算法项目里,典型应用是批量下载数据集或者并发请求多个API。跟多进程比,协程的创建成本极低,你可以轻松开几千个并发任务。
import asyncio import aiohttp async def fetch(session, url): async with session.get(url) as resp: return await resp.text() async def main(): async with aiohttp.ClientSession() as session: tasks = [fetch(session, f"https://example.com/data/{i}") for i in range(1000)] pages = await asyncio.gather(*tasks) print(f"fetched {len(pages)} pages") asyncio.run(main())这段代码在单线程内发起1000个并发请求。注意await关键字的位置,它让出控制权给事件循环。如果你在协程里用了阻塞的同步IO(比如requests.get),那整个事件循环都会被卡住,协程就白写了。
5.4 Numba:给纯Python数值计算开外挂
很多算法库的梯度计算、朴素贝叶斯、K近邻,其实核心循环都可以用Numba加速。Numba是一个JIT编译器,它能把Python函数在运行时编译成机器码,加速效果从几倍到几十倍不等。
一个经典的加速对比:
from numba import jit import numpy as np import time def loop_sum(n): s = 0 for i in range(n): s += i * i return s @jit(nopython=True) def numba_sum(n): s = 0 for i in range(n): s += i * i return s n = 100_000_000 start = time.time() loop_sum(n) print(f"pure python: {time.time() - start:.2f}s") start = time.time() numba_sum(n) print(f"numba: {time.time() - start:.2f}s")我第一次跑这个对比时,纯Python用了大概15秒,Numba只用了不到0.1秒。这个差距在特征工程的自定义函数上尤为明显。用Numba的前提是你的函数内部用的是NumPy原生类型,且nopython=True(这意味着Numba不会偷偷把操作退回Python解释器)。如果遇到不支持的函数,编译器会明确报错,你只需要把不支持的逻辑拆出去或改写即可。
6. 完整案例:一个量化策略回测里的库组合打法
聊了这么多理论,不如看一个完整的场景。量化交易策略回测是算法工程师面试题里的常客,也是检验你Python高级库功底的试金石。核心工作流是:获取历史行情数据、计算交易信号、模拟撮合、输出绩效指标。
6.1 数据准备与信号计算
先用一个小例子演示整个链路。假设你手里有一个日线级别的DataFrame,包含date、close两列,要计算一个简单的双均线策略信号——短期均线上穿长期均线时买入,反之卖出。
import pandas as pd import numpy as np # 模拟1000个交易日的收盘价 np.random.seed(42) dates = pd.date_range('2022-01-01', periods=1000, freq='D') close = pd.Series(np.random.randn(1000).cumsum() + 100, index=dates) df = pd.DataFrame({'close': close}) df['ma_short'] = df['close'].rolling(window=20).mean() df['ma_long'] = df['close'].rolling(window=60).mean() # 信号:短均线 > 长均线时为1,反之为0 df['signal'] = (df['ma_short'] > df['ma_long']).astype(int) # 交易:signal从0变1说明当天买入,从1变0说明卖出 df['position'] = df['signal'].diff().fillna(0)rolling是Pandas做时间序列窗口计算的利器。双均线的逻辑不需要循环,直接用rolling(window=N).mean()就完成了。这就是第3节说的"向量化思维"在时间序列里的体现。
6.2 回测撮合与绩效输出
有了信号后,计算策略每日收益。核心是用shift(1)把当天的信号和次日的收益对齐,避免未来数据泄露。
df['daily_ret'] = df['close'].pct_change() df['strategy_ret'] = df['signal'].shift(1) * df['daily_ret'] df['cum_ret'] = (1 + df['strategy_ret']).cumprod() # 简单绩效指标 total_return = df['cum_ret'].iloc[-1] - 1 annual_vol = df['strategy_ret'].std() * np.sqrt(252) sharpe = df['strategy_ret'].mean() / df['strategy_ret'].std() * np.sqrt(252) max_drawdown = (df['cum_ret'] / df['cum_ret'].cummax() - 1).min() print(f"累计收益: {total_return:.2%}") print(f"年化波动率: {annual_vol:.2%}") print(f"夏普比率: {sharpe:.2f}") print(f"最大回撤: {max_drawdown:.2%}")这个回测框架虽然简陋,但反映了一个关键思想:回测本质上是向量化运算的组合。所有数据都是列式对齐的,信号计算、收益计算、统计指标全都不用循环。当你把这个框架应用到几千只股票上时,你会发现全靠向量化和Pandas的分组聚合才撑得起这样的计算量。
6.3 性能瓶颈出现在哪里
如果股票数量很多(比如沪深300),回测的热点往往在分组计算信号和逐日撮合上。对300只股票做滚动均线,简单用groupby加rolling可能会很慢,因为对每只股票都要单独排序和滚动。这时候有两个优化方向:一是用Polars替代Pandas,它在groupby和rolling上的实现是数据并行化的,速度快很多;二是用Numba把核心信号计算逻辑编译成机器码。
给一个Polars的简单对比示例:
import polars as pl # 用Polars读CSV,groupby rolling计算均线 df_pl = pl.read_csv('prices.csv', try_parse_dates=True) df_pl = df_pl.sort(['symbol', 'date']) df_pl = df_pl.with_columns([ pl.col('close').rolling_mean(window_size=20).over('symbol').alias('ma_short'), pl.col('close').rolling_mean(window_size=60).over('symbol').alias('ma_long') ])在百万行量级的数据上,Polars的处理时间通常只有Pandas的几分之一。当然,Polars的API和Pandas有一定差异,切换需要一点学习成本。我的建议是:如果项目还在探索阶段,用Pandas先把逻辑跑通;如果确定了要长期运行且数据量很大,再考虑切换到Polars。不要一上来就在两种API之间反复横跳,那样只会浪费时间。
7. 避坑笔记:我踩过且你大概率也会踩的坑
7.1 版本锁定的重要性
算法项目最怕的是"昨天还能跑,今天就报错"。大多数情况是因为某个依赖库被自动升级了。解决办法很简单:项目的根目录一定要放一份requirements.txt,并且锁定所有核心依赖的版本号。比如:
numpy==1.24.3 pandas==2.0.3 scikit-learn==1.3.0 torch==2.0.1锁定版本不是让你永不升级,而是保证可复现性。每次环境更新前,先查看变更日志,确认兼容性后再统一升级。
还有一个跟版本相关的坑:requirements.txt最好使用pip freeze的输出,但pip freeze会列出所有间接依赖,比较啰嗦。实际操作中我一般用手工维护核心依赖,加注释说明版本选择的原因。这样别人拿到项目时,即使遇到问题也能大概判断是哪里的兼容性问题。
7.2 Windows下与Linux下的行为差异
同样的代码,在Windows和Linux上跑经常有细微差异。最典型的是:
multiprocessing的进程启动方式不同。Windows默认是spawn,会重新导入主模块,所以主模块里的if __name__ == '__main__':保护必须写,否则子进程会无限递归启动新进程。- 路径分隔符不一样。用
os.path.join或者pathlib.Path处理路径,不要硬编码/或\。 - PyTorch在Windows下的
DataLoader的num_workers如果设置过大,可能出现"DataLoader worker (pid(s) xxxx) exited unexpectedly"的报错。解决办法是逐步减小num_workers,必要时设为0。
7.3 数据泄露:算法工程师最容易犯的职业病
最后说一个模型层面的坑,因为太典型了。很多人在做特征工程时,不小心把目标变量的信息混进了特征里,导致训练时指标漂亮得吓人,一上线就崩。
举个例子:做用户流失预测,历史数据里有churn_label列,你要构造特征时顺手用df.groupby('user_id')['churn_label'].transform('mean')拼了一个"历史流失率"特征。这在逻辑上就是作弊——因为你用到了未来的信息。正确的做法是只允许使用时间点之前的信息构造特征,这也是为什么时间序列类项目里"先分组再排序"的严格时间窗口十分重要。
判断是否泄露有一个很朴素的检验方法:单拿出这个特征,看它和目标的相关性是否高到不合理。如果一个特征独自就能达到0.9的AUC,大概率就是泄露出问题了。碰到这种情况,先检查特征构造时用到的列里有没有包含目标列或者目标的衍生列。
7.4 写Numba加速时的两个常见报错
用Numba的人几乎必遇两个报错。一个是"Untyped global name 'xxx'",意思是你用了Numba不认识的全局变量或函数。解决办法是把这些外部变量作为参数传给被装饰的函数,而不是直接在函数体内引用。另一个是"Cannot unify array(float64, 1d) and array(float64, 2d)",这是类型推断问题,说明你在同一个变量上既赋了一维数组又赋了二维数组。Numba对变量的类型有严格一致性要求,代码里需要避免这种复用变量的写法。
遇到Numba报错不要慌,先看错误信息定位是哪一行,再把那一段逻辑拆出来用普通Python跑通,确认结果正确后再用Numba包装。nopython=True虽然限制多,但它能保证你的代码不会静默变慢,报错反而是在帮你的忙。
8. 最后分享一点环境管理的个人习惯
把正文里拆开讲的内容串起来,我最后想聊一个很多人忽略的点:给每个项目配独立环境,这不是洁癖,是效率投资。我以前也偷懒,所有项目共用一个base环境,结果某次为了测试一个新库升级了Pandas,直接把另一个跑了一个月的项目搞挂了。从那以后我严格执行"一个项目一个conda环境"的策略,虽然创建环境时多花两分钟,但换来的确定性是无价的。
另外,如果你经常在不同机器间迁移项目,可以把环境导出成文件:
conda env export > environment.yaml换机器后直接用conda env create -f environment.yaml恢复环境。这样能把整个依赖树(包括conda的channel和原生库版本)完整复刻,比单独的requirements.txt更可靠。
当然,environment.yaml里如果包含本地路径相关的信息(比如某些源码安装的包),跨机器时可能需要手动调整。我的习惯是:用conda的yaml文件做环境备份,用requirements.txt做项目文档里的依赖说明,两者配合,既不臃肿也够完整。
做算法工程师这几年,我最大的体会是:真正的进阶不是背更多的API,而是理解每个库在什么场景下为什么好用,以及如何把它们组合起来解决问题。希望这篇文章能让你少走一些我走过的弯路。如果你在实操中遇到什么奇怪的报错或者坑,欢迎在评论区聊聊,说不定你踩的那个坑我也踩过。